Merge remote-tracking branch 'upstream' into feat/cli/detype
This commit is contained in:
commit
b7450aef9d
68
.github/workflows/publish.yaml
vendored
Normal file
68
.github/workflows/publish.yaml
vendored
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
name: Publish www
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- 'apps/www/**'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'apps/www/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
deployments: write
|
||||||
|
name: Publish to Cloudflare Pages
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
# Run a build step here
|
||||||
|
- name: Setup Node.js environment
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v2
|
||||||
|
name: Install pnpm
|
||||||
|
with:
|
||||||
|
version: 8
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Get pnpm store directory
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: actions/cache@v3
|
||||||
|
name: Setup pnpm cache
|
||||||
|
with:
|
||||||
|
path: ${{ env.STORE_PATH }}
|
||||||
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pnpm-store-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm i --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build www
|
||||||
|
run: pnpm build
|
||||||
|
|
||||||
|
# Run a action to publish docs
|
||||||
|
- name: Publish to Cloudflare Pages
|
||||||
|
uses: cloudflare/pages-action@v1.5.0
|
||||||
|
with:
|
||||||
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
|
projectName: shadcn-vue
|
||||||
|
directory: .vitepress/dist
|
||||||
|
# Optional: Enable this if you want to have GitHub Deployments triggered
|
||||||
|
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
# Optional: Switch what branch you are publishing to.
|
||||||
|
# By default this will be the branch which triggered this workflow
|
||||||
|
# branch: main
|
||||||
|
# Optional: Change the working directory
|
||||||
|
workingDirectory: apps/www
|
||||||
|
wranglerVersion: '3'
|
||||||
|
|
@ -15,15 +15,15 @@ Accessible and customizable components that you can copy and paste into your app
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
Visit https://shadcn-vue.com/docs to view the documentation.
|
[View documentation here](https://www.shadcn-vue.com/docs/introduction.html)
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
All credits go to these open-source works and resources
|
All credits go to these open-source works and resources
|
||||||
|
|
||||||
- [Shadnc UI](https://ui.shadcn.com) for creating this beautiful project
|
- [Shadcn UI](https://ui.shadcn.com) for creating this beautiful project.
|
||||||
- [Shadnc Svelte](https://shadcn-svelte.com) for some inspiration for registry
|
- [Shadcn Svelte](https://shadcn-svelte.com) for some inspiration for registry.
|
||||||
- [Radix Vue](https://radix-vue.com) for doing all the hard work to make sure components are accessible
|
- [Radix Vue](https://radix-vue.com) for doing all the hard work to make sure components are accessible.
|
||||||
- [VueUse](https://vueuse.org) for providing many useful utilities.
|
- [VueUse](https://vueuse.org) for providing many useful utilities.
|
||||||
|
|
||||||
- [ahmedmayara](https://github.com/ahmedmayara/shadcn-vue) for populating many components
|
- [ahmedmayara](https://github.com/ahmedmayara/shadcn-vue) for populating many components
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import { defineConfig } from 'vitepress'
|
import { defineConfig } from 'vitepress'
|
||||||
import Icons from 'unplugin-icons/vite'
|
import Icons from 'unplugin-icons/vite'
|
||||||
|
import tailwind from 'tailwindcss'
|
||||||
|
import autoprefixer from 'autoprefixer'
|
||||||
import { siteConfig } from './theme/config/site'
|
import { siteConfig } from './theme/config/site'
|
||||||
import ComponentPreviewPlugin from './theme/plugins/previewer'
|
import ComponentPreviewPlugin from './theme/plugins/previewer'
|
||||||
|
|
||||||
|
|
@ -54,8 +56,16 @@ export default defineConfig({
|
||||||
'content/(.*)': '(.*)',
|
'content/(.*)': '(.*)',
|
||||||
},
|
},
|
||||||
vite: {
|
vite: {
|
||||||
|
css: {
|
||||||
|
postcss: {
|
||||||
|
plugins: [
|
||||||
|
tailwind(),
|
||||||
|
autoprefixer(),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
Icons({ compiler: 'vue3', autoInstall: true }) as any,
|
Icons({ compiler: 'vue3', autoInstall: true }),
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ const { style } = useConfigStore()
|
||||||
<StyleSwitcher />
|
<StyleSwitcher />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
:class="cn('preview flex min-h-[350px] w-full justify-center p-10', {
|
:class="cn('preview flex min-h-[350px] w-full justify-center p-6 lg:p-10', {
|
||||||
'items-center': align === 'center',
|
'items-center': align === 'center',
|
||||||
'items-start': align === 'start',
|
'items-start': align === 'start',
|
||||||
'items-end': align === 'end',
|
'items-end': align === 'end',
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ const examples = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Forms',
|
name: 'Forms',
|
||||||
href: '/examples/forms',
|
href: '/examples/forms/forms',
|
||||||
code: 'https://github.com/radix-vue/shadcn-vue/tree/dev/apps/www/src/examples/forms',
|
code: 'https://github.com/radix-vue/shadcn-vue/tree/dev/apps/www/src/examples/forms',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,7 @@ const { config } = useConfigStore()
|
||||||
<Select v-model="config.style">
|
<Select v-model="config.style">
|
||||||
<SelectTrigger :class="cn('h-7 w-[145px] text-xs [&_svg]:h-4 [&_svg]:w-4', props.class)">
|
<SelectTrigger :class="cn('h-7 w-[145px] text-xs [&_svg]:h-4 [&_svg]:w-4', props.class)">
|
||||||
<span class="text-muted-foreground">Style: </span>
|
<span class="text-muted-foreground">Style: </span>
|
||||||
<SelectValue placeholder="Select style">
|
<SelectValue placeholder="Select style" />
|
||||||
{{ styles.find(s => s.name === config.style)?.label }}
|
|
||||||
</SelectValue>
|
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem v-for="style in styles" :key="style.name" :value="style.name" class="text-xs">
|
<SelectItem v-for="style in styles" :key="style.name" :value="style.name" class="text-xs">
|
||||||
|
|
|
||||||
|
|
@ -9,21 +9,19 @@
|
||||||
"dev": "vitepress dev",
|
"dev": "vitepress dev",
|
||||||
"build": "vitepress build",
|
"build": "vitepress build",
|
||||||
"preview": "vitepress preview",
|
"preview": "vitepress preview",
|
||||||
"build:registry": "ts-node --esm --project ./tsconfig.scripts.json ./scripts/build-registry.ts"
|
"build:registry": "tsx ./scripts/build-registry.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@morev/vue-transitions": "^2.3.6",
|
"@morev/vue-transitions": "^2.3.6",
|
||||||
"@tanstack/vue-table": "^8.9.3",
|
"@tanstack/vue-table": "^8.9.8",
|
||||||
"@unovis/ts": "^1.2.1",
|
"@unovis/ts": "^1.2.1",
|
||||||
"@vitejs/plugin-vue-jsx": "^3.0.2",
|
"@vueuse/core": "^10.4.1",
|
||||||
"@vueuse/core": "^10.2.1",
|
"class-variance-authority": "^0.7.0",
|
||||||
"class-variance-authority": "^0.6.1",
|
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
"lucide-vue-next": "^0.268.0",
|
"lucide-vue-next": "^0.276.0",
|
||||||
"tailwindcss-animate": "^1.0.6",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"v-calendar": "^3.0.3",
|
"v-calendar": "^3.0.3",
|
||||||
"vitepress": "^1.0.0-rc.10",
|
|
||||||
"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.2"
|
||||||
|
|
@ -35,20 +33,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.5.7",
|
"@types/node": "^20.5.7",
|
||||||
"@vitejs/plugin-vue": "^4.1.0",
|
"@vitejs/plugin-vue": "^4.3.4",
|
||||||
|
"@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.14",
|
"autoprefixer": "^10.4.15",
|
||||||
"lodash.template": "^4.5.0",
|
"lodash.template": "^4.5.0",
|
||||||
"postcss": "^8.4.24",
|
"radix-vue": "^0.1.34",
|
||||||
"radix-vue": "^0.1.32",
|
|
||||||
"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",
|
||||||
"ts-node": "^10.9.1",
|
"tsx": "^3.12.8",
|
||||||
"typescript": "^5.0.2",
|
"typescript": "^5.2.2",
|
||||||
"unplugin-icons": "^0.16.6",
|
"unplugin-icons": "^0.17.0",
|
||||||
"vite": "^4.3.9",
|
"vite": "^4.4.9",
|
||||||
"vue-tsc": "^1.4.2"
|
"vitepress": "^1.0.0-rc.12",
|
||||||
|
"vue-tsc": "^1.8.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
export default {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
@ -18,4 +18,8 @@ The Figma UI Kit is open sourced by [Pietro Schirano](https://twitter.com/skiran
|
||||||
|
|
||||||
## Grab a copy
|
## Grab a copy
|
||||||
|
|
||||||
|
<div class="break-words">
|
||||||
|
|
||||||
https://www.figma.com/community/file/1203061493325953101
|
https://www.figma.com/community/file/1203061493325953101
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
|
||||||
5
apps/www/src/content/examples/forms/account.md
Normal file
5
apps/www/src/content/examples/forms/account.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script setup>
|
||||||
|
import AccountExample from "@/examples/forms/Account.vue"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AccountExample />
|
||||||
5
apps/www/src/content/examples/forms/appearance.md
Normal file
5
apps/www/src/content/examples/forms/appearance.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script setup>
|
||||||
|
import AppearanceExample from "@/examples/forms/Appearance.vue"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AppearanceExample />
|
||||||
5
apps/www/src/content/examples/forms/display.md
Normal file
5
apps/www/src/content/examples/forms/display.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script setup>
|
||||||
|
import DisplayExample from "@/examples/forms/Display.vue"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DisplayExample />
|
||||||
5
apps/www/src/content/examples/forms/forms.md
Normal file
5
apps/www/src/content/examples/forms/forms.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script setup>
|
||||||
|
import FormsExample from "@/examples/forms/Example.vue"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormsExample />
|
||||||
5
apps/www/src/content/examples/forms/notifications.md
Normal file
5
apps/www/src/content/examples/forms/notifications.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script setup>
|
||||||
|
import NotificationsExample from "@/examples/forms/Notifications.vue"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<NotificationsExample />
|
||||||
10
apps/www/src/examples/forms/Account.vue
Normal file
10
apps/www/src/examples/forms/Account.vue
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import FormsLayout from './layouts/FormsLayout.vue'
|
||||||
|
import AccountForm from './components/AccountForm.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FormsLayout>
|
||||||
|
<AccountForm />
|
||||||
|
</FormsLayout>
|
||||||
|
</template>
|
||||||
10
apps/www/src/examples/forms/Appearance.vue
Normal file
10
apps/www/src/examples/forms/Appearance.vue
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import FormsLayout from './layouts/FormsLayout.vue'
|
||||||
|
import AppearanceForm from './components/AppearanceForm.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FormsLayout>
|
||||||
|
<AppearanceForm />
|
||||||
|
</FormsLayout>
|
||||||
|
</template>
|
||||||
10
apps/www/src/examples/forms/Display.vue
Normal file
10
apps/www/src/examples/forms/Display.vue
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import FormsLayout from './layouts/FormsLayout.vue'
|
||||||
|
import DisplayForm from './components/DisplayForm.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FormsLayout>
|
||||||
|
<DisplayForm />
|
||||||
|
</FormsLayout>
|
||||||
|
</template>
|
||||||
10
apps/www/src/examples/forms/Example.vue
Normal file
10
apps/www/src/examples/forms/Example.vue
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import FormsLayout from './layouts/FormsLayout.vue'
|
||||||
|
import ProfileForm from './components/ProfileForm.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FormsLayout>
|
||||||
|
<ProfileForm />
|
||||||
|
</FormsLayout>
|
||||||
|
</template>
|
||||||
10
apps/www/src/examples/forms/Notifications.vue
Normal file
10
apps/www/src/examples/forms/Notifications.vue
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import FormsLayout from './layouts/FormsLayout.vue'
|
||||||
|
import NotificationsForm from './components/NotificationsForm.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FormsLayout>
|
||||||
|
<NotificationsForm />
|
||||||
|
</FormsLayout>
|
||||||
|
</template>
|
||||||
158
apps/www/src/examples/forms/components/AccountForm.vue
Normal file
158
apps/www/src/examples/forms/components/AccountForm.vue
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import * as z from 'zod'
|
||||||
|
import { format } from 'date-fns'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
import RadixIconsCalendar from '~icons/radix-icons/calendar'
|
||||||
|
|
||||||
|
import { Input } from '@/lib/registry/new-york/ui/input'
|
||||||
|
import { Label } from '@/lib/registry/new-york/ui/label'
|
||||||
|
import { Separator } from '@/lib/registry/new-york/ui/separator'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/lib/registry/new-york/ui/select'
|
||||||
|
import { Button } from '@/lib/registry/new-york/ui/button'
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from '@/lib/registry/default/ui/popover'
|
||||||
|
import { Calendar } from '@/lib/registry/new-york/ui/calendar'
|
||||||
|
|
||||||
|
const accountForm = ref({
|
||||||
|
name: '',
|
||||||
|
dob: null,
|
||||||
|
language: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
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 accountFormSchema = z.object({
|
||||||
|
name: z
|
||||||
|
.string()
|
||||||
|
.min(2, {
|
||||||
|
message: 'Name must be at least 2 characters.',
|
||||||
|
})
|
||||||
|
.max(30, {
|
||||||
|
message: 'Name must not be longer than 30 characters.',
|
||||||
|
}),
|
||||||
|
dob: z.date({
|
||||||
|
required_error: 'A date of birth is required.',
|
||||||
|
}),
|
||||||
|
language: z.string().nonempty({
|
||||||
|
message: 'Please select a language.',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
type AccountFormValues = z.infer<typeof accountFormSchema>
|
||||||
|
const errors = ref<z.ZodFormattedError<AccountFormValues> | null>(null)
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
const result = accountFormSchema.safeParse(accountForm.value)
|
||||||
|
if (!result.success) {
|
||||||
|
errors.value = result.error.format()
|
||||||
|
console.log(errors.value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Form submitted!', accountForm.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-medium">
|
||||||
|
Account
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
Update your account settings. Set your preferred language and timezone.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<form class="space-y-8" @submit.prevent="handleSubmit">
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="name" :class="cn('text-sm', errors?.name && 'text-destructive')">
|
||||||
|
Name
|
||||||
|
</Label>
|
||||||
|
<Input id="name" v-model="accountForm.name" placeholder="Your name" />
|
||||||
|
<span class="text-muted-foreground text-sm">
|
||||||
|
This is the name that will be displayed on your profile and in emails.
|
||||||
|
</span>
|
||||||
|
<div v-if="errors?.name" class="text-sm text-destructive">
|
||||||
|
<span v-for="error in errors.name._errors" :key="error">{{ error }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="dob" :class="cn('text-sm', errors?.dob && 'text-destructive')">
|
||||||
|
Date of Birth
|
||||||
|
</Label>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger as-child>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
:class="cn(
|
||||||
|
'w-[280px] pl-3 text-left font-normal',
|
||||||
|
!accountForm.dob && 'text-muted-foreground',
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<span>{{ accountForm.dob ? format(accountForm.dob, "PPP") : "Pick a date" }}</span>
|
||||||
|
<RadixIconsCalendar class="ml-auto h-4 w-4 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent class="p-0">
|
||||||
|
<Calendar v-model="accountForm.dob" />
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<span class="text-muted-foreground text-sm">
|
||||||
|
Your date of birth is used to calculate your age.
|
||||||
|
</span>
|
||||||
|
<div v-if="errors?.dob" class="text-sm text-destructive">
|
||||||
|
<span v-for="error in errors.dob._errors" :key="error">{{ error }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="language" :class="cn('text-sm', errors?.language && 'text-destructive')">
|
||||||
|
Language
|
||||||
|
</Label>
|
||||||
|
<Select id="language" v-model="accountForm.language">
|
||||||
|
<SelectTrigger class="w-[200px]">
|
||||||
|
<SelectValue placeholder="Select a language" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectItem v-for="language in languages" :key="language.value" :value="language.value">
|
||||||
|
{{ language.label }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<span class="text-muted-foreground text-sm">
|
||||||
|
This is the language that will be used in the dashboard.
|
||||||
|
</span>
|
||||||
|
<div v-if="errors?.language" class="text-sm text-destructive">
|
||||||
|
<span v-for="error in errors.language._errors" :key="error">{{ error }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-start">
|
||||||
|
<Button type="submit">
|
||||||
|
Update account
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
169
apps/www/src/examples/forms/components/AppearanceForm.vue
Normal file
169
apps/www/src/examples/forms/components/AppearanceForm.vue
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import * as z from 'zod'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
import { Label } from '@/lib/registry/new-york/ui/label'
|
||||||
|
import { Separator } from '@/lib/registry/new-york/ui/separator'
|
||||||
|
import { RadioGroup, RadioGroupItem } from '@/lib/registry/default/ui/radio-group'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/lib/registry/new-york/ui/select'
|
||||||
|
import { Button } from '@/lib/registry/new-york/ui/button'
|
||||||
|
|
||||||
|
const appearenceForm = ref({
|
||||||
|
theme: 'light',
|
||||||
|
font: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const appearanceFormSchema = z.object({
|
||||||
|
theme: z.enum(['light', 'dark'], {
|
||||||
|
required_error: 'Please select a theme.',
|
||||||
|
}),
|
||||||
|
font: z.enum(['inter', 'manrope', 'system'], {
|
||||||
|
invalid_type_error: 'Select a font',
|
||||||
|
required_error: 'Please select a font.',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
type AppearanceFormValues = z.infer<typeof appearanceFormSchema>
|
||||||
|
const errors = ref<z.ZodFormattedError<AppearanceFormValues> | null>(null)
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
const result = appearanceFormSchema.safeParse(appearenceForm.value)
|
||||||
|
if (!result.success) {
|
||||||
|
errors.value = result.error.format()
|
||||||
|
console.log(errors.value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Form submitted!', appearenceForm.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-medium">
|
||||||
|
Appearence
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
Customize the appearance of the app. Automatically switch between day and night themes.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<form class="space-y-8" @submit.prevent="handleSubmit">
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="font" :class="cn('text-sm', errors?.font && 'text-destructive')">
|
||||||
|
Font
|
||||||
|
</Label>
|
||||||
|
<Select id="font" v-model="appearenceForm.font">
|
||||||
|
<SelectTrigger class="w-[200px]">
|
||||||
|
<SelectValue placeholder="Select a font" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectItem value="inter">
|
||||||
|
Inter
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="manrope">
|
||||||
|
Manrope
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="system">
|
||||||
|
System
|
||||||
|
</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<span class="text-muted-foreground text-xs">
|
||||||
|
Set the font you want to use in the dashboard.
|
||||||
|
</span>
|
||||||
|
<div v-if="errors?.font" class="text-sm text-destructive">
|
||||||
|
<span v-for="error in errors.font._errors" :key="error">{{ error }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="theme" :class="cn('text-sm', errors?.theme && 'text-destructive')">
|
||||||
|
Theme
|
||||||
|
</Label>
|
||||||
|
<span class="text-muted-foreground text-xs">
|
||||||
|
Select the theme for the dashboard.
|
||||||
|
</span>
|
||||||
|
<RadioGroup
|
||||||
|
v-model="appearenceForm.theme"
|
||||||
|
default-value="light"
|
||||||
|
class="grid max-w-md grid-cols-2 gap-8 pt-2"
|
||||||
|
>
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label class="[&:has([data-state=checked])>div]:border-primary">
|
||||||
|
<div>
|
||||||
|
<RadioGroupItem value="light" class="sr-only" />
|
||||||
|
</div>
|
||||||
|
<div class="items-center rounded-md border-2 border-muted p-1 hover:border-accent">
|
||||||
|
<div class="space-y-2 rounded-sm bg-[#ecedef] p-2">
|
||||||
|
<div class="space-y-2 rounded-md bg-white p-2 shadow-sm">
|
||||||
|
<div class="h-2 w-[80px] rounded-lg bg-[#ecedef]" />
|
||||||
|
<div class="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm">
|
||||||
|
<div class="h-4 w-4 rounded-full bg-[#ecedef]" />
|
||||||
|
<div class="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm">
|
||||||
|
<div class="h-4 w-4 rounded-full bg-[#ecedef]" />
|
||||||
|
<div class="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="block w-full p-2 text-center font-normal">
|
||||||
|
Light
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label class="[&:has([data-state=checked])>div]:border-primary">
|
||||||
|
<div>
|
||||||
|
<RadioGroupItem value="dark" class="sr-only" />
|
||||||
|
</div>
|
||||||
|
<div class="items-center rounded-md border-2 border-muted bg-popover p-1 hover:bg-accent hover:text-accent-foreground">
|
||||||
|
<div class="space-y-2 rounded-sm bg-slate-950 p-2">
|
||||||
|
<div class="space-y-2 rounded-md bg-slate-800 p-2 shadow-sm">
|
||||||
|
<div class="h-2 w-[80px] rounded-lg bg-slate-400" />
|
||||||
|
<div class="h-2 w-[100px] rounded-lg bg-slate-400" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm">
|
||||||
|
<div class="h-4 w-4 rounded-full bg-slate-400" />
|
||||||
|
<div class="h-2 w-[100px] rounded-lg bg-slate-400" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm">
|
||||||
|
<div class="h-4 w-4 rounded-full bg-slate-400" />
|
||||||
|
<div class="h-2 w-[100px] rounded-lg bg-slate-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="block w-full p-2 text-center font-normal">
|
||||||
|
Dark
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-2">
|
||||||
|
<span v-if="errors?.theme" class="text-sm text-destructive">
|
||||||
|
<span v-for="error in errors.theme._errors" :key="error">{{ error }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-start">
|
||||||
|
<Button type="submit">
|
||||||
|
Update preferences
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
98
apps/www/src/examples/forms/components/DisplayForm.vue
Normal file
98
apps/www/src/examples/forms/components/DisplayForm.vue
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import * as z from 'zod'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
import { Label } from '@/lib/registry/new-york/ui/label'
|
||||||
|
import { Separator } from '@/lib/registry/new-york/ui/separator'
|
||||||
|
import { Checkbox } from '@/lib/registry/new-york/ui/checkbox'
|
||||||
|
import { Button } from '@/lib/registry/new-york/ui/button'
|
||||||
|
|
||||||
|
const displayForm = ref({
|
||||||
|
items: ['recents', 'home'],
|
||||||
|
})
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
id: 'recents',
|
||||||
|
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 displayFormSchema = z.object({
|
||||||
|
items: z.array(z.string()).refine(value => value.some(item => item), {
|
||||||
|
message: 'You have to select at least one item.',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
type DisplayFormValues = z.infer<typeof displayFormSchema>
|
||||||
|
const errors = ref<z.ZodFormattedError<DisplayFormValues> | null>(null)
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
const result = displayFormSchema.safeParse(displayForm.value)
|
||||||
|
if (!result.success) {
|
||||||
|
errors.value = result.error.format()
|
||||||
|
console.log(errors.value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Form submitted!', displayForm.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-medium">
|
||||||
|
Display
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
Turn items on or off to control what's displayed in the app.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<form @submit.prevent="handleSubmit">
|
||||||
|
<div class="mb-4">
|
||||||
|
<Label for="sidebar" :class="cn('text-md', errors?.items && 'text-destructive')">
|
||||||
|
Sidebar
|
||||||
|
</Label>
|
||||||
|
<span class="text-xs text-muted-foreground">
|
||||||
|
Select the items you want to display in the sidebar.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-for="item in items" :key="item.id" class="pb-1">
|
||||||
|
<div class="flex flex-row items-center space-x-3 space-y-0">
|
||||||
|
<Checkbox :id="item.id" :checked="displayForm.items.includes(item.id)" @change="displayForm.items.includes(item.id) ? displayForm.items.splice(displayForm.items.indexOf(item.id), 1) : displayForm.items.push(item.id)" />
|
||||||
|
<Label :for="item.id">{{ item.label }}</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-start mt-4">
|
||||||
|
<Button type="submit">
|
||||||
|
Update display
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
176
apps/www/src/examples/forms/components/NotificationsForm.vue
Normal file
176
apps/www/src/examples/forms/components/NotificationsForm.vue
Normal file
|
|
@ -0,0 +1,176 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import * as z from 'zod'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
import { Label } from '@/lib/registry/new-york/ui/label'
|
||||||
|
import { Separator } from '@/lib/registry/new-york/ui/separator'
|
||||||
|
import { RadioGroup, RadioGroupItem } from '@/lib/registry/new-york/ui/radio-group'
|
||||||
|
import { Switch } from '@/lib/registry/new-york/ui/switch'
|
||||||
|
import { Checkbox } from '@/lib/registry/new-york/ui/checkbox'
|
||||||
|
import { Button } from '@/lib/registry/new-york/ui/button'
|
||||||
|
|
||||||
|
const notificationsForm = ref({
|
||||||
|
type: '',
|
||||||
|
mobile: false,
|
||||||
|
communication_emails: false,
|
||||||
|
social_emails: true,
|
||||||
|
marketing_emails: false,
|
||||||
|
security_emails: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const notificationsFormSchema = z.object({
|
||||||
|
type: z.enum(['all', 'mentions', 'none'], {
|
||||||
|
required_error: 'You need to select a notification type.',
|
||||||
|
}),
|
||||||
|
mobile: z.boolean().default(false).optional(),
|
||||||
|
communication_emails: z.boolean().default(false).optional(),
|
||||||
|
social_emails: z.boolean().default(false).optional(),
|
||||||
|
marketing_emails: z.boolean().default(false).optional(),
|
||||||
|
security_emails: z.boolean(),
|
||||||
|
})
|
||||||
|
|
||||||
|
type notificationsFormValues = z.infer<typeof notificationsFormSchema>
|
||||||
|
const errors = ref<z.ZodFormattedError<notificationsFormValues> | null>(null)
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
const result = notificationsFormSchema.safeParse(notificationsForm.value)
|
||||||
|
if (!result.success) {
|
||||||
|
errors.value = result.error.format()
|
||||||
|
console.log(errors.value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Form submitted!', notificationsForm.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-medium">
|
||||||
|
Notifications
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
Configure how you receive notifications.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<form class="space-y-8" @submit.prevent="handleSubmit">
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="font" :class="cn('text-sm', errors?.type && 'text-destructive')">
|
||||||
|
Notify me about...
|
||||||
|
</Label>
|
||||||
|
<RadioGroup
|
||||||
|
v-model="notificationsForm.type"
|
||||||
|
default-value="all"
|
||||||
|
class="flex flex-col space-y-1"
|
||||||
|
>
|
||||||
|
<div class="flex items-center space-x-3 space-y-0">
|
||||||
|
<RadioGroupItem id="all" value="all" />
|
||||||
|
<Label for="all">All new messages</Label>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-3 space-y-0">
|
||||||
|
<RadioGroupItem id="mentions" value="mentions" />
|
||||||
|
<Label for="mentions">Direct messages and mentions</Label>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-3 space-y-0">
|
||||||
|
<RadioGroupItem id="none" value="none" />
|
||||||
|
<Label for="none">Nothing</Label>
|
||||||
|
</div>
|
||||||
|
<div v-if="errors?.type" class="text-sm text-destructive">
|
||||||
|
<span v-for="error in errors.type._errors" :key="error">{{ error }}</span>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<h3 class="mb-4 text-lg font-medium">
|
||||||
|
Email Notifications
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||||
|
<div class="space-y-0.5">
|
||||||
|
<Label class="text-base" for="communication_emails">
|
||||||
|
Communication emails
|
||||||
|
</Label>
|
||||||
|
<span class="text-xs text-muted-foreground">
|
||||||
|
Receive emails about your account activity.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="communication_emails"
|
||||||
|
v-model:checked="notificationsForm.communication_emails"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||||
|
<div class="space-y-0.5">
|
||||||
|
<Label class="text-base" for="marketing_emails">
|
||||||
|
Marketing emails
|
||||||
|
</Label>
|
||||||
|
<span class="text-xs text-muted-foreground">
|
||||||
|
Receive emails about new products, features, and more.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="marketing_emails"
|
||||||
|
v-model:checked="notificationsForm.marketing_emails"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||||
|
<div class="space-y-0.5">
|
||||||
|
<Label class="text-base" for="social_emails">
|
||||||
|
Social emails
|
||||||
|
</Label>
|
||||||
|
<span class="text-xs text-muted-foreground">
|
||||||
|
Receive emails for friend requests, follows, and more.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="social_emails"
|
||||||
|
v-model:checked="notificationsForm.social_emails"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||||
|
<div class="space-y-0.5">
|
||||||
|
<Label class="text-base" for="security_emails">
|
||||||
|
Security emails
|
||||||
|
</Label>
|
||||||
|
<span class="text-xs text-muted-foreground">
|
||||||
|
Receive emails about your account activity and security.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="security_emails"
|
||||||
|
v-model:checked="notificationsForm.security_emails"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<div class="flex flex-row items-start space-x-3 space-y-0">
|
||||||
|
<Checkbox
|
||||||
|
id="mobile"
|
||||||
|
v-model:checked="notificationsForm.mobile"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Label for="mobile">
|
||||||
|
Use different settings for my mobile devices
|
||||||
|
</Label>
|
||||||
|
<span class="text-xs text-muted-foreground">
|
||||||
|
You can manage your mobile notifications in the {{ " " }}
|
||||||
|
<a href="/examples/forms">mobile settings</a> page.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-start">
|
||||||
|
<Button type="submit">
|
||||||
|
Update notifications
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
152
apps/www/src/examples/forms/components/ProfileForm.vue
Normal file
152
apps/www/src/examples/forms/components/ProfileForm.vue
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import * as z from 'zod'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
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 { Textarea } from '@/lib/registry/new-york/ui/textarea'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/lib/registry/new-york/ui/select'
|
||||||
|
import { Button } from '@/lib/registry/new-york/ui/button'
|
||||||
|
|
||||||
|
const verifiedEmails = ref(['m@example.com', 'm@google.com', 'm@support.com'])
|
||||||
|
|
||||||
|
const profileForm = ref({
|
||||||
|
username: '',
|
||||||
|
email: '',
|
||||||
|
bio: 'I own a computer.',
|
||||||
|
urls: [
|
||||||
|
{ value: 'https://shadcn.com' },
|
||||||
|
{ value: 'http://twitter.com/shadcn' },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const profileFormSchema = z.object({
|
||||||
|
username: z
|
||||||
|
.string()
|
||||||
|
.min(2, {
|
||||||
|
message: 'Username must be at least 2 characters.',
|
||||||
|
})
|
||||||
|
.max(30, {
|
||||||
|
message: 'Username must not be longer than 30 characters.',
|
||||||
|
}),
|
||||||
|
email: z
|
||||||
|
.string({
|
||||||
|
required_error: 'Please select an email to display.',
|
||||||
|
})
|
||||||
|
.email(),
|
||||||
|
bio: z.string().max(160, { message: 'Bio must not be longer than 160 characters.' }).min(4, { message: 'Bio must be at least 2 characters.' }),
|
||||||
|
urls: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
value: z.string().url({ message: 'Please enter a valid URL.' }),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
type ProfileFormValues = z.infer<typeof profileFormSchema>
|
||||||
|
const errors = ref<z.ZodFormattedError<ProfileFormValues> | null>(null)
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
const result = profileFormSchema.safeParse(profileForm.value)
|
||||||
|
if (!result.success) {
|
||||||
|
errors.value = result.error.format()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
errors.value = null
|
||||||
|
console.log('Form submitted!')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-medium">
|
||||||
|
Profile
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
This is how others will see you on the site.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<form class="space-y-8" @submit.prevent="handleSubmit">
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="username" :class="cn('text-sm', errors?.username && 'text-destructive')">
|
||||||
|
Username
|
||||||
|
</Label>
|
||||||
|
<Input id="username" v-model="profileForm.username" placeholder="shadcn" />
|
||||||
|
<span class="text-muted-foreground text-sm">
|
||||||
|
This is your public display name. It can be your real name or a pseudonym. You can only change this once every 30 days.
|
||||||
|
</span>
|
||||||
|
<div v-if="errors?.username" class="text-sm text-destructive">
|
||||||
|
<span v-for="error in errors.username._errors" :key="error">{{ error }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="email" :class="cn('text-sm', errors?.email && 'text-destructive')">
|
||||||
|
Email
|
||||||
|
</Label>
|
||||||
|
<Select id="email" v-model="profileForm.email">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select an email" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectItem v-for="email in verifiedEmails" :key="email" :value="email">
|
||||||
|
{{ email }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<span class="text-muted-foreground text-sm">
|
||||||
|
You can manage verified email addresses in your email settings.
|
||||||
|
</span>
|
||||||
|
<div v-if="errors?.email" class="text-sm text-destructive">
|
||||||
|
<span v-for="error in errors.email._errors" :key="error">{{ error }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="bio" :class="cn('text-sm', errors?.bio && 'text-destructive')">
|
||||||
|
Bio
|
||||||
|
</Label>
|
||||||
|
<Textarea id="bio" v-model="profileForm.bio" placeholder="Tell us about yourself." />
|
||||||
|
<span class="text-muted-foreground text-sm">
|
||||||
|
You can @mention other users and organizations to link to them.
|
||||||
|
</span>
|
||||||
|
<div v-if="errors?.bio" class="text-sm text-destructive">
|
||||||
|
<span v-for="error in errors.bio._errors" :key="error">{{ error }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="urls" :class="cn('text-sm', errors?.urls && 'text-destructive')">
|
||||||
|
URLs
|
||||||
|
</Label>
|
||||||
|
<Input v-for="(url, index) in profileForm.urls" id="urls" :key="index" v-model="url.value" />
|
||||||
|
<div v-if="errors?.urls" class="text-sm text-destructive">
|
||||||
|
<span v-for="error in errors.urls._errors" :key="error">{{ error }}</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
class="text-xs w-20 mt-2"
|
||||||
|
@click="profileForm.urls?.push({ value: '' })"
|
||||||
|
>
|
||||||
|
Add URL
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-start">
|
||||||
|
<Button type="submit">
|
||||||
|
Update profile
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
53
apps/www/src/examples/forms/components/SidebarNav.vue
Normal file
53
apps/www/src/examples/forms/components/SidebarNav.vue
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useRoute } from 'vitepress'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { Button } from '@/lib/registry/new-york/ui/button'
|
||||||
|
|
||||||
|
interface Item {
|
||||||
|
title: string
|
||||||
|
href: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const $route = useRoute()
|
||||||
|
|
||||||
|
const sidebarNavItems: Item[] = [
|
||||||
|
{
|
||||||
|
title: 'Profile',
|
||||||
|
href: '/examples/forms/forms',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Account',
|
||||||
|
href: '/examples/forms/account',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Appearance',
|
||||||
|
href: '/examples/forms/appearance',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Notifications',
|
||||||
|
href: '/examples/forms/notifications',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Display',
|
||||||
|
href: '/examples/forms/display',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<nav class="flex space-x-2 lg:flex-col lg:space-x-0 lg:space-y-1">
|
||||||
|
<Button
|
||||||
|
v-for="item in sidebarNavItems"
|
||||||
|
:key="item.title"
|
||||||
|
as="a"
|
||||||
|
:href="item.href"
|
||||||
|
variant="ghost"
|
||||||
|
:class="cn(
|
||||||
|
'w-full text-left justify-start',
|
||||||
|
$route.path === `${item.href}.html` && 'bg-muted hover:bg-muted',
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
{{ item.title }}
|
||||||
|
</Button>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
29
apps/www/src/examples/forms/layouts/FormsLayout.vue
Normal file
29
apps/www/src/examples/forms/layouts/FormsLayout.vue
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import SidebarNav from '../components/SidebarNav.vue'
|
||||||
|
import { Separator } from '@/lib/registry/new-york/ui/separator'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="md:hidden" />
|
||||||
|
<div class="hidden space-y-6 p-10 pb-16 md:block">
|
||||||
|
<div class="space-y-0.5">
|
||||||
|
<h2 class="text-2xl font-bold tracking-tight">
|
||||||
|
Settings
|
||||||
|
</h2>
|
||||||
|
<p class="text-muted-foreground">
|
||||||
|
Manage your account settings and set e-mail preferences.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Separator class="my-6" />
|
||||||
|
<div class="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0">
|
||||||
|
<aside class="-mx-4 lg:w-1/5">
|
||||||
|
<SidebarNav />
|
||||||
|
</aside>
|
||||||
|
<div class="flex-1 lg:max-w-2xl">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -103,7 +103,7 @@ const columns: ColumnDef<Payment>[] = [
|
||||||
return h(Button, {
|
return h(Button, {
|
||||||
variant: 'ghost',
|
variant: 'ghost',
|
||||||
onClick: () => column.toggleSorting(column.getIsSorted() === 'asc'),
|
onClick: () => column.toggleSorting(column.getIsSorted() === 'asc'),
|
||||||
}, ['Email', h(ArrowUpDown, { class: 'ml-2 h-4 w-4' })])
|
}, () => ['Email', h(ArrowUpDown, { class: 'ml-2 h-4 w-4' })])
|
||||||
},
|
},
|
||||||
cell: ({ row }) => h('div', { class: 'lowercase' }, row.getValue('email')),
|
cell: ({ row }) => h('div', { class: 'lowercase' }, row.getValue('email')),
|
||||||
},
|
},
|
||||||
|
|
@ -128,9 +128,9 @@ const columns: ColumnDef<Payment>[] = [
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const payment = row.original
|
const payment = row.original
|
||||||
|
|
||||||
return h(DropdownAction, {
|
return h('div', { class: 'relative' }, h(DropdownAction, {
|
||||||
payment,
|
payment,
|
||||||
})
|
}))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
@ -162,7 +162,7 @@ const table = useVueTable({
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<div class="flex items-center py-4">
|
<div class="flex gap-2 items-center py-4">
|
||||||
<Input
|
<Input
|
||||||
class="max-w-sm"
|
class="max-w-sm"
|
||||||
placeholder="Filter emails..."
|
placeholder="Filter emails..."
|
||||||
|
|
@ -244,7 +244,6 @@ const table = useVueTable({
|
||||||
:disabled="!table.getCanNextPage()"
|
:disabled="!table.getCanNextPage()"
|
||||||
@click="table.nextPage()"
|
@click="table.nextPage()"
|
||||||
>
|
>
|
||||||
>
|
|
||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RadioGroupRoot, type RadioGroupRootProps } from 'radix-vue'
|
import { RadioGroupRoot, type RadioGroupRootEmits, type RadioGroupRootProps } from 'radix-vue'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn, useEmitAsProps } from '@/lib/utils'
|
||||||
|
|
||||||
const props = defineProps<RadioGroupRootProps & { class?: string }>()
|
const props = defineProps<RadioGroupRootProps & { class?: string }>()
|
||||||
|
|
||||||
|
const emits = defineEmits<RadioGroupRootEmits>()
|
||||||
|
|
||||||
|
const emitsAsProps = useEmitAsProps(emits)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<RadioGroupRoot :class="cn('grid gap-2', props.class)" v-bind="props">
|
<RadioGroupRoot :class="cn('grid gap-2', props.class)" v-bind="{ ...props, ...emitsAsProps }">
|
||||||
<slot />
|
<slot />
|
||||||
</RadioGroupRoot>
|
</RadioGroupRoot>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RadioGroupRoot, type RadioGroupRootProps } from 'radix-vue'
|
import { RadioGroupRoot, type RadioGroupRootEmits, type RadioGroupRootProps } from 'radix-vue'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn, useEmitAsProps } from '@/lib/utils'
|
||||||
|
|
||||||
const props = defineProps<RadioGroupRootProps & { class?: string }>()
|
const props = defineProps<RadioGroupRootProps & { class?: string }>()
|
||||||
|
|
||||||
|
const emits = defineEmits<RadioGroupRootEmits>()
|
||||||
|
|
||||||
|
const emitsAsProps = useEmitAsProps(emits)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<RadioGroupRoot :class="cn('grid gap-2', props.class)" v-bind="props">
|
<RadioGroupRoot :class="cn('grid gap-2', props.class)" v-bind="{ ...props, ...emitsAsProps }">
|
||||||
<slot />
|
<slot />
|
||||||
</RadioGroupRoot>
|
</RadioGroupRoot>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ const emitsAsProps = useEmitAsProps(emits)
|
||||||
>
|
>
|
||||||
<SelectViewport
|
<SelectViewport
|
||||||
:class="
|
:class="
|
||||||
cn('p-1',
|
cn('p-0',
|
||||||
position === 'popper'
|
position === 'popper'
|
||||||
&& 'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]')"
|
&& 'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]')"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ const props = withDefaults(
|
||||||
v-bind="props"
|
v-bind="props"
|
||||||
:class="[
|
:class="[
|
||||||
cn(
|
cn(
|
||||||
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
'flex h-9 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
props.class,
|
props.class,
|
||||||
),
|
),
|
||||||
props.invalid
|
props.invalid
|
||||||
|
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"include": [".contentlayer/generated", "src/**/*.ts", "scripts/**/*.ts"],
|
|
||||||
"exclude": ["node_modules"],
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2019",
|
|
||||||
"lib": [
|
|
||||||
"es2019",
|
|
||||||
"DOM"
|
|
||||||
],
|
|
||||||
"module": "ESNext",
|
|
||||||
"declaration": true,
|
|
||||||
"strict": true,
|
|
||||||
"strictNullChecks": true,
|
|
||||||
"noImplicitThis": true,
|
|
||||||
"alwaysStrict": true,
|
|
||||||
"noImplicitReturns": true,
|
|
||||||
"noFallthroughCasesInSwitch": false,
|
|
||||||
"experimentalDecorators": true,
|
|
||||||
"strictPropertyInitialization": false,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"typeRoots": [
|
|
||||||
"./node_modules/@types"
|
|
||||||
],
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"moduleResolution": "node"
|
|
||||||
},
|
|
||||||
"ts-node": {
|
|
||||||
"esm": true,
|
|
||||||
"experimentalSpecifierResolution": "node",
|
|
||||||
"transpileOnly": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -60,7 +60,7 @@
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"ora": "^7.0.1",
|
"ora": "^7.0.1",
|
||||||
"prompts": "^2.4.2",
|
"prompts": "^2.4.2",
|
||||||
"radix-vue": "^0.1.32",
|
"radix-vue": "^0.1.34",
|
||||||
"recast": "^0.23.4",
|
"recast": "^0.23.4",
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"ts-morph": "^19.0.0",
|
"ts-morph": "^19.0.0",
|
||||||
|
|
|
||||||
554
pnpm-lock.yaml
554
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user