Merge branch 'dev' into feat/splitter

This commit is contained in:
Sadegh Barati 2024-03-06 18:17:52 +03:30
commit 707da510b3
97 changed files with 4355 additions and 2479 deletions

View File

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

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

@ -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

@ -214,6 +214,12 @@ export const docsConfig: DocsConfig = {
href: '/docs/components/dialog',
items: [],
},
{
title: 'Drawer',
href: '/docs/components/drawer',
items: [],
label: 'New',
},
{
title: 'Dropdown Menu',
href: '/docs/components/dropdown-menu',

View File

@ -84,7 +84,7 @@ 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"
@ -290,9 +290,7 @@ watch(() => $route.path, (n) => {
</DialogContent>
</Dialog>
<DefaultToaster />
<ClientOnly>
<NewYorkSonner :theme="isDark ? 'dark' : 'light'" />
</ClientOnly>
<NewYorkSonner :theme="'system'" />
<NewYorkToaster />
</div>
</template>

View File

@ -1,35 +1,19 @@
<script setup lang="ts">
import { onMounted, watch } from 'vue'
import { Paintbrush } from 'lucide-vue-next'
import { useData } from 'vitepress'
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 { RADII, useConfigStore } from '@/stores/config'
import { colors } from '@/lib/registry'
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 { Button } from '@/lib/registry/new-york/ui/button'
import { Label } from '@/lib/registry/new-york/ui/label'
import { Popover, PopoverContent, PopoverTrigger } from '@/lib/registry/new-york/ui/popover'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/lib/registry/new-york/ui/tooltip'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/lib/registry/new-york/ui/dialog'
import RadixIconsCheck from '~icons/radix-icons/check'
import RadixIconsSun from '~icons/radix-icons/sun'
import RadixIconsMoon from '~icons/radix-icons/moon'
type Color =
| 'zinc'
| 'slate'
| 'stone'
| 'gray'
| 'neutral'
| 'red'
| 'rose'
| 'orange'
| 'green'
| 'blue'
| 'yellow'
| 'violet'
import { Drawer, DrawerContent, DrawerTrigger } from '@/lib/registry/new-york/ui/drawer'
// Create an array of color values
const allColors: Color[] = [
@ -47,8 +31,7 @@ const allColors: Color[] = [
'violet',
]
const { theme, radius, setRadius, setTheme } = useConfigStore()
const { isDark } = useData()
const { theme, radius } = useConfigStore()
// Whenever the component is mounted, update the document class list
onMounted(() => {
@ -72,173 +55,63 @@ watch(radius, (radius) => {
<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>
<Popover>
<PopoverTrigger as-child>
<Button variant="outline" class="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">
<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>
<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>
<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>
</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>
</div>
</div>
</div>
</div>
<section>
<slot />
</section>

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

@ -297,6 +297,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",
@ -402,6 +409,27 @@ export const Index = {
component: () => import("../src/lib/registry/default/example/DialogScrollOverlayDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/DialogScrollOverlayDemo.vue"],
},
"DrawerDemo": {
name: "DrawerDemo",
type: "components:example",
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",
type: "components:example",
@ -409,6 +437,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",
@ -605,6 +640,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",
@ -626,6 +668,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",
@ -640,6 +689,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",
@ -1271,6 +1327,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",
@ -1376,6 +1439,27 @@ 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"],
},
"DrawerDemo": {
name: "DrawerDemo",
type: "components:example",
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",
type: "components:example",
@ -1383,6 +1467,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",
@ -1579,6 +1670,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",
@ -1600,6 +1698,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",
@ -1614,6 +1719,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

@ -16,28 +16,28 @@
},
"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",
"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.276.0",
"radix-vue": "^1.4.8",
"radix-vue": "^1.4.9",
"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.1",
"vue-wrap-balancer": "^1.1.3",
"zod": "^3.22.4"
},
@ -46,27 +46,27 @@
"@iconify-json/tabler": "^1.1.89",
"@iconify/json": "^2.2.108",
"@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.24",
"@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.7.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",
"tsx": "^4.7.1",
"typescript": "^5.3.3",
"unplugin-icons": "^0.18.3",
"vite": "^5.0.12",
"vitepress": "^1.0.0-rc.41",
"vue-tsc": "^1.8.27"
"unplugin-icons": "^0.18.5",
"vite": "^5.1.4",
"vitepress": "^1.0.0-rc.44",
"vue-tsc": "^2.0.3"
}
}

View File

@ -12,7 +12,6 @@ description: Autocomplete input and command palette with a list of suggestions.
</Callout>
## Installation
The Combobox is built using a composition of the `<Popover />` and the `<Command />` components.
@ -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

@ -5,7 +5,6 @@ source: apps/www/src/lib/registry/default/ui/dropdown-menu
primitive: https://www.radix-vue.com/components/dropdown-menu.html
---
<ComponentPreview name="DropdownMenuDemo" />
## Installation
@ -41,3 +40,13 @@ import {
</DropdownMenu>
</template>
```
## Examples
### Checkboxes
<ComponentPreview name="DropdownMenuCheckboxes" />
### Radio Group
<ComponentPreview name="DropdownMenuRadioGroup" />

View File

@ -327,6 +327,7 @@ 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)

View File

@ -3,7 +3,6 @@ title: Input
description: Displays a form input field or a component that looks like an input field.
---
<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,12 +5,10 @@ source: apps/www/src/lib/registry/default/ui/select
primitive: https://www.radix-vue.com/components/select.html
---
<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

@ -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

@ -26,3 +26,9 @@ import { Slider } from '@/components/ui/slider'
/>
</template>
```
## Examples
### Form
<ComponentPreview name="SliderForm" />

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

@ -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,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,63 @@
<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,
})
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 }" name="duration">
<FormItem>
<FormLabel>Duration</FormLabel>
<FormControl>
<Slider
v-bind="componentField"
:default-value="[30]"
:max="60"
:min="5"
:step="5"
/>
<FormDescription class="flex justify-between">
<span>How many minutes are you available?</span>
<span>{{ componentField.modelValue?.[0] ?? "30" }} min</span>
</FormDescription>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<Button type="submit">
Submit
</Button>
</form>
</template>

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

@ -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

@ -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

@ -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/new-york/ui/button'
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/lib/registry/new-york/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/new-york/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/lib/registry/new-york/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,117 @@
<script lang="ts" setup>
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from '@/lib/registry/new-york/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/new-york/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,63 @@
<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/new-york/ui/button'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/lib/registry/new-york/ui/form'
import { Slider } from '@/lib/registry/new-york/ui/slider'
import { toast } from '@/lib/registry/new-york/ui/toast'
const formSchema = toTypedSchema(z.object({
duration: z.array(
z.number().min(0).max(60),
),
}))
const { handleSubmit } = useForm({
validationSchema: formSchema,
})
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 }" name="duration">
<FormItem>
<FormLabel>Duration</FormLabel>
<FormControl>
<Slider
v-bind="componentField"
:default-value="[30]"
:max="60"
:min="5"
:step="5"
/>
<FormDescription class="flex justify-between">
<span>How many minutes are you available?</span>
<span>{{ componentField.modelValue?.[0] ?? "30" }} min</span>
</FormDescription>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<Button type="submit">
Submit
</Button>
</form>
</template>

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

@ -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

@ -244,6 +244,24 @@
],
"type": "components:ui"
},
{
"name": "drawer",
"dependencies": [],
"registryDependencies": [
"utils"
],
"files": [
"ui/drawer/Drawer.vue",
"ui/drawer/DrawerContent.vue",
"ui/drawer/DrawerDescription.vue",
"ui/drawer/DrawerFooter.vue",
"ui/drawer/DrawerHeader.vue",
"ui/drawer/DrawerOverlay.vue",
"ui/drawer/DrawerTitle.vue",
"ui/drawer/index.ts"
],
"type": "components:ui"
},
{
"name": "dropdown-menu",
"dependencies": [],

View File

@ -0,0 +1,42 @@
{
"name": "drawer",
"dependencies": [],
"registryDependencies": [
"utils"
],
"files": [
{
"name": "Drawer.vue",
"content": "<script lang=\"ts\" setup>\nimport type { DrawerRootEmits, DrawerRootProps } from 'vaul-vue'\nimport { DrawerRoot } from 'vaul-vue'\nimport { useForwardPropsEmits } from 'radix-vue'\n\nconst props = withDefaults(defineProps<DrawerRootProps>(), {\n shouldScaleBackground: true,\n})\n\nconst emits = defineEmits<DrawerRootEmits>()\n\nconst forwarded = useForwardPropsEmits(props, emits)\n</script>\n\n<template>\n <DrawerRoot v-bind=\"forwarded\">\n <slot />\n </DrawerRoot>\n</template>\n"
},
{
"name": "DrawerContent.vue",
"content": "<script lang=\"ts\" setup>\nimport { DrawerContent, DrawerPortal } from 'vaul-vue'\nimport type { DialogContentEmits, DialogContentProps } from 'radix-vue'\nimport { useForwardPropsEmits } from 'radix-vue'\nimport type { HtmlHTMLAttributes } from 'vue'\nimport DrawerOverlay from './DrawerOverlay.vue'\nimport { cn } from '@/lib/utils'\n\nconst props = defineProps<DialogContentProps & { class?: HtmlHTMLAttributes['class'] }>()\nconst emits = defineEmits<DialogContentEmits>()\n\nconst forwarded = useForwardPropsEmits(props, emits)\n</script>\n\n<template>\n <DrawerPortal>\n <DrawerOverlay />\n <DrawerContent\n v-bind=\"forwarded\" :class=\"cn(\n 'fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background',\n props.class,\n )\"\n >\n <div class=\"mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted\" />\n <slot />\n </DrawerContent>\n </DrawerPortal>\n</template>\n"
},
{
"name": "DrawerDescription.vue",
"content": "<script lang=\"ts\" setup>\nimport type { DrawerDescriptionProps } from 'vaul-vue'\nimport { DrawerDescription } from 'vaul-vue'\nimport { type HtmlHTMLAttributes, computed } from 'vue'\nimport { cn } from '@/lib/utils'\n\nconst props = defineProps<DrawerDescriptionProps & { class?: HtmlHTMLAttributes['class'] }>()\n\nconst delegatedProps = computed(() => {\n const { class: _, ...delegated } = props\n\n return delegated\n})\n</script>\n\n<template>\n <DrawerDescription v-bind=\"delegatedProps\" :class=\"cn('text-sm text-muted-foreground', props.class)\">\n <slot />\n </DrawerDescription>\n</template>\n"
},
{
"name": "DrawerFooter.vue",
"content": "<script lang=\"ts\" setup>\nimport type { HtmlHTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\n\nconst props = defineProps<{\n class?: HtmlHTMLAttributes['class']\n}>()\n</script>\n\n<template>\n <div :class=\"cn('mt-auto flex flex-col gap-2 p-4', props.class)\">\n <slot />\n </div>\n</template>\n"
},
{
"name": "DrawerHeader.vue",
"content": "<script lang=\"ts\" setup>\nimport type { HtmlHTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\n\nconst props = defineProps<{\n class?: HtmlHTMLAttributes['class']\n}>()\n</script>\n\n<template>\n <div :class=\"cn('grid gap-1.5 p-4 text-center sm:text-left', props.class)\">\n <slot />\n </div>\n</template>\n"
},
{
"name": "DrawerOverlay.vue",
"content": "<script lang=\"ts\" setup>\nimport { DrawerOverlay } from 'vaul-vue'\nimport type { DialogOverlayProps } from 'radix-vue'\nimport { type HtmlHTMLAttributes, computed } from 'vue'\nimport { cn } from '@/lib/utils'\n\nconst props = defineProps<DialogOverlayProps & { class?: HtmlHTMLAttributes['class'] }>()\n\nconst delegatedProps = computed(() => {\n const { class: _, ...delegated } = props\n\n return delegated\n})\n</script>\n\n<template>\n <DrawerOverlay v-bind=\"delegatedProps\" :class=\"cn('fixed inset-0 z-50 bg-black/80', props.class)\" />\n</template>\n"
},
{
"name": "DrawerTitle.vue",
"content": "<script lang=\"ts\" setup>\nimport type { DrawerTitleProps } from 'vaul-vue'\nimport { DrawerTitle } from 'vaul-vue'\nimport { type HtmlHTMLAttributes, computed } from 'vue'\nimport { cn } from '@/lib/utils'\n\nconst props = defineProps<DrawerTitleProps & { class?: HtmlHTMLAttributes['class'] }>()\n\nconst delegatedProps = computed(() => {\n const { class: _, ...delegated } = props\n\n return delegated\n})\n</script>\n\n<template>\n <DrawerTitle v-bind=\"delegatedProps\" :class=\"cn('text-lg font-semibold leading-none tracking-tight', props.class)\">\n <slot />\n </DrawerTitle>\n</template>\n"
},
{
"name": "index.ts",
"content": "export { DrawerPortal, DrawerTrigger, DrawerClose } from 'vaul-vue'\nexport { default as Drawer } from './Drawer.vue'\nexport { default as DrawerOverlay } from './DrawerOverlay.vue'\nexport { default as DrawerContent } from './DrawerContent.vue'\nexport { default as DrawerHeader } from './DrawerHeader.vue'\nexport { default as DrawerFooter } from './DrawerFooter.vue'\nexport { default as DrawerTitle } from './DrawerTitle.vue'\nexport { default as DrawerDescription } from './DrawerDescription.vue'\n"
}
],
"type": "components:ui"
}

View File

@ -7,7 +7,7 @@
"files": [
{
"name": "Sonner.vue",
"content": "<script lang=\"ts\" setup>\nimport { Toaster as Sonner, type ToasterProps } from 'vue-sonner'\n\nconst props = defineProps<ToasterProps>()\n</script>\n\n<template>\n <Sonner\n class=\"toaster group\"\n :class-names=\"{\n toast:\n 'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',\n description: 'group-[.toast]:text-muted-foreground',\n actionButton:\n 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',\n cancelButton:\n 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',\n }\"\n v-bind=\"props\"\n />\n</template>\n"
"content": "<script lang=\"ts\" setup>\nimport { Toaster as Sonner, type ToasterProps } from 'vue-sonner'\n\nconst props = defineProps<ToasterProps>()\n</script>\n\n<template>\n <Sonner\n class=\"toaster group\"\n v-bind=\"props\"\n :toast-options=\"{\n classes: {\n toast: 'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',\n description: 'group-[.toast]:text-muted-foreground',\n actionButton:\n 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',\n cancelButton:\n 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',\n },\n }\"\n />\n</template>\n"
},
{
"name": "index.ts",

View File

@ -0,0 +1,42 @@
{
"name": "drawer",
"dependencies": [],
"registryDependencies": [
"utils"
],
"files": [
{
"name": "Drawer.vue",
"content": "<script lang=\"ts\" setup>\nimport type { DrawerRootEmits, DrawerRootProps } from 'vaul-vue'\nimport { DrawerRoot } from 'vaul-vue'\nimport { useForwardPropsEmits } from 'radix-vue'\n\nconst props = withDefaults(defineProps<DrawerRootProps>(), {\n shouldScaleBackground: true,\n})\n\nconst emits = defineEmits<DrawerRootEmits>()\n\nconst forwarded = useForwardPropsEmits(props, emits)\n</script>\n\n<template>\n <DrawerRoot v-bind=\"forwarded\">\n <slot />\n </DrawerRoot>\n</template>\n"
},
{
"name": "DrawerContent.vue",
"content": "<script lang=\"ts\" setup>\nimport { DrawerContent, DrawerPortal } from 'vaul-vue'\nimport type { DialogContentEmits, DialogContentProps } from 'radix-vue'\nimport { useForwardPropsEmits } from 'radix-vue'\nimport type { HtmlHTMLAttributes } from 'vue'\nimport DrawerOverlay from './DrawerOverlay.vue'\nimport { cn } from '@/lib/utils'\n\nconst props = defineProps<DialogContentProps & { class?: HtmlHTMLAttributes['class'] }>()\nconst emits = defineEmits<DialogContentEmits>()\n\nconst forwarded = useForwardPropsEmits(props, emits)\n</script>\n\n<template>\n <DrawerPortal>\n <DrawerOverlay />\n <DrawerContent\n v-bind=\"forwarded\" :class=\"cn(\n 'fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background',\n props.class,\n )\"\n >\n <div class=\"mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted\" />\n <slot />\n </DrawerContent>\n </DrawerPortal>\n</template>\n"
},
{
"name": "DrawerDescription.vue",
"content": "<script lang=\"ts\" setup>\nimport type { DrawerDescriptionProps } from 'vaul-vue'\nimport { DrawerDescription } from 'vaul-vue'\nimport { type HtmlHTMLAttributes, computed } from 'vue'\nimport { cn } from '@/lib/utils'\n\nconst props = defineProps<DrawerDescriptionProps & { class?: HtmlHTMLAttributes['class'] }>()\n\nconst delegatedProps = computed(() => {\n const { class: _, ...delegated } = props\n\n return delegated\n})\n</script>\n\n<template>\n <DrawerDescription v-bind=\"delegatedProps\" :class=\"cn('text-sm text-muted-foreground', props.class)\">\n <slot />\n </DrawerDescription>\n</template>\n"
},
{
"name": "DrawerFooter.vue",
"content": "<script lang=\"ts\" setup>\nimport type { HtmlHTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\n\nconst props = defineProps<{\n class?: HtmlHTMLAttributes['class']\n}>()\n</script>\n\n<template>\n <div :class=\"cn('mt-auto flex flex-col gap-2 p-4', props.class)\">\n <slot />\n </div>\n</template>\n"
},
{
"name": "DrawerHeader.vue",
"content": "<script lang=\"ts\" setup>\nimport type { HtmlHTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\n\nconst props = defineProps<{\n class?: HtmlHTMLAttributes['class']\n}>()\n</script>\n\n<template>\n <div :class=\"cn('grid gap-1.5 p-4 text-center sm:text-left', props.class)\">\n <slot />\n </div>\n</template>\n"
},
{
"name": "DrawerOverlay.vue",
"content": "<script lang=\"ts\" setup>\nimport { DrawerOverlay } from 'vaul-vue'\nimport type { DialogOverlayProps } from 'radix-vue'\nimport { type HtmlHTMLAttributes, computed } from 'vue'\nimport { cn } from '@/lib/utils'\n\nconst props = defineProps<DialogOverlayProps & { class?: HtmlHTMLAttributes['class'] }>()\n\nconst delegatedProps = computed(() => {\n const { class: _, ...delegated } = props\n\n return delegated\n})\n</script>\n\n<template>\n <DrawerOverlay v-bind=\"delegatedProps\" :class=\"cn('fixed inset-0 z-50 bg-black/80', props.class)\" />\n</template>\n"
},
{
"name": "DrawerTitle.vue",
"content": "<script lang=\"ts\" setup>\nimport type { DrawerTitleProps } from 'vaul-vue'\nimport { DrawerTitle } from 'vaul-vue'\nimport { type HtmlHTMLAttributes, computed } from 'vue'\nimport { cn } from '@/lib/utils'\n\nconst props = defineProps<DrawerTitleProps & { class?: HtmlHTMLAttributes['class'] }>()\n\nconst delegatedProps = computed(() => {\n const { class: _, ...delegated } = props\n\n return delegated\n})\n</script>\n\n<template>\n <DrawerTitle v-bind=\"delegatedProps\" :class=\"cn('text-lg font-semibold leading-none tracking-tight', props.class)\">\n <slot />\n </DrawerTitle>\n</template>\n"
},
{
"name": "index.ts",
"content": "export { DrawerPortal, DrawerTrigger, DrawerClose } from 'vaul-vue'\nexport { default as Drawer } from './Drawer.vue'\nexport { default as DrawerOverlay } from './DrawerOverlay.vue'\nexport { default as DrawerContent } from './DrawerContent.vue'\nexport { default as DrawerHeader } from './DrawerHeader.vue'\nexport { default as DrawerFooter } from './DrawerFooter.vue'\nexport { default as DrawerTitle } from './DrawerTitle.vue'\nexport { default as DrawerDescription } from './DrawerDescription.vue'\n"
}
],
"type": "components:ui"
}

View File

@ -7,7 +7,7 @@
"files": [
{
"name": "Sonner.vue",
"content": "<script lang=\"ts\" setup>\nimport { Toaster as Sonner, type ToasterProps } from 'vue-sonner'\n\nconst props = defineProps<ToasterProps>()\n</script>\n\n<template>\n <Sonner\n class=\"toaster group\"\n :class-names=\"{\n toast:\n 'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',\n description: 'group-[.toast]:text-muted-foreground',\n actionButton:\n 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',\n cancelButton:\n 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',\n }\"\n v-bind=\"props\"\n />\n</template>\n"
"content": "<script lang=\"ts\" setup>\nimport { Toaster as Sonner, type ToasterProps } from 'vue-sonner'\n\nconst props = defineProps<ToasterProps>()\n</script>\n\n<template>\n <Sonner\n class=\"toaster group\"\n v-bind=\"props\"\n :toast-options=\"{\n classes: {\n toast: 'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',\n description: 'group-[.toast]:text-muted-foreground',\n actionButton:\n 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',\n cancelButton:\n 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',\n },\n }\"\n />\n</template>\n"
},
{
"name": "index.ts",

View File

@ -42,6 +42,9 @@
},
"components": {
"type": "string"
},
"ui": {
"type": "string"
}
},
"required": ["utils", "components"]

View File

@ -24,6 +24,7 @@ export default antfu(
'no-tabs': 0,
'import/first': 0,
'node/prefer-global/process': 0,
'style/no-tabs': 0,
},
},
)

View File

@ -3,7 +3,7 @@
"type": "module",
"version": "0.9.0",
"private": true,
"packageManager": "pnpm@8.15.3",
"packageManager": "pnpm@8.15.4",
"license": "MIT",
"repository": "radix-vue/shadcn-vue",
"workspaces": [
@ -27,11 +27,11 @@
"taze:minor": "taze minor -fwri --ignore-paths ./packages/cli/test/** --exclude /@iconify/"
},
"devDependencies": {
"@antfu/eslint-config": "^2.6.4",
"@commitlint/cli": "^18.6.1",
"@commitlint/config-conventional": "^18.6.2",
"bumpp": "^9.3.0",
"eslint": "^8.56.0",
"@antfu/eslint-config": "^2.7.0",
"@commitlint/cli": "^19.0.3",
"@commitlint/config-conventional": "^19.0.3",
"bumpp": "^9.3.1",
"eslint": "^8.57.0",
"lint-staged": "^15.2.2",
"simple-git-hooks": "^2.9.0",
"taze": "^0.13.3",

View File

@ -32,7 +32,7 @@
"dev": "tsup --watch",
"build": "tsup",
"typecheck": "tsc --noEmit",
"clean": "rimraf dist && rimraf components",
"clean": "node ./scripts/rimraf.js",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"start:dev": "COMPONENTS_REGISTRY_URL=http://localhost:3001 node dist/index.js",
@ -45,41 +45,39 @@
"test:ui": "vitest --ui"
},
"dependencies": {
"@antfu/ni": "^0.21.8",
"@babel/core": "^7.22.17",
"@babel/parser": "^7.22.16",
"@babel/plugin-transform-typescript": "^7.22.15",
"@babel/core": "^7.24.0",
"@babel/parser": "^7.24.0",
"@vue/compiler-sfc": "^3.4",
"chalk": "5.3.0",
"commander": "^11.0.0",
"cosmiconfig": "^8.3.6",
"c12": "^1.9.0",
"commander": "^12.0.0",
"consola": "^3.2.3",
"detype": "npm:detypes@^0.7.9",
"diff": "^5.1.0",
"execa": "^8.0.1",
"fs-extra": "^11.1.1",
"https-proxy-agent": "^7.0.2",
"diff": "^5.2.0",
"fs-extra": "^11.2.0",
"https-proxy-agent": "^7.0.4",
"lodash.template": "^4.5.0",
"magic-string": "^0.30.3",
"node-fetch": "^3.3.2",
"ora": "^7.0.1",
"magic-string": "^0.30.8",
"nypm": "^0.3.8",
"ofetch": "^1.3.3",
"ora": "^8.0.1",
"pathe": "^1.1.2",
"prompts": "^2.4.2",
"radix-vue": "^1.4.8",
"recast": "^0.23.4",
"rimraf": "^5.0.1",
"ts-morph": "^19.0.0",
"radix-vue": "^1.4.9",
"ts-morph": "^21.0.1",
"tsconfig-paths": "^4.2.0",
"vite-tsconfig-paths": "^4.2.1",
"zod": "^3.22.2"
"zod": "^3.22.4"
},
"devDependencies": {
"@types/babel__core": "^7.20.1",
"@types/diff": "^5.0.3",
"@types/fs-extra": "^11.0.1",
"@types/lodash.template": "^4.5.1",
"@types/prompts": "^2.4.4",
"@types/babel__core": "^7.20.5",
"@types/diff": "^5.0.9",
"@types/fs-extra": "^11.0.4",
"@types/lodash.template": "^4.5.3",
"@types/node": "^20.11.24",
"@types/prompts": "^2.4.9",
"@vitest/ui": "^0.34.4",
"tsup": "^7.2.0",
"type-fest": "^4.3.1",
"typescript": "^5.2.2"
"tsup": "^8.0.2",
"type-fest": "^4.10.3",
"typescript": "^5.3.3",
"vite-tsconfig-paths": "^4.3.1"
}
}

View File

@ -0,0 +1,10 @@
import fsp from 'node:fs/promises'
function rmdir(dirs) {
dirs.forEach(async (dir) => {
await fsp.unlink(dir).catch(() => {})
await fsp.rm(dir, { recursive: true, force: true }).catch(() => {})
})
}
rmdir(['dist', 'components'])

View File

@ -1,17 +1,16 @@
import { existsSync, promises as fs, rmSync } from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import chalk from 'chalk'
import path from 'pathe'
import { consola } from 'consola'
import { colors } from 'consola/utils'
import { Command } from 'commander'
import { execa } from 'execa'
import ora from 'ora'
import prompts from 'prompts'
import * as z from 'zod'
import { z } from 'zod'
import { addDependency, addDevDependency } from 'nypm'
import { transform } from '@/src/utils/transformers'
import { getConfig } from '@/src/utils/get-config'
import { getPackageManager } from '@/src/utils/get-package-manager'
import { handleError } from '@/src/utils/handle-error'
import { logger } from '@/src/utils/logger'
import {
fetchTree,
getItemTargetPath,
@ -52,15 +51,15 @@ export const add = new Command()
const cwd = path.resolve(options.cwd)
if (!existsSync(cwd)) {
logger.error(`The path ${cwd} does not exist. Please try again.`)
consola.error(`The path ${cwd} does not exist. Please try again.`)
process.exit(1)
}
const config = await getConfig(cwd)
if (!config) {
logger.warn(
`Configuration is missing. Please run ${chalk.green('init')} to create a components.json file.`,
)
consola.warn(`Configuration is missing. Please run ${colors.green('init')} to create a components.json file.`)
process.exit(1)
}
@ -88,7 +87,7 @@ export const add = new Command()
}
if (!selectedComponents?.length) {
logger.warn('No components selected. Exiting.')
consola.warn('No components selected. Exiting.')
process.exit(0)
}
@ -97,7 +96,7 @@ export const add = new Command()
const baseColor = await getRegistryBaseColor(config.tailwind.baseColor)
if (!payload.length) {
logger.warn('Selected components not found. Exiting.')
consola.warn('Selected components not found. Exiting.')
process.exit(0)
}
@ -114,7 +113,6 @@ export const add = new Command()
}
const spinner = ora('Installing components...').start()
const skippedDeps = new Set<string>()
for (const item of payload) {
spinner.text = `Installing ${item.name}...`
const targetDir = getItemTargetPath(
@ -144,8 +142,8 @@ export const add = new Command()
})
if (!overwrite) {
logger.info(
`Skipped ${item.name}. To overwrite, run with the ${chalk.green(
consola.info(
`Skipped ${item.name}. To overwrite, run with the ${colors.green(
'--overwrite',
)} flag.`,
)
@ -159,6 +157,20 @@ export const add = new Command()
}
}
// Install dependencies.
await Promise.allSettled(
[
item.dependencies?.length && await addDependency(item.dependencies, {
cwd,
silent: true,
}),
item.devDependencies?.length && await addDevDependency(item.devDependencies, {
cwd,
silent: true,
}),
],
)
const componentDir = path.resolve(targetDir, item.name)
if (!existsSync(componentDir))
await fs.mkdir(componentDir, { recursive: true })
@ -201,25 +213,6 @@ export const add = new Command()
await fs.writeFile(filePath, content)
}
// Install dependencies.
if (item.dependencies?.length) {
item.dependencies.forEach(dep =>
skippedDeps.add(dep),
)
const packageManager = await getPackageManager(cwd)
await execa(
packageManager,
[
packageManager === 'npm' ? 'install' : 'add',
...item.dependencies,
],
{
cwd,
},
)
}
}
spinner.succeed('Done.')
}

View File

@ -1,14 +1,14 @@
import { existsSync, promises as fs } from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import chalk from 'chalk'
import path from 'pathe'
import { consola } from 'consola'
import { colors } from 'consola/utils'
import { Command } from 'commander'
import { type Change, diffLines } from 'diff'
import * as z from 'zod'
import { z } from 'zod'
import type { Config } from '@/src/utils/get-config'
import { getConfig } from '@/src/utils/get-config'
import { handleError } from '@/src/utils/handle-error'
import { logger } from '@/src/utils/logger'
import {
fetchTree,
getItemTargetPath,
@ -45,14 +45,14 @@ export const diff = new Command()
const cwd = path.resolve(options.cwd)
if (!existsSync(cwd)) {
logger.error(`The path ${cwd} does not exist. Please try again.`)
consola.error(`The path ${cwd} does not exist. Please try again.`)
process.exit(1)
}
const config = await getConfig(cwd)
if (!config) {
logger.warn(
`Configuration is missing. Please run ${chalk.green(
consola.warn(
`Configuration is missing. Please run ${colors.green(
'init',
)} to create a components.json file.`,
)
@ -88,19 +88,20 @@ export const diff = new Command()
}
if (!componentsWithUpdates.length) {
logger.info('No updates found.')
consola.info('No updates found.')
process.exit(0)
}
logger.info('The following components have updates available:')
consola.info('The following components have updates available:')
for (const component of componentsWithUpdates) {
logger.info(`- ${component.name}`)
consola.info(`- ${component.name}`)
for (const change of component.changes)
logger.info(` - ${change.filePath}`)
consola.info(` - ${change.filePath}`)
}
logger.break()
logger.info(
`Run ${chalk.green('diff <component>')} to see the changes.`,
consola.log('')
consola.info(
`Run ${colors.green('diff <component>')} to see the changes.`,
)
process.exit(0)
}
@ -111,8 +112,8 @@ export const diff = new Command()
)
if (!component) {
logger.error(
`The component ${chalk.green(options.component)} does not exist.`,
consola.error(
`The component ${colors.green(options.component)} does not exist.`,
)
process.exit(1)
}
@ -120,14 +121,14 @@ export const diff = new Command()
const changes = await diffComponent(component, config)
if (!changes.length) {
logger.info(`No updates found for ${options.component}.`)
consola.info(`No updates found for ${options.component}.`)
process.exit(0)
}
for (const change of changes) {
logger.info(`- ${change.filePath}`)
consola.info(`- ${change.filePath}`)
printDiff(change.patch)
logger.info('')
consola.log('')
}
}
catch (error) {
@ -184,10 +185,10 @@ function printDiff(diff: Change[]) {
diff.forEach((part) => {
if (part) {
if (part.added)
return process.stdout.write(chalk.green(part.value))
return process.stdout.write(colors.green(part.value))
if (part.removed)
return process.stdout.write(chalk.red(part.value))
return process.stdout.write(colors.red(part.value))
return process.stdout.write(part.value)
}

View File

@ -1,22 +1,21 @@
import { existsSync, promises as fs } from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import chalk from 'chalk'
import path from 'pathe'
import { Command } from 'commander'
import { execa } from 'execa'
import template from 'lodash.template'
import ora from 'ora'
import prompts from 'prompts'
import * as z from 'zod'
import { z } from 'zod'
import { addDependency, addDevDependency } from 'nypm'
import { consola } from 'consola'
import { colors } from 'consola/utils'
import * as templates from '../utils/templates'
import {
getRegistryBaseColor,
getRegistryBaseColors,
getRegistryStyles,
} from '../utils/registry'
import { logger } from '../utils/logger'
import { handleError } from '../utils/handle-error'
import { getPackageManager } from '../utils/get-package-manager'
import { transformByDetype } from '../utils/transformers/transform-sfc'
import {
type Config,
@ -29,6 +28,7 @@ import {
resolveConfigPaths,
} from '../utils/get-config'
import { transformCJSToESM } from '../utils/transformers/transform-cjs-to-esm'
import { applyPrefixesCss } from '../utils/transformers/transform-tw-prefix'
const PROJECT_DEPENDENCIES = {
base: [
@ -64,7 +64,7 @@ export const init = new Command()
// Ensure target directory exists.
if (!existsSync(cwd)) {
logger.error(`The path ${cwd} does not exist. Please try again.`)
consola.error(`The path ${cwd} does not exist. Please try again.`)
process.exit(1)
}
@ -74,11 +74,11 @@ export const init = new Command()
await runInit(cwd, config)
logger.info('')
logger.info(
`${chalk.green('Success!')} Project initialization completed.`,
consola.log('')
consola.info(
`${colors.green('Success!')} Project initialization completed.`,
)
logger.info('')
consola.log('')
}
catch (error) {
handleError(error)
@ -90,7 +90,7 @@ export async function promptForConfig(
defaultConfig: Config | null = null,
skip = false,
) {
const highlight = (text: string) => chalk.cyan(text)
const highlight = (text: string) => colors.cyan(text)
const styles = await getRegistryStyles()
const baseColors = await getRegistryBaseColors()
@ -151,6 +151,14 @@ export async function promptForConfig(
active: 'yes',
inactive: 'no',
},
// {
// type: 'text',
// name: 'tailwindPrefix',
// message: `Are you using a custom ${highlight(
// 'tailwind prefix eg. tw-',
// )}? (Leave blank if not)`,
// initial: '',
// },
{
type: 'text',
name: 'tailwindConfig',
@ -187,6 +195,7 @@ export async function promptForConfig(
css: options.tailwindCss,
baseColor: options.tailwindBaseColor,
cssVariables: options.tailwindCssVariables,
// prefix: options.tailwindPrefix,
},
aliases: {
utils: options.utils,
@ -207,7 +216,7 @@ export async function promptForConfig(
}
// Write to file.
logger.info('')
consola.log('')
const spinner = ora('Writing components.json...').start()
const targetPath = path.resolve(cwd, 'components.json')
await fs.writeFile(targetPath, JSON.stringify(config, null, 2), 'utf8')
@ -247,8 +256,8 @@ export async function runInit(cwd: string, config: Config) {
transformCJSToESM(
config.resolvedPaths.tailwindConfig,
config.tailwind.cssVariables
? template(templates.TAILWIND_CONFIG_WITH_VARIABLES)({ extension, framework: config.framework })
: template(templates.TAILWIND_CONFIG)({ extension, framework: config.framework }),
? template(templates.TAILWIND_CONFIG_WITH_VARIABLES)({ extension, framework: config.framework, prefix: config.tailwind.prefix })
: template(templates.TAILWIND_CONFIG)({ extension, framework: config.framework, prefix: config.tailwind.prefix }),
),
'utf8',
)
@ -259,7 +268,9 @@ export async function runInit(cwd: string, config: Config) {
await fs.writeFile(
config.resolvedPaths.tailwindCss,
config.tailwind.cssVariables
? baseColor.cssVarsTemplate
? config.tailwind.prefix
? applyPrefixesCss(baseColor.cssVarsTemplate, config.tailwind.prefix)
: baseColor.cssVarsTemplate
: baseColor.inlineColorsTemplate,
'utf8',
)
@ -276,20 +287,29 @@ export async function runInit(cwd: string, config: Config) {
// Install dependencies.
const dependenciesSpinner = ora('Installing dependencies...')?.start()
const packageManager = await getPackageManager(cwd)
const deps = PROJECT_DEPENDENCIES.base.concat(
config.framework === 'nuxt' ? PROJECT_DEPENDENCIES.nuxt : [],
).concat(
config.style === 'new-york' ? ['@radix-icons/vue'] : ['lucide-vue-next'],
).filter(Boolean)
await execa(
packageManager,
[packageManager === 'npm' ? 'install' : 'add', ...deps],
{
cwd,
},
async function addNuxtDevDeps() {
if (config.framework === 'nuxt') {
await addDevDependency(PROJECT_DEPENDENCIES.nuxt, {
cwd,
silent: true,
})
}
}
await Promise.allSettled(
[
addNuxtDevDeps(),
addDependency(deps, {
cwd,
silent: true,
}),
],
)
dependenciesSpinner?.succeed()
}

View File

@ -1,9 +1,9 @@
import path from 'node:path'
import { existsSync } from 'node:fs'
import { cosmiconfig } from 'cosmiconfig'
import path from 'pathe'
import { loadConfig as c12LoadConfig } from 'c12'
import type { ConfigLoaderResult } from 'tsconfig-paths'
import { loadConfig } from 'tsconfig-paths'
import * as z from 'zod'
import { z } from 'zod'
import { resolveImport } from '@/src/utils/resolve-import'
export const DEFAULT_STYLE = 'default'
@ -19,12 +19,6 @@ export const TAILWIND_CSS_PATH = {
astro: 'src/styles/globals.css',
}
// TODO: Figure out if we want to support all cosmiconfig formats.
// A simple components.json file would be nice.
const explorer = cosmiconfig('components', {
searchPlaces: ['components.json'],
})
export const rawConfigSchema = z
.object({
$schema: z.string().optional(),
@ -35,11 +29,13 @@ export const rawConfigSchema = z
css: z.string(),
baseColor: z.string(),
cssVariables: z.boolean().default(true),
prefix: z.string().optional(),
}),
framework: z.string().default('Vite'),
aliases: z.object({
components: z.string(),
utils: z.string(),
ui: z.string().default('').optional(),
}),
})
.strict()
@ -53,6 +49,7 @@ export const configSchema = rawConfigSchema
tailwindCss: z.string(),
utils: z.string(),
components: z.string(),
ui: z.string(),
}),
})
@ -103,15 +100,22 @@ export async function resolveConfigPaths(cwd: string, config: RawConfig) {
tailwindCss: path.resolve(cwd, config.tailwind.css),
utils: resolveImport(config.aliases.utils, tsConfig),
components: resolveImport(config.aliases.components, tsConfig),
ui: config.aliases.ui
? resolveImport(config.aliases.ui, tsConfig)
: resolveImport(config.aliases.components, tsConfig),
},
})
}
export async function getRawConfig(cwd: string): Promise<RawConfig | null> {
try {
const configResult = await explorer.search(cwd)
const configResult = await c12LoadConfig({
name: 'components',
configFile: 'components',
cwd,
})
if (!configResult)
if (!configResult.config || Object.keys(configResult.config).length === 0)
return null
return rawConfigSchema.parse(configResult.config)

View File

@ -1,5 +1,5 @@
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import path from 'pathe'
import fs from 'fs-extra'
import { type PackageJson } from 'type-fest'

View File

@ -1,16 +0,0 @@
import { detect } from '@antfu/ni'
export async function getPackageManager(
targetDir: string,
): Promise<'yarn' | 'pnpm' | 'bun' | 'npm'> {
const packageManager = await detect({ programmatic: true, cwd: targetDir })
if (packageManager === 'yarn@berry')
return 'yarn'
if (packageManager === 'pnpm@6')
return 'pnpm'
if (packageManager === 'bun')
return 'bun'
return packageManager ?? 'npm'
}

View File

@ -1,5 +1,5 @@
import { existsSync } from 'node:fs'
import path from 'node:path'
import path from 'pathe'
import fs from 'fs-extra'
export async function getProjectInfo() {

View File

@ -1,16 +1,16 @@
import { logger } from './logger'
import { consola } from 'consola'
export function handleError(error: unknown) {
if (typeof error === 'string') {
logger.error(error)
consola.error(error)
process.exit(1)
}
if (error instanceof Error) {
logger.error(error.message)
consola.error(error.message)
process.exit(1)
}
logger.error('Something went wrong. Please try again.')
consola.error('Something went wrong. Please try again.')
process.exit(1)
}

View File

@ -1,19 +0,0 @@
import chalk from 'chalk'
export const logger = {
error(...args: unknown[]) {
console.log(chalk.red(...args))
},
warn(...args: unknown[]) {
console.log(chalk.yellow(...args))
},
info(...args: unknown[]) {
console.log(chalk.cyan(...args))
},
success(...args: unknown[]) {
console.log(chalk.green(...args))
},
break() {
console.log('')
},
}

View File

@ -1,8 +1,9 @@
import path from 'node:path'
import process from 'node:process'
import path from 'pathe'
import { HttpsProxyAgent } from 'https-proxy-agent'
import fetch from 'node-fetch'
import { ofetch } from 'ofetch'
import type * as z from 'zod'
import consola from 'consola'
import {
registryBaseColorSchema,
registryIndexSchema,
@ -122,9 +123,12 @@ export function getItemTargetPath(
override?: string,
) {
// Allow overrides for all items but ui.
if (override && item.type !== 'components:ui')
if (override)
return override
if (item.type === 'components:ui' && config.aliases.ui)
return config.resolvedPaths.ui
const [parent, type] = item.type.split(':')
if (!(parent in config.resolvedPaths))
return null
@ -139,17 +143,18 @@ async function fetchRegistry(paths: string[]) {
try {
const results = await Promise.all(
paths.map(async (path) => {
const response = await fetch(`${baseUrl}/registry/${path}`, {
const response = await ofetch(`${baseUrl}/registry/${path}`, {
// @ts-expect-error agent type
agent,
})
return await response.json()
return response
}),
)
return results
}
catch (error) {
// eslint-disable-next-line no-console
console.log(error)
consola.error(error)
throw new Error(`Failed to fetch registry from ${baseUrl}.`)
}
}

View File

@ -1,9 +1,10 @@
import * as z from 'zod'
import { z } from 'zod'
// TODO: Extract this to a shared package.
export const registryItemSchema = z.object({
name: z.string(),
dependencies: z.array(z.string()).optional(),
devDependencies: z.array(z.string()).optional(),
registryDependencies: z.array(z.string()).optional(),
files: z.array(z.string()),
type: z.enum(['components:ui', 'components:component', 'components:example']),

View File

@ -1,6 +1,5 @@
export const UTILS = `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))
@ -18,6 +17,7 @@ module.exports = {
'./app/**/*.{<%- extension %>,<%- extension %>x,vue}',
'./src/**/*.{<%- extension %>,<%- extension %>x,vue}',
],
prefix: "<%- prefix %>",
theme: {
container: {
center: true,
@ -52,6 +52,7 @@ export const TAILWIND_CONFIG_WITH_VARIABLES = `const animate = require("tailwind
module.exports = {
darkMode: ["class"],
safelist: ["dark"],
prefix: "<%- prefix %>",
<% if (framework === 'vite') { %>
content: [
'./pages/**/*.{<%- extension %>,<%- extension %>x,vue}',

View File

@ -1,12 +1,13 @@
import { promises as fs } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import path from 'pathe'
import { Project, ScriptKind, type SourceFile } from 'ts-morph'
import type * as z from 'zod'
import type { Config } from '@/src/utils/get-config'
import type { registryBaseColorSchema } from '@/src/utils/registry/schema'
import { transformCssVars } from '@/src/utils/transformers/transform-css-vars'
import { transformImport } from '@/src/utils/transformers/transform-import'
import { transformTwPrefixes } from '@/src/utils/transformers/transform-tw-prefix'
import { transformSFC } from '@/src/utils/transformers/transform-sfc'
export interface TransformOpts {
@ -25,6 +26,7 @@ export type Transformer<Output = SourceFile> = (
const transformers: Transformer[] = [
transformCssVars,
transformImport,
// transformTwPrefixes,
]
const project = new Project({

View File

@ -88,29 +88,28 @@ export function applyColorMapping(
if (input.includes(' border '))
input = input.replace(' border ', ' border border-border ')
// Build color mappings.
const classNames = input.split(' ')
const lightMode: string[] = []
const darkMode: string[] = []
const lightMode = new Set<string>()
const darkMode = new Set<string>()
for (const className of classNames) {
const [variant, value, modifier] = splitClassName(className)
const prefix = PREFIXES.find(prefix => value?.startsWith(prefix))
if (!prefix) {
if (!lightMode.includes(className))
lightMode.push(className)
if (!lightMode.has(className))
lightMode.add(className)
continue
}
const needle = value?.replace(prefix, '')
if (needle && needle in mapping.light) {
lightMode.push(
lightMode.add(
[variant, `${prefix}${mapping.light[needle]}`]
.filter(Boolean)
.join(':') + (modifier ? `/${modifier}` : ''),
)
darkMode.push(
darkMode.add(
['dark', variant, `${prefix}${mapping.dark[needle]}`]
.filter(Boolean)
.join(':') + (modifier ? `/${modifier}` : ''),
@ -118,9 +117,9 @@ export function applyColorMapping(
continue
}
if (!lightMode.includes(className))
lightMode.push(className)
if (!lightMode.has(className))
lightMode.add(className)
}
const combined = `${lightMode.join(' ').replace(/\'/g, '')} ${darkMode.join(' ').trim()}`.trim()
return `${combined}`
return [...Array.from(lightMode), ...Array.from(darkMode)].join(' ').trim()
}

View File

@ -8,12 +8,19 @@ export const transformImport: Transformer = async ({ sourceFile, config }) => {
// Replace @/lib/registry/[style] with the components alias.
if (moduleSpecifier.startsWith('@/lib/registry/')) {
importDeclaration.setModuleSpecifier(
moduleSpecifier.replace(
/^@\/lib\/registry\/[^/]+/,
config.aliases.components,
),
)
if (config.aliases.ui) {
importDeclaration.setModuleSpecifier(
moduleSpecifier.replace(/^@\/lib\/registry\/[^/]+\/ui/, config.aliases.ui),
)
}
else {
importDeclaration.setModuleSpecifier(
moduleSpecifier.replace(
/^@\/lib\/registry\/[^/]+/,
config.aliases.components,
),
)
}
}
// Replace `import { cn } from "@/lib/utils"`

View File

@ -0,0 +1,80 @@
import { SyntaxKind } from 'ts-morph'
import { MagicString, parse } from '@vue/compiler-sfc'
import type { SFCTemplateBlock } from '@vue/compiler-sfc'
import { splitClassName } from './transform-css-vars'
import type { Transformer } from '@/src/utils/transformers'
export const transformTwPrefixes: Transformer = async ({
sourceFile,
config,
}) => {
const isVueFile = sourceFile.getFilePath().endsWith('vue')
if (!config.tailwind?.prefix)
return sourceFile
let template: SFCTemplateBlock | null = null
if (isVueFile) {
const parsed = parse(sourceFile.getText())
template = parsed.descriptor.template
if (!template)
return sourceFile
}
sourceFile.getDescendantsOfKind(SyntaxKind.StringLiteral).forEach((node) => {
if (template && template.loc.start.offset >= node.getPos())
return sourceFile
const attrName = sourceFile.getDescendantAtPos(node.getPos() - 2)?.getText()
if (isVueFile && attrName !== 'class')
return sourceFile
const value = node.getText()
const hasClosingDoubleQuote = value.match(/"/g)?.length === 2
if (value.search('\'') === -1 && hasClosingDoubleQuote) {
const mapped = applyPrefix(value.replace(/"/g, ''), config.tailwind.prefix)
node.replaceWithText(`"${mapped}"`)
}
else {
const s = new MagicString(value)
s.replace(/'(.*?)'/g, (substring) => {
return `'${applyPrefix(substring.replace(/\'/g, ''), config.tailwind.prefix)}'`
})
node.replaceWithText(s.toString())
}
})
return sourceFile
}
export function applyPrefix(input: string, prefix: string = '') {
const classNames = input.split(' ')
const prefixed: string[] = []
for (const className of classNames) {
const [variant, value, modifier] = splitClassName(className)
if (variant) {
modifier
? prefixed.push(`${variant}:${prefix}${value}/${modifier}`)
: prefixed.push(`${variant}:${prefix}${value}`)
}
else {
modifier
? prefixed.push(`${prefix}${value}/${modifier}`)
: prefixed.push(`${prefix}${value}`)
}
}
return prefixed.join(' ')
}
export function applyPrefixesCss(css: string, prefix: string) {
const lines = css.split('\n')
for (const line of lines) {
if (line.includes('@apply')) {
const originalTWCls = line.replace('@apply', '').trim()
const prefixedTwCls = applyPrefix(originalTWCls, prefix)
css = css.replace(originalTWCls, prefixedTwCls)
}
}
return css
}

View File

@ -1,14 +1,13 @@
import fs from 'node:fs'
import path from 'node:path'
import { execa } from 'execa'
import path from 'pathe'
import { addDependency, addDevDependency } from 'nypm'
import { afterEach, expect, test, vi } from 'vitest'
import { runInit } from '../../src/commands/init'
import { getConfig } from '../../src/utils/get-config'
import * as getPackageManger from '../../src/utils/get-package-manager'
import * as registry from '../../src/utils/registry'
vi.mock('execa')
vi.mock('nypm')
vi.mock('fs/promises', () => ({
writeFile: vi.fn(),
mkdir: vi.fn(),
@ -16,7 +15,6 @@ vi.mock('fs/promises', () => ({
vi.mock('ora')
test('init config-full', async () => {
vi.spyOn(getPackageManger, 'getPackageManager').mockResolvedValue('pnpm')
vi.spyOn(registry, 'getRegistryBaseColor').mockResolvedValue({
inlineColors: {},
cssVars: {},
@ -67,10 +65,8 @@ test('init config-full', async () => {
expect.stringContaining("import { type ClassValue, clsx } from 'clsx'"),
'utf8',
)
expect(execa).toHaveBeenCalledWith(
'pnpm',
expect(addDependency).toHaveBeenCalledWith(
[
'add',
'tailwindcss-animate',
'class-variance-authority',
'clsx',
@ -80,6 +76,7 @@ test('init config-full', async () => {
],
{
cwd: targetDir,
silent: true,
},
)
@ -88,7 +85,6 @@ test('init config-full', async () => {
})
test('init config-partial', async () => {
vi.spyOn(getPackageManger, 'getPackageManager').mockResolvedValue('npm')
vi.spyOn(registry, 'getRegistryBaseColor').mockResolvedValue({
inlineColors: {},
cssVars: {},
@ -139,10 +135,8 @@ test('init config-partial', async () => {
expect.stringContaining("import { type ClassValue, clsx } from 'clsx'"),
'utf8',
)
expect(execa).toHaveBeenCalledWith(
'npm',
expect(addDependency).toHaveBeenCalledWith(
[
'install',
'tailwindcss-animate',
'class-variance-authority',
'clsx',
@ -152,6 +146,7 @@ test('init config-partial', async () => {
],
{
cwd: targetDir,
silent: true,
},
)

View File

@ -4,10 +4,12 @@
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "zinc",
"cssVariables": true
"cssVariables": true,
"prefix": "tw-"
},
"aliases": {
"utils": "~/lib/utils",
"components": "~/components"
"components": "~/components",
"ui": "~/ui"
}
}

View File

@ -12,6 +12,7 @@ export default {
'./app/**/*.{<%- extension %>,<%- extension %>x,vue}',
'./src/**/*.{<%- extension %>,<%- extension %>x,vue}',
],
prefix: \\"<%- prefix %>\\",
theme: {
container: {
center: true,
@ -48,6 +49,7 @@ exports[`handle tailwind config template correctly 2`] = `
export default {
darkMode: [\\"class\\"],
safelist: [\\"dark\\"],
prefix: \\"<%- prefix %>\\",
<% if (framework === 'vite') { %>
content: [
'./pages/**/*.{<%- extension %>,<%- extension %>x,vue}',

View File

@ -0,0 +1,127 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`transform tailwind prefix 1`] = `
"const testVariants = cva(
\\"tw-bg-background hover:tw-bg-muted tw-text-primary-foreground sm:focus:tw-text-accent-foreground\\",
{
variants: {
variant: {
default:
\\"tw-bg-primary tw-text-primary-foreground hover:tw-bg-primary/90\\",
},
size: {
default: \\"tw-h-10 tw-px-4 tw-py-2\\",
},
},
}
);
"
`;
exports[`transform tailwind prefix 2`] = `
"<template>
<div class=\\"tw-bg-background hover:tw-bg-muted tw-text-primary-foreground sm:focus:tw-text-accent-foreground\\">
foo
</div>
</template>
"
`;
exports[`transform tailwind prefix 3`] = `
"<template>
<div class=\\"tw-bg-white hover:tw-bg-stone-100 tw-text-stone-50 sm:focus:tw-text-stone-900 dark:tw-bg-stone-950 dark:hover:tw-bg-stone-800 dark:tw-text-stone-900 dark:sm:focus:tw-text-stone-50\\">
foo
</div>
</template>
"
`;
exports[`transform tailwind prefix 4`] = `
"<template>
<div id=\\"testing\\" v-bind=\\"props\\" @click=\\"handleSomething\\" :data-test=\\"true\\" :class=\\"cn('tw-bg-white hover:tw-bg-stone-100 dark:tw-bg-stone-950 dark:hover:tw-bg-stone-800', true && 'tw-text-stone-50 sm:focus:tw-text-stone-900 dark:tw-text-stone-900 dark:sm:focus:tw-text-stone-50')\\">
foo
</div>
</template>
"
`;
exports[`transform tailwind prefix 5`] = `
"@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 224 71.4% 4.1%;
--muted: 220 14.3% 95.9%;
--muted-foreground: 220 8.9% 46.1%;
--popover: 0 0% 100%;
--popover-foreground: 224 71.4% 4.1%;
--card: 0 0% 100%;
--card-foreground: 224 71.4% 4.1%;
--border: 220 13% 91%;
--input: 220 13% 91%;
--primary: 220.9 39.3% 11%;
--primary-foreground: 210 20% 98%;
--secondary: 220 14.3% 95.9%;
--secondary-foreground: 220.9 39.3% 11%;
--accent: 220 14.3% 95.9%;
--accent-foreground: 220.9 39.3% 11%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 20% 98%;
--ring: 217.9 10.6% 64.9%;
--radius: 0.5rem;
}
.dark {
--background: 224 71.4% 4.1%;
--foreground: 210 20% 98%;
--muted: 215 27.9% 16.9%;
--muted-foreground: 217.9 10.6% 64.9%;
--popover: 224 71.4% 4.1%;
--popover-foreground: 210 20% 98%;
--card: 224 71.4% 4.1%;
--card-foreground: 210 20% 98%;
--border: 215 27.9% 16.9%;
--input: 215 27.9% 16.9%;
--primary: 210 20% 98%;
--primary-foreground: 220.9 39.3% 11%;
--secondary: 215 27.9% 16.9%;
--secondary-foreground: 210 20% 98%;
--accent: 215 27.9% 16.9%;
--accent-foreground: 210 20% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 85.7% 97.3%;
--ring: 215 27.9% 16.9%;
}
}
@layer base {
* {
@apply tw-border-border;
}
body {
@apply tw-bg-background tw-text-foreground;
}
}"
`;

View File

@ -1,4 +1,4 @@
import { describe, expect, test } from 'vitest'
import { describe, expect, it } from 'vitest'
import {
applyColorMapping,
@ -7,7 +7,7 @@ import {
import baseColor from '../fixtures/colors/slate.json'
describe('split class', () => {
test.each([
it.each([
{
input: 'bg-popover',
output: [null, 'bg-popover', null],
@ -50,7 +50,7 @@ describe('split class', () => {
})
describe('apply color mapping', async () => {
test.each([
it.each([
{
input: 'bg-background text-foreground',
output: 'bg-white text-slate-950 dark:bg-slate-950 dark:text-slate-50',
@ -64,7 +64,7 @@ describe('apply color mapping', async () => {
input:
'text-destructive border-destructive/50 dark:border-destructive [&>svg]:text-destructive text-destructive',
output:
'text-red-500 border-red-500/50 dark:border-red-500 [&>svg]:text-red-500 text-red-500 dark:text-red-900 dark:border-red-900/50 dark:dark:border-red-900 dark:[&>svg]:text-red-900 dark:text-red-900',
'text-red-500 border-red-500/50 dark:border-red-500 [&>svg]:text-red-500 dark:text-red-900 dark:border-red-900/50 dark:dark:border-red-900 dark:[&>svg]:text-red-900',
},
{
input:

View File

@ -0,0 +1,42 @@
import { describe, expect, it } from 'vitest'
import { applyPrefix } from '../../src/utils/transformers/transform-tw-prefix'
describe('apply tailwind prefix', () => {
it.each([
{
input: 'bg-slate-800 text-gray-500',
output: 'tw-bg-slate-800 tw-text-gray-500',
},
{
input: 'hover:dark:bg-background dark:text-foreground',
output: 'hover:dark:tw-bg-background dark:tw-text-foreground',
},
{
input:
'rounded-lg border border-slate-200 bg-white text-slate-950 shadow-sm dark:border-slate-800 dark:bg-slate-950 dark:text-slate-50',
output:
'tw-rounded-lg tw-border tw-border-slate-200 tw-bg-white tw-text-slate-950 tw-shadow-sm dark:tw-border-slate-800 dark:tw-bg-slate-950 dark:tw-text-slate-50',
},
{
input:
'text-red-500 border-red-500/50 dark:border-red-500 [&>svg]:text-red-500 text-red-500 dark:text-red-900 dark:border-red-900/50 dark:dark:border-red-900 dark:[&>svg]:text-red-900 dark:text-red-900',
output:
'tw-text-red-500 tw-border-red-500/50 dark:tw-border-red-500 [&>svg]:tw-text-red-500 tw-text-red-500 dark:tw-text-red-900 dark:tw-border-red-900/50 dark:dark:tw-border-red-900 dark:[&>svg]:tw-text-red-900 dark:tw-text-red-900',
},
{
input:
'flex h-full w-full items-center justify-center rounded-full bg-muted',
output:
'tw-flex tw-h-full tw-w-full tw-items-center tw-justify-center tw-rounded-full tw-bg-muted',
},
{
input:
'absolute right-4 top-4 bg-primary rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary',
output:
'tw-absolute tw-right-4 tw-top-4 tw-bg-primary tw-rounded-sm tw-opacity-70 tw-ring-offset-background tw-transition-opacity hover:tw-opacity-100 focus:tw-outline-none focus:tw-ring-2 focus:tw-ring-ring focus:tw-ring-offset-2 disabled:tw-pointer-events-none data-[state=open]:tw-bg-secondary',
},
])(`applyTwPrefix($input) -> $output`, ({ input, output }) => {
expect(applyPrefix(input, 'tw-')).toBe(output)
})
})

View File

@ -1,9 +1,9 @@
import path from 'node:path'
import { expect, test } from 'vitest'
import path from 'pathe'
import { expect, it } from 'vitest'
import { getConfig, getRawConfig } from '../../src/utils/get-config'
test('get raw config', async () => {
it('get raw config', async () => {
expect(
await getRawConfig(path.resolve(__dirname, '../fixtures/config-none')),
).toEqual(null)
@ -31,7 +31,7 @@ test('get raw config', async () => {
).rejects.toThrowError()
})
test('get config', async () => {
it('get config', async () => {
expect(
await getConfig(path.resolve(__dirname, '../fixtures/config-none')),
).toEqual(null)
@ -71,6 +71,11 @@ test('get config', async () => {
'../fixtures/config-partial',
'./components',
),
ui: path.resolve(
__dirname,
'../fixtures/config-partial',
'./components',
),
utils: path.resolve(
__dirname,
'../fixtures/config-partial',
@ -89,9 +94,11 @@ test('get config', async () => {
baseColor: 'zinc',
css: 'src/app/globals.css',
cssVariables: true,
prefix: 'tw-',
},
aliases: {
components: '~/components',
ui: '~/ui',
utils: '~/lib/utils',
},
framework: 'Vite',
@ -111,6 +118,11 @@ test('get config', async () => {
'../fixtures/config-full',
'./src/components',
),
ui: path.resolve(
__dirname,
'../fixtures/config-full',
'./src/ui',
),
utils: path.resolve(
__dirname,
'../fixtures/config-full',
@ -152,6 +164,11 @@ test('get config', async () => {
'../fixtures/config-js',
'./components',
),
ui: path.resolve(
__dirname,
'../fixtures/config-js',
'./components',
),
utils: path.resolve(__dirname, '../fixtures/config-js', './lib/utils'),
},
})

View File

@ -1,26 +0,0 @@
import path from 'node:path'
import { expect, test } from 'vitest'
import { getPackageManager } from '../../src/utils/get-package-manager'
test('get package manager', async () => {
expect(
await getPackageManager(path.resolve(__dirname, '../fixtures/project-yarn')),
).toBe('yarn')
expect(
await getPackageManager(path.resolve(__dirname, '../fixtures/project-npm')),
).toBe('npm')
expect(
await getPackageManager(path.resolve(__dirname, '../fixtures/project-pnpm')),
).toBe('pnpm')
expect(
await getPackageManager(path.resolve(__dirname, '../fixtures/project-bun')),
).toBe('bun')
expect(
await getPackageManager(path.resolve(__dirname, '../fixtures/next')),
).toBe('pnpm')
})

View File

@ -1,4 +1,4 @@
import path from 'node:path'
import path from 'pathe'
import { type ConfigLoaderSuccessResult, loadConfig } from 'tsconfig-paths'
import { expect, test } from 'vitest'

View File

@ -1,9 +1,9 @@
import { expect, test } from 'vitest'
import { expect, it } from 'vitest'
import { transform } from '../../src/utils/transformers'
import stone from '../fixtures/colors/stone.json'
test('transform css vars', async () => {
it('transform css vars', async () => {
expect(
await transform({
filename: 'app.vue',

View File

@ -1,4 +1,4 @@
import { resolve } from 'node:path'
import { resolve } from 'pathe'
import { describe, expect, test } from 'vitest'
import { transform } from '../../src/utils/transformers'

View File

@ -0,0 +1,115 @@
import { expect, it } from 'vitest'
import { transform } from '../../src/utils/transformers'
import { applyPrefixesCss } from '../../src/utils/transformers/transform-tw-prefix'
import stone from '../fixtures/colors/stone.json'
it('transform tailwind prefix', async () => {
// expect(
// await transform({
// filename: 'test.ts',
// raw: `const testVariants = cva(
// 'bg-background hover:bg-muted text-primary-foreground sm:focus:text-accent-foreground',
// {
// variants: {
// variant: {
// default: 'bg-primary text-primary-foreground hover:bg-primary/90',
// },
// size: {
// default: 'h-10 px-4 py-2',
// },
// },
// },
// )`,
// config: {
// tailwind: {
// baseColor: 'stone',
// prefix: 'tw-',
// },
// aliases: {
// components: '@/components',
// utils: '@/lib/utils',
// },
// },
// baseColor: 'stone',
// }),
// ).toMatchSnapshot()
// expect(
// await transform({
// filename: 'app.vue',
// raw: `<template>
// <div class="bg-background hover:bg-muted text-primary-foreground sm:focus:text-accent-foreground">
// foo
// </div>
// </template>
// `,
// config: {
// tailwind: {
// baseColor: 'stone',
// prefix: 'tw-',
// },
// aliases: {
// components: '@/components',
// utils: '@/lib/utils',
// },
// },
// baseColor: 'stone',
// }),
// ).toMatchSnapshot()
// expect(
// await transform({
// filename: 'app.vue',
// raw: `<template>
// <div class="bg-background hover:bg-muted text-primary-foreground sm:focus:text-accent-foreground">
// foo
// </div>
// </template>
// `,
// config: {
// tailwind: {
// baseColor: 'stone',
// cssVariables: false,
// prefix: 'tw-',
// },
// aliases: {
// components: '@/components',
// utils: '@/lib/utils',
// },
// },
// baseColor: stone,
// }),
// ).toMatchSnapshot()
// expect(
// await transform({
// filename: 'app.vue',
// raw: `<template>
// <div id="testing" v-bind="props" @click="handleSomething" :data-test="true" :class="cn('bg-background hover:bg-muted', true && 'text-primary-foreground sm:focus:text-accent-foreground')">
// foo
// </div>
// </template>
// `,
// config: {
// tailwind: {
// baseColor: 'stone',
// cssVariables: false,
// prefix: 'tw-',
// },
// aliases: {
// components: '@/components',
// utils: '@/lib/utils',
// },
// },
// baseColor: stone,
// }),
// ).toMatchSnapshot()
// expect(
// applyPrefixesCss(
// '@tailwind base;\n@tailwind components;\n@tailwind utilities;\n \n@layer base {\n :root {\n --background: 0 0% 100%;\n --foreground: 224 71.4% 4.1%;\n \n --muted: 220 14.3% 95.9%;\n --muted-foreground: 220 8.9% 46.1%;\n \n --popover: 0 0% 100%;\n --popover-foreground: 224 71.4% 4.1%;\n \n --card: 0 0% 100%;\n --card-foreground: 224 71.4% 4.1%;\n \n --border: 220 13% 91%;\n --input: 220 13% 91%;\n \n --primary: 220.9 39.3% 11%;\n --primary-foreground: 210 20% 98%;\n \n --secondary: 220 14.3% 95.9%;\n --secondary-foreground: 220.9 39.3% 11%;\n \n --accent: 220 14.3% 95.9%;\n --accent-foreground: 220.9 39.3% 11%;\n \n --destructive: 0 84.2% 60.2%;\n --destructive-foreground: 210 20% 98%;\n \n --ring: 217.9 10.6% 64.9%;\n \n --radius: 0.5rem;\n }\n \n .dark {\n --background: 224 71.4% 4.1%;\n --foreground: 210 20% 98%;\n \n --muted: 215 27.9% 16.9%;\n --muted-foreground: 217.9 10.6% 64.9%;\n \n --popover: 224 71.4% 4.1%;\n --popover-foreground: 210 20% 98%;\n \n --card: 224 71.4% 4.1%;\n --card-foreground: 210 20% 98%;\n \n --border: 215 27.9% 16.9%;\n --input: 215 27.9% 16.9%;\n \n --primary: 210 20% 98%;\n --primary-foreground: 220.9 39.3% 11%;\n \n --secondary: 215 27.9% 16.9%;\n --secondary-foreground: 210 20% 98%;\n \n --accent: 215 27.9% 16.9%;\n --accent-foreground: 210 20% 98%;\n \n --destructive: 0 62.8% 30.6%;\n --destructive-foreground: 0 85.7% 97.3%;\n \n --ring: 215 27.9% 16.9%;\n }\n}\n \n@layer base {\n * {\n @apply border-border;\n }\n body {\n @apply bg-background text-foreground;\n }\n}',
// 'tw-',
// ),
// ).toMatchSnapshot()
})

View File

@ -35,17 +35,17 @@
"release": "pnpm run prepack && pnpm publish && git push --follow-tags"
},
"dependencies": {
"@nuxt/kit": "^3.8.2",
"oxc-parser": "^0.2.0"
"@nuxt/kit": "^3.10.3",
"oxc-parser": "^0.7.0"
},
"devDependencies": {
"@nuxt/devtools": "latest",
"@nuxt/eslint-config": "^0.2.0",
"@nuxt/module-builder": "^0.5.4",
"@nuxt/schema": "^3.8.2",
"@nuxt/test-utils": "^3.8.1",
"@types/node": "^20.9.3",
"nuxt": "^3.8.2",
"@nuxt/module-builder": "^0.5.5",
"@nuxt/schema": "^3.10.3",
"@nuxt/test-utils": "^3.11.0",
"@types/node": "^20.11.24",
"nuxt": "^3.10.3",
"vitest": "^0.33.0"
}
}

File diff suppressed because it is too large Load Diff