Files
curo1305 708fd7fad0 docs(phase-6.2): record planning complete — 4 plans verified, state updated
- ROADMAP.md: progress table → Planned; wave annotations already added by planner
- STATE.md: phase 6.2 row → Planned (4 plans, 3 waves); session note added
- 06.2-03-PLAN.md: remove incorrect SHARE-03/SHARE-05 from requirements field
- 06.2-RESEARCH.md: mark Open Questions section as RESOLVED
- 06.2-UI-SPEC.md: add to version control (was untracked)

Verification: 0 blockers, 2 cosmetic warnings fixed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 11:41:32 +02:00

15 KiB

phase, slug, status, shadcn_initialized, preset, created
phase slug status shadcn_initialized preset created
6.2 close-v1-sharing-cloud-delete-csv-export-gaps draft false none 2026-05-31

Phase 6.2 — UI Design Contract

Visual and interaction contract for Phase 6.2: Close v1 sharing + cloud-delete + CSV export gaps. Generated by gsd-ui-researcher. Verified by gsd-ui-checker.


Design System

Property Value
Tool none — pure Tailwind CSS v3.4
Preset not applicable
Component library none (Heroicons inline SVG only)
Icon library Heroicons stroke, w-4 / w-5 sizes (inline SVG, no package)
Font system-ui (browser default stack — no custom font loaded)

Source: codebase scan — no components.json, no component registry, confirmed via directory listing.


Spacing Scale

Declared values (all multiples of 4, mapped to Tailwind utilities):

Token Value Tailwind Class Usage
xs 4px gap-1, p-1 Icon gaps, inline badge padding
sm 8px gap-2, p-2 Compact element spacing, button icon padding
md 16px p-4, gap-4 Default element spacing, card body padding
lg 24px p-6, gap-6 Modal body padding, section padding
xl 32px p-8 Empty state vertical padding
2xl 48px py-12 Page-level empty state vertical rhythm
3xl 64px n/a Not used in this phase

Exceptions:

  • Icon-only action buttons: min-h-[44px] min-w-[44px] touch target — established in DocumentCard and maintained for all new icon buttons. Source: DocumentCard.vue line 43.
  • Share row inline items: py-2 (8px vertical) per row — matches existing ShareModal.vue recipient list rhythm.
  • Permission dropdown in share creation row: no extra spacing; sits inline within the existing flex gap-2 row.

Typography

Role Size Weight Line Height Tailwind Classes
Body 14px 400 1.5 text-sm
Label / caption 12px 600 1.4 text-xs font-semibold
Heading (modal title) 18px 600 1.2 text-lg font-semibold
Mono (timestamps, IDs) 12px 400 1.4 text-xs font-mono

Source: codebase scan — ShareModal.vue uses text-lg font-semibold for modal title, text-sm for body text, text-xs for labels and badges. AuditLogTab.vue uses font-mono text-xs for timestamps and IP addresses. All four roles are already present in the components being modified.


Color

Role Value Tailwind Class Usage
Dominant (60%) #F9FAFB bg-gray-50 Page background, table header row
Secondary (30%) #FFFFFF bg-white Cards, modals, table body, dropdown panels
Accent (10%) #4F46E5 bg-indigo-600 / text-indigo-600 Primary action buttons, focus rings, topic pills, shared badge
Destructive #EF4444 text-red-500 / text-red-600 / bg-red-50 Remove access button, cloud delete failure modal warning text, error inline text

Accent reserved for (explicit list):

  1. Primary CTA buttons: "Share document", "Apply filters", "Export CSV", "Download" — bg-indigo-600 hover:bg-indigo-700 text-white
  2. Focus rings on all text inputs and selects: focus:ring-2 focus:ring-indigo-500
  3. "Shared" indicator pill on DocumentCard: bg-indigo-50 text-indigo-600
  4. Permission badge when set to "edit" (distinguished from "view" gray): bg-indigo-50 text-indigo-600 — matches topic pill pattern
  5. View/Edit toggle active state: bg-indigo-50 text-indigo-600

Destructive reserved for:

  1. "Remove access" inline link in ShareModal recipient row — text-red-500 hover:text-red-700
  2. Cloud delete failure modal — warning message text text-red-700, border accent on the modal (see Component Contracts below)
  3. Inline error text under inputs — text-red-600 text-xs

Source: codebase scan — colors extracted from ShareModal.vue, DocumentCard.vue, AuditLogTab.vue, AdminUsersTab.vue.


Copywriting Contract

Permission Dropdown (ShareModal — share creation row)

Element Copy
Dropdown label (aria-label) "Permission level"
Option: view "Can view"
Option: edit "Can edit"
Default selected "Can view"

View/Edit Toggle (ShareModal — per share row)

Element Copy
Toggle label: view active "View"
Toggle label: edit active "Edit"
Aria-label pattern "Change permission for {handle}"
Optimistic error (toggle fails) "Failed to update permission."

Cloud Delete Failure Modal

Element Copy
Modal heading "Cloud delete failed"
Body text "The file could not be deleted from {provider}. Remove it from DocuVault anyway? The file will remain on {provider}."
Primary CTA (remove from app) "Remove from app"
Secondary action (cancel) "Cancel"
Aria-label for modal "Cloud delete warning"

Note: {provider} is replaced at runtime with the cloud provider display name (e.g., "Google Drive", "OneDrive"). If the provider name is unavailable, fall back to "your cloud storage".

Audit Log — Daily Exports Section

Element Copy
Section label "Daily exports"
Dropdown label "Select date"
Dropdown placeholder "Choose a date"
Download button "Download"
Empty state (no exports in bucket) "No daily exports available."
Loading state (fetching list) "Loading exports…"

Audit Log — CSV Export Fix (behavior only, no copy change)

The "Export CSV" button label is unchanged. The behavior changes from window.location.href to fetch() + Blob URL. No new copy needed — the button already reads "Export CSV".

Audit Log — User Filter Label

Element Copy
Filter field label (was "User") "User handle"
Input placeholder (was "All users") "All users"

Shared Badge Fix (DocumentCard — no copy change)

The "Shared" pill copy is unchanged (Shared). Only the v-if condition changes from doc.share_count > 0 to doc.is_shared. No copy update.

Error States

Scenario Copy
Share creation — user not found "User not found. Check the handle and try again." (unchanged, already in ShareModal)
Share creation — already shared "This document is already shared with that user." (unchanged)
Share creation — generic error "Something went wrong. Please try again." (unchanged)
Permission update failed "Failed to update permission."
Daily export download failed "Download failed. Please try again."
CSV export request failed "Export failed. Please try again."
Cloud delete failure See modal copy above.

Destructive Actions

Action Confirmation Approach
Remove access (revoke share) Optimistic — immediate removal on click, restore on API failure. No confirmation dialog. Matches existing pattern in ShareModal.vue:handleRevoke().
Delete cloud document (default) Browser window.confirm() replaced by the cloud delete failure modal only when the API returns {"cloud_delete_failed": true}. Normal delete (MinIO or successful cloud delete) keeps the existing window.confirm() on DocumentView.vue.
"Remove from app" (cloud delete failure path) Confirmed via the cloud delete failure modal primary CTA. Single click on "Remove from app" proceeds without a second confirmation.

Component Contracts

C-1: Permission Dropdown in ShareModal (share creation row)

Location: frontend/src/components/sharing/ShareModal.vue — insert between the handle input and the "Share document" button within the flex gap-2 row.

Markup contract:

<select
  v-model="permission"
  aria-label="Permission level"
  class="border border-gray-300 rounded-lg px-3 py-2 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-indigo-500 shrink-0"
>
  <option value="view">Can view</option>
  <option value="edit">Can edit</option>
</select>
  • permission reactive ref defaults to "view"
  • Passed as permission: permission.value in the shareDocument() call
  • Width: shrink-0 — does not expand; native select width for 2 options is sufficient (~90px)
  • Sits between handle input (flex-1) and Share button (shrink-0)

C-2: View/Edit Toggle in ShareModal (per share row)

Location: frontend/src/components/sharing/ShareModal.vue — replace the static <span class="text-xs bg-gray-100 text-gray-600 px-2 py-1 rounded-full font-medium">view</span> in each recipient row.

Visual spec:

  • Two adjacent pill buttons: "View" and "Edit"
  • Active state: bg-indigo-50 text-indigo-600 font-medium
  • Inactive state: bg-gray-100 text-gray-600
  • Both use: text-xs px-2 py-1 rounded-full font-medium transition-colors
  • Wrapper: flex rounded-full overflow-hidden border border-gray-200 (pill group container)
  • Spacing: no gap between the two buttons (joined pills)

Interaction:

  • Clicking the inactive state calls PATCH /api/shares/{id} with { permission: "view" | "edit" }
  • Optimistic update: toggle state immediately on click, revert on API error
  • Loading state: button shows spinner inline while PATCH is in-flight; both toggle buttons opacity-50 pointer-events-none during in-flight state
  • Error: show error.value = "Failed to update permission." below the row (same text-xs text-red-600 mt-2 pattern)

C-3: Cloud Delete Failure Modal

Location: New component frontend/src/components/documents/CloudDeleteWarningModal.vue OR inline conditional block in DocumentView.vue — inline in DocumentView is preferred (mirrors how the inline delete confirmation panel works in AdminUsersTab).

Trigger: confirmDelete() in DocumentView.vue receives { cloud_delete_failed: true } from the delete API call. Instead of navigating away, set showCloudDeleteWarning.value = true.

Visual spec:

Fixed overlay: bg-black/40 flex items-center justify-center z-50
Panel: bg-white rounded-2xl shadow-xl p-6 max-w-sm w-full mx-4
  • Heading: text-lg font-semibold text-gray-900 mb-2 — "Cloud delete failed"
  • Body: text-sm text-gray-600 mb-6 — full warning sentence (see Copywriting Contract)
  • Warning icon: Heroicons ExclamationTriangleIcon w-5 h-5 text-amber-500 inline before heading, or text-red-600 — use amber-500 (warning, not error) to match the semantic distinction: this is a degraded-success, not a hard failure
  • Button row: flex gap-3 mt-4 justify-end
    • Primary ("Remove from app"): bg-red-600 hover:bg-red-700 text-white text-sm px-4 py-2 rounded-lg transition-colors
    • Secondary ("Cancel"): border border-gray-300 text-gray-700 text-sm px-4 py-2 rounded-lg hover:bg-gray-50 transition-colors
  • role="dialog" aria-modal="true" aria-labelledby on the panel
  • Click-outside (@click.self) closes the modal (same as ShareModal)
  • Pressing "Cancel" closes modal; document is NOT deleted. The pending delete is abandoned.
  • Pressing "Remove from app" calls DELETE /api/documents/{id}?remove_only=true, then navigates to / on success.

Warning icon Tailwind: text-amber-500 (Tailwind amber-500 = #F59E0B) — signals a recoverable warning state distinct from the hard red error color.

C-4: Daily Exports Section in AuditLogTab

Location: frontend/src/components/admin/AuditLogTab.vue — add as a new section below the existing pagination block.

Visual spec:

  • Section separator: <div class="border-t border-gray-100 mt-6 pt-6">
  • Section label: <h3 class="text-sm font-semibold text-gray-700 mb-3">Daily exports</h3>
  • Controls row: <div class="flex items-end gap-3">
    • Date select: <select class="text-sm border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500 bg-white"> populated from API response
    • Download button: <button class="bg-indigo-600 hover:bg-indigo-700 text-white text-sm px-4 py-2 rounded-lg disabled:opacity-50 transition-colors">Download</button>
    • Button disabled when no date is selected or download is in-flight
  • Loading state (fetching list): <p class="text-sm text-gray-400">Loading exports…</p> replaces the select
  • Empty state (bucket empty): <p class="text-sm text-gray-400 italic">No daily exports available.</p> replaces the select
  • Download in-flight: spinner inline in Download button (same animate-spin pattern as ShareModal submit button)

Date option format in select: YYYY-MM-DD as displayed value (e.g., "2026-05-30"). Options sorted newest-first. value attribute is the date string (used to construct the API call).

C-5: Audit Log User Filter Label Update

Location: frontend/src/components/admin/AuditLogTab.vue — filter bar.

  • Change <label> text from "User" to "User handle"
  • Change v-model binding from filters.user_id to filters.user_handle (or rename the reactive property)
  • Placeholder stays "All users"
  • No visual change otherwise; same text-sm border border-gray-300 rounded-lg px-3 py-2 input styling

State Inventory

Every interactive element in this phase must handle these states:

Component States Required
Permission dropdown (C-1) default (view), changed (edit), disabled (during submit)
View/Edit toggle (C-2) view-active, edit-active, loading (in-flight PATCH), error
Cloud delete warning modal (C-3) hidden, visible, removing (in-flight remove_only call)
Daily exports select (C-4) loading, empty, populated, selection-made
Daily exports download button (C-4) default, disabled (no selection), loading (in-flight download), error
CSV export button default, loading (in-flight fetch), error
Share creation button default, disabled (empty handle), loading, error

Accessibility Contract

  • All new interactive elements have an aria-label or are labelled by a visible <label> element
  • Cloud delete failure modal: role="dialog" aria-modal="true" aria-labelledby="cloud-delete-modal-title" on the panel; focus trapped within modal while open
  • View/Edit toggle buttons: each <button> has aria-pressed reflecting the current active state; wrapper has role="group" with aria-label="Permission"
  • Permission dropdown: aria-label="Permission level" (no visible label needed — sits inline)
  • All destructive buttons use text-red-600 or bg-red-600 and include descriptive accessible names
  • Minimum touch target min-h-[44px] min-w-[44px] applied to all icon-only buttons; inline text buttons (e.g., "Remove access", "Cancel") do not require the 44px minimum

Registry Safety

Registry Blocks Used Safety Gate
shadcn official none not applicable — shadcn not initialized
Third-party none not applicable

No component library. No registry. All components hand-built with Tailwind CSS following established project patterns.


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