---
phase: 32
slug: setup-sharing-system
status: approved
shadcn_initialized: false
preset: none
created: 2026-04-13
---

# Phase 32 — UI Design Contract

> Visual and interaction contract for the Setup Sharing System. Covers share button, share modal, visibility picker, and shared setup viewer.

---

## Design System

| Property | Value |
|----------|-------|
| Tool | none |
| Preset | not applicable |
| Component library | none (custom Tailwind components) |
| Icon library | Lucide via `LucideIcon` component from `lib/iconData` |
| Font | System font stack (inherited from existing app) |

---

## Spacing Scale

Declared values (must be multiples of 4):

| Token | Value | Usage |
|-------|-------|-------|
| xs | 4px | Icon gaps, inline padding |
| sm | 8px | Compact element spacing, gap-2 |
| md | 16px | Default element spacing, gap-4, p-4 |
| lg | 24px | Section padding, p-6 |
| xl | 32px | Layout gaps |
| 2xl | 48px | Major section breaks |
| 3xl | 64px | Page-level spacing |

Exceptions: none

---

## Typography

| Role | Size | Weight | Line Height |
|------|------|--------|-------------|
| Body | 14px (text-sm) | 400 | 1.5 |
| Label | 14px (text-sm) | 500 (font-medium) | 1.5 |
| Heading | 16px (text-base) | 600 (font-semibold) | 1.5 |
| Display | 20px (text-xl) | 600 (font-semibold) | 1.5 |

---

## Color

| Role | Value | Usage |
|------|-------|-------|
| Dominant (60%) | gray-50 (#f9fafb) | Page background, surfaces |
| Secondary (30%) | white (#ffffff) | Cards, modals, panels |
| Accent (10%) | gray-700 (#374151) | Primary action buttons (Add Items, share CTA) |
| Destructive | red-600 (#dc2626) | Revoke link, delete actions |

Accent reserved for: primary action buttons only (Add Items, Create Link)

### Visibility State Colors

| State | Icon | Text Color | Background |
|-------|------|------------|------------|
| Private | `lock` | gray-500 | gray-50 |
| Link | `link` | blue-600 | blue-50 |
| Public | `globe` | green-700 | green-50 |

---

## Component Specifications

### Share Button (replaces globe toggle)

**Desktop variant:**
- Position: same location as current globe toggle button in setup detail header bar
- Layout: `inline-flex items-center gap-1.5 px-3 py-2`
- Text: "Share" (always visible regardless of visibility state)
- Icon: varies by visibility state (see color table above), size 16px
- Background/text color: matches visibility state from color table
- Rounded: `rounded-lg`
- Hover: lighten background one shade

**Mobile variant:**
- Layout: `inline-flex items-center justify-center min-w-[44px] min-h-[44px] p-2`
- Icon only (no text), same icon/color logic as desktop
- `aria-label`: "Share settings"
- Rounded: `rounded-lg`

### Share Modal

**Overlay:** `fixed inset-0 z-50 bg-black/50 flex items-center justify-center`

**Modal container:**
- Desktop: `bg-white rounded-xl shadow-lg p-6 max-w-md mx-4 w-full`
- Max height: `max-h-[80vh] overflow-y-auto`
- Matches existing modal pattern (see ConfirmDialog.tsx, CreateThreadModal.tsx)

**Header:**
- Title: "Share Setup" (`text-lg font-semibold text-gray-900`)
- Close button: top-right, `LucideIcon name="x" size={20}`, `text-gray-400 hover:text-gray-600`
- Divider: `border-b border-gray-100 pb-4 mb-4`

**Visibility Picker Section:**
- Label: "Visibility" (`text-sm font-medium text-gray-700 mb-2`)
- Three radio-style buttons in a vertical stack, `gap-2`
- Each option: `flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition-colors`
  - Unselected: `border-gray-200 hover:border-gray-300`
  - Selected: `border-{state-color}-200 bg-{state-color}-50`
- Option layout:
  - Icon (size 20, state color)
  - Label (`text-sm font-medium text-gray-900`): "Private" / "Link sharing" / "Public"
  - Description (`text-xs text-gray-500`): "Only you can access" / "Anyone with the link" / "Visible on your profile"

**Create Link Section (visible when visibility is `link` or `public`):**
- Divider: `border-t border-gray-100 pt-4 mt-4`
- Section label: "Share Links" (`text-sm font-medium text-gray-700 mb-3`)
- Create row: `flex items-center gap-2`
  - Expiration dropdown: `select` element styled with `px-3 py-2 text-sm border border-gray-200 rounded-lg bg-white`
    - Options: "7 days", "14 days" (default), "30 days", "No expiration"
  - Create button: `px-4 py-2 bg-gray-700 hover:bg-gray-800 text-white text-sm font-medium rounded-lg`
    - Text: "Create Link"

**Active Links List:**
- Each link: `flex items-center gap-2 p-3 bg-gray-50 rounded-lg mb-2`
  - URL display: `text-sm text-gray-600 truncate flex-1` showing `/s/{token-prefix}...`
  - Expiration badge: `text-xs text-gray-400` showing "Expires {date}" or "No expiration"
  - Copy button: `p-1.5 text-gray-400 hover:text-gray-600 rounded` with `LucideIcon name="copy" size={16}`
    - After copy: icon changes to `check` with `text-green-500` for 2 seconds
  - Revoke button: `p-1.5 text-gray-400 hover:text-red-500 rounded` with `LucideIcon name="x" size={16}`

**Empty state (no links yet):**
- Text: "No share links yet" (`text-sm text-gray-400 text-center py-4`)

**Deactivation warning (when switching to private with active links):**
- Inline warning below visibility picker: `flex items-start gap-2 p-3 bg-amber-50 border border-amber-200 rounded-lg mt-2`
  - Icon: `LucideIcon name="alert-triangle" size={16}` in `text-amber-500`
  - Text: "Switching to private will deactivate all share links. They can be reactivated by switching back." (`text-sm text-amber-700`)

### Shared Setup Viewer

**Route:** `/setups/:setupId?share={token}`

**Shared banner:**
- Position: top of setup detail page, before header
- Layout: `flex items-center gap-2 px-4 py-2 bg-blue-50 border-b border-blue-100`
- Icon: `LucideIcon name="link" size={16}` in `text-blue-500`
- Text: "Shared setup" (`text-sm text-blue-700`)
- Appears only when viewing via share token

**Content:** Identical to public setup view (read-only item list with weight summary, no action buttons)

---

## Copywriting Contract

| Element | Copy |
|---------|------|
| Modal title | Share Setup |
| Visibility: Private label | Private |
| Visibility: Private description | Only you can access |
| Visibility: Link label | Link sharing |
| Visibility: Link description | Anyone with the link |
| Visibility: Public label | Public |
| Visibility: Public description | Visible on your profile |
| Create link CTA | Create Link |
| Empty links state | No share links yet |
| Deactivation warning | Switching to private will deactivate all share links. They can be reactivated by switching back. |
| Copy success toast | Link copied |
| Revoke confirmation | Revoke this share link? |
| Shared banner text | Shared setup |
| Expired link error | This share link has expired |
| Invalid link error | This share link is no longer valid |

---

## Registry Safety

| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| No external registries | N/A | N/A |

All components are custom Tailwind — no shadcn or third-party UI registry blocks.

---

## Responsive Behavior

| Breakpoint | Share Button | Share Modal |
|------------|-------------|-------------|
| Mobile (<768px) | Icon only, 44x44px touch target | Full width with mx-4 margin |
| Desktop (>=768px) | Icon + "Share" text | max-w-md centered |

---

## Interaction States

| Interaction | Behavior |
|-------------|----------|
| Open modal | Click share button, modal appears with current visibility pre-selected |
| Change visibility | Immediate API call on selection, optimistic update |
| Create link | API call, new link appears in list, auto-copy to clipboard |
| Copy link | Copy full URL to clipboard, show check icon for 2s |
| Revoke link | Confirmation prompt (reuse ConfirmDialog pattern), then remove from list |
| Close modal | Click X, click overlay, or press Escape |

---

## 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-04-13
