srcdev-nuxt-components 9.5.7 → 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.
Files changed (159) hide show
  1. package/.claude/skills/component-compliance-checklist.md +8 -2
  2. package/.claude/skills/component-dynamic-slots.md +3 -3
  3. package/.claude/skills/component-export-types.md +10 -16
  4. package/.claude/skills/component-naming.md +1 -1
  5. package/.claude/skills/components/alert-content.md +9 -1
  6. package/.claude/skills/components/breadcrumb.md +5 -0
  7. package/.claude/skills/components/container-glow.md +21 -8
  8. package/.claude/skills/components/display-avatar.md +13 -7
  9. package/.claude/skills/components/display-chip.md +37 -24
  10. package/.claude/skills/components/display-pill.md +58 -8
  11. package/.claude/skills/components/display-prompt.md +8 -5
  12. package/.claude/skills/components/display-toast.md +21 -4
  13. package/.claude/skills/components/glowing-border.md +20 -2
  14. package/.claude/skills/components/input-button.md +3 -0
  15. package/.claude/skills/components/responsive-header.md +59 -0
  16. package/.claude/skills/components/select-menu.md +54 -5
  17. package/.claude/skills/components/service-detail.md +32 -0
  18. package/.claude/skills/components/service-summary.md +8 -0
  19. package/.claude/skills/components/services-card-grid.md +5 -3
  20. package/.claude/skills/components/services-card.md +22 -12
  21. package/.claude/skills/components/site-header.md +2 -2
  22. package/.claude/skills/components/slider-gallery.md +10 -4
  23. package/.claude/skills/components/tabbed-content.md +300 -0
  24. package/.claude/skills/composable-anchor-scroll.md +16 -15
  25. package/.claude/skills/index.md +4 -5
  26. package/.claude/skills/storybook-add-story.md +53 -0
  27. package/.claude/skills/theming-status-tokens.md +60 -0
  28. package/.vscode/srcdev-component-display-avatar.code-snippets +2 -1
  29. package/.vscode/srcdev-component-display-chip.code-snippets +3 -2
  30. package/.vscode/srcdev-component-display-pill.code-snippets +10 -0
  31. package/.vscode/srcdev-component-responsive-header.code-snippets +16 -0
  32. package/.vscode/srcdev-component-select-menu.code-snippets +28 -0
  33. package/.vscode/srcdev-component-service-detail.code-snippets +1 -0
  34. package/.vscode/srcdev-component-service-summary.code-snippets +3 -1
  35. package/.vscode/srcdev-component-services-card.code-snippets +38 -36
  36. package/.vscode/srcdev-component-services-grids.code-snippets +2 -1
  37. package/.vscode/{srcdev-component-tabs-core.code-snippets → srcdev-component-tabbed-content.code-snippets} +35 -16
  38. package/app/assets/styles/setup/03.theming/_status.css +24 -0
  39. package/app/assets/styles/setup/03.theming/index.css +1 -0
  40. package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +39 -35
  41. package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +25 -15
  42. package/app/components/01.atoms/animations/container-glow/stories/ContainerGlow.stories.ts +130 -24
  43. package/app/components/01.atoms/animations/container-glow/tests/ContainerGlow.spec.ts +45 -0
  44. package/app/components/01.atoms/animations/container-glow/tests/__snapshots__/ContainerGlow.spec.ts.snap +2 -2
  45. package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +76 -19
  46. package/app/components/01.atoms/animations/glowing-border/GlowingBorder.vue +12 -10
  47. package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +201 -42
  48. package/app/components/01.atoms/animations/glowing-border/tests/GlowingBorder.spec.ts +15 -4
  49. package/app/components/01.atoms/animations/glowing-border/tests/__snapshots__/GlowingBorder.spec.ts.snap +1 -1
  50. package/app/components/01.atoms/display-pill/CONSUMER-STYLING.md +78 -9
  51. package/app/components/01.atoms/display-pill/DisplayPill.vue +59 -23
  52. package/app/components/01.atoms/display-pill/stories/DisplayPill.stories.ts +240 -16
  53. package/app/components/01.atoms/display-pill/tests/DisplayPill.spec.ts +43 -14
  54. package/app/components/01.atoms/display-pill/tests/__snapshots__/DisplayPill.spec.ts.snap +3 -3
  55. package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +16 -4
  56. package/app/components/01.atoms/navigation/breadcrumb/CONSUMER-STYLING.md +28 -5
  57. package/app/components/01.atoms/navigation/breadcrumb/stories/Breadcrumb.stories.ts +94 -0
  58. package/app/components/01.atoms/navigation/breadcrumb/tests/Breadcrumb.spec.ts +27 -0
  59. package/app/components/01.atoms/tabbed-content/CONSUMER-STYLING.md +201 -0
  60. package/app/components/01.atoms/tabbed-content/TabbedContent.vue +750 -0
  61. package/app/components/01.atoms/tabbed-content/stories/TabbedContent.stories.ts +279 -0
  62. package/app/components/01.atoms/tabbed-content/tests/TabbedContent.spec.ts +816 -0
  63. package/app/components/02.molecules/alert-content/AlertContent.vue +11 -2
  64. package/app/components/02.molecules/alert-content/AlertContentInner.vue +19 -0
  65. package/app/components/02.molecules/alert-content/CONSUMER-STYLING.md +17 -4
  66. package/app/components/02.molecules/alert-content/stories/AlertContent.stories.ts +200 -17
  67. package/app/components/02.molecules/alert-content/tests/AlertContent.spec.ts +33 -0
  68. package/app/components/02.molecules/display-avatar/CONSUMER-STYLING.md +7 -5
  69. package/app/components/02.molecules/display-avatar/DisplayAvatar.vue +6 -2
  70. package/app/components/02.molecules/display-avatar/stories/DisplayAvatar.stories.ts +24 -3
  71. package/app/components/02.molecules/display-avatar/tests/DisplayAvatar.spec.ts +19 -0
  72. package/app/components/02.molecules/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap +2 -1
  73. package/app/components/02.molecules/display-chip/CONSUMER-STYLING.md +52 -19
  74. package/app/components/02.molecules/display-chip/DisplayChip.vue +72 -54
  75. package/app/components/02.molecules/display-chip/stories/DisplayChip.stories.ts +103 -6
  76. package/app/components/02.molecules/display-chip/tests/DisplayChip.spec.ts +98 -29
  77. package/app/components/02.molecules/display-chip/tests/__snapshots__/DisplayChip.spec.ts.snap +8 -4
  78. package/app/components/02.molecules/prompt/CONSUMER-STYLING.md +9 -1
  79. package/app/components/02.molecules/prompt/DisplayPrompt.vue +2 -2
  80. package/app/components/02.molecules/prompt/stories/DisplayPrompt.stories.ts +208 -71
  81. package/app/components/02.molecules/prompt/tests/DisplayPrompt.spec.ts +19 -6
  82. package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +66 -8
  83. package/app/components/02.molecules/select-menu/SelectMenu.vue +141 -18
  84. package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +232 -2
  85. package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +151 -0
  86. package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +6 -2
  87. package/app/components/02.molecules/toast/CONSUMER-STYLING.md +91 -27
  88. package/app/components/02.molecules/toast/DisplayToast.vue +109 -50
  89. package/app/components/02.molecules/toast/DisplayToastProvider.vue +109 -45
  90. package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +23 -0
  91. package/app/components/02.molecules/toast/stories/DisplayToastProvider.stories.ts +73 -0
  92. package/app/components/02.molecules/toast/tests/DisplayToast.spec.ts +132 -18
  93. package/app/components/02.molecules/toast/tests/DisplayToastProvider.spec.ts +105 -39
  94. package/app/components/03.organisms/image-galleries/slider-gallery/CONSUMER-STYLING.md +19 -1
  95. package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +65 -7
  96. package/app/components/03.organisms/image-galleries/slider-gallery/stories/SliderGallery.stories.ts +201 -63
  97. package/app/components/03.organisms/image-galleries/slider-gallery/tests/SliderGallery.spec.ts +41 -2
  98. package/app/components/03.organisms/image-galleries/slider-gallery/tests/__snapshots__/SliderGallery.spec.ts.snap +6 -6
  99. package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +42 -11
  100. package/app/components/03.organisms/responsive-header/NavigationItems.vue +60 -16
  101. package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +86 -24
  102. package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +52 -0
  103. package/app/components/03.organisms/responsive-header/tests/NavigationItems.spec.ts +20 -0
  104. package/app/components/03.organisms/responsive-header/tests/ResponsiveHeader.spec.ts +40 -0
  105. package/app/components/03.organisms/services/service-detail/CONSUMER-STYLING.md +92 -41
  106. package/app/components/03.organisms/services/service-detail/ServiceDetail.vue +198 -73
  107. package/app/components/03.organisms/services/service-detail/stories/ServiceDetail.stories.ts +157 -9
  108. package/app/components/03.organisms/services/service-detail/tests/ServiceDetail.spec.ts +58 -0
  109. package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +5 -4
  110. package/app/components/03.organisms/services/service-summary/CONSUMER-STYLING.md +30 -0
  111. package/app/components/03.organisms/services/service-summary/ServiceSummary.vue +32 -8
  112. package/app/components/03.organisms/services/service-summary/stories/ServiceSummary.stories.ts +147 -0
  113. package/app/components/03.organisms/services/service-summary/tests/ServiceSummary.spec.ts +26 -0
  114. package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +2 -2
  115. package/app/components/03.organisms/services/services-card/CONSUMER-STYLING.md +104 -63
  116. package/app/components/03.organisms/services/services-card/ServicesCard.vue +92 -66
  117. package/app/components/03.organisms/services/services-card/stories/ServicesCard.stories.ts +194 -27
  118. package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +34 -8
  119. package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +8 -8
  120. package/app/components/03.organisms/services/services-grids/CONSUMER-STYLING.md +9 -0
  121. package/app/components/03.organisms/services/services-grids/ServicesCardGrid.vue +2 -1
  122. package/app/components/03.organisms/services/services-grids/stories/ServiceSummaryGrid.stories.ts +46 -6
  123. package/app/components/03.organisms/services/services-grids/stories/ServicesCardGrid.stories.ts +89 -11
  124. package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +8 -0
  125. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +6 -6
  126. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +24 -24
  127. package/app/components/05.forms/input-button/CONSUMER-STYLING.md +8 -0
  128. package/app/components/05.forms/input-button/InputButton.vue +3 -1
  129. package/app/composables/tests/useAnchorScroll.spec.ts +8 -0
  130. package/app/composables/useAnchorScroll.ts +1 -1
  131. package/app/composables/useTabs.ts +148 -206
  132. package/app/types/components/display-chip.d.ts +8 -4
  133. package/app/types/components/display-toast.d.ts +2 -0
  134. package/app/types/components/index.ts +0 -1
  135. package/app/types/components/select-menu.d.ts +2 -0
  136. package/app/utils/pausableTimeout.ts +41 -0
  137. package/nuxt.config.ts +1 -1
  138. package/package.json +1 -1
  139. package/.claude/skills/components/site-navigation.md +0 -145
  140. package/.claude/skills/components/tab-navigation.md +0 -272
  141. package/.claude/skills/components/tabs-core.md +0 -187
  142. package/.vscode/srcdev-component-site-navigation.code-snippets +0 -38
  143. package/.vscode/srcdev-component-tab-navigation.code-snippets +0 -30
  144. package/app/components/01.atoms/navigation/tabs/CONSUMER-STYLING.md +0 -120
  145. package/app/components/01.atoms/navigation/tabs/TabsCore.vue +0 -272
  146. package/app/components/01.atoms/navigation/tabs/stories/TabsCore.stories.ts +0 -199
  147. package/app/components/01.atoms/navigation/tabs/tests/TabsCore.spec.ts +0 -274
  148. package/app/components/02.molecules/navigation/site-navigation/CONSUMER-STYLING.md +0 -109
  149. package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +0 -751
  150. package/app/components/02.molecules/navigation/site-navigation/stories/SiteNavigation.stories.ts +0 -356
  151. package/app/components/02.molecules/navigation/site-navigation/tests/SiteNavigation.spec.ts +0 -367
  152. package/app/components/02.molecules/navigation/site-navigation/tests/__snapshots__/SiteNavigation.spec.ts.snap +0 -30
  153. package/app/components/02.molecules/navigation/tab-navigation/CONSUMER-STYLING.md +0 -150
  154. package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +0 -518
  155. package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts +0 -340
  156. package/app/components/02.molecules/navigation/tab-navigation/tests/TabNavigation.spec.ts +0 -339
  157. package/app/components/02.molecules/navigation/tab-navigation/tests/__snapshots__/TabNavigation.spec.ts.snap +0 -33
  158. package/app/composables/useNavCollapse.ts +0 -86
  159. 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 that takes no data or copy
139
- (e.g. a pure layout wrapper) still gets one with oversized slot content.
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 | `TabsCore` — nav `<li>` loop + content `<div>` loop linked by id |
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. TabsCore):
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/navigation/tabs/TabsCore.vue`
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/nav-item.d.ts
15
- export interface NavItem {
16
- text: string;
17
- href?: string;
18
- isExternal?: boolean;
19
- iconName?: string;
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 "./nav-item.d"
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 NavItem { ... }
43
- export interface NavItemData { ... }
37
+ export interface SelectMenuOption { ... }
44
38
 
45
39
  // After
46
- import type { NavItem, NavItemData } from "~/types/components/nav-item.d";
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 { NavItem, NavItemData } from "srcdev-nuxt-components";
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 | **decide**: `Tabs` is single-word and collision-prone | - |
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`, `-body-gap`,
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.
@@ -47,4 +47,9 @@ a `to` is a real link; the library never guesses which item is "current" from it
47
47
  - `ServiceDetail` uses this component internally for its hero banner breadcrumb, overriding
48
48
  `--breadcrumb-colour` to `white` so it reads over the hero image. See
49
49
  [service-detail.md](service-detail.md).
50
+ - Items with a blank `label` are skipped (no empty crumb, no dangling separator), and nothing renders when
51
+ none are left, so there is no empty `<nav>` landmark. An empty `separator` renders no separator element.
52
+ Added 2026-10-06.
53
+ - Long labels wrap (unbroken ones too). Set `--breadcrumb-item-line-clamp` (default `none`) to cap each
54
+ label's lines with an ellipsis, e.g. `1` for single-line crumbs. Added 2026-10-06.
50
55
  - See `CONSUMER-STYLING.md` in the component's own folder for the full `--breadcrumb-*` token API.
@@ -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 a `.glows` layer combine a thin proximity ring, a
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: string // chip diameter, e.g. "12px"
130
- maskWidth: string // cutout ring width, e.g. "4px"
131
- offset: string // distance from avatar edge, e.g. "0px"
132
- angle: string // position around avatar (0–360deg), e.g. "45deg"
133
- icon?: string // Iconify icon name
134
- label?: string // short text (max 3 characters)
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
- | `:config` | `DisplayChipConfig` | see below | Controls chip geometry and optional content. |
22
- | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes — use for status colour variants. |
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: string // chip dot diameter, e.g. "12px"
29
- maskWidth: string // cutout ring width around the chip, e.g. "4px"
30
- offset: string // extra distance from the parent edge, e.g. "0px"
31
- angle: string // position around the parent (0–360deg), e.g. "45deg"
32
- icon?: string // Iconify icon name rendered inside the chip
33
- label?: string // short text rendered inside the chip (max 3 characters)
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
- Default config: `{ size: "12px", maskWidth: "4px", offset: "0px", angle: "90deg" }`.
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
- Apply status via `styleClassPassthrough` — the component has built-in colour variants:
63
+ Set with the `status` prop:
57
64
 
58
- | Class | Token | Default |
65
+ | `status` | Token | Default |
59
66
  | --- | --- | --- |
60
- | (none) | `--display-chip-colour-offline` | `slategrey` |
61
- | `online` | `--display-chip-colour-online` | `rgb(0, 255, 135)` |
62
- | `idle` | `--display-chip-colour-idle` | `rgb(255, 185, 51)` |
63
- | `dnd` | `--display-chip-colour-dnd` | `rgb(255, 40, 80)` |
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 :style-class-passthrough="['online']">...</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 silently truncated with a `console.warn`.
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
- :style-class-passthrough="['online']"
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
- :style-class-passthrough="['idle']"
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
- :style-class-passthrough="['online']"
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
- :style-class-passthrough="['dnd']"
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" :style-class-passthrough="['online']">
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
- :style-class-passthrough="['online']"
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 `.chip-label` — ensure the host element is a direct child.
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 variant |
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,15 +89,65 @@ Colour resolves as **variant token → base token → variant default**: `--disp
89
89
  --display-pill-border-style: dashed;
90
90
  }
91
91
 
92
- /* Decorative outline ring */
93
- .ringed.display-pill {
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
 
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.
136
+
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
+
99
145
  ## Notes
100
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.
101
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).
102
152
  - `cursor: default` and `user-select: none` are applied unconditionally. `cursor: pointer` is applied automatically via `:is(button, a)` when `tag` is interactive.
103
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` | **Required in practice.** Bold heading text. Always rendered (even when empty). |
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` → `.closed` class → collapses via CSS |
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
- The `.closed` class triggers a CSS grid row animation (`grid-template-rows: 1fr → 0fr`) with
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
- - The `#title` slot renders unconditionally — an empty title `<p>` will still appear. Always
155
- provide meaningful content in `#title`.
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.