@dowel-ui/react 0.5.0 → 0.7.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.
- package/README.md +1 -1
- package/dist/blocks/admin-dashboard/meta.js +31 -0
- package/dist/blocks/admin-dashboard/meta.js.map +1 -0
- package/dist/blocks/agent-console/meta.js +27 -0
- package/dist/blocks/agent-console/meta.js.map +1 -0
- package/dist/blocks/ai-dashboard/meta.js +25 -0
- package/dist/blocks/ai-dashboard/meta.js.map +1 -0
- package/dist/blocks/ai-workspace/meta.js +33 -0
- package/dist/blocks/ai-workspace/meta.js.map +1 -0
- package/dist/blocks/analytics/meta.js +24 -0
- package/dist/blocks/analytics/meta.js.map +1 -0
- package/dist/blocks/billing/meta.js +25 -0
- package/dist/blocks/billing/meta.js.map +1 -0
- package/dist/blocks/command-center/meta.js +28 -0
- package/dist/blocks/command-center/meta.js.map +1 -0
- package/dist/blocks/crm/meta.js +32 -0
- package/dist/blocks/crm/meta.js.map +1 -0
- package/dist/blocks/onboarding/meta.js +23 -0
- package/dist/blocks/onboarding/meta.js.map +1 -0
- package/dist/components/accordion/accordion.js +1 -1
- package/dist/components/accordion/accordion.js.map +1 -1
- package/dist/components/activity-feed/activity-feed.js +1 -1
- package/dist/components/activity-feed/activity-feed.js.map +1 -1
- package/dist/components/ai-agent-plan/ai-agent-plan.js +1 -1
- package/dist/components/ai-agent-plan/ai-agent-plan.js.map +1 -1
- package/dist/components/ai-extraction-review/ai-extraction-review.js +1 -1
- package/dist/components/ai-extraction-review/ai-extraction-review.js.map +1 -1
- package/dist/components/ai-prompt-input/ai-prompt-input.js +1 -1
- package/dist/components/ai-prompt-input/ai-prompt-input.js.map +1 -1
- package/dist/components/ai-reasoning/ai-reasoning.js +1 -1
- package/dist/components/ai-reasoning/ai-reasoning.js.map +1 -1
- package/dist/components/ai-response/ai-response.js +2 -2
- package/dist/components/ai-response/ai-response.js.map +1 -1
- package/dist/components/ai-tool/ai-tool.js +2 -2
- package/dist/components/ai-tool/ai-tool.js.map +1 -1
- package/dist/components/alert/alert.d.ts +1 -1
- package/dist/components/avatar/avatar.d.ts +1 -1
- package/dist/components/badge/badge.d.ts +1 -1
- package/dist/components/breadcrumb/breadcrumb.d.ts +52 -0
- package/dist/components/breadcrumb/breadcrumb.d.ts.map +1 -0
- package/dist/components/breadcrumb/breadcrumb.js +141 -0
- package/dist/components/breadcrumb/breadcrumb.js.map +1 -0
- package/dist/components/breadcrumb/index.d.ts +2 -0
- package/dist/components/breadcrumb/index.js +2 -0
- package/dist/components/breadcrumb/meta.js +17 -0
- package/dist/components/breadcrumb/meta.js.map +1 -0
- package/dist/components/button/button.d.ts +1 -1
- package/dist/components/calendar/calendar.js +2 -2
- package/dist/components/calendar/calendar.js.map +1 -1
- package/dist/components/code-block/code-block.js +1 -1
- package/dist/components/code-block/code-block.js.map +1 -1
- package/dist/components/collapsible/collapsible.d.ts +30 -0
- package/dist/components/collapsible/collapsible.d.ts.map +1 -0
- package/dist/components/collapsible/collapsible.js +38 -0
- package/dist/components/collapsible/collapsible.js.map +1 -0
- package/dist/components/collapsible/index.d.ts +2 -0
- package/dist/components/collapsible/index.js +2 -0
- package/dist/components/collapsible/meta.js +17 -0
- package/dist/components/collapsible/meta.js.map +1 -0
- package/dist/components/combobox/combobox.js +2 -2
- package/dist/components/combobox/combobox.js.map +1 -1
- package/dist/components/command/command.js +1 -1
- package/dist/components/command/command.js.map +1 -1
- package/dist/components/data-table/data-table.d.ts.map +1 -1
- package/dist/components/data-table/data-table.js +4 -1
- package/dist/components/data-table/data-table.js.map +1 -1
- package/dist/components/dialog/dialog.js +2 -2
- package/dist/components/dialog/dialog.js.map +1 -1
- package/dist/components/diff-viewer/diff-viewer.js +4 -4
- package/dist/components/diff-viewer/diff-viewer.js.map +1 -1
- package/dist/components/direction/direction.d.ts +35 -0
- package/dist/components/direction/direction.d.ts.map +1 -0
- package/dist/components/direction/direction.js +14 -0
- package/dist/components/direction/direction.js.map +1 -0
- package/dist/components/direction/index.d.ts +2 -0
- package/dist/components/direction/index.js +2 -0
- package/dist/components/direction/meta.js +17 -0
- package/dist/components/direction/meta.js.map +1 -0
- package/dist/components/drawer/drawer.js +1 -1
- package/dist/components/drawer/drawer.js.map +1 -1
- package/dist/components/dropdown-menu/dropdown-menu.d.ts.map +1 -1
- package/dist/components/dropdown-menu/dropdown-menu.js +9 -8
- package/dist/components/dropdown-menu/dropdown-menu.js.map +1 -1
- package/dist/components/input/input.js +1 -1
- package/dist/components/input/input.js.map +1 -1
- package/dist/components/log-viewer/log-viewer.js +2 -2
- package/dist/components/log-viewer/log-viewer.js.map +1 -1
- package/dist/components/meter/meter.js +1 -1
- package/dist/components/meter/meter.js.map +1 -1
- package/dist/components/pagination/pagination.d.ts.map +1 -1
- package/dist/components/pagination/pagination.js +3 -2
- package/dist/components/pagination/pagination.js.map +1 -1
- package/dist/components/permission-matrix/permission-matrix.js +3 -3
- package/dist/components/permission-matrix/permission-matrix.js.map +1 -1
- package/dist/components/progress/progress.d.ts +1 -1
- package/dist/components/record-diff/record-diff.js +4 -4
- package/dist/components/record-diff/record-diff.js.map +1 -1
- package/dist/components/select/select.js +2 -2
- package/dist/components/select/select.js.map +1 -1
- package/dist/components/sheet/sheet.d.ts.map +1 -1
- package/dist/components/sheet/sheet.js +2 -2
- package/dist/components/sheet/sheet.js.map +1 -1
- package/dist/components/sidebar/index.d.ts +2 -0
- package/dist/components/sidebar/index.js +2 -0
- package/dist/components/sidebar/meta.js +17 -0
- package/dist/components/sidebar/meta.js.map +1 -0
- package/dist/components/sidebar/sidebar.d.ts +69 -0
- package/dist/components/sidebar/sidebar.d.ts.map +1 -0
- package/dist/components/sidebar/sidebar.js +256 -0
- package/dist/components/sidebar/sidebar.js.map +1 -0
- package/dist/components/spinner/spinner.d.ts +1 -1
- package/dist/components/table/table.js +2 -2
- package/dist/components/table/table.js.map +1 -1
- package/dist/components/tags-input/tags-input.js +1 -1
- package/dist/components/tags-input/tags-input.js.map +1 -1
- package/dist/components/textarea/index.d.ts +2 -0
- package/dist/components/textarea/index.js +2 -0
- package/dist/components/textarea/meta.js +17 -0
- package/dist/components/textarea/meta.js.map +1 -0
- package/dist/components/textarea/textarea.d.ts +43 -0
- package/dist/components/textarea/textarea.d.ts.map +1 -0
- package/dist/components/textarea/textarea.js +119 -0
- package/dist/components/textarea/textarea.js.map +1 -0
- package/dist/components/time-range-picker/time-range-picker.js +3 -3
- package/dist/components/time-range-picker/time-range-picker.js.map +1 -1
- package/dist/components/toast/toast.d.ts +1 -1
- package/dist/components/toast/toast.d.ts.map +1 -1
- package/dist/components/toast/toast.js +2 -2
- package/dist/components/toast/toast.js.map +1 -1
- package/dist/index.d.ts +12 -2
- package/dist/index.js +7 -2
- package/dist/lib/styles.d.ts +16 -1
- package/dist/lib/styles.d.ts.map +1 -1
- package/dist/lib/styles.js +16 -1
- package/dist/lib/styles.js.map +1 -1
- package/dist/registry/blocks.d.ts.map +1 -1
- package/dist/registry/blocks.js +27 -9
- package/dist/registry/blocks.js.map +1 -1
- package/dist/registry/components.d.ts.map +1 -1
- package/dist/registry/components.js +60 -50
- package/dist/registry/components.js.map +1 -1
- package/dist/registry/schema.d.ts +8 -0
- package/dist/registry/schema.d.ts.map +1 -1
- package/dist/registry/schema.js.map +1 -1
- package/package.json +5 -4
- package/src/components/accordion/accordion.tsx +1 -1
- package/src/components/activity-feed/activity-feed.tsx +1 -1
- package/src/components/ai-agent-plan/ai-agent-plan.tsx +0 -0
- package/src/components/ai-extraction-review/ai-extraction-review.tsx +1 -1
- package/src/components/ai-prompt-input/ai-prompt-input.tsx +1 -1
- package/src/components/ai-reasoning/ai-reasoning.tsx +1 -1
- package/src/components/ai-response/ai-response.tsx +2 -2
- package/src/components/ai-tool/ai-tool.tsx +2 -2
- package/src/components/breadcrumb/breadcrumb.tsx +157 -0
- package/src/components/breadcrumb/index.ts +10 -0
- package/src/components/breadcrumb/meta.ts +19 -0
- package/src/components/calendar/calendar.tsx +2 -2
- package/src/components/code-block/code-block.tsx +1 -1
- package/src/components/collapsible/collapsible.tsx +63 -0
- package/src/components/collapsible/index.ts +6 -0
- package/src/components/collapsible/meta.ts +16 -0
- package/src/components/combobox/combobox.tsx +2 -2
- package/src/components/command/command.tsx +1 -1
- package/src/components/data-table/data-table.tsx +14 -3
- package/src/components/dialog/dialog.tsx +2 -2
- package/src/components/diff-viewer/diff-viewer.tsx +4 -4
- package/src/components/direction/direction.tsx +38 -0
- package/src/components/direction/index.ts +1 -0
- package/src/components/direction/meta.ts +19 -0
- package/src/components/drawer/drawer.tsx +1 -1
- package/src/components/dropdown-menu/dropdown-menu.tsx +14 -8
- package/src/components/input/input.tsx +1 -1
- package/src/components/log-viewer/log-viewer.tsx +2 -2
- package/src/components/meter/meter.tsx +1 -1
- package/src/components/pagination/pagination.tsx +13 -2
- package/src/components/permission-matrix/permission-matrix.tsx +4 -4
- package/src/components/record-diff/record-diff.tsx +4 -4
- package/src/components/select/select.tsx +2 -2
- package/src/components/sheet/sheet.tsx +8 -4
- package/src/components/sidebar/index.ts +19 -0
- package/src/components/sidebar/meta.ts +25 -0
- package/src/components/sidebar/sidebar.tsx +374 -0
- package/src/components/table/table.tsx +3 -3
- package/src/components/tags-input/tags-input.tsx +1 -1
- package/src/components/textarea/index.ts +1 -0
- package/src/components/textarea/meta.ts +21 -0
- package/src/components/textarea/textarea.tsx +210 -0
- package/src/components/time-range-picker/time-range-picker.tsx +3 -3
- package/src/components/toast/toast.tsx +14 -6
- package/src/index.ts +5 -0
- package/src/lib/styles.ts +16 -0
- package/src/registry/blocks.ts +18 -0
- package/src/registry/components.ts +10 -0
- package/src/registry/schema.ts +8 -0
- package/src/blocks/admin-users/admin-users.tsx +0 -251
- package/src/blocks/admin-users/index.ts +0 -1
- package/src/blocks/admin-users/meta.ts +0 -27
- package/src/blocks/ai-chat/ai-chat.tsx +0 -280
- package/src/blocks/ai-chat/index.ts +0 -7
- package/src/blocks/ai-chat/meta.ts +0 -32
- package/src/blocks/dashboard/dashboard.tsx +0 -192
- package/src/blocks/dashboard/index.ts +0 -6
- package/src/blocks/dashboard/meta.ts +0 -18
- package/src/blocks/forgot-password/forgot-password.tsx +0 -158
- package/src/blocks/forgot-password/index.ts +0 -1
- package/src/blocks/forgot-password/meta.ts +0 -18
- package/src/blocks/login/index.ts +0 -1
- package/src/blocks/login/login.tsx +0 -178
- package/src/blocks/login/meta.ts +0 -18
- package/src/blocks/pricing/index.ts +0 -1
- package/src/blocks/pricing/meta.ts +0 -18
- package/src/blocks/pricing/pricing.tsx +0 -176
- package/src/blocks/settings/index.ts +0 -6
- package/src/blocks/settings/meta.ts +0 -29
- package/src/blocks/settings/settings.tsx +0 -317
- package/src/blocks/signup/index.ts +0 -1
- package/src/blocks/signup/meta.ts +0 -18
- package/src/blocks/signup/signup.tsx +0 -240
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-extraction-review.js","names":[],"sources":["../../../src/components/ai-extraction-review/ai-extraction-review.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n type ComponentPropsWithRef,\n type KeyboardEvent,\n type ReactNode,\n} from \"react\";\n\nimport { disabledStyles, focusRing } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\nimport {\n evidenceOf,\n isSourced,\n segmentSource,\n summarizeReview,\n type ExtractionField,\n type FieldDecision,\n type SourceRun,\n} from \"./extraction-model\";\n\n/**\n * The check after extraction: the document on one side, what the model read\n * out of it on the other, and a decision about every field.\n *\n * Every extraction demo shows the filled object and stops. The products that\n * run extraction for a living — invoice capture, KYC onboarding, claims intake —\n * all have this screen, each built from scratch, because a value that cannot\n * be checked can only be trusted, and nobody who is paying an invoice trusts a\n * model that far. No component library ships it.\n *\n * The link is the component. Each field carries where in the source it was\n * read from, quoted in text under the value as well as highlighted in the\n * document, so a reviewer who cannot see the highlight still has the evidence.\n * And a value with no evidence is said outright — \"the model supplied this\n * without evidence\" — because that is the case a review exists to catch, and\n * the one every filled-object view renders identically to a good value.\n *\n * Evidence is a text offset, not a bounding box. A language model reads text,\n * and a text layer with offsets is what every OCR pipeline already yields.\n * Boxes over a rendered page need page rendering, zoom and geometry, and are a\n * different component; this one does not pretend at them.\n *\n * Decisions are controlled and the component writes nothing. It reports that\n * a field was accepted as proposed, corrected from what was proposed, or\n * rejected — a richer record than a form's submitted values, and the one an\n * audit later needs — and leaves what to do with it to the application.\n */\n\nexport interface ExtractionReviewProps extends Omit<\n ComponentPropsWithRef<\"section\">,\n \"children\"\n> {\n /** What the model read. Every span indexes into it. */\n source: string;\n /** Names the source — \"Invoice 4471.pdf, page 1\". */\n sourceLabel?: string;\n /** What is being reviewed, as the heading. */\n heading: string;\n fields: ExtractionField[];\n /** Per-field decisions, keyed by field name. Controlled. */\n decisions?: Record<string, FieldDecision>;\n /** Omit to render the review as a record rather than something to operate. */\n onDecision?: (name: string, decision: FieldDecision) => void;\n /** True while fields are still arriving. Missing values wait instead of reading as not found. */\n streaming?: boolean;\n /** Confidence below this is called low, in words. */\n lowConfidenceBelow?: number;\n children?: ReactNode;\n}\n\nexport function ExtractionReview({\n className,\n source,\n sourceLabel = \"Source\",\n heading,\n fields,\n decisions = {},\n onDecision,\n streaming = false,\n lowConfidenceBelow = 0.7,\n children,\n ...props\n}: ExtractionReviewProps) {\n const headingId = useId();\n const [activeField, setActiveField] = useState<string | null>(null);\n\n const runs = useMemo(() => segmentSource(source, fields), [source, fields]);\n const summary = useMemo(\n () => summarizeReview(source, fields, decisions),\n [source, fields, decisions],\n );\n\n return (\n <section\n data-slot=\"extraction-review\"\n data-complete={summary.complete || undefined}\n aria-labelledby={headingId}\n aria-busy={streaming || undefined}\n className={cn(\"flex flex-col gap-3\", className)}\n {...props}\n >\n <div className=\"flex flex-wrap items-baseline justify-between gap-2\">\n <h3 id={headingId} className=\"text-sm font-medium\">\n {heading}\n </h3>\n {/* Live from the start and short, so each decision is followed by\n where the review stands rather than by silence. Evidence gaps are\n counted here too: they are the reason to read carefully, and worth\n knowing before starting rather than discovering on field six. */}\n <p\n data-slot=\"extraction-review-summary\"\n aria-live=\"polite\"\n className=\"text-xs text-muted-foreground\"\n >\n {streaming\n ? \"Fields are still arriving\"\n : `${String(summary.reviewed)} of ${String(summary.total)} reviewed`}\n {summary.unsourced.length > 0\n ? ` · ${String(summary.unsourced.length)} without evidence`\n : null}\n </p>\n </div>\n\n {children}\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n <SourcePanel\n label={sourceLabel}\n runs={runs}\n active={activeField}\n decisions={decisions}\n />\n\n <ol data-slot=\"extraction-fields\" className=\"m-0 flex list-none flex-col gap-3 p-0\">\n {fields.map((field) => (\n <ReviewField\n key={field.name}\n field={field}\n source={source}\n decision={decisions[field.name]}\n streaming={streaming}\n lowConfidenceBelow={lowConfidenceBelow}\n active={activeField === field.name}\n onActivate={() => {\n setActiveField(field.name);\n }}\n onDecision={onDecision}\n />\n ))}\n </ol>\n </div>\n </section>\n );\n}\n\nfunction SourcePanel({\n label,\n runs,\n active,\n decisions,\n}: {\n label: string;\n runs: SourceRun[];\n active: string | null;\n decisions: Record<string, FieldDecision>;\n}) {\n const ref = useRef<HTMLDivElement>(null);\n\n // Bring the active field's evidence into view. Focus stays where it is —\n // in the field — so a keyboard user is shown the source without being\n // moved into it.\n useEffect(() => {\n if (!active || !ref.current) return;\n const mark = ref.current.querySelector(`mark[data-active]`);\n mark?.scrollIntoView({ block: \"nearest\" });\n }, [active]);\n\n return (\n <div\n ref={ref}\n data-slot=\"extraction-source\"\n // A scrolling box has to be focusable to be scrolled by keyboard, and a\n // focusable box needs a name.\n role=\"region\"\n aria-label={label}\n tabIndex={0}\n // Sticks beside the fields on a wide screen, so the evidence for field\n // nine is still in view when the reviewer reaches it.\n className={cn(\n \"max-h-96 overflow-auto rounded-lg border border-border bg-muted/30 p-3 md:sticky md:top-0 md:self-start\",\n focusRing,\n )}\n >\n {runs.length === 0 ? (\n <p className=\"text-xs text-muted-foreground\">No source text.</p>\n ) : (\n <pre className=\"m-0 font-sans text-sm leading-relaxed break-words whitespace-pre-wrap\">\n {runs.map((run, index) =>\n run.fields.length === 0 ? (\n <span key={index}>{run.text}</span>\n ) : (\n // A mark element, so the evidence survives as structure rather\n // than only as a tint. No label is inserted into the text: a\n // document read aloud with field names spliced into it is not\n // the document, and the field already quotes its evidence.\n <mark\n key={index}\n data-fields={run.fields.join(\" \")}\n data-active={active !== null && run.fields.includes(active) ? \"\" : undefined}\n className={cn(\n \"rounded-[2px] text-inherit\",\n run.fields.every((name) => decisions[name]?.kind === \"rejected\")\n ? \"bg-muted line-through decoration-muted-foreground\"\n : \"bg-primary/15\",\n active !== null &&\n run.fields.includes(active) &&\n \"bg-primary/30 ring-1 ring-primary ring-inset\",\n )}\n >\n {run.text}\n </mark>\n ),\n )}\n </pre>\n )}\n </div>\n );\n}\n\n/** Whitespace collapsed and cut short, for quoting evidence under a field. */\nfunction quote(text: string, limit = 80): string {\n const collapsed = text.replace(/\\s+/g, \" \").trim();\n return collapsed.length > limit ? `${collapsed.slice(0, limit - 1)}…` : collapsed;\n}\n\nfunction ReviewField({\n field,\n source,\n decision,\n streaming,\n lowConfidenceBelow,\n active,\n onActivate,\n onDecision,\n}: {\n field: ExtractionField;\n source: string;\n decision?: FieldDecision;\n streaming: boolean;\n lowConfidenceBelow: number;\n active: boolean;\n onActivate: () => void;\n onDecision?: (name: string, decision: FieldDecision) => void;\n}) {\n const inputId = useId();\n const evidenceId = useId();\n const statusId = useId();\n const confidenceId = useId();\n\n // The reviewer's typing, kept apart from the model's value so the decision\n // can say which is which.\n const [draft, setDraft] = useState<string | null>(null);\n\n const proposed = field.value ?? \"\";\n const decided = decision && decision.kind !== \"rejected\" ? decision.value : undefined;\n const current = draft ?? decided ?? proposed;\n const edited = current !== proposed;\n const changedSince = decided !== undefined && current !== decided;\n\n const arriving = streaming && field.value === undefined;\n const sourced = isSourced(source, field);\n const evidence = sourced ? evidenceOf(source, field.span) : \"\";\n const unsourced = !arriving && field.value !== undefined && !sourced;\n\n const accept = () => {\n onDecision?.(\n field.name,\n edited\n ? { kind: \"corrected\", value: current, proposed }\n : { kind: \"accepted\", value: current },\n );\n };\n\n const onKeyDown = (event: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {\n if (event.key !== \"Enter\") return;\n // Enter confirms an IME candidate before it means anything else. Without\n // this, a reviewer typing a Japanese correction accepts a fragment of it.\n if (event.nativeEvent.isComposing) return;\n if (field.multiline && !(event.ctrlKey || event.metaKey)) return;\n event.preventDefault();\n accept();\n };\n\n const status = arriving\n ? \"Arriving\"\n : !decision\n ? \"Not reviewed\"\n : decision.kind === \"rejected\"\n ? \"Rejected\"\n : changedSince\n ? `Changed since it was ${decision.kind}`\n : decision.kind === \"accepted\"\n ? \"Accepted\"\n : decision.proposed\n ? `Corrected from “${quote(decision.proposed, 40)}”`\n : \"Supplied by the reviewer\";\n\n const low = field.confidence !== undefined && field.confidence < lowConfidenceBelow;\n const percent =\n field.confidence === undefined\n ? null\n : new Intl.NumberFormat(undefined, { style: \"percent\", maximumFractionDigits: 0 }).format(\n field.confidence,\n );\n\n const describedBy = [evidenceId, statusId, percent !== null ? confidenceId : null]\n .filter(Boolean)\n .join(\" \");\n\n const controlClass = cn(\n \"w-full rounded-md border border-input bg-background px-2 py-1.5 text-sm\",\n edited && \"border-warning\",\n focusRing,\n );\n\n return (\n <li\n data-slot=\"extraction-field\"\n data-field={field.name}\n data-state={arriving ? \"arriving\" : (decision?.kind ?? \"undecided\")}\n data-active={active || undefined}\n data-unsourced={unsourced || undefined}\n // Focus anywhere in the field — value, either button — is the reviewer\n // looking at it, so the source follows without a separate step.\n onFocus={onActivate}\n className={cn(\n \"flex flex-col gap-1 rounded-lg border p-3\",\n active ? \"border-primary\" : \"border-border\",\n decision?.kind === \"rejected\" && \"bg-muted/40\",\n )}\n >\n <div className=\"flex flex-wrap items-baseline justify-between gap-2\">\n {onDecision && !arriving ? (\n <label htmlFor={inputId} className=\"text-xs font-medium text-muted-foreground\">\n {field.label}\n </label>\n ) : (\n <span className=\"text-xs font-medium text-muted-foreground\">{field.label}</span>\n )}\n {/* Status as a word, outside the label so editing never renames the\n control under a screen reader user. */}\n <span\n id={statusId}\n data-slot=\"extraction-field-status\"\n className={cn(\n \"text-xs\",\n decision?.kind === \"rejected\" ? \"text-destructive\" : \"text-muted-foreground\",\n changedSince && \"text-warning\",\n )}\n >\n {status}\n </span>\n </div>\n\n {arriving ? (\n <span\n aria-hidden=\"true\"\n className=\"block h-8 w-full animate-pulse-soft rounded-md bg-muted\"\n />\n ) : !onDecision ? (\n <p className=\"rounded-md bg-muted px-2 py-1.5 text-sm break-words\">\n {current || <span className=\"text-muted-foreground\">Empty</span>}\n </p>\n ) : field.multiline ? (\n <textarea\n id={inputId}\n rows={3}\n value={current}\n aria-describedby={describedBy}\n onChange={(event) => {\n setDraft(event.target.value);\n }}\n onKeyDown={onKeyDown}\n className={cn(controlClass, \"resize-none\")}\n />\n ) : (\n <input\n id={inputId}\n type=\"text\"\n value={current}\n aria-describedby={describedBy}\n onChange={(event) => {\n setDraft(event.target.value);\n }}\n onKeyDown={onKeyDown}\n className={controlClass}\n />\n )}\n\n {/* The evidence in text. This is the accessible path to the source, and\n for a sighted reviewer it is the comparison itself — \"1 March 2026\"\n beside \"2026-03-01\" is a normalisation, not an error, and only reads\n that way with both in view. */}\n <p\n id={evidenceId}\n data-slot=\"extraction-field-evidence\"\n className={cn(\"text-xs\", unsourced ? \"text-warning\" : \"text-muted-foreground\")}\n >\n {arriving\n ? \"Waiting for the model\"\n : sourced\n ? `In the source: “${quote(evidence)}”`\n : field.value === undefined\n ? \"The model found nothing for this field.\"\n : \"Not in the source — the model supplied this without evidence.\"}\n </p>\n\n {percent !== null ? (\n <p\n id={confidenceId}\n data-slot=\"extraction-field-confidence\"\n data-low={low || undefined}\n className={cn(\"text-xs\", low ? \"text-warning\" : \"text-muted-foreground\")}\n >\n {low ? `Low confidence, ${percent} — worth checking` : `${percent} confidence`}\n </p>\n ) : null}\n\n {onDecision && !arriving ? (\n <div className=\"mt-1 flex flex-wrap items-center gap-2\">\n <FieldButton\n variant=\"primary\"\n aria-label={`${edited ? \"Accept correction\" : \"Accept\"} — ${field.label}`}\n aria-pressed={\n decision !== undefined && decision.kind !== \"rejected\" && !changedSince\n }\n onClick={accept}\n >\n {edited ? \"Accept correction\" : \"Accept\"}\n </FieldButton>\n <FieldButton\n aria-label={`Reject — ${field.label}`}\n aria-pressed={decision?.kind === \"rejected\"}\n onClick={() => {\n onDecision(field.name, { kind: \"rejected\" });\n }}\n >\n Reject\n </FieldButton>\n {sourced ? (\n <FieldButton\n aria-label={`Show in source — ${field.label}`}\n className=\"ml-auto\"\n onClick={onActivate}\n >\n Show in source\n </FieldButton>\n ) : null}\n </div>\n ) : null}\n </li>\n );\n}\n\nfunction FieldButton({\n className,\n variant = \"default\",\n ...props\n}: ComponentPropsWithRef<\"button\"> & { variant?: \"default\" | \"primary\" }) {\n return (\n <button\n type=\"button\"\n className={cn(\n \"rounded-md border px-2.5 py-1 text-xs font-medium transition-colors\",\n variant === \"primary\" &&\n \"border-primary bg-primary text-primary-foreground hover:bg-primary-hover aria-pressed:bg-primary-active\",\n variant === \"default\" &&\n \"border-input bg-background hover:bg-accent hover:text-accent-foreground aria-pressed:bg-secondary aria-pressed:text-secondary-foreground\",\n focusRing,\n disabledStyles,\n className,\n )}\n {...props}\n />\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ai-extraction-review.js","names":[],"sources":["../../../src/components/ai-extraction-review/ai-extraction-review.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n type ComponentPropsWithRef,\n type KeyboardEvent,\n type ReactNode,\n} from \"react\";\n\nimport { disabledStyles, focusRing } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\nimport {\n evidenceOf,\n isSourced,\n segmentSource,\n summarizeReview,\n type ExtractionField,\n type FieldDecision,\n type SourceRun,\n} from \"./extraction-model\";\n\n/**\n * The check after extraction: the document on one side, what the model read\n * out of it on the other, and a decision about every field.\n *\n * Every extraction demo shows the filled object and stops. The products that\n * run extraction for a living — invoice capture, KYC onboarding, claims intake —\n * all have this screen, each built from scratch, because a value that cannot\n * be checked can only be trusted, and nobody who is paying an invoice trusts a\n * model that far. No component library ships it.\n *\n * The link is the component. Each field carries where in the source it was\n * read from, quoted in text under the value as well as highlighted in the\n * document, so a reviewer who cannot see the highlight still has the evidence.\n * And a value with no evidence is said outright — \"the model supplied this\n * without evidence\" — because that is the case a review exists to catch, and\n * the one every filled-object view renders identically to a good value.\n *\n * Evidence is a text offset, not a bounding box. A language model reads text,\n * and a text layer with offsets is what every OCR pipeline already yields.\n * Boxes over a rendered page need page rendering, zoom and geometry, and are a\n * different component; this one does not pretend at them.\n *\n * Decisions are controlled and the component writes nothing. It reports that\n * a field was accepted as proposed, corrected from what was proposed, or\n * rejected — a richer record than a form's submitted values, and the one an\n * audit later needs — and leaves what to do with it to the application.\n */\n\nexport interface ExtractionReviewProps extends Omit<\n ComponentPropsWithRef<\"section\">,\n \"children\"\n> {\n /** What the model read. Every span indexes into it. */\n source: string;\n /** Names the source — \"Invoice 4471.pdf, page 1\". */\n sourceLabel?: string;\n /** What is being reviewed, as the heading. */\n heading: string;\n fields: ExtractionField[];\n /** Per-field decisions, keyed by field name. Controlled. */\n decisions?: Record<string, FieldDecision>;\n /** Omit to render the review as a record rather than something to operate. */\n onDecision?: (name: string, decision: FieldDecision) => void;\n /** True while fields are still arriving. Missing values wait instead of reading as not found. */\n streaming?: boolean;\n /** Confidence below this is called low, in words. */\n lowConfidenceBelow?: number;\n children?: ReactNode;\n}\n\nexport function ExtractionReview({\n className,\n source,\n sourceLabel = \"Source\",\n heading,\n fields,\n decisions = {},\n onDecision,\n streaming = false,\n lowConfidenceBelow = 0.7,\n children,\n ...props\n}: ExtractionReviewProps) {\n const headingId = useId();\n const [activeField, setActiveField] = useState<string | null>(null);\n\n const runs = useMemo(() => segmentSource(source, fields), [source, fields]);\n const summary = useMemo(\n () => summarizeReview(source, fields, decisions),\n [source, fields, decisions],\n );\n\n return (\n <section\n data-slot=\"extraction-review\"\n data-complete={summary.complete || undefined}\n aria-labelledby={headingId}\n aria-busy={streaming || undefined}\n className={cn(\"flex flex-col gap-3\", className)}\n {...props}\n >\n <div className=\"flex flex-wrap items-baseline justify-between gap-2\">\n <h3 id={headingId} className=\"text-sm font-medium\">\n {heading}\n </h3>\n {/* Live from the start and short, so each decision is followed by\n where the review stands rather than by silence. Evidence gaps are\n counted here too: they are the reason to read carefully, and worth\n knowing before starting rather than discovering on field six. */}\n <p\n data-slot=\"extraction-review-summary\"\n aria-live=\"polite\"\n className=\"text-xs text-muted-foreground\"\n >\n {streaming\n ? \"Fields are still arriving\"\n : `${String(summary.reviewed)} of ${String(summary.total)} reviewed`}\n {summary.unsourced.length > 0\n ? ` · ${String(summary.unsourced.length)} without evidence`\n : null}\n </p>\n </div>\n\n {children}\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n <SourcePanel\n label={sourceLabel}\n runs={runs}\n active={activeField}\n decisions={decisions}\n />\n\n <ol data-slot=\"extraction-fields\" className=\"m-0 flex list-none flex-col gap-3 p-0\">\n {fields.map((field) => (\n <ReviewField\n key={field.name}\n field={field}\n source={source}\n decision={decisions[field.name]}\n streaming={streaming}\n lowConfidenceBelow={lowConfidenceBelow}\n active={activeField === field.name}\n onActivate={() => {\n setActiveField(field.name);\n }}\n onDecision={onDecision}\n />\n ))}\n </ol>\n </div>\n </section>\n );\n}\n\nfunction SourcePanel({\n label,\n runs,\n active,\n decisions,\n}: {\n label: string;\n runs: SourceRun[];\n active: string | null;\n decisions: Record<string, FieldDecision>;\n}) {\n const ref = useRef<HTMLDivElement>(null);\n\n // Bring the active field's evidence into view. Focus stays where it is —\n // in the field — so a keyboard user is shown the source without being\n // moved into it.\n useEffect(() => {\n if (!active || !ref.current) return;\n const mark = ref.current.querySelector(`mark[data-active]`);\n mark?.scrollIntoView({ block: \"nearest\" });\n }, [active]);\n\n return (\n <div\n ref={ref}\n data-slot=\"extraction-source\"\n // A scrolling box has to be focusable to be scrolled by keyboard, and a\n // focusable box needs a name.\n role=\"region\"\n aria-label={label}\n tabIndex={0}\n // Sticks beside the fields on a wide screen, so the evidence for field\n // nine is still in view when the reviewer reaches it.\n className={cn(\n \"max-h-96 overflow-auto rounded-lg border border-border bg-muted/30 p-3 md:sticky md:top-0 md:self-start\",\n focusRing,\n )}\n >\n {runs.length === 0 ? (\n <p className=\"text-xs text-muted-foreground\">No source text.</p>\n ) : (\n <pre className=\"m-0 font-sans text-sm leading-relaxed break-words whitespace-pre-wrap\">\n {runs.map((run, index) =>\n run.fields.length === 0 ? (\n <span key={index}>{run.text}</span>\n ) : (\n // A mark element, so the evidence survives as structure rather\n // than only as a tint. No label is inserted into the text: a\n // document read aloud with field names spliced into it is not\n // the document, and the field already quotes its evidence.\n <mark\n key={index}\n data-fields={run.fields.join(\" \")}\n data-active={active !== null && run.fields.includes(active) ? \"\" : undefined}\n className={cn(\n \"rounded-[2px] text-inherit\",\n run.fields.every((name) => decisions[name]?.kind === \"rejected\")\n ? \"bg-muted line-through decoration-muted-foreground\"\n : \"bg-primary/15\",\n active !== null &&\n run.fields.includes(active) &&\n \"bg-primary/30 ring-1 ring-primary ring-inset\",\n )}\n >\n {run.text}\n </mark>\n ),\n )}\n </pre>\n )}\n </div>\n );\n}\n\n/** Whitespace collapsed and cut short, for quoting evidence under a field. */\nfunction quote(text: string, limit = 80): string {\n const collapsed = text.replace(/\\s+/g, \" \").trim();\n return collapsed.length > limit ? `${collapsed.slice(0, limit - 1)}…` : collapsed;\n}\n\nfunction ReviewField({\n field,\n source,\n decision,\n streaming,\n lowConfidenceBelow,\n active,\n onActivate,\n onDecision,\n}: {\n field: ExtractionField;\n source: string;\n decision?: FieldDecision;\n streaming: boolean;\n lowConfidenceBelow: number;\n active: boolean;\n onActivate: () => void;\n onDecision?: (name: string, decision: FieldDecision) => void;\n}) {\n const inputId = useId();\n const evidenceId = useId();\n const statusId = useId();\n const confidenceId = useId();\n\n // The reviewer's typing, kept apart from the model's value so the decision\n // can say which is which.\n const [draft, setDraft] = useState<string | null>(null);\n\n const proposed = field.value ?? \"\";\n const decided = decision && decision.kind !== \"rejected\" ? decision.value : undefined;\n const current = draft ?? decided ?? proposed;\n const edited = current !== proposed;\n const changedSince = decided !== undefined && current !== decided;\n\n const arriving = streaming && field.value === undefined;\n const sourced = isSourced(source, field);\n const evidence = sourced ? evidenceOf(source, field.span) : \"\";\n const unsourced = !arriving && field.value !== undefined && !sourced;\n\n const accept = () => {\n onDecision?.(\n field.name,\n edited\n ? { kind: \"corrected\", value: current, proposed }\n : { kind: \"accepted\", value: current },\n );\n };\n\n const onKeyDown = (event: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {\n if (event.key !== \"Enter\") return;\n // Enter confirms an IME candidate before it means anything else. Without\n // this, a reviewer typing a Japanese correction accepts a fragment of it.\n if (event.nativeEvent.isComposing) return;\n if (field.multiline && !(event.ctrlKey || event.metaKey)) return;\n event.preventDefault();\n accept();\n };\n\n const status = arriving\n ? \"Arriving\"\n : !decision\n ? \"Not reviewed\"\n : decision.kind === \"rejected\"\n ? \"Rejected\"\n : changedSince\n ? `Changed since it was ${decision.kind}`\n : decision.kind === \"accepted\"\n ? \"Accepted\"\n : decision.proposed\n ? `Corrected from “${quote(decision.proposed, 40)}”`\n : \"Supplied by the reviewer\";\n\n const low = field.confidence !== undefined && field.confidence < lowConfidenceBelow;\n const percent =\n field.confidence === undefined\n ? null\n : new Intl.NumberFormat(undefined, { style: \"percent\", maximumFractionDigits: 0 }).format(\n field.confidence,\n );\n\n const describedBy = [evidenceId, statusId, percent !== null ? confidenceId : null]\n .filter(Boolean)\n .join(\" \");\n\n const controlClass = cn(\n \"w-full rounded-md border border-input bg-background px-2 py-1.5 text-sm\",\n edited && \"border-warning\",\n focusRing,\n );\n\n return (\n <li\n data-slot=\"extraction-field\"\n data-field={field.name}\n data-state={arriving ? \"arriving\" : (decision?.kind ?? \"undecided\")}\n data-active={active || undefined}\n data-unsourced={unsourced || undefined}\n // Focus anywhere in the field — value, either button — is the reviewer\n // looking at it, so the source follows without a separate step.\n onFocus={onActivate}\n className={cn(\n \"flex flex-col gap-1 rounded-lg border p-3\",\n active ? \"border-primary\" : \"border-border\",\n decision?.kind === \"rejected\" && \"bg-muted/40\",\n )}\n >\n <div className=\"flex flex-wrap items-baseline justify-between gap-2\">\n {onDecision && !arriving ? (\n <label htmlFor={inputId} className=\"text-xs font-medium text-muted-foreground\">\n {field.label}\n </label>\n ) : (\n <span className=\"text-xs font-medium text-muted-foreground\">{field.label}</span>\n )}\n {/* Status as a word, outside the label so editing never renames the\n control under a screen reader user. */}\n <span\n id={statusId}\n data-slot=\"extraction-field-status\"\n className={cn(\n \"text-xs\",\n decision?.kind === \"rejected\" ? \"text-destructive\" : \"text-muted-foreground\",\n changedSince && \"text-warning\",\n )}\n >\n {status}\n </span>\n </div>\n\n {arriving ? (\n <span\n aria-hidden=\"true\"\n className=\"block h-8 w-full animate-pulse-soft rounded-md bg-muted\"\n />\n ) : !onDecision ? (\n <p className=\"rounded-md bg-muted px-2 py-1.5 text-sm break-words\">\n {current || <span className=\"text-muted-foreground\">Empty</span>}\n </p>\n ) : field.multiline ? (\n <textarea\n id={inputId}\n rows={3}\n value={current}\n aria-describedby={describedBy}\n onChange={(event) => {\n setDraft(event.target.value);\n }}\n onKeyDown={onKeyDown}\n className={cn(controlClass, \"resize-none\")}\n />\n ) : (\n <input\n id={inputId}\n type=\"text\"\n value={current}\n aria-describedby={describedBy}\n onChange={(event) => {\n setDraft(event.target.value);\n }}\n onKeyDown={onKeyDown}\n className={controlClass}\n />\n )}\n\n {/* The evidence in text. This is the accessible path to the source, and\n for a sighted reviewer it is the comparison itself — \"1 March 2026\"\n beside \"2026-03-01\" is a normalisation, not an error, and only reads\n that way with both in view. */}\n <p\n id={evidenceId}\n data-slot=\"extraction-field-evidence\"\n className={cn(\"text-xs\", unsourced ? \"text-warning\" : \"text-muted-foreground\")}\n >\n {arriving\n ? \"Waiting for the model\"\n : sourced\n ? `In the source: “${quote(evidence)}”`\n : field.value === undefined\n ? \"The model found nothing for this field.\"\n : \"Not in the source — the model supplied this without evidence.\"}\n </p>\n\n {percent !== null ? (\n <p\n id={confidenceId}\n data-slot=\"extraction-field-confidence\"\n data-low={low || undefined}\n className={cn(\"text-xs\", low ? \"text-warning\" : \"text-muted-foreground\")}\n >\n {low ? `Low confidence, ${percent} — worth checking` : `${percent} confidence`}\n </p>\n ) : null}\n\n {onDecision && !arriving ? (\n <div className=\"mt-1 flex flex-wrap items-center gap-2\">\n <FieldButton\n variant=\"primary\"\n aria-label={`${edited ? \"Accept correction\" : \"Accept\"} — ${field.label}`}\n aria-pressed={\n decision !== undefined && decision.kind !== \"rejected\" && !changedSince\n }\n onClick={accept}\n >\n {edited ? \"Accept correction\" : \"Accept\"}\n </FieldButton>\n <FieldButton\n aria-label={`Reject — ${field.label}`}\n aria-pressed={decision?.kind === \"rejected\"}\n onClick={() => {\n onDecision(field.name, { kind: \"rejected\" });\n }}\n >\n Reject\n </FieldButton>\n {sourced ? (\n <FieldButton\n aria-label={`Show in source — ${field.label}`}\n className=\"ms-auto\"\n onClick={onActivate}\n >\n Show in source\n </FieldButton>\n ) : null}\n </div>\n ) : null}\n </li>\n );\n}\n\nfunction FieldButton({\n className,\n variant = \"default\",\n ...props\n}: ComponentPropsWithRef<\"button\"> & { variant?: \"default\" | \"primary\" }) {\n return (\n <button\n type=\"button\"\n className={cn(\n \"rounded-md border px-2.5 py-1 text-xs font-medium transition-colors\",\n variant === \"primary\" &&\n \"border-primary bg-primary text-primary-foreground hover:bg-primary-hover aria-pressed:bg-primary-active\",\n variant === \"default\" &&\n \"border-input bg-background hover:bg-accent hover:text-accent-foreground aria-pressed:bg-secondary aria-pressed:text-secondary-foreground\",\n focusRing,\n disabledStyles,\n className,\n )}\n {...props}\n />\n );\n}\n"],"mappings":"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"}
|
|
@@ -101,7 +101,7 @@ function PromptInputSubmit({ className, label = "Send message", stopLabel = "Sto
|
|
|
101
101
|
"aria-label": busy ? stopLabel : label,
|
|
102
102
|
disabled,
|
|
103
103
|
onClick: busy ? onStop : void 0,
|
|
104
|
-
className: cn("
|
|
104
|
+
className: cn("ms-auto grid size-8 shrink-0 place-items-center rounded-lg", "bg-primary text-primary-foreground transition-colors duration-[var(--duration-fast)]", "hover:bg-primary-hover disabled:pointer-events-none disabled:opacity-55", focusRing, className),
|
|
105
105
|
...props,
|
|
106
106
|
children: busy ? /* @__PURE__ */ jsx("svg", {
|
|
107
107
|
viewBox: "0 0 24 24",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-prompt-input.js","names":[],"sources":["../../../src/components/ai-prompt-input/ai-prompt-input.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n createContext,\n useContext,\n useId,\n useLayoutEffect,\n useMemo,\n useRef,\n type ComponentPropsWithRef,\n type FormEvent,\n type KeyboardEvent as ReactKeyboardEvent,\n} from \"react\";\n\nimport { focusRing, focusRingInset } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * The composer.\n *\n * Handles the parts of a chat input that are easy to get subtly wrong:\n * auto-growing to fit the text up to a ceiling, sending on Enter without\n * breaking multi-line input, and — the one almost everybody misses —\n * not sending mid-composition when someone is typing with an IME.\n */\n\ninterface PromptInputContextValue {\n textareaId: string;\n submit: () => void;\n disabled: boolean;\n busy: boolean;\n}\n\nconst PromptInputContext = createContext<PromptInputContextValue | null>(null);\n\nfunction usePromptInput(component: string): PromptInputContextValue {\n const context = useContext(PromptInputContext);\n if (!context) {\n throw new Error(`${component} must be rendered inside <PromptInput>.`);\n }\n return context;\n}\n\nexport interface PromptInputProps extends Omit<ComponentPropsWithRef<\"form\">, \"onSubmit\"> {\n onSubmit?: (event: FormEvent<HTMLFormElement>) => void;\n /** Blocks submission and dims the composer. */\n disabled?: boolean;\n /** A response is in flight: submission is blocked and Stop is shown instead. */\n busy?: boolean;\n}\n\nexport function PromptInput({\n className,\n onSubmit,\n disabled = false,\n busy = false,\n children,\n ...props\n}: PromptInputProps) {\n const uid = useId();\n const formRef = useRef<HTMLFormElement | null>(null);\n\n const context = useMemo<PromptInputContextValue>(\n () => ({\n textareaId: `${uid}-textarea`,\n disabled,\n busy,\n submit: () => {\n formRef.current?.requestSubmit();\n },\n }),\n [uid, disabled, busy],\n );\n\n return (\n <PromptInputContext.Provider value={context}>\n <form\n ref={formRef}\n data-slot=\"prompt-input\"\n data-busy={busy || undefined}\n onSubmit={(event) => {\n if (disabled || busy) {\n event.preventDefault();\n return;\n }\n onSubmit?.(event);\n }}\n className={cn(\n \"rounded-xl border border-input bg-background shadow-xs\",\n \"transition-[border-color,box-shadow] duration-[var(--duration-fast)]\",\n \"focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/55\",\n disabled && \"opacity-60\",\n className,\n )}\n {...props}\n >\n {children}\n </form>\n </PromptInputContext.Provider>\n );\n}\n\n/** Rows the textarea will grow to before it starts scrolling. */\nconst DEFAULT_MAX_ROWS = 8;\n\nexport interface PromptInputTextareaProps extends Omit<\n ComponentPropsWithRef<\"textarea\">,\n \"rows\"\n> {\n maxRows?: number;\n /**\n * Send on Enter, newline on Shift+Enter.\n *\n * Turn it off for a composer where Enter should always insert a newline and\n * sending is only ever an explicit button press.\n */\n submitOnEnter?: boolean;\n}\n\nexport function PromptInputTextarea({\n className,\n maxRows = DEFAULT_MAX_ROWS,\n submitOnEnter = true,\n onKeyDown,\n onChange,\n ...props\n}: PromptInputTextareaProps) {\n const { textareaId, submit, disabled, busy } = usePromptInput(\"PromptInputTextarea\");\n const ref = useRef<HTMLTextAreaElement | null>(null);\n\n // Grows with the content up to maxRows, then scrolls. Measured after layout\n // so the height is right on the first paint rather than one frame late.\n useLayoutEffect(() => {\n const textarea = ref.current;\n if (!textarea) return;\n\n textarea.style.height = \"auto\";\n const lineHeight = Number.parseFloat(getComputedStyle(textarea).lineHeight) || 20;\n const maxHeight = lineHeight * maxRows;\n textarea.style.height = `${String(Math.min(textarea.scrollHeight, maxHeight))}px`;\n textarea.style.overflowY = textarea.scrollHeight > maxHeight ? \"auto\" : \"hidden\";\n });\n\n function handleKeyDown(event: ReactKeyboardEvent<HTMLTextAreaElement>) {\n onKeyDown?.(event);\n if (event.defaultPrevented || !submitOnEnter) return;\n if (event.key !== \"Enter\" || event.shiftKey) return;\n\n // Mid-composition Enter confirms the candidate an IME is offering — for\n // Japanese, Chinese, Korean and others it is part of typing a word, not a\n // request to send. Sending here truncates the sentence and is the single\n // most common way a chat composer breaks for those users.\n if (event.nativeEvent.isComposing) return;\n\n event.preventDefault();\n submit();\n }\n\n return (\n <textarea\n ref={ref}\n id={textareaId}\n data-slot=\"prompt-input-textarea\"\n rows={1}\n disabled={disabled}\n aria-disabled={busy || undefined}\n onKeyDown={handleKeyDown}\n onChange={onChange}\n className={cn(\n \"block w-full resize-none bg-transparent px-4 py-3 text-sm outline-none\",\n \"placeholder:text-muted-foreground disabled:cursor-not-allowed\",\n focusRingInset,\n \"focus-visible:ring-0\",\n className,\n )}\n {...props}\n />\n );\n}\n\n/** The row beneath the textarea: attachments, model picker, send. */\nexport function PromptInputToolbar({ className, ...props }: ComponentPropsWithRef<\"div\">) {\n return (\n <div\n data-slot=\"prompt-input-toolbar\"\n className={cn(\"flex items-center gap-2 px-2 pb-2\", className)}\n {...props}\n />\n );\n}\n\nexport interface PromptInputSubmitProps extends ComponentPropsWithRef<\"button\"> {\n /** Label while idle. */\n label?: string;\n /** Label while a response is streaming. */\n stopLabel?: string;\n /** Called instead of submitting when busy. */\n onStop?: () => void;\n}\n\n/**\n * Send, or stop.\n *\n * One control with two jobs, because that is what the space affords and what\n * people expect. The accessible name changes with the state, so it is never\n * \"Send\" while it actually stops.\n */\nexport function PromptInputSubmit({\n className,\n label = \"Send message\",\n stopLabel = \"Stop generating\",\n onStop,\n ...props\n}: PromptInputSubmitProps) {\n const { busy, disabled } = usePromptInput(\"PromptInputSubmit\");\n\n return (\n <button\n type={busy ? \"button\" : \"submit\"}\n data-slot=\"prompt-input-submit\"\n aria-label={busy ? stopLabel : label}\n disabled={disabled}\n onClick={busy ? onStop : undefined}\n className={cn(\n \"ml-auto grid size-8 shrink-0 place-items-center rounded-lg\",\n \"bg-primary text-primary-foreground transition-colors duration-[var(--duration-fast)]\",\n \"hover:bg-primary-hover disabled:pointer-events-none disabled:opacity-55\",\n focusRing,\n className,\n )}\n {...props}\n >\n {busy ? (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\" className=\"size-3.5\">\n <rect x=\"6\" y=\"6\" width=\"12\" height=\"12\" rx=\"2\" fill=\"currentColor\" />\n </svg>\n ) : (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\" className=\"size-4\">\n <path\n d=\"M12 19V5m0 0-6 6m6-6 6 6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n )}\n </button>\n );\n}\n\nexport interface PromptInputCounterProps extends ComponentPropsWithRef<\"div\"> {\n value: number;\n max: number;\n /** Fraction of the limit past which the counter starts warning. */\n warnAt?: number;\n}\n\n/**\n * Characters used against a limit.\n *\n * The visible count updates on every keystroke; the live region does not.\n *\n * The naive version toggles `aria-live` on once the count matters, but a region\n * only announces changes that happen *while* it is live — switching it on at\n * the same moment the content changes means the one update worth hearing is the\n * one that is missed. So the region is live from the start and simply has\n * nothing in it until the limit is close, which is also what keeps ordinary\n * typing silent.\n */\nexport function PromptInputCounter({\n className,\n value,\n max,\n warnAt = 0.9,\n ...props\n}: PromptInputCounterProps) {\n const warning = value >= max * warnAt;\n const over = value > max;\n const remaining = max - value;\n\n return (\n <div\n data-slot=\"prompt-input-counter\"\n data-warning={warning || undefined}\n data-over={over || undefined}\n className={cn(\n \"text-xs tabular-nums\",\n over ? \"text-destructive\" : warning ? \"text-warning\" : \"text-muted-foreground\",\n className,\n )}\n {...props}\n >\n <span aria-hidden=\"true\">\n {warning ? `${String(value)} / ${String(max)}` : String(value)}\n </span>\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {over\n ? `Over the limit by ${String(-remaining)} characters`\n : warning\n ? `${String(remaining)} characters remaining`\n : \"\"}\n </span>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ai-prompt-input.js","names":[],"sources":["../../../src/components/ai-prompt-input/ai-prompt-input.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n createContext,\n useContext,\n useId,\n useLayoutEffect,\n useMemo,\n useRef,\n type ComponentPropsWithRef,\n type FormEvent,\n type KeyboardEvent as ReactKeyboardEvent,\n} from \"react\";\n\nimport { focusRing, focusRingInset } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * The composer.\n *\n * Handles the parts of a chat input that are easy to get subtly wrong:\n * auto-growing to fit the text up to a ceiling, sending on Enter without\n * breaking multi-line input, and — the one almost everybody misses —\n * not sending mid-composition when someone is typing with an IME.\n */\n\ninterface PromptInputContextValue {\n textareaId: string;\n submit: () => void;\n disabled: boolean;\n busy: boolean;\n}\n\nconst PromptInputContext = createContext<PromptInputContextValue | null>(null);\n\nfunction usePromptInput(component: string): PromptInputContextValue {\n const context = useContext(PromptInputContext);\n if (!context) {\n throw new Error(`${component} must be rendered inside <PromptInput>.`);\n }\n return context;\n}\n\nexport interface PromptInputProps extends Omit<ComponentPropsWithRef<\"form\">, \"onSubmit\"> {\n onSubmit?: (event: FormEvent<HTMLFormElement>) => void;\n /** Blocks submission and dims the composer. */\n disabled?: boolean;\n /** A response is in flight: submission is blocked and Stop is shown instead. */\n busy?: boolean;\n}\n\nexport function PromptInput({\n className,\n onSubmit,\n disabled = false,\n busy = false,\n children,\n ...props\n}: PromptInputProps) {\n const uid = useId();\n const formRef = useRef<HTMLFormElement | null>(null);\n\n const context = useMemo<PromptInputContextValue>(\n () => ({\n textareaId: `${uid}-textarea`,\n disabled,\n busy,\n submit: () => {\n formRef.current?.requestSubmit();\n },\n }),\n [uid, disabled, busy],\n );\n\n return (\n <PromptInputContext.Provider value={context}>\n <form\n ref={formRef}\n data-slot=\"prompt-input\"\n data-busy={busy || undefined}\n onSubmit={(event) => {\n if (disabled || busy) {\n event.preventDefault();\n return;\n }\n onSubmit?.(event);\n }}\n className={cn(\n \"rounded-xl border border-input bg-background shadow-xs\",\n \"transition-[border-color,box-shadow] duration-[var(--duration-fast)]\",\n \"focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/55\",\n disabled && \"opacity-60\",\n className,\n )}\n {...props}\n >\n {children}\n </form>\n </PromptInputContext.Provider>\n );\n}\n\n/** Rows the textarea will grow to before it starts scrolling. */\nconst DEFAULT_MAX_ROWS = 8;\n\nexport interface PromptInputTextareaProps extends Omit<\n ComponentPropsWithRef<\"textarea\">,\n \"rows\"\n> {\n maxRows?: number;\n /**\n * Send on Enter, newline on Shift+Enter.\n *\n * Turn it off for a composer where Enter should always insert a newline and\n * sending is only ever an explicit button press.\n */\n submitOnEnter?: boolean;\n}\n\nexport function PromptInputTextarea({\n className,\n maxRows = DEFAULT_MAX_ROWS,\n submitOnEnter = true,\n onKeyDown,\n onChange,\n ...props\n}: PromptInputTextareaProps) {\n const { textareaId, submit, disabled, busy } = usePromptInput(\"PromptInputTextarea\");\n const ref = useRef<HTMLTextAreaElement | null>(null);\n\n // Grows with the content up to maxRows, then scrolls. Measured after layout\n // so the height is right on the first paint rather than one frame late.\n useLayoutEffect(() => {\n const textarea = ref.current;\n if (!textarea) return;\n\n textarea.style.height = \"auto\";\n const lineHeight = Number.parseFloat(getComputedStyle(textarea).lineHeight) || 20;\n const maxHeight = lineHeight * maxRows;\n textarea.style.height = `${String(Math.min(textarea.scrollHeight, maxHeight))}px`;\n textarea.style.overflowY = textarea.scrollHeight > maxHeight ? \"auto\" : \"hidden\";\n });\n\n function handleKeyDown(event: ReactKeyboardEvent<HTMLTextAreaElement>) {\n onKeyDown?.(event);\n if (event.defaultPrevented || !submitOnEnter) return;\n if (event.key !== \"Enter\" || event.shiftKey) return;\n\n // Mid-composition Enter confirms the candidate an IME is offering — for\n // Japanese, Chinese, Korean and others it is part of typing a word, not a\n // request to send. Sending here truncates the sentence and is the single\n // most common way a chat composer breaks for those users.\n if (event.nativeEvent.isComposing) return;\n\n event.preventDefault();\n submit();\n }\n\n return (\n <textarea\n ref={ref}\n id={textareaId}\n data-slot=\"prompt-input-textarea\"\n rows={1}\n disabled={disabled}\n aria-disabled={busy || undefined}\n onKeyDown={handleKeyDown}\n onChange={onChange}\n className={cn(\n \"block w-full resize-none bg-transparent px-4 py-3 text-sm outline-none\",\n \"placeholder:text-muted-foreground disabled:cursor-not-allowed\",\n focusRingInset,\n \"focus-visible:ring-0\",\n className,\n )}\n {...props}\n />\n );\n}\n\n/** The row beneath the textarea: attachments, model picker, send. */\nexport function PromptInputToolbar({ className, ...props }: ComponentPropsWithRef<\"div\">) {\n return (\n <div\n data-slot=\"prompt-input-toolbar\"\n className={cn(\"flex items-center gap-2 px-2 pb-2\", className)}\n {...props}\n />\n );\n}\n\nexport interface PromptInputSubmitProps extends ComponentPropsWithRef<\"button\"> {\n /** Label while idle. */\n label?: string;\n /** Label while a response is streaming. */\n stopLabel?: string;\n /** Called instead of submitting when busy. */\n onStop?: () => void;\n}\n\n/**\n * Send, or stop.\n *\n * One control with two jobs, because that is what the space affords and what\n * people expect. The accessible name changes with the state, so it is never\n * \"Send\" while it actually stops.\n */\nexport function PromptInputSubmit({\n className,\n label = \"Send message\",\n stopLabel = \"Stop generating\",\n onStop,\n ...props\n}: PromptInputSubmitProps) {\n const { busy, disabled } = usePromptInput(\"PromptInputSubmit\");\n\n return (\n <button\n type={busy ? \"button\" : \"submit\"}\n data-slot=\"prompt-input-submit\"\n aria-label={busy ? stopLabel : label}\n disabled={disabled}\n onClick={busy ? onStop : undefined}\n className={cn(\n \"ms-auto grid size-8 shrink-0 place-items-center rounded-lg\",\n \"bg-primary text-primary-foreground transition-colors duration-[var(--duration-fast)]\",\n \"hover:bg-primary-hover disabled:pointer-events-none disabled:opacity-55\",\n focusRing,\n className,\n )}\n {...props}\n >\n {busy ? (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\" className=\"size-3.5\">\n <rect x=\"6\" y=\"6\" width=\"12\" height=\"12\" rx=\"2\" fill=\"currentColor\" />\n </svg>\n ) : (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\" className=\"size-4\">\n <path\n d=\"M12 19V5m0 0-6 6m6-6 6 6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n )}\n </button>\n );\n}\n\nexport interface PromptInputCounterProps extends ComponentPropsWithRef<\"div\"> {\n value: number;\n max: number;\n /** Fraction of the limit past which the counter starts warning. */\n warnAt?: number;\n}\n\n/**\n * Characters used against a limit.\n *\n * The visible count updates on every keystroke; the live region does not.\n *\n * The naive version toggles `aria-live` on once the count matters, but a region\n * only announces changes that happen *while* it is live — switching it on at\n * the same moment the content changes means the one update worth hearing is the\n * one that is missed. So the region is live from the start and simply has\n * nothing in it until the limit is close, which is also what keeps ordinary\n * typing silent.\n */\nexport function PromptInputCounter({\n className,\n value,\n max,\n warnAt = 0.9,\n ...props\n}: PromptInputCounterProps) {\n const warning = value >= max * warnAt;\n const over = value > max;\n const remaining = max - value;\n\n return (\n <div\n data-slot=\"prompt-input-counter\"\n data-warning={warning || undefined}\n data-over={over || undefined}\n className={cn(\n \"text-xs tabular-nums\",\n over ? \"text-destructive\" : warning ? \"text-warning\" : \"text-muted-foreground\",\n className,\n )}\n {...props}\n >\n <span aria-hidden=\"true\">\n {warning ? `${String(value)} / ${String(max)}` : String(value)}\n </span>\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {over\n ? `Over the limit by ${String(-remaining)} characters`\n : warning\n ? `${String(remaining)} characters remaining`\n : \"\"}\n </span>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -53,7 +53,7 @@ function ReasoningContent({ className, children, ...props }) {
|
|
|
53
53
|
className: cn("overflow-hidden", "data-[state=closed]:animate-accordion-close data-[state=open]:animate-accordion-open", className),
|
|
54
54
|
...props,
|
|
55
55
|
children: /* @__PURE__ */ jsx("div", {
|
|
56
|
-
className: "mt-2 border-
|
|
56
|
+
className: "mt-2 border-s-2 border-border ps-3 text-sm whitespace-pre-wrap text-muted-foreground",
|
|
57
57
|
children
|
|
58
58
|
})
|
|
59
59
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-reasoning.js","names":["CollapsiblePrimitive"],"sources":["../../../src/components/ai-reasoning/ai-reasoning.tsx"],"sourcesContent":["\"use client\";\n\nimport { Collapsible as CollapsiblePrimitive } from \"radix-ui\";\nimport type { ComponentPropsWithRef } from \"react\";\n\nimport { focusRing } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * The model's reasoning, tucked away.\n *\n * Collapsed by default and deliberately understated. Reasoning is supporting\n * material — surfacing it at the same weight as the answer buries the answer,\n * and most readers never want it. It stays one keystroke away for the people\n * who do.\n */\nexport type ReasoningProps = ComponentPropsWithRef<typeof CollapsiblePrimitive.Root>;\n\nexport function Reasoning({ className, ...props }: ReasoningProps) {\n return (\n <CollapsiblePrimitive.Root\n data-slot=\"reasoning\"\n className={cn(\"text-sm\", className)}\n {...props}\n />\n );\n}\n\nexport interface ReasoningTriggerProps extends ComponentPropsWithRef<\n typeof CollapsiblePrimitive.Trigger\n> {\n /** Shown while reasoning is still arriving. */\n streaming?: boolean;\n label?: string;\n streamingLabel?: string;\n}\n\nexport function ReasoningTrigger({\n className,\n streaming,\n label = \"Reasoning\",\n streamingLabel = \"Thinking…\",\n children,\n ...props\n}: ReasoningTriggerProps) {\n return (\n <CollapsiblePrimitive.Trigger\n data-slot=\"reasoning-trigger\"\n className={cn(\n \"flex items-center gap-1.5 rounded-md text-xs text-muted-foreground\",\n \"transition-colors duration-[var(--duration-fast)] hover:text-foreground\",\n \"[&[data-state=open]>svg:last-child]:rotate-180\",\n focusRing,\n className,\n )}\n {...props}\n >\n <svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\" className=\"size-3.5 shrink-0\">\n <path\n d=\"M9.5 21h5M12 3a6 6 0 0 1 3.6 10.8c-.6.5-.9 1.1-1 1.7l-.1.5h-5l-.1-.5c-.1-.6-.4-1.2-1-1.7A6 6 0 0 1 12 3Z\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n {children ?? (streaming ? streamingLabel : label)}\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n aria-hidden=\"true\"\n className=\"size-3.5 shrink-0 transition-transform duration-[var(--duration-normal)]\"\n >\n <path\n d=\"m6 9 6 6 6-6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </CollapsiblePrimitive.Trigger>\n );\n}\n\nexport function ReasoningContent({\n className,\n children,\n ...props\n}: ComponentPropsWithRef<typeof CollapsiblePrimitive.Content>) {\n return (\n <CollapsiblePrimitive.Content\n data-slot=\"reasoning-content\"\n className={cn(\n \"overflow-hidden\",\n \"data-[state=closed]:animate-accordion-close data-[state=open]:animate-accordion-open\",\n className,\n )}\n {...props}\n >\n <div className=\"mt-2 border-
|
|
1
|
+
{"version":3,"file":"ai-reasoning.js","names":["CollapsiblePrimitive"],"sources":["../../../src/components/ai-reasoning/ai-reasoning.tsx"],"sourcesContent":["\"use client\";\n\nimport { Collapsible as CollapsiblePrimitive } from \"radix-ui\";\nimport type { ComponentPropsWithRef } from \"react\";\n\nimport { focusRing } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * The model's reasoning, tucked away.\n *\n * Collapsed by default and deliberately understated. Reasoning is supporting\n * material — surfacing it at the same weight as the answer buries the answer,\n * and most readers never want it. It stays one keystroke away for the people\n * who do.\n */\nexport type ReasoningProps = ComponentPropsWithRef<typeof CollapsiblePrimitive.Root>;\n\nexport function Reasoning({ className, ...props }: ReasoningProps) {\n return (\n <CollapsiblePrimitive.Root\n data-slot=\"reasoning\"\n className={cn(\"text-sm\", className)}\n {...props}\n />\n );\n}\n\nexport interface ReasoningTriggerProps extends ComponentPropsWithRef<\n typeof CollapsiblePrimitive.Trigger\n> {\n /** Shown while reasoning is still arriving. */\n streaming?: boolean;\n label?: string;\n streamingLabel?: string;\n}\n\nexport function ReasoningTrigger({\n className,\n streaming,\n label = \"Reasoning\",\n streamingLabel = \"Thinking…\",\n children,\n ...props\n}: ReasoningTriggerProps) {\n return (\n <CollapsiblePrimitive.Trigger\n data-slot=\"reasoning-trigger\"\n className={cn(\n \"flex items-center gap-1.5 rounded-md text-xs text-muted-foreground\",\n \"transition-colors duration-[var(--duration-fast)] hover:text-foreground\",\n \"[&[data-state=open]>svg:last-child]:rotate-180\",\n focusRing,\n className,\n )}\n {...props}\n >\n <svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\" className=\"size-3.5 shrink-0\">\n <path\n d=\"M9.5 21h5M12 3a6 6 0 0 1 3.6 10.8c-.6.5-.9 1.1-1 1.7l-.1.5h-5l-.1-.5c-.1-.6-.4-1.2-1-1.7A6 6 0 0 1 12 3Z\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n {children ?? (streaming ? streamingLabel : label)}\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n aria-hidden=\"true\"\n className=\"size-3.5 shrink-0 transition-transform duration-[var(--duration-normal)]\"\n >\n <path\n d=\"m6 9 6 6 6-6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </CollapsiblePrimitive.Trigger>\n );\n}\n\nexport function ReasoningContent({\n className,\n children,\n ...props\n}: ComponentPropsWithRef<typeof CollapsiblePrimitive.Content>) {\n return (\n <CollapsiblePrimitive.Content\n data-slot=\"reasoning-content\"\n className={cn(\n \"overflow-hidden\",\n \"data-[state=closed]:animate-accordion-close data-[state=open]:animate-accordion-open\",\n className,\n )}\n {...props}\n >\n <div className=\"mt-2 border-s-2 border-border ps-3 text-sm whitespace-pre-wrap text-muted-foreground\">\n {children}\n </div>\n </CollapsiblePrimitive.Content>\n );\n}\n"],"mappings":";;;;;;AAkBA,SAAgB,UAAU,EAAE,WAAW,GAAG,SAAyB;CACjE,OACE,oBAACA,YAAqB,MAAtB;EACE,aAAU;EACV,WAAW,GAAG,WAAW,SAAS;EAClC,GAAI;CACL,CAAA;AAEL;AAWA,SAAgB,iBAAiB,EAC/B,WACA,WACA,QAAQ,aACR,iBAAiB,aACjB,UACA,GAAG,SACqB;CACxB,OACE,qBAACA,YAAqB,SAAtB;EACE,aAAU;EACV,WAAW,GACT,sEACA,2EACA,kDACA,WACA,SACF;EACA,GAAI;EATN,UAAA;GAWE,oBAAC,OAAD;IAAK,SAAQ;IAAY,MAAK;IAAO,eAAY;IAAO,WAAU;IAChE,UAAA,oBAAC,QAAD;KACE,GAAE;KACF,QAAO;KACP,aAAY;KACZ,eAAc;KACd,gBAAe;IAChB,CAAA;GACE,CAAA;GACJ,aAAa,YAAY,iBAAiB;GAC3C,oBAAC,OAAD;IACE,SAAQ;IACR,MAAK;IACL,eAAY;IACZ,WAAU;IAEV,UAAA,oBAAC,QAAD;KACE,GAAE;KACF,QAAO;KACP,aAAY;KACZ,eAAc;KACd,gBAAe;IAChB,CAAA;GACE,CAAA;EACuB;;AAElC;AAEA,SAAgB,iBAAiB,EAC/B,WACA,UACA,GAAG,SAC0D;CAC7D,OACE,oBAACA,YAAqB,SAAtB;EACE,aAAU;EACV,WAAW,GACT,mBACA,wFACA,SACF;EACA,GAAI;EAEJ,UAAA,oBAAC,OAAD;GAAK,WAAU;GACZ;EACE,CAAA;CACuB,CAAA;AAElC"}
|
|
@@ -6,7 +6,7 @@ function Response({ className, streaming, children, ...props }) {
|
|
|
6
6
|
return /* @__PURE__ */ jsxs("div", {
|
|
7
7
|
"data-slot": "response",
|
|
8
8
|
"data-streaming": streaming || void 0,
|
|
9
|
-
className: cn("text-sm leading-relaxed whitespace-pre-wrap text-foreground", "[&_a]:text-primary [&_a]:underline [&_a]:underline-offset-4", "[&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-[0.9em]", "[&_ol]:my-2 [&_ol]:list-decimal [&_ol]:
|
|
9
|
+
className: cn("text-sm leading-relaxed whitespace-pre-wrap text-foreground", "[&_a]:text-primary [&_a]:underline [&_a]:underline-offset-4", "[&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-[0.9em]", "[&_ol]:my-2 [&_ol]:list-decimal [&_ol]:ps-5 [&_ul]:my-2 [&_ul]:list-disc [&_ul]:ps-5", "[&_p+p]:mt-3", className),
|
|
10
10
|
...props,
|
|
11
11
|
children: [children, streaming ? /* @__PURE__ */ jsx(ResponseCaret, {}) : null]
|
|
12
12
|
});
|
|
@@ -18,7 +18,7 @@ function ResponseCaret({ className, ...props }) {
|
|
|
18
18
|
"data-slot": "response-caret",
|
|
19
19
|
"aria-hidden": "true",
|
|
20
20
|
"data-motion": "indicator",
|
|
21
|
-
className: cn("
|
|
21
|
+
className: cn("ms-0.5 inline-block h-[1em] w-[2px] translate-y-[0.15em] animate-caret bg-current align-baseline", className),
|
|
22
22
|
...props
|
|
23
23
|
});
|
|
24
24
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-response.js","names":[],"sources":["../../../src/components/ai-response/ai-response.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ComponentPropsWithRef } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * The text of an assistant response.\n *\n * Renders plain text with paragraph whitespace preserved. Markdown is\n * deliberately not parsed here: a Markdown renderer is a dependency, a security\n * decision about raw HTML, and a styling surface all at once, and every product\n * makes those differently. Pass already-rendered content as children when you\n * need it — the prose styling below applies either way.\n *\n * Not a live region, and not by omission. See the note on `Conversation`:\n * announcing streamed text token by token is unusable with a screen reader.\n */\nexport interface ResponseProps extends ComponentPropsWithRef<\"div\"> {\n /** Shows the caret. Purely visual — it carries no announcement. */\n streaming?: boolean;\n}\n\nexport function Response({ className, streaming, children, ...props }: ResponseProps) {\n return (\n <div\n data-slot=\"response\"\n data-streaming={streaming || undefined}\n className={cn(\n \"text-sm leading-relaxed whitespace-pre-wrap text-foreground\",\n // Minimal prose styling for consumers who pass rendered Markdown.\n \"[&_a]:text-primary [&_a]:underline [&_a]:underline-offset-4\",\n \"[&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-[0.9em]\",\n \"[&_ol]:my-2 [&_ol]:list-decimal [&_ol]:
|
|
1
|
+
{"version":3,"file":"ai-response.js","names":[],"sources":["../../../src/components/ai-response/ai-response.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ComponentPropsWithRef } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * The text of an assistant response.\n *\n * Renders plain text with paragraph whitespace preserved. Markdown is\n * deliberately not parsed here: a Markdown renderer is a dependency, a security\n * decision about raw HTML, and a styling surface all at once, and every product\n * makes those differently. Pass already-rendered content as children when you\n * need it — the prose styling below applies either way.\n *\n * Not a live region, and not by omission. See the note on `Conversation`:\n * announcing streamed text token by token is unusable with a screen reader.\n */\nexport interface ResponseProps extends ComponentPropsWithRef<\"div\"> {\n /** Shows the caret. Purely visual — it carries no announcement. */\n streaming?: boolean;\n}\n\nexport function Response({ className, streaming, children, ...props }: ResponseProps) {\n return (\n <div\n data-slot=\"response\"\n data-streaming={streaming || undefined}\n className={cn(\n \"text-sm leading-relaxed whitespace-pre-wrap text-foreground\",\n // Minimal prose styling for consumers who pass rendered Markdown.\n \"[&_a]:text-primary [&_a]:underline [&_a]:underline-offset-4\",\n \"[&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-[0.9em]\",\n \"[&_ol]:my-2 [&_ol]:list-decimal [&_ol]:ps-5 [&_ul]:my-2 [&_ul]:list-disc [&_ul]:ps-5\",\n \"[&_p+p]:mt-3\",\n className,\n )}\n {...props}\n >\n {children}\n {streaming ? <ResponseCaret /> : null}\n </div>\n );\n}\n\n/** The trailing cursor. Decorative — the streaming state is announced, if at\n * all, through ConversationStatus. */\nexport function ResponseCaret({ className, ...props }: ComponentPropsWithRef<\"span\">) {\n return (\n <span\n data-slot=\"response-caret\"\n aria-hidden=\"true\"\n data-motion=\"indicator\"\n className={cn(\n \"ms-0.5 inline-block h-[1em] w-[2px] translate-y-[0.15em] animate-caret bg-current align-baseline\",\n className,\n )}\n {...props}\n />\n );\n}\n\n/**\n * Shown while waiting for the first token.\n *\n * A distinct state from streaming: nothing has arrived yet, so there is no text\n * to show a caret after. Labelled for assistive technology because, unlike the\n * caret, this is the only thing on screen.\n */\nexport interface ThinkingIndicatorProps extends ComponentPropsWithRef<\"div\"> {\n label?: string;\n}\n\nexport function ThinkingIndicator({\n className,\n label = \"Thinking\",\n ...props\n}: ThinkingIndicatorProps) {\n return (\n <div\n data-slot=\"thinking-indicator\"\n className={cn(\"flex items-center gap-1.5 text-sm text-muted-foreground\", className)}\n {...props}\n >\n <span className=\"sr-only\">{label}</span>\n {[0, 1, 2].map((index) => (\n <span\n key={index}\n aria-hidden=\"true\"\n className=\"size-1.5 animate-pulse-soft rounded-full bg-current\"\n style={{ animationDelay: `${String(index * 160)}ms` }}\n />\n ))}\n </div>\n );\n}\n"],"mappings":";;;;AAuBA,SAAgB,SAAS,EAAE,WAAW,WAAW,UAAU,GAAG,SAAwB;CACpF,OACE,qBAAC,OAAD;EACE,aAAU;EACV,kBAAgB,aAAa,KAAA;EAC7B,WAAW,GACT,+DAEA,+DACA,6GACA,wFACA,gBACA,SACF;EACA,GAAI;EAZN,UAAA,CAcG,UACA,YAAY,oBAAC,eAAD,CAAgB,CAAA,IAAI,IAC9B;;AAET;;;AAIA,SAAgB,cAAc,EAAE,WAAW,GAAG,SAAwC;CACpF,OACE,oBAAC,QAAD;EACE,aAAU;EACV,eAAY;EACZ,eAAY;EACZ,WAAW,GACT,oGACA,SACF;EACA,GAAI;CACL,CAAA;AAEL;AAaA,SAAgB,kBAAkB,EAChC,WACA,QAAQ,YACR,GAAG,SACsB;CACzB,OACE,qBAAC,OAAD;EACE,aAAU;EACV,WAAW,GAAG,2DAA2D,SAAS;EAClF,GAAI;EAHN,UAAA,CAKE,oBAAC,QAAD;GAAM,WAAU;GAAW,UAAA;EAAY,CAAA,GACtC;GAAC;GAAG;GAAG;EAAC,CAAC,CAAC,KAAK,UACd,oBAAC,QAAD;GAEE,eAAY;GACZ,WAAU;GACV,OAAO,EAAE,gBAAgB,GAAG,OAAO,QAAQ,GAAG,EAAE,IAAI;EACrD,GAJM,KAIN,CACF,CACE;;AAET"}
|
|
@@ -31,7 +31,7 @@ function Tool({ className, status = "pending", ...props }) {
|
|
|
31
31
|
function ToolHeader({ className, name, status = "pending", statusLabel, icon, ...props }) {
|
|
32
32
|
return /* @__PURE__ */ jsxs(Collapsible.Trigger, {
|
|
33
33
|
"data-slot": "tool-header",
|
|
34
|
-
className: cn("flex w-full items-center gap-2 px-3 py-2 text-
|
|
34
|
+
className: cn("flex w-full items-center gap-2 px-3 py-2 text-start", "transition-colors duration-[var(--duration-fast)] hover:bg-accent/50", "[&[data-state=open]>svg:last-child]:rotate-180", focusRing, className),
|
|
35
35
|
...props,
|
|
36
36
|
children: [
|
|
37
37
|
icon ?? /* @__PURE__ */ jsx(ToolIcon, { status }),
|
|
@@ -41,7 +41,7 @@ function ToolHeader({ className, name, status = "pending", statusLabel, icon, ..
|
|
|
41
41
|
}),
|
|
42
42
|
/* @__PURE__ */ jsx("span", {
|
|
43
43
|
"data-slot": "tool-status",
|
|
44
|
-
className: cn("
|
|
44
|
+
className: cn("ms-auto text-xs", status === "error" ? "text-destructive" : "text-muted-foreground"),
|
|
45
45
|
children: statusLabel ?? STATUS_LABEL[status]
|
|
46
46
|
}),
|
|
47
47
|
/* @__PURE__ */ jsx("svg", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-tool.js","names":["CollapsiblePrimitive"],"sources":["../../../src/components/ai-tool/ai-tool.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Collapsible as CollapsiblePrimitive } from \"radix-ui\";\nimport type { ComponentPropsWithRef, ReactNode } from \"react\";\n\nimport { focusRing } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * A tool the model called, and what came back.\n *\n * Collapsed by default: a tool call is provenance, not the answer. Someone\n * checking why a response says what it does needs to open it; everybody else\n * needs it to stay out of the way.\n *\n * The status is the whole point of the header, so it is text, never a coloured\n * dot alone.\n */\n\nexport type ToolStatus = \"pending\" | \"running\" | \"success\" | \"error\";\n\nconst STATUS_LABEL: Record<ToolStatus, string> = {\n pending: \"Queued\",\n running: \"Running\",\n success: \"Completed\",\n error: \"Failed\",\n};\n\nconst toolVariants = cva(\"overflow-hidden rounded-lg border text-sm\", {\n variants: {\n status: {\n pending: \"border-border bg-muted/30\",\n running: \"border-info/30 bg-info/5\",\n success: \"border-border bg-muted/30\",\n error: \"border-destructive/30 bg-destructive/5\",\n },\n },\n defaultVariants: {\n status: \"pending\",\n },\n});\n\nexport interface ToolProps\n extends\n ComponentPropsWithRef<typeof CollapsiblePrimitive.Root>,\n VariantProps<typeof toolVariants> {\n status?: ToolStatus;\n}\n\nexport function Tool({ className, status = \"pending\", ...props }: ToolProps) {\n return (\n <CollapsiblePrimitive.Root\n data-slot=\"tool\"\n data-status={status}\n className={cn(toolVariants({ status }), className)}\n {...props}\n />\n );\n}\n\nexport interface ToolHeaderProps extends ComponentPropsWithRef<\n typeof CollapsiblePrimitive.Trigger\n> {\n /** The tool's name, as the model called it. */\n name: string;\n status?: ToolStatus;\n /** Overrides the status wording. */\n statusLabel?: string;\n icon?: ReactNode;\n}\n\nexport function ToolHeader({\n className,\n name,\n status = \"pending\",\n statusLabel,\n icon,\n ...props\n}: ToolHeaderProps) {\n return (\n <CollapsiblePrimitive.Trigger\n data-slot=\"tool-header\"\n className={cn(\n \"flex w-full items-center gap-2 px-3 py-2 text-
|
|
1
|
+
{"version":3,"file":"ai-tool.js","names":["CollapsiblePrimitive"],"sources":["../../../src/components/ai-tool/ai-tool.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Collapsible as CollapsiblePrimitive } from \"radix-ui\";\nimport type { ComponentPropsWithRef, ReactNode } from \"react\";\n\nimport { focusRing } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * A tool the model called, and what came back.\n *\n * Collapsed by default: a tool call is provenance, not the answer. Someone\n * checking why a response says what it does needs to open it; everybody else\n * needs it to stay out of the way.\n *\n * The status is the whole point of the header, so it is text, never a coloured\n * dot alone.\n */\n\nexport type ToolStatus = \"pending\" | \"running\" | \"success\" | \"error\";\n\nconst STATUS_LABEL: Record<ToolStatus, string> = {\n pending: \"Queued\",\n running: \"Running\",\n success: \"Completed\",\n error: \"Failed\",\n};\n\nconst toolVariants = cva(\"overflow-hidden rounded-lg border text-sm\", {\n variants: {\n status: {\n pending: \"border-border bg-muted/30\",\n running: \"border-info/30 bg-info/5\",\n success: \"border-border bg-muted/30\",\n error: \"border-destructive/30 bg-destructive/5\",\n },\n },\n defaultVariants: {\n status: \"pending\",\n },\n});\n\nexport interface ToolProps\n extends\n ComponentPropsWithRef<typeof CollapsiblePrimitive.Root>,\n VariantProps<typeof toolVariants> {\n status?: ToolStatus;\n}\n\nexport function Tool({ className, status = \"pending\", ...props }: ToolProps) {\n return (\n <CollapsiblePrimitive.Root\n data-slot=\"tool\"\n data-status={status}\n className={cn(toolVariants({ status }), className)}\n {...props}\n />\n );\n}\n\nexport interface ToolHeaderProps extends ComponentPropsWithRef<\n typeof CollapsiblePrimitive.Trigger\n> {\n /** The tool's name, as the model called it. */\n name: string;\n status?: ToolStatus;\n /** Overrides the status wording. */\n statusLabel?: string;\n icon?: ReactNode;\n}\n\nexport function ToolHeader({\n className,\n name,\n status = \"pending\",\n statusLabel,\n icon,\n ...props\n}: ToolHeaderProps) {\n return (\n <CollapsiblePrimitive.Trigger\n data-slot=\"tool-header\"\n className={cn(\n \"flex w-full items-center gap-2 px-3 py-2 text-start\",\n \"transition-colors duration-[var(--duration-fast)] hover:bg-accent/50\",\n \"[&[data-state=open]>svg:last-child]:rotate-180\",\n focusRing,\n className,\n )}\n {...props}\n >\n {icon ?? <ToolIcon status={status} />}\n <span className=\"font-mono text-xs font-medium\">{name}</span>\n {/* Status in words. A coloured dot alone says nothing to a screen reader\n and nothing to anyone who cannot distinguish the colours. */}\n <span\n data-slot=\"tool-status\"\n className={cn(\n \"ms-auto text-xs\",\n status === \"error\" ? \"text-destructive\" : \"text-muted-foreground\",\n )}\n >\n {statusLabel ?? STATUS_LABEL[status]}\n </span>\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n aria-hidden=\"true\"\n className=\"size-3.5 shrink-0 text-muted-foreground transition-transform duration-[var(--duration-normal)]\"\n >\n <path\n d=\"m6 9 6 6 6-6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </CollapsiblePrimitive.Trigger>\n );\n}\n\nfunction ToolIcon({ status }: { status: ToolStatus }) {\n if (status === \"running\") {\n return (\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n aria-hidden=\"true\"\n className=\"size-3.5 shrink-0 animate-spin text-info\"\n >\n <circle cx=\"12\" cy=\"12\" r=\"9\" stroke=\"currentColor\" strokeWidth=\"2.5\" opacity=\"0.25\" />\n <path\n d=\"M12 3a9 9 0 0 1 9 9\"\n stroke=\"currentColor\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"round\"\n />\n </svg>\n );\n }\n\n return (\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n aria-hidden=\"true\"\n className={cn(\n \"size-3.5 shrink-0\",\n status === \"error\" ? \"text-destructive\" : \"text-muted-foreground\",\n )}\n >\n <path\n d={\n status === \"success\"\n ? \"m5 13 4 4L19 7\"\n : status === \"error\"\n ? \"M12 8v5m0 3h.01M10.3 4.3 2.6 18a2 2 0 0 0 1.7 3h15.4a2 2 0 0 0 1.7-3L13.7 4.3a2 2 0 0 0-3.4 0Z\"\n : \"M14.7 6.3a4 4 0 0 1-5 5L5 16v3h3l4.7-4.7a4 4 0 0 1 5-5l-2.3 2.3\"\n }\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n );\n}\n\nexport function ToolContent({\n className,\n ...props\n}: ComponentPropsWithRef<typeof CollapsiblePrimitive.Content>) {\n return (\n <CollapsiblePrimitive.Content\n data-slot=\"tool-content\"\n className={cn(\n \"overflow-hidden border-t border-border\",\n \"data-[state=closed]:animate-accordion-close data-[state=open]:animate-accordion-open\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport interface ToolSectionProps extends ComponentPropsWithRef<\"div\"> {\n label: string;\n}\n\n/** A labelled block inside the disclosure: the arguments, or the result. */\nexport function ToolSection({ className, label, children, ...props }: ToolSectionProps) {\n return (\n <div\n data-slot=\"tool-section\"\n className={cn(\"border-b border-border/60 px-3 py-2 last:border-b-0\", className)}\n {...props}\n >\n <p className=\"mb-1 text-2xs font-medium tracking-wide text-muted-foreground uppercase\">\n {label}\n </p>\n {children}\n </div>\n );\n}\n\n/**\n * Serialised arguments or output.\n *\n * A `<pre>` with a tabindex, because JSON of any size scrolls and a scrollable\n * box that cannot be focused is unreachable by keyboard.\n */\nexport interface ToolPayloadProps extends ComponentPropsWithRef<\"pre\"> {\n label?: string;\n}\n\nexport function ToolPayload({ className, label = \"Payload\", ...props }: ToolPayloadProps) {\n return (\n <pre\n data-slot=\"tool-payload\"\n tabIndex={0}\n role=\"region\"\n aria-label={label}\n className={cn(\n \"max-h-56 overflow-auto rounded-md bg-muted/60 p-2 font-mono text-xs leading-relaxed\",\n focusRing,\n className,\n )}\n {...props}\n />\n );\n}\n\nexport { toolVariants };\n"],"mappings":";;;;;;;AAsBA,MAAM,eAA2C;CAC/C,SAAS;CACT,SAAS;CACT,SAAS;CACT,OAAO;AACT;AAEA,MAAM,eAAe,IAAI,6CAA6C;CACpE,UAAU,EACR,QAAQ;EACN,SAAS;EACT,SAAS;EACT,SAAS;EACT,OAAO;CACT,EACF;CACA,iBAAiB,EACf,QAAQ,UACV;AACF,CAAC;AASD,SAAgB,KAAK,EAAE,WAAW,SAAS,WAAW,GAAG,SAAoB;CAC3E,OACE,oBAACA,YAAqB,MAAtB;EACE,aAAU;EACV,eAAa;EACb,WAAW,GAAG,aAAa,EAAE,OAAO,CAAC,GAAG,SAAS;EACjD,GAAI;CACL,CAAA;AAEL;AAaA,SAAgB,WAAW,EACzB,WACA,MACA,SAAS,WACT,aACA,MACA,GAAG,SACe;CAClB,OACE,qBAACA,YAAqB,SAAtB;EACE,aAAU;EACV,WAAW,GACT,uDACA,wEACA,kDACA,WACA,SACF;EACA,GAAI;EATN,UAAA;GAWG,QAAQ,oBAAC,UAAD,EAAkB,OAAS,CAAA;GACpC,oBAAC,QAAD;IAAM,WAAU;IAAiC,UAAA;GAAW,CAAA;GAG5D,oBAAC,QAAD;IACE,aAAU;IACV,WAAW,GACT,mBACA,WAAW,UAAU,qBAAqB,uBAC5C;IAEC,UAAA,eAAe,aAAa;GACzB,CAAA;GACN,oBAAC,OAAD;IACE,SAAQ;IACR,MAAK;IACL,eAAY;IACZ,WAAU;IAEV,UAAA,oBAAC,QAAD;KACE,GAAE;KACF,QAAO;KACP,aAAY;KACZ,eAAc;KACd,gBAAe;IAChB,CAAA;GACE,CAAA;EACuB;;AAElC;AAEA,SAAS,SAAS,EAAE,UAAkC;CACpD,IAAI,WAAW,WACb,OACE,qBAAC,OAAD;EACE,SAAQ;EACR,MAAK;EACL,eAAY;EACZ,WAAU;EAJZ,UAAA,CAME,oBAAC,UAAD;GAAQ,IAAG;GAAK,IAAG;GAAK,GAAE;GAAI,QAAO;GAAe,aAAY;GAAM,SAAQ;EAAQ,CAAA,GACtF,oBAAC,QAAD;GACE,GAAE;GACF,QAAO;GACP,aAAY;GACZ,eAAc;EACf,CAAA,CACE;;CAIT,OACE,oBAAC,OAAD;EACE,SAAQ;EACR,MAAK;EACL,eAAY;EACZ,WAAW,GACT,qBACA,WAAW,UAAU,qBAAqB,uBAC5C;EAEA,UAAA,oBAAC,QAAD;GACE,GACE,WAAW,YACP,mBACA,WAAW,UACT,mGACA;GAER,QAAO;GACP,aAAY;GACZ,eAAc;GACd,gBAAe;EAChB,CAAA;CACE,CAAA;AAET;AAEA,SAAgB,YAAY,EAC1B,WACA,GAAG,SAC0D;CAC7D,OACE,oBAACA,YAAqB,SAAtB;EACE,aAAU;EACV,WAAW,GACT,0CACA,wFACA,SACF;EACA,GAAI;CACL,CAAA;AAEL;;AAOA,SAAgB,YAAY,EAAE,WAAW,OAAO,UAAU,GAAG,SAA2B;CACtF,OACE,qBAAC,OAAD;EACE,aAAU;EACV,WAAW,GAAG,uDAAuD,SAAS;EAC9E,GAAI;EAHN,UAAA,CAKE,oBAAC,KAAD;GAAG,WAAU;GACV,UAAA;EACA,CAAA,GACF,QACE;;AAET;AAYA,SAAgB,YAAY,EAAE,WAAW,QAAQ,WAAW,GAAG,SAA2B;CACxF,OACE,oBAAC,OAAD;EACE,aAAU;EACV,UAAU;EACV,MAAK;EACL,cAAY;EACZ,WAAW,GACT,uFACA,WACA,SACF;EACA,GAAI;CACL,CAAA;AAEL"}
|
|
@@ -2,7 +2,7 @@ import { VariantProps } from "class-variance-authority";
|
|
|
2
2
|
import { ComponentPropsWithRef } from "react";
|
|
3
3
|
//#region src/components/alert/alert.d.ts
|
|
4
4
|
declare const alertVariants: (props?: ({
|
|
5
|
-
variant?: "
|
|
5
|
+
variant?: "default" | "destructive" | "success" | "warning" | "info" | null | undefined;
|
|
6
6
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
7
7
|
/**
|
|
8
8
|
* How urgently the alert should interrupt a screen reader.
|
|
@@ -3,7 +3,7 @@ import { VariantProps } from "class-variance-authority";
|
|
|
3
3
|
import { ComponentPropsWithRef } from "react";
|
|
4
4
|
//#region src/components/avatar/avatar.d.ts
|
|
5
5
|
declare const avatarVariants: (props?: ({
|
|
6
|
-
size?: "
|
|
6
|
+
size?: "sm" | "md" | "xs" | "lg" | "xl" | null | undefined;
|
|
7
7
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
8
8
|
interface AvatarProps extends ComponentPropsWithRef<typeof Avatar.Root>, VariantProps<typeof avatarVariants> {}
|
|
9
9
|
/** Image representation of a user or entity, with a text fallback. */
|
|
@@ -2,7 +2,7 @@ import { VariantProps } from "class-variance-authority";
|
|
|
2
2
|
import { ComponentPropsWithRef } from "react";
|
|
3
3
|
//#region src/components/badge/badge.d.ts
|
|
4
4
|
declare const badgeVariants: (props?: ({
|
|
5
|
-
variant?: "
|
|
5
|
+
variant?: "default" | "secondary" | "outline" | "destructive" | "success" | "warning" | "info" | null | undefined;
|
|
6
6
|
size?: "sm" | "md" | null | undefined;
|
|
7
7
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
8
8
|
interface BadgeProps extends ComponentPropsWithRef<"span">, VariantProps<typeof badgeVariants> {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ComponentPropsWithRef } from "react";
|
|
2
|
+
//#region src/components/breadcrumb/breadcrumb.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Where this page sits in the hierarchy.
|
|
5
|
+
*
|
|
6
|
+
* Three things separate a breadcrumb that works from one that only looks like
|
|
7
|
+
* one, and all three are about the last item.
|
|
8
|
+
*
|
|
9
|
+
* It is not a link. A link to the page you are already on is a control that
|
|
10
|
+
* does nothing, and in a screen reader's list of links it is indistinguishable
|
|
11
|
+
* from the ones that go somewhere.
|
|
12
|
+
*
|
|
13
|
+
* It carries `aria-current="page"`. Bolding it says "you are here" to people
|
|
14
|
+
* who can see it and to nobody else.
|
|
15
|
+
*
|
|
16
|
+
* The separators are hidden. A slash between every item is read aloud as
|
|
17
|
+
* "slash" — "Home slash Projects slash Settings" — which is the punctuation of
|
|
18
|
+
* the design leaking into the content.
|
|
19
|
+
*/
|
|
20
|
+
declare function Breadcrumb({ className, ...props }: ComponentPropsWithRef<"nav">): import("react").JSX.Element;
|
|
21
|
+
declare function BreadcrumbList({ className, ...props }: ComponentPropsWithRef<"ol">): import("react").JSX.Element;
|
|
22
|
+
declare function BreadcrumbItem({ className, ...props }: ComponentPropsWithRef<"li">): import("react").JSX.Element;
|
|
23
|
+
interface BreadcrumbLinkProps extends ComponentPropsWithRef<"a"> {
|
|
24
|
+
/** Render the child as the link, for a router's own component. */
|
|
25
|
+
asChild?: boolean;
|
|
26
|
+
}
|
|
27
|
+
declare function BreadcrumbLink({ className, asChild, ...props }: BreadcrumbLinkProps): import("react").JSX.Element;
|
|
28
|
+
/**
|
|
29
|
+
* The page you are on.
|
|
30
|
+
*
|
|
31
|
+
* A span, not a link: there is nowhere to go. `aria-current="page"` is what
|
|
32
|
+
* says "you are here" to anything that is not looking at the font weight.
|
|
33
|
+
*/
|
|
34
|
+
declare function BreadcrumbPage({ className, ...props }: ComponentPropsWithRef<"span">): import("react").JSX.Element;
|
|
35
|
+
/**
|
|
36
|
+
* The mark between items.
|
|
37
|
+
*
|
|
38
|
+
* Hidden from assistive technology and given no accessible name: the list
|
|
39
|
+
* structure already conveys the sequence, and reading "slash" between every
|
|
40
|
+
* item is the design's punctuation leaking into the content.
|
|
41
|
+
*/
|
|
42
|
+
declare function BreadcrumbSeparator({ className, children, ...props }: ComponentPropsWithRef<"li">): import("react").JSX.Element;
|
|
43
|
+
/**
|
|
44
|
+
* Stands in for the middle of a long trail.
|
|
45
|
+
*
|
|
46
|
+
* Named rather than hidden, because unlike a separator it is *content*: it
|
|
47
|
+
* says levels have been left out. A bare "…" announces as nothing at all.
|
|
48
|
+
*/
|
|
49
|
+
declare function BreadcrumbEllipsis({ className, ...props }: ComponentPropsWithRef<"span">): import("react").JSX.Element;
|
|
50
|
+
//#endregion
|
|
51
|
+
export { Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbLinkProps, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator };
|
|
52
|
+
//# sourceMappingURL=breadcrumb.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"breadcrumb.d.ts","names":[],"sources":["../../../src/components/breadcrumb/breadcrumb.tsx"],"mappings":";;;;;;;;;;;;;;;;;;;iBAwBgB,aAAa,cAAc,SAAS,+CAA4B,IAAA;iBAahE,iBAAiB,cAAc,SAAS,8CAA2B,IAAA;iBAanE,iBAAiB,cAAc,SAAS,8CAA2B,IAAA;UAUlE,4BAA4B;;EAE3C;;iBAGc,iBAAiB,WAAW,YAAoB,SAAS,sCAAmB,IAAA;;;;;;;iBAuB5E,iBAAiB,cAAc,SAAS,gDAA6B,IAAA;;;;;;;;iBAkBrE,sBACd,WACA,aACG,SACF,8CAA2B,IAAA;;;;;;;iBA8Bd,qBAAqB,cAAc,SAAS,gDAA6B,IAAA"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { focusRing } from "../../lib/styles.js";
|
|
2
|
+
import { cn } from "../../lib/utils.js";
|
|
3
|
+
import { Slot } from "radix-ui";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/components/breadcrumb/breadcrumb.tsx
|
|
6
|
+
/**
|
|
7
|
+
* Where this page sits in the hierarchy.
|
|
8
|
+
*
|
|
9
|
+
* Three things separate a breadcrumb that works from one that only looks like
|
|
10
|
+
* one, and all three are about the last item.
|
|
11
|
+
*
|
|
12
|
+
* It is not a link. A link to the page you are already on is a control that
|
|
13
|
+
* does nothing, and in a screen reader's list of links it is indistinguishable
|
|
14
|
+
* from the ones that go somewhere.
|
|
15
|
+
*
|
|
16
|
+
* It carries `aria-current="page"`. Bolding it says "you are here" to people
|
|
17
|
+
* who can see it and to nobody else.
|
|
18
|
+
*
|
|
19
|
+
* The separators are hidden. A slash between every item is read aloud as
|
|
20
|
+
* "slash" — "Home slash Projects slash Settings" — which is the punctuation of
|
|
21
|
+
* the design leaking into the content.
|
|
22
|
+
*/
|
|
23
|
+
function Breadcrumb({ className, ...props }) {
|
|
24
|
+
return /* @__PURE__ */ jsx("nav", {
|
|
25
|
+
"aria-label": "Breadcrumb",
|
|
26
|
+
"data-slot": "breadcrumb",
|
|
27
|
+
className: cn("min-w-0", className),
|
|
28
|
+
...props
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function BreadcrumbList({ className, ...props }) {
|
|
32
|
+
return /* @__PURE__ */ jsx("ol", {
|
|
33
|
+
"data-slot": "breadcrumb-list",
|
|
34
|
+
className: cn("flex flex-wrap items-center gap-1.5 text-sm text-muted-foreground", className),
|
|
35
|
+
...props
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function BreadcrumbItem({ className, ...props }) {
|
|
39
|
+
return /* @__PURE__ */ jsx("li", {
|
|
40
|
+
"data-slot": "breadcrumb-item",
|
|
41
|
+
className: cn("inline-flex items-center gap-1.5", className),
|
|
42
|
+
...props
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function BreadcrumbLink({ className, asChild = false, ...props }) {
|
|
46
|
+
const Component = asChild ? Slot.Root : "a";
|
|
47
|
+
return /* @__PURE__ */ jsx(Component, {
|
|
48
|
+
"data-slot": "breadcrumb-link",
|
|
49
|
+
className: cn("rounded-sm transition-colors duration-[var(--duration-fast)]", "hover:text-foreground", focusRing, className),
|
|
50
|
+
...props
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The page you are on.
|
|
55
|
+
*
|
|
56
|
+
* A span, not a link: there is nowhere to go. `aria-current="page"` is what
|
|
57
|
+
* says "you are here" to anything that is not looking at the font weight.
|
|
58
|
+
*/
|
|
59
|
+
function BreadcrumbPage({ className, ...props }) {
|
|
60
|
+
return /* @__PURE__ */ jsx("span", {
|
|
61
|
+
"data-slot": "breadcrumb-page",
|
|
62
|
+
"aria-current": "page",
|
|
63
|
+
className: cn("font-medium text-foreground", className),
|
|
64
|
+
...props
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The mark between items.
|
|
69
|
+
*
|
|
70
|
+
* Hidden from assistive technology and given no accessible name: the list
|
|
71
|
+
* structure already conveys the sequence, and reading "slash" between every
|
|
72
|
+
* item is the design's punctuation leaking into the content.
|
|
73
|
+
*/
|
|
74
|
+
function BreadcrumbSeparator({ className, children, ...props }) {
|
|
75
|
+
return /* @__PURE__ */ jsx("li", {
|
|
76
|
+
"data-slot": "breadcrumb-separator",
|
|
77
|
+
role: "presentation",
|
|
78
|
+
"aria-hidden": true,
|
|
79
|
+
className: cn("[&>svg]:size-3.5", className),
|
|
80
|
+
...props,
|
|
81
|
+
children: children ?? /* @__PURE__ */ jsx("svg", {
|
|
82
|
+
viewBox: "0 0 24 24",
|
|
83
|
+
fill: "none",
|
|
84
|
+
className: "rtl:-scale-x-100",
|
|
85
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
86
|
+
d: "m9 18 6-6-6-6",
|
|
87
|
+
stroke: "currentColor",
|
|
88
|
+
strokeWidth: "2",
|
|
89
|
+
strokeLinecap: "round",
|
|
90
|
+
strokeLinejoin: "round"
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Stands in for the middle of a long trail.
|
|
97
|
+
*
|
|
98
|
+
* Named rather than hidden, because unlike a separator it is *content*: it
|
|
99
|
+
* says levels have been left out. A bare "…" announces as nothing at all.
|
|
100
|
+
*/
|
|
101
|
+
function BreadcrumbEllipsis({ className, ...props }) {
|
|
102
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
103
|
+
"data-slot": "breadcrumb-ellipsis",
|
|
104
|
+
role: "presentation",
|
|
105
|
+
className: cn("flex size-5 items-center justify-center", className),
|
|
106
|
+
...props,
|
|
107
|
+
children: [/* @__PURE__ */ jsxs("svg", {
|
|
108
|
+
viewBox: "0 0 24 24",
|
|
109
|
+
fill: "none",
|
|
110
|
+
"aria-hidden": true,
|
|
111
|
+
className: "size-4",
|
|
112
|
+
children: [
|
|
113
|
+
/* @__PURE__ */ jsx("circle", {
|
|
114
|
+
cx: "5",
|
|
115
|
+
cy: "12",
|
|
116
|
+
r: "1.5",
|
|
117
|
+
fill: "currentColor"
|
|
118
|
+
}),
|
|
119
|
+
/* @__PURE__ */ jsx("circle", {
|
|
120
|
+
cx: "12",
|
|
121
|
+
cy: "12",
|
|
122
|
+
r: "1.5",
|
|
123
|
+
fill: "currentColor"
|
|
124
|
+
}),
|
|
125
|
+
/* @__PURE__ */ jsx("circle", {
|
|
126
|
+
cx: "19",
|
|
127
|
+
cy: "12",
|
|
128
|
+
r: "1.5",
|
|
129
|
+
fill: "currentColor"
|
|
130
|
+
})
|
|
131
|
+
]
|
|
132
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
133
|
+
className: "sr-only",
|
|
134
|
+
children: "Intermediate levels"
|
|
135
|
+
})]
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
//#endregion
|
|
139
|
+
export { Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator };
|
|
140
|
+
|
|
141
|
+
//# sourceMappingURL=breadcrumb.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"breadcrumb.js","names":[],"sources":["../../../src/components/breadcrumb/breadcrumb.tsx"],"sourcesContent":["import { Slot } from \"radix-ui\";\nimport type { ComponentPropsWithRef } from \"react\";\n\nimport { focusRing, mirrorForDirection } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Where this page sits in the hierarchy.\n *\n * Three things separate a breadcrumb that works from one that only looks like\n * one, and all three are about the last item.\n *\n * It is not a link. A link to the page you are already on is a control that\n * does nothing, and in a screen reader's list of links it is indistinguishable\n * from the ones that go somewhere.\n *\n * It carries `aria-current=\"page\"`. Bolding it says \"you are here\" to people\n * who can see it and to nobody else.\n *\n * The separators are hidden. A slash between every item is read aloud as\n * \"slash\" — \"Home slash Projects slash Settings\" — which is the punctuation of\n * the design leaking into the content.\n */\n\nexport function Breadcrumb({ className, ...props }: ComponentPropsWithRef<\"nav\">) {\n return (\n <nav\n // Named, because a page can have more than one navigation landmark and\n // \"navigation\" on its own does not say which this is.\n aria-label=\"Breadcrumb\"\n data-slot=\"breadcrumb\"\n className={cn(\"min-w-0\", className)}\n {...props}\n />\n );\n}\n\nexport function BreadcrumbList({ className, ...props }: ComponentPropsWithRef<\"ol\">) {\n return (\n <ol\n data-slot=\"breadcrumb-list\"\n className={cn(\n \"flex flex-wrap items-center gap-1.5 text-sm text-muted-foreground\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport function BreadcrumbItem({ className, ...props }: ComponentPropsWithRef<\"li\">) {\n return (\n <li\n data-slot=\"breadcrumb-item\"\n className={cn(\"inline-flex items-center gap-1.5\", className)}\n {...props}\n />\n );\n}\n\nexport interface BreadcrumbLinkProps extends ComponentPropsWithRef<\"a\"> {\n /** Render the child as the link, for a router's own component. */\n asChild?: boolean;\n}\n\nexport function BreadcrumbLink({ className, asChild = false, ...props }: BreadcrumbLinkProps) {\n const Component = asChild ? Slot.Root : \"a\";\n\n return (\n <Component\n data-slot=\"breadcrumb-link\"\n className={cn(\n \"rounded-sm transition-colors duration-[var(--duration-fast)]\",\n \"hover:text-foreground\",\n focusRing,\n className,\n )}\n {...props}\n />\n );\n}\n\n/**\n * The page you are on.\n *\n * A span, not a link: there is nowhere to go. `aria-current=\"page\"` is what\n * says \"you are here\" to anything that is not looking at the font weight.\n */\nexport function BreadcrumbPage({ className, ...props }: ComponentPropsWithRef<\"span\">) {\n return (\n <span\n data-slot=\"breadcrumb-page\"\n aria-current=\"page\"\n className={cn(\"font-medium text-foreground\", className)}\n {...props}\n />\n );\n}\n\n/**\n * The mark between items.\n *\n * Hidden from assistive technology and given no accessible name: the list\n * structure already conveys the sequence, and reading \"slash\" between every\n * item is the design's punctuation leaking into the content.\n */\nexport function BreadcrumbSeparator({\n className,\n children,\n ...props\n}: ComponentPropsWithRef<\"li\">) {\n return (\n <li\n data-slot=\"breadcrumb-separator\"\n role=\"presentation\"\n aria-hidden\n className={cn(\"[&>svg]:size-3.5\", className)}\n {...props}\n >\n {children ?? (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" className={mirrorForDirection}>\n <path\n d=\"m9 18 6-6-6-6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n )}\n </li>\n );\n}\n\n/**\n * Stands in for the middle of a long trail.\n *\n * Named rather than hidden, because unlike a separator it is *content*: it\n * says levels have been left out. A bare \"…\" announces as nothing at all.\n */\nexport function BreadcrumbEllipsis({ className, ...props }: ComponentPropsWithRef<\"span\">) {\n return (\n <span\n data-slot=\"breadcrumb-ellipsis\"\n role=\"presentation\"\n className={cn(\"flex size-5 items-center justify-center\", className)}\n {...props}\n >\n <svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden className=\"size-4\">\n <circle cx=\"5\" cy=\"12\" r=\"1.5\" fill=\"currentColor\" />\n <circle cx=\"12\" cy=\"12\" r=\"1.5\" fill=\"currentColor\" />\n <circle cx=\"19\" cy=\"12\" r=\"1.5\" fill=\"currentColor\" />\n </svg>\n <span className=\"sr-only\">Intermediate levels</span>\n </span>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,WAAW,EAAE,WAAW,GAAG,SAAuC;CAChF,OACE,oBAAC,OAAD;EAGE,cAAW;EACX,aAAU;EACV,WAAW,GAAG,WAAW,SAAS;EAClC,GAAI;CACL,CAAA;AAEL;AAEA,SAAgB,eAAe,EAAE,WAAW,GAAG,SAAsC;CACnF,OACE,oBAAC,MAAD;EACE,aAAU;EACV,WAAW,GACT,qEACA,SACF;EACA,GAAI;CACL,CAAA;AAEL;AAEA,SAAgB,eAAe,EAAE,WAAW,GAAG,SAAsC;CACnF,OACE,oBAAC,MAAD;EACE,aAAU;EACV,WAAW,GAAG,oCAAoC,SAAS;EAC3D,GAAI;CACL,CAAA;AAEL;AAOA,SAAgB,eAAe,EAAE,WAAW,UAAU,OAAO,GAAG,SAA8B;CAC5F,MAAM,YAAY,UAAU,KAAK,OAAO;CAExC,OACE,oBAAC,WAAD;EACE,aAAU;EACV,WAAW,GACT,gEACA,yBACA,WACA,SACF;EACA,GAAI;CACL,CAAA;AAEL;;;;;;;AAQA,SAAgB,eAAe,EAAE,WAAW,GAAG,SAAwC;CACrF,OACE,oBAAC,QAAD;EACE,aAAU;EACV,gBAAa;EACb,WAAW,GAAG,+BAA+B,SAAS;EACtD,GAAI;CACL,CAAA;AAEL;;;;;;;;AASA,SAAgB,oBAAoB,EAClC,WACA,UACA,GAAG,SAC2B;CAC9B,OACE,oBAAC,MAAD;EACE,aAAU;EACV,MAAK;EACL,eAAA;EACA,WAAW,GAAG,oBAAoB,SAAS;EAC3C,GAAI;EAEH,UAAA,YACC,oBAAC,OAAD;GAAK,SAAQ;GAAY,MAAK;GAAO,WAAA;GACnC,UAAA,oBAAC,QAAD;IACE,GAAE;IACF,QAAO;IACP,aAAY;IACZ,eAAc;IACd,gBAAe;GAChB,CAAA;EACE,CAAA;CAEL,CAAA;AAER;;;;;;;AAQA,SAAgB,mBAAmB,EAAE,WAAW,GAAG,SAAwC;CACzF,OACE,qBAAC,QAAD;EACE,aAAU;EACV,MAAK;EACL,WAAW,GAAG,2CAA2C,SAAS;EAClE,GAAI;EAJN,UAAA,CAME,qBAAC,OAAD;GAAK,SAAQ;GAAY,MAAK;GAAO,eAAA;GAAY,WAAU;GAA3D,UAAA;IACE,oBAAC,UAAD;KAAQ,IAAG;KAAI,IAAG;KAAK,GAAE;KAAM,MAAK;IAAgB,CAAA;IACpD,oBAAC,UAAD;KAAQ,IAAG;KAAK,IAAG;KAAK,GAAE;KAAM,MAAK;IAAgB,CAAA;IACrD,oBAAC,UAAD;KAAQ,IAAG;KAAK,IAAG;KAAK,GAAE;KAAM,MAAK;IAAgB,CAAA;GAClD;EACL,CAAA,GAAA,oBAAC,QAAD;GAAM,WAAU;GAAU,UAAA;EAAyB,CAAA,CAC/C;;AAEV"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbLinkProps, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from "./breadcrumb.js";
|
|
2
|
+
export { Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, type BreadcrumbLinkProps, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from "./breadcrumb.js";
|
|
2
|
+
export { Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator };
|