@brumaombra/ui-vintage 0.9.4 → 0.9.5

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/AGENTS.md CHANGED
@@ -33,7 +33,7 @@ Only build something new when no entry point fits, and compose it from library p
33
33
  - Icons are Hugeicons definitions: import each one from its own module (`import Rocket01Icon from '@hugeicons/core-free-icons/Rocket01Icon'`, never the `@hugeicons/core-free-icons` barrel, which makes the build parse the whole icon set) and pass them to `:icon` props, or render them with `HugeiconsIcon` from `@hugeicons/vue`. Both packages are peer dependencies, so the app must list them in its own `package.json`.
34
34
  - Form controls use `v-model`. Mark invalid fields with `aria-invalid` and show messages with `FieldError`.
35
35
  - Library strings are translated through vue-i18n (`uiVintage.*` keys). App copy belongs in the app's own locale files.
36
- - Pass links as plain paths (`to: '/settings'`, `app-link-to="/"`), never `localePath()` results: the components render `NuxtLinkLocale`, which adds the language prefix and navigates client-side. External URLs work in the same props. For your own link buttons, use `<Button as-child>` around a `NuxtLinkLocale`.
36
+ - Pass links as plain paths (`to: '/settings'`, `app-link-to="/"`), never `localePath()` results: the components render `NuxtLinkLocale`, which adds the language prefix and navigates client-side. External URLs work in the same props. For link buttons, give the `Button` a `to` (it renders the localized link itself), never a link wrapped around a button.
37
37
 
38
38
  ## 4. Check your work
39
39
 
package/COMPONENTS.md CHANGED
@@ -31,7 +31,7 @@ Buttons, segmented controls, menus, and keyboard hints.
31
31
 
32
32
  ### `@brumaombra/ui-vintage/button`
33
33
 
34
- Button with primary, secondary, ghost, link, and tone variants. Spring press feedback and a `loading` state that keeps the width stable. Use `as-child` to render a link.
34
+ Button with primary, secondary, ghost, link, and tone variants. Spring press feedback and a `loading` state that keeps the width stable. Give it a `to` to make it a navigation button (a localized link, external URLs work too).
35
35
 
36
36
  ```ts
37
37
  import { Button, buttonVariants } from '@brumaombra/ui-vintage/button';
@@ -44,8 +44,8 @@ import type { ButtonVariants } from '@brumaombra/ui-vintage/button';
44
44
  Save changes
45
45
  </Button>
46
46
 
47
- <Button as-child>
48
- <NuxtLink to="/pricing">Pricing</NuxtLink>
47
+ <Button to="/pricing" variant="secondary">
48
+ Pricing
49
49
  </Button>
50
50
  ```
51
51
 
@@ -57,6 +57,7 @@ import type { ButtonVariants } from '@brumaombra/ui-vintage/button';
57
57
  | `size` | `ButtonVariants['size']` | |
58
58
  | `loading` | `boolean` | `false` |
59
59
  | `disabled` | `boolean` | `false` |
60
+ | `to` | `string` | |
60
61
  | `class` | `HTMLAttributes['class']` | |
61
62
 
62
63
  - **Also accepts** all props of Reka `PrimitiveProps`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brumaombra/ui-vintage",
3
- "version": "0.9.4",
3
+ "version": "0.9.5",
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",
@@ -75,7 +75,7 @@ const headerStats = computed(() => [
75
75
  </script>
76
76
 
77
77
  <template>
78
- <div class="space-y-16 md:space-y-24">
78
+ <div class="space-y-14 md:space-y-18">
79
79
  <!-- Header section -->
80
80
  <BlogHeaderSection :announcement="headerAnnouncement"
81
81
  :title="props.title"
@@ -71,7 +71,8 @@ const hasDarkLogo = computed(() => darkLogo.value !== lightLogo.value);
71
71
 
72
72
  <template>
73
73
  <footer :class="cn('relative z-10 border-t border-border bg-card', props.class)">
74
- <div :class="cn('mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8', props.containerClass)">
74
+ <!-- Same width and padding as the landing content, so the footer lines up with the page -->
75
+ <div :class="cn('mx-auto max-w-6xl px-3 py-12 sm:px-6 lg:px-8', props.containerClass)">
75
76
  <div :class="cn('flex flex-col gap-8 md:flex-row md:items-start md:justify-between')">
76
77
  <!-- Brand section -->
77
78
  <div :class="cn('max-w-md flex-1', props.brandClass)">
@@ -23,7 +23,8 @@ const props = withDefaults(defineProps<{
23
23
  <template>
24
24
  <!-- Compact row of stats in one bordered strip -->
25
25
  <ul v-if="props.items.length > 0" data-slot="stat-strip" :class="cn('inline-flex max-w-full flex-wrap items-stretch overflow-hidden rounded border border-border bg-card text-xs shadow-elevated-sm', props.class)">
26
- <li v-for="(item, index) in props.items" :key="item.label" :class="['flex items-center gap-2 px-4 py-2.5', index > 0 && 'border-l border-border']">
26
+ <!-- Each stat draws a left and a top divider, shifted under the strip border where it is not needed (first column and first row), and grows to fill its row when the strip wraps -->
27
+ <li v-for="item in props.items" :key="item.label" class="-ml-px -mt-px flex grow items-center justify-center gap-2 border-l border-t border-border px-4 py-2.5">
27
28
  <HugeiconsIcon v-if="item.icon" :icon="item.icon" class="size-4 shrink-0 text-primary" />
28
29
  <span class="font-semibold text-foreground tabular-nums">
29
30
  <AnimatedNumber v-if="props.animate && typeof item.value === 'number'" :value="item.value" :format-options="item.formatOptions" />
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
+ import { NuxtLinkLocale } from '#components';
2
3
  import type { PrimitiveProps } from 'reka-ui';
3
- import type { HTMLAttributes } from 'vue';
4
+ import { computed, type HTMLAttributes } from 'vue';
4
5
  import type { ButtonVariants } from '.';
5
6
  import { Primitive } from 'reka-ui';
6
7
  import { cn } from '../../../lib/utils';
@@ -13,16 +14,36 @@ const props = withDefaults(defineProps<PrimitiveProps & {
13
14
  size?: ButtonVariants['size'];
14
15
  loading?: boolean;
15
16
  disabled?: boolean;
17
+ to?: string;
16
18
  class?: HTMLAttributes['class'];
17
19
  }>(), {
18
20
  as: 'button',
19
21
  loading: false,
20
22
  disabled: false
21
23
  });
24
+
25
+ // Disabled or loading: the button can't be pressed (or followed, for a navigation button)
26
+ const isInactive = computed(() => props.disabled || props.loading);
27
+
28
+ // Same-page anchor (like #setup): a plain link, since there is no route to localize
29
+ const isAnchor = computed(() => !!props.to?.startsWith('#'));
30
+
31
+ // Element to render: a localized link for navigation buttons (a plain anchor for same-page links, a span while inactive so it can't be followed), otherwise the as element
32
+ const element = computed(() => {
33
+ if (!props.to) return props.as;
34
+ if (isInactive.value) return 'span';
35
+ return isAnchor.value ? 'a' : NuxtLinkLocale;
36
+ });
37
+
38
+ // Link attribute for the rendered element (href for a plain anchor, to for the localized link)
39
+ const linkAttributes = computed(() => {
40
+ if (!props.to || isInactive.value) return {};
41
+ return isAnchor.value ? { href: props.to } : { to: props.to };
42
+ });
22
43
  </script>
23
44
 
24
45
  <template>
25
- <Primitive data-slot="button" :data-variant="variant" :data-size="size" :data-loading="props.loading ? '' : undefined" :aria-busy="props.loading || undefined" :disabled="props.disabled || props.loading || undefined" :as="as" :as-child="asChild" :class="cn(buttonVariants({ variant, size }), props.class)">
46
+ <Primitive data-slot="button" :data-variant="variant" :data-size="size" :data-loading="props.loading ? '' : undefined" :aria-busy="props.loading || undefined" :disabled="(!props.to && isInactive) || undefined" :aria-disabled="(props.to && isInactive) || undefined" v-bind="linkAttributes" :as="element" :as-child="asChild" :class="cn(buttonVariants({ variant, size }), props.class)">
26
47
  <!-- As-child buttons delegate rendering to the slotted element -->
27
48
  <slot v-if="asChild" />
28
49
 
@@ -32,7 +32,7 @@ export const buttonVariants: (props?: ButtonVariants & ClassProp) => string = cv
32
32
  [
33
33
  "relative isolate inline-flex shrink-0 cursor-pointer select-none items-center justify-center gap-2 overflow-hidden whitespace-nowrap rounded text-xs font-semibold outline-none sm:text-sm",
34
34
  "[transition:color_150ms,background-color_150ms,border-color_150ms,box-shadow_220ms,opacity_150ms,scale_380ms_var(--ease-spring)]",
35
- "enabled:active:scale-[0.97] enabled:active:duration-75 disabled:pointer-events-none disabled:opacity-50 data-loading:cursor-wait data-loading:disabled:opacity-100",
35
+ "not-disabled:not-aria-disabled:active:scale-[0.97] not-disabled:not-aria-disabled:active:duration-75 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-loading:cursor-wait data-loading:disabled:opacity-100 data-loading:aria-disabled:opacity-100",
36
36
  "focus-visible:ring-[3px] focus-visible:ring-ring/45",
37
37
  "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
38
38
  "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
@@ -2,4 +2,5 @@ declare module '#components' {
2
2
  import type { DefineComponent } from 'vue';
3
3
 
4
4
  export const NuxtImg: DefineComponent<Record<string, unknown>, Record<string, unknown>, any>;
5
+ export const NuxtLinkLocale: DefineComponent<Record<string, unknown>, Record<string, unknown>, any>;
5
6
  }