@typed/ui 1.0.0-beta.6 → 1.0.0-beta.7

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 (128) hide show
  1. package/dist/Alert.d.ts +35 -41
  2. package/dist/Alert.d.ts.map +1 -1
  3. package/dist/Alert.js +23 -20
  4. package/dist/Button.d.ts +34 -78
  5. package/dist/Button.d.ts.map +1 -1
  6. package/dist/Button.js +16 -12
  7. package/dist/Carousel.d.ts +35 -236
  8. package/dist/Carousel.d.ts.map +1 -1
  9. package/dist/Carousel.js +13 -136
  10. package/dist/Checkbox.d.ts +55 -90
  11. package/dist/Checkbox.d.ts.map +1 -1
  12. package/dist/Checkbox.js +44 -38
  13. package/dist/Collection.d.ts +15 -15
  14. package/dist/Collection.js +7 -7
  15. package/dist/Combobox.d.ts +38 -187
  16. package/dist/Combobox.d.ts.map +1 -1
  17. package/dist/Combobox.js +9 -80
  18. package/dist/Component.d.ts +30 -24
  19. package/dist/Component.d.ts.map +1 -1
  20. package/dist/Component.js +23 -12
  21. package/dist/Composite.d.ts +50 -50
  22. package/dist/Composite.js +20 -20
  23. package/dist/Dialog.d.ts +34 -34
  24. package/dist/Dialog.d.ts.map +1 -1
  25. package/dist/Dialog.js +16 -14
  26. package/dist/Disclosure.d.ts +15 -15
  27. package/dist/Disclosure.js +6 -6
  28. package/dist/Dom/Events.d.ts +4 -4
  29. package/dist/Dom/Events.js +4 -4
  30. package/dist/Dom/Props.d.ts +9 -9
  31. package/dist/Dom/Props.js +4 -4
  32. package/dist/Dom/Refs.d.ts +2 -2
  33. package/dist/Dom/Refs.js +1 -1
  34. package/dist/Dom/Render.d.ts +2 -2
  35. package/dist/Dom/Render.js +2 -2
  36. package/dist/Dom/Types.d.ts +33 -33
  37. package/dist/Dom/index.d.ts +5 -5
  38. package/dist/Dom/index.d.ts.map +1 -1
  39. package/dist/Dom/index.js +4 -4
  40. package/dist/Dom.d.ts +1 -1
  41. package/dist/Dom.js +1 -1
  42. package/dist/Focusable.d.ts +5 -5
  43. package/dist/Focusable.js +1 -1
  44. package/dist/Form.d.ts +565 -533
  45. package/dist/Form.d.ts.map +1 -1
  46. package/dist/Form.js +350 -195
  47. package/dist/Grid.d.ts +39 -220
  48. package/dist/Grid.d.ts.map +1 -1
  49. package/dist/Grid.js +12 -107
  50. package/dist/Group.d.ts +50 -69
  51. package/dist/Group.d.ts.map +1 -1
  52. package/dist/Group.js +33 -25
  53. package/dist/Heading.d.ts +41 -40
  54. package/dist/Heading.d.ts.map +1 -1
  55. package/dist/Heading.js +26 -16
  56. package/dist/Hovercard.d.ts +18 -18
  57. package/dist/Hovercard.js +6 -6
  58. package/dist/HttpRouter.d.ts +3 -3
  59. package/dist/HttpRouter.js +5 -5
  60. package/dist/Link.d.ts +17 -21
  61. package/dist/Link.d.ts.map +1 -1
  62. package/dist/Link.js +11 -0
  63. package/dist/Listbox.d.ts +31 -163
  64. package/dist/Listbox.d.ts.map +1 -1
  65. package/dist/Listbox.js +9 -80
  66. package/dist/Menu.d.ts +67 -376
  67. package/dist/Menu.d.ts.map +1 -1
  68. package/dist/Menu.js +17 -179
  69. package/dist/Menubar.d.ts +24 -142
  70. package/dist/Menubar.d.ts.map +1 -1
  71. package/dist/Menubar.js +8 -66
  72. package/dist/Meter.d.ts +72 -132
  73. package/dist/Meter.d.ts.map +1 -1
  74. package/dist/Meter.js +35 -42
  75. package/dist/NativeDetails.d.ts +1 -1
  76. package/dist/NativeDetails.js +1 -1
  77. package/dist/NativeDialog.d.ts +7 -5
  78. package/dist/NativeDialog.d.ts.map +1 -1
  79. package/dist/NativeDialog.js +42 -4
  80. package/dist/NativePopover.d.ts +6 -4
  81. package/dist/NativePopover.d.ts.map +1 -1
  82. package/dist/NativePopover.js +43 -5
  83. package/dist/Popover.d.ts +18 -19
  84. package/dist/Popover.d.ts.map +1 -1
  85. package/dist/Popover.js +7 -8
  86. package/dist/RadioGroup.d.ts +86 -188
  87. package/dist/RadioGroup.d.ts.map +1 -1
  88. package/dist/RadioGroup.js +55 -105
  89. package/dist/Role.d.ts +4 -4
  90. package/dist/Role.js +1 -1
  91. package/dist/Select.d.ts +109 -220
  92. package/dist/Select.d.ts.map +1 -1
  93. package/dist/Select.js +67 -117
  94. package/dist/Separator.d.ts +30 -26
  95. package/dist/Separator.d.ts.map +1 -1
  96. package/dist/Separator.js +16 -10
  97. package/dist/Slider.d.ts +64 -111
  98. package/dist/Slider.d.ts.map +1 -1
  99. package/dist/Slider.js +47 -42
  100. package/dist/SpinButton.d.ts +64 -111
  101. package/dist/SpinButton.d.ts.map +1 -1
  102. package/dist/SpinButton.js +47 -42
  103. package/dist/Storybook.d.ts +2 -2
  104. package/dist/Storybook.js +1 -1
  105. package/dist/Switch.d.ts +52 -91
  106. package/dist/Switch.d.ts.map +1 -1
  107. package/dist/Switch.js +30 -36
  108. package/dist/Tabs.d.ts +44 -224
  109. package/dist/Tabs.d.ts.map +1 -1
  110. package/dist/Tabs.js +10 -94
  111. package/dist/Toolbar.d.ts +24 -142
  112. package/dist/Toolbar.d.ts.map +1 -1
  113. package/dist/Toolbar.js +8 -66
  114. package/dist/Tooltip.d.ts +20 -20
  115. package/dist/Tooltip.js +6 -6
  116. package/dist/Tree.d.ts +43 -229
  117. package/dist/Tree.d.ts.map +1 -1
  118. package/dist/Tree.js +12 -113
  119. package/dist/TreeGrid.d.ts +45 -264
  120. package/dist/TreeGrid.d.ts.map +1 -1
  121. package/dist/TreeGrid.js +13 -125
  122. package/dist/VisuallyHidden.d.ts +41 -27
  123. package/dist/VisuallyHidden.d.ts.map +1 -1
  124. package/dist/VisuallyHidden.js +27 -10
  125. package/dist/WindowSplitter.d.ts +68 -108
  126. package/dist/WindowSplitter.d.ts.map +1 -1
  127. package/dist/WindowSplitter.js +106 -23
  128. package/package.json +9 -8
package/dist/Form.js CHANGED
@@ -1,9 +1,22 @@
1
+ /**
2
+ * Schema-bound native controls, decoded values, field errors, and submit lifetime.
3
+ * Start with make for application forms; explicit-state controls support library boundaries.
4
+ * Browser FormData conversion and structured input codecs are separate APIs.
5
+ *
6
+ * Read the [Form guide](/explore/ui-form) for a complete example.
7
+ *
8
+ * [Platform reference](https://html.spec.whatwg.org/multipage/forms.html#the-form-element).
9
+ * @since 1.0.0
10
+ * @category Overview
11
+ * @packageDocumentation
12
+ */
1
13
  import * as Effect from "effect/Effect";
2
14
  import * as Context from "effect/Context";
3
15
  import * as Schema from "effect/Schema";
4
16
  import * as SchemaIssue from "effect/SchemaIssue";
5
17
  import * as SchemaTransformation from "effect/SchemaTransformation";
6
- import { Fx as FxApi, RefSubject } from "@typed/fx";
18
+ import { Fx as FxApi, RefSubject, Subject } from "@typed/fx";
19
+ import * as Scope from "effect/Scope";
7
20
  import { EventHandler, html, } from "@typed/template";
8
21
  import * as Dom from "./Dom.js";
9
22
  let nextFormId = 0;
@@ -11,16 +24,13 @@ let nextFormId = 0;
11
24
  * Effect context service used by schema-bound form controls.
12
25
  *
13
26
  * @remarks
14
- * ## Why
15
27
  * Bound controls avoid threading `state` through every call while their
16
28
  * service requirement remains visible in the Fx type.
17
29
  *
18
- * ## Ownership and lifetime
19
30
  * `Form` provides the service for its child render lifetime; use outside that
20
31
  * boundary fails with the ordinary Effect missing-service defect.
21
- *
22
32
  * @since 1.0.0
23
- * @category services
33
+ * @category Form context
24
34
  */
25
35
  export const CurrentForm = Context.Service("@typed/ui/Form/CurrentForm");
26
36
  function withCurrentForm() {
@@ -43,10 +53,8 @@ function emptyOptionalFields() {
43
53
  * Builds the serializable schema for a form's hydrated state.
44
54
  *
45
55
  * @remarks
46
- * ## Why
47
56
  * The hydration payload needs validation independent from runtime-only field codecs.
48
57
  *
49
- * ## Ownership and lifetime
50
58
  * Pure schema construction; the returned Schema acquires no Scope or subscription.
51
59
  *
52
60
  * @example
@@ -57,9 +65,8 @@ function emptyOptionalFields() {
57
65
  * const codec = Schema.Struct({ email: Schema.String })
58
66
  * const stateCodec = StateSchema(codec)
59
67
  * ```
60
- *
61
68
  * @since 1.0.0
62
- * @category schemas
69
+ * @category Hydration schemas
63
70
  */
64
71
  export function StateSchema(codec) {
65
72
  return Schema.Struct({
@@ -74,11 +81,9 @@ export function StateSchema(codec) {
74
81
  * Creates a Scope-owned hydrated form RefSubject from a Struct codec.
75
82
  *
76
83
  * @remarks
77
- * ## Why
78
84
  * One constructor establishes values, defaults, validation state, field codecs,
79
85
  * and hydration identity consistently.
80
86
  *
81
- * ## Ownership and lifetime
82
87
  * Requires `Scope.Scope`. Provide an explicit `id` during SSR; the counter-based
83
88
  * fallback is process/order dependent. Only state data hydrates—`codec` and
84
89
  * `fields` are reattached from the live Struct on each runtime.
@@ -94,9 +99,8 @@ export function StateSchema(codec) {
94
99
  * return state
95
100
  * })
96
101
  * ```
97
- *
98
102
  * @since 1.0.0
99
- * @category constructors
103
+ * @category State construction
100
104
  */
101
105
  export function makeState(codec, initial) {
102
106
  const id = initial.id ?? `typed-form-${++nextFormId}`;
@@ -128,6 +132,9 @@ function decodeUpdatedField(state, name, encoded) {
128
132
  return Schema.decodeUnknownEffect(state.fields[name])(encoded).pipe(Effect.flatMap((decoded) => Effect.andThen(updateDecodedValue(state, name, decoded), () => setFieldError(state, name, undefined))), Effect.catch((error) => setFieldError(state, name, error.message)));
129
133
  }
130
134
  function inputProps(options, type, codec) {
135
+ const parts = maskParts.get(codec);
136
+ if (parts !== undefined)
137
+ return maskedInputProps(options, type, codec, parts);
131
138
  return () => ({
132
139
  type,
133
140
  name: options.name,
@@ -141,7 +148,7 @@ function inputProps(options, type, codec) {
141
148
  }
142
149
  function renderInput(options, host, type, defaultCodec) {
143
150
  const codec = options.codec ?? defaultCodec;
144
- return Dom.renderHost()(options, host, inputProps(options, type, codec), "", (props) => html `<input ...${props} />`);
151
+ return FxApi.suspend(() => Dom.renderHost()(options, host, inputProps(options, type, codec), "", (props) => html `<input ...${props} />`));
145
152
  }
146
153
  function makeInput(type, codec) {
147
154
  return function (options, host) {
@@ -161,11 +168,9 @@ function makeSchemaBoundInput(formCodec, type) {
161
168
  * Binds a native `input[type=text]` to a string field.
162
169
  *
163
170
  * @remarks
164
- * ## Why
165
171
  * The control uses the browser's real input event and a Schema codec while
166
172
  * keeping state independently testable.
167
173
  *
168
- * ## Ownership and lifetime
169
174
  * The control Scope owns DOM listeners/subscriptions; the supplied `FormState`
170
175
  * may outlive the rendered input. A custom host must apply all merged props.
171
176
  *
@@ -180,173 +185,124 @@ function makeSchemaBoundInput(formCodec, type) {
180
185
  * return TextInput({ state, name: "name" })
181
186
  * })
182
187
  * ```
183
- *
184
188
  * @since 1.0.0
185
- * @category components
189
+ * @category Native controls
186
190
  */
187
191
  export const TextInput = makeInput("text", Schema.String);
188
192
  /**
189
193
  * Binds a native search input to a string field.
190
194
  * @remarks
191
- * ## Why
192
195
  * Preserves the platform's search-input semantics while sharing Typed validation.
193
- * ## Ownership and lifetime
194
- * DOM work is control-Scope-owned; form state remains independently owned.
195
196
  * @since 1.0.0
196
- * @category components
197
+ * @category Native controls
197
198
  */
198
199
  export const SearchInput = makeInput("search", Schema.String);
199
200
  /**
200
201
  * Binds a native email input to a string field.
201
202
  * @remarks
202
- * ## Why
203
203
  * Keeps browser email affordances and constraints available alongside Schema validation.
204
- * ## Ownership and lifetime
205
- * DOM work is control-Scope-owned; form state remains independently owned.
206
204
  * @since 1.0.0
207
- * @category components
205
+ * @category Native controls
208
206
  */
209
207
  export const EmailInput = makeInput("email", Schema.String);
210
208
  /**
211
209
  * Binds a native URL input to a string field.
212
210
  * @remarks
213
- * ## Why
214
211
  * Keeps browser URL affordances while the schema remains the decoded state contract.
215
- * ## Ownership and lifetime
216
- * DOM work is control-Scope-owned; form state remains independently owned.
217
212
  * @since 1.0.0
218
- * @category components
213
+ * @category Native controls
219
214
  */
220
215
  export const UrlInput = makeInput("url", Schema.String);
221
216
  /**
222
217
  * Binds a native telephone input to a string field.
223
218
  * @remarks
224
- * ## Why
225
219
  * Preserves platform telephone keyboards and autocomplete behavior.
226
- * ## Ownership and lifetime
227
- * DOM work is control-Scope-owned; form state remains independently owned.
228
220
  * @since 1.0.0
229
- * @category components
221
+ * @category Native controls
230
222
  */
231
223
  export const TelInput = makeInput("tel", Schema.String);
232
224
  /**
233
225
  * Binds a native password input to a string field.
234
226
  * @remarks
235
- * ## Why
236
227
  * Uses browser password handling instead of recreating sensitive-input behavior.
237
- * ## Ownership and lifetime
238
- * DOM work is control-Scope-owned; form state remains independently owned.
239
228
  * @since 1.0.0
240
- * @category components
229
+ * @category Native controls
241
230
  */
242
231
  export const PasswordInput = makeInput("password", Schema.String);
243
232
  /**
244
233
  * Binds a native hidden input to a string field.
245
234
  * @remarks
246
- * ## Why
247
235
  * Allows standards-based form serialization for non-visible values.
248
- * ## Ownership and lifetime
249
- * DOM work is control-Scope-owned; form state remains independently owned.
250
236
  * @since 1.0.0
251
- * @category components
237
+ * @category Native controls
252
238
  */
253
239
  export const HiddenInput = makeInput("hidden", Schema.String);
254
240
  /**
255
241
  * Binds a native color input to a string field.
256
242
  * @remarks
257
- * ## Why
258
243
  * Retains the browser's color picker while state receives its string value.
259
- * ## Ownership and lifetime
260
- * DOM work is control-Scope-owned; form state remains independently owned.
261
244
  * @since 1.0.0
262
- * @category components
245
+ * @category Native controls
263
246
  */
264
247
  export const ColorInput = makeInput("color", Schema.String);
265
248
  /**
266
249
  * Binds a native time input to a string field.
267
250
  * @remarks
268
- * ## Why
269
251
  * Preserves browser locale and time-entry behavior without inventing a picker.
270
- * ## Ownership and lifetime
271
- * DOM work is control-Scope-owned; form state remains independently owned.
272
252
  * @since 1.0.0
273
- * @category components
253
+ * @category Native controls
274
254
  */
275
255
  export const TimeInput = makeInput("time", Schema.String);
276
256
  /**
277
257
  * Binds a native local date-time input to a string field.
278
258
  * @remarks
279
- * ## Why
280
259
  * Keeps the platform's local date-time UI and its standard encoded value.
281
- * ## Ownership and lifetime
282
- * DOM work is control-Scope-owned; form state remains independently owned.
283
260
  * @since 1.0.0
284
- * @category components
261
+ * @category Native controls
285
262
  */
286
263
  export const DateTimeLocalInput = makeInput("datetime-local", Schema.String);
287
264
  /**
288
265
  * Binds a native month input to a string field.
289
266
  * @remarks
290
- * ## Why
291
267
  * Preserves the browser month picker and standardized string encoding.
292
- * ## Ownership and lifetime
293
- * DOM work is control-Scope-owned; form state remains independently owned.
294
268
  * @since 1.0.0
295
- * @category components
269
+ * @category Native controls
296
270
  */
297
271
  export const MonthInput = makeInput("month", Schema.String);
298
272
  /**
299
273
  * Binds a native week input to a string field.
300
274
  * @remarks
301
- * ## Why
302
275
  * Preserves platform week-entry behavior and standardized string encoding.
303
- * ## Ownership and lifetime
304
- * DOM work is control-Scope-owned; form state remains independently owned.
305
276
  * @since 1.0.0
306
- * @category components
277
+ * @category Native controls
307
278
  */
308
279
  export const WeekInput = makeInput("week", Schema.String);
309
280
  /**
310
281
  * Binds a native number input to a finite number field.
311
282
  * @remarks
312
- * ## Why
313
283
  * `FiniteFromString` makes the browser's string value an explicit typed decode.
314
- * ## Ownership and lifetime
315
- * DOM work is control-Scope-owned; form state remains independently owned.
316
284
  * @since 1.0.0
317
- * @category components
285
+ * @category Native controls
318
286
  */
319
287
  export const NumberInput = makeInput("number", Schema.FiniteFromString);
320
288
  /**
321
289
  * Binds a native range input to a finite number field.
322
290
  * @remarks
323
- * ## Why
324
291
  * Retains native slider interaction while exposing a decoded numeric value.
325
- * ## Ownership and lifetime
326
- * DOM work is control-Scope-owned; form state remains independently owned.
327
292
  * @since 1.0.0
328
- * @category components
293
+ * @category Native controls
329
294
  */
330
295
  export const RangeInput = makeInput("range", Schema.FiniteFromString);
331
296
  /**
332
297
  * Binds a native date input to a `Date` field.
333
298
  * @remarks
334
- * ## Why
335
299
  * `DateFromString` makes the native encoded value's conversion explicit and fallible.
336
- * ## Ownership and lifetime
337
- * DOM work is control-Scope-owned; form state remains independently owned.
338
300
  * @since 1.0.0
339
- * @category components
301
+ * @category Native controls
340
302
  */
341
303
  export const DateInput = makeInput("date", Schema.DateFromString);
342
304
  /**
343
- * Converts native FormData to a record and preserves repeated fields as arrays.
344
- * @remarks
345
- * ## Why
346
- * `Object.fromEntries` silently loses repeated names, which breaks checkbox,
347
- * multiselect, and multi-file submissions.
348
- * ## Ownership and lifetime
349
- * The function is synchronous and resource-free; File values are not cloned.
305
+ * Converts native form data to a record, preserving repeated names as arrays.
350
306
  * @example
351
307
  * ```ts
352
308
  * import { formDataToRecord } from "@typed/ui/Form"
@@ -357,7 +313,7 @@ export const DateInput = makeInput("date", Schema.DateFromString);
357
313
  * const record = formDataToRecord(data)
358
314
  * ```
359
315
  * @since 1.0.0
360
- * @category conversions
316
+ * @category Browser form data
361
317
  */
362
318
  export function formDataToRecord(data) {
363
319
  const result = {};
@@ -375,10 +331,8 @@ export function formDataToRecord(data) {
375
331
  /**
376
332
  * Decodes native FormData through an Effect Schema codec.
377
333
  * @remarks
378
- * ## Why
379
334
  * Browser serialization, repeated values, Files, and typed validation meet at
380
335
  * one explicit fallible boundary.
381
- * ## Ownership and lifetime
382
336
  * The returned Effect is lazy and owns no browser resource; it references File
383
337
  * objects present in the supplied FormData.
384
338
  * @example
@@ -389,21 +343,21 @@ export function formDataToRecord(data) {
389
343
  * const decode = decodeFormData(Schema.Struct({ name: Schema.String }), new FormData())
390
344
  * ```
391
345
  * @since 1.0.0
392
- * @category conversions
346
+ * @category Browser form data
393
347
  */
394
348
  export function decodeFormData(codec, data) {
395
349
  return Schema.decodeEffect(codec)(formDataToRecord(data));
396
350
  }
397
351
  /**
398
- * Validates current form values and synchronizes decoded values or field errors.
352
+ * Checks retained decoded values against the form codec Type.
353
+ *
399
354
  * @remarks
400
- * ## Why
401
- * Submission needs one whole-form schema check in addition to incremental field decoding.
402
- * ## Ownership and lifetime
403
- * The returned Effect updates the supplied state when run. On success it clears
404
- * errors; on failure it records messages and re-fails with `SchemaError`.
355
+ * Success replaces values and clears errors. Failure copies the aggregate schema message across
356
+ * fields and re-fails with SchemaError; this is not per-field issue-path mapping. A prior input
357
+ * decode error does not independently block success when the retained decoded value still
358
+ * validates.
405
359
  * @since 1.0.0
406
- * @category validation
360
+ * @category Validation
407
361
  */
408
362
  export function validate(state) {
409
363
  return Effect.flatMap(state, (current) => Schema.decodeUnknownEffect(Schema.toType(state.codec))(current.values)).pipe(Effect.flatMap((values) => Effect.as(RefSubject.update(state, (current) => ({
@@ -424,9 +378,7 @@ function formErrors(values, errors, error) {
424
378
  /**
425
379
  * Creates a named, Schema-decoded mask slot.
426
380
  * @remarks
427
- * ## Why
428
381
  * Length and character constraints are expressed beside the codec that owns conversion.
429
- * ## Ownership and lifetime
430
382
  * Pure constructor; the returned descriptor retains the codec but acquires no Scope.
431
383
  * @example
432
384
  * ```ts
@@ -436,19 +388,21 @@ function formErrors(values, errors, error) {
436
388
  * const areaCode = slot("area", Schema.String, { length: 3, charset: /[0-9]/ })
437
389
  * ```
438
390
  * @since 1.0.0
439
- * @category constructors
391
+ * @category Input codecs
440
392
  */
441
393
  export function slot(name, codec, options = {}) {
442
394
  return { _tag: "MaskSlot", name, codec, ...options };
443
395
  }
396
+ const maskParts = new WeakMap();
397
+ const maskedInputResets = new WeakMap();
444
398
  /**
445
399
  * Builds a bidirectional Schema codec from literal text and named slots.
446
400
  * @remarks
447
- * ## Why
448
- * Display formatting and decoding share one ordered specification and produce
449
- * ordinary Schema issues on invalid length, characters, literals, or slot values.
450
- * ## Ownership and lifetime
451
- * Pure codec construction. Decode/encode Effects are lazy and Scope-free.
401
+ * Strict encoding and decoding share the same parts and reject invalid length,
402
+ * characters, literals, or slot values. MaskedInput also uses these parts to
403
+ * retain drafts and format fixed-width slots with explicit charsets. Literal
404
+ * characters must be distinguishable from editable characters for auto-formatting;
405
+ * ambiguous or variable-width masks retain ordinary strict text entry.
452
406
  * @example
453
407
  * ```ts
454
408
  * import { mask, slot } from "@typed/ui/Form"
@@ -458,16 +412,223 @@ export function slot(name, codec, options = {}) {
458
412
  * slot("number", Schema.String, { length: 7 }))
459
413
  * ```
460
414
  * @since 1.0.0
461
- * @category schemas
415
+ * @category Input codecs
462
416
  */
463
417
  export function mask(...parts) {
464
418
  const valueSchema = Schema.declare((value) => typeof value === "object" &&
465
419
  value !== null &&
466
420
  parts.every((part) => typeof part === "string" || Reflect.has(value, part.name)));
467
- return Schema.String.pipe(Schema.decodeTo(valueSchema, SchemaTransformation.transformOrFail({
421
+ const codec = Schema.String.pipe(Schema.decodeTo(valueSchema, SchemaTransformation.transformOrFail({
468
422
  decode: (display, options) => decodeMask(parts, display, options),
469
423
  encode: (value, options) => encodeMask(parts, value, options),
470
424
  })));
425
+ maskParts.set(codec, parts);
426
+ return codec;
427
+ }
428
+ function maskedInputProps(options, type, codec, parts) {
429
+ return () => {
430
+ let draft;
431
+ let lastValue;
432
+ let composing = false;
433
+ let invalidDraft = false;
434
+ let editRevision = 0;
435
+ let pending;
436
+ let input;
437
+ const format = maskDraftFormatter(parts);
438
+ const message = `Enter a complete value in the format ${parts
439
+ .map((part) => (typeof part === "string" ? part : "_".repeat(part.length ?? 3)))
440
+ .join("")}.`;
441
+ const commit = Effect.fn(function* (element) {
442
+ const revision = ++editRevision;
443
+ const snapshot = yield* options.state;
444
+ pending = snapshot;
445
+ input = element;
446
+ const formatted = format?.(element.value, element.selectionStart ?? element.value.length);
447
+ if (formatted !== undefined) {
448
+ element.value = formatted.value;
449
+ element.setSelectionRange(formatted.cursor, formatted.cursor);
450
+ }
451
+ draft = element.value;
452
+ return yield* Schema.decodeEffect(codec)(draft).pipe(Effect.matchEffect({
453
+ onSuccess: Effect.fn(function* (decoded) {
454
+ if (revision !== editRevision ||
455
+ (yield* options.state).values[options.name] !== snapshot.values[options.name])
456
+ return;
457
+ pending = undefined;
458
+ lastValue = decoded;
459
+ invalidDraft = false;
460
+ element.setCustomValidity("");
461
+ yield* updateDecodedValue(options.state, options.name, decoded);
462
+ yield* setFieldError(options.state, options.name, undefined);
463
+ }),
464
+ onFailure: Effect.fn(function* () {
465
+ if (revision !== editRevision ||
466
+ (yield* options.state).values[options.name] !== snapshot.values[options.name])
467
+ return;
468
+ pending = undefined;
469
+ invalidDraft = true;
470
+ element.setCustomValidity(message);
471
+ yield* setFieldError(options.state, options.name, message);
472
+ }),
473
+ }));
474
+ });
475
+ return {
476
+ type,
477
+ name: options.name,
478
+ "aria-describedby": RefSubject.map(options.state, (state) => state.errors[options.name] === undefined
479
+ ? undefined
480
+ : fieldErrorId(options.state, options.name)),
481
+ "aria-invalid": RefSubject.map(options.state, (state) => state.errors[options.name] === undefined ? undefined : true),
482
+ ".value": Effect.flatMap(options.state, (state) => Schema.encodeUnknownEffect(codec)(state.values[options.name])),
483
+ ref: Effect.fn(function* (element) {
484
+ input = element;
485
+ const initial = yield* options.state;
486
+ lastValue = initial.values[options.name];
487
+ const resetInput = Effect.gen(function* () {
488
+ const revision = ++editRevision;
489
+ pending = undefined;
490
+ draft = undefined;
491
+ invalidDraft = false;
492
+ composing = false;
493
+ const value = (yield* options.state).values[options.name];
494
+ lastValue = value;
495
+ element.setCustomValidity("");
496
+ const encoded = yield* Schema.encodeUnknownEffect(codec)(value);
497
+ if (revision === editRevision && (yield* options.state).values[options.name] === value) {
498
+ element.value = encoded;
499
+ }
500
+ });
501
+ let resets = maskedInputResets.get(options.state);
502
+ if (resets === undefined) {
503
+ resets = { signal: Subject.unsafeMake(), users: 0 };
504
+ maskedInputResets.set(options.state, resets);
505
+ }
506
+ const resetSignal = resets;
507
+ resetSignal.users++;
508
+ yield* Scope.addFinalizer(yield* Effect.scope, Effect.suspend(() => {
509
+ if (--resetSignal.users > 0)
510
+ return Effect.void;
511
+ maskedInputResets.delete(options.state);
512
+ return resetSignal.signal.interrupt;
513
+ }));
514
+ yield* Effect.forkScoped(FxApi.observe(resetSignal.signal, () => resetInput));
515
+ yield* Effect.forkScoped(FxApi.observe(options.state, Effect.fn(function* (state) {
516
+ if (pending !== undefined &&
517
+ state.values[options.name] !== pending.values[options.name]) {
518
+ pending = undefined;
519
+ editRevision++;
520
+ draft = undefined;
521
+ invalidDraft = false;
522
+ }
523
+ const value = state.values[options.name];
524
+ if (value === lastValue &&
525
+ (composing ||
526
+ (draft !== undefined &&
527
+ (!invalidDraft || state.errors[options.name] !== undefined))))
528
+ return;
529
+ lastValue = value;
530
+ draft = undefined;
531
+ invalidDraft = false;
532
+ element.setCustomValidity("");
533
+ const revision = editRevision;
534
+ const encoded = yield* Schema.encodeUnknownEffect(codec)(value);
535
+ if (revision === editRevision &&
536
+ (yield* options.state).values[options.name] === value &&
537
+ element.value !== encoded) {
538
+ element.value = encoded;
539
+ }
540
+ })));
541
+ }),
542
+ onbeforeinput: EventHandler.make(Effect.fn(function* (event) {
543
+ if (event.isComposing || composing || !event.cancelable || format === undefined)
544
+ return;
545
+ if (event.inputType !== "deleteContentBackward" &&
546
+ event.inputType !== "deleteContentForward")
547
+ return;
548
+ const element = Dom.currentTarget(event);
549
+ const start = element.selectionStart;
550
+ if (start === null || start !== element.selectionEnd)
551
+ return;
552
+ const backwards = event.inputType === "deleteContentBackward";
553
+ const separators = parts
554
+ .filter((part) => typeof part === "string")
555
+ .join("");
556
+ let index = backwards ? start - 1 : start;
557
+ if (!separators.includes(element.value[index] ?? "\u0000"))
558
+ return;
559
+ while (index >= 0 &&
560
+ index < element.value.length &&
561
+ separators.includes(element.value[index])) {
562
+ index += backwards ? -1 : 1;
563
+ }
564
+ if (index < 0 || index >= element.value.length)
565
+ return;
566
+ event.preventDefault();
567
+ element.setRangeText("", index, index + 1, "start");
568
+ return yield* commit(element);
569
+ })),
570
+ oninput: EventHandler.make(Effect.fn(function* (event) {
571
+ input = Dom.currentTarget(event);
572
+ if (composing || event.isComposing)
573
+ return;
574
+ return yield* commit(input);
575
+ })),
576
+ oncompositionstart: EventHandler.make(() => Effect.sync(() => {
577
+ composing = true;
578
+ })),
579
+ oncompositionend: EventHandler.make(Effect.fn(function* (event) {
580
+ composing = false;
581
+ return yield* commit(Dom.currentTarget(event));
582
+ })),
583
+ };
584
+ };
585
+ }
586
+ // Only auto-format when fixed-width slots explicitly distinguish editable
587
+ // characters from literals. Ambiguous/variable-width codecs still retain drafts
588
+ // and validate, without guessing which characters the user meant to enter.
589
+ function maskDraftFormatter(parts) {
590
+ const slots = parts.filter((part) => typeof part !== "string");
591
+ const literals = parts.filter((part) => typeof part === "string").join("");
592
+ if (slots.length === 0 ||
593
+ slots.some((slot) => slot.length === undefined || slot.charset === undefined || slot.length < 1) ||
594
+ [...literals].some((character) => slots.some((slot) => matchesCharset(slot, character))))
595
+ return undefined;
596
+ return (text, cursor) => {
597
+ const characters = [];
598
+ let beforeCursor = 0;
599
+ for (let index = 0; index < text.length; index++) {
600
+ const character = text[index];
601
+ if (literals.includes(character))
602
+ continue;
603
+ characters.push(character);
604
+ if (index < cursor)
605
+ beforeCursor++;
606
+ }
607
+ let offset = 0;
608
+ let value = "";
609
+ let nextCursor = 0;
610
+ for (const [index, part] of parts.entries()) {
611
+ if (typeof part === "string") {
612
+ const complete = characters.length === slots.reduce((sum, slot) => sum + slot.length, 0);
613
+ const suffix = parts.slice(index + 1).every((remaining) => typeof remaining === "string");
614
+ if (offset < characters.length || (complete && suffix))
615
+ value += part;
616
+ continue;
617
+ }
618
+ const segment = characters.slice(offset, offset + part.length);
619
+ if (segment.some((character) => !matchesCharset(part, character)))
620
+ return undefined;
621
+ for (const character of segment) {
622
+ value += character;
623
+ offset++;
624
+ if (offset <= beforeCursor)
625
+ nextCursor = value.length;
626
+ }
627
+ }
628
+ if (offset !== characters.length)
629
+ return undefined;
630
+ return { value, cursor: beforeCursor === characters.length ? value.length : nextCursor };
631
+ };
471
632
  }
472
633
  function decodeMask(parts, display, options) {
473
634
  return Effect.gen(function* () {
@@ -541,14 +702,15 @@ function invalidMask(input, options) {
541
702
  /**
542
703
  * Binds a native text input to a structured mask value.
543
704
  * @remarks
544
- * ## Why
545
- * The supplied Schema codec controls both display encoding and input decoding;
546
- * failed edits update field errors rather than corrupting decoded state.
547
- * ## Ownership and lifetime
548
- * DOM listeners and reactive value binding live in the control Scope. The
549
- * supplied state can be tested and retained without mounting this control.
705
+ * A codec created by mask supplies the editable format. Fixed-width slots with
706
+ * explicit charsets receive literal insertion and caret-aware deletion; other
707
+ * codecs retain strict text entry. Incomplete drafts remain visible while decoded
708
+ * state keeps its previous value. Native custom validity and field error text
709
+ * prevent native submission of an incomplete mask. Composition is committed only
710
+ * after compositionend. New edits and resets supersede pending slot decoders.
711
+ * Each mounted input owns its draft observer and reset registration in Scope.
550
712
  * @since 1.0.0
551
- * @category components
713
+ * @category Input codecs
552
714
  */
553
715
  export function MaskedInput(options, host) {
554
716
  const { mask, ...inputOptions } = options;
@@ -571,14 +733,12 @@ function checkboxProps(options) {
571
733
  /**
572
734
  * Binds a native checkbox to a boolean form field.
573
735
  * @remarks
574
- * ## Why
575
736
  * Both the checked attribute and live property follow state, while the browser's
576
737
  * real change event is decoded through the field codec.
577
- * ## Ownership and lifetime
578
738
  * The rendered Scope owns the input, listener, and subscriptions. A custom host
579
739
  * must apply merged name, ARIA, checked, and change props.
580
740
  * @since 1.0.0
581
- * @category components
741
+ * @category Native controls
582
742
  */
583
743
  export function Checkbox(options, host) {
584
744
  return Dom.renderHost()(options, host, checkboxProps(options), "", (props) => html `<input ...${props} />`);
@@ -597,13 +757,11 @@ function selectProps(options) {
597
757
  /**
598
758
  * Binds a native select element to a string form field.
599
759
  * @remarks
600
- * ## Why
601
760
  * Native keyboard, accessibility, option, and form semantics remain browser-owned.
602
- * ## Ownership and lifetime
603
761
  * The rendered Scope owns the select/content subscriptions. A custom host must
604
762
  * preserve supplied name, ARIA, value, and change props.
605
763
  * @since 1.0.0
606
- * @category components
764
+ * @category Native controls
607
765
  */
608
766
  export function Select(options, host) {
609
767
  return Dom.renderHost()(options, host, selectProps(options), options.content, (props, content) => html `<select ...${props}>
@@ -616,12 +774,10 @@ function labelProps(options) {
616
774
  /**
617
775
  * Renders a native label with an explicit control relationship.
618
776
  * @remarks
619
- * ## Why
620
777
  * The browser supplies click-to-focus and accessible-name behavior with no synthetic layer.
621
- * ## Ownership and lifetime
622
778
  * The Scope owns label output/content; the referenced element remains separately owned.
623
779
  * @since 1.0.0
624
- * @category components
780
+ * @category Field relationships
625
781
  */
626
782
  export function Label(options, host) {
627
783
  return Dom.renderHost()(options, host, labelProps(options), options.content, (props, content) => html `<label ...${props}>${content}</label>`);
@@ -630,14 +786,13 @@ function descriptionProps() {
630
786
  return () => ({});
631
787
  }
632
788
  /**
633
- * Renders form description content in a neutral native div by default.
789
+ * Renders visible explanatory content in a neutral div.
790
+ *
634
791
  * @remarks
635
- * ## Why
636
- * Consumers can connect the resulting element with ordinary ARIA props where needed.
637
- * ## Ownership and lifetime
638
- * The Scope owns the description host/content and no external control.
792
+ * No control relationship is created automatically. The current input binding owns its generated
793
+ * error aria-describedby; do not assume consumer description IDs are merged into it.
639
794
  * @since 1.0.0
640
- * @category components
795
+ * @category Field relationships
641
796
  */
642
797
  export function Description(options, host) {
643
798
  return Dom.renderHost()(options, host, descriptionProps(), options.content, (props, content) => html `<div ...${props}>${content}</div>`);
@@ -649,15 +804,14 @@ function errorProps(options) {
649
804
  });
650
805
  }
651
806
  /**
652
- * Renders the current field error as a native ARIA alert region.
807
+ * Renders the named field message with a generated ID and alert role.
808
+ *
653
809
  * @remarks
654
- * ## Why
655
- * The same derived ID is placed on the error and in the control's
656
- * `aria-describedby`, keeping validation messaging coherent.
657
- * ## Ownership and lifetime
658
- * The Scope owns the alert and state subscription; removing it does not remove form state.
810
+ * Bound inputs refer to this ID through aria-describedby and expose aria-invalid when an error
811
+ * exists. Keep the form ID stable and render one matching error host per field. Alert timing
812
+ * still depends on the browser and assistive technology; this is not an announcement queue.
659
813
  * @since 1.0.0
660
- * @category components
814
+ * @category Field relationships
661
815
  */
662
816
  export function Error(options, host) {
663
817
  const content = RefSubject.map(options.state, (state) => state.errors[options.name] ?? "");
@@ -669,12 +823,10 @@ function submitProps() {
669
823
  /**
670
824
  * Renders a native `type=submit` button.
671
825
  * @remarks
672
- * ## Why
673
826
  * Keyboard activation, form association, and accessibility stay browser-standard.
674
- * ## Ownership and lifetime
675
827
  * The Scope owns the button/content; the surrounding Form owns submission sequencing.
676
828
  * @since 1.0.0
677
- * @category components
829
+ * @category Form actions
678
830
  */
679
831
  export function Submit(options, host) {
680
832
  return Dom.renderHost()(options, host, submitProps(), options.content, (props, content) => html `<button ...${props}>${content}</button>`);
@@ -688,13 +840,11 @@ function resetProps(options) {
688
840
  /**
689
841
  * Renders a native reset button that restores Typed form defaults.
690
842
  * @remarks
691
- * ## Why
692
843
  * It prevents the browser's independent control mutation and resets the single
693
844
  * RefSubject source of truth, clearing errors, metadata, and submitting state.
694
- * ## Ownership and lifetime
695
845
  * The Scope owns the click handler/content. The supplied form state remains independently owned.
696
846
  * @since 1.0.0
697
- * @category components
847
+ * @category Form actions
698
848
  */
699
849
  export function Reset(options, host) {
700
850
  return Dom.renderHost()(options, host, resetProps(options), options.content, (props, content) => html `<button ...${props}>${content}</button>`);
@@ -705,12 +855,10 @@ function groupProps(options) {
705
855
  /**
706
856
  * Renders an ARIA group with an optional accessible name.
707
857
  * @remarks
708
- * ## Why
709
858
  * Related controls can expose their relationship without a framework-specific wrapper.
710
- * ## Ownership and lifetime
711
859
  * The Scope owns host/content; child controls retain their own DOM/state contracts.
712
860
  * @since 1.0.0
713
- * @category components
861
+ * @category Field relationships
714
862
  */
715
863
  export function Group(options, host) {
716
864
  return Dom.renderHost()(options, host, groupProps(options), options.content, (props, content) => html `<div ...${props}>${content}</div>`);
@@ -724,12 +872,10 @@ function pushProps(options) {
724
872
  /**
725
873
  * Renders a button that appends one item to an array field.
726
874
  * @remarks
727
- * ## Why
728
875
  * Array mutation is immutable, typed, and marks the field dirty/touched.
729
- * ## Ownership and lifetime
730
876
  * The Scope owns the button handler/content; state may outlive the button.
731
877
  * @since 1.0.0
732
- * @category components
878
+ * @category Form actions
733
879
  */
734
880
  export function Push(options, host) {
735
881
  return Dom.renderHost()(options, host, pushProps(options), options.content, (props, content) => html `<button ...${props}>${content}</button>`);
@@ -743,25 +889,23 @@ function removeProps(options) {
743
889
  /**
744
890
  * Renders a button that removes one array item by index.
745
891
  * @remarks
746
- * ## Why
747
892
  * Array mutation is immutable, typed, and marks the field dirty/touched.
748
- * ## Ownership and lifetime
749
893
  * The Scope owns the button handler/content; state may outlive the button.
750
894
  * @since 1.0.0
751
- * @category components
895
+ * @category Form actions
752
896
  */
753
897
  export function Remove(options, host) {
754
898
  return Dom.renderHost()(options, host, removeProps(options), options.content, (props, content) => html `<button ...${props}>${content}</button>`);
755
899
  }
756
900
  /**
757
- * Sets one decoded field value and updates dirty/touched metadata.
901
+ * Assigns one decoded field value and updates dirty/touched metadata.
902
+ *
758
903
  * @remarks
759
- * ## Why
760
- * Programmatic updates use the same renderer-independent state transition as controls.
761
- * ## Ownership and lifetime
762
- * The returned Effect mutates only the supplied RefSubject when run.
904
+ * The helper does not decode or validate the supplied value and does not clear an existing field
905
+ * error. Use validate for an explicit whole-form check. Dirty tracking compares against the
906
+ * default field with !==; updates replace the top-level values record.
763
907
  * @since 1.0.0
764
- * @category state
908
+ * @category State transitions
765
909
  */
766
910
  export function setValue(state, key, value) {
767
911
  return updateDecodedValue(state, key, value);
@@ -782,15 +926,13 @@ function updateRecord(values, key, value) {
782
926
  return updated;
783
927
  }
784
928
  /**
785
- * Restores default values and clears errors, interaction metadata, and submission state.
929
+ * Restores default values and clears errors, metadata, and submitting.
930
+ *
786
931
  * @remarks
787
- * ## Why
788
- * Resetting the RefSubject, rather than only DOM controls, keeps every renderer
789
- * and test observer consistent with the source of truth.
790
- * ## Ownership and lifetime
791
- * The returned Effect performs one update when run and requires the same services as `state`.
932
+ * The defaultValues object is reused rather than deep-cloned. This state operation does not
933
+ * cancel a running submission Effect or reset independently owned result state.
792
934
  * @since 1.0.0
793
- * @category state
935
+ * @category State transitions
794
936
  */
795
937
  export function reset(state) {
796
938
  return RefSubject.update(state, (current) => ({
@@ -799,17 +941,15 @@ export function reset(state) {
799
941
  errors: {},
800
942
  meta: {},
801
943
  submitting: false,
802
- }));
944
+ })).pipe(Effect.tap(() => Effect.suspend(() => maskedInputResets.get(state)?.signal.onSuccess(undefined) ?? Effect.void)));
803
945
  }
804
946
  /**
805
947
  * Appends one item to an array field and marks it dirty and touched.
806
948
  * @remarks
807
- * ## Why
808
949
  * The immutable state transition is usable in tests, commands, or any renderer.
809
- * ## Ownership and lifetime
810
950
  * The returned Effect performs one RefSubject update when run.
811
951
  * @since 1.0.0
812
- * @category state
952
+ * @category State transitions
813
953
  */
814
954
  export function pushValue(state, name, value) {
815
955
  return RefSubject.update(state, (current) => {
@@ -825,12 +965,10 @@ export function pushValue(state, name, value) {
825
965
  /**
826
966
  * Removes one item by index from an array field and marks it dirty and touched.
827
967
  * @remarks
828
- * ## Why
829
968
  * The transition is explicit and renderer-independent; an out-of-range index leaves values unchanged.
830
- * ## Ownership and lifetime
831
969
  * The returned Effect performs one RefSubject update when run.
832
970
  * @since 1.0.0
833
- * @category state
971
+ * @category State transitions
834
972
  */
835
973
  export function removeValue(state, name, index) {
836
974
  return RefSubject.update(state, (current) => {
@@ -860,16 +998,14 @@ function formProps(options) {
860
998
  });
861
999
  }
862
1000
  /**
863
- * Renders a native form and provides its state to schema-bound descendants.
1001
+ * Renders a native form and provides its state to bound descendants.
1002
+ *
864
1003
  * @remarks
865
- * ## Why
866
- * Native submission is intercepted once, whole-form Schema validation runs,
867
- * then `onValidSubmit` receives decoded values. Reset updates the RefSubject so
868
- * all renderers stay coherent.
869
- * ## Ownership and lifetime
870
- * The root Scope owns its listeners/content and `CurrentForm` service. A valid
871
- * submit Effect runs in that lifetime; `submitting` is cleared in finalization
872
- * even on failure or interruption. Hydration does not itself attach client UI.
1004
+ * The submit listener synchronously prevents browser navigation, marks submitting, validates
1005
+ * decoded state, and runs onValidSubmit on success. Finalization clears submitting. Native
1006
+ * constraint validation can prevent submit dispatch before this handler runs. The root does not
1007
+ * read FormData, serialize a request, cancel duplicate submissions, or infer server errors.
1008
+ *
873
1009
  * @example
874
1010
  * ```ts
875
1011
  * import { Form, Submit, TextInput, makeState } from "@typed/ui/Form"
@@ -887,7 +1023,7 @@ function formProps(options) {
887
1023
  * })
888
1024
  * ```
889
1025
  * @since 1.0.0
890
- * @category components
1026
+ * @category Form roots
891
1027
  */
892
1028
  export function Form(options, host) {
893
1029
  const rendered = Dom.renderHost()(options, host, formProps(options), options.content, (props, content) => html `<form ...${props}>${content}</form>`);
@@ -896,32 +1032,51 @@ export function Form(options, host) {
896
1032
  });
897
1033
  }
898
1034
  /**
899
- * Creates a schema-bound form component family from one Struct codec.
1035
+ * Binds a native form component family to one Struct codec.
1036
+ *
900
1037
  * @remarks
901
- * ## Why
902
- * Defining the schema once removes repeated state/codec arguments and makes
903
- * incompatible field/component combinations compile-time errors.
904
- * ## Ownership and lifetime
905
- * Factory creation is pure and retains the codec. `state` requires Scope and
906
- * returns a hydrated RefSubject. `Root` provides that state through Effect
907
- * context only for its render lifetime; controls borrow it.
1038
+ * state accepts decoded defaults and allocates a Scope-owned hydrated subject. Root provides
1039
+ * that form through CurrentForm. Bound controls select compatible field names from both decoded
1040
+ * and encoded schema types; their context requirement remains in the Fx until Root provides it.
1041
+ * Use explicit stable IDs when server rendering and hydrating.
1042
+ *
908
1043
  * @example
909
1044
  * ```ts
910
- * import { make } from "@typed/ui/Form"
911
- * import { Effect, Schema } from "effect"
912
- * import { html } from "@typed/template"
1045
+ * import { Schema } from "effect";
1046
+ * import { html } from "@typed/template";
1047
+ * import { RefSubject } from "@typed/fx";
1048
+ * import { component } from "@typed/ui/Component";
1049
+ * import * as Form from "@typed/ui/Form";
913
1050
  *
914
- * const Signup = make(Schema.Struct({ email: Schema.String, accepted: Schema.Boolean }))
915
- * const view = Effect.gen(function* () {
916
- * const form = yield* Signup.state({ email: "", accepted: false }, { id: "signup" })
917
- * return Signup.Root({
1051
+ * const Order = Form.make(Schema.Struct({
1052
+ * copies: Schema.FiniteFromString.pipe(Schema.check(Schema.isGreaterThan(0))),
1053
+ * includeNotes: Schema.Boolean,
1054
+ * }));
1055
+ *
1056
+ * export const PrintOrder = component(function* () {
1057
+ * const form = yield* Order.state({ copies: 1, includeNotes: false }, { id: "print-order" });
1058
+ * const submitting = RefSubject.map(form, (state) => state.submitting);
1059
+ * const preview = yield* RefSubject.make("No print request preview yet.");
1060
+ * return html`<section>${Order.Root({
918
1061
  * form,
919
- * content: html`${Signup.EmailInput({ name: "email" })}${Signup.Checkbox({ name: "accepted" })}`
920
- * })
921
- * })
1062
+ * content: [
1063
+ * Order.Label({ for: "order-copies", content: "Copies" }),
1064
+ * Order.NumberInput({ name: "copies", props: { id: "order-copies", min: 1, required: true } }),
1065
+ * Order.Error({ name: "copies" }),
1066
+ * Order.Checkbox({ name: "includeNotes", props: { id: "order-notes" } }),
1067
+ * Order.Label({ for: "order-notes", content: "Include speaker notes" }),
1068
+ * Order.Submit({ content: "Preview print request", props: { "?disabled": submitting } }),
1069
+ * Order.Reset({ content: "Restore defaults" }),
1070
+ * ],
1071
+ * onValidSubmit: (values) => RefSubject.set(
1072
+ * preview,
1073
+ * `${values.copies} copies; notes ${values.includeNotes ? "included" : "excluded"}.`,
1074
+ * ),
1075
+ * })}<p role="status">${preview}</p></section>`;
1076
+ * });
922
1077
  * ```
923
1078
  * @since 1.0.0
924
- * @category constructors
1079
+ * @category Schema-bound components
925
1080
  */
926
1081
  export function make(codec) {
927
1082
  const state = (values, options = {}) => makeState(codec, { ...options, values });