@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.
- package/.devcontainer/Dockerfile +7 -0
- package/.devcontainer/devcontainer.json +21 -0
- package/.editorconfig +43 -0
- package/.github/workflows/package.yml +73 -0
- package/.nuxtignore +6 -0
- package/.releaserc.js +132 -0
- package/.yarn/releases/yarn-4.18.0.cjs +1000 -0
- package/.yarnrc.yml +8 -0
- package/CHANGELOG.md +141 -0
- package/LICENSE +77 -0
- package/README.md +3004 -0
- package/app/app.vue +455 -0
- package/app/assets/css/animation.css +81 -0
- package/app/assets/css/backgrounds.css +57 -0
- package/app/assets/css/base.css +182 -0
- package/app/assets/css/index.css +9 -0
- package/app/assets/css/links.css +12 -0
- package/app/assets/css/multimedia.css +102 -0
- package/app/assets/css/preloader.css +68 -0
- package/app/assets/css/print.css +40 -0
- package/app/assets/css/tailwind.css +20 -0
- package/app/assets/css/theme.css +29 -0
- package/app/components/consent/GoogleMap.vue +59 -0
- package/app/components/consent/Youtube.vue +27 -0
- package/app/components/cookie/Banner.vue +70 -0
- package/app/components/cookie/ConsentWrapper.vue +71 -0
- package/app/components/cookie/PrivacyPolicy.vue +179 -0
- package/app/components/cookie/SettingsModal.vue +387 -0
- package/app/components/grid/Col.vue +262 -0
- package/app/components/grid/Container.vue +236 -0
- package/app/components/grid/Row.vue +248 -0
- package/app/components/grid/Stack.vue +233 -0
- package/app/components/myelophone/Copyright.vue +5 -0
- package/app/components/myelophone/Welcome.vue +107 -0
- package/app/components/page/FullscreenPreloader.vue +177 -0
- package/app/components/page/Preloader.vue +18 -0
- package/app/components/page/ReadyLocalized.vue +19 -0
- package/app/components/ready/.gitkeep +0 -0
- package/app/components/safe/Email.vue +52 -0
- package/app/components/safe/ImgWithLoader.vue +180 -0
- package/app/components/safe/NuxtImg.vue +25 -0
- package/app/components/safe/NuxtPicture.vue +29 -0
- package/app/components/section/.gitkeep +0 -0
- package/app/components/seo/BrandInfo.vue +76 -0
- package/app/components/seo/ContentNoIndex.vue +35 -0
- package/app/components/seo/NoIndex.vue +22 -0
- package/app/components/seo/OrgInfo.vue +130 -0
- package/app/components/seo/ProfileInfo.vue +137 -0
- package/app/components/ui/Accordion.vue +163 -0
- package/app/components/ui/Alert.vue +179 -0
- package/app/components/ui/Avatar.vue +88 -0
- package/app/components/ui/Badge.vue +150 -0
- package/app/components/ui/Banner.vue +133 -0
- package/app/components/ui/Button.vue +175 -0
- package/app/components/ui/Card.vue +66 -0
- package/app/components/ui/Checkbox.vue +134 -0
- package/app/components/ui/Chip.vue +136 -0
- package/app/components/ui/CommandPalette.vue +434 -0
- package/app/components/ui/Console.vue +583 -0
- package/app/components/ui/CursorCreative.vue +148 -0
- package/app/components/ui/FeatureAccordion.vue +165 -0
- package/app/components/ui/FileUpload.vue +705 -0
- package/app/components/ui/GeoDependent.vue +72 -0
- package/app/components/ui/Heading.vue +51 -0
- package/app/components/ui/Icon.vue +42 -0
- package/app/components/ui/Icon8.vue +39 -0
- package/app/components/ui/Input.vue +88 -0
- package/app/components/ui/LangLink.vue +34 -0
- package/app/components/ui/LangVisible.vue +33 -0
- package/app/components/ui/LanguageSelect.vue +202 -0
- package/app/components/ui/LightBox.vue +155 -0
- package/app/components/ui/LocalTime.vue +170 -0
- package/app/components/ui/Modal.vue +161 -0
- package/app/components/ui/Popup.vue +200 -0
- package/app/components/ui/PostContent.server.vue +23 -0
- package/app/components/ui/ProtectedEmail.vue +52 -0
- package/app/components/ui/RelativeTime.vue +141 -0
- package/app/components/ui/ScheduledContent.vue +150 -0
- package/app/components/ui/ScrollToTop.client.vue +48 -0
- package/app/components/ui/SegmentedControl.vue +148 -0
- package/app/components/ui/SiteSearch.vue +1191 -0
- package/app/components/ui/Skeleton.vue +91 -0
- package/app/components/ui/SmartContrast.vue +74 -0
- package/app/components/ui/SnapContainer.vue +222 -0
- package/app/components/ui/SnapSection.vue +294 -0
- package/app/components/ui/SonnerStyle.vue +7 -0
- package/app/components/ui/SplitSection.vue +99 -0
- package/app/components/ui/Steps.vue +858 -0
- package/app/components/ui/StickyWrapper.vue +46 -0
- package/app/components/ui/Table.vue +251 -0
- package/app/components/ui/Tabs.vue +451 -0
- package/app/components/ui/TextColumn.vue +36 -0
- package/app/components/ui/Textarea.vue +251 -0
- package/app/components/ui/Toggler.vue +40 -0
- package/app/components/ui/Tooltip.vue +524 -0
- package/app/components/ui/TruncateText.vue +105 -0
- package/app/components/ui/ViewportSpacer.vue +72 -0
- package/app/components/view/AnimatedCounter.vue +121 -0
- package/app/components/view/AnnouncementBar.vue +104 -0
- package/app/components/view/AvatarGroup.vue +104 -0
- package/app/components/view/Breadcrumbs.vue +97 -0
- package/app/components/view/ContentWithToc.vue +152 -0
- package/app/components/view/Copyright.vue +68 -0
- package/app/components/view/CountdownTimer.vue +184 -0
- package/app/components/view/CreativeCTA.vue +143 -0
- package/app/components/view/CurrencySelect.vue +254 -0
- package/app/components/view/FeatureBoxGrid.vue +142 -0
- package/app/components/view/HorizontalMenu.vue +97 -0
- package/app/components/view/ImageCompare.vue +141 -0
- package/app/components/view/ImgSlider.vue +333 -0
- package/app/components/view/InfiniteMarquee.vue +147 -0
- package/app/components/view/InfoBar.vue +86 -0
- package/app/components/view/LogoGrid.vue +106 -0
- package/app/components/view/LogoSlider.vue +83 -0
- package/app/components/view/NotFound.vue +81 -0
- package/app/components/view/PartialBackground.vue +313 -0
- package/app/components/view/QuoteBig.vue +111 -0
- package/app/components/view/ResponsiveMenu.vue +219 -0
- package/app/components/view/ReviewsSlider.vue +230 -0
- package/app/components/view/SocialBar.vue +78 -0
- package/app/components/view/StickyHeadingContent.vue +158 -0
- package/app/components/view/TallyForm.vue +295 -0
- package/app/components/view/WarningOutdated.vue +113 -0
- package/app/composables/useApi.ts +88 -0
- package/app/composables/useAppSeo.ts +78 -0
- package/app/composables/useBreadcrumbs.ts +106 -0
- package/app/composables/useChangeUrl.ts +73 -0
- package/app/composables/useCommand.ts +28 -0
- package/app/composables/useCookieControl.ts +111 -0
- package/app/composables/useCookieModal.ts +9 -0
- package/app/composables/useDevice.ts +109 -0
- package/app/composables/useGeoFetch.ts +139 -0
- package/app/composables/useLoadScript.ts +36 -0
- package/app/composables/useModalState.ts +37 -0
- package/app/composables/useMultiLang.ts +278 -0
- package/app/composables/useNetwork.ts +49 -0
- package/app/composables/useNuxtVersion.ts +5 -0
- package/app/composables/usePiniaStore.ts +5 -0
- package/app/composables/usePopupControl.ts +57 -0
- package/app/composables/useQuery.ts +307 -0
- package/app/composables/useReadyPage.ts +182 -0
- package/app/composables/useSafeList.ts +90 -0
- package/app/composables/useScriptLoader.ts +221 -0
- package/app/composables/useSiteSearch.ts +32 -0
- package/app/composables/useSnapScroll.ts +24 -0
- package/app/composables/useStatic.ts +5 -0
- package/app/composables/useStorage.ts +252 -0
- package/app/composables/useStoreBroadcast.ts +146 -0
- package/app/composables/useThemeSync.ts +81 -0
- package/app/constants/layout.ts +109 -0
- package/app/constants/predefinedCookieScripts.ts +1222 -0
- package/app/constants/ui-events.ts +1 -0
- package/app/error.vue +169 -0
- package/app/layouts/default-blog.vue +5 -0
- package/app/layouts/default.vue +3 -0
- package/app/locales/en/commands.json +5 -0
- package/app/locales/en/cookies.json +182 -0
- package/app/locales/en/error.json +4 -0
- package/app/locales/en/interface.json +33 -0
- package/app/locales/en/myelophone.json +8 -0
- package/app/locales/en/outdated.json +5 -0
- package/app/locales/en/times.json +18 -0
- package/app/locales/pl/commands.json +5 -0
- package/app/locales/pl/cookies.json +182 -0
- package/app/locales/pl/error.json +4 -0
- package/app/locales/pl/interface.json +33 -0
- package/app/locales/pl/myelophone.json +8 -0
- package/app/locales/pl/outdated.json +5 -0
- package/app/locales/pl/times.json +18 -0
- package/app/locales/ru/commands.json +5 -0
- package/app/locales/ru/cookies.json +182 -0
- package/app/locales/ru/error.json +4 -0
- package/app/locales/ru/interface.json +33 -0
- package/app/locales/ru/myelophone.json +8 -0
- package/app/locales/ru/outdated.json +5 -0
- package/app/locales/ru/times.json +18 -0
- package/app/middleware/blog-layout.ts +28 -0
- package/app/middleware/i18n.global.ts +17 -0
- package/app/middleware/not-found-status.ts +11 -0
- package/app/modules/default-blog/index.ts +39 -0
- package/app/modules/default-blog/posts.ts +80 -0
- package/app/modules/default-sitemap/index.ts +116 -0
- package/app/modules/default-sitemap/runtime/sitemap.get.ts +42 -0
- package/app/modules/fullscreen-preloader/module.ts +43 -0
- package/app/modules/htaccess/.htaccess +38 -0
- package/app/modules/htaccess/index.ts +23 -0
- package/app/modules/i18n-shaker.mjs +221 -0
- package/app/modules/multi18n/module.ts +71 -0
- package/app/modules/myelophone-config/module.ts +241 -0
- package/app/pages/[...all].vue +9 -0
- package/app/pages/index.vue +3 -0
- package/app/pages/post/[...slug].vue +30 -0
- package/app/plugins/bfcache-refresh.client.ts +9 -0
- package/app/plugins/canonical.ts +30 -0
- package/app/plugins/csp-nonce.server.ts +41 -0
- package/app/plugins/multi18n.client.ts +21 -0
- package/app/plugins/multi18n.server.ts +15 -0
- package/app/plugins/nuxt-version.client.ts +4 -0
- package/app/plugins/restore.client.ts +21 -0
- package/app/plugins/reveal.ts +89 -0
- package/app/plugins/url-helper.client.ts +66 -0
- package/app/server/api/exchange-rates.get.ts +216 -0
- package/app/server/api/healthz.ts +1 -0
- package/app/server/api/logout.post.ts +6 -0
- package/app/server/middleware/normalize-url.ts +14 -0
- package/app/server/middleware/security-headers.ts +147 -0
- package/app/server/middleware/url-helper.ts +43 -0
- package/app/server/plugins/clean-html-spaces.ts +15 -0
- package/app/server/plugins/clean-styles.ts +94 -0
- package/app/server/plugins/compression.ts +122 -0
- package/app/server/plugins/css-preload.ts +26 -0
- package/app/server/plugins/early-hints.ts +31 -0
- package/app/server/plugins/force-noindex.ts +43 -0
- package/app/server/plugins/html-one-line.ts +17 -0
- package/app/server/plugins/inline-style.ts +7 -0
- package/app/server/routes/healthz.get.ts +1 -0
- package/app/server/tsconfig.json +3 -0
- package/app/stores/cart.ts +945 -0
- package/app/stores/commands.ts +43 -0
- package/app/stores/device.ts +156 -0
- package/app/stores/querySync.ts +5 -0
- package/app/stores/settings.ts +145 -0
- package/app/stores/user.ts +607 -0
- package/app/types/cookie.ts +89 -0
- package/app/types/myelophone.d.ts +87 -0
- package/app/types/site-search.ts +110 -0
- package/app/utils/asyncRefresh.ts +4 -0
- package/app/utils/blog.server.ts +142 -0
- package/app/utils/jsonLd.ts +56 -0
- package/app/utils/myelophoneConfig.ts +4 -0
- package/app/utils/sortBy.ts +18 -0
- package/app/utils/transliterate.ts +75 -0
- package/app/utils/ytVideo.ts +37 -0
- package/app/workers/site-search.worker.ts +661 -0
- package/biome.json +94 -0
- package/commitlint.config.js +3 -0
- package/nuxt.config.ts +539 -0
- package/package.json +101 -0
- package/playground/content/.gitkeep +0 -0
- package/playground/locales/.gitkeep +0 -0
- package/playground/myelophone.dev.ts +45 -0
- package/playground/nuxt.config.ts +105 -0
- package/playground/package.json +5 -0
- package/playground/pages/.gitkeep +0 -0
- package/playground/tsconfig.json +3 -0
- package/playwright.config.ts +23 -0
- package/public/browsers/chrome.svg +77 -0
- package/public/browsers/edge.svg +98 -0
- package/public/browsers/firefox.svg +300 -0
- package/public/favicon.ico +0 -0
- package/public/myelophone_eng.png +0 -0
- package/public/myelophone_eng_white.png +0 -0
- package/spa-loading-template.html +72 -0
- package/tests/home.spec.ts +9 -0
- package/tests/utils.ts +23 -0
- package/tsconfig.json +16 -0
- package/vite/plugins/clean-css.ts +190 -0
- package/vite/plugins/postcss-add-viewportunits.ts +33 -0
- package/vite/plugins/postcss-viewport-fallback.ts +49 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
type Alignment = "left" | "center" | "right";
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(
|
|
5
|
+
defineProps<{
|
|
6
|
+
text: string;
|
|
7
|
+
authorName?: string;
|
|
8
|
+
authorRole?: string;
|
|
9
|
+
avatarSrc?: string;
|
|
10
|
+
avatarChipColor?: string;
|
|
11
|
+
textSizeClass?: string;
|
|
12
|
+
quoteAlign?: Alignment;
|
|
13
|
+
footerAlign?: Alignment;
|
|
14
|
+
}>(),
|
|
15
|
+
{
|
|
16
|
+
textSizeClass: "text-xl sm:text-2xl md:text-3xl leading-relaxed",
|
|
17
|
+
quoteAlign: "center",
|
|
18
|
+
footerAlign: "center",
|
|
19
|
+
},
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const quoteContainerClass = computed(() => {
|
|
23
|
+
switch (props.quoteAlign) {
|
|
24
|
+
case "left":
|
|
25
|
+
return "text-left";
|
|
26
|
+
case "right":
|
|
27
|
+
return "text-right";
|
|
28
|
+
default:
|
|
29
|
+
return "text-center";
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const footerContainerClass = computed(() => {
|
|
34
|
+
switch (props.footerAlign) {
|
|
35
|
+
case "left":
|
|
36
|
+
return "justify-start text-left";
|
|
37
|
+
case "right":
|
|
38
|
+
return "justify-start text-right flex-row-reverse";
|
|
39
|
+
default:
|
|
40
|
+
return "justify-center text-center flex-col sm:flex-row";
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const authorTextWrapperClass = computed(() => {
|
|
45
|
+
switch (props.footerAlign) {
|
|
46
|
+
case "left":
|
|
47
|
+
return "items-start";
|
|
48
|
+
case "right":
|
|
49
|
+
return "items-end";
|
|
50
|
+
default:
|
|
51
|
+
return "items-center";
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<template>
|
|
57
|
+
<figure class="flex flex-col max-w-4xl mx-auto px-4 py-8 sm:py-10">
|
|
58
|
+
<div
|
|
59
|
+
class="mb-4 sm:mb-6 self-start text-primary-500/30 dark:text-primary-400/30 select-none"
|
|
60
|
+
aria-hidden="true"
|
|
61
|
+
>
|
|
62
|
+
<UiIcon
|
|
63
|
+
name="ri:double-quotes-l"
|
|
64
|
+
class="w-16! h-16! sm:w-20! sm:h-20! mask-size-[100%_100%]"
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<blockquote
|
|
69
|
+
class="font-medium text-gray-900 dark:text-white relative z-10 w-full mb-8 sm:mb-10"
|
|
70
|
+
:class="quoteContainerClass"
|
|
71
|
+
>
|
|
72
|
+
<p :class="textSizeClass">
|
|
73
|
+
{{ text }}
|
|
74
|
+
</p>
|
|
75
|
+
</blockquote>
|
|
76
|
+
|
|
77
|
+
<figcaption
|
|
78
|
+
v-if="authorName || authorRole || avatarSrc"
|
|
79
|
+
class="flex items-center gap-4 w-full"
|
|
80
|
+
:class="footerContainerClass"
|
|
81
|
+
>
|
|
82
|
+
<div v-if="avatarSrc" class="shrink-0">
|
|
83
|
+
<AppAvatar
|
|
84
|
+
:src="avatarSrc"
|
|
85
|
+
:chip-color="avatarChipColor"
|
|
86
|
+
class="w-12 h-12 sm:w-14 sm:h-14 shadow-sm"
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div
|
|
91
|
+
v-if="authorName || authorRole"
|
|
92
|
+
class="flex flex-col"
|
|
93
|
+
:class="authorTextWrapperClass"
|
|
94
|
+
>
|
|
95
|
+
<cite
|
|
96
|
+
v-if="authorName"
|
|
97
|
+
class="not-italic font-bold text-gray-900 dark:text-white text-base sm:text-lg"
|
|
98
|
+
>
|
|
99
|
+
{{ authorName }}
|
|
100
|
+
</cite>
|
|
101
|
+
|
|
102
|
+
<span
|
|
103
|
+
v-if="authorRole"
|
|
104
|
+
class="text-sm sm:text-base text-gray-500 dark:text-gray-400 mt-0.5"
|
|
105
|
+
>
|
|
106
|
+
{{ authorRole }}
|
|
107
|
+
</span>
|
|
108
|
+
</div>
|
|
109
|
+
</figcaption>
|
|
110
|
+
</figure>
|
|
111
|
+
</template>
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
interface MenuItem {
|
|
3
|
+
label: string;
|
|
4
|
+
to: string;
|
|
5
|
+
[key: string]: any;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface MenuClasses {
|
|
9
|
+
wrapper?: string;
|
|
10
|
+
link?: string;
|
|
11
|
+
linkActive?: string;
|
|
12
|
+
moreButton?: string;
|
|
13
|
+
dropdown?: string;
|
|
14
|
+
dropdownLink?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const props = withDefaults(
|
|
18
|
+
defineProps<{
|
|
19
|
+
items: MenuItem[];
|
|
20
|
+
ui?: MenuClasses;
|
|
21
|
+
}>(),
|
|
22
|
+
{
|
|
23
|
+
ui: () => ({}),
|
|
24
|
+
},
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const route = useRoute();
|
|
28
|
+
|
|
29
|
+
const containerRef = ref<HTMLElement | null>(null);
|
|
30
|
+
const dummyRef = ref<HTMLElement | null>(null);
|
|
31
|
+
const moreContainerRef = ref<HTMLElement | null>(null);
|
|
32
|
+
const isDropdownOpen = ref(false);
|
|
33
|
+
const availableWidth = ref(0);
|
|
34
|
+
const itemWidths = ref<number[]>([]);
|
|
35
|
+
|
|
36
|
+
const GAP = 16;
|
|
37
|
+
const MORE_BTN_WIDTH = 90;
|
|
38
|
+
|
|
39
|
+
const isActive = (to: string) => {
|
|
40
|
+
if (!to) return false;
|
|
41
|
+
const currentPath = route.path.replace(/[?#].*$/, "").replace(/\/$/, "");
|
|
42
|
+
const targetPath = to.replace(/[?#].*$/, "").replace(/\/$/, "");
|
|
43
|
+
return currentPath === targetPath;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const visibleCount = computed(() => {
|
|
47
|
+
if (!availableWidth.value || itemWidths.value.length === 0)
|
|
48
|
+
return props.items.length;
|
|
49
|
+
let currentWidth = 0;
|
|
50
|
+
let count = 0;
|
|
51
|
+
for (let i = 0; i < props.items.length; i++) {
|
|
52
|
+
const itemW = itemWidths.value[i];
|
|
53
|
+
if (itemW === undefined) break;
|
|
54
|
+
const widthAfterAdding = currentWidth + itemW + (i > 0 ? GAP : 0);
|
|
55
|
+
const isLast = i === props.items.length - 1;
|
|
56
|
+
const requiredSpace = isLast
|
|
57
|
+
? widthAfterAdding
|
|
58
|
+
: widthAfterAdding + GAP + MORE_BTN_WIDTH;
|
|
59
|
+
if (requiredSpace <= availableWidth.value) {
|
|
60
|
+
currentWidth = widthAfterAdding;
|
|
61
|
+
count++;
|
|
62
|
+
} else {
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return count;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const visibleItems = computed(() => props.items.slice(0, visibleCount.value));
|
|
70
|
+
const hiddenItems = computed(() => props.items.slice(visibleCount.value));
|
|
71
|
+
|
|
72
|
+
const measure = () => {
|
|
73
|
+
if (!containerRef.value || !dummyRef.value) return;
|
|
74
|
+
availableWidth.value = containerRef.value.offsetWidth;
|
|
75
|
+
const children = Array.from(dummyRef.value.children) as HTMLElement[];
|
|
76
|
+
itemWidths.value = children.map((el) => el.getBoundingClientRect().width);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
80
|
+
if (isDropdownOpen.value && moreContainerRef.value) {
|
|
81
|
+
if (!moreContainerRef.value.contains(event.target as Node)) {
|
|
82
|
+
isDropdownOpen.value = false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
let resizeObserver: ResizeObserver | null = null;
|
|
88
|
+
onMounted(() => {
|
|
89
|
+
measure();
|
|
90
|
+
if (containerRef.value) {
|
|
91
|
+
resizeObserver = new ResizeObserver(() =>
|
|
92
|
+
requestAnimationFrame(measure),
|
|
93
|
+
);
|
|
94
|
+
resizeObserver.observe(containerRef.value);
|
|
95
|
+
}
|
|
96
|
+
document.addEventListener("click", handleClickOutside);
|
|
97
|
+
});
|
|
98
|
+
onUnmounted(() => {
|
|
99
|
+
if (resizeObserver) resizeObserver.disconnect();
|
|
100
|
+
document.removeEventListener("click", handleClickOutside);
|
|
101
|
+
});
|
|
102
|
+
watch(
|
|
103
|
+
() => props.items,
|
|
104
|
+
async () => {
|
|
105
|
+
await nextTick();
|
|
106
|
+
measure();
|
|
107
|
+
},
|
|
108
|
+
{ deep: true },
|
|
109
|
+
);
|
|
110
|
+
watch(
|
|
111
|
+
() => route.path,
|
|
112
|
+
() => {
|
|
113
|
+
isDropdownOpen.value = false;
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const layoutClass =
|
|
118
|
+
"flex items-center whitespace-nowrap transition-colors duration-200";
|
|
119
|
+
|
|
120
|
+
const defaultLink =
|
|
121
|
+
"h-10 px-3 rounded-md hover:bg-neutral-100 dark:hover:bg-white/10 text-neutral-600 dark:text-neutral-300 font-medium text-sm";
|
|
122
|
+
const defaultActive = "bg-neutral-100 dark:bg-white/10 text-primary";
|
|
123
|
+
const defaultMore =
|
|
124
|
+
"h-10 px-3 rounded-md gap-1.5 text-sm font-medium hover:bg-neutral-100 dark:hover:bg-white/10 text-neutral-600 dark:text-neutral-300";
|
|
125
|
+
const defaultDropdown =
|
|
126
|
+
"absolute right-0 top-full mt-2 w-56 bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-lg shadow-xl py-1";
|
|
127
|
+
</script>
|
|
128
|
+
|
|
129
|
+
<template>
|
|
130
|
+
<div class="relative w-full" :class="ui.wrapper">
|
|
131
|
+
<nav
|
|
132
|
+
ref="containerRef"
|
|
133
|
+
class="flex items-center w-full gap-4 flex-nowrap"
|
|
134
|
+
>
|
|
135
|
+
<div class="flex items-center gap-4 overflow-hidden flex-1">
|
|
136
|
+
<NuxtLink
|
|
137
|
+
v-for="item in visibleItems"
|
|
138
|
+
:key="item.to"
|
|
139
|
+
:to="item.to"
|
|
140
|
+
:class="[
|
|
141
|
+
layoutClass,
|
|
142
|
+
ui.link || defaultLink,
|
|
143
|
+
isActive(item.to) ? ui.linkActive || defaultActive : '',
|
|
144
|
+
]"
|
|
145
|
+
>
|
|
146
|
+
{{ item.label }}
|
|
147
|
+
</NuxtLink>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div
|
|
151
|
+
v-if="hiddenItems.length > 0"
|
|
152
|
+
ref="moreContainerRef"
|
|
153
|
+
class="relative shrink-0 ml-auto"
|
|
154
|
+
>
|
|
155
|
+
<button
|
|
156
|
+
@click="isDropdownOpen = !isDropdownOpen"
|
|
157
|
+
:class="[
|
|
158
|
+
layoutClass,
|
|
159
|
+
ui.moreButton || defaultMore,
|
|
160
|
+
isDropdownOpen && !ui.moreButton
|
|
161
|
+
? 'bg-neutral-100 dark:bg-white/10'
|
|
162
|
+
: '',
|
|
163
|
+
]"
|
|
164
|
+
>
|
|
165
|
+
Ещё
|
|
166
|
+
<svg
|
|
167
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
168
|
+
viewBox="0 0 20 20"
|
|
169
|
+
fill="currentColor"
|
|
170
|
+
class="w-4 h-4 transition-transform duration-200"
|
|
171
|
+
:class="{ 'rotate-180': isDropdownOpen }"
|
|
172
|
+
>
|
|
173
|
+
<path
|
|
174
|
+
fill-rule="evenodd"
|
|
175
|
+
d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"
|
|
176
|
+
clip-rule="evenodd"
|
|
177
|
+
/>
|
|
178
|
+
</svg>
|
|
179
|
+
</button>
|
|
180
|
+
|
|
181
|
+
<div
|
|
182
|
+
v-if="isDropdownOpen"
|
|
183
|
+
class="z-50 overflow-hidden animate-in fade-in zoom-in-95 duration-100"
|
|
184
|
+
:class="ui.dropdown || defaultDropdown"
|
|
185
|
+
>
|
|
186
|
+
<NuxtLink
|
|
187
|
+
v-for="item in hiddenItems"
|
|
188
|
+
:key="item.to"
|
|
189
|
+
:to="item.to"
|
|
190
|
+
class="block px-4 py-2.5 text-sm text-left transition-colors"
|
|
191
|
+
:class="[
|
|
192
|
+
ui.dropdownLink ||
|
|
193
|
+
'hover:bg-neutral-50 dark:hover:bg-white/5 text-neutral-600 dark:text-neutral-300',
|
|
194
|
+
isActive(item.to)
|
|
195
|
+
? 'text-primary font-medium bg-neutral-50 dark:bg-white/5'
|
|
196
|
+
: '',
|
|
197
|
+
]"
|
|
198
|
+
>
|
|
199
|
+
{{ item.label }}
|
|
200
|
+
</NuxtLink>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
</nav>
|
|
204
|
+
|
|
205
|
+
<div
|
|
206
|
+
ref="dummyRef"
|
|
207
|
+
class="absolute top-0 left-0 invisible -z-50 flex gap-4 pointer-events-none w-max"
|
|
208
|
+
aria-hidden="true"
|
|
209
|
+
>
|
|
210
|
+
<div
|
|
211
|
+
v-for="item in props.items"
|
|
212
|
+
:key="`dummy-${item.to}`"
|
|
213
|
+
:class="[layoutClass, ui.link || defaultLink]"
|
|
214
|
+
>
|
|
215
|
+
{{ item.label }}
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
</template>
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
interface Review {
|
|
3
|
+
text: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
city?: string;
|
|
6
|
+
avatar?: string;
|
|
7
|
+
link?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const props = defineProps<{ items: Review[] }>();
|
|
11
|
+
|
|
12
|
+
const currentIndex = ref(0);
|
|
13
|
+
const isPaused = ref(false);
|
|
14
|
+
const isDragging = ref(false);
|
|
15
|
+
const isMounted = ref(false);
|
|
16
|
+
const startX = ref(0);
|
|
17
|
+
const dragOffset = ref(0);
|
|
18
|
+
const visibleCount = ref(4);
|
|
19
|
+
const desktopVisible = 4;
|
|
20
|
+
|
|
21
|
+
const maxIndex = computed(() =>
|
|
22
|
+
Math.max(0, props.items.length - visibleCount.value),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const updateVisibleCount = () => {
|
|
26
|
+
if (typeof window !== "undefined") {
|
|
27
|
+
visibleCount.value = window.innerWidth < 768 ? 1 : desktopVisible;
|
|
28
|
+
|
|
29
|
+
if (currentIndex.value > maxIndex.value)
|
|
30
|
+
currentIndex.value = maxIndex.value;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const moveNextAuto = () =>
|
|
35
|
+
currentIndex.value >= maxIndex.value
|
|
36
|
+
? (currentIndex.value = 0)
|
|
37
|
+
: currentIndex.value++;
|
|
38
|
+
|
|
39
|
+
const moveNextManual = () => {
|
|
40
|
+
if (currentIndex.value < maxIndex.value) currentIndex.value++;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const movePrevManual = () => {
|
|
44
|
+
if (currentIndex.value > 0) currentIndex.value--;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const getClientX = (e: MouseEvent | TouchEvent): number => {
|
|
48
|
+
if ("touches" in e) {
|
|
49
|
+
return e.touches[0]?.clientX ?? 0;
|
|
50
|
+
}
|
|
51
|
+
return e.clientX;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const onStart = (e: MouseEvent | TouchEvent) => {
|
|
55
|
+
isPaused.value = true;
|
|
56
|
+
isDragging.value = true;
|
|
57
|
+
startX.value = getClientX(e);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const onMove = (e: MouseEvent | TouchEvent) => {
|
|
61
|
+
if (!isDragging.value) return;
|
|
62
|
+
const currentX = getClientX(e);
|
|
63
|
+
const diff = currentX - startX.value;
|
|
64
|
+
const isAtStart = currentIndex.value === 0 && diff > 0;
|
|
65
|
+
const isAtEnd = currentIndex.value >= maxIndex.value && diff < 0;
|
|
66
|
+
dragOffset.value = isAtStart || isAtEnd ? diff * 0.2 : diff;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const onEnd = () => {
|
|
70
|
+
if (!isDragging.value) return;
|
|
71
|
+
if (dragOffset.value < -50) moveNextManual();
|
|
72
|
+
else if (dragOffset.value > 50) movePrevManual();
|
|
73
|
+
isDragging.value = false;
|
|
74
|
+
dragOffset.value = 0;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
let interval: any = null;
|
|
78
|
+
|
|
79
|
+
const startAutoPlay = () => {
|
|
80
|
+
if (interval) clearInterval(interval);
|
|
81
|
+
interval = setInterval(() => {
|
|
82
|
+
if (!isPaused.value) moveNextAuto();
|
|
83
|
+
}, 4000);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
onMounted(() => {
|
|
87
|
+
updateVisibleCount();
|
|
88
|
+
window.addEventListener("resize", updateVisibleCount);
|
|
89
|
+
startAutoPlay();
|
|
90
|
+
nextTick(() => (isMounted.value = true));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
onUnmounted(() => {
|
|
94
|
+
if (interval) clearInterval(interval);
|
|
95
|
+
window.removeEventListener("resize", updateVisibleCount);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const { t } = useMultiLang(["interface"]);
|
|
99
|
+
</script>
|
|
100
|
+
|
|
101
|
+
<template>
|
|
102
|
+
<div class="slider-wrapper relative w-full overflow-hidden py-4">
|
|
103
|
+
<div
|
|
104
|
+
class="slider-track flex touch-pan-y transition-transform duration-500 ease-[cubic-bezier(0.25,1,0.5,1)]"
|
|
105
|
+
:class="{ 'transition-none': isDragging }"
|
|
106
|
+
:style="{
|
|
107
|
+
transform: `translateX(calc(-${currentIndex} * var(--slide-width) + ${dragOffset}px))`,
|
|
108
|
+
}"
|
|
109
|
+
@mousedown="onStart"
|
|
110
|
+
@mousemove="onMove"
|
|
111
|
+
@mouseup="onEnd"
|
|
112
|
+
@mouseleave="onEnd"
|
|
113
|
+
@touchstart="onStart"
|
|
114
|
+
@touchmove="onMove"
|
|
115
|
+
@touchend="onEnd"
|
|
116
|
+
>
|
|
117
|
+
<div
|
|
118
|
+
v-for="(item, index) in items"
|
|
119
|
+
:key="index"
|
|
120
|
+
class="slider-item shrink-0 px-1.5 flex select-none"
|
|
121
|
+
>
|
|
122
|
+
<div
|
|
123
|
+
class="card-inner w-full bg-white dark:bg-zinc-900 rounded-2xl p-5 md:p-6 shadow-sm border border-zinc-100 dark:border-zinc-800 flex flex-col pointer-events-none"
|
|
124
|
+
>
|
|
125
|
+
<div class="grow">
|
|
126
|
+
<p
|
|
127
|
+
class="text-zinc-700 dark:text-zinc-300 text-sm md:text-[15px] leading-snug"
|
|
128
|
+
>
|
|
129
|
+
{{ item.text }}
|
|
130
|
+
</p>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<div
|
|
134
|
+
v-if="item.name || item.link || item.avatar"
|
|
135
|
+
class="mt-4 pt-4 border-t border-zinc-50 dark:border-zinc-800/50 flex items-center justify-between"
|
|
136
|
+
>
|
|
137
|
+
<div class="flex items-center gap-3">
|
|
138
|
+
<div
|
|
139
|
+
v-if="item.avatar"
|
|
140
|
+
class="relative w-10 h-10 shrink-0 rounded-full overflow-hidden avatar-skeleton bg-zinc-100 dark:bg-zinc-800"
|
|
141
|
+
>
|
|
142
|
+
<ImgWithLoader
|
|
143
|
+
:src="item.avatar"
|
|
144
|
+
class="avatar-img w-full h-full object-cover"
|
|
145
|
+
alt="Reviewer"
|
|
146
|
+
/>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<div v-if="item.name || item.city">
|
|
150
|
+
<div
|
|
151
|
+
v-if="item.name"
|
|
152
|
+
class="font-bold text-zinc-900 dark:text-white text-sm leading-tight"
|
|
153
|
+
>
|
|
154
|
+
{{ item.name }}
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<div
|
|
158
|
+
v-if="item.city"
|
|
159
|
+
class="text-zinc-400 text-xs mt-0.5"
|
|
160
|
+
>
|
|
161
|
+
{{ item.city }}
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<a
|
|
167
|
+
v-if="item.link"
|
|
168
|
+
:href="item.link"
|
|
169
|
+
class="text-blue-500 dark:text-blue-400 text-xs font-medium hover:underline pointer-events-auto shrink-0 ml-4"
|
|
170
|
+
target="_blank"
|
|
171
|
+
>
|
|
172
|
+
{{ t("interface.goMore") }} →
|
|
173
|
+
</a>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<div
|
|
180
|
+
class="pagination-container flex justify-center gap-1.5 mt-6 transition-opacity duration-500 h-1"
|
|
181
|
+
:class="{ 'opacity-0': !isMounted, 'opacity-100': isMounted }"
|
|
182
|
+
>
|
|
183
|
+
<button
|
|
184
|
+
v-for="(_, i) in items.length - visibleCount + 1"
|
|
185
|
+
:key="i"
|
|
186
|
+
@click="
|
|
187
|
+
currentIndex = i;
|
|
188
|
+
|
|
189
|
+
isPaused = true;
|
|
190
|
+
"
|
|
191
|
+
class="h-full rounded-full transition-all duration-300"
|
|
192
|
+
:class="
|
|
193
|
+
currentIndex === i
|
|
194
|
+
? 'w-6 bg-zinc-800 dark:bg-zinc-200'
|
|
195
|
+
: 'w-1.5 bg-zinc-200 dark:bg-zinc-700'
|
|
196
|
+
"
|
|
197
|
+
></button>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
</template>
|
|
201
|
+
|
|
202
|
+
<style scoped>
|
|
203
|
+
.slider-wrapper {
|
|
204
|
+
--slide-width: 100%;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
@media (min-width: 768px) {
|
|
208
|
+
.slider-wrapper {
|
|
209
|
+
--slide-width: 25%;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.slider-item {
|
|
214
|
+
width: var(--slide-width);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.slider-track {
|
|
218
|
+
align-items: stretch;
|
|
219
|
+
cursor: grab;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.slider-track:active {
|
|
223
|
+
cursor: grabbing;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.select-none {
|
|
227
|
+
user-select: none;
|
|
228
|
+
-webkit-user-drag: none;
|
|
229
|
+
}
|
|
230
|
+
</style>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
interface SocialItem {
|
|
3
|
+
name: string;
|
|
4
|
+
to: string;
|
|
5
|
+
label: string;
|
|
6
|
+
color?: string;
|
|
7
|
+
target?: "_blank" | "_self";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type Variant = "simple" | "colored" | "circle" | "solid" | "glass";
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
items: SocialItem[];
|
|
14
|
+
variant?: Variant;
|
|
15
|
+
size?: string | number;
|
|
16
|
+
gap?: string;
|
|
17
|
+
iconClass?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
21
|
+
variant: "simple",
|
|
22
|
+
size: 24,
|
|
23
|
+
gap: "gap-4",
|
|
24
|
+
iconClass: "",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const baseLinkClass =
|
|
28
|
+
"flex items-center justify-center transition-colors duration-200 social-link no-external-icon";
|
|
29
|
+
|
|
30
|
+
const variantClasses: Record<Variant, string> = {
|
|
31
|
+
simple: "text-neutral-500 hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-white p-1",
|
|
32
|
+
colored: "hover:opacity-80 p-1",
|
|
33
|
+
circle: `
|
|
34
|
+
w-10 h-10 rounded-full border
|
|
35
|
+
border-neutral-200 text-neutral-600
|
|
36
|
+
hover:border-black hover:bg-black hover:text-white
|
|
37
|
+
dark:border-neutral-700 dark:text-neutral-400
|
|
38
|
+
dark:hover:border-white dark:hover:bg-white dark:hover:text-black
|
|
39
|
+
`,
|
|
40
|
+
solid: `
|
|
41
|
+
w-10 h-10 rounded-full
|
|
42
|
+
bg-neutral-100 text-neutral-600
|
|
43
|
+
hover:bg-neutral-900 hover:text-white
|
|
44
|
+
dark:bg-neutral-800 dark:text-neutral-300
|
|
45
|
+
dark:hover:bg-neutral-100 dark:hover:text-black
|
|
46
|
+
`,
|
|
47
|
+
glass: `
|
|
48
|
+
w-10 h-10 rounded-xl border
|
|
49
|
+
bg-white/50 border-white/20 text-neutral-700 hover:bg-white/80
|
|
50
|
+
dark:bg-white/5 dark:border-white/10 dark:text-neutral-300 dark:hover:bg-white/10
|
|
51
|
+
`,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const getLinkClass = (item: SocialItem) => {
|
|
55
|
+
let cls = `${baseLinkClass} ${variantClasses[props.variant].replace(/\s+/g, " ")}`;
|
|
56
|
+
if (props.variant === "colored" || props.variant === "simple") {
|
|
57
|
+
if (item.color) cls += ` ${item.color}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return cls;
|
|
61
|
+
};
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<template>
|
|
65
|
+
<div class="flex flex-wrap items-center" :class="gap">
|
|
66
|
+
<NuxtLink
|
|
67
|
+
v-for="(item, index) in items"
|
|
68
|
+
:key="index"
|
|
69
|
+
:to="item.to"
|
|
70
|
+
:target="item.target || '_blank'"
|
|
71
|
+
:aria-label="item.label"
|
|
72
|
+
:class="getLinkClass(item)"
|
|
73
|
+
external
|
|
74
|
+
>
|
|
75
|
+
<UiIcon :name="item.name" :size="size" :class="`${iconClass}`" />
|
|
76
|
+
</NuxtLink>
|
|
77
|
+
</div>
|
|
78
|
+
</template>
|