@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,91 @@
1
+ <script setup lang="ts">
2
+ import type { CSSProperties } from "vue";
3
+
4
+ type SkeletonRounded = "none" | "sm" | "md" | "lg" | "full";
5
+
6
+ interface Props {
7
+ as?: string | Component;
8
+ animated?: boolean;
9
+ rounded?: SkeletonRounded;
10
+ width?: string | number;
11
+ height?: string | number;
12
+ ariaLabel?: string;
13
+ }
14
+
15
+ const props = withDefaults(defineProps<Props>(), {
16
+ as: "div",
17
+ animated: true,
18
+ rounded: "md",
19
+ });
20
+
21
+ const roundedClasses: Record<SkeletonRounded, string> = {
22
+ none: "rounded-none",
23
+ sm: "rounded-sm",
24
+ md: "rounded-md",
25
+ lg: "rounded-lg",
26
+ full: "rounded-full",
27
+ };
28
+
29
+ const toCssSize = (value: string | number | undefined) =>
30
+ typeof value === "number" ? `${value}px` : value;
31
+
32
+ const skeletonStyles = computed<CSSProperties>(() => ({
33
+ width: toCssSize(props.width),
34
+ height: toCssSize(props.height),
35
+ }));
36
+ </script>
37
+
38
+ <template>
39
+ <component
40
+ :is="as"
41
+ :class="[
42
+ 'ui-skeleton',
43
+ roundedClasses[rounded],
44
+ { 'ui-skeleton--animated': animated },
45
+ ]"
46
+ :style="skeletonStyles"
47
+ :role="ariaLabel ? 'status' : undefined"
48
+ :aria-label="ariaLabel"
49
+ :aria-hidden="ariaLabel ? undefined : 'true'"
50
+ data-nosnippet
51
+ />
52
+ </template>
53
+
54
+ <style scoped>
55
+ .ui-skeleton {
56
+ display: block;
57
+ flex-shrink: 0;
58
+ background-color: var(--ui-border);
59
+ background-color: color-mix(in srgb, var(--ui-text) 11%, var(--ui-bg));
60
+ }
61
+
62
+ .ui-skeleton--animated {
63
+ will-change: opacity;
64
+ animation: ui-skeleton-pulse 1.6s ease-in-out infinite;
65
+ }
66
+
67
+ @keyframes ui-skeleton-pulse {
68
+ 0%,
69
+ 100% {
70
+ opacity: 1;
71
+ }
72
+
73
+ 50% {
74
+ opacity: 0.48;
75
+ }
76
+ }
77
+
78
+ @media (prefers-reduced-motion: reduce) {
79
+ .ui-skeleton--animated {
80
+ animation: none;
81
+ will-change: auto;
82
+ }
83
+ }
84
+
85
+ @media (forced-colors: active) {
86
+ .ui-skeleton {
87
+ background-color: CanvasText;
88
+ opacity: 0.2;
89
+ }
90
+ }
91
+ </style>
@@ -0,0 +1,74 @@
1
+ <script setup lang="ts">
2
+ const props = defineProps({
3
+ src: { type: String, default: "" },
4
+ gradient: { type: String, default: "" },
5
+ fixed: { type: Boolean, default: false },
6
+ auto: { type: Boolean, default: true },
7
+ height: { type: String, default: "100vh" },
8
+ });
9
+
10
+ const wrapperStyles = computed(() => {
11
+ const styles: Record<string, string> = {
12
+ height: props.height,
13
+ };
14
+
15
+ if (props.src) {
16
+ styles.backgroundImage = `url('${props.src}')`;
17
+ } else if (props.gradient) {
18
+ styles.background = props.gradient;
19
+ }
20
+
21
+ if (props.fixed) {
22
+ styles.backgroundAttachment = "fixed";
23
+ }
24
+
25
+ return styles;
26
+ });
27
+ </script>
28
+
29
+ <template>
30
+ <section
31
+ class="smart-contrast-wrapper"
32
+ :class="{ 'auto-mode': auto }"
33
+ :style="wrapperStyles"
34
+ >
35
+ <slot />
36
+ </section>
37
+ </template>
38
+
39
+ <style scoped>
40
+ .smart-contrast-wrapper {
41
+ width: 100%;
42
+ display: flex;
43
+ flex-direction: column;
44
+ justify-content: center;
45
+ align-items: center;
46
+ text-align: center;
47
+ position: relative;
48
+ background-color: #888;
49
+ background-size: cover;
50
+ background-position: center;
51
+ background-repeat: no-repeat;
52
+ isolation: isolate;
53
+ z-index: 0;
54
+ }
55
+
56
+ .smart-contrast-wrapper.auto-mode :deep(h1),
57
+ .smart-contrast-wrapper.auto-mode :deep(h2),
58
+ .smart-contrast-wrapper.auto-mode :deep(h3),
59
+ .smart-contrast-wrapper.auto-mode :deep(p),
60
+ .smart-contrast-wrapper.auto-mode :deep(span),
61
+ .smart-contrast-wrapper.auto-mode :deep(a) {
62
+ color: #fff !important;
63
+ mix-blend-mode: difference;
64
+ }
65
+
66
+ .smart-contrast-wrapper :deep(.invert-text) {
67
+ color: #fff !important;
68
+ mix-blend-mode: difference;
69
+ }
70
+
71
+ .smart-contrast-wrapper.auto-mode :deep(a) {
72
+ text-decoration: underline;
73
+ }
74
+ </style>
@@ -0,0 +1,222 @@
1
+ <script setup lang="ts">
2
+ const props = defineProps<{ direction?: "vertical" | "horizontal" }>();
3
+ const containerRef = ref<HTMLElement | null>(null);
4
+ const wrapperRef = ref<HTMLElement | null>(null);
5
+ const currentIndex = ref(0);
6
+ const isAnimating = ref(false);
7
+ const sectionsCount = ref(0);
8
+ const isVertical = computed(
9
+ () => (props.direction || "vertical") === "vertical",
10
+ );
11
+
12
+ provide("snapActiveIndex", currentIndex);
13
+ provide("snapIsVertical", isVertical);
14
+
15
+ let isIntersecting = false;
16
+ let intersectionRatio = 0;
17
+ let touchStartY = 0;
18
+ let touchStartX = 0;
19
+
20
+ const scrollTo = (index: number, scrollPage: boolean = true) => {
21
+ if (isAnimating.value || index < 0 || index >= sectionsCount.value) return;
22
+
23
+ isAnimating.value = true;
24
+ currentIndex.value = index;
25
+
26
+ if (scrollPage && wrapperRef.value) {
27
+ const rect = wrapperRef.value.getBoundingClientRect();
28
+ const behavior = Math.abs(rect.top) > 50 ? "smooth" : "auto";
29
+ window.scrollTo({ top: window.scrollY + rect.top, behavior });
30
+ }
31
+
32
+ setTimeout(() => {
33
+ isAnimating.value = false;
34
+ }, 1200);
35
+ };
36
+
37
+ const processMove = (delta: number, preventDefault?: () => void) => {
38
+ if (!wrapperRef.value || !isIntersecting) return;
39
+ const rect = wrapperRef.value.getBoundingClientRect();
40
+ const isAligned = Math.abs(rect.top) < 10;
41
+
42
+ if (!isAligned) {
43
+ if (intersectionRatio > 0.5) {
44
+ if (rect.top > 0 && delta > 0) {
45
+ preventDefault?.();
46
+ scrollTo(0, true);
47
+ return;
48
+ }
49
+ if (rect.top < 0 && delta < 0) {
50
+ preventDefault?.();
51
+ scrollTo(sectionsCount.value - 1, true);
52
+ return;
53
+ }
54
+ }
55
+ return;
56
+ }
57
+
58
+ if (isAnimating.value) {
59
+ preventDefault?.();
60
+ return;
61
+ }
62
+
63
+ const isAtFirst = currentIndex.value === 0;
64
+ const isAtLast = currentIndex.value === sectionsCount.value - 1;
65
+
66
+ if ((isAtFirst && delta < 0) || (isAtLast && delta > 0)) return;
67
+
68
+ preventDefault?.();
69
+ if (Math.abs(delta) > 20) {
70
+ scrollTo(
71
+ delta > 0 ? currentIndex.value + 1 : currentIndex.value - 1,
72
+ true,
73
+ );
74
+ }
75
+ };
76
+
77
+ const handleWheel = (e: WheelEvent) => {
78
+ processMove(e.deltaY, () => e.preventDefault());
79
+ };
80
+
81
+ const onTouchStart = (e: TouchEvent) => {
82
+ const touch = e.touches[0];
83
+ if (!touch) return;
84
+
85
+ touchStartY = touch.clientY;
86
+ touchStartX = touch.clientX;
87
+ };
88
+
89
+ const onTouchMove = (e: TouchEvent) => {
90
+ if (!isIntersecting) return;
91
+
92
+ const touch = e.touches[0];
93
+ if (!touch) return;
94
+
95
+ const currentY = touch.clientY;
96
+ const currentX = touch.clientX;
97
+ const deltaY = touchStartY - currentY;
98
+ const deltaX = touchStartX - currentX;
99
+ const delta = isVertical.value ? deltaY : deltaX;
100
+
101
+ const rect = wrapperRef.value?.getBoundingClientRect();
102
+ const isAligned = rect && Math.abs(rect.top) < 10;
103
+
104
+ if (isAligned) {
105
+ const isAtFirst = currentIndex.value === 0;
106
+ const isAtLast = currentIndex.value === sectionsCount.value - 1;
107
+ const tryingToExitTop = isAtFirst && delta < 0;
108
+ const tryingToExitBottom = isAtLast && delta > 0;
109
+
110
+ if (!tryingToExitTop && !tryingToExitBottom) {
111
+ if (e.cancelable) e.preventDefault();
112
+ }
113
+ }
114
+ };
115
+
116
+ const onTouchEnd = (e: TouchEvent) => {
117
+ const touch = e.changedTouches[0];
118
+ if (!touch) return;
119
+
120
+ const endY = touch.clientY;
121
+ const endX = touch.clientX;
122
+ const delta = isVertical.value ? touchStartY - endY : touchStartX - endX;
123
+
124
+ if (Math.abs(delta) > 40) {
125
+ processMove(delta);
126
+ }
127
+ };
128
+
129
+ onMounted(() => {
130
+ sectionsCount.value =
131
+ containerRef.value?.querySelectorAll(".snap-section").length || 0;
132
+ window.addEventListener("wheel", handleWheel, { passive: false });
133
+
134
+ const el = wrapperRef.value;
135
+ if (el) {
136
+ el.addEventListener("touchstart", onTouchStart, { passive: true });
137
+ el.addEventListener("touchmove", onTouchMove, { passive: false });
138
+ el.addEventListener("touchend", onTouchEnd, { passive: true });
139
+ }
140
+
141
+ const observer = new IntersectionObserver(
142
+ (entries) => {
143
+ const entry = entries[0];
144
+ if (!entry) return;
145
+
146
+ isIntersecting = entry.isIntersecting;
147
+ intersectionRatio = entry.intersectionRatio;
148
+ },
149
+ { threshold: Array.from({ length: 11 }, (_, i) => i / 10) },
150
+ );
151
+ if (wrapperRef.value) observer.observe(wrapperRef.value);
152
+ });
153
+
154
+ onUnmounted(() => {
155
+ window.removeEventListener("wheel", handleWheel);
156
+ });
157
+
158
+ const resetToFirst = () => {
159
+ console.log("Сигнал получен: сбрасываю currentIndex в 0");
160
+ isAnimating.value = false;
161
+ scrollTo(0, false);
162
+ currentIndex.value = 0;
163
+ };
164
+
165
+ const { registerContainer } = useSnapScroll();
166
+ registerContainer(resetToFirst);
167
+
168
+ provide("snapActiveIndex", currentIndex);
169
+ </script>
170
+
171
+ <template>
172
+ <div
173
+ ref="wrapperRef"
174
+ class="relative w-full h-dvh overflow-hidden select-none1"
175
+ :style="{
176
+ backgroundColor: 'var(--ui-bg, #000)',
177
+ color: 'var(--ui-text, #fff)',
178
+ }"
179
+ >
180
+ <div
181
+ class="absolute z-50 flex gap-2"
182
+ :class="[
183
+ isVertical
184
+ ? 'right-6 top-1/2 -translate-y-1/2 flex-col'
185
+ : 'bottom-6 left-1/2 -translate-x-1/2 flex-row',
186
+ ]"
187
+ >
188
+ <button
189
+ v-for="(_, i) in sectionsCount"
190
+ :key="i"
191
+ @click="scrollTo(i)"
192
+ class="group flex items-center justify-center w-1 h-4 focus:outline-none"
193
+ >
194
+ <div
195
+ class="transition-all duration-500 rounded-full"
196
+ :style="{
197
+ backgroundColor:
198
+ 'var(--loader-fill-color, currentColor)',
199
+ }"
200
+ :class="
201
+ currentIndex === i
202
+ ? 'w-3 h-3 opacity-100'
203
+ : 'w-1.5 h-2 opacity-20 hover:opacity-100'
204
+ "
205
+ />
206
+ </button>
207
+ </div>
208
+
209
+ <div
210
+ ref="containerRef"
211
+ class="w-full h-full will-change-transform flex transition-transform duration-1200 ease-[cubic-bezier(0.22,1,0.36,1)]"
212
+ :class="[isVertical ? 'flex-col' : 'flex-row']"
213
+ :style="{
214
+ transform: isVertical
215
+ ? `translateY(-${currentIndex * 100}%)`
216
+ : `translateX(-${currentIndex * 100}%)`,
217
+ }"
218
+ >
219
+ <slot />
220
+ </div>
221
+ </div>
222
+ </template>
@@ -0,0 +1,294 @@
1
+ <script setup lang="ts">
2
+ const props = defineProps<{
3
+ id: string;
4
+ bgClass?: string;
5
+ bgImage?: string;
6
+ bgVideo?: string;
7
+ bgVideoMobile?: string;
8
+ poster?: string;
9
+ posterMobile?: string;
10
+ youtubeId?: string;
11
+ youtubeIdMobile?: string;
12
+ overlay?: boolean | string;
13
+ lazy?: boolean;
14
+ contentClass?: string;
15
+ }>();
16
+
17
+ const sectionRef = ref<HTMLElement | null>(null);
18
+ const activeIndex = inject<Ref<number>>("snapActiveIndex", ref(0));
19
+ const isVertical = inject<Ref<boolean>>("snapIsVertical");
20
+ const myIndex = ref(-1);
21
+ const videoRef = ref<HTMLVideoElement | null>(null);
22
+ const ytFrameRef = ref<HTMLIFrameElement | null>(null);
23
+
24
+ const windowWidth = ref(
25
+ typeof window !== "undefined" ? window.innerWidth : 1024,
26
+ );
27
+
28
+ const handleResize = () => {
29
+ windowWidth.value = window.innerWidth;
30
+ };
31
+
32
+ onMounted(() => {
33
+ window.addEventListener("resize", handleResize);
34
+ if (sectionRef.value?.parentElement) {
35
+ myIndex.value = Array.from(
36
+ sectionRef.value.parentElement.children,
37
+ ).indexOf(sectionRef.value);
38
+ }
39
+ });
40
+
41
+ onUnmounted(() => {
42
+ window.removeEventListener("resize", handleResize);
43
+ });
44
+
45
+ const isMobile = computed(() => windowWidth.value < 768);
46
+
47
+ const currentVideo = computed(() =>
48
+ isMobile.value && props.bgVideoMobile ? props.bgVideoMobile : props.bgVideo,
49
+ );
50
+ const currentPoster = computed(() =>
51
+ isMobile.value && props.posterMobile ? props.posterMobile : props.poster,
52
+ );
53
+
54
+ const currentYoutubeUrl = computed(() => {
55
+ const target =
56
+ isMobile.value && props.youtubeIdMobile
57
+ ? props.youtubeIdMobile
58
+ : props.youtubeId;
59
+ return getYoutubeEmbed(target);
60
+ });
61
+
62
+ const currentYoutubePoster = computed(() => {
63
+ const target =
64
+ isMobile.value && props.youtubeIdMobile
65
+ ? props.youtubeIdMobile
66
+ : props.youtubeId;
67
+ return getYoutubePoster(target);
68
+ });
69
+
70
+ const parallaxStyle = computed(() => {
71
+ if (activeIndex === undefined || myIndex.value === -1) return {};
72
+ const diff = activeIndex.value - myIndex.value;
73
+ const offset = diff * 20;
74
+ return {
75
+ transform: isVertical?.value
76
+ ? `translate3d(0, ${offset}%, 0)`
77
+ : `translate3d(${offset}%, 0, 0)`,
78
+ transition: "transform 1200ms cubic-bezier(0.22, 1, 0.36, 1)",
79
+ };
80
+ });
81
+
82
+ const overlayStyle = computed(() => {
83
+ if (!props.overlay) return null;
84
+ return {
85
+ background:
86
+ typeof props.overlay === "string"
87
+ ? props.overlay
88
+ : "rgba(0, 0, 0, 0.4)",
89
+ };
90
+ });
91
+
92
+ watch(activeIndex, (newIdx) => {
93
+ if (!videoRef.value) return;
94
+ if (newIdx === myIndex.value) {
95
+ videoRef.value.play().catch(() => {});
96
+ } else {
97
+ videoRef.value.pause();
98
+ }
99
+ });
100
+
101
+ const toggleVideo = () => {
102
+ if (!videoRef.value) return;
103
+
104
+ if (
105
+ activeIndex?.value === myIndex.value &&
106
+ document.visibilityState === "visible"
107
+ ) {
108
+ videoRef.value.play().catch(() => {});
109
+ } else {
110
+ videoRef.value.pause();
111
+ }
112
+ };
113
+
114
+ watch(activeIndex, () => {
115
+ toggleVideo();
116
+ });
117
+
118
+ const sendCommand = (command: "playVideo" | "pauseVideo") => {
119
+ if (!ytFrameRef.value || !ytFrameRef.value.contentWindow) return;
120
+
121
+ ytFrameRef.value.contentWindow.postMessage(
122
+ JSON.stringify({
123
+ event: "command",
124
+ func: command,
125
+ args: [],
126
+ }),
127
+ "*",
128
+ );
129
+ };
130
+
131
+ const toggleYoutube = () => {
132
+ if (!currentYoutubeUrl.value) return;
133
+
134
+ if (
135
+ activeIndex?.value === myIndex.value &&
136
+ document.visibilityState === "visible"
137
+ ) {
138
+ sendCommand("playVideo");
139
+ } else {
140
+ sendCommand("pauseVideo");
141
+ }
142
+ };
143
+
144
+ watch(activeIndex, () => {
145
+ toggleYoutube();
146
+ });
147
+
148
+ const handleVisibilityChange = () => {
149
+ toggleVideo();
150
+ toggleYoutube();
151
+ };
152
+
153
+ onMounted(() => {
154
+ document.addEventListener("visibilitychange", handleVisibilityChange);
155
+ toggleVideo();
156
+ setTimeout(toggleYoutube, 1000);
157
+ });
158
+
159
+ onUnmounted(() => {
160
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
161
+ });
162
+ </script>
163
+
164
+ <template>
165
+ <section
166
+ ref="sectionRef"
167
+ :id="id"
168
+ class="snap-section w-full h-full shrink-0 overflow-hidden flex relative"
169
+ :class="bgClass"
170
+ :style="
171
+ !bgClass && !bgImage && !bgVideo && !youtubeId
172
+ ? { backgroundColor: 'var(--ui-bg, #000)' }
173
+ : {}
174
+ "
175
+ >
176
+ <div class="absolute inset-0 z-0 pointer-events-none select-none">
177
+ <div
178
+ v-if="currentYoutubeUrl"
179
+ class="absolute inset-0 overflow-hidden"
180
+ >
181
+ <iframe
182
+ ref="ytFrameRef"
183
+ :src="currentYoutubeUrl"
184
+ class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 yt-bg-frame"
185
+ frameborder="0"
186
+ allow="autoplay; encrypted-media; playsinline"
187
+ :loading="lazy ? 'lazy' : 'eager'"
188
+ ></iframe>
189
+ </div>
190
+
191
+ <video
192
+ ref="videoRef"
193
+ v-else-if="currentVideo"
194
+ autoplay
195
+ muted
196
+ loop
197
+ loading="lazy"
198
+ playsinline
199
+ :poster="currentPoster"
200
+ class="absolute inset-0 w-full h-full object-cover"
201
+ >
202
+ <source :src="currentVideo" />
203
+ </video>
204
+
205
+ <div
206
+ v-else-if="bgImage || currentYoutubePoster"
207
+ class="absolute inset-0 bg-cover bg-center bg-no-repeat"
208
+ :style="{
209
+ backgroundImage: bgImage
210
+ ? `url(${bgImage})`
211
+ : currentYoutubePoster,
212
+ }"
213
+ >
214
+ <SafeNuxtImg
215
+ v-if="lazy && bgImage"
216
+ :src="bgImage"
217
+ class="hidden"
218
+ loading="lazy"
219
+ alt=""
220
+ />
221
+ </div>
222
+
223
+ <div
224
+ v-if="overlay"
225
+ class="absolute inset-0 z-1"
226
+ :style="overlayStyle"
227
+ ></div>
228
+ </div>
229
+
230
+ <div
231
+ class="w-full max-w-7xl mx-auto px-6 will-change-transform z-10 relative pointer-events-auto select-text"
232
+ :class="[
233
+ contentClass || 'flex flex-col items-center justify-center',
234
+ parallaxStyle,
235
+ ]"
236
+ :style="parallaxStyle"
237
+ >
238
+ <div class="w-full"><slot /></div>
239
+ </div>
240
+
241
+ <div
242
+ v-if="myIndex === 0"
243
+ class="absolute z-20 transition-opacity duration-1000"
244
+ :class="[
245
+ isVertical
246
+ ? 'bottom-8 left-1/2 -translate-x-1/2'
247
+ : 'right-8 top-1/2 -translate-y-1/2',
248
+ activeIndex === 0 ? 'opacity-40' : 'opacity-0',
249
+ ]"
250
+ >
251
+ <div
252
+ class="relative overflow-hidden bg-current/20 rounded-full"
253
+ :class="isVertical ? 'w-[1.5px] h-12' : 'h-[1.5px] w-12'"
254
+ :style="{ color: 'var(--loader-fill-color, currentColor)' }"
255
+ >
256
+ <div
257
+ class="absolute bg-current rounded-full animate-premium-scroll"
258
+ :class="
259
+ isVertical
260
+ ? 'w-full h-1/3 left-0'
261
+ : 'h-full w-1/3 top-0'
262
+ "
263
+ ></div>
264
+ </div>
265
+ </div>
266
+ </section>
267
+ </template>
268
+
269
+ <style scoped>
270
+ .yt-bg-frame {
271
+ width: 100vw;
272
+ height: 56.25vw;
273
+ min-height: 100vh;
274
+ min-width: 177.77vh;
275
+ position: absolute;
276
+ top: 50%;
277
+ left: 50%;
278
+ width: 140%;
279
+ height: 140%;
280
+ transform: translate(-50%, -50%);
281
+ aspect-ratio: 16 / 9;
282
+ min-width: 100%;
283
+ min-height: 100%;
284
+ }
285
+
286
+ @media (max-width: 768px) {
287
+ .yt-bg-frame {
288
+ height: 100%;
289
+ width: auto;
290
+ aspect-ratio: 16 / 9;
291
+ min-width: 200vw;
292
+ }
293
+ }
294
+ </style>
@@ -0,0 +1,7 @@
1
+ <script setup>
2
+ onMounted(() => {
3
+ import("vue-sonner/style.css");
4
+ });
5
+ </script>
6
+
7
+ <template></template>