@brumaombra/ui-vintage 0.7.9 → 0.7.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brumaombra/ui-vintage",
3
- "version": "0.7.9",
3
+ "version": "0.7.11",
4
4
  "description": "Source-published Nuxt UI runtime with shared theme tokens and built-in Nuxt integrations.",
5
5
  "private": false,
6
6
  "author": "Mauro Brambilla",
@@ -0,0 +1,110 @@
1
+ <script setup lang="ts">
2
+ // Props
3
+ const props = defineProps<{
4
+ value: Record<string, unknown>;
5
+ }>();
6
+ </script>
7
+
8
+ <template>
9
+ <div class="blog-content">
10
+ <ContentRenderer :value="props.value" />
11
+ </div>
12
+ </template>
13
+
14
+ <style scoped>
15
+ .blog-content {
16
+ --blog-content-text: var(--text-primary-light);
17
+ --blog-content-code-bg: var(--bg-selected-light);
18
+ color: var(--blog-content-text);
19
+ }
20
+
21
+ .dark .blog-content {
22
+ --blog-content-text: var(--text-primary-dark);
23
+ --blog-content-code-bg: var(--bg-selected-dark);
24
+ }
25
+
26
+ .blog-content :deep(h1:not(.not-prose):not(.not-prose *)),
27
+ .blog-content :deep(h2:not(.not-prose):not(.not-prose *)),
28
+ .blog-content :deep(h3:not(.not-prose):not(.not-prose *)) {
29
+ color: var(--blog-content-text);
30
+ }
31
+
32
+ .blog-content :deep(h1:not(.not-prose):not(.not-prose *)) {
33
+ font-size: 1.75rem;
34
+ font-weight: 700;
35
+ margin-bottom: 2rem;
36
+ }
37
+
38
+ .blog-content :deep(h2:not(.not-prose):not(.not-prose *)) {
39
+ margin-top: 2rem;
40
+ margin-bottom: 1.5rem;
41
+ }
42
+
43
+ .blog-content :deep(h2:not(.not-prose):not(.not-prose *) a:not(.not-prose):not(.not-prose *)) {
44
+ font-size: 1.375rem;
45
+ font-weight: 700;
46
+ line-height: 2rem;
47
+ }
48
+
49
+ .blog-content :deep(h3:not(.not-prose):not(.not-prose *)) {
50
+ margin-top: 2rem;
51
+ margin-bottom: 1rem;
52
+ }
53
+
54
+ .blog-content :deep(h3:not(.not-prose):not(.not-prose *) a:not(.not-prose):not(.not-prose *)) {
55
+ font-size: 1.125rem;
56
+ font-weight: 700;
57
+ line-height: 1.75rem;
58
+ }
59
+
60
+ .blog-content :deep(p:not(.not-prose):not(.not-prose *)),
61
+ .blog-content :deep(li:not(.not-prose):not(.not-prose *)),
62
+ .blog-content :deep(blockquote:not(.not-prose):not(.not-prose *)),
63
+ .blog-content :deep(td:not(.not-prose):not(.not-prose *)),
64
+ .blog-content :deep(th:not(.not-prose):not(.not-prose *)) {
65
+ color: var(--blog-content-text);
66
+ }
67
+
68
+ .blog-content :deep(p:not(.not-prose):not(.not-prose *)) {
69
+ font-size: 15px;
70
+ margin-top: 1rem;
71
+ margin-bottom: 1rem;
72
+ line-height: 1.75;
73
+ }
74
+
75
+ .blog-content :deep(p:not(.not-prose):not(.not-prose *) strong:not(.not-prose):not(.not-prose *)),
76
+ .blog-content :deep(blockquote:not(.not-prose):not(.not-prose *) strong:not(.not-prose):not(.not-prose *)) {
77
+ font-weight: 700;
78
+ }
79
+
80
+ .blog-content :deep(p:not(.not-prose):not(.not-prose *) a:not(.not-prose):not(.not-prose *)),
81
+ .blog-content :deep(li:not(.not-prose):not(.not-prose *) a:not(.not-prose):not(.not-prose *)),
82
+ .blog-content :deep(blockquote:not(.not-prose):not(.not-prose *) a:not(.not-prose):not(.not-prose *)) {
83
+ color: var(--blog-content-text);
84
+ text-decoration: none;
85
+ font-weight: 700;
86
+ }
87
+
88
+ .blog-content :deep(p:not(.not-prose):not(.not-prose *) a:not(.not-prose):not(.not-prose *):hover),
89
+ .blog-content :deep(li:not(.not-prose):not(.not-prose *) a:not(.not-prose):not(.not-prose *):hover),
90
+ .blog-content :deep(blockquote:not(.not-prose):not(.not-prose *) a:not(.not-prose):not(.not-prose *):hover) {
91
+ text-decoration: underline;
92
+ }
93
+
94
+ .blog-content :deep(p:not(.not-prose):not(.not-prose *) code:not(.not-prose):not(.not-prose *)),
95
+ .blog-content :deep(pre:not(.not-prose):not(.not-prose *) code:not(.not-prose):not(.not-prose *)) {
96
+ color: var(--blog-content-text);
97
+ background-color: var(--blog-content-code-bg);
98
+ }
99
+
100
+ @media (max-width: 640px) {
101
+ .blog-content :deep(p:not(.not-prose):not(.not-prose *)) {
102
+ font-size: 13px;
103
+ line-height: 1.75;
104
+ }
105
+
106
+ .blog-content :deep(h2:not(.not-prose):not(.not-prose *)) {
107
+ margin-bottom: 1rem;
108
+ }
109
+ }
110
+ </style>
@@ -40,7 +40,7 @@ const formattedModifiedDate = computed(() => formatDateLongItalyTimezone(props.d
40
40
  <div>
41
41
  <p class="text-sm text-(--text-secondary-light) dark:text-(--text-secondary-dark)">{{ t('uiVintage.blog.author') }}</p>
42
42
  <p class="font-bold text-(--text-primary-light) dark:text-(--text-primary-dark)">
43
- <NuxtLink :to="props.authorUrl" target="_blank" rel="noopener noreferrer" class="hover:text-(--text-secondary-light) dark:hover:text-(--text-secondary-dark) transition-colors">
43
+ <NuxtLink :to="props.authorUrl" target="_blank" rel="noopener noreferrer" class="hover:underline">
44
44
  {{ props.author }}
45
45
  </NuxtLink>
46
46
  </p>
@@ -0,0 +1,77 @@
1
+ <script setup lang="ts">
2
+ import { onMounted, onUnmounted, ref } from 'vue';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { cn } from '../../lib/utils';
5
+
6
+ // Props
7
+ const props = withDefaults(defineProps<{
8
+ class?: HTMLAttributes['class'];
9
+ ariaLabel?: string;
10
+ }>(), {
11
+ ariaLabel: 'Reading progress'
12
+ });
13
+
14
+ const progress = ref(0);
15
+ let animationFrame = 0;
16
+
17
+ // Update progress based on scroll position
18
+ const updateProgress = () => {
19
+ const documentHeight = document.documentElement.scrollHeight - window.innerHeight;
20
+ progress.value = documentHeight > 0 ? Math.min(100, Math.max(0, (window.scrollY / documentHeight) * 100)) : 100;
21
+ };
22
+
23
+ // Handle scroll event with requestAnimationFrame for performance
24
+ const handleScroll = () => {
25
+ cancelAnimationFrame(animationFrame);
26
+ animationFrame = requestAnimationFrame(updateProgress);
27
+ };
28
+
29
+ // On component mounted
30
+ onMounted(() => {
31
+ updateProgress();
32
+ window.addEventListener('scroll', handleScroll, { passive: true });
33
+ window.addEventListener('resize', updateProgress);
34
+ });
35
+
36
+ // On component unmounted
37
+ onUnmounted(() => {
38
+ cancelAnimationFrame(animationFrame);
39
+ window.removeEventListener('scroll', handleScroll);
40
+ window.removeEventListener('resize', updateProgress);
41
+ });
42
+ </script>
43
+
44
+ <template>
45
+ <div :class="cn('relative h-1 w-full border-t border-(--border-light) bg-(--bg-selected-light) dark:border-(--border-dark) dark:bg-(--bg-selected-dark) landing-navbar-progress', props.class)"
46
+ role="progressbar"
47
+ :aria-label="props.ariaLabel"
48
+ aria-valuemin="0"
49
+ aria-valuemax="100"
50
+ :aria-valuenow="Math.round(progress)">
51
+ <div class="h-full origin-left bg-primary transition-[width] duration-150 ease-out" :style="{ width: `${progress}%` }" />
52
+ </div>
53
+ </template>
54
+
55
+ <style scoped>
56
+ .landing-navbar-progress {
57
+ animation: landing-navbar-progress-fade linear both;
58
+ animation-range: 0 160px;
59
+ animation-timeline: scroll(root);
60
+ }
61
+
62
+ @keyframes landing-navbar-progress-fade {
63
+ from {
64
+ opacity: 0;
65
+ }
66
+
67
+ to {
68
+ opacity: 1;
69
+ }
70
+ }
71
+
72
+ @supports not (animation-timeline: scroll()) {
73
+ .landing-navbar-progress {
74
+ opacity: 1;
75
+ }
76
+ }
77
+ </style>
@@ -144,7 +144,7 @@ onUnmounted(() => {
144
144
  <button v-if="props.featuredPosts.length > 1"
145
145
  @click="prevSlide"
146
146
  :aria-label="t('uiVintage.blog.previousSlide')"
147
- class="absolute left-2 top-[calc(50%-4rem)] -translate-y-1/2 z-10 border border-(--border-light) dark:border-(--border-dark) bg-(--bg-card-light) dark:bg-(--bg-card-dark) px-3 py-3 rounded-none transition-colors duration-200 hover:border-(--border-hover-light) dark:hover:border-(--border-hover-dark) cursor-pointer">
147
+ class="absolute left-2 top-[calc(50%-4rem)] -translate-y-1/2 z-10 border border-(--border-light) dark:border-(--border-dark) bg-(--bg-card-light) dark:bg-(--bg-card-dark) px-3 py-3 rounded-[3px] transition-colors duration-200 hover:border-(--border-hover-light) dark:hover:border-(--border-hover-dark) cursor-pointer">
148
148
  <HugeiconsIcon :icon="ArrowLeft01Icon" class="size-4 text-(--text-secondary-light) dark:text-(--text-secondary-dark)" />
149
149
  </button>
150
150
 
@@ -152,7 +152,7 @@ onUnmounted(() => {
152
152
  <button v-if="props.featuredPosts.length > 1"
153
153
  @click="nextSlide"
154
154
  :aria-label="t('uiVintage.blog.nextSlide')"
155
- class="absolute right-2 top-[calc(50%-4rem)] -translate-y-1/2 z-10 border border-(--border-light) dark:border-(--border-dark) bg-(--bg-card-light) dark:bg-(--bg-card-dark) px-3 py-3 rounded-none transition-colors duration-200 hover:border-(--border-hover-light) dark:hover:border-(--border-hover-dark) cursor-pointer">
155
+ class="absolute right-2 top-[calc(50%-4rem)] -translate-y-1/2 z-10 border border-(--border-light) dark:border-(--border-dark) bg-(--bg-card-light) dark:bg-(--bg-card-dark) px-3 py-3 rounded-[3px] transition-colors duration-200 hover:border-(--border-hover-light) dark:hover:border-(--border-hover-dark) cursor-pointer">
156
156
  <HugeiconsIcon :icon="ArrowRight01Icon" class="size-4 text-(--text-secondary-light) dark:text-(--text-secondary-dark)" />
157
157
  </button>
158
158
  </div>
@@ -53,42 +53,44 @@ const socialPlatforms = computed(() => {
53
53
  </script>
54
54
 
55
55
  <template>
56
- <!-- Desktop sidebar on the right -->
57
- <div class="fixed right-[calc(50%-600px)] top-35 z-10 hidden xl:block">
58
- <Card class="p-5! flex flex-col space-y-4 items-center">
59
- <CardContent class="p-0! gap-4 items-center">
60
- <!-- Share icon -->
61
- <HugeiconsIcon :icon="Share08Icon" class="size-5 text-(--text-secondary-light) dark:text-(--text-secondary-dark) mb-2" />
56
+ <Teleport to="body">
57
+ <!-- Desktop sidebar on the right -->
58
+ <div class="fixed right-[calc(50%-600px)] top-35 z-20 hidden xl:block">
59
+ <Card class="p-5! flex flex-col space-y-4 items-center">
60
+ <CardContent class="p-0! gap-4 items-center">
61
+ <!-- Share icon -->
62
+ <HugeiconsIcon :icon="Share08Icon" class="size-5 text-(--text-secondary-light) dark:text-(--text-secondary-dark) mb-2" />
62
63
 
63
- <!-- Social share links (desktop) -->
64
- <template v-for="platform in socialPlatforms" :key="platform.name">
65
- <NuxtLink :to="platform.url"
66
- target="_blank"
67
- rel="noopener noreferrer"
68
- :aria-label="platform.label"
69
- :class="['w-10 h-10 rounded flex items-center justify-center text-white transition-all duration-200 hover:opacity-80 hover:scale-105', platform.color]">
70
- <HugeiconsIcon :icon="platform.icon" class="size-4" />
71
- </NuxtLink>
72
- </template>
73
- </CardContent>
74
- </Card>
75
- </div>
64
+ <!-- Social share links (desktop) -->
65
+ <template v-for="platform in socialPlatforms" :key="platform.name">
66
+ <NuxtLink :to="platform.url"
67
+ target="_blank"
68
+ rel="noopener noreferrer"
69
+ :aria-label="platform.label"
70
+ :class="['w-10 h-10 rounded flex items-center justify-center text-white transition-all duration-200 hover:opacity-80 hover:scale-105', platform.color]">
71
+ <HugeiconsIcon :icon="platform.icon" class="size-4" />
72
+ </NuxtLink>
73
+ </template>
74
+ </CardContent>
75
+ </Card>
76
+ </div>
76
77
 
77
- <!-- Mobile view (floating bar at bottom) -->
78
- <div class="fixed bottom-4 left-1/2 z-10 -translate-x-1/2 xl:hidden">
79
- <Card class="p-3! w-fit">
80
- <CardContent class="flex-row! p-0! items-center justify-center gap-3">
81
- <!-- Social share links (mobile) -->
82
- <template v-for="platform in socialPlatforms" :key="platform.name">
83
- <NuxtLink :to="platform.url"
84
- target="_blank"
85
- rel="noopener noreferrer"
86
- :aria-label="platform.label"
87
- :class="['w-10 h-10 rounded flex items-center justify-center text-white transition-all duration-200 hover:opacity-80 hover:scale-105', platform.color]">
88
- <HugeiconsIcon :icon="platform.icon" class="size-4" />
89
- </NuxtLink>
90
- </template>
91
- </CardContent>
92
- </Card>
93
- </div>
78
+ <!-- Mobile view (floating bar at bottom) -->
79
+ <div class="fixed bottom-4 left-1/2 z-20 -translate-x-1/2 xl:hidden">
80
+ <Card class="p-3! w-fit">
81
+ <CardContent class="flex-row! p-0! items-center justify-center gap-3">
82
+ <!-- Social share links (mobile) -->
83
+ <template v-for="platform in socialPlatforms" :key="platform.name">
84
+ <NuxtLink :to="platform.url"
85
+ target="_blank"
86
+ rel="noopener noreferrer"
87
+ :aria-label="platform.label"
88
+ :class="['w-10 h-10 rounded flex items-center justify-center text-white transition-all duration-200 hover:opacity-80 hover:scale-105', platform.color]">
89
+ <HugeiconsIcon :icon="platform.icon" class="size-4" />
90
+ </NuxtLink>
91
+ </template>
92
+ </CardContent>
93
+ </Card>
94
+ </div>
95
+ </Teleport>
94
96
  </template>
@@ -1,4 +1,5 @@
1
1
  export { default as AllPostsSection } from './AllPostsSection.vue';
2
+ export { default as BlogContentRenderer } from './BlogContentRenderer.vue';
2
3
  export { default as BlogFAQSection } from './BlogFAQSection.vue';
3
4
  export { default as BlogInfoSection } from './BlogInfoSection.vue';
4
5
  export { default as BlogHeaderSection } from './BlogHeaderSection.vue';
@@ -25,6 +25,7 @@ const props = withDefaults(defineProps<{
25
25
  appName: string;
26
26
  appLogo?: string;
27
27
  appLinkTo?: string;
28
+ logoClass?: HTMLAttributes['class'];
28
29
  appDescription?: string;
29
30
  sections?: LandingFooterSection[];
30
31
  linkComponent?: Component | string;
@@ -79,7 +80,7 @@ const getLinkProps = (link: LandingFooterLink) => {
79
80
  <div :class="cn('max-w-md flex-1', props.brandClass)">
80
81
  <!-- App logo and name -->
81
82
  <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-80">
82
- <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="size-8 shrink-0 object-contain" />
83
+ <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)" />
83
84
  <span>{{ props.appName }}</span>
84
85
  </a>
85
86
 
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { NuxtImg } from '#components';
3
3
  import type { HTMLAttributes } from 'vue';
4
+ import BlogProgressBar from '../../blog/BlogProgressBar.vue';
4
5
  import { cn } from '../../../lib/utils';
5
6
 
6
7
  // Props
@@ -13,15 +14,17 @@ const props = withDefaults(defineProps<{
13
14
  containerClass?: HTMLAttributes['class'];
14
15
  leftClass?: HTMLAttributes['class'];
15
16
  rightClass?: HTMLAttributes['class'];
17
+ showProgress?: boolean;
16
18
  }>(), {
17
19
  appName: '',
18
20
  appLogo: '',
19
- appLinkTo: '/'
21
+ appLinkTo: '/',
22
+ showProgress: false
20
23
  });
21
24
  </script>
22
25
 
23
26
  <template>
24
- <header :class="cn('z-30 flex h-20 w-full flex-wrap sm:flex-nowrap sm:justify-start', props.class)">
27
+ <header :class="cn('sticky top-0 z-30 flex h-20 w-full flex-wrap border-b border-transparent bg-transparent text-card-foreground landing-navbar-sticky sm:flex-nowrap sm:justify-start', props.class)">
25
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)">
26
29
  <!-- Left-aligned content -->
27
30
  <div :class="cn('flex items-center', props.leftClass)">
@@ -43,5 +46,36 @@ const props = withDefaults(defineProps<{
43
46
  <slot name="right" />
44
47
  </div>
45
48
  </nav>
49
+
50
+ <div v-if="props.showProgress" class="absolute inset-x-0 bottom-0 translate-y-full">
51
+ <BlogProgressBar aria-label="Blog reading progress" />
52
+ </div>
46
53
  </header>
47
- </template>
54
+ </template>
55
+
56
+ <style scoped>
57
+ .landing-navbar-sticky {
58
+ animation: landing-navbar-scroll linear both;
59
+ animation-range: 0 160px;
60
+ animation-timeline: scroll(root);
61
+ }
62
+
63
+ @keyframes landing-navbar-scroll {
64
+ from {
65
+ background-color: transparent;
66
+ border-bottom-color: transparent;
67
+ }
68
+
69
+ to {
70
+ background-color: var(--card);
71
+ border-bottom-color: var(--border);
72
+ }
73
+ }
74
+
75
+ @supports not (animation-timeline: scroll()) {
76
+ .landing-navbar-sticky {
77
+ border-bottom-color: var(--border);
78
+ background-color: var(--card);
79
+ }
80
+ }
81
+ </style>
@@ -30,9 +30,9 @@ const hasStructuredSlots = computed(() => {
30
30
  <!-- If structured slots are provided, render them -->
31
31
  <template v-if="hasStructuredSlots">
32
32
  <!-- Navbar -->
33
- <header :class="cn('relative z-10', props.navbarClass)">
33
+ <div :class="cn('contents', props.navbarClass)">
34
34
  <slot name="navbar" />
35
- </header>
35
+ </div>
36
36
 
37
37
  <!-- Main content -->
38
38
  <main :class="cn('relative z-10 flex-1', props.mainClass)">