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.
Files changed (121) hide show
  1. package/.claude/skills/component-compliance-checklist.md +22 -3
  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/container-glow.md +21 -8
  7. package/.claude/skills/components/display-avatar.md +27 -10
  8. package/.claude/skills/components/display-chip.md +38 -24
  9. package/.claude/skills/components/display-pill.md +58 -10
  10. package/.claude/skills/components/display-prompt.md +8 -5
  11. package/.claude/skills/components/display-toast.md +21 -4
  12. package/.claude/skills/components/glowing-border.md +20 -2
  13. package/.claude/skills/components/google-reviews.md +2 -2
  14. package/.claude/skills/components/profile-section.md +59 -21
  15. package/.claude/skills/components/responsive-header.md +11 -1
  16. package/.claude/skills/components/select-menu.md +54 -5
  17. package/.claude/skills/components/site-header.md +2 -2
  18. package/.claude/skills/components/social-icons-list.md +2 -0
  19. package/.claude/skills/components/tabbed-content.md +300 -0
  20. package/.claude/skills/composable-anchor-scroll.md +16 -15
  21. package/.claude/skills/index.md +8 -7
  22. package/.claude/skills/storybook-add-story.md +55 -0
  23. package/.claude/skills/theming-component-token-pattern.md +48 -0
  24. package/.claude/skills/theming-status-tokens.md +60 -0
  25. package/.vscode/srcdev-component-display-avatar.code-snippets +9 -1
  26. package/.vscode/srcdev-component-display-chip.code-snippets +3 -2
  27. package/.vscode/srcdev-component-display-pill.code-snippets +10 -0
  28. package/.vscode/srcdev-component-profile-section.code-snippets +27 -4
  29. package/.vscode/srcdev-component-responsive-header.code-snippets +6 -0
  30. package/.vscode/srcdev-component-select-menu.code-snippets +28 -0
  31. package/.vscode/{srcdev-component-tabs-core.code-snippets → srcdev-component-tabbed-content.code-snippets} +35 -16
  32. package/app/assets/styles/setup/03.theming/_status.css +24 -0
  33. package/app/assets/styles/setup/03.theming/index.css +1 -0
  34. package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +39 -35
  35. package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +25 -15
  36. package/app/components/01.atoms/animations/container-glow/stories/ContainerGlow.stories.ts +130 -24
  37. package/app/components/01.atoms/animations/container-glow/tests/ContainerGlow.spec.ts +45 -0
  38. package/app/components/01.atoms/animations/container-glow/tests/__snapshots__/ContainerGlow.spec.ts.snap +2 -2
  39. package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +76 -19
  40. package/app/components/01.atoms/animations/glowing-border/GlowingBorder.vue +12 -10
  41. package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +201 -42
  42. package/app/components/01.atoms/animations/glowing-border/tests/GlowingBorder.spec.ts +15 -4
  43. package/app/components/01.atoms/animations/glowing-border/tests/__snapshots__/GlowingBorder.spec.ts.snap +1 -1
  44. package/app/components/01.atoms/display-pill/CONSUMER-STYLING.md +75 -9
  45. package/app/components/01.atoms/display-pill/DisplayPill.vue +57 -24
  46. package/app/components/01.atoms/display-pill/stories/DisplayPill.stories.ts +240 -16
  47. package/app/components/01.atoms/display-pill/tests/DisplayPill.spec.ts +43 -14
  48. package/app/components/01.atoms/display-pill/tests/__snapshots__/DisplayPill.spec.ts.snap +3 -3
  49. package/app/components/01.atoms/tabbed-content/CONSUMER-STYLING.md +201 -0
  50. package/app/components/01.atoms/tabbed-content/TabbedContent.vue +750 -0
  51. package/app/components/01.atoms/tabbed-content/stories/TabbedContent.stories.ts +279 -0
  52. package/app/components/01.atoms/tabbed-content/tests/TabbedContent.spec.ts +816 -0
  53. package/app/components/02.molecules/alert-content/AlertContent.vue +11 -2
  54. package/app/components/02.molecules/alert-content/AlertContentInner.vue +19 -0
  55. package/app/components/02.molecules/alert-content/CONSUMER-STYLING.md +17 -4
  56. package/app/components/02.molecules/alert-content/stories/AlertContent.stories.ts +200 -17
  57. package/app/components/02.molecules/alert-content/tests/AlertContent.spec.ts +33 -0
  58. package/app/components/02.molecules/display-avatar/CONSUMER-STYLING.md +33 -15
  59. package/app/components/02.molecules/display-avatar/DisplayAvatar.vue +81 -36
  60. package/app/components/02.molecules/display-avatar/stories/DisplayAvatar.stories.ts +108 -6
  61. package/app/components/02.molecules/display-avatar/tests/DisplayAvatar.spec.ts +104 -13
  62. package/app/components/02.molecules/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap +4 -3
  63. package/app/components/02.molecules/display-chip/CONSUMER-STYLING.md +57 -19
  64. package/app/components/02.molecules/display-chip/DisplayChip.vue +72 -54
  65. package/app/components/02.molecules/display-chip/stories/DisplayChip.stories.ts +103 -6
  66. package/app/components/02.molecules/display-chip/tests/DisplayChip.spec.ts +98 -29
  67. package/app/components/02.molecules/display-chip/tests/__snapshots__/DisplayChip.spec.ts.snap +8 -4
  68. package/app/components/02.molecules/profile-section/CONSUMER-STYLING.md +103 -13
  69. package/app/components/02.molecules/profile-section/ProfileSection.vue +123 -23
  70. package/app/components/02.molecules/profile-section/stories/ProfileSection.stories.ts +353 -115
  71. package/app/components/02.molecules/profile-section/tests/ProfileSection.spec.ts +144 -11
  72. package/app/components/02.molecules/profile-section/tests/__snapshots__/ProfileSection.spec.ts.snap +6 -11
  73. package/app/components/02.molecules/prompt/CONSUMER-STYLING.md +9 -1
  74. package/app/components/02.molecules/prompt/DisplayPrompt.vue +2 -2
  75. package/app/components/02.molecules/prompt/stories/DisplayPrompt.stories.ts +208 -71
  76. package/app/components/02.molecules/prompt/tests/DisplayPrompt.spec.ts +19 -6
  77. package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +66 -8
  78. package/app/components/02.molecules/select-menu/SelectMenu.vue +141 -18
  79. package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +232 -2
  80. package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +151 -0
  81. package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +6 -2
  82. package/app/components/02.molecules/social-icons-list/CONSUMER-STYLING.md +6 -2
  83. package/app/components/02.molecules/social-icons-list/SocialIconsList.vue +1 -0
  84. package/app/components/02.molecules/toast/CONSUMER-STYLING.md +91 -27
  85. package/app/components/02.molecules/toast/DisplayToast.vue +109 -50
  86. package/app/components/02.molecules/toast/DisplayToastProvider.vue +109 -45
  87. package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +23 -0
  88. package/app/components/02.molecules/toast/stories/DisplayToastProvider.stories.ts +73 -0
  89. package/app/components/02.molecules/toast/tests/DisplayToast.spec.ts +132 -18
  90. package/app/components/02.molecules/toast/tests/DisplayToastProvider.spec.ts +105 -39
  91. package/app/components/03.organisms/google-reviews/tests/__snapshots__/GoogleReviewCard.spec.ts.snap +1 -1
  92. package/app/components/03.organisms/google-reviews/tests/__snapshots__/GoogleReviews.spec.ts.snap +6 -6
  93. package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +38 -11
  94. package/app/components/03.organisms/responsive-header/NavigationItems.vue +27 -12
  95. package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +35 -20
  96. package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +1 -1
  97. package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +1 -1
  98. package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +1 -1
  99. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +3 -3
  100. package/app/composables/useTabs.ts +148 -206
  101. package/app/types/components/display-chip.d.ts +8 -4
  102. package/app/types/components/display-toast.d.ts +2 -0
  103. package/app/types/components/index.ts +0 -1
  104. package/app/types/components/select-menu.d.ts +2 -0
  105. package/app/utils/pausableTimeout.ts +41 -0
  106. package/nuxt.config.ts +1 -1
  107. package/package.json +1 -1
  108. package/.claude/skills/components/tab-navigation.md +0 -288
  109. package/.claude/skills/components/tabs-core.md +0 -187
  110. package/.vscode/srcdev-component-tab-navigation.code-snippets +0 -30
  111. package/app/components/01.atoms/navigation/tabs/CONSUMER-STYLING.md +0 -120
  112. package/app/components/01.atoms/navigation/tabs/TabsCore.vue +0 -272
  113. package/app/components/01.atoms/navigation/tabs/stories/TabsCore.stories.ts +0 -199
  114. package/app/components/01.atoms/navigation/tabs/tests/TabsCore.spec.ts +0 -274
  115. package/app/components/02.molecules/navigation/tab-navigation/CONSUMER-STYLING.md +0 -191
  116. package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +0 -550
  117. package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts +0 -259
  118. package/app/components/02.molecules/navigation/tab-navigation/tests/TabNavigation.spec.ts +0 -379
  119. package/app/components/02.molecules/navigation/tab-navigation/tests/__snapshots__/TabNavigation.spec.ts.snap +0 -33
  120. package/app/composables/useNavCollapse.ts +0 -86
  121. package/app/types/components/nav-item.d.ts +0 -11
@@ -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.
@@ -53,6 +53,7 @@ interface DisplayToastConfig {
53
53
  title?: string // bold title line
54
54
  description?: string // smaller description line
55
55
  customIcon?: string // icon name override (e.g. "akar-icons:check-box")
56
+ dismissLabel?: string // screen-reader text for the close button — default: "Close"
56
57
  }
57
58
  }
58
59
  ```
@@ -111,6 +112,17 @@ const toastVisible = ref(false)
111
112
  - `onBeforeRouteLeave` dismisses the toast on navigation. This emits a Vue Router warning in Vitest (no active route record) — harmless.
112
113
  - `returnFocusTo` accepts an `HTMLElement` or a component instance with `$el`.
113
114
  - The toast uses `<Teleport to="body">`. In tests, query `document.querySelector(".display-toast")` not `wrapper.find()`.
115
+ - Position, alignment and state are `data-position`, `data-alignment` (`full-width` when `fullWidth`) and `data-state="show|hide"`, not classes (changed 2026-10-08: the old bare `.top`/`.hide`/... classes could collide with a consumer's utility classes, since the toast is teleported into `<body>`).
116
+ - In tests, unmount every toast in `afterEach` before clearing `document.body`: a leftover instance's pending dismiss timer otherwise re-renders into the removed container (`insertBefore` of null) during a later test's timer advance.
117
+
118
+ ### Auto-dismiss pause
119
+
120
+ Auto-dismiss pauses while hovered, or while keyboard focus is on or inside the toast, and resumes with
121
+ the remaining time (`createPausableTimeout` in `app/utils/pausableTimeout.ts`, plus
122
+ `focusPausesAutoDismiss`). The toast's own programmatic focus only pauses it when the root matches
123
+ `:focus-visible` (keyboard modality), so mouse users still get auto-dismiss. `data-paused` freezes the
124
+ progress bar via `animation-play-state`. In jsdom `:focus-visible` matches any focus, so tests that
125
+ depend on modality spy on `Element.prototype.matches` (see `DisplayToast.spec.ts`).
114
126
 
115
127
  ---
116
128
 
@@ -208,12 +220,15 @@ Stacked dismissals use a FLIP animation — remaining toasts slide smoothly to f
208
220
 
209
221
  ### CSS / styling
210
222
 
211
- Override tokens via an unscoped style block scoped to a page or layout class:
223
+ Both components teleport to `<body>`, so tokens set on a page or layout class don't reach them: set
224
+ them globally. Own tokens: `--display-toast(-provider)-max-width` (default `min(48rem, 100% - 2 * gutter)`),
225
+ `-progress-colour`, `-focus-ring-colour`, `-z-index`. Card styling comes from `--alert-content-*`.
226
+ Full list in `CONSUMER-STYLING.md`.
212
227
 
213
228
  ```css
214
- .my-page .display-toast-provider-item {
215
- --theme-surface: oklch(15% 0 0);
216
- --theme-text: oklch(95% 0 0);
229
+ :where(html) {
230
+ --display-toast-provider-max-width: min(36rem, 100% - 48px);
231
+ --display-toast-provider-progress-colour: var(--brand-accent);
217
232
  }
218
233
  ```
219
234
 
@@ -234,5 +249,7 @@ The inner content layout is handled by the shared `AlertContentInner` molecule
234
249
  - `useToastQueue` state persists across route changes — no `onBeforeRouteLeave` cleanup needed.
235
250
  - `crypto.randomUUID()` generates toast IDs (`toast-<uuid>`). IDs are returned by `show()` for targeted `dismiss(id)` calls.
236
251
  - In tests, query via `document.querySelector(".display-toast-provider-item")` — the Teleport renders outside the component wrapper.
252
+ - Every mounted provider reacts to the shared `useToastQueue` state, so tests must unmount providers in `afterEach`; otherwise an earlier test's provider runs its own timer for a new toast and dismisses it.
253
+ - Each item pauses independently (hover / keyboard focus, same rule as the standalone toast), tracked per toast id.
237
254
  - The progress bar (`.display-toast-provider-progress`) is only rendered when `autoDismiss: true`.
238
255
  - Dismissal can be triggered by: close button click, Escape key (when the toast item has focus), `dismiss(id)`, auto-dismiss timer, or `clear()`.
@@ -17,7 +17,7 @@ a configurable root element via the `tag` prop.
17
17
  | Prop | Type | Default | Description |
18
18
  |------|------|---------|-------------|
19
19
  | `tag` | `"div" \| "p" \| "span" \| "section" \| "article" \| "aside" \| "header" \| "footer" \| "main" \| "nav" \| "ul" \| "ol"` | `"div"` | Root element tag. |
20
- | `variant` | `"subtle" \| "vivid" \| "silver" \| "steel" \| "green"` | `"subtle"` | Glow colour variant. |
20
+ | `variant` | `"subtle" \| "vivid" \| "silver" \| "steel" \| "green"` | `"subtle"` | Glow colour variant, rendered as `data-variant` on the root (not a class). |
21
21
  | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
22
22
 
23
23
  ## Slots
@@ -36,7 +36,7 @@ a configurable root element via the `tag` prop.
36
36
 
37
37
  ## Styling
38
38
 
39
- Border width, radius, surface colour, animation duration, and every variant's 5 gradient colour
39
+ Border width, radius, surface colour (defaults to `--theme-surface-subtle`), text colour (`--theme-text`), animation duration, and every variant's 5 gradient colour
40
40
  stops are public CSS custom properties — see `CONSUMER-STYLING.md` for the full token table.
41
41
 
42
42
  ```vue
@@ -48,6 +48,14 @@ stops are public CSS custom properties — see `CONSUMER-STYLING.md` for the ful
48
48
  </GlowingBorder>
49
49
  ```
50
50
 
51
+ ## Highlighting a single control
52
+
53
+ Wrap just an `InputButton` or `InputTextCore` so the glow becomes the control's border (a design
54
+ call-out for something "special"). Set `--glowing-border-radius` to the control's radius plus the
55
+ glow width, `--glowing-border-surface: transparent`, `--glowing-border-overflow: visible` (or the
56
+ control's focus ring is clipped), shrink-to-fit display, and `--input-text-border: transparent` for
57
+ the input. Full recipe in `CONSUMER-STYLING.md`; `HighlightedControls` story.
58
+
51
59
  ## Motion
52
60
 
53
61
  The glow animation stops automatically under `prefers-reduced-motion: reduce`.
@@ -64,3 +72,13 @@ The glow animation stops automatically under `prefers-reduced-motion: reduce`.
64
72
  override surface.
65
73
  - 2026-09-20: added `green` variant (vibrant, saturated greens: `#00ff87`, `#39ff14`, `#00c853`,
66
74
  `#64dd17`, `#00e676`), each stop overridable via `--glowing-border-green-color-{1-5}`.
75
+ - 2026-10-08 stress-test pass (`StressTest` story): the variant moved from a bare root class
76
+ (`.vivid`, `.green`...) to `data-variant`, since those names collide with common consumer
77
+ classes; `@keyframes glow` renamed to `glowing-border-rotate` for the same reason. Added
78
+ `overflow-wrap: anywhere` so long unbroken text wraps rather than being clipped by
79
+ `overflow: hidden`. `--glowing-border-surface` default moved from `canvas` to
80
+ `--theme-surface-subtle`, and a new `--glowing-border-text-colour` token defaults to
81
+ `--theme-text`. Stories gained story-only canvas and surface background controls with dark
82
+ values; the hardcoded dark stage was removed.
83
+ - 2026-10-08: added `--glowing-border-overflow` (default `hidden`) so a wrapped control's hover/focus
84
+ outlines can show, plus the `HighlightedControls` story and recipe.
@@ -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
 
@@ -1,24 +1,26 @@
1
1
  ---
2
2
  name: ProfileSection
3
- description: ProfileSection molecule — props, dynamic profile-info slots, eyebrowText/heroText/profileLinks slots, layout, accessibility
3
+ description: ProfileSection molecule — props, dynamic profile-info slots, eyebrowText/heroText/profileLinks slots, container-query layout, accessibility
4
4
  type: reference
5
5
  ---
6
6
 
7
7
  # ProfileSection
8
8
 
9
+ > **Changed 2026-10-14 (breaking):** New `headerPlacement` prop (`above`, `picture-bottom`, `picture-top`) can put the header over the bottom or top of the picture. `profileInfoCount` removed — only the `profile-info-N` slots you give are rendered (it used to pad with empty blocks when none were given). The two-column switch is a container query on the component's own width, not a viewport `@media` query, and both columns are `minmax(0, …)`. The `<header>` is only rendered when a header slot is given. Highlight colours go through new `--profile-section-*` tokens. An invalid `profilePicture.width`/`height` (0, negative, `NaN`) falls back to 828×1104.
10
+ >
9
11
  > **Classes renamed 2026-09-27:** `.picture` → `.profile-section-picture`, `.profile-picture` → `.profile-section-image`, `.profile-info` → `.profile-section-info`, `.profile-info-content` → `.profile-section-info-content`, `.profile-info-block` → `.profile-section-info-block`, `.profile-links` → `.profile-section-links`.
10
12
 
11
13
  ## Overview
12
14
 
13
- `ProfileSection` is a molecule that renders a practitioner/author profile: a header area (eyebrow + heading), a profile picture, and a flexible set of bio/info blocks alongside optional profile links. It is landmark-aware — when `tag` is `section`, `article`, or `aside`, the root element automatically gets `aria-labelledby` wired to the heading inside the `#heroText` slot (**you must bind `headingId` yourself, and a console warning fires if you forget** — see [component-aria-landmark.md](../component-aria-landmark.md)). `tag="main"` is never auto-labelled.
15
+ `ProfileSection` is a molecule that renders a practitioner/author profile: a header area (eyebrow + heading), a profile picture, and a flexible set of bio/info blocks alongside optional profile links. It is landmark-aware — when `tag` is `section` or `article`, the root element automatically gets `aria-labelledby` wired to the heading inside the `#heroText` slot (**you must bind `headingId` yourself, and a console warning fires if you forget** — see [component-aria-landmark.md](../component-aria-landmark.md)). `tag="main"` is never auto-labelled.
14
16
 
15
17
  ## Props
16
18
 
17
19
  | Prop | Type | Default | Description |
18
20
  |------|------|---------|-------------|
19
- | `profilePicture` | `ProfilePicture` (`{ src; alt; width?; height? }`, exported from the layer types) | — | **Required.** Rendered via `NuxtImg` with `width`/`height` defaulting to `828`/`1104` (3:4, a Vercel-allowed width). |
21
+ | `profilePicture` | `ProfilePicture` (`{ src; alt; width?; height? }`, exported from the layer types) | — | **Required.** Rendered via `NuxtImg`. `width`/`height` default to `828`/`1104` (3:4, a Vercel-allowed width), and a missing or invalid value (0, negative, `NaN`, `Infinity`) uses the default. |
20
22
  | `tag` | `"div" \| "section" \| "article" \| "main"` | `"div"` | HTML element rendered as the root. |
21
- | `profileInfoCount` | `number` | `3` | Number of `profile-info-N` slots to generate when none are explicitly provided. |
23
+ | `headerPlacement` | `"above" \| "picture-bottom" \| "picture-top"` | `"above"` | Where the `#eyebrowText`/`#heroText` header goes: above the grid, or over the bottom or top of the picture on a scrim. Template form: `header-placement`. |
22
24
  | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
23
25
 
24
26
  ## Slots
@@ -28,17 +30,19 @@ type: reference
28
30
  | `#eyebrowText` | Optional eyebrow label above the heading. Use `EyebrowText`. |
29
31
  | `#heroText` | Heading slot. Receives `headingId` as a slot prop — bind it to the heading's `id` for accessible `aria-labelledby`. |
30
32
  | `#profile-info-1` … `#profile-info-N` | Bio / info blocks. Provide as many numbered slots as needed. They are rendered in ascending numeric order. |
31
- | `#profileLinks` | Links / actions shown at the bottom-right of the info column (e.g. social icons, booking CTA). |
33
+ | `#profileLinks` | Links / actions shown at the bottom-right of the info column (e.g. social icons, booking CTA). Wraps onto more lines when there are many. |
34
+
35
+ The `<header class="profile-section-header">` is only rendered when `#eyebrowText` or `#heroText` is given. It renders above the grid, or inside the picture when `header-placement` is `picture-bottom` or `picture-top`.
32
36
 
33
37
  ## Basic usage
34
38
 
35
39
  ```vue
36
40
  <ProfileSection
37
41
  tag="section"
38
- :profile-picture="{ src: '/images/profile/mel.jpg', alt: 'Mel Stafford, reflexologist' }"
42
+ :profile-picture="{ src: '/images/profile/alex.jpg', alt: 'Alex Morgan, hair stylist' }"
39
43
  >
40
44
  <template #eyebrowText>
41
- <EyebrowText font-size="large" text="About Mel" />
45
+ <EyebrowText font-size="large" text-content="About Alex" />
42
46
  </template>
43
47
 
44
48
  <template #heroText="{ headingId }">
@@ -47,8 +51,8 @@ type: reference
47
51
  :id="headingId"
48
52
  font-size="title"
49
53
  :text-content="[
50
- { text: 'Mel Stafford', styleClass: 'normal' },
51
- { text: 'Reflexologist', styleClass: 'accent' },
54
+ { text: 'Alex Morgan', styleClass: 'normal' },
55
+ { text: 'Hair stylist', styleClass: 'accent' },
52
56
  ]"
53
57
  />
54
58
  </template>
@@ -58,7 +62,7 @@ type: reference
58
62
  </template>
59
63
 
60
64
  <template #profile-info-2>
61
- <p>Mel is based in Lichfield…</p>
65
+ <p class="location">Based in <span class="highlight">Northtown</span>…</p>
62
66
  </template>
63
67
 
64
68
  <template #profileLinks>
@@ -69,11 +73,12 @@ type: reference
69
73
 
70
74
  ## Dynamic profile-info slots
71
75
 
72
- The `profile-info-N` slots are discovered at runtime by filtering `useSlots()` for keys matching `/^profile-info-\d+$/`. The slots are rendered in ascending numeric order.
76
+ The `profile-info-N` slots are discovered at runtime by filtering `useSlots()` for keys matching `/^profile-info-\d+$/`, and rendered in ascending numeric order.
73
77
 
74
78
  - If you provide `#profile-info-1`, `#profile-info-2`, `#profile-info-3` — all three render in order.
75
- - If you provide no `profile-info-*` slots, the component renders `profileInfoCount` empty blocks (they used to contain visible placeholder text, removed 2026-09-27). Slot discovery runs every render, so slots added after mount appear.
76
- - Gaps are supported — you can provide `#profile-info-1` and `#profile-info-3` without `#profile-info-2`; they sort correctly.
79
+ - If you provide none, no info blocks render.
80
+ - Gaps are supported — you can provide `#profile-info-1` and `#profile-info-3` without `#profile-info-2`; `#profile-info-12` sorts after `#profile-info-2`.
81
+ - Discovery runs every render, so slots added or removed after mount (a `v-if` on the slot template) appear and disappear.
77
82
 
78
83
  ## heroText slot prop
79
84
 
@@ -89,10 +94,41 @@ If you use a different heading component, bind `headingId` to whatever prop rend
89
94
 
90
95
  ## Layout
91
96
 
92
- - **Mobile**: single column — picture stacked above info.
93
- - **768px+**: two columns — `--profile-section-picture-width` (`384px`) picture column, `1fr` info column, `--profile-section-gap-wide` (`4rem`) gap (`--profile-section-gap`, `2rem`, below 768px).
94
- - Picture frame (`.profile-section-picture`): `--profile-section-picture-aspect-ratio` (`3 / 4`), `--profile-section-picture-border-radius` (`8px`), `overflow: hidden`. Full token list: `CONSUMER-STYLING.md` in the component folder. The `NuxtImg` fills the frame with `object-fit: cover`.
95
- - Profile links: `align-items: end; justify-content: flex-end` — right-aligned to the bottom of the info column.
97
+ - The root is a size container named `profile-section`. Layout follows **the component's own width**, not the viewport.
98
+ - **Under 768px wide**: single column — picture stacked above info, `--profile-section-gap` (`2rem`).
99
+ - **768px wide or more**: two columns — `minmax(0, var(--profile-section-picture-width))` (`38.4rem`) picture column, `minmax(0, 1fr)` info column, `--profile-section-gap-wide` (`4rem`) gap.
100
+ - Being an inline-size container, the root can't size itself from its content: give it a width in a flex row or other shrink-to-fit context.
101
+ - The root sets `overflow-wrap: anywhere`, so unbroken text and URLs in any slot wrap rather than widening the grid.
102
+ - Picture frame (`.profile-section-picture`): `--profile-section-picture-aspect-ratio` (`3 / 4`), `--profile-section-picture-border-radius` (`0.8rem`), `overflow: hidden`. The `NuxtImg` fills the frame with `object-fit: cover`. Full token list: `CONSUMER-STYLING.md` in the component folder.
103
+ - Profile links: `flex-wrap: wrap; align-items: end; justify-content: flex-end` — right-aligned to the bottom of the info column.
104
+ - To change the picture column width, set `--profile-section-picture-width`; don't override `grid-template-columns` in a viewport `@media` query, which no longer lines up with the component's own switch.
105
+
106
+ ## Header in picture
107
+
108
+ With `header-placement="picture-bottom"` or `"picture-top"`, the `<header>` renders inside `.profile-section-picture`, before the image in the DOM (so it is read first), and sits at the bottom or top of the frame on a scrim that fades away from that edge. It follows ServicesCard's `titles-within-image-wrapper`:
109
+
110
+ - Scrim and text come from `--profile-section-picture-header-*` tokens: scrim colour (`#000`) and opacity (`75%`), a solid stop (`0%`, how far the scrim stays full strength before fading) and a target colour (`transparent`, what it fades into, set it to roughly the photo's colour where the fade ends so it blends instead of leaving a grey band), text colour (`contrast-color()` of the scrim colour where supported, else `#fff`), eyebrow colour, padding (`2.4rem`), gap (`0.4rem`). Full list in `CONSUMER-STYLING.md`.
111
+ - The frame drops `overflow: hidden` in this mode, so its aspect ratio is a minimum: a long header grows the frame rather than being clipped. The image is absolutely positioned and still fills the frame.
112
+ - `aria-labelledby` still points at the heading.
113
+ - Drop spacing utilities like `mb-20` from the heading in this mode; the header's padding handles spacing.
114
+
115
+ ```vue
116
+ <ProfileSection tag="section" header-placement="picture-bottom" :profile-picture="picture">
117
+ <template #eyebrowText>
118
+ <EyebrowText tag="p" font-size="medium" text-content="About Alex" />
119
+ </template>
120
+ <template #heroText="{ headingId }">
121
+ <HeroText :id="headingId" tag="h2" font-size="heading" :text-content="[{ text: 'Alex Morgan', styleClass: 'normal' }]" />
122
+ </template>
123
+ ...
124
+ </ProfileSection>
125
+ ```
126
+
127
+ Two stories show themes built only from tokens: `Header In Picture (Dark, Bottom)`, ServicesCard-style, and `Header In Picture (Light, Top)`. A light scrim needs a dark `--profile-section-picture-header-text-colour`; `contrast-color()` picks one where supported, but set it for older browsers. `--profile-section-info-text-colour` sets the info column's text colour for a themed background.
128
+
129
+ ## Content hooks
130
+
131
+ A `.highlight` inside an element with class `location` gets `--profile-section-highlight-colour` (default `var(--colour-text-accent)`); inside `services` it gets `--profile-section-link-colour` / `--profile-section-link-colour-hover` (defaults `var(--colour-link-default)` / `var(--colour-link-hover)`). Both use `--profile-section-highlight-font-weight` (`600`). The `location`/`services` class can sit on the `<p>` itself or a wrapper.
96
132
 
97
133
  ## CSS notes
98
134
 
@@ -106,8 +142,6 @@ If you use a different heading component, bind `headingId` to whatever prop rend
106
142
  }
107
143
  ```
108
144
 
109
- - Accent-coloured text within `.location` or `.services` info blocks can use the `.highlight` class for `var(--colour-text-accent)` or `var(--colour-link-default)` colouring respectively.
110
-
111
145
  ## Consumer styling scaffold
112
146
 
113
147
  ```vue
@@ -121,6 +155,8 @@ If you use a different heading component, bind `headingId` to whatever prop rend
121
155
  <style>
122
156
  .profile-section {
123
157
  &.my-profile {
158
+ --profile-section-picture-width: 32rem;
159
+
124
160
  .profile-section-header {
125
161
  margin-block-end: 3.2rem;
126
162
  }
@@ -131,6 +167,8 @@ If you use a different heading component, bind `headingId` to whatever prop rend
131
167
 
132
168
  ## Notes
133
169
 
134
- - `NuxtImg` is used for the profile picture — provide a real `src` path. A placeholder (grey box) is shown if the image 404s.
135
- - The component does not expose `imgWidth` / `imgHeight` props for the picture. If IPX optimisation is critical, use a fixed-dimension image and rely on the `384px` column cap as the natural size constraint.
170
+ - `NuxtImg` is used for the profile picture — provide a real `src` path. A broken image shows its alt text inside the 3:4 frame.
171
+ - Pass `width`/`height` in `profilePicture` when your image isn't 3:4 at 828px wide, so IPX/Vercel request the right size.
172
+ - The `With Social Icons` story shows a `SocialIconsList` in `#profileLinks`; size it with `--social-icons-list-icon-size` and `--social-icons-list-gap` on your own wrapper.
173
+ - The `StressTest` story covers hostile content (broken image, unbroken text, RTL, out-of-order slots, many links) at every canvas width.
136
174
  - Component is auto-imported in Nuxt — no import needed.
@@ -132,6 +132,9 @@ Route links and anchor links can be mixed in the same data. See the "Same-Page A
132
132
 
133
133
  ## Migrating from TabNavigation
134
134
 
135
+ > `TabNavigation` was deprecated 2026-10-06 and **removed 2026-10-07**, along with its `NavItem` /
136
+ > `NavItemData` types and the `useNavCollapse` composable. Type the new data as `ResponsiveHeaderProp`.
137
+
135
138
  `ResponsiveHeader` covers everything `TabNavigation` does, and collapses progressively (only the items
136
139
  that don't fit move into the overflow menu) instead of all-or-nothing. To switch:
137
140
 
@@ -149,8 +152,12 @@ that don't fit move into the overflow menu) instead of all-or-nothing. To switch
149
152
  | `anchor-scroll-offset` | `anchor-scroll-offset` |
150
153
  | collapse everything to a burger when items don't fit | default is per-item overflow; add `collapse-at-main-nav-intersection` for the old all-or-nothing burger |
151
154
  | `--tab-nav-link-color` | `--responsive-header-link-color` |
155
+ | `--tab-nav-link-hover-color` / `--tab-nav-link-active-color` | `--responsive-header-link-color-hover` / `-active` (added 2026-10-07) |
156
+ | `--tab-nav-decorator-hovered-bg` | `--responsive-nav-decorator-hovered-bg` |
152
157
  | `--tab-nav-decorator-indicator-color` | `--responsive-nav-decorator-indicator-color` |
153
- | `--tab-nav-*` panel and burger tokens | the overflow tokens in `CONSUMER-STYLING.md` and NavigationItems' `--overflow-nav-*` |
158
+ | `--tab-nav-panel-link-color` / `-hover-color` / `-active-color` | `--overflow-nav-link-color` / `-color-hover` / `-color-active` |
159
+ | `--tab-nav-panel-bg` / `--tab-nav-panel-border-color` / `--tab-nav-panel-item-border` | `--responsive-header-overflow-nav-bg` / `--responsive-header-overflow-nav-border` (full shorthand) / `--overflow-nav-link-border-color` |
160
+ | `--tab-nav-burger-color` | `--responsive-header-overflow-btn-icon-color` |
154
161
 
155
162
  ```vue
156
163
  <!-- Before -->
@@ -203,9 +210,12 @@ All tokens are read via `var(--token, default)` — see the full list in the com
203
210
  |---|---|---|
204
211
  | `--responsive-header-link-font-size` | `inherit` | Nav-link font-size. **Set this to a fixed value** — see the measurement-pipeline note above for why leaving it `inherit` from a fluid ancestor is a footgun. |
205
212
  | `--responsive-header-link-color` | `inherit` | Link/summary text colour. |
213
+ | `--responsive-header-link-color-hover` / `-active` | the resting colour | Per-state link colour in the top bar (added 2026-10-07). Hover falls back to the item's current state. `--overflow-nav-link-color-hover` / `-active` do the same in the overflow panel. |
206
214
  | `--responsive-header-bg` / `--responsive-header-padding-*` / `--responsive-header-border*` | transparent / `0` / `none` | Root element theming. |
207
215
  | `--responsive-header-overflow-btn-*` | various | Overflow burger button sizing/colour. |
208
216
  | `--responsive-header-sub-nav-*` / `--responsive-header-overflow-nav-*` | various | Top-bar dropdown panel and overflow-panel container theming. |
217
+
218
+ Defaults are for a **light** header (changed 2026-10-07): panels use `--page-bg`, borders and hover tints are `color-mix()`es of `--theme-text`, the overflow button is transparent with a `--theme-border-focus` hover outline. They used to assume a dark bar (`Canvas`, translucent white borders and tints). A dark header sets the tokens.
209
219
  | `--responsive-nav-decorator-indicator-color` / `--responsive-nav-decorator-hovered-*` | `currentColor` / inherits | The sliding active/hover indicator bar under the main nav. |
210
220
 
211
221
  ---
@@ -21,14 +21,18 @@ open between picks.
21
21
 
22
22
  | Prop | Type | Default | Notes |
23
23
  |---|---|---|---|
24
- | `options` | `SelectMenuOption[]` | — | **Required.** `{ value: string \| number; label: string; icon?: string }[]`. |
24
+ | `options` | `SelectMenuOption[]` | — | **Required.** `{ value: string \| number; label: string; icon?: string; dotColor?: string }[]`. `dotColor` (any CSS colour) colours the trigger's status dot while that option is selected (`indicator="dot"`, single-select) and shows a matching dot beside the option in the list when it has no `icon`. |
25
25
  | `label` | `string` | — | **Required.** Accessible name for the trigger + listbox (`aria-label`). Also the trigger's fallback text when nothing is selected and no `placeholder` is set — this doubles as a visible category tag, e.g. `"Choose a service"`. |
26
26
  | `placeholder` | `string` | `undefined` | Trigger text shown when nothing is selected. Falls back to `label` when omitted. |
27
- | `showIcon` | `boolean` | `true` | Show the selected option's icon in the trigger. |
27
+ | `showIcon` | `boolean` | `true` | Show the leading icon in the trigger: the selected option's icon (single-select), otherwise `triggerIcon`. |
28
+ | `triggerIcon` | `string` | `undefined` | Fixed leading icon, e.g. a filter category icon. Shown in both modes; in single-select a selected option's own icon replaces it. |
29
+ | `indicator` | `"none" \| "count" \| "dot"` | `"none"` | Marker after the trigger text, visible only while something is selected; otherwise kept in the layout with `visibility: hidden` (`.select-menu-trigger-indicator-empty`) so the trigger width doesn't jump. `count` = badge with the number selected (for `multiple`); `dot` = status dot (for single-select filters, flags the filter as active). `aria-hidden`. |
30
+ | `selectedCountLabel` | `string` | `"{count} selected"` | Appended to the trigger's `aria-label` while the indicator shows (`"Category, 3 selected"`). `{count}` is replaced. Override for translation. |
28
31
  | `showLabel` | `boolean` | `true` | Show the selected option's label (or placeholder/label fallback) text in the trigger. Set `false` for an icon-only compact trigger. |
29
32
  | `showChevron` | `boolean` | `true` | Show the trailing chevron in the trigger. |
30
- | `multiple` | `boolean` | `false` | Allow selecting more than one option. Each option gets a checkbox indicator, `v-model` becomes an array, and picking an option leaves the popover open so more can be toggled. Trigger icon is not shown when `true` (no single option to represent). |
33
+ | `multiple` | `boolean` | `false` | Allow selecting more than one option. Each option gets a checkbox indicator, `v-model` becomes an array, and picking an option leaves the popover open so more can be toggled. No option icon in the trigger when `true` (no single option to represent); `triggerIcon` still shows. |
31
34
  | `showSelectionInTrigger` | `boolean` | `false` | Multiple mode only. When `true`, the trigger text updates to a comma-separated list of the currently checked options instead of staying fixed on `placeholder`/`label`. No effect outside `multiple`. |
35
+ | `reserveLabelWidth` | `boolean` | `false` | Single-select only. Renders hidden copies of the placeholder and every option label in the same grid cell as the visible text, so the trigger is always as wide as the longest one and doesn't resize when the selection changes. Off by default, since sometimes a trigger that hugs its text is wanted. No effect with `multiple`. |
32
36
  | `inputVariant` | `InputUiVariant` | `"normal"` | Trigger border style, same prop as `InputSelect`: `"normal"` (bordered box) or `"underlined"` (bottom border only, square corners), added as a class on the root. `"outlined"` has no CSS, same as the `05.forms` inputs. |
33
37
  | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes on the root `<div>`. |
34
38
 
@@ -132,6 +136,39 @@ Set `showSelectionInTrigger` to opt out of that and have the trigger update inst
132
136
 
133
137
  ---
134
138
 
139
+ ## Filter trigger: icon + count or status dot
140
+
141
+ ```vue
142
+ <!-- Multi-select category filter: list icon, "Category", badge "3", chevron -->
143
+ <SelectMenu
144
+ v-model="categories"
145
+ :options="categoryOptions"
146
+ label="Category"
147
+ multiple
148
+ trigger-icon="lucide:list"
149
+ indicator="count"
150
+ />
151
+
152
+ <!-- Single-select status filter: dot appears once a status is picked, in that option's dotColor
153
+ statusOptions = [{ value: "active", label: "Active", dotColor: "var(--status-success)" }, ...] -->
154
+ <SelectMenu
155
+ v-model="status"
156
+ :options="statusOptions"
157
+ label="Status"
158
+ trigger-icon="lucide:circle-dot"
159
+ indicator="dot"
160
+ />
161
+ ```
162
+
163
+ The indicator is only visible while something is selected, so an unfiltered trigger looks plain,
164
+ but its space is always reserved so the trigger doesn't resize as the filter is set and cleared.
165
+ Per-option `dotColor` beats `--select-menu-trigger-dot-color`, which is the fallback (and the only
166
+ colour in `multiple` mode).
167
+ Badge/dot tokens (`--select-menu-trigger-count-*`, `--select-menu-trigger-dot-*`) are in
168
+ `CONSUMER-STYLING.md`.
169
+
170
+ ---
171
+
135
172
  ## Filter bar (multiple categories)
136
173
 
137
174
  `SelectMenu` is single-select per instance — a faceted filter bar is built by placing one instance
@@ -167,6 +204,13 @@ Quick reference:
167
204
  }
168
205
  ```
169
206
 
207
+ ### Long content
208
+
209
+ The root caps at its container width, so a long trigger label truncates with an ellipsis. The
210
+ popover caps at `--select-menu-popover-max-width` (default `calc(100vw - 3.2rem)`) and option labels
211
+ wrap; `--select-menu-item-label-line-clamp` (default `none`) caps them, `1` for single-line
212
+ ellipsis. The `StressTest` story covers this at every canvas width.
213
+
170
214
  ---
171
215
 
172
216
  ## Notes
@@ -188,7 +232,8 @@ Quick reference:
188
232
  drop the whole rule.
189
233
  Confirmed broken on a Safari 16 iPad before this (trigger rendered, menu never opened).
190
234
  - **Left-aligned popover** (`left: anchor(left)`) — unlike `ActionMenu` which right-aligns. Matches
191
- native `<select>` dropdown behaviour. Flips above the trigger near the bottom of the viewport.
235
+ native `<select>` dropdown behaviour. Flips above the trigger near the bottom of the viewport,
236
+ and to right-aligned when it would overflow the right edge (anchor-positioning browsers only).
192
237
  - **Keyboard navigation** follows the WAI-ARIA listbox pattern: `ArrowDown`/`ArrowUp` move between
193
238
  options (wraps around), `Home`/`End` jump to first/last, `Enter`/`Space` select the focused
194
239
  option, `Tab` closes without stealing focus back to the trigger (matches `ActionMenu`'s
@@ -209,7 +254,7 @@ Quick reference:
209
254
  weight doesn't resize the popover. Hover/focus has `-text-color-hover` and `-border-hover`, which
210
255
  fall back to the row's *current* state (selected or resting), not always to resting. There's no
211
256
  hover font-weight token on purpose: the width reserve only covers one alternate weight.
212
- - **Chevron rotation** keys off the trigger's `aria-expanded` (from the composable's `isOpen`), not
257
+ - **Chevron flip** (`scaleY(-1)`, was `rotate(180deg)` until 2026-10-07) keys off the trigger's `aria-expanded` (from the composable's `isOpen`), not
213
258
  `:has(...:popover-open)`, so it also works without the Popover API (changed 2026-10-05).
214
259
  - **No native `<select>`/`appearance: base-select` involved** — this is a from-scratch popover
215
260
  listbox, not a styled native select, so it doesn't inherit `InputSelectCore`'s WebKit
@@ -217,3 +262,7 @@ Quick reference:
217
262
  - **Not a form field** — `SelectMenu` has no `fieldHasError`/`theme`/`inputVariant` props like the
218
263
  `05.forms` inputs. Use `InputSelectCore` instead for an actual form field that needs validation
219
264
  state and native `<select>` semantics.
265
+ - **2026-10-08 stress-test pass**: root `max-inline-size: 100%` (a long trigger label overflowed
266
+ narrow containers instead of truncating), popover `--select-menu-popover-max-width` and
267
+ `flip-inline` fallbacks (a long option made the menu wider than the viewport), option labels wrap
268
+ with a `--select-menu-item-label-line-clamp` token instead of `nowrap`.
@@ -99,6 +99,6 @@ const responsiveNavLinks = {
99
99
 
100
100
  ## Migration notes
101
101
 
102
- - Consuming layouts previously assembling `PageRow` + `SkipLinks` + `ResponsiveHeader` (or `TabNavigation`) by hand can switch to `SiteHeader` directly; the two `styleClassPassthrough` props map 1:1 onto the two independent passthrough hooks that pattern used.
103
- - `TabNavigation`'s `NavItemData` shape (`{ main: [{ text, href, iconName, cssName }] }`) is **not** the same shape as `ResponsiveHeaderProp` (`{ groupKey: [{ name, path, childLinksTitle, childLinks }] }`) — migrating a `TabNavigation` consumer means reshaping the nav data, not just swapping the component. `TabNavigation`'s `navAlign` prop has no `SiteHeader` equivalent since `ResponsiveHeader`'s layout is fixed (branding column + nav column).
102
+ - Consuming layouts previously assembling `PageRow` + `SkipLinks` + `ResponsiveHeader` (or the removed `TabNavigation`) by hand can switch to `SiteHeader` directly; the two `styleClassPassthrough` props map 1:1 onto the two independent passthrough hooks that pattern used.
103
+ - `TabNavigation` (removed 2026-10-07) took `NavItemData` (`{ main: [{ text, href, iconName, cssName }] }`, also removed), which is **not** the same shape as `ResponsiveHeaderProp` (`{ groupKey: [{ name, path, childLinksTitle, childLinks }] }`): migrating means reshaping the nav data, not just swapping the component. See "Migrating from TabNavigation" in `responsive-header.md`. Its `navAlign` prop has no `SiteHeader` equivalent since `ResponsiveHeader`'s layout is fixed (branding column + nav column).
104
104
  - Requires the consuming layout's main/footer regions to carry `id="main-content"` / `id="footer-content"` — same requirement as using `SkipLinks` directly.
@@ -57,6 +57,8 @@ import type { ISocialIcon } from "srcdev-nuxt-components";
57
57
  | YouTube | `logos:youtube-icon` | `https://www.youtube.com/@` |
58
58
  | TikTok | `logos:tiktok-icon` | `https://www.tiktok.com/@` |
59
59
 
60
+ `logos:instagram-icon`, `logos:x` and other dark brand icons vanish on a dark background, and their colours can't be changed. On a dark background use a mono icon such as `mdi:instagram` (any set that draws in `currentColor`) and colour it with `--social-icons-list-icon-colour` (default `currentColor`). Installed mono Instagram icons: `mdi:instagram`, `bi:instagram`, `lucide:instagram`, `akar-icons:instagram-fill`, `radix-icons:instagram-logo`.
61
+
60
62
  ---
61
63
 
62
64
  ## Usage example