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.
Files changed (34) hide show
  1. package/.claude/settings.json +8 -27
  2. package/.claude/settings.local.json +24 -52
  3. package/.claude/skills/components/action-menu.md +141 -0
  4. package/.claude/skills/components/stepper-list.md +52 -18
  5. package/.claude/skills/css-nesting-conventions.md +121 -0
  6. package/.claude/skills/index.md +3 -0
  7. package/.claude/skills/pull-request-description.md +48 -0
  8. package/.claude/skills/testing-add-unit-test.md +40 -2
  9. package/README.md +26 -5
  10. package/app/components/02.molecules/action-menu/ActionMenu.vue +218 -0
  11. package/app/components/02.molecules/action-menu/ActionMenuItemCore.vue +123 -0
  12. package/app/components/02.molecules/action-menu/CONSUMER-STYLING.md +125 -0
  13. package/app/components/02.molecules/action-menu/stories/ActionMenu.stories.ts +326 -0
  14. package/app/components/02.molecules/action-menu/tests/ActionMenu.spec.ts +458 -0
  15. package/app/components/02.molecules/action-menu/tests/ActionMenuItemCore.spec.ts +199 -0
  16. package/app/components/02.molecules/action-menu/tests/__snapshots__/ActionMenu.spec.ts.snap +28 -0
  17. package/app/components/02.molecules/action-menu/tests/__snapshots__/ActionMenuItemCore.spec.ts.snap +27 -0
  18. package/app/components/02.molecules/display-chip/tests/DisplayChip.spec.ts +5 -1
  19. package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +2 -1
  20. package/app/components/02.molecules/stepper-list/CONSUMER-STYLING.md +138 -0
  21. package/app/components/02.molecules/stepper-list/StepperList.vue +50 -24
  22. package/app/components/03.organisms/image-galleries/slider-gallery/tests/SliderGallery.spec.ts +5 -1
  23. package/app/components/05.forms/input-button/InputButtonCore.vue +1 -1
  24. package/app/components/carousels/tests/CarouselFlip.spec.ts +1 -0
  25. package/app/components/responsive-header/NavigationItems.vue +302 -81
  26. package/app/components/responsive-header/ResponsiveHeader.vue +360 -56
  27. package/app/layouts/default.vue +43 -352
  28. package/app/layouts/site-navigation-demo.vue +36 -34
  29. package/app/pages/page-hero-highlights.vue +3 -3
  30. package/app/pages/ui/scroll-reveal-image.vue +3 -3
  31. package/app/pages/ui/simple-grid.vue +9 -20
  32. package/modules/colour-scheme.ts +1 -1
  33. package/nuxt.config.ts +11 -2
  34. 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
+ };