Merge remote-tracking branch 'origin/dev' into charting

This commit is contained in:
zernonia 2024-03-12 10:17:08 +08:00
commit 73f1d8f2f0
191 changed files with 7230 additions and 2648 deletions

View File

@ -1,7 +1,6 @@
{
"recommendations": [
"Vue.volar",
"Vue.vscode-typescript-vue-plugin",
"dbaeumer.vscode-eslint"
]
}

View File

@ -74,7 +74,7 @@ export default defineConfig({
css: {
postcss: {
plugins: [
tailwind(),
tailwind() as any,
autoprefixer(),
],
},

View File

@ -15,7 +15,7 @@ const { copy, copied } = useClipboard()
const codeRef = ref<HTMLElement>()
async function copyCode() {
await copy(codeRef.value?.innerText.replace(/\u00A0/g, ' ') ?? '')
await copy(codeRef.value?.textContent?.replace(/\u00A0/g, ' ') ?? '')
}
</script>

View File

@ -8,6 +8,11 @@ import ArrowRightIcon from '~icons/radix-icons/arrow-right'
const { path } = toRefs(useRoute())
const examples = [
{
name: 'Mail',
href: '/examples/mail',
code: 'https://github.com/radix-vue/shadcn-vue/tree/dev/apps/www/src/examples/mail',
},
{
name: 'Dashboard',
href: '/examples/dashboard',
@ -58,7 +63,7 @@ const currentExample = computed(() => examples.find(ex => path.value.startsWith(
:href="example.href"
:class="cn(
'flex items-center px-4',
path?.startsWith(example.href) || (path === '/' && example.name === 'Dashboard')
path?.startsWith(example.href) || (path === '/' && example.name === 'Mail')
? 'font-bold text-primary'
: 'font-medium text-muted-foreground',
)"

View File

@ -0,0 +1,54 @@
<script lang="ts" setup>
import type { Color } from '../types/colors'
import { useConfigStore } from '@/stores/config'
import { colors } from '@/lib/registry'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/lib/registry/new-york/ui/tooltip'
import RadixIconsCheck from '~icons/radix-icons/check'
defineProps<{
allColors: Color[]
}>()
const { theme, setTheme } = useConfigStore()
</script>
<template>
<div>
<TooltipProvider
v-for="(color, index) in allColors.slice(0, 5)"
:key="index"
>
<Tooltip>
<TooltipTrigger as-child>
<button
:key="index"
class="flex h-9 w-9 items-center justify-center rounded-full border-2 border-border text-xs"
:class="
color === theme
? 'border-primary'
: 'border-transparent'
"
@click="setTheme(color)"
>
<span
class="flex h-6 w-6 items-center justify-center rounded-full"
:style="{ backgroundColor: colors[color][6].rgb }"
>
<RadixIconsCheck
v-if="color === theme"
class="h-4 w-4 text-white"
/>
</span>
</button>
</TooltipTrigger>
<TooltipContent
align="center"
:side-offset="1"
class="capitalize bg-zinc-900 text-zinc-50"
>
{{ allColors[index] }}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</template>

View File

@ -11,7 +11,7 @@ import { buttonVariants } from '@/lib/registry/new-york/ui/button'
import { Separator } from '@/lib/registry/new-york/ui/separator'
import { cn } from '@/lib/utils'
import DashboardExample from '@/examples/dashboard/Example.vue'
import MailExample from '@/examples/mail/Example.vue'
</script>
<template>
@ -55,17 +55,17 @@ import DashboardExample from '@/examples/dashboard/Example.vue'
<ExamplesNav />
<section class="space-y-8 overflow-hidden rounded-lg border-2 border-primary dark:border-muted md:hidden">
<VPImage
alt="Dashboard"
alt="Mail"
width="1280"
height="866" class="block" :image="{
dark: '/examples/dashboard-dark.png',
light: '/examples/dashboard-light.png',
dark: '/examples/mail-dark.png',
light: '/examples/mail-light.png',
}"
/>
</section>
<section class="hidden md:block">
<div class="overflow-hidden rounded-[0.5rem] border bg-background shadow">
<DashboardExample />
<MailExample />
</div>
</section>
</template>

View File

@ -2,7 +2,7 @@
</script>
<template>
<a href="/" class="mr-6 flex items-center space-x-2">
<a href="/" class="mr-4 md:mr-2 xl:mr-6 flex items-center lg:space-x1 xl:space-x-2">
<svg class="h-6 w-6" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_102_1338)">
<path d="M208 128L128 208" stroke="#41B883" stroke-width="16" stroke-linecap="round" stroke-linejoin="round" />
@ -15,7 +15,7 @@
</defs>
</svg>
<span class="font-bold ">
<span class="font-bold">
shadcn-vue
</span>
</a>

View File

@ -5,8 +5,6 @@ import Logo from './Logo.vue'
import { Sheet, SheetContent, SheetTrigger } from '@/lib/registry/default/ui/sheet'
import { Button } from '@/lib/registry/default/ui/button'
import { ScrollArea } from '@/lib/registry/default/ui/scroll-area'
import { Badge } from '@/lib/registry/default/ui/badge'
import ViewVerticalIcon from '~icons/radix-icons/view-vertical'
const open = ref(false)
</script>
@ -18,7 +16,35 @@ const open = ref(false)
variant="ghost"
class="mr-2 px-2 text-base flex-shrink-0 hover:bg-transparent focus-visible:bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 md:hidden"
>
<ViewVerticalIcon class="h-5 w-5" />
<svg
strokeWidth="1.5"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
>
<path
d="M3 5H11"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M3 12H16"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M3 19H21"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<span class="sr-only">Toggle Menu</span>
</Button>
</SheetTrigger>

View File

@ -5,6 +5,7 @@ import type { TableOfContents, TableOfContentsItem } from '../types/docs'
import TableOfContentTree from './TableOfContentTree.vue'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/lib/registry/default/ui/collapsible'
import { buttonVariants } from '@/lib/registry/default/ui/button'
import { ScrollArea } from '@/lib/registry/default/ui/scroll-area'
const headers = shallowRef<TableOfContents>()
@ -22,7 +23,7 @@ function getHeadingsWithHierarchy(divId: string) {
headings.forEach((heading: HTMLHeadingElement) => {
const level = Number.parseInt(heading.tagName.charAt(1))
if (!heading.id) {
const newId = heading.innerText
const newId = heading.textContent
.replaceAll(/[^a-zA-Z0-9 ]/g, '')
.replaceAll(' ', '-')
.toLowerCase()
@ -55,11 +56,15 @@ onContentUpdated(() => {
</script>
<template>
<div class="space-y-2 hidden xl:block">
<p class="font-medium">
On This Page
</p>
<TableOfContentTree :tree="headers" :level="1" />
<div class="hidden xl:block">
<ScrollArea orientation="vertical" class="h-[calc(100vh-6.5rem)] z-30 md:block overflow-y-auto" type="hover">
<div class="space-y-2">
<p class="font-medium">
On This Page
</p>
<TableOfContentTree :tree="headers" :level="1" />
</div>
</ScrollArea>
</div>
<div class="block xl:hidden mb-6">

View File

@ -0,0 +1,106 @@
<script lang="ts" setup>
import { useData } from 'vitepress'
import type { Color } from '../types/colors'
import { RADII, useConfigStore } from '@/stores/config'
import { Button } from '@/lib/registry/new-york/ui/button'
import { Label } from '@/lib/registry/new-york/ui/label'
import { colors } from '@/lib/registry'
import RadixIconsCheck from '~icons/radix-icons/check'
import RadixIconsSun from '~icons/radix-icons/sun'
import RadixIconsMoon from '~icons/radix-icons/moon'
defineProps<{
allColors: Color[]
}>()
const { theme, radius, setRadius, setTheme } = useConfigStore()
const { isDark } = useData()
</script>
<template>
<div class="p-4">
<div class="grid space-y-1">
<h1 class="text-md text-foreground font-semibold">
Customize
</h1>
<p class="text-xs text-muted-foreground">
Pick a style and color for your components.
</p>
</div>
<div class="space-y-1.5 pt-6">
<Label for="color" class="text-xs"> Color </Label>
<div class="grid grid-cols-3 gap-2 py-1.5">
<Button
v-for="(color, index) in allColors"
:key="index"
variant="outline"
class="h-8 justify-start px-3"
:class="
color === theme
? 'border-foreground border-2'
: ''
"
@click="setTheme(color)"
>
<span
class="h-5 w-5 rounded-full flex items-center justify-center"
:style="{ backgroundColor: colors[color][7].rgb }"
>
<RadixIconsCheck
v-if="color === theme"
class="h-3 w-3 text-white"
/>
</span>
<span class="ml-2 text-xs capitalize">
{{ color }}
</span>
</Button>
</div>
</div>
<div class="space-y-1.5 pt-6">
<Label for="radius" class="text-xs"> Radius </Label>
<div class="grid grid-cols-5 gap-2 py-1.5">
<Button
v-for="(r, index) in RADII"
:key="index"
variant="outline"
class="h-8 justify-center px-3"
:class="
r === radius
? 'border-foreground border-2'
: ''
"
@click="setRadius(r)"
>
<span class="text-xs">
{{ r }}
</span>
</Button>
</div>
</div>
<div class="space-y-1.5 pt-6">
<Label for="theme" class="text-xs"> Theme </Label>
<div class="flex space-x-2 py-1.5">
<Button
class="h-8"
variant="outline"
:class="{ 'border-2 border-foreground': !isDark }"
@click="isDark = false"
>
<RadixIconsSun class="w-4 h-4 mr-2" />
<span class="text-xs">Light</span>
</Button>
<Button
class="h-8"
variant="outline"
:class="{ 'border-2 border-foreground': isDark }"
@click="isDark = true"
>
<RadixIconsMoon class="w-4 h-4 mr-2" />
<span class="text-xs">Dark</span>
</Button>
</div>
</div>
</div>
</template>

View File

@ -14,7 +14,7 @@ import CardChat from '@/lib/registry/new-york/example/CardChat.vue'
import ActivityGoal from '@/lib/registry/new-york/example/Cards/ActivityGoal.vue'
import Metric from '@/lib/registry/new-york/example/Cards/Metric.vue'
import DataTable from '@/lib/registry/new-york/example/Cards/DataTable.vue'
import CardStats from '@/lib/registry/default/example/CardStats.vue'
import CardStats from '@/lib/registry/new-york/example/CardStats.vue'
import { Card } from '@/lib/registry/new-york/ui/card'
import { Calendar } from '@/lib/registry/new-york/ui/calendar'

View File

@ -23,7 +23,7 @@ interface DocsConfig {
export const docsConfig: DocsConfig = {
mainNav: [
{
title: 'Documentation',
title: 'Docs',
href: '/docs/introduction',
},
{
@ -36,7 +36,7 @@ export const docsConfig: DocsConfig = {
},
{
title: 'Examples',
href: '/examples/dashboard',
href: '/examples/mail',
},
{
title: 'GitHub',
@ -64,6 +64,11 @@ export const docsConfig: DocsConfig = {
title: 'Theming',
href: '/docs/theming',
},
{
title: 'Dark Mode',
href: '/docs/dark-mode',
items: [],
},
{
title: 'CLI',
href: '/docs/cli',
@ -206,6 +211,12 @@ export const docsConfig: DocsConfig = {
title: 'Dialog',
href: '/docs/components/dialog',
},
{
title: 'Drawer',
href: '/docs/components/drawer',
items: [],
label: 'New',
},
{
title: 'Dropdown Menu',
href: '/docs/components/dropdown-menu',
@ -256,6 +267,12 @@ export const docsConfig: DocsConfig = {
title: 'Radio Group',
href: '/docs/components/radio-group',
},
{
title: 'Resizable',
href: '/docs/components/resizable',
label: 'New',
items: [],
},
{
title: 'Scroll Area',
href: '/docs/components/scroll-area',
@ -336,6 +353,11 @@ interface Example {
code: string
}
export const examples: Example[] = [
{
name: 'Mail',
href: '/examples/mail',
code: 'https://github.com/radix-vue/shadcn-vue/tree/dev/apps/www/src/examples/mail',
},
{
name: 'Dashboard',
href: '/examples/dashboard',

View File

@ -1,4 +1,3 @@
/* eslint-disable vue/component-definition-name-casing */
// https://vitepress.dev/guide/custom-theme
import Layout from './layout/MainLayout.vue'
import DocsLayout from './layout/DocsLayout.vue'

View File

@ -85,14 +85,12 @@ watch(() => $route.path, (n) => {
</script>
<template>
<div class="flex min-h-screen flex-col bg-background">
<div vaul-drawer-wrapper class="flex min-h-screen flex-col bg-background">
<header class="sticky z-40 top-0 bg-background/80 backdrop-blur-lg border-b border-border">
<div
class="container flex justify-between h-14 max-w-screen-2xl items-center"
class="container flex h-14 max-w-screen-2xl items-center"
>
<MobileNav />
<div class="mr-4 hidden md:flex">
<div class="mr-4 md:mr-1 hidden md:flex">
<Logo />
<nav
@ -106,30 +104,33 @@ watch(() => $route.path, (n) => {
class="transition-colors hover:text-foreground/80 text-foreground/60"
:class="{
'font-semibold !text-foreground': $route.path === `${route.href}.html`,
'hidden lg:block': route?.href?.includes('github'),
}"
>
{{ route.title }}
</a>
</nav>
</div>
<MobileNav />
<div class=" flex items-center justify-end space-x-2 ">
<Button
variant="outline"
class="w-72 h-9 px-3 hidden lg:flex lg:justify-between lg:items-center"
@click="isOpen = true"
>
<div class="flex items-center">
<SearchIcon class="w-4 h-4 mr-2 text-muted-foreground" />
<span class="text-muted-foreground"> Search for anything... </span>
</div>
<div class="flex items-center gap-x-1">
<Kbd> <span></span>K </Kbd>
</div>
</Button>
<ThemePopover />
<div class="flex flex-1 items-center justify-between space-x-2 md:justify-end">
<div class="w-full flex-1 md:w-auto md:flex-none">
<Button
variant="outline"
class="relative h-8 w-full justify-start rounded-[0.5rem] bg-background text-sm font-normal text-muted-foreground shadow-none sm:pr-12 md:w-40 lg:w-64"
@click="isOpen = true"
>
<span className="hidden lg:inline-flex">Search documentation...</span>
<span className="inline-flex lg:hidden">Search...</span>
<kbd className="pointer-events-none absolute right-[0.3rem] top-[0.3rem] hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex">
<span className="text-xs"></span>K
</kbd>
</Button>
</div>
<nav class="flex items-center gap-x-1">
<ThemePopover />
<div class="flex items-center gap-x-1">
<Button
v-for="link in links"
:key="link.name"
@ -154,7 +155,7 @@ watch(() => $route.path, (n) => {
/>
</Button>
</ClientOnly>
</div>
</nav>
</div>
</div>
</header>
@ -293,9 +294,7 @@ watch(() => $route.path, (n) => {
</DialogContent>
</Dialog>
<DefaultToaster />
<ClientOnly>
<NewYorkSonner :theme="isDark ? 'dark' : 'light'" />
</ClientOnly>
<NewYorkSonner :theme="'system'" />
<NewYorkToaster />
</div>
</template>

View File

@ -1,96 +1,117 @@
<script setup lang="ts">
import { onMounted, watch } from 'vue'
import { Paintbrush } from 'lucide-vue-next'
import PageHeader from '../components/PageHeader.vue'
import PageHeaderHeading from '../components/PageHeaderHeading.vue'
import PageHeaderDescription from '../components/PageHeaderDescription.vue'
import CustomizerCode from '../components/CustomizerCode.vue'
import ThemePopover from '../components/ThemePopover.vue'
import { allColors } from '../components/theming/utils/data'
import type { Color } from '../types/colors'
import ThemeCustomizer from '../components/ThemeCustomizer.vue'
import InlineThemePicker from '../components/InlineThemePicker.vue'
import PageAction from '../components/PageAction.vue'
import { useConfigStore } from '@/stores/config'
import { colors } from '@/lib/registry'
import { Button } from '@/lib/registry/new-york/ui/button'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/lib/registry/new-york/ui/tooltip'
import { Popover, PopoverContent, PopoverTrigger } from '@/lib/registry/new-york/ui/popover'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/lib/registry/new-york/ui/dialog'
import RadixIconsCheck from '~icons/radix-icons/check'
import { Drawer, DrawerContent, DrawerTrigger } from '@/lib/registry/new-york/ui/drawer'
const { theme, setTheme } = useConfigStore()
// Create an array of color values
const allColors: Color[] = [
'zinc',
'rose',
'blue',
'green',
'orange',
'red',
'slate',
'stone',
'gray',
'neutral',
'yellow',
'violet',
]
const { theme, radius } = useConfigStore()
// Whenever the component is mounted, update the document class list
onMounted(() => {
document.documentElement.style.setProperty('--radius', `${radius.value}rem`)
document.documentElement.classList.add(`theme-${theme.value}`)
})
// Whenever the theme value changes, update the document class list
watch(theme, (theme) => {
document.documentElement.classList.remove(
...allColors.map(color => `theme-${color}`),
)
document.documentElement.classList.add(`theme-${theme}`)
})
// Whenever the radius value changes, update the document style
watch(radius, (radius) => {
document.documentElement.style.setProperty('--radius', `${radius}rem`)
})
</script>
<template>
<div class="container relative">
<div class="flex justify-between items-center">
<div>
<PageHeader class="page-header pb-8">
<PageHeaderHeading class="hidden md:block">
Make it yours.
</PageHeaderHeading>
<PageHeaderDescription>
Hand-picked themes that you can copy and paste into your apps.
</PageHeaderDescription>
</PageHeader>
</div>
<div class="px-4 pb-8 md:ml-auto md:pb-0">
<div class="flex items-center space-x-2">
<div class="hidden md:flex">
<div class="mr-4 hidden items-center space-x-1 lg:flex">
<TooltipProvider
v-for="(color, index) in allColors.slice(0, 5)"
:key="index"
>
<Tooltip>
<TooltipTrigger as-child>
<button
:key="index"
class="flex h-9 w-9 items-center justify-center rounded-full border-2 border-border text-xs"
:class="
color === theme
? 'border-primary'
: 'border-transparent'
"
@click="setTheme(color)"
>
<span
class="flex h-6 w-6 items-center justify-center rounded-full"
:style="{ backgroundColor: colors[color][6].rgb }"
>
<RadixIconsCheck
v-if="color === theme"
class="h-4 w-4 text-white"
/>
</span>
</button>
</TooltipTrigger>
<TooltipContent
align="center"
:side-offset="1"
class="capitalize bg-zinc-900 text-zinc-50"
>
{{ allColors[index] }}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<ThemePopover title="Customize" />
<Dialog>
<DialogTrigger as-child>
<Button class="h-9 ml-2 rounded-[0.5rem]">
Copy code
</Button>
</DialogTrigger>
<DialogContent class="sm:max-w-[625px]">
<DialogHeader>
<DialogTitle>Theme</DialogTitle>
<DialogDescription>
Copy and paste the following code into your CSS file.
</DialogDescription>
</DialogHeader>
<CustomizerCode />
</DialogContent>
</Dialog>
</div>
</div>
</div>
</div>
<PageHeader>
<PageHeaderHeading class="hidden md:block">
Add colors. Make it yours.
</PageHeaderHeading>
<PageHeaderHeading class="md:hidden">
Make it yours
</PageHeaderHeading>
<PageHeaderDescription>
Hand-picked themes that you can copy and paste into your apps.
</PageHeaderDescription>
<PageAction>
<InlineThemePicker class="gap-x-1 me-4 hidden lg:flex" :all-colors="allColors" />
<Drawer>
<DrawerTrigger as-child>
<Button variant="outline" class="md:hidden h-9 rounded-[0.5rem]">
<Paintbrush class="w-4 h-4 mr-2" />
Customize
</Button>
</DrawerTrigger>
<DrawerContent class="p-6 pt-0">
<ThemeCustomizer :all-colors="allColors" />
</DrawerContent>
</Drawer>
<Popover>
<PopoverTrigger as-child>
<Button variant="outline" class="hidden md:flex h-9 rounded-[0.5rem]">
<Paintbrush class="w-4 h-4 mr-2" />
Customize
</Button>
</PopoverTrigger>
<PopoverContent :side-offset="8" align="end" class="w-96">
<ThemeCustomizer :all-colors="allColors" />
</PopoverContent>
</Popover>
<Dialog>
<DialogTrigger as-child>
<Button class="h-9 ml-2 rounded-[0.5rem]">
Copy code
</Button>
</DialogTrigger>
<DialogContent class="sm:max-w-[625px]">
<DialogHeader>
<DialogTitle>Theme</DialogTitle>
<DialogDescription>
Copy and paste the following code into your CSS file.
</DialogDescription>
</DialogHeader>
<CustomizerCode />
</DialogContent>
</Dialog>
</PageAction>
</PageHeader>
<section>
<slot />
</section>

View File

@ -7,26 +7,26 @@
--font-geist-sans: "geist-sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 72.22% 50.59%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 5% 64.9%;
--radius: 0.5rem;
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 72.22% 50.59%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 5% 64.9%;
--radius: 0.5rem;
--vis-tooltip-background-color: none !important;
--vis-tooltip-border-color: none !important;
@ -68,7 +68,9 @@
}
body {
@apply bg-background text-foreground min-h-screen antialiased font-sans;
font-feature-settings: "rlig" 1, "calt" 1;
/* font-feature-settings: "rlig" 1, "calt" 1; */
font-synthesis-weight: none;
text-rendering: optimizeLegibility;
}
/* Mobile tap highlight */

View File

@ -0,0 +1,13 @@
export type Color =
| 'zinc'
| 'slate'
| 'stone'
| 'gray'
| 'neutral'
| 'red'
| 'rose'
| 'orange'
| 'green'
| 'blue'
| 'yellow'
| 'violet'

View File

@ -4,7 +4,7 @@ import { dependencies as deps } from '../../../package.json'
import { Index as demoIndex } from '../../../../www/__registry__'
import tailwindConfigRaw from '../../../tailwind.config?raw'
import cssRaw from '../../../../../packages/cli/test/fixtures/nuxt/assets/css/tailwind.css?raw'
import { type Style } from '@/lib/registry/styles'
import type { Style } from '@/lib/registry/styles'
export function makeCodeSandboxParams(componentName: string, style: Style, sources: Record<string, string>) {
let files: Record<string, any> = {}
@ -54,7 +54,7 @@ export default defineConfig({
<title>Vite + Vue + TS</title>
</head>
<body>
<div id="app"></div>
<div vaul-drawer-wrapper id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
@ -90,6 +90,9 @@ function constructFiles(componentName: string, style: Style, sources: Record<str
[iconPackage]: 'latest',
'shadcn-vue': 'latest',
'typescript': 'latest',
'vaul-vue': 'latest',
'@unovis/vue': 'latest',
'@unovis/ts': 'latest',
}
const devDependencies = {
@ -164,7 +167,6 @@ function constructFiles(componentName: string, style: Style, sources: Record<str
isBinary: false,
content: `import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
import { camelize, getCurrentInstance, toHandlerKey } from 'vue'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))

View File

@ -241,6 +241,13 @@ export const Index = {
component: () => import("../src/lib/registry/default/example/CarouselSpacing.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/CarouselSpacing.vue"],
},
"CarouselThumbnails": {
name: "CarouselThumbnails",
type: "components:example",
registryDependencies: ["carousel","card"],
component: () => import("../src/lib/registry/default/example/CarouselThumbnails.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/CarouselThumbnails.vue"],
},
"CheckboxDemo": {
name: "CheckboxDemo",
type: "components:example",
@ -311,6 +318,13 @@ export const Index = {
component: () => import("../src/lib/registry/default/example/ComboboxPopover.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/ComboboxPopover.vue"],
},
"ComboboxResponsive": {
name: "ComboboxResponsive",
type: "components:example",
registryDependencies: ["button","command","drawer","popover"],
component: () => import("../src/lib/registry/default/example/ComboboxResponsive.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/ComboboxResponsive.vue"],
},
"CommandDemo": {
name: "CommandDemo",
type: "components:example",
@ -416,12 +430,26 @@ export const Index = {
component: () => import("../src/lib/registry/default/example/DialogScrollOverlayDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/DialogScrollOverlayDemo.vue"],
},
"DonutChartDemo": {
name: "DonutChartDemo",
"DrawerDemo": {
name: "DrawerDemo",
type: "components:example",
registryDependencies: ["chart-donut"],
component: () => import("../src/lib/registry/default/example/DonutChartDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/DonutChartDemo.vue"],
registryDependencies: ["button","drawer"],
component: () => import("../src/lib/registry/default/example/DrawerDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/DrawerDemo.vue"],
},
"DrawerDialog": {
name: "DrawerDialog",
type: "components:example",
registryDependencies: ["button","dialog","drawer","label","input"],
component: () => import("../src/lib/registry/default/example/DrawerDialog.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/DrawerDialog.vue"],
},
"DropdownMenuCheckboxes": {
name: "DropdownMenuCheckboxes",
type: "components:example",
registryDependencies: ["button","dropdown-menu"],
component: () => import("../src/lib/registry/default/example/DropdownMenuCheckboxes.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/DropdownMenuCheckboxes.vue"],
},
"DropdownMenuDemo": {
name: "DropdownMenuDemo",
@ -430,6 +458,13 @@ export const Index = {
component: () => import("../src/lib/registry/default/example/DropdownMenuDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/DropdownMenuDemo.vue"],
},
"DropdownMenuRadioGroup": {
name: "DropdownMenuRadioGroup",
type: "components:example",
registryDependencies: ["button","dropdown-menu"],
component: () => import("../src/lib/registry/default/example/DropdownMenuRadioGroup.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/DropdownMenuRadioGroup.vue"],
},
"HoverCardDemo": {
name: "HoverCardDemo",
type: "components:example",
@ -535,6 +570,13 @@ export const Index = {
component: () => import("../src/lib/registry/default/example/PaginationDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/PaginationDemo.vue"],
},
"PinInputControlled": {
name: "PinInputControlled",
type: "components:example",
registryDependencies: ["pin-input"],
component: () => import("../src/lib/registry/default/example/PinInputControlled.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/PinInputControlled.vue"],
},
"PinInputDemo": {
name: "PinInputDemo",
type: "components:example",
@ -542,6 +584,13 @@ export const Index = {
component: () => import("../src/lib/registry/default/example/PinInputDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/PinInputDemo.vue"],
},
"PinInputDisabled": {
name: "PinInputDisabled",
type: "components:example",
registryDependencies: ["pin-input"],
component: () => import("../src/lib/registry/default/example/PinInputDisabled.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/PinInputDisabled.vue"],
},
"PinInputFormDemo": {
name: "PinInputFormDemo",
type: "components:example",
@ -549,6 +598,13 @@ export const Index = {
component: () => import("../src/lib/registry/default/example/PinInputFormDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/PinInputFormDemo.vue"],
},
"PinInputSeparatorDemo": {
name: "PinInputSeparatorDemo",
type: "components:example",
registryDependencies: ["pin-input"],
component: () => import("../src/lib/registry/default/example/PinInputSeparatorDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/PinInputSeparatorDemo.vue"],
},
"PopoverDemo": {
name: "PopoverDemo",
type: "components:example",
@ -584,6 +640,27 @@ export const Index = {
component: () => import("../src/lib/registry/default/example/RangePickerWithSlot.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/RangePickerWithSlot.vue"],
},
"ResizableDemo": {
name: "ResizableDemo",
type: "components:example",
registryDependencies: ["resizable"],
component: () => import("../src/lib/registry/default/example/ResizableDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/ResizableDemo.vue"],
},
"ResizableHandleDemo": {
name: "ResizableHandleDemo",
type: "components:example",
registryDependencies: ["resizable"],
component: () => import("../src/lib/registry/default/example/ResizableHandleDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/ResizableHandleDemo.vue"],
},
"ResizableVerticalDemo": {
name: "ResizableVerticalDemo",
type: "components:example",
registryDependencies: ["resizable"],
component: () => import("../src/lib/registry/default/example/ResizableVerticalDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/ResizableVerticalDemo.vue"],
},
"ScrollAreaDemo": {
name: "ScrollAreaDemo",
type: "components:example",
@ -612,6 +689,13 @@ export const Index = {
component: () => import("../src/lib/registry/default/example/SelectForm.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/SelectForm.vue"],
},
"SelectScrollable": {
name: "SelectScrollable",
type: "components:example",
registryDependencies: ["select"],
component: () => import("../src/lib/registry/default/example/SelectScrollable.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/SelectScrollable.vue"],
},
"SeparatorDemo": {
name: "SeparatorDemo",
type: "components:example",
@ -633,6 +717,13 @@ export const Index = {
component: () => import("../src/lib/registry/default/example/SheetSideDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/SheetSideDemo.vue"],
},
"SkeletonCard": {
name: "SkeletonCard",
type: "components:example",
registryDependencies: ["skeleton"],
component: () => import("../src/lib/registry/default/example/SkeletonCard.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/SkeletonCard.vue"],
},
"SkeletonDemo": {
name: "SkeletonDemo",
type: "components:example",
@ -647,6 +738,13 @@ export const Index = {
component: () => import("../src/lib/registry/default/example/SliderDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/SliderDemo.vue"],
},
"SliderForm": {
name: "SliderForm",
type: "components:example",
registryDependencies: ["button","form","slider","toast"],
component: () => import("../src/lib/registry/default/example/SliderForm.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/SliderForm.vue"],
},
"SonnerDemo": {
name: "SonnerDemo",
type: "components:example",
@ -1222,6 +1320,13 @@ export const Index = {
component: () => import("../src/lib/registry/new-york/example/CarouselSpacing.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/CarouselSpacing.vue"],
},
"CarouselThumbnails": {
name: "CarouselThumbnails",
type: "components:example",
registryDependencies: ["carousel","card"],
component: () => import("../src/lib/registry/new-york/example/CarouselThumbnails.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/CarouselThumbnails.vue"],
},
"CheckboxDemo": {
name: "CheckboxDemo",
type: "components:example",
@ -1292,6 +1397,13 @@ export const Index = {
component: () => import("../src/lib/registry/new-york/example/ComboboxPopover.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/ComboboxPopover.vue"],
},
"ComboboxResponsive": {
name: "ComboboxResponsive",
type: "components:example",
registryDependencies: ["button","command","drawer","popover"],
component: () => import("../src/lib/registry/new-york/example/ComboboxResponsive.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/ComboboxResponsive.vue"],
},
"CommandDemo": {
name: "CommandDemo",
type: "components:example",
@ -1397,12 +1509,26 @@ export const Index = {
component: () => import("../src/lib/registry/new-york/example/DialogScrollOverlayDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/DialogScrollOverlayDemo.vue"],
},
"DonutChartDemo": {
name: "DonutChartDemo",
"DrawerDemo": {
name: "DrawerDemo",
type: "components:example",
registryDependencies: ["chart-donut"],
component: () => import("../src/lib/registry/new-york/example/DonutChartDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/DonutChartDemo.vue"],
registryDependencies: ["button","drawer"],
component: () => import("../src/lib/registry/new-york/example/DrawerDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/DrawerDemo.vue"],
},
"DrawerDialog": {
name: "DrawerDialog",
type: "components:example",
registryDependencies: ["button","dialog","drawer","label","input"],
component: () => import("../src/lib/registry/new-york/example/DrawerDialog.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/DrawerDialog.vue"],
},
"DropdownMenuCheckboxes": {
name: "DropdownMenuCheckboxes",
type: "components:example",
registryDependencies: ["button","dropdown-menu"],
component: () => import("../src/lib/registry/new-york/example/DropdownMenuCheckboxes.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/DropdownMenuCheckboxes.vue"],
},
"DropdownMenuDemo": {
name: "DropdownMenuDemo",
@ -1411,6 +1537,13 @@ export const Index = {
component: () => import("../src/lib/registry/new-york/example/DropdownMenuDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/DropdownMenuDemo.vue"],
},
"DropdownMenuRadioGroup": {
name: "DropdownMenuRadioGroup",
type: "components:example",
registryDependencies: ["button","dropdown-menu"],
component: () => import("../src/lib/registry/new-york/example/DropdownMenuRadioGroup.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/DropdownMenuRadioGroup.vue"],
},
"HoverCardDemo": {
name: "HoverCardDemo",
type: "components:example",
@ -1516,6 +1649,13 @@ export const Index = {
component: () => import("../src/lib/registry/new-york/example/PaginationDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/PaginationDemo.vue"],
},
"PinInputControlled": {
name: "PinInputControlled",
type: "components:example",
registryDependencies: ["pin-input"],
component: () => import("../src/lib/registry/new-york/example/PinInputControlled.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/PinInputControlled.vue"],
},
"PinInputDemo": {
name: "PinInputDemo",
type: "components:example",
@ -1523,6 +1663,13 @@ export const Index = {
component: () => import("../src/lib/registry/new-york/example/PinInputDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/PinInputDemo.vue"],
},
"PinInputDisabled": {
name: "PinInputDisabled",
type: "components:example",
registryDependencies: ["pin-input"],
component: () => import("../src/lib/registry/new-york/example/PinInputDisabled.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/PinInputDisabled.vue"],
},
"PinInputFormDemo": {
name: "PinInputFormDemo",
type: "components:example",
@ -1530,6 +1677,13 @@ export const Index = {
component: () => import("../src/lib/registry/new-york/example/PinInputFormDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/PinInputFormDemo.vue"],
},
"PinInputSeparatorDemo": {
name: "PinInputSeparatorDemo",
type: "components:example",
registryDependencies: ["pin-input"],
component: () => import("../src/lib/registry/new-york/example/PinInputSeparatorDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/PinInputSeparatorDemo.vue"],
},
"PopoverDemo": {
name: "PopoverDemo",
type: "components:example",
@ -1565,6 +1719,27 @@ export const Index = {
component: () => import("../src/lib/registry/new-york/example/RangePickerWithSlot.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/RangePickerWithSlot.vue"],
},
"ResizableDemo": {
name: "ResizableDemo",
type: "components:example",
registryDependencies: ["resizable"],
component: () => import("../src/lib/registry/new-york/example/ResizableDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/ResizableDemo.vue"],
},
"ResizableHandleDemo": {
name: "ResizableHandleDemo",
type: "components:example",
registryDependencies: ["resizable"],
component: () => import("../src/lib/registry/new-york/example/ResizableHandleDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/ResizableHandleDemo.vue"],
},
"ResizableVerticalDemo": {
name: "ResizableVerticalDemo",
type: "components:example",
registryDependencies: ["resizable"],
component: () => import("../src/lib/registry/new-york/example/ResizableVerticalDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/ResizableVerticalDemo.vue"],
},
"ScrollAreaDemo": {
name: "ScrollAreaDemo",
type: "components:example",
@ -1593,6 +1768,13 @@ export const Index = {
component: () => import("../src/lib/registry/new-york/example/SelectForm.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/SelectForm.vue"],
},
"SelectScrollable": {
name: "SelectScrollable",
type: "components:example",
registryDependencies: ["select"],
component: () => import("../src/lib/registry/new-york/example/SelectScrollable.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/SelectScrollable.vue"],
},
"SeparatorDemo": {
name: "SeparatorDemo",
type: "components:example",
@ -1614,6 +1796,13 @@ export const Index = {
component: () => import("../src/lib/registry/new-york/example/SheetSideDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/SheetSideDemo.vue"],
},
"SkeletonCard": {
name: "SkeletonCard",
type: "components:example",
registryDependencies: ["skeleton"],
component: () => import("../src/lib/registry/new-york/example/SkeletonCard.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/SkeletonCard.vue"],
},
"SkeletonDemo": {
name: "SkeletonDemo",
type: "components:example",
@ -1628,6 +1817,13 @@ export const Index = {
component: () => import("../src/lib/registry/new-york/example/SliderDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/SliderDemo.vue"],
},
"SliderForm": {
name: "SliderForm",
type: "components:example",
registryDependencies: ["button","form","slider","toast"],
component: () => import("../src/lib/registry/new-york/example/SliderForm.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/SliderForm.vue"],
},
"SonnerDemo": {
name: "SonnerDemo",
type: "components:example",

View File

@ -1,7 +1,7 @@
{
"name": "www",
"type": "module",
"version": "0.9.0",
"version": "0.10.1",
"files": [
"dist"
],
@ -16,57 +16,58 @@
},
"dependencies": {
"@formkit/auto-animate": "^0.8.1",
"@morev/vue-transitions": "^2.3.6",
"@radix-icons/vue": "^1.0.0",
"@stackblitz/sdk": "^1.9.0",
"@tanstack/vue-table": "^8.11.8",
"@unovis/ts": "^1.3.3",
"@unovis/vue": "^1.3.3",
"@tanstack/vue-table": "^8.13.2",
"@unovis/ts": "^1.3.5",
"@unovis/vue": "^1.3.5",
"@vee-validate/zod": "^4.12.5",
"@vueuse/core": "^10.7.2",
"@vueuse/core": "^10.9.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"codesandbox": "^2.2.3",
"date-fns": "^2.30.0",
"embla-carousel": "^8.0.0-rc22",
"embla-carousel-autoplay": "^8.0.0-rc22",
"embla-carousel-vue": "^8.0.0-rc22",
"lucide-vue-next": "^0.276.0",
"radix-vue": "^1.4.6",
"date-fns": "^3.3.1",
"embla-carousel": "^8.0.0",
"embla-carousel-autoplay": "^8.0.0",
"embla-carousel-vue": "^8.0.0",
"lucide-vue-next": "^0.350.0",
"radix-vue": "^1.5.0",
"tailwindcss-animate": "^1.0.7",
"v-calendar": "^3.1.2",
"vaul-vue": "^0.1.0",
"vee-validate": "4.12.5",
"vue": "^3.4.15",
"vue-sonner": "^1.0.3",
"vue": "^3.4.21",
"vue-sonner": "^1.1.2",
"vue-wrap-balancer": "^1.1.3",
"zod": "^3.22.4"
},
"devDependencies": {
"@iconify-json/radix-icons": "^1.1.11",
"@iconify-json/tabler": "^1.1.89",
"@iconify/json": "^2.2.108",
"@iconify-json/radix-icons": "^1.1.14",
"@iconify-json/simple-icons": "^1.1.94",
"@iconify-json/tabler": "^1.1.106",
"@iconify/json": "^2.2.189",
"@iconify/vue": "^4.1.1",
"@shikijs/transformers": "^1.0.0-beta.3",
"@types/lodash.template": "^4.5.2",
"@types/node": "^20.8.10",
"@vitejs/plugin-vue": "^5.0.3",
"@shikijs/transformers": "^1.1.7",
"@types/lodash.template": "^4.5.3",
"@types/node": "^20.11.25",
"@vitejs/plugin-vue": "^5.0.4",
"@vitejs/plugin-vue-jsx": "^3.1.0",
"@vue/compiler-core": "^3.4.15",
"@vue/compiler-dom": "^3.4.15",
"@vue/compiler-core": "^3.4.21",
"@vue/compiler-dom": "^3.4.21",
"@vue/tsconfig": "^0.5.1",
"autoprefixer": "^10.4.17",
"autoprefixer": "^10.4.18",
"lodash.template": "^4.5.0",
"oxc-parser": "^0.2.0",
"oxc-parser": "^0.8.0",
"pathe": "^1.1.2",
"rimraf": "^5.0.5",
"shiki": "^1.0.0-beta.3",
"shiki": "^1.1.7",
"tailwind-merge": "^2.2.1",
"tailwindcss": "^3.4.1",
"tsx": "^4.7.0",
"typescript": "^5.3.3",
"unplugin-icons": "^0.18.3",
"vite": "^5.0.12",
"vitepress": "^1.0.0-rc.41",
"vue-tsc": "^1.8.27"
"tsx": "^4.7.1",
"typescript": "^5.4.2",
"unplugin-icons": "^0.18.5",
"vite": "^5.1.5",
"vitepress": "^1.0.0-rc.45",
"vue-tsc": "^2.0.6"
}
}

View File

@ -1,24 +1,21 @@
---
title: Carousel
description: A carousel with motion and swipe built using Embla.
source: apps/www/src/lib/registry/default/ui/carousel
source: apps/www/src/lib/registry/default/ui/carousel
primitive: https://www.embla-carousel.com/api
---
<ComponentPreview name="CarouselDemo" />
<ComponentPreview name="CarouselDemo" />
## About
The carousel component is built using the [Embla Carousel](https://www.embla-carousel.com/) library.
## Installation
```bash
npx shadcn-vue@latest add carousel
```
```
## Usage
@ -54,7 +51,6 @@ To set the size of the items, you can use the `basis` utility class on the `<Car
<ComponentPreview name="CarouselSize" />
Example
```vue:line-numbers title="Example" {4-6}
@ -68,7 +64,6 @@ Example
</Carousel>
```
Responsive
```vue:line-numbers title="Responsive" {4-6}
@ -151,6 +146,10 @@ Use the `orientation` prop to set the orientation of the carousel.
</Carousel>
```
### Thumbnails
<ComponentPreview name="CarouselThumbnails" />
## Options
You can pass options to the carousel using the `opts` prop. See the [Embla Carousel docs](https://www.embla-carousel.com/api/options/) for more information.
@ -259,7 +258,6 @@ You can use the `plugins` prop to add plugins to the carousel.
npm i embla-carousel-autoplay
```
```vue:line-numbers {2,8-10}
<script setup lang="ts">
import Autoplay from 'embla-carousel-autoplay'

View File

@ -1,9 +1,9 @@
---
title: Combobox
description: Autocomplete input and command palette with a list of suggestions.
description: Autocomplete input and command palette with a list of suggestions.
---
<ComponentPreview name="ComboboxDemo" />
<ComponentPreview name="ComboboxDemo" />
<br>
<Callout title="Note" class="bg-destructive">
@ -11,14 +11,13 @@ description: Autocomplete input and command palette with a list of suggestions.
[Radix Vue](https://github.com/radix-vue/radix-vue/releases/tag/v1.2.0) introduced a breaking change. You will need to wrap `ComboboxGroup` and `ComboboxItem` inside of `ComboboxList` now.
</Callout>
## Installation
The Combobox is built using a composition of the `<Popover />` and the `<Command />` components.
See installation instructions for the [Popover](/docs/components/popover#installation) and the [Command](/docs/components/command#installation) components.
## Usage
```vue
@ -110,6 +109,12 @@ const value = ref({})
<ComponentPreview name="ComboboxDropdownMenu" />
### Responsive
You can create a responsive combobox by using the `<Popover />` on desktop and the `<Drawer />` components on mobile.
<ComponentPreview name="ComboboxResponsive" />
### Form
<ComponentPreview name="ComboboxForm" />

View File

@ -4,7 +4,6 @@ description: Powerful table and datagrids built using TanStack Table.
primitive: https://tanstack.com/table/v8/docs/guide/introduction
---
<ComponentPreview name="DataTableDemo" />
## Introduction
@ -56,7 +55,6 @@ npm install @tanstack/vue-table
<ComponentPreview name="DataTableColumnPinningDemo" />
## Prerequisites
We are going to build a table to show recent payments. Here's what our data looks like:
@ -219,7 +217,6 @@ const table = useVueTable({
</Callout>
### Render the table
Finally, we'll render our table in our index component.
@ -270,7 +267,6 @@ Let's format the amount cell to display the dollar amount. We'll also align the
Update the `header` and `cell` definitions for amount as follows:
```ts:line-numbers title="components/payments/columns.ts" {5-17}
import { h } from 'vue'
@ -345,7 +341,6 @@ function copy(id: string) {
Update our columns definition to add a new `actions` column. The `actions` cell returns a `<Dropdown />` component.
```ts:line-numbers showLineNumber{2,6-16}
import { ColumnDef } from "@tanstack/vue-table"
import DropdownAction from '@/components/DataTableDropDown.vue'
@ -459,7 +454,6 @@ Let's make the email column sortable.
```ts:line-numbers {5,6,12-17}
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
import { camelize, getCurrentInstance, toHandlerKey } from 'vue'
import type { Updater } from '@tanstack/vue-table'
import { type Ref } from 'vue'
@ -971,11 +965,9 @@ export const columns = [
{
accessorKey: "email",
header: ({ column }) => (
return h(DataTableColumnHeader, {
props: {
column: column,
title: 'Email'
}
h(DataTableColumnHeader, {
column: column,
title: 'Email'
})
),
},

View File

@ -0,0 +1,63 @@
---
title: Drawer
description: A drawer component for vue.
source: apps/www/src/lib/registry/default/ui/drawer
primitive: https://github.com/radix-vue/vaul-vue
---
<ComponentPreview name="DrawerDemo" />
## About
Drawer is built on top of [Vaul Vue](https://github.com/radix-vue/vaul-vue).
## Installation
```bash
npx shadcn-vue@latest add drawer
```
## Usage
```vue showLineNumbers
<script setup lang="ts">
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from '@/components/ui/drawer'
</script>
<template>
<Drawer>
<DrawerTrigger>Open</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Are you absolutely sure?</DrawerTitle>
<DrawerDescription>This action cannot be undone.</DrawerDescription>
</DrawerHeader>
<DrawerFooter>
<Button>Submit</Button>
<DrawerClose>
<Button variant="outline">
Cancel
</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
</template>
```
## Examples
### Responsive Dialog
You can combine the `Dialog` and `Drawer` components to create a responsive dialog. This renders a `Dialog` component on desktop and a `Drawer` on mobile.
<ComponentPreview name="DrawerDialog" />

View File

@ -1,18 +1,17 @@
---
title: Dropdown Menu
description: Displays a menu to the user — such as a set of actions or functions — triggered by a button.
source: apps/www/src/lib/registry/default/ui/dropdown-menu
source: apps/www/src/lib/registry/default/ui/dropdown-menu
primitive: https://www.radix-vue.com/components/dropdown-menu.html
---
<ComponentPreview name="DropdownMenuDemo" />
<ComponentPreview name="DropdownMenuDemo" />
## Installation
```bash
npx shadcn-vue@latest add dropdown-menu
```
```
## Usage
```vue
@ -40,4 +39,14 @@ import {
</DropdownMenuContent>
</DropdownMenu>
</template>
```
```
## Examples
### Checkboxes
<ComponentPreview name="DropdownMenuCheckboxes" />
### Radio Group
<ComponentPreview name="DropdownMenuRadioGroup" />

View File

@ -16,12 +16,10 @@ Well-designed HTML forms are:
In this guide, we will take a look at building forms with [`vee-validate`](https://vee-validate.logaretm.com/v4/) and [`zod`](https://zod.dev). We're going to use a `<FormField>` component to compose accessible forms using Radix Vue components.
## Features
The `<Form />` component is a wrapper around the `vee-validate` library. It provides a few things:
- Composable components for building forms.
- A `<FormField />` component for building controlled form fields.
- Form validation using `zod`.
@ -53,7 +51,6 @@ The `<Form />` component is a wrapper around the `vee-validate` library. It prov
## Example
<TabPreview name="Component" :names="['Component', 'Native']">
<template #Component>
@ -170,12 +167,10 @@ const formSchema = toTypedSchema(z.object({
</script>
```
### Define a form
Use the `useForm` composable from `vee-validate` or use `<Form />` component to create a form.
<TabPreview name="Composition" :names="['Composition', 'Component']">
<template #Composition>
@ -327,11 +322,11 @@ See the following links for more examples on how to use the `vee-validate` featu
- [Input](/docs/components/input#form)
- [Radio Group](/docs/components/radio-group#form)
- [Select](/docs/components/select#form)
- [Slider](/docs/components/slider#form)
- [Switch](/docs/components/switch#form)
- [Textarea](/docs/components/textarea#form)
- [Combobox](/docs/components/combobox#form)
## Extras
This example shows how to add motion to your forms with [Formkit AutoAnimate](https://auto-animate.formkit.com/)

View File

@ -3,8 +3,7 @@ title: Input
description: Displays a form input field or a component that looks like an input field.
---
<ComponentPreview name="InputDemo" class="max-w-xs" />
<ComponentPreview name="InputDemo" class="max-w-xs" />
## Installation
@ -26,8 +25,6 @@ npx shadcn-vue@latest add input
</Steps>
</template>
</TabPreview>
@ -43,6 +40,8 @@ import { Input } from '@/components/ui/input'
</template>
```
## Examples
### Default
<ComponentPreview name="InputDemo" class="max-w-xs" />

View File

@ -5,8 +5,7 @@ source: apps/www/src/lib/registry/default/ui/pin-input
primitive: https://www.radix-vue.com/components/pin-input.html
---
<ComponentPreview name="PinInputDemo" />
<ComponentPreview name="PinInputDemo" />
## Installation
@ -16,6 +15,18 @@ npx shadcn-vue@latest add pin-input
## Usage
### Controlled
<ComponentPreview name="PinInputControlled" />
### Disabled
<ComponentPreview name="PinInputDisabled" />
### Separator
<ComponentPreview name="PinInputSeparatorDemo" />
### Form
<ComponentPreview name="PinInputFormDemo" />
<ComponentPreview name="PinInputFormDemo" />

View File

@ -0,0 +1,117 @@
---
title: Resizable
description: Accessible resizable panel groups and layouts with keyboard support.
source: apps/www/src/lib/registry/default/ui/resizable
primitive: https://www.radix-vue.com/components/splitter.html
---
<ComponentPreview name="ResizableDemo" />
## Installation
<TabPreview name="CLI">
<template #CLI>
```bash
npx shadcn-vue@latest add resizable
```
</template>
<template #Manual>
<Steps>
### Install the following dependency:
```bash
npm install radix-vue
```
### Copy and paste the following code into your project:
`index.ts`
<<< @/lib/registry/default/ui/resizable/index.ts
`ResizablePanelGroup.vue`
<<< @/lib/registry/default/ui/resizable/ResizablePanelGroup.vue
`ResizableHandle.vue`
<<< @/lib/registry/default/ui/resizable/ResizableHandle.vue
</Steps>
</template>
</TabPreview>
## Usage
```vue
<script setup lang="ts">
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from '@/components/ui/resizable'
</script>
<template>
<ResizablePanelGroup direction="horizontal">
<ResizablePanel>One</ResizablePanel>
<ResizableHandle />
<ResizablePanel>Two</ResizablePanel>
</ResizablePanelGroup>
</template>
```
## Examples
### Vertical
Use the direction prop to set the direction of the resizable panels.
<ComponentPreview name="ResizableVerticalDemo" />
```vue:line-numbers {10}
<script setup lang="ts">
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from '@/components/ui/resizable'
</script>
<template>
<ResizablePanelGroup direction="horizontal">
<ResizablePanel>One</ResizablePanel>
<ResizableHandle />
<ResizablePanel>Two</ResizablePanel>
</ResizablePanelGroup>
</template>
```
### Handle
You can set or hide the handle by using the withHandle prop on the ResizableHandle component.
<ComponentPreview name="ResizableHandleDemo" />
```vue:line-numbers {12}
<script setup lang="ts">
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from '@/components/ui/resizable'
</script>
<template>
<ResizablePanelGroup direction="horizontal">
<ResizablePanel>One</ResizablePanel>
<ResizableHandle with-handle />
<ResizablePanel>Two</ResizablePanel>
</ResizablePanelGroup>
</template>
```

View File

@ -1,16 +1,14 @@
---
title: Select
description: Displays a list of options for the user to pick from—triggered by a button.
source: apps/www/src/lib/registry/default/ui/select
source: apps/www/src/lib/registry/default/ui/select
primitive: https://www.radix-vue.com/components/select.html
---
<ComponentPreview name="SelectDemo" />
<ComponentPreview name="SelectDemo" />
## Installation
```bash
npx shadcn-vue@latest add select
```
@ -49,6 +47,10 @@ import {
## Examples
### Scrollable
<ComponentPreview name="SelectScrollable" />
### Form
<ComponentPreview name="SelectForm" />

View File

@ -1,9 +1,9 @@
---
title: Skeleton
description: Use to show a placeholder while content is loading.
description: Use to show a placeholder while content is loading.
---
<ComponentPreview name="SkeletonDemo" />
<ComponentPreview name="SkeletonDemo" />
## Installation
@ -21,7 +21,6 @@ npx shadcn-vue@latest add skeleton
### Copy and paste the following code into your project
<<< @/lib/registry/default/ui/skeleton/Skeleton.vue
</Steps>
@ -40,3 +39,9 @@ import { Skeleton } from '@/components/ui/skeleton'
<Skeleton class="w-[100px] h-5 rounded-full" />
</template>
```
## Examples
### Card
<ComponentPreview name="SkeletonCard" />

View File

@ -1,11 +1,11 @@
---
title: Slider
description: An input where the user selects a value from within a given range.
source: apps/www/src/lib/registry/default/ui/slider
source: apps/www/src/lib/registry/default/ui/slider
primitive: https://www.radix-vue.com/components/slider.html
---
<ComponentPreview name="SliderDemo" />
<ComponentPreview name="SliderDemo" />
## Installation
@ -25,4 +25,10 @@ import { Slider } from '@/components/ui/slider'
:default-value="[33]" :max="100" :step="1"
/>
</template>
```
```
## Examples
### Form
<ComponentPreview name="SliderForm" />

View File

@ -0,0 +1,39 @@
---
title: Dark Mode
description: Adding dark mode to your site.
---
<script setup>
import { useData } from 'vitepress'
const { isDark } = useData()
import ViteIcon from '~icons/simple-icons/vite'
import NuxtIcon from '~icons/simple-icons/nuxtdotjs'
import AstroIcon from '~icons/simple-icons/astro'
</script>
<div class="grid gap-4 mt-8 sm:grid-cols-2 sm:gap-6 not-docs">
<LinkedCard href="/docs/dark-mode/vite">
<ViteIcon class="w-10 h-10" />
<p class="mt-2 font-medium">Vite</p>
</LinkedCard>
<LinkedCard href="/docs/dark-mode/nuxt">
<NuxtIcon class="w-10 h-10" />
<p class="mt-2 font-medium">Nuxt</p>
</LinkedCard>
<LinkedCard href="/docs/dark-mode/vitepress">
<svg width="48" height="48" viewBox="0 0 48 48" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M5.03628 7.87818C4.75336 5.83955 6.15592 3.95466 8.16899 3.66815L33.6838 0.0367403C35.6969 -0.24977 37.5581 1.1706 37.841 3.20923L42.9637 40.1218C43.2466 42.1604 41.8441 44.0453 39.831 44.3319L14.3162 47.9633C12.3031 48.2498 10.4419 46.8294 10.159 44.7908L5.03628 7.87818Z" />
<path d="M6.85877 7.6188C6.71731 6.59948 7.41859 5.65703 8.42512 5.51378L33.9399 1.88237C34.9465 1.73911 35.8771 2.4493 36.0186 3.46861L41.1412 40.3812C41.2827 41.4005 40.5814 42.343 39.5749 42.4862L14.0601 46.1176C13.0535 46.2609 12.1229 45.5507 11.9814 44.5314L6.85877 7.6188Z" class="fill-background"/>
<path d="M33.1857 14.9195L25.8505 34.1576C25.6991 34.5547 25.1763 34.63 24.9177 34.2919L12.3343 17.8339C12.0526 17.4655 12.3217 16.9339 12.7806 16.9524L22.9053 17.3607C22.9698 17.3633 23.0344 17.3541 23.0956 17.3337L32.5088 14.1992C32.9431 14.0546 33.3503 14.4878 33.1857 14.9195Z" />
<path d="M27.0251 12.5756L19.9352 15.0427C19.8187 15.0832 19.7444 15.1986 19.7546 15.3231L20.3916 23.063C20.4066 23.2453 20.5904 23.3628 20.7588 23.2977L22.7226 22.5392C22.9064 22.4682 23.1021 22.6138 23.0905 22.8128L22.9102 25.8903C22.8982 26.0974 23.1093 26.2436 23.295 26.1567L24.4948 25.5953C24.6808 25.5084 24.892 25.6549 24.8795 25.8624L24.5855 30.6979C24.5671 31.0004 24.9759 31.1067 25.1013 30.8321L25.185 30.6487L29.4298 17.8014C29.5008 17.5863 29.2968 17.3809 29.0847 17.454L27.0519 18.1547C26.8609 18.2205 26.6675 18.0586 26.6954 17.8561L27.3823 12.8739C27.4103 12.6712 27.2163 12.5091 27.0251 12.5756Z" class="stroke-background"/>
</svg>
<p class="mt-2 font-medium">Vitepress</p>
</LinkedCard>
<LinkedCard href="/docs/dark-mode/astro">
<AstroIcon class="w-10 h-10" />
<p class="mt-2 font-medium">Astro</p>
</LinkedCard>
</div>

View File

@ -0,0 +1,116 @@
---
title: Astro
description: Adding dark mode to your astro app.
---
## Dark mode
<Steps>
### Create an inline theme script
```astro title="src/pages/index.astro"
---
import '../styles/globals.css'
---
<script is:inline>
const getThemePreference = () => {
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
return localStorage.getItem('theme');
}
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
};
const isDark = getThemePreference() === 'dark';
document.documentElement.classList[isDark ? 'add' : 'remove']('dark');
if (typeof localStorage !== 'undefined') {
const observer = new MutationObserver(() => {
const isDark = document.documentElement.classList.contains('dark');
localStorage.setItem('theme', isDark ? 'dark' : 'light');
});
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
}
</script>
<html lang="en">
<body>
<h1>Astro</h1>
</body>
</html>
</script>
```
### Install Dependencies
```bash
npm install @vueuse/core
```
Optional, to include icons for theme button.
```bash
npm install -D @iconify/vue @iconify-json/radix-icons
```
### Add a mode toggle
Place a mode toggle on your site to toggle between light and dark mode.
We're using [`useColorMode`](https://vueuse.org/core/usecolormode/) from [`@vueuse/core`](https://vueuse.org/core/).
> Reactive color mode (dark / light / customs) with auto data persistence.
```vue
<script setup lang="ts">
import { useColorMode } from '@vueuse/core'
import { Icon } from '@iconify/vue'
import { Button } from '@/components/ui/button'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
const mode = useColorMode()
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="outline">
<Icon icon="radix-icons:moon" class="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Icon icon="radix-icons:sun" class="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span class="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem @click="mode = 'light'">
Light
</DropdownMenuItem>
<DropdownMenuItem @click="mode = 'dark'">
Dark
</DropdownMenuItem>
<DropdownMenuItem @click="mode = 'auto'">
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>
```
### Display the mode toggle
Place a mode toggle on your site to toggle between light and dark mode.
```astro title="src/pages/index.astro"
---
import '../styles/globals.css'
import { ModeToggle } from '@/components/ModeToggle.vue';
---
<!-- Inline script -->
<html lang="en">
<body>
<h1>Astro</h1>
<ModeToggle client:load />
</body>
</html>
```
</Steps>

View File

@ -0,0 +1,74 @@
---
title: Nuxt
description: Adding dark mode to your nuxt app.
---
## Dark mode
<Steps>
### Install Dependencies
```bash
npm install -D @nuxtjs/color-mode
```
Then, add `@nuxtjs/color-mode` to the modules section of your `nuxt.config.ts`
```ts
export default defineNuxtConfig({
modules: [
'@nuxtjs/tailwindcss',
'@nuxtjs/color-mode'
],
colorMode: {
classSuffix: ''
}
})
```
Optional, to include icons for theme button.
```bash
npm install -D @iconify/vue @iconify-json/radix-icons
```
### Add a mode toggle
Place a mode toggle on your site to toggle between light and dark mode.
We're using [`useColorMode`](https://color-mode.nuxtjs.org/#usage) from [`Nuxt Color Mode`](https://color-mode.nuxtjs.org/).
```vue
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Button } from '@/components/ui/button'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
const colorMode = useColorMode()
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="outline">
<Icon icon="radix-icons:moon" class="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Icon icon="radix-icons:sun" class="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span class="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem @click="colorMode.preference = 'light'">
Light
</DropdownMenuItem>
<DropdownMenuItem @click="colorMode.preference = 'dark'">
Dark
</DropdownMenuItem>
<DropdownMenuItem @click="colorMode.preference = 'system'">
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>
```
</Steps>

View File

@ -0,0 +1,62 @@
---
title: Vite
description: Adding dark mode to your vite app.
---
## Dark mode
<Steps>
### Install Dependencies
```bash
npm install @vueuse/core
```
Optional, to include icons for theme button.
```bash
npm install -D @iconify/vue @iconify-json/radix-icons
```
### Add a mode toggle
Place a mode toggle on your site to toggle between light and dark mode.
We're using [`useColorMode`](https://vueuse.org/core/usecolormode/) from [`@vueuse/core`](https://vueuse.org/core/).
> Reactive color mode (dark / light / customs) with auto data persistence.
```vue
<script setup lang="ts">
import { useColorMode } from '@vueuse/core'
import { Icon } from '@iconify/vue'
import { Button } from '@/components/ui/button'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
const mode = useColorMode()
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="outline">
<Icon icon="radix-icons:moon" class="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Icon icon="radix-icons:sun" class="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span class="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem @click="mode = 'light'">
Light
</DropdownMenuItem>
<DropdownMenuItem @click="mode = 'dark'">
Dark
</DropdownMenuItem>
<DropdownMenuItem @click="mode = 'auto'">
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>
```
</Steps>

View File

@ -0,0 +1,47 @@
---
title: Vitepress
description: Adding dark mode to your vitepress app.
---
## Dark mode
<Steps>
### Install Dependencies
```bash
npm install @vueuse/core
```
Optional, to include icons for theme button.
```bash
npm install -D @iconify/vue @iconify-json/radix-icons
```
### Add a mode toggle
Place a mode toggle on your site to toggle between light and dark mode.
We're using [`useToggle`](https://vueuse.org/shared/useToggle/) from [`@vueuse/core`](https://vueuse.org/core/).
> A boolean switcher with utility functions.
```vue
<script setup lang="ts">
import { useData } from 'vitepress'
import { useToggle } from '@vueuse/core'
import { Button } from '@/lib/registry/default/ui/button'
const { frontmatter, isDark } = useData()
const toggleDark = useToggle(isDark)
</script>
<template>
<Button variant="outline">
<Icon icon="radix-icons:moon" class="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Icon icon="radix-icons:sun" class="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span class="sr-only">Toggle theme</span>
</Button>
</template>
```
</Steps>

View File

@ -21,8 +21,6 @@ npm create vite@latest my-vue-app -- --template vue-ts
Install `tailwindcss` and its peer dependencies, then generate your `tailwind.config.js` and configure `postcss` plugins
<TabsMarkdown>
<TabMarkdown title="vite.config">
@ -59,7 +57,6 @@ Install `tailwindcss` and its peer dependencies, then generate your `tailwind.co
</TabMarkdown>
<TabMarkdown title="postcss.config.js">
```bash
@ -80,7 +77,6 @@ Install `tailwindcss` and its peer dependencies, then generate your `tailwind.co
</TabMarkdown>
</TabsMarkdown>
### Edit tsconfig.json
Add the code below to the compilerOptions of your tsconfig.json so your app can resolve paths without error
@ -107,11 +103,14 @@ Add the code below to the vite.config.ts so your app can resolve paths without e
npm i -D @types/node
```
```typescript {12-16}
```typescript {15-19}
import path from "path"
import vue from "@vitejs/plugin-vue"
import { defineConfig } from "vite"
import tailwind from "tailwindcss"
import autoprefixer from "autoprefixer"
export default defineConfig({
css: {
postcss: {
@ -148,7 +147,7 @@ Where is your global CSS file? src/index.css
Do you want to use CSS variables for colors? no / yes
Where is your tailwind.config.js located? tailwind.config.js
Configure the import alias for components: @/components
Configure the import alias for utils: @/lib/utils
Configure the import alias for utils: @/lib/utils
```
### That's it

View File

@ -0,0 +1,5 @@
<script setup>
import MailExample from "@/examples/mail/Example.vue"
</script>
<MailExample />

View File

@ -72,7 +72,7 @@ const onSubmit = handleSubmit((values) => {
</option>
</select>
</FormControl>
<ChevronDownIcon class="absolute right-3 top-2.5 h-4 w-4 opacity-50" />
<ChevronDownIcon class="pointer-events-none absolute right-3 top-2.5 h-4 w-4 opacity-50" />
</div>
<FormDescription>
Set the font you want to use in the dashboard.

View File

@ -0,0 +1,30 @@
<script lang="ts" setup>
import Mail from './components/Mail.vue'
import { accounts, mails } from './data/mails'
</script>
<template>
<div class="md:hidden">
<image
src="/examples/mail-dark.png"
:width="1280"
:height="727"
alt="Mail"
class="hidden dark:block"
/>
<image
src="/examples/mail-light.png"
:width="1280"
:height="727"
alt="Mail"
class="block dark:hidden"
/>
</div>
<div class="hidden flex-col md:flex">
<Mail
:accounts="accounts"
:mails="mails"
:nav-collapsed-size="4"
/>
</div>
</template>

View File

@ -0,0 +1,49 @@
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { Icon } from '@iconify/vue'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/lib/registry/new-york/ui/select'
import { cn } from '@/lib/utils'
interface AccountSwitcherProps {
isCollapsed: boolean
accounts: {
label: string
email: string
icon: string
}[]
}
const props = defineProps<AccountSwitcherProps>()
const selectedEmail = ref<string>(props.accounts[0].email)
const selectedEmailData = computed(() => props.accounts.find(item => item.email === selectedEmail.value))
</script>
<template>
<Select v-model="selectedEmail">
<SelectTrigger
aria-label="Select account"
:class="cn(
'flex items-center gap-2 [&>span]:line-clamp-1 [&>span]:flex [&>span]:w-full [&>span]:items-center [&>span]:gap-1 [&>span]:truncate [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0',
{ 'flex h-9 w-9 shrink-0 items-center justify-center p-0 [&>span]:w-auto [&>svg]:hidden': isCollapsed },
)"
>
<SelectValue placeholder="Select an account">
<div class="flex items-center gap-3">
<Icon class="size-4" :icon="selectedEmailData!.icon" />
<span v-if="!isCollapsed">
{{ selectedEmailData!.label }}
</span>
</div>
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem v-for="account of accounts" :key="account.email" :value="account.email">
<div class="flex items-center gap-3 [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-foreground">
<Icon class="size-4" :icon="account.icon" />
{{ account.email }}
</div>
</SelectItem>
</SelectContent>
</Select>
</template>

View File

@ -0,0 +1,223 @@
<script lang="ts" setup>
import {
Search,
} from 'lucide-vue-next'
import { computed, ref } from 'vue'
import { refDebounced } from '@vueuse/core'
import type { Mail } from '../data/mails'
import AccountSwitcher from './AccountSwitcher.vue'
import MailList from './MailList.vue'
import MailDisplay from './MailDisplay.vue'
import Nav, { type LinkProp } from './Nav.vue'
import { cn } from '@/lib/utils'
import { Separator } from '@/lib/registry/new-york/ui/separator'
import { Input } from '@/lib/registry/new-york/ui/input'
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '@/lib/registry/new-york/ui/tabs'
import { TooltipProvider } from '@/lib/registry/new-york/ui/tooltip'
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/lib/registry/new-york/ui/resizable'
interface MailProps {
accounts: {
label: string
email: string
icon: string
}[]
mails: Mail[]
defaultLayout?: number[]
defaultCollapsed?: boolean
navCollapsedSize: number
}
const props = withDefaults(defineProps<MailProps>(), {
defaultCollapsed: false,
defaultLayout: () => [265, 440, 655],
})
const isCollapsed = ref(props.defaultCollapsed)
const selectedMail = ref<string | undefined>(props.mails[0].id)
const searchValue = ref('')
const debouncedSearch = refDebounced(searchValue, 250)
const filteredMailList = computed(() => {
let output: Mail[] = []
const serachValue = debouncedSearch.value?.trim()
if (!serachValue) {
output = props.mails
}
else {
output = props.mails.filter((item) => {
return item.name.includes(debouncedSearch.value)
|| item.email.includes(debouncedSearch.value)
|| item.name.includes(debouncedSearch.value)
|| item.subject.includes(debouncedSearch.value)
|| item.text.includes(debouncedSearch.value)
})
}
return output
})
const unreadMailList = computed(() => filteredMailList.value.filter(item => !item.read))
const selectedMailData = computed(() => props.mails.find(item => item.id === selectedMail.value))
const links: LinkProp[] = [
{
title: 'Inbox',
label: '128',
icon: 'lucide:inbox',
variant: 'default',
},
{
title: 'Drafts',
label: '9',
icon: 'lucide:file',
variant: 'ghost',
},
{
title: 'Sent',
label: '',
icon: 'lucide:send',
variant: 'ghost',
},
{
title: 'Junk',
label: '23',
icon: 'lucide:archive',
variant: 'ghost',
},
{
title: 'Trash',
label: '',
icon: 'lucide:trash',
variant: 'ghost',
},
{
title: 'Archive',
label: '',
icon: 'lucide:archive',
variant: 'ghost',
},
]
const links2: LinkProp[] = [
{
title: 'Social',
label: '972',
icon: 'lucide:user-2',
variant: 'ghost',
},
{
title: 'Updates',
label: '342',
icon: 'lucide:alert-circle',
variant: 'ghost',
},
{
title: 'Forums',
label: '128',
icon: 'lucide:message-square',
variant: 'ghost',
},
{
title: 'Shopping',
label: '8',
icon: 'lucide:shopping-cart',
variant: 'ghost',
},
{
title: 'Promotions',
label: '21',
icon: 'lucide:archive',
variant: 'ghost',
},
]
function onCollapse() {
isCollapsed.value = true
}
function onExpand() {
isCollapsed.value = false
}
</script>
<template>
<TooltipProvider :delay-duration="0">
<ResizablePanelGroup
id="resize-panel-group-1"
direction="horizontal"
class="h-full max-h-[800px] items-stretch"
>
<ResizablePanel
id="resize-panel-1"
:default-size="defaultLayout[0]"
:collapsed-size="navCollapsedSize"
collapsible
:min-size="15"
:max-size="20"
:class="cn(isCollapsed && 'min-w-[50px] transition-all duration-300 ease-in-out')"
@expand="onExpand"
@collapse="onCollapse"
>
<div :class="cn('flex h-[52px] items-center justify-center', isCollapsed ? 'h-[52px]' : 'px-2')">
<AccountSwitcher :is-collapsed="isCollapsed" :accounts="accounts" />
</div>
<Separator />
<Nav
:is-collapsed="isCollapsed"
:links="links"
/>
<Separator />
<Nav
:is-collapsed="isCollapsed"
:links="links2"
/>
</ResizablePanel>
<ResizableHandle id="resize-handle-1" with-handle />
<ResizablePanel id="resize-panel-2" :default-size="defaultLayout[1]" :min-size="30">
<Tabs default-value="all">
<div class="flex items-center px-4 py-2">
<h1 class="text-xl font-bold">
Inbox
</h1>
<TabsList class="ml-auto">
<TabsTrigger value="all" class="text-zinc-600 dark:text-zinc-200">
All mail
</TabsTrigger>
<TabsTrigger value="unread" class="text-zinc-600 dark:text-zinc-200">
Unread
</TabsTrigger>
</TabsList>
</div>
<Separator />
<div class="bg-background/95 p-4 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<form>
<div class="relative">
<Search class="absolute left-2 top-2.5 size-4 text-muted-foreground" />
<Input v-model="searchValue" placeholder="Search" class="pl-8" />
</div>
</form>
</div>
<TabsContent value="all" class="m-0">
<MailList v-model:selected-mail="selectedMail" :items="filteredMailList" />
</TabsContent>
<TabsContent value="unread" class="m-0">
<MailList v-model:selected-mail="selectedMail" :items="unreadMailList" />
</TabsContent>
</Tabs>
</ResizablePanel>
<ResizableHandle id="resiz-handle-2" with-handle />
<ResizablePanel id="resize-panel-3" :default-size="defaultLayout[2]">
<MailDisplay :mail="selectedMailData" />
</ResizablePanel>
</ResizablePanelGroup>
</TooltipProvider>
</template>

View File

@ -0,0 +1,236 @@
<script lang="ts" setup>
import { Archive, ArchiveX, Clock, Forward, MoreVertical, Reply, ReplyAll, Trash2 } from 'lucide-vue-next'
import { computed } from 'vue'
import addDays from 'date-fns/addDays'
import addHours from 'date-fns/addHours'
import format from 'date-fns/format'
import nextSaturday from 'date-fns/nextSaturday'
import type { Mail } from '../data/mails'
import { Calendar } from '@/lib/registry/new-york/ui/calendar'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/lib/registry/new-york/ui/dropdown-menu'
import { Popover, PopoverContent, PopoverTrigger } from '@/lib/registry/new-york/ui/popover'
import { Avatar, AvatarFallback } from '@/lib/registry/new-york/ui/avatar'
import { Button } from '@/lib/registry/new-york/ui/button'
import { Label } from '@/lib/registry/new-york/ui/label'
import { Separator } from '@/lib/registry/new-york/ui/separator'
import { Switch } from '@/lib/registry/new-york/ui/switch'
import { Textarea } from '@/lib/registry/new-york/ui/textarea'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/lib/registry/new-york/ui/tooltip'
interface MailDisplayProps {
mail: Mail | undefined
}
const props = defineProps<MailDisplayProps>()
const mailFallbackName = computed(() => {
return props.mail?.name
.split(' ')
.map(chunk => chunk[0])
.join('')
})
const today = new Date()
</script>
<template>
<div class="flex h-full flex-col">
<div class="flex items-center p-2">
<div class="flex items-center gap-2">
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" :disabled="!mail">
<Archive class="size-4" />
<span class="sr-only">Archive</span>
</Button>
</TooltipTrigger>
<TooltipContent>Archive</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" :disabled="!mail">
<ArchiveX class="size-4" />
<span class="sr-only">Move to junk</span>
</Button>
</TooltipTrigger>
<TooltipContent>Move to junk</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" :disabled="!mail">
<Trash2 class="size-4" />
<span class="sr-only">Move to trash</span>
</Button>
</TooltipTrigger>
<TooltipContent>Move to trash</TooltipContent>
</Tooltip>
<Separator orientation="vertical" class="mx-1 h-6" />
<Tooltip>
<Popover>
<PopoverTrigger as-child>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" :disabled="!mail">
<Clock class="size-4" />
<span class="sr-only">Snooze</span>
</Button>
</TooltipTrigger>
</PopoverTrigger>
<PopoverContent class="flex w-[535px] p-0">
<div class="flex flex-col gap-2 border-r px-2 py-4">
<div class="px-4 text-sm font-medium">
Snooze until
</div>
<div class="grid min-w-[250px] gap-1">
<Button
variant="ghost"
class="justify-start font-normal"
>
Later today
<span class="ml-auto text-muted-foreground">
{{ format(addHours(today, 4), "E, h:m b") }}
</span>
</Button>
<Button
variant="ghost"
class="justify-start font-normal"
>
Tomorrow
<span class="ml-auto text-muted-foreground">
{{ format(addDays(today, 1), "E, h:m b") }}
</span>
</Button>
<Button
variant="ghost"
class="justify-start font-normal"
>
This weekend
<span class="ml-auto text-muted-foreground">
{{ format(nextSaturday(today), "E, h:m b") }}
</span>
</Button>
<Button
variant="ghost"
class="justify-start font-normal"
>
Next week
<span class="ml-auto text-muted-foreground">
{{ format(addDays(today, 7), "E, h:m b") }}
</span>
</Button>
</div>
</div>
<div class="p-2">
<Calendar />
</div>
</PopoverContent>
</Popover>
<TooltipContent>Snooze</TooltipContent>
</Tooltip>
</div>
<div class="ml-auto flex items-center gap-2">
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" :disabled="!mail">
<Reply class="size-4" />
<span class="sr-only">Reply</span>
</Button>
</TooltipTrigger>
<TooltipContent>Reply</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" :disabled="!mail">
<ReplyAll class="size-4" />
<span class="sr-only">Reply all</span>
</Button>
</TooltipTrigger>
<TooltipContent>Reply all</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" :disabled="!mail">
<Forward class="size-4" />
<span class="sr-only">Forward</span>
</Button>
</TooltipTrigger>
<TooltipContent>Forward</TooltipContent>
</Tooltip>
</div>
<Separator orientation="vertical" class="mx-2 h-6" />
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="ghost" size="icon" :disabled="!mail">
<MoreVertical class="size-4" />
<span class="sr-only">More</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Mark as unread</DropdownMenuItem>
<DropdownMenuItem>Star thread</DropdownMenuItem>
<DropdownMenuItem>Add label</DropdownMenuItem>
<DropdownMenuItem>Mute thread</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<Separator />
<div v-if="mail" class="flex flex-1 flex-col">
<div class="flex items-start p-4">
<div class="flex items-start gap-4 text-sm">
<Avatar>
<AvatarFallback>
{{ mailFallbackName }}
</AvatarFallback>
</Avatar>
<div class="grid gap-1">
<div class="font-semibold">
{{ mail.name }}
</div>
<div class="line-clamp-1 text-xs">
{{ mail.subject }}
</div>
<div class="line-clamp-1 text-xs">
<span class="font-medium">Reply-To:</span> {{ mail.email }}
</div>
</div>
</div>
<div v-if="mail.date" class="ml-auto text-xs text-muted-foreground">
{{ format(new Date(mail.date), "PPpp") }}
</div>
</div>
<Separator />
<div class="flex-1 whitespace-pre-wrap p-4 text-sm">
{{ mail.text }}
</div>
<Separator class="mt-auto" />
<div class="p-4">
<form>
<div class="grid gap-4">
<Textarea
class="p-4"
:placeholder="`Reply ${mail.name}...`"
/>
<div class="flex items-center">
<Label
html-for="mute"
class="flex items-center gap-2 text-xs font-normal"
>
<Switch id="mute" aria-label="Mute thread" /> Mute this
thread
</Label>
<Button
type="button"
size="sm"
class="ml-auto"
>
Send
</Button>
</div>
</div>
</form>
</div>
</div>
<div v-else class="p-8 text-center text-muted-foreground">
No message selected
</div>
</div>
</template>

View File

@ -0,0 +1,93 @@
<script lang="ts" setup>
import { formatDistanceToNow } from 'date-fns'
import type { Mail } from '../data/mails'
import { ScrollArea } from '@/lib/registry/new-york/ui/scroll-area'
import { cn } from '@/lib/utils'
import { Badge } from '@/lib/registry/new-york/ui/badge'
interface MailListProps {
items: Mail[]
}
defineProps<MailListProps>()
const selectedMail = defineModel<string>('selectedMail', { required: false })
function getBadgeVariantFromLabel(label: string) {
if (['work'].includes(label.toLowerCase()))
return 'default'
if (['personal'].includes(label.toLowerCase()))
return 'outline'
return 'secondary'
}
</script>
<template>
<ScrollArea class="h-screen flex">
<div class="flex-1 flex flex-col gap-2 p-4 pt-0">
<TransitionGroup name="list" appear>
<button
v-for="item of items"
:key="item.id"
:class="cn(
'flex flex-col items-start gap-2 rounded-lg border p-3 text-left text-sm transition-all hover:bg-accent',
selectedMail === item.id && 'bg-muted',
)"
@click="selectedMail = item.id"
>
<div class="flex w-full flex-col gap-1">
<div class="flex items-center">
<div class="flex items-center gap-2">
<div class="font-semibold">
{{ item.name }}
</div>
<span v-if="!item.read" class="flex h-2 w-2 rounded-full bg-blue-600" />
</div>
<div
:class="cn(
'ml-auto text-xs',
selectedMail === item.id
? 'text-foreground'
: 'text-muted-foreground',
)"
>
{{ formatDistanceToNow(new Date(item.date), { addSuffix: true }) }}
</div>
</div>
<div class="text-xs font-medium">
{{ item.subject }}
</div>
</div>
<div class="line-clamp-2 text-xs text-muted-foreground">
{{ item.text.substring(0, 300) }}
</div>
<div class="flex items-center gap-2">
<Badge v-for="label of item.labels" :key="label" :variant="getBadgeVariantFromLabel(label)">
{{ label }}
</Badge>
</div>
</button>
</TransitionGroup>
</div>
</ScrollArea>
</template>
<style scoped>
.list-move,
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateY(15px);
}
.list-leave-active {
position: absolute;
}
</style>

View File

@ -0,0 +1,83 @@
<script lang="ts" setup>
import { Icon } from '@iconify/vue'
import { cn } from '@/lib/utils'
import { buttonVariants } from '@/lib/registry/new-york/ui/button'
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/lib/registry/new-york/ui/tooltip'
export interface LinkProp {
title: string
label?: string
icon: string
variant: 'default' | 'ghost'
}
interface NavProps {
isCollapsed: boolean
links: LinkProp[]
}
defineProps<NavProps>()
</script>
<template>
<div
:data-collapsed="isCollapsed"
class="group flex flex-col gap-4 py-2 data-[collapsed=true]:py-2"
>
<nav class="grid gap-1 px-2 group-[[data-collapsed=true]]:justify-center group-[[data-collapsed=true]]:px-2">
<template v-for="(link, index) of links">
<Tooltip v-if="isCollapsed" :key="`1-${index}`" :delay-duration="0">
<TooltipTrigger as-child>
<a
href="#"
:class="cn(
buttonVariants({ variant: link.variant, size: 'icon' }),
'h-9 w-9',
link.variant === 'default'
&& 'dark:bg-muted dark:text-muted-foreground dark:hover:bg-muted dark:hover:text-white',
)"
>
<Icon :icon="link.icon" class="size-4" />
<span class="sr-only">{{ link.title }}</span>
</a>
</TooltipTrigger>
<TooltipContent side="right" class="flex items-center gap-4">
{{ link.title }}
<span v-if="link.label" class="ml-auto text-muted-foreground">
{{ link.label }}
</span>
</TooltipContent>
</Tooltip>
<a
v-else
:key="`2-${index}`"
href="#"
:class="cn(
buttonVariants({ variant: link.variant, size: 'sm' }),
link.variant === 'default'
&& 'dark:bg-muted dark:text-white dark:hover:bg-muted dark:hover:text-white',
'justify-start',
)"
>
<Icon :icon="link.icon" class="mr-2 size-4" />
{{ link.title }}
<span
v-if="link.label"
:class="cn(
'ml-auto',
link.variant === 'default'
&& 'text-background dark:text-white',
)"
>
{{ link.label }}
</span>
</a>
</template>
</nav>
</div>
</template>

View File

@ -0,0 +1,279 @@
export const mails = [
{
id: '6c84fb90-12c4-11e1-840d-7b25c5ee775a',
name: 'William Smith',
email: 'williamsmith@example.com',
subject: 'Meeting Tomorrow',
text: 'Hi, let\'s have a meeting tomorrow to discuss the project. I\'ve been reviewing the project details and have some ideas I\'d like to share. It\'s crucial that we align on our next steps to ensure the project\'s success.\n\nPlease come prepared with any questions or insights you may have. Looking forward to our meeting!\n\nBest regards, William',
date: '2023-10-22T09:00:00',
read: true,
labels: ['meeting', 'work', 'important'],
},
{
id: '110e8400-e29b-11d4-a716-446655440000',
name: 'Alice Smith',
email: 'alicesmith@example.com',
subject: 'Re: Project Update',
text: 'Thank you for the project update. It looks great! I\'ve gone through the report, and the progress is impressive. The team has done a fantastic job, and I appreciate the hard work everyone has put in.\n\nI have a few minor suggestions that I\'ll include in the attached document.\n\nLet\'s discuss these during our next meeting. Keep up the excellent work!\n\nBest regards, Alice',
date: '2023-10-22T10:30:00',
read: true,
labels: ['work', 'important'],
},
{
id: '3e7c3f6d-bdf5-46ae-8d90-171300f27ae2',
name: 'Bob Johnson',
email: 'bobjohnson@example.com',
subject: 'Weekend Plans',
text: 'Any plans for the weekend? I was thinking of going hiking in the nearby mountains. It\'s been a while since we had some outdoor fun.\n\nIf you\'re interested, let me know, and we can plan the details. It\'ll be a great way to unwind and enjoy nature.\n\nLooking forward to your response!\n\nBest, Bob',
date: '2023-04-10T11:45:00',
read: true,
labels: ['personal'],
},
{
id: '61c35085-72d7-42b4-8d62-738f700d4b92',
name: 'Emily Davis',
email: 'emilydavis@example.com',
subject: 'Re: Question about Budget',
text: 'I have a question about the budget for the upcoming project. It seems like there\'s a discrepancy in the allocation of resources.\n\nI\'ve reviewed the budget report and identified a few areas where we might be able to optimize our spending without compromising the project\'s quality.\n\nI\'ve attached a detailed analysis for your reference. Let\'s discuss this further in our next meeting.\n\nThanks, Emily',
date: '2023-03-25T13:15:00',
read: false,
labels: ['work', 'budget'],
},
{
id: '8f7b5db9-d935-4e42-8e05-1f1d0a3dfb97',
name: 'Michael Wilson',
email: 'michaelwilson@example.com',
subject: 'Important Announcement',
text: 'I have an important announcement to make during our team meeting. It pertains to a strategic shift in our approach to the upcoming product launch. We\'ve received valuable feedback from our beta testers, and I believe it\'s time to make some adjustments to better meet our customers\' needs.\n\nThis change is crucial to our success, and I look forward to discussing it with the team. Please be prepared to share your insights during the meeting.\n\nRegards, Michael',
date: '2023-03-10T15:00:00',
read: false,
labels: ['meeting', 'work', 'important'],
},
{
id: '1f0f2c02-e299-40de-9b1d-86ef9e42126b',
name: 'Sarah Brown',
email: 'sarahbrown@example.com',
subject: 'Re: Feedback on Proposal',
text: 'Thank you for your feedback on the proposal. It looks great! I\'m pleased to hear that you found it promising. The team worked diligently to address all the key points you raised, and I believe we now have a strong foundation for the project.\n\nI\'ve attached the revised proposal for your review.\n\nPlease let me know if you have any further comments or suggestions. Looking forward to your response.\n\nBest regards, Sarah',
date: '2023-02-15T16:30:00',
read: true,
labels: ['work'],
},
{
id: '17c0a96d-4415-42b1-8b4f-764efab57f66',
name: 'David Lee',
email: 'davidlee@example.com',
subject: 'New Project Idea',
text: 'I have an exciting new project idea to discuss with you. It involves expanding our services to target a niche market that has shown considerable growth in recent months.\n\nI\'ve prepared a detailed proposal outlining the potential benefits and the strategy for execution.\n\nThis project has the potential to significantly impact our business positively. Let\'s set up a meeting to dive into the details and determine if it aligns with our current goals.\n\nBest regards, David',
date: '2023-01-28T17:45:00',
read: false,
labels: ['meeting', 'work', 'important'],
},
{
id: '2f0130cb-39fc-44c4-bb3c-0a4337edaaab',
name: 'Olivia Wilson',
email: 'oliviawilson@example.com',
subject: 'Vacation Plans',
text: 'Let\'s plan our vacation for next month. What do you think? I\'ve been thinking of visiting a tropical paradise, and I\'ve put together some destination options.\n\nI believe it\'s time for us to unwind and recharge. Please take a look at the options and let me know your preferences.\n\nWe can start making arrangements to ensure a smooth and enjoyable trip.\n\nExcited to hear your thoughts! Olivia',
date: '2022-12-20T18:30:00',
read: true,
labels: ['personal'],
},
{
id: 'de305d54-75b4-431b-adb2-eb6b9e546014',
name: 'James Martin',
email: 'jamesmartin@example.com',
subject: 'Re: Conference Registration',
text: 'I\'ve completed the registration for the conference next month. The event promises to be a great networking opportunity, and I\'m looking forward to attending the various sessions and connecting with industry experts.\n\nI\'ve also attached the conference schedule for your reference.\n\nIf there are any specific topics or sessions you\'d like me to explore, please let me know. It\'s an exciting event, and I\'ll make the most of it.\n\nBest regards, James',
date: '2022-11-30T19:15:00',
read: true,
labels: ['work', 'conference'],
},
{
id: '7dd90c63-00f6-40f3-bd87-5060a24e8ee7',
name: 'Sophia White',
email: 'sophiawhite@example.com',
subject: 'Team Dinner',
text: 'Let\'s have a team dinner next week to celebrate our success. We\'ve achieved some significant milestones, and it\'s time to acknowledge our hard work and dedication.\n\nI\'ve made reservations at a lovely restaurant, and I\'m sure it\'ll be an enjoyable evening.\n\nPlease confirm your availability and any dietary preferences. Looking forward to a fun and memorable dinner with the team!\n\nBest, Sophia',
date: '2022-11-05T20:30:00',
read: false,
labels: ['meeting', 'work'],
},
{
id: '99a88f78-3eb4-4d87-87b7-7b15a49a0a05',
name: 'Daniel Johnson',
email: 'danieljohnson@example.com',
subject: 'Feedback Request',
text: 'I\'d like your feedback on the latest project deliverables. We\'ve made significant progress, and I value your input to ensure we\'re on the right track.\n\nI\'ve attached the deliverables for your review, and I\'m particularly interested in any areas where you think we can further enhance the quality or efficiency.\n\nYour feedback is invaluable, and I appreciate your time and expertise. Let\'s work together to make this project a success.\n\nRegards, Daniel',
date: '2022-10-22T09:30:00',
read: false,
labels: ['work'],
},
{
id: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
name: 'Ava Taylor',
email: 'avataylor@example.com',
subject: 'Re: Meeting Agenda',
text: 'Here\'s the agenda for our meeting next week. I\'ve included all the topics we need to cover, as well as time allocations for each.\n\nIf you have any additional items to discuss or any specific points to address, please let me know, and we can integrate them into the agenda.\n\nIt\'s essential that our meeting is productive and addresses all relevant matters.\n\nLooking forward to our meeting! Ava',
date: '2022-10-10T10:45:00',
read: true,
labels: ['meeting', 'work'],
},
{
id: 'c1a0ecb4-2540-49c5-86f8-21e5ce79e4e6',
name: 'William Anderson',
email: 'williamanderson@example.com',
subject: 'Product Launch Update',
text: 'The product launch is on track. I\'ll provide an update during our call. We\'ve made substantial progress in the development and marketing of our new product.\n\nI\'m excited to share the latest updates with you during our upcoming call. It\'s crucial that we coordinate our efforts to ensure a successful launch. Please come prepared with any questions or insights you may have.\n\nLet\'s make this product launch a resounding success!\n\nBest regards, William',
date: '2022-09-20T12:00:00',
read: false,
labels: ['meeting', 'work', 'important'],
},
{
id: 'ba54eefd-4097-4949-99f2-2a9ae4d1a836',
name: 'Mia Harris',
email: 'miaharris@example.com',
subject: 'Re: Travel Itinerary',
text: 'I\'ve received the travel itinerary. It looks great! Thank you for your prompt assistance in arranging the details. I\'ve reviewed the schedule and the accommodations, and everything seems to be in order. I\'m looking forward to the trip, and I\'m confident it\'ll be a smooth and enjoyable experience.\n\nIf there are any specific activities or attractions you recommend at our destination, please feel free to share your suggestions.\n\nExcited for the trip! Mia',
date: '2022-09-10T13:15:00',
read: true,
labels: ['personal', 'travel'],
},
{
id: 'df09b6ed-28bd-4e0c-85a9-9320ec5179aa',
name: 'Ethan Clark',
email: 'ethanclark@example.com',
subject: 'Team Building Event',
text: 'Let\'s plan a team-building event for our department. Team cohesion and morale are vital to our success, and I believe a well-organized team-building event can be incredibly beneficial. I\'ve done some research and have a few ideas for fun and engaging activities.\n\nPlease let me know your thoughts and availability. We want this event to be both enjoyable and productive.\n\nTogether, we\'ll strengthen our team and boost our performance.\n\nRegards, Ethan',
date: '2022-08-25T15:30:00',
read: false,
labels: ['meeting', 'work'],
},
{
id: 'd67c1842-7f8b-4b4b-9be1-1b3b1ab4611d',
name: 'Chloe Hall',
email: 'chloehall@example.com',
subject: 'Re: Budget Approval',
text: 'The budget has been approved. We can proceed with the project. I\'m delighted to inform you that our budget proposal has received the green light from the finance department. This is a significant milestone, and it means we can move forward with the project as planned.\n\nI\'ve attached the finalized budget for your reference. Let\'s ensure that we stay on track and deliver the project on time and within budget.\n\nIt\'s an exciting time for us! Chloe',
date: '2022-08-10T16:45:00',
read: true,
labels: ['work', 'budget'],
},
{
id: '6c9a7f94-8329-4d70-95d3-51f68c186ae1',
name: 'Samuel Turner',
email: 'samuelturner@example.com',
subject: 'Weekend Hike',
text: 'Who\'s up for a weekend hike in the mountains? I\'ve been craving some outdoor adventure, and a hike in the mountains sounds like the perfect escape. If you\'re up for the challenge, we can explore some scenic trails and enjoy the beauty of nature.\n\nI\'ve done some research and have a few routes in mind.\n\nLet me know if you\'re interested, and we can plan the details.\n\nIt\'s sure to be a memorable experience! Samuel',
date: '2022-07-28T17:30:00',
read: false,
labels: ['personal'],
},
]
export type Mail = (typeof mails)[number]
export const accounts = [
{
label: 'Alicia Koch',
email: 'alicia@example.com',
icon: 'ion:logo-vercel',
},
{
label: 'Alicia Koch',
email: 'alicia@gmail.com',
icon: 'mdi:google',
},
{
label: 'Alicia Koch',
email: 'alicia@me.com',
icon: 'bx:bxl-gmail',
},
]
export type Account = (typeof accounts)[number]
export const contacts = [
{
name: 'Emma Johnson',
email: 'emma.johnson@example.com',
},
{
name: 'Liam Wilson',
email: 'liam.wilson@example.com',
},
{
name: 'Olivia Davis',
email: 'olivia.davis@example.com',
},
{
name: 'Noah Martinez',
email: 'noah.martinez@example.com',
},
{
name: 'Ava Taylor',
email: 'ava.taylor@example.com',
},
{
name: 'Lucas Brown',
email: 'lucas.brown@example.com',
},
{
name: 'Sophia Smith',
email: 'sophia.smith@example.com',
},
{
name: 'Ethan Wilson',
email: 'ethan.wilson@example.com',
},
{
name: 'Isabella Jackson',
email: 'isabella.jackson@example.com',
},
{
name: 'Mia Clark',
email: 'mia.clark@example.com',
},
{
name: 'Mason Lee',
email: 'mason.lee@example.com',
},
{
name: 'Layla Harris',
email: 'layla.harris@example.com',
},
{
name: 'William Anderson',
email: 'william.anderson@example.com',
},
{
name: 'Ella White',
email: 'ella.white@example.com',
},
{
name: 'James Thomas',
email: 'james.thomas@example.com',
},
{
name: 'Harper Lewis',
email: 'harper.lewis@example.com',
},
{
name: 'Benjamin Moore',
email: 'benjamin.moore@example.com',
},
{
name: 'Aria Hall',
email: 'aria.hall@example.com',
},
{
name: 'Henry Turner',
email: 'henry.turner@example.com',
},
{
name: 'Scarlett Adams',
email: 'scarlett.adams@example.com',
},
]
export type Contact = (typeof contacts)[number]

View File

@ -17,7 +17,7 @@ import {
} from '@tanstack/vue-table'
import { ref } from 'vue'
import { type Task } from '../data/schema'
import type { Task } from '../data/schema'
import DataTablePagination from './DataTablePagination.vue'
import DataTableToolbar from './DataTableToolbar.vue'
import { valueUpdater } from '@/lib/utils'

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { Column } from '@tanstack/vue-table'
import { type Task } from '../data/schema'
import type { Task } from '../data/schema'
import ArrowDownIcon from '~icons/radix-icons/arrow-down'
import ArrowUpIcon from '~icons/radix-icons/arrow-up'
import CaretSortIcon from '~icons/radix-icons/caret-sort'

View File

@ -109,7 +109,7 @@ const selectedValues = computed(() => new Set(props.column?.getFilterValue() as
>
<CheckIcon :class="cn('h-4 w-4')" />
</div>
<option.icon v-if="option.icon" class="mr-2 h-4 w-4 text-muted-foreground" />
<component :is="option.icon" v-if="option.icon" class="mr-2 h-4 w-4 text-muted-foreground" />
<span>{{ option.label }}</span>
<span v-if="facets?.get(option.value)" class="ml-auto flex h-4 w-4 items-center justify-center font-mono text-xs">
{{ facets.get(option.value) }}

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { type Table } from '@tanstack/vue-table'
import { type Task } from '../data/schema'
import type { Table } from '@tanstack/vue-table'
import type { Task } from '../data/schema'
import ChevronLeftIcon from '~icons/radix-icons/chevron-left'
import ChevronRightIcon from '~icons/radix-icons/chevron-right'
import DoubleArrowLeftIcon from '~icons/radix-icons/double-arrow-left'

View File

@ -3,7 +3,7 @@ import type { Row } from '@tanstack/vue-table'
import { computed } from 'vue'
import { labels } from '../data/data'
import { taskSchema } from '../data/schema'
import { type Task } from '../data/schema'
import type { Task } from '../data/schema'
import DotsHorizontalIcon from '~icons/radix-icons/dots-horizontal'
import { Button } from '@/lib/registry/new-york/ui/button'

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { type Table } from '@tanstack/vue-table'
import type { Table } from '@tanstack/vue-table'
import { computed } from 'vue'
import { type Task } from '../data/schema'
import type { Task } from '../data/schema'
import { priorities, statuses } from '../data/data'
import DataTableFacetedFilter from './DataTableFacetedFilter.vue'

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import type { Table } from '@tanstack/vue-table'
import { computed } from 'vue'
import { type Task } from '../data/schema'
import type { Task } from '../data/schema'
import MixerHorizontalIcon from '~icons/radix-icons/mixer-horizontal'
import { Button } from '@/lib/registry/new-york/ui/button'

View File

@ -11,10 +11,13 @@ import { Badge } from '@/lib/registry/new-york/ui/badge'
export const columns: ColumnDef<Task>[] = [
{
id: 'select',
header: ({ table }) => h(Checkbox,
{ 'checked': table.getIsAllPageRowsSelected(), 'onUpdate:checked': value => table.toggleAllPageRowsSelected(!!value), 'ariaLabel': 'Select all', 'class': 'translate-y-0.5' }),
cell: ({ row }) => h(Checkbox,
{ 'checked': row.getIsSelected(), 'onUpdate:checked': value => row.toggleSelected(!!value), 'ariaLabel': 'Select row', 'class': 'translate-y-0.5' }),
header: ({ table }) => h(Checkbox, {
'checked': table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && 'indeterminate'),
'onUpdate:checked': value => table.toggleAllPageRowsSelected(!!value),
'ariaLabel': 'Select all',
'class': 'translate-y-0.5',
}),
cell: ({ row }) => h(Checkbox, { 'checked': row.getIsSelected(), 'onUpdate:checked': value => row.toggleSelected(!!value), 'ariaLabel': 'Select row', 'class': 'translate-y-0.5' }),
enableSorting: false,
enableHiding: false,
},
@ -33,7 +36,7 @@ export const columns: ColumnDef<Task>[] = [
const label = labels.find(label => label.value === row.original.label)
return h('div', { class: 'flex space-x-2' }, [
label && h(Badge, { variant: 'outline' }, label.label),
label ? h(Badge, { variant: 'outline' }, () => label.label) : null,
h('span', { class: 'max-w-[500px] truncate font-medium' }, row.getValue('title')),
])
},
@ -72,7 +75,7 @@ export const columns: ColumnDef<Task>[] = [
return h('div', { class: 'flex items-center' }, [
priority.icon && h(priority.icon, { class: 'mr-2 h-4 w-4 text-muted-foreground' }),
h('span', priority.label),
h('span', {}, priority.label),
])
},
filterFn: (row, id, value) => {

View File

@ -81,7 +81,7 @@ const columns: ColumnDef<Payment>[] = [
{
id: 'select',
header: ({ table }) => h(Checkbox, {
'checked': table.getIsAllPageRowsSelected(),
'checked': table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && 'indeterminate'),
'onUpdate:checked': value => table.toggleAllPageRowsSelected(!!value),
'ariaLabel': 'Select all',
}),

View File

@ -0,0 +1,72 @@
<script setup lang="ts">
import { ref } from 'vue'
import { watchOnce } from '@vueuse/core'
import { Carousel, type CarouselApi, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@/lib/registry/default/ui/carousel'
import { Card, CardContent } from '@/lib/registry/default/ui/card'
const emblaMainApi = ref<CarouselApi>()
const emblaThumbnailApi = ref<CarouselApi>()
const selectedIndex = ref(0)
function onSelect() {
if (!emblaMainApi.value || !emblaThumbnailApi.value)
return
selectedIndex.value = emblaMainApi.value.selectedScrollSnap()
emblaThumbnailApi.value.scrollTo(emblaMainApi.value.selectedScrollSnap())
}
function onThumbClick(index: number) {
if (!emblaMainApi.value || !emblaThumbnailApi.value)
return
emblaMainApi.value.scrollTo(index)
}
watchOnce(emblaMainApi, (emblaMainApi) => {
if (!emblaMainApi)
return
onSelect()
emblaMainApi.on('select', onSelect)
emblaMainApi.on('reInit', onSelect)
})
</script>
<template>
<div class="w-full sm:w-auto">
<Carousel
class="relative w-full max-w-xs"
@init-api="(val) => emblaMainApi = val"
>
<CarouselContent>
<CarouselItem v-for="(_, index) in 10" :key="index">
<div class="p-1">
<Card>
<CardContent class="flex aspect-square items-center justify-center p-6">
<span class="text-4xl font-semibold">{{ index + 1 }}</span>
</CardContent>
</Card>
</div>
</CarouselItem>
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
<Carousel
class="relative w-full max-w-xs"
@init-api="(val) => emblaThumbnailApi = val"
>
<CarouselContent class="flex gap-1 ml-0">
<CarouselItem v-for="(_, index) in 10" :key="index" class="pl-0 basis-1/4 cursor-pointer" @click="onThumbClick(index)">
<div class="p-1" :class="index === selectedIndex ? '' : 'opacity-50'">
<Card>
<CardContent class="flex aspect-square items-center justify-center p-6">
<span class="text-4xl font-semibold">{{ index + 1 }}</span>
</CardContent>
</Card>
</div>
</CarouselItem>
</CarouselContent>
</Carousel>
</div>
</template>

View File

@ -0,0 +1,94 @@
<script lang="ts" setup>
import { createReusableTemplate, useMediaQuery } from '@vueuse/core'
import { ref } from 'vue'
import { Button } from '@/lib/registry/default/ui/button'
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/lib/registry/default/ui/command'
import { Drawer, DrawerContent, DrawerTrigger } from '@/lib/registry/default/ui/drawer'
import { Popover, PopoverContent, PopoverTrigger } from '@/lib/registry/default/ui/popover'
interface Status {
value: string
label: string
}
const statuses: Status[] = [
{
value: 'backlog',
label: 'Backlog',
},
{
value: 'todo',
label: 'Todo',
},
{
value: 'in progress',
label: 'In Progress',
},
{
value: 'done',
label: 'Done',
},
{
value: 'canceled',
label: 'Canceled',
},
]
const [UseTemplate, StatusList] = createReusableTemplate()
const isDesktop = useMediaQuery('(min-width: 768px)')
const isOpen = ref(false)
const selectedStatus = ref<Status | null>(null)
function onStatusSelect(status: Status) {
selectedStatus.value = status
isOpen.value = false
}
</script>
<template>
<div>
<UseTemplate>
<Command>
<CommandInput placeholder="Filter status..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
<CommandItem
v-for="status of statuses"
:key="status.value"
:value="status.value"
@select="onStatusSelect(status)"
>
{{ status.label }}
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</UseTemplate>
<Popover v-if="isDesktop" v-model:open="isOpen">
<PopoverTrigger as-child>
<Button variant="outline" class="w-[150px] justify-start">
{{ selectedStatus ? selectedStatus.label : "+ Set status" }}
</Button>
</PopoverTrigger>
<PopoverContent class="w-[200px] p-0" align="start">
<StatusList />
</PopoverContent>
</Popover>
<Drawer v-else :open="isOpen" @update:open="(newOpenValue) => isOpen = newOpenValue">
<DrawerTrigger as-child>
<Button variant="outline" class="w-[150px] justify-start">
{{ selectedStatus ? selectedStatus.label : "+ Set status" }}
</Button>
</DrawerTrigger>
<DrawerContent>
<div class="mt-4 border-t">
<StatusList />
</div>
</DrawerContent>
</Drawer>
</div>
</template>

View File

@ -84,11 +84,11 @@ const columns = [
columnHelper.display({
id: 'select',
header: ({ table }) => h(Checkbox, {
'checked': table.getIsAllPageRowsSelected(),
'checked': table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && 'indeterminate'),
'onUpdate:checked': value => table.toggleAllPageRowsSelected(!!value),
'ariaLabel': 'Select all',
}),
cell: ({ row, column }) => {
cell: ({ row }) => {
return h(Checkbox, {
'checked': row.getIsSelected(),
'onUpdate:checked': value => row.toggleSelected(!!value),
@ -165,8 +165,6 @@ const table = useVueTable({
},
},
})
const getState = table.getState()
</script>
<template>

View File

@ -80,7 +80,7 @@ const columns: ColumnDef<Payment>[] = [
{
id: 'select',
header: ({ table }) => h(Checkbox, {
'checked': table.getIsAllPageRowsSelected(),
'checked': table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && 'indeterminate'),
'onUpdate:checked': value => table.toggleAllPageRowsSelected(!!value),
'ariaLabel': 'Select all',
}),

View File

@ -0,0 +1,111 @@
<script setup lang="ts">
import { ref } from 'vue'
import { Minus, Plus } from 'lucide-vue-next'
import { VisStackedBar, VisXYContainer } from '@unovis/vue'
import { Button } from '@/lib/registry/default/ui/button'
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from '@/lib/registry/default/ui/drawer'
const goal = ref(350)
type Data = typeof data[number]
const data = [
{ goal: 400 },
{ goal: 300 },
{ goal: 200 },
{ goal: 300 },
{ goal: 200 },
{ goal: 278 },
{ goal: 189 },
{ goal: 239 },
{ goal: 300 },
{ goal: 200 },
{ goal: 278 },
{ goal: 189 },
{ goal: 349 },
]
</script>
<template>
<Drawer>
<DrawerTrigger as-child>
<Button variant="outline">
Open Drawer
</Button>
</DrawerTrigger>
<DrawerContent>
<div class="mx-auto w-full max-w-sm">
<DrawerHeader>
<DrawerTitle>Move Goal</DrawerTitle>
<DrawerDescription>Set your daily activity goal.</DrawerDescription>
</DrawerHeader>
<div class="p-4 pb-0">
<div class="flex items-center justify-center space-x-2">
<Button
variant="outline"
size="icon"
class="h-8 w-8 shrink-0 rounded-full"
:disabled="goal <= 200"
@click="goal -= 10"
>
<Minus class="h-4 w-4" />
<span class="sr-only">Decrease</span>
</Button>
<div class="flex-1 text-center">
<div class="text-7xl font-bold tracking-tighter">
{{ goal }}
</div>
<div class="text-[0.70rem] uppercase text-muted-foreground">
Calories/day
</div>
</div>
<Button
variant="outline"
size="icon"
class="h-8 w-8 shrink-0 rounded-full"
:disabled="goal >= 400"
@click="goal += 10"
>
<Plus class="h-4 w-4" />
<span class="sr-only">Increase</span>
</Button>
</div>
<div class="my-3 px-3 h-[120px]">
<VisXYContainer
:data="data"
class="h-[120px]"
:style="{
'opacity': 0.9,
'--theme-primary': `hsl(var(--foreground))`,
}"
>
<VisStackedBar
:x="(d: Data, i :number) => i"
:y="(d: Data) => d.goal"
color="var(--theme-primary)"
:bar-padding="0.1"
:rounded-corners="0"
/>
</VisXYContainer>
</div>
</div>
<DrawerFooter>
<Button>Submit</Button>
<DrawerClose as-child>
<Button variant="outline">
Cancel
</Button>
</DrawerClose>
</DrawerFooter>
</div>
</DrawerContent>
</Drawer>
</template>

View File

@ -0,0 +1,90 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { createReusableTemplate, useMediaQuery } from '@vueuse/core'
import { Button } from '@/lib/registry/default/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/lib/registry/default/ui/dialog'
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from '@/lib/registry/default/ui/drawer'
import { Label } from '@/lib/registry/default/ui/label'
import { Input } from '@/lib/registry/default/ui/input'
// Reuse `form` section
const [UseTemplate, GridForm] = createReusableTemplate()
const isDesktop = useMediaQuery('(min-width: 768px)')
const isOpen = ref(false)
</script>
<template>
<UseTemplate>
<form class="grid items-start gap-4 px-4">
<div class="grid gap-2">
<Label html-for="email">Email</Label>
<Input id="email" type="email" default-value="shadcn@example.com" />
</div>
<div class="grid gap-2">
<Label html-for="username">Username</Label>
<Input id="username" default-value="@shadcn" />
</div>
<Button type="submit">
Save changes
</Button>
</form>
</UseTemplate>
<Dialog v-if="isDesktop" v-model:open="isOpen">
<DialogTrigger as-child>
<Button variant="outline">
Edit Profile
</Button>
</DialogTrigger>
<DialogContent class="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>
Make changes to your profile here. Click save when you're done.
</DialogDescription>
</DialogHeader>
<GridForm />
</DialogContent>
</Dialog>
<Drawer v-else v-model:open="isOpen">
<DrawerTrigger as-child>
<Button variant="outline">
Edit Profile
</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader class="text-left">
<DrawerTitle>Edit profile</DrawerTitle>
<DrawerDescription>
Make changes to your profile here. Click save when you're done.
</DrawerDescription>
</DrawerHeader>
<GridForm />
<DrawerFooter class="pt-2">
<DrawerClose as-child>
<Button variant="outline">
Cancel
</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
</template>

View File

@ -0,0 +1,49 @@
<script lang="ts" setup>
import type { DropdownMenuCheckboxItemProps } from 'radix-vue'
import { ref } from 'vue'
import { Button } from '@/lib/registry/default/ui/button'
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/lib/registry/default/ui/dropdown-menu'
type Checked = DropdownMenuCheckboxItemProps['checked']
const showStatusBar = ref<Checked>(true)
const showActivityBar = ref<Checked>(false)
const showPanel = ref<Checked>(false)
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="outline">
Open
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent class="w-56">
<DropdownMenuLabel>Appearance</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem
v-model:checked="showStatusBar"
>
Status Bar
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
v-model:checked="showActivityBar"
disabled
>
Activity Bar
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
v-model:checked="showPanel"
>
Panel
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
</template>

View File

@ -0,0 +1,40 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { Button } from '@/lib/registry/default/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/lib/registry/default/ui/dropdown-menu'
const position = ref('bottom')
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="outline">
Open
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent class="w-56">
<DropdownMenuLabel>Panel Position</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup v-model="position">
<DropdownMenuRadioItem value="top">
Top
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="bottom">
Bottom
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="right">
Right
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</template>

View File

@ -49,5 +49,5 @@ const onSubmit = handleSubmit((values) => {
<Button type="submit">
Submit
</Button>
</Form>
</form>
</template>

View File

@ -50,5 +50,5 @@ const onSubmit = handleSubmit((values) => {
<Button type="submit">
Submit
</Button>
</Form>
</form>
</template>

View File

@ -0,0 +1,30 @@
<script setup lang="ts">
import { ref } from 'vue'
import {
PinInput,
PinInputGroup,
PinInputInput,
} from '@/lib/registry/default/ui/pin-input'
const value = ref<string[]>(['1', '2', '3'])
const handleComplete = (e: string[]) => alert(e.join(''))
</script>
<template>
<div>
<PinInput
id="pin-input"
v-model="value"
placeholder="○"
@complete="handleComplete"
>
<PinInputGroup>
<PinInputInput
v-for="(id, index) in 5"
:key="id"
:index="index"
/>
</PinInputGroup>
</PinInput>
</div>
</template>

View File

@ -2,6 +2,7 @@
import { ref } from 'vue'
import {
PinInput,
PinInputGroup,
PinInputInput,
} from '@/lib/registry/default/ui/pin-input'
@ -15,14 +16,15 @@ const handleComplete = (e: string[]) => alert(e.join(''))
id="pin-input"
v-model="value"
placeholder="○"
class="flex gap-2 items-center mt-1"
@complete="handleComplete"
>
<PinInputInput
v-for="(id, index) in 5"
:key="id"
:index="index"
/>
<PinInputGroup>
<PinInputInput
v-for="(id, index) in 5"
:key="id"
:index="index"
/>
</PinInputGroup>
</PinInput>
</div>
</template>

View File

@ -0,0 +1,29 @@
<script setup lang="ts">
import { ref } from 'vue'
import {
PinInput,
PinInputGroup,
PinInputInput,
} from '@/lib/registry/default/ui/pin-input'
const value = ref<string[]>([])
</script>
<template>
<div>
<PinInput
id="pin-input"
v-model="value"
placeholder="○"
disabled
>
<PinInputGroup>
<PinInputInput
v-for="(id, index) in 5"
:key="id"
:index="index"
/>
</PinInputGroup>
</PinInput>
</div>
</template>

View File

@ -5,8 +5,9 @@ import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import {
PinInput,
PinInputGroup,
PinInputInput,
} from '@/lib/registry/new-york/ui/pin-input'
} from '@/lib/registry/default/ui/pin-input'
import { Button } from '@/lib/registry/default/ui/button'
import {
FormControl,
@ -25,7 +26,7 @@ const formSchema = toTypedSchema(z.object({
const { handleSubmit, setValues } = useForm({
validationSchema: formSchema,
initialValues: {
pin: [],
pin: ['1', '2', '3'],
},
})
@ -59,11 +60,13 @@ const handleComplete = (e: string[]) => console.log(e.join(''))
})
}"
>
<PinInputInput
v-for="(id, index) in 5"
:key="id"
:index="index"
/>
<PinInputGroup>
<PinInputInput
v-for="(id, index) in 5"
:key="id"
:index="index"
/>
</PinInputGroup>
</PinInput>
</FormControl>
<FormDescription>

View File

@ -0,0 +1,35 @@
<script setup lang="ts">
import { ref } from 'vue'
import {
PinInput,
PinInputGroup,
PinInputInput,
PinInputSeparator,
} from '@/lib/registry/default/ui/pin-input'
const value = ref<string[]>([])
const handleComplete = (e: string[]) => alert(e.join(''))
</script>
<template>
<div>
<PinInput
id="pin-input"
v-model="value"
placeholder="○"
@complete="handleComplete"
>
<PinInputGroup class="gap-1">
<template v-for="(id, index) in 5" :key="id">
<PinInputInput
class="rounded-md border"
:index="index"
/>
<template v-if="index !== 4">
<PinInputSeparator />
</template>
</template>
</PinInputGroup>
</PinInput>
</div>
</template>

View File

@ -0,0 +1,37 @@
<script setup lang="ts">
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from '@/lib/registry/default/ui/resizable'
</script>
<template>
<ResizablePanelGroup
id="demo-group-1"
direction="horizontal"
class="max-w-md rounded-lg border"
>
<ResizablePanel id="demo-panel-1" :default-size="50">
<div class="flex h-[200px] items-center justify-center p-6">
<span class="font-semibold">One</span>
</div>
</ResizablePanel>
<ResizableHandle id="demo-handle-1" />
<ResizablePanel id="demo-panel-2" :default-size="50">
<ResizablePanelGroup id="demo-group-2" direction="vertical">
<ResizablePanel id="demo-panel-3" :default-size="25">
<div class="flex h-full items-center justify-center p-6">
<span class="font-semibold">Two</span>
</div>
</ResizablePanel>
<ResizableHandle id="demo-handle-2" />
<ResizablePanel id="demo-panel-4" :default-size="75">
<div class="flex h-full items-center justify-center p-6">
<span class="font-semibold">Three</span>
</div>
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
</ResizablePanelGroup>
</template>

View File

@ -0,0 +1,27 @@
<script setup lang="ts">
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from '@/lib/registry/default/ui/resizable'
</script>
<template>
<ResizablePanelGroup
id="handle-demo-group-1"
direction="horizontal"
class="min-h-[200px] max-w-md rounded-lg border"
>
<ResizablePanel id="handle-demo-panel-1" :default-size="25">
<div class="flex h-full items-center justify-center p-6">
<span class="font-semibold">Sidebar</span>
</div>
</ResizablePanel>
<ResizableHandle id="handle-demo-handle-1" with-handle />
<ResizablePanel id="handle-demo-panel-2" :default-size="75">
<div class="flex h-full items-center justify-center p-6">
<span class="font-semibold">Content</span>
</div>
</ResizablePanel>
</ResizablePanelGroup>
</template>

View File

@ -0,0 +1,27 @@
<script setup lang="ts">
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from '@/lib/registry/default/ui/resizable'
</script>
<template>
<ResizablePanelGroup
id="vertical-demo-group-1"
direction="vertical"
class="min-h-[200px] max-w-md rounded-lg border"
>
<ResizablePanel id="vertical-demo-panel-1" :default-size="25">
<div class="flex h-full items-center justify-center p-6">
<span class="font-semibold">Header</span>
</div>
</ResizablePanel>
<ResizableHandle id="vertical-demo-handle-1" />
<ResizablePanel id="vertical-demo-panel-2" :default-size="75">
<div class="flex h-full items-center justify-center p-6">
<span class="font-semibold">Content</span>
</div>
</ResizablePanel>
</ResizablePanelGroup>
</template>

View File

@ -0,0 +1,117 @@
<script lang="ts" setup>
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from '@/lib/registry/default/ui/select'
</script>
<template>
<Select>
<SelectTrigger class="w-[280px]">
<SelectValue placeholder="Select a timezone" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>North America</SelectLabel>
<SelectItem value="est">
Eastern Standard Time (EST)
</SelectItem>
<SelectItem value="cst">
Central Standard Time (CST)
</SelectItem>
<SelectItem value="mst">
Mountain Standard Time (MST)
</SelectItem>
<SelectItem value="pst">
Pacific Standard Time (PST)
</SelectItem>
<SelectItem value="akst">
Alaska Standard Time (AKST)
</SelectItem>
<SelectItem value="hst">
Hawaii Standard Time (HST)
</SelectItem>
</SelectGroup>
<SelectGroup>
<SelectLabel>Europe & Africa</SelectLabel>
<SelectItem value="gmt">
Greenwich Mean Time (GMT)
</SelectItem>
<SelectItem value="cet">
Central European Time (CET)
</SelectItem>
<SelectItem value="eet">
Eastern European Time (EET)
</SelectItem>
<SelectItem value="west">
Western European Summer Time (WEST)
</SelectItem>
<SelectItem value="cat">
Central Africa Time (CAT)
</SelectItem>
<SelectItem value="eat">
East Africa Time (EAT)
</SelectItem>
</SelectGroup>
<SelectGroup>
<SelectLabel>Asia</SelectLabel>
<SelectItem value="msk">
Moscow Time (MSK)
</SelectItem>
<SelectItem value="ist">
India Standard Time (IST)
</SelectItem>
<SelectItem value="cst_china">
China Standard Time (CST)
</SelectItem>
<SelectItem value="jst">
Japan Standard Time (JST)
</SelectItem>
<SelectItem value="kst">
Korea Standard Time (KST)
</SelectItem>
<SelectItem value="ist_indonesia">
Indonesia Central Standard Time (WITA)
</SelectItem>
</SelectGroup>
<SelectGroup>
<SelectLabel>Australia & Pacific</SelectLabel>
<SelectItem value="awst">
Australian Western Standard Time (AWST)
</SelectItem>
<SelectItem value="acst">
Australian Central Standard Time (ACST)
</SelectItem>
<SelectItem value="aest">
Australian Eastern Standard Time (AEST)
</SelectItem>
<SelectItem value="nzst">
New Zealand Standard Time (NZST)
</SelectItem>
<SelectItem value="fjt">
Fiji Time (FJT)
</SelectItem>
</SelectGroup>
<SelectGroup>
<SelectLabel>South America</SelectLabel>
<SelectItem value="art">
Argentina Time (ART)
</SelectItem>
<SelectItem value="bot">
Bolivia Time (BOT)
</SelectItem>
<SelectItem value="brt">
Brasilia Time (BRT)
</SelectItem>
<SelectItem value="clt">
Chile Standard Time (CLT)
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</template>

View File

@ -0,0 +1,13 @@
<script lang="ts" setup>
import { Skeleton } from '@/lib/registry/default/ui/skeleton'
</script>
<template>
<div class="flex flex-col space-y-3">
<Skeleton class="h-[125px] w-[250px] rounded-xl" />
<div class="space-y-2">
<Skeleton class="h-4 w-[250px]" />
<Skeleton class="h-4 w-[200px]" />
</div>
</div>
</template>

View File

@ -0,0 +1,66 @@
<script setup lang="ts">
import { h } from 'vue'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { Button } from '@/lib/registry/default/ui/button'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/lib/registry/default/ui/form'
import { Slider } from '@/lib/registry/default/ui/slider'
import { toast } from '@/lib/registry/default/ui/toast'
const formSchema = toTypedSchema(z.object({
duration: z.array(
z.number().min(0).max(60),
),
}))
const { handleSubmit } = useForm({
validationSchema: formSchema,
initialValues: {
duration: [30],
},
})
const onSubmit = handleSubmit((values) => {
toast({
title: 'You submitted the following values:',
description: h('pre', { class: 'mt-2 w-[340px] rounded-md bg-slate-950 p-4' }, h('code', { class: 'text-white' }, JSON.stringify(values, null, 2))),
})
})
</script>
<template>
<form class="w-2/3 space-y-6" @submit="onSubmit">
<FormField v-slot="{ componentField, value }" name="duration">
<FormItem>
<FormLabel>Duration</FormLabel>
<FormControl>
<Slider
v-bind="componentField"
:default-value="[30]"
:max="100"
:min="0"
:step="5"
/>
<FormDescription class="flex justify-between">
<span>How many minutes are you available?</span>
<span>{{ value?.[0] }} min</span>
</FormDescription>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<Button type="submit">
Submit
</Button>
</form>
</template>

View File

@ -4,7 +4,9 @@ import type { ComboboxContentEmits, ComboboxContentProps } from 'radix-vue'
import { ComboboxContent, useForwardPropsEmits } from 'radix-vue'
import { cn } from '@/lib/utils'
const props = defineProps<ComboboxContentProps & { class?: HTMLAttributes['class'] }>()
const props = withDefaults(defineProps<ComboboxContentProps & { class?: HTMLAttributes['class'] }>(), {
dismissable: false,
})
const emits = defineEmits<ComboboxContentEmits>()
const delegatedProps = computed(() => {

View File

@ -37,6 +37,13 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
)
"
v-bind="forwarded"
@pointer-down-outside="(event) => {
const originalEvent = event.detail.originalEvent;
const target = originalEvent.target as HTMLElement;
if (originalEvent.offsetX > target.clientWidth || originalEvent.offsetY > target.clientHeight) {
event.preventDefault();
}
}"
>
<slot />

View File

@ -0,0 +1,19 @@
<script lang="ts" setup>
import type { DrawerRootEmits, DrawerRootProps } from 'vaul-vue'
import { DrawerRoot } from 'vaul-vue'
import { useForwardPropsEmits } from 'radix-vue'
const props = withDefaults(defineProps<DrawerRootProps>(), {
shouldScaleBackground: true,
})
const emits = defineEmits<DrawerRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DrawerRoot v-bind="forwarded">
<slot />
</DrawerRoot>
</template>

View File

@ -0,0 +1,28 @@
<script lang="ts" setup>
import { DrawerContent, DrawerPortal } from 'vaul-vue'
import type { DialogContentEmits, DialogContentProps } from 'radix-vue'
import { useForwardPropsEmits } from 'radix-vue'
import type { HtmlHTMLAttributes } from 'vue'
import DrawerOverlay from './DrawerOverlay.vue'
import { cn } from '@/lib/utils'
const props = defineProps<DialogContentProps & { class?: HtmlHTMLAttributes['class'] }>()
const emits = defineEmits<DialogContentEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DrawerPortal>
<DrawerOverlay />
<DrawerContent
v-bind="forwarded" :class="cn(
'fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background',
props.class,
)"
>
<div class="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
<slot />
</DrawerContent>
</DrawerPortal>
</template>

View File

@ -0,0 +1,20 @@
<script lang="ts" setup>
import type { DrawerDescriptionProps } from 'vaul-vue'
import { DrawerDescription } from 'vaul-vue'
import { type HtmlHTMLAttributes, computed } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<DrawerDescriptionProps & { class?: HtmlHTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<DrawerDescription v-bind="delegatedProps" :class="cn('text-sm text-muted-foreground', props.class)">
<slot />
</DrawerDescription>
</template>

View File

@ -0,0 +1,14 @@
<script lang="ts" setup>
import type { HtmlHTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HtmlHTMLAttributes['class']
}>()
</script>
<template>
<div :class="cn('mt-auto flex flex-col gap-2 p-4', props.class)">
<slot />
</div>
</template>

View File

@ -0,0 +1,14 @@
<script lang="ts" setup>
import type { HtmlHTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HtmlHTMLAttributes['class']
}>()
</script>
<template>
<div :class="cn('grid gap-1.5 p-4 text-center sm:text-left', props.class)">
<slot />
</div>
</template>

View File

@ -0,0 +1,18 @@
<script lang="ts" setup>
import { DrawerOverlay } from 'vaul-vue'
import type { DialogOverlayProps } from 'radix-vue'
import { type HtmlHTMLAttributes, computed } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<DialogOverlayProps & { class?: HtmlHTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<DrawerOverlay v-bind="delegatedProps" :class="cn('fixed inset-0 z-50 bg-black/80', props.class)" />
</template>

View File

@ -0,0 +1,20 @@
<script lang="ts" setup>
import type { DrawerTitleProps } from 'vaul-vue'
import { DrawerTitle } from 'vaul-vue'
import { type HtmlHTMLAttributes, computed } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<DrawerTitleProps & { class?: HtmlHTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<DrawerTitle v-bind="delegatedProps" :class="cn('text-lg font-semibold leading-none tracking-tight', props.class)">
<slot />
</DrawerTitle>
</template>

View File

@ -0,0 +1,8 @@
export { DrawerPortal, DrawerTrigger, DrawerClose } from 'vaul-vue'
export { default as Drawer } from './Drawer.vue'
export { default as DrawerOverlay } from './DrawerOverlay.vue'
export { default as DrawerContent } from './DrawerContent.vue'
export { default as DrawerHeader } from './DrawerHeader.vue'
export { default as DrawerFooter } from './DrawerFooter.vue'
export { default as DrawerTitle } from './DrawerTitle.vue'
export { default as DrawerDescription } from './DrawerDescription.vue'

View File

@ -0,0 +1,18 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import { Primitive, type PrimitiveProps, useForwardProps } from 'radix-vue'
import { cn } from '@/lib/utils'
const props = defineProps<PrimitiveProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<Primitive v-bind="forwardedProps" :class="cn('flex items-center', props.class)">
<slot />
</primitive>
</template>

View File

@ -14,5 +14,5 @@ const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<PinInputInput v-bind="forwardedProps" :class="cn('flex w-10 h-10 text-center rounded-md border border-input bg-background text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', props.class)" />
<PinInputInput v-bind="forwardedProps" :class="cn('relative text-center focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md', props.class)" />
</template>

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
import { Primitive, type PrimitiveProps, useForwardProps } from 'radix-vue'
import { Dot } from 'lucide-vue-next'
const props = defineProps<PrimitiveProps>()
const forwardedProps = useForwardProps(props)
</script>
<template>
<Primitive v-bind="forwardedProps">
<slot>
<Dot />
</slot>
</primitive>
</template>

View File

@ -1,2 +1,4 @@
export { default as PinInput } from './PinInput.vue'
export { default as PinInputGroup } from './PinInputGroup.vue'
export { default as PinInputSeparator } from './PinInputSeparator.vue'
export { default as PinInputInput } from './PinInputInput.vue'

View File

@ -0,0 +1,26 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import { SplitterResizeHandle, type SplitterResizeHandleEmits, type SplitterResizeHandleProps, useForwardPropsEmits } from 'radix-vue'
import { GripVertical } from 'lucide-vue-next'
import { cn } from '@/lib/utils'
const props = defineProps<SplitterResizeHandleProps & { class?: HTMLAttributes['class'], withHandle?: boolean }>()
const emits = defineEmits<SplitterResizeHandleEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<SplitterResizeHandle v-bind="forwarded" :class="cn('relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 [&[data-orientation=vertical]]:h-px [&[data-orientation=vertical]]:w-full [&[data-orientation=vertical]]:after:left-0 [&[data-orientation=vertical]]:after:h-1 [&[data-orientation=vertical]]:after:w-full [&[data-orientation=vertical]]:after:-translate-y-1/2 [&[data-orientation=vertical]]:after:translate-x-0 [&[data-orientation=vertical]>div]:rotate-90', props.class)">
<template v-if="props.withHandle">
<div class="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
<GripVertical class="h-2.5 w-2.5" />
</div>
</template>
</SplitterResizeHandle>
</template>

View File

@ -0,0 +1,21 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import { SplitterGroup, type SplitterGroupEmits, type SplitterGroupProps, useForwardPropsEmits } from 'radix-vue'
import { cn } from '@/lib/utils'
const props = defineProps<SplitterGroupProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<SplitterGroupEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<SplitterGroup v-bind="forwarded" :class="cn('flex h-full w-full data-[panel-group-direction=vertical]:flex-col', props.class)">
<slot />
</SplitterGroup>
</template>

View File

@ -0,0 +1,3 @@
export { default as ResizablePanelGroup } from './ResizablePanelGroup.vue'
export { default as ResizableHandle } from './ResizableHandle.vue'
export { SplitterPanel as ResizablePanel } from 'radix-vue'

View File

@ -7,15 +7,16 @@ const props = defineProps<ToasterProps>()
<template>
<Sonner
class="toaster group"
:class-names="{
toast:
'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
description: 'group-[.toast]:text-muted-foreground',
actionButton:
'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
cancelButton:
'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
}"
v-bind="props"
:toast-options="{
classes: {
toast: 'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
description: 'group-[.toast]:text-muted-foreground',
actionButton:
'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
cancelButton:
'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
},
}"
/>
</template>

View File

@ -81,7 +81,7 @@ const columns: ColumnDef<Payment>[] = [
{
id: 'select',
header: ({ table }) => h(Checkbox, {
'checked': table.getIsAllPageRowsSelected(),
'checked': table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && 'indeterminate'),
'onUpdate:checked': value => table.toggleAllPageRowsSelected(!!value),
'ariaLabel': 'Select all',
}),
@ -104,7 +104,7 @@ const columns: ColumnDef<Payment>[] = [
return h(Button, {
variant: 'ghost',
onClick: () => column.toggleSorting(column.getIsSorted() === 'asc'),
}, ['Email', h(CaretSortIcon, { class: 'ml-2 h-4 w-4' })])
}, () => ['Email', h(CaretSortIcon, { class: 'ml-2 h-4 w-4' })])
},
cell: ({ row }) => h('div', { class: 'lowercase' }, row.getValue('email')),
},

View File

@ -0,0 +1,72 @@
<script setup lang="ts">
import { ref } from 'vue'
import { watchOnce } from '@vueuse/core'
import { Carousel, type CarouselApi, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@/lib/registry/new-york/ui/carousel'
import { Card, CardContent } from '@/lib/registry/new-york/ui/card'
const emblaMainApi = ref<CarouselApi>()
const emblaThumbnailApi = ref<CarouselApi>()
const selectedIndex = ref(0)
function onSelect() {
if (!emblaMainApi.value || !emblaThumbnailApi.value)
return
selectedIndex.value = emblaMainApi.value.selectedScrollSnap()
emblaThumbnailApi.value.scrollTo(emblaMainApi.value.selectedScrollSnap())
}
function onThumbClick(index: number) {
if (!emblaMainApi.value || !emblaThumbnailApi.value)
return
emblaMainApi.value.scrollTo(index)
}
watchOnce(emblaMainApi, (emblaMainApi) => {
if (!emblaMainApi)
return
onSelect()
emblaMainApi.on('select', onSelect)
emblaMainApi.on('reInit', onSelect)
})
</script>
<template>
<div class="w-full sm:w-auto">
<Carousel
class="relative w-full max-w-xs"
@init-api="(val) => emblaMainApi = val"
>
<CarouselContent>
<CarouselItem v-for="(_, index) in 10" :key="index">
<div class="p-1">
<Card>
<CardContent class="flex aspect-square items-center justify-center p-6">
<span class="text-4xl font-semibold">{{ index + 1 }}</span>
</CardContent>
</Card>
</div>
</CarouselItem>
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
<Carousel
class="relative w-full max-w-xs"
@init-api="(val) => emblaThumbnailApi = val"
>
<CarouselContent class="flex gap-1 ml-0">
<CarouselItem v-for="(_, index) in 10" :key="index" class="pl-0 basis-1/4 cursor-pointer" @click="onThumbClick(index)">
<div class="p-1" :class="index === selectedIndex ? '' : 'opacity-50'">
<Card>
<CardContent class="flex aspect-square items-center justify-center p-6">
<span class="text-4xl font-semibold">{{ index + 1 }}</span>
</CardContent>
</Card>
</div>
</CarouselItem>
</CarouselContent>
</Carousel>
</div>
</template>

View File

@ -0,0 +1,94 @@
<script lang="ts" setup>
import { createReusableTemplate, useMediaQuery } from '@vueuse/core'
import { ref } from 'vue'
import { Button } from '@/lib/registry/new-york/ui/button'
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/lib/registry/new-york/ui/command'
import { Drawer, DrawerContent, DrawerTrigger } from '@/lib/registry/new-york/ui/drawer'
import { Popover, PopoverContent, PopoverTrigger } from '@/lib/registry/new-york/ui/popover'
interface Status {
value: string
label: string
}
const statuses: Status[] = [
{
value: 'backlog',
label: 'Backlog',
},
{
value: 'todo',
label: 'Todo',
},
{
value: 'in progress',
label: 'In Progress',
},
{
value: 'done',
label: 'Done',
},
{
value: 'canceled',
label: 'Canceled',
},
]
const [UseTemplate, StatusList] = createReusableTemplate()
const isDesktop = useMediaQuery('(min-width: 768px)')
const isOpen = ref(false)
const selectedStatus = ref<Status | null>(null)
function onStatusSelect(status: Status) {
selectedStatus.value = status
isOpen.value = false
}
</script>
<template>
<div>
<UseTemplate>
<Command>
<CommandInput placeholder="Filter status..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
<CommandItem
v-for="status of statuses"
:key="status.value"
:value="status.value"
@select="onStatusSelect(status)"
>
{{ status.label }}
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</UseTemplate>
<Popover v-if="isDesktop" v-model:open="isOpen">
<PopoverTrigger as-child>
<Button variant="outline" class="w-[150px] justify-start">
{{ selectedStatus ? selectedStatus.label : "+ Set status" }}
</Button>
</PopoverTrigger>
<PopoverContent class="w-[200px] p-0" align="start">
<StatusList />
</PopoverContent>
</Popover>
<Drawer v-else v-model:open="isOpen">
<DrawerTrigger as-child>
<Button variant="outline" class="w-[150px] justify-start">
{{ selectedStatus ? selectedStatus.label : "+ Set status" }}
</Button>
</DrawerTrigger>
<DrawerContent>
<div class="mt-4 border-t">
<StatusList />
</div>
</DrawerContent>
</Drawer>
</div>
</template>

View File

@ -84,11 +84,11 @@ const columns = [
columnHelper.display({
id: 'select',
header: ({ table }) => h(Checkbox, {
'checked': table.getIsAllPageRowsSelected(),
'checked': table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && 'indeterminate'),
'onUpdate:checked': value => table.toggleAllPageRowsSelected(!!value),
'ariaLabel': 'Select all',
}),
cell: ({ row, column }) => {
cell: ({ row }) => {
return h(Checkbox, {
'checked': row.getIsSelected(),
'onUpdate:checked': value => row.toggleSelected(!!value),
@ -165,8 +165,6 @@ const table = useVueTable({
},
},
})
const getState = table.getState()
</script>
<template>

View File

@ -80,7 +80,7 @@ const columns: ColumnDef<Payment>[] = [
{
id: 'select',
header: ({ table }) => h(Checkbox, {
'checked': table.getIsAllPageRowsSelected(),
'checked': table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && 'indeterminate'),
'onUpdate:checked': value => table.toggleAllPageRowsSelected(!!value),
'ariaLabel': 'Select all',
}),

Some files were not shown because too many files have changed in this diff Show More