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

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.
@@ -1 +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): 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 of: [{ name: \"post\", title: \"Post\", type: \"reference\", reference: { to } }],\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,\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}) {\n const schema = React.useMemo(() => relatedSchema([collection], m.mode, topic), [collection, m.mode, topic]);\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":"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"}
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"}
@@ -107,6 +107,7 @@ export interface FieldSchema {
107
107
  to: string[];
108
108
  filter?: string;
109
109
  noun?: string;
110
+ excludeId?: string;
110
111
  };
111
112
  numeric?: {
112
113
  unit?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","names":[],"sources":["../../src/schema/types.ts"],"sourcesContent":["/**\n * BETTERCMS SCHEMA — the long-term contract for every editable thing (blocks, pages, entries, globals).\n * One schema language → one form engine (`@/components/kit`) → used by BOTH the Visuals inspector and the\n * Content document editor. Modelled on Sanity's schema (types, fieldsets, groups, previews, validation,\n * conditional fields) plus BetterCMS's block slots for nested layouts.\n *\n * Design principles (CPO D8):\n * - Scales to deep nesting without inline-accordion soup: arrays render compact preview rows; opening an item\n * DRILLS IN (panel slides, breadcrumb grows). Small objects may render inline; lightweight settings open in a POPOVER.\n * - Every field has the same chrome: label · help · required · validation · counter · ⋯ menu · presence · comments.\n * - Blocks can contain blocks (slots) → complex layouts (tabs, columns, bento, mega menus, carousels).\n */\n\nimport type { RichTextFeature } from \"@bettercms-ai/types\";\nimport type { Cap } from \"./sources\";\nimport type { TableFieldOptions } from \"./table\";\n\nexport type PrimitiveType =\n | \"string\" // single line\n | \"text\" // multi line, auto-grow\n | \"richtext\" // portable-text style rich text\n | \"number\"\n | \"boolean\"\n | \"slug\"\n | \"url\"\n | \"email\"\n | \"date\"\n | \"datetime\"\n | \"color\"\n | \"select\" // options: single\n | \"multiselect\"\n | \"tags\"\n | \"image\"\n | \"file\"\n | \"video\"\n | \"icon\" // icon picker (Tabler name)\n | \"link\" // url | page ref + target + rel (one unit)\n | \"button\" // label + link + style (one unit — founder rule)\n | \"reference\" // to a collection entry / page / form\n | \"code\"\n | \"json\"\n | \"geopoint\"\n | \"rating\"\n | \"table\"; // typed columns × keyed rows (CPO D42) — value: TableValue (src/schema/table.ts)\n\nexport type CompoundType =\n | \"object\" // nested fields\n | \"array\" // list of `of` (objects, primitives or references)\n | \"slot\"; // list of nested BLOCKS (block-in-block layouts)\n\nexport type SchemaType = PrimitiveType | CompoundType;\n\nexport interface Option {\n value: string;\n label: string;\n icon?: string;\n description?: string;\n}\n\nexport interface Validation {\n required?: boolean;\n min?: number; // chars for strings, items for arrays, value for numbers\n max?: number;\n /** soft limit: warning (amber) instead of error — e.g. SEO title 60 */\n recommendedMax?: number;\n pattern?: string;\n patternMessage?: string;\n unique?: boolean;\n}\n\n/** How a compound field is edited when opened. */\nexport type OpenMode =\n | \"inline\" // render fields in place (small objects, ≤3 primitives)\n | \"drill\" // slide the panel to a child view; breadcrumb grows (default for array items & objects)\n | \"popover\" // anchored popover (link settings, image crop/alt, small option sets)\n | \"dialog\"; // centered modal for heavy editors (rich text full screen, code, table)\n\nexport interface PreviewConfig {\n /** dotted paths into the item value */\n title?: string;\n subtitle?: string;\n media?: string;\n /** Fallback label, e.g. \"Untitled question\" */\n fallback?: string;\n /** Tabler icon name (kebab-case, e.g. \"help-circle\") shown when there is no media. */\n icon?: string;\n}\n\n/** A condition's operator. Absent: `equals` reads as is / is one of, `notEmpty` as has a value. */\nexport type ConditionOp = \"is\" | \"isNot\" | \"in\" | \"notIn\" | \"empty\" | \"notEmpty\" | \"gt\" | \"lt\";\n\n/**\n * \"Show only when\" (V2 types.ts). The field shows only while the rule holds:\n * - `field` names a key beside it (default), in the object that holds those fields (`scope: \"parent\"`) or at the top of\n * the component or CMS item (`scope: \"root\"`). `_variant` is the component's variant (its first variant when unset).\n * - `op` + `value` (is · is not · is one of · is not one of · is empty · has a value · more than · less than). The legacy\n * `equals` / `notEmpty` forms still read the same.\n * - `also`: more rules, combined by `match` (\"all\", the default, or \"any\").\n */\nexport interface Condition {\n field: string;\n equals?: unknown | unknown[];\n notEmpty?: boolean;\n op?: ConditionOp;\n value?: unknown;\n scope?: \"parent\" | \"root\";\n also?: Condition[];\n match?: \"all\" | \"any\";\n}\n\nexport interface FieldSchema {\n name: string; // key in the value object\n title: string; // label (sentence case)\n type: SchemaType;\n /** Help text under the label (CPO D39). Unset → a house convention from `@/schema/help` may apply; \"\" = none. */\n description?: string;\n /** Longer explanation behind the ⓘ next to the label (tooltip). Use when one line can't carry it. */\n helpMore?: string;\n placeholder?: string;\n initialValue?: unknown;\n validation?: Validation;\n options?: Option[]; // select / multiselect / radio-like\n layout?: \"dropdown\" | \"radio\" | \"segmented\" | \"cards\" | \"switch\" | \"checkbox\" | \"slider\" | \"grid\" | \"list\";\n /** compound */\n fields?: FieldSchema[]; // object\n of?: FieldSchema[] | { type: \"block\"; allow?: string[] }; // array items / slot allowed block types\n openMode?: OpenMode;\n preview?: PreviewConfig;\n /** organisation */\n fieldset?: string; // groups fields into a collapsible fieldset\n group?: string; // which tab (Content / Media / Settings / SEO…)\n /** behaviour */\n hidden?: Condition | boolean;\n /** true = never editable in the form (computed or system values). Who may see or change a field is `access`. */\n readOnly?: boolean;\n /** Archived (V2 types.ts): kept in the schema and its values stay stored, but no form renders it. Schema editors show it. */\n archived?: boolean;\n /**\n * D58 field-level access. `view` missing → everyone who can open the form sees it; `edit` missing → the form's own\n * rule (the host's content.edit). A field the lens can't `view` is not rendered; one it can't `edit` renders as plain\n * text. Fields in the `seo` group default to `{ edit: \"seo.edit\" }`.\n */\n access?: FieldAccess;\n localized?: boolean;\n advanced?: boolean; // tucked into the \"Advanced\" fieldset by default (calm UX)\n ai?: boolean; // show the AI assist sparkle\n // reference targets (collection ids, \"page\", \"form\"); `noun`: what the picker calls one (\"author\": \"Select an author\",\n // \"Create new author\"), else the target's (\"CMS item\", \"page\", …)\n reference?: { to: string[]; filter?: string; noun?: string };\n numeric?: { unit?: string; step?: number; min?: number; max?: number }; // number / slider\n /** number: a developer-controlled SCALE (tokens or a stepped range) with an optional custom escape hatch (CPO D41).\n * Value shape → `ScaleValue`. Takes precedence over `layout: \"slider\"` + `numeric` (which still work, fixed-only). */\n scale?: ScaleSchema;\n /** table: column types allowed, developer-locked columns, row limits, caption rule, JSON-LD offered (CPO D42). */\n table?: TableFieldOptions;\n\n /* ── additive (Block Kit v1) ─────────────────────────────────────────── */\n /** Tabler icon name (kebab-case) used on rows, crumbs and the block picker. */\n icon?: string;\n /** text: minimum visible rows. */\n rows?: number;\n /** Value is synced from a global block (\"Navigation\") — edits apply everywhere it is used. */\n synced?: { from: string };\n /** slot: per-child layout settings stored on each child under `_settings` (e.g. bento span). */\n slotSettings?: FieldSchema[];\n /** array: singular noun for \"Add {noun}\" and empty states (\"question\"). Defaults from the item title. */\n itemNoun?: string;\n /** array: how rows render. \"grid\" = tiles (good for images / logos). */\n arrayLayout?: \"list\" | \"grid\";\n /** Help line shown under a field the lens can see but not change (\"Set by the developer in code.\"). Never a key or a lock. */\n lockReason?: string;\n /**\n * Host-only, never persisted: the list's ORDER is set by the site's code (identity rule 36). The rows stay editable;\n * drag and Move up / down are off and the grip slot shows a lock (V2 content/editor/page-sections.tsx:31,155).\n */\n orderLocked?: boolean;\n /** link / image / button: store a plain string (href or URL) instead of the object — for legacy data. */\n valueShape?: \"string\" | \"object\";\n /** image: an inline-SVG icon, which takes SVG files only (prod `config.media: \"icon\"`). */\n svgOnly?: boolean;\n /**\n * A link LABEL's href: the sibling it links with (prod `config.linkHref`), as the owner answered the editable plan's\n * `link-pair` question where the names do not say (`title` + `to`). Read first by `linkLabelHref`.\n */\n linkHref?: string;\n /**\n * date / datetime: time zone handling (CPO D43). true = the editor picks a zone (defaults to the site's) · \"site\" = always\n * the site's zone · \"fixed:<IANA>\" = always that zone · false = no zone. Default: true for datetime, false for date.\n * Zoned values store ISO with offset + the IANA zone: \"2026-10-14T09:00:00+05:30[Asia/Kolkata]\" (RFC 9557).\n */\n timezone?: boolean | \"site\" | `fixed:${string}`;\n /** code: the field's own language and the only languages its picker offers (prod config.language / languages). */\n language?: string;\n languages?: string[];\n /**\n * richtext / document: the blocks and marks the field offers, from types RICHTEXT_FEATURES (prod config.features).\n * Absent = all of them, as V2's rich text offers (normV2 folds the full set to absent). A schema read from JSON may\n * hold an alias (`headings`) or an unknown name: the adapter expands the one and names the other.\n */\n features?: RichTextFeature[];\n /**\n * richtext / document: the content models its related posts and author bios draw from (prod config.references,\n * plan RT8), by id. posts absent: the entry's own collection; authors absent: no author bio is offered.\n */\n references?: { posts?: string; authors?: string };\n}\n\n/** D58: the caps that let someone see / change one field. */\nexport interface FieldAccess {\n view?: Cap;\n edit?: Cap;\n}\n/** Developer-owned values (anchors, custom CSS, ids): only people who can change the schema edit them. */\nexport const DEV_FIELD: FieldAccess = { edit: \"schema.manage\" };\n/** Developer-only settings: hidden unless you can see the schema. */\nexport const DEV_ONLY_FIELD: FieldAccess = { view: \"schema.view\", edit: \"schema.manage\" };\n\n/* ── Scales (CPO D41) ─────────────────────────────────────────────────── */\nexport type ScaleUnit = \"px\" | \"%\" | \"rem\" | \"em\" | \"vw\" | \"vh\";\n\n/** One allowed value of a token scale. `value` is what is stored; `label` is the short readout (\"XL\"). */\nexport interface ScaleStep {\n value: string;\n label: string;\n /** Long name for screen readers and tooltips (\"Extra large\"). Defaults to `label`. */\n title?: string;\n /** Resolved size in px, shown as \"XL · 48px\" and used to place custom values on the track. */\n px?: number;\n /** Resolved CSS when it isn't px (e.g. \"100vh\", \"var(--space-xl)\"). */\n css?: string;\n}\n\n/** A stepped numeric range (continuous with snapping). The stored value is a bare number in `defaultUnit`. */\nexport interface ScaleRange {\n min: number;\n max: number;\n step?: number;\n}\n\n/** Bounds for the custom escape hatch. min / max may be one number or per unit ({ px: 0, \"%\": 0 }). */\nexport interface ScaleCustom {\n min?: number | Partial<Record<ScaleUnit, number>>;\n max?: number | Partial<Record<ScaleUnit, number>>;\n /** Units the editor may type. Defaults to the scale's `units`. */\n units?: ScaleUnit[];\n /** Arrow-key nudge (Shift ×10, Alt ×0.1). Default 1. */\n step?: number;\n}\n\nexport interface ScaleSchema {\n /** Token steps (S M L XL 2XL) or a numeric range ({ min, max, step }). */\n steps: ScaleStep[] | ScaleRange;\n /** Units of the scale; the first is the default. A range with no units is unitless. */\n units?: ScaleUnit[];\n defaultUnit?: ScaleUnit;\n /** false (default) = fixed only: the editor can pick only the allowed values. true / object = fixed + custom. */\n allowCustom?: boolean | ScaleCustom;\n /** Quick picks in the unit menu when custom is allowed (\"50%\", \"100vh\"). */\n presets?: { label?: string; value: ScaleValue }[];\n}\n\n/**\n * How a scale value is STORED — three shapes, told apart by `typeof`, so tokens and custom values never collide and\n * older data (bare token strings, bare numbers) is already valid:\n * - token → the step's value string (\"xl\")\n * - range → a bare number in the default unit (15 → 15%)\n * - custom → an object ({ value: 200, unit: \"px\" })\n */\nexport type ScaleValue = string | number | ScaleCustomValue;\nexport interface ScaleCustomValue {\n value: number;\n unit: ScaleUnit;\n}\n\nexport interface Fieldset {\n name: string;\n title: string;\n description?: string;\n collapsed?: boolean; // collapsed by default\n columns?: 1 | 2; // short fields side by side\n /** CPO D45: the header toggles the section (default true). false = a static heading for 2–3 fields that are always needed. */\n collapsible?: boolean;\n /**\n * CPO D45: \"breakpoints\" = the members are objects, one per screen size (desktop · tablet · phone), shown as ONE group with a\n * segmented switcher instead of three stacked groups. Sibling objects named desktop / laptop / tablet / mobile / phone get\n * this automatically.\n */\n layout?: \"stack\" | \"breakpoints\";\n}\n\nexport interface FieldGroup {\n name: string;\n title: string;\n icon?: string;\n default?: boolean;\n}\n\n/** A developer-defined layout of a block (swap without losing content). */\nexport interface BlockVariant {\n id: string;\n name: string;\n description?: string;\n}\n\n/** A document/block type. */\nexport interface TypeSchema {\n name: string; // e.g. \"hero\", \"journalStory\"\n title: string;\n kind: \"block\" | \"page\" | \"entry\" | \"global\" | \"object\";\n icon?: string;\n description?: string;\n fields: FieldSchema[];\n fieldsets?: Fieldset[];\n groups?: FieldGroup[];\n preview?: PreviewConfig;\n /* ── additive (Block Kit v1) ── */\n /** Block variants (layouts) — shown as a chip on slot rows and a switcher in the block frame. */\n variants?: BlockVariant[];\n /** Block picker category (\"Layout\", \"Content\", \"Media\"…). */\n category?: string;\n /**\n * kind \"block\" only (CPO D55): where the block fits. \"inline\" = it fits a text column, so a rich-text body lists it in\n * the Insert menu's Article group. \"section\" (default) = page-width, reachable in a body only through More components.\n * \"page\" = a whole page or a feed of other content; never inside a body.\n */\n placement?: \"inline\" | \"section\" | \"page\";\n /** Starting value for a new instance (block added to a slot, new document). */\n initialValue?: Record<string, unknown>;\n}\n\n/** Every array item / slot child carries a stable key. Slot children also carry their block type. */\nexport interface KeyedItem {\n _key?: string;\n /** item type name (arrays with several `of` types) or block type (slots) */\n _type?: string;\n}\n/** A block inside a slot: its own props at the root, plus variant and per-slot layout settings. */\nexport interface SlotChild extends KeyedItem {\n _type: string;\n _variant?: string;\n _settings?: Record<string, unknown>;\n [prop: string]: unknown;\n}\n\n/** Path from the root document/block to the field being edited, e.g. [\"items\", 1, \"answer\"] */\nexport type FieldPath = (string | number)[];\n\n/** One crumb in the inspector/editor breadcrumb (drill-in navigation stack). */\nexport interface Crumb {\n path: FieldPath;\n title: string;\n icon?: string;\n /** tableGroup / tableRow / tableColumn: a table field's rows and columns drill like array items (D50) */\n kind: \"root\" | \"object\" | \"item\" | \"block\" | \"button\" | \"history\" | \"tableGroup\" | \"tableRow\" | \"tableColumn\";\n}\n\n/** Reserved path segment: the field's version history as a stacked panel, e.g. [\"title\", \"@history\"]. */\nexport const HISTORY_SEGMENT = \"@history\";\n"],"mappings":";;AAqNA,IAAa,YAAyB,EAAE,MAAM,gBAAgB;;AAE9D,IAAa,iBAA8B;CAAE,MAAM;CAAe,MAAM;AAAgB;;AA8IxF,IAAa,kBAAkB"}
1
+ {"version":3,"file":"types.js","names":[],"sources":["../../src/schema/types.ts"],"sourcesContent":["/**\n * BETTERCMS SCHEMA — the long-term contract for every editable thing (blocks, pages, entries, globals).\n * One schema language → one form engine (`@/components/kit`) → used by BOTH the Visuals inspector and the\n * Content document editor. Modelled on Sanity's schema (types, fieldsets, groups, previews, validation,\n * conditional fields) plus BetterCMS's block slots for nested layouts.\n *\n * Design principles (CPO D8):\n * - Scales to deep nesting without inline-accordion soup: arrays render compact preview rows; opening an item\n * DRILLS IN (panel slides, breadcrumb grows). Small objects may render inline; lightweight settings open in a POPOVER.\n * - Every field has the same chrome: label · help · required · validation · counter · ⋯ menu · presence · comments.\n * - Blocks can contain blocks (slots) → complex layouts (tabs, columns, bento, mega menus, carousels).\n */\n\nimport type { RichTextFeature } from \"@bettercms-ai/types\";\nimport type { Cap } from \"./sources\";\nimport type { TableFieldOptions } from \"./table\";\n\nexport type PrimitiveType =\n | \"string\" // single line\n | \"text\" // multi line, auto-grow\n | \"richtext\" // portable-text style rich text\n | \"number\"\n | \"boolean\"\n | \"slug\"\n | \"url\"\n | \"email\"\n | \"date\"\n | \"datetime\"\n | \"color\"\n | \"select\" // options: single\n | \"multiselect\"\n | \"tags\"\n | \"image\"\n | \"file\"\n | \"video\"\n | \"icon\" // icon picker (Tabler name)\n | \"link\" // url | page ref + target + rel (one unit)\n | \"button\" // label + link + style (one unit — founder rule)\n | \"reference\" // to a collection entry / page / form\n | \"code\"\n | \"json\"\n | \"geopoint\"\n | \"rating\"\n | \"table\"; // typed columns × keyed rows (CPO D42) — value: TableValue (src/schema/table.ts)\n\nexport type CompoundType =\n | \"object\" // nested fields\n | \"array\" // list of `of` (objects, primitives or references)\n | \"slot\"; // list of nested BLOCKS (block-in-block layouts)\n\nexport type SchemaType = PrimitiveType | CompoundType;\n\nexport interface Option {\n value: string;\n label: string;\n icon?: string;\n description?: string;\n}\n\nexport interface Validation {\n required?: boolean;\n min?: number; // chars for strings, items for arrays, value for numbers\n max?: number;\n /** soft limit: warning (amber) instead of error — e.g. SEO title 60 */\n recommendedMax?: number;\n pattern?: string;\n patternMessage?: string;\n unique?: boolean;\n}\n\n/** How a compound field is edited when opened. */\nexport type OpenMode =\n | \"inline\" // render fields in place (small objects, ≤3 primitives)\n | \"drill\" // slide the panel to a child view; breadcrumb grows (default for array items & objects)\n | \"popover\" // anchored popover (link settings, image crop/alt, small option sets)\n | \"dialog\"; // centered modal for heavy editors (rich text full screen, code, table)\n\nexport interface PreviewConfig {\n /** dotted paths into the item value */\n title?: string;\n subtitle?: string;\n media?: string;\n /** Fallback label, e.g. \"Untitled question\" */\n fallback?: string;\n /** Tabler icon name (kebab-case, e.g. \"help-circle\") shown when there is no media. */\n icon?: string;\n}\n\n/** A condition's operator. Absent: `equals` reads as is / is one of, `notEmpty` as has a value. */\nexport type ConditionOp = \"is\" | \"isNot\" | \"in\" | \"notIn\" | \"empty\" | \"notEmpty\" | \"gt\" | \"lt\";\n\n/**\n * \"Show only when\" (V2 types.ts). The field shows only while the rule holds:\n * - `field` names a key beside it (default), in the object that holds those fields (`scope: \"parent\"`) or at the top of\n * the component or CMS item (`scope: \"root\"`). `_variant` is the component's variant (its first variant when unset).\n * - `op` + `value` (is · is not · is one of · is not one of · is empty · has a value · more than · less than). The legacy\n * `equals` / `notEmpty` forms still read the same.\n * - `also`: more rules, combined by `match` (\"all\", the default, or \"any\").\n */\nexport interface Condition {\n field: string;\n equals?: unknown | unknown[];\n notEmpty?: boolean;\n op?: ConditionOp;\n value?: unknown;\n scope?: \"parent\" | \"root\";\n also?: Condition[];\n match?: \"all\" | \"any\";\n}\n\nexport interface FieldSchema {\n name: string; // key in the value object\n title: string; // label (sentence case)\n type: SchemaType;\n /** Help text under the label (CPO D39). Unset → a house convention from `@/schema/help` may apply; \"\" = none. */\n description?: string;\n /** Longer explanation behind the ⓘ next to the label (tooltip). Use when one line can't carry it. */\n helpMore?: string;\n placeholder?: string;\n initialValue?: unknown;\n validation?: Validation;\n options?: Option[]; // select / multiselect / radio-like\n layout?: \"dropdown\" | \"radio\" | \"segmented\" | \"cards\" | \"switch\" | \"checkbox\" | \"slider\" | \"grid\" | \"list\";\n /** compound */\n fields?: FieldSchema[]; // object\n of?: FieldSchema[] | { type: \"block\"; allow?: string[] }; // array items / slot allowed block types\n openMode?: OpenMode;\n preview?: PreviewConfig;\n /** organisation */\n fieldset?: string; // groups fields into a collapsible fieldset\n group?: string; // which tab (Content / Media / Settings / SEO…)\n /** behaviour */\n hidden?: Condition | boolean;\n /** true = never editable in the form (computed or system values). Who may see or change a field is `access`. */\n readOnly?: boolean;\n /** Archived (V2 types.ts): kept in the schema and its values stay stored, but no form renders it. Schema editors show it. */\n archived?: boolean;\n /**\n * D58 field-level access. `view` missing → everyone who can open the form sees it; `edit` missing → the form's own\n * rule (the host's content.edit). A field the lens can't `view` is not rendered; one it can't `edit` renders as plain\n * text. Fields in the `seo` group default to `{ edit: \"seo.edit\" }`.\n */\n access?: FieldAccess;\n localized?: boolean;\n advanced?: boolean; // tucked into the \"Advanced\" fieldset by default (calm UX)\n ai?: boolean; // show the AI assist sparkle\n // reference targets (collection ids, \"page\", \"form\"); `noun`: what the picker calls one (\"author\": \"Select an author\",\n // \"Create new author\"), else the target's (\"CMS item\", \"page\", …)\n // `excludeId`: a record this picker never lists (the entry a rich-text island sits in, which its own form cannot see)\n reference?: { to: string[]; filter?: string; noun?: string; excludeId?: string };\n numeric?: { unit?: string; step?: number; min?: number; max?: number }; // number / slider\n /** number: a developer-controlled SCALE (tokens or a stepped range) with an optional custom escape hatch (CPO D41).\n * Value shape → `ScaleValue`. Takes precedence over `layout: \"slider\"` + `numeric` (which still work, fixed-only). */\n scale?: ScaleSchema;\n /** table: column types allowed, developer-locked columns, row limits, caption rule, JSON-LD offered (CPO D42). */\n table?: TableFieldOptions;\n\n /* ── additive (Block Kit v1) ─────────────────────────────────────────── */\n /** Tabler icon name (kebab-case) used on rows, crumbs and the block picker. */\n icon?: string;\n /** text: minimum visible rows. */\n rows?: number;\n /** Value is synced from a global block (\"Navigation\") — edits apply everywhere it is used. */\n synced?: { from: string };\n /** slot: per-child layout settings stored on each child under `_settings` (e.g. bento span). */\n slotSettings?: FieldSchema[];\n /** array: singular noun for \"Add {noun}\" and empty states (\"question\"). Defaults from the item title. */\n itemNoun?: string;\n /** array: how rows render. \"grid\" = tiles (good for images / logos). */\n arrayLayout?: \"list\" | \"grid\";\n /** Help line shown under a field the lens can see but not change (\"Set by the developer in code.\"). Never a key or a lock. */\n lockReason?: string;\n /**\n * Host-only, never persisted: the list's ORDER is set by the site's code (identity rule 36). The rows stay editable;\n * drag and Move up / down are off and the grip slot shows a lock (V2 content/editor/page-sections.tsx:31,155).\n */\n orderLocked?: boolean;\n /** link / image / button: store a plain string (href or URL) instead of the object — for legacy data. */\n valueShape?: \"string\" | \"object\";\n /** image: an inline-SVG icon, which takes SVG files only (prod `config.media: \"icon\"`). */\n svgOnly?: boolean;\n /**\n * A link LABEL's href: the sibling it links with (prod `config.linkHref`), as the owner answered the editable plan's\n * `link-pair` question where the names do not say (`title` + `to`). Read first by `linkLabelHref`.\n */\n linkHref?: string;\n /**\n * date / datetime: time zone handling (CPO D43). true = the editor picks a zone (defaults to the site's) · \"site\" = always\n * the site's zone · \"fixed:<IANA>\" = always that zone · false = no zone. Default: true for datetime, false for date.\n * Zoned values store ISO with offset + the IANA zone: \"2026-10-14T09:00:00+05:30[Asia/Kolkata]\" (RFC 9557).\n */\n timezone?: boolean | \"site\" | `fixed:${string}`;\n /** code: the field's own language and the only languages its picker offers (prod config.language / languages). */\n language?: string;\n languages?: string[];\n /**\n * richtext / document: the blocks and marks the field offers, from types RICHTEXT_FEATURES (prod config.features).\n * Absent = all of them, as V2's rich text offers (normV2 folds the full set to absent). A schema read from JSON may\n * hold an alias (`headings`) or an unknown name: the adapter expands the one and names the other.\n */\n features?: RichTextFeature[];\n /**\n * richtext / document: the content models its related posts and author bios draw from (prod config.references,\n * plan RT8), by id. posts absent: the entry's own collection; authors absent: no author bio is offered.\n */\n references?: { posts?: string; authors?: string };\n}\n\n/** D58: the caps that let someone see / change one field. */\nexport interface FieldAccess {\n view?: Cap;\n edit?: Cap;\n}\n/** Developer-owned values (anchors, custom CSS, ids): only people who can change the schema edit them. */\nexport const DEV_FIELD: FieldAccess = { edit: \"schema.manage\" };\n/** Developer-only settings: hidden unless you can see the schema. */\nexport const DEV_ONLY_FIELD: FieldAccess = { view: \"schema.view\", edit: \"schema.manage\" };\n\n/* ── Scales (CPO D41) ─────────────────────────────────────────────────── */\nexport type ScaleUnit = \"px\" | \"%\" | \"rem\" | \"em\" | \"vw\" | \"vh\";\n\n/** One allowed value of a token scale. `value` is what is stored; `label` is the short readout (\"XL\"). */\nexport interface ScaleStep {\n value: string;\n label: string;\n /** Long name for screen readers and tooltips (\"Extra large\"). Defaults to `label`. */\n title?: string;\n /** Resolved size in px, shown as \"XL · 48px\" and used to place custom values on the track. */\n px?: number;\n /** Resolved CSS when it isn't px (e.g. \"100vh\", \"var(--space-xl)\"). */\n css?: string;\n}\n\n/** A stepped numeric range (continuous with snapping). The stored value is a bare number in `defaultUnit`. */\nexport interface ScaleRange {\n min: number;\n max: number;\n step?: number;\n}\n\n/** Bounds for the custom escape hatch. min / max may be one number or per unit ({ px: 0, \"%\": 0 }). */\nexport interface ScaleCustom {\n min?: number | Partial<Record<ScaleUnit, number>>;\n max?: number | Partial<Record<ScaleUnit, number>>;\n /** Units the editor may type. Defaults to the scale's `units`. */\n units?: ScaleUnit[];\n /** Arrow-key nudge (Shift ×10, Alt ×0.1). Default 1. */\n step?: number;\n}\n\nexport interface ScaleSchema {\n /** Token steps (S M L XL 2XL) or a numeric range ({ min, max, step }). */\n steps: ScaleStep[] | ScaleRange;\n /** Units of the scale; the first is the default. A range with no units is unitless. */\n units?: ScaleUnit[];\n defaultUnit?: ScaleUnit;\n /** false (default) = fixed only: the editor can pick only the allowed values. true / object = fixed + custom. */\n allowCustom?: boolean | ScaleCustom;\n /** Quick picks in the unit menu when custom is allowed (\"50%\", \"100vh\"). */\n presets?: { label?: string; value: ScaleValue }[];\n}\n\n/**\n * How a scale value is STORED — three shapes, told apart by `typeof`, so tokens and custom values never collide and\n * older data (bare token strings, bare numbers) is already valid:\n * - token → the step's value string (\"xl\")\n * - range → a bare number in the default unit (15 → 15%)\n * - custom → an object ({ value: 200, unit: \"px\" })\n */\nexport type ScaleValue = string | number | ScaleCustomValue;\nexport interface ScaleCustomValue {\n value: number;\n unit: ScaleUnit;\n}\n\nexport interface Fieldset {\n name: string;\n title: string;\n description?: string;\n collapsed?: boolean; // collapsed by default\n columns?: 1 | 2; // short fields side by side\n /** CPO D45: the header toggles the section (default true). false = a static heading for 2–3 fields that are always needed. */\n collapsible?: boolean;\n /**\n * CPO D45: \"breakpoints\" = the members are objects, one per screen size (desktop · tablet · phone), shown as ONE group with a\n * segmented switcher instead of three stacked groups. Sibling objects named desktop / laptop / tablet / mobile / phone get\n * this automatically.\n */\n layout?: \"stack\" | \"breakpoints\";\n}\n\nexport interface FieldGroup {\n name: string;\n title: string;\n icon?: string;\n default?: boolean;\n}\n\n/** A developer-defined layout of a block (swap without losing content). */\nexport interface BlockVariant {\n id: string;\n name: string;\n description?: string;\n}\n\n/** A document/block type. */\nexport interface TypeSchema {\n name: string; // e.g. \"hero\", \"journalStory\"\n title: string;\n kind: \"block\" | \"page\" | \"entry\" | \"global\" | \"object\";\n icon?: string;\n description?: string;\n fields: FieldSchema[];\n fieldsets?: Fieldset[];\n groups?: FieldGroup[];\n preview?: PreviewConfig;\n /* ── additive (Block Kit v1) ── */\n /** Block variants (layouts) — shown as a chip on slot rows and a switcher in the block frame. */\n variants?: BlockVariant[];\n /** Block picker category (\"Layout\", \"Content\", \"Media\"…). */\n category?: string;\n /**\n * kind \"block\" only (CPO D55): where the block fits. \"inline\" = it fits a text column, so a rich-text body lists it in\n * the Insert menu's Article group. \"section\" (default) = page-width, reachable in a body only through More components.\n * \"page\" = a whole page or a feed of other content; never inside a body.\n */\n placement?: \"inline\" | \"section\" | \"page\";\n /** Starting value for a new instance (block added to a slot, new document). */\n initialValue?: Record<string, unknown>;\n}\n\n/** Every array item / slot child carries a stable key. Slot children also carry their block type. */\nexport interface KeyedItem {\n _key?: string;\n /** item type name (arrays with several `of` types) or block type (slots) */\n _type?: string;\n}\n/** A block inside a slot: its own props at the root, plus variant and per-slot layout settings. */\nexport interface SlotChild extends KeyedItem {\n _type: string;\n _variant?: string;\n _settings?: Record<string, unknown>;\n [prop: string]: unknown;\n}\n\n/** Path from the root document/block to the field being edited, e.g. [\"items\", 1, \"answer\"] */\nexport type FieldPath = (string | number)[];\n\n/** One crumb in the inspector/editor breadcrumb (drill-in navigation stack). */\nexport interface Crumb {\n path: FieldPath;\n title: string;\n icon?: string;\n /** tableGroup / tableRow / tableColumn: a table field's rows and columns drill like array items (D50) */\n kind: \"root\" | \"object\" | \"item\" | \"block\" | \"button\" | \"history\" | \"tableGroup\" | \"tableRow\" | \"tableColumn\";\n}\n\n/** Reserved path segment: the field's version history as a stacked panel, e.g. [\"title\", \"@history\"]. */\nexport const HISTORY_SEGMENT = \"@history\";\n"],"mappings":";;AAsNA,IAAa,YAAyB,EAAE,MAAM,gBAAgB;;AAE9D,IAAa,iBAA8B;CAAE,MAAM;CAAe,MAAM;AAAgB;;AA8IxF,IAAa,kBAAkB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bettercms-ai/kit",
3
- "version": "2.0.0-next.4",
3
+ "version": "2.0.0-next.6",
4
4
  "description": "The BetterCMS V2 Block Kit: schema language, form engine, controls and design tokens.",
5
5
  "license": "MIT",
6
6
  "type": "module",