@tai42/studio-sdk 9.2.1 → 9.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,51 +1,36 @@
1
1
  /**
2
2
  * The string field renderer. A plain string renders as a text input (its
3
3
  * `type` derived from any JSON-Schema `format`); an expression-annotated schema
4
- * renders the jq field (a resting textarea with the visual-editor door); a
5
- * media-annotated schema renders the upload control instead; and when the form
6
- * supplies a completion provider, the input is backed by argument autocomplete.
4
+ * renders the INJECTED expression door (a resting textarea with the visual-editor
5
+ * button) when the host supplies one; a media-annotated schema renders the upload
6
+ * control instead; and when the form supplies a completion provider, the input is
7
+ * backed by argument autocomplete.
7
8
  *
8
- * `JqField` is reached through the SDK's own `./jq` re-export — the ONE
9
- * `@tai42/jq-studio` instance the whole deployment shares — never from
10
- * `@tai42/jq-studio` directly, which would bundle a second copy and orphan its
11
- * worker (see the `jq` module doc-comment).
12
- *
13
- * That door is loaded LAZILY, through a dynamic `import('../jq')` behind
14
- * {@link JqField}. The jq authoring door drags a heavy subgraph — the xyflow
15
- * visual editor, a Web Worker entry, and a wasm engine chunk — that a form
16
- * whose fields carry no `x-tai42-expression` annotation must never ship. A
17
- * static import would pin that subgraph into every consumer that bundles the
18
- * SDK (a host that externalises the SDK is unaffected either way); the dynamic
19
- * import keeps it out of `schema-form`'s static graph and pulls it only when an
20
- * annotated field actually mounts.
9
+ * NO JQ EDGE. This module — and every module under `schema-form` — names no jq
10
+ * type and imports the SDK's `../jq` re-export neither statically NOR dynamically.
11
+ * The jq authoring door drags a heavy subgraph (the xyflow visual editor, a Web
12
+ * Worker entry, and a multi-megabyte wasm engine), and a bundler EMITS that whole
13
+ * subgraph for a dynamic import just as it does for a static one — `lazy` defers
14
+ * the FETCH, never the emission. A consumer that bundles the SDK to render forms
15
+ * would still ship the chunks, the worker file, and the wasm it can never run.
16
+ * So the door arrives from the host instead, through
17
+ * {@link ExpressionFieldContext} (or the form's `expressionField` prop), and an
18
+ * annotated field with no injected door falls back to the plain string input.
21
19
  */
22
20
  import type { ReactNode } from 'react';
23
- import { Suspense, lazy, useCallback, useContext, useMemo, useState } from 'react';
21
+ import { Suspense, useCallback, useContext, useMemo, useState } from 'react';
24
22
 
25
23
  import { CompletionInput } from '../components/completion-input';
26
24
  import { Field } from '../components/field';
27
25
  import { AlertTriangleIcon } from '../components/icons';
28
26
  import { TextInput, Textarea } from '../components/inputs';
29
27
  import { errorMessage } from '../errors';
30
- import type { JqInputShapeDescriptor } from '../jq';
31
28
  import type { ExpressionAnnotation, MediaUpload } from './classify';
32
- import { CompletionProviderContext } from './context';
29
+ import type { ExpressionFieldComponent, ExpressionInputShape } from './context';
30
+ import { CompletionProviderContext, ExpressionFieldContext } from './context';
33
31
  import { MediaField } from './media-field';
34
32
  import type { CompletionProvider } from './SchemaForm';
35
33
 
36
- // The lazily-loaded jq door. Defining it at module scope (never inside a
37
- // component) is what keeps the lazy component's identity stable across renders,
38
- // so React resolves the chunk once and never re-suspends a mounted field. The
39
- // `import('../jq')` here is a DYNAMIC import — the sole runtime edge from
40
- // `schema-form` to the jq door — so the visual editor, worker, and wasm live in
41
- // a split chunk that a non-annotated form never requests. The `type`-only
42
- // import of `JqInputShapeDescriptor` above is erased at compile time and adds no
43
- // such edge.
44
- const JqField = lazy(async () => {
45
- const { JqField } = await import('../jq');
46
- return { default: JqField };
47
- });
48
-
49
34
  export function StringField({
50
35
  heading,
51
36
  description,
@@ -70,6 +55,7 @@ export function StringField({
70
55
  onChange: (value: unknown) => void;
71
56
  }): ReactNode {
72
57
  const completionProvider = useContext(CompletionProviderContext);
58
+ const expressionField = useContext(ExpressionFieldContext);
73
59
  const current = typeof value === 'string' ? value : '';
74
60
  // An empty optional field emits `undefined` (drops the key); a required one
75
61
  // keeps the empty string so validation can flag it.
@@ -77,15 +63,20 @@ export function StringField({
77
63
  onChange(next === '' && !required ? undefined : next);
78
64
  };
79
65
 
80
- // An expression-annotated field renders the jq field — the resting control plus
81
- // the visual-editor door. Checked before `media` because the expression
66
+ // An expression-annotated field renders the injected door — the resting control
67
+ // plus its visual-editor button. Checked before `media` because the expression
82
68
  // annotation is the more specific, deliberate opt-in (`x-tai42-expression`);
83
- // a schema carrying both is contradictory, and a jq expression is text, not an
69
+ // a schema carrying both is contradictory, and an expression is text, not an
84
70
  // upload. Not reachable together with the completion provider path: an
85
71
  // expression is authored, not completed from server suggestions.
86
- if (expression !== undefined) {
72
+ //
73
+ // With no door injected the annotation is INERT: the field falls through to the
74
+ // ordinary paths below, exactly as a malformed annotation does, so a form
75
+ // renders the same bytes either way.
76
+ if (expression !== undefined && expressionField !== undefined) {
87
77
  return (
88
- <JqExpressionField
78
+ <ExpressionField
79
+ component={expressionField}
89
80
  heading={heading}
90
81
  description={description}
91
82
  error={error}
@@ -198,29 +189,31 @@ function CompletionField({
198
189
  }
199
190
 
200
191
  /**
201
- * An expression-annotated string field, rendered as `JqField`: a multiline
202
- * resting control with the always-present visual-editor door. `JqField` brings
203
- * its own label/description/error chrome (a11y-linked slots), so it is NOT
204
- * wrapped in the form's `Field` component — the annotation's descriptor fields
205
- * map onto jq-studio's input-shape descriptor, and the form's per-field error
206
- * feeds the `error` prop.
192
+ * An expression-annotated string field, rendered through the injected door: a
193
+ * multiline resting control with the door's own visual-editor button. The door
194
+ * brings its own label/description/error chrome (a11y-linked slots), so it is NOT
195
+ * wrapped in the form's `Field` component — the annotation's descriptor fields map
196
+ * onto the input-shape descriptor, and the form's per-field error feeds `error`.
197
+ *
198
+ * The door is rendered as an ELEMENT, never called as a function, so its hooks and
199
+ * state (editor open, worker handle) belong to it and survive re-renders of this
200
+ * field.
207
201
  *
208
- * Deliberately unwired:
209
- * - `serverValidate` — no author-time validation endpoint applies to a
210
- * schema-declared field (the WASM runtime still powers the Test panel locally).
211
- * - `onEditorOpenChange` — the hosts that render `SchemaForm` register no global
212
- * keyboard shortcuts, so there is nothing to mute while the editor is open.
213
- * - `compact` — the form renders every sibling field with a visible label;
214
- * the compact variant (visually-hidden label, icon-only door) is for dense
215
- * host rows, which the schema form does not have.
202
+ * It mounts inside a `Suspense` boundary because a host is free to inject a `lazy`
203
+ * door to keep the jq subgraph in a split chunk of ITS OWN bundle. The fallback
204
+ * paints the resting shell — label/description/error chrome around the current
205
+ * value in a read-only multiline control — so the field holds its layout and keeps
206
+ * the value visible while that chunk resolves, and the swap does not flash. A door
207
+ * injected eagerly never suspends and renders straight through.
216
208
  *
217
- * The door is code-split ({@link JqField} is a `lazy` component), so it mounts
218
- * inside a `Suspense` boundary. Its fallback paints the resting shell —
219
- * label/description/error chrome around the current value in a read-only
220
- * multiline control — so the field holds its layout and keeps the value visible
221
- * while the chunk resolves, and the swap to the live door does not flash.
209
+ * The form passes only the props the field owns; a jq door's `serverValidate`,
210
+ * `onEditorOpenChange`, and `compact` stay unwired, because no author-time
211
+ * validation endpoint applies to a schema-declared field, the hosts that render
212
+ * `SchemaForm` register no global shortcuts to mute, and every sibling field shows
213
+ * a visible label (the compact variant is for dense host rows).
222
214
  */
223
- function JqExpressionField({
215
+ function ExpressionField({
216
+ component: Door,
224
217
  heading,
225
218
  description,
226
219
  error,
@@ -229,6 +222,7 @@ function JqExpressionField({
229
222
  value,
230
223
  onChange,
231
224
  }: {
225
+ component: ExpressionFieldComponent;
232
226
  heading: string;
233
227
  description: string | undefined;
234
228
  error: string | undefined;
@@ -237,13 +231,13 @@ function JqExpressionField({
237
231
  value: string;
238
232
  onChange: (value: string) => void;
239
233
  }): ReactNode {
240
- // Memoised: the descriptor's identity feeds jq-studio's own memoisation, so a
234
+ // Memoised: the descriptor's identity feeds the door's own memoisation, so a
241
235
  // fresh object each keystroke would defeat it.
242
236
  const shape = useMemo(() => expressionShape(expression, argName), [expression, argName]);
243
237
  return (
244
238
  <Suspense
245
239
  fallback={
246
- <JqExpressionFieldSkeleton
240
+ <ExpressionFieldSkeleton
247
241
  heading={heading}
248
242
  description={description}
249
243
  error={error}
@@ -251,7 +245,7 @@ function JqExpressionField({
251
245
  />
252
246
  }
253
247
  >
254
- <JqField
248
+ <Door
255
249
  label={heading}
256
250
  description={description}
257
251
  error={error}
@@ -265,13 +259,13 @@ function JqExpressionField({
265
259
  }
266
260
 
267
261
  /**
268
- * The resting shell shown while the jq door chunk loads. It mirrors the live
269
- * door's footprint — the same label/description/error chrome wrapping a
262
+ * The resting shell shown while a lazily-injected door resolves. It mirrors the
263
+ * live door's footprint — the same label/description/error chrome wrapping a
270
264
  * multiline control seeded with the current value — but the control is inert
271
265
  * (read-only, `aria-busy`) because there is nothing to edit yet. Matching the
272
266
  * footprint is what keeps the Suspense swap from shifting layout or flashing.
273
267
  */
274
- function JqExpressionFieldSkeleton({
268
+ function ExpressionFieldSkeleton({
275
269
  heading,
276
270
  description,
277
271
  error,
@@ -290,17 +284,16 @@ function JqExpressionFieldSkeleton({
290
284
  }
291
285
 
292
286
  /**
293
- * Map a classified expression annotation onto jq-studio's input-shape
294
- * descriptor. A bare annotation (`language` only) maps to NO descriptor — the
295
- * undeclared-shape `JqField` is the honest rendering when the server said
296
- * nothing about `.`. When any descriptor field is present, the required
297
- * descriptor members the annotation omits fall back to neutral values rather
298
- * than invented copy.
287
+ * Map a classified expression annotation onto the door's input-shape descriptor.
288
+ * A bare annotation (`language` only) maps to NO descriptor — an undeclared shape
289
+ * is the honest rendering when the server said nothing about `.`. When any
290
+ * descriptor field is present, the required descriptor members the annotation
291
+ * omits fall back to neutral values rather than invented copy.
299
292
  */
300
293
  function expressionShape(
301
294
  expression: ExpressionAnnotation,
302
295
  argName: string,
303
- ): JqInputShapeDescriptor | undefined {
296
+ ): ExpressionInputShape | undefined {
304
297
  const declared =
305
298
  expression.label !== undefined ||
306
299
  expression.blurb !== undefined ||