# Step Library Page Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Wire up the existing Step Library components into a fully functional standalone page — replacing the "Coming Soon" stub — with create, edit, delete, preview, and save-to-library actions.

**Architecture:** `StepLibraryPage` owns all modal state and orchestrates four components: `StepLibraryBrowser` (list + filters), `StepFormModal` (new wrapper for create/edit), `StepDetailModal` (already exists), and a delete confirmation dialog. `StepCard` and `StepLibraryBrowser` get new optional props for library-page-specific actions. No new API calls beyond what already exists in `stepsApi`.

**Tech Stack:** React 19, TypeScript, Tailwind CSS, Zustand (`useAuthStore` for current user ID), `stepsApi` + `stepCategoriesApi` (all endpoints already wired to backend).

---

## Key Files Reference

- `frontend/src/pages/StepLibraryPage.tsx` — currently a "Coming Soon" stub; will be rewritten
- `frontend/src/components/step-library/StepLibraryBrowser.tsx` — list + filters component
- `frontend/src/components/step-library/StepCard.tsx` — individual step card
- `frontend/src/components/step-library/StepDetailModal.tsx` — preview modal (already complete)
- `frontend/src/components/step-library/StepForm.tsx` — create/edit form (already complete)
- `frontend/src/api/steps.ts` — `stepsApi.create`, `.get`, `.update`, `.delete` already implemented
- `frontend/src/types/step.ts` — `Step`, `StepListItem`, `StepCreate`, `StepUpdate` types
- `frontend/src/store/authStore.ts` — use `useAuthStore((s) => s.user)` to get current user
- `frontend/src/hooks/usePermissions.ts` — `canCreateSteps` already defined

---

## How to Get Current User ID

```tsx
import { useAuthStore } from '@/store/authStore'
const user = useAuthStore((s) => s.user)
// user.id is the current user's UUID string
```

---

## Task 1: Extend StepCard with library-page actions

**Files:**
- Modify: `frontend/src/components/step-library/StepCard.tsx`

This task adds `onEdit`, `onDelete`, `onSave`, and `currentUserId` props. When on the library page (these props are present), the action buttons change based on ownership.

**Step 1: Read the current file**

Already read above. Current interface:
```ts
interface StepCardProps {
  step: StepListItem
  onPreview: (step: StepListItem) => void
  onInsert: (step: StepListItem) => void
}
```

**Step 2: Update the interface and button logic**

Replace the `StepCardProps` interface and the Actions section at the bottom of `StepCard.tsx`.

New interface (all new props are optional so existing `CustomStepModal` usage is unchanged):
```tsx
interface StepCardProps {
  step: StepListItem
  onPreview: (step: StepListItem) => void
  onInsert?: (step: StepListItem) => void      // session context (existing)
  onEdit?: (step: StepListItem) => void        // library page
  onDelete?: (stepId: string) => void          // library page
  onSave?: (step: StepListItem) => void        // library page (save copy)
  currentUserId?: string                       // to determine ownership
}
```

Replace the Actions section (the `<div className="flex gap-2">` at the bottom) with:

```tsx
{/* Actions */}
<div className="flex gap-2">
  {/* Library page context */}
  {(onEdit || onDelete || onSave) ? (
    isOwn ? (
      // Own step: Preview + Edit + Delete icon
      <>
        <button
          onClick={() => onPreview(step)}
          className="flex flex-1 items-center justify-center gap-2 rounded-md border border-border px-3 py-2 text-sm font-medium text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
        >
          <Eye className="h-4 w-4" />
          Preview
        </button>
        <button
          onClick={() => onEdit?.(step)}
          className="flex flex-1 items-center justify-center gap-2 rounded-md border border-border px-3 py-2 text-sm font-medium text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
        >
          <Pencil className="h-4 w-4" />
          Edit
        </button>
        <button
          onClick={() => onDelete?.(step.id)}
          className="flex items-center justify-center rounded-md border border-border px-3 py-2 text-muted-foreground hover:bg-red-400/10 hover:text-red-400 hover:border-red-400/30 transition-colors"
          aria-label="Delete step"
        >
          <Trash2 className="h-4 w-4" />
        </button>
      </>
    ) : (
      // Others' step: Preview + Save
      <>
        <button
          onClick={() => onPreview(step)}
          className="flex flex-1 items-center justify-center gap-2 rounded-md border border-border px-3 py-2 text-sm font-medium text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
        >
          <Eye className="h-4 w-4" />
          Preview
        </button>
        <button
          onClick={() => onSave?.(step)}
          className="flex flex-1 items-center justify-center gap-2 rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-3 py-2 text-sm font-medium hover:opacity-90 transition-colors"
        >
          <Bookmark className="h-4 w-4" />
          Save
        </button>
      </>
    )
  ) : (
    // Session context (original): Preview + Insert
    <>
      <button
        onClick={() => onPreview(step)}
        className="flex flex-1 items-center justify-center gap-2 rounded-md border border-border px-3 py-2 text-sm font-medium text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
      >
        <Eye className="h-4 w-4" />
        Preview
      </button>
      <button
        onClick={() => onInsert?.(step)}
        className="flex flex-1 items-center justify-center gap-2 rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-3 py-2 text-sm font-medium hover:opacity-90 transition-colors"
      >
        <Plus className="h-4 w-4" />
        Insert
      </button>
    </>
  )}
</div>
```

Add `isOwn` derived value near top of the component function (before the return):
```tsx
const isOwn = currentUserId ? step.created_by === currentUserId : false
```

Add new imports at top of file:
```tsx
import { Star, User, Calendar, TrendingUp, Eye, Plus, HelpCircle, Zap, CheckCircle, Pencil, Trash2, Bookmark } from 'lucide-react'
```

**Step 3: Verify TypeScript compiles**

```bash
cd /home/michaelchihlas/dev/patherly/frontend && npx tsc --noEmit 2>&1 | head -30
```

Expected: no errors related to StepCard.

**Step 4: Commit**

```bash
cd /home/michaelchihlas/dev/patherly
git add frontend/src/components/step-library/StepCard.tsx
git commit -m "feat: add library-page action props to StepCard (edit/delete/save)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
```

---

## Task 2: Extend StepLibraryBrowser with library-page props

**Files:**
- Modify: `frontend/src/components/step-library/StepLibraryBrowser.tsx`

Pass the new `onEdit`, `onDelete`, `onSave`, `currentUserId` props through from browser to each `StepCard`. Also expose a `refreshKey` prop so the page can trigger a reload after create/edit/delete/save.

**Step 1: Update the interface**

Current interface:
```ts
interface StepLibraryBrowserProps {
  onInsert: (step: Step) => void
  onCreateNew?: () => void
  showCreateButton?: boolean
}
```

New interface:
```ts
interface StepLibraryBrowserProps {
  onInsert?: (step: Step) => void       // now optional (not needed on library page)
  onCreateNew?: () => void
  showCreateButton?: boolean
  onEdit?: (step: StepListItem) => void
  onDelete?: (stepId: string) => void
  onSave?: (step: StepListItem) => void
  currentUserId?: string
  refreshKey?: number                   // increment to trigger reload
}
```

**Step 2: Wire refreshKey into the steps useEffect**

In the existing `useEffect` that calls `loadSteps`, add `refreshKey` to the dependency array:

```tsx
useEffect(() => {
  const loadSteps = async () => { ... }
  loadSteps()
}, [searchQuery, selectedCategoryId, selectedStepType, minRating, sortBy, selectedTag, refreshKey])
```

**Step 3: Pass new props to StepCard**

In all three `groupedSteps.private/team/public` map blocks, update `StepCard` usage:

```tsx
<StepCard
  key={step.id}
  step={step}
  onPreview={handlePreview}
  onInsert={onInsert ? handleInsertFromCard : undefined}
  onEdit={onEdit}
  onDelete={onDelete}
  onSave={onSave}
  currentUserId={currentUserId}
/>
```

**Step 4: Verify TypeScript compiles**

```bash
cd /home/michaelchihlas/dev/patherly/frontend && npx tsc --noEmit 2>&1 | head -30
```

**Step 5: Commit**

```bash
cd /home/michaelchihlas/dev/patherly
git add frontend/src/components/step-library/StepLibraryBrowser.tsx
git commit -m "feat: pass library-page action props through StepLibraryBrowser + refreshKey

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
```

---

## Task 3: Create StepFormModal

**Files:**
- Create: `frontend/src/components/step-library/StepFormModal.tsx`

A thin modal wrapper around the existing `StepForm`. Handles both create and edit modes.

**Step 1: Create the file**

```tsx
import { useState } from 'react'
import { X } from 'lucide-react'
import { stepsApi } from '@/api/steps'
import { StepForm } from './StepForm'
import type { Step, StepCreate, StepListItem } from '@/types/step'

interface StepFormModalProps {
  isOpen: boolean
  onClose: () => void
  onSuccess: (step: Step) => void
  editingStep?: StepListItem | null   // if set, edit mode; if null/undefined, create mode
}

export function StepFormModal({ isOpen, onClose, onSuccess, editingStep }: StepFormModalProps) {
  const [isSubmitting, setIsSubmitting] = useState(false)
  const [error, setError] = useState<string | null>(null)

  if (!isOpen) return null

  const isEditMode = !!editingStep

  const handleSubmit = async (data: StepCreate) => {
    setIsSubmitting(true)
    setError(null)
    try {
      let result: Step
      if (isEditMode && editingStep) {
        result = await stepsApi.update(editingStep.id, data)
      } else {
        result = await stepsApi.create(data)
      }
      onSuccess(result)
    } catch (err) {
      console.error('Failed to save step:', err)
      setError('Failed to save step. Please try again.')
    } finally {
      setIsSubmitting(false)
    }
  }

  // Build initialData from editingStep for edit mode
  // StepListItem doesn't have `content`, so for edit we need to fetch full step
  // This is handled by the parent (StepLibraryPage fetches full step before opening modal)
  const initialData = editingStep ? {
    title: editingStep.title,
    step_type: editingStep.step_type as 'decision' | 'action' | 'solution',
    visibility: editingStep.visibility as 'private' | 'team' | 'public',
    category_id: editingStep.category_id,
    tags: editingStep.tags,
  } : undefined

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
      <div className="relative flex h-[90vh] w-full max-w-2xl flex-col bg-card border border-border rounded-2xl shadow-lg">
        {/* Header */}
        <div className="flex items-center justify-between border-b border-border p-6 pb-4">
          <h2 className="text-lg font-semibold text-foreground">
            {isEditMode ? 'Edit Step' : 'Create Step'}
          </h2>
          <button
            onClick={onClose}
            disabled={isSubmitting}
            className="rounded-md p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-50"
            aria-label="Close"
          >
            <X className="h-5 w-5" />
          </button>
        </div>

        {/* Error */}
        {error && (
          <div className="mx-6 mt-4 rounded-lg border border-red-400/20 bg-red-400/10 p-3 text-sm text-red-400">
            {error}
          </div>
        )}

        {/* Body */}
        <div className="flex-1 overflow-y-auto p-6">
          <StepForm
            onSubmit={handleSubmit}
            onCancel={onClose}
            initialData={initialData}
            submitLabel={isEditMode ? 'Save Changes' : 'Create Step'}
            isSubmitting={isSubmitting}
          />
        </div>
      </div>
    </div>
  )
}
```

**Step 2: Update StepForm to accept `submitLabel` and `isSubmitting` props**

`StepForm` currently has a hardcoded submit button label ("Insert Step") and no loading state. Add these two optional props:

```ts
interface StepFormProps {
  onSubmit: (data: StepCreate) => void
  onCancel: () => void
  initialData?: Partial<StepCreate>
  submitLabel?: string      // default: 'Insert Step'
  isSubmitting?: boolean    // default: false
}
```

In `StepForm`, use them on the submit button:
```tsx
<button
  type="submit"
  disabled={isSubmitting}
  className="flex-1 rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium hover:opacity-90 disabled:opacity-50"
>
  {isSubmitting ? 'Saving...' : (submitLabel ?? 'Insert Step')}
</button>
```

**Step 3: Verify TypeScript compiles**

```bash
cd /home/michaelchihlas/dev/patherly/frontend && npx tsc --noEmit 2>&1 | head -30
```

**Step 4: Commit**

```bash
cd /home/michaelchihlas/dev/patherly
git add frontend/src/components/step-library/StepFormModal.tsx \
        frontend/src/components/step-library/StepForm.tsx
git commit -m "feat: StepFormModal wrapper + submitLabel/isSubmitting props on StepForm

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
```

---

## Task 4: Rewrite StepLibraryPage

**Files:**
- Modify: `frontend/src/pages/StepLibraryPage.tsx`

This is the main wiring task. Replace the stub with the full page.

**Step 1: Write the new page**

```tsx
import { useState } from 'react'
import { Bookmark, Trash2 } from 'lucide-react'
import { useAuthStore } from '@/store/authStore'
import { usePermissions } from '@/hooks/usePermissions'
import { stepsApi } from '@/api/steps'
import { StepLibraryBrowser } from '@/components/step-library/StepLibraryBrowser'
import { StepFormModal } from '@/components/step-library/StepFormModal'
import type { Step, StepListItem } from '@/types/step'

export default function StepLibraryPage() {
  const user = useAuthStore((s) => s.user)
  const { canCreateSteps } = usePermissions()

  // Modal state
  const [createOpen, setCreateOpen] = useState(false)
  const [editingStep, setEditingStep] = useState<StepListItem | null>(null)
  const [deletingStep, setDeletingStep] = useState<StepListItem | null>(null)
  const [isDeleting, setIsDeleting] = useState(false)
  const [deleteError, setDeleteError] = useState<string | null>(null)
  const [saveToast, setSaveToast] = useState<string | null>(null)

  // Increment to trigger StepLibraryBrowser reload
  const [refreshKey, setRefreshKey] = useState(0)
  const refresh = () => setRefreshKey(k => k + 1)

  const handleEdit = (step: StepListItem) => {
    setEditingStep(step)
  }

  const handleDeleteRequest = (stepId: string) => {
    // Find the step in order to show its title in the confirmation
    // We store the StepListItem via the browser's onDelete callback
    // The step object is passed from StepCard which has the full StepListItem
    setDeletingStep({ id: stepId } as StepListItem)
  }

  const handleDeleteConfirm = async () => {
    if (!deletingStep) return
    setIsDeleting(true)
    setDeleteError(null)
    try {
      await stepsApi.delete(deletingStep.id)
      setDeletingStep(null)
      refresh()
    } catch (err) {
      console.error('Failed to delete step:', err)
      setDeleteError('Failed to delete step. Please try again.')
    } finally {
      setIsDeleting(false)
    }
  }

  const handleSave = async (step: StepListItem) => {
    try {
      // Fetch full step to get content fields
      const full = await stepsApi.get(step.id)
      await stepsApi.create({
        title: full.title,
        step_type: full.step_type,
        content: full.content,
        visibility: 'private',
        category_id: full.category_id,
        tags: full.tags,
      })
      setSaveToast(`"${full.title}" saved to My Steps`)
      setTimeout(() => setSaveToast(null), 3000)
      refresh()
    } catch (err) {
      console.error('Failed to save step:', err)
    }
  }

  const handleFormSuccess = (_step: Step) => {
    setCreateOpen(false)
    setEditingStep(null)
    refresh()
  }

  return (
    <div className="flex h-full flex-col">
      {/* Page Header */}
      <div className="flex items-center justify-between border-b border-border px-6 py-4">
        <div className="flex items-center gap-3">
          <span title="Step Library">
            <Bookmark className="h-6 w-6 text-muted-foreground" />
          </span>
          <div>
            <h1 className="text-xl font-bold font-heading text-foreground">Step Library</h1>
            <p className="text-sm text-muted-foreground">Reusable steps you can insert into any flow</p>
          </div>
        </div>
        {canCreateSteps && (
          <button
            onClick={() => setCreateOpen(true)}
            className="rounded-md bg-gradient-brand px-4 py-2 text-sm font-medium text-white shadow-lg shadow-primary/20 hover:opacity-90"
          >
            + Create Step
          </button>
        )}
      </div>

      {/* Browser fills remaining height */}
      <div className="flex-1 overflow-hidden">
        <StepLibraryBrowser
          onEdit={handleEdit}
          onDelete={(stepId) => {
            // We need the full StepListItem for the confirmation title.
            // Pass a minimal object; the title will show as "this step" if not available.
            setDeletingStep({ id: stepId, title: '' } as StepListItem)
          }}
          onSave={handleSave}
          currentUserId={user?.id}
          refreshKey={refreshKey}
          showCreateButton={false}
        />
      </div>

      {/* Create / Edit Modal */}
      <StepFormModal
        isOpen={createOpen || !!editingStep}
        onClose={() => { setCreateOpen(false); setEditingStep(null) }}
        onSuccess={handleFormSuccess}
        editingStep={editingStep}
      />

      {/* Delete Confirmation Dialog */}
      {deletingStep && (
        <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
          <div className="w-full max-w-sm rounded-xl bg-card border border-border p-6 shadow-lg">
            <div className="mb-4 flex items-center gap-3">
              <div className="rounded-full bg-red-400/10 p-2">
                <Trash2 className="h-5 w-5 text-red-400" />
              </div>
              <h2 className="text-base font-semibold text-foreground">Delete Step</h2>
            </div>
            <p className="mb-2 text-sm text-muted-foreground">
              {deletingStep.title
                ? <>Are you sure you want to delete <span className="font-medium text-foreground">"{deletingStep.title}"</span>?</>
                : 'Are you sure you want to delete this step?'
              }
            </p>
            <p className="mb-6 text-xs text-muted-foreground">This cannot be undone.</p>
            {deleteError && (
              <p className="mb-4 text-sm text-red-400">{deleteError}</p>
            )}
            <div className="flex gap-2">
              <button
                onClick={() => { setDeletingStep(null); setDeleteError(null) }}
                disabled={isDeleting}
                className="flex-1 rounded-md border border-border px-4 py-2 text-sm font-medium text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-50"
              >
                Cancel
              </button>
              <button
                onClick={handleDeleteConfirm}
                disabled={isDeleting}
                className="flex-1 rounded-md bg-red-500 px-4 py-2 text-sm font-medium text-white hover:bg-red-600 disabled:opacity-50"
              >
                {isDeleting ? 'Deleting...' : 'Delete'}
              </button>
            </div>
          </div>
        </div>
      )}

      {/* Save Toast */}
      {saveToast && (
        <div className="fixed bottom-6 left-1/2 z-50 -translate-x-1/2 rounded-lg border border-border bg-card px-4 py-2 text-sm text-foreground shadow-lg">
          {saveToast}
        </div>
      )}
    </div>
  )
}
```

**NOTE on delete title:** The `onDelete` callback from `StepCard` only passes `stepId: string`, not the full `StepListItem`. To show the step title in the confirmation dialog, change the `StepLibraryBrowser`'s `onDelete` prop type to pass the full `StepListItem` instead:

In `StepLibraryBrowser.tsx`, change:
```ts
onDelete?: (stepId: string) => void
```
to:
```ts
onDelete?: (step: StepListItem) => void
```

And update where it calls `onDelete` from cards — pass the full `step` object. Update `StepCard` similarly: change `onDelete?: (stepId: string) => void` to `onDelete?: (step: StepListItem) => void` and call `onDelete?.(step)` instead of `onDelete?.(step.id)`.

Then in `StepLibraryPage`, use `handleDeleteRequest(step: StepListItem)` and set `setDeletingStep(step)` directly — no need to pass a minimal object.

**Step 2: Run TypeScript check**

```bash
cd /home/michaelchihlas/dev/patherly/frontend && npx tsc --noEmit 2>&1 | head -40
```

Fix any type errors before proceeding.

**Step 3: Run build**

```bash
cd /home/michaelchihlas/dev/patherly/frontend && npm run build 2>&1 | tail -20
```

Expected: build succeeds with no errors.

**Step 4: Commit**

```bash
cd /home/michaelchihlas/dev/patherly
git add frontend/src/pages/StepLibraryPage.tsx \
        frontend/src/components/step-library/StepLibraryBrowser.tsx \
        frontend/src/components/step-library/StepCard.tsx
git commit -m "feat: Step Library page — create, edit, delete, save-to-library

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
```

---

## Task 5: Manual verification checklist

Start the dev server and verify these flows work end-to-end:

```bash
docker start patherly_postgres
cd /home/michaelchihlas/dev/patherly/backend && source venv/bin/activate && uvicorn app.main:app --reload &
cd /home/michaelchihlas/dev/patherly/frontend && npm run dev
```

Navigate to `http://localhost:5173/step-library` and verify:

- [ ] Page loads without errors (not "Coming Soon")
- [ ] "+ Create Step" button appears (login as engineer or admin)
- [ ] Creating a step via the modal saves it and it appears under "My Steps" on reload
- [ ] "Edit" button appears on your own step cards
- [ ] Editing a step opens the form pre-filled (note: `content` fields won't pre-fill since `StepListItem` doesn't have content — this is acceptable for now; see note below)
- [ ] "Delete" button appears on your own step cards
- [ ] Delete confirmation shows step title; confirming removes it from the list
- [ ] "Save" button appears on team/community step cards
- [ ] Saving a step copies it to "My Steps" and shows toast
- [ ] "Preview" opens `StepDetailModal` correctly on all card types
- [ ] Filters (category, type, rating, sort) work
- [ ] Popular tags clickable and filter results

**Note on edit pre-fill:** `StepListItem` does not include `content`. The `StepFormModal` passes `initialData` from `editingStep`, but `content` will be missing. For a full pre-fill, `StepLibraryPage.handleEdit` should fetch the full step via `stepsApi.get(step.id)` before opening the modal, and store the result as a `Step` (not `StepListItem`) in `editingStep` state. Update `editingStep` state type to `Step | null` and fetch in `handleEdit`:

```tsx
const [editingStep, setEditingStep] = useState<Step | null>(null)

const handleEdit = async (step: StepListItem) => {
  try {
    const full = await stepsApi.get(step.id)
    setEditingStep(full)
  } catch (err) {
    console.error('Failed to load step for edit:', err)
  }
}
```

Update `StepFormModal`'s `editingStep` prop type to accept `Step | null` and build `initialData` from the full `Step` including `content`:

```tsx
editingStep?: Step | null

const initialData = editingStep ? {
  title: editingStep.title,
  step_type: editingStep.step_type,
  content: editingStep.content,
  visibility: editingStep.visibility,
  category_id: editingStep.category_id,
  tags: editingStep.tags,
} : undefined
```

This should be done as part of Task 4 before verifying.

---

## Task 6: Final build validation and commit

```bash
cd /home/michaelchihlas/dev/patherly/frontend && npm run build 2>&1 | tail -20
```

Expected: clean build, no TypeScript errors, no warnings about missing exports.

If clean:
```bash
cd /home/michaelchihlas/dev/patherly
git add -A
git status  # confirm only expected files changed
git commit -m "chore: step library page final build validation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
```
