srcdev-nuxt-components 9.6.0 → 9.6.1
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/skills/component-compliance-checklist.md +8 -2
- package/.claude/skills/component-dynamic-slots.md +3 -3
- package/.claude/skills/component-export-types.md +10 -16
- package/.claude/skills/component-naming.md +1 -1
- package/.claude/skills/components/alert-content.md +9 -1
- package/.claude/skills/components/container-glow.md +21 -8
- package/.claude/skills/components/display-avatar.md +13 -7
- package/.claude/skills/components/display-chip.md +37 -24
- package/.claude/skills/components/display-pill.md +58 -10
- package/.claude/skills/components/display-prompt.md +8 -5
- package/.claude/skills/components/display-toast.md +21 -4
- package/.claude/skills/components/glowing-border.md +20 -2
- package/.claude/skills/components/responsive-header.md +11 -1
- package/.claude/skills/components/select-menu.md +54 -5
- package/.claude/skills/components/site-header.md +2 -2
- package/.claude/skills/components/tabbed-content.md +300 -0
- package/.claude/skills/composable-anchor-scroll.md +16 -15
- package/.claude/skills/index.md +4 -4
- package/.claude/skills/storybook-add-story.md +53 -0
- package/.claude/skills/theming-status-tokens.md +60 -0
- package/.vscode/srcdev-component-display-avatar.code-snippets +2 -1
- package/.vscode/srcdev-component-display-chip.code-snippets +3 -2
- package/.vscode/srcdev-component-display-pill.code-snippets +10 -0
- package/.vscode/srcdev-component-responsive-header.code-snippets +6 -0
- package/.vscode/srcdev-component-select-menu.code-snippets +28 -0
- package/.vscode/{srcdev-component-tabs-core.code-snippets → srcdev-component-tabbed-content.code-snippets} +35 -16
- package/app/assets/styles/setup/03.theming/_status.css +24 -0
- package/app/assets/styles/setup/03.theming/index.css +1 -0
- package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +39 -35
- package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +25 -15
- package/app/components/01.atoms/animations/container-glow/stories/ContainerGlow.stories.ts +130 -24
- package/app/components/01.atoms/animations/container-glow/tests/ContainerGlow.spec.ts +45 -0
- package/app/components/01.atoms/animations/container-glow/tests/__snapshots__/ContainerGlow.spec.ts.snap +2 -2
- package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +76 -19
- package/app/components/01.atoms/animations/glowing-border/GlowingBorder.vue +12 -10
- package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +201 -42
- package/app/components/01.atoms/animations/glowing-border/tests/GlowingBorder.spec.ts +15 -4
- package/app/components/01.atoms/animations/glowing-border/tests/__snapshots__/GlowingBorder.spec.ts.snap +1 -1
- package/app/components/01.atoms/display-pill/CONSUMER-STYLING.md +75 -9
- package/app/components/01.atoms/display-pill/DisplayPill.vue +57 -24
- package/app/components/01.atoms/display-pill/stories/DisplayPill.stories.ts +240 -16
- package/app/components/01.atoms/display-pill/tests/DisplayPill.spec.ts +43 -14
- package/app/components/01.atoms/display-pill/tests/__snapshots__/DisplayPill.spec.ts.snap +3 -3
- package/app/components/01.atoms/tabbed-content/CONSUMER-STYLING.md +201 -0
- package/app/components/01.atoms/tabbed-content/TabbedContent.vue +750 -0
- package/app/components/01.atoms/tabbed-content/stories/TabbedContent.stories.ts +279 -0
- package/app/components/01.atoms/tabbed-content/tests/TabbedContent.spec.ts +816 -0
- package/app/components/02.molecules/alert-content/AlertContent.vue +11 -2
- package/app/components/02.molecules/alert-content/AlertContentInner.vue +19 -0
- package/app/components/02.molecules/alert-content/CONSUMER-STYLING.md +17 -4
- package/app/components/02.molecules/alert-content/stories/AlertContent.stories.ts +200 -17
- package/app/components/02.molecules/alert-content/tests/AlertContent.spec.ts +33 -0
- package/app/components/02.molecules/display-avatar/CONSUMER-STYLING.md +7 -5
- package/app/components/02.molecules/display-avatar/DisplayAvatar.vue +6 -2
- package/app/components/02.molecules/display-avatar/stories/DisplayAvatar.stories.ts +24 -3
- package/app/components/02.molecules/display-avatar/tests/DisplayAvatar.spec.ts +19 -0
- package/app/components/02.molecules/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap +2 -1
- package/app/components/02.molecules/display-chip/CONSUMER-STYLING.md +52 -19
- package/app/components/02.molecules/display-chip/DisplayChip.vue +72 -54
- package/app/components/02.molecules/display-chip/stories/DisplayChip.stories.ts +103 -6
- package/app/components/02.molecules/display-chip/tests/DisplayChip.spec.ts +98 -29
- package/app/components/02.molecules/display-chip/tests/__snapshots__/DisplayChip.spec.ts.snap +8 -4
- package/app/components/02.molecules/prompt/CONSUMER-STYLING.md +9 -1
- package/app/components/02.molecules/prompt/DisplayPrompt.vue +2 -2
- package/app/components/02.molecules/prompt/stories/DisplayPrompt.stories.ts +208 -71
- package/app/components/02.molecules/prompt/tests/DisplayPrompt.spec.ts +19 -6
- package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +66 -8
- package/app/components/02.molecules/select-menu/SelectMenu.vue +141 -18
- package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +232 -2
- package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +151 -0
- package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +6 -2
- package/app/components/02.molecules/toast/CONSUMER-STYLING.md +91 -27
- package/app/components/02.molecules/toast/DisplayToast.vue +109 -50
- package/app/components/02.molecules/toast/DisplayToastProvider.vue +109 -45
- package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +23 -0
- package/app/components/02.molecules/toast/stories/DisplayToastProvider.stories.ts +73 -0
- package/app/components/02.molecules/toast/tests/DisplayToast.spec.ts +132 -18
- package/app/components/02.molecules/toast/tests/DisplayToastProvider.spec.ts +105 -39
- package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +38 -11
- package/app/components/03.organisms/responsive-header/NavigationItems.vue +27 -12
- package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +35 -20
- package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +1 -1
- package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +1 -1
- package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +1 -1
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +3 -3
- package/app/composables/useTabs.ts +148 -206
- package/app/types/components/display-chip.d.ts +8 -4
- package/app/types/components/display-toast.d.ts +2 -0
- package/app/types/components/index.ts +0 -1
- package/app/types/components/select-menu.d.ts +2 -0
- package/app/utils/pausableTimeout.ts +41 -0
- package/nuxt.config.ts +1 -1
- package/package.json +1 -1
- package/.claude/skills/components/tab-navigation.md +0 -288
- package/.claude/skills/components/tabs-core.md +0 -187
- package/.vscode/srcdev-component-tab-navigation.code-snippets +0 -30
- package/app/components/01.atoms/navigation/tabs/CONSUMER-STYLING.md +0 -120
- package/app/components/01.atoms/navigation/tabs/TabsCore.vue +0 -272
- package/app/components/01.atoms/navigation/tabs/stories/TabsCore.stories.ts +0 -199
- package/app/components/01.atoms/navigation/tabs/tests/TabsCore.spec.ts +0 -274
- package/app/components/02.molecules/navigation/tab-navigation/CONSUMER-STYLING.md +0 -191
- package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +0 -550
- package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts +0 -259
- package/app/components/02.molecules/navigation/tab-navigation/tests/TabNavigation.spec.ts +0 -379
- package/app/components/02.molecules/navigation/tab-navigation/tests/__snapshots__/TabNavigation.spec.ts.snap +0 -33
- package/app/composables/useNavCollapse.ts +0 -86
- package/app/types/components/nav-item.d.ts +0 -11
|
@@ -98,6 +98,9 @@ The ledger can't see items 9, 10, 11 and 14, so check those by hand.
|
|
|
98
98
|
name directly. A component whose layout responds to its container gets the `CanvasSwitcher`
|
|
99
99
|
decorator (see `PageRow.stories.ts` or `GoogleReviews.stories.ts`, which also shows a per-story
|
|
100
100
|
starting canvas via `parameters.initialCanvas`) rather than a hand-sized wrapper or a width arg.
|
|
101
|
+
If other components render this one inside them (it's a child or building block of theirs),
|
|
102
|
+
each story gets a short note that explains the relationship and links to the parent's story
|
|
103
|
+
(`storybook-add-story.md`, "Link to the stories of components that use this one").
|
|
101
104
|
6a. **Storybook Controls-panel reactivity** — check every story `Template`/`render` function in
|
|
102
105
|
the component's `stories/*.stories.ts` files for this bug: `@storybook/vue3` mounts the story
|
|
103
106
|
component **once** and, on every Controls-panel change, mutates the same reactive `args`
|
|
@@ -135,8 +138,11 @@ The ledger can't see items 9, 10, 11 and 14, so check those by hand.
|
|
|
135
138
|
"Line-clamp tokens". Give each line-clamp token a Controls-panel select in the stories (set on
|
|
136
139
|
a wrapper `:style`, stripped from the component's args), labelled as a story-only CSS token so
|
|
137
140
|
nobody mistakes it for a prop: `storybook-add-story.md`, "Label token controls as story-only".
|
|
138
|
-
Add a unit test for any logic fix. A component
|
|
139
|
-
|
|
141
|
+
Add a unit test for any logic fix. A grid or layout component still gets one with oversized
|
|
142
|
+
slot content (an unbroken string in one cell finds `min-inline-size` overflow bugs). Components
|
|
143
|
+
with nothing to stress (animation wrappers, decorative containers that only take a slot and
|
|
144
|
+
tag/variant props, Storybook tooling) are exempt: they are listed in `stressStoryExempt` in
|
|
145
|
+
`.claude/component-ledger/build.mjs`; add a new one there rather than writing a padding story.
|
|
140
146
|
7. **Skill doc** — create or update `.claude/skills/components/<component-name>.md`, following the
|
|
141
147
|
pattern of an existing one. A labelled `<Name>Field` wrapper (or any other wrapper) is documented
|
|
142
148
|
in its control's doc under a "Variants" section, not in a doc of its own
|
|
@@ -92,7 +92,7 @@ predictable pattern (`item-0`, `item-1`, …).
|
|
|
92
92
|
|
|
93
93
|
|---|---|
|
|
94
94
|
| Multiple slot types per item must be grouped into one rendered element | `AccordianCore` — `summary`/`icon`/`content` per `<ExpandingPanel>` |
|
|
95
|
-
| Two parallel loops must stay in sync for aria linking | `
|
|
95
|
+
| Two parallel loops must stay in sync for aria linking | `TabbedContent` — trigger `<button>` loop + panel `<div>` loop linked by id |
|
|
96
96
|
| `itemCount` drives non-slot logic (z-index, CSS scope, counters) | `WipeAwayVertical` — `z-index: itemCount - key`, `timelineScope` computed |
|
|
97
97
|
| `$slots` inspection per iteration is needed to conditionally render | `StepperList` — checks `$slots[indicator-N]` to toggle class per `<li>` |
|
|
98
98
|
|
|
@@ -119,7 +119,7 @@ const props = defineProps<Props>();
|
|
|
119
119
|
|
|
120
120
|
### Multi-type indexed slots
|
|
121
121
|
|
|
122
|
-
Some components pair multiple slot types per index (e.g.
|
|
122
|
+
Some components pair multiple slot types per index (e.g. TabbedContent):
|
|
123
123
|
|
|
124
124
|
```vue
|
|
125
125
|
<template v-for="index in itemCount" :key="index">
|
|
@@ -130,7 +130,7 @@ Some components pair multiple slot types per index (e.g. TabsCore):
|
|
|
130
130
|
|
|
131
131
|
### Reference components (indexed dynamic slots)
|
|
132
132
|
|
|
133
|
-
- `app/components/01.atoms/
|
|
133
|
+
- `app/components/01.atoms/tabbed-content/TabbedContent.vue`
|
|
134
134
|
- `app/components/accordian/AccordianCore.vue`
|
|
135
135
|
- `app/components/02.molecules/stepper-list/StepperList.vue`
|
|
136
136
|
- `app/components/01.atoms/animations/view-timeline/WipeAwayVertical.vue`
|
|
@@ -11,17 +11,12 @@ Types defined inline in a `.vue` component file are not easily importable by con
|
|
|
11
11
|
Create `app/types/components/<component-name>.d.ts` with the exported interfaces:
|
|
12
12
|
|
|
13
13
|
```ts
|
|
14
|
-
// app/types/components/
|
|
15
|
-
export interface
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
cssName?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface NavItemData {
|
|
24
|
-
[key: string]: NavItem[];
|
|
14
|
+
// app/types/components/select-menu.d.ts
|
|
15
|
+
export interface SelectMenuOption {
|
|
16
|
+
value: string | number;
|
|
17
|
+
label: string;
|
|
18
|
+
icon?: string;
|
|
19
|
+
dotColor?: string;
|
|
25
20
|
}
|
|
26
21
|
```
|
|
27
22
|
|
|
@@ -30,7 +25,7 @@ export interface NavItemData {
|
|
|
30
25
|
In `app/types/components/index.ts`, add an export line:
|
|
31
26
|
|
|
32
27
|
```ts
|
|
33
|
-
export * from "./
|
|
28
|
+
export * from "./select-menu.d"
|
|
34
29
|
```
|
|
35
30
|
|
|
36
31
|
### 3. Update the component
|
|
@@ -39,11 +34,10 @@ Replace the inline `export interface` blocks in the `.vue` file with an import f
|
|
|
39
34
|
|
|
40
35
|
```ts
|
|
41
36
|
// Before
|
|
42
|
-
export interface
|
|
43
|
-
export interface NavItemData { ... }
|
|
37
|
+
export interface SelectMenuOption { ... }
|
|
44
38
|
|
|
45
39
|
// After
|
|
46
|
-
import type {
|
|
40
|
+
import type { SelectMenuOption } from "~/types/components/select-menu";
|
|
47
41
|
```
|
|
48
42
|
|
|
49
43
|
## Consuming app usage
|
|
@@ -60,7 +54,7 @@ app/types/components/<component>.d.ts
|
|
|
60
54
|
`types.d.ts` is the `"types"` entry point in `package.json`, so once a type is added to the barrel it is importable directly from the package name:
|
|
61
55
|
|
|
62
56
|
```ts
|
|
63
|
-
import type {
|
|
57
|
+
import type { SelectMenuOption } from "srcdev-nuxt-components";
|
|
64
58
|
```
|
|
65
59
|
|
|
66
60
|
## Notes
|
|
@@ -101,7 +101,7 @@ external consumer, free to rename. Run `/check-component-usage` for the current
|
|
|
101
101
|
| AlertMaskCore | `AlertMask` | - |
|
|
102
102
|
| CardCore | **decide**: `Card` is single-word and collision-prone; prefer a prefixed name (`Display*`, `Content*`) | yes |
|
|
103
103
|
| AccordianCore | **decide**: also fixes the "Accordian" misspelling; bare `Accordion` is single-word and collision-prone, and it is built on `<details>`/`<summary>` | yes |
|
|
104
|
-
| TabsCore |
|
|
104
|
+
| ~~TabsCore~~ | ✅ `TabbedContent` (2026-10-07) | - |
|
|
105
105
|
|
|
106
106
|
The `input-checkbox` family also has `MultipleCheckboxes`/`SingleCheckbox` on top of the Core; look
|
|
107
107
|
at how the four relate before choosing names there.
|
|
@@ -14,6 +14,10 @@ wrappers. Don't use `AlertContentInner` directly in pages.
|
|
|
14
14
|
|
|
15
15
|
**Location**: `app/components/02.molecules/alert-content/AlertContent.vue`, `AlertContentInner.vue`
|
|
16
16
|
|
|
17
|
+
> **Changed 2026-10-08**: added `styleClassPassthrough` to `AlertContent`, line-clamp tokens for the
|
|
18
|
+
> title and message, and wrapping for long unbroken text and long `InputButton` labels in `#actions`.
|
|
19
|
+
> `StressTest` story added.
|
|
20
|
+
|
|
17
21
|
> **Changed 2026-09-27**: skill doc renamed from `alert-content-inner.md`. Added the `showIcon` prop
|
|
18
22
|
> and `#actions` slot, public `--alert-content-*` tokens for the previously hardcoded values, and
|
|
19
23
|
> renamed `.title`/`.content` to `.alert-content-title`/`.alert-content-text`.
|
|
@@ -30,6 +34,7 @@ Same on `AlertContent`, `AlertContentInner` and `AlertMaskedContent`:
|
|
|
30
34
|
| `dismissible` | `boolean` | `false` | Shows the dismiss button, which emits `dismiss`. |
|
|
31
35
|
| `contentId` | `string` | `undefined` | `id` on `.alert-content-body`, for `aria-describedby` wiring. |
|
|
32
36
|
| `ariaLive` | `"polite" \| "assertive" \| "off"` | `undefined` | `aria-live` on `.alert-content-body`. Leave unset for static content (e.g. a banner present on page load). |
|
|
37
|
+
| `styleClassPassthrough` | `string \| string[]` | `[]` | `AlertContent` only. Extra classes on the root `.alert-content`. |
|
|
33
38
|
|
|
34
39
|
## Slots
|
|
35
40
|
|
|
@@ -96,7 +101,8 @@ slot always wins over app.config.
|
|
|
96
101
|
Full token list in `CONSUMER-STYLING.md` next to the component. Outer shape (`AlertContent` only):
|
|
97
102
|
`--alert-content-accent`, `-accent-width`, `-border`, `-border-radius-start`/`-end`. Inside (applies to
|
|
98
103
|
`AlertMaskedContent` too): `--alert-content-inner-background`, `-gap`, `-padding`, `-text-colour`,
|
|
99
|
-
`-icon-colour`, `-icon-size`, `-title-font-size`, `-text-font-size`, `-
|
|
104
|
+
`-icon-colour`, `-icon-size`, `-title-font-size`, `-text-font-size`, `-title-line-clamp`,
|
|
105
|
+
`-text-line-clamp` (both default `none`), `-body-gap`,
|
|
100
106
|
`-actions-spacing`, `-actions-gap`, `-actions-justify` (default `flex-end`, right-aligned), and the `-dismiss-*` tokens.
|
|
101
107
|
|
|
102
108
|
## Notes
|
|
@@ -105,6 +111,8 @@ Full token list in `CONSUMER-STYLING.md` next to the component. Outer shape (`Al
|
|
|
105
111
|
SVG mask shows through; change its fill with `--alert-masked-content-background` instead.
|
|
106
112
|
- `AlertContent`'s root is `display: grid` so the inner stretches to the root's height (e.g. in a
|
|
107
113
|
stretched row next to a taller alert) instead of showing the accent colour below it.
|
|
114
|
+
- Long unbroken text in the title or message wraps (`overflow-wrap: anywhere`). The actions row sets
|
|
115
|
+
`--input-button-text-white-space: normal`, so long `InputButton` labels wrap rather than being clipped.
|
|
108
116
|
- The inner is a flex row (icon | main | dismiss), so a missing icon or dismiss button leaves no
|
|
109
117
|
empty column or stray gap. Before 2026-09-27 it was a fixed three-column grid, and a
|
|
110
118
|
non-dismissible alert carried an empty trailing column plus its gap.
|
|
@@ -17,7 +17,7 @@ It uses the named dynamic slots pattern (`v-for="(_, name) in $slots"`), so ther
|
|
|
17
17
|
|
|
18
18
|
| Prop | Type | Default | Description |
|
|
19
19
|
|------|------|---------|-------------|
|
|
20
|
-
| `tag` | `"div" \| "li" \| "article" \| "section"` | `"div"` | HTML tag rendered for each card. |
|
|
20
|
+
| `tag` | `"div" \| "li" \| "article" \| "section"` | `"div"` | HTML tag rendered for each card. With `"li"` the wrapper renders as a `<ul>` (list styling reset); otherwise a `<div>`. |
|
|
21
21
|
| `config` | `ContainerGlowConfig` | `{}` | Proximity/spread/blur/gap/direction/inactive-opacity — see below. All fields optional. |
|
|
22
22
|
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root wrapper element. |
|
|
23
23
|
|
|
@@ -25,12 +25,14 @@ It uses the named dynamic slots pattern (`v-for="(_, name) in $slots"`), so ther
|
|
|
25
25
|
|
|
26
26
|
| Field | Type | Default | Description |
|
|
27
27
|
|---|---|---|---|
|
|
28
|
-
| `proximity` | `number` | `40` | Pointer distance (px) at which a card starts glowing. |
|
|
29
|
-
| `spread` | `number` | `80` | Angular spread of the glow (degrees). |
|
|
30
|
-
| `blur` | `number` | `20` | Blur applied to the glow layer (px). |
|
|
31
|
-
| `gap` | `number` | `32` | Gap between cards in the wrapper (px). |
|
|
28
|
+
| `proximity` | `number` | `40` | Pointer distance (px) at which a card starts glowing. Minimum `0`. |
|
|
29
|
+
| `spread` | `number` | `80` | Angular spread of the glow (degrees). Clamped to `0`–`360`. |
|
|
30
|
+
| `blur` | `number` | `20` | Blur applied to the glow layer (px). Minimum `0`. |
|
|
31
|
+
| `gap` | `number` | `32` | Gap between cards in the wrapper (px). Minimum `0`. |
|
|
32
32
|
| `vertical` | `boolean` | `false` | Stack cards vertically instead of horizontally. |
|
|
33
|
-
| `inactiveOpacity` | `number` | `0` | Glow opacity when the pointer isn't nearby. |
|
|
33
|
+
| `inactiveOpacity` | `number` | `0` | Glow opacity when the pointer isn't nearby. Clamped to `0`–`1`. |
|
|
34
|
+
|
|
35
|
+
Non-finite values (`NaN`, `Infinity`) fall back to the default.
|
|
34
36
|
|
|
35
37
|
## Slots
|
|
36
38
|
|
|
@@ -62,16 +64,19 @@ name list or count prop.
|
|
|
62
64
|
that on a private `--_start` custom property (registered via `@property` so it participates in
|
|
63
65
|
the conic-gradient mask math); the card's `--_opacity-active` is set to `1` (or
|
|
64
66
|
`config.inactiveOpacity` otherwise).
|
|
65
|
-
- Two pseudo-elements (`::before`/`::after`) and
|
|
67
|
+
- Two pseudo-elements (`::before`/`::after`) and an `aria-hidden` `.container-glow-glows` layer combine a thin proximity ring, a
|
|
66
68
|
coloured gradient glow, and a blurred backdrop glow, all masked by conic gradients driven from
|
|
67
69
|
`--_start` and `--_spread`.
|
|
68
70
|
- `config.gap`/`config.blur`/`config.spread`/`config.vertical` are applied once (and on every
|
|
69
71
|
`config` change, via a deep `watch`) as private custom properties on the wrapper, which cards
|
|
70
72
|
inherit.
|
|
73
|
+
- The wrapper is a wrapping flex row (or column), so many cards flow onto new rows rather than
|
|
74
|
+
overflowing. Each card is `280px` wide by default, capped at the wrapper's width, and wraps long
|
|
75
|
+
unbroken text (`overflow-wrap: anywhere`).
|
|
71
76
|
|
|
72
77
|
## Styling
|
|
73
78
|
|
|
74
|
-
Card background/padding/sizing/border-radius/highlight-colour/brightness/transition-duration and
|
|
79
|
+
Card background (defaults to `--theme-surface-subtle`)/text colour (`--theme-text`)/padding/sizing/border-radius/highlight-colour/brightness/transition-duration and
|
|
75
80
|
the glow gradient itself are public CSS custom properties — see `CONSUMER-STYLING.md` for the full
|
|
76
81
|
token table. Layout and interaction behaviour (proximity, spread, blur, gap, direction, inactive
|
|
77
82
|
opacity) go through the `config` prop instead, since they're computed in JavaScript per pointer
|
|
@@ -121,3 +126,11 @@ apply instantly instead of fading).
|
|
|
121
126
|
`--_direction`/`--_gradient` to match the private-token convention (they're config-driven, not a
|
|
122
127
|
direct CSS override surface).
|
|
123
128
|
- Added a `prefers-reduced-motion: reduce` guard disabling the opacity transition.
|
|
129
|
+
- 2026-10-08 stress-test pass (`StressTest` story): the glow layer class `.glows` was renamed to
|
|
130
|
+
`.container-glow-glows` (pitfall #16) and marked `aria-hidden`; `tag="li"` now renders the
|
|
131
|
+
wrapper as a `<ul>` instead of an invalid `<li>`-in-`<div>`; the wrapper wraps (`flex-wrap`)
|
|
132
|
+
instead of overflowing sideways with many cards; cards cap `min-width` at the wrapper's width and
|
|
133
|
+
use `overflow-wrap: anywhere`; `config` numbers are clamped (negative gap/blur/proximity, spread
|
|
134
|
+
outside 0–360, opacity outside 0–1, non-finite values) so a bad value can't produce an invalid
|
|
135
|
+
`calc()`. Card background default moved from `white` to `--theme-surface-subtle`, and a new
|
|
136
|
+
`--container-glow-text-colour` token defaults to `--theme-text`.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# DisplayAvatar Component
|
|
2
2
|
|
|
3
|
+
> **Changed 2026-10-08:** new `status`/`status-label` props, forwarded to the chip. Chip status used to be an `online`/`idle`/`dnd` class through `style-class-passthrough`, which no longer colours the dot.
|
|
4
|
+
|
|
3
5
|
## Overview
|
|
4
6
|
|
|
5
7
|
`DisplayAvatar` renders a circular avatar — either an image (via `NuxtImg`) or a text fallback showing initials derived from the `alt` prop. Optionally wraps in a `DisplayChip` to show a status indicator badge.
|
|
@@ -18,6 +20,8 @@
|
|
|
18
20
|
| `text` | `string` | — | Override the auto-derived initials with an explicit string. |
|
|
19
21
|
| `size` | `"xs" \| "s" \| "md" \| "lg" \| "xl"` | `"md"` | Controls width, height, and font-size. |
|
|
20
22
|
| `chip` | `boolean \| DisplayChipConfig` | — | Add a status chip. `true` uses defaults; pass a config object to customise. |
|
|
23
|
+
| `status` | `DisplayChipStatus` | — | Chip status colour (`offline`/`online`/`idle`/`dnd`), forwarded to `DisplayChip`. Ignored without `chip`; the chip defaults to `offline`. |
|
|
24
|
+
| `status-label` | `string` | — | Screen-reader text for the chip status (e.g. "Online"), forwarded to `DisplayChip`. Ignored without `chip`. |
|
|
21
25
|
| `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes on the root element. |
|
|
22
26
|
|
|
23
27
|
### Size dimensions
|
|
@@ -102,10 +106,12 @@ text="?" → "?"
|
|
|
102
106
|
src="/images/profile.jpg"
|
|
103
107
|
alt="Jane Smith"
|
|
104
108
|
:chip="true"
|
|
109
|
+
status="online"
|
|
110
|
+
status-label="Online"
|
|
105
111
|
/>
|
|
106
112
|
```
|
|
107
113
|
|
|
108
|
-
Default chip config: `{ size: "12px", maskWidth: "4px", offset: "0px", angle: "90deg" }`.
|
|
114
|
+
Default chip config: `{ size: "12px", maskWidth: "4px", offset: "0px", angle: "90deg" }`. Without `status` the dot is `offline`.
|
|
109
115
|
|
|
110
116
|
### With a custom chip
|
|
111
117
|
|
|
@@ -126,12 +132,12 @@ Full `DisplayChipConfig` shape (pass directly as the `chip` value):
|
|
|
126
132
|
|
|
127
133
|
```ts
|
|
128
134
|
interface DisplayChipConfig {
|
|
129
|
-
size
|
|
130
|
-
maskWidth
|
|
131
|
-
offset
|
|
132
|
-
angle
|
|
133
|
-
icon?: string
|
|
134
|
-
label?: string
|
|
135
|
+
size?: string // chip diameter, e.g. "12px"
|
|
136
|
+
maskWidth?: string // cutout ring width, e.g. "4px"
|
|
137
|
+
offset?: string // distance from avatar edge, e.g. "0px"
|
|
138
|
+
angle?: string // position around avatar (0–360deg), e.g. "45deg"
|
|
139
|
+
icon?: string // Iconify icon name
|
|
140
|
+
label?: string // short text (max 3 characters)
|
|
135
141
|
}
|
|
136
142
|
```
|
|
137
143
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# DisplayChip Component
|
|
2
2
|
|
|
3
|
+
> **Changed 2026-10-08 (breaking):** status is the new `status` prop (`data-status`), not an `online`/`idle`/`dnd` class through `styleClassPassthrough`. Shape is `data-shape`, not a `.circle`/`.square` class. Inner classes are `.display-chip-icon`/`.display-chip-label` (were `.chip-icon`/`.chip-label`), with `data-length` instead of `.length-N`. Every `config` field is optional and falls back to a public `--display-chip-*` geometry token. Labels are counted and truncated by grapheme, so emoji are never split. New `statusLabel` prop for screen-reader text.
|
|
4
|
+
|
|
3
5
|
> **Renamed 2026-09-27:** root class `.display-chip-core` → `.display-chip`. Status colours are now public tokens (`--display-chip-colour-*`, were unprefixed `--color-*`); the geometry vars fed from `config` are private (`--_chip-*`).
|
|
4
6
|
|
|
5
7
|
## Overview
|
|
@@ -18,23 +20,28 @@ Used directly for standalone chip overlays, and internally by `DisplayAvatar` wh
|
|
|
18
20
|
| -------------------------- | -------------------------- | ---------- | -------------------------------------------------- |
|
|
19
21
|
| `tag` | `"div" \| "span"` | `"span"` | Root element tag. |
|
|
20
22
|
| `shape` | `"circle" \| "square"` | `"circle"` | Affects position maths — must match the parent shape. |
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
+
| `status` | `DisplayChipStatus` | `"offline"` | `"offline" \| "online" \| "idle" \| "dnd"`. Sets the dot colour via `data-status`. |
|
|
24
|
+
| `status-label` | `string` | `""` | Screen-reader text for the status (e.g. "Online"), so it isn't conveyed by colour alone. Pass translated copy. |
|
|
25
|
+
| `:config` | `DisplayChipConfig` | `{}` | Geometry and optional content. Every field is optional. |
|
|
26
|
+
| `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes on the root. |
|
|
23
27
|
|
|
24
28
|
### DisplayChipConfig
|
|
25
29
|
|
|
26
30
|
```ts
|
|
27
31
|
interface DisplayChipConfig {
|
|
28
|
-
size
|
|
29
|
-
maskWidth
|
|
30
|
-
offset
|
|
31
|
-
angle
|
|
32
|
-
icon?: string
|
|
33
|
-
label?: string
|
|
32
|
+
size?: string // chip dot diameter, e.g. "12px"
|
|
33
|
+
maskWidth?: string // cutout ring width around the chip, e.g. "4px"
|
|
34
|
+
offset?: string // extra distance from the parent edge, e.g. "0px"
|
|
35
|
+
angle?: string // position around the parent (0–360deg), e.g. "45deg"
|
|
36
|
+
icon?: string // Iconify icon name rendered inside the chip (decorative, aria-hidden)
|
|
37
|
+
label?: string // short text rendered inside the chip (max 3 characters)
|
|
34
38
|
}
|
|
35
39
|
```
|
|
36
40
|
|
|
37
|
-
|
|
41
|
+
A missing geometry field falls back to its token: `--display-chip-size` (`1.2rem`),
|
|
42
|
+
`--display-chip-mask-width` (`0.4rem`), `--display-chip-offset` (`0rem`), `--display-chip-angle`
|
|
43
|
+
(`90deg`). A field set in `config` beats the token. Negative `size`/`maskWidth` clamp to `0`; values
|
|
44
|
+
without units (`"45"`) are invalid CSS and hide the dot.
|
|
38
45
|
|
|
39
46
|
### Angle reference
|
|
40
47
|
|
|
@@ -53,27 +60,30 @@ Default config: `{ size: "12px", maskWidth: "4px", offset: "0px", angle: "90deg"
|
|
|
53
60
|
|
|
54
61
|
## Status colours
|
|
55
62
|
|
|
56
|
-
|
|
63
|
+
Set with the `status` prop:
|
|
57
64
|
|
|
58
|
-
|
|
|
65
|
+
| `status` | Token | Default |
|
|
59
66
|
| --- | --- | --- |
|
|
60
|
-
| (
|
|
61
|
-
| `online` | `--display-chip-colour-online` | `
|
|
62
|
-
| `idle` | `--display-chip-colour-idle` | `
|
|
63
|
-
| `dnd` | `--display-chip-colour-dnd` | `
|
|
67
|
+
| `offline` (default) | `--display-chip-colour-offline` | `var(--status-neutral)` |
|
|
68
|
+
| `online` | `--display-chip-colour-online` | `var(--status-success)` |
|
|
69
|
+
| `idle` | `--display-chip-colour-idle` | `var(--status-warning)` |
|
|
70
|
+
| `dnd` | `--display-chip-colour-dnd` | `var(--status-danger)` |
|
|
71
|
+
|
|
72
|
+
Defaults come from the global status tokens (changed 2026-10-07, were neon `rgb()` values); see
|
|
73
|
+
`.claude/skills/theming-status-tokens.md`.
|
|
64
74
|
|
|
65
75
|
Icon and label colour: `--display-chip-text-colour` (default `black`). Full reference:
|
|
66
76
|
`app/components/02.molecules/display-chip/CONSUMER-STYLING.md`.
|
|
67
77
|
|
|
68
78
|
```vue
|
|
69
|
-
<DisplayChip
|
|
79
|
+
<DisplayChip status="online" status-label="Online">...</DisplayChip>
|
|
70
80
|
```
|
|
71
81
|
|
|
72
82
|
---
|
|
73
83
|
|
|
74
84
|
## Label constraints
|
|
75
85
|
|
|
76
|
-
- Max 3 characters. Longer values are
|
|
86
|
+
- Max 3 characters, counted as graphemes: an emoji (including ZWJ sequences like 👨👩👧👦 and flags) counts as one and is never split. Longer values are truncated with a `console.warn`. Whitespace is trimmed; a whitespace-only label renders nothing.
|
|
77
87
|
- Font-size scales automatically with chip size via `--_font-size-adjust`:
|
|
78
88
|
- 1 char → `0.7 × size`
|
|
79
89
|
- 2 chars → `0.6 × size`
|
|
@@ -97,7 +107,7 @@ Icon and label colour: `--display-chip-text-colour` (default `black`). Full refe
|
|
|
97
107
|
<DisplayChip
|
|
98
108
|
shape="circle"
|
|
99
109
|
:config="{ size: '12px', maskWidth: '4px', offset: '0px', angle: '45deg' }"
|
|
100
|
-
|
|
110
|
+
status="online"
|
|
101
111
|
>
|
|
102
112
|
<div class="avatar">SRC</div>
|
|
103
113
|
</DisplayChip>
|
|
@@ -109,7 +119,7 @@ Icon and label colour: `--display-chip-text-colour` (default `black`). Full refe
|
|
|
109
119
|
<DisplayChip
|
|
110
120
|
shape="square"
|
|
111
121
|
:config="{ size: '10px', maskWidth: '3px', offset: '2px', angle: '135deg' }"
|
|
112
|
-
|
|
122
|
+
status="idle"
|
|
113
123
|
>
|
|
114
124
|
<img src="/thumbnail.jpg" alt="Card thumbnail" />
|
|
115
125
|
</DisplayChip>
|
|
@@ -120,7 +130,7 @@ Icon and label colour: `--display-chip-text-colour` (default `black`). Full refe
|
|
|
120
130
|
```vue
|
|
121
131
|
<DisplayChip
|
|
122
132
|
:config="{ size: '16px', maskWidth: '4px', offset: '0px', angle: '45deg', icon: 'bi:check-circle-fill' }"
|
|
123
|
-
|
|
133
|
+
status="online"
|
|
124
134
|
>
|
|
125
135
|
<div class="avatar">SRC</div>
|
|
126
136
|
</DisplayChip>
|
|
@@ -132,7 +142,7 @@ Icon and label colour: `--display-chip-text-colour` (default `black`). Full refe
|
|
|
132
142
|
<!-- 1–3 characters only; longer values are truncated with a warning -->
|
|
133
143
|
<DisplayChip
|
|
134
144
|
:config="{ size: '16px', maskWidth: '4px', offset: '0px', angle: '45deg', label: '+2' }"
|
|
135
|
-
|
|
145
|
+
status="dnd"
|
|
136
146
|
>
|
|
137
147
|
<div class="avatar">SRC</div>
|
|
138
148
|
</DisplayChip>
|
|
@@ -156,7 +166,7 @@ const chipConfig = computed((): DisplayChipConfig => ({
|
|
|
156
166
|
</script>
|
|
157
167
|
|
|
158
168
|
<template>
|
|
159
|
-
<DisplayChip shape="circle" :config="chipConfig"
|
|
169
|
+
<DisplayChip shape="circle" :config="chipConfig" status="online">
|
|
160
170
|
<div class="avatar">SRC</div>
|
|
161
171
|
</DisplayChip>
|
|
162
172
|
</template>
|
|
@@ -171,7 +181,8 @@ Prefer `DisplayAvatar` with its `chip` prop over wiring `DisplayChip` directly:
|
|
|
171
181
|
src="/images/profile.jpg"
|
|
172
182
|
alt="Jane Smith"
|
|
173
183
|
:chip="{ size: '12px', maskWidth: '4px', offset: '0px', angle: '45deg' }"
|
|
174
|
-
|
|
184
|
+
status="online"
|
|
185
|
+
status-label="Online"
|
|
175
186
|
/>
|
|
176
187
|
```
|
|
177
188
|
|
|
@@ -184,6 +195,7 @@ See [display-avatar.md](./display-avatar.md) for the full API.
|
|
|
184
195
|
```vue
|
|
185
196
|
<DisplayChip
|
|
186
197
|
:config="chipConfig"
|
|
198
|
+
status="online"
|
|
187
199
|
:style-class-passthrough="['my-chip']"
|
|
188
200
|
>
|
|
189
201
|
<div class="avatar">SRC</div>
|
|
@@ -214,5 +226,6 @@ See [display-avatar.md](./display-avatar.md) for the full API.
|
|
|
214
226
|
- `shape` must match the actual shape of the slot content — the position maths differs between `circle` (radius-based) and `square` (clamped corner-aware).
|
|
215
227
|
- `config` values are geometric inputs to CSS `calc(cos())` / `calc(sin())` expressions. Pass them as strings with units (`"12px"`, `"45deg"`), not plain numbers.
|
|
216
228
|
- The chip dot is rendered via `::after` pseudo-element; icon and label sit above it at `z-index: 2`.
|
|
217
|
-
- The mask cutout is applied to all direct children of `.display-chip` except `.chip-icon` and `.
|
|
229
|
+
- The mask cutout is applied to all direct children of `.display-chip` except `.display-chip-icon`, `.display-chip-label` and the `.sr-only` status text — ensure the host element is a direct child.
|
|
230
|
+
- The `StressTest` story shows the worst-case labels and config values.
|
|
218
231
|
- `DisplayChipConfig` and `DisplayChipProps` are both exported from the layer types. Use `DisplayChipConfig` when passing geometry values (the `config` prop). Use `DisplayChipProps` only if you need to pass the full component prop set (e.g. when building a wrapper component).
|
|
@@ -9,10 +9,10 @@ A classic pill/badge component for displaying status labels, tags, or metadata.
|
|
|
9
9
|
| Prop | Type | Default | Description |
|
|
10
10
|
|------|------|---------|-------------|
|
|
11
11
|
| `tag` | `"span" \| "div" \| "button" \| "a"` | `"span"` | Root element. Use `"button"` or `"a"` for interactive pills: cursor switches to pointer and a `:focus-visible` outline is shown. A `button` gets `type="button"`; pass `href` (falls through) for an `a`. |
|
|
12
|
-
| `label` | `string` | `undefined` | Pill text. When set, renders inside `.display-pill-label` and suppresses the default slot |
|
|
13
|
-
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Size
|
|
14
|
-
| `variant` | `"default" \| "primary" \| "success" \| "warning" \| "danger" \| "neutral"` | `"default"` | Colour variant |
|
|
15
|
-
| `reversed` | `boolean` | `false` | Reverses icon/label order (`flex-direction: row-reverse`) |
|
|
12
|
+
| `label` | `string` | `undefined` | Pill text. When set (and not just whitespace), renders inside `.display-pill-label` and suppresses the default slot |
|
|
13
|
+
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Size, rendered as `data-size` |
|
|
14
|
+
| `variant` | `"default" \| "primary" \| "success" \| "warning" \| "danger" \| "neutral"` | `"default"` | Colour variant, rendered as `data-variant`; an unknown value gets the default colours |
|
|
15
|
+
| `reversed` | `boolean` | `false` | Reverses icon/label order (`flex-direction: row-reverse`), rendered as `data-reversed` |
|
|
16
16
|
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes merged onto the root element |
|
|
17
17
|
|
|
18
18
|
## Slots
|
|
@@ -20,7 +20,7 @@ A classic pill/badge component for displaying status labels, tags, or metadata.
|
|
|
20
20
|
| Slot | Description |
|
|
21
21
|
|------|-------------|
|
|
22
22
|
| `icon` | Icon content, wrapped in `.display-pill-icon` (sized by `--display-pill-icon-size`) — rendered before the label by default, after when `reversed=true` |
|
|
23
|
-
| `default` | Custom label content — only used when the `label` prop is not set |
|
|
23
|
+
| `default` | Custom label content, wrapped in `.display-pill-label` (so it's line-clamped like `label`) — only used when the `label` prop is not set or is whitespace |
|
|
24
24
|
|
|
25
25
|
## Basic usage
|
|
26
26
|
|
|
@@ -69,7 +69,7 @@ When `label` is not set, the default slot is rendered instead:
|
|
|
69
69
|
|
|
70
70
|
Full token list in `CONSUMER-STYLING.md` next to the component. All tokens are `--display-pill-*` (renamed from `--theme-pill-*` on 2026-09-27; `bg`/`color` became `background`/`text-colour`).
|
|
71
71
|
|
|
72
|
-
Colour resolves as **variant token → base token → variant default**: `--display-pill-background`/`-text-colour` restyle every variant; `--display-pill-success-background` etc. win for that variant only. Sizes have `-sm`/`-lg` suffixed tokens for font size and icon size only; padding and gap are the icon size times a ratio (`--display-pill-padding-inline-ratio` 0.7, `-padding-inline-icon-ratio` 0.4, `-padding-block-ratio` 0.3, `-gap-ratio` 0.35), so spacing scales with the pill. Absolute `--display-pill-padding-inline`/`-padding-block`/`-gap` tokens still override. The icon side of a pill gets `--display-pill-padding-inline-icon` (tighter than `--display-pill-padding-inline`), placed by `data-icon-position` (`start`, `end` when `reversed`, `only` for icon-only, absent with no icon), so label-only pills stay evenly padded.
|
|
72
|
+
Colour resolves as **variant token → base token → variant default**: `--display-pill-background`/`-text-colour` restyle every variant; `--display-pill-success-background` etc. win for that variant only. Borders follow the same chain: `--display-pill-<variant>-border-colour` → `--display-pill-border-colour` → `transparent`. So do rings (`--display-pill-ring-width`, a box-shadow): `--display-pill-<variant>-ring-colour` → `--display-pill-ring-colour` → the pill's background. Sizes have `-sm`/`-lg` suffixed tokens for font size and icon size only; padding and gap are the icon size times a ratio (`--display-pill-padding-inline-ratio` 0.7, `-padding-inline-icon-ratio` 0.4, `-padding-block-ratio` 0.3, `-gap-ratio` 0.35), so spacing scales with the pill. Absolute `--display-pill-padding-inline`/`-padding-block`/`-gap` tokens still override. The icon side of a pill gets `--display-pill-padding-inline-icon` (tighter than `--display-pill-padding-inline`), placed by `data-icon-position` (`start`, `end` when `reversed`, `only` for icon-only, absent with no icon), so label-only pills stay evenly padded.
|
|
73
73
|
|
|
74
74
|
```vue
|
|
75
75
|
<DisplayPill label="New" variant="primary" style-class-passthrough="pill-new" />
|
|
@@ -89,17 +89,65 @@ Colour resolves as **variant token → base token → variant default**: `--disp
|
|
|
89
89
|
--display-pill-border-style: dashed;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
/*
|
|
93
|
-
.
|
|
92
|
+
/* Two-tone: dark border in the variant's text colour, light ring in its background colour */
|
|
93
|
+
.two-tone.display-pill {
|
|
94
|
+
--display-pill-border-colour: currentColor;
|
|
95
|
+
--display-pill-ring-width: 0.2rem; /* --display-pill-ring-colour defaults to the variant background */
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* Offset outline (a gap between pill and line); unlike the ring, outline ignores border-radius before Safari 16.4 */
|
|
99
|
+
.offset-outline.display-pill {
|
|
94
100
|
--display-pill-outline: 0.2rem solid currentColor;
|
|
95
101
|
--display-pill-outline-offset: 0.3rem;
|
|
96
102
|
}
|
|
97
103
|
```
|
|
98
104
|
|
|
99
|
-
##
|
|
105
|
+
## Outlined variants
|
|
106
|
+
|
|
107
|
+
The default text steps (`-09`/`-10`) are near-black in every hue, so an outlined pill using
|
|
108
|
+
`currentColor` for its border makes the variants look alike (`warning` and `danger` especially).
|
|
109
|
+
Lift each variant's text to its `-07` step (all 48% lightness, so contrast stays even); the border
|
|
110
|
+
follows. Neutral is normally the dark inverted pill, so give it a dark text step here.
|
|
111
|
+
|
|
112
|
+
```css
|
|
113
|
+
.tag-list {
|
|
114
|
+
--display-pill-background: transparent;
|
|
115
|
+
--display-pill-border-colour: currentColor;
|
|
116
|
+
--display-pill-text-colour: var(--slate-07);
|
|
117
|
+
--display-pill-primary-text-colour: var(--blue-07);
|
|
118
|
+
--display-pill-success-text-colour: var(--green-07);
|
|
119
|
+
--display-pill-warning-text-colour: var(--orange-07);
|
|
120
|
+
--display-pill-danger-text-colour: var(--red-07);
|
|
121
|
+
--display-pill-neutral-text-colour: var(--slate-10);
|
|
122
|
+
|
|
123
|
+
/* Optional 2px ring in each variant's -02 step */
|
|
124
|
+
--display-pill-ring-width: 0.2rem;
|
|
125
|
+
--display-pill-ring-colour: var(--slate-02);
|
|
126
|
+
--display-pill-primary-ring-colour: var(--blue-02);
|
|
127
|
+
--display-pill-success-ring-colour: var(--green-02);
|
|
128
|
+
--display-pill-warning-ring-colour: var(--orange-02);
|
|
129
|
+
--display-pill-danger-ring-colour: var(--red-02);
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
To keep the dark text and colour only the border, set `--display-pill-<variant>-border-colour`
|
|
134
|
+
instead (e.g. `var(--status-success)`). The ring is a box-shadow: it takes no layout space, so
|
|
135
|
+
leave a gap between ringed pills.
|
|
100
136
|
|
|
101
|
-
|
|
137
|
+
## Stories
|
|
138
|
+
|
|
139
|
+
`Default`, `AllVariants`, `OutlinedViaBaseTokens` (per-variant mid-step borders), `OutlinedMonochrome`
|
|
140
|
+
(the recipe above, with a story-only `ring` control), `RingedTwoTone` (dark border inside a light ring;
|
|
141
|
+
the `ring` control swaps the ring to `-02`), and `StressTest`. `labelLineClamp` and `ring` are
|
|
142
|
+
story-only controls that set CSS tokens, not props. The stories link to `ServiceSummary` and
|
|
143
|
+
`ServiceDetail`, which render neutral duration/price pills.
|
|
144
|
+
|
|
145
|
+
## Notes
|
|
102
146
|
|
|
147
|
+
- Since 2026-10-06 a pill never grows past its container: a label too long for the space ends in an ellipsis. Since 2026-10-08 that's a line clamp, `--display-pill-label-line-clamp` (default `1`); set `none` to let long labels wrap (worth it for text that must stay readable, like a price, in a narrow column).
|
|
148
|
+
- Since 2026-10-08 a pill with no text and no icon (empty or whitespace-only `label`, no slots) renders nothing visible (`:empty { display: none }`), and a whitespace-only `label` with an icon gives an icon-only pill.
|
|
149
|
+
- 2026-10-08 (breaking): size/variant/reversed moved from bare classes (`.sm`, `.primary`, `.is-reversed`) to `data-size`/`data-variant`/`data-reversed`, so a consumer's own `.primary`/`.success` utility classes can't hit the pill. Selectors like `.display-pill.primary` become `.display-pill[data-variant="primary"]`.
|
|
150
|
+
- `StressTest` story covers long/unbroken/RTL/emoji/HTML-like labels, empty and whitespace labels, a broken icon and an unknown variant at three container widths.
|
|
103
151
|
- `border-radius` defaults to `100vw` — this always collapses to the tightest possible radius on the shorter axis, making it geometry-independent (no magic number needed).
|
|
104
152
|
- `cursor: default` and `user-select: none` are applied unconditionally. `cursor: pointer` is applied automatically via `:is(button, a)` when `tag` is interactive.
|
|
105
153
|
- All variant colour pairs use a light-tint background (`--color-01`) with a dark text colour (`--color-09`/`--color-10`) to ensure WCAG AA contrast. The project colour scale runs `00` (lightest) → `10` (darkest) — the opposite of Tailwind.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# DisplayPrompt
|
|
2
2
|
|
|
3
|
+
> **Changed 2026-10-08:** the root's `.closed` class is now `data-state="open" | "closed"`, so a consumer's own `.closed` class can't collide with it.
|
|
4
|
+
|
|
3
5
|
> **Changed 2026-09-28:** root class `.display-prompt-core` → `.display-prompt` (and `data-test-id` `display-prompt-core-{theme}` → `display-prompt-{theme}`). The documented `outlined` passthrough modifier never had any CSS behind it and was removed from this doc.
|
|
4
6
|
|
|
5
7
|
## Overview
|
|
@@ -27,7 +29,7 @@ the DOM. Dismiss can be controlled locally (closes itself) or by a parent via `v
|
|
|
27
29
|
|
|
28
30
|
| Slot | Description |
|
|
29
31
|
|---|---|
|
|
30
|
-
| `#title` |
|
|
32
|
+
| `#title` | Bold heading text. The title `<p>` is omitted when this slot isn't provided. |
|
|
31
33
|
| `#content` | Body text below the title. The `<p>` element is omitted when this slot is empty. |
|
|
32
34
|
| `#customDecoratorIcon` | Replaces the default theme icon. |
|
|
33
35
|
| `#customCloseIcon` | Replaces the default × close icon inside the dismiss button. |
|
|
@@ -52,10 +54,10 @@ Two modes depending on whether `v-model` is bound:
|
|
|
52
54
|
|
|
53
55
|
| Scenario | What happens on close |
|
|
54
56
|
|---|---|
|
|
55
|
-
| No `v-model` (or `v-model="false"`) | Sets internal `componentOpen = false` →
|
|
57
|
+
| No `v-model` (or `v-model="false"`) | Sets internal `componentOpen = false` → `data-state="closed"` on the root → collapses via CSS |
|
|
56
58
|
| `v-model="true"` | Emits `update:modelValue = false`; internal state unchanged — parent controls visibility |
|
|
57
59
|
|
|
58
|
-
|
|
60
|
+
`data-state="closed"` triggers a CSS grid row animation (`grid-template-rows: 1fr → 0fr`) with
|
|
59
61
|
`opacity: 0` and `pointer-events: none`, and the root becomes `inert` so the dismiss button leaves the
|
|
60
62
|
tab order and the accessibility tree. The transition is off under `prefers-reduced-motion: reduce`;
|
|
61
63
|
its duration is `--display-prompt-transition-duration` (`200ms`).
|
|
@@ -151,5 +153,6 @@ glass effect to be visible.
|
|
|
151
153
|
- The root gets `tabindex="-1"` only when `useAutoFocus` is on, so it can take programmatic focus without joining the tab order (it used to be `tabindex="0"` always).
|
|
152
154
|
- `useAutoFocus` focuses the root element on mount (useful when injecting a prompt in response to a
|
|
153
155
|
user action that has already moved focus elsewhere).
|
|
154
|
-
-
|
|
155
|
-
|
|
156
|
+
- Long titles/messages wrap (unbroken strings included). To cap them, set AlertContent's
|
|
157
|
+
`--alert-content-title-line-clamp` / `--alert-content-text-line-clamp` on an ancestor. The
|
|
158
|
+
`StressTest` story shows the worst-case content.
|
|
@@ -53,6 +53,7 @@ interface DisplayToastConfig {
|
|
|
53
53
|
title?: string // bold title line
|
|
54
54
|
description?: string // smaller description line
|
|
55
55
|
customIcon?: string // icon name override (e.g. "akar-icons:check-box")
|
|
56
|
+
dismissLabel?: string // screen-reader text for the close button — default: "Close"
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
```
|
|
@@ -111,6 +112,17 @@ const toastVisible = ref(false)
|
|
|
111
112
|
- `onBeforeRouteLeave` dismisses the toast on navigation. This emits a Vue Router warning in Vitest (no active route record) — harmless.
|
|
112
113
|
- `returnFocusTo` accepts an `HTMLElement` or a component instance with `$el`.
|
|
113
114
|
- The toast uses `<Teleport to="body">`. In tests, query `document.querySelector(".display-toast")` not `wrapper.find()`.
|
|
115
|
+
- 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
|
+
- 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.
|
|
117
|
+
|
|
118
|
+
### Auto-dismiss pause
|
|
119
|
+
|
|
120
|
+
Auto-dismiss pauses while hovered, or while keyboard focus is on or inside the toast, and resumes with
|
|
121
|
+
the remaining time (`createPausableTimeout` in `app/utils/pausableTimeout.ts`, plus
|
|
122
|
+
`focusPausesAutoDismiss`). The toast's own programmatic focus only pauses it when the root matches
|
|
123
|
+
`:focus-visible` (keyboard modality), so mouse users still get auto-dismiss. `data-paused` freezes the
|
|
124
|
+
progress bar via `animation-play-state`. In jsdom `:focus-visible` matches any focus, so tests that
|
|
125
|
+
depend on modality spy on `Element.prototype.matches` (see `DisplayToast.spec.ts`).
|
|
114
126
|
|
|
115
127
|
---
|
|
116
128
|
|
|
@@ -208,12 +220,15 @@ Stacked dismissals use a FLIP animation — remaining toasts slide smoothly to f
|
|
|
208
220
|
|
|
209
221
|
### CSS / styling
|
|
210
222
|
|
|
211
|
-
|
|
223
|
+
Both components teleport to `<body>`, so tokens set on a page or layout class don't reach them: set
|
|
224
|
+
them globally. Own tokens: `--display-toast(-provider)-max-width` (default `min(48rem, 100% - 2 * gutter)`),
|
|
225
|
+
`-progress-colour`, `-focus-ring-colour`, `-z-index`. Card styling comes from `--alert-content-*`.
|
|
226
|
+
Full list in `CONSUMER-STYLING.md`.
|
|
212
227
|
|
|
213
228
|
```css
|
|
214
|
-
|
|
215
|
-
--
|
|
216
|
-
--
|
|
229
|
+
:where(html) {
|
|
230
|
+
--display-toast-provider-max-width: min(36rem, 100% - 48px);
|
|
231
|
+
--display-toast-provider-progress-colour: var(--brand-accent);
|
|
217
232
|
}
|
|
218
233
|
```
|
|
219
234
|
|
|
@@ -234,5 +249,7 @@ The inner content layout is handled by the shared `AlertContentInner` molecule
|
|
|
234
249
|
- `useToastQueue` state persists across route changes — no `onBeforeRouteLeave` cleanup needed.
|
|
235
250
|
- `crypto.randomUUID()` generates toast IDs (`toast-<uuid>`). IDs are returned by `show()` for targeted `dismiss(id)` calls.
|
|
236
251
|
- In tests, query via `document.querySelector(".display-toast-provider-item")` — the Teleport renders outside the component wrapper.
|
|
252
|
+
- 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
|
+
- Each item pauses independently (hover / keyboard focus, same rule as the standalone toast), tracked per toast id.
|
|
237
254
|
- The progress bar (`.display-toast-provider-progress`) is only rendered when `autoDismiss: true`.
|
|
238
255
|
- Dismissal can be triggered by: close button click, Escape key (when the toast item has focus), `dismiss(id)`, auto-dismiss timer, or `clear()`.
|