@bettercms-ai/kit 2.0.0 → 2.0.1

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.
Files changed (120) hide show
  1. package/dist/adapters/lossy.d.ts +12 -4
  2. package/dist/adapters/lossy.js +19 -8
  3. package/dist/adapters/lossy.js.map +1 -1
  4. package/dist/adapters/prod-types.d.ts +46 -14
  5. package/dist/adapters/prod-types.js +37 -35
  6. package/dist/adapters/prod-types.js.map +1 -1
  7. package/dist/adapters/schema.d.ts +39 -0
  8. package/dist/adapters/schema.js +281 -82
  9. package/dist/adapters/schema.js.map +1 -1
  10. package/dist/context.d.ts +39 -0
  11. package/dist/context.js +40 -1
  12. package/dist/context.js.map +1 -1
  13. package/dist/controls/form-picker-register.d.ts +7 -1
  14. package/dist/controls/form-picker-register.js +1 -5
  15. package/dist/controls/form-picker-register.js.map +1 -1
  16. package/dist/controls/icon.d.ts +1 -1
  17. package/dist/controls/icon.js +1 -1
  18. package/dist/controls/icon.js.map +1 -1
  19. package/dist/controls/link.d.ts +11 -4
  20. package/dist/controls/link.js +120 -202
  21. package/dist/controls/link.js.map +1 -1
  22. package/dist/controls/location.js +3 -8
  23. package/dist/controls/location.js.map +1 -1
  24. package/dist/controls/media.d.ts +0 -14
  25. package/dist/controls/media.js +52 -95
  26. package/dist/controls/media.js.map +1 -1
  27. package/dist/controls/reference-services.d.ts +23 -2
  28. package/dist/controls/reference.d.ts +2 -2
  29. package/dist/controls/reference.js +79 -28
  30. package/dist/controls/reference.js.map +1 -1
  31. package/dist/controls/rich-text/article-shell.d.ts +145 -0
  32. package/dist/controls/rich-text/article-shell.js +514 -0
  33. package/dist/controls/rich-text/article-shell.js.map +1 -0
  34. package/dist/controls/rich-text/author.d.ts +25 -0
  35. package/dist/controls/rich-text/author.js +107 -0
  36. package/dist/controls/rich-text/author.js.map +1 -0
  37. package/dist/controls/rich-text/blocks.d.ts +19 -2
  38. package/dist/controls/rich-text/blocks.js +275 -638
  39. package/dist/controls/rich-text/blocks.js.map +1 -1
  40. package/dist/controls/rich-text/button.d.ts +23 -0
  41. package/dist/controls/rich-text/button.js +81 -0
  42. package/dist/controls/rich-text/button.js.map +1 -0
  43. package/dist/controls/rich-text/catalog.d.ts +2 -5
  44. package/dist/controls/rich-text/catalog.js +121 -7
  45. package/dist/controls/rich-text/catalog.js.map +1 -1
  46. package/dist/controls/rich-text/checklist.d.ts +24 -0
  47. package/dist/controls/rich-text/checklist.js +115 -0
  48. package/dist/controls/rich-text/checklist.js.map +1 -0
  49. package/dist/controls/rich-text/codec.d.ts +15 -1
  50. package/dist/controls/rich-text/codec.js +11 -1
  51. package/dist/controls/rich-text/codec.js.map +1 -1
  52. package/dist/controls/rich-text/columns.d.ts +41 -0
  53. package/dist/controls/rich-text/columns.js +181 -0
  54. package/dist/controls/rich-text/columns.js.map +1 -0
  55. package/dist/controls/rich-text/control.js +1 -0
  56. package/dist/controls/rich-text/control.js.map +1 -1
  57. package/dist/controls/rich-text/dom.d.ts +84 -4
  58. package/dist/controls/rich-text/dom.js +171 -10
  59. package/dist/controls/rich-text/dom.js.map +1 -1
  60. package/dist/controls/rich-text/editor.d.ts +4 -2
  61. package/dist/controls/rich-text/editor.js +199 -26
  62. package/dist/controls/rich-text/editor.js.map +1 -1
  63. package/dist/controls/rich-text/embed.d.ts +28 -0
  64. package/dist/controls/rich-text/embed.js +210 -0
  65. package/dist/controls/rich-text/embed.js.map +1 -0
  66. package/dist/controls/rich-text/footnote.d.ts +41 -0
  67. package/dist/controls/rich-text/footnote.js +185 -0
  68. package/dist/controls/rich-text/footnote.js.map +1 -0
  69. package/dist/controls/rich-text/gallery.d.ts +28 -0
  70. package/dist/controls/rich-text/gallery.js +167 -0
  71. package/dist/controls/rich-text/gallery.js.map +1 -0
  72. package/dist/controls/rich-text/link-form.d.ts +13 -1
  73. package/dist/controls/rich-text/link-form.js +41 -7
  74. package/dist/controls/rich-text/link-form.js.map +1 -1
  75. package/dist/controls/rich-text/ops.js +6 -0
  76. package/dist/controls/rich-text/ops.js.map +1 -1
  77. package/dist/controls/rich-text/related.d.ts +49 -0
  78. package/dist/controls/rich-text/related.js +233 -0
  79. package/dist/controls/rich-text/related.js.map +1 -0
  80. package/dist/controls/rich-text/toc.d.ts +47 -0
  81. package/dist/controls/rich-text/toc.js +160 -0
  82. package/dist/controls/rich-text/toc.js.map +1 -0
  83. package/dist/controls/slot-register.d.ts +6 -1
  84. package/dist/controls/slot-register.js +1 -4
  85. package/dist/controls/slot-register.js.map +1 -1
  86. package/dist/core/core.d.ts +11 -3
  87. package/dist/core/core.js +24 -6
  88. package/dist/core/core.js.map +1 -1
  89. package/dist/core/facets.d.ts +16 -0
  90. package/dist/core/facets.js +17 -1
  91. package/dist/core/facets.js.map +1 -1
  92. package/dist/core/index.js +2 -2
  93. package/dist/index.js +16 -5
  94. package/dist/index.js.map +1 -1
  95. package/dist/ledgers/exclusions.v1.js +141 -0
  96. package/dist/ledgers/exclusions.v1.js.map +1 -0
  97. package/dist/library/coverage.d.ts +13 -1
  98. package/dist/library/coverage.js +12 -1
  99. package/dist/library/coverage.js.map +1 -1
  100. package/dist/library/kit-catalog.json +156 -153
  101. package/dist/schema/types.d.ts +17 -0
  102. package/dist/schema/types.js.map +1 -1
  103. package/dist/styles.css +1 -1
  104. package/dist/testing/coverage-extras.d.ts +8 -0
  105. package/dist/testing/coverage-extras.js +44 -0
  106. package/dist/testing/coverage-extras.js.map +1 -0
  107. package/dist/testing/exclusions.d.ts +30 -0
  108. package/dist/testing/exclusions.js +13 -0
  109. package/dist/testing/exclusions.js.map +1 -0
  110. package/dist/ui/autocomplete.d.ts +29 -1
  111. package/dist/ui/autocomplete.js +102 -47
  112. package/dist/ui/autocomplete.js.map +1 -1
  113. package/dist/ui/button.js +3 -5
  114. package/dist/ui/button.js.map +1 -1
  115. package/dist/ui/index.js +2 -2
  116. package/dist/ui/overlay.d.ts +4 -1
  117. package/dist/ui/overlay.js +2 -1
  118. package/dist/ui/overlay.js.map +1 -1
  119. package/dist/ui/toast.js +1 -1
  120. package/package.json +8 -2
@@ -0,0 +1,233 @@
1
+ import { useRichTextRefs } from "../../context.js";
2
+ import { ArticleEmpty, ArticlePanelBlock } from "./article-shell.js";
3
+ import * as React from "react";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ import { IconPlus } from "@tabler/icons-react";
6
+ //#region src/controls/rich-text/related.tsx
7
+ /**
8
+ * RICH TEXT · Related posts (kit-richtext-ui-spec.md §6): V2's article block, stored as the PT block object
9
+ * `{ _type: "relatedPosts", title?, mode, count, posts: { _ref }[] }`. Each post's title, cover, category and URL are
10
+ * derived from the referenced or queried entries, never stored.
11
+ *
12
+ * modes e1 2026-10-24 (signed deviation from V2's Automatic | Hand-picked): Same topic (topic) · Newest (newest) ·
13
+ * Hand-picked (manual). "How many" shows for the two automatic modes, Posts for Hand-picked. The schema's
14
+ * `hidden` is "show only when" (schema/types.ts), as V2's was.
15
+ * form V2 article.ts:167-195: Title (≤40, default "Related stories"), Stories (segmented), How many (1–6),
16
+ * Posts (1–6 unique references to the field's collection). Four settings → the floating panel (D54).
17
+ * preview V2 article-blocks.tsx:572-597: the title with the automatic mode's tag, three post cards (4:3 cover,
18
+ * two-line title, category); "Pick stories" when Hand-picked holds nothing that resolves.
19
+ * data KitServices.resolveRichTextRefs (useRichTextRefs): ids for Hand-picked, a query for the automatic modes.
20
+ * A fresh insert focuses the card (V2 article-blocks.tsx:131).
21
+ */
22
+ var RELATED_LABEL = "Related posts";
23
+ /** `topic: false`: the collection has nothing to rank Same topic by, so a new block is Newest. */
24
+ var relatedDefault = (topic = true) => ({
25
+ title: "Related stories",
26
+ mode: topic ? "topic" : "newest",
27
+ count: 3,
28
+ posts: []
29
+ });
30
+ /** Why Same topic is not offered (RichTextRead.references.topic false; the entry write's relatedPosts.no_topic_field). */
31
+ var NO_TOPIC_REASON = "Same topic needs a tags or category field on this collection.";
32
+ var MODE_TAG = {
33
+ topic: "Same topic",
34
+ newest: "Newest"
35
+ };
36
+ var asMode = (v) => v === "newest" || v === "manual" ? v : "topic";
37
+ var clampCount = (v) => Math.min(6, Math.max(1, Math.round(Number(v) || 3)));
38
+ /**
39
+ * The panel form; `to` is the field's collection. "How many" explains the chosen automatic mode. With `topic` false,
40
+ * Same topic is left out and Stories says why (a stored Same topic block keeps the option, so its value still shows).
41
+ */
42
+ var relatedSchema = (to, mode, topic = true, pick = {}) => ({
43
+ name: "relatedPosts",
44
+ title: RELATED_LABEL,
45
+ kind: "object",
46
+ icon: "news",
47
+ fields: [
48
+ {
49
+ name: "title",
50
+ title: "Title",
51
+ type: "string",
52
+ validation: { max: 40 }
53
+ },
54
+ {
55
+ name: "mode",
56
+ title: "Stories",
57
+ type: "select",
58
+ layout: "segmented",
59
+ initialValue: topic ? "topic" : "newest",
60
+ options: [
61
+ ...topic || mode === "topic" ? [{
62
+ value: "topic",
63
+ label: "Same topic"
64
+ }] : [],
65
+ {
66
+ value: "newest",
67
+ label: "Newest"
68
+ },
69
+ {
70
+ value: "manual",
71
+ label: "Hand-picked"
72
+ }
73
+ ],
74
+ ...topic ? {} : { description: NO_TOPIC_REASON }
75
+ },
76
+ {
77
+ name: "count",
78
+ title: "How many",
79
+ type: "number",
80
+ hidden: {
81
+ field: "mode",
82
+ op: "in",
83
+ value: ["topic", "newest"]
84
+ },
85
+ numeric: {
86
+ min: 1,
87
+ max: 6,
88
+ step: 1
89
+ },
90
+ description: mode === "newest" ? "Newest first." : "Same topic, newest first."
91
+ },
92
+ {
93
+ name: "posts",
94
+ title: "Posts",
95
+ type: "array",
96
+ hidden: {
97
+ field: "mode",
98
+ equals: "manual"
99
+ },
100
+ validation: {
101
+ min: 1,
102
+ max: 6,
103
+ unique: true
104
+ },
105
+ of: [{
106
+ name: "post",
107
+ title: "Post",
108
+ type: "reference",
109
+ reference: {
110
+ to,
111
+ ...pick.noun ? { noun: pick.noun } : {},
112
+ ...pick.excludeId ? { excludeId: pick.excludeId } : {}
113
+ }
114
+ }]
115
+ }
116
+ ]
117
+ });
118
+ /** What the preview asks the host for: the picked ids, or the automatic mode's query. */
119
+ function relatedRequest(m, collection, excludeId) {
120
+ if (m.mode === "manual") return m.posts.length ? {
121
+ kind: "relatedPosts",
122
+ ids: m.posts
123
+ } : null;
124
+ return {
125
+ kind: "relatedPosts",
126
+ query: {
127
+ collection,
128
+ mode: m.mode,
129
+ count: m.count,
130
+ excludeId
131
+ }
132
+ };
133
+ }
134
+ function RelatedPreview({ m, onPick, ro, collection, excludeId }) {
135
+ const { items, loading, error } = useRichTextRefs(relatedRequest(m, collection, excludeId));
136
+ const posts = m.mode === "manual" ? m.posts.map((id) => items.find((x) => x.id === id)).filter((x) => !!x) : items;
137
+ if (m.mode === "manual" && !loading && !error && !posts.length) return /* @__PURE__ */ jsx(ArticleEmpty, {
138
+ icon: /* @__PURE__ */ jsx(IconPlus, { stroke: 1.5 }),
139
+ title: m.title || "Related posts",
140
+ action: "Pick stories",
141
+ onAction: onPick,
142
+ ro
143
+ });
144
+ return /* @__PURE__ */ jsxs("aside", {
145
+ className: "rounded-kit-lg border border-kit-line bg-kit-surface px-4 py-3",
146
+ "aria-label": m.title || "Related posts",
147
+ "aria-busy": loading || void 0,
148
+ children: [/* @__PURE__ */ jsxs("div", {
149
+ className: "mb-2 flex items-baseline gap-2",
150
+ children: [m.title ? /* @__PURE__ */ jsx("p", {
151
+ "data-field": "title",
152
+ className: "text-kit-sm font-semibold text-kit-fg",
153
+ children: m.title
154
+ }) : null, m.mode !== "manual" ? /* @__PURE__ */ jsx("span", {
155
+ "data-field": "mode",
156
+ className: "text-kit-2xs text-kit-fg-3",
157
+ children: MODE_TAG[m.mode]
158
+ }) : null]
159
+ }), error ? /* @__PURE__ */ jsxs("p", {
160
+ role: "status",
161
+ className: "text-kit-sm text-kit-warning-fg",
162
+ children: ["The stories couldn't load: ", error]
163
+ }) : /* @__PURE__ */ jsx("ul", {
164
+ className: "m-0 grid grid-cols-3 gap-3 p-0",
165
+ "data-field": m.mode === "manual" ? "posts" : "count",
166
+ children: posts.map((e) => /* @__PURE__ */ jsxs("li", {
167
+ className: "min-w-0 list-none",
168
+ children: [
169
+ e.cover ? /* @__PURE__ */ jsx("img", {
170
+ src: e.cover,
171
+ alt: "",
172
+ draggable: false,
173
+ className: "block aspect-[4/3] w-full rounded-kit-md border border-kit-line object-cover"
174
+ }) : /* @__PURE__ */ jsx("div", { className: "aspect-[4/3] rounded-kit-md bg-kit-muted" }),
175
+ /* @__PURE__ */ jsx("p", {
176
+ className: "mt-1.5 line-clamp-2 text-kit-xs font-medium leading-snug text-kit-fg",
177
+ children: e.title
178
+ }),
179
+ e.category ? /* @__PURE__ */ jsx("p", {
180
+ className: "mt-0.5 truncate text-kit-2xs text-kit-fg-3",
181
+ children: e.category
182
+ }) : null
183
+ ]
184
+ }, e.id))
185
+ })]
186
+ });
187
+ }
188
+ function RelatedBlock({ m, set, el, ctx, autoFocus, collection, excludeId, topic = true, noun }) {
189
+ const schema = React.useMemo(() => relatedSchema([collection], m.mode, topic, {
190
+ noun,
191
+ excludeId
192
+ }), [
193
+ collection,
194
+ m.mode,
195
+ topic,
196
+ noun,
197
+ excludeId
198
+ ]);
199
+ return /* @__PURE__ */ jsx(ArticlePanelBlock, {
200
+ label: RELATED_LABEL,
201
+ kind: "related",
202
+ schema,
203
+ m,
204
+ set,
205
+ el,
206
+ ctx,
207
+ autoFocus,
208
+ openOnInsert: false,
209
+ toForm: (x) => ({
210
+ title: x.title,
211
+ mode: x.mode,
212
+ count: x.count,
213
+ posts: x.posts
214
+ }),
215
+ fromForm: (v) => ({
216
+ title: String(v.title ?? ""),
217
+ mode: asMode(v.mode),
218
+ count: clampCount(v.count),
219
+ posts: Array.isArray(v.posts) ? v.posts.filter((x) => typeof x === "string") : []
220
+ }),
221
+ children: (open, ro) => /* @__PURE__ */ jsx(RelatedPreview, {
222
+ m,
223
+ onPick: () => open("posts"),
224
+ ro,
225
+ collection,
226
+ excludeId
227
+ })
228
+ });
229
+ }
230
+ //#endregion
231
+ export { NO_TOPIC_REASON, RELATED_LABEL, RelatedBlock, RelatedPreview, relatedDefault, relatedRequest, relatedSchema };
232
+
233
+ //# sourceMappingURL=related.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"related.js","names":[],"sources":["../../../src/controls/rich-text/related.tsx"],"sourcesContent":["/**\n * RICH TEXT · Related posts (kit-richtext-ui-spec.md §6): V2's article block, stored as the PT block object\n * `{ _type: \"relatedPosts\", title?, mode, count, posts: { _ref }[] }`. Each post's title, cover, category and URL are\n * derived from the referenced or queried entries, never stored.\n *\n * modes e1 2026-10-24 (signed deviation from V2's Automatic | Hand-picked): Same topic (topic) · Newest (newest) ·\n * Hand-picked (manual). \"How many\" shows for the two automatic modes, Posts for Hand-picked. The schema's\n * `hidden` is \"show only when\" (schema/types.ts), as V2's was.\n * form V2 article.ts:167-195: Title (≤40, default \"Related stories\"), Stories (segmented), How many (1–6),\n * Posts (1–6 unique references to the field's collection). Four settings → the floating panel (D54).\n * preview V2 article-blocks.tsx:572-597: the title with the automatic mode's tag, three post cards (4:3 cover,\n * two-line title, category); \"Pick stories\" when Hand-picked holds nothing that resolves.\n * data KitServices.resolveRichTextRefs (useRichTextRefs): ids for Hand-picked, a query for the automatic modes.\n * A fresh insert focuses the card (V2 article-blocks.tsx:131).\n */\nimport * as React from \"react\";\nimport { IconPlus } from \"@tabler/icons-react\";\nimport { useRichTextRefs, type KitRichTextRefRequest } from \"../../context\";\nimport type { TypeSchema } from \"../../schema/types\";\nimport { ArticleEmpty, ArticlePanelBlock } from \"./article-shell\";\nimport type { IslandCtx } from \"./blocks\";\nimport type { IslandEl } from \"./dom\";\n\nexport type RelatedMode = \"topic\" | \"newest\" | \"manual\";\nexport interface RelatedModel {\n title: string;\n mode: RelatedMode;\n count: number;\n /** entry ids, Hand-picked only; the PT form wraps each as `{ _key, _ref }` */\n posts: string[];\n}\nexport const RELATED_LABEL = \"Related posts\";\n/** `topic: false`: the collection has nothing to rank Same topic by, so a new block is Newest. */\nexport const relatedDefault = (topic = true): RelatedModel => ({ title: \"Related stories\", mode: topic ? \"topic\" : \"newest\", count: 3, posts: [] });\n/** Why Same topic is not offered (RichTextRead.references.topic false; the entry write's relatedPosts.no_topic_field). */\nexport const NO_TOPIC_REASON = \"Same topic needs a tags or category field on this collection.\";\nconst MODE_TAG: Record<Exclude<RelatedMode, \"manual\">, string> = { topic: \"Same topic\", newest: \"Newest\" };\nconst asMode = (v: unknown): RelatedMode => (v === \"newest\" || v === \"manual\" ? v : \"topic\");\nconst clampCount = (v: unknown) => Math.min(6, Math.max(1, Math.round(Number(v) || 3)));\n\n/**\n * The panel form; `to` is the field's collection. \"How many\" explains the chosen automatic mode. With `topic` false,\n * Same topic is left out and Stories says why (a stored Same topic block keeps the option, so its value still shows).\n */\nexport const relatedSchema = (to: string[], mode: RelatedMode, topic = true, pick: { noun?: string; excludeId?: string } = {}): TypeSchema => ({\n name: \"relatedPosts\",\n title: RELATED_LABEL,\n kind: \"object\",\n icon: \"news\",\n fields: [\n { name: \"title\", title: \"Title\", type: \"string\", validation: { max: 40 } },\n {\n name: \"mode\",\n title: \"Stories\",\n type: \"select\",\n layout: \"segmented\",\n initialValue: topic ? \"topic\" : \"newest\",\n options: [\n ...(topic || mode === \"topic\" ? [{ value: \"topic\", label: \"Same topic\" }] : []),\n { value: \"newest\", label: \"Newest\" },\n { value: \"manual\", label: \"Hand-picked\" },\n ],\n ...(topic ? {} : { description: NO_TOPIC_REASON }),\n },\n {\n name: \"count\",\n title: \"How many\",\n type: \"number\",\n hidden: { field: \"mode\", op: \"in\", value: [\"topic\", \"newest\"] },\n numeric: { min: 1, max: 6, step: 1 },\n description: mode === \"newest\" ? \"Newest first.\" : \"Same topic, newest first.\",\n },\n {\n name: \"posts\",\n title: \"Posts\",\n type: \"array\",\n hidden: { field: \"mode\", equals: \"manual\" },\n validation: { min: 1, max: 6, unique: true },\n // Hand-picked never lists the entry being edited, and names a post as its collection does\n of: [{ name: \"post\", title: \"Post\", type: \"reference\", reference: { to, ...(pick.noun ? { noun: pick.noun } : {}), ...(pick.excludeId ? { excludeId: pick.excludeId } : {}) } }],\n },\n ],\n});\n\n/** What the preview asks the host for: the picked ids, or the automatic mode's query. */\nexport function relatedRequest(m: RelatedModel, collection?: string, excludeId?: string): KitRichTextRefRequest | null {\n if (m.mode === \"manual\") return m.posts.length ? { kind: \"relatedPosts\", ids: m.posts } : null;\n return { kind: \"relatedPosts\", query: { collection, mode: m.mode, count: m.count, excludeId } };\n}\n\nexport function RelatedPreview({ m, onPick, ro, collection, excludeId }: { m: RelatedModel; onPick: () => void; ro: boolean; collection?: string; excludeId?: string }) {\n const { items, loading, error } = useRichTextRefs(relatedRequest(m, collection, excludeId));\n // Hand-picked keeps the picked order; a post that no longer resolves is skipped (V2 .filter)\n const posts = m.mode === \"manual\" ? m.posts.map((id) => items.find((x) => x.id === id)).filter((x): x is NonNullable<typeof x> => !!x) : items;\n if (m.mode === \"manual\" && !loading && !error && !posts.length)\n return <ArticleEmpty icon={<IconPlus stroke={1.5} />} title={m.title || RELATED_LABEL} action=\"Pick stories\" onAction={onPick} ro={ro} />;\n return (\n <aside className=\"rounded-kit-lg border border-kit-line bg-kit-surface px-4 py-3\" aria-label={m.title || RELATED_LABEL} aria-busy={loading || undefined}>\n <div className=\"mb-2 flex items-baseline gap-2\">\n {m.title ? (\n <p data-field=\"title\" className=\"text-kit-sm font-semibold text-kit-fg\">\n {m.title}\n </p>\n ) : null}\n {m.mode !== \"manual\" ? (\n <span data-field=\"mode\" className=\"text-kit-2xs text-kit-fg-3\">\n {MODE_TAG[m.mode]}\n </span>\n ) : null}\n </div>\n {error ? (\n <p role=\"status\" className=\"text-kit-sm text-kit-warning-fg\">\n The stories couldn't load: {error}\n </p>\n ) : (\n <ul className=\"m-0 grid grid-cols-3 gap-3 p-0\" data-field={m.mode === \"manual\" ? \"posts\" : \"count\"}>\n {posts.map((e) => (\n <li key={e.id} className=\"min-w-0 list-none\">\n {e.cover ? (\n <img src={e.cover} alt=\"\" draggable={false} className=\"block aspect-[4/3] w-full rounded-kit-md border border-kit-line object-cover\" />\n ) : (\n <div className=\"aspect-[4/3] rounded-kit-md bg-kit-muted\" />\n )}\n <p className=\"mt-1.5 line-clamp-2 text-kit-xs font-medium leading-snug text-kit-fg\">{e.title}</p>\n {e.category ? <p className=\"mt-0.5 truncate text-kit-2xs text-kit-fg-3\">{e.category}</p> : null}\n </li>\n ))}\n </ul>\n )}\n </aside>\n );\n}\n\nexport function RelatedBlock({\n m, set, el, ctx, autoFocus, collection, excludeId, topic = true, noun,\n}: {\n m: RelatedModel;\n set: (fn: (m: RelatedModel) => RelatedModel, label?: string) => void;\n el: IslandEl;\n ctx: IslandCtx;\n autoFocus: boolean;\n /** the posts collection (the field's config) */\n collection: string;\n /** the entry being edited, left out of the automatic modes */\n excludeId?: string;\n /** false: the collection has no topic field, so Same topic is not offered */\n topic?: boolean;\n /** what one post is called (the collection's singular); absent → the picker's own noun */\n noun?: string;\n}) {\n const schema = React.useMemo(() => relatedSchema([collection], m.mode, topic, { noun, excludeId }), [collection, m.mode, topic, noun, excludeId]);\n return (\n <ArticlePanelBlock<RelatedModel>\n label={RELATED_LABEL}\n kind=\"related\"\n schema={schema}\n m={m}\n set={set}\n el={el}\n ctx={ctx}\n autoFocus={autoFocus}\n openOnInsert={false}\n toForm={(x) => ({ title: x.title, mode: x.mode, count: x.count, posts: x.posts })}\n fromForm={(v) => ({\n title: String(v.title ?? \"\"),\n mode: asMode(v.mode),\n count: clampCount(v.count),\n posts: Array.isArray(v.posts) ? v.posts.filter((x): x is string => typeof x === \"string\") : [],\n })}\n >\n {(open, ro) => <RelatedPreview m={m} onPick={() => open(\"posts\")} ro={ro} collection={collection} excludeId={excludeId} />}\n </ArticlePanelBlock>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA+BA,IAAa,gBAAgB;;AAE7B,IAAa,kBAAkB,QAAQ,UAAwB;CAAE,OAAO;CAAmB,MAAM,QAAQ,UAAU;CAAU,OAAO;CAAG,OAAO,CAAC;AAAE;;AAEjJ,IAAa,kBAAkB;AAC/B,IAAM,WAA2D;CAAE,OAAO;CAAc,QAAQ;AAAS;AACzG,IAAM,UAAU,MAA6B,MAAM,YAAY,MAAM,WAAW,IAAI;AACpF,IAAM,cAAc,MAAe,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;;;;;AAMtF,IAAa,iBAAiB,IAAc,MAAmB,QAAQ,MAAM,OAA8C,CAAC,OAAmB;CAC7I,MAAM;CACN,OAAO;CACP,MAAM;CACN,MAAM;CACN,QAAQ;EACN;GAAE,MAAM;GAAS,OAAO;GAAS,MAAM;GAAU,YAAY,EAAE,KAAK,GAAG;EAAE;EACzE;GACE,MAAM;GACN,OAAO;GACP,MAAM;GACN,QAAQ;GACR,cAAc,QAAQ,UAAU;GAChC,SAAS;IACP,GAAI,SAAS,SAAS,UAAU,CAAC;KAAE,OAAO;KAAS,OAAO;IAAa,CAAC,IAAI,CAAC;IAC7E;KAAE,OAAO;KAAU,OAAO;IAAS;IACnC;KAAE,OAAO;KAAU,OAAO;IAAc;GAC1C;GACA,GAAI,QAAQ,CAAC,IAAI,EAAE,aAAa,gBAAgB;EAClD;EACA;GACE,MAAM;GACN,OAAO;GACP,MAAM;GACN,QAAQ;IAAE,OAAO;IAAQ,IAAI;IAAM,OAAO,CAAC,SAAS,QAAQ;GAAE;GAC9D,SAAS;IAAE,KAAK;IAAG,KAAK;IAAG,MAAM;GAAE;GACnC,aAAa,SAAS,WAAW,kBAAkB;EACrD;EACA;GACE,MAAM;GACN,OAAO;GACP,MAAM;GACN,QAAQ;IAAE,OAAO;IAAQ,QAAQ;GAAS;GAC1C,YAAY;IAAE,KAAK;IAAG,KAAK;IAAG,QAAQ;GAAK;GAE3C,IAAI,CAAC;IAAE,MAAM;IAAQ,OAAO;IAAQ,MAAM;IAAa,WAAW;KAAE;KAAI,GAAI,KAAK,OAAO,EAAE,MAAM,KAAK,KAAK,IAAI,CAAC;KAAI,GAAI,KAAK,YAAY,EAAE,WAAW,KAAK,UAAU,IAAI,CAAC;IAAG;GAAE,CAAC;EACjL;CACF;AACF;;AAGA,SAAgB,eAAe,GAAiB,YAAqB,WAAkD;CACrH,IAAI,EAAE,SAAS,UAAU,OAAO,EAAE,MAAM,SAAS;EAAE,MAAM;EAAgB,KAAK,EAAE;CAAM,IAAI;CAC1F,OAAO;EAAE,MAAM;EAAgB,OAAO;GAAE;GAAY,MAAM,EAAE;GAAM,OAAO,EAAE;GAAO;EAAU;CAAE;AAChG;AAEA,SAAgB,eAAe,EAAE,GAAG,QAAQ,IAAI,YAAY,aAA4G;CACtK,MAAM,EAAE,OAAO,SAAS,UAAU,gBAAgB,eAAe,GAAG,YAAY,SAAS,CAAC;CAE1F,MAAM,QAAQ,EAAE,SAAS,WAAW,EAAE,MAAM,KAAK,OAAO,MAAM,MAAM,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,QAAQ,MAAkC,CAAC,CAAC,CAAC,IAAI;CACzI,IAAI,EAAE,SAAS,YAAY,CAAC,WAAW,CAAC,SAAS,CAAC,MAAM,QACtD,OAAO,oBAAC,cAAD;EAAc,MAAM,oBAAC,UAAD,EAAU,QAAQ,IAAM,CAAA;EAAG,OAAO,EAAE,SAAA;EAAwB,QAAO;EAAe,UAAU;EAAY;CAAK,CAAA;CAC1I,OACE,qBAAC,SAAD;EAAO,WAAU;EAAiE,cAAY,EAAE,SAAA;EAAwB,aAAW,WAAW,KAAA;EAA9I,UAAA,CACE,qBAAC,OAAD;GAAK,WAAU;GAAf,UAAA,CACG,EAAE,QACD,oBAAC,KAAD;IAAG,cAAW;IAAQ,WAAU;IAC7B,UAAA,EAAE;GACF,CAAA,IACD,MACH,EAAE,SAAS,WACV,oBAAC,QAAD;IAAM,cAAW;IAAO,WAAU;IAC/B,UAAA,SAAS,EAAE;GACR,CAAA,IACJ,IACD;EACJ,CAAA,GAAA,QACC,qBAAC,KAAD;GAAG,MAAK;GAAS,WAAU;GAA3B,UAAA,CAA6D,+BAC/B,KAC3B;EAEH,CAAA,IAAA,oBAAC,MAAD;GAAI,WAAU;GAAiC,cAAY,EAAE,SAAS,WAAW,UAAU;GACxF,UAAA,MAAM,KAAK,MACV,qBAAC,MAAD;IAAe,WAAU;IAAzB,UAAA;KACG,EAAE,QACD,oBAAC,OAAD;MAAK,KAAK,EAAE;MAAO,KAAI;MAAG,WAAW;MAAO,WAAU;KAAgF,CAAA,IAEtI,oBAAC,OAAD,EAAK,WAAU,2CAA4C,CAAA;KAE7D,oBAAC,KAAD;MAAG,WAAU;MAAwE,UAAA,EAAE;KAAS,CAAA;KAC/F,EAAE,WAAW,oBAAC,KAAD;MAAG,WAAU;MAA8C,UAAA,EAAE;KAAY,CAAA,IAAI;IACzF;GARK,GAAA,EAAE,EAQP,CACL;EACC,CAAA,CAED;;AAEX;AAEA,SAAgB,aAAa,EAC3B,GAAG,KAAK,IAAI,KAAK,WAAW,YAAY,WAAW,QAAQ,MAAM,QAehE;CACD,MAAM,SAAS,MAAM,cAAc,cAAc,CAAC,UAAU,GAAG,EAAE,MAAM,OAAO;EAAE;EAAM;CAAU,CAAC,GAAG;EAAC;EAAY,EAAE;EAAM;EAAO;EAAM;CAAS,CAAC;CAChJ,OACE,oBAAC,mBAAD;EACE,OAAO;EACP,MAAK;EACG;EACL;EACE;EACD;EACC;EACM;EACX,cAAc;EACd,SAAS,OAAO;GAAE,OAAO,EAAE;GAAO,MAAM,EAAE;GAAM,OAAO,EAAE;GAAO,OAAO,EAAE;EAAM;EAC/E,WAAW,OAAO;GAChB,OAAO,OAAO,EAAE,SAAS,EAAE;GAC3B,MAAM,OAAO,EAAE,IAAI;GACnB,OAAO,WAAW,EAAE,KAAK;GACzB,OAAO,MAAM,QAAQ,EAAE,KAAK,IAAI,EAAE,MAAM,QAAQ,MAAmB,OAAO,MAAM,QAAQ,IAAI,CAAC;EAC/F;EAEE,WAAA,MAAM,OAAO,oBAAC,gBAAD;GAAmB;GAAG,cAAc,KAAK,OAAO;GAAO;GAAgB;GAAuB;EAAY,CAAA;CACxG,CAAA;AAEvB"}
@@ -0,0 +1,47 @@
1
+ import { slugify } from "@bettercms-ai/types";
2
+ import type { TypeSchema } from "../../schema/types";
3
+ import type { IslandCtx } from "./blocks";
4
+ import { type IslandEl } from "./dom";
5
+ export interface TocModel {
6
+ title: string;
7
+ depth: "2" | "3";
8
+ }
9
+ export declare const TOC_LABEL = "Table of contents";
10
+ export declare const tocDefault: () => TocModel;
11
+ /** The popover form (V2 ARTICLE_SCHEMAS.toc). Two settings → the shell's popover (D54). */
12
+ export declare const TOC_SCHEMA: TypeSchema;
13
+ export interface OutlineItem {
14
+ id: string;
15
+ text: string;
16
+ depth: 2 | 3;
17
+ }
18
+ /** V2 article.ts:250-251, the one heading-id rule the site stamps too (types heading-id.ts, plan RT5). */
19
+ export { slugify };
20
+ /** The body's H2 and H3, top level only, in order, with stable de-duplicated ids. */
21
+ export declare function outlineOf(root: ParentNode): OutlineItem[];
22
+ export declare const tocVisible: (outline: OutlineItem[], depth: TocModel["depth"]) => OutlineItem[];
23
+ /** One live outline per editor: the editor sets it after each input, every TOC preview subscribes. */
24
+ export interface OutlineStore {
25
+ get: () => OutlineItem[];
26
+ subscribe: (fn: () => void) => () => void;
27
+ set: (next: OutlineItem[]) => void;
28
+ }
29
+ export declare function createOutlineStore(): OutlineStore;
30
+ export declare const NO_OUTLINE: OutlineStore;
31
+ /** `host` is the TOC's island element; its parent is the editor surface whose headings it lists. */
32
+ export declare function TocPreview({ m, outline, host }: {
33
+ m: TocModel;
34
+ outline: OutlineStore;
35
+ host: HTMLElement;
36
+ }): import("react/jsx-runtime").JSX.Element;
37
+ export interface TocBlockProps {
38
+ m: TocModel;
39
+ set: (fn: (m: TocModel) => TocModel, label?: string) => void;
40
+ el: IslandEl;
41
+ /** the editor's live outline rides the island context */
42
+ ctx: IslandCtx & {
43
+ outline?: OutlineStore;
44
+ };
45
+ autoFocus: boolean;
46
+ }
47
+ export declare function TocBlock({ m, set, el, ctx, autoFocus }: TocBlockProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,160 @@
1
+ import { cn } from "../../lib/cn.js";
2
+ import { ArticlePopoverBlock } from "./article-shell.js";
3
+ import { placeCaret } from "./dom.js";
4
+ import { slugify } from "@bettercms-ai/types";
5
+ import * as React from "react";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ //#region src/controls/rich-text/toc.tsx
8
+ /**
9
+ * RICH TEXT · Table of contents (kit-richtext-ui-spec.md §5): V2's article block, stored as the PT block object
10
+ * `{ _type: "tableOfContents", title?, depth: "2" | "3" }`. The list is derived from the same body's top-level H2/H3,
11
+ * never stored.
12
+ *
13
+ * form V2 article.ts:137-156: Title (max 40, default "In this article"), Headings H2 | H2 and H3 (segmented)
14
+ * outline V2 article.ts:248-266: the body's top-level non-empty H2/H3 in order, ids slugified and de-duplicated
15
+ * (x, x-2, …), the same ids the site stamps on its headings
16
+ * preview V2 article-blocks.tsx:537-568: the nav as the reader sees it; each entry a button that scrolls the
17
+ * heading to the centre and puts the caret in it; "No headings yet." when there are none
18
+ * store V2 article-blocks.tsx:45-66: one live outline per editor, so every TOC follows the headings as they are typed
19
+ *
20
+ * The block itself (card, chip, popover at its corner) is the shared article shell (article-shell.tsx, kit-40); it
21
+ * opens on a fresh insert by focusing the card only (V2 article-blocks.tsx:131).
22
+ */
23
+ var TOC_LABEL = "Table of contents";
24
+ var tocDefault = () => ({
25
+ title: "In this article",
26
+ depth: "3"
27
+ });
28
+ /** The popover form (V2 ARTICLE_SCHEMAS.toc). Two settings → the shell's popover (D54). */
29
+ var TOC_SCHEMA = {
30
+ name: "tableOfContents",
31
+ title: TOC_LABEL,
32
+ kind: "object",
33
+ icon: "list-tree",
34
+ fields: [{
35
+ name: "title",
36
+ title: "Title",
37
+ type: "string",
38
+ validation: { max: 40 }
39
+ }, {
40
+ name: "depth",
41
+ title: "Headings",
42
+ type: "select",
43
+ layout: "segmented",
44
+ initialValue: "3",
45
+ options: [{
46
+ value: "2",
47
+ label: "H2"
48
+ }, {
49
+ value: "3",
50
+ label: "H2 and H3"
51
+ }]
52
+ }]
53
+ };
54
+ /** The body's H2 and H3, top level only, in order, with stable de-duplicated ids. */
55
+ function outlineOf(root) {
56
+ const used = /* @__PURE__ */ new Set();
57
+ const out = [];
58
+ root.querySelectorAll(":scope > h2, :scope > h3").forEach((h) => {
59
+ const text = (h.textContent ?? "").replace(/​/g, "").trim();
60
+ if (!text) return;
61
+ let id = slugify(text);
62
+ for (let n = 2; used.has(id); n++) id = `${slugify(text)}-${n}`;
63
+ used.add(id);
64
+ out.push({
65
+ id,
66
+ text,
67
+ depth: h.tagName === "H2" ? 2 : 3
68
+ });
69
+ });
70
+ return out;
71
+ }
72
+ var tocVisible = (outline, depth) => outline.filter((o) => o.depth === 2 || depth === "3");
73
+ function createOutlineStore() {
74
+ let cur = [];
75
+ const subs = /* @__PURE__ */ new Set();
76
+ return {
77
+ get: () => cur,
78
+ subscribe: (fn) => (subs.add(fn), () => void subs.delete(fn)),
79
+ set: (next) => {
80
+ if (JSON.stringify(next) === JSON.stringify(cur)) return;
81
+ cur = next;
82
+ subs.forEach((f) => f());
83
+ }
84
+ };
85
+ }
86
+ var NO_OUTLINE = {
87
+ get: () => [],
88
+ subscribe: () => () => {},
89
+ set: () => {}
90
+ };
91
+ /** `host` is the TOC's island element; its parent is the editor surface whose headings it lists. */
92
+ function TocPreview({ m, outline, host }) {
93
+ const all = React.useSyncExternalStore(outline.subscribe, outline.get, outline.get);
94
+ const items = tocVisible(all, m.depth);
95
+ const go = (it) => {
96
+ const root = host.parentElement;
97
+ if (!root) return;
98
+ const h = Array.from(root.querySelectorAll(":scope > h2, :scope > h3")).filter((h) => (h.textContent ?? "").replace(/​/g, "").trim())[all.indexOf(it)];
99
+ if (!h) return;
100
+ h.scrollIntoView?.({
101
+ block: "center",
102
+ behavior: "smooth"
103
+ });
104
+ root.focus({ preventScroll: true });
105
+ placeCaret(h);
106
+ };
107
+ return /* @__PURE__ */ jsxs("nav", {
108
+ "aria-label": m.title || "Table of contents",
109
+ className: "rounded-kit-lg border border-kit-line bg-kit-surface px-4 py-3",
110
+ children: [m.title ? /* @__PURE__ */ jsx("p", {
111
+ "data-field": "title",
112
+ className: "mb-1.5 text-kit-sm font-semibold text-kit-fg",
113
+ children: m.title
114
+ }) : null, items.length ? /* @__PURE__ */ jsx("ol", {
115
+ className: "m-0 flex list-none flex-col gap-0.5 p-0 text-kit-sm",
116
+ children: items.map((it, i) => /* @__PURE__ */ jsx("li", {
117
+ className: cn(it.depth === 3 && "pl-4"),
118
+ children: /* @__PURE__ */ jsx("button", {
119
+ type: "button",
120
+ onClick: (e) => (e.stopPropagation(), go(it)),
121
+ className: "max-w-full truncate rounded-kit-sm text-left text-kit-fg-2 hover:text-kit-fg hover:underline",
122
+ children: it.text
123
+ })
124
+ }, `${it.id}-${i}`))
125
+ }) : /* @__PURE__ */ jsx("p", {
126
+ className: "text-kit-sm text-kit-fg-3",
127
+ children: "No headings yet."
128
+ })]
129
+ });
130
+ }
131
+ function TocBlock({ m, set, el, ctx, autoFocus }) {
132
+ return /* @__PURE__ */ jsx(ArticlePopoverBlock, {
133
+ label: TOC_LABEL,
134
+ kind: "toc",
135
+ schema: TOC_SCHEMA,
136
+ m,
137
+ set,
138
+ el,
139
+ ctx,
140
+ autoFocus,
141
+ openOnInsert: false,
142
+ toForm: (x) => ({
143
+ title: x.title,
144
+ depth: x.depth
145
+ }),
146
+ fromForm: (v) => ({
147
+ title: String(v.title ?? ""),
148
+ depth: v.depth === "2" ? "2" : "3"
149
+ }),
150
+ children: () => /* @__PURE__ */ jsx(TocPreview, {
151
+ m,
152
+ outline: ctx.outline ?? NO_OUTLINE,
153
+ host: el
154
+ })
155
+ });
156
+ }
157
+ //#endregion
158
+ export { NO_OUTLINE, TOC_LABEL, TOC_SCHEMA, TocBlock, TocPreview, createOutlineStore, outlineOf, slugify, tocDefault, tocVisible };
159
+
160
+ //# sourceMappingURL=toc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toc.js","names":[],"sources":["../../../src/controls/rich-text/toc.tsx"],"sourcesContent":["/**\n * RICH TEXT · Table of contents (kit-richtext-ui-spec.md §5): V2's article block, stored as the PT block object\n * `{ _type: \"tableOfContents\", title?, depth: \"2\" | \"3\" }`. The list is derived from the same body's top-level H2/H3,\n * never stored.\n *\n * form V2 article.ts:137-156: Title (max 40, default \"In this article\"), Headings H2 | H2 and H3 (segmented)\n * outline V2 article.ts:248-266: the body's top-level non-empty H2/H3 in order, ids slugified and de-duplicated\n * (x, x-2, …), the same ids the site stamps on its headings\n * preview V2 article-blocks.tsx:537-568: the nav as the reader sees it; each entry a button that scrolls the\n * heading to the centre and puts the caret in it; \"No headings yet.\" when there are none\n * store V2 article-blocks.tsx:45-66: one live outline per editor, so every TOC follows the headings as they are typed\n *\n * The block itself (card, chip, popover at its corner) is the shared article shell (article-shell.tsx, kit-40); it\n * opens on a fresh insert by focusing the card only (V2 article-blocks.tsx:131).\n */\nimport * as React from \"react\";\nimport { cn } from \"../../lib/cn\";\nimport { slugify } from \"@bettercms-ai/types\";\nimport type { TypeSchema } from \"../../schema/types\";\nimport { ArticlePopoverBlock } from \"./article-shell\";\nimport type { IslandCtx } from \"./blocks\";\nimport { placeCaret, type IslandEl } from \"./dom\";\n\nexport interface TocModel {\n title: string;\n depth: \"2\" | \"3\";\n}\nexport const TOC_LABEL = \"Table of contents\";\nexport const tocDefault = (): TocModel => ({ title: \"In this article\", depth: \"3\" });\n\n/** The popover form (V2 ARTICLE_SCHEMAS.toc). Two settings → the shell's popover (D54). */\nexport const TOC_SCHEMA: TypeSchema = {\n name: \"tableOfContents\",\n title: TOC_LABEL,\n kind: \"object\",\n icon: \"list-tree\",\n fields: [\n { name: \"title\", title: \"Title\", type: \"string\", validation: { max: 40 } },\n {\n name: \"depth\",\n title: \"Headings\",\n type: \"select\",\n layout: \"segmented\",\n initialValue: \"3\",\n options: [\n { value: \"2\", label: \"H2\" },\n { value: \"3\", label: \"H2 and H3\" },\n ],\n },\n ],\n};\n\n/* ── Outline ───────────────────────────────────────────────────────── */\nexport interface OutlineItem {\n id: string;\n text: string;\n depth: 2 | 3;\n}\n/** V2 article.ts:250-251, the one heading-id rule the site stamps too (types heading-id.ts, plan RT5). */\nexport { slugify };\n\n/** The body's H2 and H3, top level only, in order, with stable de-duplicated ids. */\nexport function outlineOf(root: ParentNode): OutlineItem[] {\n const used = new Set<string>();\n const out: OutlineItem[] = [];\n root.querySelectorAll(\":scope > h2, :scope > h3\").forEach((h) => {\n const text = (h.textContent ?? \"\").replace(/​/g, \"\").trim();\n if (!text) return;\n let id = slugify(text);\n for (let n = 2; used.has(id); n++) id = `${slugify(text)}-${n}`;\n used.add(id);\n out.push({ id, text, depth: h.tagName === \"H2\" ? 2 : 3 });\n });\n return out;\n}\nexport const tocVisible = (outline: OutlineItem[], depth: TocModel[\"depth\"]) => outline.filter((o) => o.depth === 2 || depth === \"3\");\n\n/** One live outline per editor: the editor sets it after each input, every TOC preview subscribes. */\nexport interface OutlineStore {\n get: () => OutlineItem[];\n subscribe: (fn: () => void) => () => void;\n set: (next: OutlineItem[]) => void;\n}\nexport function createOutlineStore(): OutlineStore {\n let cur: OutlineItem[] = [];\n const subs = new Set<() => void>();\n return {\n get: () => cur,\n subscribe: (fn) => (subs.add(fn), () => void subs.delete(fn)),\n set: (next) => {\n if (JSON.stringify(next) === JSON.stringify(cur)) return;\n cur = next;\n subs.forEach((f) => f());\n },\n };\n}\nexport const NO_OUTLINE: OutlineStore = { get: () => [], subscribe: () => () => {}, set: () => {} };\n\n/* ── Preview (what the reader sees) ────────────────────────────────── */\n/** `host` is the TOC's island element; its parent is the editor surface whose headings it lists. */\nexport function TocPreview({ m, outline, host }: { m: TocModel; outline: OutlineStore; host: HTMLElement }) {\n const all = React.useSyncExternalStore(outline.subscribe, outline.get, outline.get);\n const items = tocVisible(all, m.depth);\n const go = (it: OutlineItem) => {\n const root = host.parentElement;\n if (!root) return;\n const hs = Array.from(root.querySelectorAll<HTMLElement>(\":scope > h2, :scope > h3\")).filter((h) => (h.textContent ?? \"\").replace(/​/g, \"\").trim());\n const h = hs[all.indexOf(it)];\n if (!h) return;\n h.scrollIntoView?.({ block: \"center\", behavior: \"smooth\" });\n root.focus({ preventScroll: true });\n placeCaret(h);\n };\n return (\n <nav aria-label={m.title || TOC_LABEL} className=\"rounded-kit-lg border border-kit-line bg-kit-surface px-4 py-3\">\n {m.title ? (\n <p data-field=\"title\" className=\"mb-1.5 text-kit-sm font-semibold text-kit-fg\">\n {m.title}\n </p>\n ) : null}\n {items.length ? (\n <ol className=\"m-0 flex list-none flex-col gap-0.5 p-0 text-kit-sm\">\n {items.map((it, i) => (\n <li key={`${it.id}-${i}`} className={cn(it.depth === 3 && \"pl-4\")}>\n <button type=\"button\" onClick={(e) => (e.stopPropagation(), go(it))} className=\"max-w-full truncate rounded-kit-sm text-left text-kit-fg-2 hover:text-kit-fg hover:underline\">\n {it.text}\n </button>\n </li>\n ))}\n </ol>\n ) : (\n <p className=\"text-kit-sm text-kit-fg-3\">No headings yet.</p>\n )}\n </nav>\n );\n}\n\n/* ── The block (V2 ArticleBlock, surface \"popover\") ────────────────── */\nexport interface TocBlockProps {\n m: TocModel;\n set: (fn: (m: TocModel) => TocModel, label?: string) => void;\n el: IslandEl;\n /** the editor's live outline rides the island context */\n ctx: IslandCtx & { outline?: OutlineStore };\n autoFocus: boolean;\n}\n\nexport function TocBlock({ m, set, el, ctx, autoFocus }: TocBlockProps) {\n return (\n <ArticlePopoverBlock<TocModel>\n label={TOC_LABEL}\n kind=\"toc\"\n schema={TOC_SCHEMA}\n m={m}\n set={set}\n el={el}\n ctx={ctx}\n autoFocus={autoFocus}\n openOnInsert={false}\n toForm={(x) => ({ title: x.title, depth: x.depth })}\n fromForm={(v) => ({ title: String(v.title ?? \"\"), depth: v.depth === \"2\" ? \"2\" : \"3\" })}\n >\n {() => <TocPreview m={m} outline={ctx.outline ?? NO_OUTLINE} host={el} />}\n </ArticlePopoverBlock>\n );\n}\n"],"mappings":"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"}
@@ -1 +1,6 @@
1
- export {};
1
+ /**
2
+ * Lane L3: a component slot (a json field the host marks `componentSlot`) draws the slot control; every other json
3
+ * field keeps the json editor. It wraps the registered json control, so it imports json-register itself (a bundler may
4
+ * reorder the barrel's bare imports).
5
+ */
6
+ import "./json-register";
@@ -1,12 +1,9 @@
1
1
  import { JsonControl } from "./json.js";
2
2
  import { CONTROLS } from "../engine/kit-field.js";
3
+ import "./json-register.js";
3
4
  import { ComponentSlotControl } from "./slot.js";
4
5
  import { createElement } from "react";
5
6
  //#region src/controls/slot-register.ts
6
- /**
7
- * Lane L3: a component slot (a json field the host marks `componentSlot`) draws the slot control; every other json
8
- * field keeps the json editor. Imported after json-register (kit barrel), so it wraps the registered json control.
9
- */
10
7
  var Json = CONTROLS.json ?? JsonControl;
11
8
  CONTROLS.json = function JsonOrSlotControl(props) {
12
9
  return createElement(props.field.componentSlot ? ComponentSlotControl : Json, props);
@@ -1 +1 @@
1
- {"version":3,"file":"slot-register.js","names":[],"sources":["../../src/controls/slot-register.ts"],"sourcesContent":["/**\n * Lane L3: a component slot (a json field the host marks `componentSlot`) draws the slot control; every other json\n * field keeps the json editor. Imported after json-register (kit barrel), so it wraps the registered json control.\n */\nimport { createElement } from \"react\";\nimport type { ControlProps } from \"../types\";\nimport { CONTROLS } from \"../engine/kit-field\";\nimport { JsonControl } from \"./json\";\nimport { ComponentSlotControl } from \"./slot\";\n\nconst Json = CONTROLS.json ?? JsonControl;\nCONTROLS.json = function JsonOrSlotControl(props: ControlProps) {\n return createElement(props.field.componentSlot ? ComponentSlotControl : Json, props);\n};\n"],"mappings":";;;;;;;;;AAUA,IAAM,OAAO,SAAS,QAAQ;AAC9B,SAAS,OAAO,SAAS,kBAAkB,OAAqB;CAC9D,OAAO,cAAc,MAAM,MAAM,gBAAgB,uBAAuB,MAAM,KAAK;AACrF"}
1
+ {"version":3,"file":"slot-register.js","names":[],"sources":["../../src/controls/slot-register.ts"],"sourcesContent":["/**\n * Lane L3: a component slot (a json field the host marks `componentSlot`) draws the slot control; every other json\n * field keeps the json editor. It wraps the registered json control, so it imports json-register itself (a bundler may\n * reorder the barrel's bare imports).\n */\nimport \"./json-register\";\nimport { createElement } from \"react\";\nimport type { ControlProps } from \"../types\";\nimport { CONTROLS } from \"../engine/kit-field\";\nimport { JsonControl } from \"./json\";\nimport { ComponentSlotControl } from \"./slot\";\n\nconst Json = CONTROLS.json ?? JsonControl;\nCONTROLS.json = function JsonOrSlotControl(props: ControlProps) {\n return createElement(props.field.componentSlot ? ComponentSlotControl : Json, props);\n};\n"],"mappings":";;;;;;AAYA,IAAM,OAAO,SAAS,QAAQ;AAC9B,SAAS,OAAO,SAAS,kBAAkB,OAAqB;CAC9D,OAAO,cAAc,MAAM,MAAM,gBAAgB,uBAAuB,MAAM,KAAK;AACrF"}
@@ -9,12 +9,15 @@
9
9
  * Laws (PBT, src/core/__tests__): norm∘embed = id on C (P1); normProd(embedProd(c)) equals c except under the paths
10
10
  * the adapter names (P2); prod zod accepts embedProd(c) (P3); every named loss is excluded, pending or out of its bound
11
11
  * (P4); both normalisers are idempotent (P5); the hash is byte-stable (P6); one prod key per facet (P7); out-of-domain
12
- * prod keys survive a write (P8).
12
+ * prod keys survive a write (P8). The hash is `core1:` + sha256 of types' canonicalJson.
13
13
  *
14
14
  * V2's equivalences folded by normV2 (what the V2 engine draws the same, so C cannot tell apart):
15
15
  * - a list's itemNoun equal to the engine's fallback (lib/schema.ts itemNoun) is the absent one
16
16
  * - a fieldset a field names but the schema does not declare is the declared { name, title: name } (engine/field-list
17
17
  * arrange), appended after the declared ones in first-use order
18
+ * - a rich text's references naming neither posts nor authors are the absent ones
19
+ * - a rich text's features are read through types normaliseRichTextFeatures (an alias is its names: `headings` is
20
+ * h1–h6), each name once; naming every RICHTEXT_FEATURES entry, in any order, is the absent list (V2 offers them all)
18
21
  */
19
22
  import { type AdapterTarget, type FromTypeSchemaResult, type ToTypeSchemaOptions } from "../adapters/schema";
20
23
  import type { ProdComponent, ProdContentModel } from "../adapters/prod-types";
@@ -33,9 +36,14 @@ export declare function embedProd(c: CoreSchema, lane: "fields"): FromTypeSchema
33
36
  export declare function embedProd(c: CoreSchema, lane: "props"): FromTypeSchemaResult<ProdComponent>;
34
37
  export declare function embedProd(c: CoreSchema, lane: AdapterTarget): FromTypeSchemaResult<ProdContentModel | ProdComponent>;
35
38
  export declare function normProd(prod: ProdContentModel | ProdComponent, lane: AdapterTarget, opts?: ToTypeSchemaOptions): CoreSchema;
36
- /** Byte-stable serialisation of C: the hash input, and the form fixtures are frozen in. */
39
+ /** Byte-stable serialisation of C: types' canonicalJson (the bytes the backend's schema_hash is taken over, same rules). */
37
40
  export declare const coreJson: (c: CoreSchema) => string;
38
- /** `schema_hash`: "v1:" + sha256 of coreJson (Web Crypto: the same in the browser, Node and Bun). */
41
+ /**
42
+ * A Core hash's prefix: `core<v>:`. Disjoint from the backend's `schema_hash` prefix (`v<n>:`, taken over a stored
43
+ * definition, types' schemaHashInput), so a Core hash can never be read as, or compared with, a stored one.
44
+ */
45
+ export declare const CORE_HASH_PREFIX: "core1:";
46
+ /** CORE_HASH_PREFIX + sha256 of coreJson (Web Crypto: the same in the browser, Node and Bun). */
39
47
  export declare function coreHash(c: CoreSchema): Promise<string>;
40
48
  /** The leaf paths where two JSON values differ ("/fields/0/title"); a missing object or list compares as an empty one. */
41
49
  export declare function leafDiff(a: unknown, b: unknown, path?: string, out?: string[]): string[];
package/dist/core/core.js CHANGED
@@ -2,6 +2,7 @@ import { SERVER_CONFIG_KEYS, isHostConfigKey } from "../adapters/lossy.js";
2
2
  import { fromTypeSchema, propsToTypeSchema, toTypeSchema } from "../adapters/schema.js";
3
3
  import { itemNoun } from "../lib/schema.js";
4
4
  import { facetOf } from "./facets.js";
5
+ import { RICHTEXT_FEATURES, canonicalJson, normaliseRichTextFeatures } from "@bettercms-ai/types";
5
6
  //#region src/core/core.ts
6
7
  /**
7
8
  * THE CORE SCHEMA `C` (P2 C1): one canonical, versioned form of a V2 schema that both directions meet in.
@@ -14,12 +15,15 @@ import { facetOf } from "./facets.js";
14
15
  * Laws (PBT, src/core/__tests__): norm∘embed = id on C (P1); normProd(embedProd(c)) equals c except under the paths
15
16
  * the adapter names (P2); prod zod accepts embedProd(c) (P3); every named loss is excluded, pending or out of its bound
16
17
  * (P4); both normalisers are idempotent (P5); the hash is byte-stable (P6); one prod key per facet (P7); out-of-domain
17
- * prod keys survive a write (P8).
18
+ * prod keys survive a write (P8). The hash is `core1:` + sha256 of types' canonicalJson.
18
19
  *
19
20
  * V2's equivalences folded by normV2 (what the V2 engine draws the same, so C cannot tell apart):
20
21
  * - a list's itemNoun equal to the engine's fallback (lib/schema.ts itemNoun) is the absent one
21
22
  * - a fieldset a field names but the schema does not declare is the declared { name, title: name } (engine/field-list
22
23
  * arrange), appended after the declared ones in first-use order
24
+ * - a rich text's references naming neither posts nor authors are the absent ones
25
+ * - a rich text's features are read through types normaliseRichTextFeatures (an alias is its names: `headings` is
26
+ * h1–h6), each name once; naming every RICHTEXT_FEATURES entry, in any order, is the absent list (V2 offers them all)
23
27
  */
24
28
  var CORE_VERSION = 1;
25
29
  var isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
@@ -31,6 +35,8 @@ function canonical(v) {
31
35
  for (const k of Object.keys(v).sort()) if (v[k] !== void 0) out[k] = canonical(v[k]);
32
36
  return out;
33
37
  }
38
+ /** Every feature, canonical: RICHTEXT_FEATURES through normaliseRichTextFeatures, so no alias is ever in it. */
39
+ var ALL_RICH_FEATURES = normaliseRichTextFeatures(RICHTEXT_FEATURES).features;
34
40
  function foldField(f) {
35
41
  const out = { ...f };
36
42
  if (out.fields) out.fields = out.fields.map(foldField);
@@ -40,6 +46,13 @@ function foldField(f) {
40
46
  ...out,
41
47
  itemNoun: void 0
42
48
  })) delete out.itemNoun;
49
+ if (out.references && out.references.posts === void 0 && out.references.authors === void 0) delete out.references;
50
+ if (Array.isArray(out.features)) {
51
+ const { features, unknown } = normaliseRichTextFeatures(out.features.filter((x) => typeof x === "string"));
52
+ const names = [.../* @__PURE__ */ new Set([...features, ...unknown])];
53
+ if (unknown.length === 0 && ALL_RICH_FEATURES.every((x) => names.includes(x))) delete out.features;
54
+ else out.features = names;
55
+ }
43
56
  return out;
44
57
  }
45
58
  /** The fieldsets fields name, depth first, the way the adapter visits them (fromTypeSchema propSetsFor / fieldsetsFor). */
@@ -76,11 +89,16 @@ function embedProd(c, lane) {
76
89
  function normProd(prod, lane, opts = {}) {
77
90
  return normV2(lane === "fields" ? toTypeSchema(prod, opts) : propsToTypeSchema(prod, opts));
78
91
  }
79
- /** Byte-stable serialisation of C: the hash input, and the form fixtures are frozen in. */
80
- var coreJson = (c) => JSON.stringify(canonical(c));
81
- /** `schema_hash`: "v1:" + sha256 of coreJson (Web Crypto: the same in the browser, Node and Bun). */
92
+ /** Byte-stable serialisation of C: types' canonicalJson (the bytes the backend's schema_hash is taken over, same rules). */
93
+ var coreJson = (c) => canonicalJson(c);
94
+ /**
95
+ * A Core hash's prefix: `core<v>:`. Disjoint from the backend's `schema_hash` prefix (`v<n>:`, taken over a stored
96
+ * definition, types' schemaHashInput), so a Core hash can never be read as, or compared with, a stored one.
97
+ */
98
+ var CORE_HASH_PREFIX = `core1:`;
99
+ /** CORE_HASH_PREFIX + sha256 of coreJson (Web Crypto: the same in the browser, Node and Bun). */
82
100
  async function coreHash(c) {
83
- return `v1:${[...new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(coreJson(c))))].map((b) => b.toString(16).padStart(2, "0")).join("")}`;
101
+ return `${CORE_HASH_PREFIX}${[...new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(coreJson(c))))].map((b) => b.toString(16).padStart(2, "0")).join("")}`;
84
102
  }
85
103
  /** The leaf paths where two JSON values differ ("/fields/0/title"); a missing object or list compares as an empty one. */
86
104
  function leafDiff(a, b, path = "", out = []) {
@@ -166,6 +184,6 @@ function preserveOutOfDomain(written, stored, lane) {
166
184
  };
167
185
  }
168
186
  //#endregion
169
- export { CORE_VERSION, canonical, coreHash, coreJson, embedProd, embedV2, leafDiff, normProd, normV2, preserveOutOfDomain, roundTripCore };
187
+ export { CORE_HASH_PREFIX, CORE_VERSION, canonical, coreHash, coreJson, embedProd, embedV2, leafDiff, normProd, normV2, preserveOutOfDomain, roundTripCore };
170
188
 
171
189
  //# sourceMappingURL=core.js.map