@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.
- package/CHANGELOG.md +31 -0
- package/dist/components/CandlestickChart/CandlestickChart.svelte +63 -0
- package/dist/components/CandlestickChart/CandlestickChart.svelte.d.ts +13 -0
- package/dist/components/ChatComposer/ChatComposer.svelte +110 -0
- package/dist/components/ChatComposer/ChatComposer.svelte.d.ts +25 -0
- package/dist/components/ChoroplethChart/ChoroplethChart.svelte +63 -0
- package/dist/components/ChoroplethChart/ChoroplethChart.svelte.d.ts +13 -0
- package/dist/components/ComposedChart/ComposedChart.svelte +63 -0
- package/dist/components/ComposedChart/ComposedChart.svelte.d.ts +13 -0
- package/dist/components/ContentsLink/ContentsLink.svelte +36 -2
- package/dist/components/ContentsLink/ContentsLink.svelte.d.ts +3 -0
- package/dist/components/ContentsNav/ContentsNav.svelte +193 -1
- package/dist/components/ContentsNav/ContentsNav.svelte.d.ts +18 -1
- package/dist/components/DataTableTD/DataTableTD.svelte +5 -7
- package/dist/components/DataTableTD/DataTableTD.svelte.d.ts +1 -1
- package/dist/components/FiveFaceRatingPicker/FiveFaceRatingPicker.svelte +32 -2
- package/dist/components/FiveFaceRatingView/FiveFaceRatingView.svelte +3 -2
- package/dist/components/FiveStarRatingPicker/FiveStarRatingPicker.svelte +34 -2
- package/dist/components/FiveStarRatingPicker/FiveStarRatingPicker.svelte.d.ts +2 -0
- package/dist/components/FunnelChart/FunnelChart.svelte +63 -0
- package/dist/components/FunnelChart/FunnelChart.svelte.d.ts +13 -0
- package/dist/components/GanttTableTD/GanttTableTD.svelte +3 -4
- package/dist/components/KanbanTableTD/KanbanTableTD.svelte +3 -4
- package/dist/components/KanbanTableTD/KanbanTableTD.svelte.d.ts +1 -1
- package/dist/components/Mark/Mark.svelte +27 -0
- package/dist/components/Mark/Mark.svelte.d.ts +9 -0
- package/dist/components/NetPromoterScorePicker/NetPromoterScorePicker.svelte +29 -0
- package/dist/components/PieChart/PieChart.svelte +63 -0
- package/dist/components/PieChart/PieChart.svelte.d.ts +13 -0
- package/dist/components/PinInputDiv/PinInputDiv.svelte +42 -2
- package/dist/components/PinInputDiv/PinInputDiv.svelte.d.ts +5 -0
- package/dist/components/RingChart/RingChart.svelte +63 -0
- package/dist/components/RingChart/RingChart.svelte.d.ts +13 -0
- package/dist/components/StreamingText/StreamingText.svelte +48 -0
- package/dist/components/StreamingText/StreamingText.svelte.d.ts +13 -0
- package/dist/components/SunburstChart/SunburstChart.svelte +63 -0
- package/dist/components/SunburstChart/SunburstChart.svelte.d.ts +13 -0
- package/dist/components/Tag/Tag.svelte +20 -8
- package/dist/components/Tag/Tag.svelte.d.ts +6 -0
- package/dist/components/ToolCall/ToolCall.svelte +54 -0
- package/dist/components/ToolCall/ToolCall.svelte.d.ts +15 -0
- package/dist/components/ToolCallError/ToolCallError.svelte +27 -0
- package/dist/components/ToolCallError/ToolCallError.svelte.d.ts +9 -0
- package/dist/components/ToolCallInput/ToolCallInput.svelte +30 -0
- package/dist/components/ToolCallInput/ToolCallInput.svelte.d.ts +11 -0
- package/dist/components/ToolCallName/ToolCallName.svelte +27 -0
- package/dist/components/ToolCallName/ToolCallName.svelte.d.ts +9 -0
- package/dist/components/ToolCallOutput/ToolCallOutput.svelte +30 -0
- package/dist/components/ToolCallOutput/ToolCallOutput.svelte.d.ts +11 -0
- package/dist/components/ToolCallStatus/ToolCallStatus.svelte +29 -0
- package/dist/components/ToolCallStatus/ToolCallStatus.svelte.d.ts +11 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +16 -0
- package/index.md +1 -1
- 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
|
-
}: {
|
|
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
|
|
6
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 ["
|
|
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
|
-
|
|
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 ["
|
|
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
|
-
|
|
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
|
|
7
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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;
|