@brumaombra/ui-vintage 0.9.4 → 0.9.6
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 +1 -1
- package/COMPONENTS.md +4 -3
- package/package.json +1 -1
- package/src/components/blog/BlogIndexPage.vue +1 -1
- package/src/components/dashboard-shell/DashboardShell.vue +2 -2
- package/src/components/landing/landing-footer/LandingFooter.vue +2 -1
- package/src/components/stat-strip/StatStrip.vue +2 -1
- package/src/components/ui/button/Button.vue +23 -2
- package/src/components/ui/button/index.ts +1 -1
- package/src/nuxt-components.d.ts +1 -0
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
|
|
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.
|
|
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
|
|
48
|
-
|
|
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
|
@@ -113,8 +113,8 @@ const logoAlt = computed(() => props.appName ? t('uiVintage.common.logo', { name
|
|
|
113
113
|
<SidebarMenuItem v-for="item in section.items" :key="item.id">
|
|
114
114
|
<SidebarMenuButton as-child :is-active="item.active" :class="cn('group/nav relative h-auto min-h-11 overflow-hidden rounded px-3 py-2.5 text-muted-foreground transition-colors duration-150 hover:bg-accent hover:text-foreground data-[active=true]:bg-primary/10 data-[active=true]:text-foreground sm:px-3.5', 'before:absolute before:inset-y-2 before:left-0 before:w-0.5 before:origin-center before:scale-y-0 before:bg-primary before:transition-transform before:duration-300 before:ease-spring data-[active=true]:before:scale-y-100', item.description ? 'items-start' : 'items-center')">
|
|
115
115
|
<NuxtLinkLocale v-bind="getSidebarItemLinkProps(item)" :aria-current="item.active ? 'page' : undefined">
|
|
116
|
-
<!-- Icon, centered on the capitals of the label (or of its first line, 1.25em tall): the line box alone centers it 0.08em too low, because of the space the font keeps for descenders (the size is important: the menu button
|
|
117
|
-
<HugeiconsIcon v-if="item.icon" :icon="item.icon" :stroke-width="1.8" :class="cn('size-
|
|
116
|
+
<!-- Icon, centered on the capitals of the label (or of its first line, 1.25em tall): the line box alone centers it 0.08em too low, because of the space the font keeps for descenders (the size is important: the menu button sizes its direct svg children itself, down to size-3.5 in compact mode) -->
|
|
117
|
+
<HugeiconsIcon v-if="item.icon" :icon="item.icon" :stroke-width="1.8" :class="cn('size-4! shrink-0 opacity-90 transition-[opacity,color] duration-150 group-hover/nav:opacity-100 group-data-[active=true]/nav:text-primary', item.description ? 'mt-[calc((1.25em_-_16px)/2_-_0.08em)]' : 'self-center -translate-y-[0.08em]')" />
|
|
118
118
|
|
|
119
119
|
<!-- Label and optional description -->
|
|
120
120
|
<div :class="cn('min-w-0 flex-1 text-left leading-tight', item.description ? 'grid' : 'flex items-center')">
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
"
|
|
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",
|
package/src/nuxt-components.d.ts
CHANGED
|
@@ -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
|
}
|