---
phase: 4
slug: folders-sharing-quotas-document-ux
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

> 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 80–95% (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." |

### Document List & Search

| 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
