@vantageos/mosaic 0.2.2 → 0.3.0-alpha.1

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 (91) hide show
  1. package/README.md +181 -0
  2. package/dist/Alert.schema-BQsZGG6P.d.cts +41 -0
  3. package/dist/Alert.schema-BQsZGG6P.d.ts +41 -0
  4. package/dist/ConfirmDialog-B3CQQGmJ.d.cts +34 -0
  5. package/dist/ConfirmDialog-B3CQQGmJ.d.ts +34 -0
  6. package/dist/Skeleton.schema-BO0ca2UX.d.cts +135 -0
  7. package/dist/Skeleton.schema-BO0ca2UX.d.ts +135 -0
  8. package/dist/Tabs.schema-BzcEyeq8.d.cts +79 -0
  9. package/dist/Tabs.schema-BzcEyeq8.d.ts +79 -0
  10. package/dist/artifacts.cjs +20 -2
  11. package/dist/artifacts.cjs.map +1 -1
  12. package/dist/artifacts.js +20 -2
  13. package/dist/artifacts.js.map +1 -1
  14. package/dist/confirmation.cjs +44 -2
  15. package/dist/confirmation.cjs.map +1 -1
  16. package/dist/confirmation.d.cts +34 -1
  17. package/dist/confirmation.d.ts +34 -1
  18. package/dist/confirmation.js +41 -3
  19. package/dist/confirmation.js.map +1 -1
  20. package/dist/display.cjs +145 -2
  21. package/dist/display.cjs.map +1 -1
  22. package/dist/display.d.cts +1 -0
  23. package/dist/display.d.ts +1 -0
  24. package/dist/display.js +140 -3
  25. package/dist/display.js.map +1 -1
  26. package/dist/forms.cjs +1412 -0
  27. package/dist/forms.cjs.map +1 -0
  28. package/dist/forms.d.cts +159 -0
  29. package/dist/forms.d.ts +159 -0
  30. package/dist/forms.js +1331 -0
  31. package/dist/forms.js.map +1 -0
  32. package/dist/index-CwXVgtGb.d.cts +917 -0
  33. package/dist/index-CwXVgtGb.d.ts +917 -0
  34. package/dist/index.cjs +188 -2
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +5 -2
  37. package/dist/index.d.ts +5 -2
  38. package/dist/index.js +177 -3
  39. package/dist/index.js.map +1 -1
  40. package/dist/input.cjs +19 -0
  41. package/dist/input.cjs.map +1 -1
  42. package/dist/input.d.cts +4 -34
  43. package/dist/input.d.ts +4 -34
  44. package/dist/input.js +18 -1
  45. package/dist/input.js.map +1 -1
  46. package/dist/media.cjs +20 -2
  47. package/dist/media.cjs.map +1 -1
  48. package/dist/media.js +20 -2
  49. package/dist/media.js.map +1 -1
  50. package/dist/preact/confirmation.d.ts +85 -1
  51. package/dist/preact/confirmation.js +263 -0
  52. package/dist/preact/confirmation.js.map +1 -1
  53. package/dist/preact/display.d.ts +240 -1
  54. package/dist/preact/display.js +366 -0
  55. package/dist/preact/display.js.map +1 -1
  56. package/dist/preact/forms.d.ts +875 -0
  57. package/dist/preact/forms.js +1323 -0
  58. package/dist/preact/forms.js.map +1 -0
  59. package/dist/preact/input.d.ts +88 -1
  60. package/dist/preact/input.js +194 -0
  61. package/dist/preact/input.js.map +1 -1
  62. package/dist/progress.cjs +20 -2
  63. package/dist/progress.cjs.map +1 -1
  64. package/dist/progress.js +20 -2
  65. package/dist/progress.js.map +1 -1
  66. package/dist/react/confirmation.cjs +267 -0
  67. package/dist/react/confirmation.cjs.map +1 -1
  68. package/dist/react/confirmation.d.cts +21 -1
  69. package/dist/react/confirmation.d.ts +21 -1
  70. package/dist/react/confirmation.js +263 -0
  71. package/dist/react/confirmation.js.map +1 -1
  72. package/dist/react/display.cjs +372 -0
  73. package/dist/react/display.cjs.map +1 -1
  74. package/dist/react/display.d.cts +112 -1
  75. package/dist/react/display.d.ts +112 -1
  76. package/dist/react/display.js +362 -0
  77. package/dist/react/display.js.map +1 -1
  78. package/dist/react/forms.cjs +1384 -0
  79. package/dist/react/forms.cjs.map +1 -0
  80. package/dist/react/forms.d.cts +5 -0
  81. package/dist/react/forms.d.ts +5 -0
  82. package/dist/react/forms.js +1323 -0
  83. package/dist/react/forms.js.map +1 -0
  84. package/dist/react/input.cjs +197 -0
  85. package/dist/react/input.cjs.map +1 -1
  86. package/dist/react/input.d.cts +13 -1
  87. package/dist/react/input.d.ts +13 -1
  88. package/dist/react/input.js +194 -0
  89. package/dist/react/input.js.map +1 -1
  90. package/package.json +73 -49
  91. package/registry.yaml +154 -1
package/dist/forms.js ADDED
@@ -0,0 +1,1331 @@
1
+ import { zodResolver } from '@hookform/resolvers/zod';
2
+ import { useForm, FormProvider as FormProvider$1, Controller, useFieldArray as useFieldArray$1 } from 'react-hook-form';
3
+ import { z } from 'zod';
4
+ import { createContext, useContext, useId, useRef, useCallback, useEffect, useState, useMemo } from 'react';
5
+ import { jsx, jsxs } from 'react/jsx-runtime';
6
+
7
+ // src/runtimes/react/components/forms/useMosaicForm.tsx
8
+
9
+ // src/components/forms/useMosaicForm.logic.ts
10
+ function toRhfMode(mode) {
11
+ return mode;
12
+ }
13
+ var MosaicFormModeSchema = z.enum(["onBlur", "onChange", "onSubmit"]);
14
+ var UseMosaicFormOptionsSchema = z.object({
15
+ mode: MosaicFormModeSchema.default("onBlur")
16
+ });
17
+ function validateOptions(raw) {
18
+ return UseMosaicFormOptionsSchema.parse(raw);
19
+ }
20
+
21
+ // src/runtimes/react/components/forms/useMosaicForm.tsx
22
+ function useMosaicForm(options) {
23
+ const parsedOptions = validateOptions({ mode: options.mode });
24
+ const form = useForm({
25
+ // biome-ignore lint/suspicious/noExplicitAny: zodResolver's generic shape forces a cast at this boundary
26
+ resolver: zodResolver(options.schema),
27
+ defaultValues: options.defaultValues,
28
+ mode: toRhfMode(parsedOptions.mode)
29
+ });
30
+ const extended = form;
31
+ extended.mosaicSchema = options.schema;
32
+ extended.mosaicMode = parsedOptions.mode;
33
+ return extended;
34
+ }
35
+
36
+ // src/components/forms/FormProvider.logic.ts
37
+ var FORM_PROVIDER_CONTEXT_MISSING = "useMosaicFormContext must be used inside <FormProvider>. Wrap your tree with <FormProvider form={...}> from @vantageos/mosaic/{react,preact}/forms.";
38
+ var MosaicFormContext = createContext(null);
39
+ function FormProvider({
40
+ form,
41
+ children
42
+ }) {
43
+ return /* @__PURE__ */ jsx(MosaicFormContext.Provider, { value: form, children: /* @__PURE__ */ jsx(FormProvider$1, { ...form, children }) });
44
+ }
45
+ function useMosaicFormContext() {
46
+ const ctx = useContext(MosaicFormContext);
47
+ if (!ctx) {
48
+ throw new Error(FORM_PROVIDER_CONTEXT_MISSING);
49
+ }
50
+ return ctx;
51
+ }
52
+ function FormField({
53
+ name,
54
+ children
55
+ }) {
56
+ const form = useMosaicFormContext();
57
+ return /* @__PURE__ */ jsx(
58
+ Controller,
59
+ {
60
+ control: form.control,
61
+ name,
62
+ render: ({ field, fieldState, formState }) => children({
63
+ field,
64
+ fieldState,
65
+ formState
66
+ })
67
+ }
68
+ );
69
+ }
70
+
71
+ // src/components/forms/ErrorDisplay.logic.ts
72
+ function formatErrorMessage(error, messageMap) {
73
+ if (!error) return void 0;
74
+ if (error.message && error.message.length > 0) return error.message;
75
+ const mapped = messageMap?.[error.type];
76
+ if (mapped && mapped.length > 0) return mapped;
77
+ return "Invalid value";
78
+ }
79
+ function ErrorDisplay({ error, messageMap, className }) {
80
+ const message = formatErrorMessage(error, messageMap);
81
+ if (!message) return null;
82
+ return /* @__PURE__ */ jsx("span", { role: "alert", className: className ?? "mosaic-forms-error", children: message });
83
+ }
84
+
85
+ // src/components/forms/SubmitButton.logic.ts
86
+ function computeSubmitButtonState(input) {
87
+ return {
88
+ disabled: input.isSubmitting || !input.isValid,
89
+ loading: input.isSubmitting
90
+ };
91
+ }
92
+ var SubmitButtonPropsSchema = z.object({
93
+ label: z.string().min(1).default("Submit"),
94
+ loadingLabel: z.string().min(1).default("Submitting\u2026")
95
+ });
96
+ function validateSubmitButtonProps(raw) {
97
+ return SubmitButtonPropsSchema.parse(raw);
98
+ }
99
+ function SubmitButton(props) {
100
+ const { label, loadingLabel } = validateSubmitButtonProps({
101
+ label: props.label,
102
+ loadingLabel: props.loadingLabel
103
+ });
104
+ const form = useMosaicFormContext();
105
+ const { isValid, isSubmitting } = form.formState;
106
+ const { disabled, loading } = computeSubmitButtonState({ isValid, isSubmitting });
107
+ return /* @__PURE__ */ jsx(
108
+ "button",
109
+ {
110
+ type: "submit",
111
+ disabled,
112
+ "aria-busy": loading || void 0,
113
+ className: props.className ?? "mosaic-forms-submit",
114
+ children: loading ? loadingLabel : label
115
+ }
116
+ );
117
+ }
118
+
119
+ // src/components/forms/Input.logic.ts
120
+ function buildInputId(formName, fieldName) {
121
+ return formName ? `${formName}-${fieldName}` : `mosaic-input-${fieldName}`;
122
+ }
123
+ function buildErrorDescribedBy(inputId, hasError) {
124
+ return hasError ? `${inputId}-error` : void 0;
125
+ }
126
+ function resolveHtmlType(type) {
127
+ return type ?? "text";
128
+ }
129
+ function Input({
130
+ name,
131
+ type,
132
+ label,
133
+ placeholder,
134
+ disabled,
135
+ autoComplete,
136
+ id,
137
+ messageMap,
138
+ className,
139
+ inputClassName,
140
+ errorClassName
141
+ }) {
142
+ const form = useMosaicFormContext();
143
+ const reactId = useId();
144
+ const inputId = id ?? `mosaic-input-${reactId.replace(/[^a-zA-Z0-9_-]/g, "")}-${String(name)}`;
145
+ const htmlType = resolveHtmlType(type);
146
+ return /* @__PURE__ */ jsx(
147
+ Controller,
148
+ {
149
+ control: form.control,
150
+ name,
151
+ render: ({ field, fieldState }) => {
152
+ const f = field;
153
+ const hasError = Boolean(fieldState.error);
154
+ const describedBy = buildErrorDescribedBy(inputId, hasError);
155
+ const message = formatErrorMessage(
156
+ fieldState.error ? { type: fieldState.error.type, message: fieldState.error.message } : void 0,
157
+ messageMap
158
+ );
159
+ const value = f.value ?? "";
160
+ return /* @__PURE__ */ jsxs("div", { className: className ?? "mosaic-forms-input", children: [
161
+ /* @__PURE__ */ jsx("label", { htmlFor: inputId, children: label }),
162
+ /* @__PURE__ */ jsx(
163
+ "input",
164
+ {
165
+ id: inputId,
166
+ name: f.name,
167
+ type: htmlType,
168
+ value,
169
+ onChange: f.onChange,
170
+ onBlur: f.onBlur,
171
+ ref: f.ref,
172
+ placeholder,
173
+ disabled: disabled ?? false,
174
+ autoComplete,
175
+ "aria-invalid": hasError || void 0,
176
+ "aria-describedby": describedBy,
177
+ className: inputClassName
178
+ }
179
+ ),
180
+ hasError && message ? /* @__PURE__ */ jsx(
181
+ "span",
182
+ {
183
+ id: `${inputId}-error`,
184
+ role: "alert",
185
+ className: errorClassName ?? "mosaic-forms-error",
186
+ children: message
187
+ }
188
+ ) : null
189
+ ] });
190
+ }
191
+ }
192
+ );
193
+ }
194
+
195
+ // src/components/forms/Textarea.logic.ts
196
+ var DEFAULT_TEXTAREA_ROWS = 3;
197
+ function clampToMaxLength(value, maxLength) {
198
+ if (maxLength === void 0) return value;
199
+ if (value.length <= maxLength) return value;
200
+ return value.slice(0, maxLength);
201
+ }
202
+ function resolveRows(rows) {
203
+ if (rows === void 0) return DEFAULT_TEXTAREA_ROWS;
204
+ if (!Number.isFinite(rows) || rows < 1) return DEFAULT_TEXTAREA_ROWS;
205
+ return Math.floor(rows);
206
+ }
207
+ function computeAutoResizeHeight(scrollHeight, minHeightPx) {
208
+ if (!Number.isFinite(scrollHeight) || scrollHeight <= 0) return minHeightPx;
209
+ return Math.max(scrollHeight, minHeightPx);
210
+ }
211
+ function Textarea(props) {
212
+ const {
213
+ name,
214
+ rows,
215
+ maxLength,
216
+ autoResize = false,
217
+ placeholder,
218
+ disabled,
219
+ label,
220
+ className,
221
+ errorMessageMap
222
+ } = props;
223
+ const effectiveRows = resolveRows(rows);
224
+ const reactId = useId();
225
+ const fieldId = `mosaic-textarea-${reactId}`;
226
+ const errorId = `${fieldId}-error`;
227
+ return /* @__PURE__ */ jsx(FormField, { name, children: ({ field, fieldState }) => {
228
+ const hasError = Boolean(fieldState.error);
229
+ return /* @__PURE__ */ jsx(
230
+ TextareaInner,
231
+ {
232
+ id: fieldId,
233
+ errorId,
234
+ label,
235
+ value: field.value ?? "",
236
+ onChange: (next) => field.onChange(clampToMaxLength(next, maxLength)),
237
+ onBlur: field.onBlur,
238
+ rows: effectiveRows,
239
+ maxLength,
240
+ autoResize,
241
+ placeholder,
242
+ disabled,
243
+ hasError,
244
+ className,
245
+ children: /* @__PURE__ */ jsx(
246
+ ErrorDisplay,
247
+ {
248
+ error: fieldState.error ? {
249
+ type: fieldState.error.type ?? "validation",
250
+ message: fieldState.error.message
251
+ } : void 0,
252
+ messageMap: errorMessageMap,
253
+ className: "mosaic-forms-error"
254
+ }
255
+ )
256
+ }
257
+ );
258
+ } });
259
+ }
260
+ function TextareaInner(props) {
261
+ const {
262
+ id,
263
+ errorId,
264
+ label,
265
+ value,
266
+ onChange,
267
+ onBlur,
268
+ rows,
269
+ maxLength,
270
+ autoResize,
271
+ placeholder,
272
+ disabled,
273
+ hasError,
274
+ className,
275
+ children
276
+ } = props;
277
+ const ref = useRef(null);
278
+ const resize = useCallback(() => {
279
+ if (!autoResize) return;
280
+ const el = ref.current;
281
+ if (!el) return;
282
+ el.style.height = "auto";
283
+ const minPx = rows * 20;
284
+ const next = computeAutoResizeHeight(el.scrollHeight, minPx);
285
+ el.style.height = `${next}px`;
286
+ }, [autoResize, rows]);
287
+ useEffect(() => {
288
+ resize();
289
+ }, [resize]);
290
+ return /* @__PURE__ */ jsxs("div", { className: className ?? "mosaic-forms-textarea", children: [
291
+ label ? /* @__PURE__ */ jsx("label", { htmlFor: id, className: "mosaic-forms-textarea-label", children: label }) : null,
292
+ /* @__PURE__ */ jsx(
293
+ "textarea",
294
+ {
295
+ ref,
296
+ id,
297
+ value,
298
+ rows,
299
+ maxLength,
300
+ placeholder,
301
+ disabled,
302
+ "aria-invalid": hasError || void 0,
303
+ "aria-describedby": hasError ? errorId : void 0,
304
+ onChange: (e) => {
305
+ onChange(e.target.value);
306
+ resize();
307
+ },
308
+ onBlur
309
+ }
310
+ ),
311
+ /* @__PURE__ */ jsx("div", { id: errorId, children })
312
+ ] });
313
+ }
314
+
315
+ // src/components/forms/FieldArray.logic.ts
316
+ function buildListAriaLabel(name) {
317
+ return `${name} items`;
318
+ }
319
+ function buildRemoveAriaLabel(name, index) {
320
+ return `Remove ${name} item ${index + 1}`;
321
+ }
322
+ function buildAddAriaLabel(name) {
323
+ return `Add ${name} item`;
324
+ }
325
+ function nextFocusIndexAfterRemove(previousLength, removedIndex) {
326
+ if (previousLength <= 1) return -1;
327
+ const newLength = previousLength - 1;
328
+ return Math.min(removedIndex, newLength - 1);
329
+ }
330
+ function useFieldArray({ name, control }) {
331
+ const ctx = useMosaicFormContext();
332
+ const resolvedControl = control ?? ctx.control;
333
+ return useFieldArray$1({
334
+ // biome-ignore lint/suspicious/noExplicitAny: bridging erased generic
335
+ control: resolvedControl,
336
+ name
337
+ });
338
+ }
339
+ function FieldArray({ name, children, ariaLabel }) {
340
+ const ctx = useMosaicFormContext();
341
+ const { fields, append, remove, move, swap } = useFieldArray$1({
342
+ // biome-ignore lint/suspicious/noExplicitAny: bridging erased generic
343
+ control: ctx.control,
344
+ name
345
+ });
346
+ const controls = {
347
+ fields,
348
+ append,
349
+ remove,
350
+ move,
351
+ swap
352
+ };
353
+ return /* @__PURE__ */ jsx(
354
+ "ul",
355
+ {
356
+ "aria-label": ariaLabel ?? buildListAriaLabel(name),
357
+ style: { listStyle: "none", padding: 0, margin: 0 },
358
+ children: fields.map((field, index) => /* @__PURE__ */ jsx(
359
+ "li",
360
+ {
361
+ "data-mosaic-fieldarray-item": true,
362
+ "data-mosaic-fieldarray-name": name,
363
+ "data-mosaic-fieldarray-index": index,
364
+ children: children(
365
+ {
366
+ field,
367
+ index
368
+ },
369
+ controls
370
+ )
371
+ },
372
+ field.id
373
+ ))
374
+ }
375
+ );
376
+ }
377
+
378
+ // src/components/forms/Checkbox.logic.ts
379
+ function resolveAriaChecked(checked, indeterminate) {
380
+ if (indeterminate) return "mixed";
381
+ return checked;
382
+ }
383
+ function resolveDescribedBy(descriptionId, description, hasError, errorId) {
384
+ const parts = [];
385
+ if (description) parts.push(descriptionId);
386
+ if (hasError) parts.push(errorId);
387
+ return parts.length > 0 ? parts.join(" ") : void 0;
388
+ }
389
+ function Checkbox(props) {
390
+ const { name, label, disabled, indeterminate, description, className, errorMessageMap } = props;
391
+ const reactId = useId();
392
+ const fieldId = `mosaic-checkbox-${reactId}`;
393
+ const errorId = `${fieldId}-error`;
394
+ const descriptionId = `${fieldId}-description`;
395
+ return /* @__PURE__ */ jsx(FormField, { name, children: ({ field, fieldState }) => {
396
+ const hasError = Boolean(fieldState.error);
397
+ return /* @__PURE__ */ jsx(
398
+ CheckboxInner,
399
+ {
400
+ id: fieldId,
401
+ errorId,
402
+ descriptionId,
403
+ label,
404
+ checked: field.value ?? false,
405
+ onChange: (next) => field.onChange(next),
406
+ onBlur: field.onBlur,
407
+ disabled,
408
+ indeterminate,
409
+ description,
410
+ hasError,
411
+ className,
412
+ children: /* @__PURE__ */ jsx(
413
+ ErrorDisplay,
414
+ {
415
+ error: fieldState.error ? {
416
+ type: fieldState.error.type ?? "validation",
417
+ message: fieldState.error.message
418
+ } : void 0,
419
+ messageMap: errorMessageMap,
420
+ className: "mosaic-forms-error"
421
+ }
422
+ )
423
+ }
424
+ );
425
+ } });
426
+ }
427
+ function CheckboxInner(props) {
428
+ const {
429
+ id,
430
+ errorId,
431
+ descriptionId,
432
+ label,
433
+ checked,
434
+ onChange,
435
+ onBlur,
436
+ disabled,
437
+ indeterminate,
438
+ description,
439
+ hasError,
440
+ className,
441
+ children
442
+ } = props;
443
+ const ref = useRef(null);
444
+ useEffect(() => {
445
+ if (ref.current) {
446
+ ref.current.indeterminate = Boolean(indeterminate);
447
+ }
448
+ }, [indeterminate]);
449
+ const ariaChecked = resolveAriaChecked(checked, indeterminate);
450
+ const describedBy = resolveDescribedBy(descriptionId, description, hasError, errorId);
451
+ return /* @__PURE__ */ jsxs("div", { className: className ?? "mosaic-forms-checkbox", children: [
452
+ /* @__PURE__ */ jsxs("label", { htmlFor: id, className: "mosaic-forms-checkbox-label", children: [
453
+ /* @__PURE__ */ jsx(
454
+ "input",
455
+ {
456
+ ref,
457
+ type: "checkbox",
458
+ id,
459
+ checked,
460
+ disabled,
461
+ "aria-checked": ariaChecked,
462
+ "aria-invalid": hasError || void 0,
463
+ "aria-describedby": describedBy,
464
+ onChange: (e) => onChange(e.target.checked),
465
+ onBlur
466
+ }
467
+ ),
468
+ label
469
+ ] }),
470
+ description ? /* @__PURE__ */ jsx("span", { id: descriptionId, className: "mosaic-forms-checkbox-description", children: description }) : null,
471
+ /* @__PURE__ */ jsx("div", { id: errorId, children })
472
+ ] });
473
+ }
474
+
475
+ // src/components/forms/MultiSelect.logic.ts
476
+ function getAvailableOptions(options, selected) {
477
+ const set = new Set(selected);
478
+ return options.filter((opt) => !set.has(opt.value));
479
+ }
480
+ function getSelectedOptions(options, selected) {
481
+ const byValue = new Map(options.map((opt) => [opt.value, opt]));
482
+ const out = [];
483
+ for (const value of selected) {
484
+ const opt = byValue.get(value);
485
+ if (opt) out.push(opt);
486
+ }
487
+ return out;
488
+ }
489
+ function filterBySearch(options, query) {
490
+ const q = query.trim().toLowerCase();
491
+ if (q.length === 0) return [...options];
492
+ return options.filter(
493
+ (opt) => opt.label.toLowerCase().includes(q) || opt.value.toLowerCase().includes(q)
494
+ );
495
+ }
496
+ function addValue(current, value, maxItems) {
497
+ if (current.includes(value)) return current;
498
+ if (typeof maxItems === "number" && current.length >= maxItems) return current;
499
+ return [...current, value];
500
+ }
501
+ function removeValue(current, value) {
502
+ return current.filter((v) => v !== value);
503
+ }
504
+ function isAtMaxItems(current, maxItems) {
505
+ return typeof maxItems === "number" && current.length >= maxItems;
506
+ }
507
+ var MultiSelectOptionSchema = z.object({
508
+ value: z.string().min(1),
509
+ label: z.string().min(1)
510
+ });
511
+ var MultiSelectPropsSchema = z.object({
512
+ name: z.string().min(1),
513
+ label: z.string().min(1),
514
+ options: z.array(MultiSelectOptionSchema).min(1),
515
+ placeholder: z.string().optional(),
516
+ disabled: z.boolean().optional(),
517
+ searchable: z.boolean().optional(),
518
+ maxItems: z.number().int().positive().optional()
519
+ });
520
+ function MultiSelect(props) {
521
+ const parsed = MultiSelectPropsSchema.parse({
522
+ name: props.name,
523
+ label: props.label,
524
+ options: props.options,
525
+ placeholder: props.placeholder,
526
+ disabled: props.disabled,
527
+ searchable: props.searchable,
528
+ maxItems: props.maxItems
529
+ });
530
+ const { name, label, options, placeholder, disabled, searchable, maxItems } = parsed;
531
+ const reactId = useId();
532
+ const triggerId = `${reactId}-trigger`;
533
+ const listboxId = `${reactId}-listbox`;
534
+ const labelId = `${reactId}-label`;
535
+ return /* @__PURE__ */ jsx(FormField, { name, children: ({ field }) => {
536
+ const value = Array.isArray(field.value) ? field.value : [];
537
+ return /* @__PURE__ */ jsx(
538
+ Inner,
539
+ {
540
+ triggerId,
541
+ listboxId,
542
+ labelId,
543
+ label,
544
+ options,
545
+ value,
546
+ disabled,
547
+ searchable,
548
+ maxItems,
549
+ placeholder,
550
+ onChange: (next) => field.onChange(next),
551
+ onBlur: () => field.onBlur(),
552
+ className: props.className
553
+ }
554
+ );
555
+ } });
556
+ }
557
+ function Inner(p) {
558
+ const [open, setOpen] = useState(false);
559
+ const [query, setQuery] = useState("");
560
+ const [activeIndex, setActiveIndex] = useState(0);
561
+ const triggerRef = useRef(null);
562
+ const selectedOptions = useMemo(
563
+ () => getSelectedOptions(p.options, p.value),
564
+ [p.options, p.value]
565
+ );
566
+ const availableAfterSelect = useMemo(
567
+ () => getAvailableOptions(p.options, p.value),
568
+ [p.options, p.value]
569
+ );
570
+ const visibleOptions = useMemo(
571
+ () => p.searchable ? filterBySearch(availableAfterSelect, query) : availableAfterSelect,
572
+ [availableAfterSelect, p.searchable, query]
573
+ );
574
+ const atMax = isAtMaxItems(p.value, p.maxItems);
575
+ const handleSelect = useCallback(
576
+ (val) => {
577
+ const next = addValue(p.value, val, p.maxItems);
578
+ if (next !== p.value) {
579
+ p.onChange(next);
580
+ }
581
+ setQuery("");
582
+ },
583
+ [p]
584
+ );
585
+ const handleRemove = useCallback(
586
+ (val) => {
587
+ p.onChange(removeValue(p.value, val));
588
+ },
589
+ [p]
590
+ );
591
+ const handleTriggerKeyDown = useCallback(
592
+ (e) => {
593
+ if (p.disabled) return;
594
+ if (e.key === "Backspace" || e.key === "Delete") {
595
+ const last = p.value[p.value.length - 1];
596
+ if (last !== void 0) {
597
+ e.preventDefault();
598
+ handleRemove(last);
599
+ }
600
+ return;
601
+ }
602
+ if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
603
+ e.preventDefault();
604
+ setOpen(true);
605
+ setActiveIndex(0);
606
+ return;
607
+ }
608
+ if (e.key === "Escape") {
609
+ setOpen(false);
610
+ }
611
+ },
612
+ [p.disabled, p.value, handleRemove]
613
+ );
614
+ const handleListKeyDown = useCallback(
615
+ (e) => {
616
+ if (e.key === "ArrowDown") {
617
+ e.preventDefault();
618
+ setActiveIndex((i) => Math.min(i + 1, Math.max(visibleOptions.length - 1, 0)));
619
+ return;
620
+ }
621
+ if (e.key === "ArrowUp") {
622
+ e.preventDefault();
623
+ setActiveIndex((i) => Math.max(i - 1, 0));
624
+ return;
625
+ }
626
+ if (e.key === "Enter") {
627
+ e.preventDefault();
628
+ const opt = visibleOptions[activeIndex];
629
+ if (opt) handleSelect(opt.value);
630
+ return;
631
+ }
632
+ if (e.key === "Escape") {
633
+ e.preventDefault();
634
+ setOpen(false);
635
+ triggerRef.current?.focus();
636
+ }
637
+ },
638
+ [visibleOptions, activeIndex, handleSelect]
639
+ );
640
+ return /* @__PURE__ */ jsxs("div", { className: p.className ?? "mosaic-multiselect", onBlur: p.onBlur, children: [
641
+ /* @__PURE__ */ jsx("span", { id: p.labelId, className: "mosaic-multiselect-label", children: p.label }),
642
+ /* @__PURE__ */ jsx("div", { className: "mosaic-multiselect-chips", "data-testid": "multiselect-chips", children: selectedOptions.map((opt) => /* @__PURE__ */ jsxs("span", { className: "mosaic-multiselect-chip", "data-chip-value": opt.value, children: [
643
+ opt.label,
644
+ /* @__PURE__ */ jsx(
645
+ "button",
646
+ {
647
+ type: "button",
648
+ "aria-label": `Remove ${opt.label}`,
649
+ "data-testid": `chip-remove-${opt.value}`,
650
+ className: "mosaic-multiselect-chip-remove",
651
+ disabled: p.disabled,
652
+ onClick: () => handleRemove(opt.value),
653
+ children: "\xD7"
654
+ }
655
+ )
656
+ ] }, opt.value)) }),
657
+ /* @__PURE__ */ jsx(
658
+ "button",
659
+ {
660
+ ref: triggerRef,
661
+ id: p.triggerId,
662
+ type: "button",
663
+ role: "combobox",
664
+ "aria-multiselectable": true,
665
+ "aria-expanded": open,
666
+ "aria-controls": p.listboxId,
667
+ "aria-labelledby": p.labelId,
668
+ "aria-disabled": p.disabled || atMax || void 0,
669
+ disabled: p.disabled,
670
+ "data-testid": "multiselect-trigger",
671
+ className: "mosaic-multiselect-trigger",
672
+ onClick: () => !p.disabled && setOpen((o) => !o),
673
+ onKeyDown: handleTriggerKeyDown,
674
+ children: p.value.length === 0 ? p.placeholder ?? "Select\u2026" : `${p.value.length} selected`
675
+ }
676
+ ),
677
+ open ? /* @__PURE__ */ jsxs("div", { className: "mosaic-multiselect-panel", children: [
678
+ p.searchable ? /* @__PURE__ */ jsx(
679
+ "input",
680
+ {
681
+ type: "text",
682
+ "aria-label": "Search options",
683
+ "data-testid": "multiselect-search",
684
+ className: "mosaic-multiselect-search",
685
+ value: query,
686
+ onChange: (e) => {
687
+ setQuery(e.target.value);
688
+ setActiveIndex(0);
689
+ }
690
+ }
691
+ ) : null,
692
+ /* @__PURE__ */ jsx(
693
+ "ul",
694
+ {
695
+ id: p.listboxId,
696
+ role: "listbox",
697
+ "aria-multiselectable": true,
698
+ "aria-labelledby": p.labelId,
699
+ tabIndex: -1,
700
+ className: "mosaic-multiselect-listbox",
701
+ onKeyDown: handleListKeyDown,
702
+ children: visibleOptions.length === 0 ? /* @__PURE__ */ jsx("li", { className: "mosaic-multiselect-empty", children: "No options" }) : visibleOptions.map((opt, i) => (
703
+ // biome-ignore lint/a11y/useFocusableInteractive: focus stays on the listbox; APG combobox active-descendant pattern.
704
+ // biome-ignore lint/a11y/useKeyWithClickEvents: keyboard handled at listbox level per APG.
705
+ /* @__PURE__ */ jsx(
706
+ "li",
707
+ {
708
+ role: "option",
709
+ "aria-selected": false,
710
+ "data-testid": `option-${opt.value}`,
711
+ "data-active": i === activeIndex || void 0,
712
+ className: "mosaic-multiselect-option",
713
+ onClick: () => handleSelect(opt.value),
714
+ children: opt.label
715
+ },
716
+ opt.value
717
+ )
718
+ ))
719
+ }
720
+ )
721
+ ] }) : null
722
+ ] });
723
+ }
724
+
725
+ // src/components/forms/RadioGroup.logic.ts
726
+ function getNavKeys(orientation) {
727
+ return orientation === "vertical" ? { prev: "ArrowUp", next: "ArrowDown" } : { prev: "ArrowLeft", next: "ArrowRight" };
728
+ }
729
+ function findNextEnabledIndex(options, currentIndex, direction) {
730
+ const len = options.length;
731
+ if (len === 0) return currentIndex;
732
+ let i = (currentIndex + direction + len) % len;
733
+ for (let tries = 0; tries < len; tries++) {
734
+ if (!options[i]?.disabled) return i;
735
+ i = (i + direction + len) % len;
736
+ }
737
+ return currentIndex;
738
+ }
739
+ function findFirstEnabledIndex(options) {
740
+ const idx = options.findIndex((o) => !o.disabled);
741
+ return idx === -1 ? 0 : idx;
742
+ }
743
+ function findLastEnabledIndex(options) {
744
+ for (let i = options.length - 1; i >= 0; i--) {
745
+ if (!options[i]?.disabled) return i;
746
+ }
747
+ return options.length - 1;
748
+ }
749
+ function findSelectedIndex(options, selectedValue) {
750
+ if (selectedValue === void 0 || selectedValue === "") return -1;
751
+ return options.findIndex((o) => o.value === selectedValue);
752
+ }
753
+ function getRovingTabIndex(options, selectedValue) {
754
+ const selectedIdx = findSelectedIndex(options, selectedValue);
755
+ if (selectedIdx >= 0) return selectedIdx;
756
+ return findFirstEnabledIndex(options);
757
+ }
758
+ function getGroupLabelId(instanceId, name) {
759
+ return `${instanceId}-${name}-label`;
760
+ }
761
+ function getOptionLabelId(instanceId, name, optionValue) {
762
+ return `${instanceId}-${name}-${optionValue}-label`;
763
+ }
764
+ function getOptionDescriptionId(instanceId, name, optionValue) {
765
+ return `${instanceId}-${name}-${optionValue}-desc`;
766
+ }
767
+ function getGroupClasses(orientation) {
768
+ return orientation === "vertical" ? "flex flex-col gap-2" : "flex flex-row flex-wrap gap-4";
769
+ }
770
+ function getOptionRowClasses(isDisabled, orientation) {
771
+ const base = "flex items-start gap-2";
772
+ const orient = orientation === "vertical" ? "flex-row" : "flex-row";
773
+ const state = isDisabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer";
774
+ return [base, orient, state].join(" ");
775
+ }
776
+ function getRadioControlClasses(isSelected, isDisabled) {
777
+ const base = "inline-flex items-center justify-center w-4 h-4 mt-0.5 rounded-full border-2 transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 shrink-0";
778
+ const state = isDisabled ? "border-slate-300 bg-slate-100" : isSelected ? "border-blue-600 bg-white" : "border-slate-400 bg-white hover:border-slate-600";
779
+ return [base, state].join(" ");
780
+ }
781
+ function isActivationKey(key) {
782
+ return key === " " || key === "Space" || key === "Enter";
783
+ }
784
+ var RadioGroupOptionSchema = z.object({
785
+ value: z.string().min(1),
786
+ label: z.string().min(1),
787
+ description: z.string().optional(),
788
+ disabled: z.boolean().optional().default(false)
789
+ });
790
+ var RadioGroupPropsSchema = z.object({
791
+ name: z.string().min(1),
792
+ label: z.string().min(1),
793
+ options: z.array(RadioGroupOptionSchema).min(1),
794
+ orientation: z.enum(["vertical", "horizontal"]).default("vertical"),
795
+ disabled: z.boolean().optional().default(false),
796
+ locale: z.enum(["en", "fr"]).default("en")
797
+ });
798
+ function validateRadioGroupProps(raw) {
799
+ return RadioGroupPropsSchema.parse(raw);
800
+ }
801
+ function RadioGroup(props) {
802
+ const validated = validateRadioGroupProps({
803
+ name: props.name,
804
+ label: props.label,
805
+ options: props.options,
806
+ orientation: props.orientation,
807
+ disabled: props.disabled
808
+ });
809
+ const { name, label, options, orientation, disabled } = validated;
810
+ const instanceId = useId();
811
+ const groupLabelId = getGroupLabelId(instanceId, name);
812
+ const { prev: prevKey, next: nextKey } = getNavKeys(orientation);
813
+ const radioRefs = useRef([]);
814
+ const focusAndSelect = useCallback(
815
+ (index, onChange) => {
816
+ const el = radioRefs.current[index];
817
+ if (el) el.focus();
818
+ const opt = options[index];
819
+ if (opt && !opt.disabled) {
820
+ onChange(opt.value);
821
+ }
822
+ },
823
+ [options]
824
+ );
825
+ return /* @__PURE__ */ jsx(FormField, { name, children: ({ field, fieldState }) => {
826
+ const selectedValue = field.value;
827
+ const rovingIdx = getRovingTabIndex(options, selectedValue);
828
+ const handleKeyDown = (e, currentIndex) => {
829
+ const { key } = e;
830
+ if (key === prevKey || key === nextKey) {
831
+ e.preventDefault();
832
+ const direction = key === nextKey ? 1 : -1;
833
+ const nextIdx = findNextEnabledIndex(options, currentIndex, direction);
834
+ focusAndSelect(nextIdx, field.onChange);
835
+ return;
836
+ }
837
+ if (key === "Home") {
838
+ e.preventDefault();
839
+ focusAndSelect(findFirstEnabledIndex(options), field.onChange);
840
+ return;
841
+ }
842
+ if (key === "End") {
843
+ e.preventDefault();
844
+ focusAndSelect(findLastEnabledIndex(options), field.onChange);
845
+ return;
846
+ }
847
+ if (isActivationKey(key)) {
848
+ e.preventDefault();
849
+ const opt = options[currentIndex];
850
+ if (opt && !opt.disabled) {
851
+ field.onChange(opt.value);
852
+ }
853
+ }
854
+ };
855
+ return /* @__PURE__ */ jsxs("div", { className: props.className ?? "mosaic-forms-radiogroup", children: [
856
+ /* @__PURE__ */ jsx("span", { id: groupLabelId, className: "mosaic-forms-radiogroup-label", children: label }),
857
+ /* @__PURE__ */ jsx(
858
+ "div",
859
+ {
860
+ role: "radiogroup",
861
+ "aria-labelledby": groupLabelId,
862
+ "aria-orientation": orientation,
863
+ "aria-disabled": disabled || void 0,
864
+ className: getGroupClasses(orientation),
865
+ children: options.map((option, index) => {
866
+ const isSelected = selectedValue === option.value;
867
+ const isDisabled = !!(disabled || option.disabled);
868
+ const optLabelId = getOptionLabelId(instanceId, name, option.value);
869
+ const optDescId = option.description ? getOptionDescriptionId(instanceId, name, option.value) : void 0;
870
+ const tabIdx = index === rovingIdx ? 0 : -1;
871
+ return /* @__PURE__ */ jsxs("div", { className: getOptionRowClasses(isDisabled, orientation), children: [
872
+ /* @__PURE__ */ jsx(
873
+ "input",
874
+ {
875
+ ref: (el) => {
876
+ radioRefs.current[index] = el;
877
+ },
878
+ type: "radio",
879
+ id: `${instanceId}-${name}-${option.value}`,
880
+ name: field.name,
881
+ value: option.value,
882
+ checked: isSelected,
883
+ disabled: isDisabled,
884
+ "aria-labelledby": optLabelId,
885
+ "aria-describedby": optDescId,
886
+ tabIndex: isDisabled ? -1 : tabIdx,
887
+ className: "mosaic-forms-radiogroup-input sr-only",
888
+ onChange: () => {
889
+ field.onChange(option.value);
890
+ },
891
+ onKeyDown: (e) => {
892
+ if (!isDisabled) handleKeyDown(e, index);
893
+ },
894
+ onBlur: field.onBlur
895
+ }
896
+ ),
897
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5", children: [
898
+ /* @__PURE__ */ jsx("span", { id: optLabelId, className: "mosaic-forms-radiogroup-option-label", children: option.label }),
899
+ option.description ? /* @__PURE__ */ jsx(
900
+ "span",
901
+ {
902
+ id: optDescId,
903
+ className: "mosaic-forms-radiogroup-option-description text-sm text-slate-500",
904
+ children: option.description
905
+ }
906
+ ) : null
907
+ ] })
908
+ ] }, option.value);
909
+ })
910
+ }
911
+ ),
912
+ fieldState.error ? /* @__PURE__ */ jsx("span", { role: "alert", className: "mosaic-forms-radiogroup-error text-sm text-red-600", children: fieldState.error.message }) : null
913
+ ] });
914
+ } });
915
+ }
916
+
917
+ // src/components/forms/Select.logic.ts
918
+ function getTriggerId(instanceId) {
919
+ return `${instanceId}-trigger`;
920
+ }
921
+ function getListboxId(instanceId) {
922
+ return `${instanceId}-listbox`;
923
+ }
924
+ function getOptionId(instanceId, value) {
925
+ return `${instanceId}-opt-${value}`;
926
+ }
927
+ function filterOptions(options, query) {
928
+ const q = query.trim().toLowerCase();
929
+ if (q === "") return options;
930
+ return options.filter((o) => o.label.toLowerCase().includes(q));
931
+ }
932
+ function indexOfValue(options, value) {
933
+ if (value == null) return -1;
934
+ return options.findIndex((o) => o.value === value);
935
+ }
936
+ function findNextEnabledIndex2(options, startIndex, direction) {
937
+ const len = options.length;
938
+ if (len === 0) return -1;
939
+ let i = startIndex;
940
+ for (let tries = 0; tries < len; tries++) {
941
+ i = (i + direction + len) % len;
942
+ if (!options[i]?.disabled) return i;
943
+ }
944
+ return -1;
945
+ }
946
+ function findFirstEnabledIndex2(options) {
947
+ const idx = options.findIndex((o) => !o.disabled);
948
+ return idx === -1 ? -1 : idx;
949
+ }
950
+ function findLastEnabledIndex2(options) {
951
+ for (let i = options.length - 1; i >= 0; i--) {
952
+ if (!options[i]?.disabled) return i;
953
+ }
954
+ return -1;
955
+ }
956
+ function findByTypeAhead(options, buffer) {
957
+ if (buffer === "") return -1;
958
+ const needle = buffer.toLowerCase();
959
+ for (let i = 0; i < options.length; i++) {
960
+ const o = options[i];
961
+ if (!o || o.disabled) continue;
962
+ if (o.label.toLowerCase().startsWith(needle)) return i;
963
+ }
964
+ return -1;
965
+ }
966
+ function getTriggerClasses(isDisabled, isOpen) {
967
+ const base = "inline-flex items-center justify-between gap-2 w-full rounded-md border px-3 py-2 text-sm transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600";
968
+ const state = isDisabled ? "border-slate-200 text-slate-400 bg-slate-50 cursor-not-allowed" : isOpen ? "border-blue-600 bg-white text-slate-900" : "border-slate-300 bg-white text-slate-900 hover:border-slate-400";
969
+ return `${base} ${state}`;
970
+ }
971
+ function getListboxClasses() {
972
+ return "absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-md border border-slate-200 bg-white py-1 shadow-lg focus:outline-none";
973
+ }
974
+ function getOptionClasses(isActive, isSelected, isDisabled) {
975
+ const base = "flex items-center px-3 py-2 text-sm select-none";
976
+ if (isDisabled) return `${base} text-slate-400 cursor-not-allowed`;
977
+ const active = isActive ? "bg-blue-50 text-blue-900" : "text-slate-900";
978
+ const selected = isSelected ? "font-medium" : "";
979
+ return `${base} ${active} ${selected} cursor-pointer`;
980
+ }
981
+ function getSearchInputClasses() {
982
+ return "w-full px-3 py-2 text-sm border-b border-slate-200 focus:outline-none focus:border-blue-600";
983
+ }
984
+ function resolveSelectedLabel(options, value) {
985
+ if (value == null) return null;
986
+ const o = options.find((opt) => opt.value === value);
987
+ return o?.label ?? null;
988
+ }
989
+ function Select({
990
+ name,
991
+ label,
992
+ options,
993
+ placeholder,
994
+ disabled = false,
995
+ searchable = false,
996
+ className,
997
+ errorMessageMap
998
+ }) {
999
+ const reactId = useId();
1000
+ const instanceId = `mosaic-select-${reactId.replace(/[^a-zA-Z0-9-]/g, "")}`;
1001
+ const triggerId = getTriggerId(instanceId);
1002
+ const listboxId = getListboxId(instanceId);
1003
+ const labelId = `${instanceId}-label`;
1004
+ const errorId = `${instanceId}-error`;
1005
+ return /* @__PURE__ */ jsx(FormField, { name, children: ({ field, fieldState }) => {
1006
+ const hasError = Boolean(fieldState.error);
1007
+ return /* @__PURE__ */ jsx(
1008
+ SelectInner,
1009
+ {
1010
+ field,
1011
+ label,
1012
+ labelId,
1013
+ triggerId,
1014
+ listboxId,
1015
+ errorId,
1016
+ instanceId,
1017
+ options,
1018
+ placeholder,
1019
+ disabled,
1020
+ searchable,
1021
+ className,
1022
+ hasError,
1023
+ children: /* @__PURE__ */ jsx(
1024
+ ErrorDisplay,
1025
+ {
1026
+ error: fieldState.error ? {
1027
+ type: fieldState.error.type ?? "validation",
1028
+ message: fieldState.error.message
1029
+ } : void 0,
1030
+ messageMap: errorMessageMap,
1031
+ className: "mosaic-forms-error"
1032
+ }
1033
+ )
1034
+ }
1035
+ );
1036
+ } });
1037
+ }
1038
+ function SelectInner({
1039
+ field,
1040
+ label,
1041
+ labelId,
1042
+ triggerId,
1043
+ listboxId,
1044
+ errorId,
1045
+ instanceId,
1046
+ options,
1047
+ placeholder,
1048
+ disabled,
1049
+ searchable,
1050
+ className,
1051
+ hasError,
1052
+ children
1053
+ }) {
1054
+ const [isOpen, setIsOpen] = useState(false);
1055
+ const [query, setQuery] = useState("");
1056
+ const [activeIndex, setActiveIndex] = useState(-1);
1057
+ const triggerRef = useRef(null);
1058
+ const searchRef = useRef(null);
1059
+ const typeAheadRef = useRef({
1060
+ buffer: "",
1061
+ timer: null
1062
+ });
1063
+ const visibleOptions = useMemo(
1064
+ () => searchable ? filterOptions(options, query) : options,
1065
+ [options, query, searchable]
1066
+ );
1067
+ useEffect(() => {
1068
+ if (!isOpen) return;
1069
+ const selectedIdx = indexOfValue(visibleOptions, field.value);
1070
+ if (selectedIdx >= 0 && !visibleOptions[selectedIdx]?.disabled) {
1071
+ setActiveIndex(selectedIdx);
1072
+ } else {
1073
+ setActiveIndex(findFirstEnabledIndex2(visibleOptions));
1074
+ }
1075
+ }, [isOpen, visibleOptions, field.value]);
1076
+ const closePopup = useCallback((restoreFocus = true) => {
1077
+ setIsOpen(false);
1078
+ setQuery("");
1079
+ if (restoreFocus) triggerRef.current?.focus();
1080
+ }, []);
1081
+ const commitOption = useCallback(
1082
+ (idx) => {
1083
+ const opt = visibleOptions[idx];
1084
+ if (!opt || opt.disabled) return;
1085
+ field.onChange(opt.value);
1086
+ closePopup(true);
1087
+ field.onBlur();
1088
+ },
1089
+ [visibleOptions, field, closePopup]
1090
+ );
1091
+ const handleKeyDown = useCallback(
1092
+ (e) => {
1093
+ if (disabled) return;
1094
+ if (!isOpen) {
1095
+ if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown" || e.key === "ArrowUp") {
1096
+ e.preventDefault();
1097
+ setIsOpen(true);
1098
+ }
1099
+ return;
1100
+ }
1101
+ switch (e.key) {
1102
+ case "ArrowDown": {
1103
+ e.preventDefault();
1104
+ const start = activeIndex < 0 ? -1 : activeIndex;
1105
+ const next = findNextEnabledIndex2(visibleOptions, start, 1);
1106
+ if (next >= 0) setActiveIndex(next);
1107
+ break;
1108
+ }
1109
+ case "ArrowUp": {
1110
+ e.preventDefault();
1111
+ const start = activeIndex < 0 ? visibleOptions.length : activeIndex;
1112
+ const prev = findNextEnabledIndex2(visibleOptions, start, -1);
1113
+ if (prev >= 0) setActiveIndex(prev);
1114
+ break;
1115
+ }
1116
+ case "Home": {
1117
+ e.preventDefault();
1118
+ const first = findFirstEnabledIndex2(visibleOptions);
1119
+ if (first >= 0) setActiveIndex(first);
1120
+ break;
1121
+ }
1122
+ case "End": {
1123
+ e.preventDefault();
1124
+ const last = findLastEnabledIndex2(visibleOptions);
1125
+ if (last >= 0) setActiveIndex(last);
1126
+ break;
1127
+ }
1128
+ case "Enter": {
1129
+ e.preventDefault();
1130
+ if (activeIndex >= 0) commitOption(activeIndex);
1131
+ break;
1132
+ }
1133
+ case "Escape": {
1134
+ e.preventDefault();
1135
+ closePopup(true);
1136
+ break;
1137
+ }
1138
+ default: {
1139
+ if (!searchable && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
1140
+ e.preventDefault();
1141
+ const ta = typeAheadRef.current;
1142
+ ta.buffer += e.key.toLowerCase();
1143
+ if (ta.timer) clearTimeout(ta.timer);
1144
+ ta.timer = setTimeout(() => {
1145
+ ta.buffer = "";
1146
+ }, 500);
1147
+ const idx = findByTypeAhead(visibleOptions, ta.buffer);
1148
+ if (idx >= 0) setActiveIndex(idx);
1149
+ }
1150
+ break;
1151
+ }
1152
+ }
1153
+ },
1154
+ [activeIndex, closePopup, commitOption, disabled, isOpen, searchable, visibleOptions]
1155
+ );
1156
+ const rootRef = useRef(null);
1157
+ useEffect(() => {
1158
+ if (!isOpen) return;
1159
+ function onDocClick(e) {
1160
+ if (!rootRef.current) return;
1161
+ if (!rootRef.current.contains(e.target)) {
1162
+ setIsOpen(false);
1163
+ setQuery("");
1164
+ field.onBlur();
1165
+ }
1166
+ }
1167
+ document.addEventListener("mousedown", onDocClick);
1168
+ return () => document.removeEventListener("mousedown", onDocClick);
1169
+ }, [isOpen, field]);
1170
+ const selectedLabel = resolveSelectedLabel(options, field.value);
1171
+ const activeId = activeIndex >= 0 ? getOptionId(instanceId, visibleOptions[activeIndex]?.value ?? "") : void 0;
1172
+ return /* @__PURE__ */ jsxs("div", { ref: rootRef, className: className ?? "relative inline-block w-full", children: [
1173
+ /* @__PURE__ */ jsx(
1174
+ "label",
1175
+ {
1176
+ id: labelId,
1177
+ htmlFor: triggerId,
1178
+ className: "block text-sm font-medium text-slate-700 mb-1",
1179
+ children: label
1180
+ }
1181
+ ),
1182
+ /* @__PURE__ */ jsxs(
1183
+ "button",
1184
+ {
1185
+ type: "button",
1186
+ id: triggerId,
1187
+ ref: (el) => {
1188
+ triggerRef.current = el;
1189
+ field.ref(el);
1190
+ },
1191
+ role: "combobox",
1192
+ "aria-haspopup": "listbox",
1193
+ "aria-expanded": isOpen,
1194
+ "aria-controls": listboxId,
1195
+ "aria-labelledby": labelId,
1196
+ "aria-activedescendant": isOpen ? activeId : void 0,
1197
+ "aria-invalid": hasError || void 0,
1198
+ "aria-describedby": hasError ? errorId : void 0,
1199
+ disabled,
1200
+ className: getTriggerClasses(disabled, isOpen),
1201
+ onClick: () => !disabled && setIsOpen((o) => !o),
1202
+ onKeyDown: handleKeyDown,
1203
+ onBlur: (e) => {
1204
+ if (rootRef.current?.contains(e.relatedTarget)) return;
1205
+ if (!isOpen) field.onBlur();
1206
+ },
1207
+ children: [
1208
+ /* @__PURE__ */ jsx("span", { className: selectedLabel ? "" : "text-slate-400", children: selectedLabel ?? placeholder ?? "" }),
1209
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u25BE" })
1210
+ ]
1211
+ }
1212
+ ),
1213
+ isOpen ? /* @__PURE__ */ jsxs("div", { className: getListboxClasses(), children: [
1214
+ searchable ? /* @__PURE__ */ jsx(
1215
+ "input",
1216
+ {
1217
+ ref: searchRef,
1218
+ type: "text",
1219
+ className: getSearchInputClasses(),
1220
+ value: query,
1221
+ onChange: (e) => setQuery(e.target.value),
1222
+ onKeyDown: handleKeyDown,
1223
+ placeholder: "Search\u2026",
1224
+ autoFocus: true,
1225
+ "aria-label": "Search options"
1226
+ }
1227
+ ) : null,
1228
+ /* @__PURE__ */ jsx("ul", { id: listboxId, role: "listbox", "aria-labelledby": labelId, tabIndex: -1, children: visibleOptions.length === 0 ? /* @__PURE__ */ jsx("li", { className: "px-3 py-2 text-sm text-slate-500", role: "presentation", children: "No options" }) : visibleOptions.map((opt, idx) => {
1229
+ const isSelected = opt.value === field.value;
1230
+ const isActive = idx === activeIndex;
1231
+ return /* @__PURE__ */ jsx(
1232
+ "li",
1233
+ {
1234
+ id: getOptionId(instanceId, opt.value),
1235
+ role: "option",
1236
+ "aria-selected": isSelected,
1237
+ "aria-disabled": opt.disabled || void 0,
1238
+ className: getOptionClasses(isActive, isSelected, !!opt.disabled),
1239
+ onMouseDown: (e) => {
1240
+ e.preventDefault();
1241
+ if (!opt.disabled) commitOption(idx);
1242
+ },
1243
+ onMouseEnter: () => {
1244
+ if (!opt.disabled) setActiveIndex(idx);
1245
+ },
1246
+ children: opt.label
1247
+ },
1248
+ opt.value
1249
+ );
1250
+ }) })
1251
+ ] }) : null,
1252
+ /* @__PURE__ */ jsx("div", { id: errorId, children })
1253
+ ] });
1254
+ }
1255
+ var FormProviderPropsSchema = z.object({
1256
+ name: z.string().optional()
1257
+ });
1258
+ var FormFieldPropsSchema = z.object({
1259
+ name: z.string().min(1)
1260
+ });
1261
+
1262
+ // src/components/forms/FormField.logic.ts
1263
+ function buildFieldId(formName, fieldName) {
1264
+ return formName ? `${formName}-${fieldName}` : fieldName;
1265
+ }
1266
+ var FieldErrorShapeSchema = z.object({
1267
+ type: z.string(),
1268
+ message: z.string().optional()
1269
+ });
1270
+ var ErrorDisplayPropsSchema = z.object({
1271
+ error: FieldErrorShapeSchema.optional(),
1272
+ messageMap: z.record(z.string(), z.string()).optional()
1273
+ });
1274
+ var InputTypeSchema = z.enum(["text", "email", "password", "number", "url"]);
1275
+ var InputPropsSchema = z.object({
1276
+ name: z.string().min(1),
1277
+ type: InputTypeSchema.default("text").optional(),
1278
+ placeholder: z.string().optional(),
1279
+ disabled: z.boolean().optional(),
1280
+ autoComplete: z.string().optional(),
1281
+ label: z.string().min(1)
1282
+ });
1283
+ var TextareaPropsSchema = z.object({
1284
+ name: z.string().min(1),
1285
+ rows: z.number().int().min(1).default(3),
1286
+ maxLength: z.number().int().min(1).optional(),
1287
+ autoResize: z.boolean().default(false),
1288
+ placeholder: z.string().optional(),
1289
+ disabled: z.boolean().optional(),
1290
+ label: z.string().optional()
1291
+ });
1292
+ function validateTextareaProps(raw) {
1293
+ return TextareaPropsSchema.parse(raw);
1294
+ }
1295
+ var FieldArrayPropsSchema = z.object({
1296
+ name: z.string().min(1)
1297
+ });
1298
+ var UseFieldArrayOptionsSchema = z.object({
1299
+ name: z.string().min(1)
1300
+ });
1301
+ var CheckboxPropsSchema = z.object({
1302
+ name: z.string().min(1),
1303
+ label: z.string().min(1),
1304
+ disabled: z.boolean().optional(),
1305
+ indeterminate: z.boolean().optional(),
1306
+ description: z.string().optional()
1307
+ });
1308
+ function validateCheckboxProps(raw) {
1309
+ return CheckboxPropsSchema.parse(raw);
1310
+ }
1311
+ var SelectOptionSchema = z.object({
1312
+ value: z.string().min(1),
1313
+ label: z.string().min(1),
1314
+ disabled: z.boolean().optional().default(false)
1315
+ });
1316
+ var SelectPropsSchema = z.object({
1317
+ name: z.string().min(1),
1318
+ label: z.string().min(1),
1319
+ options: z.array(SelectOptionSchema).min(1),
1320
+ placeholder: z.string().optional(),
1321
+ disabled: z.boolean().optional().default(false),
1322
+ searchable: z.boolean().optional().default(false),
1323
+ locale: z.enum(["en", "fr"]).default("en")
1324
+ });
1325
+ function validateSelectProps(raw) {
1326
+ return SelectPropsSchema.parse(raw);
1327
+ }
1328
+
1329
+ export { Checkbox, CheckboxPropsSchema, DEFAULT_TEXTAREA_ROWS, ErrorDisplay, ErrorDisplayPropsSchema, FORM_PROVIDER_CONTEXT_MISSING, FieldArray, FieldArrayPropsSchema, FieldErrorShapeSchema, FormField, FormFieldPropsSchema, FormProvider, FormProviderPropsSchema, Input, InputPropsSchema, InputTypeSchema, MosaicFormModeSchema, MultiSelect, MultiSelectOptionSchema, MultiSelectPropsSchema, RadioGroup, RadioGroupOptionSchema, RadioGroupPropsSchema, Select, SelectOptionSchema, SelectPropsSchema, SubmitButton, SubmitButtonPropsSchema, Textarea, TextareaPropsSchema, UseFieldArrayOptionsSchema, UseMosaicFormOptionsSchema, addValue, buildAddAriaLabel, buildErrorDescribedBy, buildFieldId, buildInputId, buildListAriaLabel, buildRemoveAriaLabel, clampToMaxLength, computeAutoResizeHeight, computeSubmitButtonState, filterBySearch, filterOptions, findByTypeAhead, findFirstEnabledIndex, findLastEnabledIndex, findNextEnabledIndex, findSelectedIndex, formatErrorMessage, getAvailableOptions, getGroupClasses, getGroupLabelId, getNavKeys, getOptionDescriptionId, getOptionLabelId, getOptionRowClasses, getRadioControlClasses, getRovingTabIndex, getSelectedOptions, indexOfValue, isActivationKey, isAtMaxItems, nextFocusIndexAfterRemove, removeValue, resolveAriaChecked, resolveDescribedBy, resolveHtmlType, resolveRows, resolveSelectedLabel, toRhfMode, useFieldArray, useMosaicForm, useMosaicFormContext, validateCheckboxProps, validateOptions, validateRadioGroupProps, validateSelectProps, validateSubmitButtonProps, validateTextareaProps };
1330
+ //# sourceMappingURL=forms.js.map
1331
+ //# sourceMappingURL=forms.js.map