srcdev-nuxt-components 9.6.13 → 9.6.15
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/story-register-components.md +1 -1
- package/.claude/skills/component-compliance-checklist.md +5 -0
- package/.claude/skills/components/display-qr-code.md +22 -4
- package/.claude/skills/components/eyebrow-text.md +9 -5
- package/.claude/skills/components/header-block.md +13 -5
- package/.claude/skills/components/hero-text.md +13 -9
- package/.claude/skills/components/link-text.md +8 -5
- package/.claude/skills/components/pop-over.md +17 -3
- package/.claude/skills/components/sliding-toggle.md +108 -0
- package/.claude/skills/components/tabbed-content.md +8 -3
- package/.claude/skills/composable-sliding-indicator.md +60 -0
- package/.claude/skills/index.md +2 -0
- package/.claude/skills/storybook-add-story.md +22 -2
- package/.claude/skills/testing-add-playwright.md +12 -7
- package/.claude/skills/testing-add-unit-test.md +25 -0
- package/.claude/skills/theming-component-token-pattern.md +18 -0
- package/.vscode/srcdev-component-display-qr-code.code-snippets +12 -1
- package/.vscode/srcdev-component-hero-text.code-snippets +21 -5
- package/.vscode/srcdev-component-link-text.code-snippets +3 -3
- package/.vscode/srcdev-component-pop-over.code-snippets +28 -0
- package/.vscode/srcdev-component-sliding-toggle.code-snippets +37 -0
- package/.vscode/srcdev-component-tabbed-content.code-snippets +1 -1
- package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +42 -9
- package/app/components/01.atoms/pop-over/PopOver.vue +28 -12
- package/app/components/01.atoms/pop-over/stories/PopOver.stories.ts +114 -2
- package/app/components/01.atoms/pop-over/tests/PopOver.spec.ts +35 -0
- package/app/components/01.atoms/pop-over/tests/__snapshots__/PopOver.spec.ts.snap +5 -1
- package/app/components/01.atoms/qr-code/CONSUMER-STYLING.md +16 -3
- package/app/components/01.atoms/qr-code/DisplayQrCode.vue +31 -0
- package/app/components/01.atoms/qr-code/stories/DisplayQrCode.stories.ts +190 -157
- package/app/components/01.atoms/qr-code/tests/DisplayQrCode.spec.ts +58 -0
- package/app/components/01.atoms/tabbed-content/CONSUMER-STYLING.md +15 -2
- package/app/components/01.atoms/tabbed-content/TabbedContent.vue +4 -4
- package/app/components/01.atoms/tabbed-content/stories/TabbedContent.stories.ts +4 -4
- package/app/components/01.atoms/text-blocks/eyebrow-text/CONSUMER-STYLING.md +9 -1
- package/app/components/01.atoms/text-blocks/eyebrow-text/EyebrowText.vue +5 -4
- package/app/components/01.atoms/text-blocks/eyebrow-text/playwright/eyebrow-text.playwright.ts +1 -1
- package/app/components/01.atoms/text-blocks/eyebrow-text/stories/EyebrowText.stories.ts +104 -17
- package/app/components/01.atoms/text-blocks/eyebrow-text/tests/EyebrowText.spec.ts +68 -118
- package/app/components/01.atoms/text-blocks/eyebrow-text/tests/__snapshots__/EyebrowText.spec.ts.snap +8 -8
- package/app/components/01.atoms/text-blocks/header-block/CONSUMER-STYLING.md +49 -24
- package/app/components/01.atoms/text-blocks/header-block/HeaderBlock.vue +9 -1
- package/app/components/01.atoms/text-blocks/header-block/stories/HeaderBlock.stories.ts +96 -26
- package/app/components/01.atoms/text-blocks/header-block/tests/HeaderBlock.spec.ts +64 -48
- package/app/components/01.atoms/text-blocks/header-block/tests/__snapshots__/HeaderBlock.spec.ts.snap +2 -2
- package/app/components/01.atoms/text-blocks/hero-text/CONSUMER-STYLING.md +22 -6
- package/app/components/01.atoms/text-blocks/hero-text/HeroText.vue +32 -23
- package/app/components/01.atoms/text-blocks/hero-text/playwright/hero-text.playwright.ts +1 -1
- package/app/components/01.atoms/text-blocks/hero-text/stories/HeroText.stories.ts +176 -23
- package/app/components/01.atoms/text-blocks/hero-text/tests/HeroText.spec.ts +129 -123
- package/app/components/01.atoms/text-blocks/hero-text/tests/__snapshots__/HeroText.spec.ts.snap +2 -2
- package/app/components/01.atoms/text-blocks/link-text/CONSUMER-STYLING.md +17 -0
- package/app/components/01.atoms/text-blocks/link-text/LinkText.vue +9 -3
- package/app/components/01.atoms/text-blocks/link-text/stories/LinkText.stories.ts +96 -99
- package/app/components/01.atoms/text-blocks/link-text/tests/LinkText.spec.ts +84 -93
- package/app/components/02.molecules/price-list/tests/__snapshots__/PriceList.spec.ts.snap +8 -8
- package/app/components/02.molecules/sliding-toggle/CONSUMER-STYLING.md +109 -0
- package/app/components/02.molecules/sliding-toggle/SlidingToggle.vue +307 -0
- package/app/components/02.molecules/sliding-toggle/stories/SlidingToggle.stories.ts +357 -0
- package/app/components/02.molecules/sliding-toggle/tests/SlidingToggle.spec.ts +335 -0
- package/app/components/02.molecules/sliding-toggle/tests/__snapshots__/SlidingToggle.spec.ts.snap +3 -0
- package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +15 -15
- package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +3 -3
- package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +3 -3
- package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +3 -3
- package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +1 -1
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +9 -9
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +9 -9
- package/app/composables/tests/useSlidingIndicator.spec.ts +143 -0
- package/app/composables/useSlidingIndicator.ts +56 -0
- package/app/composables/useTabs.ts +4 -31
- package/app/types/components/index.ts +1 -0
- package/app/types/components/sliding-toggle.d.ts +5 -0
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ paths:
|
|
|
6
6
|
# Register every component a story template uses
|
|
7
7
|
|
|
8
8
|
Every component in a story's `template` string, including ones placed in the component's slots, must
|
|
9
|
-
be listed in that story's `components: {}`.
|
|
9
|
+
be listed in that story's `components: {}`. Globally registered ones (`Icon`, `NuxtLink`) are the exception.
|
|
10
10
|
|
|
11
11
|
Why: components aren't auto-registered in story templates; unregistered ones render as unknown tags.
|
|
12
12
|
Detail: skill `storybook-add-story.md` (srcdev-nuxt-components).
|
|
@@ -138,6 +138,11 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
|
|
|
138
138
|
`computed()`: Vue renders nothing for a fractional or negative range and only warns in dev
|
|
139
139
|
(`StepperList`, 2026-10-15)
|
|
140
140
|
- broken image or link URLs
|
|
141
|
+
- data a third-party child can't handle: a library that throws while rendering (an encoder, a
|
|
142
|
+
parser) breaks the whole page render. Catch it with `onErrorCaptured` and render a fallback
|
|
143
|
+
(`DisplayQrCode` threw "Data too long" on an over-capacity value until 2026-10-19). Check story
|
|
144
|
+
control ranges against the library's real range too: its `radius` control ran to 20 while the
|
|
145
|
+
library clamps to 0-1
|
|
141
146
|
Describe what to check in the story's docs description, and look at it at every
|
|
142
147
|
`CanvasSwitcher` width. Fixes usually mean `overflow-wrap: anywhere`, `min-inline-size: 0` on
|
|
143
148
|
flex/grid children, `Array.from(str)` instead of `str[0]`, and `clamp()` on values
|
|
@@ -10,10 +10,10 @@ Renders a styled QR code SVG from a string value. Supports colour, size, corner
|
|
|
10
10
|
|------|------|---------|-------------|
|
|
11
11
|
| `qr-value` | `string` | required | The string to encode into the QR code |
|
|
12
12
|
| `variant` | `QrCodeVariant` | `{ inner: "default", marker: "default", pixel: "default" }` | Shape variants for inner markers, outer markers, and pixels |
|
|
13
|
-
| `radius` | `number` | `0` | Corner rounding
|
|
13
|
+
| `radius` | `number` | `0` | Corner rounding for the rounded, circle and dots shapes, 0 to 1 (the library clamps values outside) |
|
|
14
14
|
| `black-color` | `string` | `"currentColor"` | Colour for the dark modules |
|
|
15
15
|
| `white-color` | `string` | `"transparent"` | Colour for the light modules |
|
|
16
|
-
| `size` | `string` | `"256px"` | Width of the QR code (height matches via `aspect-ratio: 1 / 1`) |
|
|
16
|
+
| `size` | `string` | `"256px"` | Width of the QR code (height matches via `aspect-ratio: 1 / 1`), never wider than its container |
|
|
17
17
|
| `aria-label` | `string` | `"QR code"` | Accessible name for the QR image (root has `role="img"`). Pass something specific and translated, e.g. "Scan to book an appointment". |
|
|
18
18
|
| `style-class-passthrough` | `string \| string[]` | `[]` | Extra classes on the root element |
|
|
19
19
|
|
|
@@ -22,7 +22,7 @@ Renders a styled QR code SVG from a string value. Supports colour, size, corner
|
|
|
22
22
|
```typescript
|
|
23
23
|
import type { QrCodeVariant } from "srcdev-nuxt-components/types/components";
|
|
24
24
|
|
|
25
|
-
// Each field accepts: "default" | "circle" | "rounded" | "dots" | "
|
|
25
|
+
// Each field accepts: "default" | "circle" | "rounded" | "dots" | "pixelated"
|
|
26
26
|
const variant: QrCodeVariant = {
|
|
27
27
|
inner: "circle",
|
|
28
28
|
marker: "rounded",
|
|
@@ -30,6 +30,21 @@ const variant: QrCodeVariant = {
|
|
|
30
30
|
};
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
## Slots and events
|
|
34
|
+
|
|
35
|
+
| Name | Kind | Description |
|
|
36
|
+
|------|------|-------------|
|
|
37
|
+
| `fallback` | slot | Rendered in place of the code when `qr-value` is too long to encode (about 2.9KB at the lowest error correction, less at higher). Empty by default, leaving a blank box of the same size. |
|
|
38
|
+
| `error` | event | `(error: Error)`, emitted once when encoding fails. Resets when `qr-value` changes. |
|
|
39
|
+
|
|
40
|
+
```vue
|
|
41
|
+
<DisplayQrCode :qr-value="bookingUrl" @error="reportQrError">
|
|
42
|
+
<template #fallback>
|
|
43
|
+
<a :href="bookingUrl">Open the booking page</a>
|
|
44
|
+
</template>
|
|
45
|
+
</DisplayQrCode>
|
|
46
|
+
```
|
|
47
|
+
|
|
33
48
|
## CSS token
|
|
34
49
|
|
|
35
50
|
`--display-qr-code-size` overrides the `size` prop when set (e.g. on a page wrapper). Full reference: `app/components/01.atoms/qr-code/CONSUMER-STYLING.md`.
|
|
@@ -39,6 +54,7 @@ const variant: QrCodeVariant = {
|
|
|
39
54
|
| Class | Where | Description |
|
|
40
55
|
|-------|-------|-------------|
|
|
41
56
|
| `.display-qr-code` | root | always present |
|
|
57
|
+
| `.display-qr-code-fallback` | root | only when the value can't be encoded (with `data-error`) |
|
|
42
58
|
|
|
43
59
|
## Usage
|
|
44
60
|
|
|
@@ -63,7 +79,9 @@ Custom size, colour, and rounded pixels:
|
|
|
63
79
|
|
|
64
80
|
## Notes
|
|
65
81
|
|
|
66
|
-
- The component renders directly as the `Qrcode` element from `nuxt-qrcode` — there is no wrapper `<div>`. The `.display-qr-code` class is applied to the `Qrcode` root, which renders as an `<svg>`.
|
|
82
|
+
- The component renders directly as the `Qrcode` element from `nuxt-qrcode` — there is no wrapper `<div>`. The `.display-qr-code` class is applied to the `Qrcode` root, which renders as an `<svg>`. When encoding fails it renders a `<span class="display-qr-code display-qr-code-fallback">` instead.
|
|
83
|
+
- Encoding errors are caught with `onErrorCaptured`, so an over-capacity value from a CMS can't break the page render. `nuxt-qrcode` must be installed in the consuming app.
|
|
67
84
|
- `size` is applied via CSS `v-bind()` — pass any valid CSS length value (`px`, `rem`, `%`, etc.).
|
|
68
85
|
- `black-color` defaults to `currentColor`, so the QR code inherits the surrounding text colour automatically unless overridden.
|
|
69
86
|
- `white-color` defaults to `transparent`, making the background of the QR code see-through by default.
|
|
87
|
+
- 2026-10-19 migration: added the `fallback` slot and `error` event, capped the width at the container, corrected the variant list (`pixelated`, no `classy`) and added a `StressTest` story; the story's radius control now uses the real 0 to 1 range.
|
|
@@ -27,7 +27,7 @@ Do not assume placeholder text or default content.
|
|
|
27
27
|
## Basic Usage
|
|
28
28
|
|
|
29
29
|
```vue
|
|
30
|
-
<EyebrowText
|
|
30
|
+
<EyebrowText text-content="Interior Design" />
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
## With Tag and Size
|
|
@@ -37,8 +37,8 @@ Use `tag="p"` for semantic paragraph context, or `tag="span"` when inline. Choos
|
|
|
37
37
|
```vue
|
|
38
38
|
<EyebrowText
|
|
39
39
|
tag="p"
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
font-size="small"
|
|
41
|
+
text-content="Featured Collection"
|
|
42
42
|
/>
|
|
43
43
|
```
|
|
44
44
|
|
|
@@ -47,10 +47,10 @@ Use `tag="p"` for semantic paragraph context, or `tag="span"` when inline. Choos
|
|
|
47
47
|
EyebrowText is commonly placed directly above a `HeroText` heading:
|
|
48
48
|
|
|
49
49
|
```vue
|
|
50
|
-
<EyebrowText tag="p"
|
|
50
|
+
<EyebrowText tag="p" text-content="Our Services" />
|
|
51
51
|
<HeroText
|
|
52
52
|
tag="h2"
|
|
53
|
-
:
|
|
53
|
+
:text-content="[
|
|
54
54
|
{ text: 'Crafted with', styleClass: 'normal' },
|
|
55
55
|
{ text: 'Care', styleClass: 'accent' },
|
|
56
56
|
]"
|
|
@@ -59,6 +59,8 @@ EyebrowText is commonly placed directly above a `HeroText` heading:
|
|
|
59
59
|
|
|
60
60
|
## Font Size Scale
|
|
61
61
|
|
|
62
|
+
The prop is rendered as `data-font-size` on the root (not a class).
|
|
63
|
+
|
|
62
64
|
| Value | CSS variable |
|
|
63
65
|
|-------|-------------|
|
|
64
66
|
| `large` | `--eyebrow-text-large` (default `1.4rem`) |
|
|
@@ -110,3 +112,5 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
|
|
|
110
112
|
|
|
111
113
|
- Component is auto-imported in Nuxt — no import needed.
|
|
112
114
|
- `tag` defaults to `"div"` which is block-level. Use `"span"` when the eyebrow must be inline within a flow of text.
|
|
115
|
+
- Long text and unbroken strings wrap (`overflow-wrap: anywhere`). There is no line-clamp token: clamping needs `overflow: hidden`, which clips the italic overhang; `ServiceDetail` clamps its eyebrow itself with an allowance.
|
|
116
|
+
- 2026-10-19 migration: the size moved from a bare `large`/`medium`/`small` class to `data-font-size`; added wrapping, a `StressTest` story and parent-story links.
|
|
@@ -58,10 +58,11 @@ heading to have an accessible name. Bind the same id to both:
|
|
|
58
58
|
</section>
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
Three components in this library generate this id with the `useAriaLabelledById` composable and
|
|
62
|
+
expose it as a `heading-id` slot prop: `PageRow` (default slot), `ProfileSection` (`heroText` slot)
|
|
63
|
+
and `PageHeroHighlights` (`header` slot). (`LayoutGridByCols`, `ServiceSummary` and `ServiceDetail`
|
|
64
|
+
use the composable too, but label their own heading or an `sr-only` label, with no slot prop.) When
|
|
65
|
+
composing inside one of the three, bind `HeaderBlock`'s `id` to that slot prop instead of writing
|
|
65
66
|
your own id:
|
|
66
67
|
|
|
67
68
|
```vue
|
|
@@ -72,7 +73,8 @@ your own id:
|
|
|
72
73
|
|
|
73
74
|
## Styling
|
|
74
75
|
|
|
75
|
-
|
|
76
|
+
The root always has the `.header-block` class, whose only style is `overflow-wrap: anywhere` (so
|
|
77
|
+
an unbroken word wraps instead of overflowing). `classLevel` applies one of the global
|
|
76
78
|
`.page-heading-1`–`.page-heading-6` utility classes defined in
|
|
77
79
|
`app/assets/styles/setup/05.typography/02.utility-classes/_font-classes-page-heading.css`, each
|
|
78
80
|
setting `font-size` from a shared fluid type-scale token (`--step-3` through `--step-8`). All six
|
|
@@ -90,3 +92,9 @@ Moved to `01.atoms/text-blocks/header-block/`, alongside `EyebrowText`/`HeroText
|
|
|
90
92
|
added the `id` prop in the same pass — several sections need their heading to carry a specific id
|
|
91
93
|
for `aria-labelledby`, and `HeaderBlock` had no explicit way to receive one (matching `PageRow`'s
|
|
92
94
|
existing `id?: string | null` + `:id` convention).
|
|
95
|
+
|
|
96
|
+
## Migration note (2026-10-19)
|
|
97
|
+
|
|
98
|
+
Added the `.header-block` root class and `overflow-wrap: anywhere`, a `StressTest` story, a
|
|
99
|
+
story-only slot-text control and docs text. The spec now unmounts every wrapper. Corrected the list
|
|
100
|
+
of components that expose a `heading-id` slot prop.
|
|
@@ -45,7 +45,7 @@ interface TextConfig {
|
|
|
45
45
|
```vue
|
|
46
46
|
<HeroText
|
|
47
47
|
tag="h1"
|
|
48
|
-
:
|
|
48
|
+
:text-content="[
|
|
49
49
|
{ text: 'Designing', styleClass: 'normal' },
|
|
50
50
|
{ text: 'Artistry', styleClass: 'accent' },
|
|
51
51
|
{ text: 'at Home', styleClass: 'normal' },
|
|
@@ -61,8 +61,8 @@ interface TextConfig {
|
|
|
61
61
|
<HeroText
|
|
62
62
|
tag="h2"
|
|
63
63
|
icon="lucide:sparkles"
|
|
64
|
-
|
|
65
|
-
:
|
|
64
|
+
font-size="heading"
|
|
65
|
+
:text-content="[{ text: 'Featured', styleClass: 'accent' }]"
|
|
66
66
|
/>
|
|
67
67
|
```
|
|
68
68
|
|
|
@@ -74,8 +74,8 @@ Use `axis="vertical"` to stack text segments in a column instead of a row.
|
|
|
74
74
|
<HeroText
|
|
75
75
|
tag="h1"
|
|
76
76
|
axis="vertical"
|
|
77
|
-
|
|
78
|
-
:
|
|
77
|
+
font-size="display"
|
|
78
|
+
:text-content="[
|
|
79
79
|
{ text: 'Modern', styleClass: 'normal' },
|
|
80
80
|
{ text: 'Living', styleClass: 'accent' },
|
|
81
81
|
]"
|
|
@@ -96,7 +96,9 @@ Define these CSS custom properties in your consuming app to control sizes.
|
|
|
96
96
|
|
|
97
97
|
## Styling
|
|
98
98
|
|
|
99
|
-
Override via `styleClassPassthrough` or a parent HOC `<style>` block targeting `.hero-text`.
|
|
99
|
+
Override via `styleClassPassthrough` or a parent HOC `<style>` block targeting `.hero-text`. Size
|
|
100
|
+
and axis are `data-font-size` and `data-axis` attributes on the root (not classes); each segment is a
|
|
101
|
+
`<span class="hero-text-segment">` plus its `styleClass`.
|
|
100
102
|
|
|
101
103
|
Key CSS custom properties:
|
|
102
104
|
|
|
@@ -107,7 +109,6 @@ Key CSS custom properties:
|
|
|
107
109
|
| `--hero-text-{scale}` | see CONSUMER-STYLING.md | Font size per `fontSize` prop value (the library theme sets them on `:root`; the component has matching fallbacks) |
|
|
108
110
|
| `--hero-text-font-family` | `"Playfair Display"` | Font family of the whole component |
|
|
109
111
|
| `--hero-text-margin` | `0` | Margin on the root element |
|
|
110
|
-
| `--hero-text-horizontal-gap` | `0.5ch` | Gap between segments in `axis="horizontal"` mode |
|
|
111
112
|
| `--hero-text-vertical-gap` | `0.4em` | Gap between segments in `axis="vertical"` mode |
|
|
112
113
|
| `--hero-text-accent-offset` | `0.2em` | Underline-clearance offset on `.accent` spans (`padding-bottom` and the matching negative `margin-bottom`) |
|
|
113
114
|
|
|
@@ -115,7 +116,7 @@ Key CSS custom properties:
|
|
|
115
116
|
|
|
116
117
|
```css
|
|
117
118
|
.my-page {
|
|
118
|
-
.hero-text
|
|
119
|
+
.hero-text[data-axis="vertical"] {
|
|
119
120
|
--hero-text-vertical-gap: 0.6em;
|
|
120
121
|
}
|
|
121
122
|
}
|
|
@@ -149,6 +150,9 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
|
|
|
149
150
|
|
|
150
151
|
## Notes
|
|
151
152
|
|
|
152
|
-
- Text segments are trimmed and a trailing space is automatically appended between segments
|
|
153
|
+
- Text segments are trimmed and a trailing space is automatically appended between segments — do not manually pad `text` values. Segments with blank or missing `text` (common in CMS data) are dropped.
|
|
154
|
+
- The icon is decorative (`aria-hidden`).
|
|
155
|
+
- Long text and unbroken strings wrap. No line-clamp token: clamping needs `overflow: hidden`, which clips the accent descender offset; `ServiceDetail` clamps its title itself.
|
|
156
|
+
- 2026-10-19 migration: size and axis moved from bare root classes to `data-font-size`/`data-axis`, segments from `.text-block-{n}` to `.hero-text-segment`; dropped the dead `--hero-text-horizontal-gap`; added a `StressTest` story.
|
|
153
157
|
- The icon is sized to match the font size. At `subheading` scale it is `0.75em` (applied as `font-size`, since `width`/`height` on an `<Icon>` are overridden by `@nuxt/icon`).
|
|
154
158
|
- Component is auto-imported in Nuxt — no import needed.
|
|
@@ -26,13 +26,13 @@ Both slots are optional. If neither is provided, no icon wrapper elements are re
|
|
|
26
26
|
## Basic Usage
|
|
27
27
|
|
|
28
28
|
```vue
|
|
29
|
-
<LinkText to="/about"
|
|
29
|
+
<LinkText to="/about" link-text="About Us" />
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
## With Left Icon (back/previous pattern)
|
|
33
33
|
|
|
34
34
|
```vue
|
|
35
|
-
<LinkText to="/blog"
|
|
35
|
+
<LinkText to="/blog" link-text="Back to Blog">
|
|
36
36
|
<template #left>
|
|
37
37
|
<Icon name="lucide:arrow-left" />
|
|
38
38
|
</template>
|
|
@@ -42,7 +42,7 @@ Both slots are optional. If neither is provided, no icon wrapper elements are re
|
|
|
42
42
|
## With Right Icon (forward/more pattern)
|
|
43
43
|
|
|
44
44
|
```vue
|
|
45
|
-
<LinkText to="/services"
|
|
45
|
+
<LinkText to="/services" link-text="Learn More">
|
|
46
46
|
<template #right>
|
|
47
47
|
<Icon name="lucide:arrow-right" />
|
|
48
48
|
</template>
|
|
@@ -52,7 +52,7 @@ Both slots are optional. If neither is provided, no icon wrapper elements are re
|
|
|
52
52
|
## Both Icons
|
|
53
53
|
|
|
54
54
|
```vue
|
|
55
|
-
<LinkText to="/collection"
|
|
55
|
+
<LinkText to="/collection" link-text="Explore">
|
|
56
56
|
<template #left>
|
|
57
57
|
<Icon name="lucide:sparkles" />
|
|
58
58
|
</template>
|
|
@@ -67,7 +67,7 @@ Both slots are optional. If neither is provided, no icon wrapper elements are re
|
|
|
67
67
|
```vue
|
|
68
68
|
<LinkText
|
|
69
69
|
to="https://example.com"
|
|
70
|
-
|
|
70
|
+
link-text="Visit Site"
|
|
71
71
|
:external="true"
|
|
72
72
|
target="_blank"
|
|
73
73
|
>
|
|
@@ -132,3 +132,6 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
|
|
|
132
132
|
- NuxtLink handles both internal (`/path`) and external (`https://...`) URLs automatically. Only set `external: true` if you need to force external handling on an internal-looking path.
|
|
133
133
|
- The label is always rendered inside `.link-text__label` — target this class if you need to style the text independently of the icons.
|
|
134
134
|
- DOM order is always: left icon → label → right icon.
|
|
135
|
+
- Long and unbroken labels wrap inside the column (the root is capped at `max-inline-size: 100%`). There is no line-clamp token: link text names its destination.
|
|
136
|
+
- For an icon-only link, pass an empty `link-text` and an `aria-label`; it falls through to the `<a>`.
|
|
137
|
+
- 2026-10-19 migration: label wrapping, rem focus ring, `StressTest` story, docs text; the spec now unmounts every wrapper.
|
|
@@ -28,8 +28,9 @@ and Escape dismissal.
|
|
|
28
28
|
|------|------|---------|-------------|
|
|
29
29
|
| `placement` | `"top" \| "right" \| "bottom" \| "left"` | `"right"` | Which side of the trigger the popover panel opens on. Each side has a `position-try-fallbacks` so the browser flips it automatically if it would overflow the viewport. |
|
|
30
30
|
| `triggerAriaLabel` | `string` | `""` | aria-label on the trigger button — set this when the `trigger` slot is icon-only. Unset (no attribute) by default, since the trigger slot usually carries its own visible/accessible content. |
|
|
31
|
-
| `popoverAriaLabel` | `string` | `""` |
|
|
32
|
-
| `closeButtonAriaLabel` | `string` | `"Close"` | aria-label
|
|
31
|
+
| `popoverAriaLabel` | `string` | `""` | Names the popover panel and gives it `role="dialog"` (with no label it has no role, since a name on a generic element isn't announced) — set this when the `content` slot has no visible heading. |
|
|
32
|
+
| `closeButtonAriaLabel` | `string` | `"Close"` | The close button's accessible name (its `aria-label` and its visually hidden button text) — override for localisation. |
|
|
33
|
+
| `closeIcon` | `string` | `"lucide:x"` | Iconify name for the close button icon. |
|
|
33
34
|
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
|
|
34
35
|
|
|
35
36
|
## Slots
|
|
@@ -80,11 +81,20 @@ the only source of space above/below the slotted content — a plain `<p>` doesn
|
|
|
80
81
|
does the same in JS. The trigger has `aria-expanded` in every browser.
|
|
81
82
|
- On open, focus moves to the close button (the composable's `onOpen`) so keyboard users land
|
|
82
83
|
inside the panel immediately.
|
|
84
|
+
- The close button is an icon-only `InputButton variant="inline"` with class
|
|
85
|
+
`pop-over-close-button`, pinned to the panel's block-start/inline-end corner, so it sits top left
|
|
86
|
+
in right-to-left text. The content slot's top padding keeps content clear of it.
|
|
87
|
+
- The panel is never wider than the viewport minus `3.2rem` (whatever `--pop-over-width` says) or
|
|
88
|
+
taller than `--pop-over-max-block-size` (default `calc(100dvh - 3.2rem)`); taller content scrolls
|
|
89
|
+
inside the panel. Unbroken strings in the trigger and panel wrap (`overflow-wrap: anywhere`).
|
|
83
90
|
|
|
84
91
|
## CSS custom properties
|
|
85
92
|
|
|
86
93
|
See `CONSUMER-STYLING.md` for the full `--pop-over-*` token API (gap, trigger outline, panel
|
|
87
|
-
border/background/text/shadow/width, content padding, and close-button tokens).
|
|
94
|
+
border/background/text/shadow/width/max-height, content padding, and close-button tokens).
|
|
95
|
+
|
|
96
|
+
No line-clamp tokens: all text arrives through the `trigger` and `content` slots, so the consumer
|
|
97
|
+
clamps it in their own markup.
|
|
88
98
|
|
|
89
99
|
## Notes
|
|
90
100
|
|
|
@@ -101,3 +111,7 @@ border/background/text/shadow/width, content padding, and close-button tokens).
|
|
|
101
111
|
`@position-try` blocks commented out and unused; promoted every hardcoded value to a public CSS
|
|
102
112
|
token; added tests, a Storybook story, this skill doc, and a VS Code snippet (all previously
|
|
103
113
|
missing).
|
|
114
|
+
- 2026-10-19 migration: the close button became an `InputButton variant="inline"` (it was a raw
|
|
115
|
+
`<button>`) and gained a `closeIcon` prop; it now uses logical insets. The panel caps at the
|
|
116
|
+
viewport width and height and wraps unbroken strings; the trigger wraps too. Added a
|
|
117
|
+
`StressTest` story and per-story docs descriptions.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: SlidingToggle
|
|
3
|
+
description: SlidingToggle single-choice segmented radiogroup with text labels of any width, measured sliding indicator (stretch-then-catch-up) plus hover indicator, corner-shape token, CSS token API
|
|
4
|
+
type: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# SlidingToggle
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
A single-choice segmented control for short text choices such as a billing period or a site tier.
|
|
12
|
+
The selected option's background slides between options of different widths, with the same
|
|
13
|
+
stretch-then-catch-up motion as `TabbedContent`'s indicator (both use `useSlidingIndicator`), and a
|
|
14
|
+
second indicator follows the pointer. It's a WAI-ARIA radio group of `<button role="radio">`s, not
|
|
15
|
+
a form control: there are no native inputs and nothing is submitted.
|
|
16
|
+
|
|
17
|
+
Lives in `app/components/02.molecules/sliding-toggle/`.
|
|
18
|
+
|
|
19
|
+
**Which one to use:**
|
|
20
|
+
|
|
21
|
+
- `SlidingToggle` — text labels of any width, page UI state (pricing toggles, view switches).
|
|
22
|
+
- `SegmentedControl` (`05.forms`) — icon-only, equal-width options, native radio inputs that submit
|
|
23
|
+
with a form.
|
|
24
|
+
- `TabbedContent` — the choice switches between panels of content.
|
|
25
|
+
|
|
26
|
+
## Props
|
|
27
|
+
|
|
28
|
+
| Prop (template form) | Type | Default | Notes |
|
|
29
|
+
|---|---|---|---|
|
|
30
|
+
| `v-model` | `T` (`string \| number`) | (required) | The selected option's `value`. A value matching no option leaves nothing selected. |
|
|
31
|
+
| `options` | `SlidingToggleOption<T>[]` | (required) | `{ value, label, disabled? }`. Labels are plain text. |
|
|
32
|
+
| `aria-label` | `string` | `""` | Names the radiogroup. Set this or `aria-labelledby`. |
|
|
33
|
+
| `aria-labelledby` | `string` | `""` | Id of a visible heading naming the group. |
|
|
34
|
+
| `disabled` | `boolean` | `false` | Disables every option. |
|
|
35
|
+
| `track-hover` | `boolean` | `true` | Show the hover indicator. |
|
|
36
|
+
| `transition-duration` | `number` (ms) | `200` | Length of each edge's slide. |
|
|
37
|
+
| `style-class-passthrough` | `string \| string[]` | `[]` | Classes on the root. |
|
|
38
|
+
|
|
39
|
+
`SlidingToggleOption` is exported from `~/types/components`.
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
```vue
|
|
44
|
+
<script setup lang="ts">
|
|
45
|
+
const billing = ref<"monthly" | "yearly">("monthly");
|
|
46
|
+
const billingOptions = [
|
|
47
|
+
{ value: "monthly", label: "Monthly" },
|
|
48
|
+
{ value: "yearly", label: "Yearly (save 20%)" },
|
|
49
|
+
] as const;
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<template>
|
|
53
|
+
<SlidingToggle v-model="billing" :options="[...billingOptions]" aria-label="Billing period" />
|
|
54
|
+
</template>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Equal widths across a full-width switch:
|
|
58
|
+
|
|
59
|
+
```css
|
|
60
|
+
.billing-switch {
|
|
61
|
+
--sliding-toggle-option-flex: 1 1 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.billing-switch .sliding-toggle {
|
|
65
|
+
inline-size: 100%;
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Behaviour
|
|
70
|
+
|
|
71
|
+
- The root is `role="radiogroup"`, named by `aria-label` or `aria-labelledby`. Each option is a
|
|
72
|
+
`<button type="button" role="radio">` with `aria-checked`.
|
|
73
|
+
- Clicking an option sets `v-model`; changing `v-model` from outside moves the selection.
|
|
74
|
+
- Only the selected option is a tab stop. With no match, the first enabled option is.
|
|
75
|
+
- Arrow keys move focus and select, wrapping round and skipping disabled options. Right and Down go
|
|
76
|
+
to the next option, Left and Up to the previous; inside `dir="rtl"` Left and Right swap. Home and
|
|
77
|
+
End go to the first and last enabled option.
|
|
78
|
+
- `disabled` on the group or on one option blocks clicks and keys; disabled options are skipped and
|
|
79
|
+
never get the hover indicator.
|
|
80
|
+
- The selected indicator slides to the new option, its leading edge first and the trailing edge one
|
|
81
|
+
`transition-duration` later. The hover indicator follows the pointer and returns to the selected
|
|
82
|
+
option when it leaves; `track-hover="false"` turns it off. `--sliding-toggle-indicator-stretch: 0`
|
|
83
|
+
makes both indicators slide their edges together instead of stretching, mirroring TabbedContent's
|
|
84
|
+
`--tabs-indicator-stretch`.
|
|
85
|
+
- Under `prefers-reduced-motion` both indicators snap.
|
|
86
|
+
- Before it's mounted and measured (SSR, first paint) the indicators are hidden and the selected
|
|
87
|
+
option paints its own background and marker, so there's no flash of an unselected group.
|
|
88
|
+
- It re-measures when the track or any single option resizes (a late web font changes label widths
|
|
89
|
+
without resizing the track) and when options are added, removed or re-classed.
|
|
90
|
+
- Long, unbroken and many labels wrap and shrink inside the track instead of overflowing.
|
|
91
|
+
|
|
92
|
+
## CSS custom properties
|
|
93
|
+
|
|
94
|
+
See `CONSUMER-STYLING.md` for the full `--sliding-toggle-*` token API: track surface, padding, gap,
|
|
95
|
+
radii, `--sliding-toggle-corner-shape`, indicator and hover-indicator surfaces, marker, option
|
|
96
|
+
padding/flex/typography/colours and the focus ring. Colours default from `--theme-*` tokens.
|
|
97
|
+
|
|
98
|
+
`--sliding-toggle-corner-shape` takes any `corner-shape` value (`squircle`, `superellipse(n)`,
|
|
99
|
+
`bevel`, `scoop`, `notch`). It's a progressive enhancement: Chromium 139+ only, and other browsers
|
|
100
|
+
keep the `border-radius` corners.
|
|
101
|
+
|
|
102
|
+
## Notes
|
|
103
|
+
|
|
104
|
+
- Auto-imported in Nuxt.
|
|
105
|
+
- The motion comes from the `useSlidingIndicator` composable, shared with `TabbedContent` (via
|
|
106
|
+
`useTabs`). It writes `--_sliding-toggle-active-*` / `--_sliding-toggle-hover-*` on the root.
|
|
107
|
+
- Added 2026-10-19, generalised from a consumer app's pricing-page toggle (which used `aria-pressed`
|
|
108
|
+
buttons; this version uses radio semantics).
|
|
@@ -137,6 +137,10 @@ source order when the list changes.
|
|
|
137
137
|
|
|
138
138
|
## Indicator motion: edge insets (2026-10-08)
|
|
139
139
|
|
|
140
|
+
> **Changed 2026-10-19**: the edge maths moved into `useSlidingIndicator` (`composable-sliding-indicator.md`),
|
|
141
|
+
> shared with `SlidingToggle`. `useTabs.placeIndicator` now calls it and keeps the label-coverage
|
|
142
|
+
> tracking. Under `prefers-reduced-motion` the indicators now snap instead of sliding.
|
|
143
|
+
|
|
140
144
|
`useTabs.placeIndicator(kind, animate)` writes each indicator's two edges as insets from the
|
|
141
145
|
bar's start/end along the axis (`--_{kind}-start` / `--_{kind}-end`), and the CSS transitions
|
|
142
146
|
`left`/`right` (or `top`/`bottom`) separately. Moving forward, the start edge gets a delay of one
|
|
@@ -145,9 +149,10 @@ and the trailing edge catches up: the same stretch effect as before, symmetric b
|
|
|
145
149
|
settle timer. CSS retargets each edge from its current position, so a quick second move can't
|
|
146
150
|
pull an edge backwards. Hover and active have separate duration/delay variables.
|
|
147
151
|
|
|
148
|
-
`--tabs-
|
|
149
|
-
|
|
150
|
-
|
|
152
|
+
`--tabs-indicator-stretch` (default `1`) multiplies every indicator's two edge delays in CSS, so `0`
|
|
153
|
+
turns the stretch into a plain slide (both edges move together over the same duration) for the
|
|
154
|
+
hover, active and underline indicators alike. Renamed 2026-10-19 from `--tabs-hover-indicator-stretch`,
|
|
155
|
+
which covered the hover pair only.
|
|
151
156
|
|
|
152
157
|
This replaced a `translate` + `scale` model (a bar-wide box translated to the tab's left and
|
|
153
158
|
scaled to its width, plus a timeout to settle after a stretch, and one shared
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# useSlidingIndicator
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Positions one or more sliding indicators (a highlight or underline that moves between items) inside
|
|
6
|
+
a container, with a stretch-then-catch-up motion: the edge in the direction of travel moves first
|
|
7
|
+
and the trailing edge follows one duration later. Used by `useTabs` (TabbedContent) and
|
|
8
|
+
`SlidingToggle`. Lives in `app/composables/useSlidingIndicator.ts`, auto-imported.
|
|
9
|
+
|
|
10
|
+
## API
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
const { place, reset } = useSlidingIndicator(containerRef, { axis?: "x" | "y", duration });
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
| Param | Type | Notes |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| `containerRef` | `Ref<HTMLElement \| null>` | The positioned element the indicators sit in. The CSS variables are written on it. |
|
|
19
|
+
| `axis` | `MaybeRefOrGetter<"x" \| "y">` | Default `"x"`. |
|
|
20
|
+
| `duration` | `MaybeRefOrGetter<number>` | Slide length per edge, in ms. Invalid or negative values count as `0`. |
|
|
21
|
+
|
|
22
|
+
`place(name, target, animate)` measures `target` (its `offsetLeft`/`offsetTop` and size against the
|
|
23
|
+
container's client size) and writes, on the container:
|
|
24
|
+
|
|
25
|
+
- `--_{name}-start` / `--_{name}-end` — insets from the container's start and end edges, in px;
|
|
26
|
+
- `--_{name}-duration`, `--_{name}-start-delay`, `--_{name}-end-delay`.
|
|
27
|
+
|
|
28
|
+
It returns the transition length in ms, or `null` when the container or target is missing (nothing
|
|
29
|
+
written). The first placement of a name, `animate: false`, and `prefers-reduced-motion` all snap
|
|
30
|
+
(duration `0ms`).
|
|
31
|
+
|
|
32
|
+
`reset(name)` forgets the last position, so the next placement snaps instead of sliding in from
|
|
33
|
+
wherever the indicator last was (use it when an indicator is hidden).
|
|
34
|
+
|
|
35
|
+
## CSS side
|
|
36
|
+
|
|
37
|
+
```css
|
|
38
|
+
.my-indicator {
|
|
39
|
+
position: absolute;
|
|
40
|
+
left: var(--_my-thing-active-start, 0px);
|
|
41
|
+
right: var(--_my-thing-active-end, 100%);
|
|
42
|
+
transition:
|
|
43
|
+
left var(--_my-thing-active-duration, 0ms) ease var(--_my-thing-active-start-delay, 0ms),
|
|
44
|
+
right var(--_my-thing-active-duration, 0ms) ease var(--_my-thing-active-end-delay, 0ms);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Use physical `left`/`right` (and `top`/`bottom` for `"y"`): the measurements are physical, so this
|
|
49
|
+
also works in right-to-left layouts.
|
|
50
|
+
|
|
51
|
+
## Rules
|
|
52
|
+
|
|
53
|
+
- Prefix the `name` with the component (`"sliding-toggle-active"`), so a component nested inside
|
|
54
|
+
another that uses the composable can't inherit its values (pitfall #26). `useTabs` keeps the
|
|
55
|
+
historic `"active"`/`"hovered"` names that TabbedContent's CSS reads.
|
|
56
|
+
- Re-place with `animate: false` when the container or any measured item resizes, and when items
|
|
57
|
+
are added or removed (`measured-layout-observers.md`). Don't observe `style` changes: the
|
|
58
|
+
composable writes inline styles on the container.
|
|
59
|
+
- Keep the indicators hidden until the first placement (SlidingToggle's `data-ready`), and give the
|
|
60
|
+
selected item its own fallback background for SSR and first paint.
|
package/.claude/skills/index.md
CHANGED
|
@@ -75,6 +75,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
75
75
|
├── composable-cookie-consent.md — useCookieConsent: unset/granted/denied state, cookie persistence, wraps @nuxt/scripts' useScriptTriggerConsent
|
|
76
76
|
├── composable-analytics.md — useAnalytics: provider-agnostic trackEvent/page-view tracking (google-analytics only implemented), consent-gated, single call site for setup + firing events
|
|
77
77
|
├── composable-anchored-popover.md — useAnchoredPopover: popover + anchor positioning with JS fallbacks for Safari 16 (no Popover API) and 17–18 (no anchor positioning); used by ActionMenu/SelectMenu/DeepExpandingMenu/DisplayTooltip/PopOver
|
|
78
|
+
├── composable-sliding-indicator.md — useSlidingIndicator: stretch-then-catch-up sliding indicator edges written as --_{name}-* vars, reduced-motion snap; used by useTabs/TabbedContent and SlidingToggle
|
|
78
79
|
└── components/
|
|
79
80
|
├── alert-content.md — AlertContent + AlertContentInner: themed alert panel (icon/title/content/actions/dismiss) under DisplayToast, DisplayPrompt and AlertMaskedContent; showIcon, #actions row, --alert-content-* tokens, app.config icon map
|
|
80
81
|
├── alert-mask.md — AlertMask (renamed from AlertMaskCore): SVG border/fill sized to slotted content via ResizeObserver; config-prop geometry, --alert-mask-* colour tokens
|
|
@@ -164,6 +165,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
164
165
|
├── input-radio.md — MultipleRadiobuttons: fieldset radiogroup (data-driven, button or labelled), shared-name grouping bug fixed, dead label/placeholder/equalCols props removed, data-options-layout hook, spacing token API, useId button ids, wraps hostile content
|
|
165
166
|
├── toggle-switch.md — ToggleSwitch (bare control), ToggleSwitchField (labelled, layout="stacked"|"inline", replaces ToggleSwitchWithLabelInline): renamed from Core/WithLabel, role="switch", prefixed inner classes + data-checked, single-toggle click handling, reduced motion, round prop + border-radius token, track width from geometry, CSS token API, stress stories
|
|
166
167
|
├── segmented-control.md — SegmentedControl (renamed from TripleToggleSwitch): icon radio pill for any number of options with sliding marker, radiogroup + ariaLabel, system/light/dark marker-gradient tokens, reduced-motion, CSS token API
|
|
168
|
+
├── sliding-toggle.md — SlidingToggle: single-choice segmented radiogroup with text labels of any width, measured sliding + hover indicators (useSlidingIndicator), roving tabindex/arrow keys, corner-shape token, CSS token API
|
|
167
169
|
├── entry-animation.md — EntryAnimation: scroll-driven entry animation wrapper (slide-in/zoom-reveal/exit-blur utility classes), skipAnimation for above-the-fold loop items, reduced-motion handled at the CSS layer
|
|
168
170
|
├── column-flow-grid.md — ColumnFlowGrid (renamed from MasonryGrid): CSS multi-column text-flow layout, named dynamic slots (no count/data prop), itemMinWidth/gap/unit sizing, CSS token API; not a true masonry, see "which one do I want?"
|
|
169
171
|
├── masonry-grid.md — MasonryGrid: real measured-height masonry (greedy shortest-column packing, animated resize) absorbed from the now-retired MasonryGridOrdered; named dynamic slots, fixedWidth/justify, CSS token API
|
|
@@ -172,6 +172,23 @@ const Template: StoryFn<StoryArgs> = (args) => ({
|
|
|
172
172
|
|
|
173
173
|
Don't name the inline component the same as an exported story (`TS2451: Cannot redeclare`).
|
|
174
174
|
|
|
175
|
+
### Generic components (`<script setup generic="T">`)
|
|
176
|
+
|
|
177
|
+
A generic component's type doesn't fit `Meta`'s `component` field (`ConcreteComponent<Args>`), so
|
|
178
|
+
`npx nuxi typecheck` fails on `component: MyGeneric`. Cast it, with a one-line comment; the
|
|
179
|
+
autodocs props table still generates:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
import type { ConcreteComponent } from "vue";
|
|
183
|
+
|
|
184
|
+
const meta: Meta<StoryArgs> = {
|
|
185
|
+
// Generic components don't fit Meta's ConcreteComponent<Args>; the props table still generates.
|
|
186
|
+
component: SlidingToggle as unknown as ConcreteComponent<StoryArgs>,
|
|
187
|
+
};
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`SlidingToggle.stories.ts` is the reference.
|
|
191
|
+
|
|
175
192
|
## Title format
|
|
176
193
|
|
|
177
194
|
`"Category/Subcategory/ComponentName"` — this becomes the Storybook sidebar path and the
|
|
@@ -180,9 +197,12 @@ Playwright `STORY_BASE` slug. The slug is derived by lowercasing and replacing s
|
|
|
180
197
|
|
|
181
198
|
| Title | STORY_BASE slug |
|
|
182
199
|
| --------------------------------------------- | ------------------------------------------- |
|
|
183
|
-
| `"Atoms/Text Blocks/
|
|
200
|
+
| `"Atoms/Text Blocks/Hero Text"` | `atoms-text-blocks-hero-text` |
|
|
184
201
|
| `"Components/Forms/Input Text/InputText"` | `components-forms-input-text-inputtext` |
|
|
185
202
|
|
|
203
|
+
Renaming a title changes the slug: update any `playwright/*.playwright.ts` `STORY_BASE` and any
|
|
204
|
+
story links (`?path=/story/...`) that point at it.
|
|
205
|
+
|
|
186
206
|
## argTypes control types
|
|
187
207
|
|
|
188
208
|
| Prop type | `control` |
|
|
@@ -274,7 +294,7 @@ export const Default: Story = {
|
|
|
274
294
|
Key points:
|
|
275
295
|
|
|
276
296
|
- `computed` is **not** auto-imported in `.ts` story files — import it explicitly from `"vue"`.
|
|
277
|
-
- Components aren't auto-registered in a story's `template` string either. Every component used there, including ones you put in the component's slots (`EyebrowText`, `HeroText`, `SocialIconsList`, ...), must be imported and listed in `components: {}`. Otherwise Vue renders it as an empty unknown element (`<eyebrowtext>`) with no error. ProfileSection's header and social-icons stories rendered blank this way until 2026-10-14.
|
|
297
|
+
- Components aren't auto-registered in a story's `template` string either. Every component used there, including ones you put in the component's slots (`EyebrowText`, `HeroText`, `SocialIconsList`, ...), must be imported and listed in `components: {}`. Otherwise Vue renders it as an empty unknown element (`<eyebrowtext>`) with no error. ProfileSection's header and social-icons stories rendered blank this way until 2026-10-14. Globally registered components are the exception and need no entry: `Icon` (`@nuxt/icon` registers it with `global: true`) and `NuxtLink`.
|
|
278
298
|
- Props in a story's `template` string aren't type-checked either. An invalid value renders silently with no warning: ProfileSection's stories passed `font-size="large"` to `HeroText`, which only has `display`/`title`/`heading`/`subheading`/`label`, so the heading fell back to unstyled text. Check every literal prop value against the component's `interface Props`.
|
|
279
299
|
- Extra args must be stripped before `v-bind` — spreading unknown keys onto a component makes them unknown HTML attributes.
|
|
280
300
|
- CSS custom properties set via `:style` on the component root are picked up by `var()` in the component's scoped CSS.
|
|
@@ -99,13 +99,18 @@ test.describe("ComponentName — combinations", () => {
|
|
|
99
99
|
|
|
100
100
|
## Deriving the STORY_BASE slug
|
|
101
101
|
|
|
102
|
-
Take the story `title` from the `.stories.ts` file, lowercase everything,
|
|
103
|
-
and
|
|
104
|
-
|
|
105
|
-
| Story `title`
|
|
106
|
-
|
|
|
107
|
-
| `"Atoms/Text Blocks/
|
|
108
|
-
| `"Components/Forms/Input Button/InputButton"` | `components-forms-input-button-
|
|
102
|
+
Take the story `title` from the `.stories.ts` file, lowercase everything, and replace both spaces
|
|
103
|
+
and `/` with `-`:
|
|
104
|
+
|
|
105
|
+
| Story `title` | `STORY_BASE` |
|
|
106
|
+
| --------------------------------------------- | ------------------------------------------- |
|
|
107
|
+
| `"Atoms/Text Blocks/Hero Text"` | `atoms-text-blocks-hero-text` |
|
|
108
|
+
| `"Components/Forms/Input Button/InputButton"` | `components-forms-input-button-inputbutton` |
|
|
109
|
+
|
|
110
|
+
`STORY_BASE` is hardcoded, and nothing (Vitest, ESLint, the Component Ledger) checks it. When you
|
|
111
|
+
change a story `title`, update `STORY_BASE` in the component's `playwright/` folder in the same
|
|
112
|
+
change. EyebrowText and HeroText pointed at ids from an older title (`...-eyebrowtext`,
|
|
113
|
+
`...-herotext`) until 2026-10-19, so their suites loaded a missing story.
|
|
109
114
|
|
|
110
115
|
The full story URL pattern is:
|
|
111
116
|
|