@lilydesignsystem/svelte-headless 0.5.1 → 0.6.1

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 (43) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/components/AlertDialog/AlertDialog.svelte +7 -1
  3. package/dist/components/BreadcrumbListItem/BreadcrumbListItem.svelte +3 -3
  4. package/dist/components/ConvertDiv/ConvertDiv.svelte +29 -0
  5. package/dist/components/ConvertDiv/ConvertDiv.svelte.d.ts +11 -0
  6. package/dist/components/ConvertFromDiv/ConvertFromDiv.svelte +26 -0
  7. package/dist/components/ConvertFromDiv/ConvertFromDiv.svelte.d.ts +9 -0
  8. package/dist/components/ConvertFromUnitDiv/ConvertFromUnitDiv.svelte +26 -0
  9. package/dist/components/ConvertFromUnitDiv/ConvertFromUnitDiv.svelte.d.ts +9 -0
  10. package/dist/components/ConvertFromValueDiv/ConvertFromValueDiv.svelte +26 -0
  11. package/dist/components/ConvertFromValueDiv/ConvertFromValueDiv.svelte.d.ts +9 -0
  12. package/dist/components/ConvertIntoDiv/ConvertIntoDiv.svelte +26 -0
  13. package/dist/components/ConvertIntoDiv/ConvertIntoDiv.svelte.d.ts +9 -0
  14. package/dist/components/ConvertIntoUnitDiv/ConvertIntoUnitDiv.svelte +26 -0
  15. package/dist/components/ConvertIntoUnitDiv/ConvertIntoUnitDiv.svelte.d.ts +9 -0
  16. package/dist/components/ConvertIntoValueDiv/ConvertIntoValueDiv.svelte +26 -0
  17. package/dist/components/ConvertIntoValueDiv/ConvertIntoValueDiv.svelte.d.ts +9 -0
  18. package/dist/components/Dialog/Dialog.svelte +14 -1
  19. package/dist/components/Dialog/modal.d.ts +4 -0
  20. package/dist/components/Dialog/modal.js +47 -0
  21. package/dist/components/FileDialog/FileDialog.svelte +7 -1
  22. package/dist/components/HeatmapChartWithYearOfDays/HeatmapChartWithYearOfDays.svelte +74 -0
  23. package/dist/components/HeatmapChartWithYearOfDays/HeatmapChartWithYearOfDays.svelte.d.ts +17 -0
  24. package/dist/components/MiddleTruncation/MiddleTruncation.svelte +108 -0
  25. package/dist/components/MiddleTruncation/MiddleTruncation.svelte.d.ts +15 -0
  26. package/dist/components/MiddleTruncation/middle-truncation.d.ts +28 -0
  27. package/dist/components/MiddleTruncation/middle-truncation.js +67 -0
  28. package/dist/components/PasswordInput/PasswordInput.svelte +1 -1
  29. package/dist/components/PasswordInput/PasswordInput.svelte.d.ts +1 -1
  30. package/dist/components/SectionListItem/SectionListItem.svelte +1 -1
  31. package/dist/components/StepListItem/StepListItem.svelte +3 -3
  32. package/dist/components/TimeoutDialog/TimeoutDialog.svelte +14 -1
  33. package/dist/components/TourListItem/TourListItem.svelte +2 -2
  34. package/dist/components/TreeList/TreeList.svelte +2 -2
  35. package/dist/components/TreeListItem/TreeListItem.svelte +38 -2
  36. package/dist/components/TreeListItem/TreeListItem.svelte.d.ts +3 -1
  37. package/dist/components/WheelPicker/WheelPicker.svelte +150 -0
  38. package/dist/components/WheelPicker/WheelPicker.svelte.d.ts +23 -0
  39. package/dist/components/WheelPicker/wheel-picker.d.ts +43 -0
  40. package/dist/components/WheelPicker/wheel-picker.js +79 -0
  41. package/dist/index.d.ts +10 -0
  42. package/dist/index.js +10 -0
  43. package/package.json +5 -3
@@ -0,0 +1,15 @@
1
+ import { type TruncationStrategy } from "./middle-truncation";
2
+ type $$ComponentProps = {
3
+ /** The full text. */
4
+ text: string;
5
+ /** The cut marker; no default. */
6
+ ellipsis: string;
7
+ /** How the room is shared between start and end. */
8
+ strategy?: TruncationStrategy;
9
+ /** Characters kept at the end for "fixed-end" and "min-end". */
10
+ end?: number;
11
+ [key: string]: unknown;
12
+ };
13
+ declare const MiddleTruncation: import("svelte").Component<$$ComponentProps, {}, "">;
14
+ type MiddleTruncation = ReturnType<typeof MiddleTruncation>;
15
+ export default MiddleTruncation;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Middle truncation: shorten a string to fit a width by cutting out the middle, keeping the start and the end ("report-fi…q3.pdf").
3
+ * Pure and framework-neutral: the caller supplies `measure`, so the module reads no DOM and is tested without a browser.
4
+ *
5
+ * Works on grapheme clusters (Intl.Segmenter, falling back to code points), so it never splits a surrogate pair, a combining
6
+ * sequence or an emoji. The width search is binary, so it measures O(log n) candidate strings.
7
+ *
8
+ * Strategies:
9
+ * "even" the start and the end share what fits, the end taking the extra cluster when the count is odd.
10
+ * "fixed-end" exactly `end` clusters are kept at the end; the start takes all the remaining room.
11
+ * "min-end" an even split, but at least `end` clusters are kept at the end (a file extension, a domain suffix).
12
+ */
13
+ export type TruncationStrategy = "even" | "fixed-end" | "min-end";
14
+ export interface TruncateMiddleOptions {
15
+ /** The ellipsis string. Required: the module has no default text (internationalisation). */
16
+ ellipsis: string;
17
+ /** The width available to the whole result, in the unit `measure` returns. */
18
+ maxWidth: number;
19
+ /** Width of a string in the same unit as `maxWidth`. */
20
+ measure: (text: string) => number;
21
+ strategy?: TruncationStrategy;
22
+ /** Clusters kept at the end for "fixed-end" and "min-end". Ignored by "even". */
23
+ end?: number;
24
+ }
25
+ /** Split into user-perceived characters. */
26
+ export declare function splitGraphemes(text: string): string[];
27
+ /** The text shortened to fit `maxWidth`, or unchanged when it already fits. */
28
+ export declare function truncateMiddle(text: string, options: TruncateMiddleOptions): string;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Middle truncation: shorten a string to fit a width by cutting out the middle, keeping the start and the end ("report-fi…q3.pdf").
3
+ * Pure and framework-neutral: the caller supplies `measure`, so the module reads no DOM and is tested without a browser.
4
+ *
5
+ * Works on grapheme clusters (Intl.Segmenter, falling back to code points), so it never splits a surrogate pair, a combining
6
+ * sequence or an emoji. The width search is binary, so it measures O(log n) candidate strings.
7
+ *
8
+ * Strategies:
9
+ * "even" the start and the end share what fits, the end taking the extra cluster when the count is odd.
10
+ * "fixed-end" exactly `end` clusters are kept at the end; the start takes all the remaining room.
11
+ * "min-end" an even split, but at least `end` clusters are kept at the end (a file extension, a domain suffix).
12
+ */
13
+ /** Split into user-perceived characters. */
14
+ export function splitGraphemes(text) {
15
+ const Segmenter = Intl.Segmenter;
16
+ if (Segmenter)
17
+ return Array.from(new Segmenter(undefined, { granularity: "grapheme" }).segment(text), (s) => s.segment);
18
+ return Array.from(text);
19
+ }
20
+ function clampEnd(end, length) {
21
+ if (end === undefined || !Number.isFinite(end))
22
+ return 0;
23
+ return Math.min(Math.max(Math.floor(end), 0), length);
24
+ }
25
+ /** The text shortened to fit `maxWidth`, or unchanged when it already fits. */
26
+ export function truncateMiddle(text, options) {
27
+ const { ellipsis, maxWidth, measure, strategy = "even" } = options;
28
+ if (measure(text) <= maxWidth)
29
+ return text;
30
+ const g = splitGraphemes(text);
31
+ const n = g.length;
32
+ const tail = (count) => (count > 0 ? g.slice(n - count).join("") : "");
33
+ const head = (count) => g.slice(0, count).join("");
34
+ if (strategy === "fixed-end") {
35
+ const end = clampEnd(options.end, n);
36
+ const endText = tail(end);
37
+ const room = maxWidth - measure(ellipsis + endText);
38
+ let low = 0;
39
+ let high = n - end;
40
+ while (low < high) {
41
+ const middle = Math.ceil((low + high) / 2);
42
+ if (measure(head(middle)) <= room)
43
+ low = middle;
44
+ else
45
+ high = middle - 1;
46
+ }
47
+ return head(low) + ellipsis + endText;
48
+ }
49
+ const minEnd = strategy === "min-end" ? clampEnd(options.end, n) : 0;
50
+ const split = (kept) => {
51
+ const endCount = Math.min(n, Math.max(Math.ceil(kept / 2), minEnd));
52
+ return [Math.max(0, kept - endCount), endCount];
53
+ };
54
+ const room = maxWidth - measure(ellipsis);
55
+ let low = 0;
56
+ let high = n;
57
+ while (low < high) {
58
+ const middle = Math.ceil((low + high) / 2);
59
+ const [startCount, endCount] = split(middle);
60
+ if (measure(head(startCount) + tail(endCount)) <= room)
61
+ low = middle;
62
+ else
63
+ high = middle - 1;
64
+ }
65
+ const [startCount, endCount] = split(low);
66
+ return head(startCount) + ellipsis + tail(endCount);
67
+ }
@@ -62,7 +62,7 @@
62
62
  /** Current value. Bindable. */
63
63
  value?: string;
64
64
  /** Autocomplete hint. */
65
- autocomplete?: string;
65
+ autocomplete?: "current-password" | "new-password" | "off";
66
66
  /** Whether required. */
67
67
  required?: boolean;
68
68
  /** Whether disabled. */
@@ -4,7 +4,7 @@ type $$ComponentProps = {
4
4
  /** Current value. Bindable. */
5
5
  value?: string;
6
6
  /** Autocomplete hint. */
7
- autocomplete?: string;
7
+ autocomplete?: "current-password" | "new-password" | "off";
8
8
  /** Whether required. */
9
9
  required?: boolean;
10
10
  /** Whether disabled. */
@@ -20,7 +20,7 @@
20
20
  class={`section-list-item ${className}`}
21
21
 
22
22
 
23
- aria-current={current ? "page" : undefined}
23
+ data-current={current ? "" : undefined}
24
24
  {...restProps}
25
25
  >
26
26
  {@render children?.()}
@@ -9,7 +9,7 @@
9
9
  // class — string, optional. CSS class appended to the base class.
10
10
  // status — "waiting" | "in-progress" | "finished" | "error",
11
11
  // default "waiting". Renders as data-status.
12
- // current — boolean, default false. When true, renders aria-current="step".
12
+ // current — boolean, default false. When true, renders data-current.
13
13
  // label — string, optional. aria-label override.
14
14
  // children — Snippet, required. Step content (typically title and description).
15
15
  // ...restProps — additional HTML attributes spread onto the <li>.
@@ -18,7 +18,7 @@
18
18
  // <StepListItem status="finished">Cart</StepListItem>
19
19
  //
20
20
  // Accessibility:
21
- // - aria-current="step" when current=true.
21
+ // - data-current when current=true.
22
22
  // - aria-label optional override.
23
23
  //
24
24
  // Internationalization:
@@ -60,7 +60,7 @@
60
60
  <li
61
61
  class={`step-list-item ${className}`}
62
62
  data-status={status}
63
- aria-current={current ? "step" : undefined}
63
+ data-current={current ? "" : undefined}
64
64
  aria-label={label}
65
65
  {...restProps}
66
66
  >
@@ -13,6 +13,19 @@
13
13
  children: Snippet;
14
14
  [key: string]: unknown;
15
15
  } = $props();
16
+
17
+ // Always rendered, shown while `open`: as a real modal where the browser has one (see Dialog/modal.ts).
18
+ let el: HTMLDialogElement | undefined = $state(undefined);
19
+ $effect(() => {
20
+ if (!el) return;
21
+ if (open && !el.open) {
22
+ if (typeof el.showModal === "function") el.showModal();
23
+ else el.setAttribute("open", "");
24
+ } else if (!open && el.open) {
25
+ if (typeof el.close === "function") el.close();
26
+ else el.removeAttribute("open");
27
+ }
28
+ });
16
29
  </script>
17
30
 
18
31
  <!-- TimeoutDialog.svelte -->
@@ -20,7 +33,7 @@
20
33
  class={`timeout-dialog ${className}`}
21
34
  role="alertdialog"
22
35
  aria-label={label}
23
- {open}
36
+ bind:this={el}
24
37
  {...restProps}
25
38
  >
26
39
  {@render children?.()}
@@ -40,7 +40,7 @@
40
40
  // - role="group" groups the step content semantically
41
41
  // - aria-roledescription="step" identifies the element as a tour step
42
42
  // - aria-label includes step label and optional "Step N of M" progress
43
- // - aria-current="step" indicates the currently active step
43
+ // - data-current indicates the currently active step
44
44
  // - aria-hidden="true" and hidden attribute hide inactive steps
45
45
  //
46
46
  // Internationalization:
@@ -95,7 +95,7 @@
95
95
  role="group"
96
96
  aria-roledescription="step"
97
97
  aria-label={fullLabel}
98
- aria-current={current ? "step" : undefined}
98
+ data-current={current ? "" : undefined}
99
99
  aria-hidden={!current ? "true" : undefined}
100
100
  hidden={!current ? true : undefined}
101
101
  {...restProps}
@@ -40,7 +40,7 @@
40
40
  //
41
41
  // Claude rules:
42
42
  // - Headless: no CSS, no styles -- consumer provides all styling
43
- // - Queries [role='treeitem'] descendants for keyboard navigation
43
+ // - Queries [role='treeitem'] descendants for keyboard navigation, skipping items inside a collapsed (aria-expanded=false) item
44
44
  // - Uses $state() for internal DOM reference
45
45
  //
46
46
  // References:
@@ -68,7 +68,7 @@
68
68
  if (!treeRef) return;
69
69
  const items = Array.from(
70
70
  treeRef.querySelectorAll<HTMLElement>("[role='treeitem']"),
71
- );
71
+ ).filter((el) => !el.parentElement?.closest("[role='treeitem'][aria-expanded='false']"));
72
72
  const current = document.activeElement as HTMLElement;
73
73
  const index = items.indexOf(current);
74
74
  switch (event.key) {
@@ -3,16 +3,52 @@
3
3
  let {
4
4
  class: className = "",
5
5
  label = undefined,
6
+ expanded = $bindable(undefined),
6
7
  children,
7
8
  ...restProps
8
9
  }: {
9
10
  label?: string;
11
+ /** Expanded state, bindable. Leave undefined for a leaf (no aria-expanded). */
12
+ expanded?: boolean;
10
13
  children: Snippet;
11
14
  [key: string]: unknown;
12
15
  } = $props();
16
+
17
+ // ArrowRight/ArrowLeft (WAI-ARIA tree pattern). Only for the focused item itself, not a
18
+ // nested one the event bubbled up from. Swapped in right-to-left layouts.
19
+ function onkeydown(event: KeyboardEvent) {
20
+ const item = event.currentTarget as HTMLElement;
21
+ if (event.target !== item) return;
22
+ const rtl = getComputedStyle(item).direction === "rtl";
23
+ const open = rtl ? "ArrowLeft" : "ArrowRight";
24
+ const close = rtl ? "ArrowRight" : "ArrowLeft";
25
+ if (event.key === open) {
26
+ if (expanded === false) {
27
+ event.preventDefault();
28
+ expanded = true;
29
+ } else if (expanded === true) {
30
+ const child = item.querySelector<HTMLElement>("[role='treeitem']");
31
+ if (child) {
32
+ event.preventDefault();
33
+ child.focus();
34
+ }
35
+ }
36
+ } else if (event.key === close) {
37
+ if (expanded === true) {
38
+ event.preventDefault();
39
+ expanded = false;
40
+ } else {
41
+ const parent = item.parentElement?.closest<HTMLElement>("[role='treeitem']");
42
+ if (parent) {
43
+ event.preventDefault();
44
+ parent.focus();
45
+ }
46
+ }
47
+ }
48
+ }
13
49
  </script>
14
50
 
15
51
  <!-- TreeListItem.svelte -->
16
- <div class={`tree-list-item ${className}`} aria-label={label} {...restProps}>
52
+ <li class={`tree-list-item ${className}`} role="treeitem" tabindex="-1" aria-label={label} aria-expanded={expanded} {onkeydown} {...restProps}>
17
53
  {@render children?.()}
18
- </div>
54
+ </li>
@@ -1,9 +1,11 @@
1
1
  import type { Snippet } from "svelte";
2
2
  type $$ComponentProps = {
3
3
  label?: string;
4
+ /** Expanded state, bindable. Leave undefined for a leaf (no aria-expanded). */
5
+ expanded?: boolean;
4
6
  children: Snippet;
5
7
  [key: string]: unknown;
6
8
  };
7
- declare const TreeListItem: import("svelte").Component<$$ComponentProps, {}, "">;
9
+ declare const TreeListItem: import("svelte").Component<$$ComponentProps, {}, "expanded">;
8
10
  type TreeListItem = ReturnType<typeof TreeListItem>;
9
11
  export default TreeListItem;
@@ -0,0 +1,150 @@
1
+ <script lang="ts">
2
+ // WheelPicker component
3
+ //
4
+ // A vertical single-select listbox where the selection follows the cursor: moving with the arrow keys, typing, or
5
+ // clicking an option chooses it. Use it to pick one of a short list (hours, minutes, months, years) in the manner of a
6
+ // scroll wheel. The component owns the roles, the keys and the selected value; the wheel LOOK (scroll snapping, fading,
7
+ // 3-D) is the consumer's CSS. Pointer dragging and touch inertia are plain scrolling of `.wheel-picker-list`.
8
+ //
9
+ // Props:
10
+ // class — string, optional. Consumer class appended after the base class.
11
+ // label — string, required. Accessible name of the listbox.
12
+ // options — { value: string; label: string; disabled?: boolean }[], required.
13
+ // value — string, bindable, default "". The selected option's value; "" or an unknown value selects nothing.
14
+ // loop — boolean, default false. Arrow keys wrap from the last enabled option to the first and back.
15
+ // pageSize — number, default 5. Enabled options PageUp / PageDown move by.
16
+ // disabled — boolean, default false. Ignores keys and clicks; not focusable.
17
+ // name — string, optional. When set, a hidden input carries the value for a form.
18
+ // onChange — (value: string) => void, optional. Called once when the selection changes.
19
+ // ...restProps — spread onto the root <div>.
20
+ //
21
+ // Markup:
22
+ // <div class="wheel-picker">
23
+ // <div class="wheel-picker-list" role="listbox" aria-orientation="vertical" tabindex="0" aria-activedescendant>
24
+ // <div class="wheel-picker-option" role="option" aria-selected aria-disabled? data-value>…</div>
25
+ // </div>
26
+ // <input type="hidden" name value> (only with `name`)
27
+ // </div>
28
+ //
29
+ // Keyboard (APG listbox, selection follows focus): ArrowDown / ArrowUp, Home / End, PageDown / PageUp, type-ahead over
30
+ // the labels. Disabled options are skipped.
31
+ //
32
+ // Claude rules:
33
+ // - Headless: no CSS, no inline styles; no animation (the selected option is centred instantly).
34
+ // - No hardcoded user-facing strings.
35
+
36
+ import {
37
+ isWheelTypeaheadKey,
38
+ nextWheelIndex,
39
+ nextWheelPrefix,
40
+ WHEEL_MOVE_KEYS,
41
+ wheelTypeaheadIndex,
42
+ type WheelMoveKey,
43
+ type WheelOption,
44
+ type WheelTypeahead,
45
+ } from "./wheel-picker";
46
+
47
+ let {
48
+ class: className = "",
49
+ label,
50
+ options,
51
+ value = $bindable(""),
52
+ loop = false,
53
+ pageSize = 5,
54
+ disabled = false,
55
+ name,
56
+ onChange,
57
+ ...restProps
58
+ }: {
59
+ /** Accessible name. */
60
+ label: string;
61
+ /** The choices. */
62
+ options: WheelOption[];
63
+ /** The selected option's value. Bindable. */
64
+ value?: string;
65
+ /** Arrow keys wrap at the ends. */
66
+ loop?: boolean;
67
+ /** Enabled options PageUp / PageDown move by. */
68
+ pageSize?: number;
69
+ /** Ignores keys and clicks. */
70
+ disabled?: boolean;
71
+ /** Form field name for a hidden input. */
72
+ name?: string;
73
+ /** Called once when the selection changes. */
74
+ onChange?: (value: string) => void;
75
+ [key: string]: unknown;
76
+ } = $props();
77
+
78
+ const uid = $props.id();
79
+ let list: HTMLElement | undefined = $state();
80
+ let typeahead: WheelTypeahead = { prefix: "", at: 0 };
81
+
82
+ const selectedIndex = $derived(options.findIndex((o) => o.value === value));
83
+
84
+ function select(index: number): void {
85
+ if (index < 0) return;
86
+ const next = options[index].value;
87
+ if (next === value) return; // applying the selection already held is a no-op
88
+ value = next;
89
+ onChange?.(next);
90
+ }
91
+
92
+ function onkeydown(event: KeyboardEvent): void {
93
+ if (disabled) return;
94
+ if (WHEEL_MOVE_KEYS.includes(event.key)) {
95
+ event.preventDefault();
96
+ select(nextWheelIndex(options, selectedIndex, event.key as WheelMoveKey, { loop, pageSize }));
97
+ } else if (isWheelTypeaheadKey(event)) {
98
+ typeahead = nextWheelPrefix(typeahead, event.key, Date.now());
99
+ select(wheelTypeaheadIndex(options, selectedIndex, typeahead.prefix));
100
+ }
101
+ }
102
+
103
+ function onclick(event: MouseEvent): void {
104
+ if (disabled) return;
105
+ const target = (event.target as Element).closest<HTMLElement>('[role="option"]');
106
+ if (!target || target.getAttribute("aria-disabled") === "true") return;
107
+ select(Number(target.getAttribute("data-index")));
108
+ list?.focus();
109
+ }
110
+
111
+ // Keep the selected option centred in the list. Only the list scrolls (never the page), and instantly: no animation.
112
+ $effect(() => {
113
+ const index = selectedIndex;
114
+ if (!list || index < 0) return;
115
+ const option = list.querySelector<HTMLElement>(`[data-index="${index}"]`);
116
+ if (!option) return;
117
+ list.scrollTop = option.offsetTop - (list.clientHeight - option.offsetHeight) / 2;
118
+ });
119
+ </script>
120
+
121
+ <!-- WheelPicker.svelte -->
122
+ <div class={`wheel-picker ${className}`} {...restProps}>
123
+ <div
124
+ bind:this={list}
125
+ class="wheel-picker-list"
126
+ role="listbox"
127
+ aria-label={label}
128
+ aria-orientation="vertical"
129
+ aria-disabled={disabled ? "true" : undefined}
130
+ aria-activedescendant={selectedIndex >= 0 ? `${uid}-option-${selectedIndex}` : undefined}
131
+ tabindex={disabled ? -1 : 0}
132
+ {onkeydown}
133
+ {onclick}
134
+ >
135
+ {#each options as option, index (option.value)}
136
+ <div
137
+ class="wheel-picker-option"
138
+ role="option"
139
+ id={`${uid}-option-${index}`}
140
+ aria-selected={index === selectedIndex}
141
+ aria-disabled={option.disabled ? "true" : undefined}
142
+ data-value={option.value}
143
+ data-index={index}
144
+ >{option.label}</div>
145
+ {/each}
146
+ </div>
147
+ {#if name}
148
+ <input type="hidden" {name} {value} />
149
+ {/if}
150
+ </div>
@@ -0,0 +1,23 @@
1
+ import { type WheelOption } from "./wheel-picker";
2
+ type $$ComponentProps = {
3
+ /** Accessible name. */
4
+ label: string;
5
+ /** The choices. */
6
+ options: WheelOption[];
7
+ /** The selected option's value. Bindable. */
8
+ value?: string;
9
+ /** Arrow keys wrap at the ends. */
10
+ loop?: boolean;
11
+ /** Enabled options PageUp / PageDown move by. */
12
+ pageSize?: number;
13
+ /** Ignores keys and clicks. */
14
+ disabled?: boolean;
15
+ /** Form field name for a hidden input. */
16
+ name?: string;
17
+ /** Called once when the selection changes. */
18
+ onChange?: (value: string) => void;
19
+ [key: string]: unknown;
20
+ };
21
+ declare const WheelPicker: import("svelte").Component<$$ComponentProps, {}, "value">;
22
+ type WheelPicker = ReturnType<typeof WheelPicker>;
23
+ export default WheelPicker;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Keyboard model for a wheel picker: a single-select listbox where the selection follows the cursor (APG listbox, "selection
3
+ * follows focus"), so moving the cursor IS choosing. Pure and framework-neutral, tested without a browser.
4
+ *
5
+ * Disabled options are skipped by every move. Arrows wrap only when `loop` is set; Home, End, PageUp and PageDown never wrap.
6
+ */
7
+ export interface WheelOption {
8
+ value: string;
9
+ label: string;
10
+ disabled?: boolean;
11
+ }
12
+ export type WheelMoveKey = "ArrowUp" | "ArrowDown" | "Home" | "End" | "PageUp" | "PageDown";
13
+ export declare const WHEEL_MOVE_KEYS: readonly string[];
14
+ export interface WheelMoveOptions {
15
+ /** Arrow keys wrap from the last enabled option to the first and back. Default false. */
16
+ loop?: boolean;
17
+ /** Enabled options PageUp / PageDown move by. Default 5. */
18
+ pageSize?: number;
19
+ }
20
+ /**
21
+ * The index to select after `key`, or -1 when no option can be selected. `current` is the selected index, or -1 for none.
22
+ * With nothing selected, ArrowDown / PageDown / Home choose the first enabled option and ArrowUp / PageUp / End the last.
23
+ */
24
+ export declare function nextWheelIndex(options: readonly WheelOption[], current: number, key: WheelMoveKey, move?: WheelMoveOptions): number;
25
+ export declare const WHEEL_TYPEAHEAD_RESET_MS = 700;
26
+ export interface WheelTypeahead {
27
+ prefix: string;
28
+ at: number;
29
+ }
30
+ /** The prefix after `key` at time `now`: it grows while presses are within the reset time, else starts over. */
31
+ export declare function nextWheelPrefix(previous: WheelTypeahead, key: string, now: number, resetMs?: number): WheelTypeahead;
32
+ /** Whether a key press is a type-ahead character: one printable character other than space, no Ctrl, Alt or Meta. */
33
+ export declare function isWheelTypeaheadKey(event: {
34
+ key: string;
35
+ ctrlKey?: boolean;
36
+ altKey?: boolean;
37
+ metaKey?: boolean;
38
+ }): boolean;
39
+ /**
40
+ * The enabled option to move to for the typed `prefix`, or -1. Searches from the option after `current` and wraps; a prefix of one
41
+ * repeated letter ("ss") counts as that single letter, so pressing it again steps through the options that start with it.
42
+ */
43
+ export declare function wheelTypeaheadIndex(options: readonly WheelOption[], current: number, prefix: string): number;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Keyboard model for a wheel picker: a single-select listbox where the selection follows the cursor (APG listbox, "selection
3
+ * follows focus"), so moving the cursor IS choosing. Pure and framework-neutral, tested without a browser.
4
+ *
5
+ * Disabled options are skipped by every move. Arrows wrap only when `loop` is set; Home, End, PageUp and PageDown never wrap.
6
+ */
7
+ export const WHEEL_MOVE_KEYS = ["ArrowUp", "ArrowDown", "Home", "End", "PageUp", "PageDown"];
8
+ /** Indexes of the options that can be selected. */
9
+ function enabledIndexes(options) {
10
+ const out = [];
11
+ options.forEach((o, i) => {
12
+ if (!o.disabled)
13
+ out.push(i);
14
+ });
15
+ return out;
16
+ }
17
+ /**
18
+ * The index to select after `key`, or -1 when no option can be selected. `current` is the selected index, or -1 for none.
19
+ * With nothing selected, ArrowDown / PageDown / Home choose the first enabled option and ArrowUp / PageUp / End the last.
20
+ */
21
+ export function nextWheelIndex(options, current, key, move = {}) {
22
+ const enabled = enabledIndexes(options);
23
+ if (enabled.length === 0)
24
+ return -1;
25
+ const first = enabled[0];
26
+ const last = enabled[enabled.length - 1];
27
+ const position = enabled.indexOf(current);
28
+ const pageSize = Math.max(1, Math.floor(move.pageSize ?? 5));
29
+ if (key === "Home")
30
+ return first;
31
+ if (key === "End")
32
+ return last;
33
+ if (position === -1)
34
+ return key === "ArrowDown" || key === "PageDown" ? first : last;
35
+ switch (key) {
36
+ case "ArrowDown":
37
+ return position + 1 < enabled.length ? enabled[position + 1] : move.loop ? first : current;
38
+ case "ArrowUp":
39
+ return position > 0 ? enabled[position - 1] : move.loop ? last : current;
40
+ case "PageDown":
41
+ return enabled[Math.min(enabled.length - 1, position + pageSize)];
42
+ case "PageUp":
43
+ return enabled[Math.max(0, position - pageSize)];
44
+ }
45
+ }
46
+ /** Lower-case and strip diacritics so "é" is typed with "e". */
47
+ function fold(text) {
48
+ return text.normalize("NFD").replace(/\p{M}/gu, "").toLocaleLowerCase();
49
+ }
50
+ export const WHEEL_TYPEAHEAD_RESET_MS = 700;
51
+ /** The prefix after `key` at time `now`: it grows while presses are within the reset time, else starts over. */
52
+ export function nextWheelPrefix(previous, key, now, resetMs = WHEEL_TYPEAHEAD_RESET_MS) {
53
+ const continuing = previous.prefix !== "" && now - previous.at <= resetMs;
54
+ return { prefix: (continuing ? previous.prefix : "") + key, at: now };
55
+ }
56
+ /** Whether a key press is a type-ahead character: one printable character other than space, no Ctrl, Alt or Meta. */
57
+ export function isWheelTypeaheadKey(event) {
58
+ return event.key.length === 1 && event.key !== " " && !event.ctrlKey && !event.altKey && !event.metaKey;
59
+ }
60
+ /**
61
+ * The enabled option to move to for the typed `prefix`, or -1. Searches from the option after `current` and wraps; a prefix of one
62
+ * repeated letter ("ss") counts as that single letter, so pressing it again steps through the options that start with it.
63
+ */
64
+ export function wheelTypeaheadIndex(options, current, prefix) {
65
+ const folded = fold(prefix);
66
+ if (folded === "" || options.length === 0)
67
+ return -1;
68
+ const repeated = folded.length > 1 && [...folded].every((c) => c === folded[0]);
69
+ const needle = repeated ? folded[0] : folded;
70
+ const startOffset = needle.length > 1 ? 0 : 1;
71
+ const from = current < 0 ? -startOffset : current;
72
+ for (let step = 0; step < options.length; step++) {
73
+ const index = (from + startOffset + step + options.length) % options.length;
74
+ const option = options[index];
75
+ if (!option.disabled && fold(option.label).startsWith(needle))
76
+ return index;
77
+ }
78
+ return -1;
79
+ }
package/dist/index.d.ts CHANGED
@@ -124,6 +124,13 @@ export { default as ContentsNav } from "./components/ContentsNav/ContentsNav.sve
124
124
  export { default as ContextMenu } from "./components/ContextMenu/ContextMenu.svelte";
125
125
  export { default as ContextMenuItem } from "./components/ContextMenuItem/ContextMenuItem.svelte";
126
126
  export { default as ContextualHelp } from "./components/ContextualHelp/ContextualHelp.svelte";
127
+ export { default as ConvertDiv } from "./components/ConvertDiv/ConvertDiv.svelte";
128
+ export { default as ConvertFromDiv } from "./components/ConvertFromDiv/ConvertFromDiv.svelte";
129
+ export { default as ConvertFromUnitDiv } from "./components/ConvertFromUnitDiv/ConvertFromUnitDiv.svelte";
130
+ export { default as ConvertFromValueDiv } from "./components/ConvertFromValueDiv/ConvertFromValueDiv.svelte";
131
+ export { default as ConvertIntoDiv } from "./components/ConvertIntoDiv/ConvertIntoDiv.svelte";
132
+ export { default as ConvertIntoUnitDiv } from "./components/ConvertIntoUnitDiv/ConvertIntoUnitDiv.svelte";
133
+ export { default as ConvertIntoValueDiv } from "./components/ConvertIntoValueDiv/ConvertIntoValueDiv.svelte";
127
134
  export { default as CurrencyInput } from "./components/CurrencyInput/CurrencyInput.svelte";
128
135
  export { default as CymruRhifYGwasanaethIechydGwladolInput } from "./components/CymruRhifYGwasanaethIechydGwladolInput/CymruRhifYGwasanaethIechydGwladolInput.svelte";
129
136
  export { default as CymruRhifYGwasanaethIechydGwladolView } from "./components/CymruRhifYGwasanaethIechydGwladolView/CymruRhifYGwasanaethIechydGwladolView.svelte";
@@ -241,6 +248,7 @@ export { default as HangukJuminDeungnokBeonhoView } from "./components/HangukJum
241
248
  export { default as Header } from "./components/Header/Header.svelte";
242
249
  export { default as Headline } from "./components/Headline/Headline.svelte";
243
250
  export { default as HeatmapChart } from "./components/HeatmapChart/HeatmapChart.svelte";
251
+ export { default as HeatmapChartWithYearOfDays } from "./components/HeatmapChartWithYearOfDays/HeatmapChartWithYearOfDays.svelte";
244
252
  export { default as Hero } from "./components/Hero/Hero.svelte";
245
253
  export { default as HeroHeadline } from "./components/HeroHeadline/HeroHeadline.svelte";
246
254
  export { default as HiddenInput } from "./components/HiddenInput/HiddenInput.svelte";
@@ -323,6 +331,7 @@ export { default as MenuItem } from "./components/MenuItem/MenuItem.svelte";
323
331
  export { default as Meter } from "./components/Meter/Meter.svelte";
324
332
  export { default as MexicoClaveUnicaDeRegistroDePoblacionInput } from "./components/MexicoClaveUnicaDeRegistroDePoblacionInput/MexicoClaveUnicaDeRegistroDePoblacionInput.svelte";
325
333
  export { default as MexicoClaveUnicaDeRegistroDePoblacionView } from "./components/MexicoClaveUnicaDeRegistroDePoblacionView/MexicoClaveUnicaDeRegistroDePoblacionView.svelte";
334
+ export { default as MiddleTruncation } from "./components/MiddleTruncation/MiddleTruncation.svelte";
326
335
  export { default as MisrAlRaqmAlQawmiInput } from "./components/MisrAlRaqmAlQawmiInput/MisrAlRaqmAlQawmiInput.svelte";
327
336
  export { default as MisrAlRaqmAlQawmiView } from "./components/MisrAlRaqmAlQawmiView/MisrAlRaqmAlQawmiView.svelte";
328
337
  export { default as MockupBrowser } from "./components/MockupBrowser/MockupBrowser.svelte";
@@ -565,6 +574,7 @@ export { default as Visible } from "./components/Visible/Visible.svelte";
565
574
  export { default as WarningCallout } from "./components/WarningCallout/WarningCallout.svelte";
566
575
  export { default as Watermark } from "./components/Watermark/Watermark.svelte";
567
576
  export { default as WeekInput } from "./components/WeekInput/WeekInput.svelte";
577
+ export { default as WheelPicker } from "./components/WheelPicker/WheelPicker.svelte";
568
578
  export { default as YisraelTeudatZehutInput } from "./components/YisraelTeudatZehutInput/YisraelTeudatZehutInput.svelte";
569
579
  export { default as YisraelTeudatZehutView } from "./components/YisraelTeudatZehutView/YisraelTeudatZehutView.svelte";
570
580
  export { default as ZhongguoJuminShenfenzhengHaomaInput } from "./components/ZhongguoJuminShenfenzhengHaomaInput/ZhongguoJuminShenfenzhengHaomaInput.svelte";