formlift 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["/home/harendra/Desktop/project/formlift/dist/react/index.cjs","../../src/react/FormEditor.tsx","../../src/react/ui/Button.tsx","../../src/react/ui/Modal.tsx","../../src/react/ui/IconButton.tsx","../../src/react/ui/Panel.tsx","../../src/react/ui/SegmentedTabs.tsx","../../src/react/ui/Checkbox.tsx","../../src/react/ui/FormField.tsx","../../src/react/components/ElementProperties.tsx","../../src/react/components/FieldControl.tsx","../../src/react/ui/SignaturePad.tsx","../../src/react/components/FieldProperties.tsx","../../src/react/constants.ts","../../src/react/components/ConditionBuilder.tsx","../../src/react/components/LayoutElementView.tsx","../../src/react/components/PagesPanel.tsx","../../src/react/components/PdfPages.tsx","../../src/react/pdfjs.ts","../../src/react/components/ZoomControls.tsx","../../src/react/hooks/useFitZoom.ts","../../src/react/hooks/useUndo.ts","../../src/react/FormFiller.tsx","../../src/react/components/QuestionList.tsx","../../src/react/ui/Chip.tsx","../../src/react/ui/Popover.tsx","../../src/react/ui/useOutsideClose.ts","../../src/react/ui/Tag.tsx"],"names":["jsx","jsxs","box","useCallback","_p","Plus","ArrowUp","ArrowDown","useMemo"],"mappings":"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","file":"/home/harendra/Desktop/project/formlift/dist/react/index.cjs","sourcesContent":[null,"import {\n ArrowDown,\n ArrowLeft,\n ArrowUp,\n Copy,\n Eye,\n Grid3x3,\n PanelRight,\n Pencil,\n Plus,\n Redo2,\n Save,\n Trash2,\n Type,\n Undo2,\n Wand2,\n} from 'lucide-react'\nimport {\n useCallback,\n useEffect,\n useImperativeHandle,\n useMemo,\n useRef,\n useState,\n type PointerEvent as ReactPointerEvent,\n type ReactNode,\n type Ref,\n} from 'react'\nimport Button from './ui/Button'\nimport { ConfirmModal } from './ui/Modal'\nimport IconButton from './ui/IconButton'\nimport { EmptyText, PanelHeading } from './ui/Panel'\nimport SegmentedTabs from './ui/SegmentedTabs'\nimport ElementProperties from './components/ElementProperties'\nimport FieldControl from './components/FieldControl'\nimport FieldProperties from './components/FieldProperties'\nimport LayoutElementView from './components/LayoutElementView'\nimport PagesPanel from './components/PagesPanel'\nimport type { PDFDocumentProxy } from 'pdfjs-dist'\nimport PdfPages, { type PageSize, type PdfSource } from './components/PdfPages'\nimport ZoomControls from './components/ZoomControls'\nimport { useCompact, useFitZoom } from './hooks/useFitZoom'\nimport { TYPE_LABEL } from './constants'\nimport {\n defaultLayout,\n FIELD_TYPES,\n fieldStates,\n validateFields,\n validateLayout,\n type Answers,\n type FieldDef,\n type FieldType,\n type Layout,\n type LayoutElement,\n type Position,\n} from '../core/form-logic'\nimport { useUndo } from './hooks/useUndo'\nimport { boxStyle, moveBy, resizeBy, snap, toPage } from '../core/coords'\nimport { detectDocument, type Ops } from '../core/detect'\nimport { duplicate, newField, reorder, withField } from '../core/fields'\nimport { loadPdfJs } from './pdfjs'\nimport {\n deletePage,\n duplicateElement,\n ELEMENT_TOOLS,\n fitTextWidth,\n insertBlankPage,\n movePage,\n newElement,\n replaceLine,\n type Doc,\n type ElementTool,\n} from '../core/layout'\n\nconst GRID = 0.005 // snap step: 0.5 % of the page\nconst DRAG_TYPE = 'application/x-doc-item' // \"field:<type>\" or \"element:<tool>\"\n\ntype Selection = { kind: 'field' | 'element'; id: string } | null\ntype Tab = string\n/** The editor's document: fields + layout (null = the original pages untouched, nothing drawn). */\nexport type FormDoc = { fields: FieldDef[]; layout: Layout | null }\ntype EditorDoc = FormDoc\nconst EMPTY: FormDoc = { fields: [], layout: null }\n\n/** Imperative handle (`ref`): save from outside, e.g. before a workflow step. */\nexport interface FormEditorHandle {\n /** Saves if there are changes. false = invalid or onSave failed. */\n save: () => Promise<boolean>\n readonly dirty: boolean\n readonly value: FormDoc\n}\n\nexport interface FormEditorProps {\n /** The PDF: URL, Blob / File, bytes, or a function returning one. Never modified. */\n pdf: PdfSource\n /** Saved fields + layout to start from (omit for a new form). Remount (`key`) to load another. */\n initialValue?: FormDoc\n /** Called by Save with the document. A rejected promise keeps the changes unsaved. */\n onSave: (doc: FormDoc) => unknown\n /** Every change (also undo / redo). */\n onChange?: (doc: FormDoc) => void\n title?: ReactNode\n /** Back button in the toolbar (hidden when omitted). */\n onBack?: () => void\n /** Your own items at the end of the toolbar (status, workflow buttons…). */\n toolbar?: ReactNode\n /** A strip under the toolbar (e.g. \"this version is live\"). */\n banner?: ReactNode\n /** Your own tabs in the right panel (e.g. history). */\n tabs?: { id: string; label: string; content: ReactNode }[]\n /** Run auto-detect when the PDF opens. Default: only when there are no fields and no layout yet. */\n autoDetect?: boolean\n notify?: (message: string) => void\n onError?: (error: unknown) => void\n /** Extra classes on the root. It fills its container's height (`h-full`) by default: give the container a height. */\n className?: string\n ref?: Ref<FormEditorHandle>\n}\n\nconst isDefault = (l: Layout, sizes: PageSize[]) =>\n !l.elements.length && l.pages.length === sizes.length && l.pages.every((p, i) => p.source === i + 1)\n\n/**\n * Admin editor. The document in the middle: original PDF pages (and blank pages) with everything the admin\n * adds on top. Left: fields and layout tools to drag onto a page. \"Edit text\" makes every line of the original\n * clickable: a click covers it with an editable copy. Right: properties of the selection, the field list, pages\n * (reorder, delete, add blank) and history. Nothing changes the uploaded PDF; all of it is saved in the version.\n * Keyboard: Delete, Ctrl+Z / Ctrl+Y, Ctrl+D duplicate, arrows nudge (Shift = bigger steps).\n */\nexport default function FormEditor({\n pdf: pdfSource,\n initialValue = EMPTY,\n onSave,\n onChange,\n title,\n onBack,\n toolbar,\n banner,\n tabs = [],\n autoDetect,\n notify = () => {},\n onError: notifyError = (e) => console.error(e),\n className = 'h-full',\n ref,\n}: FormEditorProps) {\n const [sizes, setSizes] = useState<PageSize[] | null>(null)\n const history = useUndo<EditorDoc>(initialValue)\n const { value: doc, set } = history\n const [saved, setSaved] = useState<EditorDoc>(initialValue)\n const [selection, setSelection] = useState<Selection>(null)\n const [tab, setTab] = useState<Tab>('props')\n // Phone / tablet (below lg): the side panels open as drawers over the page; null = both closed.\n const [drawer, setDrawer] = useState<'tools' | 'panel' | null>(null)\n // A field / layout tool picked by a tap (touch screens cannot drag): the next tap on a page places it.\n const [armed, setArmed] = useState<string | null>(null)\n const compact = useCompact()\n const [snapOn, setSnapOn] = useState(true)\n const [editText, setEditText] = useState(false)\n const [preview, setPreview] = useState(false)\n const [previewAnswers, setPreviewAnswers] = useState<Answers>({})\n const [busy, setBusy] = useState(false)\n const [confirmDeletePage, setConfirmDeletePage] = useState<number | null>(null)\n const [loadError, setLoadError] = useState('')\n const [pdf, setPdf] = useState<PDFDocumentProxy | null>(null)\n const [detecting, setDetecting] = useState<{ done: number; total: number; stop: AbortController } | null>(null)\n const autoDetected = useRef(false)\n const drag = useRef<{\n sel: NonNullable<Selection>\n mode: 'move' | 'resize'\n x: number\n y: number\n start: Position\n rect: DOMRect\n } | null>(null)\n const scroller = useRef<HTMLDivElement>(null)\n const { zoom, setZoom, ref: fitRef } = useFitZoom(Math.max(...(sizes ?? [{ width: 595 }]).map((p) => p.width)))\n\n const { fields } = doc\n // The layout as shown: the saved one, or the original pages once the PDF told us their sizes.\n const layout: Layout | null = doc.layout ?? (sizes ? defaultLayout(sizes) : null)\n const pageCount = layout?.pages.length ?? 1\n const grid = snapOn ? GRID : 0\n const selectedField = selection?.kind === 'field' ? (fields.find((f) => f.id === selection.id) ?? null) : null\n const selectedElement =\n selection?.kind === 'element' ? (layout?.elements.find((e) => e.id === selection.id) ?? null) : null\n const dirty = JSON.stringify(doc) !== JSON.stringify(saved)\n const problems = useMemo(\n () =>\n sizes\n ? [...(doc.layout ? validateLayout(doc.layout, sizes.length) : []), ...validateFields(fields, pageCount)]\n : [],\n [sizes, doc.layout, fields, pageCount],\n )\n\n const onChangeRef = useRef(onChange)\n useEffect(() => {\n onChangeRef.current = onChange\n })\n const first = useRef(true)\n useEffect(() => {\n if (first.current) first.current = false\n else onChangeRef.current?.(doc)\n }, [doc])\n\n // Changes that touch the layout start from the shown one (the default until the first change).\n const withDoc = useCallback(\n (fn: (d: Doc) => Doc, record = true) =>\n set((cur) => {\n const base = cur.layout ?? (sizes ? defaultLayout(sizes) : null)\n if (!base) return cur\n return fn({ fields: cur.fields, layout: base })\n }, record),\n [set, sizes],\n )\n const setFields = useCallback(\n (fn: (f: FieldDef[]) => FieldDef[], record = true) => set((cur) => ({ ...cur, fields: fn(cur.fields) }), record),\n [set],\n )\n const setElements = useCallback(\n (fn: (e: LayoutElement[]) => LayoutElement[], record = true) =>\n withDoc((d) => ({ ...d, layout: { ...d.layout, elements: fn(d.layout.elements) } }), record),\n [withDoc],\n )\n\n const select = (s: Selection) => {\n setSelection(s)\n if (s) setTab('props')\n }\n const updateField = useCallback((f: FieldDef) => setFields((l) => withField(l, f)), [setFields])\n // Text items grow to fit longer text, so nothing is cut off.\n const updateElement = useCallback(\n (e: LayoutElement) => {\n const pageWidth = layout?.pages[e.page - 1]?.width ?? 595\n setElements((l) => l.map((x) => (x.id === e.id ? fitTextWidth(e, pageWidth) : x)))\n },\n [setElements, layout],\n )\n const removeSelected = useCallback(() => {\n if (!selection) return\n if (selection.kind === 'field') setFields((l) => l.filter((x) => x.id !== selection.id))\n else setElements((l) => l.filter((x) => x.id !== selection.id))\n setSelection(null)\n }, [selection, setFields, setElements])\n const duplicateSelected = useCallback(() => {\n if (selectedField) {\n const copy = duplicate(selectedField, fields)\n setFields((l) => [...l, copy])\n setSelection({ kind: 'field', id: copy.id })\n } else if (selectedElement) {\n const copy = duplicateElement(selectedElement)\n setElements((l) => [...l, copy])\n setSelection({ kind: 'element', id: copy.id })\n }\n }, [selectedField, selectedElement, fields, setFields, setElements])\n\n // Keyboard shortcuts (not while typing in an input).\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => {\n const t = e.target as HTMLElement\n if (preview || t.closest('input, textarea, select, [contenteditable]')) return\n const mod = e.ctrlKey || e.metaKey\n if (mod && e.key.toLowerCase() === 'z') {\n e.preventDefault()\n if (e.shiftKey) history.redo()\n else history.undo()\n } else if (mod && e.key.toLowerCase() === 'y') {\n e.preventDefault()\n history.redo()\n } else if (e.key === 'Escape' && armed) setArmed(null)\n else if (!selection) return\n else if (e.key === 'Delete' || e.key === 'Backspace') removeSelected()\n else if (mod && e.key.toLowerCase() === 'd') {\n e.preventDefault()\n duplicateSelected()\n } else if (e.key === 'Escape') setSelection(null)\n else if (e.key.startsWith('Arrow')) {\n e.preventDefault()\n const step = e.shiftKey ? 0.01 : 0.002\n const dx = e.key === 'ArrowLeft' ? -step : e.key === 'ArrowRight' ? step : 0\n const dy = e.key === 'ArrowUp' ? -step : e.key === 'ArrowDown' ? step : 0\n if (selectedField) updateField({ ...selectedField, position: moveBy(selectedField.position, dx, dy) })\n if (selectedElement) {\n const { page: _p, ...position } = moveBy({ page: selectedElement.page, ...selectedElement.position }, dx, dy)\n void _p\n updateElement({ ...selectedElement, position })\n }\n }\n }\n window.addEventListener('keydown', onKey)\n return () => window.removeEventListener('keydown', onKey)\n }, [\n preview,\n armed,\n selection,\n selectedField,\n selectedElement,\n history,\n removeSelected,\n duplicateSelected,\n updateField,\n updateElement,\n ])\n\n // Warn before leaving with unsaved changes.\n useEffect(() => {\n if (!dirty) return\n const warn = (e: BeforeUnloadEvent) => e.preventDefault()\n window.addEventListener('beforeunload', warn)\n return () => window.removeEventListener('beforeunload', warn)\n }, [dirty])\n\n function startDrag(e: ReactPointerEvent, sel: NonNullable<Selection>, start: Position, mode: 'move' | 'resize') {\n if (e.button !== 0) return\n e.stopPropagation()\n const pageEl = (e.currentTarget as HTMLElement).closest('[data-page]')\n if (!pageEl) return\n select(sel)\n history.checkpoint()\n drag.current = { sel, mode, x: e.clientX, y: e.clientY, start, rect: pageEl.getBoundingClientRect() }\n ;(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId)\n }\n function onDrag(e: ReactPointerEvent) {\n const d = drag.current\n if (!d) return\n const dx = (e.clientX - d.x) / d.rect.width\n const dy = (e.clientY - d.y) / d.rect.height\n if (d.sel.kind === 'field') {\n const position = d.mode === 'move' ? moveBy(d.start, dx, dy, grid) : resizeBy(d.start, dx, dy, grid)\n setFields((l) => l.map((x) => (x.id === d.sel.id ? { ...x, position } : x)), false)\n return\n }\n setElements(\n (l) =>\n l.map((x) => {\n if (x.id !== d.sel.id) return x\n if (d.mode === 'move') {\n const { page: _p, ...position } = moveBy(d.start, dx, dy, grid)\n void _p\n return { ...x, position }\n }\n if (x.type === 'line') {\n // Lines may be flat: a near-zero side snaps to 0 so horizontal / vertical lines stay straight.\n const flat = (n: number) => (n < 0.004 ? 0 : n)\n const width = flat(Math.min(1 - d.start.x, Math.max(0, snap(d.start.width + dx, grid))))\n const height = flat(Math.min(1 - d.start.y, Math.max(0, snap(d.start.height + dy, grid))))\n return { ...x, position: { ...x.position, width, height } }\n }\n const { page: _p, ...position } = resizeBy(d.start, dx, dy, grid)\n void _p\n return { ...x, position }\n }),\n false,\n )\n }\n const endDrag = () => (drag.current = null)\n\n /** Adds the tool `key` ('field:text', 'element:box') where the page was dropped on / tapped. */\n function placeNew(key: string, page: number, clientX: number, clientY: number, rect: DOMRect) {\n const [kind, what] = key.split(':')\n const { x, y } = toPage(clientX, clientY, rect)\n if (kind === 'field' && FIELD_TYPES.includes(what as FieldType)) {\n const f = newField(what as FieldType, page, x, y, fields)\n setFields((l) => [...l, f])\n select({ kind: 'field', id: f.id })\n return true\n }\n if (kind === 'element' && what in ELEMENT_TOOLS) {\n const el = newElement(what as ElementTool, page, x, y)\n setElements((l) => [...l, el])\n select({ kind: 'element', id: el.id })\n return true\n }\n return false\n }\n function dropNew(e: React.DragEvent, page: number) {\n const key = e.dataTransfer.getData(DRAG_TYPE)\n if (placeNew(key, page, e.clientX, e.clientY, e.currentTarget.getBoundingClientRect())) e.preventDefault()\n }\n\n function goToPage(n: number) {\n scroller.current?.querySelector(`[data-page=\"${n}\"]`)?.scrollIntoView({ block: 'start' })\n }\n\n // Suggested fields for the whole document (dashed amber boxes); one undo step removes them all.\n const detect = useCallback(\n async (auto = false) => {\n if (!pdf || !layout || detecting) return\n const stop = new AbortController()\n setDetecting({ done: 0, total: layout.pages.length, stop })\n try {\n const lib = await loadPdfJs()\n const found = await detectDocument(\n pdf,\n lib.OPS as unknown as Ops,\n layout.pages,\n fields,\n (done, total) => setDetecting((d) => d && { ...d, done, total }),\n stop.signal,\n )\n if (found.length) {\n setFields((l) => [...l, ...found])\n setTab('fields')\n }\n notify(\n found.length\n ? `${found.length} fields suggested${stop.signal.aborted ? ' (stopped early)' : ''}. Check the dashed ones, then Save.`\n : auto\n ? 'No fields found automatically. Drag them in from the left.'\n : 'No new fields found',\n )\n } catch (err) {\n notifyError(err)\n } finally {\n setDetecting(null)\n }\n },\n [pdf, layout, detecting, fields, setFields, notify, notifyError],\n )\n\n // A new form (nothing placed yet) gets suggestions right away.\n useEffect(() => {\n if (autoDetected.current || !pdf || !layout) return\n const fresh = autoDetect ?? (!initialValue.fields.length && !initialValue.layout)\n if (!fresh) {\n autoDetected.current = true\n return\n }\n // Started after this render (an effect must not set state synchronously).\n const t = setTimeout(() => {\n autoDetected.current = true\n void detect(true)\n })\n return () => clearTimeout(t)\n }, [pdf, layout, detect, autoDetect, initialValue])\n\n async function save() {\n if (problems.length) {\n notifyError(problems[0])\n return false\n }\n setBusy(true)\n try {\n // An untouched layout is saved as null: \"the original pages as they are\".\n const layoutToSave = doc.layout && sizes && isDefault(doc.layout, sizes) ? null : doc.layout\n const sent = doc\n await onSave({ fields, layout: layoutToSave })\n setSaved(sent)\n notify('Saved')\n return true\n } catch (err) {\n notifyError(err)\n return false\n } finally {\n setBusy(false)\n }\n }\n\n const saveRef = useRef(save)\n useEffect(() => {\n saveRef.current = save\n })\n useImperativeHandle(\n ref,\n () => ({\n save: () => (dirty ? saveRef.current() : Promise.resolve(problems.length === 0)),\n dirty,\n value: doc,\n }),\n [dirty, doc, problems],\n )\n\n if (loadError) return <p className=\"fl-root p-6 text-center text-sm text-danger\">{loadError}</p>\n\n const previewStates = fieldStates(fields, previewAnswers)\n const counts: Record<number, number> = {}\n for (const f of fields) counts[f.position.page] = (counts[f.position.page] ?? 0) + 1\n for (const el of layout?.elements ?? []) counts[el.page] = (counts[el.page] ?? 0) + 1\n const pageHeight = (page: number) => layout?.pages[page - 1]?.height ?? 842\n\n // Drag onto a page, or tap it and then tap the page (touch screens have no drag and drop).\n const toolItem = (key: string, label: string) => (\n <button\n type=\"button\"\n key={key}\n draggable\n onDragStart={(e) => e.dataTransfer.setData(DRAG_TYPE, key)}\n onClick={() => {\n setArmed((a) => (a === key ? null : key))\n setDrawer(null)\n }}\n aria-pressed={armed === key}\n className={`block w-full cursor-grab rounded-md border px-2.5 py-1.5 text-left text-[13px] hover:border-primary hover:bg-primary-soft active:cursor-grabbing max-lg:py-2.5 ${\n armed === key ? 'border-primary bg-primary-soft' : 'border-line'\n }`}\n >\n {label}\n </button>\n )\n const armedLabel = armed\n ? armed.startsWith('field:')\n ? TYPE_LABEL[armed.slice(6) as FieldType]\n : ELEMENT_TOOLS[armed.slice(8) as ElementTool]?.label\n : ''\n\n return (\n <div className={`fl-root flex min-h-[640px] flex-col bg-white text-ink ${className}`}>\n {/* Toolbar */}\n <div className=\"flex flex-wrap items-center gap-2 border-b border-line bg-white px-4 py-2.5\">\n {onBack && (\n <Button variant=\"secondary\" size=\"sm\" icon={ArrowLeft} onClick={onBack}>\n Back\n </Button>\n )}\n {!preview && (\n <Button\n variant={drawer === 'tools' ? 'primary' : 'secondary'}\n size=\"sm\"\n icon={Plus}\n className=\"lg:hidden\"\n onClick={() => setDrawer((d) => (d === 'tools' ? null : 'tools'))}\n >\n Add\n </Button>\n )}\n <Button\n variant={drawer === 'panel' ? 'primary' : 'secondary'}\n size=\"sm\"\n icon={PanelRight}\n className=\"lg:hidden\"\n onClick={() => setDrawer((d) => (d === 'panel' ? null : 'panel'))}\n >\n {selection ? 'Properties' : 'Panel'}\n </Button>\n {title != null && <h2 className=\"m-0 min-w-0 truncate text-[17px] font-semibold\">{title}</h2>}\n <span className=\"text-[12px] text-muted\">{dirty ? 'Unsaved changes' : 'All changes saved'}</span>\n <div className=\"ml-auto flex flex-wrap items-center gap-1.5\">\n <IconButton\n icon={Undo2}\n title=\"Undo (Ctrl+Z)\"\n disabled={!history.canUndo || preview}\n onClick={history.undo}\n />\n <IconButton\n icon={Redo2}\n title=\"Redo (Ctrl+Y)\"\n disabled={!history.canRedo || preview}\n onClick={history.redo}\n />\n <IconButton\n icon={Grid3x3}\n title={snapOn ? 'Snap to grid: on' : 'Snap to grid: off'}\n className={snapOn ? '!text-primary' : ''}\n onClick={() => setSnapOn((s) => !s)}\n />\n <ZoomControls zoom={zoom} onChange={setZoom} />\n {!preview && (\n <Button\n variant={editText ? 'primary' : 'secondary'}\n size=\"sm\"\n icon={Type}\n disabled={!layout}\n title=\"Click any line of the document to change its text\"\n onClick={() => setEditText((v) => !v)}\n >\n <span className=\"max-sm:hidden\">Edit text</span>\n </Button>\n )}\n {!preview && (\n <Button\n variant=\"secondary\"\n size=\"sm\"\n icon={Wand2}\n disabled={!pdf || !layout || !!detecting}\n title=\"Find answer lines, checkboxes and table cells and suggest fields for them\"\n onClick={() => void detect()}\n >\n <span className=\"max-sm:hidden\">Auto-detect</span>\n </Button>\n )}\n <Button\n variant=\"secondary\"\n size=\"sm\"\n icon={preview ? Pencil : Eye}\n title={preview ? 'Back to editing' : 'Preview'}\n onClick={() => {\n setPreview((p) => !p)\n setPreviewAnswers({})\n setEditText(false)\n }}\n >\n <span className=\"max-sm:hidden\">{preview ? 'Edit' : 'Preview'}</span>\n </Button>\n <Button size=\"sm\" icon={Save} title=\"Save\" disabled={!dirty || busy} onClick={() => void save()}>\n <span className=\"max-sm:hidden\">Save</span>\n </Button>\n {toolbar}\n </div>\n </div>\n {banner}\n {detecting && (\n <div className=\"flex items-center gap-3 bg-folder/15 px-4 py-1.5 text-[12.5px] text-ink-2\" role=\"status\">\n Detecting fields… page {detecting.done} of {detecting.total}\n <div className=\"h-1.5 w-40 overflow-hidden rounded bg-white\">\n <div\n className=\"h-full bg-folder\"\n style={{ width: `${(100 * detecting.done) / Math.max(1, detecting.total)}%` }}\n />\n </div>\n <button\n type=\"button\"\n className=\"cursor-pointer text-primary hover:underline\"\n onClick={() => detecting.stop.abort()}\n >\n Stop\n </button>\n </div>\n )}\n {armed && !preview && (\n <p className=\"m-0 flex items-center gap-3 bg-primary-soft px-4 py-1.5 text-[12.5px] text-ink-2\" role=\"status\">\n Tap the page where the {armedLabel} should go.\n <button type=\"button\" className=\"cursor-pointer text-primary hover:underline\" onClick={() => setArmed(null)}>\n Cancel\n </button>\n </p>\n )}\n {editText && (\n <p className=\"m-0 bg-folder/15 px-4 py-1.5 text-[12.5px] text-ink-2\">\n Edit text: click a line of the document to replace it with an editable copy (same place and size). The\n uploaded file itself never changes.\n </p>\n )}\n\n <div className=\"relative flex min-h-0 flex-1\">\n {/* Toolbox: a column on large screens, a drawer from the left below lg. */}\n {!preview && (\n <aside\n className={`w-[170px] flex-none overflow-y-auto border-r border-line bg-white max-lg:absolute max-lg:inset-y-0 max-lg:left-0 max-lg:z-30 max-lg:w-[220px] max-lg:shadow-xl ${\n drawer === 'tools' ? '' : 'max-lg:hidden'\n }`}\n >\n <PanelHeading>FIELDS</PanelHeading>\n <p className=\"m-0 px-3 pt-2 text-[11.5px] text-muted\">Drag onto the document, or tap one, then the page</p>\n <div className=\"space-y-1 p-2\">{FIELD_TYPES.map((t) => toolItem(`field:${t}`, TYPE_LABEL[t]))}</div>\n <PanelHeading>LAYOUT</PanelHeading>\n <div className=\"space-y-1 p-2\">\n {(Object.keys(ELEMENT_TOOLS) as ElementTool[]).map((t) =>\n toolItem(`element:${t}`, ELEMENT_TOOLS[t].label),\n )}\n </div>\n </aside>\n )}\n\n {/* Document */}\n <div\n ref={(n) => {\n scroller.current = n\n fitRef(n)\n }}\n className=\"min-w-0 flex-1 overflow-auto bg-hover\"\n onPointerDown={() => {\n if (!preview) setSelection(null)\n setDrawer(null)\n }}\n >\n <PdfPages\n load={pdfSource}\n zoom={zoom}\n pages={layout?.pages}\n withText={editText && !preview}\n onLoaded={(s, doc) => {\n setSizes(s)\n setPdf(doc)\n }}\n onError={() => setLoadError('The document could not be shown')}\n overlay={(page, lines) => (\n <div\n className=\"absolute inset-0\"\n style={\n snapOn && !preview\n ? {\n backgroundImage:\n 'linear-gradient(to right, color-mix(in srgb, var(--fl-primary) 7%, transparent) 1px, transparent 1px), linear-gradient(to bottom, color-mix(in srgb, var(--fl-primary) 7%, transparent) 1px, transparent 1px)',\n backgroundSize: '2% 2%',\n }\n : undefined\n }\n onDragOver={(e) => !preview && e.preventDefault()}\n onDrop={(e) => dropNew(e, page)}\n onClick={(e) => {\n if (!armed || preview) return\n placeNew(armed, page, e.clientX, e.clientY, e.currentTarget.getBoundingClientRect())\n setArmed(null)\n }}\n >\n {/* Original text lines (Edit text): a click replaces the line with an editable copy. */}\n {lines?.map((line, i) => (\n <button\n key={i}\n type=\"button\"\n title={`Edit: ${line.text}`}\n className=\"absolute cursor-text rounded-[1px] outline-1 outline-folder/60 hover:bg-folder/25 hover:outline-folder\"\n style={boxStyle({ page, ...line })}\n onPointerDown={(e) => e.stopPropagation()}\n onClick={() => {\n const el = replaceLine(line, page)\n setElements((l) => [...l, el])\n select({ kind: 'element', id: el.id })\n }}\n />\n ))}\n\n {layout?.elements\n .filter((el) => el.page === page)\n .map((el) =>\n preview ? (\n <LayoutElementView key={el.id} element={el} pageHeight={pageHeight(page)} />\n ) : (\n <div\n key={el.id}\n role=\"button\"\n tabIndex={0}\n aria-label={`${el.type} ${el.text ?? ''}`}\n className={`absolute cursor-move touch-none select-none ${\n selection?.id === el.id\n ? 'z-10 outline-2 outline-primary'\n : 'hover:outline-1 hover:outline-primary/50'\n }`}\n style={boxStyle({ page, ...el.position, height: Math.max(el.position.height, 0.004) })}\n onPointerDown={(e) =>\n startDrag(e, { kind: 'element', id: el.id }, { page, ...el.position }, 'move')\n }\n onPointerMove={onDrag}\n onPointerUp={endDrag}\n onClick={(e) => e.stopPropagation()}\n >\n <LayoutElementView\n element={el}\n pageHeight={pageHeight(page)}\n style={{ left: 0, top: 0, width: '100%', height: '100%' }}\n />\n {selection?.id === el.id && (\n <span\n aria-label=\"Resize\"\n className=\"absolute -right-1 -bottom-1 h-2.5 w-2.5 cursor-se-resize rounded-sm border border-white bg-primary\"\n onPointerDown={(e) =>\n startDrag(e, { kind: 'element', id: el.id }, { page, ...el.position }, 'resize')\n }\n onPointerMove={onDrag}\n onPointerUp={endDrag}\n />\n )}\n </div>\n ),\n )}\n\n {fields\n .filter((f) => f.position.page === page)\n .map((f) =>\n preview ? (\n previewStates[f.key]?.visible && (\n <div key={f.id} className=\"absolute [container-type:size]\" style={boxStyle(f.position)}>\n <FieldControl\n field={f}\n value={previewAnswers[f.key]}\n readOnly={previewStates[f.key].readOnly}\n compact={compact}\n onChange={(v) => setPreviewAnswers((a) => ({ ...a, [f.key]: v }))}\n />\n </div>\n )\n ) : (\n <div\n key={f.id}\n role=\"button\"\n tabIndex={0}\n aria-label={`${TYPE_LABEL[f.type]} field ${f.label || f.key}`}\n title={`${f.label || f.key} (${TYPE_LABEL[f.type]})`}\n className={`absolute cursor-move touch-none rounded-sm border text-[10px] select-none [container-type:size] ${\n selection?.id === f.id\n ? 'z-10 border-primary bg-primary/20 ring-2 ring-primary/40'\n : f.source === 'ai'\n ? 'border-dashed border-folder bg-folder/15'\n : 'border-primary/60 bg-primary/10 hover:bg-primary/15'\n }`}\n style={boxStyle(f.position)}\n onPointerDown={(e) => startDrag(e, { kind: 'field', id: f.id }, f.position, 'move')}\n onPointerMove={onDrag}\n onPointerUp={endDrag}\n onClick={(e) => e.stopPropagation()}\n >\n <span className=\"pointer-events-none block truncate px-0.5 text-[clamp(7px,55cqh,12px)] leading-tight text-primary\">\n {f.label || f.key}\n {f.required && <b className=\"text-danger\"> *</b>}\n </span>\n {selection?.id === f.id && (\n <span\n aria-label=\"Resize\"\n className=\"absolute -right-1 -bottom-1 h-2.5 w-2.5 cursor-se-resize rounded-sm border border-white bg-primary\"\n onPointerDown={(e) => startDrag(e, { kind: 'field', id: f.id }, f.position, 'resize')}\n onPointerMove={onDrag}\n onPointerUp={endDrag}\n />\n )}\n </div>\n ),\n )}\n </div>\n )}\n />\n </div>\n\n {/* Right panel: a column on large screens, a drawer from the right below lg. */}\n <aside\n className={`flex w-[350px] flex-none flex-col border-l border-line bg-white max-lg:absolute max-lg:inset-y-0 max-lg:right-0 max-lg:z-30 max-lg:w-[min(350px,92vw)] max-lg:shadow-xl ${\n drawer === 'panel' ? '' : 'max-lg:hidden'\n }`}\n >\n <div className=\"overflow-x-auto border-b border-line p-2 [&_button]:px-2.5\">\n <SegmentedTabs\n options={[\n { value: 'props', label: 'Properties' },\n { value: 'fields', label: `Fields (${fields.length})` },\n { value: 'pages', label: `Pages (${pageCount})` },\n ...tabs.map((t) => ({ value: t.id, label: t.label })),\n ]}\n value={tab}\n onChange={setTab}\n />\n </div>\n {problems.length > 0 && (\n <ul className=\"m-0 border-b border-danger/30 bg-danger/5 py-2 pr-3 pl-7 text-[12px] text-danger\">\n {problems.slice(0, 6).map((p) => (\n <li key={p}>{p}</li>\n ))}\n </ul>\n )}\n <div className=\"min-h-0 flex-1 overflow-y-auto\">\n {tab === 'props' && (\n <>\n <PanelHeading\n actions={\n selection && (\n <>\n <IconButton\n icon={Copy}\n title=\"Duplicate (Ctrl+D)\"\n light\n iconSize={16}\n onClick={duplicateSelected}\n />\n <IconButton icon={Trash2} title=\"Delete (Del)\" light iconSize={16} onClick={removeSelected} />\n </>\n )\n }\n >\n {selectedField ? 'FIELD' : selectedElement ? selectedElement.type.toUpperCase() : 'PROPERTIES'}\n </PanelHeading>\n {selectedField ? (\n <FieldProperties\n key={selectedField.id}\n field={selectedField}\n fields={fields}\n pageCount={pageCount}\n onChange={updateField}\n />\n ) : selectedElement ? (\n <ElementProperties\n key={selectedElement.id}\n element={selectedElement}\n pageCount={pageCount}\n onChange={updateElement}\n />\n ) : (\n <EmptyText>\n Select something on the document, drag a field or layout item in from the left, or use{' '}\n <b>Edit text</b> to change the document's own text.\n </EmptyText>\n )}\n </>\n )}\n\n {tab === 'fields' && (\n <>\n <PanelHeading>FILL ORDER</PanelHeading>\n {!fields.length && <EmptyText>No fields yet.</EmptyText>}\n <ol className=\"m-0 list-none p-0\">\n {fields.map((f, i) => (\n <li\n key={f.id}\n className={`flex items-center gap-1 border-b border-line-soft px-2 py-1 text-[12.5px] ${selection?.id === f.id ? 'bg-primary-tint' : ''}`}\n >\n <button\n type=\"button\"\n className=\"min-w-0 flex-1 cursor-pointer truncate text-left\"\n onClick={() => {\n select({ kind: 'field', id: f.id })\n goToPage(f.position.page)\n }}\n >\n <span className=\"text-faint\">p{f.position.page}</span> {f.label || f.key}{' '}\n <span className=\"text-muted\">· {TYPE_LABEL[f.type]}</span>\n {f.source === 'ai' && <span className=\"text-folder\"> · AI</span>}\n </button>\n <IconButton\n icon={ArrowUp}\n title=\"Move up\"\n iconSize={13}\n disabled={i === 0}\n onClick={() => setFields((l) => reorder(l, f.id, -1))}\n />\n <IconButton\n icon={ArrowDown}\n title=\"Move down\"\n iconSize={13}\n disabled={i === fields.length - 1}\n onClick={() => setFields((l) => reorder(l, f.id, 1))}\n />\n </li>\n ))}\n </ol>\n </>\n )}\n\n {tab === 'pages' &&\n (layout ? (\n <PagesPanel\n pages={layout.pages}\n counts={counts}\n onGoTo={goToPage}\n onMove={(n, step) => withDoc((d) => movePage(d, n, step))}\n onInsertBlank={(n) => withDoc((d) => insertBlankPage(d, n))}\n onDelete={(n) => (counts[n] ? setConfirmDeletePage(n) : withDoc((d) => deletePage(d, n)))}\n />\n ) : (\n <EmptyText>Loading pages…</EmptyText>\n ))}\n\n {tabs.find((t) => t.id === tab)?.content}\n </div>\n </aside>\n </div>\n\n {confirmDeletePage != null && (\n <ConfirmModal\n danger\n confirmLabel=\"Delete page\"\n onCancel={() => setConfirmDeletePage(null)}\n onConfirm={() => {\n const n = confirmDeletePage\n setConfirmDeletePage(null)\n setSelection(null)\n withDoc((d) => deletePage(d, n))\n }}\n >\n Delete page {confirmDeletePage}? Its {counts[confirmDeletePage]} fields and items go with it. (Undo brings\n them back; the uploaded file is not changed.)\n </ConfirmModal>\n )}\n\n </div>\n )\n}\n","import type { LucideIcon } from 'lucide-react'\nimport type { ButtonHTMLAttributes } from 'react'\n\nconst variants = {\n primary: 'bg-primary text-white enabled:hover:bg-primary-hover',\n secondary: 'border border-line-strong bg-white text-ink-2 enabled:hover:bg-hover',\n outline: 'border border-primary bg-white text-primary enabled:hover:bg-primary-soft',\n light: 'border border-line-strong bg-white text-primary enabled:hover:bg-primary-soft', // Filters\n heading: 'bg-heading text-white enabled:hover:bg-heading-hover', // matches panel headings\n success: 'bg-success text-white enabled:hover:bg-success-hover',\n brand: 'bg-brand text-white enabled:hover:bg-brand-hover',\n danger: 'bg-danger text-white enabled:hover:opacity-90',\n}\nconst sizes = { sm: 'px-3 py-1.5 text-[13px]', md: 'px-4 py-2 text-[13.5px]' }\n\nexport interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n variant?: keyof typeof variants\n size?: keyof typeof sizes\n /** Icon before the label. */\n icon?: LucideIcon\n}\n\n/** The app's button. `type` defaults to \"button\" so it never submits a form by accident. */\nexport default function Button({\n variant = 'primary',\n size = 'md',\n icon: Icon,\n className = '',\n type = 'button',\n children,\n ...rest\n}: ButtonProps) {\n return (\n <button\n type={type}\n className={`inline-flex cursor-pointer items-center justify-center gap-1.5 rounded-md font-semibold whitespace-nowrap transition disabled:cursor-not-allowed disabled:opacity-55 ${variants[variant]} ${sizes[size]} ${className}`}\n {...rest}\n >\n {Icon && <Icon size={size === 'sm' ? 15 : 16} />}\n {children}\n </button>\n )\n}\n","import type { ReactNode } from 'react'\nimport Button from './Button'\n\ninterface ModalProps {\n children: ReactNode\n onClose: () => void\n /** Tailwind width class, e.g. 'w-[560px]' */\n width: string\n}\n\n// Centered dialog over a dimmed backdrop; a click on the backdrop calls onClose.\nexport default function Modal({ children, onClose, width }: ModalProps) {\n return (\n <div className=\"fixed inset-0 z-[1200] flex items-center justify-center bg-[rgba(15,23,42,0.45)]\" onClick={onClose}>\n <div\n role=\"dialog\"\n aria-modal\n className={`${width} max-w-[calc(100vw-32px)] rounded-[10px] bg-white shadow-[0_20px_50px_rgba(0,0,0,0.25)]`}\n onClick={(e) => e.stopPropagation()}\n >\n {children}\n </div>\n </div>\n )\n}\n\n/** Title and × (disabled while e.g. an upload runs). */\nexport const ModalHeader = ({\n title,\n onClose,\n disabled,\n}: {\n title: string\n onClose: () => void\n disabled?: boolean\n}) => (\n <div className=\"flex items-center justify-between border-b border-line px-5 py-4\">\n <h3 className=\"m-0 text-[17px] font-semibold\">{title}</h3>\n <button\n type=\"button\"\n aria-label=\"Close\"\n disabled={disabled}\n onClick={onClose}\n className=\"cursor-pointer text-2xl leading-none text-muted enabled:hover:text-ink disabled:cursor-not-allowed disabled:opacity-40\"\n >\n ×\n </button>\n </div>\n)\n\nexport const ModalBody = ({ children }: { children: ReactNode }) => (\n <div className=\"space-y-4 px-5 py-4 text-[13.5px]\">{children}</div>\n)\n\n/** Buttons, right-aligned. */\nexport const ModalFooter = ({ children }: { children: ReactNode }) => (\n <div className=\"flex justify-end gap-2 border-t border-line px-5 py-3\">{children}</div>\n)\n\n/** \"Are you sure…?\" with Yes / No. */\nexport function ConfirmModal({\n children,\n confirmLabel = 'Yes',\n cancelLabel = 'No',\n danger,\n onConfirm,\n onCancel,\n}: {\n children: ReactNode\n confirmLabel?: string\n cancelLabel?: string\n /** Red confirm button (deletes). */\n danger?: boolean\n onConfirm: () => void\n onCancel: () => void\n}) {\n return (\n <Modal onClose={onCancel} width=\"w-[420px]\">\n <div className=\"px-6 pt-7 pb-6 text-sm\">{children}</div>\n <ModalFooter>\n <Button variant=\"secondary\" onClick={onCancel}>\n {cancelLabel}\n </Button>\n <Button variant={danger ? 'danger' : 'primary'} onClick={onConfirm}>\n {confirmLabel}\n </Button>\n </ModalFooter>\n </Modal>\n )\n}\n","import type { LucideIcon } from 'lucide-react'\nimport type { ButtonHTMLAttributes } from 'react'\n\nexport interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n /** Tooltip and accessible name. */\n title: string\n icon: LucideIcon\n /** Red on hover (delete). */\n danger?: boolean\n iconSize?: number\n /** On a coloured background (panel headings): white icon. */\n light?: boolean\n}\n\n/** Icon-only button: view, download, delete, edit, expand… */\nexport default function IconButton({\n title,\n icon: Icon,\n danger,\n iconSize = 18,\n light,\n className = '',\n ...rest\n}: IconButtonProps) {\n const tone = light\n ? 'text-white opacity-90 hover:bg-white/15 hover:opacity-100'\n : `text-muted hover:bg-line-soft ${danger ? 'hover:text-danger' : 'hover:text-primary'}`\n return (\n <button\n type=\"button\"\n title={title}\n aria-label={title}\n className={`inline-flex cursor-pointer items-center justify-center rounded p-1.5 transition disabled:cursor-not-allowed disabled:opacity-40 ${tone} ${className}`}\n {...rest}\n >\n <Icon size={iconSize} />\n </button>\n )\n}\n","import type { ReactNode } from 'react'\n\n/** White bordered box: the tree, the files list, the folder structure columns. */\nexport const Panel = ({ children, className = '' }: { children: ReactNode; className?: string }) => (\n <div className={`overflow-hidden rounded-lg border border-line bg-white ${className}`}>{children}</div>\n)\n\n/** Blue panel heading (FOLDER HIERARCHY, MODULES…) with optional buttons on the right. */\nexport const PanelHeading = ({ children, actions }: { children: ReactNode; actions?: ReactNode }) => (\n <div className=\"flex items-center justify-between bg-heading px-3.5 py-[11px] text-[13px] font-bold tracking-[0.02em] text-white\">\n {children}\n {actions && <span className=\"flex items-center gap-1 tracking-normal\">{actions}</span>}\n </div>\n)\n\n/** Grey hint in an empty list (\"No folders yet.\", \"Loading…\"). */\nexport const EmptyText = ({ children, className = '' }: { children: ReactNode; className?: string }) => (\n <div className={`m-0 px-3.5 py-3 text-[13px] text-muted ${className}`}>{children}</div>\n)\n","/** Pill toggle between a few views (\"Chats | New Chats\"). `sm`: a small one inside a panel. */\nexport default function SegmentedTabs<T extends string>({\n options,\n value,\n onChange,\n size = 'md',\n}: {\n options: { value: T; label: string }[]\n value: T\n onChange: (value: T) => void\n size?: 'sm' | 'md'\n}) {\n return (\n <div role=\"tablist\" className=\"inline-flex rounded-full border border-brand p-0.5\">\n {options.map((o) => (\n <button\n key={o.value}\n type=\"button\"\n role=\"tab\"\n aria-selected={o.value === value}\n onClick={() => onChange(o.value)}\n className={`cursor-pointer rounded-full font-semibold transition ${size === 'sm' ? 'px-3 py-0.5 text-[12px]' : 'px-5 py-1 text-[13px]'} ${\n o.value === value ? 'bg-brand text-white' : 'text-brand hover:bg-primary-soft'\n }`}\n >\n {o.label}\n </button>\n ))}\n </div>\n )\n}\n","import type { InputHTMLAttributes } from 'react'\n\n/** The app's checkbox (theme colour). `label` = accessible name when there is no visible text. */\nexport default function Checkbox({\n label,\n className = '',\n ...rest\n}: InputHTMLAttributes<HTMLInputElement> & { label: string }) {\n return (\n <input\n type=\"checkbox\"\n aria-label={label}\n className={`h-[18px] w-[18px] flex-none cursor-pointer accent-primary ${className}`}\n {...rest}\n />\n )\n}\n","import type { InputHTMLAttributes, ReactNode, SelectHTMLAttributes, TextareaHTMLAttributes } from 'react'\n\nexport const inputClass =\n 'w-full rounded-md border border-line-strong bg-white px-3 py-2 text-[13.5px] outline-none focus:border-primary'\n\n/** Label, the control, and its error under it. */\nexport function FormField({\n label,\n required,\n error,\n children,\n}: {\n label: string\n required?: boolean\n error?: string\n children: ReactNode\n}) {\n return (\n <label className=\"block text-[13.5px]\">\n <span className=\"mb-1 block font-semibold\">\n {label} {required && <span className=\"text-danger\">*</span>}\n </span>\n {children}\n {error && <small className=\"text-danger\">{error}</small>}\n </label>\n )\n}\n\nexport const TextInput = ({ className = '', ...rest }: InputHTMLAttributes<HTMLInputElement>) => (\n <input className={`${inputClass} ${className}`} {...rest} />\n)\n\nexport const Select = ({ className = '', ...rest }: SelectHTMLAttributes<HTMLSelectElement>) => (\n <select\n className={`rounded-md border border-line-strong bg-white px-2.5 py-1.5 text-[13px] outline-none ${className}`}\n {...rest}\n />\n)\n\nexport const TextArea = ({ className = '', ...rest }: TextareaHTMLAttributes<HTMLTextAreaElement>) => (\n <textarea className={`${inputClass} resize-none ${className}`} {...rest} />\n)\n","import type { ReactNode } from 'react'\nimport Checkbox from '../ui/Checkbox'\nimport { FormField, Select, TextArea, TextInput } from '../ui/FormField'\nimport { FONTS, type LayoutElement } from '../../core/form-logic'\nimport { fitInPage } from '../../core/coords'\n\nconst num = (s: string) => (s.trim() === '' || !Number.isFinite(Number(s)) ? undefined : Number(s))\nconst pct = (n: number) => Math.round(n * 1000) / 10\n\nconst Section = ({ title, children }: { title: string; children: ReactNode }) => (\n <section className=\"space-y-3 border-b border-line px-3.5 py-3\">\n <h4 className=\"m-0 text-[12px] font-bold tracking-wide text-muted uppercase\">{title}</h4>\n {children}\n </section>\n)\n\n/** A colour picker with a \"none\" option (no background / no border). */\nfunction ColorField({\n label,\n value,\n onChange,\n allowNone,\n}: {\n label: string\n value: string | undefined\n onChange: (v: string | undefined) => void\n allowNone?: boolean\n}) {\n return (\n <FormField label={label}>\n <span className=\"flex items-center gap-2\">\n <input\n type=\"color\"\n aria-label={label}\n className=\"h-8 w-12 cursor-pointer rounded border border-line-strong bg-white\"\n value={value ?? '#ffffff'}\n onChange={(e) => onChange(e.target.value)}\n />\n {allowNone && (\n <label className=\"inline-flex items-center gap-1.5 text-[12.5px]\">\n <Checkbox\n label={`No ${label.toLowerCase()}`}\n checked={!value}\n onChange={(e) => onChange(e.target.checked ? undefined : '#ffffff')}\n />\n None\n </label>\n )}\n </span>\n </FormField>\n )\n}\n\n/** Text, style, colours and position of the selected text / box / line. */\nexport default function ElementProperties({\n element: e,\n pageCount,\n onChange,\n}: {\n element: LayoutElement\n pageCount: number\n onChange: (e: LayoutElement) => void\n}) {\n const set = (patch: Partial<LayoutElement>) => onChange({ ...e, ...patch })\n const setPos = (k: 'x' | 'y' | 'width' | 'height', v: string) => {\n const n = num(v)\n if (n == null) return\n const p = { ...e.position, [k]: n / 100 }\n // A line may have zero height (horizontal) or zero width (vertical); boxes keep a minimum size.\n if (e.type === 'line')\n set({ position: { ...p, width: Math.min(p.width, 1 - p.x), height: Math.min(p.height, 1 - p.y) } })\n else {\n const { page: _page, ...fitted } = fitInPage({ page: e.page, ...p })\n void _page\n set({ position: fitted })\n }\n }\n\n return (\n <div className=\"text-[13px]\">\n {e.type === 'text' && (\n <Section title=\"Text\">\n {e.replaces != null && (\n <p className=\"m-0 rounded bg-primary-soft px-2 py-1 text-[12px] text-ink-2\">\n Replaces the original: “{e.replaces}”\n </p>\n )}\n <TextArea\n rows={4}\n aria-label=\"Text\"\n autoFocus\n value={e.text ?? ''}\n maxLength={5000}\n onChange={(ev) => set({ text: ev.target.value })}\n />\n <div className=\"grid grid-cols-2 gap-2\">\n <FormField label=\"Font\">\n <Select\n className=\"w-full\"\n value={e.font ?? 'sans'}\n onChange={(ev) => set({ font: ev.target.value as LayoutElement['font'] })}\n >\n {FONTS.map((f) => (\n <option key={f} value={f}>\n {{ sans: 'Sans (Helvetica)', serif: 'Serif (Times)', mono: 'Mono (Courier)' }[f]}\n </option>\n ))}\n </Select>\n </FormField>\n <FormField label=\"Size (pt)\">\n <TextInput\n type=\"number\"\n min={3}\n max={144}\n step={0.5}\n value={e.fontSize ?? 10}\n onChange={(ev) => {\n const n = num(ev.target.value)\n if (n != null && n >= 3 && n <= 144) set({ fontSize: n })\n }}\n />\n </FormField>\n </div>\n <div className=\"flex flex-wrap items-center gap-4\">\n {(['bold', 'italic'] as const).map((k) => (\n <label key={k} className=\"inline-flex items-center gap-1.5\">\n <Checkbox label={k} checked={!!e[k]} onChange={(ev) => set({ [k]: ev.target.checked || undefined })} />\n {k === 'bold' ? 'Bold' : 'Italic'}\n </label>\n ))}\n <Select\n aria-label=\"Alignment\"\n value={e.align ?? 'left'}\n onChange={(ev) => set({ align: ev.target.value as LayoutElement['align'] })}\n >\n <option value=\"left\">Left</option>\n <option value=\"center\">Centre</option>\n <option value=\"right\">Right</option>\n </Select>\n </div>\n <ColorField label=\"Text colour\" value={e.color ?? '#000000'} onChange={(v) => set({ color: v })} />\n </Section>\n )}\n\n <Section title=\"Colours\">\n {e.type !== 'line' && (\n <ColorField label=\"Background\" allowNone value={e.fill} onChange={(v) => set({ fill: v })} />\n )}\n {e.type !== 'text' && (\n <>\n <ColorField\n label={e.type === 'line' ? 'Line colour' : 'Border'}\n allowNone={e.type === 'box'}\n value={e.stroke}\n onChange={(v) => set({ stroke: v })}\n />\n <FormField label=\"Line width (pt)\">\n <TextInput\n type=\"number\"\n min={0}\n max={20}\n step={0.5}\n value={e.strokeWidth ?? 1}\n onChange={(ev) => {\n const n = num(ev.target.value)\n if (n != null && n >= 0 && n <= 20) set({ strokeWidth: n })\n }}\n />\n </FormField>\n </>\n )}\n {e.fill === '#ffffff' && (\n <p className=\"m-0 text-[12px] text-muted\">A white background hides the original under it.</p>\n )}\n </Section>\n\n <Section title=\"Position (% of page)\">\n <div className=\"grid grid-cols-2 gap-2\">\n <FormField label=\"Page\">\n <TextInput\n type=\"number\"\n min={1}\n max={pageCount}\n value={e.page}\n onChange={(ev) => {\n const p = num(ev.target.value)\n if (p && Number.isInteger(p) && p >= 1 && p <= pageCount) set({ page: p })\n }}\n />\n </FormField>\n <span />\n {(['x', 'y', 'width', 'height'] as const).map((k) => (\n <FormField key={k} label={{ x: 'Left', y: 'Top', width: 'Width', height: 'Height' }[k]}>\n <TextInput\n type=\"number\"\n step={0.1}\n value={pct(e.position[k])}\n onChange={(ev) => setPos(k, ev.target.value)}\n />\n </FormField>\n ))}\n </div>\n </Section>\n </div>\n )\n}\n","import { Paperclip, PenLine } from 'lucide-react'\nimport { useRef, useState } from 'react'\nimport Button from '../ui/Button'\nimport { inputClass } from '../ui/FormField'\nimport Modal, { ModalBody, ModalFooter, ModalHeader } from '../ui/Modal'\nimport SignaturePad from '../ui/SignaturePad'\nimport type { AnswerValue, FieldDef, FieldType } from '../../core/form-logic'\n\n// Text scales with the field's box (container query height), so it zooms with the page.\nconst box =\n 'h-full w-full rounded-sm border bg-primary-soft/70 px-1 text-[clamp(8px,62cqh,15px)] text-navy outline-none focus:border-primary focus:bg-white disabled:cursor-not-allowed'\n\n/** A PNG data URL (the signature pad's drawing) as a file to upload. */\nfunction pngFile(dataUrl: string, name: string) {\n const bin = atob(dataUrl.slice(dataUrl.indexOf(',') + 1))\n const bytes = new Uint8Array(bin.length)\n for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i)\n return new File([bytes], name.replace(/[\\\\/:*?\"<>|]/g, '_'), { type: 'image/png' })\n}\n\n// Typed in the full-size box on phones. 16 px text: iOS zooms the page into any smaller input it focuses.\nconst SHEET_TYPES: FieldType[] = ['text', 'textarea', 'number', 'date', 'email', 'phone', 'radio']\nconst INPUT_TYPE: Partial<Record<FieldType, string>> = { number: 'number', date: 'date', email: 'email', phone: 'tel' }\n\nfunction SheetInput({\n field,\n text,\n onChange,\n onDone,\n}: {\n field: FieldDef\n text: string\n onChange: (v: AnswerValue) => void\n onDone: () => void\n}) {\n if (field.type === 'radio')\n return (\n <div role=\"radiogroup\" aria-label={field.label || field.key} className=\"flex flex-col gap-1\">\n {(field.options ?? []).map((o) => (\n <label\n key={o}\n className=\"flex min-h-11 cursor-pointer items-center gap-3 rounded-md border border-line px-3 text-base\"\n >\n <input\n type=\"radio\"\n className=\"h-5 w-5 accent-primary\"\n name={`sheet-${field.id}`}\n checked={text === o}\n onChange={() => {\n onChange(o)\n onDone()\n }}\n />\n {o}\n </label>\n ))}\n </div>\n )\n if (field.type === 'textarea')\n return (\n <textarea\n autoFocus\n aria-label={field.label || field.key}\n className={`${inputClass} min-h-40 !text-base`}\n placeholder={field.placeholder}\n value={text}\n maxLength={field.maxLength}\n onChange={(e) => onChange(e.target.value)}\n />\n )\n return (\n <input\n autoFocus\n aria-label={field.label || field.key}\n type={INPUT_TYPE[field.type] ?? 'text'}\n className={`${inputClass} h-11 !text-base`}\n placeholder={field.placeholder}\n value={text}\n min={field.min}\n max={field.max}\n maxLength={field.maxLength}\n onChange={(e) => onChange(e.target.value)}\n onKeyDown={(e) => e.key === 'Enter' && onDone()}\n />\n )\n}\n\n/**\n * One answer control drawn inside its box on the page. `error` outlines it red; the message shows as a tooltip\n * and in the page's error list.\n */\nexport default function FieldControl({\n field,\n value,\n onChange,\n readOnly,\n error,\n fileName,\n onFile,\n onOpenFile,\n signatureSrc,\n compact,\n}: {\n field: FieldDef\n value: AnswerValue | undefined\n onChange: (v: AnswerValue) => void\n readOnly?: boolean\n error?: string\n fileName?: string\n onFile?: (file: File) => void\n onOpenFile?: () => void\n /** Image of a stored signature (the answer is its file id); signatures from before are data URLs. */\n signatureSrc?: string\n /** Phone / touch: typed answers open in a full-size box (the page's boxes are too small to type in). */\n compact?: boolean\n}) {\n const [signing, setSigning] = useState(false)\n const [editing, setEditing] = useState(false)\n const fileInput = useRef<HTMLInputElement>(null)\n const border = error ? 'border-danger' : 'border-primary/40'\n const common = {\n 'aria-label': field.label || field.key,\n 'aria-invalid': !!error,\n title: error || field.helpText || field.label,\n disabled: readOnly,\n }\n const text = typeof value === 'string' ? value : ''\n\n if (compact && !readOnly && SHEET_TYPES.includes(field.type)) {\n const name = field.label || field.key\n return (\n <>\n <button\n type=\"button\"\n {...common}\n onClick={() => setEditing(true)}\n className={`${box} ${border} flex cursor-pointer overflow-hidden text-left leading-tight ${\n field.type === 'textarea' ? 'items-start py-0.5 text-[clamp(8px,14cqh,15px)]' : 'items-center'\n }`}\n >\n <span className={`min-w-0 ${field.type === 'textarea' ? 'line-clamp-[8] whitespace-pre-wrap' : 'truncate'}`}>\n {text || <span className=\"text-faint\">{field.placeholder || ''}</span>}\n </span>\n </button>\n {editing && (\n <Modal onClose={() => setEditing(false)} width=\"w-[480px]\">\n <ModalHeader title={name} onClose={() => setEditing(false)} />\n <ModalBody>\n <SheetInput field={field} text={text} onChange={onChange} onDone={() => setEditing(false)} />\n {field.helpText && <p className=\"mt-2 mb-0 text-[13px] text-muted\">{field.helpText}</p>}\n {error && <p className=\"mt-2 mb-0 text-[13px] text-danger\">{error}</p>}\n </ModalBody>\n <ModalFooter>\n <Button onClick={() => setEditing(false)}>Done</Button>\n </ModalFooter>\n </Modal>\n )}\n </>\n )\n }\n\n switch (field.type) {\n case 'textarea':\n return (\n <textarea\n {...common}\n className={`${box} ${border} resize-none py-0.5 text-[clamp(8px,14cqh,15px)]`}\n placeholder={field.placeholder}\n value={text}\n maxLength={field.maxLength}\n onChange={(e) => onChange(e.target.value)}\n />\n )\n case 'dropdown':\n return (\n <select {...common} className={`${box} ${border}`} value={text} onChange={(e) => onChange(e.target.value)}>\n <option value=\"\">{field.placeholder || ''}</option>\n {(field.options ?? []).map((o) => (\n <option key={o}>{o}</option>\n ))}\n </select>\n )\n case 'radio':\n return (\n <div\n role=\"radiogroup\"\n {...common}\n className={`flex h-full w-full flex-wrap items-center gap-x-3 rounded-sm border px-1 text-[clamp(8px,55cqh,14px)] ${border} bg-primary-soft/50`}\n >\n {(field.options ?? []).map((o) => (\n <label key={o} className=\"inline-flex cursor-pointer items-center gap-1\">\n <input\n type=\"radio\"\n className=\"accent-primary\"\n name={field.id}\n checked={text === o}\n disabled={readOnly}\n onChange={() => onChange(o)}\n />\n {o}\n </label>\n ))}\n </div>\n )\n case 'checkbox':\n return (\n <label\n className={`flex h-full w-full items-center justify-center rounded-sm border ${border} bg-primary-soft/50`}\n >\n <input\n type=\"checkbox\"\n {...common}\n className=\"h-[min(80cqh,80cqw)] w-[min(80cqh,80cqw)] cursor-pointer accent-primary\"\n checked={value === true}\n onChange={(e) => onChange(e.target.checked)}\n />\n </label>\n )\n case 'signature':\n return (\n <>\n <button\n type=\"button\"\n {...common}\n onClick={() => setSigning(true)}\n className={`flex h-full w-full cursor-pointer items-center justify-center rounded-sm border ${border} bg-primary-soft/50 disabled:cursor-default`}\n >\n {text ? (\n <img\n src={text.startsWith('data:') ? text : signatureSrc}\n alt=\"Signature\"\n className=\"max-h-full max-w-full object-contain\"\n />\n ) : (\n <span className=\"inline-flex items-center gap-1 text-[clamp(8px,45cqh,13px)] text-primary\">\n <PenLine size={12} /> Sign\n </span>\n )}\n </button>\n {signing && (\n <Modal onClose={() => setSigning(false)} width=\"w-[640px]\">\n <ModalHeader title={`Sign: ${field.label || field.key}`} onClose={() => setSigning(false)} />\n <ModalBody>\n <SignaturePad\n onCancel={() => setSigning(false)}\n onSave={(url) => {\n // Stored as a PNG file (the answer gets its id); without an upload (editor preview) kept as is.\n if (onFile) onFile(pngFile(url, `Signature - ${field.label || field.key}.png`))\n else onChange(url)\n setSigning(false)\n }}\n />\n </ModalBody>\n </Modal>\n )}\n </>\n )\n case 'file':\n return (\n <div className={`flex h-full w-full items-center gap-1 rounded-sm border px-1 ${border} bg-primary-soft/50`}>\n <input\n ref={fileInput}\n type=\"file\"\n hidden\n onChange={(e) => {\n const f = e.target.files?.[0]\n if (f) onFile?.(f)\n e.target.value = ''\n }}\n />\n {value && (\n <button\n type=\"button\"\n onClick={onOpenFile}\n className=\"min-w-0 flex-1 cursor-pointer truncate text-left text-[clamp(8px,50cqh,13px)] text-primary underline\"\n >\n {fileName || 'Attached file'}\n </button>\n )}\n {!readOnly && (\n <button\n type=\"button\"\n {...common}\n onClick={() => fileInput.current?.click()}\n className=\"inline-flex flex-none cursor-pointer items-center gap-1 text-[clamp(8px,50cqh,13px)] text-primary\"\n >\n <Paperclip size={12} /> {value ? 'Replace' : 'Attach'}\n </button>\n )}\n </div>\n )\n default: {\n const type = INPUT_TYPE[field.type] ?? 'text'\n return (\n <input\n {...common}\n type={type}\n className={`${box} ${border}`}\n placeholder={field.placeholder}\n value={text}\n min={field.min}\n max={field.max}\n maxLength={field.maxLength}\n onChange={(e) => onChange(e.target.value)}\n />\n )\n }\n }\n}\n","import { useRef, useState } from 'react'\nimport Button from './Button'\n\n/**\n * Draw a signature with mouse, pen or finger. onSave gets a PNG data URL (transparent background).\n * Scaled down to at most `maxWidth` pixels wide so it stays small enough to send.\n */\nexport default function SignaturePad({\n onSave,\n onCancel,\n maxWidth = 600,\n}: {\n onSave: (dataUrl: string) => void\n onCancel: () => void\n maxWidth?: number\n}) {\n const canvas = useRef<HTMLCanvasElement>(null)\n const last = useRef<{ x: number; y: number } | null>(null)\n const [empty, setEmpty] = useState(true)\n\n const point = (e: React.PointerEvent<HTMLCanvasElement>) => {\n const r = e.currentTarget.getBoundingClientRect()\n return {\n x: ((e.clientX - r.left) / r.width) * e.currentTarget.width,\n y: ((e.clientY - r.top) / r.height) * e.currentTarget.height,\n }\n }\n const draw = (e: React.PointerEvent<HTMLCanvasElement>) => {\n const ctx = canvas.current?.getContext('2d')\n if (!ctx || !last.current) return\n const p = point(e)\n ctx.lineWidth = 3\n ctx.lineCap = 'round'\n ctx.strokeStyle = '#111827'\n ctx.beginPath()\n ctx.moveTo(last.current.x, last.current.y)\n ctx.lineTo(p.x, p.y)\n ctx.stroke()\n last.current = p\n setEmpty(false)\n }\n const clear = () => {\n const c = canvas.current\n c?.getContext('2d')?.clearRect(0, 0, c.width, c.height)\n setEmpty(true)\n }\n const save = () => {\n const c = canvas.current\n if (!c) return\n const scale = Math.min(1, maxWidth / c.width)\n const out = document.createElement('canvas')\n out.width = Math.round(c.width * scale)\n out.height = Math.round(c.height * scale)\n out.getContext('2d')?.drawImage(c, 0, 0, out.width, out.height)\n onSave(out.toDataURL('image/png'))\n }\n\n return (\n <div className=\"space-y-3\">\n <canvas\n ref={canvas}\n width={800}\n height={260}\n aria-label=\"Signature drawing area\"\n className=\"h-[200px] w-full touch-none rounded-md border border-dashed border-line-strong bg-surface\"\n onPointerDown={(e) => {\n e.currentTarget.setPointerCapture(e.pointerId)\n last.current = point(e)\n }}\n onPointerMove={draw}\n onPointerUp={() => (last.current = null)}\n />\n <div className=\"flex justify-end gap-2\">\n <Button variant=\"secondary\" onClick={clear}>\n Clear\n </Button>\n <Button variant=\"secondary\" onClick={onCancel}>\n Cancel\n </Button>\n <Button disabled={empty} onClick={save}>\n Use signature\n </Button>\n </div>\n </div>\n )\n}\n","import { useState, type ReactNode } from 'react'\nimport Checkbox from '../ui/Checkbox'\nimport { FormField, Select, TextArea, TextInput } from '../ui/FormField'\nimport { FIELD_TYPES, type Condition, type FieldDef, type FieldType } from '../../core/form-logic'\nimport { fitInPage } from '../../core/coords'\nimport { TYPE_LABEL } from '../constants'\nimport ConditionBuilder from './ConditionBuilder'\n\nconst num = (s: string) => (s.trim() === '' || !Number.isFinite(Number(s)) ? undefined : Number(s))\nconst pct = (n: number) => Math.round(n * 1000) / 10\n\nconst Section = ({ title, children }: { title: string; children: ReactNode }) => (\n <section className=\"space-y-3 border-b border-line px-3.5 py-3\">\n <h4 className=\"m-0 text-[12px] font-bold tracking-wide text-muted uppercase\">{title}</h4>\n {children}\n </section>\n)\n\nconst CONDITIONS = [\n ['visibleWhen', 'Show only when'],\n ['requiredWhen', 'Required when'],\n ['readOnlyWhen', 'Read-only when'],\n] as const\n\n/** Everything about the selected field: identity, rules, options, conditions, position (in % of the page). */\nexport default function FieldProperties({\n field,\n fields,\n pageCount,\n onChange,\n}: {\n field: FieldDef\n fields: FieldDef[]\n pageCount: number\n onChange: (f: FieldDef) => void\n}) {\n const set = (patch: Partial<FieldDef>) => onChange({ ...field, ...patch })\n const others = fields.filter((f) => f.id !== field.id)\n const [optionsText, setOptionsText] = useState((field.options ?? []).join('\\n'))\n const hasOptions = field.type === 'dropdown' || field.type === 'radio'\n const textual = !['checkbox', 'file', 'signature', 'dropdown', 'radio'].includes(field.type)\n const setPos = (k: 'x' | 'y' | 'width' | 'height', v: string) => {\n const n = num(v)\n if (n != null) set({ position: fitInPage({ ...field.position, [k]: n / 100 }) })\n }\n\n return (\n <div className=\"text-[13px]\">\n <Section title=\"Field\">\n {field.source === 'ai' && (\n <p className=\"m-0 rounded bg-folder/15 px-2 py-1 text-[12px]\">\n Suggested automatically\n {field.confidence != null && ` (${Math.round(field.confidence * 100)}% sure)`}. Check it.\n </p>\n )}\n <p className=\"m-0 font-mono text-[11px] text-faint\">ID {field.id}</p>\n <FormField label=\"Label\">\n <TextInput value={field.label} onChange={(e) => set({ label: e.target.value })} />\n </FormField>\n {/* Made from the label (unique, kept in step by conditions), never typed: no clashes. */}\n <FormField label=\"Key (automatic, from the label)\">\n <TextInput value={field.key} readOnly className=\"text-faint\" />\n </FormField>\n <FormField label=\"Type\">\n <Select className=\"w-full\" value={field.type} onChange={(e) => set({ type: e.target.value as FieldType })}>\n {FIELD_TYPES.map((t) => (\n <option key={t} value={t}>\n {TYPE_LABEL[t]}\n </option>\n ))}\n </Select>\n </FormField>\n {textual && (\n <FormField label=\"Placeholder\">\n <TextInput\n value={field.placeholder ?? ''}\n onChange={(e) => set({ placeholder: e.target.value || undefined })}\n />\n </FormField>\n )}\n {field.type !== 'file' && field.type !== 'signature' && (\n <FormField label={field.type === 'checkbox' ? 'Checked by default' : 'Default value'}>\n {field.type === 'checkbox' ? (\n <Checkbox\n label=\"Checked by default\"\n checked={field.defaultValue === 'true'}\n onChange={(e) => set({ defaultValue: e.target.checked ? 'true' : undefined })}\n />\n ) : (\n <TextInput\n value={field.defaultValue ?? ''}\n onChange={(e) => set({ defaultValue: e.target.value || undefined })}\n />\n )}\n </FormField>\n )}\n <FormField label=\"Help text (shown as tooltip)\">\n <TextInput value={field.helpText ?? ''} onChange={(e) => set({ helpText: e.target.value || undefined })} />\n </FormField>\n <div className=\"flex flex-wrap gap-4\">\n {(['required', 'readOnly', 'hidden'] as const).map((k) => (\n <label key={k} className=\"inline-flex items-center gap-1.5\">\n <Checkbox label={k} checked={!!field[k]} onChange={(e) => set({ [k]: e.target.checked || undefined })} />\n {{ required: 'Required', readOnly: 'Read-only', hidden: 'Hidden' }[k]}\n </label>\n ))}\n </div>\n </Section>\n\n {hasOptions && (\n <Section title=\"Options (one per line)\">\n <TextArea\n rows={4}\n value={optionsText}\n onChange={(e) => {\n setOptionsText(e.target.value)\n set({\n options: [\n ...new Set(\n e.target.value\n .split('\\n')\n .map((s) => s.trim())\n .filter(Boolean),\n ),\n ],\n })\n }}\n />\n </Section>\n )}\n\n {textual && (\n <Section title=\"Validation\">\n <div className=\"grid grid-cols-2 gap-2\">\n <FormField label=\"Min length\">\n <TextInput\n type=\"number\"\n min={0}\n value={field.minLength ?? ''}\n onChange={(e) => set({ minLength: num(e.target.value) })}\n />\n </FormField>\n <FormField label=\"Max length\">\n <TextInput\n type=\"number\"\n min={0}\n value={field.maxLength ?? ''}\n onChange={(e) => set({ maxLength: num(e.target.value) })}\n />\n </FormField>\n {field.type === 'number' && (\n <>\n <FormField label=\"Min value\">\n <TextInput\n type=\"number\"\n value={field.min ?? ''}\n onChange={(e) => set({ min: num(e.target.value) })}\n />\n </FormField>\n <FormField label=\"Max value\">\n <TextInput\n type=\"number\"\n value={field.max ?? ''}\n onChange={(e) => set({ max: num(e.target.value) })}\n />\n </FormField>\n </>\n )}\n </div>\n <FormField label=\"Regex (whole value must match)\">\n <TextInput\n className=\"font-mono\"\n value={field.pattern ?? ''}\n onChange={(e) => set({ pattern: e.target.value || undefined })}\n />\n </FormField>\n </Section>\n )}\n\n {CONDITIONS.map(([key, title]) => {\n const cond = field[key]\n return (\n <Section key={key} title={title}>\n {cond ? (\n <ConditionBuilder\n value={cond}\n fields={others}\n onChange={(c: Condition) => set({ [key]: c.rules.length ? c : undefined })}\n />\n ) : (\n <button\n type=\"button\"\n disabled={!others.length}\n className=\"cursor-pointer text-primary hover:underline disabled:cursor-not-allowed disabled:text-faint\"\n onClick={() =>\n set({ [key]: { logic: 'and', rules: [{ fieldKey: others[0].key, op: 'equals', value: '' }] } })\n }\n >\n + Add condition\n </button>\n )}\n </Section>\n )\n })}\n\n <Section title=\"Position (% of page)\">\n <div className=\"grid grid-cols-2 gap-2\">\n <FormField label=\"Page\">\n <TextInput\n type=\"number\"\n min={1}\n max={pageCount}\n value={field.position.page}\n onChange={(e) => {\n const p = num(e.target.value)\n if (p && Number.isInteger(p) && p >= 1 && p <= pageCount)\n set({ position: { ...field.position, page: p } })\n }}\n />\n </FormField>\n <span />\n {(['x', 'y', 'width', 'height'] as const).map((k) => (\n <FormField key={k} label={{ x: 'Left', y: 'Top', width: 'Width', height: 'Height' }[k]}>\n <TextInput\n type=\"number\"\n step={0.1}\n value={pct(field.position[k])}\n onChange={(e) => setPos(k, e.target.value)}\n />\n </FormField>\n ))}\n </div>\n </Section>\n </div>\n )\n}\n","import type { FieldType } from '../core/form-logic'\n\nexport const TYPE_LABEL: Record<FieldType, string> = {\n text: 'Text',\n textarea: 'Text area',\n number: 'Number',\n date: 'Date',\n email: 'Email',\n phone: 'Phone',\n dropdown: 'Dropdown',\n radio: 'Radio group',\n checkbox: 'Checkbox',\n file: 'File upload',\n signature: 'Signature',\n}\n\nexport const ZOOMS = [0.5, 0.75, 1, 1.25, 1.5, 2]\n\n/** CSS width of a page at zoom 1 is its width in points (72 dpi) × this. */\nexport const BASE_SCALE = 96 / 72\n","import { Plus, X } from 'lucide-react'\nimport Button from '../ui/Button'\nimport IconButton from '../ui/IconButton'\nimport { Select, TextInput } from '../ui/FormField'\nimport { OPERATORS, type Condition, type FieldDef, type Operator, type Rule } from '../../core/form-logic'\n\nconst OP_LABEL: Record<Operator, string> = {\n equals: 'equals',\n notEquals: 'does not equal',\n contains: 'contains',\n startsWith: 'starts with',\n endsWith: 'ends with',\n greaterThan: 'is greater than',\n lessThan: 'is less than',\n between: 'is between',\n isEmpty: 'is empty',\n isNotEmpty: 'is not empty',\n}\nconst isGroup = (r: Rule | Condition): r is Condition => 'rules' in r\n\n/**\n * IF <field> <operator> <value> rows joined by AND / OR, with nested groups. `fields` = the fields a rule may\n * point at (every field but the one being edited). The API rejects unknown and circular references again.\n */\nexport default function ConditionBuilder({\n value,\n onChange,\n fields,\n depth = 0,\n}: {\n value: Condition\n onChange: (c: Condition) => void\n fields: FieldDef[]\n depth?: number\n}) {\n const setRule = (i: number, r: Rule | Condition) =>\n onChange({ ...value, rules: value.rules.map((x, j) => (j === i ? r : x)) })\n const remove = (i: number) => onChange({ ...value, rules: value.rules.filter((_, j) => j !== i) })\n const newRule = (): Rule => ({ fieldKey: fields[0]?.key ?? '', op: 'equals', value: '' })\n\n return (\n <div className={`space-y-2 ${depth ? 'rounded-md border border-line bg-surface p-2' : ''}`}>\n <div className=\"flex items-center gap-2 text-[12px]\">\n Match\n <Select\n aria-label=\"Match all or any\"\n value={value.logic}\n onChange={(e) => onChange({ ...value, logic: e.target.value as Condition['logic'] })}\n >\n <option value=\"and\">ALL (AND)</option>\n <option value=\"or\">ANY (OR)</option>\n </Select>\n </div>\n {value.rules.map((r, i) =>\n isGroup(r) ? (\n <div key={i} className=\"flex items-start gap-1\">\n <div className=\"flex-1\">\n <ConditionBuilder value={r} onChange={(c) => setRule(i, c)} fields={fields} depth={depth + 1} />\n </div>\n <IconButton icon={X} title=\"Remove group\" danger iconSize={14} onClick={() => remove(i)} />\n </div>\n ) : (\n <div key={i} className=\"flex flex-wrap items-center gap-1\">\n <Select\n aria-label=\"Field\"\n value={r.fieldKey}\n onChange={(e) => setRule(i, { ...r, fieldKey: e.target.value })}\n >\n {!fields.some((f) => f.key === r.fieldKey) && <option value={r.fieldKey}>{r.fieldKey || '—'}</option>}\n {fields.map((f) => (\n <option key={f.id} value={f.key}>\n {f.label || f.key}\n </option>\n ))}\n </Select>\n <Select\n aria-label=\"Operator\"\n value={r.op}\n onChange={(e) => setRule(i, { ...r, op: e.target.value as Operator })}\n >\n {OPERATORS.map((op) => (\n <option key={op} value={op}>\n {OP_LABEL[op]}\n </option>\n ))}\n </Select>\n {r.op !== 'isEmpty' && r.op !== 'isNotEmpty' && (\n <TextInput\n aria-label=\"Value\"\n className=\"!w-24 !py-1\"\n value={r.value ?? ''}\n onChange={(e) => setRule(i, { ...r, value: e.target.value })}\n />\n )}\n {r.op === 'between' && (\n <TextInput\n aria-label=\"Upper value\"\n className=\"!w-24 !py-1\"\n value={r.value2 ?? ''}\n onChange={(e) => setRule(i, { ...r, value2: e.target.value })}\n />\n )}\n <IconButton icon={X} title=\"Remove rule\" danger iconSize={14} onClick={() => remove(i)} />\n </div>\n ),\n )}\n <div className=\"flex gap-2\">\n <Button\n size=\"sm\"\n variant=\"secondary\"\n icon={Plus}\n disabled={!fields.length}\n onClick={() => onChange({ ...value, rules: [...value.rules, newRule()] })}\n >\n Rule\n </Button>\n {depth < 2 && (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n icon={Plus}\n disabled={!fields.length}\n onClick={() => onChange({ ...value, rules: [...value.rules, { logic: 'and', rules: [newRule()] }] })}\n >\n Group\n </Button>\n )}\n </div>\n </div>\n )\n}\n","import type { CSSProperties } from 'react'\nimport type { LayoutElement } from '../../core/form-logic'\nimport { boxStyle } from '../../core/coords'\n\n// Same faces as the completed PDF (Helvetica / Times / Courier), so screen and export match.\nconst FAMILY = {\n sans: 'Helvetica, Arial, sans-serif',\n serif: '\"Times New Roman\", Times, serif',\n mono: '\"Courier New\", Courier, monospace',\n}\n\n/** Points on a page `pageHeight` points tall, as a size that zooms with the page (cqh of the page overlay). */\nconst ptToCqh = (pt: number, pageHeight: number) => `${(pt / pageHeight) * 100}cqh`\n\n/**\n * One of the admin's items drawn on its page: text (with optional background that hides the original under it),\n * box, or line. Purely visual; the editor wraps it for selecting and dragging.\n */\nexport default function LayoutElementView({\n element: e,\n pageHeight,\n className = '',\n style,\n}: {\n element: LayoutElement\n pageHeight: number\n className?: string\n style?: CSSProperties\n}) {\n const stroke = ptToCqh(e.strokeWidth ?? 1, pageHeight)\n const base: CSSProperties = { ...boxStyle({ page: e.page, ...e.position }), ...style }\n\n if (e.type === 'line')\n return (\n <svg className={`absolute overflow-visible ${className}`} style={base} aria-hidden>\n {/* A flat side is drawn at 0, so a horizontal / vertical line stays straight in a box given a minimum size. */}\n <line\n x1=\"0\"\n y1=\"0\"\n x2={e.position.width > 0 ? '100%' : '0'}\n y2={e.position.height > 0 ? '100%' : '0'}\n stroke={e.stroke ?? '#000000'}\n style={{ strokeWidth: stroke }}\n />\n </svg>\n )\n\n return (\n <div\n className={`absolute overflow-hidden whitespace-pre-wrap ${className}`}\n style={{\n ...base,\n background: e.fill,\n border: e.type === 'box' && e.stroke ? `${stroke} solid ${e.stroke}` : undefined,\n color: e.color ?? '#000000',\n fontFamily: FAMILY[e.font ?? 'sans'],\n fontSize: ptToCqh(e.fontSize ?? 10, pageHeight),\n fontWeight: e.bold ? 700 : 400,\n fontStyle: e.italic ? 'italic' : 'normal',\n textAlign: e.align ?? 'left',\n lineHeight: 1.2,\n wordBreak: 'break-word',\n }}\n >\n {e.type === 'text' ? e.text : null}\n </div>\n )\n}\n","import { ArrowDown, ArrowUp, FilePlus2, Trash2 } from 'lucide-react'\nimport IconButton from '../ui/IconButton'\nimport type { LayoutPage } from '../../core/form-logic'\n\n/**\n * The document's pages in order: original pages (with their number in the uploaded PDF) and blank ones.\n * Move, delete, add a blank page after. `onGoTo` scrolls the document to a page.\n */\nexport default function PagesPanel({\n pages,\n counts,\n onMove,\n onDelete,\n onInsertBlank,\n onGoTo,\n}: {\n pages: LayoutPage[]\n /** Fields + items per page (1-based index), shown so deleting a page is not a surprise. */\n counts: Record<number, number>\n onMove: (page: number, step: -1 | 1) => void\n onDelete: (page: number) => void\n onInsertBlank: (after: number) => void\n onGoTo: (page: number) => void\n}) {\n return (\n <div className=\"text-[12.5px]\">\n <button\n type=\"button\"\n className=\"flex w-full cursor-pointer items-center gap-1.5 border-b border-line-soft px-3 py-2 text-primary hover:bg-primary-soft\"\n onClick={() => onInsertBlank(0)}\n >\n <FilePlus2 size={14} /> Blank page at the start\n </button>\n <ol className=\"m-0 list-none p-0\">\n {pages.map((p, i) => {\n const n = i + 1\n return (\n <li key={i} className=\"flex items-center gap-1 border-b border-line-soft px-2 py-1\">\n <button\n type=\"button\"\n className=\"min-w-0 flex-1 cursor-pointer truncate text-left\"\n onClick={() => onGoTo(n)}\n >\n <b>Page {n}</b> <span className=\"text-muted\">{p.source ? `· original p.${p.source}` : '· blank'}</span>\n {counts[n] ? <span className=\"text-faint\"> · {counts[n]} items</span> : null}\n </button>\n <IconButton\n icon={ArrowUp}\n title=\"Move page up\"\n iconSize={13}\n disabled={n === 1}\n onClick={() => onMove(n, -1)}\n />\n <IconButton\n icon={ArrowDown}\n title=\"Move page down\"\n iconSize={13}\n disabled={n === pages.length}\n onClick={() => onMove(n, 1)}\n />\n <IconButton\n icon={FilePlus2}\n title=\"Add blank page after\"\n iconSize={13}\n onClick={() => onInsertBlank(n)}\n />\n <IconButton\n icon={Trash2}\n title=\"Delete page\"\n danger\n iconSize={13}\n disabled={pages.length <= 1}\n onClick={() => onDelete(n)}\n />\n </li>\n )\n })}\n </ol>\n </div>\n )\n}\n","import type { PDFDocumentProxy, RenderTask } from 'pdfjs-dist'\nimport { useEffect, useRef, useState, type ReactNode } from 'react'\nimport type { LayoutPage } from '../../core/form-logic'\nimport { toLines, type PdfTextItem, type TextLine } from '../../core/layout'\nimport { loadPdfJs } from '../pdfjs'\nimport { BASE_SCALE } from '../constants'\n\n/** A PDF: URL, Blob / File, bytes, or a (possibly async) function returning one of those. */\nexport type PdfBytes = string | URL | Blob | ArrayBuffer | Uint8Array\nexport type PdfSource = PdfBytes | (() => PdfBytes | Promise<PdfBytes>)\n\nexport async function readPdf(src: PdfSource): Promise<ArrayBuffer | Uint8Array> {\n const v = typeof src === 'function' ? await src() : src\n if (typeof v === 'string' || v instanceof URL) {\n const res = await fetch(v)\n if (!res.ok) throw new Error(`Could not load the PDF (${res.status})`)\n return res.arrayBuffer()\n }\n // A copy: pdf.js hands the buffer to its worker, which would empty the caller's array.\n return v instanceof Blob ? v.arrayBuffer() : (v instanceof ArrayBuffer ? new Uint8Array(v) : v).slice()\n}\n\nexport interface PageSize {\n width: number\n height: number\n}\n\n/**\n * The document, page under page: original PDF pages exactly as pdf.js draws them, and blank pages, in the order\n * of `pages` (the version's layout; omitted = the original pages in order). Pages are drawn when they scroll into\n * view and again after a zoom. `overlay(page)` is laid over each page in a box of the page's size: children\n * placed with percentage positions (utils/coords.ts boxStyle) stay on their spot at every zoom, and font sizes in\n * `cqh` scale with the page. With `withText`, overlay also gets the original text lines of the page.\n */\nexport default function PdfPages({\n load,\n zoom,\n pages,\n overlay,\n withText,\n onLoaded,\n onError,\n}: {\n /** The PDF: a URL, its bytes, or a function returning either. Read once per mount. */\n load: PdfSource\n zoom: number\n pages?: LayoutPage[] | null\n overlay?: (page: number, lines: TextLine[] | undefined) => ReactNode\n withText?: boolean\n /** Sizes (points) of the original pages and the open document, once the PDF is open. */\n onLoaded?: (sizes: PageSize[], pdf: PDFDocumentProxy) => void\n onError?: (err: unknown) => void\n}) {\n const [doc, setDoc] = useState<PDFDocumentProxy | null>(null)\n const [sizes, setSizes] = useState<PageSize[]>([])\n const loadRef = useRef(load)\n const callbacks = useRef({ onLoaded, onError })\n useEffect(() => {\n callbacks.current = { onLoaded, onError }\n })\n\n useEffect(() => {\n let live = true\n let opened: PDFDocumentProxy | null = null\n Promise.all([loadPdfJs(), readPdf(loadRef.current)])\n // isEvalSupported off (pdf.js < 5.5; newer never evals): pdf.js never compiles code from the document (fonts).\n .then(([lib, data]) => lib.getDocument({ data, ...({ isEvalSupported: false } as object) }).promise)\n .then(async (pdf) => {\n opened = pdf\n const list: PageSize[] = []\n for (let i = 1; i <= pdf.numPages; i++) {\n const vp = (await pdf.getPage(i)).getViewport({ scale: 1 })\n list.push({ width: vp.width, height: vp.height })\n }\n if (!live) return\n setSizes(list)\n setDoc(pdf)\n callbacks.current.onLoaded?.(list, pdf)\n })\n .catch((err) => live && callbacks.current.onError?.(err))\n return () => {\n live = false\n void opened?.destroy()\n }\n }, [])\n\n if (!doc) return <p className=\"p-6 text-center text-[13px] text-muted\">Loading document…</p>\n const shown: LayoutPage[] = pages ?? sizes.map((s, i) => ({ source: i + 1, ...s }))\n return (\n // min-w-full + w-max: centred when narrower than the panel, fully scrollable (left edge too) when wider.\n <div className=\"flex w-max min-w-full flex-col gap-4 p-4\">\n {shown.map((p, i) => (\n <PdfPage\n key={i}\n doc={doc}\n pageNo={i + 1}\n source={p.source}\n size={p.source ? (sizes[p.source - 1] ?? p) : p}\n zoom={zoom}\n withText={withText}\n overlay={overlay}\n />\n ))}\n </div>\n )\n}\n\nfunction PdfPage({\n doc,\n pageNo,\n source,\n size,\n zoom,\n withText,\n overlay,\n}: {\n doc: PDFDocumentProxy\n pageNo: number\n source: number | null\n size: PageSize\n zoom: number\n withText?: boolean\n overlay?: (page: number, lines: TextLine[] | undefined) => ReactNode\n}) {\n const box = useRef<HTMLDivElement>(null)\n const canvas = useRef<HTMLCanvasElement>(null)\n const [visible, setVisible] = useState(false)\n const [lines, setLines] = useState<{ source: number; lines: TextLine[] } | null>(null)\n const width = size.width * BASE_SCALE * zoom\n const height = size.height * BASE_SCALE * zoom\n\n useEffect(() => {\n const el = box.current\n if (!el || typeof IntersectionObserver === 'undefined') return setVisible(true)\n const io = new IntersectionObserver(([e]) => e.isIntersecting && setVisible(true), { rootMargin: '600px' })\n io.observe(el)\n return () => io.disconnect()\n }, [])\n\n useEffect(() => {\n if (!visible || !source || !canvas.current) return\n let task: RenderTask | null = null\n let live = true\n const el = canvas.current\n void doc.getPage(source).then((page) => {\n if (!live) return\n const ratio = window.devicePixelRatio || 1\n const viewport = page.getViewport({ scale: BASE_SCALE * zoom * ratio })\n el.width = Math.floor(viewport.width)\n el.height = Math.floor(viewport.height)\n task = page.render({ canvas: el, viewport })\n task.promise.catch(() => {}) // cancelled by a newer zoom\n })\n return () => {\n live = false\n task?.cancel()\n }\n }, [doc, source, visible, zoom])\n\n // The original's text lines, read once per page when asked for (Edit text).\n useEffect(() => {\n if (!withText || !visible || !source || lines?.source === source) return\n let live = true\n void doc\n .getPage(source)\n .then(async (page) => {\n const content = await page.getTextContent()\n // Real font names (for bold / italic) are known once the page's fonts are loaded by drawing it.\n const fontOf = (id: string) => {\n try {\n return page.commonObjs.has(id) ? (page.commonObjs.get(id) as { name?: string })?.name : undefined\n } catch {\n return undefined\n }\n }\n if (live) setLines({ source, lines: toLines(content.items as PdfTextItem[], page.view, fontOf) })\n })\n .catch(() => live && setLines({ source, lines: [] }))\n return () => {\n live = false\n }\n }, [doc, source, visible, withText, lines])\n\n return (\n <div\n ref={box}\n data-page={pageNo}\n className=\"relative mx-auto flex-none bg-white shadow-[0_1px_6px_rgba(15,23,42,0.18)]\"\n style={{ width, height }}\n >\n {source && <canvas ref={canvas} className=\"absolute inset-0 h-full w-full\" aria-label={`Page ${pageNo}`} />}\n <div className=\"absolute inset-0 [container-type:size]\">\n {overlay?.(pageNo, withText && lines?.source === source ? lines.lines : undefined)}\n </div>\n </div>\n )\n}\n","// pdf.js is loaded when the first document opens: it needs browser APIs (DOMMatrix) the moment it loads.\n// The worker: whatever the app set (setPdfWorker or pdfjs GlobalWorkerOptions), else the matching build on a CDN.\nexport type PdfJs = typeof import('pdfjs-dist')\n\nlet workerSrc: string | undefined\nlet workerPort: Worker | undefined\n\n/**\n * Where pdf.js loads its worker from. Call once before the first document opens, e.g. with Vite:\n * `setPdfWorker(new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).href)`\n * or pass a Worker. Without it the worker comes from unpkg.com (same pdfjs-dist version).\n */\nexport function setPdfWorker(src: string | Worker) {\n if (typeof src === 'string') workerSrc = src\n else workerPort = src\n}\n\nlet pdfjs: Promise<PdfJs> | null = null\nexport const loadPdfJs = () =>\n (pdfjs ??= import('pdfjs-dist').then((lib) => {\n const o = lib.GlobalWorkerOptions\n if (workerPort) o.workerPort = workerPort\n else if (workerSrc) o.workerSrc = workerSrc\n else if (!o.workerSrc && !o.workerPort)\n // ponytail: CDN fallback needs internet; self-host via setPdfWorker for offline / strict CSP.\n o.workerSrc = `https://unpkg.com/pdfjs-dist@${lib.version}/build/pdf.worker.min.mjs`\n return lib\n }))\n","import { Minus, Plus } from 'lucide-react'\nimport IconButton from '../ui/IconButton'\n\nimport { ZOOMS } from '../constants'\n\n/** − 100 % +. `zoom` may be in between the steps (fitted to the screen); the buttons go to the next step. */\nexport default function ZoomControls({ zoom, onChange }: { zoom: number; onChange: (z: number) => void }) {\n const lower = [...ZOOMS].reverse().find((z) => z < zoom - 0.01)\n const higher = ZOOMS.find((z) => z > zoom + 0.01)\n return (\n <div className=\"inline-flex items-center gap-1 text-[13px]\">\n <IconButton icon={Minus} title=\"Zoom out\" disabled={lower == null} onClick={() => onChange(lower!)} />\n <span className=\"w-12 text-center tabular-nums\">{Math.round(zoom * 100)}%</span>\n <IconButton icon={Plus} title=\"Zoom in\" disabled={higher == null} onClick={() => onChange(higher!)} />\n </div>\n )\n}\n","import { useEffect, useState } from 'react'\nimport { BASE_SCALE, ZOOMS } from '../constants'\n\n// PdfPages pads the pages by 16 px on each side.\nconst PADDING = 32\n\n/**\n * Zoom that fits a page `pageWidthPt` points wide into the scroll panel (a big screen gets a big page, up to the\n * highest zoom), following the\n * panel's size (phone, tablet, a resized window) until the user picks a zoom. Pass `ref` to the scroll panel.\n */\nexport function useFitZoom(pageWidthPt: number) {\n const [el, ref] = useState<HTMLElement | null>(null)\n const [fit, setFit] = useState(1)\n const [chosen, setZoom] = useState<number | null>(null)\n useEffect(() => {\n if (!el) return\n const measure = () => {\n const room = el.clientWidth - PADDING\n if (room > 0) setFit(Math.min(ZOOMS[ZOOMS.length - 1], Math.max(0.3, room / (pageWidthPt * BASE_SCALE))))\n }\n measure()\n if (typeof ResizeObserver === 'undefined') return\n const watch = new ResizeObserver(measure)\n watch.observe(el)\n return () => watch.disconnect()\n }, [el, pageWidthPt])\n return { zoom: chosen ?? fit, setZoom, ref }\n}\n\n/** Small screen or touch: answers are typed in a full-size box instead of on the page. */\nexport function useCompact() {\n const query = '(max-width: 767px), (pointer: coarse)'\n const get = () => typeof window !== 'undefined' && !!window.matchMedia?.(query).matches\n const [compact, setCompact] = useState(get)\n useEffect(() => {\n const m = window.matchMedia?.(query)\n if (!m) return\n const on = () => setCompact(m.matches)\n m.addEventListener('change', on)\n return () => m.removeEventListener('change', on)\n }, [])\n return compact\n}\n","import { useCallback, useState } from 'react'\n\nconst LIMIT = 100\n\n/**\n * Undo / redo over one value. `set(next, false)` changes it without a history step (during a drag; call\n * `checkpoint()` when the drag starts so the whole drag undoes in one step).\n */\nexport function useUndo<T>(initial: T) {\n const [state, setState] = useState({ past: [] as T[], present: initial, future: [] as T[] })\n const set = useCallback((next: T | ((v: T) => T), record = true) => {\n setState((s) => {\n const value = typeof next === 'function' ? (next as (v: T) => T)(s.present) : next\n if (value === s.present) return s\n return record\n ? { past: [...s.past, s.present].slice(-LIMIT), present: value, future: [] }\n : { ...s, present: value }\n })\n }, [])\n const checkpoint = useCallback(\n () => setState((s) => ({ past: [...s.past, s.present].slice(-LIMIT), present: s.present, future: [] })),\n [],\n )\n const undo = useCallback(\n () =>\n setState((s) =>\n s.past.length\n ? { past: s.past.slice(0, -1), present: s.past[s.past.length - 1], future: [s.present, ...s.future] }\n : s,\n ),\n [],\n )\n const redo = useCallback(\n () =>\n setState((s) =>\n s.future.length ? { past: [...s.past, s.present], present: s.future[0], future: s.future.slice(1) } : s,\n ),\n [],\n )\n const reset = useCallback((v: T) => setState({ past: [], present: v, future: [] }), [])\n return {\n value: state.present,\n set,\n checkpoint,\n undo,\n redo,\n reset,\n canUndo: state.past.length > 0,\n canRedo: state.future.length > 0,\n }\n}\n","import { ArrowLeft, Download, Paperclip, Save, Send } from 'lucide-react'\nimport type { PDFDocumentProxy } from 'pdfjs-dist'\nimport { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'\nimport { boxStyle } from '../core/coords'\nimport {\n checkAnswer,\n defaultAnswers,\n fieldStates,\n validateAnswers,\n type Answers,\n type AnswerValue,\n type FieldDef,\n type Layout,\n} from '../core/form-logic'\nimport { toLines, type PdfTextItem, type TextLine } from '../core/layout'\nimport { toQuestions } from '../core/questions'\nimport FieldControl from './components/FieldControl'\nimport LayoutElementView from './components/LayoutElementView'\nimport PdfPages, { type PdfSource } from './components/PdfPages'\nimport QuestionList from './components/QuestionList'\nimport ZoomControls from './components/ZoomControls'\nimport { useCompact, useFitZoom } from './hooks/useFitZoom'\nimport Button from './ui/Button'\nimport { ConfirmModal } from './ui/Modal'\nimport SegmentedTabs from './ui/SegmentedTabs'\n\n/** A file answer (upload or signature PNG) as your storage knows it. The answer's value is its `id`. */\nexport interface AnswerFileInfo {\n id: string\n fieldKey: string\n fileName: string\n /** Bytes, for the size shown next to the name. */\n fileSize?: number\n}\n\nexport interface FormFillerProps {\n /** The PDF: URL, Blob / File, bytes, or a function returning one. */\n pdf: PdfSource\n fields: FieldDef[]\n layout?: Layout | null\n title?: ReactNode\n /** A draft to continue. Default: each field's default value. */\n initialAnswers?: Answers\n /** Files already attached to these answers. */\n files?: AnswerFileInfo[]\n /** View only (a submitted form): no Save / Submit. */\n readOnly?: boolean\n /**\n * Save a draft (`submit: false`, also autosave) or submit (answers already pass validateAnswers — check again on\n * your server). Throw `{ errors: { fieldKey: message } }` to show errors at the fields.\n */\n onSave?: (answers: Answers, info: { submit: boolean }) => unknown\n /** Called after a successful submit. */\n onSubmitted?: (answers: Answers) => void\n /** Autosave delay in ms after the last change; false = off. Default 2500. */\n autosave?: number | false\n /** Store a file / signature PNG; resolve with its id. Without it, file fields are read-only and signatures are kept as PNG data URLs in the answer. */\n onUpload?: (field: FieldDef, file: File) => Promise<AnswerFileInfo>\n /** The bytes of a stored file (shows stored signatures, opens / downloads attachments). */\n loadFile?: (id: string) => Promise<Blob>\n onBack?: () => void\n /** Your own items at the end of the toolbar (e.g. \"Download completed PDF\"). */\n toolbar?: ReactNode\n notify?: (message: string) => void\n onError?: (error: unknown) => void\n /** Extra classes on the root. It fills its container's height (`h-full`) by default: give the container a height. */\n className?: string\n}\n\nconst humanSize = (n?: number) => {\n if (!n) return ''\n const units = ['B', 'KB', 'MB', 'GB']\n let i = 0\n while (n >= 1024 && i < units.length - 1) {\n n /= 1024\n i++\n }\n return `${n.toFixed(i ? 1 : 0)} ${units[i]}`\n}\n\nfunction download(blob: Blob, name: string) {\n const url = URL.createObjectURL(blob)\n const a = document.createElement('a')\n a.href = url\n a.download = name\n a.click()\n setTimeout(() => URL.revokeObjectURL(url), 1000)\n}\n\n/**\n * The PDF with the form's fields on it. The user fills them in place (or as a searchable list); answers autosave\n * through onSave, Submit checks them with the same rules as validateAnswers on your server.\n */\nexport default function FormFiller({\n pdf: pdfSource,\n fields,\n layout = null,\n title,\n initialAnswers,\n files: initialFiles = [],\n readOnly = false,\n onSave,\n onSubmitted,\n autosave = 2500,\n onUpload,\n loadFile,\n onBack,\n toolbar,\n notify = () => {},\n onError: notifyError = (e) => console.error(e),\n className = 'h-full',\n}: FormFillerProps) {\n const [answers, setAnswers] = useState<Answers>(() => initialAnswers ?? defaultAnswers(fields))\n const [files, setFiles] = useState<AnswerFileInfo[]>(initialFiles)\n // Stored signature images (file id → object URL).\n const [signatures, setSignatures] = useState<Record<string, string>>({})\n const [errors, setErrors] = useState<Record<string, string>>({})\n const [pdfWidth, setPdfWidth] = useState(595)\n const pageWidth = Math.max(...(layout?.pages.map((p) => p.width) ?? [pdfWidth]))\n const { zoom, setZoom, ref: fitRef } = useFitZoom(pageWidth)\n const compact = useCompact()\n const [saveState, setSaveState] = useState<'saved' | 'dirty' | 'saving' | 'error'>('saved')\n const [hasSaved, setHasSaved] = useState(!!initialAnswers)\n const [confirmSubmit, setConfirmSubmit] = useState(false)\n const [loadError, setLoadError] = useState('')\n const queue = useRef<Promise<unknown>>(Promise.resolve())\n const latest = useRef<Answers>(answers)\n useEffect(() => {\n latest.current = answers\n }, [answers])\n\n const states = useMemo(() => fieldStates(fields, answers), [fields, answers])\n // Document = the PDF with the controls on it; List = the same answers as questions, with search and filters.\n const [view, setView] = useState<'document' | 'list'>('document')\n // The PDF's text per original page, read when the list is first shown: questions get their own words as titles.\n const [pdf, setPdf] = useState<PDFDocumentProxy | null>(null)\n const [pageText, setPageText] = useState<Record<number, TextLine[]> | null>(null)\n useEffect(() => {\n if (view !== 'list' || !pdf || pageText) return\n let live = true\n ;(async () => {\n const out: Record<number, TextLine[]> = {}\n for (let i = 1; i <= pdf.numPages; i++) {\n const page = await pdf.getPage(i)\n out[i] = toLines((await page.getTextContent()).items as PdfTextItem[], page.view)\n }\n if (live) setPageText(out)\n })().catch(() => live && setPageText({}))\n return () => {\n live = false\n }\n }, [view, pdf, pageText])\n // Rebuilt when fields are shown / hidden by conditions, not on every keystroke.\n const visibleKeys = fields\n .filter((f) => states[f.key]?.visible)\n .map((f) => f.key)\n .join(',')\n const questions = useMemo(() => {\n const keys = new Set(visibleKeys.split(','))\n const textOf = (page: number) => {\n const source = layout ? layout.pages[page - 1]?.source : page\n return source ? pageText?.[source] : undefined\n }\n return toQuestions(\n fields.filter((f) => keys.has(f.key)),\n textOf,\n )\n }, [fields, layout, visibleKeys, pageText])\n\n // Loads the stored signatures shown on the page (once each).\n const loading = useRef(new Set<string>())\n useEffect(() => {\n if (!loadFile) return\n for (const f of fields) {\n const v = answers[f.key]\n if (f.type !== 'signature' || typeof v !== 'string' || !v || v.startsWith('data:')) continue\n if (signatures[v] || loading.current.has(v)) continue\n loading.current.add(v)\n loadFile(v)\n .then((blob) => setSignatures((m) => ({ ...m, [v]: URL.createObjectURL(blob) })))\n .catch(() => loading.current.delete(v))\n }\n }, [fields, answers, signatures, loadFile])\n const urls = useRef(signatures)\n useEffect(() => {\n urls.current = signatures\n }, [signatures])\n useEffect(() => () => Object.values(urls.current).forEach((u) => URL.revokeObjectURL(u)), [])\n\n // Saves run one after another, so a slow save never overwrites a newer one.\n const save = useCallback(\n (submit = false) => {\n if (!onSave) return Promise.resolve(true)\n const run = queue.current.then(async () => {\n setSaveState('saving')\n try {\n await onSave(latest.current, { submit })\n setHasSaved(true)\n setSaveState('saved')\n return true\n } catch (err) {\n setSaveState('error')\n const fieldErrors = (err as { errors?: unknown })?.errors\n if (fieldErrors && typeof fieldErrors === 'object') setErrors(fieldErrors as Record<string, string>)\n notifyError(err)\n return false\n }\n })\n queue.current = run\n return run\n },\n [onSave, notifyError],\n )\n\n useEffect(() => {\n if (readOnly || autosave === false || !onSave || saveState !== 'dirty') return\n const t = setTimeout(() => void save(), autosave)\n return () => clearTimeout(t)\n }, [answers, readOnly, autosave, onSave, saveState, save])\n\n function change(f: FieldDef, v: AnswerValue) {\n setAnswers((a) => ({ ...a, [f.key]: v }))\n setSaveState('dirty')\n if (errors[f.key]) {\n const err = checkAnswer(f, v, states[f.key]?.required)\n setErrors((all) => {\n const rest = { ...all }\n delete rest[f.key]\n return err ? { ...rest, [f.key]: err } : rest\n })\n }\n }\n\n async function attach(f: FieldDef, file: File) {\n if (!onUpload) return\n try {\n const info = await onUpload(f, file)\n setFiles((list) => [...list.filter((x) => x.fieldKey !== f.key), info])\n if (f.type === 'signature') setSignatures((m) => ({ ...m, [info.id]: URL.createObjectURL(file) }))\n change(f, info.id)\n } catch (err) {\n notifyError(err)\n }\n }\n\n const fetchFile = (id: string, then: (blob: Blob) => void) =>\n loadFile?.(id).then(then, () => notifyError('Unable to open the file'))\n\n async function submit() {\n setConfirmSubmit(false)\n const found = validateAnswers(fields, answers)\n setErrors(found)\n if (Object.keys(found).length) return notifyError('Some answers need attention')\n if (await save(true)) {\n notify('Submitted')\n onSubmitted?.(latest.current)\n }\n }\n\n if (loadError) return <p className=\"fl-root p-6 text-center text-sm text-danger\">{loadError}</p>\n\n const fileOf = (id: AnswerValue | undefined) => files.find((x) => x.id === id)\n // Files of upload fields that are answers now (signatures show on the page; replaced files are gone).\n const attachments = files.filter((x) =>\n fields.some((f) => f.type === 'file' && f.key === x.fieldKey && answers[f.key] === x.id),\n )\n const errorList = fields.filter((f) => errors[f.key])\n // One field's answer control (the same on the document and in the list), with the required star.\n const control = (f: FieldDef) => (\n <>\n <FieldControl\n field={f}\n value={answers[f.key]}\n readOnly={readOnly || states[f.key].readOnly || (f.type === 'file' && !onUpload)}\n compact={compact}\n error={errors[f.key]}\n onChange={(v) => change(f, v)}\n fileName={fileOf(answers[f.key])?.fileName}\n signatureSrc={signatures[String(answers[f.key])]}\n onFile={onUpload ? (file) => void attach(f, file) : undefined}\n onOpenFile={() =>\n fetchFile(String(answers[f.key]), (blob) => {\n const url = URL.createObjectURL(blob)\n window.open(url, '_blank')\n setTimeout(() => URL.revokeObjectURL(url), 60000)\n })\n }\n />\n {states[f.key].required && !readOnly && (\n <span className=\"pointer-events-none absolute -top-1.5 -right-1.5 text-[12px] font-bold text-danger\">*</span>\n )}\n </>\n )\n\n return (\n <div className={`fl-root flex min-h-[min(600px,100dvh)] flex-col bg-white text-ink ${className}`}>\n <div className=\"flex flex-wrap items-center gap-3 border-b border-line bg-white px-4 py-2.5\">\n {onBack && (\n <Button variant=\"secondary\" size=\"sm\" icon={ArrowLeft} onClick={onBack}>\n Back\n </Button>\n )}\n {title != null && (\n <h2 className=\"m-0 min-w-0 flex-1 truncate text-[17px] font-semibold max-sm:text-[15px] sm:flex-none\">\n {title}\n </h2>\n )}\n <SegmentedTabs\n options={[\n { value: 'document', label: 'Document' },\n { value: 'list', label: 'List' },\n ]}\n value={view}\n onChange={setView}\n />\n <div className=\"ml-auto flex flex-wrap items-center gap-2 max-sm:w-full max-sm:justify-end\">\n {view === 'document' && <ZoomControls zoom={zoom} onChange={setZoom} />}\n {!readOnly && onSave && (\n <>\n <span className=\"text-right text-[12px] text-muted sm:w-24\" aria-live=\"polite\">\n {\n {\n saved: hasSaved ? 'Draft saved' : '',\n dirty: 'Unsaved',\n saving: 'Saving…',\n error: 'Not saved',\n }[saveState]\n }\n </span>\n <Button variant=\"secondary\" size=\"sm\" icon={Save} title=\"Save draft\" onClick={() => void save()}>\n <span className=\"max-sm:hidden\">Save draft</span>\n </Button>\n <Button size=\"sm\" icon={Send} onClick={() => setConfirmSubmit(true)}>\n Submit\n </Button>\n </>\n )}\n {toolbar}\n </div>\n </div>\n {attachments.length > 0 && (\n <div className=\"flex flex-wrap items-center gap-x-3 gap-y-1 border-b border-line bg-white px-4 py-1.5 text-[12.5px]\">\n <span className=\"inline-flex items-center gap-1 font-semibold text-ink-2\">\n <Paperclip size={13} /> Attachments\n </span>\n {attachments.map((x) => (\n <button\n key={x.id}\n type=\"button\"\n title={`Download ${x.fileName}`}\n disabled={!loadFile}\n className=\"inline-flex max-w-full cursor-pointer items-center gap-1 text-primary hover:underline disabled:cursor-default\"\n onClick={() => fetchFile(x.id, (blob) => download(blob, x.fileName))}\n >\n <span className=\"truncate\">{x.fileName}</span>\n {x.fileSize ? <span className=\"text-muted\">({humanSize(x.fileSize)})</span> : null}\n {loadFile && <Download size={12} />}\n </button>\n ))}\n </div>\n )}\n {errorList.length > 0 && (\n <ul className=\"m-0 border-b border-danger/30 bg-danger/5 px-6 py-2 text-[13px] text-danger\">\n {errorList.map((f) => (\n <li key={f.key}>\n Page {f.position.page}: {errors[f.key]}\n </li>\n ))}\n </ul>\n )}\n {view === 'list' && (\n <div className=\"flex-1 overflow-auto bg-hover\">\n <QuestionList questions={questions} answers={answers} control={control} />\n </div>\n )}\n {/* Kept while the list shows, so the pages need not be drawn again. */}\n <div ref={fitRef} className={`flex-1 overflow-auto bg-hover ${view === 'list' ? 'hidden' : ''}`}>\n <PdfPages\n load={pdfSource}\n zoom={zoom}\n pages={layout?.pages}\n onLoaded={(sizes, doc) => {\n setPdf(doc)\n if (sizes.length) setPdfWidth(Math.max(...sizes.map((p) => p.width)))\n }}\n onError={() => setLoadError('The document could not be shown')}\n overlay={(page) => [\n // The admin's text / boxes / lines first, the answer controls on top.\n ...(layout?.elements ?? [])\n .filter((el) => el.page === page)\n .map((el) => (\n <LayoutElementView key={el.id} element={el} pageHeight={layout!.pages[page - 1]?.height ?? 842} />\n )),\n ...fields\n .filter((f) => f.position.page === page && states[f.key]?.visible)\n .map((f) => (\n <div key={f.id} className=\"absolute [container-type:size]\" style={boxStyle(f.position)}>\n {control(f)}\n </div>\n )),\n ]}\n />\n </div>\n {confirmSubmit && (\n <ConfirmModal confirmLabel=\"Submit\" onConfirm={() => void submit()} onCancel={() => setConfirmSubmit(false)}>\n Submit this form? You cannot change the answers afterwards.\n </ConfirmModal>\n )}\n </div>\n )\n}\n","import { Search, SlidersHorizontal } from 'lucide-react'\nimport { Fragment, useMemo, useState, type CSSProperties, type ReactNode } from 'react'\nimport Button from '../ui/Button'\nimport { Chip, ChipGroup, RemovableChip } from '../ui/Chip'\nimport { EmptyText } from '../ui/Panel'\nimport Popover from '../ui/Popover'\nimport SegmentedTabs from '../ui/SegmentedTabs'\nimport { Tag } from '../ui/Tag'\nimport { TYPE_LABEL } from '../constants'\nimport type { Answers, FieldDef, FieldType } from '../../core/form-logic'\nimport {\n choicesOf,\n matches,\n NO_FILTER,\n pickedOf,\n type Filled,\n type Question,\n type QuestionFilter,\n} from '../../core/questions'\n\nconst PAGE = 100\n\n// Box for a control in the list (controls size their text to the box, as on the page).\nconst SIZE: Partial<Record<FieldType, string>> = {\n checkbox: 'h-7 w-7 lg:h-8 lg:w-8',\n textarea: 'h-20 w-full',\n signature: 'h-16 w-56',\n file: 'h-9 w-full sm:w-72',\n radio: 'h-9 w-full',\n}\n// Parts of an option grid: its rows (the rest of the parts come after it).\nconst gridSize = (q: Question) => q.rows?.reduce((a, b) => a + b, 0) ?? 0\nconst wide = (t: FieldType) => t === 'textarea' || t === 'radio' || t === 'file'\n\n/**\n * The form as questions with search and filters: only the questions that match are shown. Answers are the\n * same as on the document; `control(field)` draws a field's answer control.\n */\nexport default function QuestionList({\n questions,\n answers,\n control,\n}: {\n questions: Question[]\n answers: Answers\n control: (f: FieldDef) => ReactNode\n}) {\n const [filter, setFilter] = useState<QuestionFilter>(NO_FILTER)\n const [shown, setShown] = useState(PAGE)\n // The answers the filter last looked at: typing into a remark while \"Remark empty\" is on must not make the\n // question vanish mid-word. The list follows new answers when a filter changes.\n const [seen, setSeen] = useState(answers)\n const set = (patch: Partial<QuestionFilter>) => {\n setFilter((f) => ({ ...f, ...patch }))\n setSeen(answers)\n setShown(PAGE)\n }\n const list = useMemo(() => questions.filter((q) => matches(q, seen, filter)), [questions, seen, filter])\n const count = (f: (q: Question) => boolean) => questions.filter(f).length\n const types = useMemo(() => [...new Set(questions.flatMap((q) => q.parts.map((p) => p.field.type)))], [questions])\n const categories = useMemo(\n () => [...new Set(questions.map((q) => q.category).filter((c): c is string => !!c))],\n [questions],\n )\n // Tag filters (\"Classification\", \"Risk\", \"Assigned to\"): every key read from the form, with its values.\n const tagValues = useMemo(() => {\n const m = new Map<string, Set<string>>()\n for (const q of questions)\n for (const [k, v] of Object.entries(q.tags ?? {})) m.set(k, (m.get(k) ?? new Set()).add(v))\n return [...m].map(\n ([k, vs]) => [k, [...vs].sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))] as const,\n )\n }, [questions])\n // Remarks / Comments filters only for forms that have such boxes.\n const has = (re: RegExp) => questions.some((q) => q.parts.some((p) => re.test(p.name) || re.test(p.field.label)))\n const progress = (\n [\n ['answered', 'Answered', 'Answered', 'Not answered'],\n ['remarks', 'Remarks', 'Written', 'Empty'],\n ['comments', 'Comments', 'Written', 'Empty'],\n ] as const\n ).filter(([key]) => key === 'answered' || has(key === 'remarks' ? /remark/i : /comment/i))\n const setTag = (key: string, value: string | null) => {\n const tags = { ...filter.tags }\n if (value == null) delete tags[key]\n else tags[key] = value\n set({ tags })\n }\n\n // Answer choices, each on its own (\"Yes\", \"No\", \"NA\", \"As Expected\"…), in the form's order: those offered by\n // two questions or more, with how many offer them.\n const choiceCount = useMemo(() => {\n const m = new Map<string, number>()\n for (const q of questions) for (const c of new Set(choicesOf(q))) m.set(c, (m.get(c) ?? 0) + 1)\n return [...m].filter(([, n]) => n >= 2).slice(0, 40)\n }, [questions])\n const [choiceMode, setChoiceMode] = useState<'offered' | 'picked'>('offered')\n const choiceValue = choiceMode === 'picked' ? filter.picked : filter.choices\n // Ticked counts follow the answers as they are given.\n const pickedCount = useMemo(() => {\n const m = new Map<string, number>()\n for (const q of questions) for (const c of new Set(pickedOf(q, answers))) m.set(c, (m.get(c) ?? 0) + 1)\n return m\n }, [questions, answers])\n\n // The filters in use, as chips to take them off one by one.\n const inUse: { label: string; clear: () => void }[] = [\n ...progress\n .filter(([key]) => filter[key] !== 'all')\n .map(([key, title, yes, no]) => ({\n label: `${title}: ${filter[key] === 'filled' ? yes : no}`,\n clear: () => set({ [key]: 'all' }),\n })),\n ...(filter.type !== 'all'\n ? [{ label: `Type: ${TYPE_LABEL[filter.type]}`, clear: () => set({ type: 'all' }) }]\n : []),\n ...Object.entries(filter.tags).map(([k, v]) => ({ label: `${k}: ${v}`, clear: () => setTag(k, null) })),\n ...(filter.choices !== 'all'\n ? [{ label: `Has option: ${filter.choices}`, clear: () => set({ choices: 'all' }) }]\n : []),\n ...(filter.picked !== 'all' ? [{ label: `Answered: ${filter.picked}`, clear: () => set({ picked: 'all' }) }] : []),\n ]\n const active = JSON.stringify(filter) !== JSON.stringify(NO_FILTER)\n\n return (\n <div className=\"w-full p-3 sm:p-4 lg:px-6\">\n <div className=\"sticky top-0 z-10 -mx-3 mb-3 flex flex-col gap-2 border-b border-line bg-white/95 px-3 py-2.5 shadow-sm backdrop-blur sm:-mx-4 sm:px-4 lg:-mx-6 lg:px-6\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <label className=\"relative min-w-0 flex-1 basis-full sm:basis-72\">\n <Search size={15} className=\"absolute top-1/2 left-3 -translate-y-1/2 text-muted\" />\n <input\n type=\"search\"\n aria-label=\"Search questions\"\n placeholder=\"Search questions, sections, answers…\"\n value={filter.search}\n onChange={(e) => set({ search: e.target.value })}\n className=\"w-full rounded-full border border-line-strong bg-surface py-1.5 pr-3 pl-9 text-[13px] outline-none focus:border-primary focus:bg-white max-sm:text-base\"\n />\n </label>\n <Popover label=\"Filters\" icon={SlidersHorizontal} badge={inUse.length} align=\"right\">\n <div className=\"flex flex-col gap-4\">\n <FilterSection title=\"Progress\">\n {progress.map(([key, title, yes, no]) => (\n <ChipGroup\n key={key}\n title={title}\n value={filter[key] === 'all' ? null : filter[key]}\n onChange={(v) => set({ [key]: (v ?? 'all') as Filled })}\n options={[\n { value: 'filled', label: yes },\n { value: 'empty', label: no },\n ]}\n />\n ))}\n </FilterSection>\n {choiceCount.length > 0 && (\n <FilterSection\n title=\"Answers\"\n action={\n <SegmentedTabs\n size=\"sm\"\n value={choiceMode}\n onChange={(m) => {\n setChoiceMode(m)\n // The picked choice carries over to the other way of filtering on it.\n const c = filter.choices !== 'all' ? filter.choices : filter.picked\n if (c !== 'all')\n set(m === 'picked' ? { picked: c, choices: 'all' } : { choices: c, picked: 'all' })\n }}\n options={[\n { value: 'offered', label: 'Has option' },\n { value: 'picked', label: 'Answered' },\n ]}\n />\n }\n >\n {/* One row of choices: questions that offer it, or that were answered with it. */}\n <ChipGroup\n value={choiceValue === 'all' ? null : choiceValue}\n onChange={(v) => set(choiceMode === 'picked' ? { picked: v ?? 'all' } : { choices: v ?? 'all' })}\n options={choiceCount.map(([c, n]) => ({\n value: c,\n label: c,\n count: choiceMode === 'picked' ? (pickedCount.get(c) ?? 0) : n,\n }))}\n />\n </FilterSection>\n )}\n {tagValues.length > 0 && (\n <FilterSection title=\"Question details\">\n {tagValues.map(([key, values]) => (\n <ChipGroup\n key={key}\n title={key}\n value={filter.tags[key] ?? null}\n onChange={(v) => setTag(key, v)}\n options={values.map((v) => ({ value: v, label: v, count: count((q) => q.tags?.[key] === v) }))}\n />\n ))}\n </FilterSection>\n )}\n {types.length > 1 && (\n <FilterSection title=\"Answer type\">\n <ChipGroup\n value={filter.type === 'all' ? null : filter.type}\n onChange={(v) => set({ type: (v ?? 'all') as QuestionFilter['type'] })}\n options={types.map((t) => ({\n value: t,\n label: TYPE_LABEL[t],\n count: count((q) => q.parts.some((p) => p.field.type === t)),\n }))}\n />\n </FilterSection>\n )}\n </div>\n </Popover>\n <span className=\"text-[12.5px] text-muted sm:ml-auto\" aria-live=\"polite\">\n <b className=\"font-semibold text-ink-2\">{list.length}</b> of {questions.length} questions\n </span>\n {active && (\n <Button variant=\"secondary\" size=\"sm\" onClick={() => set(NO_FILTER)}>\n Clear all\n </Button>\n )}\n </div>\n {categories.length > 0 && (\n <div role=\"group\" aria-label=\"Category\" className=\"flex flex-wrap items-center gap-1.5\">\n <span className=\"mr-1 text-[11.5px] font-semibold tracking-wide text-muted uppercase\">Category</span>\n <Chip\n label=\"All\"\n count={questions.length}\n active={filter.category === 'all'}\n onClick={() => set({ category: 'all' })}\n />\n {categories.map((c) => (\n <Chip\n key={c}\n label={c}\n count={count((q) => q.category === c)}\n active={filter.category === c}\n onClick={() => set({ category: filter.category === c ? 'all' : c })}\n />\n ))}\n </div>\n )}\n {inUse.length > 0 && (\n <div className=\"flex flex-wrap items-center gap-1.5\">\n {inUse.map((f) => (\n <RemovableChip key={f.label} label={f.label} onRemove={f.clear} />\n ))}\n </div>\n )}\n </div>\n\n {list.length === 0 && <EmptyText>No question matches these filters.</EmptyText>}\n <ul className=\"m-0 flex list-none flex-col gap-2 p-0\">\n {list.slice(0, shown).map((q, i, all) => (\n <Fragment key={q.id}>\n {/* The numbered question these answers belong to, once over its run of answers. */}\n {q.section && q.section !== all[i - 1]?.section && (\n <li className=\"mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 border-b border-line pb-1\">\n <span className=\"text-[13.5px] font-semibold text-primary lg:text-[15px]\">{q.section}</span>\n {Object.entries(q.tags ?? {}).map(([k, v]) => (\n <Tag key={k} tone=\"muted\">\n {k}: {v}\n </Tag>\n ))}\n </li>\n )}\n {/* What a run of rows on one scale asks, once over them. */}\n {q.intro && (q.intro !== all[i - 1]?.intro || q.section !== all[i - 1]?.section) && (\n <li className=\"rounded-md border-l-4 border-primary bg-primary-soft px-3 py-2 text-[13px] font-medium text-ink-2 lg:text-[14px]\">\n {q.intro}\n </li>\n )}\n <li className=\"rounded-lg border border-line bg-white p-3\">\n <div className=\"mb-2 flex items-start gap-2\">\n <p className=\"m-0 min-w-0 flex-1 text-[13.5px] font-semibold whitespace-pre-line text-ink-2 lg:text-[15px]\">\n {q.title}\n </p>\n {q.category && <Tag tone=\"muted\">{q.category}</Tag>}\n <Tag tone=\"muted\">p.{q.page}</Tag>\n </div>\n {q.columns ? (\n <TableGrid q={q} control={control} />\n ) : (\n <>\n {q.rows && <OptionGrid rows={q.rows} parts={q.parts.slice(0, gridSize(q))} control={control} />}\n {/* Parts after an option grid (the \"Finding\" box under its options) are laid out on their own. */}\n {q.parts.length > gridSize(q) && (\n <div className={`flex flex-wrap items-end gap-x-4 gap-y-2 ${q.rows ? 'mt-3' : ''}`}>\n {q.parts.slice(gridSize(q)).map(({ field, name }) => (\n <div\n key={field.id}\n className={`flex min-w-0 flex-col gap-1 ${wide(field.type) ? 'basis-full' : ''} ${\n field.type === 'checkbox'\n ? 'flex-row-reverse items-center justify-end gap-2'\n : 'max-sm:basis-full'\n }`}\n >\n {name && <span className=\"text-[12px] text-muted lg:text-[13.5px]\">{name}</span>}\n <div\n className={`relative [container-type:size] ${SIZE[field.type] ?? 'h-9 w-full sm:w-80 lg:h-10 lg:w-96'}`}\n >\n {control(field)}\n </div>\n </div>\n ))}\n </div>\n )}\n </>\n )}\n </li>\n </Fragment>\n ))}\n </ul>\n {list.length > shown && (\n <div className=\"mt-3 text-center\">\n <Button variant=\"secondary\" size=\"sm\" onClick={() => setShown((n) => n + PAGE)}>\n Show more ({list.length - shown} left)\n </Button>\n </div>\n )}\n </div>\n )\n}\n\n/**\n * Checkboxes of one grid as on the document: each line of the form is a row, a box left of its label; columns\n * as many as the widest line (2 on phones).\n */\nfunction OptionGrid({\n rows,\n parts,\n control,\n}: {\n rows: number[]\n parts: Question['parts']\n control: (f: FieldDef) => ReactNode\n}) {\n const starts = new Set(rows.map((_, i) => rows.slice(0, i).reduce((a, b) => a + b, 0)))\n return (\n <div\n className=\"grid grid-cols-2 gap-x-4 gap-y-2 sm:grid-cols-[repeat(var(--cols),minmax(0,1fr))]\"\n style={{ '--cols': Math.max(...rows) } as CSSProperties}\n >\n {parts.map(({ field, name }, i) => (\n <div\n key={field.id}\n className={`flex min-w-0 items-center gap-2 text-[13px] text-ink-2 lg:text-[14.5px] ${starts.has(i) ? 'sm:col-start-1' : ''}`}\n >\n <div className=\"relative h-6 w-6 shrink-0 [container-type:size] lg:h-7 lg:w-7\">{control(field)}</div>\n {/* The control is its own <label>: a click on the name ticks the box the same way. */}\n <span\n className=\"min-w-0 cursor-pointer break-words\"\n onClick={(e) => e.currentTarget.parentElement?.querySelector('input')?.click()}\n >\n {name}\n </span>\n </div>\n ))}\n </div>\n )\n}\n\n/** A table as on the document: its column names over the rows, each row's name on its left when it has one. */\nfunction TableGrid({ q, control }: { q: Question; control: (f: FieldDef) => ReactNode }) {\n const rows = q.rows!.map((n, i) => q.parts.slice(q.rows!.slice(0, i).reduce((a, b) => a + b, 0)).slice(0, n))\n return (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full border-separate border-spacing-x-2 border-spacing-y-1.5 text-left\">\n <thead className={q.columns!.some(Boolean) ? '' : 'sr-only'}>\n <tr>\n {q.rowNames && <th />}\n {q.columns!.map((c, i) => (\n <th key={i} scope=\"col\" className=\"min-w-32 px-0 text-[12px] font-medium text-muted lg:text-[13.5px]\">\n {c}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {rows.map((row, r) => (\n <tr key={row[0].field.id}>\n {q.rowNames && (\n <th\n scope=\"row\"\n className=\"w-px pr-2 text-[12.5px] font-semibold whitespace-nowrap text-ink-2 lg:text-[14px]\"\n >\n {q.rowNames[r]}\n </th>\n )}\n {/* Controls are size containers (no width of their own): each cell gets a minimum width. */}\n {row.map(({ field }) => (\n <td key={field.id} className=\"p-0 align-middle\">\n <div\n className={`relative [container-type:size] ${field.type === 'checkbox' ? 'h-7 w-7' : `${field.type === 'textarea' ? 'h-16' : 'h-9 lg:h-10'} w-full min-w-32`}`}\n >\n {control(field)}\n </div>\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )\n}\n\n/** A titled block of filters in the Filters panel. */\nfunction FilterSection({ title, action, children }: { title: string; action?: ReactNode; children: ReactNode }) {\n return (\n <section className=\"flex flex-col gap-2 border-b border-line-soft pb-3 last:border-0 last:pb-0\">\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <h4 className=\"m-0 text-[13px] font-semibold text-ink\">{title}</h4>\n {action}\n </div>\n {children}\n </section>\n )\n}\n","import { X } from 'lucide-react'\n\nexport interface ChipOption {\n value: string\n label: string\n /** How many items have it, shown after the label. */\n count?: number\n}\n\n/** A pill that can be switched on and off (a filter value). */\nexport function Chip({\n label,\n count,\n active,\n onClick,\n}: {\n label: string\n count?: number\n active?: boolean\n onClick: () => void\n}) {\n return (\n <button\n type=\"button\"\n aria-pressed={!!active}\n onClick={onClick}\n className={`inline-flex cursor-pointer items-center gap-1.5 rounded-full border px-3 py-1 text-[12.5px] whitespace-nowrap transition-colors ${\n active\n ? 'border-primary bg-primary-tint font-semibold text-primary'\n : 'border-line-strong bg-white text-ink-2 hover:border-faint hover:bg-surface'\n }`}\n >\n {label}\n {count != null && (\n <span\n className={`rounded-full px-1.5 text-[11px] font-semibold ${active ? 'bg-primary text-white' : 'bg-hover text-muted'}`}\n >\n {count}\n </span>\n )}\n </button>\n )\n}\n\n/** One pick out of a few pills; a click on the picked one clears it. Optional title before them. */\nexport function ChipGroup({\n title,\n options,\n value,\n onChange,\n}: {\n title?: string\n options: ChipOption[]\n value: string | null\n onChange: (value: string | null) => void\n}) {\n return (\n <div role=\"group\" aria-label={title} className=\"flex flex-wrap items-center gap-1.5\">\n {title && (\n <span className=\"mr-1 min-w-28 text-[11.5px] max-sm:basis-full font-semibold tracking-wide text-muted uppercase\">\n {title}\n </span>\n )}\n {options.map((o) => (\n <Chip\n key={o.value}\n label={o.label}\n count={o.count}\n active={value === o.value}\n onClick={() => onChange(value === o.value ? null : o.value)}\n />\n ))}\n </div>\n )\n}\n\n/** A filter in use, with × to take it off. */\nexport function RemovableChip({ label, onRemove }: { label: string; onRemove: () => void }) {\n return (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-primary-tint py-0.5 pr-1 pl-2.5 text-[12px] font-medium text-primary\">\n {label}\n <button\n type=\"button\"\n aria-label={`Remove ${label}`}\n onClick={onRemove}\n className=\"inline-flex h-4.5 w-4.5 cursor-pointer items-center justify-center rounded-full hover:bg-primary hover:text-white\"\n >\n <X size={12} />\n </button>\n </span>\n )\n}\n","import type { LucideIcon } from 'lucide-react'\nimport { useRef, useState, type ReactNode } from 'react'\nimport { useOutsideClose } from './useOutsideClose'\n\n/** A button that opens a panel under it (closes on a click outside or Escape). `badge`: a count on the button. */\nexport default function Popover({\n label,\n icon: Icon,\n badge,\n align = 'left',\n children,\n}: {\n label: string\n icon?: LucideIcon\n badge?: number\n align?: 'left' | 'right'\n children: ReactNode\n}) {\n const [open, setOpen] = useState(false)\n const box = useRef<HTMLDivElement>(null)\n useOutsideClose(box, open, () => setOpen(false))\n return (\n <div ref={box} className=\"relative\">\n <button\n type=\"button\"\n aria-expanded={open}\n aria-haspopup=\"dialog\"\n onClick={() => setOpen((o) => !o)}\n className={`inline-flex cursor-pointer items-center gap-1.5 rounded-md border px-3 py-1.5 text-[13px] font-medium ${\n open || badge\n ? 'border-primary bg-primary-soft text-primary'\n : 'border-line-strong bg-white text-ink-2 hover:border-faint'\n }`}\n >\n {Icon && <Icon size={15} />}\n {label}\n {!!badge && (\n <span className=\"inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-full bg-primary px-1 text-[11px] font-bold text-white\">\n {badge}\n </span>\n )}\n </button>\n {open && (\n <div\n role=\"dialog\"\n aria-label={label}\n className={`absolute z-30 mt-1.5 w-[min(560px,calc(100vw-24px))] max-sm:fixed max-sm:inset-x-3 max-sm:max-h-[70dvh] max-sm:w-auto max-sm:overflow-y-auto rounded-lg border border-line bg-white p-4 shadow-xl ${\n align === 'right' ? 'right-0' : 'left-0'\n }`}\n >\n {children}\n </div>\n )}\n </div>\n )\n}\n","import { useEffect, type RefObject } from 'react'\n\n/**\n * Calls onClose on a mousedown outside `ref` or on Escape, while `open`. For dropdowns and menus.\n * composedPath(), not e.target: inside a shadow root a document listener sees e.target as the\n * shadow host, so every click (even on the dropdown's own checkbox) would look \"outside\".\n */\nexport function useOutsideClose(ref: RefObject<HTMLElement | null>, open: boolean, onClose: () => void) {\n useEffect(() => {\n if (!open) return\n const onClick = (e: MouseEvent) => (ref.current && e.composedPath().includes(ref.current)) || onClose()\n const onKey = (e: KeyboardEvent) => e.key === 'Escape' && onClose()\n document.addEventListener('mousedown', onClick)\n document.addEventListener('keydown', onKey)\n return () => {\n document.removeEventListener('mousedown', onClick)\n document.removeEventListener('keydown', onKey)\n }\n }, [ref, open, onClose])\n}\n","import type { ReactNode } from 'react'\n\nconst tones = {\n success: 'bg-success/15 text-success',\n info: 'bg-heading/20 text-primary',\n muted: 'bg-hover text-muted',\n}\n\n/** Small coloured pill: a status or label on a row (Primary, Secondary…). */\nexport const Tag = ({ tone = 'info', children }: { tone?: keyof typeof tones; children: ReactNode }) => (\n <span\n className={`inline-block rounded-full px-2.5 py-0.5 text-[11px] font-semibold whitespace-nowrap ${tones[tone]}`}\n >\n {children}\n </span>\n)\n"]}
1
+ {"version":3,"sources":["/home/runner/work/formlift/formlift/dist/react/index.cjs","../../src/react/FormEditor.tsx","../../src/react/ui/slots.tsx","../../src/react/ui/Button.tsx","../../src/react/ui/Checkbox.tsx","../../src/react/ui/Chip.tsx","../../src/react/ui/FormField.tsx","../../src/react/ui/IconButton.tsx","../../src/react/ui/Modal.tsx","../../src/react/ui/Panel.tsx","../../src/react/ui/Popover.tsx","../../src/react/ui/useOutsideClose.ts","../../src/react/ui/SegmentedTabs.tsx","../../src/react/ui/SignaturePad.tsx","../../src/react/ui/Tag.tsx","../../src/react/components/ElementProperties.tsx","../../src/react/components/FieldControl.tsx","../../src/react/components/FieldProperties.tsx","../../src/react/constants.ts","../../src/react/components/ConditionBuilder.tsx","../../src/react/components/LayoutElementView.tsx","../../src/react/components/PagesPanel.tsx","../../src/react/components/PdfPages.tsx","../../src/react/pdfjs.ts","../../src/react/components/ZoomControls.tsx","../../src/react/hooks/useFitZoom.ts","../../src/react/hooks/useFormEditor.ts","../../src/react/hooks/useUndo.ts","../../src/react/FormFiller.tsx","../../src/react/components/QuestionList.tsx","../../src/react/hooks/useQuestionFilter.ts","../../src/react/hooks/useFormFiller.ts"],"names":["jsx","jsxs","Checkbox","TextArea","Select","TextInput","SignaturePad","Button","base","IconButton","box","useCallback","_p","sizes","Plus","SegmentedTabs","PanelHeading","Trash2","ArrowUp","ArrowDown","ConfirmModal","useMemo","ChipGroup","Chip","toQuestions"],"mappings":"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","file":"/home/runner/work/formlift/formlift/dist/react/index.cjs","sourcesContent":[null,"import {\n ArrowDown,\n ArrowLeft,\n ArrowUp,\n Copy,\n Eye,\n Grid3x3,\n PanelRight,\n Pencil,\n Plus,\n Redo2,\n Save,\n Trash2,\n Type,\n Undo2,\n Wand2,\n} from 'lucide-react'\nimport { useEffect, useImperativeHandle, useRef, useState, type ReactNode, type Ref } from 'react'\nimport ElementProperties from './components/ElementProperties'\nimport { AnswerControl } from './components/FieldControl'\nimport FieldProperties from './components/FieldProperties'\nimport LayoutElementView from './components/LayoutElementView'\nimport PagesPanel from './components/PagesPanel'\nimport PdfPages, { type PdfSource } from './components/PdfPages'\nimport ZoomControls from './components/ZoomControls'\nimport { useCompact, useFitZoom } from './hooks/useFitZoom'\nimport { useFormEditor, type FormDoc, type UseFormEditorOptions } from './hooks/useFormEditor'\nimport { TYPE_LABEL } from './constants'\nimport { boxStyle } from '../core/coords'\nimport type { FieldType } from '../core/form-logic'\nimport { ELEMENT_TOOLS, type ElementTool } from '../core/layout'\nimport {\n Button,\n ConfirmModal,\n EmptyText,\n FormliftProvider,\n IconButton,\n PanelHeading,\n SegmentedTabs,\n type FormliftComponents,\n} from './ui/slots'\n\nexport type { FormDoc }\n\n/** Parts of the FormEditor screen that can be hidden with `hide`. */\nexport type EditorPart =\n | 'back'\n | 'title'\n | 'status'\n | 'undo'\n | 'snap'\n | 'zoom'\n | 'editText'\n | 'autoDetect'\n | 'preview'\n | 'save'\n | 'tools'\n | 'fieldTools'\n | 'layoutTools'\n | 'panel'\n | 'propertiesTab'\n | 'fieldsTab'\n | 'pagesTab'\n | 'problems'\n\nconst DRAG_TYPE = 'application/x-doc-item' // \"field:<type>\" or \"element:<tool>\"\n\n/** Imperative handle (`ref`): save from outside, e.g. before a workflow step. */\nexport interface FormEditorHandle {\n /** Saves if there are changes. false = invalid or onSave failed. */\n save: () => Promise<boolean>\n readonly dirty: boolean\n readonly value: FormDoc\n}\n\nexport interface FormEditorProps extends UseFormEditorOptions {\n /** The PDF: URL, Blob / File, bytes, or a function returning one. Never modified. */\n pdf: PdfSource\n title?: ReactNode\n /** Back button in the toolbar (hidden when omitted). */\n onBack?: () => void\n /** Your own items at the end of the toolbar (status, workflow buttons…). */\n toolbar?: ReactNode\n /** A strip under the toolbar (e.g. \"this version is live\"). */\n banner?: ReactNode\n /** Your own tabs in the right panel (e.g. history). */\n tabs?: { id: string; label: string; content: ReactNode }[]\n /** Replace any UI primitive (Button, Modal, …), or the answer controls used in preview (FieldControl / `fields`). */\n components?: FormliftComponents\n /** Parts of the screen to leave out (the logic still runs: hide 'save' and call `ref.save()` yourself). */\n hide?: EditorPart[]\n /** Extra classes on the root. It fills its container's height (`h-full`) by default: give the container a height. */\n className?: string\n ref?: Ref<FormEditorHandle>\n}\n\n/**\n * Admin editor. The document in the middle: original PDF pages (and blank pages) with everything the admin\n * adds on top. Left: fields and layout tools to drag onto a page. \"Edit text\" makes every line of the original\n * clickable: a click covers it with an editable copy. Right: properties of the selection, the field list, pages\n * (reorder, delete, add blank) and history. Nothing changes the uploaded PDF; all of it is saved in the version.\n * Keyboard: Delete, Ctrl+Z / Ctrl+Y, Ctrl+D duplicate, arrows nudge (Shift = bigger steps).\n * All the logic lives in useFormEditor: use that hook directly for a completely custom UI.\n */\nexport default function FormEditor({\n pdf: pdfSource,\n title,\n onBack,\n toolbar,\n banner,\n tabs = [],\n components,\n hide = [],\n className = 'h-full',\n ref,\n ...options\n}: FormEditorProps) {\n const shows = (p: EditorPart) => !hide.includes(p)\n const panelTabs = [\n ...(shows('propertiesTab') ? ['props'] : []),\n ...(shows('fieldsTab') ? ['fields'] : []),\n ...(shows('pagesTab') ? ['pages'] : []),\n ...tabs.map((t) => t.id),\n ]\n const [tab, setTabState] = useState<string>(panelTabs[0] ?? 'props')\n const setTab = (t: string) => panelTabs.includes(t) && setTabState(t)\n const ed = useFormEditor({\n ...options,\n onDetected: (found) => {\n setTab('fields')\n options.onDetected?.(found)\n },\n })\n const { fields, layout, pageCount, selection, selectedField, selectedElement, preview, armed, snapOn, editText } = ed\n // Phone / tablet (below lg): the side panels open as drawers over the page; null = both closed.\n const [drawer, setDrawer] = useState<'tools' | 'panel' | null>(null)\n const compact = useCompact()\n const [confirmDeletePage, setConfirmDeletePage] = useState<number | null>(null)\n const [loadError, setLoadError] = useState('')\n const scroller = useRef<HTMLDivElement>(null)\n const { zoom, setZoom, ref: fitRef } = useFitZoom(Math.max(...(ed.pageSizes ?? [{ width: 595 }]).map((p) => p.width)))\n // A new selection shows its properties.\n useEffect(() => {\n if (selection) setTab('props')\n }, [selection])\n\n function goToPage(n: number) {\n scroller.current?.querySelector(`[data-page=\"${n}\"]`)?.scrollIntoView({ block: 'start' })\n }\n\n const saveRef = useRef(ed.save)\n useEffect(() => {\n saveRef.current = ed.save\n })\n useImperativeHandle(\n ref,\n () => ({\n save: () => (ed.dirty ? saveRef.current() : Promise.resolve(ed.problems.length === 0)),\n dirty: ed.dirty,\n value: ed.doc,\n }),\n [ed.dirty, ed.doc, ed.problems],\n )\n\n if (loadError) return <p className=\"fl-root p-6 text-center text-sm text-danger\">{loadError}</p>\n\n const { previewStates, previewAnswers, counts, pageHeight } = ed\n\n // Drag onto a page, or tap it and then tap the page (touch screens have no drag and drop).\n const toolItem = (key: string, label: string) => (\n <button\n type=\"button\"\n key={key}\n draggable\n onDragStart={(e) => e.dataTransfer.setData(DRAG_TYPE, key)}\n onClick={() => {\n ed.setArmed(armed === key ? null : key)\n setDrawer(null)\n }}\n aria-pressed={armed === key}\n className={`block w-full cursor-grab rounded-md border px-2.5 py-1.5 text-left text-[13px] hover:border-primary hover:bg-primary-soft active:cursor-grabbing max-lg:py-2.5 ${\n armed === key ? 'border-primary bg-primary-soft' : 'border-line'\n }`}\n >\n {label}\n </button>\n )\n const armedLabel = armed\n ? armed.startsWith('field:')\n ? TYPE_LABEL[armed.slice(6) as FieldType]\n : ELEMENT_TOOLS[armed.slice(8) as ElementTool]?.label\n : ''\n\n return (\n <FormliftProvider components={components}>\n <div className={`fl-root flex min-h-[640px] flex-col bg-white text-ink ${className}`}>\n {/* Toolbar */}\n <div className=\"flex flex-wrap items-center gap-2 border-b border-line bg-white px-4 py-2.5\">\n {onBack && shows('back') && (\n <Button variant=\"secondary\" size=\"sm\" icon={ArrowLeft} onClick={onBack}>\n Back\n </Button>\n )}\n {!preview && shows('tools') && (\n <Button\n variant={drawer === 'tools' ? 'primary' : 'secondary'}\n size=\"sm\"\n icon={Plus}\n className=\"lg:hidden\"\n onClick={() => setDrawer((d) => (d === 'tools' ? null : 'tools'))}\n >\n Add\n </Button>\n )}\n {shows('panel') && (\n <Button\n variant={drawer === 'panel' ? 'primary' : 'secondary'}\n size=\"sm\"\n icon={PanelRight}\n className=\"lg:hidden\"\n onClick={() => setDrawer((d) => (d === 'panel' ? null : 'panel'))}\n >\n {selection ? 'Properties' : 'Panel'}\n </Button>\n )}\n {title != null && shows('title') && (\n <h2 className=\"m-0 min-w-0 truncate text-[17px] font-semibold\">{title}</h2>\n )}\n {shows('status') && (\n <span className=\"text-[12px] text-muted\">{ed.dirty ? 'Unsaved changes' : 'All changes saved'}</span>\n )}\n <div className=\"ml-auto flex flex-wrap items-center gap-1.5\">\n {shows('undo') && (\n <>\n <IconButton icon={Undo2} title=\"Undo (Ctrl+Z)\" disabled={!ed.canUndo || preview} onClick={ed.undo} />\n <IconButton icon={Redo2} title=\"Redo (Ctrl+Y)\" disabled={!ed.canRedo || preview} onClick={ed.redo} />\n </>\n )}\n {shows('snap') && (\n <IconButton\n icon={Grid3x3}\n title={snapOn ? 'Snap to grid: on' : 'Snap to grid: off'}\n className={snapOn ? '!text-primary' : ''}\n onClick={() => ed.setSnapOn(!snapOn)}\n />\n )}\n {shows('zoom') && <ZoomControls zoom={zoom} onChange={setZoom} />}\n {!preview && shows('editText') && (\n <Button\n variant={editText ? 'primary' : 'secondary'}\n size=\"sm\"\n icon={Type}\n disabled={!layout}\n title=\"Click any line of the document to change its text\"\n onClick={() => ed.setEditText(!editText)}\n >\n <span className=\"max-sm:hidden\">Edit text</span>\n </Button>\n )}\n {!preview && shows('autoDetect') && (\n <Button\n variant=\"secondary\"\n size=\"sm\"\n icon={Wand2}\n disabled={!ed.pdf || !layout || !!ed.detecting}\n title=\"Find answer lines, checkboxes and table cells and suggest fields for them\"\n onClick={() => void ed.detect()}\n >\n <span className=\"max-sm:hidden\">Auto-detect</span>\n </Button>\n )}\n {shows('preview') && (\n <Button\n variant=\"secondary\"\n size=\"sm\"\n icon={preview ? Pencil : Eye}\n title={preview ? 'Back to editing' : 'Preview'}\n onClick={() => ed.setPreview(!preview)}\n >\n <span className=\"max-sm:hidden\">{preview ? 'Edit' : 'Preview'}</span>\n </Button>\n )}\n {shows('save') && (\n <Button size=\"sm\" icon={Save} title=\"Save\" disabled={!ed.dirty || ed.busy} onClick={() => void ed.save()}>\n <span className=\"max-sm:hidden\">Save</span>\n </Button>\n )}\n {toolbar}\n </div>\n </div>\n {banner}\n {ed.detecting && (\n <div className=\"flex items-center gap-3 bg-folder/15 px-4 py-1.5 text-[12.5px] text-ink-2\" role=\"status\">\n Detecting fields… page {ed.detecting.done} of {ed.detecting.total}\n <div className=\"h-1.5 w-40 overflow-hidden rounded bg-white\">\n <div\n className=\"h-full bg-folder\"\n style={{ width: `${(100 * ed.detecting.done) / Math.max(1, ed.detecting.total)}%` }}\n />\n </div>\n <button\n type=\"button\"\n className=\"cursor-pointer text-primary hover:underline\"\n onClick={() => ed.detecting?.stop.abort()}\n >\n Stop\n </button>\n </div>\n )}\n {armed && !preview && (\n <p className=\"m-0 flex items-center gap-3 bg-primary-soft px-4 py-1.5 text-[12.5px] text-ink-2\" role=\"status\">\n Tap the page where the {armedLabel} should go.\n <button\n type=\"button\"\n className=\"cursor-pointer text-primary hover:underline\"\n onClick={() => ed.setArmed(null)}\n >\n Cancel\n </button>\n </p>\n )}\n {editText && (\n <p className=\"m-0 bg-folder/15 px-4 py-1.5 text-[12.5px] text-ink-2\">\n Edit text: click a line of the document to replace it with an editable copy (same place and size). The\n uploaded file itself never changes.\n </p>\n )}\n\n <div className=\"relative flex min-h-0 flex-1\">\n {/* Toolbox: a column on large screens, a drawer from the left below lg. */}\n {!preview && shows('tools') && (\n <aside\n className={`w-[170px] flex-none overflow-y-auto border-r border-line bg-white max-lg:absolute max-lg:inset-y-0 max-lg:left-0 max-lg:z-30 max-lg:w-[220px] max-lg:shadow-xl ${\n drawer === 'tools' ? '' : 'max-lg:hidden'\n }`}\n >\n {shows('fieldTools') && ed.fieldTools.length > 0 && (\n <>\n <PanelHeading>FIELDS</PanelHeading>\n <p className=\"m-0 px-3 pt-2 text-[11.5px] text-muted\">\n Drag onto the document, or tap one, then the page\n </p>\n <div className=\"space-y-1 p-2\">\n {ed.fieldTools.map((k) => toolItem(k, TYPE_LABEL[k.slice(6) as FieldType]))}\n </div>\n </>\n )}\n {shows('layoutTools') && ed.elementTools.length > 0 && (\n <>\n <PanelHeading>LAYOUT</PanelHeading>\n <div className=\"space-y-1 p-2\">\n {ed.elementTools.map((k) => toolItem(k, ELEMENT_TOOLS[k.slice(8) as ElementTool].label))}\n </div>\n </>\n )}\n </aside>\n )}\n\n {/* Document */}\n <div\n ref={(n) => {\n scroller.current = n\n fitRef(n)\n }}\n className=\"min-w-0 flex-1 overflow-auto bg-hover\"\n onPointerDown={() => {\n if (!preview) ed.setSelection(null)\n setDrawer(null)\n }}\n >\n <PdfPages\n load={pdfSource}\n zoom={zoom}\n pages={layout?.pages}\n withText={editText && !preview}\n onLoaded={ed.onPdfLoaded}\n onError={() => setLoadError('The document could not be shown')}\n overlay={(page, lines) => (\n <div\n className=\"absolute inset-0\"\n style={\n snapOn && !preview\n ? {\n backgroundImage:\n 'linear-gradient(to right, color-mix(in srgb, var(--fl-primary) 7%, transparent) 1px, transparent 1px), linear-gradient(to bottom, color-mix(in srgb, var(--fl-primary) 7%, transparent) 1px, transparent 1px)',\n backgroundSize: '2% 2%',\n }\n : undefined\n }\n onDragOver={(e) => !preview && e.preventDefault()}\n onDrop={(e) => {\n const key = e.dataTransfer.getData(DRAG_TYPE)\n if (ed.placeNew(key, page, e.clientX, e.clientY, e.currentTarget.getBoundingClientRect()))\n e.preventDefault()\n }}\n onClick={(e) => {\n if (!armed || preview) return\n ed.placeNew(armed, page, e.clientX, e.clientY, e.currentTarget.getBoundingClientRect())\n ed.setArmed(null)\n }}\n >\n {/* Original text lines (Edit text): a click replaces the line with an editable copy. */}\n {lines?.map((line, i) => (\n <button\n key={i}\n type=\"button\"\n title={`Edit: ${line.text}`}\n className=\"absolute cursor-text rounded-[1px] outline-1 outline-folder/60 hover:bg-folder/25 hover:outline-folder\"\n style={boxStyle({ page, ...line })}\n onPointerDown={(e) => e.stopPropagation()}\n onClick={() => ed.editLine(line, page)}\n />\n ))}\n\n {layout?.elements\n .filter((el) => el.page === page)\n .map((el) =>\n preview ? (\n <LayoutElementView key={el.id} element={el} pageHeight={pageHeight(page)} />\n ) : (\n <div\n key={el.id}\n role=\"button\"\n tabIndex={0}\n aria-label={`${el.type} ${el.text ?? ''}`}\n className={`absolute cursor-move touch-none select-none ${\n selection?.id === el.id\n ? 'z-10 outline-2 outline-primary'\n : 'hover:outline-1 hover:outline-primary/50'\n }`}\n style={boxStyle({ page, ...el.position, height: Math.max(el.position.height, 0.004) })}\n onPointerDown={(e) =>\n ed.startDrag(e, { kind: 'element', id: el.id }, { page, ...el.position }, 'move')\n }\n onPointerMove={ed.onDrag}\n onPointerUp={ed.endDrag}\n onClick={(e) => e.stopPropagation()}\n >\n <LayoutElementView\n element={el}\n pageHeight={pageHeight(page)}\n style={{ left: 0, top: 0, width: '100%', height: '100%' }}\n />\n {selection?.id === el.id && (\n <span\n aria-label=\"Resize\"\n className=\"absolute -right-1 -bottom-1 h-2.5 w-2.5 cursor-se-resize rounded-sm border border-white bg-primary\"\n onPointerDown={(e) =>\n ed.startDrag(e, { kind: 'element', id: el.id }, { page, ...el.position }, 'resize')\n }\n onPointerMove={ed.onDrag}\n onPointerUp={ed.endDrag}\n />\n )}\n </div>\n ),\n )}\n\n {fields\n .filter((f) => f.position.page === page)\n .map((f) =>\n preview ? (\n previewStates[f.key]?.visible && (\n <div key={f.id} className=\"absolute [container-type:size]\" style={boxStyle(f.position)}>\n <AnswerControl\n components={components}\n field={f}\n value={previewAnswers[f.key]}\n readOnly={previewStates[f.key].readOnly}\n compact={compact}\n onChange={(v) => ed.setPreviewAnswers((a) => ({ ...a, [f.key]: v }))}\n />\n </div>\n )\n ) : (\n <div\n key={f.id}\n role=\"button\"\n tabIndex={0}\n aria-label={`${TYPE_LABEL[f.type]} field ${f.label || f.key}`}\n title={`${f.label || f.key} (${TYPE_LABEL[f.type]})`}\n className={`absolute cursor-move touch-none rounded-sm border text-[10px] select-none [container-type:size] ${\n selection?.id === f.id\n ? 'z-10 border-primary bg-primary/20 ring-2 ring-primary/40'\n : f.source === 'ai'\n ? 'border-dashed border-folder bg-folder/15'\n : 'border-primary/60 bg-primary/10 hover:bg-primary/15'\n }`}\n style={boxStyle(f.position)}\n onPointerDown={(e) => ed.startDrag(e, { kind: 'field', id: f.id }, f.position, 'move')}\n onPointerMove={ed.onDrag}\n onPointerUp={ed.endDrag}\n onClick={(e) => e.stopPropagation()}\n >\n <span className=\"pointer-events-none block truncate px-0.5 text-[clamp(7px,55cqh,12px)] leading-tight text-primary\">\n {f.label || f.key}\n {f.required && <b className=\"text-danger\"> *</b>}\n </span>\n {selection?.id === f.id && (\n <span\n aria-label=\"Resize\"\n className=\"absolute -right-1 -bottom-1 h-2.5 w-2.5 cursor-se-resize rounded-sm border border-white bg-primary\"\n onPointerDown={(e) => ed.startDrag(e, { kind: 'field', id: f.id }, f.position, 'resize')}\n onPointerMove={ed.onDrag}\n onPointerUp={ed.endDrag}\n />\n )}\n </div>\n ),\n )}\n </div>\n )}\n />\n </div>\n\n {/* Right panel: a column on large screens, a drawer from the right below lg. */}\n {shows('panel') && panelTabs.length > 0 && (\n <aside\n className={`flex w-[350px] flex-none flex-col border-l border-line bg-white max-lg:absolute max-lg:inset-y-0 max-lg:right-0 max-lg:z-30 max-lg:w-[min(350px,92vw)] max-lg:shadow-xl ${\n drawer === 'panel' ? '' : 'max-lg:hidden'\n }`}\n >\n <div className=\"overflow-x-auto border-b border-line p-2 [&_button]:px-2.5\">\n <SegmentedTabs\n options={[\n { value: 'props', label: 'Properties' },\n { value: 'fields', label: `Fields (${fields.length})` },\n { value: 'pages', label: `Pages (${pageCount})` },\n ...tabs.map((t) => ({ value: t.id, label: t.label })),\n ].filter((o) => panelTabs.includes(o.value))}\n value={tab}\n onChange={setTab}\n />\n </div>\n {ed.problems.length > 0 && shows('problems') && (\n <ul className=\"m-0 border-b border-danger/30 bg-danger/5 py-2 pr-3 pl-7 text-[12px] text-danger\">\n {ed.problems.slice(0, 6).map((p) => (\n <li key={p}>{p}</li>\n ))}\n </ul>\n )}\n <div className=\"min-h-0 flex-1 overflow-y-auto\">\n {tab === 'props' && (\n <>\n <PanelHeading\n actions={\n selection && (\n <>\n <IconButton\n icon={Copy}\n title=\"Duplicate (Ctrl+D)\"\n light\n iconSize={16}\n onClick={ed.duplicateSelected}\n />\n <IconButton\n icon={Trash2}\n title=\"Delete (Del)\"\n light\n iconSize={16}\n onClick={ed.removeSelected}\n />\n </>\n )\n }\n >\n {selectedField ? 'FIELD' : selectedElement ? selectedElement.type.toUpperCase() : 'PROPERTIES'}\n </PanelHeading>\n {selectedField ? (\n <FieldProperties\n key={selectedField.id}\n field={selectedField}\n fields={fields}\n pageCount={pageCount}\n onChange={ed.updateField}\n />\n ) : selectedElement ? (\n <ElementProperties\n key={selectedElement.id}\n element={selectedElement}\n pageCount={pageCount}\n onChange={ed.updateElement}\n />\n ) : (\n <EmptyText>\n Select something on the document, drag a field or layout item in from the left, or use{' '}\n <b>Edit text</b> to change the document's own text.\n </EmptyText>\n )}\n </>\n )}\n\n {tab === 'fields' && (\n <>\n <PanelHeading>FILL ORDER</PanelHeading>\n {!fields.length && <EmptyText>No fields yet.</EmptyText>}\n <ol className=\"m-0 list-none p-0\">\n {fields.map((f, i) => (\n <li\n key={f.id}\n className={`flex items-center gap-1 border-b border-line-soft px-2 py-1 text-[12.5px] ${selection?.id === f.id ? 'bg-primary-tint' : ''}`}\n >\n <button\n type=\"button\"\n className=\"min-w-0 flex-1 cursor-pointer truncate text-left\"\n onClick={() => {\n ed.setSelection({ kind: 'field', id: f.id })\n goToPage(f.position.page)\n }}\n >\n <span className=\"text-faint\">p{f.position.page}</span> {f.label || f.key}{' '}\n <span className=\"text-muted\">· {TYPE_LABEL[f.type]}</span>\n {f.source === 'ai' && <span className=\"text-folder\"> · AI</span>}\n </button>\n <IconButton\n icon={ArrowUp}\n title=\"Move up\"\n iconSize={13}\n disabled={i === 0}\n onClick={() => ed.reorderField(f.id, -1)}\n />\n <IconButton\n icon={ArrowDown}\n title=\"Move down\"\n iconSize={13}\n disabled={i === fields.length - 1}\n onClick={() => ed.reorderField(f.id, 1)}\n />\n </li>\n ))}\n </ol>\n </>\n )}\n\n {tab === 'pages' &&\n (layout ? (\n <PagesPanel\n pages={layout.pages}\n counts={counts}\n onGoTo={goToPage}\n onMove={ed.movePage}\n onInsertBlank={ed.insertBlankPage}\n onDelete={(n) => (counts[n] ? setConfirmDeletePage(n) : ed.deletePage(n))}\n />\n ) : (\n <EmptyText>Loading pages…</EmptyText>\n ))}\n\n {tabs.find((t) => t.id === tab)?.content}\n </div>\n </aside>\n )}\n </div>\n\n {confirmDeletePage != null && (\n <ConfirmModal\n danger\n confirmLabel=\"Delete page\"\n onCancel={() => setConfirmDeletePage(null)}\n onConfirm={() => {\n const n = confirmDeletePage\n setConfirmDeletePage(null)\n ed.deletePage(n)\n }}\n >\n Delete page {confirmDeletePage}? Its {counts[confirmDeletePage]} fields and items go with it. (Undo brings\n them back; the uploaded file is not changed.)\n </ConfirmModal>\n )}\n </div>\n </FormliftProvider>\n )\n}\n","// Every UI building block formlift draws, swappable. `<FormliftProvider components={{ Button: MyButton }}>` (or the\n// `components` prop of FormEditor / FormFiller) replaces it everywhere, inside the editor's panels too. Each\n// replacement gets the same props as the built-in one (see UIComponents).\nimport { createContext, useContext, type ComponentType, type ReactNode } from 'react'\nimport BaseButton from './Button'\nimport BaseCheckbox from './Checkbox'\nimport * as chip from './Chip'\nimport * as form from './FormField'\nimport BaseIconButton from './IconButton'\nimport BaseModal, * as modal from './Modal'\nimport * as panel from './Panel'\nimport BasePopover from './Popover'\nimport BaseSegmentedTabs from './SegmentedTabs'\nimport BaseSignaturePad from './SignaturePad'\nimport * as tag from './Tag'\nimport type { FieldControlProps } from '../components/FieldControl'\nimport type { FieldType } from '../../core/form-logic'\n\nconst base = {\n Button: BaseButton,\n IconButton: BaseIconButton,\n Checkbox: BaseCheckbox,\n FormField: form.FormField,\n TextInput: form.TextInput,\n Select: form.Select,\n TextArea: form.TextArea,\n Modal: BaseModal,\n ModalHeader: modal.ModalHeader,\n ModalBody: modal.ModalBody,\n ModalFooter: modal.ModalFooter,\n ConfirmModal: modal.ConfirmModal,\n PanelHeading: panel.PanelHeading,\n EmptyText: panel.EmptyText,\n SegmentedTabs: BaseSegmentedTabs,\n Chip: chip.Chip,\n ChipGroup: chip.ChipGroup,\n RemovableChip: chip.RemovableChip,\n Popover: BasePopover,\n Tag: tag.Tag,\n SignaturePad: BaseSignaturePad,\n}\n\n/** The built-in UI primitives. A replacement takes the same props: `ComponentProps<UIComponents['Button']>`. */\nexport type UIComponents = typeof base\nexport const defaultComponents: UIComponents = base\n\nconst UIContext = createContext<Partial<UIComponents>>({})\n\n/** Replaces formlift's UI primitives for everything under it. Nested providers merge (inner wins). */\nexport function FormliftProvider({\n components,\n children,\n}: {\n components?: Partial<UIComponents>\n children: ReactNode\n}) {\n const outer = useContext(UIContext)\n return <UIContext.Provider value={components ? { ...outer, ...components } : outer}>{children}</UIContext.Provider>\n}\n\nfunction slot<K extends keyof UIComponents>(name: K): UIComponents[K] {\n const Slot = (props: object) => {\n const C = (useContext(UIContext)[name] ?? base[name]) as ComponentType<object>\n return <C {...props} />\n }\n Slot.displayName = `formlift.${name}`\n return Slot as unknown as UIComponents[K]\n}\n\nexport const Button = slot('Button')\nexport const IconButton = slot('IconButton')\nexport const Checkbox = slot('Checkbox')\nexport const FormField = slot('FormField')\nexport const TextInput = slot('TextInput')\nexport const Select = slot('Select')\nexport const TextArea = slot('TextArea')\nexport const Modal = slot('Modal')\nexport const ModalHeader = slot('ModalHeader')\nexport const ModalBody = slot('ModalBody')\nexport const ModalFooter = slot('ModalFooter')\nexport const ConfirmModal = slot('ConfirmModal')\nexport const PanelHeading = slot('PanelHeading')\nexport const EmptyText = slot('EmptyText')\nexport const SegmentedTabs = slot('SegmentedTabs')\nexport const Chip = slot('Chip')\nexport const ChipGroup = slot('ChipGroup')\nexport const RemovableChip = slot('RemovableChip')\nexport const Popover = slot('Popover')\nexport const Tag = slot('Tag')\nexport const SignaturePad = slot('SignaturePad')\nexport { inputClass } from './FormField'\n\n/**\n * What `components` on FormEditor / FormFiller takes: UI primitives, the answer control for every field\n * (`FieldControl`), or one per field type (`fields: { date: MyDatePicker }`; wins over FieldControl).\n */\nexport type FormliftComponents = Partial<UIComponents> & {\n FieldControl?: ComponentType<FieldControlProps>\n fields?: Partial<Record<FieldType, ComponentType<FieldControlProps>>>\n}\n","import type { LucideIcon } from 'lucide-react'\nimport type { ButtonHTMLAttributes } from 'react'\n\nconst variants = {\n primary: 'bg-primary text-white enabled:hover:bg-primary-hover',\n secondary: 'border border-line-strong bg-white text-ink-2 enabled:hover:bg-hover',\n outline: 'border border-primary bg-white text-primary enabled:hover:bg-primary-soft',\n light: 'border border-line-strong bg-white text-primary enabled:hover:bg-primary-soft', // Filters\n heading: 'bg-heading text-white enabled:hover:bg-heading-hover', // matches panel headings\n success: 'bg-success text-white enabled:hover:bg-success-hover',\n brand: 'bg-brand text-white enabled:hover:bg-brand-hover',\n danger: 'bg-danger text-white enabled:hover:opacity-90',\n}\nconst sizes = { sm: 'px-3 py-1.5 text-[13px]', md: 'px-4 py-2 text-[13.5px]' }\n\nexport interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n variant?: keyof typeof variants\n size?: keyof typeof sizes\n /** Icon before the label. */\n icon?: LucideIcon\n}\n\n/** The app's button. `type` defaults to \"button\" so it never submits a form by accident. */\nexport default function Button({\n variant = 'primary',\n size = 'md',\n icon: Icon,\n className = '',\n type = 'button',\n children,\n ...rest\n}: ButtonProps) {\n return (\n <button\n type={type}\n className={`inline-flex cursor-pointer items-center justify-center gap-1.5 rounded-md font-semibold whitespace-nowrap transition disabled:cursor-not-allowed disabled:opacity-55 ${variants[variant]} ${sizes[size]} ${className}`}\n {...rest}\n >\n {Icon && <Icon size={size === 'sm' ? 15 : 16} />}\n {children}\n </button>\n )\n}\n","import type { InputHTMLAttributes } from 'react'\n\n/** The app's checkbox (theme colour). `label` = accessible name when there is no visible text. */\nexport default function Checkbox({\n label,\n className = '',\n ...rest\n}: InputHTMLAttributes<HTMLInputElement> & { label: string }) {\n return (\n <input\n type=\"checkbox\"\n aria-label={label}\n className={`h-[18px] w-[18px] flex-none cursor-pointer accent-primary ${className}`}\n {...rest}\n />\n )\n}\n","import { X } from 'lucide-react'\n\nexport interface ChipOption {\n value: string\n label: string\n /** How many items have it, shown after the label. */\n count?: number\n}\n\n/** A pill that can be switched on and off (a filter value). */\nexport function Chip({\n label,\n count,\n active,\n onClick,\n}: {\n label: string\n count?: number\n active?: boolean\n onClick: () => void\n}) {\n return (\n <button\n type=\"button\"\n aria-pressed={!!active}\n onClick={onClick}\n className={`inline-flex cursor-pointer items-center gap-1.5 rounded-full border px-3 py-1 text-[12.5px] whitespace-nowrap transition-colors ${\n active\n ? 'border-primary bg-primary-tint font-semibold text-primary'\n : 'border-line-strong bg-white text-ink-2 hover:border-faint hover:bg-surface'\n }`}\n >\n {label}\n {count != null && (\n <span\n className={`rounded-full px-1.5 text-[11px] font-semibold ${active ? 'bg-primary text-white' : 'bg-hover text-muted'}`}\n >\n {count}\n </span>\n )}\n </button>\n )\n}\n\n/** One pick out of a few pills; a click on the picked one clears it. Optional title before them. */\nexport function ChipGroup({\n title,\n options,\n value,\n onChange,\n}: {\n title?: string\n options: ChipOption[]\n value: string | null\n onChange: (value: string | null) => void\n}) {\n return (\n <div role=\"group\" aria-label={title} className=\"flex flex-wrap items-center gap-1.5\">\n {title && (\n <span className=\"mr-1 min-w-28 text-[11.5px] max-sm:basis-full font-semibold tracking-wide text-muted uppercase\">\n {title}\n </span>\n )}\n {options.map((o) => (\n <Chip\n key={o.value}\n label={o.label}\n count={o.count}\n active={value === o.value}\n onClick={() => onChange(value === o.value ? null : o.value)}\n />\n ))}\n </div>\n )\n}\n\n/** A filter in use, with × to take it off. */\nexport function RemovableChip({ label, onRemove }: { label: string; onRemove: () => void }) {\n return (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-primary-tint py-0.5 pr-1 pl-2.5 text-[12px] font-medium text-primary\">\n {label}\n <button\n type=\"button\"\n aria-label={`Remove ${label}`}\n onClick={onRemove}\n className=\"inline-flex h-4.5 w-4.5 cursor-pointer items-center justify-center rounded-full hover:bg-primary hover:text-white\"\n >\n <X size={12} />\n </button>\n </span>\n )\n}\n","import type { InputHTMLAttributes, ReactNode, SelectHTMLAttributes, TextareaHTMLAttributes } from 'react'\n\nexport const inputClass =\n 'w-full rounded-md border border-line-strong bg-white px-3 py-2 text-[13.5px] outline-none focus:border-primary'\n\n/** Label, the control, and its error under it. */\nexport function FormField({\n label,\n required,\n error,\n children,\n}: {\n label: string\n required?: boolean\n error?: string\n children: ReactNode\n}) {\n return (\n <label className=\"block text-[13.5px]\">\n <span className=\"mb-1 block font-semibold\">\n {label} {required && <span className=\"text-danger\">*</span>}\n </span>\n {children}\n {error && <small className=\"text-danger\">{error}</small>}\n </label>\n )\n}\n\nexport const TextInput = ({ className = '', ...rest }: InputHTMLAttributes<HTMLInputElement>) => (\n <input className={`${inputClass} ${className}`} {...rest} />\n)\n\nexport const Select = ({ className = '', ...rest }: SelectHTMLAttributes<HTMLSelectElement>) => (\n <select\n className={`rounded-md border border-line-strong bg-white px-2.5 py-1.5 text-[13px] outline-none ${className}`}\n {...rest}\n />\n)\n\nexport const TextArea = ({ className = '', ...rest }: TextareaHTMLAttributes<HTMLTextAreaElement>) => (\n <textarea className={`${inputClass} resize-none ${className}`} {...rest} />\n)\n","import type { LucideIcon } from 'lucide-react'\nimport type { ButtonHTMLAttributes } from 'react'\n\nexport interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n /** Tooltip and accessible name. */\n title: string\n icon: LucideIcon\n /** Red on hover (delete). */\n danger?: boolean\n iconSize?: number\n /** On a coloured background (panel headings): white icon. */\n light?: boolean\n}\n\n/** Icon-only button: view, download, delete, edit, expand… */\nexport default function IconButton({\n title,\n icon: Icon,\n danger,\n iconSize = 18,\n light,\n className = '',\n ...rest\n}: IconButtonProps) {\n const tone = light\n ? 'text-white opacity-90 hover:bg-white/15 hover:opacity-100'\n : `text-muted hover:bg-line-soft ${danger ? 'hover:text-danger' : 'hover:text-primary'}`\n return (\n <button\n type=\"button\"\n title={title}\n aria-label={title}\n className={`inline-flex cursor-pointer items-center justify-center rounded p-1.5 transition disabled:cursor-not-allowed disabled:opacity-40 ${tone} ${className}`}\n {...rest}\n >\n <Icon size={iconSize} />\n </button>\n )\n}\n","import type { ReactNode } from 'react'\nimport Button from './Button'\n\ninterface ModalProps {\n children: ReactNode\n onClose: () => void\n /** Tailwind width class, e.g. 'w-[560px]' */\n width: string\n}\n\n// Centered dialog over a dimmed backdrop; a click on the backdrop calls onClose.\nexport default function Modal({ children, onClose, width }: ModalProps) {\n return (\n <div className=\"fixed inset-0 z-[1200] flex items-center justify-center bg-[rgba(15,23,42,0.45)]\" onClick={onClose}>\n <div\n role=\"dialog\"\n aria-modal\n className={`${width} max-w-[calc(100vw-32px)] rounded-[10px] bg-white shadow-[0_20px_50px_rgba(0,0,0,0.25)]`}\n onClick={(e) => e.stopPropagation()}\n >\n {children}\n </div>\n </div>\n )\n}\n\n/** Title and × (disabled while e.g. an upload runs). */\nexport const ModalHeader = ({\n title,\n onClose,\n disabled,\n}: {\n title: string\n onClose: () => void\n disabled?: boolean\n}) => (\n <div className=\"flex items-center justify-between border-b border-line px-5 py-4\">\n <h3 className=\"m-0 text-[17px] font-semibold\">{title}</h3>\n <button\n type=\"button\"\n aria-label=\"Close\"\n disabled={disabled}\n onClick={onClose}\n className=\"cursor-pointer text-2xl leading-none text-muted enabled:hover:text-ink disabled:cursor-not-allowed disabled:opacity-40\"\n >\n ×\n </button>\n </div>\n)\n\nexport const ModalBody = ({ children }: { children: ReactNode }) => (\n <div className=\"space-y-4 px-5 py-4 text-[13.5px]\">{children}</div>\n)\n\n/** Buttons, right-aligned. */\nexport const ModalFooter = ({ children }: { children: ReactNode }) => (\n <div className=\"flex justify-end gap-2 border-t border-line px-5 py-3\">{children}</div>\n)\n\n/** \"Are you sure…?\" with Yes / No. */\nexport function ConfirmModal({\n children,\n confirmLabel = 'Yes',\n cancelLabel = 'No',\n danger,\n onConfirm,\n onCancel,\n}: {\n children: ReactNode\n confirmLabel?: string\n cancelLabel?: string\n /** Red confirm button (deletes). */\n danger?: boolean\n onConfirm: () => void\n onCancel: () => void\n}) {\n return (\n <Modal onClose={onCancel} width=\"w-[420px]\">\n <div className=\"px-6 pt-7 pb-6 text-sm\">{children}</div>\n <ModalFooter>\n <Button variant=\"secondary\" onClick={onCancel}>\n {cancelLabel}\n </Button>\n <Button variant={danger ? 'danger' : 'primary'} onClick={onConfirm}>\n {confirmLabel}\n </Button>\n </ModalFooter>\n </Modal>\n )\n}\n","import type { ReactNode } from 'react'\n\n/** White bordered box: the tree, the files list, the folder structure columns. */\nexport const Panel = ({ children, className = '' }: { children: ReactNode; className?: string }) => (\n <div className={`overflow-hidden rounded-lg border border-line bg-white ${className}`}>{children}</div>\n)\n\n/** Blue panel heading (FOLDER HIERARCHY, MODULES…) with optional buttons on the right. */\nexport const PanelHeading = ({ children, actions }: { children: ReactNode; actions?: ReactNode }) => (\n <div className=\"flex items-center justify-between bg-heading px-3.5 py-[11px] text-[13px] font-bold tracking-[0.02em] text-white\">\n {children}\n {actions && <span className=\"flex items-center gap-1 tracking-normal\">{actions}</span>}\n </div>\n)\n\n/** Grey hint in an empty list (\"No folders yet.\", \"Loading…\"). */\nexport const EmptyText = ({ children, className = '' }: { children: ReactNode; className?: string }) => (\n <div className={`m-0 px-3.5 py-3 text-[13px] text-muted ${className}`}>{children}</div>\n)\n","import type { LucideIcon } from 'lucide-react'\nimport { useRef, useState, type ReactNode } from 'react'\nimport { useOutsideClose } from './useOutsideClose'\n\n/** A button that opens a panel under it (closes on a click outside or Escape). `badge`: a count on the button. */\nexport default function Popover({\n label,\n icon: Icon,\n badge,\n align = 'left',\n children,\n}: {\n label: string\n icon?: LucideIcon\n badge?: number\n align?: 'left' | 'right'\n children: ReactNode\n}) {\n const [open, setOpen] = useState(false)\n const box = useRef<HTMLDivElement>(null)\n useOutsideClose(box, open, () => setOpen(false))\n return (\n <div ref={box} className=\"relative\">\n <button\n type=\"button\"\n aria-expanded={open}\n aria-haspopup=\"dialog\"\n onClick={() => setOpen((o) => !o)}\n className={`inline-flex cursor-pointer items-center gap-1.5 rounded-md border px-3 py-1.5 text-[13px] font-medium ${\n open || badge\n ? 'border-primary bg-primary-soft text-primary'\n : 'border-line-strong bg-white text-ink-2 hover:border-faint'\n }`}\n >\n {Icon && <Icon size={15} />}\n {label}\n {!!badge && (\n <span className=\"inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-full bg-primary px-1 text-[11px] font-bold text-white\">\n {badge}\n </span>\n )}\n </button>\n {open && (\n <div\n role=\"dialog\"\n aria-label={label}\n className={`absolute z-30 mt-1.5 w-[min(560px,calc(100vw-24px))] max-sm:fixed max-sm:inset-x-3 max-sm:max-h-[70dvh] max-sm:w-auto max-sm:overflow-y-auto rounded-lg border border-line bg-white p-4 shadow-xl ${\n align === 'right' ? 'right-0' : 'left-0'\n }`}\n >\n {children}\n </div>\n )}\n </div>\n )\n}\n","import { useEffect, type RefObject } from 'react'\n\n/**\n * Calls onClose on a mousedown outside `ref` or on Escape, while `open`. For dropdowns and menus.\n * composedPath(), not e.target: inside a shadow root a document listener sees e.target as the\n * shadow host, so every click (even on the dropdown's own checkbox) would look \"outside\".\n */\nexport function useOutsideClose(ref: RefObject<HTMLElement | null>, open: boolean, onClose: () => void) {\n useEffect(() => {\n if (!open) return\n const onClick = (e: MouseEvent) => (ref.current && e.composedPath().includes(ref.current)) || onClose()\n const onKey = (e: KeyboardEvent) => e.key === 'Escape' && onClose()\n document.addEventListener('mousedown', onClick)\n document.addEventListener('keydown', onKey)\n return () => {\n document.removeEventListener('mousedown', onClick)\n document.removeEventListener('keydown', onKey)\n }\n }, [ref, open, onClose])\n}\n","/** Pill toggle between a few views (\"Chats | New Chats\"). `sm`: a small one inside a panel. */\nexport default function SegmentedTabs<T extends string>({\n options,\n value,\n onChange,\n size = 'md',\n}: {\n options: { value: T; label: string }[]\n value: T\n onChange: (value: T) => void\n size?: 'sm' | 'md'\n}) {\n return (\n <div role=\"tablist\" className=\"inline-flex rounded-full border border-brand p-0.5\">\n {options.map((o) => (\n <button\n key={o.value}\n type=\"button\"\n role=\"tab\"\n aria-selected={o.value === value}\n onClick={() => onChange(o.value)}\n className={`cursor-pointer rounded-full font-semibold transition ${size === 'sm' ? 'px-3 py-0.5 text-[12px]' : 'px-5 py-1 text-[13px]'} ${\n o.value === value ? 'bg-brand text-white' : 'text-brand hover:bg-primary-soft'\n }`}\n >\n {o.label}\n </button>\n ))}\n </div>\n )\n}\n","import { useRef, useState } from 'react'\nimport Button from './Button'\n\n/**\n * Draw a signature with mouse, pen or finger. onSave gets a PNG data URL (transparent background).\n * Scaled down to at most `maxWidth` pixels wide so it stays small enough to send.\n */\nexport default function SignaturePad({\n onSave,\n onCancel,\n maxWidth = 600,\n}: {\n onSave: (dataUrl: string) => void\n onCancel: () => void\n maxWidth?: number\n}) {\n const canvas = useRef<HTMLCanvasElement>(null)\n const last = useRef<{ x: number; y: number } | null>(null)\n const [empty, setEmpty] = useState(true)\n\n const point = (e: React.PointerEvent<HTMLCanvasElement>) => {\n const r = e.currentTarget.getBoundingClientRect()\n return {\n x: ((e.clientX - r.left) / r.width) * e.currentTarget.width,\n y: ((e.clientY - r.top) / r.height) * e.currentTarget.height,\n }\n }\n const draw = (e: React.PointerEvent<HTMLCanvasElement>) => {\n const ctx = canvas.current?.getContext('2d')\n if (!ctx || !last.current) return\n const p = point(e)\n ctx.lineWidth = 3\n ctx.lineCap = 'round'\n ctx.strokeStyle = '#111827'\n ctx.beginPath()\n ctx.moveTo(last.current.x, last.current.y)\n ctx.lineTo(p.x, p.y)\n ctx.stroke()\n last.current = p\n setEmpty(false)\n }\n const clear = () => {\n const c = canvas.current\n c?.getContext('2d')?.clearRect(0, 0, c.width, c.height)\n setEmpty(true)\n }\n const save = () => {\n const c = canvas.current\n if (!c) return\n const scale = Math.min(1, maxWidth / c.width)\n const out = document.createElement('canvas')\n out.width = Math.round(c.width * scale)\n out.height = Math.round(c.height * scale)\n out.getContext('2d')?.drawImage(c, 0, 0, out.width, out.height)\n onSave(out.toDataURL('image/png'))\n }\n\n return (\n <div className=\"space-y-3\">\n <canvas\n ref={canvas}\n width={800}\n height={260}\n aria-label=\"Signature drawing area\"\n className=\"h-[200px] w-full touch-none rounded-md border border-dashed border-line-strong bg-surface\"\n onPointerDown={(e) => {\n e.currentTarget.setPointerCapture(e.pointerId)\n last.current = point(e)\n }}\n onPointerMove={draw}\n onPointerUp={() => (last.current = null)}\n />\n <div className=\"flex justify-end gap-2\">\n <Button variant=\"secondary\" onClick={clear}>\n Clear\n </Button>\n <Button variant=\"secondary\" onClick={onCancel}>\n Cancel\n </Button>\n <Button disabled={empty} onClick={save}>\n Use signature\n </Button>\n </div>\n </div>\n )\n}\n","import type { ReactNode } from 'react'\n\nconst tones = {\n success: 'bg-success/15 text-success',\n info: 'bg-heading/20 text-primary',\n muted: 'bg-hover text-muted',\n}\n\n/** Small coloured pill: a status or label on a row (Primary, Secondary…). */\nexport const Tag = ({ tone = 'info', children }: { tone?: keyof typeof tones; children: ReactNode }) => (\n <span\n className={`inline-block rounded-full px-2.5 py-0.5 text-[11px] font-semibold whitespace-nowrap ${tones[tone]}`}\n >\n {children}\n </span>\n)\n","import type { ReactNode } from 'react'\nimport { FONTS, type LayoutElement } from '../../core/form-logic'\nimport { fitInPage } from '../../core/coords'\nimport { Checkbox, FormField, Select, TextArea, TextInput } from '../ui/slots'\n\nconst num = (s: string) => (s.trim() === '' || !Number.isFinite(Number(s)) ? undefined : Number(s))\nconst pct = (n: number) => Math.round(n * 1000) / 10\n\nconst Section = ({ title, children }: { title: string; children: ReactNode }) => (\n <section className=\"space-y-3 border-b border-line px-3.5 py-3\">\n <h4 className=\"m-0 text-[12px] font-bold tracking-wide text-muted uppercase\">{title}</h4>\n {children}\n </section>\n)\n\n/** A colour picker with a \"none\" option (no background / no border). */\nfunction ColorField({\n label,\n value,\n onChange,\n allowNone,\n}: {\n label: string\n value: string | undefined\n onChange: (v: string | undefined) => void\n allowNone?: boolean\n}) {\n return (\n <FormField label={label}>\n <span className=\"flex items-center gap-2\">\n <input\n type=\"color\"\n aria-label={label}\n className=\"h-8 w-12 cursor-pointer rounded border border-line-strong bg-white\"\n value={value ?? '#ffffff'}\n onChange={(e) => onChange(e.target.value)}\n />\n {allowNone && (\n <label className=\"inline-flex items-center gap-1.5 text-[12.5px]\">\n <Checkbox\n label={`No ${label.toLowerCase()}`}\n checked={!value}\n onChange={(e) => onChange(e.target.checked ? undefined : '#ffffff')}\n />\n None\n </label>\n )}\n </span>\n </FormField>\n )\n}\n\n/** Text, style, colours and position of the selected text / box / line. */\nexport default function ElementProperties({\n element: e,\n pageCount,\n onChange,\n}: {\n element: LayoutElement\n pageCount: number\n onChange: (e: LayoutElement) => void\n}) {\n const set = (patch: Partial<LayoutElement>) => onChange({ ...e, ...patch })\n const setPos = (k: 'x' | 'y' | 'width' | 'height', v: string) => {\n const n = num(v)\n if (n == null) return\n const p = { ...e.position, [k]: n / 100 }\n // A line may have zero height (horizontal) or zero width (vertical); boxes keep a minimum size.\n if (e.type === 'line')\n set({ position: { ...p, width: Math.min(p.width, 1 - p.x), height: Math.min(p.height, 1 - p.y) } })\n else {\n const { page: _page, ...fitted } = fitInPage({ page: e.page, ...p })\n void _page\n set({ position: fitted })\n }\n }\n\n return (\n <div className=\"text-[13px]\">\n {e.type === 'text' && (\n <Section title=\"Text\">\n {e.replaces != null && (\n <p className=\"m-0 rounded bg-primary-soft px-2 py-1 text-[12px] text-ink-2\">\n Replaces the original: “{e.replaces}”\n </p>\n )}\n <TextArea\n rows={4}\n aria-label=\"Text\"\n autoFocus\n value={e.text ?? ''}\n maxLength={5000}\n onChange={(ev) => set({ text: ev.target.value })}\n />\n <div className=\"grid grid-cols-2 gap-2\">\n <FormField label=\"Font\">\n <Select\n className=\"w-full\"\n value={e.font ?? 'sans'}\n onChange={(ev) => set({ font: ev.target.value as LayoutElement['font'] })}\n >\n {FONTS.map((f) => (\n <option key={f} value={f}>\n {{ sans: 'Sans (Helvetica)', serif: 'Serif (Times)', mono: 'Mono (Courier)' }[f]}\n </option>\n ))}\n </Select>\n </FormField>\n <FormField label=\"Size (pt)\">\n <TextInput\n type=\"number\"\n min={3}\n max={144}\n step={0.5}\n value={e.fontSize ?? 10}\n onChange={(ev) => {\n const n = num(ev.target.value)\n if (n != null && n >= 3 && n <= 144) set({ fontSize: n })\n }}\n />\n </FormField>\n </div>\n <div className=\"flex flex-wrap items-center gap-4\">\n {(['bold', 'italic'] as const).map((k) => (\n <label key={k} className=\"inline-flex items-center gap-1.5\">\n <Checkbox label={k} checked={!!e[k]} onChange={(ev) => set({ [k]: ev.target.checked || undefined })} />\n {k === 'bold' ? 'Bold' : 'Italic'}\n </label>\n ))}\n <Select\n aria-label=\"Alignment\"\n value={e.align ?? 'left'}\n onChange={(ev) => set({ align: ev.target.value as LayoutElement['align'] })}\n >\n <option value=\"left\">Left</option>\n <option value=\"center\">Centre</option>\n <option value=\"right\">Right</option>\n </Select>\n </div>\n <ColorField label=\"Text colour\" value={e.color ?? '#000000'} onChange={(v) => set({ color: v })} />\n </Section>\n )}\n\n <Section title=\"Colours\">\n {e.type !== 'line' && (\n <ColorField label=\"Background\" allowNone value={e.fill} onChange={(v) => set({ fill: v })} />\n )}\n {e.type !== 'text' && (\n <>\n <ColorField\n label={e.type === 'line' ? 'Line colour' : 'Border'}\n allowNone={e.type === 'box'}\n value={e.stroke}\n onChange={(v) => set({ stroke: v })}\n />\n <FormField label=\"Line width (pt)\">\n <TextInput\n type=\"number\"\n min={0}\n max={20}\n step={0.5}\n value={e.strokeWidth ?? 1}\n onChange={(ev) => {\n const n = num(ev.target.value)\n if (n != null && n >= 0 && n <= 20) set({ strokeWidth: n })\n }}\n />\n </FormField>\n </>\n )}\n {e.fill === '#ffffff' && (\n <p className=\"m-0 text-[12px] text-muted\">A white background hides the original under it.</p>\n )}\n </Section>\n\n <Section title=\"Position (% of page)\">\n <div className=\"grid grid-cols-2 gap-2\">\n <FormField label=\"Page\">\n <TextInput\n type=\"number\"\n min={1}\n max={pageCount}\n value={e.page}\n onChange={(ev) => {\n const p = num(ev.target.value)\n if (p && Number.isInteger(p) && p >= 1 && p <= pageCount) set({ page: p })\n }}\n />\n </FormField>\n <span />\n {(['x', 'y', 'width', 'height'] as const).map((k) => (\n <FormField key={k} label={{ x: 'Left', y: 'Top', width: 'Width', height: 'Height' }[k]}>\n <TextInput\n type=\"number\"\n step={0.1}\n value={pct(e.position[k])}\n onChange={(ev) => setPos(k, ev.target.value)}\n />\n </FormField>\n ))}\n </div>\n </Section>\n </div>\n )\n}\n","import type { FormliftComponents } from '../ui/slots'\nimport { Paperclip, PenLine } from 'lucide-react'\nimport { useRef, useState } from 'react'\nimport type { AnswerValue, FieldDef, FieldType } from '../../core/form-logic'\nimport { Button, inputClass, Modal, ModalBody, ModalFooter, ModalHeader, SignaturePad } from '../ui/slots'\n\n// Text scales with the field's box (container query height), so it zooms with the page.\nconst box =\n 'h-full w-full rounded-sm border bg-primary-soft/70 px-1 text-[clamp(8px,62cqh,15px)] text-navy outline-none focus:border-primary focus:bg-white disabled:cursor-not-allowed'\n\n/** A PNG data URL (the signature pad's drawing) as a file to upload. */\nfunction pngFile(dataUrl: string, name: string) {\n const bin = atob(dataUrl.slice(dataUrl.indexOf(',') + 1))\n const bytes = new Uint8Array(bin.length)\n for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i)\n return new File([bytes], name.replace(/[\\\\/:*?\"<>|]/g, '_'), { type: 'image/png' })\n}\n\n// Typed in the full-size box on phones. 16 px text: iOS zooms the page into any smaller input it focuses.\nconst SHEET_TYPES: FieldType[] = ['text', 'textarea', 'number', 'date', 'email', 'phone', 'radio']\nconst INPUT_TYPE: Partial<Record<FieldType, string>> = { number: 'number', date: 'date', email: 'email', phone: 'tel' }\n\nfunction SheetInput({\n field,\n text,\n onChange,\n onDone,\n}: {\n field: FieldDef\n text: string\n onChange: (v: AnswerValue) => void\n onDone: () => void\n}) {\n if (field.type === 'radio')\n return (\n <div role=\"radiogroup\" aria-label={field.label || field.key} className=\"flex flex-col gap-1\">\n {(field.options ?? []).map((o) => (\n <label\n key={o}\n className=\"flex min-h-11 cursor-pointer items-center gap-3 rounded-md border border-line px-3 text-base\"\n >\n <input\n type=\"radio\"\n className=\"h-5 w-5 accent-primary\"\n name={`sheet-${field.id}`}\n checked={text === o}\n onChange={() => {\n onChange(o)\n onDone()\n }}\n />\n {o}\n </label>\n ))}\n </div>\n )\n if (field.type === 'textarea')\n return (\n <textarea\n autoFocus\n aria-label={field.label || field.key}\n className={`${inputClass} min-h-40 !text-base`}\n placeholder={field.placeholder}\n value={text}\n maxLength={field.maxLength}\n onChange={(e) => onChange(e.target.value)}\n />\n )\n return (\n <input\n autoFocus\n aria-label={field.label || field.key}\n type={INPUT_TYPE[field.type] ?? 'text'}\n className={`${inputClass} h-11 !text-base`}\n placeholder={field.placeholder}\n value={text}\n min={field.min}\n max={field.max}\n maxLength={field.maxLength}\n onChange={(e) => onChange(e.target.value)}\n onKeyDown={(e) => e.key === 'Enter' && onDone()}\n />\n )\n}\n\nexport interface FieldControlProps {\n field: FieldDef\n value: AnswerValue | undefined\n onChange: (v: AnswerValue) => void\n readOnly?: boolean\n error?: string\n fileName?: string\n onFile?: (file: File) => void\n onOpenFile?: () => void\n /** Image of a stored signature (the answer is its file id); signatures from before are data URLs. */\n signatureSrc?: string\n /** Phone / touch: typed answers open in a full-size box (the page's boxes are too small to type in). */\n compact?: boolean\n}\n\n/**\n * One answer control drawn inside its box on the page. `error` outlines it red; the message shows as a tooltip\n * and in the page's error list.\n */\nexport default function FieldControl({\n field,\n value,\n onChange,\n readOnly,\n error,\n fileName,\n onFile,\n onOpenFile,\n signatureSrc,\n compact,\n}: FieldControlProps) {\n const [signing, setSigning] = useState(false)\n const [editing, setEditing] = useState(false)\n const fileInput = useRef<HTMLInputElement>(null)\n const border = error ? 'border-danger' : 'border-primary/40'\n const common = {\n 'aria-label': field.label || field.key,\n 'aria-invalid': !!error,\n title: error || field.helpText || field.label,\n disabled: readOnly,\n }\n const text = typeof value === 'string' ? value : ''\n\n if (compact && !readOnly && SHEET_TYPES.includes(field.type)) {\n const name = field.label || field.key\n return (\n <>\n <button\n type=\"button\"\n {...common}\n onClick={() => setEditing(true)}\n className={`${box} ${border} flex cursor-pointer overflow-hidden text-left leading-tight ${\n field.type === 'textarea' ? 'items-start py-0.5 text-[clamp(8px,14cqh,15px)]' : 'items-center'\n }`}\n >\n <span className={`min-w-0 ${field.type === 'textarea' ? 'line-clamp-[8] whitespace-pre-wrap' : 'truncate'}`}>\n {text || <span className=\"text-faint\">{field.placeholder || ''}</span>}\n </span>\n </button>\n {editing && (\n <Modal onClose={() => setEditing(false)} width=\"w-[480px]\">\n <ModalHeader title={name} onClose={() => setEditing(false)} />\n <ModalBody>\n <SheetInput field={field} text={text} onChange={onChange} onDone={() => setEditing(false)} />\n {field.helpText && <p className=\"mt-2 mb-0 text-[13px] text-muted\">{field.helpText}</p>}\n {error && <p className=\"mt-2 mb-0 text-[13px] text-danger\">{error}</p>}\n </ModalBody>\n <ModalFooter>\n <Button onClick={() => setEditing(false)}>Done</Button>\n </ModalFooter>\n </Modal>\n )}\n </>\n )\n }\n\n switch (field.type) {\n case 'textarea':\n return (\n <textarea\n {...common}\n className={`${box} ${border} resize-none py-0.5 text-[clamp(8px,14cqh,15px)]`}\n placeholder={field.placeholder}\n value={text}\n maxLength={field.maxLength}\n onChange={(e) => onChange(e.target.value)}\n />\n )\n case 'dropdown':\n return (\n <select {...common} className={`${box} ${border}`} value={text} onChange={(e) => onChange(e.target.value)}>\n <option value=\"\">{field.placeholder || ''}</option>\n {(field.options ?? []).map((o) => (\n <option key={o}>{o}</option>\n ))}\n </select>\n )\n case 'radio':\n return (\n <div\n role=\"radiogroup\"\n {...common}\n className={`flex h-full w-full flex-wrap items-center gap-x-3 rounded-sm border px-1 text-[clamp(8px,55cqh,14px)] ${border} bg-primary-soft/50`}\n >\n {(field.options ?? []).map((o) => (\n <label key={o} className=\"inline-flex cursor-pointer items-center gap-1\">\n <input\n type=\"radio\"\n className=\"accent-primary\"\n name={field.id}\n checked={text === o}\n disabled={readOnly}\n onChange={() => onChange(o)}\n />\n {o}\n </label>\n ))}\n </div>\n )\n case 'checkbox':\n return (\n <label\n className={`flex h-full w-full items-center justify-center rounded-sm border ${border} bg-primary-soft/50`}\n >\n <input\n type=\"checkbox\"\n {...common}\n className=\"h-[min(80cqh,80cqw)] w-[min(80cqh,80cqw)] cursor-pointer accent-primary\"\n checked={value === true}\n onChange={(e) => onChange(e.target.checked)}\n />\n </label>\n )\n case 'signature':\n return (\n <>\n <button\n type=\"button\"\n {...common}\n onClick={() => setSigning(true)}\n className={`flex h-full w-full cursor-pointer items-center justify-center rounded-sm border ${border} bg-primary-soft/50 disabled:cursor-default`}\n >\n {text ? (\n <img\n src={text.startsWith('data:') ? text : signatureSrc}\n alt=\"Signature\"\n className=\"max-h-full max-w-full object-contain\"\n />\n ) : (\n <span className=\"inline-flex items-center gap-1 text-[clamp(8px,45cqh,13px)] text-primary\">\n <PenLine size={12} /> Sign\n </span>\n )}\n </button>\n {signing && (\n <Modal onClose={() => setSigning(false)} width=\"w-[640px]\">\n <ModalHeader title={`Sign: ${field.label || field.key}`} onClose={() => setSigning(false)} />\n <ModalBody>\n <SignaturePad\n onCancel={() => setSigning(false)}\n onSave={(url) => {\n // Stored as a PNG file (the answer gets its id); without an upload (editor preview) kept as is.\n if (onFile) onFile(pngFile(url, `Signature - ${field.label || field.key}.png`))\n else onChange(url)\n setSigning(false)\n }}\n />\n </ModalBody>\n </Modal>\n )}\n </>\n )\n case 'file':\n return (\n <div className={`flex h-full w-full items-center gap-1 rounded-sm border px-1 ${border} bg-primary-soft/50`}>\n <input\n ref={fileInput}\n type=\"file\"\n hidden\n onChange={(e) => {\n const f = e.target.files?.[0]\n if (f) onFile?.(f)\n e.target.value = ''\n }}\n />\n {value && (\n <button\n type=\"button\"\n onClick={onOpenFile}\n className=\"min-w-0 flex-1 cursor-pointer truncate text-left text-[clamp(8px,50cqh,13px)] text-primary underline\"\n >\n {fileName || 'Attached file'}\n </button>\n )}\n {!readOnly && (\n <button\n type=\"button\"\n {...common}\n onClick={() => fileInput.current?.click()}\n className=\"inline-flex flex-none cursor-pointer items-center gap-1 text-[clamp(8px,50cqh,13px)] text-primary\"\n >\n <Paperclip size={12} /> {value ? 'Replace' : 'Attach'}\n </button>\n )}\n </div>\n )\n default: {\n const type = INPUT_TYPE[field.type] ?? 'text'\n return (\n <input\n {...common}\n type={type}\n className={`${box} ${border}`}\n placeholder={field.placeholder}\n value={text}\n min={field.min}\n max={field.max}\n maxLength={field.maxLength}\n onChange={(e) => onChange(e.target.value)}\n />\n )\n }\n }\n}\n\n/** FieldControl, or the replacement from `components` (per field type first, then FieldControl). */\nexport function AnswerControl({ components, ...props }: FieldControlProps & { components?: FormliftComponents }) {\n const C = components?.fields?.[props.field.type] ?? components?.FieldControl ?? FieldControl\n return <C {...props} />\n}\n","import { useState, type ReactNode } from 'react'\nimport { FIELD_TYPES, type Condition, type FieldDef, type FieldType } from '../../core/form-logic'\nimport { fitInPage } from '../../core/coords'\nimport { TYPE_LABEL } from '../constants'\nimport ConditionBuilder from './ConditionBuilder'\nimport { Checkbox, FormField, Select, TextArea, TextInput } from '../ui/slots'\n\nconst num = (s: string) => (s.trim() === '' || !Number.isFinite(Number(s)) ? undefined : Number(s))\nconst pct = (n: number) => Math.round(n * 1000) / 10\n\nconst Section = ({ title, children }: { title: string; children: ReactNode }) => (\n <section className=\"space-y-3 border-b border-line px-3.5 py-3\">\n <h4 className=\"m-0 text-[12px] font-bold tracking-wide text-muted uppercase\">{title}</h4>\n {children}\n </section>\n)\n\nconst CONDITIONS = [\n ['visibleWhen', 'Show only when'],\n ['requiredWhen', 'Required when'],\n ['readOnlyWhen', 'Read-only when'],\n] as const\n\n/** Everything about the selected field: identity, rules, options, conditions, position (in % of the page). */\nexport default function FieldProperties({\n field,\n fields,\n pageCount,\n onChange,\n}: {\n field: FieldDef\n fields: FieldDef[]\n pageCount: number\n onChange: (f: FieldDef) => void\n}) {\n const set = (patch: Partial<FieldDef>) => onChange({ ...field, ...patch })\n const others = fields.filter((f) => f.id !== field.id)\n const [optionsText, setOptionsText] = useState((field.options ?? []).join('\\n'))\n const hasOptions = field.type === 'dropdown' || field.type === 'radio'\n const textual = !['checkbox', 'file', 'signature', 'dropdown', 'radio'].includes(field.type)\n const setPos = (k: 'x' | 'y' | 'width' | 'height', v: string) => {\n const n = num(v)\n if (n != null) set({ position: fitInPage({ ...field.position, [k]: n / 100 }) })\n }\n\n return (\n <div className=\"text-[13px]\">\n <Section title=\"Field\">\n {field.source === 'ai' && (\n <p className=\"m-0 rounded bg-folder/15 px-2 py-1 text-[12px]\">\n Suggested automatically\n {field.confidence != null && ` (${Math.round(field.confidence * 100)}% sure)`}. Check it.\n </p>\n )}\n <p className=\"m-0 font-mono text-[11px] text-faint\">ID {field.id}</p>\n <FormField label=\"Label\">\n <TextInput value={field.label} onChange={(e) => set({ label: e.target.value })} />\n </FormField>\n {/* Made from the label (unique, kept in step by conditions), never typed: no clashes. */}\n <FormField label=\"Key (automatic, from the label)\">\n <TextInput value={field.key} readOnly className=\"text-faint\" />\n </FormField>\n <FormField label=\"Type\">\n <Select className=\"w-full\" value={field.type} onChange={(e) => set({ type: e.target.value as FieldType })}>\n {FIELD_TYPES.map((t) => (\n <option key={t} value={t}>\n {TYPE_LABEL[t]}\n </option>\n ))}\n </Select>\n </FormField>\n {textual && (\n <FormField label=\"Placeholder\">\n <TextInput\n value={field.placeholder ?? ''}\n onChange={(e) => set({ placeholder: e.target.value || undefined })}\n />\n </FormField>\n )}\n {field.type !== 'file' && field.type !== 'signature' && (\n <FormField label={field.type === 'checkbox' ? 'Checked by default' : 'Default value'}>\n {field.type === 'checkbox' ? (\n <Checkbox\n label=\"Checked by default\"\n checked={field.defaultValue === 'true'}\n onChange={(e) => set({ defaultValue: e.target.checked ? 'true' : undefined })}\n />\n ) : (\n <TextInput\n value={field.defaultValue ?? ''}\n onChange={(e) => set({ defaultValue: e.target.value || undefined })}\n />\n )}\n </FormField>\n )}\n <FormField label=\"Help text (shown as tooltip)\">\n <TextInput value={field.helpText ?? ''} onChange={(e) => set({ helpText: e.target.value || undefined })} />\n </FormField>\n <div className=\"flex flex-wrap gap-4\">\n {(['required', 'readOnly', 'hidden'] as const).map((k) => (\n <label key={k} className=\"inline-flex items-center gap-1.5\">\n <Checkbox label={k} checked={!!field[k]} onChange={(e) => set({ [k]: e.target.checked || undefined })} />\n {{ required: 'Required', readOnly: 'Read-only', hidden: 'Hidden' }[k]}\n </label>\n ))}\n </div>\n </Section>\n\n {hasOptions && (\n <Section title=\"Options (one per line)\">\n <TextArea\n rows={4}\n value={optionsText}\n onChange={(e) => {\n setOptionsText(e.target.value)\n set({\n options: [\n ...new Set(\n e.target.value\n .split('\\n')\n .map((s) => s.trim())\n .filter(Boolean),\n ),\n ],\n })\n }}\n />\n </Section>\n )}\n\n {textual && (\n <Section title=\"Validation\">\n <div className=\"grid grid-cols-2 gap-2\">\n <FormField label=\"Min length\">\n <TextInput\n type=\"number\"\n min={0}\n value={field.minLength ?? ''}\n onChange={(e) => set({ minLength: num(e.target.value) })}\n />\n </FormField>\n <FormField label=\"Max length\">\n <TextInput\n type=\"number\"\n min={0}\n value={field.maxLength ?? ''}\n onChange={(e) => set({ maxLength: num(e.target.value) })}\n />\n </FormField>\n {field.type === 'number' && (\n <>\n <FormField label=\"Min value\">\n <TextInput\n type=\"number\"\n value={field.min ?? ''}\n onChange={(e) => set({ min: num(e.target.value) })}\n />\n </FormField>\n <FormField label=\"Max value\">\n <TextInput\n type=\"number\"\n value={field.max ?? ''}\n onChange={(e) => set({ max: num(e.target.value) })}\n />\n </FormField>\n </>\n )}\n </div>\n <FormField label=\"Regex (whole value must match)\">\n <TextInput\n className=\"font-mono\"\n value={field.pattern ?? ''}\n onChange={(e) => set({ pattern: e.target.value || undefined })}\n />\n </FormField>\n </Section>\n )}\n\n {CONDITIONS.map(([key, title]) => {\n const cond = field[key]\n return (\n <Section key={key} title={title}>\n {cond ? (\n <ConditionBuilder\n value={cond}\n fields={others}\n onChange={(c: Condition) => set({ [key]: c.rules.length ? c : undefined })}\n />\n ) : (\n <button\n type=\"button\"\n disabled={!others.length}\n className=\"cursor-pointer text-primary hover:underline disabled:cursor-not-allowed disabled:text-faint\"\n onClick={() =>\n set({ [key]: { logic: 'and', rules: [{ fieldKey: others[0].key, op: 'equals', value: '' }] } })\n }\n >\n + Add condition\n </button>\n )}\n </Section>\n )\n })}\n\n <Section title=\"Position (% of page)\">\n <div className=\"grid grid-cols-2 gap-2\">\n <FormField label=\"Page\">\n <TextInput\n type=\"number\"\n min={1}\n max={pageCount}\n value={field.position.page}\n onChange={(e) => {\n const p = num(e.target.value)\n if (p && Number.isInteger(p) && p >= 1 && p <= pageCount)\n set({ position: { ...field.position, page: p } })\n }}\n />\n </FormField>\n <span />\n {(['x', 'y', 'width', 'height'] as const).map((k) => (\n <FormField key={k} label={{ x: 'Left', y: 'Top', width: 'Width', height: 'Height' }[k]}>\n <TextInput\n type=\"number\"\n step={0.1}\n value={pct(field.position[k])}\n onChange={(e) => setPos(k, e.target.value)}\n />\n </FormField>\n ))}\n </div>\n </Section>\n </div>\n )\n}\n","import type { FieldType } from '../core/form-logic'\n\nexport const TYPE_LABEL: Record<FieldType, string> = {\n text: 'Text',\n textarea: 'Text area',\n number: 'Number',\n date: 'Date',\n email: 'Email',\n phone: 'Phone',\n dropdown: 'Dropdown',\n radio: 'Radio group',\n checkbox: 'Checkbox',\n file: 'File upload',\n signature: 'Signature',\n}\n\nexport const ZOOMS = [0.5, 0.75, 1, 1.25, 1.5, 2]\n\n/** CSS width of a page at zoom 1 is its width in points (72 dpi) × this. */\nexport const BASE_SCALE = 96 / 72\n","import { Plus, X } from 'lucide-react'\nimport { OPERATORS, type Condition, type FieldDef, type Operator, type Rule } from '../../core/form-logic'\nimport { Button, IconButton, Select, TextInput } from '../ui/slots'\n\nconst OP_LABEL: Record<Operator, string> = {\n equals: 'equals',\n notEquals: 'does not equal',\n contains: 'contains',\n startsWith: 'starts with',\n endsWith: 'ends with',\n greaterThan: 'is greater than',\n lessThan: 'is less than',\n between: 'is between',\n isEmpty: 'is empty',\n isNotEmpty: 'is not empty',\n}\nconst isGroup = (r: Rule | Condition): r is Condition => 'rules' in r\n\n/**\n * IF <field> <operator> <value> rows joined by AND / OR, with nested groups. `fields` = the fields a rule may\n * point at (every field but the one being edited). The API rejects unknown and circular references again.\n */\nexport default function ConditionBuilder({\n value,\n onChange,\n fields,\n depth = 0,\n}: {\n value: Condition\n onChange: (c: Condition) => void\n fields: FieldDef[]\n depth?: number\n}) {\n const setRule = (i: number, r: Rule | Condition) =>\n onChange({ ...value, rules: value.rules.map((x, j) => (j === i ? r : x)) })\n const remove = (i: number) => onChange({ ...value, rules: value.rules.filter((_, j) => j !== i) })\n const newRule = (): Rule => ({ fieldKey: fields[0]?.key ?? '', op: 'equals', value: '' })\n\n return (\n <div className={`space-y-2 ${depth ? 'rounded-md border border-line bg-surface p-2' : ''}`}>\n <div className=\"flex items-center gap-2 text-[12px]\">\n Match\n <Select\n aria-label=\"Match all or any\"\n value={value.logic}\n onChange={(e) => onChange({ ...value, logic: e.target.value as Condition['logic'] })}\n >\n <option value=\"and\">ALL (AND)</option>\n <option value=\"or\">ANY (OR)</option>\n </Select>\n </div>\n {value.rules.map((r, i) =>\n isGroup(r) ? (\n <div key={i} className=\"flex items-start gap-1\">\n <div className=\"flex-1\">\n <ConditionBuilder value={r} onChange={(c) => setRule(i, c)} fields={fields} depth={depth + 1} />\n </div>\n <IconButton icon={X} title=\"Remove group\" danger iconSize={14} onClick={() => remove(i)} />\n </div>\n ) : (\n <div key={i} className=\"flex flex-wrap items-center gap-1\">\n <Select\n aria-label=\"Field\"\n value={r.fieldKey}\n onChange={(e) => setRule(i, { ...r, fieldKey: e.target.value })}\n >\n {!fields.some((f) => f.key === r.fieldKey) && <option value={r.fieldKey}>{r.fieldKey || '—'}</option>}\n {fields.map((f) => (\n <option key={f.id} value={f.key}>\n {f.label || f.key}\n </option>\n ))}\n </Select>\n <Select\n aria-label=\"Operator\"\n value={r.op}\n onChange={(e) => setRule(i, { ...r, op: e.target.value as Operator })}\n >\n {OPERATORS.map((op) => (\n <option key={op} value={op}>\n {OP_LABEL[op]}\n </option>\n ))}\n </Select>\n {r.op !== 'isEmpty' && r.op !== 'isNotEmpty' && (\n <TextInput\n aria-label=\"Value\"\n className=\"!w-24 !py-1\"\n value={r.value ?? ''}\n onChange={(e) => setRule(i, { ...r, value: e.target.value })}\n />\n )}\n {r.op === 'between' && (\n <TextInput\n aria-label=\"Upper value\"\n className=\"!w-24 !py-1\"\n value={r.value2 ?? ''}\n onChange={(e) => setRule(i, { ...r, value2: e.target.value })}\n />\n )}\n <IconButton icon={X} title=\"Remove rule\" danger iconSize={14} onClick={() => remove(i)} />\n </div>\n ),\n )}\n <div className=\"flex gap-2\">\n <Button\n size=\"sm\"\n variant=\"secondary\"\n icon={Plus}\n disabled={!fields.length}\n onClick={() => onChange({ ...value, rules: [...value.rules, newRule()] })}\n >\n Rule\n </Button>\n {depth < 2 && (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n icon={Plus}\n disabled={!fields.length}\n onClick={() => onChange({ ...value, rules: [...value.rules, { logic: 'and', rules: [newRule()] }] })}\n >\n Group\n </Button>\n )}\n </div>\n </div>\n )\n}\n","import type { CSSProperties } from 'react'\nimport type { LayoutElement } from '../../core/form-logic'\nimport { boxStyle } from '../../core/coords'\n\n// Same faces as the completed PDF (Helvetica / Times / Courier), so screen and export match.\nconst FAMILY = {\n sans: 'Helvetica, Arial, sans-serif',\n serif: '\"Times New Roman\", Times, serif',\n mono: '\"Courier New\", Courier, monospace',\n}\n\n/** Points on a page `pageHeight` points tall, as a size that zooms with the page (cqh of the page overlay). */\nconst ptToCqh = (pt: number, pageHeight: number) => `${(pt / pageHeight) * 100}cqh`\n\n/**\n * One of the admin's items drawn on its page: text (with optional background that hides the original under it),\n * box, or line. Purely visual; the editor wraps it for selecting and dragging.\n */\nexport default function LayoutElementView({\n element: e,\n pageHeight,\n className = '',\n style,\n}: {\n element: LayoutElement\n pageHeight: number\n className?: string\n style?: CSSProperties\n}) {\n const stroke = ptToCqh(e.strokeWidth ?? 1, pageHeight)\n const base: CSSProperties = { ...boxStyle({ page: e.page, ...e.position }), ...style }\n\n if (e.type === 'line')\n return (\n <svg className={`absolute overflow-visible ${className}`} style={base} aria-hidden>\n {/* A flat side is drawn at 0, so a horizontal / vertical line stays straight in a box given a minimum size. */}\n <line\n x1=\"0\"\n y1=\"0\"\n x2={e.position.width > 0 ? '100%' : '0'}\n y2={e.position.height > 0 ? '100%' : '0'}\n stroke={e.stroke ?? '#000000'}\n style={{ strokeWidth: stroke }}\n />\n </svg>\n )\n\n return (\n <div\n className={`absolute overflow-hidden whitespace-pre-wrap ${className}`}\n style={{\n ...base,\n background: e.fill,\n border: e.type === 'box' && e.stroke ? `${stroke} solid ${e.stroke}` : undefined,\n color: e.color ?? '#000000',\n fontFamily: FAMILY[e.font ?? 'sans'],\n fontSize: ptToCqh(e.fontSize ?? 10, pageHeight),\n fontWeight: e.bold ? 700 : 400,\n fontStyle: e.italic ? 'italic' : 'normal',\n textAlign: e.align ?? 'left',\n lineHeight: 1.2,\n wordBreak: 'break-word',\n }}\n >\n {e.type === 'text' ? e.text : null}\n </div>\n )\n}\n","import { ArrowDown, ArrowUp, FilePlus2, Trash2 } from 'lucide-react'\nimport type { LayoutPage } from '../../core/form-logic'\nimport { IconButton } from '../ui/slots'\n\n/**\n * The document's pages in order: original pages (with their number in the uploaded PDF) and blank ones.\n * Move, delete, add a blank page after. `onGoTo` scrolls the document to a page.\n */\nexport default function PagesPanel({\n pages,\n counts,\n onMove,\n onDelete,\n onInsertBlank,\n onGoTo,\n}: {\n pages: LayoutPage[]\n /** Fields + items per page (1-based index), shown so deleting a page is not a surprise. */\n counts: Record<number, number>\n onMove: (page: number, step: -1 | 1) => void\n onDelete: (page: number) => void\n onInsertBlank: (after: number) => void\n onGoTo: (page: number) => void\n}) {\n return (\n <div className=\"text-[12.5px]\">\n <button\n type=\"button\"\n className=\"flex w-full cursor-pointer items-center gap-1.5 border-b border-line-soft px-3 py-2 text-primary hover:bg-primary-soft\"\n onClick={() => onInsertBlank(0)}\n >\n <FilePlus2 size={14} /> Blank page at the start\n </button>\n <ol className=\"m-0 list-none p-0\">\n {pages.map((p, i) => {\n const n = i + 1\n return (\n <li key={i} className=\"flex items-center gap-1 border-b border-line-soft px-2 py-1\">\n <button\n type=\"button\"\n className=\"min-w-0 flex-1 cursor-pointer truncate text-left\"\n onClick={() => onGoTo(n)}\n >\n <b>Page {n}</b> <span className=\"text-muted\">{p.source ? `· original p.${p.source}` : '· blank'}</span>\n {counts[n] ? <span className=\"text-faint\"> · {counts[n]} items</span> : null}\n </button>\n <IconButton\n icon={ArrowUp}\n title=\"Move page up\"\n iconSize={13}\n disabled={n === 1}\n onClick={() => onMove(n, -1)}\n />\n <IconButton\n icon={ArrowDown}\n title=\"Move page down\"\n iconSize={13}\n disabled={n === pages.length}\n onClick={() => onMove(n, 1)}\n />\n <IconButton\n icon={FilePlus2}\n title=\"Add blank page after\"\n iconSize={13}\n onClick={() => onInsertBlank(n)}\n />\n <IconButton\n icon={Trash2}\n title=\"Delete page\"\n danger\n iconSize={13}\n disabled={pages.length <= 1}\n onClick={() => onDelete(n)}\n />\n </li>\n )\n })}\n </ol>\n </div>\n )\n}\n","import type { PDFDocumentProxy, RenderTask } from 'pdfjs-dist'\nimport { useEffect, useRef, useState, type ReactNode } from 'react'\nimport type { LayoutPage } from '../../core/form-logic'\nimport { toLines, type PdfTextItem, type TextLine } from '../../core/layout'\nimport { loadPdfJs } from '../pdfjs'\nimport { BASE_SCALE } from '../constants'\n\n/** A PDF: URL, Blob / File, bytes, or a (possibly async) function returning one of those. */\nexport type PdfBytes = string | URL | Blob | ArrayBuffer | Uint8Array\nexport type PdfSource = PdfBytes | (() => PdfBytes | Promise<PdfBytes>)\n\nexport async function readPdf(src: PdfSource): Promise<ArrayBuffer | Uint8Array> {\n const v = typeof src === 'function' ? await src() : src\n if (typeof v === 'string' || v instanceof URL) {\n const res = await fetch(v)\n if (!res.ok) throw new Error(`Could not load the PDF (${res.status})`)\n return res.arrayBuffer()\n }\n // A copy: pdf.js hands the buffer to its worker, which would empty the caller's array.\n return v instanceof Blob ? v.arrayBuffer() : (v instanceof ArrayBuffer ? new Uint8Array(v) : v).slice()\n}\n\nexport interface PageSize {\n width: number\n height: number\n}\n\n/**\n * The document, page under page: original PDF pages exactly as pdf.js draws them, and blank pages, in the order\n * of `pages` (the version's layout; omitted = the original pages in order). Pages are drawn when they scroll into\n * view and again after a zoom. `overlay(page)` is laid over each page in a box of the page's size: children\n * placed with percentage positions (utils/coords.ts boxStyle) stay on their spot at every zoom, and font sizes in\n * `cqh` scale with the page. With `withText`, overlay also gets the original text lines of the page.\n */\nexport default function PdfPages({\n load,\n zoom,\n pages,\n overlay,\n withText,\n onLoaded,\n onError,\n}: {\n /** The PDF: a URL, its bytes, or a function returning either. Read once per mount. */\n load: PdfSource\n zoom: number\n pages?: LayoutPage[] | null\n overlay?: (page: number, lines: TextLine[] | undefined) => ReactNode\n withText?: boolean\n /** Sizes (points) of the original pages and the open document, once the PDF is open. */\n onLoaded?: (sizes: PageSize[], pdf: PDFDocumentProxy) => void\n onError?: (err: unknown) => void\n}) {\n const [doc, setDoc] = useState<PDFDocumentProxy | null>(null)\n const [sizes, setSizes] = useState<PageSize[]>([])\n const loadRef = useRef(load)\n const callbacks = useRef({ onLoaded, onError })\n useEffect(() => {\n callbacks.current = { onLoaded, onError }\n })\n\n useEffect(() => {\n let live = true\n let opened: PDFDocumentProxy | null = null\n Promise.all([loadPdfJs(), readPdf(loadRef.current)])\n // isEvalSupported off (pdf.js < 5.5; newer never evals): pdf.js never compiles code from the document (fonts).\n .then(([lib, data]) => lib.getDocument({ data, ...({ isEvalSupported: false } as object) }).promise)\n .then(async (pdf) => {\n opened = pdf\n const list: PageSize[] = []\n for (let i = 1; i <= pdf.numPages; i++) {\n const vp = (await pdf.getPage(i)).getViewport({ scale: 1 })\n list.push({ width: vp.width, height: vp.height })\n }\n if (!live) return\n setSizes(list)\n setDoc(pdf)\n callbacks.current.onLoaded?.(list, pdf)\n })\n .catch((err) => live && callbacks.current.onError?.(err))\n return () => {\n live = false\n void opened?.destroy()\n }\n }, [])\n\n if (!doc) return <p className=\"p-6 text-center text-[13px] text-muted\">Loading document…</p>\n const shown: LayoutPage[] = pages ?? sizes.map((s, i) => ({ source: i + 1, ...s }))\n return (\n // min-w-full + w-max: centred when narrower than the panel, fully scrollable (left edge too) when wider.\n <div className=\"flex w-max min-w-full flex-col gap-4 p-4\">\n {shown.map((p, i) => (\n <PdfPage\n key={i}\n doc={doc}\n pageNo={i + 1}\n source={p.source}\n size={p.source ? (sizes[p.source - 1] ?? p) : p}\n zoom={zoom}\n withText={withText}\n overlay={overlay}\n />\n ))}\n </div>\n )\n}\n\nfunction PdfPage({\n doc,\n pageNo,\n source,\n size,\n zoom,\n withText,\n overlay,\n}: {\n doc: PDFDocumentProxy\n pageNo: number\n source: number | null\n size: PageSize\n zoom: number\n withText?: boolean\n overlay?: (page: number, lines: TextLine[] | undefined) => ReactNode\n}) {\n const box = useRef<HTMLDivElement>(null)\n const canvas = useRef<HTMLCanvasElement>(null)\n const [visible, setVisible] = useState(false)\n const [lines, setLines] = useState<{ source: number; lines: TextLine[] } | null>(null)\n const width = size.width * BASE_SCALE * zoom\n const height = size.height * BASE_SCALE * zoom\n\n useEffect(() => {\n const el = box.current\n if (!el || typeof IntersectionObserver === 'undefined') return setVisible(true)\n const io = new IntersectionObserver(([e]) => e.isIntersecting && setVisible(true), { rootMargin: '600px' })\n io.observe(el)\n return () => io.disconnect()\n }, [])\n\n useEffect(() => {\n if (!visible || !source || !canvas.current) return\n let task: RenderTask | null = null\n let live = true\n const el = canvas.current\n void doc.getPage(source).then((page) => {\n if (!live) return\n const ratio = window.devicePixelRatio || 1\n const viewport = page.getViewport({ scale: BASE_SCALE * zoom * ratio })\n el.width = Math.floor(viewport.width)\n el.height = Math.floor(viewport.height)\n task = page.render({ canvas: el, viewport })\n task.promise.catch(() => {}) // cancelled by a newer zoom\n })\n return () => {\n live = false\n task?.cancel()\n }\n }, [doc, source, visible, zoom])\n\n // The original's text lines, read once per page when asked for (Edit text).\n useEffect(() => {\n if (!withText || !visible || !source || lines?.source === source) return\n let live = true\n void doc\n .getPage(source)\n .then(async (page) => {\n const content = await page.getTextContent()\n // Real font names (for bold / italic) are known once the page's fonts are loaded by drawing it.\n const fontOf = (id: string) => {\n try {\n return page.commonObjs.has(id) ? (page.commonObjs.get(id) as { name?: string })?.name : undefined\n } catch {\n return undefined\n }\n }\n if (live) setLines({ source, lines: toLines(content.items as PdfTextItem[], page.view, fontOf) })\n })\n .catch(() => live && setLines({ source, lines: [] }))\n return () => {\n live = false\n }\n }, [doc, source, visible, withText, lines])\n\n return (\n <div\n ref={box}\n data-page={pageNo}\n className=\"relative mx-auto flex-none bg-white shadow-[0_1px_6px_rgba(15,23,42,0.18)]\"\n style={{ width, height }}\n >\n {source && <canvas ref={canvas} className=\"absolute inset-0 h-full w-full\" aria-label={`Page ${pageNo}`} />}\n <div className=\"absolute inset-0 [container-type:size]\">\n {overlay?.(pageNo, withText && lines?.source === source ? lines.lines : undefined)}\n </div>\n </div>\n )\n}\n","// pdf.js is loaded when the first document opens: it needs browser APIs (DOMMatrix) the moment it loads.\n// The worker: whatever the app set (setPdfWorker or pdfjs GlobalWorkerOptions), else the matching build on a CDN.\nexport type PdfJs = typeof import('pdfjs-dist')\n\nlet workerSrc: string | undefined\nlet workerPort: Worker | undefined\n\n/**\n * Where pdf.js loads its worker from. Call once before the first document opens, e.g. with Vite:\n * `setPdfWorker(new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).href)`\n * or pass a Worker. Without it the worker comes from unpkg.com (same pdfjs-dist version).\n */\nexport function setPdfWorker(src: string | Worker) {\n if (typeof src === 'string') workerSrc = src\n else workerPort = src\n}\n\nlet pdfjs: Promise<PdfJs> | null = null\nexport const loadPdfJs = () =>\n (pdfjs ??= import('pdfjs-dist').then((lib) => {\n const o = lib.GlobalWorkerOptions\n if (workerPort) o.workerPort = workerPort\n else if (workerSrc) o.workerSrc = workerSrc\n else if (!o.workerSrc && !o.workerPort)\n // ponytail: CDN fallback needs internet; self-host via setPdfWorker for offline / strict CSP.\n o.workerSrc = `https://unpkg.com/pdfjs-dist@${lib.version}/build/pdf.worker.min.mjs`\n return lib\n }))\n","import { Minus, Plus } from 'lucide-react'\nimport { IconButton } from '../ui/slots'\n\nimport { ZOOMS } from '../constants'\n\n/** − 100 % +. `zoom` may be in between the steps (fitted to the screen); the buttons go to the next step. */\nexport default function ZoomControls({ zoom, onChange }: { zoom: number; onChange: (z: number) => void }) {\n const lower = [...ZOOMS].reverse().find((z) => z < zoom - 0.01)\n const higher = ZOOMS.find((z) => z > zoom + 0.01)\n return (\n <div className=\"inline-flex items-center gap-1 text-[13px]\">\n <IconButton icon={Minus} title=\"Zoom out\" disabled={lower == null} onClick={() => onChange(lower!)} />\n <span className=\"w-12 text-center tabular-nums\">{Math.round(zoom * 100)}%</span>\n <IconButton icon={Plus} title=\"Zoom in\" disabled={higher == null} onClick={() => onChange(higher!)} />\n </div>\n )\n}\n","import { useEffect, useState } from 'react'\nimport { BASE_SCALE, ZOOMS } from '../constants'\n\n// PdfPages pads the pages by 16 px on each side.\nconst PADDING = 32\n\n/**\n * Zoom that fits a page `pageWidthPt` points wide into the scroll panel (a big screen gets a big page, up to the\n * highest zoom), following the\n * panel's size (phone, tablet, a resized window) until the user picks a zoom. Pass `ref` to the scroll panel.\n */\nexport function useFitZoom(pageWidthPt: number) {\n const [el, ref] = useState<HTMLElement | null>(null)\n const [fit, setFit] = useState(1)\n const [chosen, setZoom] = useState<number | null>(null)\n useEffect(() => {\n if (!el) return\n const measure = () => {\n const room = el.clientWidth - PADDING\n if (room > 0) setFit(Math.min(ZOOMS[ZOOMS.length - 1], Math.max(0.3, room / (pageWidthPt * BASE_SCALE))))\n }\n measure()\n if (typeof ResizeObserver === 'undefined') return\n const watch = new ResizeObserver(measure)\n watch.observe(el)\n return () => watch.disconnect()\n }, [el, pageWidthPt])\n return { zoom: chosen ?? fit, setZoom, ref }\n}\n\n/** Small screen or touch: answers are typed in a full-size box instead of on the page. */\nexport function useCompact() {\n const query = '(max-width: 767px), (pointer: coarse)'\n const get = () => typeof window !== 'undefined' && !!window.matchMedia?.(query).matches\n const [compact, setCompact] = useState(get)\n useEffect(() => {\n const m = window.matchMedia?.(query)\n if (!m) return\n const on = () => setCompact(m.matches)\n m.addEventListener('change', on)\n return () => m.removeEventListener('change', on)\n }, [])\n return compact\n}\n","import type { PDFDocumentProxy } from 'pdfjs-dist'\nimport { useCallback, useEffect, useMemo, useRef, useState, type PointerEvent as ReactPointerEvent } from 'react'\nimport { moveBy, resizeBy, snap, toPage } from '../../core/coords'\nimport { detectDocument, type Ops } from '../../core/detect'\nimport { duplicate, newField, reorder, withField } from '../../core/fields'\nimport {\n defaultLayout,\n FIELD_TYPES,\n fieldStates,\n validateFields,\n validateLayout,\n type Answers,\n type FieldDef,\n type FieldType,\n type Layout,\n type LayoutElement,\n type Position,\n} from '../../core/form-logic'\nimport {\n deletePage,\n duplicateElement,\n ELEMENT_TOOLS,\n fitTextWidth,\n insertBlankPage,\n movePage,\n newElement,\n replaceLine,\n type Doc,\n type ElementTool,\n type TextLine,\n} from '../../core/layout'\nimport type { PageSize } from '../components/PdfPages'\nimport { loadPdfJs } from '../pdfjs'\nimport { useUndo } from './useUndo'\n\nconst GRID = 0.005 // snap step: 0.5 % of the page\n\nexport type Selection = { kind: 'field' | 'element'; id: string } | null\n/** The editor's document: fields + layout (null = the original pages untouched, nothing drawn). */\nexport type FormDoc = { fields: FieldDef[]; layout: Layout | null }\nconst EMPTY: FormDoc = { fields: [], layout: null }\n\nexport interface UseFormEditorOptions {\n /** Saved fields + layout to start from (omit for a new form). Remount (`key`) to load another. */\n initialValue?: FormDoc\n /** Called by save() with the document. A rejected promise keeps the changes unsaved. */\n onSave: (doc: FormDoc) => unknown\n /** Every change (also undo / redo). */\n onChange?: (doc: FormDoc) => void\n /** Run auto-detect when the PDF opens. Default: only when there are no fields and no layout yet. */\n autoDetect?: boolean\n /** Called with the suggested fields after a detect that found some. */\n onDetected?: (found: FieldDef[]) => void\n /** Delete, Ctrl+Z / Ctrl+Y, Ctrl+D, arrows, Escape on window. Default true. */\n keyboard?: boolean\n /** Field types that can be added. Default: all (FIELD_TYPES). */\n fieldTypes?: readonly FieldType[]\n /** Layout tools that can be added. Default: all. */\n elementTools?: readonly ElementTool[]\n /** Browser warning when leaving with unsaved changes. Default true. */\n warnUnsaved?: boolean\n notify?: (message: string) => void\n onError?: (error: unknown) => void\n}\n\nconst isDefault = (l: Layout, sizes: PageSize[]) =>\n !l.elements.length && l.pages.length === sizes.length && l.pages.every((p, i) => p.source === i + 1)\n\n/**\n * Everything FormEditor does, without its UI: the document with undo / redo, selection, placing / moving / resizing\n * fields and layout items, pages, auto-detect, preview answers, validation and save. Render the pages with\n * PdfPages (`onLoaded={ed.onPdfLoaded}`) and draw your own boxes, panels and toolbars around it.\n */\nexport function useFormEditor({\n initialValue = EMPTY,\n onSave,\n onChange,\n autoDetect,\n onDetected,\n keyboard = true,\n warnUnsaved = true,\n fieldTypes = FIELD_TYPES,\n elementTools = Object.keys(ELEMENT_TOOLS) as ElementTool[],\n notify = () => {},\n onError: notifyError = (e) => console.error(e),\n}: UseFormEditorOptions) {\n const [sizes, setSizes] = useState<PageSize[] | null>(null)\n const history = useUndo<FormDoc>(initialValue)\n const { value: doc, set } = history\n const [saved, setSaved] = useState<FormDoc>(initialValue)\n const [selection, setSelection] = useState<Selection>(null)\n // A field / layout tool picked by a tap (touch screens cannot drag): the next tap on a page places it.\n const [armed, setArmed] = useState<string | null>(null)\n const [snapOn, setSnapOn] = useState(true)\n const [editText, setEditText] = useState(false)\n const [preview, setPreviewState] = useState(false)\n const [previewAnswers, setPreviewAnswers] = useState<Answers>({})\n const [busy, setBusy] = useState(false)\n const [pdf, setPdf] = useState<PDFDocumentProxy | null>(null)\n const [detecting, setDetecting] = useState<{ done: number; total: number; stop: AbortController } | null>(null)\n const autoDetected = useRef(false)\n const drag = useRef<{\n sel: NonNullable<Selection>\n mode: 'move' | 'resize'\n x: number\n y: number\n start: Position\n rect: DOMRect\n } | null>(null)\n\n const { fields } = doc\n // The layout as shown: the saved one, or the original pages once the PDF told us their sizes.\n const layout: Layout | null = doc.layout ?? (sizes ? defaultLayout(sizes) : null)\n const pageCount = layout?.pages.length ?? 1\n const grid = snapOn ? GRID : 0\n const selectedField = selection?.kind === 'field' ? (fields.find((f) => f.id === selection.id) ?? null) : null\n const selectedElement =\n selection?.kind === 'element' ? (layout?.elements.find((e) => e.id === selection.id) ?? null) : null\n const dirty = JSON.stringify(doc) !== JSON.stringify(saved)\n const problems = useMemo(\n () =>\n sizes\n ? [...(doc.layout ? validateLayout(doc.layout, sizes.length) : []), ...validateFields(fields, pageCount)]\n : [],\n [sizes, doc.layout, fields, pageCount],\n )\n\n const onChangeRef = useRef(onChange)\n useEffect(() => {\n onChangeRef.current = onChange\n })\n const first = useRef(true)\n useEffect(() => {\n if (first.current) first.current = false\n else onChangeRef.current?.(doc)\n }, [doc])\n\n // Changes that touch the layout start from the shown one (the default until the first change).\n const withDoc = useCallback(\n (fn: (d: Doc) => Doc, record = true) =>\n set((cur) => {\n const base = cur.layout ?? (sizes ? defaultLayout(sizes) : null)\n if (!base) return cur\n return fn({ fields: cur.fields, layout: base })\n }, record),\n [set, sizes],\n )\n const setFields = useCallback(\n (fn: (f: FieldDef[]) => FieldDef[], record = true) => set((cur) => ({ ...cur, fields: fn(cur.fields) }), record),\n [set],\n )\n const setElements = useCallback(\n (fn: (e: LayoutElement[]) => LayoutElement[], record = true) =>\n withDoc((d) => ({ ...d, layout: { ...d.layout, elements: fn(d.layout.elements) } }), record),\n [withDoc],\n )\n\n const updateField = useCallback((f: FieldDef) => setFields((l) => withField(l, f)), [setFields])\n // Text items grow to fit longer text, so nothing is cut off.\n const updateElement = useCallback(\n (e: LayoutElement) => {\n const pageWidth = layout?.pages[e.page - 1]?.width ?? 595\n setElements((l) => l.map((x) => (x.id === e.id ? fitTextWidth(e, pageWidth) : x)))\n },\n [setElements, layout],\n )\n const removeSelected = useCallback(() => {\n if (!selection) return\n if (selection.kind === 'field') setFields((l) => l.filter((x) => x.id !== selection.id))\n else setElements((l) => l.filter((x) => x.id !== selection.id))\n setSelection(null)\n }, [selection, setFields, setElements])\n const duplicateSelected = useCallback(() => {\n if (selectedField) {\n const copy = duplicate(selectedField, fields)\n setFields((l) => [...l, copy])\n setSelection({ kind: 'field', id: copy.id })\n } else if (selectedElement) {\n const copy = duplicateElement(selectedElement)\n setElements((l) => [...l, copy])\n setSelection({ kind: 'element', id: copy.id })\n }\n }, [selectedField, selectedElement, fields, setFields, setElements])\n /** Moves the selection by a fraction of the page. */\n const nudge = useCallback(\n (dx: number, dy: number) => {\n if (selectedField) updateField({ ...selectedField, position: moveBy(selectedField.position, dx, dy) })\n if (selectedElement) {\n const { page: _p, ...position } = moveBy({ page: selectedElement.page, ...selectedElement.position }, dx, dy)\n void _p\n updateElement({ ...selectedElement, position })\n }\n },\n [selectedField, selectedElement, updateField, updateElement],\n )\n\n // Keyboard shortcuts (not while typing in an input).\n useEffect(() => {\n if (!keyboard) return\n const onKey = (e: KeyboardEvent) => {\n const t = e.target as HTMLElement\n if (preview || t.closest('input, textarea, select, [contenteditable]')) return\n const mod = e.ctrlKey || e.metaKey\n if (mod && e.key.toLowerCase() === 'z') {\n e.preventDefault()\n if (e.shiftKey) history.redo()\n else history.undo()\n } else if (mod && e.key.toLowerCase() === 'y') {\n e.preventDefault()\n history.redo()\n } else if (e.key === 'Escape' && armed) setArmed(null)\n else if (!selection) return\n else if (e.key === 'Delete' || e.key === 'Backspace') removeSelected()\n else if (mod && e.key.toLowerCase() === 'd') {\n e.preventDefault()\n duplicateSelected()\n } else if (e.key === 'Escape') setSelection(null)\n else if (e.key.startsWith('Arrow')) {\n e.preventDefault()\n const step = e.shiftKey ? 0.01 : 0.002\n nudge(\n e.key === 'ArrowLeft' ? -step : e.key === 'ArrowRight' ? step : 0,\n e.key === 'ArrowUp' ? -step : e.key === 'ArrowDown' ? step : 0,\n )\n }\n }\n window.addEventListener('keydown', onKey)\n return () => window.removeEventListener('keydown', onKey)\n }, [keyboard, preview, armed, selection, history, removeSelected, duplicateSelected, nudge])\n\n // Warn before leaving with unsaved changes.\n useEffect(() => {\n if (!dirty || !warnUnsaved) return\n const warn = (e: BeforeUnloadEvent) => e.preventDefault()\n window.addEventListener('beforeunload', warn)\n return () => window.removeEventListener('beforeunload', warn)\n }, [dirty, warnUnsaved])\n\n /** onPointerDown of a box (or its resize handle) inside an element with `data-page`. */\n function startDrag(e: ReactPointerEvent, sel: NonNullable<Selection>, start: Position, mode: 'move' | 'resize') {\n if (e.button !== 0) return\n e.stopPropagation()\n const pageEl = (e.currentTarget as HTMLElement).closest('[data-page]')\n if (!pageEl) return\n setSelection(sel)\n history.checkpoint()\n drag.current = { sel, mode, x: e.clientX, y: e.clientY, start, rect: pageEl.getBoundingClientRect() }\n ;(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId)\n }\n /** onPointerMove of the same box. */\n function onDrag(e: ReactPointerEvent) {\n const d = drag.current\n if (!d) return\n const dx = (e.clientX - d.x) / d.rect.width\n const dy = (e.clientY - d.y) / d.rect.height\n if (d.sel.kind === 'field') {\n const position = d.mode === 'move' ? moveBy(d.start, dx, dy, grid) : resizeBy(d.start, dx, dy, grid)\n setFields((l) => l.map((x) => (x.id === d.sel.id ? { ...x, position } : x)), false)\n return\n }\n setElements(\n (l) =>\n l.map((x) => {\n if (x.id !== d.sel.id) return x\n if (d.mode === 'move') {\n const { page: _p, ...position } = moveBy(d.start, dx, dy, grid)\n void _p\n return { ...x, position }\n }\n if (x.type === 'line') {\n // Lines may be flat: a near-zero side snaps to 0 so horizontal / vertical lines stay straight.\n const flat = (n: number) => (n < 0.004 ? 0 : n)\n const width = flat(Math.min(1 - d.start.x, Math.max(0, snap(d.start.width + dx, grid))))\n const height = flat(Math.min(1 - d.start.y, Math.max(0, snap(d.start.height + dy, grid))))\n return { ...x, position: { ...x.position, width, height } }\n }\n const { page: _p, ...position } = resizeBy(d.start, dx, dy, grid)\n void _p\n return { ...x, position }\n }),\n false,\n )\n }\n /** onPointerUp of the same box. */\n const endDrag = () => {\n drag.current = null\n }\n\n /** Adds the tool `key` ('field:text', 'element:box') at a point on the page `rect`. true = placed. */\n function placeNew(key: string, page: number, clientX: number, clientY: number, rect: DOMRect) {\n const [kind, what] = key.split(':')\n const { x, y } = toPage(clientX, clientY, rect)\n if (kind === 'field' && fieldTypes.includes(what as FieldType)) {\n const f = newField(what as FieldType, page, x, y, fields)\n setFields((l) => [...l, f])\n setSelection({ kind: 'field', id: f.id })\n return true\n }\n if (kind === 'element' && elementTools.includes(what as ElementTool)) {\n const el = newElement(what as ElementTool, page, x, y)\n setElements((l) => [...l, el])\n setSelection({ kind: 'element', id: el.id })\n return true\n }\n return false\n }\n /** Covers a line of the original text with an editable copy (Edit text). */\n function editLine(line: TextLine, page: number) {\n const el = replaceLine(line, page)\n setElements((l) => [...l, el])\n setSelection({ kind: 'element', id: el.id })\n }\n\n // Suggested fields for the whole document (source 'ai'); one undo step removes them all.\n const onDetectedRef = useRef(onDetected)\n useEffect(() => {\n onDetectedRef.current = onDetected\n })\n const detect = useCallback(\n async (auto = false) => {\n if (!pdf || !layout || detecting) return\n const stop = new AbortController()\n setDetecting({ done: 0, total: layout.pages.length, stop })\n try {\n const lib = await loadPdfJs()\n const found = await detectDocument(\n pdf,\n lib.OPS as unknown as Ops,\n layout.pages,\n fields,\n (done, total) => setDetecting((d) => d && { ...d, done, total }),\n stop.signal,\n )\n if (found.length) {\n setFields((l) => [...l, ...found])\n onDetectedRef.current?.(found)\n }\n notify(\n found.length\n ? `${found.length} fields suggested${stop.signal.aborted ? ' (stopped early)' : ''}. Check the dashed ones, then Save.`\n : auto\n ? 'No fields found automatically. Drag them in from the left.'\n : 'No new fields found',\n )\n } catch (err) {\n notifyError(err)\n } finally {\n setDetecting(null)\n }\n },\n [pdf, layout, detecting, fields, setFields, notify, notifyError],\n )\n\n // A new form (nothing placed yet) gets suggestions right away.\n useEffect(() => {\n if (autoDetected.current || !pdf || !layout) return\n const fresh = autoDetect ?? (!initialValue.fields.length && !initialValue.layout)\n if (!fresh) {\n autoDetected.current = true\n return\n }\n // Started after this render (an effect must not set state synchronously).\n const t = setTimeout(() => {\n autoDetected.current = true\n void detect(true)\n })\n return () => clearTimeout(t)\n }, [pdf, layout, detect, autoDetect, initialValue])\n\n /** Validates and calls onSave. false = invalid or onSave failed. */\n async function save() {\n if (problems.length) {\n notifyError(problems[0])\n return false\n }\n setBusy(true)\n try {\n // An untouched layout is saved as null: \"the original pages as they are\".\n const layoutToSave = doc.layout && sizes && isDefault(doc.layout, sizes) ? null : doc.layout\n const sent = doc\n await onSave({ fields, layout: layoutToSave })\n setSaved(sent)\n notify('Saved')\n return true\n } catch (err) {\n notifyError(err)\n return false\n } finally {\n setBusy(false)\n }\n }\n\n // Fields and layout items per page.\n const counts: Record<number, number> = {}\n for (const f of fields) counts[f.position.page] = (counts[f.position.page] ?? 0) + 1\n for (const el of layout?.elements ?? []) counts[el.page] = (counts[el.page] ?? 0) + 1\n\n return {\n doc,\n fields,\n /** The layout as shown (the original pages until changed); null until the PDF has loaded. */\n layout,\n pageCount,\n /** Page size in PDF points, for drawing layout text at the right size. */\n pageHeight: (page: number) => layout?.pages[page - 1]?.height ?? 842,\n /** Fields + layout items per page. */\n counts,\n dirty,\n /** Validation messages; save() refuses while there are any. */\n problems,\n busy,\n save,\n undo: history.undo,\n redo: history.redo,\n canUndo: history.canUndo,\n canRedo: history.canRedo,\n /** Changes the document; `record: false` = no undo step (live drags). */\n setFields,\n setElements,\n updateField,\n updateElement,\n reorderField: (id: string, step: -1 | 1) => setFields((l) => reorder(l, id, step)),\n movePage: (n: number, step: -1 | 1) => withDoc((d) => movePage(d, n, step)),\n insertBlankPage: (n: number) => withDoc((d) => insertBlankPage(d, n)),\n deletePage: (n: number) => {\n setSelection(null)\n withDoc((d) => deletePage(d, n))\n },\n selection,\n setSelection,\n selectedField,\n selectedElement,\n removeSelected,\n duplicateSelected,\n nudge,\n startDrag,\n onDrag,\n endDrag,\n /** Tools for a palette: drag with `dataTransfer` / tap to arm, then placeNew. */\n fieldTools: fieldTypes.map((t) => `field:${t}`),\n elementTools: elementTools.map((t) => `element:${t}`),\n armed,\n setArmed,\n placeNew,\n editText,\n setEditText,\n editLine,\n snapOn,\n setSnapOn,\n preview,\n /** Preview starts with empty answers; leaving it ends Edit text. */\n setPreview: (on: boolean) => {\n setPreviewState(on)\n setPreviewAnswers({})\n setEditText(false)\n },\n previewAnswers,\n setPreviewAnswers,\n previewStates: fieldStates(fields, previewAnswers),\n pdf,\n /** Pass to PdfPages' onLoaded. */\n onPdfLoaded: (s: PageSize[], d: PDFDocumentProxy) => {\n setSizes(s)\n setPdf(d)\n },\n pageSizes: sizes,\n detect,\n /** Progress while detecting; `stop.abort()` keeps what was found so far. */\n detecting,\n }\n}\n\nexport type FormEditorState = ReturnType<typeof useFormEditor>\n","import { useCallback, useState } from 'react'\n\nconst LIMIT = 100\n\n/**\n * Undo / redo over one value. `set(next, false)` changes it without a history step (during a drag; call\n * `checkpoint()` when the drag starts so the whole drag undoes in one step).\n */\nexport function useUndo<T>(initial: T) {\n const [state, setState] = useState({ past: [] as T[], present: initial, future: [] as T[] })\n const set = useCallback((next: T | ((v: T) => T), record = true) => {\n setState((s) => {\n const value = typeof next === 'function' ? (next as (v: T) => T)(s.present) : next\n if (value === s.present) return s\n return record\n ? { past: [...s.past, s.present].slice(-LIMIT), present: value, future: [] }\n : { ...s, present: value }\n })\n }, [])\n const checkpoint = useCallback(\n () => setState((s) => ({ past: [...s.past, s.present].slice(-LIMIT), present: s.present, future: [] })),\n [],\n )\n const undo = useCallback(\n () =>\n setState((s) =>\n s.past.length\n ? { past: s.past.slice(0, -1), present: s.past[s.past.length - 1], future: [s.present, ...s.future] }\n : s,\n ),\n [],\n )\n const redo = useCallback(\n () =>\n setState((s) =>\n s.future.length ? { past: [...s.past, s.present], present: s.future[0], future: s.future.slice(1) } : s,\n ),\n [],\n )\n const reset = useCallback((v: T) => setState({ past: [], present: v, future: [] }), [])\n return {\n value: state.present,\n set,\n checkpoint,\n undo,\n redo,\n reset,\n canUndo: state.past.length > 0,\n canRedo: state.future.length > 0,\n }\n}\n","import { ArrowLeft, Download, Paperclip, Save, Send } from 'lucide-react'\nimport { useEffect, useState, type ReactNode } from 'react'\nimport { boxStyle } from '../core/coords'\nimport type { FieldDef, Layout } from '../core/form-logic'\nimport { AnswerControl } from './components/FieldControl'\nimport LayoutElementView from './components/LayoutElementView'\nimport PdfPages, { type PdfSource } from './components/PdfPages'\nimport QuestionList from './components/QuestionList'\nimport ZoomControls from './components/ZoomControls'\nimport { useCompact, useFitZoom } from './hooks/useFitZoom'\nimport { useFormFiller, type AnswerFileInfo, type UseFormFillerOptions } from './hooks/useFormFiller'\nimport type { QuestionFilterState, UseQuestionFilterOptions } from './hooks/useQuestionFilter'\nimport { Button, ConfirmModal, FormliftProvider, SegmentedTabs, type FormliftComponents } from './ui/slots'\n\nexport type { AnswerFileInfo }\n\n/** Parts of the FormFiller screen that can be hidden with `hide`. */\nexport type FillerPart =\n | 'back'\n | 'title'\n | 'views'\n | 'zoom'\n | 'saveStatus'\n | 'saveDraft'\n | 'submit'\n | 'attachments'\n | 'errors'\n | 'requiredMark'\n\nexport interface FormFillerProps extends UseFormFillerOptions {\n /** The PDF: URL, Blob / File, bytes, or a function returning one. */\n pdf: PdfSource\n title?: ReactNode\n onBack?: () => void\n /** Your own items at the end of the toolbar (e.g. \"Download completed PDF\"). */\n toolbar?: ReactNode\n /** Replace any UI primitive (Button, Modal, …), the answer control (FieldControl) or one per type (`fields`). */\n components?: FormliftComponents\n /** Parts of the screen to leave out. */\n hide?: FillerPart[]\n /** The views offered, the first one shown first. Default ['document', 'list']. */\n views?: ('document' | 'list')[]\n /** The list view's filters, in order: filter objects or ids of defaultFilters. Default: defaultFilters. */\n listFilters?: UseQuestionFilterOptions['filters']\n /** Search box over the list. Default true. */\n listSearch?: boolean\n /** Leave out filters this form has nothing for (e.g. Category on a form without category codes). Default true. */\n hideUnavailableFilters?: boolean\n /** Your own search / filter bar over the list instead of the built-in one; `fs` has every filter as data. */\n renderFilters?: (fs: QuestionFilterState) => ReactNode\n /** Ask \"Submit this form?\" first. Default true. */\n confirmSubmit?: boolean\n /** Extra classes on the root. It fills its container's height (`h-full`) by default: give the container a height. */\n className?: string\n}\n\nconst humanSize = (n?: number) => {\n if (!n) return ''\n const units = ['B', 'KB', 'MB', 'GB']\n let i = 0\n while (n >= 1024 && i < units.length - 1) {\n n /= 1024\n i++\n }\n return `${n.toFixed(i ? 1 : 0)} ${units[i]}`\n}\n\n/**\n * The PDF with the form's fields on it. The user fills them in place (or as a searchable list); answers autosave\n * through onSave, Submit checks them with the same rules as validateAnswers on your server.\n * All the logic lives in useFormFiller: use that hook directly for a completely custom UI.\n */\nexport default function FormFiller({\n pdf: pdfSource,\n title,\n onBack,\n toolbar,\n components,\n hide = [],\n views = ['document', 'list'],\n listFilters,\n listSearch,\n hideUnavailableFilters,\n renderFilters,\n confirmSubmit: askFirst = true,\n className = 'h-full',\n ...options\n}: FormFillerProps) {\n const shows = (p: FillerPart) => !hide.includes(p)\n const ff = useFormFiller(options)\n const { fields, readOnly = false, onSave } = options\n const layout: Layout | null = options.layout ?? null\n const { answers, states, errors } = ff\n const pdfWidth = ff.pdfSizes.length ? Math.max(...ff.pdfSizes.map((p) => p.width)) : 595\n const { zoom, setZoom, ref: fitRef } = useFitZoom(Math.max(...(layout?.pages.map((p) => p.width) ?? [pdfWidth])))\n const compact = useCompact()\n const [confirmSubmit, setConfirmSubmit] = useState(false)\n const [loadError, setLoadError] = useState('')\n // Document = the PDF with the controls on it; List = the same answers as questions, with search and filters.\n const [view, setView] = useState<'document' | 'list'>(views[0] ?? 'document')\n const { loadPageText } = ff\n useEffect(() => {\n if (view === 'list') loadPageText()\n }, [view, loadPageText])\n\n if (loadError) return <p className=\"fl-root p-6 text-center text-sm text-danger\">{loadError}</p>\n\n // One field's answer control (the same on the document and in the list), with the required star.\n const control = (f: FieldDef) => (\n <>\n <AnswerControl\n components={components}\n field={f}\n value={answers[f.key]}\n readOnly={readOnly || states[f.key].readOnly || (f.type === 'file' && !ff.canUpload)}\n compact={compact}\n error={errors[f.key]}\n onChange={(v) => ff.change(f, v)}\n fileName={ff.fileOf(answers[f.key])?.fileName}\n signatureSrc={ff.signatureSrc(f)}\n onFile={ff.canUpload ? (file) => void ff.attach(f, file) : undefined}\n onOpenFile={() => ff.openFile(String(answers[f.key]))}\n />\n {states[f.key].required && !readOnly && shows('requiredMark') && (\n <span className=\"pointer-events-none absolute -top-1.5 -right-1.5 text-[12px] font-bold text-danger\">*</span>\n )}\n </>\n )\n\n return (\n <FormliftProvider components={components}>\n <div className={`fl-root flex min-h-[min(600px,100dvh)] flex-col bg-white text-ink ${className}`}>\n <div className=\"flex flex-wrap items-center gap-3 border-b border-line bg-white px-4 py-2.5\">\n {onBack && shows('back') && (\n <Button variant=\"secondary\" size=\"sm\" icon={ArrowLeft} onClick={onBack}>\n Back\n </Button>\n )}\n {title != null && shows('title') && (\n <h2 className=\"m-0 min-w-0 flex-1 truncate text-[17px] font-semibold max-sm:text-[15px] sm:flex-none\">\n {title}\n </h2>\n )}\n {views.length > 1 && shows('views') && (\n <SegmentedTabs\n options={views.map((v) => ({ value: v, label: v === 'document' ? 'Document' : 'List' }))}\n value={view}\n onChange={setView}\n />\n )}\n <div className=\"ml-auto flex flex-wrap items-center gap-2 max-sm:w-full max-sm:justify-end\">\n {view === 'document' && shows('zoom') && <ZoomControls zoom={zoom} onChange={setZoom} />}\n {!readOnly && onSave && (\n <>\n {shows('saveStatus') && (\n <span className=\"text-right text-[12px] text-muted sm:w-24\" aria-live=\"polite\">\n {\n {\n saved: ff.hasSaved ? 'Draft saved' : '',\n dirty: 'Unsaved',\n saving: 'Saving…',\n error: 'Not saved',\n }[ff.saveState]\n }\n </span>\n )}\n {shows('saveDraft') && (\n <Button variant=\"secondary\" size=\"sm\" icon={Save} title=\"Save draft\" onClick={() => void ff.save()}>\n <span className=\"max-sm:hidden\">Save draft</span>\n </Button>\n )}\n {shows('submit') && (\n <Button size=\"sm\" icon={Send} onClick={() => (askFirst ? setConfirmSubmit(true) : void ff.submit())}>\n Submit\n </Button>\n )}\n </>\n )}\n {toolbar}\n </div>\n </div>\n {ff.attachments.length > 0 && shows('attachments') && (\n <div className=\"flex flex-wrap items-center gap-x-3 gap-y-1 border-b border-line bg-white px-4 py-1.5 text-[12.5px]\">\n <span className=\"inline-flex items-center gap-1 font-semibold text-ink-2\">\n <Paperclip size={13} /> Attachments\n </span>\n {ff.attachments.map((x) => (\n <button\n key={x.id}\n type=\"button\"\n title={`Download ${x.fileName}`}\n disabled={!ff.canLoadFiles}\n className=\"inline-flex max-w-full cursor-pointer items-center gap-1 text-primary hover:underline disabled:cursor-default\"\n onClick={() => ff.downloadFile(x.id, x.fileName)}\n >\n <span className=\"truncate\">{x.fileName}</span>\n {x.fileSize ? <span className=\"text-muted\">({humanSize(x.fileSize)})</span> : null}\n {ff.canLoadFiles && <Download size={12} />}\n </button>\n ))}\n </div>\n )}\n {ff.errorList.length > 0 && shows('errors') && (\n <ul className=\"m-0 border-b border-danger/30 bg-danger/5 px-6 py-2 text-[13px] text-danger\">\n {ff.errorList.map((f) => (\n <li key={f.key}>\n Page {f.position.page}: {errors[f.key]}\n </li>\n ))}\n </ul>\n )}\n {view === 'list' && (\n <div className=\"flex-1 overflow-auto bg-hover\">\n <QuestionList\n questions={ff.questions}\n answers={answers}\n control={control}\n filters={listFilters}\n search={listSearch}\n hideUnavailable={hideUnavailableFilters}\n renderToolbar={renderFilters}\n />\n </div>\n )}\n {/* Kept while the list shows, so the pages need not be drawn again. */}\n <div ref={fitRef} className={`flex-1 overflow-auto bg-hover ${view === 'list' ? 'hidden' : ''}`}>\n <PdfPages\n load={pdfSource}\n zoom={zoom}\n pages={layout?.pages}\n onLoaded={ff.onPdfLoaded}\n onError={() => setLoadError('The document could not be shown')}\n overlay={(page) => [\n // The admin's text / boxes / lines first, the answer controls on top.\n ...(layout?.elements ?? [])\n .filter((el) => el.page === page)\n .map((el) => (\n <LayoutElementView key={el.id} element={el} pageHeight={layout!.pages[page - 1]?.height ?? 842} />\n )),\n ...fields\n .filter((f) => f.position.page === page && states[f.key]?.visible)\n .map((f) => (\n <div key={f.id} className=\"absolute [container-type:size]\" style={boxStyle(f.position)}>\n {control(f)}\n </div>\n )),\n ]}\n />\n </div>\n {confirmSubmit && (\n <ConfirmModal\n confirmLabel=\"Submit\"\n onConfirm={() => {\n setConfirmSubmit(false)\n void ff.submit()\n }}\n onCancel={() => setConfirmSubmit(false)}\n >\n Submit this form? You cannot change the answers afterwards.\n </ConfirmModal>\n )}\n </div>\n </FormliftProvider>\n )\n}\n","import { Search, SlidersHorizontal } from 'lucide-react'\nimport { Fragment, useEffect, useState, type CSSProperties, type ReactNode } from 'react'\nimport { useQuestionFilter, type QuestionFilterState, type UseQuestionFilterOptions } from '../hooks/useQuestionFilter'\nimport { Button, Chip, ChipGroup, RemovableChip, EmptyText, Popover, Tag } from '../ui/slots'\nimport type { Answers, FieldDef, FieldType } from '../../core/form-logic'\nimport type { Question } from '../../core/questions'\n\nconst PAGE = 100\n\n// Box for a control in the list (controls size their text to the box, as on the page).\nconst SIZE: Partial<Record<FieldType, string>> = {\n checkbox: 'h-7 w-7 lg:h-8 lg:w-8',\n textarea: 'h-20 w-full',\n signature: 'h-16 w-56',\n file: 'h-9 w-full sm:w-72',\n radio: 'h-9 w-full',\n}\n// Parts of an option grid: its rows (the rest of the parts come after it).\nconst gridSize = (q: Question) => q.rows?.reduce((a, b) => a + b, 0) ?? 0\nconst wide = (t: FieldType) => t === 'textarea' || t === 'radio' || t === 'file'\n\n/**\n * The form as questions with search and filters: only the questions that match are shown. Answers are the\n * same as on the document; `control(field)` draws a field's answer control. Filters come from useQuestionFilter:\n * `filters` / `search` choose them, `filter` drives the list from your own useQuestionFilter, `renderToolbar`\n * draws your own bar, `toolbar={false}` none. Filters with nothing to offer in this form are left out unless\n * `hideUnavailable={false}`.\n */\nexport default function QuestionList({\n questions,\n answers,\n control,\n filter: external,\n filters,\n search,\n hideUnavailable = true,\n toolbar = true,\n renderToolbar,\n}: {\n questions: Question[]\n answers: Answers\n control: (f: FieldDef) => ReactNode\n filter?: QuestionFilterState\n filters?: UseQuestionFilterOptions['filters']\n search?: boolean\n hideUnavailable?: boolean\n toolbar?: boolean\n renderToolbar?: (fs: QuestionFilterState) => ReactNode\n}) {\n const own = useQuestionFilter(questions, answers, { filters, search })\n const fs = external ?? own\n const { list } = fs\n const [shown, setShown] = useState(PAGE)\n useEffect(() => setShown(PAGE), [fs.version])\n const groups = fs.groups.filter((g) => g.available || !hideUnavailable)\n const bar = groups.filter((g) => g.section === 'bar')\n const menu = groups.filter((g) => g.section !== 'bar')\n const sections = [...new Set(menu.map((g) => g.section ?? ''))]\n\n return (\n <div className=\"w-full p-3 sm:p-4 lg:px-6\">\n {renderToolbar\n ? renderToolbar(fs)\n : toolbar && (\n <div className=\"sticky top-0 z-10 -mx-3 mb-3 flex flex-col gap-2 border-b border-line bg-white/95 px-3 py-2.5 shadow-sm backdrop-blur sm:-mx-4 sm:px-4 lg:-mx-6 lg:px-6\">\n <div className=\"flex flex-wrap items-center gap-2\">\n {fs.search != null && (\n <label className=\"relative min-w-0 flex-1 basis-full sm:basis-72\">\n <Search size={15} className=\"absolute top-1/2 left-3 -translate-y-1/2 text-muted\" />\n <input\n type=\"search\"\n aria-label=\"Search questions\"\n placeholder=\"Search questions, sections, answers…\"\n value={fs.search}\n onChange={(e) => fs.setSearch(e.target.value)}\n className=\"w-full rounded-full border border-line-strong bg-surface py-1.5 pr-3 pl-9 text-[13px] outline-none focus:border-primary focus:bg-white max-sm:text-base\"\n />\n </label>\n )}\n {menu.length > 0 && (\n <Popover label=\"Filters\" icon={SlidersHorizontal} badge={fs.inUse.length} align=\"right\">\n <div className=\"flex flex-col gap-4\">\n {sections.map((sec) => (\n <FilterSection key={sec} title={sec}>\n {menu\n .filter((g) => (g.section ?? '') === sec)\n .map((g) => (\n <ChipGroup\n key={g.id}\n title={g.title}\n value={g.value}\n onChange={g.set}\n options={g.options}\n />\n ))}\n </FilterSection>\n ))}\n </div>\n </Popover>\n )}\n <span className=\"text-[12.5px] text-muted sm:ml-auto\" aria-live=\"polite\">\n <b className=\"font-semibold text-ink-2\">{list.length}</b> of {fs.total} questions\n </span>\n {fs.active && (\n <Button variant=\"secondary\" size=\"sm\" onClick={fs.clear}>\n Clear all\n </Button>\n )}\n </div>\n {bar.map((g) => (\n <div key={g.id} role=\"group\" aria-label={g.title} className=\"flex flex-wrap items-center gap-1.5\">\n <span className=\"mr-1 text-[11.5px] font-semibold tracking-wide text-muted uppercase\">{g.title}</span>\n <Chip label=\"All\" count={fs.total} active={g.value == null} onClick={() => g.set(null)} />\n {g.options.map((o) => (\n <Chip\n key={o.value}\n label={o.label}\n count={o.count}\n active={g.value === o.value}\n onClick={() => g.set(g.value === o.value ? null : o.value)}\n />\n ))}\n </div>\n ))}\n {fs.inUse.length > 0 && (\n <div className=\"flex flex-wrap items-center gap-1.5\">\n {fs.inUse.map((f) => (\n <RemovableChip key={f.id} label={f.label} onRemove={f.clear} />\n ))}\n </div>\n )}\n </div>\n )}\n\n {list.length === 0 && <EmptyText>No question matches these filters.</EmptyText>}\n <ul className=\"m-0 flex list-none flex-col gap-2 p-0\">\n {list.slice(0, shown).map((q, i, all) => (\n <Fragment key={q.id}>\n {/* The numbered question these answers belong to, once over its run of answers. */}\n {q.section && q.section !== all[i - 1]?.section && (\n <li className=\"mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 border-b border-line pb-1\">\n <span className=\"text-[13.5px] font-semibold text-primary lg:text-[15px]\">{q.section}</span>\n {Object.entries(q.tags ?? {}).map(([k, v]) => (\n <Tag key={k} tone=\"muted\">\n {k}: {v}\n </Tag>\n ))}\n </li>\n )}\n {/* What a run of rows on one scale asks, once over them. */}\n {q.intro && (q.intro !== all[i - 1]?.intro || q.section !== all[i - 1]?.section) && (\n <li className=\"rounded-md border-l-4 border-primary bg-primary-soft px-3 py-2 text-[13px] font-medium text-ink-2 lg:text-[14px]\">\n {q.intro}\n </li>\n )}\n <li className=\"rounded-lg border border-line bg-white p-3\">\n <div className=\"mb-2 flex items-start gap-2\">\n <p className=\"m-0 min-w-0 flex-1 text-[13.5px] font-semibold whitespace-pre-line text-ink-2 lg:text-[15px]\">\n {q.title}\n </p>\n {q.category && <Tag tone=\"muted\">{q.category}</Tag>}\n <Tag tone=\"muted\">p.{q.page}</Tag>\n </div>\n {q.columns ? (\n <TableGrid q={q} control={control} />\n ) : (\n <>\n {q.rows && <OptionGrid rows={q.rows} parts={q.parts.slice(0, gridSize(q))} control={control} />}\n {/* Parts after an option grid (the \"Finding\" box under its options) are laid out on their own. */}\n {q.parts.length > gridSize(q) && (\n <div className={`flex flex-wrap items-end gap-x-4 gap-y-2 ${q.rows ? 'mt-3' : ''}`}>\n {q.parts.slice(gridSize(q)).map(({ field, name }) => (\n <div\n key={field.id}\n className={`flex min-w-0 flex-col gap-1 ${wide(field.type) ? 'basis-full' : ''} ${\n field.type === 'checkbox'\n ? 'flex-row-reverse items-center justify-end gap-2'\n : 'max-sm:basis-full'\n }`}\n >\n {name && <span className=\"text-[12px] text-muted lg:text-[13.5px]\">{name}</span>}\n <div\n className={`relative [container-type:size] ${SIZE[field.type] ?? 'h-9 w-full sm:w-80 lg:h-10 lg:w-96'}`}\n >\n {control(field)}\n </div>\n </div>\n ))}\n </div>\n )}\n </>\n )}\n </li>\n </Fragment>\n ))}\n </ul>\n {list.length > shown && (\n <div className=\"mt-3 text-center\">\n <Button variant=\"secondary\" size=\"sm\" onClick={() => setShown((n) => n + PAGE)}>\n Show more ({list.length - shown} left)\n </Button>\n </div>\n )}\n </div>\n )\n}\n\n/**\n * Checkboxes of one grid as on the document: each line of the form is a row, a box left of its label; columns\n * as many as the widest line (2 on phones).\n */\nfunction OptionGrid({\n rows,\n parts,\n control,\n}: {\n rows: number[]\n parts: Question['parts']\n control: (f: FieldDef) => ReactNode\n}) {\n const starts = new Set(rows.map((_, i) => rows.slice(0, i).reduce((a, b) => a + b, 0)))\n return (\n <div\n className=\"grid grid-cols-2 gap-x-4 gap-y-2 sm:grid-cols-[repeat(var(--cols),minmax(0,1fr))]\"\n style={{ '--cols': Math.max(...rows) } as CSSProperties}\n >\n {parts.map(({ field, name }, i) => (\n <div\n key={field.id}\n className={`flex min-w-0 items-center gap-2 text-[13px] text-ink-2 lg:text-[14.5px] ${starts.has(i) ? 'sm:col-start-1' : ''}`}\n >\n <div className=\"relative h-6 w-6 shrink-0 [container-type:size] lg:h-7 lg:w-7\">{control(field)}</div>\n {/* The control is its own <label>: a click on the name ticks the box the same way. */}\n <span\n className=\"min-w-0 cursor-pointer break-words\"\n onClick={(e) => e.currentTarget.parentElement?.querySelector('input')?.click()}\n >\n {name}\n </span>\n </div>\n ))}\n </div>\n )\n}\n\n/** A table as on the document: its column names over the rows, each row's name on its left when it has one. */\nfunction TableGrid({ q, control }: { q: Question; control: (f: FieldDef) => ReactNode }) {\n const rows = q.rows!.map((n, i) => q.parts.slice(q.rows!.slice(0, i).reduce((a, b) => a + b, 0)).slice(0, n))\n return (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full border-separate border-spacing-x-2 border-spacing-y-1.5 text-left\">\n <thead className={q.columns!.some(Boolean) ? '' : 'sr-only'}>\n <tr>\n {q.rowNames && <th />}\n {q.columns!.map((c, i) => (\n <th key={i} scope=\"col\" className=\"min-w-32 px-0 text-[12px] font-medium text-muted lg:text-[13.5px]\">\n {c}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {rows.map((row, r) => (\n <tr key={row[0].field.id}>\n {q.rowNames && (\n <th\n scope=\"row\"\n className=\"w-px pr-2 text-[12.5px] font-semibold whitespace-nowrap text-ink-2 lg:text-[14px]\"\n >\n {q.rowNames[r]}\n </th>\n )}\n {/* Controls are size containers (no width of their own): each cell gets a minimum width. */}\n {row.map(({ field }) => (\n <td key={field.id} className=\"p-0 align-middle\">\n <div\n className={`relative [container-type:size] ${field.type === 'checkbox' ? 'h-7 w-7' : `${field.type === 'textarea' ? 'h-16' : 'h-9 lg:h-10'} w-full min-w-32`}`}\n >\n {control(field)}\n </div>\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )\n}\n\n/** A titled block of filters in the Filters panel. */\nfunction FilterSection({ title, action, children }: { title: string; action?: ReactNode; children: ReactNode }) {\n return (\n <section className=\"flex flex-col gap-2 border-b border-line-soft pb-3 last:border-0 last:pb-0\">\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <h4 className=\"m-0 text-[13px] font-semibold text-ink\">{title}</h4>\n {action}\n </div>\n {children}\n </section>\n )\n}\n","import { useMemo, useState } from 'react'\nimport {\n builtinFilters,\n filterQuestions,\n resolveFilters,\n type FilterOption,\n type FilterSource,\n type FilterValues,\n} from '../../core/filters'\nimport type { Answers } from '../../core/form-logic'\nimport type { Question } from '../../core/questions'\nimport { TYPE_LABEL } from '../constants'\n\n/** The built-in filters (labels from formlift's UI). Pick from it by id, reorder it, add your own. */\nexport const defaultFilters: FilterSource[] = builtinFilters({ type: (t) => TYPE_LABEL[t] })\n\n/** One filter, ready to draw: its options for this form, the picked value, and set(). */\nexport interface FilterGroup {\n id: string\n title: string\n section?: string\n options: FilterOption[]\n /** false = this form has nothing to filter on here (no options). Show it or not: your call. */\n available: boolean\n value: string | null\n /** Picks a value; null clears it. */\n set: (value: string | null) => void\n}\n\nexport interface UseQuestionFilterOptions {\n /**\n * The filters, in order: filter objects / sources (see `formlift` core: categoryFilter, partFilter,\n * customFilter…) or ids of defaultFilters ('answered', 'remarks', 'comments', 'choices', 'picked', 'tags',\n * 'type', 'category'). Default: defaultFilters.\n */\n filters?: (FilterSource | string)[]\n /** Text search. Default true. */\n search?: boolean\n /** Starting state (e.g. restored from the URL). */\n initial?: { search?: string; values?: FilterValues }\n /** Your own rule, applied after the filters. */\n predicate?: (q: Question, answers: Answers) => boolean\n /** Every change (e.g. to keep it in the URL). */\n onChange?: (state: { search: string; values: FilterValues }) => void\n}\n\nconst byId = (id: string): FilterSource | undefined => {\n if (id === 'tags') return defaultFilters.find((s) => typeof s === 'function')\n return defaultFilters.find((s) => typeof s !== 'function' && s.id === id)\n}\n\n/**\n * Search and filters over questions, as data. formlift decides nothing about what to show: every filter you\n * pass comes back as a group with `available` (does this form have anything to filter on?); you draw the ones\n * you want, the way you want. QuestionList uses the same hook.\n * While the filter stays the same, answers typed meanwhile do not make questions vanish mid-word.\n */\nexport function useQuestionFilter(\n questions: Question[],\n answers: Answers,\n { filters = defaultFilters, search: searchOn = true, initial, predicate, onChange }: UseQuestionFilterOptions = {},\n) {\n const [values, setValues] = useState<FilterValues>(initial?.values ?? {})\n const [search, setSearchState] = useState(initial?.search ?? '')\n // The answers the filter last looked at; the list follows new answers when the filter changes.\n const [seen, setSeen] = useState(answers)\n const [version, setVersion] = useState(0)\n const update = (nextSearch: string, nextValues: FilterValues) => {\n setSearchState(nextSearch)\n setValues(nextValues)\n setSeen(answers)\n setVersion((v) => v + 1)\n onChange?.({ search: nextSearch, values: nextValues })\n }\n\n const key = filters.map((f) => (typeof f === 'string' ? f : typeof f === 'function' ? 'fn' : f.id)).join()\n const defs = useMemo(\n () =>\n resolveFilters(\n filters.map((f) => (typeof f === 'string' ? byId(f) : f)).filter((f): f is FilterSource => !!f),\n questions,\n ),\n // eslint-disable-next-line react-hooks/exhaustive-deps -- re-resolve when the list's ids change, not on every render\n [key, questions],\n )\n\n const list = useMemo(() => {\n const shown = filterQuestions(questions, seen, defs, values, searchOn ? search : '')\n return predicate ? shown.filter((q) => predicate(q, seen)) : shown\n }, [questions, seen, defs, values, search, searchOn, predicate])\n\n const groups: FilterGroup[] = defs.map((d) => {\n const options = d.options(questions, answers)\n return {\n id: d.id,\n title: d.title,\n section: d.section,\n options,\n available: options.length > 0,\n value: values[d.id] ?? null,\n set: (v) => update(search, { ...values, [d.id]: v }),\n }\n })\n const picked = groups.filter((g) => g.value != null)\n\n return {\n groups,\n /** One group by id (undefined when not in `filters`). */\n group: (id: string) => groups.find((g) => g.id === id),\n /** The search text; null when search is off. */\n search: searchOn ? search : null,\n setSearch: (text: string) => update(text, values),\n values,\n /** Filters in use, as chips to take off one by one. */\n inUse: picked.map((g) => ({\n id: g.id,\n label: `${g.title}: ${g.options.find((o) => o.value === g.value)?.label ?? g.value}`,\n clear: () => g.set(null),\n })),\n active: !!search || picked.length > 0,\n clear: () => update('', {}),\n /** The questions that match, in form order. */\n list,\n total: questions.length,\n /** Bumps on every change (reset paging / scroll on it). */\n version,\n }\n}\n\nexport type QuestionFilterState = ReturnType<typeof useQuestionFilter>\n","import type { PDFDocumentProxy } from 'pdfjs-dist'\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport {\n checkAnswer,\n defaultAnswers,\n fieldStates,\n validateAnswers,\n type Answers,\n type AnswerValue,\n type FieldDef,\n type Layout,\n} from '../../core/form-logic'\nimport { toLines, type PdfTextItem, type TextLine } from '../../core/layout'\nimport { toQuestions as builtinToQuestions, type PageText, type Question } from '../../core/questions'\nimport type { PageSize } from '../components/PdfPages'\n\n/** A file answer (upload or signature PNG) as your storage knows it. The answer's value is its `id`. */\nexport interface AnswerFileInfo {\n id: string\n fieldKey: string\n fileName: string\n /** Bytes, for the size shown next to the name. */\n fileSize?: number\n}\n\nexport type SaveState = 'saved' | 'dirty' | 'saving' | 'error'\n\nexport interface UseFormFillerOptions {\n fields: FieldDef[]\n layout?: Layout | null\n /** A draft to continue. Default: each field's default value. */\n initialAnswers?: Answers\n /** Files already attached to these answers. */\n files?: AnswerFileInfo[]\n /** View only: no saving, no autosave. */\n readOnly?: boolean\n /**\n * Save a draft (`submit: false`, also autosave) or submit (answers already pass validateAnswers — check again on\n * your server). Throw `{ errors: { fieldKey: message } }` to show errors at the fields.\n */\n onSave?: (answers: Answers, info: { submit: boolean }) => unknown\n /** Called after a successful submit. */\n onSubmitted?: (answers: Answers) => void\n /** Autosave delay in ms after the last change; false = off. Default 2500. */\n autosave?: number | false\n /** Store a file / signature PNG; resolve with its id. */\n onUpload?: (field: FieldDef, file: File) => Promise<AnswerFileInfo>\n /** The bytes of a stored file (stored signatures, opening / downloading attachments). */\n loadFile?: (id: string) => Promise<Blob>\n /**\n * Groups the visible fields into questions for the list. Default: formlift's toQuestions. Wrap it to change\n * them, e.g. `(f, t) => toQuestions(f, t, { categoryCode: false }).map(setMyCategory)`.\n */\n toQuestions?: (fields: FieldDef[], textOf: PageText) => Question[]\n notify?: (message: string) => void\n onError?: (error: unknown) => void\n}\n\nfunction download(blob: Blob, name: string) {\n const url = URL.createObjectURL(blob)\n const a = document.createElement('a')\n a.href = url\n a.download = name\n a.click()\n setTimeout(() => URL.revokeObjectURL(url), 1000)\n}\n\n/**\n * Everything FormFiller does, without its UI: answers, conditions, validation, queued saves + autosave, uploads,\n * signatures and the question list. Draw the fields yourself (`boxStyle(field.position)` over each page, or a\n * plain list of `questions`) and call `change`, `save`, `submit`.\n */\nexport function useFormFiller({\n fields,\n layout = null,\n initialAnswers,\n files: initialFiles = [],\n readOnly = false,\n onSave,\n onSubmitted,\n autosave = 2500,\n onUpload,\n loadFile,\n toQuestions = builtinToQuestions,\n notify = () => {},\n onError: notifyError = (e) => console.error(e),\n}: UseFormFillerOptions) {\n const [answers, setAnswers] = useState<Answers>(() => initialAnswers ?? defaultAnswers(fields))\n const [files, setFiles] = useState<AnswerFileInfo[]>(initialFiles)\n // Stored signature images (file id → object URL).\n const [signatures, setSignatures] = useState<Record<string, string>>({})\n const [errors, setErrors] = useState<Record<string, string>>({})\n const [saveState, setSaveState] = useState<SaveState>('saved')\n const [hasSaved, setHasSaved] = useState(!!initialAnswers)\n const [pdf, setPdf] = useState<PDFDocumentProxy | null>(null)\n const [pdfSizes, setPdfSizes] = useState<PageSize[]>([])\n const queue = useRef<Promise<unknown>>(Promise.resolve())\n const latest = useRef<Answers>(answers)\n useEffect(() => {\n latest.current = answers\n }, [answers])\n\n const states = useMemo(() => fieldStates(fields, answers), [fields, answers])\n\n // The PDF's text per original page, read on first need (loadPageText): questions get their own words as titles.\n const [wantText, setWantText] = useState(false)\n const [pageText, setPageText] = useState<Record<number, TextLine[]> | null>(null)\n useEffect(() => {\n if (!wantText || !pdf || pageText) return\n let live = true\n ;(async () => {\n const out: Record<number, TextLine[]> = {}\n for (let i = 1; i <= pdf.numPages; i++) {\n const page = await pdf.getPage(i)\n out[i] = toLines((await page.getTextContent()).items as PdfTextItem[], page.view)\n }\n if (live) setPageText(out)\n })().catch(() => live && setPageText({}))\n return () => {\n live = false\n }\n }, [wantText, pdf, pageText])\n // Rebuilt when fields are shown / hidden by conditions, not on every keystroke.\n const visibleKeys = fields\n .filter((f) => states[f.key]?.visible)\n .map((f) => f.key)\n .join(',')\n const questions = useMemo(() => {\n const keys = new Set(visibleKeys.split(','))\n const textOf = (page: number) => {\n const source = layout ? layout.pages[page - 1]?.source : page\n return source ? pageText?.[source] : undefined\n }\n return toQuestions(\n fields.filter((f) => keys.has(f.key)),\n textOf,\n )\n }, [fields, layout, visibleKeys, pageText, toQuestions])\n\n // Loads the stored signatures shown on the page (once each).\n const loading = useRef(new Set<string>())\n useEffect(() => {\n if (!loadFile) return\n for (const f of fields) {\n const v = answers[f.key]\n if (f.type !== 'signature' || typeof v !== 'string' || !v || v.startsWith('data:')) continue\n if (signatures[v] || loading.current.has(v)) continue\n loading.current.add(v)\n loadFile(v)\n .then((blob) => setSignatures((m) => ({ ...m, [v]: URL.createObjectURL(blob) })))\n .catch(() => loading.current.delete(v))\n }\n }, [fields, answers, signatures, loadFile])\n const urls = useRef(signatures)\n useEffect(() => {\n urls.current = signatures\n }, [signatures])\n useEffect(() => () => Object.values(urls.current).forEach((u) => URL.revokeObjectURL(u)), [])\n\n // Saves run one after another, so a slow save never overwrites a newer one.\n const save = useCallback(\n (submit = false) => {\n if (!onSave) return Promise.resolve(true)\n const run = queue.current.then(async () => {\n setSaveState('saving')\n try {\n await onSave(latest.current, { submit })\n setHasSaved(true)\n setSaveState('saved')\n return true\n } catch (err) {\n setSaveState('error')\n const fieldErrors = (err as { errors?: unknown })?.errors\n if (fieldErrors && typeof fieldErrors === 'object') setErrors(fieldErrors as Record<string, string>)\n notifyError(err)\n return false\n }\n })\n queue.current = run\n return run\n },\n [onSave, notifyError],\n )\n\n useEffect(() => {\n if (readOnly || autosave === false || !onSave || saveState !== 'dirty') return\n const t = setTimeout(() => void save(), autosave)\n return () => clearTimeout(t)\n }, [answers, readOnly, autosave, onSave, saveState, save])\n\n /** Sets one answer (re-checks it if it showed an error). */\n function change(f: FieldDef, v: AnswerValue) {\n setAnswers((a) => ({ ...a, [f.key]: v }))\n setSaveState('dirty')\n if (errors[f.key]) {\n const err = checkAnswer(f, v, states[f.key]?.required)\n setErrors((all) => {\n const rest = { ...all }\n delete rest[f.key]\n return err ? { ...rest, [f.key]: err } : rest\n })\n }\n }\n\n /** Uploads a file / signature PNG through onUpload and makes it the field's answer. */\n async function attach(f: FieldDef, file: File) {\n if (!onUpload) return\n try {\n const info = await onUpload(f, file)\n setFiles((list) => [...list.filter((x) => x.fieldKey !== f.key), info])\n if (f.type === 'signature') setSignatures((m) => ({ ...m, [info.id]: URL.createObjectURL(file) }))\n change(f, info.id)\n } catch (err) {\n notifyError(err)\n }\n }\n\n const fetchFile = (id: string, then: (blob: Blob) => void) =>\n loadFile?.(id).then(then, () => notifyError('Unable to open the file'))\n /** Opens a stored file in a new tab. */\n const openFile = (id: string) =>\n fetchFile(id, (blob) => {\n const url = URL.createObjectURL(blob)\n window.open(url, '_blank')\n setTimeout(() => URL.revokeObjectURL(url), 60000)\n })\n /** Downloads a stored file. */\n const downloadFile = (id: string, name: string) => fetchFile(id, (blob) => download(blob, name))\n\n /** Validates everything; when clean, saves with `submit: true`. true = submitted. */\n async function submit() {\n const found = validateAnswers(fields, answers)\n setErrors(found)\n if (Object.keys(found).length) {\n notifyError('Some answers need attention')\n return false\n }\n if (!(await save(true))) return false\n notify('Submitted')\n onSubmitted?.(latest.current)\n return true\n }\n\n const fileOf = (id: AnswerValue | undefined) => files.find((x) => x.id === id)\n\n return {\n answers,\n /** Visible / required / readOnly per field key, from the conditions and current answers. */\n states,\n /** Field key → message. */\n errors,\n setErrors,\n /** Fields with an error, in form order. */\n errorList: fields.filter((f) => errors[f.key]),\n change,\n save,\n submit,\n saveState,\n hasSaved,\n readOnly,\n files,\n fileOf,\n /** Files of upload fields that are answers now. */\n attachments: files.filter((x) =>\n fields.some((f) => f.type === 'file' && f.key === x.fieldKey && answers[f.key] === x.id),\n ),\n attach,\n canUpload: !!onUpload,\n canLoadFiles: !!loadFile,\n openFile,\n downloadFile,\n /** Image src of a signature answer (stored file or data URL). */\n signatureSrc: (f: FieldDef) => signatures[String(answers[f.key])],\n /** Visible fields grouped as questions (tables, option groups), titled from the PDF text once loaded. */\n questions,\n /** Reads the PDF text so `questions` get real titles (call when you first show them). */\n loadPageText: () => setWantText(true),\n pdf,\n pdfSizes,\n /** Pass to PdfPages' onLoaded. */\n onPdfLoaded: (sizes: PageSize[], doc: PDFDocumentProxy) => {\n setPdf(doc)\n setPdfSizes(sizes)\n },\n }\n}\n\nexport type FormFillerState = ReturnType<typeof useFormFiller>\n"]}