@bettercms-ai/mcp 0.73.0 → 0.73.2
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 +46 -17
- package/dist/index.js.map +1 -1
- package/dist/skills/bettercms/SKILL.md +1 -1
- package/dist/skills/bettercms/references/animation.md +47 -2
- package/dist/skills/bettercms/references/editor-chrome.md +1 -1
- package/dist/skills/bettercms/references/finish-site.md +26 -4
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2989,6 +2989,8 @@ 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: "a slot row's look: {title?, subtitle?, media?} TOP-LEVEL prop keys, fallback?, icon?; null clears. \xA73 of bettercms://playbook/schema",
|
|
2993
|
+
componentIcon: "the block icon, a dock icon name; null clears. \xA73 of bettercms://playbook/schema",
|
|
2992
2994
|
describes: "for editors: what it is and holds; omitted: written for you",
|
|
2993
2995
|
runtimeBundle: "instead of pageId: the server-app bundle cleanup",
|
|
2994
2996
|
pageRef: "page id or path ('/de/about'); a bare slug only when one page has it",
|
|
@@ -3034,7 +3036,7 @@ var DETACHED_COPY_NOTE = "{overrides?}: a rich-text embed's separate-copy `field
|
|
|
3034
3036
|
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
3037
|
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
3038
|
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.";
|
|
3039
|
+
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
3040
|
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
3041
|
|
|
3040
3042
|
// src/entry-query.ts
|
|
@@ -3268,10 +3270,10 @@ var propUiObject = uiObject.extend({
|
|
|
3268
3270
|
}).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
3271
|
tab: z.strictObject({
|
|
3270
3272
|
id: z.string().min(1).max(40),
|
|
3271
|
-
label: z.string().min(1).max(
|
|
3273
|
+
label: z.string().min(1).max(40).optional(),
|
|
3272
3274
|
icon: uiObject.shape.icon,
|
|
3273
3275
|
order: z.number().int().min(0).max(99).optional()
|
|
3274
|
-
}).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 unique; a group's members share one tab.")
|
|
3276
|
+
}).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.")
|
|
3275
3277
|
});
|
|
3276
3278
|
var choiceOption = z.union([z.string(), z.object({ label: z.string(), value: z.string(), icon: z.string().optional(), description: z.string().optional() })]);
|
|
3277
3279
|
var fieldShape = {
|
|
@@ -3664,7 +3666,7 @@ var withProjectId = (def) => ({
|
|
|
3664
3666
|
return currentProject.run(arg("projectId"), () => currentBranch.run(arg("branchId"), () => def.handler(args)));
|
|
3665
3667
|
}
|
|
3666
3668
|
});
|
|
3667
|
-
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.";
|
|
3669
|
+
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.";
|
|
3668
3670
|
function buildToolDefs(deps) {
|
|
3669
3671
|
async function withClient(fn) {
|
|
3670
3672
|
const token = await deps.auth.getAccessToken();
|
|
@@ -3722,7 +3724,7 @@ function buildToolDefs(deps) {
|
|
|
3722
3724
|
]).describe("block type; section/slider/tabs/columns nest child blocks"),
|
|
3723
3725
|
id: z.string().min(1).describe("stable unique block id"),
|
|
3724
3726
|
props: z.record(z.string(), z.unknown()).describe(
|
|
3725
|
-
"per-type props:
|
|
3727
|
+
"per-type props by block type: \xA75 of bettercms://playbook/schema (text/richtext {html}, NOT {text}; button {text, href, variant?, size? sm|md|lg, fullWidth?}; component {componentId, overrides?}). Containers nest blocks to any depth."
|
|
3726
3728
|
),
|
|
3727
3729
|
style: z.record(z.string(), z.unknown()).optional().describe(
|
|
3728
3730
|
"design tokens: theme, bg (none|surface|muted|accent|dark|custom), bgCustom hex, paddingTop/paddingBottom/paddingSides px, contentWidth (narrow|default|wide|full), align, corner, shadow, borderTop/borderBottom, border (all four sides, in the text colour), hideOn [mobile|tablet|desktop] (all three hides it on the page: kept in the draft, never published). A real marketing band is a `section` block carrying bg + padding + contentWidth."
|
|
@@ -3971,6 +3973,7 @@ function buildToolDefs(deps) {
|
|
|
3971
3973
|
// 'select' — required, choices
|
|
3972
3974
|
// 'group'/'table'/'object' sub-shape. Typed loosely here (the server validates the full
|
|
3973
3975
|
// recursive shape, 10 levels at most) so a deep tree needs no Zod mirror in the client.
|
|
3976
|
+
// A sub-field may be a 'slot' (its value at its key in the object or row), with the slot keys above.
|
|
3974
3977
|
fields: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
3975
3978
|
min: z.number().optional(),
|
|
3976
3979
|
// 'number'
|
|
@@ -3982,7 +3985,7 @@ function buildToolDefs(deps) {
|
|
|
3982
3985
|
// override that omits it still saves — so it is a marker the dashboard renders and
|
|
3983
3986
|
// get_site_composition counts, never a refusal. Parity: componentPropDefSchema on the server.
|
|
3984
3987
|
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."),
|
|
3985
|
-
helpText: z.string().max(200).optional().describe("hint shown under the control, muted (\u2264 200 chars)"),
|
|
3988
|
+
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"),
|
|
3986
3989
|
placeholder: z.string().max(120).optional().describe("ghost text inside a text-ish control (\u2264 120 chars)"),
|
|
3987
3990
|
// Stored markers on a prop: nothing refuses a prop value for them. Declared so an echo keeps them.
|
|
3988
3991
|
readOnly: z.union([z.literal(true), z.literal("developer")]).optional(),
|
|
@@ -4071,6 +4074,13 @@ function buildToolDefs(deps) {
|
|
|
4071
4074
|
"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."
|
|
4072
4075
|
);
|
|
4073
4076
|
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");
|
|
4077
|
+
const componentPreviewArg = z.strictObject({
|
|
4078
|
+
title: z.string().min(1).max(64).optional(),
|
|
4079
|
+
subtitle: z.string().min(1).max(64).optional(),
|
|
4080
|
+
media: z.string().min(1).max(64).optional(),
|
|
4081
|
+
fallback: z.string().min(1).max(120).optional(),
|
|
4082
|
+
icon: dockIconArg.optional()
|
|
4083
|
+
}).nullable().describe(PARAM_TEXT.componentPreview);
|
|
4074
4084
|
const createComponentInput = z.object({
|
|
4075
4085
|
name: z.string().min(1),
|
|
4076
4086
|
slug: slug.describe("url-safe unique slug (lowercase letters/numbers/hyphens)"),
|
|
@@ -4082,6 +4092,8 @@ function buildToolDefs(deps) {
|
|
|
4082
4092
|
projectId: z.string().nullable().optional().describe("owning project id; null creates a workspace-wide global component"),
|
|
4083
4093
|
allowedOn: allowedOn.optional(),
|
|
4084
4094
|
description: z.string().optional().describe(PARAM_TEXT.describes),
|
|
4095
|
+
preview: componentPreviewArg.optional(),
|
|
4096
|
+
icon: dockIconArg.nullable().optional().describe(PARAM_TEXT.componentIcon),
|
|
4085
4097
|
blockJson: z.array(blockObject).default([]).describe("the component's block tree"),
|
|
4086
4098
|
props: z.array(componentPropObject).default([]).describe(
|
|
4087
4099
|
"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
|
|
@@ -4098,6 +4110,8 @@ function buildToolDefs(deps) {
|
|
|
4098
4110
|
variantOf: z.string().min(1).max(255).optional().describe("see create_component"),
|
|
4099
4111
|
allowedOn: allowedOn.optional().describe("REPLACES the placement allowlist"),
|
|
4100
4112
|
description: z.string().optional(),
|
|
4113
|
+
preview: componentPreviewArg.optional(),
|
|
4114
|
+
icon: dockIconArg.nullable().optional().describe(PARAM_TEXT.componentIcon),
|
|
4101
4115
|
blockJson: z.array(blockObject).optional().describe("REPLACES the block tree"),
|
|
4102
4116
|
props: z.array(componentPropObject).optional().describe(
|
|
4103
4117
|
"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."
|
|
@@ -4106,7 +4120,7 @@ function buildToolDefs(deps) {
|
|
|
4106
4120
|
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"),
|
|
4107
4121
|
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"),
|
|
4108
4122
|
propLabel: z.record(z.string(), z.string().min(1).max(255)).optional().describe("prop key \u2192 label"),
|
|
4109
|
-
propHelpText: z.record(z.string(), z.string().max(200).nullable()).optional().describe("prop key \u2192 help text
|
|
4123
|
+
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)"),
|
|
4110
4124
|
override: z.boolean().optional().describe(OVERRIDE_NOTE),
|
|
4111
4125
|
ifMatch: z.number().int().min(0).optional().describe(COMPONENT_IF_MATCH_NOTE),
|
|
4112
4126
|
confirmed: z.boolean().optional().describe(PARAM_TEXT.confirmedInput)
|
|
@@ -6529,8 +6543,14 @@ Then declare what an editor may change, via \`props\`:
|
|
|
6529
6543
|
]
|
|
6530
6544
|
|
|
6531
6545
|
Each prop also takes \`required\`, \`helpText\` (\u2264 200 chars) and \`placeholder\` (\u2264 120), which the
|
|
6532
|
-
dashboard's prop editor renders.
|
|
6546
|
+
dashboard's prop editor renders. \`helpText\` is three-state: a string is yours, \`null\` is no help on
|
|
6547
|
+
purpose (send it for a prop whose /kit schema has no description; nothing refills it), and omitted
|
|
6548
|
+
lets the platform write one. They are HINTS: nothing on the write path refuses a placement
|
|
6533
6549
|
that leaves a required prop empty, so \`get_site_composition\` is what reports one.
|
|
6550
|
+
A /kit schema's document-level \`preview\` \u2192 the component's \`preview\`: how ONE instance reads as a ROW in a parent's slot list. {title?, subtitle?, media?} each name a TOP-LEVEL prop key (422 \`preview_key_unknown\` otherwise), \`fallback?\` is a literal row label (\u2264 120) and \`icon?\` a dock icon name; null clears it.
|
|
6551
|
+
A /kit schema's document-level \`icon\` \u2192 the component's \`icon\` (a dock icon name from bettercms://icons/dock, null clears): the block icon on the root crumb, slot rows and slot child frames.
|
|
6552
|
+
|
|
6553
|
+
**Prop types, one rule each.** 'select' REQUIRES \`config.options\` (a default must be one of them). 'group'/'table'/'object' REQUIRE \`config.fields\` (\`[{key,label,type,options?,fields?}]\`); a group writes an object, a table an array of them (\`config.mode: 'data'\`: ONE V2 table value, no fields). 'number' takes optional \`config.min/max/step\`. 'slot' holds ONE nested component instance: \`target.path: 'props'\` on an EMPTY component block (\`props.componentId: ""\`), \`config.componentIds\` restricts fills, defaultValue \`{componentId, overrides?}\`. 'form' holds ONE Form document: \`target.path: 'props'\` on an EMPTY form block (\`props.formId: ""\`), defaultValue \`{formId}\`, never the hydrated \`{formId, form}\` (400); do NOT re-declare the form's FIELDS as props, the Form document owns them. '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).
|
|
6534
6554
|
|
|
6535
6555
|
\u{1F534} **On a components-first page, \`props\` is the ONLY editing surface.** Click-to-edit binds
|
|
6536
6556
|
\`heading\`/\`text\`/\`button\`/\`image\` blocks; it does **not** bind a \`component\` block, because
|
|
@@ -6582,9 +6602,16 @@ A \`slot\` field (V2 blocks inside a block) takes the same \`config.blockSlugs\`
|
|
|
6582
6602
|
but stores each child INLINE: \`[{ _key, _type: '<block slug>', _variant?, _settings?, ...values }]\`,
|
|
6583
6603
|
its values judged by its block's fields. A component's \`slot\` prop with \`config.multiple\`
|
|
6584
6604
|
takes the same child shape with \`_type\` the COMPONENT id (no \`_variant\`: a variant is its own
|
|
6585
|
-
component). A
|
|
6586
|
-
\`config.
|
|
6587
|
-
\`
|
|
6605
|
+
component). A \`slot\` may also be a SUB-FIELD: inside a \`group\` / \`object\` prop's
|
|
6606
|
+
\`config.fields\` or a \`table\` row (an item type's \`fields\` too), with the slot prop's
|
|
6607
|
+
config keys (\`componentIds\`, \`multiple\`, \`minItems\`/\`maxItems\`, \`slotSettings\`). Its value
|
|
6608
|
+
sits at its key in the object or row, \`{ componentId, overrides? }\` or a list with
|
|
6609
|
+
\`config.multiple\`, judged like a slot prop's: the same allowlist, depth cap and cycle refusal.
|
|
6610
|
+
A list of several item shapes on a component is a \`table\` prop with
|
|
6611
|
+
\`config.mode: 'types'\` and \`config.itemTypes: [{ name, label?, fields, preview?, icon? }]\`, rows
|
|
6612
|
+
\`[{ _key?, _type: '<name>', ...values }]\`. An item type's \`preview\` names its OWN fields (dotted ok, \`<table>.caption\`).
|
|
6613
|
+
|
|
6614
|
+
**Block props by type** (a block's \`props\` in any \`blockJson\`): heading {text, level 1-6}; text/richtext {html} (NOT {text}), both also take level 1-6, rendering the html AS that <hN> with inline marks kept (one line of inline copy only); image {src (asset url), alt, caption?}; button {text, href, variant (hosted pages; else a Button component), size sm|md|lg, fullWidth, arrow, icon (a ui.icon name), newTab, rel nofollow|sponsored|ugc}; spacer {height px}; video {url, poster?}; form {formId}; component {componentId, overrides?}; navbar {links:[{label,href}], logo?, cta?}; footer {columns:[{heading?,links}], copyright?}; section {children: block[], background?, paddingY?, maxWidth?}; columns {columns: block[][], gap} (a column may hold ANY block); slider {slides:[{id,children:block[]}]}; tabs {tabs:[{id,label,children:block[]}]}; collection {cardComponentId?, source? {modelId}, limit?, filters?, sort?, pinnedIds?, excludeCurrent?}: a collection's published entries as cards. Containers nest blocks to any depth.
|
|
6588
6615
|
|
|
6589
6616
|
A component inside a \`richtext\` value is \`{ _type: 'bcmsBlock', _key, schemaKey: 'component',
|
|
6590
6617
|
schemaLabel?, fields: { componentId, overrides?, variant? } }\` and FOLLOWS the component: publishing
|
|
@@ -7038,8 +7065,10 @@ hints the dashboard renders \u2014 they are NOT enforced at write time, so an ov
|
|
|
7038
7065
|
required prop still saves; \`get_site_composition\` is what reports those. Use \`select\` with
|
|
7039
7066
|
\`config.options\` for layout / theme / alignment / spacing variants that change only styling,
|
|
7040
7067
|
and a SEPARATE variant component when the markup differs. Repeatable list \u2192 \`table\`; fixed
|
|
7041
|
-
cluster \u2192 \`group\`. Nesting
|
|
7042
|
-
\`table
|
|
7068
|
+
cluster \u2192 \`group\`. Nesting goes up to 10 levels counting the prop itself. A \`table\` row may hold a
|
|
7069
|
+
\`table\` (rows), a data table (\`config.mode: "data"\`) or a polymorphic list
|
|
7070
|
+
(\`config.mode: "types"\`), so a size guide's rows can each carry a chart and a multi-step
|
|
7071
|
+
form's steps can each carry their fields. Deeper is refused (\`field_depth_exceeded\`).
|
|
7043
7072
|
|
|
7044
7073
|
**5. Design, responsiveness and accessibility.** \`componentize_sections\` and \`npx
|
|
7045
7074
|
@bettercms-ai/convert --componentize\` move the section's markup VERBATIM \u2014 classes, responsive
|
|
@@ -7234,7 +7263,7 @@ conversion is yours to write.
|
|
|
7234
7263
|
markup inside a \`text\` field.
|
|
7235
7264
|
**Prose MIGRATED from another CMS needs one extra check.** Every block's \`_type\` must be one
|
|
7236
7265
|
this platform stores \u2014 \`block\`, \`table\` (\`{_type:"table", rows:[{_key, cells:[{_key,
|
|
7237
|
-
content:[...blocks], header?, group?}], group?}], caption?, notes?, rowGroups?, columnGroups?}\`,
|
|
7266
|
+
content:[...blocks], header?, group?, highlight?, sortable?}], group?, footer?, highlight?}], caption?, notes?, rowGroups?, columnGroups?, filterable?, jsonLd?: {type: "ItemList"|"Dataset"|"Offers", axis?, priceRow?}, design?: {responsive?: "stack"|"cards", sticky?}}\`,
|
|
7238
7267
|
groups \`[{_key, label}]\`; Sanity's row of plain strings, \`rows:[{cells:["a","b"]}]\`,
|
|
7239
7268
|
is admitted too), \`bcmsCode\`, \`bcmsDivider\`, \`bcmsMention\`, \`pullQuote\`
|
|
7240
7269
|
(\`{quote, name, role?, link?}\`), \`keyTakeaways\` (\`{title?, points:[...strings]}\`,
|
|
@@ -7406,7 +7435,7 @@ back each prop's stored \`ui\` or you delete it along with the prop. Props have
|
|
|
7406
7435
|
prop joins a group with \`ui.group\`: the FIRST prop of a group defines \`{ id, label, kind?, icon?,
|
|
7407
7436
|
collapsed? }\` (kind \`group\` or \`row\`; a tab is \`ui.tab\`, never a group kind), and every later prop
|
|
7408
7437
|
sends \`{ id }\` only. A prop joins a TAB with \`ui.tab\` the same way: the FIRST top-level prop in a tab
|
|
7409
|
-
defines \`{ id, label, icon?, order? }\`, later props send \`{ id }\`; at most 8 tabs, labels \
|
|
7438
|
+
defines \`{ id, label, icon?, order? }\`, later props send \`{ id }\`; at most 8 tabs, labels \u226440 characters
|
|
7410
7439
|
and unique; top-level props only (never a sub-field); all members of one \`ui.group\` share one tab. A V2
|
|
7411
7440
|
kit entry's schema \`groups\` and each field's \`group\` map 1:1 to \`ui.tab\`, so its \`design\` group is a
|
|
7412
7441
|
tab named Design holding the theme, sticky and similar props. A FIELD never takes \`ui.group\` or
|
|
@@ -7484,7 +7513,7 @@ Importance orders a screen: required fields first, then array order.
|
|
|
7484
7513
|
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.
|
|
7485
7514
|
|
|
7486
7515
|
**Tabs.** A \`kind: 'tab'\` fieldset is a tab:
|
|
7487
|
-
- at most **3** tabs, names at most
|
|
7516
|
+
- at most **3** tabs, names at most 40 characters and unique;
|
|
7488
7517
|
- top-level fields only: tabs never nest (a component's props take tabs through \`ui.tab\`, its own rules);
|
|
7489
7518
|
- untabbed fields stay PINNED above the strip;
|
|
7490
7519
|
- a group or row fieldset joins a tab with \`tab: '<tab id>'\`; groups in a non-first tab default open;
|
|
@@ -7766,7 +7795,7 @@ Read \`bettercms://playbook/schema\` \xA712 first \u2014 this is the short form
|
|
|
7766
7795
|
4. **Fields** = \`props\`: text, richtext, image, url, boolean, number, select, group, table, slot.
|
|
7767
7796
|
Always a \`defaultValue\`; \`required\` / \`helpText\` / \`placeholder\` are editor hints, not write
|
|
7768
7797
|
-time enforcement. \`select\` for styling-only variants, a separate component when the markup
|
|
7769
|
-
differs. Nesting
|
|
7798
|
+
differs. Nesting goes up to 10 levels; a \`table\` row may hold a data table or a polymorphic list.
|
|
7770
7799
|
5. **Quality.** The componentize lane and \`npx @bettercms-ai/convert --componentize\` move markup
|
|
7771
7800
|
VERBATIM \u2014 classes, responsive utilities, aria attributes, alt text all survive. Never
|
|
7772
7801
|
restyle. Dry-run first.
|