@tai42/studio-sdk 21.0.0 → 22.0.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.
Files changed (62) hide show
  1. package/dist/components/chip-group.d.ts +42 -0
  2. package/dist/components/chip-group.d.ts.map +1 -0
  3. package/dist/components/chip-group.js +85 -0
  4. package/dist/components/chip-group.js.map +1 -0
  5. package/dist/components/components.css +9 -3
  6. package/dist/index.d.ts +4 -2
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/schema-form/SchemaForm.d.ts +11 -1
  11. package/dist/schema-form/SchemaForm.d.ts.map +1 -1
  12. package/dist/schema-form/SchemaForm.js +2 -2
  13. package/dist/schema-form/SchemaForm.js.map +1 -1
  14. package/dist/schema-form/classify.d.ts.map +1 -1
  15. package/dist/schema-form/classify.js +54 -2
  16. package/dist/schema-form/classify.js.map +1 -1
  17. package/dist/schema-form/date-range-field.d.ts +44 -0
  18. package/dist/schema-form/date-range-field.d.ts.map +1 -0
  19. package/dist/schema-form/date-range-field.js +72 -0
  20. package/dist/schema-form/date-range-field.js.map +1 -0
  21. package/dist/schema-form/enum-field.d.ts +3 -1
  22. package/dist/schema-form/enum-field.d.ts.map +1 -1
  23. package/dist/schema-form/enum-field.js +8 -2
  24. package/dist/schema-form/enum-field.js.map +1 -1
  25. package/dist/schema-form/field-model.d.ts +20 -1
  26. package/dist/schema-form/field-model.d.ts.map +1 -1
  27. package/dist/schema-form/field-node.d.ts +10 -0
  28. package/dist/schema-form/field-node.d.ts.map +1 -1
  29. package/dist/schema-form/field-node.js +6 -3
  30. package/dist/schema-form/field-node.js.map +1 -1
  31. package/dist/schema-form/index.d.ts +3 -1
  32. package/dist/schema-form/index.d.ts.map +1 -1
  33. package/dist/schema-form/index.js +1 -0
  34. package/dist/schema-form/index.js.map +1 -1
  35. package/dist/schema-form/multiselect-field.d.ts.map +1 -1
  36. package/dist/schema-form/multiselect-field.js +11 -0
  37. package/dist/schema-form/multiselect-field.js.map +1 -1
  38. package/dist/schema-form/object-fields.d.ts +9 -1
  39. package/dist/schema-form/object-fields.d.ts.map +1 -1
  40. package/dist/schema-form/object-fields.js +16 -3
  41. package/dist/schema-form/object-fields.js.map +1 -1
  42. package/dist/schema-form/string-field.d.ts +6 -1
  43. package/dist/schema-form/string-field.d.ts.map +1 -1
  44. package/dist/schema-form/string-field.js +42 -8
  45. package/dist/schema-form/string-field.js.map +1 -1
  46. package/dist/schema-form/types.d.ts +36 -0
  47. package/dist/schema-form/types.d.ts.map +1 -1
  48. package/package.json +2 -2
  49. package/src/components/chip-group.tsx +157 -0
  50. package/src/components/components.css +9 -3
  51. package/src/index.ts +6 -0
  52. package/src/schema-form/SchemaForm.tsx +10 -0
  53. package/src/schema-form/classify.ts +59 -3
  54. package/src/schema-form/date-range-field.tsx +167 -0
  55. package/src/schema-form/enum-field.tsx +20 -0
  56. package/src/schema-form/field-model.ts +24 -2
  57. package/src/schema-form/field-node.tsx +19 -0
  58. package/src/schema-form/index.ts +10 -1
  59. package/src/schema-form/multiselect-field.tsx +21 -0
  60. package/src/schema-form/object-fields.tsx +36 -2
  61. package/src/schema-form/string-field.tsx +72 -1
  62. package/src/schema-form/types.ts +38 -0
@@ -41,6 +41,14 @@ interface FieldNodeProps {
41
41
  readonly markRequired?: boolean;
42
42
  readonly errors: SchemaFormErrors | undefined;
43
43
  readonly idPrefix: string;
44
+ /**
45
+ * Per-field picker placeholders keyed by property name, handed to an OBJECT node so
46
+ * its direct `enum`-select children draw their placeholder. Carried by the root form
47
+ * only; nested objects receive none.
48
+ */
49
+ readonly fieldWording?: Readonly<Record<string, { choiceLabel?: string }>>;
50
+ /** This field's own picker placeholder, when it draws as an `enum` select. */
51
+ readonly choiceLabel?: string;
44
52
  }
45
53
 
46
54
  function labelText(explicit: string | undefined, label: string | undefined): string {
@@ -73,6 +81,8 @@ function FieldNodeImpl(props: FieldNodeProps): ReactNode {
73
81
  required,
74
82
  errors,
75
83
  idPrefix,
84
+ fieldWording: props.fieldWording,
85
+ choiceLabel: props.choiceLabel,
76
86
  });
77
87
  }
78
88
 
@@ -90,6 +100,8 @@ interface RenderFieldArgs {
90
100
  readonly required: boolean;
91
101
  readonly errors: SchemaFormErrors | undefined;
92
102
  readonly idPrefix: string;
103
+ readonly fieldWording?: Readonly<Record<string, { choiceLabel?: string }>>;
104
+ readonly choiceLabel?: string;
93
105
  }
94
106
 
95
107
  /** Render the field component matching the classified model kind. */
@@ -126,6 +138,7 @@ function renderField(args: RenderFieldArgs): ReactNode {
126
138
  model={model}
127
139
  value={value}
128
140
  onChange={onChange}
141
+ choiceLabel={args.choiceLabel}
129
142
  />
130
143
  );
131
144
  case 'string':
@@ -135,6 +148,10 @@ function renderField(args: RenderFieldArgs): ReactNode {
135
148
  description={description}
136
149
  error={error}
137
150
  format={model.format}
151
+ inputMode={model.inputMode}
152
+ multiline={model.multiline}
153
+ maxLength={model.maxLength}
154
+ minLength={model.minLength}
138
155
  media={model.media}
139
156
  expression={model.expression}
140
157
  date={model.date}
@@ -187,6 +204,7 @@ function renderCompositeField(
187
204
  model: Extract<FieldModel, { kind: 'array' | 'object' | 'record' | 'union' }>,
188
205
  ): ReactNode {
189
206
  const { heading, description, error, root, value, onChange, path, errors, idPrefix } = args;
207
+ const { fieldWording } = args;
190
208
  switch (model.kind) {
191
209
  case 'array':
192
210
  return (
@@ -216,6 +234,7 @@ function renderCompositeField(
216
234
  errors={errors}
217
235
  idPrefix={idPrefix}
218
236
  skip={undefined}
237
+ fieldWording={fieldWording}
219
238
  />
220
239
  </FieldGroup>
221
240
  );
@@ -38,12 +38,21 @@ export type {
38
38
  } from './context';
39
39
  export { RecordEntryRendererContext } from './context';
40
40
  export { ExpressionFieldContext } from './context';
41
+ export type { DateRangePair } from './date-range-field';
42
+ export { dateRangePairs } from './date-range-field';
41
43
  export { defaultValueForSchema } from './default-value';
42
44
  export { resolveRef } from './resolve';
43
45
  export type { CompletionProvider, SchemaFormProps } from './SchemaForm';
44
46
  export { SchemaForm } from './SchemaForm';
45
47
  export type { SecretRef, SecretRefFieldProps } from './SecretRefField';
46
48
  export { SecretRefField } from './SecretRefField';
47
- export type { Discriminator, JsonSchema, JsonSchemaType, SchemaFormErrors } from './types';
49
+ export type {
50
+ Discriminator,
51
+ FormControl,
52
+ FormInputMode,
53
+ JsonSchema,
54
+ JsonSchemaType,
55
+ SchemaFormErrors,
56
+ } from './types';
48
57
  export { validateAgainstSchema } from './validate';
49
58
  export { isFieldVisible } from './visibility';
@@ -8,6 +8,7 @@
8
8
  import type { ReactNode } from 'react';
9
9
 
10
10
  import { Checkbox } from '../components/checkbox';
11
+ import { ChipGroup } from '../components/chip-group';
11
12
  import { Field } from '../components/field';
12
13
  import type { FieldModel } from './field-model';
13
14
 
@@ -38,6 +39,26 @@ export function MultiSelectField({
38
39
  .filter((candidate) => next.includes(candidate));
39
40
  onChange(ordered);
40
41
  };
42
+
43
+ // `control: chips` draws the multi-select as a toggle-chip grid in place of the
44
+ // checkbox group. The chips emit the selection in the OPTIONS' declared order.
45
+ if (model.control === 'chips') {
46
+ return (
47
+ <Field label={heading} description={description} error={error} group>
48
+ <ChipGroup
49
+ mode="multi"
50
+ options={model.options.map((option) => ({
51
+ value: String(option.value),
52
+ label: option.label,
53
+ }))}
54
+ value={selected}
55
+ onValueChange={(next) => {
56
+ onChange([...next]);
57
+ }}
58
+ />
59
+ </Field>
60
+ );
61
+ }
41
62
  return (
42
63
  <Field label={heading} description={description} error={error} group>
43
64
  {model.options.map((option, index) => {
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import type { ReactNode } from 'react';
8
8
 
9
+ import { DateRangeField, dateRangePairs } from './date-range-field';
9
10
  import { FieldNode } from './field-node';
10
11
  import { useKeyedCallbacks } from './keyed-callbacks';
11
12
  import type { JsonSchema, SchemaFormErrors } from './types';
@@ -26,6 +27,7 @@ export function ObjectFields({
26
27
  idPrefix,
27
28
  skip,
28
29
  markRequired = false,
30
+ fieldWording,
29
31
  }: {
30
32
  properties: readonly (readonly [string, JsonSchema])[];
31
33
  requiredKeys: ReadonlySet<string>;
@@ -38,6 +40,12 @@ export function ObjectFields({
38
40
  skip: string | undefined;
39
41
  /** Mark each required property's label with the DS required marker. */
40
42
  markRequired?: boolean;
43
+ /**
44
+ * Per-property picker placeholders, keyed by property name. Handed to the root form's
45
+ * object only; a property whose `enum` draws as a select shows its entry's
46
+ * `choiceLabel` as the select placeholder. Absent for nested objects.
47
+ */
48
+ fieldWording?: Readonly<Record<string, { choiceLabel?: string }>>;
41
49
  }): ReactNode {
42
50
  const obj = isPlainObject(value) ? value : {};
43
51
  const changeFor = useKeyedCallbacks<string, unknown>(
@@ -46,14 +54,39 @@ export function ObjectFields({
46
54
  },
47
55
  properties.map(([name]) => name),
48
56
  );
57
+ // A drawable date-range pair renders as ONE DateRangeField at the START's slot; the
58
+ // END is skipped. The pairing is `dateRangePairs`, the one definition every surface
59
+ // shares. Properties here are one page's, so the "same page" rule holds by construction.
60
+ const pairs = dateRangePairs(properties);
61
+ const pairByStart = new Map(pairs.map((pair) => [pair.startName, pair]));
62
+ const skippedEnds = new Set(pairs.map((pair) => pair.endName));
63
+ const childPathOf = (name: string): string => (path === '' ? name : `${path}.${name}`);
49
64
  return (
50
65
  <>
51
66
  {properties.map(([name, propSchema]) => {
52
67
  if (skip !== undefined && name === skip) return null;
68
+ if (skippedEnds.has(name)) return null;
53
69
  // A field hidden by its `visibleWhen` predicate (scope B) is not rendered; the
54
70
  // validator skips it in step, so a hidden field is neither shown nor required.
55
71
  if (!isFieldVisible(propSchema, obj)) return null;
56
- const childPath = path === '' ? name : `${path}.${name}`;
72
+ const pair = pairByStart.get(name);
73
+ if (pair !== undefined) {
74
+ return (
75
+ <DateRangeField
76
+ key={name}
77
+ pair={pair}
78
+ startValue={obj[pair.startName]}
79
+ endValue={obj[pair.endName]}
80
+ startRequired={requiredKeys.has(pair.startName)}
81
+ endRequired={requiredKeys.has(pair.endName)}
82
+ markRequired={markRequired}
83
+ startError={errors?.[childPathOf(pair.startName)]}
84
+ endError={errors?.[childPathOf(pair.endName)]}
85
+ onChangeStart={changeFor(pair.startName)}
86
+ onChangeEnd={changeFor(pair.endName)}
87
+ />
88
+ );
89
+ }
57
90
  return (
58
91
  <FieldNode
59
92
  key={name}
@@ -61,12 +94,13 @@ export function ObjectFields({
61
94
  root={root}
62
95
  value={obj[name]}
63
96
  onChange={changeFor(name)}
64
- path={childPath}
97
+ path={childPathOf(name)}
65
98
  label={name}
66
99
  required={requiredKeys.has(name)}
67
100
  markRequired={markRequired}
68
101
  errors={errors}
69
102
  idPrefix={idPrefix}
103
+ choiceLabel={fieldWording?.[name]?.choiceLabel}
70
104
  />
71
105
  );
72
106
  })}
@@ -31,12 +31,17 @@ import { CompletionProviderContext, ExpressionFieldContext } from './context';
31
31
  import type { DateConstraints, ExpressionAnnotation, MediaUpload } from './field-model';
32
32
  import { MediaField } from './media-field';
33
33
  import type { CompletionProvider } from './SchemaForm';
34
+ import type { FormInputMode } from './types';
34
35
 
35
36
  export function StringField({
36
37
  heading,
37
38
  description,
38
39
  error,
39
40
  format,
41
+ inputMode,
42
+ multiline,
43
+ maxLength,
44
+ minLength,
40
45
  media,
41
46
  expression,
42
47
  date,
@@ -49,6 +54,10 @@ export function StringField({
49
54
  description: string | undefined;
50
55
  error: string | undefined;
51
56
  format: string | undefined;
57
+ inputMode: FormInputMode | undefined;
58
+ multiline: boolean;
59
+ maxLength: number | undefined;
60
+ minLength: number | undefined;
52
61
  media: MediaUpload | undefined;
53
62
  expression: ExpressionAnnotation | undefined;
54
63
  date: DateConstraints | undefined;
@@ -57,6 +66,7 @@ export function StringField({
57
66
  required: boolean;
58
67
  onChange: (value: unknown) => void;
59
68
  }): ReactNode {
69
+ const control = stringControl(format, inputMode);
60
70
  const completionProvider = useContext(CompletionProviderContext);
61
71
  const expressionField = useContext(ExpressionFieldContext);
62
72
  const current = typeof value === 'string' ? value : '';
@@ -114,24 +124,58 @@ export function StringField({
114
124
  onChange={emit}
115
125
  provider={completionProvider}
116
126
  />
127
+ ) : multiline ? (
128
+ // `control: textarea` draws a multi-line control; the length bounds draw
129
+ // natively, and a live counter shows the maximum (below).
130
+ <Textarea
131
+ maxLength={maxLength}
132
+ minLength={minLength}
133
+ value={current}
134
+ onChange={(event) => {
135
+ emit(event.target.value);
136
+ }}
137
+ />
117
138
  ) : (
118
139
  <TextInput
119
- type={stringInputType(format)}
140
+ type={control.type}
141
+ // The keyboard hint for a `numeric`/`decimal` mode (a text box with a
142
+ // numeric/decimal keyboard); `undefined` for every other control, which
143
+ // selects its kind through `type` instead.
144
+ inputMode={control.inputMode}
120
145
  // A native date control enforces its inclusive bounds (scope C); on a
121
146
  // non-date input the browser ignores these, and the validator enforces the
122
147
  // rest (unavailable days, range span) that the control cannot draw.
123
148
  min={date?.min}
124
149
  max={date?.max}
150
+ // The length bounds draw natively (the control caps input at `maxLength`)
151
+ // and the validator enforces them; a counter shows the maximum (below).
152
+ maxLength={maxLength}
153
+ minLength={minLength}
125
154
  value={current}
126
155
  onChange={(event) => {
127
156
  emit(event.target.value);
128
157
  }}
129
158
  />
130
159
  )}
160
+ {/* The live length counter, shown whenever a maximum is declared — on both the
161
+ text box and the text area. It counts code points, matching the validator's
162
+ own `maxLength` check. */}
163
+ {completionProvider === undefined && maxLength !== undefined ? (
164
+ <LengthCounter current={current} max={maxLength} />
165
+ ) : null}
131
166
  </Field>
132
167
  );
133
168
  }
134
169
 
170
+ /** The `n / max` length hint under a bounded string control. */
171
+ function LengthCounter({ current, max }: { current: string; max: number }): ReactNode {
172
+ return (
173
+ <p className="tai-field-hint" style={{ margin: 0 }} aria-live="polite">
174
+ {`${String(Array.from(current).length)} / ${String(max)}`}
175
+ </p>
176
+ );
177
+ }
178
+
135
179
  /**
136
180
  * A string field backed by argument completions. Split out so its memoised
137
181
  * hooks stay unconditional (they cannot live behind the provider check in
@@ -360,3 +404,30 @@ function stringInputType(format: string | undefined): string {
360
404
  if (format === undefined) return 'text';
361
405
  return FORMAT_INPUT_TYPES[format] ?? 'text';
362
406
  }
407
+
408
+ /** The native control each `inputMode` draws. `tel`/`email`/`url` select the
409
+ * matching input `type`; `numeric`/`decimal` keep a text box and set the HTML
410
+ * `inputmode` keyboard hint; `text` is a plain text box. */
411
+ const INPUT_MODE_CONTROLS: Record<
412
+ FormInputMode,
413
+ { readonly type: string; readonly inputMode: 'numeric' | 'decimal' | undefined }
414
+ > = {
415
+ text: { type: 'text', inputMode: undefined },
416
+ numeric: { type: 'text', inputMode: 'numeric' },
417
+ decimal: { type: 'text', inputMode: 'decimal' },
418
+ tel: { type: 'tel', inputMode: undefined },
419
+ email: { type: 'email', inputMode: undefined },
420
+ url: { type: 'url', inputMode: undefined },
421
+ };
422
+
423
+ // The native control for a string field: the `inputMode` keyword OUTRANKS
424
+ // `format` when present; otherwise the format mapping stands as before. The value
425
+ // is always the typed string — `numeric`/`decimal` set only the keyboard, never a
426
+ // number coercion.
427
+ function stringControl(
428
+ format: string | undefined,
429
+ inputMode: FormInputMode | undefined,
430
+ ): { readonly type: string; readonly inputMode: 'numeric' | 'decimal' | undefined } {
431
+ if (inputMode !== undefined) return INPUT_MODE_CONTROLS[inputMode];
432
+ return { type: stringInputType(format), inputMode: undefined };
433
+ }
@@ -11,6 +11,24 @@
11
11
  export type JsonSchemaType =
12
12
  'string' | 'number' | 'integer' | 'boolean' | 'object' | 'array' | 'null';
13
13
 
14
+ /**
15
+ * The input KIND a `string` field collects, drawn as the matching keyboard or
16
+ * native control. The vocabulary is the HTML `inputmode`/control naming of the
17
+ * concept: `numeric`/`decimal` set a text box's keyboard hint; `tel`/`email`/`url`
18
+ * select the matching native control. It draws, it does not validate.
19
+ */
20
+ export type FormInputMode = 'text' | 'numeric' | 'decimal' | 'tel' | 'email' | 'url';
21
+
22
+ /**
23
+ * The CONTROL a surface draws for a property in place of the default for the
24
+ * property's shape. `chips` draws a choice (a `string` with an `enum`, or an
25
+ * `array` of such strings) as a tap-to-select chip grid instead of the
26
+ * radio/select or checkbox group. `textarea` draws a `string` as a multi-line
27
+ * text control instead of a single-line box. It changes what is DRAWN, never what
28
+ * is accepted — the value and its validation stay the property's own.
29
+ */
30
+ export type FormControl = 'chips' | 'textarea';
31
+
14
32
  /** A Pydantic-style discriminated-union tag: which property selects the variant. */
15
33
  export interface Discriminator {
16
34
  readonly propertyName: string;
@@ -61,6 +79,26 @@ export interface JsonSchema {
61
79
  readonly minItems?: number;
62
80
  readonly maxItems?: number;
63
81
  readonly pattern?: string;
82
+ /**
83
+ * The input KIND a `string` field collects — a platform keyword (see {@link
84
+ * FormInputMode}). The renderer draws the matching keyboard or native control:
85
+ * `numeric`/`decimal` a text box with that keyboard hint (the value stays the
86
+ * typed string, so `"007"` keeps its leading zeros), `tel`/`email`/`url` the
87
+ * matching native control, `text` or absent a plain text box. It DRAWS, it does
88
+ * not validate — the answer door still enforces `type`, `pattern` and `format`.
89
+ * When present it OUTRANKS `format` for the control. A value outside the set is
90
+ * read as absent (the server ask door is the authority that refuses it).
91
+ */
92
+ readonly inputMode?: FormInputMode;
93
+ /**
94
+ * The CONTROL to draw for this property in place of its shape's default — a
95
+ * platform keyword (see {@link FormControl}). `chips` draws a `string`-with-`enum`
96
+ * or an `array` of such strings as a chip grid; `textarea` draws a `string` as a
97
+ * multi-line text control. It DRAWS, it does not validate — the answer door still
98
+ * enforces the property's `type`, `enum`, `maxLength` and the rest. A value outside
99
+ * the set is read as absent (the server ask door is the authority that refuses it).
100
+ */
101
+ readonly control?: FormControl;
64
102
  /**
65
103
  * Platform date-constraint keys on a `string`/`date` (or `date-time`) property. They
66
104
  * are NOT the JSON-Schema numeric keywords (which are inert on a string): `minDate`/