---
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
