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.
- package/.claude/skills/components/action-menu.md +21 -15
- package/.claude/skills/components/deep-expanding-menu.md +18 -6
- package/.claude/skills/components/display-tooltip-defined.md +2 -1
- package/.claude/skills/components/display-tooltip.md +10 -4
- package/.claude/skills/components/pop-over.md +11 -8
- package/.claude/skills/components/select-menu.md +28 -8
- package/.claude/skills/components/triple-toggle-switch.md +7 -1
- package/.claude/skills/components/ui-block-decorated.md +7 -0
- package/.claude/skills/composable-anchored-popover.md +108 -0
- package/.claude/skills/composable-tooltips-guide.md +3 -1
- package/.claude/skills/index.md +1 -0
- package/app/assets/styles/setup/06.utility-classes/animations/_animation-scroller-x.css +5 -3
- package/app/assets/styles/setup/06.utility-classes/animations/_auto-rotate.css +5 -3
- package/app/assets/styles/setup/06.utility-classes/animations/_entry-exit-blur.css +5 -3
- package/app/assets/styles/setup/06.utility-classes/animations/_entry-slide-in.css +5 -3
- package/app/assets/styles/setup/06.utility-classes/animations/_entry-zoom-reveal.css +5 -3
- package/app/components/01.atoms/animations/entry/CONSUMER-STYLING.md +6 -2
- package/app/components/01.atoms/display-tooltip/CONSUMER-STYLING.md +15 -3
- package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +62 -3
- package/app/components/01.atoms/display-tooltip/tests/DisplayTooltip.spec.ts +36 -1
- package/app/components/01.atoms/display-tooltip/tests/__snapshots__/DisplayTooltip.spec.ts.snap +1 -1
- package/app/components/01.atoms/navigation/skip-links/SkipLinks.vue +1 -1
- package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +9 -3
- package/app/components/01.atoms/pop-over/PopOver.vue +70 -9
- package/app/components/01.atoms/pop-over/tests/PopOver.spec.ts +53 -0
- package/app/components/01.atoms/pop-over/tests/__snapshots__/PopOver.spec.ts.snap +1 -1
- package/app/components/01.atoms/ui-block-decorated/CONSUMER-STYLING.md +12 -4
- package/app/components/01.atoms/ui-block-decorated/UiBlockDecorated.vue +20 -16
- package/app/components/01.atoms/ui-block-decorated/stories/UiBlockDecorated.stories.ts +124 -96
- package/app/components/02.molecules/action-menu/ActionMenu.vue +41 -16
- package/app/components/02.molecules/action-menu/CONSUMER-STYLING.md +16 -3
- package/app/components/02.molecules/action-menu/tests/ActionMenu.spec.ts +28 -0
- package/app/components/02.molecules/action-menu/tests/__snapshots__/ActionMenu.spec.ts.snap +3 -3
- package/app/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue +2 -1
- package/app/components/02.molecules/display-tooltip-defined/stories/DisplayTooltipDefined.stories.ts +7 -4
- package/app/components/02.molecules/display-tooltip-defined/tests/DisplayTooltipDefined.spec.ts +20 -1
- package/app/components/02.molecules/display-tooltip-defined/tests/__snapshots__/DisplayTooltipDefined.spec.ts.snap +1 -1
- package/app/components/02.molecules/navigation/deep-expanding-menu/CONSUMER-STYLING.md +18 -4
- package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +87 -16
- package/app/components/02.molecules/navigation/deep-expanding-menu/tests/DeepExpandingMenu.spec.ts +83 -1
- package/app/components/02.molecules/navigation/deep-expanding-menu/tests/__snapshots__/DeepExpandingMenu.spec.ts.snap +2 -2
- package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +1 -1
- package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +1 -1
- package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +26 -6
- package/app/components/02.molecules/select-menu/SelectMenu.vue +90 -18
- package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +36 -0
- package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +9 -9
- package/app/components/03.organisms/responsive-header/NavigationItems.vue +2 -2
- package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +4 -4
- package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitch.vue +11 -3
- package/app/composables/tests/useAnchoredPopover.spec.ts +231 -0
- package/app/composables/tests/useTooltips.spec.ts +69 -0
- package/app/composables/useAnchoredPopover.ts +183 -0
- package/app/composables/useTooltips.ts +37 -20
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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** —
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
- On open, focus moves to the close button (`
|
|
79
|
-
|
|
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
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
- **
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
-
|
|
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
|
|
package/.claude/skills/index.md
CHANGED
|
@@ -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
|
-
|
|
18
|
-
animation
|
|
19
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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)
|
|
35
|
-
|
|
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
|
|
69
|
-
(
|
|
70
|
-
|
|
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
|