1ba578c7f6
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
354 lines
17 KiB
Markdown
354 lines
17 KiB
Markdown
---
|
||
phase: 3
|
||
slug: document-migration-multi-user-isolation
|
||
status: approved
|
||
reviewed_at: 2026-05-23
|
||
shadcn_initialized: false
|
||
preset: none
|
||
created: 2026-05-23
|
||
---
|
||
|
||
# Phase 3 — UI Design Contract
|
||
|
||
> Visual and interaction contract for Phase 3: Document Migration & Multi-User Isolation.
|
||
> Generated by gsd-ui-researcher, verified by gsd-ui-checker.
|
||
|
||
---
|
||
|
||
## Design System
|
||
|
||
| Property | Value |
|
||
|----------|-------|
|
||
| Tool | none — raw Tailwind CSS v3 |
|
||
| Preset | not applicable |
|
||
| Component library | none |
|
||
| Icon library | Inline SVG heroicons (stroke, no fill) — matches existing AppSidebar, DropZone, and UploadProgress patterns |
|
||
| Font | System font stack (Tailwind default: ui-sans-serif, system-ui, sans-serif) |
|
||
|
||
**Source:** Inherited from Phase 2 UI-SPEC (approved 2026-05-22). No design system changes in Phase 3. `tailwind.config.js` has empty `theme.extend` — no custom tokens.
|
||
|
||
---
|
||
|
||
## Spacing Scale
|
||
|
||
Identical to Phase 2 — inherited, not re-specified.
|
||
|
||
| Token | Value | Usage |
|
||
|-------|-------|-------|
|
||
| xs | 4px | Icon-to-label gaps, badge padding, progress step gaps |
|
||
| sm | 8px | Compact inline spacing, quota bar label gap |
|
||
| md | 16px | Default field padding, card internal padding |
|
||
| lg | 24px | Section vertical gap |
|
||
| xl | 32px | Upload card top margin |
|
||
| 2xl | 48px | Page vertical centering padding |
|
||
| 3xl | 64px | Page-level clearance |
|
||
|
||
Exceptions:
|
||
- Upload progress bar track height: 8px (`h-2`). Justification: 4px (xs) is too narrow to show color transitions clearly at small viewport widths; 8px is the established browser progress-bar idiom and remains within the scale.
|
||
- Quota bar track height: 8px (`h-2`). Same justification as upload progress bar.
|
||
- Quota bar touch/click area (sidebar widget, not interactive): no min-height required — display-only element.
|
||
|
||
**Source:** Phase 2 UI-SPEC approved spacing scale. Phase 3 adds no new interactive controls that require new spacing exceptions.
|
||
|
||
---
|
||
|
||
## Typography
|
||
|
||
Identical to Phase 2 — inherited, not re-specified.
|
||
|
||
| Role | Size | Weight | Line Height | Tailwind Class |
|
||
|------|------|--------|-------------|----------------|
|
||
| Body | 14px | 400 (regular) | 1.5 | `text-sm` |
|
||
| Label | 14px | 600 (semibold) | 1.4 | `text-sm font-semibold` |
|
||
| Heading | 20px | 600 (semibold) | 1.3 | `text-xl font-semibold` |
|
||
| Display | 24px | 600 (semibold) | 1.2 | `text-2xl font-semibold` |
|
||
|
||
Font weight scale: exactly 2 weights — regular (400) and semibold (600). Do not introduce `font-medium` (500) or `font-bold` (700). Do not use `font-medium` that already appears in `.nav-link` scoped styles — that is a pre-existing pattern in AppSidebar and must not be propagated to new components.
|
||
|
||
**New in Phase 3:**
|
||
- Quota bar label: `text-sm text-gray-500` — visual subordination via muted color, not reduced size.
|
||
- Upload step label (beneath progress bar): `text-sm text-gray-400` — subordination via color only; same contrast pattern as existing `UploadProgress.vue` status lines.
|
||
- Error detail lines in quota rejection: `text-sm text-red-600` — matches Phase 2 inline field error style.
|
||
|
||
All three usages remain within the 4-size scale (Body/Label/Heading/Display). The existing `UploadProgress.vue` pattern of `text-xs` is NOT propagated to new Phase 3 code — all new code uses `text-sm` with color differentiation for hierarchy.
|
||
|
||
**Source:** Phase 2 UI-SPEC. Maximum 4 type sizes enforced — no `text-xs` in new Phase 3 components.
|
||
|
||
---
|
||
|
||
## Color
|
||
|
||
Identical to Phase 2 palette — no new colors introduced. Phase 3 adds two state-specific usages within the existing amber and red semantic tokens.
|
||
|
||
| Role | Value | Tailwind Token | Usage |
|
||
|------|-------|----------------|-------|
|
||
| Dominant (60%) | #f9fafb | `bg-gray-50` | Page background |
|
||
| Secondary (30%) | #ffffff | `bg-white` | Cards, upload progress rows, sidebar |
|
||
| Accent (10%) | #4f46e5 | `indigo-600` / `indigo-700` | Primary CTA buttons only, active nav link text, brand logo text |
|
||
| Accent subtle | #eef2ff | `indigo-50` | Active nav link background; upload progress bar fill (in-progress step) |
|
||
| Destructive | #dc2626 | `red-600` | Quota bar at ≥ 95% fill; quota rejection error banner border/text |
|
||
| Warning | #d97706 | `amber-600` | Quota bar at ≥ 80% fill (below 95%); quota warning text |
|
||
| Success | #16a34a | `green-600` | Upload complete checkmark icon — existing UploadProgress pattern |
|
||
| Neutral border | #e5e7eb | `gray-200` | Card borders, progress row borders, quota bar track background |
|
||
| Muted text | #9ca3af | `gray-400` | Quota bar label secondary text, upload step sub-labels |
|
||
|
||
**Source:** Phase 2 UI-SPEC. CONTEXT.md STORE-04 locks amber at 80%, red at 95%. CONTEXT.md D-07 specifies HTTP 413 response — maps to `red-600` error treatment.
|
||
|
||
Accent reserved for: primary CTA buttons, active sidebar nav link text, DocuVault brand/logo text, in-progress upload step indicator fill (`indigo-50` track fill).
|
||
Accent is NOT used on: quota bar fills, error banners, or secondary link text.
|
||
|
||
### Quota Bar Color Logic (State Machine)
|
||
|
||
| Condition | Bar fill color | Label color |
|
||
|-----------|---------------|-------------|
|
||
| `usage < 80%` | `bg-indigo-500` | `text-gray-500` |
|
||
| `80% ≤ usage < 95%` | `bg-amber-500` | `text-amber-600` |
|
||
| `usage ≥ 95%` | `bg-red-500` | `text-red-600` |
|
||
|
||
Bar track (background): `bg-gray-200` always.
|
||
Bar fill width: `style="width: {percent}%"` clamped to `max-w-full`.
|
||
|
||
---
|
||
|
||
## Upload Flow — Interaction Contract
|
||
|
||
### Overview
|
||
|
||
The two-step presigned upload (CONTEXT.md D-05) is invisible to the user as a multi-step operation. The user sees a single continuous upload experience from file selection to completion.
|
||
|
||
### Step Mapping to UI States
|
||
|
||
| Internal Step | User-visible state |
|
||
|---------------|--------------------|
|
||
| Step 1: `POST /api/documents/upload-url` | "Preparing upload…" — spinner active, 0% progress |
|
||
| Step 2: `PUT {presigned_url}` (browser → MinIO) | "Uploading…" — progress bar advances 5% → 90% using XHR `progress` event |
|
||
| Step 3: `POST /api/documents/{id}/confirm` | "Processing…" — progress bar at 95%, spinner resumes |
|
||
| Celery enqueued (confirm returns 200) | "Done — classifying…" — progress bar completes to 100%, green checkmark |
|
||
| Error at any step | Error state (see Error States below) |
|
||
|
||
### Progress Bar Visual Contract
|
||
|
||
The progress bar is an addition to the existing `UploadProgress.vue` component row.
|
||
|
||
Each upload row in `UploadProgress.vue` gains a progress bar between the filename and the status line:
|
||
|
||
```
|
||
[ filename.pdf (spinner/checkmark/error icon) ]
|
||
[ ████████████░░░░░░░░░░ 62% ]
|
||
[ Uploading… ]
|
||
```
|
||
|
||
Bar specifications:
|
||
- Track: `w-full h-2 bg-gray-100 rounded-full mt-1`
|
||
- Fill: `h-2 rounded-full transition-all duration-300` + color class from state
|
||
- In-progress fill color: `bg-indigo-500`
|
||
- Complete fill color: `bg-green-500`
|
||
- Error fill color: `bg-red-400` (stops at last % value, does not reset to 0)
|
||
- Percentage label: `text-sm text-gray-400 text-right mt-1` — shown during upload, hidden after completion
|
||
|
||
### Upload Progress Values by Step
|
||
|
||
| Step | Progress value | Visual |
|
||
|------|---------------|--------|
|
||
| Awaiting upload URL | 0% | Bar at 0, status "Preparing upload…" |
|
||
| Upload URL received | 5% | Bar jumps to 5% |
|
||
| XHR progress events | 5% → 90% (linear from XHR `loaded/total`) | Bar animates smoothly |
|
||
| Confirm call in flight | 92% | Bar at 92%, status "Processing…" |
|
||
| Confirm returned 200 | 100% | Bar fills green, status "Done — classifying…" |
|
||
|
||
The 5%–90% range is reserved for the XHR PUT. The remaining 10% (90%–100%) covers confirm + classification enqueue. This prevents the bar appearing "stuck" at 100% waiting for the confirm call.
|
||
|
||
### CORS and MinIO PUT
|
||
|
||
The browser PUTs directly to MinIO over a presigned URL (cross-origin). This is transparent to the user. If the PUT fails due to a network error or CORS rejection, treat it as a generic upload error (see Error States). Do not surface "CORS" in any user-facing copy.
|
||
|
||
---
|
||
|
||
## Quota Usage Bar — Sidebar Contract
|
||
|
||
### Placement in AppSidebar.vue
|
||
|
||
Insert the quota bar between the topics nav section (`.flex-1 nav`) and the bottom settings/admin/identity footer (`.px-3.py-4.border-t`).
|
||
|
||
```
|
||
[ Topics nav section ]
|
||
[ ────────────────────── ] ← existing border-t border-gray-100
|
||
[ Quota bar widget ] ← NEW: Phase 3
|
||
[ ────────────────────── ] ← existing border-t border-gray-100
|
||
[ Admin / Settings / Footer ]
|
||
```
|
||
|
||
### Quota Bar Widget Structure
|
||
|
||
```html
|
||
<div class="px-4 py-3 border-t border-gray-100">
|
||
<!-- Label row -->
|
||
<div class="flex items-center justify-between mb-1">
|
||
<span class="text-sm font-semibold text-gray-500">Storage</span>
|
||
<span class="text-sm {label-color}">X MB of Y MB</span>
|
||
</div>
|
||
<!-- Bar track -->
|
||
<div class="w-full h-2 bg-gray-200 rounded-full">
|
||
<div class="h-2 rounded-full transition-all duration-500 {fill-color}" style="width: Z%"></div>
|
||
</div>
|
||
</div>
|
||
```
|
||
|
||
Label format: `"{used} MB of {limit} MB"` — values rounded to 1 decimal place (e.g., "12.3 MB of 100.0 MB"). Do not show bytes or KB — always MB with 1 decimal place.
|
||
|
||
### Loading and Error States
|
||
|
||
| State | Display |
|
||
|-------|---------|
|
||
| Loading (initial fetch) | Skeleton bar: `bg-gray-100 animate-pulse h-2 rounded-full w-full` — no label text |
|
||
| Fetch error | Hide widget entirely — fail silently (consistent with AdminQuotasTab pattern of filtering failed quota fetches silently) |
|
||
| `used_bytes = 0` | Show bar at 0% with label "0.0 MB of Y MB" — do not hide the widget |
|
||
|
||
### Data Source
|
||
|
||
Fetches from `GET /api/me/quota` on sidebar mount. Re-fetches after every successful document upload (confirm step returns 200) and after every document delete. No polling.
|
||
|
||
Store: quota state lives in `useAuthStore` as `quota: { used_bytes, limit_bytes }` — updated by the documents store after upload/delete. Do not create a separate quota store.
|
||
|
||
---
|
||
|
||
## Error States
|
||
|
||
### Upload Error Types
|
||
|
||
| HTTP status | Trigger | User-facing copy |
|
||
|------------|---------|-----------------|
|
||
| 401 | Access token expired mid-upload | Dismiss upload row, show session-expired toast (Phase 2 contract), redirect to `/login` |
|
||
| 413 | Quota exceeded at confirm step | Show quota rejection error block (see below) |
|
||
| Network error (XHR fail, CORS) | MinIO unreachable or CORS misconfiguration | "Upload failed. Please try again." — row error state |
|
||
| 500 | Server error on upload-url or confirm | "Something went wrong. Please try again." — row error state |
|
||
| Other 4xx | Unexpected API error | "Upload failed. Please try again." — row error state |
|
||
|
||
### Quota Rejection Error Block (413)
|
||
|
||
Displayed inline within the upload progress row, replacing the progress bar and status line. NOT a toast — inline only.
|
||
|
||
```
|
||
[ filename.pdf (error icon red) ]
|
||
[ ┌─────────────────────────────────────────────────────┐ ]
|
||
[ │ Not enough storage │ ]
|
||
[ │ This file (X MB) would exceed your quota. │ ]
|
||
[ │ You're using Y MB of Z MB. │ ]
|
||
[ │ Manage storage → │ ]
|
||
[ └─────────────────────────────────────────────────────┘ ]
|
||
```
|
||
|
||
Error block styles:
|
||
- Container: `mt-1 p-3 rounded-lg bg-red-50 border border-red-200`
|
||
- Heading: `text-sm font-semibold text-red-700`
|
||
- Body lines: `text-sm text-red-600 mt-1`
|
||
- Link: `text-sm text-red-600 underline hover:text-red-700 font-semibold` — navigates to `/settings` (the existing settings route; storage settings live there in Phase 3 since the dedicated storage page is Phase 4)
|
||
|
||
Values populated from 413 response body: `{"detail": {"used_bytes": N, "limit_bytes": M, "rejected_bytes": K}}`.
|
||
- "This file (X MB)" — `rejected_bytes` converted to MB, 1 decimal place
|
||
- "You're using Y MB of Z MB" — `used_bytes` / `limit_bytes` converted to MB, 1 decimal place
|
||
|
||
### 401 During Upload
|
||
|
||
When a 401 occurs at any upload step:
|
||
1. Mark the upload row as failed with copy "Session expired. Signing you in again…"
|
||
2. Fire the session-expired toast (Phase 2 contract: `bg-gray-900 text-white text-sm px-4 py-3 rounded-lg shadow-lg`, 5s auto-dismiss)
|
||
3. Redirect to `/login` after 1.5s (gives user time to read the row message)
|
||
4. Do NOT retry the upload automatically — the user must sign in and re-upload
|
||
|
||
---
|
||
|
||
## Loading States (Phase 3 Additions)
|
||
|
||
Extends Phase 2 loading state contract.
|
||
|
||
| Action | Loading indicator | Element state |
|
||
|--------|------------------|---------------|
|
||
| Upload step 1 (get URL) | Spinner in row icon slot, progress bar at 0% | Row status "Preparing upload…" |
|
||
| Upload step 2 (XHR PUT) | Progress bar advancing, spinner removed | Row status "Uploading…" |
|
||
| Upload step 3 (confirm) | Progress bar at 92%, spinner in row icon slot resumes | Row status "Processing…" |
|
||
| Quota bar initial load | Skeleton pulse bar | No label text |
|
||
| Quota refetch after upload | Silent — bar updates on next successful fetch; no spinner shown in sidebar |
|
||
|
||
Spinner reuse: `AppSpinner.vue` from Phase 2 — `animate-spin rounded-full border-2 border-current border-t-transparent` inheriting color.
|
||
|
||
---
|
||
|
||
## Copywriting Contract
|
||
|
||
Phase 2 copy contract is fully inherited. Phase 3 additions only.
|
||
|
||
| Element | Copy |
|
||
|---------|------|
|
||
| Upload step: preparing | "Preparing upload…" |
|
||
| Upload step: uploading | "Uploading…" |
|
||
| Upload step: processing | "Processing…" |
|
||
| Upload step: complete | "Done — classifying…" |
|
||
| Upload step: classified | "Done — classified as: {topics}" (matches existing UploadProgress pattern) |
|
||
| Upload step: no topics | "Done — no topics assigned" (matches existing UploadProgress pattern) |
|
||
| Upload generic error | "Upload failed. Please try again." |
|
||
| Upload 401 error | "Session expired. Signing you in again…" |
|
||
| Upload quota rejection heading | "Not enough storage" |
|
||
| Upload quota rejection body line 1 | "This file ({X} MB) would exceed your quota." |
|
||
| Upload quota rejection body line 2 | "You're using {Y} MB of {Z} MB." |
|
||
| Upload quota rejection link | "Manage storage →" |
|
||
| Quota bar label | "Storage" |
|
||
| Quota bar usage display | "{used} MB of {limit} MB" |
|
||
| Quota bar loading (aria) | `aria-label="Loading storage usage"` on skeleton element |
|
||
| Empty document list | Heading: "No documents yet" / Body: "Upload a file to get started." |
|
||
| Document list loading error | "Failed to load documents. Please refresh." |
|
||
|
||
### Security Copy Rules (Inherited)
|
||
|
||
- Never confirm or deny whether an email exists
|
||
- Generic error messages must not echo PII
|
||
- Quota messages display bytes only from the server-authoritative 413 response body — never from client-side file size alone
|
||
|
||
---
|
||
|
||
## Component Inventory (New or Modified — Phase 3)
|
||
|
||
| Component | Path | Change Type | Description |
|
||
|-----------|------|-------------|-------------|
|
||
| AppSidebar | `src/components/layout/AppSidebar.vue` | Modified | Add QuotaBar widget between topics nav and footer section |
|
||
| UploadProgress | `src/components/upload/UploadProgress.vue` | Modified | Add progress bar track + fill per row; add step status strings; add quota rejection error block |
|
||
| QuotaBar | `src/components/layout/QuotaBar.vue` | New | Standalone quota bar widget — extracted for testability; used inside AppSidebar |
|
||
| documents store | `src/stores/documents.js` | Modified | Replace single `upload()` with three-step flow (upload-url → PUT → confirm); add `uploadProgress` reactive map keyed by filename; dispatch quota refetch after upload/delete |
|
||
|
||
No new views required. No new layouts required. All Phase 3 UI is additive to existing AppSidebar and UploadProgress components.
|
||
|
||
---
|
||
|
||
## Registry Safety
|
||
|
||
| Registry | Blocks Used | Safety Gate |
|
||
|----------|-------------|-------------|
|
||
| shadcn official | none | not applicable — shadcn not installed |
|
||
| third-party | none | not applicable |
|
||
|
||
No third-party component registries. All components are handwritten Vue 3 SFC with raw Tailwind CSS.
|
||
|
||
---
|
||
|
||
## Accessibility
|
||
|
||
| Element | Requirement |
|
||
|---------|-------------|
|
||
| Quota bar | `role="progressbar"` on the fill `<div>`, `aria-valuenow="{percent}"`, `aria-valuemin="0"`, `aria-valuemax="100"`, `aria-label="Storage usage: {used} MB of {limit} MB"` |
|
||
| Upload progress bar | `role="progressbar"` on each row's fill `<div>`, `aria-valuenow="{percent}"`, `aria-valuemin="0"`, `aria-valuemax="100"`, `aria-label="Upload progress for {filename}"` |
|
||
| Quota rejection error | `role="alert"` on the error container so screen readers announce it immediately |
|
||
| Quota bar skeleton | `aria-label="Loading storage usage"` + `aria-busy="true"` |
|
||
|
||
---
|
||
|
||
## 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
|