@keemakr/ui-sdk 1.6.0-dev-20261027223028 → 1.6.0-dev-20261027224017

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/form.d.ts CHANGED
@@ -18,6 +18,51 @@ export declare function localInputToIso(local: string): string;
18
18
  export declare function initialDraft(field: FormFieldLike): FormDraft;
19
19
  /** What the module receives for a field. See the form schema for each kind. */
20
20
  export declare function postedValue(field: Pick<FormFieldLike, 'kind'>, draft: FormDraft): string | number | boolean | string[] | null;
21
+ /** When a field is on show: a test of what another field of the form posts (schema `showWhen`). */
22
+ export interface ShowWhen {
23
+ field: string;
24
+ includes?: string;
25
+ notIncludes?: string;
26
+ equals?: string | number | boolean;
27
+ }
28
+ /** What a form field needs for the show and required rules. */
29
+ export interface FormFieldRule {
30
+ name: string;
31
+ label: string;
32
+ required?: boolean;
33
+ showWhen?: ShowWhen;
34
+ }
35
+ /** The form as it stands: what it posts, what it hides, the shown required fields left empty. */
36
+ export interface FormState {
37
+ input: Record<string, unknown>;
38
+ hidden: ReadonlySet<string>;
39
+ missing: FormFieldRule[];
40
+ }
41
+ /**
42
+ * Apply the show and required rules to what the form posts, keyed by field name: a hidden
43
+ * field's value is left out and it is never missing; a shown field is missing when it is required
44
+ * and empty. Keys that are not fields (a send's static `input`) pass through.
45
+ */
46
+ export declare function formState(fields: readonly FormFieldRule[], posted: Readonly<Record<string, unknown>>): FormState;
47
+ export type FormInputCheck = {
48
+ ok: true;
49
+ input: Record<string, unknown>;
50
+ } | {
51
+ ok: false;
52
+ issue: string;
53
+ };
54
+ /**
55
+ * What an action received from a form, checked against the form as the module drew it: the value
56
+ * of a field `showWhen` hides is dropped, and a shown required field that is empty is refused.
57
+ * Run it in the action. A client can post anything, and a client that draws no `showWhen` shows
58
+ * every field as optional. Pass `validate: false` for a send that skips the required check (a
59
+ * "Save draft"); hidden values are still dropped.
60
+ */
61
+ export declare function checkFormInput(form: {
62
+ fields: readonly FormFieldRule[];
63
+ }, input: Readonly<Record<string, unknown>>, options?: {
64
+ validate?: boolean;
65
+ }): FormInputCheck;
21
66
  /**
22
67
  * The options of a `searchable` multiselect in drawing order: the `pinned` ones first, always on
23
68
  * show, then the rest whose label holds the query (any case). Pass the values that were ticked when
@@ -48,16 +93,21 @@ interface SendLike {
48
93
  action: string;
49
94
  confirm?: string;
50
95
  disabled?: boolean;
96
+ disabledUntilValid?: boolean;
51
97
  disabledReason?: string;
52
98
  }
53
- /** Every send button of a form in drawing order: `submit` first, then `secondary`. */
99
+ /**
100
+ * Every send button of a form in drawing order: `submit` first, then `secondary`. `valid` says
101
+ * whether every shown required field is filled (`formState(...).missing` is empty); a send with
102
+ * `disabledUntilValid` is disabled while it is not.
103
+ */
54
104
  export declare function formSubmits(form: {
55
105
  submit: SendLike;
56
106
  secondary?: ReadonlyArray<SendLike & {
57
107
  input?: Record<string, unknown>;
58
108
  validate?: boolean;
59
109
  }>;
60
- }): FormSubmit[];
110
+ }, valid?: boolean): FormSubmit[];
61
111
  /**
62
112
  * The send a submit of the form presses, or null when it must not post: the button the reader
63
113
  * pressed (`at`, its index in `formSubmits`), or the primary for Enter in a field. A disabled send
package/dist/form.js CHANGED
@@ -51,6 +51,54 @@ export function postedValue(field, draft) {
51
51
  return typeof draft === 'string' ? draft : '';
52
52
  }
53
53
  }
54
+ /** True when the field is on show, given what the form posts, keyed by field name. */
55
+ function fieldShown(field, posted) {
56
+ const when = field.showWhen;
57
+ if (!when)
58
+ return true;
59
+ const value = posted[when.field];
60
+ const list = Array.isArray(value) ? value : [];
61
+ if (when.includes !== undefined)
62
+ return list.includes(when.includes);
63
+ if (when.notIncludes !== undefined)
64
+ return !list.includes(when.notIncludes);
65
+ return value === when.equals;
66
+ }
67
+ /** Empty, as a required field may not be: nothing, blank text, no items, an unticked checkbox. */
68
+ function isEmpty(value) {
69
+ return (value === undefined ||
70
+ value === null ||
71
+ value === false ||
72
+ (typeof value === 'string' && value.trim() === '') ||
73
+ (Array.isArray(value) && value.length === 0));
74
+ }
75
+ /**
76
+ * Apply the show and required rules to what the form posts, keyed by field name: a hidden
77
+ * field's value is left out and it is never missing; a shown field is missing when it is required
78
+ * and empty. Keys that are not fields (a send's static `input`) pass through.
79
+ */
80
+ export function formState(fields, posted) {
81
+ const hidden = new Set(fields.filter((f) => !fieldShown(f, posted)).map((f) => f.name));
82
+ return {
83
+ input: Object.fromEntries(Object.entries(posted).filter(([name]) => !hidden.has(name))),
84
+ hidden,
85
+ missing: fields.filter((f) => f.required && !hidden.has(f.name) && isEmpty(posted[f.name])),
86
+ };
87
+ }
88
+ /**
89
+ * What an action received from a form, checked against the form as the module drew it: the value
90
+ * of a field `showWhen` hides is dropped, and a shown required field that is empty is refused.
91
+ * Run it in the action. A client can post anything, and a client that draws no `showWhen` shows
92
+ * every field as optional. Pass `validate: false` for a send that skips the required check (a
93
+ * "Save draft"); hidden values are still dropped.
94
+ */
95
+ export function checkFormInput(form, input, options = {}) {
96
+ const state = formState(form.fields, input);
97
+ const missing = options.validate === false ? undefined : state.missing[0];
98
+ return missing
99
+ ? { ok: false, issue: `${missing.label} is required` }
100
+ : { ok: true, input: state.input };
101
+ }
54
102
  /**
55
103
  * The options of a `searchable` multiselect in drawing order: the `pinned` ones first, always on
56
104
  * show, then the rest whose label holds the query (any case). Pass the values that were ticked when
@@ -63,14 +111,17 @@ export function searchableOptions(options, pinned, query) {
63
111
  const rest = options.filter((o) => !pinned.includes(o.value) && o.label.toLocaleLowerCase().includes(needle));
64
112
  return [...top, ...rest];
65
113
  }
66
- /** Every send button of a form in drawing order: `submit` first, then `secondary`. */
67
- export function formSubmits(form) {
114
+ /**
115
+ * Every send button of a form in drawing order: `submit` first, then `secondary`. `valid` says
116
+ * whether every shown required field is filled (`formState(...).missing` is empty); a send with
117
+ * `disabledUntilValid` is disabled while it is not.
118
+ */
119
+ export function formSubmits(form, valid = true) {
68
120
  // A reason is only for a disabled send, as on a button: an enabled one says nothing.
69
- const state = ({ disabled, disabledReason, ...rest }) => ({
70
- ...rest,
71
- disabled: disabled === true,
72
- ...(disabled && disabledReason ? { disabledReason } : {}),
73
- });
121
+ const state = ({ disabled, disabledUntilValid, disabledReason, ...rest }) => {
122
+ const off = disabled === true || (disabledUntilValid === true && !valid);
123
+ return { ...rest, disabled: off, ...(off && disabledReason ? { disabledReason } : {}) };
124
+ };
74
125
  return [
75
126
  { ...state(form.submit), validate: true, primary: true },
76
127
  ...(form.secondary ?? []).map(({ validate, ...more }) => ({
package/dist/index.d.ts CHANGED
@@ -21,8 +21,8 @@ export { PLAYBACK_RATES, SEEK_AT_MAX, formatPlayhead, isSeekPress, nextPlaybackR
21
21
  export type { PlaybackRate, PlayerKey, SeekReference } from './player.js';
22
22
  export { heatmapCellText, heatmapIntensity, heatmapMixPercent, heatmapRange, heatmapStrong, } from './heatmap.js';
23
23
  export type { HeatmapLike, HeatmapRange } from './heatmap.js';
24
- export { chosenSubmit, formPress, formSubmits, initialDraft, isoToLocalInput, localInputToIso, postedValue, searchableOptions, } from './form.js';
25
- export type { FormDraft, FormFieldKind, FormFieldLike, FormSubmit } from './form.js';
24
+ export { checkFormInput, chosenSubmit, formPress, formState, formSubmits, initialDraft, isoToLocalInput, localInputToIso, postedValue, searchableOptions, } from './form.js';
25
+ export type { FormDraft, FormFieldKind, FormFieldLike, FormFieldRule, FormInputCheck, FormState, FormSubmit, ShowWhen, } from './form.js';
26
26
  export { stackedBarParts, stackedBarValue } from './stacked-bar.js';
27
27
  export type { StackedBarLike, StackedBarParts, StackedBarSegmentPart } from './stacked-bar.js';
28
28
  export { statTrend } from './stat.js';
package/dist/index.js CHANGED
@@ -21,7 +21,7 @@ export { PUBLISHED_PARSERS } from './published/index.js';
21
21
  export { MEDIA_CONTENT_TYPES, mediaContentType, mediaPathOf } from './media.js';
22
22
  export { PLAYBACK_RATES, SEEK_AT_MAX, formatPlayhead, isSeekPress, nextPlaybackRate, playbackRateLabel, playheadWords, seekTarget, seeksIn, } from './player.js';
23
23
  export { heatmapCellText, heatmapIntensity, heatmapMixPercent, heatmapRange, heatmapStrong, } from './heatmap.js';
24
- export { chosenSubmit, formPress, formSubmits, initialDraft, isoToLocalInput, localInputToIso, postedValue, searchableOptions, } from './form.js';
24
+ export { checkFormInput, chosenSubmit, formPress, formState, formSubmits, initialDraft, isoToLocalInput, localInputToIso, postedValue, searchableOptions, } from './form.js';
25
25
  export { stackedBarParts, stackedBarValue } from './stacked-bar.js';
26
26
  export { statTrend } from './stat.js';
27
27
  export { createSectionMemory, sectionBadge, sectionCollapsible, sectionColumnWeights, sectionGridTemplate, sectionScrollHeight, } from './section.js';
@@ -79,8 +79,15 @@ import type { ChatPress, ScreenRootNode, ScreenTree } from './schema.js';
79
79
  * (newer than any published release; core's own renderer draws it). An older parse refuses it,
80
80
  * so without it the meter becomes a `text` cell with its value label ("Intake 3/5"). Header
81
81
  * facts a client draws as one line (no `header.meta`) are written the same way.
82
+ * - `form.show_when`: a form field's `showWhen`, drawn only while another field holds a value
83
+ * and required only then (newer than any published release; core's own renderer draws it).
84
+ * An older parse strips it, so the field would be drawn always and keep its `required`,
85
+ * blocking every post where it should be hidden. Without it, the field is drawn always and
86
+ * is optional: a safe superset, and the module's `checkFormInput` still refuses an empty
87
+ * shown required field. A send's `disabledUntilValid` needs no fold: an older parse strips it,
88
+ * and the send stays pressable with the browser's required check in front of it.
82
89
  */
83
- export type UiFeature = 'breadcrumbs' | 'button.disabled' | 'section.row' | 'form.field_kinds' | 'form.confirm' | 'press.chat' | 'form.radio' | 'table.cells' | 'header.meta' | 'tabs.many' | 'choice.action' | 'markdown.links' | 'form.disabled' | 'press.seek' | 'edit.inline' | 'timeline.time_tag' | 'stat.bar' | 'section.action' | 'facts.meter';
90
+ export type UiFeature = 'breadcrumbs' | 'button.disabled' | 'section.row' | 'form.field_kinds' | 'form.confirm' | 'press.chat' | 'form.radio' | 'table.cells' | 'header.meta' | 'tabs.many' | 'choice.action' | 'markdown.links' | 'form.disabled' | 'press.seek' | 'edit.inline' | 'timeline.time_tag' | 'stat.bar' | 'section.action' | 'facts.meter' | 'form.show_when';
84
91
  /** The node types and features one ui-sdk release, or one client, draws. */
85
92
  export interface NodeSet {
86
93
  /** For messages: "ui-sdk 1.4.0", "mobile 0.2.5". */
@@ -117,7 +124,7 @@ export declare const UI_SDK_SNAPSHOTS: {
117
124
  readonly current: {
118
125
  readonly name: "ui-sdk (this package)";
119
126
  readonly nodes: readonly ["heading", "text", "markdown", "stat", "button", "form", "table", "timeline", "pending_request", "pending_requests", "choice_cards", "progress", "chart", "section", "tabs", "pagination", "select", "alert", "details", "timestamp", "divider", "board", "link", "tags", "toggle", "segmented", "progress_bar", "conversation", "composer", "calendar", "image", "video", "stacked_bar", "heatmap", "audio", "document"];
120
- readonly features: readonly ["breadcrumbs", "button.disabled", "section.row", "form.field_kinds", "form.confirm", "press.chat", "form.radio", "table.cells", "header.meta", "tabs.many", "choice.action", "markdown.links", "form.disabled", "press.seek", "edit.inline", "timeline.time_tag", "stat.bar", "section.action", "facts.meter"];
127
+ readonly features: readonly ["breadcrumbs", "button.disabled", "section.row", "form.field_kinds", "form.confirm", "press.chat", "form.radio", "table.cells", "header.meta", "tabs.many", "choice.action", "markdown.links", "form.disabled", "press.seek", "edit.inline", "timeline.time_tag", "stat.bar", "section.action", "facts.meter", "form.show_when"];
121
128
  };
122
129
  };
123
130
  export type UiSdkSnapshot = keyof typeof UI_SDK_SNAPSHOTS;
package/dist/negotiate.js CHANGED
@@ -53,6 +53,7 @@ const ALL_FEATURES = [
53
53
  'stat.bar',
54
54
  'section.action',
55
55
  'facts.meter',
56
+ 'form.show_when',
56
57
  ];
57
58
  /** The nodes of ui-sdk 1.1.0 and 1.2.0 (one schema). */
58
59
  const NODES_1_1_0 = [
@@ -360,7 +361,8 @@ export function negotiateTree(tree, set) {
360
361
  return form;
361
362
  const kinds = features.has('form.field_kinds');
362
363
  const radio = features.has('form.radio');
363
- if (kinds && radio)
364
+ const showWhen = features.has('form.show_when');
365
+ if (kinds && radio && showWhen)
364
366
  return form;
365
367
  let changed = false;
366
368
  const fields = form.fields.map((field, index) => {
@@ -382,6 +384,11 @@ export function negotiateTree(tree, set) {
382
384
  next = foldFormField(next);
383
385
  fold('form.field_kinds');
384
386
  }
387
+ if (!showWhen && next.showWhen) {
388
+ const { showWhen: _showWhen, ...always } = next;
389
+ next = { ...always, required: false };
390
+ fold('form.show_when');
391
+ }
385
392
  return next;
386
393
  });
387
394
  return changed ? { ...form, fields } : form;
@@ -119,8 +119,8 @@ declare const leafNodeSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
119
119
  list: "list";
120
120
  select: "select";
121
121
  checkbox: "checkbox";
122
- textarea: "textarea";
123
122
  multiselect: "multiselect";
123
+ textarea: "textarea";
124
124
  combobox: "combobox";
125
125
  }>;
126
126
  name: z.ZodString;
package/dist/schema.d.ts CHANGED
@@ -574,8 +574,8 @@ declare const leafNodeSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
574
574
  list: "list";
575
575
  select: "select";
576
576
  checkbox: "checkbox";
577
- textarea: "textarea";
578
577
  multiselect: "multiselect";
578
+ textarea: "textarea";
579
579
  combobox: "combobox";
580
580
  radio: "radio";
581
581
  }>;
@@ -597,6 +597,12 @@ declare const leafNodeSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
597
597
  min: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>;
598
598
  max: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>;
599
599
  step: z.ZodOptional<z.ZodNumber>;
600
+ showWhen: z.ZodOptional<z.ZodObject<{
601
+ field: z.ZodString;
602
+ includes: z.ZodOptional<z.ZodString>;
603
+ notIncludes: z.ZodOptional<z.ZodString>;
604
+ equals: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
605
+ }, z.core.$strip>>;
600
606
  }, z.core.$strip>>;
601
607
  layout: z.ZodOptional<z.ZodEnum<{
602
608
  stack: "stack";
@@ -607,6 +613,7 @@ declare const leafNodeSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
607
613
  action: z.ZodString;
608
614
  confirm: z.ZodOptional<z.ZodString>;
609
615
  disabled: z.ZodOptional<z.ZodBoolean>;
616
+ disabledUntilValid: z.ZodOptional<z.ZodBoolean>;
610
617
  disabledReason: z.ZodOptional<z.ZodString>;
611
618
  }, z.core.$strip>;
612
619
  secondary: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -616,6 +623,7 @@ declare const leafNodeSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
616
623
  confirm: z.ZodOptional<z.ZodString>;
617
624
  validate: z.ZodOptional<z.ZodBoolean>;
618
625
  disabled: z.ZodOptional<z.ZodBoolean>;
626
+ disabledUntilValid: z.ZodOptional<z.ZodBoolean>;
619
627
  disabledReason: z.ZodOptional<z.ZodString>;
620
628
  }, z.core.$strip>>>;
621
629
  onChange: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
package/dist/schema.js CHANGED
@@ -1618,6 +1618,29 @@ const statSchema = z
1618
1618
  });
1619
1619
  }
1620
1620
  });
1621
+ /**
1622
+ * What is wrong with a form field's `showWhen`, or null: it names another field of the form that
1623
+ * has no `showWhen` of its own (no chains, so no loops), with exactly one test, and the test fits
1624
+ * that field's kind.
1625
+ */
1626
+ function showWhenIssue(name, when, fields) {
1627
+ const target = fields.find((field) => field.name === when.field);
1628
+ if (!target || target.name === name)
1629
+ return `showWhen names no other field "${when.field}"`;
1630
+ if (target.showWhen)
1631
+ return `showWhen names "${when.field}", which has a showWhen of its own`;
1632
+ const tests = [when.includes, when.notIncludes, when.equals].filter((t) => t !== undefined);
1633
+ if (tests.length !== 1)
1634
+ return 'showWhen takes exactly one of includes, notIncludes, equals';
1635
+ const many = target.kind === 'multiselect' || target.kind === 'list';
1636
+ if (many && when.equals !== undefined) {
1637
+ return `a ${target.kind} is tested with includes or notIncludes`;
1638
+ }
1639
+ if (!many && when.equals === undefined) {
1640
+ return `includes and notIncludes test a multiselect or list, not a ${target.kind}`;
1641
+ }
1642
+ return null;
1643
+ }
1621
1644
  /** A real YYYY-MM-DD calendar day: `2026-02-30` is refused. */
1622
1645
  function isCalendarDate(text) {
1623
1646
  if (!/^\d{4}-\d{2}-\d{2}$/.test(text))
@@ -1732,6 +1755,26 @@ const leafNodeSchema = z.discriminatedUnion('type', [
1732
1755
  max: z.union([z.number().finite(), z.string().max(40)]).optional(),
1733
1756
  /** The step of a number field. */
1734
1757
  step: z.number().finite().positive().optional(),
1758
+ /**
1759
+ * Show the field only while another field of the form holds a value: a "Why not?"
1760
+ * box while a suggested form is unticked. `includes` and `notIncludes` test a
1761
+ * multiselect or list for one value; `equals` tests any other kind against what it
1762
+ * posts (a string, a number, or true or false for a checkbox). Exactly one of the
1763
+ * three. The other field may not have a `showWhen` of its own.
1764
+ *
1765
+ * A hidden field is not drawn, not posted and not required; a shown field keeps its
1766
+ * own `required`. Check the posted input in the action with `checkFormInput`: a
1767
+ * client can post anything, and a client that draws no `showWhen` shows the field
1768
+ * always, as optional.
1769
+ */
1770
+ showWhen: z
1771
+ .object({
1772
+ field: slug,
1773
+ includes: z.string().max(500).optional(),
1774
+ notIncludes: z.string().max(500).optional(),
1775
+ equals: z.union([z.string().max(500), z.number().finite(), z.boolean()]).optional(),
1776
+ })
1777
+ .optional(),
1735
1778
  }))
1736
1779
  .max(30),
1737
1780
  /**
@@ -1753,7 +1796,16 @@ const leafNodeSchema = z.discriminatedUnion('type', [
1753
1796
  * still refuse what it is not ready for. Default false.
1754
1797
  */
1755
1798
  disabled: z.boolean().optional(),
1756
- /** Why it is disabled, tied to it and read on focus, as a button's is. Ignored when enabled. */
1799
+ /**
1800
+ * Disabled, as `disabled` is, while any shown required field is empty: "Send forms"
1801
+ * until every unticked suggested form has a reason. A client that does not know it draws
1802
+ * the send pressable, and the browser's required check still stops the post.
1803
+ */
1804
+ disabledUntilValid: z.boolean().optional(),
1805
+ /**
1806
+ * Why it is disabled, tied to it and read on focus, as a button's is. Shown while
1807
+ * `disabled`, or while `disabledUntilValid` holds the send back; ignored otherwise.
1808
+ */
1757
1809
  disabledReason: z.string().min(1).max(200).optional(),
1758
1810
  }),
1759
1811
  /**
@@ -1780,7 +1832,9 @@ const leafNodeSchema = z.discriminatedUnion('type', [
1780
1832
  validate: z.boolean().optional(),
1781
1833
  /** Shown but not pressable, as `submit.disabled` is. Default false. */
1782
1834
  disabled: z.boolean().optional(),
1783
- /** Why it is disabled, as `submit.disabledReason` is. Ignored when enabled. */
1835
+ /** As `submit.disabledUntilValid`. Not with `validate: false`. */
1836
+ disabledUntilValid: z.boolean().optional(),
1837
+ /** Why it is disabled, as `submit.disabledReason` is. */
1784
1838
  disabledReason: z.string().min(1).max(200).optional(),
1785
1839
  }))
1786
1840
  .max(3)
@@ -1836,6 +1890,13 @@ const leafNodeSchema = z.discriminatedUnion('type', [
1836
1890
  }
1837
1891
  const labels = new Set([node.submit.label]);
1838
1892
  for (const [i, more] of (node.secondary ?? []).entries()) {
1893
+ if (more.disabledUntilValid && more.validate === false) {
1894
+ ctx.addIssue({
1895
+ code: 'custom',
1896
+ path: ['secondary', i, 'disabledUntilValid'],
1897
+ message: 'a send with validate: false cannot wait for the form to be valid',
1898
+ });
1899
+ }
1839
1900
  if (labels.has(more.label)) {
1840
1901
  ctx.addIssue({
1841
1902
  code: 'custom',
@@ -1856,6 +1917,11 @@ const leafNodeSchema = z.discriminatedUnion('type', [
1856
1917
  });
1857
1918
  }
1858
1919
  names.add(field.name);
1920
+ if (field.showWhen) {
1921
+ const issue = showWhenIssue(field.name, field.showWhen, node.fields);
1922
+ if (issue)
1923
+ ctx.addIssue({ code: 'custom', path: at('showWhen'), message: issue });
1924
+ }
1859
1925
  const needsOptions = ['select', 'multiselect', 'combobox', 'radio'].includes(field.kind);
1860
1926
  if (needsOptions && !field.options?.length) {
1861
1927
  ctx.addIssue({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keemakr/ui-sdk",
3
- "version": "1.6.0-dev-20261027223028",
3
+ "version": "1.6.0-dev-20261027224017",
4
4
  "description": "Product screens for keemakr marketplace modules: a versioned, server-driven UI tree schema plus defineScreen() handlers. Module code never ships to the browser — keemakr-core validates the tree and renders it with platform components.",
5
5
  "license": "MIT",
6
6
  "type": "module",