@brumaombra/ui-vintage 0.9.7 → 0.9.8

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 CHANGED
@@ -964,7 +964,7 @@ import { RadioGroup, RadioGroupItem, RadioGroupCard } from '@brumaombra/ui-vinta
964
964
 
965
965
  ### `@brumaombra/ui-vintage/select`
966
966
 
967
- Styled Reka select. `SelectValueContent` and `SelectItemContent` render an icon, label, and description.
967
+ Styled Reka select. `SelectValueContent` and `SelectItemContent` render an icon, label, and description; the default slot of `SelectItemContent` replaces the icon with any leading visual, such as a `LanguageFlag`.
968
968
 
969
969
  ```ts
970
970
  import { Select, SelectContent, SelectGroup, SelectItem, SelectItemContent, SelectItemText, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, SelectValueContent } from '@brumaombra/ui-vintage/select';
@@ -1019,6 +1019,8 @@ import { Select, SelectContent, SelectGroup, SelectItem, SelectItemContent, Sele
1019
1019
  | `textClass` | `HTMLAttributes['class']` | |
1020
1020
  | `descriptionClass` | `HTMLAttributes['class']` | |
1021
1021
 
1022
+ - **Slots:** `default`.
1023
+
1022
1024
  #### `SelectItemText`
1023
1025
 
1024
1026
  - **Also accepts** all props of Reka `SelectItemTextProps`.
@@ -1373,6 +1375,7 @@ import { Badge } from '@brumaombra/ui-vintage/badge';
1373
1375
  | `color` | `ToneColor \| 'primary'` | `'gray'` |
1374
1376
  | `dot` | `boolean` | `false` |
1375
1377
  | `pulse` | `boolean` | `false` |
1378
+ | `interactive` | `boolean` | `false` |
1376
1379
  | `class` | `HTMLAttributes['class']` | |
1377
1380
 
1378
1381
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brumaombra/ui-vintage",
3
- "version": "0.9.7",
3
+ "version": "0.9.8",
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",
@@ -91,13 +91,6 @@ const props = defineProps<{
91
91
  line-height: 1.85;
92
92
  }
93
93
 
94
- /* Lede: the opening paragraph reads a little larger */
95
- .blog-content :deep(p:not(.not-prose):not(.not-prose *):first-of-type) {
96
- font-size: 17px;
97
- line-height: 1.8;
98
- color: var(--muted-foreground);
99
- }
100
-
101
94
  .blog-content :deep(:is(p, blockquote, li):not(.not-prose):not(.not-prose *) strong) {
102
95
  font-weight: 700;
103
96
  color: var(--foreground);
@@ -215,10 +208,6 @@ const props = defineProps<{
215
208
  line-height: 1.8;
216
209
  }
217
210
 
218
- .blog-content :deep(p:not(.not-prose):not(.not-prose *):first-of-type) {
219
- font-size: 15px;
220
- }
221
-
222
211
  .blog-content :deep(h2:not(.not-prose):not(.not-prose *)) {
223
212
  margin-top: 2.25rem;
224
213
  margin-bottom: 1rem;
@@ -20,7 +20,7 @@ const hasValidFAQs = computed(() => props.faqs && props.faqs.length > 0);
20
20
  </script>
21
21
 
22
22
  <template>
23
- <div v-if="hasValidFAQs" class="mt-12 lg:mt-16">
23
+ <div v-if="hasValidFAQs" class="mt-10 lg:mt-12">
24
24
  <!-- Title and description -->
25
25
  <BlogSectionTitle :title="t('uiVintage.blog.faq.title')" :description="t('uiVintage.blog.faq.description')" />
26
26
 
@@ -75,7 +75,7 @@ const headerStats = computed(() => [
75
75
  </script>
76
76
 
77
77
  <template>
78
- <div class="space-y-14 md:space-y-18">
78
+ <div class="space-y-12 md:space-y-14">
79
79
  <!-- Header section -->
80
80
  <BlogHeaderSection :announcement="headerAnnouncement"
81
81
  :title="props.title"
@@ -34,12 +34,12 @@ const showModifiedDate = computed(() => Boolean(props.dateModified) && formatted
34
34
 
35
35
  <template>
36
36
  <!-- Author and date information -->
37
- <Card data-aos="blur-up" class="gap-0! overflow-hidden p-0!">
37
+ <Card data-aos="blur-up" padding="none">
38
38
  <!-- Author -->
39
- <div :class="['flex gap-3 px-5 py-4', props.authorBio ? 'items-start' : 'items-center']">
40
- <!-- Author image -->
39
+ <div :class="['group/author flex gap-3 px-5 py-4', props.authorBio ? 'items-start' : 'items-center']">
40
+ <!-- Author image (grows softly while the author row is hovered) -->
41
41
  <NuxtLinkLocale :to="props.authorUrl" target="_blank" rel="noopener noreferrer" class="shrink-0 rounded outline-none focus-visible:ring-[3px] focus-visible:ring-ring/45">
42
- <NuxtImg :src="props.authorImageUrl" :alt="props.author" height="44" width="44" format="avif" quality="40" :sizes="{ 480: '44px', 1280: '44px' }" loading="lazy" decoding="async" class="size-11 rounded border border-border object-cover" />
42
+ <NuxtImg :src="props.authorImageUrl" :alt="props.author" height="44" width="44" format="avif" quality="40" :sizes="{ 480: '44px', 1280: '44px' }" loading="lazy" decoding="async" class="size-11 rounded border border-border object-cover transition-[scale] duration-500 ease-spring group-hover/author:scale-110" />
43
43
  </NuxtLinkLocale>
44
44
 
45
45
  <!-- Author name and bio -->
@@ -67,7 +67,7 @@ const hasCategory = computed(() => Boolean(props.categoryText && props.categoryS
67
67
  // Whether the details row has any column to show
68
68
  const hasDetails = computed(() => Boolean(props.author || formattedDate.value || resolvedReadingMinutes.value || hasCategory.value));
69
69
 
70
- const badgeLinkClasses = 'rounded outline-none transition-opacity duration-150 hover:opacity-80 focus-visible:ring-[3px] focus-visible:ring-ring/45';
70
+ const badgeLinkClasses = 'rounded outline-none focus-visible:ring-[3px] focus-visible:ring-ring/45';
71
71
 
72
72
  // Shared classes for the details cells (exposed to the meta slot)
73
73
  const detailCellClasses = 'min-w-0 border-t border-l border-border py-4 pl-4 pr-4';
@@ -153,7 +153,7 @@ const detailLabelClasses = 'text-[11px] font-semibold tracking-wider text-muted-
153
153
  <dt :class="detailLabelClasses">{{ t('uiVintage.blog.category') }}</dt>
154
154
  <dd class="mt-1.5">
155
155
  <NuxtLinkLocale :to="categoryPath" :class="['inline-flex', badgeLinkClasses]">
156
- <Badge color="yellow" :text="props.categoryText" pulse />
156
+ <Badge color="yellow" :text="props.categoryText" pulse interactive />
157
157
  </NuxtLinkLocale>
158
158
  </dd>
159
159
  </div>
@@ -166,15 +166,14 @@ const detailLabelClasses = 'text-[11px] font-semibold tracking-wider text-muted-
166
166
  <!-- Tags -->
167
167
  <div v-if="props.tags.length > 0" :class="['mt-4 flex flex-wrap items-center gap-2', enter('animate-uv-fade-up [animation-delay:600ms]')]">
168
168
  <NuxtLinkLocale v-for="tag in props.tags" :key="tag.slug" :to="tagPath(tag.slug)" :class="badgeLinkClasses">
169
- <Badge :text="`#${tag.name}`" />
169
+ <Badge :text="`#${tag.name}`" interactive />
170
170
  </NuxtLinkLocale>
171
171
  </div>
172
172
 
173
173
  <!-- Featured image (replaceable through the media slot) -->
174
174
  <slot name="media">
175
- <div v-if="props.image" :class="['relative mt-8 aspect-video w-full overflow-hidden rounded border border-border shadow-elevated-xl', enter('animate-uv-fade-up [animation-delay:650ms]')]">
175
+ <div v-if="props.image" :class="['relative mt-8 aspect-video w-full overflow-hidden rounded border border-border shadow-elevated-lg', enter('animate-uv-fade-up [animation-delay:650ms]')]">
176
176
  <NuxtImg :src="props.image" :alt="props.title" format="avif" quality="50" :sizes="{ 480: '480px', 1536: '896px' }" loading="eager" fetchpriority="high" preload :class="['size-full object-cover', enter('animate-uv-ken-burns')]" />
177
- <div aria-hidden="true" class="pointer-events-none absolute inset-0 bg-linear-to-t from-black/20 to-transparent" />
178
177
  </div>
179
178
  </slot>
180
179
  </header>
@@ -88,7 +88,7 @@ const postTags = computed(() => ((props.post.tags as string[] | undefined) || []
88
88
  </article>
89
89
 
90
90
  <!-- Related posts -->
91
- <section v-if="props.relatedPosts.length" class="mt-16 md:mt-24">
91
+ <section v-if="props.relatedPosts.length" class="mt-12 md:mt-16">
92
92
  <BlogSectionTitle :title="t('uiVintage.blog.readAlso')" />
93
93
  <div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
94
94
  <NuxtLinkLocale v-for="(relatedPost, index) in props.relatedPosts" :key="relatedPost.path" :to="relatedPost.path" data-aos="blur-up" :data-aos-delay="(index % 2) * 90" class="block h-full rounded outline-none focus-visible:ring-[3px] focus-visible:ring-ring/45">
@@ -102,7 +102,7 @@ const postTags = computed(() => ((props.post.tags as string[] | undefined) || []
102
102
  </section>
103
103
 
104
104
  <!-- Related categories -->
105
- <section v-if="props.relatedCategories.length" class="mt-16 md:mt-24">
105
+ <section v-if="props.relatedCategories.length" class="mt-12 md:mt-16">
106
106
  <BlogSectionTitle :title="t('uiVintage.blog.exploreCategories')">
107
107
  <template #action>
108
108
  <BlogViewAllLink to="/blog/categories" :label="t('uiVintage.blog.exploreAllCategories')" />
@@ -17,7 +17,7 @@ const props = withDefaults(defineProps<{
17
17
  <div v-if="props.tags.length" class="flex flex-wrap gap-2">
18
18
  <!-- Tag chip -->
19
19
  <span v-for="(tag, index) in props.tags" :key="tag.slug" data-aos="blur-up" :data-aos-delay="Math.min(index * 40, 400)" class="inline-flex">
20
- <NuxtLinkLocale :to="`${props.blogPath}/tags/${tag.slug}`" class="group/tag inline-flex items-stretch overflow-hidden rounded border border-border text-xs shadow-elevated-sm outline-none transition-colors duration-150 hover:border-primary/40 focus-visible:ring-[3px] focus-visible:ring-ring/45 sm:text-sm">
20
+ <NuxtLinkLocale :to="`${props.blogPath}/tags/${tag.slug}`" class="group/tag inline-flex items-stretch overflow-hidden rounded border border-border text-xs shadow-elevated-sm outline-none transition-[translate,border-color,box-shadow] duration-300 ease-out-expo hover:-translate-y-0.5 hover:border-primary/40 hover:shadow-elevated-md focus-visible:ring-[3px] focus-visible:ring-ring/45 sm:text-sm">
21
21
  <!-- Name -->
22
22
  <span class="inline-flex items-center gap-1.5 bg-card px-3 py-1.5 font-medium text-foreground transition-colors duration-150 group-hover/tag:bg-primary/10 group-hover/tag:text-primary">
23
23
  <span aria-hidden="true" class="text-primary">#</span>
@@ -16,7 +16,7 @@ const props = withDefaults(defineProps<{
16
16
  </script>
17
17
 
18
18
  <template>
19
- <Card class="group/category h-full gap-0! overflow-hidden p-0! transition-[translate,box-shadow] duration-300 ease-out-expo hover:-translate-y-0.5 hover:shadow-elevated-md">
19
+ <Card padding="none" interactive class="group/category h-full">
20
20
  <!-- Image -->
21
21
  <div class="h-32 w-full overflow-hidden border-b border-border bg-surface">
22
22
  <NuxtImg v-if="props.image" :src="props.image" :alt="props.name" height="225" width="400" format="avif" quality="35" :sizes="{ 480: '480px', 1280: '400px' }" loading="lazy" decoding="async" class="size-full object-cover transition-[scale] duration-700 ease-out-expo group-hover/category:scale-[1.03]" />
@@ -22,7 +22,7 @@ const props = withDefaults(defineProps<{
22
22
  </script>
23
23
 
24
24
  <template>
25
- <Card class="group/post h-full gap-0! overflow-hidden p-0! transition-[translate,box-shadow] duration-300 ease-out-expo hover:-translate-y-0.5 hover:shadow-elevated-md">
25
+ <Card padding="none" interactive class="group/post h-full">
26
26
  <!-- Featured image -->
27
27
  <div v-if="props.image" class="aspect-video w-full overflow-hidden border-b border-border bg-surface">
28
28
  <NuxtImg :src="props.image" :alt="props.title" height="225" width="400" format="avif" quality="35" :sizes="{ 480: '480px', 1280: '400px' }" loading="lazy" decoding="async" class="size-full object-cover transition-[scale] duration-700 ease-out-expo group-hover/post:scale-[1.03]" />
@@ -2,7 +2,7 @@
2
2
  import type { HTMLAttributes } from 'vue';
3
3
  import { computed } from 'vue';
4
4
  import { HugeiconsIcon } from '@hugeicons/vue';
5
- import { getSurfaceToneClasses, type ToneColor } from '../../../lib/color-tokens';
5
+ import { getInteractiveSurfaceToneClasses, getSurfaceToneClasses, type ToneColor } from '../../../lib/color-tokens';
6
6
  import { cn } from '../../../lib/utils';
7
7
  import type { HugeiconsIconDefinition } from '../../../lib/common-types';
8
8
 
@@ -13,20 +13,33 @@ const props = withDefaults(defineProps<{
13
13
  color?: ToneColor | 'primary';
14
14
  dot?: boolean;
15
15
  pulse?: boolean;
16
+ interactive?: boolean;
16
17
  class?: HTMLAttributes['class'];
17
18
  }>(), {
18
19
  icon: null,
19
20
  color: 'gray',
20
21
  dot: false,
21
- pulse: false
22
+ pulse: false,
23
+ interactive: false
22
24
  });
23
25
 
24
26
  // Surface of the badge: a tone, or a soft primary tint (for highlights like a category)
25
27
  const colorClasses = computed(() => props.color === 'primary' ? 'border border-primary/30 bg-primary/10 text-primary' : getSurfaceToneClasses(props.color));
28
+
29
+ // Hover of a badge used as a link: it lifts and tints like the cards (a gray badge takes the primary tint, a tone badge its stronger tone)
30
+ const interactiveClasses = computed(() => {
31
+ if (!props.interactive) return '';
32
+ const tint = props.color === 'gray'
33
+ ? 'hover:border-primary/40 hover:bg-primary/10 hover:text-primary'
34
+ : props.color === 'primary'
35
+ ? 'hover:border-primary/50 hover:bg-primary/15'
36
+ : getInteractiveSurfaceToneClasses(props.color);
37
+ return `cursor-pointer transition-[translate,color,background-color,border-color] duration-200 ease-out-expo hover:-translate-y-0.5 ${tint}`;
38
+ });
26
39
  </script>
27
40
 
28
41
  <template>
29
- <span data-slot="badge" :class="cn('inline-flex w-fit items-center justify-center gap-1.5 whitespace-nowrap rounded border px-2 py-0.5 text-xs font-semibold transition-colors duration-150', colorClasses, props.class)">
42
+ <span data-slot="badge" :class="cn('inline-flex w-fit items-center justify-center gap-1.5 whitespace-nowrap rounded border px-2 py-0.5 text-xs font-semibold transition-colors duration-150', colorClasses, interactiveClasses, props.class)">
30
43
  <!-- Status dot -->
31
44
  <span v-if="props.dot || props.pulse" class="relative flex size-1.5 shrink-0">
32
45
  <span v-if="props.pulse" class="absolute inset-0 animate-uv-ping-soft rounded-full bg-current" />
@@ -18,9 +18,11 @@ const props = defineProps<{
18
18
 
19
19
  <template>
20
20
  <div data-slot="select-item-content" :class="cn('flex min-w-0 flex-1 items-center gap-2', props.class)">
21
- <!-- Icon -->
22
- <div v-if="props.icon" data-slot="select-item-content-icon" :class="cn('shrink-0 self-center', props.iconClass)">
23
- <HugeiconsIcon :icon="props.icon" class="block size-3 sm:size-4" />
21
+ <!-- Leading visual: the default slot (for example a flag), or the icon -->
22
+ <div v-if="$slots.default || props.icon" data-slot="select-item-content-icon" :class="cn('shrink-0 self-center', props.iconClass)">
23
+ <slot>
24
+ <HugeiconsIcon :icon="props.icon" class="block size-3 sm:size-4" />
25
+ </slot>
24
26
  </div>
25
27
 
26
28
  <!-- Text and description -->