- Collapse font weights from 3 to 2: drop font-medium (500), use font-semibold (600) for row primary names, labels, column headers; font-normal (400) for body - Fix sm spacing token usage: replace p-1.5 (6px) with p-2 (8px) to match declared 8px value - Update health badge in connection root list from font-medium to font-semibold Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
267 lines
12 KiB
Markdown
267 lines
12 KiB
Markdown
---
|
||
phase: 12
|
||
slug: cloud-resource-foundation
|
||
status: draft
|
||
shadcn_initialized: false
|
||
preset: none
|
||
created: 2026-06-18
|
||
---
|
||
|
||
# Phase 12 — UI Design Contract
|
||
|
||
> Visual and interaction contract for Phase 12: cloud-resource-foundation.
|
||
> Generated by gsd-ui-researcher, verified by gsd-ui-checker.
|
||
|
||
---
|
||
|
||
## Design System
|
||
|
||
| Property | Value |
|
||
|----------|-------|
|
||
| Tool | none — manual Tailwind CSS |
|
||
| Preset | not applicable |
|
||
| Component library | none (Vue 3 custom components) |
|
||
| Icon library | AppIcon.vue (Heroicons-based, internal wrapper) |
|
||
| Font | system-ui / Inter (Tailwind default sans stack) |
|
||
|
||
No `components.json` was found. The project uses manual Tailwind CSS v3 with `@tailwindcss/forms`. No shadcn initialization is needed or applicable. All new UI must match the existing `StorageBrowser.vue` component patterns.
|
||
|
||
---
|
||
|
||
## Spacing Scale
|
||
|
||
Declared values (must be multiples of 4):
|
||
|
||
| Token | Value | Usage |
|
||
|-------|-------|-------|
|
||
| xs | 4px | Icon gaps, inline badge padding (`gap-1`, `px-1`) |
|
||
| sm | 8px | Compact element spacing, toolbar icon buttons (`p-2`, `gap-2`) |
|
||
| md | 16px | Default element spacing, row padding (`px-4`) |
|
||
| lg | 24px | Section padding, horizontal gutters (`px-6`) |
|
||
| xl | 32px | Layout gaps |
|
||
| 2xl | 48px | Major section breaks |
|
||
| 3xl | 64px | Page-level spacing |
|
||
|
||
Exceptions:
|
||
- Touch target minimum: 36×36px (`min-w-[36px] min-h-[36px]`) for row action buttons on mobile — matches existing `StorageBrowser.vue` pattern.
|
||
- Sync indicator: inline next to breadcrumb or toolbar, 20×20px icon (`w-5 h-5`), no extra surrounding padding.
|
||
|
||
---
|
||
|
||
## Typography
|
||
|
||
| Role | Size | Weight | Line Height |
|
||
|------|------|--------|-------------|
|
||
| Body | 14px (`text-sm`) | 400 regular (`font-normal`) | 1.5 |
|
||
| Label / metadata | 12px (`text-xs`) | 600 semibold (`font-semibold`) | 1.4 |
|
||
| Row primary name | 14px (`text-sm`) | 600 semibold (`font-semibold`) | 1.5 |
|
||
| Column header | 12px (`text-xs`) | 600 semibold, `uppercase tracking-wider` | 1.4 |
|
||
|
||
Two weights only: `font-normal` (400) for body, `font-semibold` (600) for all labels, metadata, column headers, and row primary names. `font-medium` (500) is not used.
|
||
|
||
Source: extracted from `StorageBrowser.vue` — do not deviate without updating this contract.
|
||
|
||
---
|
||
|
||
## Color
|
||
|
||
| Role | Tailwind Token | Usage |
|
||
|------|----------------|-------|
|
||
| Dominant (60%) | `white` / `bg-white` | Page background, sticky toolbar, row base |
|
||
| Secondary (30%) | `gray-50` / `gray-100` | List header row, alternating hover state, sidebar |
|
||
| Accent (10%) | `indigo-500` / `indigo-600` | See reserved list below |
|
||
| Warning | `amber-400` / `amber-500` | Folder icons, new-folder input border |
|
||
| Destructive | `red-500` / `red-50` | Delete action hover, error text |
|
||
|
||
**Accent (`indigo`) reserved exclusively for:**
|
||
- Primary action buttons (e.g., "Connect account", "Browse")
|
||
- Focus rings (`focus-visible:ring-indigo-500`) on all interactive elements
|
||
- Active/selected navigation items
|
||
- Inline text links and "Clear search" calls-to-action
|
||
- Connection root icon in breadcrumb bar
|
||
|
||
**Do NOT use indigo for:**
|
||
- Disabled/unsupported action controls (use `gray-400` text on `gray-100` background)
|
||
- Temporary-failure warning state (use `amber-600` text on `amber-50` background)
|
||
- Metadata labels or column headers
|
||
|
||
### New semantic colors for Phase 12
|
||
|
||
| Semantic role | Tailwind token | Applied to |
|
||
|---------------|----------------|------------|
|
||
| Unsupported (permanent) | `text-gray-400 bg-gray-100` | Disabled action buttons for structurally unsupported operations |
|
||
| Temporarily blocked | `text-amber-600 bg-amber-50 border-amber-200` | Disabled action buttons for recoverable failures (scope, auth, offline) |
|
||
| Refreshing | `text-indigo-400` (animated) | Breadcrumb sync spinner |
|
||
| Up to date | `text-green-500` | Breadcrumb sync indicator after successful refresh |
|
||
| Connection warning | `text-amber-500` | Breadcrumb sync indicator on refresh failure |
|
||
|
||
Source: D-09 from CONTEXT.md — grey = structurally unsupported; amber/warning = temporarily blocked.
|
||
|
||
---
|
||
|
||
## Component Inventory
|
||
|
||
These components are extended or introduced in Phase 12. All must follow existing architecture rules: views are thin data providers; smart components receive props and emit actions.
|
||
|
||
### Extended: `StorageBrowser.vue`
|
||
|
||
**New props (data-driven):**
|
||
|
||
| Prop | Type | Description |
|
||
|------|------|-------------|
|
||
| `capabilities` | `Record<ActionKey, CapabilityState>` | Map of action name to `{ state: 'supported' \| 'unsupported' \| 'temporarily_unavailable', message: string, reasonCode: string }` |
|
||
| `folderFreshness` | `'fresh' \| 'refreshing' \| 'warning'` | Current sync state of the displayed folder |
|
||
| `lastRefreshedAt` | `string \| null` | ISO timestamp for tooltip on sync indicator |
|
||
| `connectionRoot` | `{ id: string, displayName: string, providerType: string }` | Identity of the current connection root |
|
||
| `byteAvailability` | `Record<string, 'cloud_only' \| 'cached'>` | Per-item byte state keyed by DocuVault item UUID |
|
||
|
||
**Existing `mode` prop behavior change:** replace `mode === 'local'` action-hiding with capability rendering. Local mode supplies a full-supported capability set; cloud mode supplies provider-normalized capabilities. One rendering path serves both.
|
||
|
||
**New emitted events:**
|
||
|
||
| Event | Payload | Description |
|
||
|-------|---------|-------------|
|
||
| `capability-explain` | `{ actionKey: string, message: string }` | User tapped/focused a disabled control on touch |
|
||
|
||
### Extended: `BreadcrumbBar.vue`
|
||
|
||
Adds a sync state indicator immediately after the last breadcrumb segment:
|
||
- Refreshing: `AppSpinner` sized 16px (`w-4 h-4`) in `text-indigo-400`
|
||
- Up to date: checkmark icon 16px in `text-green-500`, fades out after 3 s
|
||
- Connection warning: warning triangle icon 16px in `text-amber-500`, persists
|
||
|
||
Tooltip on the indicator (desktop hover, touch tap): `"Last updated [relative time]"` or `"Refresh failed — retrying"`.
|
||
|
||
The breadcrumb root segment shows the connection display name and a small provider/cloud icon (16px) before it. No separate `Cloud` prefix segment.
|
||
|
||
### New: Disabled action button pattern
|
||
|
||
Unsupported and temporarily-blocked actions render as `<button>` elements with `aria-disabled="true"` and no native `disabled` attribute. This ensures they remain focusable and tappable for explanation delivery.
|
||
|
||
Unsupported (permanent):
|
||
```
|
||
text-gray-400 bg-transparent cursor-not-allowed
|
||
aria-disabled="true"
|
||
```
|
||
|
||
Temporarily blocked:
|
||
```
|
||
text-amber-600 bg-amber-50 border border-amber-200 cursor-not-allowed
|
||
aria-disabled="true"
|
||
```
|
||
|
||
On click/tap while `aria-disabled="true"`: emit `capability-explain` with the action's message. The parent view displays the explanation in a tooltip (desktop) or an inline dismissable notice below the toolbar (mobile touch).
|
||
|
||
Tooltip/notice format: plain sentence, e.g. `"Rename is not supported by this server"` or `"Reconnect with write access to enable uploads"`.
|
||
|
||
### Connection display name disambiguation
|
||
|
||
When a connection's `displayName` equals the plain provider label (e.g. `OneDrive`) and another connection shares the same default name, append the connection UUID's first 4 characters in `text-gray-400 text-xs` after the name: `OneDrive · a3f2`. This clears once the user renames either connection. Source: D-03 from CONTEXT.md.
|
||
|
||
### Byte availability indicator
|
||
|
||
Per-row compact icon at the far right of file rows (folders have no indicator):
|
||
- `cloud_only`: no marker (absence is the default state — no visual noise)
|
||
- `cached`: download/cache icon 14px (`w-3.5 h-3.5`) in `text-indigo-400`; tooltip reads `"Bytes cached in DocuVault storage — counts toward your quota"`
|
||
|
||
Source: D-17 from CONTEXT.md.
|
||
|
||
---
|
||
|
||
## Interaction Contracts
|
||
|
||
### Capability-disabled controls (CLOUD-08)
|
||
|
||
| Trigger | Behavior |
|
||
|---------|----------|
|
||
| Desktop hover over `aria-disabled` button | Show tooltip with explanation message |
|
||
| Keyboard focus on `aria-disabled` button | Show tooltip with explanation message |
|
||
| Touch tap on `aria-disabled` button | Show inline dismissable notice below toolbar; suppress action |
|
||
| Click on active (enabled) button | Emit action normally |
|
||
|
||
Never use native `disabled` attribute — disabled buttons do not receive focus or click events, which breaks touch explanation delivery. Source: RESEARCH.md planning risk #7.
|
||
|
||
### Folder freshness / background reconciliation (SYNC-01, D-11, D-12)
|
||
|
||
| State | Breadcrumb indicator | Row behavior |
|
||
|-------|----------------------|-------------|
|
||
| `refreshing` | Spinner (indigo-400, 16px, animated) | Rows remain interactive; no skeleton overlay |
|
||
| `fresh` | Checkmark (green-500, 16px, fades after 3 s) | Normal |
|
||
| `warning` | Warning triangle (amber-500, 16px, persistent) | Rows remain interactive; connection warning banner shown below toolbar |
|
||
|
||
Connection warning banner (shown when `folderFreshness === 'warning'`):
|
||
- Background: `bg-amber-50 border-b border-amber-200`
|
||
- Text: `text-amber-800 text-sm` — `"Could not refresh — showing cached results from [relative time]. Retrying automatically."`
|
||
- No dismiss button; disappears when state returns to `refreshing` or `fresh`
|
||
- Source: D-14 from CONTEXT.md.
|
||
|
||
### Connection root list (Cloud Storage landing)
|
||
|
||
- Displays one row per connected account using the same `StorageBrowser` grid
|
||
- Provider icon (24px) + connection display name + connection health badge
|
||
- Health badge: `text-green-700 bg-green-50 rounded-full text-xs font-semibold px-2 py-0.5` for connected; `text-amber-700 bg-amber-50` for warning; `text-red-700 bg-red-50` for failed
|
||
- Clicking a connection root navigates into that connection using connection UUID in the route
|
||
|
||
### Session-scoped last-visited folder (D-05)
|
||
|
||
Last visited folder is stored in `sessionStorage` only, keyed by `connectionId`. On fresh tab/session, navigation starts at the connection root. No persistent state.
|
||
|
||
---
|
||
|
||
## Copywriting Contract
|
||
|
||
| Element | Copy |
|
||
|---------|------|
|
||
| Primary CTA (connection root list, no connections) | "Connect cloud storage" |
|
||
| Primary CTA (toolbar, enter connection) | "Browse files" |
|
||
| Empty state heading (no connections yet) | "No cloud storage connected" |
|
||
| Empty state body (no connections yet) | "Connect OneDrive, Google Drive, Nextcloud, or a WebDAV server to browse your files here." |
|
||
| Empty state heading (empty folder) | "This folder is empty" |
|
||
| Empty state body (empty folder) | "Files and folders you add to this location in [provider name] will appear here." |
|
||
| Error state — refresh failure | "Could not refresh — showing cached results from [relative time]. Retrying automatically." |
|
||
| Error state — connection offline | "Could not reach [connection display name]. Check your connection or reconnect your account." |
|
||
| Unsupported action tooltip — permanent | "[Action] is not supported by this server" (e.g. "Rename is not supported by this server") |
|
||
| Temporarily blocked tooltip — insufficient scope | "Reconnect with write access to enable [action]" |
|
||
| Temporarily blocked tooltip — read-only | "This connection is read-only" |
|
||
| Byte cache tooltip | "Bytes cached in DocuVault storage — counts toward your quota" |
|
||
| Sync indicator tooltip — up to date | "Last updated [relative time]" |
|
||
| Sync indicator tooltip — warning | "Refresh failed — retrying" |
|
||
| Connection name disambiguation suffix | "[Display name] · [4-char UUID prefix]" |
|
||
|
||
No destructive actions in Phase 12 scope. Delete (CLOUD-07) is Phase 13.
|
||
|
||
---
|
||
|
||
## Accessibility Contract
|
||
|
||
- All interactive elements must have `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1` (matching existing pattern in `StorageBrowser.vue`).
|
||
- `aria-disabled="true"` controls must be keyboard-reachable and respond to Enter/Space with the explanation, not the action.
|
||
- Sync indicator must have `aria-label` reflecting current state: `"Refreshing folder"`, `"Folder up to date"`, or `"Folder refresh failed"`.
|
||
- Provider/cloud icon in breadcrumb must have `aria-hidden="true"` (decorative).
|
||
- Byte availability icon must have `title` attribute matching tooltip copy.
|
||
- Minimum touch target: 36×36px for all action buttons.
|
||
|
||
---
|
||
|
||
## Registry Safety
|
||
|
||
| Registry | Blocks Used | Safety Gate |
|
||
|----------|-------------|-------------|
|
||
| shadcn official | none — not initialized | not applicable |
|
||
| Third-party | none | not applicable |
|
||
|
||
No third-party component registries are used. All components are hand-authored Tailwind Vue SFCs following existing 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
|