* refactor: adopt shared Input/Textarea components across 15 files Replace 42 raw <input>/<textarea> elements with <Input>/<Textarea> from components/ui/. Consistent focus states, error handling, and styling across all form fields. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: replace hardcoded rgba/hex colors with Tailwind tokens - rgba(255,255,255,0.xx) → bg-white/[0.xx], border-white/[0.xx] - rgba(6,182,212,0.3) → border-primary/30 (focus states) - #0a0a0a → bg-background - Inline style hex colors → var(--color-primary), var(--color-brand-gradient-to) - 28 files updated, zero hardcoded rgba() patterns remaining Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add PageMeta to 16 pages for SEO and proper browser tab titles Public pages (Login, Register, Forgot/Reset Password, Verify Email, Survey Thank You) get descriptions for SEO. Authenticated pages (Dashboard, Flow Library, My Flows, Session History, AI Assistant, Account Settings, Step Library, My Shares, Feedback, Guides) get proper tab titles. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add page transitions and staggered list animations - ViewTransitionOutlet: wraps Outlet with fade-in-up animation keyed to route path. Sidebar/topbar stay still, only content area animates. - StaggerList: reusable component that cascades children with incremental delay (50ms default). Pure CSS via @utility stagger-item. - Applied stagger to TreeGridView, MyTreesPage cards, SessionHistoryPage. - New stagger-fade-in keyframe in @theme block. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: ViewTransitionOutlet needs h-full for React Flow canvas The wrapper div broke the height chain needed by TreeEditorPage's h-full layout, causing React Flow canvas to collapse to zero height. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: main-content flex layout for tree editor + scrollable pages Main content area is now flex-col so the ViewTransitionOutlet wrapper gets an explicit computed height via flex-1 min-h-0. This makes h-full resolve correctly in the tree editor (React Flow canvas) while still allowing overflow-y-auto scrolling for normal pages. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resolve ESLint errors in Button and Skeleton components - Button: suppress react-refresh/only-export-components for buttonVariants re-export - Skeleton: replace empty interface with type alias, replace Math.random() with static widths array Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add PageMeta, animation classes, and layout fixes to remaining pages Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
176 lines
5.7 KiB
TypeScript
176 lines
5.7 KiB
TypeScript
import { useState } from 'react'
|
|
import { Bookmark, Trash2 } from 'lucide-react'
|
|
import { PageMeta } from '@/components/common/PageMeta'
|
|
import { Button } from '@/components/ui/Button'
|
|
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 { toast } from '@/lib/toast'
|
|
import type { Step, StepListItem } from '@/types/step'
|
|
|
|
export default function StepLibraryPage() {
|
|
const user = useAuthStore((s) => s.user)
|
|
const { canCreateSteps } = usePermissions()
|
|
|
|
// Create/edit modal state
|
|
const [createOpen, setCreateOpen] = useState(false)
|
|
const [editingStep, setEditingStep] = useState<Step | null>(null)
|
|
|
|
// Delete confirmation state
|
|
const [deletingStep, setDeletingStep] = useState<StepListItem | null>(null)
|
|
const [isDeleting, setIsDeleting] = useState(false)
|
|
const [deleteError, setDeleteError] = useState<string | null>(null)
|
|
|
|
// Increment to trigger StepLibraryBrowser reload
|
|
const [refreshKey, setRefreshKey] = useState(0)
|
|
const refresh = () => setRefreshKey(k => k + 1)
|
|
|
|
// Fetch full step before opening edit modal (StepListItem lacks content)
|
|
const handleEdit = async (step: StepListItem) => {
|
|
try {
|
|
const full = await stepsApi.get(step.id)
|
|
setEditingStep(full)
|
|
} catch {
|
|
toast.error('Failed to load step for editing')
|
|
}
|
|
}
|
|
|
|
const handleDeleteRequest = (step: StepListItem) => {
|
|
setDeletingStep(step)
|
|
}
|
|
|
|
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 {
|
|
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,
|
|
})
|
|
toast.success(`"${full.title}" saved to My Steps`)
|
|
refresh()
|
|
} catch {
|
|
toast.error('Failed to save step to your library')
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
const handleFormSuccess = (_step: Step) => {
|
|
setCreateOpen(false)
|
|
setEditingStep(null)
|
|
refresh()
|
|
}
|
|
|
|
const handleCloseModal = () => {
|
|
setCreateOpen(false)
|
|
setEditingStep(null)
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<PageMeta title="Step Library" />
|
|
<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)}>
|
|
+ Create Step
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Browser fills remaining height */}
|
|
<div className="flex-1 overflow-hidden">
|
|
<StepLibraryBrowser
|
|
onEdit={(step) => { handleEdit(step) }}
|
|
onDelete={handleDeleteRequest}
|
|
onSave={handleSave}
|
|
currentUserId={user?.id}
|
|
refreshKey={refreshKey}
|
|
showCreateButton={false}
|
|
/>
|
|
</div>
|
|
|
|
{/* Create / Edit Modal */}
|
|
<StepFormModal
|
|
isOpen={createOpen || !!editingStep}
|
|
onClose={handleCloseModal}
|
|
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-xs 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">
|
|
Are you sure you want to delete{' '}
|
|
<span className="font-medium text-foreground">"{deletingStep.title}"</span>?
|
|
</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
|
|
variant="secondary"
|
|
onClick={() => { setDeletingStep(null); setDeleteError(null) }}
|
|
disabled={isDeleting}
|
|
className="flex-1"
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
variant="destructive"
|
|
onClick={handleDeleteConfirm}
|
|
loading={isDeleting}
|
|
className="flex-1"
|
|
>
|
|
{isDeleting ? 'Deleting...' : 'Delete'}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
</>
|
|
)
|
|
}
|