feat: add vee-validate (#85)

* feat: add `vee-validate`

* chore: update

* chore: update `AccountForm` example

- add `FormDescription` component
- include `src` in tsconfig

* refactor: use radix-vue `Slot` component

* chore: refresh lockfile

* chore: update `ProfileForm.vue` and `AccountForm`

fix vee-validate initialValues on components with `componentField` slotProp

* chore: update `AppearanceForm.vue`

update pnpm and some deps -_-

* refactor: update

- add new-york style
- off eslint import/first rule
- use `useId` from radix-vue

* fix: class-name -> class

* refactor: simplify validation for `Command` component

* fix: v-bind="field" -> v-bind="componentField"

* fix: useAttrs to prevent class duplication

* docs: add `form.md`

- change TabPreview.vue to showcase way of using vee-validate

* docs: add form example for `checkbox` `input` and `datepicker`

* docs: add `combobox`, `datepicker`, `radio-group`, `select`, `switch` and `textarea` form and some other exmaples

* chore: typo, update `zod`, `vite`, and `@vitejs/plugin-vue`
This commit is contained in:
Sadegh Barati 2023-10-06 07:34:38 +03:30 committed by GitHub
parent 8c2e6f1539
commit d03067db67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
88 changed files with 4694 additions and 1584 deletions

View File

@ -12,5 +12,6 @@ module.exports = {
'no-console': 'warn',
'no-tabs': 'off',
'no-invalid-character': 'off',
'import/first': 'off',
},
}

View File

@ -3,10 +3,14 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/lib/registry/default
const props = withDefaults(defineProps<{
name: string
names?: string[]
align?: 'center' | 'start' | 'end'
sfcTsCode?: string
sfcTsHtml?: string
}>(), { align: 'center' })
}>(), {
align: 'center',
names: () => ['CLI', 'Manual'],
})
</script>
<template>
@ -15,24 +19,17 @@ const props = withDefaults(defineProps<{
<div class="flex items-center justify-between pb-3">
<TabsList class="w-full justify-start rounded-none border-b bg-transparent p-0">
<TabsTrigger
value="CLI"
v-for="(tab, index) in props.names"
:key="index"
:value="tab"
class="relative h-9 rounded-none border-b-2 border-b-transparent bg-transparent px-4 pb-3 pt-2 font-semibold text-muted-foreground shadow-none transition-none data-[state=active]:border-b-primary data-[state=active]:text-foreground data-[state=active]:shadow-none"
>
CLI
</TabsTrigger>
<TabsTrigger
value="Manual"
class="relative h-9 rounded-none border-b-2 border-b-transparent bg-transparent px-4 pb-3 pt-2 font-semibold text-muted-foreground shadow-none transition-none data-[state=active]:border-b-primary data-[state=active]:text-foreground data-[state=active]:shadow-none"
>
Manual
{{ tab }}
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="CLI" class="relative space-y-10">
<slot name="CLI" />
</TabsContent>
<TabsContent value="Manual">
<slot name="Manual" />
<TabsContent v-for="(tab, index) in props.names" :key="index" :value="tab" class="relative space-y-10">
<slot :name="tab" />
</TabsContent>
</Tabs>
</div>

View File

@ -217,9 +217,8 @@ export const docsConfig: DocsConfig = {
},
{
title: 'Form',
href: '#',
label: 'Soon',
disabled: true,
href: '/docs/components/form',
label: 'New',
items: [],
},
{
@ -352,10 +351,10 @@ export const examples: Example[] = [
code: 'https://github.com/radix-vue/shadcn-vue/tree/dev/apps/www/src/examples/cards',
},
// {
// name: "Tasks",
// href: "/examples/tasks",
// label: "New",
// code: "https://github.com/radix-vue/shadcn-vue/tree/dev/apps/www/src/examples/tasks"
// name: "Tasks",
// href: "/examples/tasks",
// label: "New",
// code: "https://github.com/radix-vue/shadcn-vue/tree/dev/apps/www/src/examples/tasks"
// },
{
name: 'Playground',

View File

@ -14,19 +14,21 @@
"dependencies": {
"@morev/vue-transitions": "^2.3.6",
"@radix-icons/vue": "^1.0.0",
"@tanstack/vue-table": "^8.9.9",
"@tanstack/vue-table": "^8.10.3",
"@unovis/ts": "^1.2.1",
"@unovis/vue": "1.3.0-alpha.3",
"@vee-validate/zod": "^4.11.7",
"@vueuse/core": "^10.4.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"date-fns": "^2.30.0",
"lucide-vue-next": "^0.276.0",
"tailwindcss-animate": "^1.0.7",
"v-calendar": "^3.0.3",
"v-calendar": "^3.1.0",
"vee-validate": "4.11.7",
"vue": "^3.3.4",
"vue-wrap-balancer": "^1.1.3",
"zod": "^3.22.2"
"zod": "^3.22.4"
},
"devDependencies": {
"@iconify-json/radix-icons": "^1.1.11",
@ -35,21 +37,21 @@
"@iconify/vue": "^4.1.1",
"@types/lodash.template": "^4.5.1",
"@types/node": "^20.6.0",
"@vitejs/plugin-vue": "^4.3.4",
"@vitejs/plugin-vue": "^4.4.0",
"@vitejs/plugin-vue-jsx": "^3.0.2",
"@vue/compiler-core": "^3.3.4",
"@vue/compiler-dom": "^3.3.4",
"autoprefixer": "^10.4.15",
"autoprefixer": "^10.4.16",
"lodash.template": "^4.5.0",
"radix-vue": "^0.4.1",
"rimraf": "^5.0.1",
"tailwind-merge": "^1.14.0",
"tailwindcss": "^3.3.3",
"tsx": "^3.12.10",
"tsx": "^3.13.0",
"typescript": "^5.2.2",
"unplugin-icons": "^0.17.0",
"vite": "^4.4.9",
"vitepress": "^1.0.0-rc.13",
"vue-tsc": "^1.8.11"
"vite": "^4.4.11",
"vitepress": "^1.0.0-rc.20",
"vue-tsc": "^1.8.15"
}
}

View File

@ -30,6 +30,16 @@ import { Checkbox } from '@/components/ui/checkbox'
## Examples
### With text
<ComponentPreview name="CheckboxWithText" />
### Disabled
<ComponentPreview name="CheckboxDisabled" />
<ComponentPreview name="CheckboxDisabled" />
### Form
<ComponentPreview name="CheckboxFormSingle" />
<ComponentPreview name="CheckboxFormMultiple" />

View File

@ -86,4 +86,20 @@ const value = ref({})
</template>
```
## Examples
### Combobox
<ComponentPreview name="ComboboxDemo" />
### Popover
<ComponentPreview name="ComboboxPopover" />
### Dropdown menu
<ComponentPreview name="ComboboxDropdownMenu" />
### Form
<ComponentPreview name="ComboboxForm" />

View File

@ -56,12 +56,18 @@ const date = ref<Date>()
## Examples
### Date Range Picker
<ComponentPreview name="DatePickerWithRange" />
### Date Picker
<ComponentPreview name="DatePickerDemo" />
<ComponentPreview name="DatePickerDemo" />
### Date Range Picker
<ComponentPreview name="DatePickerWithRange" />
### With Presets
<ComponentPreview name="DatePickerWithPresets" />
### Form
<ComponentPreview name="DatePickerForm" />

View File

@ -0,0 +1,331 @@
---
title: VeeValidate
description: Building forms with VeeValidate and Zod.
---
Forms are tricky. They are one of the most common things you'll build in a web application, but also one of the most complex.
Well-designed HTML forms are:
- Well-structured and semantically correct.
- Easy to use and navigate (keyboard).
- Accessible with ARIA attributes and proper labels.
- Has support for client and server side validation.
- Well-styled and consistent with the rest of the application.
In this guide, we will take a look at building forms with [`vee-validate`](https://vee-validate.logaretm.com/v4/) and [`zod`](https://zod.dev). We're going to use a `<FormField>` component to compose accessible forms using Radix Vue components.
## Features
The `<Form />` component is a wrapper around the `vee-validate` library. It provides a few things:
- Composable components for building forms.
- A `<FormField />` component for building controlled form fields.
- Form validation using `zod`.
- Applies the correct `aria` attributes to form fields based on states, handle unqiue IDs
- Built to work with all Radix Vue components.
- Bring your own schema library. We use `zod` but you can use any other supported schema validation you want, like [`yup`](https://github.com/jquense/yup) or [`valibot`](https://valibot.dev/).
- **You have full control over the markup and styling.**
[`vee-validate`](https://vee-validate.logaretm.com/v4/) makes use of two flavors to add validation to your forms.
- Composition API
- Higher-order components (HOC)
## Anatomy
```vue
<Form>
<FormField v-slot="{ ... }">
<FormItem>
<FormLabel />
<FormControl>
<!-- any Form Input component or native input elements -->
</FormControl>
<FormDescription />
<FormMessage />
</FormItem>
</FormField>
</Form>
```
## Example
<TabPreview name="Component" :names="['Component', 'Native']">
<template #Component>
#### `Input` Component
```vue
<FormField v-slot="{ componentField }">
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="shadcn" v-bind="componentField" />
</FormControl>
<FormDescription />
<FormMessage />
</FormItem>
</FormField>
```
</template>
<template #Native>
#### native `input` element
```vue
<FormField v-slot="{ field }">
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<input placeholder="shadcn" v-bind="field" />
</FormControl>
<FormDescription />
<FormMessage />
</FormItem>
</FormField>
```
</template>
</TabPreview>
## Installation
<TabPreview name="CLI">
<template #CLI>
```bash
npx shadcn-vue@latest add form
```
</template>
<template #Manual>
<Steps>
### Install the following dependency:
```bash
npm install radix-vue vee-validate @vee-validate/zod zod
```
### Copy and paste the following codes into your project:
`index.ts`
<<< @/lib/registry/default/ui/form/index.ts
`FormItem.vue`
<<< @/lib/registry/default/ui/form/FormItem.vue
`FormLabel.vue`
<<< @/lib/registry/default/ui/form/FormLabel.vue
`FormControl.vue`
<<< @/lib/registry/default/ui/form/FormControl.vue
`FormMessage.vue`
<<< @/lib/registry/default/ui/form/FormMessage.vue
`FormDescription.vue`
<<< @/lib/registry/default/ui/form/FormDescription.vue
### Update the import paths to match your project setup.
</Steps>
</template>
</TabPreview>
## Usage
<Steps>
### Create a form schema
Define the shape of your form using a Zod schema. You can read more about using Zod in the [Zod documentation](https://zod.dev).
Use `@vee-validate/zod` to integrate Zod schema validation with `vee-validate`
`toTypedSchema` also makes the form values and submitted values typed automatically and caters for both input and output types of that schema.
```vue showLineNumbers {2-3,5-7}
<script setup lang="ts">
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
const formSchema = toTypedSchema(z.object({
username: z.string().min(2).max(50),
}))
</script>
```
### Define a form
Use the `useForm` composable from `vee-validate` or use `<Form />` component to create a from.
<TabPreview name="Composition" :names="['Composition', 'Component']">
<template #Composition>
```vue showLineNumbers {2,19-21}
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@/components/ui/form'
const formSchema = toTypedSchema(z.object({
username: z.string().min(2).max(50),
}))
const form = useForm({
validationSchema: formSchema,
})
const onSubmit = form.handleSubmit((values) => {
console.log('Form submitted!', values)
})
</script>
<template>
<form @submit="onSubmit">
...
</Form>
</template>
```
</template>
<template #Component>
```vue showLineNumbers {5,24-26}
<script setup lang="ts">
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@/components/ui/form'
const formSchema = toTypedSchema(z.object({
username: z.string().min(2).max(50),
}))
function onSubmit(values) {
console.log('Form submitted!', values)
}
</script>
<template>
<Form :validation-schema="formSchema" @submit="onSubmit">
...
</Form>
</template>
```
</template>
</TabPreview>
### Build your form
Based on last step we can either use `<Form />` component or `useForm` composable
`useForm` is recommended cause values are typed automatically
```vue showLineNumbers {2}
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { Button } from '@/components/ui/button'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
const formSchema = toTypedSchema(z.object({
username: z.string().min(2).max(50),
}))
const form = useForm({
validationSchema: formSchema,
})
const onSubmit = form.handleSubmit((values) => {
console.log('Form submitted!', values)
})
</script>
<template>
<form @submit="onSubmit">
<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>
<Button type="submit">
Submit
</Button>
</Form>
</template>
```
### Done
That's it. You now have a fully accessible form that is type-safe with client-side validation.
<ComponentPreview
name="InputForm"
class="[&_[role=tablist]]:hidden [&>div>div:first-child]:hidden"
/>
</Steps>
## Examples
See the following links for more examples on how to use the `vee-validate` features with other components:
- [Checkbox](/docs/components/checkbox#form)
- [Date Picker](/docs/components/date-picker#form)
- [Input](/docs/components/input#form)
- [Radio Group](/docs/components/radio-group#form)
- [Select](/docs/components/select#form)
- [Switch](/docs/components/switch#form)
- [Textarea](/docs/components/textarea#form)
- [Combobox](/docs/components/combobox#form)

View File

@ -41,4 +41,28 @@ import { Input } from '@/components/ui/input'
<template>
<Input />
</template>
```
```
### Default
<ComponentPreview name="InputDemo" class="[&_input]:max-w-xs" />
### File
<ComponentPreview name="InputFile" class="[&_input]:max-w-xs" />
### Disabled
<ComponentPreview name="InputDisabled" class="[&_input]:max-w-xs" />
### With Label
<ComponentPreview name="InputWithLabel" class="[&_input]:max-w-xs" />
### With Button
<ComponentPreview name="InputWithButton" class="[&_input]:max-w-xs" />
### Form
<ComponentPreview name="InputForm" />

View File

@ -34,4 +34,10 @@ import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
</div>
</RadioGroup>
</template>
```
```
## Examples
### Form
<ComponentPreview name="RadioGroupForm" />

View File

@ -46,3 +46,9 @@ import {
</Select>
</template>
```
## Examples
### Form
<ComponentPreview name="SelectForm" />

View File

@ -47,4 +47,10 @@ import { Switch } from '@/components/ui/switch'
<template>
<Switch />
</template>
```
```
## Examples
### Form
<ComponentPreview name="SwitchForm" />

View File

@ -45,4 +45,30 @@ import { Textarea } from '@/components/ui/textarea'
<template>
<Textarea />
</template>
```
```
## Examples
### Default
<ComponentPreview name="TextareaDemo" />
### Disabled
<ComponentPreview name="TextareaDisabled" />
### With Label
<ComponentPreview name="TextareaWithLabel" className="[&_div.grid]:w-full" />
### With Text
<ComponentPreview name="TextareaWithText" />
### With Button
<ComponentPreview name="TextareaWithButton" />
### Form
<ComponentPreview name="TextareaForm" />

View File

@ -2,21 +2,23 @@
import { ref } from 'vue'
import * as z from 'zod'
import { format } from 'date-fns'
import { toTypedSchema } from '@vee-validate/zod'
import { configure } from 'vee-validate'
import { Check, ChevronsUpDown } from 'lucide-vue-next'
import { cn } from '@/lib/utils'
import RadixIconsCalendar from '~icons/radix-icons/calendar'
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/lib/registry/default/ui/form'
import { Input } from '@/lib/registry/new-york/ui/input'
import { Label } from '@/lib/registry/new-york/ui/label'
import { Separator } from '@/lib/registry/new-york/ui/separator'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/lib/registry/new-york/ui/select'
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from '@/lib/registry/default/ui/command'
import { Button } from '@/lib/registry/new-york/ui/button'
import {
Popover,
@ -25,11 +27,7 @@ import {
} from '@/lib/registry/default/ui/popover'
import { Calendar } from '@/lib/registry/new-york/ui/calendar'
const accountForm = ref({
name: '',
dob: null,
language: '',
})
const open = ref(false)
const languages = [
{ label: 'English', value: 'en' },
@ -43,7 +41,7 @@ const languages = [
{ label: 'Chinese', value: 'zh' },
] as const
const accountFormSchema = z.object({
const accountFormSchema = toTypedSchema(z.object({
name: z
.string()
.min(2, {
@ -58,20 +56,14 @@ const accountFormSchema = z.object({
language: z.string().nonempty({
message: 'Please select a language.',
}),
})
}))
type AccountFormValues = z.infer<typeof accountFormSchema>
const errors = ref<z.ZodFormattedError<AccountFormValues> | null>(null)
const filterFunction = (list: typeof languages, search: string) => list.filter(i => i.value.toLowerCase().includes(search.toLowerCase()))
async function handleSubmit() {
const result = accountFormSchema.safeParse(accountForm.value)
if (!result.success) {
errors.value = result.error.format()
console.log(errors.value)
return
}
console.log('Form submitted!', accountForm.value)
// https://github.com/logaretm/vee-validate/issues/3521
// https://github.com/logaretm/vee-validate/discussions/3571
async function onSubmit(values: any) {
console.log('Form submitted!', values)
}
</script>
@ -85,74 +77,107 @@ async function handleSubmit() {
</p>
</div>
<Separator />
<form class="space-y-8" @submit.prevent="handleSubmit">
<div class="grid gap-2">
<Label for="name" :class="cn('text-sm', errors?.name && 'text-destructive')">
Name
</Label>
<Input id="name" v-model="accountForm.name" placeholder="Your name" />
<span class="text-muted-foreground text-sm">
This is the name that will be displayed on your profile and in emails.
</span>
<div v-if="errors?.name" class="text-sm text-destructive">
<span v-for="error in errors.name._errors" :key="error">{{ error }}</span>
</div>
</div>
<div class="grid gap-2">
<Label for="dob" :class="cn('text-sm', errors?.dob && 'text-destructive')">
Date of Birth
</Label>
<Popover>
<PopoverTrigger as-child>
<Button
variant="outline"
:class="cn(
'w-[280px] pl-3 text-left font-normal',
!accountForm.dob && 'text-muted-foreground',
)"
>
<span>{{ accountForm.dob ? format(accountForm.dob, "PPP") : "Pick a date" }}</span>
<RadixIconsCalendar class="ml-auto h-4 w-4 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent class="p-0">
<Calendar v-model="accountForm.dob" />
</PopoverContent>
</Popover>
<span class="text-muted-foreground text-sm">
Your date of birth is used to calculate your age.
</span>
<div v-if="errors?.dob" class="text-sm text-destructive">
<span v-for="error in errors.dob._errors" :key="error">{{ error }}</span>
</div>
</div>
<div class="grid gap-2">
<Label for="language" :class="cn('text-sm', errors?.language && 'text-destructive')">
Language
</Label>
<Select id="language" v-model="accountForm.language">
<SelectTrigger class="w-[200px]">
<SelectValue placeholder="Select a language" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem v-for="language in languages" :key="language.value" :value="language.value">
{{ language.label }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<span class="text-muted-foreground text-sm">
This is the language that will be used in the dashboard.
</span>
<div v-if="errors?.language" class="text-sm text-destructive">
<span v-for="error in errors.language._errors" :key="error">{{ error }}</span>
</div>
</div>
<Form v-slot="{ setValues }" :validation-schema="accountFormSchema" class="space-y-8" @submit="onSubmit">
<FormField v-slot="{ componentField }" name="name">
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input type="text" placeholder="Your name" v-bind="componentField" />
</FormControl>
<FormDescription>
This is the name that will be displayed on your profile and in emails.
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField, value }" name="dob">
<FormItem>
<FormLabel>Date of birth</FormLabel>
<Popover>
<PopoverTrigger as-child>
<FormControl>
<Button
variant="outline" :class="cn(
'w-[280px] pl-3 text-left font-normal',
!value && 'text-muted-foreground',
)"
>
<span>{{ value ? format(value, "PPP") : "Pick a date" }}</span>
<RadixIconsCalendar class="ml-auto h-4 w-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent class="p-0">
<Calendar v-bind="componentField" />
</PopoverContent>
</Popover>
<FormDescription>
Your date of birth is used to calculate your age.
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ value }" name="language">
<FormItem>
<FormLabel>Language</FormLabel>
<Popover v-model:open="open">
<PopoverTrigger as-child>
<FormControl>
<Button
variant="outline" role="combobox" :aria-expanded="open" :class="cn(
'w-[200px] justify-between',
!value && 'text-muted-foreground',
)"
>
{{ value ? languages.find(
(language) => language.value === value,
)?.label : 'Select language...' }}
<ChevronsUpDown class="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent class="w-[200px] p-0">
<Command>
<CommandInput placeholder="Search language..." />
<CommandEmpty>No framework found.</CommandEmpty>
<CommandGroup>
<CommandItem
v-for="language in languages" :key="language.value" :value="language.label"
@select="() => {
setValues({
language: language.value,
})
open = false
}"
>
<Check
:class="cn(
'mr-2 h-4 w-4',
value === language.value ? 'opacity-100' : 'opacity-0',
)"
/>
{{ language.label }}
</CommandItem>
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
<FormDescription>
This is the language that will be used in the dashboard.
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<div class="flex justify-start">
<Button type="submit">
Update account
</Button>
</div>
</form>
</Form>
</template>

View File

@ -1,27 +1,16 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { cn } from '@/lib/utils'
import { Label } from '@/lib/registry/new-york/ui/label'
import { ChevronDownIcon } from '@radix-icons/vue'
import { cn } from '@/lib/utils'
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/default/ui/radio-group'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/lib/registry/new-york/ui/select'
import { Button } from '@/lib/registry/new-york/ui/button'
import { Button, buttonVariants } from '@/lib/registry/new-york/ui/button'
const appearenceForm = ref({
theme: 'light',
font: '',
})
const appearanceFormSchema = z.object({
const appearanceFormSchema = toTypedSchema(z.object({
theme: z.enum(['light', 'dark'], {
required_error: 'Please select a theme.',
}),
@ -29,21 +18,19 @@ const appearanceFormSchema = z.object({
invalid_type_error: 'Select a font',
required_error: 'Please select a font.',
}),
}))
const { handleSubmit } = useForm({
validationSchema: appearanceFormSchema,
initialValues: {
theme: 'light',
font: 'inter',
},
})
type AppearanceFormValues = z.infer<typeof appearanceFormSchema>
const errors = ref<z.ZodFormattedError<AppearanceFormValues> | null>(null)
async function handleSubmit() {
const result = appearanceFormSchema.safeParse(appearenceForm.value)
if (!result.success) {
errors.value = result.error.format()
console.log(errors.value)
return
}
console.log('Form submitted!', appearenceForm.value)
}
const onSubmit = handleSubmit((values) => {
console.log('Form submitted!', values)
})
</script>
<template>
@ -56,109 +43,106 @@ async function handleSubmit() {
</p>
</div>
<Separator />
<form class="space-y-8" @submit.prevent="handleSubmit">
<div class="grid gap-2">
<Label for="font" :class="cn('text-sm', errors?.font && 'text-destructive')">
Font
</Label>
<Select id="font" v-model="appearenceForm.font">
<SelectTrigger class="w-[200px]">
<SelectValue placeholder="Select a font" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="inter">
Inter
</SelectItem>
<SelectItem value="manrope">
Manrope
</SelectItem>
<SelectItem value="system">
System
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<span class="text-muted-foreground text-xs">
Set the font you want to use in the dashboard.
</span>
<div v-if="errors?.font" class="text-sm text-destructive">
<span v-for="error in errors.font._errors" :key="error">{{ error }}</span>
</div>
</div>
<form class="space-y-8" @submit="onSubmit">
<FormField v-slot="{ field }" name="font">
<FormItem>
<FormLabel>Email</FormLabel>
<div class="relative w-[200px]">
<FormControl>
<select
:class="cn(
buttonVariants({ variant: 'outline' }),
'w-[200px] appearance-none bg-transparent font-normal',
)"
v-bind="field"
>
<option value="inter">
Inter
</option>
<option value="manrope">
Manrope
</option>
<option value="system">
System
</option>
</select>
</FormControl>
<ChevronDownIcon class="absolute right-3 top-2.5 h-4 w-4 opacity-50" />
</div>
<FormDescription>
Set the font you want to use in the dashboard.
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<div class="grid gap-2">
<Label for="theme" :class="cn('text-sm', errors?.theme && 'text-destructive')">
Theme
</Label>
<span class="text-muted-foreground text-xs">
Select the theme for the dashboard.
</span>
<RadioGroup
v-model="appearenceForm.theme"
default-value="light"
class="grid max-w-md grid-cols-2 gap-8 pt-2"
>
<div class="grid gap-2">
<Label class="[&:has([data-state=checked])>div]:border-primary">
<div>
<RadioGroupItem value="light" class="sr-only" />
</div>
<div class="items-center rounded-md border-2 border-muted p-1 hover:border-accent">
<div class="space-y-2 rounded-sm bg-[#ecedef] p-2">
<div class="space-y-2 rounded-md bg-white p-2 shadow-sm">
<div class="h-2 w-[80px] rounded-lg bg-[#ecedef]" />
<div class="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
</div>
<div class="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm">
<div class="h-4 w-4 rounded-full bg-[#ecedef]" />
<div class="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
</div>
<div class="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm">
<div class="h-4 w-4 rounded-full bg-[#ecedef]" />
<div class="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
<FormField v-slot="{ componentField }" type="radio" name="theme">
<FormItem class="space-y-1">
<FormLabel>Theme</FormLabel>
<FormDescription>
Select the theme for the dashboard.
</FormDescription>
<FormMessage />
<RadioGroup
class="grid max-w-md grid-cols-2 gap-8 pt-2"
v-bind="componentField"
>
<FormItem>
<FormLabel class="[&:has([data-state=checked])>div]:border-primary">
<FormControl>
<RadioGroupItem value="light" class="sr-only" />
</FormControl>
<div class="items-center rounded-md border-2 border-muted p-1 hover:border-accent">
<div class="space-y-2 rounded-sm bg-[#ecedef] p-2">
<div class="space-y-2 rounded-md bg-white p-2 shadow-sm">
<div class="h-2 w-[80px] rounded-lg bg-[#ecedef]" />
<div class="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
</div>
<div class="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm">
<div class="h-4 w-4 rounded-full bg-[#ecedef]" />
<div class="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
</div>
<div class="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm">
<div class="h-4 w-4 rounded-full bg-[#ecedef]" />
<div class="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
</div>
</div>
</div>
</div>
<span class="block w-full p-2 text-center font-normal">
Light
</span>
</Label>
</div>
<div>
<Label class="[&:has([data-state=checked])>div]:border-primary">
<div>
<RadioGroupItem value="dark" class="sr-only" />
</div>
<div class="items-center rounded-md border-2 border-muted bg-popover p-1 hover:bg-accent hover:text-accent-foreground">
<div class="space-y-2 rounded-sm bg-slate-950 p-2">
<div class="space-y-2 rounded-md bg-slate-800 p-2 shadow-sm">
<div class="h-2 w-[80px] rounded-lg bg-slate-400" />
<div class="h-2 w-[100px] rounded-lg bg-slate-400" />
</div>
<div class="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm">
<div class="h-4 w-4 rounded-full bg-slate-400" />
<div class="h-2 w-[100px] rounded-lg bg-slate-400" />
</div>
<div class="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm">
<div class="h-4 w-4 rounded-full bg-slate-400" />
<div class="h-2 w-[100px] rounded-lg bg-slate-400" />
<span class="block w-full p-2 text-center font-normal">
Light
</span>
</FormLabel>
</FormItem>
<FormItem>
<FormLabel class="[&:has([data-state=checked])>div]:border-primary">
<FormControl>
<RadioGroupItem value="dark" class="sr-only" />
</FormControl>
<div class="items-center rounded-md border-2 border-muted bg-popover p-1 hover:bg-accent hover:text-accent-foreground">
<div class="space-y-2 rounded-sm bg-slate-950 p-2">
<div class="space-y-2 rounded-md bg-slate-800 p-2 shadow-sm">
<div class="h-2 w-[80px] rounded-lg bg-slate-400" />
<div class="h-2 w-[100px] rounded-lg bg-slate-400" />
</div>
<div class="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm">
<div class="h-4 w-4 rounded-full bg-slate-400" />
<div class="h-2 w-[100px] rounded-lg bg-slate-400" />
</div>
<div class="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm">
<div class="h-4 w-4 rounded-full bg-slate-400" />
<div class="h-2 w-[100px] rounded-lg bg-slate-400" />
</div>
</div>
</div>
</div>
<span class="block w-full p-2 text-center font-normal">
Dark
</span>
</Label>
</div>
<div class="col-span-2">
<span v-if="errors?.theme" class="text-sm text-destructive">
<span v-for="error in errors.theme._errors" :key="error">{{ error }}</span>
</span>
</div>
</RadioGroup>
</div>
<span class="block w-full p-2 text-center font-normal">
Dark
</span>
</FormLabel>
</FormItem>
</RadioGroup>
</FormItem>
</FormField>
<div class="flex justify-start">
<Button type="submit">

View File

@ -1,17 +1,13 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { cn } from '@/lib/utils'
import { Label } from '@/lib/registry/new-york/ui/label'
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/lib/registry/default/ui/form'
import { Separator } from '@/lib/registry/new-york/ui/separator'
import { Checkbox } from '@/lib/registry/new-york/ui/checkbox'
import { Button } from '@/lib/registry/new-york/ui/button'
const displayForm = ref({
items: ['recents', 'home'],
})
const items = [
{
id: 'recents',
@ -43,25 +39,22 @@ const items = [
},
] as const
const displayFormSchema = z.object({
const displayFormSchema = toTypedSchema(z.object({
items: z.array(z.string()).refine(value => value.some(item => item), {
message: 'You have to select at least one item.',
}),
}))
const { handleSubmit } = useForm({
validationSchema: displayFormSchema,
initialValues: {
items: ['recents', 'home'],
},
})
type DisplayFormValues = z.infer<typeof displayFormSchema>
const errors = ref<z.ZodFormattedError<DisplayFormValues> | null>(null)
async function handleSubmit() {
const result = displayFormSchema.safeParse(displayForm.value)
if (!result.success) {
errors.value = result.error.format()
console.log(errors.value)
return
}
console.log('Form submitted!', displayForm.value)
}
const onSubmit = handleSubmit((values) => {
console.log('Form submitted!', values)
})
</script>
<template>
@ -74,21 +67,39 @@ async function handleSubmit() {
</p>
</div>
<Separator />
<form @submit.prevent="handleSubmit">
<div class="mb-4">
<Label for="sidebar" :class="cn('text-md', errors?.items && 'text-destructive')">
Sidebar
</Label>
<span class="text-xs text-muted-foreground">
Select the items you want to display in the sidebar.
</span>
</div>
<div v-for="item in items" :key="item.id" class="pb-1">
<div class="flex flex-row items-center space-x-3 space-y-0">
<Checkbox :id="item.id" :checked="displayForm.items.includes(item.id)" @change="displayForm.items.includes(item.id) ? displayForm.items.splice(displayForm.items.indexOf(item.id), 1) : displayForm.items.push(item.id)" />
<Label :for="item.id">{{ item.label }}</Label>
</div>
</div>
<form @submit="onSubmit">
<FormField name="items">
<FormItem>
<div class="mb-4">
<FormLabel class="text-base">
Sidebar
</FormLabel>
<FormDescription>
Select the items you want to display in the sidebar.
</FormDescription>
</div>
<FormField v-for="item in items" v-slot="{ value, handleChange }" :key="item.id" name="items">
<FormItem :key="item.id" class="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
:checked="value.includes(item.id)"
@update:checked="(checked) => {
if (Array.isArray(value)) {
handleChange(checked ? [...value, item.id] : value.filter(id => id !== item.id))
}
}"
/>
</FormControl>
<FormLabel class="font-normal">
{{ item.label }}
</FormLabel>
</FormItem>
</FormField>
<FormMessage />
</FormItem>
</FormField>
<div class="flex justify-start mt-4">
<Button type="submit">
Update display

View File

@ -1,25 +1,16 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { cn } from '@/lib/utils'
import { Label } from '@/lib/registry/new-york/ui/label'
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'
const notificationsForm = ref({
type: '',
mobile: false,
communication_emails: false,
social_emails: true,
marketing_emails: false,
security_emails: true,
})
const notificationsFormSchema = z.object({
const notificationsFormSchema = toTypedSchema(z.object({
type: z.enum(['all', 'mentions', 'none'], {
required_error: 'You need to select a notification type.',
}),
@ -28,21 +19,21 @@ const notificationsFormSchema = z.object({
social_emails: z.boolean().default(false).optional(),
marketing_emails: z.boolean().default(false).optional(),
security_emails: z.boolean(),
}))
const { handleSubmit } = useForm({
validationSchema: notificationsFormSchema,
initialValues: {
communication_emails: false,
marketing_emails: false,
social_emails: true,
security_emails: true,
},
})
type notificationsFormValues = z.infer<typeof notificationsFormSchema>
const errors = ref<z.ZodFormattedError<notificationsFormValues> | null>(null)
async function handleSubmit() {
const result = notificationsFormSchema.safeParse(notificationsForm.value)
if (!result.success) {
errors.value = result.error.format()
console.log(errors.value)
return
}
console.log('Form submitted!', notificationsForm.value)
}
const onSubmit = handleSubmit((values, { resetForm }) => {
console.log('Form submitted!', values)
})
</script>
<template>
@ -55,117 +46,149 @@ async function handleSubmit() {
</p>
</div>
<Separator />
<form class="space-y-8" @submit.prevent="handleSubmit">
<div class="grid gap-2">
<Label for="font" :class="cn('text-sm', errors?.type && 'text-destructive')">
Notify me about...
</Label>
<RadioGroup
v-model="notificationsForm.type"
default-value="all"
class="flex flex-col space-y-1"
>
<div class="flex items-center space-x-3 space-y-0">
<RadioGroupItem id="all" value="all" />
<Label for="all">All new messages</Label>
</div>
<div class="flex items-center space-x-3 space-y-0">
<RadioGroupItem id="mentions" value="mentions" />
<Label for="mentions">Direct messages and mentions</Label>
</div>
<div class="flex items-center space-x-3 space-y-0">
<RadioGroupItem id="none" value="none" />
<Label for="none">Nothing</Label>
</div>
<div v-if="errors?.type" class="text-sm text-destructive">
<span v-for="error in errors.type._errors" :key="error">{{ error }}</span>
</div>
</RadioGroup>
</div>
<form class="space-y-8" @submit="onSubmit">
<FormField v-slot="{ componentField }" type="radio" name="type">
<FormItem class="space-y-3">
<FormLabel>Notify me about...</FormLabel>
<FormControl>
<RadioGroup
class="flex flex-col space-y-1"
v-bind="componentField"
>
<FormItem class="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="all" />
</FormControl>
<FormLabel class="font-normal">
All new messages
</FormLabel>
</FormItem>
<FormItem class="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="mentions" />
</FormControl>
<FormLabel class="font-normal">
Direct messages and mentions
</FormLabel>
</FormItem>
<FormItem class="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="none" />
</FormControl>
<FormLabel class="font-normal">
Nothing
</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<div class="grid gap-2">
<div>
<h3 class="mb-4 text-lg font-medium">
Email Notifications
</h3>
<div class="space-y-4">
<div class="flex flex-row items-center justify-between rounded-lg border p-4">
<div class="space-y-0.5">
<Label class="text-base" for="communication_emails">
Communication emails
</Label>
<span class="text-xs text-muted-foreground">
Receive emails about your account activity.
</span>
</div>
<Switch
id="communication_emails"
v-model:checked="notificationsForm.communication_emails"
<FormField v-slot="{ handleChange, value }" type="checkbox" name="communication_emails">
<FormItem class="flex flex-row items-center justify-between rounded-lg border p-4">
<div class="space-y-0.5">
<FormLabel class="text-base">
Communication emails
</FormLabel>
<FormDescription>
Receive emails about your account activity.
</FormDescription>
</div>
<FormControl>
<Switch
:checked="value"
@update:checked="handleChange"
/>
</FormControl>
</FormItem>
</FormField>
<FormField v-slot="{ handleChange, value }" type="checkbox" name="marketing_emails">
<FormItem class="flex flex-row items-center justify-between rounded-lg border p-4">
<div class="space-y-0.5">
<FormLabel class="text-base">
Marketing emails
</FormLabel>
<FormDescription>
Receive emails about new products, features, and more.
</FormDescription>
</div>
<FormControl>
<Switch
:checked="value"
@update:checked="handleChange"
/>
</FormControl>
</FormItem>
</FormField>
<FormField v-slot="{ handleChange, value }" type="checkbox" name="social_emails">
<FormItem class="flex flex-row items-center justify-between rounded-lg border p-4">
<div class="space-y-0.5">
<FormLabel class="text-base">
Social emails
</FormLabel>
<FormDescription>
Receive emails for friend requests, follows, and more.
</FormDescription>
</div>
<FormControl>
<Switch
:checked="value"
@update:checked="handleChange"
/>
</FormControl>
</FormItem>
</FormField>
<FormField v-slot="{ handleChange, value }" type="checkbox" name="security_emails">
<FormItem class="flex flex-row items-center justify-between rounded-lg border p-4">
<div class="space-y-0.5">
<FormLabel class="text-base">
Security emails
</FormLabel>
<FormDescription>
Receive emails about your account activity and security.
</FormDescription>
</div>
<FormControl>
<Switch
:checked="value"
@update:checked="handleChange"
/>
</FormControl>
</FormItem>
</FormField>
</div>
</div>
<FormField v-slot="{ handleChange, value }" type="checkbox" name="mobile">
<FormItem class="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
:checked="value"
@update:checked="handleChange"
/>
</div>
</div>
<div class="flex flex-row items-center justify-between rounded-lg border p-4">
<div class="space-y-0.5">
<Label class="text-base" for="marketing_emails">
Marketing emails
</Label>
<span class="text-xs text-muted-foreground">
Receive emails about new products, features, and more.
</span>
</div>
<Switch
id="marketing_emails"
v-model:checked="notificationsForm.marketing_emails"
/>
</div>
<div class="flex flex-row items-center justify-between rounded-lg border p-4">
<div class="space-y-0.5">
<Label class="text-base" for="social_emails">
Social emails
</Label>
<span class="text-xs text-muted-foreground">
Receive emails for friend requests, follows, and more.
</span>
</div>
<Switch
id="social_emails"
v-model:checked="notificationsForm.social_emails"
/>
</div>
<div class="flex flex-row items-center justify-between rounded-lg border p-4">
<div class="space-y-0.5">
<Label class="text-base" for="security_emails">
Security emails
</Label>
<span class="text-xs text-muted-foreground">
Receive emails about your account activity and security.
</span>
</div>
<Switch
id="security_emails"
v-model:checked="notificationsForm.security_emails"
disabled
/>
</div>
</div>
<div class="grid gap-2">
<div class="flex flex-row items-start space-x-3 space-y-0">
<Checkbox
id="mobile"
v-model:checked="notificationsForm.mobile"
/>
<div>
<Label for="mobile">
</FormControl>
<div class="space-y-1 leading-none">
<FormLabel>
Use different settings for my mobile devices
</Label>
<span class="text-xs text-muted-foreground">
You can manage your mobile notifications in the {{ " " }}
<a href="/examples/forms">mobile settings</a> page.
</span>
</FormLabel>
<FormDescription>
You can manage your mobile notifications in the
<a href="/examples/forms">
mobile settings
</a> page.
</FormDescription>
</div>
</div>
</div>
</FormItem>
</FormField>
<div class="flex justify-start">
<Button type="submit">

View File

@ -1,10 +1,13 @@
<script setup lang="ts">
import { ref } from 'vue'
import { FieldArray, useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { Cross1Icon } from '@radix-icons/vue'
import { cn } from '@/lib/utils'
import { Input } from '@/lib/registry/new-york/ui/input'
import { Label } from '@/lib/registry/new-york/ui/label'
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/lib/registry/default/ui/form'
import { Separator } from '@/lib/registry/new-york/ui/separator'
import { Textarea } from '@/lib/registry/new-york/ui/textarea'
import {
@ -19,17 +22,7 @@ import { Button } from '@/lib/registry/new-york/ui/button'
const verifiedEmails = ref(['m@example.com', 'm@google.com', 'm@support.com'])
const profileForm = ref({
username: '',
email: '',
bio: 'I own a computer.',
urls: [
{ value: 'https://shadcn.com' },
{ value: 'http://twitter.com/shadcn' },
],
})
const profileFormSchema = z.object({
const profileFormSchema = toTypedSchema(z.object({
username: z
.string()
.min(2, {
@ -51,20 +44,22 @@ const profileFormSchema = z.object({
}),
)
.optional(),
}))
const { handleSubmit, resetForm } = useForm({
validationSchema: profileFormSchema,
initialValues: {
bio: 'I own a computer.',
urls: [
{ value: 'https://shadcn.com' },
{ value: 'http://twitter.com/shadcn' },
],
},
})
type ProfileFormValues = z.infer<typeof profileFormSchema>
const errors = ref<z.ZodFormattedError<ProfileFormValues> | null>(null)
async function handleSubmit() {
const result = profileFormSchema.safeParse(profileForm.value)
if (!result.success) {
errors.value = result.error.format()
return
}
errors.value = null
console.log('Form submitted!')
}
const onSubmit = handleSubmit((values) => {
console.log('Form submitted!', values)
})
</script>
<template>
@ -77,75 +72,105 @@ async function handleSubmit() {
</p>
</div>
<Separator />
<form class="space-y-8" @submit.prevent="handleSubmit">
<div class="grid gap-2">
<Label for="username" :class="cn('text-sm', errors?.username && 'text-destructive')">
Username
</Label>
<Input id="username" v-model="profileForm.username" placeholder="shadcn" />
<span class="text-muted-foreground text-sm">
This is your public display name. It can be your real name or a pseudonym. You can only change this once every 30 days.
</span>
<div v-if="errors?.username" class="text-sm text-destructive">
<span v-for="error in errors.username._errors" :key="error">{{ error }}</span>
</div>
<form class="space-y-8" @submit="onSubmit">
<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. It can be your real name or a pseudonym. You can only change this once every 30 days.
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="email">
<FormItem>
<FormLabel>Email</FormLabel>
<Select v-bind="componentField">
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select an email" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectGroup>
<SelectItem v-for="email in verifiedEmails" :key="email" :value="email">
{{ email }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<FormDescription>
You can manage verified email addresses in your email settings.
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="bio">
<FormItem>
<FormLabel>Bio</FormLabel>
<FormControl>
<Textarea placeholder="Tell us a little bit about yourself" v-bind="componentField" />
</FormControl>
<FormDescription>
You can <span>@mention</span> other users and organizations to link to them.
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<div>
<FieldArray v-slot="{ fields, push, remove }" name="urls">
<div v-for="(field, index) in fields" :key="`urls-${field.key}`">
<FormField v-slot="{ componentField }" :name="`urls[${index}].value`">
<FormItem>
<FormLabel :class="cn(index !== 0 && 'sr-only')">
URLs
</FormLabel>
<FormDescription :class="cn(index !== 0 && 'sr-only')">
Add links to your website, blog, or social media profiles.
</FormDescription>
<div class="relative flex items-center">
<FormControl>
<Input type="url" v-bind="componentField" />
</FormControl>
<button class="absolute py-2 pe-3 end-0 text-muted-foreground" @click="remove(index)">
<Cross1Icon class="w-3" />
</button>
</div>
<FormMessage />
</FormItem>
</FormField>
</div>
<Button
type="button"
variant="outline"
size="sm"
class="text-xs w-20 mt-2"
@click="push({ value: '' })"
>
Add URL
</Button>
</FieldArray>
</div>
<div class="grid gap-2">
<Label for="email" :class="cn('text-sm', errors?.email && 'text-destructive')">
Email
</Label>
<Select id="email" v-model="profileForm.email">
<SelectTrigger>
<SelectValue placeholder="Select an email" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem v-for="email in verifiedEmails" :key="email" :value="email">
{{ email }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<span class="text-muted-foreground text-sm">
You can manage verified email addresses in your email settings.
</span>
<div v-if="errors?.email" class="text-sm text-destructive">
<span v-for="error in errors.email._errors" :key="error">{{ error }}</span>
</div>
</div>
<div class="grid gap-2">
<Label for="bio" :class="cn('text-sm', errors?.bio && 'text-destructive')">
Bio
</Label>
<Textarea id="bio" v-model="profileForm.bio" placeholder="Tell us about yourself." />
<span class="text-muted-foreground text-sm">
You can @mention other users and organizations to link to them.
</span>
<div v-if="errors?.bio" class="text-sm text-destructive">
<span v-for="error in errors.bio._errors" :key="error">{{ error }}</span>
</div>
</div>
<div class="grid gap-2">
<Label for="urls" :class="cn('text-sm', errors?.urls && 'text-destructive')">
URLs
</Label>
<Input v-for="(url, index) in profileForm.urls" id="urls" :key="index" v-model="url.value" />
<div v-if="errors?.urls" class="text-sm text-destructive">
<span v-for="error in errors.urls._errors" :key="error">{{ error }}</span>
</div>
<div class="flex gap-2 justify-start">
<Button type="submit">
Update profile
</Button>
<Button
type="button"
variant="outline"
size="sm"
class="text-xs w-20 mt-2"
@click="profileForm.urls?.push({ value: '' })"
@click="resetForm"
>
Add URL
</Button>
</div>
<div class="flex justify-start">
<Button type="submit">
Update profile
Reset form
</Button>
</div>
</form>

View File

@ -67,16 +67,16 @@ const selectedUsers = ref<User[]>([])
<template>
<Card>
<CardHeader class="flex flex-row items-center justify-between">
<div className="flex items-center space-x-4">
<div class="flex items-center space-x-4">
<Avatar>
<AvatarImage src="/avatars/01.png" alt="Image" />
<AvatarFallback>OM</AvatarFallback>
</Avatar>
<div>
<p className="text-sm font-medium leading-none">
<p class="text-sm font-medium leading-none">
Sofia Davis
</p>
<p className="text-sm text-muted-foreground">
<p class="text-sm text-muted-foreground">
m@example.com
</p>
</div>

View File

@ -169,7 +169,7 @@ const table = useVueTable({
</CardHeader>
<CardContent>
<div class="w-full">
<div className="mb-4 flex items-center gap-4">
<div class="mb-4 flex items-center gap-4">
<Input
class="max-w-sm"
placeholder="Filter emails..."

View File

@ -6,7 +6,7 @@ import { Checkbox } from '@/lib/registry/default/ui/checkbox'
<div class="flex items-center space-x-2">
<Checkbox id="terms" />
<label
htmlFor="terms"
for="terms"
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Accept terms and conditions

View File

@ -0,0 +1,102 @@
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { Button } from '@/lib/registry/default/ui/button'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/lib/registry/default/ui/form'
import { Checkbox } from '@/lib/registry/default/ui/checkbox'
const items = [
{
id: 'recents',
label: 'Recents',
},
{
id: 'home',
label: 'Home',
},
{
id: 'applications',
label: 'Applications',
},
{
id: 'desktop',
label: 'Desktop',
},
{
id: 'downloads',
label: 'Downloads',
},
{
id: 'documents',
label: 'Documents',
},
] as const
const formSchema = toTypedSchema(z.object({
items: z.array(z.string()).refine(value => value.some(item => item), {
message: 'You have to select at least one item.',
}),
}))
const { handleSubmit } = useForm({
validationSchema: formSchema,
initialValues: {
items: ['recents', 'home'],
},
})
const onSubmit = handleSubmit((values) => {
console.log('Form submitted!', values)
})
</script>
<template>
<form @submit="onSubmit">
<FormField name="items">
<FormItem>
<div class="mb-4">
<FormLabel class="text-base">
Sidebar
</FormLabel>
<FormDescription>
Select the items you want to display in the sidebar.
</FormDescription>
</div>
<FormField v-for="item in items" v-slot="{ value, handleChange }" :key="item.id" name="items">
<FormItem :key="item.id" class="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
:checked="value.includes(item.id)"
@update:checked="(checked) => {
if (Array.isArray(value)) {
handleChange(checked ? [...value, item.id] : value.filter(id => id !== item.id))
}
}"
/>
</FormControl>
<FormLabel class="font-normal">
{{ item.label }}
</FormLabel>
</FormItem>
</FormField>
<FormMessage />
</FormItem>
</FormField>
<div class="flex justify-start mt-4">
<Button type="submit">
Submit
</Button>
</div>
</form>
</template>

View File

@ -0,0 +1,54 @@
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { Button } from '@/lib/registry/default/ui/button'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/lib/registry/default/ui/form'
import { Checkbox } from '@/lib/registry/default/ui/checkbox'
const formSchema = toTypedSchema(z.object({
mobile: z.boolean().default(false).optional(),
}))
const { handleSubmit } = useForm({
validationSchema: formSchema,
initialValues: {
mobile: true,
},
})
const onSubmit = handleSubmit((values) => {
console.log('Form submitted!', values)
})
</script>
<template>
<form class="space-y-6" @submit="onSubmit">
<FormField v-slot="{ value, handleChange }" name="mobile">
<FormItem class="flex flex-row items-start gap-x-3 space-y-0 rounded-md border p-4">
<FormControl>
<Checkbox :checked="value" @update:checked="handleChange" />
</FormControl>
<div class="space-y-1 leading-none">
<FormLabel>Use different settings for my mobile devices</FormLabel>
<FormDescription>
You can manage your mobile notifications in the
<a href="/examples/forms">mobile settings</a> page.
</FormDescription>
<FormMessage />
</div>
</FormItem>
</FormField>
<Button type="submit">
Submit
</Button>
</Form>
</template>

View File

@ -0,0 +1,20 @@
<script setup lang="ts">
import { Checkbox } from '@/lib/registry/default/ui/checkbox'
</script>
<template>
<div class="items-top flex gap-x-2">
<Checkbox id="terms1" />
<div class="grid gap-1.5 leading-none">
<label
for="terms1"
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Accept terms and conditions
</label>
<p class="text-sm text-muted-foreground">
You agree to our Terms of Service and Privacy Policy.
</p>
</div>
</div>
</template>

View File

@ -0,0 +1,108 @@
<script setup lang="ts">
import { ref } from 'vue'
import { Calendar, MoreHorizontal, Tags, Trash, User } from 'lucide-vue-next'
import { Button } from '@/lib/registry/default/ui/button'
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/lib/registry/default/ui/command'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from '@/lib/registry/default/ui/dropdown-menu'
const labels = [
'feature',
'bug',
'enhancement',
'documentation',
'design',
'question',
'maintenance',
]
const labelRef = ref('feature')
const open = ref(false)
</script>
<template>
<div class="flex w-full flex-col items-start justify-between rounded-md border px-4 py-3 sm:flex-row sm:items-center">
<p class="text-sm font-medium leading-none">
<span class="mr-2 rounded-lg bg-primary px-2 py-1 text-xs text-primary-foreground">
{{ labelRef }}
</span>
<span class="text-muted-foreground">Create a new project</span>
</p>
<DropdownMenu :open="open">
<DropdownMenuTrigger as-child>
<Button variant="ghost" size="sm">
<MoreHorizontal />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" class="w-[200px]">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuGroup>
<DropdownMenuItem>
<User class="mr-2 h-4 w-4" />
Assign to...
</DropdownMenuItem>
<DropdownMenuItem>
<Calendar class="mr-2 h-4 w-4" />
Set due date...
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Tags class="mr-2 h-4 w-4" />
Apply label
</DropdownMenuSubTrigger>
<DropdownMenuSubContent class="p-0">
<Command>
<CommandInput
placeholder="Filter label..."
auto-focus
/>
<CommandList>
<CommandEmpty>No label found.</CommandEmpty>
<CommandGroup>
<CommandItem
v-for="label in labels"
:key="label"
:value="label"
@select="(value) => {
labelRef = value as string
open = false
}"
>
{{ label }}
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem class="text-red-600">
<Trash class="mr-2 h-4 w-4" />
Delete
<DropdownMenuShortcut></DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</template>

View File

@ -0,0 +1,112 @@
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { Check, ChevronsUpDown } from 'lucide-vue-next'
import { cn } from '@/lib/utils'
import { Button } from '@/lib/registry/default/ui/button'
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from '@/lib/registry/default/ui/command'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/lib/registry/default/ui/form'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/lib/registry/default/ui/popover'
const languages = [
{ label: 'English', value: 'en' },
{ label: 'French', value: 'fr' },
{ label: 'German', value: 'de' },
{ label: 'Spanish', value: 'es' },
{ label: 'Portuguese', value: 'pt' },
{ label: 'Russian', value: 'ru' },
{ label: 'Japanese', value: 'ja' },
{ label: 'Korean', value: 'ko' },
{ label: 'Chinese', value: 'zh' },
]
const formSchema = toTypedSchema(z.object({
language: z.string({
required_error: 'Please select a language.',
}),
}))
const { handleSubmit, setValues } = useForm({
validationSchema: formSchema,
})
const onSubmit = handleSubmit((values) => {
console.log('Form submitted!', values)
})
</script>
<template>
<form class="space-y-6" @submit="onSubmit">
<FormField v-slot="{ value }" name="language">
<FormItem class="flex flex-col">
<FormLabel>Language</FormLabel>
<Popover>
<PopoverTrigger as-child>
<FormControl>
<Button
variant="outline"
role="combobox"
:class="cn('w-[200px] justify-between', !value && 'text-muted-foreground')"
>
{{ value ? languages.find(
(language) => language.value === value,
)?.label : 'Select language...' }}
<ChevronsUpDown class="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent class="w-[200px] p-0">
<Command>
<CommandInput placeholder="Search language..." />
<CommandEmpty>Nothing found.</CommandEmpty>
<CommandGroup>
<CommandItem
v-for="language in languages"
:key="language.value"
:value="language.label"
@select="() => {
setValues({
language: language.value,
})
}"
>
<Check
:class="cn('mr-2 h-4 w-4', language.value === value ? 'opacity-100' : 'opacity-0')"
/>
{{ language.label }}
</CommandItem>
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
<FormDescription>
This is the language that will be used in the dashboard.
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<Button type="submit">
Submit
</Button>
</form>
</template>

View File

@ -0,0 +1,118 @@
<script setup lang="ts">
import { h, ref } from 'vue'
import {
ArrowUpCircle,
CheckCircle2,
Circle,
HelpCircle,
XCircle,
} 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 {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/lib/registry/default/ui/command'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/lib/registry/default/ui/popover'
interface Status {
value: string
label: string
icon: Icon
}
const statuses: Status[] = [
{
value: 'backlog',
label: 'Backlog',
icon: HelpCircle,
},
{
value: 'todo',
label: 'Todo',
icon: Circle,
},
{
value: 'in progress',
label: 'In Progress',
icon: ArrowUpCircle,
},
{
value: 'done',
label: 'Done',
icon: CheckCircle2,
},
{
value: 'canceled',
label: 'Canceled',
icon: XCircle,
},
]
const open = ref(false)
const value = ref<typeof statuses[number]>()
const selectedStatus = ref<Status | null>(null)
</script>
<template>
<div class="flex items-center space-x-4">
<p class="text-sm text-muted-foreground">
Status
</p>
<Popover :open="open">
<PopoverTrigger as-child>
<Button
variant="outline"
size="sm"
class="w-[150px] justify-start"
>
<template v-if="selectedStatus">
<component :is="h(selectedStatus?.icon)" class="mr-2 h-4 w-4 shrink-0" />
{{ selectedStatus?.label }}
</template>
<template v-else>
+ Set status
</template>
</Button>
</PopoverTrigger>
<PopoverContent class="p-0" side="right" align="start">
<Command>
<CommandInput placeholder="Change status..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
<CommandItem
v-for="status in statuses"
:key="status.value"
:value="status.value"
@select="(value) => {
selectedStatus = statuses.find((priority) => priority.value === value) || null
open = false
}"
>
<component
:is="h(status.icon)"
:key="status.value"
:class="cn('mr-2 h-4 w-4', status.value === selectedStatus?.value ? 'opacity-100' : 'opacity-40',
)"
/>
<span>{{ status.label }}</span>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
</template>

View File

@ -0,0 +1,74 @@
<script setup lang="ts">
import { format } from 'date-fns'
import { Calendar as CalendarIcon } from 'lucide-vue-next'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { cn } from '@/lib/utils'
import { Button } from '@/lib/registry/default/ui/button'
import { Calendar } from '@/lib/registry/default/ui/calendar'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/lib/registry/default/ui/form'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/lib/registry/default/ui/popover'
const formSchema = toTypedSchema(z.object({
dob: z.date({
required_error: 'A date of birth is required.',
}),
}))
const { handleSubmit } = useForm({
validationSchema: formSchema,
})
const onSubmit = handleSubmit((values) => {
console.log('Form submitted!', values)
})
</script>
<template>
<form class="space-y-8" @submit="onSubmit">
<FormField v-slot="{ componentField, value }" name="dob">
<FormItem class="flex flex-col">
<FormLabel>Date of birth</FormLabel>
<Popover>
<PopoverTrigger as-child>
<FormControl>
<Button
variant="outline" :class="cn(
'w-[240px] ps-3 text-start font-normal',
!value && 'text-muted-foreground',
)"
>
<span>{{ value ? format(value, "PPP") : "Pick a date" }}</span>
<CalendarIcon class="ms-auto h-4 w-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent class="p-0">
<Calendar v-bind="componentField" />
</PopoverContent>
</Popover>
<FormDescription>
Your date of birth is used to calculate your age.
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<Button type="submit">
Submit
</Button>
</Form>
</template>

View File

@ -0,0 +1,73 @@
<script setup lang="ts">
import { addDays, format } from 'date-fns'
import { Calendar as CalendarIcon } from 'lucide-vue-next'
import { ref } from 'vue'
import { cn } from '@/lib/utils'
import { Button } from '@/lib/registry/default/ui/button'
import { Calendar } from '@/lib/registry/default/ui/calendar'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/lib/registry/default/ui/popover'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/lib/registry/default/ui/select'
const date = ref<Date>()
</script>
<template>
<Popover>
<PopoverTrigger as-child>
<Button
variant="outline"
:class="cn(
'w-[280px] justify-start text-left font-normal',
!date && 'text-muted-foreground',
)"
>
<CalendarIcon class="mr-2 h-4 w-4" />
<template v-if="date">
{{ format(date, "PPP") }}
</template>
<template v-else>
<span>Pick a date</span>
</template>
</Button>
</PopoverTrigger>
<PopoverContent class="flex w-auto flex-col space-y-2 p-2">
<Select
@update:model-value="(value) => {
date = addDays(new Date(), parseInt(value))
}"
>
<SelectTrigger>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent position="popper">
<SelectItem value="0">
Today
</SelectItem>
<SelectItem value="1">
Tomorrow
</SelectItem>
<SelectItem value="3">
In 3 days
</SelectItem>
<SelectItem value="7">
In a week
</SelectItem>
</SelectContent>
</Select>
<div class="rounded-md border">
<Calendar v-model="date" mode="single" />
</div>
</PopoverContent>
</Popover>
</template>

View File

@ -0,0 +1,7 @@
<script setup lang="ts">
import { Input } from '@/lib/registry/default/ui/input'
</script>
<template>
<Input disabled type="email" placeholder="Email" />
</template>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import { Input } from '@/lib/registry/default/ui/input'
import { Label } from '@/lib/registry/default/ui/label'
</script>
<template>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Label for="picture">Picture</Label>
<Input id="picture" type="file" />
</div>
</template>

View File

@ -0,0 +1,48 @@
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { Button } from '@/lib/registry/default/ui/button'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/lib/registry/default/ui/form'
import { Input } from '@/lib/registry/default/ui/input'
const formSchema = toTypedSchema(z.object({
username: z.string().min(2).max(50),
}))
const { handleSubmit } = useForm({
validationSchema: formSchema,
})
const onSubmit = handleSubmit((values) => {
console.log('Form submitted!', values)
})
</script>
<template>
<form class="w-2/3 space-y-6" @submit="onSubmit">
<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>
<Button type="submit">
Submit
</Button>
</Form>
</template>

View File

@ -0,0 +1,13 @@
<script setup lang="ts">
import { Input } from '@/lib/registry/default/ui/input'
import { Button } from '@/lib/registry/default/ui/button'
</script>
<template>
<div class="flex w-full max-w-sm items-center gap-1.5">
<Input id="email" type="email" placeholder="Email" />
<Button type="submit">
Subscribe
</Button>
</div>
</template>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import { Input } from '@/lib/registry/default/ui/input'
import { Label } from '@/lib/registry/default/ui/label'
</script>
<template>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Label for="email">Email</Label>
<Input id="email" type="email" placeholder="Email" />
</div>
</template>

View File

@ -0,0 +1,76 @@
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { Button } from '@/lib/registry/default/ui/button'
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/lib/registry/default/ui/form'
import { RadioGroup, RadioGroupItem } from '@/lib/registry/default/ui/radio-group'
const formSchema = toTypedSchema(z.object({
type: z.enum(['all', 'mentions', 'none'], {
required_error: 'You need to select a notification type.',
}),
}))
const { handleSubmit } = useForm({
validationSchema: formSchema,
})
const onSubmit = handleSubmit((values) => {
console.log('Form submitted!', values)
})
</script>
<template>
<form className="w-2/3 space-y-6" @submit="onSubmit">
<FormField v-slot="{ componentField }" name="type">
<FormItem class="space-y-3">
<FormLabel>Notify me about...</FormLabel>
<FormControl>
<RadioGroup
class="flex flex-col space-y-1"
v-bind="componentField"
>
<FormItem class="flex items-center gap-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="all" />
</FormControl>
<FormLabel class="font-normal">
All new messages
</FormLabel>
</FormItem>
<FormItem class="flex items-center gap-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="mentions" />
</FormControl>
<FormLabel class="font-normal">
Direct messages and mentions
</FormLabel>
</FormItem>
<FormItem class="flex items-center gap-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="none" />
</FormControl>
<FormLabel class="font-normal">
Nothing
</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<Button type="submit">
Submit
</Button>
</form>
</template>

View File

@ -0,0 +1,79 @@
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { Button } from '@/lib/registry/default/ui/button'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/lib/registry/default/ui/form'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/lib/registry/default/ui/select'
const formSchema = toTypedSchema(z.object({
email: z
.string({
required_error: 'Please select an email to display.',
})
.email(),
}))
const { handleSubmit } = useForm({
validationSchema: formSchema,
})
const onSubmit = handleSubmit((values) => {
console.log('Form submitted!', values)
})
</script>
<template>
<form className="w-2/3 space-y-6" @submit="onSubmit">
<FormField v-slot="{ componentField }" name="email">
<FormItem>
<FormLabel>Email</FormLabel>
<Select v-bind="componentField">
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a verified email to display" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectGroup>
<SelectItem value="m@example.com">
m@example.com
</SelectItem>
<SelectItem value="m@google.com">
m@google.com
</SelectItem>
<SelectItem value="m@support.com">
m@support.com
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<FormDescription>
You can manage email addresses in your
<a href="/examples/forms">email settings</a>.
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<Button type="submit">
Submit
</Button>
</form>
</template>

View File

@ -0,0 +1,85 @@
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { Button } from '@/lib/registry/default/ui/button'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
} from '@/lib/registry/default/ui/form'
import { Switch } from '@/lib/registry/default/ui/switch'
const formSchema = toTypedSchema(z.object({
marketing_emails: z.boolean().default(false).optional(),
security_emails: z.boolean(),
}))
const { handleSubmit } = useForm({
validationSchema: formSchema,
initialValues: {
security_emails: true,
},
})
const onSubmit = handleSubmit((values) => {
console.log('Form submitted!', values)
})
</script>
<template>
<form class="w-full space-y-6" @submit="onSubmit">
<div>
<h3 class="mb-4 text-lg font-medium">
Email Notifications
</h3>
<div class="space-y-4">
<FormField v-slot="{ value, handleChange }" name="marketing_emails">
<FormItem class="flex flex-row items-center justify-between rounded-lg border p-4">
<div class="space-y-0.5">
<FormLabel class="text-base">
Marketing emails
</FormLabel>
<FormDescription>
Receive emails about new products, features, and more.
</FormDescription>
</div>
<FormControl>
<Switch
:checked="value"
@update:checked="handleChange"
/>
</FormControl>
</FormItem>
</FormField>
<FormField v-slot="{ value, handleChange }" name="security_emails">
<FormItem class="flex flex-row items-center justify-between rounded-lg border p-4">
<div class="space-y-0.5">
<FormLabel class="text-base">
Security emails
</FormLabel>
<FormDescription>
Receive emails about your account security.
</FormDescription>
</div>
<FormControl>
<Switch
:checked="value"
disabled
aria-readonly
@update:checked="handleChange"
/>
</FormControl>
</FormItem>
</FormField>
</div>
</div>
<Button type="submit">
Submit
</Button>
</form>
</template>

View File

@ -60,12 +60,12 @@ const invoices = [
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead class-name="w-[100px]">
<TableHead class="w-[100px]">
Invoice
</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead class-name="text-right">
<TableHead class="text-right">
Amount
</TableHead>
</TableRow>

View File

@ -0,0 +1,7 @@
<script setup lang="ts">
import { Textarea } from '@/lib/registry/default/ui/textarea'
</script>
<template>
<Textarea placeholder="Type your message here." disabled />
</template>

View File

@ -0,0 +1,59 @@
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { Button } from '@/lib/registry/default/ui/button'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/lib/registry/default/ui/form'
import { Textarea } from '@/lib/registry/default/ui/textarea'
const formSchema = toTypedSchema(z.object({
bio: z
.string()
.min(10, {
message: 'Bio must be at least 10 characters.',
})
.max(160, {
message: 'Bio must not be longer than 30 characters.',
}),
}))
const { handleSubmit } = useForm({
validationSchema: formSchema,
})
const onSubmit = handleSubmit((values) => {
console.log('Form submitted!', values)
})
</script>
<template>
<form class="w-full space-y-6" @submit="onSubmit">
<FormField v-slot="{ componentField }" name="bio">
<FormItem>
<FormLabel>Bio</FormLabel>
<FormControl>
<Textarea
placeholder="Tell us a little bit about yourself"
class="resize-none"
v-bind="componentField"
/>
</FormControl>
<FormDescription>
You can <span>@mention</span> other users and organizations.
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<Button type="submit">
Submit
</Button>
</form>
</template>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import { Textarea } from '@/lib/registry/default/ui/textarea'
import { Button } from '@/lib/registry/default/ui/button'
</script>
<template>
<div class="grid w-full gap-2">
<Textarea placeholder="Type your message here." />
<Button>Send message</Button>
</div>
</template>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import { Textarea } from '@/lib/registry/default/ui/textarea'
import { Label } from '@/lib/registry/default/ui/label'
</script>
<template>
<div class="grid w-full gap-1.5">
<Label for="message">Your message</Label>
<Textarea id="message" placeholder="Type your message here." />
</div>
</template>

View File

@ -0,0 +1,14 @@
<script setup lang="ts">
import { Textarea } from '@/lib/registry/default/ui/textarea'
import { Label } from '@/lib/registry/default/ui/label'
</script>
<template>
<div class="grid w-full gap-1.5">
<Label for="message-2">Your message</Label>
<Textarea id="message-2" placeholder="Type your message here." />
<p className="text-sm text-muted-foreground">
Your message will be copied to the support team.
</p>
</div>
</template>

View File

@ -0,0 +1,16 @@
<script lang="ts" setup>
import { Slot } from 'radix-vue'
import { useFormField } from './useFormField'
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
</script>
<template>
<Slot
:id="formItemId"
:aria-describedby="!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`"
:aria-invalid="!!error"
>
<slot />
</Slot>
</template>

View File

@ -0,0 +1,22 @@
<script lang="ts" setup>
import { useAttrs } from 'vue'
import { useFormField } from './useFormField'
import { cn } from '@/lib/utils'
defineOptions({
inheritAttrs: false,
})
const { formDescriptionId } = useFormField()
const { class: className, ...rest } = useAttrs()
</script>
<template>
<p
:id="formDescriptionId"
:class="cn('text-sm text-muted-foreground', className ?? '')"
v-bind="rest"
>
<slot />
</p>
</template>

View File

@ -0,0 +1,27 @@
<script lang="ts">
import { type InjectionKey } from 'vue'
export const FORMI_TEM_INJECTION_KEY
= Symbol() as InjectionKey<string>
</script>
<script lang="ts" setup>
import { provide, useAttrs } from 'vue'
import { useId } from 'radix-vue'
import { cn } from '@/lib/utils'
defineOptions({
inheritAttrs: false,
})
const id = useId()
provide(FORMI_TEM_INJECTION_KEY, id)
const { class: className, ...rest } = useAttrs()
</script>
<template>
<div :class="cn('space-y-2', className ?? '')" v-bind="rest">
<slot />
</div>
</template>

View File

@ -0,0 +1,28 @@
<script lang="ts" setup>
import { useAttrs } from 'vue'
import { Label, type LabelProps } from 'radix-vue'
import { useFormField } from './useFormField'
import { cn } from '@/lib/utils'
defineOptions({
inheritAttrs: false,
})
const props = defineProps<LabelProps>()
const { error, formItemId } = useFormField()
const { class: className, ...rest } = useAttrs()
</script>
<template>
<Label
:class="cn(
'block text-sm tracking-tight font-medium text-foreground text-left',
error && 'text-destructive',
className ?? '',
)"
:for="formItemId"
v-bind="rest"
>
<slot />
</Label>
</template>

View File

@ -0,0 +1,16 @@
<script lang="ts" setup>
import { ErrorMessage } from 'vee-validate'
import { toValue } from 'vue'
import { useFormField } from './useFormField'
const { name, formMessageId } = useFormField()
</script>
<template>
<ErrorMessage
:id="formMessageId"
as="p"
:name="toValue(name)"
class="text-sm font-medium text-destructive"
/>
</template>

View File

@ -0,0 +1,6 @@
export { Form, Field as FormField } from 'vee-validate'
export { default as FormItem } from './FormItem.vue'
export { default as FormLabel } from './FormLabel.vue'
export { default as FormControl } from './FormControl.vue'
export { default as FormMessage } from './FormMessage.vue'
export { default as FormDescription } from './FormDescription.vue'

View File

@ -0,0 +1,30 @@
import { FieldContextKey, useFieldError, useIsFieldDirty, useIsFieldTouched, useIsFieldValid } from 'vee-validate'
import { inject } from 'vue'
import { FORMI_TEM_INJECTION_KEY } from './FormItem.vue'
export function useFormField() {
const fieldContext = inject(FieldContextKey)
const fieldItemContext = inject(FORMI_TEM_INJECTION_KEY)
const fieldState = {
valid: useIsFieldValid(),
isDirty: useIsFieldDirty(),
isTouched: useIsFieldTouched(),
error: useFieldError(),
}
if (!fieldContext)
throw new Error('useFormField should be used within <FormField>')
const { name } = fieldContext
const id = fieldItemContext
return {
id,
name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}

View File

@ -169,7 +169,7 @@ const table = useVueTable({
</CardHeader>
<CardContent>
<div class="w-full">
<div className="mb-4 flex items-center gap-4">
<div class="mb-4 flex items-center gap-4">
<Input
class="max-w-sm"
placeholder="Filter emails..."

View File

@ -6,7 +6,7 @@ import { Checkbox } from '@/lib/registry/new-york/ui/checkbox'
<div class="flex items-center space-x-2">
<Checkbox id="terms" />
<label
htmlFor="terms"
for="terms"
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Accept terms and conditions

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
import { Checkbox } from '@/lib/registry/new-york/ui/checkbox'
</script>
<template>
<div class="items-top flex space-x-2">
<Checkbox id="terms1" disabled />
<label
for="terms2"
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Accept terms and conditions
</label>
</div>
</template>

View File

@ -0,0 +1,102 @@
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { Button } from '@/lib/registry/new-york/ui/button'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/lib/registry/new-york/ui/form'
import { Checkbox } from '@/lib/registry/new-york/ui/checkbox'
const items = [
{
id: 'recents',
label: 'Recents',
},
{
id: 'home',
label: 'Home',
},
{
id: 'applications',
label: 'Applications',
},
{
id: 'desktop',
label: 'Desktop',
},
{
id: 'downloads',
label: 'Downloads',
},
{
id: 'documents',
label: 'Documents',
},
] as const
const formSchema = toTypedSchema(z.object({
items: z.array(z.string()).refine(value => value.some(item => item), {
message: 'You have to select at least one item.',
}),
}))
const { handleSubmit } = useForm({
validationSchema: formSchema,
initialValues: {
items: ['recents', 'home'],
},
})
const onSubmit = handleSubmit((values) => {
console.log('Form submitted!', values)
})
</script>
<template>
<form @submit="onSubmit">
<FormField name="items">
<FormItem>
<div class="mb-4">
<FormLabel class="text-base">
Sidebar
</FormLabel>
<FormDescription>
Select the items you want to display in the sidebar.
</FormDescription>
</div>
<FormField v-for="item in items" v-slot="{ value, handleChange }" :key="item.id" name="items">
<FormItem :key="item.id" class="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
:checked="value.includes(item.id)"
@update:checked="(checked) => {
if (Array.isArray(value)) {
handleChange(checked ? [...value, item.id] : value.filter(id => id !== item.id))
}
}"
/>
</FormControl>
<FormLabel class="font-normal">
{{ item.label }}
</FormLabel>
</FormItem>
</FormField>
<FormMessage />
</FormItem>
</FormField>
<div class="flex justify-start mt-4">
<Button type="submit">
Submit
</Button>
</div>
</form>
</template>

View File

@ -0,0 +1,54 @@
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { Button } from '@/lib/registry/new-york/ui/button'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/lib/registry/new-york/ui/form'
import { Checkbox } from '@/lib/registry/new-york/ui/checkbox'
const formSchema = toTypedSchema(z.object({
mobile: z.boolean().default(false).optional(),
}))
const { handleSubmit } = useForm({
validationSchema: formSchema,
initialValues: {
mobile: true,
},
})
const onSubmit = handleSubmit((values) => {
console.log('Form submitted!', values)
})
</script>
<template>
<form class="space-y-6" @submit="onSubmit">
<FormField v-slot="{ value, handleChange }" name="mobile">
<FormItem class="flex flex-row items-start gap-x-3 space-y-0 rounded-md border p-4 shadow">
<FormControl>
<Checkbox :checked="value" @update:checked="handleChange" />
</FormControl>
<div class="space-y-1 leading-none">
<FormLabel>Use different settings for my mobile devices</FormLabel>
<FormDescription>
You can manage your mobile notifications in the
<a href="/examples/forms">mobile settings</a> page.
</FormDescription>
<FormMessage />
</div>
</FormItem>
</FormField>
<Button type="submit">
Submit
</Button>
</Form>
</template>

View File

@ -0,0 +1,20 @@
<script setup lang="ts">
import { Checkbox } from '@/lib/registry/new-york/ui/checkbox'
</script>
<template>
<div class="items-top flex gap-x-2">
<Checkbox id="terms1" />
<div class="grid gap-1.5 leading-none">
<label
for="terms1"
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Accept terms and conditions
</label>
<p class="text-sm text-muted-foreground">
You agree to our Terms of Service and Privacy Policy.
</p>
</div>
</div>
</template>

View File

@ -3,19 +3,19 @@ import { ref } from 'vue'
import { CaretSortIcon, CheckIcon } from '@radix-icons/vue'
import { cn } from '@/lib/utils'
import { Button } from '@/lib/registry/default/ui/button'
import { Button } from '@/lib/registry/new-york/ui/button'
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from '@/lib/registry/default/ui/command'
} from '@/lib/registry/new-york/ui/command'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/lib/registry/default/ui/popover'
} from '@/lib/registry/new-york/ui/popover'
const frameworks = [
{ value: 'next.js', label: 'Next.js' },

View File

@ -0,0 +1,104 @@
<script setup lang="ts">
import { ref } from 'vue'
import { DotsHorizontalIcon } from '@radix-icons/vue'
import { Button } from '@/lib/registry/new-york/ui/button'
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/lib/registry/new-york/ui/command'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from '@/lib/registry/new-york/ui/dropdown-menu'
const labels = [
'feature',
'bug',
'enhancement',
'documentation',
'design',
'question',
'maintenance',
]
const labelRef = ref('feature')
const open = ref(false)
</script>
<template>
<div class="flex w-full flex-col items-start justify-between rounded-md border px-4 py-3 sm:flex-row sm:items-center">
<p class="text-sm font-medium leading-none">
<span class="mr-2 rounded-lg bg-primary px-2 py-1 text-xs text-primary-foreground">
{{ labelRef }}
</span>
<span class="text-muted-foreground">Create a new project</span>
</p>
<DropdownMenu :open="open">
<DropdownMenuTrigger as-child>
<Button variant="ghost" size="sm">
<DotsHorizontalIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" class="w-[200px]">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuGroup>
<DropdownMenuItem>
Assign to...
</DropdownMenuItem>
<DropdownMenuItem>
Set due date...
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger>
Apply label
</DropdownMenuSubTrigger>
<DropdownMenuSubContent class="p-0">
<Command>
<CommandInput
placeholder="Filter label..."
auto-focus
/>
<CommandList>
<CommandEmpty>No label found.</CommandEmpty>
<CommandGroup>
<CommandItem
v-for="label in labels"
:key="label"
:value="label"
@select="(value) => {
labelRef = value as string
open = false
}"
>
{{ label }}
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem class="text-red-600">
Delete
<DropdownMenuShortcut></DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</template>

View File

@ -0,0 +1,115 @@
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { CaretSortIcon, CheckIcon } from '@radix-icons/vue'
import { cn } from '@/lib/utils'
import { Button } from '@/lib/registry/new-york/ui/button'
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from '@/lib/registry/new-york/ui/command'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/lib/registry/new-york/ui/form'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/lib/registry/new-york/ui/popover'
const languages = [
{ label: 'English', value: 'en' },
{ label: 'French', value: 'fr' },
{ label: 'German', value: 'de' },
{ label: 'Spanish', value: 'es' },
{ label: 'Portuguese', value: 'pt' },
{ label: 'Russian', value: 'ru' },
{ label: 'Japanese', value: 'ja' },
{ label: 'Korean', value: 'ko' },
{ label: 'Chinese', value: 'zh' },
] as const
const formSchema = toTypedSchema(z.object({
language: z.string({
required_error: 'Please select a language.',
}),
}))
const { handleSubmit, setValues } = useForm({
validationSchema: formSchema,
initialValues: {
language: '',
},
})
const onSubmit = handleSubmit((values) => {
console.log('Form submitted!', values)
})
</script>
<template>
<form class="space-y-6" @submit="onSubmit">
<FormField v-slot="{ value }" name="language">
<FormItem class="flex flex-col">
<FormLabel>Language</FormLabel>
<Popover>
<PopoverTrigger as-child>
<FormControl>
<Button
variant="outline"
role="combobox"
:class="cn('w-[200px] justify-between', !value && 'text-muted-foreground')"
>
{{ value ? languages.find(
(language) => language.value === value,
)?.label : 'Select language...' }}
<CaretSortIcon class="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent class="w-[200px] p-0">
<Command>
<CommandInput placeholder="Search language..." />
<CommandEmpty>Nothing found.</CommandEmpty>
<CommandGroup>
<CommandItem
v-for="language in languages"
:key="language.value"
:value="language.label"
@select="() => {
setValues({
language: language.value,
})
}"
>
{{ language.label }}
<CheckIcon
:class="cn('ml-auto h-4 w-4', language.value === value ? 'opacity-100' : 'opacity-0')"
/>
</CommandItem>
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
<FormDescription>
This is the language that will be used in the dashboard.
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<Button type="submit">
Submit
</Button>
</form>
</template>

View File

@ -0,0 +1,94 @@
<script setup lang="ts">
import { ref } from 'vue'
import { Button } from '@/lib/registry/new-york/ui/button'
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/lib/registry/new-york/ui/command'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/lib/registry/new-york/ui/popover'
interface Status {
value: string
label: string
}
const statuses: Status[] = [
{
value: 'backlog',
label: 'Backlog',
},
{
value: 'todo',
label: 'Todo',
},
{
value: 'in progress',
label: 'In Progress',
},
{
value: 'done',
label: 'Done',
},
{
value: 'canceled',
label: 'Canceled',
},
]
const open = ref(false)
const selectedStatus = ref<Status | null>(null)
</script>
<template>
<div class="flex items-center space-x-4">
<p class="text-sm text-muted-foreground">
Status
</p>
<Popover :open="open">
<PopoverTrigger as-child>
<Button
variant="outline"
size="sm"
class="w-[150px] justify-start"
>
<template v-if="selectedStatus">
{{ selectedStatus?.label }}
</template>
<template v-else>
+ Set status
</template>
</Button>
</PopoverTrigger>
<PopoverContent class="p-0" side="right" align="start">
<Command>
<CommandInput placeholder="Change status..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
<CommandItem
v-for="status in statuses"
:key="status.value"
:value="status"
@select="(value) => {
selectedStatus = statuses.find((priority) => priority.value === value) || null
open = false
}"
>
{{ status.label }}
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
</template>

View File

@ -0,0 +1,74 @@
<script setup lang="ts">
import { format } from 'date-fns'
import { CalendarIcon } from '@radix-icons/vue'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { cn } from '@/lib/utils'
import { Button } from '@/lib/registry/new-york/ui/button'
import { Calendar } from '@/lib/registry/new-york/ui/calendar'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/lib/registry/new-york/ui/form'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/lib/registry/new-york/ui/popover'
const formSchema = toTypedSchema(z.object({
dob: z.date({
required_error: 'A date of birth is required.',
}),
}))
const { handleSubmit } = useForm({
validationSchema: formSchema,
})
const onSubmit = handleSubmit((values) => {
console.log('Form submitted!', values)
})
</script>
<template>
<form class="space-y-8" @submit="onSubmit">
<FormField v-slot="{ componentField, value }" name="dob">
<FormItem class="flex flex-col">
<FormLabel>Date of birth</FormLabel>
<Popover>
<PopoverTrigger as-child>
<FormControl>
<Button
variant="outline" :class="cn(
'w-[240px] ps-3 text-start font-normal',
!value && 'text-muted-foreground',
)"
>
<span>{{ value ? format(value, "PPP") : "Pick a date" }}</span>
<CalendarIcon class="ms-auto h-4 w-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent class="p-0">
<Calendar v-bind="componentField" />
</PopoverContent>
</Popover>
<FormDescription>
Your date of birth is used to calculate your age.
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<Button type="submit">
Submit
</Button>
</Form>
</template>

View File

@ -0,0 +1,73 @@
<script setup lang="ts">
import { addDays, format } from 'date-fns'
import { Calendar as CalendarIcon } from 'lucide-vue-next'
import { ref } from 'vue'
import { cn } from '@/lib/utils'
import { Button } from '@/lib/registry/new-york/ui/button'
import { Calendar } from '@/lib/registry/new-york/ui/calendar'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/lib/registry/new-york/ui/popover'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/lib/registry/new-york/ui/select'
const date = ref<Date>()
</script>
<template>
<Popover>
<PopoverTrigger as-child>
<Button
variant="outline"
:class="cn(
'w-[280px] justify-start text-left font-normal',
!date && 'text-muted-foreground',
)"
>
<CalendarIcon class="mr-2 h-4 w-4" />
<template v-if="date">
{{ format(date, "PPP") }}
</template>
<template v-else>
<span>Pick a date</span>
</template>
</Button>
</PopoverTrigger>
<PopoverContent class="flex w-auto flex-col space-y-2 p-2">
<Select
@update:model-value="(value) => {
date = addDays(new Date(), parseInt(value))
}"
>
<SelectTrigger>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent position="popper">
<SelectItem value="0">
Today
</SelectItem>
<SelectItem value="1">
Tomorrow
</SelectItem>
<SelectItem value="3">
In 3 days
</SelectItem>
<SelectItem value="7">
In a week
</SelectItem>
</SelectContent>
</Select>
<div class="rounded-md border">
<Calendar v-model="date" mode="single" />
</div>
</PopoverContent>
</Popover>
</template>

View File

@ -0,0 +1,7 @@
<script setup lang="ts">
import { Input } from '@/lib/registry/new-york/ui/input'
</script>
<template>
<Input disabled type="email" placeholder="Email" />
</template>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import { Input } from '@/lib/registry/new-york/ui/input'
import { Label } from '@/lib/registry/new-york/ui/label'
</script>
<template>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Label for="picture">Picture</Label>
<Input id="picture" type="file" />
</div>
</template>

View File

@ -0,0 +1,48 @@
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { Button } from '@/lib/registry/new-york/ui/button'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/lib/registry/new-york/ui/form'
import { Input } from '@/lib/registry/new-york/ui/input'
const formSchema = toTypedSchema(z.object({
username: z.string().min(2).max(50),
}))
const { isFieldDirty, handleSubmit } = useForm({
validationSchema: formSchema,
})
const onSubmit = handleSubmit((values) => {
console.log('Form submitted!', values)
})
</script>
<template>
<form class="w-2/3 space-y-6" @submit="onSubmit">
<FormField v-slot="{ componentField }" name="username" :validate-on-blur="!isFieldDirty">
<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>
<Button type="submit">
Submit
</Button>
</Form>
</template>

View File

@ -0,0 +1,13 @@
<script setup lang="ts">
import { Input } from '@/lib/registry/new-york/ui/input'
import { Button } from '@/lib/registry/new-york/ui/button'
</script>
<template>
<div class="flex w-full max-w-sm items-center gap-1.5">
<Input id="email" type="email" placeholder="Email" />
<Button type="submit">
Subscribe
</Button>
</div>
</template>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import { Input } from '@/lib/registry/new-york/ui/input'
import { Label } from '@/lib/registry/new-york/ui/label'
</script>
<template>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Label for="email">Email</Label>
<Input id="email" type="email" placeholder="Email" />
</div>
</template>

View File

@ -0,0 +1,76 @@
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { Button } from '@/lib/registry/new-york/ui/button'
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/lib/registry/new-york/ui/form'
import { RadioGroup, RadioGroupItem } from '@/lib/registry/new-york/ui/radio-group'
const formSchema = toTypedSchema(z.object({
type: z.enum(['all', 'mentions', 'none'], {
required_error: 'You need to select a notification type.',
}),
}))
const { handleSubmit } = useForm({
validationSchema: formSchema,
})
const onSubmit = handleSubmit((values) => {
console.log('Form submitted!', values)
})
</script>
<template>
<form className="w-2/3 space-y-6" @submit="onSubmit">
<FormField v-slot="{ componentField }" name="type">
<FormItem class="space-y-3">
<FormLabel>Notify me about...</FormLabel>
<FormControl>
<RadioGroup
class="flex flex-col space-y-1"
v-bind="componentField"
>
<FormItem class="flex items-center gap-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="all" />
</FormControl>
<FormLabel class="font-normal">
All new messages
</FormLabel>
</FormItem>
<FormItem class="flex items-center gap-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="mentions" />
</FormControl>
<FormLabel class="font-normal">
Direct messages and mentions
</FormLabel>
</FormItem>
<FormItem class="flex items-center gap-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="none" />
</FormControl>
<FormLabel class="font-normal">
Nothing
</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<Button type="submit">
Submit
</Button>
</form>
</template>

View File

@ -0,0 +1,79 @@
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { Button } from '@/lib/registry/new-york/ui/button'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/lib/registry/new-york/ui/form'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/lib/registry/new-york/ui/select'
const formSchema = toTypedSchema(z.object({
email: z
.string({
required_error: 'Please select an email to display.',
})
.email(),
}))
const { handleSubmit } = useForm({
validationSchema: formSchema,
})
const onSubmit = handleSubmit((values) => {
console.log('Form submitted!', values)
})
</script>
<template>
<form className="w-2/3 space-y-6" @submit="onSubmit">
<FormField v-slot="{ componentField }" name="email">
<FormItem>
<FormLabel>Email</FormLabel>
<Select v-bind="componentField">
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a verified email to display" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectGroup>
<SelectItem value="m@example.com">
m@example.com
</SelectItem>
<SelectItem value="m@google.com">
m@google.com
</SelectItem>
<SelectItem value="m@support.com">
m@support.com
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<FormDescription>
You can manage email addresses in your
<a href="/examples/forms">email settings</a>.
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<Button type="submit">
Submit
</Button>
</form>
</template>

View File

@ -0,0 +1,85 @@
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { Button } from '@/lib/registry/new-york/ui/button'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
} from '@/lib/registry/new-york/ui/form'
import { Switch } from '@/lib/registry/new-york/ui/switch'
const formSchema = toTypedSchema(z.object({
marketing_emails: z.boolean().default(false).optional(),
security_emails: z.boolean(),
}))
const { handleSubmit } = useForm({
validationSchema: formSchema,
initialValues: {
security_emails: true,
},
})
const onSubmit = handleSubmit((values) => {
console.log('Form submitted!', values)
})
</script>
<template>
<form class="w-full space-y-6" @submit="onSubmit">
<div>
<h3 class="mb-4 text-lg font-medium">
Email Notifications
</h3>
<div class="space-y-4">
<FormField v-slot="{ value, handleChange }" name="marketing_emails">
<FormItem class="flex flex-row items-center justify-between rounded-lg border p-4">
<div class="space-y-0.5">
<FormLabel class="text-base">
Marketing emails
</FormLabel>
<FormDescription>
Receive emails about new products, features, and more.
</FormDescription>
</div>
<FormControl>
<Switch
:checked="value"
@update:checked="handleChange"
/>
</FormControl>
</FormItem>
</FormField>
<FormField v-slot="{ value, handleChange }" name="security_emails">
<FormItem class="flex flex-row items-center justify-between rounded-lg border p-4">
<div class="space-y-0.5">
<FormLabel class="text-base">
Security emails
</FormLabel>
<FormDescription>
Receive emails about your account security.
</FormDescription>
</div>
<FormControl>
<Switch
:checked="value"
disabled
aria-readonly
@update:checked="handleChange"
/>
</FormControl>
</FormItem>
</FormField>
</div>
</div>
<Button type="submit">
Submit
</Button>
</form>
</template>

View File

@ -60,12 +60,12 @@ const invoices = [
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead class-name="w-[100px]">
<TableHead class="w-[100px]">
Invoice
</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead class-name="text-right">
<TableHead class="text-right">
Amount
</TableHead>
</TableRow>

View File

@ -0,0 +1,7 @@
<script setup lang="ts">
import { Textarea } from '@/lib/registry/new-york/ui/textarea'
</script>
<template>
<Textarea placeholder="Type your message here." disabled />
</template>

View File

@ -0,0 +1,59 @@
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { Button } from '@/lib/registry/new-york/ui/button'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/lib/registry/new-york/ui/form'
import { Textarea } from '@/lib/registry/new-york/ui/textarea'
const formSchema = toTypedSchema(z.object({
bio: z
.string()
.min(10, {
message: 'Bio must be at least 10 characters.',
})
.max(160, {
message: 'Bio must not be longer than 30 characters.',
}),
}))
const { handleSubmit } = useForm({
validationSchema: formSchema,
})
const onSubmit = handleSubmit((values) => {
console.log('Form submitted!', values)
})
</script>
<template>
<form class="w-full space-y-6" @submit="onSubmit">
<FormField v-slot="{ componentField }" name="bio">
<FormItem>
<FormLabel>Bio</FormLabel>
<FormControl>
<Textarea
placeholder="Tell us a little bit about yourself"
class="resize-none"
v-bind="componentField"
/>
</FormControl>
<FormDescription>
You can <span>@mention</span> other users and organizations.
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<Button type="submit">
Submit
</Button>
</form>
</template>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import { Textarea } from '@/lib/registry/new-york/ui/textarea'
import { Button } from '@/lib/registry/new-york/ui/button'
</script>
<template>
<div class="grid w-full gap-2">
<Textarea placeholder="Type your message here." />
<Button>Send message</Button>
</div>
</template>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import { Textarea } from '@/lib/registry/new-york/ui/textarea'
import { Label } from '@/lib/registry/new-york/ui/label'
</script>
<template>
<div class="grid w-full gap-1.5">
<Label for="message">Your message</Label>
<Textarea id="message" placeholder="Type your message here." />
</div>
</template>

View File

@ -0,0 +1,14 @@
<script setup lang="ts">
import { Textarea } from '@/lib/registry/new-york/ui/textarea'
import { Label } from '@/lib/registry/new-york/ui/label'
</script>
<template>
<div class="grid w-full gap-1.5">
<Label for="message-2">Your message</Label>
<Textarea id="message-2" placeholder="Type your message here." />
<p className="text-sm text-muted-foreground">
Your message will be copied to the support team.
</p>
</div>
</template>

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { AlertDialogAction, type AlertDialogActionProps } from 'radix-vue'
import { cn } from '@/lib/utils'
import { buttonVariants } from '@/lib/registry/default/ui/button'
import { buttonVariants } from '@/lib/registry/new-york/ui/button'
const props = defineProps<AlertDialogActionProps>()
</script>

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { AlertDialogCancel, type AlertDialogCancelProps } from 'radix-vue'
import { cn } from '@/lib/utils'
import { buttonVariants } from '@/lib/registry/default/ui/button'
import { buttonVariants } from '@/lib/registry/new-york/ui/button'
const props = defineProps<AlertDialogCancelProps>()
</script>

View File

@ -0,0 +1,16 @@
<script lang="ts" setup>
import { Slot } from 'radix-vue'
import { useFormField } from './useFormField'
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
</script>
<template>
<Slot
:id="formItemId"
:aria-describedby="!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`"
:aria-invalid="!!error"
>
<slot />
</Slot>
</template>

View File

@ -0,0 +1,15 @@
<script lang="ts" setup>
import { useFormField } from './useFormField'
import { cn } from '@/lib/utils'
const { formDescriptionId } = useFormField()
</script>
<template>
<p
:id="formDescriptionId"
:class="cn('text-[0.8rem] text-muted-foreground', $attrs.class ?? '')"
>
<slot />
</p>
</template>

View File

@ -0,0 +1,21 @@
<script lang="ts">
import { type InjectionKey } from 'vue'
export const FORMI_TEM_INJECTION_KEY
= Symbol() as InjectionKey<string>
</script>
<script lang="ts" setup>
import { provide } from 'vue'
import { useId } from 'radix-vue'
import { cn } from '@/lib/utils'
const id = useId()
provide(FORMI_TEM_INJECTION_KEY, id)
</script>
<template>
<div :class="cn('space-y-2', $attrs.class ?? '')">
<slot />
</div>
</template>

View File

@ -0,0 +1,22 @@
<script lang="ts" setup>
import { Label, type LabelProps } from 'radix-vue'
import { useFormField } from './useFormField'
import { cn } from '@/lib/utils'
const props = defineProps<LabelProps>()
const { error, formItemId } = useFormField()
</script>
<template>
<Label
:class="cn(
'block text-sm tracking-tight font-medium text-foreground text-left',
error && 'text-destructive',
$attrs.class ?? '',
)"
:for="formItemId"
>
<slot />
</Label>
</template>

View File

@ -0,0 +1,16 @@
<script lang="ts" setup>
import { ErrorMessage } from 'vee-validate'
import { toValue } from 'vue'
import { useFormField } from './useFormField'
const { name, formMessageId } = useFormField()
</script>
<template>
<ErrorMessage
:id="formMessageId"
as="p"
:name="toValue(name)"
class="text-[0.8rem] font-medium text-destructive"
/>
</template>

View File

@ -0,0 +1,6 @@
export { Form, Field as FormField } from 'vee-validate'
export { default as FormItem } from './FormItem.vue'
export { default as FormLabel } from './FormLabel.vue'
export { default as FormControl } from './FormControl.vue'
export { default as FormMessage } from './FormMessage.vue'
export { default as FormDescription } from './FormDescription.vue'

View File

@ -0,0 +1,30 @@
import { FieldContextKey, useFieldError, useIsFieldDirty, useIsFieldTouched, useIsFieldValid } from 'vee-validate'
import { inject } from 'vue'
import { FORMI_TEM_INJECTION_KEY } from './FormItem.vue'
export function useFormField() {
const fieldContext = inject(FieldContextKey)
const fieldItemContext = inject(FORMI_TEM_INJECTION_KEY)
const fieldState = {
valid: useIsFieldValid(),
isDirty: useIsFieldDirty(),
isTouched: useIsFieldTouched(),
error: useFieldError(),
}
if (!fieldContext)
throw new Error('useFormField should be used within <FormField>')
const { name } = fieldContext
const id = fieldItemContext
return {
id,
name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}

View File

@ -1,23 +1,23 @@
{
"include": ["/**/*.vue", ".vitepress/**/*.vue", "/**/*.ts", ".vitepress/**/*.mts", ".vitepress/**/*.vue", "src/lib/**/*"],
"exclude": ["node_modules", "./scripts/build-registry.ts"],
"compilerOptions": {
"target": "esnext",
"lib": ["esnext", "dom"],
"jsx": "preserve",
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"declaration": false,
"lib": ["esnext", "dom"],
"baseUrl": ".",
"skipLibCheck": true,
"outDir": "dist",
"types": ["unplugin-icons/types/vue", "node"],
"paths": {
"@/*": ["./src/*"]
}
}
},
"types": ["unplugin-icons/types/vue", "node"],
"resolveJsonModule": true,
"declaration": false,
"sourceMap": true,
"outDir": "dist",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true
},
"include": ["/**/*.vue", "src", ".vitepress/**/*.vue", "/**/*.ts", ".vitepress/**/*.mts", ".vitepress/**/*.vue", "src/lib/**/*"],
"exclude": ["node_modules", "./scripts/build-registry.ts"]
}

View File

@ -2,7 +2,7 @@
"name": "shadcn-vue",
"version": "0.3.2",
"private": true,
"packageManager": "pnpm@8.7.5",
"packageManager": "pnpm@8.8.0",
"license": "MIT",
"repository": "radix-vue/shadcn-vue",
"workspaces": [
@ -29,14 +29,14 @@
"bumpp": "^9.2.0"
},
"devDependencies": {
"@antfu/eslint-config": "^0.41.3",
"@antfu/eslint-config": "^0.43.1",
"@commitlint/cli": "^17.7.1",
"@commitlint/config-conventional": "^17.7.0",
"eslint": "^8.49.0",
"eslint": "^8.50.0",
"lint-staged": "^14.0.1",
"pnpm": "^8.7.5",
"pnpm": "^8.8.0",
"simple-git-hooks": "^2.9.0",
"taze": "^0.11.2",
"taze": "^0.11.3",
"typescript": "^5.2.2",
"vitest": "^0.34.4"
},

File diff suppressed because it is too large Load Diff