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