test(ci): add exporter heading levels and a11y typography smoke; update MAIN_PLAN
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
"use client"
|
||||
|
||||
import React, { useMemo } from 'react'
|
||||
import type { Section } from '@/lib/state/store'
|
||||
import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, DragEndEvent } from '@dnd-kit/core'
|
||||
import { SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
|
||||
function SortableItem({ id, children, testId }: { id: string; children: React.ReactNode; testId?: string }) {
|
||||
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id })
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
}
|
||||
return (
|
||||
<li ref={setNodeRef} style={style} {...attributes} {...listeners} data-testid={testId} className="flex items-center justify-between border rounded px-3 py-2">
|
||||
{children}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
export default function SectionCanvas({
|
||||
sections,
|
||||
onMove,
|
||||
onRemove,
|
||||
onSelect,
|
||||
}: {
|
||||
sections: Section[]
|
||||
onMove: (from: number, to: number) => void
|
||||
onRemove: (index: number) => void
|
||||
onSelect?: (index: number) => void
|
||||
}) {
|
||||
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } }))
|
||||
const ids = useMemo(() => sections.map((_, i) => `sec-${i}`), [sections])
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
if (!over) return
|
||||
if (active.id === over.id) return
|
||||
const from = ids.indexOf(String(active.id))
|
||||
const to = ids.indexOf(String(over.id))
|
||||
if (from !== -1 && to !== -1) onMove(from, to)
|
||||
}
|
||||
|
||||
return (
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<SortableContext items={ids} strategy={verticalListSortingStrategy}>
|
||||
<ul className="space-y-1" data-testid="section-canvas">
|
||||
{sections.map((s, i) => (
|
||||
<SortableItem id={`sec-${i}`} key={`sec-${i}`} testId={`sec-item-${i}`}>
|
||||
<span className="text-sm">{s.type}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{onSelect && (
|
||||
<button aria-label={`Select ${i}`} title={`Select ${i}`} className="text-sm border rounded px-2 py-1" onClick={() => onSelect(i)}>
|
||||
Select
|
||||
</button>
|
||||
)}
|
||||
<button aria-label={`Up ${i}`} title={`Up ${i}`} className="text-sm border rounded px-2 py-1" onClick={() => i > 0 && onMove(i, i - 1)}>
|
||||
Up
|
||||
</button>
|
||||
<button aria-label={`Down ${i}`} title={`Down ${i}`} className="text-sm border rounded px-2 py-1" onClick={() => i < sections.length - 1 && onMove(i, i + 1)}>
|
||||
Down
|
||||
</button>
|
||||
<button aria-label={`Remove ${i}`} title={`Remove ${i}`} className="text-sm text-red-600 border rounded px-2 py-1" onClick={() => onRemove(i)}>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</SortableItem>
|
||||
))}
|
||||
</ul>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user