- 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>
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.vueline 43. - Share row inline items:
py-2(8px vertical) per row — matches existingShareModal.vuerecipient list rhythm. - Permission dropdown in share creation row: no extra spacing; sits inline within the existing
flex gap-2row.
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):
- Primary CTA buttons: "Share document", "Apply filters", "Export CSV", "Download" —
bg-indigo-600 hover:bg-indigo-700 text-white - Focus rings on all text inputs and selects:
focus:ring-2 focus:ring-indigo-500 - "Shared" indicator pill on DocumentCard:
bg-indigo-50 text-indigo-600 - Permission badge when set to "edit" (distinguished from "view" gray):
bg-indigo-50 text-indigo-600— matches topic pill pattern - View/Edit toggle active state:
bg-indigo-50 text-indigo-600
Destructive reserved for:
- "Remove access" inline link in ShareModal recipient row —
text-red-500 hover:text-red-700 - Cloud delete failure modal — warning message text
text-red-700, border accent on the modal (see Component Contracts below) - 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>
permissionreactive ref defaults to"view"- Passed as
permission: permission.valuein theshareDocument()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-noneduring in-flight state - Error: show
error.value = "Failed to update permission."below the row (sametext-xs text-red-600 mt-2pattern)
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
ExclamationTriangleIconw-5 h-5 text-amber-500 inline before heading, ortext-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
- Primary ("Remove from app"):
role="dialog"aria-modal="true"aria-labelledbyon 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
- Date select:
- 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-spinpattern 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-modelbinding fromfilters.user_idtofilters.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-2input 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-labelor 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>hasaria-pressedreflecting the current active state; wrapper hasrole="group"witharia-label="Permission" - Permission dropdown:
aria-label="Permission level"(no visible label needed — sits inline) - All destructive buttons use
text-red-600orbg-red-600and 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