@stackonward/cli 1.5.3 → 1.6.0

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 (74) hide show
  1. package/dist/index.js +1 -1
  2. package/dist/index.js.map +1 -1
  3. package/dist/resources/components/nuxt/site-source/app/assets/css/site-shell.css +112 -56
  4. package/dist/resources/components/nuxt/site-source/app/assets/icons/brand/android.svg +10 -0
  5. package/dist/resources/components/nuxt/site-source/app/assets/icons/brand/apple.svg +3 -0
  6. package/dist/resources/components/nuxt/site-source/app/assets/icons/brand/whatsapp.svg +5 -0
  7. package/dist/resources/components/nuxt/site-source/app/components/app/Sidebar.vue +1 -1
  8. package/dist/resources/components/nuxt/site-source/app/components/checkout/Status.vue +3 -3
  9. package/dist/resources/components/nuxt/site-source/app/components/consent/Banner.vue +23 -8
  10. package/dist/resources/components/nuxt/site-source/app/components/content/CallToAction.vue +59 -0
  11. package/dist/resources/components/nuxt/site-source/app/components/content/Card.vue +35 -0
  12. package/dist/resources/components/nuxt/site-source/app/components/content/Compatibility.vue +10 -0
  13. package/dist/resources/components/nuxt/site-source/app/components/content/Faq.vue +56 -0
  14. package/dist/resources/components/nuxt/site-source/app/components/content/Features.vue +8 -0
  15. package/dist/resources/components/nuxt/site-source/app/components/content/List.vue +1 -8
  16. package/dist/resources/components/nuxt/site-source/app/components/content/Reviews.vue +93 -0
  17. package/dist/resources/components/nuxt/site-source/app/components/countdown/Timer.vue +112 -0
  18. package/dist/resources/components/nuxt/site-source/app/components/locale/Menu.vue +3 -3
  19. package/dist/resources/components/nuxt/site-source/app/components/site/BackToTop.vue +30 -23
  20. package/dist/resources/components/nuxt/site-source/app/components/site/Header.vue +2 -2
  21. package/dist/resources/components/nuxt/site-source/app/components/steps/Progress.vue +36 -0
  22. package/dist/resources/components/nuxt/site-source/app/components/steps/Showcase.vue +340 -0
  23. package/dist/resources/components/nuxt/site-source/app/components/theme/Menu.vue +4 -4
  24. package/dist/resources/components/nuxt/site-source/app/components/ui/Button.vue +28 -2
  25. package/dist/resources/components/nuxt/site-source/app/components/ui/GradientBorder.vue +5 -5
  26. package/dist/resources/components/nuxt/site-source/app/components/ui/Icon.vue +14 -0
  27. package/dist/resources/components/nuxt/site-source/app/components/ui/Image.vue +126 -0
  28. package/dist/resources/components/nuxt/site-source/app/components/ui/Marquee.vue +99 -0
  29. package/dist/resources/components/nuxt/site-source/app/components/ui/Modal.vue +26 -88
  30. package/dist/resources/components/nuxt/site-source/app/components/ui/Popover.vue +87 -93
  31. package/dist/resources/components/nuxt/site-source/app/components/ui/Text.vue +84 -12
  32. package/dist/resources/components/nuxt/site-source/app/components/ui/Video.vue +105 -0
  33. package/dist/resources/components/nuxt/site-source/app/components/whatsapp/Chat.vue +126 -0
  34. package/dist/resources/components/nuxt/site-source/app/components/youtube/Embed.vue +286 -73
  35. package/dist/resources/components/nuxt/site-source/app/composables/useFloatingBottomStack.ts +44 -0
  36. package/dist/resources/components/nuxt/site-source/app/composables/useNavigation.ts +6 -8
  37. package/dist/resources/components/nuxt/site-source/app/consent/types.ts +1 -4
  38. package/dist/resources/components/nuxt/site-source/app/content/types.ts +3 -0
  39. package/dist/resources/components/nuxt/site-source/app/theme/accordion.ts +5 -4
  40. package/dist/resources/components/nuxt/site-source/app/theme/breadcrumb.ts +14 -4
  41. package/dist/resources/components/nuxt/site-source/app/theme/button.ts +138 -28
  42. package/dist/resources/components/nuxt/site-source/app/theme/carousel.ts +11 -7
  43. package/dist/resources/components/nuxt/site-source/app/theme/checkbox.ts +9 -3
  44. package/dist/resources/components/nuxt/site-source/app/theme/drawer.ts +6 -0
  45. package/dist/resources/components/nuxt/site-source/app/theme/form-field.ts +8 -0
  46. package/dist/resources/components/nuxt/site-source/app/theme/icon.ts +6 -0
  47. package/dist/resources/components/nuxt/site-source/app/theme/index.ts +8 -1
  48. package/dist/resources/components/nuxt/site-source/app/theme/input.ts +34 -18
  49. package/dist/resources/components/nuxt/site-source/app/theme/modal.ts +9 -5
  50. package/dist/resources/components/nuxt/site-source/app/theme/pagination.ts +9 -2
  51. package/dist/resources/components/nuxt/site-source/app/theme/radio-group.ts +7 -0
  52. package/dist/resources/components/nuxt/site-source/app/theme/select.ts +19 -7
  53. package/dist/resources/components/nuxt/site-source/app/theme/stepper.ts +15 -6
  54. package/dist/resources/components/nuxt/site-source/app/theme/textarea.ts +6 -0
  55. package/dist/resources/components/nuxt/site-source/app/theme/toast.ts +1 -4
  56. package/dist/resources/components/nuxt/site-source/app/theme/tooltip.ts +5 -1
  57. package/dist/resources/components/registry.json +133 -45
  58. package/dist/resources/components/vue/ui/BottomSheet.vue +5 -1
  59. package/dist/resources/components/vue/ui/ScrollArea.vue +2 -1
  60. package/dist/resources/registry.json +1 -1
  61. package/dist/resources/templates/base/.claude/docs/architecture-template.md +2 -1
  62. package/dist/resources/templates/base/AGENTS.md +7 -0
  63. package/dist/resources/templates/base/CLAUDE.md +7 -0
  64. package/dist/resources/templates/nuxt/.claude/docs/stack.md +14 -1
  65. package/dist/resources/templates/nuxt/DESIGN.md +146 -0
  66. package/dist/resources/templates/nuxt/app/app.config.ts +2 -0
  67. package/dist/resources/templates/nuxt/app/pages/app/index.vue +9 -7
  68. package/dist/resources/templates/nuxt/app/pages/index.vue +12 -27
  69. package/dist/resources/templates/nuxt/app/pages/ui/index.vue +219 -3
  70. package/dist/resources/templates/nuxt/nuxt.config.ts +2 -4
  71. package/dist/resources/templates/nuxt/package.json +2 -1
  72. package/dist/resources/templates/nuxt/stylelint.config.mjs +12 -0
  73. package/package.json +3 -3
  74. package/dist/resources/components/nuxt/site-source/app/assets/icons/common/.gitkeep +0 -0
@@ -0,0 +1,93 @@
1
+ <template>
2
+ <section class="min-w-0 overflow-hidden py-15">
3
+ <div class="mx-auto max-w-7xl px-4">
4
+ <slot name="title">
5
+ <h2 v-if="title" class="mb-10 text-center text-3xl font-bold">{{ title }}</h2>
6
+ </slot>
7
+ </div>
8
+
9
+ <div class="flex flex-col gap-7.5 w-full pb-15 max-md:pb-7.5 max-md:gap-6">
10
+ <UiMarquee
11
+ v-for="(rowItems, rowIndex) in reviews"
12
+ :key="rowIndex"
13
+ :direction="rowIndex % 2 === 0 ? 'left' : 'right'"
14
+ :duration="40"
15
+ content-class="max-md:gap-6"
16
+ >
17
+ <div
18
+ v-for="(review, index) in rowItems"
19
+ :key="`row-${rowIndex}-${index}`"
20
+ class="w-[450px] min-h-[325px] p-[26px] border border-solid border-border rounded-2ml bg-surface flex-shrink-0 hover:shadow-lg max-md:px-4 max-md:w-[277px] max-md:min-h-[240px]"
21
+ >
22
+ <div class="flex items-center gap-5 mb-7.5 max-md:gap-2.5 max-md:mb-5">
23
+ <UiImage
24
+ :src="review.avatar"
25
+ :alt="`the avatar of ${review.author}`"
26
+ width="80"
27
+ height="80"
28
+ class="w-20 h-20 rounded-full object-cover max-md:w-11 max-md:h-11"
29
+ />
30
+ <div class="flex flex-col">
31
+ <p class="text-lg text-text max-md:text-base">
32
+ <b>{{ splitAuthor(review.author).name }}</b
33
+ >{{ splitAuthor(review.author).detail }}
34
+ </p>
35
+ <div class="flex gap-1 mt-2.5 max-md:mt-2">
36
+ <UiIcon
37
+ v-for="i in 5"
38
+ :key="i"
39
+ name="i-lucide-star"
40
+ class="size-icon-md max-md:size-icon-sm text-warning"
41
+ />
42
+ </div>
43
+ </div>
44
+ </div>
45
+ <p class="text-text max-md:text-xs">
46
+ {{ review.description }}
47
+ </p>
48
+ </div>
49
+ </UiMarquee>
50
+ </div>
51
+
52
+ <UiButton
53
+ v-if="showButton"
54
+ size="lg"
55
+ class="flex justify-center mx-auto"
56
+ :label="buttonText"
57
+ ripple
58
+ :to="to"
59
+ />
60
+ </section>
61
+ </template>
62
+
63
+ <script setup lang="ts">
64
+ interface ReviewsItem {
65
+ avatar: string;
66
+ author: string;
67
+ description: string;
68
+ }
69
+
70
+ defineProps<{
71
+ title?: string;
72
+ reviews: ReviewsItem[][];
73
+ showButton?: boolean;
74
+ buttonText?: string;
75
+ to?: string;
76
+ }>();
77
+
78
+ const splitAuthor = (author: string) => {
79
+ const separatorIndex = author.indexOf(",");
80
+
81
+ if (separatorIndex === -1) {
82
+ return {
83
+ name: author,
84
+ detail: "",
85
+ };
86
+ }
87
+
88
+ return {
89
+ name: author.slice(0, separatorIndex),
90
+ detail: ` ${author.slice(separatorIndex + 1).trimStart()}`,
91
+ };
92
+ };
93
+ </script>
@@ -0,0 +1,112 @@
1
+ <script setup lang="ts">
2
+ import { useCountdown } from "@stackonward/composables";
3
+
4
+ const props = withDefaults(
5
+ defineProps<{
6
+ duration?: number;
7
+ storageKey: string;
8
+ showHours?: boolean;
9
+ showMilliseconds?: boolean;
10
+ offerText?: string;
11
+ updateInterval?: number;
12
+ labels?: { hours: string; minutes: string; seconds: string; milliseconds: string };
13
+ }>(),
14
+ {
15
+ duration: 86400,
16
+ showHours: true,
17
+ showMilliseconds: false,
18
+ updateInterval: 1000,
19
+ labels: () => ({ hours: "HR", minutes: "MIN", seconds: "SEC", milliseconds: "MS" }),
20
+ },
21
+ );
22
+ const { remaining, start, stop } = useCountdown({
23
+ duration: props.duration,
24
+ storageKeyFn: () => props.storageKey,
25
+ });
26
+ const now = ref(0);
27
+ const startedAt = ref(0);
28
+ const mounted = ref(false);
29
+ const { pause, resume } = useIntervalFn(
30
+ () => {
31
+ now.value = Date.now();
32
+ },
33
+ props.updateInterval,
34
+ { immediate: false },
35
+ );
36
+ function syncStart() {
37
+ startedAt.value = Number(localStorage.getItem(props.storageKey));
38
+ now.value = Date.now();
39
+ }
40
+ watch(remaining, syncStart);
41
+ const milliseconds = computed(() =>
42
+ mounted.value && startedAt.value
43
+ ? props.duration * 1000 - ((now.value - startedAt.value) % (props.duration * 1000))
44
+ : props.duration * 1000,
45
+ );
46
+ const units = computed(() => {
47
+ const total = Math.floor(milliseconds.value / 1000);
48
+ return [
49
+ {
50
+ label: props.labels.hours,
51
+ value: Math.floor(total / 3600),
52
+ visible: props.showHours,
53
+ digits: 2,
54
+ },
55
+ {
56
+ label: props.labels.minutes,
57
+ value: Math.floor((total % 3600) / 60),
58
+ visible: true,
59
+ digits: 2,
60
+ },
61
+ { label: props.labels.seconds, value: total % 60, visible: true, digits: 2 },
62
+ {
63
+ label: props.labels.milliseconds,
64
+ value: milliseconds.value % 1000,
65
+ visible: props.showMilliseconds,
66
+ digits: 3,
67
+ },
68
+ ].filter((unit) => unit.visible);
69
+ });
70
+ onMounted(() => {
71
+ mounted.value = true;
72
+ start();
73
+ syncStart();
74
+ resume();
75
+ useEventListener(document, "visibilitychange", () => {
76
+ if (!document.hidden) {
77
+ start();
78
+ syncStart();
79
+ }
80
+ });
81
+ useEventListener(window, "focus", () => {
82
+ start();
83
+ syncStart();
84
+ });
85
+ });
86
+ onBeforeUnmount(() => {
87
+ pause();
88
+ stop();
89
+ });
90
+ </script>
91
+
92
+ <template>
93
+ <div class="flex items-center justify-center gap-1.5" role="timer">
94
+ <div
95
+ v-if="offerText"
96
+ class="flex h-10 items-center gap-1.5 rounded-lg bg-surface px-2.5 py-2.5 text-lg font-bold text-primary shadow-[2px_2px_0_rgb(0_0_0/25%)] max-xl:text-sm"
97
+ >
98
+ <UiIcon name="i-lucide-clock" class="size-icon-md shrink-0" />{{ offerText }}
99
+ </div>
100
+ <div
101
+ v-for="unit in units"
102
+ :key="unit.label"
103
+ class="flex h-10 flex-col items-center justify-center rounded-[7px] bg-surface shadow-[2px_2px_0_rgb(0_0_0/25%)]"
104
+ :class="unit.digits === 3 ? 'w-[34px]' : 'w-8'"
105
+ >
106
+ <span class="text-lg leading-none font-black text-primary tabular-nums">{{
107
+ String(unit.value).padStart(unit.digits, "0")
108
+ }}</span>
109
+ <span class="mt-px text-[10px] leading-none text-muted">{{ unit.label }}</span>
110
+ </div>
111
+ </div>
112
+ </template>
@@ -37,9 +37,9 @@ onClickOutside(dropdown, () => dropdown.value?.removeAttribute("open"));
37
37
  @keydown="handleKeydown"
38
38
  >
39
39
  <summary :aria-label="t('language.current', { language: current?.name || locale })">
40
- <UIcon name="i-lucide-languages" aria-hidden="true" />
40
+ <UiIcon name="i-lucide-languages" aria-hidden="true" />
41
41
  <span>{{ current?.code.toUpperCase() || locale.toUpperCase() }}</span>
42
- <UIcon class="site-preference__chevron" name="i-lucide-chevron-down" aria-hidden="true" />
42
+ <UiIcon class="site-preference__chevron" name="i-lucide-chevron-down" aria-hidden="true" />
43
43
  </summary>
44
44
  <div class="site-preference__menu" :aria-label="t('language.select')">
45
45
  <button
@@ -50,7 +50,7 @@ onClickOutside(dropdown, () => dropdown.value?.removeAttribute("open"));
50
50
  @click="selectLocale(option.code)"
51
51
  >
52
52
  <span>{{ option.name }}</span>
53
- <UIcon v-if="option.code === locale" name="i-lucide-check" aria-hidden="true" />
53
+ <UiIcon v-if="option.code === locale" name="i-lucide-check" aria-hidden="true" />
54
54
  </button>
55
55
  </div>
56
56
  </details>
@@ -1,34 +1,41 @@
1
1
  <script setup lang="ts">
2
- const isVisible = ref(false);
2
+ const { y } = useWindowScroll();
3
+ const isVisible = computed(() => y.value > 300);
4
+ const mobile = useMediaQuery("(max-width: 767px)");
5
+ const { dockedHeight } = useFloatingBottomStack();
6
+ const bottom = computed(() => dockedHeight.value + (mobile.value ? 72 : 24));
3
7
  const { t } = useI18n();
4
8
 
5
- function updateVisibility(): void {
6
- isVisible.value = window.scrollY > 320;
7
- }
8
-
9
9
  function scrollToTop(): void {
10
10
  const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
11
11
  window.scrollTo({ top: 0, behavior: reducedMotion ? "auto" : "smooth" });
12
12
  }
13
-
14
- onMounted(() => {
15
- updateVisibility();
16
- window.addEventListener("scroll", updateVisibility, { passive: true });
17
- });
18
-
19
- onBeforeUnmount(() => {
20
- window.removeEventListener("scroll", updateVisibility);
21
- });
22
13
  </script>
23
14
 
24
15
  <template>
25
- <button
26
- type="button"
27
- class="back-to-top"
28
- :class="{ 'back-to-top--visible': isVisible }"
29
- :aria-label="t('site.backToTop')"
30
- @click="scrollToTop"
31
- >
32
- <UIcon name="i-lucide-chevron-up" class="back-to-top__icon" />
33
- </button>
16
+ <Transition name="to-top">
17
+ <UiButton
18
+ v-if="isVisible"
19
+ color="neutral"
20
+ variant="outline"
21
+ square
22
+ icon="i-lucide-chevron-up"
23
+ class="fixed end-5 z-40 size-12 rounded-full max-md:size-10"
24
+ :style="{ bottom: `${bottom}px` }"
25
+ :aria-label="t('site.backToTop')"
26
+ @click="scrollToTop"
27
+ />
28
+ </Transition>
34
29
  </template>
30
+
31
+ <style scoped>
32
+ .to-top-enter-active,
33
+ .to-top-leave-active {
34
+ transition: opacity 300ms;
35
+ }
36
+
37
+ .to-top-enter-from,
38
+ .to-top-leave-to {
39
+ opacity: 0;
40
+ }
41
+ </style>
@@ -62,7 +62,7 @@ function hasChildren(item: NavItem): boolean {
62
62
  </NuxtLinkLocale>
63
63
  <NuxtLinkLocale :to="site.appPath" class="base-button base-button--primary">
64
64
  {{ primaryActionLabel }}
65
- <UIcon name="i-lucide-arrow-right" />
65
+ <UiIcon name="i-lucide-arrow-right" />
66
66
  </NuxtLinkLocale>
67
67
  </slot>
68
68
  <button
@@ -73,7 +73,7 @@ function hasChildren(item: NavItem): boolean {
73
73
  :aria-label="t('navigation.toggle')"
74
74
  @click="isMenuOpen = !isMenuOpen"
75
75
  >
76
- <UIcon
76
+ <UiIcon
77
77
  :name="isMenuOpen ? 'i-lucide-x' : 'i-lucide-menu'"
78
78
  class="site-header__menu-icon"
79
79
  />
@@ -0,0 +1,36 @@
1
+ <script setup lang="ts">
2
+ const props = defineProps<{ items: string[]; currentStep: number }>();
3
+ const columns = computed(() =>
4
+ props.items.map((_, index) => (index === 0 ? "auto" : "minmax(16px,1fr) auto")).join(" "),
5
+ );
6
+ </script>
7
+
8
+ <template>
9
+ <ol
10
+ class="mx-auto grid w-full max-w-[420px] items-center gap-2 text-sm font-semibold max-md:max-w-full max-md:gap-1.5 max-md:text-xs"
11
+ :style="{ gridTemplateColumns: columns }"
12
+ >
13
+ <li v-for="(item, index) in items" :key="index" class="contents">
14
+ <span
15
+ v-if="index > 0"
16
+ aria-hidden="true"
17
+ class="h-px"
18
+ :class="currentStep > index ? 'bg-primary' : 'bg-border-strong'"
19
+ />
20
+ <span
21
+ class="flex items-center gap-1.5 whitespace-nowrap"
22
+ :class="currentStep >= index + 1 ? 'text-primary' : 'text-dimmed'"
23
+ :aria-current="currentStep === index + 1 ? 'step' : undefined"
24
+ >
25
+ <span
26
+ class="grid size-5 place-items-center rounded-full text-xs text-primary-contrast"
27
+ :class="currentStep >= index + 1 ? 'bg-primary' : 'bg-border-strong'"
28
+ >
29
+ <UiIcon v-if="currentStep > index + 1" name="i-lucide-check" class="size-icon-sm" />
30
+ <template v-else>{{ index + 1 }}</template>
31
+ </span>
32
+ {{ item }}
33
+ </span>
34
+ </li>
35
+ </ol>
36
+ </template>
@@ -0,0 +1,340 @@
1
+ <script setup lang="ts">
2
+ import type { StepperItem } from "@nuxt/ui";
3
+ interface StepItem {
4
+ title: string;
5
+ description: string;
6
+ icon?: string;
7
+ }
8
+ interface StepImage {
9
+ src: string;
10
+ alt: string;
11
+ }
12
+ const props = withDefaults(
13
+ defineProps<{
14
+ title?: string;
15
+ subtitle?: string;
16
+ steps: StepItem[];
17
+ images: StepImage[];
18
+ initialStep?: number;
19
+ showButton?: boolean;
20
+ buttonText?: string;
21
+ size?: "sm" | "md" | "lg";
22
+ imageMode?: "carousel" | "static";
23
+ to?: string;
24
+ interval?: number;
25
+ autoplay?: boolean;
26
+ }>(),
27
+ { initialStep: 0, size: "lg", imageMode: "carousel", interval: 4000, autoplay: true },
28
+ );
29
+ const emit = defineEmits<{ "step-change": [step: number] }>();
30
+ const currentStep = ref(props.initialStep);
31
+ const carouselRef = ref<{ emblaApi?: { scrollTo: (index: number) => void } } | null>(null);
32
+ const reducedMotion = usePreferredReducedMotion();
33
+ const visibility = useDocumentVisibility();
34
+ const currentImage = computed(() => props.images[currentStep.value] ?? props.images[0]);
35
+ const stepperItems = computed<StepperItem[]>(() =>
36
+ props.steps.map((step, index) => ({ ...step, value: index + 1 })),
37
+ );
38
+ const { pause, resume } = useIntervalFn(
39
+ () => {
40
+ if (props.steps.length) currentStep.value = (currentStep.value + 1) % props.steps.length;
41
+ },
42
+ props.interval,
43
+ { immediate: false },
44
+ );
45
+ function resumeAutoplay() {
46
+ if (props.autoplay && reducedMotion.value !== "reduce" && visibility.value === "visible")
47
+ resume();
48
+ }
49
+ function onStepChange(value: string | number | undefined) {
50
+ const index = Number(value);
51
+ if (Number.isInteger(index) && index >= 0 && index < props.steps.length) {
52
+ currentStep.value = index;
53
+ pause();
54
+ resumeAutoplay();
55
+ }
56
+ }
57
+ watch(currentStep, (index) => {
58
+ if (props.imageMode === "carousel") carouselRef.value?.emblaApi?.scrollTo(index);
59
+ emit("step-change", index);
60
+ });
61
+ watch([reducedMotion, visibility, () => props.autoplay], () => {
62
+ pause();
63
+ resumeAutoplay();
64
+ });
65
+ onMounted(resumeAutoplay);
66
+ </script>
67
+
68
+ <template>
69
+ <section class="overflow-hidden bg-gradient-to-b from-primary/10 to-surface px-4 py-15">
70
+ <div class="mx-auto w-full max-w-7xl">
71
+ <h2 v-if="title" class="mb-5 text-center text-3xl font-bold">{{ title }}</h2>
72
+ <p v-if="subtitle" class="section-subtitle">{{ subtitle }}</p>
73
+
74
+ <div
75
+ class="flex w-full justify-between items-center step-layout step-wrapper max-lg:flex-col"
76
+ @mouseenter="pause"
77
+ @mouseleave="resumeAutoplay"
78
+ @focusin="pause"
79
+ @focusout="resumeAutoplay"
80
+ >
81
+ <div
82
+ class="flex-[0_0_32%] relative ms-[75px] max-lg:hidden max-lg:w-full max-lg:text-center"
83
+ >
84
+ <UCarousel
85
+ v-if="imageMode !== 'static'"
86
+ ref="carouselRef"
87
+ v-slot="{ item }"
88
+ dir="ltr"
89
+ :items="images"
90
+ :start-index="initialStep"
91
+ class="carousel-container relative w-full max-w-[407px] mx-auto z-1"
92
+ @select="onStepChange"
93
+ >
94
+ <UiImage
95
+ :src="item.src"
96
+ :alt="item.alt"
97
+ width="239"
98
+ height="458"
99
+ class="w-auto h-[458px] mx-auto object-cover"
100
+ />
101
+ </UCarousel>
102
+ <div
103
+ v-else-if="currentImage"
104
+ class="carousel-container relative w-full max-w-[407px] mx-auto z-1"
105
+ >
106
+ <UiImage
107
+ :src="currentImage.src"
108
+ :alt="currentImage.alt"
109
+ width="677"
110
+ height="394"
111
+ class="w-full h-auto mx-auto object-contain"
112
+ loading="lazy"
113
+ fit="contain"
114
+ />
115
+ </div>
116
+ </div>
117
+
118
+ <div class="flex-[0_0_52%] relative max-lg:w-fit max-lg:text-center">
119
+ <UStepper
120
+ orientation="vertical"
121
+ class="step-container max-lg:justify-center"
122
+ :model-value="currentStep + 1"
123
+ :items="stepperItems"
124
+ :size="size"
125
+ :linear="false"
126
+ @update:model-value="onStepChange(Number($event) - 1)"
127
+ >
128
+ <template #indicator="{ item }">
129
+ <UiIcon v-if="item.icon" class="size-icon-xl max-lg:size-icon-lg" :name="item.icon" />
130
+ </template>
131
+ <template #title="{ item }"
132
+ ><button
133
+ type="button"
134
+ class="w-full text-start"
135
+ @click="onStepChange(Number(item.value) - 1)"
136
+ >
137
+ {{ item.title }}
138
+ </button></template
139
+ >
140
+ </UStepper>
141
+ </div>
142
+ </div>
143
+
144
+ <slot name="footer">
145
+ <UiButton
146
+ v-if="showButton"
147
+ size="lg"
148
+ :label="buttonText"
149
+ class="flex justify-center mt-10 mx-auto max-lg:mt-0"
150
+ ripple
151
+ :to="to"
152
+ />
153
+ </slot>
154
+ </div>
155
+ </section>
156
+ </template>
157
+
158
+ <style scoped>
159
+ .section-subtitle {
160
+ text-align: center;
161
+ color: var(--site-color-muted);
162
+ font-size: 18px;
163
+ line-height: 21px;
164
+ margin-bottom: 60px;
165
+ }
166
+
167
+ .carousel-container {
168
+ position: relative;
169
+ width: 100%;
170
+ max-width: 407px;
171
+ margin: 0 auto;
172
+ }
173
+
174
+ .carousel-container::after {
175
+ content: "";
176
+ position: absolute;
177
+ top: 8px;
178
+ left: 50%;
179
+ width: 444px;
180
+ height: 444px;
181
+ background: color-mix(in srgb, var(--site-color-primary) 10%, var(--site-color-surface));
182
+ border-radius: 50%;
183
+ z-index: -1;
184
+ transform: translateX(-50%);
185
+ }
186
+
187
+ .step-container :deep(> div) {
188
+ gap: 48px;
189
+ }
190
+
191
+ .step-container :deep(.group) {
192
+ align-items: center;
193
+ gap: 48px;
194
+ }
195
+
196
+ .step-container :deep(.u-stepper-indicator) {
197
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--site-color-primary) 50%, transparent);
198
+ border-color: var(--site-color-surface);
199
+ }
200
+
201
+ .step-container :deep(.step-indicator) {
202
+ position: relative;
203
+ display: flex;
204
+ align-items: center;
205
+ justify-content: center;
206
+ color: var(--site-color-surface);
207
+ font-size: 28px;
208
+ font-weight: bold;
209
+ border-radius: 50%;
210
+ }
211
+
212
+ .step-container :deep([data-state="active"] .step-indicator),
213
+ .step-container :deep([data-state="completed"] .step-indicator) {
214
+ background-color: var(--color-primary);
215
+ box-shadow:
216
+ 0 0 0 2px var(--site-color-surface),
217
+ 0 0 0 4px var(--color-primary);
218
+ }
219
+
220
+ .step-container :deep([data-state="inactive"] .step-indicator) {
221
+ background-color: var(--site-color-border-strong);
222
+ box-shadow:
223
+ 0 0 0 2px var(--site-color-surface),
224
+ 0 0 0 4px var(--site-color-border-strong);
225
+ }
226
+
227
+ .step-container :deep(.step-separator) {
228
+ width: 1px;
229
+ height: calc((100% - 100px) / 2);
230
+ left: 24px;
231
+ background-color: var(--site-color-border-strong);
232
+ top: 80px;
233
+ z-index: 0;
234
+ }
235
+
236
+ [dir="rtl"] .step-container :deep(.step-separator) {
237
+ left: auto;
238
+ right: 24px;
239
+ }
240
+
241
+ .step-container :deep(.step-item:nth-child(2) .step-separator) {
242
+ top: calc(50% - 24px);
243
+ }
244
+
245
+ .step-container :deep([data-state="completed"] + .step-separator),
246
+ .step-container :deep([data-state="active"] + .step-separator) {
247
+ background-color: var(--color-primary);
248
+ }
249
+
250
+ .step-container :deep([data-state="inactive"] .step-wrapper) {
251
+ background-color: var(--site-color-surface-muted);
252
+ }
253
+
254
+ .step-container :deep(.step-wrapper) {
255
+ display: flex;
256
+ flex-direction: column;
257
+ margin-top: 0;
258
+ padding: 30px;
259
+ background: var(--color-primary);
260
+ border-radius: 10px;
261
+ width: 100%;
262
+ max-width: 566px;
263
+ color: var(--site-color-primary-contrast);
264
+ cursor: pointer;
265
+ }
266
+
267
+ .step-container :deep(.step-title) {
268
+ color: var(--site-color-primary-contrast);
269
+ font-size: 28px;
270
+ margin-bottom: 10px;
271
+ }
272
+
273
+ .step-container :deep([data-state="inactive"] .step-wrapper .step-title),
274
+ .step-container :deep([data-state="inactive"] .step-wrapper .step-description) {
275
+ color: var(--site-color-text);
276
+ }
277
+
278
+ @media (width <= 1024px) {
279
+ .step-container :deep(.step-description) {
280
+ padding-bottom: 30px !important;
281
+ }
282
+
283
+ .step-container :deep(.step-container) {
284
+ height: 100%;
285
+ }
286
+
287
+ .step-container :deep(.step-wrapper) {
288
+ max-width: 300px;
289
+ margin-top: 0;
290
+ padding: 0;
291
+ background: transparent;
292
+ color: var(--site-color-text);
293
+ }
294
+
295
+ .step-container :deep(> div) {
296
+ gap: 0;
297
+ }
298
+
299
+ .step-container :deep(.group) {
300
+ align-items: flex-start;
301
+ gap: 20px;
302
+ }
303
+
304
+ .step-container :deep(.step-separator) {
305
+ height: calc((100% - 100px) / 2);
306
+ top: 0;
307
+ left: 18px;
308
+ }
309
+
310
+ [dir="rtl"] .step-container :deep(.step-separator) {
311
+ left: auto;
312
+ right: 18px;
313
+ }
314
+
315
+ .step-container :deep(.step-item:nth-child(2) .step-separator) {
316
+ top: calc(33% + 18px);
317
+ }
318
+
319
+ .step-container :deep([data-state="inactive"] .step-wrapper) {
320
+ background: transparent;
321
+ }
322
+
323
+ .section-subtitle {
324
+ text-align: center;
325
+ color: var(--site-color-muted);
326
+ font-size: 18px;
327
+ line-height: 21px;
328
+ margin-bottom: 30px;
329
+ }
330
+ }
331
+
332
+ @media (width <= 768px) {
333
+ .step-container :deep(.step-wrapper) {
334
+ margin-top: 0;
335
+ padding: 0;
336
+ background: transparent;
337
+ color: var(--site-color-text);
338
+ }
339
+ }
340
+ </style>