@bettercms-ai/kit 2.0.0-next.6 → 2.0.0-next.7

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.
@@ -2,7 +2,7 @@
2
2
  * ICON — the icon field (DESIGN 8.14), V2 controls/icon.tsx fitted to what prod stores.
3
3
  *
4
4
  * Values (backend reference-validation `icon`, the classic PropsPanel IconValueInput):
5
- * "arrow-right" a name from the BetterCMS icon set (DOCK_ICON_NAMES, 472 names)
5
+ * "arrow-right" a name from the BetterCMS icon set (DOCK_ICON_NAMES, 473 names)
6
6
  * "https://…/logo.svg" an uploaded icon: an image URL (picked from the media library; a component prop
7
7
  * stores the URL string, like the classic panel's codec="url")
8
8
  * { url, … } an uploaded icon stored as an image value: shown and kept until replaced
@@ -16,7 +16,7 @@ import { Tooltip } from "@base-ui/react/tooltip";
16
16
  * ICON — the icon field (DESIGN 8.14), V2 controls/icon.tsx fitted to what prod stores.
17
17
  *
18
18
  * Values (backend reference-validation `icon`, the classic PropsPanel IconValueInput):
19
- * "arrow-right" a name from the BetterCMS icon set (DOCK_ICON_NAMES, 472 names)
19
+ * "arrow-right" a name from the BetterCMS icon set (DOCK_ICON_NAMES, 473 names)
20
20
  * "https://…/logo.svg" an uploaded icon: an image URL (picked from the media library; a component prop
21
21
  * stores the URL string, like the classic panel's codec="url")
22
22
  * { url, … } an uploaded icon stored as an image value: shown and kept until replaced
@@ -1 +1 @@
1
- {"version":3,"file":"icon.js","names":[],"sources":["../../src/controls/icon.tsx"],"sourcesContent":["/**\n * ICON — the icon field (DESIGN 8.14), V2 controls/icon.tsx fitted to what prod stores.\n *\n * Values (backend reference-validation `icon`, the classic PropsPanel IconValueInput):\n * \"arrow-right\" a name from the BetterCMS icon set (DOCK_ICON_NAMES, 472 names)\n * \"https://…/logo.svg\" an uploaded icon: an image URL (picked from the media library; a component prop\n * stores the URL string, like the classic panel's codec=\"url\")\n * { url, … } an uploaded icon stored as an image value: shown and kept until replaced\n * null no icon (\"No icon\"; the backend skips an empty value)\n *\n * The grid offers the BetterCMS set, the one prod accepts. Search also matches a name's Tabler twin through the\n * Tabler ↔ dock adapter (adapters/icons.ts), so \"map\" finds earth-americas. A stored name outside the set (a stale\n * Tabler or Lucide name) is shown with its reason and kept until the author picks another.\n *\n * Glyphs: the host draws them (KitServices.IconGlyph: the dashboard's DockIcon, the set the site renders). With no\n * host renderer the kit draws the Tabler twin, loaded on first use; a name with no Tabler twin shows its name.\n */\nimport * as React from \"react\";\nimport type { Icon as TablerIcon } from \"@tabler/icons-react\";\nimport { IconAlertTriangle, IconChevronDown, IconPhoto, IconSearch, IconX } from \"@tabler/icons-react\";\nimport { cn } from \"../lib/cn\";\nimport { isObj } from \"../lib/path\";\nimport { iconLabel } from \"../lib/icons\";\nimport { useKitServices } from \"../context\";\nimport { DOCK_ICON_NAMES, DOCK_ICON_NAME_SET } from \"../adapters/prod-types\";\nimport { tablerIconOf } from \"../adapters/icons\";\nimport type { ControlProps } from \"../types\";\nimport { Button } from \"../ui/button\";\nimport { Tooltip as TooltipPrimitive } from \"@base-ui/react/tooltip\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../ui/overlay\";\nimport { TooltipLayer } from \"../ui/tooltip-root\";\n\n/* ── Glyphs ───────────────────────────────────────────────────────────── */\ndeclare module \"../context\" {\n interface KitServices {\n /**\n * Draws one icon of the BetterCMS set by name, aria-hidden (the name beside it is the accessible text): the glyphs\n * the site renders. The dashboard passes `DockIcon` (src/components/dock/DockIcon). Absent → the Tabler twin.\n */\n IconGlyph?: React.ComponentType<{ name: string; className?: string }>;\n }\n}\n\nlet tablerSet: Record<string, unknown> | null = null;\nlet tablerLoad: Promise<void> | null = null;\nconst pascal = (kebab: string) => `Icon${kebab.split(\"-\").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(\"\")}`;\n\n/** The full Tabler set, loaded once and only when no host renderer is provided. */\nfunction useTablerSet(need: boolean): Record<string, unknown> | null {\n const [, landed] = React.useReducer((n: number) => n + 1, 0);\n React.useEffect(() => {\n if (!need || tablerSet) return;\n let live = true;\n tablerLoad ??= import(\"@tabler/icons-react\").then((m) => {\n tablerSet = m as unknown as Record<string, unknown>;\n });\n tablerLoad.then(() => live && landed(), () => {});\n return () => {\n live = false;\n };\n }, [need]);\n return tablerSet;\n}\n\nfunction Glyph({ name, className }: { name: string; className?: string }) {\n const Host = useKitServices().IconGlyph;\n const set = useTablerSet(!Host);\n if (Host) return <Host name={name} className={className} />;\n const T = set?.[pascal(tablerIconOf(name))] as TablerIcon | undefined;\n if (T) return <T className={className} stroke={1.5} aria-hidden />;\n // no Tabler twin (or the set is still loading): the name itself, never a stand-in shape\n return (\n <span aria-hidden className=\"max-w-full truncate px-0.5 text-[9px] leading-3 text-kit-fg-3\">\n {name}\n </span>\n );\n}\n\n/* ── Values ───────────────────────────────────────────────────────────── */\ntype Read =\n | { kind: \"empty\" }\n | { kind: \"name\"; name: string }\n | { kind: \"unknown\"; name: string }\n | { kind: \"upload\"; url: string }\n | { kind: \"unreadable\" };\nconst isUrlish = (s: string) => /[/.]/.test(s);\nfunction readIcon(v: unknown): Read {\n if (v == null || v === \"\") return { kind: \"empty\" };\n if (typeof v === \"string\") return DOCK_ICON_NAME_SET.has(v) ? { kind: \"name\", name: v } : isUrlish(v) ? { kind: \"upload\", url: v } : { kind: \"unknown\", name: v };\n if (isObj(v) && typeof v.url === \"string\" && v.url) return { kind: \"upload\", url: v.url };\n return { kind: \"unreadable\" };\n}\nconst fileName = (url: string) => decodeURIComponent(url.split(/[?#]/)[0].split(\"/\").pop() || url);\n\n/** Names picked this session, newest first (shown as \"Recent\"). */\nconst recents: string[] = [];\nconst pushRecent = (name: string) => {\n const i = recents.indexOf(name);\n if (i !== -1) recents.splice(i, 1);\n recents.unshift(name);\n recents.length = Math.min(recents.length, COLS);\n};\n\n/* ── The grid: one tab stop, arrow keys, Home / End ───────────────────── */\nconst COLS = 8;\nconst norm = (q: string) => q.toLowerCase().trim().replace(/\\s+/g, \"-\");\ninterface GridItem {\n key: string;\n label: string;\n node: React.ReactNode;\n}\n\ntype TipHandle = ReturnType<typeof TooltipPrimitive.createHandle<string>>;\n\n/** Up / down within a grid keep the column; past its edge they land in the same column of the next / previous grid. */\nfunction verticalTarget(grids: HTMLButtonElement[][], g: number, j: number, down: boolean): HTMLButtonElement | \"search\" | undefined {\n const col = j % COLS;\n const cur = grids[g];\n const within = down ? j + COLS : j - COLS;\n if (within >= 0 && within < cur.length) return cur[within];\n if (down && Math.floor(j / COLS) < Math.floor((cur.length - 1) / COLS)) return cur[cur.length - 1]; // a short last row\n const next = grids[down ? g + 1 : g - 1];\n if (!next) return down ? undefined : \"search\";\n if (down) return next[Math.min(col, next.length - 1)];\n const lastRow = Math.floor((next.length - 1) / COLS) * COLS;\n return next[Math.min(lastRow + col, next.length - 1)];\n}\n\nfunction IconGrid({ items, selected, onPick, label, tip }: { items: GridItem[]; selected?: string; onPick: (key: string) => void; label: string; tip: TipHandle }) {\n const ref = React.useRef<HTMLDivElement>(null);\n const stop = items.some((i) => i.key === selected) ? selected : items[0]?.key;\n const move = (e: React.KeyboardEvent) => {\n // one keyboard space across every group in the picker (Recent, the set, search results)\n const scope = ref.current?.closest(\"[data-icon-picker]\") ?? ref.current;\n const grids = Array.from(scope?.querySelectorAll<HTMLElement>(\"[data-icon-grid]\") ?? []).map((g) => Array.from(g.querySelectorAll<HTMLButtonElement>(\"[data-grid-item]\")));\n const btns = grids.flat();\n const i = btns.indexOf(document.activeElement as HTMLButtonElement);\n if (i === -1) return;\n const g = grids.findIndex((list) => list.includes(btns[i]));\n const j = grids[g].indexOf(btns[i]);\n let to: HTMLButtonElement | \"search\" | undefined;\n if (e.key === \"ArrowRight\") to = btns[Math.min(i + 1, btns.length - 1)];\n else if (e.key === \"ArrowLeft\") to = btns[Math.max(i - 1, 0)];\n else if (e.key === \"Home\") to = btns[0];\n else if (e.key === \"End\") to = btns[btns.length - 1];\n else if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") to = verticalTarget(grids, g, j, e.key === \"ArrowDown\") ?? btns[i];\n else return;\n e.preventDefault();\n if (to === \"search\") scope?.querySelector<HTMLInputElement>(\"input[data-icon-search]\")?.focus();\n else to?.focus();\n };\n return (\n <div ref={ref} data-icon-grid className=\"grid gap-0.5\" style={{ gridTemplateColumns: `repeat(${COLS}, minmax(0, 1fr))` }} role=\"listbox\" aria-label={label} onKeyDown={move}>\n {items.map((it) => (\n // one shared tooltip for the whole picker (hundreds of icons): each trigger hands it its label\n <TooltipPrimitive.Trigger\n key={it.key}\n handle={tip}\n payload={it.label}\n delay={500}\n render={\n <button\n type=\"button\"\n role=\"option\"\n data-grid-item\n data-icon={it.key}\n aria-selected={it.key === selected}\n aria-label={it.label}\n tabIndex={it.key === stop ? 0 : -1}\n onClick={() => onPick(it.key)}\n className={cn(\n \"flex aspect-square items-center justify-center rounded-kit-md text-kit-fg-2 transition-colors hover:bg-kit-hover hover:text-kit-fg\",\n it.key === selected && \"bg-kit-selected text-kit-fg ring-1 ring-inset ring-kit-line-strong\",\n )}\n >\n {it.node}\n </button>\n }\n />\n ))}\n </div>\n );\n}\n\nconst NONE = \"__none\";\n\n/** The picker: search, Recent, then the whole set (\"No icon\" first). Search spans the name and its Tabler twin.\n * `field`: the icon field's popover (V2 IconFieldPicker, a 22rem list); otherwise the app-UI grid (V2 IconPickerGrid, max-h-56). */\nexport function IconPicker({ value, onPick, onChooseMedia, field }: { value?: string; onPick: (name: string | null) => void; onChooseMedia?: () => void; field?: boolean }) {\n const [q, setQ] = React.useState(\"\");\n const root = React.useRef<HTMLDivElement>(null);\n const tip = React.useMemo(() => TooltipPrimitive.createHandle<string>(), []);\n const needle = norm(q);\n const item = (name: string): GridItem => ({ key: name, label: iconLabel(name), node: <Glyph name={name} className=\"size-[18px]\" /> });\n const pick = (key: string) => onPick(key === NONE ? null : key);\n const hits = needle ? DOCK_ICON_NAMES.filter((n) => n.includes(needle) || tablerIconOf(n).includes(needle)) : [];\n const recent = recents.filter((n) => DOCK_ICON_NAME_SET.has(n));\n return (\n <div ref={root} data-icon-picker className=\"flex flex-col gap-2\">\n <TooltipPrimitive.Root handle={tip}>{({ payload }) => (payload ? <TooltipLayer>{payload}</TooltipLayer> : null)}</TooltipPrimitive.Root>\n <label className=\"flex h-8 items-center gap-2 rounded-kit-md bg-kit-muted px-2.5 text-kit-fg-3 focus-within:bg-kit-field-bg focus-within:ring-1 focus-within:ring-kit-line-strong\">\n <IconSearch className=\"size-4 shrink-0\" stroke={1.5} aria-hidden />\n <input\n autoFocus\n data-icon-search\n value={q}\n onChange={(e) => setQ(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n root.current?.querySelector<HTMLButtonElement>(\"[data-grid-item]\")?.focus();\n }\n }}\n placeholder=\"Search icons\"\n aria-label=\"Search icons\"\n className=\"h-full min-w-0 flex-1 bg-transparent text-kit-sm text-kit-fg outline-none placeholder:text-kit-fg-3\"\n />\n </label>\n <div className={field ? \"max-h-[22rem] overflow-y-auto pr-0.5\" : \"max-h-56 overflow-y-auto pr-0.5\"}>\n {needle ? (\n <div className=\"flex flex-col gap-2\">\n {hits.length ? (\n <Group>\n <IconGrid label=\"Icons\" selected={value} onPick={pick} tip={tip} items={hits.map(item)} />\n </Group>\n ) : (\n <p className=\"py-6 text-center text-kit-sm text-kit-fg-3\">No icons match “{q.trim()}”</p>\n )}\n </div>\n ) : (\n <div className=\"flex flex-col gap-2\">\n {recent.length ? (\n <Group title=\"Recent\">\n <IconGrid label=\"Recent icons\" selected={value} onPick={pick} tip={tip} items={recent.map(item)} />\n </Group>\n ) : null}\n <Group title={recent.length ? \"All icons\" : undefined}>\n <IconGrid\n label=\"Icons\"\n selected={value ?? NONE}\n onPick={pick}\n tip={tip}\n items={[{ key: NONE, label: \"No icon\", node: <IconX className=\"size-4\" stroke={1.5} aria-hidden /> }, ...DOCK_ICON_NAMES.map(item)]}\n />\n </Group>\n </div>\n )}\n </div>\n {onChooseMedia ? (\n <div className=\"flex items-center justify-between gap-2 border-t border-kit-line pt-2\">\n <span className=\"whitespace-nowrap text-kit-xs text-kit-fg-3\">Images keep their colours.</span>\n <Button size=\"xs\" variant=\"ghost\" leftIcon={<IconPhoto stroke={1.5} />} onClick={onChooseMedia}>\n Choose from media\n </Button>\n </div>\n ) : null}\n </div>\n );\n}\n\nfunction Group({ title, children }: { title?: string; children: React.ReactNode }) {\n return (\n <div className=\"flex flex-col gap-1\">\n {title ? <div className=\"px-0.5 text-kit-2xs font-medium text-kit-fg-3\">{title}</div> : null}\n {children}\n </div>\n );\n}\n\n/* ── The control ──────────────────────────────────────────────────────── */\nexport function IconControl({ field, value, onChange, readOnly, id, invalid }: ControlProps) {\n const { MediaPicker, mediaSrc } = useKitServices();\n const [open, setOpen] = React.useState(false);\n const [mediaOpen, setMediaOpen] = React.useState(false);\n const read = readIcon(value);\n\n const commit = (v: string | null) => {\n onChange(v);\n if (v && DOCK_ICON_NAME_SET.has(v)) pushRecent(v);\n setOpen(false);\n };\n\n const tile =\n read.kind === \"name\" ? (\n <Glyph name={read.name} className=\"size-4\" />\n ) : read.kind === \"upload\" ? (\n <img src={mediaSrc ? mediaSrc(read.url) : read.url} alt=\"\" className=\"size-4 object-contain\" />\n ) : read.kind === \"empty\" ? (\n <span className=\"size-3 rounded-kit-md border border-dashed border-[var(--kit-emphasis-line)]\" />\n ) : (\n <IconAlertTriangle className=\"size-3.5 text-kit-warning-fg\" stroke={1.5} aria-hidden />\n );\n const text =\n read.kind === \"name\"\n ? iconLabel(read.name)\n : read.kind === \"upload\"\n ? fileName(read.url)\n : read.kind === \"unknown\"\n ? `“${read.name}” isn't in the icon set`\n : read.kind === \"unreadable\"\n ? \"This value isn't an icon this picker can read\"\n : \"Choose an icon\";\n const warn = read.kind === \"unknown\" || read.kind === \"unreadable\";\n\n return (\n <>\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger\n render={\n <button\n id={id}\n type=\"button\"\n disabled={readOnly}\n aria-invalid={invalid || undefined}\n className={cn(\n \"flex h-8 w-full items-center gap-2.5 rounded-kit-md border border-kit-line-strong bg-kit-field-bg px-1 pr-2.5 text-left text-kit-sm transition-colors hover:border-kit-line-strong data-[popup-open]:border-kit-line-strong data-[popup-open]:bg-kit-field-bg\",\n invalid && \"border-[var(--kit-field-invalid)]\",\n )}\n />\n }\n >\n <span className=\"flex size-6 items-center justify-center rounded-kit-md border border-kit-line bg-kit-control-bg text-kit-fg\">{tile}</span>\n <span className={cn(\"min-w-0 flex-1 truncate\", warn ? \"text-kit-warning-fg\" : read.kind === \"empty\" ? \"text-kit-fg-3\" : \"text-kit-fg\")}>{text}</span>\n <IconChevronDown className=\"size-3.5 text-kit-fg-3\" stroke={1.5} aria-hidden />\n </PopoverTrigger>\n <PopoverContent className=\"w-[320px] p-2\" align=\"start\" aria-label={`${field.title}: choose an icon`}>\n <IconPicker\n field\n value={read.kind === \"name\" ? read.name : read.kind === \"empty\" ? undefined : \"\"}\n onPick={commit}\n onChooseMedia={\n MediaPicker\n ? () => {\n setOpen(false);\n setMediaOpen(true);\n }\n : undefined\n }\n />\n </PopoverContent>\n </Popover>\n {MediaPicker && mediaOpen ? (\n <MediaPicker open={mediaOpen} onOpenChange={setMediaOpen} accept={[\"svg\", \"image\"]} onPick={(assets) => assets[0]?.url && commit(assets[0].url)} />\n ) : null}\n </>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"icon.js","names":[],"sources":["../../src/controls/icon.tsx"],"sourcesContent":["/**\n * ICON — the icon field (DESIGN 8.14), V2 controls/icon.tsx fitted to what prod stores.\n *\n * Values (backend reference-validation `icon`, the classic PropsPanel IconValueInput):\n * \"arrow-right\" a name from the BetterCMS icon set (DOCK_ICON_NAMES, 473 names)\n * \"https://…/logo.svg\" an uploaded icon: an image URL (picked from the media library; a component prop\n * stores the URL string, like the classic panel's codec=\"url\")\n * { url, … } an uploaded icon stored as an image value: shown and kept until replaced\n * null no icon (\"No icon\"; the backend skips an empty value)\n *\n * The grid offers the BetterCMS set, the one prod accepts. Search also matches a name's Tabler twin through the\n * Tabler ↔ dock adapter (adapters/icons.ts), so \"map\" finds earth-americas. A stored name outside the set (a stale\n * Tabler or Lucide name) is shown with its reason and kept until the author picks another.\n *\n * Glyphs: the host draws them (KitServices.IconGlyph: the dashboard's DockIcon, the set the site renders). With no\n * host renderer the kit draws the Tabler twin, loaded on first use; a name with no Tabler twin shows its name.\n */\nimport * as React from \"react\";\nimport type { Icon as TablerIcon } from \"@tabler/icons-react\";\nimport { IconAlertTriangle, IconChevronDown, IconPhoto, IconSearch, IconX } from \"@tabler/icons-react\";\nimport { cn } from \"../lib/cn\";\nimport { isObj } from \"../lib/path\";\nimport { iconLabel } from \"../lib/icons\";\nimport { useKitServices } from \"../context\";\nimport { DOCK_ICON_NAMES, DOCK_ICON_NAME_SET } from \"../adapters/prod-types\";\nimport { tablerIconOf } from \"../adapters/icons\";\nimport type { ControlProps } from \"../types\";\nimport { Button } from \"../ui/button\";\nimport { Tooltip as TooltipPrimitive } from \"@base-ui/react/tooltip\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../ui/overlay\";\nimport { TooltipLayer } from \"../ui/tooltip-root\";\n\n/* ── Glyphs ───────────────────────────────────────────────────────────── */\ndeclare module \"../context\" {\n interface KitServices {\n /**\n * Draws one icon of the BetterCMS set by name, aria-hidden (the name beside it is the accessible text): the glyphs\n * the site renders. The dashboard passes `DockIcon` (src/components/dock/DockIcon). Absent → the Tabler twin.\n */\n IconGlyph?: React.ComponentType<{ name: string; className?: string }>;\n }\n}\n\nlet tablerSet: Record<string, unknown> | null = null;\nlet tablerLoad: Promise<void> | null = null;\nconst pascal = (kebab: string) => `Icon${kebab.split(\"-\").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(\"\")}`;\n\n/** The full Tabler set, loaded once and only when no host renderer is provided. */\nfunction useTablerSet(need: boolean): Record<string, unknown> | null {\n const [, landed] = React.useReducer((n: number) => n + 1, 0);\n React.useEffect(() => {\n if (!need || tablerSet) return;\n let live = true;\n tablerLoad ??= import(\"@tabler/icons-react\").then((m) => {\n tablerSet = m as unknown as Record<string, unknown>;\n });\n tablerLoad.then(() => live && landed(), () => {});\n return () => {\n live = false;\n };\n }, [need]);\n return tablerSet;\n}\n\nfunction Glyph({ name, className }: { name: string; className?: string }) {\n const Host = useKitServices().IconGlyph;\n const set = useTablerSet(!Host);\n if (Host) return <Host name={name} className={className} />;\n const T = set?.[pascal(tablerIconOf(name))] as TablerIcon | undefined;\n if (T) return <T className={className} stroke={1.5} aria-hidden />;\n // no Tabler twin (or the set is still loading): the name itself, never a stand-in shape\n return (\n <span aria-hidden className=\"max-w-full truncate px-0.5 text-[9px] leading-3 text-kit-fg-3\">\n {name}\n </span>\n );\n}\n\n/* ── Values ───────────────────────────────────────────────────────────── */\ntype Read =\n | { kind: \"empty\" }\n | { kind: \"name\"; name: string }\n | { kind: \"unknown\"; name: string }\n | { kind: \"upload\"; url: string }\n | { kind: \"unreadable\" };\nconst isUrlish = (s: string) => /[/.]/.test(s);\nfunction readIcon(v: unknown): Read {\n if (v == null || v === \"\") return { kind: \"empty\" };\n if (typeof v === \"string\") return DOCK_ICON_NAME_SET.has(v) ? { kind: \"name\", name: v } : isUrlish(v) ? { kind: \"upload\", url: v } : { kind: \"unknown\", name: v };\n if (isObj(v) && typeof v.url === \"string\" && v.url) return { kind: \"upload\", url: v.url };\n return { kind: \"unreadable\" };\n}\nconst fileName = (url: string) => decodeURIComponent(url.split(/[?#]/)[0].split(\"/\").pop() || url);\n\n/** Names picked this session, newest first (shown as \"Recent\"). */\nconst recents: string[] = [];\nconst pushRecent = (name: string) => {\n const i = recents.indexOf(name);\n if (i !== -1) recents.splice(i, 1);\n recents.unshift(name);\n recents.length = Math.min(recents.length, COLS);\n};\n\n/* ── The grid: one tab stop, arrow keys, Home / End ───────────────────── */\nconst COLS = 8;\nconst norm = (q: string) => q.toLowerCase().trim().replace(/\\s+/g, \"-\");\ninterface GridItem {\n key: string;\n label: string;\n node: React.ReactNode;\n}\n\ntype TipHandle = ReturnType<typeof TooltipPrimitive.createHandle<string>>;\n\n/** Up / down within a grid keep the column; past its edge they land in the same column of the next / previous grid. */\nfunction verticalTarget(grids: HTMLButtonElement[][], g: number, j: number, down: boolean): HTMLButtonElement | \"search\" | undefined {\n const col = j % COLS;\n const cur = grids[g];\n const within = down ? j + COLS : j - COLS;\n if (within >= 0 && within < cur.length) return cur[within];\n if (down && Math.floor(j / COLS) < Math.floor((cur.length - 1) / COLS)) return cur[cur.length - 1]; // a short last row\n const next = grids[down ? g + 1 : g - 1];\n if (!next) return down ? undefined : \"search\";\n if (down) return next[Math.min(col, next.length - 1)];\n const lastRow = Math.floor((next.length - 1) / COLS) * COLS;\n return next[Math.min(lastRow + col, next.length - 1)];\n}\n\nfunction IconGrid({ items, selected, onPick, label, tip }: { items: GridItem[]; selected?: string; onPick: (key: string) => void; label: string; tip: TipHandle }) {\n const ref = React.useRef<HTMLDivElement>(null);\n const stop = items.some((i) => i.key === selected) ? selected : items[0]?.key;\n const move = (e: React.KeyboardEvent) => {\n // one keyboard space across every group in the picker (Recent, the set, search results)\n const scope = ref.current?.closest(\"[data-icon-picker]\") ?? ref.current;\n const grids = Array.from(scope?.querySelectorAll<HTMLElement>(\"[data-icon-grid]\") ?? []).map((g) => Array.from(g.querySelectorAll<HTMLButtonElement>(\"[data-grid-item]\")));\n const btns = grids.flat();\n const i = btns.indexOf(document.activeElement as HTMLButtonElement);\n if (i === -1) return;\n const g = grids.findIndex((list) => list.includes(btns[i]));\n const j = grids[g].indexOf(btns[i]);\n let to: HTMLButtonElement | \"search\" | undefined;\n if (e.key === \"ArrowRight\") to = btns[Math.min(i + 1, btns.length - 1)];\n else if (e.key === \"ArrowLeft\") to = btns[Math.max(i - 1, 0)];\n else if (e.key === \"Home\") to = btns[0];\n else if (e.key === \"End\") to = btns[btns.length - 1];\n else if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") to = verticalTarget(grids, g, j, e.key === \"ArrowDown\") ?? btns[i];\n else return;\n e.preventDefault();\n if (to === \"search\") scope?.querySelector<HTMLInputElement>(\"input[data-icon-search]\")?.focus();\n else to?.focus();\n };\n return (\n <div ref={ref} data-icon-grid className=\"grid gap-0.5\" style={{ gridTemplateColumns: `repeat(${COLS}, minmax(0, 1fr))` }} role=\"listbox\" aria-label={label} onKeyDown={move}>\n {items.map((it) => (\n // one shared tooltip for the whole picker (hundreds of icons): each trigger hands it its label\n <TooltipPrimitive.Trigger\n key={it.key}\n handle={tip}\n payload={it.label}\n delay={500}\n render={\n <button\n type=\"button\"\n role=\"option\"\n data-grid-item\n data-icon={it.key}\n aria-selected={it.key === selected}\n aria-label={it.label}\n tabIndex={it.key === stop ? 0 : -1}\n onClick={() => onPick(it.key)}\n className={cn(\n \"flex aspect-square items-center justify-center rounded-kit-md text-kit-fg-2 transition-colors hover:bg-kit-hover hover:text-kit-fg\",\n it.key === selected && \"bg-kit-selected text-kit-fg ring-1 ring-inset ring-kit-line-strong\",\n )}\n >\n {it.node}\n </button>\n }\n />\n ))}\n </div>\n );\n}\n\nconst NONE = \"__none\";\n\n/** The picker: search, Recent, then the whole set (\"No icon\" first). Search spans the name and its Tabler twin.\n * `field`: the icon field's popover (V2 IconFieldPicker, a 22rem list); otherwise the app-UI grid (V2 IconPickerGrid, max-h-56). */\nexport function IconPicker({ value, onPick, onChooseMedia, field }: { value?: string; onPick: (name: string | null) => void; onChooseMedia?: () => void; field?: boolean }) {\n const [q, setQ] = React.useState(\"\");\n const root = React.useRef<HTMLDivElement>(null);\n const tip = React.useMemo(() => TooltipPrimitive.createHandle<string>(), []);\n const needle = norm(q);\n const item = (name: string): GridItem => ({ key: name, label: iconLabel(name), node: <Glyph name={name} className=\"size-[18px]\" /> });\n const pick = (key: string) => onPick(key === NONE ? null : key);\n const hits = needle ? DOCK_ICON_NAMES.filter((n) => n.includes(needle) || tablerIconOf(n).includes(needle)) : [];\n const recent = recents.filter((n) => DOCK_ICON_NAME_SET.has(n));\n return (\n <div ref={root} data-icon-picker className=\"flex flex-col gap-2\">\n <TooltipPrimitive.Root handle={tip}>{({ payload }) => (payload ? <TooltipLayer>{payload}</TooltipLayer> : null)}</TooltipPrimitive.Root>\n <label className=\"flex h-8 items-center gap-2 rounded-kit-md bg-kit-muted px-2.5 text-kit-fg-3 focus-within:bg-kit-field-bg focus-within:ring-1 focus-within:ring-kit-line-strong\">\n <IconSearch className=\"size-4 shrink-0\" stroke={1.5} aria-hidden />\n <input\n autoFocus\n data-icon-search\n value={q}\n onChange={(e) => setQ(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n root.current?.querySelector<HTMLButtonElement>(\"[data-grid-item]\")?.focus();\n }\n }}\n placeholder=\"Search icons\"\n aria-label=\"Search icons\"\n className=\"h-full min-w-0 flex-1 bg-transparent text-kit-sm text-kit-fg outline-none placeholder:text-kit-fg-3\"\n />\n </label>\n <div className={field ? \"max-h-[22rem] overflow-y-auto pr-0.5\" : \"max-h-56 overflow-y-auto pr-0.5\"}>\n {needle ? (\n <div className=\"flex flex-col gap-2\">\n {hits.length ? (\n <Group>\n <IconGrid label=\"Icons\" selected={value} onPick={pick} tip={tip} items={hits.map(item)} />\n </Group>\n ) : (\n <p className=\"py-6 text-center text-kit-sm text-kit-fg-3\">No icons match “{q.trim()}”</p>\n )}\n </div>\n ) : (\n <div className=\"flex flex-col gap-2\">\n {recent.length ? (\n <Group title=\"Recent\">\n <IconGrid label=\"Recent icons\" selected={value} onPick={pick} tip={tip} items={recent.map(item)} />\n </Group>\n ) : null}\n <Group title={recent.length ? \"All icons\" : undefined}>\n <IconGrid\n label=\"Icons\"\n selected={value ?? NONE}\n onPick={pick}\n tip={tip}\n items={[{ key: NONE, label: \"No icon\", node: <IconX className=\"size-4\" stroke={1.5} aria-hidden /> }, ...DOCK_ICON_NAMES.map(item)]}\n />\n </Group>\n </div>\n )}\n </div>\n {onChooseMedia ? (\n <div className=\"flex items-center justify-between gap-2 border-t border-kit-line pt-2\">\n <span className=\"whitespace-nowrap text-kit-xs text-kit-fg-3\">Images keep their colours.</span>\n <Button size=\"xs\" variant=\"ghost\" leftIcon={<IconPhoto stroke={1.5} />} onClick={onChooseMedia}>\n Choose from media\n </Button>\n </div>\n ) : null}\n </div>\n );\n}\n\nfunction Group({ title, children }: { title?: string; children: React.ReactNode }) {\n return (\n <div className=\"flex flex-col gap-1\">\n {title ? <div className=\"px-0.5 text-kit-2xs font-medium text-kit-fg-3\">{title}</div> : null}\n {children}\n </div>\n );\n}\n\n/* ── The control ──────────────────────────────────────────────────────── */\nexport function IconControl({ field, value, onChange, readOnly, id, invalid }: ControlProps) {\n const { MediaPicker, mediaSrc } = useKitServices();\n const [open, setOpen] = React.useState(false);\n const [mediaOpen, setMediaOpen] = React.useState(false);\n const read = readIcon(value);\n\n const commit = (v: string | null) => {\n onChange(v);\n if (v && DOCK_ICON_NAME_SET.has(v)) pushRecent(v);\n setOpen(false);\n };\n\n const tile =\n read.kind === \"name\" ? (\n <Glyph name={read.name} className=\"size-4\" />\n ) : read.kind === \"upload\" ? (\n <img src={mediaSrc ? mediaSrc(read.url) : read.url} alt=\"\" className=\"size-4 object-contain\" />\n ) : read.kind === \"empty\" ? (\n <span className=\"size-3 rounded-kit-md border border-dashed border-[var(--kit-emphasis-line)]\" />\n ) : (\n <IconAlertTriangle className=\"size-3.5 text-kit-warning-fg\" stroke={1.5} aria-hidden />\n );\n const text =\n read.kind === \"name\"\n ? iconLabel(read.name)\n : read.kind === \"upload\"\n ? fileName(read.url)\n : read.kind === \"unknown\"\n ? `“${read.name}” isn't in the icon set`\n : read.kind === \"unreadable\"\n ? \"This value isn't an icon this picker can read\"\n : \"Choose an icon\";\n const warn = read.kind === \"unknown\" || read.kind === \"unreadable\";\n\n return (\n <>\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger\n render={\n <button\n id={id}\n type=\"button\"\n disabled={readOnly}\n aria-invalid={invalid || undefined}\n className={cn(\n \"flex h-8 w-full items-center gap-2.5 rounded-kit-md border border-kit-line-strong bg-kit-field-bg px-1 pr-2.5 text-left text-kit-sm transition-colors hover:border-kit-line-strong data-[popup-open]:border-kit-line-strong data-[popup-open]:bg-kit-field-bg\",\n invalid && \"border-[var(--kit-field-invalid)]\",\n )}\n />\n }\n >\n <span className=\"flex size-6 items-center justify-center rounded-kit-md border border-kit-line bg-kit-control-bg text-kit-fg\">{tile}</span>\n <span className={cn(\"min-w-0 flex-1 truncate\", warn ? \"text-kit-warning-fg\" : read.kind === \"empty\" ? \"text-kit-fg-3\" : \"text-kit-fg\")}>{text}</span>\n <IconChevronDown className=\"size-3.5 text-kit-fg-3\" stroke={1.5} aria-hidden />\n </PopoverTrigger>\n <PopoverContent className=\"w-[320px] p-2\" align=\"start\" aria-label={`${field.title}: choose an icon`}>\n <IconPicker\n field\n value={read.kind === \"name\" ? read.name : read.kind === \"empty\" ? undefined : \"\"}\n onPick={commit}\n onChooseMedia={\n MediaPicker\n ? () => {\n setOpen(false);\n setMediaOpen(true);\n }\n : undefined\n }\n />\n </PopoverContent>\n </Popover>\n {MediaPicker && mediaOpen ? (\n <MediaPicker open={mediaOpen} onOpenChange={setMediaOpen} accept={[\"svg\", \"image\"]} onPick={(assets) => assets[0]?.url && commit(assets[0].url)} />\n ) : null}\n </>\n );\n}\n"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { prod_types_exports } from "./adapters/prod-types.js";
2
2
  import { buttonFromProd, buttonToProd, codeFromProd, codeToProd, cropPreset, cropRect, dateFromProd, dateToProd, datetimeFromProd, datetimeToProd, imageFromProd, imageToProd, isCropRect, isDockIcon, linkFromProd, linkToProd, listFromProd, listToProd, parseTargetTag, ratingFromProd, ratingToProd, referenceFromProd, referenceToProd, scaleFromProd, scaleToProd, tableFromProd, tableToProd, tagsFromProd, tagsToProd, targetTag, typedRowsFromProd, typedRowsToProd, videoFromProd, videoToProd, zoneListFromProd, zoneListToProd } from "./adapters/values.js";
3
- import { CATEGORY_TO_PROD, SHOPIFY_REF_LOCK, fromTypeSchema, leafValuePlan, planFromProd, planToProd, propsToTypeSchema, schemaSlugOf, toTypeSchema } from "./adapters/schema.js";
3
+ import { CATEGORY_TO_PROD, SHOPIFY_REF_LOCK, fromTypeSchema, leafValuePlan, planFromProd, planToProd, propsToTypeSchema, schemaSlugOf, toTypeSchema, variantFamilyFromTypeSchema, variantFamilyToTypeSchema, variantGroupV2 } from "./adapters/schema.js";
4
4
  import { cn } from "./lib/cn.js";
5
5
  import { AI_PRESETS, DEFAULT_ENV, FieldHeaderSlot, FormProvider, KitEnvProvider, KitSectionTitle, KitServicesProvider, SITE_PALETTE, fieldAccessOf, registerKitServices, resolveFieldAccess, seesDevSurfaces, useForm, useKitEnv, useKitServices, useRichTextRefs, visibleFields, visibleSchema } from "./context.js";
6
6
  import { FieldDescribedByContext, FieldLabelledByContext, useFieldDescribedBy, useFieldId, useFieldLabelledBy } from "./ui/field-context.js";
@@ -53,6 +53,6 @@ import "./kit.css";
53
53
  //#region src/index.ts
54
54
  registerKitControls();
55
55
  //#endregion
56
- export { ACTIONS, ACTION_WHEN_READ_ONLY, AGENT_PRESENCE, AI_PRESETS, ARRAY_CONTROLS, ActionTray, AnimatedFields, Appear, ArrayField, Autocomplete, Avatar, AvatarStack, Badge, BlockPicker, BreakpointGroup, Button, CAP_IDS, CATEGORY_TO_PROD, COALESCE_MS, CONTROLS, Card, CardHeader, Checkbox, ChoiceCard, ChoiceList, ChoiceSwitch, DEFAULT_ENV, DEV_FIELD, DEV_ONLY_FIELD, Dot, EmptyState, ExpandedEditorModal, FLOATING_LAYER, FallbackControl, Field, FieldDescribedByContext, FieldHeaderSlot, FieldInset, FieldLabelledByContext, FieldList, FieldSection, FieldShell, FieldUnit, FieldUnitRow, FieldsetSection, FlowBody, FlowClose, FlowFooter, FlowHeader, FlowLabel, FlowModal, FormProvider, FrameCrumbs, FrameHeader, FrameSlotsProvider, FrameStage, FrameView, GroupTabs, HELP_STRINGS, HISTORY_SEGMENT, HeaderStatus, HelpMore, Highlight, HintTooltip, HistoryView, HoverCard, HoverCardContent, HoverCardTrigger, IconButton, Input, InsetScope, IssueDot, ItemList, Kbd, Kbdish, KitBreadcrumb, KitEnvProvider, KitField, KitSectionTitle, KitServicesProvider, KitToaster, LABEL_HINT_AT, Label, LegacyFallback, Menu, MenuCheckItem, MenuContent, MenuGroup, MenuItem, MenuLabel, MenuRadioGroup, MenuRadioItem, MenuSeparator, MenuSub, MenuSubContent, MenuSubTrigger, MenuTrigger, Modal, ModalBody, ModalClose, ModalContent, ModalFooter, ModalHeader, ModalTrigger, ModeBar, ModeBarDot, ModeBarLink, MoreOptions, NavRow, NestedForm, OptionRow, OrderLockSlot, PageHeader, PeerStack, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, RHYTHM, Radio, RadioGroup, RevealLink, RowChrome, RowList, RowsHostContext, SEARCH_THRESHOLD, SEGMENTED_MAX, SEGMENTED_MAX_LABEL, SHOPIFY_REF_LOCK, SITE_PALETTE, SchemaForm, SearchInput, Segmented, Select, Separator, SettingsSection, SharedSortProvider, Sheet, SheetClose, SheetContent, SheetTrigger, ShortcutHint, ShortcutHintsProvider, Skeleton, Slider, SlotField, Sparkle, Spinner, Stat, StatusMark, StatusPill, Switch, Tab, TabList, TabPanel, Table, Tabs, Td, Textarea, Th, Thumb, ToggleGroup, ToggleItem, Tooltip, TooltipProvider, TooltipScope, Tr, TrayMeta, TrayMore, UNDO_STEPS, actionTrayClass, apiName, arrangeFields, arrayMove, avatarRingSpread, badgeVariants, blockDefToSchema, buttonFromProd, buttonToProd, buttonVariants, cn, codeFromProd, codeToProd, colorFor, controls_exports as controls, conventionalHelp, cropPreset, cropRect, crumbSignature, dateFromProd, dateToProd, datetimeFromProd, datetimeToProd, defaultGroup, fieldAccessOf, fieldDefToSchema, fieldDefsToSchema, filterOptions, fitsSegmented, frameContext, frameDirection, framesToCrumbs, fromTypeSchema, groupSummary, iconButtonVariants, imageFromProd, imageToProd, inferField, initials, isBreakpointName, isCropRect, isDockIcon, isDrillable, isShort, kitPathItems, kitToast, kitToastManager, leafValuePlan, linkFromProd, linkToProd, listFromProd, listToProd, matchPositions, matchScore, nodeText, parentOfField, parentTitleOf, parseTargetTag, plainOf, planFromProd, planToProd, presenceColor, presenceSlot, prod_types_exports as prod, propDefToField, propsToTypeSchema, ratingFromProd, ratingToProd, referenceFromProd, referenceToProd, registerKitControls, registerKitServices, registerPresenceRoster, relTime, resetFieldClipboard, resolveFieldAccess, resolveFrames, resolveHelp, rootFrame, rowToggleField, scaleFromProd, scaleToProd, schemaSlugOf, seesDevSurfaces, selectTriggerClass, shortcutHintsOn, statusMap, tableFromProd, tableToProd, tagsFromProd, tagsToProd, targetTag, titleFromKey, toTypeSchema, typedRowsFromProd, typedRowsToProd, useAvailableBlocks, useDragReorder, useDrill, useFieldAccess, useFieldDescribedBy, useFieldId, useFieldLabelledBy, useFocusReturn, useForm, useFrameNavigation, useFrameSlots, useGroupStatus, useInsetDepth, useKitEnv, useKitServices, useListOps, useRhythm, useRichTextRefs, useSharedSort, useShortcutHints, videoFromProd, videoToProd, visibleFields, visibleSchema, withDefaultHelp, zoneListFromProd, zoneListToProd };
56
+ export { ACTIONS, ACTION_WHEN_READ_ONLY, AGENT_PRESENCE, AI_PRESETS, ARRAY_CONTROLS, ActionTray, AnimatedFields, Appear, ArrayField, Autocomplete, Avatar, AvatarStack, Badge, BlockPicker, BreakpointGroup, Button, CAP_IDS, CATEGORY_TO_PROD, COALESCE_MS, CONTROLS, Card, CardHeader, Checkbox, ChoiceCard, ChoiceList, ChoiceSwitch, DEFAULT_ENV, DEV_FIELD, DEV_ONLY_FIELD, Dot, EmptyState, ExpandedEditorModal, FLOATING_LAYER, FallbackControl, Field, FieldDescribedByContext, FieldHeaderSlot, FieldInset, FieldLabelledByContext, FieldList, FieldSection, FieldShell, FieldUnit, FieldUnitRow, FieldsetSection, FlowBody, FlowClose, FlowFooter, FlowHeader, FlowLabel, FlowModal, FormProvider, FrameCrumbs, FrameHeader, FrameSlotsProvider, FrameStage, FrameView, GroupTabs, HELP_STRINGS, HISTORY_SEGMENT, HeaderStatus, HelpMore, Highlight, HintTooltip, HistoryView, HoverCard, HoverCardContent, HoverCardTrigger, IconButton, Input, InsetScope, IssueDot, ItemList, Kbd, Kbdish, KitBreadcrumb, KitEnvProvider, KitField, KitSectionTitle, KitServicesProvider, KitToaster, LABEL_HINT_AT, Label, LegacyFallback, Menu, MenuCheckItem, MenuContent, MenuGroup, MenuItem, MenuLabel, MenuRadioGroup, MenuRadioItem, MenuSeparator, MenuSub, MenuSubContent, MenuSubTrigger, MenuTrigger, Modal, ModalBody, ModalClose, ModalContent, ModalFooter, ModalHeader, ModalTrigger, ModeBar, ModeBarDot, ModeBarLink, MoreOptions, NavRow, NestedForm, OptionRow, OrderLockSlot, PageHeader, PeerStack, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, RHYTHM, Radio, RadioGroup, RevealLink, RowChrome, RowList, RowsHostContext, SEARCH_THRESHOLD, SEGMENTED_MAX, SEGMENTED_MAX_LABEL, SHOPIFY_REF_LOCK, SITE_PALETTE, SchemaForm, SearchInput, Segmented, Select, Separator, SettingsSection, SharedSortProvider, Sheet, SheetClose, SheetContent, SheetTrigger, ShortcutHint, ShortcutHintsProvider, Skeleton, Slider, SlotField, Sparkle, Spinner, Stat, StatusMark, StatusPill, Switch, Tab, TabList, TabPanel, Table, Tabs, Td, Textarea, Th, Thumb, ToggleGroup, ToggleItem, Tooltip, TooltipProvider, TooltipScope, Tr, TrayMeta, TrayMore, UNDO_STEPS, actionTrayClass, apiName, arrangeFields, arrayMove, avatarRingSpread, badgeVariants, blockDefToSchema, buttonFromProd, buttonToProd, buttonVariants, cn, codeFromProd, codeToProd, colorFor, controls_exports as controls, conventionalHelp, cropPreset, cropRect, crumbSignature, dateFromProd, dateToProd, datetimeFromProd, datetimeToProd, defaultGroup, fieldAccessOf, fieldDefToSchema, fieldDefsToSchema, filterOptions, fitsSegmented, frameContext, frameDirection, framesToCrumbs, fromTypeSchema, groupSummary, iconButtonVariants, imageFromProd, imageToProd, inferField, initials, isBreakpointName, isCropRect, isDockIcon, isDrillable, isShort, kitPathItems, kitToast, kitToastManager, leafValuePlan, linkFromProd, linkToProd, listFromProd, listToProd, matchPositions, matchScore, nodeText, parentOfField, parentTitleOf, parseTargetTag, plainOf, planFromProd, planToProd, presenceColor, presenceSlot, prod_types_exports as prod, propDefToField, propsToTypeSchema, ratingFromProd, ratingToProd, referenceFromProd, referenceToProd, registerKitControls, registerKitServices, registerPresenceRoster, relTime, resetFieldClipboard, resolveFieldAccess, resolveFrames, resolveHelp, rootFrame, rowToggleField, scaleFromProd, scaleToProd, schemaSlugOf, seesDevSurfaces, selectTriggerClass, shortcutHintsOn, statusMap, tableFromProd, tableToProd, tagsFromProd, tagsToProd, targetTag, titleFromKey, toTypeSchema, typedRowsFromProd, typedRowsToProd, useAvailableBlocks, useDragReorder, useDrill, useFieldAccess, useFieldDescribedBy, useFieldId, useFieldLabelledBy, useFocusReturn, useForm, useFrameNavigation, useFrameSlots, useGroupStatus, useInsetDepth, useKitEnv, useKitServices, useListOps, useRhythm, useRichTextRefs, useSharedSort, useShortcutHints, variantFamilyFromTypeSchema, variantFamilyToTypeSchema, variantGroupV2, videoFromProd, videoToProd, visibleFields, visibleSchema, withDefaultHelp, zoneListFromProd, zoneListToProd };
57
57
 
58
58
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,103 @@
1
+ //#region ledgers/exclusions.v1.json
2
+ var about = "What the V2 → prod mapping does not carry, and why that is no loss. `exclusions`: owner-signed losses (kit-facet-table.v3.md: row 12, row 9's Shopify subset, and B's two 'CATALOG not hit' rules); nothing goes here unsigned. `equivalences`: representation moves, where prod holds the same fact elsewhere; each names the recovery its proof checks (the dashboard's kitCorpusDock gate). A lost key the gate finds must match an exclusion or a proven equivalence, or it is a gap.";
3
+ var exclusions = [
4
+ {
5
+ "id": "shopify-reference",
6
+ "plane": "schema",
7
+ "feature": "shopify reference",
8
+ "reason": "A Shopify product reference: prod has no Shopify integration behind a reference yet (deferred:shopify); the backend refuses the target by name (reference_target_unsupported).",
9
+ "signedBy": "owner, G0a (kit-facet-table.v3.md row 12)"
10
+ },
11
+ {
12
+ "id": "reference-target-shopify",
13
+ "plane": "schema",
14
+ "feature": "reference target",
15
+ "pattern": "shopify",
16
+ "reason": "The Shopify subset of reference targets on any reference field (row 9): the rest of reference targets are stored natively (config.targets).",
17
+ "signedBy": "owner, G0a (kit-facet-table.v3.md row 9)"
18
+ },
19
+ {
20
+ "id": "reference_in_definition",
21
+ "plane": "schema",
22
+ "feature": "seeded instance",
23
+ "key": "reference_in_definition",
24
+ "reason": "A seeded instance's reference value in a component definition: deliberate backend rule; a definition names no entry, the placement does.",
25
+ "signedBy": "owner (kit-facet-table.v3.md B, 'CATALOG not hit')"
26
+ },
27
+ {
28
+ "id": "unique_prop_scope",
29
+ "plane": "schema",
30
+ "feature": "not applicable (props)",
31
+ "key": "unique_prop_scope",
32
+ "reason": "A uniqueness rule scoped to a component prop: deliberate backend rule; uniqueness is a collection's, not a placement's.",
33
+ "signedBy": "owner (kit-facet-table.v3.md B, 'CATALOG not hit')"
34
+ }
35
+ ];
36
+ var equivalences = [
37
+ {
38
+ "id": "variant",
39
+ "plane": "value",
40
+ "key": "_variant",
41
+ "moves": "V2's block variant is the component itself in prod: one component per variant sharing a sectionType; a slot child names its variant's component in _type.",
42
+ "recovery": "_variant is recovered from the component id: the family member the placement names is the V2 variant."
43
+ },
44
+ {
45
+ "id": "global",
46
+ "plane": "value",
47
+ "key": "_ref | _overrides",
48
+ "moves": "A global block instance's reference and override map are the placement itself in prod (componentId + overrides), not props.",
49
+ "recovery": "_ref / _overrides resolve to the placement value: the placement's component and its overrides are V2's _ref and _overrides."
50
+ },
51
+ {
52
+ "id": "asset",
53
+ "plane": "value",
54
+ "key": "image width | height",
55
+ "moves": "An image's pixel size is kept on the media asset, never in the value (values.ts imageToProd).",
56
+ "recovery": "width / height read back from the media asset (the kit's image control reads them through useMediaAsset)."
57
+ },
58
+ {
59
+ "id": "zone",
60
+ "plane": "value",
61
+ "key": "datetime [Zone]",
62
+ "moves": "A datetime on a field with no fixed zone is stored as the instant and its offset; V2's date control writes a [Zone] only for a fixed-zone field.",
63
+ "recovery": "A datetime without a zone round-trips to the same instant, displayed in the site time zone (the stated display rule)."
64
+ },
65
+ {
66
+ "id": "rel",
67
+ "plane": "value",
68
+ "key": "rel follow",
69
+ "moves": "\"follow\" is no rel: the backend's rel list is nofollow, sponsored and ugc.",
70
+ "recovery": "\"follow\" ⇔ no rel: the link and button controls show an unset rel as Do-follow and never write it."
71
+ },
72
+ {
73
+ "id": "item-open-mode",
74
+ "plane": "schema",
75
+ "feature": "item openMode",
76
+ "key": "of[0].openMode",
77
+ "moves": "A list of object items opens as the LIST says: prod keeps one openMode per list (the array field's, a table prop's), and V2's engine reads only that one.",
78
+ "recovery": "V2 itemOpenMode(list) = list.openMode ?? (one small object item ? inline : drill); the item's own openMode never contributes, so the stored list openMode yields the same mode.",
79
+ "proof": "bettercms-v2-demo-himanshu/src/components/kit/lib/schema.ts:42-46 (itemOpenMode)",
80
+ "ruledBy": "bettercms-ff 2026-10-24 (G-REMAIN B1)"
81
+ },
82
+ {
83
+ "id": "variant-group",
84
+ "plane": "schema",
85
+ "feature": "variants",
86
+ "key": "variants",
87
+ "moves": "A block's variants on the component side are its variant family: one component per V2 variant (sectionType = the block's title, name = the variant's, slug = the block's slug + '-' + the variant id) and a component_variant_groups row whose v2.variants lists the members in order with their description.",
88
+ "recovery": "variantFamilyToTypeSchema: the group's members in v2.variants order; each variant's id is the member slug after the group slug, its name the member's name, its description the meta's.",
89
+ "proof": "kit-roundtrip.test.ts props lane: each member through componentSchemas.create, the group's v2 through variantGroupV2Schema, read back in the stored order",
90
+ "carriedBy": "variantFamilyFromTypeSchema",
91
+ "ruledBy": "bettercms-ff 2026-10-24 (3c); bettercms-72 2026-10-24 (planner in the kit)"
92
+ }
93
+ ];
94
+ var exclusions_v1_default = {
95
+ about,
96
+ version: 1,
97
+ exclusions,
98
+ equivalences
99
+ };
100
+ //#endregion
101
+ export { about, exclusions_v1_default as default, equivalences, exclusions };
102
+
103
+ //# sourceMappingURL=exclusions.v1.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exclusions.v1.js","names":[],"sources":["../../ledgers/exclusions.v1.json"],"sourcesContent":["{\n \"about\": \"What the V2 → prod mapping does not carry, and why that is no loss. `exclusions`: owner-signed losses (kit-facet-table.v3.md: row 12, row 9's Shopify subset, and B's two 'CATALOG not hit' rules); nothing goes here unsigned. `equivalences`: representation moves, where prod holds the same fact elsewhere; each names the recovery its proof checks (the dashboard's kitCorpusDock gate). A lost key the gate finds must match an exclusion or a proven equivalence, or it is a gap.\",\n \"version\": 1,\n \"exclusions\": [\n {\n \"id\": \"shopify-reference\",\n \"plane\": \"schema\",\n \"feature\": \"shopify reference\",\n \"reason\": \"A Shopify product reference: prod has no Shopify integration behind a reference yet (deferred:shopify); the backend refuses the target by name (reference_target_unsupported).\",\n \"signedBy\": \"owner, G0a (kit-facet-table.v3.md row 12)\"\n },\n {\n \"id\": \"reference-target-shopify\",\n \"plane\": \"schema\",\n \"feature\": \"reference target\",\n \"pattern\": \"shopify\",\n \"reason\": \"The Shopify subset of reference targets on any reference field (row 9): the rest of reference targets are stored natively (config.targets).\",\n \"signedBy\": \"owner, G0a (kit-facet-table.v3.md row 9)\"\n },\n {\n \"id\": \"reference_in_definition\",\n \"plane\": \"schema\",\n \"feature\": \"seeded instance\",\n \"key\": \"reference_in_definition\",\n \"reason\": \"A seeded instance's reference value in a component definition: deliberate backend rule; a definition names no entry, the placement does.\",\n \"signedBy\": \"owner (kit-facet-table.v3.md B, 'CATALOG not hit')\"\n },\n {\n \"id\": \"unique_prop_scope\",\n \"plane\": \"schema\",\n \"feature\": \"not applicable (props)\",\n \"key\": \"unique_prop_scope\",\n \"reason\": \"A uniqueness rule scoped to a component prop: deliberate backend rule; uniqueness is a collection's, not a placement's.\",\n \"signedBy\": \"owner (kit-facet-table.v3.md B, 'CATALOG not hit')\"\n }\n ],\n \"equivalences\": [\n {\n \"id\": \"variant\",\n \"plane\": \"value\",\n \"key\": \"_variant\",\n \"moves\": \"V2's block variant is the component itself in prod: one component per variant sharing a sectionType; a slot child names its variant's component in _type.\",\n \"recovery\": \"_variant is recovered from the component id: the family member the placement names is the V2 variant.\"\n },\n {\n \"id\": \"global\",\n \"plane\": \"value\",\n \"key\": \"_ref | _overrides\",\n \"moves\": \"A global block instance's reference and override map are the placement itself in prod (componentId + overrides), not props.\",\n \"recovery\": \"_ref / _overrides resolve to the placement value: the placement's component and its overrides are V2's _ref and _overrides.\"\n },\n {\n \"id\": \"asset\",\n \"plane\": \"value\",\n \"key\": \"image width | height\",\n \"moves\": \"An image's pixel size is kept on the media asset, never in the value (values.ts imageToProd).\",\n \"recovery\": \"width / height read back from the media asset (the kit's image control reads them through useMediaAsset).\"\n },\n {\n \"id\": \"zone\",\n \"plane\": \"value\",\n \"key\": \"datetime [Zone]\",\n \"moves\": \"A datetime on a field with no fixed zone is stored as the instant and its offset; V2's date control writes a [Zone] only for a fixed-zone field.\",\n \"recovery\": \"A datetime without a zone round-trips to the same instant, displayed in the site time zone (the stated display rule).\"\n },\n {\n \"id\": \"rel\",\n \"plane\": \"value\",\n \"key\": \"rel follow\",\n \"moves\": \"\\\"follow\\\" is no rel: the backend's rel list is nofollow, sponsored and ugc.\",\n \"recovery\": \"\\\"follow\\\" ⇔ no rel: the link and button controls show an unset rel as Do-follow and never write it.\"\n },\n {\n \"id\": \"item-open-mode\",\n \"plane\": \"schema\",\n \"feature\": \"item openMode\",\n \"key\": \"of[0].openMode\",\n \"moves\": \"A list of object items opens as the LIST says: prod keeps one openMode per list (the array field's, a table prop's), and V2's engine reads only that one.\",\n \"recovery\": \"V2 itemOpenMode(list) = list.openMode ?? (one small object item ? inline : drill); the item's own openMode never contributes, so the stored list openMode yields the same mode.\",\n \"proof\": \"bettercms-v2-demo-himanshu/src/components/kit/lib/schema.ts:42-46 (itemOpenMode)\",\n \"ruledBy\": \"bettercms-ff 2026-10-24 (G-REMAIN B1)\"\n },\n {\n \"id\": \"variant-group\",\n \"plane\": \"schema\",\n \"feature\": \"variants\",\n \"key\": \"variants\",\n \"moves\": \"A block's variants on the component side are its variant family: one component per V2 variant (sectionType = the block's title, name = the variant's, slug = the block's slug + '-' + the variant id) and a component_variant_groups row whose v2.variants lists the members in order with their description.\",\n \"recovery\": \"variantFamilyToTypeSchema: the group's members in v2.variants order; each variant's id is the member slug after the group slug, its name the member's name, its description the meta's.\",\n \"proof\": \"kit-roundtrip.test.ts props lane: each member through componentSchemas.create, the group's v2 through variantGroupV2Schema, read back in the stored order\",\n \"carriedBy\": \"variantFamilyFromTypeSchema\",\n \"ruledBy\": \"bettercms-ff 2026-10-24 (3c); bettercms-72 2026-10-24 (planner in the kit)\"\n }\n ]\n}\n"],"mappings":""}
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Coverage matrix logic: what the library actually tests. Entries against edge cases, field types (how many
3
+ * fields of each) or schema features, plus the deepest drill-in per entry and the columns nothing (or too little)
4
+ * tests. The V2 /kit/coverage page computed this inline; it lives here so the page only draws it.
5
+ */
6
+ import type { SchemaType } from "../schema/types";
1
7
  import { type EntryStats } from "./analysis";
2
8
  import { type CatalogCategory, type CatalogItem } from "./catalog";
3
9
  export type CoverageView = "tags" | "types" | "features";
@@ -36,5 +42,11 @@ export interface Coverage {
36
42
  fields: number;
37
43
  deepest: number;
38
44
  };
45
+ /** kit-own extras (testing/coverage-extras), their own row: the types only they exercise, never counted as V2's */
46
+ extras: {
47
+ rows: CoverageRow[];
48
+ typesOnlyHere: SchemaType[];
49
+ typesUsedWithExtras: number;
50
+ };
39
51
  }
40
- export declare function coverageOf(view: CoverageView, withCore: boolean, catalog?: CatalogItem[]): Coverage;
52
+ export declare function coverageOf(view: CoverageView, withCore: boolean, catalog?: CatalogItem[], extras?: CatalogItem[]): Coverage;
@@ -7,7 +7,7 @@ function typeColumns(rows) {
7
7
  for (const r of rows) for (const t of Object.keys(r.s.types)) if (!TYPE_COLUMNS.includes(t)) extra.add(t);
8
8
  return [...TYPE_COLUMNS, ...[...extra].sort()];
9
9
  }
10
- function coverageOf(view, withCore, catalog = CATALOG) {
10
+ function coverageOf(view, withCore, catalog = CATALOG, extras = []) {
11
11
  const items = catalog.filter((c) => withCore || !c.core);
12
12
  const rows = items.map((it) => ({
13
13
  it,
@@ -35,6 +35,12 @@ function coverageOf(view, withCore, catalog = CATALOG) {
35
35
  count: totals[i]
36
36
  })).filter((x) => x.count < bar);
37
37
  const categories = categoriesOf(items);
38
+ const extraRows = extras.map((it) => ({
39
+ it,
40
+ s: statsOf(it, blocksFor(it).resolve)
41
+ }));
42
+ const used = (rs, t) => rs.some((r) => (r.s.types[t] ?? 0) > 0);
43
+ const allTypes = typeColumns([...rows, ...extraRows]);
38
44
  return {
39
45
  rows,
40
46
  columns,
@@ -50,6 +56,11 @@ function coverageOf(view, withCore, catalog = CATALOG) {
50
56
  typesTotal: types.length,
51
57
  fields: rows.reduce((n, r) => n + r.s.fields, 0),
52
58
  deepest: rows.reduce((m, r) => Math.max(m, r.s.depth), 0)
59
+ },
60
+ extras: {
61
+ rows: extraRows,
62
+ typesOnlyHere: allTypes.filter((t) => used(extraRows, t) && !used(rows, t)),
63
+ typesUsedWithExtras: allTypes.filter((t) => used(rows, t) || used(extraRows, t)).length
53
64
  }
54
65
  };
55
66
  }
@@ -1 +1 @@
1
- {"version":3,"file":"coverage.js","names":[],"sources":["../../src/library/coverage.ts"],"sourcesContent":["/**\n * Coverage matrix logic: what the library actually tests. Entries against edge cases, field types (how many\n * fields of each) or schema features, plus the deepest drill-in per entry and the columns nothing (or too little)\n * tests. The V2 /kit/coverage page computed this inline; it lives here so the page only draws it.\n */\nimport type { SchemaType } from \"../schema/types\";\nimport { FEATURE_COLUMNS, statsOf, TYPE_COLUMNS, type EntryStats } from \"./analysis\";\nimport { blocksFor, CATALOG, categoriesOf, EDGE_TAGS, type CatalogCategory, type CatalogItem } from \"./catalog\";\n\nexport type CoverageView = \"tags\" | \"types\" | \"features\";\n\nexport interface CoverageColumn {\n key: string;\n label: string;\n hint?: string;\n /** 0 when the entry does not test it; a count for field types, 1 otherwise */\n get: (it: CatalogItem, s: EntryStats) => number;\n}\n\nexport interface CoverageRow {\n it: CatalogItem;\n s: EntryStats;\n}\n\nexport interface Coverage {\n rows: CoverageRow[];\n columns: CoverageColumn[];\n /** per column: how many entries test it */\n totals: number[];\n /** columns under the bar: fewer than 3 entries for edge cases, none for field types; never flagged for features */\n thin: { column: CoverageColumn; count: number }[];\n categories: { category: CatalogCategory; count: number }[];\n facts: { sections: number; categories: number; tagsCovered: number; tagsTotal: number; typesUsed: number; typesTotal: number; fields: number; deepest: number };\n}\n\n/** Field-type columns: the fixed reading order, then any type the schema language grows later, sorted. */\nfunction typeColumns(rows: CoverageRow[]): SchemaType[] {\n const extra = new Set<string>();\n for (const r of rows) for (const t of Object.keys(r.s.types)) if (!(TYPE_COLUMNS as string[]).includes(t)) extra.add(t);\n return [...TYPE_COLUMNS, ...([...extra].sort() as SchemaType[])];\n}\n\nexport function coverageOf(view: CoverageView, withCore: boolean, catalog: CatalogItem[] = CATALOG): Coverage {\n const items = catalog.filter((c) => withCore || !c.core);\n const rows = items.map((it) => ({ it, s: statsOf(it, blocksFor(it).resolve) }));\n const types = typeColumns(rows);\n const columns: CoverageColumn[] =\n view === \"tags\"\n ? EDGE_TAGS.map((t) => ({ key: t.tag, label: t.label, hint: t.hint, get: (it) => (it.tags.includes(t.tag) ? 1 : 0) }))\n : view === \"types\"\n ? types.map((t) => ({ key: t, label: t, get: (_it, s) => s.types[t] ?? 0 }))\n : FEATURE_COLUMNS.map((f) => ({ key: f.key, label: f.label, get: (_it, s) => (s.features[f.key] ? 1 : 0) }));\n const totals = columns.map((c) => rows.reduce((n, r) => n + (c.get(r.it, r.s) > 0 ? 1 : 0), 0));\n const bar = view === \"tags\" ? 3 : 1;\n const thin = view === \"features\" ? [] : columns.map((column, i) => ({ column, count: totals[i] })).filter((x) => x.count < bar);\n const categories = categoriesOf(items);\n return {\n rows,\n columns,\n totals,\n thin,\n categories,\n facts: {\n sections: rows.length,\n categories: categories.length,\n tagsCovered: EDGE_TAGS.filter((t) => rows.some((r) => r.it.tags.includes(t.tag))).length,\n tagsTotal: EDGE_TAGS.length,\n typesUsed: types.filter((t) => rows.some((r) => (r.s.types[t] ?? 0) > 0)).length,\n typesTotal: types.length,\n fields: rows.reduce((n, r) => n + r.s.fields, 0),\n deepest: rows.reduce((m, r) => Math.max(m, r.s.depth), 0),\n },\n };\n}\n"],"mappings":";;;;AAoCA,SAAS,YAAY,MAAmC;CACtD,MAAM,wBAAQ,IAAI,IAAY;CAC9B,KAAK,MAAM,KAAK,MAAM,KAAK,MAAM,KAAK,OAAO,KAAK,EAAE,EAAE,KAAK,GAAG,IAAI,CAAE,aAA0B,SAAS,CAAC,GAAG,MAAM,IAAI,CAAC;CACtH,OAAO,CAAC,GAAG,cAAc,GAAI,CAAC,GAAG,KAAK,CAAC,CAAC,KAAK,CAAkB;AACjE;AAEA,SAAgB,WAAW,MAAoB,UAAmB,UAAyB,SAAmB;CAC5G,MAAM,QAAQ,QAAQ,QAAQ,MAAM,YAAY,CAAC,EAAE,IAAI;CACvD,MAAM,OAAO,MAAM,KAAK,QAAQ;EAAE;EAAI,GAAG,QAAQ,IAAI,UAAU,EAAE,CAAC,CAAC,OAAO;CAAE,EAAE;CAC9E,MAAM,QAAQ,YAAY,IAAI;CAC9B,MAAM,UACJ,SAAS,SACL,UAAU,KAAK,OAAO;EAAE,KAAK,EAAE;EAAK,OAAO,EAAE;EAAO,MAAM,EAAE;EAAM,MAAM,OAAQ,GAAG,KAAK,SAAS,EAAE,GAAG,IAAI,IAAI;CAAG,EAAE,IACnH,SAAS,UACP,MAAM,KAAK,OAAO;EAAE,KAAK;EAAG,OAAO;EAAG,MAAM,KAAK,MAAM,EAAE,MAAM,MAAM;CAAE,EAAE,IACzE,gBAAgB,KAAK,OAAO;EAAE,KAAK,EAAE;EAAK,OAAO,EAAE;EAAO,MAAM,KAAK,MAAO,EAAE,SAAS,EAAE,OAAO,IAAI;CAAG,EAAE;CACjH,MAAM,SAAS,QAAQ,KAAK,MAAM,KAAK,QAAQ,GAAG,MAAM,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,IAAI,IAAI,IAAI,IAAI,CAAC,CAAC;CAC9F,MAAM,MAAM,SAAS,SAAS,IAAI;CAClC,MAAM,OAAO,SAAS,aAAa,CAAC,IAAI,QAAQ,KAAK,QAAQ,OAAO;EAAE;EAAQ,OAAO,OAAO;CAAG,EAAE,CAAC,CAAC,QAAQ,MAAM,EAAE,QAAQ,GAAG;CAC9H,MAAM,aAAa,aAAa,KAAK;CACrC,OAAO;EACL;EACA;EACA;EACA;EACA;EACA,OAAO;GACL,UAAU,KAAK;GACf,YAAY,WAAW;GACvB,aAAa,UAAU,QAAQ,MAAM,KAAK,MAAM,MAAM,EAAE,GAAG,KAAK,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;GAClF,WAAW,UAAU;GACrB,WAAW,MAAM,QAAQ,MAAM,KAAK,MAAM,OAAO,EAAE,EAAE,MAAM,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC;GAC1E,YAAY,MAAM;GAClB,QAAQ,KAAK,QAAQ,GAAG,MAAM,IAAI,EAAE,EAAE,QAAQ,CAAC;GAC/C,SAAS,KAAK,QAAQ,GAAG,MAAM,KAAK,IAAI,GAAG,EAAE,EAAE,KAAK,GAAG,CAAC;EAC1D;CACF;AACF"}
1
+ {"version":3,"file":"coverage.js","names":[],"sources":["../../src/library/coverage.ts"],"sourcesContent":["/**\n * Coverage matrix logic: what the library actually tests. Entries against edge cases, field types (how many\n * fields of each) or schema features, plus the deepest drill-in per entry and the columns nothing (or too little)\n * tests. The V2 /kit/coverage page computed this inline; it lives here so the page only draws it.\n */\nimport type { SchemaType } from \"../schema/types\";\nimport { FEATURE_COLUMNS, statsOf, TYPE_COLUMNS, type EntryStats } from \"./analysis\";\nimport { blocksFor, CATALOG, categoriesOf, EDGE_TAGS, type CatalogCategory, type CatalogItem } from \"./catalog\";\n\nexport type CoverageView = \"tags\" | \"types\" | \"features\";\n\nexport interface CoverageColumn {\n key: string;\n label: string;\n hint?: string;\n /** 0 when the entry does not test it; a count for field types, 1 otherwise */\n get: (it: CatalogItem, s: EntryStats) => number;\n}\n\nexport interface CoverageRow {\n it: CatalogItem;\n s: EntryStats;\n}\n\nexport interface Coverage {\n rows: CoverageRow[];\n columns: CoverageColumn[];\n /** per column: how many entries test it */\n totals: number[];\n /** columns under the bar: fewer than 3 entries for edge cases, none for field types; never flagged for features */\n thin: { column: CoverageColumn; count: number }[];\n categories: { category: CatalogCategory; count: number }[];\n facts: { sections: number; categories: number; tagsCovered: number; tagsTotal: number; typesUsed: number; typesTotal: number; fields: number; deepest: number };\n /** kit-own extras (testing/coverage-extras), their own row: the types only they exercise, never counted as V2's */\n extras: { rows: CoverageRow[]; typesOnlyHere: SchemaType[]; typesUsedWithExtras: number };\n}\n\n/** Field-type columns: the fixed reading order, then any type the schema language grows later, sorted. */\nfunction typeColumns(rows: CoverageRow[]): SchemaType[] {\n const extra = new Set<string>();\n for (const r of rows) for (const t of Object.keys(r.s.types)) if (!(TYPE_COLUMNS as string[]).includes(t)) extra.add(t);\n return [...TYPE_COLUMNS, ...([...extra].sort() as SchemaType[])];\n}\n\nexport function coverageOf(view: CoverageView, withCore: boolean, catalog: CatalogItem[] = CATALOG, extras: CatalogItem[] = []): Coverage {\n const items = catalog.filter((c) => withCore || !c.core);\n const rows = items.map((it) => ({ it, s: statsOf(it, blocksFor(it).resolve) }));\n const types = typeColumns(rows);\n const columns: CoverageColumn[] =\n view === \"tags\"\n ? EDGE_TAGS.map((t) => ({ key: t.tag, label: t.label, hint: t.hint, get: (it) => (it.tags.includes(t.tag) ? 1 : 0) }))\n : view === \"types\"\n ? types.map((t) => ({ key: t, label: t, get: (_it, s) => s.types[t] ?? 0 }))\n : FEATURE_COLUMNS.map((f) => ({ key: f.key, label: f.label, get: (_it, s) => (s.features[f.key] ? 1 : 0) }));\n const totals = columns.map((c) => rows.reduce((n, r) => n + (c.get(r.it, r.s) > 0 ? 1 : 0), 0));\n const bar = view === \"tags\" ? 3 : 1;\n const thin = view === \"features\" ? [] : columns.map((column, i) => ({ column, count: totals[i] })).filter((x) => x.count < bar);\n const categories = categoriesOf(items);\n const extraRows = extras.map((it) => ({ it, s: statsOf(it, blocksFor(it).resolve) }));\n const used = (rs: CoverageRow[], t: SchemaType) => rs.some((r) => (r.s.types[t] ?? 0) > 0);\n const allTypes = typeColumns([...rows, ...extraRows]);\n return {\n rows,\n columns,\n totals,\n thin,\n categories,\n facts: {\n sections: rows.length,\n categories: categories.length,\n tagsCovered: EDGE_TAGS.filter((t) => rows.some((r) => r.it.tags.includes(t.tag))).length,\n tagsTotal: EDGE_TAGS.length,\n typesUsed: types.filter((t) => rows.some((r) => (r.s.types[t] ?? 0) > 0)).length,\n typesTotal: types.length,\n fields: rows.reduce((n, r) => n + r.s.fields, 0),\n deepest: rows.reduce((m, r) => Math.max(m, r.s.depth), 0),\n },\n extras: {\n rows: extraRows,\n typesOnlyHere: allTypes.filter((t) => used(extraRows, t) && !used(rows, t)),\n typesUsedWithExtras: allTypes.filter((t) => used(rows, t) || used(extraRows, t)).length,\n },\n };\n}\n"],"mappings":";;;;AAsCA,SAAS,YAAY,MAAmC;CACtD,MAAM,wBAAQ,IAAI,IAAY;CAC9B,KAAK,MAAM,KAAK,MAAM,KAAK,MAAM,KAAK,OAAO,KAAK,EAAE,EAAE,KAAK,GAAG,IAAI,CAAE,aAA0B,SAAS,CAAC,GAAG,MAAM,IAAI,CAAC;CACtH,OAAO,CAAC,GAAG,cAAc,GAAI,CAAC,GAAG,KAAK,CAAC,CAAC,KAAK,CAAkB;AACjE;AAEA,SAAgB,WAAW,MAAoB,UAAmB,UAAyB,SAAS,SAAwB,CAAC,GAAa;CACxI,MAAM,QAAQ,QAAQ,QAAQ,MAAM,YAAY,CAAC,EAAE,IAAI;CACvD,MAAM,OAAO,MAAM,KAAK,QAAQ;EAAE;EAAI,GAAG,QAAQ,IAAI,UAAU,EAAE,CAAC,CAAC,OAAO;CAAE,EAAE;CAC9E,MAAM,QAAQ,YAAY,IAAI;CAC9B,MAAM,UACJ,SAAS,SACL,UAAU,KAAK,OAAO;EAAE,KAAK,EAAE;EAAK,OAAO,EAAE;EAAO,MAAM,EAAE;EAAM,MAAM,OAAQ,GAAG,KAAK,SAAS,EAAE,GAAG,IAAI,IAAI;CAAG,EAAE,IACnH,SAAS,UACP,MAAM,KAAK,OAAO;EAAE,KAAK;EAAG,OAAO;EAAG,MAAM,KAAK,MAAM,EAAE,MAAM,MAAM;CAAE,EAAE,IACzE,gBAAgB,KAAK,OAAO;EAAE,KAAK,EAAE;EAAK,OAAO,EAAE;EAAO,MAAM,KAAK,MAAO,EAAE,SAAS,EAAE,OAAO,IAAI;CAAG,EAAE;CACjH,MAAM,SAAS,QAAQ,KAAK,MAAM,KAAK,QAAQ,GAAG,MAAM,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,IAAI,IAAI,IAAI,IAAI,CAAC,CAAC;CAC9F,MAAM,MAAM,SAAS,SAAS,IAAI;CAClC,MAAM,OAAO,SAAS,aAAa,CAAC,IAAI,QAAQ,KAAK,QAAQ,OAAO;EAAE;EAAQ,OAAO,OAAO;CAAG,EAAE,CAAC,CAAC,QAAQ,MAAM,EAAE,QAAQ,GAAG;CAC9H,MAAM,aAAa,aAAa,KAAK;CACrC,MAAM,YAAY,OAAO,KAAK,QAAQ;EAAE;EAAI,GAAG,QAAQ,IAAI,UAAU,EAAE,CAAC,CAAC,OAAO;CAAE,EAAE;CACpF,MAAM,QAAQ,IAAmB,MAAkB,GAAG,MAAM,OAAO,EAAE,EAAE,MAAM,MAAM,KAAK,CAAC;CACzF,MAAM,WAAW,YAAY,CAAC,GAAG,MAAM,GAAG,SAAS,CAAC;CACpD,OAAO;EACL;EACA;EACA;EACA;EACA;EACA,OAAO;GACL,UAAU,KAAK;GACf,YAAY,WAAW;GACvB,aAAa,UAAU,QAAQ,MAAM,KAAK,MAAM,MAAM,EAAE,GAAG,KAAK,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;GAClF,WAAW,UAAU;GACrB,WAAW,MAAM,QAAQ,MAAM,KAAK,MAAM,OAAO,EAAE,EAAE,MAAM,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC;GAC1E,YAAY,MAAM;GAClB,QAAQ,KAAK,QAAQ,GAAG,MAAM,IAAI,EAAE,EAAE,QAAQ,CAAC;GAC/C,SAAS,KAAK,QAAQ,GAAG,MAAM,KAAK,IAAI,GAAG,EAAE,EAAE,KAAK,GAAG,CAAC;EAC1D;EACA,QAAQ;GACN,MAAM;GACN,eAAe,SAAS,QAAQ,MAAM,KAAK,WAAW,CAAC,KAAK,CAAC,KAAK,MAAM,CAAC,CAAC;GAC1E,qBAAqB,SAAS,QAAQ,MAAM,KAAK,MAAM,CAAC,KAAK,KAAK,WAAW,CAAC,CAAC,CAAC,CAAC;EACnF;CACF;AACF"}