srcdev-nuxt-components 9.6.4 → 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.
Files changed (27) hide show
  1. package/.claude/skills/component-compliance-checklist.md +9 -1
  2. package/.claude/skills/components/progress-tracker.md +140 -0
  3. package/.claude/skills/components/stepper-list.md +10 -3
  4. package/.claude/skills/components/tabbed-content.md +4 -0
  5. package/.claude/skills/index.md +2 -1
  6. package/.claude/skills/storybook-add-story.md +6 -0
  7. package/.vscode/srcdev-component-progress-tracker.code-snippets +45 -0
  8. package/.vscode/srcdev-component-stepper-list.code-snippets +3 -2
  9. package/.vscode/srcdev-component-tabbed-content.code-snippets +1 -0
  10. package/app/components/01.atoms/tabbed-content/CONSUMER-STYLING.md +5 -1
  11. package/app/components/01.atoms/tabbed-content/TabbedContent.vue +2 -2
  12. package/app/components/01.atoms/tabbed-content/stories/TabbedContent.stories.ts +26 -0
  13. package/app/components/02.molecules/contact-section/tests/__snapshots__/ContactSection.spec.ts.snap +28 -28
  14. package/app/components/02.molecules/progress-tracker/CONSUMER-STYLING.md +116 -0
  15. package/app/components/02.molecules/progress-tracker/ProgressTracker.vue +275 -0
  16. package/app/components/02.molecules/progress-tracker/stories/ProgressTracker.stories.ts +424 -0
  17. package/app/components/02.molecules/progress-tracker/tests/ProgressTracker.spec.ts +253 -0
  18. package/app/components/02.molecules/progress-tracker/tests/__snapshots__/ProgressTracker.spec.ts.snap +46 -0
  19. package/app/components/02.molecules/stepper-list/CONSUMER-STYLING.md +42 -7
  20. package/app/components/02.molecules/stepper-list/StepperList.vue +42 -19
  21. package/app/components/02.molecules/stepper-list/stories/StepperList.stories.ts +288 -2
  22. package/app/components/02.molecules/stepper-list/tests/StepperList.spec.ts +161 -34
  23. package/app/components/02.molecules/stepper-list/tests/__snapshots__/StepperList.spec.ts.snap +40 -40
  24. package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +7 -7
  25. package/app/types/components/index.ts +1 -0
  26. package/app/types/components/progress-tracker.d.ts +6 -0
  27. package/package.json +1 -1
@@ -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.
@@ -0,0 +1,275 @@
1
+ <template>
2
+ <div class="progress-tracker" :class="[elementClasses, { 'is-complete': isComplete }]">
3
+ <div v-if="total" class="progress-tracker-header">
4
+ <p ref="labelEl" class="progress-tracker-label" aria-live="polite" tabindex="-1">{{ headerLabel }}</p>
5
+ <div
6
+ class="progress-tracker-bar"
7
+ role="progressbar"
8
+ aria-valuemin="0"
9
+ :aria-valuemax="total"
10
+ :aria-valuenow="current"
11
+ :aria-valuetext="headerLabel"
12
+ :style="{ '--_progress-tracker-progress': String(current / total) }"
13
+ >
14
+ <span class="progress-tracker-bar-fill"></span>
15
+ </div>
16
+ </div>
17
+
18
+ <StepperList
19
+ v-if="total"
20
+ tag="ol"
21
+ :item-count="total"
22
+ indicator-alignment="top"
23
+ :connected="true"
24
+ class="progress-tracker-list"
25
+ >
26
+ <template v-for="(_, index) in steps" :key="`indicator-${index}`" #[`indicator-${index}`]>
27
+ <span class="progress-tracker-indicator" :class="`is-${stepState(index)}`" aria-hidden="true">
28
+ <Icon v-if="stepState(index) === 'done'" :name="doneIcon" class="progress-tracker-indicator-icon" />
29
+ <template v-else>{{ index + 1 }}</template>
30
+ </span>
31
+ </template>
32
+
33
+ <template v-for="(step, index) in steps" :key="`item-${index}`" #[`item-${index}`]>
34
+ <div class="progress-tracker-step" :class="`is-${stepState(index)}`">
35
+ <component
36
+ :is="headingTag"
37
+ :id="headingId(index)"
38
+ class="progress-tracker-title"
39
+ tabindex="-1"
40
+ :aria-current="stepState(index) === 'current' ? 'step' : undefined"
41
+ >
42
+ {{ step.title }}<span v-if="stepState(index) === 'done'" class="sr-only">, {{ doneLabel }}</span>
43
+ </component>
44
+ <p v-if="step.body" class="progress-tracker-body">{{ step.body }}</p>
45
+ <div v-if="stepState(index) === 'current' && $slots[`step-${index}`]" class="progress-tracker-slot">
46
+ <slot :name="`step-${index}`" :step="step" :index="index" :heading-id="headingId(index)"></slot>
47
+ </div>
48
+ </div>
49
+ </template>
50
+ </StepperList>
51
+ </div>
52
+ </template>
53
+
54
+ <script setup lang="ts">
55
+ import type { ProgressTrackerStep, ProgressTrackerStepState } from "~/types/components";
56
+
57
+ interface Props {
58
+ steps: ProgressTrackerStep[];
59
+ headingTag?: "h2" | "h3" | "h4" | "h5" | "h6";
60
+ stepLabel?: string;
61
+ completeLabel?: string;
62
+ doneLabel?: string;
63
+ doneIcon?: string;
64
+ focusOnChange?: boolean;
65
+ styleClassPassthrough?: string | string[];
66
+ }
67
+
68
+ const props = withDefaults(defineProps<Props>(), {
69
+ headingTag: "h3",
70
+ stepLabel: "Step {current} of {total}",
71
+ completeLabel: "All steps complete",
72
+ doneLabel: "Completed",
73
+ doneIcon: "lucide:check",
74
+ focusOnChange: true,
75
+ styleClassPassthrough: () => [],
76
+ });
77
+
78
+ const model = defineModel<number>({ required: true });
79
+
80
+ const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
81
+
82
+ watch(
83
+ () => props.styleClassPassthrough,
84
+ () => resetElementClasses(props.styleClassPassthrough)
85
+ );
86
+
87
+ const baseId = useId();
88
+ const labelEl = ref<HTMLElement | null>(null);
89
+
90
+ const total = computed(() => props.steps.length);
91
+ const current = computed(() => Math.min(total.value, Math.max(0, Math.floor(Number(model.value)) || 0)));
92
+ const isComplete = computed(() => total.value > 0 && current.value === total.value);
93
+
94
+ const headerLabel = computed(() =>
95
+ isComplete.value
96
+ ? props.completeLabel
97
+ : props.stepLabel.replaceAll("{current}", String(current.value + 1)).replaceAll("{total}", String(total.value))
98
+ );
99
+
100
+ const headingId = (index: number) => `${baseId}-step-${index}`;
101
+
102
+ const stepState = (index: number): ProgressTrackerStepState =>
103
+ index < current.value ? "done" : index === current.value ? "current" : "upcoming";
104
+
105
+ watch(current, async () => {
106
+ if (!props.focusOnChange || !total.value) return;
107
+ await nextTick();
108
+ const target = isComplete.value ? labelEl.value : document.getElementById(headingId(current.value));
109
+ target?.focus();
110
+ });
111
+ </script>
112
+
113
+ <style lang="css">
114
+ @layer components {
115
+ .progress-tracker {
116
+ overflow-wrap: anywhere;
117
+
118
+ .progress-tracker-header {
119
+ display: flex;
120
+ flex-wrap: wrap;
121
+ align-items: center;
122
+ justify-content: space-between;
123
+ gap: 1.2rem 2.4rem;
124
+ margin-block-end: var(--progress-tracker-header-gap, 2.4rem);
125
+ }
126
+
127
+ .progress-tracker-label {
128
+ margin: 0;
129
+ min-inline-size: 0;
130
+ font-family: var(--progress-tracker-label-font-family, inherit);
131
+ font-size: var(--progress-tracker-label-font-size, var(--step-4));
132
+ font-weight: var(--progress-tracker-label-font-weight, 600);
133
+ letter-spacing: var(--progress-tracker-label-letter-spacing, 0.12em);
134
+ text-transform: var(--progress-tracker-label-text-transform, uppercase);
135
+ color: var(--progress-tracker-label-colour, color-mix(in oklch, var(--theme-text) 70%, transparent));
136
+ }
137
+
138
+ .progress-tracker-bar {
139
+ flex: 0 1 var(--progress-tracker-bar-inline-size, 16rem);
140
+ block-size: var(--progress-tracker-bar-height, 0.6rem);
141
+ border-radius: var(--progress-tracker-bar-radius, 99rem);
142
+ background: var(--progress-tracker-track, color-mix(in oklch, var(--theme-text) 15%, transparent));
143
+ overflow: hidden;
144
+ }
145
+
146
+ .progress-tracker-bar-fill {
147
+ display: block;
148
+ block-size: 100%;
149
+ inline-size: calc(var(--_progress-tracker-progress, 0) * 100%);
150
+ border-radius: inherit;
151
+ background: var(--progress-tracker-accent, var(--theme-accent));
152
+ }
153
+
154
+ .progress-tracker-list {
155
+ --stepper-list-gap: var(--progress-tracker-gap, 4rem);
156
+ --stepper-list-padding-block: var(--progress-tracker-step-padding-block, 1.2rem);
157
+ --stepper-list-connector-width: var(--progress-tracker-connector-width, 0.2rem);
158
+ --stepper-list-connector-color: var(
159
+ --progress-tracker-track,
160
+ color-mix(in oklch, var(--theme-text) 15%, transparent)
161
+ );
162
+ --stepper-list-connector-gap: 0;
163
+ --stepper-list-connector-image: none;
164
+ margin: 0;
165
+
166
+ .stepper-list-item:has(.progress-tracker-indicator.is-done) {
167
+ --stepper-list-connector-color: var(--progress-tracker-accent, var(--theme-accent));
168
+ }
169
+ }
170
+
171
+ .progress-tracker-indicator {
172
+ display: grid;
173
+ place-items: center;
174
+ box-sizing: border-box;
175
+ inline-size: var(--progress-tracker-indicator-size, 4.4rem);
176
+ block-size: var(--progress-tracker-indicator-size, 4.4rem);
177
+ border: var(--progress-tracker-indicator-border-width, 0.2rem) solid
178
+ var(--progress-tracker-track, color-mix(in oklch, var(--theme-text) 15%, transparent));
179
+ border-radius: var(--progress-tracker-indicator-radius, 50%);
180
+ font-size: var(--progress-tracker-indicator-font-size, var(--step-4));
181
+ font-weight: 700;
182
+ font-variant-numeric: tabular-nums;
183
+ color: var(--progress-tracker-upcoming-text, color-mix(in oklch, var(--theme-text) 60%, transparent));
184
+
185
+ &.is-done {
186
+ border-color: var(--progress-tracker-accent, var(--theme-accent));
187
+ background: var(--progress-tracker-accent, var(--theme-accent));
188
+ color: var(--progress-tracker-on-accent, var(--theme-on-surface));
189
+ }
190
+
191
+ &.is-current {
192
+ margin-block-start: var(--progress-tracker-card-padding-block, 1.6rem);
193
+ border-color: var(--progress-tracker-accent, var(--theme-accent));
194
+ color: var(--progress-tracker-accent, var(--theme-accent));
195
+ box-shadow: 0 0 0 var(--progress-tracker-ring-width, 0.6rem)
196
+ var(--progress-tracker-ring-colour, color-mix(in oklch, var(--theme-accent) 20%, transparent));
197
+ }
198
+
199
+ .progress-tracker-indicator-icon {
200
+ inline-size: 55%;
201
+ block-size: 55%;
202
+ }
203
+ }
204
+
205
+ .progress-tracker-step {
206
+ display: flex;
207
+ flex-direction: column;
208
+ gap: var(--progress-tracker-content-gap, 0.8rem);
209
+ min-inline-size: 0;
210
+
211
+ &.is-current {
212
+ margin-inline-start: calc(-1 * var(--progress-tracker-card-padding-inline, 2rem));
213
+ padding-block: var(--progress-tracker-card-padding-block, 1.6rem);
214
+ padding-inline: var(--progress-tracker-card-padding-inline, 2rem);
215
+ border-radius: var(--progress-tracker-card-radius, 1.6rem);
216
+ background: var(--progress-tracker-card-surface, var(--theme-surface-subtle));
217
+ box-shadow: var(
218
+ --progress-tracker-card-shadow,
219
+ 0 0.2rem 0.4rem rgb(0 0 0 / 0.04),
220
+ 0 1.2rem 3.2rem -1.2rem rgb(0 0 0 / 0.18)
221
+ );
222
+ }
223
+
224
+ &.is-upcoming {
225
+ .progress-tracker-title,
226
+ .progress-tracker-body {
227
+ color: var(--progress-tracker-upcoming-text, color-mix(in oklch, var(--theme-text) 60%, transparent));
228
+ }
229
+ }
230
+ }
231
+
232
+ .progress-tracker-title {
233
+ display: -webkit-box;
234
+ -webkit-box-orient: vertical;
235
+ -webkit-line-clamp: var(--progress-tracker-title-line-clamp, none);
236
+ line-clamp: var(--progress-tracker-title-line-clamp, none);
237
+ overflow: hidden;
238
+ margin: 0;
239
+ margin-block-start: max(0px, calc((var(--progress-tracker-indicator-size, 4.4rem) - 1lh) / 2));
240
+ font-size: var(--progress-tracker-title-font-size, var(--step-5));
241
+ font-weight: var(--progress-tracker-title-font-weight, 600);
242
+ line-height: 1.3;
243
+ color: var(--progress-tracker-title-colour, var(--theme-text));
244
+ }
245
+
246
+ .progress-tracker-body {
247
+ display: -webkit-box;
248
+ -webkit-box-orient: vertical;
249
+ -webkit-line-clamp: var(--progress-tracker-body-line-clamp, none);
250
+ line-clamp: var(--progress-tracker-body-line-clamp, none);
251
+ overflow: hidden;
252
+ margin: 0;
253
+ font-size: var(--progress-tracker-body-font-size, var(--step-4));
254
+ line-height: 1.5;
255
+ color: var(--progress-tracker-body-colour, color-mix(in oklch, var(--theme-text) 80%, transparent));
256
+ }
257
+
258
+ .progress-tracker-slot {
259
+ margin-block-start: var(--progress-tracker-slot-gap, 0.8rem);
260
+ }
261
+
262
+ @media (prefers-reduced-motion: no-preference) {
263
+ .progress-tracker-bar-fill {
264
+ transition: inline-size var(--progress-tracker-transition-duration, 200ms) ease;
265
+ }
266
+
267
+ .progress-tracker-indicator,
268
+ .progress-tracker-step {
269
+ transition-property: background-color, border-color, box-shadow, color;
270
+ transition-duration: var(--progress-tracker-transition-duration, 200ms);
271
+ }
272
+ }
273
+ }
274
+ }
275
+ </style>