From 752cf987aa7e60c64d48c533702a51d5fb123612 Mon Sep 17 00:00:00 2001 From: curo1305 Date: Mon, 25 May 2026 14:30:10 +0200 Subject: [PATCH] docs(04): UI design contract Co-Authored-By: Claude Sonnet 4.6 --- .../04-UI-SPEC.md | 56 ++++++++++++------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/.planning/phases/04-folders-sharing-quotas-document-ux/04-UI-SPEC.md b/.planning/phases/04-folders-sharing-quotas-document-ux/04-UI-SPEC.md index 85cbd97..9b57afb 100644 --- a/.planning/phases/04-folders-sharing-quotas-document-ux/04-UI-SPEC.md +++ b/.planning/phases/04-folders-sharing-quotas-document-ux/04-UI-SPEC.md @@ -1,10 +1,12 @@ --- phase: 4 slug: folders-sharing-quotas-document-ux -status: draft +status: approved shadcn_initialized: false preset: none created: 2026-05-25 +revised: 2026-05-25 +reviewed_at: 2026-05-25 --- # Phase 4 — UI Design Contract @@ -36,7 +38,7 @@ Declared values (multiples of 4 only). Source: existing component scan — `p-4` | Token | Value | Usage | |-------|-------|-------| | 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`) | | lg | 24px | Section padding (`px-6 py-5`), page section breaks | | 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). - 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. -- 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` | | 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. @@ -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. -**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. @@ -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. +Primary focal point: the document/folder list. Secondary: the breadcrumb navigation and search bar. Tertiary: sort controls. + ### FolderRow.vue - 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` @@ -120,7 +134,7 @@ New components required for Phase 4. All follow existing utility-class patterns. - Segments: `flex items-center gap-2 text-sm`. - Each clickable segment: `text-indigo-600 hover:underline font-medium`. - 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`. ### 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`. - 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". -- Handle input row: `flex gap-2` — `` + 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` — `` + 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`. - 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"). - - Right: "Revoke" `text-xs text-red-500 hover:text-red-700 font-medium` button. + - 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: "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." - 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. - 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." -- 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) - 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. - 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`. - - 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`. - 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) 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`. 5. Topics list (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): -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. -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`. +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-1 rounded-full` — "Shared" — shown inline below the metadata line when `doc.share_count > 0`. --- @@ -221,7 +235,7 @@ Source: 04-CONTEXT.md `` block + requirement descriptions. | 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 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 success (no toast — inline list refresh only) | n/a | @@ -232,12 +246,12 @@ Source: 04-CONTEXT.md `` block + requirement descriptions. | Share button aria-label | "Share document" | | Share modal title | "Share document" | | 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." | | Already shared error | "This document is already shared with that user." | | Share success (no toast — list refreshes inline) | n/a | -| Revoke action label | "Revoke" | -| Revoke confirm (inline, no modal) | None — single click revokes immediately (low-stakes, immediately visible in list) | +| Remove access action label | "Remove access" | +| 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." | | "Shared with me" sidebar label | "Shared with me" | | "Shared with me" empty state heading | "No documents shared with you yet." | @@ -311,7 +325,7 @@ Source: 04-CONTEXT.md `` block + requirement descriptions. ### 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. -- "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) - Clicking a PDF document card opens DocumentPreviewModal.vue if `pdf_open_mode === 'in_app'`. @@ -329,7 +343,7 @@ Source: 04-CONTEXT.md `` block + requirement descriptions. ## 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 opening focuses the first interactive element inside the panel. - 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/FolderBreadcrumb.vue` | Breadcrumb navigation | | `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/SearchBar.vue` | Debounced full-text search input | | `frontend/src/components/documents/SortControls.vue` | Name / Date / Size sort toggle |