feat: new ui
This commit is contained in:
parent
2bea72596e
commit
522cf7dfb9
43
components.d.ts
vendored
43
components.d.ts
vendored
@ -7,9 +7,25 @@ export {}
|
|||||||
/* prettier-ignore */
|
/* prettier-ignore */
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
|
Accordion: typeof import('./src/components/ui/accordion/Accordion.vue')['default']
|
||||||
|
AccordionContent: typeof import('./src/components/ui/accordion/AccordionContent.vue')['default']
|
||||||
|
AccordionItem: typeof import('./src/components/ui/accordion/AccordionItem.vue')['default']
|
||||||
|
AccordionTrigger: typeof import('./src/components/ui/accordion/AccordionTrigger.vue')['default']
|
||||||
|
AutoForm: typeof import('./src/components/ui/auto-form/AutoForm.vue')['default']
|
||||||
|
AutoFormField: typeof import('./src/components/ui/auto-form/AutoFormField.vue')['default']
|
||||||
|
AutoFormFieldArray: typeof import('./src/components/ui/auto-form/AutoFormFieldArray.vue')['default']
|
||||||
|
AutoFormFieldBoolean: typeof import('./src/components/ui/auto-form/AutoFormFieldBoolean.vue')['default']
|
||||||
|
AutoFormFieldDate: typeof import('./src/components/ui/auto-form/AutoFormFieldDate.vue')['default']
|
||||||
|
AutoFormFieldEnum: typeof import('./src/components/ui/auto-form/AutoFormFieldEnum.vue')['default']
|
||||||
|
AutoFormFieldFile: typeof import('./src/components/ui/auto-form/AutoFormFieldFile.vue')['default']
|
||||||
|
AutoFormFieldInput: typeof import('./src/components/ui/auto-form/AutoFormFieldInput.vue')['default']
|
||||||
|
AutoFormFieldNumber: typeof import('./src/components/ui/auto-form/AutoFormFieldNumber.vue')['default']
|
||||||
|
AutoFormFieldObject: typeof import('./src/components/ui/auto-form/AutoFormFieldObject.vue')['default']
|
||||||
|
AutoFormLabel: typeof import('./src/components/ui/auto-form/AutoFormLabel.vue')['default']
|
||||||
Avatar: typeof import('./src/components/ui/avatar/Avatar.vue')['default']
|
Avatar: typeof import('./src/components/ui/avatar/Avatar.vue')['default']
|
||||||
AvatarFallback: typeof import('./src/components/ui/avatar/AvatarFallback.vue')['default']
|
AvatarFallback: typeof import('./src/components/ui/avatar/AvatarFallback.vue')['default']
|
||||||
AvatarImage: typeof import('./src/components/ui/avatar/AvatarImage.vue')['default']
|
AvatarImage: typeof import('./src/components/ui/avatar/AvatarImage.vue')['default']
|
||||||
|
Badge: typeof import('./src/components/ui/badge/Badge.vue')['default']
|
||||||
Breadcrumb: typeof import('./src/components/ui/breadcrumb/Breadcrumb.vue')['default']
|
Breadcrumb: typeof import('./src/components/ui/breadcrumb/Breadcrumb.vue')['default']
|
||||||
BreadcrumbEllipsis: typeof import('./src/components/ui/breadcrumb/BreadcrumbEllipsis.vue')['default']
|
BreadcrumbEllipsis: typeof import('./src/components/ui/breadcrumb/BreadcrumbEllipsis.vue')['default']
|
||||||
BreadcrumbItem: typeof import('./src/components/ui/breadcrumb/BreadcrumbItem.vue')['default']
|
BreadcrumbItem: typeof import('./src/components/ui/breadcrumb/BreadcrumbItem.vue')['default']
|
||||||
@ -18,6 +34,19 @@ declare module 'vue' {
|
|||||||
BreadcrumbPage: typeof import('./src/components/ui/breadcrumb/BreadcrumbPage.vue')['default']
|
BreadcrumbPage: typeof import('./src/components/ui/breadcrumb/BreadcrumbPage.vue')['default']
|
||||||
BreadcrumbSeparator: typeof import('./src/components/ui/breadcrumb/BreadcrumbSeparator.vue')['default']
|
BreadcrumbSeparator: typeof import('./src/components/ui/breadcrumb/BreadcrumbSeparator.vue')['default']
|
||||||
Button: typeof import('./src/components/ui/button/Button.vue')['default']
|
Button: typeof import('./src/components/ui/button/Button.vue')['default']
|
||||||
|
Calendar: typeof import('./src/components/ui/calendar/Calendar.vue')['default']
|
||||||
|
CalendarCell: typeof import('./src/components/ui/calendar/CalendarCell.vue')['default']
|
||||||
|
CalendarCellTrigger: typeof import('./src/components/ui/calendar/CalendarCellTrigger.vue')['default']
|
||||||
|
CalendarGrid: typeof import('./src/components/ui/calendar/CalendarGrid.vue')['default']
|
||||||
|
CalendarGridBody: typeof import('./src/components/ui/calendar/CalendarGridBody.vue')['default']
|
||||||
|
CalendarGridHead: typeof import('./src/components/ui/calendar/CalendarGridHead.vue')['default']
|
||||||
|
CalendarGridRow: typeof import('./src/components/ui/calendar/CalendarGridRow.vue')['default']
|
||||||
|
CalendarHeadCell: typeof import('./src/components/ui/calendar/CalendarHeadCell.vue')['default']
|
||||||
|
CalendarHeader: typeof import('./src/components/ui/calendar/CalendarHeader.vue')['default']
|
||||||
|
CalendarHeading: typeof import('./src/components/ui/calendar/CalendarHeading.vue')['default']
|
||||||
|
CalendarNextButton: typeof import('./src/components/ui/calendar/CalendarNextButton.vue')['default']
|
||||||
|
CalendarPrevButton: typeof import('./src/components/ui/calendar/CalendarPrevButton.vue')['default']
|
||||||
|
Checkbox: typeof import('./src/components/ui/checkbox/Checkbox.vue')['default']
|
||||||
Collapsible: typeof import('./src/components/ui/collapsible/Collapsible.vue')['default']
|
Collapsible: typeof import('./src/components/ui/collapsible/Collapsible.vue')['default']
|
||||||
CollapsibleContent: typeof import('./src/components/ui/collapsible/CollapsibleContent.vue')['default']
|
CollapsibleContent: typeof import('./src/components/ui/collapsible/CollapsibleContent.vue')['default']
|
||||||
CollapsibleTrigger: typeof import('./src/components/ui/collapsible/CollapsibleTrigger.vue')['default']
|
CollapsibleTrigger: typeof import('./src/components/ui/collapsible/CollapsibleTrigger.vue')['default']
|
||||||
@ -35,7 +64,13 @@ declare module 'vue' {
|
|||||||
DropdownMenuSubContent: typeof import('./src/components/ui/dropdown-menu/DropdownMenuSubContent.vue')['default']
|
DropdownMenuSubContent: typeof import('./src/components/ui/dropdown-menu/DropdownMenuSubContent.vue')['default']
|
||||||
DropdownMenuSubTrigger: typeof import('./src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue')['default']
|
DropdownMenuSubTrigger: typeof import('./src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue')['default']
|
||||||
DropdownMenuTrigger: typeof import('./src/components/ui/dropdown-menu/DropdownMenuTrigger.vue')['default']
|
DropdownMenuTrigger: typeof import('./src/components/ui/dropdown-menu/DropdownMenuTrigger.vue')['default']
|
||||||
|
FormControl: typeof import('./src/components/ui/form/FormControl.vue')['default']
|
||||||
|
FormDescription: typeof import('./src/components/ui/form/FormDescription.vue')['default']
|
||||||
|
FormItem: typeof import('./src/components/ui/form/FormItem.vue')['default']
|
||||||
|
FormLabel: typeof import('./src/components/ui/form/FormLabel.vue')['default']
|
||||||
|
FormMessage: typeof import('./src/components/ui/form/FormMessage.vue')['default']
|
||||||
Image: typeof import('./src/components/Image.vue')['default']
|
Image: typeof import('./src/components/Image.vue')['default']
|
||||||
|
ImageContainer: typeof import('./src/components/ImageContainer.vue')['default']
|
||||||
Input: typeof import('./src/components/ui/input/Input.vue')['default']
|
Input: typeof import('./src/components/ui/input/Input.vue')['default']
|
||||||
Label: typeof import('./src/components/ui/label/Label.vue')['default']
|
Label: typeof import('./src/components/ui/label/Label.vue')['default']
|
||||||
Loading: typeof import('./src/components/Loading.vue')['default']
|
Loading: typeof import('./src/components/Loading.vue')['default']
|
||||||
@ -53,6 +88,12 @@ declare module 'vue' {
|
|||||||
NumberFieldDecrement: typeof import('./src/components/ui/number-field/NumberFieldDecrement.vue')['default']
|
NumberFieldDecrement: typeof import('./src/components/ui/number-field/NumberFieldDecrement.vue')['default']
|
||||||
NumberFieldIncrement: typeof import('./src/components/ui/number-field/NumberFieldIncrement.vue')['default']
|
NumberFieldIncrement: typeof import('./src/components/ui/number-field/NumberFieldIncrement.vue')['default']
|
||||||
NumberFieldInput: typeof import('./src/components/ui/number-field/NumberFieldInput.vue')['default']
|
NumberFieldInput: typeof import('./src/components/ui/number-field/NumberFieldInput.vue')['default']
|
||||||
|
ParamsCard: typeof import('./src/components/ParamsCard.vue')['default']
|
||||||
|
Popover: typeof import('./src/components/ui/popover/Popover.vue')['default']
|
||||||
|
PopoverContent: typeof import('./src/components/ui/popover/PopoverContent.vue')['default']
|
||||||
|
PopoverTrigger: typeof import('./src/components/ui/popover/PopoverTrigger.vue')['default']
|
||||||
|
RadioGroup: typeof import('./src/components/ui/radio-group/RadioGroup.vue')['default']
|
||||||
|
RadioGroupItem: typeof import('./src/components/ui/radio-group/RadioGroupItem.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
Select: typeof import('./src/components/ui/select/Select.vue')['default']
|
Select: typeof import('./src/components/ui/select/Select.vue')['default']
|
||||||
@ -100,7 +141,9 @@ declare module 'vue' {
|
|||||||
SidebarSeparator: typeof import('./src/components/ui/sidebar/SidebarSeparator.vue')['default']
|
SidebarSeparator: typeof import('./src/components/ui/sidebar/SidebarSeparator.vue')['default']
|
||||||
SidebarTrigger: typeof import('./src/components/ui/sidebar/SidebarTrigger.vue')['default']
|
SidebarTrigger: typeof import('./src/components/ui/sidebar/SidebarTrigger.vue')['default']
|
||||||
Skeleton: typeof import('./src/components/ui/skeleton/Skeleton.vue')['default']
|
Skeleton: typeof import('./src/components/ui/skeleton/Skeleton.vue')['default']
|
||||||
|
Switch: typeof import('./src/components/ui/switch/Switch.vue')['default']
|
||||||
TestHeader: typeof import('./src/components/TestHeader.vue')['default']
|
TestHeader: typeof import('./src/components/TestHeader.vue')['default']
|
||||||
|
Textarea: typeof import('./src/components/ui/textarea/Textarea.vue')['default']
|
||||||
TheFooter: typeof import('./src/components/TheFooter.vue')['default']
|
TheFooter: typeof import('./src/components/TheFooter.vue')['default']
|
||||||
TheHeader: typeof import('./src/components/TheHeader.vue')['default']
|
TheHeader: typeof import('./src/components/TheHeader.vue')['default']
|
||||||
Tooltip: typeof import('./src/components/ui/tooltip/Tooltip.vue')['default']
|
Tooltip: typeof import('./src/components/ui/tooltip/Tooltip.vue')['default']
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@unocss/preset-icons": "^0.65.4",
|
"@unocss/preset-icons": "^0.65.4",
|
||||||
|
"@vee-validate/zod": "^4.15.0",
|
||||||
"@vueuse/core": "^12.0.0",
|
"@vueuse/core": "^12.0.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@ -22,8 +23,10 @@
|
|||||||
"shadcn-vue": "^0.11.3",
|
"shadcn-vue": "^0.11.3",
|
||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.5.5",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"vee-validate": "^4.15.0",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^4.5.0"
|
"vue-router": "^4.5.0",
|
||||||
|
"zod": "^3.24.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@antfu/eslint-config": "^3.12.0",
|
"@antfu/eslint-config": "^3.12.0",
|
||||||
|
|||||||
46
pnpm-lock.yaml
generated
46
pnpm-lock.yaml
generated
@ -15,6 +15,9 @@ importers:
|
|||||||
'@unocss/preset-icons':
|
'@unocss/preset-icons':
|
||||||
specifier: ^0.65.4
|
specifier: ^0.65.4
|
||||||
version: 0.65.4
|
version: 0.65.4
|
||||||
|
'@vee-validate/zod':
|
||||||
|
specifier: ^4.15.0
|
||||||
|
version: 4.15.0(vue@3.5.13(typescript@5.6.3))(zod@3.24.1)
|
||||||
'@vueuse/core':
|
'@vueuse/core':
|
||||||
specifier: ^12.0.0
|
specifier: ^12.0.0
|
||||||
version: 12.0.0(typescript@5.6.3)
|
version: 12.0.0(typescript@5.6.3)
|
||||||
@ -39,12 +42,18 @@ importers:
|
|||||||
tailwindcss-animate:
|
tailwindcss-animate:
|
||||||
specifier: ^1.0.7
|
specifier: ^1.0.7
|
||||||
version: 1.0.7
|
version: 1.0.7
|
||||||
|
vee-validate:
|
||||||
|
specifier: ^4.15.0
|
||||||
|
version: 4.15.0(vue@3.5.13(typescript@5.6.3))
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.5.13
|
specifier: ^3.5.13
|
||||||
version: 3.5.13(typescript@5.6.3)
|
version: 3.5.13(typescript@5.6.3)
|
||||||
vue-router:
|
vue-router:
|
||||||
specifier: ^4.5.0
|
specifier: ^4.5.0
|
||||||
version: 4.5.0(vue@3.5.13(typescript@5.6.3))
|
version: 4.5.0(vue@3.5.13(typescript@5.6.3))
|
||||||
|
zod:
|
||||||
|
specifier: ^3.24.1
|
||||||
|
version: 3.24.1
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@antfu/eslint-config':
|
'@antfu/eslint-config':
|
||||||
specifier: ^3.12.0
|
specifier: ^3.12.0
|
||||||
@ -1194,6 +1203,11 @@ packages:
|
|||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
'@vee-validate/zod@4.15.0':
|
||||||
|
resolution: {integrity: sha512-MpvIKiyg9X5yD8bJW0no2AU7wtR2T5mrvD9tuPRiie951sU2n6QKgMV38qKKOiqFBCxsMSjIuLLLV3V5kVE4nQ==}
|
||||||
|
peerDependencies:
|
||||||
|
zod: ^3.24.0
|
||||||
|
|
||||||
'@vitejs/plugin-vue@5.2.1':
|
'@vitejs/plugin-vue@5.2.1':
|
||||||
resolution: {integrity: sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==}
|
resolution: {integrity: sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==}
|
||||||
engines: {node: ^18.0.0 || >=20.0.0}
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
@ -1460,6 +1474,9 @@ packages:
|
|||||||
'@vue/devtools-api@6.6.4':
|
'@vue/devtools-api@6.6.4':
|
||||||
resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
|
resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
|
||||||
|
|
||||||
|
'@vue/devtools-api@7.7.0':
|
||||||
|
resolution: {integrity: sha512-bHEv6kT85BHtyGgDhE07bAUMAy7zpv6nnR004nSTd0wWMrAOtcrYoXO5iyr20Hkf5jR8obQOfS3byW+I3l2CCA==}
|
||||||
|
|
||||||
'@vue/devtools-core@7.7.0':
|
'@vue/devtools-core@7.7.0':
|
||||||
resolution: {integrity: sha512-tSO3pghV5RZGSonZ87S2fOGru3X93epmar5IjZOWjHxH6XSwnK5UbR2aW5puZV+LgLoVYrcNou3krSo5k1F31g==}
|
resolution: {integrity: sha512-tSO3pghV5RZGSonZ87S2fOGru3X93epmar5IjZOWjHxH6XSwnK5UbR2aW5puZV+LgLoVYrcNou3krSo5k1F31g==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -3635,6 +3652,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==}
|
resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
type-fest@4.31.0:
|
||||||
|
resolution: {integrity: sha512-yCxltHW07Nkhv/1F6wWBr8kz+5BGMfP+RbRSYFnegVb0qV/UMT0G0ElBloPVerqn4M2ZV80Ir1FtCcYv1cT6vQ==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
typescript@5.6.3:
|
typescript@5.6.3:
|
||||||
resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==}
|
resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==}
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
@ -3785,6 +3806,11 @@ packages:
|
|||||||
validate-npm-package-license@3.0.4:
|
validate-npm-package-license@3.0.4:
|
||||||
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
|
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
|
||||||
|
|
||||||
|
vee-validate@4.15.0:
|
||||||
|
resolution: {integrity: sha512-PGJh1QCFwCBjbHu5aN6vB8macYVWrajbDvgo1Y/8fz9n/RVIkLmZCJDpUgu7+mUmCOPMxeyq7vXUOhbwAqdXcA==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^3.4.26
|
||||||
|
|
||||||
vite-hot-client@0.2.4:
|
vite-hot-client@0.2.4:
|
||||||
resolution: {integrity: sha512-a1nzURqO7DDmnXqabFOliz908FRmIppkBKsJthS8rbe8hBEXwEwe4C3Pp33Z1JoFCYfVL4kTOMLKk0ZZxREIeA==}
|
resolution: {integrity: sha512-a1nzURqO7DDmnXqabFOliz908FRmIppkBKsJthS8rbe8hBEXwEwe4C3Pp33Z1JoFCYfVL4kTOMLKk0ZZxREIeA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -5140,6 +5166,14 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@vee-validate/zod@4.15.0(vue@3.5.13(typescript@5.6.3))(zod@3.24.1)':
|
||||||
|
dependencies:
|
||||||
|
type-fest: 4.31.0
|
||||||
|
vee-validate: 4.15.0(vue@3.5.13(typescript@5.6.3))
|
||||||
|
zod: 3.24.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- vue
|
||||||
|
|
||||||
'@vitejs/plugin-vue@5.2.1(vite@6.0.3(@types/node@22.10.2)(jiti@2.4.0)(stylus@0.57.0)(tsx@4.19.2)(yaml@2.6.1))(vue@3.5.13(typescript@5.6.3))':
|
'@vitejs/plugin-vue@5.2.1(vite@6.0.3(@types/node@22.10.2)(jiti@2.4.0)(stylus@0.57.0)(tsx@4.19.2)(yaml@2.6.1))(vue@3.5.13(typescript@5.6.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
vite: 6.0.3(@types/node@22.10.2)(jiti@2.4.0)(stylus@0.57.0)(tsx@4.19.2)(yaml@2.6.1)
|
vite: 6.0.3(@types/node@22.10.2)(jiti@2.4.0)(stylus@0.57.0)(tsx@4.19.2)(yaml@2.6.1)
|
||||||
@ -5563,6 +5597,10 @@ snapshots:
|
|||||||
|
|
||||||
'@vue/devtools-api@6.6.4': {}
|
'@vue/devtools-api@6.6.4': {}
|
||||||
|
|
||||||
|
'@vue/devtools-api@7.7.0':
|
||||||
|
dependencies:
|
||||||
|
'@vue/devtools-kit': 7.7.0
|
||||||
|
|
||||||
'@vue/devtools-core@7.7.0(vite@6.0.3(@types/node@22.10.2)(jiti@2.4.0)(stylus@0.57.0)(tsx@4.19.2)(yaml@2.6.1))(vue@3.5.13(typescript@5.6.3))':
|
'@vue/devtools-core@7.7.0(vite@6.0.3(@types/node@22.10.2)(jiti@2.4.0)(stylus@0.57.0)(tsx@4.19.2)(yaml@2.6.1))(vue@3.5.13(typescript@5.6.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/devtools-kit': 7.7.0
|
'@vue/devtools-kit': 7.7.0
|
||||||
@ -8085,6 +8123,8 @@ snapshots:
|
|||||||
|
|
||||||
type-fest@0.8.1: {}
|
type-fest@0.8.1: {}
|
||||||
|
|
||||||
|
type-fest@4.31.0: {}
|
||||||
|
|
||||||
typescript@5.6.3: {}
|
typescript@5.6.3: {}
|
||||||
|
|
||||||
ufo@1.5.4: {}
|
ufo@1.5.4: {}
|
||||||
@ -8328,6 +8368,12 @@ snapshots:
|
|||||||
spdx-correct: 3.2.0
|
spdx-correct: 3.2.0
|
||||||
spdx-expression-parse: 3.0.1
|
spdx-expression-parse: 3.0.1
|
||||||
|
|
||||||
|
vee-validate@4.15.0(vue@3.5.13(typescript@5.6.3)):
|
||||||
|
dependencies:
|
||||||
|
'@vue/devtools-api': 7.7.0
|
||||||
|
type-fest: 4.31.0
|
||||||
|
vue: 3.5.13(typescript@5.6.3)
|
||||||
|
|
||||||
vite-hot-client@0.2.4(vite@6.0.3(@types/node@22.10.2)(jiti@2.4.0)(stylus@0.57.0)(tsx@4.19.2)(yaml@2.6.1)):
|
vite-hot-client@0.2.4(vite@6.0.3(@types/node@22.10.2)(jiti@2.4.0)(stylus@0.57.0)(tsx@4.19.2)(yaml@2.6.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
vite: 6.0.3(@types/node@22.10.2)(jiti@2.4.0)(stylus@0.57.0)(tsx@4.19.2)(yaml@2.6.1)
|
vite: 6.0.3(@types/node@22.10.2)(jiti@2.4.0)(stylus@0.57.0)(tsx@4.19.2)(yaml@2.6.1)
|
||||||
|
|||||||
@ -184,9 +184,7 @@ const data = {
|
|||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="jusitfy-center w-full flex flex-1 flex-col items-center gap-4 p-4 pt-0">
|
<RouterView />
|
||||||
<RouterView />
|
|
||||||
</div>
|
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,24 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const props = defineProps<{
|
defineProps<{
|
||||||
imageUrl: string
|
imageUrl: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
function download() {
|
|
||||||
const a = document.createElement('a')
|
|
||||||
a.href = props.imageUrl
|
|
||||||
a.download = 'image.png'
|
|
||||||
a.click()
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<img id="image_" class="w-400" :src="imageUrl">
|
<img id="image_" class="w-400" :src="imageUrl">
|
||||||
<div my-2 w-full>
|
|
||||||
<Button class="w-2/3" @click="download">
|
|
||||||
下载
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
20
src/components/ImageContainer.vue
Normal file
20
src/components/ImageContainer.vue
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
defineProps<{
|
||||||
|
imageUrl: string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="relative h-full min-h-[50vh] w-full flex flex-col rounded-xl bg-muted/50 p-4 lg:col-span-2">
|
||||||
|
<Badge variant="outline" class="absolute right-3 top-3">
|
||||||
|
Output
|
||||||
|
</Badge>
|
||||||
|
<div class="flex-1" />
|
||||||
|
<Image :image-url="imageUrl" />
|
||||||
|
|
||||||
|
<Button type="submit" size="sm" class="ml-auto gap-1.5">
|
||||||
|
下载图片
|
||||||
|
<CornerDownLeft class="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
43
src/components/ParamsCard.vue
Normal file
43
src/components/ParamsCard.vue
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { ZodObject } from 'zod'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
schema: ZodObject<any, any, any> | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
submit: [value: {
|
||||||
|
[x: string]: any
|
||||||
|
}]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Drawer>
|
||||||
|
<DrawerTrigger as-child class="min-w-[20rem]">
|
||||||
|
<Button variant="ghost" size="icon" class="md:hidden">
|
||||||
|
<Settings class="size-4" />
|
||||||
|
<span class="sr-only">Settings</span>
|
||||||
|
</Button>
|
||||||
|
</DrawerTrigger>
|
||||||
|
<DrawerContent class="max-h-[80vh]">
|
||||||
|
<DrawerHeader>
|
||||||
|
<DrawerTitle />
|
||||||
|
</DrawerHeader>
|
||||||
|
<form class="grid min-w-[20rem] w-full items-start gap-6 overflow-auto p-4 pt-0">
|
||||||
|
<fieldset class="grid gap-6 border rounded-lg p-4">
|
||||||
|
<legend class="px-1 text-sm font-medium -ml-1">
|
||||||
|
Settings
|
||||||
|
</legend>
|
||||||
|
<div class="grid gap-3">
|
||||||
|
<AutoForm v-if="schema" :schema="schema" @submit="(e) => $emit('submit', e)">
|
||||||
|
<Button type="submit" class="w-full">
|
||||||
|
绘制
|
||||||
|
</Button>
|
||||||
|
</AutoForm>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
</template>
|
||||||
19
src/components/ui/accordion/Accordion.vue
Normal file
19
src/components/ui/accordion/Accordion.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
AccordionRoot,
|
||||||
|
type AccordionRootEmits,
|
||||||
|
type AccordionRootProps,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from 'radix-vue'
|
||||||
|
|
||||||
|
const props = defineProps<AccordionRootProps>()
|
||||||
|
const emits = defineEmits<AccordionRootEmits>()
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(props, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AccordionRoot v-bind="forwarded">
|
||||||
|
<slot />
|
||||||
|
</AccordionRoot>
|
||||||
|
</template>
|
||||||
24
src/components/ui/accordion/AccordionContent.vue
Normal file
24
src/components/ui/accordion/AccordionContent.vue
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { AccordionContent, type AccordionContentProps } from 'radix-vue'
|
||||||
|
import { computed, type HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<AccordionContentProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AccordionContent
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
class="overflow-hidden text-sm transition-all data-[state=open]:animate-accordion-down data-[state=closed]:animate-accordion-up"
|
||||||
|
>
|
||||||
|
<div :class="cn('pb-4 pt-0', props.class)">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</template>
|
||||||
24
src/components/ui/accordion/AccordionItem.vue
Normal file
24
src/components/ui/accordion/AccordionItem.vue
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { AccordionItem, type AccordionItemProps, useForwardProps } from 'radix-vue'
|
||||||
|
import { computed, type HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<AccordionItemProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AccordionItem
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="cn('border-b', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</AccordionItem>
|
||||||
|
</template>
|
||||||
39
src/components/ui/accordion/AccordionTrigger.vue
Normal file
39
src/components/ui/accordion/AccordionTrigger.vue
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ChevronDown } from 'lucide-vue-next'
|
||||||
|
import {
|
||||||
|
AccordionHeader,
|
||||||
|
AccordionTrigger,
|
||||||
|
type AccordionTriggerProps,
|
||||||
|
} from 'radix-vue'
|
||||||
|
import { computed, type HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<AccordionTriggerProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AccordionHeader class="flex">
|
||||||
|
<AccordionTrigger
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
<slot name="icon">
|
||||||
|
<ChevronDown
|
||||||
|
class="h-4 w-4 shrink-0 transition-transform duration-200"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
|
</AccordionTrigger>
|
||||||
|
</AccordionHeader>
|
||||||
|
</template>
|
||||||
4
src/components/ui/accordion/index.ts
Normal file
4
src/components/ui/accordion/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export { default as Accordion } from './Accordion.vue'
|
||||||
|
export { default as AccordionContent } from './AccordionContent.vue'
|
||||||
|
export { default as AccordionItem } from './AccordionItem.vue'
|
||||||
|
export { default as AccordionTrigger } from './AccordionTrigger.vue'
|
||||||
105
src/components/ui/auto-form/AutoForm.vue
Normal file
105
src/components/ui/auto-form/AutoForm.vue
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<script setup lang="ts" generic="T extends ZodObjectOrWrapped">
|
||||||
|
import type { FormContext, GenericObject } from 'vee-validate'
|
||||||
|
import type { z, ZodAny } from 'zod'
|
||||||
|
import type { Config, ConfigItem, Dependency, Shape } from './interface'
|
||||||
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
|
import { computed, toRefs } from 'vue'
|
||||||
|
import { Form } from '~/components/ui/form'
|
||||||
|
import AutoFormField from './AutoFormField.vue'
|
||||||
|
import { provideDependencies } from './dependencies'
|
||||||
|
import { getBaseSchema, getBaseType, getDefaultValueInZodStack, getObjectFormSchema, type ZodObjectOrWrapped } from './utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
schema: T
|
||||||
|
form?: FormContext<GenericObject>
|
||||||
|
fieldConfig?: Config<z.infer<T>>
|
||||||
|
dependencies?: Dependency<z.infer<T>>[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emits = defineEmits<{
|
||||||
|
submit: [event: z.infer<T>]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { dependencies } = toRefs(props)
|
||||||
|
provideDependencies(dependencies)
|
||||||
|
|
||||||
|
const shapes = computed(() => {
|
||||||
|
// @ts-expect-error ignore {} not assignable to object
|
||||||
|
const val: { [key in keyof T]: Shape } = {}
|
||||||
|
const baseSchema = getObjectFormSchema(props.schema)
|
||||||
|
const shape = baseSchema.shape
|
||||||
|
Object.keys(shape).forEach((name) => {
|
||||||
|
const item = shape[name] as ZodAny
|
||||||
|
const baseItem = getBaseSchema(item) as ZodAny
|
||||||
|
let options = (baseItem && 'values' in baseItem._def) ? baseItem._def.values as string[] : undefined
|
||||||
|
if (!Array.isArray(options) && typeof options === 'object')
|
||||||
|
options = Object.values(options)
|
||||||
|
|
||||||
|
val[name as keyof T] = {
|
||||||
|
type: getBaseType(item),
|
||||||
|
default: getDefaultValueInZodStack(item),
|
||||||
|
options,
|
||||||
|
required: !['ZodOptional', 'ZodNullable'].includes(item._def.typeName),
|
||||||
|
schema: baseItem,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return val
|
||||||
|
})
|
||||||
|
|
||||||
|
const fields = computed(() => {
|
||||||
|
// @ts-expect-error ignore {} not assignable to object
|
||||||
|
const val: { [key in keyof z.infer<T>]: { shape: Shape, fieldName: string, config: ConfigItem } } = {}
|
||||||
|
for (const key in shapes.value) {
|
||||||
|
const shape = shapes.value[key]
|
||||||
|
val[key as keyof z.infer<T>] = {
|
||||||
|
shape,
|
||||||
|
config: props.fieldConfig?.[key] as ConfigItem,
|
||||||
|
fieldName: key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
})
|
||||||
|
|
||||||
|
const formComponent = computed(() => props.form ? 'form' : Form)
|
||||||
|
const formComponentProps = computed(() => {
|
||||||
|
if (props.form) {
|
||||||
|
return {
|
||||||
|
onSubmit: props.form.handleSubmit(val => emits('submit', val)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const formSchema = toTypedSchema(props.schema)
|
||||||
|
return {
|
||||||
|
keepValues: true,
|
||||||
|
validationSchema: formSchema,
|
||||||
|
onSubmit: (val: GenericObject) => emits('submit', val),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component
|
||||||
|
:is="formComponent"
|
||||||
|
v-bind="formComponentProps"
|
||||||
|
>
|
||||||
|
<slot name="customAutoForm" :fields="fields">
|
||||||
|
<template v-for="(shape, key) of shapes" :key="key">
|
||||||
|
<slot
|
||||||
|
:shape="shape"
|
||||||
|
:name="key.toString() as keyof z.infer<T>"
|
||||||
|
:field-name="key.toString()"
|
||||||
|
:config="fieldConfig?.[key as keyof typeof fieldConfig] as ConfigItem"
|
||||||
|
>
|
||||||
|
<AutoFormField
|
||||||
|
:config="fieldConfig?.[key as keyof typeof fieldConfig] as ConfigItem"
|
||||||
|
:field-name="key.toString()"
|
||||||
|
:shape="shape"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
</slot>
|
||||||
|
|
||||||
|
<slot :shapes="shapes" />
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
45
src/components/ui/auto-form/AutoFormField.vue
Normal file
45
src/components/ui/auto-form/AutoFormField.vue
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<script setup lang="ts" generic="U extends ZodAny">
|
||||||
|
import type { ZodAny } from 'zod'
|
||||||
|
import type { Config, ConfigItem, Shape } from './interface'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { DEFAULT_ZOD_HANDLERS, INPUT_COMPONENTS } from './constant'
|
||||||
|
import useDependencies from './dependencies'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fieldName: string
|
||||||
|
shape: Shape
|
||||||
|
config?: ConfigItem | Config<U>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function isValidConfig(config: any): config is ConfigItem {
|
||||||
|
return !!config?.component
|
||||||
|
}
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
if (['ZodObject', 'ZodArray'].includes(props.shape?.type))
|
||||||
|
return { schema: props.shape?.schema }
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const { isDisabled, isHidden, isRequired, overrideOptions } = useDependencies(props.fieldName)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component
|
||||||
|
:is="isValidConfig(config)
|
||||||
|
? typeof config.component === 'string'
|
||||||
|
? INPUT_COMPONENTS[config.component!]
|
||||||
|
: config.component
|
||||||
|
: INPUT_COMPONENTS[DEFAULT_ZOD_HANDLERS[shape.type]] "
|
||||||
|
v-if="!isHidden"
|
||||||
|
:field-name="fieldName"
|
||||||
|
:label="shape.schema?.description"
|
||||||
|
:required="isRequired || shape.required"
|
||||||
|
:options="overrideOptions || shape.options"
|
||||||
|
:disabled="isDisabled"
|
||||||
|
:config="config"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
110
src/components/ui/auto-form/AutoFormFieldArray.vue
Normal file
110
src/components/ui/auto-form/AutoFormFieldArray.vue
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<script setup lang="ts" generic="T extends z.ZodAny">
|
||||||
|
import type { Config, ConfigItem } from './interface'
|
||||||
|
import { PlusIcon, TrashIcon } from 'lucide-vue-next'
|
||||||
|
import { FieldArray, FieldContextKey, useField } from 'vee-validate'
|
||||||
|
import { computed, provide } from 'vue'
|
||||||
|
import * as z from 'zod'
|
||||||
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '~/components/ui/accordion'
|
||||||
|
import { Button } from '~/components/ui/button'
|
||||||
|
import { FormItem, FormMessage } from '~/components/ui/form'
|
||||||
|
import { Separator } from '~/components/ui/separator'
|
||||||
|
import AutoFormField from './AutoFormField.vue'
|
||||||
|
import AutoFormLabel from './AutoFormLabel.vue'
|
||||||
|
import { beautifyObjectName, getBaseType } from './utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fieldName: string
|
||||||
|
required?: boolean
|
||||||
|
config?: Config<T>
|
||||||
|
schema?: z.ZodArray<T>
|
||||||
|
disabled?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function isZodArray(
|
||||||
|
item: z.ZodArray<any> | z.ZodDefault<any>,
|
||||||
|
): item is z.ZodArray<any> {
|
||||||
|
return item instanceof z.ZodArray
|
||||||
|
}
|
||||||
|
|
||||||
|
function isZodDefault(
|
||||||
|
item: z.ZodArray<any> | z.ZodDefault<any>,
|
||||||
|
): item is z.ZodDefault<any> {
|
||||||
|
return item instanceof z.ZodDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemShape = computed(() => {
|
||||||
|
if (!props.schema)
|
||||||
|
return
|
||||||
|
|
||||||
|
const schema: z.ZodAny = isZodArray(props.schema)
|
||||||
|
? props.schema._def.type
|
||||||
|
: isZodDefault(props.schema)
|
||||||
|
// @ts-expect-error missing schema
|
||||||
|
? props.schema._def.innerType._def.type
|
||||||
|
: null
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: getBaseType(schema),
|
||||||
|
schema,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const fieldContext = useField(props.fieldName)
|
||||||
|
// @ts-expect-error ignore missing `id`
|
||||||
|
provide(FieldContextKey, fieldContext)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FieldArray v-slot="{ fields, remove, push }" as="section" :name="fieldName">
|
||||||
|
<slot v-bind="props">
|
||||||
|
<Accordion type="multiple" class="w-full" :disabled="disabled" as-child collapsible>
|
||||||
|
<FormItem>
|
||||||
|
<AccordionItem :value="fieldName" class="border-none">
|
||||||
|
<AccordionTrigger>
|
||||||
|
<AutoFormLabel class="text-base" :required="required">
|
||||||
|
{{ schema?.description || beautifyObjectName(fieldName) }}
|
||||||
|
</AutoFormLabel>
|
||||||
|
</AccordionTrigger>
|
||||||
|
|
||||||
|
<AccordionContent>
|
||||||
|
<template v-for="(field, index) of fields" :key="field.key">
|
||||||
|
<div class="mb-4 p-1">
|
||||||
|
<AutoFormField
|
||||||
|
:field-name="`${fieldName}[${index}]`"
|
||||||
|
:label="fieldName"
|
||||||
|
:shape="itemShape!"
|
||||||
|
:config="config as ConfigItem"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex justify-end !my-4">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="icon"
|
||||||
|
variant="secondary"
|
||||||
|
@click="remove(index)"
|
||||||
|
>
|
||||||
|
<TrashIcon :size="16" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Separator v-if="!field.isLast" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
class="mt-4 flex items-center"
|
||||||
|
@click="push(null)"
|
||||||
|
>
|
||||||
|
<PlusIcon class="mr-2" :size="16" />
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</AccordionContent>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</AccordionItem>
|
||||||
|
</FormItem>
|
||||||
|
</Accordion>
|
||||||
|
</slot>
|
||||||
|
</FieldArray>
|
||||||
|
</template>
|
||||||
41
src/components/ui/auto-form/AutoFormFieldBoolean.vue
Normal file
41
src/components/ui/auto-form/AutoFormFieldBoolean.vue
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FieldProps } from './interface'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { Checkbox } from '~/components/ui/checkbox'
|
||||||
|
import { FormControl, FormDescription, FormField, FormItem, FormMessage } from '~/components/ui/form'
|
||||||
|
import { Switch } from '~/components/ui/switch'
|
||||||
|
import AutoFormLabel from './AutoFormLabel.vue'
|
||||||
|
import { beautifyObjectName } from './utils'
|
||||||
|
|
||||||
|
const props = defineProps<FieldProps>()
|
||||||
|
|
||||||
|
const booleanComponent = computed(() => props.config?.component === 'switch' ? Switch : Checkbox)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FormField v-slot="slotProps" :name="fieldName">
|
||||||
|
<FormItem>
|
||||||
|
<div class="mb-3 flex items-center gap-3 space-y-0">
|
||||||
|
<FormControl>
|
||||||
|
<slot v-bind="slotProps">
|
||||||
|
<component
|
||||||
|
:is="booleanComponent"
|
||||||
|
v-bind="{ ...slotProps.componentField }"
|
||||||
|
:disabled="disabled"
|
||||||
|
:checked="slotProps.componentField.modelValue"
|
||||||
|
@update:checked="slotProps.componentField['onUpdate:modelValue']"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
|
</FormControl>
|
||||||
|
<AutoFormLabel v-if="!config?.hideLabel" :required="required">
|
||||||
|
{{ config?.label || beautifyObjectName(label ?? fieldName) }}
|
||||||
|
</AutoFormLabel>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormDescription v-if="config?.description">
|
||||||
|
{{ config.description }}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</template>
|
||||||
57
src/components/ui/auto-form/AutoFormFieldDate.vue
Normal file
57
src/components/ui/auto-form/AutoFormFieldDate.vue
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FieldProps } from './interface'
|
||||||
|
import { DateFormatter, getLocalTimeZone } from '@internationalized/date'
|
||||||
|
import { CalendarIcon } from 'lucide-vue-next'
|
||||||
|
import { Button } from '~/components/ui/button'
|
||||||
|
import { Calendar } from '~/components/ui/calendar'
|
||||||
|
import { FormControl, FormDescription, FormField, FormItem, FormMessage } from '~/components/ui/form'
|
||||||
|
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from '~/components/ui/popover'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
import AutoFormLabel from './AutoFormLabel.vue'
|
||||||
|
import { beautifyObjectName } from './utils'
|
||||||
|
|
||||||
|
defineProps<FieldProps>()
|
||||||
|
|
||||||
|
const df = new DateFormatter('en-US', {
|
||||||
|
dateStyle: 'long',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FormField v-slot="slotProps" :name="fieldName">
|
||||||
|
<FormItem>
|
||||||
|
<AutoFormLabel v-if="!config?.hideLabel" :required="required">
|
||||||
|
{{ config?.label || beautifyObjectName(label ?? fieldName) }}
|
||||||
|
</AutoFormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<slot v-bind="slotProps">
|
||||||
|
<div>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger as-child :disabled="disabled">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
:class="cn(
|
||||||
|
'w-full justify-start text-left font-normal',
|
||||||
|
!slotProps.componentField.modelValue && 'text-muted-foreground',
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<CalendarIcon class="mr-2 h-4 w-4" :size="16" />
|
||||||
|
{{ slotProps.componentField.modelValue ? df.format(slotProps.componentField.modelValue.toDate(getLocalTimeZone())) : "Pick a date" }}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent class="w-auto p-0">
|
||||||
|
<Calendar initial-focus v-bind="slotProps.componentField" />
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormDescription v-if="config?.description">
|
||||||
|
{{ config.description }}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</template>
|
||||||
49
src/components/ui/auto-form/AutoFormFieldEnum.vue
Normal file
49
src/components/ui/auto-form/AutoFormFieldEnum.vue
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FieldProps } from './interface'
|
||||||
|
import { FormControl, FormDescription, FormField, FormItem, FormMessage } from '~/components/ui/form'
|
||||||
|
import { Label } from '~/components/ui/label'
|
||||||
|
import { RadioGroup, RadioGroupItem } from '~/components/ui/radio-group'
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '~/components/ui/select'
|
||||||
|
import AutoFormLabel from './AutoFormLabel.vue'
|
||||||
|
import { beautifyObjectName } from './utils'
|
||||||
|
|
||||||
|
defineProps<FieldProps & {
|
||||||
|
options?: string[]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FormField v-slot="slotProps" :name="fieldName">
|
||||||
|
<FormItem>
|
||||||
|
<AutoFormLabel v-if="!config?.hideLabel" :required="required">
|
||||||
|
{{ config?.label || beautifyObjectName(label ?? fieldName) }}
|
||||||
|
</AutoFormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<slot v-bind="slotProps">
|
||||||
|
<RadioGroup v-if="config?.component === 'radio'" :disabled="disabled" orientation="vertical" v-bind="{ ...slotProps.componentField }">
|
||||||
|
<div v-for="(option, index) in options" :key="option" class="mb-2 flex items-center gap-3 space-y-0">
|
||||||
|
<RadioGroupItem :id="`${option}-${index}`" :value="option" />
|
||||||
|
<Label :for="`${option}-${index}`">{{ beautifyObjectName(option) }}</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
<Select v-else :disabled="disabled" v-bind="{ ...slotProps.componentField }">
|
||||||
|
<SelectTrigger class="w-full">
|
||||||
|
<SelectValue :placeholder="config?.inputProps?.placeholder" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem v-for="option in options" :key="option" :value="option">
|
||||||
|
{{ beautifyObjectName(option) }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</slot>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormDescription v-if="config?.description">
|
||||||
|
{{ config.description }}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</template>
|
||||||
74
src/components/ui/auto-form/AutoFormFieldFile.vue
Normal file
74
src/components/ui/auto-form/AutoFormFieldFile.vue
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FieldProps } from './interface'
|
||||||
|
import { TrashIcon } from 'lucide-vue-next'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { Button } from '~/components/ui/button'
|
||||||
|
import { FormControl, FormDescription, FormField, FormItem, FormMessage } from '~/components/ui/form'
|
||||||
|
import { Input } from '~/components/ui/input'
|
||||||
|
import AutoFormLabel from './AutoFormLabel.vue'
|
||||||
|
import { beautifyObjectName } from './utils'
|
||||||
|
|
||||||
|
defineProps<FieldProps>()
|
||||||
|
|
||||||
|
const inputFile = ref<File>()
|
||||||
|
async function parseFileAsString(file: File | undefined): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onloadend = () => {
|
||||||
|
resolve(reader.result as string)
|
||||||
|
}
|
||||||
|
reader.onerror = (err) => {
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FormField v-slot="slotProps" :name="fieldName">
|
||||||
|
<FormItem v-bind="$attrs">
|
||||||
|
<AutoFormLabel v-if="!config?.hideLabel" :required="required">
|
||||||
|
{{ config?.label || beautifyObjectName(label ?? fieldName) }}
|
||||||
|
</AutoFormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<slot v-bind="slotProps">
|
||||||
|
<Input
|
||||||
|
v-if="!inputFile"
|
||||||
|
type="file"
|
||||||
|
v-bind="{ ...config?.inputProps }"
|
||||||
|
:disabled="disabled"
|
||||||
|
@change="async (ev: InputEvent) => {
|
||||||
|
const file = (ev.target as HTMLInputElement).files?.[0]
|
||||||
|
inputFile = file
|
||||||
|
const parsed = await parseFileAsString(file)
|
||||||
|
slotProps.componentField.onInput(parsed)
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<div v-else class="h-10 w-full flex items-center justify-between border border-input rounded-md bg-transparent py-1 pl-3 pr-1 text-sm shadow-sm transition-colors">
|
||||||
|
<p>{{ inputFile?.name }}</p>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
class="h-[26px] w-[26px]"
|
||||||
|
aria-label="Remove file"
|
||||||
|
type="button"
|
||||||
|
@click="() => {
|
||||||
|
inputFile = undefined
|
||||||
|
slotProps.componentField.onInput(undefined)
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<TrashIcon :size="16" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription v-if="config?.description">
|
||||||
|
{{ config.description }}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</template>
|
||||||
36
src/components/ui/auto-form/AutoFormFieldInput.vue
Normal file
36
src/components/ui/auto-form/AutoFormFieldInput.vue
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FieldProps } from './interface'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { FormControl, FormDescription, FormField, FormItem, FormMessage } from '~/components/ui/form'
|
||||||
|
import { Input } from '~/components/ui/input'
|
||||||
|
import { Textarea } from '~/components/ui/textarea'
|
||||||
|
import AutoFormLabel from './AutoFormLabel.vue'
|
||||||
|
import { beautifyObjectName } from './utils'
|
||||||
|
|
||||||
|
const props = defineProps<FieldProps>()
|
||||||
|
const inputComponent = computed(() => props.config?.component === 'textarea' ? Textarea : Input)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FormField v-slot="slotProps" :name="fieldName">
|
||||||
|
<FormItem v-bind="$attrs">
|
||||||
|
<AutoFormLabel v-if="!config?.hideLabel" :required="required">
|
||||||
|
{{ config?.label || beautifyObjectName(label ?? fieldName) }}
|
||||||
|
</AutoFormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<slot v-bind="slotProps">
|
||||||
|
<component
|
||||||
|
:is="inputComponent"
|
||||||
|
type="text"
|
||||||
|
v-bind="{ ...slotProps.componentField, ...config?.inputProps }"
|
||||||
|
:disabled="disabled"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription v-if="config?.description">
|
||||||
|
{{ config.description }}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</template>
|
||||||
32
src/components/ui/auto-form/AutoFormFieldNumber.vue
Normal file
32
src/components/ui/auto-form/AutoFormFieldNumber.vue
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FieldProps } from './interface'
|
||||||
|
import { FormControl, FormDescription, FormField, FormItem, FormMessage } from '~/components/ui/form'
|
||||||
|
import { Input } from '~/components/ui/input'
|
||||||
|
import AutoFormLabel from './AutoFormLabel.vue'
|
||||||
|
import { beautifyObjectName } from './utils'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
defineProps<FieldProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FormField v-slot="slotProps" :name="fieldName">
|
||||||
|
<FormItem>
|
||||||
|
<AutoFormLabel v-if="!config?.hideLabel" :required="required">
|
||||||
|
{{ config?.label || beautifyObjectName(label ?? fieldName) }}
|
||||||
|
</AutoFormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<slot v-bind="slotProps">
|
||||||
|
<Input type="number" v-bind="{ ...slotProps.componentField, ...config?.inputProps }" :disabled="disabled" />
|
||||||
|
</slot>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription v-if="config?.description">
|
||||||
|
{{ config.description }}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</template>
|
||||||
78
src/components/ui/auto-form/AutoFormFieldObject.vue
Normal file
78
src/components/ui/auto-form/AutoFormFieldObject.vue
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<script setup lang="ts" generic="T extends ZodRawShape">
|
||||||
|
import type { ZodAny, ZodObject, ZodRawShape } from 'zod'
|
||||||
|
import type { Config, ConfigItem, Shape } from './interface'
|
||||||
|
import { FieldContextKey, useField } from 'vee-validate'
|
||||||
|
import { computed, provide } from 'vue'
|
||||||
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '~/components/ui/accordion'
|
||||||
|
import { FormItem } from '~/components/ui/form'
|
||||||
|
import AutoFormField from './AutoFormField.vue'
|
||||||
|
import AutoFormLabel from './AutoFormLabel.vue'
|
||||||
|
import { beautifyObjectName, getBaseSchema, getBaseType, getDefaultValueInZodStack } from './utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fieldName: string
|
||||||
|
required?: boolean
|
||||||
|
config?: Config<T>
|
||||||
|
schema?: ZodObject<T>
|
||||||
|
disabled?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const shapes = computed(() => {
|
||||||
|
// @ts-expect-error ignore {} not assignable to object
|
||||||
|
const val: { [key in keyof T]: Shape } = {}
|
||||||
|
|
||||||
|
if (!props.schema)
|
||||||
|
return
|
||||||
|
const shape = getBaseSchema(props.schema)?.shape
|
||||||
|
if (!shape)
|
||||||
|
return
|
||||||
|
Object.keys(shape).forEach((name) => {
|
||||||
|
const item = shape[name] as ZodAny
|
||||||
|
const baseItem = getBaseSchema(item) as ZodAny
|
||||||
|
let options = (baseItem && 'values' in baseItem._def) ? baseItem._def.values as string[] : undefined
|
||||||
|
if (!Array.isArray(options) && typeof options === 'object')
|
||||||
|
options = Object.values(options)
|
||||||
|
|
||||||
|
val[name as keyof T] = {
|
||||||
|
type: getBaseType(item),
|
||||||
|
default: getDefaultValueInZodStack(item),
|
||||||
|
options,
|
||||||
|
required: !['ZodOptional', 'ZodNullable'].includes(item._def.typeName),
|
||||||
|
schema: item,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return val
|
||||||
|
})
|
||||||
|
|
||||||
|
const fieldContext = useField(props.fieldName)
|
||||||
|
// @ts-expect-error ignore missing `id`
|
||||||
|
provide(FieldContextKey, fieldContext)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section>
|
||||||
|
<slot v-bind="props">
|
||||||
|
<Accordion type="single" as-child class="w-full" collapsible :disabled="disabled">
|
||||||
|
<FormItem>
|
||||||
|
<AccordionItem :value="fieldName" class="border-none">
|
||||||
|
<AccordionTrigger>
|
||||||
|
<AutoFormLabel class="text-base" :required="required">
|
||||||
|
{{ schema?.description || beautifyObjectName(fieldName) }}
|
||||||
|
</AutoFormLabel>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent class="p-1 space-y-5">
|
||||||
|
<template v-for="(shape, key) in shapes" :key="key">
|
||||||
|
<AutoFormField
|
||||||
|
:config="config?.[key as keyof typeof config] as ConfigItem"
|
||||||
|
:field-name="`${fieldName}.${key.toString()}`"
|
||||||
|
:label="key.toString()"
|
||||||
|
:shape="shape"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</FormItem>
|
||||||
|
</Accordion>
|
||||||
|
</slot>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
14
src/components/ui/auto-form/AutoFormLabel.vue
Normal file
14
src/components/ui/auto-form/AutoFormLabel.vue
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { FormLabel } from '~/components/ui/form'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
required?: boolean
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FormLabel>
|
||||||
|
<slot />
|
||||||
|
<span v-if="required" class="text-destructive"> *</span>
|
||||||
|
</FormLabel>
|
||||||
|
</template>
|
||||||
40
src/components/ui/auto-form/constant.ts
Normal file
40
src/components/ui/auto-form/constant.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import type { InputComponents } from './interface'
|
||||||
|
import AutoFormFieldArray from './AutoFormFieldArray.vue'
|
||||||
|
import AutoFormFieldBoolean from './AutoFormFieldBoolean.vue'
|
||||||
|
import AutoFormFieldDate from './AutoFormFieldDate.vue'
|
||||||
|
import AutoFormFieldEnum from './AutoFormFieldEnum.vue'
|
||||||
|
import AutoFormFieldFile from './AutoFormFieldFile.vue'
|
||||||
|
import AutoFormFieldInput from './AutoFormFieldInput.vue'
|
||||||
|
import AutoFormFieldNumber from './AutoFormFieldNumber.vue'
|
||||||
|
import AutoFormFieldObject from './AutoFormFieldObject.vue'
|
||||||
|
|
||||||
|
export const INPUT_COMPONENTS: InputComponents = {
|
||||||
|
date: AutoFormFieldDate,
|
||||||
|
select: AutoFormFieldEnum,
|
||||||
|
radio: AutoFormFieldEnum,
|
||||||
|
checkbox: AutoFormFieldBoolean,
|
||||||
|
switch: AutoFormFieldBoolean,
|
||||||
|
textarea: AutoFormFieldInput,
|
||||||
|
number: AutoFormFieldNumber,
|
||||||
|
string: AutoFormFieldInput,
|
||||||
|
file: AutoFormFieldFile,
|
||||||
|
array: AutoFormFieldArray,
|
||||||
|
object: AutoFormFieldObject,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define handlers for specific Zod types.
|
||||||
|
* You can expand this object to support more types.
|
||||||
|
*/
|
||||||
|
export const DEFAULT_ZOD_HANDLERS: {
|
||||||
|
[key: string]: keyof typeof INPUT_COMPONENTS
|
||||||
|
} = {
|
||||||
|
ZodString: 'string',
|
||||||
|
ZodBoolean: 'checkbox',
|
||||||
|
ZodDate: 'date',
|
||||||
|
ZodEnum: 'select',
|
||||||
|
ZodNativeEnum: 'select',
|
||||||
|
ZodNumber: 'number',
|
||||||
|
ZodArray: 'array',
|
||||||
|
ZodObject: 'object',
|
||||||
|
}
|
||||||
92
src/components/ui/auto-form/dependencies.ts
Normal file
92
src/components/ui/auto-form/dependencies.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import type { Ref } from 'vue'
|
||||||
|
import type * as z from 'zod'
|
||||||
|
import { createContext } from 'radix-vue'
|
||||||
|
import { useFieldValue, useFormValues } from 'vee-validate'
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { type Dependency, DependencyType, type EnumValues } from './interface'
|
||||||
|
import { getFromPath, getIndexIfArray } from './utils'
|
||||||
|
|
||||||
|
export const [injectDependencies, provideDependencies] = createContext<Ref<Dependency<z.infer<z.ZodObject<any>>>[] | undefined>>('AutoFormDependencies')
|
||||||
|
|
||||||
|
export default function useDependencies(
|
||||||
|
fieldName: string,
|
||||||
|
) {
|
||||||
|
const form = useFormValues()
|
||||||
|
// parsed test[0].age => test.age
|
||||||
|
const currentFieldName = fieldName.replace(/\[\d+\]/g, '')
|
||||||
|
const currentFieldValue = useFieldValue<any>(fieldName)
|
||||||
|
|
||||||
|
if (!form)
|
||||||
|
throw new Error('useDependencies should be used within <AutoForm>')
|
||||||
|
|
||||||
|
const dependencies = injectDependencies()
|
||||||
|
const isDisabled = ref(false)
|
||||||
|
const isHidden = ref(false)
|
||||||
|
const isRequired = ref(false)
|
||||||
|
const overrideOptions = ref<EnumValues | undefined>()
|
||||||
|
|
||||||
|
const currentFieldDependencies = computed(() => dependencies.value?.filter(
|
||||||
|
dependency => dependency.targetField === currentFieldName,
|
||||||
|
))
|
||||||
|
|
||||||
|
function getSourceValue(dep: Dependency<any>) {
|
||||||
|
const source = dep.sourceField as string
|
||||||
|
const index = getIndexIfArray(fieldName) ?? -1
|
||||||
|
const [sourceLast, ...sourceInitial] = source.split('.').toReversed()
|
||||||
|
const [_targetLast, ...targetInitial] = (dep.targetField as string).split('.').toReversed()
|
||||||
|
|
||||||
|
if (index >= 0 && sourceInitial.join(',') === targetInitial.join(',')) {
|
||||||
|
const [_currentLast, ...currentInitial] = fieldName.split('.').toReversed()
|
||||||
|
return getFromPath(form.value, currentInitial.join('.') + sourceLast)
|
||||||
|
}
|
||||||
|
|
||||||
|
return getFromPath(form.value, source)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceFieldValues = computed(() => currentFieldDependencies.value?.map(dep => getSourceValue(dep)))
|
||||||
|
|
||||||
|
const resetConditionState = () => {
|
||||||
|
isDisabled.value = false
|
||||||
|
isHidden.value = false
|
||||||
|
isRequired.value = false
|
||||||
|
overrideOptions.value = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
watch([sourceFieldValues, dependencies], () => {
|
||||||
|
resetConditionState()
|
||||||
|
currentFieldDependencies.value?.forEach((dep) => {
|
||||||
|
const sourceValue = getSourceValue(dep)
|
||||||
|
const conditionMet = dep.when(sourceValue, currentFieldValue.value)
|
||||||
|
|
||||||
|
switch (dep.type) {
|
||||||
|
case DependencyType.DISABLES:
|
||||||
|
if (conditionMet)
|
||||||
|
isDisabled.value = true
|
||||||
|
|
||||||
|
break
|
||||||
|
case DependencyType.REQUIRES:
|
||||||
|
if (conditionMet)
|
||||||
|
isRequired.value = true
|
||||||
|
|
||||||
|
break
|
||||||
|
case DependencyType.HIDES:
|
||||||
|
if (conditionMet)
|
||||||
|
isHidden.value = true
|
||||||
|
|
||||||
|
break
|
||||||
|
case DependencyType.SETS_OPTIONS:
|
||||||
|
if (conditionMet)
|
||||||
|
overrideOptions.value = dep.options
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, { immediate: true, deep: true })
|
||||||
|
|
||||||
|
return {
|
||||||
|
isDisabled,
|
||||||
|
isHidden,
|
||||||
|
isRequired,
|
||||||
|
overrideOptions,
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/components/ui/auto-form/index.ts
Normal file
15
src/components/ui/auto-form/index.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export { default as AutoForm } from './AutoForm.vue'
|
||||||
|
export { default as AutoFormField } from './AutoFormField.vue'
|
||||||
|
|
||||||
|
export { default as AutoFormFieldArray } from './AutoFormFieldArray.vue'
|
||||||
|
export { default as AutoFormFieldBoolean } from './AutoFormFieldBoolean.vue'
|
||||||
|
export { default as AutoFormFieldDate } from './AutoFormFieldDate.vue'
|
||||||
|
|
||||||
|
export { default as AutoFormFieldEnum } from './AutoFormFieldEnum.vue'
|
||||||
|
export { default as AutoFormFieldFile } from './AutoFormFieldFile.vue'
|
||||||
|
export { default as AutoFormFieldInput } from './AutoFormFieldInput.vue'
|
||||||
|
export { default as AutoFormFieldNumber } from './AutoFormFieldNumber.vue'
|
||||||
|
export { default as AutoFormFieldObject } from './AutoFormFieldObject.vue'
|
||||||
|
export { default as AutoFormLabel } from './AutoFormLabel.vue'
|
||||||
|
export type { Config, ConfigItem, FieldProps } from './interface'
|
||||||
|
export { getBaseSchema, getBaseType, getObjectFormSchema } from './utils'
|
||||||
95
src/components/ui/auto-form/interface.ts
Normal file
95
src/components/ui/auto-form/interface.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import type { Component, InputHTMLAttributes } from 'vue'
|
||||||
|
import type { z, ZodAny } from 'zod'
|
||||||
|
import type { INPUT_COMPONENTS } from './constant'
|
||||||
|
|
||||||
|
export interface FieldProps {
|
||||||
|
fieldName: string
|
||||||
|
label?: string
|
||||||
|
required?: boolean
|
||||||
|
config?: ConfigItem
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Shape {
|
||||||
|
type: string
|
||||||
|
default?: any
|
||||||
|
required?: boolean
|
||||||
|
options?: string[]
|
||||||
|
schema?: ZodAny
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InputComponents {
|
||||||
|
date: Component
|
||||||
|
select: Component
|
||||||
|
radio: Component
|
||||||
|
checkbox: Component
|
||||||
|
switch: Component
|
||||||
|
textarea: Component
|
||||||
|
number: Component
|
||||||
|
string: Component
|
||||||
|
file: Component
|
||||||
|
array: Component
|
||||||
|
object: Component
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigItem {
|
||||||
|
/** Value for the `FormLabel` */
|
||||||
|
label?: string
|
||||||
|
/** Value for the `FormDescription` */
|
||||||
|
description?: string
|
||||||
|
/** Pick which component to be rendered. */
|
||||||
|
component?: keyof typeof INPUT_COMPONENTS | Component
|
||||||
|
/** Hide `FormLabel`. */
|
||||||
|
hideLabel?: boolean
|
||||||
|
inputProps?: InputHTMLAttributes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define a type to unwrap an array
|
||||||
|
type UnwrapArray<T> = T extends (infer U)[] ? U : never
|
||||||
|
|
||||||
|
export type Config<SchemaType extends object> = {
|
||||||
|
// If SchemaType.key is an object, create a nested Config, otherwise ConfigItem
|
||||||
|
[Key in keyof SchemaType]?:
|
||||||
|
SchemaType[Key] extends any[]
|
||||||
|
? UnwrapArray<Config<SchemaType[Key]>>
|
||||||
|
: SchemaType[Key] extends object
|
||||||
|
? Config<SchemaType[Key]>
|
||||||
|
: ConfigItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DependencyType {
|
||||||
|
DISABLES,
|
||||||
|
REQUIRES,
|
||||||
|
HIDES,
|
||||||
|
SETS_OPTIONS,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BaseDependency<SchemaType extends z.infer<z.ZodObject<any, any>>> {
|
||||||
|
sourceField: keyof SchemaType
|
||||||
|
type: DependencyType
|
||||||
|
targetField: keyof SchemaType
|
||||||
|
when: (sourceFieldValue: any, targetFieldValue: any) => boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ValueDependency<SchemaType extends z.infer<z.ZodObject<any, any>>> =
|
||||||
|
BaseDependency<SchemaType> & {
|
||||||
|
type:
|
||||||
|
| DependencyType.DISABLES
|
||||||
|
| DependencyType.REQUIRES
|
||||||
|
| DependencyType.HIDES
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EnumValues = readonly [string, ...string[]]
|
||||||
|
|
||||||
|
export type OptionsDependency<
|
||||||
|
SchemaType extends z.infer<z.ZodObject<any, any>>,
|
||||||
|
> = BaseDependency<SchemaType> & {
|
||||||
|
type: DependencyType.SETS_OPTIONS
|
||||||
|
|
||||||
|
// Partial array of values from sourceField that will trigger the dependency
|
||||||
|
options: EnumValues
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Dependency<SchemaType extends z.infer<z.ZodObject<any, any>>> =
|
||||||
|
| ValueDependency<SchemaType>
|
||||||
|
| OptionsDependency<SchemaType>
|
||||||
171
src/components/ui/auto-form/utils.ts
Normal file
171
src/components/ui/auto-form/utils.ts
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
import type { z } from 'zod'
|
||||||
|
|
||||||
|
// TODO: This should support recursive ZodEffects but TypeScript doesn't allow circular type definitions.
|
||||||
|
export type ZodObjectOrWrapped =
|
||||||
|
| z.ZodObject<any, any>
|
||||||
|
| z.ZodEffects<z.ZodObject<any, any>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Beautify a camelCase string.
|
||||||
|
* e.g. "myString" -> "My String"
|
||||||
|
*/
|
||||||
|
export function beautifyObjectName(string: string) {
|
||||||
|
// Remove bracketed indices
|
||||||
|
// if numbers only return the string
|
||||||
|
let output = string.replace(/\[\d+\]/g, '').replace(/([A-Z])/g, ' $1')
|
||||||
|
output = output.charAt(0).toUpperCase() + output.slice(1)
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse string and extract the index
|
||||||
|
* @param string
|
||||||
|
* @returns index or undefined
|
||||||
|
*/
|
||||||
|
export function getIndexIfArray(string: string) {
|
||||||
|
const indexRegex = /\[(\d+)\]/
|
||||||
|
// Match the index
|
||||||
|
const match = string.match(indexRegex)
|
||||||
|
// Extract the index (number)
|
||||||
|
const index = match ? Number.parseInt(match[1]) : undefined
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the lowest level Zod type.
|
||||||
|
* This will unpack optionals, refinements, etc.
|
||||||
|
*/
|
||||||
|
export function getBaseSchema<
|
||||||
|
ChildType extends z.ZodAny | z.AnyZodObject = z.ZodAny,
|
||||||
|
>(schema: ChildType | z.ZodEffects<ChildType>): ChildType | null {
|
||||||
|
if (!schema)
|
||||||
|
return null
|
||||||
|
if ('innerType' in schema._def)
|
||||||
|
return getBaseSchema(schema._def.innerType as ChildType)
|
||||||
|
|
||||||
|
if ('schema' in schema._def)
|
||||||
|
return getBaseSchema(schema._def.schema as ChildType)
|
||||||
|
|
||||||
|
return schema as ChildType
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the type name of the lowest level Zod type.
|
||||||
|
* This will unpack optionals, refinements, etc.
|
||||||
|
*/
|
||||||
|
export function getBaseType(schema: z.ZodAny) {
|
||||||
|
const baseSchema = getBaseSchema(schema)
|
||||||
|
return baseSchema ? baseSchema._def.typeName : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for a "ZodDefault" in the Zod stack and return its value.
|
||||||
|
*/
|
||||||
|
export function getDefaultValueInZodStack(schema: z.ZodAny): any {
|
||||||
|
const typedSchema = schema as unknown as z.ZodDefault<
|
||||||
|
z.ZodNumber | z.ZodString
|
||||||
|
>
|
||||||
|
|
||||||
|
if (typedSchema._def.typeName === 'ZodDefault')
|
||||||
|
return typedSchema._def.defaultValue()
|
||||||
|
|
||||||
|
if ('innerType' in typedSchema._def) {
|
||||||
|
return getDefaultValueInZodStack(
|
||||||
|
typedSchema._def.innerType as unknown as z.ZodAny,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if ('schema' in typedSchema._def) {
|
||||||
|
return getDefaultValueInZodStack(
|
||||||
|
(typedSchema._def as any).schema as z.ZodAny,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getObjectFormSchema(
|
||||||
|
schema: ZodObjectOrWrapped,
|
||||||
|
): z.ZodObject<any, any> {
|
||||||
|
if (schema?._def.typeName === 'ZodEffects') {
|
||||||
|
const typedSchema = schema as z.ZodEffects<z.ZodObject<any, any>>
|
||||||
|
return getObjectFormSchema(typedSchema._def.schema)
|
||||||
|
}
|
||||||
|
return schema as z.ZodObject<any, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
function isIndex(value: unknown): value is number {
|
||||||
|
return Number(value) >= 0
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Constructs a path with dot paths for arrays to use brackets to be compatible with vee-validate path syntax
|
||||||
|
*/
|
||||||
|
export function normalizeFormPath(path: string): string {
|
||||||
|
const pathArr = path.split('.')
|
||||||
|
if (!pathArr.length)
|
||||||
|
return ''
|
||||||
|
|
||||||
|
let fullPath = String(pathArr[0])
|
||||||
|
for (let i = 1; i < pathArr.length; i++) {
|
||||||
|
if (isIndex(pathArr[i])) {
|
||||||
|
fullPath += `[${pathArr[i]}]`
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath += `.${pathArr[i]}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullPath
|
||||||
|
}
|
||||||
|
|
||||||
|
type NestedRecord = Record<string, unknown> | { [k: string]: NestedRecord }
|
||||||
|
/**
|
||||||
|
* Checks if the path opted out of nested fields using `[fieldName]` syntax
|
||||||
|
*/
|
||||||
|
export function isNotNestedPath(path: string) {
|
||||||
|
return /^\[.+\]$/.test(path)
|
||||||
|
}
|
||||||
|
function isObject(obj: unknown): obj is Record<string, unknown> {
|
||||||
|
return obj !== null && !!obj && typeof obj === 'object' && !Array.isArray(obj)
|
||||||
|
}
|
||||||
|
function isContainerValue(value: unknown): value is Record<string, unknown> {
|
||||||
|
return isObject(value) || Array.isArray(value)
|
||||||
|
}
|
||||||
|
function cleanupNonNestedPath(path: string) {
|
||||||
|
if (isNotNestedPath(path))
|
||||||
|
return path.replace(/\[|\]/g, '')
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a nested property value from an object
|
||||||
|
*/
|
||||||
|
export function getFromPath<TValue = unknown>(object: NestedRecord | undefined, path: string): TValue | undefined
|
||||||
|
export function getFromPath<TValue = unknown, TFallback = TValue>(
|
||||||
|
object: NestedRecord | undefined,
|
||||||
|
path: string,
|
||||||
|
fallback?: TFallback,
|
||||||
|
): TValue | TFallback
|
||||||
|
export function getFromPath<TValue = unknown, TFallback = TValue>(
|
||||||
|
object: NestedRecord | undefined,
|
||||||
|
path: string,
|
||||||
|
fallback?: TFallback,
|
||||||
|
): TValue | TFallback | undefined {
|
||||||
|
if (!object)
|
||||||
|
return fallback
|
||||||
|
|
||||||
|
if (isNotNestedPath(path))
|
||||||
|
return object[cleanupNonNestedPath(path)] as TValue | undefined
|
||||||
|
|
||||||
|
const resolvedValue = (path || '')
|
||||||
|
.split(/\.|\[(\d+)\]/)
|
||||||
|
.filter(Boolean)
|
||||||
|
.reduce((acc, propKey) => {
|
||||||
|
if (isContainerValue(acc) && propKey in acc)
|
||||||
|
return acc[propKey]
|
||||||
|
|
||||||
|
return fallback
|
||||||
|
}, object as unknown)
|
||||||
|
|
||||||
|
return resolvedValue as TValue | undefined
|
||||||
|
}
|
||||||
16
src/components/ui/badge/Badge.vue
Normal file
16
src/components/ui/badge/Badge.vue
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
import { type BadgeVariants, badgeVariants } from '.'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
variant?: BadgeVariants['variant']
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="cn(badgeVariants({ variant }), props.class)">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
25
src/components/ui/badge/index.ts
Normal file
25
src/components/ui/badge/index.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { cva, type VariantProps } from 'class-variance-authority'
|
||||||
|
|
||||||
|
export { default as Badge } from './Badge.vue'
|
||||||
|
|
||||||
|
export const badgeVariants = cva(
|
||||||
|
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
|
||||||
|
secondary:
|
||||||
|
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||||
|
destructive:
|
||||||
|
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
|
||||||
|
outline: 'text-foreground',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export type BadgeVariants = VariantProps<typeof badgeVariants>
|
||||||
60
src/components/ui/calendar/Calendar.vue
Normal file
60
src/components/ui/calendar/Calendar.vue
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { CalendarRoot, type CalendarRootEmits, type CalendarRootProps, useForwardPropsEmits } from 'radix-vue'
|
||||||
|
import { computed, type HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
import { CalendarCell, CalendarCellTrigger, CalendarGrid, CalendarGridBody, CalendarGridHead, CalendarGridRow, CalendarHeadCell, CalendarHeader, CalendarHeading, CalendarNextButton, CalendarPrevButton } from '.'
|
||||||
|
|
||||||
|
const props = defineProps<CalendarRootProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const emits = defineEmits<CalendarRootEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarRoot
|
||||||
|
v-slot="{ grid, weekDays }"
|
||||||
|
:class="cn('p-3', props.class)"
|
||||||
|
v-bind="forwarded"
|
||||||
|
>
|
||||||
|
<CalendarHeader>
|
||||||
|
<CalendarPrevButton />
|
||||||
|
<CalendarHeading />
|
||||||
|
<CalendarNextButton />
|
||||||
|
</CalendarHeader>
|
||||||
|
|
||||||
|
<div class="mt-4 flex flex-col gap-y-4 sm:flex-row sm:gap-x-4 sm:gap-y-0">
|
||||||
|
<CalendarGrid v-for="month in grid" :key="month.value.toString()">
|
||||||
|
<CalendarGridHead>
|
||||||
|
<CalendarGridRow>
|
||||||
|
<CalendarHeadCell
|
||||||
|
v-for="day in weekDays" :key="day"
|
||||||
|
>
|
||||||
|
{{ day }}
|
||||||
|
</CalendarHeadCell>
|
||||||
|
</CalendarGridRow>
|
||||||
|
</CalendarGridHead>
|
||||||
|
<CalendarGridBody>
|
||||||
|
<CalendarGridRow v-for="(weekDates, index) in month.rows" :key="`weekDate-${index}`" class="mt-2 w-full">
|
||||||
|
<CalendarCell
|
||||||
|
v-for="weekDate in weekDates"
|
||||||
|
:key="weekDate.toString()"
|
||||||
|
:date="weekDate"
|
||||||
|
>
|
||||||
|
<CalendarCellTrigger
|
||||||
|
:day="weekDate"
|
||||||
|
:month="month.value"
|
||||||
|
/>
|
||||||
|
</CalendarCell>
|
||||||
|
</CalendarGridRow>
|
||||||
|
</CalendarGridBody>
|
||||||
|
</CalendarGrid>
|
||||||
|
</div>
|
||||||
|
</CalendarRoot>
|
||||||
|
</template>
|
||||||
24
src/components/ui/calendar/CalendarCell.vue
Normal file
24
src/components/ui/calendar/CalendarCell.vue
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { CalendarCell, type CalendarCellProps, useForwardProps } from 'radix-vue'
|
||||||
|
import { computed, type HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<CalendarCellProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarCell
|
||||||
|
:class="cn('relative h-9 w-9 p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([data-selected])]:rounded-md [&:has([data-selected])]:bg-accent [&:has([data-selected][data-outside-view])]:bg-accent/50', props.class)"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</CalendarCell>
|
||||||
|
</template>
|
||||||
38
src/components/ui/calendar/CalendarCellTrigger.vue
Normal file
38
src/components/ui/calendar/CalendarCellTrigger.vue
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { CalendarCellTrigger, type CalendarCellTriggerProps, useForwardProps } from 'radix-vue'
|
||||||
|
import { computed, type HTMLAttributes } from 'vue'
|
||||||
|
import { buttonVariants } from '~/components/ui/button'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<CalendarCellTriggerProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarCellTrigger
|
||||||
|
:class="cn(
|
||||||
|
buttonVariants({ variant: 'ghost' }),
|
||||||
|
'h-9 w-9 p-0 font-normal',
|
||||||
|
'[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground',
|
||||||
|
// Selected
|
||||||
|
'data-[selected]:bg-primary data-[selected]:text-primary-foreground data-[selected]:opacity-100 data-[selected]:hover:bg-primary data-[selected]:hover:text-primary-foreground data-[selected]:focus:bg-primary data-[selected]:focus:text-primary-foreground',
|
||||||
|
// Disabled
|
||||||
|
'data-[disabled]:text-muted-foreground data-[disabled]:opacity-50',
|
||||||
|
// Unavailable
|
||||||
|
'data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through',
|
||||||
|
// Outside months
|
||||||
|
'data-[outside-view]:text-muted-foreground data-[outside-view]:opacity-50 [&[data-outside-view][data-selected]]:bg-accent/50 [&[data-outside-view][data-selected]]:text-muted-foreground [&[data-outside-view][data-selected]]:opacity-30',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</CalendarCellTrigger>
|
||||||
|
</template>
|
||||||
24
src/components/ui/calendar/CalendarGrid.vue
Normal file
24
src/components/ui/calendar/CalendarGrid.vue
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { CalendarGrid, type CalendarGridProps, useForwardProps } from 'radix-vue'
|
||||||
|
import { computed, type HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<CalendarGridProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarGrid
|
||||||
|
:class="cn('w-full border-collapse space-y-1', props.class)"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</CalendarGrid>
|
||||||
|
</template>
|
||||||
11
src/components/ui/calendar/CalendarGridBody.vue
Normal file
11
src/components/ui/calendar/CalendarGridBody.vue
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { CalendarGridBody, type CalendarGridBodyProps } from 'radix-vue'
|
||||||
|
|
||||||
|
const props = defineProps<CalendarGridBodyProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarGridBody v-bind="props">
|
||||||
|
<slot />
|
||||||
|
</CalendarGridBody>
|
||||||
|
</template>
|
||||||
11
src/components/ui/calendar/CalendarGridHead.vue
Normal file
11
src/components/ui/calendar/CalendarGridHead.vue
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { CalendarGridHead, type CalendarGridHeadProps } from 'radix-vue'
|
||||||
|
|
||||||
|
const props = defineProps<CalendarGridHeadProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarGridHead v-bind="props">
|
||||||
|
<slot />
|
||||||
|
</CalendarGridHead>
|
||||||
|
</template>
|
||||||
21
src/components/ui/calendar/CalendarGridRow.vue
Normal file
21
src/components/ui/calendar/CalendarGridRow.vue
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { CalendarGridRow, type CalendarGridRowProps, useForwardProps } from 'radix-vue'
|
||||||
|
import { computed, type HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<CalendarGridRowProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarGridRow :class="cn('flex', props.class)" v-bind="forwardedProps">
|
||||||
|
<slot />
|
||||||
|
</CalendarGridRow>
|
||||||
|
</template>
|
||||||
21
src/components/ui/calendar/CalendarHeadCell.vue
Normal file
21
src/components/ui/calendar/CalendarHeadCell.vue
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { CalendarHeadCell, type CalendarHeadCellProps, useForwardProps } from 'radix-vue'
|
||||||
|
import { computed, type HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<CalendarHeadCellProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarHeadCell :class="cn('w-9 rounded-md text-[0.8rem] font-normal text-muted-foreground', props.class)" v-bind="forwardedProps">
|
||||||
|
<slot />
|
||||||
|
</CalendarHeadCell>
|
||||||
|
</template>
|
||||||
21
src/components/ui/calendar/CalendarHeader.vue
Normal file
21
src/components/ui/calendar/CalendarHeader.vue
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { CalendarHeader, type CalendarHeaderProps, useForwardProps } from 'radix-vue'
|
||||||
|
import { computed, type HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<CalendarHeaderProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarHeader :class="cn('relative flex w-full items-center justify-between pt-1', props.class)" v-bind="forwardedProps">
|
||||||
|
<slot />
|
||||||
|
</CalendarHeader>
|
||||||
|
</template>
|
||||||
27
src/components/ui/calendar/CalendarHeading.vue
Normal file
27
src/components/ui/calendar/CalendarHeading.vue
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { CalendarHeading, type CalendarHeadingProps, useForwardProps } from 'radix-vue'
|
||||||
|
import { computed, type HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<CalendarHeadingProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarHeading
|
||||||
|
v-slot="{ headingValue }"
|
||||||
|
:class="cn('text-sm font-medium', props.class)"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
>
|
||||||
|
<slot :heading-value>
|
||||||
|
{{ headingValue }}
|
||||||
|
</slot>
|
||||||
|
</CalendarHeading>
|
||||||
|
</template>
|
||||||
32
src/components/ui/calendar/CalendarNextButton.vue
Normal file
32
src/components/ui/calendar/CalendarNextButton.vue
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ChevronRight } from 'lucide-vue-next'
|
||||||
|
import { CalendarNext, type CalendarNextProps, useForwardProps } from 'radix-vue'
|
||||||
|
import { computed, type HTMLAttributes } from 'vue'
|
||||||
|
import { buttonVariants } from '~/components/ui/button'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<CalendarNextProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarNext
|
||||||
|
:class="cn(
|
||||||
|
buttonVariants({ variant: 'outline' }),
|
||||||
|
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
>
|
||||||
|
<slot>
|
||||||
|
<ChevronRight class="h-4 w-4" />
|
||||||
|
</slot>
|
||||||
|
</CalendarNext>
|
||||||
|
</template>
|
||||||
32
src/components/ui/calendar/CalendarPrevButton.vue
Normal file
32
src/components/ui/calendar/CalendarPrevButton.vue
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ChevronLeft } from 'lucide-vue-next'
|
||||||
|
import { CalendarPrev, type CalendarPrevProps, useForwardProps } from 'radix-vue'
|
||||||
|
import { computed, type HTMLAttributes } from 'vue'
|
||||||
|
import { buttonVariants } from '~/components/ui/button'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<CalendarPrevProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarPrev
|
||||||
|
:class="cn(
|
||||||
|
buttonVariants({ variant: 'outline' }),
|
||||||
|
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
>
|
||||||
|
<slot>
|
||||||
|
<ChevronLeft class="h-4 w-4" />
|
||||||
|
</slot>
|
||||||
|
</CalendarPrev>
|
||||||
|
</template>
|
||||||
12
src/components/ui/calendar/index.ts
Normal file
12
src/components/ui/calendar/index.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export { default as Calendar } from './Calendar.vue'
|
||||||
|
export { default as CalendarCell } from './CalendarCell.vue'
|
||||||
|
export { default as CalendarCellTrigger } from './CalendarCellTrigger.vue'
|
||||||
|
export { default as CalendarGrid } from './CalendarGrid.vue'
|
||||||
|
export { default as CalendarGridBody } from './CalendarGridBody.vue'
|
||||||
|
export { default as CalendarGridHead } from './CalendarGridHead.vue'
|
||||||
|
export { default as CalendarGridRow } from './CalendarGridRow.vue'
|
||||||
|
export { default as CalendarHeadCell } from './CalendarHeadCell.vue'
|
||||||
|
export { default as CalendarHeader } from './CalendarHeader.vue'
|
||||||
|
export { default as CalendarHeading } from './CalendarHeading.vue'
|
||||||
|
export { default as CalendarNextButton } from './CalendarNextButton.vue'
|
||||||
|
export { default as CalendarPrevButton } from './CalendarPrevButton.vue'
|
||||||
33
src/components/ui/checkbox/Checkbox.vue
Normal file
33
src/components/ui/checkbox/Checkbox.vue
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { CheckboxRootEmits, CheckboxRootProps } from 'radix-vue'
|
||||||
|
import { Check } from 'lucide-vue-next'
|
||||||
|
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from 'radix-vue'
|
||||||
|
import { computed, type HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<CheckboxRootProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
const emits = defineEmits<CheckboxRootEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CheckboxRoot
|
||||||
|
v-bind="forwarded"
|
||||||
|
:class="
|
||||||
|
cn('peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
|
||||||
|
props.class)"
|
||||||
|
>
|
||||||
|
<CheckboxIndicator class="h-full w-full flex items-center justify-center text-current">
|
||||||
|
<slot>
|
||||||
|
<Check class="h-4 w-4" />
|
||||||
|
</slot>
|
||||||
|
</CheckboxIndicator>
|
||||||
|
</CheckboxRoot>
|
||||||
|
</template>
|
||||||
1
src/components/ui/checkbox/index.ts
Normal file
1
src/components/ui/checkbox/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as Checkbox } from './Checkbox.vue'
|
||||||
16
src/components/ui/form/FormControl.vue
Normal file
16
src/components/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>
|
||||||
20
src/components/ui/form/FormDescription.vue
Normal file
20
src/components/ui/form/FormDescription.vue
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
import { useFormField } from './useFormField'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { formDescriptionId } = useFormField()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<p
|
||||||
|
:id="formDescriptionId"
|
||||||
|
:class="cn('text-sm text-muted-foreground', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
19
src/components/ui/form/FormItem.vue
Normal file
19
src/components/ui/form/FormItem.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { useId } from 'radix-vue'
|
||||||
|
import { type HTMLAttributes, provide } from 'vue'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
import { FORM_ITEM_INJECTION_KEY } from './injectionKeys'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const id = useId()
|
||||||
|
provide(FORM_ITEM_INJECTION_KEY, id)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="cn('space-y-2', props.class)">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
23
src/components/ui/form/FormLabel.vue
Normal file
23
src/components/ui/form/FormLabel.vue
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { LabelProps } from 'radix-vue'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { Label } from '~/components/ui/label'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
import { useFormField } from './useFormField'
|
||||||
|
|
||||||
|
const props = defineProps<LabelProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const { error, formItemId } = useFormField()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Label
|
||||||
|
:class="cn(
|
||||||
|
error && 'text-destructive',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
:for="formItemId"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Label>
|
||||||
|
</template>
|
||||||
16
src/components/ui/form/FormMessage.vue
Normal file
16
src/components/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 text-destructive font-medium"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
7
src/components/ui/form/index.ts
Normal file
7
src/components/ui/form/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export { default as FormControl } from './FormControl.vue'
|
||||||
|
export { default as FormDescription } from './FormDescription.vue'
|
||||||
|
export { default as FormItem } from './FormItem.vue'
|
||||||
|
export { default as FormLabel } from './FormLabel.vue'
|
||||||
|
export { default as FormMessage } from './FormMessage.vue'
|
||||||
|
export { FORM_ITEM_INJECTION_KEY } from './injectionKeys'
|
||||||
|
export { Form, Field as FormField } from 'vee-validate'
|
||||||
5
src/components/ui/form/injectionKeys.ts
Normal file
5
src/components/ui/form/injectionKeys.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import type { InjectionKey } from 'vue'
|
||||||
|
|
||||||
|
export const FORM_ITEM_INJECTION_KEY
|
||||||
|
// eslint-disable-next-line symbol-description
|
||||||
|
= Symbol() as InjectionKey<string>
|
||||||
30
src/components/ui/form/useFormField.ts
Normal file
30
src/components/ui/form/useFormField.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { FieldContextKey, useFieldError, useIsFieldDirty, useIsFieldTouched, useIsFieldValid } from 'vee-validate'
|
||||||
|
import { inject } from 'vue'
|
||||||
|
import { FORM_ITEM_INJECTION_KEY } from './injectionKeys'
|
||||||
|
|
||||||
|
export function useFormField() {
|
||||||
|
const fieldContext = inject(FieldContextKey)
|
||||||
|
const fieldItemContext = inject(FORM_ITEM_INJECTION_KEY)
|
||||||
|
|
||||||
|
if (!fieldContext)
|
||||||
|
throw new Error('useFormField should be used within <FormField>')
|
||||||
|
|
||||||
|
const { name } = fieldContext
|
||||||
|
const id = fieldItemContext
|
||||||
|
|
||||||
|
const fieldState = {
|
||||||
|
valid: useIsFieldValid(name),
|
||||||
|
isDirty: useIsFieldDirty(name),
|
||||||
|
isTouched: useIsFieldTouched(name),
|
||||||
|
error: useFieldError(name),
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
formItemId: `${id}-form-item`,
|
||||||
|
formDescriptionId: `${id}-form-item-description`,
|
||||||
|
formMessageId: `${id}-form-item-message`,
|
||||||
|
...fieldState,
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/components/ui/popover/Popover.vue
Normal file
15
src/components/ui/popover/Popover.vue
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PopoverRootEmits, PopoverRootProps } from 'radix-vue'
|
||||||
|
import { PopoverRoot, useForwardPropsEmits } from 'radix-vue'
|
||||||
|
|
||||||
|
const props = defineProps<PopoverRootProps>()
|
||||||
|
const emits = defineEmits<PopoverRootEmits>()
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(props, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PopoverRoot v-bind="forwarded">
|
||||||
|
<slot />
|
||||||
|
</PopoverRoot>
|
||||||
|
</template>
|
||||||
48
src/components/ui/popover/PopoverContent.vue
Normal file
48
src/components/ui/popover/PopoverContent.vue
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
PopoverContent,
|
||||||
|
type PopoverContentEmits,
|
||||||
|
type PopoverContentProps,
|
||||||
|
PopoverPortal,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from 'radix-vue'
|
||||||
|
import { computed, type HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<PopoverContentProps & { class?: HTMLAttributes['class'] }>(),
|
||||||
|
{
|
||||||
|
align: 'center',
|
||||||
|
sideOffset: 4,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const emits = defineEmits<PopoverContentEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PopoverPortal>
|
||||||
|
<PopoverContent
|
||||||
|
v-bind="{ ...forwarded, ...$attrs }"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</PopoverContent>
|
||||||
|
</PopoverPortal>
|
||||||
|
</template>
|
||||||
11
src/components/ui/popover/PopoverTrigger.vue
Normal file
11
src/components/ui/popover/PopoverTrigger.vue
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { PopoverTrigger, type PopoverTriggerProps } from 'radix-vue'
|
||||||
|
|
||||||
|
const props = defineProps<PopoverTriggerProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PopoverTrigger v-bind="props">
|
||||||
|
<slot />
|
||||||
|
</PopoverTrigger>
|
||||||
|
</template>
|
||||||
3
src/components/ui/popover/index.ts
Normal file
3
src/components/ui/popover/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { default as Popover } from './Popover.vue'
|
||||||
|
export { default as PopoverContent } from './PopoverContent.vue'
|
||||||
|
export { default as PopoverTrigger } from './PopoverTrigger.vue'
|
||||||
25
src/components/ui/radio-group/RadioGroup.vue
Normal file
25
src/components/ui/radio-group/RadioGroup.vue
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { RadioGroupRoot, type RadioGroupRootEmits, type RadioGroupRootProps, useForwardPropsEmits } from 'radix-vue'
|
||||||
|
import { computed, type HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<RadioGroupRootProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
const emits = defineEmits<RadioGroupRootEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<RadioGroupRoot
|
||||||
|
:class="cn('grid gap-2', props.class)"
|
||||||
|
v-bind="forwarded"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</RadioGroupRoot>
|
||||||
|
</template>
|
||||||
39
src/components/ui/radio-group/RadioGroupItem.vue
Normal file
39
src/components/ui/radio-group/RadioGroupItem.vue
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Circle } from 'lucide-vue-next'
|
||||||
|
import {
|
||||||
|
RadioGroupIndicator,
|
||||||
|
RadioGroupItem,
|
||||||
|
type RadioGroupItemProps,
|
||||||
|
useForwardProps,
|
||||||
|
} from 'radix-vue'
|
||||||
|
import { computed, type HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<RadioGroupItemProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<RadioGroupItem
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<RadioGroupIndicator
|
||||||
|
class="flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<Circle class="h-2.5 w-2.5 fill-current text-current" />
|
||||||
|
</RadioGroupIndicator>
|
||||||
|
</RadioGroupItem>
|
||||||
|
</template>
|
||||||
2
src/components/ui/radio-group/index.ts
Normal file
2
src/components/ui/radio-group/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { default as RadioGroup } from './RadioGroup.vue'
|
||||||
|
export { default as RadioGroupItem } from './RadioGroupItem.vue'
|
||||||
39
src/components/ui/switch/Switch.vue
Normal file
39
src/components/ui/switch/Switch.vue
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
SwitchRoot,
|
||||||
|
type SwitchRootEmits,
|
||||||
|
type SwitchRootProps,
|
||||||
|
SwitchThumb,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from 'radix-vue'
|
||||||
|
import { computed, type HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<SwitchRootProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const emits = defineEmits<SwitchRootEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SwitchRoot
|
||||||
|
v-bind="forwarded"
|
||||||
|
:class="cn(
|
||||||
|
'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<SwitchThumb
|
||||||
|
:class="cn('pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5')"
|
||||||
|
>
|
||||||
|
<slot name="thumb" />
|
||||||
|
</SwitchThumb>
|
||||||
|
</SwitchRoot>
|
||||||
|
</template>
|
||||||
1
src/components/ui/switch/index.ts
Normal file
1
src/components/ui/switch/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as Switch } from './Switch.vue'
|
||||||
24
src/components/ui/textarea/Textarea.vue
Normal file
24
src/components/ui/textarea/Textarea.vue
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { useVModel } from '@vueuse/core'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
defaultValue?: string | number
|
||||||
|
modelValue?: string | number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emits = defineEmits<{
|
||||||
|
(e: 'update:modelValue', payload: string | number): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const modelValue = useVModel(props, 'modelValue', emits, {
|
||||||
|
passive: true,
|
||||||
|
defaultValue: props.defaultValue,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<textarea v-model="modelValue" :class="cn('flex min-h-20 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', props.class)" />
|
||||||
|
</template>
|
||||||
1
src/components/ui/textarea/index.ts
Normal file
1
src/components/ui/textarea/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as Textarea } from './Textarea.vue'
|
||||||
@ -7,6 +7,7 @@ meta:
|
|||||||
</route>
|
</route>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { z } from 'zod'
|
||||||
import { API_BASE_URL } from '~/CONSTANT'
|
import { API_BASE_URL } from '~/CONSTANT'
|
||||||
|
|
||||||
const selectedMode = ref('观测的二阶多项式拟合')
|
const selectedMode = ref('观测的二阶多项式拟合')
|
||||||
@ -19,16 +20,30 @@ const modes = [
|
|||||||
'扰动分量的正弦波拟合',
|
'扰动分量的正弦波拟合',
|
||||||
'径向风-纬向风矢量图',
|
'径向风-纬向风矢量图',
|
||||||
'温度-水平风矢量图',
|
'温度-水平风矢量图',
|
||||||
]
|
] as const
|
||||||
|
const formSchema = shallowRef<z.ZodObject< any, any, any > | null>(null)
|
||||||
|
const stagedDates = ref<string[]>([])
|
||||||
|
|
||||||
const selectedDate = ref('2022-01-01')
|
const selectedDate = ref('2022-01-01')
|
||||||
|
|
||||||
const dates = ref([] as string[])
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const resp = await fetch(`${API_BASE_URL}/balloon/metadata`)
|
await fetch(`${API_BASE_URL}/balloon/metadata`).then(resp => resp.json()).then((data) => {
|
||||||
const data = await resp.json()
|
stagedDates.value = data
|
||||||
dates.value = data
|
formSchema.value = z.object({
|
||||||
|
selectedMode: z.enum(modes),
|
||||||
|
selectedDate: z.enum(data.map((d: string) => {
|
||||||
|
const datePattern = /_\d{8}T\d{6}/
|
||||||
|
if (!datePattern.test(d)) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const capture = datePattern.exec(d)
|
||||||
|
if (!capture) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return capture[0]
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
async function get_image() {
|
async function get_image() {
|
||||||
@ -49,49 +64,27 @@ async function get_image() {
|
|||||||
watch([selectedMode, selectedDate], () => {
|
watch([selectedMode, selectedDate], () => {
|
||||||
get_image()
|
get_image()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function submit(value: Record<string, any>) {
|
||||||
|
if (!(value.selectedMode && value.selectedDate)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
selectedMode.value = value.selectedMode
|
||||||
|
// search for the date in stagedDates
|
||||||
|
for (const d of stagedDates.value) {
|
||||||
|
if (d.includes(value.selectedDate)) {
|
||||||
|
selectedDate.value = d
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div flex="~ row items-center">
|
<div flex="~ row items-begin justify-stretch gap-3 w-full">
|
||||||
<div>
|
<ParamsCard :schema="formSchema" @submit="(e) => submit(e)" />
|
||||||
<div flex="~ col items-center gap-3" py-3>
|
|
||||||
<Select v-model="selectedMode">
|
<ImageContainer :image-url="imageUrl" />
|
||||||
<SelectTrigger class="w-[180px]">
|
|
||||||
<SelectValue placeholder="Select a fruit" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectGroup>
|
|
||||||
<SelectLabel>计算模式</SelectLabel>
|
|
||||||
<SelectItem v-for="mode in modes" :key="mode" :value="mode">
|
|
||||||
{{ mode }}
|
|
||||||
</SelectItem>
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<Select v-model="selectedDate">
|
|
||||||
<SelectTrigger class="w-[180px]">
|
|
||||||
<SelectValue placeholder="Select a fruit" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectGroup>
|
|
||||||
<SelectLabel>天</SelectLabel>
|
|
||||||
<SelectItem v-for="date in dates" :key="date" :value="date">
|
|
||||||
{{ date.match(/_(\d{8}T\d{6})/)?.[1] }}
|
|
||||||
</SelectItem>
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div v-if="hasWaveThisDay">
|
|
||||||
<Image :image-url="imageUrl" />
|
|
||||||
</div>
|
|
||||||
<div v-else h-40 flex="~ col items-center justify-center" text-xl>
|
|
||||||
{{ selectedDate.match(/_(\d{8}T\d{6})/)?.[1] }} <br>
|
|
||||||
这个时刻没有数据
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user