srcdev-nuxt-components 9.5.4 → 9.5.6

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 (55) hide show
  1. package/.claude/skills/components/action-menu.md +21 -15
  2. package/.claude/skills/components/deep-expanding-menu.md +18 -6
  3. package/.claude/skills/components/display-tooltip-defined.md +2 -1
  4. package/.claude/skills/components/display-tooltip.md +10 -4
  5. package/.claude/skills/components/pop-over.md +11 -8
  6. package/.claude/skills/components/select-menu.md +28 -8
  7. package/.claude/skills/components/triple-toggle-switch.md +7 -1
  8. package/.claude/skills/components/ui-block-decorated.md +7 -0
  9. package/.claude/skills/composable-anchored-popover.md +108 -0
  10. package/.claude/skills/composable-tooltips-guide.md +3 -1
  11. package/.claude/skills/index.md +1 -0
  12. package/app/assets/styles/setup/06.utility-classes/animations/_animation-scroller-x.css +5 -3
  13. package/app/assets/styles/setup/06.utility-classes/animations/_auto-rotate.css +5 -3
  14. package/app/assets/styles/setup/06.utility-classes/animations/_entry-exit-blur.css +5 -3
  15. package/app/assets/styles/setup/06.utility-classes/animations/_entry-slide-in.css +5 -3
  16. package/app/assets/styles/setup/06.utility-classes/animations/_entry-zoom-reveal.css +5 -3
  17. package/app/components/01.atoms/animations/entry/CONSUMER-STYLING.md +6 -2
  18. package/app/components/01.atoms/display-tooltip/CONSUMER-STYLING.md +15 -3
  19. package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +62 -3
  20. package/app/components/01.atoms/display-tooltip/tests/DisplayTooltip.spec.ts +36 -1
  21. package/app/components/01.atoms/display-tooltip/tests/__snapshots__/DisplayTooltip.spec.ts.snap +1 -1
  22. package/app/components/01.atoms/navigation/skip-links/SkipLinks.vue +1 -1
  23. package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +9 -3
  24. package/app/components/01.atoms/pop-over/PopOver.vue +70 -9
  25. package/app/components/01.atoms/pop-over/tests/PopOver.spec.ts +53 -0
  26. package/app/components/01.atoms/pop-over/tests/__snapshots__/PopOver.spec.ts.snap +1 -1
  27. package/app/components/01.atoms/ui-block-decorated/CONSUMER-STYLING.md +12 -4
  28. package/app/components/01.atoms/ui-block-decorated/UiBlockDecorated.vue +20 -16
  29. package/app/components/01.atoms/ui-block-decorated/stories/UiBlockDecorated.stories.ts +124 -96
  30. package/app/components/02.molecules/action-menu/ActionMenu.vue +41 -16
  31. package/app/components/02.molecules/action-menu/CONSUMER-STYLING.md +16 -3
  32. package/app/components/02.molecules/action-menu/tests/ActionMenu.spec.ts +28 -0
  33. package/app/components/02.molecules/action-menu/tests/__snapshots__/ActionMenu.spec.ts.snap +3 -3
  34. package/app/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue +2 -1
  35. package/app/components/02.molecules/display-tooltip-defined/stories/DisplayTooltipDefined.stories.ts +7 -4
  36. package/app/components/02.molecules/display-tooltip-defined/tests/DisplayTooltipDefined.spec.ts +20 -1
  37. package/app/components/02.molecules/display-tooltip-defined/tests/__snapshots__/DisplayTooltipDefined.spec.ts.snap +1 -1
  38. package/app/components/02.molecules/navigation/deep-expanding-menu/CONSUMER-STYLING.md +18 -4
  39. package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +87 -16
  40. package/app/components/02.molecules/navigation/deep-expanding-menu/tests/DeepExpandingMenu.spec.ts +83 -1
  41. package/app/components/02.molecules/navigation/deep-expanding-menu/tests/__snapshots__/DeepExpandingMenu.spec.ts.snap +2 -2
  42. package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +1 -1
  43. package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +1 -1
  44. package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +26 -6
  45. package/app/components/02.molecules/select-menu/SelectMenu.vue +90 -18
  46. package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +36 -0
  47. package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +9 -9
  48. package/app/components/03.organisms/responsive-header/NavigationItems.vue +2 -2
  49. package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +4 -4
  50. package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitch.vue +11 -3
  51. package/app/composables/tests/useAnchoredPopover.spec.ts +231 -0
  52. package/app/composables/tests/useTooltips.spec.ts +69 -0
  53. package/app/composables/useAnchoredPopover.ts +183 -0
  54. package/app/composables/useTooltips.ts +37 -20
  55. package/package.json +1 -1
@@ -8,7 +8,7 @@
8
8
  Clicking it opens an anchored menu list populated via indexed dynamic slots (`item-{n}`). Each slot
9
9
  should contain a single `ActionMenuItem` — either a `<button>` (for actions) or a link (for
10
10
  navigation). The popover API and CSS anchor positioning handle positioning and dismiss behaviour
11
- natively; no JavaScript click-outside logic is needed.
11
+ natively, with a JS fallback for older Safari (see Notes).
12
12
 
13
13
  **Location**: `app/components/02.molecules/action-menu/`
14
14
 
@@ -16,7 +16,7 @@ natively; no JavaScript click-outside logic is needed.
16
16
 
17
17
  ## Components
18
18
 
19
- ### ActionMenu
19
+ ### `ActionMenu`
20
20
 
21
21
  | Prop | Type | Default | Notes |
22
22
  |---|---|---|---|
@@ -24,7 +24,7 @@ natively; no JavaScript click-outside logic is needed.
24
24
  | `triggerIcon` | `string` | `"lucide:ellipsis"` | Icon on the trigger button. |
25
25
  | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes on the root `<div>`. |
26
26
 
27
- **Slots**
27
+ #### Slots
28
28
 
29
29
  | Slot | When used |
30
30
  |---|---|
@@ -32,7 +32,7 @@ natively; no JavaScript click-outside logic is needed.
32
32
 
33
33
  ---
34
34
 
35
- ### ActionMenuItem
35
+ ### `ActionMenuItem`
36
36
 
37
37
  | Prop | Type | Default | Notes |
38
38
  |---|---|---|---|
@@ -41,19 +41,20 @@ natively; no JavaScript click-outside logic is needed.
41
41
  | `arrowIcon` | `string` | `"lucide:arrow-right"` | Decorative trailing arrow icon. |
42
42
  | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes on the root element. |
43
43
 
44
- **Slots**
44
+ #### Slots
45
45
 
46
46
  | Slot | Content |
47
47
  |---|---|
48
48
  | `#icon` | Optional left icon (e.g. `<Icon name="lucide:pencil" />`). Wrapped in `aria-hidden` span. |
49
49
 
50
- **Emits**
50
+ #### Emits
51
51
 
52
52
  | Event | Payload | Notes |
53
53
  |---|---|---|
54
54
  | `click` | `MouseEvent` | Fired on every click regardless of whether the item is a button or link. |
55
55
 
56
- **Notes on routing**
56
+ #### Notes on routing
57
+
57
58
  - Internal paths (`/…`) resolve to `<NuxtLink>` via `resolveComponent`.
58
59
  - External URLs or relative paths without a leading `/` render as plain `<a>`.
59
60
  - `type="button"` is set automatically on `<button>` elements to prevent accidental form submission.
@@ -63,7 +64,7 @@ natively; no JavaScript click-outside logic is needed.
63
64
  ## Basic usage
64
65
 
65
66
  ```vue
66
- <ActionMenu :item-count="3" label="Row actions">
67
+ <ActionMenu label="Row actions">
67
68
  <template #item-0>
68
69
  <ActionMenuItem label="Edit" @click="handleEdit">
69
70
  <template #icon><Icon name="lucide:pencil" /></template>
@@ -122,13 +123,18 @@ Quick reference:
122
123
 
123
124
  ## Notes
124
125
 
125
- - **Popover API + CSS anchor positioning** — the menu uses the `popover` attribute (broadly supported)
126
- and `position-anchor` (newer: Chromium 125+, Safari 26+). Without anchor positioning the popover
127
- still opens, just not anchored to the trigger. No polyfill is included.
128
- - **Auto-close** — clicking any `<li>` row fires `hidePopover()` on the menu. The `ActionMenuItem`
129
- emitting `click` triggers normally before the menu closes.
130
- - **Focus management** — on open the `toggle` event fires `handleToggle`, which moves focus to the
131
- first `[role="menuitem"]` inside the popover.
126
+ - **Popover API + CSS anchor positioning, with fallbacks** — open/close and positioning go through
127
+ the shared `useAnchoredPopover` composable, same as `SelectMenu`. Without anchor
128
+ positioning (Safari 17–18) JS measures the trigger and writes `--_anchor-*` px values
129
+ for the `@supports not (anchor-name: --a)` block. Without the Popover API (Safari 16) the trigger
130
+ toggles `isOpen`, `.action-menu-popover-open` shows the menu, outside pointerdown and Escape
131
+ close it, and `--action-menu-popover-z-index` (default `999999`) applies. See the `select-menu`
132
+ skill doc for the details and the `:popover-open` selector-list caveat.
133
+ - **`aria-expanded`** on the trigger tracks the open state in every browser (added 2026-10-05).
134
+ - **Auto-close** — clicking any `<li>` row calls the composable's `hide()` (native `hidePopover()`
135
+ or the fallback). The `ActionMenuItem` emitting `click` triggers normally before the menu closes.
136
+ - **Focus management** — on open (the `toggle` event, or after render in the fallback), focus moves
137
+ to the first `[role="menuitem"]` inside the popover.
132
138
  - **Keyboard navigation `currentIndex === -1` guard** — `handleKeydown` computes the current
133
139
  position via `items.indexOf(document.activeElement)`. When focus is outside the menu this returns
134
140
  `-1`. Always guard explicitly before applying wrap-around math: `ArrowDown` should focus
@@ -13,12 +13,12 @@ type: reference
13
13
  anchor-positioning (`anchor-name`/`position-anchor`/`anchor()`) and the native Popover API
14
14
  (`popover`/`popovertarget`).
15
15
 
16
- **Browser support caveat:** CSS anchor-positioning and the Popover API are both comparatively
17
- recent — check current browser support before shipping this on a page that must work everywhere,
18
- same caution as `ExpandingPanel`'s `::details-content` gap (CLAUDE.md pitfall #19). If broad
19
- support is a hard requirement, use [`DeepExpandingMenuClassic`](deep-expanding-menu-classic.md)
20
- instead — same API and visual result, built on `<details>`/`<summary>` +
21
- `onClickOutside` instead.
16
+ **Browser support:** since 2026-10-05 open/close and positioning go through the shared
17
+ `useAnchoredPopover` composable (see `composable-anchored-popover.md`), so the menu also works
18
+ without anchor positioning (Safari 17–18: JS-measured `position: fixed`) and without the Popover
19
+ API (Safari 16: JS open/close, outside click and Escape). Before that, panels never opened on
20
+ Safari 16. [`DeepExpandingMenuClassic`](deep-expanding-menu-classic.md) is still the alternative
21
+ built on `<details>`/`<summary>`: same API and visual result, with no popover layer involved.
22
22
 
23
23
  ## Props
24
24
 
@@ -82,6 +82,18 @@ See `CONSUMER-STYLING.md` for the full token API and what's not tokenised.
82
82
  - Auto-imported in Nuxt — no manual import needed.
83
83
  - Anchor names and popover target ids are generated per-instance via `useId()`, so multiple
84
84
  instances on one page don't collide.
85
+ - One `useAnchoredPopover` instance serves every group: `activeKey` tracks the open group, and the
86
+ composable's `triggerRef`/`popoverRef` are computeds over per-group element maps (filled by
87
+ function refs). Native `toggle` events for two auto popovers can arrive in either order (B opens,
88
+ then A closes), so a close event is ignored unless it's for the active group.
89
+ - Each group toggle has `aria-expanded`; the caret flip keys off it rather than
90
+ `:has(:popover-open)`, so it works in the fallback too.
91
+ - Panels have no ARIA role: this is the disclosure-navigation pattern (a toggle with
92
+ `aria-expanded` revealing ordinary links), not an ARIA menu. `role="menu"` was removed 2026-10-05
93
+ because it requires `menuitem` children and arrow-key handling the panels don't have, so screen
94
+ readers announced a menu that didn't behave like one. Don't add it back.
95
+ - 2026-10-05: removed two never-referenced `@position-try` blocks (`--anchor-left`, and an invalid
96
+ `@position-try-fallbacks --anchor-right` at-rule).
85
97
  - 2026-09-07 migration: moved from an unplaced top-level folder into
86
98
  `02.molecules/navigation/deep-expanding-menu/`; converted options-style `defineProps` to
87
99
  `interface Props` + `withDefaults`; the previous hardcoded `nav-1` anchor/popover id prefix
@@ -10,7 +10,8 @@ type: reference
10
10
 
11
11
  `DisplayTooltipDefined` composes [`DisplayTooltip`](display-tooltip.md), supplying a structured
12
12
  title/body/action content shape (each rendered with a consumer-chosen tag) plus a built-in close
13
- button wired to the popover's `popovertarget`. Use this when you want the standard tooltip content
13
+ button wired to the popover's `popovertarget` and to `DisplayTooltip`'s scoped `close()`, so it also
14
+ closes the tooltip on Safari 16 (no Popover API). Use this when you want the standard tooltip content
14
15
  layout without hand-building the `tooltipContent` slot markup each time; use plain `DisplayTooltip`
15
16
  directly when you need full control over the popover's content.
16
17
 
@@ -14,9 +14,11 @@ the native Popover API (`popover`/`popovertarget`) and CSS anchor-positioning
14
14
  with a built-in close button, see [`DisplayTooltipDefined`](display-tooltip-defined.md), which
15
15
  composes this component.
16
16
 
17
- **Browser support caveat:** CSS anchor-positioning and the Popover API are both comparatively
18
- recent — check current browser support before relying on this where broad support is a hard
19
- requirement, same caution as `DeepExpandingMenu`'s equivalent gap.
17
+ **Browser support:** since 2026-10-05 open/close and positioning go through the shared
18
+ `useAnchoredPopover` composable (`side: "right"`, see `composable-anchored-popover.md`). Without
19
+ anchor positioning (Safari 17–18) the panel is placed from the trigger's measured position, flipping
20
+ to the left when there's no room on the right. Without the Popover API (Safari 16) it opens and
21
+ closes in JS, with outside-click and Escape dismissal. The trigger has `aria-expanded`.
20
22
 
21
23
  ## Props
22
24
 
@@ -32,7 +34,7 @@ requirement, same caution as `DeepExpandingMenu`'s equivalent gap.
32
34
  | Slot | Description |
33
35
  |------|-------------|
34
36
  | `triggerContent` | Optional content rendered before the trigger button (e.g. a label the tooltip icon sits next to). |
35
- | `tooltipContent` | Content rendered inside the popover panel. |
37
+ | `tooltipContent` | Content rendered inside the popover panel. Scoped prop `close()` hides the panel in every browser: wire your own close button to it (`#tooltipContent="{ close }"`), since `popovertargetaction="hide"` alone does nothing without the Popover API. |
36
38
 
37
39
  ## Basic usage
38
40
 
@@ -54,6 +56,10 @@ popover width/colours/radius, and the close-button tokens `DisplayTooltipDefined
54
56
 
55
57
  ## Notes
56
58
 
59
+ - Slotted content: direct children of `tooltipContent` get `margin-block: 0` (in `:where()`, so
60
+ consumer margins win) and the panel sets `--display-tooltip-popover-font-size` (`1.4rem`) and
61
+ `-line-height` (`1.4`), so a bare `<p>` reads as tooltip text without extra styling.
62
+
57
63
  - Auto-imported in Nuxt — no manual import needed.
58
64
  - `tooltipId` is only reactive at mount — changing it after mount does not regenerate the linked
59
65
  `popovertarget`/`id` pair reactively beyond Vue's normal re-render, since both derive from the
@@ -15,9 +15,12 @@ with fully consumer-supplied content, built on the native Popover API
15
15
  text) or `ActionMenu` (menu semantics with arrow-key navigation), `PopOver` makes no assumption
16
16
  about what the trigger or content look like, and always renders its own visible close button.
17
17
 
18
- **Browser support caveat:** CSS anchor-positioning and the Popover API are both comparatively
19
- recent — check current browser support before relying on this where broad support is a hard
20
- requirement, same caution as `DisplayTooltip`/`DeepExpandingMenu`'s equivalent gap.
18
+ **Browser support:** since 2026-10-05 open/close and positioning go through the shared
19
+ `useAnchoredPopover` composable, with `side` following the `placement` prop (see
20
+ `composable-anchored-popover.md`). Without anchor positioning (Safari 17–18) the panel is placed
21
+ from the trigger's measured position on the `placement` side, flipping to the opposite side when
22
+ there's no room. Without the Popover API (Safari 16) it opens and closes in JS, with outside-click
23
+ and Escape dismissal.
21
24
 
22
25
  ## Props
23
26
 
@@ -72,11 +75,11 @@ the only source of space above/below the slotted content — a plain `<p>` doesn
72
75
 
73
76
  ## Behaviour
74
77
 
75
- - Opening/closing is handled natively by the Popover API: clicking the trigger toggles the panel,
76
- clicking the close button or pressing Escape hides it, and focus returns to the trigger
77
- automatically on close.
78
- - On open, focus moves to the close button (`handleToggle`, driven by the popover's native
79
- `toggle` event) so keyboard users land inside the panel immediately.
78
+ - Clicking the trigger toggles the panel; the close button, Escape or an outside click hides it,
79
+ and focus returns to the trigger. Natively this is the Popover API; on Safari 16 the composable
80
+ does the same in JS. The trigger has `aria-expanded` in every browser.
81
+ - On open, focus moves to the close button (the composable's `onOpen`) so keyboard users land
82
+ inside the panel immediately.
80
83
 
81
84
  ## CSS custom properties
82
85
 
@@ -171,11 +171,22 @@ Quick reference:
171
171
 
172
172
  ## Notes
173
173
 
174
- - **Popover API + CSS anchor positioning** — same mechanism as `ActionMenu`. The Popover API is
175
- broadly supported (Chrome 114+, Firefox 125+, Safari 17+), but CSS anchor positioning is much
176
- newer (Chromium 125+, Safari 26+), and without it the popover isn't placed under the trigger. No
177
- polyfill or fallback is included, so check support before using this where older devices
178
- matter; a native `<select>` (`InputSelect`) is the safer choice there.
174
+ - **Popover API + CSS anchor positioning, with fallbacks** — same mechanism as `ActionMenu`, both
175
+ via the shared `useAnchoredPopover` composable (`app/composables/useAnchoredPopover.ts`). The
176
+ Popover API arrived in Safari 17 and CSS anchor positioning in Safari 26, so:
177
+ - No anchor positioning (Safari 17–18): JS measures the trigger and writes `--_anchor-*` px values for the
178
+ `@supports not (anchor-name: --a)` block, re-measuring on scroll/resize and flipping above
179
+ (`data-placement="top"`) when there's no room below.
180
+ - No Popover API (Safari 16, e.g. first-generation iPad Pros that can't update): the trigger's
181
+ `@click` toggles `isOpen`, `.select-menu-popover-open` shows the list, and document listeners
182
+ close it on outside pointerdown and Escape. Not top-layer here, hence
183
+ `--select-menu-popover-z-index` (default `999999`).
184
+ - Detection runs in `onMounted` (`typeof HTMLElement.prototype.showPopover`, `CSS.supports`), so
185
+ SSR markup is identical everywhere. Every close goes through the composable's `hide()`, never
186
+ `hidePopover()` directly, so it works in both modes.
187
+ - Never put `:popover-open` in a selector list with the fallback class: browsers without it
188
+ drop the whole rule.
189
+ Confirmed broken on a Safari 16 iPad before this (trigger rendered, menu never opened).
179
190
  - **Left-aligned popover** (`left: anchor(left)`) — unlike `ActionMenu` which right-aligns. Matches
180
191
  native `<select>` dropdown behaviour. Flips above the trigger near the bottom of the viewport.
181
192
  - **Keyboard navigation** follows the WAI-ARIA listbox pattern: `ArrowDown`/`ArrowUp` move between
@@ -188,9 +199,18 @@ Quick reference:
188
199
  `[role="option"]` if one exists (in `multiple` mode, the first checked item), otherwise the first
189
200
  option (unlike `ActionMenu`, which always focuses the first item since it has no selection state).
190
201
  - **`aria-multiselectable="true"`** is set on the listbox only when `multiple` is `true`.
191
- - **Chevron rotation is pure CSS** via `:has(.select-menu-popover:popover-open)` on the root — no
192
- JS state drives the visual. `isOpen` is still tracked internally, but only to set `aria-expanded`
193
- on the trigger.
202
+ - **Selected rows are tinted** via `[aria-selected="true"]` (`--select-menu-item-surface-selected`,
203
+ default `--theme-surface-subtle`), in both single and multiple mode. Hover/focus is declared after
204
+ it at equal specificity, so the hover surface still shows on a selected row. Selected text colour,
205
+ font weight and border colour have their own `-selected` tokens falling back to the resting ones.
206
+ The row border width (`--select-menu-item-border-width`, default `0`) applies to every row with a
207
+ transparent resting colour, so a selected border never shifts layout. A hidden `::after` carrying
208
+ `data-label` in the selected weight reserves the label column's width, so a bolder selected
209
+ weight doesn't resize the popover. Hover/focus has `-text-color-hover` and `-border-hover`, which
210
+ fall back to the row's *current* state (selected or resting), not always to resting. There's no
211
+ 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
213
+ `:has(...:popover-open)`, so it also works without the Popover API (changed 2026-10-05).
194
214
  - **No native `<select>`/`appearance: base-select` involved** — this is a from-scratch popover
195
215
  listbox, not a styled native select, so it doesn't inherit `InputSelectCore`'s WebKit
196
216
  `appearance: base-select` constraints.
@@ -28,7 +28,7 @@ for that for a theme switch; use `TripleToggleSwitch` directly for any other thr
28
28
  | `name` | `string` | `"triple-toggle-switch"` | Shared radio `name`. Give each instance on a page its own. |
29
29
  | `aria-label` | `string` | `""` | Accessible name for the `role="radiogroup"`. Omitted from the DOM when empty. Always pass one. |
30
30
  | `theme` | `FormUiTheme` | `"default"` | Rendered as `data-theme`. |
31
- | `step-animation-duration` | `string` | `"250ms"` | Marker slide duration. Ignored under `prefers-reduced-motion`. |
31
+ | `step-animation-duration` | `string` | `"250ms"` | Marker slide duration; also times the icon colour swap (see Behaviour). Ignored under `prefers-reduced-motion`. |
32
32
  | `style-class-passthrough` | `string \| string[]` | `[]` | Classes on the root. |
33
33
 
34
34
  Option `id`s are used as element ids (and as a class on each icon), so they must be unique on the
@@ -66,6 +66,12 @@ const options = ref<IFormMultipleOptions>({
66
66
  - The marker is measured from the first option's width on mount and fades in after ~250ms. It
67
67
  isn't re-measured on resize, so changing the icon-size/padding tokens after mount (e.g. toggling
68
68
  a class) leaves the marker at its old size.
69
+ - Icon colour swaps are timed to the marker: the newly active icon waits half of
70
+ `stepAnimationDuration`, then fades to the active colour over the other half, so it lands as the
71
+ marker arrives rather than turning white on the bare track. The previous icon fades back straight
72
+ away. The marker takes the same time whatever the distance, so no per-index delay is needed.
73
+ - Option circles are sized by their content (1em icon + equal padding) with `place-items: center`,
74
+ not `aspect-ratio`; older iPad Safari mis-centred the icons under the `aspect-ratio` version.
69
75
  - Marker gradients only exist for the values `system`, `light` and `dark`; other values get the
70
76
  plain `--triple-toggle-switch-marker-surface` background.
71
77
 
@@ -42,6 +42,13 @@ short: `--ui-block-decorated-border-1` .. `-6`, `--ui-block-decorated-shadow-1`
42
42
  `--ui-block-decorated-inner-shadow-1` .. `-4`, and `--ui-block-decorated-inner-shadow-highlight`
43
43
  (layered on top of every inner shadow level).
44
44
 
45
+ To recolour every level at once (added 2026-10-05): `--ui-block-decorated-border-colour` (default
46
+ `var(--theme-border)`), `--ui-block-decorated-shadow-colour` (default `#000`) and
47
+ `--ui-block-decorated-inner-shadow-colour` (default: the shadow colour). Each shadow level mixes the
48
+ colour with its own opacity via `color-mix()` (8% .. 18%), so defaults render exactly as before.
49
+ The per-level tokens still override the whole value. The story has colour pickers for all three,
50
+ plus story-only background and text colour controls for checking contrast.
51
+
45
52
  ## Notes
46
53
 
47
54
  - 2026-09-16 migration: previously used options-style `defineProps` and referenced six families of
@@ -0,0 +1,108 @@
1
+ # useAnchoredPopover
2
+
3
+ `app/composables/useAnchoredPopover.ts`. Open/close state and positioning for a trigger + `popover`
4
+ element anchored with CSS anchor positioning, with fallbacks for browsers that lack either
5
+ feature. Used by `ActionMenu`, `SelectMenu`, `DeepExpandingMenu` (all `side: "bottom"`, the default)
6
+ `DisplayTooltip` (`side: "right"`) and `PopOver` (`side: () => props.placement`). Use it for any new anchored popover instead of calling `showPopover()`/`hidePopover()`
7
+ directly.
8
+
9
+ Several popovers where only one is open at a time (e.g. `DeepExpandingMenu`'s groups) can share
10
+ one instance: keep an `activeKey`, pass computeds over per-item element maps as `triggerRef`/
11
+ `popoverRef` (the options accept readonly refs), and ignore a `toggle` close event that isn't for
12
+ the active item, since auto popovers' open and close events can arrive in either order.
13
+
14
+ ## Why
15
+
16
+ - The Popover API arrived in Safari 17; CSS anchor positioning only in Safari 26.
17
+ - Safari 16 (e.g. first-generation iPad Pros, which can't update past iPadOS 16) has neither: the
18
+ trigger renders but the menu never opens.
19
+ - Safari 17–18 has the Popover API but no anchor positioning: the menu opens in the wrong place.
20
+
21
+ ## Modes (detected in `onMounted`, so SSR markup is the same everywhere)
22
+
23
+ | Support | Behaviour |
24
+ |---|---|
25
+ | Popover API + anchor positioning | Fully native. The composable only tracks `isOpen` from `toggle`. |
26
+ | Popover API, no anchor positioning | JS measures the trigger on `beforetoggle`/`toggle` and on scroll/resize, and writes the trigger's edges as `--_anchor-*` px values (see below); flips to the opposite side (`data-placement`) when the preferred side has no room and the opposite side does. |
27
+ | No Popover API | Also opens/closes in JS: trigger `@click` toggles `isOpen`, document `pointerdown` outside the root and `Escape` close it, and `hide()` returns focus to the trigger when focus was inside the popover, as native popovers do. The component shows the popover from a fallback class. |
28
+
29
+ ## API
30
+
31
+ ```ts
32
+ const {
33
+ isOpen, // Ref<boolean>, accurate in every mode; bind to aria-expanded
34
+ usesFallbackPopover, // Ref<boolean>, true without the Popover API
35
+ needsPositioning, // Ref<boolean>, true without anchor positioning
36
+ positionStyle, // bind to the popover's :style (undefined when not needed)
37
+ popoverPlacement, // bind to :data-placement: the resolved side, or undefined when native
38
+ show, hide, // always use these, never showPopover()/hidePopover() directly
39
+ handleTriggerClick, // trigger @click (no-op where popovertarget works)
40
+ handleBeforeToggle, // popover @beforetoggle
41
+ handleToggle, // popover @toggle
42
+ } = useAnchoredPopover({ rootRef, triggerRef, popoverRef, side: "bottom", onOpen: focusFirstItem });
43
+ ```
44
+
45
+ `rootRef` must contain both the trigger and the popover (outside-click detection). `onOpen` runs
46
+ after the popover is visible, so it can move focus into it.
47
+
48
+ `side` (`"bottom"` default, `"top"`, `"right"`, `"left"`, or a ref/getter of one, e.g. `() => props.placement`) is the preferred side for the fallback
49
+ flip; it doesn't affect native anchor positioning, which the component's own CSS controls.
50
+
51
+ ### Fallback position variables
52
+
53
+ They mirror `anchor()`: `--_anchor-{top|bottom|left|right}` is the trigger edge in viewport px, for
54
+ the `top`/`left` properties (`top: anchor(bottom)` becomes `top: var(--_anchor-bottom)`).
55
+ `--_anchor-{edge}-inverse` is the same edge measured from the viewport's bottom/right, for the
56
+ `bottom`/`right` properties (`right: anchor(left)` becomes `right: var(--_anchor-left-inverse)`).
57
+ All eight are always written, so CSS picks what each side needs.
58
+
59
+ ## Component wiring
60
+
61
+ ```vue
62
+ <div ref="rootRef" class="x-menu">
63
+ <button ref="triggerRef" :popovertarget="id" :aria-expanded="isOpen" @click="handleTriggerClick">…</button>
64
+ <div
65
+ :id="id"
66
+ ref="popoverRef"
67
+ popover
68
+ class="x-menu-popover"
69
+ :class="{ 'x-menu-popover-open': usesFallbackPopover && isOpen }"
70
+ :style="positionStyle"
71
+ :data-placement="popoverPlacement"
72
+ @beforetoggle="handleBeforeToggle"
73
+ @toggle="handleToggle"
74
+ >…</div>
75
+ </div>
76
+ ```
77
+
78
+ ```css
79
+ .x-menu-popover {
80
+ &:popover-open { display: block; }
81
+ /* Separate rule: a selector list containing :popover-open is dropped whole where unsupported. */
82
+ &.x-menu-popover-open { display: block; }
83
+
84
+ @supports not (anchor-name: --a) {
85
+ position: fixed;
86
+ top: calc(var(--_anchor-bottom, 0px) + var(--x-menu-block-distance, 0.4rem));
87
+ left: var(--_anchor-left, 0px);
88
+ z-index: var(--x-menu-popover-z-index, 999999);
89
+
90
+ &[data-placement="top"] {
91
+ top: auto;
92
+ bottom: calc(var(--_anchor-top-inverse, 0px) + var(--x-menu-block-distance, 0.4rem));
93
+ }
94
+ }
95
+ }
96
+ ```
97
+
98
+ Drive any "open" visuals (e.g. a chevron) from `[aria-expanded="true"]` on the trigger, not
99
+ `:has(:popover-open)`.
100
+
101
+ ## Caveats
102
+
103
+ - In the no-Popover-API mode the popover isn't in the top layer: give it a public z-index token
104
+ (default `999999`, pitfall #17), and an ancestor with `transform`/`filter`/`contain` becomes its
105
+ containing block and can misplace it.
106
+ - Tests: happy-dom has no Popover API and its `CSS.supports` can't be spied on. Define/undefine
107
+ `HTMLElement.prototype.showPopover` and swap `globalThis.CSS` (restore it in `afterEach`); see
108
+ `app/composables/tests/useAnchoredPopover.spec.ts`.
@@ -8,7 +8,7 @@
8
8
 
9
9
  ## Prerequisites
10
10
 
11
- - Uses the native HTML Popover API — supported in all modern browsers (Chrome 114+, Firefox 125+, Safari 17+)
11
+ - Built around the native Popover API (Chrome 114+, Firefox 125+, Safari 17+). With `DisplayTooltip`/`DisplayTooltipDefined` it also works on Safari 16, because those components fall back to JS open/close (`useAnchoredPopover`) and the guide drives them through their trigger buttons. Hand-written raw `[popover]` markup like the example below still needs the Popover API.
12
12
  - Popovers must have `id` attributes and corresponding trigger buttons with `popovertarget` and `popovertargetaction="toggle"` attributes
13
13
  - Close buttons inside each popover must have `popovertargetaction="hide"`
14
14
 
@@ -74,6 +74,8 @@ await startGuide()
74
74
  2. If `autoStart` is `true`, calls `startGuide()` which iterates the popovers in DOM order.
75
75
  3. For each popover: finds the `[popovertarget][popovertargetaction="toggle"]` trigger button and clicks it to open the popover, then waits for the `[popovertargetaction="hide"]` button to be clicked before advancing.
76
76
  4. After the last popover is dismissed, `autoRunGuide` is set to `false` and `isGuideRunning` becomes `false`.
77
+ 5. With no close button, a step ends when the trigger's `aria-expanded` turns `"false"` (a `MutationObserver`), or on the popover's `toggle` event with `newState: "closed"` when there's no trigger either.
78
+ 6. `stopGuide()`/`restartGuide()` close an open step by clicking its trigger when `aria-expanded="true"`, or with `hidePopover()` where `:popover-open` is supported. They never call `togglePopover()` or match `:popover-open` unguarded, both of which throw on Safari 16.
77
79
 
78
80
  ## API reference
79
81
 
@@ -66,6 +66,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
66
66
  ├── composable-tooltips-guide.md — useTooltipsGuide: sequential popover guide with auto-start, dismiss-to-advance, manual controls
67
67
  ├── composable-cookie-consent.md — useCookieConsent: unset/granted/denied state, cookie persistence, wraps @nuxt/scripts' useScriptTriggerConsent
68
68
  ├── composable-analytics.md — useAnalytics: provider-agnostic trackEvent/page-view tracking (google-analytics only implemented), consent-gated, single call site for setup + firing events
69
+ ├── composable-anchored-popover.md — useAnchoredPopover: popover + anchor positioning with JS fallbacks for Safari 16 (no Popover API) and 17–18 (no anchor positioning); used by ActionMenu/SelectMenu/DeepExpandingMenu/DisplayTooltip/PopOver
69
70
  └── components/
70
71
  ├── alert-content.md — AlertContent + AlertContentInner: themed alert panel (icon/title/content/actions/dismiss) under DisplayToast, DisplayPrompt and AlertMaskedContent; showIcon, #actions row, --alert-content-* tokens, app.config icon map
71
72
  ├── alert-mask-core.md — AlertMaskCore: SVG border/background mask sized to slotted content via ResizeObserver, config-prop-driven geometry/colour (no CSS token API)
@@ -14,8 +14,10 @@
14
14
  /* Used to scroll scale elements when scrolling x-axis like a carousel. */
15
15
 
16
16
  @media (prefers-reduced-motion: no-preference) {
17
- .animation-scroller-x {
18
- animation: scroller linear both;
19
- animation-timeline: view(x);
17
+ @supports (animation-timeline: view()) {
18
+ .animation-scroller-x {
19
+ animation: scroller linear both;
20
+ animation-timeline: view(x);
21
+ }
20
22
  }
21
23
  }
@@ -8,8 +8,10 @@
8
8
  }
9
9
 
10
10
  @media (prefers-reduced-motion: no-preference) {
11
- .auto-rotate {
12
- animation: autoRotateAnimation;
13
- animation-timeline: view();
11
+ @supports (animation-timeline: view()) {
12
+ .auto-rotate {
13
+ animation: autoRotateAnimation;
14
+ animation-timeline: view();
15
+ }
14
16
  }
15
17
  }
@@ -1,7 +1,9 @@
1
1
  @media (prefers-reduced-motion: no-preference) {
2
- .entry-exit-blur {
3
- animation: entryExitBlurAnimation linear both;
4
- animation-timeline: view();
2
+ @supports (animation-timeline: view()) {
3
+ .entry-exit-blur {
4
+ animation: entryExitBlurAnimation linear both;
5
+ animation-timeline: view();
6
+ }
5
7
  }
6
8
  }
7
9
  @keyframes entryExitBlurAnimation {
@@ -8,8 +8,10 @@
8
8
  }
9
9
 
10
10
  @media (prefers-reduced-motion: no-preference) {
11
- .entry-slide-in {
12
- animation: entrySlideInAnimation forwards;
13
- animation-timeline: view(70% 5%);
11
+ @supports (animation-timeline: view()) {
12
+ .entry-slide-in {
13
+ animation: entrySlideInAnimation forwards;
14
+ animation-timeline: view(70% 5%);
15
+ }
14
16
  }
15
17
  }
@@ -10,8 +10,10 @@
10
10
  }
11
11
 
12
12
  @media (prefers-reduced-motion: no-preference) {
13
- .entry-zoom-reveal {
14
- animation: entryZoomRevealAnimation both;
15
- animation-timeline: view(70% 5%);
13
+ @supports (animation-timeline: view()) {
14
+ .entry-zoom-reveal {
15
+ animation: entryZoomRevealAnimation both;
16
+ animation-timeline: view(70% 5%);
17
+ }
16
18
  }
17
19
  }
@@ -31,8 +31,12 @@ any passthrough classes.
31
31
  ## Motion
32
32
 
33
33
  - All three classes only animate inside `@media (prefers-reduced-motion: no-preference)`.
34
- - They use `animation-timeline: view(...)` (scroll-driven). In a browser without scroll-driven
35
- animation support the animation completes immediately, so the content just appears in place.
34
+ - They use `animation-timeline: view(...)` (scroll-driven), inside an
35
+ `@supports (animation-timeline: view())` guard, so a browser without scroll-driven animations
36
+ (Safari before 26, Firefox) shows the content in place with no animation.
37
+
38
+ > Changed 2026-10-05: the guard is new. Before it, an unsupported browser ran the animation with a
39
+ > zero duration and kept its end keyframe, which for `entry-exit-blur` left the content blurred.
36
40
 
37
41
  ---
38
42
 
@@ -17,6 +17,8 @@
17
17
  | `--display-tooltip-popover-width` | `30rem` | Popover panel width |
18
18
  | `--display-tooltip-popover-padding` | `1.2rem` | Popover panel content padding |
19
19
  | `--display-tooltip-popover-content-gap` | `1.2rem` | Gap between stacked children in the `tooltipContent` slot |
20
+ | `--display-tooltip-popover-font-size` | `1.4rem` | Popover text size (`DisplayTooltipDefined` sets its own per-element sizes) |
21
+ | `--display-tooltip-popover-line-height` | `1.4` | Popover text line height |
20
22
  | `--display-tooltip-popover-outline-width` | `0.1rem` | Popover panel outline (border) width |
21
23
  | `--display-tooltip-popover-outline-colour` | `var(--slate-02)` | Popover panel outline (border) colour |
22
24
  | `--display-tooltip-popover-text-colour` | `var(--slate-09)` | Popover panel text colour |
@@ -24,6 +26,7 @@
24
26
  | `--display-tooltip-popover-border-radius` | `0.8rem` | Popover panel corner radius |
25
27
  | `--display-tooltip-popover-shadow` | `0 0.4rem 1.6rem rgba(0,0,0,.12)` | Popover panel elevation shadow |
26
28
  | `--display-tooltip-popover-offset` | `0.1rem` | Gap between the trigger and the popover panel |
29
+ | `--display-tooltip-popover-z-index` | `999999` | Panel stacking order in browsers without CSS anchor positioning. Ignored where the panel renders in the top layer |
27
30
  | `--display-tooltip-close-button-colour` | `var(--slate-09)` | Close button text colour (used by `DisplayTooltipDefined`) |
28
31
  | `--display-tooltip-close-button-border-width` | `0.1rem` | Close button border width |
29
32
  | `--display-tooltip-close-button-border-colour` | `var(--slate-03)` | Close button border colour |
@@ -61,13 +64,22 @@ Set the tokens above on an element you own (a page or section class, or a class
61
64
 
62
65
  ## Notes
63
66
 
67
+ - Direct children of the `tooltipContent` slot have their block margins reset to `0` (inside
68
+ `:where()`, so any margin you set on them still wins): the gap token is the only spacing between
69
+ them. Before 2026-10-05 a slotted `<p>` kept its browser `1em` margins on top of the padding.
70
+
64
71
  - The trigger button is vertically centred on the first line of `triggerContent` text, using
65
72
  `--display-tooltip-trigger-icon-box-size`, `-padding` and `-border-width` to work out its height.
66
73
  Give `--display-tooltip-trigger-padding` a single length (e.g. `0.4rem`): a two-value shorthand
67
74
  breaks that calculation, and the button is square anyway.
68
- - Built on the native Popover API (`popover`/`popovertarget`) and CSS anchor-positioning
69
- (`anchor-name`/`position-anchor`/`anchor()`) — check current browser support before relying on
70
- it where broad support is a hard requirement.
75
+ - Built on the native Popover API and CSS anchor positioning, with fallbacks: without anchor
76
+ positioning (e.g. Safari 17–18) the panel is placed with `position: fixed` from the trigger's
77
+ measured position and flips left (`.display-tooltip-popover[data-placement="left"]`) when there's
78
+ no room on the right. Without the Popover API (Safari 16) it opens and closes in JS, shows via
79
+ `.display-tooltip-popover-open` (style it in a separate rule from `:popover-open`: a selector list
80
+ containing `:popover-open` is dropped whole where unsupported), isn't in the top layer (so the
81
+ z-index token applies), and an ancestor with `transform`, `filter` or `contain` can misplace it.
82
+ The trigger's `aria-expanded` tracks the open state in every browser.
71
83
  - The `.display-tooltip-close-button` styled here is rendered by `DisplayTooltipDefined`, not this
72
84
  component directly — `DisplayTooltip` only provides the `tooltipContent` slot, so a plain
73
85
  `DisplayTooltip` usage supplies its own close affordance if it wants one (give it