@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,121 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span
|
|
3
|
+
ref="target"
|
|
4
|
+
class="relative inline-flex flex-col items-center justify-center tabular-nums leading-none whitespace-nowrap"
|
|
5
|
+
:aria-label="`${prefix}${to}${suffix}`"
|
|
6
|
+
>
|
|
7
|
+
<span
|
|
8
|
+
class="opacity-0 select-none pointer-events-none"
|
|
9
|
+
aria-hidden="true"
|
|
10
|
+
>
|
|
11
|
+
{{ prefix }}{{ formattedTarget }}{{ suffix }}
|
|
12
|
+
</span>
|
|
13
|
+
<span
|
|
14
|
+
class="absolute inset-0 flex items-center justify-center"
|
|
15
|
+
aria-hidden="true"
|
|
16
|
+
>
|
|
17
|
+
{{ prefix }}{{ formattedCurrent }}{{ suffix }}
|
|
18
|
+
</span>
|
|
19
|
+
</span>
|
|
20
|
+
</template>
|
|
21
|
+
|
|
22
|
+
<script setup lang="ts">
|
|
23
|
+
interface Props {
|
|
24
|
+
to: number;
|
|
25
|
+
from?: number;
|
|
26
|
+
duration?: number;
|
|
27
|
+
prefix?: string;
|
|
28
|
+
suffix?: string;
|
|
29
|
+
decimals?: number;
|
|
30
|
+
separator?: string;
|
|
31
|
+
once?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
35
|
+
from: 0,
|
|
36
|
+
duration: 1500,
|
|
37
|
+
prefix: "",
|
|
38
|
+
suffix: "",
|
|
39
|
+
decimals: 0,
|
|
40
|
+
separator: " ",
|
|
41
|
+
once: true,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const target = ref<HTMLElement | null>(null);
|
|
45
|
+
const current = ref(props.from);
|
|
46
|
+
const hasAnimated = ref(false);
|
|
47
|
+
|
|
48
|
+
const formatNumber = (num: number) => {
|
|
49
|
+
return new Intl.NumberFormat("ru-RU", {
|
|
50
|
+
minimumFractionDigits: props.decimals,
|
|
51
|
+
maximumFractionDigits: props.decimals,
|
|
52
|
+
})
|
|
53
|
+
.format(num)
|
|
54
|
+
.replace(/,/g, ".");
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const formattedTarget = computed(() => formatNumber(props.to));
|
|
58
|
+
const formattedCurrent = computed(() => formatNumber(current.value));
|
|
59
|
+
|
|
60
|
+
const animate = () => {
|
|
61
|
+
const startTimestamp = performance.now();
|
|
62
|
+
const startValue = current.value;
|
|
63
|
+
const endValue = props.to;
|
|
64
|
+
const diff = endValue - startValue;
|
|
65
|
+
|
|
66
|
+
const step = (timestamp: number) => {
|
|
67
|
+
const elapsed = timestamp - startTimestamp;
|
|
68
|
+
const progress = Math.min(elapsed / props.duration, 1);
|
|
69
|
+
|
|
70
|
+
const ease = 1 - Math.pow(1 - progress, 4);
|
|
71
|
+
|
|
72
|
+
current.value = startValue + diff * ease;
|
|
73
|
+
|
|
74
|
+
if (progress < 1) {
|
|
75
|
+
requestAnimationFrame(step);
|
|
76
|
+
} else {
|
|
77
|
+
current.value = endValue;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
requestAnimationFrame(step);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
onMounted(() => {
|
|
85
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
86
|
+
animate();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const observer = new IntersectionObserver(
|
|
91
|
+
(entries) => {
|
|
92
|
+
entries.forEach((entry) => {
|
|
93
|
+
if (entry.isIntersecting) {
|
|
94
|
+
if (props.once && hasAnimated.value) return;
|
|
95
|
+
|
|
96
|
+
if (!hasAnimated.value) current.value = props.from;
|
|
97
|
+
|
|
98
|
+
animate();
|
|
99
|
+
hasAnimated.value = true;
|
|
100
|
+
|
|
101
|
+
if (props.once) observer.disconnect();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
{ threshold: 0.2 },
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
if (target.value) {
|
|
109
|
+
observer.observe(target.value);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
watch(
|
|
114
|
+
() => props.to,
|
|
115
|
+
() => {
|
|
116
|
+
if (hasAnimated.value) {
|
|
117
|
+
animate();
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
</script>
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const props = withDefaults(
|
|
3
|
+
defineProps<{
|
|
4
|
+
title?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
|
|
7
|
+
icon?: string;
|
|
8
|
+
|
|
9
|
+
color?: "primary" | "gray" | "info" | "success" | "warning" | "error";
|
|
10
|
+
variant?: "solid" | "soft" | "outline";
|
|
11
|
+
|
|
12
|
+
sticky?: boolean;
|
|
13
|
+
closable?: boolean;
|
|
14
|
+
}>(),
|
|
15
|
+
{
|
|
16
|
+
color: "primary",
|
|
17
|
+
variant: "soft",
|
|
18
|
+
sticky: false,
|
|
19
|
+
closable: false,
|
|
20
|
+
},
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const visible = ref(true);
|
|
24
|
+
|
|
25
|
+
const close = () => {
|
|
26
|
+
visible.value = false;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const colorVariants: Record<
|
|
30
|
+
NonNullable<typeof props.color>,
|
|
31
|
+
Record<NonNullable<typeof props.variant>, string>
|
|
32
|
+
> = {
|
|
33
|
+
gray: {
|
|
34
|
+
solid: "bg-gray-800 text-white border-b border-gray-800/30",
|
|
35
|
+
soft: "bg-gray-100 text-gray-900 border-b border-gray-300",
|
|
36
|
+
outline: "bg-transparent text-gray-900 border-b border-gray-400",
|
|
37
|
+
},
|
|
38
|
+
primary: {
|
|
39
|
+
solid: "bg-primary-600 text-white border-b border-primary-600/30",
|
|
40
|
+
soft: "bg-primary-100 text-primary-900 border-b border-primary-300",
|
|
41
|
+
outline: "bg-transparent text-primary-900 border-b border-primary-400",
|
|
42
|
+
},
|
|
43
|
+
info: {
|
|
44
|
+
solid: "bg-blue-600 text-white border-b border-blue-600/30",
|
|
45
|
+
soft: "bg-blue-100 text-blue-900 border-b border-blue-300",
|
|
46
|
+
outline: "bg-transparent text-blue-900 border-b border-blue-400",
|
|
47
|
+
},
|
|
48
|
+
success: {
|
|
49
|
+
solid: "bg-emerald-600 text-white border-b border-emerald-600/30",
|
|
50
|
+
soft: "bg-emerald-100 text-emerald-900 border-b border-emerald-300",
|
|
51
|
+
outline: "bg-transparent text-emerald-900 border-b border-emerald-400",
|
|
52
|
+
},
|
|
53
|
+
warning: {
|
|
54
|
+
solid: "bg-amber-500 text-white border-b border-amber-500/30",
|
|
55
|
+
soft: "bg-amber-100 text-amber-900 border-b border-amber-300",
|
|
56
|
+
outline: "bg-transparent text-amber-900 border-b border-amber-400",
|
|
57
|
+
},
|
|
58
|
+
error: {
|
|
59
|
+
solid: "bg-red-600 text-white border-b border-red-600/30",
|
|
60
|
+
soft: "bg-red-100 text-red-900 border-b border-red-300",
|
|
61
|
+
outline: "bg-transparent text-red-900 border-b border-red-400",
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const classes = computed(() => [
|
|
66
|
+
"w-full flex items-center gap-4 px-4 py-2 text-sm",
|
|
67
|
+
props.sticky ? "sticky top-0 z-50" : null,
|
|
68
|
+
colorVariants[props.color][props.variant],
|
|
69
|
+
]);
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<template>
|
|
73
|
+
<div v-if="visible" role="region" :class="classes">
|
|
74
|
+
<div v-if="icon" class="flex items-center shrink-0">
|
|
75
|
+
<UiIcon :name="icon" class="h-5 w-5" />
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div class="flex-1 min-w-0 flex items-center gap-2">
|
|
79
|
+
<span v-if="$slots.title || title" class="font-medium">
|
|
80
|
+
<slot name="title">{{ title }}</slot>
|
|
81
|
+
</span>
|
|
82
|
+
|
|
83
|
+
<span
|
|
84
|
+
v-if="$slots.default || description"
|
|
85
|
+
class="opacity-90 truncate"
|
|
86
|
+
>
|
|
87
|
+
<slot>{{ description }}</slot>
|
|
88
|
+
</span>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div v-if="$slots.actions" class="flex items-center gap-2 shrink-0">
|
|
92
|
+
<slot name="actions" />
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<button
|
|
96
|
+
v-if="closable"
|
|
97
|
+
@click="close"
|
|
98
|
+
class="ml-2 opacity-60 hover:opacity-100 transition shrink-0"
|
|
99
|
+
aria-label="Close announcement"
|
|
100
|
+
>
|
|
101
|
+
<UiIcon name="heroicons:x-mark" class="h-4 w-4" />
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
</template>
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { Fragment, isVNode } from "vue";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
size?: "sm" | "md" | "lg" | "xl";
|
|
6
|
+
max?: number;
|
|
7
|
+
class?: any;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
11
|
+
size: "md",
|
|
12
|
+
max: 7,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const slots = useSlots();
|
|
16
|
+
|
|
17
|
+
function flatten(nodes: any[]): any[] {
|
|
18
|
+
const out: any[] = [];
|
|
19
|
+
|
|
20
|
+
for (const n of nodes) {
|
|
21
|
+
if (!n) continue;
|
|
22
|
+
|
|
23
|
+
if (Array.isArray(n)) {
|
|
24
|
+
out.push(...flatten(n));
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (isVNode(n) && n.type === Fragment && Array.isArray(n.children)) {
|
|
29
|
+
out.push(...flatten(n.children as any[]));
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (isVNode(n) && (n.type === Text || n.type === Comment)) continue;
|
|
34
|
+
|
|
35
|
+
out.push(n);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const children = computed(() => flatten(slots.default?.() ?? []));
|
|
42
|
+
|
|
43
|
+
const visibleChildren = computed(() => {
|
|
44
|
+
if (!props.max) return children.value;
|
|
45
|
+
return children.value.slice(0, props.max);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const remainingCount = computed(() => {
|
|
49
|
+
return Math.max(0, children.value.length - (props.max || 0));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const baseItemClass =
|
|
53
|
+
"relative -ml-2 ring-2 ring-white dark:ring-zinc-900 transition-transform hover:z-50";
|
|
54
|
+
|
|
55
|
+
const rendered = computed(() => {
|
|
56
|
+
const list = [...visibleChildren.value].reverse();
|
|
57
|
+
|
|
58
|
+
return list.map((child: any, index: number) => {
|
|
59
|
+
if (!isVNode(child)) return child;
|
|
60
|
+
|
|
61
|
+
const childProps = (child.props ?? {}) as Record<string, any>;
|
|
62
|
+
|
|
63
|
+
return h(
|
|
64
|
+
child.type as any,
|
|
65
|
+
{
|
|
66
|
+
...childProps,
|
|
67
|
+
key: child.key ?? index,
|
|
68
|
+
size: props.size,
|
|
69
|
+
class: [baseItemClass, childProps.class],
|
|
70
|
+
style: [childProps.style, { zIndex: index + 1 }],
|
|
71
|
+
},
|
|
72
|
+
child.children as any,
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
<template>
|
|
79
|
+
<div
|
|
80
|
+
:class="[
|
|
81
|
+
'flex items-center justify-start flex-row-reverse',
|
|
82
|
+
props.class,
|
|
83
|
+
]"
|
|
84
|
+
>
|
|
85
|
+
<UiAvatar
|
|
86
|
+
v-if="remainingCount > 0"
|
|
87
|
+
:text="`+${remainingCount}`"
|
|
88
|
+
:size="size"
|
|
89
|
+
class="relative -ml-2 ring-2 ring-white dark:ring-zinc-900 z-0"
|
|
90
|
+
/>
|
|
91
|
+
|
|
92
|
+
<component
|
|
93
|
+
v-for="node in rendered"
|
|
94
|
+
:key="(node as any)?.key"
|
|
95
|
+
:is="node"
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
</template>
|
|
99
|
+
|
|
100
|
+
<style scoped>
|
|
101
|
+
.flex-row-reverse > *:last-child {
|
|
102
|
+
margin-left: 0;
|
|
103
|
+
}
|
|
104
|
+
</style>
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<nav class="ui-breadcrumbs" :class="containerClass" aria-label="Breadcrumb">
|
|
3
|
+
<ol class="breadcrumb-list">
|
|
4
|
+
<li class="breadcrumb-item">
|
|
5
|
+
<NuxtLink :to="homePath" class="breadcrumb-link">
|
|
6
|
+
{{ homeLabel }}
|
|
7
|
+
</NuxtLink>
|
|
8
|
+
<span v-if="breadcrumbs.length > 0" class="separator">{{
|
|
9
|
+
separator
|
|
10
|
+
}}</span>
|
|
11
|
+
</li>
|
|
12
|
+
|
|
13
|
+
<li
|
|
14
|
+
v-for="(crumb, index) in breadcrumbs"
|
|
15
|
+
:key="crumb.path"
|
|
16
|
+
class="breadcrumb-item"
|
|
17
|
+
>
|
|
18
|
+
<span
|
|
19
|
+
v-if="index === breadcrumbs.length - 1"
|
|
20
|
+
class="current-page"
|
|
21
|
+
>
|
|
22
|
+
{{ truncate(crumb.title, maxChars) }}
|
|
23
|
+
</span>
|
|
24
|
+
|
|
25
|
+
<template v-else>
|
|
26
|
+
<NuxtLink :to="crumb.path" class="breadcrumb-link">
|
|
27
|
+
{{ truncate(crumb.title, maxChars) }}
|
|
28
|
+
</NuxtLink>
|
|
29
|
+
<span class="separator">{{ separator }}</span>
|
|
30
|
+
</template>
|
|
31
|
+
</li>
|
|
32
|
+
</ol>
|
|
33
|
+
</nav>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<script setup lang="ts">
|
|
37
|
+
const props = withDefaults(
|
|
38
|
+
defineProps<{
|
|
39
|
+
maxChars?: number;
|
|
40
|
+
separator?: string;
|
|
41
|
+
containerClass?: string;
|
|
42
|
+
}>(),
|
|
43
|
+
{
|
|
44
|
+
maxChars: 30,
|
|
45
|
+
separator: "/",
|
|
46
|
+
containerClass: "",
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const { breadcrumbs, homePath, homeLabel, truncate } = useBreadcrumbs();
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<style scoped>
|
|
54
|
+
.ui-breadcrumbs {
|
|
55
|
+
padding: 0.75rem 0;
|
|
56
|
+
font-size: 0.875rem;
|
|
57
|
+
}
|
|
58
|
+
.breadcrumb-list {
|
|
59
|
+
display: flex;
|
|
60
|
+
flex-wrap: wrap;
|
|
61
|
+
list-style: none;
|
|
62
|
+
padding: 0;
|
|
63
|
+
margin: 0;
|
|
64
|
+
align-items: center;
|
|
65
|
+
}
|
|
66
|
+
.breadcrumb-item {
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
color: var(--ui-text);
|
|
70
|
+
line-height: 1.2;
|
|
71
|
+
}
|
|
72
|
+
.breadcrumb-link {
|
|
73
|
+
color: var(--ui-text);
|
|
74
|
+
opacity: 0.6;
|
|
75
|
+
text-decoration: none;
|
|
76
|
+
transition: opacity 0.2s;
|
|
77
|
+
}
|
|
78
|
+
.breadcrumb-link:hover {
|
|
79
|
+
opacity: 1;
|
|
80
|
+
}
|
|
81
|
+
.separator {
|
|
82
|
+
margin: 0 0.5rem;
|
|
83
|
+
opacity: 0.3;
|
|
84
|
+
user-select: none;
|
|
85
|
+
font-size: 0.9em;
|
|
86
|
+
}
|
|
87
|
+
.current-page {
|
|
88
|
+
font-weight: 500;
|
|
89
|
+
opacity: 0.9;
|
|
90
|
+
text-transform: capitalize;
|
|
91
|
+
}
|
|
92
|
+
@media (max-width: 640px) {
|
|
93
|
+
.ui-breadcrumbs {
|
|
94
|
+
font-size: 0.8rem;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
</style>
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cloneVNode, Fragment } from "vue";
|
|
3
|
+
import { transliterate } from "~/utils/transliterate";
|
|
4
|
+
|
|
5
|
+
function getTextFromNodes(nodes: VNode[] | VNode | string): string {
|
|
6
|
+
if (typeof nodes === "string") return nodes;
|
|
7
|
+
if (Array.isArray(nodes)) return nodes.map(getTextFromNodes).join("");
|
|
8
|
+
if (!nodes) return "";
|
|
9
|
+
if (nodes.type === Text) return (nodes.children as string) || "";
|
|
10
|
+
if (nodes.type === Fragment && Array.isArray(nodes.children))
|
|
11
|
+
return getTextFromNodes(nodes.children as VNode[]);
|
|
12
|
+
if (typeof nodes.children === "string") return nodes.children;
|
|
13
|
+
if (Array.isArray(nodes.children))
|
|
14
|
+
return getTextFromNodes(nodes.children as VNode[]);
|
|
15
|
+
if (typeof nodes.children === "object" && (nodes.children as any).default) {
|
|
16
|
+
try {
|
|
17
|
+
return getTextFromNodes((nodes.children as any).default());
|
|
18
|
+
} catch (e) {
|
|
19
|
+
return "";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function flattenFragments(nodes: VNode[]): VNode[] {
|
|
26
|
+
let result: VNode[] = [];
|
|
27
|
+
for (const node of nodes) {
|
|
28
|
+
if (node.type === Fragment && Array.isArray(node.children)) {
|
|
29
|
+
result = result.concat(flattenFragments(node.children as VNode[]));
|
|
30
|
+
} else {
|
|
31
|
+
result.push(node);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default defineComponent({
|
|
38
|
+
name: "ContentWithToc",
|
|
39
|
+
setup(_, { slots }) {
|
|
40
|
+
const route = useRoute();
|
|
41
|
+
|
|
42
|
+
const scrollToId = (id: string, smooth = true) => {
|
|
43
|
+
const element = document.getElementById(id);
|
|
44
|
+
if (element) {
|
|
45
|
+
const headerOffset = 60;
|
|
46
|
+
const elementPosition = element.getBoundingClientRect().top;
|
|
47
|
+
const offsetPosition =
|
|
48
|
+
elementPosition + window.pageYOffset - headerOffset;
|
|
49
|
+
|
|
50
|
+
window.scrollTo({
|
|
51
|
+
top: offsetPosition,
|
|
52
|
+
behavior: smooth ? "smooth" : "auto",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const handleNavClick = (id: string) => {
|
|
58
|
+
if (history.pushState) {
|
|
59
|
+
history.pushState(null, "", `#${id}`);
|
|
60
|
+
} else {
|
|
61
|
+
window.location.hash = id;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
scrollToId(id, true);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
onMounted(async () => {
|
|
68
|
+
await nextTick();
|
|
69
|
+
|
|
70
|
+
if (route.hash) {
|
|
71
|
+
const id = route.hash.replace("#", "");
|
|
72
|
+
setTimeout(() => {
|
|
73
|
+
scrollToId(id, true);
|
|
74
|
+
}, 300);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return () => {
|
|
79
|
+
const rawChildren = slots.default ? slots.default() : [];
|
|
80
|
+
const children = flattenFragments(rawChildren);
|
|
81
|
+
const tocItems: { id: string; text: string }[] = [];
|
|
82
|
+
|
|
83
|
+
const modifiedChildren = children.map((node, index) => {
|
|
84
|
+
const type = node.type as any;
|
|
85
|
+
const tag =
|
|
86
|
+
typeof type === "string"
|
|
87
|
+
? type
|
|
88
|
+
: type.name || type.__name || "";
|
|
89
|
+
|
|
90
|
+
const isHeading =
|
|
91
|
+
["h2", "h3", "h4"].includes(tag) ||
|
|
92
|
+
tag.includes("Heading") ||
|
|
93
|
+
(node.props && node.props.level);
|
|
94
|
+
|
|
95
|
+
if (isHeading) {
|
|
96
|
+
const text = getTextFromNodes(node);
|
|
97
|
+
if (text && text.trim().length > 0) {
|
|
98
|
+
let id = node.props?.id;
|
|
99
|
+
if (!id) {
|
|
100
|
+
id = transliterate(text);
|
|
101
|
+
if (tocItems.find((t) => t.id === id))
|
|
102
|
+
id = `${id}-${index}`;
|
|
103
|
+
}
|
|
104
|
+
tocItems.push({ id, text });
|
|
105
|
+
return cloneVNode(node, {
|
|
106
|
+
id,
|
|
107
|
+
style: "scroll-margin-top: 1rem;",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return node;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const nav =
|
|
115
|
+
tocItems.length > 0
|
|
116
|
+
? h("nav", { class: "w-full mb-2" }, [
|
|
117
|
+
h(
|
|
118
|
+
"div",
|
|
119
|
+
{ class: "flex flex-wrap gap-2" },
|
|
120
|
+
tocItems.map((item) =>
|
|
121
|
+
h(
|
|
122
|
+
"a",
|
|
123
|
+
{
|
|
124
|
+
href: `#${item.id}`,
|
|
125
|
+
class: `
|
|
126
|
+
inline-flex items-center px-3 py-1.5 rounded-md
|
|
127
|
+
text-xs font-medium uppercase tracking-wide
|
|
128
|
+
transition-all duration-200
|
|
129
|
+
|
|
130
|
+
bg-gray-100 text-gray-700
|
|
131
|
+
dark:bg-white/5 dark:text-gray-200
|
|
132
|
+
|
|
133
|
+
hover:bg-gray-200 dark:hover:bg-white/10
|
|
134
|
+
hover:shadow-sm
|
|
135
|
+
`,
|
|
136
|
+
onClick: (e: Event) => {
|
|
137
|
+
e.preventDefault();
|
|
138
|
+
handleNavClick(item.id);
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
item.text,
|
|
142
|
+
),
|
|
143
|
+
),
|
|
144
|
+
),
|
|
145
|
+
])
|
|
146
|
+
: null;
|
|
147
|
+
|
|
148
|
+
return h("div", { class: "relative" }, [nav, modifiedChildren]);
|
|
149
|
+
};
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
</script>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="ui-copyright" :class="containerClass">
|
|
3
|
+
<span class="copyright-text">
|
|
4
|
+
{{ symbol }} {{ yearRange }} {{ owner }}.
|
|
5
|
+
</span>
|
|
6
|
+
<span v-if="allRightsReserved" class="rights-text">
|
|
7
|
+
{{ t("common.all_rights_reserved") }}
|
|
8
|
+
</span>
|
|
9
|
+
</div>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script setup lang="ts">
|
|
13
|
+
const props = withDefaults(
|
|
14
|
+
defineProps<{
|
|
15
|
+
owner: string;
|
|
16
|
+
startYear?: number | string;
|
|
17
|
+
allRightsReserved?: boolean;
|
|
18
|
+
containerClass?: string;
|
|
19
|
+
}>(),
|
|
20
|
+
{
|
|
21
|
+
allRightsReserved: true,
|
|
22
|
+
},
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const { t } = useMultiLang(["common"]);
|
|
26
|
+
|
|
27
|
+
const symbol = "©";
|
|
28
|
+
|
|
29
|
+
const yearRange = computed(() => {
|
|
30
|
+
const currentYear = new Date().getFullYear();
|
|
31
|
+
const start =
|
|
32
|
+
typeof props.startYear === "string"
|
|
33
|
+
? parseInt(props.startYear, 10)
|
|
34
|
+
: props.startYear;
|
|
35
|
+
|
|
36
|
+
if (!start || isNaN(start) || start >= currentYear) {
|
|
37
|
+
return currentYear.toString();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return `${start}-${currentYear}`;
|
|
41
|
+
});
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<style scoped>
|
|
45
|
+
.ui-copyright {
|
|
46
|
+
color: var(--ui-text);
|
|
47
|
+
opacity: 0.7;
|
|
48
|
+
font-size: 0.875rem;
|
|
49
|
+
display: flex;
|
|
50
|
+
flex-wrap: wrap;
|
|
51
|
+
align-items: center;
|
|
52
|
+
gap: 0.5ch;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.copyright-text {
|
|
56
|
+
white-space: nowrap;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.rights-text {
|
|
60
|
+
text-transform: capitalize;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@media (max-width: 640px) {
|
|
64
|
+
.ui-copyright {
|
|
65
|
+
font-size: 0.8rem;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
</style>
|