@golemui/gui-mcp 0.15.1 → 0.16.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/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 0.15.1 (2026-05-27)
2
+
3
+ This was a version bump only for gui-mcp to align it with other projects, there were no code changes.
4
+
1
5
  ## 0.15.0 (2026-05-26)
2
6
 
3
7
  ### 🩹 Fixes
package/README.md CHANGED
@@ -91,14 +91,53 @@ version-locked to a specific GolemUI release. A CI check in this monorepo
91
91
  against the exact same schema definitions as `@golemui/gui-*@X.Y.Z`.
92
92
 
93
93
  No LLM calls happen inside this server — every tool is deterministic. The MCP is the
94
- *grounding layer* the host IDE's model calls into.
94
+ _grounding layer_ the host IDE's model calls into.
95
95
 
96
96
  ## Development
97
97
 
98
+ ### Interactive testing with MCP Inspector
99
+
100
+ Start the MCP server and open a local web UI at http://localhost:5173
101
+
102
+ ```bash
103
+ npm run start:mcp
104
+ ```
105
+
106
+ To use the server from within Claude Code conversations, register with local Claude Code:
107
+
108
+ #### With the Calude code CLI
109
+
110
+ ```bash
111
+ claude mcp add golemui-local -- node /Users/{USER}/{...}/golem/golemui/dist/libs/gui/mcp/index.js
112
+ ```
113
+
114
+ Then restart or reload the session. The tools appear in Claude's tool list.
115
+
116
+ Remove with claude mcp remove golemui-local when done.
117
+
118
+ #### With the Calude code extension
119
+
120
+ Create or edit the project-level MCP config at .mcp.json in the workspace root:
121
+
122
+ ```json
123
+ {
124
+ "mcpServers": {
125
+ "golemui-local": {
126
+ "command": "node",
127
+ "args": ["/Users/{USER}/{...}/golem/golemui/dist/libs/gui/mcp/index.js"]
128
+ }
129
+ }
130
+ }
131
+ ```
132
+
133
+ Then reload the VS Code window (Cmd+Shift+P -> "Developer: Reload Window"). The 4 tools will appear in Claude's tool list for this workspace.
134
+
135
+ Remove the entry from .mcp.json when done.
136
+
137
+ ### Other development commands
138
+
98
139
  ```bash
99
- # from the repo root
100
- npx nx run gui-mcp:build # build to dist/libs/gui/mcp
101
- npx nx run gui-mcp:vite:test # run the test suite
140
+ npx nx run gui-mcp:vite:test # run the test suite
102
141
  npm run sync:mcp-schemas # refresh bundled schemas from libs/gui/shared
103
142
  npm run check:mcp-schemas # CI mode — exits non-zero if out of sync
104
143
  ```
package/index.js CHANGED
@@ -133,7 +133,7 @@ function jsonSchemaToGui(schema, opts = {}) {
133
133
  }
134
134
  ] : fields;
135
135
  return {
136
- formDefinition: { form: wrapped },
136
+ formDefinition: { $schema: "https://golemui.com/schemas/form.schema.json", form: wrapped },
137
137
  unmapped
138
138
  };
139
139
  }
@@ -283,7 +283,13 @@ function buildArrayField(path, _name, label, schema, required2, unmapped) {
283
283
  const required22 = new Set(items.required ?? []);
284
284
  const childParent = `${path}.items`;
285
285
  for (const [childName, childSchema] of Object.entries(items.properties ?? {})) {
286
- const w = mapProperty(childName, unwrap(childSchema), required22.has(childName), childParent, unmapped);
286
+ const w = mapProperty(
287
+ childName,
288
+ unwrap(childSchema),
289
+ required22.has(childName),
290
+ childParent,
291
+ unmapped
292
+ );
287
293
  if (w) templateChildren.push(w);
288
294
  }
289
295
  if (!templateChildren.length) {
@@ -363,7 +369,7 @@ function cleanFields(o) {
363
369
  const $schema$u = "https://json-schema.org/draft/2020-12/schema";
364
370
  const $id$u = "https://golemui.com/schemas/common.schema.json";
365
371
  const title$u = "Golem Common Definitions";
366
- const $defs$t = { "reactiveExpression": { "type": "string", "description": "A reactive expression string. Use `$form` to access form data e.g. `$form.termsAccepted === true`" }, "dotPath": { "type": "string", "description": "A dot-separated path to a property in the form data" }, "dateRange": { "type": "object", "description": "A single date range used by calendar/date-picker widgets to mark days as disabled.", "properties": { "start": { "type": "string" }, "end": { "type": "string" } }, "required": ["start"], "additionalProperties": false }, "uid": { "type": "string", "description": "Unique identifier for the widget" }, "localizable": { "oneOf": [{ "type": "string" }, { "type": "object", "properties": { "key": { "type": "string" }, "default": { "type": "string" }, "params": { "type": "object" } }, "required": ["key"], "additionalProperties": false }] }, "include": { "oneOf": [{ "type": "object", "properties": { "in": { "type": "array", "items": { "type": "string" } } }, "required": ["in"], "additionalProperties": false }, { "type": "object", "properties": { "when": { "$ref": "#/$defs/reactiveExpression" } }, "required": ["when"], "additionalProperties": false }] }, "exclude": { "oneOf": [{ "type": "object", "properties": { "from": { "type": "array", "items": { "type": "string" } } }, "required": ["from"], "additionalProperties": false }, { "type": "object", "properties": { "when": { "$ref": "#/$defs/reactiveExpression" } }, "required": ["when"], "additionalProperties": false }] }, "boolOrWhen": { "oneOf": [{ "type": "boolean" }, { "type": "object", "properties": { "when": { "$ref": "#/$defs/reactiveExpression" } }, "required": ["when"], "additionalProperties": false }] }, "on": { "type": "object", "properties": { "load": { "type": "string" }, "click": { "type": "string" }, "change": { "type": "string" }, "filter": { "type": "string" }, "blur": { "type": "string" } }, "patternProperties": { "^load\\.[^.]+$": { "type": "string" }, "^click\\.[^.]+$": { "type": "string" }, "^change\\.[^.]+$": { "type": "string" }, "^filter\\.[^.]+$": { "type": "string" }, "^blur\\.[^.]+$": { "type": "string" } }, "additionalProperties": false }, "baseWidget": { "type": "object", "properties": { "$schema": { "type": "string" }, "uid": { "$ref": "#/$defs/uid" }, "type": { "type": "string" }, "size": { "type": "number" }, "include": { "$ref": "#/$defs/include" }, "exclude": { "$ref": "#/$defs/exclude" } }, "required": ["type"] } };
372
+ const $defs$t = { "reactiveExpression": { "type": "string", "description": "A reactive expression string. Use `$form` to access form data e.g. `$form.termsAccepted === true`" }, "dotPath": { "type": "string", "description": "A dot-separated path to a property in the form data" }, "dateRange": { "type": "object", "description": "A single date range used by calendar/date-picker widgets to mark days as disabled.", "properties": { "start": { "type": "string", "description": "ISO-8601 date string for the start of the disabled range (inclusive)" }, "end": { "type": "string", "description": "ISO-8601 date string for the end of the disabled range (inclusive). Omit to disable a single day." } }, "required": ["start"], "additionalProperties": false }, "uid": { "type": "string", "description": "Unique identifier for the widget" }, "localizable": { "description": "A value that can be a plain string or an i18n key object. Use a plain string for static text, or `{key, default, params}` to reference a translation key.", "oneOf": [{ "type": "string" }, { "type": "object", "properties": { "key": { "type": "string", "description": "The i18n translation key" }, "default": { "type": "string", "description": "Fallback text shown when the translation key is not found" }, "params": { "type": "object", "description": "Interpolation parameters passed to the translation function" } }, "required": ["key"], "additionalProperties": false }] }, "include": { "description": "Show this widget only in certain form states (`{in: [stateNames]}`) or when a reactive expression is true (`{when: expr}`). If omitted the widget is always shown.", "oneOf": [{ "type": "object", "properties": { "in": { "type": "array", "items": { "type": "string" }, "description": "List of state names (defined in the form `states` map) in which this widget should be visible" } }, "required": ["in"], "additionalProperties": false }, { "type": "object", "properties": { "when": { "$ref": "#/$defs/reactiveExpression" } }, "required": ["when"], "additionalProperties": false }] }, "exclude": { "description": "Hide this widget in certain form states (`{from: [stateNames]}`) or when a reactive expression is true (`{when: expr}`). If omitted the widget is never hidden.", "oneOf": [{ "type": "object", "properties": { "from": { "type": "array", "items": { "type": "string" }, "description": "List of state names (defined in the form `states` map) in which this widget should be hidden" } }, "required": ["from"], "additionalProperties": false }, { "type": "object", "properties": { "when": { "$ref": "#/$defs/reactiveExpression" } }, "required": ["when"], "additionalProperties": false }] }, "boolOrWhen": { "description": "A static boolean or a conditional `{when: reactiveExpression}` object evaluated at runtime. Use `{when: '$form.someField === true'}` for dynamic behavior.", "oneOf": [{ "type": "boolean" }, { "type": "object", "properties": { "when": { "$ref": "#/$defs/reactiveExpression" } }, "required": ["when"], "additionalProperties": false }] }, "on": { "type": "object", "description": 'Event handlers for this widget. Values are action names dispatched to the form\'s event bus. Each key also accepts a state-suffixed variant (`"click.<stateName>"`) that overrides the base handler when the named state is active.', "properties": { "load": { "type": "string", "description": "Action dispatched when the widget is first mounted" }, "click": { "type": "string", "description": "Action dispatched when the user clicks the widget" }, "change": { "type": "string", "description": "Action dispatched when the widget value changes" }, "filter": { "type": "string", "description": "Action dispatched on search/filter input, typically used with async data sources" }, "blur": { "type": "string", "description": "Action dispatched when the widget loses focus" } }, "patternProperties": { "^load\\.[^.]+$": { "type": "string", "description": 'State-suffixed load handler: used instead of `load` when the named state is active (e.g. `"load.myState"`)' }, "^click\\.[^.]+$": { "type": "string", "description": 'State-suffixed click handler: used instead of `click` when the named state is active (e.g. `"click.myState"`)' }, "^change\\.[^.]+$": { "type": "string", "description": 'State-suffixed change handler: used instead of `change` when the named state is active (e.g. `"change.myState"`)' }, "^filter\\.[^.]+$": { "type": "string", "description": 'State-suffixed filter handler: used instead of `filter` when the named state is active (e.g. `"filter.myState"`)' }, "^blur\\.[^.]+$": { "type": "string", "description": 'State-suffixed blur handler: used instead of `blur` when the named state is active (e.g. `"blur.myState"`)' } }, "additionalProperties": false }, "baseWidget": { "type": "object", "description": 'Base properties shared by all widgets. Root-level props that support state-suffixing (e.g. `"label.myState"`) are: `label`, `disabled`, `readonly`, `validator`, `size`. Any key inside `props` can also be suffixed. State names must be declared in the form `states` map.', "properties": { "$schema": { "type": "string" }, "uid": { "$ref": "#/$defs/uid" }, "type": { "type": "string" }, "size": { "type": "number", "description": "Column span in the form grid (1–12). Defaults to full width when omitted.", "minimum": 1, "maximum": 12 }, "include": { "$ref": "#/$defs/include" }, "exclude": { "$ref": "#/$defs/exclude" } }, "required": ["type"] } };
367
373
  const commonSchema = {
368
374
  $schema: $schema$u,
369
375
  $id: $id$u,
@@ -374,10 +380,10 @@ const $schema$t = "https://json-schema.org/draft/2020-12/schema";
374
380
  const $id$t = "https://golemui.com/schemas/form.schema.json";
375
381
  const title$t = "Golem Form DSL";
376
382
  const type$r = "object";
377
- const properties$r = { "$schema": { "type": "string" }, "states": { "type": "object", "additionalProperties": { "$ref": "common.schema.json#/$defs/reactiveExpression" } }, "form": { "type": "array", "items": { "$ref": "#/$defs/formWidget" } }, "autocomplete": { "type": "string" } };
383
+ const properties$r = { "$schema": { "type": "string" }, "states": { "type": "object", "description": 'Named boolean conditions keyed by state name, each mapping to a reactive expression. States serve two purposes: (1) gating widget visibility via `include.in` / `exclude.from`; (2) overriding individual widget properties when a state is active by appending `".<stateName>"` to any property key — e.g. `"label.myState": "Active label"` or `"props.hint.myState": "Active hint"`. Root-level suffixable props: `label`, `disabled`, `readonly`, `validator`, `size`. Any key inside `props` can also be suffixed.', "additionalProperties": { "$ref": "common.schema.json#/$defs/reactiveExpression" } }, "form": { "type": "array", "description": "The ordered list of widgets that make up the form body", "items": { "$ref": "#/$defs/formWidget" } }, "autocomplete": { "type": "string", "description": "HTML `autocomplete` attribute applied to the `<form>` element (e.g. `on`, `off`)" } };
378
384
  const required$r = ["form"];
379
385
  const additionalProperties = false;
380
- const $defs$s = { "chunkRef": { "type": "object", "properties": { "$ref": { "type": "string" } }, "required": ["$ref"], "additionalProperties": false }, "formWidget": { "oneOf": [{ "$ref": "./components/accordion.schema.json" }, { "$ref": "./components/alert.schema.json" }, { "$ref": "./components/button.schema.json" }, { "$ref": "./components/calendar.schema.json" }, { "$ref": "./components/checkbox.schema.json" }, { "$ref": "./components/currency.schema.json" }, { "$ref": "./components/dateinput.schema.json" }, { "$ref": "./components/datepicker.schema.json" }, { "$ref": "./components/dropdown.schema.json" }, { "$ref": "./components/list.schema.json" }, { "$ref": "./components/markdown.schema.json" }, { "$ref": "./components/markdowntext.schema.json" }, { "$ref": "./components/number.schema.json" }, { "$ref": "./components/password.schema.json" }, { "$ref": "./components/radiogroup.schema.json" }, { "$ref": "./components/rangecalendar.schema.json" }, { "$ref": "./components/rangedateinput.schema.json" }, { "$ref": "./components/rangedatepicker.schema.json" }, { "$ref": "./components/repeater.schema.json" }, { "$ref": "./components/select.schema.json" }, { "$ref": "./components/flex.schema.json" }, { "$ref": "./components/grid.schema.json" }, { "$ref": "./components/tabs.schema.json" }, { "$ref": "./components/tags.schema.json" }, { "$ref": "./components/textarea.schema.json" }, { "$ref": "./components/textinput.schema.json" }, { "$ref": "./components/toggle.schema.json" }, { "$ref": "#/$defs/chunkRef" }] } };
386
+ const $defs$s = { "chunkRef": { "type": "object", "description": "A reference to an external chunk file whose widget array will be inlined at this position in the form", "properties": { "$ref": { "type": "string", "description": "Relative path to a JSON chunk file" } }, "required": ["$ref"], "additionalProperties": false }, "formWidget": { "oneOf": [{ "$ref": "./components/accordion.schema.json" }, { "$ref": "./components/alert.schema.json" }, { "$ref": "./components/button.schema.json" }, { "$ref": "./components/calendar.schema.json" }, { "$ref": "./components/checkbox.schema.json" }, { "$ref": "./components/currency.schema.json" }, { "$ref": "./components/dateinput.schema.json" }, { "$ref": "./components/datepicker.schema.json" }, { "$ref": "./components/dropdown.schema.json" }, { "$ref": "./components/list.schema.json" }, { "$ref": "./components/markdown.schema.json" }, { "$ref": "./components/markdowntext.schema.json" }, { "$ref": "./components/number.schema.json" }, { "$ref": "./components/password.schema.json" }, { "$ref": "./components/radiogroup.schema.json" }, { "$ref": "./components/rangecalendar.schema.json" }, { "$ref": "./components/rangedateinput.schema.json" }, { "$ref": "./components/rangedatepicker.schema.json" }, { "$ref": "./components/repeater.schema.json" }, { "$ref": "./components/select.schema.json" }, { "$ref": "./components/flex.schema.json" }, { "$ref": "./components/grid.schema.json" }, { "$ref": "./components/tabs.schema.json" }, { "$ref": "./components/tags.schema.json" }, { "$ref": "./components/textarea.schema.json" }, { "$ref": "./components/textinput.schema.json" }, { "$ref": "./components/toggle.schema.json" }, { "$ref": "#/$defs/chunkRef" }] } };
381
387
  const formSchema = {
382
388
  $schema: $schema$t,
383
389
  $id: $id$t,
@@ -401,7 +407,7 @@ const layoutWidgetSchema = {
401
407
  const $schema$r = "https://json-schema.org/draft/2020-12/schema";
402
408
  const $id$r = "https://golemui.com/schemas/validators.schema.json";
403
409
  const title$r = "GolemUI Validator Definitions";
404
- const $defs$r = { "localizable": { "$ref": "./common.schema.json#/$defs/localizable" }, "stringValidator": { "type": "object", "properties": { "type": { "const": "string" }, "required": { "type": "boolean" }, "minLength": { "type": "number" }, "maxLength": { "type": "number" }, "pattern": { "type": "string", "description": "A regular expression pattern the value must match" }, "format": { "type": "string", "enum": ["email", "hostname", "ipv4", "ipv6", "url", "uuid", "date", "time", "date-time", "duration"] }, "const": {}, "enum": { "type": "array" }, "messages": { "type": "object", "properties": { "invalid": { "$ref": "#/$defs/localizable" }, "required": { "$ref": "#/$defs/localizable" }, "minLength": { "$ref": "#/$defs/localizable" }, "maxLength": { "$ref": "#/$defs/localizable" }, "pattern": { "$ref": "#/$defs/localizable" }, "format": { "$ref": "#/$defs/localizable" }, "enum": { "$ref": "#/$defs/localizable" }, "const": { "$ref": "#/$defs/localizable" } }, "additionalProperties": false } }, "required": ["type"], "additionalProperties": false }, "numberValidator": { "type": "object", "properties": { "type": { "type": "string", "enum": ["number", "integer"] }, "required": { "type": "boolean" }, "minimum": { "type": "number" }, "maximum": { "type": "number" }, "exclusiveMinimum": { "type": "number" }, "exclusiveMaximum": { "type": "number" }, "multipleOf": { "type": "number" }, "const": {}, "enum": { "type": "array" }, "messages": { "type": "object", "properties": { "invalid": { "$ref": "#/$defs/localizable" }, "minimum": { "$ref": "#/$defs/localizable" }, "maximum": { "$ref": "#/$defs/localizable" }, "exclusiveMinimum": { "$ref": "#/$defs/localizable" }, "exclusiveMaximum": { "$ref": "#/$defs/localizable" }, "multipleOf": { "$ref": "#/$defs/localizable" }, "enum": { "$ref": "#/$defs/localizable" }, "const": { "$ref": "#/$defs/localizable" } }, "additionalProperties": false } }, "required": ["type"], "additionalProperties": false }, "booleanValidator": { "type": "object", "properties": { "type": { "const": "boolean" }, "required": { "type": "boolean" }, "const": {}, "messages": { "type": "object", "properties": { "invalid": { "$ref": "#/$defs/localizable" }, "const": { "$ref": "#/$defs/localizable" } }, "additionalProperties": false } }, "required": ["type"], "additionalProperties": false }, "arrayValidator": { "type": "object", "properties": { "type": { "const": "array" }, "required": { "type": "boolean" }, "minItems": { "type": "number" }, "maxItems": { "type": "number" }, "uniqueItems": { "type": "boolean" }, "messages": { "type": "object", "properties": { "invalid": { "$ref": "#/$defs/localizable" }, "required": { "$ref": "#/$defs/localizable" }, "minItems": { "$ref": "#/$defs/localizable" }, "maxItems": { "$ref": "#/$defs/localizable" } }, "additionalProperties": false } }, "required": ["type"], "additionalProperties": false }, "customValidator": { "type": "object", "properties": { "type": { "const": "custom" }, "required": { "type": "boolean" } }, "required": ["type"], "additionalProperties": true }, "validator": { "oneOf": [{ "$ref": "#/$defs/stringValidator" }, { "$ref": "#/$defs/numberValidator" }, { "$ref": "#/$defs/booleanValidator" }, { "$ref": "#/$defs/arrayValidator" }, { "$ref": "#/$defs/customValidator" }] } };
410
+ const $defs$r = { "localizable": { "$ref": "./common.schema.json#/$defs/localizable" }, "stringValidator": { "type": "object", "properties": { "type": { "const": "string" }, "required": { "type": "boolean", "description": "Makes the field mandatory — validation fails if the value is empty or absent" }, "minLength": { "type": "number", "description": "Minimum number of characters the string must contain" }, "maxLength": { "type": "number", "description": "Maximum number of characters the string may contain" }, "pattern": { "type": "string", "description": "A regular expression pattern the value must match" }, "format": { "type": "string", "description": "A named format the string must conform to (e.g. `email`, `url`, `date`, `uuid`)", "enum": ["email", "hostname", "ipv4", "ipv6", "url", "uuid", "date", "time", "date-time", "duration"] }, "const": { "description": "The string must equal exactly this value" }, "enum": { "type": "array", "description": "The string must be one of these allowed values" }, "messages": { "type": "object", "description": "Custom error messages that override the default text for each constraint violation", "properties": { "invalid": { "$ref": "#/$defs/localizable", "description": "Shown when the value fails general type validation" }, "required": { "$ref": "#/$defs/localizable", "description": "Shown when a required field is empty" }, "minLength": { "$ref": "#/$defs/localizable", "description": "Shown when the value is shorter than `minLength`" }, "maxLength": { "$ref": "#/$defs/localizable", "description": "Shown when the value is longer than `maxLength`" }, "pattern": { "$ref": "#/$defs/localizable", "description": "Shown when the value does not match the `pattern` regex" }, "format": { "$ref": "#/$defs/localizable", "description": "Shown when the value does not conform to the specified `format`" }, "enum": { "$ref": "#/$defs/localizable", "description": "Shown when the value is not in the `enum` list" }, "const": { "$ref": "#/$defs/localizable", "description": "Shown when the value does not equal `const`" } }, "additionalProperties": false } }, "required": ["type"], "additionalProperties": false }, "numberValidator": { "type": "object", "properties": { "type": { "type": "string", "enum": ["number", "integer"] }, "required": { "type": "boolean", "description": "Makes the field mandatory — validation fails if the value is absent" }, "minimum": { "type": "number", "description": "The value must be greater than or equal to this number (inclusive)" }, "maximum": { "type": "number", "description": "The value must be less than or equal to this number (inclusive)" }, "exclusiveMinimum": { "type": "number", "description": "The value must be strictly greater than this number (exclusive)" }, "exclusiveMaximum": { "type": "number", "description": "The value must be strictly less than this number (exclusive)" }, "multipleOf": { "type": "number", "description": "The value must be a multiple of this number" }, "const": { "description": "The value must equal exactly this number" }, "enum": { "type": "array", "description": "The value must be one of these allowed numbers" }, "messages": { "type": "object", "description": "Custom error messages that override the default text for each constraint violation", "properties": { "invalid": { "$ref": "#/$defs/localizable", "description": "Shown when the value fails general type validation" }, "minimum": { "$ref": "#/$defs/localizable", "description": "Shown when the value is below `minimum`" }, "maximum": { "$ref": "#/$defs/localizable", "description": "Shown when the value exceeds `maximum`" }, "exclusiveMinimum": { "$ref": "#/$defs/localizable", "description": "Shown when the value is not strictly above `exclusiveMinimum`" }, "exclusiveMaximum": { "$ref": "#/$defs/localizable", "description": "Shown when the value is not strictly below `exclusiveMaximum`" }, "multipleOf": { "$ref": "#/$defs/localizable", "description": "Shown when the value is not a multiple of `multipleOf`" }, "enum": { "$ref": "#/$defs/localizable", "description": "Shown when the value is not in the `enum` list" }, "const": { "$ref": "#/$defs/localizable", "description": "Shown when the value does not equal `const`" } }, "additionalProperties": false } }, "required": ["type"], "additionalProperties": false }, "booleanValidator": { "type": "object", "properties": { "type": { "const": "boolean" }, "required": { "type": "boolean", "description": "Makes the field mandatory — validation fails if the value is absent or false (e.g. an unchecked required checkbox)" }, "const": { "description": "The value must equal exactly this boolean (e.g. `true` to require acceptance)" }, "messages": { "type": "object", "description": "Custom error messages that override the default text for each constraint violation", "properties": { "invalid": { "$ref": "#/$defs/localizable", "description": "Shown when the value fails general type validation" }, "const": { "$ref": "#/$defs/localizable", "description": "Shown when the value does not equal `const`" } }, "additionalProperties": false } }, "required": ["type"], "additionalProperties": false }, "arrayValidator": { "type": "object", "properties": { "type": { "const": "array" }, "required": { "type": "boolean", "description": "Makes the field mandatory — validation fails if the array is empty or absent" }, "minItems": { "type": "number", "description": "The array must contain at least this many items" }, "maxItems": { "type": "number", "description": "The array must contain no more than this many items" }, "uniqueItems": { "type": "boolean", "description": "When true, all items in the array must be distinct values" }, "messages": { "type": "object", "description": "Custom error messages that override the default text for each constraint violation", "properties": { "invalid": { "$ref": "#/$defs/localizable", "description": "Shown when the value fails general type validation" }, "required": { "$ref": "#/$defs/localizable", "description": "Shown when a required array is empty or absent" }, "minItems": { "$ref": "#/$defs/localizable", "description": "Shown when the array has fewer items than `minItems`" }, "maxItems": { "$ref": "#/$defs/localizable", "description": "Shown when the array has more items than `maxItems`" } }, "additionalProperties": false } }, "required": ["type"], "additionalProperties": false }, "customValidator": { "type": "object", "description": "A custom validator that delegates validation to application code. The registered validator function receives these constraint keys and must return a Standard Schema V1-compliant schema object (https://standardschema.dev). Add arbitrary constraint keys alongside `type` — they are passed as-is to the registered validator function.", "properties": { "type": { "const": "custom" }, "required": { "type": "boolean", "description": "Makes the field mandatory — evaluation is still delegated to the custom validator" } }, "required": ["type"], "additionalProperties": true }, "validator": { "description": "Validation rules for this field. The `type` discriminant selects the validator: `string`, `number`, `integer`, `boolean`, `array`, or `custom`.", "oneOf": [{ "$ref": "#/$defs/stringValidator" }, { "$ref": "#/$defs/numberValidator" }, { "$ref": "#/$defs/booleanValidator" }, { "$ref": "#/$defs/arrayValidator" }, { "$ref": "#/$defs/customValidator" }] } };
405
411
  const validatorsSchema = {
406
412
  $schema: $schema$r,
407
413
  $id: $id$r,
@@ -412,9 +418,9 @@ const $schema$q = "https://json-schema.org/draft/2020-12/schema";
412
418
  const $id$q = "https://golemui.com/schemas/components/accordion.schema.json";
413
419
  const title$q = "Accordion Widget";
414
420
  const type$q = "object";
415
- const $defs$q = { "singleOpenProp": { "type": "boolean" }, "defaultOpenProp": { "type": "object", "additionalProperties": { "type": "boolean" }, "minProperties": 1 }, "renderModeProp": { "enum": ["all", "activeOnly"] }, "sectionsProp": { "type": "array", "minItems": 1, "items": { "type": "object", "properties": { "label": { "$ref": "../common.schema.json#/$defs/localizable" }, "uid": { "type": "string" } }, "required": ["label", "uid"], "additionalProperties": false } } };
421
+ const $defs$q = { "singleOpenProp": { "type": "boolean", "description": "When true, opening one section automatically closes all others. Defaults to false." }, "defaultOpenProp": { "type": "object", "description": "Map of section UIDs to booleans indicating which sections are open on initial render", "additionalProperties": { "type": "boolean" }, "minProperties": 1 }, "renderModeProp": { "description": "Controls when section content is rendered: `all` keeps all sections in the DOM, `activeOnly` unmounts inactive ones", "enum": ["all", "activeOnly"] }, "sectionsProp": { "type": "array", "description": "Ordered list of section definitions. Each section's `uid` must match the widget UIDs used in `children`.", "minItems": 1, "items": { "type": "object", "properties": { "label": { "$ref": "../common.schema.json#/$defs/localizable" }, "uid": { "type": "string", "description": "Unique identifier linking this section definition to its widgets in `children`" } }, "required": ["label", "uid"], "additionalProperties": false } } };
416
422
  const allOf$q = [{ "$ref": "../common.schema.json#/$defs/baseWidget" }];
417
- const properties$q = { "kind": { "const": "layout" }, "type": { "const": "accordion" }, "on": { "$ref": "../common.schema.json#/$defs/on" }, "children": { "type": "array", "minItems": 1, "items": { "$ref": "../form.schema.json#/$defs/formWidget" } }, "props": { "type": "object", "properties": { "singleOpen": { "$ref": "#/$defs/singleOpenProp" }, "defaultOpen": { "$ref": "#/$defs/defaultOpenProp" }, "renderMode": { "$ref": "#/$defs/renderModeProp" }, "sections": { "$ref": "#/$defs/sectionsProp" } }, "required": ["sections"], "patternProperties": { "^singleOpen\\.[^.]+$": { "$ref": "#/$defs/singleOpenProp" }, "^defaultOpen\\.[^.]+$": { "$ref": "#/$defs/defaultOpenProp" }, "^renderMode\\.[^.]+$": { "$ref": "#/$defs/renderModeProp" }, "^sections\\.[^.]+$": { "$ref": "#/$defs/sectionsProp" } }, "additionalProperties": false } };
423
+ const properties$q = { "kind": { "const": "layout" }, "type": { "const": "accordion" }, "on": { "$ref": "../common.schema.json#/$defs/on" }, "children": { "type": "array", "description": "Flat list of widgets belonging to all sections. Each widget must have a `uid` matching one of the section UIDs in `props.sections`.", "minItems": 1, "items": { "$ref": "../form.schema.json#/$defs/formWidget" } }, "props": { "type": "object", "properties": { "singleOpen": { "$ref": "#/$defs/singleOpenProp" }, "defaultOpen": { "$ref": "#/$defs/defaultOpenProp" }, "renderMode": { "$ref": "#/$defs/renderModeProp" }, "sections": { "$ref": "#/$defs/sectionsProp" } }, "required": ["sections"], "patternProperties": { "^singleOpen\\.[^.]+$": { "$ref": "#/$defs/singleOpenProp" }, "^defaultOpen\\.[^.]+$": { "$ref": "#/$defs/defaultOpenProp" }, "^renderMode\\.[^.]+$": { "$ref": "#/$defs/renderModeProp" }, "^sections\\.[^.]+$": { "$ref": "#/$defs/sectionsProp" } }, "additionalProperties": false } };
418
424
  const required$q = ["kind", "type", "children", "props"];
419
425
  const unevaluatedProperties$q = false;
420
426
  const accordion = {
@@ -474,7 +480,7 @@ const $schema$n = "https://json-schema.org/draft/2020-12/schema";
474
480
  const $id$n = "https://golemui.com/schemas/components/calendar.schema.json";
475
481
  const title$n = "Calendar Widget";
476
482
  const type$n = "object";
477
- const $defs$n = { "labelDef": { "$ref": "../common.schema.json#/$defs/localizable" }, "disabledDef": { "$ref": "../common.schema.json#/$defs/boolOrWhen" }, "readonlyDef": { "$ref": "../common.schema.json#/$defs/boolOrWhen" }, "hintProp": { "$ref": "../common.schema.json#/$defs/localizable" }, "prevMonthIconProp": { "type": "string" }, "nextMonthIconProp": { "type": "string" }, "prevMonthAriaLabelProp": { "$ref": "../common.schema.json#/$defs/localizable" }, "nextMonthAriaLabelProp": { "$ref": "../common.schema.json#/$defs/localizable" }, "dayFormatProp": { "enum": ["numeric", "2-digit"] }, "weekdayFormatProp": { "enum": ["short", "long", "narrow"] }, "monthFormatProp": { "enum": ["numeric", "2-digit", "long", "short", "narrow"] }, "minDateProp": { "type": "string" }, "maxDateProp": { "type": "string" }, "numberOfMonthsProp": { "type": "number" }, "disabledRangesProp": { "type": "array", "items": { "$ref": "../common.schema.json#/$defs/dateRange" } } };
483
+ const $defs$n = { "labelDef": { "$ref": "../common.schema.json#/$defs/localizable" }, "disabledDef": { "$ref": "../common.schema.json#/$defs/boolOrWhen" }, "readonlyDef": { "$ref": "../common.schema.json#/$defs/boolOrWhen" }, "hintProp": { "$ref": "../common.schema.json#/$defs/localizable" }, "prevMonthIconProp": { "type": "string" }, "nextMonthIconProp": { "type": "string" }, "prevMonthAriaLabelProp": { "$ref": "../common.schema.json#/$defs/localizable" }, "nextMonthAriaLabelProp": { "$ref": "../common.schema.json#/$defs/localizable" }, "dayFormatProp": { "description": "Formatting of day numbers in the calendar grid (`numeric` = 1–31, `2-digit` = 01–31)", "enum": ["numeric", "2-digit"] }, "weekdayFormatProp": { "description": "Formatting of weekday column headers (`short` = Mon, `long` = Monday, `narrow` = M)", "enum": ["short", "long", "narrow"] }, "monthFormatProp": { "description": "Formatting of the month heading (`long` = January, `short` = Jan, `narrow` = J, `numeric` = 1, `2-digit` = 01)", "enum": ["numeric", "2-digit", "long", "short", "narrow"] }, "minDateProp": { "type": "string", "description": "ISO-8601 date string for the earliest selectable date (inclusive)" }, "maxDateProp": { "type": "string", "description": "ISO-8601 date string for the latest selectable date (inclusive)" }, "numberOfMonthsProp": { "type": "number", "description": "Number of month panels to display side by side. Defaults to 1." }, "disabledRangesProp": { "type": "array", "description": "Date ranges that are shown but cannot be selected", "items": { "$ref": "../common.schema.json#/$defs/dateRange" } } };
478
484
  const allOf$n = [{ "$ref": "../common.schema.json#/$defs/baseWidget" }];
479
485
  const properties$n = { "kind": { "const": "input" }, "type": { "const": "calendar" }, "path": { "$ref": "../common.schema.json#/$defs/dotPath" }, "label": { "$ref": "#/$defs/labelDef" }, "disabled": { "$ref": "#/$defs/disabledDef" }, "readonly": { "$ref": "#/$defs/readonlyDef" }, "on": { "$ref": "../common.schema.json#/$defs/on" }, "validator": { "$ref": "../validators.schema.json#/$defs/validator" }, "props": { "type": "object", "properties": { "hint": { "$ref": "#/$defs/hintProp" }, "prevMonthIcon": { "$ref": "#/$defs/prevMonthIconProp" }, "nextMonthIcon": { "$ref": "#/$defs/nextMonthIconProp" }, "prevMonthAriaLabel": { "$ref": "#/$defs/prevMonthAriaLabelProp" }, "nextMonthAriaLabel": { "$ref": "#/$defs/nextMonthAriaLabelProp" }, "dayFormat": { "$ref": "#/$defs/dayFormatProp" }, "weekdayFormat": { "$ref": "#/$defs/weekdayFormatProp" }, "monthFormat": { "$ref": "#/$defs/monthFormatProp" }, "minDate": { "$ref": "#/$defs/minDateProp" }, "maxDate": { "$ref": "#/$defs/maxDateProp" }, "numberOfMonths": { "$ref": "#/$defs/numberOfMonthsProp" }, "disabledRanges": { "$ref": "#/$defs/disabledRangesProp" } }, "patternProperties": { "^hint\\.[^.]+$": { "$ref": "#/$defs/hintProp" }, "^prevMonthIcon\\.[^.]+$": { "$ref": "#/$defs/prevMonthIconProp" }, "^nextMonthIcon\\.[^.]+$": { "$ref": "#/$defs/nextMonthIconProp" }, "^prevMonthAriaLabel\\.[^.]+$": { "$ref": "#/$defs/prevMonthAriaLabelProp" }, "^nextMonthAriaLabel\\.[^.]+$": { "$ref": "#/$defs/nextMonthAriaLabelProp" }, "^dayFormat\\.[^.]+$": { "$ref": "#/$defs/dayFormatProp" }, "^weekdayFormat\\.[^.]+$": { "$ref": "#/$defs/weekdayFormatProp" }, "^monthFormat\\.[^.]+$": { "$ref": "#/$defs/monthFormatProp" }, "^minDate\\.[^.]+$": { "$ref": "#/$defs/minDateProp" }, "^maxDate\\.[^.]+$": { "$ref": "#/$defs/maxDateProp" }, "^numberOfMonths\\.[^.]+$": { "$ref": "#/$defs/numberOfMonthsProp" }, "^disabledRanges\\.[^.]+$": { "$ref": "#/$defs/disabledRangesProp" } }, "additionalProperties": false } };
480
486
  const patternProperties$j = { "^label\\.[^.]+$": { "$ref": "#/$defs/labelDef" }, "^disabled\\.[^.]+$": { "$ref": "#/$defs/disabledDef" }, "^readonly\\.[^.]+$": { "$ref": "#/$defs/readonlyDef" }, "^validator\\.[^.]+$": { "$ref": "../validators.schema.json#/$defs/validator" } };
@@ -584,7 +590,7 @@ const $schema$i = "https://json-schema.org/draft/2020-12/schema";
584
590
  const $id$i = "https://golemui.com/schemas/components/dropdown.schema.json";
585
591
  const title$i = "Dropdown Widget";
586
592
  const type$i = "object";
587
- const $defs$i = { "disabledDef": { "$ref": "../common.schema.json#/$defs/boolOrWhen" }, "readonlyDef": { "$ref": "../common.schema.json#/$defs/boolOrWhen" }, "labelDef": { "$ref": "../common.schema.json#/$defs/localizable" }, "placeholderProp": { "$ref": "../common.schema.json#/$defs/localizable" }, "hintProp": { "$ref": "../common.schema.json#/$defs/localizable" }, "itemsProp": { "type": "array", "description": "Items are either primitive values (string/number) or arbitrary record objects (paired with `labelField`/`valueField`/`itemRenderer` for custom rendering). Note: widget templates inside items are only supported on `repeater`, not here.", "items": { "type": ["string", "number", "object"] } }, "labelFieldProp": { "type": "string" }, "valueFieldProp": { "type": "string" }, "searchFieldsProp": { "type": "array", "items": { "type": "string" } }, "heightProp": { "type": "number" }, "itemHeightProp": { "type": "number" }, "itemRendererProp": { "type": "string" }, "inputDebounceProp": { "type": "number" }, "autocompleteProp": { "type": "string" } };
593
+ const $defs$i = { "disabledDef": { "$ref": "../common.schema.json#/$defs/boolOrWhen" }, "readonlyDef": { "$ref": "../common.schema.json#/$defs/boolOrWhen" }, "labelDef": { "$ref": "../common.schema.json#/$defs/localizable" }, "placeholderProp": { "$ref": "../common.schema.json#/$defs/localizable" }, "hintProp": { "$ref": "../common.schema.json#/$defs/localizable" }, "itemsProp": { "type": "array", "description": "Items are either primitive values (string/number) or arbitrary record objects (paired with `labelField`/`valueField`/`itemRenderer` for custom rendering). Note: widget templates inside items are only supported on `repeater`, not here.", "items": { "type": ["string", "number", "object"] } }, "labelFieldProp": { "type": "string", "description": 'Object property used as the display label when items are objects (e.g. `"name"` reads `item.name`)' }, "valueFieldProp": { "type": "string", "description": 'Object property used as the stored value when items are objects (e.g. `"id"` stores `item.id`)' }, "searchFieldsProp": { "type": "array", "description": "Object properties searched when the user types in the filter box. Defaults to `labelField` when omitted.", "items": { "type": "string" } }, "heightProp": { "type": "number", "description": "Fixed pixel height of the dropdown list panel (enables virtual scrolling for large lists)" }, "itemHeightProp": { "type": "number", "description": "Fixed pixel height of each item row, required for virtual scrolling when `height` is set" }, "itemRendererProp": { "type": "string", "description": "Name of a registered custom item renderer component used to display each option" }, "inputDebounceProp": { "type": "number", "description": "Milliseconds to debounce the filter input before firing the `on.filter` event (default: 0)" }, "autocompleteProp": { "type": "string", "description": "HTML `autocomplete` attribute applied to the underlying input element" } };
588
594
  const allOf$i = [{ "$ref": "../common.schema.json#/$defs/baseWidget" }];
589
595
  const properties$i = { "kind": { "const": "input" }, "type": { "const": "dropdown" }, "path": { "$ref": "../common.schema.json#/$defs/dotPath" }, "label": { "$ref": "#/$defs/labelDef" }, "disabled": { "$ref": "#/$defs/disabledDef" }, "readonly": { "$ref": "#/$defs/readonlyDef" }, "on": { "$ref": "../common.schema.json#/$defs/on" }, "validator": { "$ref": "../validators.schema.json#/$defs/validator" }, "props": { "type": "object", "required": ["items"], "properties": { "placeholder": { "$ref": "#/$defs/placeholderProp" }, "hint": { "$ref": "#/$defs/hintProp" }, "items": { "$ref": "#/$defs/itemsProp" }, "labelField": { "$ref": "#/$defs/labelFieldProp" }, "valueField": { "$ref": "#/$defs/valueFieldProp" }, "searchFields": { "$ref": "#/$defs/searchFieldsProp" }, "height": { "$ref": "#/$defs/heightProp" }, "itemHeight": { "$ref": "#/$defs/itemHeightProp" }, "itemRenderer": { "$ref": "#/$defs/itemRendererProp" }, "inputDebounce": { "$ref": "#/$defs/inputDebounceProp" }, "autocomplete": { "$ref": "#/$defs/autocompleteProp" } }, "patternProperties": { "^placeholder\\.[^.]+$": { "$ref": "#/$defs/placeholderProp" }, "^hint\\.[^.]+$": { "$ref": "#/$defs/hintProp" }, "^items\\.[^.]+$": { "$ref": "#/$defs/itemsProp" }, "^labelField\\.[^.]+$": { "$ref": "#/$defs/labelFieldProp" }, "^valueField\\.[^.]+$": { "$ref": "#/$defs/valueFieldProp" }, "^searchFields\\.[^.]+$": { "$ref": "#/$defs/searchFieldsProp" }, "^height\\.[^.]+$": { "$ref": "#/$defs/heightProp" }, "^itemHeight\\.[^.]+$": { "$ref": "#/$defs/itemHeightProp" }, "^itemRenderer\\.[^.]+$": { "$ref": "#/$defs/itemRendererProp" }, "^inputDebounce\\.[^.]+$": { "$ref": "#/$defs/inputDebounceProp" }, "^autocomplete\\.[^.]+$": { "$ref": "#/$defs/autocompleteProp" } }, "additionalProperties": false } };
590
596
  const patternProperties$e = { "^label\\.[^.]+$": { "$ref": "#/$defs/labelDef" }, "^disabled\\.[^.]+$": { "$ref": "#/$defs/disabledDef" }, "^readonly\\.[^.]+$": { "$ref": "#/$defs/readonlyDef" }, "^validator\\.[^.]+$": { "$ref": "../validators.schema.json#/$defs/validator" } };
@@ -754,7 +760,7 @@ const $schema$a = "https://json-schema.org/draft/2020-12/schema";
754
760
  const $id$a = "https://golemui.com/schemas/components/radiogroup.schema.json";
755
761
  const title$a = "RadioGroup Widget";
756
762
  const type$a = "object";
757
- const $defs$a = { "disabledDef": { "$ref": "../common.schema.json#/$defs/boolOrWhen" }, "readonlyDef": { "$ref": "../common.schema.json#/$defs/boolOrWhen" }, "labelDef": { "$ref": "../common.schema.json#/$defs/localizable" }, "hintProp": { "$ref": "../common.schema.json#/$defs/localizable" }, "optionsProp": { "type": "array", "description": "Options may be primitive values (string/number), structured `{ label, value }` records, or arbitrary objects (paired with `labelField`/`valueField` for custom rendering).", "items": { "type": ["string", "number", "object"] } }, "labelFieldProp": { "type": "string" }, "valueFieldProp": { "type": "string" }, "directionProp": { "enum": ["row", "column"], "default": "column" } };
763
+ const $defs$a = { "disabledDef": { "$ref": "../common.schema.json#/$defs/boolOrWhen" }, "readonlyDef": { "$ref": "../common.schema.json#/$defs/boolOrWhen" }, "labelDef": { "$ref": "../common.schema.json#/$defs/localizable" }, "hintProp": { "$ref": "../common.schema.json#/$defs/localizable" }, "optionsProp": { "type": "array", "description": "Options may be primitive values (string/number), structured `{ label, value }` records, or arbitrary objects (paired with `labelField`/`valueField` for custom rendering).", "items": { "type": ["string", "number", "object"] } }, "labelFieldProp": { "type": "string", "description": 'Object property used as the display label when options are objects (e.g. `"name"` reads `item.name`)' }, "valueFieldProp": { "type": "string", "description": 'Object property used as the stored value when options are objects (e.g. `"id"` stores `item.id`)' }, "directionProp": { "description": "Layout direction for the radio buttons (`row` = horizontal, `column` = vertical). Defaults to `column`.", "enum": ["row", "column"], "default": "column" } };
758
764
  const allOf$a = [{ "$ref": "../common.schema.json#/$defs/baseWidget" }];
759
765
  const properties$a = { "kind": { "const": "input" }, "type": { "const": "radiogroup" }, "path": { "$ref": "../common.schema.json#/$defs/dotPath" }, "label": { "$ref": "#/$defs/labelDef" }, "disabled": { "$ref": "#/$defs/disabledDef" }, "readonly": { "$ref": "#/$defs/readonlyDef" }, "on": { "$ref": "../common.schema.json#/$defs/on" }, "validator": { "$ref": "../validators.schema.json#/$defs/validator" }, "props": { "type": "object", "properties": { "hint": { "$ref": "#/$defs/hintProp" }, "options": { "$ref": "#/$defs/optionsProp" }, "labelField": { "$ref": "#/$defs/labelFieldProp" }, "valueField": { "$ref": "#/$defs/valueFieldProp" }, "direction": { "$ref": "#/$defs/directionProp" } }, "patternProperties": { "^hint\\.[^.]+$": { "$ref": "#/$defs/hintProp" }, "^options\\.[^.]+$": { "$ref": "#/$defs/optionsProp" }, "^labelField\\.[^.]+$": { "$ref": "#/$defs/labelFieldProp" }, "^valueField\\.[^.]+$": { "$ref": "#/$defs/valueFieldProp" }, "^direction\\.[^.]+$": { "$ref": "#/$defs/directionProp" } }, "additionalProperties": false } };
760
766
  const patternProperties$9 = { "^label\\.[^.]+$": { "$ref": "#/$defs/labelDef" }, "^disabled\\.[^.]+$": { "$ref": "#/$defs/disabledDef" }, "^readonly\\.[^.]+$": { "$ref": "#/$defs/readonlyDef" }, "^validator\\.[^.]+$": { "$ref": "../validators.schema.json#/$defs/validator" } };
@@ -776,7 +782,7 @@ const $schema$9 = "https://json-schema.org/draft/2020-12/schema";
776
782
  const $id$9 = "https://golemui.com/schemas/components/rangecalendar.schema.json";
777
783
  const title$9 = "RangeCalendar Widget";
778
784
  const type$9 = "object";
779
- const $defs$9 = { "disabledDef": { "$ref": "../common.schema.json#/$defs/boolOrWhen" }, "readonlyDef": { "$ref": "../common.schema.json#/$defs/boolOrWhen" }, "labelDef": { "$ref": "../common.schema.json#/$defs/localizable" }, "hintProp": { "$ref": "../common.schema.json#/$defs/localizable" }, "prevMonthIconProp": { "type": "string" }, "nextMonthIconProp": { "type": "string" }, "prevMonthAriaLabelProp": { "$ref": "../common.schema.json#/$defs/localizable" }, "nextMonthAriaLabelProp": { "$ref": "../common.schema.json#/$defs/localizable" }, "removePillAriaLabelProp": { "$ref": "../common.schema.json#/$defs/localizable" }, "dayFormatProp": { "enum": ["numeric", "2-digit"] }, "weekdayFormatProp": { "enum": ["short", "long", "narrow"] }, "monthFormatProp": { "enum": ["numeric", "2-digit", "long", "short", "narrow"] }, "minDateProp": { "type": "string" }, "maxDateProp": { "type": "string" }, "numberOfMonthsProp": { "type": "number" }, "disabledRangesProp": { "type": "array", "items": { "$ref": "../common.schema.json#/$defs/dateRange" } } };
785
+ const $defs$9 = { "disabledDef": { "$ref": "../common.schema.json#/$defs/boolOrWhen" }, "readonlyDef": { "$ref": "../common.schema.json#/$defs/boolOrWhen" }, "labelDef": { "$ref": "../common.schema.json#/$defs/localizable" }, "hintProp": { "$ref": "../common.schema.json#/$defs/localizable" }, "prevMonthIconProp": { "type": "string" }, "nextMonthIconProp": { "type": "string" }, "prevMonthAriaLabelProp": { "$ref": "../common.schema.json#/$defs/localizable" }, "nextMonthAriaLabelProp": { "$ref": "../common.schema.json#/$defs/localizable" }, "removePillAriaLabelProp": { "description": "Accessible label for the button that clears the selected range pill", "$ref": "../common.schema.json#/$defs/localizable" }, "dayFormatProp": { "description": "Formatting of day numbers in the calendar grid (`numeric` = 1–31, `2-digit` = 01–31)", "enum": ["numeric", "2-digit"] }, "weekdayFormatProp": { "description": "Formatting of weekday column headers (`short` = Mon, `long` = Monday, `narrow` = M)", "enum": ["short", "long", "narrow"] }, "monthFormatProp": { "description": "Formatting of the month heading (`long` = January, `short` = Jan, `narrow` = J, `numeric` = 1, `2-digit` = 01)", "enum": ["numeric", "2-digit", "long", "short", "narrow"] }, "minDateProp": { "type": "string", "description": "ISO-8601 date string for the earliest selectable date (inclusive)" }, "maxDateProp": { "type": "string", "description": "ISO-8601 date string for the latest selectable date (inclusive)" }, "numberOfMonthsProp": { "type": "number", "description": "Number of month panels to display side by side. Defaults to 1." }, "disabledRangesProp": { "type": "array", "description": "Date ranges that are shown but cannot be selected", "items": { "$ref": "../common.schema.json#/$defs/dateRange" } } };
780
786
  const allOf$9 = [{ "$ref": "../common.schema.json#/$defs/baseWidget" }];
781
787
  const properties$9 = { "kind": { "const": "input" }, "type": { "const": "rangeCalendar" }, "path": { "$ref": "../common.schema.json#/$defs/dotPath" }, "label": { "$ref": "#/$defs/labelDef" }, "disabled": { "$ref": "#/$defs/disabledDef" }, "readonly": { "$ref": "#/$defs/readonlyDef" }, "on": { "$ref": "../common.schema.json#/$defs/on" }, "validator": { "$ref": "../validators.schema.json#/$defs/validator" }, "props": { "type": "object", "properties": { "hint": { "$ref": "#/$defs/hintProp" }, "prevMonthIcon": { "$ref": "#/$defs/prevMonthIconProp" }, "nextMonthIcon": { "$ref": "#/$defs/nextMonthIconProp" }, "prevMonthAriaLabel": { "$ref": "#/$defs/prevMonthAriaLabelProp" }, "nextMonthAriaLabel": { "$ref": "#/$defs/nextMonthAriaLabelProp" }, "removePillAriaLabel": { "$ref": "#/$defs/removePillAriaLabelProp" }, "dayFormat": { "$ref": "#/$defs/dayFormatProp" }, "weekdayFormat": { "$ref": "#/$defs/weekdayFormatProp" }, "monthFormat": { "$ref": "#/$defs/monthFormatProp" }, "minDate": { "$ref": "#/$defs/minDateProp" }, "maxDate": { "$ref": "#/$defs/maxDateProp" }, "numberOfMonths": { "$ref": "#/$defs/numberOfMonthsProp" }, "disabledRanges": { "$ref": "#/$defs/disabledRangesProp" } }, "patternProperties": { "^hint\\.[^.]+$": { "$ref": "#/$defs/hintProp" }, "^prevMonthIcon\\.[^.]+$": { "$ref": "#/$defs/prevMonthIconProp" }, "^nextMonthIcon\\.[^.]+$": { "$ref": "#/$defs/nextMonthIconProp" }, "^prevMonthAriaLabel\\.[^.]+$": { "$ref": "#/$defs/prevMonthAriaLabelProp" }, "^nextMonthAriaLabel\\.[^.]+$": { "$ref": "#/$defs/nextMonthAriaLabelProp" }, "^removePillAriaLabel\\.[^.]+$": { "$ref": "#/$defs/removePillAriaLabelProp" }, "^dayFormat\\.[^.]+$": { "$ref": "#/$defs/dayFormatProp" }, "^weekdayFormat\\.[^.]+$": { "$ref": "#/$defs/weekdayFormatProp" }, "^monthFormat\\.[^.]+$": { "$ref": "#/$defs/monthFormatProp" }, "^minDate\\.[^.]+$": { "$ref": "#/$defs/minDateProp" }, "^maxDate\\.[^.]+$": { "$ref": "#/$defs/maxDateProp" }, "^numberOfMonths\\.[^.]+$": { "$ref": "#/$defs/numberOfMonthsProp" }, "^disabledRanges\\.[^.]+$": { "$ref": "#/$defs/disabledRangesProp" } }, "additionalProperties": false } };
782
788
  const patternProperties$8 = { "^label\\.[^.]+$": { "$ref": "#/$defs/labelDef" }, "^disabled\\.[^.]+$": { "$ref": "#/$defs/disabledDef" }, "^readonly\\.[^.]+$": { "$ref": "#/$defs/readonlyDef" }, "^validator\\.[^.]+$": { "$ref": "../validators.schema.json#/$defs/validator" } };
@@ -842,7 +848,7 @@ const $schema$6 = "https://json-schema.org/draft/2020-12/schema";
842
848
  const $id$6 = "https://golemui.com/schemas/components/repeater.schema.json";
843
849
  const title$6 = "Repeater Widget";
844
850
  const type$6 = "object";
845
- const $defs$6 = { "labelDef": { "$ref": "../common.schema.json#/$defs/localizable" }, "addLabelProp": { "$ref": "../common.schema.json#/$defs/localizable" }, "removeLabelProp": { "$ref": "../common.schema.json#/$defs/localizable" }, "limitProp": { "type": "number" }, "templateProp": { "$ref": "../layout-widget.schema.json" }, "titleProp": { "$ref": "../common.schema.json#/$defs/localizable" }, "addButtonIconProp": { "type": "string" }, "removeButtonIconProp": { "type": "string" } };
851
+ const $defs$6 = { "labelDef": { "$ref": "../common.schema.json#/$defs/localizable" }, "addLabelProp": { "description": "Label for the button that adds a new repeated item", "$ref": "../common.schema.json#/$defs/localizable" }, "removeLabelProp": { "description": "Label for the button that removes an existing repeated item", "$ref": "../common.schema.json#/$defs/localizable" }, "limitProp": { "type": "number", "description": "Maximum number of items the user can add. No limit when omitted." }, "templateProp": { "description": "Widget tree used as the template for each repeated item. Paths inside are resolved relative to the item's index in the array.", "$ref": "../layout-widget.schema.json" }, "titleProp": { "description": "Title displayed in the header of each item panel (supports reactive expressions referencing the item's index via `$index`)", "$ref": "../common.schema.json#/$defs/localizable" }, "addButtonIconProp": { "type": "string", "description": "Icon name or CSS class for the add-item button icon" }, "removeButtonIconProp": { "type": "string", "description": "Icon name or CSS class for the remove-item button icon" } };
846
852
  const allOf$6 = [{ "$ref": "../common.schema.json#/$defs/baseWidget" }];
847
853
  const properties$6 = { "kind": { "const": "input" }, "type": { "const": "repeater" }, "path": { "$ref": "../common.schema.json#/$defs/dotPath" }, "label": { "$ref": "#/$defs/labelDef" }, "on": { "$ref": "../common.schema.json#/$defs/on" }, "validator": { "$ref": "../validators.schema.json#/$defs/validator" }, "props": { "type": "object", "properties": { "addLabel": { "$ref": "#/$defs/addLabelProp" }, "removeLabel": { "$ref": "#/$defs/removeLabelProp" }, "limit": { "$ref": "#/$defs/limitProp" }, "template": { "$ref": "#/$defs/templateProp" }, "title": { "$ref": "#/$defs/titleProp" }, "addButtonIcon": { "$ref": "#/$defs/addButtonIconProp" }, "removeButtonIcon": { "$ref": "#/$defs/removeButtonIconProp" } }, "required": ["template"], "patternProperties": { "^addLabel\\.[^.]+$": { "$ref": "#/$defs/addLabelProp" }, "^removeLabel\\.[^.]+$": { "$ref": "#/$defs/removeLabelProp" }, "^limit\\.[^.]+$": { "$ref": "#/$defs/limitProp" }, "^template\\.[^.]+$": { "$ref": "#/$defs/templateProp" }, "^title\\.[^.]+$": { "$ref": "#/$defs/titleProp" }, "^addButtonIcon\\.[^.]+$": { "$ref": "#/$defs/addButtonIconProp" }, "^removeButtonIcon\\.[^.]+$": { "$ref": "#/$defs/removeButtonIconProp" } }, "additionalProperties": false } };
848
854
  const patternProperties$5 = { "^label\\.[^.]+$": { "$ref": "#/$defs/labelDef" }, "^validator\\.[^.]+$": { "$ref": "../validators.schema.json#/$defs/validator" } };
@@ -864,7 +870,7 @@ const $schema$5 = "https://json-schema.org/draft/2020-12/schema";
864
870
  const $id$5 = "https://golemui.com/schemas/components/select.schema.json";
865
871
  const title$5 = "Select Widget";
866
872
  const type$5 = "object";
867
- const $defs$5 = { "disabledDef": { "$ref": "../common.schema.json#/$defs/boolOrWhen" }, "readonlyDef": { "$ref": "../common.schema.json#/$defs/boolOrWhen" }, "labelDef": { "$ref": "../common.schema.json#/$defs/localizable" }, "hintProp": { "$ref": "../common.schema.json#/$defs/localizable" }, "iconProp": { "type": "string" }, "optionsProp": { "type": "array", "description": "Options may be primitive values (string/number), structured `{ label, value }` records, or arbitrary objects (paired with `labelField`/`valueField` for custom rendering).", "items": { "type": ["string", "number", "object"] } }, "placeholderProp": { "$ref": "../common.schema.json#/$defs/localizable" }, "labelFieldProp": { "type": "string" }, "valueFieldProp": { "type": "string" }, "autocompleteProp": { "type": "string" } };
873
+ const $defs$5 = { "disabledDef": { "$ref": "../common.schema.json#/$defs/boolOrWhen" }, "readonlyDef": { "$ref": "../common.schema.json#/$defs/boolOrWhen" }, "labelDef": { "$ref": "../common.schema.json#/$defs/localizable" }, "hintProp": { "$ref": "../common.schema.json#/$defs/localizable" }, "iconProp": { "type": "string" }, "optionsProp": { "type": "array", "description": "Options may be primitive values (string/number), structured `{ label, value }` records, or arbitrary objects (paired with `labelField`/`valueField` for custom rendering).", "items": { "type": ["string", "number", "object"] } }, "placeholderProp": { "$ref": "../common.schema.json#/$defs/localizable" }, "labelFieldProp": { "type": "string", "description": 'Object property used as the display label when options are objects (e.g. `"name"` reads `item.name`)' }, "valueFieldProp": { "type": "string", "description": 'Object property used as the stored value when options are objects (e.g. `"id"` stores `item.id`)' }, "autocompleteProp": { "type": "string", "description": "HTML `autocomplete` attribute applied to the underlying input element" } };
868
874
  const allOf$5 = [{ "$ref": "../common.schema.json#/$defs/baseWidget" }];
869
875
  const properties$5 = { "kind": { "const": "input" }, "type": { "const": "select" }, "path": { "$ref": "../common.schema.json#/$defs/dotPath" }, "label": { "$ref": "#/$defs/labelDef" }, "disabled": { "$ref": "#/$defs/disabledDef" }, "readonly": { "$ref": "#/$defs/readonlyDef" }, "on": { "$ref": "../common.schema.json#/$defs/on" }, "validator": { "$ref": "../validators.schema.json#/$defs/validator" }, "props": { "type": "object", "properties": { "hint": { "$ref": "#/$defs/hintProp" }, "icon": { "$ref": "#/$defs/iconProp" }, "options": { "$ref": "#/$defs/optionsProp" }, "placeholder": { "$ref": "#/$defs/placeholderProp" }, "labelField": { "$ref": "#/$defs/labelFieldProp" }, "valueField": { "$ref": "#/$defs/valueFieldProp" }, "autocomplete": { "$ref": "#/$defs/autocompleteProp" } }, "patternProperties": { "^hint\\.[^.]+$": { "$ref": "#/$defs/hintProp" }, "^icon\\.[^.]+$": { "$ref": "#/$defs/iconProp" }, "^options\\.[^.]+$": { "$ref": "#/$defs/optionsProp" }, "^placeholder\\.[^.]+$": { "$ref": "#/$defs/placeholderProp" }, "^labelField\\.[^.]+$": { "$ref": "#/$defs/labelFieldProp" }, "^valueField\\.[^.]+$": { "$ref": "#/$defs/valueFieldProp" }, "^autocomplete\\.[^.]+$": { "$ref": "#/$defs/autocompleteProp" } }, "additionalProperties": false } };
870
876
  const patternProperties$4 = { "^label\\.[^.]+$": { "$ref": "#/$defs/labelDef" }, "^disabled\\.[^.]+$": { "$ref": "#/$defs/disabledDef" }, "^readonly\\.[^.]+$": { "$ref": "#/$defs/readonlyDef" }, "^validator\\.[^.]+$": { "$ref": "../validators.schema.json#/$defs/validator" } };
@@ -886,9 +892,9 @@ const $schema$4 = "https://json-schema.org/draft/2020-12/schema";
886
892
  const $id$4 = "https://golemui.com/schemas/components/tabs.schema.json";
887
893
  const title$4 = "Tabs Widget";
888
894
  const type$4 = "object";
889
- const $defs$4 = { "defaultOpenProp": { "type": "string" }, "renderModeProp": { "enum": ["all", "activeOnly"] }, "tabsProp": { "type": "array", "minItems": 1, "items": { "type": "object", "properties": { "label": { "$ref": "../common.schema.json#/$defs/localizable" }, "uid": { "type": "string" } }, "required": ["label", "uid"], "additionalProperties": false } } };
895
+ const $defs$4 = { "defaultOpenProp": { "type": "string", "description": "UID of the tab that is active on initial render. Defaults to the first tab." }, "renderModeProp": { "description": "Controls when tab content is rendered: `all` keeps all tabs in the DOM, `activeOnly` unmounts inactive ones", "enum": ["all", "activeOnly"] }, "tabsProp": { "type": "array", "description": "Ordered list of tab definitions. Each tab's `uid` must match the widget UIDs used in `children`.", "minItems": 1, "items": { "type": "object", "properties": { "label": { "$ref": "../common.schema.json#/$defs/localizable" }, "uid": { "type": "string", "description": "Unique identifier linking this tab definition to its widgets in `children`" } }, "required": ["label", "uid"], "additionalProperties": false } } };
890
896
  const allOf$4 = [{ "$ref": "../common.schema.json#/$defs/baseWidget" }];
891
- const properties$4 = { "kind": { "const": "layout" }, "type": { "const": "tabs" }, "on": { "$ref": "../common.schema.json#/$defs/on" }, "children": { "type": "array", "minItems": 1, "items": { "$ref": "../form.schema.json#/$defs/formWidget" } }, "props": { "type": "object", "properties": { "defaultOpen": { "$ref": "#/$defs/defaultOpenProp" }, "renderMode": { "$ref": "#/$defs/renderModeProp" }, "tabs": { "$ref": "#/$defs/tabsProp" } }, "required": ["tabs"], "patternProperties": { "^defaultOpen\\.[^.]+$": { "$ref": "#/$defs/defaultOpenProp" }, "^renderMode\\.[^.]+$": { "$ref": "#/$defs/renderModeProp" }, "^tabs\\.[^.]+$": { "$ref": "#/$defs/tabsProp" } }, "additionalProperties": false } };
897
+ const properties$4 = { "kind": { "const": "layout" }, "type": { "const": "tabs" }, "on": { "$ref": "../common.schema.json#/$defs/on" }, "children": { "type": "array", "description": "Flat list of widgets belonging to all tabs. Each widget must have a `uid` matching one of the tab UIDs in `props.tabs`.", "minItems": 1, "items": { "$ref": "../form.schema.json#/$defs/formWidget" } }, "props": { "type": "object", "properties": { "defaultOpen": { "$ref": "#/$defs/defaultOpenProp" }, "renderMode": { "$ref": "#/$defs/renderModeProp" }, "tabs": { "$ref": "#/$defs/tabsProp" } }, "required": ["tabs"], "patternProperties": { "^defaultOpen\\.[^.]+$": { "$ref": "#/$defs/defaultOpenProp" }, "^renderMode\\.[^.]+$": { "$ref": "#/$defs/renderModeProp" }, "^tabs\\.[^.]+$": { "$ref": "#/$defs/tabsProp" } }, "additionalProperties": false } };
892
898
  const required$4 = ["kind", "type", "children", "props"];
893
899
  const unevaluatedProperties$4 = false;
894
900
  const tabs = {
@@ -906,7 +912,7 @@ const $schema$3 = "https://json-schema.org/draft/2020-12/schema";
906
912
  const $id$3 = "https://golemui.com/schemas/components/tags.schema.json";
907
913
  const title$3 = "Tags Widget";
908
914
  const type$3 = "object";
909
- const $defs$3 = { "disabledDef": { "$ref": "../common.schema.json#/$defs/boolOrWhen" }, "readonlyDef": { "$ref": "../common.schema.json#/$defs/boolOrWhen" }, "labelDef": { "$ref": "../common.schema.json#/$defs/localizable" }, "hintProp": { "$ref": "../common.schema.json#/$defs/localizable" }, "placeholderProp": { "$ref": "../common.schema.json#/$defs/localizable" }, "iconProp": { "type": "string" }, "separatorsProp": { "type": "array", "items": { "type": "string" } }, "allowDuplicatesProp": { "type": "boolean" }, "trimProp": { "type": "boolean" }, "limitProp": { "type": "number" }, "removeAriaLabelProp": { "$ref": "../common.schema.json#/$defs/localizable" }, "removeIconProp": { "type": "string" } };
915
+ const $defs$3 = { "disabledDef": { "$ref": "../common.schema.json#/$defs/boolOrWhen" }, "readonlyDef": { "$ref": "../common.schema.json#/$defs/boolOrWhen" }, "labelDef": { "$ref": "../common.schema.json#/$defs/localizable" }, "hintProp": { "$ref": "../common.schema.json#/$defs/localizable" }, "placeholderProp": { "$ref": "../common.schema.json#/$defs/localizable" }, "iconProp": { "type": "string" }, "separatorsProp": { "type": "array", "description": 'Characters or strings that trigger a new tag when typed (e.g. `[",", " "]`). Defaults to Enter key only.', "items": { "type": "string" } }, "allowDuplicatesProp": { "type": "boolean", "description": "When true, the same tag value may be added more than once. Defaults to false." }, "trimProp": { "type": "boolean", "description": "When true, whitespace is trimmed from each tag before it is added. Defaults to true." }, "limitProp": { "type": "number", "description": "Maximum number of tags the user can add. No limit when omitted." }, "removeAriaLabelProp": { "$ref": "../common.schema.json#/$defs/localizable" }, "removeIconProp": { "type": "string" } };
910
916
  const allOf$3 = [{ "$ref": "../common.schema.json#/$defs/baseWidget" }];
911
917
  const properties$3 = { "kind": { "const": "input" }, "type": { "const": "tags" }, "path": { "$ref": "../common.schema.json#/$defs/dotPath" }, "label": { "$ref": "#/$defs/labelDef" }, "disabled": { "$ref": "#/$defs/disabledDef" }, "readonly": { "$ref": "#/$defs/readonlyDef" }, "on": { "$ref": "../common.schema.json#/$defs/on" }, "validator": { "$ref": "../validators.schema.json#/$defs/validator" }, "defaultValue": { "type": "array", "items": { "type": "string" } }, "props": { "type": "object", "properties": { "hint": { "$ref": "#/$defs/hintProp" }, "placeholder": { "$ref": "#/$defs/placeholderProp" }, "icon": { "$ref": "#/$defs/iconProp" }, "separators": { "$ref": "#/$defs/separatorsProp" }, "allowDuplicates": { "$ref": "#/$defs/allowDuplicatesProp" }, "trim": { "$ref": "#/$defs/trimProp" }, "limit": { "$ref": "#/$defs/limitProp" }, "removeAriaLabel": { "$ref": "#/$defs/removeAriaLabelProp" }, "removeIcon": { "$ref": "#/$defs/removeIconProp" } }, "patternProperties": { "^hint\\.[^.]+$": { "$ref": "#/$defs/hintProp" }, "^placeholder\\.[^.]+$": { "$ref": "#/$defs/placeholderProp" }, "^icon\\.[^.]+$": { "$ref": "#/$defs/iconProp" }, "^separators\\.[^.]+$": { "$ref": "#/$defs/separatorsProp" }, "^allowDuplicates\\.[^.]+$": { "$ref": "#/$defs/allowDuplicatesProp" }, "^trim\\.[^.]+$": { "$ref": "#/$defs/trimProp" }, "^limit\\.[^.]+$": { "$ref": "#/$defs/limitProp" }, "^removeAriaLabel\\.[^.]+$": { "$ref": "#/$defs/removeAriaLabelProp" }, "^removeIcon\\.[^.]+$": { "$ref": "#/$defs/removeIconProp" } }, "additionalProperties": false } };
912
918
  const patternProperties$3 = { "^disabled\\.[^.]+$": { "$ref": "#/$defs/disabledDef" }, "^readonly\\.[^.]+$": { "$ref": "#/$defs/readonlyDef" }, "^label\\.[^.]+$": { "$ref": "#/$defs/labelDef" }, "^validator\\.[^.]+$": { "$ref": "../validators.schema.json#/$defs/validator" } };
@@ -1410,13 +1416,13 @@ function dedupe(errors) {
1410
1416
  }
1411
1417
  function lintReactiveExpressions(formDefinition) {
1412
1418
  const findings = [];
1413
- walk(formDefinition, "", findings);
1419
+ walk$1(formDefinition, "", findings);
1414
1420
  return findings;
1415
1421
  }
1416
- function walk(node, path, out) {
1422
+ function walk$1(node, path, out) {
1417
1423
  if (node === null || typeof node !== "object") return;
1418
1424
  if (Array.isArray(node)) {
1419
- node.forEach((item, i) => walk(item, `${path}/${i}`, out));
1425
+ node.forEach((item, i) => walk$1(item, `${path}/${i}`, out));
1420
1426
  return;
1421
1427
  }
1422
1428
  const obj = node;
@@ -1444,7 +1450,7 @@ function walk(node, path, out) {
1444
1450
  }
1445
1451
  }
1446
1452
  if (typeof v === "object" && v !== null) {
1447
- walk(v, `${path}/${k}`, out);
1453
+ walk$1(v, `${path}/${k}`, out);
1448
1454
  }
1449
1455
  }
1450
1456
  }
@@ -1477,12 +1483,12 @@ function checkExpression(expr, path, out) {
1477
1483
  });
1478
1484
  return;
1479
1485
  }
1480
- if (!/\$form\b|\$meta\b|\$item\b|\$index\b/.test(trimmed)) {
1486
+ if (!/\$form\b|\$meta\b|\$formIsInvalid\b/.test(trimmed)) {
1481
1487
  out.push({
1482
1488
  path,
1483
1489
  expression: expr,
1484
- message: "Expression does not reference `$form`, `$meta`, `$item`, or `$index`.",
1485
- suggestion: "GolemUI expressions read form data via `$form.fieldName` or form metadata via `$meta.key`. Did you forget the prefix?"
1490
+ message: "Expression does not reference `$form`, `$meta`, or `$formIsInvalid`.",
1491
+ suggestion: "GolemUI expressions read form data via `$form.fieldName`, form metadata via `$meta.key`, or the built-in `$formIsInvalid` boolean. Did you forget the prefix?"
1486
1492
  });
1487
1493
  }
1488
1494
  if (/(?<![=!<>])=(?![=>])/.test(trimmed)) {
@@ -1509,15 +1515,15 @@ function checkExpression(expr, path, out) {
1509
1515
  suggestion: "Use strict equality `===` / `!==` to avoid type coercion (e.g. `$form.x !== undefined` rather than `$form.x != null`)."
1510
1516
  });
1511
1517
  }
1512
- if (/(?<![=!])!\s*\$(?:form|meta|item|index)\b/.test(trimmed)) {
1518
+ if (/(?<![=!])!\s*\$(?:form|meta)\b/.test(trimmed)) {
1513
1519
  out.push({
1514
1520
  path,
1515
1521
  expression: expr,
1516
- message: "Expression negates a `$form`/`$meta`/`$item`/`$index` reference (relies on truthy/falsy coercion).",
1522
+ message: "Expression negates a `$form`/`$meta` reference (relies on truthy/falsy coercion).",
1517
1523
  suggestion: 'Form data values can be `undefined`. Pick the case you actually mean and write it explicitly — `$form.x === undefined`, `$form.x === null`, `$form.x === 0`, `$form.x === ""` — instead of `!$form.x`.'
1518
1524
  });
1519
1525
  }
1520
- const refChainRe = /\$(?:form|meta|item|index)((?:\.[\w?]+)*)/g;
1526
+ const refChainRe = /\$(?:form|meta)\b((?:\.[\w?]+)*)/g;
1521
1527
  let chainFlagged = false;
1522
1528
  let chainMatch;
1523
1529
  while ((chainMatch = refChainRe.exec(trimmed)) !== null) {
@@ -1542,9 +1548,9 @@ function checkExpression(expr, path, out) {
1542
1548
  chainFlagged = true;
1543
1549
  }
1544
1550
  }
1545
- const refOnly = /^\$(?:form|meta|item|index)(?:\.[\w?]+)*$/;
1546
- const refBeforeBool = /\$(?:form|meta|item|index)(?:\.[\w?]+)*\s*(?:&&|\|\||\?(?![.?]))/;
1547
- const refAfterBool = /(?:&&|\|\|)\s*\$(?:form|meta|item|index)(?:\.[\w?]+)*\s*$/;
1551
+ const refOnly = /^\$(?:form|meta)(?:\.[\w?]+)*$/;
1552
+ const refBeforeBool = /\$(?:form|meta)(?:\.[\w?]+)*\s*(?:&&|\|\||\?(?![.?]))/;
1553
+ const refAfterBool = /(?:&&|\|\|)\s*\$(?:form|meta)(?:\.[\w?]+)*\s*$/;
1548
1554
  if (refOnly.test(trimmed) || refBeforeBool.test(trimmed) || refAfterBool.test(trimmed)) {
1549
1555
  out.push({
1550
1556
  path,
@@ -1553,7 +1559,7 @@ function checkExpression(expr, path, out) {
1553
1559
  suggestion: 'Form data values can be `undefined`. Compare explicitly: `$form.x !== undefined`, `$form.x === "value"`, `$form.items?.length > 0`. For default values use nullish coalescing: `$form.x ?? defaultValue`.'
1554
1560
  });
1555
1561
  }
1556
- const refForCmpRe = /\$(?:form|meta|item)(?:\.[\w?]+)+/g;
1562
+ const refForCmpRe = /\$(?:form|meta)(?:\.[\w?]+)+/g;
1557
1563
  let r5Flagged = false;
1558
1564
  let cmpMatch;
1559
1565
  while ((cmpMatch = refForCmpRe.exec(trimmed)) !== null) {
@@ -1576,11 +1582,128 @@ function checkExpression(expr, path, out) {
1576
1582
  }
1577
1583
  }
1578
1584
  }
1585
+ function lintStringInterpolations(formDefinition) {
1586
+ const findings = [];
1587
+ walk(formDefinition, "", findings);
1588
+ return findings;
1589
+ }
1590
+ const SLOT_REGEX = /\{\{([^}]*(?:\}[^}]+)*)\}\}/g;
1591
+ function walk(node, path, out) {
1592
+ if (node === null || typeof node !== "object") {
1593
+ return;
1594
+ }
1595
+ if (Array.isArray(node)) {
1596
+ node.forEach((item, i) => walk(item, `${path}/${i}`, out));
1597
+ return;
1598
+ }
1599
+ const obj = node;
1600
+ const isTranslationConfig = typeof obj["key"] === "string" && obj["params"] !== null && typeof obj["params"] === "object" && !Array.isArray(obj["params"]);
1601
+ if (isTranslationConfig) {
1602
+ const params = obj["params"];
1603
+ for (const [paramKey, paramValue] of Object.entries(params)) {
1604
+ if (typeof paramValue === "string") {
1605
+ checkParamExpression(paramValue, `${path}/params/${paramKey}`, out);
1606
+ }
1607
+ }
1608
+ }
1609
+ for (const [key, value] of Object.entries(obj)) {
1610
+ if (key === "params" && isTranslationConfig) {
1611
+ continue;
1612
+ }
1613
+ if (key === "defaultValue" || key.startsWith("defaultValue.")) {
1614
+ continue;
1615
+ }
1616
+ const childPath = `${path}/${key}`;
1617
+ if (typeof value === "string") {
1618
+ checkTemplate(value, childPath, out);
1619
+ } else {
1620
+ walk(value, childPath, out);
1621
+ }
1622
+ }
1623
+ }
1624
+ function checkParamExpression(value, path, out) {
1625
+ if (value.includes("{{") || value.includes("}}")) {
1626
+ out.push({
1627
+ path,
1628
+ slot: value,
1629
+ message: "i18n param expression should not use `{{` / `}}` delimiters.",
1630
+ suggestion: 'Use a bare expression: `"$form.fieldName"` not `"{{$form.fieldName}}"`.'
1631
+ });
1632
+ return;
1633
+ }
1634
+ if (value.startsWith("$") && /(?<![=!<>])=(?![=>])/.test(value)) {
1635
+ out.push({
1636
+ path,
1637
+ slot: value,
1638
+ message: "i18n param expression contains a single `=` (assignment).",
1639
+ suggestion: "Param expressions are read-only. Did you mean `===` for equality?"
1640
+ });
1641
+ }
1642
+ }
1643
+ function checkTemplate(value, path, out) {
1644
+ const openCount = (value.match(/\{\{/g) ?? []).length;
1645
+ const closeCount = (value.match(/\}\}/g) ?? []).length;
1646
+ if (openCount !== closeCount) {
1647
+ out.push({
1648
+ path,
1649
+ slot: value,
1650
+ message: "String template has unbalanced `{{` / `}}` delimiters.",
1651
+ suggestion: "Every `{{` must have a matching `}}`."
1652
+ });
1653
+ return;
1654
+ }
1655
+ if (openCount === 0) return;
1656
+ let match;
1657
+ SLOT_REGEX.lastIndex = 0;
1658
+ while ((match = SLOT_REGEX.exec(value)) !== null) {
1659
+ const slot = match[0];
1660
+ const expr = match[1];
1661
+ checkSlot(expr, slot, path, out);
1662
+ }
1663
+ }
1664
+ function checkSlot(expr, slot, path, out) {
1665
+ const trimmed = expr.trim();
1666
+ if (!trimmed) {
1667
+ out.push({
1668
+ path,
1669
+ slot,
1670
+ message: "String interpolation slot is empty.",
1671
+ suggestion: "Add an expression, e.g. `{{$form.fieldName}}`."
1672
+ });
1673
+ return;
1674
+ }
1675
+ if (trimmed.includes("{{")) {
1676
+ out.push({
1677
+ path,
1678
+ slot,
1679
+ message: "Interpolation slot contains nested `{{`.",
1680
+ suggestion: "Slots cannot be nested. Check for a copy-paste error."
1681
+ });
1682
+ return;
1683
+ }
1684
+ if (!/\$form\b|\$meta\b|\$errors\b|\$formIsInvalid\b/.test(trimmed)) {
1685
+ out.push({
1686
+ path,
1687
+ slot,
1688
+ message: "Interpolation slot does not reference `$form`, `$meta`, `$errors`, or `$formIsInvalid`.",
1689
+ suggestion: "GolemUI template slots read data via `$form.fieldName`, metadata via `$meta.key`, validation errors via `$errors.fieldName`, or the built-in `$formIsInvalid` boolean."
1690
+ });
1691
+ }
1692
+ if (/(?<![=!<>])=(?![=>])/.test(trimmed)) {
1693
+ out.push({
1694
+ path,
1695
+ slot,
1696
+ message: "Interpolation slot contains a single `=` (assignment).",
1697
+ suggestion: "Template slots are read-only. Did you mean `===` for equality?"
1698
+ });
1699
+ }
1700
+ }
1579
1701
  function validateFormDefinition(input) {
1580
1702
  const validate = getFormValidator();
1581
1703
  const ajvOk = validate(input.formDefinition);
1582
1704
  const { errors, warnings } = formatAjvErrors(validate.errors, input.formDefinition);
1583
1705
  const expressionWarnings = lintReactiveExpressions(input.formDefinition);
1706
+ const interpolationWarnings = lintStringInterpolations(input.formDefinition);
1584
1707
  if (!ajvOk && errors.length === 0 && warnings.length === 0) {
1585
1708
  errors.push({
1586
1709
  path: "/",
@@ -1592,12 +1715,13 @@ function validateFormDefinition(input) {
1592
1715
  valid: errors.length === 0,
1593
1716
  errors,
1594
1717
  warnings,
1595
- expressionWarnings
1718
+ expressionWarnings,
1719
+ interpolationWarnings
1596
1720
  };
1597
1721
  }
1598
1722
  const VALIDATE_FORM_DEFINITION_TOOL = {
1599
1723
  name: "validate_form_definition",
1600
- description: "Validate a GolemUI form definition against the bundled JSON Schemas. Use this AFTER generating or modifying a form definition to guarantee it is correct before the user pastes it into their codebase. Returns `{ valid, errors, warnings, expressionWarnings }`. Hard mistakes (typos in widget `type`, missing required props, invalid validator shapes) show up in `errors` and flip `valid` to false. Likely-custom widgets (a `type` value that isn't a built-in and isn't close to one) show up in `warnings` instead — they don't affect `valid`. Reactive expressions (`include.when`, `disabled.when`, etc.) are linted separately into `expressionWarnings`.",
1724
+ description: "Validate a GolemUI form definition against the bundled JSON Schemas. Use this AFTER generating or modifying a form definition to guarantee it is correct before the user pastes it into their codebase. Returns `{ valid, errors, warnings, expressionWarnings, interpolationWarnings }`. Hard mistakes (typos in widget `type`, missing required props, invalid validator shapes) show up in `errors` and flip `valid` to false. Likely-custom widgets (a `type` value that isn't a built-in and isn't close to one) show up in `warnings` instead — they don't affect `valid`. Reactive expressions (`include.when`, `disabled.when`, etc.) are linted separately into `expressionWarnings`. String interpolation templates (`{{$form.x}}`, `{{$meta.y}}`, expressions like `{{$form.count + 1}}`, etc.) in widget props, and bare expressions inside i18n `params` objects, are linted into `interpolationWarnings`.",
1601
1725
  inputSchema: {
1602
1726
  type: "object",
1603
1727
  properties: {
@@ -1775,7 +1899,7 @@ function resolveLocalRef(ref, doc) {
1775
1899
  }
1776
1900
  const GENERATE_FROM_OPENAPI_TOOL = {
1777
1901
  name: "generate_from_openapi",
1778
- description: 'Generate a GolemUI form for a specific OpenAPI 3.x operation (e.g. "POST /users"). Resolves the operation\'s JSON request body, dereferences `$ref`s, then maps it to a validated form definition. Falls back to operation parameters when no request body is present. Pass either a parsed `document` or a `documentUrl` to fetch.',
1902
+ description: 'Generate a GolemUI form for a specific OpenAPI 3.x operation (e.g. "POST /users"). Resolves the operation\'s JSON request body, dereferences `$ref`s, then maps it to a form definition that is validated against the GolemUI JSON Schemas before being returned, so it is guaranteed syntactically correct. Falls back to operation parameters when no request body is present. Anything the mapper cannot handle is reported in `unmapped` rather than silently dropped — use that list to surface remaining work to the user. Pass either a parsed `document` or a `documentUrl` to fetch.',
1779
1903
  inputSchema: {
1780
1904
  type: "object",
1781
1905
  properties: {
@@ -1804,6 +1928,298 @@ const GENERATE_FROM_OPENAPI_TOOL = {
1804
1928
  required: ["operation"]
1805
1929
  }
1806
1930
  };
1931
+ const STATES_CONCEPT = {
1932
+ concept: "states",
1933
+ summary: "States are named boolean conditions declared at the form root. Each state name maps to a reactive expression string (using `$form`, `$meta`, or `$formIsInvalid`) that the runtime evaluates continuously as the user interacts with the form. Once declared, state names can gate widget visibility (include / exclude) and swap individual widget properties per-state — a capability unique to named states that has no inline `when` equivalent.",
1934
+ patterns: [
1935
+ {
1936
+ name: "Declare states at the form root",
1937
+ description: 'Add a `"states"` object to the top-level form definition. Each key is a state name; each value is a reactive expression string. Expressions are evaluated at runtime — they have access to `$form` (all current form values), `$meta` (host-supplied metadata), and `$formIsInvalid` (built-in boolean — `true` when any field currently fails validation). State names can contain letters, numbers, hyphens, and underscores. Colons enable hierarchical composition — see the "Composed sub-states (colon notation)" pattern below.',
1938
+ example: {
1939
+ $schema: "https://golemui.com/schemas/form.schema.json",
1940
+ states: {
1941
+ termsAccepted: "$form.terms === true",
1942
+ hasDiscount: "$form.hasDiscountCode === true",
1943
+ limitReached: "$form.users?.length === 5"
1944
+ },
1945
+ form: ["/* ... widgets ... */"]
1946
+ }
1947
+ },
1948
+ {
1949
+ name: "Composed sub-states (colon notation)",
1950
+ description: 'Colons in a state name denote hierarchy: `"register"`, `"register:adult"`, `"register:minor:tall"`. At runtime the form engine rewrites every child expression by ANDing the full ancestor chain in front of it. This means you write only the *incremental* condition in a child — the parent conditions are inherited automatically. A child state is active only when ALL of its ancestors are also active.',
1951
+ example: {
1952
+ // What you write in the form definition:
1953
+ statesAsAuthored: {
1954
+ register: "$form.agreeTerms === true",
1955
+ "register:adult": "$form.user?.age >= 18",
1956
+ "register:minor": "$form.user?.age < 18",
1957
+ "register:minor:tall": "$form.user?.height > 180"
1958
+ },
1959
+ // What the runtime actually evaluates (expandStateExpressions output):
1960
+ statesAtRuntime: {
1961
+ register: "($form.agreeTerms === true)",
1962
+ "register:adult": "($form.agreeTerms === true) && ($form.user?.age >= 18)",
1963
+ "register:minor": "($form.agreeTerms === true) && ($form.user?.age < 18)",
1964
+ "register:minor:tall": "($form.agreeTerms === true) && ($form.user?.age < 18) && ($form.user?.height > 180)"
1965
+ }
1966
+ }
1967
+ },
1968
+ {
1969
+ name: "Conditional rendering with include / exclude",
1970
+ description: 'Use `"include": { "in": ["stateName"] }` on any widget to render it only when the named state is active. Use `"exclude": { "from": ["stateName"] }` to render it only when the state is NOT active. Both `in` and `from` are arrays — a widget can be gated on multiple states simultaneously. Prefer the named-state form (`in`/`from`) over the inline `when` expression when the same condition applies to several widgets; use `when` for one-off conditions with no reuse.',
1971
+ example: {
1972
+ $schema: "https://golemui.com/schemas/form.schema.json",
1973
+ states: {
1974
+ hasDiscount: "$form.hasDiscountCode === true"
1975
+ },
1976
+ form: [
1977
+ {
1978
+ kind: "input",
1979
+ type: "checkbox",
1980
+ path: "hasDiscountCode",
1981
+ label: "I have a discount code"
1982
+ },
1983
+ {
1984
+ kind: "input",
1985
+ type: "textinput",
1986
+ path: "discountCode",
1987
+ label: "Discount code",
1988
+ include: { in: ["hasDiscount"] }
1989
+ },
1990
+ {
1991
+ kind: "display",
1992
+ type: "alert",
1993
+ props: { text: "No discount applied." },
1994
+ exclude: { from: ["hasDiscount"] }
1995
+ }
1996
+ ]
1997
+ }
1998
+ },
1999
+ {
2000
+ name: "State-suffixed props",
2001
+ description: 'Override individual widget properties when a named state is active by appending `".<stateName>"` to the property key. The unsuffixed key holds the default value; each suffixed key supplies the override for that state. This works on root-level widget properties (`label`, `disabled`, `readonly`, `validator`, `size`) AND on any key inside the `props` object (`hint`, `placeholder`, `items`, `addLabel`, etc.). There is NO inline `when` equivalent for this — state-suffixed props REQUIRE a named state. Multiple suffixes can coexist on the same property; when more than one state is active, the last matching suffix in document order wins.',
2002
+ example: {
2003
+ $schema: "https://golemui.com/schemas/form.schema.json",
2004
+ states: {
2005
+ termsAccepted: "$form.terms === true",
2006
+ busy: "$meta.submitting === true"
2007
+ },
2008
+ form: [
2009
+ {
2010
+ kind: "input",
2011
+ type: "checkbox",
2012
+ path: "terms",
2013
+ label: "I accept the terms"
2014
+ },
2015
+ {
2016
+ kind: "action",
2017
+ type: "button",
2018
+ label: "Submit",
2019
+ "label.termsAccepted": "Submit ✓",
2020
+ "label.busy": "Submitting…",
2021
+ disabled: true,
2022
+ "disabled.termsAccepted": false,
2023
+ "disabled.busy": true
2024
+ },
2025
+ {
2026
+ kind: "display",
2027
+ type: "alert",
2028
+ props: {
2029
+ level: "warning",
2030
+ text: "Please accept the terms to continue.",
2031
+ "text.termsAccepted": "Ready to submit!"
2032
+ },
2033
+ exclude: { from: ["busy"] }
2034
+ },
2035
+ {
2036
+ kind: "input",
2037
+ type: "repeater",
2038
+ path: "users",
2039
+ label: "Users",
2040
+ addLabel: "Add user",
2041
+ "addLabel.limitReached": "Limit reached — can't add more",
2042
+ props: {
2043
+ removeLabel: "Remove",
2044
+ template: {
2045
+ kind: "layout",
2046
+ type: "flex",
2047
+ props: { direction: "column" },
2048
+ children: [
2049
+ { kind: "input", type: "textinput", path: "users.items.name", label: "Name" }
2050
+ ]
2051
+ }
2052
+ }
2053
+ }
2054
+ ]
2055
+ }
2056
+ }
2057
+ ],
2058
+ rules: [
2059
+ 'Every state name used in `include.in`, `exclude.from`, or as a property suffix MUST be declared in the root `"states"` map.',
2060
+ "Child state expressions must contain only the *additional* (incremental) condition — ancestor conditions are ANDed in automatically by the runtime. Duplicating a parent condition in a child expression is wrong and redundant.",
2061
+ 'A sub-state is only ever active when all of its ancestor states are also active. When using `include.in: ["register:adult"]` you do NOT need to also add `"register"` to the array.',
2062
+ "State-suffixed root props — only these support suffixes at the widget root level: `label`, `disabled`, `readonly`, `validator`, `size`. All other overridable properties live inside `props`.",
2063
+ 'State-suffixed props inside `props` — any key inside the `props` object can be suffixed: `"hint.<state>"`, `"placeholder.<state>"`, `"items.<state>"`, `"addLabel.<state>"`, etc.',
2064
+ 'Suffix names must not contain dots (the dot is the separator between property and state name): `"label.myState"` ✅, `"label.register:adult"` ✅ — `"label.my.state"` ❌.',
2065
+ 'Reactive expressions must reference `$form`, `$meta`, or `$formIsInvalid`. A bare identifier like `termsAccepted` without a root reference is invalid. `$formIsInvalid` is a built-in boolean (no property chain — use it as-is: `disabled: { when: "$formIsInvalid" }` or inside a state expression: `states: { formInvalid: "$formIsInvalid" }`).',
2066
+ "Use `===` / `!==` for equality, `&&` / `||` for logic. Avoid `=` (assignment), `==`/`!=` (loose equality), or bitwise `&`/`|`.",
2067
+ 'When multiple states are active at the same time and a property has more than one matching suffix, the longest state name wins (most specific takes priority). Example: if both `register` and `register:adult` are active, `"label.register:adult"` overrides `"label.register"`.',
2068
+ "The `include.when` / `exclude.when` inline form is an alternative to named states for one-off conditions, but it cannot replace state-suffixed props — those require a named state.",
2069
+ '`include.in` and `exclude.from` each accept an Array of state names. A widget included `in: ["a", "b"]` renders when state `a` OR state `b` is active.',
2070
+ "Use optional chaining (`?.`) when accessing nested fields that may not yet exist in the form data: `$form.user?.age >= 18` not `$form.user.age >= 18`."
2071
+ ]
2072
+ };
2073
+ const STRING_INTERPOLATION_CONCEPT = {
2074
+ concept: "string-interpolation",
2075
+ summary: "GolemUI supports live data binding in text props via `{{expression}}` template slots. Any string-valued property (e.g. `props.text`, `props.hint`, `label`) can embed one or more `{{...}}` slots. Each slot is a JavaScript-like expression evaluated against the live form state using a safe subset of JavaScript (no side effects, no function calls). i18n translation `params` objects accept a matching bare-expression format — the same expressions but without the `{{}}` delimiters.",
2076
+ patterns: [
2077
+ {
2078
+ name: "Template slots in display text",
2079
+ description: "Embed `{{expression}}` in any string prop to inject live values. Available scopes: `$form` (all current field values), `$meta` (host-supplied metadata), `$errors` (current validation error messages keyed by field uid), `$formIsInvalid` (boolean — `true` when any field currently fails validation). Use optional chaining (`?.`) when accessing nested fields that may not yet exist. Multiple slots can appear in a single string.",
2080
+ example: {
2081
+ $schema: "https://golemui.com/schemas/form.schema.json",
2082
+ form: [
2083
+ {
2084
+ uid: "userName",
2085
+ kind: "input",
2086
+ type: "textinput",
2087
+ path: "userName",
2088
+ validator: { type: "string", required: true }
2089
+ },
2090
+ {
2091
+ uid: "submitBtn",
2092
+ kind: "action",
2093
+ type: "button",
2094
+ label: "Submit",
2095
+ actionType: "submit"
2096
+ },
2097
+ {
2098
+ uid: "greeting",
2099
+ kind: "display",
2100
+ type: "alert",
2101
+ props: { text: "Hello {{$form.userName}}" }
2102
+ },
2103
+ {
2104
+ uid: "status",
2105
+ kind: "display",
2106
+ type: "alert",
2107
+ props: {
2108
+ text: "Error: {{$errors.userName}} | Form invalid: {{$formIsInvalid}}"
2109
+ }
2110
+ },
2111
+ {
2112
+ uid: "meta-info",
2113
+ kind: "display",
2114
+ type: "alert",
2115
+ props: { text: "Connected as {{$meta.role}} on {{$meta.server}}" }
2116
+ }
2117
+ ]
2118
+ }
2119
+ },
2120
+ {
2121
+ name: "Expressions in slots",
2122
+ description: "Slots support full JavaScript-like expressions: arithmetic, string concatenation, ternary conditionals, and optional chaining. The expression is evaluated against the same scope object (`$form`, `$meta`, `$errors`, `$formIsInvalid`). If the expression evaluates to `null` or `undefined`, the slot renders as an empty string.",
2123
+ example: {
2124
+ $schema: "https://golemui.com/schemas/form.schema.json",
2125
+ data: { firstName: "Jane", lastName: "Doe", count: 4, role: "admin" },
2126
+ form: [
2127
+ {
2128
+ uid: "full-name",
2129
+ kind: "display",
2130
+ type: "alert",
2131
+ props: { text: "Full name: {{$form.firstName + ' ' + $form.lastName}}" }
2132
+ },
2133
+ {
2134
+ uid: "next-count",
2135
+ kind: "display",
2136
+ type: "alert",
2137
+ props: { text: "Next item: {{$form.count + 1}}" }
2138
+ },
2139
+ {
2140
+ uid: "role-label",
2141
+ kind: "display",
2142
+ type: "alert",
2143
+ props: { text: "Role: {{$form.role === 'admin' ? 'Administrator' : 'User'}}" }
2144
+ },
2145
+ {
2146
+ uid: "nested",
2147
+ kind: "display",
2148
+ type: "alert",
2149
+ props: { text: "City: {{$form.address?.city}}" }
2150
+ }
2151
+ ]
2152
+ }
2153
+ },
2154
+ {
2155
+ name: "i18n param expressions",
2156
+ description: "When using i18n translations, `params` values support the same expression language as `{{}}` slots, but as **bare expressions** without the `{{}}` delimiters. Params that start with a `$` scope prefix are evaluated; others are passed as static strings. The expression has access to `$form`, `$meta`, `$errors`, and `$formIsInvalid`.",
2157
+ example: {
2158
+ $schema: "https://golemui.com/schemas/form.schema.json",
2159
+ data: { firstName: "Jane", lastName: "Doe", count: 4 },
2160
+ meta: { connectionStatus: "online" },
2161
+ form: [
2162
+ {
2163
+ uid: "greeting",
2164
+ kind: "display",
2165
+ type: "alert",
2166
+ props: {
2167
+ text: {
2168
+ key: "user.greeting",
2169
+ params: {
2170
+ hello: "Hola",
2171
+ fullName: "$form.firstName + ' ' + $form.lastName",
2172
+ n: "$form.count + 1",
2173
+ status: "$meta.connectionStatus"
2174
+ }
2175
+ }
2176
+ }
2177
+ }
2178
+ ]
2179
+ }
2180
+ }
2181
+ ],
2182
+ rules: [
2183
+ "Slots must reference at least one of `$form`, `$meta`, `$errors`, or `$formIsInvalid`. A bare identifier without a scope prefix is invalid inside `{{}}`: use `{{$form.name}}` not `{{name}}`.",
2184
+ "`$formIsInvalid` is a built-in boolean — use it as-is: `{{$formIsInvalid}}`. Do not chain properties onto it.",
2185
+ "If an expression evaluates to `null` or `undefined`, the slot renders as an empty string in display text.",
2186
+ "Use optional chaining (`?.`) when accessing nested fields that may not yet exist: `{{$form.address?.city}}` not `{{$form.address.city}}`.",
2187
+ "Do not use assignment `=` inside a slot — slots are read-only. Use `===` for equality checks.",
2188
+ "Slots cannot be nested: `{{$form.a {{$form.b}}}}` is invalid.",
2189
+ "Every `{{` must have a matching `}}`. Unbalanced delimiters cause a lint warning.",
2190
+ 'i18n `params` values are bare expressions — do NOT wrap them in `{{}}`. Write `"$form.name"` not `"{{$form.name}}"`.',
2191
+ 'Static string params (not starting with `$`) are passed through as-is — use them for constant values like `"Hola"` or `"px"`.',
2192
+ "Supported operators in expressions: arithmetic (`+`, `-`, `*`, `/`, `%`), comparison (`===`, `!==`, `<`, `>`, `<=`, `>=`), logical (`&&`, `||`, `!`), ternary (`? :`), optional chaining (`?.`), nullish coalescing (`??`).",
2193
+ "Expressions are evaluated using a safe subset of JavaScript — no `eval`, no function calls, no side effects."
2194
+ ]
2195
+ };
2196
+ const CONCEPTS = {
2197
+ states: STATES_CONCEPT,
2198
+ "string-interpolation": STRING_INTERPOLATION_CONCEPT
2199
+ };
2200
+ function getConcept(input) {
2201
+ const result = CONCEPTS[input.concept];
2202
+ if (!result) {
2203
+ const known = Object.keys(CONCEPTS).sort().map((c) => `\`${c}\``).join(", ");
2204
+ throw new Error(`Unknown concept \`${input.concept}\`. Known concepts: ${known}.`);
2205
+ }
2206
+ return result;
2207
+ }
2208
+ const GET_CONCEPT_TOOL = {
2209
+ name: "get_concept",
2210
+ description: 'Return a detailed guide for a cross-cutting GolemUI form concept — things that span multiple widgets and affect the whole form, rather than the API of a single widget. Call this when you need to: (1) change a widget\'s props based on form state (state-suffixed props like `"label.stateName": "…"`), or (2) reuse the same condition across multiple widgets (`include: { in: […] }` / `exclude: { from: […] }`). For a one-off show/hide on a single widget, use `include: { when: "…" }` or `exclude: { when: "…" }` directly — no states needed, no need to call this tool. Currently supported concepts: `states`, `string-interpolation`.',
2211
+ inputSchema: {
2212
+ type: "object",
2213
+ properties: {
2214
+ concept: {
2215
+ type: "string",
2216
+ description: 'The concept to explain. Currently supported: `"states"`, `"string-interpolation"`.',
2217
+ enum: Object.keys(CONCEPTS)
2218
+ }
2219
+ },
2220
+ required: ["concept"]
2221
+ }
2222
+ };
1807
2223
  const EXAMPLES = {
1808
2224
  accordion: {
1809
2225
  kind: "layout",
@@ -2021,7 +2437,8 @@ const EXAMPLES = {
2021
2437
  const NOTES = {
2022
2438
  textinput: [
2023
2439
  "`path` is the dot-path into form data this field writes to.",
2024
- "`validator.format` supports: `email`, `hostname`, `ipv4`, `ipv6`, `url`, `uuid`, `date`, `time`, `date-time`, `duration`."
2440
+ "`validator.format` supports: `email`, `hostname`, `ipv4`, `ipv6`, `url`, `uuid`, `date`, `time`, `date-time`, `duration`.",
2441
+ 'Root props `label`, `disabled`, `readonly`, `validator`, and `size` accept state suffixes — e.g. `"label.<stateName>": "New label"` overrides the label only when that named state is active. Props inside `props` (e.g. `hint`, `placeholder`) also accept suffixes as `"hint.<stateName>"`. Call `get_concept({ concept: "states" })` for the full pattern.'
2025
2442
  ],
2026
2443
  markdownText: [
2027
2444
  "Display-only widget for rendering markdown. Can be used as a top-level form widget (inside any layout) or inside templates like `dropdown.props.items[].template`.",
@@ -2053,13 +2470,19 @@ const NOTES = {
2053
2470
  repeater: [
2054
2471
  "`props.template` must be a layout widget (flex/grid/tabs/accordion) whose children are the per-item fields.",
2055
2472
  'Child paths inside the template MUST follow the form `<repeater.path>.items.<fieldName>`. The `items` segment is reserved — the runtime substitutes it with the current array index per row. For example, a repeater at `path: "users"` with a child `firstName` uses `path: "users.items.firstName"`. Plain `firstName` will NOT bind to the array.',
2056
- 'Nested repeaters chain the convention: a repeater at `path: "teams"` whose template contains a repeater at `path: "teams.items.members"` whose children use `path: "teams.items.members.items.<field>"`.'
2473
+ 'Nested repeaters chain the convention: a repeater at `path: "teams"` whose template contains a repeater at `path: "teams.items.members"` whose children use `path: "teams.items.members.items.<field>"`.',
2474
+ '`addLabel` supports state suffixes: `"addLabel.<stateName>": "Limit reached"` swaps the add-button label when a named state is active — useful for capping array length. Call `get_concept({ concept: "states" })` for the full pattern.'
2057
2475
  ],
2058
2476
  button: [
2059
2477
  "`actionType` controls the button's role. `actionType: \"submit\"` makes the button fire the form's `formSubmit` event natively — the host listens for it via `(formSubmit)` (Angular), `@formSubmit` (Vue), `onFormSubmit` (React), or the `form-submit` event (Lit). No custom handler needed. Use this for the primary submit button on a form.",
2060
- '`actionType: "button"` (the default, can be omitted) is a regular action button. Wire it via `on.click: "<handlerName>"` where `<handlerName>` is registered in the form config\'s event handlers.'
2478
+ '`actionType: "button"` (the default, can be omitted) is a regular action button. Wire it via `on.click: "<handlerName>"` where `<handlerName>` is registered in the form config\'s event handlers.',
2479
+ 'Supports state-suffixed props: `"label.<stateName>"` and `"disabled.<stateName>"` swap the label or disabled state when a named state is active — e.g. disable the submit button until terms are accepted, then re-enable it. Call `get_concept({ concept: "states" })` for the full pattern.'
2061
2480
  ],
2062
- checkbox: ["Set `validator.const: true` to require the user to tick it (e.g. terms acceptance)."]
2481
+ checkbox: ["Set `validator.const: true` to require the user to tick it (e.g. terms acceptance)."],
2482
+ alert: [
2483
+ 'Use `include: { in: ["stateName"] }` to show this alert only when a named state is active, or `exclude: { from: ["stateName"] }` to hide it when a state is active. This is cleaner than `include: { when: "..." }` when the same condition is reused across multiple widgets. Call `get_concept({ concept: "states" })` for the full states pattern.',
2484
+ 'Props inside `props` (e.g. `text`, `level`) accept state suffixes: `"text.<stateName>": "New message"` swaps the message when that state is active.'
2485
+ ]
2063
2486
  };
2064
2487
  function synthesizeExample(widgetType, schema) {
2065
2488
  const props = schema["properties"];
@@ -2115,11 +2538,13 @@ const TOOLS = [
2115
2538
  VALIDATE_FORM_DEFINITION_TOOL,
2116
2539
  GENERATE_FROM_JSON_SCHEMA_TOOL,
2117
2540
  GENERATE_FROM_OPENAPI_TOOL,
2118
- GET_WIDGET_SPEC_TOOL
2541
+ GET_WIDGET_SPEC_TOOL,
2542
+ GET_CONCEPT_TOOL
2119
2543
  ];
2544
+ const SERVER_INSTRUCTIONS = 'This server builds and validates GolemUI form definitions — declarative, JSON-serializable forms shaped as `{ form: [...widgets], states?: {...} }`. Its job is to help you produce a form definition that is guaranteed correct before the user pastes it into their codebase.\n\nRecommended workflow:\n1. Starting from an existing schema? Use a generator — both return a pre-validated definition, so check the returned `unmapped` list and surface anything left over to the user. For a raw JSON Schema (e.g. an API request body), call `generate_from_json_schema`. For an OpenAPI 3.x spec, call `generate_from_openapi`: pass `operation` as "METHOD /path" (e.g. "POST /users") or an exact operationId, plus the spec as a parsed `document` or a `documentUrl` to fetch — it resolves the operation\'s request body, dereferences `$ref`s, and falls back to the operation\'s parameters when there is no request body.\n2. Building or editing by hand? Look up a single widget with `get_widget_spec` (its `kind`, `props`, and `validator` shape), and cross-cutting behavior that spans widgets — conditional rendering, per-state prop overrides — with `get_concept`.\n3. ALWAYS finish by calling `validate_form_definition`. It checks the definition against the bundled JSON Schemas and returns `{ valid, errors, warnings, expressionWarnings }`. Treat `errors` as blocking: fix them and re-validate until `valid` is true. `warnings` (likely-custom widgets) and `expressionWarnings` (linted reactive expressions) are advisory — surface them, but they do not flip `valid`.\n\nDo not hand a form definition to the user until `validate_form_definition` reports `valid: true`.';
2120
2545
  const server = new Server(
2121
2546
  { name: PKG_NAME, version: PKG_VERSION },
2122
- { capabilities: { tools: {} } }
2547
+ { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS }
2123
2548
  );
2124
2549
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
2125
2550
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -2134,6 +2559,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2134
2559
  return ok(await generateFromOpenapi(args));
2135
2560
  case "get_widget_spec":
2136
2561
  return ok(getWidgetSpec(args));
2562
+ case "get_concept":
2563
+ return ok(getConcept(args));
2137
2564
  default:
2138
2565
  return err(`Unknown tool: ${name}`);
2139
2566
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@golemui/gui-mcp",
3
- "version": "0.15.1",
3
+ "version": "0.16.0",
4
4
  "description": "Model Context Protocol server for GolemUI — gives AI coding assistants deterministic schema validation and form generation for GolemUI form definitions.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -31,7 +31,8 @@
31
31
  "dependencies": {
32
32
  "@modelcontextprotocol/sdk": "^1.0.0",
33
33
  "ajv": "^8.17.1",
34
- "ajv-formats": "^3.0.1"
34
+ "ajv-formats": "^3.0.1",
35
+ "@golemui/gui-schemas": "0.16.0"
35
36
  },
36
37
  "engines": {
37
38
  "node": ">=18.0.0"