docs: add exact clients tablo parity design

This commit is contained in:
Arthur Belleville 2026-04-16 09:01:39 +02:00
parent b1c1c595ab
commit 423d419893
No known key found for this signature in database

View file

@ -0,0 +1,249 @@
# Clients Exact Tablo Parity
**Date**: 2026-04-16
**Status**: Draft
**Supersedes**: `docs/superpowers/specs/2026-04-15-client-portal-tablo-parity-design.md`
## Overview
`clients.xtablo.com` is intended to be a portal for the clients of our clients. For the single-tablo experience, it must render the exact same UI surface as `app.xtablo.com` on `apps/main/src/pages/tablo-details.tsx`.
The current codebase does not guarantee that outcome because `apps/main` and `apps/clients` still compose the page separately. Shared sections and some shared CSS exist, but the full single-tablo route is not owned by one shared render surface.
The target is stricter than "close parity":
- same page shell
- same header structure
- same metadata row
- same tab bar
- same overview layout
- same section framing
- same responsive behavior
- same route-level CSS source
- same component tree for the single-tablo view
The only intended differences are:
- `apps/main` keeps URL-backed section state
- `apps/clients` keeps in-memory section state
- `apps/main` exposes full admin and mutation capabilities
- `apps/clients` exposes a restricted client-safe capability set
## Problem Statement
`clients.xtablo.com` is not at feature parity today for structural reasons, not because of one isolated CSS bug.
### Current causes of drift
1. `apps/clients/src/pages/ClientTabloPage.tsx` reconstructs a client-specific page instead of rendering the same single-tablo surface as `apps/main`.
2. `apps/clients/src/components/ClientLayout.tsx` owns a separate app shell, so spacing, header behavior, and responsive layout can drift from the main app.
3. Shared CSS exists only partially. Route-level tokens and chat/page styling have been extracted in places, but the full single-tablo view is still not governed by one shared route stylesheet plus one shared render tree.
4. Permissions are mixed into page composition instead of being expressed as a clean capability model. That forces `clients` to fork render logic instead of rendering the same surface with different behavior gates.
The result is predictable: any visual or structural change to the main tablo route risks being manually reimplemented in `clients`, and parity becomes a maintenance task instead of an invariant.
## Goals
- Make the single-tablo UI in `clients.xtablo.com` visually identical to `apps/main/src/pages/tablo-details.tsx`
- Enforce parity through one shared single-tablo render surface
- Share the route-level CSS and responsive behavior between both apps
- Keep `apps/clients` on in-memory tab state
- Keep `discussion` writable in client mode
- Keep the rest of the client experience read-only or admin-hidden as approved
## Non-Goals
- Rebuilding the client portal as a separate visual concept
- Introducing query-param section routing into `apps/clients`
- Enabling client-side admin actions
- Refactoring unrelated routes outside the single-tablo experience
- Deleting the newer client-specific invite system from the codebase
## Hard Requirement
The UI must be the exact same.
That rules out maintaining two parallel page compositions for the single-tablo route. "Shared components plus duplicated page assembly" is not sufficient because parity will drift again. The single-tablo view must be rendered from one shared composition surface consumed by both apps.
## Chosen Approach
Create a shared single-tablo route surface in `packages/tablo-views` and make both apps consume it.
This shared surface owns the exact structure, responsive layout, CSS import, tab order, overview composition, and section framing for the route.
Each app becomes a thin adapter:
- `apps/main` passes full-capability handlers and URL-backed section state
- `apps/clients` passes restricted capabilities and in-memory section state
This is a consolidation, not a styling pass.
## Architecture
### Shared package ownership
`packages/tablo-views` should own the single-tablo route surface for both apps, including:
- header layout
- title and icon/image block
- metadata row
- sticky tab navigation
- overview block composition
- section container layout
- discussion full-height layout behavior
- route-specific CSS for this surface
This shared surface should render the same DOM structure and use the same styling hooks regardless of app.
### App adapter ownership
`apps/main` should own:
- `?section=` query-param state
- full mutation handlers
- admin-only actions
- share and invite workflows
- main-only routing integrations
`apps/clients` should own:
- local in-memory tab state
- client-safe data loading
- capability restrictions
- client-safe app shell concerns outside the single-tablo surface
### Key principle
The apps must differ by inputs, not by page composition.
## Capability Model
The shared single-tablo surface should branch on capabilities rather than on app identity.
Suggested capability contract:
- `canCreateTasks`
- `canEditTasks`
- `canEditEvents`
- `canManageFiles`
- `canManageMembers`
- `canInviteMembers`
- `canEditLayout`
- `canWriteDiscussion`
### Approved client boundary
For `clients.xtablo.com`:
- `discussion`: writable
- file read/download behavior: allowed where already supported
- task edits: disabled
- event edits: disabled
- layout edits: hidden
- member management: hidden or read-only
- invite/share management: hidden
This preserves the same page structure while changing behavior safely.
## CSS And Responsiveness
Exact UI parity requires a single route-level CSS source for the single-tablo experience.
### Requirements
- the shared single-tablo surface imports one shared route stylesheet from `packages/tablo-views`
- route-level tokens for navbar, metadata, sticky tabs, and discussion/chat visuals come from that shared stylesheet
- responsive breakpoints and overflow behavior are not redefined independently in `apps/main` and `apps/clients`
### Constraint
`apps/clients` must not carry a competing route-specific single-tablo style layer that can override the shared surface in divergent ways.
App-local CSS can remain for app-wide concerns, but the single-tablo route must have one styling owner.
## State Model
The visual surface is shared, but tab state differs by app.
### Main app
- active section comes from `?section=...`
- existing route behavior remains intact
### Clients app
- active section is stored in local React state
- no query-param synchronization is required
This is acceptable because the user explicitly approved in-memory state for clients.
## Migration Plan
### Phase 1: Consolidate the surface
- identify all remaining structure in `apps/main/src/pages/tablo-details.tsx` that is still page-owned instead of shared
- move that structure into a shared single-tablo route surface in `packages/tablo-views`
- make `apps/main` consume the shared surface first, preserving current behavior
### Phase 2: Convert the clients app into an adapter
- remove duplicated page composition from `apps/clients/src/pages/ClientTabloPage.tsx`
- replace it with a thin adapter that passes client-safe data and capability flags into the shared surface
- keep local in-memory tab state in the adapter only
### Phase 3: Normalize the shell
- ensure the surrounding shell and route-level CSS used by the single-tablo surface are shared consistently
- keep `ClientLayout` responsible only for client-portal app concerns, not for redefining the single-tablo view
### Phase 4: Lock parity with tests
- add tests that assert `main` and `clients` render the same shared single-tablo structure
- add client-mode tests for capability restrictions
- add responsive regression coverage where practical
## Testing Strategy
Verification must prove parity, not just correctness.
### Automated
- shared render tests for the single-tablo route surface in `packages/tablo-views`
- adapter tests proving `apps/main` and `apps/clients` pass different state/capabilities into the same surface
- regression tests that the client app hides or disables admin-only actions while keeping discussion writable
- targeted CSS contract tests ensuring both apps import the same shared route stylesheet
### Manual
Side-by-side comparison between:
- `apps/main` `/tablos/:tabloId`
- `apps/clients` `/tablo/:tabloId`
At minimum verify:
- desktop layout
- mobile layout
- sticky tabs
- overview cards
- discussion layout
- header wrapping and spacing
- empty states
## Risks
- `apps/main/src/pages/tablo-details.tsx` may still contain too much mixed business logic and render logic, making extraction noisy
- some shared sections may still assume main-app permissions implicitly
- partial CSS ownership may continue to cause drift if not fully normalized
- client-safe data access may reveal places where the UI currently assumes admin or member-level data is always present
## Success Criteria
This work is successful when:
- `clients.xtablo.com` renders the same single-tablo UI as `app.xtablo.com`
- future UI changes to the single-tablo route normally require edits in one shared place
- `apps/clients` remains in-memory for tab state
- client restrictions match the approved boundary
- discussion remains writable in client mode
- parity is enforced structurally, not maintained manually