docs(04): UI design contract

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-05-25 14:30:10 +02:00
parent ff379ad6e3
commit 752cf987aa
@@ -1,10 +1,12 @@
--- ---
phase: 4 phase: 4
slug: folders-sharing-quotas-document-ux slug: folders-sharing-quotas-document-ux
status: draft status: approved
shadcn_initialized: false shadcn_initialized: false
preset: none preset: none
created: 2026-05-25 created: 2026-05-25
revised: 2026-05-25
reviewed_at: 2026-05-25
--- ---
# Phase 4 — UI Design Contract # Phase 4 — UI Design Contract
@@ -36,7 +38,7 @@ Declared values (multiples of 4 only). Source: existing component scan — `p-4`
| Token | Value | Usage | | Token | Value | Usage |
|-------|-------|-------| |-------|-------|-------|
| xs | 4px | Icon gaps, topic badge gaps (`gap-1`) | | xs | 4px | Icon gaps, topic badge gaps (`gap-1`) |
| sm | 8px | Compact element spacing, inline button padding (`py-1.5`) | | sm | 8px | Compact element spacing, inline button padding (`py-2`) |
| md | 16px | Default element spacing, card padding (`p-4`) | | md | 16px | Default element spacing, card padding (`p-4`) |
| lg | 24px | Section padding (`px-6 py-5`), page section breaks | | lg | 24px | Section padding (`px-6 py-5`), page section breaks |
| xl | 32px | Page content padding (`p-8`) | | xl | 32px | Page content padding (`p-8`) |
@@ -47,7 +49,7 @@ Exceptions:
- Touch targets (icon-only buttons): minimum 44px height enforced via `min-h-[44px]` (established in ConfirmBlock.vue — apply to all icon-only action buttons in this phase). - Touch targets (icon-only buttons): minimum 44px height enforced via `min-h-[44px]` (established in ConfirmBlock.vue — apply to all icon-only action buttons in this phase).
- Breadcrumb segment gap: 8px (`gap-2`) with a separator chevron (SVG, w-3 h-3). - Breadcrumb segment gap: 8px (`gap-2`) with a separator chevron (SVG, w-3 h-3).
- Modal overlay: full-viewport fixed overlay `inset-0`, modal panel `max-w-md w-full` centered with `p-6` internal padding. - Modal overlay: full-viewport fixed overlay `inset-0`, modal panel `max-w-md w-full` centered with `p-6` internal padding.
- Breadcrumb truncation ellipsis button: `px-1 py-0.5` compact. - Breadcrumb truncation ellipsis button: `px-2 py-1` compact (4px-grid minimum).
--- ---
@@ -62,7 +64,17 @@ Extend existing scale — do not introduce new sizes. Source: DocumentCard.vue,
| Body / card label | 14px (text-sm) | 500 (font-medium) | 1.5 | `text-sm font-medium text-gray-900` | | Body / card label | 14px (text-sm) | 500 (font-medium) | 1.5 | `text-sm font-medium text-gray-900` |
| Caption / metadata | 12px (text-xs) | 400 (normal) | 1.4 | `text-xs text-gray-400` | | Caption / metadata | 12px (text-xs) | 400 (normal) | 1.4 | `text-xs text-gray-400` |
Weights in use: 400 (normal), 500 (medium), 600 (semibold), 700 (bold). No new weights added. ### Typography Exceptions — Brownfield Baseline
The existing Phase 2/3 components (AppSidebar.vue, DocumentCard.vue, DocumentView.vue) use a 4-weight scale: 400 (normal), 500 (medium), 600 (semibold), 700 (bold). Collapsing this to 2 weights would require a mass refactor of all pre-existing components and is out of scope for Phase 4.
**Brownfield exception:** The 4-weight scale is an inherited baseline. It is not a new design decision introduced in Phase 4.
**Net-new Phase 4 typography tokens** are restricted to exactly 2 weights:
- `font-medium` (500) — new UI labels, nav entries, inline text in new components
- `font-semibold` (600) — new section headings, modal titles, emphasis labels in new components
`font-bold` (700) is already used exclusively for the logo text in AppSidebar.vue. Phase 4 introduces no new elements using `font-bold`.
Monospace (extracted text panel): `text-xs font-mono` at `text-gray-600` on `bg-gray-50` — established in DocumentView.vue, reuse for audit log detail cells. Monospace (extracted text panel): `text-xs font-mono` at `text-gray-600` on `bg-gray-50` — established in DocumentView.vue, reuse for audit log detail cells.
@@ -96,7 +108,7 @@ Source: AppSidebar.vue, QuotaBar.vue, DocumentCard.vue, DocumentView.vue. No new
Accent is NOT used on: hover states of neutral UI, sort controls, breadcrumb separators, audit log rows, sidebar section labels, permission badges (use gray), or any informational text. Accent is NOT used on: hover states of neutral UI, sort controls, breadcrumb separators, audit log rows, sidebar section labels, permission badges (use gray), or any informational text.
**Shared indicator badge** (SHARE-05): small `bg-indigo-50 text-indigo-600 text-xs font-medium px-2 py-0.5 rounded-full` pill labeled "Shared" — shown inline in DocumentCard below the metadata line when `doc.share_count > 0`. **Shared indicator badge** (SHARE-05): small `bg-indigo-50 text-indigo-600 text-xs font-medium px-2 py-1 rounded-full` pill labeled "Shared" — shown inline in DocumentCard below the metadata line when `doc.share_count > 0`.
**"Shared with me" virtual folder entry** (D-06): rendered with a distinct inbox-style icon (`bg-purple-50 text-purple-500`) to visually separate it from user-owned folders. Purple is used ONLY for this one sidebar entry to signal "received from others" vs. "my own". No other UI element uses purple. **"Shared with me" virtual folder entry** (D-06): rendered with a distinct inbox-style icon (`bg-purple-50 text-purple-500`) to visually separate it from user-owned folders. Purple is used ONLY for this one sidebar entry to signal "received from others" vs. "my own". No other UI element uses purple.
@@ -108,6 +120,8 @@ Folder row icons in main content: `bg-gray-100 text-gray-500` (neutral — folde
New components required for Phase 4. All follow existing utility-class patterns. New components required for Phase 4. All follow existing utility-class patterns.
Primary focal point: the document/folder list. Secondary: the breadcrumb navigation and search bar. Tertiary: sort controls.
### FolderRow.vue ### FolderRow.vue
- Used in HomeView main content area to render sub-folders within a folder. - Used in HomeView main content area to render sub-folders within a folder.
- Layout: `flex items-center gap-3 px-4 py-3 bg-white border border-gray-200 rounded-xl hover:border-indigo-300 hover:shadow-sm transition-all cursor-pointer` - Layout: `flex items-center gap-3 px-4 py-3 bg-white border border-gray-200 rounded-xl hover:border-indigo-300 hover:shadow-sm transition-all cursor-pointer`
@@ -120,7 +134,7 @@ New components required for Phase 4. All follow existing utility-class patterns.
- Segments: `flex items-center gap-2 text-sm`. - Segments: `flex items-center gap-2 text-sm`.
- Each clickable segment: `text-indigo-600 hover:underline font-medium`. - Each clickable segment: `text-indigo-600 hover:underline font-medium`.
- Separator: SVG chevron-right `w-3 h-3 text-gray-400 shrink-0`. - Separator: SVG chevron-right `w-3 h-3 text-gray-400 shrink-0`.
- Truncation rule (D-02): when depth > 4, show first segment + `…` button (plain text, `px-1 py-0.5 text-gray-400 hover:text-gray-600`) + last 2 segments. - Truncation rule (D-02): when depth > 4, show first segment + `…` button (plain text, `px-2 py-1 text-gray-400 hover:text-gray-600`) + last 2 segments.
- Current (non-clickable) final segment: `text-gray-900 font-medium`. - Current (non-clickable) final segment: `text-gray-900 font-medium`.
### ShareModal.vue ### ShareModal.vue
@@ -128,11 +142,11 @@ New components required for Phase 4. All follow existing utility-class patterns.
- Modal overlay: `fixed inset-0 bg-black/40 flex items-center justify-center z-50`. - Modal overlay: `fixed inset-0 bg-black/40 flex items-center justify-center z-50`.
- Panel: `bg-white rounded-2xl shadow-xl p-6 max-w-md w-full mx-4`. - Panel: `bg-white rounded-2xl shadow-xl p-6 max-w-md w-full mx-4`.
- Title: `text-lg font-semibold text-gray-900 mb-4` — "Share document". - Title: `text-lg font-semibold text-gray-900 mb-4` — "Share document".
- Handle input row: `flex gap-2``<input type="text" placeholder="Enter username handle" class="flex-1 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500">` + primary button "Share" `bg-indigo-600 hover:bg-indigo-700 text-white text-sm px-4 py-2 rounded-lg`. - Handle input row: `flex gap-2``<input type="text" placeholder="Enter username handle" class="flex-1 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500">` + primary button "Share document" `bg-indigo-600 hover:bg-indigo-700 text-white text-sm px-4 py-2 rounded-lg`.
- Separator: `border-t border-gray-100 my-4`. - Separator: `border-t border-gray-100 my-4`.
- Recipients list: each row `flex items-center justify-between py-2`. - Recipients list: each row `flex items-center justify-between py-2`.
- Left: handle `text-sm text-gray-900` + permission badge `text-xs bg-gray-100 text-gray-600 px-2 py-0.5 rounded-full font-medium ml-2` (shows "view"). - Left: handle `text-sm text-gray-900` + permission badge `text-xs bg-gray-100 text-gray-600 px-2 py-1 rounded-full font-medium ml-2` (shows "view").
- Right: "Revoke" `text-xs text-red-500 hover:text-red-700 font-medium` button. - Right: "Remove access" `text-xs text-red-500 hover:text-red-700 font-medium` button.
- Empty state: `text-sm text-gray-400 italic py-2` — "Not shared with anyone yet." - Empty state: `text-sm text-gray-400 italic py-2` — "Not shared with anyone yet."
- Close button: top-right `absolute top-4 right-4`, `text-gray-400 hover:text-gray-600`, SVG X icon w-5 h-5. - Close button: top-right `absolute top-4 right-4`, `text-gray-400 hover:text-gray-600`, SVG X icon w-5 h-5.
@@ -142,7 +156,7 @@ New components required for Phase 4. All follow existing utility-class patterns.
- Warning icon: `w-10 h-10 bg-red-50 rounded-full flex items-center justify-center` with `text-red-500` exclamation SVG, centered at top of modal. - Warning icon: `w-10 h-10 bg-red-50 rounded-full flex items-center justify-center` with `text-red-500` exclamation SVG, centered at top of modal.
- Heading: `text-lg font-semibold text-gray-900 mt-4 text-center` — "Delete folder?" - Heading: `text-lg font-semibold text-gray-900 mt-4 text-center` — "Delete folder?"
- Body: `text-sm text-gray-600 text-center mt-2 mb-6` — "This folder contains {N} documents. Deleting it will permanently delete all documents inside. This cannot be undone." - Body: `text-sm text-gray-600 text-center mt-2 mb-6` — "This folder contains {N} documents. Deleting it will permanently delete all documents inside. This cannot be undone."
- Buttons: `flex gap-3 justify-end`. Cancel: `text-sm text-gray-600 hover:text-gray-800 px-4 py-2`. Confirm: `bg-red-600 hover:bg-red-700 text-white text-sm font-semibold px-4 py-2 rounded-lg min-h-[44px]` — "Delete folder and documents". - Buttons: `flex gap-3 justify-end`. Dismiss: `text-sm text-gray-600 hover:text-gray-800 px-4 py-2` — "Keep folder". Confirm: `bg-red-600 hover:bg-red-700 text-white text-sm font-semibold px-4 py-2 rounded-lg min-h-[44px]` — "Delete folder and documents".
### DocumentPreviewModal.vue (in-app mode only) ### DocumentPreviewModal.vue (in-app mode only)
- Used when `user.pdf_open_mode === 'in_app'` (D-10). - Used when `user.pdf_open_mode === 'in_app'` (D-10).
@@ -177,7 +191,7 @@ New components required for Phase 4. All follow existing utility-class patterns.
- Columns: Timestamp | User | Action Type | IP Address. - Columns: Timestamp | User | Action Type | IP Address.
- Body row: `border-b border-gray-100 hover:bg-gray-50`. Cells: `px-4 py-3 text-sm text-gray-700`. - Body row: `border-b border-gray-100 hover:bg-gray-50`. Cells: `px-4 py-3 text-sm text-gray-700`.
- Timestamp: monospace `font-mono text-xs text-gray-500`. - Timestamp: monospace `font-mono text-xs text-gray-500`.
- Action type: pill badge `text-xs px-2 py-0.5 rounded-full font-medium` — color-coded by category: auth=`bg-blue-50 text-blue-600`, document=`bg-gray-100 text-gray-600`, folder/share=`bg-purple-50 text-purple-600`, admin=`bg-amber-50 text-amber-700`. - Action type: pill badge `text-xs px-2 py-1 rounded-full font-medium` — color-coded by category: auth=`bg-blue-50 text-blue-600`, document=`bg-gray-100 text-gray-600`, folder/share=`bg-purple-50 text-purple-600`, admin=`bg-amber-50 text-amber-700`.
- Pagination: `flex items-center justify-between mt-4`. "Previous" / "Next" text buttons. Page indicator `text-sm text-gray-500`. - Pagination: `flex items-center justify-between mt-4`. "Previous" / "Next" text buttons. Page indicator `text-sm text-gray-500`.
- Empty state: centered `py-12 text-gray-400 text-sm` — "No audit log entries match the selected filters." - Empty state: centered `py-12 text-gray-400 text-sm` — "No audit log entries match the selected filters."
@@ -189,7 +203,7 @@ Extend AppSidebar.vue with new sections (D-01, D-06). Order top to bottom:
1. Logo section (unchanged) 1. Logo section (unchanged)
2. Nav links: Home, All Topics (unchanged) 2. Nav links: Home, All Topics (unchanged)
3. **NEW — "Shared with me" entry** (fixed, above folders): rendered with inbox icon `bg-purple-50 text-purple-500`. If `sharedCount > 0`, show count badge `ml-auto bg-purple-100 text-purple-600 text-xs font-semibold rounded-full px-1.5 min-w-[18px] text-center`. Route: `/shared`. 3. **NEW — "Shared with me" entry** (fixed, above folders): rendered with inbox icon `bg-purple-50 text-purple-500`. If `sharedCount > 0`, show count badge `ml-auto bg-purple-100 text-purple-600 text-xs font-semibold rounded-full px-2 min-w-[18px] text-center`. Route: `/shared`.
4. **NEW — Folders section**: section label `text-xs font-semibold text-gray-400 uppercase tracking-wider mb-1` — "Folders". "New folder" inline button: `text-xs text-indigo-600 hover:underline` — appears to the right of the section label. Each top-level folder: `nav-link` style (same as topic links) with folder SVG icon `w-4 h-4 mr-2` and truncated name. Active when `$route.params.folderId === folder.id`. 4. **NEW — Folders section**: section label `text-xs font-semibold text-gray-400 uppercase tracking-wider mb-1` — "Folders". "New folder" inline button: `text-xs text-indigo-600 hover:underline` — appears to the right of the section label. Each top-level folder: `nav-link` style (same as topic links) with folder SVG icon `w-4 h-4 mr-2` and truncated name. Active when `$route.params.folderId === folder.id`.
5. Topics list (unchanged) 5. Topics list (unchanged)
6. QuotaBar (unchanged) 6. QuotaBar (unchanged)
@@ -201,8 +215,8 @@ Extend AppSidebar.vue with new sections (D-01, D-06). Order top to bottom:
Extend DocumentCard.vue with two additions (D-05, SHARE-05): Extend DocumentCard.vue with two additions (D-05, SHARE-05):
1. **Share button**: icon-only button, appears on card hover (`opacity-0 group-hover:opacity-100 transition-opacity`). Add `group` class to outer div. Button: `p-1.5 rounded-md text-gray-400 hover:text-indigo-600 hover:bg-indigo-50 transition-colors min-h-[44px] min-w-[44px] flex items-center justify-center` — share/export SVG icon w-4 h-4. Position: absolute top-right within the card header row, alongside a potential future menu. 1. **Share button**: icon-only button, appears on card hover (`opacity-0 group-hover:opacity-100 transition-opacity`). Add `group` class to outer div. Button: `p-2 rounded-md text-gray-400 hover:text-indigo-600 hover:bg-indigo-50 transition-colors min-h-[44px] min-w-[44px] flex items-center justify-center` — share/export SVG icon w-4 h-4. Position: absolute top-right within the card header row, alongside a potential future menu.
2. **Shared indicator pill** (SHARE-05): `bg-indigo-50 text-indigo-600 text-xs font-medium px-2 py-0.5 rounded-full` — "Shared" — shown inline below the metadata line when `doc.share_count > 0`. 2. **Shared indicator pill** (SHARE-05): `bg-indigo-50 text-indigo-600 text-xs font-medium px-2 py-1 rounded-full` — "Shared" — shown inline below the metadata line when `doc.share_count > 0`.
--- ---
@@ -221,7 +235,7 @@ Source: 04-CONTEXT.md `<specifics>` block + requirement descriptions.
| Non-empty folder delete heading | "Delete folder?" | | Non-empty folder delete heading | "Delete folder?" |
| Non-empty folder delete body | "This folder contains {N} document{s}. Deleting it will permanently delete all documents inside. This cannot be undone." | | Non-empty folder delete body | "This folder contains {N} document{s}. Deleting it will permanently delete all documents inside. This cannot be undone." |
| Non-empty folder delete confirm button | "Delete folder and documents" | | Non-empty folder delete confirm button | "Delete folder and documents" |
| Non-empty folder delete cancel button | "Cancel" | | Non-empty folder delete dismiss button | "Keep folder" |
| Move document action | "Move to folder" | | Move document action | "Move to folder" |
| Move document success (no toast — inline list refresh only) | n/a | | Move document success (no toast — inline list refresh only) | n/a |
@@ -232,12 +246,12 @@ Source: 04-CONTEXT.md `<specifics>` block + requirement descriptions.
| Share button aria-label | "Share document" | | Share button aria-label | "Share document" |
| Share modal title | "Share document" | | Share modal title | "Share document" |
| Handle input placeholder | "Enter username handle" | | Handle input placeholder | "Enter username handle" |
| Share submit CTA | "Share" | | Share submit CTA | "Share document" |
| User not found error (D-04) | "User not found. Check the handle and try again." | | User not found error (D-04) | "User not found. Check the handle and try again." |
| Already shared error | "This document is already shared with that user." | | Already shared error | "This document is already shared with that user." |
| Share success (no toast — list refreshes inline) | n/a | | Share success (no toast — list refreshes inline) | n/a |
| Revoke action label | "Revoke" | | Remove access action label | "Remove access" |
| Revoke confirm (inline, no modal) | None — single click revokes immediately (low-stakes, immediately visible in list) | | Remove access confirm (inline, no modal) | None — single click removes immediately (low-stakes, immediately visible in list) |
| Empty recipients state | "Not shared with anyone yet." | | Empty recipients state | "Not shared with anyone yet." |
| "Shared with me" sidebar label | "Shared with me" | | "Shared with me" sidebar label | "Shared with me" |
| "Shared with me" empty state heading | "No documents shared with you yet." | | "Shared with me" empty state heading | "No documents shared with you yet." |
@@ -311,7 +325,7 @@ Source: 04-CONTEXT.md `<specifics>` block + requirement descriptions.
### Share Modal ### Share Modal
- Share modal does not close after a successful share — the handle input clears and the recipient list updates inline, allowing additional shares without reopening. - Share modal does not close after a successful share — the handle input clears and the recipient list updates inline, allowing additional shares without reopening.
- "Revoke" removes the row immediately (optimistic UI — if API fails, row re-appears with `text-red-500 text-xs` error appended to it). - "Remove access" removes the row immediately (optimistic UI — if API fails, row re-appears with `text-red-500 text-xs` error appended to it).
### PDF Preview (in-app mode) ### PDF Preview (in-app mode)
- Clicking a PDF document card opens DocumentPreviewModal.vue if `pdf_open_mode === 'in_app'`. - Clicking a PDF document card opens DocumentPreviewModal.vue if `pdf_open_mode === 'in_app'`.
@@ -329,7 +343,7 @@ Source: 04-CONTEXT.md `<specifics>` block + requirement descriptions.
## Accessibility Contracts ## Accessibility Contracts
- All icon-only buttons (share, close, three-dot menu, revoke) have `aria-label` attributes. - All icon-only buttons (share, close, three-dot menu, remove access) have `aria-label` attributes.
- Modal overlays set `role="dialog" aria-modal="true" aria-labelledby="{modal-title-id}"`. - Modal overlays set `role="dialog" aria-modal="true" aria-labelledby="{modal-title-id}"`.
- Modal opening focuses the first interactive element inside the panel. - Modal opening focuses the first interactive element inside the panel.
- Modal closing returns focus to the trigger element. - Modal closing returns focus to the trigger element.
@@ -360,7 +374,7 @@ No third-party component registries. All new components are hand-authored Tailwi
| `frontend/src/components/folders/FolderRow.vue` | Folder row in main content area | | `frontend/src/components/folders/FolderRow.vue` | Folder row in main content area |
| `frontend/src/components/folders/FolderBreadcrumb.vue` | Breadcrumb navigation | | `frontend/src/components/folders/FolderBreadcrumb.vue` | Breadcrumb navigation |
| `frontend/src/components/folders/FolderDeleteModal.vue` | Non-empty folder delete confirmation | | `frontend/src/components/folders/FolderDeleteModal.vue` | Non-empty folder delete confirmation |
| `frontend/src/components/sharing/ShareModal.vue` | Share by handle + revoke list | | `frontend/src/components/sharing/ShareModal.vue` | Share by handle + remove-access list |
| `frontend/src/components/documents/DocumentPreviewModal.vue` | In-app PDF preview via iframe | | `frontend/src/components/documents/DocumentPreviewModal.vue` | In-app PDF preview via iframe |
| `frontend/src/components/documents/SearchBar.vue` | Debounced full-text search input | | `frontend/src/components/documents/SearchBar.vue` | Debounced full-text search input |
| `frontend/src/components/documents/SortControls.vue` | Name / Date / Size sort toggle | | `frontend/src/components/documents/SortControls.vue` | Name / Date / Size sort toggle |