feat: framework done

This commit is contained in:
Dustella 2025-02-08 16:56:16 +08:00
parent 39076673dd
commit 80c746f14e
Signed by: Dustella
GPG Key ID: 35AA0AA3DC402D5C
82 changed files with 1666 additions and 30 deletions

3
.gitignore vendored
View File

@ -1,3 +1,4 @@
node_modules
target
dist
dist
.env

View File

@ -17,17 +17,24 @@
"sizecheck": "npx vite-bundle-visualizer"
},
"dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"@internationalized/date": "^3.7.0",
"@unhead/vue": "^1.11.15",
"@unocss/preset-web-fonts": "^65.4.3",
"@unocss/reset": "^65.4.0",
"@vueuse/core": "^12.4.0",
"@vueuse/head": "^2.0.0",
"beasties": "^0.2.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-vue-next": "^0.474.0",
"nprogress": "^0.2.0",
"pinia": "^2.3.0",
"radix-vue": "^1.9.13",
"shadcn-vue": "^0.11.3",
"tailwind-merge": "^3.0.1",
"unplugin-vue-define-options": "1.5.5",
"vaul-vue": "^0.2.1",
"vue": "^3.5.13",
"vue-demi": "^0.14.10",
"vue-i18n": "^11.0.1",

105
pnpm-lock.yaml generated
View File

@ -13,6 +13,12 @@ importers:
.:
dependencies:
'@amap/amap-jsapi-loader':
specifier: ^1.0.1
version: 1.0.1
'@internationalized/date':
specifier: ^3.7.0
version: 3.7.0
'@unhead/vue':
specifier: ^1.11.15
version: 1.11.15(vue@3.5.13(typescript@5.7.3))
@ -31,21 +37,36 @@ importers:
beasties:
specifier: ^0.2.0
version: 0.2.0
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
clsx:
specifier: ^2.1.1
version: 2.1.1
lucide-vue-next:
specifier: ^0.474.0
version: 0.474.0(vue@3.5.13(typescript@5.7.3))
nprogress:
specifier: ^0.2.0
version: 0.2.0
pinia:
specifier: ^2.3.0
version: 2.3.0(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3))
radix-vue:
specifier: ^1.9.13
version: 1.9.13(vue@3.5.13(typescript@5.7.3))
shadcn-vue:
specifier: ^0.11.3
version: 0.11.3(@vitest/ui@2.1.8)(eslint@9.18.0(jiti@2.4.0))(vitest@2.1.8)(vue@3.5.13(typescript@5.7.3))
tailwind-merge:
specifier: ^3.0.1
version: 3.0.1
unplugin-vue-define-options:
specifier: 1.5.5
version: 1.5.5(vue@3.5.13(typescript@5.7.3))
vaul-vue:
specifier: ^0.2.1
version: 0.2.1(radix-vue@1.9.13(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
vue:
specifier: ^3.5.13
version: 3.5.13(typescript@5.7.3)
@ -195,6 +216,9 @@ packages:
resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==}
engines: {node: '>=0.10.0'}
'@amap/amap-jsapi-loader@1.0.1':
resolution: {integrity: sha512-nPyLKt7Ow/ThHLkSvn2etQlUzqxmTVgK7bIgwdBRTg2HK5668oN7xVxkaiRe3YZEzGzfV2XgH5Jmu2T73ljejw==}
'@ampproject/remapping@2.3.0':
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'}
@ -2103,6 +2127,15 @@ packages:
vue:
optional: true
'@vue-macros/common@1.16.1':
resolution: {integrity: sha512-Pn/AWMTjoMYuquepLZP813BIcq8DTZiNCoaceuNlvaYuOTd8DqBZWc5u0uOMQZMInwME1mdSmmBAcTluiV9Jtg==}
engines: {node: '>=16.14.0'}
peerDependencies:
vue: ^2.7.0 || ^3.2.25
peerDependenciesMeta:
vue:
optional: true
'@vue-macros/config@0.5.1':
resolution: {integrity: sha512-LqrsrrtqTgTLLnducOoG2mfDkrP52Qar4IcQfjXfsr+U3QXc4TqgbGxYfZ5Hk5Sf6lCymwPTLU6QEJo/hVpGTA==}
engines: {node: '>=16.14.0'}
@ -2458,6 +2491,10 @@ packages:
resolution: {integrity: sha512-gdvX700WVC6sHCJQ7bJGfDvtuKAh6Sa6weIZROxfzUZKP7BjvB8y0SMlM/o4omSQ3L60PQSJROBJsb0vEViVnA==}
engines: {node: '>=16.14.0'}
ast-kit@1.4.0:
resolution: {integrity: sha512-BlGeOw73FDsX7z0eZE/wuuafxYoek2yzNJ6l6A1nsb4+z/p87TOPbHaWuN53kFKNuUXiCQa2M+xLF71IqQmRSw==}
engines: {node: '>=16.14.0'}
ast-types@0.14.2:
resolution: {integrity: sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA==}
engines: {node: '>=4'}
@ -2685,6 +2722,9 @@ packages:
citty@0.1.6:
resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==}
class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
clean-css@5.3.2:
resolution: {integrity: sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==}
engines: {node: '>= 10.0'}
@ -4257,10 +4297,19 @@ packages:
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
lucide-vue-next@0.474.0:
resolution: {integrity: sha512-bQaSBjfJ33xiPQCxCf4JD3rcUgZFgWZzxSY8SScNa4Mcq2vWGlbvQx6icTL1UXRqsxzfoT13RXawePSmgg4iWw==}
peerDependencies:
vue: '>=3.0.1'
magic-string-ast@0.6.3:
resolution: {integrity: sha512-C9sgUzVZtUtzCBoMdYtwrIRQ4IucGRFGgdhkjL7PXsVfPYmTuWtewqzk7dlipaCMWH/gOYehW9rgMoa4Oebtpw==}
engines: {node: '>=16.14.0'}
magic-string-ast@0.7.0:
resolution: {integrity: sha512-686fgAHaJY7wLTFEq7nnKqeQrhqmXB19d1HnqT35Ci7BN6hbAYLZUezTQ062uUHM7ggZEQlqJ94Ftls+KDXU8Q==}
engines: {node: '>=16.14.0'}
magic-string@0.25.9:
resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
@ -5780,6 +5829,10 @@ packages:
resolution: {integrity: sha512-vh9hOHd/VRxZ5QHdLdl0WO8aZWuVx8GN8eExCP665o8anh0LC8Xj4PF5zx0BxRcDi6AZOnTQZPgfQlA0Iw/74Q==}
engines: {node: '>=16.14.0'}
unplugin-vue-define-options@1.5.5:
resolution: {integrity: sha512-V50sWbpoADsjyVgovxewoLo2IDW0zfgHJbKiAl2EdZT8OL3g3h1Mz3QKoAAu09i8+LnkDatIEQMgBVeHHxWXNg==}
engines: {node: '>=16.14.0'}
unplugin-vue-macros@2.13.8:
resolution: {integrity: sha512-+TR+276cFvaXpU/h/4eSCOeVBF+JPLpiJ0NYDcVC3T0wn4RqeJapMJPZnZi1YfADFptoRhsfy/mTLg3XJZl0jw==}
engines: {node: '>=16.14.0'}
@ -5838,6 +5891,12 @@ packages:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
vaul-vue@0.2.1:
resolution: {integrity: sha512-iF91R1JQZzxb9mb9uGNHYv8rVFxR5bL5Fj51iqvyXjzMPAzNMciCrXb9OUBu2NdlhcF6rVtEADXnQoTY+pKIeA==}
peerDependencies:
radix-vue: ^1.4.0
vue: ^3.3.0
verror@1.10.0:
resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==}
engines: {'0': node >=0.6.0}
@ -6234,6 +6293,8 @@ snapshots:
'@aashutoshrathi/word-wrap@1.2.6': {}
'@amap/amap-jsapi-loader@1.0.1': {}
'@ampproject/remapping@2.3.0':
dependencies:
'@jridgewell/gen-mapping': 0.3.5
@ -8424,6 +8485,17 @@ snapshots:
transitivePeerDependencies:
- rollup
'@vue-macros/common@1.16.1(vue@3.5.13(typescript@5.7.3))':
dependencies:
'@vue/compiler-sfc': 3.5.13
ast-kit: 1.4.0
local-pkg: 1.0.0
magic-string-ast: 0.7.0
pathe: 2.0.2
picomatch: 4.0.2
optionalDependencies:
vue: 3.5.13(typescript@5.7.3)
'@vue-macros/config@0.5.1(rollup@4.30.1)(vue@3.5.13(typescript@5.7.3))':
dependencies:
'@vue-macros/common': 1.15.1(rollup@4.30.1)(vue@3.5.13(typescript@5.7.3))
@ -8951,6 +9023,11 @@ snapshots:
'@babel/parser': 7.26.3
pathe: 1.1.2
ast-kit@1.4.0:
dependencies:
'@babel/parser': 7.26.7
pathe: 2.0.2
ast-types@0.14.2:
dependencies:
tslib: 2.8.1
@ -9191,6 +9268,10 @@ snapshots:
dependencies:
consola: 3.3.1
class-variance-authority@0.7.1:
dependencies:
clsx: 2.1.1
clean-css@5.3.2:
dependencies:
source-map: 0.6.1
@ -11009,10 +11090,18 @@ snapshots:
dependencies:
yallist: 3.1.1
lucide-vue-next@0.474.0(vue@3.5.13(typescript@5.7.3)):
dependencies:
vue: 3.5.13(typescript@5.7.3)
magic-string-ast@0.6.3:
dependencies:
magic-string: 0.30.17
magic-string-ast@0.7.0:
dependencies:
magic-string: 0.30.17
magic-string@0.25.9:
dependencies:
sourcemap-codec: 1.4.8
@ -12856,6 +12945,14 @@ snapshots:
- rollup
- vue
unplugin-vue-define-options@1.5.5(vue@3.5.13(typescript@5.7.3)):
dependencies:
'@vue-macros/common': 1.16.1(vue@3.5.13(typescript@5.7.3))
ast-walker-scope: 0.6.2
unplugin: 2.1.2
transitivePeerDependencies:
- vue
unplugin-vue-macros@2.13.8(@vueuse/core@12.4.0(typescript@5.7.3))(esbuild@0.23.1)(rollup@4.30.1)(typescript@5.7.3)(vite@6.0.7(@types/node@20.2.3)(jiti@2.4.0)(stylus@0.57.0)(terser@5.17.6)(tsx@4.19.2)(yaml@2.6.1))(vue-tsc@2.2.0(typescript@5.7.3))(vue@3.5.13(typescript@5.7.3)):
dependencies:
'@vue-macros/better-define': 1.11.1(rollup@4.30.1)(vue@3.5.13(typescript@5.7.3))
@ -12971,6 +13068,14 @@ snapshots:
vary@1.1.2: {}
vaul-vue@0.2.1(radix-vue@1.9.13(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3)):
dependencies:
'@vueuse/core': 10.11.1(vue@3.5.13(typescript@5.7.3))
radix-vue: 1.9.13(vue@3.5.13(typescript@5.7.3))
vue: 3.5.13(typescript@5.7.3)
transitivePeerDependencies:
- '@vue/composition-api'
verror@1.10.0:
dependencies:
assert-plus: 1.0.0

View File

@ -25,5 +25,7 @@ useHead({
</script>
<template>
<RouterView />
<TooltipProvider>
<RouterView />
</TooltipProvider>
</template>

View File

@ -9,6 +9,7 @@ declare global {
const EffectScope: typeof import('vue')['EffectScope']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const baseFetch: typeof import('./composables/baseFetch')['baseFetch']
const computed: typeof import('vue')['computed']
const computedAsync: typeof import('@vueuse/core')['computedAsync']
const computedEager: typeof import('@vueuse/core')['computedEager']
@ -212,6 +213,7 @@ declare global {
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
const useOnline: typeof import('@vueuse/core')['useOnline']
const useOptions: typeof import('./composables/dateOpt')['useOptions']
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
const useParallax: typeof import('@vueuse/core')['useParallax']
const useParentElement: typeof import('@vueuse/core')['useParentElement']
@ -316,6 +318,7 @@ declare module 'vue' {
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly baseFetch: UnwrapRef<typeof import('./composables/baseFetch')['baseFetch']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
@ -517,6 +520,7 @@ declare module 'vue' {
readonly useObjectUrl: UnwrapRef<typeof import('@vueuse/core')['useObjectUrl']>
readonly useOffsetPagination: UnwrapRef<typeof import('@vueuse/core')['useOffsetPagination']>
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
readonly useOptions: UnwrapRef<typeof import('./composables/dateOpt')['useOptions']>
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>

48
src/components.d.ts vendored
View File

@ -7,11 +7,59 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
Badge: typeof import('./components/ui/badge/Badge.vue')['default']
Button: typeof import('./components/ui/button/Button.vue')['default']
Calendar: typeof import('./components/ui/calendar/Calendar.vue')['default']
CalendarCell: typeof import('./components/ui/calendar/CalendarCell.vue')['default']
CalendarCellTrigger: typeof import('./components/ui/calendar/CalendarCellTrigger.vue')['default']
CalendarGrid: typeof import('./components/ui/calendar/CalendarGrid.vue')['default']
CalendarGridBody: typeof import('./components/ui/calendar/CalendarGridBody.vue')['default']
CalendarGridHead: typeof import('./components/ui/calendar/CalendarGridHead.vue')['default']
CalendarGridRow: typeof import('./components/ui/calendar/CalendarGridRow.vue')['default']
CalendarHeadCell: typeof import('./components/ui/calendar/CalendarHeadCell.vue')['default']
CalendarHeader: typeof import('./components/ui/calendar/CalendarHeader.vue')['default']
CalendarHeading: typeof import('./components/ui/calendar/CalendarHeading.vue')['default']
CalendarNextButton: typeof import('./components/ui/calendar/CalendarNextButton.vue')['default']
CalendarPrevButton: typeof import('./components/ui/calendar/CalendarPrevButton.vue')['default']
Card: typeof import('./components/ui/card/Card.vue')['default']
CardContent: typeof import('./components/ui/card/CardContent.vue')['default']
CardDescription: typeof import('./components/ui/card/CardDescription.vue')['default']
CardFooter: typeof import('./components/ui/card/CardFooter.vue')['default']
CardHeader: typeof import('./components/ui/card/CardHeader.vue')['default']
CardTitle: typeof import('./components/ui/card/CardTitle.vue')['default']
Drawer: typeof import('./components/ui/drawer/Drawer.vue')['default']
DrawerContent: typeof import('./components/ui/drawer/DrawerContent.vue')['default']
DrawerDescription: typeof import('./components/ui/drawer/DrawerDescription.vue')['default']
DrawerFooter: typeof import('./components/ui/drawer/DrawerFooter.vue')['default']
DrawerHeader: typeof import('./components/ui/drawer/DrawerHeader.vue')['default']
DrawerOverlay: typeof import('./components/ui/drawer/DrawerOverlay.vue')['default']
DrawerTitle: typeof import('./components/ui/drawer/DrawerTitle.vue')['default']
Input: typeof import('./components/ui/input/Input.vue')['default']
Label: typeof import('./components/ui/label/Label.vue')['default']
Popover: typeof import('./components/ui/popover/Popover.vue')['default']
PopoverContent: typeof import('./components/ui/popover/PopoverContent.vue')['default']
PopoverTrigger: typeof import('./components/ui/popover/PopoverTrigger.vue')['default']
README: typeof import('./components/README.md')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Select: typeof import('./components/ui/select/Select.vue')['default']
SelectContent: typeof import('./components/ui/select/SelectContent.vue')['default']
SelectGroup: typeof import('./components/ui/select/SelectGroup.vue')['default']
SelectItem: typeof import('./components/ui/select/SelectItem.vue')['default']
SelectItemText: typeof import('./components/ui/select/SelectItemText.vue')['default']
SelectLabel: typeof import('./components/ui/select/SelectLabel.vue')['default']
SelectScrollDownButton: typeof import('./components/ui/select/SelectScrollDownButton.vue')['default']
SelectScrollUpButton: typeof import('./components/ui/select/SelectScrollUpButton.vue')['default']
SelectSeparator: typeof import('./components/ui/select/SelectSeparator.vue')['default']
SelectTrigger: typeof import('./components/ui/select/SelectTrigger.vue')['default']
SelectValue: typeof import('./components/ui/select/SelectValue.vue')['default']
Textarea: typeof import('./components/ui/textarea/Textarea.vue')['default']
TheCounter: typeof import('./components/TheCounter.vue')['default']
TheFooter: typeof import('./components/TheFooter.vue')['default']
TheInput: typeof import('./components/TheInput.vue')['default']
Tooltip: typeof import('./components/ui/tooltip/Tooltip.vue')['default']
TooltipContent: typeof import('./components/ui/tooltip/TooltipContent.vue')['default']
TooltipProvider: typeof import('./components/ui/tooltip/TooltipProvider.vue')['default']
TooltipTrigger: typeof import('./components/ui/tooltip/TooltipTrigger.vue')['default']
}
}

View File

@ -1,9 +0,0 @@
## Components
Components in this dir will be auto-registered and on-demand, powered by [`unplugin-vue-components`](https://github.com/antfu/unplugin-vue-components).
### Icons
You can use icons from almost any icon sets by the power of [Iconify](https://iconify.design/).
It will only bundle the icons you use. Check out [`unplugin-icons`](https://github.com/antfu/unplugin-icons) for more details.

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { availableLocales, loadLanguageAsync } from '~/modules/i18n'
import { availableLocales, loadLanguageAsync } from '@/modules/i18n'
const { t, locale } = useI18n()

View 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>

View 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>

View File

@ -0,0 +1,26 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { Primitive, type PrimitiveProps } from 'radix-vue'
import { type ButtonVariants, buttonVariants } from '.'
interface Props extends PrimitiveProps {
variant?: ButtonVariants['variant']
size?: ButtonVariants['size']
class?: HTMLAttributes['class']
}
const props = withDefaults(defineProps<Props>(), {
as: 'button',
})
</script>
<template>
<Primitive
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"
>
<slot />
</Primitive>
</template>

View File

@ -0,0 +1,35 @@
import { cva, type VariantProps } from 'class-variance-authority'
export { default as Button } from './Button.vue'
export const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{
variants: {
variant: {
default:
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline:
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary:
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)
export type ButtonVariants = VariantProps<typeof buttonVariants>

View File

@ -0,0 +1,60 @@
<script lang="ts" setup>
import { cn } from '@/lib/utils'
import { CalendarRoot, type CalendarRootEmits, type CalendarRootProps, useForwardPropsEmits } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
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>

View File

@ -0,0 +1,24 @@
<script lang="ts" setup>
import { cn } from '@/lib/utils'
import { CalendarCell, type CalendarCellProps, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<CalendarCellProps & { class?: HTMLAttributes['class'] }>()
const 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>

View File

@ -0,0 +1,38 @@
<script lang="ts" setup>
import { buttonVariants } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import { CalendarCellTrigger, type CalendarCellTriggerProps, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<CalendarCellTriggerProps & { class?: HTMLAttributes['class'] }>()
const 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>

View File

@ -0,0 +1,24 @@
<script lang="ts" setup>
import { cn } from '@/lib/utils'
import { CalendarGrid, type CalendarGridProps, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
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>

View 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>

View 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>

View File

@ -0,0 +1,21 @@
<script lang="ts" setup>
import { cn } from '@/lib/utils'
import { CalendarGridRow, type CalendarGridRowProps, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
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>

View File

@ -0,0 +1,21 @@
<script lang="ts" setup>
import { cn } from '@/lib/utils'
import { CalendarHeadCell, type CalendarHeadCellProps, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
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>

View File

@ -0,0 +1,21 @@
<script lang="ts" setup>
import { cn } from '@/lib/utils'
import { CalendarHeader, type CalendarHeaderProps, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
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>

View File

@ -0,0 +1,27 @@
<script lang="ts" setup>
import { cn } from '@/lib/utils'
import { CalendarHeading, type CalendarHeadingProps, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
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>

View File

@ -0,0 +1,32 @@
<script lang="ts" setup>
import { buttonVariants } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import { ChevronRight } from 'lucide-vue-next'
import { CalendarNext, type CalendarNextProps, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
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>

View File

@ -0,0 +1,32 @@
<script lang="ts" setup>
import { buttonVariants } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import { ChevronLeft } from 'lucide-vue-next'
import { CalendarPrev, type CalendarPrevProps, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
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>

View 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'

View File

@ -0,0 +1,21 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
:class="
cn(
'rounded-lg border bg-card text-card-foreground shadow-sm',
props.class,
)
"
>
<slot />
</div>
</template>

View File

@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div :class="cn('p-6 pt-0', props.class)">
<slot />
</div>
</template>

View File

@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<p :class="cn('text-sm text-muted-foreground', props.class)">
<slot />
</p>
</template>

View File

@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div :class="cn('flex items-center p-6 pt-0', props.class)">
<slot />
</div>
</template>

View File

@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div :class="cn('flex flex-col gap-y-1.5 p-6', props.class)">
<slot />
</div>
</template>

View File

@ -0,0 +1,18 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<h3
:class="
cn('text-2xl font-semibold leading-none tracking-tight', props.class)
"
>
<slot />
</h3>
</template>

View File

@ -0,0 +1,6 @@
export { default as Card } from './Card.vue'
export { default as CardContent } from './CardContent.vue'
export { default as CardDescription } from './CardDescription.vue'
export { default as CardFooter } from './CardFooter.vue'
export { default as CardHeader } from './CardHeader.vue'
export { default as CardTitle } from './CardTitle.vue'

View File

@ -0,0 +1,19 @@
<script lang="ts" setup>
import type { DrawerRootEmits, DrawerRootProps } from 'vaul-vue'
import { useForwardPropsEmits } from 'radix-vue'
import { DrawerRoot } from 'vaul-vue'
const props = withDefaults(defineProps<DrawerRootProps>(), {
shouldScaleBackground: true,
})
const emits = defineEmits<DrawerRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DrawerRoot v-bind="forwarded">
<slot />
</DrawerRoot>
</template>

View File

@ -0,0 +1,28 @@
<script lang="ts" setup>
import type { DialogContentEmits, DialogContentProps } from 'radix-vue'
import type { HtmlHTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { useForwardPropsEmits } from 'radix-vue'
import { DrawerContent, DrawerPortal } from 'vaul-vue'
import DrawerOverlay from './DrawerOverlay.vue'
const props = defineProps<DialogContentProps & { class?: HtmlHTMLAttributes['class'] }>()
const emits = defineEmits<DialogContentEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DrawerPortal>
<DrawerOverlay />
<DrawerContent
v-bind="forwarded" :class="cn(
'fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background',
props.class,
)"
>
<div class="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
<slot />
</DrawerContent>
</DrawerPortal>
</template>

View File

@ -0,0 +1,20 @@
<script lang="ts" setup>
import type { DrawerDescriptionProps } from 'vaul-vue'
import { cn } from '@/lib/utils'
import { DrawerDescription } from 'vaul-vue'
import { computed, type HtmlHTMLAttributes } from 'vue'
const props = defineProps<DrawerDescriptionProps & { class?: HtmlHTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<DrawerDescription v-bind="delegatedProps" :class="cn('text-sm text-muted-foreground', props.class)">
<slot />
</DrawerDescription>
</template>

View File

@ -0,0 +1,14 @@
<script lang="ts" setup>
import type { HtmlHTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HtmlHTMLAttributes['class']
}>()
</script>
<template>
<div :class="cn('mt-auto flex flex-col gap-2 p-4', props.class)">
<slot />
</div>
</template>

View File

@ -0,0 +1,14 @@
<script lang="ts" setup>
import type { HtmlHTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HtmlHTMLAttributes['class']
}>()
</script>
<template>
<div :class="cn('grid gap-1.5 p-4 text-center sm:text-left', props.class)">
<slot />
</div>
</template>

View File

@ -0,0 +1,18 @@
<script lang="ts" setup>
import type { DialogOverlayProps } from 'radix-vue'
import { cn } from '@/lib/utils'
import { DrawerOverlay } from 'vaul-vue'
import { computed, type HtmlHTMLAttributes } from 'vue'
const props = defineProps<DialogOverlayProps & { class?: HtmlHTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<DrawerOverlay v-bind="delegatedProps" :class="cn('fixed inset-0 z-50 bg-black/80', props.class)" />
</template>

View File

@ -0,0 +1,20 @@
<script lang="ts" setup>
import type { DrawerTitleProps } from 'vaul-vue'
import { cn } from '@/lib/utils'
import { DrawerTitle } from 'vaul-vue'
import { computed, type HtmlHTMLAttributes } from 'vue'
const props = defineProps<DrawerTitleProps & { class?: HtmlHTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<DrawerTitle v-bind="delegatedProps" :class="cn('text-lg font-semibold leading-none tracking-tight', props.class)">
<slot />
</DrawerTitle>
</template>

View File

@ -0,0 +1,8 @@
export { default as Drawer } from './Drawer.vue'
export { default as DrawerContent } from './DrawerContent.vue'
export { default as DrawerDescription } from './DrawerDescription.vue'
export { default as DrawerFooter } from './DrawerFooter.vue'
export { default as DrawerHeader } from './DrawerHeader.vue'
export { default as DrawerOverlay } from './DrawerOverlay.vue'
export { default as DrawerTitle } from './DrawerTitle.vue'
export { DrawerClose, DrawerPortal, DrawerTrigger } from 'vaul-vue'

View File

@ -0,0 +1,24 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { useVModel } from '@vueuse/core'
const props = defineProps<{
defaultValue?: string | number
modelValue?: string | number
class?: HTMLAttributes['class']
}>()
const emits = defineEmits<{
(e: 'update:modelValue', payload: string | number): void
}>()
const modelValue = useVModel(props, 'modelValue', emits, {
passive: true,
defaultValue: props.defaultValue,
})
</script>
<template>
<input v-model="modelValue" :class="cn('flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium 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>

View File

@ -0,0 +1 @@
export { default as Input } from './Input.vue'

View File

@ -0,0 +1,27 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { Label, type LabelProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<LabelProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<Label
v-bind="delegatedProps"
:class="
cn(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
props.class,
)
"
>
<slot />
</Label>
</template>

View File

@ -0,0 +1 @@
export { default as Label } from './Label.vue'

View 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>

View File

@ -0,0 +1,48 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import {
PopoverContent,
type PopoverContentEmits,
type PopoverContentProps,
PopoverPortal,
useForwardPropsEmits,
} from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
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>

View 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>

View 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'

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
import type { SelectRootEmits, SelectRootProps } from 'radix-vue'
import { SelectRoot, useForwardPropsEmits } from 'radix-vue'
const props = defineProps<SelectRootProps>()
const emits = defineEmits<SelectRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<SelectRoot v-bind="forwarded">
<slot />
</SelectRoot>
</template>

View File

@ -0,0 +1,53 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import {
SelectContent,
type SelectContentEmits,
type SelectContentProps,
SelectPortal,
SelectViewport,
useForwardPropsEmits,
} from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
import { SelectScrollDownButton, SelectScrollUpButton } from '.'
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(
defineProps<SelectContentProps & { class?: HTMLAttributes['class'] }>(),
{
position: 'popper',
},
)
const emits = defineEmits<SelectContentEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<SelectPortal>
<SelectContent
v-bind="{ ...forwarded, ...$attrs }" :class="cn(
'relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
position === 'popper'
&& 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
props.class,
)
"
>
<SelectScrollUpButton />
<SelectViewport :class="cn('p-1', position === 'popper' && 'h-[--radix-select-trigger-height] w-full min-w-[--radix-select-trigger-width]')">
<slot />
</SelectViewport>
<SelectScrollDownButton />
</SelectContent>
</SelectPortal>
</template>

View File

@ -0,0 +1,19 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { SelectGroup, type SelectGroupProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<SelectGroupProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<SelectGroup :class="cn('p-1 w-full', props.class)" v-bind="delegatedProps">
<slot />
</SelectGroup>
</template>

View File

@ -0,0 +1,44 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { Check } from 'lucide-vue-next'
import {
SelectItem,
SelectItemIndicator,
type SelectItemProps,
SelectItemText,
useForwardProps,
} from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<SelectItemProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<SelectItem
v-bind="forwardedProps"
:class="
cn(
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
props.class,
)
"
>
<span class="absolute left-2 h-3.5 w-3.5 flex items-center justify-center">
<SelectItemIndicator>
<Check class="h-4 w-4" />
</SelectItemIndicator>
</span>
<SelectItemText>
<slot />
</SelectItemText>
</SelectItem>
</template>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import { SelectItemText, type SelectItemTextProps } from 'radix-vue'
const props = defineProps<SelectItemTextProps>()
</script>
<template>
<SelectItemText v-bind="props">
<slot />
</SelectItemText>
</template>

View File

@ -0,0 +1,13 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { SelectLabel, type SelectLabelProps } from 'radix-vue'
const props = defineProps<SelectLabelProps & { class?: HTMLAttributes['class'] }>()
</script>
<template>
<SelectLabel :class="cn('py-1.5 pl-8 pr-2 text-sm font-semibold', props.class)">
<slot />
</SelectLabel>
</template>

View File

@ -0,0 +1,24 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { ChevronDown } from 'lucide-vue-next'
import { SelectScrollDownButton, type SelectScrollDownButtonProps, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<SelectScrollDownButtonProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<SelectScrollDownButton v-bind="forwardedProps" :class="cn('flex cursor-default items-center justify-center py-1', props.class)">
<slot>
<ChevronDown class="h-4 w-4" />
</slot>
</SelectScrollDownButton>
</template>

View File

@ -0,0 +1,24 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { ChevronUp } from 'lucide-vue-next'
import { SelectScrollUpButton, type SelectScrollUpButtonProps, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<SelectScrollUpButtonProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<SelectScrollUpButton v-bind="forwardedProps" :class="cn('flex cursor-default items-center justify-center py-1', props.class)">
<slot>
<ChevronUp class="h-4 w-4" />
</slot>
</SelectScrollUpButton>
</template>

View File

@ -0,0 +1,17 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { SelectSeparator, type SelectSeparatorProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<SelectSeparatorProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<SelectSeparator v-bind="delegatedProps" :class="cn('-mx-1 my-1 h-px bg-muted', props.class)" />
</template>

View File

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

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import { SelectValue, type SelectValueProps } from 'radix-vue'
const props = defineProps<SelectValueProps>()
</script>
<template>
<SelectValue v-bind="props">
<slot />
</SelectValue>
</template>

View File

@ -0,0 +1,11 @@
export { default as Select } from './Select.vue'
export { default as SelectContent } from './SelectContent.vue'
export { default as SelectGroup } from './SelectGroup.vue'
export { default as SelectItem } from './SelectItem.vue'
export { default as SelectItemText } from './SelectItemText.vue'
export { default as SelectLabel } from './SelectLabel.vue'
export { default as SelectScrollDownButton } from './SelectScrollDownButton.vue'
export { default as SelectScrollUpButton } from './SelectScrollUpButton.vue'
export { default as SelectSeparator } from './SelectSeparator.vue'
export { default as SelectTrigger } from './SelectTrigger.vue'
export { default as SelectValue } from './SelectValue.vue'

View File

@ -0,0 +1,24 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { useVModel } from '@vueuse/core'
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>

View File

@ -0,0 +1 @@
export { default as Textarea } from './Textarea.vue'

View File

@ -0,0 +1,14 @@
<script setup lang="ts">
import { TooltipRoot, type TooltipRootEmits, type TooltipRootProps, useForwardPropsEmits } from 'radix-vue'
const props = defineProps<TooltipRootProps>()
const emits = defineEmits<TooltipRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<TooltipRoot v-bind="forwarded">
<slot />
</TooltipRoot>
</template>

View File

@ -0,0 +1,31 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { TooltipContent, type TooltipContentEmits, type TooltipContentProps, TooltipPortal, useForwardPropsEmits } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<TooltipContentProps & { class?: HTMLAttributes['class'] }>(), {
sideOffset: 4,
})
const emits = defineEmits<TooltipContentEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<TooltipPortal>
<TooltipContent v-bind="{ ...forwarded, ...$attrs }" :class="cn('z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 />
</TooltipContent>
</TooltipPortal>
</template>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import { TooltipProvider, type TooltipProviderProps } from 'radix-vue'
const props = defineProps<TooltipProviderProps>()
</script>
<template>
<TooltipProvider v-bind="props">
<slot />
</TooltipProvider>
</template>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import { TooltipTrigger, type TooltipTriggerProps } from 'radix-vue'
const props = defineProps<TooltipTriggerProps>()
</script>
<template>
<TooltipTrigger v-bind="props">
<slot />
</TooltipTrigger>
</template>

View File

@ -0,0 +1,4 @@
export { default as Tooltip } from './Tooltip.vue'
export { default as TooltipContent } from './TooltipContent.vue'
export { default as TooltipProvider } from './TooltipProvider.vue'
export { default as TooltipTrigger } from './TooltipTrigger.vue'

View File

@ -0,0 +1,5 @@
import { createFetch } from '@vueuse/core'
export const baseFetch = createFetch({
baseUrl: import.meta.env.VITE_BACKEND_URL,
})

View File

@ -0,0 +1,14 @@
function _useOptions() {
const options = reactive({
castBeginDate: '2022-01-01',
castDayNo: 0,
renderMode: 'heatmap' as 'heatmap' | 'image',
isPlaying: false,
})
return {
options,
}
}
export const useOptions = createSharedComposable(() => _useOptions())

View File

@ -1,12 +1,25 @@
<script setup lang="ts">
const router = useRouter()
onMounted(() => {
router.push('/map')
})
</script>
<template>
<main
px-4 py-10
text="center gray-700 dark:gray-200"
>
<RouterView />
<TheFooter />
<div mx-auto mt-5 text-center text-sm opacity-50>
[Default Layout]
<div class="grid h-screen w-full">
<div class="flex flex-col">
<header
class="sticky top-0 z-10 h-[53px] flex items-center gap-1 border-b bg-background px-4"
>
<h1 class="text-xl font-semibold">
次季节台风预报系统
</h1>
</header>
<main
class="grid flex-1 gap-4 overflow-auto p-4 lg:grid-cols-3 md:grid-cols-2"
>
<RouterView />
</main>
</div>
</main>
</div>
</template>

233
src/layouts/map.vue Normal file
View File

@ -0,0 +1,233 @@
<script setup lang="ts">
import { useOptions } from '@/composables/dateOpt'
import { cn } from '@/lib/utils'
import {
DateFormatter,
type DateValue,
getLocalTimeZone,
isSameDay,
parseDate,
} from '@internationalized/date'
import { DrawerTrigger } from 'vaul-vue'
const [DefineFormTemplate, ReuseFormTemplate] = createReusableTemplate()
const df = new DateFormatter('en-US', {
dateStyle: 'long',
})
const castBeginDate = ref<DateValue>()
const castTargetDate = ref<DateValue>()
const { options } = useOptions()
const mapping = ref < Record<string, string>>({})
onMounted(async () => {
const resp = await baseFetch('/tc/metadata').json()
const data = resp.data.value.data
mapping.value = data
})
const isDateDisabled = computed(() => {
const availableDates = Object.keys(mapping.value)
.map((date: string) => {
// date is like 20020202, we need to first trans to 2002-02-02
try {
const newDateStr = `${date.slice(0, 4)}-${date.slice(4, 6)}-${date.slice(6, 8)}`
const newDate = parseDate(newDateStr)
return newDate
}
catch {
return null
}
})
.filter(date => date !== null) as DateValue[]
function _isDateDisabled(date: DateValue): boolean {
const hasMatch = availableDates.some((thisDate) => {
return isSameDay(date, thisDate)
})
return !hasMatch
}
return _isDateDisabled
})
const isTargetDateDisabled = computed(() => {
const castBegin = castBeginDate.value
function _functionIsTargetDateDisabled(date: DateValue): boolean {
if (!castBegin) {
return true
}
const castEnd = castBegin.add({ days: 29 })
return date.compare(castBegin) < 0 || date.compare(castEnd) > 0
}
return _functionIsTargetDateDisabled
})
</script>
<template>
<div class="grid h-screen w-full">
<DefineFormTemplate>
<form class="grid w-full items-start gap-6">
<fieldset class="grid gap-6 border rounded-lg p-4">
<legend class="px-1 text-sm font-medium -ml-1">
日期设置
</legend>
<div class="grid gap-3">
<Label for="model">渲染模式</Label>
<Select v-model="options.renderMode">
<SelectTrigger
id="model"
class="items-start [&_[data-description]]:hidden"
>
<SelectValue placeholder="Select a model" />
</SelectTrigger>
<SelectContent>
<SelectItem value="heatmap">
<div class="flex items-start gap-3 text-muted-foreground">
<Rabbit class="size-5" />
<div class="grid gap-0.5">
<p>
热图
<span class="text-foreground font-medium">
使用点状热力图渲染
</span>
</p>
<p class="text-xs" data-description>
放大缩小可能不准确
</p>
</div>
</div>
</SelectItem>
<SelectItem value="image">
<div class="flex items-start gap-3 text-muted-foreground">
<Bird class="size-5" />
<div class="grid gap-0.5">
<p>
图片
<span class="text-foreground font-medium">
使用图片叠加层渲染
</span>
</p>
<p class="text-xs" data-description>
相对准确
</p>
</div>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<Label for="temperature">发布日期</Label>
<Popover>
<PopoverTrigger as-child>
<Button
variant="outline"
:class="cn(
'w-full justify-start text-left font-normal',
!castBeginDate && 'text-muted-foreground',
)"
>
<CalendarIcon class="mr-2 h-4 w-4" />
{{ castBeginDate ? df.format(castBeginDate.toDate(getLocalTimeZone())) : "Pick a date" }}
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0">
<Calendar v-model="castBeginDate" initial-focus :is-date-disabled="isDateDisabled" />
</PopoverContent>
</Popover>
</div>
<div>
<Label for="top-p">目标预报日期</Label>
<Popover>
<PopoverTrigger as-child>
<Button
variant="outline"
:class="cn(
'w-full justify-start text-left font-normal',
!castTargetDate && 'text-muted-foreground',
)"
>
<CalendarIcon class="mr-2 h-4 w-4" />
{{ castTargetDate ? df.format(castTargetDate.toDate(getLocalTimeZone())) : "Pick a date" }}
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0">
<Calendar
v-model="castTargetDate" initial-focus
:is-date-disabled="isTargetDateDisabled"
/>
</PopoverContent>
</Popover>
</div>
</div>
<div class="grid gap-3" />
</fieldset>
<fieldset class="grid gap-6 border rounded-lg p-4">
<legend class="px-1 text-sm font-medium -ml-1">
渲染
</legend>
<Button>
开始自动播放
</Button>
</fieldset>
</form>
</DefineFormTemplate>
<div class="flex flex-col">
<header
class="sticky top-0 z-10 h-[53px] flex items-center gap-1 border-b bg-background px-4"
>
<h1 class="text-xl font-semibold">
次季节台风预报系统
</h1>
<Drawer>
<DrawerTrigger as-child>
<Button variant="ghost" size="icon" class="md:hidden">
<Settings class="size-4" />
<span class="sr-only">地图设置</span>
</Button>
</DrawerTrigger>
<DrawerContent class="max-h-[80vh]">
<DrawerHeader>
<DrawerTitle>地图设置</DrawerTitle>
<DrawerDescription>
选择日期和渲染方式
</DrawerDescription>
</DrawerHeader>
<ReuseFormTemplate />
</DrawerContent>
</Drawer>
</header>
<main
class="grid flex-1 gap-4 overflow-auto p-4 lg:grid-cols-3 md:grid-cols-2"
>
<div class="relative hidden flex-col items-start gap-8 md:flex">
<ReuseFormTemplate />
</div>
<div
class="relative h-full min-h-[50vh] flex flex-col rounded-xl bg-muted/50 p-4 lg:col-span-2"
>
<Badge variant="outline" class="absolute left-3 top-3 z-20 bg-slate-100">
地图
</Badge>
<div class="flex-1" />
<RouterView />
<form
class="relative overflow-hidden border rounded-lg bg-background focus-within:ring-1 focus-within:ring-ring"
>
<Label for="message" class="sr-only"> Message </Label>
<Card
id="message"
placeholder="Type your message here..."
class="min-h-22 resize-none border-0 p-3 shadow-none focus-visible:ring-0"
>
something here
</Card>
</form>
</div>
</main>
</div>
</div>
</template>

View File

@ -3,7 +3,7 @@
A custom user module system. Place a `.ts` file with the following template, it will be installed automatically.
```ts
import type { UserModule } from '~/types'
import type { UserModule } from '@/types'
export const install: UserModule = ({ app, router, isClient }) => {
// do something

View File

@ -1,5 +1,5 @@
import type { UserModule } from '@/types'
import type { Locale } from 'vue-i18n'
import type { UserModule } from '~/types'
import { createI18n } from 'vue-i18n'
// Import i18n resources

View File

@ -1,4 +1,4 @@
import type { UserModule } from '~/types'
import type { UserModule } from '@/types'
import NProgress from 'nprogress'
export const install: UserModule = ({ isClient, router }) => {

View File

@ -1,4 +1,4 @@
import type { UserModule } from '~/types'
import type { UserModule } from '@/types'
import { createPinia } from 'pinia'
// Setup Pinia

View File

@ -1,4 +1,4 @@
import type { UserModule } from '~/types'
import type { UserModule } from '@/types'
// https://github.com/antfu/vite-plugin-pwa#automatic-reload-when-new-content-available
export const install: UserModule = ({ isClient, router }) => {

View File

@ -5,7 +5,7 @@ Check out [`unplugin-vue-router`](https://github.com/posva/unplugin-vue-router)
### Path Aliasing
`~/` is aliased to `./src/` folder.
`@/` is aliased to `./src/` folder.
For example, instead of having
@ -16,5 +16,5 @@ import { isDark } from '../../../../composables'
now, you can use
```ts
import { isDark } from '~/composables'
import { isDark } from '@/composables'
```

View File

@ -52,5 +52,5 @@ const { t } = useI18n()
<route lang="yaml">
meta:
layout: home
layout: default
</route>

34
src/pages/map.vue Normal file
View File

@ -0,0 +1,34 @@
<route lang="yaml">
meta:
layout: map
</route>
<script setup lang="ts">
import AMapLoader from '@amap/amap-jsapi-loader'
const amapInstance = ref(null)
const map = ref(null)
const apiKey = import.meta.env.VITE_AMAP_API_KEY
onMounted(async () => {
const AMap = await AMapLoader.load({
key: apiKey,
version: '2.0',
plugins: [''],
})
amapInstance.value = AMap
map.value = new AMap.Map('amap-container', {
zoom: 5,
center: [130, 17],
// layers: [
/* new AMap.TileLayer.Satellite() */
// new AMap.TileLayer.RoadNet(),
// ],
})
})
</script>
<template>
<div id="amap-container" class="m-0 h-full w-full p-0" />
</template>

View File

@ -22,6 +22,7 @@ declare module 'vue-router/auto-routes' {
'/[...all]': RouteRecordInfo<'/[...all]', '/:all(.*)', { all: ParamValue<true> }, { all: ParamValue<false> }>,
'/about': RouteRecordInfo<'/about', '/about', Record<never, never>, Record<never, never>>,
'/hi/[name]': RouteRecordInfo<'/hi/[name]', '/hi/:name', { name: ParamValue<true> }, { name: ParamValue<false> }>,
'/map': RouteRecordInfo<'/map', '/map', Record<never, never>, Record<never, never>>,
'/README': RouteRecordInfo<'/README', '/README', Record<never, never>, Record<never, never>>,
}
}

View File

@ -7,7 +7,7 @@
"module": "ESNext",
"moduleResolution": "Bundler",
"paths": {
"~/*": ["src/*"]
"@/*": ["./src/*"]
},
"resolveJsonModule": true,
"types": [

View File

@ -31,6 +31,7 @@ export default defineConfig({
include: [/\.vue$/, /\.md$/],
}),
},
betterDefine: false,
}),
// https://github.com/posva/unplugin-vue-router