Merge remote-tracking branch 'origin/dev' into pr/zernonia/619

This commit is contained in:
zernonia 2024-10-11 12:44:06 +08:00
commit 71c5904a6a
155 changed files with 8108 additions and 4707 deletions

12
.editorconfig Normal file
View File

@ -0,0 +1,12 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

View File

@ -10,6 +10,7 @@
"source.fixAll.eslint": "explicit", "source.fixAll.eslint": "explicit",
"source.organizeImports": "never" "source.organizeImports": "never"
}, },
"eslint.useFlatConfig": true,
"eslint.rules.customizations": [ "eslint.rules.customizations": [
{ "rule": "style/*", "severity": "off" }, { "rule": "style/*", "severity": "off" },
{ "rule": "format/*", "severity": "off" }, { "rule": "format/*", "severity": "off" },

View File

@ -1,14 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { CircleHelp, Info, Monitor, Smartphone, Tablet } from 'lucide-vue-next'
import { reactive, ref, watch } from 'vue'
import { codeToHtml } from 'shiki'
import { compileScript, parse, walk } from 'vue/compiler-sfc'
import MagicString from 'magic-string'
import { cssVariables } from '../config/shiki'
import StyleSwitcher from './StyleSwitcher.vue'
import Spinner from './Spinner.vue'
import BlockCopyButton from './BlockCopyButton.vue'
import { useConfigStore } from '@/stores/config' import { useConfigStore } from '@/stores/config'
import { CircleHelp, Info, Monitor, Smartphone, Tablet } from 'lucide-vue-next'
import MagicString from 'magic-string'
import { codeToHtml } from 'shiki'
import { reactive, ref, watch } from 'vue'
import { compileScript, parse, walk } from 'vue/compiler-sfc'
import { cssVariables } from '../config/shiki'
import BlockCopyButton from './BlockCopyButton.vue'
import Spinner from './Spinner.vue'
import StyleSwitcher from './StyleSwitcher.vue'
// import { V0Button } from '@/components/v0-button' // import { V0Button } from '@/components/v0-button'
import { Badge } from '@/lib/registry/new-york/ui/badge' import { Badge } from '@/lib/registry/new-york/ui/badge'
@ -187,7 +187,7 @@ watch([style, codeConfig], async () => {
</p> </p>
<p> <p>
The <span class="font-medium">Default</span> style has The <span class="font-medium">Default</span> style has
larger inputs, uses lucide-react for icons and larger inputs, uses lucide-vue-next for icons and
tailwindcss-animate for animations. tailwindcss-animate for animations.
</p> </p>
<p> <p>

View File

@ -1,6 +1,6 @@
import { CreditCard } from 'lucide-vue-next'
import RiAppleFill from '~icons/ri/apple-fill' import RiAppleFill from '~icons/ri/apple-fill'
import RiPaypalFill from '~icons/ri/paypal-fill' import RiPaypalFill from '~icons/ri/paypal-fill'
import { CreditCard } from 'lucide-vue-next'
type Color = type Color =
| 'zinc' | 'zinc'
@ -32,11 +32,11 @@ export const allColors: Color[] = [
'violet', 'violet',
] ]
interface Payment { // interface Payment {
status: string // status: string
email: string // email: string
amount: number // amount: number
} // }
interface TeamMember { interface TeamMember {
name: string name: string

View File

@ -321,6 +321,11 @@ export const docsConfig: DocsConfig = {
href: '/docs/components/sonner', href: '/docs/components/sonner',
items: [], items: [],
}, },
{
title: 'Stepper',
href: '/docs/components/stepper',
label: 'New',
},
{ {
title: 'Switch', title: 'Switch',
href: '/docs/components/switch', href: '/docs/components/switch',

View File

@ -1,12 +1,11 @@
import { getParameters } from 'codesandbox/lib/api/define' import type { Style } from '@/lib/registry/styles'
import sdk from '@stackblitz/sdk' import sdk from '@stackblitz/sdk'
import { dependencies as deps } from '../../../package.json' import { getParameters } from 'codesandbox/lib/api/define'
import { Index as demoIndex } from '../../../../www/__registry__' import { Index as demoIndex } from '../../../../www/__registry__'
// @ts-expect-error ?raw // @ts-expect-error ?raw
import tailwindConfigRaw from '../../../tailwind.config?raw' import tailwindConfigRaw from '../../../tailwind.config?raw'
// @ts-expect-error ?raw // @ts-expect-error ?raw
import cssRaw from '../../../../../packages/cli/test/fixtures/nuxt/assets/css/tailwind.css?raw' import cssRaw from '../../../../../packages/cli/test/fixtures/nuxt/assets/css/tailwind.css?raw'
import type { Style } from '@/lib/registry/styles'
export function makeCodeSandboxParams(componentName: string, style: Style, sources: Record<string, string>) { export function makeCodeSandboxParams(componentName: string, style: Style, sources: Record<string, string>) {
let files: Record<string, any> = {} let files: Record<string, any> = {}
@ -92,7 +91,7 @@ function constructFiles(componentName: string, style: Style, sources: Record<str
const iconPackage = style === 'default' ? 'lucide-vue-next' : '@radix-icons/vue' const iconPackage = style === 'default' ? 'lucide-vue-next' : '@radix-icons/vue'
const dependencies = { const dependencies = {
'vue': 'latest', 'vue': 'latest',
'radix-vue': deps['radix-vue'], 'radix-vue': 'latest',
'@radix-ui/colors': 'latest', '@radix-ui/colors': 'latest',
'clsx': 'latest', 'clsx': 'latest',
'class-variance-authority': 'latest', 'class-variance-authority': 'latest',

View File

@ -521,6 +521,13 @@ export const Index = {
component: () => import("../src/lib/registry/default/example/DataTableDemoColumn.vue").then((m) => m.default), component: () => import("../src/lib/registry/default/example/DataTableDemoColumn.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/DataTableDemoColumn.vue"], files: ["../src/lib/registry/default/example/DataTableDemoColumn.vue"],
}, },
"DataTableReactiveDemo": {
name: "DataTableReactiveDemo",
type: "components:example",
registryDependencies: ["button","checkbox","dropdown-menu","input","table","utils"],
component: () => import("../src/lib/registry/default/example/DataTableReactiveDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/DataTableReactiveDemo.vue"],
},
"DatePickerDemo": { "DatePickerDemo": {
name: "DatePickerDemo", name: "DatePickerDemo",
type: "components:example", type: "components:example",
@ -570,6 +577,13 @@ export const Index = {
component: () => import("../src/lib/registry/default/example/DialogDemo.vue").then((m) => m.default), component: () => import("../src/lib/registry/default/example/DialogDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/DialogDemo.vue"], files: ["../src/lib/registry/default/example/DialogDemo.vue"],
}, },
"DialogForm": {
name: "DialogForm",
type: "components:example",
registryDependencies: ["button","form","dialog","input","toast"],
component: () => import("../src/lib/registry/default/example/DialogForm.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/DialogForm.vue"],
},
"DialogScrollBodyDemo": { "DialogScrollBodyDemo": {
name: "DialogScrollBodyDemo", name: "DialogScrollBodyDemo",
type: "components:example", type: "components:example",
@ -752,13 +766,6 @@ export const Index = {
component: () => import("../src/lib/registry/default/example/NavigationMenuDemo.vue").then((m) => m.default), component: () => import("../src/lib/registry/default/example/NavigationMenuDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/NavigationMenuDemo.vue"], files: ["../src/lib/registry/default/example/NavigationMenuDemo.vue"],
}, },
"NavigationMenuDemoItem": {
name: "NavigationMenuDemoItem",
type: "components:example",
registryDependencies: ["utils","navigation-menu"],
component: () => import("../src/lib/registry/default/example/NavigationMenuDemoItem.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/NavigationMenuDemoItem.vue"],
},
"NumberFieldCurrency": { "NumberFieldCurrency": {
name: "NumberFieldCurrency", name: "NumberFieldCurrency",
type: "components:example", type: "components:example",
@ -997,6 +1004,34 @@ export const Index = {
component: () => import("../src/lib/registry/default/example/SonnerWithDialog.vue").then((m) => m.default), component: () => import("../src/lib/registry/default/example/SonnerWithDialog.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/SonnerWithDialog.vue"], files: ["../src/lib/registry/default/example/SonnerWithDialog.vue"],
}, },
"StepperDemo": {
name: "StepperDemo",
type: "components:example",
registryDependencies: ["stepper"],
component: () => import("../src/lib/registry/default/example/StepperDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/StepperDemo.vue"],
},
"StepperForm": {
name: "StepperForm",
type: "components:example",
registryDependencies: ["stepper","form","select","input","button","toast"],
component: () => import("../src/lib/registry/default/example/StepperForm.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/StepperForm.vue"],
},
"StepperHorizental": {
name: "StepperHorizental",
type: "components:example",
registryDependencies: ["stepper","button"],
component: () => import("../src/lib/registry/default/example/StepperHorizental.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/StepperHorizental.vue"],
},
"StepperVertical": {
name: "StepperVertical",
type: "components:example",
registryDependencies: ["stepper","button"],
component: () => import("../src/lib/registry/default/example/StepperVertical.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/StepperVertical.vue"],
},
"SwitchDemo": { "SwitchDemo": {
name: "SwitchDemo", name: "SwitchDemo",
type: "components:example", type: "components:example",
@ -1025,6 +1060,13 @@ export const Index = {
component: () => import("../src/lib/registry/default/example/TabsDemo.vue").then((m) => m.default), component: () => import("../src/lib/registry/default/example/TabsDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/TabsDemo.vue"], files: ["../src/lib/registry/default/example/TabsDemo.vue"],
}, },
"TabsVerticalDemo": {
name: "TabsVerticalDemo",
type: "components:example",
registryDependencies: ["button","card","input","label","tabs"],
component: () => import("../src/lib/registry/default/example/TabsVerticalDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/TabsVerticalDemo.vue"],
},
"TagsInputComboboxDemo": { "TagsInputComboboxDemo": {
name: "TagsInputComboboxDemo", name: "TagsInputComboboxDemo",
type: "components:example", type: "components:example",
@ -1039,6 +1081,13 @@ export const Index = {
component: () => import("../src/lib/registry/default/example/TagsInputDemo.vue").then((m) => m.default), component: () => import("../src/lib/registry/default/example/TagsInputDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/TagsInputDemo.vue"], files: ["../src/lib/registry/default/example/TagsInputDemo.vue"],
}, },
"TagsInputFormDemo": {
name: "TagsInputFormDemo",
type: "components:example",
registryDependencies: ["tags-input","button","form","toast"],
component: () => import("../src/lib/registry/default/example/TagsInputFormDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/TagsInputFormDemo.vue"],
},
"TextareaDemo": { "TextareaDemo": {
name: "TextareaDemo", name: "TextareaDemo",
type: "components:example", type: "components:example",
@ -1971,6 +2020,13 @@ export const Index = {
component: () => import("../src/lib/registry/new-york/example/DataTableDemoColumn.vue").then((m) => m.default), component: () => import("../src/lib/registry/new-york/example/DataTableDemoColumn.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/DataTableDemoColumn.vue"], files: ["../src/lib/registry/new-york/example/DataTableDemoColumn.vue"],
}, },
"DataTableReactiveDemo": {
name: "DataTableReactiveDemo",
type: "components:example",
registryDependencies: ["button","checkbox","dropdown-menu","input","table","utils"],
component: () => import("../src/lib/registry/new-york/example/DataTableReactiveDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/DataTableReactiveDemo.vue"],
},
"DatePickerDemo": { "DatePickerDemo": {
name: "DatePickerDemo", name: "DatePickerDemo",
type: "components:example", type: "components:example",
@ -2020,6 +2076,13 @@ export const Index = {
component: () => import("../src/lib/registry/new-york/example/DialogDemo.vue").then((m) => m.default), component: () => import("../src/lib/registry/new-york/example/DialogDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/DialogDemo.vue"], files: ["../src/lib/registry/new-york/example/DialogDemo.vue"],
}, },
"DialogForm": {
name: "DialogForm",
type: "components:example",
registryDependencies: ["button","form","dialog","input","toast"],
component: () => import("../src/lib/registry/new-york/example/DialogForm.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/DialogForm.vue"],
},
"DialogScrollBodyDemo": { "DialogScrollBodyDemo": {
name: "DialogScrollBodyDemo", name: "DialogScrollBodyDemo",
type: "components:example", type: "components:example",
@ -2202,13 +2265,6 @@ export const Index = {
component: () => import("../src/lib/registry/new-york/example/NavigationMenuDemo.vue").then((m) => m.default), component: () => import("../src/lib/registry/new-york/example/NavigationMenuDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/NavigationMenuDemo.vue"], files: ["../src/lib/registry/new-york/example/NavigationMenuDemo.vue"],
}, },
"NavigationMenuDemoItem": {
name: "NavigationMenuDemoItem",
type: "components:example",
registryDependencies: ["utils","navigation-menu"],
component: () => import("../src/lib/registry/new-york/example/NavigationMenuDemoItem.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/NavigationMenuDemoItem.vue"],
},
"NumberFieldCurrency": { "NumberFieldCurrency": {
name: "NumberFieldCurrency", name: "NumberFieldCurrency",
type: "components:example", type: "components:example",
@ -2447,6 +2503,34 @@ export const Index = {
component: () => import("../src/lib/registry/new-york/example/SonnerWithDialog.vue").then((m) => m.default), component: () => import("../src/lib/registry/new-york/example/SonnerWithDialog.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/SonnerWithDialog.vue"], files: ["../src/lib/registry/new-york/example/SonnerWithDialog.vue"],
}, },
"StepperDemo": {
name: "StepperDemo",
type: "components:example",
registryDependencies: ["stepper"],
component: () => import("../src/lib/registry/new-york/example/StepperDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/StepperDemo.vue"],
},
"StepperForm": {
name: "StepperForm",
type: "components:example",
registryDependencies: ["stepper","form","select","input","button","toast"],
component: () => import("../src/lib/registry/new-york/example/StepperForm.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/StepperForm.vue"],
},
"StepperHorizental": {
name: "StepperHorizental",
type: "components:example",
registryDependencies: ["stepper","button"],
component: () => import("../src/lib/registry/new-york/example/StepperHorizental.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/StepperHorizental.vue"],
},
"StepperVertical": {
name: "StepperVertical",
type: "components:example",
registryDependencies: ["stepper","button"],
component: () => import("../src/lib/registry/new-york/example/StepperVertical.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/StepperVertical.vue"],
},
"SwitchDemo": { "SwitchDemo": {
name: "SwitchDemo", name: "SwitchDemo",
type: "components:example", type: "components:example",
@ -2475,6 +2559,13 @@ export const Index = {
component: () => import("../src/lib/registry/new-york/example/TabsDemo.vue").then((m) => m.default), component: () => import("../src/lib/registry/new-york/example/TabsDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/TabsDemo.vue"], files: ["../src/lib/registry/new-york/example/TabsDemo.vue"],
}, },
"TabsVerticalDemo": {
name: "TabsVerticalDemo",
type: "components:example",
registryDependencies: ["button","card","input","label","tabs"],
component: () => import("../src/lib/registry/new-york/example/TabsVerticalDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/TabsVerticalDemo.vue"],
},
"TagsInputComboboxDemo": { "TagsInputComboboxDemo": {
name: "TagsInputComboboxDemo", name: "TagsInputComboboxDemo",
type: "components:example", type: "components:example",
@ -2489,6 +2580,13 @@ export const Index = {
component: () => import("../src/lib/registry/new-york/example/TagsInputDemo.vue").then((m) => m.default), component: () => import("../src/lib/registry/new-york/example/TagsInputDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/TagsInputDemo.vue"], files: ["../src/lib/registry/new-york/example/TagsInputDemo.vue"],
}, },
"TagsInputFormDemo": {
name: "TagsInputFormDemo",
type: "components:example",
registryDependencies: ["tags-input","button","form","toast"],
component: () => import("../src/lib/registry/new-york/example/TagsInputFormDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/TagsInputFormDemo.vue"],
},
"TextareaDemo": { "TextareaDemo": {
name: "TextareaDemo", name: "TextareaDemo",
type: "components:example", type: "components:example",

View File

@ -17,65 +17,65 @@
}, },
"dependencies": { "dependencies": {
"@formkit/auto-animate": "^0.8.2", "@formkit/auto-animate": "^0.8.2",
"@internationalized/date": "^3.5.4", "@internationalized/date": "^3.5.5",
"@radix-icons/vue": "^1.0.0", "@radix-icons/vue": "^1.0.0",
"@stackblitz/sdk": "^1.10.0", "@stackblitz/sdk": "^1.11.0",
"@tanstack/vue-table": "^8.17.3", "@tanstack/vue-table": "^8.20.5",
"@unovis/ts": "^1.4.1", "@unovis/ts": "^1.4.4",
"@unovis/vue": "^1.4.1", "@unovis/vue": "^1.4.4",
"@vee-validate/zod": "^4.13.1", "@vee-validate/zod": "^4.13.2",
"@vueuse/core": "^10.11.0", "@vueuse/core": "^11.1.0",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"codesandbox": "^2.2.3", "codesandbox": "^2.2.3",
"date-fns": "^3.6.0", "date-fns": "^4.1.0",
"embla-carousel-autoplay": "^8.1.5", "embla-carousel-autoplay": "^8.3.0",
"embla-carousel-vue": "^8.1.5", "embla-carousel-vue": "^8.3.0",
"lucide-vue-next": "^0.383.0", "lucide-vue-next": "^0.441.0",
"magic-string": "^0.30.10", "magic-string": "^0.30.11",
"radix-vue": "^1.8.4", "radix-vue": "catalog:",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"v-calendar": "^3.1.2", "v-calendar": "^3.1.2",
"vaul-vue": "^0.2.0", "vaul-vue": "^0.2.0",
"vee-validate": "4.13.1", "vee-validate": "4.13.2",
"vue": "^3.4.29", "vue": "^3.5.6",
"vue-sonner": "^1.1.2", "vue-sonner": "^1.1.5",
"vue-wrap-balancer": "^1.1.3", "vue-wrap-balancer": "^1.2.1",
"zod": "^3.23.8" "zod": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@babel/traverse": "^7.24.7", "@babel/traverse": "^7.25.6",
"@iconify-json/gravity-ui": "^1.1.3", "@iconify-json/gravity-ui": "^1.1.3",
"@iconify-json/lucide": "^1.1.190", "@iconify-json/lucide": "^1.1.198",
"@iconify-json/ph": "^1.1.13", "@iconify-json/ph": "^1.1.13",
"@iconify-json/radix-icons": "^1.1.14", "@iconify-json/radix-icons": "^1.1.14",
"@iconify-json/ri": "^1.1.20", "@iconify-json/ri": "^1.1.21",
"@iconify-json/simple-icons": "^1.1.104", "@iconify-json/simple-icons": "^1.1.108",
"@iconify-json/tabler": "^1.1.113", "@iconify-json/tabler": "^1.1.116",
"@iconify/vue": "^4.1.2", "@iconify/vue": "^4.1.2",
"@oxc-parser/wasm": "^0.14.0", "@oxc-parser/wasm": "catalog:",
"@shikijs/transformers": "^1.7.0", "@shikijs/transformers": "^1.17.7",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^20.14.4", "@types/node": "^22.5.5",
"@vitejs/plugin-vue": "^5.0.5", "@vitejs/plugin-vue": "^5.1.4",
"@vitejs/plugin-vue-jsx": "^4.0.0", "@vitejs/plugin-vue-jsx": "^4.0.1",
"@vue/compiler-core": "^3.4.29", "@vue/compiler-core": "^3.5.6",
"@vue/compiler-dom": "^3.4.29", "@vue/compiler-dom": "^3.5.6",
"@vue/tsconfig": "^0.5.1", "@vue/tsconfig": "^0.5.1",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.20",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"pathe": "^1.1.2", "pathe": "^1.1.2",
"rimraf": "^5.0.7", "rimraf": "^6.0.1",
"shiki": "^1.7.0", "shiki": "^1.17.7",
"tailwind-merge": "^2.3.0", "tailwind-merge": "^2.5.2",
"tailwindcss": "^3.4.4", "tailwindcss": "^3.4.12",
"tsx": "^4.15.6", "tsx": "^4.19.1",
"typescript": "^5.4.5", "typescript": "^5.6.2",
"unplugin-icons": "^0.19.0", "unplugin-icons": "^0.19.3",
"vitepress": "^1.2.3", "vitepress": "^1.3.4",
"vue-component-meta": "^2.0.21", "vue-component-meta": "^2.1.6",
"vue-tsc": "^2.0.21" "vue-tsc": "^2.1.6"
} }
} }

View File

@ -134,6 +134,6 @@ The Sonner component is provided by [vue-sonner](https://vue-sonner.vercel.app/)
### New Component - Carousel ### New Component - Carousel
[`Carousel`](/docs/components/toggle-group.html) - A carousel with motion and swipe built using [Embla](https://www.embla-carousel.com/) library. [`Carousel`](/docs/components/carousel.html) - A carousel with motion and swipe built using [Embla](https://www.embla-carousel.com/) library.
<ComponentPreview name="CarouselDemo" /> <ComponentPreview name="CarouselDemo" />

View File

@ -46,7 +46,7 @@ import { AspectRatio } from '@/components/ui/aspect-ratio'
<template> <template>
<div class="w-[450px]"> <div class="w-[450px]">
<AspectRatio :ratio="16 / 9"> <AspectRatio :ratio="16 / 9">
<img src="..." alt="Image" class="rounded-md object-cover"> <img src="..." alt="Image" class="rounded-md object-cover w-full h-full">
</AspectRatio> </AspectRatio>
</div> </div>
</template> </template>

View File

@ -58,7 +58,6 @@ Use a custom component as `slot` for `<BreadcrumbSeparator />` to create a custo
```vue showLineNumbers {2,20-22} ```vue showLineNumbers {2,20-22}
<script setup lang="ts"> <script setup lang="ts">
import { Slash } from 'lucide-react'
import { import {
Breadcrumb, Breadcrumb,
BreadcrumbItem, BreadcrumbItem,
@ -66,6 +65,7 @@ import {
BreadcrumbList, BreadcrumbList,
BreadcrumbSeparator, BreadcrumbSeparator,
} from '@/components/ui/breadcrumb' } from '@/components/ui/breadcrumb'
import { Slash } from 'lucide-vue-next'
</script> </script>
<template> <template>
@ -99,6 +99,8 @@ You can compose `<BreadcrumbItem />` with a `<DropdownMenu />` to create a dropd
```vue showLineNumbers {2-7,16-26} ```vue showLineNumbers {2-7,16-26}
<script setup lang="ts"> <script setup lang="ts">
import { BreadcrumbItem } from '@/components/ui/breadcrumb'
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@ -106,8 +108,6 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/lib/components/ui/dropdown-menu' } from '@/lib/components/ui/dropdown-menu'
import { BreadcrumbItem } from '@/components/ui/breadcrumb'
import ChevronDownIcon from '~icons/radix-icons/chevron-down' import ChevronDownIcon from '~icons/radix-icons/chevron-down'
</script> </script>
@ -169,13 +169,13 @@ To use a custom link component from your routing library, you can use the `asChi
```vue showLineNumbers {15-19} ```vue showLineNumbers {15-19}
<script setup lang="ts"> <script setup lang="ts">
import { RouterLink } from 'vue-router'
import { import {
Breadcrumb, Breadcrumb,
BreadcrumbItem, BreadcrumbItem,
BreadcrumbLink, BreadcrumbLink,
BreadcrumbList, BreadcrumbList,
} from '@/components/ui/breadcrumb' } from '@/components/ui/breadcrumb'
import { RouterLink } from 'vue-router'
</script> </script>
<template> <template>

View File

@ -24,6 +24,10 @@ If you're looking for a range calendar, check out the [Range Calendar](/docs/com
```bash ```bash
npx shadcn-vue@latest add calendar npx shadcn-vue@latest add calendar
``` ```
::: tip
The component depends on the [@internationalized/date](https://react-spectrum.adobe.com/internationalized/date/index.html) package, which solves a lot of the problems that come with working with dates and times in JavaScript.
Check [Dates & Times in Radix Vue](https://www.radix-vue.com/guides/dates.html) for more information and installation instructions.
:::
## Datepicker ## Datepicker

View File

@ -10,7 +10,7 @@ primitive: https://tanstack.com/table/v8/docs/guide/introduction
Every data table or datagrid I've created has been unique. They all behave differently, have specific sorting and filtering requirements, and work with different data sources. Every data table or datagrid I've created has been unique. They all behave differently, have specific sorting and filtering requirements, and work with different data sources.
It doesn't make sense to combine all of these variations into a single component. If we do that, we'll lose the flexibility that [headless UI](https://tanstack.com/table/v8/docs/guide/introduction#what-is-headless-ui) provides. It doesn't make sense to combine all of these variations into a single component. If we do that, we'll lose the flexibility that [headless UI](https://tanstack.com/table/latest/docs/introduction#what-is-headless-ui) provides.
So instead of a data-table component, I thought it would be more helpful to provide a guide on how to build your own. So instead of a data-table component, I thought it would be more helpful to provide a guide on how to build your own.
@ -55,6 +55,20 @@ npm install @tanstack/vue-table
<ComponentPreview name="DataTableColumnPinningDemo" /> <ComponentPreview name="DataTableColumnPinningDemo" />
### Reactive Table
A reactive table was added in `v8.20.0` of the TanStack Table. You can see the [docs](https://tanstack.com/table/latest/docs/framework/vue/guide/table-state#using-reactive-data) for more information. We added an example where we are randomizing `status` column. One main point is that you need to mutate **full** data, as it is a `shallowRef` object.
> __*⚠️ `shallowRef` is used under the hood for performance reasons, meaning that the data is not deeply reactive, only the `.value` is. To update the data you have to mutate the data directly.*__
Relative PR: [Tanstack/table #5687](https://github.com/TanStack/table/pull/5687#issuecomment-2281067245)
If you want to mutate `props.data`, you should use [`defineModel`](https://vuejs.org/api/sfc-script-setup.html#definemodel).
There is no difference between using `ref` or `shallowRef` for your data object; it will be automatically mutated by the TanStack Table to `shallowRef`.
<ComponentPreview name="DataTableReactiveDemo" />
## Prerequisites ## Prerequisites
We are going to build a table to show recent payments. Here's what our data looks like: We are going to build a table to show recent payments. Here's what our data looks like:
@ -149,12 +163,6 @@ Next, we'll create a `<DataTable />` component to render our table.
```vue ```vue
<script setup lang="ts" generic="TData, TValue"> <script setup lang="ts" generic="TData, TValue">
import type { ColumnDef } from '@tanstack/vue-table' import type { ColumnDef } from '@tanstack/vue-table'
import {
FlexRender,
getCoreRowModel,
useVueTable,
} from '@tanstack/vue-table'
import { import {
Table, Table,
TableBody, TableBody,
@ -164,6 +172,12 @@ import {
TableRow, TableRow,
} from '@/components/ui/table' } from '@/components/ui/table'
import {
FlexRender,
getCoreRowModel,
useVueTable,
} from '@tanstack/vue-table'
const props = defineProps<{ const props = defineProps<{
columns: ColumnDef<TData, TValue>[] columns: ColumnDef<TData, TValue>[]
data: TData[] data: TData[]
@ -227,9 +241,9 @@ Finally, we'll render our table in our index component.
```vue ```vue
<script setup lang="ts"> <script setup lang="ts">
import type { Payment } from './components/columns'
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { columns } from './components/columns' import { columns } from './components/columns'
import type { Payment } from './components/columns'
import DataTable from './components/DataTable.vue' import DataTable from './components/DataTable.vue'
const data = ref<Payment[]>([]) const data = ref<Payment[]>([])
@ -303,9 +317,9 @@ Let's add row actions to our table. We'll use a `<Dropdown />` component for thi
```vue ```vue
<script setup lang="ts"> <script setup lang="ts">
import { MoreHorizontal } from 'lucide-vue-next'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { MoreHorizontal } from 'lucide-vue-next'
defineProps<{ defineProps<{
payment: { payment: {
@ -344,8 +358,8 @@ function copy(id: string) {
Update our columns definition to add a new `actions` column. The `actions` cell returns a `<Dropdown />` component. Update our columns definition to add a new `actions` column. The `actions` cell returns a `<Dropdown />` component.
```ts ```ts
import { ColumnDef } from '@tanstack/vue-table'
import DropdownAction from '@/components/DataTableDropDown.vue' import DropdownAction from '@/components/DataTableDropDown.vue'
import { ColumnDef } from '@tanstack/vue-table'
export const columns: ColumnDef<Payment>[] = [ export const columns: ColumnDef<Payment>[] = [
// ... // ...
@ -451,12 +465,12 @@ Let's make the email column sortable.
### Add the following into your `utils` file ### Add the following into your `utils` file
```ts ```ts
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
import type { Updater } from '@tanstack/vue-table' import type { Updater } from '@tanstack/vue-table'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))
} }
@ -866,11 +880,237 @@ This adds a checkbox to each row and a checkbox in the header to select all rows
You can show the number of selected rows using the `table.getFilteredSelectedRowModel()` API. You can show the number of selected rows using the `table.getFilteredSelectedRowModel()` API.
```vue ```vue:line-numbers {8-11}
<div class="flex-1 text-sm text-muted-foreground"> <template>
{{ table.getFilteredSelectedRowModel().rows.length }} of <div>
{{ table.getFilteredRowModel().rows.length }} row(s) selected. <div class="border rounded-md">
</div> <Table />
</div>
<div class="flex items-center justify-end space-x-2 py-4">
<div class="flex-1 text-sm text-muted-foreground">
{{ table.getFilteredSelectedRowModel().rows.length }} of
{{ table.getFilteredRowModel().rows.length }} row(s) selected.
</div>
<div class="space-x-2">
<PaginationButtons />
</div>
</div>
</div>
</template>
```
</Steps>
<Steps>
## Expanding
Let's make rows expandable.
### Update `<DataTable>`
```vue:line-numbers {7,30,43,52,57,63,103-116}
<script setup lang="ts" generic="TData, TValue">
import type {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
ExpandedState,
} from '@tanstack/vue-table'
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { valueUpdater } from '@/lib/utils'
import { ArrowUpDown, ChevronDown } from 'lucide-vue-next'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { h, ref } from 'vue'
import {
FlexRender,
getCoreRowModel,
getPaginationRowModel,
getFilteredRowModel,
getSortedRowModel,
getExpandedRowModel,
useVueTable,
} from "@tanstack/vue-table"
const props = defineProps<{
columns: ColumnDef<TData, TValue>[]
data: TData[]
}>()
const sorting = ref<SortingState>([])
const columnFilters = ref<ColumnFiltersState>([])
const columnVisibility = ref<VisibilityState>({})
const rowSelection = ref({})
const expanded = ref<ExpandedState>({})
const table = useVueTable({
get data() { return props.data },
get columns() { return props.columns },
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getExpandedRowModel: getExpandedRowModel(),
onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting),
onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters),
onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility),
onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelection),
onExpandedChange: updaterOrValue => valueUpdater(updaterOrValue, expanded),
state: {
get sorting() { return sorting.value },
get columnFilters() { return columnFilters.value },
get columnVisibility() { return columnVisibility.value },
get rowSelection() { return rowSelection.value },
get expanded() { return expanded.value },
},
})
</script>
<template>
<div>
<div class="flex items-center py-4">
<Input class="max-w-sm" placeholder="Filter emails..."
:model-value="table.getColumn('email')?.getFilterValue() as string"
@update:model-value=" table.getColumn('email')?.setFilterValue($event)" />
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="outline" class="ml-auto">
Columns
<ChevronDown class="w-4 h-4 ml-2" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuCheckboxItem
v-for="column in table.getAllColumns().filter((column) => column.getCanHide())" :key="column.id"
class="capitalize" :checked="column.getIsVisible()" @update:checked="(value) => {
column.toggleVisibility(!!value)
}">
{{ column.id }}
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div class="border rounded-md">
<Table>
<TableHeader>
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
<TableHead v-for="header in headerGroup.headers" :key="header.id">
<FlexRender v-if="!header.isPlaceholder" :render="header.column.columnDef.header"
:props="header.getContext()" />
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<template v-if="table.getRowModel().rows?.length">
<template v-for="row in table.getRowModel().rows" :key="row.id">
<TableRow :data-state="row.getIsSelected() ? 'selected' : undefined">
<TableCell v-for="cell in row.getVisibleCells()" :key="cell.id">
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
</TableCell>
</TableRow>
<TableRow v-if="row.getIsExpanded()">
<TableCell :colspan="row.getAllCells().length">
{{ JSON.stringify(row.original) }}
</TableCell>
</TableRow>
</template>
</template>
<template v-else>
<TableRow>
<TableCell :colSpan="columns.length" class="h-24 text-center">
No results.
</TableCell>
</TableRow>
</template>
</TableBody>
</Table>
</div>
</div>
</template>
```
### Add the expand action to the `DataTableDropDown.vue` component
```vue:line-numbers {12-14,34-36}
<script setup lang="ts">
import { MoreHorizontal } from 'lucide-vue-next'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { Button } from '@/components/ui/button'
defineProps<{
payment: {
id: string
}
}>()
defineEmits<{
(e: 'expand'): void
}>()
function copy(id: string) {
navigator.clipboard.writeText(id)
}
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="ghost" class="w-8 h-8 p-0">
<span class="sr-only">Open menu</span>
<MoreHorizontal class="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem @click="copy(payment.id)">
Copy payment ID
</DropdownMenuItem>
<DropdownMenuItem @click="$emit('expand')">
Expand
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>View customer</DropdownMenuItem>
<DropdownMenuItem>View payment details</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>
```
### Make rows expandable
Now we can update the action cell to add the expand control.
```vue:line-numbers {11}
<script setup lang="ts">
export const columns: ColumnDef<Payment>[] = [
{
id: 'actions',
enableHiding: false,
cell: ({ row }) => {
const payment = row.original
return h('div', { class: 'relative' }, h(DropdownAction, {
payment,
onExpand: row.toggleExpanded,
}))
},
},
]
</script>
``` ```
</Steps> </Steps>

View File

@ -62,6 +62,10 @@ import {
<ComponentPreview name="DialogScrollOverlayDemo" /> <ComponentPreview name="DialogScrollOverlayDemo" />
### Form
<ComponentPreview name="DialogForm" />
## Notes ## Notes
To activate the `Dialog` component from within a `Context Menu` or `Dropdown Menu`, you must encase the `Context Menu` or `Dropdown Menu` component in the `Dialog` component. For more information, refer to the linked issue [here](https://github.com/radix-ui/primitives/issues/1836). To activate the `Dialog` component from within a `Context Menu` or `Dropdown Menu`, you must encase the `Context Menu` or `Dropdown Menu` component in the `Dialog` component. For more information, refer to the linked issue [here](https://github.com/radix-ui/primitives/issues/1836).

View File

@ -22,14 +22,14 @@ npx shadcn-vue@latest add number-field
```vue ```vue
<script setup lang="ts"> <script setup lang="ts">
import { Label } from '@/components/ui/label'
import { import {
NumberField, NumberField,
NumberFieldContent, NumberFieldContent,
NumberFieldDecrement, NumberFieldDecrement,
NumberFieldIncrement, NumberFieldIncrement,
NumberFieldInput, NumberFieldInput,
} from '@/lib/registry/default/ui/number-field' } from '@/components/ui/number-field'
import { Label } from '@/lib/registry/default/ui/label'
</script> </script>
<template> <template>

View File

@ -44,6 +44,6 @@ import { Separator } from '@/components/ui/separator'
</script> </script>
<template> <template>
<Separator /> <Separator label="Or" />
</template> </template>
``` ```

View File

@ -0,0 +1,64 @@
---
title: Stepper
description: A set of steps that are used to indicate progress through a multi-step process.
source: apps/www/src/lib/registry/default/ui/stepper
primitive: https://www.radix-vue.com/components/stepper.html
---
<ComponentPreview name="StepperDemo" />
## Installation
```bash
npx shadcn-vue@latest add stepper
```
## Usage
```vue
<script setup lang="ts">
import {
Stepper,
StepperDescription,
StepperIndicator,
StepperItem,
StepperSeparator,
StepperTitle,
StepperTrigger,
} from '@/components/ui/stepper'
</script>
<template>
<Stepper>
<StepperItem :step="1">
<StepperTrigger>
<StepperIndicator>1</StepperIndicator>
<StepperTitle>Step 1</StepperTitle>
<StepperDescription>This is the first step</StepperDescription>
</StepperTrigger>
<StepperSeparator />
</StepperItem>
<StepperItem :step="2">
<StepperTrigger>
<StepperIndicator>2</StepperIndicator>
<StepperTitle>Step 2</StepperTitle>
<StepperDescription>This is the second step</StepperDescription>
</StepperTrigger>
</StepperItem>
</Stepper>
</template>
```
## Examples
### Horizontal
<ComponentPreview name="StepperHorizental" />
### Vertical
<ComponentPreview name="StepperVertical" />
### Form
<ComponentPreview name="StepperForm" />

View File

@ -39,3 +39,9 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
</Tabs> </Tabs>
</template> </template>
``` ```
## Examples
### Vertical
<ComponentPreview name="TabsVerticalDemo" />

View File

@ -18,3 +18,7 @@ npx shadcn-vue@latest add tags-input
### Tags with Combobox ### Tags with Combobox
<ComponentPreview name="TagsInputComboboxDemo" /> <ComponentPreview name="TagsInputComboboxDemo" />
### Form
<ComponentPreview name="TagsInputFormDemo" />

View File

@ -2,6 +2,15 @@
title: Contribution title: Contribution
description: Learn on how to contribute to shadcn/vue. description: Learn on how to contribute to shadcn/vue.
--- ---
<script setup lang="ts">
import { Button } from "@/lib/registry/new-york/ui/button"
const latestSyncCommitTag = "06cc0cdf3d080555d26abbe6639f2d7f6341ec73"
const latestSyncCommitUrl = `https://github.com/shadcn-ui/ui/commit/${latestSyncCommitTag}`
const diffUrl = `https://github.com/shadcn-ui/ui/compare/${latestSyncCommitTag}...main`
</script>
## Introduction ## Introduction
Thanks for your interest in contributing to shadcn-vue.com. We're happy to have you here. Thanks for your interest in contributing to shadcn-vue.com. We're happy to have you here.
@ -212,9 +221,9 @@ Take a look at `DrawerDescription.vue`.
```vue ```vue
<script lang="ts" setup> <script lang="ts" setup>
import type { DrawerDescriptionProps } from 'vaul-vue' import type { DrawerDescriptionProps } from 'vaul-vue'
import { DrawerDescription } from 'vaul-vue'
import { type HtmlHTMLAttributes, computed } from 'vue'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { DrawerDescription } from 'vaul-vue'
import { computed, type HtmlHTMLAttributes } from 'vue'
const props = defineProps<DrawerDescriptionProps & { class?: HtmlHTMLAttributes['class'] }>() const props = defineProps<DrawerDescriptionProps & { class?: HtmlHTMLAttributes['class'] }>()
@ -261,9 +270,9 @@ Take a look at `AccordionItem.vue`
```vue ```vue
<script setup lang="ts"> <script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import { AccordionItem, type AccordionItemProps, useForwardProps } from 'radix-vue'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { AccordionItem, type AccordionItemProps, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<AccordionItemProps & { class?: HTMLAttributes['class'] }>() const props = defineProps<AccordionItemProps & { class?: HTMLAttributes['class'] }>()
@ -298,9 +307,9 @@ Let's take a look at `Button.vue`
```vue ```vue
<script setup lang="ts"> <script setup lang="ts">
import type { HTMLAttributes } from 'vue' import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { Primitive, type PrimitiveProps } from 'radix-vue' import { Primitive, type PrimitiveProps } from 'radix-vue'
import { type ButtonVariants, buttonVariants } from '.' import { type ButtonVariants, buttonVariants } from '.'
import { cn } from '@/lib/utils'
interface Props extends PrimitiveProps { interface Props extends PrimitiveProps {
variant?: ButtonVariants['variant'] variant?: ButtonVariants['variant']
@ -326,6 +335,25 @@ const props = withDefaults(defineProps<Props>(), {
You'll need to extend `PrimitiveProps` in your props to support `Primitive` component. In most cases you would also need a default value for [`as`](https://www.radix-vue.com/utilities/primitive.html#changing-as-value) property. You'll need to extend `PrimitiveProps` in your props to support `Primitive` component. In most cases you would also need a default value for [`as`](https://www.radix-vue.com/utilities/primitive.html#changing-as-value) property.
## Updating with `shadcn/ui`
`shadcn/vue` is an unofficial, community-led Vue port of `shadcn/ui`, as time goes by, they might get out of sync.
As of today, we are in sync with this <a :href="latestSyncCommitUrl" target="_blank">commit</a> of `shadcn/ui`.
Click on the following link to check if there are newer commits that we should be synced with.
<div class="text-center">
<a :href="diffUrl" target="_blank">
<Button>
Check Diff
</Button>
</a>
</div>
1. There are no changes - If you see "There isnt anything to compare", nothing needs to be done as we are synced with latest version.
2. If there are changes, you should review thoese changes and try to apply them on `shadcn/vue` codebase and create a PR, remember to update the `latestSyncCommitTag` in [this file](https://github.com/radix-vue/shadcn-vue/blob/dev/apps/www/src/content/docs/contribution.md) too.
## Debugging ## Debugging
Here are some tools and techniques that can help you debug more effectively while contributing to `shadcn/vue` or developing your own projects. Here are some tools and techniques that can help you debug more effectively while contributing to `shadcn/vue` or developing your own projects.

View File

@ -61,10 +61,10 @@ We're using [`useColorMode`](https://vueuse.org/core/usecolormode/) from [`@vueu
```vue ```vue
<script setup lang="ts"> <script setup lang="ts">
import { useColorMode } from '@vueuse/core'
import { Icon } from '@iconify/vue'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { Icon } from '@iconify/vue'
import { useColorMode } from '@vueuse/core'
const mode = useColorMode() const mode = useColorMode()
</script> </script>
@ -100,7 +100,7 @@ Place a mode toggle on your site to toggle between light and dark mode.
```astro title="src/pages/index.astro" ```astro title="src/pages/index.astro"
--- ---
import '../styles/globals.css' import '../styles/globals.css'
import { ModeToggle } from '@/components/ModeToggle.vue'; import ModeToggle from '@/components/ModeToggle.vue';
--- ---
<!-- Inline script --> <!-- Inline script -->

View File

@ -7,7 +7,7 @@ description: Adding dark mode to your nuxt app.
<Steps> <Steps>
### Install Dependencies <!-- ### Install Dependencies
```bash ```bash
npm install -D @nuxtjs/color-mode npm install -D @nuxtjs/color-mode
@ -25,24 +25,26 @@ export default defineNuxtConfig({
classSuffix: '' classSuffix: ''
} }
}) })
``` ``` -->
### Add a mode toggle
Place a mode toggle on your site to toggle between light and dark mode.
The `@nuxtjs/color-mode` module is automatically installed and configured during the installation of the `shadcn-nuxt` module, so you literally have nothing to do.
We're using [`useColorMode`](https://color-mode.nuxtjs.org/#usage) from [`Nuxt Color Mode`](https://color-mode.nuxtjs.org/).
Optional, to include icons for theme button. Optional, to include icons for theme button.
```bash ```bash
npm install -D @iconify/vue @iconify-json/radix-icons npm install -D @iconify/vue @iconify-json/radix-icons
``` ```
### Add a mode toggle
Place a mode toggle on your site to toggle between light and dark mode.
We're using [`useColorMode`](https://color-mode.nuxtjs.org/#usage) from [`Nuxt Color Mode`](https://color-mode.nuxtjs.org/).
```vue ```vue
<script setup lang="ts"> <script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { Icon } from '@iconify/vue'
const colorMode = useColorMode() const colorMode = useColorMode()
</script> </script>

View File

@ -27,11 +27,12 @@ We're using [`useColorMode`](https://vueuse.org/core/usecolormode/) from [`@vueu
```vue ```vue
<script setup lang="ts"> <script setup lang="ts">
import { useColorMode } from '@vueuse/core'
import { Icon } from '@iconify/vue'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { Icon } from '@iconify/vue'
import { useColorMode } from '@vueuse/core'
// Pass { disableTransition: false } to enable transitions
const mode = useColorMode() const mode = useColorMode()
</script> </script>

View File

@ -115,7 +115,7 @@ Write configuration to components.json. Proceed? > Y/n
### Import the globals.css file ### Import the globals.css file
Import the `globals.css` file in the `src/index.astro` file: Import the `globals.css` file in the `src/pages/index.astro` file:
```ts:line-numbers {2} ```ts:line-numbers {2}
--- ---

View File

@ -10,7 +10,7 @@ description: Install and configure Laravel with Inertia
Start by creating a new Laravel project with Inertia and Vue using the Laravel installer `laravel new my-app`: Start by creating a new Laravel project with Inertia and Vue using the Laravel installer `laravel new my-app`:
```bash ```bash
laravel new my-app --typescript --breeze --stack=vue --git --no-interaction laravel new my-app --breeze --stack=vue --git
``` ```
### Run the CLI ### Run the CLI

View File

@ -9,6 +9,12 @@ description: Install and configure Nuxt.
Start by creating a new Nuxt project using `create-nuxt-app`: Start by creating a new Nuxt project using `create-nuxt-app`:
<Callout>
If you're using the JS template, `jsconfig.json` must exist for the CLI to run without errors.
</Callout>
```bash ```bash
npx nuxi@latest init my-app npx nuxi@latest init my-app
``` ```
@ -82,7 +88,8 @@ export default defineNuxtModule<ShadcnVueOptions>({
}, },
}, },
async setup({ componentDir, prefix }) { async setup({ componentDir, prefix }) {
const isVeeValidateExist = await tryResolveModule('vee-validate'); const veeValidate = await tryResolveModule('vee-validate');
const vaulVue = await tryResolveModule('vaul-vue');
addComponentsDir( addComponentsDir(
{ {
@ -96,7 +103,7 @@ export default defineNuxtModule<ShadcnVueOptions>({
} }
); );
if (isVeeValidateExist !== undefined) { if (veeValidate !== undefined) {
addComponent({ addComponent({
filePath: 'vee-validate', filePath: 'vee-validate',
export: 'Form', export: 'Form',
@ -112,6 +119,17 @@ export default defineNuxtModule<ShadcnVueOptions>({
}); });
} }
if(vaulVue !== undefined) {
['DrawerPortal', 'DrawerTrigger', 'DrawerClose'].forEach((item) => {
addComponent({
filePath: 'vaul-vue',
export: item,
name: prefix + item,
priority: 999,
});
})
}
addComponent({ addComponent({
filePath: 'radix-vue', filePath: 'radix-vue',
export: 'PaginationRoot', export: 'PaginationRoot',

View File

@ -45,12 +45,12 @@ Install `tailwindcss` and its peer dependencies, then generate your `tailwind.co
#### `vite.config` #### `vite.config`
```typescript {5,6,9-13} ```typescript {5,6,9-13}
import path from "path" import path from 'node:path'
import { defineConfig } from "vite" import vue from '@vitejs/plugin-vue'
import vue from "@vitejs/plugin-vue" import autoprefixer from 'autoprefixer'
import tailwind from "tailwindcss" import tailwind from 'tailwindcss'
import autoprefixer from "autoprefixer" import { defineConfig } from 'vite'
export default defineConfig({ export default defineConfig({
css: { css: {
@ -61,7 +61,7 @@ Install `tailwindcss` and its peer dependencies, then generate your `tailwind.co
plugins: [vue()], plugins: [vue()],
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(__dirname, "./src"), '@': path.resolve(__dirname, './src'),
}, },
}, },
}) })
@ -91,6 +91,12 @@ Install `tailwindcss` and its peer dependencies, then generate your `tailwind.co
### Edit tsconfig/jsconfig.json ### Edit tsconfig/jsconfig.json
<Callout>
If you're using TypeScript, the current version of Vite splits configuration into three files, requiring the same change for `tsconfig.app.json`.
</Callout>
Add the code below to the compilerOptions of your `tsconfig.json` or `jsconfig.json` so your app can resolve paths without error Add the code below to the compilerOptions of your `tsconfig.json` or `jsconfig.json` so your app can resolve paths without error
```json {4-7} ```json {4-7}
@ -116,12 +122,12 @@ npm i -D @types/node
``` ```
```typescript {15-19} ```typescript {15-19}
import path from "path" import path from 'node:path'
import vue from "@vitejs/plugin-vue" import vue from '@vitejs/plugin-vue'
import { defineConfig } from "vite" import autoprefixer from 'autoprefixer'
import tailwind from "tailwindcss" import tailwind from 'tailwindcss'
import autoprefixer from "autoprefixer" import { defineConfig } from 'vite'
export default defineConfig({ export default defineConfig({
css: { css: {
@ -132,7 +138,7 @@ export default defineConfig({
plugins: [vue()], plugins: [vue()],
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(__dirname, "./src"), '@': path.resolve(__dirname, './src'),
}, },
}, },
}) })
@ -160,7 +166,7 @@ Which framework are you using? Vite / Nuxt / Laravel
Which style would you like to use? Default Which style would you like to use? Default
Which color would you like to use as base color? Slate Which color would you like to use as base color? Slate
Where is your tsconfig.json or jsconfig.json file? ./tsconfig.json Where is your tsconfig.json or jsconfig.json file? ./tsconfig.json
Where is your global CSS file? src/index.css Where is your global CSS file? src/assets/index.css
Do you want to use CSS variables for colors? no / yes Do you want to use CSS variables for colors? no / yes
Where is your tailwind.config.js located? tailwind.config.js Where is your tailwind.config.js located? tailwind.config.js
Configure the import alias for components: @/components Configure the import alias for components: @/components

View File

@ -3,8 +3,8 @@ title: Introduction
description: Re-usable components built with Radix Vue, and Tailwind CSS. description: Re-usable components built with Radix Vue, and Tailwind CSS.
--- ---
<script setup > <script setup>
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/lib/registry/default/ui/accordion' import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/lib/registry/new-york/ui/accordion'
</script> </script>
An unofficial, community-led [Vue](https://vuejs.org/) port of [shadcn/ui](https://ui.shadcn.com). We are not affiliated with [shadcn](https://twitter.com/shadcn), but we did get his blessing before creating a Vue version of his work. This project was born out of the need for a similar project for the Vue ecosystem. An unofficial, community-led [Vue](https://vuejs.org/) port of [shadcn/ui](https://ui.shadcn.com). We are not affiliated with [shadcn](https://twitter.com/shadcn), but we did get his blessing before creating a Vue version of his work. This project was born out of the need for a similar project for the Vue ecosystem.
@ -19,8 +19,13 @@ Pick the components you need. Use the CLI to automatically add the components, o
_Use this as a reference to build your own component libraries._ _Use this as a reference to build your own component libraries._
<div class="[&>h2]:!mb-0">
## FAQ ## FAQ
</div>
<div class="[&_h3]:!mt-0">
<Accordion type="multiple"> <Accordion type="multiple">
<AccordionItem value="faq-1"> <AccordionItem value="faq-1">
@ -58,3 +63,4 @@ But let us know if you do use it. We'd love to see what you build with it.
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
</div>

View File

@ -11,7 +11,7 @@ import { Label } from '@/lib/registry/new-york/ui/label'
<Label for="date" class="shrink-0"> <Label for="date" class="shrink-0">
Pick a date Pick a date
</Label> </Label>
<DatePickerWithRange class="[&>button]:w-[260px]" /> <DatePickerWithRange />
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -60,7 +60,7 @@ import { Textarea } from '@/lib/registry/new-york/ui/textarea'
<Select default-value="2"> <Select default-value="2">
<SelectTrigger <SelectTrigger
id="security-level" id="security-level"
class="line-clamp-1 w-full truncate" class="w-full truncate"
> >
<SelectValue placeholder="Select level" /> <SelectValue placeholder="Select level" />
</SelectTrigger> </SelectTrigger>

View File

@ -1,13 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'
import ChevronDownIcon from '~icons/radix-icons/chevron-down'
import { import {
Avatar, Avatar,
AvatarFallback, AvatarFallback,
AvatarImage, AvatarImage,
} from '@/lib/registry/new-york/ui/avatar' } from '@/lib/registry/new-york/ui/avatar'
import { Button } from '@/lib/registry/new-york/ui/button' import { Button } from '@/lib/registry/new-york/ui/button'
import { import {
Card, Card,
CardContent, CardContent,
@ -16,12 +14,14 @@ import {
CardTitle, CardTitle,
} from '@/lib/registry/new-york/ui/card' } from '@/lib/registry/new-york/ui/card'
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/lib/registry/new-york/ui/command' import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/lib/registry/new-york/ui/command'
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from '@/lib/registry/new-york/ui/popover' } from '@/lib/registry/new-york/ui/popover'
import ChevronDownIcon from '~icons/radix-icons/chevron-down'
import { ref } from 'vue'
const sofiaRole = ref('Owner') const sofiaRole = ref('Owner')
const jacksonRole = ref('Member') const jacksonRole = ref('Member')
@ -64,25 +64,25 @@ const jacksonRole = ref('Member')
<CommandList> <CommandList>
<CommandEmpty>No roles found.</CommandEmpty> <CommandEmpty>No roles found.</CommandEmpty>
<CommandGroup> <CommandGroup>
<CommandItem value="Viewer" class="teamaspace-y-1 flex flex-col items-start px-4 py-2"> <CommandItem value="Viewer" class="space-y-1 flex flex-col items-start px-4 py-2">
<p>Viewer</p> <p>Viewer</p>
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
Can view and comment. Can view and comment.
</p> </p>
</CommandItem> </CommandItem>
<CommandItem value="Developer" class="teamaspace-y-1 flex flex-col items-start px-4 py-2"> <CommandItem value="Developer" class="space-y-1 flex flex-col items-start px-4 py-2">
<p>Developer</p> <p>Developer</p>
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
Can view, comment and edit. Can view, comment and edit.
</p> </p>
</CommandItem> </CommandItem>
<CommandItem value="Billing" class="teamaspace-y-1 flex flex-col items-start px-4 py-2"> <CommandItem value="Billing" class="space-y-1 flex flex-col items-start px-4 py-2">
<p>Billing</p> <p>Billing</p>
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
Can view, comment and manage billing. Can view, comment and manage billing.
</p> </p>
</CommandItem> </CommandItem>
<CommandItem value="Owner" class="teamaspace-y-1 flex flex-col items-start px-4 py-2"> <CommandItem value="Owner" class="space-y-1 flex flex-col items-start px-4 py-2">
<p>Owner</p> <p>Owner</p>
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
Admin-level access to all resources. Admin-level access to all resources.
@ -122,25 +122,25 @@ const jacksonRole = ref('Member')
<CommandList> <CommandList>
<CommandEmpty>No roles found.</CommandEmpty> <CommandEmpty>No roles found.</CommandEmpty>
<CommandGroup> <CommandGroup>
<CommandItem value="Viewer" class="teamaspace-y-1 flex flex-col items-start px-4 py-2"> <CommandItem value="Viewer" class="space-y-1 flex flex-col items-start px-4 py-2">
<p>Viewer</p> <p>Viewer</p>
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
Can view and comment. Can view and comment.
</p> </p>
</CommandItem> </CommandItem>
<CommandItem value="Developer" class="teamaspace-y-1 flex flex-col items-start px-4 py-2"> <CommandItem value="Developer" class="space-y-1 flex flex-col items-start px-4 py-2">
<p>Developer</p> <p>Developer</p>
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
Can view, comment and edit. Can view, comment and edit.
</p> </p>
</CommandItem> </CommandItem>
<CommandItem value="Billing" class="teamaspace-y-1 flex flex-col items-start px-4 py-2"> <CommandItem value="Billing" class="space-y-1 flex flex-col items-start px-4 py-2">
<p>Billing</p> <p>Billing</p>
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
Can view, comment and manage billing. Can view, comment and manage billing.
</p> </p>
</CommandItem> </CommandItem>
<CommandItem value="Owner" class="teamaspace-y-1 flex flex-col items-start px-4 py-2"> <CommandItem value="Owner" class="space-y-1 flex flex-col items-start px-4 py-2">
<p>Owner</p> <p>Owner</p>
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
Admin-level access to all resources. Admin-level access to all resources.

View File

@ -1,16 +1,16 @@
<script setup lang="ts"> <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 { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/lib/registry/default/ui/form' import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/lib/registry/default/ui/form'
import { Separator } from '@/lib/registry/new-york/ui/separator'
import { RadioGroup, RadioGroupItem } from '@/lib/registry/new-york/ui/radio-group'
import { Switch } from '@/lib/registry/new-york/ui/switch'
import { Checkbox } from '@/lib/registry/new-york/ui/checkbox'
import { Button } from '@/lib/registry/new-york/ui/button' import { Button } from '@/lib/registry/new-york/ui/button'
import { Checkbox } from '@/lib/registry/new-york/ui/checkbox'
import { RadioGroup, RadioGroupItem } from '@/lib/registry/new-york/ui/radio-group'
import { Separator } from '@/lib/registry/new-york/ui/separator'
import { Switch } from '@/lib/registry/new-york/ui/switch'
import { toast } from '@/lib/registry/new-york/ui/toast' import { toast } from '@/lib/registry/new-york/ui/toast'
import { toTypedSchema } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
import { h } from 'vue'
import * as z from 'zod'
const notificationsFormSchema = toTypedSchema(z.object({ const notificationsFormSchema = toTypedSchema(z.object({
type: z.enum(['all', 'mentions', 'none'], { type: z.enum(['all', 'mentions', 'none'], {
@ -33,7 +33,7 @@ const { handleSubmit } = useForm({
}, },
}) })
const onSubmit = handleSubmit((values, { resetForm }) => { const onSubmit = handleSubmit((values) => {
toast({ toast({
title: 'You submitted the following values:', 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))), 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))),

View File

@ -1,9 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { type HTMLAttributes, type Ref, computed } from 'vue'
import { CalendarRoot, type CalendarRootEmits, type CalendarRootProps, useDateFormatter, useForwardPropsEmits } from 'radix-vue'
import { createDecade, createYear, toDate } from 'radix-vue/date'
import { type DateValue, getLocalTimeZone, today } from '@internationalized/date'
import { useVModel } from '@vueuse/core'
import { CalendarCell, CalendarCellTrigger, CalendarGrid, CalendarGridBody, CalendarGridHead, CalendarGridRow, CalendarHeadCell, CalendarHeader, CalendarHeading } from '@/lib/registry/default/ui/calendar' import { CalendarCell, CalendarCellTrigger, CalendarGrid, CalendarGridBody, CalendarGridHead, CalendarGridRow, CalendarHeadCell, CalendarHeader, CalendarHeading } from '@/lib/registry/default/ui/calendar'
import { import {
Select, Select,
@ -13,6 +8,11 @@ import {
SelectValue, SelectValue,
} from '@/lib/registry/default/ui/select' } from '@/lib/registry/default/ui/select'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { type DateValue, getLocalTimeZone, today } from '@internationalized/date'
import { useVModel } from '@vueuse/core'
import { CalendarRoot, type CalendarRootEmits, type CalendarRootProps, useDateFormatter, useForwardPropsEmits } from 'radix-vue'
import { createDecade, createYear, toDate } from 'radix-vue/date'
import { computed, type HTMLAttributes, type Ref } from 'vue'
const props = withDefaults(defineProps<CalendarRootProps & { class?: HTMLAttributes['class'] }>(), { const props = withDefaults(defineProps<CalendarRootProps & { class?: HTMLAttributes['class'] }>(), {
modelValue: undefined, modelValue: undefined,
@ -72,7 +72,7 @@ const formatter = useDateFormatter('en')
</Select> </Select>
<Select <Select
:default-value="props.placeholder.year.toString()" :default-value="placeholder.year.toString()"
@update:model-value="(v) => { @update:model-value="(v) => {
if (!v || !placeholder) return; if (!v || !placeholder) return;
if (Number(v) === placeholder?.year) return; if (Number(v) === placeholder?.year) return;

View File

@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@/lib/registry/default/ui/carousel'
import { Card, CardContent } from '@/lib/registry/default/ui/card' import { Card, CardContent } from '@/lib/registry/default/ui/card'
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@/lib/registry/default/ui/carousel'
</script> </script>
<template> <template>
<Carousel class="relative w-full max-w-xs"> <Carousel v-slot="{ canScrollNext }" class="relative w-full max-w-xs">
<CarouselContent> <CarouselContent>
<CarouselItem v-for="(_, index) in 5" :key="index"> <CarouselItem v-for="(_, index) in 5" :key="index">
<div class="p-1"> <div class="p-1">
@ -17,6 +17,6 @@ import { Card, CardContent } from '@/lib/registry/default/ui/card'
</CarouselItem> </CarouselItem>
</CarouselContent> </CarouselContent>
<CarouselPrevious /> <CarouselPrevious />
<CarouselNext /> <CarouselNext v-if="canScrollNext" />
</Carousel> </Carousel>
</template> </template>

View File

@ -1,15 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'
import {
ArrowUpCircle,
CheckCircle2,
Circle,
HelpCircle,
XCircle,
} from 'lucide-vue-next'
import type { Icon } from 'lucide-vue-next' import type { Icon } from 'lucide-vue-next'
import { cn } from '@/lib/utils'
import { Button } from '@/lib/registry/default/ui/button' import { Button } from '@/lib/registry/default/ui/button'
import { import {
Command, Command,
@ -19,11 +9,21 @@ import {
CommandItem, CommandItem,
CommandList, CommandList,
} from '@/lib/registry/default/ui/command' } from '@/lib/registry/default/ui/command'
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from '@/lib/registry/default/ui/popover' } from '@/lib/registry/default/ui/popover'
import { cn } from '@/lib/utils'
import {
ArrowUpCircle,
CheckCircle2,
Circle,
HelpCircle,
XCircle,
} from 'lucide-vue-next'
import { ref } from 'vue'
interface Status { interface Status {
value: string value: string
@ -60,7 +60,7 @@ const statuses: Status[] = [
] ]
const open = ref(false) const open = ref(false)
const value = ref<typeof statuses[number]>() // const value = ref<typeof statuses[number]>()
const selectedStatus = ref<Status>() const selectedStatus = ref<Status>()
</script> </script>

View File

@ -1,25 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import type { import type {
ColumnFiltersState, ColumnFiltersState,
ExpandedState,
SortingState, SortingState,
VisibilityState, VisibilityState,
} from '@tanstack/vue-table' } from '@tanstack/vue-table'
import {
FlexRender,
createColumnHelper,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useVueTable,
} from '@tanstack/vue-table'
import { ArrowUpDown, ChevronDown } from 'lucide-vue-next'
import { h, ref } from 'vue'
import DropdownAction from './DataTableDemoColumn.vue'
import { Button } from '@/lib/registry/default/ui/button' import { Button } from '@/lib/registry/default/ui/button'
import { Checkbox } from '@/lib/registry/default/ui/checkbox' import { Checkbox } from '@/lib/registry/default/ui/checkbox'
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuCheckboxItem, DropdownMenuCheckboxItem,
@ -36,6 +24,19 @@ import {
TableRow, TableRow,
} from '@/lib/registry/default/ui/table' } from '@/lib/registry/default/ui/table'
import { cn, valueUpdater } from '@/lib/utils' import { cn, valueUpdater } from '@/lib/utils'
import {
createColumnHelper,
FlexRender,
getCoreRowModel,
getExpandedRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useVueTable,
} from '@tanstack/vue-table'
import { ArrowUpDown, ChevronDown } from 'lucide-vue-next'
import { h, ref } from 'vue'
import DropdownAction from './DataTableDemoColumn.vue'
export interface Payment { export interface Payment {
id: string id: string
@ -133,6 +134,7 @@ const columns = [
return h('div', { class: 'relative' }, h(DropdownAction, { return h('div', { class: 'relative' }, h(DropdownAction, {
payment, payment,
onExpand: row.toggleExpanded,
})) }))
}, },
}), }),
@ -142,6 +144,7 @@ const sorting = ref<SortingState>([])
const columnFilters = ref<ColumnFiltersState>([]) const columnFilters = ref<ColumnFiltersState>([])
const columnVisibility = ref<VisibilityState>({}) const columnVisibility = ref<VisibilityState>({})
const rowSelection = ref({}) const rowSelection = ref({})
const expanded = ref<ExpandedState>({})
const table = useVueTable({ const table = useVueTable({
data, data,
@ -150,15 +153,18 @@ const table = useVueTable({
getPaginationRowModel: getPaginationRowModel(), getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(), getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(), getFilteredRowModel: getFilteredRowModel(),
getExpandedRowModel: getExpandedRowModel(),
onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting), onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting),
onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters), onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters),
onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility), onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility),
onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelection), onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelection),
onExpandedChange: updaterOrValue => valueUpdater(updaterOrValue, expanded),
state: { state: {
get sorting() { return sorting.value }, get sorting() { return sorting.value },
get columnFilters() { return columnFilters.value }, get columnFilters() { return columnFilters.value },
get columnVisibility() { return columnVisibility.value }, get columnVisibility() { return columnVisibility.value },
get rowSelection() { return rowSelection.value }, get rowSelection() { return rowSelection.value },
get expanded() { return expanded.value },
columnPinning: { columnPinning: {
left: ['status'], left: ['status'],
}, },
@ -213,21 +219,24 @@ const table = useVueTable({
</TableHeader> </TableHeader>
<TableBody> <TableBody>
<template v-if="table.getRowModel().rows?.length"> <template v-if="table.getRowModel().rows?.length">
<TableRow <template v-for="row in table.getRowModel().rows" :key="row.id">
v-for="row in table.getRowModel().rows" <TableRow :data-state="row.getIsSelected() && 'selected'">
:key="row.id" <TableCell
:data-state="row.getIsSelected() && 'selected'" v-for="cell in row.getVisibleCells()" :key="cell.id" :data-pinned="cell.column.getIsPinned()"
> :class="cn(
<TableCell { 'sticky bg-background/95': cell.column.getIsPinned() },
v-for="cell in row.getVisibleCells()" :key="cell.id" :data-pinned="cell.column.getIsPinned()" cell.column.getIsPinned() === 'left' ? 'left-0' : 'right-0',
:class="cn( )"
{ 'sticky bg-background/95': cell.column.getIsPinned() }, >
cell.column.getIsPinned() === 'left' ? 'left-0' : 'right-0', <FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
)" </TableCell>
> </TableRow>
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" /> <TableRow v-if="row.getIsExpanded()">
</TableCell> <TableCell :colspan="row.getAllCells().length">
</TableRow> {{ row.original }}
</TableCell>
</TableRow>
</template>
</template> </template>
<TableRow v-else> <TableRow v-else>

View File

@ -2,23 +2,13 @@
import type { import type {
ColumnDef, ColumnDef,
ColumnFiltersState, ColumnFiltersState,
ExpandedState,
SortingState, SortingState,
VisibilityState, VisibilityState,
} from '@tanstack/vue-table' } from '@tanstack/vue-table'
import {
FlexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useVueTable,
} from '@tanstack/vue-table'
import { ArrowUpDown, ChevronDown } from 'lucide-vue-next'
import { h, ref } from 'vue'
import DropdownAction from './DataTableDemoColumn.vue'
import { Button } from '@/lib/registry/default/ui/button' import { Button } from '@/lib/registry/default/ui/button'
import { Checkbox } from '@/lib/registry/default/ui/checkbox' import { Checkbox } from '@/lib/registry/default/ui/checkbox'
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuCheckboxItem, DropdownMenuCheckboxItem,
@ -35,6 +25,18 @@ import {
TableRow, TableRow,
} from '@/lib/registry/default/ui/table' } from '@/lib/registry/default/ui/table'
import { valueUpdater } from '@/lib/utils' import { valueUpdater } from '@/lib/utils'
import {
FlexRender,
getCoreRowModel,
getExpandedRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useVueTable,
} from '@tanstack/vue-table'
import { ArrowUpDown, ChevronDown } from 'lucide-vue-next'
import { h, ref } from 'vue'
import DropdownAction from './DataTableDemoColumn.vue'
export interface Payment { export interface Payment {
id: string id: string
@ -130,6 +132,7 @@ const columns: ColumnDef<Payment>[] = [
return h('div', { class: 'relative' }, h(DropdownAction, { return h('div', { class: 'relative' }, h(DropdownAction, {
payment, payment,
onExpand: row.toggleExpanded,
})) }))
}, },
}, },
@ -139,6 +142,7 @@ const sorting = ref<SortingState>([])
const columnFilters = ref<ColumnFiltersState>([]) const columnFilters = ref<ColumnFiltersState>([])
const columnVisibility = ref<VisibilityState>({}) const columnVisibility = ref<VisibilityState>({})
const rowSelection = ref({}) const rowSelection = ref({})
const expanded = ref<ExpandedState>({})
const table = useVueTable({ const table = useVueTable({
data, data,
@ -147,15 +151,18 @@ const table = useVueTable({
getPaginationRowModel: getPaginationRowModel(), getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(), getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(), getFilteredRowModel: getFilteredRowModel(),
getExpandedRowModel: getExpandedRowModel(),
onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting), onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting),
onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters), onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters),
onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility), onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility),
onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelection), onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelection),
onExpandedChange: updaterOrValue => valueUpdater(updaterOrValue, expanded),
state: { state: {
get sorting() { return sorting.value }, get sorting() { return sorting.value },
get columnFilters() { return columnFilters.value }, get columnFilters() { return columnFilters.value },
get columnVisibility() { return columnVisibility.value }, get columnVisibility() { return columnVisibility.value },
get rowSelection() { return rowSelection.value }, get rowSelection() { return rowSelection.value },
get expanded() { return expanded.value },
}, },
}) })
</script> </script>
@ -201,15 +208,18 @@ const table = useVueTable({
</TableHeader> </TableHeader>
<TableBody> <TableBody>
<template v-if="table.getRowModel().rows?.length"> <template v-if="table.getRowModel().rows?.length">
<TableRow <template v-for="row in table.getRowModel().rows" :key="row.id">
v-for="row in table.getRowModel().rows" <TableRow :data-state="row.getIsSelected() && 'selected'">
:key="row.id" <TableCell v-for="cell in row.getVisibleCells()" :key="cell.id">
:data-state="row.getIsSelected() && 'selected'" <FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
> </TableCell>
<TableCell v-for="cell in row.getVisibleCells()" :key="cell.id"> </TableRow>
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" /> <TableRow v-if="row.getIsExpanded()">
</TableCell> <TableCell :colspan="row.getAllCells().length">
</TableRow> {{ JSON.stringify(row.original) }}
</TableCell>
</TableRow>
</template>
</template> </template>
<TableRow v-else> <TableRow v-else>

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { MoreHorizontal } from 'lucide-vue-next'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/lib/registry/default/ui/dropdown-menu'
import { Button } from '@/lib/registry/default/ui/button' import { Button } from '@/lib/registry/default/ui/button'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/lib/registry/default/ui/dropdown-menu'
import { MoreHorizontal } from 'lucide-vue-next'
defineProps<{ defineProps<{
payment: { payment: {
@ -9,6 +9,10 @@ defineProps<{
} }
}>() }>()
defineEmits<{
(e: 'expand'): void
}>()
function copy(id: string) { function copy(id: string) {
navigator.clipboard.writeText(id) navigator.clipboard.writeText(id)
} }
@ -27,6 +31,9 @@ function copy(id: string) {
<DropdownMenuItem @click="copy(payment.id)"> <DropdownMenuItem @click="copy(payment.id)">
Copy payment ID Copy payment ID
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem @click="$emit('expand')">
Expand
</DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem>View customer</DropdownMenuItem> <DropdownMenuItem>View customer</DropdownMenuItem>
<DropdownMenuItem>View payment details</DropdownMenuItem> <DropdownMenuItem>View payment details</DropdownMenuItem>

View File

@ -0,0 +1,273 @@
<script setup lang="ts">
import type {
ColumnDef,
ColumnFiltersState,
ExpandedState,
SortingState,
VisibilityState,
} from '@tanstack/vue-table'
import { Button } from '@/lib/registry/default/ui/button'
import { Checkbox } from '@/lib/registry/default/ui/checkbox'
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@/lib/registry/default/ui/dropdown-menu'
import { Input } from '@/lib/registry/default/ui/input'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/lib/registry/default/ui/table'
import { valueUpdater } from '@/lib/utils'
import {
FlexRender,
getCoreRowModel,
getExpandedRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useVueTable,
} from '@tanstack/vue-table'
import { ArrowUpDown, ChevronDown } from 'lucide-vue-next'
import { h, ref, shallowRef } from 'vue'
import DropdownAction from './DataTableDemoColumn.vue'
export interface Payment {
id: string
amount: number
status: 'pending' | 'processing' | 'success' | 'failed'
email: string
}
const data = shallowRef<Payment[]>([
{
id: 'm5gr84i9',
amount: 316,
status: 'success',
email: 'ken99@yahoo.com',
},
{
id: '3u1reuv4',
amount: 242,
status: 'success',
email: 'Abe45@gmail.com',
},
{
id: 'derv1ws0',
amount: 837,
status: 'processing',
email: 'Monserrat44@gmail.com',
},
{
id: '5kma53ae',
amount: 874,
status: 'success',
email: 'Silas22@gmail.com',
},
{
id: 'bhqecj4p',
amount: 721,
status: 'failed',
email: 'carmella@hotmail.com',
},
])
const columns: ColumnDef<Payment>[] = [
{
id: 'select',
header: ({ table }) => h(Checkbox, {
'checked': table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && 'indeterminate'),
'onUpdate:checked': value => table.toggleAllPageRowsSelected(!!value),
'ariaLabel': 'Select all',
}),
cell: ({ row }) => h(Checkbox, {
'checked': row.getIsSelected(),
'onUpdate:checked': value => row.toggleSelected(!!value),
'ariaLabel': 'Select row',
}),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: 'status',
header: 'Status',
cell: ({ row }) => h('div', { class: 'capitalize' }, row.getValue('status')),
},
{
accessorKey: 'email',
header: ({ column }) => {
return h(Button, {
variant: 'ghost',
onClick: () => column.toggleSorting(column.getIsSorted() === 'asc'),
}, () => ['Email', h(ArrowUpDown, { class: 'ml-2 h-4 w-4' })])
},
cell: ({ row }) => h('div', { class: 'lowercase' }, row.getValue('email')),
},
{
accessorKey: 'amount',
header: () => h('div', { class: 'text-right' }, 'Amount'),
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
// Format the amount as a dollar amount
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(amount)
return h('div', { class: 'text-right font-medium' }, formatted)
},
},
{
id: 'actions',
enableHiding: false,
cell: ({ row }) => {
const payment = row.original
return h('div', { class: 'relative' }, h(DropdownAction, {
payment,
onExpand: row.toggleExpanded,
}))
},
},
]
const sorting = ref<SortingState>([])
const columnFilters = ref<ColumnFiltersState>([])
const columnVisibility = ref<VisibilityState>({})
const rowSelection = ref({})
const expanded = ref<ExpandedState>({})
const table = useVueTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getExpandedRowModel: getExpandedRowModel(),
onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting),
onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters),
onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility),
onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelection),
onExpandedChange: updaterOrValue => valueUpdater(updaterOrValue, expanded),
state: {
get sorting() { return sorting.value },
get columnFilters() { return columnFilters.value },
get columnVisibility() { return columnVisibility.value },
get rowSelection() { return rowSelection.value },
get expanded() { return expanded.value },
},
})
const statuses: Payment['status'][] = ['pending', 'processing', 'success', 'failed']
function randomize() {
data.value = data.value.map(item => ({
...item,
status: statuses[Math.floor(Math.random() * statuses.length)],
}))
}
</script>
<template>
<div class="w-full">
<div class="flex gap-2 items-center py-4">
<Input
class="max-w-52"
placeholder="Filter emails..."
:model-value="table.getColumn('email')?.getFilterValue() as string"
@update:model-value=" table.getColumn('email')?.setFilterValue($event)"
/>
<Button @click="randomize">
Randomize
</Button>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="outline" class="ml-auto">
Columns <ChevronDown class="ml-2 h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuCheckboxItem
v-for="column in table.getAllColumns().filter((column) => column.getCanHide())"
:key="column.id"
class="capitalize"
:checked="column.getIsVisible()"
@update:checked="(value) => {
column.toggleVisibility(!!value)
}"
>
{{ column.id }}
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div class="rounded-md border">
<Table>
<TableHeader>
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
<TableHead v-for="header in headerGroup.headers" :key="header.id">
<FlexRender v-if="!header.isPlaceholder" :render="header.column.columnDef.header" :props="header.getContext()" />
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<template v-if="table.getRowModel().rows?.length">
<template v-for="row in table.getRowModel().rows" :key="row.id">
<TableRow :data-state="row.getIsSelected() && 'selected'">
<TableCell v-for="cell in row.getVisibleCells()" :key="cell.id">
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
</TableCell>
</TableRow>
<TableRow v-if="row.getIsExpanded()">
<TableCell :colspan="row.getAllCells().length">
{{ JSON.stringify(row.original) }}
</TableCell>
</TableRow>
</template>
</template>
<TableRow v-else>
<TableCell
:colspan="columns.length"
class="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
<div class="flex items-center justify-end space-x-2 py-4">
<div class="flex-1 text-sm text-muted-foreground">
{{ table.getFilteredSelectedRowModel().rows.length }} of
{{ table.getFilteredRowModel().rows.length }} row(s) selected.
</div>
<div class="space-x-2">
<Button
variant="outline"
size="sm"
:disabled="!table.getCanPreviousPage()"
@click="table.previousPage()"
>
Previous
</Button>
<Button
variant="outline"
size="sm"
:disabled="!table.getCanNextPage()"
@click="table.nextPage()"
>
Next
</Button>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,79 @@
<script setup lang="ts">
import { Button } from '@/lib/registry/default/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/lib/registry/default/ui/dialog'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/lib/registry/default/ui/form'
import { Input } from '@/lib/registry/default/ui/input'
import { toast } from '@/lib/registry/default/ui/toast'
import { toTypedSchema } from '@vee-validate/zod'
import { h } from 'vue'
import * as z from 'zod'
const formSchema = toTypedSchema(z.object({
username: z.string().min(2).max(50),
}))
function onSubmit(values: any) {
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 v-slot="{ submitForm }" as="" :validation-schema="formSchema" @submit="onSubmit">
<Dialog>
<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>
<form @submit="submitForm">
<FormField v-slot="{ componentField }" name="username">
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input type="text" placeholder="shadcn" v-bind="componentField" />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
</form>
<DialogFooter>
<Button type="submit" form="dialogForm">
Save changes
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Form>
</template>

View File

@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import ListItem from './NavigationMenuDemoItem.vue'
import { import {
NavigationMenu, NavigationMenu,
NavigationMenuContent, NavigationMenuContent,
@ -73,15 +72,46 @@ const components: { title: string, href: string, description: string }[] = [
</a> </a>
</NavigationMenuLink> </NavigationMenuLink>
</li> </li>
<ListItem href="/docs" title="Introduction">
Re-usable components built using Radix UI and Tailwind CSS. <li>
</ListItem> <NavigationMenuLink as-child>
<ListItem href="/docs/installation" title="Installation"> <a
How to install dependencies and structure your app. href="/docs"
</ListItem> class="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
<ListItem href="/docs/primitives/typography" title="Typography"> >
Styles for headings, paragraphs, lists...etc <div class="text-sm font-medium leading-none">Introduction</div>
</ListItem> <p class="line-clamp-2 text-sm leading-snug text-muted-foreground">
Re-usable components built using Radix UI and Tailwind CSS.
</p>
</a>
</NavigationMenuLink>
</li>
<li>
<NavigationMenuLink as-child>
<a
href="/docs/installation"
class="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
>
<div class="text-sm font-medium leading-none">Installation</div>
<p class="line-clamp-2 text-sm leading-snug text-muted-foreground">
How to install dependencies and structure your app.
</p>
</a>
</NavigationMenuLink>
</li>
<li>
<NavigationMenuLink as-child>
<a
href="/docs/primitives/typography"
class="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
>
<div class="text-sm font-medium leading-none">Typography</div>
<p class="line-clamp-2 text-sm leading-snug text-muted-foreground">
Styles for headings, paragraphs, lists...etc
</p>
</a>
</NavigationMenuLink>
</li>
</ul> </ul>
</NavigationMenuContent> </NavigationMenuContent>
</NavigationMenuItem> </NavigationMenuItem>
@ -89,14 +119,19 @@ const components: { title: string, href: string, description: string }[] = [
<NavigationMenuTrigger>Components</NavigationMenuTrigger> <NavigationMenuTrigger>Components</NavigationMenuTrigger>
<NavigationMenuContent> <NavigationMenuContent>
<ul class="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px] "> <ul class="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px] ">
<ListItem <li v-for="component in components" :key="component.title">
v-for="component in components" <NavigationMenuLink as-child>
:key="component.title" <a
:title="component.title" :href="component.href"
:href="component.href" class="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
> >
{{ component.description }} <div class="text-sm font-medium leading-none">{{ component.title }}</div>
</ListItem> <p class="line-clamp-2 text-sm leading-snug text-muted-foreground">
{{ component.description }}
</p>
</a>
</NavigationMenuLink>
</li>
</ul> </ul>
</NavigationMenuContent> </NavigationMenuContent>
</NavigationMenuItem> </NavigationMenuItem>

View File

@ -1,27 +0,0 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import {
NavigationMenuLink,
} from '@/lib/registry/default/ui/navigation-menu'
defineProps<{ title?: string, href?: string }>()
</script>
<template>
<li>
<NavigationMenuLink as-child>
<a
:href="href"
:class="cn(
'block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground',
$attrs.class ?? '',
)"
>
<div class="text-sm font-medium leading-none">{{ title }}</div>
<p class="line-clamp-2 text-sm leading-snug text-muted-foreground">
<slot />
</p>
</a>
</NavigationMenuLink>
</li>
</template>

View File

@ -1,9 +1,4 @@
<script setup lang="ts"> <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 { Button } from '@/lib/registry/default/ui/button'
import { import {
FormControl, FormControl,
@ -22,6 +17,11 @@ import {
} from '@/lib/registry/default/ui/number-field' } from '@/lib/registry/default/ui/number-field'
import { toast } from '@/lib/registry/default/ui/toast' import { toast } from '@/lib/registry/default/ui/toast'
import { toTypedSchema } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
import { h } from 'vue'
import * as z from 'zod'
const formSchema = toTypedSchema(z.object({ const formSchema = toTypedSchema(z.object({
payment: z.number().min(10, 'Min 10 euros to send payment').max(5000, 'Max 5000 euros to send payment'), payment: z.number().min(10, 'Min 10 euros to send payment').max(5000, 'Max 5000 euros to send payment'),
})) }))
@ -43,7 +43,7 @@ const onSubmit = handleSubmit((values) => {
<template> <template>
<form class="w-2/3 space-y-6" @submit="onSubmit"> <form class="w-2/3 space-y-6" @submit="onSubmit">
<FormField name="payment"> <FormField v-slot="{ value }" name="payment">
<FormItem> <FormItem>
<FormLabel>Payment</FormLabel> <FormLabel>Payment</FormLabel>
<NumberField <NumberField
@ -55,6 +55,7 @@ const onSubmit = handleSubmit((values) => {
currencyDisplay: 'code', currencyDisplay: 'code',
currencySign: 'accounting', currencySign: 'accounting',
}" }"
:model-value="value"
@update:model-value="(v) => { @update:model-value="(v) => {
if (v) { if (v) {
setFieldValue('payment', v) setFieldValue('payment', v)

View File

@ -1,13 +1,4 @@
<script setup lang="ts"> <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 {
PinInput,
PinInputGroup,
PinInputInput,
} from '@/lib/registry/default/ui/pin-input'
import { Button } from '@/lib/registry/default/ui/button' import { Button } from '@/lib/registry/default/ui/button'
import { import {
FormControl, FormControl,
@ -17,7 +8,16 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from '@/lib/registry/default/ui/form' } from '@/lib/registry/default/ui/form'
import {
PinInput,
PinInputGroup,
PinInputInput,
} from '@/lib/registry/default/ui/pin-input'
import { toast } from '@/lib/registry/default/ui/toast' import { toast } from '@/lib/registry/default/ui/toast'
import { toTypedSchema } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
import { h } from 'vue'
import * as z from 'zod'
const formSchema = toTypedSchema(z.object({ const formSchema = toTypedSchema(z.object({
pin: z.array(z.coerce.string()).length(5, { message: 'Invalid input' }), pin: z.array(z.coerce.string()).length(5, { message: 'Invalid input' }),
@ -48,7 +48,7 @@ const handleComplete = (e: string[]) => console.log(e.join(''))
<FormControl> <FormControl>
<PinInput <PinInput
id="pin-input" id="pin-input"
v-model="value!" :model-value="value"
placeholder="○" placeholder="○"
class="flex gap-2 items-center mt-1" class="flex gap-2 items-center mt-1"
otp otp

View File

@ -12,7 +12,7 @@ import { Separator } from '@/lib/registry/default/ui/separator'
An open-source UI component library. An open-source UI component library.
</p> </p>
</div> </div>
<Separator class="my-4" /> <Separator class="my-4" label="Or" />
<div class="flex h-5 items-center space-x-4 text-sm"> <div class="flex h-5 items-center space-x-4 text-sm">
<div>Blog</div> <div>Blog</div>
<Separator orientation="vertical" /> <Separator orientation="vertical" />

View File

@ -0,0 +1,56 @@
<script setup lang="ts">
import { Stepper, StepperDescription, StepperIndicator, StepperItem, StepperSeparator, StepperTitle, StepperTrigger } from '@/lib/registry/default/ui/stepper'
import { BookUser, Check, CreditCard, Truck } from 'lucide-vue-next'
const steps = [{
step: 1,
title: 'Address',
description: 'Add your address here',
icon: BookUser,
}, {
step: 2,
title: 'Shipping',
description: 'Set your preferred shipping method',
icon: Truck,
}, {
step: 3,
title: 'Payment',
description: 'Add any payment information you have',
icon: CreditCard,
}, {
step: 4,
title: 'Checkout',
description: 'Confirm your order',
icon: Check,
}]
</script>
<template>
<Stepper>
<StepperItem
v-for="item in steps"
:key="item.step"
class="basis-1/4"
:step="item.step"
>
<StepperTrigger>
<StepperIndicator>
<component :is="item.icon" class="w-4 h-4" />
</StepperIndicator>
<div class="flex flex-col">
<StepperTitle>
{{ item.title }}
</StepperTitle>
<StepperDescription>
{{ item.description }}
</StepperDescription>
</div>
</StepperTrigger>
<StepperSeparator
v-if="item.step !== steps[steps.length - 1].step"
class="w-full h-px"
/>
</StepperItem>
</Stepper>
</template>

View File

@ -0,0 +1,223 @@
<script setup lang="ts">
import { Button } from '@/lib/registry/default/ui/button'
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/lib/registry/default/ui/form'
import { Input } from '@/lib/registry/default/ui/input'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/lib/registry/default/ui/select'
import { Stepper, StepperDescription, StepperItem, StepperSeparator, StepperTitle, StepperTrigger } from '@/lib/registry/default/ui/stepper'
import { toast } from '@/lib/registry/default/ui/toast'
import { toTypedSchema } from '@vee-validate/zod'
import { Check, Circle, Dot } from 'lucide-vue-next'
import { h, ref } from 'vue'
import * as z from 'zod'
const formSchema = [
z.object({
fullName: z.string(),
email: z.string().email(),
}),
z.object({
password: z.string().min(2).max(50),
confirmPassword: z.string(),
}).refine(
(values) => {
return values.password === values.confirmPassword
},
{
message: 'Passwords must match!',
path: ['confirmPassword'],
},
),
z.object({
favoriteDrink: z.union([z.literal('coffee'), z.literal('tea'), z.literal('soda')]),
}),
]
const stepIndex = ref(1)
const steps = [
{
step: 1,
title: 'Your details',
description: 'Provide your name and email',
},
{
step: 2,
title: 'Your password',
description: 'Choose a password',
},
{
step: 3,
title: 'Your Favorite Drink',
description: 'Choose a drink',
},
]
function onSubmit(values: any) {
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
v-slot="{ meta, values, validate }"
as="" keep-values :validation-schema="toTypedSchema(formSchema[stepIndex - 1])"
>
<Stepper v-slot="{ isNextDisabled, isPrevDisabled, nextStep, prevStep }" v-model="stepIndex" class="block w-full">
<form
@submit="(e) => {
e.preventDefault()
validate()
if (stepIndex === steps.length && meta.valid) {
onSubmit(values)
}
}"
>
<div class="flex w-full flex-start gap-2">
<StepperItem
v-for="step in steps"
:key="step.step"
v-slot="{ state }"
class="relative flex w-full flex-col items-center justify-center"
:step="step.step"
>
<StepperSeparator
v-if="step.step !== steps[steps.length - 1].step"
class="absolute left-[calc(50%+20px)] right-[calc(-50%+10px)] top-5 block h-0.5 shrink-0 rounded-full bg-muted group-data-[state=completed]:bg-primary"
/>
<StepperTrigger as-child>
<Button
:variant="state === 'completed' || state === 'active' ? 'default' : 'outline'"
size="icon"
class="z-10 rounded-full shrink-0"
:class="[state === 'active' && 'ring-2 ring-ring ring-offset-2 ring-offset-background']"
:disabled="state !== 'completed' && !meta.valid"
>
<Check v-if="state === 'completed'" class="size-5" />
<Circle v-if="state === 'active'" />
<Dot v-if="state === 'inactive'" />
</Button>
</StepperTrigger>
<div class="mt-5 flex flex-col items-center text-center">
<StepperTitle
:class="[state === 'active' && 'text-primary']"
class="text-sm font-semibold transition lg:text-base"
>
{{ step.title }}
</StepperTitle>
<StepperDescription
:class="[state === 'active' && 'text-primary']"
class="sr-only text-xs text-muted-foreground transition md:not-sr-only lg:text-sm"
>
{{ step.description }}
</StepperDescription>
</div>
</StepperItem>
</div>
<div class="flex flex-col gap-4 mt-4">
<template v-if="stepIndex === 1">
<FormField v-slot="{ componentField }" name="fullName">
<FormItem>
<FormLabel>Full Name</FormLabel>
<FormControl>
<Input type="text" v-bind="componentField" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="email">
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email " v-bind="componentField" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</template>
<template v-if="stepIndex === 2">
<FormField v-slot="{ componentField }" name="password">
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" v-bind="componentField" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="confirmPassword">
<FormItem>
<FormLabel>Confirm Password</FormLabel>
<FormControl>
<Input type="password" v-bind="componentField" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</template>
<template v-if="stepIndex === 3">
<FormField v-slot="{ componentField }" name="favoriteDrink">
<FormItem>
<FormLabel>Drink</FormLabel>
<Select v-bind="componentField">
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a drink" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectGroup>
<SelectItem value="coffee">
Coffe
</SelectItem>
<SelectItem value="tea">
Tea
</SelectItem>
<SelectItem value="soda">
Soda
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
</FormField>
</template>
</div>
<div class="flex items-center justify-between mt-4">
<Button :disabled="isPrevDisabled" variant="outline" size="sm" @click="prevStep()">
Back
</Button>
<div class="flex items-center gap-3">
<Button v-if="stepIndex !== 3" :type="meta.valid ? 'button' : 'submit'" :disabled="isNextDisabled" size="sm" @click="meta.valid && nextStep()">
Next
</Button>
<Button
v-if="stepIndex === 3" size="sm" type="submit"
>
Submit
</Button>
</div>
</div>
</form>
</Stepper>
</Form>
</template>

View File

@ -0,0 +1,69 @@
<script setup lang="ts">
import { Button } from '@/lib/registry/default/ui/button'
import { Stepper, StepperDescription, StepperItem, StepperSeparator, StepperTitle, StepperTrigger } from '@/lib/registry/default/ui/stepper'
import { Check, Circle, Dot } from 'lucide-vue-next'
const steps = [
{
step: 1,
title: 'Your details',
description: 'Provide your name and email',
},
{
step: 2,
title: 'Company details',
description: 'A few details about your company',
},
{
step: 3,
title: 'Invite your team',
description: 'Start collaborating with your team',
},
]
</script>
<template>
<Stepper class="flex w-full items-start gap-2">
<StepperItem
v-for="step in steps"
:key="step.step"
v-slot="{ state }"
class="relative flex w-full flex-col items-center justify-center"
:step="step.step"
>
<StepperSeparator
v-if="step.step !== steps[steps.length - 1].step"
class="absolute left-[calc(50%+20px)] right-[calc(-50%+10px)] top-5 block h-0.5 shrink-0 rounded-full bg-muted group-data-[state=completed]:bg-primary"
/>
<StepperTrigger as-child>
<Button
:variant="state === 'completed' || state === 'active' ? 'default' : 'outline'"
size="icon"
class="z-10 rounded-full shrink-0"
:class="[state === 'active' && 'ring-2 ring-ring ring-offset-2 ring-offset-background']"
>
<Check v-if="state === 'completed'" class="size-5" />
<Circle v-if="state === 'active'" />
<Dot v-if="state === 'inactive'" />
</Button>
</StepperTrigger>
<div class="mt-5 flex flex-col items-center text-center">
<StepperTitle
:class="[state === 'active' && 'text-primary']"
class="text-sm font-semibold transition lg:text-base"
>
{{ step.title }}
</StepperTitle>
<StepperDescription
:class="[state === 'active' && 'text-primary']"
class="sr-only text-xs text-muted-foreground transition md:not-sr-only lg:text-sm"
>
{{ step.description }}
</StepperDescription>
</div>
</StepperItem>
</Stepper>
</template>

View File

@ -0,0 +1,71 @@
<script setup lang="ts">
import { Button } from '@/lib/registry/default/ui/button'
import { Stepper, StepperDescription, StepperItem, StepperSeparator, StepperTitle, StepperTrigger } from '@/lib/registry/default/ui/stepper'
import { Check, Circle, Dot } from 'lucide-vue-next'
const steps = [
{
step: 1,
title: 'Your details',
description:
'Provide your name and email address. We will use this information to create your account',
},
{
step: 2,
title: 'Company details',
description: 'A few details about your company will help us personalize your experience',
},
{
step: 3,
title: 'Invite your team',
description:
'Start collaborating with your team by inviting them to join your account. You can skip this step and invite them later',
},
]
</script>
<template>
<Stepper orientation="vertical" class="mx-auto flex w-full max-w-md flex-col justify-start gap-10">
<StepperItem
v-for="step in steps"
:key="step.step"
v-slot="{ state }"
class="relative flex w-full items-start gap-6"
:step="step.step"
>
<StepperSeparator
v-if="step.step !== steps[steps.length - 1].step"
class="absolute left-[18px] top-[38px] block h-[105%] w-0.5 shrink-0 rounded-full bg-muted group-data-[state=completed]:bg-primary"
/>
<StepperTrigger as-child>
<Button
:variant="state === 'completed' || state === 'active' ? 'default' : 'outline'"
size="icon"
class="z-10 rounded-full shrink-0"
:class="[state === 'active' && 'ring-2 ring-ring ring-offset-2 ring-offset-background']"
>
<Check v-if="state === 'completed'" class="size-5" />
<Circle v-if="state === 'active'" />
<Dot v-if="state === 'inactive'" />
</Button>
</StepperTrigger>
<div class="flex flex-col gap-1">
<StepperTitle
:class="[state === 'active' && 'text-primary']"
class="text-sm font-semibold transition lg:text-base"
>
{{ step.title }}
</StepperTitle>
<StepperDescription
:class="[state === 'active' && 'text-primary']"
class="sr-only text-xs text-muted-foreground transition md:not-sr-only lg:text-sm"
>
{{ step.description }}
</StepperDescription>
</div>
</StepperItem>
</Stepper>
</template>

View File

@ -0,0 +1,78 @@
<script setup lang="ts">
import { Button } from '@/lib/registry/default/ui/button'
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/lib/registry/default/ui/card'
import { Input } from '@/lib/registry/default/ui/input'
import { Label } from '@/lib/registry/default/ui/label'
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '@/lib/registry/default/ui/tabs'
</script>
<template>
<Tabs default-value="account" class="w-[400px]" orientation="vertical">
<TabsList class="grid w-full grid-cols-1">
<TabsTrigger value="account">
Accounts
</TabsTrigger>
<TabsTrigger value="password">
Password
</TabsTrigger>
</TabsList>
<TabsContent value="account">
<Card>
<CardHeader>
<CardTitle>Account</CardTitle>
<CardDescription>
Make changes to your account here. Click save when you're done.
</CardDescription>
</CardHeader>
<CardContent class="space-y-2">
<div class="space-y-1">
<Label for="name">Name</Label>
<Input id="name" default-value="Pedro Duarte" />
</div>
<div class="space-y-1">
<Label for="username">Username</Label>
<Input id="username" default-value="@peduarte" />
</div>
</CardContent>
<CardFooter>
<Button>Save changes</Button>
</CardFooter>
</Card>
</TabsContent>
<TabsContent value="password">
<Card>
<CardHeader>
<CardTitle>Password</CardTitle>
<CardDescription>
Change your password here. After saving, you'll be logged out.
</CardDescription>
</CardHeader>
<CardContent class="space-y-2">
<div class="space-y-1">
<Label for="current">Current password</Label>
<Input id="current" type="password" />
</div>
<div class="space-y-1">
<Label for="new">New password</Label>
<Input id="new" type="password" />
</div>
</CardContent>
<CardFooter>
<Button>Save password</Button>
</CardFooter>
</Card>
</TabsContent>
</Tabs>
</template>

View File

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue'
import { ComboboxAnchor, ComboboxInput, ComboboxPortal, ComboboxRoot } from 'radix-vue'
import { CommandEmpty, CommandGroup, CommandItem, CommandList } from '@/lib/registry/default/ui/command' import { CommandEmpty, CommandGroup, CommandItem, CommandList } from '@/lib/registry/default/ui/command'
import { TagsInput, TagsInputInput, TagsInputItem, TagsInputItemDelete, TagsInputItemText } from '@/lib/registry/default/ui/tags-input' import { TagsInput, TagsInputInput, TagsInputItem, TagsInputItemDelete, TagsInputItemText } from '@/lib/registry/default/ui/tags-input'
import { ComboboxAnchor, ComboboxContent, ComboboxInput, ComboboxPortal, ComboboxRoot } from 'radix-vue'
import { computed, ref } from 'vue'
const frameworks = [ const frameworks = [
{ value: 'next.js', label: 'Next.js' }, { value: 'next.js', label: 'Next.js' },
@ -28,7 +28,7 @@ const filteredFrameworks = computed(() => frameworks.filter(i => !modelValue.val
</TagsInputItem> </TagsInputItem>
</div> </div>
<ComboboxRoot v-model="modelValue" v-model:open="open" v-model:searchTerm="searchTerm" class="w-full"> <ComboboxRoot v-model="modelValue" v-model:open="open" v-model:search-term="searchTerm" class="w-full">
<ComboboxAnchor as-child> <ComboboxAnchor as-child>
<ComboboxInput placeholder="Framework..." as-child> <ComboboxInput placeholder="Framework..." as-child>
<TagsInputInput class="w-full px-3" :class="modelValue.length > 0 ? 'mt-2' : ''" @keydown.enter.prevent /> <TagsInputInput class="w-full px-3" :class="modelValue.length > 0 ? 'mt-2' : ''" @keydown.enter.prevent />
@ -36,29 +36,31 @@ const filteredFrameworks = computed(() => frameworks.filter(i => !modelValue.val
</ComboboxAnchor> </ComboboxAnchor>
<ComboboxPortal> <ComboboxPortal>
<CommandList <ComboboxContent>
position="popper" <CommandList
class="w-[--radix-popper-anchor-width] rounded-md mt-2 border bg-popover text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2" position="popper"
> class="w-[--radix-popper-anchor-width] rounded-md mt-2 border bg-popover text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
<CommandEmpty /> >
<CommandGroup> <CommandEmpty />
<CommandItem <CommandGroup>
v-for="framework in filteredFrameworks" :key="framework.value" :value="framework.label" <CommandItem
@select.prevent="(ev) => { v-for="framework in filteredFrameworks" :key="framework.value" :value="framework.label"
if (typeof ev.detail.value === 'string') { @select.prevent="(ev) => {
searchTerm = '' if (typeof ev.detail.value === 'string') {
modelValue.push(ev.detail.value) searchTerm = ''
} modelValue.push(ev.detail.value)
}
if (filteredFrameworks.length === 0) { if (filteredFrameworks.length === 0) {
open = false open = false
} }
}" }"
> >
{{ framework.label }} {{ framework.label }}
</CommandItem> </CommandItem>
</CommandGroup> </CommandGroup>
</CommandList> </CommandList>
</ComboboxContent>
</ComboboxPortal> </ComboboxPortal>
</ComboboxRoot> </ComboboxRoot>
</TagsInput> </TagsInput>

View File

@ -0,0 +1,62 @@
<script setup lang="ts">
import { Button } from '@/lib/registry/default/ui/button'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/lib/registry/default/ui/form'
import { TagsInput, TagsInputInput, TagsInputItem, TagsInputItemDelete, TagsInputItemText } from '@/lib/registry/default/ui/tags-input'
import { toast } from '@/lib/registry/default/ui/toast'
import { toTypedSchema } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
import { h } from 'vue'
import { z } from 'zod'
const formSchema = toTypedSchema(z.object({
fruits: z.array(z.string()).min(1).max(3),
}))
const { handleSubmit } = useForm({
validationSchema: formSchema,
initialValues: {
fruits: ['Apple', 'Banana'],
},
})
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="{ value }" name="fruits">
<FormItem>
<FormLabel>Fruits</FormLabel>
<FormControl>
<TagsInput :model-value="value">
<TagsInputItem v-for="item in value" :key="item" :value="item">
<TagsInputItemText />
<TagsInputItemDelete />
</TagsInputItem>
<TagsInputInput placeholder="Fruits..." />
</TagsInput>
</FormControl>
<FormDescription>
Select your favorite fruits.
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<Button type="submit">
Submit
</Button>
</form>
</template>

View File

@ -1,13 +1,13 @@
<script setup lang="ts" generic="T extends ZodObjectOrWrapped"> <script setup lang="ts" generic="T extends ZodObjectOrWrapped">
import { computed, toRefs } from 'vue'
import type { ZodAny, z } from 'zod'
import { toTypedSchema } from '@vee-validate/zod'
import type { FormContext, GenericObject } from 'vee-validate' import type { FormContext, GenericObject } from 'vee-validate'
import { type ZodObjectOrWrapped, getBaseSchema, getBaseType, getDefaultValueInZodStack, getObjectFormSchema } from './utils' import type { z, ZodAny } from 'zod'
import type { Config, ConfigItem, Dependency, Shape } from './interface' import type { Config, ConfigItem, Dependency, Shape } from './interface'
import { Form } from '@/lib/registry/default/ui/form'
import { toTypedSchema } from '@vee-validate/zod'
import { computed, toRefs } from 'vue'
import AutoFormField from './AutoFormField.vue' import AutoFormField from './AutoFormField.vue'
import { provideDependencies } from './dependencies' import { provideDependencies } from './dependencies'
import { Form } from '@/lib/registry/default/ui/form' import { getBaseSchema, getBaseType, getDefaultValueInZodStack, getObjectFormSchema, type ZodObjectOrWrapped } from './utils'
const props = defineProps<{ const props = defineProps<{
schema: T schema: T
@ -17,7 +17,7 @@ const props = defineProps<{
}>() }>()
const emits = defineEmits<{ const emits = defineEmits<{
submit: [event: GenericObject] submit: [event: z.infer<T>]
}>() }>()
const { dependencies } = toRefs(props) const { dependencies } = toRefs(props)

View File

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { type HTMLAttributes, computed } from 'vue'
import { CalendarCell, type CalendarCellProps, useForwardProps } from 'radix-vue'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { CalendarCell, type CalendarCellProps, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<CalendarCellProps & { class?: HTMLAttributes['class'] }>() const props = defineProps<CalendarCellProps & { class?: HTMLAttributes['class'] }>()
@ -16,7 +16,7 @@ const forwardedProps = useForwardProps(delegatedProps)
<template> <template>
<CalendarCell <CalendarCell
:class="cn('relative h-9 w-9 p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([data-selected])]:rounded-md [&:has([data-selected])]:bg-accent [&:has([data-selected][data-outside-month])]:bg-accent/50', props.class)" :class="cn('relative h-9 w-9 p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([data-selected])]:rounded-md [&:has([data-selected])]:bg-accent [&:has([data-selected][data-outside-view])]:bg-accent/50', props.class)"
v-bind="forwardedProps" v-bind="forwardedProps"
> >
<slot /> <slot />

View File

@ -1,8 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { type HTMLAttributes, computed } from 'vue'
import { CalendarCellTrigger, type CalendarCellTriggerProps, useForwardProps } from 'radix-vue'
import { buttonVariants } from '@/lib/registry/default/ui/button' import { buttonVariants } from '@/lib/registry/default/ui/button'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { CalendarCellTrigger, type CalendarCellTriggerProps, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<CalendarCellTriggerProps & { class?: HTMLAttributes['class'] }>() const props = defineProps<CalendarCellTriggerProps & { class?: HTMLAttributes['class'] }>()
@ -28,7 +28,7 @@ const forwardedProps = useForwardProps(delegatedProps)
// Unavailable // Unavailable
'data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through', 'data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through',
// Outside months // Outside months
'data-[outside-month]:pointer-events-none data-[outside-month]:text-muted-foreground data-[outside-month]:opacity-50 [&[data-outside-month][data-selected]]:bg-accent/50 [&[data-outside-month][data-selected]]:text-muted-foreground [&[data-outside-month][data-selected]]:opacity-30', 'data-[outside-view]:text-muted-foreground data-[outside-view]:opacity-50 [&[data-outside-view][data-selected]]:bg-accent/50 [&[data-outside-view][data-selected]]:text-muted-foreground [&[data-outside-view][data-selected]]:opacity-30',
props.class, props.class,
)" )"
v-bind="forwardedProps" v-bind="forwardedProps"

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useProvideCarousel } from './useCarousel'
import type { CarouselEmits, CarouselProps, WithClassAsProps } from './interface' import type { CarouselEmits, CarouselProps, WithClassAsProps } from './interface'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useProvideCarousel } from './useCarousel'
const props = withDefaults(defineProps<CarouselProps & WithClassAsProps>(), { const props = withDefaults(defineProps<CarouselProps & WithClassAsProps>(), {
orientation: 'horizontal', orientation: 'horizontal',
@ -9,9 +9,17 @@ const props = withDefaults(defineProps<CarouselProps & WithClassAsProps>(), {
const emits = defineEmits<CarouselEmits>() const emits = defineEmits<CarouselEmits>()
const carouselArgs = useProvideCarousel(props, emits) const { canScrollNext, canScrollPrev, carouselApi, carouselRef, orientation, scrollNext, scrollPrev } = useProvideCarousel(props, emits)
defineExpose(carouselArgs) defineExpose({
canScrollNext,
canScrollPrev,
carouselApi,
carouselRef,
orientation,
scrollNext,
scrollPrev,
})
function onKeyDown(event: KeyboardEvent) { function onKeyDown(event: KeyboardEvent) {
const prevKey = props.orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft' const prevKey = props.orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft'
@ -19,14 +27,14 @@ function onKeyDown(event: KeyboardEvent) {
if (event.key === prevKey) { if (event.key === prevKey) {
event.preventDefault() event.preventDefault()
carouselArgs.scrollPrev() scrollPrev()
return return
} }
if (event.key === nextKey) { if (event.key === nextKey) {
event.preventDefault() event.preventDefault()
carouselArgs.scrollNext() scrollNext()
} }
} }
</script> </script>
@ -39,6 +47,6 @@ function onKeyDown(event: KeyboardEvent) {
tabindex="0" tabindex="0"
@keydown="onKeyDown" @keydown="onKeyDown"
> >
<slot v-bind="carouselArgs" /> <slot :can-scroll-next :can-scroll-prev :carousel-api :carousel-ref :orientation :scroll-next :scroll-prev />
</div> </div>
</template> </template>

View File

@ -1,12 +1,13 @@
<script setup lang="ts" generic="T extends Record<string, any>"> <script setup lang="ts" generic="T extends Record<string, any>">
import { type BulletLegendItemInterface, CurveType } from '@unovis/ts'
import { VisArea, VisAxis, VisLine, VisXYContainer } from '@unovis/vue'
import { Area, Axis, Line } from '@unovis/ts'
import { type Component, computed, ref } from 'vue'
import { useMounted } from '@vueuse/core'
import type { BaseChartProps } from '.' import type { BaseChartProps } from '.'
import { ChartCrosshair, ChartLegend, defaultColors } from '@/lib/registry/default/ui/chart' import { ChartCrosshair, ChartLegend, defaultColors } from '@/lib/registry/default/ui/chart'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { type BulletLegendItemInterface, CurveType } from '@unovis/ts'
import { Area, Axis, Line } from '@unovis/ts'
import { VisArea, VisAxis, VisLine, VisXYContainer } from '@unovis/vue'
import { useMounted } from '@vueuse/core'
import { useId } from 'radix-vue'
import { type Component, computed, ref } from 'vue'
const props = withDefaults(defineProps<BaseChartProps<T> & { const props = withDefaults(defineProps<BaseChartProps<T> & {
/** /**
@ -41,6 +42,8 @@ const emits = defineEmits<{
type KeyOfT = Extract<keyof T, string> type KeyOfT = Extract<keyof T, string>
type Data = typeof props.data[number] type Data = typeof props.data[number]
const chartRef = useId()
const index = computed(() => props.index as KeyOfT) const index = computed(() => props.index as KeyOfT)
const colors = computed(() => props.colors?.length ? props.colors : defaultColors(props.categories.length)) const colors = computed(() => props.colors?.length ? props.colors : defaultColors(props.categories.length))
@ -64,7 +67,7 @@ function handleLegendItemClick(d: BulletLegendItemInterface, i: number) {
<VisXYContainer :style="{ height: isMounted ? '100%' : 'auto' }" :margin="{ left: 20, right: 20 }" :data="data"> <VisXYContainer :style="{ height: isMounted ? '100%' : 'auto' }" :margin="{ left: 20, right: 20 }" :data="data">
<svg width="0" height="0"> <svg width="0" height="0">
<defs> <defs>
<linearGradient v-for="(color, i) in colors" :id="`color-${i}`" :key="i" x1="0" y1="0" x2="0" y2="1"> <linearGradient v-for="(color, i) in colors" :id="`${chartRef}-color-${i}`" :key="i" x1="0" y1="0" x2="0" y2="1">
<template v-if="showGradiant"> <template v-if="showGradiant">
<stop offset="5%" :stop-color="color" stop-opacity="0.4" /> <stop offset="5%" :stop-color="color" stop-opacity="0.4" />
<stop offset="95%" :stop-color="color" stop-opacity="0" /> <stop offset="95%" :stop-color="color" stop-opacity="0" />
@ -86,7 +89,7 @@ function handleLegendItemClick(d: BulletLegendItemInterface, i: number) {
:curve-type="curveType" :curve-type="curveType"
:attributes="{ :attributes="{
[Area.selectors.area]: { [Area.selectors.area]: {
fill: `url(#color-${i})`, fill: `url(#${chartRef}-color-${i})`,
}, },
}" }"
:opacity="legendItems.find(item => item.name === category)?.inactive ? filterOpacity : 1" :opacity="legendItems.find(item => item.name === category)?.inactive ? filterOpacity : 1"

View File

@ -6,19 +6,19 @@ export function useFormField() {
const fieldContext = inject(FieldContextKey) const fieldContext = inject(FieldContextKey)
const fieldItemContext = inject(FORM_ITEM_INJECTION_KEY) const fieldItemContext = inject(FORM_ITEM_INJECTION_KEY)
const fieldState = {
valid: useIsFieldValid(),
isDirty: useIsFieldDirty(),
isTouched: useIsFieldTouched(),
error: useFieldError(),
}
if (!fieldContext) if (!fieldContext)
throw new Error('useFormField should be used within <FormField>') throw new Error('useFormField should be used within <FormField>')
const { name } = fieldContext const { name } = fieldContext
const id = fieldItemContext const id = fieldItemContext
const fieldState = {
valid: useIsFieldValid(name),
isDirty: useIsFieldDirty(name),
isTouched: useIsFieldTouched(name),
error: useFieldError(name),
}
return { return {
id, id,
name, name,

View File

@ -8,7 +8,7 @@ const props = defineProps<{
</script> </script>
<template> <template>
<div :class="cn('relative', props.class)"> <div :class="cn('relative [&>[data-slot=input]]:has-[[data-slot=increment]]:pr-5 [&>[data-slot=input]]:has-[[data-slot=decrement]]:pl-5', props.class)">
<slot /> <slot />
</div> </div>
</template> </template>

View File

@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { NumberFieldDecrementProps } from 'radix-vue' import type { NumberFieldDecrementProps } from 'radix-vue'
import { NumberFieldDecrement, useForwardProps } from 'radix-vue'
import { type HTMLAttributes, computed } from 'vue'
import { Minus } from 'lucide-vue-next'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Minus } from 'lucide-vue-next'
import { NumberFieldDecrement, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<NumberFieldDecrementProps & { class?: HTMLAttributes['class'] }>() const props = defineProps<NumberFieldDecrementProps & { class?: HTMLAttributes['class'] }>()
@ -17,7 +17,7 @@ const forwarded = useForwardProps(delegatedProps)
</script> </script>
<template> <template>
<NumberFieldDecrement v-bind="forwarded" :class="cn('absolute top-1/2 -translate-y-1/2 left-0 p-3 disabled:cursor-not-allowed disabled:opacity-20', props.class)"> <NumberFieldDecrement data-slot="decrement" v-bind="forwarded" :class="cn('absolute top-1/2 -translate-y-1/2 left-0 p-3 disabled:cursor-not-allowed disabled:opacity-20', props.class)">
<slot> <slot>
<Minus class="h-4 w-4" /> <Minus class="h-4 w-4" />
</slot> </slot>

View File

@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { NumberFieldIncrementProps } from 'radix-vue' import type { NumberFieldIncrementProps } from 'radix-vue'
import { NumberFieldIncrement, useForwardProps } from 'radix-vue'
import { type HTMLAttributes, computed } from 'vue'
import { Plus } from 'lucide-vue-next'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Plus } from 'lucide-vue-next'
import { NumberFieldIncrement, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<NumberFieldIncrementProps & { class?: HTMLAttributes['class'] }>() const props = defineProps<NumberFieldIncrementProps & { class?: HTMLAttributes['class'] }>()
@ -17,7 +17,7 @@ const forwarded = useForwardProps(delegatedProps)
</script> </script>
<template> <template>
<NumberFieldIncrement v-bind="forwarded" :class="cn('absolute top-1/2 -translate-y-1/2 right-0 disabled:cursor-not-allowed disabled:opacity-20 p-3', props.class)"> <NumberFieldIncrement data-slot="increment" v-bind="forwarded" :class="cn('absolute top-1/2 -translate-y-1/2 right-0 disabled:cursor-not-allowed disabled:opacity-20 p-3', props.class)">
<slot> <slot>
<Plus class="h-4 w-4" /> <Plus class="h-4 w-4" />
</slot> </slot>

View File

@ -1,8 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { NumberFieldInput } from 'radix-vue' import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { NumberFieldInput } from 'radix-vue'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script> </script>
<template> <template>
<NumberFieldInput :class="cn('flex h-10 w-full rounded-md border border-input bg-background px-10 py-2 text-sm text-center ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50')" /> <NumberFieldInput
data-slot="input"
:class="cn('flex h-10 w-full rounded-md border border-input bg-background py-2 text-sm text-center ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', props.class)"
/>
</template> </template>

View File

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { type HTMLAttributes, computed } from 'vue'
import { RangeCalendarCell, type RangeCalendarCellProps, useForwardProps } from 'radix-vue'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { RangeCalendarCell, type RangeCalendarCellProps, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<RangeCalendarCellProps & { class?: HTMLAttributes['class'] }>() const props = defineProps<RangeCalendarCellProps & { class?: HTMLAttributes['class'] }>()
@ -16,7 +16,7 @@ const forwardedProps = useForwardProps(delegatedProps)
<template> <template>
<RangeCalendarCell <RangeCalendarCell
:class="cn('relative h-9 w-9 p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([data-selected])]:bg-accent first:[&:has([data-selected])]:rounded-l-md last:[&:has([data-selected])]:rounded-r-md [&:has([data-selected][data-outside-month])]:bg-accent/50 [&:has([data-selected][data-selection-end])]:rounded-r-md [&:has([data-selected][data-selection-start])]:rounded-l-md', props.class)" :class="cn('relative h-9 w-9 p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([data-selected])]:bg-accent first:[&:has([data-selected])]:rounded-l-md last:[&:has([data-selected])]:rounded-r-md [&:has([data-selected][data-outside-view])]:bg-accent/50 [&:has([data-selected][data-selection-end])]:rounded-r-md [&:has([data-selected][data-selection-start])]:rounded-l-md', props.class)"
v-bind="forwardedProps" v-bind="forwardedProps"
> >
<slot /> <slot />

View File

@ -1,8 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { type HTMLAttributes, computed } from 'vue'
import { RangeCalendarCellTrigger, type RangeCalendarCellTriggerProps, useForwardProps } from 'radix-vue'
import { buttonVariants } from '@/lib/registry/default/ui/button' import { buttonVariants } from '@/lib/registry/default/ui/button'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { RangeCalendarCellTrigger, type RangeCalendarCellTriggerProps, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<RangeCalendarCellTriggerProps & { class?: HTMLAttributes['class'] }>() const props = defineProps<RangeCalendarCellTriggerProps & { class?: HTMLAttributes['class'] }>()
@ -26,7 +26,7 @@ const forwardedProps = useForwardProps(delegatedProps)
// Selection End // Selection End
'data-[selection-end]:bg-primary data-[selection-end]:text-primary-foreground data-[selection-end]:hover:bg-primary data-[selection-end]:hover:text-primary-foreground data-[selection-end]:focus:bg-primary data-[selection-end]:focus:text-primary-foreground', 'data-[selection-end]:bg-primary data-[selection-end]:text-primary-foreground data-[selection-end]:hover:bg-primary data-[selection-end]:hover:text-primary-foreground data-[selection-end]:focus:bg-primary data-[selection-end]:focus:text-primary-foreground',
// Outside months // Outside months
'data-[outside-month]:pointer-events-none data-[outside-month]:text-muted-foreground data-[outside-month]:opacity-50 [&[data-outside-month][data-selected]]:bg-accent/50 [&[data-outside-month][data-selected]]:text-muted-foreground [&[data-outside-month][data-selected]]:opacity-30', 'data-[outside-view]:text-muted-foreground data-[outside-view]:opacity-50 [&[data-outside-view][data-selected]]:bg-accent/50 [&[data-outside-view][data-selected]]:text-muted-foreground [&[data-outside-view][data-selected]]:opacity-30',
// Disabled // Disabled
'data-[disabled]:text-muted-foreground data-[disabled]:opacity-50', 'data-[disabled]:text-muted-foreground data-[disabled]:opacity-50',
// Unavailable // Unavailable

View File

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { type HTMLAttributes, computed } from 'vue'
import { RangeCalendarHeadCell, type RangeCalendarHeadCellProps, useForwardProps } from 'radix-vue'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { RangeCalendarHeadCell, type RangeCalendarHeadCellProps, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<RangeCalendarHeadCellProps & { class?: HTMLAttributes['class'] }>() const props = defineProps<RangeCalendarHeadCellProps & { class?: HTMLAttributes['class'] }>()
@ -15,7 +15,7 @@ const forwardedProps = useForwardProps(delegatedProps)
</script> </script>
<template> <template>
<RangeCalendarHeadCell :class="cn('w-8 rounded-md text-[0.8rem] font-normal text-muted-foreground', props.class)" v-bind="forwardedProps"> <RangeCalendarHeadCell :class="cn('w-9 rounded-md text-[0.8rem] font-normal text-muted-foreground', props.class)" v-bind="forwardedProps">
<slot /> <slot />
</RangeCalendarHeadCell> </RangeCalendarHeadCell>
</template> </template>

View File

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import { SelectIcon, SelectTrigger, type SelectTriggerProps, useForwardProps } from 'radix-vue'
import { ChevronDown } from 'lucide-vue-next'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { ChevronDown } from 'lucide-vue-next'
import { SelectIcon, SelectTrigger, type SelectTriggerProps, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<SelectTriggerProps & { class?: HTMLAttributes['class'] }>() const props = defineProps<SelectTriggerProps & { class?: HTMLAttributes['class'] }>()
@ -19,13 +19,13 @@ const forwardedProps = useForwardProps(delegatedProps)
<SelectTrigger <SelectTrigger
v-bind="forwardedProps" v-bind="forwardedProps"
:class="cn( :class="cn(
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1', 'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:truncate text-start',
props.class, props.class,
)" )"
> >
<slot /> <slot />
<SelectIcon as-child> <SelectIcon as-child>
<ChevronDown class="w-4 h-4 opacity-50" /> <ChevronDown class="w-4 h-4 opacity-50 shrink-0" />
</SelectIcon> </SelectIcon>
</SelectTrigger> </SelectTrigger>
</template> </template>

View File

@ -1,9 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import { Separator, type SeparatorProps } from 'radix-vue'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Separator, type SeparatorProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<SeparatorProps & { class?: HTMLAttributes['class'] }>() const props = defineProps<
SeparatorProps & { class?: HTMLAttributes['class'], label?: string }
>()
const delegatedProps = computed(() => { const delegatedProps = computed(() => {
const { class: _, ...delegated } = props const { class: _, ...delegated } = props
@ -15,6 +17,19 @@ const delegatedProps = computed(() => {
<template> <template>
<Separator <Separator
v-bind="delegatedProps" v-bind="delegatedProps"
:class="cn('shrink-0 bg-border', props.orientation === 'vertical' ? 'w-px h-full' : 'h-px w-full', props.class)" :class="
/> cn(
'shrink-0 bg-border relative',
props.orientation === 'vertical' ? 'w-px h-full' : 'h-px w-full',
props.class,
)
"
>
<span
v-if="props.label"
:class="cn('text-xs text-muted-foreground bg-background absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex justify-center items-center',
props.orientation === 'vertical' ? 'w-[1px] px-1 py-2' : 'h-[1px] py-1 px-2',
)"
>{{ props.label }}</span>
</Separator>
</template> </template>

View File

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import type { SliderRootEmits, SliderRootProps } from 'radix-vue' import type { SliderRootEmits, SliderRootProps } from 'radix-vue'
import { SliderRange, SliderRoot, SliderThumb, SliderTrack, useForwardPropsEmits } from 'radix-vue'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { SliderRange, SliderRoot, SliderThumb, SliderTrack, useForwardPropsEmits } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<SliderRootProps & { class?: HTMLAttributes['class'] }>() const props = defineProps<SliderRootProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<SliderRootEmits>() const emits = defineEmits<SliderRootEmits>()
@ -19,13 +19,13 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<template> <template>
<SliderRoot <SliderRoot
:class="cn( :class="cn(
'relative flex w-full touch-none select-none items-center', 'relative flex w-full touch-none select-none items-center data-[orientation=vertical]:flex-col data-[orientation=vertical]:w-2 data-[orientation=vertical]:h-full',
props.class, props.class,
)" )"
v-bind="forwarded" v-bind="forwarded"
> >
<SliderTrack class="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary"> <SliderTrack class="relative h-2 w-full data-[orientation=vertical]:w-2 grow overflow-hidden rounded-full bg-secondary">
<SliderRange class="absolute h-full bg-primary" /> <SliderRange class="absolute h-full data-[orientation=vertical]:w-full bg-primary" />
</SliderTrack> </SliderTrack>
<SliderThumb <SliderThumb
v-for="(_, key) in modelValue" v-for="(_, key) in modelValue"

View File

@ -0,0 +1,31 @@
<script lang="ts" setup>
import type { StepperRootEmits, StepperRootProps } from 'radix-vue'
import { cn } from '@/lib/utils'
import { StepperRoot, useForwardPropsEmits } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<StepperRootProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<StepperRootEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<StepperRoot
v-slot="slotProps"
:class="cn(
'flex gap-2',
props.class,
)"
v-bind="forwarded"
>
<slot v-bind="slotProps" />
</StepperRoot>
</template>

View File

@ -0,0 +1,23 @@
<script lang="ts" setup>
import type { StepperDescriptionProps } from 'radix-vue'
import { cn } from '@/lib/utils'
import { StepperDescription, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<StepperDescriptionProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardProps(delegatedProps)
</script>
<template>
<StepperDescription v-slot="slotProps" v-bind="forwarded" :class="cn('text-xs text-muted-foreground', props.class)">
<slot v-bind="slotProps" />
</StepperDescription>
</template>

View File

@ -0,0 +1,35 @@
<script lang="ts" setup>
import type { StepperIndicatorProps } from 'radix-vue'
import { cn } from '@/lib/utils'
import { StepperIndicator, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<StepperIndicatorProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardProps(delegatedProps)
</script>
<template>
<StepperIndicator
v-bind="forwarded"
:class="cn(
'inline-flex items-center justify-center rounded-full text-muted-foreground/50 w-10 h-10',
// Disabled
'group-data-[disabled]:text-muted-foreground group-data-[disabled]:opacity-50',
// Active
'group-data-[state=active]:bg-primary group-data-[state=active]:text-primary-foreground',
// Completed
'group-data-[state=completed]:bg-accent group-data-[state=completed]:text-accent-foreground',
props.class,
)"
>
<slot />
</StepperIndicator>
</template>

View File

@ -0,0 +1,27 @@
<script lang="ts" setup>
import type { StepperItemProps } from 'radix-vue'
import { cn } from '@/lib/utils'
import { StepperItem, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<StepperItemProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardProps(delegatedProps)
</script>
<template>
<StepperItem
v-slot="slotProps"
v-bind="forwarded"
:class="cn('flex items-center gap-2 group data-[disabled]:pointer-events-none', props.class)"
>
<slot v-bind="slotProps" />
</StepperItem>
</template>

View File

@ -0,0 +1,31 @@
<script lang="ts" setup>
import type { StepperSeparatorProps } from 'radix-vue'
import { cn } from '@/lib/utils'
import { StepperSeparator, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<StepperSeparatorProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardProps(delegatedProps)
</script>
<template>
<StepperSeparator
v-bind="forwarded"
:class="cn(
'bg-muted',
// Disabled
'group-data-[disabled]:bg-muted group-data-[disabled]:opacity-50',
// Completed
'group-data-[state=completed]:bg-accent-foreground',
props.class,
)"
/>
</template>

View File

@ -0,0 +1,23 @@
<script lang="ts" setup>
import type { StepperTitleProps } from 'radix-vue'
import { cn } from '@/lib/utils'
import { StepperTitle, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<StepperTitleProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardProps(delegatedProps)
</script>
<template>
<StepperTitle v-bind="forwarded" :class="cn('text-md font-semibold whitespace-nowrap', props.class)">
<slot />
</StepperTitle>
</template>

View File

@ -0,0 +1,26 @@
<script lang="ts" setup>
import type { StepperTriggerProps } from 'radix-vue'
import { cn } from '@/lib/utils'
import { StepperTrigger, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<StepperTriggerProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardProps(delegatedProps)
</script>
<template>
<StepperTrigger
v-bind="forwarded"
:class="cn('p-2 flex flex-col items-center text-center gap-2 rounded-md', props.class)"
>
<slot />
</StepperTrigger>
</template>

View File

@ -0,0 +1,7 @@
export { default as Stepper } from './Stepper.vue'
export { default as StepperDescription } from './StepperDescription.vue'
export { default as StepperIndicator } from './StepperIndicator.vue'
export { default as StepperItem } from './StepperItem.vue'
export { default as StepperSeparator } from './StepperSeparator.vue'
export { default as StepperTitle } from './StepperTitle.vue'
export { default as StepperTrigger } from './StepperTrigger.vue'

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import { TabsList, type TabsListProps } from 'radix-vue'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { TabsList, type TabsListProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<TabsListProps & { class?: HTMLAttributes['class'] }>() const props = defineProps<TabsListProps & { class?: HTMLAttributes['class'] }>()
@ -16,7 +16,7 @@ const delegatedProps = computed(() => {
<TabsList <TabsList
v-bind="delegatedProps" v-bind="delegatedProps"
:class="cn( :class="cn(
'inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground', 'inline-flex items-center justify-center rounded-md bg-muted p-1 text-muted-foreground',
props.class, props.class,
)" )"
> >

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import { TabsTrigger, type TabsTriggerProps, useForwardProps } from 'radix-vue'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { TabsTrigger, type TabsTriggerProps, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<TabsTriggerProps & { class?: HTMLAttributes['class'] }>() const props = defineProps<TabsTriggerProps & { class?: HTMLAttributes['class'] }>()
@ -22,6 +22,8 @@ const forwardedProps = useForwardProps(delegatedProps)
props.class, props.class,
)" )"
> >
<slot /> <span class="truncate">
<slot />
</span>
</TabsTrigger> </TabsTrigger>
</template> </template>

View File

@ -1,9 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { type HTMLAttributes, type Ref, computed } from 'vue'
import { CalendarRoot, type CalendarRootEmits, type CalendarRootProps, useDateFormatter, useForwardPropsEmits } from 'radix-vue'
import { createDecade, createYear, toDate } from 'radix-vue/date'
import { type DateValue, getLocalTimeZone, today } from '@internationalized/date'
import { useVModel } from '@vueuse/core'
import { CalendarCell, CalendarCellTrigger, CalendarGrid, CalendarGridBody, CalendarGridHead, CalendarGridRow, CalendarHeadCell, CalendarHeader, CalendarHeading } from '@/lib/registry/new-york/ui/calendar' import { CalendarCell, CalendarCellTrigger, CalendarGrid, CalendarGridBody, CalendarGridHead, CalendarGridRow, CalendarHeadCell, CalendarHeader, CalendarHeading } from '@/lib/registry/new-york/ui/calendar'
import { import {
Select, Select,
@ -13,6 +8,11 @@ import {
SelectValue, SelectValue,
} from '@/lib/registry/new-york/ui/select' } from '@/lib/registry/new-york/ui/select'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { type DateValue, getLocalTimeZone, today } from '@internationalized/date'
import { useVModel } from '@vueuse/core'
import { CalendarRoot, type CalendarRootEmits, type CalendarRootProps, useDateFormatter, useForwardPropsEmits } from 'radix-vue'
import { createDecade, createYear, toDate } from 'radix-vue/date'
import { computed, type HTMLAttributes, type Ref } from 'vue'
const props = withDefaults(defineProps<CalendarRootProps & { class?: HTMLAttributes['class'] }>(), { const props = withDefaults(defineProps<CalendarRootProps & { class?: HTMLAttributes['class'] }>(), {
modelValue: undefined, modelValue: undefined,
@ -72,7 +72,7 @@ const formatter = useDateFormatter('en')
</Select> </Select>
<Select <Select
:default-value="props.placeholder.year.toString()" :default-value="placeholder.year.toString()"
@update:model-value="(v) => { @update:model-value="(v) => {
if (!v || !placeholder) return; if (!v || !placeholder) return;
if (Number(v) === placeholder?.year) return; if (Number(v) === placeholder?.year) return;

View File

@ -1,25 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import type { import type {
ColumnFiltersState, ColumnFiltersState,
ExpandedState,
SortingState, SortingState,
VisibilityState, VisibilityState,
} from '@tanstack/vue-table' } from '@tanstack/vue-table'
import {
FlexRender,
createColumnHelper,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useVueTable,
} from '@tanstack/vue-table'
import { CaretSortIcon, ChevronDownIcon } from '@radix-icons/vue'
import { h, ref } from 'vue'
import DropdownAction from './DataTableDemoColumn.vue'
import { Button } from '@/lib/registry/new-york/ui/button' import { Button } from '@/lib/registry/new-york/ui/button'
import { Checkbox } from '@/lib/registry/new-york/ui/checkbox' import { Checkbox } from '@/lib/registry/new-york/ui/checkbox'
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuCheckboxItem, DropdownMenuCheckboxItem,
@ -36,6 +24,19 @@ import {
TableRow, TableRow,
} from '@/lib/registry/new-york/ui/table' } from '@/lib/registry/new-york/ui/table'
import { cn, valueUpdater } from '@/lib/utils' import { cn, valueUpdater } from '@/lib/utils'
import { CaretSortIcon, ChevronDownIcon } from '@radix-icons/vue'
import {
createColumnHelper,
FlexRender,
getCoreRowModel,
getExpandedRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useVueTable,
} from '@tanstack/vue-table'
import { h, ref } from 'vue'
import DropdownAction from './DataTableDemoColumn.vue'
export interface Payment { export interface Payment {
id: string id: string
@ -133,6 +134,7 @@ const columns = [
return h('div', { class: 'relative' }, h(DropdownAction, { return h('div', { class: 'relative' }, h(DropdownAction, {
payment, payment,
onExpand: row.toggleExpanded,
})) }))
}, },
}), }),
@ -142,6 +144,7 @@ const sorting = ref<SortingState>([])
const columnFilters = ref<ColumnFiltersState>([]) const columnFilters = ref<ColumnFiltersState>([])
const columnVisibility = ref<VisibilityState>({}) const columnVisibility = ref<VisibilityState>({})
const rowSelection = ref({}) const rowSelection = ref({})
const expanded = ref<ExpandedState>({})
const table = useVueTable({ const table = useVueTable({
data, data,
@ -150,15 +153,18 @@ const table = useVueTable({
getPaginationRowModel: getPaginationRowModel(), getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(), getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(), getFilteredRowModel: getFilteredRowModel(),
getExpandedRowModel: getExpandedRowModel(),
onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting), onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting),
onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters), onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters),
onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility), onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility),
onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelection), onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelection),
onExpandedChange: updaterOrValue => valueUpdater(updaterOrValue, expanded),
state: { state: {
get sorting() { return sorting.value }, get sorting() { return sorting.value },
get columnFilters() { return columnFilters.value }, get columnFilters() { return columnFilters.value },
get columnVisibility() { return columnVisibility.value }, get columnVisibility() { return columnVisibility.value },
get rowSelection() { return rowSelection.value }, get rowSelection() { return rowSelection.value },
get expanded() { return expanded.value },
columnPinning: { columnPinning: {
left: ['status'], left: ['status'],
}, },
@ -213,21 +219,24 @@ const table = useVueTable({
</TableHeader> </TableHeader>
<TableBody> <TableBody>
<template v-if="table.getRowModel().rows?.length"> <template v-if="table.getRowModel().rows?.length">
<TableRow <template v-for="row in table.getRowModel().rows" :key="row.id">
v-for="row in table.getRowModel().rows" <TableRow :data-state="row.getIsSelected() && 'selected'">
:key="row.id" <TableCell
:data-state="row.getIsSelected() && 'selected'" v-for="cell in row.getVisibleCells()" :key="cell.id" :data-pinned="cell.column.getIsPinned()"
> :class="cn(
<TableCell { 'sticky bg-background/95': cell.column.getIsPinned() },
v-for="cell in row.getVisibleCells()" :key="cell.id" :data-pinned="cell.column.getIsPinned()" cell.column.getIsPinned() === 'left' ? 'left-0' : 'right-0',
:class="cn( )"
{ 'sticky bg-background/95': cell.column.getIsPinned() }, >
cell.column.getIsPinned() === 'left' ? 'left-0' : 'right-0', <FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
)" </TableCell>
> </TableRow>
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" /> <TableRow v-if="row.getIsExpanded()">
</TableCell> <TableCell :colspan="row.getAllCells().length">
</TableRow> {{ JSON.stringify(row.original) }}
</TableCell>
</TableRow>
</template>
</template> </template>
<TableRow v-else> <TableRow v-else>

View File

@ -2,21 +2,10 @@
import type { import type {
ColumnDef, ColumnDef,
ColumnFiltersState, ColumnFiltersState,
ExpandedState,
SortingState, SortingState,
VisibilityState, VisibilityState,
} from '@tanstack/vue-table' } from '@tanstack/vue-table'
import {
FlexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useVueTable,
} from '@tanstack/vue-table'
import { h, ref } from 'vue'
import { CaretSortIcon, ChevronDownIcon } from '@radix-icons/vue'
import DropdownAction from './DataTableDemoColumn.vue'
import { Button } from '@/lib/registry/new-york/ui/button' import { Button } from '@/lib/registry/new-york/ui/button'
import { Checkbox } from '@/lib/registry/new-york/ui/checkbox' import { Checkbox } from '@/lib/registry/new-york/ui/checkbox'
import { import {
@ -26,6 +15,7 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/lib/registry/new-york/ui/dropdown-menu' } from '@/lib/registry/new-york/ui/dropdown-menu'
import { Input } from '@/lib/registry/new-york/ui/input' import { Input } from '@/lib/registry/new-york/ui/input'
import { import {
Table, Table,
TableBody, TableBody,
@ -35,6 +25,18 @@ import {
TableRow, TableRow,
} from '@/lib/registry/new-york/ui/table' } from '@/lib/registry/new-york/ui/table'
import { valueUpdater } from '@/lib/utils' import { valueUpdater } from '@/lib/utils'
import { CaretSortIcon, ChevronDownIcon } from '@radix-icons/vue'
import {
FlexRender,
getCoreRowModel,
getExpandedRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useVueTable,
} from '@tanstack/vue-table'
import { h, ref } from 'vue'
import DropdownAction from './DataTableDemoColumn.vue'
export interface Payment { export interface Payment {
id: string id: string
@ -103,7 +105,7 @@ const columns: ColumnDef<Payment>[] = [
return h(Button, { return h(Button, {
variant: 'ghost', variant: 'ghost',
onClick: () => column.toggleSorting(column.getIsSorted() === 'asc'), onClick: () => column.toggleSorting(column.getIsSorted() === 'asc'),
}, ['Email', h(CaretSortIcon, { class: 'ml-2 h-4 w-4' })]) }, () => ['Email', h(CaretSortIcon, { class: 'ml-2 h-4 w-4' })])
}, },
cell: ({ row }) => h('div', { class: 'lowercase' }, row.getValue('email')), cell: ({ row }) => h('div', { class: 'lowercase' }, row.getValue('email')),
}, },
@ -130,6 +132,7 @@ const columns: ColumnDef<Payment>[] = [
return h(DropdownAction, { return h(DropdownAction, {
payment, payment,
onExpand: row.toggleExpanded,
}) })
}, },
}, },
@ -139,6 +142,7 @@ const sorting = ref<SortingState>([])
const columnFilters = ref<ColumnFiltersState>([]) const columnFilters = ref<ColumnFiltersState>([])
const columnVisibility = ref<VisibilityState>({}) const columnVisibility = ref<VisibilityState>({})
const rowSelection = ref({}) const rowSelection = ref({})
const expanded = ref<ExpandedState>({})
const table = useVueTable({ const table = useVueTable({
data, data,
@ -147,15 +151,18 @@ const table = useVueTable({
getPaginationRowModel: getPaginationRowModel(), getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(), getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(), getFilteredRowModel: getFilteredRowModel(),
getExpandedRowModel: getExpandedRowModel(),
onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting), onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting),
onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters), onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters),
onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility), onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility),
onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelection), onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelection),
onExpandedChange: updaterOrValue => valueUpdater(updaterOrValue, expanded),
state: { state: {
get sorting() { return sorting.value }, get sorting() { return sorting.value },
get columnFilters() { return columnFilters.value }, get columnFilters() { return columnFilters.value },
get columnVisibility() { return columnVisibility.value }, get columnVisibility() { return columnVisibility.value },
get rowSelection() { return rowSelection.value }, get rowSelection() { return rowSelection.value },
get expanded() { return expanded.value },
}, },
}) })
</script> </script>
@ -201,15 +208,18 @@ const table = useVueTable({
</TableHeader> </TableHeader>
<TableBody> <TableBody>
<template v-if="table.getRowModel().rows?.length"> <template v-if="table.getRowModel().rows?.length">
<TableRow <template v-for="row in table.getRowModel().rows" :key="row.id">
v-for="row in table.getRowModel().rows" <TableRow :data-state="row.getIsSelected() && 'selected'">
:key="row.id" <TableCell v-for="cell in row.getVisibleCells()" :key="cell.id">
:data-state="row.getIsSelected() && 'selected'" <FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
> </TableCell>
<TableCell v-for="cell in row.getVisibleCells()" :key="cell.id"> </TableRow>
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" /> <TableRow v-if="row.getIsExpanded()">
</TableCell> <TableCell :colspan="row.getAllCells().length">
</TableRow> {{ JSON.stringify(row.original) }}
</TableCell>
</TableRow>
</template>
</template> </template>
<TableRow v-else> <TableRow v-else>

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { DotsHorizontalIcon } from '@radix-icons/vue'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/lib/registry/new-york/ui/dropdown-menu'
import { Button } from '@/lib/registry/new-york/ui/button' import { Button } from '@/lib/registry/new-york/ui/button'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/lib/registry/new-york/ui/dropdown-menu'
import { DotsHorizontalIcon } from '@radix-icons/vue'
defineProps<{ defineProps<{
payment: { payment: {
@ -9,6 +9,10 @@ defineProps<{
} }
}>() }>()
defineEmits<{
(e: 'expand'): void
}>()
function copy(id: string) { function copy(id: string) {
navigator.clipboard.writeText(id) navigator.clipboard.writeText(id)
} }
@ -27,6 +31,9 @@ function copy(id: string) {
<DropdownMenuItem @click="copy(payment.id)"> <DropdownMenuItem @click="copy(payment.id)">
Copy payment ID Copy payment ID
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem @click="$emit('expand')">
Expand
</DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem>View customer</DropdownMenuItem> <DropdownMenuItem>View customer</DropdownMenuItem>
<DropdownMenuItem>View payment details</DropdownMenuItem> <DropdownMenuItem>View payment details</DropdownMenuItem>

View File

@ -0,0 +1,273 @@
<script setup lang="ts">
import type {
ColumnDef,
ColumnFiltersState,
ExpandedState,
SortingState,
VisibilityState,
} from '@tanstack/vue-table'
import { Button } from '@/lib/registry/new-york/ui/button'
import { Checkbox } from '@/lib/registry/new-york/ui/checkbox'
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@/lib/registry/new-york/ui/dropdown-menu'
import { Input } from '@/lib/registry/new-york/ui/input'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/lib/registry/new-york/ui/table'
import { valueUpdater } from '@/lib/utils'
import {
FlexRender,
getCoreRowModel,
getExpandedRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useVueTable,
} from '@tanstack/vue-table'
import { ArrowUpDown, ChevronDown } from 'lucide-vue-next'
import { h, ref, shallowRef } from 'vue'
import DropdownAction from './DataTableDemoColumn.vue'
export interface Payment {
id: string
amount: number
status: 'pending' | 'processing' | 'success' | 'failed'
email: string
}
const data = shallowRef<Payment[]>([
{
id: 'm5gr84i9',
amount: 316,
status: 'success',
email: 'ken99@yahoo.com',
},
{
id: '3u1reuv4',
amount: 242,
status: 'success',
email: 'Abe45@gmail.com',
},
{
id: 'derv1ws0',
amount: 837,
status: 'processing',
email: 'Monserrat44@gmail.com',
},
{
id: '5kma53ae',
amount: 874,
status: 'success',
email: 'Silas22@gmail.com',
},
{
id: 'bhqecj4p',
amount: 721,
status: 'failed',
email: 'carmella@hotmail.com',
},
])
const columns: ColumnDef<Payment>[] = [
{
id: 'select',
header: ({ table }) => h(Checkbox, {
'checked': table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && 'indeterminate'),
'onUpdate:checked': value => table.toggleAllPageRowsSelected(!!value),
'ariaLabel': 'Select all',
}),
cell: ({ row }) => h(Checkbox, {
'checked': row.getIsSelected(),
'onUpdate:checked': value => row.toggleSelected(!!value),
'ariaLabel': 'Select row',
}),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: 'status',
header: 'Status',
cell: ({ row }) => h('div', { class: 'capitalize' }, row.getValue('status')),
},
{
accessorKey: 'email',
header: ({ column }) => {
return h(Button, {
variant: 'ghost',
onClick: () => column.toggleSorting(column.getIsSorted() === 'asc'),
}, () => ['Email', h(ArrowUpDown, { class: 'ml-2 h-4 w-4' })])
},
cell: ({ row }) => h('div', { class: 'lowercase' }, row.getValue('email')),
},
{
accessorKey: 'amount',
header: () => h('div', { class: 'text-right' }, 'Amount'),
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
// Format the amount as a dollar amount
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(amount)
return h('div', { class: 'text-right font-medium' }, formatted)
},
},
{
id: 'actions',
enableHiding: false,
cell: ({ row }) => {
const payment = row.original
return h('div', { class: 'relative' }, h(DropdownAction, {
payment,
onExpand: row.toggleExpanded,
}))
},
},
]
const sorting = ref<SortingState>([])
const columnFilters = ref<ColumnFiltersState>([])
const columnVisibility = ref<VisibilityState>({})
const rowSelection = ref({})
const expanded = ref<ExpandedState>({})
const table = useVueTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getExpandedRowModel: getExpandedRowModel(),
onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting),
onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters),
onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility),
onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelection),
onExpandedChange: updaterOrValue => valueUpdater(updaterOrValue, expanded),
state: {
get sorting() { return sorting.value },
get columnFilters() { return columnFilters.value },
get columnVisibility() { return columnVisibility.value },
get rowSelection() { return rowSelection.value },
get expanded() { return expanded.value },
},
})
const statuses: Payment['status'][] = ['pending', 'processing', 'success', 'failed']
function randomize() {
data.value = data.value.map(item => ({
...item,
status: statuses[Math.floor(Math.random() * statuses.length)],
}))
}
</script>
<template>
<div class="w-full">
<div class="flex gap-2 items-center py-4">
<Input
class="max-w-52"
placeholder="Filter emails..."
:model-value="table.getColumn('email')?.getFilterValue() as string"
@update:model-value=" table.getColumn('email')?.setFilterValue($event)"
/>
<Button @click="randomize">
Randomize
</Button>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="outline" class="ml-auto">
Columns <ChevronDown class="ml-2 h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuCheckboxItem
v-for="column in table.getAllColumns().filter((column) => column.getCanHide())"
:key="column.id"
class="capitalize"
:checked="column.getIsVisible()"
@update:checked="(value) => {
column.toggleVisibility(!!value)
}"
>
{{ column.id }}
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div class="rounded-md border">
<Table>
<TableHeader>
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
<TableHead v-for="header in headerGroup.headers" :key="header.id">
<FlexRender v-if="!header.isPlaceholder" :render="header.column.columnDef.header" :props="header.getContext()" />
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<template v-if="table.getRowModel().rows?.length">
<template v-for="row in table.getRowModel().rows" :key="row.id">
<TableRow :data-state="row.getIsSelected() && 'selected'">
<TableCell v-for="cell in row.getVisibleCells()" :key="cell.id">
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
</TableCell>
</TableRow>
<TableRow v-if="row.getIsExpanded()">
<TableCell :colspan="row.getAllCells().length">
{{ JSON.stringify(row.original) }}
</TableCell>
</TableRow>
</template>
</template>
<TableRow v-else>
<TableCell
:colspan="columns.length"
class="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
<div class="flex items-center justify-end space-x-2 py-4">
<div class="flex-1 text-sm text-muted-foreground">
{{ table.getFilteredSelectedRowModel().rows.length }} of
{{ table.getFilteredRowModel().rows.length }} row(s) selected.
</div>
<div class="space-x-2">
<Button
variant="outline"
size="sm"
:disabled="!table.getCanPreviousPage()"
@click="table.previousPage()"
>
Previous
</Button>
<Button
variant="outline"
size="sm"
:disabled="!table.getCanNextPage()"
@click="table.nextPage()"
>
Next
</Button>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,79 @@
<script setup lang="ts">
import { Button } from '@/lib/registry/new-york/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/lib/registry/new-york/ui/dialog'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/lib/registry/new-york/ui/form'
import { Input } from '@/lib/registry/new-york/ui/input'
import { toast } from '@/lib/registry/new-york/ui/toast'
import { toTypedSchema } from '@vee-validate/zod'
import { h } from 'vue'
import * as z from 'zod'
const formSchema = toTypedSchema(z.object({
username: z.string().min(2).max(50),
}))
function onSubmit(values: any) {
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 v-slot="{ submitForm }" as="" keep-values :validation-schema="formSchema" @submit="onSubmit">
<Dialog>
<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>
<form @submit="submitForm">
<FormField v-slot="{ componentField }" name="username">
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input type="text" placeholder="shadcn" v-bind="componentField" />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
</form>
<DialogFooter>
<Button type="submit" form="dialogForm">
Save changes
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Form>
</template>

View File

@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import ListItem from './NavigationMenuDemoItem.vue'
import { import {
NavigationMenu, NavigationMenu,
NavigationMenuContent, NavigationMenuContent,
@ -73,15 +72,45 @@ const components: { title: string, href: string, description: string }[] = [
</a> </a>
</NavigationMenuLink> </NavigationMenuLink>
</li> </li>
<ListItem href="/docs" title="Introduction"> <li>
Re-usable components built using Radix UI and Tailwind CSS. <NavigationMenuLink as-child>
</ListItem> <a
<ListItem href="/docs/installation" title="Installation"> href="/docs"
How to install dependencies and structure your app. class="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
</ListItem> >
<ListItem href="/docs/primitives/typography" title="Typography"> <div class="text-sm font-medium leading-none">Introduction</div>
Styles for headings, paragraphs, lists...etc <p class="line-clamp-2 text-sm leading-snug text-muted-foreground">
</ListItem> Re-usable components built using Radix UI and Tailwind CSS.
</p>
</a>
</NavigationMenuLink>
</li>
<li>
<NavigationMenuLink as-child>
<a
href="/docs/installation"
class="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
>
<div class="text-sm font-medium leading-none">Installation</div>
<p class="line-clamp-2 text-sm leading-snug text-muted-foreground">
How to install dependencies and structure your app.
</p>
</a>
</NavigationMenuLink>
</li>
<li>
<NavigationMenuLink as-child>
<a
href="/docs/primitives/typography"
class="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
>
<div class="text-sm font-medium leading-none">Typography</div>
<p class="line-clamp-2 text-sm leading-snug text-muted-foreground">
Styles for headings, paragraphs, lists...etc
</p>
</a>
</NavigationMenuLink>
</li>
</ul> </ul>
</NavigationMenuContent> </NavigationMenuContent>
</NavigationMenuItem> </NavigationMenuItem>
@ -89,14 +118,19 @@ const components: { title: string, href: string, description: string }[] = [
<NavigationMenuTrigger>Components</NavigationMenuTrigger> <NavigationMenuTrigger>Components</NavigationMenuTrigger>
<NavigationMenuContent> <NavigationMenuContent>
<ul class="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px] "> <ul class="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px] ">
<ListItem <li v-for="component in components" :key="component.title">
v-for="component in components" <NavigationMenuLink as-child>
:key="component.title" <a
:title="component.title" :href="component.href"
:href="component.href" class="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
> >
{{ component.description }} <div class="text-sm font-medium leading-none">{{ component.title }}</div>
</ListItem> <p class="line-clamp-2 text-sm leading-snug text-muted-foreground">
{{ component.description }}
</p>
</a>
</NavigationMenuLink>
</li>
</ul> </ul>
</NavigationMenuContent> </NavigationMenuContent>
</NavigationMenuItem> </NavigationMenuItem>

View File

@ -1,27 +0,0 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import {
NavigationMenuLink,
} from '@/lib/registry/new-york/ui/navigation-menu'
defineProps<{ title?: string, href?: string }>()
</script>
<template>
<li>
<NavigationMenuLink as-child>
<a
:href="href"
:class="cn(
'block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground',
$attrs.class ?? '',
)"
>
<div class="text-sm font-medium leading-none">{{ title }}</div>
<p class="line-clamp-2 text-sm leading-snug text-muted-foreground">
<slot />
</p>
</a>
</NavigationMenuLink>
</li>
</template>

View File

@ -1,9 +1,4 @@
<script setup lang="ts"> <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 { Button } from '@/lib/registry/new-york/ui/button'
import { import {
FormControl, FormControl,
@ -22,6 +17,11 @@ import {
} from '@/lib/registry/new-york/ui/number-field' } from '@/lib/registry/new-york/ui/number-field'
import { toast } from '@/lib/registry/new-york/ui/toast' import { toast } from '@/lib/registry/new-york/ui/toast'
import { toTypedSchema } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
import { h } from 'vue'
import * as z from 'zod'
const formSchema = toTypedSchema(z.object({ const formSchema = toTypedSchema(z.object({
payment: z.number().min(10, 'Min 10 euros to send payment').max(5000, 'Max 5000 euros to send payment'), payment: z.number().min(10, 'Min 10 euros to send payment').max(5000, 'Max 5000 euros to send payment'),
})) }))
@ -43,7 +43,7 @@ const onSubmit = handleSubmit((values) => {
<template> <template>
<form class="w-2/3 space-y-6" @submit="onSubmit"> <form class="w-2/3 space-y-6" @submit="onSubmit">
<FormField name="payment"> <FormField v-slot="{ value }" name="payment">
<FormItem> <FormItem>
<FormLabel>Payment</FormLabel> <FormLabel>Payment</FormLabel>
<NumberField <NumberField
@ -55,6 +55,7 @@ const onSubmit = handleSubmit((values) => {
currencyDisplay: 'code', currencyDisplay: 'code',
currencySign: 'accounting', currencySign: 'accounting',
}" }"
:model-value="value"
@update:model-value="(v) => { @update:model-value="(v) => {
if (v) { if (v) {
setFieldValue('payment', v) setFieldValue('payment', v)

View File

@ -1,13 +1,4 @@
<script setup lang="ts"> <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 {
PinInput,
PinInputGroup,
PinInputInput,
} from '@/lib/registry/new-york/ui/pin-input'
import { Button } from '@/lib/registry/new-york/ui/button' import { Button } from '@/lib/registry/new-york/ui/button'
import { import {
FormControl, FormControl,
@ -17,7 +8,16 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from '@/lib/registry/new-york/ui/form' } from '@/lib/registry/new-york/ui/form'
import {
PinInput,
PinInputGroup,
PinInputInput,
} from '@/lib/registry/new-york/ui/pin-input'
import { toast } from '@/lib/registry/new-york/ui/toast' import { toast } from '@/lib/registry/new-york/ui/toast'
import { toTypedSchema } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
import { h } from 'vue'
import * as z from 'zod'
const formSchema = toTypedSchema(z.object({ const formSchema = toTypedSchema(z.object({
pin: z.array(z.coerce.string()).length(5, { message: 'Invalid input' }), pin: z.array(z.coerce.string()).length(5, { message: 'Invalid input' }),
@ -48,7 +48,7 @@ const handleComplete = (e: string[]) => console.log(e.join(''))
<FormControl> <FormControl>
<PinInput <PinInput
id="pin-input" id="pin-input"
v-model="value!" :model-value="value"
placeholder="○" placeholder="○"
class="flex gap-2 items-center mt-1" class="flex gap-2 items-center mt-1"
otp otp

View File

@ -12,7 +12,7 @@ import { Separator } from '@/lib/registry/new-york/ui/separator'
An open-source UI component library. An open-source UI component library.
</p> </p>
</div> </div>
<Separator class="my-4" /> <Separator class="my-4" label="Or" />
<div class="flex h-5 items-center space-x-4 text-sm"> <div class="flex h-5 items-center space-x-4 text-sm">
<div>Blog</div> <div>Blog</div>
<Separator orientation="vertical" /> <Separator orientation="vertical" />

View File

@ -0,0 +1,56 @@
<script setup lang="ts">
import { Stepper, StepperDescription, StepperIndicator, StepperItem, StepperSeparator, StepperTitle, StepperTrigger } from '@/lib/registry/new-york/ui/stepper'
import { BookUser, Check, CreditCard, Truck } from 'lucide-vue-next'
const steps = [{
step: 1,
title: 'Address',
description: 'Add your address here',
icon: BookUser,
}, {
step: 2,
title: 'Shipping',
description: 'Set your preferred shipping method',
icon: Truck,
}, {
step: 3,
title: 'Payment',
description: 'Add any payment information you have',
icon: CreditCard,
}, {
step: 4,
title: 'Checkout',
description: 'Confirm your order',
icon: Check,
}]
</script>
<template>
<Stepper>
<StepperItem
v-for="item in steps"
:key="item.step"
class="basis-1/4"
:step="item.step"
>
<StepperTrigger>
<StepperIndicator>
<component :is="item.icon" class="w-4 h-4" />
</StepperIndicator>
<div class="flex flex-col">
<StepperTitle>
{{ item.title }}
</StepperTitle>
<StepperDescription>
{{ item.description }}
</StepperDescription>
</div>
</StepperTrigger>
<StepperSeparator
v-if="item.step !== steps[steps.length - 1].step"
class="w-full h-px"
/>
</StepperItem>
</Stepper>
</template>

View File

@ -0,0 +1,223 @@
<script setup lang="ts">
import { Button } from '@/lib/registry/new-york/ui/button'
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/lib/registry/new-york/ui/form'
import { Input } from '@/lib/registry/new-york/ui/input'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/lib/registry/new-york/ui/select'
import { Stepper, StepperDescription, StepperItem, StepperSeparator, StepperTitle, StepperTrigger } from '@/lib/registry/new-york/ui/stepper'
import { toast } from '@/lib/registry/new-york/ui/toast'
import { CheckIcon, CircleIcon, DotIcon } from '@radix-icons/vue'
import { toTypedSchema } from '@vee-validate/zod'
import { h, ref } from 'vue'
import * as z from 'zod'
const formSchema = [
z.object({
fullName: z.string(),
email: z.string().email(),
}),
z.object({
password: z.string().min(2).max(50),
confirmPassword: z.string(),
}).refine(
(values) => {
return values.password === values.confirmPassword
},
{
message: 'Passwords must match!',
path: ['confirmPassword'],
},
),
z.object({
favoriteDrink: z.union([z.literal('coffee'), z.literal('tea'), z.literal('soda')]),
}),
]
const stepIndex = ref(1)
const steps = [
{
step: 1,
title: 'Your details',
description: 'Provide your name and email',
},
{
step: 2,
title: 'Your password',
description: 'Choose a password',
},
{
step: 3,
title: 'Your Favorite Drink',
description: 'Choose a drink',
},
]
function onSubmit(values: any) {
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
v-slot="{ meta, values, validate }"
as="" keep-values :validation-schema="toTypedSchema(formSchema[stepIndex - 1])"
>
<Stepper v-slot="{ isNextDisabled, isPrevDisabled, nextStep, prevStep }" v-model="stepIndex" class="block w-full">
<form
@submit="(e) => {
e.preventDefault()
validate()
if (stepIndex === steps.length && meta.valid) {
onSubmit(values)
}
}"
>
<div class="flex w-full flex-start gap-2">
<StepperItem
v-for="step in steps"
:key="step.step"
v-slot="{ state }"
class="relative flex w-full flex-col items-center justify-center"
:step="step.step"
>
<StepperSeparator
v-if="step.step !== steps[steps.length - 1].step"
class="absolute left-[calc(50%+20px)] right-[calc(-50%+10px)] top-5 block h-0.5 shrink-0 rounded-full bg-muted group-data-[state=completed]:bg-primary"
/>
<StepperTrigger as-child>
<Button
:variant="state === 'completed' || state === 'active' ? 'default' : 'outline'"
size="icon"
class="z-10 rounded-full shrink-0"
:class="[state === 'active' && 'ring-2 ring-ring ring-offset-2 ring-offset-background']"
:disabled="state !== 'completed' && !meta.valid"
>
<CheckIcon v-if="state === 'completed'" class="size-5" />
<CircleIcon v-if="state === 'active'" />
<DotIcon v-if="state === 'inactive'" />
</Button>
</StepperTrigger>
<div class="mt-5 flex flex-col items-center text-center">
<StepperTitle
:class="[state === 'active' && 'text-primary']"
class="text-sm font-semibold transition lg:text-base"
>
{{ step.title }}
</StepperTitle>
<StepperDescription
:class="[state === 'active' && 'text-primary']"
class="sr-only text-xs text-muted-foreground transition md:not-sr-only lg:text-sm"
>
{{ step.description }}
</StepperDescription>
</div>
</StepperItem>
</div>
<div class="flex flex-col gap-4 mt-4">
<template v-if="stepIndex === 1">
<FormField v-slot="{ componentField }" name="fullName">
<FormItem>
<FormLabel>Full Name</FormLabel>
<FormControl>
<Input type="text" v-bind="componentField" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="email">
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email " v-bind="componentField" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</template>
<template v-if="stepIndex === 2">
<FormField v-slot="{ componentField }" name="password">
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" v-bind="componentField" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="confirmPassword">
<FormItem>
<FormLabel>Confirm Password</FormLabel>
<FormControl>
<Input type="password" v-bind="componentField" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</template>
<template v-if="stepIndex === 3">
<FormField v-slot="{ componentField }" name="favoriteDrink">
<FormItem>
<FormLabel>Drink</FormLabel>
<Select v-bind="componentField">
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a drink" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectGroup>
<SelectItem value="coffee">
Coffe
</SelectItem>
<SelectItem value="tea">
Tea
</SelectItem>
<SelectItem value="soda">
Soda
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
</FormField>
</template>
</div>
<div class="flex items-center justify-between mt-4">
<Button :disabled="isPrevDisabled" variant="outline" size="sm" @click="prevStep()">
Back
</Button>
<div class="flex items-center gap-3">
<Button v-if="stepIndex !== 3" :type="meta.valid ? 'button' : 'submit'" :disabled="isNextDisabled" size="sm" @click="meta.valid && nextStep()">
Next
</Button>
<Button
v-if="stepIndex === 3" size="sm" type="submit"
>
Submit
</Button>
</div>
</div>
</form>
</Stepper>
</Form>
</template>

View File

@ -0,0 +1,69 @@
<script setup lang="ts">
import { Button } from '@/lib/registry/new-york/ui/button'
import { Stepper, StepperDescription, StepperItem, StepperSeparator, StepperTitle, StepperTrigger } from '@/lib/registry/new-york/ui/stepper'
import { CheckIcon, CircleIcon, DotIcon } from '@radix-icons/vue'
const steps = [
{
step: 1,
title: 'Your details',
description: 'Provide your name and email',
},
{
step: 2,
title: 'Company details',
description: 'A few details about your company',
},
{
step: 3,
title: 'Invite your team',
description: 'Start collaborating with your team',
},
]
</script>
<template>
<Stepper class="flex w-full items-start gap-2">
<StepperItem
v-for="step in steps"
:key="step.step"
v-slot="{ state }"
class="relative flex w-full flex-col items-center justify-center"
:step="step.step"
>
<StepperSeparator
v-if="step.step !== steps[steps.length - 1].step"
class="absolute left-[calc(50%+20px)] right-[calc(-50%+10px)] top-5 block h-0.5 shrink-0 rounded-full bg-muted group-data-[state=completed]:bg-primary"
/>
<StepperTrigger as-child>
<Button
:variant="state === 'completed' || state === 'active' ? 'default' : 'outline'"
size="icon"
class="z-10 rounded-full shrink-0"
:class="[state === 'active' && 'ring-2 ring-ring ring-offset-2 ring-offset-background']"
>
<CheckIcon v-if="state === 'completed'" class="size-5" />
<CircleIcon v-if="state === 'active'" />
<DotIcon v-if="state === 'inactive'" />
</Button>
</StepperTrigger>
<div class="mt-5 flex flex-col items-center text-center">
<StepperTitle
:class="[state === 'active' && 'text-primary']"
class="text-sm font-semibold transition lg:text-base"
>
{{ step.title }}
</StepperTitle>
<StepperDescription
:class="[state === 'active' && 'text-primary']"
class="sr-only text-xs text-muted-foreground transition md:not-sr-only lg:text-sm"
>
{{ step.description }}
</StepperDescription>
</div>
</StepperItem>
</Stepper>
</template>

View File

@ -0,0 +1,71 @@
<script setup lang="ts">
import { Button } from '@/lib/registry/new-york/ui/button'
import { Stepper, StepperDescription, StepperItem, StepperSeparator, StepperTitle, StepperTrigger } from '@/lib/registry/new-york/ui/stepper'
import { CheckIcon, CircleIcon, DotIcon } from '@radix-icons/vue'
const steps = [
{
step: 1,
title: 'Your details',
description:
'Provide your name and email address. We will use this information to create your account',
},
{
step: 2,
title: 'Company details',
description: 'A few details about your company will help us personalize your experience',
},
{
step: 3,
title: 'Invite your team',
description:
'Start collaborating with your team by inviting them to join your account. You can skip this step and invite them later',
},
]
</script>
<template>
<Stepper orientation="vertical" class="mx-auto flex w-full max-w-md flex-col justify-start gap-10">
<StepperItem
v-for="step in steps"
:key="step.step"
v-slot="{ state }"
class="relative flex w-full items-start gap-6"
:step="step.step"
>
<StepperSeparator
v-if="step.step !== steps[steps.length - 1].step"
class="absolute left-[18px] top-[38px] block h-[105%] w-0.5 shrink-0 rounded-full bg-muted group-data-[state=completed]:bg-primary"
/>
<StepperTrigger as-child>
<Button
:variant="state === 'completed' || state === 'active' ? 'default' : 'outline'"
size="icon"
class="z-10 rounded-full shrink-0"
:class="[state === 'active' && 'ring-2 ring-ring ring-offset-2 ring-offset-background']"
>
<CheckIcon v-if="state === 'completed'" class="size-5" />
<CircleIcon v-if="state === 'active'" />
<DotIcon v-if="state === 'inactive'" />
</Button>
</StepperTrigger>
<div class="flex flex-col gap-1">
<StepperTitle
:class="[state === 'active' && 'text-primary']"
class="text-sm font-semibold transition lg:text-base"
>
{{ step.title }}
</StepperTitle>
<StepperDescription
:class="[state === 'active' && 'text-primary']"
class="sr-only text-xs text-muted-foreground transition md:not-sr-only lg:text-sm"
>
{{ step.description }}
</StepperDescription>
</div>
</StepperItem>
</Stepper>
</template>

View File

@ -0,0 +1,78 @@
<script setup lang="ts">
import { Button } from '@/lib/registry/new-york/ui/button'
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/lib/registry/new-york/ui/card'
import { Input } from '@/lib/registry/new-york/ui/input'
import { Label } from '@/lib/registry/new-york/ui/label'
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '@/lib/registry/new-york/ui/tabs'
</script>
<template>
<Tabs default-value="account" class="w-[400px]" orientation="vertical">
<TabsList class="grid w-full grid-cols-1">
<TabsTrigger value="account">
Account
</TabsTrigger>
<TabsTrigger value="password">
Password
</TabsTrigger>
</TabsList>
<TabsContent value="account">
<Card>
<CardHeader>
<CardTitle>Account</CardTitle>
<CardDescription>
Make changes to your account here. Click save when you're done.
</CardDescription>
</CardHeader>
<CardContent class="space-y-2">
<div class="space-y-1">
<Label for="name">Name</Label>
<Input id="name" default-value="Pedro Duarte" />
</div>
<div class="space-y-1">
<Label for="username">Username</Label>
<Input id="username" default-value="@peduarte" />
</div>
</CardContent>
<CardFooter>
<Button>Save changes</Button>
</CardFooter>
</Card>
</TabsContent>
<TabsContent value="password">
<Card>
<CardHeader>
<CardTitle>Password</CardTitle>
<CardDescription>
Change your password here. After saving, you'll be logged out.
</CardDescription>
</CardHeader>
<CardContent class="space-y-2">
<div class="space-y-1">
<Label for="current">Current password</Label>
<Input id="current" type="password" />
</div>
<div class="space-y-1">
<Label for="new">New password</Label>
<Input id="new" type="password" />
</div>
</CardContent>
<CardFooter>
<Button>Save password</Button>
</CardFooter>
</Card>
</TabsContent>
</Tabs>
</template>

View File

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue'
import { ComboboxAnchor, ComboboxInput, ComboboxPortal, ComboboxRoot } from 'radix-vue'
import { CommandEmpty, CommandGroup, CommandItem, CommandList } from '@/lib/registry/new-york/ui/command' import { CommandEmpty, CommandGroup, CommandItem, CommandList } from '@/lib/registry/new-york/ui/command'
import { TagsInput, TagsInputInput, TagsInputItem, TagsInputItemDelete, TagsInputItemText } from '@/lib/registry/new-york/ui/tags-input' import { TagsInput, TagsInputInput, TagsInputItem, TagsInputItemDelete, TagsInputItemText } from '@/lib/registry/new-york/ui/tags-input'
import { ComboboxAnchor, ComboboxContent, ComboboxInput, ComboboxPortal, ComboboxRoot } from 'radix-vue'
import { computed, ref } from 'vue'
const frameworks = [ const frameworks = [
{ value: 'next.js', label: 'Next.js' }, { value: 'next.js', label: 'Next.js' },
@ -28,7 +28,7 @@ const filteredFrameworks = computed(() => frameworks.filter(i => !modelValue.val
</TagsInputItem> </TagsInputItem>
</div> </div>
<ComboboxRoot v-model="modelValue" v-model:open="open" v-model:searchTerm="searchTerm" class="w-full"> <ComboboxRoot v-model="modelValue" v-model:open="open" v-model:search-term="searchTerm" class="w-full">
<ComboboxAnchor as-child> <ComboboxAnchor as-child>
<ComboboxInput placeholder="Framework..." as-child> <ComboboxInput placeholder="Framework..." as-child>
<TagsInputInput class="w-full px-3" :class="modelValue.length > 0 ? 'mt-2' : ''" @keydown.enter.prevent /> <TagsInputInput class="w-full px-3" :class="modelValue.length > 0 ? 'mt-2' : ''" @keydown.enter.prevent />
@ -36,29 +36,31 @@ const filteredFrameworks = computed(() => frameworks.filter(i => !modelValue.val
</ComboboxAnchor> </ComboboxAnchor>
<ComboboxPortal> <ComboboxPortal>
<CommandList <ComboboxContent>
position="popper" <CommandList
class="w-[--radix-popper-anchor-width] rounded-md mt-2 border bg-popover text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2" position="popper"
> class="w-[--radix-popper-anchor-width] rounded-md mt-2 border bg-popover text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
<CommandEmpty /> >
<CommandGroup> <CommandEmpty />
<CommandItem <CommandGroup>
v-for="framework in filteredFrameworks" :key="framework.value" :value="framework.label" <CommandItem
@select.prevent="(ev) => { v-for="framework in filteredFrameworks" :key="framework.value" :value="framework.label"
if (typeof ev.detail.value === 'string') { @select.prevent="(ev) => {
searchTerm = '' if (typeof ev.detail.value === 'string') {
modelValue.push(ev.detail.value) searchTerm = ''
} modelValue.push(ev.detail.value)
}
if (filteredFrameworks.length === 0) { if (filteredFrameworks.length === 0) {
open = false open = false
} }
}" }"
> >
{{ framework.label }} {{ framework.label }}
</CommandItem> </CommandItem>
</CommandGroup> </CommandGroup>
</CommandList> </CommandList>
</ComboboxContent>
</ComboboxPortal> </ComboboxPortal>
</ComboboxRoot> </ComboboxRoot>
</TagsInput> </TagsInput>

View File

@ -0,0 +1,62 @@
<script setup lang="ts">
import { Button } from '@/lib/registry/new-york/ui/button'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/lib/registry/new-york/ui/form'
import { TagsInput, TagsInputInput, TagsInputItem, TagsInputItemDelete, TagsInputItemText } from '@/lib/registry/new-york/ui/tags-input'
import { toast } from '@/lib/registry/new-york/ui/toast'
import { toTypedSchema } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
import { h } from 'vue'
import { z } from 'zod'
const formSchema = toTypedSchema(z.object({
fruits: z.array(z.string()).min(1).max(3),
}))
const { handleSubmit } = useForm({
validationSchema: formSchema,
initialValues: {
fruits: ['Apple', 'Banana'],
},
})
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="{ value }" name="fruits">
<FormItem>
<FormLabel>Fruits</FormLabel>
<FormControl>
<TagsInput :model-value="value">
<TagsInputItem v-for="item in value" :key="item" :value="item">
<TagsInputItemText />
<TagsInputItemDelete />
</TagsInputItem>
<TagsInputInput placeholder="Fruits..." />
</TagsInput>
</FormControl>
<FormDescription>
Select your favorite fruits.
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<Button type="submit">
Submit
</Button>
</form>
</template>

View File

@ -1,13 +1,13 @@
<script setup lang="ts" generic="T extends ZodObjectOrWrapped"> <script setup lang="ts" generic="T extends ZodObjectOrWrapped">
import { computed, toRefs } from 'vue'
import type { ZodAny, z } from 'zod'
import { toTypedSchema } from '@vee-validate/zod'
import type { FormContext, GenericObject } from 'vee-validate' import type { FormContext, GenericObject } from 'vee-validate'
import { type ZodObjectOrWrapped, getBaseSchema, getBaseType, getDefaultValueInZodStack, getObjectFormSchema } from './utils' import type { z, ZodAny } from 'zod'
import type { Config, ConfigItem, Dependency, Shape } from './interface' import type { Config, ConfigItem, Dependency, Shape } from './interface'
import { Form } from '@/lib/registry/new-york/ui/form'
import { toTypedSchema } from '@vee-validate/zod'
import { computed, toRefs } from 'vue'
import AutoFormField from './AutoFormField.vue' import AutoFormField from './AutoFormField.vue'
import { provideDependencies } from './dependencies' import { provideDependencies } from './dependencies'
import { Form } from '@/lib/registry/new-york/ui/form' import { getBaseSchema, getBaseType, getDefaultValueInZodStack, getObjectFormSchema, type ZodObjectOrWrapped } from './utils'
const props = defineProps<{ const props = defineProps<{
schema: T schema: T
@ -17,7 +17,7 @@ const props = defineProps<{
}>() }>()
const emits = defineEmits<{ const emits = defineEmits<{
submit: [event: GenericObject] submit: [event: z.infer<T>]
}>() }>()
const { dependencies } = toRefs(props) const { dependencies } = toRefs(props)

View File

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { type HTMLAttributes, computed } from 'vue'
import { CalendarCell, type CalendarCellProps, useForwardProps } from 'radix-vue'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { CalendarCell, type CalendarCellProps, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<CalendarCellProps & { class?: HTMLAttributes['class'] }>() const props = defineProps<CalendarCellProps & { class?: HTMLAttributes['class'] }>()
@ -16,7 +16,7 @@ const forwardedProps = useForwardProps(delegatedProps)
<template> <template>
<CalendarCell <CalendarCell
:class="cn('relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([data-selected])]:rounded-md [&:has([data-selected])]:bg-accent [&:has([data-selected][data-outside-month])]:bg-accent/50', props.class)" :class="cn('relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([data-selected])]:rounded-md [&:has([data-selected])]:bg-accent [&:has([data-selected][data-outside-view])]:bg-accent/50', props.class)"
v-bind="forwardedProps" v-bind="forwardedProps"
> >
<slot /> <slot />

View File

@ -1,8 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { type HTMLAttributes, computed } from 'vue'
import { CalendarCellTrigger, type CalendarCellTriggerProps, useForwardProps } from 'radix-vue'
import { buttonVariants } from '@/lib/registry/new-york/ui/button' import { buttonVariants } from '@/lib/registry/new-york/ui/button'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { CalendarCellTrigger, type CalendarCellTriggerProps, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<CalendarCellTriggerProps & { class?: HTMLAttributes['class'] }>() const props = defineProps<CalendarCellTriggerProps & { class?: HTMLAttributes['class'] }>()
@ -28,7 +28,7 @@ const forwardedProps = useForwardProps(delegatedProps)
// Unavailable // Unavailable
'data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through', 'data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through',
// Outside months // Outside months
'data-[outside-month]:pointer-events-none data-[outside-month]:text-muted-foreground data-[outside-month]:opacity-50 [&[data-outside-month][data-selected]]:bg-accent/50 [&[data-outside-month][data-selected]]:text-muted-foreground [&[data-outside-month][data-selected]]:opacity-30', 'data-[outside-view]:text-muted-foreground data-[outside-view]:opacity-50 [&[data-outside-view][data-selected]]:bg-accent/50 [&[data-outside-view][data-selected]]:text-muted-foreground [&[data-outside-view][data-selected]]:opacity-30',
props.class, props.class,
)" )"
v-bind="forwardedProps" v-bind="forwardedProps"

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useProvideCarousel } from './useCarousel'
import type { CarouselEmits, CarouselProps, WithClassAsProps } from './interface' import type { CarouselEmits, CarouselProps, WithClassAsProps } from './interface'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useProvideCarousel } from './useCarousel'
const props = withDefaults(defineProps<CarouselProps & WithClassAsProps>(), { const props = withDefaults(defineProps<CarouselProps & WithClassAsProps>(), {
orientation: 'horizontal', orientation: 'horizontal',
@ -9,9 +9,17 @@ const props = withDefaults(defineProps<CarouselProps & WithClassAsProps>(), {
const emits = defineEmits<CarouselEmits>() const emits = defineEmits<CarouselEmits>()
const carouselArgs = useProvideCarousel(props, emits) const { canScrollNext, canScrollPrev, carouselApi, carouselRef, orientation, scrollNext, scrollPrev } = useProvideCarousel(props, emits)
defineExpose(carouselArgs) defineExpose({
canScrollNext,
canScrollPrev,
carouselApi,
carouselRef,
orientation,
scrollNext,
scrollPrev,
})
function onKeyDown(event: KeyboardEvent) { function onKeyDown(event: KeyboardEvent) {
const prevKey = props.orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft' const prevKey = props.orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft'
@ -19,14 +27,14 @@ function onKeyDown(event: KeyboardEvent) {
if (event.key === prevKey) { if (event.key === prevKey) {
event.preventDefault() event.preventDefault()
carouselArgs.scrollPrev() scrollPrev()
return return
} }
if (event.key === nextKey) { if (event.key === nextKey) {
event.preventDefault() event.preventDefault()
carouselArgs.scrollNext() scrollNext()
} }
} }
</script> </script>
@ -39,6 +47,6 @@ function onKeyDown(event: KeyboardEvent) {
tabindex="0" tabindex="0"
@keydown="onKeyDown" @keydown="onKeyDown"
> >
<slot v-bind="carouselArgs" /> <slot :can-scroll-next :can-scroll-prev :carousel-api :carousel-ref :orientation :scroll-next :scroll-prev />
</div> </div>
</template> </template>

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