--- phase: 5 slug: cloud-storage-backends status: approved shadcn_initialized: false preset: none created: 2026-05-28 --- # Phase 5 — UI Design Contract > Visual and interaction contract for Phase 5: Cloud Storage Backends. > Generated by gsd-ui-researcher, verified by gsd-ui-checker. > Design system is vanilla Tailwind CSS (no shadcn) — matches Phases 1–4. --- ## Design System | Property | Value | |----------|-------| | Tool | none | | Preset | not applicable | | Component library | none | | Icon library | inline SVG, stroke-based, w-4 h-4 for nav, w-5 h-5 for status icons | | Font | system-ui (Tailwind default) | No shadcn gate applies — this is a Vue 3 / Tailwind project without shadcn. --- ## Spacing Scale All values match the existing 8-point scale already established in Phases 1–4: | Token | Value | Tailwind | Usage | |-------|-------|----------|-------| | xs | 4px | `gap-1`, `p-1` | Icon gaps, tight inline pairs | | sm | 8px | `gap-2`, `p-2` | Badge padding, button icon gap | | md | 16px | `gap-4`, `p-4` | Default element spacing | | lg | 24px | `p-6` | Section card inner padding | | xl | 32px | `p-8` | Page padding (SettingsView wrapper) | | 2xl | 48px | `mb-12` | Major section breaks (not used in this phase) | | 3xl | 64px | — | Page-level spacing (not used in this phase) | Exceptions: - Provider card row inner padding: `px-4 py-3` (12px top/bottom, 16px sides) — matches existing admin table row density - Modal inner padding: `p-6` (lg) - Touch target minimum: `min-h-[44px]` on all primary action buttons in modal (existing ConfirmBlock contract) - Badge/pill label padding: `px-2 py-0.5` (status badges) and `px-1.5 py-0.5` ("Recommended" tag) — optical micro-sizing for pill text, carry-forward from Phases 1–4 badge pattern - Sidebar section header icon gap: `gap-0.5` (2px) between chevron and nav link — optical icon alignment, matches existing Folders section pattern --- ## Typography All roles match the locked type scale from Phases 1–4. Exactly 4 distinct font size tokens are in use. | Role | Tailwind Classes | Usage in Phase 5 | |------|-----------------|------------------| | Page heading | `text-2xl font-semibold text-gray-900` | "Settings" page title (unchanged) | | Section heading | `text-xl font-semibold text-gray-800` | Tab content section headers (e.g. "Connected providers"); also: modal header `

` in CloudCredentialModal (same size role) | | Subsection heading | `text-sm font-semibold text-gray-900` | Provider name inside card, modal section labels | | Body | `text-sm text-gray-700 leading-relaxed` | Description text, modal helper text, provider display names | | Secondary body | `text-sm text-gray-600` | Status description, connected-at date | | Meta / label | `text-xs text-gray-500` | "Connected on …" date stamps, badge text on status pills | Font size token summary (4 tokens, no others permitted): - `text-2xl` — Page heading - `text-xl` — Section heading (also: modal header) - `text-sm` — Body / Subsection heading - `text-xs` — Meta / Label Weights in use: 400 (regular — body, secondary, meta) and 600 (semibold — headings, button labels, subsection labels). No additional weights introduced. Body line height: `leading-relaxed` (1.625) — applied to the `text-sm text-gray-700` role. Heading line height: `leading-tight` (1.25) — applied to `text-2xl` and `text-xl` heading roles. --- ## Color Matches 60/30/10 contract locked in Phases 1–4: | Role | Tailwind Value | Usage | |------|---------------|-------| | Dominant (60%) | `bg-gray-50` | Page background | | Secondary (30%) | `bg-white` + `border-gray-200` | Section cards, modal background, provider rows | | Accent (10%) | `indigo-600` / `indigo-700` | Primary buttons (Connect {provider}, Save, Reconnect {provider}), active tab underline, focus rings | | Destructive | `red-600` / `red-700` | Remove/Disconnect button, disconnect-all action, error banner background tint | Accent (`indigo-600`) is reserved for: - "Connect {provider}" button on unconnected providers - "Reconnect {provider}" button on REQUIRES_REAUTH providers - "Save" button in WebDAV/Nextcloud credential modal - Active tab indicator underline in SettingsView tab strip - Focus rings (`focus:ring-indigo-500`) on all inputs in the credential modal Additional semantic colors introduced by Phase 5: | Role | Tailwind Classes | Usage | |------|-----------------|-------| | Success (ACTIVE status) | `bg-green-100 text-green-700` | ACTIVE badge | | Warning (REQUIRES_REAUTH) | `bg-yellow-100 text-yellow-800` | REQUIRES_REAUTH badge, warning banner background | | Error (ERROR status) | `bg-red-100 text-red-700` | ERROR badge | | Neutral (not connected) | `bg-gray-100 text-gray-600` | "Not connected" badge | | Warning banner | `bg-yellow-50 border border-yellow-200 text-yellow-800` | REQUIRES_REAUTH inline banner within provider row | | Error banner | `bg-red-50 border border-red-200 text-red-700` | OAuth error banner (persistent, query param triggered) | --- ## Focal Point **Primary focal anchor for the Cloud Storage tab view:** The provider list rows — specifically the status badges and action buttons on the right side of each row. This is where the user's eye is directed: badges communicate current state at a glance, action buttons drive the next step. Visual hierarchy to support this: provider name (`font-semibold`) draws attention first, status badge follows as inline confirmation, action button column (`shrink-0`, right-aligned) provides a predictable vertical target for scanning. The "Disconnect all cloud storage" link at the bottom is intentionally low-contrast (plain `text-red-600` text link) to keep it out of the primary scan path. --- ## New UI Surfaces ### Surface 1: SettingsView Tab Conversion **Change:** Convert SettingsView from flat stacked sections to a 3-tab layout matching AdminView's pattern exactly. **Tab structure:** ``` tabs = [ { id: 'preferences', label: 'Preferences' }, { id: 'ai', label: 'AI Configuration' }, { id: 'cloud', label: 'Cloud Storage' }, ] ``` **Tab strip (copy AdminView verbatim):** ```
``` **Tab content mapping:** - `preferences` tab: existing "Document Preferences" section (pdf_open_mode radios) — extracted into `SettingsPreferencesTab.vue` - `ai` tab: existing "AI configuration" section (admin-managed notice) — extracted into `SettingsAiTab.vue` - `cloud` tab: new cloud storage management — `SettingsCloudTab.vue` **Active tab on mount:** `preferences` (first tab, user's existing default context) **Active tab override on OAuth redirect:** If `?cloud_connected=` or `?cloud_error=` query param is present in `onMounted`, set `activeTab = 'cloud'` before clearing the query params. **SettingsView wrapper:** Retain `p-8 max-w-3xl mx-auto`. The heading ("Settings") and description stay above the tab strip. --- ### Surface 2: SettingsCloudTab — Provider Cards **Component:** `frontend/src/components/settings/SettingsCloudTab.vue` **Layout:** A single section card (`bg-white border border-gray-200 rounded-xl p-6`) containing: 1. Section heading: `

Cloud Storage

` 2. Description: `

Connect a cloud storage provider to use as a document destination.

` 3. Provider list: one row per provider, stacked with `divide-y divide-gray-100` 4. "Disconnect all" action (shown only when at least one provider is ACTIVE or ERROR) **Provider row structure** (one row per provider, always shown — all 4 providers always visible): ```
{{ provider.label }}
``` **Provider labels and icons:** | Provider key | Display label | Icon description | |-------------|--------------|-----------------| | `google_drive` | Google Drive | Cloud icon with "G" text or inline SVG cloud, `text-blue-500` | | `onedrive` | OneDrive | Cloud icon, `text-sky-500` | | `nextcloud` | Nextcloud | Cloud icon, `text-orange-500` | | `webdav` | WebDAV server | Server/database icon, `text-gray-500` | Provider icons are inline SVG, `w-5 h-5`, stroke-based consistent with the project's existing icon vocabulary. Use the standard cloud path from the project's SVG set. Color is applied via `class` on the SVG element. **Status badges** (pill component, `StatusBadge.vue` or inline): | Status | Classes | Label | |--------|---------|-------| | `ACTIVE` | `bg-green-100 text-green-700 text-xs font-semibold px-2 py-0.5 rounded-full` | Active | | `REQUIRES_REAUTH` | `bg-yellow-100 text-yellow-800 text-xs font-semibold px-2 py-0.5 rounded-full` | Reconnect needed | | `ERROR` | `bg-red-100 text-red-700 text-xs font-semibold px-2 py-0.5 rounded-full` | Error | | `not_connected` | `bg-gray-100 text-gray-600 text-xs font-semibold px-2 py-0.5 rounded-full` | Not connected | **Action button label pattern:** All action buttons include the provider's display label as the noun object. The `{provider}` placeholder resolves to the display label from the provider labels table above (e.g., "Google Drive", "OneDrive", "Nextcloud", "WebDAV server"). **Action buttons per status:** | Status | Button label | Classes | |--------|--------------|---------| | `not_connected` | "Connect {provider}" | `bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold px-4 py-2 rounded-lg transition-colors` | | `ACTIVE` | "Remove {provider}" | `text-sm px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 text-gray-700 transition-colors` | | `REQUIRES_REAUTH` | "Reconnect {provider}" (primary) + "Remove {provider}" (secondary) | Reconnect: `bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold px-4 py-2 rounded-lg transition-colors`; Remove: `text-sm px-3 py-2 text-gray-500 hover:text-gray-700 transition-colors` | | `ERROR` | "Remove {provider}" | `text-sm px-4 py-2 border border-red-300 rounded-lg hover:bg-red-50 text-red-600 transition-colors` | Example rendered labels: "Connect Google Drive", "Reconnect OneDrive", "Remove Nextcloud", "Remove WebDAV server". **Loading state:** When a Connect or Reconnect button is clicked and the API call is in-flight, the button shows a spinner icon (`animate-spin`) replacing the label, and is `disabled opacity-50 cursor-not-allowed`. Width is held fixed to prevent layout shift (use `min-w-[160px]` on the button — wide enough for the longest provider name combination). **Connected-at date:** For ACTIVE and ERROR connections, show below the badge: `Connected {date}`. Use locale date format: `new Date(connection.connected_at).toLocaleDateString()`. Not shown for `not_connected` or `REQUIRES_REAUTH`. **REQUIRES_REAUTH inline banner:** Below the row (not inside it), conditionally rendered per provider: ```

Your {{ provider.label }} connection needs to be re-authorized. Click Reconnect {{ provider.label }} to restore access.

``` **"Disconnect all" action:** Rendered at the bottom of the section card, below the provider list. Only visible when at least one connection exists with status `ACTIVE` or `ERROR`. ```
``` Clicking opens a confirmation block using the existing `ConfirmBlock` component pattern: ``` Message: "This will permanently delete all cloud storage credentials. Your documents will remain in DocuVault, but cloud documents may become inaccessible." Confirm label: "Disconnect all" Cancel label: "Keep all connected" confirmClass: "bg-red-600 hover:bg-red-700 text-white" ``` **Empty state:** No explicit empty state needed — the tab always shows all 4 providers (3 OAuth + 1 WebDAV), each with "Not connected" status. The section heading description serves as the orientation copy. --- ### Surface 3: OAuth Success / Error Toast **Trigger:** `onMounted` in SettingsView reads `window.location.search` for `?cloud_connected={provider}` or `?cloud_error={message}`. After reading, replace the URL using `router.replace({ path: '/settings' })` to clean the query params. **Success toast:** ```

{{ providerLabel }} connected

Your files are now available in the sidebar.

``` - Auto-dismiss after 5000 ms (`setTimeout(() => oauthSuccessProvider = null, 5000)`) - Position: `fixed top-4 right-4 z-50` — top-right corner, above all content - Shadow: `shadow-lg` **Error banner** (persistent — requires manual dismissal): ```

Connection failed

{{ oauthError }}

Try connecting again. If the problem persists, check that the app has the correct permissions in your provider's account settings.

``` - Position: inline, rendered above the section card inside the "Cloud Storage" tab content - Persistent until manually dismissed or page navigation - The `?cloud_error=` value is URL-decoded and displayed as the error message body **Provider label mapping** (for toast message): | Query param value | Display label | |------------------|--------------| | `google_drive` | Google Drive | | `onedrive` | OneDrive | | `nextcloud` | Nextcloud | | `webdav` | WebDAV server | --- ### Surface 4: WebDAV / Nextcloud Credential Modal **Trigger:** Clicking "Connect {provider}" on the Nextcloud or WebDAV provider row. **Component:** `CloudCredentialModal.vue` — a centered modal overlay. **Overlay:** `fixed inset-0 bg-gray-900 bg-opacity-40 z-40 flex items-center justify-center p-4` **Modal panel:** `bg-white rounded-xl shadow-xl w-full max-w-md p-6` **Header:** ```

Connect {{ providerLabel }}

``` Note: The modal `

` uses `text-xl font-semibold` — the Section heading role from the typography scale. **Form fields:** 1. **Server URL** (Nextcloud and WebDAV only — not shown for OAuth providers): ```

Full WebDAV endpoint URL including username path segment.

``` 2. **Username:** ``` ``` 3. **Auth method toggle** (radio group, displayed between Username and Password fields): ```

Authentication method

``` Default selected: `app_password`. 4. **Password / App password field:** ``` ``` **Validation error display** (inline, shown below offending field): ```

{{ fieldError }}

``` **Connection test error** (shown above buttons after failed test): ```

Connection failed

{{ connectError }}

Check that the server URL is correct, the credentials are valid, and the server allows WebDAV access from external clients.

``` **Footer buttons:** ```
``` **Dismiss behavior:** Clicking the X button, "Keep current settings", or pressing Escape closes the modal. Clicking the overlay background also closes. When `saving` is true, all close actions are disabled (prevent accidental dismissal during the API call). --- ### Surface 5: Cloud Provider Nodes in Sidebar **Placement:** A new "Cloud Storage" section added to `AppSidebar.vue`, positioned immediately after the Folders section (after the `` closing the Folders collapsible block), before the Topics section. **Section header and collapsible pattern** (mirrors Folders section exactly): ```html
``` **`activeCloudConnections`:** Only connections with status `ACTIVE` are shown as tree nodes. `REQUIRES_REAUTH` and `ERROR` connections are not shown in the sidebar (user must go to Settings to resolve them). **`CloudProviderTreeItem.vue`** — new component, mirrors `FolderTreeItem.vue` structure: ```html ``` **Provider icon colors:** | Provider | `providerIconColor` class | |----------|--------------------------| | `google_drive` | `text-blue-500` | | `onedrive` | `text-sky-500` | | `nextcloud` | `text-orange-500` | | `webdav` | `text-gray-500` | **Loading behavior for cloud folder expansion:** - On first expand: show `text-xs text-gray-400` "Loading…" at `pl-12` (depth 1 * 12 = 12px left + icon space) - On success: render `CloudFolderTreeItem` nodes - On error: show `text-xs text-red-500` "Failed to load — tap to retry" at `pl-12`, clicking retries the fetch - 60-second TTL cache handled server-side; frontend always calls the API on expand if `childrenLoaded === false` **`CloudFolderTreeItem.vue`:** A simplified version of `FolderTreeItem.vue` for cloud folder nodes. Uses a plain folder icon (`text-gray-400`). Navigates to `/cloud/{provider}/{folder_id}` on click. Indentation via `depth * 12` matching the existing pattern. Lazy-loads nested children via the same expand/toggle mechanism. **Depth / indentation:** Cloud tree nodes use the same `depth * 12` px left-padding formula as `FolderTreeItem`. Provider root is at depth 1 (same level as local root folders). Cloud sub-folders start at depth 2. --- ## Copywriting Contract ### Primary CTA labels All action buttons that target a specific provider use the `{verb} {provider}` pattern. The `{provider}` token resolves to the display label from the provider labels table. | Action | Button label pattern | Example | |--------|---------------------|---------| | Connect OAuth provider | "Connect {provider}" | "Connect Google Drive" | | Connect WebDAV / Nextcloud (modal submit) | "Connect {provider}" | "Connect Nextcloud" | | Reconnect (REQUIRES_REAUTH) | "Reconnect {provider}" | "Reconnect OneDrive" | | Remove single active/error provider | "Remove {provider}" | "Remove Google Drive" | | Remove single from REQUIRES_REAUTH row (secondary) | "Remove {provider}" | "Remove Nextcloud" | | Disconnect all providers (trigger link) | "Disconnect all cloud storage" | (no provider token — applies to all) | | Disconnect all (ConfirmBlock confirm label) | "Disconnect all" | | | Cancel WebDAV/Nextcloud modal | "Keep current settings" | | | Dismiss disconnect-single ConfirmBlock | "Keep connected" | | | Dismiss disconnect-all ConfirmBlock | "Keep all connected" | | ### Status badge labels | Status | Badge label | |--------|------------| | `ACTIVE` | Active | | `REQUIRES_REAUTH` | Reconnect needed | | `ERROR` | Error | | `not_connected` | Not connected | ### Empty states | Location | Copy | |----------|------| | Cloud tab, no active connections | (no explicit empty state — all 4 providers always shown with "Not connected" badge) | | Sidebar cloud section, no active connections | "No cloud storage connected" | | Cloud folder tree node, empty provider root | "Empty" | | Cloud folder tree node, load error | "Failed to load — tap to retry" | ### Error messages | Error scenario | Copy | |---------------|------| | WebDAV connection test failed (modal inline) | "Connection failed" (heading) + server error message (body) + "Check that the server URL is correct, the credentials are valid, and the server allows WebDAV access from external clients." | | OAuth callback error (`?cloud_error=`) | "Connection failed" (heading) + decoded error value (body) + "Try connecting again. If the problem persists, check that the app has the correct permissions in your provider's account settings." | | Disconnect failed (API error) | Toast or inline: "Failed to disconnect. Please try again." | | Cloud folder load failed (sidebar tree) | "Failed to load — tap to retry" | ### REQUIRES_REAUTH inline banner "Your {provider label} connection needs to be re-authorized. Click **Reconnect {provider label}** to restore access." ### Disconnect single confirmation Uses existing `ConfirmBlock` component: - Message: "This will permanently remove your {provider label} credentials from DocuVault. Your cloud documents will remain in your {provider label} account." - Confirm label: "Remove {provider label}" (e.g. "Remove Google Drive") - Cancel label: "Keep connected" - `confirmClass`: `"bg-red-600 hover:bg-red-700 text-white"` ### Disconnect-all confirmation Uses existing `ConfirmBlock` component: - Message: "This will permanently delete all cloud storage credentials. Your documents will remain in DocuVault, but cloud documents may become inaccessible." - Confirm label: "Disconnect all" - Cancel label: "Keep all connected" - `confirmClass`: `"bg-red-600 hover:bg-red-700 text-white"` ### OAuth success toast "{provider label} connected" (heading) + "Your files are now available in the sidebar." (body) --- ## Component Inventory New components introduced in Phase 5: | Component path | Purpose | Extends | |---------------|---------|---------| | `frontend/src/components/settings/SettingsCloudTab.vue` | Cloud Storage tab content | New | | `frontend/src/components/settings/SettingsPreferencesTab.vue` | Extracted Preferences tab (pdf_open_mode) | Extracted from SettingsView | | `frontend/src/components/settings/SettingsAiTab.vue` | Extracted AI config tab | Extracted from SettingsView | | `frontend/src/components/cloud/CloudCredentialModal.vue` | WebDAV / Nextcloud credential input modal | New | | `frontend/src/components/cloud/CloudProviderTreeItem.vue` | Provider root node in sidebar tree | Mirrors FolderTreeItem | | `frontend/src/components/cloud/CloudFolderTreeItem.vue` | Cloud sub-folder node in sidebar tree | Mirrors FolderTreeItem | Modified components: | Component path | Change | |---------------|--------| | `frontend/src/views/SettingsView.vue` | Convert to 3-tab layout; add OAuth param handling in `onMounted`; add success toast + error banner state | | `frontend/src/components/layout/AppSidebar.vue` | Add "Cloud Storage" collapsible section below Folders | New Pinia store: | Store | State | |-------|-------| | `frontend/src/stores/cloudConnections.js` | `connections: []`, `loading: bool`, `error: string\|null`. Actions: `fetchConnections()`, `disconnect(id)`, `disconnectAll()` | --- ## Registry Safety No shadcn registry. No third-party component registries. All UI is custom Tailwind + inline SVG matching the existing project pattern. | Registry | Blocks Used | Safety Gate | |----------|-------------|-------------| | shadcn official | none | not applicable | | third-party | none | not applicable | --- ## Interaction States Summary | Component | States | |-----------|--------| | Connect {provider} button | default, hover, loading (spinner + disabled), disabled (already connected) | | Remove {provider} button | default, hover, loading (spinner + disabled) | | Reconnect {provider} button | default, hover, loading (spinner + disabled) | | WebDAV modal "Connect {provider}" button | default, hover, loading (spinner + disabled) | | WebDAV modal inputs | default, focus (indigo ring), error (red border + error text below) | | Status badge | static — no hover state | | Success toast | visible (auto-dismiss 5s), dismiss on "Dismiss notification" X button | | Error banner | visible (persistent), dismiss on "Dismiss error" X button | | REQUIRES_REAUTH banner | visible when status === REQUIRES_REAUTH, disappears after reconnect | | Cloud tree provider node | default, hover (bg-gray-100), active/selected (bg-indigo-50 text-indigo-700) | | Cloud tree expand arrow | default (text-gray-400), hover (text-gray-600), expanded (rotate-90) | | Cloud folder tree loading | "Loading…" text (text-xs text-gray-400) | | Cloud folder tree error | "Failed to load — tap to retry" (text-xs text-red-500, cursor-pointer) | --- ## Checker Sign-Off - [x] Dimension 1 Copywriting: PASS - [x] Dimension 2 Visuals: PASS - [x] Dimension 3 Color: PASS - [x] Dimension 4 Typography: PASS - [x] Dimension 5 Spacing: PASS - [x] Dimension 6 Registry Safety: PASS **Approval:** approved 2026-05-28