@myelophone/nuxt 0.18.2

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 (259) hide show
  1. package/.devcontainer/Dockerfile +7 -0
  2. package/.devcontainer/devcontainer.json +21 -0
  3. package/.editorconfig +43 -0
  4. package/.github/workflows/package.yml +73 -0
  5. package/.nuxtignore +6 -0
  6. package/.releaserc.js +132 -0
  7. package/.yarn/releases/yarn-4.18.0.cjs +1000 -0
  8. package/.yarnrc.yml +8 -0
  9. package/CHANGELOG.md +141 -0
  10. package/LICENSE +77 -0
  11. package/README.md +3004 -0
  12. package/app/app.vue +455 -0
  13. package/app/assets/css/animation.css +81 -0
  14. package/app/assets/css/backgrounds.css +57 -0
  15. package/app/assets/css/base.css +182 -0
  16. package/app/assets/css/index.css +9 -0
  17. package/app/assets/css/links.css +12 -0
  18. package/app/assets/css/multimedia.css +102 -0
  19. package/app/assets/css/preloader.css +68 -0
  20. package/app/assets/css/print.css +40 -0
  21. package/app/assets/css/tailwind.css +20 -0
  22. package/app/assets/css/theme.css +29 -0
  23. package/app/components/consent/GoogleMap.vue +59 -0
  24. package/app/components/consent/Youtube.vue +27 -0
  25. package/app/components/cookie/Banner.vue +70 -0
  26. package/app/components/cookie/ConsentWrapper.vue +71 -0
  27. package/app/components/cookie/PrivacyPolicy.vue +179 -0
  28. package/app/components/cookie/SettingsModal.vue +387 -0
  29. package/app/components/grid/Col.vue +262 -0
  30. package/app/components/grid/Container.vue +236 -0
  31. package/app/components/grid/Row.vue +248 -0
  32. package/app/components/grid/Stack.vue +233 -0
  33. package/app/components/myelophone/Copyright.vue +5 -0
  34. package/app/components/myelophone/Welcome.vue +107 -0
  35. package/app/components/page/FullscreenPreloader.vue +177 -0
  36. package/app/components/page/Preloader.vue +18 -0
  37. package/app/components/page/ReadyLocalized.vue +19 -0
  38. package/app/components/ready/.gitkeep +0 -0
  39. package/app/components/safe/Email.vue +52 -0
  40. package/app/components/safe/ImgWithLoader.vue +180 -0
  41. package/app/components/safe/NuxtImg.vue +25 -0
  42. package/app/components/safe/NuxtPicture.vue +29 -0
  43. package/app/components/section/.gitkeep +0 -0
  44. package/app/components/seo/BrandInfo.vue +76 -0
  45. package/app/components/seo/ContentNoIndex.vue +35 -0
  46. package/app/components/seo/NoIndex.vue +22 -0
  47. package/app/components/seo/OrgInfo.vue +130 -0
  48. package/app/components/seo/ProfileInfo.vue +137 -0
  49. package/app/components/ui/Accordion.vue +163 -0
  50. package/app/components/ui/Alert.vue +179 -0
  51. package/app/components/ui/Avatar.vue +88 -0
  52. package/app/components/ui/Badge.vue +150 -0
  53. package/app/components/ui/Banner.vue +133 -0
  54. package/app/components/ui/Button.vue +175 -0
  55. package/app/components/ui/Card.vue +66 -0
  56. package/app/components/ui/Checkbox.vue +134 -0
  57. package/app/components/ui/Chip.vue +136 -0
  58. package/app/components/ui/CommandPalette.vue +434 -0
  59. package/app/components/ui/Console.vue +583 -0
  60. package/app/components/ui/CursorCreative.vue +148 -0
  61. package/app/components/ui/FeatureAccordion.vue +165 -0
  62. package/app/components/ui/FileUpload.vue +705 -0
  63. package/app/components/ui/GeoDependent.vue +72 -0
  64. package/app/components/ui/Heading.vue +51 -0
  65. package/app/components/ui/Icon.vue +42 -0
  66. package/app/components/ui/Icon8.vue +39 -0
  67. package/app/components/ui/Input.vue +88 -0
  68. package/app/components/ui/LangLink.vue +34 -0
  69. package/app/components/ui/LangVisible.vue +33 -0
  70. package/app/components/ui/LanguageSelect.vue +202 -0
  71. package/app/components/ui/LightBox.vue +155 -0
  72. package/app/components/ui/LocalTime.vue +170 -0
  73. package/app/components/ui/Modal.vue +161 -0
  74. package/app/components/ui/Popup.vue +200 -0
  75. package/app/components/ui/PostContent.server.vue +23 -0
  76. package/app/components/ui/ProtectedEmail.vue +52 -0
  77. package/app/components/ui/RelativeTime.vue +141 -0
  78. package/app/components/ui/ScheduledContent.vue +150 -0
  79. package/app/components/ui/ScrollToTop.client.vue +48 -0
  80. package/app/components/ui/SegmentedControl.vue +148 -0
  81. package/app/components/ui/SiteSearch.vue +1191 -0
  82. package/app/components/ui/Skeleton.vue +91 -0
  83. package/app/components/ui/SmartContrast.vue +74 -0
  84. package/app/components/ui/SnapContainer.vue +222 -0
  85. package/app/components/ui/SnapSection.vue +294 -0
  86. package/app/components/ui/SonnerStyle.vue +7 -0
  87. package/app/components/ui/SplitSection.vue +99 -0
  88. package/app/components/ui/Steps.vue +858 -0
  89. package/app/components/ui/StickyWrapper.vue +46 -0
  90. package/app/components/ui/Table.vue +251 -0
  91. package/app/components/ui/Tabs.vue +451 -0
  92. package/app/components/ui/TextColumn.vue +36 -0
  93. package/app/components/ui/Textarea.vue +251 -0
  94. package/app/components/ui/Toggler.vue +40 -0
  95. package/app/components/ui/Tooltip.vue +524 -0
  96. package/app/components/ui/TruncateText.vue +105 -0
  97. package/app/components/ui/ViewportSpacer.vue +72 -0
  98. package/app/components/view/AnimatedCounter.vue +121 -0
  99. package/app/components/view/AnnouncementBar.vue +104 -0
  100. package/app/components/view/AvatarGroup.vue +104 -0
  101. package/app/components/view/Breadcrumbs.vue +97 -0
  102. package/app/components/view/ContentWithToc.vue +152 -0
  103. package/app/components/view/Copyright.vue +68 -0
  104. package/app/components/view/CountdownTimer.vue +184 -0
  105. package/app/components/view/CreativeCTA.vue +143 -0
  106. package/app/components/view/CurrencySelect.vue +254 -0
  107. package/app/components/view/FeatureBoxGrid.vue +142 -0
  108. package/app/components/view/HorizontalMenu.vue +97 -0
  109. package/app/components/view/ImageCompare.vue +141 -0
  110. package/app/components/view/ImgSlider.vue +333 -0
  111. package/app/components/view/InfiniteMarquee.vue +147 -0
  112. package/app/components/view/InfoBar.vue +86 -0
  113. package/app/components/view/LogoGrid.vue +106 -0
  114. package/app/components/view/LogoSlider.vue +83 -0
  115. package/app/components/view/NotFound.vue +81 -0
  116. package/app/components/view/PartialBackground.vue +313 -0
  117. package/app/components/view/QuoteBig.vue +111 -0
  118. package/app/components/view/ResponsiveMenu.vue +219 -0
  119. package/app/components/view/ReviewsSlider.vue +230 -0
  120. package/app/components/view/SocialBar.vue +78 -0
  121. package/app/components/view/StickyHeadingContent.vue +158 -0
  122. package/app/components/view/TallyForm.vue +295 -0
  123. package/app/components/view/WarningOutdated.vue +113 -0
  124. package/app/composables/useApi.ts +88 -0
  125. package/app/composables/useAppSeo.ts +78 -0
  126. package/app/composables/useBreadcrumbs.ts +106 -0
  127. package/app/composables/useChangeUrl.ts +73 -0
  128. package/app/composables/useCommand.ts +28 -0
  129. package/app/composables/useCookieControl.ts +111 -0
  130. package/app/composables/useCookieModal.ts +9 -0
  131. package/app/composables/useDevice.ts +109 -0
  132. package/app/composables/useGeoFetch.ts +139 -0
  133. package/app/composables/useLoadScript.ts +36 -0
  134. package/app/composables/useModalState.ts +37 -0
  135. package/app/composables/useMultiLang.ts +278 -0
  136. package/app/composables/useNetwork.ts +49 -0
  137. package/app/composables/useNuxtVersion.ts +5 -0
  138. package/app/composables/usePiniaStore.ts +5 -0
  139. package/app/composables/usePopupControl.ts +57 -0
  140. package/app/composables/useQuery.ts +307 -0
  141. package/app/composables/useReadyPage.ts +182 -0
  142. package/app/composables/useSafeList.ts +90 -0
  143. package/app/composables/useScriptLoader.ts +221 -0
  144. package/app/composables/useSiteSearch.ts +32 -0
  145. package/app/composables/useSnapScroll.ts +24 -0
  146. package/app/composables/useStatic.ts +5 -0
  147. package/app/composables/useStorage.ts +252 -0
  148. package/app/composables/useStoreBroadcast.ts +146 -0
  149. package/app/composables/useThemeSync.ts +81 -0
  150. package/app/constants/layout.ts +109 -0
  151. package/app/constants/predefinedCookieScripts.ts +1222 -0
  152. package/app/constants/ui-events.ts +1 -0
  153. package/app/error.vue +169 -0
  154. package/app/layouts/default-blog.vue +5 -0
  155. package/app/layouts/default.vue +3 -0
  156. package/app/locales/en/commands.json +5 -0
  157. package/app/locales/en/cookies.json +182 -0
  158. package/app/locales/en/error.json +4 -0
  159. package/app/locales/en/interface.json +33 -0
  160. package/app/locales/en/myelophone.json +8 -0
  161. package/app/locales/en/outdated.json +5 -0
  162. package/app/locales/en/times.json +18 -0
  163. package/app/locales/pl/commands.json +5 -0
  164. package/app/locales/pl/cookies.json +182 -0
  165. package/app/locales/pl/error.json +4 -0
  166. package/app/locales/pl/interface.json +33 -0
  167. package/app/locales/pl/myelophone.json +8 -0
  168. package/app/locales/pl/outdated.json +5 -0
  169. package/app/locales/pl/times.json +18 -0
  170. package/app/locales/ru/commands.json +5 -0
  171. package/app/locales/ru/cookies.json +182 -0
  172. package/app/locales/ru/error.json +4 -0
  173. package/app/locales/ru/interface.json +33 -0
  174. package/app/locales/ru/myelophone.json +8 -0
  175. package/app/locales/ru/outdated.json +5 -0
  176. package/app/locales/ru/times.json +18 -0
  177. package/app/middleware/blog-layout.ts +28 -0
  178. package/app/middleware/i18n.global.ts +17 -0
  179. package/app/middleware/not-found-status.ts +11 -0
  180. package/app/modules/default-blog/index.ts +39 -0
  181. package/app/modules/default-blog/posts.ts +80 -0
  182. package/app/modules/default-sitemap/index.ts +116 -0
  183. package/app/modules/default-sitemap/runtime/sitemap.get.ts +42 -0
  184. package/app/modules/fullscreen-preloader/module.ts +43 -0
  185. package/app/modules/htaccess/.htaccess +38 -0
  186. package/app/modules/htaccess/index.ts +23 -0
  187. package/app/modules/i18n-shaker.mjs +221 -0
  188. package/app/modules/multi18n/module.ts +71 -0
  189. package/app/modules/myelophone-config/module.ts +241 -0
  190. package/app/pages/[...all].vue +9 -0
  191. package/app/pages/index.vue +3 -0
  192. package/app/pages/post/[...slug].vue +30 -0
  193. package/app/plugins/bfcache-refresh.client.ts +9 -0
  194. package/app/plugins/canonical.ts +30 -0
  195. package/app/plugins/csp-nonce.server.ts +41 -0
  196. package/app/plugins/multi18n.client.ts +21 -0
  197. package/app/plugins/multi18n.server.ts +15 -0
  198. package/app/plugins/nuxt-version.client.ts +4 -0
  199. package/app/plugins/restore.client.ts +21 -0
  200. package/app/plugins/reveal.ts +89 -0
  201. package/app/plugins/url-helper.client.ts +66 -0
  202. package/app/server/api/exchange-rates.get.ts +216 -0
  203. package/app/server/api/healthz.ts +1 -0
  204. package/app/server/api/logout.post.ts +6 -0
  205. package/app/server/middleware/normalize-url.ts +14 -0
  206. package/app/server/middleware/security-headers.ts +147 -0
  207. package/app/server/middleware/url-helper.ts +43 -0
  208. package/app/server/plugins/clean-html-spaces.ts +15 -0
  209. package/app/server/plugins/clean-styles.ts +94 -0
  210. package/app/server/plugins/compression.ts +122 -0
  211. package/app/server/plugins/css-preload.ts +26 -0
  212. package/app/server/plugins/early-hints.ts +31 -0
  213. package/app/server/plugins/force-noindex.ts +43 -0
  214. package/app/server/plugins/html-one-line.ts +17 -0
  215. package/app/server/plugins/inline-style.ts +7 -0
  216. package/app/server/routes/healthz.get.ts +1 -0
  217. package/app/server/tsconfig.json +3 -0
  218. package/app/stores/cart.ts +945 -0
  219. package/app/stores/commands.ts +43 -0
  220. package/app/stores/device.ts +156 -0
  221. package/app/stores/querySync.ts +5 -0
  222. package/app/stores/settings.ts +145 -0
  223. package/app/stores/user.ts +607 -0
  224. package/app/types/cookie.ts +89 -0
  225. package/app/types/myelophone.d.ts +87 -0
  226. package/app/types/site-search.ts +110 -0
  227. package/app/utils/asyncRefresh.ts +4 -0
  228. package/app/utils/blog.server.ts +142 -0
  229. package/app/utils/jsonLd.ts +56 -0
  230. package/app/utils/myelophoneConfig.ts +4 -0
  231. package/app/utils/sortBy.ts +18 -0
  232. package/app/utils/transliterate.ts +75 -0
  233. package/app/utils/ytVideo.ts +37 -0
  234. package/app/workers/site-search.worker.ts +661 -0
  235. package/biome.json +94 -0
  236. package/commitlint.config.js +3 -0
  237. package/nuxt.config.ts +539 -0
  238. package/package.json +101 -0
  239. package/playground/content/.gitkeep +0 -0
  240. package/playground/locales/.gitkeep +0 -0
  241. package/playground/myelophone.dev.ts +45 -0
  242. package/playground/nuxt.config.ts +105 -0
  243. package/playground/package.json +5 -0
  244. package/playground/pages/.gitkeep +0 -0
  245. package/playground/tsconfig.json +3 -0
  246. package/playwright.config.ts +23 -0
  247. package/public/browsers/chrome.svg +77 -0
  248. package/public/browsers/edge.svg +98 -0
  249. package/public/browsers/firefox.svg +300 -0
  250. package/public/favicon.ico +0 -0
  251. package/public/myelophone_eng.png +0 -0
  252. package/public/myelophone_eng_white.png +0 -0
  253. package/spa-loading-template.html +72 -0
  254. package/tests/home.spec.ts +9 -0
  255. package/tests/utils.ts +23 -0
  256. package/tsconfig.json +16 -0
  257. package/vite/plugins/clean-css.ts +190 -0
  258. package/vite/plugins/postcss-add-viewportunits.ts +33 -0
  259. package/vite/plugins/postcss-viewport-fallback.ts +49 -0
@@ -0,0 +1,858 @@
1
+ <script setup lang="ts">
2
+ type StepValue = string | number;
3
+ type StepsVariant = "progress" | "steps" | "cards";
4
+ type StepsPlacement = "top" | "bottom";
5
+ type StepsAlign = "start" | "center" | "end";
6
+ type StepStatus = "completed" | "active" | "locked" | "upcoming";
7
+
8
+ interface StepItem {
9
+ title: string;
10
+ value?: StepValue;
11
+ description?: string;
12
+ content?: string;
13
+ icon?: string;
14
+ disabled?: boolean;
15
+ requires?: StepValue | StepValue[];
16
+ requiresPrevious?: boolean;
17
+ [key: string]: unknown;
18
+ }
19
+
20
+ interface StepsClasses {
21
+ root?: string;
22
+ navigation?: string;
23
+ item?: string;
24
+ activeItem?: string;
25
+ completedItem?: string;
26
+ panel?: string;
27
+ }
28
+
29
+ interface Props {
30
+ items: StepItem[];
31
+ modelValue?: StepValue;
32
+ completed?: StepValue[];
33
+ defaultValue?: StepValue;
34
+ variant?: StepsVariant;
35
+ placement?: StepsPlacement;
36
+ align?: StepsAlign;
37
+ clickable?: boolean;
38
+ linear?: boolean;
39
+ requirePrevious?: boolean;
40
+ stackOnMobile?: boolean;
41
+ ariaLabel?: string;
42
+ ui?: StepsClasses;
43
+ }
44
+
45
+ const props = withDefaults(defineProps<Props>(), {
46
+ variant: "steps",
47
+ placement: "top",
48
+ align: "start",
49
+ clickable: true,
50
+ linear: false,
51
+ requirePrevious: false,
52
+ stackOnMobile: true,
53
+ ariaLabel: "Progress steps",
54
+ ui: () => ({}),
55
+ });
56
+
57
+ const emit = defineEmits<{
58
+ "update:modelValue": [value: StepValue];
59
+ "update:completed": [value: StepValue[]];
60
+ change: [value: StepValue, item: StepItem, index: number];
61
+ complete: [value: StepValue, item: StepItem, index: number];
62
+ uncomplete: [value: StepValue, item: StepItem, index: number];
63
+ }>();
64
+
65
+ const componentId = useId();
66
+ const stepButtons = ref<Array<HTMLButtonElement | null>>([]);
67
+
68
+ const getStepValue = (item: StepItem, index: number): StepValue =>
69
+ item.value ?? index;
70
+ const firstEnabledIndex = () => props.items.findIndex((item) => !item.disabled);
71
+ const firstEnabledValue = () => {
72
+ const index = firstEnabledIndex();
73
+ return index >= 0
74
+ ? getStepValue(props.items[index] as StepItem, index)
75
+ : undefined;
76
+ };
77
+
78
+ const internalValue = ref<StepValue | undefined>(
79
+ props.modelValue ?? props.defaultValue ?? firstEnabledValue(),
80
+ );
81
+ const completedValues = ref<StepValue[]>([...(props.completed ?? [])]);
82
+ const activeValue = computed(() => props.modelValue ?? internalValue.value);
83
+ const activeIndex = computed(() =>
84
+ props.items.findIndex(
85
+ (item, index) => getStepValue(item, index) === activeValue.value,
86
+ ),
87
+ );
88
+ const activeItem = computed(() => props.items[activeIndex.value]);
89
+ const furthestIndex = ref(Math.max(activeIndex.value, 0));
90
+
91
+ const enabledIndices = computed(() =>
92
+ props.items.flatMap((item, index) => (item.disabled ? [] : [index])),
93
+ );
94
+ const progress = computed(() => {
95
+ if (props.items.length <= 1) return props.items.length ? 100 : 0;
96
+ return (Math.max(0, activeIndex.value + 1) / props.items.length) * 100;
97
+ });
98
+ const progressStyle = computed(() => ({ width: `${progress.value}%` }));
99
+ const progressSegmentsStyle = computed(() => ({
100
+ gridTemplateColumns: `repeat(${Math.max(props.items.length, 1)}, minmax(0, 1fr))`,
101
+ }));
102
+
103
+ const rootClasses = computed(() => [
104
+ "ui-steps",
105
+ `ui-steps--${props.variant}`,
106
+ `ui-steps--placement-${props.placement}`,
107
+ `ui-steps--align-${props.align}`,
108
+ { "ui-steps--stack-mobile": props.stackOnMobile },
109
+ props.ui.root,
110
+ ]);
111
+
112
+ const isCompleted = (target: StepValue) => {
113
+ const index = resolveIndex(target);
114
+ const item = props.items[index];
115
+ return item
116
+ ? completedValues.value.includes(getStepValue(item, index))
117
+ : false;
118
+ };
119
+
120
+ const previousEnabledIndex = (index: number) => {
121
+ for (let candidate = index - 1; candidate >= 0; candidate--) {
122
+ if (!props.items[candidate]?.disabled) return candidate;
123
+ }
124
+ return -1;
125
+ };
126
+
127
+ const completionRequirementsMet = (item: StepItem, index: number) => {
128
+ const requirements =
129
+ item.requires === undefined
130
+ ? []
131
+ : Array.isArray(item.requires)
132
+ ? item.requires
133
+ : [item.requires];
134
+ if (!requirements.every((value) => isCompleted(value))) return false;
135
+
136
+ if (item.requiresPrevious ?? props.requirePrevious) {
137
+ const previousIndex = previousEnabledIndex(index);
138
+ if (previousIndex >= 0) {
139
+ const previousItem = props.items[previousIndex] as StepItem;
140
+ if (
141
+ !completedValues.value.includes(
142
+ getStepValue(previousItem, previousIndex),
143
+ )
144
+ )
145
+ return false;
146
+ }
147
+ }
148
+
149
+ return true;
150
+ };
151
+
152
+ const canSelect = (item: StepItem, index: number) => {
153
+ if (item.disabled) return false;
154
+ if (!completionRequirementsMet(item, index)) return false;
155
+ if (!props.linear) return true;
156
+ return index <= furthestIndex.value + 1;
157
+ };
158
+
159
+ const statusFor = (index: number): StepStatus => {
160
+ const item = props.items[index];
161
+ if (!item) return "upcoming";
162
+ if (index === activeIndex.value) return "active";
163
+ if (completedValues.value.includes(getStepValue(item, index)))
164
+ return "completed";
165
+ if (!canSelect(item, index)) return "locked";
166
+ return "upcoming";
167
+ };
168
+
169
+ const selectStep = (item: StepItem, index: number, force = false) => {
170
+ if ((!props.clickable && !force) || !canSelect(item, index)) return false;
171
+
172
+ const value = getStepValue(item, index);
173
+ if (value === activeValue.value) return true;
174
+
175
+ internalValue.value = value;
176
+ furthestIndex.value = Math.max(furthestIndex.value, index);
177
+ emit("update:modelValue", value);
178
+ emit("change", value, item, index);
179
+ return true;
180
+ };
181
+
182
+ const resolveIndex = (target: StepValue) => {
183
+ const valueIndex = props.items.findIndex(
184
+ (item, index) => getStepValue(item, index) === target,
185
+ );
186
+ if (valueIndex >= 0) return valueIndex;
187
+ return typeof target === "number" && Number.isInteger(target) ? target : -1;
188
+ };
189
+
190
+ const goTo = (target: StepValue) => {
191
+ const index = resolveIndex(target);
192
+ const item = props.items[index];
193
+ return item ? selectStep(item, index, true) : false;
194
+ };
195
+
196
+ const canGoTo = (target: StepValue) => {
197
+ const index = resolveIndex(target);
198
+ const item = props.items[index];
199
+ return item ? canSelect(item, index) : false;
200
+ };
201
+
202
+ const complete = (target: StepValue = activeValue.value as StepValue) => {
203
+ const index = resolveIndex(target);
204
+ const item = props.items[index];
205
+ if (!item || item.disabled) return false;
206
+
207
+ const value = getStepValue(item, index);
208
+ if (completedValues.value.includes(value)) return true;
209
+ completedValues.value = [...completedValues.value, value];
210
+ emit("update:completed", [...completedValues.value]);
211
+ emit("complete", value, item, index);
212
+ return true;
213
+ };
214
+
215
+ const uncomplete = (target: StepValue = activeValue.value as StepValue) => {
216
+ const index = resolveIndex(target);
217
+ const item = props.items[index];
218
+ if (!item) return false;
219
+
220
+ const value = getStepValue(item, index);
221
+ if (!completedValues.value.includes(value)) return true;
222
+ completedValues.value = completedValues.value.filter(
223
+ (completedValue) => completedValue !== value,
224
+ );
225
+ emit("update:completed", [...completedValues.value]);
226
+ emit("uncomplete", value, item, index);
227
+ return true;
228
+ };
229
+
230
+ const completeAndNext = () => {
231
+ if (!complete()) return false;
232
+ return nextTick().then(() => next());
233
+ };
234
+
235
+ const move = (direction: 1 | -1) => {
236
+ let index = activeIndex.value + direction;
237
+ while (index >= 0 && index < props.items.length) {
238
+ const item = props.items[index];
239
+ if (item && !item.disabled) return selectStep(item, index, true);
240
+ index += direction;
241
+ }
242
+ return false;
243
+ };
244
+
245
+ const next = () => move(1);
246
+ const previous = () => move(-1);
247
+
248
+ const focusStep = (index: number) => {
249
+ const item = props.items[index];
250
+ if (!item || !canSelect(item, index)) return;
251
+ stepButtons.value[index]?.focus();
252
+ selectStep(item, index);
253
+ };
254
+
255
+ const moveFocus = (currentIndex: number, direction: 1 | -1) => {
256
+ const selectable = enabledIndices.value.filter((index) => {
257
+ const item = props.items[index];
258
+ return item ? canSelect(item, index) : false;
259
+ });
260
+ const position = selectable.indexOf(currentIndex);
261
+ if (position < 0 || !selectable.length) return;
262
+ const nextPosition = Math.min(
263
+ Math.max(position + direction, 0),
264
+ selectable.length - 1,
265
+ );
266
+ focusStep(selectable[nextPosition] as number);
267
+ };
268
+
269
+ const onStepKeydown = (event: KeyboardEvent, index: number) => {
270
+ if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
271
+ event.preventDefault();
272
+ moveFocus(index, -1);
273
+ } else if (event.key === "ArrowRight" || event.key === "ArrowDown") {
274
+ event.preventDefault();
275
+ moveFocus(index, 1);
276
+ } else if (event.key === "Home") {
277
+ event.preventDefault();
278
+ focusStep(enabledIndices.value[0] as number);
279
+ } else if (event.key === "End") {
280
+ event.preventDefault();
281
+ focusStep(enabledIndices.value.at(-1) as number);
282
+ }
283
+ };
284
+
285
+ watch(
286
+ () => props.modelValue,
287
+ (value) => {
288
+ if (value === undefined) return;
289
+ const index = resolveIndex(value);
290
+ if (index >= 0)
291
+ furthestIndex.value = Math.max(furthestIndex.value, index);
292
+ },
293
+ );
294
+
295
+ watch(
296
+ () => props.completed,
297
+ (value) => {
298
+ if (value) completedValues.value = [...value];
299
+ },
300
+ { deep: true },
301
+ );
302
+
303
+ watch(
304
+ () =>
305
+ props.items.map((item, index) => [
306
+ getStepValue(item, index),
307
+ item.disabled,
308
+ ]),
309
+ () => {
310
+ if (activeIndex.value >= 0 && !activeItem.value?.disabled) return;
311
+ internalValue.value = firstEnabledValue();
312
+ },
313
+ { immediate: true },
314
+ );
315
+
316
+ onBeforeUpdate(() => {
317
+ stepButtons.value = [];
318
+ });
319
+
320
+ defineExpose({
321
+ goTo,
322
+ next,
323
+ previous,
324
+ complete,
325
+ uncomplete,
326
+ completeAndNext,
327
+ isCompleted,
328
+ canGoTo,
329
+ activeIndex,
330
+ activeItem,
331
+ activeValue,
332
+ });
333
+ </script>
334
+
335
+ <template>
336
+ <div :class="rootClasses">
337
+ <nav
338
+ v-if="items.length"
339
+ :class="['ui-steps__navigation', ui.navigation]"
340
+ :aria-label="ariaLabel"
341
+ >
342
+ <div v-if="variant === 'progress'" class="ui-steps__progress">
343
+ <div class="ui-steps__progress-meta">
344
+ <span class="ui-steps__progress-title">{{
345
+ activeItem?.title
346
+ }}</span>
347
+ <span class="ui-steps__progress-count">
348
+ {{ Math.max(activeIndex + 1, 0) }} / {{ items.length }}
349
+ </span>
350
+ </div>
351
+ <div class="ui-steps__progress-track">
352
+ <span
353
+ class="ui-steps__progress-fill"
354
+ :style="progressStyle"
355
+ role="progressbar"
356
+ :aria-valuemin="1"
357
+ :aria-valuemax="items.length"
358
+ :aria-valuenow="activeIndex + 1"
359
+ :aria-valuetext="activeItem?.title"
360
+ ></span>
361
+ <div
362
+ class="ui-steps__progress-segments"
363
+ :style="progressSegmentsStyle"
364
+ >
365
+ <button
366
+ v-for="(item, index) in items"
367
+ :key="getStepValue(item, index)"
368
+ type="button"
369
+ class="ui-steps__progress-target"
370
+ :disabled="!clickable || !canSelect(item, index)"
371
+ :aria-label="`${index + 1}. ${item.title}`"
372
+ :aria-current="
373
+ activeIndex === index ? 'step' : undefined
374
+ "
375
+ @click="selectStep(item, index)"
376
+ ></button>
377
+ </div>
378
+ </div>
379
+ </div>
380
+
381
+ <ol
382
+ v-else
383
+ :class="['ui-steps__list', `ui-steps__list--${variant}`]"
384
+ >
385
+ <template
386
+ v-for="(item, index) in items"
387
+ :key="getStepValue(item, index)"
388
+ >
389
+ <li
390
+ :class="[
391
+ 'ui-steps__list-item',
392
+ `ui-steps__list-item--${statusFor(index)}`,
393
+ ui.item,
394
+ activeIndex === index && ui.activeItem,
395
+ statusFor(index) === 'completed' &&
396
+ ui.completedItem,
397
+ ]"
398
+ >
399
+ <button
400
+ :id="`${componentId}-step-${index}`"
401
+ :ref="
402
+ (element) =>
403
+ (stepButtons[index] =
404
+ element as HTMLButtonElement | null)
405
+ "
406
+ type="button"
407
+ class="ui-steps__step"
408
+ :disabled="!clickable || !canSelect(item, index)"
409
+ :tabindex="activeIndex === index ? 0 : -1"
410
+ :aria-current="
411
+ activeIndex === index ? 'step' : undefined
412
+ "
413
+ :aria-controls="`${componentId}-panel`"
414
+ @click="selectStep(item, index)"
415
+ @keydown="onStepKeydown($event, index)"
416
+ >
417
+ <slot
418
+ name="step"
419
+ :item="item"
420
+ :index="index"
421
+ :status="statusFor(index)"
422
+ :active="activeIndex === index"
423
+ >
424
+ <span
425
+ class="ui-steps__marker"
426
+ aria-hidden="true"
427
+ >
428
+ <slot
429
+ name="icon"
430
+ :item="item"
431
+ :index="index"
432
+ :status="statusFor(index)"
433
+ >
434
+ <UiIcon
435
+ v-if="item.icon"
436
+ :name="item.icon"
437
+ class="ui-steps__icon"
438
+ />
439
+ <span v-else>{{ index + 1 }}</span>
440
+ </slot>
441
+ </span>
442
+ <span class="ui-steps__copy">
443
+ <span class="ui-steps__title">{{
444
+ item.title
445
+ }}</span>
446
+ <span
447
+ v-if="item.description"
448
+ class="ui-steps__description"
449
+ >{{ item.description }}</span
450
+ >
451
+ </span>
452
+ </slot>
453
+ </button>
454
+ </li>
455
+
456
+ <li
457
+ v-if="variant === 'steps' && index < items.length - 1"
458
+ class="ui-steps__connector"
459
+ :class="{
460
+ 'ui-steps__connector--completed': isCompleted(
461
+ getStepValue(item, index),
462
+ ),
463
+ }"
464
+ aria-hidden="true"
465
+ ></li>
466
+ </template>
467
+ </ol>
468
+ </nav>
469
+ <slot v-else name="empty"></slot>
470
+
471
+ <div
472
+ v-if="activeItem && ($slots.default || activeItem.content)"
473
+ :id="`${componentId}-panel`"
474
+ :key="activeValue"
475
+ :class="['ui-steps__panel', ui.panel]"
476
+ role="region"
477
+ :aria-labelledby="
478
+ variant === 'progress'
479
+ ? undefined
480
+ : `${componentId}-step-${activeIndex}`
481
+ "
482
+ >
483
+ <slot
484
+ :item="activeItem"
485
+ :index="activeIndex"
486
+ :value="activeValue"
487
+ :go-to="goTo"
488
+ :next="next"
489
+ :previous="previous"
490
+ :complete="complete"
491
+ :uncomplete="uncomplete"
492
+ :complete-and-next="completeAndNext"
493
+ :is-completed="isCompleted"
494
+ :can-go-to="canGoTo"
495
+ :is-first="activeIndex === firstEnabledIndex()"
496
+ :is-last="activeIndex === enabledIndices.at(-1)"
497
+ >
498
+ {{ activeItem.content }}
499
+ </slot>
500
+ </div>
501
+ </div>
502
+ </template>
503
+
504
+ <style scoped>
505
+ .ui-steps {
506
+ display: flex;
507
+ width: 100%;
508
+ min-width: 0;
509
+ flex-direction: column;
510
+ gap: 1.25rem;
511
+ color: var(--ui-text);
512
+ --ui-steps-accent: var(--ui-primary, #00a968);
513
+ --ui-steps-muted: color-mix(in srgb, var(--ui-text) 46%, transparent);
514
+ --ui-steps-border: color-mix(in srgb, var(--ui-text) 16%, transparent);
515
+ --ui-steps-surface: color-mix(in srgb, var(--ui-bg) 92%, var(--ui-text) 8%);
516
+ }
517
+
518
+ .ui-steps--placement-bottom .ui-steps__navigation {
519
+ order: 2;
520
+ }
521
+
522
+ .ui-steps__navigation {
523
+ min-width: 0;
524
+ max-width: 100%;
525
+ }
526
+
527
+ .ui-steps--align-start .ui-steps__navigation {
528
+ align-self: flex-start;
529
+ }
530
+
531
+ .ui-steps--align-center .ui-steps__navigation {
532
+ align-self: center;
533
+ }
534
+
535
+ .ui-steps--align-end .ui-steps__navigation {
536
+ align-self: flex-end;
537
+ }
538
+
539
+ .ui-steps__progress {
540
+ width: min(32rem, calc(100vw - 2rem));
541
+ }
542
+
543
+ .ui-steps__progress-meta {
544
+ display: flex;
545
+ align-items: baseline;
546
+ justify-content: space-between;
547
+ gap: 1rem;
548
+ margin-bottom: 0.65rem;
549
+ }
550
+
551
+ .ui-steps__progress-title {
552
+ min-width: 0;
553
+ font-weight: 700;
554
+ }
555
+
556
+ .ui-steps__progress-count {
557
+ flex: none;
558
+ font-size: 0.8rem;
559
+ color: var(--ui-steps-muted);
560
+ }
561
+
562
+ .ui-steps__progress-track {
563
+ position: relative;
564
+ height: 0.5rem;
565
+ background: var(--ui-steps-border);
566
+ border-radius: 999px;
567
+ }
568
+
569
+ .ui-steps__progress-fill {
570
+ position: absolute;
571
+ inset: 0 auto 0 0;
572
+ background: var(--ui-steps-accent);
573
+ border-radius: inherit;
574
+ transition: width 220ms ease;
575
+ }
576
+
577
+ .ui-steps__progress-segments {
578
+ position: absolute;
579
+ inset: -0.65rem 0;
580
+ display: grid;
581
+ }
582
+
583
+ .ui-steps__progress-target {
584
+ min-width: 0;
585
+ padding: 0;
586
+ background: transparent;
587
+ border: 0;
588
+ cursor: pointer;
589
+ }
590
+
591
+ .ui-steps__progress-target:disabled {
592
+ cursor: default;
593
+ }
594
+
595
+ .ui-steps__progress-target:focus-visible {
596
+ outline: 2px solid var(--ui-steps-accent);
597
+ outline-offset: 0.25rem;
598
+ border-radius: 0.25rem;
599
+ }
600
+
601
+ .ui-steps__list {
602
+ display: flex;
603
+ max-width: 100%;
604
+ margin: 0;
605
+ padding: 0;
606
+ list-style: none;
607
+ }
608
+
609
+ .ui-steps__list--steps {
610
+ align-items: flex-start;
611
+ padding: 0.3rem;
612
+ overflow-x: auto;
613
+ scrollbar-width: thin;
614
+ }
615
+
616
+ .ui-steps__list--cards {
617
+ display: grid;
618
+ grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
619
+ gap: 0.75rem;
620
+ }
621
+
622
+ .ui-steps--cards .ui-steps__navigation {
623
+ width: 100%;
624
+ }
625
+
626
+ .ui-steps--align-start .ui-steps__list {
627
+ justify-content: flex-start;
628
+ }
629
+
630
+ .ui-steps--align-center .ui-steps__list {
631
+ justify-content: center;
632
+ }
633
+
634
+ .ui-steps--align-end .ui-steps__list {
635
+ justify-content: flex-end;
636
+ }
637
+
638
+ .ui-steps__list-item {
639
+ min-width: 0;
640
+ }
641
+
642
+ .ui-steps__list--steps .ui-steps__list-item {
643
+ width: max-content;
644
+ max-width: 10rem;
645
+ }
646
+
647
+ .ui-steps__step {
648
+ display: flex;
649
+ min-width: 0;
650
+ padding: 0;
651
+ font: inherit;
652
+ color: inherit;
653
+ text-align: left;
654
+ background: transparent;
655
+ border: 0;
656
+ cursor: pointer;
657
+ }
658
+
659
+ .ui-steps__step:disabled {
660
+ cursor: not-allowed;
661
+ }
662
+
663
+ .ui-steps__step:focus-visible {
664
+ outline: 2px solid var(--ui-steps-accent);
665
+ outline-offset: 0.3rem;
666
+ border-radius: 0.5rem;
667
+ }
668
+
669
+ .ui-steps__list--steps .ui-steps__step {
670
+ flex-direction: column;
671
+ align-items: center;
672
+ gap: 0.55rem;
673
+ text-align: center;
674
+ }
675
+
676
+ .ui-steps__marker {
677
+ display: inline-flex;
678
+ width: 2.35rem;
679
+ height: 2.35rem;
680
+ flex: 0 0 2.35rem;
681
+ align-items: center;
682
+ justify-content: center;
683
+ font-size: 0.85rem;
684
+ font-weight: 800;
685
+ color: var(--ui-steps-muted);
686
+ background: var(--ui-bg);
687
+ border: 2px solid var(--ui-steps-border);
688
+ border-radius: 999px;
689
+ transition: 160ms ease;
690
+ }
691
+
692
+ .ui-steps__icon {
693
+ width: 1.1rem;
694
+ height: 1.1rem;
695
+ }
696
+
697
+ .ui-steps__copy {
698
+ display: flex;
699
+ min-width: 0;
700
+ flex-direction: column;
701
+ gap: 0.18rem;
702
+ }
703
+
704
+ .ui-steps__title {
705
+ font-size: 0.9rem;
706
+ font-weight: 700;
707
+ line-height: 1.25;
708
+ }
709
+
710
+ .ui-steps__description {
711
+ font-size: 0.78rem;
712
+ line-height: 1.3;
713
+ color: var(--ui-steps-muted);
714
+ }
715
+
716
+ .ui-steps__list-item--active .ui-steps__marker {
717
+ color: white;
718
+ background: var(--ui-steps-accent);
719
+ border-color: var(--ui-steps-accent);
720
+ box-shadow: 0 0 0 0.25rem
721
+ color-mix(in srgb, var(--ui-steps-accent) 18%, transparent);
722
+ }
723
+
724
+ .ui-steps__list-item--completed .ui-steps__marker {
725
+ color: var(--ui-steps-accent);
726
+ border-color: var(--ui-steps-accent);
727
+ }
728
+
729
+ .ui-steps__list-item--upcoming .ui-steps__step:disabled {
730
+ opacity: 0.48;
731
+ }
732
+
733
+ .ui-steps__list-item--locked .ui-steps__step {
734
+ opacity: 0.48;
735
+ }
736
+
737
+ .ui-steps__connector {
738
+ width: clamp(1.5rem, 5vw, 5rem);
739
+ height: 2px;
740
+ flex: 0 1 auto;
741
+ margin: 1.15rem 0.35rem 0;
742
+ background: var(--ui-steps-border);
743
+ transition: background-color 160ms ease;
744
+ }
745
+
746
+ .ui-steps__connector--completed {
747
+ background: var(--ui-steps-accent);
748
+ }
749
+
750
+ .ui-steps__list--cards .ui-steps__list-item {
751
+ min-width: 0;
752
+ }
753
+
754
+ .ui-steps__list--cards .ui-steps__step {
755
+ width: 100%;
756
+ height: 100%;
757
+ align-items: flex-start;
758
+ gap: 0.85rem;
759
+ padding: 1rem;
760
+ background: var(--ui-steps-surface);
761
+ border: 1px solid var(--ui-steps-border);
762
+ border-radius: 0.8rem;
763
+ transition:
764
+ border-color 160ms ease,
765
+ box-shadow 160ms ease,
766
+ transform 160ms ease;
767
+ }
768
+
769
+ .ui-steps__list--cards .ui-steps__step:not(:disabled):hover {
770
+ transform: translateY(-1px);
771
+ border-color: color-mix(
772
+ in srgb,
773
+ var(--ui-steps-accent) 55%,
774
+ var(--ui-steps-border)
775
+ );
776
+ }
777
+
778
+ .ui-steps__list--cards .ui-steps__marker {
779
+ width: 2.5rem;
780
+ height: 2.5rem;
781
+ flex-basis: 2.5rem;
782
+ }
783
+
784
+ .ui-steps__list--cards .ui-steps__list-item--active .ui-steps__step {
785
+ border-color: var(--ui-steps-accent);
786
+ box-shadow: 0 0 0 2px
787
+ color-mix(in srgb, var(--ui-steps-accent) 18%, transparent);
788
+ }
789
+
790
+ .ui-steps__list--cards .ui-steps__copy {
791
+ padding-top: 0.1rem;
792
+ }
793
+
794
+ .ui-steps__list--cards .ui-steps__title {
795
+ font-size: 0.95rem;
796
+ }
797
+
798
+ .ui-steps__panel {
799
+ min-width: 0;
800
+ }
801
+
802
+ @media (max-width: 640px) {
803
+ .ui-steps--stack-mobile .ui-steps__navigation,
804
+ .ui-steps--stack-mobile .ui-steps__progress {
805
+ width: 100%;
806
+ }
807
+
808
+ .ui-steps--stack-mobile .ui-steps__list--steps {
809
+ width: 100%;
810
+ flex-direction: column;
811
+ padding: 0.3rem;
812
+ overflow: visible;
813
+ }
814
+
815
+ .ui-steps--stack-mobile .ui-steps__list--steps .ui-steps__list-item {
816
+ width: 100%;
817
+ max-width: none;
818
+ }
819
+
820
+ .ui-steps--stack-mobile .ui-steps__list--steps .ui-steps__step {
821
+ width: 100%;
822
+ flex-direction: row;
823
+ align-items: center;
824
+ text-align: left;
825
+ }
826
+
827
+ .ui-steps--stack-mobile .ui-steps__list--steps .ui-steps__connector {
828
+ width: 2px;
829
+ height: 1.25rem;
830
+ margin: 0.25rem 0 0.25rem 1.12rem;
831
+ }
832
+
833
+ .ui-steps--stack-mobile .ui-steps__list--cards,
834
+ .ui-steps--stack-mobile .ui-steps__list--cards .ui-steps__list-item {
835
+ width: 100%;
836
+ }
837
+
838
+ .ui-steps--stack-mobile .ui-steps__list--cards .ui-steps__list-item {
839
+ flex-basis: auto;
840
+ }
841
+ }
842
+
843
+ @media (prefers-reduced-motion: reduce) {
844
+ .ui-steps__progress-fill,
845
+ .ui-steps__marker,
846
+ .ui-steps__connector,
847
+ .ui-steps__step {
848
+ transition: none;
849
+ }
850
+ }
851
+
852
+ @media (forced-colors: active) {
853
+ .ui-steps__list-item--active .ui-steps__marker,
854
+ .ui-steps__list--cards .ui-steps__list-item--active .ui-steps__step {
855
+ border-color: Highlight;
856
+ }
857
+ }
858
+ </style>