@golemui/gui-mcp 1.0.3 → 1.1.1-rc.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.
@@ -1,8 +1,11 @@
1
- import { h as checkReactiveExpression } from "./get-concept-Py3iLVmb.js";
1
+ import { h as checkReactiveExpression } from "./get-concept-DRFSt-XX.js";
2
2
  import { existsSync } from "node:fs";
3
3
  import { createRequire } from "node:module";
4
4
  import { resolve, join, dirname } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { Decoder, err, ok, object, array, string, oneOf, boolean, optional, succeed, number, literal, lazy, discriminatedUnion, record } from "ts.data.json";
7
+ import "subscript/justin";
8
+ import { pipe, map, distinctUntilChanged, filter } from "rxjs";
6
9
  const CONFIG_FIELDS = ["include", "exclude", "disabled", "readonly"];
7
10
  function lintDxSnippet(ts, sourceText, lineOffset) {
8
11
  const sf = ts.createSourceFile("__dx_lint__.ts", sourceText, ts.ScriptTarget.ES2020, true);
@@ -23,7 +26,7 @@ function lintDxSnippet(ts, sourceText, lineOffset) {
23
26
  while (ts.isPropertyAccessExpression(e)) e = e.expression;
24
27
  return ts.isIdentifier(e) && e.text === "gui";
25
28
  };
26
- const visit = (node) => {
29
+ const visit = (node, inTemplate) => {
27
30
  if (ts.isObjectLiteralExpression(node)) {
28
31
  const spreadsGui = node.properties.some(
29
32
  (p) => ts.isSpreadAssignment(p) && isGuiCall(p.expression)
@@ -46,13 +49,17 @@ function lintDxSnippet(ts, sourceText, lineOffset) {
46
49
  }
47
50
  if (ts.isPropertyAssignment(node) && nameOf(node.name) === "when" && ts.isStringLiteralLike(node.initializer)) {
48
51
  const { line, column } = posOf(node.initializer);
49
- for (const f of checkReactiveExpression(node.initializer.text, `when@${line}:${column}`)) {
52
+ const findings = checkReactiveExpression(node.initializer.text, `when@${line}:${column}`, {
53
+ inRepeaterTemplate: inTemplate
54
+ });
55
+ for (const f of findings) {
50
56
  expressionWarnings.push(f);
51
57
  }
52
58
  }
53
- ts.forEachChild(node, visit);
59
+ const childInTemplate = inTemplate || ts.isPropertyAssignment(node) && nameOf(node.name) === "template";
60
+ ts.forEachChild(node, (child) => visit(child, childInTemplate));
54
61
  };
55
- visit(sf);
62
+ visit(sf, false);
56
63
  return { diagnostics, expressionWarnings };
57
64
  }
58
65
  let cachedRequire;
@@ -63,8 +70,7 @@ const GOLEMUI_TYPE_PACKAGES = [
63
70
  { spec: "@golemui/gui-shared", rel: "gui/shared/index.d.ts" },
64
71
  { spec: "@golemui/gui-shared/internals", rel: "gui/shared/internals.d.ts" },
65
72
  { spec: "@golemui/gui-validators", rel: "gui/validators/index.d.ts" },
66
- { spec: "@golemui/core", rel: "core/index.d.ts" },
67
- { spec: "@golemui/core/internals", rel: "core/internals.d.ts" }
73
+ { spec: "@golemui/core", rel: "core/index.d.ts" }
68
74
  ];
69
75
  const DIST_SEARCH_DEPTH = 8;
70
76
  const SNIPPET_FILE = "__dx_snippet__.ts";
@@ -213,6 +219,249 @@ const DX_CHECK_CODE_TOOL = {
213
219
  required: ["code"]
214
220
  }
215
221
  };
222
+ function objectWithSuffix(specs) {
223
+ return new Decoder((json) => {
224
+ if (typeof json !== "object" || json === null) {
225
+ return err([{ message: `Expected object literal, got "${typeof json}"`, path: [] }]);
226
+ }
227
+ const out = {};
228
+ for (const [specKey, spec] of Object.entries(specs)) {
229
+ const res = spec.decoder.decode(json[specKey]);
230
+ if (!res.isOk()) {
231
+ return err(
232
+ res.issues.map((issue) => ({
233
+ message: issue.message,
234
+ path: [specKey, ...issue.path]
235
+ }))
236
+ );
237
+ }
238
+ out[specKey] = res.value;
239
+ if (spec.suffixed) {
240
+ for (const [jsonKey, jsonValue] of Object.entries(json)) {
241
+ if (jsonKey.startsWith(specKey + ".")) {
242
+ const res2 = spec.decoder.decode(jsonValue);
243
+ if (!res2.isOk()) {
244
+ return err(
245
+ res2.issues.map((issue) => ({
246
+ message: issue.message,
247
+ path: [jsonKey, ...issue.path]
248
+ }))
249
+ );
250
+ }
251
+ out[jsonKey] = res2.value;
252
+ }
253
+ }
254
+ }
255
+ }
256
+ return ok(out);
257
+ });
258
+ }
259
+ const shortUUID = () => crypto.randomUUID().slice(0, 8);
260
+ const isInputWidget = (widget) => typeof widget !== "function" && widget.kind === "input";
261
+ const isLayoutWidget = (widget) => typeof widget !== "function" && widget.kind === "layout";
262
+ const isFunctionWidget = (widget) => typeof widget === "function";
263
+ const inDecoder = object({ in: array(string()) });
264
+ const whenDecoder = object({ when: string() });
265
+ const includeDecoder = oneOf([inDecoder, whenDecoder]);
266
+ const fromDecoder = object({ from: array(string()) });
267
+ const excludeDecoder = oneOf([fromDecoder, whenDecoder]);
268
+ const boolWhenDecoder = oneOf([boolean(), whenDecoder]);
269
+ const widgetPropFnDecoder = new Decoder((json) => {
270
+ const jsonTypeof = typeof json;
271
+ if (jsonTypeof === "function") {
272
+ return ok(json);
273
+ } else {
274
+ return err([{ message: `Expected a function, got '${jsonTypeof}'`, path: [] }]);
275
+ }
276
+ });
277
+ const decodeWidgetPropOrWidgetPropFn = (decoder) => oneOf([decoder, widgetPropFnDecoder]);
278
+ const onDecoder = objectWithSuffix({
279
+ load: { suffixed: true, decoder: decodeWidgetPropOrWidgetPropFn(optional(string())) },
280
+ click: { suffixed: true, decoder: decodeWidgetPropOrWidgetPropFn(optional(string())) },
281
+ change: { suffixed: true, decoder: decodeWidgetPropOrWidgetPropFn(optional(string())) },
282
+ filter: { suffixed: true, decoder: decodeWidgetPropOrWidgetPropFn(optional(string())) },
283
+ blur: { suffixed: true, decoder: decodeWidgetPropOrWidgetPropFn(optional(string())) }
284
+ });
285
+ const translationConfigDecoder = object({
286
+ key: string(),
287
+ default: optional(string()),
288
+ params: optional(succeed())
289
+ });
290
+ const localizableDecoder = oneOf([string(), translationConfigDecoder]);
291
+ const uidDecoder = optional(string());
292
+ const displayWidgetDecoder = objectWithSuffix({
293
+ kind: { decoder: literal("display") },
294
+ uid: { decoder: uidDecoder },
295
+ type: { decoder: string() },
296
+ size: { suffixed: true, decoder: optional(number()) },
297
+ include: { decoder: optional(includeDecoder) },
298
+ exclude: { decoder: optional(excludeDecoder) },
299
+ props: { decoder: optional(succeed()) }
300
+ });
301
+ const actionWidgetDecoder = objectWithSuffix({
302
+ kind: { decoder: literal("action") },
303
+ uid: { decoder: uidDecoder },
304
+ type: { decoder: string() },
305
+ actionType: {
306
+ decoder: optional(oneOf([literal("button"), literal("submit")]))
307
+ },
308
+ size: { suffixed: true, decoder: optional(number()) },
309
+ include: { decoder: optional(includeDecoder) },
310
+ exclude: { decoder: optional(excludeDecoder) },
311
+ label: {
312
+ suffixed: true,
313
+ decoder: decodeWidgetPropOrWidgetPropFn(optional(localizableDecoder))
314
+ },
315
+ disabled: { suffixed: true, decoder: optional(boolWhenDecoder) },
316
+ on: { decoder: optional(onDecoder) },
317
+ props: { decoder: optional(succeed()) }
318
+ });
319
+ const functionWidgetDecoder = new Decoder((json) => {
320
+ const jsonTypeof = typeof json;
321
+ if (jsonTypeof === "function") {
322
+ const fnWidget = json;
323
+ const widget = fnWidget({
324
+ $form: {},
325
+ errors: void 0,
326
+ touched: void 0,
327
+ translate: void 0
328
+ });
329
+ fnWidget.uid = fnWidget.uid || widget.uid || shortUUID();
330
+ fnWidget.type = widget.type;
331
+ fnWidget.path = widget.path;
332
+ return ok(fnWidget);
333
+ } else {
334
+ return err([{ message: `Expected a function, got '${jsonTypeof}'`, path: [] }]);
335
+ }
336
+ });
337
+ const inputWidgetDecoder = objectWithSuffix({
338
+ kind: { decoder: literal("input") },
339
+ uid: { decoder: optional(string()) },
340
+ type: { decoder: string() },
341
+ size: { suffixed: true, decoder: optional(number()) },
342
+ include: { decoder: optional(includeDecoder) },
343
+ exclude: { decoder: optional(excludeDecoder) },
344
+ disabled: { suffixed: true, decoder: optional(boolWhenDecoder) },
345
+ readonly: { suffixed: true, decoder: optional(boolWhenDecoder) },
346
+ on: { decoder: optional(onDecoder) },
347
+ props: { decoder: optional(succeed()) },
348
+ label: {
349
+ suffixed: true,
350
+ decoder: decodeWidgetPropOrWidgetPropFn(optional(localizableDecoder))
351
+ },
352
+ path: { decoder: string() },
353
+ defaultValue: { decoder: optional(succeed()) },
354
+ validator: {
355
+ suffixed: true,
356
+ decoder: decodeWidgetPropOrWidgetPropFn(optional(succeed()))
357
+ }
358
+ }).map((ctrl) => {
359
+ const transformed = { ...ctrl };
360
+ if (!ctrl.uid) {
361
+ transformed.uid = `${ctrl.path}-${ctrl.type}`;
362
+ }
363
+ if (ctrl.type === "repeater") {
364
+ const props = ctrl["props"];
365
+ transformed.props = { ...props, template: layoutWidgetDecoder.parse(props["template"]) };
366
+ }
367
+ return transformed;
368
+ });
369
+ const formWidgetDecoder = lazy(() => {
370
+ const objectWidgetDecoder = discriminatedUnion("kind", {
371
+ input: inputWidgetDecoder,
372
+ layout: layoutWidgetDecoder,
373
+ display: displayWidgetDecoder,
374
+ action: actionWidgetDecoder
375
+ });
376
+ return new Decoder(
377
+ (json) => typeof json === "function" ? functionWidgetDecoder.decode(json) : objectWidgetDecoder.decode(json)
378
+ );
379
+ });
380
+ const layoutWidgetDecoder = objectWithSuffix({
381
+ kind: { decoder: literal("layout") },
382
+ uid: { decoder: uidDecoder },
383
+ type: { decoder: string() },
384
+ size: { suffixed: true, decoder: optional(number()) },
385
+ include: { decoder: optional(includeDecoder) },
386
+ exclude: { decoder: optional(excludeDecoder) },
387
+ props: { decoder: optional(succeed()) },
388
+ on: { decoder: optional(onDecoder) },
389
+ children: { decoder: array(formWidgetDecoder) }
390
+ });
391
+ function assignDeterministicUids(root) {
392
+ assignUidByPosition(root, "#0");
393
+ }
394
+ function assignUidByPosition(widget, positionUid) {
395
+ if (isFunctionWidget(widget)) {
396
+ return;
397
+ }
398
+ if (!widget.uid) {
399
+ widget.uid = positionUid;
400
+ }
401
+ if (isLayoutWidget(widget)) {
402
+ widget.children.forEach((child, index) => {
403
+ assignUidByPosition(child, `${positionUid}.${index}`);
404
+ });
405
+ }
406
+ if (isInputWidget(widget) && widget.type === "repeater") {
407
+ const template = widget.props?.["template"];
408
+ if (template) {
409
+ assignUidByPosition(template, `${positionUid}.t`);
410
+ }
411
+ }
412
+ }
413
+ object({
414
+ states: optional(record(string())),
415
+ form: lazy(() => layoutWidgetDecoder)
416
+ }).map((formDef) => {
417
+ assignDeterministicUids(formDef.form);
418
+ return formDef;
419
+ });
420
+ const formEventNames = {
421
+ submit: "formSubmit"
422
+ };
423
+ pipe(
424
+ map((store) => store.data),
425
+ distinctUntilChanged()
426
+ );
427
+ pipe(
428
+ filter((store) => store.touched === true),
429
+ map((store) => store.validations),
430
+ distinctUntilChanged()
431
+ );
432
+ pipe(
433
+ filter((store) => store.touched === true),
434
+ map((store) => store.injectedValidations),
435
+ distinctUntilChanged()
436
+ );
437
+ pipe(
438
+ map((store) => store.lang),
439
+ distinctUntilChanged()
440
+ );
441
+ pipe(
442
+ map((store) => store.calculatedWidgets),
443
+ distinctUntilChanged()
444
+ );
445
+ pipe(
446
+ map((store) => store.widgetFlags),
447
+ distinctUntilChanged()
448
+ );
449
+ pipe(
450
+ map((store) => store.touchedControls),
451
+ distinctUntilChanged()
452
+ );
453
+ pipe(
454
+ map((store) => store.formHealth),
455
+ distinctUntilChanged((prev, current) => {
456
+ if (prev.status !== current.status) {
457
+ return false;
458
+ }
459
+ if (prev.status === "errored" && current.status === "errored") {
460
+ return prev.message === current.message && prev.code === current.code;
461
+ }
462
+ return true;
463
+ })
464
+ );
216
465
  const DX_FRAMEWORKS = ["react", "angular", "vue", "lit", "vanilla"];
217
466
  function resolveDxFramework() {
218
467
  const raw = typeof process !== "undefined" ? process.env?.["GOLEMUI_FRAMEWORK"]?.toLowerCase() : void 0;
@@ -222,8 +471,8 @@ const FRAMEWORK_SETUP = {
222
471
  react: "RENDER (React) — `import { gui } from '@golemui/gui-shared'; import { GuiForm } from '@golemui/gui-react'; import type { FormSubmitEvent } from '@golemui/core';`, then render `<GuiForm config={{ formDef: form }} formSubmit={(e: FormSubmitEvent) => { /* e.data is the form data */ }} />`. A `gui.displays.display(() => <h2>…</h2>)` returns React JSX.",
223
472
  angular: "RENDER (Angular) — `import { gui } from '@golemui/gui-shared'; import { FormComponent } from '@golemui/gui-angular';`, add `FormComponent` to the standalone component's `imports`, then in the template `<gui-form [config]=\"{ formDef: form }\" (formSubmit)=\"onSubmit($event)\"></gui-form>` — `$event` is a `FormSubmitEvent` (type from `@golemui/core`), `$event.data` is the form data.",
224
473
  vue: "RENDER (Vue) — `import { gui } from '@golemui/gui-shared'; import { GuiForm } from '@golemui/gui-vue';`, then `<GuiForm :config=\"{ formDef: form }\" @form-submit=\"onSubmit\" />` — the handler receives a `FormSubmitEvent` (`.data` is the form data). The event is `form-submit` (kebab-case), not `formSubmit`.",
225
- lit: "RENDER (Lit) — `import { gui } from '@golemui/gui-shared'; import '@golemui/gui-lit';` (registers the `<gui-form>` custom element), then `<gui-form .config=${{ formDef: form }} @form-submit=${(e: CustomEvent) => { /* e.detail is the FormSubmitEvent; e.detail.data */ }}></gui-form>`.",
226
- vanilla: "RENDER (vanilla JS) — `import { gui } from '@golemui/gui-shared'; import '@golemui/gui-lit';` (registers `<gui-form>`), then `const el = document.querySelector('gui-form'); el.config = { formDef: form }; el.addEventListener('form-submit', (e) => { /* e.detail.data */ });`"
474
+ lit: "RENDER (Lit) — `import { gui } from '@golemui/gui-shared'; import '@golemui/gui-lit';` (registers the `<gui-form>` custom element), then `<gui-form .config=${{ formDef: form }} @" + formEventNames.submit + "=${(e: CustomEvent) => { /* e.detail is the FormSubmitEvent; e.detail.data */ }}></gui-form>`. The event name is `" + formEventNames.submit + "` (camelCase) — Lit dispatches a raw CustomEvent, so there is no kebab-case alias.",
475
+ vanilla: "RENDER (vanilla JS) — `import { gui } from '@golemui/gui-shared'; import '@golemui/gui-lit';` (registers `<gui-form>`), then `const el = document.querySelector('gui-form'); el.config = { formDef: form }; el.addEventListener('" + formEventNames.submit + "', (e) => { /* e.detail.data */ });`. In TypeScript, type the element — `import type { FormElement } from '@golemui/gui-lit'; const el = document.querySelector<FormElement>('gui-form');` — the published types do not register `gui-form` in `HTMLElementTagNameMap`, so an untyped `querySelector` yields `Element` and `el.config` fails to compile."
227
476
  };
228
477
  function commonNote(fw = "react") {
229
478
  return "GolemUI builds FORMS — data collection and validation. It is NOT a general-purpose UI toolkit: it never renders documents, page content, or markdown for display. A form is just an array of these items: `export const form = [ /* items */ ];`. " + FRAMEWORK_SETUP[fw] + " Import the component stylesheet ONCE — `@golemui/gui-components/index.css` — or the form renders unstyled. To RECEIVE A SUBMIT: add a `gui.actions.button({ label, actionType: 'submit' })` to the form and listen for the submit on the host component (the RENDER line above shows how for your framework) — the handler gets a `FormSubmitEvent` whose `.data` is the collected form data. To DISABLE submit until the form is valid, add `disabled: { when: '$formIsInvalid' }` to that button (`$formIsInvalid` is a built-in validity flag) — see the conditional-and-state-props pattern. The SAME `formDef` renders in every framework (React/Angular/Vue/Lit/vanilla) — only the host wrapper changes. FORM-LEVEL CONFIG — `formDef` is ALWAYS the bare array. Anything form-wide (named `states`, `validateOn`) goes in a sibling `formConfig` on the config (`config={{ formDef: form, formConfig: { states, validateOn } }}`), NEVER inside `formDef`. Do NOT wrap the array as `{ states, form: [...] }` and pass THAT as `formDef` — `formDef` is typed `Record<string, any>` so it COMPILES, but the `gui.*` items are never resolved and the form renders BLANK with no error. See the form-level-states pattern. Common fields like `include`/`exclude` (conditional visibility) go INSIDE a factory’s config argument — never spread them onto the result (`{ ...gui.inputs.x(...), include }` compiles but silently does nothing). See the conditional-visibility pattern. STATIC CONTENT — a section heading or any non-input text/block is the HOST’s job, not GolemUI’s: use `gui.displays.display(() => <h2>…</h2>)` returning your framework’s own node (React JSX, Vue/Angular/Lit node) — it needs no dependency and always renders. MARKDOWN has exactly ONE use: `gui.inputs.markdown`, an INPUT where the user EDITS markdown (its value is their markdown string). There is NO markdown-for-display widget — never use markdown to render a heading or content; use `display` for that. VALIDATOR `type` — one rule, three cases (so you never have to guess): (1) choice widgets (`dropdown`, `radiogroup`, `select`) REQUIRE an explicit `type`: `validator: { type: 'string', required: true }`. (2) `repeater` (array) validators auto-supply `type: 'array'` — supply only the rules, e.g. `validator: { required: true, minItems: 1 }`, never `type`. (3) everything else (text, number, date) takes the loose validator with NO `type`: `validator: { required: true }`. EVENT HANDLERS — `onChange`/`onLoad`/`onFilter`/`onBlur` (inputs/layouts) and `onClick` (actions) are FUNCTIONS, never bare strings: return a string to dispatch a host event by that name (`onChange: () => 'languageChanged'`), or take the event to push live changes (`onChange: (event) => event.update({ path: 'city', options: [...] })`).";
@@ -264,6 +513,7 @@ const INPUTS = [
264
513
  {
265
514
  factory: "textInput",
266
515
  namespace: "inputs",
516
+ docSlug: "textinput",
267
517
  call: "gui.inputs.textInput(path, { label, placeholder?, defaultValue?, validator? })",
268
518
  example: "gui.inputs.textInput('fullName', { label: 'Full name', validator: { required: true, minLength: 2 } })",
269
519
  notes: [
@@ -274,6 +524,7 @@ const INPUTS = [
274
524
  {
275
525
  factory: "numberInput",
276
526
  namespace: "inputs",
527
+ docSlug: "number",
277
528
  call: "gui.inputs.numberInput(path, { label, defaultValue?, validator? })",
278
529
  example: "gui.inputs.numberInput('age', { label: 'Age', validator: { required: true, minimum: 0, maximum: 120 } })",
279
530
  notes: [
@@ -283,6 +534,7 @@ const INPUTS = [
283
534
  {
284
535
  factory: "booleanInput",
285
536
  namespace: "inputs",
537
+ docSlug: "toggle",
286
538
  call: "gui.inputs.booleanInput(path, { label, defaultValue? })",
287
539
  example: "gui.inputs.booleanInput('newsletter', { label: 'Subscribe to newsletter', defaultValue: false })",
288
540
  notes: ["The on/off toggle for a single boolean. Use `checkbox` for a checkbox presentation."]
@@ -290,6 +542,7 @@ const INPUTS = [
290
542
  {
291
543
  factory: "checkbox",
292
544
  namespace: "inputs",
545
+ docSlug: "checkbox",
293
546
  call: "gui.inputs.checkbox(path, { label, defaultValue? })",
294
547
  example: "gui.inputs.checkbox('terms', { label: 'I accept the terms', defaultValue: false })",
295
548
  notes: ["A single boolean rendered as a checkbox."]
@@ -297,6 +550,7 @@ const INPUTS = [
297
550
  {
298
551
  factory: "textarea",
299
552
  namespace: "inputs",
553
+ docSlug: "textarea",
300
554
  call: "gui.inputs.textarea(path, { label, placeholder?, validator? })",
301
555
  example: "gui.inputs.textarea('bio', { label: 'Bio', validator: { maxLength: 500 } })",
302
556
  notes: ["Multi-line text; same loose string validator as `textInput`."]
@@ -304,6 +558,7 @@ const INPUTS = [
304
558
  {
305
559
  factory: "password",
306
560
  namespace: "inputs",
561
+ docSlug: "password",
307
562
  call: "gui.inputs.password(path, { label, validator? })",
308
563
  example: "gui.inputs.password('password', { label: 'Password', validator: { required: true, minLength: 8 } })",
309
564
  notes: ["Masked text input; loose string validator."]
@@ -311,6 +566,7 @@ const INPUTS = [
311
566
  {
312
567
  factory: "dropdown",
313
568
  namespace: "inputs",
569
+ docSlug: "dropdown",
314
570
  call: "gui.inputs.dropdown(path, { label, items, validator? })",
315
571
  example: "gui.inputs.dropdown('country', { label: 'Country', items: [{ value: 'us', label: 'United States' }, { value: 'ca', label: 'Canada' }], validator: { type: 'string', required: true } })",
316
572
  notes: [
@@ -321,6 +577,7 @@ const INPUTS = [
321
577
  {
322
578
  factory: "radiogroup",
323
579
  namespace: "inputs",
580
+ docSlug: "radiogroup",
324
581
  call: "gui.inputs.radiogroup(path, { label, options, defaultValue?, validator? })",
325
582
  example: "gui.inputs.radiogroup('accountType', { label: 'Account type', defaultValue: 'personal', options: [{ value: 'personal', label: 'Personal' }, { value: 'business', label: 'Business' }] })",
326
583
  notes: [
@@ -331,6 +588,7 @@ const INPUTS = [
331
588
  {
332
589
  factory: "datePicker",
333
590
  namespace: "inputs",
591
+ docSlug: "date-picker",
334
592
  call: "gui.inputs.datePicker(path, { label, minDate?, maxDate?, validator? })",
335
593
  example: "gui.inputs.datePicker('startDate', { label: 'Coverage start', minDate: '2025-01-01', validator: { required: true } })",
336
594
  notes: [
@@ -341,6 +599,7 @@ const INPUTS = [
341
599
  {
342
600
  factory: "currency",
343
601
  namespace: "inputs",
602
+ docSlug: "currency",
344
603
  call: "gui.inputs.currency(path, { label, validator? })",
345
604
  example: "gui.inputs.currency('price', { label: 'Price', validator: { required: true, minimum: 0 } })",
346
605
  notes: ["Numeric money input; number-style validator."]
@@ -348,6 +607,7 @@ const INPUTS = [
348
607
  {
349
608
  factory: "select",
350
609
  namespace: "inputs",
610
+ docSlug: "select",
351
611
  call: "gui.inputs.select(path, { label, options, validator? })",
352
612
  example: "gui.inputs.select('plan', { label: 'Plan', options: [{ value: 'free', label: 'Free' }, { value: 'pro', label: 'Pro' }], validator: { type: 'string', required: true } })",
353
613
  notes: [
@@ -358,6 +618,7 @@ const INPUTS = [
358
618
  {
359
619
  factory: "dateInput",
360
620
  namespace: "inputs",
621
+ docSlug: "dateinput",
361
622
  call: "gui.inputs.dateInput(path, { label, minDate?, maxDate?, validator? })",
362
623
  example: "gui.inputs.dateInput('startDate', { label: 'Start date', validator: { required: true } })",
363
624
  notes: [
@@ -365,9 +626,40 @@ const INPUTS = [
365
626
  "`minDate` / `maxDate` (ISO `YYYY-MM-DD` strings) constrain the accepted range — see `datePicker`."
366
627
  ]
367
628
  },
629
+ {
630
+ factory: "timeInput",
631
+ namespace: "inputs",
632
+ docSlug: "timeinput",
633
+ call: "gui.inputs.timeInput(path, { label, hourFormat?, minuteStep?, validator? })",
634
+ example: "gui.inputs.timeInput('meetingTime', { label: 'Meeting time', minuteStep: 15 })",
635
+ notes: [
636
+ "Typed time entry (hh:mm segments). Emits an ISO time string (`HH:mm:ss`) — pair with the `{ format: 'time' }` validator. `hourFormat` forces '12'/'24' (default: locale); `minuteStep` sets the arrow-key minute increment."
637
+ ]
638
+ },
639
+ {
640
+ factory: "timePicker",
641
+ namespace: "inputs",
642
+ docSlug: "timepicker",
643
+ call: "gui.inputs.timePicker(path, { label, minTime?, maxTime?, minuteStep?, disabledRanges?, allowCustomTime?, validator? })",
644
+ example: "gui.inputs.timePicker('meetingTime', { label: 'Meeting time', minTime: '09:00', maxTime: '18:00', minuteStep: 30 })",
645
+ notes: [
646
+ "Time field with a popover list of slots built from `minTime`..`maxTime` stepping `minuteStep` (default 30). `disabledRanges` (`{ start, end }[]`, inclusive) greys slots out. Typing is off unless `allowCustomTime: true`. Emits `HH:mm:ss` — pair with the `{ format: 'time' }` validator."
647
+ ]
648
+ },
649
+ {
650
+ factory: "dateTimeInput",
651
+ namespace: "inputs",
652
+ docSlug: "datetimeinput",
653
+ call: "gui.inputs.dateTimeInput(path, { label, hourFormat?, minuteStep?, validator? })",
654
+ example: "gui.inputs.dateTimeInput('meetingAt', { label: 'Meeting at' })",
655
+ notes: [
656
+ "Typed date+time entry in one locale-ordered row. Emits a local ISO date-time (`YYYY-MM-DDTHH:mm:ss`) — pair with the `{ format: 'date-time' }` validator. `hourFormat`/`minuteStep` as in `timeInput`."
657
+ ]
658
+ },
368
659
  {
369
660
  factory: "calendar",
370
661
  namespace: "inputs",
662
+ docSlug: "calendar",
371
663
  call: "gui.inputs.calendar(path, { label, minDate?, maxDate? })",
372
664
  example: "gui.inputs.calendar('day', { label: 'Pick a day', minDate: '2025-01-01' })",
373
665
  notes: [
@@ -375,9 +667,34 @@ const INPUTS = [
375
667
  "`minDate` / `maxDate` (ISO `YYYY-MM-DD` strings) constrain the selectable range — see `datePicker`."
376
668
  ]
377
669
  },
670
+ {
671
+ factory: "dateTimeCalendar",
672
+ namespace: "inputs",
673
+ docSlug: "datetimecalendar",
674
+ call: "gui.inputs.dateTimeCalendar(path, { label, minDate?, maxDate?, minTime?, maxTime?, minuteStep?, disabledTimeRanges?, allowCustomTime? })",
675
+ example: "gui.inputs.dateTimeCalendar('appointmentAt', { label: 'Appointment', minTime: '09:00', maxTime: '18:00' })",
676
+ notes: [
677
+ "An INLINE calendar with an embedded time picker: a segmented time input between the header and the days grid opens a time grid in place of the days (like the year selector).",
678
+ "Emits a local ISO date-time (`YYYY-MM-DDTHH:mm:ss`) only when BOTH day and time are selected — pair with a `{ type: 'string', format: 'date-time' }` validator. Picking a different day clears the time and resets the value to null.",
679
+ "`disabledTimeRanges` entries take `start`/`end` ISO times plus optional `date` (ISO date) and/or `weekdays` (getDay() numbering: 0=Sunday … 6=Saturday) to scope the range to specific days."
680
+ ]
681
+ },
682
+ {
683
+ factory: "dateTimePicker",
684
+ namespace: "inputs",
685
+ docSlug: "datetimepicker",
686
+ call: "gui.inputs.dateTimePicker(path, { label, minDate?, maxDate?, minTime?, maxTime?, minuteStep?, disabledTimeRanges?, allowCustomTime? })",
687
+ example: "gui.inputs.dateTimePicker('appointmentAt', { label: 'Appointment', minTime: '09:00', maxTime: '18:00' })",
688
+ notes: [
689
+ "A compact date-time FIELD that opens a `dateTimeCalendar` POPOVER on focus — the space-saving counterpart to the inline `dateTimeCalendar`, like `datePicker` is to `calendar`.",
690
+ "Emits a local ISO date-time (`YYYY-MM-DDTHH:mm:ss`); the popover closes only when BOTH day and time are selected. Pair with a `{ type: 'string', format: 'date-time' }` validator.",
691
+ "Takes the same time props as `dateTimeCalendar` (`minTime`/`maxTime`/`minuteStep`/`disabledTimeRanges` with per-date/weekday scoping/`allowCustomTime`) plus `icon` and `invalidDateMessage` for the typed input."
692
+ ]
693
+ },
378
694
  {
379
695
  factory: "markdown",
380
696
  namespace: "inputs",
697
+ docSlug: "markdown",
381
698
  call: "gui.inputs.markdown(path, { label })",
382
699
  example: "gui.inputs.markdown('notes', { label: 'Notes (markdown)' })",
383
700
  notes: [
@@ -387,6 +704,7 @@ const INPUTS = [
387
704
  {
388
705
  factory: "tags",
389
706
  namespace: "inputs",
707
+ docSlug: "tags",
390
708
  call: "gui.inputs.tags(path, { label })",
391
709
  example: "gui.inputs.tags('skills', { label: 'Skills' })",
392
710
  notes: ["Free-form multi-value tag input; the value is a string array."]
@@ -394,6 +712,7 @@ const INPUTS = [
394
712
  {
395
713
  factory: "repeater",
396
714
  namespace: "inputs",
715
+ docSlug: "repeater",
397
716
  call: "gui.inputs.repeater(path, { label?, addLabel?, removeLabel?, limit?, template })",
398
717
  example: "gui.inputs.repeater('attendees', { label: 'Attendees', addLabel: 'Add attendee', template: [ gui.inputs.textInput('attendees.items.name', { label: 'Name' }) ] })",
399
718
  notes: [
@@ -405,6 +724,7 @@ const INPUTS = [
405
724
  {
406
725
  factory: "list",
407
726
  namespace: "inputs",
727
+ docSlug: "list",
408
728
  call: "gui.inputs.list(path, { label, items, height?, itemHeight? })",
409
729
  example: "gui.inputs.list('selection', { label: 'Pick an option', items: ['Option 1', 'Option 2', 'Option 3'], height: 200, itemHeight: 40 })",
410
730
  notes: [
@@ -415,6 +735,7 @@ const INPUTS = [
415
735
  {
416
736
  factory: "rangeCalendar",
417
737
  namespace: "inputs",
738
+ docSlug: "range-calendar",
418
739
  call: "gui.inputs.rangeCalendar(path, { label? })",
419
740
  example: "gui.inputs.rangeCalendar('stayDates', { label: 'Stay dates' })",
420
741
  notes: [
@@ -424,22 +745,78 @@ const INPUTS = [
424
745
  {
425
746
  factory: "rangeDateInput",
426
747
  namespace: "inputs",
748
+ docSlug: "range-date-input",
427
749
  call: "gui.inputs.rangeDateInput(path, { label? })",
428
750
  example: "gui.inputs.rangeDateInput('stayDates', { label: 'Stay dates' })",
429
751
  notes: ["Typed start–end date **range** entry (the range sibling of `dateInput`)."]
430
752
  },
753
+ {
754
+ factory: "rangeTimeInput",
755
+ namespace: "inputs",
756
+ docSlug: "range-time-input",
757
+ call: "gui.inputs.rangeTimeInput(path, { label?, minTime?, maxTime? })",
758
+ example: "gui.inputs.rangeTimeInput('shift', { label: 'Shift', minTime: '06:00:00', maxTime: '22:00:00' })",
759
+ notes: [
760
+ "Typed start–end time **range** entry (the range sibling of `timeInput`); value is `TimeRange[]`. End time must be after start time."
761
+ ]
762
+ },
763
+ {
764
+ factory: "rangeDateTimeInput",
765
+ namespace: "inputs",
766
+ docSlug: "range-datetime-input",
767
+ call: "gui.inputs.rangeDateTimeInput(path, { label?, minDateTime?, maxDateTime? })",
768
+ example: "gui.inputs.rangeDateTimeInput('window', { label: 'Window', minDateTime: '2026-03-01T06:00:00', maxDateTime: '2026-03-31T22:00:00' })",
769
+ notes: [
770
+ "Typed start–end date-time **range** entry (the range sibling of `dateTimeInput`); value is `DateTimeRange[]`. A backward selection reorders (swaps) instead of erroring.",
771
+ "Each endpoint is an instant, so it is bounded by instants: use **`minDateTime`** / **`maxDateTime`** (ISO `YYYY-MM-DDTHH:mm:ss`), not `minDate`/`maxDate`. There is no `minTime`/`maxTime` here — a per-day time window is a different constraint from an instant bound."
772
+ ]
773
+ },
774
+ {
775
+ factory: "rangeDateTimeCalendar",
776
+ namespace: "inputs",
777
+ docSlug: "range-datetime-calendar",
778
+ call: "gui.inputs.rangeDateTimeCalendar(path, { label?, minDateTime?, maxDateTime?, disabledRanges?, startTimeLabel?, endTimeLabel? })",
779
+ example: "gui.inputs.rangeDateTimeCalendar('stay', { label: 'Stay', startTimeLabel: 'Check-in', endTimeLabel: 'Check-out' })",
780
+ notes: [
781
+ "INLINE range calendar with TWO embedded time pickers (start/end); value is `DateTimeRange[]`, rendered as pills. Pick a date range, then a start time (enables the end time), then an end time to commit a pill. A day holding more than one range shows a count badge.",
782
+ "Everything is in instant-space: bounds are **`minDateTime`** / **`maxDateTime`** and **`disabledRanges`** are `DateTimeRange[]` instant spans (block a whole day with `00:00:00`–`23:59:59`). There is no `minDate`/`maxDate`/`minTime`/`maxTime`/`disabledTimeRanges` — a time-of-day constraint cannot bound a multi-day span."
783
+ ]
784
+ },
785
+ {
786
+ factory: "rangeDateTimePicker",
787
+ namespace: "inputs",
788
+ docSlug: "range-datetime-picker",
789
+ call: "gui.inputs.rangeDateTimePicker(path, { label?, minDateTime?, maxDateTime?, disabledRanges?, startTimeLabel?, endTimeLabel? })",
790
+ example: "gui.inputs.rangeDateTimePicker('stay', { label: 'Stay', startTimeLabel: 'Check-in', endTimeLabel: 'Check-out' })",
791
+ notes: [
792
+ "POPOVER date-time range picker: the typed `rangeDateTimeInput` as the trigger (pills live there) with the `rangeDateTimeCalendar` in a dropdown. Value is `DateTimeRange[]`. Committing a pill keeps the popover open so several ranges can be added; it closes on outside-click, blur or Escape.",
793
+ "Everything is in instant-space: bounds are **`minDateTime`** / **`maxDateTime`** and **`disabledRanges`** are `DateTimeRange[]` instant spans (block a whole day with `00:00:00`–`23:59:59`). There is no `minDate`/`maxDate`/`minTime`/`maxTime`/`disabledTimeRanges`."
794
+ ]
795
+ },
431
796
  {
432
797
  factory: "rangeDatePicker",
433
798
  namespace: "inputs",
799
+ docSlug: "range-date-picker",
434
800
  call: "gui.inputs.rangeDatePicker(path, { label? })",
435
801
  example: "gui.inputs.rangeDatePicker('stayDates', { label: 'Stay dates' })",
436
802
  notes: ["Popover calendar for a start–end date **range** (the range sibling of `datePicker`)."]
803
+ },
804
+ {
805
+ factory: "rangeTimePicker",
806
+ namespace: "inputs",
807
+ docSlug: "range-time-picker",
808
+ call: "gui.inputs.rangeTimePicker(path, { label?, minTime?, maxTime? })",
809
+ example: "gui.inputs.rangeTimePicker('shift', { label: 'Shift', minTime: '06:00:00', maxTime: '22:00:00' })",
810
+ notes: [
811
+ "Two-list popover for a start–end time **range** (the range sibling of `timePicker`); value is `TimeRange[]`. The out list floors one slot after the chosen in so end is strictly after start."
812
+ ]
437
813
  }
438
814
  ];
439
815
  const ACTIONS = [
440
816
  {
441
817
  factory: "button",
442
818
  namespace: "actions",
819
+ docSlug: "button",
443
820
  call: "gui.actions.button({ label, actionType?: 'submit', onClick? })",
444
821
  example: "gui.actions.button({ label: 'Sign up', actionType: 'submit' })",
445
822
  notes: [
@@ -453,6 +830,7 @@ const DISPLAYS = [
453
830
  {
454
831
  factory: "alert",
455
832
  namespace: "displays",
833
+ docSlug: "alert",
456
834
  call: "gui.displays.alert({ text })",
457
835
  example: "gui.displays.alert({ text: 'Please review your details before submitting.' })",
458
836
  notes: [
@@ -462,6 +840,7 @@ const DISPLAYS = [
462
840
  {
463
841
  factory: "display",
464
842
  namespace: "displays",
843
+ docSlug: "renderer",
465
844
  call: "gui.displays.display(render)",
466
845
  example: "gui.displays.display(() => 'Order summary')",
467
846
  notes: [
@@ -474,6 +853,7 @@ const LAYOUTS = [
474
853
  {
475
854
  factory: "flex",
476
855
  namespace: "layouts",
856
+ docSlug: "flex",
477
857
  call: "gui.layouts.flex(children, props?)",
478
858
  example: "gui.layouts.flex([ gui.inputs.textInput('firstName', { label: 'First name' }), gui.inputs.textInput('lastName', { label: 'Last name' }) ])",
479
859
  notes: [
@@ -484,6 +864,7 @@ const LAYOUTS = [
484
864
  {
485
865
  factory: "verticalFlex",
486
866
  namespace: "layouts",
867
+ docSlug: "flex",
487
868
  call: "gui.layouts.verticalFlex(children, props?)",
488
869
  example: "gui.layouts.verticalFlex([ gui.inputs.textInput('a', { label: 'A' }), gui.inputs.textInput('b', { label: 'B' }) ])",
489
870
  notes: ["A `flex` with direction fixed to vertical."]
@@ -491,6 +872,7 @@ const LAYOUTS = [
491
872
  {
492
873
  factory: "horizontalFlex",
493
874
  namespace: "layouts",
875
+ docSlug: "flex",
494
876
  call: "gui.layouts.horizontalFlex(children, props?)",
495
877
  example: "gui.layouts.horizontalFlex([ gui.inputs.textInput('a', { label: 'A' }), gui.inputs.textInput('b', { label: 'B' }) ])",
496
878
  notes: ["A `flex` with direction fixed to horizontal."]
@@ -498,6 +880,7 @@ const LAYOUTS = [
498
880
  {
499
881
  factory: "grid",
500
882
  namespace: "layouts",
883
+ docSlug: "grid",
501
884
  call: "gui.layouts.grid(children, props?)",
502
885
  example: "gui.layouts.grid([ gui.inputs.textInput('a', { label: 'A' }), gui.inputs.textInput('b', { label: 'B' }) ])",
503
886
  notes: ["Grid layout; `horizontalGrid` / `verticalGrid` lock the direction."]
@@ -505,6 +888,7 @@ const LAYOUTS = [
505
888
  {
506
889
  factory: "verticalGrid",
507
890
  namespace: "layouts",
891
+ docSlug: "grid",
508
892
  call: "gui.layouts.verticalGrid(children, props?)",
509
893
  example: "gui.layouts.verticalGrid([ gui.inputs.textInput('a', { label: 'A' }), gui.inputs.textInput('b', { label: 'B' }) ])",
510
894
  notes: ["A `grid` with direction fixed to vertical."]
@@ -512,6 +896,7 @@ const LAYOUTS = [
512
896
  {
513
897
  factory: "horizontalGrid",
514
898
  namespace: "layouts",
899
+ docSlug: "grid",
515
900
  call: "gui.layouts.horizontalGrid(children, props?)",
516
901
  example: "gui.layouts.horizontalGrid([ gui.inputs.textInput('a', { label: 'A' }), gui.inputs.textInput('b', { label: 'B' }) ])",
517
902
  notes: ["A `grid` with direction fixed to horizontal."]
@@ -519,6 +904,7 @@ const LAYOUTS = [
519
904
  {
520
905
  factory: "tabs",
521
906
  namespace: "layouts",
907
+ docSlug: "tabs",
522
908
  call: "gui.layouts.tabs(sections)",
523
909
  example: "gui.layouts.tabs([ { label: 'Account', children: [ gui.inputs.textInput('email', { label: 'Email' }) ] }, { label: 'Profile', children: [ gui.inputs.textInput('name', { label: 'Name' }) ] } ])",
524
910
  notes: [
@@ -528,6 +914,7 @@ const LAYOUTS = [
528
914
  {
529
915
  factory: "accordion",
530
916
  namespace: "layouts",
917
+ docSlug: "accordion",
531
918
  call: "gui.layouts.accordion(sections)",
532
919
  example: "gui.layouts.accordion([ { label: 'Billing', children: [ gui.inputs.textInput('card', { label: 'Card' }) ] } ])",
533
920
  notes: [