@brumaombra/ui-vintage 0.9.6 → 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/AGENTS.md +3 -1
- package/COMPONENTS.md +132 -15
- package/package.json +4 -2
- package/src/components/action-bar/ActionBar.vue +38 -0
- package/src/components/action-bar/index.ts +2 -0
- package/src/components/blog/BlogContentRenderer.vue +3 -14
- package/src/components/blog/BlogFAQSection.vue +1 -1
- package/src/components/blog/BlogIndexPage.vue +1 -1
- package/src/components/blog/BlogInfoSection.vue +4 -4
- package/src/components/blog/BlogPostHeader.vue +4 -5
- package/src/components/blog/BlogPostPage.vue +6 -2
- package/src/components/blog/BlogSectionTitle.vue +7 -21
- package/src/components/blog/BlogSummary.vue +31 -0
- package/src/components/blog/BlogTagsSection.vue +1 -1
- package/src/components/blog/CategoryCard.vue +1 -1
- package/src/components/blog/PostCard.vue +1 -1
- package/src/components/blog/blogPageUtils.ts +1 -0
- package/src/components/blog/index.ts +1 -0
- package/src/components/chip/Chip.vue +1 -1
- package/src/components/dashboard-shell/DashboardShell.vue +3 -3
- package/src/components/data-list/DataListText.vue +7 -4
- package/src/components/language-flag/flags/ChineseFlag.vue +1 -1
- package/src/components/language-flag/flags/EnglishFlag.vue +1 -1
- package/src/components/language-flag/flags/FrenchFlag.vue +1 -1
- package/src/components/language-flag/flags/GermanFlag.vue +1 -1
- package/src/components/language-flag/flags/ItalianFlag.vue +1 -1
- package/src/components/language-flag/flags/JapaneseFlag.vue +1 -1
- package/src/components/language-flag/flags/PortugueseFlag.vue +1 -1
- package/src/components/language-flag/flags/RussianFlag.vue +1 -1
- package/src/components/language-flag/flags/SpanishFlag.vue +1 -1
- package/src/components/language-selector/LanguageSelector.vue +2 -2
- package/src/components/section-title/SectionTitle.vue +47 -0
- package/src/components/section-title/index.ts +2 -0
- package/src/components/single-value-card/SingleValueCard.vue +1 -1
- package/src/components/ui/alert-dialog/AlertDialogAction.vue +6 -6
- package/src/components/ui/avatar/Avatar.vue +4 -2
- package/src/components/ui/avatar/AvatarFallback.vue +25 -5
- package/src/components/ui/badge/Badge.vue +21 -4
- package/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue +1 -1
- package/src/components/ui/breadcrumb/BreadcrumbLink.vue +1 -1
- package/src/components/ui/button/Button.vue +18 -2
- package/src/components/ui/button/index.ts +4 -0
- package/src/components/ui/card/Card.vue +25 -1
- package/src/components/ui/card/CardMedia.vue +15 -0
- package/src/components/ui/card/CardTitle.vue +8 -3
- package/src/components/ui/card/index.ts +1 -0
- package/src/components/ui/checkbox/Checkbox.vue +1 -1
- package/src/components/ui/combobox/ComboboxSelect.vue +2 -2
- package/src/components/ui/combobox/ComboboxTrigger.vue +1 -1
- package/src/components/ui/data-table/DataTable.vue +1 -1
- package/src/components/ui/dialog/DialogContent.vue +7 -3
- package/src/components/ui/dialog/DialogScrollContent.vue +7 -3
- package/src/components/ui/dialog/index.ts +11 -1
- package/src/components/ui/file-dropzone/FileDropzone.vue +1 -1
- package/src/components/ui/input/Input.vue +42 -9
- package/src/components/ui/kbd/Kbd.vue +8 -4
- package/src/components/ui/number-field/NumberField.vue +2 -2
- package/src/components/ui/progress/Progress.vue +17 -5
- package/src/components/ui/select/SelectItem.vue +1 -1
- package/src/components/ui/select/SelectItemContent.vue +5 -3
- package/src/components/ui/skeleton/Skeleton.vue +7 -3
- package/src/components/ui/switch/Switch.vue +1 -1
- package/src/components/ui/table/TableHeader.vue +1 -1
- package/src/components/ui/table/TableRow.vue +11 -4
- package/src/components/ui/tabs/Tabs.vue +1 -1
- package/src/components/ui/tabs/TabsContent.vue +8 -5
- package/src/components/ui/tabs/TabsTrigger.vue +1 -1
- package/src/components/ui/tags-input/TagsInputItemDelete.vue +1 -1
- package/src/components/ui/textarea/Textarea.vue +1 -1
- package/src/components/ui/toggle-group/ToggleGroup.vue +2 -2
- package/src/components/ui/toggle-group/ToggleGroupItem.vue +3 -1
- package/src/components/ui/toggle-group/index.ts +2 -2
- package/src/i18n/de.json +1 -0
- package/src/i18n/en.json +1 -0
- package/src/i18n/es.json +1 -0
- package/src/i18n/fr.json +1 -0
- package/src/i18n/it.json +1 -0
- package/src/i18n/ja.json +1 -0
- package/src/i18n/pt.json +1 -0
- package/src/i18n/ru.json +1 -0
- package/src/i18n/zh.json +1 -0
- package/src/lib/color-tokens.ts +6 -6
- package/src/styles.css +11 -6
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useI18n } from 'vue-i18n';
|
|
3
|
+
import Idea01Icon from '@hugeicons/core-free-icons/Idea01Icon';
|
|
4
|
+
import { HugeiconsIcon } from '@hugeicons/vue';
|
|
5
|
+
import { Card } from '../ui/card';
|
|
6
|
+
|
|
7
|
+
// Props (the short answer of the post, from the summary field of its frontmatter)
|
|
8
|
+
const props = defineProps<{
|
|
9
|
+
summary: string;
|
|
10
|
+
}>();
|
|
11
|
+
|
|
12
|
+
const { t } = useI18n();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<Card data-slot="blog-summary" role="region" padding="none" class="mb-6" :aria-label="t('uiVintage.blog.inShort')">
|
|
17
|
+
<!-- Header (same layout as the table of contents accordion) -->
|
|
18
|
+
<div class="flex items-center gap-3 px-5 py-3.5">
|
|
19
|
+
<!-- Icon tile -->
|
|
20
|
+
<span class="flex size-7 shrink-0 items-center justify-center rounded border border-primary/30 text-primary">
|
|
21
|
+
<HugeiconsIcon :icon="Idea01Icon" class="size-3.5" />
|
|
22
|
+
</span>
|
|
23
|
+
|
|
24
|
+
<!-- Label -->
|
|
25
|
+
<p class="text-sm font-semibold text-foreground">{{ t('uiVintage.blog.inShort') }}</p>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<!-- Summary -->
|
|
29
|
+
<p class="border-t border-border px-5 py-4 text-sm leading-7 text-foreground md:text-base">{{ props.summary }}</p>
|
|
30
|
+
</Card>
|
|
31
|
+
</template>
|
|
@@ -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-
|
|
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
|
|
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
|
|
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]" />
|
|
@@ -5,6 +5,7 @@ export { default as BlogInfoSection } from './BlogInfoSection.vue';
|
|
|
5
5
|
export { default as BlogPostHeader } from './BlogPostHeader.vue';
|
|
6
6
|
export { default as BlogHeaderSection } from './BlogHeaderSection.vue';
|
|
7
7
|
export { default as BlogSectionTitle } from './BlogSectionTitle.vue';
|
|
8
|
+
export { default as BlogSummary } from './BlogSummary.vue';
|
|
8
9
|
export { default as BlogTagsSection } from './BlogTagsSection.vue';
|
|
9
10
|
export { default as CategoriesList } from './CategoriesList.vue';
|
|
10
11
|
export { default as CategoriesSection } from './CategoriesSection.vue';
|
|
@@ -37,7 +37,7 @@ const handleRemove = () => {
|
|
|
37
37
|
<span class="leading-none">{{ props.text }}</span>
|
|
38
38
|
|
|
39
39
|
<!-- Remove button -->
|
|
40
|
-
<button type="button" :aria-label="resolvedRemoveLabel" class="group/remove inline-flex size-4 cursor-pointer items-center justify-center rounded
|
|
40
|
+
<button type="button" :aria-label="resolvedRemoveLabel" class="group/remove inline-flex size-4 cursor-pointer items-center justify-center rounded opacity-70 [transition:opacity_150ms,background-color_150ms,scale_300ms_var(--ease-spring)] hover:bg-black/5 hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-current/20 active:scale-75 dark:hover:bg-white/10" @click="handleRemove">
|
|
41
41
|
<HugeiconsIcon :icon="Cancel01Icon" class="size-3 transition-transform duration-300 ease-spring group-hover/remove:rotate-90" />
|
|
42
42
|
</button>
|
|
43
43
|
</div>
|
|
@@ -75,7 +75,7 @@ const logoAlt = computed(() => props.appName ? t('uiVintage.common.logo', { name
|
|
|
75
75
|
<!-- Sidebar -->
|
|
76
76
|
<Sidebar :collapsible="props.collapsible" :compact="props.compact">
|
|
77
77
|
<!-- Sidebar header -->
|
|
78
|
-
<SidebarHeader v-if="props.appName || lightLogo || $slots['sidebar-header']" class="h-16 justify-center
|
|
78
|
+
<SidebarHeader v-if="props.appName || lightLogo || $slots['sidebar-header']" class="h-16 justify-center shadow-[inset_0_-1px_0_var(--sidebar-border)]">
|
|
79
79
|
<slot name="sidebar-header">
|
|
80
80
|
<SidebarMenu>
|
|
81
81
|
<SidebarMenuItem>
|
|
@@ -149,8 +149,8 @@ const logoAlt = computed(() => props.appName ? t('uiVintage.common.logo', { name
|
|
|
149
149
|
<!-- Background -->
|
|
150
150
|
<BackgroundGrid v-if="props.showBackground" />
|
|
151
151
|
|
|
152
|
-
<!-- Topbar -->
|
|
153
|
-
<header :class="cn('sticky top-0 z-20 flex h-16 shrink-0 items-center gap-2
|
|
152
|
+
<!-- Topbar (its bottom line is an inset shadow, so the whole 64px is free and the content centers on whole pixels) -->
|
|
153
|
+
<header :class="cn('sticky top-0 z-20 flex h-16 shrink-0 items-center gap-2 shadow-[inset_0_-1px_0_var(--sidebar-border)] bg-sidebar/80 px-4 backdrop-blur-md backdrop-saturate-150', props.topbarClass)">
|
|
154
154
|
<!-- Sidebar trigger -->
|
|
155
155
|
<SidebarTrigger class="-ml-1" />
|
|
156
156
|
|
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
import type { HTMLAttributes } from 'vue';
|
|
3
3
|
import { cn } from '../../lib/utils';
|
|
4
4
|
|
|
5
|
-
// Props
|
|
6
|
-
const props = defineProps<{
|
|
5
|
+
// Props (long text is clamped to 3 lines, wrap shows all of it, keeping its line breaks)
|
|
6
|
+
const props = withDefaults(defineProps<{
|
|
7
|
+
wrap?: boolean;
|
|
7
8
|
class?: HTMLAttributes['class'];
|
|
8
|
-
}>()
|
|
9
|
+
}>(), {
|
|
10
|
+
wrap: false
|
|
11
|
+
});
|
|
9
12
|
</script>
|
|
10
13
|
|
|
11
14
|
<template>
|
|
12
|
-
<div data-slot="data-list-text" :class="cn('line-clamp-3', props.class)">
|
|
15
|
+
<div data-slot="data-list-text" :class="cn(props.wrap ? 'whitespace-pre-wrap break-words' : 'line-clamp-3', props.class)">
|
|
13
16
|
<slot />
|
|
14
17
|
</div>
|
|
15
18
|
</template>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480" aria-hidden="true" focusable="false">
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-[1.35rem] shrink-0 rounded-[2px]" viewBox="0 0 640 480" aria-hidden="true" focusable="false">
|
|
3
3
|
<path fill="#ee1c25" d="M0 0h640v480H0z" />
|
|
4
4
|
<path fill="#ff0" d="M76.8 177.6 120 48l43.2 129.6H48z" transform="matrix(.55555 0 0 .55555 53.333 53.333)" />
|
|
5
5
|
<path fill="#ff0" d="M76.8 177.6 120 48l43.2 129.6H48z" transform="matrix(-.0955 -.15916 .15916 -.0955 240.3 48)" />
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480" aria-hidden="true" focusable="false">
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-[1.35rem] shrink-0 rounded-[2px]" viewBox="0 0 640 480" aria-hidden="true" focusable="false">
|
|
3
3
|
<path fill="#012169" d="M0 0h640v480H0z" />
|
|
4
4
|
<path fill="#FFF" d="m75 0 244 181L562 0h78v62L400 241l240 178v61h-80L320 301 81 480H0v-60l239-178L0 64V0z" />
|
|
5
5
|
<path fill="#C8102E" d="m424 281 216 159v40L369 281zm-184 20 6 35L54 480H0zM640 0v3L391 191l2-44L590 0zM0 0l239 176h-60L0 42z" />
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2" aria-hidden="true" focusable="false">
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-[1.35rem] shrink-0 rounded-[2px]" viewBox="0 0 3 2" aria-hidden="true" focusable="false">
|
|
3
3
|
<rect width="1" height="2" x="0" fill="#002654" />
|
|
4
4
|
<rect width="1" height="2" x="1" fill="#ffffff" />
|
|
5
5
|
<rect width="1" height="2" x="2" fill="#ce1126" />
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 5 3" aria-hidden="true" focusable="false">
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-[1.35rem] shrink-0 rounded-[2px]" viewBox="0 0 5 3" aria-hidden="true" focusable="false">
|
|
3
3
|
<rect width="5" height="1" y="0" fill="#000000" />
|
|
4
4
|
<rect width="5" height="1" y="1" fill="#dd0000" />
|
|
5
5
|
<rect width="5" height="1" y="2" fill="#ffce00" />
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480" aria-hidden="true" focusable="false">
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-[1.35rem] shrink-0 rounded-[2px]" viewBox="0 0 640 480" aria-hidden="true" focusable="false">
|
|
3
3
|
<g fill-rule="evenodd" stroke-width="1pt">
|
|
4
4
|
<path fill="#fff" d="M0 0h640v480H0z" />
|
|
5
5
|
<path fill="#009246" d="M0 0h213.3v480H0z" />
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480" aria-hidden="true" focusable="false">
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-[1.35rem] shrink-0 rounded-[2px]" viewBox="0 0 640 480" aria-hidden="true" focusable="false">
|
|
3
3
|
<path fill="#fff" d="M0 0h640v480H0z" />
|
|
4
4
|
<circle cx="320" cy="240" r="112" fill="#bc002d" />
|
|
5
5
|
</svg>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 640 480" aria-hidden="true" focusable="false">
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-[1.35rem] shrink-0 rounded-[2px]" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 640 480" aria-hidden="true" focusable="false">
|
|
3
3
|
<path fill="red" d="M256 0h384v480H256z" />
|
|
4
4
|
<path fill="#060" d="M0 0h256v480H0z" />
|
|
5
5
|
<g fill="#ff0" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width=".6">
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480" aria-hidden="true" focusable="false">
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-[1.35rem] shrink-0 rounded-[2px]" viewBox="0 0 640 480" aria-hidden="true" focusable="false">
|
|
3
3
|
<path fill="#fff" d="M0 0h640v160H0z" />
|
|
4
4
|
<path fill="#0039a6" d="M0 160h640v160H0z" />
|
|
5
5
|
<path fill="#d52b1e" d="M0 320h640v160H0z" />
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 750 500" aria-hidden="true" focusable="false">
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-[1.35rem] shrink-0 rounded-[2px]" viewBox="0 0 750 500" aria-hidden="true" focusable="false">
|
|
3
3
|
<rect width="750" height="500" fill="#c60b1e" />
|
|
4
4
|
<rect y="125" width="750" height="250" fill="#ffc400" />
|
|
5
5
|
</svg>
|
|
@@ -139,7 +139,7 @@ const handleSelectLanguage = (language: string) => {
|
|
|
139
139
|
<!-- Trigger button showing the current language flag -->
|
|
140
140
|
<PopoverTrigger as-child>
|
|
141
141
|
<Button variant="secondary" size="icon" :aria-label="t('uiVintage.language.ariaLabel')">
|
|
142
|
-
<component :is="currentFlagComponent" v-if="currentFlagComponent"
|
|
142
|
+
<component :is="currentFlagComponent" v-if="currentFlagComponent" />
|
|
143
143
|
</Button>
|
|
144
144
|
</PopoverTrigger>
|
|
145
145
|
|
|
@@ -149,7 +149,7 @@ const handleSelectLanguage = (language: string) => {
|
|
|
149
149
|
<CommandList :aria-label="t('uiVintage.language.title')">
|
|
150
150
|
<CommandGroup :heading="t('uiVintage.language.title')" stagger>
|
|
151
151
|
<CommandItem v-for="option in normalizedLanguages" :key="option.code" :value="option.code" @select="handleSelectLanguage(option.code)">
|
|
152
|
-
<component :is="resolveFlagComponent(option.code)"
|
|
152
|
+
<component :is="resolveFlagComponent(option.code)" />
|
|
153
153
|
<span class="flex-1">{{ option.label }}</span>
|
|
154
154
|
<HugeiconsIcon :icon="Tick02Icon" class="ml-auto size-4 shrink-0" :class="props.modelValue === option.code ? 'opacity-100' : 'opacity-0'" />
|
|
155
155
|
</CommandItem>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { HugeiconsIcon } from '@hugeicons/vue';
|
|
3
|
+
import type { HeadingLevel, HugeiconsIconDefinition } from '../../lib/common-types';
|
|
4
|
+
|
|
5
|
+
// Props (a section heading over a rule that starts with a primary segment, with an optional icon tile, description and trailing action)
|
|
6
|
+
const props = withDefaults(defineProps<{
|
|
7
|
+
title: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
icon?: HugeiconsIconDefinition | null;
|
|
10
|
+
headingLevel?: HeadingLevel;
|
|
11
|
+
}>(), {
|
|
12
|
+
description: '',
|
|
13
|
+
icon: null,
|
|
14
|
+
headingLevel: 2
|
|
15
|
+
});
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<div data-slot="section-title" :class="['relative mb-6 flex gap-3 border-b border-border pb-4 md:mb-8', props.description ? 'flex-col sm:flex-row sm:items-end sm:justify-between' : 'items-center justify-between']">
|
|
20
|
+
<!-- Primary segment at the start of the rule -->
|
|
21
|
+
<span aria-hidden="true" class="absolute -bottom-px left-0 h-0.5 w-12 bg-primary" />
|
|
22
|
+
|
|
23
|
+
<div class="flex min-w-0 items-center gap-3">
|
|
24
|
+
<!-- Icon tile -->
|
|
25
|
+
<span v-if="props.icon" class="flex size-9 shrink-0 items-center justify-center rounded border border-primary/30 text-primary">
|
|
26
|
+
<HugeiconsIcon :icon="props.icon" class="size-4.5" :stroke-width="1.6" />
|
|
27
|
+
</span>
|
|
28
|
+
|
|
29
|
+
<div class="min-w-0">
|
|
30
|
+
<!-- Title -->
|
|
31
|
+
<component :is="`h${props.headingLevel}`" class="text-xl font-semibold tracking-tight text-foreground md:text-2xl">
|
|
32
|
+
{{ props.title }}
|
|
33
|
+
</component>
|
|
34
|
+
|
|
35
|
+
<!-- Description -->
|
|
36
|
+
<p v-if="props.description" class="mt-1 max-w-2xl text-xs leading-6 text-muted-foreground md:text-sm">
|
|
37
|
+
{{ props.description }}
|
|
38
|
+
</p>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<!-- Optional trailing action (for example a "View all" link) -->
|
|
43
|
+
<div v-if="$slots.action" class="shrink-0">
|
|
44
|
+
<slot name="action" />
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</template>
|
|
@@ -72,7 +72,7 @@ const staticValue = computed(() => typeof props.value === 'number' && props.form
|
|
|
72
72
|
</span>
|
|
73
73
|
|
|
74
74
|
<!-- Trend badge -->
|
|
75
|
-
<span v-if="props.trend !== null" :class="cn('inline-flex items-center gap-0.5 rounded
|
|
75
|
+
<span v-if="props.trend !== null" :class="cn('inline-flex items-center gap-0.5 rounded px-1 py-0.5 text-[11px] font-semibold tabular-nums', trendIsPositive ? 'bg-success/10 text-success' : 'bg-destructive/10 text-destructive')">
|
|
76
76
|
<HugeiconsIcon :icon="trendIsPositive ? ArrowUpRight01Icon : ArrowDownRight01Icon" class="size-3" />
|
|
77
77
|
{{ formattedTrend }}
|
|
78
78
|
</span>
|
|
@@ -4,17 +4,17 @@ import type { HTMLAttributes } from 'vue';
|
|
|
4
4
|
import { reactiveOmit } from '@vueuse/core';
|
|
5
5
|
import { AlertDialogAction } from 'reka-ui';
|
|
6
6
|
import { cn } from '../../../lib/utils';
|
|
7
|
-
import { buttonVariants } from '../button';
|
|
7
|
+
import { buttonVariants, type ButtonVariants } from '../button';
|
|
8
8
|
|
|
9
|
-
// Props
|
|
10
|
-
const props = defineProps<AlertDialogActionProps & { class?: HTMLAttributes['class'] }>();
|
|
9
|
+
// Props (variant styles the action like a Button, for example red for a destructive confirmation)
|
|
10
|
+
const props = defineProps<AlertDialogActionProps & { variant?: ButtonVariants['variant']; class?: HTMLAttributes['class'] }>();
|
|
11
11
|
|
|
12
|
-
// Omit local
|
|
13
|
-
const delegatedProps = reactiveOmit(props, 'class');
|
|
12
|
+
// Omit local props from delegated props
|
|
13
|
+
const delegatedProps = reactiveOmit(props, 'class', 'variant');
|
|
14
14
|
</script>
|
|
15
15
|
|
|
16
16
|
<template>
|
|
17
|
-
<AlertDialogAction v-bind="delegatedProps" :class="cn(buttonVariants(), props.class)">
|
|
17
|
+
<AlertDialogAction v-bind="delegatedProps" :class="cn(buttonVariants({ variant: props.variant }), props.class)">
|
|
18
18
|
<slot />
|
|
19
19
|
</AlertDialogAction>
|
|
20
20
|
</template>
|
|
@@ -14,13 +14,15 @@ const props = withDefaults(defineProps<AvatarRootProps & {
|
|
|
14
14
|
size?: AvatarSize;
|
|
15
15
|
shape?: AvatarShape;
|
|
16
16
|
status?: AvatarStatus;
|
|
17
|
+
ring?: boolean;
|
|
17
18
|
class?: HTMLAttributes['class'];
|
|
18
19
|
}>(), {
|
|
19
20
|
size: 'md',
|
|
20
21
|
shape: 'circle',
|
|
22
|
+
ring: false,
|
|
21
23
|
});
|
|
22
24
|
|
|
23
|
-
const delegatedProps = reactiveOmit(props, 'class', 'size', 'shape', 'status');
|
|
25
|
+
const delegatedProps = reactiveOmit(props, 'class', 'size', 'shape', 'status', 'ring');
|
|
24
26
|
|
|
25
27
|
const { t } = useI18n();
|
|
26
28
|
|
|
@@ -49,7 +51,7 @@ const statusPositionClass = computed(() => props.shape === 'square' ? '-right-0.
|
|
|
49
51
|
</script>
|
|
50
52
|
|
|
51
53
|
<template>
|
|
52
|
-
<span data-slot="avatar" :data-size="props.size" :data-shape="props.shape" :data-status="props.status" :class="cn('relative inline-flex shrink-0 select-none align-middle', avatarSizeClasses[props.size], avatarShapeClasses[props.shape], props.class)">
|
|
54
|
+
<span data-slot="avatar" :data-size="props.size" :data-shape="props.shape" :data-status="props.status" :class="cn('relative inline-flex shrink-0 select-none align-middle', avatarSizeClasses[props.size], avatarShapeClasses[props.shape], props.ring && 'ring-2 ring-card', props.class)">
|
|
53
55
|
<!-- Avatar surface -->
|
|
54
56
|
<AvatarRoot v-bind="delegatedProps" :class="cn('relative flex size-full items-center justify-center overflow-hidden bg-surface', avatarShapeClasses[props.shape])">
|
|
55
57
|
<slot />
|
|
@@ -7,13 +7,33 @@ import { computed, inject } from 'vue';
|
|
|
7
7
|
import { cn } from '../../../lib/utils';
|
|
8
8
|
import { avatarContextKey, avatarShapeClasses, avatarTextClasses, getInitials } from '.';
|
|
9
9
|
|
|
10
|
-
// Props
|
|
11
|
-
const props = defineProps<AvatarFallbackProps & {
|
|
10
|
+
// Props (auto tints the initials with a stable color picked from the name, primary uses the brand color, for example for the current user)
|
|
11
|
+
const props = withDefaults(defineProps<AvatarFallbackProps & {
|
|
12
12
|
name?: string;
|
|
13
|
+
color?: 'neutral' | 'auto' | 'primary';
|
|
13
14
|
class?: HTMLAttributes['class'];
|
|
14
|
-
}>()
|
|
15
|
+
}>(), {
|
|
16
|
+
color: 'neutral'
|
|
17
|
+
});
|
|
15
18
|
|
|
16
|
-
const delegatedProps = reactiveOmit(props, 'class', 'name');
|
|
19
|
+
const delegatedProps = reactiveOmit(props, 'class', 'name', 'color');
|
|
20
|
+
|
|
21
|
+
// Tints built on the chart tokens, so they adapt to the theme (the text is mixed with the foreground, so it stays readable on the tint in both themes)
|
|
22
|
+
const autoColorClasses = [
|
|
23
|
+
'bg-[color-mix(in_oklab,var(--chart-1)_22%,var(--card))] text-[color-mix(in_oklab,var(--chart-1)_45%,var(--foreground))]',
|
|
24
|
+
'bg-[color-mix(in_oklab,var(--chart-2)_22%,var(--card))] text-[color-mix(in_oklab,var(--chart-2)_45%,var(--foreground))]',
|
|
25
|
+
'bg-[color-mix(in_oklab,var(--chart-3)_22%,var(--card))] text-[color-mix(in_oklab,var(--chart-3)_45%,var(--foreground))]',
|
|
26
|
+
'bg-[color-mix(in_oklab,var(--chart-4)_22%,var(--card))] text-[color-mix(in_oklab,var(--chart-4)_45%,var(--foreground))]',
|
|
27
|
+
'bg-[color-mix(in_oklab,var(--chart-5)_22%,var(--card))] text-[color-mix(in_oklab,var(--chart-5)_45%,var(--foreground))]'
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// Surface and text color (the same name always gets the same tint)
|
|
31
|
+
const colorClass = computed(() => {
|
|
32
|
+
if (props.color === 'primary') return 'bg-primary text-primary-foreground';
|
|
33
|
+
if (props.color !== 'auto') return 'bg-surface text-muted-foreground';
|
|
34
|
+
const hash = [...(props.name ?? '')].reduce((total, character) => total + character.charCodeAt(0), 0);
|
|
35
|
+
return autoColorClasses[hash % autoColorClasses.length];
|
|
36
|
+
});
|
|
17
37
|
|
|
18
38
|
// Size and shape come from the parent avatar
|
|
19
39
|
const context = inject(avatarContextKey, null);
|
|
@@ -26,7 +46,7 @@ const initials = computed(() => getInitials(props.name));
|
|
|
26
46
|
|
|
27
47
|
<template>
|
|
28
48
|
<!-- With a name the initials act as an image of the person, so screen readers announce the name instead of the letters -->
|
|
29
|
-
<AvatarFallback data-slot="avatar-fallback" v-bind="delegatedProps" :role="props.name ? 'img' : undefined" :aria-label="props.name || undefined" :class="cn('flex size-full items-center justify-center
|
|
49
|
+
<AvatarFallback data-slot="avatar-fallback" v-bind="delegatedProps" :role="props.name ? 'img' : undefined" :aria-label="props.name || undefined" :class="cn('flex size-full items-center justify-center font-semibold uppercase', colorClass, textClass, shapeClass, props.class)">
|
|
30
50
|
<slot>{{ initials }}</slot>
|
|
31
51
|
</AvatarFallback>
|
|
32
52
|
</template>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { HTMLAttributes } from 'vue';
|
|
3
|
+
import { computed } from 'vue';
|
|
3
4
|
import { HugeiconsIcon } from '@hugeicons/vue';
|
|
4
|
-
import { getSurfaceToneClasses, type ToneColor } from '../../../lib/color-tokens';
|
|
5
|
+
import { getInteractiveSurfaceToneClasses, getSurfaceToneClasses, type ToneColor } from '../../../lib/color-tokens';
|
|
5
6
|
import { cn } from '../../../lib/utils';
|
|
6
7
|
import type { HugeiconsIconDefinition } from '../../../lib/common-types';
|
|
7
8
|
|
|
@@ -9,20 +10,36 @@ import type { HugeiconsIconDefinition } from '../../../lib/common-types';
|
|
|
9
10
|
const props = withDefaults(defineProps<{
|
|
10
11
|
text: string;
|
|
11
12
|
icon?: HugeiconsIconDefinition | null;
|
|
12
|
-
color?: ToneColor;
|
|
13
|
+
color?: ToneColor | 'primary';
|
|
13
14
|
dot?: boolean;
|
|
14
15
|
pulse?: boolean;
|
|
16
|
+
interactive?: boolean;
|
|
15
17
|
class?: HTMLAttributes['class'];
|
|
16
18
|
}>(), {
|
|
17
19
|
icon: null,
|
|
18
20
|
color: 'gray',
|
|
19
21
|
dot: false,
|
|
20
|
-
pulse: false
|
|
22
|
+
pulse: false,
|
|
23
|
+
interactive: false
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Surface of the badge: a tone, or a soft primary tint (for highlights like a category)
|
|
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}`;
|
|
21
38
|
});
|
|
22
39
|
</script>
|
|
23
40
|
|
|
24
41
|
<template>
|
|
25
|
-
<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',
|
|
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)">
|
|
26
43
|
<!-- Status dot -->
|
|
27
44
|
<span v-if="props.dot || props.pulse" class="relative flex size-1.5 shrink-0">
|
|
28
45
|
<span v-if="props.pulse" class="absolute inset-0 animate-uv-ping-soft rounded-full bg-current" />
|
|
@@ -14,7 +14,7 @@ const { t } = useI18n();
|
|
|
14
14
|
</script>
|
|
15
15
|
|
|
16
16
|
<template>
|
|
17
|
-
<span data-slot="breadcrumb-ellipsis" role="presentation" :class="cn('flex size-6 items-center justify-center rounded
|
|
17
|
+
<span data-slot="breadcrumb-ellipsis" role="presentation" :class="cn('flex size-6 items-center justify-center rounded 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" aria-hidden="true" />
|
|
@@ -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('rounded
|
|
14
|
+
<Primitive data-slot="breadcrumb-link" :as="as" :as-child="asChild" :class="cn('rounded 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>
|
|
@@ -4,6 +4,7 @@ import type { PrimitiveProps } from 'reka-ui';
|
|
|
4
4
|
import { computed, type HTMLAttributes } from 'vue';
|
|
5
5
|
import type { ButtonVariants } from '.';
|
|
6
6
|
import { Primitive } from 'reka-ui';
|
|
7
|
+
import { getSurfaceToneClasses } from '../../../lib/color-tokens';
|
|
7
8
|
import { cn } from '../../../lib/utils';
|
|
8
9
|
import { Spinner } from '../spinner';
|
|
9
10
|
import { buttonVariants } from '.';
|
|
@@ -15,11 +16,15 @@ const props = withDefaults(defineProps<PrimitiveProps & {
|
|
|
15
16
|
loading?: boolean;
|
|
16
17
|
disabled?: boolean;
|
|
17
18
|
to?: string;
|
|
19
|
+
count?: number | null;
|
|
20
|
+
countMax?: number;
|
|
18
21
|
class?: HTMLAttributes['class'];
|
|
19
22
|
}>(), {
|
|
20
23
|
as: 'button',
|
|
21
24
|
loading: false,
|
|
22
|
-
disabled: false
|
|
25
|
+
disabled: false,
|
|
26
|
+
count: null,
|
|
27
|
+
countMax: 99
|
|
23
28
|
});
|
|
24
29
|
|
|
25
30
|
// Disabled or loading: the button can't be pressed (or followed, for a navigation button)
|
|
@@ -35,6 +40,10 @@ const element = computed(() => {
|
|
|
35
40
|
return isAnchor.value ? 'a' : NuxtLinkLocale;
|
|
36
41
|
});
|
|
37
42
|
|
|
43
|
+
// Notification count shown in a badge on the corner (capped, like 99+)
|
|
44
|
+
const hasCount = computed(() => !props.asChild && typeof props.count === 'number' && props.count > 0);
|
|
45
|
+
const countText = computed(() => props.count && props.count > props.countMax ? `${props.countMax}+` : String(props.count));
|
|
46
|
+
|
|
38
47
|
// Link attribute for the rendered element (href for a plain anchor, to for the localized link)
|
|
39
48
|
const linkAttributes = computed(() => {
|
|
40
49
|
if (!props.to || isInactive.value) return {};
|
|
@@ -43,7 +52,7 @@ const linkAttributes = computed(() => {
|
|
|
43
52
|
</script>
|
|
44
53
|
|
|
45
54
|
<template>
|
|
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)">
|
|
55
|
+
<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 }), hasCount && 'overflow-visible', props.class)">
|
|
47
56
|
<!-- As-child buttons delegate rendering to the slotted element -->
|
|
48
57
|
<slot v-if="asChild" />
|
|
49
58
|
|
|
@@ -58,6 +67,13 @@ const linkAttributes = computed(() => {
|
|
|
58
67
|
<span v-if="props.loading" class="absolute inset-0 flex items-center justify-center">
|
|
59
68
|
<Spinner size="sm" />
|
|
60
69
|
</span>
|
|
70
|
+
|
|
71
|
+
<!-- Notification count: a soft badge on an opaque backing (so the tint never shows the button through), its digits trimmed to the cap height so they sit centered in any font -->
|
|
72
|
+
<span v-if="hasCount" data-slot="button-count" class="pointer-events-none absolute -top-2 -right-2 z-10 flex rounded bg-background">
|
|
73
|
+
<span :class="cn('flex h-5 min-w-5 items-center justify-center rounded px-1 text-[0.6875rem] font-semibold leading-none tabular-nums', getSurfaceToneClasses('red'))">
|
|
74
|
+
<span class="[text-box:trim-both_cap_alphabetic]">{{ countText }}</span>
|
|
75
|
+
</span>
|
|
76
|
+
</span>
|
|
61
77
|
</template>
|
|
62
78
|
</Primitive>
|
|
63
79
|
</template>
|
|
@@ -59,6 +59,10 @@ export const buttonVariants: (props?: ButtonVariants & ClassProp) => string = cv
|
|
|
59
59
|
'icon-lg': 'size-[52px]',
|
|
60
60
|
},
|
|
61
61
|
},
|
|
62
|
+
// Link buttons read as inline links, so they have no box padding
|
|
63
|
+
compoundVariants: [
|
|
64
|
+
{ variant: 'link', class: 'min-h-0 px-0 py-0 has-[>svg]:px-0' },
|
|
65
|
+
],
|
|
62
66
|
defaultVariants: {
|
|
63
67
|
variant: 'primary',
|
|
64
68
|
size: 'default',
|
|
@@ -8,16 +8,36 @@ import {
|
|
|
8
8
|
import { cn } from '../../../lib/utils';
|
|
9
9
|
|
|
10
10
|
type CardColor = 'default' | ToneColor;
|
|
11
|
+
type CardPadding = 'default' | 'none';
|
|
12
|
+
type CardElevation = 'none' | 'sm' | 'md' | 'lg' | 'xl';
|
|
11
13
|
|
|
12
14
|
// Props
|
|
13
15
|
const props = withDefaults(defineProps<{
|
|
14
16
|
color?: CardColor;
|
|
17
|
+
padding?: CardPadding;
|
|
18
|
+
elevation?: CardElevation;
|
|
15
19
|
interactive?: boolean;
|
|
16
20
|
class?: HTMLAttributes['class'];
|
|
17
21
|
}>(), {
|
|
22
|
+
padding: 'default',
|
|
18
23
|
interactive: false
|
|
19
24
|
});
|
|
20
25
|
|
|
26
|
+
// Shadow per elevation level
|
|
27
|
+
const elevationClasses: Record<CardElevation, string> = {
|
|
28
|
+
none: 'shadow-none',
|
|
29
|
+
sm: 'shadow-elevated-sm',
|
|
30
|
+
md: 'shadow-elevated-md',
|
|
31
|
+
lg: 'shadow-elevated-lg',
|
|
32
|
+
xl: 'shadow-elevated-xl'
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Inner spacing: the default fits CardHeader / CardContent / CardFooter, none is for cards that lay out their own sections edge to edge (clipped to the corners)
|
|
36
|
+
const paddingClasses: Record<CardPadding, string> = {
|
|
37
|
+
default: 'gap-4 sm:gap-6 py-6',
|
|
38
|
+
none: 'gap-0 overflow-hidden'
|
|
39
|
+
};
|
|
40
|
+
|
|
21
41
|
// Get the appropriate classes
|
|
22
42
|
const colorClasses = computed(() => {
|
|
23
43
|
// If color is not specified or is "default", use the default card styles
|
|
@@ -32,8 +52,12 @@ const colorClasses = computed(() => {
|
|
|
32
52
|
|
|
33
53
|
<template>
|
|
34
54
|
<div data-slot="card" :data-interactive="props.interactive ? '' : undefined" :class="cn(
|
|
35
|
-
'relative flex flex-col
|
|
55
|
+
'relative flex flex-col rounded transition-[border-color,box-shadow,background-color,translate] duration-300 ease-out-expo',
|
|
56
|
+
paddingClasses[props.padding],
|
|
57
|
+
// A CardMedia as first child bleeds to the top edge, clipped to the corners
|
|
58
|
+
'has-[>[data-slot=card-media]:first-child]:overflow-hidden has-[>[data-slot=card-media]:first-child]:pt-0',
|
|
36
59
|
colorClasses,
|
|
60
|
+
props.elevation && elevationClasses[props.elevation],
|
|
37
61
|
props.interactive && 'cursor-pointer hover:-translate-y-0.5 hover:shadow-elevated-md active:translate-y-0 active:duration-100',
|
|
38
62
|
props.class,
|
|
39
63
|
)">
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'vue';
|
|
3
|
+
import { cn } from '../../../lib/utils';
|
|
4
|
+
|
|
5
|
+
// Props (edge-to-edge media, like an image or a chart: as the first child of a Card it bleeds to the top edge)
|
|
6
|
+
const props = defineProps<{
|
|
7
|
+
class?: HTMLAttributes['class'];
|
|
8
|
+
}>();
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<div data-slot="card-media" :class="cn('overflow-hidden', props.class)">
|
|
13
|
+
<slot />
|
|
14
|
+
</div>
|
|
15
|
+
</template>
|