@vantageos/mosaic 0.2.2 → 0.3.0

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