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

17 KiB
Raw Permalink Blame History

phase, slug, status, reviewed_at, shadcn_initialized, preset, created
phase slug status reviewed_at shadcn_initialized preset created
3 document-migration-multi-user-isolation approved 2026-05-23 false none 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

<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