srcdev-nuxt-components 9.6.3 → 9.6.5
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/.claude/rules/consumer/vue-no-self-closing.md +3 -2
- package/.claude/skills/component-compliance-checklist.md +17 -3
- package/.claude/skills/component-inline-action-button.md +33 -2
- package/.claude/skills/components/cookie-consent-banner.md +9 -6
- package/.claude/skills/components/input-copy.md +47 -100
- package/.claude/skills/components/progress-tracker.md +140 -0
- package/.claude/skills/components/stepper-list.md +10 -3
- package/.claude/skills/components/tabbed-content.md +4 -0
- package/.claude/skills/composable-cookie-consent.md +1 -1
- package/.claude/skills/index.md +4 -3
- package/.claude/skills/storybook-add-story.md +14 -0
- package/.claude/skills/testing-add-unit-test.md +19 -0
- package/.claude/skills/theming-component-token-pattern.md +1 -1
- package/.vscode/srcdev-component-cookie-consent-banner.code-snippets +7 -5
- package/.vscode/srcdev-component-input-copy.code-snippets +24 -42
- package/.vscode/srcdev-component-progress-tracker.code-snippets +45 -0
- package/.vscode/srcdev-component-stepper-list.code-snippets +3 -2
- package/.vscode/srcdev-component-tabbed-content.code-snippets +1 -0
- package/app/components/01.atoms/tabbed-content/CONSUMER-STYLING.md +5 -1
- package/app/components/01.atoms/tabbed-content/TabbedContent.vue +2 -2
- package/app/components/01.atoms/tabbed-content/stories/TabbedContent.stories.ts +26 -0
- package/app/components/02.molecules/contact-section/tests/__snapshots__/ContactSection.spec.ts.snap +28 -28
- package/app/components/02.molecules/cookie-consent-banner/CONSUMER-STYLING.md +13 -4
- package/app/components/02.molecules/cookie-consent-banner/CookieConsentBanner.vue +23 -14
- package/app/components/02.molecules/cookie-consent-banner/stories/CookieConsentBanner.stories.ts +106 -5
- package/app/components/02.molecules/cookie-consent-banner/tests/CookieConsentBanner.spec.ts +33 -11
- package/app/components/02.molecules/input-copy/CONSUMER-STYLING.md +82 -124
- package/app/components/02.molecules/input-copy/InputCopy.vue +110 -85
- package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +204 -47
- package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +147 -171
- package/app/components/02.molecules/progress-tracker/CONSUMER-STYLING.md +116 -0
- package/app/components/02.molecules/progress-tracker/ProgressTracker.vue +275 -0
- package/app/components/02.molecules/progress-tracker/stories/ProgressTracker.stories.ts +424 -0
- package/app/components/02.molecules/progress-tracker/tests/ProgressTracker.spec.ts +253 -0
- package/app/components/02.molecules/progress-tracker/tests/__snapshots__/ProgressTracker.spec.ts.snap +46 -0
- package/app/components/02.molecules/stepper-list/CONSUMER-STYLING.md +42 -7
- package/app/components/02.molecules/stepper-list/StepperList.vue +42 -19
- package/app/components/02.molecules/stepper-list/stories/StepperList.stories.ts +288 -2
- package/app/components/02.molecules/stepper-list/tests/StepperList.spec.ts +161 -34
- package/app/components/02.molecules/stepper-list/tests/__snapshots__/StepperList.spec.ts.snap +40 -40
- package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +7 -7
- package/app/types/components/cookie-consent-banner.d.ts +2 -0
- package/app/types/components/index.ts +1 -0
- package/app/types/components/progress-tracker.d.ts +6 -0
- package/package.json +1 -1
|
@@ -1,222 +1,198 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import {
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { nextTick } from "vue";
|
|
3
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
4
|
+
import type { VueWrapper } from "@vue/test-utils";
|
|
3
5
|
import InputCopy from "../InputCopy.vue";
|
|
4
6
|
|
|
7
|
+
let wrapper: VueWrapper | undefined;
|
|
8
|
+
let writeText: ReturnType<typeof vi.fn>;
|
|
9
|
+
|
|
10
|
+
async function mountCopy(props: Record<string, unknown> = {}) {
|
|
11
|
+
wrapper = await mountSuspended(InputCopy, { props: { value: "test-key-123", ...props } });
|
|
12
|
+
return wrapper;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function clickCopy(w: VueWrapper) {
|
|
16
|
+
await w.find("button").trigger("click");
|
|
17
|
+
await nextTick();
|
|
18
|
+
await nextTick();
|
|
19
|
+
}
|
|
20
|
+
|
|
5
21
|
describe("InputCopy", () => {
|
|
6
22
|
beforeEach(() => {
|
|
7
|
-
vi.
|
|
23
|
+
writeText = vi.fn(() => Promise.resolve());
|
|
24
|
+
Object.defineProperty(navigator, "clipboard", { value: { writeText }, configurable: true });
|
|
8
25
|
});
|
|
9
26
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
label: "Test key",
|
|
15
|
-
},
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
const input = wrapper.find("input");
|
|
19
|
-
expect(input.element.value).toBe("test-key-123");
|
|
20
|
-
expect(input.element.readOnly).toBe(true);
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
wrapper?.unmount();
|
|
29
|
+
wrapper = undefined;
|
|
30
|
+
vi.restoreAllMocks();
|
|
21
31
|
});
|
|
22
32
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
describe("rendering", () => {
|
|
34
|
+
it("renders a readonly input with the value", async () => {
|
|
35
|
+
const w = await mountCopy();
|
|
36
|
+
const input = w.find("input").element as HTMLInputElement;
|
|
37
|
+
expect(input.value).toBe("test-key-123");
|
|
38
|
+
expect(input.readOnly).toBe(true);
|
|
29
39
|
});
|
|
30
40
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
props: {
|
|
37
|
-
value: "test-key",
|
|
38
|
-
description: undefined,
|
|
39
|
-
},
|
|
41
|
+
it("renders the root class and styleClassPassthrough classes", async () => {
|
|
42
|
+
const w = await mountCopy({ styleClassPassthrough: ["custom-class", "another-class"] });
|
|
43
|
+
expect(w.classes()).toContain("input-copy");
|
|
44
|
+
expect(w.classes()).toContain("custom-class");
|
|
45
|
+
expect(w.classes()).toContain("another-class");
|
|
40
46
|
});
|
|
41
47
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const wrapper = mount(InputCopy, {
|
|
48
|
-
props: {
|
|
49
|
-
value: "test-key",
|
|
50
|
-
buttonText: "Copy",
|
|
51
|
-
},
|
|
48
|
+
it("updates passthrough classes when the prop changes", async () => {
|
|
49
|
+
const w = await mountCopy({ styleClassPassthrough: ["first"] });
|
|
50
|
+
await w.setProps({ styleClassPassthrough: ["second"] });
|
|
51
|
+
expect(w.classes()).toContain("second");
|
|
52
|
+
expect(w.classes()).not.toContain("first");
|
|
52
53
|
});
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
value: "test-key",
|
|
62
|
-
buttonText: "Copy to clipboard",
|
|
63
|
-
},
|
|
55
|
+
it("renders the copy button as an inline InputButton", async () => {
|
|
56
|
+
const w = await mountCopy();
|
|
57
|
+
const button = w.find("button");
|
|
58
|
+
expect(button.classes()).toContain("input-button");
|
|
59
|
+
expect(button.classes()).toContain("inline");
|
|
60
|
+
expect(button.classes()).toContain("input-copy__button");
|
|
61
|
+
expect(button.attributes("type")).toBe("button");
|
|
64
62
|
});
|
|
65
63
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const wrapper = mount(InputCopy, {
|
|
72
|
-
props: {
|
|
73
|
-
value: "test-key",
|
|
74
|
-
isDisabled: true,
|
|
75
|
-
},
|
|
64
|
+
it("renders default and custom button text", async () => {
|
|
65
|
+
const w = await mountCopy();
|
|
66
|
+
expect(w.find("button").text()).toContain("Copy");
|
|
67
|
+
await w.setProps({ buttonText: "Copy to clipboard" });
|
|
68
|
+
expect(w.find("button").text()).toContain("Copy to clipboard");
|
|
76
69
|
});
|
|
77
70
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const wrapper = mount(InputCopy, {
|
|
84
|
-
props: {
|
|
85
|
-
value: "test-key",
|
|
86
|
-
isDisabled: false,
|
|
87
|
-
},
|
|
71
|
+
it("renders the description only when provided", async () => {
|
|
72
|
+
const w = await mountCopy();
|
|
73
|
+
expect(w.find(".input-copy__description").exists()).toBe(false);
|
|
74
|
+
await w.setProps({ description: "This is your license key." });
|
|
75
|
+
expect(w.find(".input-copy__description").text()).toBe("This is your license key.");
|
|
88
76
|
});
|
|
89
77
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
it("has proper aria-label on input from label prop", () => {
|
|
95
|
-
const wrapper = mount(InputCopy, {
|
|
96
|
-
props: {
|
|
97
|
-
value: "test-key",
|
|
98
|
-
label: "License key",
|
|
99
|
-
},
|
|
78
|
+
it("renders the icon in the button", async () => {
|
|
79
|
+
const w = await mountCopy();
|
|
80
|
+
expect(w.find("button .input-copy__icon").exists()).toBe(true);
|
|
100
81
|
});
|
|
101
|
-
|
|
102
|
-
const input = wrapper.find("input");
|
|
103
|
-
expect(input.attributes("aria-label")).toBe("License key");
|
|
104
82
|
});
|
|
105
83
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
},
|
|
84
|
+
describe("id and name", () => {
|
|
85
|
+
it("auto-generates an id and mirrors it as the name", async () => {
|
|
86
|
+
const w = await mountCopy();
|
|
87
|
+
const input = w.find("input");
|
|
88
|
+
expect(input.attributes("id")).toBeTruthy();
|
|
89
|
+
expect(input.attributes("name")).toBe(input.attributes("id"));
|
|
113
90
|
});
|
|
114
91
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
props: {
|
|
122
|
-
value: "test-key",
|
|
123
|
-
label: "API token",
|
|
124
|
-
},
|
|
92
|
+
it("uses the provided id, and name over the id", async () => {
|
|
93
|
+
const w = await mountCopy({ id: "license-key-input" });
|
|
94
|
+
expect(w.find("input").attributes("id")).toBe("license-key-input");
|
|
95
|
+
expect(w.find("input").attributes("name")).toBe("license-key-input");
|
|
96
|
+
await w.setProps({ name: "licenseKey" });
|
|
97
|
+
expect(w.find("input").attributes("name")).toBe("licenseKey");
|
|
125
98
|
});
|
|
126
|
-
|
|
127
|
-
const button = wrapper.find("button");
|
|
128
|
-
expect(button.attributes("aria-label")).toContain("Copy");
|
|
129
|
-
expect(button.attributes("aria-label")).toContain("API token");
|
|
130
99
|
});
|
|
131
100
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
101
|
+
describe("accessibility", () => {
|
|
102
|
+
it("labels the input from label, with ariaLabel taking precedence", async () => {
|
|
103
|
+
const w = await mountCopy({ label: "License key" });
|
|
104
|
+
expect(w.find("input").attributes("aria-label")).toBe("License key");
|
|
105
|
+
await w.setProps({ ariaLabel: "Unique license identifier" });
|
|
106
|
+
expect(w.find("input").attributes("aria-label")).toBe("Unique license identifier");
|
|
137
107
|
});
|
|
138
108
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
it("input wrapper contains both input and button", () => {
|
|
144
|
-
const wrapper = mount(InputCopy, {
|
|
145
|
-
props: {
|
|
146
|
-
value: "test-key",
|
|
147
|
-
},
|
|
109
|
+
it("builds a default button aria-label from the label", async () => {
|
|
110
|
+
const w = await mountCopy({ label: "API token" });
|
|
111
|
+
expect(w.find("button").attributes("aria-label")).toBe("Copy API token to clipboard");
|
|
148
112
|
});
|
|
149
113
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const button = wrapper_el.find("button");
|
|
114
|
+
it("uses buttonAriaLabel when provided", async () => {
|
|
115
|
+
const w = await mountCopy({ label: "API token", buttonAriaLabel: "API-Token kopieren" });
|
|
116
|
+
expect(w.find("button").attributes("aria-label")).toBe("API-Token kopieren");
|
|
117
|
+
});
|
|
155
118
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
119
|
+
it("links the description to the input with aria-describedby", async () => {
|
|
120
|
+
const w = await mountCopy({ id: "key", description: "Keep it safe." });
|
|
121
|
+
expect(w.find("input").attributes("aria-describedby")).toBe("key-description");
|
|
122
|
+
expect(w.find("#key-description").text()).toBe("Keep it safe.");
|
|
123
|
+
});
|
|
159
124
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
value: "test-key",
|
|
164
|
-
},
|
|
125
|
+
it("has no aria-describedby without a description", async () => {
|
|
126
|
+
const w = await mountCopy();
|
|
127
|
+
expect(w.find("input").attributes("aria-describedby")).toBeUndefined();
|
|
165
128
|
});
|
|
166
129
|
|
|
167
|
-
|
|
130
|
+
it("has an empty status region until a copy succeeds", async () => {
|
|
131
|
+
const w = await mountCopy();
|
|
132
|
+
expect(w.find("[role='status']").text()).toBe("");
|
|
133
|
+
});
|
|
168
134
|
});
|
|
169
135
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
styleClassPassthrough: ["custom-class", "another-class"],
|
|
175
|
-
},
|
|
136
|
+
describe("disabled", () => {
|
|
137
|
+
it("disables the button when isDisabled is true", async () => {
|
|
138
|
+
const w = await mountCopy({ isDisabled: true });
|
|
139
|
+
expect(w.find("button").attributes("disabled")).toBeDefined();
|
|
176
140
|
});
|
|
177
141
|
|
|
178
|
-
|
|
179
|
-
|
|
142
|
+
it("leaves the button enabled by default", async () => {
|
|
143
|
+
const w = await mountCopy();
|
|
144
|
+
expect(w.find("button").attributes("disabled")).toBeUndefined();
|
|
145
|
+
});
|
|
180
146
|
});
|
|
181
147
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
148
|
+
describe("copying", () => {
|
|
149
|
+
it("writes the value to the clipboard and emits copy and copied", async () => {
|
|
150
|
+
const w = await mountCopy();
|
|
151
|
+
await clickCopy(w);
|
|
152
|
+
expect(writeText).toHaveBeenCalledWith("test-key-123");
|
|
153
|
+
expect(w.emitted("copy")).toEqual([["test-key-123"]]);
|
|
154
|
+
expect(w.emitted("copied")).toEqual([["test-key-123"]]);
|
|
187
155
|
});
|
|
188
156
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
it("uses the provided id prop instead of auto-generating one", () => {
|
|
197
|
-
const wrapper = mount(InputCopy, {
|
|
198
|
-
props: {
|
|
199
|
-
value: "test-key",
|
|
200
|
-
id: "license-key-input",
|
|
201
|
-
},
|
|
157
|
+
it("shows copiedText, announces it and adds the copied class", async () => {
|
|
158
|
+
const w = await mountCopy({ copiedText: "Kopiert!" });
|
|
159
|
+
await clickCopy(w);
|
|
160
|
+
expect(w.find("button").text()).toContain("Kopiert!");
|
|
161
|
+
expect(w.find("[role='status']").text()).toBe("Kopiert!");
|
|
162
|
+
expect(w.classes()).toContain("input-copy-copied");
|
|
202
163
|
});
|
|
203
164
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
165
|
+
it("keeps the button focusable but aria-disabled while copied, ignoring repeat clicks", async () => {
|
|
166
|
+
const w = await mountCopy();
|
|
167
|
+
await clickCopy(w);
|
|
168
|
+
const button = w.find("button");
|
|
169
|
+
expect(button.attributes("disabled")).toBeUndefined();
|
|
170
|
+
expect(button.attributes("aria-disabled")).toBe("true");
|
|
171
|
+
await clickCopy(w);
|
|
172
|
+
expect(writeText).toHaveBeenCalledOnce();
|
|
173
|
+
});
|
|
208
174
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
175
|
+
it("resets after copiedDuration", async () => {
|
|
176
|
+
const w = await mountCopy({ copiedDuration: 1000 });
|
|
177
|
+
await clickCopy(w);
|
|
178
|
+
vi.advanceTimersByTime(999);
|
|
179
|
+
await nextTick();
|
|
180
|
+
expect(w.classes()).toContain("input-copy-copied");
|
|
181
|
+
vi.advanceTimersByTime(1);
|
|
182
|
+
await nextTick();
|
|
183
|
+
expect(w.classes()).not.toContain("input-copy-copied");
|
|
184
|
+
expect(w.find("button").text()).toContain("Copy");
|
|
185
|
+
expect(w.find("button").attributes("aria-disabled")).toBe("false");
|
|
216
186
|
});
|
|
217
187
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
188
|
+
it("logs and stays in the resting state when the clipboard write fails", async () => {
|
|
189
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
190
|
+
writeText.mockImplementation(() => Promise.reject(new Error("denied")));
|
|
191
|
+
const w = await mountCopy();
|
|
192
|
+
await clickCopy(w);
|
|
193
|
+
expect(errorSpy).toHaveBeenCalled();
|
|
194
|
+
expect(w.emitted("copied")).toBeUndefined();
|
|
195
|
+
expect(w.classes()).not.toContain("input-copy-copied");
|
|
196
|
+
});
|
|
221
197
|
});
|
|
222
198
|
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# ProgressTracker — Consumer Styling Guide
|
|
2
|
+
|
|
3
|
+
## Public token API
|
|
4
|
+
|
|
5
|
+
All `--progress-tracker-*` tokens are the stable override surface. Set them on an ancestor you
|
|
6
|
+
own, or on a class added with `:style-class-passthrough`.
|
|
7
|
+
|
|
8
|
+
### Colours
|
|
9
|
+
|
|
10
|
+
| Token | Default | Controls |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| `--progress-tracker-accent` | `var(--theme-accent)` | Done indicator fill, current indicator outline and number, connectors below done steps, progress bar fill |
|
|
13
|
+
| `--progress-tracker-on-accent` | `var(--theme-on-surface)` | Tick colour on the done indicator; keep it in contrast with the accent |
|
|
14
|
+
| `--progress-tracker-track` | `--theme-text` mixed to 15% | Progress bar track, upcoming indicator outline, connectors below current and upcoming steps |
|
|
15
|
+
| `--progress-tracker-ring-colour` | `--theme-accent` mixed to 20% | Halo around the current indicator |
|
|
16
|
+
| `--progress-tracker-upcoming-text` | `--theme-text` mixed to 60% | Upcoming step numbers, titles and bodies |
|
|
17
|
+
| `--progress-tracker-title-colour` | `var(--theme-text)` | Done and current step titles |
|
|
18
|
+
| `--progress-tracker-body-colour` | `--theme-text` mixed to 80% | Done and current step bodies |
|
|
19
|
+
| `--progress-tracker-label-colour` | `--theme-text` mixed to 70% | Header label |
|
|
20
|
+
|
|
21
|
+
### Header
|
|
22
|
+
|
|
23
|
+
| Token | Default | Controls |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `--progress-tracker-header-gap` | `2.4rem` | Space between the header and the first step |
|
|
26
|
+
| `--progress-tracker-label-font-family` | `inherit` | Header label font (e.g. a monospace stack) |
|
|
27
|
+
| `--progress-tracker-label-font-size` | `var(--step-4)` | Header label size |
|
|
28
|
+
| `--progress-tracker-label-font-weight` | `600` | Header label weight |
|
|
29
|
+
| `--progress-tracker-label-letter-spacing` | `0.12em` | Header label tracking |
|
|
30
|
+
| `--progress-tracker-label-text-transform` | `uppercase` | Header label case; `none` keeps the prop's casing |
|
|
31
|
+
| `--progress-tracker-bar-inline-size` | `16rem` | Preferred bar width; it shrinks first and wraps below the label when space runs out |
|
|
32
|
+
| `--progress-tracker-bar-height` | `0.6rem` | Bar thickness |
|
|
33
|
+
| `--progress-tracker-bar-radius` | `99rem` | Bar corner rounding |
|
|
34
|
+
|
|
35
|
+
### Steps and indicators
|
|
36
|
+
|
|
37
|
+
| Token | Default | Controls |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `--progress-tracker-gap` | `4rem` | Space between the indicator column and the step content |
|
|
40
|
+
| `--progress-tracker-step-padding-block` | `1.2rem` | Vertical padding of each step row |
|
|
41
|
+
| `--progress-tracker-connector-width` | `0.2rem` | Connector line thickness |
|
|
42
|
+
| `--progress-tracker-indicator-size` | `4.4rem` | Indicator width and height; titles are centred against it |
|
|
43
|
+
| `--progress-tracker-indicator-border-width` | `0.2rem` | Indicator outline width |
|
|
44
|
+
| `--progress-tracker-indicator-radius` | `50%` | Indicator shape; `0.8rem` gives rounded squares |
|
|
45
|
+
| `--progress-tracker-indicator-font-size` | `var(--step-4)` | Step number size |
|
|
46
|
+
| `--progress-tracker-ring-width` | `0.6rem` | Halo width around the current indicator |
|
|
47
|
+
| `--progress-tracker-content-gap` | `0.8rem` | Space between title, body and slot content in a step |
|
|
48
|
+
| `--progress-tracker-slot-gap` | `0.8rem` | Extra space above the `#step-{n}` slot content |
|
|
49
|
+
| `--progress-tracker-title-font-size` | `var(--step-5)` | Step title size |
|
|
50
|
+
| `--progress-tracker-title-font-weight` | `600` | Step title weight |
|
|
51
|
+
| `--progress-tracker-title-line-clamp` | `none` | Lines before a title is cut off with an ellipsis (`1` is single-line) |
|
|
52
|
+
| `--progress-tracker-body-font-size` | `var(--step-4)` | Step body size |
|
|
53
|
+
| `--progress-tracker-body-line-clamp` | `none` | Lines before a body is cut off with an ellipsis |
|
|
54
|
+
|
|
55
|
+
### Focus card (current step)
|
|
56
|
+
|
|
57
|
+
| Token | Default | Controls |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| `--progress-tracker-card-surface` | `var(--theme-surface-subtle)` | Card background; set `#ffffff` (or similar) to lift it off a grey panel |
|
|
60
|
+
| `--progress-tracker-card-shadow` | soft two-layer shadow | Card shadow; `none` for a flat card |
|
|
61
|
+
| `--progress-tracker-card-radius` | `1.6rem` | Card corner rounding |
|
|
62
|
+
| `--progress-tracker-card-padding-block` | `1.6rem` | Card top and bottom padding; the current indicator moves down by the same amount to stay level with the title |
|
|
63
|
+
| `--progress-tracker-card-padding-inline` | `2rem` | Card side padding; the card bleeds left by the same amount so its text lines up with the other steps. Clearance between the current ring and the card is `--progress-tracker-gap` minus this minus `--progress-tracker-ring-width` (1.4rem by default), so raise the gap with it |
|
|
64
|
+
|
|
65
|
+
### Motion
|
|
66
|
+
|
|
67
|
+
| Token | Default | Controls |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| `--progress-tracker-transition-duration` | `200ms` | Bar fill and state colour transitions. Off under `prefers-reduced-motion: reduce` |
|
|
70
|
+
|
|
71
|
+
Private (not public API): `--_progress-tracker-progress`, the done fraction the component sets
|
|
72
|
+
inline on the bar.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## State hooks
|
|
77
|
+
|
|
78
|
+
| Hook | Element | When |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `.is-complete` | root | The model equals `steps.length` |
|
|
81
|
+
| `.is-done`, `.is-current`, `.is-upcoming` | `.progress-tracker-step` and `.progress-tracker-indicator` | That step's state |
|
|
82
|
+
| `aria-current="step"` | `.progress-tracker-title` | The current step |
|
|
83
|
+
|
|
84
|
+
Inner classes: `.progress-tracker-header`, `.progress-tracker-label`, `.progress-tracker-bar`,
|
|
85
|
+
`.progress-tracker-bar-fill`, `.progress-tracker-list` (the StepperList root),
|
|
86
|
+
`.progress-tracker-indicator`, `.progress-tracker-indicator-icon`, `.progress-tracker-step`,
|
|
87
|
+
`.progress-tracker-title`, `.progress-tracker-body`, `.progress-tracker-slot`.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Local overrides
|
|
92
|
+
|
|
93
|
+
Set the tokens above on an element you own (a page or section class, or a class added with
|
|
94
|
+
`:style-class-passthrough`): they inherit down into the component. Keep the block **unlayered** (no
|
|
95
|
+
`@layer` wrapper) so it beats the library's `@layer components`. If your own file uses
|
|
96
|
+
`<style scoped>`, tokens set on your element still work, but selectors that reach inside the component need
|
|
97
|
+
`:deep()`. Patterns and examples: `.claude/skills/component-local-style-override.md`.
|
|
98
|
+
|
|
99
|
+
**Caveat: StepperList tokens.** The list is a `StepperList`, and ProgressTracker sets its
|
|
100
|
+
`--stepper-list-*` tokens on `.progress-tracker-list` from its own tokens. A `--stepper-list-*`
|
|
101
|
+
value set on an ancestor never reaches it; use the `--progress-tracker-*` equivalents
|
|
102
|
+
(`-gap`, `-step-padding-block`, `-connector-width`, `-accent`, `-track`).
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Class passthrough
|
|
107
|
+
|
|
108
|
+
`style-class-passthrough` adds classes to the root `div`. Tokens set on a passthrough class apply to
|
|
109
|
+
the whole tracker. Reactive after mount.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Notes
|
|
114
|
+
|
|
115
|
+
- Title centring against the indicator uses the `lh` unit. Safari before 16.4 ignores it and the
|
|
116
|
+
title sits level with the top of the indicator instead.
|