@typed/ui 1.0.0-beta.4 → 1.0.0-beta.5

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 (157) hide show
  1. package/README.md +53 -9
  2. package/dist/Alert.d.ts +78 -0
  3. package/dist/Alert.d.ts.map +1 -0
  4. package/dist/Alert.js +37 -0
  5. package/dist/Button.d.ts +144 -0
  6. package/dist/Button.d.ts.map +1 -0
  7. package/dist/Button.js +38 -0
  8. package/dist/Carousel.d.ts +547 -0
  9. package/dist/Carousel.d.ts.map +1 -0
  10. package/dist/Carousel.js +387 -0
  11. package/dist/Checkbox.d.ts +203 -0
  12. package/dist/Checkbox.d.ts.map +1 -0
  13. package/dist/Checkbox.js +140 -0
  14. package/dist/Collection.d.ts +254 -0
  15. package/dist/Collection.d.ts.map +1 -0
  16. package/dist/Collection.js +218 -0
  17. package/dist/Combobox.d.ts +516 -0
  18. package/dist/Combobox.d.ts.map +1 -0
  19. package/dist/Combobox.js +371 -0
  20. package/dist/Component.d.ts +127 -0
  21. package/dist/Component.d.ts.map +1 -0
  22. package/dist/Component.js +60 -0
  23. package/dist/Composite.d.ts +823 -0
  24. package/dist/Composite.d.ts.map +1 -0
  25. package/dist/Composite.js +615 -0
  26. package/dist/Dialog.d.ts +544 -0
  27. package/dist/Dialog.d.ts.map +1 -0
  28. package/dist/Dialog.js +355 -0
  29. package/dist/Disclosure.d.ts +219 -0
  30. package/dist/Disclosure.d.ts.map +1 -0
  31. package/dist/Disclosure.js +128 -0
  32. package/dist/Dom/Events.d.ts +122 -0
  33. package/dist/Dom/Events.d.ts.map +1 -0
  34. package/dist/Dom/Events.js +192 -0
  35. package/dist/Dom/Props.d.ts +161 -0
  36. package/dist/Dom/Props.d.ts.map +1 -0
  37. package/dist/Dom/Props.js +110 -0
  38. package/dist/Dom/Refs.d.ts +58 -0
  39. package/dist/Dom/Refs.d.ts.map +1 -0
  40. package/dist/Dom/Refs.js +61 -0
  41. package/dist/Dom/Render.d.ts +59 -0
  42. package/dist/Dom/Render.d.ts.map +1 -0
  43. package/dist/Dom/Render.js +71 -0
  44. package/dist/Dom/Types.d.ts +570 -0
  45. package/dist/Dom/Types.d.ts.map +1 -0
  46. package/dist/Dom/Types.js +1 -0
  47. package/dist/Dom/index.d.ts +20 -0
  48. package/dist/Dom/index.d.ts.map +1 -0
  49. package/dist/Dom/index.js +8 -0
  50. package/dist/Dom.d.ts +14 -0
  51. package/dist/Dom.d.ts.map +1 -0
  52. package/dist/Dom.js +13 -0
  53. package/dist/Focusable.d.ts +85 -0
  54. package/dist/Focusable.d.ts.map +1 -0
  55. package/dist/Focusable.js +35 -0
  56. package/dist/Form.d.ts +1695 -0
  57. package/dist/Form.d.ts.map +1 -0
  58. package/dist/Form.js +987 -0
  59. package/dist/Grid.d.ts +569 -0
  60. package/dist/Grid.d.ts.map +1 -0
  61. package/dist/Grid.js +379 -0
  62. package/dist/Group.d.ts +147 -0
  63. package/dist/Group.d.ts.map +1 -0
  64. package/dist/Group.js +63 -0
  65. package/dist/Heading.d.ts +86 -0
  66. package/dist/Heading.d.ts.map +1 -0
  67. package/dist/Heading.js +48 -0
  68. package/dist/Hovercard.d.ts +297 -0
  69. package/dist/Hovercard.d.ts.map +1 -0
  70. package/dist/Hovercard.js +188 -0
  71. package/dist/HttpRouter.d.ts +129 -6
  72. package/dist/HttpRouter.d.ts.map +1 -1
  73. package/dist/HttpRouter.js +196 -53
  74. package/dist/Link.d.ts +67 -28
  75. package/dist/Link.d.ts.map +1 -1
  76. package/dist/Link.js +91 -37
  77. package/dist/Listbox.d.ts +437 -0
  78. package/dist/Listbox.d.ts.map +1 -0
  79. package/dist/Listbox.js +316 -0
  80. package/dist/Menu.d.ts +972 -0
  81. package/dist/Menu.d.ts.map +1 -0
  82. package/dist/Menu.js +731 -0
  83. package/dist/Menubar.d.ts +367 -0
  84. package/dist/Menubar.d.ts.map +1 -0
  85. package/dist/Menubar.js +265 -0
  86. package/dist/Meter.d.ts +217 -0
  87. package/dist/Meter.d.ts.map +1 -0
  88. package/dist/Meter.js +94 -0
  89. package/dist/NativeDetails.d.ts +41 -0
  90. package/dist/NativeDetails.d.ts.map +1 -0
  91. package/dist/NativeDetails.js +40 -0
  92. package/dist/NativeDialog.d.ts +64 -0
  93. package/dist/NativeDialog.d.ts.map +1 -0
  94. package/dist/NativeDialog.js +50 -0
  95. package/dist/NativePopover.d.ts +41 -0
  96. package/dist/NativePopover.d.ts.map +1 -0
  97. package/dist/NativePopover.js +46 -0
  98. package/dist/Popover.d.ts +241 -0
  99. package/dist/Popover.d.ts.map +1 -0
  100. package/dist/Popover.js +141 -0
  101. package/dist/RadioGroup.d.ts +432 -0
  102. package/dist/RadioGroup.d.ts.map +1 -0
  103. package/dist/RadioGroup.js +291 -0
  104. package/dist/Role.d.ts +64 -0
  105. package/dist/Role.d.ts.map +1 -0
  106. package/dist/Role.js +27 -0
  107. package/dist/Select.d.ts +529 -0
  108. package/dist/Select.d.ts.map +1 -0
  109. package/dist/Select.js +407 -0
  110. package/dist/Separator.d.ts +54 -0
  111. package/dist/Separator.d.ts.map +1 -0
  112. package/dist/Separator.js +26 -0
  113. package/dist/Slider.d.ts +188 -0
  114. package/dist/Slider.d.ts.map +1 -0
  115. package/dist/Slider.js +96 -0
  116. package/dist/SpinButton.d.ts +188 -0
  117. package/dist/SpinButton.d.ts.map +1 -0
  118. package/dist/SpinButton.js +96 -0
  119. package/dist/Storybook.d.ts +76 -0
  120. package/dist/Storybook.d.ts.map +1 -0
  121. package/dist/Storybook.js +102 -0
  122. package/dist/Switch.d.ts +187 -0
  123. package/dist/Switch.d.ts.map +1 -0
  124. package/dist/Switch.js +116 -0
  125. package/dist/Tab.d.ts +26 -0
  126. package/dist/Tab.d.ts.map +1 -0
  127. package/dist/Tab.js +25 -0
  128. package/dist/Tabs.d.ts +591 -0
  129. package/dist/Tabs.d.ts.map +1 -0
  130. package/dist/Tabs.js +346 -0
  131. package/dist/Toolbar.d.ts +366 -0
  132. package/dist/Toolbar.d.ts.map +1 -0
  133. package/dist/Toolbar.js +245 -0
  134. package/dist/Tooltip.d.ts +296 -0
  135. package/dist/Tooltip.d.ts.map +1 -0
  136. package/dist/Tooltip.js +172 -0
  137. package/dist/Tree.d.ts +591 -0
  138. package/dist/Tree.d.ts.map +1 -0
  139. package/dist/Tree.js +434 -0
  140. package/dist/TreeGrid.d.ts +645 -0
  141. package/dist/TreeGrid.d.ts.map +1 -0
  142. package/dist/TreeGrid.js +420 -0
  143. package/dist/VisuallyHidden.d.ts +54 -0
  144. package/dist/VisuallyHidden.d.ts.map +1 -0
  145. package/dist/VisuallyHidden.js +27 -0
  146. package/dist/WindowSplitter.d.ts +376 -0
  147. package/dist/WindowSplitter.d.ts.map +1 -0
  148. package/dist/WindowSplitter.js +222 -0
  149. package/dist/index.d.ts +49 -0
  150. package/dist/index.d.ts.map +1 -1
  151. package/dist/index.js +49 -0
  152. package/package.json +45 -18
  153. package/src/HttpRouter.test.ts +0 -256
  154. package/src/HttpRouter.ts +0 -165
  155. package/src/Link.test.ts +0 -84
  156. package/src/Link.ts +0 -107
  157. package/src/index.ts +0 -2
package/dist/Form.js ADDED
@@ -0,0 +1,987 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Context from "effect/Context";
3
+ import * as Schema from "effect/Schema";
4
+ import * as SchemaIssue from "effect/SchemaIssue";
5
+ import * as SchemaTransformation from "effect/SchemaTransformation";
6
+ import { Fx as FxApi, RefSubject } from "@typed/fx";
7
+ import { EventHandler, html, } from "@typed/template";
8
+ import * as Dom from "./Dom.js";
9
+ let nextFormId = 0;
10
+ /**
11
+ * Effect context service used by schema-bound form controls.
12
+ *
13
+ * @remarks
14
+ * ## Why
15
+ * Bound controls avoid threading `state` through every call while their
16
+ * service requirement remains visible in the Fx type.
17
+ *
18
+ * ## Ownership and lifetime
19
+ * `Form` provides the service for its child render lifetime; use outside that
20
+ * boundary fails with the ordinary Effect missing-service defect.
21
+ *
22
+ * @since 1.0.0
23
+ * @category services
24
+ */
25
+ export const CurrentForm = Context.Service("@typed/ui/Form/CurrentForm");
26
+ function withCurrentForm() {
27
+ return (f) => FxApi.gen(function* () {
28
+ const current = yield* CurrentForm;
29
+ return f(current.state);
30
+ });
31
+ }
32
+ const FieldMetaSchema = Schema.Struct({
33
+ dirty: Schema.Boolean,
34
+ touched: Schema.Boolean,
35
+ });
36
+ function optionalFields(fields, value) {
37
+ return Object.fromEntries(Object.keys(fields).map((key) => [key, Schema.optionalKey(value)]));
38
+ }
39
+ function emptyOptionalFields() {
40
+ return {};
41
+ }
42
+ /**
43
+ * Builds the serializable schema for a form's hydrated state.
44
+ *
45
+ * @remarks
46
+ * ## Why
47
+ * The hydration payload needs validation independent from runtime-only field codecs.
48
+ *
49
+ * ## Ownership and lifetime
50
+ * Pure schema construction; the returned Schema acquires no Scope or subscription.
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * import { StateSchema } from "@typed/ui/Form"
55
+ * import { Schema } from "effect"
56
+ *
57
+ * const codec = Schema.Struct({ email: Schema.String })
58
+ * const stateCodec = StateSchema(codec)
59
+ * ```
60
+ *
61
+ * @since 1.0.0
62
+ * @category schemas
63
+ */
64
+ export function StateSchema(codec) {
65
+ return Schema.Struct({
66
+ values: codec,
67
+ defaultValues: codec,
68
+ errors: Schema.Struct(optionalFields(codec.fields, Schema.String)),
69
+ meta: Schema.Struct(optionalFields(codec.fields, FieldMetaSchema)),
70
+ submitting: Schema.Boolean,
71
+ });
72
+ }
73
+ /**
74
+ * Creates a Scope-owned hydrated form RefSubject from a Struct codec.
75
+ *
76
+ * @remarks
77
+ * ## Why
78
+ * One constructor establishes values, defaults, validation state, field codecs,
79
+ * and hydration identity consistently.
80
+ *
81
+ * ## Ownership and lifetime
82
+ * Requires `Scope.Scope`. Provide an explicit `id` during SSR; the counter-based
83
+ * fallback is process/order dependent. Only state data hydrates—`codec` and
84
+ * `fields` are reattached from the live Struct on each runtime.
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * import { makeState } from "@typed/ui/Form"
89
+ * import { Effect, Schema } from "effect"
90
+ *
91
+ * const codec = Schema.Struct({ email: Schema.String })
92
+ * const program = Effect.gen(function* () {
93
+ * const state = yield* makeState(codec, { id: "signup", values: { email: "" } })
94
+ * return state
95
+ * })
96
+ * ```
97
+ *
98
+ * @since 1.0.0
99
+ * @category constructors
100
+ */
101
+ export function makeState(codec, initial) {
102
+ const id = initial.id ?? `typed-form-${++nextFormId}`;
103
+ return Effect.map(RefSubject.hydrate(StateSchema(codec), {
104
+ values: initial.values,
105
+ defaultValues: initial.defaultValues ?? initial.values,
106
+ errors: initial.errors ?? emptyOptionalFields(),
107
+ meta: initial.meta ?? emptyOptionalFields(),
108
+ submitting: initial.submitting ?? false,
109
+ }), (state) => Object.assign(state, { codec, fields: codec.fields, id }));
110
+ }
111
+ function setFieldError(state, key, error) {
112
+ return RefSubject.update(state, (current) => {
113
+ const errors = { ...current.errors };
114
+ if (error === undefined)
115
+ delete errors[key];
116
+ else
117
+ errors[key] = error;
118
+ return { ...current, errors };
119
+ });
120
+ }
121
+ function fieldErrorId(state, name) {
122
+ return `${state.id}-${encodeURIComponent(name)}-error`;
123
+ }
124
+ function decodeField(state, name, codec, value) {
125
+ return Schema.decodeEffect(codec)(value).pipe(Effect.flatMap((decoded) => Effect.andThen(updateDecodedValue(state, name, decoded), () => setFieldError(state, name, undefined))), Effect.catch((error) => setFieldError(state, name, error.message)));
126
+ }
127
+ function decodeUpdatedField(state, name, encoded) {
128
+ 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
+ }
130
+ function inputProps(options, type, codec) {
131
+ return () => ({
132
+ type,
133
+ name: options.name,
134
+ "aria-describedby": RefSubject.map(options.state, (state) => state.errors[options.name] === undefined
135
+ ? undefined
136
+ : fieldErrorId(options.state, options.name)),
137
+ "aria-invalid": RefSubject.map(options.state, (state) => state.errors[options.name] === undefined ? undefined : true),
138
+ ".value": RefSubject.mapEffect(options.state, (state) => Schema.encodeUnknownEffect(codec)(state.values[options.name])),
139
+ oninput: EventHandler.make(Effect.fn((event) => decodeField(options.state, options.name, codec, Dom.currentTarget(event).value))),
140
+ });
141
+ }
142
+ function renderInput(options, host, type, defaultCodec) {
143
+ const codec = options.codec ?? defaultCodec;
144
+ return Dom.renderHost()(options, host, inputProps(options, type, codec), "", (props) => html `<input ...${props} />`);
145
+ }
146
+ function makeInput(type, codec) {
147
+ return function (options, host) {
148
+ return renderInput(options, host, type, codec);
149
+ };
150
+ }
151
+ function makeSchemaBoundInput(formCodec, type) {
152
+ return function (options, host) {
153
+ return withCurrentForm()((state) => {
154
+ const inputOptions = { ...options, state };
155
+ const fieldCodec = formCodec.fields[options.name];
156
+ return renderInput(inputOptions, host, type, fieldCodec);
157
+ });
158
+ };
159
+ }
160
+ /**
161
+ * Binds a native `input[type=text]` to a string field.
162
+ *
163
+ * @remarks
164
+ * ## Why
165
+ * The control uses the browser's real input event and a Schema codec while
166
+ * keeping state independently testable.
167
+ *
168
+ * ## Ownership and lifetime
169
+ * The control Scope owns DOM listeners/subscriptions; the supplied `FormState`
170
+ * may outlive the rendered input. A custom host must apply all merged props.
171
+ *
172
+ * @example
173
+ * ```ts
174
+ * import { TextInput, makeState } from "@typed/ui/Form"
175
+ * import { Effect, Schema } from "effect"
176
+ *
177
+ * const codec = Schema.Struct({ name: Schema.String })
178
+ * const input = Effect.gen(function* () {
179
+ * const state = yield* makeState(codec, { values: { name: "" } })
180
+ * return TextInput({ state, name: "name" })
181
+ * })
182
+ * ```
183
+ *
184
+ * @since 1.0.0
185
+ * @category components
186
+ */
187
+ export const TextInput = makeInput("text", Schema.String);
188
+ /**
189
+ * Binds a native search input to a string field.
190
+ * @remarks
191
+ * ## Why
192
+ * 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
+ * @since 1.0.0
196
+ * @category components
197
+ */
198
+ export const SearchInput = makeInput("search", Schema.String);
199
+ /**
200
+ * Binds a native email input to a string field.
201
+ * @remarks
202
+ * ## Why
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
+ * @since 1.0.0
207
+ * @category components
208
+ */
209
+ export const EmailInput = makeInput("email", Schema.String);
210
+ /**
211
+ * Binds a native URL input to a string field.
212
+ * @remarks
213
+ * ## Why
214
+ * 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
+ * @since 1.0.0
218
+ * @category components
219
+ */
220
+ export const UrlInput = makeInput("url", Schema.String);
221
+ /**
222
+ * Binds a native telephone input to a string field.
223
+ * @remarks
224
+ * ## Why
225
+ * Preserves platform telephone keyboards and autocomplete behavior.
226
+ * ## Ownership and lifetime
227
+ * DOM work is control-Scope-owned; form state remains independently owned.
228
+ * @since 1.0.0
229
+ * @category components
230
+ */
231
+ export const TelInput = makeInput("tel", Schema.String);
232
+ /**
233
+ * Binds a native password input to a string field.
234
+ * @remarks
235
+ * ## Why
236
+ * 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
+ * @since 1.0.0
240
+ * @category components
241
+ */
242
+ export const PasswordInput = makeInput("password", Schema.String);
243
+ /**
244
+ * Binds a native hidden input to a string field.
245
+ * @remarks
246
+ * ## Why
247
+ * 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
+ * @since 1.0.0
251
+ * @category components
252
+ */
253
+ export const HiddenInput = makeInput("hidden", Schema.String);
254
+ /**
255
+ * Binds a native color input to a string field.
256
+ * @remarks
257
+ * ## Why
258
+ * 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
+ * @since 1.0.0
262
+ * @category components
263
+ */
264
+ export const ColorInput = makeInput("color", Schema.String);
265
+ /**
266
+ * Binds a native time input to a string field.
267
+ * @remarks
268
+ * ## Why
269
+ * 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
+ * @since 1.0.0
273
+ * @category components
274
+ */
275
+ export const TimeInput = makeInput("time", Schema.String);
276
+ /**
277
+ * Binds a native local date-time input to a string field.
278
+ * @remarks
279
+ * ## Why
280
+ * 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
+ * @since 1.0.0
284
+ * @category components
285
+ */
286
+ export const DateTimeLocalInput = makeInput("datetime-local", Schema.String);
287
+ /**
288
+ * Binds a native month input to a string field.
289
+ * @remarks
290
+ * ## Why
291
+ * 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
+ * @since 1.0.0
295
+ * @category components
296
+ */
297
+ export const MonthInput = makeInput("month", Schema.String);
298
+ /**
299
+ * Binds a native week input to a string field.
300
+ * @remarks
301
+ * ## Why
302
+ * 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
+ * @since 1.0.0
306
+ * @category components
307
+ */
308
+ export const WeekInput = makeInput("week", Schema.String);
309
+ /**
310
+ * Binds a native number input to a finite number field.
311
+ * @remarks
312
+ * ## Why
313
+ * `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
+ * @since 1.0.0
317
+ * @category components
318
+ */
319
+ export const NumberInput = makeInput("number", Schema.FiniteFromString);
320
+ /**
321
+ * Binds a native range input to a finite number field.
322
+ * @remarks
323
+ * ## Why
324
+ * 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
+ * @since 1.0.0
328
+ * @category components
329
+ */
330
+ export const RangeInput = makeInput("range", Schema.FiniteFromString);
331
+ /**
332
+ * Binds a native date input to a `Date` field.
333
+ * @remarks
334
+ * ## Why
335
+ * `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
+ * @since 1.0.0
339
+ * @category components
340
+ */
341
+ export const DateInput = makeInput("date", Schema.DateFromString);
342
+ /**
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.
350
+ * @example
351
+ * ```ts
352
+ * import { formDataToRecord } from "@typed/ui/Form"
353
+ *
354
+ * const data = new FormData()
355
+ * data.append("tag", "one")
356
+ * data.append("tag", "two")
357
+ * const record = formDataToRecord(data)
358
+ * ```
359
+ * @since 1.0.0
360
+ * @category conversions
361
+ */
362
+ export function formDataToRecord(data) {
363
+ const result = {};
364
+ for (const [name, value] of data.entries()) {
365
+ const existing = result[name];
366
+ result[name] =
367
+ existing === undefined
368
+ ? value
369
+ : Array.isArray(existing)
370
+ ? [...existing, value]
371
+ : [existing, value];
372
+ }
373
+ return result;
374
+ }
375
+ /**
376
+ * Decodes native FormData through an Effect Schema codec.
377
+ * @remarks
378
+ * ## Why
379
+ * Browser serialization, repeated values, Files, and typed validation meet at
380
+ * one explicit fallible boundary.
381
+ * ## Ownership and lifetime
382
+ * The returned Effect is lazy and owns no browser resource; it references File
383
+ * objects present in the supplied FormData.
384
+ * @example
385
+ * ```ts
386
+ * import { decodeFormData } from "@typed/ui/Form"
387
+ * import { Schema } from "effect"
388
+ *
389
+ * const decode = decodeFormData(Schema.Struct({ name: Schema.String }), new FormData())
390
+ * ```
391
+ * @since 1.0.0
392
+ * @category conversions
393
+ */
394
+ export function decodeFormData(codec, data) {
395
+ return Schema.decodeEffect(codec)(formDataToRecord(data));
396
+ }
397
+ /**
398
+ * Validates current form values and synchronizes decoded values or field errors.
399
+ * @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`.
405
+ * @since 1.0.0
406
+ * @category validation
407
+ */
408
+ export function validate(state) {
409
+ return Effect.flatMap(state, (current) => Schema.decodeUnknownEffect(Schema.toType(state.codec))(current.values)).pipe(Effect.flatMap((values) => Effect.as(RefSubject.update(state, (current) => ({
410
+ ...current,
411
+ values,
412
+ errors: {},
413
+ })), values)), Effect.catch((error) => RefSubject.update(state, (current) => ({
414
+ ...current,
415
+ errors: formErrors(current.values, current.errors, error.message),
416
+ })).pipe(Effect.andThen(Effect.fail(error)))));
417
+ }
418
+ function formErrors(values, errors, error) {
419
+ const next = { ...errors };
420
+ for (const key of Object.keys(values))
421
+ Reflect.set(next, key, error);
422
+ return next;
423
+ }
424
+ /**
425
+ * Creates a named, Schema-decoded mask slot.
426
+ * @remarks
427
+ * ## Why
428
+ * Length and character constraints are expressed beside the codec that owns conversion.
429
+ * ## Ownership and lifetime
430
+ * Pure constructor; the returned descriptor retains the codec but acquires no Scope.
431
+ * @example
432
+ * ```ts
433
+ * import { slot } from "@typed/ui/Form"
434
+ * import { Schema } from "effect"
435
+ *
436
+ * const areaCode = slot("area", Schema.String, { length: 3, charset: /[0-9]/ })
437
+ * ```
438
+ * @since 1.0.0
439
+ * @category constructors
440
+ */
441
+ export function slot(name, codec, options = {}) {
442
+ return { _tag: "MaskSlot", name, codec, ...options };
443
+ }
444
+ /**
445
+ * Builds a bidirectional Schema codec from literal text and named slots.
446
+ * @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.
452
+ * @example
453
+ * ```ts
454
+ * import { mask, slot } from "@typed/ui/Form"
455
+ * import { Schema } from "effect"
456
+ *
457
+ * const phone = mask("(", slot("area", Schema.String, { length: 3 }), ") ",
458
+ * slot("number", Schema.String, { length: 7 }))
459
+ * ```
460
+ * @since 1.0.0
461
+ * @category schemas
462
+ */
463
+ export function mask(...parts) {
464
+ const valueSchema = Schema.declare((value) => typeof value === "object" &&
465
+ value !== null &&
466
+ parts.every((part) => typeof part === "string" || Reflect.has(value, part.name)));
467
+ return Schema.String.pipe(Schema.decodeTo(valueSchema, SchemaTransformation.transformOrFail({
468
+ decode: (display, options) => decodeMask(parts, display, options),
469
+ encode: (value, options) => encodeMask(parts, value, options),
470
+ })));
471
+ }
472
+ function decodeMask(parts, display, options) {
473
+ return Effect.gen(function* () {
474
+ const value = {};
475
+ let offset = 0;
476
+ for (let index = 0; index < parts.length; index++) {
477
+ const part = parts[index];
478
+ if (typeof part === "string") {
479
+ if (!display.startsWith(part, offset))
480
+ return yield* invalidMask(display, options);
481
+ offset += part.length;
482
+ continue;
483
+ }
484
+ const nextLiteral = parts.slice(index + 1).find((next) => typeof next === "string");
485
+ const end = part.length === undefined
486
+ ? nextLiteral === undefined
487
+ ? display.length
488
+ : display.indexOf(nextLiteral, offset)
489
+ : offset + part.length;
490
+ if (end < offset)
491
+ return yield* invalidMask(display, options);
492
+ const encoded = display.slice(offset, end);
493
+ if (encoded.length === 0 ||
494
+ (part.length !== undefined && encoded.length !== part.length) ||
495
+ [...encoded].some((character) => !matchesCharset(part, character)))
496
+ return yield* invalidMask(display, options);
497
+ const decoded = yield* Schema.decodeEffect(part.codec)(encoded).pipe(Effect.mapError(() => new SchemaIssue.InvalidValue({ message: `Invalid ${part.name}` }, display, options)));
498
+ Reflect.set(value, part.name, decoded);
499
+ offset = end;
500
+ }
501
+ if (offset !== display.length || !isMaskValue(parts, value))
502
+ return yield* invalidMask(display, options);
503
+ return value;
504
+ });
505
+ }
506
+ function encodeMask(parts, value, options) {
507
+ return Effect.gen(function* () {
508
+ let display = "";
509
+ for (const part of parts) {
510
+ if (typeof part === "string") {
511
+ display += part;
512
+ continue;
513
+ }
514
+ const encoded = yield* Schema.encodeUnknownEffect(part.codec)(Reflect.get(value, part.name)).pipe(Effect.mapError(() => new SchemaIssue.InvalidValue({ message: `Invalid ${part.name}` }, value, options)));
515
+ if (encoded.length === 0 ||
516
+ (part.length !== undefined && encoded.length !== part.length) ||
517
+ [...encoded].some((character) => !matchesCharset(part, character)))
518
+ return yield* invalidMask(value, options);
519
+ display += encoded;
520
+ }
521
+ return display;
522
+ });
523
+ }
524
+ function isMaskValue(parts, value) {
525
+ return (typeof value === "object" &&
526
+ value !== null &&
527
+ parts.every((part) => typeof part === "string" || Reflect.has(value, part.name)));
528
+ }
529
+ function matchesCharset(slot, character) {
530
+ if (slot.charset === undefined)
531
+ return true;
532
+ if (slot.charset instanceof RegExp) {
533
+ slot.charset.lastIndex = 0;
534
+ return slot.charset.test(character);
535
+ }
536
+ return slot.charset(character);
537
+ }
538
+ function invalidMask(input, options) {
539
+ return Effect.fail(new SchemaIssue.InvalidValue({ message: "Invalid mask" }, input, options));
540
+ }
541
+ /**
542
+ * Binds a native text input to a structured mask value.
543
+ * @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.
550
+ * @since 1.0.0
551
+ * @category components
552
+ */
553
+ export function MaskedInput(options, host) {
554
+ const { mask, ...inputOptions } = options;
555
+ return renderInput(inputOptions, host, "text", mask);
556
+ }
557
+ function checkboxProps(options) {
558
+ const checked = RefSubject.map(options.state, (state) => state.values[options.name] === true);
559
+ return () => ({
560
+ type: "checkbox",
561
+ name: options.name,
562
+ "aria-describedby": RefSubject.map(options.state, (state) => state.errors[options.name] === undefined
563
+ ? undefined
564
+ : fieldErrorId(options.state, options.name)),
565
+ "aria-invalid": RefSubject.map(options.state, (state) => state.errors[options.name] === undefined ? undefined : true),
566
+ "?checked": checked,
567
+ ".checked": checked,
568
+ onchange: EventHandler.make(Effect.fn((event) => decodeUpdatedField(options.state, options.name, Dom.currentTarget(event).checked))),
569
+ });
570
+ }
571
+ /**
572
+ * Binds a native checkbox to a boolean form field.
573
+ * @remarks
574
+ * ## Why
575
+ * Both the checked attribute and live property follow state, while the browser's
576
+ * real change event is decoded through the field codec.
577
+ * ## Ownership and lifetime
578
+ * The rendered Scope owns the input, listener, and subscriptions. A custom host
579
+ * must apply merged name, ARIA, checked, and change props.
580
+ * @since 1.0.0
581
+ * @category components
582
+ */
583
+ export function Checkbox(options, host) {
584
+ return Dom.renderHost()(options, host, checkboxProps(options), "", (props) => html `<input ...${props} />`);
585
+ }
586
+ function selectProps(options) {
587
+ return () => ({
588
+ name: options.name,
589
+ "aria-describedby": RefSubject.map(options.state, (state) => state.errors[options.name] === undefined
590
+ ? undefined
591
+ : fieldErrorId(options.state, options.name)),
592
+ "aria-invalid": RefSubject.map(options.state, (state) => state.errors[options.name] === undefined ? undefined : true),
593
+ ".value": RefSubject.map(options.state, (state) => String(state.values[options.name])),
594
+ onchange: EventHandler.make(Effect.fn((event) => decodeUpdatedField(options.state, options.name, Dom.currentTarget(event).value))),
595
+ });
596
+ }
597
+ /**
598
+ * Binds a native select element to a string form field.
599
+ * @remarks
600
+ * ## Why
601
+ * Native keyboard, accessibility, option, and form semantics remain browser-owned.
602
+ * ## Ownership and lifetime
603
+ * The rendered Scope owns the select/content subscriptions. A custom host must
604
+ * preserve supplied name, ARIA, value, and change props.
605
+ * @since 1.0.0
606
+ * @category components
607
+ */
608
+ export function Select(options, host) {
609
+ return Dom.renderHost()(options, host, selectProps(options), options.content, (props, content) => html `<select ...${props}>
610
+ ${content}
611
+ </select>`);
612
+ }
613
+ function labelProps(options) {
614
+ return () => ({ for: options.for });
615
+ }
616
+ /**
617
+ * Renders a native label with an explicit control relationship.
618
+ * @remarks
619
+ * ## Why
620
+ * The browser supplies click-to-focus and accessible-name behavior with no synthetic layer.
621
+ * ## Ownership and lifetime
622
+ * The Scope owns label output/content; the referenced element remains separately owned.
623
+ * @since 1.0.0
624
+ * @category components
625
+ */
626
+ export function Label(options, host) {
627
+ return Dom.renderHost()(options, host, labelProps(options), options.content, (props, content) => html `<label ...${props}>${content}</label>`);
628
+ }
629
+ function descriptionProps() {
630
+ return () => ({});
631
+ }
632
+ /**
633
+ * Renders form description content in a neutral native div by default.
634
+ * @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.
639
+ * @since 1.0.0
640
+ * @category components
641
+ */
642
+ export function Description(options, host) {
643
+ return Dom.renderHost()(options, host, descriptionProps(), options.content, (props, content) => html `<div ...${props}>${content}</div>`);
644
+ }
645
+ function errorProps(options) {
646
+ return () => ({
647
+ id: fieldErrorId(options.state, options.name),
648
+ role: "alert",
649
+ });
650
+ }
651
+ /**
652
+ * Renders the current field error as a native ARIA alert region.
653
+ * @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.
659
+ * @since 1.0.0
660
+ * @category components
661
+ */
662
+ export function Error(options, host) {
663
+ const content = RefSubject.map(options.state, (state) => state.errors[options.name] ?? "");
664
+ return Dom.renderHost()(options, host, errorProps(options), content, (props, value) => html `<div ...${props}>${value}</div>`);
665
+ }
666
+ function submitProps() {
667
+ return () => ({ type: "submit" });
668
+ }
669
+ /**
670
+ * Renders a native `type=submit` button.
671
+ * @remarks
672
+ * ## Why
673
+ * Keyboard activation, form association, and accessibility stay browser-standard.
674
+ * ## Ownership and lifetime
675
+ * The Scope owns the button/content; the surrounding Form owns submission sequencing.
676
+ * @since 1.0.0
677
+ * @category components
678
+ */
679
+ export function Submit(options, host) {
680
+ return Dom.renderHost()(options, host, submitProps(), options.content, (props, content) => html `<button ...${props}>${content}</button>`);
681
+ }
682
+ function resetProps(options) {
683
+ return () => ({
684
+ type: "reset",
685
+ onclick: EventHandler.preventDefault(EventHandler.fromEffectOrEventHandler(reset(options.state))),
686
+ });
687
+ }
688
+ /**
689
+ * Renders a native reset button that restores Typed form defaults.
690
+ * @remarks
691
+ * ## Why
692
+ * It prevents the browser's independent control mutation and resets the single
693
+ * RefSubject source of truth, clearing errors, metadata, and submitting state.
694
+ * ## Ownership and lifetime
695
+ * The Scope owns the click handler/content. The supplied form state remains independently owned.
696
+ * @since 1.0.0
697
+ * @category components
698
+ */
699
+ export function Reset(options, host) {
700
+ return Dom.renderHost()(options, host, resetProps(options), options.content, (props, content) => html `<button ...${props}>${content}</button>`);
701
+ }
702
+ function groupProps(options) {
703
+ return () => ({ role: "group", "aria-label": options.label });
704
+ }
705
+ /**
706
+ * Renders an ARIA group with an optional accessible name.
707
+ * @remarks
708
+ * ## Why
709
+ * Related controls can expose their relationship without a framework-specific wrapper.
710
+ * ## Ownership and lifetime
711
+ * The Scope owns host/content; child controls retain their own DOM/state contracts.
712
+ * @since 1.0.0
713
+ * @category components
714
+ */
715
+ export function Group(options, host) {
716
+ return Dom.renderHost()(options, host, groupProps(options), options.content, (props, content) => html `<div ...${props}>${content}</div>`);
717
+ }
718
+ function pushProps(options) {
719
+ return () => ({
720
+ type: "button",
721
+ onclick: pushValue(options.state, options.name, options.value),
722
+ });
723
+ }
724
+ /**
725
+ * Renders a button that appends one item to an array field.
726
+ * @remarks
727
+ * ## Why
728
+ * Array mutation is immutable, typed, and marks the field dirty/touched.
729
+ * ## Ownership and lifetime
730
+ * The Scope owns the button handler/content; state may outlive the button.
731
+ * @since 1.0.0
732
+ * @category components
733
+ */
734
+ export function Push(options, host) {
735
+ return Dom.renderHost()(options, host, pushProps(options), options.content, (props, content) => html `<button ...${props}>${content}</button>`);
736
+ }
737
+ function removeProps(options) {
738
+ return () => ({
739
+ type: "button",
740
+ onclick: removeValue(options.state, options.name, options.index),
741
+ });
742
+ }
743
+ /**
744
+ * Renders a button that removes one array item by index.
745
+ * @remarks
746
+ * ## Why
747
+ * Array mutation is immutable, typed, and marks the field dirty/touched.
748
+ * ## Ownership and lifetime
749
+ * The Scope owns the button handler/content; state may outlive the button.
750
+ * @since 1.0.0
751
+ * @category components
752
+ */
753
+ export function Remove(options, host) {
754
+ return Dom.renderHost()(options, host, removeProps(options), options.content, (props, content) => html `<button ...${props}>${content}</button>`);
755
+ }
756
+ /**
757
+ * Sets one decoded field value and updates dirty/touched metadata.
758
+ * @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.
763
+ * @since 1.0.0
764
+ * @category state
765
+ */
766
+ export function setValue(state, key, value) {
767
+ return updateDecodedValue(state, key, value);
768
+ }
769
+ function updateDecodedValue(state, key, value) {
770
+ return RefSubject.update(state, (current) => ({
771
+ ...current,
772
+ values: updateRecord(current.values, key, value),
773
+ meta: {
774
+ ...current.meta,
775
+ [key]: { dirty: current.defaultValues[key] !== value, touched: true },
776
+ },
777
+ }));
778
+ }
779
+ function updateRecord(values, key, value) {
780
+ const updated = { ...values };
781
+ Reflect.set(updated, key, value);
782
+ return updated;
783
+ }
784
+ /**
785
+ * Restores default values and clears errors, interaction metadata, and submission state.
786
+ * @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`.
792
+ * @since 1.0.0
793
+ * @category state
794
+ */
795
+ export function reset(state) {
796
+ return RefSubject.update(state, (current) => ({
797
+ ...current,
798
+ values: current.defaultValues,
799
+ errors: {},
800
+ meta: {},
801
+ submitting: false,
802
+ }));
803
+ }
804
+ /**
805
+ * Appends one item to an array field and marks it dirty and touched.
806
+ * @remarks
807
+ * ## Why
808
+ * The immutable state transition is usable in tests, commands, or any renderer.
809
+ * ## Ownership and lifetime
810
+ * The returned Effect performs one RefSubject update when run.
811
+ * @since 1.0.0
812
+ * @category state
813
+ */
814
+ export function pushValue(state, name, value) {
815
+ return RefSubject.update(state, (current) => {
816
+ const previous = Reflect.get(current.values, name);
817
+ const values = Array.isArray(previous) ? [...previous, value] : [value];
818
+ return {
819
+ ...current,
820
+ values: updateRecord(current.values, name, values),
821
+ meta: { ...current.meta, [name]: { dirty: true, touched: true } },
822
+ };
823
+ });
824
+ }
825
+ /**
826
+ * Removes one item by index from an array field and marks it dirty and touched.
827
+ * @remarks
828
+ * ## Why
829
+ * The transition is explicit and renderer-independent; an out-of-range index leaves values unchanged.
830
+ * ## Ownership and lifetime
831
+ * The returned Effect performs one RefSubject update when run.
832
+ * @since 1.0.0
833
+ * @category state
834
+ */
835
+ export function removeValue(state, name, index) {
836
+ return RefSubject.update(state, (current) => {
837
+ const previous = Reflect.get(current.values, name);
838
+ const values = Array.isArray(previous)
839
+ ? previous.filter((_, currentIndex) => currentIndex !== index)
840
+ : [];
841
+ return {
842
+ ...current,
843
+ values: updateRecord(current.values, name, values),
844
+ meta: { ...current.meta, [name]: { dirty: true, touched: true } },
845
+ };
846
+ });
847
+ }
848
+ function formProps(options) {
849
+ const setSubmitting = Effect.fn((submitting) => RefSubject.update(options.state, (current) => ({ ...current, submitting })));
850
+ return () => ({
851
+ ref: options.state,
852
+ onsubmit: EventHandler.make(Effect.fn((event) => Effect.andThen(setSubmitting(true), Effect.matchEffect(validate(options.state), {
853
+ onFailure: Effect.fn(() => Effect.void),
854
+ onSuccess: Effect.fn((values) => {
855
+ const result = options.onValidSubmit?.(values, event);
856
+ return Effect.isEffect(result) ? result : Effect.void;
857
+ }),
858
+ })).pipe(Effect.ensuring(setSubmitting(false).pipe(Effect.catch(Effect.fn(() => Effect.void)))))), { preventDefault: true }),
859
+ onreset: EventHandler.preventDefault(EventHandler.fromEffectOrEventHandler(reset(options.state))),
860
+ });
861
+ }
862
+ /**
863
+ * Renders a native form and provides its state to schema-bound descendants.
864
+ * @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.
873
+ * @example
874
+ * ```ts
875
+ * import { Form, Submit, TextInput, makeState } from "@typed/ui/Form"
876
+ * import { Effect, Schema } from "effect"
877
+ * import { html } from "@typed/template"
878
+ *
879
+ * const codec = Schema.Struct({ email: Schema.String })
880
+ * const view = Effect.gen(function* () {
881
+ * const state = yield* makeState(codec, { id: "signup", values: { email: "" } })
882
+ * return Form({
883
+ * state,
884
+ * content: html`${TextInput({ state, name: "email" })}${Submit({ content: "Join" })}`,
885
+ * onValidSubmit: (values) => Effect.log(`Submitting ${values.email}`)
886
+ * })
887
+ * })
888
+ * ```
889
+ * @since 1.0.0
890
+ * @category components
891
+ */
892
+ export function Form(options, host) {
893
+ const rendered = Dom.renderHost()(options, host, formProps(options), options.content, (props, content) => html `<form ...${props}>${content}</form>`);
894
+ return FxApi.provideService(rendered, CurrentForm, {
895
+ state: options.state,
896
+ });
897
+ }
898
+ /**
899
+ * Creates a schema-bound form component family from one Struct codec.
900
+ * @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.
908
+ * @example
909
+ * ```ts
910
+ * import { make } from "@typed/ui/Form"
911
+ * import { Effect, Schema } from "effect"
912
+ * import { html } from "@typed/template"
913
+ *
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({
918
+ * form,
919
+ * content: html`${Signup.EmailInput({ name: "email" })}${Signup.Checkbox({ name: "accepted" })}`
920
+ * })
921
+ * })
922
+ * ```
923
+ * @since 1.0.0
924
+ * @category constructors
925
+ */
926
+ export function make(codec) {
927
+ const state = (values, options = {}) => makeState(codec, { ...options, values });
928
+ const Root = (options, host) => {
929
+ const { form, ...rootOptions } = options;
930
+ return Form({ ...rootOptions, state: form }, host);
931
+ };
932
+ const BoundCheckbox = (options, host) => withCurrentForm()((state) => {
933
+ const inputOptions = { ...options, state };
934
+ return Checkbox(inputOptions, host);
935
+ });
936
+ const BoundSelect = (options, host) => withCurrentForm()((state) => {
937
+ const inputOptions = { ...options, state };
938
+ return Select(inputOptions, host);
939
+ });
940
+ const BoundError = (options, host) => withCurrentForm()((state) => {
941
+ const inputOptions = { ...options, state };
942
+ return Error(inputOptions, host);
943
+ });
944
+ const BoundReset = (options, host) => withCurrentForm()((state) => {
945
+ const inputOptions = { ...options, state };
946
+ return Reset(inputOptions, host);
947
+ });
948
+ const BoundPush = (options, host) => withCurrentForm()((state) => {
949
+ const inputOptions = { ...options, state };
950
+ return Push(inputOptions, host);
951
+ });
952
+ const BoundRemove = (options, host) => withCurrentForm()((state) => {
953
+ const inputOptions = { ...options, state };
954
+ return Remove(inputOptions, host);
955
+ });
956
+ return {
957
+ codec,
958
+ state,
959
+ Root,
960
+ TextInput: makeSchemaBoundInput(codec, "text"),
961
+ SearchInput: makeSchemaBoundInput(codec, "search"),
962
+ EmailInput: makeSchemaBoundInput(codec, "email"),
963
+ UrlInput: makeSchemaBoundInput(codec, "url"),
964
+ TelInput: makeSchemaBoundInput(codec, "tel"),
965
+ PasswordInput: makeSchemaBoundInput(codec, "password"),
966
+ HiddenInput: makeSchemaBoundInput(codec, "hidden"),
967
+ ColorInput: makeSchemaBoundInput(codec, "color"),
968
+ TimeInput: makeSchemaBoundInput(codec, "time"),
969
+ DateTimeLocalInput: makeSchemaBoundInput(codec, "datetime-local"),
970
+ MonthInput: makeSchemaBoundInput(codec, "month"),
971
+ WeekInput: makeSchemaBoundInput(codec, "week"),
972
+ NumberInput: makeSchemaBoundInput(codec, "number"),
973
+ RangeInput: makeSchemaBoundInput(codec, "range"),
974
+ DateInput: makeSchemaBoundInput(codec, "date"),
975
+ MaskedInput: makeSchemaBoundInput(codec, "text"),
976
+ Checkbox: BoundCheckbox,
977
+ Select: BoundSelect,
978
+ Error: BoundError,
979
+ Reset: BoundReset,
980
+ Push: BoundPush,
981
+ Remove: BoundRemove,
982
+ Label,
983
+ Description,
984
+ Submit,
985
+ Group,
986
+ };
987
+ }