srcdev-nuxt-components 9.6.3 → 9.6.5

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 (45) hide show
  1. package/.claude/rules/consumer/vue-no-self-closing.md +3 -2
  2. package/.claude/skills/component-compliance-checklist.md +17 -3
  3. package/.claude/skills/component-inline-action-button.md +33 -2
  4. package/.claude/skills/components/cookie-consent-banner.md +9 -6
  5. package/.claude/skills/components/input-copy.md +47 -100
  6. package/.claude/skills/components/progress-tracker.md +140 -0
  7. package/.claude/skills/components/stepper-list.md +10 -3
  8. package/.claude/skills/components/tabbed-content.md +4 -0
  9. package/.claude/skills/composable-cookie-consent.md +1 -1
  10. package/.claude/skills/index.md +4 -3
  11. package/.claude/skills/storybook-add-story.md +14 -0
  12. package/.claude/skills/testing-add-unit-test.md +19 -0
  13. package/.claude/skills/theming-component-token-pattern.md +1 -1
  14. package/.vscode/srcdev-component-cookie-consent-banner.code-snippets +7 -5
  15. package/.vscode/srcdev-component-input-copy.code-snippets +24 -42
  16. package/.vscode/srcdev-component-progress-tracker.code-snippets +45 -0
  17. package/.vscode/srcdev-component-stepper-list.code-snippets +3 -2
  18. package/.vscode/srcdev-component-tabbed-content.code-snippets +1 -0
  19. package/app/components/01.atoms/tabbed-content/CONSUMER-STYLING.md +5 -1
  20. package/app/components/01.atoms/tabbed-content/TabbedContent.vue +2 -2
  21. package/app/components/01.atoms/tabbed-content/stories/TabbedContent.stories.ts +26 -0
  22. package/app/components/02.molecules/contact-section/tests/__snapshots__/ContactSection.spec.ts.snap +28 -28
  23. package/app/components/02.molecules/cookie-consent-banner/CONSUMER-STYLING.md +13 -4
  24. package/app/components/02.molecules/cookie-consent-banner/CookieConsentBanner.vue +23 -14
  25. package/app/components/02.molecules/cookie-consent-banner/stories/CookieConsentBanner.stories.ts +106 -5
  26. package/app/components/02.molecules/cookie-consent-banner/tests/CookieConsentBanner.spec.ts +33 -11
  27. package/app/components/02.molecules/input-copy/CONSUMER-STYLING.md +82 -124
  28. package/app/components/02.molecules/input-copy/InputCopy.vue +110 -85
  29. package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +204 -47
  30. package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +147 -171
  31. package/app/components/02.molecules/progress-tracker/CONSUMER-STYLING.md +116 -0
  32. package/app/components/02.molecules/progress-tracker/ProgressTracker.vue +275 -0
  33. package/app/components/02.molecules/progress-tracker/stories/ProgressTracker.stories.ts +424 -0
  34. package/app/components/02.molecules/progress-tracker/tests/ProgressTracker.spec.ts +253 -0
  35. package/app/components/02.molecules/progress-tracker/tests/__snapshots__/ProgressTracker.spec.ts.snap +46 -0
  36. package/app/components/02.molecules/stepper-list/CONSUMER-STYLING.md +42 -7
  37. package/app/components/02.molecules/stepper-list/StepperList.vue +42 -19
  38. package/app/components/02.molecules/stepper-list/stories/StepperList.stories.ts +288 -2
  39. package/app/components/02.molecules/stepper-list/tests/StepperList.spec.ts +161 -34
  40. package/app/components/02.molecules/stepper-list/tests/__snapshots__/StepperList.spec.ts.snap +40 -40
  41. package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +7 -7
  42. package/app/types/components/cookie-consent-banner.d.ts +2 -0
  43. package/app/types/components/index.ts +1 -0
  44. package/app/types/components/progress-tracker.d.ts +6 -0
  45. package/package.json +1 -1
@@ -1,9 +1,26 @@
1
+ import { computed } from "vue";
1
2
  import StepperList from "../StepperList.vue";
2
3
  import type { Meta, StoryObj } from "@nuxtjs/storybook";
3
4
 
5
+ // Links in the canvas open in the Storybook manager (target="_top"), not inside the preview iframe.
6
+ const storyNoteStyle = "margin: 0 0 1.6rem; font-size: 1.4rem;";
7
+ const parentStoriesNote = `
8
+ <p style="${storyNoteStyle}">
9
+ StepperList in real use: ServiceDetail renders its process steps from CMS data (<a href="/?path=/story/organisms-services-service-detail--default" target="_top">ServiceDetail</a> story), and ProgressTracker adds done, current and upcoming states, a progress header and a focus card (<a href="/?path=/story/molecules-progresstracker--default" target="_top">ProgressTracker</a> story).
10
+ </p>
11
+ `;
12
+
4
13
  const meta: Meta<typeof StepperList> = {
5
14
  title: "Molecules/Lists/StepperList",
6
15
  component: StepperList,
16
+ parameters: {
17
+ docs: {
18
+ description: {
19
+ component:
20
+ "A list of steps, each with an indicator and a content area. `itemCount` sets how many rows render; fill them with the zero-indexed `#item-{n}` slots. Each row shows an auto-numbered counter bubble (`indicatorVariant`: disc, circle or square; `indicatorSize`) unless an `#indicator-{n}` slot replaces it. `connected` draws a line between indicators. Used by [ServiceDetail](?path=/story/organisms-services-service-detail--default) for its process steps and by [ProgressTracker](?path=/story/molecules-progresstracker--default), which adds step states, a progress header and a focus card.",
21
+ },
22
+ },
23
+ },
7
24
  argTypes: {
8
25
  tag: {
9
26
  control: { type: "select" },
@@ -30,7 +47,7 @@ const meta: Meta<typeof StepperList> = {
30
47
  },
31
48
  connected: {
32
49
  control: "boolean",
33
- description: "Whether to show connectors between indicators (only visible in supported browsers)",
50
+ description: "Draws a vertical line between indicators, positioned from measured indicator sizes",
34
51
  },
35
52
  styleClassPassthrough: {
36
53
  control: "object",
@@ -66,6 +83,7 @@ export const Default: Story = {
66
83
  return { args };
67
84
  },
68
85
  template: `
86
+ ${parentStoriesNote}
69
87
  <StepperList v-bind="args">
70
88
  <template #item-0><p class="page-body-normal">Plan your project goals and define what success looks like</p></template>
71
89
  <template #item-1><p class="page-body-normal">Set up your development environment</p></template>
@@ -90,6 +108,7 @@ export const OrderedList: Story = {
90
108
  return { args };
91
109
  },
92
110
  template: `
111
+ ${parentStoriesNote}
93
112
  <StepperList v-bind="args">
94
113
  <template #item-0><p class="page-body-normal">Preheat the oven to 180°C</p></template>
95
114
  <template #item-1><p class="page-body-normal">Sift together the flour, baking powder, and a pinch of salt into a large bowl</p></template>
@@ -115,6 +134,7 @@ export const WithCustomIndicators: Story = {
115
134
  return { args };
116
135
  },
117
136
  template: `
137
+ ${parentStoriesNote}
118
138
  <StepperList v-bind="args">
119
139
  <template #indicator-0>
120
140
  <svg class="indicator-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -169,6 +189,7 @@ export const MixedIndicators: Story = {
169
189
  return { args };
170
190
  },
171
191
  template: `
192
+ ${parentStoriesNote}
172
193
  <StepperList v-bind="args">
173
194
  <template #indicator-0>
174
195
  <svg class="indicator-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -208,6 +229,7 @@ export const AlignmentCenter: Story = {
208
229
  return { args };
209
230
  },
210
231
  template: `
232
+ ${parentStoriesNote}
211
233
  <StepperList v-bind="args">
212
234
  <template #item-0><p class="page-body-normal">Connect your data source</p></template>
213
235
  <template #item-1><p class="page-body-normal">Configure sync settings</p></template>
@@ -219,6 +241,62 @@ export const AlignmentCenter: Story = {
219
241
  }),
220
242
  };
221
243
 
244
+ type ConnectorGapArgs = InstanceType<typeof StepperList>["$props"] & { connectorGap: string };
245
+
246
+ export const ConnectorGap: StoryObj<ConnectorGapArgs> = {
247
+ name: "Connector Gap",
248
+ argTypes: {
249
+ connectorGap: {
250
+ name: "Connector gap",
251
+ control: "select",
252
+ options: ["0", "0.2rem", "0.4rem", "0.6rem", "0.8rem", "1.2rem"],
253
+ description:
254
+ "**Story control, not a prop.** Sets the `--stepper-list-connector-gap` CSS token so you can try it here. " +
255
+ "To use it in an app, set the token in your own CSS, e.g. `.how-it-works { --stepper-list-connector-gap: 0.6rem; }`. " +
256
+ "`0` (the default) makes the line touch both indicators.",
257
+ table: { category: "CSS tokens (story only, set in your CSS)", defaultValue: { summary: "0" } },
258
+ },
259
+ },
260
+ args: {
261
+ tag: "ol",
262
+ itemCount: 5,
263
+ connected: true,
264
+ indicatorAlignment: "center",
265
+ connectorGap: "0.6rem",
266
+ },
267
+ parameters: {
268
+ docs: {
269
+ description: {
270
+ story:
271
+ "`--stepper-list-connector-gap` leaves a space between each end of the connector line and the indicators it joins. It defaults to `0`, where the line touches both indicators. The Connector gap control sets that CSS token on a wrapper; it isn't a prop.",
272
+ },
273
+ },
274
+ },
275
+ render: (args) => ({
276
+ components: { StepperList },
277
+ setup() {
278
+ const componentArgs = computed(() => {
279
+ const { connectorGap: _connectorGap, ...rest } = args;
280
+ return rest;
281
+ });
282
+ const tokenStyles = computed(() => ({ "--stepper-list-connector-gap": args.connectorGap }));
283
+ return { componentArgs, tokenStyles };
284
+ },
285
+ template: `
286
+ ${parentStoriesNote}
287
+ <div :style="tokenStyles">
288
+ <StepperList v-bind="componentArgs">
289
+ <template #item-0><p class="page-body-normal">Connect your data source</p></template>
290
+ <template #item-1><p class="page-body-normal">Configure sync settings</p></template>
291
+ <template #item-2><p class="page-body-normal">Map your fields</p></template>
292
+ <template #item-3><p class="page-body-normal">Run a test import to check everything looks right</p></template>
293
+ <template #item-4><p class="page-body-normal">Publish</p></template>
294
+ </StepperList>
295
+ </div>
296
+ `,
297
+ }),
298
+ };
299
+
222
300
  /** Disc variant — filled circle with solid background (default). */
223
301
  export const VariantDisc: Story = {
224
302
  name: "Variant — Disc",
@@ -233,6 +311,7 @@ export const VariantDisc: Story = {
233
311
  return { args };
234
312
  },
235
313
  template: `
314
+ ${parentStoriesNote}
236
315
  <StepperList v-bind="args">
237
316
  <template #item-0><p class="page-body-normal">Research and define requirements with stakeholders</p></template>
238
317
  <template #item-1><p class="page-body-normal">Wireframe key user journeys</p></template>
@@ -258,6 +337,7 @@ export const VariantCircle: Story = {
258
337
  return { args };
259
338
  },
260
339
  template: `
340
+ ${parentStoriesNote}
261
341
  <StepperList v-bind="args">
262
342
  <template #item-0><p class="page-body-normal">Research and define requirements with stakeholders</p></template>
263
343
  <template #item-1><p class="page-body-normal">Wireframe key user journeys</p></template>
@@ -283,6 +363,7 @@ export const VariantSquare: Story = {
283
363
  return { args };
284
364
  },
285
365
  template: `
366
+ ${parentStoriesNote}
286
367
  <StepperList v-bind="args">
287
368
  <template #item-0><p class="page-body-normal">Research and define requirements with stakeholders</p></template>
288
369
  <template #item-1><p class="page-body-normal">Wireframe key user journeys</p></template>
@@ -310,6 +391,7 @@ export const CustomIndicatorSize: Story = {
310
391
  return { args };
311
392
  },
312
393
  template: `
394
+ ${parentStoriesNote}
313
395
  <StepperList v-bind="args">
314
396
  <template #item-0><p class="page-body-normal">Create your account</p></template>
315
397
  <template #item-1><p class="page-body-normal">Verify your email and complete your profile with a photo, bio, and preferred contact details</p></template>
@@ -333,6 +415,7 @@ export const RichContent: Story = {
333
415
  return { args };
334
416
  },
335
417
  template: `
418
+ ${parentStoriesNote}
336
419
  <StepperList v-bind="args">
337
420
  <template #item-0>
338
421
  <div>
@@ -381,6 +464,7 @@ export const SingleItem: Story = {
381
464
  return { args };
382
465
  },
383
466
  template: `
467
+ ${parentStoriesNote}
384
468
  <StepperList v-bind="args">
385
469
  <template #item-0><p class="page-body-normal">The only item</p></template>
386
470
  </StepperList>
@@ -398,7 +482,7 @@ export const Empty: Story = {
398
482
  setup() {
399
483
  return { args };
400
484
  },
401
- template: `<StepperList v-bind="args" />`,
485
+ template: `${parentStoriesNote}<StepperList v-bind="args" />`,
402
486
  }),
403
487
  };
404
488
 
@@ -414,6 +498,7 @@ export const WithStyleClassPassthrough: Story = {
414
498
  return { args };
415
499
  },
416
500
  template: `
501
+ ${parentStoriesNote}
417
502
  <StepperList v-bind="args">
418
503
  <template #item-0><p class="page-body-normal">First item</p></template>
419
504
  <template #item-1><p class="page-body-normal">Second item</p></template>
@@ -421,3 +506,204 @@ export const WithStyleClassPassthrough: Story = {
421
506
  `,
422
507
  }),
423
508
  };
509
+
510
+ // ─── Styled demo ──────────────────────────────────────────────────────────────
511
+
512
+ type TimelineState = "done" | "current" | "upcoming";
513
+
514
+ const timelineStateLabels: Record<TimelineState, string> = {
515
+ done: "Done",
516
+ current: "In progress",
517
+ upcoming: "Up next",
518
+ };
519
+
520
+ const timelineSteps: { state: TimelineState; title: string; body: string }[] = [
521
+ { state: "done", title: "Create your account", body: "Enter your email and choose a password to get started." },
522
+ {
523
+ state: "current",
524
+ title: "Verify your email",
525
+ body: "Click the link we sent to your inbox. If you don't see it, check your spam folder: it should arrive within a few minutes.",
526
+ },
527
+ { state: "upcoming", title: "Choose a plan", body: "Select from Starter, Pro, or Enterprise depending on your needs." },
528
+ { state: "upcoming", title: "Invite your team", body: "Add colleagues by email so they can collaborate on your workspace." },
529
+ { state: "upcoming", title: "Start building", body: "You're all set. Dive into the dashboard and start your first project." },
530
+ ];
531
+
532
+ const timelineCss = `
533
+ .stepper-timeline-demo {
534
+ background: #16171b;
535
+ color: #e8e9ec;
536
+ padding: 4rem 3.2rem;
537
+ border-radius: 1.2rem;
538
+ }
539
+
540
+ .stepper-list.stepper-timeline {
541
+ --stepper-list-gap: 3.2rem;
542
+ --stepper-list-padding-block: 2rem;
543
+ --stepper-list-connector-width: 0.2rem;
544
+ --stepper-list-connector-color: transparent;
545
+ --stepper-list-connector-image: repeating-linear-gradient(to bottom, #3a3d45 0 0.2rem, transparent 0.2rem 0.6rem);
546
+ margin: 0;
547
+
548
+ .stepper-list-item:has(.stepper-timeline-marker.is-done) {
549
+ --stepper-list-connector-image: repeating-linear-gradient(to bottom, #00c8d7 0 0.4rem, transparent 0.4rem 0.7rem);
550
+ }
551
+
552
+ .stepper-timeline-marker {
553
+ display: grid;
554
+ place-items: center;
555
+ inline-size: 3.2rem;
556
+ block-size: 3.2rem;
557
+ margin-block-start: -0.6rem;
558
+
559
+ &::before {
560
+ content: "";
561
+ inline-size: 2.2rem;
562
+ block-size: 2.2rem;
563
+ rotate: 45deg;
564
+ border: 0.2rem solid #4a4d55;
565
+ background: transparent;
566
+ }
567
+
568
+ &.is-done::before {
569
+ border-color: #00c8d7;
570
+ background: #00c8d7;
571
+ }
572
+
573
+ &.is-current::before {
574
+ border-color: #00c8d7;
575
+ background: #16171b;
576
+ box-shadow: 0 0 0 0.4rem rgb(0 200 215 / 0.15), 0 0 1.6rem 0.2rem rgb(0 200 215 / 0.45);
577
+ }
578
+ }
579
+
580
+ .stepper-timeline-eyebrow {
581
+ margin: 0 0 0.6rem;
582
+ font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
583
+ font-size: var(--step-4);
584
+ font-weight: 600;
585
+ letter-spacing: 0.16em;
586
+ text-transform: uppercase;
587
+ color: #8a8d96;
588
+ }
589
+
590
+ .stepper-timeline-title {
591
+ margin: 0 0 0.8rem;
592
+ font-size: 2.2rem;
593
+ font-weight: 600;
594
+ line-height: 1.2;
595
+ color: #ffffff;
596
+ }
597
+
598
+ .stepper-timeline-body {
599
+ margin: 0;
600
+ font-size: 1.6rem;
601
+ line-height: 1.5;
602
+ color: #a6a9b2;
603
+ }
604
+
605
+ .stepper-list-item:has(.is-current) .stepper-timeline-eyebrow {
606
+ color: #00c8d7;
607
+ }
608
+
609
+ .stepper-list-item:has(.is-upcoming) .stepper-list-content {
610
+ opacity: 0.45;
611
+ }
612
+ }
613
+ `;
614
+
615
+ export const TimelineDemo: Story = {
616
+ name: "Timeline (styled demo)",
617
+ args: {
618
+ tag: "ol",
619
+ itemCount: timelineSteps.length,
620
+ connected: true,
621
+ indicatorAlignment: "top",
622
+ indicatorSize: "3.2rem",
623
+ styleClassPassthrough: ["stepper-timeline"],
624
+ },
625
+ parameters: {
626
+ docs: {
627
+ description: {
628
+ story:
629
+ "A consumer-styled timeline built only from public tokens and slots, no extra props. Diamond markers are rotated squares in the `#indicator-{n}` slots, each with a state class (`is-done`, `is-current`, `is-upcoming`). `.stepper-list-item:has(.is-done)` swaps that item's connector to a cyan dashed line through `--stepper-list-connector-image`; the rest stay grey and dotted. The current step's eyebrow turns cyan and upcoming steps are dimmed. The state is also written in each eyebrow, so it never depends on colour alone. See CONSUMER-STYLING.md, \"Recipe: per-step states and dashed connectors\".",
630
+ },
631
+ },
632
+ },
633
+ render: (args) => ({
634
+ components: { StepperList },
635
+ setup() {
636
+ return { args, timelineSteps, timelineStateLabels, timelineCss };
637
+ },
638
+ template: `
639
+ ${parentStoriesNote}
640
+ <div class="stepper-timeline-demo">
641
+ <component :is="'style'">{{ timelineCss }}</component>
642
+ <StepperList v-bind="args">
643
+ <template v-for="(step, i) in timelineSteps" :key="'indicator-' + i" #[\`indicator-\${i}\`]>
644
+ <span class="stepper-timeline-marker" :class="'is-' + step.state" aria-hidden="true"></span>
645
+ </template>
646
+ <template v-for="(step, i) in timelineSteps" :key="'item-' + i" #[\`item-\${i}\`]>
647
+ <p class="stepper-timeline-eyebrow">Step {{ String(i + 1).padStart(2, "0") }} · {{ timelineStateLabels[step.state] }}</p>
648
+ <h3 class="stepper-timeline-title" :aria-current="step.state === 'current' ? 'step' : undefined">{{ step.title }}</h3>
649
+ <p class="stepper-timeline-body">{{ step.body }}</p>
650
+ </template>
651
+ </StepperList>
652
+ </div>
653
+ `,
654
+ }),
655
+ };
656
+
657
+ // ─── Stress test ──────────────────────────────────────────────────────────────
658
+
659
+ const unbroken = "Haarpflegebehandlungsterminvereinbarungsbestätigungsschreiben".repeat(3);
660
+ const longGerman =
661
+ "Bitte vereinbaren Sie vor der ersten Behandlung einen ausführlichen Beratungstermin, damit wir Ihre Haarstruktur, Ihre bisherigen Färbungen und Ihre Wünsche gemeinsam besprechen können.";
662
+ const longUrl = "https://example.com/a/very/long/url/without/any/spaces/that/keeps/going/and/going/and/going";
663
+
664
+ export const StressTest: Story = {
665
+ name: "Stress Test (Worst-Case Data)",
666
+ args: {
667
+ tag: "ol",
668
+ itemCount: 12,
669
+ connected: true,
670
+ indicatorVariant: "circle",
671
+ },
672
+ parameters: {
673
+ docs: {
674
+ description: {
675
+ story:
676
+ "Worst-case content in every row. Check that: unbroken strings and the long URL wrap inside the content column instead of widening the list; rows 10 to 12 (no slots provided) render as numbered rows with an empty content area; the two-digit counters still fit their bubble; the HTML-like text renders as text; the RTL row reads right to left; the wide custom indicator in row 2 still has the connector line running from its centre; and the second list (fractional `itemCount` 2.9) renders 2 rows rather than none.",
677
+ },
678
+ story: { inline: false, height: "1400px" },
679
+ },
680
+ },
681
+ render: (args) => ({
682
+ components: { StepperList },
683
+ setup() {
684
+ return { args, unbroken, longGerman, longUrl };
685
+ },
686
+ template: `
687
+ ${parentStoriesNote}
688
+ <div style="max-inline-size: 48rem;">
689
+ <StepperList v-bind="args">
690
+ <template #item-0><p class="page-body-normal">{{ unbroken }}</p></template>
691
+ <template #indicator-1><span style="white-space: nowrap; padding-inline: 0.8rem; border: 2px solid currentColor; border-radius: 100vw;">Step 2 of 12 ✨</span></template>
692
+ <template #item-1><p class="page-body-normal">{{ longGerman }} {{ longGerman }}</p></template>
693
+ <template #item-2><p class="page-body-normal"><a :href="longUrl">{{ longUrl }}</a></p></template>
694
+ <template #item-3><p class="page-body-normal">🦄💇‍♀️ Emoji first, then text</p></template>
695
+ <template #item-4><p class="page-body-normal" dir="rtl">مرحبا بكم في صالوننا، نحن نقدم جميع خدمات الشعر</p></template>
696
+ <template #item-5><p class="page-body-normal">&lt;script&gt;alert(1)&lt;/script&gt; renders as text</p></template>
697
+ <template #item-6><p class="page-body-normal">A</p></template>
698
+ <template #item-7></template>
699
+ <template #item-8><p class="page-body-normal">{{ longGerman }}</p><p class="page-body-normal">{{ unbroken }}</p></template>
700
+ </StepperList>
701
+ <StepperList :item-count="2.9" indicator-variant="square" style="margin-block-start: 3.2rem;">
702
+ <template #item-0><p class="page-body-normal">Fractional itemCount: first row</p></template>
703
+ <template #item-1><p class="page-body-normal">Second row (a third would be wrong)</p></template>
704
+ <template #item-2><p class="page-body-normal">Third row: should not render</p></template>
705
+ </StepperList>
706
+ </div>
707
+ `,
708
+ }),
709
+ };