Files
curo1305 752cf987aa docs(04): UI design contract
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 14:30:10 +02:00

23 KiB
Raw Permalink Blame History

phase, slug, status, shadcn_initialized, preset, created, revised, reviewed_at
phase slug status shadcn_initialized preset created revised reviewed_at
4 folders-sharing-quotas-document-ux approved false none 2026-05-25 2026-05-25 2026-05-25

Phase 4 — UI Design Contract

Visual and interaction contract for Phase 4: Folders, Sharing, Quotas & Document UX. Generated by gsd-ui-researcher, verified by gsd-ui-checker. Source: existing codebase scan + 04-CONTEXT.md decisions.


Design System

Property Value
Tool none
Preset not applicable
Component library none (inline SVG icons, Tailwind utility classes only)
Icon library Inline SVG (Heroicons stroke style, w-4 h-4 / w-5 h-5) — do not add an icon package
Font System font stack (Tailwind default — no custom font loaded)

No design system change for Phase 4. All new components extend the existing Tailwind utility pattern established in Phase 2/3.


Spacing Scale

Declared values (multiples of 4 only). Source: existing component scan — p-4, px-6 py-5, gap-3, px-3 py-2.

Token Value Usage
xs 4px Icon gaps, topic badge gaps (gap-1)
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)
2xl 48px Major empty-state vertical padding (py-12)
3xl 64px Not used in Phase 4

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-2 py-1 compact (4px-grid minimum).

Typography

Extend existing scale — do not introduce new sizes. Source: DocumentCard.vue, AppSidebar.vue, DocumentView.vue.

Role Size Weight Line Height Tailwind classes
Page heading 24px (text-2xl) 700 (font-bold) 1.2 text-2xl font-bold text-gray-900
Section heading 18px (text-lg) 600 (font-semibold) 1.3 text-lg font-semibold text-gray-800
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

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.

Section label / nav category: text-xs font-semibold text-gray-400 uppercase tracking-wider — established pattern, reuse for folder section header in sidebar.


Color

Source: AppSidebar.vue, QuotaBar.vue, DocumentCard.vue, DocumentView.vue. No new colors introduced.

Role Tailwind value Hex (approx) Usage
Dominant (60%) bg-white #ffffff Page background, cards, sidebar background
Secondary (30%) bg-gray-50 / border-gray-200 #f9fafb / #e5e7eb Card hover surface, extracted text block background, modal backdrop inner panel, table row stripes
Accent (10%) indigo-600 / indigo-50 / indigo-700 #4f46e5 Reserved-for list below
Destructive red-600 / red-700 #dc2626 Destructive action buttons only
Warning — quota amber amber-500 / amber-600 #f59e0b QuotaBar 8095% (pre-existing, do not change)
Warning — quota red red-500 / red-600 #ef4444 QuotaBar ≥95% (pre-existing, do not change)

Accent (indigo) reserved for:

  • Active nav link background: bg-indigo-50 text-indigo-700
  • Primary action buttons (filled): bg-indigo-600 hover:bg-indigo-700 text-white
  • Document icon background: bg-indigo-50 with text-indigo-500 icon
  • Logo text: text-indigo-600
  • User avatar background: bg-indigo-100 text-indigo-700
  • QuotaBar fill (normal state): bg-indigo-500
  • Link text (back navigation, inline links): text-indigo-600 hover:underline
  • Checkbox / radio checked state: text-indigo-600 (Tailwind form plugin default)
  • Share badge (shared indicator on document card): bg-indigo-50 text-indigo-600 pill

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-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.

Folder row icons in main content: bg-gray-100 text-gray-500 (neutral — folders are structural, not accent).


Component Inventory

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
  • Left: folder icon w-9 h-9 rounded-lg bg-gray-100 flex items-center justify-center with text-gray-500 SVG folder icon (w-5 h-5).
  • Middle: folder name font-medium text-gray-900 text-sm, sub-label text-xs text-gray-400 showing document count (e.g., "3 documents").
  • Right: three-dot menu button text-gray-400 hover:text-gray-600 with dropdown for Rename and Delete actions.

FolderBreadcrumb.vue

  • Rendered above document list when inside a folder.
  • 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-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

  • Triggered by share icon button on DocumentCard.
  • 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<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.
  • 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-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.

FolderDeleteModal.vue

  • Triggered when deleting a non-empty folder (D-03).
  • Reuses ConfirmBlock.vue pattern but rendered in a modal panel (same overlay style as ShareModal).
  • 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. 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).
  • Overlay: fixed inset-0 bg-black/60 z-50 flex flex-col.
  • Header bar: bg-white border-b border-gray-200 px-6 py-3 flex items-center justify-between.
    • Left: document name text-sm font-medium text-gray-900 truncate max-w-xs.
    • Right: close button text-gray-400 hover:text-gray-600, SVG X w-5 h-5.
  • Content: flex-1 overflow-hidden<iframe class="w-full h-full border-0" :src="proxyUrl" title="Document preview">.
  • No loading spinner needed — browser handles iframe load state.

SearchBar.vue (inline in HomeView/FolderView)

  • Rendered above the document list, right-aligned alongside sort controls.
  • <input type="search" placeholder="Search documents…" class="border border-gray-300 rounded-lg px-3 py-2 text-sm w-56 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent">.
  • Debounced 300 ms. Minimum 2 characters before firing. Clears on Escape key.
  • While searching (debounce pending): no spinner — results just update.
  • No results: inline empty state text below the list area (see copywriting below).

SortControls.vue (inline in HomeView/FolderView)

  • Three text buttons: "Name" / "Date" / "Size" — text-xs text-gray-500 hover:text-gray-900 font-medium px-2 py-1 rounded.
  • Active sort: text-indigo-600 font-semibold. Direction indicator: or suffix appended to active label.

AuditLogTab.vue (extends AdminView)

  • New tab added to AdminView alongside existing Users, Quotas, AI Config tabs.
  • Filter bar: flex flex-wrap gap-3 mb-4 items-end.
    • Date range: two <input type="date"> fields with text-sm border border-gray-300 rounded-lg px-3 py-2 focus:ring-indigo-500.
    • User dropdown: <select> with same styling — "All users" default.
    • Action type dropdown: same styling — "All actions" default.
    • Apply button: bg-indigo-600 text-white text-sm px-4 py-2 rounded-lg hover:bg-indigo-700.
    • Export button: border border-gray-300 text-gray-700 text-sm px-4 py-2 rounded-lg hover:bg-gray-50 — "Export CSV".
  • Table: w-full text-sm border-collapse.
    • Header row: text-xs font-semibold text-gray-400 uppercase tracking-wider on bg-gray-50 border-b border-gray-200.
    • 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-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."

AppSidebar Extensions

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-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)
  7. Settings + Admin footer (unchanged)

DocumentCard Extensions

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-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.

Copywriting Contract

Source: 04-CONTEXT.md <specifics> block + requirement descriptions.

Folders

Element Copy
New folder CTA "New folder"
Folder rename action "Rename"
Folder delete action "Delete folder"
Empty folder state (no documents, no sub-folders) "This folder is empty." / "Upload documents or create sub-folders to organize your files."
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 dismiss button "Keep folder"
Move document action "Move to folder"
Move document success (no toast — inline list refresh only) n/a

Sharing

Element Copy
Share button aria-label "Share document"
Share modal title "Share document"
Handle input placeholder "Enter username handle"
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
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."
"Shared with me" empty state body "When someone shares a document with you, it will appear here."
Element Copy
Search placeholder "Search documents…"
No results state (after search) "No documents match "{query}"." / "Try a different search term."
Document list empty state (no documents, no search) "No documents yet. Upload one above." (pre-existing, unchanged)
Sort label prefix "Sort by:" — rendered as accessible visually-hidden label; buttons labeled "Name", "Date", "Size"

PDF Preview

Element Copy
Document preferences section heading "Document Preferences"
PDF open mode radio option 1 "Open documents in-app"
PDF open mode radio option 2 "Open documents in new tab"
Preview close button aria-label "Close preview"

Audit Log

Element Copy
Audit log tab label "Audit Log"
Filter apply CTA "Apply filters"
Export CTA "Export CSV"
Empty state (no results) "No audit log entries match the selected filters."
Date range label "From" / "To"
User filter label "User" with default option "All users"
Action type filter label "Action" with default option "All actions"
Pagination previous "Previous"
Pagination next "Next"

Error States (general)

Element Copy
Generic API failure "Something went wrong. Please try again."
Network error "Unable to connect. Check your connection and try again."
404 (folder or document not found) "This {folder/document} no longer exists."
Quota rejection on upload (pre-existing, do not change) As implemented in Phase 3 UploadProgress.vue

Interaction Contracts

Folder Navigation

  • Clicking a top-level folder in the sidebar navigates to /folders/{id}.
  • Within a folder view, sub-folder rows are rendered above document cards.
  • Clicking a sub-folder row navigates deeper: /folders/{id}.
  • Breadcrumb segments are anchor/router-link elements — keyboard navigable.
  • "New folder" triggers an inline text field directly in the folder list (not a modal), pre-focused, Escape cancels, Enter submits, empty name submits are rejected client-side with text-red-500 text-xs mt-1 error inline.

Folder Rename

  • Three-dot menu on FolderRow opens a position: absolute dropdown bg-white border border-gray-200 rounded-lg shadow-md py-1 min-w-[120px] z-10 with two items: "Rename" and "Delete folder". Menu closes on outside click.
  • Rename: replaces folder name text with an inline <input> pre-filled with current name. Same commit behavior as new folder (Enter = save, Escape = cancel).

Document Sort

  • Default sort: Date, descending (newest first).
  • Sort state lives in local view state (not URL params) for Phase 4 MVP.
  • Sort change triggers immediate re-fetch with ?sort=name|date|size&order=asc|desc query params.

Search Debounce

  • 300 ms debounce. Fires only when input length >= 2.
  • While a search request is in-flight, a text-gray-400 text-xs "Searching…" message appears inline below the search bar (not a spinner).
  • Clearing the search input (empty string) immediately restores the full document list without an API call.

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.
  • "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'.
  • Non-PDF documents continue to navigate to DocumentView (metadata/extracted text).
  • PDFs also show metadata/extracted text in DocumentView when accessed via direct URL (modal is a shortcut, not the only path).
  • Clicking outside the modal panel (on the overlay) closes the modal.
  • Pressing Escape closes the modal.

Audit Log Pagination

  • Page size: 50 rows per page.
  • Pagination via ?page=N&page_size=50 query params on the audit log API endpoint.
  • Filter changes reset to page 1.

Accessibility Contracts

  • 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.
  • Sort buttons set aria-pressed="true" on the active sort. Direction change is conveyed via visible / character plus aria-label including direction (e.g., aria-label="Sort by name, ascending").
  • Breadcrumb wrapper: <nav aria-label="Folder navigation"> with <ol> list structure.
  • Search input: role="search" on the wrapping <div>, aria-label="Search documents" on the input.
  • QuotaBar: role="progressbar" already implemented — no change.
  • Audit log table: <table> with <thead>, <th scope="col"> headers.
  • FolderDeleteModal, ShareModal: trap focus within modal while open.

Registry Safety

Registry Blocks Used Safety Gate
shadcn official none not applicable — no shadcn in this project
Third-party none not applicable

No third-party component registries. All new components are hand-authored Tailwind utility classes following established project patterns. No new npm packages for UI components.


New Files Required

File Purpose
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 + 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
frontend/src/components/admin/AuditLogTab.vue Admin audit log viewer with filters + export
frontend/src/views/FolderView.vue Folder contents view (sub-folders + documents + breadcrumb)
frontend/src/views/SharedView.vue "Shared with me" virtual folder view

Existing files extended (not replaced):

  • frontend/src/components/layout/AppSidebar.vue — add Shared with me entry + Folders section
  • frontend/src/components/documents/DocumentCard.vue — add share button + shared indicator pill
  • frontend/src/views/HomeView.vue — add SearchBar + SortControls above document list
  • frontend/src/views/DocumentView.vue — add PDF preview trigger logic
  • frontend/src/views/AdminView.vue — add AuditLog tab
  • frontend/src/views/SettingsView.vue — add Document Preferences card with pdf_open_mode radio

Checker Sign-Off

  • Dimension 1 Copywriting: PASS
  • Dimension 2 Visuals: PASS
  • Dimension 3 Color: PASS
  • Dimension 4 Typography: PASS
  • Dimension 5 Spacing: PASS
  • Dimension 6 Registry Safety: PASS

Approval: pending