---
phase: 32-setup-sharing-system
plan: 03
type: execute
wave: 3
depends_on: [01, 02]
files_modified:
  - src/client/components/ShareModal.tsx
  - src/client/hooks/useShares.ts
  - src/client/routes/setups/$setupId.tsx
autonomous: true
requirements:
  - TBD

must_haves:
  truths:
    - "Share button on setup detail page reflects current visibility state (lock/link/globe icon with state color)"
    - "Clicking share button opens the share modal"
    - "Share modal shows visibility picker with three options (private/link/public)"
    - "Changing visibility in modal immediately updates via API"
    - "Share modal shows create link form when visibility is link or public"
    - "Creating a share link auto-copies to clipboard and shows in links list"
    - "Each share link has copy and revoke actions"
    - "Switching to private shows deactivation warning"
    - "Share modal works on both desktop and mobile"
  artifacts:
    - path: "src/client/components/ShareModal.tsx"
      provides: "Share modal with visibility picker, link creation, and link management"
      exports: ["ShareModal"]
    - path: "src/client/hooks/useShares.ts"
      provides: "React Query hooks for share link CRUD"
      exports: ["useShareLinks", "useCreateShareLink", "useRevokeShareLink"]
  key_links:
    - from: "src/client/components/ShareModal.tsx"
      to: "src/client/hooks/useShares.ts"
      via: "Share CRUD mutations"
      pattern: "useShareLinks|useCreateShareLink|useRevokeShareLink"
    - from: "src/client/routes/setups/$setupId.tsx"
      to: "src/client/components/ShareModal.tsx"
      via: "Modal open state and render"
      pattern: "ShareModal|shareModalOpen"
---

<objective>
Create the share modal component and wire it into the setup detail page, replacing the temporary visibility badge from Plan 01 with a full share button that opens a Google Docs-style share dialog.

Purpose: This implements the primary user-facing share UX (D-13 through D-16). Users manage visibility and share links from a single modal.

Output: ShareModal component, share hooks, and updated setup detail page.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/32-setup-sharing-system/32-CONTEXT.md
@.planning/phases/32-setup-sharing-system/32-UI-SPEC.md
@.planning/phases/32-setup-sharing-system/32-01-SUMMARY.md
@.planning/phases/32-setup-sharing-system/32-02-SUMMARY.md

<interfaces>
<!-- Key types and contracts from Plans 01 and 02. -->

Share API endpoints (from Plan 02):
```
POST   /api/setups/:id/shares        → { id, setupId, token, permission, expiresAt, createdAt, revokedAt }
GET    /api/setups/:id/shares         → Array<Share>
DELETE /api/setups/:id/shares/:shareId → { id, setupId, token, ..., revokedAt }
```

Setup update endpoint (from Plan 01):
```
PUT /api/setups/:id → accepts { name, visibility } → returns updated setup
```

From src/client/lib/api.ts:
```typescript
export function apiGet<T>(url: string): Promise<T>;
export function apiPost<T>(url: string, body: unknown): Promise<T>;
export function apiDelete<T>(url: string): Promise<T>;
```

From src/client/lib/iconData.tsx:
```typescript
export function LucideIcon({ name, size, className }: { name: string; size?: number; className?: string }): JSX.Element;
// Available icons: lock, link, globe, copy, check, x, alert-triangle, share-2, plus
```

From src/client/hooks/useSetups.ts:
```typescript
export function useUpdateSetup(setupId: number): UseMutationResult;
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create share hooks for React Query</name>
  <files>src/client/hooks/useShares.ts</files>
  <read_first>src/client/hooks/useSetups.ts, src/client/lib/api.ts</read_first>
  <action>
Create `src/client/hooks/useShares.ts` following existing hook patterns in `useSetups.ts`:

```typescript
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiDelete, apiGet, apiPost } from "../lib/api";

interface ShareLink {
  id: number;
  setupId: number;
  token: string;
  permission: string;
  expiresAt: string | null;
  createdAt: string;
  revokedAt: string | null;
}

export function useShareLinks(setupId: number | null) {
  return useQuery({
    queryKey: ["shares", setupId],
    queryFn: () => apiGet<ShareLink[]>(`/api/setups/${setupId}/shares`),
    enabled: !!setupId,
  });
}

export function useCreateShareLink(setupId: number) {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: (data: { expiresInDays: number | null }) =>
      apiPost<ShareLink>(`/api/setups/${setupId}/shares`, data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["shares", setupId] });
    },
  });
}

export function useRevokeShareLink(setupId: number) {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: (shareId: number) =>
      apiDelete<ShareLink>(`/api/setups/${setupId}/shares/${shareId}`),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["shares", setupId] });
    },
  });
}
```
  </action>
  <verify>
    <automated>grep -q "useShareLinks" src/client/hooks/useShares.ts && grep -q "useCreateShareLink" src/client/hooks/useShares.ts && grep -q "useRevokeShareLink" src/client/hooks/useShares.ts && echo "PASS" || echo "FAIL"</automated>
  </verify>
  <acceptance_criteria>
    - `src/client/hooks/useShares.ts` exports `useShareLinks`, `useCreateShareLink`, `useRevokeShareLink`
    - Hooks follow same patterns as `useSetups.ts` (React Query, apiGet/apiPost/apiDelete)
    - Query invalidation on mutations targets `["shares", setupId]` key
  </acceptance_criteria>
  <done>Share hooks created with query and mutation patterns</done>
</task>

<task type="auto">
  <name>Task 2: Create ShareModal component and wire into setup detail page</name>
  <files>src/client/components/ShareModal.tsx, src/client/routes/setups/$setupId.tsx</files>
  <read_first>src/client/routes/setups/$setupId.tsx, src/client/components/ConfirmDialog.tsx, src/client/components/CreateThreadModal.tsx, src/client/hooks/useSetups.ts, .planning/phases/32-setup-sharing-system/32-UI-SPEC.md</read_first>
  <action>
**Create `src/client/components/ShareModal.tsx`** following the 32-UI-SPEC.md contract exactly:

Props:
```typescript
interface ShareModalProps {
  isOpen: boolean;
  onClose: () => void;
  setupId: number;
  currentVisibility: "private" | "link" | "public";
  onVisibilityChange: (visibility: "private" | "link" | "public") => void;
}
```

Component structure (all per 32-UI-SPEC.md):

1. **Overlay:** `fixed inset-0 z-50 bg-black/50 flex items-center justify-center`. Click overlay to close. Listen for Escape key.

2. **Modal container:** `bg-white rounded-xl shadow-lg p-6 max-w-md mx-4 w-full max-h-[80vh] overflow-y-auto`

3. **Header:** "Share Setup" in `text-lg font-semibold text-gray-900`, close X button top-right.

4. **Visibility Picker:** Three radio-style buttons in vertical stack with `gap-2`:
   - Each: `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`
   - Private: lock icon (gray-500), "Private", "Only you can access"
   - Link: link icon (blue-600), "Link sharing", "Anyone with the link"
   - Public: globe icon (green-700), "Public", "Visible on your profile"
   - On click: call `onVisibilityChange(newVisibility)` (immediate API call)

5. **Deactivation warning** (show when selecting private while links exist):
   - `flex items-start gap-2 p-3 bg-amber-50 border border-amber-200 rounded-lg mt-2`
   - alert-triangle icon in text-amber-500
   - "Switching to private will deactivate all share links. They can be reactivated by switching back."

6. **Share Links Section** (visible when visibility is "link" or "public"):
   - Divider: `border-t border-gray-100 pt-4 mt-4`
   - Label: "Share Links" in `text-sm font-medium text-gray-700 mb-3`
   - Create row: `flex items-center gap-2`
     - Expiration select: `px-3 py-2 text-sm border border-gray-200 rounded-lg bg-white`
       - Options: "7 days", "14 days" (default selected), "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"
       - On click: call `createShareLink.mutate({ expiresInDays })`, on success copy the generated URL to clipboard

7. **Active Links List:** For each non-revoked share from `useShareLinks`:
   - `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 short URL `{origin}/s/{token.slice(0,8)}...`
   - Expiration badge: `text-xs text-gray-400` — "Expires {formatted date}" or "No expiration"
   - Copy button: `p-1.5 text-gray-400 hover:text-gray-600 rounded` with copy icon (16px)
     - On click: copy full share URL to clipboard, swap icon to check (green-500) for 2 seconds
   - Revoke button: `p-1.5 text-gray-400 hover:text-red-500 rounded` with x icon (16px)
     - On click: call `revokeShareLink.mutate(shareId)`

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

**Clipboard helper:** Use `navigator.clipboard.writeText(url)`. Construct full URL as `${window.location.origin}/s/${share.token}`.

**Update `src/client/routes/setups/$setupId.tsx`:**

1. Add import: `import { ShareModal } from "../../components/ShareModal";`
2. Add state: `const [shareModalOpen, setShareModalOpen] = useState(false);`
3. Replace the temporary visibility badge (from Plan 01) with the share button per UI-SPEC:

**Desktop variant:**
```tsx
<button
  type="button"
  onClick={() => setShareModalOpen(true)}
  className={`hidden md:inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium rounded-lg transition-colors ${
    setup.visibility === "public"
      ? "text-green-700 bg-green-50 hover:bg-green-100"
      : setup.visibility === "link"
        ? "text-blue-600 bg-blue-50 hover:bg-blue-100"
        : "text-gray-500 bg-gray-50 hover:bg-gray-100"
  }`}
>
  <LucideIcon name={setup.visibility === "public" ? "globe" : setup.visibility === "link" ? "link" : "lock"} size={16} />
  Share
</button>
```

**Mobile variant:**
```tsx
<button
  type="button"
  onClick={() => setShareModalOpen(true)}
  className={`md:hidden inline-flex items-center justify-center min-w-[44px] min-h-[44px] p-2 rounded-lg transition-colors ${
    setup.visibility === "public"
      ? "text-green-700 bg-green-50 hover:bg-green-100"
      : setup.visibility === "link"
        ? "text-blue-600 bg-blue-50 hover:bg-blue-100"
        : "text-gray-500 bg-gray-50 hover:bg-gray-100"
  }`}
  aria-label="Share settings"
  title="Share settings"
>
  <LucideIcon name={setup.visibility === "public" ? "globe" : setup.visibility === "link" ? "link" : "lock"} size={16} />
</button>
```

4. Render ShareModal:
```tsx
{shareModalOpen && (
  <ShareModal
    isOpen={shareModalOpen}
    onClose={() => setShareModalOpen(false)}
    setupId={numericId}
    currentVisibility={setup.visibility}
    onVisibilityChange={(v) => updateSetup.mutate({ visibility: v })}
  />
)}
```

5. Only show share button when `isAuthenticated` (same guard as current toggle).
  </action>
  <verify>
    <automated>grep -q "ShareModal" src/client/components/ShareModal.tsx && grep -q "ShareModal" src/client/routes/setups/\$setupId.tsx && bun run lint && echo "PASS" || echo "FAIL"</automated>
  </verify>
  <acceptance_criteria>
    - `src/client/components/ShareModal.tsx` renders: visibility picker with 3 options, create link form, active links list
    - Visibility picker options use correct icons: lock (private), link (link), globe (public)
    - Visibility picker colors match UI-SPEC: gray-500/gray-50, blue-600/blue-50, green-700/green-50
    - Create link form has expiration dropdown with options: 7 days, 14 days, 30 days, No expiration
    - Copy button copies `${origin}/s/${token}` to clipboard and shows check icon for 2s
    - Revoke button calls delete mutation
    - Deactivation warning shows when selecting private with active links
    - `src/client/routes/setups/$setupId.tsx` renders share button with visibility-state icon/color
    - Share button opens ShareModal on click
    - `bun run lint` passes
  </acceptance_criteria>
  <done>Share modal fully functional with visibility management, link creation, copy, and revoke</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| client->clipboard | Share URL written to system clipboard |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-32-08 | Information Disclosure | clipboard copy | accept | Share URLs are intentionally shareable — copying to clipboard is the feature's purpose |
</threat_model>

<verification>
1. `bun run lint` passes
2. Share button visible on setup detail page with correct icon/color per visibility state
3. Modal opens, visibility picker works, create link generates copyable URL
4. Revoking a link removes it from the list
5. Switching to private shows warning and deactivates links
</verification>

<success_criteria>
- Share modal is the single UI for managing visibility and share links (per D-13)
- Share icon button replaces old globe toggle (per D-14)
- Modal contains visibility picker, create link, and active links list (per D-15)
- Works on both desktop and mobile (per D-16)
</success_criteria>

<output>
After completion, create `.planning/phases/32-setup-sharing-system/32-03-SUMMARY.md`
</output>
