srcdev-nuxt-components 9.1.50 → 9.1.52
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/settings.json +8 -27
- package/.claude/settings.local.json +24 -52
- package/.claude/skills/components/action-menu.md +141 -0
- package/.claude/skills/components/stepper-list.md +52 -18
- package/.claude/skills/css-nesting-conventions.md +121 -0
- package/.claude/skills/index.md +3 -0
- package/.claude/skills/pull-request-description.md +48 -0
- package/.claude/skills/testing-add-unit-test.md +40 -2
- package/README.md +26 -5
- package/app/components/02.molecules/action-menu/ActionMenu.vue +218 -0
- package/app/components/02.molecules/action-menu/ActionMenuItemCore.vue +123 -0
- package/app/components/02.molecules/action-menu/CONSUMER-STYLING.md +125 -0
- package/app/components/02.molecules/action-menu/stories/ActionMenu.stories.ts +326 -0
- package/app/components/02.molecules/action-menu/tests/ActionMenu.spec.ts +458 -0
- package/app/components/02.molecules/action-menu/tests/ActionMenuItemCore.spec.ts +199 -0
- package/app/components/02.molecules/action-menu/tests/__snapshots__/ActionMenu.spec.ts.snap +28 -0
- package/app/components/02.molecules/action-menu/tests/__snapshots__/ActionMenuItemCore.spec.ts.snap +27 -0
- package/app/components/02.molecules/display-chip/tests/DisplayChip.spec.ts +5 -1
- package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +2 -1
- package/app/components/02.molecules/stepper-list/CONSUMER-STYLING.md +138 -0
- package/app/components/02.molecules/stepper-list/StepperList.vue +50 -24
- package/app/components/03.organisms/image-galleries/slider-gallery/tests/SliderGallery.spec.ts +5 -1
- package/app/components/05.forms/input-button/InputButtonCore.vue +1 -1
- package/app/components/carousels/tests/CarouselFlip.spec.ts +1 -0
- package/app/components/responsive-header/NavigationItems.vue +302 -81
- package/app/components/responsive-header/ResponsiveHeader.vue +360 -56
- package/app/layouts/default.vue +43 -352
- package/app/layouts/site-navigation-demo.vue +36 -34
- package/app/pages/page-hero-highlights.vue +3 -3
- package/app/pages/ui/scroll-reveal-image.vue +3 -3
- package/app/pages/ui/simple-grid.vue +9 -20
- package/modules/colour-scheme.ts +1 -1
- package/nuxt.config.ts +11 -2
- package/package.json +17 -16
package/README.md
CHANGED
|
@@ -104,15 +104,36 @@ When disabled, no `data-color-scheme` attribute is set on `<html>` and `useColou
|
|
|
104
104
|
|
|
105
105
|
---
|
|
106
106
|
|
|
107
|
-
## Known
|
|
107
|
+
## Known Build / Production Issues
|
|
108
108
|
|
|
109
|
-
### `
|
|
109
|
+
### `ERR_MODULE_NOT_FOUND: vue/index.mjs` — 500 on every request (Node 22)
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
**Symptom**: After `npm run build`, running `node .output/server/index.mjs` returns HTTP 500 on every page. The terminal shows:
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
```text
|
|
114
|
+
Error [ERR_MODULE_NOT_FOUND]: Cannot find module
|
|
115
|
+
'.output/server/node_modules/vue/index.mjs'
|
|
116
|
+
Did you mean to import
|
|
117
|
+
'.output/server/node_modules/.nitro/vue@3.5.34/dist/vue.cjs.prod.js'?
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Root cause**: Nitro's dependency tracer externalises `vue` and copies it to `.output/server/node_modules/vue/`, but only traces the CJS build (`dist/vue.cjs.prod.js`). Vue's `package.json` exports map resolves the `"import"` + `"node"` condition (used by Node 22 when importing from an ES module) to `./index.mjs` — a file that was never copied. This is a Nitro tracing bug exposed by Node 22's stricter ESM condition matching, made more likely by `vue.runtimeCompiler: true` (which changes how Nuxt aliases Vue at build time, causing Nitro to fall back to externalising it as a node_modules package).
|
|
121
|
+
|
|
122
|
+
**Fix** (already applied in this repo's `nuxt.config.ts`):
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
nitro: {
|
|
126
|
+
externals: {
|
|
127
|
+
inline: ["vue", "@vue/runtime-core", "@vue/runtime-dom", "@vue/reactivity", "@vue/shared", "@vue/server-renderer"],
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
This tells Nitro to bundle these packages inline rather than externalise them, which sidesteps the runtime resolution entirely.
|
|
133
|
+
|
|
134
|
+
**Consumer apps**: This config is not gated behind `isStandalone`, so it is inherited automatically by any app that extends this layer. No consumer-side action is required.
|
|
114
135
|
|
|
115
|
-
**
|
|
136
|
+
**When it's safe to remove**: If a future Nitro release fixes the dependency tracer so that all export-condition files are copied correctly, the `inline` list can be removed. Verify by checking that `.output/server/node_modules/vue/index.mjs` exists after a clean build without the config.
|
|
116
137
|
|
|
117
138
|
---
|
|
118
139
|
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="action-menu" :class="[elementClasses]" :style="`--_anchor-name: ${anchorName}`">
|
|
3
|
+
<button
|
|
4
|
+
ref="triggerRef"
|
|
5
|
+
:popovertarget="menuId"
|
|
6
|
+
popovertargetaction="toggle"
|
|
7
|
+
type="button"
|
|
8
|
+
class="action-menu-trigger"
|
|
9
|
+
:aria-label="label"
|
|
10
|
+
aria-haspopup="menu"
|
|
11
|
+
>
|
|
12
|
+
<Icon name="lucide:ellipsis" class="action-menu-trigger-icon" aria-hidden="true" />
|
|
13
|
+
</button>
|
|
14
|
+
|
|
15
|
+
<div
|
|
16
|
+
:id="menuId"
|
|
17
|
+
ref="popoverRef"
|
|
18
|
+
popover
|
|
19
|
+
class="action-menu-popover"
|
|
20
|
+
@toggle="handleToggle"
|
|
21
|
+
@keydown="handleKeydown"
|
|
22
|
+
>
|
|
23
|
+
<ul class="action-menu-list" role="menu" :aria-label="label">
|
|
24
|
+
<li v-for="n in itemCount" :key="n - 1" class="action-menu-list-item" role="none" @click="closeMenu">
|
|
25
|
+
<slot :name="`item-${n - 1}`"></slot>
|
|
26
|
+
</li>
|
|
27
|
+
</ul>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<script setup lang="ts">
|
|
33
|
+
interface Props {
|
|
34
|
+
label?: string;
|
|
35
|
+
styleClassPassthrough?: string | string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
39
|
+
label: "Open actions menu",
|
|
40
|
+
styleClassPassthrough: () => [],
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const slots = useSlots();
|
|
44
|
+
const itemCount = computed(() => Object.keys(slots).filter((name) => /^item-\d+$/.test(name)).length);
|
|
45
|
+
|
|
46
|
+
const id = useId();
|
|
47
|
+
const menuId = `action-menu-${id}`;
|
|
48
|
+
const anchorName = `--action-menu-anchor-${id}`;
|
|
49
|
+
|
|
50
|
+
const triggerRef = ref<HTMLButtonElement | null>(null);
|
|
51
|
+
const popoverRef = ref<HTMLDivElement | null>(null);
|
|
52
|
+
|
|
53
|
+
/** Returns all focusable menuitems in DOM order. */
|
|
54
|
+
const getMenuItems = (): HTMLElement[] =>
|
|
55
|
+
Array.from(popoverRef.value?.querySelectorAll<HTMLElement>('[role="menuitem"]') ?? []);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Close the menu and return focus to the trigger.
|
|
59
|
+
* Called on item click — Escape is handled natively by the Popover API
|
|
60
|
+
* (which also restores focus to the trigger automatically).
|
|
61
|
+
*/
|
|
62
|
+
const closeMenu = () => {
|
|
63
|
+
popoverRef.value?.hidePopover();
|
|
64
|
+
triggerRef.value?.focus();
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const handleToggle = (event: Event) => {
|
|
68
|
+
const toggleEvent = event as ToggleEvent;
|
|
69
|
+
if (toggleEvent.newState === "open") {
|
|
70
|
+
getMenuItems()[0]?.focus();
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Keyboard navigation following the WAI-ARIA menu pattern.
|
|
76
|
+
*
|
|
77
|
+
* ArrowDown / ArrowUp — move between items (wraps around).
|
|
78
|
+
* Home / End — jump to first / last item.
|
|
79
|
+
* Tab — close the menu; let the browser Tab naturally
|
|
80
|
+
* (do NOT focus the trigger — Tab should advance
|
|
81
|
+
* to the next element in the page).
|
|
82
|
+
* Escape — handled natively by the Popover API.
|
|
83
|
+
*/
|
|
84
|
+
const handleKeydown = (event: KeyboardEvent) => {
|
|
85
|
+
const items = getMenuItems();
|
|
86
|
+
if (!items.length) return;
|
|
87
|
+
|
|
88
|
+
const currentIndex = items.indexOf(document.activeElement as HTMLElement);
|
|
89
|
+
|
|
90
|
+
switch (event.key) {
|
|
91
|
+
case "ArrowDown":
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
items[currentIndex === -1 ? 0 : (currentIndex + 1) % items.length]?.focus();
|
|
94
|
+
break;
|
|
95
|
+
case "ArrowUp":
|
|
96
|
+
event.preventDefault();
|
|
97
|
+
items[currentIndex === -1 ? items.length - 1 : (currentIndex - 1 + items.length) % items.length]?.focus();
|
|
98
|
+
break;
|
|
99
|
+
case "Home":
|
|
100
|
+
event.preventDefault();
|
|
101
|
+
items[0]?.focus();
|
|
102
|
+
break;
|
|
103
|
+
case "End":
|
|
104
|
+
event.preventDefault();
|
|
105
|
+
items[items.length - 1]?.focus();
|
|
106
|
+
break;
|
|
107
|
+
case "Tab":
|
|
108
|
+
// Close without stealing focus — Tab exits to the next DOM element naturally.
|
|
109
|
+
popoverRef.value?.hidePopover();
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
115
|
+
</script>
|
|
116
|
+
|
|
117
|
+
<style lang="css">
|
|
118
|
+
@layer components {
|
|
119
|
+
.action-menu {
|
|
120
|
+
--_block-distance: var(--action-menu-block-distance, 0.4rem);
|
|
121
|
+
--_trigger-size: var(--action-menu-trigger-size, 3.2rem);
|
|
122
|
+
--_trigger-border-radius: var(--action-menu-trigger-border-radius, var(--button-border-radius-icon-only, 50%));
|
|
123
|
+
--_trigger-surface: var(--action-menu-trigger-surface, transparent);
|
|
124
|
+
--_trigger-surface-hover: var(--action-menu-trigger-surface-hover, var(--slate-01));
|
|
125
|
+
--_trigger-icon-size: var(--action-menu-trigger-icon-size, 2rem);
|
|
126
|
+
--_trigger-icon-color: var(--action-menu-trigger-icon-color, var(--slate-07));
|
|
127
|
+
|
|
128
|
+
--_popover-background: var(--action-menu-popover-background, var(--slate-00));
|
|
129
|
+
--_popover-border: var(--action-menu-popover-border, 0.1rem solid var(--slate-03));
|
|
130
|
+
--_popover-border-radius: var(--action-menu-popover-border-radius, 0.8rem);
|
|
131
|
+
--_popover-min-width: var(--action-menu-popover-min-width, 20rem);
|
|
132
|
+
--_popover-shadow: var(--action-menu-popover-shadow, 0 0.4rem 1.6rem rgba(0, 0, 0, 0.1));
|
|
133
|
+
--_popover-transition-duration: var(--action-menu-popover-transition-duration, 200ms);
|
|
134
|
+
--_item-divider: var(--action-menu-item-divider, 0.1rem solid var(--slate-02));
|
|
135
|
+
|
|
136
|
+
position: relative;
|
|
137
|
+
display: inline-block;
|
|
138
|
+
|
|
139
|
+
.action-menu-trigger {
|
|
140
|
+
all: unset;
|
|
141
|
+
cursor: pointer;
|
|
142
|
+
display: grid;
|
|
143
|
+
place-items: center;
|
|
144
|
+
width: var(--_trigger-size);
|
|
145
|
+
height: var(--_trigger-size);
|
|
146
|
+
border-radius: var(--_trigger-border-radius);
|
|
147
|
+
background-color: var(--_trigger-surface);
|
|
148
|
+
color: var(--_trigger-icon-color);
|
|
149
|
+
anchor-name: var(--_anchor-name);
|
|
150
|
+
transition: background-color var(--control-transition-duration, 200ms) var(--control-transition-ease, ease);
|
|
151
|
+
|
|
152
|
+
&:hover,
|
|
153
|
+
&:focus-visible {
|
|
154
|
+
background-color: var(--_trigger-surface-hover);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
&:focus-visible {
|
|
158
|
+
outline: var(--button-outline-width, 0.2rem) solid var(--theme-button-primary-outline, currentcolor);
|
|
159
|
+
outline-offset: 0.2rem;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.action-menu-trigger-icon {
|
|
163
|
+
display: block;
|
|
164
|
+
width: var(--_trigger-icon-size);
|
|
165
|
+
height: var(--_trigger-icon-size);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.action-menu-popover {
|
|
170
|
+
border: var(--_popover-border);
|
|
171
|
+
margin: 0;
|
|
172
|
+
padding: 0;
|
|
173
|
+
inset: auto;
|
|
174
|
+
background-color: var(--_popover-background);
|
|
175
|
+
border-radius: var(--_popover-border-radius);
|
|
176
|
+
min-width: var(--_popover-min-width);
|
|
177
|
+
box-shadow: var(--_popover-shadow);
|
|
178
|
+
overflow: hidden;
|
|
179
|
+
|
|
180
|
+
position-anchor: var(--_anchor-name);
|
|
181
|
+
top: calc(anchor(bottom) + var(--_block-distance));
|
|
182
|
+
right: anchor(right);
|
|
183
|
+
left: auto;
|
|
184
|
+
position-try-fallbacks: flip-block;
|
|
185
|
+
|
|
186
|
+
opacity: 0;
|
|
187
|
+
display: none;
|
|
188
|
+
transition:
|
|
189
|
+
opacity var(--_popover-transition-duration),
|
|
190
|
+
display var(--_popover-transition-duration),
|
|
191
|
+
overlay var(--_popover-transition-duration);
|
|
192
|
+
transition-behavior: allow-discrete;
|
|
193
|
+
|
|
194
|
+
&:popover-open {
|
|
195
|
+
display: block;
|
|
196
|
+
opacity: 1;
|
|
197
|
+
|
|
198
|
+
@starting-style {
|
|
199
|
+
display: block;
|
|
200
|
+
opacity: 0;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.action-menu-list {
|
|
205
|
+
list-style: none;
|
|
206
|
+
padding: 0;
|
|
207
|
+
margin: 0;
|
|
208
|
+
|
|
209
|
+
.action-menu-list-item {
|
|
210
|
+
&:not(:last-child) {
|
|
211
|
+
border-bottom: var(--_item-divider);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
</style>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component
|
|
3
|
+
:is="tag"
|
|
4
|
+
:type="!isLink ? 'button' : undefined"
|
|
5
|
+
:href="isLink ? href : undefined"
|
|
6
|
+
class="action-menu-item-core"
|
|
7
|
+
:class="[elementClasses]"
|
|
8
|
+
role="menuitem"
|
|
9
|
+
@click="emit('click', $event)"
|
|
10
|
+
>
|
|
11
|
+
<span v-if="slots.icon" class="action-menu-item-icon" aria-hidden="true">
|
|
12
|
+
<slot name="icon"></slot>
|
|
13
|
+
</span>
|
|
14
|
+
<span class="action-menu-item-label">{{ label }}</span>
|
|
15
|
+
<span class="action-menu-item-arrow" aria-hidden="true">
|
|
16
|
+
<Icon name="lucide:arrow-right" class="action-menu-item-arrow-icon" />
|
|
17
|
+
</span>
|
|
18
|
+
</component>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<script setup lang="ts">
|
|
22
|
+
interface Props {
|
|
23
|
+
label: string;
|
|
24
|
+
href?: string;
|
|
25
|
+
styleClassPassthrough?: string | string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
29
|
+
href: undefined,
|
|
30
|
+
styleClassPassthrough: () => [],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const emit = defineEmits<{
|
|
34
|
+
click: [event: MouseEvent];
|
|
35
|
+
}>();
|
|
36
|
+
|
|
37
|
+
const NuxtLink = resolveComponent("NuxtLink");
|
|
38
|
+
const isLink = computed(() => Boolean(props.href));
|
|
39
|
+
const isInternalLink = computed(() => isLink.value && props.href?.startsWith("/"));
|
|
40
|
+
const tag = computed(() => {
|
|
41
|
+
if (isInternalLink.value) return NuxtLink;
|
|
42
|
+
if (isLink.value) return "a";
|
|
43
|
+
return "button";
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const slots = useSlots();
|
|
47
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
48
|
+
|
|
49
|
+
watch(
|
|
50
|
+
() => props.styleClassPassthrough,
|
|
51
|
+
() => {
|
|
52
|
+
resetElementClasses(props.styleClassPassthrough);
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<style lang="css">
|
|
58
|
+
@layer components {
|
|
59
|
+
.action-menu-item-core {
|
|
60
|
+
--_surface-hover: var(--action-menu-item-surface-hover, light-dark(var(--slate-01), var(--slate-09)));
|
|
61
|
+
--_text-color: var(--action-menu-item-text-color, light-dark(var(--slate-09), var(--slate-01)));
|
|
62
|
+
--_icon-size: var(--action-menu-item-icon-size, 2rem);
|
|
63
|
+
--_padding-inline: var(--action-menu-item-padding-inline, 1.6rem);
|
|
64
|
+
--_padding-block: var(--action-menu-item-padding-block, 1.2rem);
|
|
65
|
+
|
|
66
|
+
all: unset;
|
|
67
|
+
box-sizing: border-box;
|
|
68
|
+
cursor: pointer;
|
|
69
|
+
display: grid;
|
|
70
|
+
grid-template-columns: auto 1fr auto;
|
|
71
|
+
align-items: center;
|
|
72
|
+
gap: 1.2rem;
|
|
73
|
+
width: 100%;
|
|
74
|
+
padding-inline: var(--_padding-inline);
|
|
75
|
+
padding-block: var(--_padding-block);
|
|
76
|
+
color: var(--_text-color);
|
|
77
|
+
font-family: var(--font-family);
|
|
78
|
+
font-size: var(--button-font-size, 1.4rem);
|
|
79
|
+
font-weight: var(--button-font-weight, 500);
|
|
80
|
+
line-height: var(--button-line-height, 1.2);
|
|
81
|
+
transition: background-color var(--control-transition-duration, 200ms) var(--control-transition-ease, ease);
|
|
82
|
+
text-decoration: none;
|
|
83
|
+
|
|
84
|
+
&:hover,
|
|
85
|
+
&:focus-visible {
|
|
86
|
+
background-color: var(--_surface-hover);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
&:focus-visible {
|
|
90
|
+
outline: var(--button-outline-width, 0.2rem) solid var(--theme-button-primary-outline, currentcolor);
|
|
91
|
+
outline-offset: -0.2rem;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.action-menu-item-icon {
|
|
95
|
+
display: flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
justify-content: center;
|
|
98
|
+
width: var(--_icon-size);
|
|
99
|
+
height: var(--_icon-size);
|
|
100
|
+
flex-shrink: 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.action-menu-item-label {
|
|
104
|
+
white-space: nowrap;
|
|
105
|
+
overflow: hidden;
|
|
106
|
+
text-overflow: ellipsis;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.action-menu-item-arrow {
|
|
110
|
+
display: flex;
|
|
111
|
+
align-items: center;
|
|
112
|
+
flex-shrink: 0;
|
|
113
|
+
opacity: 0.5;
|
|
114
|
+
|
|
115
|
+
.action-menu-item-arrow-icon {
|
|
116
|
+
display: block;
|
|
117
|
+
width: 1.6rem;
|
|
118
|
+
height: 1.6rem;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
</style>
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# ActionMenu — Consumer Styling Guide
|
|
2
|
+
|
|
3
|
+
## Public token API
|
|
4
|
+
|
|
5
|
+
All `--action-menu-*` tokens are the stable override surface. Because action menus appear
|
|
6
|
+
repeatedly across the UI (tables, cards, list rows) the recommended approach is to set tokens
|
|
7
|
+
once in a **global CSS file** rather than per-instance via `styleClassPassthrough`.
|
|
8
|
+
|
|
9
|
+
### Trigger button
|
|
10
|
+
|
|
11
|
+
| Token | Default | Controls |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `--action-menu-trigger-size` | `3.2rem` | Trigger button width and height |
|
|
14
|
+
| `--action-menu-trigger-border-radius` | `var(--button-border-radius-icon-only, 50%)` | Trigger corner rounding |
|
|
15
|
+
| `--action-menu-trigger-surface` | `transparent` | Trigger background (rest state) |
|
|
16
|
+
| `--action-menu-trigger-surface-hover` | `light-dark(var(--slate-01), var(--slate-09))` | Trigger background on hover/focus |
|
|
17
|
+
| `--action-menu-trigger-icon-size` | `2rem` | Ellipsis icon size |
|
|
18
|
+
| `--action-menu-trigger-icon-color` | `light-dark(var(--slate-07), var(--slate-03))` | Ellipsis icon colour |
|
|
19
|
+
|
|
20
|
+
### Menu popover
|
|
21
|
+
|
|
22
|
+
| Token | Default | Controls |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| `--action-menu-block-distance` | `0.4rem` | Gap between trigger bottom and menu top |
|
|
25
|
+
| `--action-menu-popover-background` | `light-dark(var(--slate-00), var(--slate-10))` | Menu panel background |
|
|
26
|
+
| `--action-menu-popover-border` | `0.1rem solid light-dark(var(--slate-03), var(--slate-07))` | Menu panel border shorthand |
|
|
27
|
+
| `--action-menu-popover-border-radius` | `0.8rem` | Menu panel corner rounding |
|
|
28
|
+
| `--action-menu-popover-min-width` | `20rem` | Minimum menu width |
|
|
29
|
+
| `--action-menu-popover-shadow` | `0 0.4rem 1.6rem light-dark(rgba(0,0,0,0.1), rgba(0,0,0,0.4))` | Menu panel drop shadow |
|
|
30
|
+
| `--action-menu-popover-transition-duration` | `200ms` | Open/close fade duration |
|
|
31
|
+
|
|
32
|
+
### Menu items (`ActionMenuItemCore`)
|
|
33
|
+
|
|
34
|
+
| Token | Default | Controls |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| `--action-menu-item-divider` | `0.1rem solid light-dark(var(--slate-02), var(--slate-08))` | Divider line between items |
|
|
37
|
+
| `--action-menu-item-surface-hover` | `light-dark(var(--slate-01), var(--slate-09))` | Item row background on hover/focus |
|
|
38
|
+
| `--action-menu-item-text-color` | `light-dark(var(--slate-09), var(--slate-01))` | Item label and icon colour |
|
|
39
|
+
| `--action-menu-item-icon-size` | `2rem` | Left icon container size |
|
|
40
|
+
| `--action-menu-item-padding-inline` | `1.6rem` | Item horizontal padding |
|
|
41
|
+
| `--action-menu-item-padding-block` | `1.2rem` | Item vertical padding |
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Global theming — recommended approach
|
|
46
|
+
|
|
47
|
+
Create `assets/styles/setup/07.components/action-menu.css` in the consuming app and set tokens
|
|
48
|
+
on `:root`. This applies to every `ActionMenu` across the site.
|
|
49
|
+
|
|
50
|
+
```css
|
|
51
|
+
/* assets/styles/setup/07.components/action-menu.css */
|
|
52
|
+
:root {
|
|
53
|
+
--action-menu-trigger-border-radius: 0.4rem;
|
|
54
|
+
--action-menu-trigger-surface-hover: var(--brand-surface-subtle);
|
|
55
|
+
--action-menu-trigger-icon-color: var(--brand-text-muted);
|
|
56
|
+
|
|
57
|
+
--action-menu-popover-background: var(--brand-surface);
|
|
58
|
+
--action-menu-popover-border: 0.1rem solid var(--brand-border);
|
|
59
|
+
--action-menu-popover-border-radius: 0.6rem;
|
|
60
|
+
--action-menu-popover-shadow: 0 0.8rem 2.4rem rgba(0, 0, 0, 0.15);
|
|
61
|
+
|
|
62
|
+
--action-menu-item-surface-hover: var(--brand-surface-subtle);
|
|
63
|
+
--action-menu-item-text-color: var(--brand-text);
|
|
64
|
+
--action-menu-item-divider: 0.1rem solid var(--brand-border);
|
|
65
|
+
--action-menu-block-distance: 0.6rem;
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Page-scoped overrides
|
|
72
|
+
|
|
73
|
+
Override tokens for a specific section by scoping them under the page or layout wrapper.
|
|
74
|
+
No `:deep()` is required (component styles are unscoped).
|
|
75
|
+
|
|
76
|
+
```css
|
|
77
|
+
/* In the consuming page's unscoped <style> block */
|
|
78
|
+
.admin-table {
|
|
79
|
+
.action-menu {
|
|
80
|
+
--action-menu-trigger-size: 2.8rem;
|
|
81
|
+
--action-menu-trigger-icon-size: 1.6rem;
|
|
82
|
+
--action-menu-popover-min-width: 16rem;
|
|
83
|
+
--action-menu-item-padding-block: 0.8rem;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Per-instance overrides via styleClassPassthrough
|
|
91
|
+
|
|
92
|
+
Use sparingly — prefer global or page-scoped CSS. When a single instance needs a distinct
|
|
93
|
+
visual style, pass a modifier class:
|
|
94
|
+
|
|
95
|
+
```vue
|
|
96
|
+
<ActionMenu
|
|
97
|
+
:style-class-passthrough="['danger-actions']"
|
|
98
|
+
>
|
|
99
|
+
...
|
|
100
|
+
</ActionMenu>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```css
|
|
104
|
+
.action-menu.danger-actions {
|
|
105
|
+
--action-menu-trigger-icon-color: var(--color-danger);
|
|
106
|
+
--action-menu-trigger-surface-hover: light-dark(var(--red-01), var(--red-09));
|
|
107
|
+
--action-menu-item-surface-hover: light-dark(var(--red-01), var(--red-09));
|
|
108
|
+
--action-menu-item-text-color: light-dark(var(--red-09), var(--red-01));
|
|
109
|
+
--action-menu-item-divider: 0.1rem solid light-dark(var(--red-02), var(--red-08));
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Notes
|
|
116
|
+
|
|
117
|
+
- `--action-menu-block-distance` accepts any valid `<length>`. Negative values will cause the
|
|
118
|
+
menu to overlap the trigger.
|
|
119
|
+
- The menu opens **below** the trigger and right-aligns with it by default. It flips above
|
|
120
|
+
when near the bottom of the viewport (`position-try-fallbacks: flip-block`).
|
|
121
|
+
- `--action-menu-popover-min-width` sets the floor — long labels will naturally expand the
|
|
122
|
+
menu wider. Set `width: max-content` on `.action-menu-popover` in a consumer override if
|
|
123
|
+
you want to suppress that.
|
|
124
|
+
- The `--action-menu-item-*` tokens resolve on `.action-menu-item-core` elements, so they
|
|
125
|
+
take effect even when items are used in other contexts.
|