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