@brumaombra/ui-vintage 0.8.1 → 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 +4 -1
- package/package.json +2 -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/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/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 +3 -1
- package/src/i18n/en.json +3 -1
- package/src/i18n/es.json +3 -1
- package/src/i18n/fr.json +3 -1
- package/src/i18n/it.json +3 -1
- package/src/i18n/ja.json +3 -1
- package/src/i18n/pt.json +3 -1
- package/src/i18n/ru.json +3 -1
- package/src/i18n/zh.json +3 -1
- package/src/styles.css +40 -0
|
@@ -286,7 +286,7 @@ const openMobileToc = () => {
|
|
|
286
286
|
// Get classes for heading button
|
|
287
287
|
const getHeadingButtonClasses = (level: number, isActive: boolean) => {
|
|
288
288
|
// Base classes for all titles
|
|
289
|
-
const baseClasses = 'block w-full
|
|
289
|
+
const baseClasses = 'relative block w-full cursor-pointer rounded px-2.5 py-2 text-left outline-none transition-[background-color,color,padding] duration-200 hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/45 md:px-3 before:absolute before:inset-y-1.5 before:left-0 before:w-[3px] before:origin-center before:scale-y-0 before:rounded-full before:bg-primary before:transition-transform before:duration-300 before:ease-spring';
|
|
290
290
|
|
|
291
291
|
// Add classes based on heading level
|
|
292
292
|
let levelClasses = '';
|
|
@@ -297,7 +297,7 @@ const getHeadingButtonClasses = (level: number, isActive: boolean) => {
|
|
|
297
297
|
}
|
|
298
298
|
|
|
299
299
|
// Add classes if the heading is active
|
|
300
|
-
const activeClasses = isActive ? 'bg-accent
|
|
300
|
+
const activeClasses = isActive ? 'bg-accent text-foreground! before:scale-y-100' : '';
|
|
301
301
|
|
|
302
302
|
// Return combined classes
|
|
303
303
|
return [baseClasses, levelClasses, activeClasses];
|
|
@@ -354,20 +354,20 @@ onUnmounted(() => {
|
|
|
354
354
|
<Teleport to="body">
|
|
355
355
|
<!-- Mobile persistent control -->
|
|
356
356
|
<Transition name="toc-tab">
|
|
357
|
-
<button v-if="!isDesktop && isDocked" ref="mobileTriggerRef" type="button" class="fixed top-4/5 left-0 z-20 flex h-16 w-
|
|
358
|
-
<HugeiconsIcon :icon="ArrowRight01Icon" class="size-
|
|
357
|
+
<button v-if="!isDesktop && isDocked" ref="mobileTriggerRef" type="button" class="group/toc-tab fixed top-4/5 left-0 z-20 flex h-16 w-7 -translate-y-1/2 items-center justify-center rounded-r border border-l-0 border-border bg-card text-primary shadow-elevated-sm transition-[width] duration-300 ease-out-expo hover:w-8 focus-visible:z-30 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/45" :aria-label="t('uiVintage.blog.openTableOfContents')" :aria-expanded="isMobileOpen" aria-haspopup="dialog" aria-controls="toc-mobile-overlay" @click="openMobileToc">
|
|
358
|
+
<HugeiconsIcon :icon="ArrowRight01Icon" class="size-3.5 transition-[translate] duration-300 ease-spring group-hover/toc-tab:translate-x-0.5" aria-hidden="true" />
|
|
359
359
|
</button>
|
|
360
360
|
</Transition>
|
|
361
361
|
|
|
362
362
|
<!-- Mobile full-screen overlay -->
|
|
363
363
|
<Sheet :open="isMobileOpen" @update:open="handleMobileOpenChange">
|
|
364
|
-
<SheetContent id="toc-mobile-overlay" side="left" class="flex h-full w-full max-w-none flex-col gap-0 border-0 bg-
|
|
365
|
-
<SheetHeader class="flex-row items-center justify-between gap-4 border-b border-
|
|
364
|
+
<SheetContent id="toc-mobile-overlay" side="left" class="flex h-full w-full max-w-none flex-col gap-0 border-0 bg-card p-0 data-[state=open]:[animation-duration:320ms] data-[state=open]:[animation-timing-function:var(--ease-out-expo)] sm:max-w-none">
|
|
365
|
+
<SheetHeader class="flex-row items-center justify-between gap-4 border-b border-border p-5">
|
|
366
366
|
<!-- Mobile table of contents header -->
|
|
367
367
|
<div class="flex min-w-0 items-center gap-3">
|
|
368
368
|
<!-- Mobile table of contents icon -->
|
|
369
|
-
<div class="flex
|
|
370
|
-
<HugeiconsIcon :icon="Bookmark01Icon" class="size-4 text-
|
|
369
|
+
<div class="flex size-10 shrink-0 items-center justify-center rounded border border-primary/30 bg-primary/10">
|
|
370
|
+
<HugeiconsIcon :icon="Bookmark01Icon" class="size-4 text-primary" />
|
|
371
371
|
</div>
|
|
372
372
|
|
|
373
373
|
<!-- Mobile table of contents title and description -->
|
|
@@ -404,14 +404,18 @@ onUnmounted(() => {
|
|
|
404
404
|
</template>
|
|
405
405
|
|
|
406
406
|
<style scoped>
|
|
407
|
-
.toc-tab-enter-active
|
|
407
|
+
.toc-tab-enter-active {
|
|
408
|
+
transition: opacity 0.3s var(--ease-out-expo), transform 0.32s var(--ease-out-expo);
|
|
409
|
+
}
|
|
410
|
+
|
|
408
411
|
.toc-tab-leave-active {
|
|
409
|
-
transition: opacity 0.
|
|
412
|
+
transition: opacity 0.15s var(--ease-snappy), transform 0.15s var(--ease-snappy);
|
|
410
413
|
}
|
|
411
414
|
|
|
412
415
|
.toc-tab-enter-from,
|
|
413
416
|
.toc-tab-leave-to {
|
|
414
417
|
opacity: 0;
|
|
418
|
+
transform: translateX(-100%);
|
|
415
419
|
}
|
|
416
420
|
|
|
417
421
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -14,21 +14,33 @@ const props = withDefaults(defineProps<{
|
|
|
14
14
|
|
|
15
15
|
// Icon container classes
|
|
16
16
|
const getIconContainerClasses = () => {
|
|
17
|
-
|
|
17
|
+
const toneClasses = {
|
|
18
|
+
checkmark: 'border-green-200 bg-green-50 dark:border-green-900/50 dark:bg-green-950/30',
|
|
19
|
+
cross: 'border-red-200 bg-red-50 dark:border-red-900/50 dark:bg-red-950/30',
|
|
20
|
+
numbered: 'border-primary/30 bg-primary/10',
|
|
21
|
+
circle: 'border-primary/30 bg-primary/10'
|
|
22
|
+
};
|
|
23
|
+
return ['flex items-center justify-center size-8 border rounded mr-4 shrink-0', toneClasses[props.variant]];
|
|
18
24
|
};
|
|
19
25
|
|
|
20
26
|
// Icon color classes
|
|
21
27
|
const getIconClasses = () => {
|
|
22
|
-
|
|
28
|
+
const toneClasses = {
|
|
29
|
+
checkmark: 'text-green-600 dark:text-green-400',
|
|
30
|
+
cross: 'text-red-600 dark:text-red-400',
|
|
31
|
+
numbered: 'text-primary',
|
|
32
|
+
circle: 'text-primary'
|
|
33
|
+
};
|
|
34
|
+
return ['text-xs size-4', toneClasses[props.variant]];
|
|
23
35
|
};
|
|
24
36
|
</script>
|
|
25
37
|
|
|
26
38
|
<template>
|
|
27
39
|
<div class="not-prose my-6 sm:my-8">
|
|
28
|
-
<Card>
|
|
40
|
+
<Card data-aos="blur-up">
|
|
29
41
|
<CardContent class="gap-0">
|
|
30
42
|
<ul class="text-xs md:text-sm p-0! space-y-6!">
|
|
31
|
-
<li v-for="(item, index) in props.items" :key="index" class="flex items-center">
|
|
43
|
+
<li v-for="(item, index) in props.items" :key="index" data-aos="blur-up" :data-aos-delay="100 + index * 80" class="flex items-center">
|
|
32
44
|
<!-- Circle -->
|
|
33
45
|
<div v-if="props.variant === 'circle'" :class="getIconContainerClasses()">
|
|
34
46
|
<HugeiconsIcon :icon="CircleSmallIcon" :class="getIconClasses()" />
|
|
@@ -16,7 +16,7 @@ const props = withDefaults(defineProps<{
|
|
|
16
16
|
// Header cell classes
|
|
17
17
|
const getHeaderClasses = (colIndex: number) => {
|
|
18
18
|
const baseClasses = 'text-left font-semibold text-foreground';
|
|
19
|
-
const highlightClass = props.highlightCol === colIndex ? 'bg-
|
|
19
|
+
const highlightClass = props.highlightCol === colIndex ? 'bg-primary/10 text-primary' : '';
|
|
20
20
|
return [baseClasses, highlightClass];
|
|
21
21
|
};
|
|
22
22
|
|
|
@@ -24,14 +24,14 @@ const getHeaderClasses = (colIndex: number) => {
|
|
|
24
24
|
const getCellClasses = (colIndex: number) => {
|
|
25
25
|
const baseClasses = 'align-top text-foreground leading-relaxed whitespace-normal';
|
|
26
26
|
const firstColClass = colIndex === 0 ? 'font-semibold text-muted-foreground' : '';
|
|
27
|
-
const highlightClass = props.highlightCol === colIndex ? 'bg-
|
|
27
|
+
const highlightClass = props.highlightCol === colIndex ? 'bg-primary/5 font-semibold' : '';
|
|
28
28
|
return [baseClasses, firstColClass, highlightClass];
|
|
29
29
|
};
|
|
30
30
|
</script>
|
|
31
31
|
|
|
32
32
|
<template>
|
|
33
33
|
<div class="not-prose my-6 sm:my-8">
|
|
34
|
-
<Card>
|
|
34
|
+
<Card data-aos="blur-up">
|
|
35
35
|
<CardContent>
|
|
36
36
|
<div class="w-full overflow-x-auto">
|
|
37
37
|
<Table class="w-full text-sm text-left">
|
|
@@ -46,7 +46,7 @@ const getCellClasses = (colIndex: number) => {
|
|
|
46
46
|
|
|
47
47
|
<!-- Table body -->
|
|
48
48
|
<TableBody>
|
|
49
|
-
<TableRow v-for="(row, rowIndex) in props.rows" :key="rowIndex" class="text-muted-foreground hover:bg-
|
|
49
|
+
<TableRow v-for="(row, rowIndex) in props.rows" :key="rowIndex" class="text-muted-foreground transition-colors duration-150 hover:bg-surface/70">
|
|
50
50
|
<TableCell v-for="(cell, colIndex) in row" :key="colIndex" :class="getCellClasses(colIndex)">
|
|
51
51
|
{{ cell }}
|
|
52
52
|
</TableCell>
|
|
@@ -76,11 +76,11 @@ const copyCode = async () => {
|
|
|
76
76
|
|
|
77
77
|
<template>
|
|
78
78
|
<div class="not-prose my-6 sm:my-8">
|
|
79
|
-
<Card class="gap-0! sm:gap-0! overflow-hidden p-0! text-foreground">
|
|
79
|
+
<Card data-aos="blur-up" class="gap-0! sm:gap-0! overflow-hidden p-0! text-foreground">
|
|
80
80
|
<!-- Code header -->
|
|
81
|
-
<div class="flex items-center justify-between gap-3 border-b border-
|
|
81
|
+
<div class="flex items-center justify-between gap-3 border-b border-border bg-card px-3 py-2 sm:px-4">
|
|
82
82
|
<div class="flex min-w-0 items-center gap-2">
|
|
83
|
-
<HugeiconsIcon :icon="CodeIcon" class="size-4 shrink-0 text-
|
|
83
|
+
<HugeiconsIcon :icon="CodeIcon" class="size-4 shrink-0 text-primary" />
|
|
84
84
|
<span class="truncate text-xs font-semibold text-muted-foreground">{{ resolvedTitle }}</span>
|
|
85
85
|
</div>
|
|
86
86
|
|
|
@@ -88,7 +88,7 @@ const copyCode = async () => {
|
|
|
88
88
|
<Button v-if="props.copyable"
|
|
89
89
|
variant="ghost"
|
|
90
90
|
size="icon"
|
|
91
|
-
class="size-7 text-
|
|
91
|
+
class="size-7 text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
92
92
|
:aria-label="t('uiVintage.code.copy')"
|
|
93
93
|
:title="t('uiVintage.code.copy')"
|
|
94
94
|
:disabled="!displayedCode"
|
|
@@ -99,7 +99,7 @@ const copyCode = async () => {
|
|
|
99
99
|
|
|
100
100
|
<!-- Code content -->
|
|
101
101
|
<div v-if="highlightedCode" class="code-block-content overflow-x-auto bg-surface" v-html="highlightedCode" />
|
|
102
|
-
<pre v-else class="code-block-content overflow-x-auto bg-
|
|
102
|
+
<pre v-else class="code-block-content overflow-x-auto bg-surface px-4 py-3 text-xs leading-6 sm:px-5 sm:py-4 sm:text-sm"><code>{{ displayedCode }}</code></pre>
|
|
103
103
|
</Card>
|
|
104
104
|
</div>
|
|
105
105
|
</template>
|
|
@@ -42,7 +42,7 @@ const isVertical = () => {
|
|
|
42
42
|
|
|
43
43
|
<template>
|
|
44
44
|
<div class="not-prose my-6 sm:my-8">
|
|
45
|
-
<Card>
|
|
45
|
+
<Card data-aos="blur-up">
|
|
46
46
|
<!-- Card header -->
|
|
47
47
|
<CardHeader v-if="props.title || props.description">
|
|
48
48
|
<!-- Card title -->
|
|
@@ -62,12 +62,12 @@ const isVertical = () => {
|
|
|
62
62
|
<!-- Flow items -->
|
|
63
63
|
<template v-for="(item, index) in props.items" :key="`${getItemLabel(item)}-${index}`">
|
|
64
64
|
<!-- Flow item -->
|
|
65
|
-
<Card class="flex-1 overflow-hidden md:flex md:items-center">
|
|
65
|
+
<Card data-aos="blur-up" :data-aos-delay="100 + index * 90" class="flex-1 overflow-hidden transition-[border-color,translate] duration-300 ease-spring hover:-translate-y-0.5 hover:border-primary/40 md:flex md:items-center">
|
|
66
66
|
<CardContent class="w-full gap-3 md:justify-center">
|
|
67
67
|
<!-- Flow item header -->
|
|
68
68
|
<div class="inline-flex items-center gap-3 text-xs font-semibold text-foreground">
|
|
69
69
|
<!-- Flow item number -->
|
|
70
|
-
<span class="inline-flex
|
|
70
|
+
<span class="inline-flex size-8 shrink-0 items-center justify-center rounded border border-primary/30 bg-primary/10 text-xs font-bold text-primary">
|
|
71
71
|
{{ index + 1 }}
|
|
72
72
|
</span>
|
|
73
73
|
|
|
@@ -83,7 +83,7 @@ const isVertical = () => {
|
|
|
83
83
|
</Card>
|
|
84
84
|
|
|
85
85
|
<!-- Flow item separator -->
|
|
86
|
-
<div v-if="index < props.items.length - 1" class="flex items-center justify-center text-
|
|
86
|
+
<div v-if="index < props.items.length - 1" data-aos="blur-up" :data-aos-delay="140 + index * 90" class="flex items-center justify-center text-primary">
|
|
87
87
|
<!-- Flow item separator for vertical orientation -->
|
|
88
88
|
<HugeiconsIcon v-if="isVertical()" :icon="ArrowDown01Icon" class="size-5 shrink-0" />
|
|
89
89
|
|
|
@@ -12,18 +12,18 @@ const props = withDefaults(defineProps<{
|
|
|
12
12
|
</script>
|
|
13
13
|
|
|
14
14
|
<template>
|
|
15
|
-
<div aria-hidden="true" class="not-prose my-
|
|
15
|
+
<div aria-hidden="true" data-aos="blur-up" class="not-prose my-8 sm:my-14">
|
|
16
16
|
<div class="flex items-center gap-3 sm:gap-4">
|
|
17
17
|
<!-- Left line -->
|
|
18
|
-
<div class="h-px flex-1 bg-linear-to-r from-transparent via-
|
|
18
|
+
<div class="h-px flex-1 bg-linear-to-r from-transparent via-border to-primary/50" />
|
|
19
19
|
|
|
20
20
|
<!-- Icon in the middle -->
|
|
21
|
-
<div class="flex
|
|
22
|
-
<HugeiconsIcon :icon="props.icon" class="size-4 text-
|
|
21
|
+
<div class="flex size-10 rotate-45 items-center justify-center rounded border border-primary/30 bg-card shadow-glow">
|
|
22
|
+
<HugeiconsIcon :icon="props.icon" class="size-4 -rotate-45 text-primary" />
|
|
23
23
|
</div>
|
|
24
24
|
|
|
25
25
|
<!-- Right line -->
|
|
26
|
-
<div class="h-px flex-1 bg-linear-to-l from-transparent via-
|
|
26
|
+
<div class="h-px flex-1 bg-linear-to-l from-transparent via-border to-primary/50" />
|
|
27
27
|
</div>
|
|
28
28
|
</div>
|
|
29
29
|
</template>
|
|
@@ -42,10 +42,16 @@ const copyCommands = async () => {
|
|
|
42
42
|
|
|
43
43
|
<template>
|
|
44
44
|
<div class="not-prose my-6 sm:my-8">
|
|
45
|
-
<Card class="gap-0! sm:gap-0! overflow-hidden p-0! text-foreground">
|
|
45
|
+
<Card data-aos="blur-up" class="gap-0! sm:gap-0! overflow-hidden p-0! text-foreground">
|
|
46
46
|
<!-- Terminal header -->
|
|
47
|
-
<div class="flex items-center justify-between gap-3 border-b border-
|
|
47
|
+
<div class="flex items-center justify-between gap-3 border-b border-border bg-card px-3 py-2 sm:px-4">
|
|
48
48
|
<div class="flex min-w-0 items-center gap-2">
|
|
49
|
+
<!-- Window dots -->
|
|
50
|
+
<span aria-hidden="true" class="mr-1 flex items-center gap-1.5">
|
|
51
|
+
<span class="size-2.5 rounded-full bg-red-400/80" />
|
|
52
|
+
<span class="size-2.5 rounded-full bg-amber-400/80" />
|
|
53
|
+
<span class="size-2.5 rounded-full bg-green-400/80" />
|
|
54
|
+
</span>
|
|
49
55
|
<HugeiconsIcon :icon="TerminalIcon" class="size-4 shrink-0 text-muted-foreground" />
|
|
50
56
|
<span class="truncate text-xs font-semibold text-muted-foreground">{{ resolvedTitle }}</span>
|
|
51
57
|
</div>
|
|
@@ -54,7 +60,7 @@ const copyCommands = async () => {
|
|
|
54
60
|
<Button v-if="props.copyable"
|
|
55
61
|
variant="ghost"
|
|
56
62
|
size="icon"
|
|
57
|
-
class="size-7 text-
|
|
63
|
+
class="size-7 text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
58
64
|
:aria-label="t('uiVintage.terminal.copy')"
|
|
59
65
|
:title="t('uiVintage.terminal.copy')"
|
|
60
66
|
:disabled="!commandText"
|
|
@@ -64,7 +70,7 @@ const copyCommands = async () => {
|
|
|
64
70
|
</div>
|
|
65
71
|
|
|
66
72
|
<!-- Terminal commands -->
|
|
67
|
-
<pre class="overflow-x-auto bg-
|
|
73
|
+
<pre class="overflow-x-auto bg-surface px-4 py-3 text-xs leading-6 sm:px-5 sm:py-4 sm:text-sm"><code><span v-for="(command, index) in props.commands" :key="`${command}-${index}`" class="flex min-w-max"><span class="mr-3 select-none text-primary" aria-hidden="true">{{ props.prompt }}</span><span>{{ command }}</span></span></code></pre>
|
|
68
74
|
</Card>
|
|
69
75
|
</div>
|
|
70
76
|
</template>
|
|
@@ -77,12 +77,15 @@ const getLinkProps = (link: LandingFooterLink) => {
|
|
|
77
77
|
|
|
78
78
|
<template>
|
|
79
79
|
<footer :class="cn('relative z-10 border-t border-border bg-card', props.class)">
|
|
80
|
+
<!-- Accent line -->
|
|
81
|
+
<div aria-hidden="true" class="absolute inset-x-0 -top-px h-px bg-linear-to-r from-transparent via-primary/60 to-transparent" />
|
|
82
|
+
|
|
80
83
|
<div :class="cn('mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8', props.containerClass)">
|
|
81
84
|
<div :class="cn('flex flex-col gap-8 md:flex-row md:items-start md:justify-between')">
|
|
82
85
|
<!-- Brand section -->
|
|
83
86
|
<div :class="cn('max-w-md flex-1', props.brandClass)">
|
|
84
87
|
<!-- App logo and name -->
|
|
85
|
-
<a :href="props.appLinkTo" class="mb-4 inline-flex items-center gap-2 text-lg font-semibold tracking-tight text-foreground transition-opacity hover:opacity-
|
|
88
|
+
<a :href="props.appLinkTo" class="mb-4 inline-flex items-center gap-2 text-lg font-semibold tracking-tight text-foreground transition-[opacity,scale] duration-300 ease-spring hover:scale-[1.02] hover:opacity-90">
|
|
86
89
|
<NuxtImg v-if="props.appLogo" :src="props.appLogo" :alt="`${props.appName} logo`" width="32" height="32" :sizes="{ 320: '32px', 1280: '32px' }" loading="lazy" decoding="async" :class="cn('size-8 shrink-0 object-contain', props.logoClass)" />
|
|
87
90
|
<span>{{ props.appName }}</span>
|
|
88
91
|
</a>
|
|
@@ -104,7 +107,8 @@ const getLinkProps = (link: LandingFooterLink) => {
|
|
|
104
107
|
<!-- Section links -->
|
|
105
108
|
<ul class="space-y-3 text-xs sm:text-sm">
|
|
106
109
|
<li v-for="link in props.sections[0]?.links" :key="link.id">
|
|
107
|
-
<component :is="props.linkComponent" v-bind="getLinkProps(link)" class="text-muted-foreground hover:
|
|
110
|
+
<component :is="props.linkComponent" v-bind="getLinkProps(link)" class="group/link inline-flex items-center gap-1.5 text-muted-foreground transition-colors duration-150 hover:text-primary">
|
|
111
|
+
<span class="h-px w-0 bg-primary transition-[width] duration-300 ease-spring group-hover/link:w-3" />
|
|
108
112
|
{{ link.label }}
|
|
109
113
|
</component>
|
|
110
114
|
</li>
|
|
@@ -123,7 +127,8 @@ const getLinkProps = (link: LandingFooterLink) => {
|
|
|
123
127
|
<!-- Section links -->
|
|
124
128
|
<ul class="space-y-3 text-xs sm:text-sm">
|
|
125
129
|
<li v-for="link in section.links" :key="link.id">
|
|
126
|
-
<component :is="props.linkComponent" v-bind="getLinkProps(link)" class="text-muted-foreground hover:
|
|
130
|
+
<component :is="props.linkComponent" v-bind="getLinkProps(link)" class="group/link inline-flex items-center gap-1.5 text-muted-foreground transition-colors duration-150 hover:text-primary">
|
|
131
|
+
<span class="h-px w-0 bg-primary transition-[width] duration-300 ease-spring group-hover/link:w-3" />
|
|
127
132
|
{{ link.label }}
|
|
128
133
|
</component>
|
|
129
134
|
</li>
|
|
@@ -142,7 +147,7 @@ const getLinkProps = (link: LandingFooterLink) => {
|
|
|
142
147
|
<!-- Author -->
|
|
143
148
|
<div v-if="props.authorName && props.authorLink" class="flex flex-wrap items-center justify-center gap-1.5 md:justify-end">
|
|
144
149
|
<span>{{ t('uiVintage.footer.madeWith') }}</span>
|
|
145
|
-
<HugeiconsIcon :icon="FavouriteIcon" class="size-4 shrink-0 text-red-
|
|
150
|
+
<HugeiconsIcon :icon="FavouriteIcon" class="size-4 shrink-0 text-red-600 transition-[scale] duration-300 ease-bounce hover:scale-125 dark:text-red-400" />
|
|
146
151
|
<span>{{ t('uiVintage.footer.by') }}</span>
|
|
147
152
|
<a :href="props.authorLink" target="_blank" rel="noopener noreferrer" class="font-semibold text-foreground hover:underline">
|
|
148
153
|
{{ props.authorName }}
|
|
@@ -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
|
|
|
@@ -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": {
|
package/src/i18n/en.json
CHANGED