srcdev-nuxt-components 9.6.0 → 9.6.2
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 +22 -3
- 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 +27 -10
- package/.claude/skills/components/display-chip.md +38 -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/google-reviews.md +2 -2
- package/.claude/skills/components/profile-section.md +59 -21
- 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/social-icons-list.md +2 -0
- package/.claude/skills/components/tabbed-content.md +300 -0
- package/.claude/skills/composable-anchor-scroll.md +16 -15
- package/.claude/skills/index.md +8 -7
- package/.claude/skills/storybook-add-story.md +55 -0
- package/.claude/skills/theming-component-token-pattern.md +48 -0
- package/.claude/skills/theming-status-tokens.md +60 -0
- package/.vscode/srcdev-component-display-avatar.code-snippets +9 -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-profile-section.code-snippets +27 -4
- 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 +33 -15
- package/app/components/02.molecules/display-avatar/DisplayAvatar.vue +81 -36
- package/app/components/02.molecules/display-avatar/stories/DisplayAvatar.stories.ts +108 -6
- package/app/components/02.molecules/display-avatar/tests/DisplayAvatar.spec.ts +104 -13
- package/app/components/02.molecules/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap +4 -3
- package/app/components/02.molecules/display-chip/CONSUMER-STYLING.md +57 -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/profile-section/CONSUMER-STYLING.md +103 -13
- package/app/components/02.molecules/profile-section/ProfileSection.vue +123 -23
- package/app/components/02.molecules/profile-section/stories/ProfileSection.stories.ts +353 -115
- package/app/components/02.molecules/profile-section/tests/ProfileSection.spec.ts +144 -11
- package/app/components/02.molecules/profile-section/tests/__snapshots__/ProfileSection.spec.ts.snap +6 -11
- 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/social-icons-list/CONSUMER-STYLING.md +6 -2
- package/app/components/02.molecules/social-icons-list/SocialIconsList.vue +1 -0
- 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/google-reviews/tests/__snapshots__/GoogleReviewCard.spec.ts.snap +1 -1
- package/app/components/03.organisms/google-reviews/tests/__snapshots__/GoogleReviews.spec.ts.snap +6 -6
- 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,13 @@ 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").
|
|
104
|
+
Sample copy in stories, and examples in the skill doc, use placeholders (a name like "Alex
|
|
105
|
+
Morgan", a town like "Northtown"), never a real client's name, town, business or site: the
|
|
106
|
+
stories and docs ship in the public package. **Existing component:** check for, and replace, any
|
|
107
|
+
copy lifted from a consumer app (`ProfileSection` had both).
|
|
101
108
|
6a. **Storybook Controls-panel reactivity** — check every story `Template`/`render` function in
|
|
102
109
|
the component's `stories/*.stories.ts` files for this bug: `@storybook/vue3` mounts the story
|
|
103
110
|
component **once** and, on every Controls-panel change, mutates the same reactive `args`
|
|
@@ -131,12 +138,17 @@ The ledger can't see items 9, 10, 11 and 14, so check those by hand.
|
|
|
131
138
|
descriptions, body text) a line-clamp token, `--<component>-<element>-line-clamp`, which
|
|
132
139
|
covers ellipsis too (`1` is single-line ellipsis). Default it to `none` unless the layout
|
|
133
140
|
clearly needs a cap, and never clamp text that must stay readable (legal or attribution text,
|
|
134
|
-
link text that names its destination).
|
|
141
|
+
link text that names its destination). Only text the component renders itself (props and data) needs a token: text that arrives
|
|
142
|
+
through a slot is the consumer's own markup, so they clamp it directly and the component adds
|
|
143
|
+
no token for it (`ProfileSection` is all slots and has none). Shape and rules: `theming-component-token-pattern.md`,
|
|
135
144
|
"Line-clamp tokens". Give each line-clamp token a Controls-panel select in the stories (set on
|
|
136
145
|
a wrapper `:style`, stripped from the component's args), labelled as a story-only CSS token so
|
|
137
146
|
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
|
-
|
|
147
|
+
Add a unit test for any logic fix. A grid or layout component still gets one with oversized
|
|
148
|
+
slot content (an unbroken string in one cell finds `min-inline-size` overflow bugs). Components
|
|
149
|
+
with nothing to stress (animation wrappers, decorative containers that only take a slot and
|
|
150
|
+
tag/variant props, Storybook tooling) are exempt: they are listed in `stressStoryExempt` in
|
|
151
|
+
`.claude/component-ledger/build.mjs`; add a new one there rather than writing a padding story.
|
|
140
152
|
7. **Skill doc** — create or update `.claude/skills/components/<component-name>.md`, following the
|
|
141
153
|
pattern of an existing one. A labelled `<Name>Field` wrapper (or any other wrapper) is documented
|
|
142
154
|
in its control's doc under a "Variants" section, not in a doc of its own
|
|
@@ -216,3 +228,10 @@ The ledger can't see items 9, 10, 11 and 14, so check those by hand.
|
|
|
216
228
|
- `NuxtImg` gets explicit `width` and `height`; a `<video>` uses a `<source :src>` child with
|
|
217
229
|
`:key` and `.load()` (pitfall #12).
|
|
218
230
|
- No `#components` import (breaks Storybook); use `resolveComponent()`.
|
|
231
|
+
- Layout breakpoints respond to the component's own width (`container: <name> / inline-size` on the
|
|
232
|
+
root, `@container <name> (min-width: …)`), not the viewport, so it still works in a narrow
|
|
233
|
+
column. **Existing component:** when converting a viewport `@media` breakpoint, grep consumer
|
|
234
|
+
repos for `@media` overrides of the same element (they now switch at a different point) and list
|
|
235
|
+
the edits in private memory. Note that an inline-size container can't size itself from its
|
|
236
|
+
content: say so in `CONSUMER-STYLING.md` (`ProfileSection`).
|
|
237
|
+
- Grid tracks that hold text are `minmax(0, …)` so an unbroken string can't widen the grid.
|
|
@@ -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,9 @@
|
|
|
1
1
|
# DisplayAvatar Component
|
|
2
2
|
|
|
3
|
+
> **Changed 2026-10-14:** size is a `data-size` attribute, not a bare class (`.display-avatar.lg` → `.display-avatar[data-size="lg"]`). New `image-size` prop (default `112`): `NuxtImg` used to get `width="100%"`, which @nuxt/image ignores, so the Vercel provider requested its largest screen width for every avatar. A broken image now falls back to the initials, initials are emoji-safe, and long fallback text is clipped inside the circle. With `chip`, the status dot was drawn at the avatar's diameter instead of its configured size: avatar and chip both declared a private `--_size` on the same root element and the avatar's size rules won. The avatar's private tokens are now `--_avatar-size`/`--_avatar-font-size`. Content (both slots too) now sits in a full-size `.display-avatar-surface` that carries the background, so the chip's cutout covers the whole avatar instead of landing on the initials span and cutting into the letters.
|
|
4
|
+
|
|
5
|
+
> **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.
|
|
6
|
+
|
|
3
7
|
## Overview
|
|
4
8
|
|
|
5
9
|
`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.
|
|
@@ -16,8 +20,11 @@
|
|
|
16
20
|
| `src` | `string` | — | Image URL. Renders `NuxtImg` when set; fallback text otherwise. |
|
|
17
21
|
| `alt` | `string` | — | Alt text for the image; also used to derive initials. |
|
|
18
22
|
| `text` | `string` | — | Override the auto-derived initials with an explicit string. |
|
|
19
|
-
| `size` | `"xs" \| "s" \| "md" \| "lg" \| "xl"` | `"md"` |
|
|
23
|
+
| `size` | `"xs" \| "s" \| "md" \| "lg" \| "xl"` | `"md"` | Sets `data-size`, which picks width, height and font-size. |
|
|
20
24
|
| `chip` | `boolean \| DisplayChipConfig` | — | Add a status chip. `true` uses defaults; pass a config object to customise. |
|
|
25
|
+
| `status` | `DisplayChipStatus` | — | Chip status colour (`offline`/`online`/`idle`/`dnd`), forwarded to `DisplayChip`. Ignored without `chip`; the chip defaults to `offline`. |
|
|
26
|
+
| `status-label` | `string` | — | Screen-reader text for the chip status (e.g. "Online"), forwarded to `DisplayChip`. Ignored without `chip`. |
|
|
27
|
+
| `:image-size` | `number` | `112` | Pixel width and height requested from `@nuxt/image` (1x; 2x is double). Raise it if you resize the avatar past `xl` with `--display-avatar-size`. |
|
|
21
28
|
| `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes on the root element. |
|
|
22
29
|
|
|
23
30
|
### Size dimensions
|
|
@@ -41,24 +48,32 @@ Each step is a token (`--display-avatar-size-md`, `--display-avatar-font-size-md
|
|
|
41
48
|
| `default` | Replaces the auto image/fallback content entirely. |
|
|
42
49
|
| `icon` | Appended inside the avatar (e.g. an icon overlay over the image). |
|
|
43
50
|
|
|
51
|
+
Both slots render inside `.display-avatar-surface`, a full-size element that carries the background. With `chip`, that surface is what gets the cutout mask.
|
|
52
|
+
|
|
44
53
|
---
|
|
45
54
|
|
|
46
55
|
## Fallback text logic
|
|
47
56
|
|
|
48
57
|
When `src` is not set, a `<span>` renders the fallback value:
|
|
49
58
|
|
|
50
|
-
1. `text` prop — used as-is if
|
|
51
|
-
2. `alt` initials — first character
|
|
59
|
+
1. `text` prop — used as-is if it has any non-whitespace content.
|
|
60
|
+
2. `alt` initials — the first character (a whole grapheme, so emoji and flags stay intact) of the first two words. Extra whitespace is ignored.
|
|
52
61
|
3. Empty string — if neither is set.
|
|
53
62
|
|
|
54
63
|
When `alt` is set, the initials are `aria-hidden` and the full `alt` is rendered as `sr-only` text, so a screen reader announces "Jane Smith" rather than "J S". With only `text`, the text is read as-is.
|
|
55
64
|
|
|
65
|
+
If the image fails to load, the initials fallback shows instead (an empty circle when there is no `alt`). A new `src` tries the image again.
|
|
66
|
+
|
|
67
|
+
The fallback is one line, clipped to the circle: keep `text` short (1–3 characters).
|
|
68
|
+
|
|
56
69
|
With `src`, the image uses `alt`, or an empty (decorative) `alt` when none is given. Pass `alt` unless the name is already visible next to the avatar.
|
|
57
70
|
|
|
58
71
|
```
|
|
59
72
|
alt="John Doe" → "JD"
|
|
60
73
|
alt="Alice" → "A"
|
|
61
74
|
alt="Alice Bob C" → "AB"
|
|
75
|
+
alt="🦄 Unicorn" → "🦄U"
|
|
76
|
+
alt=" Jane Smith " → "JS"
|
|
62
77
|
text="?" → "?"
|
|
63
78
|
```
|
|
64
79
|
|
|
@@ -102,10 +117,12 @@ text="?" → "?"
|
|
|
102
117
|
src="/images/profile.jpg"
|
|
103
118
|
alt="Jane Smith"
|
|
104
119
|
:chip="true"
|
|
120
|
+
status="online"
|
|
121
|
+
status-label="Online"
|
|
105
122
|
/>
|
|
106
123
|
```
|
|
107
124
|
|
|
108
|
-
Default chip config: `{ size: "12px", maskWidth: "4px", offset: "0px", angle: "90deg" }`.
|
|
125
|
+
Default chip config: `{ size: "12px", maskWidth: "4px", offset: "0px", angle: "90deg" }`. Without `status` the dot is `offline`.
|
|
109
126
|
|
|
110
127
|
### With a custom chip
|
|
111
128
|
|
|
@@ -126,12 +143,12 @@ Full `DisplayChipConfig` shape (pass directly as the `chip` value):
|
|
|
126
143
|
|
|
127
144
|
```ts
|
|
128
145
|
interface DisplayChipConfig {
|
|
129
|
-
size
|
|
130
|
-
maskWidth
|
|
131
|
-
offset
|
|
132
|
-
angle
|
|
133
|
-
icon?: string
|
|
134
|
-
label?: string
|
|
146
|
+
size?: string // chip diameter, e.g. "12px"
|
|
147
|
+
maskWidth?: string // cutout ring width, e.g. "4px"
|
|
148
|
+
offset?: string // distance from avatar edge, e.g. "0px"
|
|
149
|
+
angle?: string // position around avatar (0–360deg), e.g. "45deg"
|
|
150
|
+
icon?: string // Iconify icon name
|
|
151
|
+
label?: string // short text (max 3 characters)
|
|
135
152
|
}
|
|
136
153
|
```
|
|
137
154
|
|
|
@@ -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,7 @@ 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. The mask is positioned in percentages of each child's own box, so slot exactly one host element that fills the chip and carries its own background (an `<img>`, or a full-size wrapper around text). A small text span as a direct child gets the notch cut into its letters, and a background set on `.display-chip` itself is never cut. `DisplayAvatar` wraps its content in a full-size `.display-avatar-surface` for this reason.
|
|
230
|
+
- A component that renders `DisplayChip` as its own root (as `DisplayAvatar` does) shares that element, so it must not declare the chip's private tokens (`--_size`, `--_offset`, ...) for its own values: they collide on the same element. `DisplayAvatar` uses `--_avatar-size`.
|
|
231
|
+
- The `StressTest` story shows the worst-case labels and config values.
|
|
218
232
|
- `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.
|