@brumaombra/ui-vintage 0.8.0 → 0.8.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/COMPONENTS.md +46 -2
- package/README.md +1 -1
- package/package.json +3 -2
- package/src/components/blog/AllPostsSection.vue +1 -2
- package/src/components/blog/BlogContentRenderer.vue +144 -49
- package/src/components/blog/BlogFAQSection.vue +13 -15
- package/src/components/blog/BlogHeaderSection.vue +27 -9
- package/src/components/blog/BlogInfoSection.vue +37 -36
- package/src/components/blog/BlogProgressBar.vue +5 -2
- package/src/components/blog/BlogSectionTitle.vue +24 -5
- package/src/components/blog/BlogTagsSection.vue +8 -5
- package/src/components/blog/CategoriesList.vue +2 -2
- package/src/components/blog/CategoryCard.vue +25 -16
- package/src/components/blog/HeaderCarousel.vue +109 -146
- package/src/components/blog/PostCard.vue +34 -35
- package/src/components/blog/PostsList.vue +3 -2
- package/src/components/blog/SocialShareSidebar.vue +51 -41
- package/src/components/blog/TableOfContents.vue +14 -10
- package/src/components/content/BlogList.vue +16 -4
- package/src/components/content/BlogTable.vue +4 -4
- package/src/components/content/CodeBlock.vue +5 -5
- package/src/components/content/Flow.vue +4 -4
- package/src/components/content/ProseHr.vue +5 -5
- package/src/components/content/Terminal.vue +10 -4
- package/src/components/cookie-consent/CookieConsent.vue +82 -0
- package/src/components/cookie-consent/index.ts +3 -0
- package/src/components/cookie-consent/useCookieConsent.ts +55 -0
- package/src/components/landing/landing-footer/LandingFooter.vue +9 -4
- package/src/components/landing/landing-navbar/LandingNavbar.vue +47 -51
- package/src/components/landing/landing-shell/LandingShell.vue +7 -2
- package/src/components/ui/accordion/Accordion.vue +6 -6
- package/src/components/ui/animated-number/AnimatedNumber.vue +19 -5
- package/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue +1 -1
- package/src/components/ui/breadcrumb/BreadcrumbLink.vue +1 -1
- package/src/components/ui/breadcrumb/BreadcrumbList.vue +1 -1
- package/src/components/ui/breadcrumb/BreadcrumbPage.vue +1 -1
- package/src/components/ui/breadcrumb/BreadcrumbSeparator.vue +2 -6
- package/src/components/ui/card/Card.vue +1 -1
- package/src/components/ui/command/CommandItem.vue +2 -2
- package/src/i18n/de.json +10 -1
- package/src/i18n/en.json +10 -1
- package/src/i18n/es.json +10 -1
- package/src/i18n/fr.json +10 -1
- package/src/i18n/it.json +10 -1
- package/src/i18n/ja.json +10 -1
- package/src/i18n/pt.json +10 -1
- package/src/i18n/ru.json +10 -1
- package/src/i18n/zh.json +10 -1
- package/src/styles.css +40 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { NuxtImg } from '#components';
|
|
3
|
+
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
|
3
4
|
import type { HTMLAttributes } from 'vue';
|
|
4
5
|
import BlogProgressBar from '../../blog/BlogProgressBar.vue';
|
|
5
6
|
import { cn } from '../../../lib/utils';
|
|
@@ -21,62 +22,57 @@ const props = withDefaults(defineProps<{
|
|
|
21
22
|
appLinkTo: '/',
|
|
22
23
|
showProgress: false
|
|
23
24
|
});
|
|
24
|
-
</script>
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
<nav :class="cn('mx-auto flex max-w-6xl basis-full items-center justify-between px-3 sm:px-6 lg:px-8', props.containerClass)">
|
|
29
|
-
<!-- Left-aligned content -->
|
|
30
|
-
<div :class="cn('flex items-center', props.leftClass)">
|
|
31
|
-
<slot name="left">
|
|
32
|
-
<a :href="props.appLinkTo" class="inline-flex items-center gap-2 transition-opacity hover:opacity-80">
|
|
33
|
-
<!-- App logo -->
|
|
34
|
-
<NuxtImg v-if="props.appLogo" :src="props.appLogo" :alt="`${props.appName} logo`" width="44" height="44" :sizes="{ 320: '44px', 640: '36px' }" loading="eager" fetchpriority="high" :class="cn('size-11 sm:size-9 shrink-0 object-contain', props.logoClass)" />
|
|
26
|
+
const SCROLL_THRESHOLD_PX = 12;
|
|
27
|
+
const isScrolled = ref(false);
|
|
35
28
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
</a>
|
|
41
|
-
</slot>
|
|
42
|
-
</div>
|
|
29
|
+
// Float the navbar once the page leaves the top
|
|
30
|
+
const handleScroll = () => {
|
|
31
|
+
isScrolled.value = window.scrollY > SCROLL_THRESHOLD_PX;
|
|
32
|
+
};
|
|
43
33
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
34
|
+
// On component mounted
|
|
35
|
+
onMounted(() => {
|
|
36
|
+
handleScroll();
|
|
37
|
+
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
38
|
+
});
|
|
49
39
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
</template>
|
|
40
|
+
// On before component unmount
|
|
41
|
+
onBeforeUnmount(() => {
|
|
42
|
+
window.removeEventListener('scroll', handleScroll);
|
|
43
|
+
});
|
|
44
|
+
</script>
|
|
56
45
|
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
46
|
+
<template>
|
|
47
|
+
<!-- Transparent at the top of the page, a floating card once scrolled -->
|
|
48
|
+
<header :data-scrolled="isScrolled ? '' : undefined" :class="cn('sticky top-0 z-30 w-full text-card-foreground transition-[padding] duration-500 ease-out-expo', isScrolled ? 'px-2 pt-2 sm:px-4 sm:pt-3' : 'px-0 pt-0', props.class)">
|
|
49
|
+
<div :class="cn('relative mx-auto border transition-[max-width,border-radius,background-color,border-color,box-shadow] duration-500 ease-out-expo', isScrolled ? 'max-w-5xl rounded border-border bg-card shadow-elevated-md' : 'max-w-6xl rounded-none border-transparent bg-transparent shadow-none')">
|
|
50
|
+
<nav :class="cn('flex items-center justify-between px-3 transition-[height] duration-500 ease-out-expo sm:px-6 lg:px-8', isScrolled ? 'h-16 sm:px-5 lg:px-5' : 'h-20', props.containerClass)">
|
|
51
|
+
<!-- Left-aligned content -->
|
|
52
|
+
<div :class="cn('flex items-center', props.leftClass)">
|
|
53
|
+
<slot name="left">
|
|
54
|
+
<a :href="props.appLinkTo" class="inline-flex items-center gap-2 transition-[opacity,scale] duration-300 ease-spring hover:scale-[1.02] hover:opacity-90">
|
|
55
|
+
<!-- App logo -->
|
|
56
|
+
<NuxtImg v-if="props.appLogo" :src="props.appLogo" :alt="`${props.appName} logo`" width="44" height="44" :sizes="{ 320: '44px', 640: '36px' }" loading="eager" fetchpriority="high" :class="cn('size-11 sm:size-9 shrink-0 object-contain', props.logoClass)" />
|
|
63
57
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
58
|
+
<!-- App name -->
|
|
59
|
+
<span v-if="props.appName" class="hidden sm:inline! text-xl font-bold tracking-tight text-foreground">
|
|
60
|
+
{{ props.appName }}
|
|
61
|
+
</span>
|
|
62
|
+
</a>
|
|
63
|
+
</slot>
|
|
64
|
+
</div>
|
|
69
65
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
66
|
+
<!-- Right-aligned content -->
|
|
67
|
+
<div :class="cn('flex items-center justify-end gap-2 sm:gap-3', props.rightClass)">
|
|
68
|
+
<slot name="right" />
|
|
69
|
+
</div>
|
|
70
|
+
</nav>
|
|
75
71
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
</
|
|
72
|
+
<!-- Reading progress along the bottom edge of the floating bar -->
|
|
73
|
+
<div v-if="props.showProgress" :class="cn('absolute inset-x-1 bottom-0 transition-opacity duration-300', isScrolled ? 'opacity-100' : 'opacity-0')">
|
|
74
|
+
<BlogProgressBar />
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</header>
|
|
78
|
+
</template>
|
|
@@ -24,8 +24,13 @@ const hasStructuredSlots = computed(() => {
|
|
|
24
24
|
|
|
25
25
|
<template>
|
|
26
26
|
<div :class="cn('relative flex min-h-screen flex-col bg-background text-foreground', props.rootClass)">
|
|
27
|
-
<!-- Background grid -->
|
|
28
|
-
<
|
|
27
|
+
<!-- Background grid and a warm ambient glow at the top -->
|
|
28
|
+
<template v-if="props.showBackground">
|
|
29
|
+
<BackgroundGrid />
|
|
30
|
+
<div aria-hidden="true" class="pointer-events-none absolute inset-x-0 top-0 z-0 h-[36rem] overflow-hidden">
|
|
31
|
+
<div class="absolute -top-48 left-1/2 h-96 w-[56rem] max-w-[140%] -translate-x-1/2 rounded-full bg-primary/10 blur-[110px]" />
|
|
32
|
+
</div>
|
|
33
|
+
</template>
|
|
29
34
|
|
|
30
35
|
<!-- If structured slots are provided, render them -->
|
|
31
36
|
<template v-if="hasStructuredSlots">
|
|
@@ -40,13 +40,13 @@ const toggleExpanded = () => {
|
|
|
40
40
|
</script>
|
|
41
41
|
|
|
42
42
|
<template>
|
|
43
|
-
<Card :data-state="expanded ? 'open' : 'closed'" :class="cn('group/accordion gap-0! overflow-hidden sm:gap-0! p-0! data-[state=open]:border-
|
|
43
|
+
<Card :data-state="expanded ? 'open' : 'closed'" :class="cn('group/accordion gap-0! overflow-hidden sm:gap-0! p-0! data-[state=open]:border-primary/25', props.class)">
|
|
44
44
|
<!-- Header -->
|
|
45
|
-
<button type="button" :aria-expanded="expanded" :aria-controls="contentId" class="flex w-full cursor-pointer items-center justify-between gap-4 p-5 text-left outline-none
|
|
45
|
+
<button type="button" :aria-expanded="expanded" :aria-controls="contentId" class="group/trigger flex w-full cursor-pointer items-center justify-between gap-4 p-5 text-left outline-none focus-visible:bg-surface focus-visible:ring-[3px] focus-visible:ring-inset focus-visible:ring-ring/40" @click="toggleExpanded">
|
|
46
46
|
<div class="flex min-w-0 items-center gap-3">
|
|
47
47
|
<!-- Icon -->
|
|
48
|
-
<div v-if="props.icon" class="flex size-8 shrink-0 items-center justify-center rounded border border-border bg-card transition-[border-color,color] duration-200 group-data-[state=open]/accordion:border-primary/60 md:size-10">
|
|
49
|
-
<HugeiconsIcon :icon="props.icon" :class="cn('size-4 text-muted-foreground transition-colors duration-200 group-data-[state=open]/accordion:text-primary', props.iconClass)" />
|
|
48
|
+
<div v-if="props.icon" class="flex size-8 shrink-0 items-center justify-center rounded border border-border bg-card transition-[border-color,color] duration-200 group-hover/trigger:border-primary/60 group-data-[state=open]/accordion:border-primary/60 md:size-10">
|
|
49
|
+
<HugeiconsIcon :icon="props.icon" :class="cn('size-4 text-muted-foreground transition-colors duration-200 group-hover/trigger:text-primary group-data-[state=open]/accordion:text-primary', props.iconClass)" />
|
|
50
50
|
</div>
|
|
51
51
|
|
|
52
52
|
<!-- Title -->
|
|
@@ -56,8 +56,8 @@ const toggleExpanded = () => {
|
|
|
56
56
|
</div>
|
|
57
57
|
|
|
58
58
|
<!-- Toggle icon -->
|
|
59
|
-
<div class="flex size-8 shrink-0 items-center justify-center rounded border border-border bg-card transition-[rotate,border-color,background-color] duration-[420ms] ease-spring group-data-[state=open]/accordion:rotate-180 group-data-[state=open]/accordion:border-primary/50 group-data-[state=open]/accordion:bg-primary/10">
|
|
60
|
-
<HugeiconsIcon :icon="ArrowDown01Icon" class="size-4 text-muted-foreground transition-colors duration-200 group-data-[state=open]/accordion:text-primary" />
|
|
59
|
+
<div class="flex size-8 shrink-0 items-center justify-center rounded border border-border bg-card transition-[rotate,border-color,background-color] duration-[420ms] ease-spring group-hover/trigger:border-primary/50 group-data-[state=open]/accordion:rotate-180 group-data-[state=open]/accordion:border-primary/50 group-data-[state=open]/accordion:bg-primary/10">
|
|
60
|
+
<HugeiconsIcon :icon="ArrowDown01Icon" class="size-4 text-muted-foreground transition-colors duration-200 group-hover/trigger:text-primary group-data-[state=open]/accordion:text-primary" />
|
|
61
61
|
</div>
|
|
62
62
|
</button>
|
|
63
63
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { HTMLAttributes } from 'vue';
|
|
3
|
-
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
3
|
+
import { computed, getCurrentInstance, onBeforeMount, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
4
4
|
import { cn } from '../../../lib/utils';
|
|
5
5
|
|
|
6
6
|
// Props
|
|
@@ -22,8 +22,12 @@ const props = withDefaults(defineProps<{
|
|
|
22
22
|
animateOnMount: true
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
// The server always renders the final value (search engines, link previews and the first paint never see a 0), the count-up only runs for components created in the browser
|
|
26
|
+
const isServer = typeof window === 'undefined';
|
|
27
|
+
const displayValue = ref(props.animateOnMount && !isServer ? 0 : props.value);
|
|
26
28
|
const direction = ref<'up' | 'down' | null>(null);
|
|
29
|
+
const instance = getCurrentInstance();
|
|
30
|
+
let isHydrated = false;
|
|
27
31
|
let frame: number | null = null;
|
|
28
32
|
|
|
29
33
|
// Format the displayed value
|
|
@@ -34,6 +38,9 @@ const formatter = computed(() => {
|
|
|
34
38
|
});
|
|
35
39
|
const formattedValue = computed(() => `${props.prefix}${formatter.value.format(displayValue.value)}${props.suffix}`);
|
|
36
40
|
|
|
41
|
+
// Final value announced to screen readers from visually hidden text (the animated digits are hidden from them, and aria-label is not allowed on a span without a role)
|
|
42
|
+
const accessibleValue = computed(() => `${props.prefix}${formatter.value.format(props.value)}${props.suffix}`);
|
|
43
|
+
|
|
37
44
|
// Decelerating curve so the count settles gently on the final value
|
|
38
45
|
const easeOutExpo = (progress: number) => progress === 1 ? 1 : 1 - Math.pow(2, -10 * progress);
|
|
39
46
|
|
|
@@ -70,9 +77,15 @@ const animateTo = (target: number) => {
|
|
|
70
77
|
// Re-animate whenever the value changes
|
|
71
78
|
watch(() => props.value, value => animateTo(value));
|
|
72
79
|
|
|
73
|
-
//
|
|
80
|
+
// When hydrating server HTML (the element already exists before the first render), keep the final value the server rendered, so there is no mismatch and no flash to 0
|
|
81
|
+
onBeforeMount(() => {
|
|
82
|
+
isHydrated = !!instance?.vnode.el;
|
|
83
|
+
if (isHydrated) displayValue.value = props.value;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Count up on mount (only for components created in the browser, server-rendered ones already show the final value)
|
|
74
87
|
onMounted(() => {
|
|
75
|
-
if (props.animateOnMount) animateTo(props.value);
|
|
88
|
+
if (props.animateOnMount && !isHydrated) animateTo(props.value);
|
|
76
89
|
});
|
|
77
90
|
|
|
78
91
|
// Stop any pending frame
|
|
@@ -82,7 +95,8 @@ onBeforeUnmount(() => {
|
|
|
82
95
|
</script>
|
|
83
96
|
|
|
84
97
|
<template>
|
|
85
|
-
<span data-slot="animated-number" :data-direction="direction ?? undefined" :
|
|
98
|
+
<span data-slot="animated-number" :data-direction="direction ?? undefined" :class="cn('inline-block tabular-nums', props.class)">
|
|
86
99
|
<span aria-hidden="true">{{ formattedValue }}</span>
|
|
100
|
+
<span class="sr-only">{{ accessibleValue }}</span>
|
|
87
101
|
</span>
|
|
88
102
|
</template>
|
|
@@ -14,7 +14,7 @@ const { t } = useI18n();
|
|
|
14
14
|
</script>
|
|
15
15
|
|
|
16
16
|
<template>
|
|
17
|
-
<span data-slot="breadcrumb-ellipsis" role="presentation" aria-hidden="true" :class="cn('flex size-
|
|
17
|
+
<span data-slot="breadcrumb-ellipsis" role="presentation" aria-hidden="true" :class="cn('flex size-6 items-center justify-center rounded-sm transition-colors duration-150 hover:bg-accent hover:text-primary', props.class)">
|
|
18
18
|
<!-- Default slot content -->
|
|
19
19
|
<slot>
|
|
20
20
|
<HugeiconsIcon :icon="MoreHorizontalIcon" class="size-4" />
|
|
@@ -11,7 +11,7 @@ const props = withDefaults(defineProps<PrimitiveProps & { class?: HTMLAttributes
|
|
|
11
11
|
</script>
|
|
12
12
|
|
|
13
13
|
<template>
|
|
14
|
-
<Primitive data-slot="breadcrumb-link" :as="as" :as-child="asChild" :class="cn('text-muted-foreground hover:
|
|
14
|
+
<Primitive data-slot="breadcrumb-link" :as="as" :as-child="asChild" :class="cn('rounded-sm bg-[linear-gradient(currentColor,currentColor)] bg-[length:0%_1px] bg-left-bottom bg-no-repeat text-muted-foreground outline-none transition-[color,background-size] duration-300 ease-out-expo hover:bg-[length:100%_1px] hover:text-primary focus-visible:ring-[3px] focus-visible:ring-ring/45', props.class)">
|
|
15
15
|
<slot />
|
|
16
16
|
</Primitive>
|
|
17
17
|
</template>
|
|
@@ -9,7 +9,7 @@ const props = defineProps<{
|
|
|
9
9
|
</script>
|
|
10
10
|
|
|
11
11
|
<template>
|
|
12
|
-
<ol data-slot="breadcrumb-list" :class="cn('
|
|
12
|
+
<ol data-slot="breadcrumb-list" :class="cn('flex w-fit max-w-full flex-wrap items-center gap-1.5 rounded border border-border bg-card px-3 py-1.5 text-xs wrap-break-word text-muted-foreground shadow-elevated-sm sm:gap-2 sm:text-sm', props.class)">
|
|
13
13
|
<slot />
|
|
14
14
|
</ol>
|
|
15
15
|
</template>
|
|
@@ -9,7 +9,7 @@ const props = defineProps<{
|
|
|
9
9
|
</script>
|
|
10
10
|
|
|
11
11
|
<template>
|
|
12
|
-
<span data-slot="breadcrumb-page" role="link" aria-disabled="true" aria-current="page" :class="cn('
|
|
12
|
+
<span data-slot="breadcrumb-page" role="link" aria-disabled="true" aria-current="page" :class="cn('min-w-0 font-semibold text-foreground', props.class)">
|
|
13
13
|
<slot />
|
|
14
14
|
</span>
|
|
15
15
|
</template>
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { HTMLAttributes } from 'vue';
|
|
3
|
-
import { ArrowRight01Icon } from '@hugeicons/core-free-icons';
|
|
4
|
-
import { HugeiconsIcon } from '@hugeicons/vue';
|
|
5
3
|
import { cn } from '../../../lib/utils';
|
|
6
4
|
|
|
7
5
|
// Props
|
|
@@ -11,9 +9,7 @@ const props = defineProps<{
|
|
|
11
9
|
</script>
|
|
12
10
|
|
|
13
11
|
<template>
|
|
14
|
-
<li data-slot="breadcrumb-separator" role="presentation" aria-hidden="true" :class="cn('[&>svg]:size-3.5', props.class)">
|
|
15
|
-
<slot>
|
|
16
|
-
<HugeiconsIcon :icon="ArrowRight01Icon" class="size-3.5" />
|
|
17
|
-
</slot>
|
|
12
|
+
<li data-slot="breadcrumb-separator" role="presentation" aria-hidden="true" :class="cn('font-semibold text-primary/50 select-none [&>svg]:size-3.5', props.class)">
|
|
13
|
+
<slot>/</slot>
|
|
18
14
|
</li>
|
|
19
15
|
</template>
|
|
@@ -44,7 +44,7 @@ const handlePointerMove = (event: PointerEvent) => {
|
|
|
44
44
|
<div ref="cardRef" data-slot="card" :data-interactive="props.interactive ? '' : undefined" :class="cn(
|
|
45
45
|
'relative flex flex-col gap-4 sm:gap-6! rounded py-6 transition-[border-color,box-shadow,background-color,translate] duration-300 ease-out-expo',
|
|
46
46
|
colorClasses,
|
|
47
|
-
props.interactive && 'cursor-pointer hover:-translate-y-0.5 hover:border-
|
|
47
|
+
props.interactive && 'cursor-pointer hover:-translate-y-0.5 hover:border-primary/25 hover:shadow-elevated-lg active:translate-y-0 active:duration-100 before:pointer-events-none before:absolute before:inset-0 before:rounded-[inherit] before:opacity-0 before:transition-opacity before:duration-300 before:bg-[radial-gradient(360px_circle_at_var(--uv-spot-x,50%)_var(--uv-spot-y,50%),color-mix(in_oklab,var(--primary)_9%,transparent),transparent_70%)] hover:before:opacity-100',
|
|
48
48
|
props.class,
|
|
49
49
|
)" @pointermove="handlePointerMove">
|
|
50
50
|
<slot />
|
|
@@ -67,8 +67,8 @@ onUnmounted(() => {
|
|
|
67
67
|
|
|
68
68
|
<template>
|
|
69
69
|
<ListboxItem v-if="isRender" v-bind="forwarded" :id="id" ref="itemRef" data-slot="command-item" :class="cn(
|
|
70
|
-
'relative flex cursor-pointer items-center gap-3 rounded px-3 py-2 text-left text-sm font-semibold text-muted-foreground outline-hidden transition-colors duration-150 select-none data-highlighted:bg-accent data-highlighted:text-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
|
|
71
|
-
'before:absolute before:inset-y-1.5 before:left-0 before:w-0.5 before:scale-y-0 before:rounded-full before:bg-primary before:transition-transform before:duration-300 before:ease-spring data-highlighted:before:scale-y-100',
|
|
70
|
+
'relative flex cursor-pointer items-center gap-3 rounded px-3 py-2 text-left text-sm font-semibold text-muted-foreground outline-hidden transition-colors duration-150 select-none data-highlighted:bg-accent data-highlighted:text-foreground data-[state=checked]:bg-accent data-[state=checked]:text-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
|
|
71
|
+
'before:absolute before:inset-y-1.5 before:left-0 before:w-0.5 before:scale-y-0 before:rounded-full before:bg-primary before:transition-transform before:duration-300 before:ease-spring data-highlighted:before:scale-y-100 data-[state=checked]:before:scale-y-100',
|
|
72
72
|
'[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4 [&_svg:not([class*=\'text-\'])]:text-current',
|
|
73
73
|
props.class,
|
|
74
74
|
)" @select="
|
package/src/i18n/de.json
CHANGED
|
@@ -92,7 +92,9 @@
|
|
|
92
92
|
"x": "Auf X teilen",
|
|
93
93
|
"whatsapp": "Auf WhatsApp teilen",
|
|
94
94
|
"linkedin": "Auf LinkedIn teilen",
|
|
95
|
-
"telegram": "Auf Telegram teilen"
|
|
95
|
+
"telegram": "Auf Telegram teilen",
|
|
96
|
+
"copy": "Link kopieren",
|
|
97
|
+
"copied": "Link kopiert"
|
|
96
98
|
}
|
|
97
99
|
},
|
|
98
100
|
"select": {
|
|
@@ -118,6 +120,13 @@
|
|
|
118
120
|
"toast": {
|
|
119
121
|
"region": "Benachrichtigungen"
|
|
120
122
|
},
|
|
123
|
+
"cookieConsent": {
|
|
124
|
+
"region": "Cookie-Einwilligung",
|
|
125
|
+
"message": "Wir verwenden optionale Cookies, um zu verstehen, wie die Website genutzt wird. Sie werden nur gesetzt, wenn du zustimmst.",
|
|
126
|
+
"policy": "Cookie-Richtlinie",
|
|
127
|
+
"accept": "Akzeptieren",
|
|
128
|
+
"decline": "Ablehnen"
|
|
129
|
+
},
|
|
121
130
|
"dataTable": {
|
|
122
131
|
"selectAll": "Alle Zeilen auswählen",
|
|
123
132
|
"selectRow": "Zeile auswählen"
|
package/src/i18n/en.json
CHANGED
|
@@ -92,7 +92,9 @@
|
|
|
92
92
|
"x": "Share on X",
|
|
93
93
|
"whatsapp": "Share on WhatsApp",
|
|
94
94
|
"linkedin": "Share on LinkedIn",
|
|
95
|
-
"telegram": "Share on Telegram"
|
|
95
|
+
"telegram": "Share on Telegram",
|
|
96
|
+
"copy": "Copy link",
|
|
97
|
+
"copied": "Link copied"
|
|
96
98
|
}
|
|
97
99
|
},
|
|
98
100
|
"select": {
|
|
@@ -118,6 +120,13 @@
|
|
|
118
120
|
"toast": {
|
|
119
121
|
"region": "Notifications"
|
|
120
122
|
},
|
|
123
|
+
"cookieConsent": {
|
|
124
|
+
"region": "Cookie consent",
|
|
125
|
+
"message": "We use optional cookies to understand how the site is used. They are only set if you accept.",
|
|
126
|
+
"policy": "Cookie policy",
|
|
127
|
+
"accept": "Accept",
|
|
128
|
+
"decline": "Decline"
|
|
129
|
+
},
|
|
121
130
|
"dataTable": {
|
|
122
131
|
"selectAll": "Select all rows",
|
|
123
132
|
"selectRow": "Select row"
|
package/src/i18n/es.json
CHANGED
|
@@ -92,7 +92,9 @@
|
|
|
92
92
|
"x": "Compartir en X",
|
|
93
93
|
"whatsapp": "Compartir en WhatsApp",
|
|
94
94
|
"linkedin": "Compartir en LinkedIn",
|
|
95
|
-
"telegram": "Compartir en Telegram"
|
|
95
|
+
"telegram": "Compartir en Telegram",
|
|
96
|
+
"copy": "Copiar enlace",
|
|
97
|
+
"copied": "Enlace copiado"
|
|
96
98
|
}
|
|
97
99
|
},
|
|
98
100
|
"select": {
|
|
@@ -118,6 +120,13 @@
|
|
|
118
120
|
"toast": {
|
|
119
121
|
"region": "Notificaciones"
|
|
120
122
|
},
|
|
123
|
+
"cookieConsent": {
|
|
124
|
+
"region": "Consentimiento de cookies",
|
|
125
|
+
"message": "Usamos cookies opcionales para entender cómo se usa el sitio. Solo se instalan si aceptas.",
|
|
126
|
+
"policy": "Política de cookies",
|
|
127
|
+
"accept": "Aceptar",
|
|
128
|
+
"decline": "Rechazar"
|
|
129
|
+
},
|
|
121
130
|
"dataTable": {
|
|
122
131
|
"selectAll": "Seleccionar todas las filas",
|
|
123
132
|
"selectRow": "Seleccionar fila"
|
package/src/i18n/fr.json
CHANGED
|
@@ -92,7 +92,9 @@
|
|
|
92
92
|
"x": "Partager sur X",
|
|
93
93
|
"whatsapp": "Partager sur WhatsApp",
|
|
94
94
|
"linkedin": "Partager sur LinkedIn",
|
|
95
|
-
"telegram": "Partager sur Telegram"
|
|
95
|
+
"telegram": "Partager sur Telegram",
|
|
96
|
+
"copy": "Copier le lien",
|
|
97
|
+
"copied": "Lien copié"
|
|
96
98
|
}
|
|
97
99
|
},
|
|
98
100
|
"select": {
|
|
@@ -118,6 +120,13 @@
|
|
|
118
120
|
"toast": {
|
|
119
121
|
"region": "Notifications"
|
|
120
122
|
},
|
|
123
|
+
"cookieConsent": {
|
|
124
|
+
"region": "Consentement aux cookies",
|
|
125
|
+
"message": "Nous utilisons des cookies facultatifs pour comprendre comment le site est utilisé. Ils ne sont déposés que si vous acceptez.",
|
|
126
|
+
"policy": "Politique relative aux cookies",
|
|
127
|
+
"accept": "Accepter",
|
|
128
|
+
"decline": "Refuser"
|
|
129
|
+
},
|
|
121
130
|
"dataTable": {
|
|
122
131
|
"selectAll": "Sélectionner toutes les lignes",
|
|
123
132
|
"selectRow": "Sélectionner la ligne"
|
package/src/i18n/it.json
CHANGED
|
@@ -92,7 +92,9 @@
|
|
|
92
92
|
"x": "Condividi su X",
|
|
93
93
|
"whatsapp": "Condividi su WhatsApp",
|
|
94
94
|
"linkedin": "Condividi su LinkedIn",
|
|
95
|
-
"telegram": "Condividi su Telegram"
|
|
95
|
+
"telegram": "Condividi su Telegram",
|
|
96
|
+
"copy": "Copia link",
|
|
97
|
+
"copied": "Link copiato"
|
|
96
98
|
}
|
|
97
99
|
},
|
|
98
100
|
"select": {
|
|
@@ -118,6 +120,13 @@
|
|
|
118
120
|
"toast": {
|
|
119
121
|
"region": "Notifiche"
|
|
120
122
|
},
|
|
123
|
+
"cookieConsent": {
|
|
124
|
+
"region": "Consenso ai cookie",
|
|
125
|
+
"message": "Usiamo cookie facoltativi per capire come viene usato il sito. Vengono impostati solo se accetti.",
|
|
126
|
+
"policy": "Politica sui cookie",
|
|
127
|
+
"accept": "Accetta",
|
|
128
|
+
"decline": "Rifiuta"
|
|
129
|
+
},
|
|
121
130
|
"dataTable": {
|
|
122
131
|
"selectAll": "Seleziona tutte le righe",
|
|
123
132
|
"selectRow": "Seleziona riga"
|
package/src/i18n/ja.json
CHANGED
|
@@ -92,7 +92,9 @@
|
|
|
92
92
|
"x": "X で共有",
|
|
93
93
|
"whatsapp": "WhatsApp で共有",
|
|
94
94
|
"linkedin": "LinkedIn で共有",
|
|
95
|
-
"telegram": "Telegram で共有"
|
|
95
|
+
"telegram": "Telegram で共有",
|
|
96
|
+
"copy": "リンクをコピー",
|
|
97
|
+
"copied": "リンクをコピーしました"
|
|
96
98
|
}
|
|
97
99
|
},
|
|
98
100
|
"select": {
|
|
@@ -118,6 +120,13 @@
|
|
|
118
120
|
"toast": {
|
|
119
121
|
"region": "通知"
|
|
120
122
|
},
|
|
123
|
+
"cookieConsent": {
|
|
124
|
+
"region": "Cookieの同意",
|
|
125
|
+
"message": "サイトの利用状況を把握するため、任意のCookieを使用しています。同意した場合にのみ設定されます。",
|
|
126
|
+
"policy": "Cookieポリシー",
|
|
127
|
+
"accept": "同意する",
|
|
128
|
+
"decline": "拒否する"
|
|
129
|
+
},
|
|
121
130
|
"dataTable": {
|
|
122
131
|
"selectAll": "すべての行を選択",
|
|
123
132
|
"selectRow": "行を選択"
|
package/src/i18n/pt.json
CHANGED
|
@@ -92,7 +92,9 @@
|
|
|
92
92
|
"x": "Partilhar no X",
|
|
93
93
|
"whatsapp": "Partilhar no WhatsApp",
|
|
94
94
|
"linkedin": "Partilhar no LinkedIn",
|
|
95
|
-
"telegram": "Partilhar no Telegram"
|
|
95
|
+
"telegram": "Partilhar no Telegram",
|
|
96
|
+
"copy": "Copiar link",
|
|
97
|
+
"copied": "Link copiado"
|
|
96
98
|
}
|
|
97
99
|
},
|
|
98
100
|
"select": {
|
|
@@ -118,6 +120,13 @@
|
|
|
118
120
|
"toast": {
|
|
119
121
|
"region": "Notificações"
|
|
120
122
|
},
|
|
123
|
+
"cookieConsent": {
|
|
124
|
+
"region": "Consentimento de cookies",
|
|
125
|
+
"message": "Usamos cookies opcionais para entender como o site é usado. Eles só são definidos se você aceitar.",
|
|
126
|
+
"policy": "Política de cookies",
|
|
127
|
+
"accept": "Aceitar",
|
|
128
|
+
"decline": "Recusar"
|
|
129
|
+
},
|
|
121
130
|
"dataTable": {
|
|
122
131
|
"selectAll": "Selecionar todas as linhas",
|
|
123
132
|
"selectRow": "Selecionar linha"
|
package/src/i18n/ru.json
CHANGED
|
@@ -92,7 +92,9 @@
|
|
|
92
92
|
"x": "Поделиться в X",
|
|
93
93
|
"whatsapp": "Поделиться в WhatsApp",
|
|
94
94
|
"linkedin": "Поделиться в LinkedIn",
|
|
95
|
-
"telegram": "Поделиться в Telegram"
|
|
95
|
+
"telegram": "Поделиться в Telegram",
|
|
96
|
+
"copy": "Копировать ссылку",
|
|
97
|
+
"copied": "Ссылка скопирована"
|
|
96
98
|
}
|
|
97
99
|
},
|
|
98
100
|
"select": {
|
|
@@ -118,6 +120,13 @@
|
|
|
118
120
|
"toast": {
|
|
119
121
|
"region": "Уведомления"
|
|
120
122
|
},
|
|
123
|
+
"cookieConsent": {
|
|
124
|
+
"region": "Согласие на cookie",
|
|
125
|
+
"message": "Мы используем необязательные файлы cookie, чтобы понимать, как используется сайт. Они устанавливаются только с вашего согласия.",
|
|
126
|
+
"policy": "Политика cookie",
|
|
127
|
+
"accept": "Принять",
|
|
128
|
+
"decline": "Отклонить"
|
|
129
|
+
},
|
|
121
130
|
"dataTable": {
|
|
122
131
|
"selectAll": "Выбрать все строки",
|
|
123
132
|
"selectRow": "Выбрать строку"
|
package/src/i18n/zh.json
CHANGED
|
@@ -92,7 +92,9 @@
|
|
|
92
92
|
"x": "分享到 X",
|
|
93
93
|
"whatsapp": "分享到 WhatsApp",
|
|
94
94
|
"linkedin": "分享到 LinkedIn",
|
|
95
|
-
"telegram": "分享到 Telegram"
|
|
95
|
+
"telegram": "分享到 Telegram",
|
|
96
|
+
"copy": "复制链接",
|
|
97
|
+
"copied": "链接已复制"
|
|
96
98
|
}
|
|
97
99
|
},
|
|
98
100
|
"select": {
|
|
@@ -118,6 +120,13 @@
|
|
|
118
120
|
"toast": {
|
|
119
121
|
"region": "通知"
|
|
120
122
|
},
|
|
123
|
+
"cookieConsent": {
|
|
124
|
+
"region": "Cookie 同意",
|
|
125
|
+
"message": "我们使用可选的 Cookie 来了解网站的使用情况。只有在您同意后才会设置。",
|
|
126
|
+
"policy": "Cookie 政策",
|
|
127
|
+
"accept": "接受",
|
|
128
|
+
"decline": "拒绝"
|
|
129
|
+
},
|
|
121
130
|
"dataTable": {
|
|
122
131
|
"selectAll": "全选所有行",
|
|
123
132
|
"selectRow": "选择行"
|
package/src/styles.css
CHANGED
|
@@ -167,6 +167,24 @@ html {
|
|
|
167
167
|
--animate-uv-ping-soft: uv-ping-soft 1.8s var(--ease-out-expo) infinite;
|
|
168
168
|
--animate-uv-indeterminate: uv-indeterminate 1.4s var(--ease-snappy) infinite;
|
|
169
169
|
--animate-uv-gradient: uv-gradient 6s ease infinite;
|
|
170
|
+
--animate-uv-word-in: uv-word-in 0.9s var(--ease-spring) both;
|
|
171
|
+
--animate-uv-grow-x: uv-grow-x 0.9s var(--ease-out-expo) both;
|
|
172
|
+
--animate-uv-ken-burns: uv-ken-burns 9s ease-out both;
|
|
173
|
+
|
|
174
|
+
@keyframes uv-word-in {
|
|
175
|
+
0% { opacity: 0; transform: translateY(0.45em); filter: blur(8px); }
|
|
176
|
+
100% { opacity: 1; transform: translateY(0); filter: blur(0); }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
@keyframes uv-grow-x {
|
|
180
|
+
0% { transform: scaleX(0); }
|
|
181
|
+
100% { transform: scaleX(1); }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
@keyframes uv-ken-burns {
|
|
185
|
+
0% { transform: scale(1.12); }
|
|
186
|
+
100% { transform: scale(1); }
|
|
187
|
+
}
|
|
170
188
|
|
|
171
189
|
@keyframes uv-shimmer {
|
|
172
190
|
0% { transform: translateX(-100%); }
|
|
@@ -419,6 +437,28 @@ body {
|
|
|
419
437
|
color: var(--text-primary-dark);
|
|
420
438
|
}
|
|
421
439
|
|
|
440
|
+
/*********************** AOS scroll animations ***********************/
|
|
441
|
+
|
|
442
|
+
/*
|
|
443
|
+
* "blur-up" animation for AOS (https://github.com/michalsnik/aos), used by the blog components through data-aos="blur-up".
|
|
444
|
+
* Elements are only hidden once AOS has initialized them (.aos-init), so apps without AOS always show the content.
|
|
445
|
+
* The repeated attribute selector outranks the global AOS easing and duration rules; data-aos-delay keeps working.
|
|
446
|
+
*/
|
|
447
|
+
[data-aos="blur-up"][data-aos="blur-up"][data-aos="blur-up"].aos-init {
|
|
448
|
+
opacity: 0;
|
|
449
|
+
transform: translate3d(0, 18px, 0);
|
|
450
|
+
filter: blur(6px);
|
|
451
|
+
transition-property: opacity, transform, filter;
|
|
452
|
+
transition-duration: 700ms, 900ms, 700ms;
|
|
453
|
+
transition-timing-function: var(--ease-out-expo), var(--ease-spring), var(--ease-out-expo);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
[data-aos="blur-up"][data-aos="blur-up"][data-aos="blur-up"].aos-init.aos-animate {
|
|
457
|
+
opacity: 1;
|
|
458
|
+
transform: none;
|
|
459
|
+
filter: none;
|
|
460
|
+
}
|
|
461
|
+
|
|
422
462
|
/*********************** Page transition animations ***********************/
|
|
423
463
|
|
|
424
464
|
.page-enter-active {
|