srcdev-nuxt-components 9.6.4 → 9.6.6
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/a11y-no-focus-steal.md +14 -0
- package/.claude/skills/component-compliance-checklist.md +14 -2
- package/.claude/skills/components/display-toast.md +6 -3
- package/.claude/skills/components/progress-tracker.md +140 -0
- package/.claude/skills/components/social-icons-list.md +32 -5
- package/.claude/skills/components/stepper-list.md +10 -3
- package/.claude/skills/components/tabbed-content.md +4 -0
- package/.claude/skills/css-dynamic-border-radius.md +114 -0
- package/.claude/skills/index.md +4 -2
- package/.claude/skills/storybook-add-story.md +12 -0
- package/.vscode/srcdev-component-display-toast.code-snippets +6 -8
- package/.vscode/srcdev-component-progress-tracker.code-snippets +45 -0
- package/.vscode/srcdev-component-social-icons-list.code-snippets +14 -0
- package/.vscode/srcdev-component-stepper-list.code-snippets +3 -2
- package/.vscode/srcdev-component-tabbed-content.code-snippets +1 -0
- package/.vscode/srcdev-utility-dynamic-border-radius.code-snippets +31 -0
- package/app/assets/styles/setup/06.utility-classes/_dynamic-border-radius.css +27 -0
- package/app/assets/styles/setup/06.utility-classes/index.css +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/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/social-icons-list/CONSUMER-STYLING.md +97 -8
- package/app/components/02.molecules/social-icons-list/SocialIconsList.vue +80 -14
- package/app/components/02.molecules/social-icons-list/stories/SocialIconsList.stories.ts +230 -2
- package/app/components/02.molecules/social-icons-list/stories/social-icons-list-recipes.css +108 -0
- package/app/components/02.molecules/social-icons-list/tests/SocialIconsList.spec.ts +123 -52
- package/app/components/02.molecules/social-icons-list/tests/__snapshots__/SocialIconsList.spec.ts.snap +10 -4
- 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/02.molecules/toast/CONSUMER-STYLING.md +9 -3
- package/app/components/02.molecules/toast/DisplayToast.vue +10 -10
- package/app/components/02.molecules/toast/DisplayToastProvider.vue +12 -4
- package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +200 -2
- package/app/components/02.molecules/toast/tests/DisplayToast.spec.ts +33 -12
- package/app/components/02.molecules/toast/tests/DisplayToastProvider.spec.ts +20 -0
- package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +7 -7
- package/app/types/components/index.ts +1 -0
- package/app/types/components/progress-tracker.d.ts +6 -0
- package/app/types/components/social-icons-list.d.ts +9 -0
- package/package.json +1 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "app/**/*.vue"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Status messages never take focus
|
|
7
|
+
|
|
8
|
+
Never `.focus()` a toast, banner or other status message when it appears. Announce it with `role`
|
|
9
|
+
+ `aria-live`, keep it reachable with `tabindex="0"`, and listen for Escape on the document while
|
|
10
|
+
it's shown (skipping `event.defaultPrevented`).
|
|
11
|
+
|
|
12
|
+
Why: WCAG 4.1.3 says status messages are announced without receiving focus, and moving focus
|
|
13
|
+
interrupts typing. Chrome also counts script focus before any pointer input as `:focus-visible`,
|
|
14
|
+
so `:focus:not(:focus-visible)` can't hide the ring (`DisplayToast`, 2026-10-16).
|
|
@@ -130,7 +130,10 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
|
|
|
130
130
|
number of items
|
|
131
131
|
- emoji (including at the start of anything that takes initials or first letters), HTML-like
|
|
132
132
|
text (must render as text), right-to-left text
|
|
133
|
-
- out-of-range or odd numbers (0, negative, above the maximum, fractional, very large)
|
|
133
|
+
- out-of-range or odd numbers (0, negative, above the maximum, fractional, very large). A
|
|
134
|
+
count prop used as a `v-for` range (`itemCount`) needs flooring and clamping in a
|
|
135
|
+
`computed()`: Vue renders nothing for a fractional or negative range and only warns in dev
|
|
136
|
+
(`StepperList`, 2026-10-15)
|
|
134
137
|
- broken image or link URLs
|
|
135
138
|
Describe what to check in the story's docs description, and look at it at every
|
|
136
139
|
`CanvasSwitcher` width. Fixes usually mean `overflow-wrap: anywhere`, `min-inline-size: 0` on
|
|
@@ -175,14 +178,20 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
|
|
|
175
178
|
stories/tests up one level, fix relative imports); `pathPrefix: false` keeps the auto-import
|
|
176
179
|
names unchanged. All existing `variants/` folders were flattened 2026-09-25.
|
|
177
180
|
8. **VS Code snippet** — create or update `.vscode/srcdev-component-{family}.code-snippets`,
|
|
178
|
-
covering the control and its Field wrapper in one file.
|
|
181
|
+
covering the control and its Field wrapper in one file. For a teleported component, a CSS
|
|
182
|
+
override scaffold must not nest its selector under a page class: it never matches (the
|
|
183
|
+
`DisplayToast` snippet did, fixed 2026-10-16).
|
|
179
184
|
9. **Accessibility** — check for gaps beyond what already-passing tests would catch:
|
|
180
185
|
- Interactive controls (buttons, toggles, custom form-like widgets) have an accessible name —
|
|
181
186
|
visible text, `aria-label`, or `aria-labelledby`.
|
|
187
|
+
- A `<ul>`/`<ol>` styled with `list-style: none` gets `role="list"`, or Safari/VoiceOver stops
|
|
188
|
+
announcing it as a list (`StepperList`, 2026-10-15).
|
|
182
189
|
- Keyboard behaviour matches what's actually implemented — no keydown case that only calls
|
|
183
190
|
`event.preventDefault()` with a comment like "could add this later", and no screen-reader
|
|
184
191
|
copy (visible or `sr-only`) that describes an interaction the component doesn't actually
|
|
185
192
|
perform. Cross-check every claim in the copy against the handler that's supposed to back it.
|
|
193
|
+
- A toast, banner or status message doesn't `.focus()` itself when it appears (rule
|
|
194
|
+
`consumer/a11y-no-focus-steal.md`).
|
|
186
195
|
- Focus is visible (`:focus-visible`, not a suppressed outline) on anything focusable.
|
|
187
196
|
- Live-updating or auto-advancing content (carousels, marquees, auto-dismissing toasts) has a
|
|
188
197
|
way to pause it, per WCAG 2.2.2 — a visible control, or pause-on-hover/focus at minimum.
|
|
@@ -253,6 +262,9 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
|
|
|
253
262
|
the edits in private memory. Note that an inline-size container can't size itself from its
|
|
254
263
|
content: say so in `CONSUMER-STYLING.md` (`ProfileSection`).
|
|
255
264
|
- Grid tracks that hold text are `minmax(0, …)` so an unbroken string can't widen the grid.
|
|
265
|
+
- Layout measured in script (connector lines, indicators) re-measures when any measured item
|
|
266
|
+
resizes or changes class, not only when the container resizes (`measured-layout-observers.md`,
|
|
267
|
+
pitfall #28).
|
|
256
268
|
- Action buttons are `InputButton` (`variant="inline"` when the wrapper styles them), not a raw
|
|
257
269
|
`<button>` (`component-inline-button.md`, `component-inline-action-button.md`). The ledger
|
|
258
270
|
doesn't check this, which is how `InputCopy` scored 5/5 with one (fixed 2026-10-15).
|
|
@@ -111,6 +111,7 @@ const toastVisible = ref(false)
|
|
|
111
111
|
|
|
112
112
|
- `onBeforeRouteLeave` dismisses the toast on navigation. This emits a Vue Router warning in Vitest (no active route record) — harmless.
|
|
113
113
|
- `returnFocusTo` accepts an `HTMLElement` or a component instance with `$el`.
|
|
114
|
+
- Neither `DisplayToast` nor `DisplayToastProvider` moves focus when a toast appears (changed 2026-10-16; both used to `.focus()` the toast). Toasts are status messages announced through `role` + `aria-live` (WCAG 4.1.3), and stealing focus interrupted typing and drew a focus ring in Chrome when the toast opened before any pointer input. Keyboard users can still Tab to it (`tabindex="0"`), and Escape is heard on the document while a toast is shown (ignored if another handler called `preventDefault()`).
|
|
114
115
|
- The toast uses `<Teleport to="body">`. In tests, query `document.querySelector(".display-toast")` not `wrapper.find()`.
|
|
115
116
|
- Position, alignment and state are `data-position`, `data-alignment` (`full-width` when `fullWidth`) and `data-state="show|hide"`, not classes (changed 2026-10-08: the old bare `.top`/`.hide`/... classes could collide with a consumer's utility classes, since the toast is teleported into `<body>`).
|
|
116
117
|
- In tests, unmount every toast in `afterEach` before clearing `document.body`: a leftover instance's pending dismiss timer otherwise re-renders into the removed container (`insertBefore` of null) during a later test's timer advance.
|
|
@@ -119,8 +120,8 @@ const toastVisible = ref(false)
|
|
|
119
120
|
|
|
120
121
|
Auto-dismiss pauses while hovered, or while keyboard focus is on or inside the toast, and resumes with
|
|
121
122
|
the remaining time (`createPausableTimeout` in `app/utils/pausableTimeout.ts`, plus
|
|
122
|
-
`focusPausesAutoDismiss`).
|
|
123
|
-
`:focus-visible` (keyboard modality), so mouse
|
|
123
|
+
`focusPausesAutoDismiss`). Focus on the toast root only pauses it when the root matches
|
|
124
|
+
`:focus-visible` (keyboard modality), so a mouse click on it doesn't stop auto-dismiss. `data-paused` freezes the
|
|
124
125
|
progress bar via `animation-play-state`. In jsdom `:focus-visible` matches any focus, so tests that
|
|
125
126
|
depend on modality spy on `Element.prototype.matches` (see `DisplayToast.spec.ts`).
|
|
126
127
|
|
|
@@ -244,6 +245,8 @@ The SVG border shape matches the standard toast exactly (8px left radius, 4px ri
|
|
|
244
245
|
|
|
245
246
|
The inner content layout is handled by the shared `AlertContentInner` molecule — both variants reuse it. `AlertMaskedContent` sets `--alert-content-inner-background: transparent` so the glass effect shows through.
|
|
246
247
|
|
|
248
|
+
**Contrast caveat:** a toast is fixed to the viewport, so whatever scrolls behind it (body copy, busy photos) shows through the fill. 80% holds up over most backdrops; lowering `--alert-masked-content-background` can make the message hard to read, and the 1px accent border can disappear over a backdrop of similar hue. Set that token on `.display-toast` (or a `styleClassPassthrough` class) or `.display-toast-provider-item`: both teleport to `<body>`, so a page-level ancestor never reaches them. Prefer the opaque variant for error/warning toasts. Storybook's "Masked: Low Contrast Risk" story demos the failure.
|
|
249
|
+
|
|
247
250
|
### Provider notes
|
|
248
251
|
|
|
249
252
|
- `useToastQueue` state persists across route changes — no `onBeforeRouteLeave` cleanup needed.
|
|
@@ -252,4 +255,4 @@ The inner content layout is handled by the shared `AlertContentInner` molecule
|
|
|
252
255
|
- Every mounted provider reacts to the shared `useToastQueue` state, so tests must unmount providers in `afterEach`; otherwise an earlier test's provider runs its own timer for a new toast and dismisses it.
|
|
253
256
|
- Each item pauses independently (hover / keyboard focus, same rule as the standalone toast), tracked per toast id.
|
|
254
257
|
- The progress bar (`.display-toast-provider-progress`) is only rendered when `autoDismiss: true`.
|
|
255
|
-
- Dismissal can be triggered by: close button click, Escape key (when the
|
|
258
|
+
- Dismissal can be triggered by: close button click, Escape key (dismisses that item when focus is inside it, otherwise the newest visible item), `dismiss(id)`, auto-dismiss timer, or `clear()`.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# ProgressTracker Component
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`ProgressTracker` (`app/components/02.molecules/progress-tracker/`) is a presentational progress
|
|
6
|
+
track for a multi-step flow: a header with "Step X of Y" and a progress bar, then the steps as a
|
|
7
|
+
`StepperList`. Steps before the `v-model` index are done (filled indicator with a tick), the model's
|
|
8
|
+
step is current (ringed number, content in a focus card) and later steps are upcoming (outlined
|
|
9
|
+
number, muted text). The current step's `#step-{n}` slot is where a parent puts a form or actions,
|
|
10
|
+
so it also works as an accordion-style n-step wizard. The parent owns all state; the component has
|
|
11
|
+
no events and never changes the model. Styling tokens:
|
|
12
|
+
`app/components/02.molecules/progress-tracker/CONSUMER-STYLING.md`.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Props reference
|
|
17
|
+
|
|
18
|
+
> **Hyphenation rule**: write camelCase props hyphenated in templates: `:style-class-passthrough`.
|
|
19
|
+
|
|
20
|
+
| Prop (template form) | Type | Default | Notes |
|
|
21
|
+
|---|---|---|---|
|
|
22
|
+
| `v-model` | `number` | (required) | 0-based index of the current step. `steps.length` means every step is done. Floored, and clamped to `0`–`steps.length`. |
|
|
23
|
+
| `:steps` | `ProgressTrackerStep[]` | (required) | `{ title: string; body?: string }`, exported from `~/types/components`. An empty array renders nothing. |
|
|
24
|
+
| `heading-tag` | `"h2" \| "h3" \| "h4" \| "h5" \| "h6"` | `"h3"` | Tag for each step title. |
|
|
25
|
+
| `step-label` | `string` | `"Step {current} of {total}"` | Header text and the bar's `aria-valuetext`; `{current}` is 1-based. |
|
|
26
|
+
| `complete-label` | `string` | `"All steps complete"` | Header text once every step is done. |
|
|
27
|
+
| `done-label` | `string` | `"Completed"` | Screen-reader text appended to each done title (", Completed"). |
|
|
28
|
+
| `done-icon` | `string` | `"lucide:check"` | Iconify name for the tick. |
|
|
29
|
+
| `:focus-on-change` | `boolean` | `true` | Move focus when the model changes (see Behaviour). |
|
|
30
|
+
| `:style-class-passthrough` | `string \| string[]` | `[]` | Classes on the root `div`. |
|
|
31
|
+
|
|
32
|
+
## Slots
|
|
33
|
+
|
|
34
|
+
| Slot | Props | Notes |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| `#step-{n}` | `{ step, index, headingId }` | Rendered **only while step `n` is current**, inside its focus card under the body. `headingId` is the step title's id, for `aria-labelledby` on a form. |
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Behaviour
|
|
41
|
+
|
|
42
|
+
- One row per step, with its title and body; a step without a body has no body paragraph.
|
|
43
|
+
- Steps before the model are done, the model's step is current, later steps are upcoming.
|
|
44
|
+
- The current title has `aria-current="step"`; done titles carry `done-label` as screen-reader
|
|
45
|
+
text; indicators are `aria-hidden`. The list is an `ol`, so position ("2 of 5") is announced too.
|
|
46
|
+
- The header shows `step-label` filled in, or `complete-label` when every step is done. It's a
|
|
47
|
+
polite live region, so a change is announced.
|
|
48
|
+
- The bar is `role="progressbar"` with `aria-valuemin="0"`, `aria-valuemax` = step count,
|
|
49
|
+
`aria-valuenow` = done count and `aria-valuetext` = the header label.
|
|
50
|
+
- Changing the model after mount moves focus to the new current title (`tabindex="-1"`), or to the
|
|
51
|
+
header label when complete, so keyboard and screen-reader users aren't dropped when a slot's
|
|
52
|
+
focused button unmounts. Never on first render. `:focus-on-change="false"` turns it off (e.g.
|
|
53
|
+
when the parent restores saved progress after mount).
|
|
54
|
+
- Done steps are plain text: there is nothing clickable in the component itself.
|
|
55
|
+
- The model is clamped: negative counts as 0, above `steps.length` counts as complete, fractions
|
|
56
|
+
are floored.
|
|
57
|
+
- Survives worst-case data (unbroken strings, long copy, 20+ steps, empty steps) without breaking
|
|
58
|
+
the layout; the header wraps the bar below the label when space runs out.
|
|
59
|
+
- Bar and state colour transitions are off under `prefers-reduced-motion: reduce`.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
### Display only
|
|
66
|
+
|
|
67
|
+
```vue
|
|
68
|
+
<ProgressTracker v-model="currentStep" :steps="onboardingSteps" />
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Wizard (parent-owned forms)
|
|
72
|
+
|
|
73
|
+
```vue
|
|
74
|
+
<script setup lang="ts">
|
|
75
|
+
import type { ProgressTrackerStep } from "~/types/components";
|
|
76
|
+
|
|
77
|
+
const currentStep = ref(0);
|
|
78
|
+
const form = reactive({ name: "", email: "" });
|
|
79
|
+
|
|
80
|
+
const steps = computed<ProgressTrackerStep[]>(() => [
|
|
81
|
+
{ title: "Your details", body: currentStep.value > 0 ? form.name : "Tell us who you are." },
|
|
82
|
+
{ title: "Contact", body: currentStep.value > 1 ? form.email : "Where can we reach you?" },
|
|
83
|
+
{ title: "Review and submit" },
|
|
84
|
+
]);
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
<template>
|
|
88
|
+
<ProgressTracker v-model="currentStep" :steps="steps">
|
|
89
|
+
<template #step-0="{ headingId }">
|
|
90
|
+
<form :aria-labelledby="headingId" @submit.prevent="currentStep++">
|
|
91
|
+
<!-- fields, then a submit InputButton labelled Continue -->
|
|
92
|
+
</form>
|
|
93
|
+
</template>
|
|
94
|
+
<template #step-1="{ headingId }">
|
|
95
|
+
<form :aria-labelledby="headingId" @submit.prevent="currentStep++">
|
|
96
|
+
<!-- fields, a Back InputButton (@click="currentStep--") and Continue -->
|
|
97
|
+
</form>
|
|
98
|
+
</template>
|
|
99
|
+
<template #step-2="{ headingId }">
|
|
100
|
+
<form :aria-labelledby="headingId" @submit.prevent="currentStep = steps.length">
|
|
101
|
+
<!-- review, Back and Submit -->
|
|
102
|
+
</form>
|
|
103
|
+
</template>
|
|
104
|
+
</ProgressTracker>
|
|
105
|
+
</template>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Validation, saving and what "complete" means are the parent's job. Pass a summary of each completed
|
|
109
|
+
step back in as its `body` so done steps show what was entered.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Composition
|
|
114
|
+
|
|
115
|
+
- Built on `StepperList` (`tag="ol"`, `indicator-alignment="top"`), filling its `indicator-{n}` and
|
|
116
|
+
`item-{n}` slots. ProgressTracker sets StepperList's `--stepper-list-*` tokens on
|
|
117
|
+
`.progress-tracker-list` from its own, so an ancestor's `--stepper-list-*` values don't reach it.
|
|
118
|
+
- Connector colour per state uses `.stepper-list-item:has(.progress-tracker-indicator.is-done)`,
|
|
119
|
+
the per-step state recipe from StepperList's `CONSUMER-STYLING.md`.
|
|
120
|
+
- The focus card bleeds left by `--progress-tracker-card-padding-inline` so the current step's text
|
|
121
|
+
stays aligned with the other steps; the current indicator moves down by the card's block padding
|
|
122
|
+
to stay level with its title. Clearance between the current ring and the card is
|
|
123
|
+
`gap - card-padding-inline - ring-width` (4rem - 2rem - 0.6rem = 1.4rem by default). The first
|
|
124
|
+
defaults (3.2rem gap, 2.4rem padding) left 0.2rem and the ring touched the card, so change these
|
|
125
|
+
three together.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Stories
|
|
130
|
+
|
|
131
|
+
Default, All Complete, Interactive (Mark complete), Wizard (parent-owned forms), Translated
|
|
132
|
+
Labels, White Card on a Panel, Stress Test. Title: `Molecules/ProgressTracker`.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Notes
|
|
137
|
+
|
|
138
|
+
- Out of scope: clickable or back-navigable steps, a built-in complete event or validation,
|
|
139
|
+
horizontal layout, auto-advance.
|
|
140
|
+
- Auto-imported in Nuxt; import the `ProgressTrackerStep` type from `~/types/components`.
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
| `items` | `ISocialIcon[]` | — | **Required.** Array of social network items. |
|
|
14
14
|
| `label` | `string` | `"Social media profiles"` | `aria-label` applied to the `<ul>` element. |
|
|
15
15
|
| `linkLabelTemplate` | `string` | `"{network} profile (opens in a new tab)"` | `aria-label` for each link; `{network}` is replaced with the item's `networkName`. |
|
|
16
|
+
| `showLabel` | `boolean` | `false` | Show each item's `networkName` as visible text after the icon. Filling the `label` slot shows it too. |
|
|
16
17
|
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the root `<ul>`. |
|
|
17
18
|
|
|
18
19
|
---
|
|
@@ -26,11 +27,25 @@ interface ISocialIcon {
|
|
|
26
27
|
iconName: string; // Iconify icon name, e.g. "logos:instagram-icon"
|
|
27
28
|
baseHref: string; // Base URL including trailing slash, e.g. "https://www.instagram.com/"
|
|
28
29
|
profileId: string; // Profile identifier appended to baseHref
|
|
30
|
+
brandColour?: string; // Exposed as --social-icons-list-brand-colour on the item, for recipes
|
|
29
31
|
}
|
|
30
32
|
```
|
|
31
33
|
|
|
32
34
|
The rendered `href` is `${baseHref}${profileId}`.
|
|
33
35
|
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Slots
|
|
39
|
+
|
|
40
|
+
Both are scoped and receive `{ item }` (the `ISocialIcon`). Types: `SocialIconsListSlots`.
|
|
41
|
+
|
|
42
|
+
| Slot | Replaces | Notes |
|
|
43
|
+
|------|----------|-------|
|
|
44
|
+
| `icon` | The `<Icon>` inside `.social-icons-list-icon-wrapper` | The wrapper is `aria-hidden` and keeps the size and circle tokens, so slot content (an initial, `DisplayAvatar`, an image) is decorative and sized by `--social-icons-list-icon-size`. |
|
|
45
|
+
| `label` | The visible `networkName` text | Filling it shows the label without `showLabel` (e.g. name plus `@handle`). |
|
|
46
|
+
|
|
47
|
+
Slot content sits inside the link: keep it non-interactive (no buttons or nested links).
|
|
48
|
+
|
|
34
49
|
### Importing the type
|
|
35
50
|
|
|
36
51
|
**Within the layer** (e.g. in another layer component or composable):
|
|
@@ -57,6 +72,8 @@ import type { ISocialIcon } from "srcdev-nuxt-components";
|
|
|
57
72
|
| YouTube | `logos:youtube-icon` | `https://www.youtube.com/@` |
|
|
58
73
|
| TikTok | `logos:tiktok-icon` | `https://www.tiktok.com/@` |
|
|
59
74
|
|
|
75
|
+
Mono equivalents for recipes, with brand colours: `bi:facebook` (`#1877f2`), `bi:twitter-x` (`#000000`), `bi:instagram` (`#e1306c`), `bi:youtube` (`#ff0000`), `bi:tiktok` (`#000000`).
|
|
76
|
+
|
|
60
77
|
`logos:instagram-icon`, `logos:x` and other dark brand icons vanish on a dark background, and their colours can't be changed. On a dark background use a mono icon such as `mdi:instagram` (any set that draws in `currentColor`) and colour it with `--social-icons-list-icon-colour` (default `currentColor`). Installed mono Instagram icons: `mdi:instagram`, `bi:instagram`, `lucide:instagram`, `akar-icons:instagram-fill`, `radix-icons:instagram-logo`.
|
|
61
78
|
|
|
62
79
|
---
|
|
@@ -88,14 +105,21 @@ import type { ISocialIcon } from "srcdev-nuxt-components";
|
|
|
88
105
|
|
|
89
106
|
| Property | Default | Notes |
|
|
90
107
|
|----------|---------|-------|
|
|
91
|
-
| `--social-icons-list-icon-size` | `2.4rem` | Icon size (
|
|
92
|
-
| `--social-icons-list-gap` | `1.2rem` | Gap between
|
|
108
|
+
| `--social-icons-list-icon-size` | `2.4rem` | Icon size (a `font-size` on the icon wrapper) |
|
|
109
|
+
| `--social-icons-list-gap` | `1.2rem` | Gap between items |
|
|
110
|
+
| `--social-icons-list-icon-colour` / `-hover` | `currentColor` | Mono icons only; `logos:` colours are fixed |
|
|
111
|
+
| `--social-icons-list-link-padding` / `-border` / `-radius` / `-background` / `-background-hover` / `-shadow` / `-shadow-hover` / `-gap` | none / `0.8rem` gap | Turn links into tiles or pills |
|
|
112
|
+
| `--social-icons-list-icon-wrapper-padding` / `-border` / `-radius` / `-background` | none | Circle or tile behind the icon |
|
|
113
|
+
| `--social-icons-list-label-colour` / `-font-size` / `-font-weight` | `currentColor` / `--step-4` / `600` | Visible label |
|
|
93
114
|
| `--social-icons-list-hover-scale` | `1.15` | Hover/focus scale (none under reduced motion) |
|
|
94
115
|
| `--social-icons-list-hover-opacity` | `0.85` | Hover/focus opacity |
|
|
116
|
+
| `--social-icons-list-focus-ring-colour` | `var(--theme-ring, currentColor)` | Keyboard focus outline |
|
|
117
|
+
|
|
118
|
+
Full table with defaults in `CONSUMER-STYLING.md`. **Tokens that read `--social-icons-list-brand-colour` must be set on `.social-icons-list-item`**, where the brand colour is defined: a `var()` in a custom property resolves on the declaring element, so set on the list root it finds nothing. Four recipes (mono tiles with brand hover, dark dock with glow, labelled pills, overlapping stack) are in `stories/social-icons-list-recipes.css` and the `Recipe: …` stories; reuse them rather than inventing new structure.
|
|
95
119
|
|
|
96
|
-
> Renamed 2026-09-28 from `--theme-social-icon-size` / `--theme-social-icon-gap`. Full reference: `CONSUMER-STYLING.md` in the component folder.
|
|
120
|
+
> Renamed 2026-09-28 from `--theme-social-icon-size` / `--theme-social-icon-gap`. Inner classes renamed 2026-10-16 (`.social-icon-item`/`-link`/`.social-icon` are now `.social-icons-list-item`/`-link`/`-icon`). Full reference: `CONSUMER-STYLING.md` in the component folder.
|
|
97
121
|
|
|
98
|
-
|
|
122
|
+
The size token is a `font-size` on `.social-icons-list-icon-wrapper`, whose content box is a fixed `1em` square (`box-sizing: content-box`, padding added around it). The default icon fills it, any `icon` slot content is centred in it, and an icon name that doesn't resolve still leaves a visible, focusable target. With uniform `--social-icons-list-icon-wrapper-padding` and a `50%` radius it's always a circle; slot content bigger than `1em` overflows the square, so size it to fit (e.g. `font-size: 0.8em` for a letter).
|
|
99
123
|
|
|
100
124
|
---
|
|
101
125
|
|
|
@@ -120,7 +144,7 @@ Offer this scaffold when placing the component in a consuming page or section. T
|
|
|
120
144
|
/* --social-icons-list-gap: 1.6rem; */
|
|
121
145
|
/* margin-block-start: 1.6rem; */
|
|
122
146
|
|
|
123
|
-
.social-
|
|
147
|
+
.social-icons-list-link {
|
|
124
148
|
/* border-radius: 0.4rem; */
|
|
125
149
|
/* padding: 0.4rem; */
|
|
126
150
|
/* outline: 1px solid transparent; */
|
|
@@ -138,6 +162,9 @@ need to target a specific instance. See `component-local-style-override.md` for
|
|
|
138
162
|
## Notes
|
|
139
163
|
|
|
140
164
|
- All links open in a new tab with `rel="noopener noreferrer"`.
|
|
165
|
+
- An item is skipped (with a dev-mode `console.warn`) when its `baseHref + profileId` isn't `http:`/`https:`, `mailto:`, `tel:` or a site-relative `/path`: `items` often comes from a CMS, and a `javascript:` href would otherwise render as a working link. `mailto:` and `tel:` items are fine, so email and phone can sit in the same row.
|
|
166
|
+
- Keys are `index + networkName`, so two accounts on the same network (two Instagram profiles) both render.
|
|
167
|
+
- The `<ul>` has `role="list"`: with `list-style: none`, Safari/VoiceOver stops announcing it as a list otherwise.
|
|
141
168
|
- Each link's `aria-label` comes from `linkLabelTemplate` (default `"{network} profile (opens in a new tab)"`, `{network}` = `networkName`), or the item's own `label`. Pass a translated template for other languages. The icon is `aria-hidden`.
|
|
142
169
|
- The `logos:*` Iconify collection requires `@iconify-json/logos` to be installed in the consumer app. Without it, icons will fall back to a CDN fetch (causing FOUC). See [icon-sets.md](../icon-sets.md).
|
|
143
170
|
- Auto-imported in Nuxt — no manual import needed.
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
| … | … |
|
|
21
21
|
|
|
22
22
|
**Rules:**
|
|
23
|
+
|
|
23
24
|
- Slots are always **zero-indexed**: first item = `item-0`, last item = `item-{itemCount - 1}`.
|
|
24
25
|
- `item-{n}` slots are required if you want content in each row.
|
|
25
26
|
- `indicator-{n}` slots are **optional**. When omitted, a CSS counter bubble is rendered automatically. When provided, the custom content replaces the counter entirely — the CSS counter is hidden for that item only.
|
|
@@ -32,7 +33,7 @@
|
|
|
32
33
|
|
|
33
34
|
| Prop | Type | Default | Notes |
|
|
34
35
|
|------|------|---------|-------|
|
|
35
|
-
| `itemCount` | `number` | — | **Required.** Controls how many `<li>` elements are rendered and which slots exist. |
|
|
36
|
+
| `itemCount` | `number` | — | **Required.** Controls how many `<li>` elements are rendered and which slots exist. Floored to a whole number; negative or `NaN` renders no rows. |
|
|
36
37
|
| `tag` | `"ul" \| "ol"` | `"ul"` | Use `"ol"` for sequentially meaningful content (recipes, instructions). |
|
|
37
38
|
| `indicatorAlignment` | `"top" \| "center"` | `"top"` | Vertical alignment of the indicator relative to each item's content. `"center"` suits single-line items; `"top"` suits multi-line content. |
|
|
38
39
|
| `indicatorVariant` | `"disc" \| "circle" \| "square"` | `"disc"` | Visual style of the auto-generated counter bubble. |
|
|
@@ -160,12 +161,14 @@ for full examples (global theming, page-scoped, and per-instance overrides).
|
|
|
160
161
|
| `--stepper-list-counter-font-size` | `1.4rem` | Counter number font size |
|
|
161
162
|
| `--stepper-list-counter-font-weight` | `600` | Counter number font weight |
|
|
162
163
|
| `--stepper-list-connector-width` | `0.2rem` | Connector line thickness |
|
|
164
|
+
| `--stepper-list-connector-gap` | `0` | Space between each end of the connector line and its indicators |
|
|
163
165
|
|
|
164
166
|
### Colours
|
|
165
167
|
|
|
166
168
|
| Token | Default | Controls |
|
|
167
169
|
|---|---|---|
|
|
168
170
|
| `--stepper-list-connector-color` | `currentColor` | Connector line colour |
|
|
171
|
+
| `--stepper-list-connector-image` | `none` | Image over the connector colour, e.g. a `repeating-linear-gradient()` for a dashed line |
|
|
169
172
|
| `--stepper-list-icon-color` | `currentColor` | Icon colour for custom `indicator-icon` elements |
|
|
170
173
|
|
|
171
174
|
### Per-variant tokens
|
|
@@ -190,10 +193,10 @@ for full examples (global theming, page-scoped, and per-instance overrides).
|
|
|
190
193
|
## Connector behaviour
|
|
191
194
|
|
|
192
195
|
- Connectors are drawn as `::after` pseudo-elements on each `<li>` except the last.
|
|
193
|
-
- JS (`ResizeObserver`) measures the bottom of each indicator and the top of the next to set `--_connector-top` and `--_connector-height` precisely, keeping the line flush between bubbles regardless of content height.
|
|
196
|
+
- JS (`ResizeObserver`) measures the bottom of each indicator and the top of the next to set `--_connector-top` and `--_connector-height` precisely, keeping the line flush between bubbles regardless of content height. It also sets `--_connector-left` to the indicator's measured centre, so the line stays centred under a custom indicator wider than the counter bubble (before JS runs it falls back to half of `--_counter-size`).
|
|
194
197
|
- Before JS runs (SSR / initial paint), CSS fallback values approximate the correct position for `indicatorAlignment="top"`.
|
|
195
198
|
- If `connected="false"`, no connectors are rendered and JS measurement is skipped.
|
|
196
|
-
- Connectors recalculate
|
|
199
|
+
- Connectors recalculate when `itemCount`, `connected` or `indicatorAlignment` change, when the list or any `<li>` resizes (a `ResizeObserver` on the list and every item), and when a `class` attribute or child nodes change anywhere inside the list (a `MutationObserver`). The last two cover a consumer restyling a step without the list changing size, e.g. ProgressTracker moving its focus card from one step to the next. Inline `style` changes are not watched, since the connector values themselves are written there.
|
|
197
200
|
|
|
198
201
|
---
|
|
199
202
|
|
|
@@ -222,6 +225,7 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
|
|
|
222
225
|
/* --stepper-list-counter-font-size: 1.4rem; */
|
|
223
226
|
/* --stepper-list-counter-font-weight: 600; */
|
|
224
227
|
/* --stepper-list-connector-width: 0.2rem; */
|
|
228
|
+
/* --stepper-list-connector-gap: 0.6rem; */
|
|
225
229
|
|
|
226
230
|
/* Counter bubble — disc variant */
|
|
227
231
|
/* --stepper-list-counter-disc-background: var(--brand-primary); */
|
|
@@ -246,6 +250,7 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
|
|
|
246
250
|
|
|
247
251
|
/* Connector line */
|
|
248
252
|
/* --stepper-list-connector-color: var(--brand-primary); */
|
|
253
|
+
/* --stepper-list-connector-image: repeating-linear-gradient(to bottom, currentColor 0 0.4rem, transparent 0.4rem 0.8rem); */
|
|
249
254
|
}
|
|
250
255
|
}
|
|
251
256
|
</style>
|
|
@@ -258,4 +263,6 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
|
|
|
258
263
|
- Always keep `itemCount` in sync with the number of `#item-*` slots you provide — mismatches will render empty `<li>` rows.
|
|
259
264
|
- Slot names are **zero-indexed** — `#item-0` not `#item-1`.
|
|
260
265
|
- The `indicator-icon` class is defined inside the component CSS and sizes the SVG to `var(--_counter-size)` (driven by `indicatorSize` or `--stepper-list-counter-size`). Always add it to custom SVGs.
|
|
266
|
+
- Markup: root `<ul>`/`<ol>` with `role="list"` (Safari drops list semantics under `list-style: none`), each row `li.stepper-list-item`, content in `.stepper-list-content`. The content column is `minmax(0, 1fr)` with `overflow-wrap: anywhere`, so long unbroken strings wrap.
|
|
267
|
+
- Used by `ServiceDetail` for its process steps (circle variant, `tag="ol"`, item count from CMS data) and by `ProgressTracker`, which fills the indicator and item slots with done/current/upcoming states (skill `progress-tracker.md`).
|
|
261
268
|
- Auto-imported in Nuxt — no manual import needed.
|
|
@@ -145,6 +145,10 @@ and the trailing edge catches up: the same stretch effect as before, symmetric b
|
|
|
145
145
|
settle timer. CSS retargets each edge from its current position, so a quick second move can't
|
|
146
146
|
pull an edge backwards. Hover and active have separate duration/delay variables.
|
|
147
147
|
|
|
148
|
+
`--tabs-hover-indicator-stretch` (default `1`) multiplies the hover pair's two edge delays in CSS,
|
|
149
|
+
so `0` turns the hover stretch into a plain slide (both edges move together over the same
|
|
150
|
+
duration). The active indicator and underline always stretch.
|
|
151
|
+
|
|
148
152
|
This replaced a `translate` + `scale` model (a bar-wide box translated to the tab's left and
|
|
149
153
|
scaled to its width, plus a timeout to settle after a stretch, and one shared
|
|
150
154
|
`--_transition-duration`). Rightward moves animated both properties together in the settle step,
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# CSS Dynamic Border Radius
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`.dynamic-border-radius` (global utility, `app/assets/styles/setup/06.utility-classes/_dynamic-border-radius.css`,
|
|
6
|
+
`utilities` layer) rounds an element's corners while it's narrower than its container, and squares them once it
|
|
7
|
+
spans the full container width. A capped-width image reads as a rounded card on wide screens and as an edge-to-edge
|
|
8
|
+
band on narrow ones, with no media query and no JS.
|
|
9
|
+
|
|
10
|
+
```css
|
|
11
|
+
.dynamic-border-radius {
|
|
12
|
+
border-radius: clamp(0px, (100cqi - 100%) * 1e5, var(--dynamic-border-radius, 5rem));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.dynamic-border-radius-transition {
|
|
16
|
+
max-inline-size: var(--dynamic-border-radius-max-width, 100%);
|
|
17
|
+
border-radius: clamp(0px, (100cqi - var(--dynamic-border-radius-max-width, 100%)) * 1e5, var(--dynamic-border-radius, 5rem));
|
|
18
|
+
|
|
19
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
20
|
+
transition: border-radius var(--dynamic-border-radius-duration, 300ms) var(--dynamic-border-radius-easing, ease-out);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
It's a utility class, not a component: it works on any element (`<img>`, `NuxtImg`, `<video>`, a card), and the
|
|
26
|
+
container it measures is an ancestor the element doesn't own.
|
|
27
|
+
|
|
28
|
+
Storybook: **Foundations/Utilities/Dynamic Border Radius**.
|
|
29
|
+
|
|
30
|
+
## Token
|
|
31
|
+
|
|
32
|
+
| Token | Default | Controls |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `--dynamic-border-radius` | `5rem` | Radius while the element is narrower than its container. Set it on the element or any ancestor. |
|
|
35
|
+
| `--dynamic-border-radius-max-width` | `100%` | `.dynamic-border-radius-transition` only: the element's max width, as a length. Sets its `max-inline-size` and is what the container is compared against. |
|
|
36
|
+
| `--dynamic-border-radius-duration` | `300ms` | Transition duration, with `.dynamic-border-radius-transition`. |
|
|
37
|
+
| `--dynamic-border-radius-easing` | `ease-out` | Transition easing, with `.dynamic-border-radius-transition`. |
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
```html
|
|
42
|
+
<div class="hero-media">
|
|
43
|
+
<img class="dynamic-border-radius" src="/images/hero.jpg" alt="" width="1600" height="900">
|
|
44
|
+
</div>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```css
|
|
48
|
+
.hero-media {
|
|
49
|
+
container-type: inline-size;
|
|
50
|
+
|
|
51
|
+
img {
|
|
52
|
+
display: block;
|
|
53
|
+
inline-size: 100%;
|
|
54
|
+
max-inline-size: 50rem;
|
|
55
|
+
margin-inline: auto;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## How it works
|
|
61
|
+
|
|
62
|
+
`100cqi` is the container's inline size; inside `border-radius`, `100%` is the element's own width. While the element
|
|
63
|
+
is narrower, the difference is positive, the `* 1e5` blows it up past the max, and `clamp()` caps it at the token.
|
|
64
|
+
When the element fills the container, the difference is 0 and so is the radius.
|
|
65
|
+
|
|
66
|
+
## Optional transition
|
|
67
|
+
|
|
68
|
+
Use `.dynamic-border-radius-transition` instead of the base class to ease the switch, and give it the element's max
|
|
69
|
+
width as a token (it sets `max-inline-size` from it, so don't also set `max-inline-size` yourself):
|
|
70
|
+
|
|
71
|
+
```html
|
|
72
|
+
<img class="dynamic-border-radius-transition" style="--dynamic-border-radius-max-width: 50rem" src="/images/hero.jpg" alt="" width="1600" height="900">
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- **Why a different formula.** Don't just add a `transition` to the base class: it flickers while the container
|
|
76
|
+
shrinks. A transition starts from the old *computed* value, where `100cqi` is already pixels (the previous
|
|
77
|
+
frame's container width) but `100%` stays unresolved until layout, so it's re-evaluated against this frame's,
|
|
78
|
+
smaller, element width. Previous container minus current element is positive, `* 1e5` clamps it to the full
|
|
79
|
+
radius, and every resize frame restarts a transition from there. Comparing against a length token instead makes
|
|
80
|
+
the computed value a plain length (full radius or 0): nothing changes while you resize either side of the
|
|
81
|
+
threshold, and crossing it starts exactly one transition.
|
|
82
|
+
- **Without the token** it falls back to `100%`: the base formula, so it works but flickers on resize.
|
|
83
|
+
- **The token must match the element's real max width**, as a single length (`50rem`, not `min(50rem, 90%)`). If
|
|
84
|
+
something else caps the element narrower (a consumer `max-inline-size`, a small intrinsic image size), the radius
|
|
85
|
+
stays at 0 even though the element doesn't fill the container.
|
|
86
|
+
- It sets `transition`, which replaces any transition already on the element (the utilities layer beats
|
|
87
|
+
`@layer components`). If the element needs its own transition too, add `border-radius` to your own combined
|
|
88
|
+
`transition` instead (`transform 200ms, border-radius 300ms ease-out`).
|
|
89
|
+
- It only animates when the **container** resizes. If only the token changes (at a breakpoint), the value changes
|
|
90
|
+
too, so that animates as well; an image loading doesn't affect it.
|
|
91
|
+
- The transition is skipped under `prefers-reduced-motion: reduce`; the radius still switches.
|
|
92
|
+
|
|
93
|
+
## Caveats
|
|
94
|
+
|
|
95
|
+
1. **It needs a `container-type: inline-size` ancestor.** Without one, `cqi` falls back to the viewport, so the
|
|
96
|
+
radius only drops when the element is as wide as the screen. It doesn't error; it just looks wrong. Put the
|
|
97
|
+
container on the element's direct layout wrapper: if the nearest container is wider than the element's real
|
|
98
|
+
column, the element never "fills" it.
|
|
99
|
+
2. **It works partly by luck. Don't split it into horizontal and vertical radii.** The single value sets both radii, and
|
|
100
|
+
the vertical one resolves `100%` against the element's *height*, so at full width it's usually still at the max.
|
|
101
|
+
The corner is square anyway, because CSS draws a square corner when either radius is 0. A `border-radius: X / Y`
|
|
102
|
+
rewrite, or a percentage max, can break this.
|
|
103
|
+
3. **The element must actually reach full width.** Give an image `display: block; inline-size: 100%` plus a
|
|
104
|
+
`max-inline-size`. With only a max, an image whose intrinsic width is smaller than the container (a `w=400` CDN
|
|
105
|
+
URL, for example) never fills it, so the radius never drops.
|
|
106
|
+
4. **Subpixel widths.** A 500.4px container around a 500px element keeps the radius on. That's correct (it isn't
|
|
107
|
+
full width), but it can look like the switch is late at fractional zoom levels.
|
|
108
|
+
5. **Support:** container query units and `clamp()` with mixed units work in Safari 16, the layer's support
|
|
109
|
+
baseline.
|
|
110
|
+
|
|
111
|
+
## Snippet
|
|
112
|
+
|
|
113
|
+
`.vscode/srcdev-utility-dynamic-border-radius.code-snippets`: `srcdev-dynamic-border-radius` (markup with the
|
|
114
|
+
container wrapper, transition class as an optional placeholder) and `srcdev-dynamic-border-radius-css` (the wrapper and image rules).
|
package/.claude/skills/index.md
CHANGED
|
@@ -51,6 +51,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
51
51
|
├── css-nesting-conventions.md — native CSS nesting rules: why &__child Sass BEM concatenation silently breaks, correct patterns
|
|
52
52
|
├── css-grid-max-width-gutters.md — cap a centre grid column width by growing gutters, with start/center alignment variants
|
|
53
53
|
├── css-animation-utilities.md — scroll-driven animation utility classes: scroller-x (carousel), entry-zoom-reveal, entry-slide-in, entry-exit-blur, auto-rotate
|
|
54
|
+
├── css-dynamic-border-radius.md — .dynamic-border-radius utility: rounded until the element fills its container-type ancestor, then square; --dynamic-border-radius token, opt-in -transition class, container/full-width caveats
|
|
54
55
|
├── component-aria-landmark.md — useAriaLabelledById composable: aria-labelledby for section/article/aside tags (not main), consumer-bound vs self-bound pattern, built-in broken-reference console warning
|
|
55
56
|
├── component-export-types.md — move inline component types to app/types/components/ barrel for consumer imports
|
|
56
57
|
├── component-inline-action-button.md — InputButton variant="inline" pattern for buttons embedded in custom input wrappers
|
|
@@ -99,7 +100,8 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
99
100
|
├── skip-links.md — SkipLinks: focus-revealed accessibility skip-nav, links (SkipLink[]) data-driven, homeLink slot, CSS token API
|
|
100
101
|
├── tabbed-content.md — TabbedContent (was TabsCore): tablist + panels from indexed slots (itemCount), arrow-key nav, moving hover/active/underline indicators, CSS token API
|
|
101
102
|
├── contact-section.md — ContactSection props (stepperIndicatorSize pass-through), 3-item info+form layout, slot API
|
|
102
|
-
├── stepper-list.md — StepperList dynamic slots (item-{n}/indicator-{n}), props,
|
|
103
|
+
├── stepper-list.md — StepperList dynamic slots (item-{n}/indicator-{n}), props, measured connectors (gap, dashed image tokens), per-step state recipe
|
|
104
|
+
├── progress-tracker.md — ProgressTracker: steps (ProgressTrackerStep[]) + v-model current index over StepperList, header label + progressbar, current-step #step-{n} slot for wizard forms, focus on change, CSS token API
|
|
103
105
|
├── expanding-panel.md — ExpandingPanel v-model, forceOpened, contentIsOnTop overlay mode, slots (summary/icon/content), ARIA wiring, CSS token API
|
|
104
106
|
├── content-docs.md — ContentDocs docs-page shell: prop-driven docsNav/docsPageNav (not slots), DocsNavItem icons, container-width breakpoint behaviour, shared/per-side CSS token API
|
|
105
107
|
├── glass-panel.md — GlassPanel: frosted-glass container, light-only token defaults in-component (light-dark() block removed from _default.css, no built-in dark glass), highlight behind content, section/article labelled via headingId slot prop, CSS token API
|
|
@@ -114,7 +116,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
114
116
|
├── wipe-away-vertical.md — WipeAwayVertical: scroll-driven vertical wipe-away effect, pure-CSS grid overlay (no JS), indexed dynamic slots, sticky-centering-via-calc gotcha, CSS tokens
|
|
115
117
|
├── marquee-scroller.md — MarqueeScroller: infinite logo/badge scroller, per-item dynamic slots (marqueeData id), hover/focus/keyboard pause, reduced-motion, CSS tokens
|
|
116
118
|
├── rotating-carousel-image.md — RotatingCarouselImage: 3D rotating image carousel, scroll-parallax tilt, focus/keyboard/hover pause, reduced-motion, CSS tokens
|
|
117
|
-
├── social-icons-list.md — SocialIconsList: data-driven social icon links, ISocialIcon type, logos: icon names (dark ones vanish on dark backgrounds; mono alternatives),
|
|
119
|
+
├── social-icons-list.md — SocialIconsList: data-driven social icon links, ISocialIcon type, logos: icon names (dark ones vanish on dark backgrounds; mono alternatives), showLabel + scoped icon/label slots, per-item brandColour (set brand-colour tokens on .social-icons-list-item), tile/circle/label tokens, four recipes (mono tiles, dark dock, pills, overlapping stack), unsafe hrefs skipped
|
|
118
120
|
├── display-qr-code.md — DisplayQrCode: QR code SVG from a string value, colour/size/variant/radius props, currentColor default
|
|
119
121
|
├── capture-qr-code.md — CaptureQrCode: live camera scanner, error state, visibility/route/KeepAlive lifecycle, media stream cleanup
|
|
120
122
|
├── decode-qr-code.md — DecodeQrCode: file picker + drag-and-drop image decoder, shared results list, CSS override points
|
|
@@ -242,6 +242,12 @@ export default meta;
|
|
|
242
242
|
type Story = StoryObj<typeof ComponentName>; // ← still strict for individual stories
|
|
243
243
|
```
|
|
244
244
|
|
|
245
|
+
When the story assigns a model arg itself (a button doing `args.modelValue = index + 1`, a wizard's
|
|
246
|
+
Back/Continue), a type built from `InstanceType<typeof C>["$props"]` fails `vue-tsc` with "Cannot
|
|
247
|
+
assign to 'modelValue' because it is a read-only property". Re-declare it writable:
|
|
248
|
+
`Omit<InstanceType<typeof C>["$props"], "modelValue"> & { modelValue: number; ... }`
|
|
249
|
+
(`ProgressTracker.stories.ts`).
|
|
250
|
+
|
|
245
251
|
Strip extra args before `v-bind` using a `useStorySetup` helper in `setup()`:
|
|
246
252
|
|
|
247
253
|
```ts
|
|
@@ -315,6 +321,12 @@ the earlier nightly `@nuxtjs/storybook` didn't render Docs pages at all, so a lo
|
|
|
315
321
|
screens, and sets `docs.story: { inline: false }`. `CookieConsentBanner` and `DisplayToast`
|
|
316
322
|
follow this. A teleported component doesn't inherit a wrapper's `dir`, so wrap RTL slot copy
|
|
317
323
|
in `<span dir="rtl">`.
|
|
324
|
+
- The same goes for tokens: a teleported component doesn't inherit custom properties from a story
|
|
325
|
+
wrapper. To demo a token override, inject a `<style>` rule targeting the component's own root
|
|
326
|
+
class in setup (remove it in `onUnmounted`); that also keeps `var(--theme-*)` inside the value
|
|
327
|
+
resolving against the component's own `data-theme`. A backdrop meant to sit behind it (to demo
|
|
328
|
+
translucency) must be `position: fixed; inset: 0`. `DisplayToast`'s `canvas` / `maskedFill`
|
|
329
|
+
story controls follow this.
|
|
318
330
|
- Opt a file out with `tags: ["!autodocs"]` only when it isn't a component reference (tooling, or
|
|
319
331
|
a file with its own attached `.mdx` page, like `ColourRamps.stories.ts` with `SetupGuide.mdx`).
|
|
320
332
|
|
|
@@ -62,16 +62,14 @@
|
|
|
62
62
|
]
|
|
63
63
|
},
|
|
64
64
|
"SRCDEV DisplayToast CSS Override": {
|
|
65
|
-
"description": "CSS override scaffold for DisplayToastProvider items",
|
|
65
|
+
"description": "CSS override scaffold for DisplayToastProvider items (teleported to body, so not nested in a page class)",
|
|
66
66
|
"scope": "css",
|
|
67
67
|
"body": [
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
" --theme-border: ;",
|
|
74
|
-
" }",
|
|
68
|
+
".display-toast-provider-item {",
|
|
69
|
+
" --theme-surface: $1;",
|
|
70
|
+
" --theme-text: $2;",
|
|
71
|
+
" --theme-accent: $3;",
|
|
72
|
+
" --theme-border: $4;",
|
|
75
73
|
"}"
|
|
76
74
|
]
|
|
77
75
|
}
|