@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,86 @@
1
+ <script setup lang="ts">
2
+ interface InfoItem {
3
+ icon: string;
4
+ label?: string | number;
5
+ iconClass?: string;
6
+ textClass?: string;
7
+ action?: () => void;
8
+ }
9
+
10
+ interface ActionItem {
11
+ icon: string;
12
+ label?: string;
13
+ iconClass?: string;
14
+ action?: () => void;
15
+ }
16
+
17
+ const props = withDefaults(
18
+ defineProps<{
19
+ items?: InfoItem[];
20
+ actions?: ActionItem[];
21
+ wrapperClass?: string;
22
+ iconSize?: string;
23
+ }>(),
24
+ {
25
+ items: () => [],
26
+ actions: () => [],
27
+ wrapperClass: "",
28
+ },
29
+ );
30
+
31
+ const iconFixBase = "[mask-size:100%_100%] bg-current";
32
+ </script>
33
+
34
+ <template>
35
+ <div
36
+ class="w-full flex flex-wrap items-center justify-between gap-y-2 gap-x-4 py-2"
37
+ :class="wrapperClass"
38
+ >
39
+ <div v-if="items.length" class="flex flex-wrap items-center gap-4">
40
+ <div
41
+ v-for="(item, index) in items"
42
+ :key="`info-${index}`"
43
+ class="flex items-center gap-1.5 group select-none transition-opacity"
44
+ :class="{ 'cursor-pointer hover:opacity-80': !!item.action }"
45
+ @click="item.action && item.action()"
46
+ >
47
+ <UiIcon
48
+ :name="item.icon"
49
+ class="transition-transform group-active:scale-90"
50
+ :size="`${iconSize}`"
51
+ :class="`${iconFixBase} ${item.iconClass || 'text-gray-900 dark:text-gray-100'}`"
52
+ />
53
+
54
+ <span
55
+ v-if="item.label !== undefined"
56
+ class="font-medium leading-none"
57
+ :class="
58
+ item.textClass || 'text-gray-900 dark:text-gray-100'
59
+ "
60
+ >
61
+ {{ item.label }}
62
+ </span>
63
+ </div>
64
+ </div>
65
+
66
+ <div
67
+ v-if="actions.length"
68
+ class="flex items-center gap-3 ml-auto sm:ml-0"
69
+ >
70
+ <button
71
+ v-for="(act, index) in actions"
72
+ :key="`act-${index}`"
73
+ type="button"
74
+ class="p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors focus:outline-none"
75
+ @click="act.action && act.action()"
76
+ :title="act.label"
77
+ >
78
+ <UiIcon
79
+ :name="act.icon"
80
+ :size="`${iconSize}`"
81
+ :class="`${iconFixBase} ${act.iconClass || 'text-gray-900 dark:text-gray-100'}`"
82
+ />
83
+ </button>
84
+ </div>
85
+ </div>
86
+ </template>
@@ -0,0 +1,106 @@
1
+ <template>
2
+ <section class="w-full py-12 md:py-24 overflow-hidden">
3
+ <div v-if="title" class="text-center mb-16 md:mb-24 px-4">
4
+ <p
5
+ class="text-xs font-bold uppercase tracking-[0.25em] text-(--ui-text) opacity-40"
6
+ >
7
+ {{ title }}
8
+ </p>
9
+ </div>
10
+
11
+ <div v-if="items && items.length > 0">
12
+ <div
13
+ class="flex flex-wrap justify-center items-center -mx-2 md:-mx-4 lg:-mx-6"
14
+ >
15
+ <component
16
+ :is="logo.link ? 'NuxtLink' : 'div'"
17
+ v-for="(logo, index) in items"
18
+ :key="index"
19
+ :to="logo.link"
20
+ :target="
21
+ logo.link && logo.link.startsWith('http')
22
+ ? '_blank'
23
+ : undefined
24
+ "
25
+ class="relative flex items-center justify-center select-none group transition-transform duration-300"
26
+ :class="[
27
+ index >= safeMobileLimit ? 'hidden md:flex' : 'flex',
28
+ mobileLimit === 3 ? 'w-1/3 px-2' : 'w-1/2 px-2',
29
+ 'sm:w-1/3 sm:px-4',
30
+ 'md:w-1/4 md:px-4',
31
+ 'lg:w-1/6 lg:px-6',
32
+ 'xl:w-[14.28%] xl:px-6',
33
+ '2xl:w-[12.5%] 2xl:px-6',
34
+ 'py-8 md:py-12',
35
+
36
+ logo.link ? 'cursor-pointer' : 'cursor-default',
37
+ ]"
38
+ >
39
+ <img
40
+ :src="logo.src"
41
+ :alt="logo.alt || 'Logo'"
42
+ class="h-7 md:h-12 w-full object-contain transition-all duration-700 ease-in-out logoimg"
43
+ :class="[
44
+ getVariantClasses,
45
+ { 'dark:invert': logo.dark },
46
+ ]"
47
+ loading="lazy"
48
+ />
49
+ </component>
50
+
51
+ <div
52
+ v-if="showMoreCount && items.length > safeMobileLimit"
53
+ class="md:hidden flex w-full items-center justify-center mt-8"
54
+ >
55
+ <div
56
+ class="h-8 flex items-center justify-center text-[10px] font-bold text-(--ui-text) opacity-40 border border-(--ui-text)/20 rounded-full px-4 whitespace-nowrap"
57
+ >
58
+ +{{ items.length - safeMobileLimit }}
59
+ </div>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ </section>
64
+ </template>
65
+
66
+ <script setup lang="ts">
67
+ import { computed } from "vue";
68
+
69
+ interface LogoItem {
70
+ src: string;
71
+ alt?: string;
72
+ link?: string;
73
+ dark?: boolean;
74
+ }
75
+
76
+ interface Props {
77
+ items?: LogoItem[];
78
+ title?: string;
79
+ variant?: "gray-hover" | "color" | "gray";
80
+ mobileLimit?: number;
81
+ showMoreCount?: boolean;
82
+ }
83
+
84
+ const props = withDefaults(defineProps<Props>(), {
85
+ items: () => [],
86
+ title: "",
87
+ variant: "gray-hover",
88
+ mobileLimit: 6,
89
+ showMoreCount: false,
90
+ });
91
+
92
+ const safeMobileLimit = computed(() => Math.max(0, props.mobileLimit));
93
+
94
+ const getVariantClasses = computed(() => {
95
+ switch (props.variant) {
96
+ case "gray-hover":
97
+ return "grayscale opacity-40 group-hover:grayscale-0 group-hover:opacity-100";
98
+ case "color":
99
+ return "grayscale-0 opacity-60 group-hover:opacity-100";
100
+ case "gray":
101
+ return "grayscale opacity-40 group-hover:opacity-80";
102
+ default:
103
+ return "";
104
+ }
105
+ });
106
+ </script>
@@ -0,0 +1,83 @@
1
+ <script setup lang="ts">
2
+ interface Logo {
3
+ src: string;
4
+ alt?: string;
5
+ href?: string;
6
+ }
7
+
8
+ interface Props {
9
+ logos: Logo[];
10
+ speed?: number;
11
+ grayscale?: boolean;
12
+ containerClass?: string;
13
+ }
14
+
15
+ const props = withDefaults(defineProps<Props>(), {
16
+ speed: 30,
17
+ grayscale: true,
18
+ containerClass: "",
19
+ });
20
+ </script>
21
+
22
+ <template>
23
+ <div
24
+ :class="[
25
+ 'relative w-full overflow-hidden bg-(--ui-bg) py-2 sm:py-4',
26
+ containerClass,
27
+ ]"
28
+ >
29
+ <div
30
+ class="pointer-events-none absolute inset-y-0 left-0 z-10 w-12 sm:w-20 bg-linear-to-r from-(--ui-bg) to-transparent"
31
+ ></div>
32
+ <div
33
+ class="pointer-events-none absolute inset-y-0 right-0 z-10 w-12 sm:w-20 bg-linear-to-l from-(--ui-bg) to-transparent"
34
+ ></div>
35
+
36
+ <div class="flex overflow-hidden select-none group">
37
+ <div
38
+ v-for="loop in 2"
39
+ :key="loop"
40
+ class="flex min-w-full shrink-0 items-center justify-around gap-6 sm:gap-10 animate-infinite-scroll group-hover:[animation-play-state:paused]"
41
+ :style="{ 'animation-duration': `${speed}s` }"
42
+ :aria-hidden="loop === 2"
43
+ >
44
+ <component
45
+ :is="logo.href ? 'a' : 'div'"
46
+ v-for="(logo, index) in logos"
47
+ :key="`${loop}-${index}`"
48
+ :href="logo.href"
49
+ :target="logo.href ? '_blank' : undefined"
50
+ :rel="logo.href ? 'noopener noreferrer' : undefined"
51
+ class="flex h-10 w-24 sm:h-14 sm:w-32 items-center justify-center p-1 transition-transform"
52
+ :class="{ 'cursor-pointer': logo.href }"
53
+ >
54
+ <SafeNuxtImg
55
+ :src="logo.src"
56
+ :alt="logo.alt ? logo.alt : ''"
57
+ class="max-h-full max-w-full object-contain transition-all duration-300"
58
+ :class="[
59
+ grayscale
60
+ ? 'filter grayscale hover:grayscale-0 opacity-70 hover:opacity-100'
61
+ : 'opacity-90 hover:opacity-100',
62
+ '',
63
+ ]"
64
+ />
65
+ </component>
66
+ </div>
67
+ </div>
68
+ </div>
69
+ </template>
70
+
71
+ <style scoped>
72
+ @keyframes scroll {
73
+ from {
74
+ transform: translateX(0);
75
+ }
76
+ to {
77
+ transform: translateX(-100%);
78
+ }
79
+ }
80
+ .animate-infinite-scroll {
81
+ animation: scroll linear infinite;
82
+ }
83
+ </style>
@@ -0,0 +1,81 @@
1
+ <template>
2
+ <div class="min-h-screen flex items-center justify-center px-4">
3
+ <div class="max-w-xl text-center">
4
+ <h1
5
+ class="text-7xl sm:text-9xl font-extrabold tracking-widest text-red-500 dark:text-red-400"
6
+ >
7
+ 404
8
+ </h1>
9
+ <p class="text-2xl md:text-3xl mt-6 text-center">
10
+ {{ t("interface.pageNotFound") }}
11
+ </p>
12
+
13
+ <p
14
+ class="mt-4 text-gray-600 dark:text-gray-400 text-center max-w-md"
15
+ >
16
+ {{ t("interface.pageNotFoundText") }}
17
+ </p>
18
+
19
+ <NuxtLink
20
+ to="/"
21
+ class="mt-8 inline-block bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 pb-3 px-6 rounded-lg transition"
22
+ >
23
+ {{ t("interface.goHomeFull") }}
24
+ </NuxtLink>
25
+ </div>
26
+ </div>
27
+ </template>
28
+
29
+ <script setup lang="ts">
30
+ const props = withDefaults(
31
+ defineProps<{
32
+ setResponseStatus?: boolean;
33
+ }>(),
34
+ {
35
+ setResponseStatus: true,
36
+ },
37
+ );
38
+
39
+ if (import.meta.server && props.setResponseStatus) {
40
+ const event = useRequestEvent();
41
+ if (event) {
42
+ setResponseStatus(event, 404, "Not Found");
43
+ }
44
+ }
45
+
46
+ const isNotFound = useState<boolean>("is_404", () => false);
47
+ const activateNotFound = () => {
48
+ isNotFound.value = true;
49
+ };
50
+ const deactivateNotFound = () => {
51
+ isNotFound.value = false;
52
+ };
53
+
54
+ activateNotFound();
55
+ onActivated(activateNotFound);
56
+ onDeactivated(deactivateNotFound);
57
+ onUnmounted(deactivateNotFound);
58
+
59
+ const { t } = useMultiLang(["interface"]);
60
+
61
+ useAppSeo({
62
+ title: () => `404 — ${t("interface.pageNotFound")}`,
63
+ description: () => `404 — ${t("interface.pageNotFoundDescription")}`,
64
+ noIndex: true,
65
+ });
66
+ </script>
67
+
68
+ <style scoped>
69
+ h1 {
70
+ animation: bounce 3s ease infinite;
71
+ }
72
+ @keyframes bounce {
73
+ 0%,
74
+ 100% {
75
+ transform: translateY(0);
76
+ }
77
+ 50% {
78
+ transform: translateY(-15px);
79
+ }
80
+ }
81
+ </style>
@@ -0,0 +1,313 @@
1
+ <script setup lang="ts">
2
+ import type { CSSProperties } from "vue";
3
+
4
+ import type { LayoutSize } from "~/constants/layout";
5
+
6
+ interface Props {
7
+ as?: string;
8
+ containerSize?: LayoutSize;
9
+ backgroundHeightLeft?: string;
10
+ backgroundHeightRight?: string;
11
+ backgroundBeforeContent?: boolean;
12
+ backgroundBeforeContentHeight?: string;
13
+ backgroundAfterContent?: boolean;
14
+ backgroundAfterContentHeight?: string;
15
+ minHeight?: string;
16
+ contentAlign?: "top" | "center" | "bottom";
17
+ backgroundColor?: string;
18
+ backgroundColorDark?: string;
19
+ background?: string;
20
+ backgroundDark?: string;
21
+ backgroundSize?: string;
22
+ backgroundPosition?: string;
23
+ backgroundRepeat?: string;
24
+ mask?: string;
25
+ maskDark?: string;
26
+ maskSize?: string;
27
+ maskPosition?: string;
28
+ maskRepeat?: string;
29
+ layerClass?: string;
30
+ contentClass?: string;
31
+ }
32
+
33
+ const props = withDefaults(defineProps<Props>(), {
34
+ as: "section",
35
+ containerSize: "content",
36
+ backgroundHeightLeft: "60dvh",
37
+ backgroundHeightRight: "60dvh",
38
+ backgroundBeforeContent: false,
39
+ backgroundBeforeContentHeight: "clamp(5rem, 12dvh, 10rem)",
40
+ backgroundAfterContent: false,
41
+ backgroundAfterContentHeight: "clamp(5rem, 12dvh, 10rem)",
42
+ contentAlign: "top",
43
+ backgroundColor: "transparent",
44
+ background: "transparent",
45
+ backgroundSize: "cover",
46
+ backgroundPosition: "center",
47
+ backgroundRepeat: "no-repeat",
48
+ mask: "none",
49
+ maskSize: "cover",
50
+ maskPosition: "center",
51
+ maskRepeat: "no-repeat",
52
+ layerClass: "",
53
+ contentClass: "",
54
+ });
55
+
56
+ type PartialBackgroundStyles = CSSProperties &
57
+ Record<`--partial-background-${string}`, string>;
58
+
59
+ const rootStyles = computed<PartialBackgroundStyles>(() => ({
60
+ "--partial-background-left": props.backgroundHeightLeft,
61
+ "--partial-background-right": props.backgroundHeightRight,
62
+ "--partial-background-before-content-height":
63
+ props.backgroundBeforeContentHeight,
64
+ "--partial-background-after-content-height":
65
+ props.backgroundAfterContentHeight,
66
+ "--partial-background-min-height": props.minHeight || "auto",
67
+ "--partial-background-color-light": props.backgroundColor,
68
+ "--partial-background-color-dark":
69
+ props.backgroundColorDark || props.backgroundColor,
70
+ "--partial-background-light": props.background,
71
+ "--partial-background-dark": props.backgroundDark || props.background,
72
+ "--partial-background-size": props.backgroundSize,
73
+ "--partial-background-position": props.backgroundPosition,
74
+ "--partial-background-repeat": props.backgroundRepeat,
75
+ "--partial-background-mask-light": props.mask,
76
+ "--partial-background-mask-dark": props.maskDark || props.mask,
77
+ "--partial-background-mask-size": props.maskSize,
78
+ "--partial-background-mask-position": props.maskPosition,
79
+ "--partial-background-mask-repeat": props.maskRepeat,
80
+ }));
81
+
82
+ const contentAlignmentClass = computed(
83
+ () =>
84
+ ({
85
+ top: "justify-start",
86
+ center: "justify-center",
87
+ bottom: "justify-end",
88
+ })[props.contentAlign],
89
+ );
90
+
91
+ const contentClasses = computed(() =>
92
+ [
93
+ "view-partial-background__content",
94
+ contentAlignmentClass.value,
95
+ props.contentClass,
96
+ ]
97
+ .filter(Boolean)
98
+ .join(" "),
99
+ );
100
+
101
+ const rootClasses = computed(() =>
102
+ [
103
+ "view-partial-background h-[auto]",
104
+ props.backgroundBeforeContent &&
105
+ "view-partial-background--before-content",
106
+ props.backgroundAfterContent &&
107
+ "view-partial-background--after-content",
108
+ ]
109
+ .filter(Boolean)
110
+ .join(" "),
111
+ );
112
+ </script>
113
+
114
+ <template>
115
+ <GridStack :as="as" :class="rootClasses" :style="rootStyles">
116
+ <div
117
+ v-if="backgroundBeforeContent"
118
+ class="view-partial-background__layer view-partial-background__layer--before"
119
+ :class="layerClass"
120
+ aria-hidden="true"
121
+ >
122
+ <slot name="background-before">
123
+ <slot name="background" />
124
+ </slot>
125
+ </div>
126
+
127
+ <GridContainer :size="containerSize" :class="contentClasses">
128
+ <slot />
129
+ </GridContainer>
130
+
131
+ <div
132
+ v-if="backgroundAfterContent"
133
+ class="view-partial-background__layer view-partial-background__layer--after"
134
+ :class="layerClass"
135
+ aria-hidden="true"
136
+ >
137
+ <slot name="background-after">
138
+ <slot name="background" />
139
+ </slot>
140
+ </div>
141
+
142
+ <div
143
+ v-if="!backgroundBeforeContent && !backgroundAfterContent"
144
+ class="view-partial-background__layer view-partial-background__layer--behind"
145
+ :class="layerClass"
146
+ aria-hidden="true"
147
+ >
148
+ <slot name="background" />
149
+ </div>
150
+ </GridStack>
151
+ </template>
152
+
153
+ <style>
154
+ .view-partial-background {
155
+ position: relative;
156
+ isolation: isolate;
157
+ width: 100%;
158
+ min-height: var(--partial-background-min-height);
159
+ }
160
+
161
+ .view-partial-background__layer {
162
+ position: absolute;
163
+ z-index: -1;
164
+ inset: 0;
165
+ overflow: hidden;
166
+ pointer-events: none;
167
+ background: var(--partial-background-light);
168
+ background-color: var(--partial-background-color-light);
169
+ background-position: var(--partial-background-position);
170
+ background-repeat: var(--partial-background-repeat);
171
+ background-size: var(--partial-background-size);
172
+ clip-path: polygon(
173
+ 0 0,
174
+ 100% 0,
175
+ 100% var(--partial-background-right),
176
+ 0 var(--partial-background-left)
177
+ );
178
+ -webkit-mask: var(--partial-background-mask-light);
179
+ mask: var(--partial-background-mask-light);
180
+ -webkit-mask-position: var(--partial-background-mask-position);
181
+ mask-position: var(--partial-background-mask-position);
182
+ -webkit-mask-repeat: var(--partial-background-mask-repeat);
183
+ mask-repeat: var(--partial-background-mask-repeat);
184
+ -webkit-mask-size: var(--partial-background-mask-size);
185
+ mask-size: var(--partial-background-mask-size);
186
+ }
187
+
188
+ .view-partial-background__content {
189
+ position: relative;
190
+ z-index: 1;
191
+ min-height: inherit;
192
+ }
193
+
194
+ .view-partial-background--before-content::before,
195
+ .view-partial-background--after-content::before {
196
+ position: absolute;
197
+ z-index: -1;
198
+ right: 0;
199
+ left: 0;
200
+ content: "";
201
+ pointer-events: none;
202
+ background-color: var(--partial-background-color-light);
203
+ }
204
+
205
+ .view-partial-background--before-content::before {
206
+ top: var(--partial-background-before-content-height);
207
+ bottom: 0;
208
+ }
209
+
210
+ .view-partial-background--after-content::before {
211
+ top: 0;
212
+ bottom: var(--partial-background-after-content-height);
213
+ }
214
+
215
+ .view-partial-background--before-content.view-partial-background--after-content::before {
216
+ top: var(--partial-background-before-content-height);
217
+ bottom: var(--partial-background-after-content-height);
218
+ }
219
+
220
+ .view-partial-background__layer--before,
221
+ .view-partial-background__layer--after {
222
+ position: relative;
223
+ z-index: 0;
224
+ inset: auto;
225
+ width: 100%;
226
+ flex: none;
227
+ }
228
+
229
+ .view-partial-background__layer--before {
230
+ height: var(--partial-background-before-content-height);
231
+ clip-path: polygon(
232
+ 0 calc(100% - var(--partial-background-left)),
233
+ 100% calc(100% - var(--partial-background-right)),
234
+ 100% 100%,
235
+ 0 100%
236
+ );
237
+ }
238
+
239
+ .view-partial-background__layer--after {
240
+ height: var(--partial-background-after-content-height);
241
+ clip-path: polygon(
242
+ 0 0,
243
+ 100% 0,
244
+ 100% var(--partial-background-right),
245
+ 0 var(--partial-background-left)
246
+ );
247
+ }
248
+
249
+ @media (prefers-color-scheme: dark) {
250
+ .view-partial-background__layer {
251
+ background: var(--partial-background-dark);
252
+ background-color: var(--partial-background-color-dark);
253
+ background-position: var(--partial-background-position);
254
+ background-repeat: var(--partial-background-repeat);
255
+ background-size: var(--partial-background-size);
256
+ -webkit-mask: var(--partial-background-mask-dark);
257
+ mask: var(--partial-background-mask-dark);
258
+ -webkit-mask-position: var(--partial-background-mask-position);
259
+ mask-position: var(--partial-background-mask-position);
260
+ -webkit-mask-repeat: var(--partial-background-mask-repeat);
261
+ mask-repeat: var(--partial-background-mask-repeat);
262
+ -webkit-mask-size: var(--partial-background-mask-size);
263
+ mask-size: var(--partial-background-mask-size);
264
+ }
265
+
266
+ .view-partial-background--before-content::before,
267
+ .view-partial-background--after-content::before {
268
+ background-color: var(--partial-background-color-dark);
269
+ }
270
+ }
271
+
272
+ [data-theme="light"] .view-partial-background__layer {
273
+ background: var(--partial-background-light);
274
+ background-color: var(--partial-background-color-light);
275
+ background-position: var(--partial-background-position);
276
+ background-repeat: var(--partial-background-repeat);
277
+ background-size: var(--partial-background-size);
278
+ -webkit-mask: var(--partial-background-mask-light);
279
+ mask: var(--partial-background-mask-light);
280
+ -webkit-mask-position: var(--partial-background-mask-position);
281
+ mask-position: var(--partial-background-mask-position);
282
+ -webkit-mask-repeat: var(--partial-background-mask-repeat);
283
+ mask-repeat: var(--partial-background-mask-repeat);
284
+ -webkit-mask-size: var(--partial-background-mask-size);
285
+ mask-size: var(--partial-background-mask-size);
286
+ }
287
+
288
+ [data-theme="light"] .view-partial-background--before-content::before,
289
+ [data-theme="light"] .view-partial-background--after-content::before {
290
+ background-color: var(--partial-background-color-light);
291
+ }
292
+
293
+ [data-theme="dark"] .view-partial-background__layer {
294
+ background: var(--partial-background-dark);
295
+ background-color: var(--partial-background-color-dark);
296
+ background-position: var(--partial-background-position);
297
+ background-repeat: var(--partial-background-repeat);
298
+ background-size: var(--partial-background-size);
299
+ -webkit-mask: var(--partial-background-mask-dark);
300
+ mask: var(--partial-background-mask-dark);
301
+ -webkit-mask-position: var(--partial-background-mask-position);
302
+ mask-position: var(--partial-background-mask-position);
303
+ -webkit-mask-repeat: var(--partial-background-mask-repeat);
304
+ mask-repeat: var(--partial-background-mask-repeat);
305
+ -webkit-mask-size: var(--partial-background-mask-size);
306
+ mask-size: var(--partial-background-mask-size);
307
+ }
308
+
309
+ [data-theme="dark"] .view-partial-background--before-content::before,
310
+ [data-theme="dark"] .view-partial-background--after-content::before {
311
+ background-color: var(--partial-background-color-dark);
312
+ }
313
+ </style>