# 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
