@lilydesignsystem/svelte-headless 0.3.0 → 0.5.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/CHANGELOG.md +31 -0
  2. package/dist/components/CandlestickChart/CandlestickChart.svelte +63 -0
  3. package/dist/components/CandlestickChart/CandlestickChart.svelte.d.ts +13 -0
  4. package/dist/components/ChatComposer/ChatComposer.svelte +110 -0
  5. package/dist/components/ChatComposer/ChatComposer.svelte.d.ts +25 -0
  6. package/dist/components/ChoroplethChart/ChoroplethChart.svelte +63 -0
  7. package/dist/components/ChoroplethChart/ChoroplethChart.svelte.d.ts +13 -0
  8. package/dist/components/ComposedChart/ComposedChart.svelte +63 -0
  9. package/dist/components/ComposedChart/ComposedChart.svelte.d.ts +13 -0
  10. package/dist/components/ContentsLink/ContentsLink.svelte +36 -2
  11. package/dist/components/ContentsLink/ContentsLink.svelte.d.ts +3 -0
  12. package/dist/components/ContentsNav/ContentsNav.svelte +193 -1
  13. package/dist/components/ContentsNav/ContentsNav.svelte.d.ts +18 -1
  14. package/dist/components/DataTableTD/DataTableTD.svelte +5 -7
  15. package/dist/components/DataTableTD/DataTableTD.svelte.d.ts +1 -1
  16. package/dist/components/FiveFaceRatingPicker/FiveFaceRatingPicker.svelte +32 -2
  17. package/dist/components/FiveFaceRatingView/FiveFaceRatingView.svelte +3 -2
  18. package/dist/components/FiveStarRatingPicker/FiveStarRatingPicker.svelte +34 -2
  19. package/dist/components/FiveStarRatingPicker/FiveStarRatingPicker.svelte.d.ts +2 -0
  20. package/dist/components/FunnelChart/FunnelChart.svelte +63 -0
  21. package/dist/components/FunnelChart/FunnelChart.svelte.d.ts +13 -0
  22. package/dist/components/GanttTableTD/GanttTableTD.svelte +3 -4
  23. package/dist/components/KanbanTableTD/KanbanTableTD.svelte +3 -4
  24. package/dist/components/KanbanTableTD/KanbanTableTD.svelte.d.ts +1 -1
  25. package/dist/components/Mark/Mark.svelte +27 -0
  26. package/dist/components/Mark/Mark.svelte.d.ts +9 -0
  27. package/dist/components/NetPromoterScorePicker/NetPromoterScorePicker.svelte +29 -0
  28. package/dist/components/PieChart/PieChart.svelte +63 -0
  29. package/dist/components/PieChart/PieChart.svelte.d.ts +13 -0
  30. package/dist/components/PinInputDiv/PinInputDiv.svelte +42 -2
  31. package/dist/components/PinInputDiv/PinInputDiv.svelte.d.ts +5 -0
  32. package/dist/components/RingChart/RingChart.svelte +63 -0
  33. package/dist/components/RingChart/RingChart.svelte.d.ts +13 -0
  34. package/dist/components/StreamingText/StreamingText.svelte +48 -0
  35. package/dist/components/StreamingText/StreamingText.svelte.d.ts +13 -0
  36. package/dist/components/SunburstChart/SunburstChart.svelte +63 -0
  37. package/dist/components/SunburstChart/SunburstChart.svelte.d.ts +13 -0
  38. package/dist/components/Tag/Tag.svelte +20 -8
  39. package/dist/components/Tag/Tag.svelte.d.ts +6 -0
  40. package/dist/components/ToolCall/ToolCall.svelte +54 -0
  41. package/dist/components/ToolCall/ToolCall.svelte.d.ts +15 -0
  42. package/dist/components/ToolCallError/ToolCallError.svelte +27 -0
  43. package/dist/components/ToolCallError/ToolCallError.svelte.d.ts +9 -0
  44. package/dist/components/ToolCallInput/ToolCallInput.svelte +30 -0
  45. package/dist/components/ToolCallInput/ToolCallInput.svelte.d.ts +11 -0
  46. package/dist/components/ToolCallName/ToolCallName.svelte +27 -0
  47. package/dist/components/ToolCallName/ToolCallName.svelte.d.ts +9 -0
  48. package/dist/components/ToolCallOutput/ToolCallOutput.svelte +30 -0
  49. package/dist/components/ToolCallOutput/ToolCallOutput.svelte.d.ts +11 -0
  50. package/dist/components/ToolCallStatus/ToolCallStatus.svelte +29 -0
  51. package/dist/components/ToolCallStatus/ToolCallStatus.svelte.d.ts +11 -0
  52. package/dist/index.d.ts +16 -0
  53. package/dist/index.js +16 -0
  54. package/index.md +1 -1
  55. package/package.json +3 -2
@@ -10,8 +10,21 @@
10
10
  // className — string, optional. CSS class name.
11
11
  // label — string, required. Accessible name for the navigation landmark (e.g., "Contents").
12
12
  // children — Snippet, required. Typically a ContentsList component.
13
+ // spy — boolean, default false. Scrollspy: mark the link of the section currently in view.
14
+ // offset — number, default 0. Scrollspy activation line, in px below the top of `root`
15
+ // (set it to a fixed header's height, matching the targets' scroll-margin-top).
16
+ // root — HTMLElement | null, default null (the viewport). A scrolling element other than the viewport.
17
+ // current — string, default "true". The aria-current token set on the active link.
18
+ // active — string, bindable. The id of the section currently in view (spy only).
19
+ // onActiveChange — (id: string) => void, optional. Called when the active section changes.
13
20
  // ...restProps — additional HTML attributes spread onto the <nav>.
14
21
  //
22
+ // Scrollspy (spy): the links are the consumer's own `<a href="#id">` elements inside the nav. The
23
+ // active section is the last one whose top has passed the activation line; at the bottom of the
24
+ // scroll range the last section is active; before the first section, the first. The active link gets
25
+ // aria-current={current} and data-active; its ancestor <li>s get data-descendant-active. A click makes
26
+ // its link active at once and holds it until the scroll settles. Lily never animates or moves focus.
27
+ //
15
28
  // Syntax:
16
29
  // <ContentsNav label="Contents">
17
30
  // <ContentsList>
@@ -53,12 +66,191 @@
53
66
  class: className = "",
54
67
  label,
55
68
  children,
69
+ spy = false,
70
+ offset = 0,
71
+ root = null,
72
+ current = "true",
73
+ active = $bindable(""),
74
+ onActiveChange,
56
75
  ...restProps
57
- }: { label: string; children: Snippet; [key: string]: unknown } = $props();
76
+ }: {
77
+ label: string;
78
+ children: Snippet;
79
+ spy?: boolean;
80
+ offset?: number;
81
+ root?: HTMLElement | null;
82
+ current?: string;
83
+ active?: string;
84
+ onActiveChange?: (id: string) => void;
85
+ [key: string]: unknown;
86
+ } = $props();
87
+
88
+ let navEl: HTMLElement | undefined = $state();
89
+
90
+ $effect(() => {
91
+ const nav = navEl;
92
+ if (!spy || !nav || typeof window === "undefined") return;
93
+ const spyOffset = offset;
94
+ const spyRoot = root;
95
+ const token = current;
96
+ let locked = false;
97
+ let lockTimer: ReturnType<typeof setTimeout> | undefined;
98
+ let frame = 0;
99
+ let last = "";
100
+ // Until the reader first scrolls, the URL hash decides (links may render after mount).
101
+ let hashPending = false;
102
+
103
+ const scroller: Window | HTMLElement = spyRoot ?? window;
104
+
105
+ function mark(link: HTMLAnchorElement | undefined): void {
106
+ for (const a of contentsLinks(nav!)) {
107
+ if (a === link) {
108
+ a.setAttribute("aria-current", token);
109
+ a.setAttribute("data-active", "");
110
+ } else {
111
+ a.removeAttribute("aria-current");
112
+ a.removeAttribute("data-active");
113
+ }
114
+ }
115
+ for (const li of nav!.querySelectorAll("li")) {
116
+ if (link && li.contains(link) && li.querySelector(":scope > a") !== link) {
117
+ li.setAttribute("data-descendant-active", "");
118
+ } else {
119
+ li.removeAttribute("data-descendant-active");
120
+ }
121
+ }
122
+ const id = link ? contentsLinkId(link) : "";
123
+ if (id !== last) {
124
+ last = id;
125
+ active = id;
126
+ if (id) onActiveChange?.(id);
127
+ }
128
+ }
129
+
130
+ function update(): void {
131
+ if (locked) return;
132
+ if (hashPending) {
133
+ const id = decodeURIComponent(window.location.hash.slice(1));
134
+ const hashed = id ? contentsLinks(nav!).find((a) => contentsLinkId(a) === id) : undefined;
135
+ if (hashed) {
136
+ mark(hashed);
137
+ return;
138
+ }
139
+ }
140
+ const links = contentsLinks(nav!);
141
+ const pairs = links
142
+ .map((a) => ({ a, el: contentsTarget(a, nav!.ownerDocument) }))
143
+ .filter((p): p is { a: HTMLAnchorElement; el: HTMLElement } => p.el !== null);
144
+ if (pairs.length === 0) {
145
+ mark(undefined);
146
+ return;
147
+ }
148
+ const base = spyRoot ? spyRoot.getBoundingClientRect().top : 0;
149
+ const tops = pairs.map((p) => p.el.getBoundingClientRect().top - base);
150
+ const atBottom = spyRoot
151
+ ? spyRoot.scrollTop + spyRoot.clientHeight >= spyRoot.scrollHeight - 1
152
+ : window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 1;
153
+ mark(pairs[activeSectionIndex(tops, spyOffset, atBottom)].a);
154
+ }
155
+
156
+ function schedule(): void {
157
+ if (frame) return;
158
+ frame = requestAnimationFrame(() => {
159
+ frame = 0;
160
+ update();
161
+ });
162
+ }
163
+
164
+ function onScroll(): void {
165
+ hashPending = false;
166
+ schedule();
167
+ }
168
+
169
+ function release(): void {
170
+ locked = false;
171
+ if (lockTimer) clearTimeout(lockTimer);
172
+ update();
173
+ }
174
+
175
+ function onClick(event: Event): void {
176
+ const link = (event.target as Element | null)?.closest?.("a");
177
+ if (!link || !nav!.contains(link) || !contentsTarget(link as HTMLAnchorElement, nav!.ownerDocument)) return;
178
+ // The link is current at once; observer-style updates wait until the scroll settles.
179
+ mark(link as HTMLAnchorElement);
180
+ locked = true;
181
+ if (lockTimer) clearTimeout(lockTimer);
182
+ lockTimer = setTimeout(release, 1000);
183
+ }
184
+
185
+ function onHashChange(): void {
186
+ const id = decodeURIComponent(window.location.hash.slice(1));
187
+ const link = contentsLinks(nav!).find((a) => contentsLinkId(a) === id);
188
+ if (link) mark(link);
189
+ }
190
+
191
+ scroller.addEventListener("scroll", onScroll, { passive: true });
192
+ scroller.addEventListener("scrollend", release);
193
+ window.addEventListener("resize", schedule);
194
+ window.addEventListener("hashchange", onHashChange);
195
+ nav.addEventListener("click", onClick);
196
+ // Links rendered or re-pointed after mount (a list built from data) are picked up too.
197
+ const mutations = typeof MutationObserver === "undefined" ? null : new MutationObserver(schedule);
198
+ mutations?.observe(nav, { childList: true, subtree: true, attributes: true, attributeFilter: ["href"] });
199
+
200
+ // The URL hash wins on load; otherwise the position decides.
201
+ const initial = decodeURIComponent(window.location.hash.slice(1));
202
+ hashPending = initial !== "";
203
+ const hashLink = initial ? contentsLinks(nav).find((a) => contentsLinkId(a) === initial) : undefined;
204
+ if (hashLink) mark(hashLink);
205
+ else update();
206
+
207
+ return () => {
208
+ scroller.removeEventListener("scroll", onScroll);
209
+ scroller.removeEventListener("scrollend", release);
210
+ window.removeEventListener("resize", schedule);
211
+ window.removeEventListener("hashchange", onHashChange);
212
+ nav.removeEventListener("click", onClick);
213
+ mutations?.disconnect();
214
+ if (frame) cancelAnimationFrame(frame);
215
+ if (lockTimer) clearTimeout(lockTimer);
216
+ };
217
+ });
218
+ </script>
219
+
220
+ <script lang="ts" module>
221
+ /**
222
+ * Which section is current: the last whose top has passed the activation line
223
+ * (`tops` are each section's top relative to the scroller; `offset` is the line).
224
+ * At the bottom of the scroll range the last section wins (a short last section
225
+ * never reaches the line); before the first section, the first.
226
+ */
227
+ export function activeSectionIndex(tops: number[], offset: number, atBottom: boolean): number {
228
+ if (tops.length === 0) return -1;
229
+ if (atBottom) return tops.length - 1;
230
+ let index = 0;
231
+ for (let i = 0; i < tops.length; i++) {
232
+ if (tops[i] <= offset + 1) index = i;
233
+ }
234
+ return index;
235
+ }
236
+
237
+ /** The in-page `<a href="#id">` links inside `nav`. */
238
+ export function contentsLinks(nav: Element): HTMLAnchorElement[] {
239
+ return Array.from(nav.querySelectorAll<HTMLAnchorElement>('a[href^="#"]')).filter((a) => a.getAttribute("href")!.length > 1);
240
+ }
241
+
242
+ export function contentsLinkId(link: Element): string {
243
+ return decodeURIComponent((link.getAttribute("href") ?? "").slice(1));
244
+ }
245
+
246
+ export function contentsTarget(link: Element, doc: Document): HTMLElement | null {
247
+ return doc.getElementById(contentsLinkId(link));
248
+ }
58
249
  </script>
59
250
 
60
251
  <!-- ContentsNav.svelte -->
61
252
  <nav
253
+ bind:this={navEl}
62
254
  class={`contents-nav ${className}`}
63
255
  aria-label={label}
64
256
  {...restProps}
@@ -1,9 +1,26 @@
1
+ /**
2
+ * Which section is current: the last whose top has passed the activation line
3
+ * (`tops` are each section's top relative to the scroller; `offset` is the line).
4
+ * At the bottom of the scroll range the last section wins (a short last section
5
+ * never reaches the line); before the first section, the first.
6
+ */
7
+ export declare function activeSectionIndex(tops: number[], offset: number, atBottom: boolean): number;
8
+ /** The in-page `<a href="#id">` links inside `nav`. */
9
+ export declare function contentsLinks(nav: Element): HTMLAnchorElement[];
10
+ export declare function contentsLinkId(link: Element): string;
11
+ export declare function contentsTarget(link: Element, doc: Document): HTMLElement | null;
1
12
  import type { Snippet } from "svelte";
2
13
  type $$ComponentProps = {
3
14
  label: string;
4
15
  children: Snippet;
16
+ spy?: boolean;
17
+ offset?: number;
18
+ root?: HTMLElement | null;
19
+ current?: string;
20
+ active?: string;
21
+ onActiveChange?: (id: string) => void;
5
22
  [key: string]: unknown;
6
23
  };
7
- declare const ContentsNav: import("svelte").Component<$$ComponentProps, {}, "">;
24
+ declare const ContentsNav: import("svelte").Component<$$ComponentProps, {}, "active">;
8
25
  type ContentsNav = ReturnType<typeof ContentsNav>;
9
26
  export default ContentsNav;
@@ -2,13 +2,12 @@
2
2
  // DataTableTD component
3
3
  //
4
4
  // A single data cell within a DataTableRow. Renders as a <td> with
5
- // role="gridcell". Supports an active state for indicating the currently
6
- // focused or selected cell, communicated via aria-selected for screen
7
- // readers. Uses a roving tabindex pattern.
5
+ // role="gridcell". Supports an active state marking the grid's single roving tab
6
+ // stop (tabindex="0"); it does not set aria-selected.
8
7
  //
9
8
  // Props:
10
9
  // className — string, optional. CSS class name.
11
- // active — boolean, default false. Whether this cell is active/selected.
10
+ // active — boolean, default false. Whether this cell is the roving tab stop.
12
11
  // children — Snippet, required. Cell content.
13
12
  // ...restProps — additional HTML attributes spread onto the <td>.
14
13
  //
@@ -21,7 +20,7 @@
21
20
  //
22
21
  // Accessibility:
23
22
  // - role="gridcell" identifies the cell as part of an interactive grid
24
- // - aria-selected set to true when active; omitted otherwise
23
+ // - `active` is the roving tab stop only: it never sets aria-selected, because focus is not selection (a grid with real selection passes aria-selected itself)
25
24
  // - Roving tabindex: tabindex="0" when active, "-1" otherwise
26
25
  //
27
26
  // Claude rules:
@@ -39,7 +38,7 @@
39
38
  children,
40
39
  ...restProps
41
40
  }: {
42
- /** Whether this cell is active/selected. */
41
+ /** Whether this cell is the roving tab stop (tabindex="0"). Never sets aria-selected. */
43
42
  active?: boolean;
44
43
  /** Cell content. */
45
44
  children: Snippet;
@@ -51,7 +50,6 @@
51
50
  <td
52
51
  class={`data-table-td ${className}`}
53
52
  role="gridcell"
54
- aria-selected={active || undefined}
55
53
  tabindex={active ? 0 : -1}
56
54
  {...restProps}
57
55
  >
@@ -1,6 +1,6 @@
1
1
  import type { Snippet } from "svelte";
2
2
  type $$ComponentProps = {
3
- /** Whether this cell is active/selected. */
3
+ /** Whether this cell is the roving tab stop (tabindex="0"). Never sets aria-selected. */
4
4
  active?: boolean;
5
5
  /** Cell content. */
6
6
  children: Snippet;
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { untrack } from "svelte";
2
3
  // FiveFaceRatingPicker component
3
4
  //
4
5
  // A five-face rating input that lets users select a satisfaction level from 1 to 5
@@ -11,7 +12,7 @@
11
12
  // label — string, required. Accessible name for the rating group via aria-label.
12
13
  // value — number, default 0. Current rating (1-5, 0 = none); bindable via bind:value.
13
14
  // name — string, default "face-rating". Radio group name for form submission.
14
- // labels — string[], default ["Very bad", "Bad", "Okay", "Good", "Very good"]. Accessible labels for each face.
15
+ // labels — string[], default ["1", "2", "3", "4", "5"] (no English default; pass words, e.g. ["Very bad", …, "Very good"]). Accessible labels for each face.
15
16
  // disabled — boolean, default false. Whether the entire group is disabled.
16
17
  // ...restProps — additional HTML attributes spread onto the fieldset.
17
18
  //
@@ -53,7 +54,8 @@
53
54
  label,
54
55
  value = $bindable(0),
55
56
  name = "face-rating",
56
- labels = ["Very bad", "Bad", "Okay", "Good", "Very good"],
57
+ // The numbers, not English words: the words are the consumer's to supply.
58
+ labels = ["1", "2", "3", "4", "5"],
57
59
  disabled = false,
58
60
  ...restProps
59
61
  }: {
@@ -71,10 +73,38 @@
71
73
  } = $props();
72
74
 
73
75
  const faces = [1, 2, 3, 4, 5];
76
+
77
+ // Form reset: the radios use a one-way `checked`, which a reset clears
78
+ // without telling this component, so `value` would keep a rating the form no
79
+ // longer submits. Restore the initial value instead — one tick later, after
80
+ // the browser has reset the DOM, and not when the reset was cancelled.
81
+ let root: HTMLFieldSetElement | undefined = $state();
82
+ const initialValue = untrack(() => value);
83
+ $effect(() => {
84
+ const form = root?.form ?? root?.closest("form");
85
+ if (!form) return;
86
+ let timer: ReturnType<typeof setTimeout> | undefined;
87
+ const onReset = (event: Event) => {
88
+ clearTimeout(timer);
89
+ timer = setTimeout(() => {
90
+ if (event.defaultPrevented || !root) return;
91
+ value = initialValue;
92
+ for (const input of root.querySelectorAll<HTMLInputElement>('input[type="radio"]')) {
93
+ input.checked = input.value === String(initialValue);
94
+ }
95
+ }, 0);
96
+ };
97
+ form.addEventListener("reset", onReset);
98
+ return () => {
99
+ clearTimeout(timer);
100
+ form.removeEventListener("reset", onReset);
101
+ };
102
+ });
74
103
  </script>
75
104
 
76
105
  <!-- FiveFaceRatingPicker.svelte -->
77
106
  <fieldset
107
+ bind:this={root}
78
108
  class={`five-face-rating-picker ${className}`}
79
109
  role="radiogroup"
80
110
  aria-label={label}
@@ -10,7 +10,7 @@
10
10
  // className — string, optional. CSS class name.
11
11
  // value — number, required. Rating value (1-5) to display.
12
12
  // label — string, required. Accessible description via aria-label.
13
- // labels — string[], default ["Very bad", "Bad", "Okay", "Good", "Very good"]. Text labels for each face.
13
+ // labels — string[], default ["1", "2", "3", "4", "5"] (no English default; pass words, e.g. ["Very bad", …, "Very good"]). Text labels for each face.
14
14
  // ...restProps — additional HTML attributes spread onto the span.
15
15
  //
16
16
  // Syntax:
@@ -46,7 +46,8 @@
46
46
  class: className = "",
47
47
  value,
48
48
  label,
49
- labels = ["Very bad", "Bad", "Okay", "Good", "Very good"],
49
+ // The numbers, not English words: the words are the consumer's to supply.
50
+ labels = ["1", "2", "3", "4", "5"],
50
51
  ...restProps
51
52
  }: {
52
53
  /** Rating value (1-5). */
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { untrack } from "svelte";
2
3
  // FiveStarRatingPicker component
3
4
  //
4
5
  // A five-star rating input that lets users select a rating from 1 to 5 stars using
@@ -9,6 +10,7 @@
9
10
  // Props:
10
11
  // className — string, optional. CSS class name.
11
12
  // label — string, required. Accessible name for the rating group via aria-label.
13
+ // starLabel — (star: number) => string, optional. Each option's text; defaults to the number (no English).
12
14
  // value — number, default 0. Current rating (1-5, 0 = none); bindable via bind:value.
13
15
  // name — string, default "rating". Radio group name for form submission.
14
16
  // disabled — boolean, default false. Whether the entire group is disabled.
@@ -52,6 +54,7 @@
52
54
  label,
53
55
  value = $bindable(0),
54
56
  name = "rating",
57
+ starLabel,
55
58
  disabled = false,
56
59
  ...restProps
57
60
  }: {
@@ -61,16 +64,46 @@
61
64
  value?: number;
62
65
  /** Radio group name for form submission. */
63
66
  name?: string;
67
+ /** Each option's text, e.g. (n) => `${n} stars`. Defaults to the number alone: no English default. */
68
+ starLabel?: (star: number) => string;
64
69
  /** Whether the entire group is disabled. */
65
70
  disabled?: boolean;
66
71
  [key: string]: unknown;
67
72
  } = $props();
68
73
 
69
74
  const stars = [1, 2, 3, 4, 5];
75
+
76
+ // Form reset: the radios use a one-way `checked`, which a reset clears
77
+ // without telling this component, so `value` would keep a rating the form no
78
+ // longer submits. Restore the initial value instead — one tick later, after
79
+ // the browser has reset the DOM, and not when the reset was cancelled.
80
+ let root: HTMLFieldSetElement | undefined = $state();
81
+ const initialValue = untrack(() => value);
82
+ $effect(() => {
83
+ const form = root?.form ?? root?.closest("form");
84
+ if (!form) return;
85
+ let timer: ReturnType<typeof setTimeout> | undefined;
86
+ const onReset = (event: Event) => {
87
+ clearTimeout(timer);
88
+ timer = setTimeout(() => {
89
+ if (event.defaultPrevented || !root) return;
90
+ value = initialValue;
91
+ for (const input of root.querySelectorAll<HTMLInputElement>('input[type="radio"]')) {
92
+ input.checked = input.value === String(initialValue);
93
+ }
94
+ }, 0);
95
+ };
96
+ form.addEventListener("reset", onReset);
97
+ return () => {
98
+ clearTimeout(timer);
99
+ form.removeEventListener("reset", onReset);
100
+ };
101
+ });
70
102
  </script>
71
103
 
72
104
  <!-- FiveStarRatingPicker.svelte -->
73
105
  <fieldset
106
+ bind:this={root}
74
107
  class={`five-star-rating-picker ${className}`}
75
108
  role="radiogroup"
76
109
  aria-label={label}
@@ -86,8 +119,7 @@
86
119
  checked={value === star}
87
120
  onchange={() => (value = star)}
88
121
  />
89
- {star}
90
- {star === 1 ? "star" : "stars"}
122
+ {starLabel ? starLabel(star) : star}
91
123
  </label>
92
124
  {/each}
93
125
  </fieldset>
@@ -5,6 +5,8 @@ type $$ComponentProps = {
5
5
  value?: number;
6
6
  /** Radio group name for form submission. */
7
7
  name?: string;
8
+ /** Each option's text, e.g. (n) => `${n} stars`. Defaults to the number alone: no English default. */
9
+ starLabel?: (star: number) => string;
8
10
  /** Whether the entire group is disabled. */
9
11
  disabled?: boolean;
10
12
  [key: string]: unknown;
@@ -0,0 +1,63 @@
1
+ <script lang="ts">
2
+ // FunnelChart component
3
+ //
4
+ // A headless chart container for a chart of stages narrowing from top to bottom, showing how a quantity drops at each step. The consumer supplies the inline
5
+ // svg as `children`; the component draws nothing. The graphic is a
6
+ // role="img" wrapper named by `label`. The optional `dataTable` snippet
7
+ // renders the accessible table alternative as a SIBLING of that image
8
+ // wrapper, never inside it: a role="img" element makes its descendants
9
+ // presentational, so a table placed inside would be invisible to
10
+ // assistive technology.
11
+ //
12
+ // Props:
13
+ // class — string, optional. Consumer class appended after the base class.
14
+ // label — string, optional. Accessible name of the chart image.
15
+ // children — Snippet. The consumer-supplied inline svg.
16
+ // dataTable — Snippet, optional. The accessible table alternative.
17
+ // ...restProps — spread onto the root <figure>.
18
+ //
19
+ // Markup:
20
+ // <figure class="funnel-chart">
21
+ // <div class="funnel-chart-graphic" role="img" aria-label>…svg…</div>
22
+ // <div class="funnel-chart-data-table">…table…</div> (only when provided)
23
+ // </figure>
24
+ //
25
+ // Keyboard: none on the graphic; the data table follows native table behaviour.
26
+ //
27
+ // Claude rules:
28
+ // - Headless: no CSS, no inline styles; no drawing.
29
+ // - No hardcoded user-facing strings.
30
+
31
+ import type { Snippet } from "svelte";
32
+
33
+ let {
34
+ class: className = "",
35
+ label = undefined,
36
+ children,
37
+ dataTable,
38
+ ...restProps
39
+ }: {
40
+ /** Accessible name for the chart. */
41
+ label?: string;
42
+ /** The consumer-supplied inline svg. */
43
+ children: Snippet;
44
+ /** Optional accessible data table alternative. */
45
+ dataTable?: Snippet;
46
+ [key: string]: unknown;
47
+ } = $props();
48
+ </script>
49
+
50
+ <!-- FunnelChart.svelte -->
51
+ <figure
52
+ class={`funnel-chart ${className}`}
53
+ {...restProps}
54
+ >
55
+ <div class="funnel-chart-graphic" role="img" aria-label={label}>
56
+ {@render children?.()}
57
+ </div>
58
+ {#if dataTable}
59
+ <div class="funnel-chart-data-table">
60
+ {@render dataTable()}
61
+ </div>
62
+ {/if}
63
+ </figure>
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from "svelte";
2
+ type $$ComponentProps = {
3
+ /** Accessible name for the chart. */
4
+ label?: string;
5
+ /** The consumer-supplied inline svg. */
6
+ children: Snippet;
7
+ /** Optional accessible data table alternative. */
8
+ dataTable?: Snippet;
9
+ [key: string]: unknown;
10
+ };
11
+ declare const FunnelChart: import("svelte").Component<$$ComponentProps, {}, "">;
12
+ type FunnelChart = ReturnType<typeof FunnelChart>;
13
+ export default FunnelChart;
@@ -3,8 +3,8 @@
3
3
  //
4
4
  // A single cell within a GanttTableTr representing a time period in the
5
5
  // Gantt table grid. Renders as a <td> with role="gridcell". Supports an
6
- // active state to indicate the task spans this time period, communicated
7
- // via aria-selected for screen readers.
6
+ // active state that makes the cell a tab stop (tabindex="0"); it does
7
+ // not set aria-selected, which would announce focus as selection.
8
8
  //
9
9
  // Props:
10
10
  // className — string, optional. CSS class name.
@@ -31,7 +31,7 @@
31
31
  //
32
32
  // Accessibility:
33
33
  // - role="gridcell" identifies the cell as part of a grid
34
- // - aria-selected set to true when active; omitted otherwise
34
+ // - `active` is the roving tab stop only: it never sets aria-selected, because focus is not selection (a grid with real selection passes aria-selected itself)
35
35
  // - Roving tabindex: tabindex="0" when active, "-1" otherwise
36
36
  //
37
37
  // Internationalization:
@@ -65,7 +65,6 @@
65
65
  <td
66
66
  class={`gantt-table-td ${className}`}
67
67
  role="gridcell"
68
- aria-selected={active || undefined}
69
68
  tabindex={active ? 0 : -1}
70
69
  {...restProps}
71
70
  >
@@ -7,7 +7,7 @@
7
7
  //
8
8
  // Props:
9
9
  // className — string, optional. CSS class name.
10
- // active — boolean, optional. Marks the cell as currently active/selected.
10
+ // active — boolean, optional. Marks the cell as the roving tab stop.
11
11
  // label — string, optional. Accessible name for the cell via aria-label.
12
12
  // children — Snippet, optional. Cell content such as task cards or text.
13
13
  // ...restProps — additional HTML attributes spread onto the <td>.
@@ -28,7 +28,7 @@
28
28
  //
29
29
  // Accessibility:
30
30
  // - role="gridcell" identifies the cell as part of a grid
31
- // - aria-selected indicates the active/selected state when active is true
31
+ // - `active` is the roving tab stop only: it never sets aria-selected, because focus is not selection (a grid with real selection passes aria-selected itself)
32
32
  // - tabindex enables roving tabindex (0 when active, -1 otherwise)
33
33
  // - aria-label optionally provides an accessible name for the cell
34
34
  //
@@ -52,7 +52,7 @@
52
52
  children,
53
53
  ...restProps
54
54
  }: {
55
- /** Marks the cell as currently active/selected. */
55
+ /** Marks the cell as the roving tab stop (tabindex="0"). Never sets aria-selected. */
56
56
  active?: boolean;
57
57
  /** Accessible name for the cell. */
58
58
  label?: string;
@@ -66,7 +66,6 @@
66
66
  <td
67
67
  class={`kanban-table-td ${className}`}
68
68
  role="gridcell"
69
- aria-selected={active || undefined}
70
69
  tabindex={active ? 0 : -1}
71
70
  aria-label={label}
72
71
  {...restProps}
@@ -1,6 +1,6 @@
1
1
  import type { Snippet } from "svelte";
2
2
  type $$ComponentProps = {
3
- /** Marks the cell as currently active/selected. */
3
+ /** Marks the cell as the roving tab stop (tabindex="0"). Never sets aria-selected. */
4
4
  active?: boolean;
5
5
  /** Accessible name for the cell. */
6
6
  label?: string;
@@ -0,0 +1,27 @@
1
+ <script lang="ts">
2
+ // Mark component
3
+ //
4
+ // A headless inline wrapper: an inline highlight marking text as relevant or referenced, such as a search match, using the native mark element. Contract: a plain wrapper with the base class and children.
5
+ // Draws nothing and carries no strings of its own; the consumer supplies the text.
6
+ import type { Snippet } from "svelte";
7
+
8
+ let {
9
+ class: className = "",
10
+ children,
11
+ ...restProps
12
+ }: {
13
+
14
+ /** The content. */
15
+ children?: Snippet;
16
+ [key: string]: unknown;
17
+ } = $props();
18
+ </script>
19
+
20
+ <!-- Mark.svelte -->
21
+ <mark
22
+ class={`mark ${className}`}
23
+
24
+ {...restProps}
25
+ >
26
+ {@render children?.()}
27
+ </mark>
@@ -0,0 +1,9 @@
1
+ import type { Snippet } from "svelte";
2
+ type $$ComponentProps = {
3
+ /** The content. */
4
+ children?: Snippet;
5
+ [key: string]: unknown;
6
+ };
7
+ declare const Mark: import("svelte").Component<$$ComponentProps, {}, "">;
8
+ type Mark = ReturnType<typeof Mark>;
9
+ export default Mark;