@bettercms-ai/mcp 0.72.2 → 0.73.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2989,6 +2989,7 @@ var PARAM_TEXT = {
2989
2989
  recommendedMax: "a SOFT limit (characters, items or value): the editor warns past it, never refuses",
2990
2990
  openMode: "object/array/group/repeater: how the value (or each row) opens: 'inline', 'drill', 'popover' or 'dialog'",
2991
2991
  fromBuiltin: "a builtin:* id from list_components: copies it into the project; fields you send win",
2992
+ componentPreview: "how ONE instance reads as a ROW in a parent's slot list (a /kit schema's document-level `preview`): {title?, subtitle?, media?} each a TOP-LEVEL prop key (422 preview_key_unknown otherwise), fallback? a literal row label (\u2264120), icon? a dock icon name. null clears it",
2992
2993
  describes: "for editors: what it is and holds; omitted: written for you",
2993
2994
  runtimeBundle: "instead of pageId: the server-app bundle cleanup",
2994
2995
  pageRef: "page id or path ('/de/about'); a bare slug only when one page has it",
@@ -3034,7 +3035,7 @@ var DETACHED_COPY_NOTE = "{overrides?}: a rich-text embed's separate-copy `field
3034
3035
  var UNPROPPED_COPY_NOTE = "A `warnings` entry UNPROPPED_COPY names copy no prop targets, which nobody can edit: add a prop with a `target` for each leaf it lists.";
3035
3036
  var UPDATE_PAGE_IF_MATCH_NOTE = "get_page's `optimisticVersion`: the write applies only at that version, and a 409 means the page changed since, so read it again. Send it whenever you pass blockJson, which REPLACES the whole composition.";
3036
3037
  var SLOT_MULTIPLE_NOTE = "'slot' with config.multiple holds a list [{componentId, overrides?}] (config.maxItems, default 12, at most 50): target.path 'props.children' on a section. V2 slots, 'table' mode 'types': playbook \xA75.";
3037
- var PROP_V2_NOTE = "Props take the fields' V2 keys (\xA714): 'object' = ONE {child: value} from config.fields, \u226410 levels (field_depth_exceeded); datetime|slug|email|phone|json|location|tags|rating|code|reference|multi-reference as that field (a reference names contentModelId, or polymorphic + allowedModelSlugs; page reads hydrate its entries); helpMore, advanced, recommendedMax, openMode, config.scale, ui.layout rows|chips|gallery, ui.preview.fallback, showIf notEmpty|empty.";
3038
+ var PROP_V2_NOTE = "Props take the fields' V2 keys (\xA714): 'object' = ONE {child: value} from config.fields, \u226410 levels (field_depth_exceeded); datetime|slug|email|phone|json|location|tags|rating|code|reference|multi-reference as that field (a reference names contentModelId, or polymorphic + allowedModelSlugs; page reads hydrate its entries); helpMore, advanced, recommendedMax, openMode, config.scale, ui.layout rows|chips|gallery, ui.preview.fallback, showIf notEmpty|empty. Send helpText: null for a prop whose /kit schema has no description (omitted, the platform writes one).";
3038
3039
  var LAYOUT_LOCK_NOTE = "A Global field with `overridable:false` + `lockReason` (add-field `field`, update-field `patch`) refuses page overrides (LAYOUT_FIELD_NOT_OVERRIDABLE); get_layout scope page lists stored ones as `shadowed`.";
3039
3040
 
3040
3041
  // src/entry-query.ts
@@ -3265,7 +3266,13 @@ var propUiObject = uiObject.extend({
3265
3266
  collapsible: z.boolean().optional(),
3266
3267
  layout: z.enum(["stack", "breakpoints"]).optional(),
3267
3268
  order: z.number().int().min(0).max(99).optional()
3268
- }).optional().describe("the group this prop (or sub-field) sits in. The FIRST member defines {id, label, kind?, icon?, collapsed?, description?, columns?, collapsible?, layout?, order?}; later ones send only {id}.")
3269
+ }).optional().describe("the group this prop (or sub-field) sits in. The FIRST member defines {id, label, kind?, icon?, collapsed?, description?, columns?, collapsible?, layout?, order?}; later ones send only {id}."),
3270
+ tab: z.strictObject({
3271
+ id: z.string().min(1).max(40),
3272
+ label: z.string().min(1).max(40).optional(),
3273
+ icon: uiObject.shape.icon,
3274
+ order: z.number().int().min(0).max(99).optional()
3275
+ }).optional().describe("TOP-LEVEL props only: the dock tab this prop sits in (a V2 kit schema `groups` entry: Content, Design, \u2026). The FIRST prop in a tab defines {id, label, icon?, order?}; later ones send only {id}. \u22648 tabs, labels \u226440 chars and unique; a group's members share one tab.")
3269
3276
  });
3270
3277
  var choiceOption = z.union([z.string(), z.object({ label: z.string(), value: z.string(), icon: z.string().optional(), description: z.string().optional() })]);
3271
3278
  var fieldShape = {
@@ -3284,7 +3291,7 @@ var fieldShape = {
3284
3291
  "choices when type is 'select'. A plain string, or {label, value, icon?, description?} when the stored value differs from what the editor shows (e.g. {label:'Sold out', value:'soldOut'}); icon a dock name, description one line \u2264140."
3285
3292
  ),
3286
3293
  config: z.record(z.string(), z.unknown()).optional().describe(
3287
- "per-type config: reference {contentModelId}, multi-reference {contentModelId,min,max}, array {itemType: text|number|date|image|link|richtext}, date {includeTime, timezone}, modular|slot {blockSlugs: ['quote','gallery'], minItems?, maxItems?} (slot: playbook \xA75) \u2014 blockSlugs is REQUIRED, non-empty, and each slug must name an existing kind:'block' model; richtext {inline:true, hostTag:'h2', accept:['h2','p','b','i','u','link'], features:['bold','italic','underline','link','headings'], allowMultipleParagraphs:false} \u2014 for a field bound to ONE element on the page: `hostTag` is that element's tag and `accept` must name it, so the editor's block-style control opens already showing 'Heading 2' instead of 'Paragraph'. Name exactly ONE heading level: publish refuses an <h3> value bound to an <h2> rather than nesting one heading inside the other. Keep 'p' in `accept` or the sanitiser strips a block demoted to Paragraph; limits: text/longtext {minLength,maxLength,pattern,patternMessage}, richtext {minLength,maxLength}, number {validation:{min,max}}, array {minItems,maxItems}, tags {min,max}, rating {min,max,step}"
3294
+ "per-type config: reference {contentModelId}, multi-reference {contentModelId,min,max}, repeater {minItems, maxItems} (the list's item limits; the editor shows 'max N' and stops Add there), array {itemType: text|number|date|image|link|richtext}, date {includeTime, timezone}, modular|slot {blockSlugs: ['quote','gallery'], minItems?, maxItems?} (slot: playbook \xA75) \u2014 blockSlugs is REQUIRED, non-empty, and each slug must name an existing kind:'block' model; richtext {inline:true, hostTag:'h2', accept:['h2','p','b','i','u','link'], features:['bold','italic','underline','link','headings'], allowMultipleParagraphs:false} \u2014 for a field bound to ONE element on the page: `hostTag` is that element's tag and `accept` must name it, so the editor's block-style control opens already showing 'Heading 2' instead of 'Paragraph'. Name exactly ONE heading level: publish refuses an <h3> value bound to an <h2> rather than nesting one heading inside the other. Keep 'p' in `accept` or the sanitiser strips a block demoted to Paragraph; limits: text/longtext {minLength,maxLength,pattern,patternMessage}, richtext {minLength,maxLength}, number {validation:{min,max}}, array {minItems,maxItems}, tags {min,max}, rating {min,max,step}"
3288
3295
  ),
3289
3296
  fields: z.array(z.lazy(() => fieldObject)).optional().describe(
3290
3297
  "NESTED child fields \u2014 REQUIRED for type 'group' (one nested object, a Non-Repeatable Zone like blog_hero \u2192 heading, description, hero_image) and type 'repeater' (a repeatable array of such objects, a Repeatable Zone / section-list like testimonials \u2192 quote, author). A section with repeating items is a 'repeater'; a fixed grouped block is a 'group'. Recurse to any depth \u2014 do NOT flatten zones into separate top-level fields."
@@ -3455,7 +3462,8 @@ function toField(f) {
3455
3462
  return { ...base, type: "array", config: { zones: { nonRepeatable: toFields(f.fields) } } };
3456
3463
  }
3457
3464
  if (f.type === "repeater") {
3458
- return { ...base, type: "array", config: { zones: { repeatable: { fields: toFields(f.fields) } } } };
3465
+ const { minItems, maxItems } = f.config ?? {};
3466
+ return { ...base, type: "array", config: { zones: { repeatable: { fields: toFields(f.fields), ...minItems !== void 0 ? { minItems } : {}, ...maxItems !== void 0 ? { maxItems } : {} } } } };
3459
3467
  }
3460
3468
  if (f.type === "array" && f.config && typeof f.config === "object" && "zones" in f.config) {
3461
3469
  const zones = f.config.zones ?? {};
@@ -3488,8 +3496,8 @@ var UPDATE_MODEL_DESCRIPTION = "Edit a content model: rename it, change descript
3488
3496
  var FIELD_REMOVAL_NOTE = "top-level field keys (from get_content_model) to DELETE from this model with every entry's value for them; needs allowFieldRemoval: true. An unknown key refuses the whole call. Never for a rename: rename the label instead.";
3489
3497
  var ALLOW_FIELD_REMOVAL_NOTE = "true = the user accepts that removeFields discards those fields' data in every entry. There is no dry run on this route: confirm first.";
3490
3498
  var MODEL_IF_MATCH_NOTE = "optimisticVersion from get_content_model (with pageId: get_page backingModel). The write applies only at that version; a 409 version-conflict means it changed since: read it again and re-apply, never retry blindly. REQUIRED with field*, override or a fieldset kind/tab/icon/collapsed (428 without); otherwise, omitted, the last write wins.";
3491
- var COMPONENT_IF_MATCH_NOTE = "optimisticVersion from get_component. The write applies only at that version; a 409 version-conflict means it changed since: read it again and re-apply. REQUIRED with prop*, override or a prop ui icon/itemLabel/group (428 without).";
3492
- var PROP_UI_ARG_NOTE = "top-level prop key \u2192 ui patch, the same vocabulary as a prop's `ui`: each key merges, a null key clears it, null clears the whole ui";
3499
+ var COMPONENT_IF_MATCH_NOTE = "optimisticVersion from get_component. The write applies only at that version; a 409 version-conflict means it changed since: read it again and re-apply. REQUIRED with prop*, override or a prop ui icon/itemLabel/group/tab (428 without).";
3500
+ var PROP_UI_ARG_NOTE = "top-level prop key \u2192 ui patch, the same vocabulary as a prop's `ui` (group and tab included): each key merges, a null key clears it, null clears the whole ui. Patches keep every other prop as stored; sending `props` instead REPLACES the whole array, so echo each prop's stored `ui` (its `ui.group` AND `ui.tab`) or the prop leaves its tab";
3493
3501
  var OVERRIDE_NOTE = "replace chrome a person set (otherwise 409 ui_human_owned names those paths). Send it only on the user's word.";
3494
3502
  var FIELD_UI_ARG_NOTE = "path \u2192 ui patch, a field's `ui` vocabulary: each key merges, a null key clears it, null clears the whole ui";
3495
3503
  var MODEL_UPDATE_KEYS = [
@@ -3657,7 +3665,7 @@ var withProjectId = (def) => ({
3657
3665
  return currentProject.run(arg("projectId"), () => currentBranch.run(arg("branchId"), () => def.handler(args)));
3658
3666
  }
3659
3667
  });
3660
- var autoHelpNote = (args) => typeof args.helpText === "string" && args.helpText.trim() ? "" : " No helpText was sent, so the platform wrote it (helpSource: auto); send helpText with update_field to write your own.";
3668
+ var autoHelpNote = (args) => args.helpText === null || typeof args.helpText === "string" && args.helpText.trim() ? "" : " No helpText was sent, so the platform wrote it (helpSource: auto); send helpText with update_field to write your own.";
3661
3669
  function buildToolDefs(deps) {
3662
3670
  async function withClient(fn) {
3663
3671
  const token = await deps.auth.getAccessToken();
@@ -3964,6 +3972,7 @@ function buildToolDefs(deps) {
3964
3972
  // 'select' — required, choices
3965
3973
  // 'group'/'table'/'object' sub-shape. Typed loosely here (the server validates the full
3966
3974
  // recursive shape, 10 levels at most) so a deep tree needs no Zod mirror in the client.
3975
+ // A sub-field may be a 'slot' (its value at its key in the object or row), with the slot keys above.
3967
3976
  fields: z.array(z.record(z.string(), z.unknown())).optional(),
3968
3977
  min: z.number().optional(),
3969
3978
  // 'number'
@@ -3975,7 +3984,7 @@ function buildToolDefs(deps) {
3975
3984
  // override that omits it still saves — so it is a marker the dashboard renders and
3976
3985
  // get_site_composition counts, never a refusal. Parity: componentPropDefSchema on the server.
3977
3986
  required: z.boolean().optional().describe("mark the field required in the editor. An editor HINT: the API still accepts a placement that leaves it empty, and get_site_composition is what reports one."),
3978
- helpText: z.string().max(200).optional().describe("hint shown under the control, muted (\u2264 200 chars)"),
3987
+ helpText: z.string().max(200).nullable().optional().describe("hint shown under the control, muted (\u2264 200 chars). null = no help on purpose (a /kit schema with no description); omitted = the platform writes it"),
3979
3988
  placeholder: z.string().max(120).optional().describe("ghost text inside a text-ish control (\u2264 120 chars)"),
3980
3989
  // Stored markers on a prop: nothing refuses a prop value for them. Declared so an echo keeps them.
3981
3990
  readOnly: z.union([z.literal(true), z.literal("developer")]).optional(),
@@ -4064,6 +4073,13 @@ function buildToolDefs(deps) {
4064
4073
  "section family, e.g. 'Hero' \u2014 set this with a library category to make the component selectable in the page editor's 'Add a section' picker. Components sharing one are layout variants of one section."
4065
4074
  );
4066
4075
  const allowedOn = z.array(z.string().regex(/^(\*|slug:[a-z0-9-]+|type:(singleton|dynamic|template))$/)).max(100).describe("placement allowlist: *, slug:<page-slug>, and/or type:singleton|dynamic|template");
4076
+ const componentPreviewArg = z.strictObject({
4077
+ title: z.string().min(1).max(64).optional(),
4078
+ subtitle: z.string().min(1).max(64).optional(),
4079
+ media: z.string().min(1).max(64).optional(),
4080
+ fallback: z.string().min(1).max(120).optional(),
4081
+ icon: dockIconArg.optional()
4082
+ }).nullable().describe(PARAM_TEXT.componentPreview);
4067
4083
  const createComponentInput = z.object({
4068
4084
  name: z.string().min(1),
4069
4085
  slug: slug.describe("url-safe unique slug (lowercase letters/numbers/hyphens)"),
@@ -4075,6 +4091,7 @@ function buildToolDefs(deps) {
4075
4091
  projectId: z.string().nullable().optional().describe("owning project id; null creates a workspace-wide global component"),
4076
4092
  allowedOn: allowedOn.optional(),
4077
4093
  description: z.string().optional().describe(PARAM_TEXT.describes),
4094
+ preview: componentPreviewArg.optional(),
4078
4095
  blockJson: z.array(blockObject).default([]).describe("the component's block tree"),
4079
4096
  props: z.array(componentPropObject).default([]).describe(
4080
4097
  "overridable fields. DECLARE A PROP \u2014 WITH A `target` \u2014 for every string, link and image a marketer will ever touch: only the leaves a prop targets are click-to-edit. Each one may carry `ui` to say how its control presents in the inspector \u2014 a collapsed group, what titles and thumbnails a table's rows, whether that table's rows can be dragged at all. 'button' is ONE unit {label, href, variant?, size?, arrow?, icon?, newTab?} on a button block's 'props', never a text prop plus a url prop. 'link' an href, 'date' YYYY-MM-DD, 'color' a hex, 'file' {url, name?}, 'icon' an icon name or an image, 'multiselect' a string[] from config.options (required). " + SLOT_MULTIPLE_NOTE + " " + PROP_V2_NOTE
@@ -4091,14 +4108,16 @@ function buildToolDefs(deps) {
4091
4108
  variantOf: z.string().min(1).max(255).optional().describe("see create_component"),
4092
4109
  allowedOn: allowedOn.optional().describe("REPLACES the placement allowlist"),
4093
4110
  description: z.string().optional(),
4111
+ preview: componentPreviewArg.optional(),
4094
4112
  blockJson: z.array(blockObject).optional().describe("REPLACES the block tree"),
4095
4113
  props: z.array(componentPropObject).optional().describe(
4096
4114
  "REPLACES the prop array \u2014 include every prop you want to keep, WITH its stored `ui`, or that prop's authoring chrome is dropped along with the prop. Read get_component first."
4097
4115
  ),
4098
4116
  propUi: z.record(z.string(), z.record(z.string(), z.unknown()).nullable()).optional().describe(PROP_UI_ARG_NOTE),
4099
4117
  propGroup: z.record(z.string(), z.record(z.string(), z.unknown()).nullable()).optional().describe("prop key \u2192 its ui.group ({id, label?, kind?, icon?, collapsed?}), null takes it out"),
4118
+ propTab: z.record(z.string(), z.record(z.string(), z.unknown()).nullable()).optional().describe("prop key \u2192 its ui.tab ({id, label?, icon?, order?}), the V2 schema group it sits in. The FIRST top-level prop in a tab defines {id, label, icon?, order?}, later ones send {id}; \u22648 tabs; one group's members share one tab. null takes it out"),
4100
4119
  propLabel: z.record(z.string(), z.string().min(1).max(255)).optional().describe("prop key \u2192 label"),
4101
- propHelpText: z.record(z.string(), z.string().max(200).nullable()).optional().describe("prop key \u2192 help text, null clears it"),
4120
+ propHelpText: z.record(z.string(), z.string().max(200).nullable()).optional().describe("prop key \u2192 help text; null = no help on purpose (stays empty, never refilled)"),
4102
4121
  override: z.boolean().optional().describe(OVERRIDE_NOTE),
4103
4122
  ifMatch: z.number().int().min(0).optional().describe(COMPONENT_IF_MATCH_NOTE),
4104
4123
  confirmed: z.boolean().optional().describe(PARAM_TEXT.confirmedInput)
@@ -6521,8 +6540,11 @@ Then declare what an editor may change, via \`props\`:
6521
6540
  ]
6522
6541
 
6523
6542
  Each prop also takes \`required\`, \`helpText\` (\u2264 200 chars) and \`placeholder\` (\u2264 120), which the
6524
- dashboard's prop editor renders. They are HINTS: nothing on the write path refuses a placement
6543
+ dashboard's prop editor renders. \`helpText\` is three-state: a string is yours, \`null\` is no help on
6544
+ purpose (send it for a prop whose /kit schema has no description; nothing refills it), and omitted
6545
+ lets the platform write one. They are HINTS: nothing on the write path refuses a placement
6525
6546
  that leaves a required prop empty, so \`get_site_composition\` is what reports one.
6547
+ A /kit schema's document-level \`preview\` \u2192 the component's \`preview\` ({title?, subtitle?, media?} are top-level prop keys, plus \`fallback?\`, \`icon?\`); slot rows read it.
6526
6548
 
6527
6549
  \u{1F534} **On a components-first page, \`props\` is the ONLY editing surface.** Click-to-edit binds
6528
6550
  \`heading\`/\`text\`/\`button\`/\`image\` blocks; it does **not** bind a \`component\` block, because
@@ -6574,7 +6596,12 @@ A \`slot\` field (V2 blocks inside a block) takes the same \`config.blockSlugs\`
6574
6596
  but stores each child INLINE: \`[{ _key, _type: '<block slug>', _variant?, _settings?, ...values }]\`,
6575
6597
  its values judged by its block's fields. A component's \`slot\` prop with \`config.multiple\`
6576
6598
  takes the same child shape with \`_type\` the COMPONENT id (no \`_variant\`: a variant is its own
6577
- component). A list of several item shapes on a component is a \`table\` prop with
6599
+ component). A \`slot\` may also be a SUB-FIELD: inside a \`group\` / \`object\` prop's
6600
+ \`config.fields\` or a \`table\` row (an item type's \`fields\` too), with the slot prop's
6601
+ config keys (\`componentIds\`, \`multiple\`, \`minItems\`/\`maxItems\`, \`slotSettings\`). Its value
6602
+ sits at its key in the object or row, \`{ componentId, overrides? }\` or a list with
6603
+ \`config.multiple\`, judged like a slot prop's: the same allowlist, depth cap and cycle refusal.
6604
+ A list of several item shapes on a component is a \`table\` prop with
6578
6605
  \`config.mode: 'types'\` and \`config.itemTypes: [{ name, label?, fields }]\`, rows
6579
6606
  \`[{ _key?, _type: '<name>', ...values }]\`.
6580
6607
 
@@ -7030,8 +7057,10 @@ hints the dashboard renders \u2014 they are NOT enforced at write time, so an ov
7030
7057
  required prop still saves; \`get_site_composition\` is what reports those. Use \`select\` with
7031
7058
  \`config.options\` for layout / theme / alignment / spacing variants that change only styling,
7032
7059
  and a SEPARATE variant component when the markup differs. Repeatable list \u2192 \`table\`; fixed
7033
- cluster \u2192 \`group\`. Nesting is capped at TWO levels from the prop (a \`table\` row may hold a
7034
- \`table\`, and that inner row is scalars only). Flatten anything deeper.
7060
+ cluster \u2192 \`group\`. Nesting goes up to 10 levels counting the prop itself. A \`table\` row may hold a
7061
+ \`table\` (rows), a data table (\`config.mode: "data"\`) or a polymorphic list
7062
+ (\`config.mode: "types"\`), so a size guide's rows can each carry a chart and a multi-step
7063
+ form's steps can each carry their fields. Deeper is refused (\`field_depth_exceeded\`).
7035
7064
 
7036
7065
  **5. Design, responsiveness and accessibility.** \`componentize_sections\` and \`npx
7037
7066
  @bettercms-ai/convert --componentize\` move the section's markup VERBATIM \u2014 classes, responsive
@@ -7396,8 +7425,13 @@ them \u2014 except on a \`slot\`, whose children belong to whichever component f
7396
7425
  cannot be resolved at save time. \`update_component\` REPLACES the whole \`props\` array, so echo
7397
7426
  back each prop's stored \`ui\` or you delete it along with the prop. Props have no fieldsets, so a
7398
7427
  prop joins a group with \`ui.group\`: the FIRST prop of a group defines \`{ id, label, kind?, icon?,
7399
- collapsed? }\` (kind \`group\` or \`row\`, never \`tab\`), and every later prop sends \`{ id }\` only. A
7400
- FIELD never takes \`ui.group\`; it joins a fieldset with \`fieldsetId\`.
7428
+ collapsed? }\` (kind \`group\` or \`row\`; a tab is \`ui.tab\`, never a group kind), and every later prop
7429
+ sends \`{ id }\` only. A prop joins a TAB with \`ui.tab\` the same way: the FIRST top-level prop in a tab
7430
+ defines \`{ id, label, icon?, order? }\`, later props send \`{ id }\`; at most 8 tabs, labels \u226440 characters
7431
+ and unique; top-level props only (never a sub-field); all members of one \`ui.group\` share one tab. A V2
7432
+ kit entry's schema \`groups\` and each field's \`group\` map 1:1 to \`ui.tab\`, so its \`design\` group is a
7433
+ tab named Design holding the theme, sticky and similar props. A FIELD never takes \`ui.group\` or
7434
+ \`ui.tab\`; it joins a fieldset with \`fieldsetId\`.
7401
7435
 
7402
7436
  \`\`\`jsonc
7403
7437
  { "key": "slides", "label": "Slides", "type": "table",
@@ -7466,13 +7500,13 @@ Importance orders a screen: required fields first, then array order.
7466
7500
  | A star rating | \`type: 'rating'\`: a number from 0 to \`config.max\` (5), \`config.step\` 1 or 0.5 (\`rating_value_invalid\`) |
7467
7501
  | A code snippet | \`type: 'code'\`: \`{ language?, code }\` or a string; \`config.language\` default, \`config.languages\` allowlist (\`code_value_invalid\`) |
7468
7502
  | "Add question" instead of "Add item" | \`ui.itemLabel: 'Question'\` (V2 \`itemNoun\`; there is no itemNoun key) |
7469
- | The same section as a component (the dock edits a page section through its props) | the same keys on its \`props\`: \`type: 'object'\` with \`config.fields\` (\`group\` and \`table\` too, 10 levels), \`datetime\`, \`slug\`, \`email\`, \`phone\`, \`json\`, \`location\`, \`tags\`, \`rating\`, \`code\`, plus the two CMS-item types (\`reference\`, \`multi-reference\`: \`config.contentModelId\` or \`polymorphic\` + \`allowedModelSlugs\`; page reads hydrate them, a text slot shows the title), and every key above except fieldsets (props group with \`ui.group\`) |
7503
+ | The same section as a component (the dock edits a page section through its props) | the same keys on its \`props\`: \`type: 'object'\` with \`config.fields\` (\`group\` and \`table\` too, 10 levels), \`datetime\`, \`slug\`, \`email\`, \`phone\`, \`json\`, \`location\`, \`tags\`, \`rating\`, \`code\`, plus the two CMS-item types (\`reference\`, \`multi-reference\`: \`config.contentModelId\` or \`polymorphic\` + \`allowedModelSlugs\`; page reads hydrate them, a text slot shows the title), and every key above except fieldsets (props group with \`ui.group\` and tab with \`ui.tab\`) |
7470
7504
 
7471
7505
  An unknown \`openMode\` is refused with \`openMode_invalid\`, one on a leaf \`openMode_leaf\`; a bad \`config.scale\` with \`scale_invalid\`; more than 10 nested field levels with \`field_depth_exceeded\`. update_field sets and clears (null) helpMore, advanced, recommendedMax and openMode on an existing field.
7472
7506
 
7473
7507
  **Tabs.** A \`kind: 'tab'\` fieldset is a tab:
7474
- - at most **3** tabs, names at most 20 characters and unique;
7475
- - top-level fields only: tabs never nest and props never take them;
7508
+ - at most **3** tabs, names at most 40 characters and unique;
7509
+ - top-level fields only: tabs never nest (a component's props take tabs through \`ui.tab\`, its own rules);
7476
7510
  - untabbed fields stay PINNED above the strip;
7477
7511
  - a group or row fieldset joins a tab with \`tab: '<tab id>'\`; groups in a non-first tab default open;
7478
7512
  - with fewer than 2 non-empty tabs no strip renders, so a tab for one fieldset is noise.
@@ -7491,7 +7525,7 @@ An unknown \`openMode\` is refused with \`openMode_invalid\`, one on a leaf \`op
7491
7525
  \`fieldLabel\`, \`fieldHelpText\` (keyed by dotted key path, \`'hero.title'\`) and \`fieldsets\`. A page's
7492
7526
  schema: the same call with \`pageId\` instead of \`modelId\`, and the version from \`get_page\`'s
7493
7527
  \`backingModel\`.
7494
- - A component's props: \`update_component\` with \`propUi\`, \`propGroup\`, \`propLabel\`, \`propHelpText\` (keyed by prop key).
7528
+ - A component's props: \`update_component\` with \`propUi\`, \`propGroup\`, \`propTab\`, \`propLabel\`, \`propHelpText\` (keyed by prop key).
7495
7529
  Name props from the code (\`plan.cents\` \u2192 Cents); help text guides ("Whole number, no currency symbol."), never repeats
7496
7530
  the value; repeated cards are one \`table\` prop. An icon (\`config.media: "icon"\`) is content: group it, never prune it.
7497
7531
  They write the DRAFT props; no publish is needed for the editor to show them.
@@ -7504,6 +7538,8 @@ An unknown \`openMode\` is refused with \`openMode_invalid\`, one on a leaf \`op
7504
7538
  - **409** \`ui_human_owned\` \u2014 a person shaped those paths. Leave them, or send \`override: true\`, and
7505
7539
  only when the user said so.
7506
7540
  - \`tab_unknown\`, \`tab_nested\`, \`tab_props\`, \`tab_max\`, \`tab_name\`, \`tab_depth\` \u2014 the tab rules above.
7541
+ - \`prop_tab_unknown\`, \`prop_tab_redefined\`, \`prop_group_split_tabs\` \u2014 a prop's \`ui.tab\` rules (\`tab_max\`,
7542
+ \`tab_name\` and \`tab_nested\` too); \`tab_props\` is a \`ui.group\` with \`kind: 'tab'\` \u2014 use \`ui.tab\`.
7507
7543
  - \`fieldset_unknown\` / \`fieldset_orphan\` \u2014 a \`fieldsetId\` naming no fieldset, or a list that drops a
7508
7544
  fieldset its fields still name.
7509
7545
  - \`row_member_type\`, \`row_member_count\`, \`row_member_control\` \u2014 the row rule.
@@ -7629,7 +7665,7 @@ shape, then ask me, once and batched). Icon names are in bettercms://icons/dock.
7629
7665
  - fields that exist: \`update_content_model\` fieldUi / fieldFieldset / fieldOrder / fieldLabel /
7630
7666
  fieldHelpText by field path, plus \`fieldsets\`. A singleton page: send \`pageId\` instead of
7631
7667
  modelId, with its backingModel's version;
7632
- - component props: \`update_component\` propUi / propGroup / propLabel / propHelpText;
7668
+ - component props: \`update_component\` propUi / propGroup / propTab / propLabel / propHelpText;
7633
7669
  - a NEW field: ui / helpText / showIf in the same \`add_field\` call, with \`after\`.
7634
7670
  A 409 \`ui_human_owned\` names chrome a person set: leave it, or ask me and send \`override: true\`
7635
7671
  only on my word.
@@ -7751,7 +7787,7 @@ Read \`bettercms://playbook/schema\` \xA712 first \u2014 this is the short form
7751
7787
  4. **Fields** = \`props\`: text, richtext, image, url, boolean, number, select, group, table, slot.
7752
7788
  Always a \`defaultValue\`; \`required\` / \`helpText\` / \`placeholder\` are editor hints, not write
7753
7789
  -time enforcement. \`select\` for styling-only variants, a separate component when the markup
7754
- differs. Nesting is capped at two levels \u2014 flatten anything deeper.
7790
+ differs. Nesting goes up to 10 levels; a \`table\` row may hold a data table or a polymorphic list.
7755
7791
  5. **Quality.** The componentize lane and \`npx @bettercms-ai/convert --componentize\` move markup
7756
7792
  VERBATIM \u2014 classes, responsive utilities, aria attributes, alt text all survive. Never
7757
7793
  restyle. Dry-run first.