@tai42/studio-sdk 17.1.2 → 18.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.
- package/dist/components/execution-key-picker.d.ts +37 -0
- package/dist/components/execution-key-picker.d.ts.map +1 -0
- package/dist/components/execution-key-picker.js +49 -0
- package/dist/components/execution-key-picker.js.map +1 -0
- package/dist/components/json-tree-nodes.d.ts.map +1 -1
- package/dist/components/json-tree-nodes.js +24 -9
- package/dist/components/json-tree-nodes.js.map +1 -1
- package/dist/components/overflow-measure.d.ts +11 -3
- package/dist/components/overflow-measure.d.ts.map +1 -1
- package/dist/components/overflow-measure.js +8 -5
- package/dist/components/overflow-measure.js.map +1 -1
- package/dist/components/scroll-region.d.ts +12 -2
- package/dist/components/scroll-region.d.ts.map +1 -1
- package/dist/components/scroll-region.js +7 -5
- package/dist/components/scroll-region.js.map +1 -1
- package/dist/components/subject-section.d.ts +78 -0
- package/dist/components/subject-section.d.ts.map +1 -0
- package/dist/components/subject-section.js +66 -0
- package/dist/components/subject-section.js.map +1 -0
- package/dist/components/tool-picker.d.ts.map +1 -1
- package/dist/components/tool-picker.js +11 -1
- package/dist/components/tool-picker.js.map +1 -1
- package/dist/hooks/useApi.d.ts +96 -45
- package/dist/hooks/useApi.d.ts.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/schema-form/array-field.d.ts.map +1 -1
- package/dist/schema-form/array-field.js +3 -3
- package/dist/schema-form/array-field.js.map +1 -1
- package/dist/schema-form/context.d.ts +19 -0
- package/dist/schema-form/context.d.ts.map +1 -1
- package/dist/schema-form/context.js.map +1 -1
- package/dist/schema-form/field-model.d.ts +19 -0
- package/dist/schema-form/field-model.d.ts.map +1 -1
- package/dist/schema-form/field-node.d.ts +10 -3
- package/dist/schema-form/field-node.d.ts.map +1 -1
- package/dist/schema-form/field-node.js +16 -2
- package/dist/schema-form/field-node.js.map +1 -1
- package/dist/schema-form/keyed-callbacks.d.ts +2 -0
- package/dist/schema-form/keyed-callbacks.d.ts.map +1 -0
- package/dist/schema-form/keyed-callbacks.js +45 -0
- package/dist/schema-form/keyed-callbacks.js.map +1 -0
- package/dist/schema-form/object-fields.d.ts.map +1 -1
- package/dist/schema-form/object-fields.js +5 -3
- package/dist/schema-form/object-fields.js.map +1 -1
- package/dist/schema-form/record-field.d.ts.map +1 -1
- package/dist/schema-form/record-field.js +4 -6
- package/dist/schema-form/record-field.js.map +1 -1
- package/dist/schema-form/string-annotations.d.ts.map +1 -1
- package/dist/schema-form/string-annotations.js +26 -0
- package/dist/schema-form/string-annotations.js.map +1 -1
- package/dist/schema-form/string-field.js +22 -6
- package/dist/schema-form/string-field.js.map +1 -1
- package/dist/testing.d.ts +9 -7
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +16 -13
- package/dist/testing.js.map +1 -1
- package/package.json +3 -3
- package/src/components/execution-key-picker.tsx +116 -0
- package/src/components/json-tree-nodes.tsx +36 -18
- package/src/components/overflow-measure.ts +16 -5
- package/src/components/scroll-region.tsx +16 -3
- package/src/components/subject-section.tsx +177 -0
- package/src/components/tool-picker.tsx +14 -2
- package/src/index.ts +11 -0
- package/src/schema-form/array-field.tsx +3 -3
- package/src/schema-form/context.ts +20 -0
- package/src/schema-form/field-model.ts +20 -0
- package/src/schema-form/field-node.tsx +11 -3
- package/src/schema-form/keyed-callbacks.ts +46 -0
- package/src/schema-form/object-fields.tsx +8 -3
- package/src/schema-form/record-field.tsx +7 -6
- package/src/schema-form/string-annotations.ts +32 -1
- package/src/schema-form/string-field.tsx +25 -6
- package/src/testing.ts +21 -13
package/src/index.ts
CHANGED
|
@@ -221,6 +221,7 @@ export { Button, Card, EmptyState, ErrorState, Skeleton, Spinner } from './compo
|
|
|
221
221
|
export type { RadioGroupProps, RadioOption } from './components/radio-group';
|
|
222
222
|
export { RadioGroup } from './components/radio-group';
|
|
223
223
|
export type {
|
|
224
|
+
OverflowAxis,
|
|
224
225
|
OverflowRegionAttributes,
|
|
225
226
|
ProseScrollLabels,
|
|
226
227
|
ScrollRegionProps,
|
|
@@ -295,6 +296,8 @@ export { openTargetProps } from './components/open-target';
|
|
|
295
296
|
// hands back the parsed URL, so a caller that needs the URL does not re-parse it.
|
|
296
297
|
export type { CopyFieldProps } from './components/copy-field';
|
|
297
298
|
export { CopyField } from './components/copy-field';
|
|
299
|
+
export type { ExecutionKeyPickerProps, ExecutionKeyQuery } from './components/execution-key-picker';
|
|
300
|
+
export { ExecutionKeyPicker, isExecutionKeyListEmpty } from './components/execution-key-picker';
|
|
298
301
|
export type { ExtensionComboBuilderProps } from './components/extension-combo-builder';
|
|
299
302
|
export { ExtensionComboBuilder } from './components/extension-combo-builder';
|
|
300
303
|
export type { ExtensionFamily, ExtensionKindGroup } from './components/extension-grouping';
|
|
@@ -311,6 +314,14 @@ export type { ExternalLinkButtonProps } from './components/external-link';
|
|
|
311
314
|
export { safeHttpUrl } from './components/primitives';
|
|
312
315
|
export type { RevealInputProps } from './components/reveal-input';
|
|
313
316
|
export { RevealInput } from './components/reveal-input';
|
|
317
|
+
export type { SubjectSectionProps, SubjectTargetOption } from './components/subject-section';
|
|
318
|
+
export {
|
|
319
|
+
buildSubject,
|
|
320
|
+
SUBJECT_INCOMPLETE_MESSAGE,
|
|
321
|
+
SubjectSection,
|
|
322
|
+
toSubjectTargetOptions,
|
|
323
|
+
useSubjectFields,
|
|
324
|
+
} from './components/subject-section';
|
|
314
325
|
export type { TokenName } from './components/tokens';
|
|
315
326
|
export { TOKEN_NAMES } from './components/tokens';
|
|
316
327
|
export type { ToolPickerProps } from './components/tool-picker';
|
|
@@ -11,6 +11,7 @@ import { classifySchema } from './classify';
|
|
|
11
11
|
import { defaultValueForSchema } from './default-value';
|
|
12
12
|
import { FieldGroup } from './field-group';
|
|
13
13
|
import { FieldNode } from './field-node';
|
|
14
|
+
import { useKeyedCallbacks } from './keyed-callbacks';
|
|
14
15
|
import type { JsonSchema, SchemaFormErrors } from './types';
|
|
15
16
|
|
|
16
17
|
export function ArrayField({
|
|
@@ -46,6 +47,7 @@ export function ArrayField({
|
|
|
46
47
|
const setItem = (index: number, next: unknown): void => {
|
|
47
48
|
onChange(list.map((item, position) => (position === index ? next : item)));
|
|
48
49
|
};
|
|
50
|
+
const changeFor = useKeyedCallbacks<number, unknown>(setItem, list.keys());
|
|
49
51
|
|
|
50
52
|
return (
|
|
51
53
|
<FieldGroup heading={heading} description={description} error={error} atRoot={false}>
|
|
@@ -63,9 +65,7 @@ export function ArrayField({
|
|
|
63
65
|
schema={items}
|
|
64
66
|
root={root}
|
|
65
67
|
value={item}
|
|
66
|
-
onChange={(
|
|
67
|
-
setItem(index, next);
|
|
68
|
-
}}
|
|
68
|
+
onChange={changeFor(index)}
|
|
69
69
|
path={`${path}[${String(index)}]`}
|
|
70
70
|
label={`Item ${String(index + 1)}`}
|
|
71
71
|
required
|
|
@@ -52,6 +52,23 @@ export interface ExpressionInputKey {
|
|
|
52
52
|
readonly gloss: string;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* One named jq variable the form hands the door beside `.`, mirroring jq-studio's
|
|
57
|
+
* variable descriptor without importing it. The door lists it in the legend, offers
|
|
58
|
+
* `$name` as a path root, and seeds the Test panel with its `sample`.
|
|
59
|
+
*/
|
|
60
|
+
export interface ExpressionInputVariable {
|
|
61
|
+
/** The variable name WITHOUT the leading `$` (referenced as `$name`). */
|
|
62
|
+
readonly name: string;
|
|
63
|
+
/** One sentence: what this variable holds. */
|
|
64
|
+
readonly blurb: string;
|
|
65
|
+
/** The top-level keys of the variable's value; empty when it is a scalar or
|
|
66
|
+
* the annotation glosses none. */
|
|
67
|
+
readonly keys: readonly ExpressionInputKey[];
|
|
68
|
+
/** A static skeleton of the variable's value for the door's Test panel. */
|
|
69
|
+
readonly sample?: unknown;
|
|
70
|
+
}
|
|
71
|
+
|
|
55
72
|
/**
|
|
56
73
|
* What `.` IS for an expression-annotated field, as the form hands it to the
|
|
57
74
|
* injected door. Built by the renderer from the schema's `x-tai42-expression`
|
|
@@ -77,6 +94,9 @@ export interface ExpressionInputShape {
|
|
|
77
94
|
readonly caveats?: readonly string[];
|
|
78
95
|
/** A static skeleton of `.` for the door's test surface. */
|
|
79
96
|
readonly sample?: unknown;
|
|
97
|
+
/** The named variables the expression reads as `$name` beside `.`; omitted when
|
|
98
|
+
* the field declares none. */
|
|
99
|
+
readonly variables?: readonly ExpressionInputVariable[];
|
|
80
100
|
}
|
|
81
101
|
|
|
82
102
|
/**
|
|
@@ -34,6 +34,23 @@ export interface ExpressionAnnotationKey {
|
|
|
34
34
|
readonly gloss: string;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* One named jq variable the expression may read as `$name` beside its `.` input:
|
|
39
|
+
* its blurb and a representative sample the editor's Test panel binds it to. The
|
|
40
|
+
* `x-tai42-expression` annotation carries these under `variables`; a field whose
|
|
41
|
+
* `.` is its only input carries none.
|
|
42
|
+
*/
|
|
43
|
+
export interface ExpressionAnnotationVariable {
|
|
44
|
+
readonly name: string;
|
|
45
|
+
readonly blurb: string;
|
|
46
|
+
/** True when the annotation carries a `sample` for this variable — tracked
|
|
47
|
+
* separately because a legitimate sample may be JSON `null`. */
|
|
48
|
+
readonly hasSample: boolean;
|
|
49
|
+
/** The sample value the Test panel binds `$name` to. Meaningful only when
|
|
50
|
+
* {@link hasSample} is true. */
|
|
51
|
+
readonly sample: unknown;
|
|
52
|
+
}
|
|
53
|
+
|
|
37
54
|
/**
|
|
38
55
|
* The `x-tai42-expression` annotation on a string schema: the server declares
|
|
39
56
|
* that the field's value is authored in a pipeline expression language (only jq
|
|
@@ -53,6 +70,9 @@ export interface ExpressionAnnotation {
|
|
|
53
70
|
readonly blurb: string | undefined;
|
|
54
71
|
/** The top-level keys of `.`, each with a one-liner. */
|
|
55
72
|
readonly keys: readonly ExpressionAnnotationKey[] | undefined;
|
|
73
|
+
/** The named variables the expression reads as `$name` beside `.`. Absent when
|
|
74
|
+
* the annotation declares none (the field's `.` is its only input). */
|
|
75
|
+
readonly variables: readonly ExpressionAnnotationVariable[] | undefined;
|
|
56
76
|
/** What the expression must return (e.g. "true or false"). */
|
|
57
77
|
readonly returns: string | undefined;
|
|
58
78
|
/** Per-field caveats. */
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* inline; every other kind delegates to a dedicated field component). Object,
|
|
5
5
|
* array, and union renderers recurse back through here for their children.
|
|
6
6
|
*/
|
|
7
|
-
import type
|
|
7
|
+
import { memo, type ReactNode } from 'react';
|
|
8
8
|
|
|
9
9
|
import { Checkbox } from '../components/checkbox';
|
|
10
10
|
import { Field } from '../components/field';
|
|
@@ -45,8 +45,14 @@ function labelText(explicit: string | undefined, label: string | undefined): str
|
|
|
45
45
|
return explicit ?? label ?? 'Value';
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
/**
|
|
49
|
-
|
|
48
|
+
/**
|
|
49
|
+
* A single schema node, dispatched on its classified model kind. Wrapped in
|
|
50
|
+
* `memo` so a field whose props are unchanged is skipped: the containers hand
|
|
51
|
+
* each child a referentially-stable `onChange` (see {@link useKeyedCallbacks})
|
|
52
|
+
* and the controlled value keeps unedited branches identity-equal, so editing
|
|
53
|
+
* one field re-renders only that field's subtree, not every sibling.
|
|
54
|
+
*/
|
|
55
|
+
function FieldNodeImpl(props: FieldNodeProps): ReactNode {
|
|
50
56
|
const { root, value, onChange, path, required, idPrefix, errors } = props;
|
|
51
57
|
const classified = classifySchema(props.schema, root);
|
|
52
58
|
const base = labelText(classified.title, props.label);
|
|
@@ -187,6 +193,8 @@ export function FieldNode(props: FieldNodeProps): ReactNode {
|
|
|
187
193
|
}
|
|
188
194
|
}
|
|
189
195
|
|
|
196
|
+
export const FieldNode = memo(FieldNodeImpl);
|
|
197
|
+
|
|
190
198
|
/** The label/description/error chrome every scalar leaf field shares. */
|
|
191
199
|
interface ScalarFieldChrome {
|
|
192
200
|
readonly heading: string;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A stable per-key callback factory. A container (object, array, record) renders
|
|
3
|
+
* one child per key and folds that child's edits back into the container value.
|
|
4
|
+
* Written inline, each child's `onChange` is a fresh closure every render, which
|
|
5
|
+
* defeats {@link FieldNode}'s `memo`: an untouched sibling re-renders whenever any
|
|
6
|
+
* field changes. This returns the SAME function identity for a given key across
|
|
7
|
+
* renders — so a memoized child keyed on that identity is skipped when a sibling
|
|
8
|
+
* changes — while each call reads the latest folding logic through a ref, so the
|
|
9
|
+
* callback never closes over a stale container value.
|
|
10
|
+
*
|
|
11
|
+
* The cache is bounded by `liveKeys`, the container's current key set: every
|
|
12
|
+
* render evicts each cached handler whose key is not live, so a mounted form that
|
|
13
|
+
* adds and drops entries holds at most one handler per live key (the record
|
|
14
|
+
* field keys by a monotonic row id, so without eviction it would leak one dead
|
|
15
|
+
* handler per id ever created). Eviction runs during render on the ref-held Map —
|
|
16
|
+
* no effect, no extra render — and never touches a live key's handler, so
|
|
17
|
+
* identity stays stable for every key that survives the render.
|
|
18
|
+
*/
|
|
19
|
+
import { useRef } from 'react';
|
|
20
|
+
|
|
21
|
+
export function useKeyedCallbacks<K, A>(
|
|
22
|
+
fold: (key: K, arg: A) => void,
|
|
23
|
+
liveKeys: Iterable<K>,
|
|
24
|
+
): (key: K) => (arg: A) => void {
|
|
25
|
+
const foldRef = useRef(fold);
|
|
26
|
+
foldRef.current = fold;
|
|
27
|
+
// The cache lives in a ref, lazily initialised, so its identity is HARD-stable
|
|
28
|
+
// for the component's whole life: a `useMemo(() => new Map(), [])` cache is only
|
|
29
|
+
// a hint React may discard, which would drop every cached handler and re-render
|
|
30
|
+
// every sibling — the regression the memo is here to prevent.
|
|
31
|
+
const cacheRef = useRef<Map<K, (arg: A) => void> | undefined>(undefined);
|
|
32
|
+
const cache = (cacheRef.current ??= new Map<K, (arg: A) => void>());
|
|
33
|
+
const live = new Set<K>(liveKeys);
|
|
34
|
+
for (const key of cache.keys()) {
|
|
35
|
+
if (!live.has(key)) cache.delete(key);
|
|
36
|
+
}
|
|
37
|
+
return (key: K) => {
|
|
38
|
+
const cached = cache.get(key);
|
|
39
|
+
if (cached !== undefined) return cached;
|
|
40
|
+
const handler = (arg: A): void => {
|
|
41
|
+
foldRef.current(key, arg);
|
|
42
|
+
};
|
|
43
|
+
cache.set(key, handler);
|
|
44
|
+
return handler;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import type { ReactNode } from 'react';
|
|
8
8
|
|
|
9
9
|
import { FieldNode } from './field-node';
|
|
10
|
+
import { useKeyedCallbacks } from './keyed-callbacks';
|
|
10
11
|
import type { JsonSchema, SchemaFormErrors } from './types';
|
|
11
12
|
|
|
12
13
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
@@ -38,6 +39,12 @@ export function ObjectFields({
|
|
|
38
39
|
markRequired?: boolean;
|
|
39
40
|
}): ReactNode {
|
|
40
41
|
const obj = isPlainObject(value) ? value : {};
|
|
42
|
+
const changeFor = useKeyedCallbacks<string, unknown>(
|
|
43
|
+
(name, next) => {
|
|
44
|
+
onChange(setKey(obj, name, next));
|
|
45
|
+
},
|
|
46
|
+
properties.map(([name]) => name),
|
|
47
|
+
);
|
|
41
48
|
return (
|
|
42
49
|
<>
|
|
43
50
|
{properties.map(([name, propSchema]) => {
|
|
@@ -49,9 +56,7 @@ export function ObjectFields({
|
|
|
49
56
|
schema={propSchema}
|
|
50
57
|
root={root}
|
|
51
58
|
value={obj[name]}
|
|
52
|
-
onChange={(
|
|
53
|
-
onChange(setKey(obj, name, next));
|
|
54
|
-
}}
|
|
59
|
+
onChange={changeFor(name)}
|
|
55
60
|
path={childPath}
|
|
56
61
|
label={name}
|
|
57
62
|
required={requiredKeys.has(name)}
|
|
@@ -22,6 +22,7 @@ import { RecordEntryRendererContext } from './context';
|
|
|
22
22
|
import { defaultValueForSchema } from './default-value';
|
|
23
23
|
import { FieldGroup } from './field-group';
|
|
24
24
|
import { FieldNode } from './field-node';
|
|
25
|
+
import { useKeyedCallbacks } from './keyed-callbacks';
|
|
25
26
|
import type { JsonSchema, SchemaFormErrors } from './types';
|
|
26
27
|
|
|
27
28
|
interface Row {
|
|
@@ -119,6 +120,10 @@ export function RecordField({
|
|
|
119
120
|
const setValue = (id: number, next: unknown): void => {
|
|
120
121
|
commit(rows.map((row) => (row.id === id ? { ...row, value: next } : row)));
|
|
121
122
|
};
|
|
123
|
+
const valueChangeFor = useKeyedCallbacks<number, unknown>(
|
|
124
|
+
setValue,
|
|
125
|
+
rows.map((row) => row.id),
|
|
126
|
+
);
|
|
122
127
|
const addRow = (): void => {
|
|
123
128
|
commit([...rows, { id: nextId.current++, key: '', value: newEntryValue(values, root) }]);
|
|
124
129
|
};
|
|
@@ -146,9 +151,7 @@ export function RecordField({
|
|
|
146
151
|
schema={values}
|
|
147
152
|
root={root}
|
|
148
153
|
value={row.value}
|
|
149
|
-
onChange={(
|
|
150
|
-
setValue(row.id, next);
|
|
151
|
-
}}
|
|
154
|
+
onChange={valueChangeFor(row.id)}
|
|
152
155
|
path={entryPath}
|
|
153
156
|
label={undefined}
|
|
154
157
|
required
|
|
@@ -179,9 +182,7 @@ export function RecordField({
|
|
|
179
182
|
path: entryPath,
|
|
180
183
|
valueSchema: values,
|
|
181
184
|
value: row.value,
|
|
182
|
-
onChange: (
|
|
183
|
-
setValue(row.id, next);
|
|
184
|
-
},
|
|
185
|
+
onChange: valueChangeFor(row.id),
|
|
185
186
|
defaultField,
|
|
186
187
|
})}
|
|
187
188
|
</div>
|
|
@@ -6,7 +6,12 @@
|
|
|
6
6
|
* renders differently from the plain string it falls back to.
|
|
7
7
|
*/
|
|
8
8
|
import { isRecord } from '../guards';
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
ExpressionAnnotation,
|
|
11
|
+
ExpressionAnnotationKey,
|
|
12
|
+
ExpressionAnnotationVariable,
|
|
13
|
+
MediaUpload,
|
|
14
|
+
} from './field-model';
|
|
10
15
|
import type { JsonSchema } from './types';
|
|
11
16
|
|
|
12
17
|
const EXPRESSION_KEYWORD = 'x-tai42-expression';
|
|
@@ -58,6 +63,29 @@ function parseAnnotationKeys(
|
|
|
58
63
|
return keys;
|
|
59
64
|
}
|
|
60
65
|
|
|
66
|
+
/** Parse the optional `variables` array: absent → `undefined`, malformed → `null`
|
|
67
|
+
* (reject). Each entry needs a string `name` and `blurb`; `sample` is any JSON
|
|
68
|
+
* (including `null`), so its presence is tracked rather than typed. */
|
|
69
|
+
function parseAnnotationVariables(
|
|
70
|
+
value: unknown,
|
|
71
|
+
): readonly ExpressionAnnotationVariable[] | undefined | null {
|
|
72
|
+
if (value === undefined) return undefined;
|
|
73
|
+
if (!Array.isArray(value)) return null;
|
|
74
|
+
const variables: ExpressionAnnotationVariable[] = [];
|
|
75
|
+
for (const entry of value as readonly unknown[]) {
|
|
76
|
+
if (!isRecord(entry) || typeof entry.name !== 'string' || typeof entry.blurb !== 'string') {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
variables.push({
|
|
80
|
+
name: entry.name,
|
|
81
|
+
blurb: entry.blurb,
|
|
82
|
+
hasSample: 'sample' in entry,
|
|
83
|
+
sample: entry.sample,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return variables;
|
|
87
|
+
}
|
|
88
|
+
|
|
61
89
|
/** Parse the optional `caveats` array: absent → `undefined`, malformed → `null` (reject). */
|
|
62
90
|
function parseCaveatList(value: unknown): readonly string[] | undefined | null {
|
|
63
91
|
if (value === undefined) return undefined;
|
|
@@ -94,6 +122,8 @@ export function expressionAnnotation(schema: JsonSchema): ExpressionAnnotation |
|
|
|
94
122
|
|
|
95
123
|
const keys = parseAnnotationKeys(raw.keys);
|
|
96
124
|
if (keys === null) return undefined;
|
|
125
|
+
const variables = parseAnnotationVariables(raw.variables);
|
|
126
|
+
if (variables === null) return undefined;
|
|
97
127
|
const caveats = parseCaveatList(raw.caveats);
|
|
98
128
|
if (caveats === null) return undefined;
|
|
99
129
|
|
|
@@ -103,6 +133,7 @@ export function expressionAnnotation(schema: JsonSchema): ExpressionAnnotation |
|
|
|
103
133
|
label: label.value,
|
|
104
134
|
blurb: blurb.value,
|
|
105
135
|
keys,
|
|
136
|
+
variables,
|
|
106
137
|
returns: returns.value,
|
|
107
138
|
caveats,
|
|
108
139
|
hasSample: 'sample' in raw,
|
|
@@ -296,12 +296,14 @@ function expressionShape(
|
|
|
296
296
|
argName: string,
|
|
297
297
|
): ExpressionInputShape | undefined {
|
|
298
298
|
const declared =
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
299
|
+
[
|
|
300
|
+
expression.label,
|
|
301
|
+
expression.blurb,
|
|
302
|
+
expression.keys,
|
|
303
|
+
expression.variables,
|
|
304
|
+
expression.returns,
|
|
305
|
+
expression.caveats,
|
|
306
|
+
].some((field) => field !== undefined) || expression.hasSample;
|
|
305
307
|
if (!declared) return undefined;
|
|
306
308
|
return {
|
|
307
309
|
// Opaque, host-namespaced, stable per field (memoisation/telemetry only).
|
|
@@ -312,6 +314,23 @@ function expressionShape(
|
|
|
312
314
|
returns: expression.returns ?? '',
|
|
313
315
|
...(expression.caveats === undefined ? {} : { caveats: expression.caveats }),
|
|
314
316
|
...(expression.hasSample ? { sample: expression.sample } : {}),
|
|
317
|
+
...(expression.variables === undefined
|
|
318
|
+
? {}
|
|
319
|
+
: { variables: expression.variables.map(expressionVariable) }),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Map one classified annotation variable onto the door's variable descriptor. */
|
|
324
|
+
function expressionVariable(
|
|
325
|
+
variable: NonNullable<ExpressionAnnotation['variables']>[number],
|
|
326
|
+
): NonNullable<ExpressionInputShape['variables']>[number] {
|
|
327
|
+
return {
|
|
328
|
+
name: variable.name,
|
|
329
|
+
blurb: variable.blurb,
|
|
330
|
+
// The annotation glosses no sub-keys of a variable's value, so the door gets
|
|
331
|
+
// the variable's shape as untyped (empty keys).
|
|
332
|
+
keys: [],
|
|
333
|
+
...(variable.hasSample ? { sample: variable.sample } : {}),
|
|
315
334
|
};
|
|
316
335
|
}
|
|
317
336
|
|
package/src/testing.ts
CHANGED
|
@@ -109,25 +109,33 @@ export function flushResizeObserversFor(target: Element): void {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
/** The faked
|
|
113
|
-
const
|
|
114
|
-
const
|
|
112
|
+
/** The faked extents: equal reads as "fits", unequal as "scrolls". */
|
|
113
|
+
const FITTING_EXTENT = 100;
|
|
114
|
+
const OVERFLOWING_SCROLL_EXTENT = 400;
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
|
-
* Makes `element` report itself as
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
117
|
+
* Makes `element` report itself as overflowing (or not) on `axis`. jsdom runs no
|
|
118
|
+
* layout, so `scroll*` and `client*` are both 0 and every overflow test reads as
|
|
119
|
+
* "fits"; this shadows the prototype getters with own, configurable values so a
|
|
120
|
+
* test can drive either state. Call it again with the opposite flag to flip the
|
|
121
|
+
* element back.
|
|
122
122
|
*
|
|
123
123
|
* @param element - the element whose scroll metrics to fake.
|
|
124
|
-
* @param overflowing - true to report content
|
|
124
|
+
* @param overflowing - true to report content larger than the box on `axis`.
|
|
125
|
+
* @param axis - the dimension to fake: `horizontal` (width, the default) or
|
|
126
|
+
* `vertical` (height).
|
|
125
127
|
*/
|
|
126
|
-
export function setElementOverflow(
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
export function setElementOverflow(
|
|
129
|
+
element: HTMLElement,
|
|
130
|
+
overflowing: boolean,
|
|
131
|
+
axis: 'horizontal' | 'vertical' = 'horizontal',
|
|
132
|
+
): void {
|
|
133
|
+
const [client, scroll] =
|
|
134
|
+
axis === 'vertical' ? ['clientHeight', 'scrollHeight'] : ['clientWidth', 'scrollWidth'];
|
|
135
|
+
Object.defineProperty(element, client, { configurable: true, value: FITTING_EXTENT });
|
|
136
|
+
Object.defineProperty(element, scroll, {
|
|
129
137
|
configurable: true,
|
|
130
|
-
value: overflowing ?
|
|
138
|
+
value: overflowing ? OVERFLOWING_SCROLL_EXTENT : FITTING_EXTENT,
|
|
131
139
|
});
|
|
132
140
|
}
|
|
133
141
|
|