@bettercms-ai/types 1.16.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -65,6 +65,15 @@ interface BlockStyle {
65
65
  paddingBottom?: number;
66
66
  paddingSides?: number;
67
67
  contentWidth?: ContentWidthToken;
68
+ /**
69
+ * Where the constrained content column sits, when `contentWidth` constrains it.
70
+ *
71
+ * Split out of `align`, which still means text alignment and is still the fallback when this
72
+ * is unset — so everything written before the split renders byte-identically. The two were one
73
+ * token until anything MEASURED a section's text alignment: copying a source site's
74
+ * `text-align: left` would otherwise also shove its column to the page edge.
75
+ */
76
+ contentAlign?: AlignToken;
68
77
  fullHeight?: boolean;
69
78
  /** 0–100. */
70
79
  opacity?: number;
@@ -89,6 +98,8 @@ interface HeadingBlock {
89
98
  }
90
99
  interface TextProps {
91
100
  html: string;
101
+ /** @see RichTextProps.level — the same payload takes the same level. */
102
+ level?: 1 | 2 | 3 | 4 | 5 | 6;
92
103
  }
93
104
  interface TextBlock {
94
105
  type: "text";
@@ -152,6 +163,13 @@ interface RichTextProps {
152
163
  * the stale string silently shadows the newer structure.
153
164
  */
154
165
  content?: PortableTextDocument;
166
+ /**
167
+ * Render the rich copy AS `<h1>`–`<h6>` instead of a paragraph container, so a title keeps
168
+ * its inline marks (a brand-coloured span) AND its heading semantics. Absent ⇒ ordinary rich
169
+ * text, as before. Set ⇒ the value must be ONE line of inline copy; the write schema refuses
170
+ * more. A component prop of type `number` or `select` can target `props.level`.
171
+ */
172
+ level?: 1 | 2 | 3 | 4 | 5 | 6;
155
173
  }
156
174
  interface RichTextBlock {
157
175
  type: "richtext";
@@ -217,6 +235,12 @@ interface VideoProps {
217
235
  poster?: string;
218
236
  autoplay?: boolean;
219
237
  loop?: boolean;
238
+ /**
239
+ * What is said in the video (CPO-093). The block carries its own because its url may be a
240
+ * pasted provider link with no library asset to hang one from. Rendered as <details> after
241
+ * the player and published in the .md twin.
242
+ */
243
+ transcript?: string;
220
244
  }
221
245
  interface VideoBlock {
222
246
  type: "video";
@@ -443,8 +467,13 @@ declare function getBlockType(value: unknown): BlockType | null;
443
467
  * `url` never takes a caret — a URL typed onto the page has nowhere to report a malformed
444
468
  * value — so it is edited in the element toolbar. `array` is a repeater slot: selectable and
445
469
  * locatable, edited in the dock.
470
+ *
471
+ * `document` is a whole Portable Text body: selectable as ONE container, repainted whole from the
472
+ * form, and edited per keyed block inside it. It is not a longer `richtext` — a Body stamped
473
+ * `richtext` goes into the rich-text lane, where an opt-out rewrites the value as an `{html}`
474
+ * envelope and the document's own block structure is destroyed.
446
475
  */
447
- type BindingKind = "text" | "richtext" | "image" | "url" | "number" | "array";
476
+ type BindingKind = "text" | "richtext" | "image" | "document" | "url" | "number" | "array";
448
477
  interface BlockBinding {
449
478
  /** Path template in grammar A, relative to the block's `props`. `[]` = an array to walk. */
450
479
  readonly path: string;
@@ -564,6 +593,206 @@ declare function componentInstanceAddresses(instanceBlockId: string, props: read
564
593
  /** A CSS declaration object (key → value) usable as a React `style` prop, or undefined when empty. */
565
594
  declare function blockStyleToCss(style: BlockStyle | undefined): Record<string, string | number> | undefined;
566
595
 
596
+ /**
597
+ * ContentModel — the structured content model (schema) for a workspace.
598
+ * Defines which content fields are available for authors to fill in.
599
+ */
600
+ interface ContentModel {
601
+ id: string;
602
+ workspaceId: string;
603
+ name: string;
604
+ description?: string;
605
+ /** 'block' models hold no entries — they are instantiable only inside a `modular` field. */
606
+ kind?: ContentModelKind;
607
+ fields: ContentModelField[];
608
+ /** Authoring-only field grouping for the schema builder. Never affects the API shape. */
609
+ fieldsets?: Fieldset[];
610
+ createdAt: string;
611
+ updatedAt: string;
612
+ }
613
+ /**
614
+ * Canonical field types. Nesting lives on `array` via `config.zones`
615
+ * (see {@link ArrayZoneConfig}): `group`/`repeater` are accepted as legacy input
616
+ * by the API but normalized to `array` + zones on write, so stored/delivered
617
+ * content never contains them.
618
+ */
619
+ type ContentModelFieldType = "text" | "richtext" | "document" | "image" | "boolean" | "number" | "select" | "reference" | "multi-reference" | "multiReference" | "array" | "date" | "datetime" | "longtext" | "slug" | "email" | "phone" | "link" | "color" | "json" | "file" | "component-ref" | "modular" | "sections" | "location";
620
+ type ContentModelKind = "model" | "block";
621
+ /** A named, UI-only grouping of a model's fields. Order is the array index. */
622
+ interface Fieldset {
623
+ id: string;
624
+ name: string;
625
+ /** One line shown under the card title in the editor. Editor-only. */
626
+ description?: string;
627
+ }
628
+ /**
629
+ * Nested-content config for an `array` field. A zone holds ordinary fields, and a
630
+ * zone field may itself be an `array` with its own zones, so nesting is recursive
631
+ * to any depth.
632
+ * - `nonRepeatable` — a single fixed block of fields (former `group`).
633
+ * - `repeatable` — a repeating list of field-objects (former `repeater`).
634
+ * Mutually exclusive with the primitive form (`config.itemType`).
635
+ */
636
+ interface ArrayZoneConfig {
637
+ nonRepeatable?: ContentModelField[];
638
+ repeatable?: {
639
+ fields: ContentModelField[];
640
+ minItems?: number;
641
+ maxItems?: number;
642
+ };
643
+ }
644
+ /** Per-type config for a field (the typed shape behind the loose `config` bag). */
645
+ interface FieldConfig {
646
+ /** primitive array: list of scalars */
647
+ itemType?: "text" | "number" | "date";
648
+ /** nested array: zoned fields (recursive) */
649
+ zones?: ArrayZoneConfig;
650
+ /** reference / multi-reference */
651
+ contentModelId?: string;
652
+ min?: number;
653
+ max?: number;
654
+ /** date / datetime */
655
+ includeTime?: boolean;
656
+ /** modular: SLUGS of the kind='block' models this field accepts. Non-empty. */
657
+ blockSlugs?: string[];
658
+ /** modular: bounds on how many block instances the field may hold. */
659
+ minItems?: number;
660
+ maxItems?: number;
661
+ /**
662
+ * sections: SLUGS of the sections an author may insert into this zone.
663
+ *
664
+ * Deliberately NOT named `blockSlugs`. That key means "kind='block' content models" and is
665
+ * read by ModularField; these are section-registry slugs resolved from a different place.
666
+ * One key meaning two things is how `blockModels`/`blockSlugs` already drifted apart in the
667
+ * dashboard — a distinct name costs nothing and cannot be confused at a call site.
668
+ *
669
+ * ⚠ ABSENT ≠ EMPTY:
670
+ * absent → UNRESTRICTED, every registry section may be inserted
671
+ * [] → DENY ALL, a deliberate empty restriction
672
+ * [a, b] → only those
673
+ * `[]` means DENY here to match `components.allowed_on` (migration 0190), so two adjacent
674
+ * allow-lists never disagree about what empty means.
675
+ *
676
+ * Absent rather than "all current slugs" because the registry GROWS — it is fed by repo
677
+ * pushes and project components, so materializing today's slugs at field-creation time would
678
+ * silently exclude every section shipped afterwards.
679
+ *
680
+ * NOT READ DIRECTLY. Insertability is this list INTERSECTED with the component's `allowedOn`
681
+ * (where that section may be placed), computed by one resolver so the two cannot drift.
682
+ *
683
+ * Enforced on NEW picks only. Narrowing the list later must never invalidate sections an
684
+ * author already placed, the same rule modular's blockSlugs and component-ref's allowlist
685
+ * follow.
686
+ */
687
+ allowedSections?: string[];
688
+ /** Durable project-local authored Zone mode. Legacy zones omit this key. */
689
+ mode?: "authored-v2";
690
+ /** Durable Zone id. Present when `mode` is `authored-v2`. */
691
+ zoneId?: string;
692
+ /** Explicit migration acknowledgement while the legacy allowlist remains readable. */
693
+ legacyResolved?: boolean;
694
+ legacyMappings?: Record<string, string>;
695
+ [key: string]: unknown;
696
+ }
697
+ /**
698
+ * Where a field renders in the entry editor.
699
+ *
700
+ * AUTHORING ONLY — this has no delivery or storage effect. It does not change the
701
+ * shape of `data`, is not read by any renderer in @bettercms-ai/next or /astro, and
702
+ * cannot move a value onto or off a published page. It decides one thing: where the
703
+ * control sits while somebody is writing.
704
+ *
705
+ * Granularity is REGION-level, never interleaved between body blocks: the canvas is
706
+ * `[hero slots] → [free block flow] → [inline fields] → [panel fields in the drawer]`.
707
+ * A field cannot be positioned "between two paragraphs" — there is no coordinate space
708
+ * for that, and inventing one would mean mounting form controls inside the Lexical tree.
709
+ *
710
+ * - `cover` / `title` / `excerpt` / `body` — the four hero slots, rendered unlabelled
711
+ * and typographically native. SCHEMA-LEVEL ONLY: a per-entry override may never
712
+ * claim one, or one author's click renders anonymous 40px text over anonymous text
713
+ * on that entry alone.
714
+ * - `inline` — in the canvas, below the block flow, labelled.
715
+ * - `panel` — in the drawer.
716
+ *
717
+ * ABSENT is meaningful and is the default for every model that predates this: the
718
+ * editor falls back to its title-gated heuristic. Only an explicit value promotes a
719
+ * field past that gate.
720
+ */
721
+ type ContentModelFieldPlacement = "cover" | "title" | "excerpt" | "body" | "inline" | "panel";
722
+ interface ContentModelField {
723
+ key: string;
724
+ label: string;
725
+ type: ContentModelFieldType;
726
+ required?: boolean;
727
+ /**
728
+ * No two entries of the model may hold the same value (trimmed, case-insensitive). On a
729
+ * repeater child, every row of every entry at that path. Scalar types only.
730
+ */
731
+ unique?: boolean;
732
+ defaultValue?: unknown;
733
+ options?: string[];
734
+ /** Per-type config. For `array`: either `itemType` (primitive) or `zones` (nested). */
735
+ config?: FieldConfig | null;
736
+ /**
737
+ * Which fieldset this field renders under in the schema builder. Authoring-only: it is
738
+ * never delivered and cannot move a value — see {@link Fieldset}.
739
+ */
740
+ fieldsetId?: string;
741
+ /**
742
+ * Authoring-only editor placement. See {@link ContentModelFieldPlacement}.
743
+ * Field ORDER is the array index — there is deliberately no `order` property.
744
+ */
745
+ placement?: ContentModelFieldPlacement;
746
+ /**
747
+ * Authoring chrome — how the field PRESENTS in the editor. Never delivered, never read by a
748
+ * renderer, and never able to move a value; the same contract as {@link placement}.
749
+ *
750
+ * A SIBLING of `config`, not a member of it: `config` is per-type and strict on several
751
+ * types, and the normalizer rebuilds it wholesale for every nesting field — so a `config.ui`
752
+ * survives on leaves and vanishes on exactly the fields `preview` is for.
753
+ *
754
+ * `preview.title` / `preview.media` name a CHILD FIELD KEY of this field (validated on
755
+ * write). Still no `order`: order is the array index, per the note on {@link placement}.
756
+ */
757
+ ui?: ContentModelFieldUi;
758
+ }
759
+ /** @see ContentModelField.ui */
760
+ interface ContentModelFieldUi {
761
+ /** Start this nesting field's panel collapsed. Absent ≡ the editor's own default. */
762
+ collapsed?: boolean;
763
+ /** How one item of a nesting field summarises itself when collapsed. Keys, not values. */
764
+ preview?: {
765
+ /** Child field key whose value titles the row. */
766
+ title?: string;
767
+ /** Child field key whose value is the row's thumbnail. Type is not constrained. */
768
+ media?: string;
769
+ };
770
+ /**
771
+ * How the ITEMS of a nesting field are arranged. ABSENT ≡ `"list"` — every field stored
772
+ * before this key existed means `"list"`, so a reader must treat undefined as list and never
773
+ * as "unset, pick something".
774
+ *
775
+ * Same placement rule as {@link ContentModelFieldUi.preview}: nesting fields only.
776
+ */
777
+ layout?: "list" | "grid" | "table";
778
+ /**
779
+ * Whether the items of a list may be dragged. ABSENT ≡ `true`, for the same reason — nothing
780
+ * stored today carries it, and everything stored today is reorderable.
781
+ *
782
+ * `false` LOCKS the order: a "three steps" band, a semantic nav, a timeline. Valid only where
783
+ * there is a list (a repeatable zone, a `repeater`, or `modular`), so a reader that honours it
784
+ * must disable the sortable behaviour, not just hide the grip — a hidden grip still reorders
785
+ * from the keyboard.
786
+ */
787
+ reorderable?: boolean;
788
+ }
789
+ /**
790
+ * A field inside an array zone — identical to a top-level {@link ContentModelField}.
791
+ * Kept as a named alias so consumers can express "zone field" intent; recursion
792
+ * (an array zone field with its own `config.zones`) is supported.
793
+ */
794
+ type ZoneField = ContentModelField;
795
+
567
796
  /**
568
797
  * Generated from lucide-react@1.8.0 dynamic icon names.
569
798
  * Legacy CamelCase identifiers remain valid for existing Layout documents.
@@ -940,6 +1169,34 @@ type ComponentCategory = "navbar" | "footer" | "button" | "section" | "slider" |
940
1169
  * "props.text"). Keeping overrides a declared allowlist (not arbitrary nested
941
1170
  * rewrites) keeps instance data small and the contract explicit.
942
1171
  */
1172
+ /**
1173
+ * THE prop vocabulary. One list, imported everywhere a prop type is enumerated.
1174
+ *
1175
+ * It exists as a value (not just a union) because four places used to spell it out by hand —
1176
+ * this file, the Drizzle schema, `COMPONENT_PROP_TYPES` in the backend's Zod schemas, and the
1177
+ * MCP tool shape — and the MCP copy is the dangerous one: `update_component` REPLACES the whole
1178
+ * `props` array, so a type its enum omits cannot be echoed back, and an agent that reads a
1179
+ * component and writes it straight back DESTROYS every prop of that type with a 200.
1180
+ *
1181
+ * Order is not semantic; append rather than re-sort, so a diff stays readable.
1182
+ */
1183
+ declare const COMPONENT_PROP_TYPES: readonly ["text", "richtext", "image", "url", "boolean", "number", "select", "group", "table", "slot", "form"];
1184
+ type ComponentPropType = (typeof COMPONENT_PROP_TYPES)[number];
1185
+ /**
1186
+ * The value a `form` prop STORES: just the id. The form itself is a Form document and stays the
1187
+ * single source of truth for its fields — a component references one, it never re-declares it.
1188
+ *
1189
+ * `.strict()` at the write boundary, so a client that POSTs a hydrated form object alongside the
1190
+ * id gets a 400 rather than a silent strip.
1191
+ *
1192
+ * There is deliberately NO resolved twin of this shape. Every renderer already receives forms the
1193
+ * same way — a catalogue beside the blocks (`BcmsBlocks({blocks, forms})`, the site renderer's
1194
+ * forms map) — and looks the id up in it. A hydrated `{formId, form}` prop value would be a shape
1195
+ * that exists nowhere in delivery.
1196
+ */
1197
+ interface ComponentFormPropRef {
1198
+ formId: string;
1199
+ }
943
1200
  interface ComponentPropDef {
944
1201
  key: string;
945
1202
  label: string;
@@ -969,7 +1226,7 @@ interface ComponentPropDef {
969
1226
  * `reference` is absent on purpose: nothing resolves a content-entry id at component render
970
1227
  * time, so such a prop would save cleanly and render as nothing.
971
1228
  */
972
- type: "text" | "richtext" | "image" | "url" | "boolean" | "number" | "select" | "group" | "table" | "slot";
1229
+ type: ComponentPropType;
973
1230
  defaultValue?: unknown;
974
1231
  /**
975
1232
  * EDITOR HINTS (playbook §12). Stored in the props JSON, so no migration.
@@ -998,13 +1255,26 @@ interface ComponentPropDef {
998
1255
  max?: number;
999
1256
  step?: number;
1000
1257
  } & Record<string, unknown>;
1258
+ /**
1259
+ * AUTHORING CHROME — the same object a content-model FIELD carries ({@link ContentModelFieldUi}),
1260
+ * deliberately: an agent that has learned `ui` once has learned it on both lanes.
1261
+ *
1262
+ * A SIBLING of `config`, not a member of it. `config` is a loose bag, so a `config.ui` already
1263
+ * survived the write — and that is the problem this key fixes: surviving is not declared, and
1264
+ * an undeclared prop is a typo that saves cleanly and renders as nothing.
1265
+ *
1266
+ * WHERE EACH KEY IS LEGAL differs from the field lane, because the vocabulary does:
1267
+ * `preview`/`layout` on `group`, `table` and `slot`; `reorderable` on `table` ALONE, the only
1268
+ * prop type whose value is a list. The save schema refuses the rest with the valid set named.
1269
+ */
1270
+ ui?: ContentModelFieldUi;
1001
1271
  }
1002
1272
  /** One field inside a `group` prop or a `table` row. Recursive, capped at 3 levels deep. */
1003
1273
  interface ComponentSubField {
1004
1274
  key: string;
1005
1275
  label: string;
1006
1276
  /** No `slot`: a nested component instance belongs on the prop itself, not inside a row. */
1007
- type: Exclude<ComponentPropDef["type"], "slot">;
1277
+ type: Exclude<ComponentPropDef["type"], "slot" | "form">;
1008
1278
  defaultValue?: unknown;
1009
1279
  /** The same editor hints as the prop above, one level down. @see ComponentPropDef */
1010
1280
  required?: boolean;
@@ -1124,157 +1394,6 @@ interface ContentEntry {
1124
1394
  updatedAt: string;
1125
1395
  }
1126
1396
 
1127
- /**
1128
- * ContentModel — the structured content model (schema) for a workspace.
1129
- * Defines which content fields are available for authors to fill in.
1130
- */
1131
- interface ContentModel {
1132
- id: string;
1133
- workspaceId: string;
1134
- name: string;
1135
- description?: string;
1136
- /** 'block' models hold no entries — they are instantiable only inside a `modular` field. */
1137
- kind?: ContentModelKind;
1138
- fields: ContentModelField[];
1139
- /** Authoring-only field grouping for the schema builder. Never affects the API shape. */
1140
- fieldsets?: Fieldset[];
1141
- createdAt: string;
1142
- updatedAt: string;
1143
- }
1144
- /**
1145
- * Canonical field types. Nesting lives on `array` via `config.zones`
1146
- * (see {@link ArrayZoneConfig}): `group`/`repeater` are accepted as legacy input
1147
- * by the API but normalized to `array` + zones on write, so stored/delivered
1148
- * content never contains them.
1149
- */
1150
- type ContentModelFieldType = "text" | "richtext" | "document" | "image" | "boolean" | "number" | "select" | "reference" | "multi-reference" | "multiReference" | "array" | "date" | "datetime" | "longtext" | "slug" | "email" | "phone" | "link" | "color" | "json" | "file" | "component-ref" | "modular" | "sections" | "location";
1151
- type ContentModelKind = "model" | "block";
1152
- /** A named, UI-only grouping of a model's fields. Order is the array index. */
1153
- interface Fieldset {
1154
- id: string;
1155
- name: string;
1156
- }
1157
- /**
1158
- * Nested-content config for an `array` field. A zone holds ordinary fields, and a
1159
- * zone field may itself be an `array` with its own zones, so nesting is recursive
1160
- * to any depth.
1161
- * - `nonRepeatable` — a single fixed block of fields (former `group`).
1162
- * - `repeatable` — a repeating list of field-objects (former `repeater`).
1163
- * Mutually exclusive with the primitive form (`config.itemType`).
1164
- */
1165
- interface ArrayZoneConfig {
1166
- nonRepeatable?: ContentModelField[];
1167
- repeatable?: {
1168
- fields: ContentModelField[];
1169
- minItems?: number;
1170
- maxItems?: number;
1171
- };
1172
- }
1173
- /** Per-type config for a field (the typed shape behind the loose `config` bag). */
1174
- interface FieldConfig {
1175
- /** primitive array: list of scalars */
1176
- itemType?: "text" | "number" | "date";
1177
- /** nested array: zoned fields (recursive) */
1178
- zones?: ArrayZoneConfig;
1179
- /** reference / multi-reference */
1180
- contentModelId?: string;
1181
- min?: number;
1182
- max?: number;
1183
- /** date / datetime */
1184
- includeTime?: boolean;
1185
- /** modular: SLUGS of the kind='block' models this field accepts. Non-empty. */
1186
- blockSlugs?: string[];
1187
- /** modular: bounds on how many block instances the field may hold. */
1188
- minItems?: number;
1189
- maxItems?: number;
1190
- /**
1191
- * sections: SLUGS of the sections an author may insert into this zone.
1192
- *
1193
- * Deliberately NOT named `blockSlugs`. That key means "kind='block' content models" and is
1194
- * read by ModularField; these are section-registry slugs resolved from a different place.
1195
- * One key meaning two things is how `blockModels`/`blockSlugs` already drifted apart in the
1196
- * dashboard — a distinct name costs nothing and cannot be confused at a call site.
1197
- *
1198
- * ⚠ ABSENT ≠ EMPTY:
1199
- * absent → UNRESTRICTED, every registry section may be inserted
1200
- * [] → DENY ALL, a deliberate empty restriction
1201
- * [a, b] → only those
1202
- * `[]` means DENY here to match `components.allowed_on` (migration 0190), so two adjacent
1203
- * allow-lists never disagree about what empty means.
1204
- *
1205
- * Absent rather than "all current slugs" because the registry GROWS — it is fed by repo
1206
- * pushes and project components, so materializing today's slugs at field-creation time would
1207
- * silently exclude every section shipped afterwards.
1208
- *
1209
- * NOT READ DIRECTLY. Insertability is this list INTERSECTED with the component's `allowedOn`
1210
- * (where that section may be placed), computed by one resolver so the two cannot drift.
1211
- *
1212
- * Enforced on NEW picks only. Narrowing the list later must never invalidate sections an
1213
- * author already placed, the same rule modular's blockSlugs and component-ref's allowlist
1214
- * follow.
1215
- */
1216
- allowedSections?: string[];
1217
- /** Durable project-local authored Zone mode. Legacy zones omit this key. */
1218
- mode?: "authored-v2";
1219
- /** Durable Zone id. Present when `mode` is `authored-v2`. */
1220
- zoneId?: string;
1221
- /** Explicit migration acknowledgement while the legacy allowlist remains readable. */
1222
- legacyResolved?: boolean;
1223
- legacyMappings?: Record<string, string>;
1224
- [key: string]: unknown;
1225
- }
1226
- /**
1227
- * Where a field renders in the entry editor.
1228
- *
1229
- * AUTHORING ONLY — this has no delivery or storage effect. It does not change the
1230
- * shape of `data`, is not read by any renderer in @bettercms-ai/next or /astro, and
1231
- * cannot move a value onto or off a published page. It decides one thing: where the
1232
- * control sits while somebody is writing.
1233
- *
1234
- * Granularity is REGION-level, never interleaved between body blocks: the canvas is
1235
- * `[hero slots] → [free block flow] → [inline fields] → [panel fields in the drawer]`.
1236
- * A field cannot be positioned "between two paragraphs" — there is no coordinate space
1237
- * for that, and inventing one would mean mounting form controls inside the Lexical tree.
1238
- *
1239
- * - `cover` / `title` / `excerpt` / `body` — the four hero slots, rendered unlabelled
1240
- * and typographically native. SCHEMA-LEVEL ONLY: a per-entry override may never
1241
- * claim one, or one author's click renders anonymous 40px text over anonymous text
1242
- * on that entry alone.
1243
- * - `inline` — in the canvas, below the block flow, labelled.
1244
- * - `panel` — in the drawer.
1245
- *
1246
- * ABSENT is meaningful and is the default for every model that predates this: the
1247
- * editor falls back to its title-gated heuristic. Only an explicit value promotes a
1248
- * field past that gate.
1249
- */
1250
- type ContentModelFieldPlacement = "cover" | "title" | "excerpt" | "body" | "inline" | "panel";
1251
- interface ContentModelField {
1252
- key: string;
1253
- label: string;
1254
- type: ContentModelFieldType;
1255
- required?: boolean;
1256
- defaultValue?: unknown;
1257
- options?: string[];
1258
- /** Per-type config. For `array`: either `itemType` (primitive) or `zones` (nested). */
1259
- config?: FieldConfig | null;
1260
- /**
1261
- * Which fieldset this field renders under in the schema builder. Authoring-only: it is
1262
- * never delivered and cannot move a value — see {@link Fieldset}.
1263
- */
1264
- fieldsetId?: string;
1265
- /**
1266
- * Authoring-only editor placement. See {@link ContentModelFieldPlacement}.
1267
- * Field ORDER is the array index — there is deliberately no `order` property.
1268
- */
1269
- placement?: ContentModelFieldPlacement;
1270
- }
1271
- /**
1272
- * A field inside an array zone — identical to a top-level {@link ContentModelField}.
1273
- * Kept as a named alias so consumers can express "zone field" intent; recursion
1274
- * (an array zone field with its own `config.zones`) is supported.
1275
- */
1276
- type ZoneField = ContentModelField;
1277
-
1278
1397
  /**
1279
1398
  * MediaAsset — a file uploaded to the CMS media library.
1280
1399
  */
@@ -1290,10 +1409,40 @@ interface MediaAsset {
1290
1409
  width?: number;
1291
1410
  height?: number;
1292
1411
  altText?: string;
1412
+ /**
1413
+ * Video/audio only: what is said in the recording (CPO-093). Stored once on the asset and
1414
+ * resolved into every file value that references it. null/absent = nobody has written one.
1415
+ */
1416
+ transcript?: string | null;
1417
+ /**
1418
+ * RESERVED (CPO-093 §2) — caption/subtitle tracks for the player (WCAG 1.2.2). Not rendered,
1419
+ * not writable and not returned yet; the shape is fixed now so the follow-up is additive.
1420
+ */
1421
+ captions?: MediaCaptionTrack[];
1293
1422
  isActive: boolean;
1294
1423
  createdAt: string;
1295
1424
  updatedAt: string;
1296
1425
  }
1426
+ /** RESERVED — one `.vtt` track. See MediaAsset.captions. */
1427
+ interface MediaCaptionTrack {
1428
+ lang: string;
1429
+ label: string;
1430
+ url: string;
1431
+ kind: "captions" | "subtitles";
1432
+ default?: boolean;
1433
+ }
1434
+ /**
1435
+ * A `file` (or `image`) field value as the delivery API returns it. `transcript` is the
1436
+ * RESOLVED value — the referenced video/audio asset's own — so a client never follows the asset
1437
+ * to find it. Present only when the asset is video or audio and has one.
1438
+ */
1439
+ interface MediaFieldValue {
1440
+ id?: string;
1441
+ url: string;
1442
+ name?: string;
1443
+ altText?: string | null;
1444
+ transcript?: string;
1445
+ }
1297
1446
 
1298
1447
  /**
1299
1448
  * Delivery API response shapes — the read surface consumed by framework
@@ -1310,6 +1459,13 @@ interface DeliveryEntry<T = Record<string, unknown>> {
1310
1459
  publishedAt: string | null;
1311
1460
  updatedAt: string | null;
1312
1461
  data: T;
1462
+ /**
1463
+ * The entry's SEO, ready for `<head>`: its collection template's meta and JSON-LD with
1464
+ * `{{field}}` tokens already resolved for this entry, the entry's own values merged over it
1465
+ * field by field, and an empty title/description auto-filled from its content. Feed it to
1466
+ * `resolveSeo(entrySeoInput(entry), siteDefaults)`; there is no token work left to do.
1467
+ */
1468
+ seo?: DeliveryEntrySeo;
1313
1469
  /** Hydrated reference fields, present when `depth >= 1`. */
1314
1470
  _hydration?: Record<string, DeliveryEntry | DeliveryEntry[]>;
1315
1471
  _meta?: {
@@ -1360,6 +1516,12 @@ interface PageMetaJson {
1360
1516
  };
1361
1517
  schema?: Record<string, unknown> | Array<Record<string, unknown>>;
1362
1518
  }
1519
+ /** `DeliveryEntry.seo`: the page meta shape plus the entry's flat title, description and noindex. */
1520
+ interface DeliveryEntrySeo extends PageMetaJson {
1521
+ metaTitle?: string;
1522
+ metaDescription?: string;
1523
+ noindex?: boolean;
1524
+ }
1363
1525
  /**
1364
1526
  * Site-level SEO defaults a headless consumer passes to `resolveSeo`/`buildMetadata`
1365
1527
  * so per-page meta layers over project-wide fallbacks (mirrors the server renderer's
@@ -1444,6 +1606,7 @@ interface FormField {
1444
1606
  equals: string;
1445
1607
  };
1446
1608
  validation?: FormFieldValidation;
1609
+ ui?: FormFieldUi;
1447
1610
  }
1448
1611
  /**
1449
1612
  * Per-field validation rules (FLO-1017). Each key is only meaningful on the field
@@ -1464,6 +1627,27 @@ type FormFieldValidation = {
1464
1627
  /** `text` | `textarea` | `url` — anchored regex, compiled and length-capped at save time. */
1465
1628
  pattern?: string;
1466
1629
  };
1630
+ /**
1631
+ * Per-field PRESENTATION options. Currently only the phone field's country picker.
1632
+ *
1633
+ * Deliberately NOT part of `FormFieldValidation`: that object is "rules the submit endpoint
1634
+ * enforces", and `lib/forms/validate` never reads this one. The save route gates each key to
1635
+ * the field types that actually render it, so a key accepted here is a key something draws.
1636
+ */
1637
+ type FormFieldUi = {
1638
+ /** `phone` — render a country picker beside the number. The value is stored as E.164. */
1639
+ countryPicker?: boolean;
1640
+ /** `phone` — ISO 3166-1 alpha-2, uppercase. The country the picker starts on. */
1641
+ defaultCountry?: string;
1642
+ /**
1643
+ * `phone` — show the country's flag beside the dialling code.
1644
+ *
1645
+ * Only the dashboard's searchable picker can honour this: the site renderers use a native
1646
+ * `<select>`, which cannot contain an SVG, and an emoji flag degrades to two grey letters on
1647
+ * Windows. They show the country NAME instead, which carries the same information.
1648
+ */
1649
+ showFlags?: boolean;
1650
+ };
1467
1651
  /**
1468
1652
  * FormSubmission — a single form submission submitted by a visitor.
1469
1653
  */
@@ -1597,4 +1781,4 @@ type DeepReadonly<T> = T extends Function ? T : T extends object ? {
1597
1781
  */
1598
1782
  type AssertEqual<L, R> = [L] extends [R] ? ([R] extends [L] ? true : false) : false;
1599
1783
 
1600
- export { type AlignToken, type ApiError, type ApiKey, type ApiKeyPermission, type ApiKeyTokenType, type ArrayZoneConfig, type AssertEqual, type AuthResponse, type AuthSession, type AuthUser, BLOCK_BINDINGS, type BetterCMSErrorCode, type BindingKind, type BlockBinding, type BlockStyle, type BlockType, type ButtonBlock, type ButtonProps, type CanonicalInputDefinition, type CollectionBlock, type CollectionProps, type ColorToken, type ColumnsBlock, type ColumnsProps, type Component, type ComponentBlock, type ComponentCategory, type ComponentPropDef, type ComponentProps, type ComponentSubField, type ComponentVariantGroup, type Content, type ContentBlock, type ContentEntry, type ContentModel, type ContentModelField, type ContentModelFieldType, type ContentPageResult, type ContentResponse, type DeepReadonly, type DeliveredLayout, type DeliveryComponent, type DeliveryEntry, type DeliveryList, type DeliveryPage, type ExpandedBinding, FOOTER_SECTION_ID, type FieldConfig, type FontSizeToken, type FooterBlock, type FooterColumn, type FooterProps, type Form, type FormBlock, type FormField, type FormProps, type FormSubmission, type HeadingBlock, type HeadingProps, type ImageBlock, type ImageProps, LAYOUT_SECTION_ICONS, LAYOUT_SECTION_ICON_SET, type LayoutComponentItem, type LayoutDataDocument, type LayoutFieldDefinition, type LayoutFieldItem, type LayoutFieldType, type LayoutScalarFieldType, type LayoutSection, type LayoutSectionIcon, type LayoutSectionItem, type LayoutStructureDocument, type LayoutZone, type ManagementLayoutCommand, type MediaAsset, NAVIGATION_SECTION_ID, type NavLink, type NavbarBlock, type NavbarProps, PAGE_CONTENT_ANCHOR_ID, type Page, type PageLayoutOverrideDataDocument, type PageLayoutOverrideDocument, type PageLayoutOverrideState, type PageLayoutOverrideStructureDocument, type PageLayoutSectionOverride, type PageLayoutSectionStructureOverride, type PageMetaJson, type PageOnlyLayoutSection, type PageSectionPlacement, type PaginatedResult, type Perspective, type PortableTextDocument, type PortableTextNode, type RadiusToken, type Redirect, type ReservedLayoutSection, type ResolvedLayoutNode, type RichTextBlock, type RichTextProps, SECTION_DOCTRINE, type SectionBlock, type SectionProps, type SignInInput, type SignUpInput, type SiteSeoDefaults, type SlideItem, type SliderBlock, type SliderProps, type SpaceToken, type SpacerBlock, type SpacerProps, type TabItem, type TabsBlock, type TabsProps, type TextBlock, type TextProps, type VariantInputMapping, type VideoBlock, type VideoProps, type WeightToken, type Workspace, type ZoneField, blockStyleToCss, componentInstanceAddresses, componentPropTargetKey, expandBlockBindings, formatPropsAttribute, getBlockType, isBlock, parsePropsAttribute, readPath };
1784
+ export { type AlignToken, type ApiError, type ApiKey, type ApiKeyPermission, type ApiKeyTokenType, type ArrayZoneConfig, type AssertEqual, type AuthResponse, type AuthSession, type AuthUser, BLOCK_BINDINGS, type BetterCMSErrorCode, type BgToken, type BindingKind, type BlockBinding, type BlockStyle, type BlockType, type ButtonBlock, type ButtonProps, COMPONENT_PROP_TYPES, type CanonicalInputDefinition, type CollectionBlock, type CollectionProps, type ColorToken, type ColumnsBlock, type ColumnsProps, type Component, type ComponentBlock, type ComponentCategory, type ComponentFormPropRef, type ComponentPropDef, type ComponentPropType, type ComponentProps, type ComponentSubField, type ComponentVariantGroup, type Content, type ContentBlock, type ContentEntry, type ContentModel, type ContentModelField, type ContentModelFieldType, type ContentModelFieldUi, type ContentPageResult, type ContentResponse, type ContentWidthToken, type CornerToken, type DeepReadonly, type DeliveredLayout, type DeliveryComponent, type DeliveryEntry, type DeliveryEntrySeo, type DeliveryList, type DeliveryPage, type ExpandedBinding, FOOTER_SECTION_ID, type FieldConfig, type FontSizeToken, type FooterBlock, type FooterColumn, type FooterProps, type Form, type FormBlock, type FormField, type FormProps, type FormSubmission, type HeadingBlock, type HeadingProps, type ImageBlock, type ImageProps, LAYOUT_SECTION_ICONS, LAYOUT_SECTION_ICON_SET, type LayoutComponentItem, type LayoutDataDocument, type LayoutFieldDefinition, type LayoutFieldItem, type LayoutFieldType, type LayoutScalarFieldType, type LayoutSection, type LayoutSectionIcon, type LayoutSectionItem, type LayoutStructureDocument, type LayoutZone, type ManagementLayoutCommand, type MediaAsset, type MediaCaptionTrack, type MediaFieldValue, NAVIGATION_SECTION_ID, type NavLink, type NavbarBlock, type NavbarProps, PAGE_CONTENT_ANCHOR_ID, type Page, type PageLayoutOverrideDataDocument, type PageLayoutOverrideDocument, type PageLayoutOverrideState, type PageLayoutOverrideStructureDocument, type PageLayoutSectionOverride, type PageLayoutSectionStructureOverride, type PageMetaJson, type PageOnlyLayoutSection, type PageSectionPlacement, type PaginatedResult, type Perspective, type PortableTextDocument, type PortableTextNode, type RadiusToken, type Redirect, type ReservedLayoutSection, type ResolvedLayoutNode, type RichTextBlock, type RichTextProps, SECTION_DOCTRINE, type SectionBlock, type SectionProps, type ShadowToken, type SignInInput, type SignUpInput, type SiteSeoDefaults, type SlideItem, type SliderBlock, type SliderProps, type SpaceToken, type SpacerBlock, type SpacerProps, type TabItem, type TabsBlock, type TabsProps, type TextBlock, type TextProps, type ThemeToken, type VariantInputMapping, type VideoBlock, type VideoProps, type WeightToken, type Workspace, type ZoneField, blockStyleToCss, componentInstanceAddresses, componentPropTargetKey, expandBlockBindings, formatPropsAttribute, getBlockType, isBlock, parsePropsAttribute, readPath };
package/dist/index.js CHANGED
@@ -128,6 +128,19 @@ function blockStyleToCss(style) {
128
128
 
129
129
  // src/component.ts
130
130
  var SECTION_DOCTRINE = "STRUCTURE (separate from schema): a page is composed of SECTIONS. NEVER build a page out of loose top-level heading/text/image/button/spacer blocks \u2014 they cannot be moved, duplicated or swapped as a unit, the visual editor cannot outline or name them, and every one of them becomes its own section in the editor. A hero of a headline, a lede and two CTAs is ONE section, not four. TWO SHAPES, and the choice is about REUSE. (1) A band that appears on more than one page, or that needs layout variants, is a COMPONENT with a `sectionType` \u2014 see create_component. Components sharing a `sectionType` are that section's VARIANTS (one Hero: 'Centered' for the home page and 'Two-column' for about, same prop keys so a swap keeps the content). This is also the only shape the editor's 'Add a section' picker can insert, and the only one that gets a family name and a variant switcher. (2) A genuinely one-off band on a single page is a `section` BLOCK whose `props.children` hold its blocks. THE TRADEOFF, stated in the present tense because it is real today: inside a component, ONLY the leaves a declared prop TARGETS are click-to-edit on the canvas \u2014 each declared prop's `target` is re-keyed to that PLACEMENT's own override address, so editing it changes this page and not the shared definition. A prop with no `target` still SHOWS as a dock control, but its override reaches no rendered slot, so editing it changes nothing on the page. Copy that no prop points at is worse still: no binding and no control, unreachable from click-to-edit AND from the dock, changeable only by editing the component definition, which rewrites every page that places it. A `section` block's children stay click-to-edit unconditionally. So when you choose a component, DECLARE A PROP \u2014 WITH A `target` \u2014 for every string, link and image a marketer will ever touch; a component with un-propped editable copy is the defect, not the component. In the dock an unset prop shows EMPTY and inherits the definition's default, so set props explicitly when you want the current copy visible there. Do not hand-write a band's JSON: start from a built-in section blueprint (list_components returns locked `builtin:*` blueprints with no projectId \u2014 hero-centered, hero-split, feature-grid-three, cta-banner and nine more), each already rooted in a `section` block with its editable leaves declared as props. INLINE its blockJson as a `section` block for a one-off band; for a recurring band, materialize the blueprint with create_component so it becomes project-scoped before implementation validation, Output or publication. Direct `builtin:*` component references exist only for legacy delivery compatibility. Two consecutive call-to-action buttons are two sibling `button` blocks inside the same section \u2014 never a `columns` block, which is a `repeat(N,1fr)` grid and would stretch each CTA to half the container. Buttons are inline-level and flow side by side on their own.";
131
+ var COMPONENT_PROP_TYPES = [
132
+ "text",
133
+ "richtext",
134
+ "image",
135
+ "url",
136
+ "boolean",
137
+ "number",
138
+ "select",
139
+ "group",
140
+ "table",
141
+ "slot",
142
+ "form"
143
+ ];
131
144
 
132
145
  // src/layout-lucide-icons.ts
133
146
  var LAYOUT_SECTION_ICONS = Object.freeze([
@@ -2097,6 +2110,7 @@ var NAVIGATION_SECTION_ID = "navigation";
2097
2110
  var FOOTER_SECTION_ID = "footer";
2098
2111
  export {
2099
2112
  BLOCK_BINDINGS,
2113
+ COMPONENT_PROP_TYPES,
2100
2114
  FOOTER_SECTION_ID,
2101
2115
  LAYOUT_SECTION_ICONS,
2102
2116
  LAYOUT_SECTION_ICON_SET,