@menapo/forms 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,683 @@
1
+ import { a as nameOf, i as merge, n as get, o as patternOf, r as leaves, s as set, t as equal } from "./paths-DqNafpnl.js";
2
+ import { liveRegion } from "@menapo/machine/dom";
3
+ //#region src/core/env.ts
4
+ /**
5
+ * What every form shares (PLAN §6.2): development checks, the error format
6
+ * and the nullable DevTools hook (D18). One module, so `inspect` and the
7
+ * tests reach the hook without a second copy.
8
+ */
9
+ /**
10
+ * Development checks and messages: Vite replaces `import.meta.env.DEV` with
11
+ * a literal, so a production build drops every `if (DEV)` branch. Without
12
+ * Vite, `env` is undefined and the checks are off.
13
+ */
14
+ const DEV = !!import.meta.env?.DEV;
15
+ /** `[forms:CODE] message`: an error that names what went wrong and the fix. */
16
+ const fail = (code, text) => /* @__PURE__ */ new Error(`[forms:${code}] ${text}`);
17
+ /** A development warning, in the same format. */
18
+ const warn = (code, text) => console.warn(`[forms:${code}] ${text}`);
19
+ const env = { server: typeof document === "undefined" };
20
+ /** One field: its error, flags and cached parts. Values live in the instance's tree. */
21
+ var Rec = class {
22
+ /** Its path: renamed when an array moves its item (D14). */
23
+ p;
24
+ e = void 0;
25
+ s = 0;
26
+ t = false;
27
+ /** Checks or async validations running. */
28
+ vd = 0;
29
+ /** A view read the hint: it is rendered, so the control links it. */
30
+ h = false;
31
+ /** A view asked for it, or the DOM has its control: issues at its path are its own. */
32
+ k = false;
33
+ /** Moves with what the parts show. */
34
+ ver = 0;
35
+ pc = void 0;
36
+ pv = -1;
37
+ oc = void 0;
38
+ constructor(path) {
39
+ this.p = path;
40
+ }
41
+ };
42
+ const byId = (id) => typeof document === "object" ? document.getElementById(id) : null;
43
+ /** Each path and its ancestors: a change to `invites.0.email` is one to `invites` too. */
44
+ function withParents(path) {
45
+ const out = [path];
46
+ for (let at = path.lastIndexOf("."); at > 0; at = path.lastIndexOf(".", at - 1)) out.push(path.slice(0, at));
47
+ return out;
48
+ }
49
+ var Instance = class {
50
+ F;
51
+ /** The DOM id the form's ids derive from. */
52
+ id;
53
+ N;
54
+ /** Values: an immutable tree (paths.ts). */
55
+ v;
56
+ /** What `dirty` compares against: the defaults, then the last success's values. */
57
+ b;
58
+ /** The merged defaults (D11). */
59
+ d;
60
+ /** The hook's own `defaults`, as last given: a new object is followed. */
61
+ hd;
62
+ r = /* @__PURE__ */ new Map();
63
+ /** Paths whose value differs from the baseline. */
64
+ dp = /* @__PURE__ */ new Set();
65
+ ls = [];
66
+ onSuccess;
67
+ pending = false;
68
+ error = void 0;
69
+ /** The form error came from the schema (an issue with no field), so a valid run clears it. */
70
+ ce = false;
71
+ data = void 0;
72
+ progress = 0;
73
+ /** A submit was attempted: errors now update on input and blur (D8). */
74
+ at = false;
75
+ /** The order of paths in the last issues, for `errors`. */
76
+ ord = [];
77
+ /** `errors`, cached until an error changes. */
78
+ ec = void 0;
79
+ /** Full validations started; a result for an older one is dropped. */
80
+ vv = 0;
81
+ ac = void 0;
82
+ /** After the next commit: focus the summary or the first invalid control (D12). */
83
+ fo = false;
84
+ /** Messages to announce after the next commit. */
85
+ an = [];
86
+ /** Inside a submit: errors are announced by focus, not the live region. */
87
+ inSubmit = false;
88
+ mounted = false;
89
+ /** The form parts, cached by `pending`. */
90
+ fp = void 0;
91
+ /** The delegated handlers, made with the first parts so their identity never changes (D6). */
92
+ hs = void 0;
93
+ fpv = void 0;
94
+ /** The array module's stable keys, by array path (D14). */
95
+ ak = void 0;
96
+ /** The checks module's state (D8). */
97
+ ck = void 0;
98
+ /** The definition's shape, once these values extended it (view.ts). */
99
+ sh = void 0;
100
+ constructor(F, id, N, options) {
101
+ this.F = F;
102
+ this.id = id;
103
+ this.N = N;
104
+ this.hd = options?.defaults;
105
+ this.onSuccess = options?.onSuccess;
106
+ this.d = merge(F.o.defaults, options?.defaults);
107
+ this.v = this.b = this.d;
108
+ const state = options?.state;
109
+ if (state) {
110
+ this.v = merge(this.v, state.values);
111
+ for (const [path, message] of Object.entries(state.errors)) {
112
+ const rec = this.rec(path);
113
+ rec.e = message;
114
+ rec.s = 1;
115
+ this.ord.push(path);
116
+ }
117
+ for (const [path] of leaves(this.v)) this.dirty(path);
118
+ if (state.error) this.error = new Error(state.error);
119
+ this.at = true;
120
+ }
121
+ }
122
+ rec(path) {
123
+ let rec = this.r.get(path);
124
+ if (!rec) this.r.set(path, rec = new Rec(path));
125
+ return rec;
126
+ }
127
+ spec(path) {
128
+ return this.F.f[patternOf(path)];
129
+ }
130
+ fid(path) {
131
+ return `${this.id}-${path}`;
132
+ }
133
+ tell(paths, mask, form) {
134
+ if (form & 4) this.ec = void 0;
135
+ for (const listener of this.ls) listener.hear(paths, mask, form);
136
+ if (DEV) env.devtools?.({
137
+ kind: "change",
138
+ instance: this
139
+ });
140
+ }
141
+ /** Every view moves: a reset, new defaults. */
142
+ all() {
143
+ this.ec = void 0;
144
+ for (const rec of this.r.values()) rec.ver++;
145
+ this.tell([...this.r.keys(), ...withParents("")], -1, -1);
146
+ }
147
+ /** Recomputes whether `path` is dirty; the form's bit when the set changed size between empty and not. */
148
+ dirty(path) {
149
+ const was = this.dp.size;
150
+ if (equal(get(this.v, path), get(this.b, path))) this.dp.delete(path);
151
+ else this.dp.add(path);
152
+ return !was !== !this.dp.size ? 8 : 0;
153
+ }
154
+ /** Writes a value: from the DOM (`dom`), or from code, which also writes the control. */
155
+ set(path, value, dom) {
156
+ if (equal(get(this.v, path), value)) return;
157
+ this.v = set(this.v, path, value);
158
+ const rec = this.rec(path);
159
+ let mask = 1;
160
+ const wasDirty = this.dp.has(path);
161
+ let form = this.dirty(path);
162
+ if (wasDirty !== this.dp.has(path)) mask |= 8;
163
+ if (rec.s === 1) {
164
+ this.error_(rec, void 0, 0);
165
+ mask |= 34;
166
+ form |= 4;
167
+ }
168
+ if (!dom) this.#write(path, value);
169
+ this.tell(withParents(path), mask, form);
170
+ if (Array.isArray(value)) this.tell([path], 64, 0);
171
+ if (this.at && rec.e !== void 0) this.#validate(path);
172
+ this.F.c?.run(this, path, value);
173
+ }
174
+ /** A control's event (D6): its value by its type and the field's spec. */
175
+ #input(el) {
176
+ const path = el.name;
177
+ if (!path || !("form" in el)) return;
178
+ this.rec(path).k = true;
179
+ if (el.type === "radio" && !el.checked) return;
180
+ this.set(path, this.#read(el), true);
181
+ }
182
+ #read(el) {
183
+ const path = el.name;
184
+ const spec = this.spec(path);
185
+ const input = el;
186
+ switch (input.type) {
187
+ case "checkbox": {
188
+ if (spec?.t !== "array" && !Array.isArray(get(this.v, path))) return input.checked;
189
+ const all = input.form?.elements.namedItem(path);
190
+ return (all && "length" in all ? [...all] : [input]).filter((b) => b.checked).map((b) => b.value);
191
+ }
192
+ case "file": return spec?.m ? [...input.files ?? []] : input.files?.[0] ?? void 0;
193
+ case "select-multiple": return [...el.selectedOptions].map((o) => o.value);
194
+ }
195
+ const text = el.value;
196
+ if (spec?.t === "number" || spec?.t === "integer" || input.type === "number") return text === "" ? void 0 : Number.isNaN(Number(text)) ? text : Number(text);
197
+ return text;
198
+ }
199
+ /** Writes `value` into the controls named `path` (S4): a set, a reset, defaults followed. */
200
+ #write(path, value) {
201
+ const found = byId(this.id)?.elements.namedItem(path) ?? byId(this.fid(path));
202
+ if (!found) return;
203
+ const els = "length" in found && !("type" in found) ? [...found] : [found];
204
+ for (const el of els) {
205
+ const input = el;
206
+ if (input.type === "checkbox") input.checked = Array.isArray(value) ? value.includes(input.value) : value === true;
207
+ else if (input.type === "radio") input.checked = value != null && String(value) === input.value;
208
+ else if (input.type === "file") {
209
+ if (value == null) input.value = "";
210
+ } else if (input.type === "select-multiple") for (const option of el.options) option.selected = Array.isArray(value) && value.includes(option.value);
211
+ else el.value = value == null ? "" : String(value);
212
+ }
213
+ }
214
+ blur(el) {
215
+ const path = el.name;
216
+ if (!path) return;
217
+ const rec = this.rec(path);
218
+ rec.k = true;
219
+ if (!rec.t) {
220
+ rec.t = true;
221
+ this.tell([path], 4, 0);
222
+ }
223
+ if (this.at) this.#validate(path);
224
+ }
225
+ /** Sets a field's error: one place, so the live region and the summary follow every change. */
226
+ error_(rec, message, source) {
227
+ if (rec.e === message) {
228
+ if (message !== void 0) rec.s = source;
229
+ return false;
230
+ }
231
+ rec.e = message;
232
+ rec.s = message === void 0 ? 0 : source;
233
+ rec.ver++;
234
+ if (message !== void 0 && !this.inSubmit && typeof document === "object") {
235
+ const control = byId(this.fid(rec.p));
236
+ if (document.activeElement !== control) this.an.push(message);
237
+ }
238
+ return true;
239
+ }
240
+ /** Field errors from issues; those with no field become the form's error. */
241
+ #apply(issues, only, source) {
242
+ const found = /* @__PURE__ */ new Map();
243
+ const order = [];
244
+ for (const issue of issues) {
245
+ const path = nameOf(issue.path);
246
+ let message = issue.message;
247
+ if (typeof message !== "string") {
248
+ if (DEV) warn("MESSAGE_NOT_TEXT", `${path || this.F.n}: the schema's message is a msg() reference; word it where it runs: v.minLength(1, () => t("Name is required.")).`);
249
+ message = String(message);
250
+ }
251
+ if (!found.has(path)) {
252
+ found.set(path, message);
253
+ order.push(path);
254
+ }
255
+ }
256
+ const paths = [];
257
+ if (only !== void 0) {
258
+ const rec = this.rec(only);
259
+ const message = found.get(only);
260
+ if ((message !== void 0 || rec.s !== 1) && this.error_(rec, message, source)) paths.push(only);
261
+ } else {
262
+ const unmatched = [];
263
+ for (const rec of this.r.values()) {
264
+ const message = found.get(rec.p);
265
+ if (message === void 0 && rec.s === 1 && source === 0) continue;
266
+ if (this.error_(rec, message, source)) paths.push(rec.p);
267
+ }
268
+ for (const [path, message] of found) {
269
+ if (this.r.has(path)) continue;
270
+ if (path && this.#known(path)) {
271
+ const rec = this.rec(path);
272
+ rec.k = true;
273
+ if (this.error_(rec, message, source)) paths.push(path);
274
+ } else unmatched.push(message);
275
+ }
276
+ this.ord = order;
277
+ if (source === 0) {
278
+ if (unmatched.length) {
279
+ this.error = Object.assign(new Error(unmatched.join(" ")), { issues });
280
+ this.ce = true;
281
+ } else if (this.ce) {
282
+ this.error = void 0;
283
+ this.ce = false;
284
+ }
285
+ }
286
+ if (paths.length || unmatched.length) this.tell(paths, 34, 6);
287
+ return found.size > 0;
288
+ }
289
+ if (paths.length) this.tell(paths, 34, 4);
290
+ return found.has(only);
291
+ }
292
+ /** Whether the DOM has a control named `path`. */
293
+ #known(path) {
294
+ return !!byId(this.id)?.elements.namedItem(path);
295
+ }
296
+ /**
297
+ * Validates one field's error, after input or blur (D8). An async schema
298
+ * resolves later; a result for a value since replaced is dropped.
299
+ */
300
+ #validate(only) {
301
+ const schema = this.F.s;
302
+ if (!schema) return false;
303
+ const version = ++this.vv;
304
+ const result = schema["~standard"].validate(this.v);
305
+ const done = (r) => version === this.vv && this.#apply(r.issues ?? [], only, 0);
306
+ if (!("then" in result)) return done(result);
307
+ const rec = this.rec(only);
308
+ if (!rec.vd++) this.tell([only], 16, 0);
309
+ return result.then((r) => {
310
+ if (!--rec.vd) this.tell([only], 16, 0);
311
+ return done(r);
312
+ });
313
+ }
314
+ async submit() {
315
+ if (this.pending || this.inSubmit) return void 0;
316
+ this.at = true;
317
+ this.inSubmit = true;
318
+ const start = DEV ? performance.now() : 0;
319
+ try {
320
+ await this.F.c?.settle(this);
321
+ const schema = this.F.s;
322
+ let output = this.v;
323
+ let invalid = false;
324
+ if (schema) {
325
+ const version = ++this.vv;
326
+ let result = schema["~standard"].validate(this.v);
327
+ if ("then" in result) result = await result;
328
+ if (version === this.vv) this.#apply(result.issues ?? [], void 0, 0);
329
+ if (result.issues) invalid = true;
330
+ else output = result.value;
331
+ }
332
+ for (const rec of this.r.values()) if (rec.e !== void 0) invalid = true;
333
+ if (invalid || this.ce && this.error !== void 0) {
334
+ this.fo = true;
335
+ this.tell([], 0, 4);
336
+ return;
337
+ }
338
+ const input = this.v;
339
+ this.ac?.abort();
340
+ const controller = this.ac = new AbortController();
341
+ this.#flags(true, void 0, 0);
342
+ try {
343
+ const data = await this.F.x(this.F, input, output, {
344
+ signal: controller.signal,
345
+ progress: (ratio) => this.#flags(true, void 0, ratio)
346
+ });
347
+ if (controller.signal.aborted) return void 0;
348
+ this.#succeed(input, data);
349
+ if (DEV) env.devtools?.({
350
+ kind: "submit",
351
+ instance: this,
352
+ ms: performance.now() - start
353
+ });
354
+ this.onSuccess?.(data);
355
+ return data;
356
+ } catch (error) {
357
+ if (controller.signal.aborted) return void 0;
358
+ if (DEV) env.devtools?.({
359
+ kind: "submit",
360
+ instance: this,
361
+ ms: performance.now() - start,
362
+ error
363
+ });
364
+ this.#fail_(error);
365
+ return;
366
+ } finally {
367
+ if (this.ac === controller) this.ac = void 0;
368
+ }
369
+ } finally {
370
+ this.inSubmit = false;
371
+ }
372
+ }
373
+ #flags(pending, error, progress) {
374
+ const form = (pending !== this.pending ? 1 : 0) | (error !== this.error ? 2 : 0) | (progress !== this.progress ? 32 : 0);
375
+ this.pending = pending;
376
+ this.error = error;
377
+ this.progress = progress;
378
+ if (form) this.tell([], 0, form);
379
+ }
380
+ #succeed(input, data) {
381
+ this.b = input;
382
+ this.at = false;
383
+ this.data = data;
384
+ const paths = [];
385
+ for (const rec of this.r.values()) {
386
+ if (this.error_(rec, void 0, 0)) paths.push(rec.p);
387
+ rec.ver++;
388
+ }
389
+ this.dp.clear();
390
+ for (const [path] of leaves(this.v)) this.dirty(path);
391
+ this.ce = false;
392
+ this.pending = false;
393
+ this.error = void 0;
394
+ this.tell([...this.r.keys()], 42, -1);
395
+ }
396
+ /** A failed submit: `issues` on their fields, the rest the form's error (D9). */
397
+ #fail_(error) {
398
+ const issues = error?.issues;
399
+ let matched = false;
400
+ if (Array.isArray(issues) && issues.length) {
401
+ this.ce = false;
402
+ for (const rec of this.r.values()) if (rec.s === 1) this.error_(rec, void 0, 0);
403
+ const unmatched = issues.filter((issue) => {
404
+ const path = nameOf(issue.path);
405
+ return !path || !(this.r.get(path)?.k || this.#known(path));
406
+ });
407
+ this.#apply(issues.filter((issue) => !unmatched.includes(issue)), void 0, 1);
408
+ matched = unmatched.length < issues.length;
409
+ this.#flags(false, unmatched.length ? error : void 0, this.progress);
410
+ } else this.#flags(false, error, this.progress);
411
+ if (matched) this.fo = true;
412
+ this.tell([], 0, 4);
413
+ }
414
+ reset(values) {
415
+ this.ac?.abort();
416
+ this.v = this.b = values === void 0 ? this.d : merge(this.d, values);
417
+ this.dp.clear();
418
+ this.at = this.ce = this.pending = false;
419
+ this.error = void 0;
420
+ this.progress = 0;
421
+ this.ord = [];
422
+ for (const rec of this.r.values()) {
423
+ rec.e = void 0;
424
+ rec.s = 0;
425
+ rec.t = false;
426
+ }
427
+ this.ak = void 0;
428
+ this.#writeAll();
429
+ this.all();
430
+ }
431
+ /** Every named control gets its value (a reset, a new baseline). */
432
+ #writeAll() {
433
+ const form = byId(this.id);
434
+ const done = /* @__PURE__ */ new Set();
435
+ for (const el of form ? [...form.elements] : []) {
436
+ const name = el.name;
437
+ if (!name || done.has(name) || el.type === "submit") continue;
438
+ done.add(name);
439
+ this.#write(name, get(this.v, name));
440
+ }
441
+ }
442
+ /** The hook's options again (D11): pristine fields follow new defaults; edited ones keep their text. */
443
+ options(options) {
444
+ this.onSuccess = options?.onSuccess;
445
+ if (options?.defaults === this.hd) return;
446
+ this.hd = options?.defaults;
447
+ const next = merge(this.F.o.defaults, options?.defaults);
448
+ if (equal(next, this.d)) return;
449
+ const old = this.b;
450
+ let values = this.v;
451
+ const written = [];
452
+ for (const [path, value] of leaves(next)) if (equal(get(values, path), get(old, path)) && !equal(get(values, path), value)) {
453
+ values = set(values, path, value);
454
+ written.push([path, value]);
455
+ }
456
+ this.d = next;
457
+ this.b = next;
458
+ this.v = values;
459
+ this.dp.clear();
460
+ for (const [path] of leaves(values)) this.dirty(path);
461
+ for (const [path, value] of written) this.#write(path, value);
462
+ this.all();
463
+ }
464
+ mount() {
465
+ if (this.mounted || env.server) return;
466
+ this.mounted = true;
467
+ if (DEV) env.devtools?.({
468
+ kind: "change",
469
+ instance: this
470
+ });
471
+ const form = byId(this.id);
472
+ if (!form) return;
473
+ form.noValidate = true;
474
+ for (const el of [...form.elements]) {
475
+ const name = el.name;
476
+ if (!name || el.type === "submit" || el.type === "button" || el.type === "file") continue;
477
+ if (el.type === "radio" && !el.checked) continue;
478
+ this.rec(name).k = true;
479
+ const value = this.#read(el);
480
+ const now = get(this.v, name);
481
+ if (now === void 0 && (value === "" || value === false || Array.isArray(value) && !value.length)) {
482
+ this.v = set(this.v, name, value);
483
+ this.b = set(this.b, name, value);
484
+ } else if (!equal(value, now)) this.set(name, value, true);
485
+ }
486
+ if (DEV) for (const rec of this.r.values()) {
487
+ const control = byId(this.fid(rec.p));
488
+ if (control && !form.contains(control)) warn("OUTSIDE_FORM", `${rec.p} has a control outside <form {...f.parts.form}>; its events don't reach the form. Move it inside, or call field.set() from its handler.`);
489
+ }
490
+ }
491
+ unmount() {
492
+ if (!this.mounted) return;
493
+ this.mounted = false;
494
+ this.ac?.abort();
495
+ this.F.c?.stop(this);
496
+ if (DEV) env.devtools?.({
497
+ kind: "gone",
498
+ instance: this
499
+ });
500
+ }
501
+ /** After the framework committed: focus after a failed submit, then announcements (D12). */
502
+ commit() {
503
+ if (env.server) return;
504
+ if (this.fo) {
505
+ this.fo = false;
506
+ (byId(`${this.id}-summary`) ?? byId(this.id)?.querySelector("[aria-invalid=\"true\"]") ?? null)?.focus();
507
+ }
508
+ if (this.an.length) {
509
+ liveRegion(this.an.join(" "));
510
+ this.an = [];
511
+ }
512
+ }
513
+ formParts() {
514
+ if (this.fp && this.fpv === this.pending) return this.fp;
515
+ const N = this.N;
516
+ const h = this.hs ??= {
517
+ i: (event) => this.#input(event.target),
518
+ b: (event) => this.blur(event.target),
519
+ s: (event) => {
520
+ event.preventDefault();
521
+ this.submit();
522
+ },
523
+ r: (event) => {
524
+ event.preventDefault();
525
+ this.reset();
526
+ },
527
+ c: (event) => {
528
+ const link = event.target.closest?.("a[href^='#']");
529
+ const target = link && byId(decodeURIComponent(link.getAttribute("href").slice(1)));
530
+ if (!target) return;
531
+ event.preventDefault();
532
+ target.focus();
533
+ }
534
+ };
535
+ const pending = this.pending || void 0;
536
+ const form = {
537
+ [N.id]: this.id,
538
+ [N.method]: "post",
539
+ "data-pending": pending,
540
+ [N.onSubmit]: h.s,
541
+ [N.onReset]: h.r,
542
+ [N.blur]: h.b
543
+ };
544
+ for (const key of N.input) form[key] = h.i;
545
+ for (const spec of Object.values(this.F.f)) if (spec.t === "file") form[N.encType] = "multipart/form-data";
546
+ this.fpv = this.pending;
547
+ return this.fp = {
548
+ form,
549
+ submit: {
550
+ [N.type]: "submit",
551
+ "aria-disabled": pending,
552
+ "data-pending": pending
553
+ },
554
+ summary: {
555
+ [N.id]: `${this.id}-summary`,
556
+ [N.tabIndex]: -1,
557
+ [N.onClick]: h.c
558
+ }
559
+ };
560
+ }
561
+ /** A field's parts, rebuilt only when what they show moved; each one made when first read. */
562
+ fieldParts(rec) {
563
+ if (rec.pc && rec.pv === rec.ver) return rec.pc;
564
+ rec.oc = void 0;
565
+ rec.pv = rec.ver;
566
+ return rec.pc = new Parts(this, rec);
567
+ }
568
+ /**
569
+ * The control's attributes (§5.3): id, name, value, aria wiring and
570
+ * constraints (D13). The value is the current one, not the baseline, so
571
+ * a server's `state` renders what was posted.
572
+ */
573
+ control(rec) {
574
+ const N = this.N;
575
+ const path = rec.p;
576
+ const id = this.fid(path);
577
+ const spec = this.spec(path);
578
+ const invalid = rec.e !== void 0 || void 0;
579
+ const control = {
580
+ [N.id]: id,
581
+ [N.name]: path
582
+ };
583
+ const initial = get(this.v, path);
584
+ if (spec?.t === "boolean") {
585
+ control[N.type] = "checkbox";
586
+ control[N.checked] = initial === true;
587
+ } else if (spec?.t === "file") {
588
+ control[N.type] = "file";
589
+ if (spec.a) control[N.accept] = spec.a;
590
+ if (spec.m) control[N.multiple] = true;
591
+ } else control[N.value] = initial == null ? "" : String(initial);
592
+ control["aria-invalid"] = invalid;
593
+ control["aria-describedby"] = (invalid ? `${id}-error` : "") + (rec.h ? (invalid ? " " : "") + `${id}-hint` : "") || void 0;
594
+ if (spec) {
595
+ if (spec.r && spec.t !== "boolean") control[N.required] = true;
596
+ if (spec.mn !== void 0) control[N.minLength] = spec.mn;
597
+ if (spec.mx !== void 0) control[N.maxLength] = spec.mx;
598
+ if (spec.lo !== void 0) control[N.min] = spec.lo;
599
+ if (spec.hi !== void 0) control[N.max] = spec.hi;
600
+ }
601
+ return control;
602
+ }
603
+ /** One radio, or one checkbox of an array field. */
604
+ optionParts(rec, value) {
605
+ this.fieldParts(rec);
606
+ let found = (rec.oc ??= /* @__PURE__ */ new Map()).get(value);
607
+ if (found) return found;
608
+ const N = this.N;
609
+ const initial = get(this.v, rec.p);
610
+ const array = this.spec(rec.p)?.t === "array" || Array.isArray(initial);
611
+ const id = `${this.fid(rec.p)}-${String(value)}`;
612
+ found = {
613
+ control: {
614
+ [N.type]: array ? "checkbox" : "radio",
615
+ [N.name]: rec.p,
616
+ value: String(value),
617
+ [N.id]: id,
618
+ [N.checked]: array ? Array.isArray(initial) && initial.includes(value) : equal(initial, value),
619
+ "aria-invalid": rec.e !== void 0 || void 0
620
+ },
621
+ label: { [N.htmlFor]: id }
622
+ };
623
+ rec.oc.set(value, found);
624
+ return found;
625
+ }
626
+ /** The errors shown, in the order the last issues listed them. */
627
+ errors() {
628
+ if (this.ec) return this.ec;
629
+ const shown = [];
630
+ for (const rec of this.r.values()) if (rec.e !== void 0) shown.push({
631
+ path: rec.p,
632
+ message: rec.e,
633
+ href: `#${this.fid(rec.p)}`
634
+ });
635
+ const at = (path) => {
636
+ const i = this.ord.indexOf(path);
637
+ return i < 0 ? this.ord.length : i;
638
+ };
639
+ return this.ec = shown.sort((a, b) => at(a.path) - at(b.path));
640
+ }
641
+ };
642
+ /** A field's parts, each built when first read: a render that shows only the control builds only it. */
643
+ var Parts = class {
644
+ #i;
645
+ #r;
646
+ #control;
647
+ #label;
648
+ #error;
649
+ #hint;
650
+ #group;
651
+ constructor(instance, rec) {
652
+ this.#i = instance;
653
+ this.#r = rec;
654
+ }
655
+ get control() {
656
+ return this.#control ??= this.#i.control(this.#r);
657
+ }
658
+ get label() {
659
+ return this.#label ??= { [this.#i.N.htmlFor]: this.#i.fid(this.#r.p) };
660
+ }
661
+ get error() {
662
+ const { N } = this.#i;
663
+ const id = `${this.#i.fid(this.#r.p)}-error`;
664
+ return this.#error ??= N.text ? {
665
+ [N.id]: id,
666
+ [N.text]: this.#r.e
667
+ } : { [N.id]: id };
668
+ }
669
+ get hint() {
670
+ const { N } = this.#i;
671
+ const id = `${this.#i.fid(this.#r.p)}-hint`;
672
+ const text = N.text && this.#i.spec(this.#r.p)?.d;
673
+ return this.#hint ??= text ? {
674
+ [N.id]: id,
675
+ [N.text]: text
676
+ } : { [N.id]: id };
677
+ }
678
+ get group() {
679
+ return this.#group ??= { "aria-describedby": this.#r.e === void 0 ? void 0 : `${this.#i.fid(this.#r.p)}-error` };
680
+ }
681
+ };
682
+ //#endregion
683
+ export { fail as i, DEV as n, env as r, Instance as t };