srcdev-nuxt-components 9.6.1 → 9.6.3

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 (66) hide show
  1. package/.claude/rules/consumer/consumer-overrides.md +17 -0
  2. package/.claude/rules/consumer/css-comment-slash.md +13 -0
  3. package/.claude/rules/consumer/css-native-nesting.md +13 -0
  4. package/.claude/rules/consumer/css-no-deep.md +13 -0
  5. package/.claude/rules/consumer/css-rem-base.md +11 -0
  6. package/.claude/rules/consumer/css-surface-text-pairing.md +13 -0
  7. package/.claude/rules/consumer/css-webkit-support.md +15 -0
  8. package/.claude/rules/consumer/explicit-vue-imports.md +11 -0
  9. package/.claude/rules/consumer/nuxt-img-dimensions.md +12 -0
  10. package/.claude/rules/consumer/srcdev-skills-location.md +9 -0
  11. package/.claude/rules/consumer/story-args.md +12 -0
  12. package/.claude/rules/consumer/story-register-components.md +12 -0
  13. package/.claude/rules/consumer/vue-auto-imports.md +14 -0
  14. package/.claude/rules/consumer/vue-no-self-closing.md +11 -0
  15. package/.claude/rules/consumer/vue-prop-hyphenation.md +11 -0
  16. package/.claude/rules/consumer/vue-props-pattern.md +13 -0
  17. package/.claude/rules/consumer/vue-slots-no-computed.md +14 -0
  18. package/.claude/rules/consumer/vue-video-source.md +12 -0
  19. package/.claude/skills/component-compliance-checklist.md +37 -2
  20. package/.claude/skills/component-naming.md +1 -1
  21. package/.claude/skills/components/display-avatar.md +14 -3
  22. package/.claude/skills/components/display-chip.md +2 -1
  23. package/.claude/skills/components/google-reviews.md +2 -2
  24. package/.claude/skills/components/pricing-card.md +68 -84
  25. package/.claude/skills/components/profile-section.md +59 -21
  26. package/.claude/skills/components/social-icons-list.md +2 -0
  27. package/.claude/skills/index.md +13 -6
  28. package/.claude/skills/new-app-scaffold.md +4 -2
  29. package/.claude/skills/setup-postinstall.md +5 -4
  30. package/.claude/skills/storybook-add-story.md +38 -0
  31. package/.claude/skills/theming-component-token-pattern.md +94 -0
  32. package/.claude/skills/theming-partial-override.md +1 -1
  33. package/.vscode/srcdev-component-display-avatar.code-snippets +7 -0
  34. package/.vscode/srcdev-component-pricing-card.code-snippets +46 -84
  35. package/.vscode/srcdev-component-profile-section.code-snippets +27 -4
  36. package/README.md +5 -5
  37. package/app/components/01.atoms/animations/scroll-reveal-frame/stories/ScrollRevealFrame.stories.ts +2 -0
  38. package/app/components/01.atoms/animations/scroll-reveal-image/stories/ScrollRevealImage.stories.ts +2 -0
  39. package/app/components/01.atoms/animations/section-parallax/stories/SectionParallax.stories.ts +4 -0
  40. package/app/components/01.atoms/animations/view-timeline/stories/WipeAwayVertical.stories.ts +2 -0
  41. package/app/components/02.molecules/cookie-consent-banner/stories/CookieConsentBanner.stories.ts +2 -0
  42. package/app/components/02.molecules/display-avatar/CONSUMER-STYLING.md +26 -10
  43. package/app/components/02.molecules/display-avatar/DisplayAvatar.vue +75 -34
  44. package/app/components/02.molecules/display-avatar/stories/DisplayAvatar.stories.ts +84 -3
  45. package/app/components/02.molecules/display-avatar/tests/DisplayAvatar.spec.ts +85 -13
  46. package/app/components/02.molecules/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap +3 -3
  47. package/app/components/02.molecules/display-chip/CONSUMER-STYLING.md +7 -2
  48. package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +0 -1
  49. package/app/components/02.molecules/pricing-card/CONSUMER-STYLING.md +96 -149
  50. package/app/components/02.molecules/pricing-card/PricingCard.vue +84 -44
  51. package/app/components/02.molecules/pricing-card/stories/PricingCard.stories.ts +241 -104
  52. package/app/components/02.molecules/pricing-card/tests/PricingCard.spec.ts +149 -123
  53. package/app/components/02.molecules/profile-section/CONSUMER-STYLING.md +103 -13
  54. package/app/components/02.molecules/profile-section/ProfileSection.vue +123 -23
  55. package/app/components/02.molecules/profile-section/stories/ProfileSection.stories.ts +353 -115
  56. package/app/components/02.molecules/profile-section/tests/ProfileSection.spec.ts +144 -11
  57. package/app/components/02.molecules/profile-section/tests/__snapshots__/ProfileSection.spec.ts.snap +6 -11
  58. package/app/components/02.molecules/social-icons-list/CONSUMER-STYLING.md +6 -2
  59. package/app/components/02.molecules/social-icons-list/SocialIconsList.vue +1 -0
  60. package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +4 -0
  61. package/app/components/02.molecules/toast/stories/DisplayToastProvider.stories.ts +4 -0
  62. package/app/components/03.organisms/google-reviews/tests/__snapshots__/GoogleReviewCard.spec.ts.snap +1 -1
  63. package/app/components/03.organisms/google-reviews/tests/__snapshots__/GoogleReviews.spec.ts.snap +6 -6
  64. package/app/components/05.forms/toggle-switch/ToggleSwitchWithLabel.vue +1 -1
  65. package/app/composables/useAriaDescribedById.ts +1 -1
  66. package/package.json +2 -1
@@ -0,0 +1,17 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.vue"
4
+ - "app/**/*.css"
5
+ ---
6
+
7
+ # Overriding srcdev-nuxt-components styles
8
+
9
+ Override a layer component through its public tokens (`--component-name-*`, listed in its
10
+ `CONSUMER-STYLING.md`) set on an ancestor you own. Never wrap override `<style>` blocks in a named
11
+ `@layer`, never set its private `--_` tokens, and override colours and geometry only, never
12
+ behaviour (`display`, `visibility`, `pointer-events`, `z-index`, animations). Applies in apps that
13
+ extend the layer, not to a component's own styles inside srcdev-nuxt-components.
14
+
15
+ Why: unlayered CSS always beats the layer's `@layer components`, while a named layer can be ordered
16
+ below it by SSR-inlined chunks; private tokens can change in any release.
17
+ Detail: skill `component-local-style-override.md` (srcdev-nuxt-components).
@@ -0,0 +1,13 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.vue"
4
+ - "app/**/*.css"
5
+ ---
6
+
7
+ # No `*/` inside CSS comments
8
+
9
+ Never join token names with a bare slash in a CSS comment (`--_form-*/--_scheme-icon-font-size`);
10
+ write "and" or space the slash.
11
+
12
+ Why: `*/` closes the comment early and yields a misleading "Unclosed string" error lines later.
13
+
@@ -0,0 +1,13 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.vue"
4
+ - "app/**/*.css"
5
+ ---
6
+
7
+ # Native CSS nesting, no Sass concatenation
8
+
9
+ Never write `&__child` or `&-modifier`. Use `& .block__child` or a top-level `.block__child {}` rule.
10
+ Nest child rules inside the component's root block.
11
+
12
+ Why: it's Sass syntax; esbuild turns `&__foo` into `:is(__foo)`, which silently matches nothing.
13
+ Detail: skill `css-nesting-conventions.md` (srcdev-nuxt-components).
@@ -0,0 +1,13 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.vue"
4
+ ---
5
+
6
+ # `:deep()` only in `<style scoped>`
7
+
8
+ Never use `:deep()` in an unscoped `<style>` block (srcdev-nuxt-components' own styles are never
9
+ scoped). There, reach a child component with a plain descendant selector (`& .input-button`), or
10
+ better, set the child's public tokens on an ancestor. In a `<style scoped>` block it works as normal.
11
+
12
+ Why: outside `<style scoped>` `:deep()` stays as an unknown pseudo-class and the browser silently
13
+ drops the whole rule.
@@ -0,0 +1,11 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.vue"
4
+ - "app/**/*.css"
5
+ ---
6
+
7
+ # 1rem = 10px
8
+
9
+ Calculate rem values on a 10px base (`html { font-size: 62.5% }`): `1.6rem` = 16px, `2.4rem` = 24px.
10
+
11
+ Why: the browser default 16px base gives wrong sizes.
@@ -0,0 +1,13 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.vue"
4
+ - "app/**/*.css"
5
+ ---
6
+
7
+ # Text on dark surfaces uses `--theme-on-surface`
8
+
9
+ Pair a dark/bold surface (`--theme-surface-hover`, `--theme-surface-inverted`) with
10
+ `--theme-on-surface`, never `--theme-text` or `--theme-text-inverted`.
11
+
12
+ Why: those text tokens assume a light surface, so the pairing renders dark-on-dark in light mode.
13
+ Detail: skill `theming-partial-override.md` (srcdev-nuxt-components).
@@ -0,0 +1,15 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.vue"
4
+ - "app/**/*.css"
5
+ ---
6
+
7
+ # Load-bearing CSS must work in WebKit / Safari 16
8
+
9
+ Never rely on a new CSS feature (`::details-content`, `@starting-style`, `appearance: base-select`,
10
+ anchor positioning, ...) for anything load-bearing unless Safari 16 has a working path; decorative
11
+ extras may degrade. Prefer routing to a proven alternative implementation over a CSS-only
12
+ `@supports` patch.
13
+
14
+ Why: WebKit silently drops unsupported rules, and Chrome's device emulation is still Blink so it
15
+ can't show the gap.
@@ -0,0 +1,11 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.spec.ts"
4
+ - "**/*.stories.ts"
5
+ ---
6
+
7
+ # Import Vue APIs explicitly in tests and stories
8
+
9
+ Always import `nextTick`, `computed`, `ref`, etc. from `"vue"` in `.spec.ts` and `.stories.ts` files.
10
+
11
+ Why: auto-imports only apply inside components.
@@ -0,0 +1,12 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.vue"
4
+ ---
5
+
6
+ # NuxtImg needs numeric width and height
7
+
8
+ Always give `NuxtImg` numeric `width` and `height` props, roughly the largest rendered pixel width
9
+ (pass a size prop through when it varies). Never `width="100%"`. CSS sizes the displayed image.
10
+
11
+ Why: without them the Vercel provider requests a width it doesn't allow (`w=1536`), and a
12
+ non-numeric width makes it request its largest screen width for every image.
@@ -0,0 +1,9 @@
1
+ # Where srcdev-nuxt-components skills live
2
+
3
+ A skill named as "skill `x.md` (srcdev-nuxt-components)" is at
4
+ `.claude/skills/srcdev-nuxt-components/x.md`. Per-component usage docs are in its `components/`
5
+ subfolder (`.claude/skills/srcdev-nuxt-components/components/<component-name>.md`). Inside the
6
+ srcdev-nuxt-components repo itself, the same files are directly under `.claude/skills/`.
7
+
8
+ Why: the layer's rules name skills rather than linking a path, so they read the same in the
9
+ library repo and in consuming apps.
@@ -0,0 +1,12 @@
1
+ ---
2
+ paths:
3
+ - "**/*.stories.ts"
4
+ ---
5
+
6
+ # Story args stay reactive
7
+
8
+ Never destructure, copy (`ref(args.x)`) or `watch` `args` at setup time. Strip non-prop extra args
9
+ inside a `computed` before `v-bind`, and type the meta as `Meta<StoryArgs>` when it has extra args.
10
+
11
+ Why: setup runs once, so copied args and watchers miss control changes, and unstripped extras become
12
+ unknown HTML attributes. Detail: skill `storybook-add-story.md` (srcdev-nuxt-components).
@@ -0,0 +1,12 @@
1
+ ---
2
+ paths:
3
+ - "**/*.stories.ts"
4
+ ---
5
+
6
+ # Register every component a story template uses
7
+
8
+ Every component in a story's `template` string, including ones placed in the component's slots, must
9
+ be listed in that story's `components: {}`.
10
+
11
+ Why: components aren't auto-registered in story templates; unregistered ones render as unknown tags.
12
+ Detail: skill `storybook-add-story.md` (srcdev-nuxt-components).
@@ -0,0 +1,14 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.vue"
4
+ - "app/composables/**/*.ts"
5
+ - "app/utils/**/*.ts"
6
+ ---
7
+
8
+ # Vue and Nuxt auto-imports
9
+
10
+ Never import `ref`, `reactive`, `computed`, `watch`, `nextTick`, `useState`, `useRoute`, `useRouter`
11
+ or anything from `~/composables/` in app code. Import only `type`s and external packages.
12
+
13
+ Why: Nuxt auto-imports them; manual imports are noise and drift from the rest of the codebase.
14
+ Test and story files are the exception (see `explicit-vue-imports.md`).
@@ -0,0 +1,11 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.vue"
4
+ ---
5
+
6
+ # No self-closing non-void elements
7
+
8
+ Always write explicit closing tags on non-void elements and slots: `<slot name="x"></slot>`,
9
+ `<span></span>`. Never `<slot name="x" />`.
10
+
11
+ Why: ESLint `vue/html-self-closing` rejects it.
@@ -0,0 +1,11 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.vue"
4
+ - "**/*.stories.ts"
5
+ ---
6
+
7
+ # Hyphenated props in templates
8
+
9
+ Always write camelCase props hyphenated in templates: `:item-count`, `:style-class-passthrough`.
10
+
11
+ Why: ESLint `vue/attribute-hyphenation` requires it.
@@ -0,0 +1,13 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.vue"
4
+ ---
5
+
6
+ # Props pattern
7
+
8
+ Always declare props as `interface Props` + `withDefaults(defineProps<Props>(), {...})`, with factory
9
+ functions for array/object defaults. Never use options-style `defineProps({ x: { type, default } })`.
10
+ Never declare `class` or `style` as props; hide them from Storybook controls via `argTypes` instead.
11
+
12
+ Why: typed props are the standard across srcdev-nuxt-components and its apps, and a
13
+ `class`/`style` prop breaks Vue's automatic `$attrs` inheritance.
@@ -0,0 +1,14 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.vue"
4
+ - "app/composables/**/*.ts"
5
+ ---
6
+
7
+ # Never derive state from slots in a computed
8
+
9
+ Never read `useSlots()` inside a `computed()`. Use a plain function called from the template
10
+ (`v-if="hasFooter()"`, `:aria-describedby="ariaDescribedby()"`), including anything derived from it.
11
+ Test by toggling a `v-if`'d slot in a host component after mount.
12
+
13
+ Why: the slots object isn't reactive, so the computed caches its first value and leaves split state
14
+ when a slot is added or removed after mount.
@@ -0,0 +1,12 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.vue"
4
+ ---
5
+
6
+ # Video src goes on a `<source>` child
7
+
8
+ Never bind `:src` on a `<video>`. Use `<source :src="src">` inside it, `:key="src"` on the video, and
9
+ an explicit `videoEl.load()` on mount and src change.
10
+
11
+ Why: Vue patching `:src` on client-side navigation silently skips the fetch.
12
+ Detail: skill `vue-video-autoplay.md` (srcdev-nuxt-components).
@@ -15,7 +15,7 @@ A component passes when its Component Ledger row (`node .claude/component-ledger
15
15
  `.claude/component-ledger/audit.json`) shows `score: 5`, a real tier, and `variants`, `legacy_props`,
16
16
  `story_args_bug`, `eslint_issues`, `redundant_priv_tokens`, `styling_doc_outdated` and `missing_stress_story`
17
17
  all `false`.
18
- The ledger can't see items 9, 10, 11 and 14, so check those by hand.
18
+ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
19
19
 
20
20
  ## The checklist
21
21
 
@@ -101,6 +101,10 @@ The ledger can't see items 9, 10, 11 and 14, so check those by hand.
101
101
  If other components render this one inside them (it's a child or building block of theirs),
102
102
  each story gets a short note that explains the relationship and links to the parent's story
103
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).
104
108
  6a. **Storybook Controls-panel reactivity** — check every story `Template`/`render` function in
105
109
  the component's `stories/*.stories.ts` files for this bug: `@storybook/vue3` mounts the story
106
110
  component **once** and, on every Controls-panel change, mutates the same reactive `args`
@@ -134,7 +138,9 @@ The ledger can't see items 9, 10, 11 and 14, so check those by hand.
134
138
  descriptions, body text) a line-clamp token, `--<component>-<element>-line-clamp`, which
135
139
  covers ellipsis too (`1` is single-line ellipsis). Default it to `none` unless the layout
136
140
  clearly needs a cap, and never clamp text that must stay readable (legal or attribution text,
137
- link text that names its destination). Shape and rules: `theming-component-token-pattern.md`,
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`,
138
144
  "Line-clamp tokens". Give each line-clamp token a Controls-panel select in the stories (set on
139
145
  a wrapper `:style`, stripped from the component's args), labelled as a story-only CSS token so
140
146
  nobody mistakes it for a prop: `storybook-add-story.md`, "Label token controls as story-only".
@@ -143,6 +149,21 @@ The ledger can't see items 9, 10, 11 and 14, so check those by hand.
143
149
  with nothing to stress (animation wrappers, decorative containers that only take a slot and
144
150
  tag/variant props, Storybook tooling) are exempt: they are listed in `stressStoryExempt` in
145
151
  `.claude/component-ledger/build.mjs`; add a new one there rather than writing a padding story.
152
+ 6c. **Storybook docs page** — every story file gets a generated Docs page: `tags: ["autodocs"]` is
153
+ set globally in `.storybook/preview.ts` (since 2026-10-14), so don't repeat it in the meta.
154
+ Open the component's Docs page and check that it reads well:
155
+ - A `parameters.docs.description.component` in the meta says what the component is for and
156
+ names its main props/slots/events. Per-story `docs.description.story` text explains what
157
+ that story shows (the Docs page is the only place either is visible).
158
+ - The props table is accurate. Every story-only arg has a `description` and a category, and
159
+ CSS-token args are labelled story-only (6b).
160
+ - Every story renders inline. A story that misbehaves on the shared page (fixed/sticky
161
+ overlays, `layout: "fullscreen"` canvases, dialogs, toasts, scroll effects, heavy stress
162
+ data) gets `parameters.docs.story: { inline: false, height: "<px>" }` so it renders in its
163
+ own iframe. Don't drop it from the page.
164
+ - A story file that isn't a component reference (Storybook tooling, a file that already has an
165
+ attached `.mdx` page) opts out with `tags: ["!autodocs"]`. `ColourRamps.stories.ts` does this.
166
+ See `storybook-add-story.md`, "Docs page".
146
167
  7. **Skill doc** — create or update `.claude/skills/components/<component-name>.md`, following the
147
168
  pattern of an existing one. A labelled `<Name>Field` wrapper (or any other wrapper) is documented
148
169
  in its control's doc under a "Variants" section, not in a doc of its own
@@ -222,3 +243,17 @@ The ledger can't see items 9, 10, 11 and 14, so check those by hand.
222
243
  - `NuxtImg` gets explicit `width` and `height`; a `<video>` uses a `<source :src>` child with
223
244
  `:key` and `.load()` (pitfall #12).
224
245
  - No `#components` import (breaks Storybook); use `resolveComponent()`.
246
+ - Layout breakpoints respond to the component's own width (`container: <name> / inline-size` on the
247
+ root, `@container <name> (min-width: …)`), not the viewport, so it still works in a narrow
248
+ column. **Existing component:** when converting a viewport `@media` breakpoint, grep consumer
249
+ repos for `@media` overrides of the same element (they now switch at a different point) and list
250
+ the edits in private memory. Note that an inline-size container can't size itself from its
251
+ content: say so in `CONSUMER-STYLING.md` (`ProfileSection`).
252
+ - Grid tracks that hold text are `minmax(0, …)` so an unbroken string can't widen the grid.
253
+ - No `:deep()` in the component's `<style>`: these blocks are never scoped, so Vue leaves it
254
+ as-is and the browser drops the whole rule (pitfall #27). Use a descendant selector, or set
255
+ the child component's public tokens on an ancestor.
256
+ - No `font-size` (or font-size token default) under `1rem`. `html` is `62.5%`, so `1rem` is
257
+ 10px, and values like `0.75rem`/`0.9rem` are almost always copied from a 16px-base design
258
+ and render at 7.5-9px. `PricingCard` had both (fixed 2026-10-15); grep
259
+ `font-size:\s*(var\([^,]+,\s*)?0?\.[0-9]+rem` to find them.
@@ -58,7 +58,7 @@ stripping `Core`.
58
58
 
59
59
  1. `git mv` the `.vue` files, their stories and tests (and the skill doc if it's named after the old
60
60
  control name).
61
- 2. `sed` the old names across `app/`, `.claude/`, `.vscode/`, `MIGRATION.md` and `Claude.md`:
61
+ 2. `sed` the old names across `app/`, `.claude/`, `.vscode/`, `MIGRATION.md` and `CLAUDE.md`:
62
62
  component names, `import` paths, story titles/`*StoryArgs` interfaces, `describe()` names, and the
63
63
  root/base DOM class (update `CONSUMER-STYLING.md` and test selectors that use it).
64
64
  3. Add a "Renamed YYYY-MM-DD" note at the top of the skill doc listing old → new names and classes.
@@ -1,5 +1,7 @@
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
+
3
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.
4
6
 
5
7
  ## Overview
@@ -18,10 +20,11 @@
18
20
  | `src` | `string` | — | Image URL. Renders `NuxtImg` when set; fallback text otherwise. |
19
21
  | `alt` | `string` | — | Alt text for the image; also used to derive initials. |
20
22
  | `text` | `string` | — | Override the auto-derived initials with an explicit string. |
21
- | `size` | `"xs" \| "s" \| "md" \| "lg" \| "xl"` | `"md"` | Controls width, height, and font-size. |
23
+ | `size` | `"xs" \| "s" \| "md" \| "lg" \| "xl"` | `"md"` | Sets `data-size`, which picks width, height and font-size. |
22
24
  | `chip` | `boolean \| DisplayChipConfig` | — | Add a status chip. `true` uses defaults; pass a config object to customise. |
23
25
  | `status` | `DisplayChipStatus` | — | Chip status colour (`offline`/`online`/`idle`/`dnd`), forwarded to `DisplayChip`. Ignored without `chip`; the chip defaults to `offline`. |
24
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`. |
25
28
  | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes on the root element. |
26
29
 
27
30
  ### Size dimensions
@@ -45,24 +48,32 @@ Each step is a token (`--display-avatar-size-md`, `--display-avatar-font-size-md
45
48
  | `default` | Replaces the auto image/fallback content entirely. |
46
49
  | `icon` | Appended inside the avatar (e.g. an icon overlay over the image). |
47
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
+
48
53
  ---
49
54
 
50
55
  ## Fallback text logic
51
56
 
52
57
  When `src` is not set, a `<span>` renders the fallback value:
53
58
 
54
- 1. `text` prop — used as-is if provided.
55
- 2. `alt` initials — first character of each word, capped at two characters.
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.
56
61
  3. Empty string — if neither is set.
57
62
 
58
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.
59
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
+
60
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.
61
70
 
62
71
  ```
63
72
  alt="John Doe" → "JD"
64
73
  alt="Alice" → "A"
65
74
  alt="Alice Bob C" → "AB"
75
+ alt="🦄 Unicorn" → "🦄U"
76
+ alt=" Jane Smith " → "JS"
66
77
  text="?" → "?"
67
78
  ```
68
79
 
@@ -226,6 +226,7 @@ See [display-avatar.md](./display-avatar.md) for the full API.
226
226
  - `shape` must match the actual shape of the slot content — the position maths differs between `circle` (radius-based) and `square` (clamped corner-aware).
227
227
  - `config` values are geometric inputs to CSS `calc(cos())` / `calc(sin())` expressions. Pass them as strings with units (`"12px"`, `"45deg"`), not plain numbers.
228
228
  - The chip dot is rendered via `::after` pseudo-element; icon and label sit above it at `z-index: 2`.
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.
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`.
230
231
  - The `StressTest` story shows the worst-case labels and config values.
231
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).
@@ -132,8 +132,8 @@ replaced.
132
132
  - Survives hostile data (see the `StressTest` story): long and unbroken text wraps inside its card,
133
133
  reviewer names are cut to 2 lines (`--google-reviews-author-line-clamp`),
134
134
  initials take whole characters (emoji-safe), star fill is clamped to 0–5, review text renders as
135
- plain text. A broken photo URL still shows the browser's broken-image icon, since
136
- `DisplayAvatar` has no image-error fallback.
135
+ plain text. A broken photo URL falls back to the reviewer's initials (`DisplayAvatar`'s image-error
136
+ fallback).
137
137
 
138
138
  ## Variants
139
139