@bettercms-ai/kit 2.0.0-next.1 → 2.0.0-next.2
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/dist/adapters/lossy.d.ts +4 -0
- package/dist/adapters/lossy.js +8 -2
- package/dist/adapters/lossy.js.map +1 -1
- package/dist/adapters/schema.js +28 -0
- package/dist/adapters/schema.js.map +1 -1
- package/dist/controls/rich-text/blocks.d.ts +6 -0
- package/dist/controls/rich-text/blocks.js +187 -0
- package/dist/controls/rich-text/blocks.js.map +1 -1
- package/dist/controls/rich-text/catalog.d.ts +2 -5
- package/dist/controls/rich-text/catalog.js +104 -6
- package/dist/controls/rich-text/catalog.js.map +1 -1
- package/dist/controls/rich-text/codec.d.ts +10 -1
- package/dist/controls/rich-text/codec.js +7 -0
- package/dist/controls/rich-text/codec.js.map +1 -1
- package/dist/controls/rich-text/control.js +1 -0
- package/dist/controls/rich-text/control.js.map +1 -1
- package/dist/controls/rich-text/dom.d.ts +79 -0
- package/dist/controls/rich-text/dom.js +159 -7
- package/dist/controls/rich-text/dom.js.map +1 -1
- package/dist/controls/rich-text/editor.d.ts +4 -2
- package/dist/controls/rich-text/editor.js +93 -3
- package/dist/controls/rich-text/editor.js.map +1 -1
- package/dist/controls/rich-text/ops.js +6 -0
- package/dist/controls/rich-text/ops.js.map +1 -1
- package/dist/controls/rich-text/toc.d.ts +3 -2
- package/dist/controls/rich-text/toc.js +1 -2
- package/dist/controls/rich-text/toc.js.map +1 -1
- package/dist/core/core.d.ts +1 -0
- package/dist/core/core.js +2 -0
- package/dist/core/core.js.map +1 -1
- package/dist/core/facets.d.ts +8 -0
- package/dist/core/facets.js +8 -0
- package/dist/core/facets.js.map +1 -1
- package/dist/library/kit-catalog.json +4 -2
- package/dist/schema/types.d.ts +8 -0
- package/dist/schema/types.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog.js","names":[],"sources":["../../../src/controls/rich-text/catalog.tsx"],"sourcesContent":["/**\n * RICH TEXT · the block catalogue shared by the toolbar Insert menu, the gutter \"+\" and the \"/\" slash menu (V2 HEAD\n * controls/rich-text/catalog.tsx, CPO D55).\n *\n * browse Text · Media · Data, grouped, then one quiet \"More components N\" row that opens the components the\n * field may embed (V2 HEAD catalog.tsx MORE), with its own search and a way back (\"All blocks\")\n * search every block and every embeddable component in one list: recently used first among the strong matches,\n * then by match (label, aliases, then description)\n * no match \"No match.\"\n *\n * Only the blocks the field accepts are offered (`features`, from the host's codec: each block is offered only where\n * the field's stored shape has a place for it). Not in this catalogue, and why:\n * Footnote, Gallery, Embed, Columns, Table of contents, Related posts, Author bio\n * production's rich text has no stored block for them yet (plan RT3–RT8; production's embed\n * frames the raw link, which providers refuse, so Video is the one media link offered). A stored embed (an\n * imported `<iframe>`, whose address is already the provider's frame) opens and is edited as V2's Embed block.\n *\n * Components come from the host (KitServices.useInsertableComponents: the project's components, narrowed to the ones\n * the field's config accepts), handed in through CatalogComponentsProvider. Without that service none is offered.\n * Deviation from V2: the library has no inline / page-section placement, so every component sits in one\n * \"Components\" group (V2: \"Article\" and \"Page sections\"), and no live preview flyout is drawn beside a row.\n */\nimport * as React from \"react\";\nimport {\n IconClick, IconListCheck,\n IconArrowLeft, IconBlockquote, IconChevronRight, IconComponents, IconFileText, IconH1, IconH2, IconH3, IconH4, IconH5, IconH6, IconInfoCircle, IconLayoutNavbarCollapse, IconList, IconListDetails, IconListNumbers, IconPhoto,\n IconPilcrow, IconQuote, IconSearch, IconSeparatorHorizontal, IconTable, IconTerminal2, IconVideo,\n} from \"@tabler/icons-react\";\nimport { cn } from \"../../lib/cn\";\nimport { PopoverContent } from \"../../ui/overlay\";\nimport type { RichTextFeature } from \"./codec\";\nimport type { KitInsertableComponent } from \"../component-services\";\n\nexport type BasicId =\n | \"p\" | \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\" | \"quote\" | \"pullquote\" | \"callout\" | \"toggle\" | \"takeaways\" | \"ul\" | \"ol\" | \"code\" | \"hr\"\n | \"image\" | \"video\" | \"file\" | \"table\" | \"task\" | \"button\";\n\nexport interface CatalogItem {\n key: string;\n /** a basic block (absent on a component, the More components row and the way back) */\n id?: BasicId;\n /** component: embeds `component`; folder: opens More components; back: returns to every block */\n kind?: \"component\" | \"folder\" | \"back\";\n component?: KitInsertableComponent;\n /** the folder row's component count */\n count?: number;\n label: string;\n description: string;\n icon: React.ReactNode;\n /** extra words that find it */\n aliases?: string[];\n}\n\nconst i = (I: React.ComponentType<{ stroke?: number }>) => <I stroke={1.5} />;\ntype Row = [BasicId, string, string, React.ReactNode, string[]?];\nconst rows = (list: Row[]): CatalogItem[] => list.map(([id, label, description, icon, aliases]) => ({ key: id, id, label, description, icon, aliases }));\n\nconst TEXT = rows([\n [\"p\", \"Text\", \"Plain body text\", i(IconPilcrow), [\"paragraph\", \"body\"]],\n [\"h2\", \"Heading 2\", \"Section heading\", i(IconH2), [\"title\", \"subtitle\"]],\n [\"h3\", \"Heading 3\", \"Subsection heading\", i(IconH3)],\n [\"h4\", \"Heading 4\", \"Minor heading\", i(IconH4)],\n [\"quote\", \"Quote\", \"A quoted passage\", i(IconBlockquote), [\"blockquote\"]],\n [\"pullquote\", \"Pull quote\", \"A highlighted line with who said it\", i(IconQuote), [\"pq\", \"testimonial\", \"attribution\"]],\n [\"ul\", \"List\", \"Bulleted list\", i(IconList), [\"bullets\", \"bulleted list\", \"unordered\"]],\n [\"ol\", \"Numbered list\", \"Ordered steps\", i(IconListNumbers), [\"ordered\", \"steps\"]],\n [\"toggle\", \"Toggle\", \"Collapsible title and body\", i(IconLayoutNavbarCollapse), [\"accordion\", \"details\", \"collapse\"]],\n [\"callout\", \"Callout\", \"A highlighted note with a tone\", i(IconInfoCircle), [\"note\", \"tip\", \"warning\", \"aside\"]],\n [\"takeaways\", \"Key takeaways\", \"The story in a few points\", i(IconListDetails), [\"summary\", \"tldr\", \"tl;dr\", \"highlights\"]],\n [\"hr\", \"Divider\", \"A line between parts\", i(IconSeparatorHorizontal), [\"line\", \"separator\", \"rule\"]],\n [\"code\", \"Code\", \"Monospaced snippet\", i(IconTerminal2), [\"code block\", \"snippet\", \"pre\"]],\n]);\nconst MEDIA = rows([\n [\"image\", \"Image\", \"Image with alt text and caption\", i(IconPhoto), [\"img\", \"picture\", \"photo\"]],\n [\"video\", \"Video\", \"A YouTube, Vimeo or video file link\", i(IconVideo), [\"mp4\", \"youtube\", \"yt\", \"vimeo\", \"clip\", \"movie\"]],\n [\"file\", \"File\", \"A downloadable PDF or file\", i(IconFileText), [\"pdf\", \"download\", \"attachment\"]],\n]);\nconst DATA = rows([[\"table\", \"Table\", \"Rows and columns\", i(IconTable), [\"grid\", \"data\", \"comparison\", \"spreadsheet\"]]]);\n/** Found by search, not listed: rare in a body (H1 is the page title). */\nconst SEARCH_ONLY = rows([\n [\"h1\", \"Heading 1\", \"Largest heading\", i(IconH1)],\n [\"h5\", \"Heading 5\", \"Smallest heading\", i(IconH5)],\n [\"h6\", \"Heading 6\", \"Eyebrow heading\", i(IconH6)],\n // V2 catalog.tsx:89, :91 (rare in a story, so found by search)\n [\"task\", \"Checklist\", \"To-do list with checkboxes\", i(IconListCheck), [\"todo\", \"checkbox\"]],\n [\"button\", \"Button\", \"A call to action link\", i(IconClick), [\"link button\"]],\n]);\n\n/** The feature a block needs (Text needs none). */\nconst NEEDS: Record<BasicId, RichTextFeature | null> = {\n p: null, h1: \"h1\", h2: \"h2\", h3: \"h3\", h4: \"h4\", h5: \"h5\", h6: \"h6\", quote: \"blockquote\", ul: \"ul\", ol: \"ol\", code: \"pre\", hr: \"hr\", image: \"image\",\n pullquote: \"pullquote\", takeaways: \"takeaways\", callout: \"callout\", toggle: \"toggle\", video: \"video\", file: \"file\", table: \"table\",\n task: \"checklist\", button: \"button\",\n};\nconst MORE: CatalogItem = { key: \"more\", kind: \"folder\", label: \"More blocks\", description: \"Shared across your site\", icon: i(IconComponents) };\nconst BACK: CatalogItem = { key: \"back\", kind: \"back\", label: \"All blocks\", description: \"\", icon: i(IconArrowLeft) };\nconst componentRow = (c: KitInsertableComponent): CatalogItem => ({\n key: `c:${c.id}`,\n kind: \"component\",\n component: c,\n label: c.name,\n description: c.description || \"Shared across your site\",\n icon: i(IconComponents),\n});\n\n/** The components this field may embed, as the host answered (KitServices.useInsertableComponents). */\nexport interface CatalogComponents {\n components: KitInsertableComponent[];\n loading?: boolean;\n error?: string;\n}\nconst ComponentsContext = React.createContext<CatalogComponents | null>(null);\nexport const CatalogComponentsProvider = ComponentsContext.Provider;\n/** The components this field may embed (a component card's Swap component). */\nexport const useCatalogComponents = () => React.useContext(ComponentsContext);\n\nexport const blockAllowed = (id: BasicId, allows: (f: RichTextFeature) => boolean) => {\n const need = NEEDS[id];\n return !need || allows(need);\n};\n\n/* ── Recently used (per person, CPO D55): this browser's storage; a browser that keeps none just has no recents ── */\nconst RECENTS_KEY = \"kit-rt-insert-recents\";\nlet recents: string[] = (() => {\n try {\n const v = JSON.parse(globalThis.localStorage?.getItem(RECENTS_KEY) ?? \"[]\") as unknown;\n return Array.isArray(v) ? v.filter((x): x is string => typeof x === \"string\") : [];\n } catch {\n return [];\n }\n})();\nconst recentSubs = new Set<() => void>();\n/** Remember a pick so search ranks it first next time. */\nexport function noteRecent(it: CatalogItem) {\n recents = [it.key, ...recents.filter((k) => k !== it.key)].slice(0, 12);\n try {\n globalThis.localStorage?.setItem(RECENTS_KEY, JSON.stringify(recents));\n } catch {\n /* storage is off: recents last for this session */\n }\n recentSubs.forEach((s) => s());\n}\nfunction useRecents() {\n return React.useSyncExternalStore(\n (cb) => {\n recentSubs.add(cb);\n return () => recentSubs.delete(cb);\n },\n () => recents,\n () => recents,\n );\n}\n\nexport interface CatalogSection {\n title: string | null;\n items: CatalogItem[];\n}\n\n/** 5 exact (label, id or alias) · 4 label or alias starts with it · 3 a word of the label starts with it · 2 inside the\n * label · 1 the description. */\nconst score = (q: string, it: CatalogItem) => {\n const l = it.label.toLowerCase();\n const al = it.aliases ?? [];\n if (l === q || it.id === q || al.includes(q)) return 5;\n if (l.startsWith(q) || al.some((a) => a.startsWith(q))) return 4;\n if (l.split(/[\\s-]+/).some((w) => w.startsWith(q))) return 3;\n if (l.includes(q)) return 2;\n return it.description.toLowerCase().includes(q) ? 1 : 0;\n};\n/** Ranked by recently used among the strong matches, then by match, then catalogue order. */\nconst rank = (q: string, list: CatalogItem[], recent: string[]) =>\n list\n .map((it, i) => {\n const s = score(q, it);\n const r = recent.indexOf(it.key);\n return { it, s, strong: s >= 3 ? 1 : 0, r: s >= 3 && r >= 0 ? r : 999, i };\n })\n .filter((x) => x.s > 0)\n .sort((a, b) => b.strong - a.strong || a.r - b.r || b.s - a.s || a.i - b.i)\n .map((x) => x.it);\n\n/** `folder`: the More components view (its way back first, then the components, searched by `query`). */\nexport function useCatalog(query: string, allows: (f: RichTextFeature) => boolean, folder = false, own?: CatalogComponents | null) {\n const recent = useRecents();\n const provided = React.useContext(ComponentsContext);\n // the editor reads its own list (it renders the provider, so cannot read it)\n const lib = own === undefined ? provided : own;\n return React.useMemo(() => {\n const q = query.trim().toLowerCase().replace(/^\\//, \"\");\n const keep = (list: CatalogItem[]) => list.filter((it) => !it.id || blockAllowed(it.id, allows));\n const comps = (lib?.components ?? []).map(componentRow);\n let sections: CatalogSection[];\n let noMatch = false;\n let emptyText = \"No match.\";\n if (folder) {\n const found = q ? rank(q, comps, recent) : comps;\n noMatch = !!q && !!comps.length && !found.length;\n if (!comps.length) emptyText = lib?.error ?? (lib?.loading ? \"Loading blocks…\" : \"This field takes no blocks.\");\n sections = [{ title: null, items: [BACK] }, { title: \"Blocks\", items: found }];\n } else if (q) {\n const found = rank(q, [...keep(TEXT), ...keep(MEDIA), ...keep(DATA), ...keep(SEARCH_ONLY), ...comps], recent);\n noMatch = !found.length;\n sections = [{ title: null, items: found }];\n } else\n sections = [\n { title: \"Text\", items: keep(TEXT) },\n { title: \"Media\", items: keep(MEDIA) },\n { title: \"Data\", items: keep(DATA) },\n // offered while the list loads or failed too: opening it says which\n { title: null, items: lib && (comps.length || lib.loading || lib.error) ? [{ ...MORE, ...(comps.length ? { count: comps.length } : {}) }] : [] },\n ];\n sections = sections.filter((s) => s.items.length);\n const flat = sections.flatMap((s) => s.items);\n const empty = !flat.some((it) => it.kind !== \"back\");\n return { sections, flat, empty, noMatch, emptyText };\n }, [query, allows, recent, lib, folder]);\n}\n\n/* ── The list ───────────────────────────────────────────────────────────\n * Browsing: compact one-line rows (icon · name), grouped. Search results add the one-line description. */\nexport function CatalogList({\n sections,\n flat,\n active,\n onHover,\n onPick,\n empty,\n emptyText = \"No match.\",\n noMatch,\n detail,\n listId,\n className,\n}: {\n sections: CatalogSection[];\n flat: CatalogItem[];\n active: number;\n onHover: (i: number) => void;\n onPick: (it: CatalogItem) => void;\n empty: boolean;\n /** what an empty list says (\"No match.\", or why More components has none) */\n emptyText?: string;\n noMatch?: boolean;\n /** show each row's description (search results, More components) */\n detail?: boolean;\n listId: string;\n className?: string;\n}) {\n const box = React.useRef<HTMLDivElement>(null);\n React.useEffect(() => {\n // scroll only the list (scrollIntoView would also scroll the page behind a floating menu)\n const b = box.current;\n const row = b?.querySelector<HTMLElement>(`[data-idx=\"${active}\"]`);\n if (!b || !row) return;\n if (row.offsetTop < b.scrollTop) b.scrollTop = row.offsetTop - 6;\n else if (row.offsetTop + row.offsetHeight > b.scrollTop + b.clientHeight) b.scrollTop = row.offsetTop + row.offsetHeight - b.clientHeight + 6;\n }, [active]);\n return (\n <div ref={box} id={listId} role=\"listbox\" aria-label=\"Blocks\" className={cn(\"relative max-h-[320px] overflow-y-auto p-1.5\", className)}>\n {noMatch ? <div className=\"px-2 pb-1 pt-1.5 text-kit-sm text-kit-fg-3\">No match.</div> : null}\n {sections.map((s, si) => (\n <div\n key={s.title ?? `s${si}`}\n role=\"group\"\n aria-label={s.title ?? undefined}\n className={cn(!s.title && si > 0 && s.items[0]?.kind === \"folder\" && \"mt-1.5 border-t border-kit-line-subtle pt-1.5\")}\n >\n {s.title ? <div className=\"px-2 pb-0.5 pt-2 text-kit-2xs font-medium text-kit-fg-3\">{s.title}</div> : null}\n {s.items.map((it) => {\n const idx = flat.indexOf(it);\n const on = idx === active;\n const desc = detail && it.kind !== \"back\" && it.kind !== \"folder\" ? it.description : \"\";\n return (\n <button\n key={it.key}\n type=\"button\"\n role=\"option\"\n aria-selected={on}\n aria-description={!desc && it.description ? it.description : undefined}\n data-idx={idx}\n id={`${listId}-${idx}`}\n onMouseDown={(e) => e.preventDefault()}\n onMouseMove={() => !on && onHover(idx)}\n onClick={() => onPick(it)}\n className={cn(\n \"flex w-full items-center gap-2 rounded-kit-md px-2 text-left transition-colors motion-reduce:transition-none\",\n desc ? \"py-1.5\" : \"h-8\",\n it.kind === \"back\" ? \"text-kit-fg-2\" : \"text-kit-fg\",\n on && \"bg-kit-hover text-kit-fg\",\n )}\n >\n <span className={cn(\"flex shrink-0 items-center justify-center text-kit-fg-2 [&_svg]:size-4\", desc ? \"size-7 rounded-kit-md border border-kit-line bg-kit-control-bg\" : \"size-5\")}>\n {it.icon}\n </span>\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-kit-sm\">{it.label}</span>\n {desc ? <span className=\"block truncate text-kit-xs text-kit-fg-3\">{desc}</span> : null}\n </span>\n {it.count != null ? <span className=\"text-kit-2xs tabular-nums text-kit-fg-3\">{it.count}</span> : null}\n {it.kind === \"folder\" ? <IconChevronRight className=\"size-3.5 shrink-0 text-kit-fg-3\" stroke={1.5} aria-hidden /> : null}\n </button>\n );\n })}\n </div>\n ))}\n {empty ? <div className=\"px-2 py-6 text-center text-kit-sm text-kit-fg-3\">{emptyText}</div> : null}\n </div>\n );\n}\n\nconst Footer = () => (\n <div data-hint=\"shortcuts\" className=\"flex h-7 shrink-0 items-center border-t border-kit-line px-3 text-kit-2xs text-kit-fg-3\">\n <span>\n <kbd className=\"font-kit-sans\">↵</kbd> select · type to search\n </span>\n </div>\n);\n\n/** Shared keyboard model: ↑↓ wrap, Home/End, Enter / Tab, → into More components. Returns true when handled. */\nexport function catalogKey(\n e: React.KeyboardEvent | KeyboardEvent,\n flat: CatalogItem[],\n active: number,\n setActive: (i: number) => void,\n pick: (it: CatalogItem) => void,\n homeEnd = true,\n) {\n const n = flat.length;\n if (!n) return false;\n if (e.key === \"ArrowDown\") return setActive((active + 1) % n), true;\n if (e.key === \"ArrowUp\") return setActive((active - 1 + n) % n), true;\n if (homeEnd && e.key === \"Home\") return setActive(0), true;\n if (homeEnd && e.key === \"End\") return setActive(n - 1), true;\n if (e.key === \"ArrowRight\" && flat[active]?.kind === \"folder\") return pick(flat[active]), true;\n if (e.key === \"Enter\" || e.key === \"Tab\") {\n const it = flat[active];\n if (it) pick(it);\n return true;\n }\n return false;\n}\n\n/* ── Insert menu (popover content for the toolbar \"Insert\" and the gutter \"+\") ─ */\nexport function InsertMenuContent({\n onInsert,\n allows,\n keepFocusOnClose,\n align = \"start\",\n side = \"bottom\",\n}: {\n onInsert: (it: CatalogItem) => void;\n allows: (f: RichTextFeature) => boolean;\n /** after an insert the caret stays in the editor: the menu gives no focus back to its trigger */\n keepFocusOnClose?: () => boolean;\n align?: \"start\" | \"center\" | \"end\";\n side?: \"top\" | \"bottom\" | \"left\" | \"right\";\n}) {\n const [q, setQ] = React.useState(\"\");\n const [active, setActive] = React.useState(0);\n const input = React.useRef<HTMLInputElement>(null);\n const listId = React.useId();\n const [folder, setFolder] = React.useState(false);\n const { sections, flat, empty, emptyText, noMatch } = useCatalog(q, allows, folder);\n // in More components the first row is the way back: the first component starts active\n React.useEffect(() => setActive(folder && flat.length > 1 ? 1 : 0), [q, folder]); // eslint-disable-line react-hooks/exhaustive-deps\n const leave = () => {\n setFolder(false);\n setQ(\"\");\n input.current?.focus();\n };\n const pick = (it: CatalogItem) => {\n if (it.kind === \"folder\") {\n setFolder(true);\n setQ(\"\");\n input.current?.focus();\n } else if (it.kind === \"back\") leave();\n else onInsert(it);\n };\n return (\n <PopoverContent\n side={side}\n align={align}\n className=\"flex max-h-[420px] w-[320px] flex-col overflow-hidden p-0\"\n initialFocus={input}\n finalFocus={keepFocusOnClose ? () => !keepFocusOnClose() : undefined}\n // Esc in More components steps back to every block, wherever focus sits in the menu (the list scrolls, so the\n // browser can focus it): caught before the popover's own Esc, which would close it\n onKeyDownCapture={(e) => {\n if (!folder || e.key !== \"Escape\") return;\n e.preventDefault();\n e.stopPropagation();\n leave();\n }}\n >\n <div className=\"flex h-10 shrink-0 items-center gap-2 border-b border-kit-line px-3\">\n <IconSearch className=\"size-4 shrink-0 text-kit-fg-3\" stroke={1.5} aria-hidden />\n <input\n ref={input}\n value={q}\n onChange={(e) => setQ(e.target.value)}\n onKeyDown={(e) => {\n if (folder && e.key === \"Backspace\" && !q) {\n // ⌫ in an empty search steps back to every block (Esc: the popup's capture above)\n e.preventDefault();\n e.stopPropagation();\n leave();\n } else if (catalogKey(e, flat, active, setActive, pick, !q)) e.preventDefault();\n }}\n role=\"combobox\"\n aria-expanded\n aria-controls={listId}\n aria-activedescendant={flat.length ? `${listId}-${active}` : undefined}\n aria-label={folder ? \"Search blocks\" : \"Search or pick a block\"}\n placeholder={folder ? \"Search blocks\" : \"Search or pick a block\"}\n className=\"h-full min-w-0 flex-1 bg-transparent text-kit-sm text-kit-fg outline-none placeholder:text-kit-fg-3 focus-visible:shadow-none\"\n />\n </div>\n <CatalogList\n sections={sections}\n flat={flat}\n active={active}\n onHover={setActive}\n onPick={pick}\n empty={empty}\n emptyText={emptyText}\n noMatch={noMatch}\n detail={!!q || folder}\n listId={listId}\n className=\"min-h-0 flex-1\"\n />\n <Footer />\n </PopoverContent>\n );\n}\n\n/* ── Slash menu panel (positioned by the editor; never takes focus) ── */\nexport function SlashMenuPanel({\n query,\n folder = false,\n active,\n setActive,\n onPick,\n allows,\n maxHeight,\n}: {\n /** the room the editor found for the menu (collision-aware); the list scrolls inside it */\n maxHeight?: number;\n query: string;\n /** the More components view (the editor keeps it with the rest of the slash state) */\n folder?: boolean;\n active: number;\n setActive: (i: number) => void;\n onPick: (it: CatalogItem) => void;\n allows: (f: RichTextFeature) => boolean;\n}) {\n const listId = React.useId();\n const { sections, flat, empty, emptyText, noMatch } = useCatalog(query, allows, folder);\n return (\n <div\n data-kit-scope=\"\"\n className=\"glass-strong flex w-[320px] flex-col overflow-hidden rounded-kit-xl border border-kit-line\"\n style={maxHeight ? { maxHeight } : undefined}\n onMouseDown={(e) => e.preventDefault()}\n >\n <div className=\"flex h-8 shrink-0 items-center gap-2 border-b border-kit-line px-3 text-kit-sm\">\n <span className={cn(\"flex-1 truncate\", query ? \"text-kit-fg\" : \"text-kit-fg-3\")}>{query ? `${folder ? \"More blocks · \" : \"\"}/${query}` : folder ? \"More blocks\" : \"Type to search\"}</span>\n <kbd data-hint=\"shortcuts\" className=\"font-kit-sans text-kit-2xs text-kit-fg-3\">\n esc\n </kbd>\n </div>\n <CatalogList\n sections={sections}\n flat={flat}\n active={active}\n onHover={setActive}\n onPick={onPick}\n empty={empty}\n emptyText={emptyText}\n noMatch={noMatch}\n detail={!!query || folder}\n listId={listId}\n className=\"max-h-[300px] min-h-0 flex-1\"\n />\n <Footer />\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqDA,IAAM,KAAK,MAAgD,oBAAC,GAAD,EAAG,QAAQ,IAAM,CAAA;AAE5E,IAAM,QAAQ,SAA+B,KAAK,KAAK,CAAC,IAAI,OAAO,aAAa,MAAM,cAAc;CAAE,KAAK;CAAI;CAAI;CAAO;CAAa;CAAM;AAAQ,EAAE;AAEvJ,IAAM,OAAO,KAAK;CAChB;EAAC;EAAK;EAAQ;EAAmB,EAAE,WAAW;EAAG,CAAC,aAAa,MAAM;CAAC;CACtE;EAAC;EAAM;EAAa;EAAmB,EAAE,MAAM;EAAG,CAAC,SAAS,UAAU;CAAC;CACvE;EAAC;EAAM;EAAa;EAAsB,EAAE,MAAM;CAAC;CACnD;EAAC;EAAM;EAAa;EAAiB,EAAE,MAAM;CAAC;CAC9C;EAAC;EAAS;EAAS;EAAoB,EAAE,cAAc;EAAG,CAAC,YAAY;CAAC;CACxE;EAAC;EAAa;EAAc;EAAuC,EAAE,SAAS;EAAG;GAAC;GAAM;GAAe;EAAa;CAAC;CACrH;EAAC;EAAM;EAAQ;EAAiB,EAAE,QAAQ;EAAG;GAAC;GAAW;GAAiB;EAAW;CAAC;CACtF;EAAC;EAAM;EAAiB;EAAiB,EAAE,eAAe;EAAG,CAAC,WAAW,OAAO;CAAC;CACjF;EAAC;EAAU;EAAU;EAA8B,EAAE,wBAAwB;EAAG;GAAC;GAAa;GAAW;EAAU;CAAC;CACpH;EAAC;EAAW;EAAW;EAAkC,EAAE,cAAc;EAAG;GAAC;GAAQ;GAAO;GAAW;EAAO;CAAC;CAC/G;EAAC;EAAa;EAAiB;EAA6B,EAAE,eAAe;EAAG;GAAC;GAAW;GAAQ;GAAS;EAAY;CAAC;CAC1H;EAAC;EAAM;EAAW;EAAwB,EAAE,uBAAuB;EAAG;GAAC;GAAQ;GAAa;EAAM;CAAC;CACnG;EAAC;EAAQ;EAAQ;EAAsB,EAAE,aAAa;EAAG;GAAC;GAAc;GAAW;EAAK;CAAC;AAC3F,CAAC;AACD,IAAM,QAAQ,KAAK;CACjB;EAAC;EAAS;EAAS;EAAmC,EAAE,SAAS;EAAG;GAAC;GAAO;GAAW;EAAO;CAAC;CAC/F;EAAC;EAAS;EAAS;EAAuC,EAAE,SAAS;EAAG;GAAC;GAAO;GAAW;GAAM;GAAS;GAAQ;EAAO;CAAC;CAC1H;EAAC;EAAQ;EAAQ;EAA8B,EAAE,YAAY;EAAG;GAAC;GAAO;GAAY;EAAY;CAAC;AACnG,CAAC;AACD,IAAM,OAAO,KAAK,CAAC;CAAC;CAAS;CAAS;CAAoB,EAAE,SAAS;CAAG;EAAC;EAAQ;EAAQ;EAAc;CAAa;AAAC,CAAC,CAAC;;AAEvH,IAAM,cAAc,KAAK;CACvB;EAAC;EAAM;EAAa;EAAmB,EAAE,MAAM;CAAC;CAChD;EAAC;EAAM;EAAa;EAAoB,EAAE,MAAM;CAAC;CACjD;EAAC;EAAM;EAAa;EAAmB,EAAE,MAAM;CAAC;CAEhD;EAAC;EAAQ;EAAa;EAA8B,EAAE,aAAa;EAAG,CAAC,QAAQ,UAAU;CAAC;CAC1F;EAAC;EAAU;EAAU;EAAyB,EAAE,SAAS;EAAG,CAAC,aAAa;CAAC;AAC7E,CAAC;;AAGD,IAAM,QAAiD;CACrD,GAAG;CAAM,IAAI;CAAM,IAAI;CAAM,IAAI;CAAM,IAAI;CAAM,IAAI;CAAM,IAAI;CAAM,OAAO;CAAc,IAAI;CAAM,IAAI;CAAM,MAAM;CAAO,IAAI;CAAM,OAAO;CAC5I,WAAW;CAAa,WAAW;CAAa,SAAS;CAAW,QAAQ;CAAU,OAAO;CAAS,MAAM;CAAQ,OAAO;CAC3H,MAAM;CAAa,QAAQ;AAC7B;AACA,IAAM,OAAoB;CAAE,KAAK;CAAQ,MAAM;CAAU,OAAO;CAAe,aAAa;CAA2B,MAAM,EAAE,cAAc;AAAE;AAC/I,IAAM,OAAoB;CAAE,KAAK;CAAQ,MAAM;CAAQ,OAAO;CAAc,aAAa;CAAI,MAAM,EAAE,aAAa;AAAE;AACpH,IAAM,gBAAgB,OAA4C;CAChE,KAAK,KAAK,EAAE;CACZ,MAAM;CACN,WAAW;CACX,OAAO,EAAE;CACT,aAAa,EAAE,eAAe;CAC9B,MAAM,EAAE,cAAc;AACxB;AAQA,IAAM,oBAAoB,MAAM,cAAwC,IAAI;AAC5E,IAAa,4BAA4B,kBAAkB;;AAE3D,IAAa,6BAA6B,MAAM,WAAW,iBAAiB;AAE5E,IAAa,gBAAgB,IAAa,WAA4C;CACpF,MAAM,OAAO,MAAM;CACnB,OAAO,CAAC,QAAQ,OAAO,IAAI;AAC7B;AAGA,IAAM,cAAc;AACpB,IAAI,iBAA2B;CAC7B,IAAI;EACF,MAAM,IAAI,KAAK,MAAM,WAAW,cAAc,QAAQ,WAAW,KAAK,IAAI;EAC1E,OAAO,MAAM,QAAQ,CAAC,IAAI,EAAE,QAAQ,MAAmB,OAAO,MAAM,QAAQ,IAAI,CAAC;CACnF,QAAQ;EACN,OAAO,CAAC;CACV;AACF,EAAA,CAAG;AACH,IAAM,6BAAa,IAAI,IAAgB;;AAEvC,SAAgB,WAAW,IAAiB;CAC1C,UAAU,CAAC,GAAG,KAAK,GAAG,QAAQ,QAAQ,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,MAAM,GAAG,EAAE;CACtE,IAAI;EACF,WAAW,cAAc,QAAQ,aAAa,KAAK,UAAU,OAAO,CAAC;CACvE,QAAQ,CAER;CACA,WAAW,SAAS,MAAM,EAAE,CAAC;AAC/B;AACA,SAAS,aAAa;CACpB,OAAO,MAAM,sBACV,OAAO;EACN,WAAW,IAAI,EAAE;EACjB,aAAa,WAAW,OAAO,EAAE;CACnC,SACM,eACA,OACR;AACF;;;AASA,IAAM,SAAS,GAAW,OAAoB;CAC5C,MAAM,IAAI,GAAG,MAAM,YAAY;CAC/B,MAAM,KAAK,GAAG,WAAW,CAAC;CAC1B,IAAI,MAAM,KAAK,GAAG,OAAO,KAAK,GAAG,SAAS,CAAC,GAAG,OAAO;CACrD,IAAI,EAAE,WAAW,CAAC,KAAK,GAAG,MAAM,MAAM,EAAE,WAAW,CAAC,CAAC,GAAG,OAAO;CAC/D,IAAI,EAAE,MAAM,QAAQ,CAAC,CAAC,MAAM,MAAM,EAAE,WAAW,CAAC,CAAC,GAAG,OAAO;CAC3D,IAAI,EAAE,SAAS,CAAC,GAAG,OAAO;CAC1B,OAAO,GAAG,YAAY,YAAY,CAAC,CAAC,SAAS,CAAC,IAAI,IAAI;AACxD;;AAEA,IAAM,QAAQ,GAAW,MAAqB,WAC5C,KACG,KAAK,IAAI,MAAM;CACd,MAAM,IAAI,MAAM,GAAG,EAAE;CACrB,MAAM,IAAI,OAAO,QAAQ,GAAG,GAAG;CAC/B,OAAO;EAAE;EAAI;EAAG,QAAQ,KAAK,IAAI,IAAI;EAAG,GAAG,KAAK,KAAK,KAAK,IAAI,IAAI;EAAK;CAAE;AAC3E,CAAC,CAAC,CACD,QAAQ,MAAM,EAAE,IAAI,CAAC,CAAC,CACtB,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAC1E,KAAK,MAAM,EAAE,EAAE;;AAGpB,SAAgB,WAAW,OAAe,QAAyC,SAAS,OAAO,KAAgC;CACjI,MAAM,SAAS,WAAW;CAC1B,MAAM,WAAW,MAAM,WAAW,iBAAiB;CAEnD,MAAM,MAAM,QAAQ,KAAA,IAAY,WAAW;CAC3C,OAAO,MAAM,cAAc;EACzB,MAAM,IAAI,MAAM,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,QAAQ,OAAO,EAAE;EACtD,MAAM,QAAQ,SAAwB,KAAK,QAAQ,OAAO,CAAC,GAAG,MAAM,aAAa,GAAG,IAAI,MAAM,CAAC;EAC/F,MAAM,SAAS,KAAK,cAAc,CAAC,EAAA,CAAG,IAAI,YAAY;EACtD,IAAI;EACJ,IAAI,UAAU;EACd,IAAI,YAAY;EAChB,IAAI,QAAQ;GACV,MAAM,QAAQ,IAAI,KAAK,GAAG,OAAO,MAAM,IAAI;GAC3C,UAAU,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,UAAU,CAAC,MAAM;GAC1C,IAAI,CAAC,MAAM,QAAQ,YAAY,KAAK,UAAU,KAAK,UAAU,oBAAoB;GACjF,WAAW,CAAC;IAAE,OAAO;IAAM,OAAO,CAAC,IAAI;GAAE,GAAG;IAAE,OAAO;IAAU,OAAO;GAAM,CAAC;EAC/E,OAAO,IAAI,GAAG;GACZ,MAAM,QAAQ,KAAK,GAAG;IAAC,GAAG,KAAK,IAAI;IAAG,GAAG,KAAK,KAAK;IAAG,GAAG,KAAK,IAAI;IAAG,GAAG,KAAK,WAAW;IAAG,GAAG;GAAK,GAAG,MAAM;GAC5G,UAAU,CAAC,MAAM;GACjB,WAAW,CAAC;IAAE,OAAO;IAAM,OAAO;GAAM,CAAC;EAC3C,OACE,WAAW;GACT;IAAE,OAAO;IAAQ,OAAO,KAAK,IAAI;GAAE;GACnC;IAAE,OAAO;IAAS,OAAO,KAAK,KAAK;GAAE;GACrC;IAAE,OAAO;IAAQ,OAAO,KAAK,IAAI;GAAE;GAEnC;IAAE,OAAO;IAAM,OAAO,QAAQ,MAAM,UAAU,IAAI,WAAW,IAAI,SAAS,CAAC;KAAE,GAAG;KAAM,GAAI,MAAM,SAAS,EAAE,OAAO,MAAM,OAAO,IAAI,CAAC;IAAG,CAAC,IAAI,CAAC;GAAE;EACjJ;EACF,WAAW,SAAS,QAAQ,MAAM,EAAE,MAAM,MAAM;EAChD,MAAM,OAAO,SAAS,SAAS,MAAM,EAAE,KAAK;EAC5C,MAAM,QAAQ,CAAC,KAAK,MAAM,OAAO,GAAG,SAAS,MAAM;EACnD,OAAO;GAAE;GAAU;GAAM;GAAO;GAAS;EAAU;CACrD,GAAG;EAAC;EAAO;EAAQ;EAAQ;EAAK;CAAM,CAAC;AACzC;AAIA,SAAgB,YAAY,EAC1B,UACA,MACA,QACA,SACA,QACA,OACA,YAAY,aACZ,SACA,QACA,QACA,aAeC;CACD,MAAM,MAAM,MAAM,OAAuB,IAAI;CAC7C,MAAM,gBAAgB;EAEpB,MAAM,IAAI,IAAI;EACd,MAAM,MAAM,GAAG,cAA2B,cAAc,OAAO,GAAG;EAClE,IAAI,CAAC,KAAK,CAAC,KAAK;EAChB,IAAI,IAAI,YAAY,EAAE,WAAW,EAAE,YAAY,IAAI,YAAY;OAC1D,IAAI,IAAI,YAAY,IAAI,eAAe,EAAE,YAAY,EAAE,cAAc,EAAE,YAAY,IAAI,YAAY,IAAI,eAAe,EAAE,eAAe;CAC9I,GAAG,CAAC,MAAM,CAAC;CACX,OACE,qBAAC,OAAD;EAAK,KAAK;EAAK,IAAI;EAAQ,MAAK;EAAU,cAAW;EAAS,WAAW,GAAG,gDAAgD,SAAS;EAArI,UAAA;GACG,UAAU,oBAAC,OAAD;IAAK,WAAU;IAA6C,UAAA;GAAc,CAAA,IAAI;GACxF,SAAS,KAAK,GAAG,OAChB,qBAAC,OAAD;IAEE,MAAK;IACL,cAAY,EAAE,SAAS,KAAA;IACvB,WAAW,GAAG,CAAC,EAAE,SAAS,KAAK,KAAK,EAAE,MAAM,EAAE,EAAE,SAAS,YAAY,+CAA+C;IAJtH,UAAA,CAMG,EAAE,QAAQ,oBAAC,OAAD;KAAK,WAAU;KAA2D,UAAA,EAAE;IAAW,CAAA,IAAI,MACrG,EAAE,MAAM,KAAK,OAAO;KACnB,MAAM,MAAM,KAAK,QAAQ,EAAE;KAC3B,MAAM,KAAK,QAAQ;KACnB,MAAM,OAAO,UAAU,GAAG,SAAS,UAAU,GAAG,SAAS,WAAW,GAAG,cAAc;KACrF,OACE,qBAAC,UAAD;MAEE,MAAK;MACL,MAAK;MACL,iBAAe;MACf,oBAAkB,CAAC,QAAQ,GAAG,cAAc,GAAG,cAAc,KAAA;MAC7D,YAAU;MACV,IAAI,GAAG,OAAO,GAAG;MACjB,cAAc,MAAM,EAAE,eAAe;MACrC,mBAAmB,CAAC,MAAM,QAAQ,GAAG;MACrC,eAAe,OAAO,EAAE;MACxB,WAAW,GACT,gHACA,OAAO,WAAW,OAClB,GAAG,SAAS,SAAS,kBAAkB,eACvC,MAAM,0BACR;MAhBF,UAAA;OAkBE,oBAAC,QAAD;QAAM,WAAW,GAAG,0EAA0E,OAAO,mEAAmE,QAAQ;QAC7K,UAAA,GAAG;OACA,CAAA;OACN,qBAAC,QAAD;QAAM,WAAU;QAAhB,UAAA,CACE,oBAAC,QAAD;SAAM,WAAU;SAA8B,UAAA,GAAG;QAAY,CAAA,GAC5D,OAAO,oBAAC,QAAD;SAAM,WAAU;SAA4C,UAAA;QAAW,CAAA,IAAI,IAC/E;;OACL,GAAG,SAAS,OAAO,oBAAC,QAAD;QAAM,WAAU;QAA2C,UAAA,GAAG;OAAY,CAAA,IAAI;OACjG,GAAG,SAAS,WAAW,oBAAC,kBAAD;QAAkB,WAAU;QAAkC,QAAQ;QAAK,eAAA;OAAa,CAAA,IAAI;MAC9G;KA1BD,GAAA,GAAG,GA0BF;IAEZ,CAAC,CACE;GAzCE,GAAA,EAAE,SAAS,IAAI,IAyCjB,CACN;GACA,QAAQ,oBAAC,OAAD;IAAK,WAAU;IAAmD,UAAA;GAAe,CAAA,IAAI;EAC3F;;AAET;AAEA,IAAM,eACJ,oBAAC,OAAD;CAAK,aAAU;CAAY,WAAU;CACnC,UAAA,qBAAC,QAAD,EAAA,UAAA,CACE,oBAAC,OAAD;EAAK,WAAU;EAAgB,UAAA;CAAM,CAAA,GAAC,0BAClC,EAAA,CAAA;AACH,CAAA;;AAIP,SAAgB,WACd,GACA,MACA,QACA,WACA,MACA,UAAU,MACV;CACA,MAAM,IAAI,KAAK;CACf,IAAI,CAAC,GAAG,OAAO;CACf,IAAI,EAAE,QAAQ,aAAa,OAAO,WAAW,SAAS,KAAK,CAAC,GAAG;CAC/D,IAAI,EAAE,QAAQ,WAAW,OAAO,WAAW,SAAS,IAAI,KAAK,CAAC,GAAG;CACjE,IAAI,WAAW,EAAE,QAAQ,QAAQ,OAAO,UAAU,CAAC,GAAG;CACtD,IAAI,WAAW,EAAE,QAAQ,OAAO,OAAO,UAAU,IAAI,CAAC,GAAG;CACzD,IAAI,EAAE,QAAQ,gBAAgB,KAAK,OAAO,EAAE,SAAS,UAAU,OAAO,KAAK,KAAK,OAAO,GAAG;CAC1F,IAAI,EAAE,QAAQ,WAAW,EAAE,QAAQ,OAAO;EACxC,MAAM,KAAK,KAAK;EAChB,IAAI,IAAI,KAAK,EAAE;EACf,OAAO;CACT;CACA,OAAO;AACT;AAGA,SAAgB,kBAAkB,EAChC,UACA,QACA,kBACA,QAAQ,SACR,OAAO,YAQN;CACD,MAAM,CAAC,GAAG,QAAQ,MAAM,SAAS,EAAE;CACnC,MAAM,CAAC,QAAQ,aAAa,MAAM,SAAS,CAAC;CAC5C,MAAM,QAAQ,MAAM,OAAyB,IAAI;CACjD,MAAM,SAAS,MAAM,MAAM;CAC3B,MAAM,CAAC,QAAQ,aAAa,MAAM,SAAS,KAAK;CAChD,MAAM,EAAE,UAAU,MAAM,OAAO,WAAW,YAAY,WAAW,GAAG,QAAQ,MAAM;CAElF,MAAM,gBAAgB,UAAU,UAAU,KAAK,SAAS,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC;CAC/E,MAAM,cAAc;EAClB,UAAU,KAAK;EACf,KAAK,EAAE;EACP,MAAM,SAAS,MAAM;CACvB;CACA,MAAM,QAAQ,OAAoB;EAChC,IAAI,GAAG,SAAS,UAAU;GACxB,UAAU,IAAI;GACd,KAAK,EAAE;GACP,MAAM,SAAS,MAAM;EACvB,OAAO,IAAI,GAAG,SAAS,QAAQ,MAAM;OAChC,SAAS,EAAE;CAClB;CACA,OACE,qBAAC,gBAAD;EACQ;EACC;EACP,WAAU;EACV,cAAc;EACd,YAAY,yBAAyB,CAAC,iBAAiB,IAAI,KAAA;EAG3D,mBAAmB,MAAM;GACvB,IAAI,CAAC,UAAU,EAAE,QAAQ,UAAU;GACnC,EAAE,eAAe;GACjB,EAAE,gBAAgB;GAClB,MAAM;EACR;EAbF,UAAA;GAeE,qBAAC,OAAD;IAAK,WAAU;IAAf,UAAA,CACE,oBAAC,YAAD;KAAY,WAAU;KAAgC,QAAQ;KAAK,eAAA;IAAa,CAAA,GAChF,oBAAC,SAAD;KACE,KAAK;KACL,OAAO;KACP,WAAW,MAAM,KAAK,EAAE,OAAO,KAAK;KACpC,YAAY,MAAM;MAChB,IAAI,UAAU,EAAE,QAAQ,eAAe,CAAC,GAAG;OAEzC,EAAE,eAAe;OACjB,EAAE,gBAAgB;OAClB,MAAM;MACR,OAAO,IAAI,WAAW,GAAG,MAAM,QAAQ,WAAW,MAAM,CAAC,CAAC,GAAG,EAAE,eAAe;KAChF;KACA,MAAK;KACL,iBAAA;KACA,iBAAe;KACf,yBAAuB,KAAK,SAAS,GAAG,OAAO,GAAG,WAAW,KAAA;KAC7D,cAAY,SAAS,kBAAkB;KACvC,aAAa,SAAS,kBAAkB;KACxC,WAAU;IACX,CAAA,CACE;;GACL,oBAAC,aAAD;IACY;IACJ;IACE;IACR,SAAS;IACT,QAAQ;IACD;IACI;IACF;IACT,QAAQ,CAAC,CAAC,KAAK;IACP;IACR,WAAU;GACX,CAAA;GACD,oBAAC,QAAD,CAAS,CAAA;EACK;;AAEpB;AAGA,SAAgB,eAAe,EAC7B,OACA,SAAS,OACT,QACA,WACA,QACA,QACA,aAWC;CACD,MAAM,SAAS,MAAM,MAAM;CAC3B,MAAM,EAAE,UAAU,MAAM,OAAO,WAAW,YAAY,WAAW,OAAO,QAAQ,MAAM;CACtF,OACE,qBAAC,OAAD;EACE,kBAAe;EACf,WAAU;EACV,OAAO,YAAY,EAAE,UAAU,IAAI,KAAA;EACnC,cAAc,MAAM,EAAE,eAAe;EAJvC,UAAA;GAME,qBAAC,OAAD;IAAK,WAAU;IAAf,UAAA,CACE,oBAAC,QAAD;KAAM,WAAW,GAAG,mBAAmB,QAAQ,gBAAgB,eAAe;KAAI,UAAA,QAAQ,GAAG,SAAS,mBAAmB,GAAG,GAAG,UAAU,SAAS,gBAAgB;IAAuB,CAAA,GACzL,oBAAC,OAAD;KAAK,aAAU;KAAY,WAAU;KAA2C,UAAA;IAE3E,CAAA,CACF;;GACL,oBAAC,aAAD;IACY;IACJ;IACE;IACR,SAAS;IACD;IACD;IACI;IACF;IACT,QAAQ,CAAC,CAAC,SAAS;IACX;IACR,WAAU;GACX,CAAA;GACD,oBAAC,QAAD,CAAS,CAAA;EACN;;AAET"}
|
|
1
|
+
{"version":3,"file":"catalog.js","names":[],"sources":["../../../src/controls/rich-text/catalog.tsx"],"sourcesContent":["/**\n * RICH TEXT · the block catalogue shared by the toolbar Insert menu, the gutter \"+\" and the \"/\" slash menu (V2 HEAD\n * controls/rich-text/catalog.tsx, CPO D55).\n *\n * browse Text · Media · Data, grouped, then one quiet \"More components N\" row that opens the components the\n * field may embed (V2 HEAD catalog.tsx MORE), with its own search and a way back (\"All blocks\")\n * search every block and every embeddable component in one list: recently used first among the strong matches,\n * then by match (label, aliases, then description)\n * no match \"No match.\"\n *\n * Only the blocks the field accepts are offered (`features`, from the host's codec: each block is offered only where\n * the field's stored shape has a place for it). Not in this catalogue, and why:\n * (none: every V2 block has a stored form now, plan RT0–RT8; each is offered only where the field's codec declares it)\n *\n * Components come from the host (KitServices.useInsertableComponents: the project's components, narrowed to the ones\n * the field's config accepts), handed in through CatalogComponentsProvider. Without that service none is offered.\n * Deviation from V2: the library has no inline / page-section placement, so every component sits in one\n * \"Components\" group (V2: \"Article\" and \"Page sections\"), and no live preview flyout is drawn beside a row.\n */\nimport * as React from \"react\";\nimport {\n IconClick, IconCode, IconColumns2, IconNews, IconSuperscript, IconUserCircle, IconLayoutGrid, IconListCheck, IconListTree,\n IconArrowLeft, IconBlockquote, IconChevronRight, IconComponents, IconFileText, IconH1, IconH2, IconH3, IconH4, IconH5, IconH6, IconInfoCircle, IconLayoutNavbarCollapse, IconList, IconListDetails, IconListNumbers, IconPhoto,\n IconPilcrow, IconQuote, IconSearch, IconSeparatorHorizontal, IconTable, IconTerminal2, IconVideo,\n} from \"@tabler/icons-react\";\nimport { cn } from \"../../lib/cn\";\nimport { PopoverContent } from \"../../ui/overlay\";\nimport type { RichTextFeature } from \"./codec\";\nimport type { KitInsertableComponent } from \"../component-services\";\n\nexport type BasicId =\n | \"p\" | \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\" | \"quote\" | \"pullquote\" | \"callout\" | \"toggle\" | \"takeaways\" | \"ul\" | \"ol\" | \"code\" | \"hr\"\n | \"image\" | \"video\" | \"file\" | \"table\" | \"task\" | \"button\" | \"gallery\" | \"embed\" | \"toc\" | \"footnote\" | \"columns\" | \"related\" | \"author\";\n\nexport interface CatalogItem {\n key: string;\n /** a basic block (absent on a component, the More components row and the way back) */\n id?: BasicId;\n /** component: embeds `component`; folder: opens More components; back: returns to every block */\n kind?: \"component\" | \"folder\" | \"back\";\n component?: KitInsertableComponent;\n /** the folder row's component count */\n count?: number;\n label: string;\n description: string;\n icon: React.ReactNode;\n /** extra words that find it */\n aliases?: string[];\n}\n\nconst i = (I: React.ComponentType<{ stroke?: number }>) => <I stroke={1.5} />;\ntype Row = [BasicId, string, string, React.ReactNode, string[]?];\nconst rows = (list: Row[]): CatalogItem[] => list.map(([id, label, description, icon, aliases]) => ({ key: id, id, label, description, icon, aliases }));\n\nconst TEXT = rows([\n [\"p\", \"Text\", \"Plain body text\", i(IconPilcrow), [\"paragraph\", \"body\"]],\n [\"h2\", \"Heading 2\", \"Section heading\", i(IconH2), [\"title\", \"subtitle\"]],\n [\"h3\", \"Heading 3\", \"Subsection heading\", i(IconH3)],\n [\"h4\", \"Heading 4\", \"Minor heading\", i(IconH4)],\n [\"quote\", \"Quote\", \"A quoted passage\", i(IconBlockquote), [\"blockquote\"]],\n [\"pullquote\", \"Pull quote\", \"A highlighted line with who said it\", i(IconQuote), [\"pq\", \"testimonial\", \"attribution\"]],\n [\"ul\", \"List\", \"Bulleted list\", i(IconList), [\"bullets\", \"bulleted list\", \"unordered\"]],\n [\"ol\", \"Numbered list\", \"Ordered steps\", i(IconListNumbers), [\"ordered\", \"steps\"]],\n [\"toggle\", \"Toggle\", \"Collapsible title and body\", i(IconLayoutNavbarCollapse), [\"accordion\", \"details\", \"collapse\"]],\n [\"callout\", \"Callout\", \"A highlighted note with a tone\", i(IconInfoCircle), [\"note\", \"tip\", \"warning\", \"aside\"]],\n [\"takeaways\", \"Key takeaways\", \"The story in a few points\", i(IconListDetails), [\"summary\", \"tldr\", \"tl;dr\", \"highlights\"]],\n [\"hr\", \"Divider\", \"A line between parts\", i(IconSeparatorHorizontal), [\"line\", \"separator\", \"rule\"]],\n [\"code\", \"Code\", \"Monospaced snippet\", i(IconTerminal2), [\"code block\", \"snippet\", \"pre\"]],\n [\"footnote\", \"Footnote\", \"A numbered note at the end\", i(IconSuperscript), [\"fn\", \"reference\", \"citation\", \"source\"]],\n]);\nconst MEDIA = rows([\n [\"image\", \"Image\", \"Image with alt text and caption\", i(IconPhoto), [\"img\", \"picture\", \"photo\"]],\n [\"gallery\", \"Gallery\", \"Several images as a grid or carousel\", i(IconLayoutGrid), [\"images\", \"photos\", \"carousel\", \"slideshow\"]],\n [\"video\", \"Video\", \"A YouTube, Vimeo or video file link\", i(IconVideo), [\"mp4\", \"youtube\", \"yt\", \"vimeo\", \"clip\", \"movie\"]],\n [\"embed\", \"Embed\", \"YouTube, X, CodePen, Figma, maps or audio\", i(IconCode), [\"yt\", \"youtube\", \"vimeo\", \"x\", \"tweet\", \"twitter\", \"codepen\", \"figma\", \"map\", \"maps\", \"spotify\", \"podcast\", \"audio\", \"iframe\"]],\n [\"file\", \"File\", \"A downloadable PDF or file\", i(IconFileText), [\"pdf\", \"download\", \"attachment\"]],\n]);\n/** V2 catalog.tsx ARTICLE: article blocks written in the body itself (the rest land with plan RT6–RT8). */\nconst ARTICLE = rows([\n [\"related\", \"Related posts\", \"More stories, picked or automatic\", i(IconNews), [\"related stories\", \"read next\", \"more stories\"]],\n [\"toc\", \"Table of contents\", \"Built from the headings\", i(IconListTree), [\"contents\", \"outline\", \"index\"]],\n [\"author\", \"Author bio\", \"Photo, name and short bio\", i(IconUserCircle), [\"bio\", \"byline\", \"about the author\"]],\n]);\nconst DATA = rows([[\"table\", \"Table\", \"Rows and columns\", i(IconTable), [\"grid\", \"data\", \"comparison\", \"spreadsheet\"]]]);\n/** Found by search, not listed: rare in a body (H1 is the page title). */\nconst SEARCH_ONLY = rows([\n [\"h1\", \"Heading 1\", \"Largest heading\", i(IconH1)],\n [\"h5\", \"Heading 5\", \"Smallest heading\", i(IconH5)],\n [\"h6\", \"Heading 6\", \"Eyebrow heading\", i(IconH6)],\n // V2 catalog.tsx:89, :91 (rare in a story, so found by search)\n [\"task\", \"Checklist\", \"To-do list with checkboxes\", i(IconListCheck), [\"todo\", \"checkbox\"]],\n [\"columns\", \"Columns\", \"Side by side text\", i(IconColumns2), [\"layout\"]],\n [\"button\", \"Button\", \"A call to action link\", i(IconClick), [\"link button\"]],\n]);\n\n/** The feature a block needs (Text needs none). */\nconst NEEDS: Record<BasicId, RichTextFeature | null> = {\n p: null, h1: \"h1\", h2: \"h2\", h3: \"h3\", h4: \"h4\", h5: \"h5\", h6: \"h6\", quote: \"blockquote\", ul: \"ul\", ol: \"ol\", code: \"pre\", hr: \"hr\", image: \"image\",\n pullquote: \"pullquote\", takeaways: \"takeaways\", callout: \"callout\", toggle: \"toggle\", video: \"video\", file: \"file\", table: \"table\",\n task: \"checklist\", button: \"button\", gallery: \"gallery\", embed: \"embed\", toc: \"toc\",\n footnote: \"footnote\", columns: \"columns\", related: \"related\", author: \"author\",\n};\nconst MORE: CatalogItem = { key: \"more\", kind: \"folder\", label: \"More blocks\", description: \"Shared across your site\", icon: i(IconComponents) };\nconst BACK: CatalogItem = { key: \"back\", kind: \"back\", label: \"All blocks\", description: \"\", icon: i(IconArrowLeft) };\nconst componentRow = (c: KitInsertableComponent): CatalogItem => ({\n key: `c:${c.id}`,\n kind: \"component\",\n component: c,\n label: c.name,\n description: c.description || \"Shared across your site\",\n icon: i(IconComponents),\n});\n\n/** The components this field may embed, as the host answered (KitServices.useInsertableComponents). */\nexport interface CatalogComponents {\n components: KitInsertableComponent[];\n loading?: boolean;\n error?: string;\n}\nconst ComponentsContext = React.createContext<CatalogComponents | null>(null);\nexport const CatalogComponentsProvider = ComponentsContext.Provider;\n/** The components this field may embed (a component card's Swap component). */\nexport const useCatalogComponents = () => React.useContext(ComponentsContext);\n\nexport const blockAllowed = (id: BasicId, allows: (f: RichTextFeature) => boolean) => {\n const need = NEEDS[id];\n return !need || allows(need);\n};\n\n/* ── Recently used (per person, CPO D55): this browser's storage; a browser that keeps none just has no recents ── */\nconst RECENTS_KEY = \"kit-rt-insert-recents\";\nlet recents: string[] = (() => {\n try {\n const v = JSON.parse(globalThis.localStorage?.getItem(RECENTS_KEY) ?? \"[]\") as unknown;\n return Array.isArray(v) ? v.filter((x): x is string => typeof x === \"string\") : [];\n } catch {\n return [];\n }\n})();\nconst recentSubs = new Set<() => void>();\n/** Remember a pick so search ranks it first next time. */\nexport function noteRecent(it: CatalogItem) {\n recents = [it.key, ...recents.filter((k) => k !== it.key)].slice(0, 12);\n try {\n globalThis.localStorage?.setItem(RECENTS_KEY, JSON.stringify(recents));\n } catch {\n /* storage is off: recents last for this session */\n }\n recentSubs.forEach((s) => s());\n}\nfunction useRecents() {\n return React.useSyncExternalStore(\n (cb) => {\n recentSubs.add(cb);\n return () => recentSubs.delete(cb);\n },\n () => recents,\n () => recents,\n );\n}\n\nexport interface CatalogSection {\n title: string | null;\n items: CatalogItem[];\n}\n\n/** 5 exact (label, id or alias) · 4 label or alias starts with it · 3 a word of the label starts with it · 2 inside the\n * label · 1 the description. */\nconst score = (q: string, it: CatalogItem) => {\n const l = it.label.toLowerCase();\n const al = it.aliases ?? [];\n if (l === q || it.id === q || al.includes(q)) return 5;\n if (l.startsWith(q) || al.some((a) => a.startsWith(q))) return 4;\n if (l.split(/[\\s-]+/).some((w) => w.startsWith(q))) return 3;\n if (l.includes(q)) return 2;\n return it.description.toLowerCase().includes(q) ? 1 : 0;\n};\n/** Ranked by recently used among the strong matches, then by match, then catalogue order. */\nconst rank = (q: string, list: CatalogItem[], recent: string[]) =>\n list\n .map((it, i) => {\n const s = score(q, it);\n const r = recent.indexOf(it.key);\n return { it, s, strong: s >= 3 ? 1 : 0, r: s >= 3 && r >= 0 ? r : 999, i };\n })\n .filter((x) => x.s > 0)\n .sort((a, b) => b.strong - a.strong || a.r - b.r || b.s - a.s || a.i - b.i)\n .map((x) => x.it);\n\n/** `folder`: the More components view (its way back first, then the components, searched by `query`). */\nexport function useCatalog(query: string, allows: (f: RichTextFeature) => boolean, folder = false, own?: CatalogComponents | null) {\n const recent = useRecents();\n const provided = React.useContext(ComponentsContext);\n // the editor reads its own list (it renders the provider, so cannot read it)\n const lib = own === undefined ? provided : own;\n return React.useMemo(() => {\n const q = query.trim().toLowerCase().replace(/^\\//, \"\");\n const keep = (list: CatalogItem[]) => list.filter((it) => !it.id || blockAllowed(it.id, allows));\n const comps = (lib?.components ?? []).map(componentRow);\n let sections: CatalogSection[];\n let noMatch = false;\n let emptyText = \"No match.\";\n if (folder) {\n const found = q ? rank(q, comps, recent) : comps;\n noMatch = !!q && !!comps.length && !found.length;\n if (!comps.length) emptyText = lib?.error ?? (lib?.loading ? \"Loading blocks…\" : \"This field takes no blocks.\");\n sections = [{ title: null, items: [BACK] }, { title: \"Blocks\", items: found }];\n } else if (q) {\n const found = rank(q, [...keep(TEXT), ...keep(MEDIA), ...keep(DATA), ...keep(ARTICLE), ...keep(SEARCH_ONLY), ...comps], recent);\n noMatch = !found.length;\n sections = [{ title: null, items: found }];\n } else\n sections = [\n { title: \"Text\", items: keep(TEXT) },\n { title: \"Media\", items: keep(MEDIA) },\n { title: \"Data\", items: keep(DATA) },\n { title: \"Article\", items: keep(ARTICLE) },\n // offered while the list loads or failed too: opening it says which\n { title: null, items: lib && (comps.length || lib.loading || lib.error) ? [{ ...MORE, ...(comps.length ? { count: comps.length } : {}) }] : [] },\n ];\n sections = sections.filter((s) => s.items.length);\n const flat = sections.flatMap((s) => s.items);\n const empty = !flat.some((it) => it.kind !== \"back\");\n return { sections, flat, empty, noMatch, emptyText };\n }, [query, allows, recent, lib, folder]);\n}\n\n/* ── The list ───────────────────────────────────────────────────────────\n * Browsing: compact one-line rows (icon · name), grouped. Search results add the one-line description. */\nexport function CatalogList({\n sections,\n flat,\n active,\n onHover,\n onPick,\n empty,\n emptyText = \"No match.\",\n noMatch,\n detail,\n listId,\n className,\n}: {\n sections: CatalogSection[];\n flat: CatalogItem[];\n active: number;\n onHover: (i: number) => void;\n onPick: (it: CatalogItem) => void;\n empty: boolean;\n /** what an empty list says (\"No match.\", or why More components has none) */\n emptyText?: string;\n noMatch?: boolean;\n /** show each row's description (search results, More components) */\n detail?: boolean;\n listId: string;\n className?: string;\n}) {\n const box = React.useRef<HTMLDivElement>(null);\n React.useEffect(() => {\n // scroll only the list (scrollIntoView would also scroll the page behind a floating menu)\n const b = box.current;\n const row = b?.querySelector<HTMLElement>(`[data-idx=\"${active}\"]`);\n if (!b || !row) return;\n if (row.offsetTop < b.scrollTop) b.scrollTop = row.offsetTop - 6;\n else if (row.offsetTop + row.offsetHeight > b.scrollTop + b.clientHeight) b.scrollTop = row.offsetTop + row.offsetHeight - b.clientHeight + 6;\n }, [active]);\n return (\n <div ref={box} id={listId} role=\"listbox\" aria-label=\"Blocks\" className={cn(\"relative max-h-[320px] overflow-y-auto p-1.5\", className)}>\n {noMatch ? <div className=\"px-2 pb-1 pt-1.5 text-kit-sm text-kit-fg-3\">No match.</div> : null}\n {sections.map((s, si) => (\n <div\n key={s.title ?? `s${si}`}\n role=\"group\"\n aria-label={s.title ?? undefined}\n className={cn(!s.title && si > 0 && s.items[0]?.kind === \"folder\" && \"mt-1.5 border-t border-kit-line-subtle pt-1.5\")}\n >\n {s.title ? <div className=\"px-2 pb-0.5 pt-2 text-kit-2xs font-medium text-kit-fg-3\">{s.title}</div> : null}\n {s.items.map((it) => {\n const idx = flat.indexOf(it);\n const on = idx === active;\n const desc = detail && it.kind !== \"back\" && it.kind !== \"folder\" ? it.description : \"\";\n return (\n <button\n key={it.key}\n type=\"button\"\n role=\"option\"\n aria-selected={on}\n aria-description={!desc && it.description ? it.description : undefined}\n data-idx={idx}\n id={`${listId}-${idx}`}\n onMouseDown={(e) => e.preventDefault()}\n onMouseMove={() => !on && onHover(idx)}\n onClick={() => onPick(it)}\n className={cn(\n \"flex w-full items-center gap-2 rounded-kit-md px-2 text-left transition-colors motion-reduce:transition-none\",\n desc ? \"py-1.5\" : \"h-8\",\n it.kind === \"back\" ? \"text-kit-fg-2\" : \"text-kit-fg\",\n on && \"bg-kit-hover text-kit-fg\",\n )}\n >\n <span className={cn(\"flex shrink-0 items-center justify-center text-kit-fg-2 [&_svg]:size-4\", desc ? \"size-7 rounded-kit-md border border-kit-line bg-kit-control-bg\" : \"size-5\")}>\n {it.icon}\n </span>\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-kit-sm\">{it.label}</span>\n {desc ? <span className=\"block truncate text-kit-xs text-kit-fg-3\">{desc}</span> : null}\n </span>\n {it.count != null ? <span className=\"text-kit-2xs tabular-nums text-kit-fg-3\">{it.count}</span> : null}\n {it.kind === \"folder\" ? <IconChevronRight className=\"size-3.5 shrink-0 text-kit-fg-3\" stroke={1.5} aria-hidden /> : null}\n </button>\n );\n })}\n </div>\n ))}\n {empty ? <div className=\"px-2 py-6 text-center text-kit-sm text-kit-fg-3\">{emptyText}</div> : null}\n </div>\n );\n}\n\nconst Footer = () => (\n <div data-hint=\"shortcuts\" className=\"flex h-7 shrink-0 items-center border-t border-kit-line px-3 text-kit-2xs text-kit-fg-3\">\n <span>\n <kbd className=\"font-kit-sans\">↵</kbd> select · type to search\n </span>\n </div>\n);\n\n/** Shared keyboard model: ↑↓ wrap, Home/End, Enter / Tab, → into More components. Returns true when handled. */\nexport function catalogKey(\n e: React.KeyboardEvent | KeyboardEvent,\n flat: CatalogItem[],\n active: number,\n setActive: (i: number) => void,\n pick: (it: CatalogItem) => void,\n homeEnd = true,\n) {\n const n = flat.length;\n if (!n) return false;\n if (e.key === \"ArrowDown\") return setActive((active + 1) % n), true;\n if (e.key === \"ArrowUp\") return setActive((active - 1 + n) % n), true;\n if (homeEnd && e.key === \"Home\") return setActive(0), true;\n if (homeEnd && e.key === \"End\") return setActive(n - 1), true;\n if (e.key === \"ArrowRight\" && flat[active]?.kind === \"folder\") return pick(flat[active]), true;\n if (e.key === \"Enter\" || e.key === \"Tab\") {\n const it = flat[active];\n if (it) pick(it);\n return true;\n }\n return false;\n}\n\n/* ── Insert menu (popover content for the toolbar \"Insert\" and the gutter \"+\") ─ */\nexport function InsertMenuContent({\n onInsert,\n allows,\n keepFocusOnClose,\n align = \"start\",\n side = \"bottom\",\n}: {\n onInsert: (it: CatalogItem) => void;\n allows: (f: RichTextFeature) => boolean;\n /** after an insert the caret stays in the editor: the menu gives no focus back to its trigger */\n keepFocusOnClose?: () => boolean;\n align?: \"start\" | \"center\" | \"end\";\n side?: \"top\" | \"bottom\" | \"left\" | \"right\";\n}) {\n const [q, setQ] = React.useState(\"\");\n const [active, setActive] = React.useState(0);\n const input = React.useRef<HTMLInputElement>(null);\n const listId = React.useId();\n const [folder, setFolder] = React.useState(false);\n const { sections, flat, empty, emptyText, noMatch } = useCatalog(q, allows, folder);\n // in More components the first row is the way back: the first component starts active\n React.useEffect(() => setActive(folder && flat.length > 1 ? 1 : 0), [q, folder]); // eslint-disable-line react-hooks/exhaustive-deps\n const leave = () => {\n setFolder(false);\n setQ(\"\");\n input.current?.focus();\n };\n const pick = (it: CatalogItem) => {\n if (it.kind === \"folder\") {\n setFolder(true);\n setQ(\"\");\n input.current?.focus();\n } else if (it.kind === \"back\") leave();\n else onInsert(it);\n };\n return (\n <PopoverContent\n side={side}\n align={align}\n className=\"flex max-h-[420px] w-[320px] flex-col overflow-hidden p-0\"\n initialFocus={input}\n finalFocus={keepFocusOnClose ? () => !keepFocusOnClose() : undefined}\n // Esc in More components steps back to every block, wherever focus sits in the menu (the list scrolls, so the\n // browser can focus it): caught before the popover's own Esc, which would close it\n onKeyDownCapture={(e) => {\n if (!folder || e.key !== \"Escape\") return;\n e.preventDefault();\n e.stopPropagation();\n leave();\n }}\n >\n <div className=\"flex h-10 shrink-0 items-center gap-2 border-b border-kit-line px-3\">\n <IconSearch className=\"size-4 shrink-0 text-kit-fg-3\" stroke={1.5} aria-hidden />\n <input\n ref={input}\n value={q}\n onChange={(e) => setQ(e.target.value)}\n onKeyDown={(e) => {\n if (folder && e.key === \"Backspace\" && !q) {\n // ⌫ in an empty search steps back to every block (Esc: the popup's capture above)\n e.preventDefault();\n e.stopPropagation();\n leave();\n } else if (catalogKey(e, flat, active, setActive, pick, !q)) e.preventDefault();\n }}\n role=\"combobox\"\n aria-expanded\n aria-controls={listId}\n aria-activedescendant={flat.length ? `${listId}-${active}` : undefined}\n aria-label={folder ? \"Search blocks\" : \"Search or pick a block\"}\n placeholder={folder ? \"Search blocks\" : \"Search or pick a block\"}\n className=\"h-full min-w-0 flex-1 bg-transparent text-kit-sm text-kit-fg outline-none placeholder:text-kit-fg-3 focus-visible:shadow-none\"\n />\n </div>\n <CatalogList\n sections={sections}\n flat={flat}\n active={active}\n onHover={setActive}\n onPick={pick}\n empty={empty}\n emptyText={emptyText}\n noMatch={noMatch}\n detail={!!q || folder}\n listId={listId}\n className=\"min-h-0 flex-1\"\n />\n <Footer />\n </PopoverContent>\n );\n}\n\n/* ── Slash menu panel (positioned by the editor; never takes focus) ── */\nexport function SlashMenuPanel({\n query,\n folder = false,\n active,\n setActive,\n onPick,\n allows,\n maxHeight,\n}: {\n /** the room the editor found for the menu (collision-aware); the list scrolls inside it */\n maxHeight?: number;\n query: string;\n /** the More components view (the editor keeps it with the rest of the slash state) */\n folder?: boolean;\n active: number;\n setActive: (i: number) => void;\n onPick: (it: CatalogItem) => void;\n allows: (f: RichTextFeature) => boolean;\n}) {\n const listId = React.useId();\n const { sections, flat, empty, emptyText, noMatch } = useCatalog(query, allows, folder);\n return (\n <div\n data-kit-scope=\"\"\n className=\"glass-strong flex w-[320px] flex-col overflow-hidden rounded-kit-xl border border-kit-line\"\n style={maxHeight ? { maxHeight } : undefined}\n onMouseDown={(e) => e.preventDefault()}\n >\n <div className=\"flex h-8 shrink-0 items-center gap-2 border-b border-kit-line px-3 text-kit-sm\">\n <span className={cn(\"flex-1 truncate\", query ? \"text-kit-fg\" : \"text-kit-fg-3\")}>{query ? `${folder ? \"More blocks · \" : \"\"}/${query}` : folder ? \"More blocks\" : \"Type to search\"}</span>\n <kbd data-hint=\"shortcuts\" className=\"font-kit-sans text-kit-2xs text-kit-fg-3\">\n esc\n </kbd>\n </div>\n <CatalogList\n sections={sections}\n flat={flat}\n active={active}\n onHover={setActive}\n onPick={onPick}\n empty={empty}\n emptyText={emptyText}\n noMatch={noMatch}\n detail={!!query || folder}\n listId={listId}\n className=\"max-h-[300px] min-h-0 flex-1\"\n />\n <Footer />\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import type { FieldPath, FieldSchema } from "../../schema/types";
|
|
14
14
|
import type { UnknownValue } from "../unknown-value";
|
|
15
15
|
/** What a field may hold, in the editor's vocabulary. The editor offers nothing outside it. */
|
|
16
|
-
export type RichTextFeature = "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "blockquote" | "ul" | "ol" | "pre" | "hr" | "bold" | "italic" | "underline" | "strike" | "sup" | "sub" | "code" | "link" | "image" | "table" | "pullquote" | "takeaways" | "callout" | "toggle" | "video" | "file" | "checklist" | "button" | "embedAspect";
|
|
16
|
+
export type RichTextFeature = "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "blockquote" | "ul" | "ol" | "pre" | "hr" | "bold" | "italic" | "underline" | "strike" | "sup" | "sub" | "code" | "link" | "image" | "table" | "pullquote" | "takeaways" | "callout" | "toggle" | "video" | "file" | "checklist" | "button" | "embed" | "gallery" | "toc" | "footnote" | "columns" | "author" | "related" | "embedAspect";
|
|
17
17
|
export declare const ALL_FEATURES: readonly RichTextFeature[];
|
|
18
18
|
export interface RichTextRead {
|
|
19
19
|
/** the HTML the editor opens on */
|
|
@@ -33,6 +33,15 @@ export interface RichTextRead {
|
|
|
33
33
|
* the editor with change-to and remove (H4; see `remap`). Absent or empty = none.
|
|
34
34
|
*/
|
|
35
35
|
undeclared?: UnknownValue[];
|
|
36
|
+
/**
|
|
37
|
+
* Where an author bio and related posts point (plan RT8), from the field's config: the authors and posts collections,
|
|
38
|
+
* and the entry being edited (left out of the automatic related modes). Author bio is offered only with `authors`.
|
|
39
|
+
*/
|
|
40
|
+
references?: {
|
|
41
|
+
authors?: string;
|
|
42
|
+
posts?: string;
|
|
43
|
+
entryId?: string;
|
|
44
|
+
};
|
|
36
45
|
}
|
|
37
46
|
export interface RichTextWrite {
|
|
38
47
|
/** the value to store, in the surface's own shape */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codec.js","names":[],"sources":["../../../src/controls/rich-text/codec.ts"],"sourcesContent":["/**\n * RICH TEXT · the storage contract between the kit editor and its host.\n *\n * The V2 editor edits HTML (contentEditable). Production stores Portable Text, in the shape the classic editor of the\n * same surface writes (a block array for an entry field, the `{ content, html }` envelope for a component prop). The\n * conversion lives in the host, which owns the real converters, schema conformance and the field's Accept config; the\n * kit never imports them. A host with no codec gets its own control for rich text (LegacyFallback), never a guess.\n *\n * The control opens a value only when it survives the trip editor → codec unchanged (`same`), so nothing the HTML\n * editor cannot hold (a checklist's ticks, a link's rel, an image's layout) is dropped by a first keystroke: such a\n * value is edited by the host's own control instead, with the reason named.\n */\nimport type { FieldPath, FieldSchema } from \"../../schema/types\";\nimport type { UnknownValue } from \"../unknown-value\";\n\n/** What a field may hold, in the editor's vocabulary. The editor offers nothing outside it. */\nexport type RichTextFeature =\n | \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\"\n | \"blockquote\" | \"ul\" | \"ol\" | \"pre\" | \"hr\"\n | \"bold\" | \"italic\" | \"underline\" | \"strike\" | \"sup\" | \"sub\" | \"code\"\n | \"link\" | \"image\"\n /* blocks, each stored as its own Portable Text block (see dom.ts) */\n | \"table\" | \"pullquote\" | \"takeaways\" | \"callout\" | \"toggle\" | \"video\" | \"file\"\n /* a task list (PT listItem \"check\" + checked) and a call-to-action link (the link markDef with button: true) */\n | \"checklist\" | \"button\"\n /* an embed's Shape (aspect): the host declares it once its stored embed keeps one (plan RT3) */\n | \"embedAspect\";\n\nexport const ALL_FEATURES: readonly RichTextFeature[] = [\n \"h1\", \"h2\", \"h3\", \"h4\", \"h5\", \"h6\", \"blockquote\", \"ul\", \"ol\", \"pre\", \"hr\", \"bold\", \"italic\", \"underline\", \"strike\", \"sup\", \"sub\", \"code\", \"link\", \"image\",\n \"table\", \"pullquote\", \"takeaways\", \"callout\", \"toggle\", \"video\", \"file\", \"checklist\", \"button\", \"embedAspect\",\n];\n\nexport interface RichTextRead {\n /** the HTML the editor opens on */\n html: string;\n /** the stored value as the classic editor would open it (compared against the editor's round trip) */\n seed: unknown;\n /** what this value already cannot carry from an earlier editor, in words (\"2 links\"); shown before the first edit */\n losses: string[];\n /** what the field accepts */\n features: RichTextFeature[];\n /** one paragraph, stored inline (allowMultipleParagraphs: false) */\n single: boolean;\n /** links may open in a new tab */\n newTab: boolean;\n /**\n * the styles, marks and lists the value holds that the field no longer declares: kept by every write, flagged above\n * the editor with change-to and remove (H4; see `remap`). Absent or empty = none.\n */\n undeclared?: UnknownValue[];\n}\n\nexport interface RichTextWrite {\n /** the value to store, in the surface's own shape */\n value: unknown;\n /** what the stored shape would drop from this HTML, in words; non-empty = the write is refused */\n losses: string[];\n}\n\nexport interface RichTextAt {\n field: FieldSchema;\n path: FieldPath;\n}\n\nexport interface KitRichTextCodec {\n read(value: unknown, at: RichTextAt): RichTextRead;\n /** `prev` is the stored value being replaced (its keys are reused so an edit does not re-key the document) */\n write(html: string, at: RichTextAt, prev: unknown): RichTextWrite;\n /** the same document, ignoring keys, span splits and storage wrapper */\n same(a: unknown, b: unknown): boolean;\n /** the stored value with one `undeclared` value (its `key`) changed to `to`, or removed (`to` null); the words stay */\n remap?(value: unknown, at: RichTextAt, key: string, to: string | null): unknown;\n}\n\ndeclare module \"../../context\" {\n interface KitServices {\n /**\n * Stored rich text ↔ the HTML the kit editor edits (see controls/rich-text/codec.ts). Absent → a rich text field\n * draws the host's own control for it (LegacyControl).\n */\n richText?: KitRichTextCodec;\n }\n}\n"],"mappings":";
|
|
1
|
+
{"version":3,"file":"codec.js","names":[],"sources":["../../../src/controls/rich-text/codec.ts"],"sourcesContent":["/**\n * RICH TEXT · the storage contract between the kit editor and its host.\n *\n * The V2 editor edits HTML (contentEditable). Production stores Portable Text, in the shape the classic editor of the\n * same surface writes (a block array for an entry field, the `{ content, html }` envelope for a component prop). The\n * conversion lives in the host, which owns the real converters, schema conformance and the field's Accept config; the\n * kit never imports them. A host with no codec gets its own control for rich text (LegacyFallback), never a guess.\n *\n * The control opens a value only when it survives the trip editor → codec unchanged (`same`), so nothing the HTML\n * editor cannot hold (a checklist's ticks, a link's rel, an image's layout) is dropped by a first keystroke: such a\n * value is edited by the host's own control instead, with the reason named.\n */\nimport type { FieldPath, FieldSchema } from \"../../schema/types\";\nimport type { UnknownValue } from \"../unknown-value\";\n\n/** What a field may hold, in the editor's vocabulary. The editor offers nothing outside it. */\nexport type RichTextFeature =\n | \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\"\n | \"blockquote\" | \"ul\" | \"ol\" | \"pre\" | \"hr\"\n | \"bold\" | \"italic\" | \"underline\" | \"strike\" | \"sup\" | \"sub\" | \"code\"\n | \"link\" | \"image\"\n /* blocks, each stored as its own Portable Text block (see dom.ts) */\n | \"table\" | \"pullquote\" | \"takeaways\" | \"callout\" | \"toggle\" | \"video\" | \"file\"\n /* a task list (PT listItem \"check\" + checked) and a call-to-action link (the link markDef with button: true) */\n | \"checklist\" | \"button\"\n /* V2's article blocks, each a Portable Text block of its own (plan RT3–RT5): embed, gallery, table of contents */\n | \"embed\" | \"gallery\" | \"toc\"\n /* plan RT6–RT8: a footnote (inline, in the text), columns of text, an author bio and related posts (references) */\n | \"footnote\" | \"columns\" | \"author\" | \"related\"\n /* an embed's Shape (aspect): the host declares it once its stored embed keeps one (plan RT3) */\n | \"embedAspect\";\n\nexport const ALL_FEATURES: readonly RichTextFeature[] = [\n \"h1\", \"h2\", \"h3\", \"h4\", \"h5\", \"h6\", \"blockquote\", \"ul\", \"ol\", \"pre\", \"hr\", \"bold\", \"italic\", \"underline\", \"strike\", \"sup\", \"sub\", \"code\", \"link\", \"image\",\n \"table\", \"pullquote\", \"takeaways\", \"callout\", \"toggle\", \"video\", \"file\", \"checklist\", \"button\", \"embed\", \"gallery\", \"toc\", \"footnote\", \"columns\", \"author\", \"related\", \"embedAspect\",\n];\n\nexport interface RichTextRead {\n /** the HTML the editor opens on */\n html: string;\n /** the stored value as the classic editor would open it (compared against the editor's round trip) */\n seed: unknown;\n /** what this value already cannot carry from an earlier editor, in words (\"2 links\"); shown before the first edit */\n losses: string[];\n /** what the field accepts */\n features: RichTextFeature[];\n /** one paragraph, stored inline (allowMultipleParagraphs: false) */\n single: boolean;\n /** links may open in a new tab */\n newTab: boolean;\n /**\n * the styles, marks and lists the value holds that the field no longer declares: kept by every write, flagged above\n * the editor with change-to and remove (H4; see `remap`). Absent or empty = none.\n */\n undeclared?: UnknownValue[];\n /**\n * Where an author bio and related posts point (plan RT8), from the field's config: the authors and posts collections,\n * and the entry being edited (left out of the automatic related modes). Author bio is offered only with `authors`.\n */\n references?: { authors?: string; posts?: string; entryId?: string };\n}\n\nexport interface RichTextWrite {\n /** the value to store, in the surface's own shape */\n value: unknown;\n /** what the stored shape would drop from this HTML, in words; non-empty = the write is refused */\n losses: string[];\n}\n\nexport interface RichTextAt {\n field: FieldSchema;\n path: FieldPath;\n}\n\nexport interface KitRichTextCodec {\n read(value: unknown, at: RichTextAt): RichTextRead;\n /** `prev` is the stored value being replaced (its keys are reused so an edit does not re-key the document) */\n write(html: string, at: RichTextAt, prev: unknown): RichTextWrite;\n /** the same document, ignoring keys, span splits and storage wrapper */\n same(a: unknown, b: unknown): boolean;\n /** the stored value with one `undeclared` value (its `key`) changed to `to`, or removed (`to` null); the words stay */\n remap?(value: unknown, at: RichTextAt, key: string, to: string | null): unknown;\n}\n\ndeclare module \"../../context\" {\n interface KitServices {\n /**\n * Stored rich text ↔ the HTML the kit editor edits (see controls/rich-text/codec.ts). Absent → a rich text field\n * draws the host's own control for it (LegacyControl).\n */\n richText?: KitRichTextCodec;\n }\n}\n"],"mappings":";AAgCA,IAAa,eAA2C;CACtD;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;CAAc;CAAM;CAAM;CAAO;CAAM;CAAQ;CAAU;CAAa;CAAU;CAAO;CAAO;CAAQ;CAAQ;CAClJ;CAAS;CAAa;CAAa;CAAW;CAAU;CAAS;CAAQ;CAAa;CAAU;CAAS;CAAW;CAAO;CAAY;CAAW;CAAU;CAAW;AACzK"}
|
|
@@ -238,6 +238,7 @@ function CodecRichText({ codec, field, value, onChange, readOnly, path, id, inva
|
|
|
238
238
|
id,
|
|
239
239
|
invalid: invalid || !!refusal,
|
|
240
240
|
features: read.features,
|
|
241
|
+
references: read.references,
|
|
241
242
|
single: read.single,
|
|
242
243
|
allowNewTab: read.newTab,
|
|
243
244
|
components: insertable && !read.single && !readOnly ? insertable : void 0,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"control.js","names":[],"sources":["../../../src/controls/rich-text/control.tsx"],"sourcesContent":["/**\n * Rich text field (V2 HEAD controls/richtext.tsx, CPO D49).\n * - The editor stays in place: the document variant is the full editor in the column, the panel variant (inspector)\n * the same editor height-capped.\n * - Expanding (the toolbar's expand button, ⌘↵, or the expand icon in the field's label row at rest) opens the shared\n * ExpandedEditorModal: breadcrumb \"Section › Item › Field\", word count, Done. Esc closes and focus returns.\n *\n * STORAGE. The editor edits HTML; the stored value stays what production stores for the field, through the host's\n * codec (KitServices.richText, see ./codec.ts):\n * - read: stored value → the HTML the editor opens on (+ what the field accepts)\n * - write: edited HTML → the stored value, in the surface's own shape; a write that would drop something is refused,\n * named, and nothing is stored\n * - probe: a value the editor cannot carry unchanged (open → no edit → write ≠ stored) is not opened here at all: the\n * host's own control edits it (LegacyFallback), with the reason above it. Opening never writes.\n */\nimport * as React from \"react\";\nimport { IconArrowsMaximize } from \"@tabler/icons-react\";\nimport { cn } from \"../../lib/cn\";\nimport { useForm, useKitEnv, useKitServices } from \"../../context\";\nimport { resolveFrames } from \"../../lib/frames\";\nimport { pathKey } from \"../../lib/path\";\nimport type { ControlProps } from \"../../types\";\nimport { Tooltip } from \"../../ui/overlay\";\nimport { LegacyFallback } from \"../../engine/kit-field\";\nimport type { KitRichTextCodec, RichTextRead } from \"./codec\";\nimport { editorRoundTrip } from \"./dom\";\nimport { RichTextEditor } from \"./editor\";\nimport { paragraphsHtml, plainParagraphs, usePlainRichText } from \"./plain\";\nimport { Textarea } from \"../../ui/input\";\nimport { TEXT_FOCUS } from \"../text\";\nimport { UnknownValueNote } from \"../unknown-value\";\n\n/* ── which rich-text field (docKey) has its full editor open — shared with the label-row action ── */\nlet openKey: string | null = null;\nconst subs = new Set<() => void>();\nexport function setFullEditor(key: string | null) {\n openKey = key;\n subs.forEach((s) => s());\n}\nfunction useFullEditorKey() {\n return React.useSyncExternalStore(\n (cb) => {\n subs.add(cb);\n return () => subs.delete(cb);\n },\n () => openKey,\n () => openKey,\n );\n}\n\n/* ── fields whose value went to the host's own control: the label-row expand button is not theirs to draw ── */\nconst handedOff = new Set<string>();\nfunction setHandedOff(key: string, on: boolean) {\n if (handedOff.has(key) === on) return;\n if (on) handedOff.add(key);\n else handedOff.delete(key);\n subs.forEach((s) => s());\n}\nfunction useHandedOff(key: string) {\n return React.useSyncExternalStore(\n (cb) => {\n subs.add(cb);\n return () => subs.delete(cb);\n },\n () => handedOff.has(key),\n () => handedOff.has(key),\n );\n}\n\n/** The expanded editor's breadcrumb: the form's frames (root › drilled items) and this field. */\nfunction useExpandCrumbs(fieldTitle: string): string[] {\n const form = useForm();\n const env = useKitEnv();\n if (!form) return [env.docTitle, fieldTitle].filter(Boolean) as string[];\n const { frames } = resolveFrames(form.schema, form.value, form.path, form.resolveSchema);\n const titles = frames.filter((f) => f.kind !== \"history\").map((f, i) => (i === 0 ? env.docTitle || f.title : f.title));\n return [...titles, fieldTitle];\n}\n\n/** A stored block array, or the `{ content }` / `{ format, value }` envelope around one, holding a pull quote block. */\nconst holdsPullQuote = (seed: unknown): boolean =>\n Array.isArray(seed)\n ? seed.some((b) => !!b && typeof b === \"object\" && (b as { _type?: unknown })._type === \"pullQuote\")\n : !!seed && typeof seed === \"object\" && holdsPullQuote((seed as { content?: unknown; value?: unknown }).content ?? (seed as { value?: unknown }).value);\n\n/** What the control decided for one stored value. */\ntype Opened =\n | { ok: true; read: RichTextRead; html: string }\n | { ok: false; reason: string };\n\n/**\n * Open a stored value: read it, then prove the editor carries it unchanged (load → serialize → write === stored).\n * A codec that throws, or a value that does not survive, is handed to the host's own control with the reason.\n */\nexport function openValue(codec: KitRichTextCodec, value: unknown, at: Parameters<KitRichTextCodec[\"read\"]>[1]): Opened {\n let read: RichTextRead;\n try {\n read = codec.read(value, at);\n } catch (err) {\n return { ok: false, reason: `the stored text could not be read (${err instanceof Error ? err.message : String(err)})` };\n }\n // a pull quote (V2 D55 article block) opens as the editor's pull-quote island, but only when the codec renders the\n // stored block as its markup in a field that offers it: a codec that renders nothing for it, or a field with no place\n // for one (a single paragraph), would lose it, so the standard editor edits it\n if (holdsPullQuote(read.seed) && (!/data-rt=\"pullquote\"/.test(read.html) || !read.features.includes(\"pullquote\")))\n return { ok: false, reason: \"it holds a pull quote, which the standard editor edits\" };\n // tables and pull quotes are islands (V2 TableBlock, PullQuote); the probe below proves they survive unchanged\n const html = editorRoundTrip(read.html);\n try {\n const back = codec.write(html, at, value);\n if (back.losses.length) return { ok: false, reason: `this editor would drop ${back.losses.join(\", \")}` };\n if (!codec.same(back.value, read.seed)) return { ok: false, reason: \"it holds formatting this editor cannot keep\" };\n } catch (err) {\n return { ok: false, reason: `the text could not be checked (${err instanceof Error ? err.message : String(err)})` };\n }\n return { ok: true, read, html };\n}\n\nexport function RichTextControl(props: ControlProps) {\n const { richText } = useKitServices();\n if (!richText) return <LegacyFallback {...props} />;\n return <CodecRichText codec={richText} {...props} />;\n}\n\nfunction CodecRichText({ codec, field, value, onChange, readOnly, path, id, invalid }: ControlProps & { codec: KitRichTextCodec }) {\n const env = useKitEnv();\n const docKey = `${env.docId}:${pathKey(path)}`;\n const open = useFullEditorKey() === docKey;\n const crumbs = useExpandCrumbs(field.title);\n const plainScope = usePlainRichText();\n const at = React.useMemo(() => ({ field, path }), [field, path]);\n // the components this field may embed (More components): a hook with one identity for the form's life, so calling it\n // only when the host provides it keeps the hook order fixed\n const { useInsertableComponents } = useKitServices();\n const insertable = useInsertableComponents?.(at);\n /* our own last write: its value comes back as `value`, and the editor keeps the HTML it emitted (no reload, no probe) */\n const mine = React.useRef<{ value: unknown; html: string } | null>(null);\n const [refusal, setRefusal] = React.useState<string | null>(null);\n const opened = React.useMemo<Opened>(() => {\n if (mine.current && mine.current.value === value) {\n try {\n return { ok: true, read: codec.read(value, at), html: mine.current.html };\n } catch {\n /* judged afresh below, which names the failure */\n }\n }\n return openValue(codec, value, at);\n // `at` changes identity with the field object; the field's content is what matters\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [codec, value, JSON.stringify(field), pathKey(path)]);\n\n // close when this field goes away (block deselected, drilled elsewhere)\n React.useEffect(\n () => () => {\n if (openKey === docKey) setFullEditor(null);\n setHandedOff(docKey, false);\n },\n [docKey],\n );\n React.useLayoutEffect(() => {\n setHandedOff(docKey, !opened.ok);\n if (!opened.ok && openKey === docKey) setFullEditor(null);\n }, [docKey, opened.ok]);\n\n if (!opened.ok)\n return (\n <div className=\"flex min-w-0 flex-col gap-1.5\">\n <p role=\"note\" data-slot=\"kit-richtext-handoff\" className=\"rounded-kit-md bg-kit-subtle px-2.5 py-1.5 text-kit-xs text-kit-fg-2\">\n Edited with the standard editor: {opened.reason}.\n </p>\n <LegacyFallback field={field} value={value} onChange={onChange} readOnly={readOnly} path={path} id={id} invalid={invalid} />\n </div>\n );\n\n const { read, html } = opened;\n const write = (next: string) => {\n let res;\n try {\n res = codec.write(next, at, value);\n } catch (err) {\n setRefusal(`Not saved: the text could not be stored (${err instanceof Error ? err.message : String(err)}). Undo the last change with ⌘Z; leaving the field puts back the saved text.`);\n return;\n }\n if (res.losses.length) {\n setRefusal(`Not saved: this field cannot keep ${res.losses.join(\", \")}. Undo the last change with ⌘Z; leaving the field puts back the saved text.`);\n return;\n }\n setRefusal(null);\n if (codec.same(res.value, value)) return;\n mine.current = { value: res.value, html: next };\n onChange(res.value);\n };\n\n // The Values popover (PlainRichTextScope): plain paragraphs are V2's growing textarea (its `text` control), written\n // back through the same codec, one `<p>` per line. Anything richer keeps the editor below.\n const plain = plainScope && !read.losses.length ? plainParagraphs(html) : null;\n if (plain !== null)\n return (\n <div className=\"flex min-w-0 flex-col gap-1.5\">\n <Textarea\n id={id}\n rows={3}\n value={plain}\n readOnly={readOnly}\n aria-invalid={invalid || !!refusal}\n placeholder={field.placeholder}\n onChange={(e) => write(paragraphsHtml(e.target.value))}\n className={cn(TEXT_FOCUS, \"leading-5\")}\n />\n {refusal ? (\n <p role=\"alert\" className=\"text-kit-xs text-kit-danger-fg\">\n {refusal}\n </p>\n ) : null}\n </div>\n );\n\n /* V2 richtext.tsx:57: the editor sits in a plain <div>; the dash-only notes keep the 6px gap as their own margin */\n return (\n <div>\n {read.losses.length ? (\n <p role=\"status\" className=\"mb-1.5 rounded-kit-md bg-kit-warning-soft px-2.5 py-1.5 text-kit-xs text-kit-warning-fg\">\n Saved by an earlier editor. Editing here would drop {read.losses.join(\", \")}. Nothing changes until you edit.\n </p>\n ) : null}\n {read.undeclared?.length && codec.remap ? (\n <div className=\"mb-1.5\">\n <UnknownValueNote\n items={read.undeclared}\n onRemap={(key, to) => onChange(codec.remap!(value, at, key, to))}\n onRemove={(key) => onChange(codec.remap!(value, at, key, null))}\n readOnly={readOnly}\n />\n </div>\n ) : null}\n <RichTextEditor\n value={html}\n onChange={write}\n readOnly={readOnly}\n placeholder={field.placeholder}\n docKey={docKey}\n id={id}\n invalid={invalid || !!refusal}\n features={read.features}\n single={read.single}\n allowNewTab={read.newTab}\n components={insertable && !read.single && !readOnly ? insertable : undefined}\n variant={env.variant === \"document\" ? \"document\" : \"panel\"}\n maxHeight={env.variant === \"document\" ? undefined : 320}\n expandCrumbs={crumbs}\n expandedOpen={open}\n onExpandedOpenChange={(o) => setFullEditor(o ? docKey : null)}\n // select words → Comment in the toolbar: the host's comment popover opens on this field, quoting them\n onComment={env.onComment ? (quote) => env.onComment?.(path, field, quote) : undefined}\n />\n {refusal ? (\n <p role=\"alert\" className=\"mt-1.5 text-kit-xs text-kit-danger-fg\">\n {refusal}\n </p>\n ) : null}\n </div>\n );\n}\n\n/** Label-row action (at rest): the field's toolbar holds the same button while it's open. */\nfunction readsPlain(codec: KitRichTextCodec, value: unknown, field: ControlProps[\"field\"], path: ControlProps[\"path\"]) {\n try {\n const read = codec.read(value, { field, path });\n return !read.losses.length && plainParagraphs(editorRoundTrip(read.html)) !== null;\n } catch {\n return false; // the control names the failure itself\n }\n}\n\nexport function RichTextExpandAction({ path, field, value }: ControlProps) {\n const env = useKitEnv();\n const { richText } = useKitServices();\n const handed = useHandedOff(`${env.docId}:${pathKey(path)}`);\n const plainScope = usePlainRichText();\n // the host's own control (no codec, or a value handed to it) has its own full-screen button\n if (env.variant === \"document\" || !richText || handed) return null;\n // drawn as a plain textarea (V2's `text` control has no ⤢)\n if (plainScope && readsPlain(richText, value, field, path)) return null;\n return (\n <Tooltip content=\"Expand editor\" shortcut=\"⌘↵\">\n <button\n type=\"button\"\n aria-label={`Expand ${field.title}`}\n onClick={() => setFullEditor(`${env.docId}:${pathKey(path)}`)}\n className={cn(\n \"-my-0.5 hidden size-6 items-center justify-center rounded-kit-md text-kit-fg-3 hover:bg-kit-hover hover:text-kit-fg\",\n \"group-hover/field:inline-flex group-focus-within/field:!hidden [@media(pointer:coarse)]:inline-flex\",\n )}\n >\n <IconArrowsMaximize className=\"size-3.5\" stroke={1.5} aria-hidden />\n </button>\n </Tooltip>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"control.js","names":[],"sources":["../../../src/controls/rich-text/control.tsx"],"sourcesContent":["/**\n * Rich text field (V2 HEAD controls/richtext.tsx, CPO D49).\n * - The editor stays in place: the document variant is the full editor in the column, the panel variant (inspector)\n * the same editor height-capped.\n * - Expanding (the toolbar's expand button, ⌘↵, or the expand icon in the field's label row at rest) opens the shared\n * ExpandedEditorModal: breadcrumb \"Section › Item › Field\", word count, Done. Esc closes and focus returns.\n *\n * STORAGE. The editor edits HTML; the stored value stays what production stores for the field, through the host's\n * codec (KitServices.richText, see ./codec.ts):\n * - read: stored value → the HTML the editor opens on (+ what the field accepts)\n * - write: edited HTML → the stored value, in the surface's own shape; a write that would drop something is refused,\n * named, and nothing is stored\n * - probe: a value the editor cannot carry unchanged (open → no edit → write ≠ stored) is not opened here at all: the\n * host's own control edits it (LegacyFallback), with the reason above it. Opening never writes.\n */\nimport * as React from \"react\";\nimport { IconArrowsMaximize } from \"@tabler/icons-react\";\nimport { cn } from \"../../lib/cn\";\nimport { useForm, useKitEnv, useKitServices } from \"../../context\";\nimport { resolveFrames } from \"../../lib/frames\";\nimport { pathKey } from \"../../lib/path\";\nimport type { ControlProps } from \"../../types\";\nimport { Tooltip } from \"../../ui/overlay\";\nimport { LegacyFallback } from \"../../engine/kit-field\";\nimport type { KitRichTextCodec, RichTextRead } from \"./codec\";\nimport { editorRoundTrip } from \"./dom\";\nimport { RichTextEditor } from \"./editor\";\nimport { paragraphsHtml, plainParagraphs, usePlainRichText } from \"./plain\";\nimport { Textarea } from \"../../ui/input\";\nimport { TEXT_FOCUS } from \"../text\";\nimport { UnknownValueNote } from \"../unknown-value\";\n\n/* ── which rich-text field (docKey) has its full editor open — shared with the label-row action ── */\nlet openKey: string | null = null;\nconst subs = new Set<() => void>();\nexport function setFullEditor(key: string | null) {\n openKey = key;\n subs.forEach((s) => s());\n}\nfunction useFullEditorKey() {\n return React.useSyncExternalStore(\n (cb) => {\n subs.add(cb);\n return () => subs.delete(cb);\n },\n () => openKey,\n () => openKey,\n );\n}\n\n/* ── fields whose value went to the host's own control: the label-row expand button is not theirs to draw ── */\nconst handedOff = new Set<string>();\nfunction setHandedOff(key: string, on: boolean) {\n if (handedOff.has(key) === on) return;\n if (on) handedOff.add(key);\n else handedOff.delete(key);\n subs.forEach((s) => s());\n}\nfunction useHandedOff(key: string) {\n return React.useSyncExternalStore(\n (cb) => {\n subs.add(cb);\n return () => subs.delete(cb);\n },\n () => handedOff.has(key),\n () => handedOff.has(key),\n );\n}\n\n/** The expanded editor's breadcrumb: the form's frames (root › drilled items) and this field. */\nfunction useExpandCrumbs(fieldTitle: string): string[] {\n const form = useForm();\n const env = useKitEnv();\n if (!form) return [env.docTitle, fieldTitle].filter(Boolean) as string[];\n const { frames } = resolveFrames(form.schema, form.value, form.path, form.resolveSchema);\n const titles = frames.filter((f) => f.kind !== \"history\").map((f, i) => (i === 0 ? env.docTitle || f.title : f.title));\n return [...titles, fieldTitle];\n}\n\n/** A stored block array, or the `{ content }` / `{ format, value }` envelope around one, holding a pull quote block. */\nconst holdsPullQuote = (seed: unknown): boolean =>\n Array.isArray(seed)\n ? seed.some((b) => !!b && typeof b === \"object\" && (b as { _type?: unknown })._type === \"pullQuote\")\n : !!seed && typeof seed === \"object\" && holdsPullQuote((seed as { content?: unknown; value?: unknown }).content ?? (seed as { value?: unknown }).value);\n\n/** What the control decided for one stored value. */\ntype Opened =\n | { ok: true; read: RichTextRead; html: string }\n | { ok: false; reason: string };\n\n/**\n * Open a stored value: read it, then prove the editor carries it unchanged (load → serialize → write === stored).\n * A codec that throws, or a value that does not survive, is handed to the host's own control with the reason.\n */\nexport function openValue(codec: KitRichTextCodec, value: unknown, at: Parameters<KitRichTextCodec[\"read\"]>[1]): Opened {\n let read: RichTextRead;\n try {\n read = codec.read(value, at);\n } catch (err) {\n return { ok: false, reason: `the stored text could not be read (${err instanceof Error ? err.message : String(err)})` };\n }\n // a pull quote (V2 D55 article block) opens as the editor's pull-quote island, but only when the codec renders the\n // stored block as its markup in a field that offers it: a codec that renders nothing for it, or a field with no place\n // for one (a single paragraph), would lose it, so the standard editor edits it\n if (holdsPullQuote(read.seed) && (!/data-rt=\"pullquote\"/.test(read.html) || !read.features.includes(\"pullquote\")))\n return { ok: false, reason: \"it holds a pull quote, which the standard editor edits\" };\n // tables and pull quotes are islands (V2 TableBlock, PullQuote); the probe below proves they survive unchanged\n const html = editorRoundTrip(read.html);\n try {\n const back = codec.write(html, at, value);\n if (back.losses.length) return { ok: false, reason: `this editor would drop ${back.losses.join(\", \")}` };\n if (!codec.same(back.value, read.seed)) return { ok: false, reason: \"it holds formatting this editor cannot keep\" };\n } catch (err) {\n return { ok: false, reason: `the text could not be checked (${err instanceof Error ? err.message : String(err)})` };\n }\n return { ok: true, read, html };\n}\n\nexport function RichTextControl(props: ControlProps) {\n const { richText } = useKitServices();\n if (!richText) return <LegacyFallback {...props} />;\n return <CodecRichText codec={richText} {...props} />;\n}\n\nfunction CodecRichText({ codec, field, value, onChange, readOnly, path, id, invalid }: ControlProps & { codec: KitRichTextCodec }) {\n const env = useKitEnv();\n const docKey = `${env.docId}:${pathKey(path)}`;\n const open = useFullEditorKey() === docKey;\n const crumbs = useExpandCrumbs(field.title);\n const plainScope = usePlainRichText();\n const at = React.useMemo(() => ({ field, path }), [field, path]);\n // the components this field may embed (More components): a hook with one identity for the form's life, so calling it\n // only when the host provides it keeps the hook order fixed\n const { useInsertableComponents } = useKitServices();\n const insertable = useInsertableComponents?.(at);\n /* our own last write: its value comes back as `value`, and the editor keeps the HTML it emitted (no reload, no probe) */\n const mine = React.useRef<{ value: unknown; html: string } | null>(null);\n const [refusal, setRefusal] = React.useState<string | null>(null);\n const opened = React.useMemo<Opened>(() => {\n if (mine.current && mine.current.value === value) {\n try {\n return { ok: true, read: codec.read(value, at), html: mine.current.html };\n } catch {\n /* judged afresh below, which names the failure */\n }\n }\n return openValue(codec, value, at);\n // `at` changes identity with the field object; the field's content is what matters\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [codec, value, JSON.stringify(field), pathKey(path)]);\n\n // close when this field goes away (block deselected, drilled elsewhere)\n React.useEffect(\n () => () => {\n if (openKey === docKey) setFullEditor(null);\n setHandedOff(docKey, false);\n },\n [docKey],\n );\n React.useLayoutEffect(() => {\n setHandedOff(docKey, !opened.ok);\n if (!opened.ok && openKey === docKey) setFullEditor(null);\n }, [docKey, opened.ok]);\n\n if (!opened.ok)\n return (\n <div className=\"flex min-w-0 flex-col gap-1.5\">\n <p role=\"note\" data-slot=\"kit-richtext-handoff\" className=\"rounded-kit-md bg-kit-subtle px-2.5 py-1.5 text-kit-xs text-kit-fg-2\">\n Edited with the standard editor: {opened.reason}.\n </p>\n <LegacyFallback field={field} value={value} onChange={onChange} readOnly={readOnly} path={path} id={id} invalid={invalid} />\n </div>\n );\n\n const { read, html } = opened;\n const write = (next: string) => {\n let res;\n try {\n res = codec.write(next, at, value);\n } catch (err) {\n setRefusal(`Not saved: the text could not be stored (${err instanceof Error ? err.message : String(err)}). Undo the last change with ⌘Z; leaving the field puts back the saved text.`);\n return;\n }\n if (res.losses.length) {\n setRefusal(`Not saved: this field cannot keep ${res.losses.join(\", \")}. Undo the last change with ⌘Z; leaving the field puts back the saved text.`);\n return;\n }\n setRefusal(null);\n if (codec.same(res.value, value)) return;\n mine.current = { value: res.value, html: next };\n onChange(res.value);\n };\n\n // The Values popover (PlainRichTextScope): plain paragraphs are V2's growing textarea (its `text` control), written\n // back through the same codec, one `<p>` per line. Anything richer keeps the editor below.\n const plain = plainScope && !read.losses.length ? plainParagraphs(html) : null;\n if (plain !== null)\n return (\n <div className=\"flex min-w-0 flex-col gap-1.5\">\n <Textarea\n id={id}\n rows={3}\n value={plain}\n readOnly={readOnly}\n aria-invalid={invalid || !!refusal}\n placeholder={field.placeholder}\n onChange={(e) => write(paragraphsHtml(e.target.value))}\n className={cn(TEXT_FOCUS, \"leading-5\")}\n />\n {refusal ? (\n <p role=\"alert\" className=\"text-kit-xs text-kit-danger-fg\">\n {refusal}\n </p>\n ) : null}\n </div>\n );\n\n /* V2 richtext.tsx:57: the editor sits in a plain <div>; the dash-only notes keep the 6px gap as their own margin */\n return (\n <div>\n {read.losses.length ? (\n <p role=\"status\" className=\"mb-1.5 rounded-kit-md bg-kit-warning-soft px-2.5 py-1.5 text-kit-xs text-kit-warning-fg\">\n Saved by an earlier editor. Editing here would drop {read.losses.join(\", \")}. Nothing changes until you edit.\n </p>\n ) : null}\n {read.undeclared?.length && codec.remap ? (\n <div className=\"mb-1.5\">\n <UnknownValueNote\n items={read.undeclared}\n onRemap={(key, to) => onChange(codec.remap!(value, at, key, to))}\n onRemove={(key) => onChange(codec.remap!(value, at, key, null))}\n readOnly={readOnly}\n />\n </div>\n ) : null}\n <RichTextEditor\n value={html}\n onChange={write}\n readOnly={readOnly}\n placeholder={field.placeholder}\n docKey={docKey}\n id={id}\n invalid={invalid || !!refusal}\n features={read.features}\n references={read.references}\n single={read.single}\n allowNewTab={read.newTab}\n components={insertable && !read.single && !readOnly ? insertable : undefined}\n variant={env.variant === \"document\" ? \"document\" : \"panel\"}\n maxHeight={env.variant === \"document\" ? undefined : 320}\n expandCrumbs={crumbs}\n expandedOpen={open}\n onExpandedOpenChange={(o) => setFullEditor(o ? docKey : null)}\n // select words → Comment in the toolbar: the host's comment popover opens on this field, quoting them\n onComment={env.onComment ? (quote) => env.onComment?.(path, field, quote) : undefined}\n />\n {refusal ? (\n <p role=\"alert\" className=\"mt-1.5 text-kit-xs text-kit-danger-fg\">\n {refusal}\n </p>\n ) : null}\n </div>\n );\n}\n\n/** Label-row action (at rest): the field's toolbar holds the same button while it's open. */\nfunction readsPlain(codec: KitRichTextCodec, value: unknown, field: ControlProps[\"field\"], path: ControlProps[\"path\"]) {\n try {\n const read = codec.read(value, { field, path });\n return !read.losses.length && plainParagraphs(editorRoundTrip(read.html)) !== null;\n } catch {\n return false; // the control names the failure itself\n }\n}\n\nexport function RichTextExpandAction({ path, field, value }: ControlProps) {\n const env = useKitEnv();\n const { richText } = useKitServices();\n const handed = useHandedOff(`${env.docId}:${pathKey(path)}`);\n const plainScope = usePlainRichText();\n // the host's own control (no codec, or a value handed to it) has its own full-screen button\n if (env.variant === \"document\" || !richText || handed) return null;\n // drawn as a plain textarea (V2's `text` control has no ⤢)\n if (plainScope && readsPlain(richText, value, field, path)) return null;\n return (\n <Tooltip content=\"Expand editor\" shortcut=\"⌘↵\">\n <button\n type=\"button\"\n aria-label={`Expand ${field.title}`}\n onClick={() => setFullEditor(`${env.docId}:${pathKey(path)}`)}\n className={cn(\n \"-my-0.5 hidden size-6 items-center justify-center rounded-kit-md text-kit-fg-3 hover:bg-kit-hover hover:text-kit-fg\",\n \"group-hover/field:inline-flex group-focus-within/field:!hidden [@media(pointer:coarse)]:inline-flex\",\n )}\n >\n <IconArrowsMaximize className=\"size-3.5\" stroke={1.5} aria-hidden />\n </button>\n </Tooltip>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,IAAI,UAAyB;AAC7B,IAAM,uBAAO,IAAI,IAAgB;AACjC,SAAgB,cAAc,KAAoB;CAChD,UAAU;CACV,KAAK,SAAS,MAAM,EAAE,CAAC;AACzB;AACA,SAAS,mBAAmB;CAC1B,OAAO,MAAM,sBACV,OAAO;EACN,KAAK,IAAI,EAAE;EACX,aAAa,KAAK,OAAO,EAAE;CAC7B,SACM,eACA,OACR;AACF;AAGA,IAAM,4BAAY,IAAI,IAAY;AAClC,SAAS,aAAa,KAAa,IAAa;CAC9C,IAAI,UAAU,IAAI,GAAG,MAAM,IAAI;CAC/B,IAAI,IAAI,UAAU,IAAI,GAAG;MACpB,UAAU,OAAO,GAAG;CACzB,KAAK,SAAS,MAAM,EAAE,CAAC;AACzB;AACA,SAAS,aAAa,KAAa;CACjC,OAAO,MAAM,sBACV,OAAO;EACN,KAAK,IAAI,EAAE;EACX,aAAa,KAAK,OAAO,EAAE;CAC7B,SACM,UAAU,IAAI,GAAG,SACjB,UAAU,IAAI,GAAG,CACzB;AACF;;AAGA,SAAS,gBAAgB,YAA8B;CACrD,MAAM,OAAO,QAAQ;CACrB,MAAM,MAAM,UAAU;CACtB,IAAI,CAAC,MAAM,OAAO,CAAC,IAAI,UAAU,UAAU,CAAC,CAAC,OAAO,OAAO;CAC3D,MAAM,EAAE,WAAW,cAAc,KAAK,QAAQ,KAAK,OAAO,KAAK,MAAM,KAAK,aAAa;CAEvF,OAAO,CAAC,GADO,OAAO,QAAQ,MAAM,EAAE,SAAS,SAAS,CAAC,CAAC,KAAK,GAAG,MAAO,MAAM,IAAI,IAAI,YAAY,EAAE,QAAQ,EAAE,KACpG,GAAQ,UAAU;AAC/B;;AAGA,IAAM,kBAAkB,SACtB,MAAM,QAAQ,IAAI,IACd,KAAK,MAAM,MAAM,CAAC,CAAC,KAAK,OAAO,MAAM,YAAa,EAA0B,UAAU,WAAW,IACjG,CAAC,CAAC,QAAQ,OAAO,SAAS,YAAY,eAAgB,KAAgD,WAAY,KAA6B,KAAK;;;;;AAW1J,SAAgB,UAAU,OAAyB,OAAgB,IAAqD;CACtH,IAAI;CACJ,IAAI;EACF,OAAO,MAAM,KAAK,OAAO,EAAE;CAC7B,SAAS,KAAK;EACZ,OAAO;GAAE,IAAI;GAAO,QAAQ,sCAAsC,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE;EAAG;CACxH;CAIA,IAAI,eAAe,KAAK,IAAI,MAAM,CAAC,sBAAsB,KAAK,KAAK,IAAI,KAAK,CAAC,KAAK,SAAS,SAAS,WAAW,IAC7G,OAAO;EAAE,IAAI;EAAO,QAAQ;CAAyD;CAEvF,MAAM,OAAO,gBAAgB,KAAK,IAAI;CACtC,IAAI;EACF,MAAM,OAAO,MAAM,MAAM,MAAM,IAAI,KAAK;EACxC,IAAI,KAAK,OAAO,QAAQ,OAAO;GAAE,IAAI;GAAO,QAAQ,0BAA0B,KAAK,OAAO,KAAK,IAAI;EAAI;EACvG,IAAI,CAAC,MAAM,KAAK,KAAK,OAAO,KAAK,IAAI,GAAG,OAAO;GAAE,IAAI;GAAO,QAAQ;EAA8C;CACpH,SAAS,KAAK;EACZ,OAAO;GAAE,IAAI;GAAO,QAAQ,kCAAkC,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE;EAAG;CACpH;CACA,OAAO;EAAE,IAAI;EAAM;EAAM;CAAK;AAChC;AAEA,SAAgB,gBAAgB,OAAqB;CACnD,MAAM,EAAE,aAAa,eAAe;CACpC,IAAI,CAAC,UAAU,OAAO,oBAAC,gBAAD,EAAgB,GAAI,MAAQ,CAAA;CAClD,OAAO,oBAAC,eAAD;EAAe,OAAO;EAAU,GAAI;CAAQ,CAAA;AACrD;AAEA,SAAS,cAAc,EAAE,OAAO,OAAO,OAAO,UAAU,UAAU,MAAM,IAAI,WAAuD;CACjI,MAAM,MAAM,UAAU;CACtB,MAAM,SAAS,GAAG,IAAI,MAAM,GAAG,QAAQ,IAAI;CAC3C,MAAM,OAAO,iBAAiB,MAAM;CACpC,MAAM,SAAS,gBAAgB,MAAM,KAAK;CAC1C,MAAM,aAAa,iBAAiB;CACpC,MAAM,KAAK,MAAM,eAAe;EAAE;EAAO;CAAK,IAAI,CAAC,OAAO,IAAI,CAAC;CAG/D,MAAM,EAAE,4BAA4B,eAAe;CACnD,MAAM,aAAa,0BAA0B,EAAE;CAE/C,MAAM,OAAO,MAAM,OAAgD,IAAI;CACvE,MAAM,CAAC,SAAS,cAAc,MAAM,SAAwB,IAAI;CAChE,MAAM,SAAS,MAAM,cAAsB;EACzC,IAAI,KAAK,WAAW,KAAK,QAAQ,UAAU,OACzC,IAAI;GACF,OAAO;IAAE,IAAI;IAAM,MAAM,MAAM,KAAK,OAAO,EAAE;IAAG,MAAM,KAAK,QAAQ;GAAK;EAC1E,QAAQ,CAER;EAEF,OAAO,UAAU,OAAO,OAAO,EAAE;CAGnC,GAAG;EAAC;EAAO;EAAO,KAAK,UAAU,KAAK;EAAG,QAAQ,IAAI;CAAC,CAAC;CAGvD,MAAM,sBACQ;EACV,IAAI,YAAY,QAAQ,cAAc,IAAI;EAC1C,aAAa,QAAQ,KAAK;CAC5B,GACA,CAAC,MAAM,CACT;CACA,MAAM,sBAAsB;EAC1B,aAAa,QAAQ,CAAC,OAAO,EAAE;EAC/B,IAAI,CAAC,OAAO,MAAM,YAAY,QAAQ,cAAc,IAAI;CAC1D,GAAG,CAAC,QAAQ,OAAO,EAAE,CAAC;CAEtB,IAAI,CAAC,OAAO,IACV,OACE,qBAAC,OAAD;EAAK,WAAU;EAAf,UAAA,CACE,qBAAC,KAAD;GAAG,MAAK;GAAO,aAAU;GAAuB,WAAU;GAA1D,UAAA;IAAiI;IAC7F,OAAO;IAAO;GAC/C;EACH,CAAA,GAAA,oBAAC,gBAAD;GAAuB;GAAc;GAAiB;GAAoB;GAAgB;GAAU;GAAa;EAAU,CAAA,CACxH;;CAGT,MAAM,EAAE,MAAM,SAAS;CACvB,MAAM,SAAS,SAAiB;EAC9B,IAAI;EACJ,IAAI;GACF,MAAM,MAAM,MAAM,MAAM,IAAI,KAAK;EACnC,SAAS,KAAK;GACZ,WAAW,4CAA4C,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE,6EAA6E;GACrL;EACF;EACA,IAAI,IAAI,OAAO,QAAQ;GACrB,WAAW,qCAAqC,IAAI,OAAO,KAAK,IAAI,EAAE,4EAA4E;GAClJ;EACF;EACA,WAAW,IAAI;EACf,IAAI,MAAM,KAAK,IAAI,OAAO,KAAK,GAAG;EAClC,KAAK,UAAU;GAAE,OAAO,IAAI;GAAO,MAAM;EAAK;EAC9C,SAAS,IAAI,KAAK;CACpB;CAIA,MAAM,QAAQ,cAAc,CAAC,KAAK,OAAO,SAAS,gBAAgB,IAAI,IAAI;CAC1E,IAAI,UAAU,MACZ,OACE,qBAAC,OAAD;EAAK,WAAU;EAAf,UAAA,CACE,oBAAC,UAAD;GACM;GACJ,MAAM;GACN,OAAO;GACG;GACV,gBAAc,WAAW,CAAC,CAAC;GAC3B,aAAa,MAAM;GACnB,WAAW,MAAM,MAAM,eAAe,EAAE,OAAO,KAAK,CAAC;GACrD,WAAW,GAAG,YAAY,WAAW;EACtC,CAAA,GACA,UACC,oBAAC,KAAD;GAAG,MAAK;GAAQ,WAAU;GACvB,UAAA;EACA,CAAA,IACD,IACD;;CAIT,OACE,qBAAC,OAAD,EAAA,UAAA;EACG,KAAK,OAAO,SACX,qBAAC,KAAD;GAAG,MAAK;GAAS,WAAU;GAA3B,UAAA;IAAqH;IAC9D,KAAK,OAAO,KAAK,IAAI;IAAE;GAC3E;EACD,CAAA,IAAA;EACH,KAAK,YAAY,UAAU,MAAM,QAChC,oBAAC,OAAD;GAAK,WAAU;GACb,UAAA,oBAAC,kBAAD;IACE,OAAO,KAAK;IACZ,UAAU,KAAK,OAAO,SAAS,MAAM,MAAO,OAAO,IAAI,KAAK,EAAE,CAAC;IAC/D,WAAW,QAAQ,SAAS,MAAM,MAAO,OAAO,IAAI,KAAK,IAAI,CAAC;IACpD;GACX,CAAA;EACE,CAAA,IACH;EACJ,oBAAC,gBAAD;GACE,OAAO;GACP,UAAU;GACA;GACV,aAAa,MAAM;GACX;GACJ;GACJ,SAAS,WAAW,CAAC,CAAC;GACtB,UAAU,KAAK;GACf,YAAY,KAAK;GACjB,QAAQ,KAAK;GACb,aAAa,KAAK;GAClB,YAAY,cAAc,CAAC,KAAK,UAAU,CAAC,WAAW,aAAa,KAAA;GACnE,SAAS,IAAI,YAAY,aAAa,aAAa;GACnD,WAAW,IAAI,YAAY,aAAa,KAAA,IAAY;GACpD,cAAc;GACd,cAAc;GACd,uBAAuB,MAAM,cAAc,IAAI,SAAS,IAAI;GAE5D,WAAW,IAAI,aAAa,UAAU,IAAI,YAAY,MAAM,OAAO,KAAK,IAAI,KAAA;EAC7E,CAAA;EACA,UACC,oBAAC,KAAD;GAAG,MAAK;GAAQ,WAAU;GACvB,UAAA;EACA,CAAA,IACD;CACD,EAAA,CAAA;AAET;;AAGA,SAAS,WAAW,OAAyB,OAAgB,OAA8B,MAA4B;CACrH,IAAI;EACF,MAAM,OAAO,MAAM,KAAK,OAAO;GAAE;GAAO;EAAK,CAAC;EAC9C,OAAO,CAAC,KAAK,OAAO,UAAU,gBAAgB,gBAAgB,KAAK,IAAI,CAAC,MAAM;CAChF,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAgB,qBAAqB,EAAE,MAAM,OAAO,SAAuB;CACzE,MAAM,MAAM,UAAU;CACtB,MAAM,EAAE,aAAa,eAAe;CACpC,MAAM,SAAS,aAAa,GAAG,IAAI,MAAM,GAAG,QAAQ,IAAI,GAAG;CAC3D,MAAM,aAAa,iBAAiB;CAEpC,IAAI,IAAI,YAAY,cAAc,CAAC,YAAY,QAAQ,OAAO;CAE9D,IAAI,cAAc,WAAW,UAAU,OAAO,OAAO,IAAI,GAAG,OAAO;CACnE,OACE,oBAAC,SAAD;EAAS,SAAQ;EAAgB,UAAS;EACxC,UAAA,oBAAC,UAAD;GACE,MAAK;GACL,cAAY,UAAU,MAAM;GAC5B,eAAe,cAAc,GAAG,IAAI,MAAM,GAAG,QAAQ,IAAI,GAAG;GAC5D,WAAW,GACT,uHACA,qGACF;GAEA,UAAA,oBAAC,oBAAD;IAAoB,WAAU;IAAW,QAAQ;IAAK,eAAA;GAAa,CAAA;EAC7D,CAAA;CACD,CAAA;AAEb"}
|
|
@@ -58,9 +58,35 @@ export interface ImageModel {
|
|
|
58
58
|
align?: ImageAlign;
|
|
59
59
|
size?: ImageSize;
|
|
60
60
|
href?: string;
|
|
61
|
+
/** RT9: how much is cut from each edge, 0–1 of the source (image-url's crop) */
|
|
62
|
+
crop?: ImageCrop;
|
|
63
|
+
/** RT9: the subject, 0–1 of the source (image-url's focal) */
|
|
64
|
+
focal?: {
|
|
65
|
+
x: number;
|
|
66
|
+
y: number;
|
|
67
|
+
};
|
|
61
68
|
/** the stored block's other keys, opaque to the editor */
|
|
62
69
|
extra?: string;
|
|
63
70
|
}
|
|
71
|
+
/** RT9: image-url's crop, each edge a 0–1 fraction of the source. */
|
|
72
|
+
export interface ImageCrop {
|
|
73
|
+
top: number;
|
|
74
|
+
right: number;
|
|
75
|
+
bottom: number;
|
|
76
|
+
left: number;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* RT9: an image's framing as production writes it on the `<img>` (richtext richTextImageFrame): `data-src` the stored
|
|
80
|
+
* url when the drawn one carries the crop, `data-crop` "top,right,bottom,left" and `data-focal` "x,y", each 0–1.
|
|
81
|
+
*/
|
|
82
|
+
export declare function readImageFrame(img: Element | null): {
|
|
83
|
+
src: string;
|
|
84
|
+
crop?: ImageCrop;
|
|
85
|
+
focal?: {
|
|
86
|
+
x: number;
|
|
87
|
+
y: number;
|
|
88
|
+
};
|
|
89
|
+
};
|
|
64
90
|
/** A table island IS the kit's table value (V2 TableModel); only what production's table keeps is stored (see above). */
|
|
65
91
|
export type TableModel = TableValue;
|
|
66
92
|
/** V2 PullQuoteModel (article.ts). */
|
|
@@ -100,6 +126,8 @@ export interface EmbedModel {
|
|
|
100
126
|
url?: string;
|
|
101
127
|
title?: string;
|
|
102
128
|
aspect?: "16:9" | "4:3" | "1:1" | "9:16";
|
|
129
|
+
/** the title is also the figure's aria-label, as production writes it (RT3) and a new embed does; kept as found */
|
|
130
|
+
labelled?: true;
|
|
103
131
|
extra?: string;
|
|
104
132
|
}
|
|
105
133
|
export type EmbedProvider = "youtube" | "vimeo" | "x" | "codepen" | "figma" | "maps" | "spotify" | "podcast" | "audio" | "link";
|
|
@@ -111,6 +139,52 @@ export interface EmbedInfo {
|
|
|
111
139
|
host: string;
|
|
112
140
|
}
|
|
113
141
|
export declare function detectEmbed(url: string): EmbedInfo;
|
|
142
|
+
/** V2's article Gallery (plan RT4): `<figure data-rt="gallery" data-layout><ul><li><figure><img src alt><figcaption>`. */
|
|
143
|
+
export interface GalleryIslandModel {
|
|
144
|
+
layout?: "grid" | "carousel";
|
|
145
|
+
/** `_key` is the editor's own (rows keep their identity while edited); src, alt and caption are what is stored */
|
|
146
|
+
images: {
|
|
147
|
+
_key: string;
|
|
148
|
+
src: string;
|
|
149
|
+
alt?: string;
|
|
150
|
+
caption?: string;
|
|
151
|
+
crop?: ImageCrop;
|
|
152
|
+
focal?: {
|
|
153
|
+
x: number;
|
|
154
|
+
y: number;
|
|
155
|
+
};
|
|
156
|
+
}[];
|
|
157
|
+
extra?: string;
|
|
158
|
+
}
|
|
159
|
+
/** V2's article Table of contents (plan RT5): `<nav data-rt="toc" data-depth><p data-title>`; the list is derived. */
|
|
160
|
+
export interface TocIslandModel {
|
|
161
|
+
title?: string;
|
|
162
|
+
depth?: "2" | "3";
|
|
163
|
+
extra?: string;
|
|
164
|
+
}
|
|
165
|
+
/** V2's Columns (plan RT7): `<div data-rt="columns" data-gap data-cols><div data-col="true">…</div></div>`, text only. */
|
|
166
|
+
export interface ColumnsIslandModel {
|
|
167
|
+
gap?: "s" | "m" | "l";
|
|
168
|
+
/** each column's HTML (paragraphs and marks); `id` is the editor's own */
|
|
169
|
+
cols: {
|
|
170
|
+
id: string;
|
|
171
|
+
html: string;
|
|
172
|
+
}[];
|
|
173
|
+
extra?: string;
|
|
174
|
+
}
|
|
175
|
+
/** V2's Author bio (plan RT8): `<aside data-rt="author" data-author>`; name, avatar and bio are resolved, never stored. */
|
|
176
|
+
export interface AuthorIslandModel {
|
|
177
|
+
author?: string;
|
|
178
|
+
extra?: string;
|
|
179
|
+
}
|
|
180
|
+
/** V2's Related posts (plan RT8): `<aside data-rt="related" data-mode data-count data-posts>`; the list is resolved. */
|
|
181
|
+
export interface RelatedIslandModel {
|
|
182
|
+
title?: string;
|
|
183
|
+
mode?: "topic" | "newest" | "manual";
|
|
184
|
+
count?: number;
|
|
185
|
+
posts: string[];
|
|
186
|
+
extra?: string;
|
|
187
|
+
}
|
|
114
188
|
export interface FileModel {
|
|
115
189
|
url?: string;
|
|
116
190
|
name?: string;
|
|
@@ -150,6 +224,11 @@ export interface IslandModels {
|
|
|
150
224
|
embed: EmbedModel;
|
|
151
225
|
file: FileModel;
|
|
152
226
|
component: ComponentModel;
|
|
227
|
+
gallery: GalleryIslandModel;
|
|
228
|
+
toc: TocIslandModel;
|
|
229
|
+
columns: ColumnsIslandModel;
|
|
230
|
+
author: AuthorIslandModel;
|
|
231
|
+
related: RelatedIslandModel;
|
|
153
232
|
}
|
|
154
233
|
export type IslandKind = keyof IslandModels;
|
|
155
234
|
export declare const ISLAND_KINDS: IslandKind[];
|