--- phase: 6.2 slug: close-v1-sharing-cloud-delete-csv-export-gaps status: draft shadcn_initialized: false preset: none created: 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:** ``` ``` - `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 `view` 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: `