Files
2026-05-23 10:21:05 +02:00

354 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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