This commit is contained in:
2026-05-06 18:33:25 +08:00
parent 01b648a3e7
commit fd1b7730ad
26 changed files with 520 additions and 3 deletions
@@ -0,0 +1,20 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
data-slot="empty"
:class="cn(
'gap-4 rounded-xl border-dashed p-6 flex w-full min-w-0 flex-1 flex-col items-center justify-center text-center text-balance',
props.class,
)"
>
<slot />
</div>
</template>
@@ -0,0 +1,20 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
data-slot="empty-content"
:class="cn(
'gap-2.5 text-sm flex w-full max-w-sm min-w-0 flex-col items-center text-balance',
props.class,
)"
>
<slot />
</div>
</template>
@@ -0,0 +1,20 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<p
data-slot="empty-description"
:class="cn(
'text-sm/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary',
$attrs.class ?? '',
)"
>
<slot />
</p>
</template>
@@ -0,0 +1,20 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
data-slot="empty-header"
:class="cn(
'gap-2 flex max-w-sm flex-col items-center',
props.class,
)"
>
<slot />
</div>
</template>
@@ -0,0 +1,23 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import type { EmptyMediaVariants } from '.'
import { cn } from '@/lib/utils'
import { emptyMediaVariants } from '.'
const props = withDefaults(defineProps<{
class?: HTMLAttributes['class']
variant?: EmptyMediaVariants['variant']
}>(), {
variant: 'default',
})
</script>
<template>
<div
data-slot="empty-icon"
:data-variant="variant"
:class="cn(emptyMediaVariants({ variant }), props.class)"
>
<slot />
</div>
</template>
@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
data-slot="empty-title"
:class="cn('text-sm font-medium tracking-tight cn-font-heading', props.class)"
>
<slot />
</div>
</template>
@@ -0,0 +1,26 @@
import type { VariantProps } from 'class-variance-authority'
import { cva } from 'class-variance-authority'
export { default as Empty } from './Empty.vue'
export { default as EmptyContent } from './EmptyContent.vue'
export { default as EmptyDescription } from './EmptyDescription.vue'
export { default as EmptyHeader } from './EmptyHeader.vue'
export { default as EmptyMedia } from './EmptyMedia.vue'
export { default as EmptyTitle } from './EmptyTitle.vue'
export const emptyMediaVariants = cva(
'mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0',
{
variants: {
variant: {
default: 'bg-transparent',
icon: 'bg-muted text-foreground flex size-8 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*=size-])]:size-4',
},
},
defaultVariants: {
variant: 'default',
},
},
)
export type EmptyMediaVariants = VariantProps<typeof emptyMediaVariants>
@@ -0,0 +1,31 @@
<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { ItemVariants } from '.'
import { Primitive } from 'reka-ui'
import { cn } from '@/lib/utils'
import { itemVariants } from '.'
const props = withDefaults(defineProps<PrimitiveProps & {
class?: HTMLAttributes['class']
variant?: ItemVariants['variant']
size?: ItemVariants['size']
}>(), {
as: 'div',
variant: 'default',
size: 'default',
})
</script>
<template>
<Primitive
data-slot="item"
:data-variant="variant"
:data-size="size"
:as="as"
:as-child="asChild"
:class="cn(itemVariants({ variant, size }), props.class)"
>
<slot />
</Primitive>
</template>
@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
data-slot="item-actions"
:class="cn('gap-2 flex items-center', props.class)"
>
<slot />
</div>
</template>
@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
data-slot="item-content"
:class="cn('gap-1 group-data-[size=xs]/item:gap-0 flex flex-1 flex-col [&+[data-slot=item-content]]:flex-none', props.class)"
>
<slot />
</div>
</template>
@@ -0,0 +1,20 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<p
data-slot="item-description"
:class="cn(
'text-muted-foreground text-left text-sm leading-normal group-data-[size=xs]/item:text-xs line-clamp-2 font-normal [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary',
props.class,
)"
>
<slot />
</p>
</template>
@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
data-slot="item-footer"
:class="cn('gap-2 flex basis-full items-center justify-between', props.class)"
>
<slot />
</div>
</template>
@@ -0,0 +1,18 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
role="list"
data-slot="item-group"
:class="cn('gap-4 has-data-[size=sm]:gap-2.5 has-data-[size=xs]:gap-2 group/item-group flex w-full flex-col', props.class)"
>
<slot />
</div>
</template>
@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
data-slot="item-header"
:class="cn('gap-2 flex basis-full items-center justify-between', props.class)"
>
<slot />
</div>
</template>
@@ -0,0 +1,21 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import type { ItemMediaVariants } from '.'
import { cn } from '@/lib/utils'
import { itemMediaVariants } from '.'
const props = defineProps<{
class?: HTMLAttributes['class']
variant?: ItemMediaVariants['variant']
}>()
</script>
<template>
<div
data-slot="item-media"
:data-variant="props.variant"
:class="cn(itemMediaVariants({ variant }), props.class)"
>
<slot />
</div>
</template>
@@ -0,0 +1,18 @@
<script setup lang="ts">
import type { SeparatorProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { Separator } from '@/components/ui/separator'
const props = defineProps<
SeparatorProps & { class?: HTMLAttributes['class'] }
>()
</script>
<template>
<Separator
data-slot="item-separator"
orientation="horizontal"
:class="cn('my-2', props.class)"
/>
</template>
@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
data-slot="item-title"
:class="cn('gap-2 text-sm leading-snug font-medium underline-offset-4 cn-font-heading line-clamp-1 flex w-fit items-center', props.class)"
>
<slot />
</div>
</template>
@@ -0,0 +1,54 @@
import type { VariantProps } from 'class-variance-authority'
import { cva } from 'class-variance-authority'
export { default as Item } from './Item.vue'
export { default as ItemActions } from './ItemActions.vue'
export { default as ItemContent } from './ItemContent.vue'
export { default as ItemDescription } from './ItemDescription.vue'
export { default as ItemFooter } from './ItemFooter.vue'
export { default as ItemGroup } from './ItemGroup.vue'
export { default as ItemHeader } from './ItemHeader.vue'
export { default as ItemMedia } from './ItemMedia.vue'
export { default as ItemSeparator } from './ItemSeparator.vue'
export { default as ItemTitle } from './ItemTitle.vue'
export const itemVariants = cva(
'[a]:hover:bg-muted rounded-lg border text-sm group/item flex w-full flex-wrap items-center transition-colors duration-100 outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 [a]:transition-colors',
{
variants: {
variant: {
default: 'border-transparent',
outline: 'border-border',
muted: 'bg-muted/50 border-transparent',
},
size: {
default: 'gap-2.5 px-3 py-2.5',
sm: 'gap-2.5 px-3 py-2.5',
xs: 'gap-2 px-2.5 py-2 in-data-[slot=dropdown-menu-content]:p-0',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)
export const itemMediaVariants = cva(
'gap-2 group-has-data-[slot=item-description]/item:translate-y-0.5 group-has-data-[slot=item-description]/item:self-start flex shrink-0 items-center justify-center [&_svg]:pointer-events-none',
{
variants: {
variant: {
default: 'bg-transparent',
icon: '[&_svg:not([class*=size-])]:size-4',
image: 'size-10 overflow-hidden rounded-sm group-data-[size=sm]/item:size-8 group-data-[size=xs]/item:size-6 [&_img]:size-full [&_img]:object-cover',
},
},
defaultVariants: {
variant: 'default',
},
},
)
export type ItemVariants = VariantProps<typeof itemVariants>
export type ItemMediaVariants = VariantProps<typeof itemMediaVariants>
@@ -0,0 +1,38 @@
<script setup lang="ts">
import type { ProgressRootProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import {
ProgressIndicator,
ProgressRoot,
} from 'reka-ui'
import { cn } from '@/lib/utils'
const props = withDefaults(
defineProps<ProgressRootProps & { class?: HTMLAttributes['class'] }>(),
{
modelValue: 0,
},
)
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<ProgressRoot
data-slot="progress"
v-bind="delegatedProps"
:class="
cn(
'bg-muted h-1 rounded-full relative flex w-full items-center overflow-x-hidden',
props.class,
)
"
>
<ProgressIndicator
data-slot="progress-indicator"
class="bg-primary size-full flex-1 transition-all"
:style="`transform: translateX(-${100 - (props.modelValue ?? 0)}%);`"
/>
</ProgressRoot>
</template>
@@ -0,0 +1 @@
export { default as Progress } from './Progress.vue'
@@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { Loader2Icon } from 'lucide-vue-next'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<Loader2Icon role="status" aria-label="Loading" :class="cn('size-4 animate-spin', props.class)" />
</template>
@@ -0,0 +1 @@
export { default as Spinner } from './Spinner.vue'