feat: Add custom step continuation flow with save/use/branch options

Custom steps during tree navigation now support a complete workflow:
- PostStepActionModal: Save for Later / Use Now / Both options
- ContinuationModal: Pick descendant nodes or build custom branch
- ForkTreeModal: Save modified tree as personal copy at completion
- Custom steps are recorded in decisions array for export
- Fix popular-tags API endpoint URL mismatch
- Add aria-labels for accessibility on select/button elements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-02-03 20:53:48 -05:00
parent 8498d25efb
commit 6bd21d7efc
9 changed files with 726 additions and 40 deletions

View File

@@ -1,7 +1,6 @@
import { useState } from 'react'
import { X } from 'lucide-react'
import { cn } from '@/lib/utils'
import { stepsApi } from '@/api'
import { StepForm } from './StepForm'
import { StepLibraryBrowser } from './StepLibraryBrowser'
import type { Step, StepCreate } from '@/types/step'
@@ -25,7 +24,7 @@ export interface CustomStepDraft {
interface CustomStepModalProps {
isOpen: boolean
onClose: () => void
onInsertStep: (step: Step | CustomStepDraft) => void
onInsertStep: (step: Step | CustomStepDraft, isFromLibrary: boolean) => void
}
type Tab = 'create' | 'browse'
@@ -37,26 +36,22 @@ export function CustomStepModal({ isOpen, onClose, onInsertStep }: CustomStepMod
if (!isOpen) return null
const handleFormSubmit = async (data: StepCreate, saveToLibrary: boolean) => {
const handleFormSubmit = async (data: StepCreate, _saveToLibrary: boolean) => {
// Note: saveToLibrary preference is no longer used here - the PostStepActionModal
// handles the decision of whether to save to library, use now, or both
setIsSubmitting(true)
setError(null)
try {
if (saveToLibrary) {
// Save to library first, then return the saved step
const savedStep = await stepsApi.create(data)
onInsertStep(savedStep)
} else {
// Return as draft (not saved to library)
const draft: CustomStepDraft = {
title: data.title,
step_type: data.step_type,
content: data.content,
category_id: data.category_id,
tags: data.tags
}
onInsertStep(draft)
// Always create a draft - saving to library is handled by PostStepActionModal
const draft: CustomStepDraft = {
title: data.title,
step_type: data.step_type,
content: data.content,
category_id: data.category_id,
tags: data.tags
}
onInsertStep(draft, false) // false = not from library (user typed it)
} catch (err) {
console.error('Failed to create step:', err)
setError('Failed to create step. Please try again.')
@@ -65,7 +60,7 @@ export function CustomStepModal({ isOpen, onClose, onInsertStep }: CustomStepMod
}
const handleBrowserInsert = (step: Step) => {
onInsertStep(step)
onInsertStep(step, true) // true = from library (already saved)
}
return (

View File

@@ -340,6 +340,7 @@ export function StepForm({ onSubmit, onCancel, initialData }: StepFormProps) {
type="button"
onClick={() => removeTag(tag)}
className="rounded-full hover:bg-primary/20"
aria-label={`Remove tag ${tag}`}
>
<X className="h-3 w-3" />
</button>

View File

@@ -149,6 +149,7 @@ export function StepLibraryBrowser({ onInsert, onCreateNew, showCreateButton = f
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
{/* Category Filter */}
<select
aria-label="Filter by category"
value={selectedCategoryId || ''}
onChange={(e) => setSelectedCategoryId(e.target.value || undefined)}
className="rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
@@ -161,6 +162,7 @@ export function StepLibraryBrowser({ onInsert, onCreateNew, showCreateButton = f
{/* Type Filter */}
<select
aria-label="Filter by step type"
value={selectedStepType || ''}
onChange={(e) => setSelectedStepType((e.target.value as any) || undefined)}
className="rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
@@ -173,6 +175,7 @@ export function StepLibraryBrowser({ onInsert, onCreateNew, showCreateButton = f
{/* Min Rating Filter */}
<select
aria-label="Filter by minimum rating"
value={minRating?.toString() || ''}
onChange={(e) => setMinRating(e.target.value ? Number(e.target.value) : undefined)}
className="rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
@@ -185,6 +188,7 @@ export function StepLibraryBrowser({ onInsert, onCreateNew, showCreateButton = f
{/* Sort By */}
<select
aria-label="Sort steps by"
value={sortBy}
onChange={(e) => setSortBy(e.target.value as any)}
className="rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"