@sentropic/design-system-svelte 0.34.32 → 0.34.34
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/dist/Badge.svelte +66 -2
- package/dist/Badge.svelte.d.ts +21 -0
- package/dist/Badge.svelte.d.ts.map +1 -1
- package/dist/Collapsible.svelte +55 -1
- package/dist/Collapsible.svelte.d.ts +15 -0
- package/dist/Collapsible.svelte.d.ts.map +1 -1
- package/dist/Collapsible.test.d.ts +2 -0
- package/dist/Collapsible.test.d.ts.map +1 -0
- package/dist/Collapsible.test.js +68 -0
- package/dist/SelectableList.svelte +36 -17
- package/dist/SelectableList.svelte.d.ts.map +1 -1
- package/dist/SelectableRow.svelte +53 -1
- package/dist/SelectableRow.svelte.d.ts +10 -0
- package/dist/SelectableRow.svelte.d.ts.map +1 -1
- package/dist/SolidGaugeChart.svelte +276 -0
- package/dist/SolidGaugeChart.svelte.d.ts +46 -0
- package/dist/SolidGaugeChart.svelte.d.ts.map +1 -0
- package/dist/StateTimelineChart.svelte +451 -0
- package/dist/StateTimelineChart.svelte.d.ts +43 -0
- package/dist/StateTimelineChart.svelte.d.ts.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/package.json +3 -3
package/dist/Badge.svelte
CHANGED
|
@@ -4,13 +4,50 @@
|
|
|
4
4
|
|
|
5
5
|
type BadgeProps = Omit<HTMLAttributes<HTMLSpanElement>, "class"> & {
|
|
6
6
|
tone?: "neutral" | "success" | "warning" | "error" | "info";
|
|
7
|
+
/**
|
|
8
|
+
* Badge shape — `"pill"` (default) is the current render (radius 999px, width
|
|
9
|
+
* grows with content). `"circle"` renders an equal-sided round bubble
|
|
10
|
+
* (`min-width === min-height`, equal inline/block padding, tabular-nums) — best
|
|
11
|
+
* for ≤2-digit counts. 3+ digit content degrades GRACEFULLY to a rounded-rect
|
|
12
|
+
* (never clipped), so consumer counts reaching the 1000s stay legible.
|
|
13
|
+
* Mirrors `Avatar`'s `shape`. Additive: with `shape` unset the badge renders
|
|
14
|
+
* byte-identically to before.
|
|
15
|
+
*/
|
|
16
|
+
shape?: "pill" | "circle";
|
|
17
|
+
/**
|
|
18
|
+
* Density — `"md"` (default) is the current render. `"sm"` shrinks the
|
|
19
|
+
* font-size (the rail-bubble scale). Additive: with `size` unset the badge
|
|
20
|
+
* renders byte-identically to before.
|
|
21
|
+
*/
|
|
22
|
+
size?: "sm" | "md";
|
|
7
23
|
class?: string;
|
|
24
|
+
/**
|
|
25
|
+
* The number / text. Stays content-driven (no `value` prop). For SR users a
|
|
26
|
+
* bare count is ambiguous — pass an `aria-label` via `...rest` describing what
|
|
27
|
+
* is counted (e.g. `aria-label="128 entities"`).
|
|
28
|
+
*/
|
|
8
29
|
children?: Snippet;
|
|
9
30
|
};
|
|
10
31
|
|
|
11
|
-
let {
|
|
32
|
+
let {
|
|
33
|
+
tone = "neutral",
|
|
34
|
+
shape = "pill",
|
|
35
|
+
size = "md",
|
|
36
|
+
class: className,
|
|
37
|
+
children,
|
|
38
|
+
...rest
|
|
39
|
+
}: BadgeProps = $props();
|
|
12
40
|
|
|
13
|
-
const classes = () =>
|
|
41
|
+
const classes = () =>
|
|
42
|
+
[
|
|
43
|
+
"st-badge",
|
|
44
|
+
`st-badge--${tone}`,
|
|
45
|
+
`st-badge--${shape}`,
|
|
46
|
+
`st-badge--${size}`,
|
|
47
|
+
className
|
|
48
|
+
]
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.join(" ");
|
|
14
51
|
</script>
|
|
15
52
|
|
|
16
53
|
<span {...rest} class={classes()}>
|
|
@@ -34,6 +71,33 @@
|
|
|
34
71
|
text-transform: var(--st-component-badge-textTransform, none);
|
|
35
72
|
}
|
|
36
73
|
|
|
74
|
+
/* Shape variants (additive). `pill` is the UNTOUCHED base `.st-badge` above — no
|
|
75
|
+
`--pill` rule exists, so a `shape="pill"` (or unset) badge renders
|
|
76
|
+
byte-identically. `--circle` overlays an equal-sided round bubble ON TOP of
|
|
77
|
+
the base rules: equal min-width/min-height, equal inline/block padding,
|
|
78
|
+
centered, tabular-nums for stable digit width. 3+ digit content degrades to a
|
|
79
|
+
rounded-rect (the box grows past the diameter, never clips), so large counts
|
|
80
|
+
stay legible. Every leaf falls back to a base literal so a theme that emits no
|
|
81
|
+
`--st-component-badge-circle-*` renders the variant identically. */
|
|
82
|
+
.st-badge--circle {
|
|
83
|
+
border-radius: var(--st-component-badge-circle-radius, 50%);
|
|
84
|
+
box-sizing: border-box;
|
|
85
|
+
font-variant-numeric: tabular-nums;
|
|
86
|
+
justify-content: center;
|
|
87
|
+
min-width: var(--st-component-badge-circle-size, 1.25rem);
|
|
88
|
+
min-height: var(--st-component-badge-circle-size, 1.25rem);
|
|
89
|
+
/* Equal inline/block padding keeps 1–2 digits round; the inline padding lets
|
|
90
|
+
3+ digits grow the box into a rounded-rect instead of clipping. */
|
|
91
|
+
padding: var(--st-component-badge-circle-padding, 0.125rem);
|
|
92
|
+
text-align: center;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* Density variant (additive). `md` is the UNTOUCHED base font-size; `--sm`
|
|
96
|
+
reuses the Tag `sm` scale for cross-house consistency. */
|
|
97
|
+
.st-badge--sm {
|
|
98
|
+
font-size: var(--st-component-badge-sm-fontSize, 0.6875rem);
|
|
99
|
+
}
|
|
100
|
+
|
|
37
101
|
.st-badge--neutral {
|
|
38
102
|
background: var(--st-semantic-surface-subtle);
|
|
39
103
|
color: var(--st-semantic-text-secondary);
|
package/dist/Badge.svelte.d.ts
CHANGED
|
@@ -2,7 +2,28 @@ import type { Snippet } from "svelte";
|
|
|
2
2
|
import type { HTMLAttributes } from "svelte/elements";
|
|
3
3
|
type BadgeProps = Omit<HTMLAttributes<HTMLSpanElement>, "class"> & {
|
|
4
4
|
tone?: "neutral" | "success" | "warning" | "error" | "info";
|
|
5
|
+
/**
|
|
6
|
+
* Badge shape — `"pill"` (default) is the current render (radius 999px, width
|
|
7
|
+
* grows with content). `"circle"` renders an equal-sided round bubble
|
|
8
|
+
* (`min-width === min-height`, equal inline/block padding, tabular-nums) — best
|
|
9
|
+
* for ≤2-digit counts. 3+ digit content degrades GRACEFULLY to a rounded-rect
|
|
10
|
+
* (never clipped), so consumer counts reaching the 1000s stay legible.
|
|
11
|
+
* Mirrors `Avatar`'s `shape`. Additive: with `shape` unset the badge renders
|
|
12
|
+
* byte-identically to before.
|
|
13
|
+
*/
|
|
14
|
+
shape?: "pill" | "circle";
|
|
15
|
+
/**
|
|
16
|
+
* Density — `"md"` (default) is the current render. `"sm"` shrinks the
|
|
17
|
+
* font-size (the rail-bubble scale). Additive: with `size` unset the badge
|
|
18
|
+
* renders byte-identically to before.
|
|
19
|
+
*/
|
|
20
|
+
size?: "sm" | "md";
|
|
5
21
|
class?: string;
|
|
22
|
+
/**
|
|
23
|
+
* The number / text. Stays content-driven (no `value` prop). For SR users a
|
|
24
|
+
* bare count is ambiguous — pass an `aria-label` via `...rest` describing what
|
|
25
|
+
* is counted (e.g. `aria-label="128 entities"`).
|
|
26
|
+
*/
|
|
6
27
|
children?: Snippet;
|
|
7
28
|
};
|
|
8
29
|
declare const Badge: import("svelte").Component<BadgeProps, {}, "">;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Badge.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Badge.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGpD,KAAK,UAAU,GAAG,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC,GAAG;IACjE,IAAI,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;IAC5D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;
|
|
1
|
+
{"version":3,"file":"Badge.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Badge.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGpD,KAAK,UAAU,GAAG,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC,GAAG;IACjE,IAAI,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;IAC5D;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC1B;;;;OAIG;IACH,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAmCJ,QAAA,MAAM,KAAK,gDAAwC,CAAC;AACpD,KAAK,KAAK,GAAG,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC;AACtC,eAAe,KAAK,CAAC"}
|
package/dist/Collapsible.svelte
CHANGED
|
@@ -7,7 +7,22 @@
|
|
|
7
7
|
/** État ouvert (bindable). */
|
|
8
8
|
open?: boolean;
|
|
9
9
|
title: string;
|
|
10
|
+
/**
|
|
11
|
+
* Density of the trigger — `"md"` (default) is the current render. `"sm"`
|
|
12
|
+
* de-emphasizes the trigger (smaller font/weight/padding) for NESTED /
|
|
13
|
+
* level-2 collapsibles; `"lg"` enlarges it. Additive: with `size` unset the
|
|
14
|
+
* trigger renders byte-identically to before.
|
|
15
|
+
*/
|
|
16
|
+
size?: "sm" | "md" | "lg";
|
|
10
17
|
disabled?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Trailing content rendered inside the trigger, BETWEEN the title and the
|
|
20
|
+
* chevron (e.g. a count Badge, a status Tag, a glyph). The chevron stays the
|
|
21
|
+
* rightmost affordance. If the trailing content carries information SR users
|
|
22
|
+
* need as part of the trigger name, set `aria-label` on the Collapsible via
|
|
23
|
+
* `...rest` (e.g. `aria-label="Entities, 128 items"`).
|
|
24
|
+
*/
|
|
25
|
+
trailing?: Snippet;
|
|
11
26
|
onToggle?: (open: boolean) => void;
|
|
12
27
|
class?: string;
|
|
13
28
|
children?: Snippet;
|
|
@@ -16,7 +31,9 @@
|
|
|
16
31
|
let {
|
|
17
32
|
open = $bindable(false),
|
|
18
33
|
title,
|
|
34
|
+
size = "md",
|
|
19
35
|
disabled = false,
|
|
36
|
+
trailing,
|
|
20
37
|
onToggle,
|
|
21
38
|
class: className,
|
|
22
39
|
children,
|
|
@@ -26,7 +43,14 @@
|
|
|
26
43
|
const uid = `st-collapsible-${Math.random().toString(36).slice(2, 9)}`;
|
|
27
44
|
|
|
28
45
|
const classes = $derived(
|
|
29
|
-
[
|
|
46
|
+
[
|
|
47
|
+
"st-collapsible",
|
|
48
|
+
`st-collapsible--${size}`,
|
|
49
|
+
open ? "st-collapsible--open" : null,
|
|
50
|
+
className
|
|
51
|
+
]
|
|
52
|
+
.filter(Boolean)
|
|
53
|
+
.join(" ")
|
|
30
54
|
);
|
|
31
55
|
|
|
32
56
|
function toggle() {
|
|
@@ -47,6 +71,9 @@
|
|
|
47
71
|
onclick={toggle}
|
|
48
72
|
>
|
|
49
73
|
<span class="st-collapsible__title">{title}</span>
|
|
74
|
+
{#if trailing}
|
|
75
|
+
<span class="st-collapsible__trailing">{@render trailing()}</span>
|
|
76
|
+
{/if}
|
|
50
77
|
<span class="st-collapsible__icon" aria-hidden="true">
|
|
51
78
|
<ChevronDown size={18} strokeWidth={2.25} />
|
|
52
79
|
</span>
|
|
@@ -103,10 +130,37 @@
|
|
|
103
130
|
cursor: not-allowed;
|
|
104
131
|
}
|
|
105
132
|
|
|
133
|
+
/* Density variants (additive). `md` is the UNTOUCHED base `.st-collapsible__trigger`
|
|
134
|
+
above — no `--md` rule exists, so a `size="md"` (or unset) trigger renders
|
|
135
|
+
byte-identically. `--sm` de-emphasizes for nesting; `--lg` enlarges. Every
|
|
136
|
+
leaf falls back to a base literal so a theme that emits no
|
|
137
|
+
`--st-component-collapsible-*` renders these variants identically. */
|
|
138
|
+
.st-collapsible--sm .st-collapsible__trigger {
|
|
139
|
+
font-size: var(--st-component-collapsible-sm-fontSize, 0.875rem);
|
|
140
|
+
font-weight: var(--st-component-collapsible-sm-fontWeight, 500);
|
|
141
|
+
padding: var(--st-component-collapsible-sm-paddingBlock, 0.4rem)
|
|
142
|
+
var(--st-component-collapsible-sm-paddingInline, 0.25rem);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.st-collapsible--lg .st-collapsible__trigger {
|
|
146
|
+
font-size: var(--st-component-collapsible-lg-fontSize, 1rem);
|
|
147
|
+
padding: var(--st-component-collapsible-lg-paddingBlock, 0.875rem)
|
|
148
|
+
var(--st-component-collapsible-lg-paddingInline, 0.25rem);
|
|
149
|
+
}
|
|
150
|
+
|
|
106
151
|
.st-collapsible__title {
|
|
107
152
|
flex: 1 1 auto;
|
|
108
153
|
}
|
|
109
154
|
|
|
155
|
+
/* Trigger trailing slot (additive). Holds a count badge / status / glyph
|
|
156
|
+
between the title and the chevron; never grows, so the chevron stays the
|
|
157
|
+
rightmost affordance and the title keeps `flex: 1 1 auto`. */
|
|
158
|
+
.st-collapsible__trailing {
|
|
159
|
+
align-items: center;
|
|
160
|
+
display: inline-flex;
|
|
161
|
+
flex: 0 0 auto;
|
|
162
|
+
}
|
|
163
|
+
|
|
110
164
|
.st-collapsible__icon {
|
|
111
165
|
align-items: center;
|
|
112
166
|
color: var(--st-semantic-text-secondary);
|
|
@@ -4,7 +4,22 @@ type CollapsibleProps = Omit<HTMLAttributes<HTMLDivElement>, "class" | "title">
|
|
|
4
4
|
/** État ouvert (bindable). */
|
|
5
5
|
open?: boolean;
|
|
6
6
|
title: string;
|
|
7
|
+
/**
|
|
8
|
+
* Density of the trigger — `"md"` (default) is the current render. `"sm"`
|
|
9
|
+
* de-emphasizes the trigger (smaller font/weight/padding) for NESTED /
|
|
10
|
+
* level-2 collapsibles; `"lg"` enlarges it. Additive: with `size` unset the
|
|
11
|
+
* trigger renders byte-identically to before.
|
|
12
|
+
*/
|
|
13
|
+
size?: "sm" | "md" | "lg";
|
|
7
14
|
disabled?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Trailing content rendered inside the trigger, BETWEEN the title and the
|
|
17
|
+
* chevron (e.g. a count Badge, a status Tag, a glyph). The chevron stays the
|
|
18
|
+
* rightmost affordance. If the trailing content carries information SR users
|
|
19
|
+
* need as part of the trigger name, set `aria-label` on the Collapsible via
|
|
20
|
+
* `...rest` (e.g. `aria-label="Entities, 128 items"`).
|
|
21
|
+
*/
|
|
22
|
+
trailing?: Snippet;
|
|
8
23
|
onToggle?: (open: boolean) => void;
|
|
9
24
|
class?: string;
|
|
10
25
|
children?: Snippet;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Collapsible.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Collapsible.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGpD,KAAK,gBAAgB,GAAG,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,GAAG;IAChF,8BAA8B;IAC9B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;
|
|
1
|
+
{"version":3,"file":"Collapsible.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Collapsible.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGpD,KAAK,gBAAgB,GAAG,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,GAAG;IAChF,8BAA8B;IAC9B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AA4DJ,QAAA,MAAM,WAAW,0DAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Collapsible.test.d.ts","sourceRoot":"","sources":["../src/lib/Collapsible.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { fireEvent, render } from "@testing-library/svelte";
|
|
2
|
+
import { createRawSnippet } from "svelte";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import Collapsible from "./Collapsible.svelte";
|
|
5
|
+
const snippet = (html) => createRawSnippet(() => ({ render: () => `<span>${html}</span>` }));
|
|
6
|
+
// Svelte scopes component styles by appending a per-component hash class (e.g.
|
|
7
|
+
// "svelte-pav3x3") to every styled element's class list. Compare against the
|
|
8
|
+
// SEMANTIC class only (the first token) so the assertions are stable across
|
|
9
|
+
// builds and read the structural class, not the scope hash.
|
|
10
|
+
const structuralClass = (el) => el.className.split(/\s+/)[0];
|
|
11
|
+
describe("Collapsible — base (byte-identity)", () => {
|
|
12
|
+
it("renders the default trigger with no trailing span and the md density class", () => {
|
|
13
|
+
const { container } = render(Collapsible, { props: { title: "Entities" } });
|
|
14
|
+
const root = container.querySelector(".st-collapsible");
|
|
15
|
+
expect(root).toBeTruthy();
|
|
16
|
+
// Default density is md (additive class), and there is NO trailing span when
|
|
17
|
+
// the slot is absent — the trigger keeps its original [title][chevron] shape.
|
|
18
|
+
expect(root.classList.contains("st-collapsible--md")).toBe(true);
|
|
19
|
+
expect(root.classList.contains("st-collapsible--sm")).toBe(false);
|
|
20
|
+
expect(container.querySelector(".st-collapsible__trailing")).toBeNull();
|
|
21
|
+
expect(container.querySelector(".st-collapsible__title")?.textContent).toBe("Entities");
|
|
22
|
+
// Trigger order is preserved: only the title span then the chevron.
|
|
23
|
+
const trigger = container.querySelector(".st-collapsible__trigger");
|
|
24
|
+
const spans = Array.from(trigger.children).map(structuralClass);
|
|
25
|
+
expect(spans).toEqual(["st-collapsible__title", "st-collapsible__icon"]);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
describe("Collapsible — size", () => {
|
|
29
|
+
it("size=\"sm\" toggles st-collapsible--sm", () => {
|
|
30
|
+
const { container } = render(Collapsible, { props: { title: "Type", size: "sm" } });
|
|
31
|
+
const root = container.querySelector(".st-collapsible");
|
|
32
|
+
expect(root.classList.contains("st-collapsible--sm")).toBe(true);
|
|
33
|
+
expect(root.classList.contains("st-collapsible--md")).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
it("size=\"lg\" toggles st-collapsible--lg", () => {
|
|
36
|
+
const { container } = render(Collapsible, { props: { title: "Section", size: "lg" } });
|
|
37
|
+
expect(container.querySelector(".st-collapsible").classList.contains("st-collapsible--lg")).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe("Collapsible — trailing slot", () => {
|
|
41
|
+
it("renders trailing content between the title and the chevron", () => {
|
|
42
|
+
const { container } = render(Collapsible, {
|
|
43
|
+
props: { title: "Entities", trailing: snippet("128") },
|
|
44
|
+
});
|
|
45
|
+
const trailing = container.querySelector(".st-collapsible__trailing");
|
|
46
|
+
expect(trailing).toBeTruthy();
|
|
47
|
+
expect(trailing?.textContent).toContain("128");
|
|
48
|
+
// Order inside the trigger: title, trailing, chevron (chevron stays last).
|
|
49
|
+
const trigger = container.querySelector(".st-collapsible__trigger");
|
|
50
|
+
const order = Array.from(trigger.children).map(structuralClass);
|
|
51
|
+
expect(order).toEqual([
|
|
52
|
+
"st-collapsible__title",
|
|
53
|
+
"st-collapsible__trailing",
|
|
54
|
+
"st-collapsible__icon",
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe("Collapsible — a11y unchanged", () => {
|
|
59
|
+
it("toggles aria-expanded on click with size + trailing set", async () => {
|
|
60
|
+
const { container } = render(Collapsible, {
|
|
61
|
+
props: { title: "Entities", size: "sm", trailing: snippet("7") },
|
|
62
|
+
});
|
|
63
|
+
const trigger = container.querySelector(".st-collapsible__trigger");
|
|
64
|
+
expect(trigger.getAttribute("aria-expanded")).toBe("false");
|
|
65
|
+
await fireEvent.click(trigger);
|
|
66
|
+
expect(trigger.getAttribute("aria-expanded")).toBe("true");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -58,8 +58,11 @@
|
|
|
58
58
|
let internal = $state<Set<string>>(new Set());
|
|
59
59
|
const selectedValues = $derived(controlled ? toSet(value) : internal);
|
|
60
60
|
|
|
61
|
-
// --- Row registry
|
|
62
|
-
//
|
|
61
|
+
// --- Row registry. Rows are stored in INSERTION order (registration is O(1));
|
|
62
|
+
// the DOM-ordered view is computed LAZILY (see `orderedEntries`) and only
|
|
63
|
+
// when a consumer actually needs visual order (arrow nav / roving tab stop).
|
|
64
|
+
// Sorting eagerly on every register() was O(n) per mount → O(n²) for the
|
|
65
|
+
// whole list (issue #26); deferring it makes a large list mount in O(n).
|
|
63
66
|
type Entry = { el: HTMLElement; value: string | undefined; disabled?: boolean };
|
|
64
67
|
let entries = $state<Entry[]>([]);
|
|
65
68
|
|
|
@@ -76,17 +79,28 @@
|
|
|
76
79
|
});
|
|
77
80
|
}
|
|
78
81
|
|
|
82
|
+
// DOM-ordered view of the registry. Memoised by `$derived.by` so the O(n log n)
|
|
83
|
+
// `compareDocumentPosition` sort runs at most ONCE per registry change (a batch
|
|
84
|
+
// of mounts in the same tick collapses into a single recompute), not once per
|
|
85
|
+
// register() call. ORDER-dependent readers (`navigate`, `effectiveTabStop`'s
|
|
86
|
+
// "first enabled row") read THIS so visual order is correct regardless of
|
|
87
|
+
// registration timing; order-INDEPENDENT lookups (`valueOf`/`isSelected`,
|
|
88
|
+
// disabled-membership) read the raw `entries`. register() itself stays O(1).
|
|
89
|
+
const orderedEntries = $derived.by(() => sortByDom(entries));
|
|
90
|
+
|
|
79
91
|
// register/unregister are called from each row's $effect. They read AND write
|
|
80
92
|
// `entries`, so the read must be untracked — otherwise the calling effect would
|
|
81
93
|
// subscribe to `entries`, and writing it would re-run the effect forever.
|
|
82
|
-
//
|
|
83
|
-
//
|
|
94
|
+
// register() APPENDS in insertion order (O(1)); the DOM sort is deferred to the
|
|
95
|
+
// lazy `orderedEntries`. Disabled rows are registered with disabled:true so
|
|
96
|
+
// navigate() can skip them explicitly, making the skip correct even when the
|
|
97
|
+
// disabled state changes mid-session.
|
|
84
98
|
function register(el: HTMLElement, rowValue: string | undefined, rowDisabled = false): () => void {
|
|
85
99
|
untrack(() => {
|
|
86
|
-
entries =
|
|
100
|
+
entries = [
|
|
87
101
|
...entries.filter((e) => e.el !== el),
|
|
88
102
|
{ el, value: rowValue, disabled: rowDisabled }
|
|
89
|
-
]
|
|
103
|
+
];
|
|
90
104
|
});
|
|
91
105
|
return () => {
|
|
92
106
|
untrack(() => {
|
|
@@ -103,7 +117,8 @@
|
|
|
103
117
|
const entry = entries.find((e) => e.el === tabStopEl);
|
|
104
118
|
if (entry && !entry.disabled) return tabStopEl;
|
|
105
119
|
}
|
|
106
|
-
|
|
120
|
+
// "First enabled row" must be in DOM order, so read the ordered view.
|
|
121
|
+
return orderedEntries.find((e) => !e.disabled)?.el ?? null;
|
|
107
122
|
});
|
|
108
123
|
|
|
109
124
|
// Si la row qui détient le focus DOM devient disabled (in-place, sans unmount),
|
|
@@ -161,38 +176,42 @@
|
|
|
161
176
|
}
|
|
162
177
|
|
|
163
178
|
function navigate(el: HTMLElement, key: string) {
|
|
164
|
-
|
|
165
|
-
|
|
179
|
+
// Keyboard navigation walks rows in VISUAL (DOM) order, so read the lazily
|
|
180
|
+
// sorted view here. This is the first point the deferred sort is forced — the
|
|
181
|
+
// O(n²) register-time sort storm is gone, the sort runs once on demand.
|
|
182
|
+
const ordered = orderedEntries;
|
|
183
|
+
if (ordered.length === 0) return;
|
|
184
|
+
const idx = ordered.findIndex((e) => e.el === el);
|
|
166
185
|
if (idx === -1) return;
|
|
167
186
|
|
|
168
187
|
let targetIdx: number | null = null;
|
|
169
188
|
|
|
170
189
|
if (key === "ArrowDown" || key === "ArrowRight") {
|
|
171
190
|
// Walk forward from current position, find the next non-disabled entry.
|
|
172
|
-
for (let i = idx + 1; i <
|
|
173
|
-
if (!
|
|
191
|
+
for (let i = idx + 1; i < ordered.length; i++) {
|
|
192
|
+
if (!ordered[i].disabled) { targetIdx = i; break; }
|
|
174
193
|
}
|
|
175
194
|
} else if (key === "ArrowUp" || key === "ArrowLeft") {
|
|
176
195
|
// Walk backward from current position, find the previous non-disabled entry.
|
|
177
196
|
for (let i = idx - 1; i >= 0; i--) {
|
|
178
|
-
if (!
|
|
197
|
+
if (!ordered[i].disabled) { targetIdx = i; break; }
|
|
179
198
|
}
|
|
180
199
|
} else if (key === "Home") {
|
|
181
200
|
// First non-disabled entry.
|
|
182
|
-
for (let i = 0; i <
|
|
183
|
-
if (!
|
|
201
|
+
for (let i = 0; i < ordered.length; i++) {
|
|
202
|
+
if (!ordered[i].disabled) { targetIdx = i; break; }
|
|
184
203
|
}
|
|
185
204
|
} else if (key === "End") {
|
|
186
205
|
// Last non-disabled entry.
|
|
187
|
-
for (let i =
|
|
188
|
-
if (!
|
|
206
|
+
for (let i = ordered.length - 1; i >= 0; i--) {
|
|
207
|
+
if (!ordered[i].disabled) { targetIdx = i; break; }
|
|
189
208
|
}
|
|
190
209
|
}
|
|
191
210
|
|
|
192
211
|
// If no target found (all remaining are disabled, or already at boundary), stay put.
|
|
193
212
|
if (targetIdx === null) return;
|
|
194
213
|
|
|
195
|
-
const target =
|
|
214
|
+
const target = ordered[targetIdx]?.el;
|
|
196
215
|
if (target) {
|
|
197
216
|
tabStopEl = target;
|
|
198
217
|
target.focus();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SelectableList.svelte.d.ts","sourceRoot":"","sources":["../src/lib/SelectableList.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,MAAM,MAAM,mBAAmB,GAAG;IAChC,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,KAAK,IAAI,CAAC;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;
|
|
1
|
+
{"version":3,"file":"SelectableList.svelte.d.ts","sourceRoot":"","sources":["../src/lib/SelectableList.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,MAAM,MAAM,mBAAmB,GAAG;IAChC,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,KAAK,IAAI,CAAC;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AA6NJ,QAAA,MAAM,cAAc,yDAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
|
|
@@ -60,6 +60,16 @@
|
|
|
60
60
|
trailing?: Snippet;
|
|
61
61
|
/** Main content. */
|
|
62
62
|
children?: Snippet;
|
|
63
|
+
/**
|
|
64
|
+
* Optional secondary line (the "legend") rendered MUTED and smaller UNDER
|
|
65
|
+
* `children`. When present the content column stacks vertically (a
|
|
66
|
+
* `--hasCaption` modifier); when absent the row stays single-line and
|
|
67
|
+
* byte-identical. The caption joins the row's accessible name by default (the
|
|
68
|
+
* SR reads "label, caption"); wrap it `aria-hidden` if it is purely
|
|
69
|
+
* decorative. MUST NOT contain interactive controls — a row is a single tab
|
|
70
|
+
* stop.
|
|
71
|
+
*/
|
|
72
|
+
caption?: Snippet;
|
|
63
73
|
class?: string;
|
|
64
74
|
};
|
|
65
75
|
</script>
|
|
@@ -77,6 +87,7 @@
|
|
|
77
87
|
leading,
|
|
78
88
|
trailing,
|
|
79
89
|
children,
|
|
90
|
+
caption,
|
|
80
91
|
class: className
|
|
81
92
|
}: SelectableRowProps = $props();
|
|
82
93
|
|
|
@@ -125,6 +136,7 @@
|
|
|
125
136
|
isSelected ? "st-selectableRow--selected" : null,
|
|
126
137
|
disabled ? "st-selectableRow--disabled" : null,
|
|
127
138
|
accentBar ? "st-selectableRow--accentBar" : null,
|
|
139
|
+
caption ? "st-selectableRow--hasCaption" : null,
|
|
128
140
|
className
|
|
129
141
|
]
|
|
130
142
|
.filter(Boolean)
|
|
@@ -190,7 +202,17 @@
|
|
|
190
202
|
{#if leading}
|
|
191
203
|
<span class="st-selectableRow__leading">{@render leading()}</span>
|
|
192
204
|
{/if}
|
|
193
|
-
|
|
205
|
+
{#if caption}
|
|
206
|
+
<!-- Caption present: the content column stacks the primary label over a muted
|
|
207
|
+
second line. Both lines truncate independently (each min-width:0 + ellipsis)
|
|
208
|
+
so a long caption never pushes the row width. -->
|
|
209
|
+
<span class="st-selectableRow__content st-selectableRow__content--stacked">
|
|
210
|
+
<span class="st-selectableRow__label">{@render children?.()}</span>
|
|
211
|
+
<span class="st-selectableRow__caption">{@render caption()}</span>
|
|
212
|
+
</span>
|
|
213
|
+
{:else}
|
|
214
|
+
<span class="st-selectableRow__content">{@render children?.()}</span>
|
|
215
|
+
{/if}
|
|
194
216
|
{#if trailing}
|
|
195
217
|
<span class="st-selectableRow__trailing">{@render trailing()}</span>
|
|
196
218
|
{/if}
|
|
@@ -300,6 +322,36 @@
|
|
|
300
322
|
white-space: nowrap;
|
|
301
323
|
}
|
|
302
324
|
|
|
325
|
+
/* Caption variant (additive). Rows WITHOUT a caption keep the single-line
|
|
326
|
+
`.st-selectableRow__content` above byte-identically (no `--stacked` rule
|
|
327
|
+
applies). `--stacked` overlays a vertical column: the primary `__label` keeps
|
|
328
|
+
its own single-line ellipsis, and the muted `__caption` truncates
|
|
329
|
+
independently so a long legend never pushes the row width. Every leaf falls
|
|
330
|
+
back to a base literal so a theme that emits no
|
|
331
|
+
`--st-component-selectableRow-caption*` renders the caption identically. */
|
|
332
|
+
.st-selectableRow__content--stacked {
|
|
333
|
+
display: flex;
|
|
334
|
+
flex-direction: column;
|
|
335
|
+
/* The column stack drops the inline ellipsis/nowrap (each line truncates on
|
|
336
|
+
its own child); keep min-width:0 so the column can shrink and ellipsize. */
|
|
337
|
+
overflow: visible;
|
|
338
|
+
white-space: normal;
|
|
339
|
+
gap: var(--st-component-selectableRow-captionGap, 0.125rem);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.st-selectableRow__label,
|
|
343
|
+
.st-selectableRow__caption {
|
|
344
|
+
min-width: 0;
|
|
345
|
+
overflow: hidden;
|
|
346
|
+
text-overflow: ellipsis;
|
|
347
|
+
white-space: nowrap;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.st-selectableRow__caption {
|
|
351
|
+
color: var(--st-component-selectableRow-captionColor, var(--st-semantic-text-muted));
|
|
352
|
+
font-size: var(--st-component-selectableRow-captionFontSize, 0.75rem);
|
|
353
|
+
}
|
|
354
|
+
|
|
303
355
|
@media (prefers-reduced-motion: reduce) {
|
|
304
356
|
.st-selectableRow { transition: none; }
|
|
305
357
|
}
|
|
@@ -56,6 +56,16 @@ export type SelectableRowProps = {
|
|
|
56
56
|
trailing?: Snippet;
|
|
57
57
|
/** Main content. */
|
|
58
58
|
children?: Snippet;
|
|
59
|
+
/**
|
|
60
|
+
* Optional secondary line (the "legend") rendered MUTED and smaller UNDER
|
|
61
|
+
* `children`. When present the content column stacks vertically (a
|
|
62
|
+
* `--hasCaption` modifier); when absent the row stays single-line and
|
|
63
|
+
* byte-identical. The caption joins the row's accessible name by default (the
|
|
64
|
+
* SR reads "label, caption"); wrap it `aria-hidden` if it is purely
|
|
65
|
+
* decorative. MUST NOT contain interactive controls — a row is a single tab
|
|
66
|
+
* stop.
|
|
67
|
+
*/
|
|
68
|
+
caption?: Snippet;
|
|
59
69
|
class?: string;
|
|
60
70
|
};
|
|
61
71
|
declare const SelectableRow: import("svelte").Component<SelectableRowProps, {}, "selected">;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SelectableRow.svelte.d.ts","sourceRoot":"","sources":["../src/lib/SelectableRow.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,eAA+B,CAAC;AAEhE,MAAM,MAAM,qBAAqB,GAAG;IAClC,gEAAgE;IAChE,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC;IACvB,wDAAwD;IACxD,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,wIAAwI;IACxI,QAAQ,EAAE,CAAC,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,CAAC,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;IACzF,uDAAuD;IACvD,UAAU,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,OAAO,CAAC;IACzC,iFAAiF;IACjF,SAAS,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,OAAO,CAAC;IACxC,6EAA6E;IAC7E,QAAQ,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,IAAI,CAAC;IACpC,wDAAwD;IACxD,QAAQ,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,IAAI,CAAC;IACpC,gDAAgD;IAChD,QAAQ,EAAE,CAAC,EAAE,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAClD,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;IACvC,iCAAiC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,oCAAoC;IACpC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,mCAAmC;IACnC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,oBAAoB;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;
|
|
1
|
+
{"version":3,"file":"SelectableRow.svelte.d.ts","sourceRoot":"","sources":["../src/lib/SelectableRow.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,eAA+B,CAAC;AAEhE,MAAM,MAAM,qBAAqB,GAAG;IAClC,gEAAgE;IAChE,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC;IACvB,wDAAwD;IACxD,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,wIAAwI;IACxI,QAAQ,EAAE,CAAC,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,CAAC,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;IACzF,uDAAuD;IACvD,UAAU,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,OAAO,CAAC;IACzC,iFAAiF;IACjF,SAAS,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,OAAO,CAAC;IACxC,6EAA6E;IAC7E,QAAQ,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,IAAI,CAAC;IACpC,wDAAwD;IACxD,QAAQ,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,IAAI,CAAC;IACpC,gDAAgD;IAChD,QAAQ,EAAE,CAAC,EAAE,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAClD,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;IACvC,iCAAiC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,oCAAoC;IACpC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,mCAAmC;IACnC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,oBAAoB;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AA2IJ,QAAA,MAAM,aAAa,gEAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|