From 423d41989323c1b1358d68e6e58d3f3834390159 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Thu, 16 Apr 2026 09:01:39 +0200 Subject: [PATCH] docs: add exact clients tablo parity design --- ...04-16-clients-exact-tablo-parity-design.md | 249 ++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-16-clients-exact-tablo-parity-design.md diff --git a/docs/superpowers/specs/2026-04-16-clients-exact-tablo-parity-design.md b/docs/superpowers/specs/2026-04-16-clients-exact-tablo-parity-design.md new file mode 100644 index 0000000..3b43b5a --- /dev/null +++ b/docs/superpowers/specs/2026-04-16-clients-exact-tablo-parity-design.md @@ -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