@ai-matrx/design-system 0.86.356 → 0.86.357
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/CHANGELOG.md +5 -0
- package/dist/auth-gate-dialog.js +2 -2
- package/dist/carousel.js +7 -6
- package/dist/carousel.js.map +1 -1
- package/dist/{chunk-UWTODOMD.js → chunk-2BQAHLU3.js} +4 -4
- package/dist/chunk-5Z5GTXKL.js +79 -0
- package/dist/chunk-5Z5GTXKL.js.map +1 -0
- package/dist/{chunk-2PO3N42Q.js → chunk-ACZALXD6.js} +3 -3
- package/dist/{chunk-4WSXAP24.js → chunk-BB5LA7FQ.js} +3 -1
- package/dist/chunk-BB5LA7FQ.js.map +1 -0
- package/dist/{chunk-RLM4RSED.js → chunk-GUGBVQ4T.js} +6 -4
- package/dist/{chunk-RLM4RSED.js.map → chunk-GUGBVQ4T.js.map} +1 -1
- package/dist/{chunk-LJKF267O.js → chunk-HS6GMHKZ.js} +18 -76
- package/dist/chunk-HS6GMHKZ.js.map +1 -0
- package/dist/{chunk-ZMUPFGQO.js → chunk-TV2IDHPZ.js} +2 -2
- package/dist/{chunk-CFUXEGW2.js → chunk-XZGYKKCE.js} +2 -2
- package/dist/clipboard-fallback-dialog.js +2 -2
- package/dist/controls.cjs +15 -3
- package/dist/controls.cjs.map +1 -1
- package/dist/controls.js +5 -4
- package/dist/data-table/data-row-window.js +3 -3
- package/dist/data-table/drill-chart.cjs +15 -3
- package/dist/data-table/drill-chart.cjs.map +1 -1
- package/dist/data-table/drill-chart.js +2 -1
- package/dist/data-table/drill-chart.js.map +1 -1
- package/dist/data-table/index.cjs +15 -3
- package/dist/data-table/index.cjs.map +1 -1
- package/dist/data-table/index.js +8 -7
- package/dist/data-table/index.js.map +1 -1
- package/dist/desktop.cjs +26 -5
- package/dist/desktop.cjs.map +1 -1
- package/dist/desktop.js +15 -7
- package/dist/desktop.js.map +1 -1
- package/dist/email-input-dialog.cjs +15 -3
- package/dist/email-input-dialog.cjs.map +1 -1
- package/dist/email-input-dialog.js +4 -3
- package/dist/email-input-dialog.js.map +1 -1
- package/dist/error-sentence.cjs +130 -0
- package/dist/error-sentence.cjs.map +1 -0
- package/dist/error-sentence.d.cts +49 -0
- package/dist/error-sentence.d.ts +49 -0
- package/dist/error-sentence.js +22 -0
- package/dist/error-sentence.js.map +1 -0
- package/dist/index.cjs +16 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +17 -15
- package/dist/index.js.map +1 -1
- package/dist/item.js +2 -2
- package/dist/lite-table.cjs +15 -3
- package/dist/lite-table.cjs.map +1 -1
- package/dist/lite-table.js +3 -2
- package/dist/lite-table.js.map +1 -1
- package/dist/notice-stack.cjs +2 -0
- package/dist/notice-stack.cjs.map +1 -1
- package/dist/notice-stack.d.cts +11 -1
- package/dist/notice-stack.d.ts +11 -1
- package/dist/notice-stack.js +3 -1
- package/dist/record-list.cjs +15 -3
- package/dist/record-list.cjs.map +1 -1
- package/dist/record-list.js +3 -2
- package/dist/record-list.js.map +1 -1
- package/dist/sandbox-gate-dialog.js +2 -2
- package/dist/scope-mismatch-dialog.js +2 -2
- package/dist/swipe-deck.cjs +15 -3
- package/dist/swipe-deck.cjs.map +1 -1
- package/dist/swipe-deck.js +2 -1
- package/dist/swipe-deck.js.map +1 -1
- package/dist/text-input-dialog.cjs +15 -3
- package/dist/text-input-dialog.cjs.map +1 -1
- package/dist/text-input-dialog.js +4 -3
- package/dist/text-input-dialog.js.map +1 -1
- package/dist/value-prompts-dialog.js +2 -2
- package/dist/writing-field.js +2 -2
- package/package.json +12 -2
- package/dist/chunk-4WSXAP24.js.map +0 -1
- package/dist/chunk-LJKF267O.js.map +0 -1
- /package/dist/{chunk-UWTODOMD.js.map → chunk-2BQAHLU3.js.map} +0 -0
- /package/dist/{chunk-2PO3N42Q.js.map → chunk-ACZALXD6.js.map} +0 -0
- /package/dist/{chunk-ZMUPFGQO.js.map → chunk-TV2IDHPZ.js.map} +0 -0
- /package/dist/{chunk-CFUXEGW2.js.map → chunk-XZGYKKCE.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.86.357
|
|
4
|
+
|
|
5
|
+
UNVERIFIED: this release has no Unreleased entry and the source comparison could not be made (the caller supplied no source comparison). Whether source changed is unknown; consumer action is unknown.
|
|
6
|
+
Check the package's gitHead on npm against this version's gitHead: `git diff <previous gitHead> <this gitHead> -- apps/shared/design-system/src`.
|
|
7
|
+
|
|
3
8
|
## 0.86.356
|
|
4
9
|
|
|
5
10
|
- Slider forwards an explicitly supplied `aria-valuetext` to its focusable thumb, so controls such as PageCompare announce their formatted value. Keyboard and numeric value behavior are unchanged. Consumer action: none.
|
package/dist/auth-gate-dialog.js
CHANGED
|
@@ -5,10 +5,10 @@ import {
|
|
|
5
5
|
DialogDescription,
|
|
6
6
|
DialogHeader,
|
|
7
7
|
DialogTitle
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-2BQAHLU3.js";
|
|
9
|
+
import "./chunk-LEPD6TV7.js";
|
|
9
10
|
import "./chunk-U24MOJWK.js";
|
|
10
11
|
import "./chunk-NDTDBBMM.js";
|
|
11
|
-
import "./chunk-LEPD6TV7.js";
|
|
12
12
|
import "./chunk-MOZRIHJF.js";
|
|
13
13
|
import {
|
|
14
14
|
Button
|
package/dist/carousel.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import {
|
|
3
3
|
IconButton
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-ACZALXD6.js";
|
|
5
5
|
import "./chunk-6NOLVA4E.js";
|
|
6
6
|
import "./chunk-5IR3BZOS.js";
|
|
7
7
|
import "./chunk-AV5C4YLL.js";
|
|
@@ -10,16 +10,17 @@ import "./chunk-WQ2QCMMA.js";
|
|
|
10
10
|
import "./chunk-YRF22M5S.js";
|
|
11
11
|
import "./chunk-GMNROZQJ.js";
|
|
12
12
|
import "./chunk-K4XCB576.js";
|
|
13
|
-
import "./chunk-
|
|
13
|
+
import "./chunk-5Z5GTXKL.js";
|
|
14
|
+
import "./chunk-HS6GMHKZ.js";
|
|
14
15
|
import "./chunk-DRSBLHOF.js";
|
|
15
16
|
import "./chunk-TH6DDCWF.js";
|
|
16
|
-
import "./chunk-
|
|
17
|
-
import "./chunk-IMQ3RX35.js";
|
|
18
|
-
import "./chunk-U24MOJWK.js";
|
|
19
|
-
import "./chunk-NDTDBBMM.js";
|
|
17
|
+
import "./chunk-2BQAHLU3.js";
|
|
20
18
|
import {
|
|
21
19
|
holdDragCursor
|
|
22
20
|
} from "./chunk-LEPD6TV7.js";
|
|
21
|
+
import "./chunk-IMQ3RX35.js";
|
|
22
|
+
import "./chunk-U24MOJWK.js";
|
|
23
|
+
import "./chunk-NDTDBBMM.js";
|
|
23
24
|
import "./chunk-MTE6N7XU.js";
|
|
24
25
|
import "./chunk-WFBPCMPF.js";
|
|
25
26
|
import {
|
package/dist/carousel.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/carousel/Carousel.tsx","../src/carousel/model.ts"],"sourcesContent":["\"use client\";\n\n/**\n * Carousel — THE slide-strip primitive. One component, two modes:\n *\n * viewer swipe (native scroll-snap) · arrows · Left/Right/Home/End · dots +\n * counter · thumbnail strip · aspect 1:1 | 4:5 | 9:16 | 16:9\n * editor viewer + add / duplicate / delete / drag-or-button reorder /\n * edit / version history / export\n *\n * The carousel never draws a slide. The host passes `renderSlide`, so the\n * same primitive carries generated images, text-on-image social slides and\n * presentation slides. It never stores anything either: `slides` is the\n * host's, every edit leaves through `onSlidesChange`, and a slide edit also\n * leaves through `onSlideChange` with the PREVIOUS version so the host can\n * keep history (see `model.ts` — the store contract). Export is a callback\n * for the same reason: downloads go through the host's export door.\n *\n * NO LAYOUT SHIFT. The frame is an aspect-ratio box and every control lives\n * inside it, in a fixed-height row below, or in the thumbnail strip; nothing\n * appears or disappears into the flow when the slide changes.\n */\n\nimport * as React from \"react\";\n\nimport { cn } from \"../cn\";\nimport { holdDragCursor } from \"../drag-cursor\";\nimport { IconButton } from \"../icon-button\";\nimport { ArrowLeftIcon, ArrowRightIcon, ChevronLeftIcon, ChevronRightIcon, CopyIcon, DownloadIcon, HistoryIcon, PencilIcon, PlusIcon, Trash2Icon } from \"../icons\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../popover\";\nimport { TooltipProvider } from \"../tooltip\";\n\nimport {\n DEFAULT_MAX_SLIDES,\n DEFAULT_MIN_SLIDES,\n aspectRatioOf,\n clampIndex,\n duplicateSlide,\n insertSlide,\n moveSlide,\n removeSlide,\n replaceSlide,\n stepIndex,\n type CarouselAspect,\n type CarouselSlideBase,\n type SlideChange,\n type SlideVersion,\n} from \"./model\";\n\nexport type CarouselMode = \"viewer\" | \"editor\";\n\nexport interface CarouselSlideContext<T> {\n readonly index: number;\n readonly count: number;\n readonly active: boolean;\n readonly mode: CarouselMode;\n /** True when drawing the small thumbnail copy — skip interactive chrome. */\n readonly thumbnail: boolean;\n /** Save an edited slide: reports `previous` + `next` to `onSlideChange`. */\n readonly update: (next: T) => void;\n}\n\nexport interface CarouselLabels {\n readonly carousel: string;\n readonly previous: string;\n readonly next: string;\n readonly slide: (index: number, count: number) => string;\n readonly add: string;\n readonly duplicate: string;\n readonly remove: string;\n readonly moveEarlier: string;\n readonly moveLater: string;\n readonly edit: string;\n readonly history: string;\n readonly restore: string;\n readonly noHistory: string;\n readonly export: string;\n}\n\nexport const DEFAULT_CAROUSEL_LABELS: CarouselLabels = {\n carousel: \"Slides\",\n previous: \"Previous slide\",\n next: \"Next slide\",\n slide: (index, count) => `Slide ${index + 1} of ${count}`,\n add: \"Add slide\",\n duplicate: \"Duplicate slide\",\n remove: \"Delete slide\",\n moveEarlier: \"Move earlier\",\n moveLater: \"Move later\",\n edit: \"Edit slide\",\n history: \"Version history\",\n restore: \"Restore\",\n noHistory: \"No earlier versions\",\n export: \"Export\",\n};\n\nexport interface CarouselProps<T extends CarouselSlideBase> {\n slides: readonly T[];\n /** Draws one slide. It fills the frame; the carousel owns the aspect box. */\n renderSlide: (slide: T, ctx: CarouselSlideContext<T>) => React.ReactNode;\n mode?: CarouselMode;\n aspect?: CarouselAspect;\n /** Controlled active index; omit for the carousel to keep its own. */\n index?: number;\n onIndexChange?: (index: number) => void;\n loop?: boolean;\n dots?: boolean;\n counter?: boolean;\n thumbnails?: boolean;\n /** Overrides the visible-slide height cap for tall aspects (CSS length). */\n maxHeight?: string;\n labels?: Partial<CarouselLabels>;\n className?: string;\n\n // Editor\n onSlidesChange?: (slides: T[], index: number) => void;\n /** Called with the previous and next slide whenever a slide is updated or restored. */\n onSlideChange?: (change: SlideChange<T>) => void;\n /** Makes \"Add\" available; returns a brand-new slide with a fresh id. */\n createSlide?: () => T;\n /** Fresh id for a duplicate; defaults to `crypto.randomUUID()`. */\n createId?: () => string;\n /** Deep copy hook for duplicating; defaults to a shallow copy. */\n cloneSlide?: (slide: T) => T;\n /** Shows an Edit button; the host opens its own editor for the slide. */\n onEditSlide?: (slide: T, index: number) => void;\n minSlides?: number;\n maxSlides?: number;\n /** Kept versions per slide id, newest first — the host's store, read-only here. */\n history?: Readonly<Record<string, readonly SlideVersion<T>[]>>;\n /** Shows an Export button. The host runs the download. */\n onExport?: (slides: readonly T[]) => void;\n}\n\nconst MAX_DOTS = 12;\n/** Thumbnail geometry (px): the strip row is 64px with 2px padding top and bottom. */\nconst THUMB_HEIGHT = 60;\nconst THUMB_CANVAS = 360;\n\nfunction prefersReducedMotion(): boolean {\n return typeof window !== \"undefined\" && window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches === true;\n}\n\nexport function Carousel<T extends CarouselSlideBase>({\n slides,\n renderSlide,\n mode = \"viewer\",\n aspect = \"1:1\",\n index: controlledIndex,\n onIndexChange,\n loop = false,\n dots = true,\n counter = true,\n thumbnails = true,\n maxHeight = \"70vh\",\n labels: labelOverrides,\n className,\n onSlidesChange,\n onSlideChange,\n createSlide,\n createId,\n cloneSlide,\n onEditSlide,\n minSlides = DEFAULT_MIN_SLIDES,\n maxSlides = DEFAULT_MAX_SLIDES,\n history,\n onExport,\n}: CarouselProps<T>) {\n const labels = { ...DEFAULT_CAROUSEL_LABELS, ...labelOverrides };\n const count = slides.length;\n const editor = mode === \"editor\";\n const [ownIndex, setOwnIndex] = React.useState(0);\n const index = clampIndex(controlledIndex ?? ownIndex, count);\n\n const trackRef = React.useRef<HTMLDivElement>(null);\n const target = React.useRef<number | null>(null);\n const settle = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const setIndex = React.useCallback(\n (next: number) => {\n if (controlledIndex === undefined) setOwnIndex(next);\n onIndexChange?.(next);\n },\n [controlledIndex, onIndexChange],\n );\n\n const scrollToIndex = React.useCallback((i: number, smooth: boolean) => {\n const track = trackRef.current;\n if (!track) return;\n track.scrollTo?.({ left: i * track.clientWidth, behavior: smooth && !prefersReducedMotion() ? \"smooth\" : \"auto\" });\n }, []);\n\n const goTo = React.useCallback(\n (raw: number) => {\n const next = clampIndex(raw, count);\n target.current = next;\n if (settle.current) clearTimeout(settle.current);\n settle.current = setTimeout(() => {\n target.current = null;\n }, 700);\n setIndex(next);\n scrollToIndex(next, true);\n },\n [count, scrollToIndex, setIndex],\n );\n\n // Keep the scroller on the active slide when the index or the deck changes from outside\n // (add, delete, reorder, a controlled parent) — without fighting a smooth scroll in flight.\n React.useLayoutEffect(() => {\n const track = trackRef.current;\n if (!track || target.current !== null || track.clientWidth === 0) return;\n if (Math.round(track.scrollLeft / track.clientWidth) !== index) scrollToIndex(index, false);\n }, [index, count, scrollToIndex]);\n\n React.useEffect(() => {\n const track = trackRef.current;\n if (!track || typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(() => scrollToIndex(index, false));\n observer.observe(track);\n return () => observer.disconnect();\n }, [index, scrollToIndex]);\n\n React.useEffect(\n () => () => {\n if (settle.current) clearTimeout(settle.current);\n },\n [],\n );\n\n const onScroll = () => {\n const track = trackRef.current;\n if (!track || track.clientWidth === 0) return;\n const seen = clampIndex(Math.round(track.scrollLeft / track.clientWidth), count);\n if (target.current !== null) {\n if (seen === target.current) target.current = null;\n return;\n }\n if (seen !== index) setIndex(seen);\n };\n\n const onKeyDown = (event: React.KeyboardEvent) => {\n if (event.target !== event.currentTarget) return;\n if (event.key === \"ArrowLeft\") goTo(stepIndex(index, -1, count, loop));\n else if (event.key === \"ArrowRight\") goTo(stepIndex(index, 1, count, loop));\n else if (event.key === \"Home\") goTo(0);\n else if (event.key === \"End\") goTo(count - 1);\n else return;\n event.preventDefault();\n };\n\n // ---- editor operations -------------------------------------------------\n\n const emit = (edit: { slides: T[]; index: number } | null) => {\n if (!edit) return;\n onSlidesChange?.(edit.slides, edit.index);\n target.current = edit.index;\n setIndex(edit.index);\n // Land the scroller on the new active slide once the host's deck is rendered.\n requestAnimationFrame(() => {\n scrollToIndex(edit.index, false);\n target.current = null;\n });\n };\n\n const commitSlide = (next: T) => {\n const result = replaceSlide(slides, next);\n if (!result) return;\n onSlidesChange?.(result.slides, index);\n onSlideChange?.({ id: next.id, index: result.index, previous: result.previous, next });\n };\n\n const active = slides[index];\n const canAdd = editor && createSlide !== undefined && count < maxSlides;\n const canRemove = editor && count > minSlides;\n const activeHistory = active ? (history?.[active.id] ?? []) : [];\n\n const [w, h] = aspectRatioOf(aspect);\n const frameStyle: React.CSSProperties = {\n aspectRatio: `${w} / ${h}`,\n maxWidth: `min(100%, calc(${maxHeight} * ${w} / ${h}))`,\n };\n\n const contextFor = (_slide: T, i: number, thumbnail: boolean): CarouselSlideContext<T> => ({\n index: i,\n count,\n active: i === index,\n mode,\n thumbnail,\n update: thumbnail ? () => undefined : commitSlide,\n });\n\n return (\n // Its own provider: the control tooltips work in any host root (a portal, a window, a bare test).\n <TooltipProvider>\n <section\n aria-roledescription=\"carousel\"\n aria-label={labels.carousel}\n data-matrx-carousel={mode}\n className={cn(\"flex w-full min-w-0 flex-col gap-2\", className)}\n >\n <div\n className=\"relative mx-auto w-full overflow-hidden rounded-lg border border-border bg-muted\"\n style={frameStyle}\n >\n <div\n ref={trackRef}\n tabIndex={0}\n aria-live=\"polite\"\n aria-label={active ? labels.slide(index, count) : labels.carousel}\n onScroll={onScroll}\n onKeyDown={onKeyDown}\n className=\"flex h-full w-full snap-x snap-mandatory overflow-x-auto overscroll-x-contain outline-none [scrollbar-width:none] focus-visible:ring-2 focus-visible:ring-ring [&::-webkit-scrollbar]:hidden\"\n >\n {slides.map((slide, i) => (\n <div\n key={slide.id}\n role=\"group\"\n aria-roledescription=\"slide\"\n aria-label={labels.slide(i, count)}\n aria-hidden={i !== index}\n inert={i !== index}\n className=\"h-full w-full shrink-0 snap-center snap-always\"\n >\n {renderSlide(slide, contextFor(slide, i, false))}\n </div>\n ))}\n </div>\n\n {count > 1 && (\n <>\n <div className=\"pointer-events-none absolute inset-y-0 left-2 flex items-center\">\n <IconButton\n icon={ChevronLeftIcon}\n variant=\"outline\"\n tooltip={labels.previous}\n disabled={!loop && index === 0}\n onClick={() => goTo(stepIndex(index, -1, count, loop))}\n className=\"pointer-events-auto bg-card\"\n />\n </div>\n <div className=\"pointer-events-none absolute inset-y-0 right-2 flex items-center\">\n <IconButton\n icon={ChevronRightIcon}\n variant=\"outline\"\n tooltip={labels.next}\n disabled={!loop && index === count - 1}\n onClick={() => goTo(stepIndex(index, 1, count, loop))}\n className=\"pointer-events-auto bg-card\"\n />\n </div>\n </>\n )}\n {counter && count > 0 && (\n <span\n data-matrx-carousel-counter\n className=\"pointer-events-none absolute right-2 top-2 rounded-full bg-card px-2 py-0.5 text-xs tabular-nums text-foreground ring-1 ring-border\"\n >\n {index + 1} / {count}\n </span>\n )}\n </div>\n\n {/* One fixed-height row: dots when they fit, so the strip below never jumps. */}\n {dots && count > 1 && count <= MAX_DOTS && (\n <div className=\"flex h-3 items-center justify-center gap-1.5\" role=\"tablist\" aria-label={labels.carousel}>\n {slides.map((slide, i) => (\n <button\n key={slide.id}\n type=\"button\"\n role=\"tab\"\n aria-selected={i === index}\n aria-label={labels.slide(i, count)}\n onClick={() => goTo(i)}\n className={cn(\n \"h-1.5 rounded-full transition-[width,background-color]\",\n i === index ? \"w-4 bg-primary\" : \"w-1.5 bg-muted-foreground/40\",\n )}\n />\n ))}\n </div>\n )}\n\n {editor && (\n <div className=\"flex flex-wrap items-center justify-center gap-1\" role=\"toolbar\" aria-label={labels.carousel}>\n <IconButton\n icon={ArrowLeftIcon}\n tooltip={labels.moveEarlier}\n disabled={index === 0}\n onClick={() => emit(moveSlide(slides, index, index - 1, index))}\n />\n <IconButton\n icon={ArrowRightIcon}\n tooltip={labels.moveLater}\n disabled={index >= count - 1}\n onClick={() => emit(moveSlide(slides, index, index + 1, index))}\n />\n {onEditSlide && active && (\n <IconButton icon={PencilIcon} tooltip={labels.edit} onClick={() => onEditSlide(active, index)} />\n )}\n <IconButton\n icon={PlusIcon}\n tooltip={labels.add}\n disabled={!canAdd}\n onClick={() => createSlide && emit(insertSlide(slides, createSlide(), index, maxSlides))}\n />\n <IconButton\n icon={CopyIcon}\n tooltip={labels.duplicate}\n disabled={!active || count >= maxSlides}\n onClick={() =>\n emit(duplicateSlide(slides, index, createId ? createId() : crypto.randomUUID(), maxSlides, cloneSlide))\n }\n />\n <IconButton\n icon={Trash2Icon}\n tooltip={labels.remove}\n disabled={!canRemove}\n onClick={() => emit(removeSlide(slides, index, index, minSlides))}\n />\n {history !== undefined && active && (\n <Popover>\n <PopoverTrigger asChild>\n <IconButton icon={HistoryIcon} tooltip={labels.history} />\n </PopoverTrigger>\n <PopoverContent className=\"w-56 p-1\" align=\"center\">\n {activeHistory.length === 0 ? (\n <p className=\"px-2 py-1.5 text-xs text-muted-foreground\">{labels.noHistory}</p>\n ) : (\n <ul className=\"flex flex-col\">\n {activeHistory.map((version) => (\n <li key={version.at} className=\"flex items-center justify-between gap-2 px-2 py-1\">\n <time className=\"text-xs tabular-nums text-muted-foreground\" dateTime={new Date(version.at).toISOString()}>\n {new Date(version.at).toLocaleTimeString([], { hour: \"numeric\", minute: \"2-digit\" })}\n </time>\n <button\n type=\"button\"\n className=\"text-xs text-primary hover:underline\"\n onClick={() => commitSlide({ ...version.slide, id: active.id })}\n >\n {labels.restore}\n </button>\n </li>\n ))}\n </ul>\n )}\n </PopoverContent>\n </Popover>\n )}\n {onExport && (\n <IconButton icon={DownloadIcon} tooltip={labels.export} onClick={() => onExport(slides)} />\n )}\n </div>\n )}\n\n {thumbnails && count > 1 && (\n <Thumbnails\n slides={slides}\n index={index}\n aspect={aspect}\n editor={editor}\n onSelect={goTo}\n onReorder={(from, to) => emit(moveSlide(slides, from, to, index))}\n renderThumb={(slide, i) => renderSlide(slide, contextFor(slide, i, true))}\n label={labels.slide}\n />\n )}\n </section>\n </TooltipProvider>\n );\n}\n\nfunction Thumbnails<T extends CarouselSlideBase>({\n slides,\n index,\n aspect,\n editor,\n onSelect,\n onReorder,\n renderThumb,\n label,\n}: {\n slides: readonly T[];\n index: number;\n aspect: CarouselAspect;\n editor: boolean;\n onSelect: (i: number) => void;\n onReorder: (from: number, to: number) => void;\n renderThumb: (slide: T, i: number) => React.ReactNode;\n label: CarouselLabels[\"slide\"];\n}) {\n const [w, h] = aspectRatioOf(aspect);\n const dragFrom = React.useRef<number | null>(null);\n const release = React.useRef<(() => void) | null>(null);\n const [over, setOver] = React.useState<number | null>(null);\n const activeRef = React.useRef<HTMLButtonElement>(null);\n\n React.useEffect(() => {\n activeRef.current?.scrollIntoView?.({ block: \"nearest\", inline: \"nearest\" });\n }, [index]);\n\n const end = () => {\n dragFrom.current = null;\n release.current?.();\n release.current = null;\n setOver(null);\n };\n\n return (\n <div\n data-matrx-carousel-thumbnails\n className=\"flex h-16 gap-2 overflow-x-auto py-0.5 [scrollbar-width:thin]\"\n >\n {slides.map((slide, i) => (\n <button\n key={slide.id}\n ref={i === index ? activeRef : undefined}\n type=\"button\"\n aria-label={label(i, slides.length)}\n aria-current={i === index}\n draggable={editor}\n data-clickable\n onClick={() => onSelect(i)}\n onDragStart={(event) => {\n dragFrom.current = i;\n release.current = holdDragCursor(\"grabbing\");\n event.dataTransfer.effectAllowed = \"move\";\n event.dataTransfer.setData(\"text/plain\", slide.id);\n }}\n onDragOver={(event) => {\n if (dragFrom.current === null) return;\n event.preventDefault();\n setOver(i);\n }}\n onDrop={(event) => {\n event.preventDefault();\n const from = dragFrom.current;\n end();\n if (from !== null && from !== i) onReorder(from, i);\n }}\n onDragEnd={end}\n style={{ aspectRatio: `${w} / ${h}`, height: \"100%\" }}\n className={cn(\n \"relative shrink-0 overflow-hidden rounded-md border bg-muted\",\n i === index ? \"border-primary ring-2 ring-primary/40\" : \"border-border opacity-80 hover:opacity-100\",\n over === i && dragFrom.current !== i && \"ring-2 ring-ring\",\n )}\n >\n {/* The slide draws at a fixed 360px-wide canvas and is scaled down, so a thumbnail is a true miniature. */}\n <span\n className=\"pointer-events-none block origin-top-left\"\n style={{\n width: THUMB_CANVAS,\n height: (THUMB_CANVAS * h) / w,\n transform: `scale(${(THUMB_HEIGHT * (w / h)) / THUMB_CANVAS})`,\n }}\n >\n {renderThumb(slide, i)}\n </span>\n <span className=\"pointer-events-none absolute bottom-0.5 left-0.5 rounded bg-card px-1 text-[10px] tabular-nums text-foreground\">\n {i + 1}\n </span>\n </button>\n ))}\n </div>\n );\n}\n","/**\n * The carousel's pure slide logic — reorder, add, duplicate, delete, index\n * bookkeeping, and the version-history contract. No React, no DOM: the\n * component only calls these, so the editing rules are proven without a\n * browser (`model.test.ts`).\n *\n * VERSION HISTORY IS A STORE CONTRACT, NOT STORAGE. The carousel never saves\n * anything. When a slide is edited it reports `{ previous, next }`; the host\n * keeps `previous` wherever it keeps history (`pushVersion` is the one\n * bounded, newest-first way to do it) and hands the list back through the\n * `history` prop so the History menu can show it.\n */\n\n/** Every slide carries a stable id — reorder and history are keyed on it. */\nexport interface CarouselSlideBase {\n readonly id: string;\n}\n\n/** The aspect ratios social and presentation slides come in. */\nexport const CAROUSEL_ASPECTS = [\"1:1\", \"4:5\", \"9:16\", \"16:9\"] as const;\nexport type CarouselAspect = (typeof CAROUSEL_ASPECTS)[number];\n\n/** `[width, height]` of an aspect — the one place the ratio is spelled. */\nexport function aspectRatioOf(aspect: CarouselAspect): readonly [number, number] {\n switch (aspect) {\n case \"1:1\":\n return [1, 1];\n case \"4:5\":\n return [4, 5];\n case \"9:16\":\n return [9, 16];\n case \"16:9\":\n return [16, 9];\n }\n}\n\n/** Social carousels run 2-10 slides; a host with other limits passes its own. */\nexport const DEFAULT_MIN_SLIDES = 1;\nexport const DEFAULT_MAX_SLIDES = 10;\nexport const DEFAULT_HISTORY_LIMIT = 20;\n\nexport function clampIndex(index: number, length: number): number {\n if (length <= 0) return 0;\n if (!Number.isFinite(index)) return 0;\n return Math.min(Math.max(Math.trunc(index), 0), length - 1);\n}\n\n/** Step `index` by `delta`; wraps when `loop`, otherwise stops at the ends. */\nexport function stepIndex(index: number, delta: number, length: number, loop: boolean): number {\n if (length <= 0) return 0;\n const next = index + delta;\n if (loop) return ((next % length) + length) % length;\n return clampIndex(next, length);\n}\n\nexport interface SlideEdit<T> {\n readonly slides: T[];\n /** Where the active slide ends up after the edit. */\n readonly index: number;\n}\n\n/** Move the slide at `from` to position `to`. A no-op returns the same array. */\nexport function moveSlide<T>(slides: readonly T[], from: number, to: number, active: number): SlideEdit<T> {\n const last = slides.length - 1;\n if (from < 0 || from > last || to < 0 || to > last || from === to) {\n return { slides: slides as T[], index: active };\n }\n const next = slides.slice();\n const [moved] = next.splice(from, 1);\n next.splice(to, 0, moved as T);\n // The active slide follows the same item through the move.\n let index = active;\n if (active === from) index = to;\n else if (from < active && to >= active) index = active - 1;\n else if (from > active && to <= active) index = active + 1;\n return { slides: next, index };\n}\n\n/** Insert `slide` after `after` (default: at the end); the new slide becomes active. */\nexport function insertSlide<T>(\n slides: readonly T[],\n slide: T,\n after: number = slides.length - 1,\n max: number = DEFAULT_MAX_SLIDES,\n): SlideEdit<T> | null {\n if (slides.length >= max) return null;\n const at = clampIndex(after, slides.length + 1) + (slides.length === 0 ? 0 : 1);\n const next = slides.slice();\n next.splice(at, 0, slide);\n return { slides: next, index: at };\n}\n\n/** Copy the slide at `index` right after itself with a new id; the copy becomes active. */\nexport function duplicateSlide<T extends CarouselSlideBase>(\n slides: readonly T[],\n index: number,\n newId: string,\n max: number = DEFAULT_MAX_SLIDES,\n clone: (slide: T) => T = (slide) => ({ ...slide }),\n): SlideEdit<T> | null {\n const source = slides[index];\n if (!source || slides.length >= max) return null;\n const copy = { ...clone(source), id: newId } as T;\n const next = slides.slice();\n next.splice(index + 1, 0, copy);\n return { slides: next, index: index + 1 };\n}\n\n/** Remove the slide at `index`; refuses below `min` and keeps the active slide in range. */\nexport function removeSlide<T>(\n slides: readonly T[],\n index: number,\n active: number,\n min: number = DEFAULT_MIN_SLIDES,\n): SlideEdit<T> | null {\n if (index < 0 || index >= slides.length || slides.length <= min) return null;\n const next = slides.slice();\n next.splice(index, 1);\n let nextActive = active;\n if (index < active || (index === active && active === slides.length - 1)) nextActive = active - 1;\n return { slides: next, index: clampIndex(nextActive, next.length) };\n}\n\n/** Replace the slide with `next.id`; reports what it replaced. */\nexport function replaceSlide<T extends CarouselSlideBase>(\n slides: readonly T[],\n next: T,\n): { slides: T[]; previous: T; index: number } | null {\n const index = slides.findIndex((slide) => slide.id === next.id);\n const previous = slides[index];\n if (index < 0 || previous === undefined) return null;\n const copy = slides.slice();\n copy[index] = next;\n return { slides: copy, previous, index };\n}\n\n// ---------------------------------------------------------------------------\n// Version history — the store contract.\n// ---------------------------------------------------------------------------\n\n/** One kept version of a slide; `at` is epoch ms the host stamped when it kept it. */\nexport interface SlideVersion<T> {\n readonly slide: T;\n readonly at: number;\n}\n\n/** What `onSlideChange` hands the host: the slide before, the slide after. */\nexport interface SlideChange<T> {\n readonly id: string;\n readonly index: number;\n readonly previous: T;\n readonly next: T;\n}\n\n/** Keep `previous` at the head of `history`, newest first, bounded to `limit`. */\nexport function pushVersion<T>(\n history: readonly SlideVersion<T>[] | undefined,\n previous: T,\n at: number,\n limit: number = DEFAULT_HISTORY_LIMIT,\n): SlideVersion<T>[] {\n return [{ slide: previous, at }, ...(history ?? [])].slice(0, Math.max(limit, 0));\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/carousel/Carousel.tsx","../src/carousel/model.ts"],"sourcesContent":["\"use client\";\n\n/**\n * Carousel — THE slide-strip primitive. One component, two modes:\n *\n * viewer swipe (native scroll-snap) · arrows · Left/Right/Home/End · dots +\n * counter · thumbnail strip · aspect 1:1 | 4:5 | 9:16 | 16:9\n * editor viewer + add / duplicate / delete / drag-or-button reorder /\n * edit / version history / export\n *\n * The carousel never draws a slide. The host passes `renderSlide`, so the\n * same primitive carries generated images, text-on-image social slides and\n * presentation slides. It never stores anything either: `slides` is the\n * host's, every edit leaves through `onSlidesChange`, and a slide edit also\n * leaves through `onSlideChange` with the PREVIOUS version so the host can\n * keep history (see `model.ts` — the store contract). Export is a callback\n * for the same reason: downloads go through the host's export door.\n *\n * NO LAYOUT SHIFT. The frame is an aspect-ratio box and every control lives\n * inside it, in a fixed-height row below, or in the thumbnail strip; nothing\n * appears or disappears into the flow when the slide changes.\n */\n\nimport * as React from \"react\";\n\nimport { cn } from \"../cn\";\nimport { holdDragCursor } from \"../drag-cursor\";\nimport { IconButton } from \"../icon-button\";\nimport { ArrowLeftIcon, ArrowRightIcon, ChevronLeftIcon, ChevronRightIcon, CopyIcon, DownloadIcon, HistoryIcon, PencilIcon, PlusIcon, Trash2Icon } from \"../icons\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../popover\";\nimport { TooltipProvider } from \"../tooltip\";\n\nimport {\n DEFAULT_MAX_SLIDES,\n DEFAULT_MIN_SLIDES,\n aspectRatioOf,\n clampIndex,\n duplicateSlide,\n insertSlide,\n moveSlide,\n removeSlide,\n replaceSlide,\n stepIndex,\n type CarouselAspect,\n type CarouselSlideBase,\n type SlideChange,\n type SlideVersion,\n} from \"./model\";\n\nexport type CarouselMode = \"viewer\" | \"editor\";\n\nexport interface CarouselSlideContext<T> {\n readonly index: number;\n readonly count: number;\n readonly active: boolean;\n readonly mode: CarouselMode;\n /** True when drawing the small thumbnail copy — skip interactive chrome. */\n readonly thumbnail: boolean;\n /** Save an edited slide: reports `previous` + `next` to `onSlideChange`. */\n readonly update: (next: T) => void;\n}\n\nexport interface CarouselLabels {\n readonly carousel: string;\n readonly previous: string;\n readonly next: string;\n readonly slide: (index: number, count: number) => string;\n readonly add: string;\n readonly duplicate: string;\n readonly remove: string;\n readonly moveEarlier: string;\n readonly moveLater: string;\n readonly edit: string;\n readonly history: string;\n readonly restore: string;\n readonly noHistory: string;\n readonly export: string;\n}\n\nexport const DEFAULT_CAROUSEL_LABELS: CarouselLabels = {\n carousel: \"Slides\",\n previous: \"Previous slide\",\n next: \"Next slide\",\n slide: (index, count) => `Slide ${index + 1} of ${count}`,\n add: \"Add slide\",\n duplicate: \"Duplicate slide\",\n remove: \"Delete slide\",\n moveEarlier: \"Move earlier\",\n moveLater: \"Move later\",\n edit: \"Edit slide\",\n history: \"Version history\",\n restore: \"Restore\",\n noHistory: \"No earlier versions\",\n export: \"Export\",\n};\n\nexport interface CarouselProps<T extends CarouselSlideBase> {\n slides: readonly T[];\n /** Draws one slide. It fills the frame; the carousel owns the aspect box. */\n renderSlide: (slide: T, ctx: CarouselSlideContext<T>) => React.ReactNode;\n mode?: CarouselMode;\n aspect?: CarouselAspect;\n /** Controlled active index; omit for the carousel to keep its own. */\n index?: number;\n onIndexChange?: (index: number) => void;\n loop?: boolean;\n dots?: boolean;\n counter?: boolean;\n thumbnails?: boolean;\n /** Overrides the visible-slide height cap for tall aspects (CSS length). */\n maxHeight?: string;\n labels?: Partial<CarouselLabels>;\n className?: string;\n\n // Editor\n onSlidesChange?: (slides: T[], index: number) => void;\n /** Called with the previous and next slide whenever a slide is updated or restored. */\n onSlideChange?: (change: SlideChange<T>) => void;\n /** Makes \"Add\" available; returns a brand-new slide with a fresh id. */\n createSlide?: () => T;\n /** Fresh id for a duplicate; defaults to `crypto.randomUUID()`. */\n createId?: () => string;\n /** Deep copy hook for duplicating; defaults to a shallow copy. */\n cloneSlide?: (slide: T) => T;\n /** Shows an Edit button; the host opens its own editor for the slide. */\n onEditSlide?: (slide: T, index: number) => void;\n minSlides?: number;\n maxSlides?: number;\n /** Kept versions per slide id, newest first — the host's store, read-only here. */\n history?: Readonly<Record<string, readonly SlideVersion<T>[]>>;\n /** Shows an Export button. The host runs the download. */\n onExport?: (slides: readonly T[]) => void;\n}\n\nconst MAX_DOTS = 12;\n/** Thumbnail geometry (px): the strip row is 64px with 2px padding top and bottom. */\nconst THUMB_HEIGHT = 60;\nconst THUMB_CANVAS = 360;\n\nfunction prefersReducedMotion(): boolean {\n return typeof window !== \"undefined\" && window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches === true;\n}\n\nexport function Carousel<T extends CarouselSlideBase>({\n slides,\n renderSlide,\n mode = \"viewer\",\n aspect = \"1:1\",\n index: controlledIndex,\n onIndexChange,\n loop = false,\n dots = true,\n counter = true,\n thumbnails = true,\n maxHeight = \"70vh\",\n labels: labelOverrides,\n className,\n onSlidesChange,\n onSlideChange,\n createSlide,\n createId,\n cloneSlide,\n onEditSlide,\n minSlides = DEFAULT_MIN_SLIDES,\n maxSlides = DEFAULT_MAX_SLIDES,\n history,\n onExport,\n}: CarouselProps<T>) {\n const labels = { ...DEFAULT_CAROUSEL_LABELS, ...labelOverrides };\n const count = slides.length;\n const editor = mode === \"editor\";\n const [ownIndex, setOwnIndex] = React.useState(0);\n const index = clampIndex(controlledIndex ?? ownIndex, count);\n\n const trackRef = React.useRef<HTMLDivElement>(null);\n const target = React.useRef<number | null>(null);\n const settle = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const setIndex = React.useCallback(\n (next: number) => {\n if (controlledIndex === undefined) setOwnIndex(next);\n onIndexChange?.(next);\n },\n [controlledIndex, onIndexChange],\n );\n\n const scrollToIndex = React.useCallback((i: number, smooth: boolean) => {\n const track = trackRef.current;\n if (!track) return;\n track.scrollTo?.({ left: i * track.clientWidth, behavior: smooth && !prefersReducedMotion() ? \"smooth\" : \"auto\" });\n }, []);\n\n const goTo = React.useCallback(\n (raw: number) => {\n const next = clampIndex(raw, count);\n target.current = next;\n if (settle.current) clearTimeout(settle.current);\n settle.current = setTimeout(() => {\n target.current = null;\n }, 700);\n setIndex(next);\n scrollToIndex(next, true);\n },\n [count, scrollToIndex, setIndex],\n );\n\n // Keep the scroller on the active slide when the index or the deck changes from outside\n // (add, delete, reorder, a controlled parent) — without fighting a smooth scroll in flight.\n React.useLayoutEffect(() => {\n const track = trackRef.current;\n if (!track || target.current !== null || track.clientWidth === 0) return;\n if (Math.round(track.scrollLeft / track.clientWidth) !== index) scrollToIndex(index, false);\n }, [index, count, scrollToIndex]);\n\n React.useEffect(() => {\n const track = trackRef.current;\n if (!track || typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(() => scrollToIndex(index, false));\n observer.observe(track);\n return () => observer.disconnect();\n }, [index, scrollToIndex]);\n\n React.useEffect(\n () => () => {\n if (settle.current) clearTimeout(settle.current);\n },\n [],\n );\n\n const onScroll = () => {\n const track = trackRef.current;\n if (!track || track.clientWidth === 0) return;\n const seen = clampIndex(Math.round(track.scrollLeft / track.clientWidth), count);\n if (target.current !== null) {\n if (seen === target.current) target.current = null;\n return;\n }\n if (seen !== index) setIndex(seen);\n };\n\n const onKeyDown = (event: React.KeyboardEvent) => {\n if (event.target !== event.currentTarget) return;\n if (event.key === \"ArrowLeft\") goTo(stepIndex(index, -1, count, loop));\n else if (event.key === \"ArrowRight\") goTo(stepIndex(index, 1, count, loop));\n else if (event.key === \"Home\") goTo(0);\n else if (event.key === \"End\") goTo(count - 1);\n else return;\n event.preventDefault();\n };\n\n // ---- editor operations -------------------------------------------------\n\n const emit = (edit: { slides: T[]; index: number } | null) => {\n if (!edit) return;\n onSlidesChange?.(edit.slides, edit.index);\n target.current = edit.index;\n setIndex(edit.index);\n // Land the scroller on the new active slide once the host's deck is rendered.\n requestAnimationFrame(() => {\n scrollToIndex(edit.index, false);\n target.current = null;\n });\n };\n\n const commitSlide = (next: T) => {\n const result = replaceSlide(slides, next);\n if (!result) return;\n onSlidesChange?.(result.slides, index);\n onSlideChange?.({ id: next.id, index: result.index, previous: result.previous, next });\n };\n\n const active = slides[index];\n const canAdd = editor && createSlide !== undefined && count < maxSlides;\n const canRemove = editor && count > minSlides;\n const activeHistory = active ? (history?.[active.id] ?? []) : [];\n\n const [w, h] = aspectRatioOf(aspect);\n const frameStyle: React.CSSProperties = {\n aspectRatio: `${w} / ${h}`,\n maxWidth: `min(100%, calc(${maxHeight} * ${w} / ${h}))`,\n };\n\n const contextFor = (_slide: T, i: number, thumbnail: boolean): CarouselSlideContext<T> => ({\n index: i,\n count,\n active: i === index,\n mode,\n thumbnail,\n update: thumbnail ? () => undefined : commitSlide,\n });\n\n return (\n // Its own provider: the control tooltips work in any host root (a portal, a window, a bare test).\n <TooltipProvider>\n <section\n aria-roledescription=\"carousel\"\n aria-label={labels.carousel}\n data-matrx-carousel={mode}\n className={cn(\"flex w-full min-w-0 flex-col gap-2\", className)}\n >\n <div\n className=\"relative mx-auto w-full overflow-hidden rounded-lg border border-border bg-muted\"\n style={frameStyle}\n >\n <div\n ref={trackRef}\n tabIndex={0}\n aria-live=\"polite\"\n aria-label={active ? labels.slide(index, count) : labels.carousel}\n onScroll={onScroll}\n onKeyDown={onKeyDown}\n className=\"flex h-full w-full snap-x snap-mandatory overflow-x-auto overscroll-x-contain outline-none [scrollbar-width:none] focus-visible:ring-2 focus-visible:ring-ring [&::-webkit-scrollbar]:hidden\"\n >\n {slides.map((slide, i) => (\n <div\n key={slide.id}\n role=\"group\"\n aria-roledescription=\"slide\"\n aria-label={labels.slide(i, count)}\n aria-hidden={i !== index}\n inert={i !== index}\n className=\"h-full w-full shrink-0 snap-center snap-always\"\n >\n {renderSlide(slide, contextFor(slide, i, false))}\n </div>\n ))}\n </div>\n\n {count > 1 && (\n <>\n <div className=\"pointer-events-none absolute inset-y-0 left-2 flex items-center\">\n <IconButton\n icon={ChevronLeftIcon}\n variant=\"outline\"\n tooltip={labels.previous}\n disabled={!loop && index === 0}\n onClick={() => goTo(stepIndex(index, -1, count, loop))}\n className=\"pointer-events-auto bg-card\"\n />\n </div>\n <div className=\"pointer-events-none absolute inset-y-0 right-2 flex items-center\">\n <IconButton\n icon={ChevronRightIcon}\n variant=\"outline\"\n tooltip={labels.next}\n disabled={!loop && index === count - 1}\n onClick={() => goTo(stepIndex(index, 1, count, loop))}\n className=\"pointer-events-auto bg-card\"\n />\n </div>\n </>\n )}\n {counter && count > 0 && (\n <span\n data-matrx-carousel-counter\n className=\"pointer-events-none absolute right-2 top-2 rounded-full bg-card px-2 py-0.5 text-xs tabular-nums text-foreground ring-1 ring-border\"\n >\n {index + 1} / {count}\n </span>\n )}\n </div>\n\n {/* One fixed-height row: dots when they fit, so the strip below never jumps. */}\n {dots && count > 1 && count <= MAX_DOTS && (\n <div className=\"flex h-3 items-center justify-center gap-1.5\" role=\"tablist\" aria-label={labels.carousel}>\n {slides.map((slide, i) => (\n <button\n key={slide.id}\n type=\"button\"\n role=\"tab\"\n aria-selected={i === index}\n aria-label={labels.slide(i, count)}\n onClick={() => goTo(i)}\n className={cn(\n \"h-1.5 rounded-full transition-[width,background-color]\",\n i === index ? \"w-4 bg-primary\" : \"w-1.5 bg-muted-foreground/40\",\n )}\n />\n ))}\n </div>\n )}\n\n {editor && (\n <div className=\"flex flex-wrap items-center justify-center gap-1\" role=\"toolbar\" aria-label={labels.carousel}>\n <IconButton\n icon={ArrowLeftIcon}\n tooltip={labels.moveEarlier}\n disabled={index === 0}\n onClick={() => emit(moveSlide(slides, index, index - 1, index))}\n />\n <IconButton\n icon={ArrowRightIcon}\n tooltip={labels.moveLater}\n disabled={index >= count - 1}\n onClick={() => emit(moveSlide(slides, index, index + 1, index))}\n />\n {onEditSlide && active && (\n <IconButton icon={PencilIcon} tooltip={labels.edit} onClick={() => onEditSlide(active, index)} />\n )}\n <IconButton\n icon={PlusIcon}\n tooltip={labels.add}\n disabled={!canAdd}\n onClick={() => createSlide && emit(insertSlide(slides, createSlide(), index, maxSlides))}\n />\n <IconButton\n icon={CopyIcon}\n tooltip={labels.duplicate}\n disabled={!active || count >= maxSlides}\n onClick={() =>\n emit(duplicateSlide(slides, index, createId ? createId() : crypto.randomUUID(), maxSlides, cloneSlide))\n }\n />\n <IconButton\n icon={Trash2Icon}\n tooltip={labels.remove}\n disabled={!canRemove}\n onClick={() => emit(removeSlide(slides, index, index, minSlides))}\n />\n {history !== undefined && active && (\n <Popover>\n <PopoverTrigger asChild>\n <IconButton icon={HistoryIcon} tooltip={labels.history} />\n </PopoverTrigger>\n <PopoverContent className=\"w-56 p-1\" align=\"center\">\n {activeHistory.length === 0 ? (\n <p className=\"px-2 py-1.5 text-xs text-muted-foreground\">{labels.noHistory}</p>\n ) : (\n <ul className=\"flex flex-col\">\n {activeHistory.map((version) => (\n <li key={version.at} className=\"flex items-center justify-between gap-2 px-2 py-1\">\n <time className=\"text-xs tabular-nums text-muted-foreground\" dateTime={new Date(version.at).toISOString()}>\n {new Date(version.at).toLocaleTimeString([], { hour: \"numeric\", minute: \"2-digit\" })}\n </time>\n <button\n type=\"button\"\n className=\"text-xs text-primary hover:underline\"\n onClick={() => commitSlide({ ...version.slide, id: active.id })}\n >\n {labels.restore}\n </button>\n </li>\n ))}\n </ul>\n )}\n </PopoverContent>\n </Popover>\n )}\n {onExport && (\n <IconButton icon={DownloadIcon} tooltip={labels.export} onClick={() => onExport(slides)} />\n )}\n </div>\n )}\n\n {thumbnails && count > 1 && (\n <Thumbnails\n slides={slides}\n index={index}\n aspect={aspect}\n editor={editor}\n onSelect={goTo}\n onReorder={(from, to) => emit(moveSlide(slides, from, to, index))}\n renderThumb={(slide, i) => renderSlide(slide, contextFor(slide, i, true))}\n label={labels.slide}\n />\n )}\n </section>\n </TooltipProvider>\n );\n}\n\nfunction Thumbnails<T extends CarouselSlideBase>({\n slides,\n index,\n aspect,\n editor,\n onSelect,\n onReorder,\n renderThumb,\n label,\n}: {\n slides: readonly T[];\n index: number;\n aspect: CarouselAspect;\n editor: boolean;\n onSelect: (i: number) => void;\n onReorder: (from: number, to: number) => void;\n renderThumb: (slide: T, i: number) => React.ReactNode;\n label: CarouselLabels[\"slide\"];\n}) {\n const [w, h] = aspectRatioOf(aspect);\n const dragFrom = React.useRef<number | null>(null);\n const release = React.useRef<(() => void) | null>(null);\n const [over, setOver] = React.useState<number | null>(null);\n const activeRef = React.useRef<HTMLButtonElement>(null);\n\n React.useEffect(() => {\n activeRef.current?.scrollIntoView?.({ block: \"nearest\", inline: \"nearest\" });\n }, [index]);\n\n const end = () => {\n dragFrom.current = null;\n release.current?.();\n release.current = null;\n setOver(null);\n };\n\n return (\n <div\n data-matrx-carousel-thumbnails\n className=\"flex h-16 gap-2 overflow-x-auto py-0.5 [scrollbar-width:thin]\"\n >\n {slides.map((slide, i) => (\n <button\n key={slide.id}\n ref={i === index ? activeRef : undefined}\n type=\"button\"\n aria-label={label(i, slides.length)}\n aria-current={i === index}\n draggable={editor}\n data-clickable\n onClick={() => onSelect(i)}\n onDragStart={(event) => {\n dragFrom.current = i;\n release.current = holdDragCursor(\"grabbing\");\n event.dataTransfer.effectAllowed = \"move\";\n event.dataTransfer.setData(\"text/plain\", slide.id);\n }}\n onDragOver={(event) => {\n if (dragFrom.current === null) return;\n event.preventDefault();\n setOver(i);\n }}\n onDrop={(event) => {\n event.preventDefault();\n const from = dragFrom.current;\n end();\n if (from !== null && from !== i) onReorder(from, i);\n }}\n onDragEnd={end}\n style={{ aspectRatio: `${w} / ${h}`, height: \"100%\" }}\n className={cn(\n \"relative shrink-0 overflow-hidden rounded-md border bg-muted\",\n i === index ? \"border-primary ring-2 ring-primary/40\" : \"border-border opacity-80 hover:opacity-100\",\n over === i && dragFrom.current !== i && \"ring-2 ring-ring\",\n )}\n >\n {/* The slide draws at a fixed 360px-wide canvas and is scaled down, so a thumbnail is a true miniature. */}\n <span\n className=\"pointer-events-none block origin-top-left\"\n style={{\n width: THUMB_CANVAS,\n height: (THUMB_CANVAS * h) / w,\n transform: `scale(${(THUMB_HEIGHT * (w / h)) / THUMB_CANVAS})`,\n }}\n >\n {renderThumb(slide, i)}\n </span>\n <span className=\"pointer-events-none absolute bottom-0.5 left-0.5 rounded bg-card px-1 text-[10px] tabular-nums text-foreground\">\n {i + 1}\n </span>\n </button>\n ))}\n </div>\n );\n}\n","/**\n * The carousel's pure slide logic — reorder, add, duplicate, delete, index\n * bookkeeping, and the version-history contract. No React, no DOM: the\n * component only calls these, so the editing rules are proven without a\n * browser (`model.test.ts`).\n *\n * VERSION HISTORY IS A STORE CONTRACT, NOT STORAGE. The carousel never saves\n * anything. When a slide is edited it reports `{ previous, next }`; the host\n * keeps `previous` wherever it keeps history (`pushVersion` is the one\n * bounded, newest-first way to do it) and hands the list back through the\n * `history` prop so the History menu can show it.\n */\n\n/** Every slide carries a stable id — reorder and history are keyed on it. */\nexport interface CarouselSlideBase {\n readonly id: string;\n}\n\n/** The aspect ratios social and presentation slides come in. */\nexport const CAROUSEL_ASPECTS = [\"1:1\", \"4:5\", \"9:16\", \"16:9\"] as const;\nexport type CarouselAspect = (typeof CAROUSEL_ASPECTS)[number];\n\n/** `[width, height]` of an aspect — the one place the ratio is spelled. */\nexport function aspectRatioOf(aspect: CarouselAspect): readonly [number, number] {\n switch (aspect) {\n case \"1:1\":\n return [1, 1];\n case \"4:5\":\n return [4, 5];\n case \"9:16\":\n return [9, 16];\n case \"16:9\":\n return [16, 9];\n }\n}\n\n/** Social carousels run 2-10 slides; a host with other limits passes its own. */\nexport const DEFAULT_MIN_SLIDES = 1;\nexport const DEFAULT_MAX_SLIDES = 10;\nexport const DEFAULT_HISTORY_LIMIT = 20;\n\nexport function clampIndex(index: number, length: number): number {\n if (length <= 0) return 0;\n if (!Number.isFinite(index)) return 0;\n return Math.min(Math.max(Math.trunc(index), 0), length - 1);\n}\n\n/** Step `index` by `delta`; wraps when `loop`, otherwise stops at the ends. */\nexport function stepIndex(index: number, delta: number, length: number, loop: boolean): number {\n if (length <= 0) return 0;\n const next = index + delta;\n if (loop) return ((next % length) + length) % length;\n return clampIndex(next, length);\n}\n\nexport interface SlideEdit<T> {\n readonly slides: T[];\n /** Where the active slide ends up after the edit. */\n readonly index: number;\n}\n\n/** Move the slide at `from` to position `to`. A no-op returns the same array. */\nexport function moveSlide<T>(slides: readonly T[], from: number, to: number, active: number): SlideEdit<T> {\n const last = slides.length - 1;\n if (from < 0 || from > last || to < 0 || to > last || from === to) {\n return { slides: slides as T[], index: active };\n }\n const next = slides.slice();\n const [moved] = next.splice(from, 1);\n next.splice(to, 0, moved as T);\n // The active slide follows the same item through the move.\n let index = active;\n if (active === from) index = to;\n else if (from < active && to >= active) index = active - 1;\n else if (from > active && to <= active) index = active + 1;\n return { slides: next, index };\n}\n\n/** Insert `slide` after `after` (default: at the end); the new slide becomes active. */\nexport function insertSlide<T>(\n slides: readonly T[],\n slide: T,\n after: number = slides.length - 1,\n max: number = DEFAULT_MAX_SLIDES,\n): SlideEdit<T> | null {\n if (slides.length >= max) return null;\n const at = clampIndex(after, slides.length + 1) + (slides.length === 0 ? 0 : 1);\n const next = slides.slice();\n next.splice(at, 0, slide);\n return { slides: next, index: at };\n}\n\n/** Copy the slide at `index` right after itself with a new id; the copy becomes active. */\nexport function duplicateSlide<T extends CarouselSlideBase>(\n slides: readonly T[],\n index: number,\n newId: string,\n max: number = DEFAULT_MAX_SLIDES,\n clone: (slide: T) => T = (slide) => ({ ...slide }),\n): SlideEdit<T> | null {\n const source = slides[index];\n if (!source || slides.length >= max) return null;\n const copy = { ...clone(source), id: newId } as T;\n const next = slides.slice();\n next.splice(index + 1, 0, copy);\n return { slides: next, index: index + 1 };\n}\n\n/** Remove the slide at `index`; refuses below `min` and keeps the active slide in range. */\nexport function removeSlide<T>(\n slides: readonly T[],\n index: number,\n active: number,\n min: number = DEFAULT_MIN_SLIDES,\n): SlideEdit<T> | null {\n if (index < 0 || index >= slides.length || slides.length <= min) return null;\n const next = slides.slice();\n next.splice(index, 1);\n let nextActive = active;\n if (index < active || (index === active && active === slides.length - 1)) nextActive = active - 1;\n return { slides: next, index: clampIndex(nextActive, next.length) };\n}\n\n/** Replace the slide with `next.id`; reports what it replaced. */\nexport function replaceSlide<T extends CarouselSlideBase>(\n slides: readonly T[],\n next: T,\n): { slides: T[]; previous: T; index: number } | null {\n const index = slides.findIndex((slide) => slide.id === next.id);\n const previous = slides[index];\n if (index < 0 || previous === undefined) return null;\n const copy = slides.slice();\n copy[index] = next;\n return { slides: copy, previous, index };\n}\n\n// ---------------------------------------------------------------------------\n// Version history — the store contract.\n// ---------------------------------------------------------------------------\n\n/** One kept version of a slide; `at` is epoch ms the host stamped when it kept it. */\nexport interface SlideVersion<T> {\n readonly slide: T;\n readonly at: number;\n}\n\n/** What `onSlideChange` hands the host: the slide before, the slide after. */\nexport interface SlideChange<T> {\n readonly id: string;\n readonly index: number;\n readonly previous: T;\n readonly next: T;\n}\n\n/** Keep `previous` at the head of `history`, newest first, bounded to `limit`. */\nexport function pushVersion<T>(\n history: readonly SlideVersion<T>[] | undefined,\n previous: T,\n at: number,\n limit: number = DEFAULT_HISTORY_LIMIT,\n): SlideVersion<T>[] {\n return [{ slide: previous, at }, ...(history ?? [])].slice(0, Math.max(limit, 0));\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuBA,YAAY,WAAW;;;ACJhB,IAAM,mBAAmB,CAAC,OAAO,OAAO,QAAQ,MAAM;AAItD,SAAS,cAAc,QAAmD;AAC/E,UAAQ,QAAQ;AAAA,IACd,KAAK;AACH,aAAO,CAAC,GAAG,CAAC;AAAA,IACd,KAAK;AACH,aAAO,CAAC,GAAG,CAAC;AAAA,IACd,KAAK;AACH,aAAO,CAAC,GAAG,EAAE;AAAA,IACf,KAAK;AACH,aAAO,CAAC,IAAI,CAAC;AAAA,EACjB;AACF;AAGO,IAAM,qBAAqB;AAC3B,IAAM,qBAAqB;AAC3B,IAAM,wBAAwB;AAE9B,SAAS,WAAW,OAAe,QAAwB;AAChE,MAAI,UAAU,EAAG,QAAO;AACxB,MAAI,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO;AACpC,SAAO,KAAK,IAAI,KAAK,IAAI,KAAK,MAAM,KAAK,GAAG,CAAC,GAAG,SAAS,CAAC;AAC5D;AAGO,SAAS,UAAU,OAAe,OAAe,QAAgB,MAAuB;AAC7F,MAAI,UAAU,EAAG,QAAO;AACxB,QAAM,OAAO,QAAQ;AACrB,MAAI,KAAM,SAAS,OAAO,SAAU,UAAU;AAC9C,SAAO,WAAW,MAAM,MAAM;AAChC;AASO,SAAS,UAAa,QAAsB,MAAc,IAAY,QAA8B;AACzG,QAAM,OAAO,OAAO,SAAS;AAC7B,MAAI,OAAO,KAAK,OAAO,QAAQ,KAAK,KAAK,KAAK,QAAQ,SAAS,IAAI;AACjE,WAAO,EAAE,QAAuB,OAAO,OAAO;AAAA,EAChD;AACA,QAAM,OAAO,OAAO,MAAM;AAC1B,QAAM,CAAC,KAAK,IAAI,KAAK,OAAO,MAAM,CAAC;AACnC,OAAK,OAAO,IAAI,GAAG,KAAU;AAE7B,MAAI,QAAQ;AACZ,MAAI,WAAW,KAAM,SAAQ;AAAA,WACpB,OAAO,UAAU,MAAM,OAAQ,SAAQ,SAAS;AAAA,WAChD,OAAO,UAAU,MAAM,OAAQ,SAAQ,SAAS;AACzD,SAAO,EAAE,QAAQ,MAAM,MAAM;AAC/B;AAGO,SAAS,YACd,QACA,OACA,QAAgB,OAAO,SAAS,GAChC,MAAc,oBACO;AACrB,MAAI,OAAO,UAAU,IAAK,QAAO;AACjC,QAAM,KAAK,WAAW,OAAO,OAAO,SAAS,CAAC,KAAK,OAAO,WAAW,IAAI,IAAI;AAC7E,QAAM,OAAO,OAAO,MAAM;AAC1B,OAAK,OAAO,IAAI,GAAG,KAAK;AACxB,SAAO,EAAE,QAAQ,MAAM,OAAO,GAAG;AACnC;AAGO,SAAS,eACd,QACA,OACA,OACA,MAAc,oBACd,QAAyB,CAAC,WAAW,EAAE,GAAG,MAAM,IAC3B;AACrB,QAAM,SAAS,OAAO,KAAK;AAC3B,MAAI,CAAC,UAAU,OAAO,UAAU,IAAK,QAAO;AAC5C,QAAM,OAAO,EAAE,GAAG,MAAM,MAAM,GAAG,IAAI,MAAM;AAC3C,QAAM,OAAO,OAAO,MAAM;AAC1B,OAAK,OAAO,QAAQ,GAAG,GAAG,IAAI;AAC9B,SAAO,EAAE,QAAQ,MAAM,OAAO,QAAQ,EAAE;AAC1C;AAGO,SAAS,YACd,QACA,OACA,QACA,MAAc,oBACO;AACrB,MAAI,QAAQ,KAAK,SAAS,OAAO,UAAU,OAAO,UAAU,IAAK,QAAO;AACxE,QAAM,OAAO,OAAO,MAAM;AAC1B,OAAK,OAAO,OAAO,CAAC;AACpB,MAAI,aAAa;AACjB,MAAI,QAAQ,UAAW,UAAU,UAAU,WAAW,OAAO,SAAS,EAAI,cAAa,SAAS;AAChG,SAAO,EAAE,QAAQ,MAAM,OAAO,WAAW,YAAY,KAAK,MAAM,EAAE;AACpE;AAGO,SAAS,aACd,QACA,MACoD;AACpD,QAAM,QAAQ,OAAO,UAAU,CAAC,UAAU,MAAM,OAAO,KAAK,EAAE;AAC9D,QAAM,WAAW,OAAO,KAAK;AAC7B,MAAI,QAAQ,KAAK,aAAa,OAAW,QAAO;AAChD,QAAM,OAAO,OAAO,MAAM;AAC1B,OAAK,KAAK,IAAI;AACd,SAAO,EAAE,QAAQ,MAAM,UAAU,MAAM;AACzC;AAqBO,SAAS,YACd,SACA,UACA,IACA,QAAgB,uBACG;AACnB,SAAO,CAAC,EAAE,OAAO,UAAU,GAAG,GAAG,GAAI,WAAW,CAAC,CAAE,EAAE,MAAM,GAAG,KAAK,IAAI,OAAO,CAAC,CAAC;AAClF;;;ADwJY,SAeF,UAfE,KAeF,YAfE;AA3OL,IAAM,0BAA0C;AAAA,EACrD,UAAU;AAAA,EACV,UAAU;AAAA,EACV,MAAM;AAAA,EACN,OAAO,CAAC,OAAO,UAAU,SAAS,QAAQ,CAAC,OAAO,KAAK;AAAA,EACvD,KAAK;AAAA,EACL,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,WAAW;AAAA,EACX,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS;AAAA,EACT,WAAW;AAAA,EACX,QAAQ;AACV;AAwCA,IAAM,WAAW;AAEjB,IAAM,eAAe;AACrB,IAAM,eAAe;AAErB,SAAS,uBAAgC;AACvC,SAAO,OAAO,WAAW,eAAe,OAAO,aAAa,kCAAkC,EAAE,YAAY;AAC9G;AAEO,SAAS,SAAsC;AAAA,EACpD;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP,SAAS;AAAA,EACT,OAAO;AAAA,EACP;AAAA,EACA,OAAO;AAAA,EACP,OAAO;AAAA,EACP,UAAU;AAAA,EACV,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ;AAAA,EACA;AACF,GAAqB;AACnB,QAAM,SAAS,EAAE,GAAG,yBAAyB,GAAG,eAAe;AAC/D,QAAM,QAAQ,OAAO;AACrB,QAAM,SAAS,SAAS;AACxB,QAAM,CAAC,UAAU,WAAW,IAAU,eAAS,CAAC;AAChD,QAAM,QAAQ,WAAW,mBAAmB,UAAU,KAAK;AAE3D,QAAM,WAAiB,aAAuB,IAAI;AAClD,QAAM,SAAe,aAAsB,IAAI;AAC/C,QAAM,SAAe,aAA6C,IAAI;AAEtE,QAAM,WAAiB;AAAA,IACrB,CAAC,SAAiB;AAChB,UAAI,oBAAoB,OAAW,aAAY,IAAI;AACnD,sBAAgB,IAAI;AAAA,IACtB;AAAA,IACA,CAAC,iBAAiB,aAAa;AAAA,EACjC;AAEA,QAAM,gBAAsB,kBAAY,CAAC,GAAW,WAAoB;AACtE,UAAM,QAAQ,SAAS;AACvB,QAAI,CAAC,MAAO;AACZ,UAAM,WAAW,EAAE,MAAM,IAAI,MAAM,aAAa,UAAU,UAAU,CAAC,qBAAqB,IAAI,WAAW,OAAO,CAAC;AAAA,EACnH,GAAG,CAAC,CAAC;AAEL,QAAM,OAAa;AAAA,IACjB,CAAC,QAAgB;AACf,YAAM,OAAO,WAAW,KAAK,KAAK;AAClC,aAAO,UAAU;AACjB,UAAI,OAAO,QAAS,cAAa,OAAO,OAAO;AAC/C,aAAO,UAAU,WAAW,MAAM;AAChC,eAAO,UAAU;AAAA,MACnB,GAAG,GAAG;AACN,eAAS,IAAI;AACb,oBAAc,MAAM,IAAI;AAAA,IAC1B;AAAA,IACA,CAAC,OAAO,eAAe,QAAQ;AAAA,EACjC;AAIA,EAAM,sBAAgB,MAAM;AAC1B,UAAM,QAAQ,SAAS;AACvB,QAAI,CAAC,SAAS,OAAO,YAAY,QAAQ,MAAM,gBAAgB,EAAG;AAClE,QAAI,KAAK,MAAM,MAAM,aAAa,MAAM,WAAW,MAAM,MAAO,eAAc,OAAO,KAAK;AAAA,EAC5F,GAAG,CAAC,OAAO,OAAO,aAAa,CAAC;AAEhC,EAAM,gBAAU,MAAM;AACpB,UAAM,QAAQ,SAAS;AACvB,QAAI,CAAC,SAAS,OAAO,mBAAmB,YAAa;AACrD,UAAM,WAAW,IAAI,eAAe,MAAM,cAAc,OAAO,KAAK,CAAC;AACrE,aAAS,QAAQ,KAAK;AACtB,WAAO,MAAM,SAAS,WAAW;AAAA,EACnC,GAAG,CAAC,OAAO,aAAa,CAAC;AAEzB,EAAM;AAAA,IACJ,MAAM,MAAM;AACV,UAAI,OAAO,QAAS,cAAa,OAAO,OAAO;AAAA,IACjD;AAAA,IACA,CAAC;AAAA,EACH;AAEA,QAAM,WAAW,MAAM;AACrB,UAAM,QAAQ,SAAS;AACvB,QAAI,CAAC,SAAS,MAAM,gBAAgB,EAAG;AACvC,UAAM,OAAO,WAAW,KAAK,MAAM,MAAM,aAAa,MAAM,WAAW,GAAG,KAAK;AAC/E,QAAI,OAAO,YAAY,MAAM;AAC3B,UAAI,SAAS,OAAO,QAAS,QAAO,UAAU;AAC9C;AAAA,IACF;AACA,QAAI,SAAS,MAAO,UAAS,IAAI;AAAA,EACnC;AAEA,QAAM,YAAY,CAAC,UAA+B;AAChD,QAAI,MAAM,WAAW,MAAM,cAAe;AAC1C,QAAI,MAAM,QAAQ,YAAa,MAAK,UAAU,OAAO,IAAI,OAAO,IAAI,CAAC;AAAA,aAC5D,MAAM,QAAQ,aAAc,MAAK,UAAU,OAAO,GAAG,OAAO,IAAI,CAAC;AAAA,aACjE,MAAM,QAAQ,OAAQ,MAAK,CAAC;AAAA,aAC5B,MAAM,QAAQ,MAAO,MAAK,QAAQ,CAAC;AAAA,QACvC;AACL,UAAM,eAAe;AAAA,EACvB;AAIA,QAAM,OAAO,CAAC,SAAgD;AAC5D,QAAI,CAAC,KAAM;AACX,qBAAiB,KAAK,QAAQ,KAAK,KAAK;AACxC,WAAO,UAAU,KAAK;AACtB,aAAS,KAAK,KAAK;AAEnB,0BAAsB,MAAM;AAC1B,oBAAc,KAAK,OAAO,KAAK;AAC/B,aAAO,UAAU;AAAA,IACnB,CAAC;AAAA,EACH;AAEA,QAAM,cAAc,CAAC,SAAY;AAC/B,UAAM,SAAS,aAAa,QAAQ,IAAI;AACxC,QAAI,CAAC,OAAQ;AACb,qBAAiB,OAAO,QAAQ,KAAK;AACrC,oBAAgB,EAAE,IAAI,KAAK,IAAI,OAAO,OAAO,OAAO,UAAU,OAAO,UAAU,KAAK,CAAC;AAAA,EACvF;AAEA,QAAM,SAAS,OAAO,KAAK;AAC3B,QAAM,SAAS,UAAU,gBAAgB,UAAa,QAAQ;AAC9D,QAAM,YAAY,UAAU,QAAQ;AACpC,QAAM,gBAAgB,SAAU,UAAU,OAAO,EAAE,KAAK,CAAC,IAAK,CAAC;AAE/D,QAAM,CAAC,GAAG,CAAC,IAAI,cAAc,MAAM;AACnC,QAAM,aAAkC;AAAA,IACtC,aAAa,GAAG,CAAC,MAAM,CAAC;AAAA,IACxB,UAAU,kBAAkB,SAAS,MAAM,CAAC,MAAM,CAAC;AAAA,EACrD;AAEA,QAAM,aAAa,CAAC,QAAW,GAAW,eAAiD;AAAA,IACzF,OAAO;AAAA,IACP;AAAA,IACA,QAAQ,MAAM;AAAA,IACd;AAAA,IACA;AAAA,IACA,QAAQ,YAAY,MAAM,SAAY;AAAA,EACxC;AAEA;AAAA;AAAA,IAEE,oBAAC,mBACD;AAAA,MAAC;AAAA;AAAA,QACC,wBAAqB;AAAA,QACrB,cAAY,OAAO;AAAA,QACnB,uBAAqB;AAAA,QACrB,WAAW,GAAG,sCAAsC,SAAS;AAAA,QAE7D;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,WAAU;AAAA,cACV,OAAO;AAAA,cAEP;AAAA;AAAA,kBAAC;AAAA;AAAA,oBACC,KAAK;AAAA,oBACL,UAAU;AAAA,oBACV,aAAU;AAAA,oBACV,cAAY,SAAS,OAAO,MAAM,OAAO,KAAK,IAAI,OAAO;AAAA,oBACzD;AAAA,oBACA;AAAA,oBACA,WAAU;AAAA,oBAET,iBAAO,IAAI,CAAC,OAAO,MAClB;AAAA,sBAAC;AAAA;AAAA,wBAEC,MAAK;AAAA,wBACL,wBAAqB;AAAA,wBACrB,cAAY,OAAO,MAAM,GAAG,KAAK;AAAA,wBACjC,eAAa,MAAM;AAAA,wBACnB,OAAO,MAAM;AAAA,wBACb,WAAU;AAAA,wBAET,sBAAY,OAAO,WAAW,OAAO,GAAG,KAAK,CAAC;AAAA;AAAA,sBAR1C,MAAM;AAAA,oBASb,CACD;AAAA;AAAA,gBACH;AAAA,gBAEC,QAAQ,KACP,iCACE;AAAA,sCAAC,SAAI,WAAU,mEACb;AAAA,oBAAC;AAAA;AAAA,sBACC,MAAM;AAAA,sBACN,SAAQ;AAAA,sBACR,SAAS,OAAO;AAAA,sBAChB,UAAU,CAAC,QAAQ,UAAU;AAAA,sBAC7B,SAAS,MAAM,KAAK,UAAU,OAAO,IAAI,OAAO,IAAI,CAAC;AAAA,sBACrD,WAAU;AAAA;AAAA,kBACZ,GACF;AAAA,kBACA,oBAAC,SAAI,WAAU,oEACb;AAAA,oBAAC;AAAA;AAAA,sBACC,MAAM;AAAA,sBACN,SAAQ;AAAA,sBACR,SAAS,OAAO;AAAA,sBAChB,UAAU,CAAC,QAAQ,UAAU,QAAQ;AAAA,sBACrC,SAAS,MAAM,KAAK,UAAU,OAAO,GAAG,OAAO,IAAI,CAAC;AAAA,sBACpD,WAAU;AAAA;AAAA,kBACZ,GACF;AAAA,mBACF;AAAA,gBAED,WAAW,QAAQ,KAClB;AAAA,kBAAC;AAAA;AAAA,oBACC,+BAA2B;AAAA,oBAC3B,WAAU;AAAA,oBAET;AAAA,8BAAQ;AAAA,sBAAE;AAAA,sBAAI;AAAA;AAAA;AAAA,gBACjB;AAAA;AAAA;AAAA,UAEJ;AAAA,UAGC,QAAQ,QAAQ,KAAK,SAAS,YAC7B,oBAAC,SAAI,WAAU,gDAA+C,MAAK,WAAU,cAAY,OAAO,UAC7F,iBAAO,IAAI,CAAC,OAAO,MAClB;AAAA,YAAC;AAAA;AAAA,cAEC,MAAK;AAAA,cACL,MAAK;AAAA,cACL,iBAAe,MAAM;AAAA,cACrB,cAAY,OAAO,MAAM,GAAG,KAAK;AAAA,cACjC,SAAS,MAAM,KAAK,CAAC;AAAA,cACrB,WAAW;AAAA,gBACT;AAAA,gBACA,MAAM,QAAQ,mBAAmB;AAAA,cACnC;AAAA;AAAA,YATK,MAAM;AAAA,UAUb,CACD,GACH;AAAA,UAGD,UACC,qBAAC,SAAI,WAAU,oDAAmD,MAAK,WAAU,cAAY,OAAO,UAClG;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,MAAM;AAAA,gBACN,SAAS,OAAO;AAAA,gBAChB,UAAU,UAAU;AAAA,gBACpB,SAAS,MAAM,KAAK,UAAU,QAAQ,OAAO,QAAQ,GAAG,KAAK,CAAC;AAAA;AAAA,YAChE;AAAA,YACA;AAAA,cAAC;AAAA;AAAA,gBACC,MAAM;AAAA,gBACN,SAAS,OAAO;AAAA,gBAChB,UAAU,SAAS,QAAQ;AAAA,gBAC3B,SAAS,MAAM,KAAK,UAAU,QAAQ,OAAO,QAAQ,GAAG,KAAK,CAAC;AAAA;AAAA,YAChE;AAAA,YACC,eAAe,UACd,oBAAC,cAAW,MAAM,YAAY,SAAS,OAAO,MAAM,SAAS,MAAM,YAAY,QAAQ,KAAK,GAAG;AAAA,YAEjG;AAAA,cAAC;AAAA;AAAA,gBACC,MAAM;AAAA,gBACN,SAAS,OAAO;AAAA,gBAChB,UAAU,CAAC;AAAA,gBACX,SAAS,MAAM,eAAe,KAAK,YAAY,QAAQ,YAAY,GAAG,OAAO,SAAS,CAAC;AAAA;AAAA,YACzF;AAAA,YACA;AAAA,cAAC;AAAA;AAAA,gBACC,MAAM;AAAA,gBACN,SAAS,OAAO;AAAA,gBAChB,UAAU,CAAC,UAAU,SAAS;AAAA,gBAC9B,SAAS,MACP,KAAK,eAAe,QAAQ,OAAO,WAAW,SAAS,IAAI,OAAO,WAAW,GAAG,WAAW,UAAU,CAAC;AAAA;AAAA,YAE1G;AAAA,YACA;AAAA,cAAC;AAAA;AAAA,gBACC,MAAM;AAAA,gBACN,SAAS,OAAO;AAAA,gBAChB,UAAU,CAAC;AAAA,gBACX,SAAS,MAAM,KAAK,YAAY,QAAQ,OAAO,OAAO,SAAS,CAAC;AAAA;AAAA,YAClE;AAAA,YACC,YAAY,UAAa,UACxB,qBAAC,WACC;AAAA,kCAAC,kBAAe,SAAO,MACrB,8BAAC,cAAW,MAAM,aAAa,SAAS,OAAO,SAAS,GAC1D;AAAA,cACA,oBAAC,kBAAe,WAAU,YAAW,OAAM,UACxC,wBAAc,WAAW,IACxB,oBAAC,OAAE,WAAU,6CAA6C,iBAAO,WAAU,IAE3E,oBAAC,QAAG,WAAU,iBACX,wBAAc,IAAI,CAAC,YAClB,qBAAC,QAAoB,WAAU,qDAC7B;AAAA,oCAAC,UAAK,WAAU,8CAA6C,UAAU,IAAI,KAAK,QAAQ,EAAE,EAAE,YAAY,GACrG,cAAI,KAAK,QAAQ,EAAE,EAAE,mBAAmB,CAAC,GAAG,EAAE,MAAM,WAAW,QAAQ,UAAU,CAAC,GACrF;AAAA,gBACA;AAAA,kBAAC;AAAA;AAAA,oBACC,MAAK;AAAA,oBACL,WAAU;AAAA,oBACV,SAAS,MAAM,YAAY,EAAE,GAAG,QAAQ,OAAO,IAAI,OAAO,GAAG,CAAC;AAAA,oBAE7D,iBAAO;AAAA;AAAA,gBACV;AAAA,mBAVO,QAAQ,EAWjB,CACD,GACH,GAEJ;AAAA,eACF;AAAA,YAED,YACC,oBAAC,cAAW,MAAM,cAAc,SAAS,OAAO,QAAQ,SAAS,MAAM,SAAS,MAAM,GAAG;AAAA,aAE7F;AAAA,UAGD,cAAc,QAAQ,KACrB;AAAA,YAAC;AAAA;AAAA,cACC;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA,cACA,UAAU;AAAA,cACV,WAAW,CAAC,MAAM,OAAO,KAAK,UAAU,QAAQ,MAAM,IAAI,KAAK,CAAC;AAAA,cAChE,aAAa,CAAC,OAAO,MAAM,YAAY,OAAO,WAAW,OAAO,GAAG,IAAI,CAAC;AAAA,cACxE,OAAO,OAAO;AAAA;AAAA,UAChB;AAAA;AAAA;AAAA,IAEJ,GACA;AAAA;AAEJ;AAEA,SAAS,WAAwC;AAAA,EAC/C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GASG;AACD,QAAM,CAAC,GAAG,CAAC,IAAI,cAAc,MAAM;AACnC,QAAM,WAAiB,aAAsB,IAAI;AACjD,QAAM,UAAgB,aAA4B,IAAI;AACtD,QAAM,CAAC,MAAM,OAAO,IAAU,eAAwB,IAAI;AAC1D,QAAM,YAAkB,aAA0B,IAAI;AAEtD,EAAM,gBAAU,MAAM;AACpB,cAAU,SAAS,iBAAiB,EAAE,OAAO,WAAW,QAAQ,UAAU,CAAC;AAAA,EAC7E,GAAG,CAAC,KAAK,CAAC;AAEV,QAAM,MAAM,MAAM;AAChB,aAAS,UAAU;AACnB,YAAQ,UAAU;AAClB,YAAQ,UAAU;AAClB,YAAQ,IAAI;AAAA,EACd;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,kCAA8B;AAAA,MAC9B,WAAU;AAAA,MAET,iBAAO,IAAI,CAAC,OAAO,MAClB;AAAA,QAAC;AAAA;AAAA,UAEC,KAAK,MAAM,QAAQ,YAAY;AAAA,UAC/B,MAAK;AAAA,UACL,cAAY,MAAM,GAAG,OAAO,MAAM;AAAA,UAClC,gBAAc,MAAM;AAAA,UACpB,WAAW;AAAA,UACX,kBAAc;AAAA,UACd,SAAS,MAAM,SAAS,CAAC;AAAA,UACzB,aAAa,CAAC,UAAU;AACtB,qBAAS,UAAU;AACnB,oBAAQ,UAAU,eAAe,UAAU;AAC3C,kBAAM,aAAa,gBAAgB;AACnC,kBAAM,aAAa,QAAQ,cAAc,MAAM,EAAE;AAAA,UACnD;AAAA,UACA,YAAY,CAAC,UAAU;AACrB,gBAAI,SAAS,YAAY,KAAM;AAC/B,kBAAM,eAAe;AACrB,oBAAQ,CAAC;AAAA,UACX;AAAA,UACA,QAAQ,CAAC,UAAU;AACjB,kBAAM,eAAe;AACrB,kBAAM,OAAO,SAAS;AACtB,gBAAI;AACJ,gBAAI,SAAS,QAAQ,SAAS,EAAG,WAAU,MAAM,CAAC;AAAA,UACpD;AAAA,UACA,WAAW;AAAA,UACX,OAAO,EAAE,aAAa,GAAG,CAAC,MAAM,CAAC,IAAI,QAAQ,OAAO;AAAA,UACpD,WAAW;AAAA,YACT;AAAA,YACA,MAAM,QAAQ,0CAA0C;AAAA,YACxD,SAAS,KAAK,SAAS,YAAY,KAAK;AAAA,UAC1C;AAAA,UAGA;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,WAAU;AAAA,gBACV,OAAO;AAAA,kBACL,OAAO;AAAA,kBACP,QAAS,eAAe,IAAK;AAAA,kBAC7B,WAAW,SAAU,gBAAgB,IAAI,KAAM,YAAY;AAAA,gBAC7D;AAAA,gBAEC,sBAAY,OAAO,CAAC;AAAA;AAAA,YACvB;AAAA,YACA,oBAAC,UAAK,WAAU,kHACb,cAAI,GACP;AAAA;AAAA;AAAA,QA9CK,MAAM;AAAA,MA+Cb,CACD;AAAA;AAAA,EACH;AAEJ;","names":[]}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import {
|
|
3
|
+
holdDragCursor
|
|
4
|
+
} from "./chunk-LEPD6TV7.js";
|
|
2
5
|
import {
|
|
3
6
|
CloseRow,
|
|
4
7
|
CloseSlotProvider,
|
|
@@ -12,9 +15,6 @@ import {
|
|
|
12
15
|
useModalLayer,
|
|
13
16
|
useRadixDialogModal
|
|
14
17
|
} from "./chunk-NDTDBBMM.js";
|
|
15
|
-
import {
|
|
16
|
-
holdDragCursor
|
|
17
|
-
} from "./chunk-LEPD6TV7.js";
|
|
18
18
|
import {
|
|
19
19
|
keepMovedFocus
|
|
20
20
|
} from "./chunk-DGNHZBJY.js";
|
|
@@ -613,4 +613,4 @@ export {
|
|
|
613
613
|
DialogTitle,
|
|
614
614
|
DialogDescription
|
|
615
615
|
};
|
|
616
|
-
//# sourceMappingURL=chunk-
|
|
616
|
+
//# sourceMappingURL=chunk-2BQAHLU3.js.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
peopleErrorSentence
|
|
4
|
+
} from "./chunk-HS6GMHKZ.js";
|
|
5
|
+
|
|
6
|
+
// src/error-box.tsx
|
|
7
|
+
import * as React from "react";
|
|
8
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
var CONTEXT_KEY = /* @__PURE__ */ Symbol.for("@ai-matrx/design-system/error-actions");
|
|
10
|
+
var registry = globalThis;
|
|
11
|
+
var ErrorActionsContext = registry[CONTEXT_KEY] ?? (registry[CONTEXT_KEY] = React.createContext(null));
|
|
12
|
+
function ErrorActionsProvider({
|
|
13
|
+
render,
|
|
14
|
+
children
|
|
15
|
+
}) {
|
|
16
|
+
return /* @__PURE__ */ jsx(ErrorActionsContext.Provider, { value: render, children });
|
|
17
|
+
}
|
|
18
|
+
var MENUS_KEY = /* @__PURE__ */ Symbol.for("@ai-matrx/design-system/error-card-menus");
|
|
19
|
+
var menusRegistry = globalThis;
|
|
20
|
+
var ErrorCardMenusContext = menusRegistry[MENUS_KEY] ?? (menusRegistry[MENUS_KEY] = React.createContext(null));
|
|
21
|
+
function ErrorCardMenusProvider({
|
|
22
|
+
menus,
|
|
23
|
+
children
|
|
24
|
+
}) {
|
|
25
|
+
return /* @__PURE__ */ jsx(ErrorCardMenusContext.Provider, { value: menus, children });
|
|
26
|
+
}
|
|
27
|
+
function useErrorCardMenus() {
|
|
28
|
+
return React.useContext(ErrorCardMenusContext);
|
|
29
|
+
}
|
|
30
|
+
function useErrorActions() {
|
|
31
|
+
return React.useContext(ErrorActionsContext);
|
|
32
|
+
}
|
|
33
|
+
function ErrorActions(props) {
|
|
34
|
+
const render = useErrorActions();
|
|
35
|
+
if (!render) return null;
|
|
36
|
+
return /* @__PURE__ */ jsx(Fragment, { children: render(props) });
|
|
37
|
+
}
|
|
38
|
+
function sayable(body, error) {
|
|
39
|
+
if (typeof body === "string") return peopleErrorSentence(error, body);
|
|
40
|
+
if (Array.isArray(body)) return body.map((b) => typeof b === "string" && b.trim() ? peopleErrorSentence(error, b) : b);
|
|
41
|
+
return body;
|
|
42
|
+
}
|
|
43
|
+
var ErrorBox = React.forwardRef(function ErrorBox2({
|
|
44
|
+
as = "p",
|
|
45
|
+
message,
|
|
46
|
+
title,
|
|
47
|
+
error,
|
|
48
|
+
operation,
|
|
49
|
+
records,
|
|
50
|
+
unsavedInput,
|
|
51
|
+
origin,
|
|
52
|
+
children,
|
|
53
|
+
...rest
|
|
54
|
+
}, ref) {
|
|
55
|
+
const Tag = as;
|
|
56
|
+
const facts = {
|
|
57
|
+
...message !== void 0 ? { message } : {},
|
|
58
|
+
...title !== void 0 ? { title } : {},
|
|
59
|
+
...error !== void 0 ? { error } : {},
|
|
60
|
+
...operation !== void 0 ? { operation } : {},
|
|
61
|
+
...records !== void 0 ? { records } : {},
|
|
62
|
+
...unsavedInput !== void 0 ? { unsavedInput } : {},
|
|
63
|
+
...origin !== void 0 ? { origin } : {}
|
|
64
|
+
};
|
|
65
|
+
return /* @__PURE__ */ jsxs(Tag, { ref, role: "alert", "data-error-box": "", ...rest, children: [
|
|
66
|
+
sayable(children ?? message, error),
|
|
67
|
+
/* @__PURE__ */ jsx(ErrorActions, { ...facts })
|
|
68
|
+
] });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
export {
|
|
72
|
+
ErrorActionsProvider,
|
|
73
|
+
ErrorCardMenusProvider,
|
|
74
|
+
useErrorCardMenus,
|
|
75
|
+
useErrorActions,
|
|
76
|
+
ErrorActions,
|
|
77
|
+
ErrorBox
|
|
78
|
+
};
|
|
79
|
+
//# sourceMappingURL=chunk-5Z5GTXKL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/error-box.tsx"],"sourcesContent":["/**\n * ErrorBox — THE one way a package draws an error on screen, and the seam a\n * host uses to put its own actions on every one of them.\n *\n * Every error sentence this package (and every sibling package that depends\n * on it) shows is an `ErrorBox`. The box is a plain `role=\"alert\"` element —\n * its tag and classes are the caller's, so nothing about the look changes — and\n * after its children it renders whatever the host's `ErrorActionsProvider`\n * returns for that error. With no provider it renders nothing extra.\n *\n * Why a slot and not a menu: the package cannot know the host's AI copy, bug\n * report, or support flow (Matrx fills it with its Alchemy copy-for-AI menu at\n * the app root). The package supplies the error's facts — the sentence, the\n * raw error, the operation, the records — and the host decides the actions.\n *\n * Guard: `error-box.census.test.ts` fails when a package file renders\n * `role=\"alert\"` for an error without going through `ErrorBox` (or, for the\n * `Alert` family, `ErrorActions`).\n */\nimport * as React from \"react\";\nimport { peopleErrorSentence } from \"./error-sentence\";\n\nexport interface ErrorActionsRecord {\n type: string;\n id: string;\n label?: string;\n}\n\n/** The facts a package knows about an error it is showing. All optional. */\nexport interface ErrorActionsInput {\n /** The sentence shown. Omit when the words are only in the rendered box. */\n message?: string | undefined;\n title?: string | undefined;\n /** The raw error (code/status/stack stay available to the host). */\n error?: unknown | undefined;\n /** What was being attempted (\"Load saved views\"). */\n operation?: string | undefined;\n records?: readonly ErrorActionsRecord[] | undefined;\n /** The person's input that did not land. */\n unsavedInput?: unknown | undefined;\n /** The package + component that drew it (\"design-system/SavedViewsControl\"). */\n origin?: string | undefined;\n /** Extra structured facts for the AI payload (never shown). */\n details?: Record<string, unknown> | undefined;\n code?: string | number | undefined;\n status?: number | undefined;\n /** The tables / RPCs the failed call used — pins its captured request as the cause. */\n calls?: readonly string[] | undefined;\n /** Where it was drawn: an `ErrorNotice` card/line (\"inline\") or a package alert box (\"alert\", the default). */\n source?: \"inline\" | \"alert\" | undefined;\n}\n\nexport type ErrorActionsRenderer = (input: ErrorActionsInput) => React.ReactNode;\n\n/**\n * ONE context for every copy of this module. tsup builds each entry point\n * (`.`, `./data-table`, …) without code splitting, so this file is inlined into\n * several bundles; a plain `createContext` would give each bundle its own\n * context and the host's provider (imported from `.`) would never reach an\n * error drawn by the data table. The registry key makes them one.\n */\nconst CONTEXT_KEY = Symbol.for(\"@ai-matrx/design-system/error-actions\");\ntype ContextRegistry = Record<symbol, React.Context<ErrorActionsRenderer | null> | undefined>;\nconst registry = globalThis as unknown as ContextRegistry;\nconst ErrorActionsContext: React.Context<ErrorActionsRenderer | null> =\n registry[CONTEXT_KEY] ??\n (registry[CONTEXT_KEY] = React.createContext<ErrorActionsRenderer | null>(null));\n\n/** Mount once at the host's app root; every ErrorBox below renders `render(...)`. */\nexport function ErrorActionsProvider({\n render,\n children,\n}: {\n render: ErrorActionsRenderer;\n children: React.ReactNode;\n}) {\n return (\n <ErrorActionsContext.Provider value={render}>{children}</ErrorActionsContext.Provider>\n );\n}\n\n/**\n * The host's content menu around an error card (`ErrorNotice`): the corner ⋯\n * button and the right-click boundary that show the host's content actions\n * over the error's sentence. Read-only — nothing here may change a record.\n * Without a provider the card draws neither (the actions slot still renders).\n */\nexport interface ErrorCardMenus {\n /** The corner ⋯ button over `{ title, text }`. */\n Button: React.ComponentType<{ title: string; text: string; className?: string }>;\n /** Wraps the whole card so right-click opens the same menu. */\n Boundary: React.ComponentType<{ title: string; text: string; children: React.ReactNode }>;\n}\n\nconst MENUS_KEY = Symbol.for(\"@ai-matrx/design-system/error-card-menus\");\ntype MenusRegistry = Record<symbol, React.Context<ErrorCardMenus | null> | undefined>;\nconst menusRegistry = globalThis as unknown as MenusRegistry;\nconst ErrorCardMenusContext: React.Context<ErrorCardMenus | null> =\n menusRegistry[MENUS_KEY] ??\n (menusRegistry[MENUS_KEY] = React.createContext<ErrorCardMenus | null>(null));\n\n/** Mount once at the host's app root beside `ErrorActionsProvider`. */\nexport function ErrorCardMenusProvider({\n menus,\n children,\n}: {\n menus: ErrorCardMenus;\n children: React.ReactNode;\n}) {\n return <ErrorCardMenusContext.Provider value={menus}>{children}</ErrorCardMenusContext.Provider>;\n}\n\nexport function useErrorCardMenus(): ErrorCardMenus | null {\n return React.useContext(ErrorCardMenusContext);\n}\n\nexport function useErrorActions(): ErrorActionsRenderer | null {\n return React.useContext(ErrorActionsContext);\n}\n\n/**\n * The host's actions for one error, placed wherever the caller puts this\n * element. Renders nothing without a provider. Use inside a box you do not\n * render yourself (the `Alert` family); everything else uses `ErrorBox`.\n */\nexport function ErrorActions(props: ErrorActionsInput) {\n const render = useErrorActions();\n if (!render) return null;\n return <>{render(props)}</>;\n}\n\ntype ErrorBoxTag = \"p\" | \"div\" | \"span\" | \"section\" | \"li\";\n\nexport interface ErrorBoxProps\n extends Omit<React.HTMLAttributes<HTMLElement>, \"role\" | \"title\">,\n ErrorActionsInput {\n /** The element drawn — the caller's existing tag, so layout never shifts. */\n as?: ErrorBoxTag;\n}\n\n/** A plain-string body goes through the one funnel (error-sentence.ts); rich children are the caller's. */\nfunction sayable(body: React.ReactNode, error: unknown): React.ReactNode {\n if (typeof body === \"string\") return peopleErrorSentence(error, body);\n if (Array.isArray(body)) return body.map((b) => (typeof b === \"string\" && b.trim() ? peopleErrorSentence(error, b) : b));\n return body;\n}\n\nexport const ErrorBox = React.forwardRef<HTMLElement, ErrorBoxProps>(function ErrorBox(\n {\n as = \"p\",\n message,\n title,\n error,\n operation,\n records,\n unsavedInput,\n origin,\n children,\n ...rest\n },\n ref,\n) {\n // A component type, never a bare ElementType: an app compiling this source may carry global JSX\n // elements (react-three-fiber) that make a bare ElementType's props `never` (2026-10-22).\n const Tag = as as unknown as React.ComponentType<React.HTMLAttributes<HTMLElement> & { ref?: React.Ref<HTMLElement> }>;\n const facts: ErrorActionsInput = {\n ...(message !== undefined ? { message } : {}),\n ...(title !== undefined ? { title } : {}),\n ...(error !== undefined ? { error } : {}),\n ...(operation !== undefined ? { operation } : {}),\n ...(records !== undefined ? { records } : {}),\n ...(unsavedInput !== undefined ? { unsavedInput } : {}),\n ...(origin !== undefined ? { origin } : {}),\n };\n return (\n <Tag ref={ref} role=\"alert\" data-error-box=\"\" {...rest}>\n {sayable(children ?? message, error)}\n <ErrorActions {...facts} />\n </Tag>\n );\n});\n"],"mappings":";;;;;;AAmBA,YAAY,WAAW;AA0DnB,SAmDK,UAnDL,KAkGA,YAlGA;AAhBJ,IAAM,cAAc,uBAAO,IAAI,uCAAuC;AAEtE,IAAM,WAAW;AACjB,IAAM,sBACJ,SAAS,WAAW,MACnB,SAAS,WAAW,IAAU,oBAA2C,IAAI;AAGzE,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA;AACF,GAGG;AACD,SACE,oBAAC,oBAAoB,UAApB,EAA6B,OAAO,QAAS,UAAS;AAE3D;AAeA,IAAM,YAAY,uBAAO,IAAI,0CAA0C;AAEvE,IAAM,gBAAgB;AACtB,IAAM,wBACJ,cAAc,SAAS,MACtB,cAAc,SAAS,IAAU,oBAAqC,IAAI;AAGtE,SAAS,uBAAuB;AAAA,EACrC;AAAA,EACA;AACF,GAGG;AACD,SAAO,oBAAC,sBAAsB,UAAtB,EAA+B,OAAO,OAAQ,UAAS;AACjE;AAEO,SAAS,oBAA2C;AACzD,SAAa,iBAAW,qBAAqB;AAC/C;AAEO,SAAS,kBAA+C;AAC7D,SAAa,iBAAW,mBAAmB;AAC7C;AAOO,SAAS,aAAa,OAA0B;AACrD,QAAM,SAAS,gBAAgB;AAC/B,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,gCAAG,iBAAO,KAAK,GAAE;AAC1B;AAYA,SAAS,QAAQ,MAAuB,OAAiC;AACvE,MAAI,OAAO,SAAS,SAAU,QAAO,oBAAoB,OAAO,IAAI;AACpE,MAAI,MAAM,QAAQ,IAAI,EAAG,QAAO,KAAK,IAAI,CAAC,MAAO,OAAO,MAAM,YAAY,EAAE,KAAK,IAAI,oBAAoB,OAAO,CAAC,IAAI,CAAE;AACvH,SAAO;AACT;AAEO,IAAM,WAAiB,iBAAuC,SAASA,UAC5E;AAAA,EACE,KAAK;AAAA,EACL;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GACA,KACA;AAGA,QAAM,MAAM;AACZ,QAAM,QAA2B;AAAA,IAC/B,GAAI,YAAY,SAAY,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC3C,GAAI,UAAU,SAAY,EAAE,MAAM,IAAI,CAAC;AAAA,IACvC,GAAI,UAAU,SAAY,EAAE,MAAM,IAAI,CAAC;AAAA,IACvC,GAAI,cAAc,SAAY,EAAE,UAAU,IAAI,CAAC;AAAA,IAC/C,GAAI,YAAY,SAAY,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC3C,GAAI,iBAAiB,SAAY,EAAE,aAAa,IAAI,CAAC;AAAA,IACrD,GAAI,WAAW,SAAY,EAAE,OAAO,IAAI,CAAC;AAAA,EAC3C;AACA,SACE,qBAAC,OAAI,KAAU,MAAK,SAAQ,kBAAe,IAAI,GAAG,MAC/C;AAAA,YAAQ,YAAY,SAAS,KAAK;AAAA,IACnC,oBAAC,gBAAc,GAAG,OAAO;AAAA,KAC3B;AAEJ,CAAC;","names":["ErrorBox"]}
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
} from "./chunk-YRF22M5S.js";
|
|
27
27
|
import {
|
|
28
28
|
ErrorActions
|
|
29
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-5Z5GTXKL.js";
|
|
30
30
|
import {
|
|
31
31
|
HostLink
|
|
32
32
|
} from "./chunk-TH6DDCWF.js";
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
DialogDescription,
|
|
37
37
|
DialogHeader,
|
|
38
38
|
DialogTitle
|
|
39
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-2BQAHLU3.js";
|
|
40
40
|
import {
|
|
41
41
|
useIsMobile
|
|
42
42
|
} from "./chunk-U24MOJWK.js";
|
|
@@ -4007,4 +4007,4 @@ export {
|
|
|
4007
4007
|
TileCard,
|
|
4008
4008
|
TileCardGrid
|
|
4009
4009
|
};
|
|
4010
|
-
//# sourceMappingURL=chunk-
|
|
4010
|
+
//# sourceMappingURL=chunk-ACZALXD6.js.map
|
|
@@ -13,6 +13,7 @@ var NOTICE_STACK = {
|
|
|
13
13
|
/** The card's width: small, never the window's. */
|
|
14
14
|
widthRem: 22
|
|
15
15
|
};
|
|
16
|
+
var NOTICE_TEXT_BUDGET = { titleChars: 24, lineChars: 40 };
|
|
16
17
|
function noticeKey(tone, title, message) {
|
|
17
18
|
const text = (v) => typeof v === "string" || typeof v === "number" ? String(v).trim() : "";
|
|
18
19
|
return `${tone}\0${text(title)}\0${text(message)}`;
|
|
@@ -43,9 +44,10 @@ function noticeLifetimeMs(tone) {
|
|
|
43
44
|
|
|
44
45
|
export {
|
|
45
46
|
NOTICE_STACK,
|
|
47
|
+
NOTICE_TEXT_BUDGET,
|
|
46
48
|
noticeKey,
|
|
47
49
|
collapseNotices,
|
|
48
50
|
visibleNotices,
|
|
49
51
|
noticeLifetimeMs
|
|
50
52
|
};
|
|
51
|
-
//# sourceMappingURL=chunk-
|
|
53
|
+
//# sourceMappingURL=chunk-BB5LA7FQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/notice-stack.ts"],"sourcesContent":["/**\n * @ai-matrx/design-system/notice-stack — THE ONE NOTICE LOOK AND STACK RULE, for the web and Matrx 2.\n *\n * Arman, 2026-10-23, looking at two full-width red bars across Matrx 2 Cloud Chat: \"Let's eliminate\n * this terrible pattern of having giant errors that shit the entire ui. If we're going to have a\n * place for errors, it should be a small something that resembles errors from a normal macos\".\n *\n * Champion: the macOS notification banner. A small rounded card of translucent material in the\n * window's TOP-RIGHT corner (where macOS puts every notification, so the eye already looks there;\n * the bottom of our windows holds the composer and the list's foot, which a notice must not sit on),\n * the tone's icon, a one-line title, an optional second line, at most one action, and ×.\n *\n * The stack rule, the same on both hosts:\n * - identical notices (same tone, title and line) collapse into ONE card with a count;\n * - at most 3 cards show; the rest fold into \"N more\";\n * - info and success go after NOTICE_STACK.infoMs; errors and warnings stay until dismissed\n * or until NOTICE_STACK.errorMs (long, so a notice nobody saw does not sit there for ever).\n *\n * The desktop renders it through `Notices` + `Banner` (desktop/layout.tsx, desktop/state.tsx); the\n * web through its one sonner Toaster (matrx-frontend components/ui/sonner.tsx), which reads these\n * constants and keys every toast with `noticeKey` so the same notice never stacks twice.\n */\n\nexport const NOTICE_STACK = {\n /** The corner every notice floats in. */\n corner: \"top-right\",\n /** Cards shown at once; the rest fold into \"N more\". */\n maxVisible: 3,\n /** Info and success: gone after this long. */\n infoMs: 4000,\n /** Errors and warnings: stay until dismissed, or this long at most. */\n errorMs: 30000,\n /** The card's width: small, never the window's. */\n widthRem: 22,\n} as const;\n\n/**\n * How much a card can SAY with nothing cut by \"…\" at the smallest window (900 × 560, Matrx 2 Cloud\n * Chat: the card is 324 px wide over the viewer, and an action such as Retry shares the row, leaving\n * ~180 px of text): the title is one line, the line under it at most two short ones. Measured\n * 2026-10-24 (\"Your change was not saved …\" / \"Check the connection and tr…\"). In-app text is layout.\n */\nexport const NOTICE_TEXT_BUDGET = { titleChars: 24, lineChars: 40 } as const;\n\nexport type NoticeStackTone = \"neutral\" | \"info\" | \"warning\" | \"danger\" | \"success\";\n\n/** The identity two notices share when they say the same thing (tone, title, line). */\nexport function noticeKey(tone: string, title: unknown, message?: unknown): string {\n const text = (v: unknown): string => (typeof v === \"string\" || typeof v === \"number\" ? String(v).trim() : \"\");\n return `${tone}\\u0000${text(title)}\\u0000${text(message)}`;\n}\n\nexport interface CollapsedNotice<T> {\n /** The latest notice of the group (its dismiss and action are the live ones). */\n readonly item: T;\n /** Every notice the group holds, oldest first. */\n readonly members: readonly T[];\n /** How many identical notices it stands for. */\n readonly count: number;\n}\n\n/**\n * Identical notices become one with a count, in the order each first appeared. A notice whose key\n * is null (a rich one that says nothing comparable) stands alone.\n */\nexport function collapseNotices<T>(items: readonly T[], keyOf: (item: T) => string | null): CollapsedNotice<T>[] {\n const groups: { key: string | null; members: T[] }[] = [];\n const byKey = new Map<string, { key: string | null; members: T[] }>();\n for (const item of items) {\n const key = keyOf(item);\n const found = key !== null ? byKey.get(key) : undefined;\n if (found) {\n found.members.push(item);\n continue;\n }\n const group = { key, members: [item] };\n groups.push(group);\n if (key !== null) byKey.set(key, group);\n }\n return groups.map((g) => ({ item: g.members[g.members.length - 1] as T, members: g.members, count: g.members.length }));\n}\n\n/** The cards on screen and how many fold away (the newest show). */\nexport function visibleNotices<T>(groups: readonly T[], max: number = NOTICE_STACK.maxVisible): { shown: T[]; folded: number } {\n if (groups.length <= max) return { shown: [...groups], folded: 0 };\n return { shown: groups.slice(groups.length - max), folded: groups.length - max };\n}\n\n/** How long a notice of this tone stays (ms; Infinity never happens: errors cap at errorMs). */\nexport function noticeLifetimeMs(tone: NoticeStackTone | \"error\"): number {\n return tone === \"danger\" || tone === \"warning\" || tone === \"error\" ? NOTICE_STACK.errorMs : NOTICE_STACK.infoMs;\n}\n"],"mappings":";;;AAuBO,IAAM,eAAe;AAAA;AAAA,EAE1B,QAAQ;AAAA;AAAA,EAER,YAAY;AAAA;AAAA,EAEZ,QAAQ;AAAA;AAAA,EAER,SAAS;AAAA;AAAA,EAET,UAAU;AACZ;AAQO,IAAM,qBAAqB,EAAE,YAAY,IAAI,WAAW,GAAG;AAK3D,SAAS,UAAU,MAAc,OAAgB,SAA2B;AACjF,QAAM,OAAO,CAAC,MAAwB,OAAO,MAAM,YAAY,OAAO,MAAM,WAAW,OAAO,CAAC,EAAE,KAAK,IAAI;AAC1G,SAAO,GAAG,IAAI,KAAS,KAAK,KAAK,CAAC,KAAS,KAAK,OAAO,CAAC;AAC1D;AAeO,SAAS,gBAAmB,OAAqB,OAAyD;AAC/G,QAAM,SAAiD,CAAC;AACxD,QAAM,QAAQ,oBAAI,IAAkD;AACpE,aAAW,QAAQ,OAAO;AACxB,UAAM,MAAM,MAAM,IAAI;AACtB,UAAM,QAAQ,QAAQ,OAAO,MAAM,IAAI,GAAG,IAAI;AAC9C,QAAI,OAAO;AACT,YAAM,QAAQ,KAAK,IAAI;AACvB;AAAA,IACF;AACA,UAAM,QAAQ,EAAE,KAAK,SAAS,CAAC,IAAI,EAAE;AACrC,WAAO,KAAK,KAAK;AACjB,QAAI,QAAQ,KAAM,OAAM,IAAI,KAAK,KAAK;AAAA,EACxC;AACA,SAAO,OAAO,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,SAAS,CAAC,GAAQ,SAAS,EAAE,SAAS,OAAO,EAAE,QAAQ,OAAO,EAAE;AACxH;AAGO,SAAS,eAAkB,QAAsB,MAAc,aAAa,YAA4C;AAC7H,MAAI,OAAO,UAAU,IAAK,QAAO,EAAE,OAAO,CAAC,GAAG,MAAM,GAAG,QAAQ,EAAE;AACjE,SAAO,EAAE,OAAO,OAAO,MAAM,OAAO,SAAS,GAAG,GAAG,QAAQ,OAAO,SAAS,IAAI;AACjF;AAGO,SAAS,iBAAiB,MAAyC;AACxE,SAAO,SAAS,YAAY,SAAS,aAAa,SAAS,UAAU,aAAa,UAAU,aAAa;AAC3G;","names":[]}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import {
|
|
3
3
|
ErrorActions,
|
|
4
|
-
isTimedOutError,
|
|
5
|
-
peopleErrorSentence,
|
|
6
4
|
useErrorCardMenus
|
|
7
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-5Z5GTXKL.js";
|
|
6
|
+
import {
|
|
7
|
+
isTimedOutError,
|
|
8
|
+
peopleErrorSentence
|
|
9
|
+
} from "./chunk-HS6GMHKZ.js";
|
|
8
10
|
import {
|
|
9
11
|
Button
|
|
10
12
|
} from "./chunk-7IOSA55D.js";
|
|
@@ -333,4 +335,4 @@ export {
|
|
|
333
335
|
ReadEmpty,
|
|
334
336
|
ReadStaleNotice
|
|
335
337
|
};
|
|
336
|
-
//# sourceMappingURL=chunk-
|
|
338
|
+
//# sourceMappingURL=chunk-GUGBVQ4T.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/error-notice.tsx","../src/read-state/reload-page-button.tsx","../src/read-state/read-failure.tsx","../src/read-state/stale-data-notice.tsx","../src/read-state/read-gate.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * ErrorNotice — THE inline error card. Every error a surface renders inline\n * (\"Not saved\", \"Failed to load…\", a dialog's refusal, a form's failure) is\n * this component, so every one of them carries the Alchemy Menu and hands an\n * AI the sentence, the code, the operation, the records, the surface's declared\n * values and the person's unsaved input.\n *\n * <ErrorNotice\n * title=\"Not saved\"\n * message={item.error}\n * operation=\"Save comment\"\n * records={[{ type: \"document\", id: docId }]}\n * unsavedInput={{ comment: draft }}\n * actions={<Button onClick={retry}>Retry</Button>}\n * />\n *\n * Moved from matrx-frontend (chat host slots get real package defaults,\n * 2026-10-08). The host's actions (Matrx: the Alchemy Menu) arrive through\n * `ErrorActionsProvider`; the corner ⋯ and right-click menu through\n * `ErrorCardMenusProvider`. Without them the card still says everything.\n */\nimport type { ReactNode } from \"react\";\nimport { AlertTriangleIcon as AlertTriangle } from \"./icons\";\nimport { cn } from \"./cn\";\nimport { Button } from \"./button\";\nimport {\n ErrorActions,\n useErrorCardMenus,\n type ErrorActionsInput,\n type ErrorActionsRecord,\n} from \"./error-box\";\n\nimport {\n errorSentenceOf,\n isTimedOutError,\n peopleErrorSentence,\n TIMED_OUT_SENTENCE,\n} from \"./error-sentence\";\n\nexport { errorSentenceOf, isTimedOutError, TIMED_OUT_SENTENCE };\n\nexport type ErrorNoticeProps = {\n /** Short heading (\"Not saved\"). */\n title?: string;\n /** The sentence shown. Falls back to the error's own message. */\n message?: string | null;\n /** The raw error — code/status/stack go to the AI payload, never the screen. */\n error?: unknown;\n operation?: string;\n records?: readonly ErrorActionsRecord[];\n unsavedInput?: unknown;\n details?: Record<string, unknown>;\n code?: string | number;\n status?: number;\n /** The tables / RPCs the failed call used — pins its captured request as the cause. */\n calls?: readonly string[];\n /** Retry / Discard / other controls, rendered under the sentence. */\n actions?: ReactNode;\n /**\n * Run the failed call again. Shown as \"Try again\" when the error is the\n * store's statement timeout and no `actions` were given.\n */\n onRetry?: () => void;\n /** Extra content under the sentence. */\n children?: ReactNode;\n /**\n * `default` = the card. `compact` = the dense card used inside lists and\n * side panels. `inline` = a one-line field/form error (no card chrome): the\n * sentence in the destructive color with the menu at its end — the shape\n * every `<p role=\"alert\" className=\"text-destructive\">` render moves onto.\n */\n size?: \"inline\" | \"compact\" | \"default\";\n icon?: boolean;\n className?: string;\n};\n\nexport function ErrorNotice({\n title,\n message,\n error,\n operation,\n records,\n unsavedInput,\n details,\n code,\n status,\n calls,\n actions: actionsProp,\n onRetry,\n children,\n size = \"default\",\n icon = true,\n className,\n}: ErrorNoticeProps) {\n const timedOut = isTimedOutError(error, message, code);\n // The one funnel (error-sentence.ts): raw error text and code paths never reach the screen.\n const sentence = peopleErrorSentence(error, message, { code });\n const actions =\n actionsProp ??\n (timedOut && onRetry ? (\n <Button type=\"button\" size=\"sm\" variant=\"outline\" data-error-retry=\"\" onClick={onRetry}>\n Try again\n </Button>\n ) : undefined);\n const menus = useErrorCardMenus();\n const input: ErrorActionsInput = {\n title,\n message: sentence,\n error,\n operation,\n records,\n unsavedInput,\n details,\n code,\n status,\n ...(calls ? { calls } : {}),\n source: \"inline\",\n origin: \"design-system/ErrorNotice\",\n };\n if (size === \"inline\") {\n return (\n <div\n role=\"alert\"\n data-error-notice=\"\"\n className={cn(\"flex items-start gap-1 text-destructive\", className)}\n >\n <span className=\"min-w-0 flex-1 break-words\">\n {title ? (\n <span data-error-title=\"\" className=\"font-medium\">\n {title}:{\" \"}\n </span>\n ) : null}\n {sentence}\n </span>\n {children}\n {actions}\n <ErrorActions {...input} />\n </div>\n );\n }\n const compact = size === \"compact\";\n // Title, actions or extra content make the card taller than the ⋯ + menu column.\n const stacked = Boolean(title || actions || children);\n // The card is an action host like any other content (ALC-15): right-click,\n // ⋯ (the same menu, opened at the button), the phone's sheet and the palette\n // (⌘/Ctrl+Shift+K) all show the ONE registry's actions over this sentence.\n // Read-only: nothing here may change a record.\n const menuTitle = title ?? \"Error\";\n const menuText = title ? `${title}: ${sentence}` : sentence;\n const card = (\n <div\n role=\"alert\"\n data-error-notice=\"\"\n className={cn(\n \"rounded-md border border-destructive/30 bg-destructive/5\",\n compact ? \"px-2 py-1.5 text-xs\" : \"px-3 py-2.5 text-sm\",\n className,\n )}\n >\n <div className=\"flex items-start gap-2\">\n {icon && (\n <AlertTriangle\n aria-hidden\n className={cn(\n \"shrink-0 text-destructive\",\n compact ? \"mt-px h-3.5 w-3.5\" : \"mt-0.5 h-4 w-4\",\n )}\n />\n )}\n <div className=\"min-w-0 flex-1\">\n {title && (\n <p data-error-title=\"\" className=\"font-medium text-destructive\">\n {title}\n </p>\n )}\n <p className={cn(\"break-words text-foreground\", title && \"mt-0.5\")}>\n {sentence}\n {!stacked && <ErrorActions {...input} />}\n </p>\n {children}\n {actions && <div className=\"mt-1 flex flex-wrap gap-1\">{actions}</div>}\n </div>\n {/* The corner column the ⋯ already owns carries the menu under it: no\n new column (beside the ⋯ it narrowed every line — 176→224px at\n 375px) and no new line (inline, a full last line wrapped it,\n +16px). A one-line notice with nothing under its sentence is\n shorter than that column, so there the menu rides the sentence\n (RC-B12 layout rule). */}\n <div className=\"flex shrink-0 flex-col items-center\">\n {menus ? (\n <menus.Button\n title={menuTitle}\n text={menuText}\n {...(compact ? { className: \"h-6 w-6\" } : {})}\n />\n ) : null}\n {stacked && <ErrorActions {...input} />}\n </div>\n </div>\n </div>\n );\n if (!menus) return card;\n return (\n <menus.Boundary title={menuTitle} text={menuText}>\n {card}\n </menus.Boundary>\n );\n}\n","\"use client\";\n\n/**\n * THE SERVER-SAFE RETRY. A server page cannot hand a failure notice a retry\n * function — functions do not cross the server/client boundary — so its\n * \"Couldn't load …\" card had no way forward but the browser's own reload\n * (page-pass 2026-09-27, /chat/message-templates/<id>). This control is the\n * retry any server-rendered failure can carry: it takes no props a server\n * cannot send, and it reloads THIS url (path, query and hash), which re-runs the\n * server read that failed.\n *\n * Its words say what it does — \"Reload page\" — because a reload also drops\n * anything unsaved elsewhere on the page; a client surface that can retry just\n * its own read passes `onRetry` to `ReadFailure` instead.\n */\nimport { RefreshCwIcon as RefreshCw } from \"../icons\";\nimport { Button } from \"../button\";\n\nexport const RELOAD_PAGE_LABEL = \"Reload page\";\n\nexport function ReloadPageButton({ className }: { className?: string }) {\n return (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className={className ?? \"h-7 gap-1.5 text-xs\"}\n onClick={() => window.location.reload()}\n >\n <RefreshCw className=\"h-3.5 w-3.5\" aria-hidden /> {RELOAD_PAGE_LABEL}\n </Button>\n );\n}\n","\"use client\";\n\n/**\n * The failed-read view every list uses instead of its empty state (RC-B12\n * round 11): the error, with the Alchemy Menu (through ErrorNotice) and a\n * retry when the caller has one. `error` may be the error itself or just the\n * read's failed flag — the sentence never pretends there is nothing.\n *\n * ALWAYS A WAY FORWARD. With `onRetry` it offers \"Try again\" (the surface's own\n * read, nothing else touched). Without one — every SERVER page, which cannot\n * pass a function — it offers `ReloadPageButton`: a same-URL reload that re-runs\n * the server read. A failure card with no retry at all is gone.\n */\nimport { RefreshCwIcon as RefreshCw } from \"../icons\";\nimport { Button } from \"../button\";\nimport { ErrorNotice } from \"../error-notice\";\nimport { ReloadPageButton } from \"./reload-page-button\";\n\nexport type ReadFailureProps = {\n error: unknown;\n /** What could not be read, in the reader's words: \"your tasks\". */\n what?: string | undefined;\n /** Retry just this read. Absent (always, from a server page): a same-URL reload is offered. */\n onRetry?: (() => void) | undefined;\n className?: string | undefined;\n size?: \"compact\" | \"default\" | undefined;\n};\n\nexport function ReadFailure({\n error,\n what = \"this list\",\n onRetry,\n className,\n size = \"compact\",\n}: ReadFailureProps) {\n const flagOnly = error === true || error == null || error === \"\";\n return (\n <ErrorNotice\n size={size}\n className={className ?? \"m-3\"}\n title={`Couldn't load ${what}`}\n {...(flagOnly\n ? { message: `The read for ${what} failed, so nothing here is an answer — not \"empty\".` }\n : { error })}\n operation={`Load ${what}`}\n actions={\n onRetry ? (\n <Button variant=\"outline\" size=\"sm\" className=\"h-7 gap-1.5 text-xs\" onClick={onRetry}>\n <RefreshCw className=\"h-3.5 w-3.5\" /> Try again\n </Button>\n ) : (\n <ReloadPageButton />\n )\n }\n />\n );\n}\n","\"use client\";\n\n/**\n * StaleDataNotice — what a surface shows when a READ FAILED and the rows on\n * screen are therefore not to be trusted.\n *\n * THE DEAD END this kills: a list loads fine, the user acts, the surface\n * reloads — and the reload fails. A toast fires and fades. What remains is the\n * PREVIOUS list, rendered exactly like a fresh one. The user reads a roster\n * that no longer reflects the database and has no way to know. Bugbot found\n * this on the admins roster and the feedback table in the same pass, which is\n * what made it a primitive instead of two patches.\n *\n * This is the campaign's never-report-green rule at its sharpest: **a surface\n * must never present data it could not actually read as though it had.** The\n * sibling `DeepLinkMissNotice` states a definitive negative and so demands\n * proof the list WAS read; this states the opposite — that the read broke —\n * and is what that component's \"show the surface's ordinary error state\n * instead\" was pointing at. Neither is complete without the other.\n *\n * Two cases, and conflating them is the actual bug:\n *\n * 1. **Stale** (`hasData`) — rows are on screen from an earlier successful\n * read. Say they may be out of date. Do NOT blank them: old data plus an\n * honest label beats an empty screen, and the user may still need it.\n * 2. **Unknown** (`!hasData`) — nothing was ever read. An empty list here\n * reads as \"there are none\", which is a claim about the database nobody\n * is entitled to make. Say the read failed instead.\n *\n * `onRetry` is not optional decoration — a problem you can detect ships with\n * its one-click fix. The user should never have to reload the page to recover\n * from a failed fetch.\n */\n\nimport { AlertTriangleIcon as AlertTriangle, RefreshCwIcon as RefreshCw } from \"../icons\";\nimport { Button } from \"../button\";\nimport { cn } from \"../cn\";\nimport { ErrorActions } from \"../error-box\";\n\nexport interface StaleDataNoticeProps {\n /**\n * Whether rows from an earlier SUCCESSFUL read are currently displayed.\n * Decides between \"this may be out of date\" and \"we never read this\".\n */\n hasData: boolean;\n /**\n * What could not be read, as a plain noun phrase the user recognizes —\n * \"the admin roster\", \"feedback\". Never a table or endpoint name: our user\n * is a subject-matter expert, not an engineer.\n */\n what: string;\n /** Re-run the read. Required — the notice always carries its own fix. */\n onRetry: () => void;\n /** Disables the retry control and shows motion while a retry is in flight. */\n retrying?: boolean;\n /** Optional detail (a server message). Shown verbatim, never interpreted. */\n detail?: string | null;\n /**\n * A multi-source list where SOME sources read and some did not: the rows on\n * screen are current, but `what` is missing from them. Says so instead of\n * \"may be out of date\" (RC-B12 round 12 — a partly failed roster).\n */\n partial?: boolean;\n className?: string;\n}\n\nexport function StaleDataNotice({\n hasData,\n what,\n onRetry,\n retrying = false,\n detail,\n partial = false,\n className,\n}: StaleDataNoticeProps) {\n return (\n <div\n role=\"status\"\n className={cn(\n \"flex flex-wrap items-center gap-x-3 gap-y-1.5 rounded-md border border-amber-500/40 bg-amber-500/10 px-3 py-2 type-body\",\n className,\n )}\n >\n <AlertTriangle className=\"h-4 w-4 shrink-0 text-amber-600 dark:text-amber-500\" />\n <span className=\"min-w-0 flex-1 text-foreground\">\n {partial ? (\n <>\n Couldn't load {what}, so <strong>they are missing</strong> from\n the list below — everything else loaded.\n </>\n ) : hasData ? (\n <>\n Couldn't refresh {what}. What you see below is the last version\n we loaded and <strong>may be out of date</strong>.\n </>\n ) : (\n <>\n Couldn't load {what}, so this list is empty because the read\n failed — <strong>not</strong> because there is nothing here.\n </>\n )}\n {detail ? (\n <span className=\"ml-1 text-muted-foreground\">({detail})</span>\n ) : null}\n {/* A failed read is an error on screen: it carries the Alchemy Menu (RC-B12),\n INSIDE the sentence — as its own flex item it narrowed the text and\n grew the notice 118→138px at 375px. */}\n <ErrorActions\n title={partial || !hasData ? `Couldn't load ${what}` : `Couldn't refresh ${what}`}\n message={\n detail\n ? `${hasData ? \"Couldn't refresh\" : \"Couldn't load\"} ${what}: ${detail}`\n : `${hasData ? \"Couldn't refresh\" : \"Couldn't load\"} ${what}.`\n }\n operation={`${hasData ? \"Refresh\" : \"Load\"} ${what}`}\n details={partial ? { rows_on_screen_are_partial: true } : { rows_on_screen_are_stale: hasData }}\n source=\"inline\"\n origin=\"design-system/StaleDataNotice\"\n />\n </span>\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={onRetry}\n disabled={retrying}\n className=\"h-11 shrink-0 gap-1.5 lg:h-7\"\n >\n <RefreshCw className={cn(\"h-3.5 w-3.5\", retrying && \"matrx-spin\")} />\n {retrying ? \"Retrying…\" : \"Try again\"}\n </Button>\n </div>\n );\n}\n","\"use client\";\n\n/**\n * THE rule for every empty view (RC-B12 round 11): \"nothing here\" is an answer\n * only after a read that SUCCEEDED and returned nothing.\n *\n * <ReadGate status={readStatusOf(query)} error={query.error} what=\"your tasks\"\n * isEmpty={tasks.length === 0} empty={<p>No tasks yet.</p>} onRetry={refetch}>\n * <TaskList tasks={tasks} />\n * </ReadGate>\n *\n * loading → a wait · error → the failure (with the menu) · ready + empty → the\n * empty view · ready → the rows. When a refresh fails but earlier rows are on\n * screen, the rows stay and the failure is said above them.\n * Guarded by the census `findUngatedEmptyStates` and the lint rule\n * matrx/error-render-carries-alchemy (an empty view under a loading check\n * with no failure check is refused).\n */\nimport type { ReactNode } from \"react\";\nimport { Loader2Icon as Loader2 } from \"../icons\";\nimport { ReadFailure } from \"./read-failure\";\nimport { StaleDataNotice } from \"./stale-data-notice\";\n\nexport type ReadStatus = \"loading\" | \"error\" | \"ready\";\n\n/** The usual shapes a read reports itself in, folded into one status. */\nexport function readStatusOf(read: {\n status?: string | null;\n isLoading?: boolean;\n loading?: boolean;\n isFetching?: boolean;\n isError?: boolean;\n error?: unknown;\n}): ReadStatus {\n if (read.isError || (read.error != null && read.error !== false && read.error !== \"\")) return \"error\";\n if (read.status === \"error\" || read.status === \"failed\") return \"error\";\n if (read.isLoading || read.loading || read.status === \"loading\" || read.status === \"idle\" || read.status === \"pending\") return \"loading\";\n return \"ready\";\n}\n\n/**\n * A read's outcome handed to a list/table primitive (RC-B12 round 13). Every\n * primitive that takes an `emptyState` also takes `read?: ReadOutcome` and\n * renders the empty state only when `status === \"ready\"`: \"error\" with no rows\n * → <ReadFailure/> with the retry; \"error\" with rows → the rows under a stale\n * notice; \"loading\" with no rows → loading. Structurally identical to the\n * design-system table's `MatrxDataTableRead`, so one value serves both.\n */\nexport interface ReadOutcome {\n status: ReadStatus;\n /** The failure itself, or a truthy flag when the read only says it failed. */\n error?: unknown;\n onRetry?: (() => void) | undefined;\n /** What was read, in the reader's words: \"your tasks\". */\n what?: string | undefined;\n /**\n * A successful read has already produced a value (rows, a count). Then a\n * refetch in flight is not \"loading\" and a failed refresh is not \"no value\":\n * counts keep their last known value (with the stale mark) — the same\n * stale-while-error rule as rows (RC-B12 r13 ruling).\n */\n hasData?: boolean | undefined;\n}\n\ntype ReadLike = Parameters<typeof readStatusOf>[0] & {\n refetch?: () => unknown;\n retry?: () => unknown;\n /** The read's value (react-query `data`, a hook's rows) — present means a read already landed. */\n data?: unknown;\n /** react-query: when the data last arrived (0 = never). */\n dataUpdatedAt?: number;\n /** A hook that tracks it directly. */\n hasData?: boolean;\n};\n\n/** Has a successful read already produced a value? */\nfunction readHasData(read: ReadLike): boolean {\n if (typeof read.hasData === \"boolean\") return read.hasData;\n if (typeof read.dataUpdatedAt === \"number\" && read.dataUpdatedAt > 0) return true;\n // An empty array is often a hook's initial placeholder, not an answer — it\n // proves nothing was read; only a hook's own `hasData`/`dataUpdatedAt` can.\n if (Array.isArray(read.data)) return read.data.length > 0;\n return read.data !== undefined && read.data !== null;\n}\n\n/**\n * Fold a query/hook result into a `ReadOutcome`:\n * <MatrxDataTable data={rows} emptyState={…} read={readOf(query, { what: \"your tasks\" })} />\n * The retry is the read's own `refetch` / `retry` unless `onRetry` is given.\n */\nexport function readOf(read: ReadLike, options: { what?: string; onRetry?: () => void } = {}): ReadOutcome {\n const again = options.onRetry ?? read.refetch ?? read.retry;\n return {\n status: readStatusOf(read),\n error: read.error ?? (read.isError ? true : undefined),\n ...(again ? { onRetry: () => void again() } : {}),\n ...(options.what ? { what: options.what } : {}),\n hasData: readHasData(read),\n };\n}\n\nexport function ReadGate({\n read,\n status: statusProp,\n error: errorProp,\n what: whatProp,\n isEmpty,\n empty,\n loading,\n onRetry: onRetryProp,\n children,\n}: {\n /**\n * The read's outcome in one value — what a list primitive's `read` prop\n * carries. Absent (and no `status`), the gate is \"ready\": a primitive whose\n * caller passed no read renders exactly as before.\n */\n read?: ReadOutcome | undefined;\n status?: ReadStatus;\n error?: unknown;\n what?: string;\n isEmpty: boolean;\n empty: ReactNode;\n loading?: ReactNode;\n onRetry?: (() => void) | undefined;\n children: ReactNode;\n}) {\n const status = statusProp ?? read?.status ?? \"ready\";\n const error = errorProp ?? read?.error;\n const what = whatProp ?? read?.what ?? \"this list\";\n const onRetry = onRetryProp ?? read?.onRetry;\n if (status === \"error\") {\n if (isEmpty) return <ReadFailure error={error ?? true} what={what} onRetry={onRetry} />;\n // Rows from an earlier read stay; the notice says they may be out of date.\n return (\n <>\n {onRetry ? (\n <StaleDataNotice hasData what={what} onRetry={onRetry} className=\"mb-2\" />\n ) : (\n <ReadFailure error={error ?? true} what={what} />\n )}\n {children}\n </>\n );\n }\n if (status === \"loading\" && isEmpty) {\n return (\n loading ?? (\n <div className=\"flex items-center justify-center gap-2 p-4 text-xs text-muted-foreground\" role=\"status\" aria-busy=\"true\">\n <Loader2 className=\"h-4 w-4 matrx-spin\" /> Loading {what}…\n </div>\n )\n );\n }\n if (isEmpty) return <>{empty}</>;\n return <>{children}</>;\n}\n\n/**\n * A list primitive's EMPTY branch (RC-B12 round 13): the failure while its read\n * failed, the wait while it loads, and the primitive's own empty view only\n * after a read that succeeded. No `read` → the empty view, as before.\n *\n * if (items.length === 0) return <ReadEmpty read={read}>{emptyState}</ReadEmpty>;\n */\nexport function ReadEmpty({ read, loading, children }: { read?: ReadOutcome | undefined; loading?: ReactNode; children?: ReactNode }) {\n return (\n <ReadGate read={read} isEmpty empty={children ?? null} {...(loading === undefined ? {} : { loading })}>\n {null}\n </ReadGate>\n );\n}\n\n/**\n * Above a primitive's ROWS: when the latest read failed, the rows on screen are\n * from an earlier read — say so (StaleDataNotice with its retry), never blank them.\n */\nexport function ReadStaleNotice({ read, className }: { read?: ReadOutcome | undefined; className?: string }) {\n if (read?.status !== \"error\") return null;\n const what = read.what ?? \"this list\";\n if (!read.onRetry) return <ReadFailure error={read.error ?? true} what={what} {...(className ? { className } : {})} />;\n return <StaleDataNotice hasData what={what} onRetry={read.onRetry} {...(className ? { className } : {})} />;\n}\n"],"mappings":"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","names":["jsx","jsxs","jsx","jsxs","jsx","jsxs","Fragment","jsx","jsxs"]}
|
|
1
|
+
{"version":3,"sources":["../src/error-notice.tsx","../src/read-state/reload-page-button.tsx","../src/read-state/read-failure.tsx","../src/read-state/stale-data-notice.tsx","../src/read-state/read-gate.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * ErrorNotice — THE inline error card. Every error a surface renders inline\n * (\"Not saved\", \"Failed to load…\", a dialog's refusal, a form's failure) is\n * this component, so every one of them carries the Alchemy Menu and hands an\n * AI the sentence, the code, the operation, the records, the surface's declared\n * values and the person's unsaved input.\n *\n * <ErrorNotice\n * title=\"Not saved\"\n * message={item.error}\n * operation=\"Save comment\"\n * records={[{ type: \"document\", id: docId }]}\n * unsavedInput={{ comment: draft }}\n * actions={<Button onClick={retry}>Retry</Button>}\n * />\n *\n * Moved from matrx-frontend (chat host slots get real package defaults,\n * 2026-10-08). The host's actions (Matrx: the Alchemy Menu) arrive through\n * `ErrorActionsProvider`; the corner ⋯ and right-click menu through\n * `ErrorCardMenusProvider`. Without them the card still says everything.\n */\nimport type { ReactNode } from \"react\";\nimport { AlertTriangleIcon as AlertTriangle } from \"./icons\";\nimport { cn } from \"./cn\";\nimport { Button } from \"./button\";\nimport {\n ErrorActions,\n useErrorCardMenus,\n type ErrorActionsInput,\n type ErrorActionsRecord,\n} from \"./error-box\";\n\nimport {\n errorSentenceOf,\n isTimedOutError,\n peopleErrorSentence,\n TIMED_OUT_SENTENCE,\n} from \"./error-sentence\";\n\nexport { errorSentenceOf, isTimedOutError, TIMED_OUT_SENTENCE };\n\nexport type ErrorNoticeProps = {\n /** Short heading (\"Not saved\"). */\n title?: string;\n /** The sentence shown. Falls back to the error's own message. */\n message?: string | null;\n /** The raw error — code/status/stack go to the AI payload, never the screen. */\n error?: unknown;\n operation?: string;\n records?: readonly ErrorActionsRecord[];\n unsavedInput?: unknown;\n details?: Record<string, unknown>;\n code?: string | number;\n status?: number;\n /** The tables / RPCs the failed call used — pins its captured request as the cause. */\n calls?: readonly string[];\n /** Retry / Discard / other controls, rendered under the sentence. */\n actions?: ReactNode;\n /**\n * Run the failed call again. Shown as \"Try again\" when the error is the\n * store's statement timeout and no `actions` were given.\n */\n onRetry?: () => void;\n /** Extra content under the sentence. */\n children?: ReactNode;\n /**\n * `default` = the card. `compact` = the dense card used inside lists and\n * side panels. `inline` = a one-line field/form error (no card chrome): the\n * sentence in the destructive color with the menu at its end — the shape\n * every `<p role=\"alert\" className=\"text-destructive\">` render moves onto.\n */\n size?: \"inline\" | \"compact\" | \"default\";\n icon?: boolean;\n className?: string;\n};\n\nexport function ErrorNotice({\n title,\n message,\n error,\n operation,\n records,\n unsavedInput,\n details,\n code,\n status,\n calls,\n actions: actionsProp,\n onRetry,\n children,\n size = \"default\",\n icon = true,\n className,\n}: ErrorNoticeProps) {\n const timedOut = isTimedOutError(error, message, code);\n // The one funnel (error-sentence.ts): raw error text and code paths never reach the screen.\n const sentence = peopleErrorSentence(error, message, { code });\n const actions =\n actionsProp ??\n (timedOut && onRetry ? (\n <Button type=\"button\" size=\"sm\" variant=\"outline\" data-error-retry=\"\" onClick={onRetry}>\n Try again\n </Button>\n ) : undefined);\n const menus = useErrorCardMenus();\n const input: ErrorActionsInput = {\n title,\n message: sentence,\n error,\n operation,\n records,\n unsavedInput,\n details,\n code,\n status,\n ...(calls ? { calls } : {}),\n source: \"inline\",\n origin: \"design-system/ErrorNotice\",\n };\n if (size === \"inline\") {\n return (\n <div\n role=\"alert\"\n data-error-notice=\"\"\n className={cn(\"flex items-start gap-1 text-destructive\", className)}\n >\n <span className=\"min-w-0 flex-1 break-words\">\n {title ? (\n <span data-error-title=\"\" className=\"font-medium\">\n {title}:{\" \"}\n </span>\n ) : null}\n {sentence}\n </span>\n {children}\n {actions}\n <ErrorActions {...input} />\n </div>\n );\n }\n const compact = size === \"compact\";\n // Title, actions or extra content make the card taller than the ⋯ + menu column.\n const stacked = Boolean(title || actions || children);\n // The card is an action host like any other content (ALC-15): right-click,\n // ⋯ (the same menu, opened at the button), the phone's sheet and the palette\n // (⌘/Ctrl+Shift+K) all show the ONE registry's actions over this sentence.\n // Read-only: nothing here may change a record.\n const menuTitle = title ?? \"Error\";\n const menuText = title ? `${title}: ${sentence}` : sentence;\n const card = (\n <div\n role=\"alert\"\n data-error-notice=\"\"\n className={cn(\n \"rounded-md border border-destructive/30 bg-destructive/5\",\n compact ? \"px-2 py-1.5 text-xs\" : \"px-3 py-2.5 text-sm\",\n className,\n )}\n >\n <div className=\"flex items-start gap-2\">\n {icon && (\n <AlertTriangle\n aria-hidden\n className={cn(\n \"shrink-0 text-destructive\",\n compact ? \"mt-px h-3.5 w-3.5\" : \"mt-0.5 h-4 w-4\",\n )}\n />\n )}\n <div className=\"min-w-0 flex-1\">\n {title && (\n <p data-error-title=\"\" className=\"font-medium text-destructive\">\n {title}\n </p>\n )}\n <p className={cn(\"break-words text-foreground\", title && \"mt-0.5\")}>\n {sentence}\n {!stacked && <ErrorActions {...input} />}\n </p>\n {children}\n {actions && <div className=\"mt-1 flex flex-wrap gap-1\">{actions}</div>}\n </div>\n {/* The corner column the ⋯ already owns carries the menu under it: no\n new column (beside the ⋯ it narrowed every line — 176→224px at\n 375px) and no new line (inline, a full last line wrapped it,\n +16px). A one-line notice with nothing under its sentence is\n shorter than that column, so there the menu rides the sentence\n (RC-B12 layout rule). */}\n <div className=\"flex shrink-0 flex-col items-center\">\n {menus ? (\n <menus.Button\n title={menuTitle}\n text={menuText}\n {...(compact ? { className: \"h-6 w-6\" } : {})}\n />\n ) : null}\n {stacked && <ErrorActions {...input} />}\n </div>\n </div>\n </div>\n );\n if (!menus) return card;\n return (\n <menus.Boundary title={menuTitle} text={menuText}>\n {card}\n </menus.Boundary>\n );\n}\n","\"use client\";\n\n/**\n * THE SERVER-SAFE RETRY. A server page cannot hand a failure notice a retry\n * function — functions do not cross the server/client boundary — so its\n * \"Couldn't load …\" card had no way forward but the browser's own reload\n * (page-pass 2026-09-27, /chat/message-templates/<id>). This control is the\n * retry any server-rendered failure can carry: it takes no props a server\n * cannot send, and it reloads THIS url (path, query and hash), which re-runs the\n * server read that failed.\n *\n * Its words say what it does — \"Reload page\" — because a reload also drops\n * anything unsaved elsewhere on the page; a client surface that can retry just\n * its own read passes `onRetry` to `ReadFailure` instead.\n */\nimport { RefreshCwIcon as RefreshCw } from \"../icons\";\nimport { Button } from \"../button\";\n\nexport const RELOAD_PAGE_LABEL = \"Reload page\";\n\nexport function ReloadPageButton({ className }: { className?: string }) {\n return (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className={className ?? \"h-7 gap-1.5 text-xs\"}\n onClick={() => window.location.reload()}\n >\n <RefreshCw className=\"h-3.5 w-3.5\" aria-hidden /> {RELOAD_PAGE_LABEL}\n </Button>\n );\n}\n","\"use client\";\n\n/**\n * The failed-read view every list uses instead of its empty state (RC-B12\n * round 11): the error, with the Alchemy Menu (through ErrorNotice) and a\n * retry when the caller has one. `error` may be the error itself or just the\n * read's failed flag — the sentence never pretends there is nothing.\n *\n * ALWAYS A WAY FORWARD. With `onRetry` it offers \"Try again\" (the surface's own\n * read, nothing else touched). Without one — every SERVER page, which cannot\n * pass a function — it offers `ReloadPageButton`: a same-URL reload that re-runs\n * the server read. A failure card with no retry at all is gone.\n */\nimport { RefreshCwIcon as RefreshCw } from \"../icons\";\nimport { Button } from \"../button\";\nimport { ErrorNotice } from \"../error-notice\";\nimport { ReloadPageButton } from \"./reload-page-button\";\n\nexport type ReadFailureProps = {\n error: unknown;\n /** What could not be read, in the reader's words: \"your tasks\". */\n what?: string | undefined;\n /** Retry just this read. Absent (always, from a server page): a same-URL reload is offered. */\n onRetry?: (() => void) | undefined;\n className?: string | undefined;\n size?: \"compact\" | \"default\" | undefined;\n};\n\nexport function ReadFailure({\n error,\n what = \"this list\",\n onRetry,\n className,\n size = \"compact\",\n}: ReadFailureProps) {\n const flagOnly = error === true || error == null || error === \"\";\n return (\n <ErrorNotice\n size={size}\n className={className ?? \"m-3\"}\n title={`Couldn't load ${what}`}\n {...(flagOnly\n ? { message: `The read for ${what} failed, so nothing here is an answer — not \"empty\".` }\n : { error })}\n operation={`Load ${what}`}\n actions={\n onRetry ? (\n <Button variant=\"outline\" size=\"sm\" className=\"h-7 gap-1.5 text-xs\" onClick={onRetry}>\n <RefreshCw className=\"h-3.5 w-3.5\" /> Try again\n </Button>\n ) : (\n <ReloadPageButton />\n )\n }\n />\n );\n}\n","\"use client\";\n\n/**\n * StaleDataNotice — what a surface shows when a READ FAILED and the rows on\n * screen are therefore not to be trusted.\n *\n * THE DEAD END this kills: a list loads fine, the user acts, the surface\n * reloads — and the reload fails. A toast fires and fades. What remains is the\n * PREVIOUS list, rendered exactly like a fresh one. The user reads a roster\n * that no longer reflects the database and has no way to know. Bugbot found\n * this on the admins roster and the feedback table in the same pass, which is\n * what made it a primitive instead of two patches.\n *\n * This is the campaign's never-report-green rule at its sharpest: **a surface\n * must never present data it could not actually read as though it had.** The\n * sibling `DeepLinkMissNotice` states a definitive negative and so demands\n * proof the list WAS read; this states the opposite — that the read broke —\n * and is what that component's \"show the surface's ordinary error state\n * instead\" was pointing at. Neither is complete without the other.\n *\n * Two cases, and conflating them is the actual bug:\n *\n * 1. **Stale** (`hasData`) — rows are on screen from an earlier successful\n * read. Say they may be out of date. Do NOT blank them: old data plus an\n * honest label beats an empty screen, and the user may still need it.\n * 2. **Unknown** (`!hasData`) — nothing was ever read. An empty list here\n * reads as \"there are none\", which is a claim about the database nobody\n * is entitled to make. Say the read failed instead.\n *\n * `onRetry` is not optional decoration — a problem you can detect ships with\n * its one-click fix. The user should never have to reload the page to recover\n * from a failed fetch.\n */\n\nimport { AlertTriangleIcon as AlertTriangle, RefreshCwIcon as RefreshCw } from \"../icons\";\nimport { Button } from \"../button\";\nimport { cn } from \"../cn\";\nimport { ErrorActions } from \"../error-box\";\n\nexport interface StaleDataNoticeProps {\n /**\n * Whether rows from an earlier SUCCESSFUL read are currently displayed.\n * Decides between \"this may be out of date\" and \"we never read this\".\n */\n hasData: boolean;\n /**\n * What could not be read, as a plain noun phrase the user recognizes —\n * \"the admin roster\", \"feedback\". Never a table or endpoint name: our user\n * is a subject-matter expert, not an engineer.\n */\n what: string;\n /** Re-run the read. Required — the notice always carries its own fix. */\n onRetry: () => void;\n /** Disables the retry control and shows motion while a retry is in flight. */\n retrying?: boolean;\n /** Optional detail (a server message). Shown verbatim, never interpreted. */\n detail?: string | null;\n /**\n * A multi-source list where SOME sources read and some did not: the rows on\n * screen are current, but `what` is missing from them. Says so instead of\n * \"may be out of date\" (RC-B12 round 12 — a partly failed roster).\n */\n partial?: boolean;\n className?: string;\n}\n\nexport function StaleDataNotice({\n hasData,\n what,\n onRetry,\n retrying = false,\n detail,\n partial = false,\n className,\n}: StaleDataNoticeProps) {\n return (\n <div\n role=\"status\"\n className={cn(\n \"flex flex-wrap items-center gap-x-3 gap-y-1.5 rounded-md border border-amber-500/40 bg-amber-500/10 px-3 py-2 type-body\",\n className,\n )}\n >\n <AlertTriangle className=\"h-4 w-4 shrink-0 text-amber-600 dark:text-amber-500\" />\n <span className=\"min-w-0 flex-1 text-foreground\">\n {partial ? (\n <>\n Couldn't load {what}, so <strong>they are missing</strong> from\n the list below — everything else loaded.\n </>\n ) : hasData ? (\n <>\n Couldn't refresh {what}. What you see below is the last version\n we loaded and <strong>may be out of date</strong>.\n </>\n ) : (\n <>\n Couldn't load {what}, so this list is empty because the read\n failed — <strong>not</strong> because there is nothing here.\n </>\n )}\n {detail ? (\n <span className=\"ml-1 text-muted-foreground\">({detail})</span>\n ) : null}\n {/* A failed read is an error on screen: it carries the Alchemy Menu (RC-B12),\n INSIDE the sentence — as its own flex item it narrowed the text and\n grew the notice 118→138px at 375px. */}\n <ErrorActions\n title={partial || !hasData ? `Couldn't load ${what}` : `Couldn't refresh ${what}`}\n message={\n detail\n ? `${hasData ? \"Couldn't refresh\" : \"Couldn't load\"} ${what}: ${detail}`\n : `${hasData ? \"Couldn't refresh\" : \"Couldn't load\"} ${what}.`\n }\n operation={`${hasData ? \"Refresh\" : \"Load\"} ${what}`}\n details={partial ? { rows_on_screen_are_partial: true } : { rows_on_screen_are_stale: hasData }}\n source=\"inline\"\n origin=\"design-system/StaleDataNotice\"\n />\n </span>\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={onRetry}\n disabled={retrying}\n className=\"h-11 shrink-0 gap-1.5 lg:h-7\"\n >\n <RefreshCw className={cn(\"h-3.5 w-3.5\", retrying && \"matrx-spin\")} />\n {retrying ? \"Retrying…\" : \"Try again\"}\n </Button>\n </div>\n );\n}\n","\"use client\";\n\n/**\n * THE rule for every empty view (RC-B12 round 11): \"nothing here\" is an answer\n * only after a read that SUCCEEDED and returned nothing.\n *\n * <ReadGate status={readStatusOf(query)} error={query.error} what=\"your tasks\"\n * isEmpty={tasks.length === 0} empty={<p>No tasks yet.</p>} onRetry={refetch}>\n * <TaskList tasks={tasks} />\n * </ReadGate>\n *\n * loading → a wait · error → the failure (with the menu) · ready + empty → the\n * empty view · ready → the rows. When a refresh fails but earlier rows are on\n * screen, the rows stay and the failure is said above them.\n * Guarded by the census `findUngatedEmptyStates` and the lint rule\n * matrx/error-render-carries-alchemy (an empty view under a loading check\n * with no failure check is refused).\n */\nimport type { ReactNode } from \"react\";\nimport { Loader2Icon as Loader2 } from \"../icons\";\nimport { ReadFailure } from \"./read-failure\";\nimport { StaleDataNotice } from \"./stale-data-notice\";\n\nexport type ReadStatus = \"loading\" | \"error\" | \"ready\";\n\n/** The usual shapes a read reports itself in, folded into one status. */\nexport function readStatusOf(read: {\n status?: string | null;\n isLoading?: boolean;\n loading?: boolean;\n isFetching?: boolean;\n isError?: boolean;\n error?: unknown;\n}): ReadStatus {\n if (read.isError || (read.error != null && read.error !== false && read.error !== \"\")) return \"error\";\n if (read.status === \"error\" || read.status === \"failed\") return \"error\";\n if (read.isLoading || read.loading || read.status === \"loading\" || read.status === \"idle\" || read.status === \"pending\") return \"loading\";\n return \"ready\";\n}\n\n/**\n * A read's outcome handed to a list/table primitive (RC-B12 round 13). Every\n * primitive that takes an `emptyState` also takes `read?: ReadOutcome` and\n * renders the empty state only when `status === \"ready\"`: \"error\" with no rows\n * → <ReadFailure/> with the retry; \"error\" with rows → the rows under a stale\n * notice; \"loading\" with no rows → loading. Structurally identical to the\n * design-system table's `MatrxDataTableRead`, so one value serves both.\n */\nexport interface ReadOutcome {\n status: ReadStatus;\n /** The failure itself, or a truthy flag when the read only says it failed. */\n error?: unknown;\n onRetry?: (() => void) | undefined;\n /** What was read, in the reader's words: \"your tasks\". */\n what?: string | undefined;\n /**\n * A successful read has already produced a value (rows, a count). Then a\n * refetch in flight is not \"loading\" and a failed refresh is not \"no value\":\n * counts keep their last known value (with the stale mark) — the same\n * stale-while-error rule as rows (RC-B12 r13 ruling).\n */\n hasData?: boolean | undefined;\n}\n\ntype ReadLike = Parameters<typeof readStatusOf>[0] & {\n refetch?: () => unknown;\n retry?: () => unknown;\n /** The read's value (react-query `data`, a hook's rows) — present means a read already landed. */\n data?: unknown;\n /** react-query: when the data last arrived (0 = never). */\n dataUpdatedAt?: number;\n /** A hook that tracks it directly. */\n hasData?: boolean;\n};\n\n/** Has a successful read already produced a value? */\nfunction readHasData(read: ReadLike): boolean {\n if (typeof read.hasData === \"boolean\") return read.hasData;\n if (typeof read.dataUpdatedAt === \"number\" && read.dataUpdatedAt > 0) return true;\n // An empty array is often a hook's initial placeholder, not an answer — it\n // proves nothing was read; only a hook's own `hasData`/`dataUpdatedAt` can.\n if (Array.isArray(read.data)) return read.data.length > 0;\n return read.data !== undefined && read.data !== null;\n}\n\n/**\n * Fold a query/hook result into a `ReadOutcome`:\n * <MatrxDataTable data={rows} emptyState={…} read={readOf(query, { what: \"your tasks\" })} />\n * The retry is the read's own `refetch` / `retry` unless `onRetry` is given.\n */\nexport function readOf(read: ReadLike, options: { what?: string; onRetry?: () => void } = {}): ReadOutcome {\n const again = options.onRetry ?? read.refetch ?? read.retry;\n return {\n status: readStatusOf(read),\n error: read.error ?? (read.isError ? true : undefined),\n ...(again ? { onRetry: () => void again() } : {}),\n ...(options.what ? { what: options.what } : {}),\n hasData: readHasData(read),\n };\n}\n\nexport function ReadGate({\n read,\n status: statusProp,\n error: errorProp,\n what: whatProp,\n isEmpty,\n empty,\n loading,\n onRetry: onRetryProp,\n children,\n}: {\n /**\n * The read's outcome in one value — what a list primitive's `read` prop\n * carries. Absent (and no `status`), the gate is \"ready\": a primitive whose\n * caller passed no read renders exactly as before.\n */\n read?: ReadOutcome | undefined;\n status?: ReadStatus;\n error?: unknown;\n what?: string;\n isEmpty: boolean;\n empty: ReactNode;\n loading?: ReactNode;\n onRetry?: (() => void) | undefined;\n children: ReactNode;\n}) {\n const status = statusProp ?? read?.status ?? \"ready\";\n const error = errorProp ?? read?.error;\n const what = whatProp ?? read?.what ?? \"this list\";\n const onRetry = onRetryProp ?? read?.onRetry;\n if (status === \"error\") {\n if (isEmpty) return <ReadFailure error={error ?? true} what={what} onRetry={onRetry} />;\n // Rows from an earlier read stay; the notice says they may be out of date.\n return (\n <>\n {onRetry ? (\n <StaleDataNotice hasData what={what} onRetry={onRetry} className=\"mb-2\" />\n ) : (\n <ReadFailure error={error ?? true} what={what} />\n )}\n {children}\n </>\n );\n }\n if (status === \"loading\" && isEmpty) {\n return (\n loading ?? (\n <div className=\"flex items-center justify-center gap-2 p-4 text-xs text-muted-foreground\" role=\"status\" aria-busy=\"true\">\n <Loader2 className=\"h-4 w-4 matrx-spin\" /> Loading {what}…\n </div>\n )\n );\n }\n if (isEmpty) return <>{empty}</>;\n return <>{children}</>;\n}\n\n/**\n * A list primitive's EMPTY branch (RC-B12 round 13): the failure while its read\n * failed, the wait while it loads, and the primitive's own empty view only\n * after a read that succeeded. No `read` → the empty view, as before.\n *\n * if (items.length === 0) return <ReadEmpty read={read}>{emptyState}</ReadEmpty>;\n */\nexport function ReadEmpty({ read, loading, children }: { read?: ReadOutcome | undefined; loading?: ReactNode; children?: ReactNode }) {\n return (\n <ReadGate read={read} isEmpty empty={children ?? null} {...(loading === undefined ? {} : { loading })}>\n {null}\n </ReadGate>\n );\n}\n\n/**\n * Above a primitive's ROWS: when the latest read failed, the rows on screen are\n * from an earlier read — say so (StaleDataNotice with its retry), never blank them.\n */\nexport function ReadStaleNotice({ read, className }: { read?: ReadOutcome | undefined; className?: string }) {\n if (read?.status !== \"error\") return null;\n const what = read.what ?? \"this list\";\n if (!read.onRetry) return <ReadFailure error={read.error ?? true} what={what} {...(className ? { className } : {})} />;\n return <StaleDataNotice hasData what={what} onRetry={read.onRetry} {...(className ? { className } : {})} />;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAsGM,cA4BM,YA5BN;AAxBC,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP,OAAO;AAAA,EACP;AACF,GAAqB;AACnB,QAAM,WAAW,gBAAgB,OAAO,SAAS,IAAI;AAErD,QAAM,WAAW,oBAAoB,OAAO,SAAS,EAAE,KAAK,CAAC;AAC7D,QAAM,UACJ,gBACC,YAAY,UACX,oBAAC,UAAO,MAAK,UAAS,MAAK,MAAK,SAAQ,WAAU,oBAAiB,IAAG,SAAS,SAAS,uBAExF,IACE;AACN,QAAM,QAAQ,kBAAkB;AAChC,QAAM,QAA2B;AAAA,IAC/B;AAAA,IACA,SAAS;AAAA,IACT;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;AAAA,IACzB,QAAQ;AAAA,IACR,QAAQ;AAAA,EACV;AACA,MAAI,SAAS,UAAU;AACrB,WACE;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,qBAAkB;AAAA,QAClB,WAAW,GAAG,2CAA2C,SAAS;AAAA,QAElE;AAAA,+BAAC,UAAK,WAAU,8BACb;AAAA,oBACC,qBAAC,UAAK,oBAAiB,IAAG,WAAU,eACjC;AAAA;AAAA,cAAM;AAAA,cAAE;AAAA,eACX,IACE;AAAA,YACH;AAAA,aACH;AAAA,UACC;AAAA,UACA;AAAA,UACD,oBAAC,gBAAc,GAAG,OAAO;AAAA;AAAA;AAAA,IAC3B;AAAA,EAEJ;AACA,QAAM,UAAU,SAAS;AAEzB,QAAM,UAAU,QAAQ,SAAS,WAAW,QAAQ;AAKpD,QAAM,YAAY,SAAS;AAC3B,QAAM,WAAW,QAAQ,GAAG,KAAK,KAAK,QAAQ,KAAK;AACnD,QAAM,OACJ;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,qBAAkB;AAAA,MAClB,WAAW;AAAA,QACT;AAAA,QACA,UAAU,wBAAwB;AAAA,QAClC;AAAA,MACF;AAAA,MAEA,+BAAC,SAAI,WAAU,0BACZ;AAAA,gBACC;AAAA,UAAC;AAAA;AAAA,YACC,eAAW;AAAA,YACX,WAAW;AAAA,cACT;AAAA,cACA,UAAU,sBAAsB;AAAA,YAClC;AAAA;AAAA,QACF;AAAA,QAEF,qBAAC,SAAI,WAAU,kBACZ;AAAA,mBACC,oBAAC,OAAE,oBAAiB,IAAG,WAAU,gCAC9B,iBACH;AAAA,UAEF,qBAAC,OAAE,WAAW,GAAG,+BAA+B,SAAS,QAAQ,GAC9D;AAAA;AAAA,YACA,CAAC,WAAW,oBAAC,gBAAc,GAAG,OAAO;AAAA,aACxC;AAAA,UACC;AAAA,UACA,WAAW,oBAAC,SAAI,WAAU,6BAA6B,mBAAQ;AAAA,WAClE;AAAA,QAOA,qBAAC,SAAI,WAAU,uCACZ;AAAA,kBACC;AAAA,YAAC,MAAM;AAAA,YAAN;AAAA,cACC,OAAO;AAAA,cACP,MAAM;AAAA,cACL,GAAI,UAAU,EAAE,WAAW,UAAU,IAAI,CAAC;AAAA;AAAA,UAC7C,IACE;AAAA,UACH,WAAW,oBAAC,gBAAc,GAAG,OAAO;AAAA,WACvC;AAAA,SACF;AAAA;AAAA,EACF;AAEF,MAAI,CAAC,MAAO,QAAO;AACnB,SACE,oBAAC,MAAM,UAAN,EAAe,OAAO,WAAW,MAAM,UACrC,gBACH;AAEJ;;;AC3LI,SAOE,OAAAA,MAPF,QAAAC,aAAA;AAJG,IAAM,oBAAoB;AAE1B,SAAS,iBAAiB,EAAE,UAAU,GAA2B;AACtE,SACE,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,WAAW,aAAa;AAAA,MACxB,SAAS,MAAM,OAAO,SAAS,OAAO;AAAA,MAEtC;AAAA,wBAAAD,KAAC,iBAAU,WAAU,eAAc,eAAW,MAAC;AAAA,QAAE;AAAA,QAAE;AAAA;AAAA;AAAA,EACrD;AAEJ;;;ACeU,SACE,OAAAE,MADF,QAAAC,aAAA;AAnBH,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA,OAAO;AACT,GAAqB;AACnB,QAAM,WAAW,UAAU,QAAQ,SAAS,QAAQ,UAAU;AAC9D,SACE,gBAAAD;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,WAAW,aAAa;AAAA,MACxB,OAAO,iBAAiB,IAAI;AAAA,MAC3B,GAAI,WACD,EAAE,SAAS,gBAAgB,IAAI,4DAAuD,IACtF,EAAE,MAAM;AAAA,MACZ,WAAW,QAAQ,IAAI;AAAA,MACvB,SACE,UACE,gBAAAC,MAAC,UAAO,SAAQ,WAAU,MAAK,MAAK,WAAU,uBAAsB,SAAS,SAC3E;AAAA,wBAAAD,KAAC,iBAAU,WAAU,eAAc;AAAA,QAAE;AAAA,SACvC,IAEA,gBAAAA,KAAC,oBAAiB;AAAA;AAAA,EAGxB;AAEJ;;;AC2BM,SAGI,UAHJ,OAAAE,MAGI,QAAAC,aAHJ;AAjBC,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA,UAAU;AAAA,EACV;AACF,GAAyB;AACvB,SACE,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MAEA;AAAA,wBAAAD,KAAC,qBAAc,WAAU,uDAAsD;AAAA,QAC/E,gBAAAC,MAAC,UAAK,WAAU,kCACb;AAAA,oBACC,gBAAAA,MAAA,YAAE;AAAA;AAAA,YACoB;AAAA,YAAK;AAAA,YAAK,gBAAAD,KAAC,YAAO,8BAAgB;AAAA,YAAS;AAAA,aAEjE,IACE,UACF,gBAAAC,MAAA,YAAE;AAAA;AAAA,YACuB;AAAA,YAAK;AAAA,YACd,gBAAAD,KAAC,YAAO,gCAAkB;AAAA,YAAS;AAAA,aACnD,IAEA,gBAAAC,MAAA,YAAE;AAAA;AAAA,YACoB;AAAA,YAAK;AAAA,YAChB,gBAAAD,KAAC,YAAO,iBAAG;AAAA,YAAS;AAAA,aAC/B;AAAA,UAED,SACC,gBAAAC,MAAC,UAAK,WAAU,8BAA6B;AAAA;AAAA,YAAE;AAAA,YAAO;AAAA,aAAC,IACrD;AAAA,UAIJ,gBAAAD;AAAA,YAAC;AAAA;AAAA,cACC,OAAO,WAAW,CAAC,UAAU,iBAAiB,IAAI,KAAK,oBAAoB,IAAI;AAAA,cAC/E,SACE,SACI,GAAG,UAAU,qBAAqB,eAAe,IAAI,IAAI,KAAK,MAAM,KACpE,GAAG,UAAU,qBAAqB,eAAe,IAAI,IAAI;AAAA,cAE/D,WAAW,GAAG,UAAU,YAAY,MAAM,IAAI,IAAI;AAAA,cAClD,SAAS,UAAU,EAAE,4BAA4B,KAAK,IAAI,EAAE,0BAA0B,QAAQ;AAAA,cAC9F,QAAO;AAAA,cACP,QAAO;AAAA;AAAA,UACT;AAAA,WACF;AAAA,QACA,gBAAAC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,SAAS;AAAA,YACT,UAAU;AAAA,YACV,WAAU;AAAA,YAEV;AAAA,8BAAAD,KAAC,iBAAU,WAAW,GAAG,eAAe,YAAY,YAAY,GAAG;AAAA,cAClE,WAAW,mBAAc;AAAA;AAAA;AAAA,QAC5B;AAAA;AAAA;AAAA,EACF;AAEJ;;;ACAwB,SAGlB,YAAAE,WAHkB,OAAAC,MAGlB,QAAAC,aAHkB;AA1GjB,SAAS,aAAa,MAOd;AACb,MAAI,KAAK,WAAY,KAAK,SAAS,QAAQ,KAAK,UAAU,SAAS,KAAK,UAAU,GAAK,QAAO;AAC9F,MAAI,KAAK,WAAW,WAAW,KAAK,WAAW,SAAU,QAAO;AAChE,MAAI,KAAK,aAAa,KAAK,WAAW,KAAK,WAAW,aAAa,KAAK,WAAW,UAAU,KAAK,WAAW,UAAW,QAAO;AAC/H,SAAO;AACT;AAsCA,SAAS,YAAY,MAAyB;AAC5C,MAAI,OAAO,KAAK,YAAY,UAAW,QAAO,KAAK;AACnD,MAAI,OAAO,KAAK,kBAAkB,YAAY,KAAK,gBAAgB,EAAG,QAAO;AAG7E,MAAI,MAAM,QAAQ,KAAK,IAAI,EAAG,QAAO,KAAK,KAAK,SAAS;AACxD,SAAO,KAAK,SAAS,UAAa,KAAK,SAAS;AAClD;AAOO,SAAS,OAAO,MAAgB,UAAmD,CAAC,GAAgB;AACzG,QAAM,QAAQ,QAAQ,WAAW,KAAK,WAAW,KAAK;AACtD,SAAO;AAAA,IACL,QAAQ,aAAa,IAAI;AAAA,IACzB,OAAO,KAAK,UAAU,KAAK,UAAU,OAAO;AAAA,IAC5C,GAAI,QAAQ,EAAE,SAAS,MAAM,KAAK,MAAM,EAAE,IAAI,CAAC;AAAA,IAC/C,GAAI,QAAQ,OAAO,EAAE,MAAM,QAAQ,KAAK,IAAI,CAAC;AAAA,IAC7C,SAAS,YAAY,IAAI;AAAA,EAC3B;AACF;AAEO,SAAS,SAAS;AAAA,EACvB;AAAA,EACA,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,MAAM;AAAA,EACN;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT;AACF,GAeG;AACD,QAAM,SAAS,cAAc,MAAM,UAAU;AAC7C,QAAM,QAAQ,aAAa,MAAM;AACjC,QAAM,OAAO,YAAY,MAAM,QAAQ;AACvC,QAAM,UAAU,eAAe,MAAM;AACrC,MAAI,WAAW,SAAS;AACtB,QAAI,QAAS,QAAO,gBAAAD,KAAC,eAAY,OAAO,SAAS,MAAM,MAAY,SAAkB;AAErF,WACE,gBAAAC,MAAAF,WAAA,EACG;AAAA,gBACC,gBAAAC,KAAC,mBAAgB,SAAO,MAAC,MAAY,SAAkB,WAAU,QAAO,IAExE,gBAAAA,KAAC,eAAY,OAAO,SAAS,MAAM,MAAY;AAAA,MAEhD;AAAA,OACH;AAAA,EAEJ;AACA,MAAI,WAAW,aAAa,SAAS;AACnC,WACE,WACE,gBAAAC,MAAC,SAAI,WAAU,4EAA2E,MAAK,UAAS,aAAU,QAChH;AAAA,sBAAAD,KAAC,eAAQ,WAAU,sBAAqB;AAAA,MAAE;AAAA,MAAU;AAAA,MAAK;AAAA,OAC3D;AAAA,EAGN;AACA,MAAI,QAAS,QAAO,gBAAAA,KAAAD,WAAA,EAAG,iBAAM;AAC7B,SAAO,gBAAAC,KAAAD,WAAA,EAAG,UAAS;AACrB;AASO,SAAS,UAAU,EAAE,MAAM,SAAS,SAAS,GAAkF;AACpI,SACE,gBAAAC,KAAC,YAAS,MAAY,SAAO,MAAC,OAAO,YAAY,MAAO,GAAI,YAAY,SAAY,CAAC,IAAI,EAAE,QAAQ,GAChG,gBACH;AAEJ;AAMO,SAAS,gBAAgB,EAAE,MAAM,UAAU,GAA2D;AAC3G,MAAI,MAAM,WAAW,QAAS,QAAO;AACrC,QAAM,OAAO,KAAK,QAAQ;AAC1B,MAAI,CAAC,KAAK,QAAS,QAAO,gBAAAA,KAAC,eAAY,OAAO,KAAK,SAAS,MAAM,MAAa,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC,GAAI;AACpH,SAAO,gBAAAA,KAAC,mBAAgB,SAAO,MAAC,MAAY,SAAS,KAAK,SAAU,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC,GAAI;AAC3G;","names":["jsx","jsxs","jsx","jsxs","jsx","jsxs","Fragment","jsx","jsxs"]}
|