srcdev-nuxt-components 9.6.13 → 9.6.14
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/storybook-add-story.md +5 -2
- package/.claude/skills/testing-add-playwright.md +12 -7
- 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/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/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/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/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.
|
|
@@ -180,9 +180,12 @@ Playwright `STORY_BASE` slug. The slug is derived by lowercasing and replacing s
|
|
|
180
180
|
|
|
181
181
|
| Title | STORY_BASE slug |
|
|
182
182
|
| --------------------------------------------- | ------------------------------------------- |
|
|
183
|
-
| `"Atoms/Text Blocks/
|
|
183
|
+
| `"Atoms/Text Blocks/Hero Text"` | `atoms-text-blocks-hero-text` |
|
|
184
184
|
| `"Components/Forms/Input Text/InputText"` | `components-forms-input-text-inputtext` |
|
|
185
185
|
|
|
186
|
+
Renaming a title changes the slug: update any `playwright/*.playwright.ts` `STORY_BASE` and any
|
|
187
|
+
story links (`?path=/story/...`) that point at it.
|
|
188
|
+
|
|
186
189
|
## argTypes control types
|
|
187
190
|
|
|
188
191
|
| Prop type | `control` |
|
|
@@ -274,7 +277,7 @@ export const Default: Story = {
|
|
|
274
277
|
Key points:
|
|
275
278
|
|
|
276
279
|
- `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.
|
|
280
|
+
- 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
281
|
- 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
282
|
- Extra args must be stripped before `v-bind` — spreading unknown keys onto a component makes them unknown HTML attributes.
|
|
280
283
|
- 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
|
|
|
@@ -15,8 +15,19 @@
|
|
|
15
15
|
" aria-label=\"$2\"",
|
|
16
16
|
" black-color=\"${3:currentColor}\"",
|
|
17
17
|
" :radius=\"${4:0.5}\"",
|
|
18
|
-
" :variant=\"{ inner: '${5|default,circle,rounded,dots,
|
|
18
|
+
" :variant=\"{ inner: '${5|default,circle,rounded,dots,pixelated|}', marker: '${6|default,circle,rounded,dots,pixelated|}', pixel: '${7|default,circle,rounded,dots,pixelated|}' }\"",
|
|
19
19
|
"/>"
|
|
20
20
|
]
|
|
21
|
+
},
|
|
22
|
+
"SRCDEV DisplayQrCode Fallback": {
|
|
23
|
+
"description": "DisplayQrCode with a fallback for values too long to encode",
|
|
24
|
+
"scope": "vue,html",
|
|
25
|
+
"body": [
|
|
26
|
+
"<DisplayQrCode :qr-value=\"${1:url}\" aria-label=\"${2:QR code}\" @error=\"${3:onQrError}\">",
|
|
27
|
+
" <template #fallback>",
|
|
28
|
+
" $0",
|
|
29
|
+
" </template>",
|
|
30
|
+
"</DisplayQrCode>"
|
|
31
|
+
]
|
|
21
32
|
}
|
|
22
33
|
}
|
|
@@ -4,11 +4,27 @@
|
|
|
4
4
|
"scope": "vue,html",
|
|
5
5
|
"body": [
|
|
6
6
|
"<HeroText",
|
|
7
|
-
" tag=\"$
|
|
8
|
-
"
|
|
9
|
-
" :
|
|
10
|
-
" { text: '$
|
|
11
|
-
" { text: '$
|
|
7
|
+
" tag=\"${1|h1,h2,h3,h4,h5,h6|}\"",
|
|
8
|
+
" font-size=\"${2|title,display,heading,subheading,label|}\"",
|
|
9
|
+
" :text-content=\"[",
|
|
10
|
+
" { text: '${3:Designing}', styleClass: 'normal' },",
|
|
11
|
+
" { text: '${4:Artistry}', styleClass: 'accent' },",
|
|
12
|
+
" ]\"",
|
|
13
|
+
"/>"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"SRCDEV HeroText Vertical With Icon": {
|
|
17
|
+
"description": "HeroText stacked vertically with a decorative icon",
|
|
18
|
+
"scope": "vue,html",
|
|
19
|
+
"body": [
|
|
20
|
+
"<HeroText",
|
|
21
|
+
" tag=\"${1|h2,h1,h3,h4,h5,h6|}\"",
|
|
22
|
+
" axis=\"vertical\"",
|
|
23
|
+
" icon=\"${2:lucide:sparkles}\"",
|
|
24
|
+
" font-size=\"${3|display,title,heading,subheading,label|}\"",
|
|
25
|
+
" :text-content=\"[",
|
|
26
|
+
" { text: '${4:Modern}', styleClass: 'normal' },",
|
|
27
|
+
" { text: '${5:Living}', styleClass: 'accent' },",
|
|
12
28
|
" ]\"",
|
|
13
29
|
"/>"
|
|
14
30
|
]
|
|
@@ -19,11 +19,11 @@
|
|
|
19
19
|
"description": "CSS override scaffold for LinkText tokens",
|
|
20
20
|
"scope": "css",
|
|
21
21
|
"body": [
|
|
22
|
-
".$
|
|
22
|
+
".${1:my-page} {",
|
|
23
23
|
" --link-text-colour: $2;",
|
|
24
24
|
" --link-text-colour-hover: $3;",
|
|
25
|
-
" --link-text-decoration: $
|
|
26
|
-
" --link-text-decoration-hover: $
|
|
25
|
+
" --link-text-decoration: ${4:underline};",
|
|
26
|
+
" --link-text-decoration-hover: ${5:none};",
|
|
27
27
|
"}"
|
|
28
28
|
]
|
|
29
29
|
}
|
|
@@ -40,5 +40,33 @@
|
|
|
40
40
|
" </template>",
|
|
41
41
|
"</PopOver>"
|
|
42
42
|
]
|
|
43
|
+
},
|
|
44
|
+
"SRCDEV PopOver Localised Close": {
|
|
45
|
+
"description": "PopOver with a localised close label and a custom close icon",
|
|
46
|
+
"scope": "vue,html",
|
|
47
|
+
"body": [
|
|
48
|
+
"<PopOver close-button-aria-label=\"${1:Close}\" close-icon=\"${2:lucide:x}\">",
|
|
49
|
+
" <template #trigger>",
|
|
50
|
+
" <span>$3</span>",
|
|
51
|
+
" </template>",
|
|
52
|
+
" <template #content>",
|
|
53
|
+
" <p>$0</p>",
|
|
54
|
+
" </template>",
|
|
55
|
+
"</PopOver>"
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
"SRCDEV PopOver CSS Overrides": {
|
|
59
|
+
"description": "Unlayered CSS override scaffold for PopOver public tokens",
|
|
60
|
+
"scope": "css,vue",
|
|
61
|
+
"body": [
|
|
62
|
+
".${1:my-section} {",
|
|
63
|
+
" --pop-over-width: ${2:30rem};",
|
|
64
|
+
" --pop-over-gap: ${3:1rem};",
|
|
65
|
+
" --pop-over-background-colour: ${4:var(--slate-00)};",
|
|
66
|
+
" --pop-over-text-colour: ${5:var(--slate-09)};",
|
|
67
|
+
" --pop-over-border-colour: ${6:var(--slate-02)};",
|
|
68
|
+
" --pop-over-content-padding: ${7:1.6rem};$0",
|
|
69
|
+
"}"
|
|
70
|
+
]
|
|
43
71
|
}
|
|
44
72
|
}
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
| `--pop-over-gap` | `1rem` | Space between the trigger and the popover panel |
|
|
8
8
|
| `--pop-over-trigger-outline-width` | `0.1rem` | Trigger focus outline width |
|
|
9
9
|
| `--pop-over-trigger-outline-colour-hover` | `var(--theme-ring)` | Trigger outline colour on focus-visible |
|
|
10
|
-
| `--pop-over-width` | `30rem` | Popover panel width |
|
|
10
|
+
| `--pop-over-width` | `30rem` | Popover panel width, capped at the viewport width minus `3.2rem` |
|
|
11
|
+
| `--pop-over-max-block-size` | `calc(100dvh - 3.2rem)` | Popover panel maximum height; taller content scrolls inside the panel |
|
|
11
12
|
| `--pop-over-border-width` | `0.1rem` | Popover panel border width |
|
|
12
13
|
| `--pop-over-border-colour` | `var(--slate-02)` | Popover panel border colour |
|
|
13
14
|
| `--pop-over-border-radius` | `0.8rem` | Popover panel corner radius |
|
|
@@ -18,7 +19,7 @@
|
|
|
18
19
|
| `--pop-over-z-index` | `999999` | Panel stacking order in browsers without CSS anchor positioning. Ignored where the panel renders in the top layer |
|
|
19
20
|
| `--pop-over-content-padding` | `1.6rem` | Padding around the `content` slot |
|
|
20
21
|
| `--pop-over-content-padding-block-start` | `3.2rem` | Extra top padding so content clears the close button |
|
|
21
|
-
| `--pop-over-close-button-offset` | `0.8rem` | Distance of the close button from the panel's top
|
|
22
|
+
| `--pop-over-close-button-offset` | `0.8rem` | Distance of the close button from the panel's block-start and inline-end edges (top right in left-to-right text, top left in right-to-left) |
|
|
22
23
|
| `--pop-over-close-button-size` | `2.4rem` | Close button box width/height |
|
|
23
24
|
| `--pop-over-close-button-border-radius` | `50%` | Close button corner radius |
|
|
24
25
|
| `--pop-over-close-button-colour` | `var(--slate-06)` | Close button icon colour |
|
|
@@ -27,6 +28,41 @@
|
|
|
27
28
|
| `--pop-over-close-button-surface-hover` | `var(--slate-01)` | Close button background on hover/focus-visible |
|
|
28
29
|
| `--pop-over-close-button-outline-colour-hover` | `var(--theme-ring)` | Close button outline colour on hover/focus-visible |
|
|
29
30
|
|
|
31
|
+
Private tokens (not public API): `--_anchor-name` (the generated CSS anchor name), and `--_anchor-top`, `--_anchor-right`, `--_anchor-bottom`, `--_anchor-left`, and their `-inverse` forms (measured trigger edges, set by `useAnchoredPopover` only where CSS anchor positioning is unsupported).
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## State hooks
|
|
36
|
+
|
|
37
|
+
| Hook | Where | Meaning |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `[data-placement]` | `.pop-over` root | The `placement` prop: `top`, `right`, `bottom` or `left` |
|
|
40
|
+
| `[data-placement]` | `.pop-over-popover` | The resolved side, which can be the opposite of the prop when there is no room (only set where CSS anchor positioning is unsupported) |
|
|
41
|
+
| `:popover-open` | `.pop-over-popover` | Panel open, where the Popover API exists |
|
|
42
|
+
| `.pop-over-popover-open` | `.pop-over-popover` | Panel open on Safari 16 (no Popover API). Style it in a separate rule from `:popover-open` |
|
|
43
|
+
| `[role="dialog"]` | `.pop-over-popover` | Present only when `popoverAriaLabel` is set |
|
|
44
|
+
|
|
45
|
+
Inner class names: `.pop-over-trigger` (the trigger `<button>`), `.pop-over-popover` (the panel),
|
|
46
|
+
`.pop-over-close-button` (an `InputButton variant="inline"`, so also `.input-button`),
|
|
47
|
+
`.pop-over-close-button-icon`, `.pop-over-content` (wraps the `content` slot).
|
|
48
|
+
|
|
49
|
+
> Changed 2026-10-19: the close button is now an `InputButton` (`variant="inline"`, icon-only, with
|
|
50
|
+
> the `closeButtonAriaLabel` text visually hidden) instead of a raw `<button>`, and sits at the
|
|
51
|
+
> inline end, so it moves to the left in right-to-left text. The panel now caps at the viewport
|
|
52
|
+
> width and height and wraps unbroken strings.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Local overrides
|
|
57
|
+
|
|
58
|
+
Set the tokens above on an element you own (a page or section class, or a class added with
|
|
59
|
+
`:style-class-passthrough`): they inherit down into the component. Keep the block **unlayered** (no
|
|
60
|
+
`@layer` wrapper) so it beats the library's `@layer components`. If your own file uses
|
|
61
|
+
`<style scoped>`, tokens set on your element still work, but selectors that reach inside the component need
|
|
62
|
+
`:deep()`. Patterns and examples: `.claude/skills/component-local-style-override.md`.
|
|
63
|
+
|
|
64
|
+
### Page or section
|
|
65
|
+
|
|
30
66
|
```css
|
|
31
67
|
.my-page {
|
|
32
68
|
--pop-over-background-colour: #1a1a1a;
|
|
@@ -35,7 +71,7 @@
|
|
|
35
71
|
}
|
|
36
72
|
```
|
|
37
73
|
|
|
38
|
-
|
|
74
|
+
### One instance
|
|
39
75
|
|
|
40
76
|
```vue
|
|
41
77
|
<PopOver style-class-passthrough="promo-pop-over">...</PopOver>
|
|
@@ -43,13 +79,10 @@ Or scope to a single instance via `styleClassPassthrough`:
|
|
|
43
79
|
|
|
44
80
|
---
|
|
45
81
|
|
|
46
|
-
##
|
|
82
|
+
## Class passthrough
|
|
47
83
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
`@layer` wrapper) so it beats the library's `@layer components`. If your own file uses
|
|
51
|
-
`<style scoped>`, tokens set on your element still work, but selectors that reach inside the component need
|
|
52
|
-
`:deep()`. Patterns and examples: `.claude/skills/component-local-style-override.md`.
|
|
84
|
+
`style-class-passthrough` adds classes to the `.pop-over` root, which wraps both the trigger and the
|
|
85
|
+
panel, so tokens set on that class reach the panel too.
|
|
53
86
|
|
|
54
87
|
---
|
|
55
88
|
|