@ansh-ui/vue 0.2.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 (109) hide show
  1. package/LICENSE +21 -0
  2. package/dist/chunk-2VSROZ2N.js +1281 -0
  3. package/dist/chunk-2VSROZ2N.js.map +1 -0
  4. package/dist/chunk-5BQEZYG6.cjs +1294 -0
  5. package/dist/chunk-5BQEZYG6.cjs.map +1 -0
  6. package/dist/chunk-6732H4BQ.cjs +28 -0
  7. package/dist/chunk-6732H4BQ.cjs.map +1 -0
  8. package/dist/chunk-6ABPGOJP.js +722 -0
  9. package/dist/chunk-6ABPGOJP.js.map +1 -0
  10. package/dist/chunk-6XJOB6LE.js +62 -0
  11. package/dist/chunk-6XJOB6LE.js.map +1 -0
  12. package/dist/chunk-774WBLDR.cjs +24 -0
  13. package/dist/chunk-774WBLDR.cjs.map +1 -0
  14. package/dist/chunk-B6OXKW5Q.js +567 -0
  15. package/dist/chunk-B6OXKW5Q.js.map +1 -0
  16. package/dist/chunk-DMPNT3MY.cjs +64 -0
  17. package/dist/chunk-DMPNT3MY.cjs.map +1 -0
  18. package/dist/chunk-EISHOEB6.cjs +158 -0
  19. package/dist/chunk-EISHOEB6.cjs.map +1 -0
  20. package/dist/chunk-EVO23GUS.js +223 -0
  21. package/dist/chunk-EVO23GUS.js.map +1 -0
  22. package/dist/chunk-FVKLCGA4.js +1035 -0
  23. package/dist/chunk-FVKLCGA4.js.map +1 -0
  24. package/dist/chunk-IOXGBPOV.cjs +3681 -0
  25. package/dist/chunk-IOXGBPOV.cjs.map +1 -0
  26. package/dist/chunk-J2LA4GRP.cjs +1051 -0
  27. package/dist/chunk-J2LA4GRP.cjs.map +1 -0
  28. package/dist/chunk-JQ4YLXXM.cjs +580 -0
  29. package/dist/chunk-JQ4YLXXM.cjs.map +1 -0
  30. package/dist/chunk-KCA7RNJ4.cjs +781 -0
  31. package/dist/chunk-KCA7RNJ4.cjs.map +1 -0
  32. package/dist/chunk-NJDDNO4C.js +23 -0
  33. package/dist/chunk-NJDDNO4C.js.map +1 -0
  34. package/dist/chunk-OAQ7MDYX.js +22 -0
  35. package/dist/chunk-OAQ7MDYX.js.map +1 -0
  36. package/dist/chunk-PP6OYURM.cjs +238 -0
  37. package/dist/chunk-PP6OYURM.cjs.map +1 -0
  38. package/dist/chunk-SHX2JGID.cjs +269 -0
  39. package/dist/chunk-SHX2JGID.cjs.map +1 -0
  40. package/dist/chunk-SOYRICHU.js +3645 -0
  41. package/dist/chunk-SOYRICHU.js.map +1 -0
  42. package/dist/chunk-T425FIC7.js +773 -0
  43. package/dist/chunk-T425FIC7.js.map +1 -0
  44. package/dist/chunk-TMG3M6TK.js +152 -0
  45. package/dist/chunk-TMG3M6TK.js.map +1 -0
  46. package/dist/chunk-XLLSS464.cjs +286 -0
  47. package/dist/chunk-XLLSS464.cjs.map +1 -0
  48. package/dist/chunk-XZ5ZFLZM.js +280 -0
  49. package/dist/chunk-XZ5ZFLZM.js.map +1 -0
  50. package/dist/chunk-Z7XOFKM4.cjs +733 -0
  51. package/dist/chunk-Z7XOFKM4.cjs.map +1 -0
  52. package/dist/chunk-ZGMXOMCB.js +266 -0
  53. package/dist/chunk-ZGMXOMCB.js.map +1 -0
  54. package/dist/data.cjs +70 -0
  55. package/dist/data.cjs.map +1 -0
  56. package/dist/data.d.cts +963 -0
  57. package/dist/data.d.ts +963 -0
  58. package/dist/data.js +5 -0
  59. package/dist/data.js.map +1 -0
  60. package/dist/form.cjs +151 -0
  61. package/dist/form.cjs.map +1 -0
  62. package/dist/form.d.cts +2639 -0
  63. package/dist/form.d.ts +2639 -0
  64. package/dist/form.js +6 -0
  65. package/dist/form.js.map +1 -0
  66. package/dist/index.cjs +1158 -0
  67. package/dist/index.cjs.map +1 -0
  68. package/dist/index.d.cts +439 -0
  69. package/dist/index.d.ts +439 -0
  70. package/dist/index.js +547 -0
  71. package/dist/index.js.map +1 -0
  72. package/dist/layout.cjs +67 -0
  73. package/dist/layout.cjs.map +1 -0
  74. package/dist/layout.d.cts +729 -0
  75. package/dist/layout.d.ts +729 -0
  76. package/dist/layout.js +6 -0
  77. package/dist/layout.js.map +1 -0
  78. package/dist/motion.cjs +50 -0
  79. package/dist/motion.cjs.map +1 -0
  80. package/dist/motion.d.cts +681 -0
  81. package/dist/motion.d.ts +681 -0
  82. package/dist/motion.js +5 -0
  83. package/dist/motion.js.map +1 -0
  84. package/dist/navigation.cjs +48 -0
  85. package/dist/navigation.cjs.map +1 -0
  86. package/dist/navigation.d.cts +515 -0
  87. package/dist/navigation.d.ts +515 -0
  88. package/dist/navigation.js +7 -0
  89. package/dist/navigation.js.map +1 -0
  90. package/dist/overlay.cjs +61 -0
  91. package/dist/overlay.cjs.map +1 -0
  92. package/dist/overlay.d.cts +772 -0
  93. package/dist/overlay.d.ts +772 -0
  94. package/dist/overlay.js +8 -0
  95. package/dist/overlay.js.map +1 -0
  96. package/dist/position-Cn7qP1pn.d.cts +50 -0
  97. package/dist/position-Cn7qP1pn.d.ts +50 -0
  98. package/dist/primitives.cjs +129 -0
  99. package/dist/primitives.cjs.map +1 -0
  100. package/dist/primitives.d.cts +300 -0
  101. package/dist/primitives.d.ts +300 -0
  102. package/dist/primitives.js +8 -0
  103. package/dist/primitives.js.map +1 -0
  104. package/dist/sidebar-CQqEmpid.d.cts +124 -0
  105. package/dist/sidebar-CTZ-Ckko.d.ts +124 -0
  106. package/dist/styles.css +5593 -0
  107. package/dist/utils-IKPG3zcC.d.cts +46 -0
  108. package/dist/utils-IKPG3zcC.d.ts +46 -0
  109. package/package.json +91 -0
@@ -0,0 +1,3645 @@
1
+ import { useAnchoredPosition, useDismiss } from './chunk-XZ5ZFLZM.js';
2
+ import { useControllableState } from './chunk-OAQ7MDYX.js';
3
+ import { useId, cx, resolveSlotContent } from './chunk-NJDDNO4C.js';
4
+ import { defineComponent, computed, provide, h, ref, watch, inject, Teleport, getCurrentScope, onScopeDispose } from 'vue';
5
+ import { rgbToHsv, tryParseColor, parseColor, toHex, hsvToRgb, contrastRatio, toOklchString } from '@ansh-ui/core';
6
+
7
+ var FieldKey = /* @__PURE__ */ Symbol("ansh-field");
8
+ function useFieldControl() {
9
+ const field = inject(FieldKey, null);
10
+ return computed(() => {
11
+ if (!field) return {};
12
+ return {
13
+ id: field.id,
14
+ ...field.describedBy ? { "aria-describedby": field.describedBy } : null,
15
+ ...field.invalid ? { "aria-invalid": true } : null,
16
+ ...field.required ? { "aria-required": true } : null,
17
+ ...field.disabled ? { disabled: true } : null
18
+ };
19
+ });
20
+ }
21
+ function useFieldContext() {
22
+ return inject(FieldKey, null);
23
+ }
24
+ var Field = defineComponent({
25
+ name: "AnshField",
26
+ props: {
27
+ label: { type: [String, Object, Function], default: void 0 },
28
+ /** Helper text below the label. Announced as part of the control's description. */
29
+ description: { type: [String, Object, Function], default: void 0 },
30
+ /** Error message. Its presence sets `aria-invalid` on the control. */
31
+ error: { type: [String, Object, Function], default: void 0 },
32
+ /** Marks the field required and appends an asterisk to the label. */
33
+ required: { type: Boolean, default: false },
34
+ disabled: { type: Boolean, default: false },
35
+ /** Label to the side rather than above — denser, for settings screens. */
36
+ orientation: { type: String, default: "vertical" }
37
+ },
38
+ setup(props, { slots, attrs }) {
39
+ const id = useId("field");
40
+ const descriptionId = `${id}-description`;
41
+ const errorId = `${id}-error`;
42
+ const describedBy = computed(
43
+ () => [props.error ? errorId : null, props.description ? descriptionId : null].filter(Boolean).join(" ") || void 0
44
+ );
45
+ provide(FieldKey, {
46
+ id,
47
+ get describedBy() {
48
+ return describedBy.value;
49
+ },
50
+ get invalid() {
51
+ return Boolean(props.error);
52
+ },
53
+ get required() {
54
+ return props.required;
55
+ },
56
+ get disabled() {
57
+ return props.disabled;
58
+ }
59
+ });
60
+ return () => h(
61
+ "div",
62
+ {
63
+ class: cx(
64
+ "ansh-field",
65
+ `ansh-field--${props.orientation}`,
66
+ props.disabled && "ansh-field--disabled",
67
+ attrs.class
68
+ )
69
+ },
70
+ [
71
+ props.label ? h("label", { class: "ansh-field__label", for: id }, [
72
+ resolveSlotContent(props.label),
73
+ props.required ? [
74
+ h("span", { "aria-hidden": "true", class: "ansh-field__required" }, "*"),
75
+ // The asterisk alone is a visual convention with no meaning to a screen
76
+ // reader; `aria-required` on the control carries it, and this covers the rest.
77
+ h("span", { class: "ansh-visually-hidden" }, " (required)")
78
+ ] : null
79
+ ]) : null,
80
+ h("div", { class: "ansh-field__control" }, slots.default?.()),
81
+ props.description ? h("p", { class: "ansh-field__description", id: descriptionId }, [
82
+ resolveSlotContent(props.description)
83
+ ]) : null,
84
+ props.error ? (
85
+ // `role="alert"` announces the message when it appears after a failed submit, which
86
+ // is the moment it matters. `aria-describedby` alone would only be read on re-focus.
87
+ h("p", { class: "ansh-field__error", id: errorId, role: "alert" }, [
88
+ resolveSlotContent(props.error)
89
+ ])
90
+ ) : null
91
+ ]
92
+ );
93
+ }
94
+ });
95
+ var Fieldset = defineComponent({
96
+ name: "AnshFieldset",
97
+ props: {
98
+ legend: { type: [String, Object, Function], required: true },
99
+ description: { type: [String, Object, Function], default: void 0 }
100
+ },
101
+ setup(props, { slots, attrs }) {
102
+ const id = useId("fieldset");
103
+ return () => h(
104
+ "fieldset",
105
+ {
106
+ class: cx("ansh-fieldset", attrs.class),
107
+ ...props.description ? { "aria-describedby": id } : null
108
+ },
109
+ [
110
+ h("legend", { class: "ansh-fieldset__legend" }, [resolveSlotContent(props.legend)]),
111
+ props.description ? h("p", { class: "ansh-fieldset__description", id }, [
112
+ resolveSlotContent(props.description)
113
+ ]) : null,
114
+ slots.default?.()
115
+ ]
116
+ );
117
+ }
118
+ });
119
+ var VisuallyHidden = defineComponent({
120
+ name: "AnshVisuallyHidden",
121
+ setup(_props, { slots, attrs }) {
122
+ return () => h("span", { class: "ansh-visually-hidden", ...attrs }, slots.default?.());
123
+ }
124
+ });
125
+ var Input = defineComponent({
126
+ name: "AnshInput",
127
+ inheritAttrs: false,
128
+ props: {
129
+ modelValue: { type: String, default: void 0 },
130
+ defaultValue: { type: String, default: "" },
131
+ size: { type: String, default: "md" },
132
+ /** Content inside the field, before the text — an icon or a currency symbol. */
133
+ prefix: { type: [String, Object, Function], default: void 0 },
134
+ /** Content inside the field, after the text — a unit, or a button. */
135
+ suffix: { type: [String, Object, Function], default: void 0 },
136
+ /** Show a clear button when non-empty. */
137
+ clearable: { type: Boolean, default: false },
138
+ /** Force the error style outside a `Field`. */
139
+ invalid: { type: Boolean, default: false },
140
+ disabled: { type: Boolean, default: false },
141
+ readonly: { type: Boolean, default: false },
142
+ /** Class for the outer wrapper, when there is a prefix or suffix. */
143
+ wrapperClassName: { type: String, default: void 0 }
144
+ },
145
+ emits: ["update:modelValue", "input", "change", "clear"],
146
+ setup(props, { attrs, emit, expose, slots }) {
147
+ const fieldProps = useFieldControl();
148
+ const inputRef = ref(null);
149
+ const [value, setValue] = useControllableState(
150
+ () => props.modelValue,
151
+ props.defaultValue,
152
+ (next) => emit("update:modelValue", next)
153
+ );
154
+ expose({
155
+ focus: (options) => inputRef.value?.focus(options),
156
+ blur: () => inputRef.value?.blur(),
157
+ get element() {
158
+ return inputRef.value;
159
+ }
160
+ });
161
+ const grouped = computed(() => Boolean(props.prefix || props.suffix || props.clearable));
162
+ const showClear = computed(
163
+ () => props.clearable && value.value.length > 0 && !props.disabled && !props.readonly
164
+ );
165
+ const renderInput = () => {
166
+ const { class: attrClass, ...restAttrs } = attrs;
167
+ return h("input", {
168
+ ref: inputRef,
169
+ class: cx(
170
+ "ansh-input",
171
+ `ansh-input--${props.size}`,
172
+ !grouped.value && attrClass
173
+ ),
174
+ disabled: props.disabled,
175
+ readonly: props.readonly || void 0,
176
+ ...fieldProps.value,
177
+ ...restAttrs,
178
+ value: value.value,
179
+ ...props.invalid ? { "aria-invalid": true } : null,
180
+ onInput: (event) => {
181
+ setValue(event.target.value);
182
+ emit("input", event);
183
+ },
184
+ onChange: (event) => emit("change", event)
185
+ });
186
+ };
187
+ return () => {
188
+ if (!grouped.value) return renderInput();
189
+ const { class: attrClass } = attrs;
190
+ return h(
191
+ "div",
192
+ {
193
+ class: cx(
194
+ "ansh-input-group",
195
+ `ansh-input-group--${props.size}`,
196
+ props.disabled && "ansh-input-group--disabled",
197
+ (props.invalid || fieldProps.value["aria-invalid"]) && "ansh-input-group--invalid",
198
+ props.wrapperClassName,
199
+ attrClass
200
+ ),
201
+ // Clicking anywhere in the group focuses the input, including the padding and the icons.
202
+ // Guarded so a click on a button inside the suffix still reaches that button.
203
+ onMousedown: (event) => {
204
+ if (event.target === event.currentTarget) {
205
+ event.preventDefault();
206
+ inputRef.value?.focus();
207
+ }
208
+ }
209
+ },
210
+ [
211
+ props.prefix ? h("span", { class: "ansh-input-group__affix", "aria-hidden": "true" }, [
212
+ resolveSlotContent(props.prefix ?? slots.prefix?.())
213
+ ]) : null,
214
+ renderInput(),
215
+ showClear.value ? h(
216
+ "button",
217
+ {
218
+ type: "button",
219
+ class: "ansh-input-group__clear ansh-focusable",
220
+ "aria-label": "Clear",
221
+ onClick: () => {
222
+ setValue("");
223
+ inputRef.value?.focus();
224
+ emit("clear");
225
+ }
226
+ },
227
+ [
228
+ h(
229
+ "svg",
230
+ { viewBox: "0 0 16 16", width: "12", height: "12", "aria-hidden": "true" },
231
+ [
232
+ h("path", {
233
+ d: "M4 4l8 8M12 4l-8 8",
234
+ stroke: "currentColor",
235
+ "stroke-width": "1.75",
236
+ "stroke-linecap": "round",
237
+ fill: "none"
238
+ })
239
+ ]
240
+ )
241
+ ]
242
+ ) : null,
243
+ props.suffix ? h("span", { class: "ansh-input-group__affix" }, [resolveSlotContent(props.suffix)]) : null
244
+ ]
245
+ );
246
+ };
247
+ }
248
+ });
249
+ var Textarea = defineComponent({
250
+ name: "AnshTextarea",
251
+ inheritAttrs: false,
252
+ props: {
253
+ modelValue: { type: String, default: void 0 },
254
+ defaultValue: { type: String, default: "" },
255
+ /** Grow with the content instead of scrolling. */
256
+ autoResize: { type: Boolean, default: false },
257
+ /** Upper bound for `autoResize`, in rows. Default `12`. */
258
+ maxRows: { type: Number, default: 12 },
259
+ /** Show a live character count. Needs `maxlength` (passed through as a native attribute). */
260
+ showCount: { type: Boolean, default: false },
261
+ invalid: { type: Boolean, default: false }
262
+ },
263
+ emits: ["update:modelValue", "input", "change"],
264
+ setup(props, { attrs, emit, expose }) {
265
+ const fieldProps = useFieldControl();
266
+ const textareaRef = ref(null);
267
+ const [value, setValue] = useControllableState(
268
+ () => props.modelValue,
269
+ props.defaultValue,
270
+ (next) => emit("update:modelValue", next)
271
+ );
272
+ expose({
273
+ focus: (options) => textareaRef.value?.focus(options),
274
+ blur: () => textareaRef.value?.blur(),
275
+ get element() {
276
+ return textareaRef.value;
277
+ }
278
+ });
279
+ const resize = () => {
280
+ const node = textareaRef.value;
281
+ if (!node || !props.autoResize) return;
282
+ node.style.height = "auto";
283
+ const lineHeight = Number.parseFloat(getComputedStyle(node).lineHeight) || 20;
284
+ const max = lineHeight * props.maxRows;
285
+ node.style.height = `${Math.min(node.scrollHeight, max)}px`;
286
+ node.style.overflowY = node.scrollHeight > max ? "auto" : "hidden";
287
+ };
288
+ return () => {
289
+ const { class: attrClass, maxlength, ...restAttrs } = attrs;
290
+ const max = maxlength !== void 0 ? Number(maxlength) : void 0;
291
+ return h("div", { class: cx("ansh-textarea-wrap", attrClass) }, [
292
+ h("textarea", {
293
+ ref: (node) => {
294
+ textareaRef.value = node ?? null;
295
+ resize();
296
+ },
297
+ class: cx("ansh-input", "ansh-textarea", props.autoResize && "ansh-textarea--auto"),
298
+ maxlength,
299
+ ...fieldProps.value,
300
+ ...restAttrs,
301
+ value: value.value,
302
+ ...props.invalid ? { "aria-invalid": true } : null,
303
+ onInput: (event) => {
304
+ setValue(event.target.value);
305
+ resize();
306
+ emit("input", event);
307
+ },
308
+ onChange: (event) => emit("change", event)
309
+ }),
310
+ props.showCount ? h(
311
+ "span",
312
+ {
313
+ class: cx(
314
+ "ansh-textarea__count",
315
+ max !== void 0 && value.value.length > max * 0.9 && "ansh-textarea__count--near"
316
+ ),
317
+ // Announced politely so a screen-reader user isn't interrupted on every keystroke.
318
+ "aria-live": "polite"
319
+ },
320
+ max !== void 0 ? `${value.value.length} / ${max}` : String(value.value.length)
321
+ ) : null
322
+ ]);
323
+ };
324
+ }
325
+ });
326
+ var NumberInput = defineComponent({
327
+ name: "AnshNumberInput",
328
+ inheritAttrs: false,
329
+ props: {
330
+ modelValue: { type: Number, default: void 0 },
331
+ defaultValue: { type: Number, default: 0 },
332
+ min: { type: Number, default: Number.NEGATIVE_INFINITY },
333
+ max: { type: Number, default: Number.POSITIVE_INFINITY },
334
+ step: { type: Number, default: 1 },
335
+ /** Decimal places to display. Default: inferred from `step`. */
336
+ precision: { type: Number, default: void 0 },
337
+ size: { type: String, default: "md" },
338
+ /** Unit shown after the number — "kg", "%", "days". */
339
+ unit: { type: [String, Object, Function], default: void 0 },
340
+ /** Show the stepper buttons. Default `true`. */
341
+ steppers: { type: Boolean, default: true },
342
+ disabled: { type: Boolean, default: false }
343
+ },
344
+ emits: ["update:modelValue", "change"],
345
+ setup(props, { attrs, emit }) {
346
+ const fieldProps = useFieldControl();
347
+ const inputRef = ref(null);
348
+ const editing = ref(false);
349
+ const [current, setCurrent] = useControllableState(
350
+ () => props.modelValue,
351
+ props.defaultValue,
352
+ (next) => emit("change", next)
353
+ );
354
+ const decimals = computed(
355
+ () => props.precision ?? (String(props.step).includes(".") ? String(props.step).split(".")[1].length : 0)
356
+ );
357
+ const text = ref(current.value.toFixed(decimals.value));
358
+ const syncText = () => {
359
+ if (!editing.value) text.value = current.value.toFixed(decimals.value);
360
+ };
361
+ watch([current, decimals], syncText);
362
+ const clamp = (n) => Math.min(Math.max(n, props.min), props.max);
363
+ const nudge = (direction, multiplier = 1) => {
364
+ const next = clamp(current.value + props.step * direction * multiplier);
365
+ setCurrent(Number(next.toFixed(decimals.value)));
366
+ syncText();
367
+ };
368
+ const commit = (raw) => {
369
+ const parsed = Number.parseFloat(raw);
370
+ const next = Number.isFinite(parsed) ? clamp(parsed) : current.value;
371
+ setCurrent(Number(next.toFixed(decimals.value)));
372
+ text.value = next.toFixed(decimals.value);
373
+ };
374
+ return () => {
375
+ const { class: attrClass, ...restAttrs } = attrs;
376
+ return h(
377
+ "div",
378
+ {
379
+ class: cx(
380
+ "ansh-number",
381
+ `ansh-number--${props.size}`,
382
+ props.disabled && "ansh-number--disabled",
383
+ attrClass
384
+ )
385
+ },
386
+ [
387
+ h("input", {
388
+ ref: inputRef,
389
+ type: "text",
390
+ inputmode: "decimal",
391
+ class: "ansh-input ansh-number__input",
392
+ value: text.value,
393
+ disabled: props.disabled,
394
+ // The ARIA spinbutton contract, so the current, minimum and maximum are announced.
395
+ role: "spinbutton",
396
+ "aria-valuenow": current.value,
397
+ ...Number.isFinite(props.min) ? { "aria-valuemin": props.min } : null,
398
+ ...Number.isFinite(props.max) ? { "aria-valuemax": props.max } : null,
399
+ ...fieldProps.value,
400
+ ...restAttrs,
401
+ onFocus: () => {
402
+ editing.value = true;
403
+ },
404
+ onInput: (event) => {
405
+ text.value = event.target.value;
406
+ },
407
+ onBlur: (event) => {
408
+ editing.value = false;
409
+ commit(event.target.value);
410
+ },
411
+ onKeydown: (event) => {
412
+ if (event.key === "ArrowUp") {
413
+ event.preventDefault();
414
+ nudge(1, event.shiftKey ? 10 : 1);
415
+ } else if (event.key === "ArrowDown") {
416
+ event.preventDefault();
417
+ nudge(-1, event.shiftKey ? 10 : 1);
418
+ } else if (event.key === "PageUp") {
419
+ event.preventDefault();
420
+ nudge(1, 10);
421
+ } else if (event.key === "PageDown") {
422
+ event.preventDefault();
423
+ nudge(-1, 10);
424
+ } else if (event.key === "Home" && Number.isFinite(props.min)) {
425
+ event.preventDefault();
426
+ setCurrent(props.min);
427
+ syncText();
428
+ } else if (event.key === "End" && Number.isFinite(props.max)) {
429
+ event.preventDefault();
430
+ setCurrent(props.max);
431
+ syncText();
432
+ } else if (event.key === "Enter") {
433
+ commit(event.target.value);
434
+ }
435
+ }
436
+ }),
437
+ props.unit ? h("span", { class: "ansh-number__unit" }, [resolveSlotContent(props.unit)]) : null,
438
+ props.steppers ? (
439
+ // `tabindex="-1"` and `aria-hidden`: the input is already a spinbutton with
440
+ // arrow-key support, so these buttons would be redundant tab stops announcing
441
+ // nothing new.
442
+ h("span", { class: "ansh-number__steppers", "aria-hidden": "true" }, [
443
+ h(
444
+ "button",
445
+ {
446
+ type: "button",
447
+ tabindex: "-1",
448
+ class: "ansh-number__step",
449
+ disabled: props.disabled || current.value >= props.max,
450
+ onClick: () => nudge(1)
451
+ },
452
+ [
453
+ h("svg", { viewBox: "0 0 10 6", width: "10", height: "6" }, [
454
+ h("path", {
455
+ d: "M1 5l4-4 4 4",
456
+ stroke: "currentColor",
457
+ "stroke-width": "1.5",
458
+ fill: "none",
459
+ "stroke-linecap": "round"
460
+ })
461
+ ])
462
+ ]
463
+ ),
464
+ h(
465
+ "button",
466
+ {
467
+ type: "button",
468
+ tabindex: "-1",
469
+ class: "ansh-number__step",
470
+ disabled: props.disabled || current.value <= props.min,
471
+ onClick: () => nudge(-1)
472
+ },
473
+ [
474
+ h("svg", { viewBox: "0 0 10 6", width: "10", height: "6" }, [
475
+ h("path", {
476
+ d: "M1 1l4 4 4-4",
477
+ stroke: "currentColor",
478
+ "stroke-width": "1.5",
479
+ fill: "none",
480
+ "stroke-linecap": "round"
481
+ })
482
+ ])
483
+ ]
484
+ )
485
+ ])
486
+ ) : null
487
+ ]
488
+ );
489
+ };
490
+ }
491
+ });
492
+ var Checkbox = defineComponent({
493
+ name: "AnshCheckbox",
494
+ inheritAttrs: false,
495
+ props: {
496
+ modelValue: { type: Boolean, default: void 0 },
497
+ defaultValue: { type: Boolean, default: false },
498
+ /** Neither checked nor unchecked — a parent whose children are partly selected. */
499
+ indeterminate: { type: Boolean, default: false },
500
+ label: { type: [String, Object, Function], default: void 0 },
501
+ description: { type: [String, Object, Function], default: void 0 },
502
+ size: { type: String, default: "md" },
503
+ invalid: { type: Boolean, default: false },
504
+ disabled: { type: Boolean, default: false }
505
+ },
506
+ emits: ["update:modelValue"],
507
+ setup(props, { attrs, emit, expose }) {
508
+ const fieldProps = useFieldControl();
509
+ const inputRef = ref(null);
510
+ const generatedId = useId();
511
+ const [checked, setChecked] = useControllableState(
512
+ () => props.modelValue,
513
+ props.defaultValue,
514
+ (next) => emit("update:modelValue", next)
515
+ );
516
+ expose({
517
+ focus: (options) => inputRef.value?.focus(options),
518
+ blur: () => inputRef.value?.blur(),
519
+ get element() {
520
+ return inputRef.value;
521
+ }
522
+ });
523
+ watch(
524
+ () => props.indeterminate,
525
+ (value) => {
526
+ if (inputRef.value) inputRef.value.indeterminate = value;
527
+ }
528
+ );
529
+ return () => {
530
+ const { id: attrId, class: attrClass, ...restAttrs } = attrs;
531
+ const id = attrId ?? fieldProps.value.id ?? generatedId;
532
+ const descriptionId = `${id}-hint`;
533
+ const isDisabled = props.disabled || Boolean(fieldProps.value.disabled);
534
+ return h(
535
+ "div",
536
+ {
537
+ class: cx(
538
+ "ansh-choice",
539
+ `ansh-choice--${props.size}`,
540
+ isDisabled && "ansh-choice--disabled",
541
+ attrClass
542
+ )
543
+ },
544
+ [
545
+ h("span", { class: "ansh-choice__box" }, [
546
+ h("input", {
547
+ ref: (node) => {
548
+ const element = node ?? null;
549
+ inputRef.value = element;
550
+ if (element) element.indeterminate = props.indeterminate;
551
+ },
552
+ type: "checkbox",
553
+ class: "ansh-choice__input",
554
+ checked: checked.value,
555
+ disabled: isDisabled,
556
+ // Announces "mixed" rather than "checked"/"unchecked" — the whole point of the state.
557
+ ...props.indeterminate ? { "aria-checked": "mixed" } : null,
558
+ ...props.invalid ? { "aria-invalid": true } : null,
559
+ ...props.description ? { "aria-describedby": descriptionId } : null,
560
+ ...restAttrs,
561
+ id,
562
+ onChange: (event) => setChecked(event.target.checked)
563
+ }),
564
+ h(
565
+ "span",
566
+ {
567
+ class: "ansh-choice__indicator ansh-choice__indicator--check",
568
+ "aria-hidden": "true"
569
+ },
570
+ [
571
+ props.indeterminate ? h("svg", { viewBox: "0 0 14 14", width: "10", height: "10" }, [
572
+ h("path", {
573
+ d: "M3 7h8",
574
+ stroke: "currentColor",
575
+ "stroke-width": "2",
576
+ "stroke-linecap": "round"
577
+ })
578
+ ]) : h("svg", { viewBox: "0 0 14 14", width: "10", height: "10" }, [
579
+ h("path", {
580
+ d: "M2.5 7.5l3 3 6-6.5",
581
+ stroke: "currentColor",
582
+ "stroke-width": "2",
583
+ "stroke-linecap": "round",
584
+ "stroke-linejoin": "round",
585
+ fill: "none"
586
+ })
587
+ ])
588
+ ]
589
+ )
590
+ ]),
591
+ props.label || props.description ? h("span", { class: "ansh-choice__text" }, [
592
+ props.label ? h("label", { class: "ansh-choice__label", for: id }, [
593
+ resolveSlotContent(props.label)
594
+ ]) : null,
595
+ props.description ? h("span", { class: "ansh-choice__description", id: descriptionId }, [
596
+ resolveSlotContent(props.description)
597
+ ]) : null
598
+ ]) : null
599
+ ]
600
+ );
601
+ };
602
+ }
603
+ });
604
+ var Switch = defineComponent({
605
+ name: "AnshSwitch",
606
+ inheritAttrs: false,
607
+ props: {
608
+ modelValue: { type: Boolean, default: void 0 },
609
+ defaultValue: { type: Boolean, default: false },
610
+ label: { type: [String, Object, Function], default: void 0 },
611
+ description: { type: [String, Object, Function], default: void 0 },
612
+ /** Where the label sits. Default `'end'`; `'start'` suits settings rows. */
613
+ labelPosition: { type: String, default: "end" },
614
+ size: { type: String, default: "md" },
615
+ disabled: { type: Boolean, default: false }
616
+ },
617
+ emits: ["update:modelValue"],
618
+ setup(props, { attrs, emit, expose }) {
619
+ const fieldProps = useFieldControl();
620
+ const inputRef = ref(null);
621
+ const generatedId = useId();
622
+ const [checked, setChecked] = useControllableState(
623
+ () => props.modelValue,
624
+ props.defaultValue,
625
+ (next) => emit("update:modelValue", next)
626
+ );
627
+ expose({
628
+ focus: (options) => inputRef.value?.focus(options),
629
+ blur: () => inputRef.value?.blur(),
630
+ get element() {
631
+ return inputRef.value;
632
+ }
633
+ });
634
+ return () => {
635
+ const { id: attrId, class: attrClass, ...restAttrs } = attrs;
636
+ const id = attrId ?? fieldProps.value.id ?? generatedId;
637
+ const descriptionId = `${id}-hint`;
638
+ const isDisabled = props.disabled || Boolean(fieldProps.value.disabled);
639
+ return h(
640
+ "div",
641
+ {
642
+ class: cx(
643
+ "ansh-switch-row",
644
+ `ansh-switch-row--${props.labelPosition}`,
645
+ isDisabled && "ansh-choice--disabled",
646
+ attrClass
647
+ )
648
+ },
649
+ [
650
+ h("span", { class: "ansh-switch", "data-size": props.size }, [
651
+ h("input", {
652
+ ref: inputRef,
653
+ type: "checkbox",
654
+ role: "switch",
655
+ class: "ansh-choice__input",
656
+ checked: checked.value,
657
+ disabled: isDisabled,
658
+ ...props.description ? { "aria-describedby": descriptionId } : null,
659
+ ...restAttrs,
660
+ id,
661
+ onChange: (event) => setChecked(event.target.checked)
662
+ }),
663
+ h("span", { class: "ansh-switch__track", "aria-hidden": "true" }, [
664
+ h("span", { class: "ansh-switch__thumb" })
665
+ ])
666
+ ]),
667
+ props.label || props.description ? h("span", { class: "ansh-choice__text" }, [
668
+ props.label ? h("label", { class: "ansh-choice__label", for: id }, [
669
+ resolveSlotContent(props.label)
670
+ ]) : null,
671
+ props.description ? h("span", { class: "ansh-choice__description", id: descriptionId }, [
672
+ resolveSlotContent(props.description)
673
+ ]) : null
674
+ ]) : null
675
+ ]
676
+ );
677
+ };
678
+ }
679
+ });
680
+ var RadioGroupKey = /* @__PURE__ */ Symbol("ansh-radio-group");
681
+ var RadioGroup = defineComponent({
682
+ name: "AnshRadioGroup",
683
+ props: {
684
+ modelValue: { type: String, default: void 0 },
685
+ defaultValue: { type: String, default: void 0 },
686
+ name: { type: String, default: void 0 },
687
+ label: { type: String, default: void 0 },
688
+ orientation: { type: String, default: "vertical" },
689
+ disabled: { type: Boolean, default: false }
690
+ },
691
+ emits: ["update:modelValue"],
692
+ setup(props, { slots, emit, attrs }) {
693
+ const generatedName = useId("radiogroup");
694
+ const [current, setCurrent] = useControllableState(
695
+ () => props.modelValue,
696
+ props.defaultValue,
697
+ (next) => emit("update:modelValue", next)
698
+ );
699
+ provide(RadioGroupKey, {
700
+ get name() {
701
+ return props.name ?? generatedName;
702
+ },
703
+ value: current,
704
+ setValue: setCurrent,
705
+ get disabled() {
706
+ return props.disabled;
707
+ }
708
+ });
709
+ return () => h(
710
+ "div",
711
+ {
712
+ role: "radiogroup",
713
+ ...props.label ? { "aria-label": props.label } : null,
714
+ class: cx(
715
+ "ansh-radio-group",
716
+ `ansh-radio-group--${props.orientation}`,
717
+ attrs.class
718
+ )
719
+ },
720
+ slots.default?.()
721
+ );
722
+ }
723
+ });
724
+ var Radio = defineComponent({
725
+ name: "AnshRadio",
726
+ inheritAttrs: false,
727
+ props: {
728
+ value: { type: String, required: true },
729
+ label: { type: [String, Object, Function], default: void 0 },
730
+ description: { type: [String, Object, Function], default: void 0 },
731
+ size: { type: String, default: "md" },
732
+ disabled: { type: Boolean, default: false }
733
+ },
734
+ setup(props, { attrs, expose }) {
735
+ const group = inject(RadioGroupKey, null);
736
+ const inputRef = ref(null);
737
+ const generatedId = useId();
738
+ expose({
739
+ focus: (options) => inputRef.value?.focus(options),
740
+ blur: () => inputRef.value?.blur(),
741
+ get element() {
742
+ return inputRef.value;
743
+ }
744
+ });
745
+ return () => {
746
+ const { id: attrId, class: attrClass, ...restAttrs } = attrs;
747
+ const id = attrId ?? generatedId;
748
+ const descriptionId = `${id}-hint`;
749
+ const isDisabled = props.disabled || Boolean(group?.disabled);
750
+ return h(
751
+ "div",
752
+ {
753
+ class: cx(
754
+ "ansh-choice",
755
+ `ansh-choice--${props.size}`,
756
+ isDisabled && "ansh-choice--disabled",
757
+ attrClass
758
+ )
759
+ },
760
+ [
761
+ h("span", { class: "ansh-choice__box" }, [
762
+ h("input", {
763
+ ref: inputRef,
764
+ type: "radio",
765
+ class: "ansh-choice__input",
766
+ // Omitted rather than set to `undefined` when there is no group: `checked` is a DOM
767
+ // property, and explicitly assigning `undefined` to it coerces to `false`, forcing an
768
+ // uncontrolled radio back to unchecked on every re-render.
769
+ ...group ? { name: group.name, checked: group.value.value === props.value } : null,
770
+ value: props.value,
771
+ disabled: isDisabled,
772
+ ...props.description ? { "aria-describedby": descriptionId } : null,
773
+ ...restAttrs,
774
+ id,
775
+ onChange: () => group?.setValue(props.value)
776
+ }),
777
+ h("span", {
778
+ class: "ansh-choice__indicator ansh-choice__indicator--dot",
779
+ "aria-hidden": "true"
780
+ })
781
+ ]),
782
+ props.label || props.description ? h("span", { class: "ansh-choice__text" }, [
783
+ props.label ? h("label", { class: "ansh-choice__label", for: id }, [
784
+ resolveSlotContent(props.label)
785
+ ]) : null,
786
+ props.description ? h("span", { class: "ansh-choice__description", id: descriptionId }, [
787
+ resolveSlotContent(props.description)
788
+ ]) : null
789
+ ]) : null
790
+ ]
791
+ );
792
+ };
793
+ }
794
+ });
795
+ var ChoiceCard = defineComponent({
796
+ name: "AnshChoiceCard",
797
+ inheritAttrs: false,
798
+ props: {
799
+ value: { type: String, required: true },
800
+ label: { type: [String, Object, Function], default: void 0 },
801
+ description: { type: [String, Object, Function], default: void 0 },
802
+ icon: { type: [String, Object, Function], default: void 0 },
803
+ size: { type: String, default: "md" },
804
+ disabled: { type: Boolean, default: false }
805
+ },
806
+ setup(props, { attrs, expose }) {
807
+ const group = inject(RadioGroupKey, null);
808
+ const radioRef = ref(null);
809
+ expose({
810
+ focus: (options) => radioRef.value?.focus(options),
811
+ blur: () => radioRef.value?.blur(),
812
+ get element() {
813
+ return radioRef.value?.element ?? null;
814
+ }
815
+ });
816
+ return () => {
817
+ const { class: attrClass, ...restAttrs } = attrs;
818
+ const selected = group?.value.value === props.value;
819
+ return h(
820
+ "label",
821
+ {
822
+ class: cx(
823
+ "ansh-choice-card",
824
+ selected && "ansh-choice-card--selected",
825
+ attrClass
826
+ )
827
+ },
828
+ [
829
+ h(Radio, {
830
+ ref: radioRef,
831
+ value: props.value,
832
+ size: props.size,
833
+ disabled: props.disabled,
834
+ class: "ansh-choice-card__radio",
835
+ ...restAttrs
836
+ }),
837
+ props.icon ? h("span", { class: "ansh-choice-card__icon" }, [resolveSlotContent(props.icon)]) : null,
838
+ h("span", { class: "ansh-choice-card__body" }, [
839
+ h("span", { class: "ansh-choice-card__label" }, [resolveSlotContent(props.label)]),
840
+ props.description ? h("span", { class: "ansh-choice-card__description" }, [
841
+ resolveSlotContent(props.description)
842
+ ]) : null
843
+ ])
844
+ ]
845
+ );
846
+ };
847
+ }
848
+ });
849
+ function stepActive(options, active, delta, loop = true) {
850
+ if (options.length === 0) return -1;
851
+ let index = active;
852
+ for (let attempts = 0; attempts < options.length; attempts++) {
853
+ index += delta;
854
+ if (index < 0) {
855
+ if (!loop) return active;
856
+ index = options.length - 1;
857
+ }
858
+ if (index >= options.length) {
859
+ if (!loop) return active;
860
+ index = 0;
861
+ }
862
+ if (!options[index]?.disabled) return index;
863
+ }
864
+ return active;
865
+ }
866
+ function firstEnabled(options, from = 0) {
867
+ for (let i = from; i < options.length; i++) {
868
+ if (!options[i]?.disabled) return i;
869
+ }
870
+ return -1;
871
+ }
872
+ function useTypeahead(getOptions, onMatch, timeout = 600) {
873
+ let buffer = "";
874
+ let timer;
875
+ if (getCurrentScope()) onScopeDispose(() => clearTimeout(timer));
876
+ return (key) => {
877
+ if (key.length !== 1 || key === " ") return false;
878
+ clearTimeout(timer);
879
+ buffer += key.toLowerCase();
880
+ timer = setTimeout(() => {
881
+ buffer = "";
882
+ }, timeout);
883
+ const options = getOptions();
884
+ const index = options.findIndex(
885
+ (option) => !option.disabled && option.label.toLowerCase().startsWith(buffer)
886
+ );
887
+ if (index >= 0) {
888
+ onMatch(index);
889
+ return true;
890
+ }
891
+ return false;
892
+ };
893
+ }
894
+ function scoreOption(query, option) {
895
+ if (query.trim() === "") return 0;
896
+ const q = query.toLowerCase();
897
+ const haystacks = [option.label, option.keywords ?? "", option.description ?? ""];
898
+ let best = -1;
899
+ for (let h15 = 0; h15 < haystacks.length; h15++) {
900
+ const text = haystacks[h15].toLowerCase();
901
+ let queryIndex = 0;
902
+ let total = 0;
903
+ let run = 0;
904
+ for (let i = 0; i < text.length && queryIndex < q.length; i++) {
905
+ if (text[i] === q[queryIndex]) {
906
+ run++;
907
+ total += run * 2 + (i === 0 ? 8 : 0);
908
+ queryIndex++;
909
+ } else {
910
+ run = 0;
911
+ }
912
+ }
913
+ if (queryIndex === q.length) best = Math.max(best, total - h15 * 4);
914
+ }
915
+ return best;
916
+ }
917
+ function filterOptions(options, query) {
918
+ if (query.trim() === "") return options;
919
+ return options.map((option) => ({ option, score: scoreOption(query, option) })).filter((entry) => entry.score >= 0).sort((a, b) => b.score - a.score).map((entry) => entry.option);
920
+ }
921
+ function highlightMatches(text, query) {
922
+ const q = query.toLowerCase();
923
+ const lower = text.toLowerCase();
924
+ const parts = [];
925
+ let queryIndex = 0;
926
+ let plain = "";
927
+ for (let i = 0; i < text.length; i++) {
928
+ if (queryIndex < q.length && lower[i] === q[queryIndex]) {
929
+ if (plain) {
930
+ parts.push(plain);
931
+ plain = "";
932
+ }
933
+ parts.push(h("mark", { key: i, class: "ansh-listbox__mark" }, text[i]));
934
+ queryIndex++;
935
+ } else {
936
+ plain += text[i];
937
+ }
938
+ }
939
+ if (plain) parts.push(plain);
940
+ return parts;
941
+ }
942
+ var OptionList = defineComponent({
943
+ name: "AnshOptionList",
944
+ props: {
945
+ options: { type: Array, required: true },
946
+ active: { type: Number, required: true },
947
+ selected: { type: Array, required: true },
948
+ listId: { type: String, required: true },
949
+ multiple: { type: Boolean, default: false },
950
+ emptyMessage: {
951
+ type: [String, Object, Function],
952
+ default: "No results"
953
+ },
954
+ onSelect: { type: Function, required: true },
955
+ onActiveChange: { type: Function, required: true },
956
+ query: { type: String, default: void 0 }
957
+ },
958
+ setup(props) {
959
+ return () => {
960
+ if (props.options.length === 0) {
961
+ return h("div", { class: "ansh-listbox__empty" }, [resolveSlotContent(props.emptyMessage)]);
962
+ }
963
+ const selectedSet = new Set(props.selected);
964
+ let lastGroup;
965
+ return props.options.map((option, index) => {
966
+ const heading = option.group && option.group !== lastGroup ? option.group : null;
967
+ lastGroup = option.group;
968
+ const isSelected = selectedSet.has(option.value);
969
+ return h("div", { key: option.value, role: "presentation" }, [
970
+ heading ? h("div", { class: "ansh-listbox__group", role: "presentation" }, heading) : null,
971
+ h(
972
+ "div",
973
+ {
974
+ id: `${props.listId}-option-${index}`,
975
+ role: "option",
976
+ "aria-selected": isSelected,
977
+ "aria-disabled": option.disabled || void 0,
978
+ class: cx(
979
+ "ansh-listbox__option",
980
+ index === props.active && "ansh-listbox__option--active",
981
+ isSelected && "ansh-listbox__option--selected",
982
+ option.disabled && "ansh-listbox__option--disabled"
983
+ ),
984
+ // A mousedown on the list would blur the input/trigger and close the popup before the
985
+ // click ever lands, so it is prevented here.
986
+ onMousedown: (event) => event.preventDefault(),
987
+ onMouseenter: () => !option.disabled && props.onActiveChange(index),
988
+ onClick: () => !option.disabled && props.onSelect(option)
989
+ },
990
+ [
991
+ props.multiple ? h(
992
+ "span",
993
+ {
994
+ class: cx("ansh-listbox__check", isSelected && "ansh-listbox__check--on"),
995
+ "aria-hidden": "true"
996
+ },
997
+ [
998
+ h("svg", { viewBox: "0 0 14 14", width: "10", height: "10" }, [
999
+ h("path", {
1000
+ d: "M2.5 7.5l3 3 6-6.5",
1001
+ stroke: "currentColor",
1002
+ "stroke-width": "2",
1003
+ "stroke-linecap": "round",
1004
+ "stroke-linejoin": "round",
1005
+ fill: "none"
1006
+ })
1007
+ ])
1008
+ ]
1009
+ ) : null,
1010
+ option.icon ? h("span", { class: "ansh-listbox__icon" }, [resolveSlotContent(option.icon)]) : null,
1011
+ h("span", { class: "ansh-listbox__text" }, [
1012
+ h("span", { class: "ansh-listbox__label" }, [
1013
+ props.query ? highlightMatches(option.label, props.query) : option.label
1014
+ ]),
1015
+ option.description ? h("span", { class: "ansh-listbox__description" }, option.description) : null
1016
+ ]),
1017
+ !props.multiple && isSelected ? h("span", { class: "ansh-listbox__tick", "aria-hidden": "true" }, [
1018
+ h("svg", { viewBox: "0 0 14 14", width: "12", height: "12" }, [
1019
+ h("path", {
1020
+ d: "M2.5 7.5l3 3 6-6.5",
1021
+ stroke: "currentColor",
1022
+ "stroke-width": "2",
1023
+ "stroke-linecap": "round",
1024
+ "stroke-linejoin": "round",
1025
+ fill: "none"
1026
+ })
1027
+ ])
1028
+ ]) : null
1029
+ ]
1030
+ )
1031
+ ]);
1032
+ });
1033
+ };
1034
+ }
1035
+ });
1036
+ var Select = defineComponent({
1037
+ name: "AnshSelect",
1038
+ props: {
1039
+ options: { type: Array, required: true },
1040
+ // `null` is a legitimate "cleared" value alongside `string`; Vue's optional-prop check skips
1041
+ // type validation whenever the resolved value is `null` or `undefined`, so declaring the runtime
1042
+ // type as `String` here does not produce a dev-mode warning when `null` is passed.
1043
+ modelValue: { type: String, default: void 0 },
1044
+ defaultValue: { type: String, default: null },
1045
+ placeholder: { type: String, default: "Select\u2026" },
1046
+ disabled: { type: Boolean, default: false },
1047
+ invalid: { type: Boolean, default: false },
1048
+ size: { type: String, default: "md" },
1049
+ /** Show a clear button once something is selected. */
1050
+ clearable: { type: Boolean, default: false },
1051
+ /** Accessible name when there is no wrapping `Field`. */
1052
+ label: { type: String, default: void 0 },
1053
+ /** Emit a hidden input so the value posts in a native form submit. */
1054
+ name: { type: String, default: void 0 },
1055
+ /** Max height of the popup before it scrolls. Default `'18rem'`. */
1056
+ maxHeight: { type: String, default: "18rem" },
1057
+ emptyMessage: { type: [String, Object, Function], default: void 0 }
1058
+ },
1059
+ emits: ["update:modelValue"],
1060
+ setup(props, { attrs, emit }) {
1061
+ const fieldProps = useFieldControl();
1062
+ const open = ref(false);
1063
+ const [current, setCurrent] = useControllableState(
1064
+ () => props.modelValue,
1065
+ props.defaultValue,
1066
+ (next) => emit("update:modelValue", next)
1067
+ );
1068
+ const triggerRef = ref(null);
1069
+ const listRef = ref(null);
1070
+ const listId = useId("select");
1071
+ const selectedIndex = () => props.options.findIndex((option) => option.value === current.value);
1072
+ const active = ref(0);
1073
+ const { position } = useAnchoredPosition(triggerRef, listRef, open, {
1074
+ placement: "bottom-start",
1075
+ offset: 6,
1076
+ matchWidth: true
1077
+ });
1078
+ useDismiss(
1079
+ listRef,
1080
+ open,
1081
+ () => {
1082
+ open.value = false;
1083
+ },
1084
+ { ignore: [triggerRef] }
1085
+ );
1086
+ const openList = () => {
1087
+ if (props.disabled) return;
1088
+ const index = selectedIndex();
1089
+ active.value = index >= 0 ? index : firstEnabled(props.options);
1090
+ open.value = true;
1091
+ };
1092
+ const commit = (option) => {
1093
+ setCurrent(option.value);
1094
+ open.value = false;
1095
+ triggerRef.value?.focus();
1096
+ };
1097
+ const typeahead = useTypeahead(
1098
+ () => props.options,
1099
+ (index) => {
1100
+ active.value = index;
1101
+ if (!open.value) setCurrent(props.options[index].value);
1102
+ }
1103
+ );
1104
+ const onTriggerKeydown = (event) => {
1105
+ if (["ArrowDown", "ArrowUp", "Enter", " "].includes(event.key)) {
1106
+ event.preventDefault();
1107
+ openList();
1108
+ return;
1109
+ }
1110
+ if (typeahead(event.key)) event.preventDefault();
1111
+ };
1112
+ const onListKeydown = (event) => {
1113
+ switch (event.key) {
1114
+ case "ArrowDown":
1115
+ event.preventDefault();
1116
+ active.value = stepActive(props.options, active.value, 1);
1117
+ break;
1118
+ case "ArrowUp":
1119
+ event.preventDefault();
1120
+ active.value = stepActive(props.options, active.value, -1);
1121
+ break;
1122
+ case "Home":
1123
+ event.preventDefault();
1124
+ active.value = firstEnabled(props.options);
1125
+ break;
1126
+ case "End":
1127
+ event.preventDefault();
1128
+ active.value = stepActive(props.options, props.options.length, -1, false);
1129
+ break;
1130
+ case "Enter":
1131
+ case " ": {
1132
+ event.preventDefault();
1133
+ const option = props.options[active.value];
1134
+ if (option && !option.disabled) commit(option);
1135
+ break;
1136
+ }
1137
+ case "Escape":
1138
+ event.preventDefault();
1139
+ open.value = false;
1140
+ triggerRef.value?.focus();
1141
+ break;
1142
+ case "Tab":
1143
+ event.preventDefault();
1144
+ open.value = false;
1145
+ triggerRef.value?.focus();
1146
+ break;
1147
+ default:
1148
+ typeahead(event.key);
1149
+ }
1150
+ };
1151
+ watch(
1152
+ [active, open],
1153
+ () => {
1154
+ if (!open.value) return;
1155
+ document.getElementById(`${listId}-option-${active.value}`)?.scrollIntoView?.({ block: "nearest" });
1156
+ },
1157
+ { flush: "post" }
1158
+ );
1159
+ return () => {
1160
+ const selected = (() => {
1161
+ const index = selectedIndex();
1162
+ return index >= 0 ? props.options[index] : void 0;
1163
+ })();
1164
+ return h(
1165
+ "div",
1166
+ { class: cx("ansh-select", `ansh-select--${props.size}`, attrs.class) },
1167
+ [
1168
+ h(
1169
+ "button",
1170
+ {
1171
+ ref: triggerRef,
1172
+ type: "button",
1173
+ class: cx(
1174
+ "ansh-select__trigger",
1175
+ "ansh-focusable",
1176
+ open.value && "ansh-select__trigger--open",
1177
+ (props.invalid || fieldProps.value["aria-invalid"]) && "ansh-select__trigger--invalid",
1178
+ !selected && "ansh-select__trigger--placeholder"
1179
+ ),
1180
+ disabled: props.disabled || fieldProps.value.disabled,
1181
+ "aria-haspopup": "listbox",
1182
+ "aria-expanded": open.value,
1183
+ "aria-controls": open.value ? listId : void 0,
1184
+ ...props.label ? { "aria-label": props.label } : null,
1185
+ ...fieldProps.value,
1186
+ onClick: () => open.value ? open.value = false : openList(),
1187
+ onKeydown: onTriggerKeydown
1188
+ },
1189
+ [
1190
+ selected?.icon ? h("span", { class: "ansh-select__icon" }, [resolveSlotContent(selected.icon)]) : null,
1191
+ h("span", { class: "ansh-select__value" }, selected?.label ?? props.placeholder),
1192
+ props.clearable && selected && !props.disabled ? (
1193
+ // A `<span role="button">` rather than a nested `<button>`: nesting interactive
1194
+ // elements is invalid HTML and browsers resolve it by dropping the inner one.
1195
+ h(
1196
+ "span",
1197
+ {
1198
+ role: "button",
1199
+ tabindex: "-1",
1200
+ "aria-label": "Clear selection",
1201
+ class: "ansh-select__clear",
1202
+ onClick: (event) => {
1203
+ event.stopPropagation();
1204
+ setCurrent(null);
1205
+ }
1206
+ },
1207
+ [
1208
+ h(
1209
+ "svg",
1210
+ { viewBox: "0 0 16 16", width: "12", height: "12", "aria-hidden": "true" },
1211
+ [
1212
+ h("path", {
1213
+ d: "M4 4l8 8M12 4l-8 8",
1214
+ stroke: "currentColor",
1215
+ "stroke-width": "1.75",
1216
+ "stroke-linecap": "round",
1217
+ fill: "none"
1218
+ })
1219
+ ]
1220
+ )
1221
+ ]
1222
+ )
1223
+ ) : null,
1224
+ h("span", { class: "ansh-select__arrow", "aria-hidden": "true" }, [
1225
+ h("svg", { viewBox: "0 0 12 12", width: "12", height: "12" }, [
1226
+ h("path", {
1227
+ d: "M3 4.5L6 7.5l3-3",
1228
+ stroke: "currentColor",
1229
+ "stroke-width": "1.5",
1230
+ fill: "none",
1231
+ "stroke-linecap": "round",
1232
+ "stroke-linejoin": "round"
1233
+ })
1234
+ ])
1235
+ ])
1236
+ ]
1237
+ ),
1238
+ // Posts the value in a plain HTML form. Without this the select is invisible to a
1239
+ // non-JavaScript submit, which still matters for progressively enhanced forms.
1240
+ props.name ? h("input", { type: "hidden", name: props.name, value: current.value ?? "" }) : null,
1241
+ open.value ? h(Teleport, { to: "body" }, [
1242
+ h(
1243
+ "div",
1244
+ {
1245
+ // Move focus into the listbox the moment it attaches. Focus has to be *on* the
1246
+ // element carrying `aria-activedescendant` — that is what the attribute means.
1247
+ // Leaving focus on the trigger and pointing at an option from the list is a
1248
+ // common shortcut that screen readers ignore, producing a select that navigates
1249
+ // in silence.
1250
+ //
1251
+ // Done with a function ref rather than a watcher: the listbox is teleported, and
1252
+ // its content mounts on its own schedule, so a watcher here can run while the
1253
+ // teleported node does not yet exist, leaving focus on `<body>`. A function ref
1254
+ // fires exactly when Vue attaches the node, which is the only moment that is
1255
+ // guaranteed to be correct.
1256
+ ref: (node) => {
1257
+ const element = node ?? null;
1258
+ listRef.value = element;
1259
+ element?.focus({ preventScroll: true });
1260
+ },
1261
+ id: listId,
1262
+ role: "listbox",
1263
+ tabindex: "-1",
1264
+ "aria-activedescendant": props.options[active.value] ? `${listId}-option-${active.value}` : void 0,
1265
+ ...props.label ? { "aria-label": props.label } : null,
1266
+ class: "ansh-listbox",
1267
+ style: { ...position.value.style, maxHeight: props.maxHeight },
1268
+ "data-side": position.value.side,
1269
+ onKeydown: onListKeydown
1270
+ },
1271
+ [
1272
+ h(OptionList, {
1273
+ options: props.options,
1274
+ active: active.value,
1275
+ selected: current.value ? [current.value] : [],
1276
+ listId,
1277
+ emptyMessage: props.emptyMessage,
1278
+ onSelect: commit,
1279
+ onActiveChange: (index) => {
1280
+ active.value = index;
1281
+ }
1282
+ })
1283
+ ]
1284
+ )
1285
+ ]) : null
1286
+ ]
1287
+ );
1288
+ };
1289
+ }
1290
+ });
1291
+ var CREATE_VALUE = "__au_create__";
1292
+ var Combobox = defineComponent({
1293
+ name: "AnshCombobox",
1294
+ props: {
1295
+ options: { type: Array, required: true },
1296
+ /** Selected values. Single-select uses a one-element array. */
1297
+ modelValue: { type: Array, default: void 0 },
1298
+ defaultValue: { type: Array, default: () => [] },
1299
+ multiple: { type: Boolean, default: false },
1300
+ placeholder: { type: String, default: "Search\u2026" },
1301
+ disabled: { type: Boolean, default: false },
1302
+ invalid: { type: Boolean, default: false },
1303
+ size: { type: String, default: "md" },
1304
+ label: { type: String, default: void 0 },
1305
+ maxHeight: { type: String, default: "18rem" },
1306
+ emptyMessage: { type: [String, Object, Function], default: void 0 },
1307
+ /**
1308
+ * Offer the typed text as a new option. Returns the created option's value.
1309
+ *
1310
+ * Kept as a plain callback prop rather than a Vue `emit` (unlike `search`, below): the returned
1311
+ * value feeds straight back into what gets selected, and `emit` has no return channel for a
1312
+ * parent to hand a value back synchronously.
1313
+ */
1314
+ onCreate: { type: Function, default: void 0 },
1315
+ /** Text for the create row. Receives the current query. */
1316
+ createLabel: {
1317
+ type: Function,
1318
+ default: (query) => `Create \u201C${query}\u201D`
1319
+ },
1320
+ /** Skip local filtering, e.g. when a `search` listener already returns filtered options. */
1321
+ filter: { type: Boolean, default: true },
1322
+ /** Cap on how many chips render before collapsing to "+N". Default `Infinity`. */
1323
+ maxVisibleChips: { type: Number, default: Number.POSITIVE_INFINITY }
1324
+ },
1325
+ emits: ["update:modelValue", "search"],
1326
+ setup(props, { emit, attrs }) {
1327
+ const fieldProps = useFieldControl();
1328
+ const open = ref(false);
1329
+ const query = ref("");
1330
+ const active = ref(0);
1331
+ const [selected, setSelected] = useControllableState(
1332
+ () => props.modelValue,
1333
+ props.defaultValue,
1334
+ (next) => emit("update:modelValue", next)
1335
+ );
1336
+ const inputRef = ref(null);
1337
+ const wrapRef = ref(null);
1338
+ const listRef = ref(null);
1339
+ const listId = useId("combobox");
1340
+ const visible = computed(
1341
+ () => props.filter ? filterOptions(props.options, query.value) : props.options
1342
+ );
1343
+ const byValue = computed(() => new Map(props.options.map((o) => [o.value, o])));
1344
+ const canCreate = computed(
1345
+ () => Boolean(props.onCreate) && query.value.trim().length > 0 && !props.options.some(
1346
+ (option) => option.label.toLowerCase() === query.value.trim().toLowerCase()
1347
+ )
1348
+ );
1349
+ const rows = computed(
1350
+ () => canCreate.value ? [...visible.value, { value: CREATE_VALUE, label: props.createLabel(query.value.trim()) }] : visible.value
1351
+ );
1352
+ const { position } = useAnchoredPosition(wrapRef, listRef, open, {
1353
+ placement: "bottom-start",
1354
+ offset: 6,
1355
+ matchWidth: true
1356
+ });
1357
+ useDismiss(
1358
+ listRef,
1359
+ open,
1360
+ () => {
1361
+ open.value = false;
1362
+ },
1363
+ { ignore: [wrapRef] }
1364
+ );
1365
+ watch(rows, () => active.value = firstEnabled(rows.value), { immediate: true });
1366
+ const toggle = (option) => {
1367
+ let optionValue = option.value;
1368
+ if (optionValue === CREATE_VALUE && props.onCreate) {
1369
+ optionValue = props.onCreate(query.value.trim());
1370
+ }
1371
+ if (props.multiple) {
1372
+ const next = selected.value.includes(optionValue) ? selected.value.filter((v) => v !== optionValue) : [...selected.value, optionValue];
1373
+ setSelected(next);
1374
+ query.value = "";
1375
+ inputRef.value?.focus();
1376
+ } else {
1377
+ setSelected([optionValue]);
1378
+ query.value = "";
1379
+ open.value = false;
1380
+ }
1381
+ };
1382
+ const remove = (optionValue) => {
1383
+ setSelected(selected.value.filter((v) => v !== optionValue));
1384
+ };
1385
+ const onKeydown = (event) => {
1386
+ switch (event.key) {
1387
+ case "ArrowDown":
1388
+ event.preventDefault();
1389
+ if (!open.value) {
1390
+ open.value = true;
1391
+ return;
1392
+ }
1393
+ active.value = stepActive(rows.value, active.value, 1);
1394
+ break;
1395
+ case "ArrowUp":
1396
+ event.preventDefault();
1397
+ if (!open.value) {
1398
+ open.value = true;
1399
+ return;
1400
+ }
1401
+ active.value = stepActive(rows.value, active.value, -1);
1402
+ break;
1403
+ case "Home":
1404
+ if (!open.value) return;
1405
+ event.preventDefault();
1406
+ active.value = firstEnabled(rows.value);
1407
+ break;
1408
+ case "End":
1409
+ if (!open.value) return;
1410
+ event.preventDefault();
1411
+ active.value = stepActive(rows.value, rows.value.length, -1, false);
1412
+ break;
1413
+ case "Enter": {
1414
+ if (!open.value) return;
1415
+ const option = rows.value[active.value];
1416
+ if (option && !option.disabled) {
1417
+ event.preventDefault();
1418
+ toggle(option);
1419
+ }
1420
+ break;
1421
+ }
1422
+ case "Escape":
1423
+ if (!open.value) return;
1424
+ event.preventDefault();
1425
+ open.value = false;
1426
+ break;
1427
+ case "Backspace":
1428
+ if (props.multiple && query.value === "" && selected.value.length > 0) {
1429
+ remove(selected.value[selected.value.length - 1]);
1430
+ }
1431
+ break;
1432
+ }
1433
+ };
1434
+ watch(
1435
+ [active, open],
1436
+ () => {
1437
+ if (!open.value) return;
1438
+ document.getElementById(`${listId}-option-${active.value}`)?.scrollIntoView?.({ block: "nearest" });
1439
+ },
1440
+ { flush: "post" }
1441
+ );
1442
+ return () => {
1443
+ const chips = props.multiple ? selected.value : [];
1444
+ const overflow = Math.max(0, chips.length - props.maxVisibleChips);
1445
+ const shownChips = overflow > 0 ? chips.slice(0, props.maxVisibleChips) : chips;
1446
+ const singleLabel = !props.multiple && selected.value[0] ? byValue.value.get(selected.value[0])?.label ?? "" : "";
1447
+ const inputValue = open.value || props.multiple ? query.value : singleLabel;
1448
+ return h(
1449
+ "div",
1450
+ { class: cx("ansh-combobox", `ansh-combobox--${props.size}`, attrs.class) },
1451
+ [
1452
+ h(
1453
+ "div",
1454
+ {
1455
+ ref: wrapRef,
1456
+ class: cx(
1457
+ "ansh-combobox__control",
1458
+ "ansh-focusable-within",
1459
+ open.value && "ansh-combobox__control--open",
1460
+ (props.invalid || fieldProps.value["aria-invalid"]) && "ansh-combobox__control--invalid",
1461
+ props.disabled && "ansh-combobox__control--disabled"
1462
+ ),
1463
+ // Clicking the padding focuses the input, but a click on a chip's remove button must
1464
+ // still reach that button.
1465
+ onMousedown: (event) => {
1466
+ if (event.target === event.currentTarget) {
1467
+ event.preventDefault();
1468
+ inputRef.value?.focus();
1469
+ open.value = true;
1470
+ }
1471
+ }
1472
+ },
1473
+ [
1474
+ ...shownChips.map((chipValue) => {
1475
+ const option = byValue.value.get(chipValue);
1476
+ return h("span", { key: chipValue, class: "ansh-chip" }, [
1477
+ option?.icon ? h("span", { class: "ansh-chip__icon" }, [resolveSlotContent(option.icon)]) : null,
1478
+ h("span", { class: "ansh-chip__label" }, option?.label ?? chipValue),
1479
+ h(
1480
+ "button",
1481
+ {
1482
+ type: "button",
1483
+ class: "ansh-chip__remove",
1484
+ "aria-label": `Remove ${option?.label ?? chipValue}`,
1485
+ disabled: props.disabled,
1486
+ onClick: () => remove(chipValue)
1487
+ },
1488
+ [
1489
+ h(
1490
+ "svg",
1491
+ { viewBox: "0 0 16 16", width: "10", height: "10", "aria-hidden": "true" },
1492
+ [
1493
+ h("path", {
1494
+ d: "M4 4l8 8M12 4l-8 8",
1495
+ stroke: "currentColor",
1496
+ "stroke-width": "2",
1497
+ "stroke-linecap": "round",
1498
+ fill: "none"
1499
+ })
1500
+ ]
1501
+ )
1502
+ ]
1503
+ )
1504
+ ]);
1505
+ }),
1506
+ overflow > 0 ? h("span", { class: "ansh-chip ansh-chip--count" }, `+${overflow}`) : null,
1507
+ h("input", {
1508
+ ref: inputRef,
1509
+ type: "text",
1510
+ role: "combobox",
1511
+ class: "ansh-combobox__input",
1512
+ value: inputValue,
1513
+ placeholder: chips.length > 0 ? "" : props.placeholder,
1514
+ disabled: props.disabled || fieldProps.value.disabled,
1515
+ autocomplete: "off",
1516
+ spellcheck: false,
1517
+ "aria-expanded": open.value,
1518
+ "aria-controls": open.value ? listId : void 0,
1519
+ "aria-autocomplete": "list",
1520
+ "aria-activedescendant": open.value && rows.value[active.value] ? `${listId}-option-${active.value}` : void 0,
1521
+ ...props.label ? { "aria-label": props.label } : null,
1522
+ ...fieldProps.value,
1523
+ onInput: (event) => {
1524
+ query.value = event.target.value;
1525
+ open.value = true;
1526
+ emit("search", query.value);
1527
+ },
1528
+ onFocus: () => {
1529
+ open.value = true;
1530
+ },
1531
+ onKeydown
1532
+ }),
1533
+ h("span", { class: "ansh-select__arrow", "aria-hidden": "true" }, [
1534
+ h("svg", { viewBox: "0 0 12 12", width: "12", height: "12" }, [
1535
+ h("path", {
1536
+ d: "M3 4.5L6 7.5l3-3",
1537
+ stroke: "currentColor",
1538
+ "stroke-width": "1.5",
1539
+ fill: "none",
1540
+ "stroke-linecap": "round",
1541
+ "stroke-linejoin": "round"
1542
+ })
1543
+ ])
1544
+ ])
1545
+ ]
1546
+ ),
1547
+ // The count is announced when it changes, so a screen-reader user knows filtering is
1548
+ // narrowing the list without having to arrow through it.
1549
+ h(
1550
+ "span",
1551
+ { class: "ansh-visually-hidden", "aria-live": "polite" },
1552
+ open.value ? `${visible.value.length} result${visible.value.length === 1 ? "" : "s"} available` : ""
1553
+ ),
1554
+ open.value ? h(Teleport, { to: "body" }, [
1555
+ h(
1556
+ "div",
1557
+ {
1558
+ ref: listRef,
1559
+ id: listId,
1560
+ role: "listbox",
1561
+ "aria-multiselectable": props.multiple || void 0,
1562
+ ...props.label ? { "aria-label": props.label } : null,
1563
+ class: "ansh-listbox",
1564
+ style: { ...position.value.style, maxHeight: props.maxHeight },
1565
+ "data-side": position.value.side
1566
+ },
1567
+ [
1568
+ h(OptionList, {
1569
+ options: rows.value,
1570
+ active: active.value,
1571
+ selected: selected.value,
1572
+ listId,
1573
+ multiple: props.multiple,
1574
+ query: query.value,
1575
+ emptyMessage: props.emptyMessage,
1576
+ onSelect: toggle,
1577
+ onActiveChange: (index) => {
1578
+ active.value = index;
1579
+ }
1580
+ })
1581
+ ]
1582
+ )
1583
+ ]) : null
1584
+ ]
1585
+ );
1586
+ };
1587
+ }
1588
+ });
1589
+ function snap(value, min, max, step) {
1590
+ if (step <= 0) return Math.min(Math.max(value, min), max);
1591
+ const stepped = Math.round((value - min) / step) * step + min;
1592
+ const decimals = String(step).includes(".") ? String(step).split(".")[1].length : 0;
1593
+ return Number(Math.min(Math.max(stepped, min), max).toFixed(decimals));
1594
+ }
1595
+ var Slider = defineComponent({
1596
+ name: "AnshSlider",
1597
+ props: {
1598
+ modelValue: { type: Number, default: void 0 },
1599
+ defaultValue: { type: Number, default: 0 },
1600
+ min: { type: Number, default: 0 },
1601
+ max: { type: Number, default: 100 },
1602
+ step: { type: Number, default: 1 },
1603
+ disabled: { type: Boolean, default: false },
1604
+ /** Vertical sliders are for mixers and volume; default is horizontal. */
1605
+ orientation: { type: String, default: "horizontal" },
1606
+ marks: { type: Array, default: void 0 },
1607
+ /** Show the value in a bubble above the thumb while dragging. */
1608
+ showValue: { type: [Boolean, String], default: false },
1609
+ /**
1610
+ * Format the bubble and the screen-reader announcement.
1611
+ *
1612
+ * `String` itself as the default — not a factory returning it. Vue never invokes a `Function`-
1613
+ * typed prop's `default` to resolve it (unlike `Object`/`Array`, where a factory is required);
1614
+ * it is used as-is. Wrapping it (`() => String`) would make `formatValue` itself resolve to that
1615
+ * wrapper, so calling `formatValue(current.value)` returns the `String` constructor instead of a
1616
+ * formatted string.
1617
+ */
1618
+ formatValue: { type: Function, default: String },
1619
+ label: { type: String, default: void 0 },
1620
+ name: { type: String, default: void 0 }
1621
+ },
1622
+ emits: ["update:modelValue", "commit"],
1623
+ setup(props, { emit, attrs }) {
1624
+ const fieldProps = useFieldControl();
1625
+ const [current, setCurrent] = useControllableState(
1626
+ () => props.modelValue,
1627
+ props.defaultValue,
1628
+ (next) => emit("update:modelValue", next)
1629
+ );
1630
+ const trackRef = ref(null);
1631
+ const dragging = ref(false);
1632
+ const id = useId("slider");
1633
+ const percent = computed(() => (current.value - props.min) / (props.max - props.min) * 100);
1634
+ const valueFromPointer = (event) => {
1635
+ const track = trackRef.value;
1636
+ if (!track) return current.value;
1637
+ const rect = track.getBoundingClientRect();
1638
+ const fraction = props.orientation === "horizontal" ? (event.clientX - rect.left) / rect.width : (
1639
+ // Inverted: in a vertical slider, up is more.
1640
+ 1 - (event.clientY - rect.top) / rect.height
1641
+ );
1642
+ return snap(props.min + fraction * (props.max - props.min), props.min, props.max, props.step);
1643
+ };
1644
+ const onPointerdown = (event) => {
1645
+ if (props.disabled) return;
1646
+ event.preventDefault();
1647
+ event.currentTarget.setPointerCapture(event.pointerId);
1648
+ dragging.value = true;
1649
+ let latest = valueFromPointer(event);
1650
+ setCurrent(latest);
1651
+ const onMove = (moveEvent) => {
1652
+ latest = valueFromPointer(moveEvent);
1653
+ setCurrent(latest);
1654
+ };
1655
+ const onUp = () => {
1656
+ dragging.value = false;
1657
+ emit("commit", latest);
1658
+ window.removeEventListener("pointermove", onMove);
1659
+ window.removeEventListener("pointerup", onUp);
1660
+ window.removeEventListener("pointercancel", onUp);
1661
+ };
1662
+ window.addEventListener("pointermove", onMove);
1663
+ window.addEventListener("pointerup", onUp);
1664
+ window.addEventListener("pointercancel", onUp);
1665
+ };
1666
+ const onKeydown = (event) => {
1667
+ if (props.disabled) return;
1668
+ const large = (props.max - props.min) / 10;
1669
+ let next = null;
1670
+ switch (event.key) {
1671
+ case "ArrowRight":
1672
+ case "ArrowUp":
1673
+ next = current.value + props.step;
1674
+ break;
1675
+ case "ArrowLeft":
1676
+ case "ArrowDown":
1677
+ next = current.value - props.step;
1678
+ break;
1679
+ case "PageUp":
1680
+ next = current.value + large;
1681
+ break;
1682
+ case "PageDown":
1683
+ next = current.value - large;
1684
+ break;
1685
+ case "Home":
1686
+ next = props.min;
1687
+ break;
1688
+ case "End":
1689
+ next = props.max;
1690
+ break;
1691
+ }
1692
+ if (next === null) return;
1693
+ event.preventDefault();
1694
+ const snapped = snap(next, props.min, props.max, props.step);
1695
+ setCurrent(snapped);
1696
+ emit("commit", snapped);
1697
+ };
1698
+ return () => h(
1699
+ "div",
1700
+ {
1701
+ class: cx(
1702
+ "ansh-slider",
1703
+ `ansh-slider--${props.orientation}`,
1704
+ props.disabled && "ansh-slider--disabled",
1705
+ dragging.value && "ansh-slider--dragging",
1706
+ attrs.class
1707
+ ),
1708
+ style: { ["--ansh-slider-percent"]: `${percent.value}%` }
1709
+ },
1710
+ [
1711
+ h("div", { ref: trackRef, class: "ansh-slider__track", onPointerdown }, [
1712
+ h("div", { class: "ansh-slider__fill" }),
1713
+ ...props.marks?.map((mark) => {
1714
+ const markPercent = (mark.value - props.min) / (props.max - props.min) * 100;
1715
+ return h(
1716
+ "span",
1717
+ {
1718
+ key: mark.value,
1719
+ class: cx(
1720
+ "ansh-slider__mark",
1721
+ current.value >= mark.value && "ansh-slider__mark--passed"
1722
+ ),
1723
+ style: { ["--ansh-mark"]: `${markPercent}%` }
1724
+ },
1725
+ [
1726
+ mark.label ? h("span", { class: "ansh-slider__mark-label" }, [
1727
+ resolveSlotContent(mark.label)
1728
+ ]) : null
1729
+ ]
1730
+ );
1731
+ }) ?? [],
1732
+ h(
1733
+ "div",
1734
+ {
1735
+ role: "slider",
1736
+ tabindex: props.disabled ? "-1" : "0",
1737
+ class: "ansh-slider__thumb ansh-focusable",
1738
+ "aria-valuenow": current.value,
1739
+ "aria-valuemin": props.min,
1740
+ "aria-valuemax": props.max,
1741
+ // The formatted string, when it differs from the raw number. A slider showing
1742
+ // "£1,200" should not be announced as "1200".
1743
+ ...props.formatValue(current.value) !== String(current.value) ? { "aria-valuetext": props.formatValue(current.value) } : null,
1744
+ "aria-orientation": props.orientation,
1745
+ "aria-disabled": props.disabled || void 0,
1746
+ ...props.label ? { "aria-label": props.label } : null,
1747
+ ...fieldProps.value.id ? { id: fieldProps.value.id } : null,
1748
+ ...fieldProps.value["aria-describedby"] ? { "aria-describedby": fieldProps.value["aria-describedby"] } : null,
1749
+ onPointerdown,
1750
+ onKeydown
1751
+ },
1752
+ [
1753
+ props.showValue === "always" || props.showValue && dragging.value ? h(
1754
+ "span",
1755
+ { class: "ansh-slider__bubble", "aria-hidden": "true" },
1756
+ props.formatValue(current.value)
1757
+ ) : null
1758
+ ]
1759
+ )
1760
+ ]),
1761
+ props.name ? h("input", {
1762
+ type: "hidden",
1763
+ name: props.name,
1764
+ value: current.value,
1765
+ id: `${id}-hidden`
1766
+ }) : null
1767
+ ]
1768
+ );
1769
+ }
1770
+ });
1771
+ var RangeSlider = defineComponent({
1772
+ name: "AnshRangeSlider",
1773
+ props: {
1774
+ // `Array as PropType<readonly [number, number]>` doesn't typecheck directly — TS won't accept
1775
+ // the cast because `any[]` (what `ArrayConstructor` produces) doesn't sufficiently overlap with
1776
+ // a fixed-length tuple. Routing through `unknown` is the standard escape for a tuple-typed Vue
1777
+ // prop; the runtime `type: Array` validation is unaffected.
1778
+ modelValue: {
1779
+ type: Array,
1780
+ default: void 0
1781
+ },
1782
+ defaultValue: {
1783
+ type: Array,
1784
+ default: () => [0, 100]
1785
+ },
1786
+ min: { type: Number, default: 0 },
1787
+ max: { type: Number, default: 100 },
1788
+ step: { type: Number, default: 1 },
1789
+ /** Smallest allowed gap between the two thumbs. Default `0`. */
1790
+ minDistance: { type: Number, default: 0 },
1791
+ disabled: { type: Boolean, default: false },
1792
+ marks: { type: Array, default: void 0 },
1793
+ showValue: { type: [Boolean, String], default: false },
1794
+ // `String` itself, not a factory — see the note on `Slider`'s `formatValue` above.
1795
+ formatValue: { type: Function, default: String },
1796
+ label: { type: String, default: void 0 }
1797
+ },
1798
+ emits: ["update:modelValue", "commit"],
1799
+ setup(props, { emit, attrs }) {
1800
+ const [range, setRange] = useControllableState(
1801
+ () => props.modelValue,
1802
+ props.defaultValue,
1803
+ (next) => emit("update:modelValue", next)
1804
+ );
1805
+ const trackRef = ref(null);
1806
+ const dragging = ref(null);
1807
+ const low = computed(() => range.value[0]);
1808
+ const high = computed(() => range.value[1]);
1809
+ const lowPercent = computed(() => (low.value - props.min) / (props.max - props.min) * 100);
1810
+ const highPercent = computed(() => (high.value - props.min) / (props.max - props.min) * 100);
1811
+ const valueFromPointer = (event) => {
1812
+ const track = trackRef.value;
1813
+ if (!track) return low.value;
1814
+ const rect = track.getBoundingClientRect();
1815
+ return snap(
1816
+ props.min + (event.clientX - rect.left) / rect.width * (props.max - props.min),
1817
+ props.min,
1818
+ props.max,
1819
+ props.step
1820
+ );
1821
+ };
1822
+ const apply = (thumb, next) => {
1823
+ if (thumb === 0) {
1824
+ return next > high.value - props.minDistance ? [high.value - props.minDistance, high.value] : [next, high.value];
1825
+ }
1826
+ return next < low.value + props.minDistance ? [low.value, low.value + props.minDistance] : [low.value, next];
1827
+ };
1828
+ const startDrag = (thumb) => (event) => {
1829
+ if (props.disabled) return;
1830
+ event.preventDefault();
1831
+ event.currentTarget.setPointerCapture(event.pointerId);
1832
+ dragging.value = thumb;
1833
+ let latest = range.value;
1834
+ const onMove = (moveEvent) => {
1835
+ latest = apply(thumb, valueFromPointer(moveEvent));
1836
+ setRange(latest);
1837
+ };
1838
+ const onUp = () => {
1839
+ dragging.value = null;
1840
+ emit("commit", latest);
1841
+ window.removeEventListener("pointermove", onMove);
1842
+ window.removeEventListener("pointerup", onUp);
1843
+ };
1844
+ window.addEventListener("pointermove", onMove);
1845
+ window.addEventListener("pointerup", onUp);
1846
+ };
1847
+ const keyHandler = (thumb) => (event) => {
1848
+ if (props.disabled) return;
1849
+ const currentValue = range.value[thumb];
1850
+ const large = (props.max - props.min) / 10;
1851
+ let next = null;
1852
+ switch (event.key) {
1853
+ case "ArrowRight":
1854
+ case "ArrowUp":
1855
+ next = currentValue + props.step;
1856
+ break;
1857
+ case "ArrowLeft":
1858
+ case "ArrowDown":
1859
+ next = currentValue - props.step;
1860
+ break;
1861
+ case "PageUp":
1862
+ next = currentValue + large;
1863
+ break;
1864
+ case "PageDown":
1865
+ next = currentValue - large;
1866
+ break;
1867
+ case "Home":
1868
+ next = props.min;
1869
+ break;
1870
+ case "End":
1871
+ next = props.max;
1872
+ break;
1873
+ }
1874
+ if (next === null) return;
1875
+ event.preventDefault();
1876
+ const applied = apply(thumb, snap(next, props.min, props.max, props.step));
1877
+ setRange(applied);
1878
+ emit("commit", applied);
1879
+ };
1880
+ const renderThumb = (index, thumbLabel) => h(
1881
+ "div",
1882
+ {
1883
+ key: index,
1884
+ role: "slider",
1885
+ tabindex: props.disabled ? "-1" : "0",
1886
+ class: "ansh-slider__thumb ansh-focusable",
1887
+ style: {
1888
+ ["--ansh-thumb"]: `${index === 0 ? lowPercent.value : highPercent.value}%`
1889
+ },
1890
+ "aria-label": props.label ? `${props.label} ${thumbLabel}` : thumbLabel,
1891
+ "aria-valuenow": range.value[index],
1892
+ // Each thumb's range is bounded by the other, so the announced limits match what the
1893
+ // control will actually accept.
1894
+ "aria-valuemin": index === 0 ? props.min : low.value + props.minDistance,
1895
+ "aria-valuemax": index === 0 ? high.value - props.minDistance : props.max,
1896
+ ...props.formatValue(range.value[index]) !== String(range.value[index]) ? { "aria-valuetext": props.formatValue(range.value[index]) } : null,
1897
+ "aria-disabled": props.disabled || void 0,
1898
+ onPointerdown: startDrag(index),
1899
+ onKeydown: keyHandler(index)
1900
+ },
1901
+ [
1902
+ props.showValue === "always" || props.showValue && dragging.value === index ? h(
1903
+ "span",
1904
+ { class: "ansh-slider__bubble", "aria-hidden": "true" },
1905
+ props.formatValue(range.value[index])
1906
+ ) : null
1907
+ ]
1908
+ );
1909
+ return () => h(
1910
+ "div",
1911
+ {
1912
+ class: cx(
1913
+ "ansh-slider",
1914
+ "ansh-slider--horizontal",
1915
+ "ansh-slider--range",
1916
+ props.disabled && "ansh-slider--disabled",
1917
+ attrs.class
1918
+ ),
1919
+ style: {
1920
+ ["--ansh-range-start"]: `${lowPercent.value}%`,
1921
+ ["--ansh-range-end"]: `${highPercent.value}%`
1922
+ }
1923
+ },
1924
+ [
1925
+ h("div", { ref: trackRef, class: "ansh-slider__track" }, [
1926
+ h("div", { class: "ansh-slider__fill ansh-slider__fill--range" }),
1927
+ ...props.marks?.map(
1928
+ (mark) => h(
1929
+ "span",
1930
+ {
1931
+ key: mark.value,
1932
+ class: cx(
1933
+ "ansh-slider__mark",
1934
+ mark.value >= low.value && mark.value <= high.value && "ansh-slider__mark--passed"
1935
+ ),
1936
+ style: {
1937
+ ["--ansh-mark"]: `${(mark.value - props.min) / (props.max - props.min) * 100}%`
1938
+ }
1939
+ },
1940
+ [
1941
+ mark.label ? h("span", { class: "ansh-slider__mark-label" }, [
1942
+ resolveSlotContent(mark.label)
1943
+ ]) : null
1944
+ ]
1945
+ )
1946
+ ) ?? [],
1947
+ renderThumb(0, "minimum"),
1948
+ renderThumb(1, "maximum")
1949
+ ])
1950
+ ]
1951
+ );
1952
+ }
1953
+ });
1954
+ var PinInput = defineComponent({
1955
+ name: "AnshPinInput",
1956
+ props: {
1957
+ /** Number of cells. Default `6`. */
1958
+ length: { type: Number, default: 6 },
1959
+ modelValue: { type: String, default: void 0 },
1960
+ defaultValue: { type: String, default: "" },
1961
+ /** Restrict input. Default `'numeric'`. */
1962
+ type: { type: String, default: "numeric" },
1963
+ /** Render as password dots. */
1964
+ mask: { type: Boolean, default: false },
1965
+ disabled: { type: Boolean, default: false },
1966
+ invalid: { type: Boolean, default: false },
1967
+ /** Focus the first cell on mount. Native `autofocus`, spelled lower-case to match the attribute. */
1968
+ autofocus: { type: Boolean, default: false },
1969
+ label: { type: String, default: "Verification code" }
1970
+ },
1971
+ emits: ["update:modelValue", "complete"],
1972
+ setup(props, { emit, attrs }) {
1973
+ const [code, setCode] = useControllableState(
1974
+ () => props.modelValue,
1975
+ props.defaultValue,
1976
+ (next) => emit("update:modelValue", next)
1977
+ );
1978
+ const inputs = ref([]);
1979
+ const id = useId("pin");
1980
+ let completed = false;
1981
+ watch(
1982
+ code,
1983
+ (value) => {
1984
+ if (value.length === props.length && !completed) {
1985
+ completed = true;
1986
+ emit("complete", value);
1987
+ } else if (value.length < props.length) {
1988
+ completed = false;
1989
+ }
1990
+ },
1991
+ { immediate: true }
1992
+ );
1993
+ const setCharAt = (index, char) => {
1994
+ const chars = code.value.padEnd(props.length, " ").split("");
1995
+ chars[index] = char;
1996
+ setCode(chars.join("").trimEnd());
1997
+ };
1998
+ const focusCell = (index) => {
1999
+ const target = inputs.value[Math.min(Math.max(index, 0), props.length - 1)];
2000
+ target?.focus();
2001
+ target?.select();
2002
+ };
2003
+ const onCellChange = (index, raw) => {
2004
+ const pattern = props.type === "numeric" ? /[^0-9]/g : /[^a-zA-Z0-9]/g;
2005
+ const cleaned = raw.replace(pattern, "");
2006
+ if (cleaned === "") {
2007
+ setCharAt(index, " ");
2008
+ return;
2009
+ }
2010
+ if (cleaned.length > 1) {
2011
+ const chars = code.value.padEnd(props.length, " ").split("");
2012
+ for (let i = 0; i < cleaned.length && index + i < props.length; i++) {
2013
+ chars[index + i] = cleaned[i];
2014
+ }
2015
+ setCode(chars.join("").trimEnd());
2016
+ focusCell(index + cleaned.length);
2017
+ return;
2018
+ }
2019
+ setCharAt(index, cleaned);
2020
+ focusCell(index + 1);
2021
+ };
2022
+ const onCellKeydown = (index) => (event) => {
2023
+ switch (event.key) {
2024
+ case "Backspace": {
2025
+ event.preventDefault();
2026
+ if ((code.value[index] ?? " ").trim() === "") {
2027
+ setCharAt(index - 1, " ");
2028
+ focusCell(index - 1);
2029
+ } else {
2030
+ setCharAt(index, " ");
2031
+ }
2032
+ break;
2033
+ }
2034
+ case "Delete":
2035
+ event.preventDefault();
2036
+ setCharAt(index, " ");
2037
+ break;
2038
+ case "ArrowLeft":
2039
+ event.preventDefault();
2040
+ focusCell(index - 1);
2041
+ break;
2042
+ case "ArrowRight":
2043
+ event.preventDefault();
2044
+ focusCell(index + 1);
2045
+ break;
2046
+ case "Home":
2047
+ event.preventDefault();
2048
+ focusCell(0);
2049
+ break;
2050
+ case "End":
2051
+ event.preventDefault();
2052
+ focusCell(props.length - 1);
2053
+ break;
2054
+ }
2055
+ };
2056
+ return () => h(
2057
+ "div",
2058
+ {
2059
+ class: cx(
2060
+ "ansh-pin",
2061
+ props.invalid && "ansh-pin--invalid",
2062
+ props.disabled && "ansh-pin--disabled",
2063
+ attrs.class
2064
+ ),
2065
+ role: "group",
2066
+ "aria-label": props.label,
2067
+ // The group is described by the live region below, so progress is announced without each
2068
+ // cell interrupting with "1 of 6, edit text".
2069
+ "aria-describedby": `${id}-status`
2070
+ },
2071
+ [
2072
+ ...Array.from(
2073
+ { length: props.length },
2074
+ (_, index) => h("input", {
2075
+ key: index,
2076
+ ref: (node) => {
2077
+ inputs.value[index] = node ?? null;
2078
+ },
2079
+ class: "ansh-pin__cell ansh-focusable",
2080
+ type: props.mask ? "password" : "text",
2081
+ inputmode: props.type === "numeric" ? "numeric" : "text",
2082
+ // Only the first cell advertises one-time-code; on the others it makes the platform
2083
+ // offer to fill a single character, which is nonsense.
2084
+ autocomplete: index === 0 ? "one-time-code" : "off",
2085
+ autofocus: props.autofocus && index === 0,
2086
+ maxlength: props.length,
2087
+ disabled: props.disabled,
2088
+ "aria-label": `Digit ${index + 1} of ${props.length}`,
2089
+ "aria-invalid": props.invalid || void 0,
2090
+ value: (code.value[index] ?? "").trim(),
2091
+ onInput: (event) => onCellChange(index, event.target.value),
2092
+ onKeydown: onCellKeydown(index),
2093
+ onFocus: (event) => event.target.select()
2094
+ })
2095
+ ),
2096
+ h(
2097
+ "span",
2098
+ { class: "ansh-visually-hidden", id: `${id}-status`, "aria-live": "polite" },
2099
+ code.value.trim().length === props.length ? "Code complete" : `${code.value.trim().length} of ${props.length} digits entered`
2100
+ )
2101
+ ]
2102
+ );
2103
+ }
2104
+ });
2105
+ var TagsInput = defineComponent({
2106
+ name: "AnshTagsInput",
2107
+ props: {
2108
+ modelValue: { type: Array, default: void 0 },
2109
+ defaultValue: { type: Array, default: () => [] },
2110
+ placeholder: { type: String, default: "Add and press Enter" },
2111
+ /** Maximum number of tags. The input hides once reached. */
2112
+ max: { type: Number, default: Number.POSITIVE_INFINITY },
2113
+ /** Keys that commit the current text. Default Enter, Tab and comma. */
2114
+ delimiters: {
2115
+ type: Array,
2116
+ default: () => ["Enter", "Tab", ","]
2117
+ },
2118
+ /** Reject a value — return an error string, or `null` to accept. */
2119
+ validate: { type: Function, default: void 0 },
2120
+ /** Fold duplicates silently instead of flagging them. Default `true`. */
2121
+ dedupe: { type: Boolean, default: true },
2122
+ disabled: { type: Boolean, default: false },
2123
+ invalid: { type: Boolean, default: false },
2124
+ label: { type: String, default: "Tags" }
2125
+ },
2126
+ emits: ["update:modelValue"],
2127
+ setup(props, { emit, attrs }) {
2128
+ const fieldProps = useFieldControl();
2129
+ const [tags, setTags] = useControllableState(
2130
+ () => props.modelValue,
2131
+ props.defaultValue,
2132
+ (next) => emit("update:modelValue", next)
2133
+ );
2134
+ const draft = ref("");
2135
+ const error = ref(null);
2136
+ const inputRef = ref(null);
2137
+ const tagRefs = ref([]);
2138
+ const commit = (raw) => {
2139
+ const text = raw.trim();
2140
+ if (text === "") return false;
2141
+ if (tags.value.length >= props.max) {
2142
+ error.value = `Up to ${props.max} allowed`;
2143
+ return false;
2144
+ }
2145
+ if (props.dedupe && tags.value.includes(text)) {
2146
+ draft.value = "";
2147
+ return true;
2148
+ }
2149
+ const failure = props.validate?.(text);
2150
+ if (failure) {
2151
+ error.value = failure;
2152
+ return false;
2153
+ }
2154
+ setTags([...tags.value, text]);
2155
+ draft.value = "";
2156
+ error.value = null;
2157
+ return true;
2158
+ };
2159
+ const removeAt = (index) => {
2160
+ setTags(tags.value.filter((_, i) => i !== index));
2161
+ error.value = null;
2162
+ queueMicrotask(() => {
2163
+ const next = tagRefs.value[index] ?? tagRefs.value[index - 1];
2164
+ if (next?.isConnected) next.focus();
2165
+ else inputRef.value?.focus();
2166
+ });
2167
+ };
2168
+ const onKeydown = (event) => {
2169
+ if (event.key === "Tab" && draft.value.trim() === "") return;
2170
+ if (props.delimiters.includes(event.key)) {
2171
+ event.preventDefault();
2172
+ commit(draft.value);
2173
+ return;
2174
+ }
2175
+ if (event.key === "Backspace" && draft.value === "" && tags.value.length > 0) {
2176
+ event.preventDefault();
2177
+ removeAt(tags.value.length - 1);
2178
+ }
2179
+ };
2180
+ return () => {
2181
+ const atLimit = tags.value.length >= props.max;
2182
+ return h("div", { class: cx("ansh-tags", attrs.class) }, [
2183
+ h(
2184
+ "div",
2185
+ {
2186
+ class: cx(
2187
+ "ansh-tags__control",
2188
+ "ansh-focusable-within",
2189
+ (props.invalid || error.value) && "ansh-tags__control--invalid",
2190
+ props.disabled && "ansh-tags__control--disabled"
2191
+ ),
2192
+ onMousedown: (event) => {
2193
+ if (event.target === event.currentTarget) {
2194
+ event.preventDefault();
2195
+ inputRef.value?.focus();
2196
+ }
2197
+ }
2198
+ },
2199
+ [
2200
+ // Labelled distinctly from the input. Giving both the same accessible name produces two
2201
+ // different elements indistinguishable in a screen reader's element list — and ambiguous
2202
+ // to every testing query.
2203
+ h(
2204
+ "ul",
2205
+ { class: "ansh-tags__list", "aria-label": `${props.label}: selected` },
2206
+ tags.value.map(
2207
+ (tag, index) => h("li", { key: `${tag}-${index}`, class: "ansh-chip" }, [
2208
+ h("span", { class: "ansh-chip__label" }, tag),
2209
+ h(
2210
+ "button",
2211
+ {
2212
+ ref: (node) => {
2213
+ tagRefs.value[index] = node ?? null;
2214
+ },
2215
+ type: "button",
2216
+ class: "ansh-chip__remove",
2217
+ "aria-label": `Remove ${tag}`,
2218
+ disabled: props.disabled,
2219
+ onClick: () => removeAt(index)
2220
+ },
2221
+ [
2222
+ h(
2223
+ "svg",
2224
+ { viewBox: "0 0 16 16", width: "10", height: "10", "aria-hidden": "true" },
2225
+ [
2226
+ h("path", {
2227
+ d: "M4 4l8 8M12 4l-8 8",
2228
+ stroke: "currentColor",
2229
+ "stroke-width": "2",
2230
+ "stroke-linecap": "round",
2231
+ fill: "none"
2232
+ })
2233
+ ]
2234
+ )
2235
+ ]
2236
+ )
2237
+ ])
2238
+ )
2239
+ ),
2240
+ !atLimit ? h("input", {
2241
+ ref: inputRef,
2242
+ type: "text",
2243
+ class: "ansh-tags__input",
2244
+ value: draft.value,
2245
+ placeholder: tags.value.length === 0 ? props.placeholder : "",
2246
+ disabled: props.disabled,
2247
+ "aria-label": props.label,
2248
+ "aria-invalid": props.invalid || Boolean(error.value) || void 0,
2249
+ ...fieldProps.value,
2250
+ onInput: (event) => {
2251
+ draft.value = event.target.value;
2252
+ error.value = null;
2253
+ },
2254
+ onKeydown,
2255
+ // Committing on blur stops the most common data-loss complaint: typing a tag,
2256
+ // clicking Save, and finding it was never added.
2257
+ onBlur: () => commit(draft.value),
2258
+ onPaste: (event) => {
2259
+ const text = event.clipboardData?.getData("text") ?? "";
2260
+ if (!/[,\n\t]/.test(text)) return;
2261
+ event.preventDefault();
2262
+ for (const part of text.split(/[,\n\t]+/)) commit(part);
2263
+ }
2264
+ }) : null
2265
+ ]
2266
+ ),
2267
+ error.value ? h("p", { class: "ansh-field__error", role: "alert" }, error.value) : null
2268
+ ]);
2269
+ };
2270
+ }
2271
+ });
2272
+ var Rating = defineComponent({
2273
+ name: "AnshRating",
2274
+ props: {
2275
+ modelValue: { type: Number, default: void 0 },
2276
+ defaultValue: { type: Number, default: 0 },
2277
+ /** Number of symbols. Default `5`. */
2278
+ max: { type: Number, default: 5 },
2279
+ /** Allow halves. Default `false`. */
2280
+ allowHalf: { type: Boolean, default: false },
2281
+ /** Display only — no interaction, no focus. */
2282
+ readonly: { type: Boolean, default: false },
2283
+ disabled: { type: Boolean, default: false },
2284
+ size: { type: String, default: "md" },
2285
+ /** Custom symbol. Defaults to a star. */
2286
+ icon: { type: [String, Object, Function], default: void 0 },
2287
+ label: { type: String, default: "Rating" },
2288
+ name: { type: String, default: void 0 }
2289
+ },
2290
+ emits: ["update:modelValue"],
2291
+ setup(props, { emit, attrs }) {
2292
+ const [current, setCurrent] = useControllableState(
2293
+ () => props.modelValue,
2294
+ props.defaultValue,
2295
+ (next) => emit("update:modelValue", next)
2296
+ );
2297
+ const preview = ref(null);
2298
+ const groupName = useId("rating");
2299
+ const shown = computed(() => preview.value ?? current.value);
2300
+ const renderSymbol = () => props.icon ? resolveSlotContent(props.icon) : h("svg", { viewBox: "0 0 20 20", class: "ansh-rating__glyph", "aria-hidden": "true" }, [
2301
+ h("path", {
2302
+ d: "M10 1.6l2.6 5.3 5.8.8-4.2 4.1 1 5.8-5.2-2.7-5.2 2.7 1-5.8L1.6 7.7l5.8-.8z",
2303
+ fill: "currentColor"
2304
+ })
2305
+ ]);
2306
+ return () => {
2307
+ if (props.readonly) {
2308
+ return h(
2309
+ "span",
2310
+ {
2311
+ class: cx(
2312
+ "ansh-rating",
2313
+ `ansh-rating--${props.size}`,
2314
+ "ansh-rating--readonly",
2315
+ attrs.class
2316
+ ),
2317
+ role: "img",
2318
+ "aria-label": `${current.value} out of ${props.max}`
2319
+ },
2320
+ Array.from(
2321
+ { length: props.max },
2322
+ (_, index) => h(
2323
+ "span",
2324
+ {
2325
+ key: index,
2326
+ class: "ansh-rating__item",
2327
+ // A percentage per symbol, so 4.2 fills the fifth star 20% of the way rather than
2328
+ // rounding to a whole star and misreporting the value.
2329
+ style: {
2330
+ ["--ansh-fill"]: `${Math.min(Math.max(current.value - index, 0), 1) * 100}%`
2331
+ }
2332
+ },
2333
+ [renderSymbol()]
2334
+ )
2335
+ )
2336
+ );
2337
+ }
2338
+ const steps = props.allowHalf ? props.max * 2 : props.max;
2339
+ return h(
2340
+ "span",
2341
+ {
2342
+ role: "radiogroup",
2343
+ "aria-label": props.label,
2344
+ class: cx(
2345
+ "ansh-rating",
2346
+ `ansh-rating--${props.size}`,
2347
+ props.disabled && "ansh-rating--disabled",
2348
+ attrs.class
2349
+ ),
2350
+ onMouseleave: () => {
2351
+ preview.value = null;
2352
+ }
2353
+ },
2354
+ Array.from({ length: steps }, (_, step) => {
2355
+ const stepValue = props.allowHalf ? (step + 1) / 2 : step + 1;
2356
+ const index = Math.ceil(stepValue) - 1;
2357
+ const isHalf = props.allowHalf && stepValue % 1 !== 0;
2358
+ return h(
2359
+ "label",
2360
+ {
2361
+ key: stepValue,
2362
+ class: cx("ansh-rating__hit", isHalf && "ansh-rating__hit--half"),
2363
+ onMouseenter: () => {
2364
+ if (!props.disabled) preview.value = stepValue;
2365
+ }
2366
+ },
2367
+ [
2368
+ h("input", {
2369
+ type: "radio",
2370
+ class: "ansh-visually-hidden",
2371
+ name: props.name ?? groupName,
2372
+ value: stepValue,
2373
+ checked: current.value === stepValue,
2374
+ disabled: props.disabled,
2375
+ "aria-label": `${stepValue} ${stepValue === 1 ? "star" : "stars"}`,
2376
+ onChange: () => setCurrent(stepValue)
2377
+ }),
2378
+ // Only whole steps draw a symbol; the half-step label is an invisible hit area
2379
+ // layered over the left side of the one that follows it.
2380
+ !isHalf ? h(
2381
+ "span",
2382
+ {
2383
+ class: "ansh-rating__item",
2384
+ style: {
2385
+ ["--ansh-fill"]: `${Math.min(Math.max(shown.value - index, 0), 1) * 100}%`
2386
+ }
2387
+ },
2388
+ [renderSymbol()]
2389
+ ) : null
2390
+ ]
2391
+ );
2392
+ })
2393
+ );
2394
+ };
2395
+ }
2396
+ });
2397
+ function formatBytes(bytes) {
2398
+ if (bytes < 1024) return `${bytes} B`;
2399
+ const units = ["KB", "MB", "GB"];
2400
+ let size = bytes / 1024;
2401
+ let unit = 0;
2402
+ while (size >= 1024 && unit < units.length - 1) {
2403
+ size /= 1024;
2404
+ unit++;
2405
+ }
2406
+ return `${size.toFixed(size >= 10 ? 0 : 1)} ${units[unit]}`;
2407
+ }
2408
+ var Dropzone = defineComponent({
2409
+ name: "AnshDropzone",
2410
+ props: {
2411
+ /** `accept` attribute, e.g. `'image/*,.pdf'`. */
2412
+ accept: { type: String, default: void 0 },
2413
+ multiple: { type: Boolean, default: false },
2414
+ /** Per-file size cap, in bytes. */
2415
+ maxSize: { type: Number, default: void 0 },
2416
+ /** Maximum number of files. */
2417
+ maxFiles: { type: Number, default: Number.POSITIVE_INFINITY },
2418
+ disabled: { type: Boolean, default: false },
2419
+ /** Show thumbnails for images. Default `true`. */
2420
+ previews: { type: Boolean, default: true },
2421
+ label: {
2422
+ type: [String, Object, Function],
2423
+ default: "Drop files here, or browse"
2424
+ },
2425
+ hint: { type: [String, Object, Function], default: void 0 }
2426
+ },
2427
+ emits: ["files"],
2428
+ setup(props, { emit, attrs, slots }) {
2429
+ const files = ref([]);
2430
+ const dragging = ref(false);
2431
+ let depth = 0;
2432
+ const inputRef = ref(null);
2433
+ const id = useId("dropzone");
2434
+ const validate = (file) => {
2435
+ if (props.maxSize !== void 0 && file.size > props.maxSize) {
2436
+ return `Larger than ${formatBytes(props.maxSize)}`;
2437
+ }
2438
+ if (props.accept) {
2439
+ const patterns = props.accept.split(",").map((part) => part.trim().toLowerCase());
2440
+ const name = file.name.toLowerCase();
2441
+ const type = file.type.toLowerCase();
2442
+ const ok = patterns.some((pattern) => {
2443
+ if (pattern.startsWith(".")) return name.endsWith(pattern);
2444
+ if (pattern.endsWith("/*")) return type.startsWith(pattern.slice(0, -1));
2445
+ return type === pattern;
2446
+ });
2447
+ if (!ok) return "Unsupported file type";
2448
+ }
2449
+ return void 0;
2450
+ };
2451
+ const add = (incoming) => {
2452
+ if (!incoming || props.disabled) return;
2453
+ const accepted = [];
2454
+ const room = props.maxFiles - files.value.length;
2455
+ for (const file of Array.from(incoming).slice(0, props.multiple ? room : 1)) {
2456
+ const error = validate(file);
2457
+ accepted.push({
2458
+ file,
2459
+ id: `${file.name}-${file.size}-${file.lastModified}`,
2460
+ ...props.previews && file.type.startsWith("image/") && !error ? { preview: URL.createObjectURL(file) } : null,
2461
+ ...error ? { error } : null
2462
+ });
2463
+ }
2464
+ const next = props.multiple ? [...files.value, ...accepted] : accepted;
2465
+ if (!props.multiple) {
2466
+ for (const previous of files.value) {
2467
+ if (previous.preview) URL.revokeObjectURL(previous.preview);
2468
+ }
2469
+ }
2470
+ files.value = next;
2471
+ emit("files", next);
2472
+ };
2473
+ const remove = (fileId) => {
2474
+ const target = files.value.find((entry) => entry.id === fileId);
2475
+ if (target?.preview) URL.revokeObjectURL(target.preview);
2476
+ const next = files.value.filter((entry) => entry.id !== fileId);
2477
+ files.value = next;
2478
+ emit("files", next);
2479
+ if (inputRef.value) inputRef.value.value = "";
2480
+ };
2481
+ return () => {
2482
+ const rejected = files.value.filter((entry) => entry.error);
2483
+ return h("div", { class: cx("ansh-dropzone-wrap", attrs.class) }, [
2484
+ h(
2485
+ "div",
2486
+ {
2487
+ class: cx(
2488
+ "ansh-dropzone",
2489
+ dragging.value && "ansh-dropzone--active",
2490
+ props.disabled && "ansh-dropzone--disabled"
2491
+ ),
2492
+ onDragenter: (event) => {
2493
+ event.preventDefault();
2494
+ depth++;
2495
+ if (!props.disabled) dragging.value = true;
2496
+ },
2497
+ onDragover: (event) => {
2498
+ event.preventDefault();
2499
+ if (event.dataTransfer)
2500
+ event.dataTransfer.dropEffect = props.disabled ? "none" : "copy";
2501
+ },
2502
+ onDragleave: (event) => {
2503
+ event.preventDefault();
2504
+ depth--;
2505
+ if (depth <= 0) {
2506
+ depth = 0;
2507
+ dragging.value = false;
2508
+ }
2509
+ },
2510
+ onDrop: (event) => {
2511
+ event.preventDefault();
2512
+ depth = 0;
2513
+ dragging.value = false;
2514
+ add(event.dataTransfer?.files ?? null);
2515
+ }
2516
+ },
2517
+ [
2518
+ h("input", {
2519
+ ref: inputRef,
2520
+ id,
2521
+ type: "file",
2522
+ class: "ansh-dropzone__input",
2523
+ accept: props.accept,
2524
+ multiple: props.multiple,
2525
+ disabled: props.disabled,
2526
+ onChange: (event) => add(event.target.files)
2527
+ }),
2528
+ h("label", { for: id, class: "ansh-dropzone__label" }, [
2529
+ h("span", { class: "ansh-dropzone__icon", "aria-hidden": "true" }, [
2530
+ h(
2531
+ "svg",
2532
+ {
2533
+ viewBox: "0 0 24 24",
2534
+ width: "28",
2535
+ height: "28",
2536
+ fill: "none",
2537
+ stroke: "currentColor",
2538
+ "stroke-width": "1.5"
2539
+ },
2540
+ [
2541
+ h("path", {
2542
+ d: "M12 16V4m0 0L8 8m4-4l4 4",
2543
+ "stroke-linecap": "round",
2544
+ "stroke-linejoin": "round"
2545
+ }),
2546
+ h("path", {
2547
+ d: "M3 15v3a3 3 0 003 3h12a3 3 0 003-3v-3",
2548
+ "stroke-linecap": "round"
2549
+ })
2550
+ ]
2551
+ )
2552
+ ]),
2553
+ h("span", { class: "ansh-dropzone__text" }, [resolveSlotContent(props.label)]),
2554
+ props.hint ? h("span", { class: "ansh-dropzone__hint" }, [resolveSlotContent(props.hint)]) : null
2555
+ ]),
2556
+ slots.default?.()
2557
+ ]
2558
+ ),
2559
+ files.value.length > 0 ? h(
2560
+ "ul",
2561
+ { class: "ansh-dropzone__files" },
2562
+ files.value.map(
2563
+ (entry) => h(
2564
+ "li",
2565
+ {
2566
+ key: entry.id,
2567
+ class: cx("ansh-dropzone__file", entry.error && "ansh-dropzone__file--error")
2568
+ },
2569
+ [
2570
+ entry.preview ? (
2571
+ // Decorative: the file name beside it is the accessible content.
2572
+ h("img", { src: entry.preview, alt: "", class: "ansh-dropzone__thumb" })
2573
+ ) : h(
2574
+ "span",
2575
+ {
2576
+ class: "ansh-dropzone__thumb ansh-dropzone__thumb--generic",
2577
+ "aria-hidden": "true"
2578
+ },
2579
+ entry.file.name.split(".").pop()?.slice(0, 4).toUpperCase()
2580
+ ),
2581
+ h("span", { class: "ansh-dropzone__meta" }, [
2582
+ h("span", { class: "ansh-dropzone__name" }, entry.file.name),
2583
+ h(
2584
+ "span",
2585
+ { class: "ansh-dropzone__size" },
2586
+ entry.error ?? formatBytes(entry.file.size)
2587
+ )
2588
+ ]),
2589
+ h(
2590
+ "button",
2591
+ {
2592
+ type: "button",
2593
+ class: "ansh-dropzone__remove ansh-focusable",
2594
+ "aria-label": `Remove ${entry.file.name}`,
2595
+ onClick: () => remove(entry.id)
2596
+ },
2597
+ [
2598
+ h(
2599
+ "svg",
2600
+ {
2601
+ viewBox: "0 0 16 16",
2602
+ width: "12",
2603
+ height: "12",
2604
+ "aria-hidden": "true"
2605
+ },
2606
+ [
2607
+ h("path", {
2608
+ d: "M4 4l8 8M12 4l-8 8",
2609
+ stroke: "currentColor",
2610
+ "stroke-width": "1.75",
2611
+ "stroke-linecap": "round",
2612
+ fill: "none"
2613
+ })
2614
+ ]
2615
+ )
2616
+ ]
2617
+ )
2618
+ ]
2619
+ )
2620
+ )
2621
+ ) : null,
2622
+ // Rejections are announced. A file silently missing from the list after a drop is the
2623
+ // single most confusing failure mode this component has.
2624
+ h(
2625
+ "span",
2626
+ { class: "ansh-visually-hidden", role: "status", "aria-live": "polite" },
2627
+ rejected.length > 0 ? `${rejected.length} file${rejected.length === 1 ? "" : "s"} rejected` : files.value.length > 0 ? `${files.value.length} file${files.value.length === 1 ? "" : "s"} ready` : ""
2628
+ )
2629
+ ]);
2630
+ };
2631
+ }
2632
+ });
2633
+ var ColorPicker = defineComponent({
2634
+ name: "AnshColorPicker",
2635
+ props: {
2636
+ modelValue: { type: String, default: void 0 },
2637
+ defaultValue: { type: String, default: "#0275f4" },
2638
+ format: { type: String, default: "hex" },
2639
+ alpha: { type: Boolean, default: false },
2640
+ presets: { type: Array, default: void 0 },
2641
+ contrastAgainst: { type: String, default: void 0 },
2642
+ disabled: { type: Boolean, default: false },
2643
+ label: { type: String, default: "Colour" }
2644
+ },
2645
+ emits: ["update:modelValue"],
2646
+ setup(props, { emit, attrs }) {
2647
+ const [current, setCurrent] = useControllableState(
2648
+ () => props.modelValue,
2649
+ props.defaultValue,
2650
+ (next) => emit("update:modelValue", next)
2651
+ );
2652
+ const open = ref(false);
2653
+ const triggerRef = ref(null);
2654
+ const panelRef = ref(null);
2655
+ const planeRef = ref(null);
2656
+ const id = useId("colorpicker");
2657
+ let hueMemory = rgbToHsv(tryParseColor(current.value) ?? parseColor(props.defaultValue)).h;
2658
+ const hsv = computed(() => {
2659
+ const rgb = tryParseColor(current.value) ?? parseColor(props.defaultValue);
2660
+ const parsed = rgbToHsv(rgb);
2661
+ if (parsed.s > 1e-3 && parsed.v > 1e-3) hueMemory = parsed.h;
2662
+ return { ...parsed, h: parsed.s < 1e-3 || parsed.v < 1e-3 ? hueMemory : parsed.h };
2663
+ });
2664
+ const commitHsv = (next) => {
2665
+ const rgb = hsvToRgb(next);
2666
+ switch (props.format) {
2667
+ case "rgb":
2668
+ setCurrent(
2669
+ next.a < 1 ? `rgba(${Math.round(rgb.r * 255)}, ${Math.round(rgb.g * 255)}, ${Math.round(rgb.b * 255)}, ${Number(next.a.toFixed(2))})` : `rgb(${Math.round(rgb.r * 255)}, ${Math.round(rgb.g * 255)}, ${Math.round(rgb.b * 255)})`
2670
+ );
2671
+ break;
2672
+ case "hsl":
2673
+ setCurrent(
2674
+ `hsl(${Math.round(next.h)} ${Math.round(next.s * 100)}% ${Math.round(next.v * (1 - next.s / 2) * 100)}%${next.a < 1 ? ` / ${Number(next.a.toFixed(2))}` : ""})`
2675
+ );
2676
+ break;
2677
+ case "oklch":
2678
+ setCurrent(toOklchString(rgb));
2679
+ break;
2680
+ default:
2681
+ setCurrent(toHex(rgb, next.a < 1));
2682
+ }
2683
+ };
2684
+ const { position } = useAnchoredPosition(triggerRef, panelRef, open, {
2685
+ placement: "bottom-start",
2686
+ offset: 8
2687
+ });
2688
+ useDismiss(
2689
+ panelRef,
2690
+ open,
2691
+ () => {
2692
+ open.value = false;
2693
+ },
2694
+ { ignore: [triggerRef] }
2695
+ );
2696
+ const dragPlane = (event) => {
2697
+ if (props.disabled) return;
2698
+ const plane = planeRef.value;
2699
+ if (!plane) return;
2700
+ event.currentTarget.setPointerCapture(event.pointerId);
2701
+ const apply = (clientX, clientY) => {
2702
+ const rect = plane.getBoundingClientRect();
2703
+ const s = Math.min(Math.max((clientX - rect.left) / rect.width, 0), 1);
2704
+ const v = 1 - Math.min(Math.max((clientY - rect.top) / rect.height, 0), 1);
2705
+ commitHsv({ ...hsv.value, s, v });
2706
+ };
2707
+ apply(event.clientX, event.clientY);
2708
+ const onMove = (moveEvent) => apply(moveEvent.clientX, moveEvent.clientY);
2709
+ const onUp = () => {
2710
+ window.removeEventListener("pointermove", onMove);
2711
+ window.removeEventListener("pointerup", onUp);
2712
+ };
2713
+ window.addEventListener("pointermove", onMove);
2714
+ window.addEventListener("pointerup", onUp);
2715
+ };
2716
+ const onPlaneKeyDown = (event) => {
2717
+ const step = event.shiftKey ? 0.1 : 0.02;
2718
+ let { s, v } = hsv.value;
2719
+ switch (event.key) {
2720
+ case "ArrowRight":
2721
+ s = Math.min(1, s + step);
2722
+ break;
2723
+ case "ArrowLeft":
2724
+ s = Math.max(0, s - step);
2725
+ break;
2726
+ case "ArrowUp":
2727
+ v = Math.min(1, v + step);
2728
+ break;
2729
+ case "ArrowDown":
2730
+ v = Math.max(0, v - step);
2731
+ break;
2732
+ default:
2733
+ return;
2734
+ }
2735
+ event.preventDefault();
2736
+ commitHsv({ ...hsv.value, s, v });
2737
+ };
2738
+ return () => {
2739
+ const swatch = toHex(hsvToRgb(hsv.value), hsv.value.a < 1);
2740
+ const pureHue = toHex(hsvToRgb({ h: hsv.value.h, s: 1, v: 1, a: 1 }));
2741
+ const contrast = props.contrastAgainst ? contrastRatio(hsvToRgb(hsv.value), parseColor(props.contrastAgainst)) : null;
2742
+ return h("div", { class: cx("ansh-colorpicker", attrs.class) }, [
2743
+ h(
2744
+ "button",
2745
+ {
2746
+ ref: triggerRef,
2747
+ type: "button",
2748
+ class: "ansh-colorpicker__trigger ansh-focusable",
2749
+ disabled: props.disabled,
2750
+ "aria-haspopup": "dialog",
2751
+ "aria-expanded": open.value,
2752
+ "aria-label": `${props.label}: ${swatch}`,
2753
+ onClick: () => {
2754
+ open.value = !open.value;
2755
+ }
2756
+ },
2757
+ [
2758
+ h("span", {
2759
+ class: "ansh-colorpicker__swatch",
2760
+ style: { background: swatch },
2761
+ "aria-hidden": "true"
2762
+ }),
2763
+ h("span", { class: "ansh-colorpicker__value" }, current.value)
2764
+ ]
2765
+ ),
2766
+ open.value ? h(Teleport, { to: "body" }, [
2767
+ h(
2768
+ "div",
2769
+ {
2770
+ ref: panelRef,
2771
+ role: "dialog",
2772
+ "aria-label": props.label,
2773
+ class: "ansh-colorpicker__panel",
2774
+ style: position.value.style
2775
+ },
2776
+ [
2777
+ h(
2778
+ "div",
2779
+ {
2780
+ ref: planeRef,
2781
+ class: "ansh-colorpicker__plane",
2782
+ style: { "--ansh-hue": pureHue },
2783
+ onPointerdown: dragPlane
2784
+ },
2785
+ [
2786
+ h("div", {
2787
+ role: "slider",
2788
+ tabindex: "0",
2789
+ class: "ansh-colorpicker__knob ansh-focusable",
2790
+ style: {
2791
+ left: `${hsv.value.s * 100}%`,
2792
+ top: `${(1 - hsv.value.v) * 100}%`,
2793
+ background: swatch
2794
+ },
2795
+ "aria-label": "Saturation and brightness",
2796
+ "aria-valuetext": `Saturation ${Math.round(hsv.value.s * 100)}%, brightness ${Math.round(hsv.value.v * 100)}%`,
2797
+ "aria-valuenow": Math.round(hsv.value.s * 100),
2798
+ "aria-valuemin": 0,
2799
+ "aria-valuemax": 100,
2800
+ onKeydown: onPlaneKeyDown
2801
+ })
2802
+ ]
2803
+ ),
2804
+ h("div", { class: "ansh-colorpicker__rails" }, [
2805
+ h("div", {
2806
+ class: "ansh-colorpicker__preview",
2807
+ style: { background: swatch },
2808
+ "aria-hidden": "true"
2809
+ }),
2810
+ h("div", { class: "ansh-colorpicker__rail-group" }, [
2811
+ h("input", {
2812
+ type: "range",
2813
+ class: "ansh-colorpicker__rail ansh-colorpicker__rail--hue ansh-focusable",
2814
+ min: 0,
2815
+ max: 360,
2816
+ step: 1,
2817
+ value: Math.round(hsv.value.h),
2818
+ "aria-label": "Hue",
2819
+ "aria-valuetext": `${Math.round(hsv.value.h)} degrees`,
2820
+ onInput: (event) => {
2821
+ commitHsv({
2822
+ ...hsv.value,
2823
+ h: Number(event.target.value)
2824
+ });
2825
+ }
2826
+ }),
2827
+ props.alpha ? h("input", {
2828
+ type: "range",
2829
+ class: "ansh-colorpicker__rail ansh-colorpicker__rail--alpha ansh-focusable",
2830
+ style: { "--ansh-solid": toHex(hsvToRgb({ ...hsv.value, a: 1 })) },
2831
+ min: 0,
2832
+ max: 100,
2833
+ step: 1,
2834
+ value: Math.round(hsv.value.a * 100),
2835
+ "aria-label": "Opacity",
2836
+ "aria-valuetext": `${Math.round(hsv.value.a * 100)} percent`,
2837
+ onInput: (event) => {
2838
+ commitHsv({
2839
+ ...hsv.value,
2840
+ a: Number(event.target.value) / 100
2841
+ });
2842
+ }
2843
+ }) : null
2844
+ ])
2845
+ ]),
2846
+ h("div", { class: "ansh-colorpicker__row" }, [
2847
+ h("input", {
2848
+ type: "text",
2849
+ class: "ansh-input ansh-input--sm ansh-colorpicker__text",
2850
+ value: current.value,
2851
+ "aria-label": `${props.label} value`,
2852
+ spellcheck: false,
2853
+ onInput: (event) => {
2854
+ const text = event.target.value;
2855
+ setCurrent(text);
2856
+ const rgb = tryParseColor(text);
2857
+ if (rgb) hueMemory = rgbToHsv(rgb).h;
2858
+ }
2859
+ })
2860
+ ]),
2861
+ contrast !== null ? h(
2862
+ "p",
2863
+ {
2864
+ class: cx(
2865
+ "ansh-colorpicker__contrast",
2866
+ contrast >= 4.5 && "ansh-colorpicker__contrast--pass",
2867
+ contrast < 3 && "ansh-colorpicker__contrast--fail"
2868
+ ),
2869
+ "aria-live": "polite"
2870
+ },
2871
+ `${contrast.toFixed(2)}:1 against ${props.contrastAgainst} \u2014 ${contrast >= 7 ? "AAA" : contrast >= 4.5 ? "AA" : contrast >= 3 ? "AA large only" : "fails"}`
2872
+ ) : null,
2873
+ props.presets && props.presets.length > 0 ? h(
2874
+ "div",
2875
+ {
2876
+ class: "ansh-colorpicker__presets",
2877
+ role: "group",
2878
+ "aria-label": "Preset colours"
2879
+ },
2880
+ props.presets.map(
2881
+ (preset) => h("button", {
2882
+ key: preset,
2883
+ type: "button",
2884
+ class: cx(
2885
+ "ansh-colorpicker__preset",
2886
+ preset.toLowerCase() === swatch.toLowerCase() && "ansh-colorpicker__preset--on"
2887
+ ),
2888
+ style: { background: preset },
2889
+ "aria-label": preset,
2890
+ "aria-pressed": preset.toLowerCase() === swatch.toLowerCase(),
2891
+ onClick: () => {
2892
+ const rgb = tryParseColor(preset);
2893
+ if (rgb) {
2894
+ hueMemory = rgbToHsv(rgb).h;
2895
+ setCurrent(preset);
2896
+ }
2897
+ }
2898
+ })
2899
+ )
2900
+ ) : null,
2901
+ h(
2902
+ "span",
2903
+ { class: "ansh-visually-hidden", "aria-live": "polite", id: `${id}-live` },
2904
+ swatch
2905
+ )
2906
+ ]
2907
+ )
2908
+ ]) : null
2909
+ ]);
2910
+ };
2911
+ }
2912
+ });
2913
+ function startOfDay(date) {
2914
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate());
2915
+ }
2916
+ function addDays(date, days) {
2917
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days);
2918
+ }
2919
+ function addMonths(date, months) {
2920
+ const target = new Date(date.getFullYear(), date.getMonth() + months, 1);
2921
+ const lastDay = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();
2922
+ return new Date(target.getFullYear(), target.getMonth(), Math.min(date.getDate(), lastDay));
2923
+ }
2924
+ function isSameDay(a, b) {
2925
+ if (!a || !b) return false;
2926
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
2927
+ }
2928
+ function defaultWeekStart(locale) {
2929
+ const region = (locale ?? (typeof navigator !== "undefined" ? navigator.language : "en-GB")).split("-")[1]?.toUpperCase();
2930
+ return region && ["US", "CA", "JP", "IL", "BR", "MX", "PH", "KR"].includes(region) ? 0 : 1;
2931
+ }
2932
+ function isoWeek(date) {
2933
+ const target = new Date(date.getFullYear(), date.getMonth(), date.getDate());
2934
+ target.setDate(target.getDate() + 3 - (target.getDay() + 6) % 7);
2935
+ const firstThursday = new Date(target.getFullYear(), 0, 4);
2936
+ firstThursday.setDate(firstThursday.getDate() + 3 - (firstThursday.getDay() + 6) % 7);
2937
+ return 1 + Math.round((target.getTime() - firstThursday.getTime()) / (7 * 864e5));
2938
+ }
2939
+ var Calendar = defineComponent({
2940
+ name: "AnshCalendar",
2941
+ props: {
2942
+ mode: { type: String, default: "single" },
2943
+ modelValue: { type: [Date, Object], default: void 0 },
2944
+ defaultValue: { type: [Date, Object], default: void 0 },
2945
+ min: { type: Date, default: void 0 },
2946
+ max: { type: Date, default: void 0 },
2947
+ isDateDisabled: { type: Function, default: void 0 },
2948
+ weekStartsOn: {
2949
+ type: Number,
2950
+ default: void 0
2951
+ },
2952
+ locale: { type: String, default: void 0 },
2953
+ months: { type: Number, default: 1 },
2954
+ showWeekNumbers: { type: Boolean, default: false }
2955
+ },
2956
+ emits: ["update:modelValue"],
2957
+ setup(props, { emit, attrs }) {
2958
+ const initialDefault = props.defaultValue ?? (props.mode === "range" ? { start: null, end: null } : null);
2959
+ const [selection, setSelection] = useControllableState(
2960
+ () => props.modelValue,
2961
+ initialDefault,
2962
+ (next) => emit("update:modelValue", next)
2963
+ );
2964
+ const singleValue = () => props.mode === "single" ? selection.value : null;
2965
+ const rangeValue = () => props.mode === "range" ? selection.value : { start: null, end: null };
2966
+ const today = startOfDay(/* @__PURE__ */ new Date());
2967
+ const initialAnchor = startOfDay(singleValue() ?? rangeValue().start ?? /* @__PURE__ */ new Date());
2968
+ const viewMonth = ref(initialAnchor);
2969
+ const cursor = ref(initialAnchor);
2970
+ const hovered = ref(null);
2971
+ const gridRef = ref(null);
2972
+ let shouldFocus = false;
2973
+ const firstWeekday = () => props.weekStartsOn ?? defaultWeekStart(props.locale);
2974
+ const dayNames = () => {
2975
+ const formatter = new Intl.DateTimeFormat(props.locale, { weekday: "short" });
2976
+ const longFormatter = new Intl.DateTimeFormat(props.locale, { weekday: "long" });
2977
+ const start = firstWeekday();
2978
+ return Array.from({ length: 7 }, (_, i) => {
2979
+ const date = new Date(2024, 0, 7 + (start + i) % 7);
2980
+ return { short: formatter.format(date), long: longFormatter.format(date) };
2981
+ });
2982
+ };
2983
+ const monthLabel = () => new Intl.DateTimeFormat(props.locale, { month: "long", year: "numeric" });
2984
+ const fullDate = () => new Intl.DateTimeFormat(props.locale, {
2985
+ weekday: "long",
2986
+ day: "numeric",
2987
+ month: "long",
2988
+ year: "numeric"
2989
+ });
2990
+ const isDisabled = (date) => {
2991
+ if (props.min && date < startOfDay(props.min)) return true;
2992
+ if (props.max && date > startOfDay(props.max)) return true;
2993
+ return props.isDateDisabled?.(date) ?? false;
2994
+ };
2995
+ const moveCursor = (next) => {
2996
+ shouldFocus = true;
2997
+ cursor.value = next;
2998
+ const firstVisible = new Date(viewMonth.value.getFullYear(), viewMonth.value.getMonth(), 1);
2999
+ const lastVisible = new Date(
3000
+ viewMonth.value.getFullYear(),
3001
+ viewMonth.value.getMonth() + props.months,
3002
+ 0
3003
+ );
3004
+ if (next < firstVisible || next > lastVisible) viewMonth.value = next;
3005
+ };
3006
+ watch(
3007
+ [cursor, viewMonth],
3008
+ () => {
3009
+ if (!shouldFocus) return;
3010
+ shouldFocus = false;
3011
+ gridRef.value?.querySelector('[data-ansh-day][tabindex="0"]')?.focus({ preventScroll: true });
3012
+ },
3013
+ { flush: "post" }
3014
+ );
3015
+ const commit = (date) => {
3016
+ if (isDisabled(date)) return;
3017
+ if (props.mode === "single") {
3018
+ setSelection(date);
3019
+ return;
3020
+ }
3021
+ const range = rangeValue();
3022
+ if (!range.start || range.end || date < range.start) {
3023
+ setSelection({ start: date, end: null });
3024
+ } else {
3025
+ setSelection({ start: range.start, end: date });
3026
+ }
3027
+ };
3028
+ const onKeyDown = (event) => {
3029
+ let next = null;
3030
+ switch (event.key) {
3031
+ case "ArrowLeft":
3032
+ next = addDays(cursor.value, -1);
3033
+ break;
3034
+ case "ArrowRight":
3035
+ next = addDays(cursor.value, 1);
3036
+ break;
3037
+ case "ArrowUp":
3038
+ next = addDays(cursor.value, -7);
3039
+ break;
3040
+ case "ArrowDown":
3041
+ next = addDays(cursor.value, 7);
3042
+ break;
3043
+ case "Home":
3044
+ next = addDays(cursor.value, -((cursor.value.getDay() - firstWeekday() + 7) % 7));
3045
+ break;
3046
+ case "End":
3047
+ next = addDays(cursor.value, 6 - (cursor.value.getDay() - firstWeekday() + 7) % 7);
3048
+ break;
3049
+ case "PageUp":
3050
+ next = addMonths(cursor.value, event.shiftKey ? -12 : -1);
3051
+ break;
3052
+ case "PageDown":
3053
+ next = addMonths(cursor.value, event.shiftKey ? 12 : 1);
3054
+ break;
3055
+ case "Enter":
3056
+ case " ":
3057
+ event.preventDefault();
3058
+ commit(cursor.value);
3059
+ return;
3060
+ default:
3061
+ return;
3062
+ }
3063
+ event.preventDefault();
3064
+ moveCursor(next);
3065
+ };
3066
+ const inRange = (date) => {
3067
+ if (props.mode !== "range") return false;
3068
+ const range = rangeValue();
3069
+ if (!range.start) return false;
3070
+ const end = range.end ?? hovered.value;
3071
+ if (!end) return false;
3072
+ const [low, high] = range.start <= end ? [range.start, end] : [end, range.start];
3073
+ return date > low && date < high;
3074
+ };
3075
+ const renderMonth = (offset) => {
3076
+ const monthDate = new Date(
3077
+ viewMonth.value.getFullYear(),
3078
+ viewMonth.value.getMonth() + offset,
3079
+ 1
3080
+ );
3081
+ const firstOfMonth = new Date(monthDate.getFullYear(), monthDate.getMonth(), 1);
3082
+ const leading = (firstOfMonth.getDay() - firstWeekday() + 7) % 7;
3083
+ const gridStart = addDays(firstOfMonth, -leading);
3084
+ const days = Array.from({ length: 42 }, (_, i) => addDays(gridStart, i));
3085
+ const weeks = Array.from({ length: 6 }, (_, w) => days.slice(w * 7, w * 7 + 7));
3086
+ const label = monthLabel();
3087
+ const fullDateFormatter = fullDate();
3088
+ const names = dayNames();
3089
+ const single = singleValue();
3090
+ const range = rangeValue();
3091
+ return h("div", { class: "ansh-calendar__month", key: offset }, [
3092
+ h(
3093
+ "div",
3094
+ { class: "ansh-calendar__caption", "aria-hidden": "true" },
3095
+ label.format(monthDate)
3096
+ ),
3097
+ h(
3098
+ "table",
3099
+ { class: "ansh-calendar__grid", role: "grid", "aria-label": label.format(monthDate) },
3100
+ [
3101
+ h("thead", {}, [
3102
+ h("tr", {}, [
3103
+ props.showWeekNumbers ? h("th", { scope: "col", class: "ansh-calendar__weekno-head" }, [
3104
+ h("span", { class: "ansh-visually-hidden" }, "Week")
3105
+ ]) : null,
3106
+ ...names.map(
3107
+ (day) => h(
3108
+ "th",
3109
+ {
3110
+ key: day.long,
3111
+ scope: "col",
3112
+ class: "ansh-calendar__weekday",
3113
+ abbr: day.long
3114
+ },
3115
+ [
3116
+ h("span", { "aria-hidden": "true" }, day.short),
3117
+ h("span", { class: "ansh-visually-hidden" }, day.long)
3118
+ ]
3119
+ )
3120
+ )
3121
+ ])
3122
+ ]),
3123
+ h(
3124
+ "tbody",
3125
+ {},
3126
+ weeks.map(
3127
+ (week) => h("tr", { key: week[0].toISOString() }, [
3128
+ props.showWeekNumbers ? h("td", { class: "ansh-calendar__weekno" }, isoWeek(week[0])) : null,
3129
+ ...week.map((date) => {
3130
+ const outside = date.getMonth() !== monthDate.getMonth();
3131
+ const dateDisabled = isDisabled(date);
3132
+ const isSelected = props.mode === "single" ? isSameDay(date, single) : isSameDay(date, range.start) || isSameDay(date, range.end);
3133
+ const isCursor = isSameDay(date, cursor.value);
3134
+ return h(
3135
+ "td",
3136
+ { key: date.toISOString(), class: "ansh-calendar__cell", role: "gridcell" },
3137
+ [
3138
+ h(
3139
+ "button",
3140
+ {
3141
+ type: "button",
3142
+ "data-ansh-day": "",
3143
+ class: cx(
3144
+ "ansh-calendar__day",
3145
+ outside && "ansh-calendar__day--outside",
3146
+ isSelected && "ansh-calendar__day--selected",
3147
+ inRange(date) && "ansh-calendar__day--in-range",
3148
+ isSameDay(date, today) && "ansh-calendar__day--today"
3149
+ ),
3150
+ // Roving tabindex: exactly one day is a tab stop, so Tab enters and
3151
+ // leaves the calendar rather than walking 42 buttons.
3152
+ tabindex: isCursor ? "0" : "-1",
3153
+ disabled: dateDisabled,
3154
+ "aria-selected": isSelected,
3155
+ "aria-current": isSameDay(date, today) ? "date" : void 0,
3156
+ // The full date is the accessible name; the visible number alone would
3157
+ // be announced as a bare "14".
3158
+ "aria-label": fullDateFormatter.format(date),
3159
+ onClick: () => {
3160
+ cursor.value = date;
3161
+ commit(date);
3162
+ },
3163
+ onMouseenter: () => {
3164
+ if (props.mode === "range") hovered.value = date;
3165
+ },
3166
+ onFocus: () => {
3167
+ cursor.value = date;
3168
+ }
3169
+ },
3170
+ String(date.getDate())
3171
+ )
3172
+ ]
3173
+ );
3174
+ })
3175
+ ])
3176
+ )
3177
+ )
3178
+ ]
3179
+ )
3180
+ ]);
3181
+ };
3182
+ return () => h(
3183
+ "div",
3184
+ {
3185
+ class: cx(
3186
+ "ansh-calendar",
3187
+ props.months > 1 && "ansh-calendar--multi",
3188
+ attrs.class
3189
+ ),
3190
+ onKeydown: onKeyDown,
3191
+ onMouseleave: () => {
3192
+ hovered.value = null;
3193
+ }
3194
+ },
3195
+ [
3196
+ h("div", { class: "ansh-calendar__header" }, [
3197
+ h(
3198
+ "button",
3199
+ {
3200
+ type: "button",
3201
+ class: "ansh-calendar__nav ansh-focusable",
3202
+ "aria-label": "Previous month",
3203
+ onClick: () => {
3204
+ viewMonth.value = addMonths(viewMonth.value, -1);
3205
+ }
3206
+ },
3207
+ [
3208
+ h(
3209
+ "svg",
3210
+ { viewBox: "0 0 12 12", width: "14", height: "14", "aria-hidden": "true" },
3211
+ [
3212
+ h("path", {
3213
+ d: "M7.5 2.5L4 6l3.5 3.5",
3214
+ stroke: "currentColor",
3215
+ "stroke-width": "1.5",
3216
+ fill: "none",
3217
+ "stroke-linecap": "round",
3218
+ "stroke-linejoin": "round"
3219
+ })
3220
+ ]
3221
+ )
3222
+ ]
3223
+ ),
3224
+ // One live region for the whole calendar. Per-month headings marked `aria-hidden`
3225
+ // above, so a month change is announced once rather than twice in a two-month view.
3226
+ h(
3227
+ "div",
3228
+ { class: "ansh-calendar__title", "aria-live": "polite" },
3229
+ props.months > 1 ? `${monthLabel().format(viewMonth.value)} \u2013 ${monthLabel().format(new Date(viewMonth.value.getFullYear(), viewMonth.value.getMonth() + props.months - 1, 1))}` : monthLabel().format(viewMonth.value)
3230
+ ),
3231
+ h(
3232
+ "button",
3233
+ {
3234
+ type: "button",
3235
+ class: "ansh-calendar__nav ansh-focusable",
3236
+ "aria-label": "Next month",
3237
+ onClick: () => {
3238
+ viewMonth.value = addMonths(viewMonth.value, 1);
3239
+ }
3240
+ },
3241
+ [
3242
+ h(
3243
+ "svg",
3244
+ { viewBox: "0 0 12 12", width: "14", height: "14", "aria-hidden": "true" },
3245
+ [
3246
+ h("path", {
3247
+ d: "M4.5 2.5L8 6l-3.5 3.5",
3248
+ stroke: "currentColor",
3249
+ "stroke-width": "1.5",
3250
+ fill: "none",
3251
+ "stroke-linecap": "round",
3252
+ "stroke-linejoin": "round"
3253
+ })
3254
+ ]
3255
+ )
3256
+ ]
3257
+ )
3258
+ ]),
3259
+ h(
3260
+ "div",
3261
+ { class: "ansh-calendar__months", ref: gridRef },
3262
+ Array.from({ length: props.months }, (_, index) => renderMonth(index))
3263
+ )
3264
+ ]
3265
+ );
3266
+ }
3267
+ });
3268
+ var calendarForwardProps = {
3269
+ min: { type: Date, default: void 0 },
3270
+ max: { type: Date, default: void 0 },
3271
+ isDateDisabled: { type: Function, default: void 0 },
3272
+ weekStartsOn: {
3273
+ type: Number,
3274
+ default: void 0
3275
+ },
3276
+ locale: { type: String, default: void 0 },
3277
+ showWeekNumbers: { type: Boolean, default: false }
3278
+ };
3279
+ var DatePicker = defineComponent({
3280
+ name: "AnshDatePicker",
3281
+ inheritAttrs: false,
3282
+ props: {
3283
+ modelValue: { type: Date, default: void 0 },
3284
+ defaultValue: { type: Date, default: void 0 },
3285
+ placeholder: { type: String, default: void 0 },
3286
+ disabled: { type: Boolean, default: false },
3287
+ invalid: { type: Boolean, default: false },
3288
+ clearable: { type: Boolean, default: true },
3289
+ format: { type: Function, default: void 0 },
3290
+ label: { type: String, default: "Date" },
3291
+ name: { type: String, default: void 0 },
3292
+ months: { type: Number, default: void 0 },
3293
+ ...calendarForwardProps
3294
+ },
3295
+ emits: ["update:modelValue"],
3296
+ setup(props, { emit, attrs }) {
3297
+ const fieldProps = useFieldControl();
3298
+ const [selected, setSelected] = useControllableState(
3299
+ () => props.modelValue,
3300
+ props.defaultValue ?? null,
3301
+ (next) => emit("update:modelValue", next)
3302
+ );
3303
+ const open = ref(false);
3304
+ const text = ref("");
3305
+ const editing = ref(false);
3306
+ const anchorRef = ref(null);
3307
+ const panelRef = ref(null);
3308
+ const inputRef = ref(null);
3309
+ const formatter = computed(
3310
+ () => new Intl.DateTimeFormat(props.locale, {
3311
+ day: "2-digit",
3312
+ month: "2-digit",
3313
+ year: "numeric"
3314
+ })
3315
+ );
3316
+ const order = computed(() => {
3317
+ const parts = formatter.value.formatToParts(new Date(2026, 2, 14));
3318
+ return parts.filter(
3319
+ (p) => ["day", "month", "year"].includes(p.type)
3320
+ ).map((p) => p.type);
3321
+ });
3322
+ const display = computed(() => {
3323
+ const sel = selected.value;
3324
+ if (props.format) return sel ? props.format(sel) : "";
3325
+ return sel ? formatter.value.format(sel) : "";
3326
+ });
3327
+ const parse = (raw) => {
3328
+ const numbers = raw.split(/\D+/).filter(Boolean).map(Number);
3329
+ if (numbers.length < 3) return null;
3330
+ const parts = {};
3331
+ order.value.forEach((type, index) => {
3332
+ parts[type] = numbers[index];
3333
+ });
3334
+ const { month = 0, day = 0 } = parts;
3335
+ let { year = 0 } = parts;
3336
+ if (year < 100) year += year < 70 ? 2e3 : 1900;
3337
+ const date = new Date(year, month - 1, day);
3338
+ if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
3339
+ return null;
3340
+ }
3341
+ return startOfDay(date);
3342
+ };
3343
+ const commitText = () => {
3344
+ editing.value = false;
3345
+ if (text.value.trim() === "") {
3346
+ setSelected(null);
3347
+ return;
3348
+ }
3349
+ const parsed = parse(text.value);
3350
+ if (parsed) setSelected(parsed);
3351
+ text.value = "";
3352
+ };
3353
+ const { position } = useAnchoredPosition(anchorRef, panelRef, open, {
3354
+ placement: "bottom-start",
3355
+ offset: 6
3356
+ });
3357
+ useDismiss(
3358
+ panelRef,
3359
+ open,
3360
+ () => {
3361
+ open.value = false;
3362
+ },
3363
+ { ignore: [anchorRef] }
3364
+ );
3365
+ const placeholderText = computed(
3366
+ () => props.placeholder ?? order.value.map((type) => type === "year" ? "yyyy" : type === "month" ? "mm" : "dd").join("/")
3367
+ );
3368
+ return () => {
3369
+ const { class: attrClass, ...restAttrs } = attrs;
3370
+ return h("div", { class: cx("ansh-datepicker", attrClass) }, [
3371
+ h(
3372
+ "div",
3373
+ {
3374
+ ref: anchorRef,
3375
+ class: cx(
3376
+ "ansh-input-group",
3377
+ "ansh-focusable-within",
3378
+ (props.invalid || fieldProps.value["aria-invalid"]) && "ansh-input-group--invalid",
3379
+ props.disabled && "ansh-input-group--disabled"
3380
+ )
3381
+ },
3382
+ [
3383
+ h("input", {
3384
+ ref: inputRef,
3385
+ type: "text",
3386
+ inputmode: "numeric",
3387
+ class: "ansh-input",
3388
+ value: editing.value ? text.value : display.value,
3389
+ placeholder: placeholderText.value,
3390
+ disabled: props.disabled || fieldProps.value.disabled,
3391
+ "aria-label": props.label,
3392
+ "aria-invalid": props.invalid || void 0,
3393
+ autocomplete: "off",
3394
+ ...fieldProps.value,
3395
+ ...restAttrs,
3396
+ onInput: (event) => {
3397
+ editing.value = true;
3398
+ text.value = event.target.value;
3399
+ },
3400
+ onBlur: commitText,
3401
+ onKeydown: (event) => {
3402
+ if (event.key === "Enter") {
3403
+ event.preventDefault();
3404
+ commitText();
3405
+ open.value = false;
3406
+ } else if (event.key === "ArrowDown" && !open.value) {
3407
+ event.preventDefault();
3408
+ open.value = true;
3409
+ } else if (event.key === "Escape" && open.value) {
3410
+ event.preventDefault();
3411
+ open.value = false;
3412
+ }
3413
+ }
3414
+ }),
3415
+ props.clearable && selected.value && !props.disabled ? h(
3416
+ "button",
3417
+ {
3418
+ type: "button",
3419
+ class: "ansh-input-group__clear ansh-focusable",
3420
+ "aria-label": "Clear date",
3421
+ onClick: () => {
3422
+ setSelected(null);
3423
+ text.value = "";
3424
+ inputRef.value?.focus();
3425
+ }
3426
+ },
3427
+ [
3428
+ h(
3429
+ "svg",
3430
+ { viewBox: "0 0 16 16", width: "12", height: "12", "aria-hidden": "true" },
3431
+ [
3432
+ h("path", {
3433
+ d: "M4 4l8 8M12 4l-8 8",
3434
+ stroke: "currentColor",
3435
+ "stroke-width": "1.75",
3436
+ "stroke-linecap": "round",
3437
+ fill: "none"
3438
+ })
3439
+ ]
3440
+ )
3441
+ ]
3442
+ ) : null,
3443
+ h(
3444
+ "button",
3445
+ {
3446
+ type: "button",
3447
+ class: "ansh-input-group__button ansh-focusable",
3448
+ "aria-label": "Open calendar",
3449
+ "aria-haspopup": "dialog",
3450
+ "aria-expanded": open.value,
3451
+ disabled: props.disabled,
3452
+ onClick: () => {
3453
+ open.value = !open.value;
3454
+ }
3455
+ },
3456
+ [
3457
+ h(
3458
+ "svg",
3459
+ {
3460
+ viewBox: "0 0 20 20",
3461
+ width: "16",
3462
+ height: "16",
3463
+ fill: "none",
3464
+ stroke: "currentColor",
3465
+ "stroke-width": "1.5",
3466
+ "aria-hidden": "true"
3467
+ },
3468
+ [
3469
+ h("rect", { x: "3", y: "4.5", width: "14", height: "13", rx: "2.5" }),
3470
+ h("path", { d: "M3 8.5h14M7 2.5v3M13 2.5v3", "stroke-linecap": "round" })
3471
+ ]
3472
+ )
3473
+ ]
3474
+ )
3475
+ ]
3476
+ ),
3477
+ props.name ? (
3478
+ // ISO in the hidden field. A server parsing "14/03/2026" has to guess the locale; an
3479
+ // ISO date is unambiguous everywhere.
3480
+ h("input", {
3481
+ type: "hidden",
3482
+ name: props.name,
3483
+ value: selected.value ? `${selected.value.getFullYear()}-${String(selected.value.getMonth() + 1).padStart(2, "0")}-${String(selected.value.getDate()).padStart(2, "0")}` : ""
3484
+ })
3485
+ ) : null,
3486
+ open.value ? h(Teleport, { to: "body" }, [
3487
+ h(
3488
+ "div",
3489
+ {
3490
+ ref: panelRef,
3491
+ role: "dialog",
3492
+ "aria-label": props.label,
3493
+ class: "ansh-datepicker__panel",
3494
+ style: position.value.style
3495
+ },
3496
+ [
3497
+ h(Calendar, {
3498
+ mode: "single",
3499
+ modelValue: selected.value,
3500
+ "onUpdate:modelValue": (next) => {
3501
+ setSelected(next);
3502
+ open.value = false;
3503
+ inputRef.value?.focus();
3504
+ },
3505
+ locale: props.locale,
3506
+ min: props.min,
3507
+ max: props.max,
3508
+ isDateDisabled: props.isDateDisabled,
3509
+ weekStartsOn: props.weekStartsOn,
3510
+ months: props.months,
3511
+ showWeekNumbers: props.showWeekNumbers
3512
+ })
3513
+ ]
3514
+ )
3515
+ ]) : null
3516
+ ]);
3517
+ };
3518
+ }
3519
+ });
3520
+ var DateRangePicker = defineComponent({
3521
+ name: "AnshDateRangePicker",
3522
+ inheritAttrs: false,
3523
+ props: {
3524
+ modelValue: { type: Object, default: void 0 },
3525
+ defaultValue: { type: Object, default: void 0 },
3526
+ disabled: { type: Boolean, default: false },
3527
+ invalid: { type: Boolean, default: false },
3528
+ label: { type: String, default: "Date range" },
3529
+ months: { type: Number, default: 2 },
3530
+ ...calendarForwardProps
3531
+ },
3532
+ emits: ["update:modelValue"],
3533
+ setup(props, { emit, attrs }) {
3534
+ const [range, setRange] = useControllableState(
3535
+ () => props.modelValue,
3536
+ props.defaultValue ?? { start: null, end: null },
3537
+ (next) => emit("update:modelValue", next)
3538
+ );
3539
+ const open = ref(false);
3540
+ const anchorRef = ref(null);
3541
+ const panelRef = ref(null);
3542
+ const formatter = computed(
3543
+ () => new Intl.DateTimeFormat(props.locale, { day: "numeric", month: "short", year: "numeric" })
3544
+ );
3545
+ const { position } = useAnchoredPosition(anchorRef, panelRef, open, {
3546
+ placement: "bottom-start",
3547
+ offset: 6
3548
+ });
3549
+ useDismiss(
3550
+ panelRef,
3551
+ open,
3552
+ () => {
3553
+ open.value = false;
3554
+ },
3555
+ { ignore: [anchorRef] }
3556
+ );
3557
+ const summary = computed(() => {
3558
+ const r = range.value;
3559
+ if (r.start && r.end)
3560
+ return `${formatter.value.format(r.start)} \u2013 ${formatter.value.format(r.end)}`;
3561
+ if (r.start) return `${formatter.value.format(r.start)} \u2013 \u2026`;
3562
+ return "Select dates";
3563
+ });
3564
+ return () => {
3565
+ const { class: attrClass, ...restAttrs } = attrs;
3566
+ return h("div", { class: cx("ansh-datepicker", attrClass) }, [
3567
+ h(
3568
+ "button",
3569
+ {
3570
+ ref: anchorRef,
3571
+ type: "button",
3572
+ class: cx(
3573
+ "ansh-select__trigger",
3574
+ "ansh-focusable",
3575
+ open.value && "ansh-select__trigger--open",
3576
+ props.invalid && "ansh-select__trigger--invalid",
3577
+ !range.value.start && "ansh-select__trigger--placeholder"
3578
+ ),
3579
+ disabled: props.disabled,
3580
+ "aria-haspopup": "dialog",
3581
+ "aria-expanded": open.value,
3582
+ "aria-label": `${props.label}: ${summary.value}`,
3583
+ ...restAttrs,
3584
+ onClick: () => {
3585
+ open.value = !open.value;
3586
+ }
3587
+ },
3588
+ [
3589
+ h(
3590
+ "svg",
3591
+ {
3592
+ viewBox: "0 0 20 20",
3593
+ width: "16",
3594
+ height: "16",
3595
+ fill: "none",
3596
+ stroke: "currentColor",
3597
+ "stroke-width": "1.5",
3598
+ "aria-hidden": "true"
3599
+ },
3600
+ [
3601
+ h("rect", { x: "3", y: "4.5", width: "14", height: "13", rx: "2.5" }),
3602
+ h("path", { d: "M3 8.5h14M7 2.5v3M13 2.5v3", "stroke-linecap": "round" })
3603
+ ]
3604
+ ),
3605
+ h("span", { class: "ansh-select__value" }, summary.value)
3606
+ ]
3607
+ ),
3608
+ open.value ? h(Teleport, { to: "body" }, [
3609
+ h(
3610
+ "div",
3611
+ {
3612
+ ref: panelRef,
3613
+ role: "dialog",
3614
+ "aria-label": props.label,
3615
+ class: "ansh-datepicker__panel",
3616
+ style: position.value.style
3617
+ },
3618
+ [
3619
+ h(Calendar, {
3620
+ mode: "range",
3621
+ months: props.months,
3622
+ modelValue: range.value,
3623
+ "onUpdate:modelValue": (next) => {
3624
+ const nextRange = next;
3625
+ setRange(nextRange);
3626
+ if (nextRange.start && nextRange.end) open.value = false;
3627
+ },
3628
+ locale: props.locale,
3629
+ min: props.min,
3630
+ max: props.max,
3631
+ isDateDisabled: props.isDateDisabled,
3632
+ weekStartsOn: props.weekStartsOn,
3633
+ showWeekNumbers: props.showWeekNumbers
3634
+ })
3635
+ ]
3636
+ )
3637
+ ]) : null
3638
+ ]);
3639
+ };
3640
+ }
3641
+ });
3642
+
3643
+ export { Calendar, Checkbox, ChoiceCard, ColorPicker, Combobox, DatePicker, DateRangePicker, Dropzone, Field, Fieldset, Input, NumberInput, OptionList, PinInput, Radio, RadioGroup, RangeSlider, Rating, Select, Slider, Switch, TagsInput, Textarea, VisuallyHidden, addDays, addMonths, filterOptions, firstEnabled, isSameDay, scoreOption, startOfDay, stepActive, useFieldContext, useFieldControl, useTypeahead };
3644
+ //# sourceMappingURL=chunk-SOYRICHU.js.map
3645
+ //# sourceMappingURL=chunk-SOYRICHU.js.map