@bettercms-ai/types 1.23.1 → 1.25.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
@@ -13,6 +13,276 @@ interface Workspace {
13
13
  updatedAt: string;
14
14
  }
15
15
 
16
+ /**
17
+ * Generated by scripts/gen-dock-icon-names.mjs from the vendored icon tarball
18
+ * (sha256 0691620e50bdf453c2fed06224d9dc52d521574ab254a149eba47664b92fb889).
19
+ * Do not edit by hand: rerun the script (`--check` fails CI on drift).
20
+ *
21
+ * The closed vocabulary for `ui.icon` (field ui, fieldsets, prop ui.group). `as const` because a
22
+ * z.enum needs the literal tuple; a `readonly string[]` would type every string as valid.
23
+ */
24
+ declare const DOCK_ICON_NAMES: readonly ["access-denied", "activity", "add", "add-circle", "add-comment", "add-document", "add-user", "api", "archive", "arrow-down", "arrow-left", "arrow-right", "arrow-top-right", "arrow-up", "asterisk", "bar-chart", "basket", "bell", "bill", "binary-document", "block-content", "block-element", "blockquote", "bold", "bolt", "book", "bookmark", "bookmark-filled", "bottle", "bug", "bulb-filled", "bulb-outline", "calendar", "case", "chart-upward", "checkmark", "checkmark-circle", "chevron-down", "chevron-left", "chevron-right", "chevron-up", "circle", "clipboard", "clipboard-image", "clock", "close", "close-circle", "code", "code-block", "cog", "collapse", "color-wheel", "comment", "component", "compose", "compose-sparkles", "confetti", "controls", "copy", "credit-card", "crop", "cube", "dashboard", "database", "desktop", "diamond", "document", "document-pdf", "document-remove", "document-sheet", "document-text", "document-video", "document-word", "document-zip", "documents", "dot", "double-chevron-down", "double-chevron-left", "double-chevron-right", "double-chevron-up", "double-quote", "download", "drag-handle", "drop", "earth-americas", "earth-globe", "edit", "ellipsis-horizontal", "ellipsis-vertical", "empty", "enter", "enter-right", "envelope", "equal", "error-filled", "error-outline", "error-screen", "expand", "eye-closed", "eye-open", "face-happy", "face-indifferent", "face-sad", "feedback", "filter", "folder", "generate", "github", "groq", "hash", "heart", "heart-filled", "help-circle", "highlight", "home", "ice-cream", "image", "image-remove", "images", "inbox", "info-filled", "info-outline", "inline", "inline-element", "insert-above", "insert-below", "italic", "joystick", "json", "launch", "leave", "lemon", "link", "link-removed", "linkedin", "list", "lock", "logo-js", "logo-ts", "marker", "marker-removed", "master-detail", "menu", "microphone", "microphone-slash", "mobile-device", "moon", "number", "ok-hand", "olist", "overage", "package", "panel-left", "panel-right", "pause", "pin", "pin-filled", "pin-removed", "play", "plug", "presentation", "progress-50", "progress-75", "projects", "publish", "read-only", "redo", "refresh", "remove", "remove-circle", "reset", "restore", "retrieve", "retry", "revert", "robot", "rocket", "schema", "search", "select", "share", "sort", "sparkle", "sparkles", "spinner", "split-horizontal", "split-vertical", "square", "stack", "stack-compact", "star", "star-filled", "stop", "strikethrough", "string", "sun", "sync", "tablet-device", "tag", "tags", "target", "task", "terminal", "text", "th-large", "th-list", "thumbs-down", "thumbs-up", "tiers", "timeline", "toggle-arrow-right", "token", "transfer", "translate", "trash", "trend-upward", "triangle-outline", "trolley", "truncate", "twitter", "ulist", "unarchive", "underline", "undo", "unknown", "unlink", "unlock", "unpublish", "upload", "user", "users", "versions", "video", "warning-filled", "warning-outline", "wrench"];
25
+ type DockIconName = (typeof DOCK_ICON_NAMES)[number];
26
+ declare const DOCK_ICON_NAME_SET: ReadonlySet<string>;
27
+
28
+ /**
29
+ * ContentModel — the structured content model (schema) for a workspace.
30
+ * Defines which content fields are available for authors to fill in.
31
+ */
32
+ interface ContentModel {
33
+ id: string;
34
+ workspaceId: string;
35
+ name: string;
36
+ description?: string;
37
+ /** 'block' models hold no entries — they are instantiable only inside a `modular` field. */
38
+ kind?: ContentModelKind;
39
+ fields: ContentModelField[];
40
+ /** Authoring-only field grouping for the schema builder. Never affects the API shape. */
41
+ fieldsets?: Fieldset[];
42
+ createdAt: string;
43
+ updatedAt: string;
44
+ }
45
+ /**
46
+ * Canonical field types. Nesting lives on `array` via `config.zones`
47
+ * (see {@link ArrayZoneConfig}): `group`/`repeater` are accepted as legacy input
48
+ * by the API but normalized to `array` + zones on write, so stored/delivered
49
+ * content never contains them.
50
+ */
51
+ 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";
52
+ type ContentModelKind = "model" | "block";
53
+ /**
54
+ * How a fieldset renders in the dock. ABSENT ≡ today's flat list (V2): a legacy `{id, name}`
55
+ * fieldset never regroups anything.
56
+ * - `group` a disclosure; `row` ≤3 pairable scalars on one line; `tab` one of ≤3 top-level tabs.
57
+ */
58
+ type FieldsetKind = "group" | "row" | "tab";
59
+ /** Who last shaped a path's authoring chrome. Server-owned: a client echo is discarded. */
60
+ type UiOrigin = "human" | "agent" | "derived";
61
+ /**
62
+ * The chrome a field had before the first agent write over a non-agent state, kept so a human
63
+ * can revert it. Server-owned, like {@link UiOrigin}.
64
+ */
65
+ interface UiBefore {
66
+ ui?: ContentModelFieldUi;
67
+ fieldsetId?: string;
68
+ /** Index among the field's siblings at the time. */
69
+ position: number;
70
+ label?: string;
71
+ helpText?: string;
72
+ }
73
+ /**
74
+ * A fieldset's chrome before the first agent change over a human/derived (or unstamped) state,
75
+ * kept so a person can revert it. `label` is the set's `name`. Server-owned, like {@link UiOrigin}:
76
+ * frozen while the origin stays `agent`, cleared by a human or derived change, carried by id.
77
+ */
78
+ interface FieldsetUiBefore {
79
+ label: string;
80
+ ui: {
81
+ kind?: FieldsetKind;
82
+ tab?: string;
83
+ icon?: DockIconName;
84
+ collapsed?: boolean;
85
+ };
86
+ }
87
+ /** A named, UI-only grouping of a model's fields. Order is the array index. */
88
+ interface Fieldset {
89
+ id: string;
90
+ name: string;
91
+ /** One line shown under the card title in the editor. Editor-only. */
92
+ description?: string;
93
+ kind?: FieldsetKind;
94
+ icon?: DockIconName;
95
+ /** A `group` that starts closed. */
96
+ collapsed?: boolean;
97
+ /** A group/row that sits inside this `kind:"tab"` fieldset. */
98
+ tab?: string;
99
+ uiOrigin?: UiOrigin;
100
+ uiBefore?: FieldsetUiBefore;
101
+ }
102
+ /**
103
+ * Nested-content config for an `array` field. A zone holds ordinary fields, and a
104
+ * zone field may itself be an `array` with its own zones, so nesting is recursive
105
+ * to any depth.
106
+ * - `nonRepeatable` — a single fixed block of fields (former `group`).
107
+ * - `repeatable` — a repeating list of field-objects (former `repeater`).
108
+ * Mutually exclusive with the primitive form (`config.itemType`).
109
+ */
110
+ interface ArrayZoneConfig {
111
+ nonRepeatable?: ContentModelField[];
112
+ repeatable?: {
113
+ fields: ContentModelField[];
114
+ minItems?: number;
115
+ maxItems?: number;
116
+ };
117
+ }
118
+ /** Per-type config for a field (the typed shape behind the loose `config` bag). */
119
+ interface FieldConfig {
120
+ /** primitive array: list of scalars */
121
+ itemType?: "text" | "number" | "date";
122
+ /** nested array: zoned fields (recursive) */
123
+ zones?: ArrayZoneConfig;
124
+ /** reference / multi-reference */
125
+ contentModelId?: string;
126
+ min?: number;
127
+ max?: number;
128
+ /** date / datetime */
129
+ includeTime?: boolean;
130
+ /** modular: SLUGS of the kind='block' models this field accepts. Non-empty. */
131
+ blockSlugs?: string[];
132
+ /** modular: bounds on how many block instances the field may hold. */
133
+ minItems?: number;
134
+ maxItems?: number;
135
+ /**
136
+ * sections: SLUGS of the sections an author may insert into this zone.
137
+ *
138
+ * Deliberately NOT named `blockSlugs`. That key means "kind='block' content models" and is
139
+ * read by ModularField; these are section-registry slugs resolved from a different place.
140
+ * One key meaning two things is how `blockModels`/`blockSlugs` already drifted apart in the
141
+ * dashboard — a distinct name costs nothing and cannot be confused at a call site.
142
+ *
143
+ * ⚠ ABSENT ≠ EMPTY:
144
+ * absent → UNRESTRICTED, every registry section may be inserted
145
+ * [] → DENY ALL, a deliberate empty restriction
146
+ * [a, b] → only those
147
+ * `[]` means DENY here to match `components.allowed_on` (migration 0190), so two adjacent
148
+ * allow-lists never disagree about what empty means.
149
+ *
150
+ * Absent rather than "all current slugs" because the registry GROWS — it is fed by repo
151
+ * pushes and project components, so materializing today's slugs at field-creation time would
152
+ * silently exclude every section shipped afterwards.
153
+ *
154
+ * NOT READ DIRECTLY. Insertability is this list INTERSECTED with the component's `allowedOn`
155
+ * (where that section may be placed), computed by one resolver so the two cannot drift.
156
+ *
157
+ * Enforced on NEW picks only. Narrowing the list later must never invalidate sections an
158
+ * author already placed, the same rule modular's blockSlugs and component-ref's allowlist
159
+ * follow.
160
+ */
161
+ allowedSections?: string[];
162
+ /** Durable project-local authored Zone mode. Legacy zones omit this key. */
163
+ mode?: "authored-v2";
164
+ /** Durable Zone id. Present when `mode` is `authored-v2`. */
165
+ zoneId?: string;
166
+ /** Explicit migration acknowledgement while the legacy allowlist remains readable. */
167
+ legacyResolved?: boolean;
168
+ legacyMappings?: Record<string, string>;
169
+ [key: string]: unknown;
170
+ }
171
+ /**
172
+ * Where a field renders in the entry editor.
173
+ *
174
+ * AUTHORING ONLY — this has no delivery or storage effect. It does not change the
175
+ * shape of `data`, is not read by any renderer in @bettercms-ai/next or /astro, and
176
+ * cannot move a value onto or off a published page. It decides one thing: where the
177
+ * control sits while somebody is writing.
178
+ *
179
+ * Granularity is REGION-level, never interleaved between body blocks: the canvas is
180
+ * `[hero slots] → [free block flow] → [inline fields] → [panel fields in the drawer]`.
181
+ * A field cannot be positioned "between two paragraphs" — there is no coordinate space
182
+ * for that, and inventing one would mean mounting form controls inside the Lexical tree.
183
+ *
184
+ * - `cover` / `title` / `excerpt` / `body` — the four hero slots, rendered unlabelled
185
+ * and typographically native. SCHEMA-LEVEL ONLY: a per-entry override may never
186
+ * claim one, or one author's click renders anonymous 40px text over anonymous text
187
+ * on that entry alone.
188
+ * - `inline` — in the canvas, below the block flow, labelled.
189
+ * - `panel` — in the drawer.
190
+ *
191
+ * ABSENT is meaningful and is the default for every model that predates this: the
192
+ * editor falls back to its title-gated heuristic. Only an explicit value promotes a
193
+ * field past that gate.
194
+ */
195
+ type ContentModelFieldPlacement = "cover" | "title" | "excerpt" | "body" | "inline" | "panel";
196
+ interface ContentModelField {
197
+ key: string;
198
+ label: string;
199
+ type: ContentModelFieldType;
200
+ required?: boolean;
201
+ /**
202
+ * No two entries of the model may hold the same value (trimmed, case-insensitive). On a
203
+ * repeater child, every row of every entry at that path. Scalar types only.
204
+ */
205
+ unique?: boolean;
206
+ defaultValue?: unknown;
207
+ options?: string[];
208
+ /** Per-type config. For `array`: either `itemType` (primitive) or `zones` (nested). */
209
+ config?: FieldConfig | null;
210
+ /**
211
+ * Which fieldset this field renders under in the schema builder. Authoring-only: it is
212
+ * never delivered and cannot move a value — see {@link Fieldset}.
213
+ */
214
+ fieldsetId?: string;
215
+ /**
216
+ * Authoring-only editor placement. See {@link ContentModelFieldPlacement}.
217
+ * Field ORDER is the array index — there is deliberately no `order` property.
218
+ */
219
+ placement?: ContentModelFieldPlacement;
220
+ /**
221
+ * Authoring chrome — how the field PRESENTS in the editor. Never delivered, never read by a
222
+ * renderer, and never able to move a value; the same contract as {@link placement}.
223
+ *
224
+ * A SIBLING of `config`, not a member of it: `config` is per-type and strict on several
225
+ * types, and the normalizer rebuilds it wholesale for every nesting field — so a `config.ui`
226
+ * survives on leaves and vanishes on exactly the fields `preview` is for.
227
+ *
228
+ * `preview.title` / `preview.media` name a CHILD FIELD KEY of this field (validated on
229
+ * write). Still no `order`: order is the array index, per the note on {@link placement}.
230
+ */
231
+ ui?: ContentModelFieldUi;
232
+ /** Server-owned provenance of this field's chrome. @see UiOrigin */
233
+ uiOrigin?: UiOrigin;
234
+ /** Server-owned revert point. @see UiBefore */
235
+ uiBefore?: UiBefore;
236
+ }
237
+ /** @see ContentModelField.ui */
238
+ interface ContentModelFieldUi {
239
+ /** Start this nesting field's panel collapsed. Absent ≡ the editor's own default. */
240
+ collapsed?: boolean;
241
+ /** How one item of a nesting field summarises itself when collapsed. Keys, not values. */
242
+ preview?: {
243
+ /** Child field key whose value titles the row. */
244
+ title?: string;
245
+ /** Child field key whose value is the row's second line. */
246
+ subtitle?: string;
247
+ /** Child field key whose value is the row's thumbnail. Type is not constrained. */
248
+ media?: string;
249
+ };
250
+ /**
251
+ * How the ITEMS of a nesting field are arranged. ABSENT ≡ `"list"` — every field stored
252
+ * before this key existed means `"list"`, so a reader must treat undefined as list and never
253
+ * as "unset, pick something".
254
+ *
255
+ * Same placement rule as {@link ContentModelFieldUi.preview}: nesting fields only.
256
+ */
257
+ layout?: "list" | "grid" | "table";
258
+ /**
259
+ * Whether the items of a list may be dragged. ABSENT ≡ `true`, for the same reason — nothing
260
+ * stored today carries it, and everything stored today is reorderable.
261
+ *
262
+ * `false` LOCKS the order: a "three steps" band, a semantic nav, a timeline. Valid only where
263
+ * there is a list (a repeatable zone, a `repeater`, or `modular`), so a reader that honours it
264
+ * must disable the sortable behaviour, not just hide the grip — a hidden grip still reorders
265
+ * from the keyboard.
266
+ */
267
+ reorderable?: boolean;
268
+ /**
269
+ * Which editor control a LEAF renders with. ABSENT ≡ the type's default (a dropdown for a
270
+ * `select`, a plain input for a `number`). `segmented` is valid on a single-choice `select`;
271
+ * `slider` on a `number` whose config.min < config.max.
272
+ */
273
+ control?: "segmented" | "slider";
274
+ /** The branch row's icon. Nesting fields only. */
275
+ icon?: DockIconName;
276
+ /** What one item is called ("Slide", "Member"), 1–24 chars. Nesting fields only. */
277
+ itemLabel?: string;
278
+ }
279
+ /**
280
+ * A field inside an array zone — identical to a top-level {@link ContentModelField}.
281
+ * Kept as a named alias so consumers can express "zone field" intent; recursion
282
+ * (an array zone field with its own `config.zones`) is supported.
283
+ */
284
+ type ZoneField = ContentModelField;
285
+
16
286
  /**
17
287
  * Page — a published content page within a workspace.
18
288
  */
@@ -27,6 +297,16 @@ interface Page {
27
297
  publishedAt?: string;
28
298
  createdAt: string;
29
299
  updatedAt: string;
300
+ /**
301
+ * The content model that holds this page's own fields (content_models.page_id), for dock
302
+ * writes: its optimistic version is the If-Match. `null` for a static page or a legacy row with
303
+ * no page_id; absent on reads that do not resolve it.
304
+ */
305
+ backingModel?: {
306
+ id: string;
307
+ optimisticVersion: number;
308
+ fieldsets: Fieldset[] | null;
309
+ } | null;
30
310
  }
31
311
 
32
312
  /**
@@ -107,6 +387,15 @@ interface BlockStyle {
107
387
  border?: boolean;
108
388
  /** 🔴 The one free-form string. MUST go through `safeCssUrl()` before reaching CSS. */
109
389
  bgImage?: string;
390
+ /** A button's look while the pointer is over it. Only the paint changes; unset keys keep the rest. */
391
+ hover?: BlockHoverStyle;
392
+ }
393
+ /** The paint a button swaps to on hover: fill, text colour, and the all-round border. */
394
+ interface BlockHoverStyle {
395
+ bg?: BgToken;
396
+ bgCustom?: string;
397
+ textColor?: ColorToken;
398
+ border?: boolean;
110
399
  }
111
400
  interface HeadingProps {
112
401
  text: string;
@@ -222,6 +511,10 @@ interface ButtonProps {
222
511
  text: string;
223
512
  href: string;
224
513
  variant?: "primary" | "secondary";
514
+ /** Padding + font-size step. Absent ⇒ "md", today's button. sm/lg render `bcms-btn-sm|lg`. */
515
+ size?: "sm" | "md" | "lg";
516
+ /** Stretch to the column's width. Absent ⇒ false. true renders `bcms-btn-full`. */
517
+ fullWidth?: boolean;
225
518
  }
226
519
  interface ButtonBlock {
227
520
  type: "button";
@@ -395,6 +688,14 @@ interface ComponentProps {
395
688
  * declaration's, because the prop holds every row.
396
689
  */
397
690
  source?: Record<string, string>;
691
+ /**
692
+ * A bound placement whose page spells its fields differently from the component's props — a
693
+ * locale copy (`/fr`) placed on its default-locale twin's component: `{ <propKey>: "<this page's
694
+ * field path>" }`. Readers resolve a prop's copy through it before `bind`, since the twin's own
695
+ * group keys (`personnalisez-titre`) are not the component's (`customize-title`). Written only
696
+ * while `bind` is; dropped with it when the instance takes its copy.
697
+ */
698
+ bindPaths?: Record<string, string>;
398
699
  }
399
700
  interface ComponentBlock {
400
701
  type: "component";
@@ -622,214 +923,6 @@ declare function componentInstanceAddresses(instanceBlockId: string, props: read
622
923
  /** A CSS declaration object (key → value) usable as a React `style` prop, or undefined when empty. */
623
924
  declare function blockStyleToCss(style: BlockStyle | undefined): Record<string, string | number> | undefined;
624
925
 
625
- /**
626
- * ContentModel — the structured content model (schema) for a workspace.
627
- * Defines which content fields are available for authors to fill in.
628
- */
629
- interface ContentModel {
630
- id: string;
631
- workspaceId: string;
632
- name: string;
633
- description?: string;
634
- /** 'block' models hold no entries — they are instantiable only inside a `modular` field. */
635
- kind?: ContentModelKind;
636
- fields: ContentModelField[];
637
- /** Authoring-only field grouping for the schema builder. Never affects the API shape. */
638
- fieldsets?: Fieldset[];
639
- createdAt: string;
640
- updatedAt: string;
641
- }
642
- /**
643
- * Canonical field types. Nesting lives on `array` via `config.zones`
644
- * (see {@link ArrayZoneConfig}): `group`/`repeater` are accepted as legacy input
645
- * by the API but normalized to `array` + zones on write, so stored/delivered
646
- * content never contains them.
647
- */
648
- 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";
649
- type ContentModelKind = "model" | "block";
650
- /** A named, UI-only grouping of a model's fields. Order is the array index. */
651
- interface Fieldset {
652
- id: string;
653
- name: string;
654
- /** One line shown under the card title in the editor. Editor-only. */
655
- description?: string;
656
- }
657
- /**
658
- * Nested-content config for an `array` field. A zone holds ordinary fields, and a
659
- * zone field may itself be an `array` with its own zones, so nesting is recursive
660
- * to any depth.
661
- * - `nonRepeatable` — a single fixed block of fields (former `group`).
662
- * - `repeatable` — a repeating list of field-objects (former `repeater`).
663
- * Mutually exclusive with the primitive form (`config.itemType`).
664
- */
665
- interface ArrayZoneConfig {
666
- nonRepeatable?: ContentModelField[];
667
- repeatable?: {
668
- fields: ContentModelField[];
669
- minItems?: number;
670
- maxItems?: number;
671
- };
672
- }
673
- /** Per-type config for a field (the typed shape behind the loose `config` bag). */
674
- interface FieldConfig {
675
- /** primitive array: list of scalars */
676
- itemType?: "text" | "number" | "date";
677
- /** nested array: zoned fields (recursive) */
678
- zones?: ArrayZoneConfig;
679
- /** reference / multi-reference */
680
- contentModelId?: string;
681
- min?: number;
682
- max?: number;
683
- /** date / datetime */
684
- includeTime?: boolean;
685
- /** modular: SLUGS of the kind='block' models this field accepts. Non-empty. */
686
- blockSlugs?: string[];
687
- /** modular: bounds on how many block instances the field may hold. */
688
- minItems?: number;
689
- maxItems?: number;
690
- /**
691
- * sections: SLUGS of the sections an author may insert into this zone.
692
- *
693
- * Deliberately NOT named `blockSlugs`. That key means "kind='block' content models" and is
694
- * read by ModularField; these are section-registry slugs resolved from a different place.
695
- * One key meaning two things is how `blockModels`/`blockSlugs` already drifted apart in the
696
- * dashboard — a distinct name costs nothing and cannot be confused at a call site.
697
- *
698
- * ⚠ ABSENT ≠ EMPTY:
699
- * absent → UNRESTRICTED, every registry section may be inserted
700
- * [] → DENY ALL, a deliberate empty restriction
701
- * [a, b] → only those
702
- * `[]` means DENY here to match `components.allowed_on` (migration 0190), so two adjacent
703
- * allow-lists never disagree about what empty means.
704
- *
705
- * Absent rather than "all current slugs" because the registry GROWS — it is fed by repo
706
- * pushes and project components, so materializing today's slugs at field-creation time would
707
- * silently exclude every section shipped afterwards.
708
- *
709
- * NOT READ DIRECTLY. Insertability is this list INTERSECTED with the component's `allowedOn`
710
- * (where that section may be placed), computed by one resolver so the two cannot drift.
711
- *
712
- * Enforced on NEW picks only. Narrowing the list later must never invalidate sections an
713
- * author already placed, the same rule modular's blockSlugs and component-ref's allowlist
714
- * follow.
715
- */
716
- allowedSections?: string[];
717
- /** Durable project-local authored Zone mode. Legacy zones omit this key. */
718
- mode?: "authored-v2";
719
- /** Durable Zone id. Present when `mode` is `authored-v2`. */
720
- zoneId?: string;
721
- /** Explicit migration acknowledgement while the legacy allowlist remains readable. */
722
- legacyResolved?: boolean;
723
- legacyMappings?: Record<string, string>;
724
- [key: string]: unknown;
725
- }
726
- /**
727
- * Where a field renders in the entry editor.
728
- *
729
- * AUTHORING ONLY — this has no delivery or storage effect. It does not change the
730
- * shape of `data`, is not read by any renderer in @bettercms-ai/next or /astro, and
731
- * cannot move a value onto or off a published page. It decides one thing: where the
732
- * control sits while somebody is writing.
733
- *
734
- * Granularity is REGION-level, never interleaved between body blocks: the canvas is
735
- * `[hero slots] → [free block flow] → [inline fields] → [panel fields in the drawer]`.
736
- * A field cannot be positioned "between two paragraphs" — there is no coordinate space
737
- * for that, and inventing one would mean mounting form controls inside the Lexical tree.
738
- *
739
- * - `cover` / `title` / `excerpt` / `body` — the four hero slots, rendered unlabelled
740
- * and typographically native. SCHEMA-LEVEL ONLY: a per-entry override may never
741
- * claim one, or one author's click renders anonymous 40px text over anonymous text
742
- * on that entry alone.
743
- * - `inline` — in the canvas, below the block flow, labelled.
744
- * - `panel` — in the drawer.
745
- *
746
- * ABSENT is meaningful and is the default for every model that predates this: the
747
- * editor falls back to its title-gated heuristic. Only an explicit value promotes a
748
- * field past that gate.
749
- */
750
- type ContentModelFieldPlacement = "cover" | "title" | "excerpt" | "body" | "inline" | "panel";
751
- interface ContentModelField {
752
- key: string;
753
- label: string;
754
- type: ContentModelFieldType;
755
- required?: boolean;
756
- /**
757
- * No two entries of the model may hold the same value (trimmed, case-insensitive). On a
758
- * repeater child, every row of every entry at that path. Scalar types only.
759
- */
760
- unique?: boolean;
761
- defaultValue?: unknown;
762
- options?: string[];
763
- /** Per-type config. For `array`: either `itemType` (primitive) or `zones` (nested). */
764
- config?: FieldConfig | null;
765
- /**
766
- * Which fieldset this field renders under in the schema builder. Authoring-only: it is
767
- * never delivered and cannot move a value — see {@link Fieldset}.
768
- */
769
- fieldsetId?: string;
770
- /**
771
- * Authoring-only editor placement. See {@link ContentModelFieldPlacement}.
772
- * Field ORDER is the array index — there is deliberately no `order` property.
773
- */
774
- placement?: ContentModelFieldPlacement;
775
- /**
776
- * Authoring chrome — how the field PRESENTS in the editor. Never delivered, never read by a
777
- * renderer, and never able to move a value; the same contract as {@link placement}.
778
- *
779
- * A SIBLING of `config`, not a member of it: `config` is per-type and strict on several
780
- * types, and the normalizer rebuilds it wholesale for every nesting field — so a `config.ui`
781
- * survives on leaves and vanishes on exactly the fields `preview` is for.
782
- *
783
- * `preview.title` / `preview.media` name a CHILD FIELD KEY of this field (validated on
784
- * write). Still no `order`: order is the array index, per the note on {@link placement}.
785
- */
786
- ui?: ContentModelFieldUi;
787
- }
788
- /** @see ContentModelField.ui */
789
- interface ContentModelFieldUi {
790
- /** Start this nesting field's panel collapsed. Absent ≡ the editor's own default. */
791
- collapsed?: boolean;
792
- /** How one item of a nesting field summarises itself when collapsed. Keys, not values. */
793
- preview?: {
794
- /** Child field key whose value titles the row. */
795
- title?: string;
796
- /** Child field key whose value is the row's second line. */
797
- subtitle?: string;
798
- /** Child field key whose value is the row's thumbnail. Type is not constrained. */
799
- media?: string;
800
- };
801
- /**
802
- * How the ITEMS of a nesting field are arranged. ABSENT ≡ `"list"` — every field stored
803
- * before this key existed means `"list"`, so a reader must treat undefined as list and never
804
- * as "unset, pick something".
805
- *
806
- * Same placement rule as {@link ContentModelFieldUi.preview}: nesting fields only.
807
- */
808
- layout?: "list" | "grid" | "table";
809
- /**
810
- * Whether the items of a list may be dragged. ABSENT ≡ `true`, for the same reason — nothing
811
- * stored today carries it, and everything stored today is reorderable.
812
- *
813
- * `false` LOCKS the order: a "three steps" band, a semantic nav, a timeline. Valid only where
814
- * there is a list (a repeatable zone, a `repeater`, or `modular`), so a reader that honours it
815
- * must disable the sortable behaviour, not just hide the grip — a hidden grip still reorders
816
- * from the keyboard.
817
- */
818
- reorderable?: boolean;
819
- /**
820
- * Which editor control a LEAF renders with. ABSENT ≡ the type's default (a dropdown for a
821
- * `select`, a plain input for a `number`). `segmented` is valid on a single-choice `select`;
822
- * `slider` on a `number` whose config.min < config.max.
823
- */
824
- control?: "segmented" | "slider";
825
- }
826
- /**
827
- * A field inside an array zone — identical to a top-level {@link ContentModelField}.
828
- * Kept as a named alias so consumers can express "zone field" intent; recursion
829
- * (an array zone field with its own `config.zones`) is supported.
830
- */
831
- type ZoneField = ContentModelField;
832
-
833
926
  /**
834
927
  * Generated from lucide-react@1.8.0 dynamic icon names.
835
928
  * Legacy CamelCase identifiers remain valid for existing Layout documents.
@@ -1330,8 +1423,28 @@ interface ComponentPropDef {
1330
1423
  * `preview`/`layout` on `group`, `table` and `slot`; `reorderable` on `table` ALONE, the only
1331
1424
  * prop type whose value is a list. The save schema refuses the rest with the valid set named.
1332
1425
  */
1333
- ui?: ContentModelFieldUi;
1426
+ ui?: ComponentPropUi;
1427
+ /** Server-owned provenance of this prop's chrome. @see UiOrigin */
1428
+ uiOrigin?: UiOrigin;
1429
+ /** Server-owned revert point. @see UiBefore */
1430
+ uiBefore?: UiBefore;
1334
1431
  }
1432
+ /**
1433
+ * A top-level prop's dock group. Props have no fieldsets column, so the group rides inline: the
1434
+ * first prop to carry an `id` defines it (label, kind, icon, collapsed), later props name `{id}`.
1435
+ */
1436
+ interface PropGroup {
1437
+ id: string;
1438
+ /** 1–32 chars. Required on the defining prop. */
1439
+ label?: string;
1440
+ kind?: Exclude<FieldsetKind, "tab">;
1441
+ icon?: DockIconName;
1442
+ collapsed?: boolean;
1443
+ }
1444
+ /** A prop's `ui`: the field-lane chrome plus its inline {@link PropGroup}. */
1445
+ type ComponentPropUi = ContentModelFieldUi & {
1446
+ group?: PropGroup;
1447
+ };
1335
1448
  /** One field inside a `group` prop or a `table` row. Recursive, capped at 3 levels deep. */
1336
1449
  interface ComponentSubField {
1337
1450
  key: string;
@@ -1443,6 +1556,158 @@ interface DeliveryComponent {
1443
1556
  props: ComponentPropDef[];
1444
1557
  }
1445
1558
 
1559
+ /**
1560
+ * The dock's click budget, as ONE pure function both ends read (V9): the backend's next-steps
1561
+ * rules call it on live data, and the dashboard's reach test measures its RENDERED dock against the
1562
+ * golden this module emits (src/__tests__/fixtures/dock-budget.golden.json). No dependencies.
1563
+ *
1564
+ * A SCREEN is one navigation level of the dock, modelled on what the dock renders:
1565
+ * - `root` the lane's first screen: the page fields, a page section, or a component's props;
1566
+ * - `list` a repeatable's items, one branch hop below the row that opens it (a repeater, an
1567
+ * `array` with a repeatable zone, a `table` prop, `modular`);
1568
+ * - `item` one item of that list, a second hop. `modular` has no item screen: its items are
1569
+ * other models.
1570
+ * A non-repeatable zone (`group`, `array` + zones.nonRepeatable, a `group` prop) is NOT a screen.
1571
+ * It renders inline as a group, collapsed only when its own `ui.collapsed` is true.
1572
+ *
1573
+ * On a screen:
1574
+ * - fieldsets with no `kind` are FLAT (V2): members render ungrouped, in document order;
1575
+ * - `kind:"group"` is a disclosure placed at its first member; `collapsed` costs 1 activation;
1576
+ * - `kind:"row"` is ONE leaf, however many members it holds;
1577
+ * - `kind:"tab"`, root screens only: a strip renders only with ≥2 non-empty tabs. Tab 1 is the
1578
+ * first panel, each later tab costs 1 activation, untabbed fields are PINNED above the strip,
1579
+ * and a group/row with `tab: <id>` sits in that tab. Below the root a tab reference is flat.
1580
+ * - component props join a group through `ui.group`; the first prop naming an id defines it.
1581
+ *
1582
+ * Budget, per screen (DOCK_BUDGET holds the limits):
1583
+ * - ungroupedLeaves: rows outside any group (a leaf, a row fieldset, a branch), as the max over
1584
+ * panels of pinned + panel. No picker term: tabs are capped at 3, so the strip never overflows.
1585
+ * - maxGroupLeaves: the largest group's own rows (a nested group counts in its own entry).
1586
+ * - depth: branch hops from the lane's root screen.
1587
+ * - maxActivations: the dearest value on the screen counted from the lane's root: +1 per branch
1588
+ * hop, +1 per collapsed group, +1 for a non-first tab. A screen with no values costs its entry.
1589
+ */
1590
+ declare const DOCK_BUDGET: {
1591
+ readonly ungrouped: 8;
1592
+ readonly group: 12;
1593
+ readonly depth: 3;
1594
+ readonly activations: 2;
1595
+ };
1596
+ type DockLane = "page-fields" | "page-section" | "component";
1597
+ interface DockBudget {
1598
+ ungroupedLeaves: number;
1599
+ maxGroupLeaves: number;
1600
+ depth: number;
1601
+ maxActivations: number;
1602
+ }
1603
+ /** One row on a screen. Keys are paths from the screen's own fields (`hero.title`). */
1604
+ type DockNode = {
1605
+ kind: "leaf";
1606
+ key: string;
1607
+ } | {
1608
+ kind: "row";
1609
+ id: string;
1610
+ keys: string[];
1611
+ } | {
1612
+ kind: "branch";
1613
+ key: string;
1614
+ } | {
1615
+ kind: "group";
1616
+ id: string;
1617
+ collapsed: boolean;
1618
+ nodes: DockNode[];
1619
+ };
1620
+ interface DockScreen {
1621
+ /** `<lane>`, then `/<branch key>` per list and `/item` per item: `page-fields/quotes/item`. */
1622
+ id: string;
1623
+ lane: DockLane;
1624
+ kind: "root" | "list" | "item";
1625
+ title: string;
1626
+ depth: number;
1627
+ /** Activations spent reaching this screen from the lane's root. */
1628
+ entry: number;
1629
+ pinned: DockNode[];
1630
+ /** One panel per tab, in fieldset order. Empty when no strip renders. */
1631
+ tabs: DockNode[][];
1632
+ }
1633
+ /** The structural subset of a model field or component prop that this module reads. */
1634
+ interface DockFieldLike {
1635
+ key: string;
1636
+ label?: string;
1637
+ type: string;
1638
+ fieldsetId?: string;
1639
+ ui?: {
1640
+ collapsed?: boolean;
1641
+ group?: DockPropGroupLike;
1642
+ } | null;
1643
+ config?: unknown;
1644
+ /** Legacy `group`/`repeater` children, and a component sub-field's own children. */
1645
+ fields?: DockFieldLike[];
1646
+ }
1647
+ interface DockFieldsetLike {
1648
+ id: string;
1649
+ name?: string;
1650
+ kind?: "group" | "row" | "tab";
1651
+ collapsed?: boolean;
1652
+ tab?: string;
1653
+ }
1654
+ interface DockPropGroupLike {
1655
+ id: string;
1656
+ label?: string;
1657
+ kind?: "group" | "row";
1658
+ collapsed?: boolean;
1659
+ }
1660
+ /**
1661
+ * Every screen of one lane, root first. Pass `props` for a component lane (its `fields` and
1662
+ * `fieldsets` are then ignored): a prop's `ui.group` becomes the fieldset it joins.
1663
+ */
1664
+ declare function dockScreens(source: {
1665
+ fields?: DockFieldLike[] | null;
1666
+ fieldsets?: DockFieldsetLike[] | null;
1667
+ }, lane: DockLane, props?: DockFieldLike[] | null): DockScreen[];
1668
+ declare function dockBudget(screen: DockScreen): DockBudget;
1669
+
1670
+ /**
1671
+ * THE button-link pairing rule, pure: which two sibling fields render as ONE button unit (the words
1672
+ * on the button + where it goes). The dashboard folds them together; the backend refuses a write
1673
+ * that splits such a pair across fieldsets (`pair_split`). One copy, so both ends agree.
1674
+ *
1675
+ * Presentation only: a pair keeps its own keys, values and writers.
1676
+ *
1677
+ * 1. A key is split into words whatever its spelling (`hero_cta_label`, `ctaText`, `button-link`);
1678
+ * a dotted path counts only its last segment.
1679
+ * 2. The LAST word is the role: `label text title cta button btn name` are TEXT roles, `href link
1680
+ * url to` LINK roles. Everything before it is the STEM.
1681
+ * 3. A text candidate is a text-typed field (`text`, `string`) with a text role. A key that is a
1682
+ * button noun (`cta`, `primaryButton`) also offers its full key as a stem.
1683
+ * 4. A link candidate is a link-typed field (`link`, `url`), or a text-typed field with a link role
1684
+ * that is not a media address (`image_url`). A link-typed field whose last word is not a link
1685
+ * role offers both its stem and its full key.
1686
+ * 5. Two fields pair when their stem holds EXACTLY one text and one link candidate. Three or more
1687
+ * candidates on a stem: none pair. A field that would pair two ways pairs neither way.
1688
+ *
1689
+ * When the key's words say nothing, the visible LABEL is read the same way, its stems namespaced
1690
+ * (`label:`) so a label reading only ever meets another label reading.
1691
+ */
1692
+ /** What the rule needs to know about one field. */
1693
+ interface ButtonLinkCandidate {
1694
+ key: string;
1695
+ type?: string;
1696
+ label?: string;
1697
+ /** The current value, when known: a structured value never pairs as text. */
1698
+ value?: unknown;
1699
+ }
1700
+ /** `hero_cta_primary_label` / `ctaText` / `primaryButton-link` → lowercase words. */
1701
+ declare function keyWords(key: string): string[];
1702
+ type Role = {
1703
+ role: "text" | "link";
1704
+ stems: string[];
1705
+ };
1706
+ /** The role one field plays, with every stem it offers, or null. */
1707
+ declare function candidateRole(candidate: ButtonLinkCandidate): Role | null;
1708
+ /** Every unambiguous pair in one sibling list, as `[textIndex, linkIndex]`, in list order. */
1709
+ declare function buttonLinkPairs(candidates: readonly ButtonLinkCandidate[]): Array<[number, number]>;
1710
+
1446
1711
  /**
1447
1712
  * ContentEntry — a single unit of content, representing the
1448
1713
  * published layout of a page: a collection of ordered blocks
@@ -2043,4 +2308,4 @@ declare function regionOf<E extends {
2043
2308
  el: E;
2044
2309
  }[], el: unknown): string | null;
2045
2310
 
2046
- 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, DIALOG_SELECTOR, type DeepReadonly, type DeliveredLayout, type DeliveryComponent, type DeliveryEntry, type DeliveryEntrySeo, type DeliveryList, type DeliveryPage, type DerivedLocator, type ElementLike, 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, LANDMARK_SELECTOR, LAYOUT_SECTION_ICONS, LAYOUT_SECTION_ICON_SET, type LandmarkChainElement, type LandmarkElement, type LayoutComponentItem, type LayoutDataDocument, type LayoutFieldDefinition, type LayoutFieldItem, type LayoutFieldType, type LayoutScalarFieldType, type LayoutSection, type LayoutSectionIcon, type LayoutSectionItem, type LayoutStructureDocument, type LayoutZone, type LocatorGrammar, MAIN_SELECTOR, 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, SECTION_SPACE_TOKENS, SPACE_BOTTOM_TOKENS, SPACE_STEP_TOKENS, type SectionBlock, type SectionProps, type SectionSpaceToken, type ShadowToken, type SignInInput, type SignUpInput, type SiteSeoDefaults, type SlideItem, type SliderBlock, type SliderProps, type SpaceStepToken, 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, isChromeLandmark, landmarkRegions, locatorFor, parsePropsAttribute, readPath, regionOf, spaceTokenCss };
2311
+ 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 BlockHoverStyle, type BlockStyle, type BlockType, type ButtonBlock, type ButtonLinkCandidate, 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 ComponentPropUi, 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, DIALOG_SELECTOR, DOCK_BUDGET, DOCK_ICON_NAMES, DOCK_ICON_NAME_SET, type DeepReadonly, type DeliveredLayout, type DeliveryComponent, type DeliveryEntry, type DeliveryEntrySeo, type DeliveryList, type DeliveryPage, type DerivedLocator, type DockBudget, type DockFieldLike, type DockFieldsetLike, type DockIconName, type DockLane, type DockNode, type DockPropGroupLike, type DockScreen, type ElementLike, type ExpandedBinding, FOOTER_SECTION_ID, type FieldConfig, type Fieldset, type FieldsetKind, type FieldsetUiBefore, 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, LANDMARK_SELECTOR, LAYOUT_SECTION_ICONS, LAYOUT_SECTION_ICON_SET, type LandmarkChainElement, type LandmarkElement, type LayoutComponentItem, type LayoutDataDocument, type LayoutFieldDefinition, type LayoutFieldItem, type LayoutFieldType, type LayoutScalarFieldType, type LayoutSection, type LayoutSectionIcon, type LayoutSectionItem, type LayoutStructureDocument, type LayoutZone, type LocatorGrammar, MAIN_SELECTOR, 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 PropGroup, type RadiusToken, type Redirect, type ReservedLayoutSection, type ResolvedLayoutNode, type RichTextBlock, type RichTextProps, SECTION_DOCTRINE, SECTION_SPACE_TOKENS, SPACE_BOTTOM_TOKENS, SPACE_STEP_TOKENS, type SectionBlock, type SectionProps, type SectionSpaceToken, type ShadowToken, type SignInInput, type SignUpInput, type SiteSeoDefaults, type SlideItem, type SliderBlock, type SliderProps, type SpaceStepToken, type SpaceToken, type SpacerBlock, type SpacerProps, type TabItem, type TabsBlock, type TabsProps, type TextBlock, type TextProps, type ThemeToken, type UiBefore, type UiOrigin, type VariantInputMapping, type VideoBlock, type VideoProps, type WeightToken, type Workspace, type ZoneField, blockStyleToCss, buttonLinkPairs, candidateRole, componentInstanceAddresses, componentPropTargetKey, dockBudget, dockScreens, expandBlockBindings, formatPropsAttribute, getBlockType, isBlock, isChromeLandmark, keyWords, landmarkRegions, locatorFor, parsePropsAttribute, readPath, regionOf, spaceTokenCss };