svseeds 0.7.1 → 0.8.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 (55) hide show
  1. package/README.md +1 -1
  2. package/_svseeds/Accordion.svelte +10 -5
  3. package/_svseeds/Accordion.svelte.d.ts +3 -3
  4. package/_svseeds/Calendar.svelte +69 -47
  5. package/_svseeds/Calendar.svelte.d.ts +37 -35
  6. package/_svseeds/CheckField.svelte +19 -15
  7. package/_svseeds/ColorPicker.svelte +8 -4
  8. package/_svseeds/ColorPicker.svelte.d.ts +6 -3
  9. package/_svseeds/ComboBox.svelte +5 -7
  10. package/_svseeds/DarkToggle.svelte +2 -2
  11. package/_svseeds/DateField.svelte +11 -14
  12. package/_svseeds/DateInput.svelte +26 -23
  13. package/_svseeds/DateInput.svelte.d.ts +22 -16
  14. package/_svseeds/Disclosure.svelte +17 -7
  15. package/_svseeds/Drawer.svelte +1 -2
  16. package/_svseeds/Drawer.svelte.d.ts +1 -2
  17. package/_svseeds/FileField.svelte +19 -15
  18. package/_svseeds/FileInput.svelte +3 -11
  19. package/_svseeds/HotkeyCapture.svelte +3 -2
  20. package/_svseeds/HotkeyCapture.svelte.d.ts +3 -0
  21. package/_svseeds/Modal.svelte +5 -3
  22. package/_svseeds/NumberField.svelte +11 -14
  23. package/_svseeds/NumberInput.svelte +39 -53
  24. package/_svseeds/NumberInput.svelte.d.ts +11 -21
  25. package/_svseeds/Pagination.svelte +29 -31
  26. package/_svseeds/Pagination.svelte.d.ts +16 -13
  27. package/_svseeds/Popover.svelte +64 -18
  28. package/_svseeds/Popover.svelte.d.ts +16 -15
  29. package/_svseeds/SelectField.svelte +10 -13
  30. package/_svseeds/Sortable.svelte +43 -8
  31. package/_svseeds/Sortable.svelte.d.ts +8 -3
  32. package/_svseeds/SortableGroup.svelte +8 -5
  33. package/_svseeds/SortableGroup.svelte.d.ts +6 -3
  34. package/_svseeds/Tabs.svelte +22 -19
  35. package/_svseeds/Tabs.svelte.d.ts +8 -5
  36. package/_svseeds/TagsInput.svelte +3 -13
  37. package/_svseeds/TagsInputField.svelte +11 -14
  38. package/_svseeds/TextField.svelte +10 -13
  39. package/_svseeds/Toast.svelte +38 -6
  40. package/_svseeds/Toast.svelte.d.ts +24 -2
  41. package/_svseeds/Toggle.svelte +1 -1
  42. package/_svseeds/ToggleGroup.svelte +39 -41
  43. package/_svseeds/ToggleGroup.svelte.d.ts +13 -10
  44. package/_svseeds/ToggleGroupField.svelte +29 -29
  45. package/_svseeds/ToggleGroupField.svelte.d.ts +12 -12
  46. package/_svseeds/Tooltip.svelte +12 -7
  47. package/_svseeds/Tooltip.svelte.d.ts +7 -7
  48. package/_svseeds/WheelPicker.svelte +21 -13
  49. package/_svseeds/ZSortableA11y.svelte +1082 -0
  50. package/_svseeds/ZSortableA11y.svelte.d.ts +182 -0
  51. package/_svseeds/_core.d.ts +37 -1
  52. package/_svseeds/_core.js +70 -1
  53. package/index.d.ts +10 -9
  54. package/index.js +1 -0
  55. package/package.json +2 -2
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/svseeds)](https://www.npmjs.com/package/svseeds)
4
4
  [![JSR Version](https://img.shields.io/jsr/v/%40svseeds/ui)](https://jsr.io/@svseeds/ui)
5
- [![license](https://img.shields.io/npm/l/svseeds)](LICENSE.md)
5
+ [![license](https://img.shields.io/npm/l/svseeds)](LICENSE)
6
6
 
7
7
  SvSeeds is a collection of essential functional components for Svelte development. These components are intentionally designed with minimal styling, giving you complete freedom to apply your own styles based on your project requirements. For specialized needs, you can download the components as .svelte files through our CLI tool and customize their functionality directly.
8
8
 
@@ -24,7 +24,7 @@
24
24
  value: string; // REQUIRED, unique within `items`. Addresses `current`.
25
25
  label: string | Snippet<[boolean, string]> | AccordionComponent;
26
26
  panel: Snippet<[string]> | AccordionComponent;
27
- disabled?: boolean; // (false)
27
+ inactive?: string | boolean; // (false) reason string OR true; forwarded to Disclosure
28
28
  };
29
29
  ```
30
30
  `value`s must be unique within `items`. Accordion is exclusive: at most one item is open at a time.
@@ -36,7 +36,7 @@
36
36
  {children}
37
37
  {:else}
38
38
  {#each items as item}
39
- <Disclosure id={item.value} label={item.label} inactive={item.disabled || undefined}>
39
+ <Disclosure id={item.value} label={item.label} inactive={item.inactive}>
40
40
  {item.panel}
41
41
  </Disclosure>
42
42
  {/each}
@@ -63,7 +63,7 @@
63
63
  value: string; // REQUIRED, unique within `items`. Addresses `current`.
64
64
  label: string | Snippet<[boolean, string]> | AccordionComponent;
65
65
  panel: Snippet<[string]> | AccordionComponent;
66
- disabled?: boolean; // (false)
66
+ inactive?: string | boolean; // (false) reason string OR true; forwarded to Disclosure
67
67
  };
68
68
 
69
69
  export const _ACCORDION_PRESET = "svs-accordion";
@@ -71,6 +71,9 @@
71
71
  function isComponent(x: unknown): x is AccordionComponent {
72
72
  return typeof x === "object" && x !== null && "component" in x;
73
73
  }
74
+ function isInactive(x?: string | boolean) {
75
+ return x === true || (typeof x === "string" && x.trim().length > 0);
76
+ }
74
77
 
75
78
  import { untrack } from "svelte";
76
79
  import { VARIANT, PARTS, _fnClass, _omit } from "./_core";
@@ -104,7 +107,9 @@
104
107
  },
105
108
  };
106
109
  _setDisclosureContext(ctx);
107
- const selected = $derived(children || !items ? current : items.some((it) => it.value === current && !it.disabled) ? current : undefined);
110
+ const selected = $derived(
111
+ children || !items ? current : items.some((it) => it.value === current && !isInactive(it.inactive)) ? current : undefined,
112
+ );
108
113
 
109
114
  // *** Initialize Child Props *** //
110
115
  const childProps = $derived({
@@ -133,7 +138,7 @@
133
138
  {#snippet panel(variant: string)}
134
139
  {@render panelContent(item.panel, variant)}
135
140
  {/snippet}
136
- <Disclosure id={item.value} {label} children={panel} inactive={item.disabled || undefined} {variant} {...childProps} />
141
+ <Disclosure id={item.value} {label} children={panel} inactive={item.inactive} {variant} {...childProps} />
137
142
  {/each}
138
143
  </div>
139
144
  {/if}
@@ -16,7 +16,7 @@ export type AccordionItem = {
16
16
  value: string;
17
17
  label: string | Snippet<[boolean, string]> | AccordionComponent;
18
18
  panel: Snippet<[string]> | AccordionComponent;
19
- disabled?: boolean;
19
+ inactive?: string | boolean;
20
20
  };
21
21
  export declare const _ACCORDION_PRESET = "svs-accordion";
22
22
  import type { Component, Snippet } from "svelte";
@@ -47,7 +47,7 @@ import type { DisclosureProps, DisclosureReqdProps, DisclosureBindProps } from "
47
47
  * value: string; // REQUIRED, unique within `items`. Addresses `current`.
48
48
  * label: string | Snippet<[boolean, string]> | AccordionComponent;
49
49
  * panel: Snippet<[string]> | AccordionComponent;
50
- * disabled?: boolean; // (false)
50
+ * inactive?: string | boolean; // (false) reason string OR true; forwarded to Disclosure
51
51
  * };
52
52
  * ```
53
53
  * `value`s must be unique within `items`. Accordion is exclusive: at most one item is open at a time.
@@ -59,7 +59,7 @@ import type { DisclosureProps, DisclosureReqdProps, DisclosureBindProps } from "
59
59
  * {children}
60
60
  * {:else}
61
61
  * {#each items as item}
62
- * <Disclosure id={item.value} label={item.label} inactive={item.disabled || undefined}>
62
+ * <Disclosure id={item.value} label={item.label} inactive={item.inactive}>
63
63
  * {item.panel}
64
64
  * </Disclosure>
65
65
  * {/each}
@@ -8,19 +8,6 @@
8
8
  ### Types
9
9
  default value: *`(value)`*
10
10
  ```ts
11
- type DayCtx = {
12
- date: Temporal.PlainDate;
13
- variant: string;
14
- weekday: number;
15
- today: boolean;
16
- selected: boolean;
17
- outside: boolean;
18
- disabled: boolean;
19
- };
20
- type TransitionProp = {
21
- fn?: (node: HTMLElement, params: any, options: { direction: "in" | "out" | "both" }) => import("svelte/transition").TransitionConfig;
22
- params?: unknown;
23
- };
24
11
  interface CalendarProps {
25
12
  value?: Temporal.PlainDate; // bindable; selected date
26
13
  display?: Temporal.PlainYearMonth; // bindable; shown month
@@ -33,8 +20,8 @@
33
20
  firstDayOfWeek?: number; // (0=Sun)
34
21
  locale?: string;
35
22
  label?: Snippet<[Temporal.PlainYearMonth, string, boolean]>;
36
- prev?: Snippet<[string]>;
37
- next?: Snippet<[string]>;
23
+ left?: Snippet<[string]>;
24
+ right?: Snippet<[string]>;
38
25
  weekday?: Snippet<[number, string]>;
39
26
  day?: Snippet<[DayCtx]>;
40
27
  bottom?: Snippet<[string, () => void]>;
@@ -43,18 +30,33 @@
43
30
  styling?: SVSClass;
44
31
  variant?: SVSVariant; // (VARIANT.NEUTRAL)
45
32
  }
33
+ type DayCtx = {
34
+ date: Temporal.PlainDate;
35
+ variant: string;
36
+ weekday: number;
37
+ today: boolean;
38
+ selected: boolean;
39
+ outside: boolean;
40
+ disabled: boolean;
41
+ };
42
+ type TransitionProp = {
43
+ fn?: (node: HTMLElement, params: any, options: { direction: "in" | "out" | "both" }) => import("svelte/transition").TransitionConfig;
44
+ params?: unknown;
45
+ };
46
46
  ```
47
47
  ### Anatomy
48
48
  ```svelte
49
- <div class="whole" role="application">
49
+ <div class="whole" role="group">
50
50
  <div class="top">
51
51
  <button class="left" />
52
52
  <button class="label" />
53
53
  <button class="right" />
54
54
  </div>
55
55
  <div class="middle">
56
- <div class="aux" role="row">weekday headers</div>
57
- <div class="main" role="grid">day cells</div>
56
+ <div class="main" role="grid">
57
+ <div class="aux" role="row">weekday headers</div>
58
+ day cells
59
+ </div>
58
60
  MonthPicker while picking
59
61
  </div>
60
62
  <div class="bottom" conditional>{bottom}</div>
@@ -67,19 +69,6 @@
67
69
  - Arrow, Home/End, PageUp/PageDown, and Shift+PageUp/PageDown update roving focus.
68
70
  -->
69
71
  <script module lang="ts">
70
- export type DayCtx = {
71
- date: Temporal.PlainDate;
72
- variant: string;
73
- weekday: number;
74
- today: boolean;
75
- selected: boolean;
76
- outside: boolean;
77
- disabled: boolean;
78
- };
79
- export type TransitionProp = {
80
- fn?: (node: HTMLElement, params: any, options: { direction: "in" | "out" | "both" }) => import("svelte/transition").TransitionConfig;
81
- params?: unknown;
82
- };
83
72
  export interface CalendarProps {
84
73
  value?: Temporal.PlainDate;
85
74
  display?: Temporal.PlainYearMonth;
@@ -92,8 +81,8 @@
92
81
  firstDayOfWeek?: number;
93
82
  locale?: string;
94
83
  label?: Snippet<[Temporal.PlainYearMonth, string, boolean]>;
95
- prev?: Snippet<[string]>;
96
- next?: Snippet<[string]>;
84
+ left?: Snippet<[string]>;
85
+ right?: Snippet<[string]>;
97
86
  weekday?: Snippet<[number, string]>;
98
87
  day?: Snippet<[DayCtx]>;
99
88
  bottom?: Snippet<[string, () => void]>;
@@ -102,6 +91,19 @@
102
91
  styling?: SVSClass;
103
92
  variant?: SVSVariant;
104
93
  }
94
+ export type DayCtx = {
95
+ date: Temporal.PlainDate;
96
+ variant: string;
97
+ weekday: number;
98
+ today: boolean;
99
+ selected: boolean;
100
+ outside: boolean;
101
+ disabled: boolean;
102
+ };
103
+ export type TransitionProp = {
104
+ fn?: (node: HTMLElement, params: any, options: { direction: "in" | "out" | "both" }) => import("svelte/transition").TransitionConfig;
105
+ params?: unknown;
106
+ };
105
107
  export type CalendarReqdProps = never;
106
108
  export type CalendarBindProps = "value" | "display" | "picking";
107
109
 
@@ -118,7 +120,7 @@
118
120
 
119
121
  <script lang="ts">
120
122
  // prettier-ignore
121
- let { value = $bindable(), display = $bindable(), picking = $bindable(false), min, max, isDisabled, outsideDays = false, fixedWeeks = false, firstDayOfWeek = 0, locale, label, prev, next, weekday, day, bottom, monthPicker, transition, styling, variant = VARIANT.NEUTRAL }: CalendarProps = $props();
123
+ let { value = $bindable(), display = $bindable(), picking = $bindable(false), min, max, isDisabled, outsideDays = false, fixedWeeks = false, firstDayOfWeek = 0, locale, label, left, right, weekday, day, bottom, monthPicker, transition, styling, variant = VARIANT.NEUTRAL }: CalendarProps = $props();
122
124
 
123
125
  // *** Initialize *** //
124
126
  const cls = $derived(_fnClass(_CALENDAR_PRESET, styling));
@@ -131,6 +133,7 @@
131
133
  // *** States *** //
132
134
  const initialDisplay = display as Temporal.PlainYearMonth;
133
135
  let focused = $state(value && sameMonth(value, initialDisplay) ? value : firstOf(initialDisplay));
136
+ let cells = $state<Record<string, HTMLElement>>({});
134
137
  const firstWeekday = $derived(normWeekday(firstDayOfWeek));
135
138
  const monthStart = $derived(firstOf(currentDisplay()));
136
139
  const captionText = $derived(monthStart.toLocaleString(locale, { year: "numeric", month: "long" }));
@@ -151,7 +154,17 @@
151
154
  const start = first.subtract({ days: lead });
152
155
  return Array.from({ length: rows }, (_, r) => Array.from({ length: 7 }, (_, c) => start.add({ days: r * 7 + c })));
153
156
  });
154
- const reduced = $derived(typeof window !== "undefined" && shouldReduceMotion());
157
+ const cellKeys = $derived.by(() => {
158
+ const shown = currentDisplay();
159
+ const keys = new Set<string>();
160
+ for (const week of weeks) {
161
+ for (const cell of week) {
162
+ if (outsideDays || sameMonth(cell, shown)) keys.add(cell.toString());
163
+ }
164
+ }
165
+ return keys;
166
+ });
167
+ const reduced = $derived(shouldReduceMotion());
155
168
  const tfn = $derived(!reduced && transition?.fn ? transition.fn : noop);
156
169
  const tparams = $derived(transition?.params as any);
157
170
 
@@ -161,6 +174,10 @@
161
174
  value;
162
175
  untrack(() => syncFocus());
163
176
  });
177
+ $effect.pre(() => {
178
+ cellKeys;
179
+ untrack(() => trimCells(cellKeys));
180
+ });
164
181
 
165
182
  function syncFocus() {
166
183
  const shown = currentDisplay();
@@ -243,8 +260,12 @@
243
260
  tick().then(() => focusDate(d));
244
261
  }
245
262
  function focusDate(d: Temporal.PlainDate) {
246
- const el = document.querySelector(`[data-svs-calendar="${uid}"] [data-date="${d.toString()}"]`) as HTMLElement | null;
247
- el?.focus();
263
+ cells[d.toString()]?.focus();
264
+ }
265
+ function trimCells(keys: Set<string>) {
266
+ for (const key of Object.keys(cells)) {
267
+ if (!keys.has(key)) delete cells[key];
268
+ }
248
269
  }
249
270
  function weekEdge(toEnd: boolean): Temporal.PlainDate {
250
271
  const offset = (weekdayOf(focused) - firstWeekday + 7) % 7;
@@ -274,16 +295,16 @@
274
295
 
275
296
  <!---------------------------------------->
276
297
 
277
- <div class={cls(PARTS.WHOLE, variant)} role="application" aria-label={appLabel} data-svs-calendar={uid}>
298
+ <div class={cls(PARTS.WHOLE, variant)} role="group" aria-label={appLabel} data-svs-calendar={uid}>
278
299
  <div class={cls(PARTS.TOP, variant)}>
279
300
  <button class={cls(PARTS.LEFT, variant)} type="button" onclick={hprev}
280
- >{#if prev}{@render prev(variant)}{:else}Previous{/if}</button
301
+ >{#if left}{@render left(variant)}{:else}Previous{/if}</button
281
302
  >
282
303
  <button id={idCaption} class={cls(PARTS.LABEL, variant)} type="button" aria-expanded={picking} onclick={hlabel}>
283
304
  {#if label}{@render label(currentDisplay(), variant, picking)}{:else}{captionText}{/if}
284
305
  </button>
285
306
  <button class={cls(PARTS.RIGHT, variant)} type="button" onclick={hnext}
286
- >{#if next}{@render next(variant)}{:else}Next{/if}</button
307
+ >{#if right}{@render right(variant)}{:else}Next{/if}</button
287
308
  >
288
309
  </div>
289
310
 
@@ -299,14 +320,14 @@
299
320
  </div>
300
321
  {:else}
301
322
  <div class={cls(PARTS.MIDDLE, variant)} transition:tfn|local={tparams}>
302
- <div class={cls(PARTS.AUX, variant)} role="row">
303
- {#each weekdayLabels as wl}
304
- <span class={cls(PARTS.EXTRA, VARIANT.NEUTRAL)} role="columnheader" data-weekday={wl.weekday} aria-label={wl.full}>
305
- {#if weekday}{@render weekday(wl.weekday, variant)}{:else}{wl.text}{/if}
306
- </span>
307
- {/each}
308
- </div>
309
323
  <div class={cls(PARTS.MAIN, variant)} role="grid" tabindex="-1" aria-labelledby={idCaption} onkeydown={hkeydown}>
324
+ <div class={cls(PARTS.AUX, variant)} role="row">
325
+ {#each weekdayLabels as wl}
326
+ <span class={cls(PARTS.EXTRA, VARIANT.NEUTRAL)} role="columnheader" data-weekday={wl.weekday} aria-label={wl.full}>
327
+ {#if weekday}{@render weekday(wl.weekday, variant)}{:else}{wl.text}{/if}
328
+ </span>
329
+ {/each}
330
+ </div>
310
331
  {#each weeks as week}
311
332
  <div role="row">
312
333
  {#each week as cell}
@@ -320,6 +341,7 @@
320
341
  type="button"
321
342
  role="gridcell"
322
343
  tabindex={cell.equals(focused) ? 0 : -1}
344
+ bind:this={cells[cell.toString()]}
323
345
  aria-selected={c.selected}
324
346
  aria-disabled={c.disabled || undefined}
325
347
  aria-current={c.today ? "date" : undefined}
@@ -1,18 +1,3 @@
1
- export type DayCtx = {
2
- date: Temporal.PlainDate;
3
- variant: string;
4
- weekday: number;
5
- today: boolean;
6
- selected: boolean;
7
- outside: boolean;
8
- disabled: boolean;
9
- };
10
- export type TransitionProp = {
11
- fn?: (node: HTMLElement, params: any, options: {
12
- direction: "in" | "out" | "both";
13
- }) => import("svelte/transition").TransitionConfig;
14
- params?: unknown;
15
- };
16
1
  export interface CalendarProps {
17
2
  value?: Temporal.PlainDate;
18
3
  display?: Temporal.PlainYearMonth;
@@ -25,8 +10,8 @@ export interface CalendarProps {
25
10
  firstDayOfWeek?: number;
26
11
  locale?: string;
27
12
  label?: Snippet<[Temporal.PlainYearMonth, string, boolean]>;
28
- prev?: Snippet<[string]>;
29
- next?: Snippet<[string]>;
13
+ left?: Snippet<[string]>;
14
+ right?: Snippet<[string]>;
30
15
  weekday?: Snippet<[number, string]>;
31
16
  day?: Snippet<[DayCtx]>;
32
17
  bottom?: Snippet<[string, () => void]>;
@@ -35,6 +20,21 @@ export interface CalendarProps {
35
20
  styling?: SVSClass;
36
21
  variant?: SVSVariant;
37
22
  }
23
+ export type DayCtx = {
24
+ date: Temporal.PlainDate;
25
+ variant: string;
26
+ weekday: number;
27
+ today: boolean;
28
+ selected: boolean;
29
+ outside: boolean;
30
+ disabled: boolean;
31
+ };
32
+ export type TransitionProp = {
33
+ fn?: (node: HTMLElement, params: any, options: {
34
+ direction: "in" | "out" | "both";
35
+ }) => import("svelte/transition").TransitionConfig;
36
+ params?: unknown;
37
+ };
38
38
  export type CalendarReqdProps = never;
39
39
  export type CalendarBindProps = "value" | "display" | "picking";
40
40
  export declare const _CALENDAR_PRESET = "svs-calendar";
@@ -50,19 +50,6 @@ import type { MonthPickerProps } from "./MonthPicker.svelte";
50
50
  * ### Types
51
51
  * default value: *`(value)`*
52
52
  * ```ts
53
- * type DayCtx = {
54
- * date: Temporal.PlainDate;
55
- * variant: string;
56
- * weekday: number;
57
- * today: boolean;
58
- * selected: boolean;
59
- * outside: boolean;
60
- * disabled: boolean;
61
- * };
62
- * type TransitionProp = {
63
- * fn?: (node: HTMLElement, params: any, options: { direction: "in" | "out" | "both" }) => import("svelte/transition").TransitionConfig;
64
- * params?: unknown;
65
- * };
66
53
  * interface CalendarProps {
67
54
  * value?: Temporal.PlainDate; // bindable; selected date
68
55
  * display?: Temporal.PlainYearMonth; // bindable; shown month
@@ -75,8 +62,8 @@ import type { MonthPickerProps } from "./MonthPicker.svelte";
75
62
  * firstDayOfWeek?: number; // (0=Sun)
76
63
  * locale?: string;
77
64
  * label?: Snippet<[Temporal.PlainYearMonth, string, boolean]>;
78
- * prev?: Snippet<[string]>;
79
- * next?: Snippet<[string]>;
65
+ * left?: Snippet<[string]>;
66
+ * right?: Snippet<[string]>;
80
67
  * weekday?: Snippet<[number, string]>;
81
68
  * day?: Snippet<[DayCtx]>;
82
69
  * bottom?: Snippet<[string, () => void]>;
@@ -85,18 +72,33 @@ import type { MonthPickerProps } from "./MonthPicker.svelte";
85
72
  * styling?: SVSClass;
86
73
  * variant?: SVSVariant; // (VARIANT.NEUTRAL)
87
74
  * }
75
+ * type DayCtx = {
76
+ * date: Temporal.PlainDate;
77
+ * variant: string;
78
+ * weekday: number;
79
+ * today: boolean;
80
+ * selected: boolean;
81
+ * outside: boolean;
82
+ * disabled: boolean;
83
+ * };
84
+ * type TransitionProp = {
85
+ * fn?: (node: HTMLElement, params: any, options: { direction: "in" | "out" | "both" }) => import("svelte/transition").TransitionConfig;
86
+ * params?: unknown;
87
+ * };
88
88
  * ```
89
89
  * ### Anatomy
90
90
  * ```svelte
91
- * <div class="whole" role="application">
91
+ * <div class="whole" role="group">
92
92
  * <div class="top">
93
93
  * <button class="left" />
94
94
  * <button class="label" />
95
95
  * <button class="right" />
96
96
  * </div>
97
97
  * <div class="middle">
98
- * <div class="aux" role="row">weekday headers</div>
99
- * <div class="main" role="grid">day cells</div>
98
+ * <div class="main" role="grid">
99
+ * <div class="aux" role="row">weekday headers</div>
100
+ * day cells
101
+ * </div>
100
102
  * MonthPicker while picking
101
103
  * </div>
102
104
  * <div class="bottom" conditional>{bottom}</div>
@@ -75,7 +75,7 @@
75
75
  export const _CHECK_FIELD_PRESET = "svs-check-field";
76
76
 
77
77
  import { untrack, onMount } from "svelte";
78
- import { VARIANT, PARTS, _fnClass, _isNeutral } from "./_core";
78
+ import { VARIANT, PARTS, _fieldAria, _fieldIds, _fieldMessage, _fnClass, _isNeutral, _verify } from "./_core";
79
79
  import type { Snippet } from "svelte";
80
80
  import type { Attachment } from "svelte/attachments";
81
81
  import type { SvelteMap } from "svelte/reactivity";
@@ -92,21 +92,23 @@
92
92
  const type = $derived(multiple ? "checkbox" : "radio");
93
93
  const uid = $props.id();
94
94
  const nm = $derived(name?.trim() ? name : `${uid}-name`);
95
- const idLabel = $derived(label?.trim() ? `${uid}-label` : undefined);
96
- const idDesc = $derived(bottom?.trim() ? `${uid}-desc` : undefined);
97
- const idErr = $derived(idDesc ?? `${uid}-err`);
95
+ const ids = $derived(_fieldIds(uid, label, bottom));
96
+ const idLabel = $derived(ids.idLabel);
97
+ const idDesc = $derived(ids.idDesc);
98
+ const idErr = $derived(ids.idErr);
98
99
  const roleGroup = $derived(multiple ? "group" : "radiogroup");
99
100
  let errmsg = $state("");
100
- const message = $derived(variant === VARIANT.INACTIVE ? errmsg || bottom : bottom);
101
+ const message = $derived(_fieldMessage(variant, errmsg, bottom));
101
102
 
102
103
  // *** States *** //
103
104
  let neutral = $state(_isNeutral(variant) ? variant : VARIANT.NEUTRAL);
104
105
  $effect(() => {
105
106
  neutral = _isNeutral(variant) ? variant : neutral;
106
107
  });
107
- const live = $derived(variant === VARIANT.INACTIVE ? "alert" : undefined);
108
+ const aria = $derived(_fieldAria(variant, message, idErr));
109
+ const live = $derived(aria.live);
108
110
  const invalid = $derived(variant === VARIANT.INACTIVE ? true : undefined);
109
- const idMsg = $derived(variant === VARIANT.INACTIVE && message?.trim() ? idErr : undefined);
111
+ const idMsg = $derived(aria.idMsg);
110
112
  const reqd = $derived(required && (!multiple || !values.length) ? true : undefined);
111
113
  function shift(oninvalid: boolean = false, msg?: string) {
112
114
  const vmsg = elements[0]?.validationMessage ?? "";
@@ -114,12 +116,7 @@
114
116
  errmsg = msg ? msg : vmsg;
115
117
  }
116
118
  function verify() {
117
- if (!elements[0]) return;
118
- for (const v of validations) {
119
- const msg = v({ value: values, validity: elements[0].validity, element: elements[0] });
120
- if (msg) return elements[0].setCustomValidity(msg);
121
- }
122
- elements[0].setCustomValidity("");
119
+ _verify(elements[0], validations, values);
123
120
  }
124
121
  function check(value: string): string {
125
122
  if (!elements[0]) return "";
@@ -132,10 +129,17 @@
132
129
 
133
130
  // *** Reactive Handlers *** //
134
131
  const opts = $derived(Array.from(options, ([value, text]) => ({ value, text, checked: values.includes(value) })));
132
+ $effect.pre(() => {
133
+ opts.length;
134
+ untrack(() => trimElements(opts.length));
135
+ });
135
136
  $effect.pre(() => {
136
137
  values;
137
138
  untrack(() => validate(true));
138
139
  });
140
+ function trimElements(length: number) {
141
+ if (elements.length > length) elements.length = length;
142
+ }
139
143
  function validate(effect?: boolean) {
140
144
  if (effect && _isNeutral(variant)) return;
141
145
  verify();
@@ -154,7 +158,7 @@
154
158
  return;
155
159
  }
156
160
  }
157
- values = elements.filter((el) => el.checked).map((el) => el.value);
161
+ values = elements.filter((el) => el?.checked).map((el) => el.value);
158
162
  validate();
159
163
  };
160
164
  const hinvalid: EventHandler<Event, HTMLInputElement> = (ev) => {
@@ -179,7 +183,7 @@
179
183
  {/if}
180
184
  {@render main()}
181
185
  {#if reserve || message?.trim()}
182
- <div class={cls(PARTS.BOTTOM, variant)} id={idDesc ?? idErr} role={live}>{message}</div>
186
+ <div class={cls(PARTS.BOTTOM, variant)} id={idErr} role={live}>{message}</div>
183
187
  {/if}
184
188
  </div>
185
189
  {/if}
@@ -8,14 +8,16 @@
8
8
  ### Types
9
9
  default value: *`(value)`*
10
10
  ```ts
11
- interface ColorPickerProps extends Omit<HTMLInputAttributes, "type" | "value"> {
11
+ interface ColorPickerProps extends Omit<HTMLInputAttributes, "type" | "value" | "alpha" | "aria-label"> {
12
12
  value?: string; // bindable ("#000000")
13
13
  alpha?: number; // (1)
14
14
  checkered?: boolean; // (true)
15
+ ariaLabel?: string;
15
16
  attach?: Attachment<HTMLInputElement>;
16
17
  element?: HTMLInputElement; // bindable
17
18
  styling?: SVSClass;
18
19
  variant?: SVSVariant; // (VARIANT.NEUTRAL)
20
+ // ariaLabel is the recommended accessible-name prop; aria-labelledby may be passed via ...rest
19
21
  // class & other HTMLInputAttributes are passed to <input> via ...rest (class is merged onto the control)
20
22
  }
21
23
  ```
@@ -24,7 +26,7 @@
24
26
  <label class="whole">
25
27
  <div class="middle"> // this middle element can provide the default transparency background when checkered is true
26
28
  <div class="main"> // this main element is color sample and positions the transparent, focusable native input overlay
27
- <input {...rest} type="color" />
29
+ <input aria-label {...rest} type="color" />
28
30
  </div>
29
31
  </div>
30
32
  </label>
@@ -36,10 +38,11 @@
36
38
  ```
37
39
  -->
38
40
  <script module lang="ts">
39
- export interface ColorPickerProps extends Omit<HTMLInputAttributes, "type" | "value" | "alpha"> {
41
+ export interface ColorPickerProps extends Omit<HTMLInputAttributes, "type" | "value" | "alpha" | "aria-label"> {
40
42
  value?: string; // bindable ("#000000")
41
43
  alpha?: number; // (1)
42
44
  checkered?: boolean; // (true)
45
+ ariaLabel?: string;
43
46
  attach?: Attachment<HTMLInputElement>;
44
47
  element?: HTMLInputElement; // bindable
45
48
  styling?: SVSClass;
@@ -89,7 +92,7 @@
89
92
 
90
93
  <script lang="ts">
91
94
  // prettier-ignore
92
- let { value = $bindable(DEFAULT_COLOR), alpha = 1, checkered = true, attach, element = $bindable(), styling, variant = VARIANT.NEUTRAL, class: c, ...rest }: ColorPickerProps = $props();
95
+ let { value = $bindable(DEFAULT_COLOR), alpha = 1, checkered = true, ariaLabel, attach, element = $bindable(), styling, variant = VARIANT.NEUTRAL, class: c, ...rest }: ColorPickerProps = $props();
93
96
 
94
97
  // *** Initialize *** //
95
98
  const cls = $derived(_fnClass(_COLOR_PICKER_PRESET, styling));
@@ -113,6 +116,7 @@
113
116
  bind:value
114
117
  bind:this={element}
115
118
  class={c}
119
+ aria-label={ariaLabel}
116
120
  {...rest}
117
121
  type="color"
118
122
  style="position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer;"
@@ -1,7 +1,8 @@
1
- export interface ColorPickerProps extends Omit<HTMLInputAttributes, "type" | "value" | "alpha"> {
1
+ export interface ColorPickerProps extends Omit<HTMLInputAttributes, "type" | "value" | "alpha" | "aria-label"> {
2
2
  value?: string;
3
3
  alpha?: number;
4
4
  checkered?: boolean;
5
+ ariaLabel?: string;
5
6
  attach?: Attachment<HTMLInputElement>;
6
7
  element?: HTMLInputElement;
7
8
  styling?: SVSClass;
@@ -24,14 +25,16 @@ import type { SVSClass, SVSVariant } from "./_core";
24
25
  * ### Types
25
26
  * default value: *`(value)`*
26
27
  * ```ts
27
- * interface ColorPickerProps extends Omit<HTMLInputAttributes, "type" | "value"> {
28
+ * interface ColorPickerProps extends Omit<HTMLInputAttributes, "type" | "value" | "alpha" | "aria-label"> {
28
29
  * value?: string; // bindable ("#000000")
29
30
  * alpha?: number; // (1)
30
31
  * checkered?: boolean; // (true)
32
+ * ariaLabel?: string;
31
33
  * attach?: Attachment<HTMLInputElement>;
32
34
  * element?: HTMLInputElement; // bindable
33
35
  * styling?: SVSClass;
34
36
  * variant?: SVSVariant; // (VARIANT.NEUTRAL)
37
+ * // ariaLabel is the recommended accessible-name prop; aria-labelledby may be passed via ...rest
35
38
  * // class & other HTMLInputAttributes are passed to <input> via ...rest (class is merged onto the control)
36
39
  * }
37
40
  * ```
@@ -40,7 +43,7 @@ import type { SVSClass, SVSVariant } from "./_core";
40
43
  * <label class="whole">
41
44
  * <div class="middle"> // this middle element can provide the default transparency background when checkered is true
42
45
  * <div class="main"> // this main element is color sample and positions the transparent, focusable native input overlay
43
- * <input {...rest} type="color" />
46
+ * <input aria-label {...rest} type="color" />
44
47
  * </div>
45
48
  * </div>
46
49
  * </label>
@@ -72,7 +72,7 @@
72
72
  export const [_getComboBoxContext, _setComboBoxContext] = _createContext<ComboBoxContext>();
73
73
 
74
74
  import { tick } from "svelte";
75
- import { VARIANT, PARTS, _fnClass, _createContext } from "./_core";
75
+ import { VARIANT, PARTS, _detectOverflow, _fnClass, _createContext } from "./_core";
76
76
  import type { Snippet } from "svelte";
77
77
  import type { Attachment } from "svelte/attachments";
78
78
  import type { SvelteSet } from "svelte/reactivity";
@@ -141,9 +141,7 @@
141
141
  overflow = { x: false, y: false };
142
142
  await tick();
143
143
  if (!listElem || typeof window === "undefined") return;
144
- const rect = listElem.getBoundingClientRect();
145
- overflow.x = window.innerWidth < rect.right;
146
- overflow.y = window.innerHeight < rect.bottom;
144
+ overflow = _detectOverflow(listElem);
147
145
  }
148
146
  function apply() {
149
147
  if (!expanded) return;
@@ -171,9 +169,9 @@
171
169
  onclick?.(ev);
172
170
  if (!expanded) open();
173
171
  };
174
- const hinput: FormEventHandler<HTMLInputElement> = (ev) => {
172
+ const hinput: EventHandler<InputEvent, HTMLInputElement> = (ev) => {
175
173
  oninput?.(ev);
176
- if ((ev as Parameters<EventHandler<InputEvent, HTMLInputElement>>[0]).isComposing) return;
174
+ if (ev.isComposing) return;
177
175
  typed = true;
178
176
  selected = effOptions?.has(effValue) ? view.indexOf(effValue) : NA;
179
177
  };
@@ -243,7 +241,7 @@
243
241
  onblur={hblur}
244
242
  onclick={hclick}
245
243
  onkeydown={hkeydown}
246
- oninput={hinput}
244
+ oninput={hinput as FormEventHandler<HTMLInputElement>}
247
245
  {@attach attach}
248
246
  />
249
247
  {#if extra}
@@ -294,9 +294,9 @@
294
294
  {@render children(value, variant, element)}
295
295
  {:else}
296
296
  {#if value}
297
- {@render svgDark()}
298
- {:else}
299
297
  {@render svgLight()}
298
+ {:else}
299
+ {@render svgDark()}
300
300
  {/if}
301
301
  {/if}
302
302
  {/snippet}