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:
parent
8c2e6f1539
commit
d03067db67
|
|
@ -12,5 +12,6 @@ module.exports = {
|
||||||
'no-console': 'warn',
|
'no-console': 'warn',
|
||||||
'no-tabs': 'off',
|
'no-tabs': 'off',
|
||||||
'no-invalid-character': 'off',
|
'no-invalid-character': 'off',
|
||||||
|
'import/first': 'off',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,14 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/lib/registry/default
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
name: string
|
name: string
|
||||||
|
names?: string[]
|
||||||
align?: 'center' | 'start' | 'end'
|
align?: 'center' | 'start' | 'end'
|
||||||
sfcTsCode?: string
|
sfcTsCode?: string
|
||||||
sfcTsHtml?: string
|
sfcTsHtml?: string
|
||||||
}>(), { align: 'center' })
|
}>(), {
|
||||||
|
align: 'center',
|
||||||
|
names: () => ['CLI', 'Manual'],
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -15,24 +19,17 @@ const props = withDefaults(defineProps<{
|
||||||
<div class="flex items-center justify-between pb-3">
|
<div class="flex items-center justify-between pb-3">
|
||||||
<TabsList class="w-full justify-start rounded-none border-b bg-transparent p-0">
|
<TabsList class="w-full justify-start rounded-none border-b bg-transparent p-0">
|
||||||
<TabsTrigger
|
<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"
|
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
|
{{ tab }}
|
||||||
</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
|
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
<TabsContent value="CLI" class="relative space-y-10">
|
<TabsContent v-for="(tab, index) in props.names" :key="index" :value="tab" class="relative space-y-10">
|
||||||
<slot name="CLI" />
|
<slot :name="tab" />
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="Manual">
|
|
||||||
<slot name="Manual" />
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -217,9 +217,8 @@ export const docsConfig: DocsConfig = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Form',
|
title: 'Form',
|
||||||
href: '#',
|
href: '/docs/components/form',
|
||||||
label: 'Soon',
|
label: 'New',
|
||||||
disabled: true,
|
|
||||||
items: [],
|
items: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -352,10 +351,10 @@ export const examples: Example[] = [
|
||||||
code: 'https://github.com/radix-vue/shadcn-vue/tree/dev/apps/www/src/examples/cards',
|
code: 'https://github.com/radix-vue/shadcn-vue/tree/dev/apps/www/src/examples/cards',
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
// name: "Tasks",
|
// name: "Tasks",
|
||||||
// href: "/examples/tasks",
|
// href: "/examples/tasks",
|
||||||
// label: "New",
|
// label: "New",
|
||||||
// code: "https://github.com/radix-vue/shadcn-vue/tree/dev/apps/www/src/examples/tasks"
|
// code: "https://github.com/radix-vue/shadcn-vue/tree/dev/apps/www/src/examples/tasks"
|
||||||
// },
|
// },
|
||||||
{
|
{
|
||||||
name: 'Playground',
|
name: 'Playground',
|
||||||
|
|
|
||||||
|
|
@ -14,19 +14,21 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@morev/vue-transitions": "^2.3.6",
|
"@morev/vue-transitions": "^2.3.6",
|
||||||
"@radix-icons/vue": "^1.0.0",
|
"@radix-icons/vue": "^1.0.0",
|
||||||
"@tanstack/vue-table": "^8.9.9",
|
"@tanstack/vue-table": "^8.10.3",
|
||||||
"@unovis/ts": "^1.2.1",
|
"@unovis/ts": "^1.2.1",
|
||||||
"@unovis/vue": "1.3.0-alpha.3",
|
"@unovis/vue": "1.3.0-alpha.3",
|
||||||
|
"@vee-validate/zod": "^4.11.7",
|
||||||
"@vueuse/core": "^10.4.1",
|
"@vueuse/core": "^10.4.1",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
"lucide-vue-next": "^0.276.0",
|
"lucide-vue-next": "^0.276.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"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": "^3.3.4",
|
||||||
"vue-wrap-balancer": "^1.1.3",
|
"vue-wrap-balancer": "^1.1.3",
|
||||||
"zod": "^3.22.2"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify-json/radix-icons": "^1.1.11",
|
"@iconify-json/radix-icons": "^1.1.11",
|
||||||
|
|
@ -35,21 +37,21 @@
|
||||||
"@iconify/vue": "^4.1.1",
|
"@iconify/vue": "^4.1.1",
|
||||||
"@types/lodash.template": "^4.5.1",
|
"@types/lodash.template": "^4.5.1",
|
||||||
"@types/node": "^20.6.0",
|
"@types/node": "^20.6.0",
|
||||||
"@vitejs/plugin-vue": "^4.3.4",
|
"@vitejs/plugin-vue": "^4.4.0",
|
||||||
"@vitejs/plugin-vue-jsx": "^3.0.2",
|
"@vitejs/plugin-vue-jsx": "^3.0.2",
|
||||||
"@vue/compiler-core": "^3.3.4",
|
"@vue/compiler-core": "^3.3.4",
|
||||||
"@vue/compiler-dom": "^3.3.4",
|
"@vue/compiler-dom": "^3.3.4",
|
||||||
"autoprefixer": "^10.4.15",
|
"autoprefixer": "^10.4.16",
|
||||||
"lodash.template": "^4.5.0",
|
"lodash.template": "^4.5.0",
|
||||||
"radix-vue": "^0.4.1",
|
"radix-vue": "^0.4.1",
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"tailwind-merge": "^1.14.0",
|
"tailwind-merge": "^1.14.0",
|
||||||
"tailwindcss": "^3.3.3",
|
"tailwindcss": "^3.3.3",
|
||||||
"tsx": "^3.12.10",
|
"tsx": "^3.13.0",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"unplugin-icons": "^0.17.0",
|
"unplugin-icons": "^0.17.0",
|
||||||
"vite": "^4.4.9",
|
"vite": "^4.4.11",
|
||||||
"vitepress": "^1.0.0-rc.13",
|
"vitepress": "^1.0.0-rc.20",
|
||||||
"vue-tsc": "^1.8.11"
|
"vue-tsc": "^1.8.15"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,16 @@ import { Checkbox } from '@/components/ui/checkbox'
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
### With text
|
||||||
|
|
||||||
|
<ComponentPreview name="CheckboxWithText" />
|
||||||
|
|
||||||
### Disabled
|
### Disabled
|
||||||
|
|
||||||
<ComponentPreview name="CheckboxDisabled" />
|
<ComponentPreview name="CheckboxDisabled" />
|
||||||
|
|
||||||
|
### Form
|
||||||
|
|
||||||
|
<ComponentPreview name="CheckboxFormSingle" />
|
||||||
|
|
||||||
|
<ComponentPreview name="CheckboxFormMultiple" />
|
||||||
|
|
|
||||||
|
|
@ -86,4 +86,20 @@ const value = ref({})
|
||||||
</template>
|
</template>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Combobox
|
||||||
|
|
||||||
|
<ComponentPreview name="ComboboxDemo" />
|
||||||
|
|
||||||
|
### Popover
|
||||||
|
|
||||||
|
<ComponentPreview name="ComboboxPopover" />
|
||||||
|
|
||||||
|
### Dropdown menu
|
||||||
|
|
||||||
|
<ComponentPreview name="ComboboxDropdownMenu" />
|
||||||
|
|
||||||
|
### Form
|
||||||
|
|
||||||
|
<ComponentPreview name="ComboboxForm" />
|
||||||
|
|
|
||||||
|
|
@ -56,12 +56,18 @@ const date = ref<Date>()
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
### Date Picker
|
||||||
|
|
||||||
|
<ComponentPreview name="DatePickerDemo" />
|
||||||
|
|
||||||
### Date Range Picker
|
### Date Range Picker
|
||||||
|
|
||||||
<ComponentPreview name="DatePickerWithRange" />
|
<ComponentPreview name="DatePickerWithRange" />
|
||||||
|
|
||||||
|
### With Presets
|
||||||
|
|
||||||
### Date Picker
|
<ComponentPreview name="DatePickerWithPresets" />
|
||||||
|
|
||||||
|
### Form
|
||||||
|
|
||||||
<ComponentPreview name="DatePickerDemo" />
|
<ComponentPreview name="DatePickerForm" />
|
||||||
|
|
|
||||||
331
apps/www/src/content/docs/components/form.md
Normal file
331
apps/www/src/content/docs/components/form.md
Normal 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)
|
||||||
|
|
@ -42,3 +42,27 @@ import { Input } from '@/components/ui/input'
|
||||||
<Input />
|
<Input />
|
||||||
</template>
|
</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" />
|
||||||
|
|
|
||||||
|
|
@ -35,3 +35,9 @@ import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</template>
|
</template>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Form
|
||||||
|
|
||||||
|
<ComponentPreview name="RadioGroupForm" />
|
||||||
|
|
|
||||||
|
|
@ -46,3 +46,9 @@ import {
|
||||||
</Select>
|
</Select>
|
||||||
</template>
|
</template>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Form
|
||||||
|
|
||||||
|
<ComponentPreview name="SelectForm" />
|
||||||
|
|
|
||||||
|
|
@ -48,3 +48,9 @@ import { Switch } from '@/components/ui/switch'
|
||||||
<Switch />
|
<Switch />
|
||||||
</template>
|
</template>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Form
|
||||||
|
|
||||||
|
<ComponentPreview name="SwitchForm" />
|
||||||
|
|
|
||||||
|
|
@ -46,3 +46,29 @@ import { Textarea } from '@/components/ui/textarea'
|
||||||
<Textarea />
|
<Textarea />
|
||||||
</template>
|
</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" />
|
||||||
|
|
|
||||||
|
|
@ -2,21 +2,23 @@
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import * as z from 'zod'
|
import * as z from 'zod'
|
||||||
import { format } from 'date-fns'
|
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 { cn } from '@/lib/utils'
|
||||||
|
|
||||||
import RadixIconsCalendar from '~icons/radix-icons/calendar'
|
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 { 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 { Separator } from '@/lib/registry/new-york/ui/separator'
|
||||||
import {
|
import {
|
||||||
Select,
|
Command,
|
||||||
SelectContent,
|
CommandEmpty,
|
||||||
SelectGroup,
|
CommandGroup,
|
||||||
SelectItem,
|
CommandInput,
|
||||||
SelectTrigger,
|
CommandItem,
|
||||||
SelectValue,
|
} from '@/lib/registry/default/ui/command'
|
||||||
} from '@/lib/registry/new-york/ui/select'
|
|
||||||
import { Button } from '@/lib/registry/new-york/ui/button'
|
import { Button } from '@/lib/registry/new-york/ui/button'
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
|
|
@ -25,11 +27,7 @@ import {
|
||||||
} from '@/lib/registry/default/ui/popover'
|
} from '@/lib/registry/default/ui/popover'
|
||||||
import { Calendar } from '@/lib/registry/new-york/ui/calendar'
|
import { Calendar } from '@/lib/registry/new-york/ui/calendar'
|
||||||
|
|
||||||
const accountForm = ref({
|
const open = ref(false)
|
||||||
name: '',
|
|
||||||
dob: null,
|
|
||||||
language: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
const languages = [
|
const languages = [
|
||||||
{ label: 'English', value: 'en' },
|
{ label: 'English', value: 'en' },
|
||||||
|
|
@ -43,7 +41,7 @@ const languages = [
|
||||||
{ label: 'Chinese', value: 'zh' },
|
{ label: 'Chinese', value: 'zh' },
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
const accountFormSchema = z.object({
|
const accountFormSchema = toTypedSchema(z.object({
|
||||||
name: z
|
name: z
|
||||||
.string()
|
.string()
|
||||||
.min(2, {
|
.min(2, {
|
||||||
|
|
@ -58,20 +56,14 @@ const accountFormSchema = z.object({
|
||||||
language: z.string().nonempty({
|
language: z.string().nonempty({
|
||||||
message: 'Please select a language.',
|
message: 'Please select a language.',
|
||||||
}),
|
}),
|
||||||
})
|
}))
|
||||||
|
|
||||||
type AccountFormValues = z.infer<typeof accountFormSchema>
|
const filterFunction = (list: typeof languages, search: string) => list.filter(i => i.value.toLowerCase().includes(search.toLowerCase()))
|
||||||
const errors = ref<z.ZodFormattedError<AccountFormValues> | null>(null)
|
|
||||||
|
|
||||||
async function handleSubmit() {
|
// https://github.com/logaretm/vee-validate/issues/3521
|
||||||
const result = accountFormSchema.safeParse(accountForm.value)
|
// https://github.com/logaretm/vee-validate/discussions/3571
|
||||||
if (!result.success) {
|
async function onSubmit(values: any) {
|
||||||
errors.value = result.error.format()
|
console.log('Form submitted!', values)
|
||||||
console.log(errors.value)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Form submitted!', accountForm.value)
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -85,74 +77,107 @@ async function handleSubmit() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<form class="space-y-8" @submit.prevent="handleSubmit">
|
<Form v-slot="{ setValues }" :validation-schema="accountFormSchema" class="space-y-8" @submit="onSubmit">
|
||||||
<div class="grid gap-2">
|
<FormField v-slot="{ componentField }" name="name">
|
||||||
<Label for="name" :class="cn('text-sm', errors?.name && 'text-destructive')">
|
<FormItem>
|
||||||
Name
|
<FormLabel>Name</FormLabel>
|
||||||
</Label>
|
<FormControl>
|
||||||
<Input id="name" v-model="accountForm.name" placeholder="Your name" />
|
<Input type="text" placeholder="Your name" v-bind="componentField" />
|
||||||
<span class="text-muted-foreground text-sm">
|
</FormControl>
|
||||||
This is the name that will be displayed on your profile and in emails.
|
<FormDescription>
|
||||||
</span>
|
This is the name that will be displayed on your profile and in emails.
|
||||||
<div v-if="errors?.name" class="text-sm text-destructive">
|
</FormDescription>
|
||||||
<span v-for="error in errors.name._errors" :key="error">{{ error }}</span>
|
<FormMessage />
|
||||||
</div>
|
</FormItem>
|
||||||
</div>
|
</FormField>
|
||||||
<div class="grid gap-2">
|
|
||||||
<Label for="dob" :class="cn('text-sm', errors?.dob && 'text-destructive')">
|
<FormField v-slot="{ componentField, value }" name="dob">
|
||||||
Date of Birth
|
<FormItem>
|
||||||
</Label>
|
<FormLabel>Date of birth</FormLabel>
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger as-child>
|
<PopoverTrigger as-child>
|
||||||
<Button
|
<FormControl>
|
||||||
variant="outline"
|
<Button
|
||||||
:class="cn(
|
variant="outline" :class="cn(
|
||||||
'w-[280px] pl-3 text-left font-normal',
|
'w-[280px] pl-3 text-left font-normal',
|
||||||
!accountForm.dob && 'text-muted-foreground',
|
!value && 'text-muted-foreground',
|
||||||
)"
|
)"
|
||||||
>
|
>
|
||||||
<span>{{ accountForm.dob ? format(accountForm.dob, "PPP") : "Pick a date" }}</span>
|
<span>{{ value ? format(value, "PPP") : "Pick a date" }}</span>
|
||||||
<RadixIconsCalendar class="ml-auto h-4 w-4 opacity-50" />
|
<RadixIconsCalendar class="ml-auto h-4 w-4 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</FormControl>
|
||||||
<PopoverContent class="p-0">
|
</PopoverTrigger>
|
||||||
<Calendar v-model="accountForm.dob" />
|
<PopoverContent class="p-0">
|
||||||
</PopoverContent>
|
<Calendar v-bind="componentField" />
|
||||||
</Popover>
|
</PopoverContent>
|
||||||
<span class="text-muted-foreground text-sm">
|
</Popover>
|
||||||
Your date of birth is used to calculate your age.
|
<FormDescription>
|
||||||
</span>
|
Your date of birth is used to calculate your age.
|
||||||
<div v-if="errors?.dob" class="text-sm text-destructive">
|
</FormDescription>
|
||||||
<span v-for="error in errors.dob._errors" :key="error">{{ error }}</span>
|
<FormMessage />
|
||||||
</div>
|
</FormItem>
|
||||||
</div>
|
</FormField>
|
||||||
<div class="grid gap-2">
|
|
||||||
<Label for="language" :class="cn('text-sm', errors?.language && 'text-destructive')">
|
<FormField v-slot="{ value }" name="language">
|
||||||
Language
|
<FormItem>
|
||||||
</Label>
|
<FormLabel>Language</FormLabel>
|
||||||
<Select id="language" v-model="accountForm.language">
|
|
||||||
<SelectTrigger class="w-[200px]">
|
<Popover v-model:open="open">
|
||||||
<SelectValue placeholder="Select a language" />
|
<PopoverTrigger as-child>
|
||||||
</SelectTrigger>
|
<FormControl>
|
||||||
<SelectContent>
|
<Button
|
||||||
<SelectGroup>
|
variant="outline" role="combobox" :aria-expanded="open" :class="cn(
|
||||||
<SelectItem v-for="language in languages" :key="language.value" :value="language.value">
|
'w-[200px] justify-between',
|
||||||
{{ language.label }}
|
!value && 'text-muted-foreground',
|
||||||
</SelectItem>
|
)"
|
||||||
</SelectGroup>
|
>
|
||||||
</SelectContent>
|
{{ value ? languages.find(
|
||||||
</Select>
|
(language) => language.value === value,
|
||||||
<span class="text-muted-foreground text-sm">
|
)?.label : 'Select language...' }}
|
||||||
This is the language that will be used in the dashboard.
|
|
||||||
</span>
|
<ChevronsUpDown class="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
<div v-if="errors?.language" class="text-sm text-destructive">
|
</Button>
|
||||||
<span v-for="error in errors.language._errors" :key="error">{{ error }}</span>
|
</FormControl>
|
||||||
</div>
|
</PopoverTrigger>
|
||||||
</div>
|
<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">
|
<div class="flex justify-start">
|
||||||
<Button type="submit">
|
<Button type="submit">
|
||||||
Update account
|
Update account
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</Form>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,16 @@
|
||||||
<script setup lang="ts">
|
<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 * 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 { Separator } from '@/lib/registry/new-york/ui/separator'
|
||||||
import { RadioGroup, RadioGroupItem } from '@/lib/registry/default/ui/radio-group'
|
import { RadioGroup, RadioGroupItem } from '@/lib/registry/default/ui/radio-group'
|
||||||
import {
|
import { Button, buttonVariants } from '@/lib/registry/new-york/ui/button'
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectGroup,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/lib/registry/new-york/ui/select'
|
|
||||||
import { Button } from '@/lib/registry/new-york/ui/button'
|
|
||||||
|
|
||||||
const appearenceForm = ref({
|
const appearanceFormSchema = toTypedSchema(z.object({
|
||||||
theme: 'light',
|
|
||||||
font: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
const appearanceFormSchema = z.object({
|
|
||||||
theme: z.enum(['light', 'dark'], {
|
theme: z.enum(['light', 'dark'], {
|
||||||
required_error: 'Please select a theme.',
|
required_error: 'Please select a theme.',
|
||||||
}),
|
}),
|
||||||
|
|
@ -29,21 +18,19 @@ const appearanceFormSchema = z.object({
|
||||||
invalid_type_error: 'Select a font',
|
invalid_type_error: 'Select a font',
|
||||||
required_error: 'Please 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 onSubmit = handleSubmit((values) => {
|
||||||
const errors = ref<z.ZodFormattedError<AppearanceFormValues> | null>(null)
|
console.log('Form submitted!', values)
|
||||||
|
})
|
||||||
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)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -56,109 +43,106 @@ async function handleSubmit() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<form class="space-y-8" @submit.prevent="handleSubmit">
|
<form class="space-y-8" @submit="onSubmit">
|
||||||
<div class="grid gap-2">
|
<FormField v-slot="{ field }" name="font">
|
||||||
<Label for="font" :class="cn('text-sm', errors?.font && 'text-destructive')">
|
<FormItem>
|
||||||
Font
|
<FormLabel>Email</FormLabel>
|
||||||
</Label>
|
<div class="relative w-[200px]">
|
||||||
<Select id="font" v-model="appearenceForm.font">
|
<FormControl>
|
||||||
<SelectTrigger class="w-[200px]">
|
<select
|
||||||
<SelectValue placeholder="Select a font" />
|
:class="cn(
|
||||||
</SelectTrigger>
|
buttonVariants({ variant: 'outline' }),
|
||||||
<SelectContent>
|
'w-[200px] appearance-none bg-transparent font-normal',
|
||||||
<SelectGroup>
|
)"
|
||||||
<SelectItem value="inter">
|
v-bind="field"
|
||||||
Inter
|
>
|
||||||
</SelectItem>
|
<option value="inter">
|
||||||
<SelectItem value="manrope">
|
Inter
|
||||||
Manrope
|
</option>
|
||||||
</SelectItem>
|
<option value="manrope">
|
||||||
<SelectItem value="system">
|
Manrope
|
||||||
System
|
</option>
|
||||||
</SelectItem>
|
<option value="system">
|
||||||
</SelectGroup>
|
System
|
||||||
</SelectContent>
|
</option>
|
||||||
</Select>
|
</select>
|
||||||
<span class="text-muted-foreground text-xs">
|
</FormControl>
|
||||||
Set the font you want to use in the dashboard.
|
<ChevronDownIcon class="absolute right-3 top-2.5 h-4 w-4 opacity-50" />
|
||||||
</span>
|
</div>
|
||||||
<div v-if="errors?.font" class="text-sm text-destructive">
|
<FormDescription>
|
||||||
<span v-for="error in errors.font._errors" :key="error">{{ error }}</span>
|
Set the font you want to use in the dashboard.
|
||||||
</div>
|
</FormDescription>
|
||||||
</div>
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<FormField v-slot="{ componentField }" type="radio" name="theme">
|
||||||
<Label for="theme" :class="cn('text-sm', errors?.theme && 'text-destructive')">
|
<FormItem class="space-y-1">
|
||||||
Theme
|
<FormLabel>Theme</FormLabel>
|
||||||
</Label>
|
<FormDescription>
|
||||||
<span class="text-muted-foreground text-xs">
|
Select the theme for the dashboard.
|
||||||
Select the theme for the dashboard.
|
</FormDescription>
|
||||||
</span>
|
<FormMessage />
|
||||||
<RadioGroup
|
|
||||||
v-model="appearenceForm.theme"
|
<RadioGroup
|
||||||
default-value="light"
|
class="grid max-w-md grid-cols-2 gap-8 pt-2"
|
||||||
class="grid max-w-md grid-cols-2 gap-8 pt-2"
|
v-bind="componentField"
|
||||||
>
|
>
|
||||||
<div class="grid gap-2">
|
<FormItem>
|
||||||
<Label class="[&:has([data-state=checked])>div]:border-primary">
|
<FormLabel class="[&:has([data-state=checked])>div]:border-primary">
|
||||||
<div>
|
<FormControl>
|
||||||
<RadioGroupItem value="light" class="sr-only" />
|
<RadioGroupItem value="light" class="sr-only" />
|
||||||
</div>
|
</FormControl>
|
||||||
<div class="items-center rounded-md border-2 border-muted p-1 hover:border-accent">
|
<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-sm bg-[#ecedef] p-2">
|
||||||
<div class="space-y-2 rounded-md bg-white p-2 shadow-sm">
|
<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-[80px] rounded-lg bg-[#ecedef]" />
|
||||||
<div class="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
|
<div class="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm">
|
<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-4 w-4 rounded-full bg-[#ecedef]" />
|
||||||
<div class="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
|
<div class="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm">
|
<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-4 w-4 rounded-full bg-[#ecedef]" />
|
||||||
<div class="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
|
<div class="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<span class="block w-full p-2 text-center font-normal">
|
||||||
<span class="block w-full p-2 text-center font-normal">
|
Light
|
||||||
Light
|
</span>
|
||||||
</span>
|
</FormLabel>
|
||||||
</Label>
|
</FormItem>
|
||||||
</div>
|
<FormItem>
|
||||||
<div>
|
<FormLabel class="[&:has([data-state=checked])>div]:border-primary">
|
||||||
<Label class="[&:has([data-state=checked])>div]:border-primary">
|
<FormControl>
|
||||||
<div>
|
<RadioGroupItem value="dark" class="sr-only" />
|
||||||
<RadioGroupItem value="dark" class="sr-only" />
|
</FormControl>
|
||||||
</div>
|
<div class="items-center rounded-md border-2 border-muted bg-popover p-1 hover:bg-accent hover:text-accent-foreground">
|
||||||
<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-sm bg-slate-950 p-2">
|
<div class="space-y-2 rounded-md bg-slate-800 p-2 shadow-sm">
|
||||||
<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-[80px] rounded-lg bg-slate-400" />
|
<div class="h-2 w-[100px] rounded-lg bg-slate-400" />
|
||||||
<div class="h-2 w-[100px] rounded-lg bg-slate-400" />
|
</div>
|
||||||
</div>
|
<div class="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm">
|
||||||
<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-4 w-4 rounded-full bg-slate-400" />
|
<div class="h-2 w-[100px] rounded-lg bg-slate-400" />
|
||||||
<div class="h-2 w-[100px] rounded-lg bg-slate-400" />
|
</div>
|
||||||
</div>
|
<div class="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm">
|
||||||
<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-4 w-4 rounded-full bg-slate-400" />
|
<div class="h-2 w-[100px] rounded-lg bg-slate-400" />
|
||||||
<div class="h-2 w-[100px] rounded-lg bg-slate-400" />
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<span class="block w-full p-2 text-center font-normal">
|
||||||
<span class="block w-full p-2 text-center font-normal">
|
Dark
|
||||||
Dark
|
</span>
|
||||||
</span>
|
</FormLabel>
|
||||||
</Label>
|
</FormItem>
|
||||||
</div>
|
</RadioGroup>
|
||||||
|
</FormItem>
|
||||||
<div class="col-span-2">
|
</FormField>
|
||||||
<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>
|
|
||||||
|
|
||||||
<div class="flex justify-start">
|
<div class="flex justify-start">
|
||||||
<Button type="submit">
|
<Button type="submit">
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,13 @@
|
||||||
<script setup lang="ts">
|
<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 * 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 { Separator } from '@/lib/registry/new-york/ui/separator'
|
||||||
import { Checkbox } from '@/lib/registry/new-york/ui/checkbox'
|
import { Checkbox } from '@/lib/registry/new-york/ui/checkbox'
|
||||||
import { Button } from '@/lib/registry/new-york/ui/button'
|
import { Button } from '@/lib/registry/new-york/ui/button'
|
||||||
|
|
||||||
const displayForm = ref({
|
|
||||||
items: ['recents', 'home'],
|
|
||||||
})
|
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
id: 'recents',
|
id: 'recents',
|
||||||
|
|
@ -43,25 +39,22 @@ const items = [
|
||||||
},
|
},
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
const displayFormSchema = z.object({
|
const displayFormSchema = toTypedSchema(z.object({
|
||||||
items: z.array(z.string()).refine(value => value.some(item => item), {
|
items: z.array(z.string()).refine(value => value.some(item => item), {
|
||||||
message: 'You have to select at least one 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 onSubmit = handleSubmit((values) => {
|
||||||
const errors = ref<z.ZodFormattedError<DisplayFormValues> | null>(null)
|
console.log('Form submitted!', values)
|
||||||
|
})
|
||||||
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)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -74,21 +67,39 @@ async function handleSubmit() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<form @submit.prevent="handleSubmit">
|
<form @submit="onSubmit">
|
||||||
<div class="mb-4">
|
<FormField name="items">
|
||||||
<Label for="sidebar" :class="cn('text-md', errors?.items && 'text-destructive')">
|
<FormItem>
|
||||||
Sidebar
|
<div class="mb-4">
|
||||||
</Label>
|
<FormLabel class="text-base">
|
||||||
<span class="text-xs text-muted-foreground">
|
Sidebar
|
||||||
Select the items you want to display in the sidebar.
|
</FormLabel>
|
||||||
</span>
|
<FormDescription>
|
||||||
</div>
|
Select the items you want to display in the sidebar.
|
||||||
<div v-for="item in items" :key="item.id" class="pb-1">
|
</FormDescription>
|
||||||
<div class="flex flex-row items-center space-x-3 space-y-0">
|
</div>
|
||||||
<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>
|
<FormField v-for="item in items" v-slot="{ value, handleChange }" :key="item.id" name="items">
|
||||||
</div>
|
<FormItem :key="item.id" class="flex flex-row items-start space-x-3 space-y-0">
|
||||||
</div>
|
<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">
|
<div class="flex justify-start mt-4">
|
||||||
<Button type="submit">
|
<Button type="submit">
|
||||||
Update display
|
Update display
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,16 @@
|
||||||
<script setup lang="ts">
|
<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 * 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 { Separator } from '@/lib/registry/new-york/ui/separator'
|
||||||
import { RadioGroup, RadioGroupItem } from '@/lib/registry/new-york/ui/radio-group'
|
import { RadioGroup, RadioGroupItem } from '@/lib/registry/new-york/ui/radio-group'
|
||||||
import { Switch } from '@/lib/registry/new-york/ui/switch'
|
import { Switch } from '@/lib/registry/new-york/ui/switch'
|
||||||
import { Checkbox } from '@/lib/registry/new-york/ui/checkbox'
|
import { Checkbox } from '@/lib/registry/new-york/ui/checkbox'
|
||||||
import { Button } from '@/lib/registry/new-york/ui/button'
|
import { Button } from '@/lib/registry/new-york/ui/button'
|
||||||
|
|
||||||
const notificationsForm = ref({
|
const notificationsFormSchema = toTypedSchema(z.object({
|
||||||
type: '',
|
|
||||||
mobile: false,
|
|
||||||
communication_emails: false,
|
|
||||||
social_emails: true,
|
|
||||||
marketing_emails: false,
|
|
||||||
security_emails: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const notificationsFormSchema = z.object({
|
|
||||||
type: z.enum(['all', 'mentions', 'none'], {
|
type: z.enum(['all', 'mentions', 'none'], {
|
||||||
required_error: 'You need to select a notification type.',
|
required_error: 'You need to select a notification type.',
|
||||||
}),
|
}),
|
||||||
|
|
@ -28,21 +19,21 @@ const notificationsFormSchema = z.object({
|
||||||
social_emails: z.boolean().default(false).optional(),
|
social_emails: z.boolean().default(false).optional(),
|
||||||
marketing_emails: z.boolean().default(false).optional(),
|
marketing_emails: z.boolean().default(false).optional(),
|
||||||
security_emails: z.boolean(),
|
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 onSubmit = handleSubmit((values, { resetForm }) => {
|
||||||
const errors = ref<z.ZodFormattedError<notificationsFormValues> | null>(null)
|
console.log('Form submitted!', values)
|
||||||
|
})
|
||||||
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)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -55,117 +46,149 @@ async function handleSubmit() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<form class="space-y-8" @submit.prevent="handleSubmit">
|
<form class="space-y-8" @submit="onSubmit">
|
||||||
<div class="grid gap-2">
|
<FormField v-slot="{ componentField }" type="radio" name="type">
|
||||||
<Label for="font" :class="cn('text-sm', errors?.type && 'text-destructive')">
|
<FormItem class="space-y-3">
|
||||||
Notify me about...
|
<FormLabel>Notify me about...</FormLabel>
|
||||||
</Label>
|
<FormControl>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
v-model="notificationsForm.type"
|
class="flex flex-col space-y-1"
|
||||||
default-value="all"
|
v-bind="componentField"
|
||||||
class="flex flex-col space-y-1"
|
>
|
||||||
>
|
<FormItem class="flex items-center space-x-3 space-y-0">
|
||||||
<div class="flex items-center space-x-3 space-y-0">
|
<FormControl>
|
||||||
<RadioGroupItem id="all" value="all" />
|
<RadioGroupItem value="all" />
|
||||||
<Label for="all">All new messages</Label>
|
</FormControl>
|
||||||
</div>
|
<FormLabel class="font-normal">
|
||||||
<div class="flex items-center space-x-3 space-y-0">
|
All new messages
|
||||||
<RadioGroupItem id="mentions" value="mentions" />
|
</FormLabel>
|
||||||
<Label for="mentions">Direct messages and mentions</Label>
|
</FormItem>
|
||||||
</div>
|
<FormItem class="flex items-center space-x-3 space-y-0">
|
||||||
<div class="flex items-center space-x-3 space-y-0">
|
<FormControl>
|
||||||
<RadioGroupItem id="none" value="none" />
|
<RadioGroupItem value="mentions" />
|
||||||
<Label for="none">Nothing</Label>
|
</FormControl>
|
||||||
</div>
|
<FormLabel class="font-normal">
|
||||||
<div v-if="errors?.type" class="text-sm text-destructive">
|
Direct messages and mentions
|
||||||
<span v-for="error in errors.type._errors" :key="error">{{ error }}</span>
|
</FormLabel>
|
||||||
</div>
|
</FormItem>
|
||||||
</RadioGroup>
|
<FormItem class="flex items-center space-x-3 space-y-0">
|
||||||
</div>
|
<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">
|
<h3 class="mb-4 text-lg font-medium">
|
||||||
Email Notifications
|
Email Notifications
|
||||||
</h3>
|
</h3>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="flex flex-row items-center justify-between rounded-lg border p-4">
|
<FormField v-slot="{ handleChange, value }" type="checkbox" name="communication_emails">
|
||||||
<div class="space-y-0.5">
|
<FormItem class="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||||
<Label class="text-base" for="communication_emails">
|
<div class="space-y-0.5">
|
||||||
Communication emails
|
<FormLabel class="text-base">
|
||||||
</Label>
|
Communication emails
|
||||||
<span class="text-xs text-muted-foreground">
|
</FormLabel>
|
||||||
Receive emails about your account activity.
|
<FormDescription>
|
||||||
</span>
|
Receive emails about your account activity.
|
||||||
</div>
|
</FormDescription>
|
||||||
<Switch
|
</div>
|
||||||
id="communication_emails"
|
<FormControl>
|
||||||
v-model:checked="notificationsForm.communication_emails"
|
<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>
|
</FormControl>
|
||||||
</div>
|
<div class="space-y-1 leading-none">
|
||||||
<div class="flex flex-row items-center justify-between rounded-lg border p-4">
|
<FormLabel>
|
||||||
<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">
|
|
||||||
Use different settings for my mobile devices
|
Use different settings for my mobile devices
|
||||||
</Label>
|
</FormLabel>
|
||||||
<span class="text-xs text-muted-foreground">
|
<FormDescription>
|
||||||
You can manage your mobile notifications in the {{ " " }}
|
You can manage your mobile notifications in the
|
||||||
<a href="/examples/forms">mobile settings</a> page.
|
<a href="/examples/forms">
|
||||||
</span>
|
mobile settings
|
||||||
|
</a> page.
|
||||||
|
</FormDescription>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</FormItem>
|
||||||
</div>
|
</FormField>
|
||||||
|
|
||||||
<div class="flex justify-start">
|
<div class="flex justify-start">
|
||||||
<Button type="submit">
|
<Button type="submit">
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { FieldArray, useForm } from 'vee-validate'
|
||||||
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
import * as z from 'zod'
|
import * as z from 'zod'
|
||||||
|
import { Cross1Icon } from '@radix-icons/vue'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
import { Input } from '@/lib/registry/new-york/ui/input'
|
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 { Separator } from '@/lib/registry/new-york/ui/separator'
|
||||||
import { Textarea } from '@/lib/registry/new-york/ui/textarea'
|
import { Textarea } from '@/lib/registry/new-york/ui/textarea'
|
||||||
import {
|
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 verifiedEmails = ref(['m@example.com', 'm@google.com', 'm@support.com'])
|
||||||
|
|
||||||
const profileForm = ref({
|
const profileFormSchema = toTypedSchema(z.object({
|
||||||
username: '',
|
|
||||||
email: '',
|
|
||||||
bio: 'I own a computer.',
|
|
||||||
urls: [
|
|
||||||
{ value: 'https://shadcn.com' },
|
|
||||||
{ value: 'http://twitter.com/shadcn' },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
const profileFormSchema = z.object({
|
|
||||||
username: z
|
username: z
|
||||||
.string()
|
.string()
|
||||||
.min(2, {
|
.min(2, {
|
||||||
|
|
@ -51,20 +44,22 @@ const profileFormSchema = z.object({
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.optional(),
|
.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 onSubmit = handleSubmit((values) => {
|
||||||
const errors = ref<z.ZodFormattedError<ProfileFormValues> | null>(null)
|
console.log('Form submitted!', values)
|
||||||
|
})
|
||||||
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!')
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -77,75 +72,105 @@ async function handleSubmit() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<form class="space-y-8" @submit.prevent="handleSubmit">
|
<form class="space-y-8" @submit="onSubmit">
|
||||||
<div class="grid gap-2">
|
<FormField v-slot="{ componentField }" name="username">
|
||||||
<Label for="username" :class="cn('text-sm', errors?.username && 'text-destructive')">
|
<FormItem>
|
||||||
Username
|
<FormLabel>Username</FormLabel>
|
||||||
</Label>
|
<FormControl>
|
||||||
<Input id="username" v-model="profileForm.username" placeholder="shadcn" />
|
<Input type="text" placeholder="shadcn" v-bind="componentField" />
|
||||||
<span class="text-muted-foreground text-sm">
|
</FormControl>
|
||||||
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>
|
||||||
</span>
|
This is your public display name. It can be your real name or a pseudonym. You can only change this once every 30 days.
|
||||||
<div v-if="errors?.username" class="text-sm text-destructive">
|
</FormDescription>
|
||||||
<span v-for="error in errors.username._errors" :key="error">{{ error }}</span>
|
<FormMessage />
|
||||||
</div>
|
</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>
|
||||||
<div class="grid gap-2">
|
|
||||||
<Label for="email" :class="cn('text-sm', errors?.email && 'text-destructive')">
|
<div class="flex gap-2 justify-start">
|
||||||
Email
|
<Button type="submit">
|
||||||
</Label>
|
Update profile
|
||||||
<Select id="email" v-model="profileForm.email">
|
</Button>
|
||||||
<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>
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
@click="resetForm"
|
||||||
class="text-xs w-20 mt-2"
|
|
||||||
@click="profileForm.urls?.push({ value: '' })"
|
|
||||||
>
|
>
|
||||||
Add URL
|
Reset form
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-start">
|
|
||||||
<Button type="submit">
|
|
||||||
Update profile
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -67,16 +67,16 @@ const selectedUsers = ref<User[]>([])
|
||||||
<template>
|
<template>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader class="flex flex-row items-center justify-between">
|
<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>
|
<Avatar>
|
||||||
<AvatarImage src="/avatars/01.png" alt="Image" />
|
<AvatarImage src="/avatars/01.png" alt="Image" />
|
||||||
<AvatarFallback>OM</AvatarFallback>
|
<AvatarFallback>OM</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium leading-none">
|
<p class="text-sm font-medium leading-none">
|
||||||
Sofia Davis
|
Sofia Davis
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p class="text-sm text-muted-foreground">
|
||||||
m@example.com
|
m@example.com
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,7 @@ const table = useVueTable({
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<div className="mb-4 flex items-center gap-4">
|
<div class="mb-4 flex items-center gap-4">
|
||||||
<Input
|
<Input
|
||||||
class="max-w-sm"
|
class="max-w-sm"
|
||||||
placeholder="Filter emails..."
|
placeholder="Filter emails..."
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { Checkbox } from '@/lib/registry/default/ui/checkbox'
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<Checkbox id="terms" />
|
<Checkbox id="terms" />
|
||||||
<label
|
<label
|
||||||
htmlFor="terms"
|
for="terms"
|
||||||
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
>
|
>
|
||||||
Accept terms and conditions
|
Accept terms and conditions
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
112
apps/www/src/lib/registry/default/example/ComboboxForm.vue
Normal file
112
apps/www/src/lib/registry/default/example/ComboboxForm.vue
Normal 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>
|
||||||
118
apps/www/src/lib/registry/default/example/ComboboxPopover.vue
Normal file
118
apps/www/src/lib/registry/default/example/ComboboxPopover.vue
Normal 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>
|
||||||
74
apps/www/src/lib/registry/default/example/DatePickerForm.vue
Normal file
74
apps/www/src/lib/registry/default/example/DatePickerForm.vue
Normal 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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
11
apps/www/src/lib/registry/default/example/InputFile.vue
Normal file
11
apps/www/src/lib/registry/default/example/InputFile.vue
Normal 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>
|
||||||
48
apps/www/src/lib/registry/default/example/InputForm.vue
Normal file
48
apps/www/src/lib/registry/default/example/InputForm.vue
Normal 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>
|
||||||
|
|
@ -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>
|
||||||
11
apps/www/src/lib/registry/default/example/InputWithLabel.vue
Normal file
11
apps/www/src/lib/registry/default/example/InputWithLabel.vue
Normal 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>
|
||||||
76
apps/www/src/lib/registry/default/example/RadioGroupForm.vue
Normal file
76
apps/www/src/lib/registry/default/example/RadioGroupForm.vue
Normal 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>
|
||||||
79
apps/www/src/lib/registry/default/example/SelectForm.vue
Normal file
79
apps/www/src/lib/registry/default/example/SelectForm.vue
Normal 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>
|
||||||
85
apps/www/src/lib/registry/default/example/SwitchForm.vue
Normal file
85
apps/www/src/lib/registry/default/example/SwitchForm.vue
Normal 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>
|
||||||
|
|
@ -60,12 +60,12 @@ const invoices = [
|
||||||
<TableCaption>A list of your recent invoices.</TableCaption>
|
<TableCaption>A list of your recent invoices.</TableCaption>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead class-name="w-[100px]">
|
<TableHead class="w-[100px]">
|
||||||
Invoice
|
Invoice
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead>Status</TableHead>
|
<TableHead>Status</TableHead>
|
||||||
<TableHead>Method</TableHead>
|
<TableHead>Method</TableHead>
|
||||||
<TableHead class-name="text-right">
|
<TableHead class="text-right">
|
||||||
Amount
|
Amount
|
||||||
</TableHead>
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
59
apps/www/src/lib/registry/default/example/TextareaForm.vue
Normal file
59
apps/www/src/lib/registry/default/example/TextareaForm.vue
Normal 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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
16
apps/www/src/lib/registry/default/ui/form/FormControl.vue
Normal file
16
apps/www/src/lib/registry/default/ui/form/FormControl.vue
Normal 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>
|
||||||
|
|
@ -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>
|
||||||
27
apps/www/src/lib/registry/default/ui/form/FormItem.vue
Normal file
27
apps/www/src/lib/registry/default/ui/form/FormItem.vue
Normal 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>
|
||||||
28
apps/www/src/lib/registry/default/ui/form/FormLabel.vue
Normal file
28
apps/www/src/lib/registry/default/ui/form/FormLabel.vue
Normal 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>
|
||||||
16
apps/www/src/lib/registry/default/ui/form/FormMessage.vue
Normal file
16
apps/www/src/lib/registry/default/ui/form/FormMessage.vue
Normal 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>
|
||||||
6
apps/www/src/lib/registry/default/ui/form/index.ts
Normal file
6
apps/www/src/lib/registry/default/ui/form/index.ts
Normal 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'
|
||||||
30
apps/www/src/lib/registry/default/ui/form/useFormField.ts
Normal file
30
apps/www/src/lib/registry/default/ui/form/useFormField.ts
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -169,7 +169,7 @@ const table = useVueTable({
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<div className="mb-4 flex items-center gap-4">
|
<div class="mb-4 flex items-center gap-4">
|
||||||
<Input
|
<Input
|
||||||
class="max-w-sm"
|
class="max-w-sm"
|
||||||
placeholder="Filter emails..."
|
placeholder="Filter emails..."
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { Checkbox } from '@/lib/registry/new-york/ui/checkbox'
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<Checkbox id="terms" />
|
<Checkbox id="terms" />
|
||||||
<label
|
<label
|
||||||
htmlFor="terms"
|
for="terms"
|
||||||
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
>
|
>
|
||||||
Accept terms and conditions
|
Accept terms and conditions
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -3,19 +3,19 @@ import { ref } from 'vue'
|
||||||
import { CaretSortIcon, CheckIcon } from '@radix-icons/vue'
|
import { CaretSortIcon, CheckIcon } from '@radix-icons/vue'
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { Button } from '@/lib/registry/default/ui/button'
|
import { Button } from '@/lib/registry/new-york/ui/button'
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
CommandGroup,
|
CommandGroup,
|
||||||
CommandInput,
|
CommandInput,
|
||||||
CommandItem,
|
CommandItem,
|
||||||
} from '@/lib/registry/default/ui/command'
|
} from '@/lib/registry/new-york/ui/command'
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from '@/lib/registry/default/ui/popover'
|
} from '@/lib/registry/new-york/ui/popover'
|
||||||
|
|
||||||
const frameworks = [
|
const frameworks = [
|
||||||
{ value: 'next.js', label: 'Next.js' },
|
{ value: 'next.js', label: 'Next.js' },
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
115
apps/www/src/lib/registry/new-york/example/ComboboxForm.vue
Normal file
115
apps/www/src/lib/registry/new-york/example/ComboboxForm.vue
Normal 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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
11
apps/www/src/lib/registry/new-york/example/InputFile.vue
Normal file
11
apps/www/src/lib/registry/new-york/example/InputFile.vue
Normal 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>
|
||||||
48
apps/www/src/lib/registry/new-york/example/InputForm.vue
Normal file
48
apps/www/src/lib/registry/new-york/example/InputForm.vue
Normal 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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
79
apps/www/src/lib/registry/new-york/example/SelectForm.vue
Normal file
79
apps/www/src/lib/registry/new-york/example/SelectForm.vue
Normal 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>
|
||||||
85
apps/www/src/lib/registry/new-york/example/SwitchForm.vue
Normal file
85
apps/www/src/lib/registry/new-york/example/SwitchForm.vue
Normal 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>
|
||||||
|
|
@ -60,12 +60,12 @@ const invoices = [
|
||||||
<TableCaption>A list of your recent invoices.</TableCaption>
|
<TableCaption>A list of your recent invoices.</TableCaption>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead class-name="w-[100px]">
|
<TableHead class="w-[100px]">
|
||||||
Invoice
|
Invoice
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead>Status</TableHead>
|
<TableHead>Status</TableHead>
|
||||||
<TableHead>Method</TableHead>
|
<TableHead>Method</TableHead>
|
||||||
<TableHead class-name="text-right">
|
<TableHead class="text-right">
|
||||||
Amount
|
Amount
|
||||||
</TableHead>
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
59
apps/www/src/lib/registry/new-york/example/TextareaForm.vue
Normal file
59
apps/www/src/lib/registry/new-york/example/TextareaForm.vue
Normal 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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { AlertDialogAction, type AlertDialogActionProps } from 'radix-vue'
|
import { AlertDialogAction, type AlertDialogActionProps } from 'radix-vue'
|
||||||
import { cn } from '@/lib/utils'
|
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>()
|
const props = defineProps<AlertDialogActionProps>()
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { AlertDialogCancel, type AlertDialogCancelProps } from 'radix-vue'
|
import { AlertDialogCancel, type AlertDialogCancelProps } from 'radix-vue'
|
||||||
import { cn } from '@/lib/utils'
|
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>()
|
const props = defineProps<AlertDialogCancelProps>()
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
16
apps/www/src/lib/registry/new-york/ui/form/FormControl.vue
Normal file
16
apps/www/src/lib/registry/new-york/ui/form/FormControl.vue
Normal 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>
|
||||||
|
|
@ -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>
|
||||||
21
apps/www/src/lib/registry/new-york/ui/form/FormItem.vue
Normal file
21
apps/www/src/lib/registry/new-york/ui/form/FormItem.vue
Normal 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>
|
||||||
22
apps/www/src/lib/registry/new-york/ui/form/FormLabel.vue
Normal file
22
apps/www/src/lib/registry/new-york/ui/form/FormLabel.vue
Normal 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>
|
||||||
16
apps/www/src/lib/registry/new-york/ui/form/FormMessage.vue
Normal file
16
apps/www/src/lib/registry/new-york/ui/form/FormMessage.vue
Normal 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>
|
||||||
6
apps/www/src/lib/registry/new-york/ui/form/index.ts
Normal file
6
apps/www/src/lib/registry/new-york/ui/form/index.ts
Normal 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'
|
||||||
30
apps/www/src/lib/registry/new-york/ui/form/useFormField.ts
Normal file
30
apps/www/src/lib/registry/new-york/ui/form/useFormField.ts
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,23 +1,23 @@
|
||||||
{
|
{
|
||||||
"include": ["/**/*.vue", ".vitepress/**/*.vue", "/**/*.ts", ".vitepress/**/*.mts", ".vitepress/**/*.vue", "src/lib/**/*"],
|
|
||||||
"exclude": ["node_modules", "./scripts/build-registry.ts"],
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "esnext",
|
"target": "esnext",
|
||||||
|
"lib": ["esnext", "dom"],
|
||||||
|
"jsx": "preserve",
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"strict": true,
|
|
||||||
"jsx": "preserve",
|
|
||||||
"sourceMap": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"declaration": false,
|
|
||||||
"lib": ["esnext", "dom"],
|
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"skipLibCheck": true,
|
|
||||||
"outDir": "dist",
|
|
||||||
"types": ["unplugin-icons/types/vue", "node"],
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./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"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
package.json
10
package.json
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "shadcn-vue",
|
"name": "shadcn-vue",
|
||||||
"version": "0.3.2",
|
"version": "0.3.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@8.7.5",
|
"packageManager": "pnpm@8.8.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": "radix-vue/shadcn-vue",
|
"repository": "radix-vue/shadcn-vue",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
|
|
@ -29,14 +29,14 @@
|
||||||
"bumpp": "^9.2.0"
|
"bumpp": "^9.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@antfu/eslint-config": "^0.41.3",
|
"@antfu/eslint-config": "^0.43.1",
|
||||||
"@commitlint/cli": "^17.7.1",
|
"@commitlint/cli": "^17.7.1",
|
||||||
"@commitlint/config-conventional": "^17.7.0",
|
"@commitlint/config-conventional": "^17.7.0",
|
||||||
"eslint": "^8.49.0",
|
"eslint": "^8.50.0",
|
||||||
"lint-staged": "^14.0.1",
|
"lint-staged": "^14.0.1",
|
||||||
"pnpm": "^8.7.5",
|
"pnpm": "^8.8.0",
|
||||||
"simple-git-hooks": "^2.9.0",
|
"simple-git-hooks": "^2.9.0",
|
||||||
"taze": "^0.11.2",
|
"taze": "^0.11.3",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"vitest": "^0.34.4"
|
"vitest": "^0.34.4"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
2233
pnpm-lock.yaml
2233
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user