srcdev-nuxt-components 9.1.37 → 9.1.39

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 (28) hide show
  1. package/.claude/settings.json +4 -1
  2. package/.claude/skills/components/carousel-flip.md +188 -0
  3. package/.claude/skills/components/data-grid.md +167 -0
  4. package/.claude/skills/index.md +4 -1
  5. package/.claude/skills/qa-panel.md +231 -0
  6. package/app/components/01.atoms/grids/data-grid/DataGrid.vue +39 -0
  7. package/app/components/01.atoms/grids/data-grid/stories/DataGrid.stories.ts +234 -0
  8. package/app/components/01.atoms/grids/data-grid/tests/DataGrid.spec.ts +140 -0
  9. package/app/components/01.atoms/grids/data-grid/tests/__snapshots__/DataGrid.spec.ts.snap +11 -0
  10. package/app/components/01.atoms/{grid-stack → grids/grid-stack}/stories/GridStack.stories.ts +1 -1
  11. package/app/components/02.molecules/qr-code/CaptureQrCode.vue +49 -49
  12. package/app/components/carousels/CarouselFlip.vue +211 -153
  13. package/app/components/carousels/stories/CarouselFlip.stories.ts +35 -0
  14. package/app/components/carousels/tests/CarouselFlip.spec.ts +38 -0
  15. package/app/pages/ui/carousel-flip.vue +222 -17
  16. package/app/pages/ui/simple-grid.vue +2 -2
  17. package/package.json +1 -1
  18. package/app/components/display-grid/DisplayGridCore.vue +0 -22
  19. /package/app/components/01.atoms/{scroll-reveal-frame → animations/scroll-reveal-frame}/ScrollRevealFrame.vue +0 -0
  20. /package/app/components/01.atoms/{scroll-reveal-frame → animations/scroll-reveal-frame}/stories/ScrollRevealFrame.stories.ts +0 -0
  21. /package/app/components/01.atoms/{scroll-reveal-frame → animations/scroll-reveal-frame}/tests/ScrollRevealFrame.spec.ts +0 -0
  22. /package/app/components/01.atoms/{scroll-reveal-frame → animations/scroll-reveal-frame}/tests/__snapshots__/ScrollRevealFrame.spec.ts.snap +0 -0
  23. /package/app/components/01.atoms/{scroll-reveal-image → animations/scroll-reveal-image}/ScrollRevealImage.vue +0 -0
  24. /package/app/components/01.atoms/{scroll-reveal-image → animations/scroll-reveal-image}/stories/ScrollRevealImage.stories.ts +0 -0
  25. /package/app/components/01.atoms/{scroll-reveal-image → animations/scroll-reveal-image}/tests/ScrollRevealImage.spec.ts +0 -0
  26. /package/app/components/01.atoms/{scroll-reveal-image → animations/scroll-reveal-image}/tests/__snapshots__/ScrollRevealImage.spec.ts.snap +0 -0
  27. /package/app/components/01.atoms/{grid-stack → grids/grid-stack}/GridStack.vue +0 -0
  28. /package/app/components/01.atoms/{grid-stack → grids/grid-stack}/tests/GridStack.spec.ts +0 -0
@@ -3,6 +3,7 @@
3
3
  ref="carouselWrapperRef"
4
4
  class="carousel-flip"
5
5
  :class="[elementClasses]"
6
+ :data-button-layout="buttonLayout"
6
7
  role="region"
7
8
  aria-label="Image carousel"
8
9
  >
@@ -10,84 +11,81 @@
10
11
  Item {{ currentActiveIndex + 1 }} of {{ itemCount }}
11
12
  </div>
12
13
 
13
- <LayoutRow tag="div" variant="full-width" :style-class-passthrough="['mbe-20']">
14
+ <div
15
+ ref="carouselContainerRef"
16
+ tabindex="0"
17
+ class="item-container"
18
+ :class="{ 'allow-overflow': allowCarouselOverflow }"
19
+ role="group"
20
+ aria-label="Carousel items"
21
+ >
14
22
  <div
15
- ref="carouselContainerRef"
16
- tabindex="0"
17
- class="item-container"
18
- :class="{ 'allow-overflow': allowCarouselOverflow }"
19
- role="group"
20
- aria-label="Carousel items"
23
+ v-for="(item, index) in carouselDataIds"
24
+ :key="index"
25
+ ref="carouselItems"
26
+ class="item"
27
+ :class="{ loaded: carouselInitComplete && userHasInteracted }"
28
+ :data-id="item"
29
+ :aria-current="currentActiveIndex === index ? 'true' : 'false'"
21
30
  >
22
- <div
23
- v-for="(item, index) in carouselDataIds"
24
- :key="index"
25
- ref="carouselItems"
26
- class="item"
27
- :class="{ loaded: carouselInitComplete && userHasInteracted }"
28
- :data-id="item"
29
- :aria-current="currentActiveIndex === index ? 'true' : 'false'"
30
- >
31
- <slot :name="item"></slot>
32
- </div>
31
+ <slot :name="item"></slot>
33
32
  </div>
34
- </LayoutRow>
35
-
36
- <LayoutRow tag="div" variant="full-width" :style-class-passthrough="['mbe-20']">
37
- <div ref="controlsContainerRef" tabindex="0" class="controls-container">
38
- <div class="markers-container">
39
- <ul class="markers-list">
40
- <li v-for="index in itemCount" :key="index" class="markers-item">
41
- <button
42
- class="btn-marker"
43
- :class="[{ active: displayActiveIndex === index - 1 }]"
44
- :aria-label="`Jump to item ${index}`"
45
- @click.prevent="jumpToFrame(index - 1)"
46
- ></button>
47
- </li>
48
- </ul>
49
- </div>
50
- <div class="buttons-container">
51
- <button type="button" class="btn-action" aria-label="Go to previous item" @click.prevent="actionPrevious()">
52
- <Icon name="ic:outline-keyboard-arrow-left" class="arrows-icon" />
53
- </button>
54
- <button type="button" class="btn-action" aria-label="Go to next item" @click.prevent="actionNext()">
55
- <Icon name="ic:outline-keyboard-arrow-right" class="arrows-icon" />
56
- </button>
57
- </div>
33
+ </div>
34
+
35
+ <div v-if="showControls" ref="controlsContainerRef" tabindex="0" class="controls-container">
36
+ <div class="markers-container">
37
+ <ul class="markers-list">
38
+ <li v-for="index in itemCount" :key="index" class="markers-item">
39
+ <button
40
+ class="btn-marker"
41
+ :class="[{ active: displayActiveIndex === index - 1 }]"
42
+ :aria-label="`Jump to item ${index}`"
43
+ @click.prevent="jumpToFrame(index - 1)"
44
+ ></button>
45
+ </li>
46
+ </ul>
58
47
  </div>
59
- </LayoutRow>
48
+ </div>
49
+
50
+ <div class="buttons-container">
51
+ <button
52
+ type="button"
53
+ class="btn-action btn-prev"
54
+ aria-label="Go to previous item"
55
+ @click.prevent="actionPrevious()"
56
+ >
57
+ <Icon name="ic:outline-keyboard-arrow-left" class="arrows-icon" />
58
+ </button>
59
+ <button type="button" class="btn-action btn-next" aria-label="Go to next item" @click.prevent="actionNext()">
60
+ <Icon name="ic:outline-keyboard-arrow-right" class="arrows-icon" />
61
+ </button>
62
+ </div>
60
63
  </section>
61
64
  </template>
62
65
 
63
66
  <script setup lang="ts">
64
67
  import { useEventListener, useResizeObserver, useSwipe } from "@vueuse/core";
65
68
 
66
- const props = defineProps({
67
- carouselDataIds: {
68
- type: Array as PropType<string[]>,
69
- default: () => [],
70
- },
71
- styleClassPassthrough: {
72
- type: [String, Array] as PropType<string | string[]>,
73
- default: () => [],
74
- },
75
- transitionSpeed: {
76
- type: Number,
77
- default: 200,
78
- },
79
- allowCarouselOverflow: {
80
- type: Boolean,
81
- default: false,
82
- },
83
- useFlipAnimation: {
84
- type: Boolean,
85
- default: false,
86
- },
87
- useSpringEffect: {
88
- type: Boolean,
89
- default: false,
90
- },
69
+ interface Props {
70
+ carouselDataIds?: string[];
71
+ styleClassPassthrough?: string | string[];
72
+ transitionSpeed?: number;
73
+ allowCarouselOverflow?: boolean;
74
+ useFlipAnimation?: boolean;
75
+ useSpringEffect?: boolean;
76
+ buttonLayout?: "sides" | "controls-flanking" | "controls-grouped-right" | "overlay";
77
+ showControls?: boolean;
78
+ }
79
+
80
+ const props = withDefaults(defineProps<Props>(), {
81
+ carouselDataIds: () => [],
82
+ styleClassPassthrough: () => [],
83
+ transitionSpeed: 200,
84
+ allowCarouselOverflow: false,
85
+ useFlipAnimation: false,
86
+ useSpringEffect: false,
87
+ buttonLayout: "sides",
88
+ showControls: true,
91
89
  });
92
90
 
93
91
  const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
@@ -417,93 +415,100 @@ onMounted(() => {
417
415
 
418
416
  <style lang="css">
419
417
  @layer components {
420
- .carousel-flip {
421
- --_carousel-item-track-gap: 10px;
422
-
423
- display: grid;
424
- grid-template-columns: 1fr;
425
- gap: 10px;
426
- opacity: 0;
427
-
428
- &.mounted {
429
- opacity: 1;
430
- }
431
-
432
- .sr-only {
433
- position: absolute;
434
- width: 1px;
435
- height: 1px;
436
- padding: 0;
437
- margin: -1px;
438
- overflow: hidden;
439
- clip: rect(0, 0, 0, 0);
440
- white-space: nowrap;
441
- border: 0;
442
- }
443
-
444
- .item-container {
445
- display: flex;
446
- gap: var(--_carousel-item-track-gap);
447
- overflow-x: hidden;
448
- position: relative;
449
- isolation: isolate;
450
-
451
- max-inline-size: var(--_carousel-display-max-width);
452
- margin-inline: auto;
418
+ .carousel-flip {
419
+ --_carousel-item-track-gap: 10px;
420
+
421
+ display: grid;
422
+ grid-template-columns: auto 1fr auto;
423
+ grid-template-rows: 1fr auto;
424
+ grid-template-areas:
425
+ "prev carousel next"
426
+ ". controls . ";
427
+ gap: 10px;
428
+ opacity: 0;
429
+
430
+ &.mounted {
431
+ opacity: 1;
432
+ }
453
433
 
454
- &.allow-overflow {
455
- overflow-x: initial;
434
+ .sr-only {
435
+ position: absolute;
436
+ width: 1px;
437
+ height: 1px;
438
+ padding: 0;
439
+ margin: -1px;
440
+ overflow: hidden;
441
+ clip: rect(0, 0, 0, 0);
442
+ white-space: nowrap;
443
+ border: 0;
456
444
  }
457
445
 
458
- .item {
446
+ .item-container {
447
+ grid-area: carousel;
459
448
  display: flex;
460
- flex: 0 0 100%;
449
+ gap: var(--_carousel-item-track-gap);
450
+ overflow-x: hidden;
461
451
  position: relative;
452
+ isolation: isolate;
462
453
 
454
+ max-inline-size: var(--_carousel-display-max-width);
463
455
  margin-inline: auto;
464
456
 
465
- max-inline-size: calc(
466
- var(--_carousel-container-max-inline-size) + var(--_carousel-item-track-gap) -
467
- (2 * var(--_carousel-item-edge-preview-width))
468
- );
457
+ &.allow-overflow {
458
+ overflow-x: initial;
459
+ }
460
+
461
+ .item {
462
+ display: flex;
463
+ flex: 0 0 100%;
464
+ position: relative;
469
465
 
470
- translate: calc(
471
- v-bind(itemWidthOffsetStr) - var(--_carousel-item-track-gap) + var(--_carousel-item-edge-preview-width)
472
- )
473
- 0;
466
+ margin-inline: auto;
474
467
 
475
- &.loaded {
476
- transition: transform v-bind(transitionSpeedStr) ease;
468
+ max-inline-size: calc(
469
+ var(--_carousel-container-max-inline-size) + var(--_carousel-item-track-gap) -
470
+ (2 * var(--_carousel-item-edge-preview-width))
471
+ );
472
+
473
+ translate: calc(
474
+ v-bind(itemWidthOffsetStr) - var(--_carousel-item-track-gap) + var(--_carousel-item-edge-preview-width)
475
+ )
476
+ 0;
477
+
478
+ &.loaded {
479
+ transition: transform v-bind(transitionSpeedStr) ease;
480
+ }
477
481
  }
478
482
  }
479
- }
480
483
 
481
- .controls-container {
482
- display: flex;
483
- align-items: center;
484
- justify-content: flex-end;
485
- max-inline-size: var(--_carousel-display-max-width);
486
- margin-inline: auto;
484
+ .controls-container {
485
+ grid-area: controls;
486
+ display: flex;
487
+ align-items: center;
488
+ justify-content: center;
489
+ max-inline-size: var(--_carousel-display-max-width);
490
+ margin-inline: auto;
487
491
 
488
- .markers-container {
489
- .markers-list {
490
- display: flex;
491
- flex-direction: row;
492
- gap: 10px;
493
- list-style-type: none;
494
- margin: unset;
495
- padding: unset;
496
-
497
- .markers-item {
498
- .btn-marker {
499
- border: 1px solid transparent;
500
- outline: 1px solid transparent;
501
- box-shadow: none;
502
- cursor: pointer;
503
- transition: background-color v-bind(transitionSpeedStr) linear;
504
-
505
- &.active {
506
- background-color: light-dark(var(--slate-10), var(--slate-00));
492
+ .markers-container {
493
+ .markers-list {
494
+ display: flex;
495
+ flex-direction: row;
496
+ gap: 10px;
497
+ list-style-type: none;
498
+ margin: unset;
499
+ padding: unset;
500
+
501
+ .markers-item {
502
+ .btn-marker {
503
+ border: 1px solid transparent;
504
+ outline: 1px solid transparent;
505
+ box-shadow: none;
506
+ cursor: pointer;
507
+ transition: background-color v-bind(transitionSpeedStr) linear;
508
+
509
+ &.active {
510
+ background-color: light-dark(var(--slate-10), var(--slate-00));
511
+ }
507
512
  }
508
513
  }
509
514
  }
@@ -511,26 +516,79 @@ onMounted(() => {
511
516
  }
512
517
 
513
518
  .buttons-container {
519
+ display: contents;
520
+ }
521
+
522
+ .btn-prev {
523
+ grid-area: prev;
524
+ align-self: center;
525
+ z-index: 1;
526
+ }
527
+
528
+ .btn-next {
529
+ grid-area: next;
530
+ align-self: center;
531
+ z-index: 1;
532
+ }
533
+
534
+ .btn-action {
514
535
  display: flex;
515
536
  align-items: center;
516
- justify-content: end;
517
- gap: 20px;
537
+ justify-content: center;
538
+ cursor: pointer;
539
+ height: fit-content;
540
+
541
+ background-color: white;
542
+ border: 1px solid light-dark(hsl(0, 29%, 3%), hsl(0, 0%, 92%));
543
+ border-radius: 100vw;
544
+ padding: 8px;
545
+
546
+ .arrows-icon {
547
+ width: 24px;
548
+ height: 24px;
549
+ }
550
+ }
551
+
552
+ /* ── controls-flanking: buttons flank the controls row ─────── */
553
+ &[data-button-layout="controls-flanking"] {
554
+ .btn-prev {
555
+ grid-area: unset;
556
+ grid-column: 1;
557
+ grid-row: 2;
558
+ }
559
+
560
+ .btn-next {
561
+ grid-area: unset;
562
+ grid-column: 3;
563
+ grid-row: 2;
564
+ }
565
+ }
566
+
567
+ /* ── controls-grouped-right: markers centred, buttons end ────── */
568
+ &[data-button-layout="controls-grouped-right"] {
569
+ grid-template-columns: 1fr;
570
+ grid-template-areas:
571
+ "carousel"
572
+ "controls";
518
573
 
519
- .btn-action {
574
+ .buttons-container {
520
575
  display: flex;
521
576
  align-items: center;
522
- justify-content: center;
523
-
524
- cursor: pointer;
525
- height: fit-content;
577
+ gap: 10px;
578
+ grid-area: controls;
579
+ justify-self: end;
580
+ }
581
+ }
526
582
 
527
- .arrows-icon {
528
- width: 24px;
529
- height: 24px;
530
- }
583
+ /* ── overlay: controls float at the bottom edge of the frame ── */
584
+ &[data-button-layout="overlay"] {
585
+ .controls-container {
586
+ grid-area: carousel;
587
+ align-self: end;
588
+ z-index: 1;
589
+ padding-block-end: 1rem;
531
590
  }
532
591
  }
533
592
  }
534
593
  }
535
- }
536
594
  </style>
@@ -16,6 +16,8 @@ interface CarouselFlipStoryArgs {
16
16
  transitionSpeed: number;
17
17
  useFlipAnimation: boolean;
18
18
  useSpringEffect: boolean;
19
+ buttonLayout: "sides" | "controls-flanking" | "controls-grouped-right" | "overlay";
20
+ showControls: boolean;
19
21
  styleClassPassthrough: string[];
20
22
  // Mock data for stories
21
23
  carouselData: CarouselItem[];
@@ -105,6 +107,21 @@ export default {
105
107
  category: "Animation",
106
108
  },
107
109
  },
110
+ buttonLayout: {
111
+ control: { type: "select" },
112
+ options: ["sides", "controls-flanking", "controls-grouped-right", "overlay"],
113
+ description: "Placement of prev/next buttons relative to the carousel and controls bar",
114
+ table: {
115
+ category: "Layout",
116
+ },
117
+ },
118
+ showControls: {
119
+ control: { type: "boolean" },
120
+ description: "Show or hide the markers/controls bar",
121
+ table: {
122
+ category: "Layout",
123
+ },
124
+ },
108
125
  styleClassPassthrough: {
109
126
  control: { type: "object" },
110
127
  description: "Additional CSS classes to pass through",
@@ -127,6 +144,8 @@ export default {
127
144
  transitionSpeed: 1000,
128
145
  useFlipAnimation: true,
129
146
  useSpringEffect: false,
147
+ buttonLayout: "sides",
148
+ showControls: true,
130
149
  styleClassPassthrough: ["carousel-flip-demo"],
131
150
  carouselData: sampleCarouselItems,
132
151
  },
@@ -289,6 +308,8 @@ const Template: StoryFn<CarouselFlipStoryArgs> = (args) => ({
289
308
  :transition-speed="args.transitionSpeed"
290
309
  :use-flip-animation="args.useFlipAnimation"
291
310
  :use-spring-effect="args.useSpringEffect"
311
+ :button-layout="args.buttonLayout"
312
+ :show-controls="args.showControls"
292
313
  :style-class-passthrough="args.styleClassPassthrough"
293
314
  >
294
315
  <template v-for="(item, index) in filteredCarouselData" #[item.id] :key="index">
@@ -350,6 +371,16 @@ NoOverflow.args = {
350
371
  styleClassPassthrough: ["carousel-flip-demo", "no-overflow"],
351
372
  };
352
373
 
374
+ export const NoControls = Template.bind({});
375
+ NoControls.args = {
376
+ carouselDataIds: [1, 2, 3, 4, 5],
377
+ transitionSpeed: 1000,
378
+ useFlipAnimation: true,
379
+ useSpringEffect: false,
380
+ allowCarouselOverflow: true,
381
+ showControls: false,
382
+ };
383
+
353
384
  export const ManyItems = Template.bind({});
354
385
  ManyItems.args = {
355
386
  carouselDataIds: [1, 2, 3, 4, 5, 6, 7, 8],
@@ -452,6 +483,8 @@ const RichContentTemplate: StoryFn<CarouselFlipStoryArgs> = (args) => ({
452
483
  :transition-speed="args.transitionSpeed"
453
484
  :use-flip-animation="args.useFlipAnimation"
454
485
  :use-spring-effect="args.useSpringEffect"
486
+ :button-layout="args.buttonLayout"
487
+ :show-controls="args.showControls"
455
488
  :style-class-passthrough="args.styleClassPassthrough"
456
489
  >
457
490
  <template v-for="(item, index) in filteredCarouselData" #[item.id] :key="index">
@@ -592,6 +625,8 @@ const AnimationShowcaseTemplate: StoryFn<CarouselFlipStoryArgs> = (args) => ({
592
625
  :transition-speed="args.transitionSpeed"
593
626
  :use-flip-animation="args.useFlipAnimation"
594
627
  :use-spring-effect="args.useSpringEffect"
628
+ :button-layout="args.buttonLayout"
629
+ :show-controls="args.showControls"
595
630
  :style-class-passthrough="args.styleClassPassthrough"
596
631
  >
597
632
  <template v-for="(item, index) in filteredCarouselData" #[item.id] :key="index">
@@ -232,6 +232,44 @@ describe("CarouselFlip", () => {
232
232
  expect(component).toBeDefined();
233
233
  // Spring effect is applied during transitions
234
234
  });
235
+
236
+ it("shows controls container by default", async () => {
237
+ await createWrapper();
238
+
239
+ expect(wrapper.find(".controls-container").exists()).toBe(true);
240
+ expect(wrapper.findAll(".btn-marker").length).toBe(6);
241
+ });
242
+
243
+ it("hides controls container when showControls is false", async () => {
244
+ await createWrapper({ showControls: false });
245
+
246
+ expect(wrapper.find(".controls-container").exists()).toBe(false);
247
+ expect(wrapper.findAll(".btn-marker").length).toBe(0);
248
+ });
249
+
250
+ it("still renders navigation buttons when showControls is false", async () => {
251
+ await createWrapper({ showControls: false });
252
+
253
+ expect(wrapper.find('[aria-label="Go to previous item"]').exists()).toBe(true);
254
+ expect(wrapper.find('[aria-label="Go to next item"]').exists()).toBe(true);
255
+ });
256
+
257
+ it("navigation still works when showControls is false", async () => {
258
+ await createWrapper({ showControls: false });
259
+ await nextTick();
260
+
261
+ const nextButton = wrapper.find('[aria-label="Go to next item"]');
262
+ await nextButton.trigger("click");
263
+ await nextTick();
264
+
265
+ expect(component.currentActiveIndex).toBe(1);
266
+ });
267
+
268
+ it("sets data-button-layout attribute from buttonLayout prop", async () => {
269
+ await createWrapper({ buttonLayout: "overlay" });
270
+
271
+ expect(wrapper.attributes("data-button-layout")).toBe("overlay");
272
+ });
235
273
  });
236
274
 
237
275
  describe("Initial Setup", () => {