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