@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 +52 -2
- package/dist/form.js +58 -7
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/negotiate.d.ts +9 -2
- package/dist/negotiate.js +8 -1
- package/dist/published/v1.4.0/schema.d.ts +1 -1
- package/dist/schema.d.ts +9 -1
- package/dist/schema.js +68 -2
- package/package.json +1 -1
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
|
-
/**
|
|
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
|
-
/**
|
|
67
|
-
|
|
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
|
-
|
|
71
|
-
disabled:
|
|
72
|
-
|
|
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';
|
package/dist/negotiate.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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-
|
|
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",
|