Documents Work Mode shell — the option-J chrome
This is the frame counsel sees when they open a matter's Documents section: a serif “Documents” heading with a live subtitle, the saved-view folder-pill bar (with a searchable More overflow), and the right-aligned Filter · ⚙ View · Upload cluster. It is the chrome that wraps the Documents Register — built and verified Storybook-first, ahead of the register table, view-state engine, popovers, and upload sheet that its sibling tickets land into the same shell.
Mode 1 (deterministic) Presentational · callback-driven
What shipped
A new co-located component family under
src/components/matter-detail/documents/, each a leaf with a
co-located *.stories.tsx (the AC: story before live
wiring):
| Component | Role | Story |
|---|---|---|
DocumentViewPill |
One saved-view folder-pill — folder glyph, optional amber star, active crimson fill, unsaved dot. | Default / Active / Starred / Active+starred / Active+edited |
DocumentsViewPills |
The pill bar. Pins active + starred inline, overflows the rest into a searchable More popover, trails a New view affordance. | Default / Few (no overflow) / Many (searchable) / Edited |
DocumentsWorkModeActions |
Right-aligned Filter trigger · ⚙ View (unsaved dot) · Upload (brand-filled). | Default / View edited / With active filters |
DocumentsWorkModeHeader |
The composite shell: heading + live subtitle + pill bar + actions. | Default / Many views / Edited / First run (empty) |
Pure logic lives in presentation.ts
(splitSavedViews, filterSavedViews,
DocumentSavedViewVm) with unit tests in
presentation.test.ts — no presentation logic in client files.
Worknav vs. views — visually distinct (AC)
The matter Worknav (section tabs — Brief, Strategy, Documents…) and the saved-view bar are two different axes of navigation and must never read as the same control. They are deliberately rendered with different shapes:
- Worknav = underlined text tabs (the existing
MatterDetailHeaderrow 2). - Saved views = filled, rounded folder-pills with a folder glyph and crimson-tint active state.
The More overflow (AC: “overflow More menu works”)
At 30+ views the bar would blow out. splitSavedViews keeps
the active view and every starred view
inline, fills the bar to a cap (default 5), and spills the tail into a
Radix Popover: a search box + scroll list, filtered live by
filterSavedViews, with an explicit empty state when nothing
matches. Selecting a view fires onSelectView, closes the
popover, and resets the query. Keyboard-operable and focus-managed via
the shared primitive.
Scope boundary — what this ticket does NOT own
DOCS-CHROME is the shell. The triggers fire callbacks; the surfaces they open belong to sibling EPIC-1489 tickets that wire into this same chrome:
onOpenView→ ⚙ View popover (VIEWPOPOVER #1511)onOpenFilter→ filter popover (sibling Filter ticket)onUpload→ upload sheet (UPLOAD-SHEET #1518)onSelectView/onNewView→ view-state engine + saved-view persistence (VIEWENGINE #1510, SAVEDVIEWS-WIRE #1517)
The saved-view shape is the canonical generated DTO —
DocumentSavedViewVm = Pick<DocumentSavedViewListItem, …>
imported from @/gen — so the chrome can never drift from the
backend contract. Live-data seam: the live Documents
route renders this header with views from
useAppListDocumentSavedViews and a real
documentCount once SAVEDVIEWS-WIRE lands; until then the
shell is exercised through Storybook with fixture views.
Mode discipline: AI-filled fields (suggested name, AI document type/tags)
are EPIC-009 (Mode 2/3) — out of this chrome entirely. Tenancy: saved
views carry firmId/matterId on the generated DTO
(firm-scoped + RLS on the backend). No new executable domain operation is
introduced here, so no manual span is added (ADR-124 — presentational UI).
i18n & tokens
- Every string resolves through
useTranslations("MatterDetail.documents"); keys added to bothmessages/en.jsonandmessages/hi.json. The subtitle uses an ICU plural so “No files / 1 file / N files” is translatable. - No color literals — active pill is
bg-brand-primary-tint+border-brand-primary-tint-border+text-brand-primary; the star is--amber-9; the unsaved dot isbg-brand-primary. - Buttons go through the
PrimaryButton/SecondaryButtonprimitives; the Filter trigger mirrors the matter header's search-trigger idiom. - Empty / zero states handled: empty saved-view list, no-overflow, and the no-results-after-search case in More.
Verification
pnpm typecheck— clean.pnpm lint— clean on the new directory.pnpm test— 529 passed (7 new inpresentation.test.tscovering the overflow split + search).pnpm build— succeeds.- Components verified visually in Storybook (
MatterDetail/Documents/*), framed at real surrounding width with a worknav stand-in.
Files changed
13 file(s), +805 / -0 lines
| Path | Role | + | - |
|---|---|---|---|
| messages/en.json | Config | +16 | -0 |
| messages/hi.json | Config | +16 | -0 |
| src/components/matter-detail/documents/document-view-pill.stories.tsx | Component | +49 | -0 |
| src/components/matter-detail/documents/document-view-pill.tsx | Component | +74 | -0 |
| src/components/matter-detail/documents/documents-story-fixtures.ts | Component | +21 | -0 |
| src/components/matter-detail/documents/documents-view-pills.stories.tsx | Component | +70 | -0 |
| src/components/matter-detail/documents/documents-view-pills.tsx | Component | +136 | -0 |
| src/components/matter-detail/documents/documents-work-mode-actions.stories.tsx | Component | +43 | -0 |
| src/components/matter-detail/documents/documents-work-mode-actions.tsx | Component | +79 | -0 |
| src/components/matter-detail/documents/documents-work-mode-header.stories.tsx | Component | +86 | -0 |
| src/components/matter-detail/documents/documents-work-mode-header.tsx | Component | +88 | -0 |
| src/components/matter-detail/documents/presentation.test.ts | Component | +68 | -0 |
| src/components/matter-detail/documents/presentation.ts | Component | +59 | -0 |