diff --git a/scripts/check-skill-examples.mjs b/scripts/check-skill-examples.mjs index be765652f2..0480b44ed8 100644 --- a/scripts/check-skill-examples.mjs +++ b/scripts/check-skill-examples.mjs @@ -460,7 +460,13 @@ export const MARKED_FLOOR = new Map([ // gate's reach, and the guide already carried five UNMARKED `vitest` fences // beside them. ⛔ Not a precedent for unmarking a fence that reds. ['ts', 13], - ['json', 39], + // 39 -> 70 (objectui#7474): the 8 multi-document listings under + // `skills/objectui/` were split into one fence per document and the two + // `...` elisions were replaced by real values, so every one of the 70 + // json/jsonc fences in the corpus now parses under its own tag and carries + // the marker. Raising the number is the ADD direction this list documents: + // the population measured 39 marked / 46 parsing before, 70 / 70 after. + ['json', 70], ]); /** diff --git a/skills/objectui/guides/schema-expressions.md b/skills/objectui/guides/schema-expressions.md index 3773413fc1..ef6b3a624c 100644 --- a/skills/objectui/guides/schema-expressions.md +++ b/skills/objectui/guides/schema-expressions.md @@ -29,16 +29,26 @@ full boundary tables -- the evaluated fields, the raw ones, `visible` over -- are in [`rules/protocol.md`](../rules/protocol.md), which is the anchor for this rule. The four cases that decide most schemas: + ```jsonc // Evaluated, then dropped -- renders an empty card { "type": "card", "props": { "title": "${data.customer.name}" } } +``` + +```jsonc // `card` declares `title`, so on the node it is evaluated AND read { "type": "card", "title": "${data.customer.name}" } +``` + +```jsonc // `text` does not declare `value` -- renders the literal "${data.customer.name}" { "type": "text", "value": "${data.customer.name}" } +``` + +```jsonc // `content` is evaluated on every component type { "type": "text", "content": "${data.customer.name}" } ``` @@ -167,10 +177,19 @@ name, so calls are case-insensitive. Each condition field has two forms — a shorthand and an `On` suffix: + +```jsonc +{ "hidden": true } // static boolean +``` + + ```jsonc -{ "hidden": true } // static boolean -{ "hidden": "${data.role !== 'admin'}" } // template expression -{ "hiddenOn": "data.role !== 'admin'" } // raw expression (no ${} needed) +{ "hidden": "${data.role !== 'admin'}" } // template expression +``` + + +```jsonc +{ "hiddenOn": "data.role !== 'admin'" } // raw expression (no ${} needed) ``` The `On` variants accept raw expressions without `${}` wrapping — the entire string is the expression. @@ -203,8 +222,13 @@ The `On` variants accept raw expressions without `${}` wrapping — the entire s ### Disabled patterns + ```json { "disabled": "${form.isSubmitting}" } +``` + + +```json { "disabledOn": "!data.canPerformAction || data.isLocked" } ``` @@ -325,6 +349,7 @@ what reaches the screen. `list` is the component authors reach for first, and it is **data-as-nodes**: the array it renders *is* the node list. It never reads `children`. + ```jsonc // ❌ Renders two EMPTY
  • . `children` is not a template — `list` never reads it, // and `${item.name}` would render literally even if it did. @@ -352,6 +377,7 @@ descriptor: section exists to close: binding `list` to ordinary records produces one empty `
  • ` per record — the right number of bullets, no text in any of them. + ```jsonc // ✅ Authored items — `title` and `ordered` are read off the node { @@ -362,6 +388,7 @@ section exists to close: binding `list` to ordinary records produces one empty } ``` + ```jsonc // ✅ Bound data, already node-shaped: dataSource = { rows: [{ "content": "Ada" }, { "content": "Linus" }] } { "type": "list", "bind": "rows" } @@ -371,6 +398,7 @@ Only `body` entries go back through `SchemaRenderer`, so they are the one place inside a list where expressions are evaluated at all — against the host scope, never against a current element: + ```jsonc // ✅ `${data.*}` works inside `body`; there is still no `${item.*}` { @@ -388,6 +416,7 @@ never against a current element: accessors (`accessorKey`). Cell values are plain property lookups — never expressions — and `table` does not read `bind`. + ```jsonc // ✅ `table`: inline rows + column accessors, no per-row scope { @@ -444,13 +473,20 @@ Expressions are compiled once per unique `(expression, variableNames)` pair and **Cause:** The field isn't expression-evaluated. Use `content`. + ```jsonc // ❌ Won't evaluate — `value` is read but never templated { "type": "text", "value": "${data.total}" } +``` + +```jsonc // ❌ Worse — evaluated inside the envelope, then discarded: renders nothing { "type": "text", "props": { "value": "${data.total}" } } +``` + +```jsonc // ✅ Evaluated and read { "type": "text", "content": "Total: ${data.total}" } ``` @@ -460,10 +496,14 @@ Expressions are compiled once per unique `(expression, variableNames)` pair and **Cause 1:** `visible` is also set and takes priority. **Cause 2:** Expression returns a non-boolean truthy value — use explicit comparison. + ```jsonc // ❌ Truthy but not boolean { "hidden": "${data.count}" } +``` + +```jsonc // ✅ Explicit boolean { "hidden": "${data.count > 0}" } ``` @@ -476,23 +516,34 @@ is **not** blocked — measured through the schema path, it returns a real `Date`. Prefer a formula function anyway: a `Date` object stringifies into the DOM as a locale-dependent blob. + ```jsonc // ✅ Works, but renders "Thu Jan 01 1970 …" { "type": "text", "content": "${new Date(data.timestamp)}" } +``` + +```jsonc // ✅ Preferred — formatted, and stable across locales { "type": "text", "content": "${DATEFORMAT(data.timestamp, 'YYYY-MM-DD')}" } +``` + +```jsonc // ❌ Rejected — any constructor other than Date / RegExp { "type": "text", "content": "${new Function('return 1')()}" } ``` ### Object literal in expression + ```jsonc // ❌ Object literals not supported { "type": "text", "style": "${{ color: 'red' }}" } +``` + +```jsonc // ✅ Use className { "type": "text", "className": "text-red-500" } ``` diff --git a/skills/objectui/rules/protocol.md b/skills/objectui/rules/protocol.md index 5ea19e06fe..51463cd724 100644 --- a/skills/objectui/rules/protocol.md +++ b/skills/objectui/rules/protocol.md @@ -154,19 +154,21 @@ key under `props` never reaches a `ui:*` / `page:*` renderer at all. **❌ FORBIDDEN:** Adding custom properties not defined in `@objectstack/spec`. **Example violation:** + ```jsonc { "type": "data-table", - "fields": [...], // ❌ spec uses "columns" + "fields": [{ "header": "Name", "accessorKey": "name" }], // ❌ spec uses "columns" "customProp": "value" // ❌ not in spec } ``` **✅ CORRECT:** + ```jsonc { "type": "data-table", - "columns": [...] // ✅ declared by DataTableSchema, read off the node + "columns": [{ "header": "Name", "accessorKey": "name" }] // ✅ declared by DataTableSchema, read off the node } ``` @@ -174,9 +176,18 @@ key under `props` never reaches a `ui:*` / `page:*` renderer at all. When the entire string is a single `${expression}`, the result preserves its type: + +```jsonc +"${data.count}" // → returns number 42, not string "42" +``` + + +```jsonc +"${data.isActive}" // → returns boolean true, not string "true" +``` + + ```jsonc -"${data.count}" // → returns number 42, not string "42" -"${data.isActive}" // → returns boolean true, not string "true" "Count: ${data.count}" // → returns string "Count: 42" (mixed template) ``` @@ -184,6 +195,7 @@ When the entire string is a single `${expression}`, the result preserves its typ The `bind` field is NOT expression-evaluated. It's a path string resolved by `useDataScope()`, and only a component that calls that hook reads it: + ```jsonc { "type": "list", diff --git a/skills/objectui/rules/styling.md b/skills/objectui/rules/styling.md index 06654f354a..60b888c8a9 100644 --- a/skills/objectui/rules/styling.md +++ b/skills/objectui/rules/styling.md @@ -167,6 +167,7 @@ const buttonVariants = cva( **Every component must accept `className` on its schema node** to allow JSON-level style overrides. Like every other key, it is read off the node itself — not out of a `props` envelope, which the renderers never read: + ```jsonc { "type": "card",