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
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { computed, ref } from "vue";
|
|
2
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
+
import ActionMenu from "../ActionMenu.vue";
|
|
4
|
+
import ActionMenuItemCore from "../ActionMenuItemCore.vue";
|
|
5
|
+
|
|
6
|
+
// ─── Meta ─────────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
interface StoryArgs {
|
|
9
|
+
itemCount?: number;
|
|
10
|
+
label?: string;
|
|
11
|
+
styleClassPassthrough?: string | string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const meta: Meta<StoryArgs> = {
|
|
15
|
+
title: "Molecules/ActionMenu",
|
|
16
|
+
component: ActionMenu,
|
|
17
|
+
argTypes: {
|
|
18
|
+
itemCount: {
|
|
19
|
+
control: { type: "number", min: 0, max: 8, step: 1 },
|
|
20
|
+
description: "Number of items to show (story control — not a component prop)",
|
|
21
|
+
table: { category: "Content" },
|
|
22
|
+
},
|
|
23
|
+
label: {
|
|
24
|
+
control: { type: "text" },
|
|
25
|
+
description: "Accessible label for both the trigger button and the menu list (`aria-label`)",
|
|
26
|
+
table: { category: "Accessibility" },
|
|
27
|
+
},
|
|
28
|
+
styleClassPassthrough: {
|
|
29
|
+
table: { disable: true },
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
args: {
|
|
33
|
+
itemCount: 5,
|
|
34
|
+
label: "Open actions menu",
|
|
35
|
+
},
|
|
36
|
+
parameters: {
|
|
37
|
+
docs: {
|
|
38
|
+
description: {
|
|
39
|
+
component:
|
|
40
|
+
"An ellipsis trigger button that opens an anchored popover menu. " +
|
|
41
|
+
"Populate the menu via indexed `item-{n}` slots, each containing a single `ActionMenuItemCore`. " +
|
|
42
|
+
"Items can be buttons (omit `href`) or links (internal `/path` → NuxtLink, external URL → `<a>`). " +
|
|
43
|
+
"The menu closes automatically when any item is clicked. " +
|
|
44
|
+
"Set shared CSS tokens globally — see `CONSUMER-STYLING.md` in the component folder.",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export default meta;
|
|
51
|
+
type Story = StoryObj<StoryArgs>;
|
|
52
|
+
|
|
53
|
+
// ─── Shared data ──────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
const actionItems = [
|
|
56
|
+
{ label: "Edit", icon: "lucide:pencil", href: undefined },
|
|
57
|
+
{ label: "View", icon: "lucide:eye", href: undefined },
|
|
58
|
+
{ label: "Share", icon: "lucide:share-2", href: undefined },
|
|
59
|
+
{ label: "Duplicate", icon: "lucide:copy", href: undefined },
|
|
60
|
+
{ label: "Delete", icon: "lucide:trash-2", href: undefined },
|
|
61
|
+
] as const;
|
|
62
|
+
|
|
63
|
+
// ─── Stories ──────────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Default — five button actions driven by the `itemCount` control.
|
|
67
|
+
* Adjust `itemCount` in the Controls panel to see fewer items (max 5 with this
|
|
68
|
+
* data set — the extra slots simply render empty).
|
|
69
|
+
*/
|
|
70
|
+
export const Default: Story = {
|
|
71
|
+
args: {
|
|
72
|
+
itemCount: 5,
|
|
73
|
+
},
|
|
74
|
+
render: (args) => ({
|
|
75
|
+
components: { ActionMenu, ActionMenuItemCore },
|
|
76
|
+
setup() {
|
|
77
|
+
const { itemCount, ...componentArgs } = args;
|
|
78
|
+
const lastAction = ref<string | null>(null);
|
|
79
|
+
const items = computed(() =>
|
|
80
|
+
actionItems.slice(0, itemCount ?? 5).map((item, i) => ({
|
|
81
|
+
...item,
|
|
82
|
+
slotName: `item-${i}`,
|
|
83
|
+
}))
|
|
84
|
+
);
|
|
85
|
+
return { componentArgs, items, lastAction };
|
|
86
|
+
},
|
|
87
|
+
template: `
|
|
88
|
+
<div style="padding: 4rem 8rem; display: flex; flex-direction: column; align-items: flex-end; gap: 2rem;">
|
|
89
|
+
<ActionMenu v-bind="componentArgs">
|
|
90
|
+
<template v-for="item in items" :key="item.slotName" #[item.slotName]>
|
|
91
|
+
<ActionMenuItemCore :label="item.label" @click="lastAction = item.label">
|
|
92
|
+
<template #icon>
|
|
93
|
+
<Icon :name="item.icon" />
|
|
94
|
+
</template>
|
|
95
|
+
</ActionMenuItemCore>
|
|
96
|
+
</template>
|
|
97
|
+
</ActionMenu>
|
|
98
|
+
<p v-if="lastAction" style="margin: 0; font-size: 1.3rem; opacity: 0.6;">
|
|
99
|
+
Last action: <strong>{{ lastAction }}</strong>
|
|
100
|
+
</p>
|
|
101
|
+
</div>
|
|
102
|
+
`,
|
|
103
|
+
}),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Mixed items — demonstrates the three `ActionMenuItemCore` rendering modes:
|
|
108
|
+
* button (no href), internal link (/path), and external link (https://…).
|
|
109
|
+
*/
|
|
110
|
+
export const MixedItems: Story = {
|
|
111
|
+
name: "Mixed — Buttons and Links",
|
|
112
|
+
args: {
|
|
113
|
+
label: "Record actions",
|
|
114
|
+
},
|
|
115
|
+
render: (args) => ({
|
|
116
|
+
components: { ActionMenu, ActionMenuItemCore },
|
|
117
|
+
setup() {
|
|
118
|
+
return { args };
|
|
119
|
+
},
|
|
120
|
+
template: `
|
|
121
|
+
<div style="padding: 4rem 8rem; display: flex; justify-content: flex-end;">
|
|
122
|
+
<ActionMenu v-bind="args">
|
|
123
|
+
<template #item-0>
|
|
124
|
+
<ActionMenuItemCore label="Edit record" @click="() => {}">
|
|
125
|
+
<template #icon><Icon name="lucide:pencil" /></template>
|
|
126
|
+
</ActionMenuItemCore>
|
|
127
|
+
</template>
|
|
128
|
+
<template #item-1>
|
|
129
|
+
<ActionMenuItemCore label="View full profile" href="/profile/123">
|
|
130
|
+
<template #icon><Icon name="lucide:user" /></template>
|
|
131
|
+
</ActionMenuItemCore>
|
|
132
|
+
</template>
|
|
133
|
+
<template #item-2>
|
|
134
|
+
<ActionMenuItemCore label="Open in new tab" href="https://example.com">
|
|
135
|
+
<template #icon><Icon name="lucide:external-link" /></template>
|
|
136
|
+
</ActionMenuItemCore>
|
|
137
|
+
</template>
|
|
138
|
+
<template #item-3>
|
|
139
|
+
<ActionMenuItemCore label="Delete record" @click="() => {}">
|
|
140
|
+
<template #icon><Icon name="lucide:trash-2" /></template>
|
|
141
|
+
</ActionMenuItemCore>
|
|
142
|
+
</template>
|
|
143
|
+
</ActionMenu>
|
|
144
|
+
</div>
|
|
145
|
+
`,
|
|
146
|
+
}),
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* No icons — items with label and arrow only. Works without the icon slot.
|
|
151
|
+
*/
|
|
152
|
+
export const NoIcons: Story = {
|
|
153
|
+
name: "No Icons",
|
|
154
|
+
args: {},
|
|
155
|
+
render: (args) => ({
|
|
156
|
+
components: { ActionMenu, ActionMenuItemCore },
|
|
157
|
+
setup() {
|
|
158
|
+
return { args };
|
|
159
|
+
},
|
|
160
|
+
template: `
|
|
161
|
+
<div style="padding: 4rem 8rem; display: flex; justify-content: flex-end;">
|
|
162
|
+
<ActionMenu v-bind="args">
|
|
163
|
+
<template #item-0>
|
|
164
|
+
<ActionMenuItemCore label="Approve" @click="() => {}" />
|
|
165
|
+
</template>
|
|
166
|
+
<template #item-1>
|
|
167
|
+
<ActionMenuItemCore label="Request changes" @click="() => {}" />
|
|
168
|
+
</template>
|
|
169
|
+
<template #item-2>
|
|
170
|
+
<ActionMenuItemCore label="Reject" @click="() => {}" />
|
|
171
|
+
</template>
|
|
172
|
+
</ActionMenu>
|
|
173
|
+
</div>
|
|
174
|
+
`,
|
|
175
|
+
}),
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* In context — the most realistic usage: menu anchored at the trailing edge
|
|
180
|
+
* of a data row inside a card. Shows how the trigger sits flush within a
|
|
181
|
+
* compact layout without adding height.
|
|
182
|
+
*/
|
|
183
|
+
export const InContext: Story = {
|
|
184
|
+
name: "In Context — Data Row",
|
|
185
|
+
args: {
|
|
186
|
+
label: "User actions",
|
|
187
|
+
},
|
|
188
|
+
render: (args) => ({
|
|
189
|
+
components: { ActionMenu, ActionMenuItemCore },
|
|
190
|
+
setup() {
|
|
191
|
+
const users = [
|
|
192
|
+
{ name: "Alex Morgan", email: "alex@example.com", role: "Admin" },
|
|
193
|
+
{ name: "Sam Patel", email: "sam@example.com", role: "Editor" },
|
|
194
|
+
{ name: "Jordan Clarke", email: "jordan@example.com", role: "Viewer" },
|
|
195
|
+
];
|
|
196
|
+
return { args, users };
|
|
197
|
+
},
|
|
198
|
+
template: `
|
|
199
|
+
<div style="padding: 4rem; max-width: 56rem; margin: 0 auto;">
|
|
200
|
+
<div
|
|
201
|
+
v-for="user in users"
|
|
202
|
+
:key="user.email"
|
|
203
|
+
style="
|
|
204
|
+
display: flex;
|
|
205
|
+
align-items: center;
|
|
206
|
+
gap: 1.2rem;
|
|
207
|
+
padding: 1.2rem 1.6rem;
|
|
208
|
+
border-bottom: 0.1rem solid light-dark(#e5e7eb, #374151);
|
|
209
|
+
"
|
|
210
|
+
>
|
|
211
|
+
<div
|
|
212
|
+
style="
|
|
213
|
+
width: 3.6rem;
|
|
214
|
+
height: 3.6rem;
|
|
215
|
+
border-radius: 50%;
|
|
216
|
+
background: light-dark(#e5e7eb, #374151);
|
|
217
|
+
display: grid;
|
|
218
|
+
place-items: center;
|
|
219
|
+
font-size: 1.4rem;
|
|
220
|
+
font-weight: 600;
|
|
221
|
+
flex-shrink: 0;
|
|
222
|
+
"
|
|
223
|
+
>
|
|
224
|
+
{{ user.name[0] }}
|
|
225
|
+
</div>
|
|
226
|
+
<div style="flex: 1; min-width: 0;">
|
|
227
|
+
<p style="margin: 0; font-size: 1.4rem; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
|
228
|
+
{{ user.name }}
|
|
229
|
+
</p>
|
|
230
|
+
<p style="margin: 0; font-size: 1.2rem; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
|
231
|
+
{{ user.email }}
|
|
232
|
+
</p>
|
|
233
|
+
</div>
|
|
234
|
+
<span style="font-size: 1.2rem; opacity: 0.5; white-space: nowrap;">{{ user.role }}</span>
|
|
235
|
+
<ActionMenu v-bind="args">
|
|
236
|
+
<template #item-0>
|
|
237
|
+
<ActionMenuItemCore label="Edit user" @click="() => {}">
|
|
238
|
+
<template #icon><Icon name="lucide:pencil" /></template>
|
|
239
|
+
</ActionMenuItemCore>
|
|
240
|
+
</template>
|
|
241
|
+
<template #item-1>
|
|
242
|
+
<ActionMenuItemCore label="View profile" :href="'/users/' + user.email">
|
|
243
|
+
<template #icon><Icon name="lucide:user" /></template>
|
|
244
|
+
</ActionMenuItemCore>
|
|
245
|
+
</template>
|
|
246
|
+
<template #item-2>
|
|
247
|
+
<ActionMenuItemCore label="Reset password" @click="() => {}">
|
|
248
|
+
<template #icon><Icon name="lucide:key" /></template>
|
|
249
|
+
</ActionMenuItemCore>
|
|
250
|
+
</template>
|
|
251
|
+
<template #item-3>
|
|
252
|
+
<ActionMenuItemCore label="Remove user" @click="() => {}">
|
|
253
|
+
<template #icon><Icon name="lucide:user-minus" /></template>
|
|
254
|
+
</ActionMenuItemCore>
|
|
255
|
+
</template>
|
|
256
|
+
</ActionMenu>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
`,
|
|
260
|
+
}),
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* ActionMenuItemCore — standalone view of the child component in its three
|
|
265
|
+
* rendering modes (button, internal link, external link). Not an interactive
|
|
266
|
+
* menu — use this story to style and test individual item rows.
|
|
267
|
+
*/
|
|
268
|
+
export const ItemCoreStandalone: Story = {
|
|
269
|
+
name: "ActionMenuItemCore — Standalone",
|
|
270
|
+
args: {},
|
|
271
|
+
render: () => ({
|
|
272
|
+
components: { ActionMenuItemCore },
|
|
273
|
+
setup() {
|
|
274
|
+
const lastClick = ref<string | null>(null);
|
|
275
|
+
return { lastClick };
|
|
276
|
+
},
|
|
277
|
+
template: `
|
|
278
|
+
<div style="padding: 4rem; max-width: 32rem; margin: 0 auto;">
|
|
279
|
+
<p style="margin: 0 0 1.2rem; font-size: 1.2rem; opacity: 0.5; text-transform: uppercase; letter-spacing: 0.08em;">Button items</p>
|
|
280
|
+
<div style="border: 0.1rem solid light-dark(#e5e7eb, #374151); border-radius: 0.8rem; overflow: hidden;">
|
|
281
|
+
<ActionMenuItemCore
|
|
282
|
+
label="Edit"
|
|
283
|
+
style="border-bottom: 0.1rem solid light-dark(#e5e7eb, #374151);"
|
|
284
|
+
@click="lastClick = 'Edit'"
|
|
285
|
+
>
|
|
286
|
+
<template #icon><Icon name="lucide:pencil" /></template>
|
|
287
|
+
</ActionMenuItemCore>
|
|
288
|
+
<ActionMenuItemCore
|
|
289
|
+
label="Duplicate"
|
|
290
|
+
style="border-bottom: 0.1rem solid light-dark(#e5e7eb, #374151);"
|
|
291
|
+
@click="lastClick = 'Duplicate'"
|
|
292
|
+
>
|
|
293
|
+
<template #icon><Icon name="lucide:copy" /></template>
|
|
294
|
+
</ActionMenuItemCore>
|
|
295
|
+
<ActionMenuItemCore
|
|
296
|
+
label="Delete"
|
|
297
|
+
@click="lastClick = 'Delete'"
|
|
298
|
+
>
|
|
299
|
+
<template #icon><Icon name="lucide:trash-2" /></template>
|
|
300
|
+
</ActionMenuItemCore>
|
|
301
|
+
</div>
|
|
302
|
+
|
|
303
|
+
<p style="margin: 2.4rem 0 1.2rem; font-size: 1.2rem; opacity: 0.5; text-transform: uppercase; letter-spacing: 0.08em;">Link items</p>
|
|
304
|
+
<div style="border: 0.1rem solid light-dark(#e5e7eb, #374151); border-radius: 0.8rem; overflow: hidden;">
|
|
305
|
+
<ActionMenuItemCore
|
|
306
|
+
label="Internal link (/about)"
|
|
307
|
+
href="/about"
|
|
308
|
+
style="border-bottom: 0.1rem solid light-dark(#e5e7eb, #374151);"
|
|
309
|
+
>
|
|
310
|
+
<template #icon><Icon name="lucide:arrow-up-right" /></template>
|
|
311
|
+
</ActionMenuItemCore>
|
|
312
|
+
<ActionMenuItemCore
|
|
313
|
+
label="External link"
|
|
314
|
+
href="https://example.com"
|
|
315
|
+
>
|
|
316
|
+
<template #icon><Icon name="lucide:external-link" /></template>
|
|
317
|
+
</ActionMenuItemCore>
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
<p v-if="lastClick" style="margin: 1.6rem 0 0; font-size: 1.3rem; opacity: 0.6;">
|
|
321
|
+
Clicked: <strong>{{ lastClick }}</strong>
|
|
322
|
+
</p>
|
|
323
|
+
</div>
|
|
324
|
+
`,
|
|
325
|
+
}),
|
|
326
|
+
};
|