create-lve 0.6.1 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-lve",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "bin": {
5
5
  "create-lve": "index.js"
6
6
  },
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "radix-nova",
4
+ "rsc": false,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "src/style.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "iconLibrary": "lucide",
14
+ "rtl": false,
15
+ "aliases": {
16
+ "components": "@/shared/components",
17
+ "utils": "@/shared/lib/utils",
18
+ "ui": "@/shared/components/ui",
19
+ "lib": "@/shared/lib",
20
+ "hooks": "@/shared/hooks"
21
+ },
22
+ "menuColor": "default",
23
+ "menuAccent": "subtle",
24
+ "registries": {}
25
+ }
@@ -10,11 +10,21 @@
10
10
  "prepare": "vp config"
11
11
  },
12
12
  "dependencies": {
13
+ "@fontsource-variable/inter": "^5.2.8",
14
+ "@radix-ui/react-alert-dialog": "^1.1.17",
15
+ "@radix-ui/react-dialog": "^1.1.17",
13
16
  "@tanstack/react-query": "latest",
17
+ "class-variance-authority": "^0.7.1",
18
+ "clsx": "^2.1.1",
14
19
  "jotai": "latest",
20
+ "lucide-react": "^1.21.0",
21
+ "radix-ui": "^1.6.0",
15
22
  "react": "latest",
16
23
  "react-dom": "latest",
17
24
  "react-router": "latest",
25
+ "shadcn": "^4.11.0",
26
+ "tailwind-merge": "^3.6.0",
27
+ "tw-animate-css": "^1.4.0",
18
28
  "zustand": "latest"
19
29
  },
20
30
  "devDependencies": {
@@ -2,7 +2,7 @@ import { Outlet } from 'react-router'
2
2
 
3
3
  export default function RootLayout() {
4
4
  return (
5
- <div className="min-h-dvh">
5
+ <div className="min-h-dvh flex flex-col">
6
6
  <Outlet />
7
7
  </div>
8
8
  )
@@ -1,4 +1,3 @@
1
- import { StrictMode } from 'react'
2
1
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
3
2
 
4
3
  const queryClient = new QueryClient({
@@ -11,9 +10,5 @@ const queryClient = new QueryClient({
11
10
  })
12
11
 
13
12
  export function Providers({ children }: { children: React.ReactNode }) {
14
- return (
15
- <StrictMode>
16
- <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
17
- </StrictMode>
18
- )
13
+ return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
19
14
  }
@@ -1,7 +1,35 @@
1
1
  export default function HomePage() {
2
2
  return (
3
- <div className="flex min-h-dvh items-center justify-center">
4
- <h1 className="text-4xl font-bold">Welcome</h1>
5
- </div>
3
+ <main className="relative flex min-h-dvh items-center justify-center bg-stone-50 px-8">
4
+ <div className="absolute right-8 top-1/2 -translate-y-1/2 font-mono text-[10px] tracking-widest text-stone-300 [writing-mode:vertical-rl]">
5
+ VITE × REACT
6
+ </div>
7
+
8
+ <div className="flex flex-col items-center gap-10">
9
+ <div className="flex items-center gap-8">
10
+ <img
11
+ src="/favicon.svg"
12
+ alt="Vite"
13
+ className="h-20 w-20 opacity-70 transition-opacity hover:opacity-100"
14
+ />
15
+ <span className="text-2xl font-extralight text-stone-300">+</span>
16
+ <img
17
+ src="/react.svg"
18
+ alt="React"
19
+ className="h-20 w-20 opacity-70 transition-opacity hover:opacity-100 animate-[spin_30s_linear_infinite]"
20
+ />
21
+ </div>
22
+
23
+ <h1 className="text-lg font-light tracking-[0.5em] text-stone-500">Vite + React</h1>
24
+
25
+ <div className="h-px w-12 bg-stone-200" />
26
+
27
+ <p className="text-xs tracking-widest text-stone-400">TypeScript · Shadcn · Tailwind CSS</p>
28
+
29
+ <p className="text-[11px] text-stone-300">Edit src/features/home/pages/index.tsx</p>
30
+ </div>
31
+
32
+ <div className="absolute bottom-8 left-8 font-mono text-[10px] text-stone-300">v0.0.1</div>
33
+ </main>
6
34
  )
7
35
  }
@@ -1,10 +1,14 @@
1
+ import './style.css'
2
+ import { StrictMode } from 'react'
1
3
  import { createRoot } from 'react-dom/client'
2
4
  import { Providers } from './app/providers'
3
5
  import { router } from './app/router'
4
6
  import { RouterProvider } from 'react-router'
5
7
 
6
8
  createRoot(document.getElementById('root')!).render(
7
- <Providers>
8
- <RouterProvider router={router} />
9
- </Providers>,
9
+ <StrictMode>
10
+ <Providers>
11
+ <RouterProvider router={router} />
12
+ </Providers>
13
+ </StrictMode>,
10
14
  )
@@ -0,0 +1,180 @@
1
+ import * as React from 'react'
2
+ import { AlertDialog as AlertDialogPrimitive } from 'radix-ui'
3
+
4
+ import { cn } from '@/shared/lib/utils'
5
+ import { Button } from '@/shared/components/ui/button'
6
+
7
+ function AlertDialog({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
8
+ return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
9
+ }
10
+
11
+ function AlertDialogTrigger({
12
+ ...props
13
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
14
+ return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
15
+ }
16
+
17
+ function AlertDialogPortal({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
18
+ return <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
19
+ }
20
+
21
+ function AlertDialogOverlay({
22
+ className,
23
+ ...props
24
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
25
+ return (
26
+ <AlertDialogPrimitive.Overlay
27
+ data-slot="alert-dialog-overlay"
28
+ className={cn(
29
+ 'fixed inset-0 z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0',
30
+ className,
31
+ )}
32
+ {...props}
33
+ />
34
+ )
35
+ }
36
+
37
+ function AlertDialogContent({
38
+ className,
39
+ size = 'default',
40
+ ...props
41
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Content> & {
42
+ size?: 'default' | 'sm'
43
+ }) {
44
+ return (
45
+ <AlertDialogPortal>
46
+ <AlertDialogOverlay />
47
+ <AlertDialogPrimitive.Content
48
+ data-slot="alert-dialog-content"
49
+ data-size={size}
50
+ className={cn(
51
+ 'group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
52
+ className,
53
+ )}
54
+ {...props}
55
+ />
56
+ </AlertDialogPortal>
57
+ )
58
+ }
59
+
60
+ function AlertDialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
61
+ return (
62
+ <div
63
+ data-slot="alert-dialog-header"
64
+ className={cn(
65
+ 'grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]',
66
+ className,
67
+ )}
68
+ {...props}
69
+ />
70
+ )
71
+ }
72
+
73
+ function AlertDialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
74
+ return (
75
+ <div
76
+ data-slot="alert-dialog-footer"
77
+ className={cn(
78
+ '-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end',
79
+ className,
80
+ )}
81
+ {...props}
82
+ />
83
+ )
84
+ }
85
+
86
+ function AlertDialogMedia({ className, ...props }: React.ComponentProps<'div'>) {
87
+ return (
88
+ <div
89
+ data-slot="alert-dialog-media"
90
+ className={cn(
91
+ "mb-2 inline-flex size-10 items-center justify-center rounded-md bg-muted sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-6",
92
+ className,
93
+ )}
94
+ {...props}
95
+ />
96
+ )
97
+ }
98
+
99
+ function AlertDialogTitle({
100
+ className,
101
+ ...props
102
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
103
+ return (
104
+ <AlertDialogPrimitive.Title
105
+ data-slot="alert-dialog-title"
106
+ className={cn(
107
+ 'font-heading text-base font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2',
108
+ className,
109
+ )}
110
+ {...props}
111
+ />
112
+ )
113
+ }
114
+
115
+ function AlertDialogDescription({
116
+ className,
117
+ ...props
118
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
119
+ return (
120
+ <AlertDialogPrimitive.Description
121
+ data-slot="alert-dialog-description"
122
+ className={cn(
123
+ 'text-sm text-balance text-muted-foreground md:text-pretty *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground',
124
+ className,
125
+ )}
126
+ {...props}
127
+ />
128
+ )
129
+ }
130
+
131
+ function AlertDialogAction({
132
+ className,
133
+ variant = 'default',
134
+ size = 'default',
135
+ ...props
136
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Action> &
137
+ Pick<React.ComponentProps<typeof Button>, 'variant' | 'size'>) {
138
+ return (
139
+ <Button variant={variant} size={size} asChild>
140
+ <AlertDialogPrimitive.Action
141
+ data-slot="alert-dialog-action"
142
+ className={cn(className)}
143
+ {...props}
144
+ />
145
+ </Button>
146
+ )
147
+ }
148
+
149
+ function AlertDialogCancel({
150
+ className,
151
+ variant = 'outline',
152
+ size = 'default',
153
+ ...props
154
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Cancel> &
155
+ Pick<React.ComponentProps<typeof Button>, 'variant' | 'size'>) {
156
+ return (
157
+ <Button variant={variant} size={size} asChild>
158
+ <AlertDialogPrimitive.Cancel
159
+ data-slot="alert-dialog-cancel"
160
+ className={cn(className)}
161
+ {...props}
162
+ />
163
+ </Button>
164
+ )
165
+ }
166
+
167
+ export {
168
+ AlertDialog,
169
+ AlertDialogAction,
170
+ AlertDialogCancel,
171
+ AlertDialogContent,
172
+ AlertDialogDescription,
173
+ AlertDialogFooter,
174
+ AlertDialogHeader,
175
+ AlertDialogMedia,
176
+ AlertDialogOverlay,
177
+ AlertDialogPortal,
178
+ AlertDialogTitle,
179
+ AlertDialogTrigger,
180
+ }
@@ -0,0 +1,67 @@
1
+ import * as React from 'react'
2
+ import { cva, type VariantProps } from 'class-variance-authority'
3
+ import { Slot } from 'radix-ui'
4
+
5
+ import { cn } from '@/shared/lib/utils'
6
+
7
+ const buttonVariants = cva(
8
+ "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: 'bg-primary text-primary-foreground hover:bg-primary/80',
13
+ outline:
14
+ 'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
15
+ secondary:
16
+ 'bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
17
+ ghost:
18
+ 'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',
19
+ destructive:
20
+ 'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
21
+ link: 'text-primary underline-offset-4 hover:underline',
22
+ },
23
+ size: {
24
+ default:
25
+ 'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
26
+ xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
27
+ sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
28
+ lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
29
+ icon: 'size-8',
30
+ 'icon-xs':
31
+ "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
32
+ 'icon-sm':
33
+ 'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',
34
+ 'icon-lg': 'size-9',
35
+ },
36
+ },
37
+ defaultVariants: {
38
+ variant: 'default',
39
+ size: 'default',
40
+ },
41
+ },
42
+ )
43
+
44
+ function Button({
45
+ className,
46
+ variant = 'default',
47
+ size = 'default',
48
+ asChild = false,
49
+ ...props
50
+ }: React.ComponentProps<'button'> &
51
+ VariantProps<typeof buttonVariants> & {
52
+ asChild?: boolean
53
+ }) {
54
+ const Comp = asChild ? Slot.Root : 'button'
55
+
56
+ return (
57
+ <Comp
58
+ data-slot="button"
59
+ data-variant={variant}
60
+ data-size={size}
61
+ className={cn(buttonVariants({ variant, size, className }))}
62
+ {...props}
63
+ />
64
+ )
65
+ }
66
+
67
+ export { Button, buttonVariants }
@@ -0,0 +1,143 @@
1
+ import * as React from 'react'
2
+ import { Dialog as DialogPrimitive } from 'radix-ui'
3
+
4
+ import { cn } from '@/shared/lib/utils'
5
+ import { Button } from '@/shared/components/ui/button'
6
+ import { XIcon } from 'lucide-react'
7
+
8
+ function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
9
+ return <DialogPrimitive.Root data-slot="dialog" {...props} />
10
+ }
11
+
12
+ function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
13
+ return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
14
+ }
15
+
16
+ function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
17
+ return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
18
+ }
19
+
20
+ function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
21
+ return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
22
+ }
23
+
24
+ function DialogOverlay({
25
+ className,
26
+ ...props
27
+ }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
28
+ return (
29
+ <DialogPrimitive.Overlay
30
+ data-slot="dialog-overlay"
31
+ className={cn(
32
+ 'fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0',
33
+ className,
34
+ )}
35
+ {...props}
36
+ />
37
+ )
38
+ }
39
+
40
+ function DialogContent({
41
+ className,
42
+ children,
43
+ showCloseButton = true,
44
+ ...props
45
+ }: React.ComponentProps<typeof DialogPrimitive.Content> & {
46
+ showCloseButton?: boolean
47
+ }) {
48
+ return (
49
+ <DialogPortal>
50
+ <DialogOverlay />
51
+ <DialogPrimitive.Content
52
+ data-slot="dialog-content"
53
+ className={cn(
54
+ 'fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
55
+ className,
56
+ )}
57
+ {...props}
58
+ >
59
+ {children}
60
+ {showCloseButton && (
61
+ <DialogPrimitive.Close data-slot="dialog-close" asChild>
62
+ <Button variant="ghost" className="absolute top-2 right-2" size="icon-sm">
63
+ <XIcon />
64
+ <span className="sr-only">Close</span>
65
+ </Button>
66
+ </DialogPrimitive.Close>
67
+ )}
68
+ </DialogPrimitive.Content>
69
+ </DialogPortal>
70
+ )
71
+ }
72
+
73
+ function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
74
+ return (
75
+ <div data-slot="dialog-header" className={cn('flex flex-col gap-2', className)} {...props} />
76
+ )
77
+ }
78
+
79
+ function DialogFooter({
80
+ className,
81
+ showCloseButton = false,
82
+ children,
83
+ ...props
84
+ }: React.ComponentProps<'div'> & {
85
+ showCloseButton?: boolean
86
+ }) {
87
+ return (
88
+ <div
89
+ data-slot="dialog-footer"
90
+ className={cn(
91
+ '-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end',
92
+ className,
93
+ )}
94
+ {...props}
95
+ >
96
+ {children}
97
+ {showCloseButton && (
98
+ <DialogPrimitive.Close asChild>
99
+ <Button variant="outline">Close</Button>
100
+ </DialogPrimitive.Close>
101
+ )}
102
+ </div>
103
+ )
104
+ }
105
+
106
+ function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
107
+ return (
108
+ <DialogPrimitive.Title
109
+ data-slot="dialog-title"
110
+ className={cn('font-heading text-base leading-none font-medium', className)}
111
+ {...props}
112
+ />
113
+ )
114
+ }
115
+
116
+ function DialogDescription({
117
+ className,
118
+ ...props
119
+ }: React.ComponentProps<typeof DialogPrimitive.Description>) {
120
+ return (
121
+ <DialogPrimitive.Description
122
+ data-slot="dialog-description"
123
+ className={cn(
124
+ 'text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground',
125
+ className,
126
+ )}
127
+ {...props}
128
+ />
129
+ )
130
+ }
131
+
132
+ export {
133
+ Dialog,
134
+ DialogClose,
135
+ DialogContent,
136
+ DialogDescription,
137
+ DialogFooter,
138
+ DialogHeader,
139
+ DialogOverlay,
140
+ DialogPortal,
141
+ DialogTitle,
142
+ DialogTrigger,
143
+ }
@@ -0,0 +1,19 @@
1
+ import * as React from 'react'
2
+
3
+ import { cn } from '@/shared/lib/utils'
4
+
5
+ function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
6
+ return (
7
+ <input
8
+ type={type}
9
+ data-slot="input"
10
+ className={cn(
11
+ 'h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40',
12
+ className,
13
+ )}
14
+ {...props}
15
+ />
16
+ )
17
+ }
18
+
19
+ export { Input }
@@ -0,0 +1,4 @@
1
+ export { useDebounce } from './use-debounce'
2
+ export { useMediaQuery, useIsMobile, useIsTablet, useIsDesktop, useIsDark } from './use-media-query'
3
+ export { useClickOutside } from './use-click-outside'
4
+ export { useCopyToClipboard } from './use-copy-to-clipboard'
@@ -0,0 +1,20 @@
1
+ import { useEffect, type RefObject } from 'react'
2
+
3
+ export function useClickOutside(ref: RefObject<HTMLElement | null>, handler: () => void) {
4
+ useEffect(() => {
5
+ const listener = (event: MouseEvent | TouchEvent) => {
6
+ if (!ref.current || ref.current.contains(event.target as Node)) {
7
+ return
8
+ }
9
+ handler()
10
+ }
11
+
12
+ document.addEventListener('mousedown', listener)
13
+ document.addEventListener('touchstart', listener)
14
+
15
+ return () => {
16
+ document.removeEventListener('mousedown', listener)
17
+ document.removeEventListener('touchstart', listener)
18
+ }
19
+ }, [ref, handler])
20
+ }
@@ -0,0 +1,22 @@
1
+ import { useState, useCallback } from 'react'
2
+
3
+ export function useCopyToClipboard(timeout = 2000) {
4
+ const [copied, setCopied] = useState(false)
5
+
6
+ const copy = useCallback(
7
+ async (text: string) => {
8
+ try {
9
+ await navigator.clipboard.writeText(text)
10
+ setCopied(true)
11
+ setTimeout(() => setCopied(false), timeout)
12
+ return true
13
+ } catch {
14
+ setCopied(false)
15
+ return false
16
+ }
17
+ },
18
+ [timeout],
19
+ )
20
+
21
+ return { copied, copy }
22
+ }
@@ -0,0 +1,17 @@
1
+ import { useState, useEffect } from 'react'
2
+
3
+ export function useDebounce<T>(value: T, delay: number): T {
4
+ const [debouncedValue, setDebouncedValue] = useState<T>(value)
5
+
6
+ useEffect(() => {
7
+ const timer = setTimeout(() => {
8
+ setDebouncedValue(value)
9
+ }, delay)
10
+
11
+ return () => {
12
+ clearTimeout(timer)
13
+ }
14
+ }, [value, delay])
15
+
16
+ return debouncedValue
17
+ }
@@ -0,0 +1,23 @@
1
+ import { useState, useEffect } from 'react'
2
+
3
+ export function useMediaQuery(query: string): boolean {
4
+ const [matches, setMatches] = useState(false)
5
+
6
+ useEffect(() => {
7
+ const media = window.matchMedia(query)
8
+ const listener = (e: MediaQueryListEvent) => setMatches(e.matches)
9
+
10
+ setMatches(media.matches)
11
+ media.addEventListener('change', listener)
12
+
13
+ return () => media.removeEventListener('change', listener)
14
+ }, [query])
15
+
16
+ return matches
17
+ }
18
+
19
+ // 预设断点
20
+ export const useIsMobile = () => useMediaQuery('(max-width: 768px)')
21
+ export const useIsTablet = () => useMediaQuery('(max-width: 1024px)')
22
+ export const useIsDesktop = () => useMediaQuery('(min-width: 1025px)')
23
+ export const useIsDark = () => useMediaQuery('(prefers-color-scheme: dark)')
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from 'clsx'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,199 @@
1
+ @import 'tailwindcss';
2
+ @import "tw-animate-css";
3
+ @import "shadcn/tailwind.css";
4
+ @import "@fontsource-variable/inter";
5
+
6
+ @custom-variant dark (&:is(.dark *));
7
+
8
+ @layer base {
9
+ /* Scrollbar */
10
+ * {
11
+ scrollbar-width: thin;
12
+ scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
13
+ @apply border-border outline-ring/50;
14
+ }
15
+ ::-webkit-scrollbar {
16
+ width: 6px;
17
+ height: 6px;
18
+ }
19
+ ::-webkit-scrollbar-thumb {
20
+ background: rgba(0, 0, 0, 0.2);
21
+ border-radius: 3px;
22
+ }
23
+ ::-webkit-scrollbar-track {
24
+ background: transparent;
25
+ }
26
+ .dark * {
27
+ scrollbar-color: rgba(255, 255, 255, 0.15) transparent;
28
+ }
29
+ .dark ::-webkit-scrollbar-thumb {
30
+ background: rgba(255, 255, 255, 0.15);
31
+ }
32
+
33
+ /* 文本渲染 */
34
+ html {
35
+ -webkit-font-smoothing: antialiased;
36
+ -moz-osx-font-smoothing: grayscale;
37
+ text-rendering: optimizeLegibility;
38
+ @apply font-sans;
39
+ }
40
+
41
+ /* 选中色 */
42
+ ::selection {
43
+ background: rgba(59, 130, 246, 0.2);
44
+ }
45
+
46
+ /* 触摸优化 */
47
+ body {
48
+ -webkit-tap-highlight-color: transparent;
49
+ @apply bg-background text-foreground;
50
+ }
51
+ }
52
+
53
+ /* 链接和图片禁止拖拽 + 长按菜单 + 点击高亮 */
54
+ a,
55
+ img {
56
+ -webkit-user-drag: none;
57
+ -webkit-tap-highlight-color: transparent;
58
+ -webkit-touch-callout: none;
59
+ }
60
+
61
+ /* input number 去掉上下箭头 */
62
+ input[type='number']::-webkit-inner-spin-button,
63
+ input[type='number']::-webkit-outer-spin-button {
64
+ -webkit-appearance: none;
65
+ margin: 0;
66
+ }
67
+
68
+ /* textarea 禁止拖拽调整大小 */
69
+ textarea {
70
+ resize: none;
71
+ }
72
+
73
+ /* details/summary 去掉默认三角 */
74
+ summary {
75
+ list-style: none;
76
+ }
77
+ summary::marker {
78
+ display: none;
79
+ content: '';
80
+ }
81
+
82
+ /* focus 焦点环 */
83
+ a:focus-visible {
84
+ outline: 2px solid var(--color-primary, #3b82f6);
85
+ outline-offset: 2px;
86
+ border-radius: 2px;
87
+ }
88
+
89
+ @theme inline {
90
+ --font-heading: var(--font-sans);
91
+ --font-sans: 'Inter Variable', sans-serif;
92
+ --color-sidebar-ring: var(--sidebar-ring);
93
+ --color-sidebar-border: var(--sidebar-border);
94
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
95
+ --color-sidebar-accent: var(--sidebar-accent);
96
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
97
+ --color-sidebar-primary: var(--sidebar-primary);
98
+ --color-sidebar-foreground: var(--sidebar-foreground);
99
+ --color-sidebar: var(--sidebar);
100
+ --color-chart-5: var(--chart-5);
101
+ --color-chart-4: var(--chart-4);
102
+ --color-chart-3: var(--chart-3);
103
+ --color-chart-2: var(--chart-2);
104
+ --color-chart-1: var(--chart-1);
105
+ --color-ring: var(--ring);
106
+ --color-input: var(--input);
107
+ --color-border: var(--border);
108
+ --color-destructive: var(--destructive);
109
+ --color-accent-foreground: var(--accent-foreground);
110
+ --color-accent: var(--accent);
111
+ --color-muted-foreground: var(--muted-foreground);
112
+ --color-muted: var(--muted);
113
+ --color-secondary-foreground: var(--secondary-foreground);
114
+ --color-secondary: var(--secondary);
115
+ --color-primary-foreground: var(--primary-foreground);
116
+ --color-primary: var(--primary);
117
+ --color-popover-foreground: var(--popover-foreground);
118
+ --color-popover: var(--popover);
119
+ --color-card-foreground: var(--card-foreground);
120
+ --color-card: var(--card);
121
+ --color-foreground: var(--foreground);
122
+ --color-background: var(--background);
123
+ --radius-sm: calc(var(--radius) * 0.6);
124
+ --radius-md: calc(var(--radius) * 0.8);
125
+ --radius-lg: var(--radius);
126
+ --radius-xl: calc(var(--radius) * 1.4);
127
+ --radius-2xl: calc(var(--radius) * 1.8);
128
+ --radius-3xl: calc(var(--radius) * 2.2);
129
+ --radius-4xl: calc(var(--radius) * 2.6);
130
+ }
131
+
132
+ :root {
133
+ --background: oklch(1 0 0);
134
+ --foreground: oklch(0.145 0 0);
135
+ --card: oklch(1 0 0);
136
+ --card-foreground: oklch(0.145 0 0);
137
+ --popover: oklch(1 0 0);
138
+ --popover-foreground: oklch(0.145 0 0);
139
+ --primary: oklch(0.205 0 0);
140
+ --primary-foreground: oklch(0.985 0 0);
141
+ --secondary: oklch(0.97 0 0);
142
+ --secondary-foreground: oklch(0.205 0 0);
143
+ --muted: oklch(0.97 0 0);
144
+ --muted-foreground: oklch(0.556 0 0);
145
+ --accent: oklch(0.97 0 0);
146
+ --accent-foreground: oklch(0.205 0 0);
147
+ --destructive: oklch(0.577 0.245 27.325);
148
+ --border: oklch(0.922 0 0);
149
+ --input: oklch(0.922 0 0);
150
+ --ring: oklch(0.708 0 0);
151
+ --chart-1: oklch(0.87 0 0);
152
+ --chart-2: oklch(0.556 0 0);
153
+ --chart-3: oklch(0.439 0 0);
154
+ --chart-4: oklch(0.371 0 0);
155
+ --chart-5: oklch(0.269 0 0);
156
+ --radius: 0.625rem;
157
+ --sidebar: oklch(0.985 0 0);
158
+ --sidebar-foreground: oklch(0.145 0 0);
159
+ --sidebar-primary: oklch(0.205 0 0);
160
+ --sidebar-primary-foreground: oklch(0.985 0 0);
161
+ --sidebar-accent: oklch(0.97 0 0);
162
+ --sidebar-accent-foreground: oklch(0.205 0 0);
163
+ --sidebar-border: oklch(0.922 0 0);
164
+ --sidebar-ring: oklch(0.708 0 0);
165
+ }
166
+
167
+ .dark {
168
+ --background: oklch(0.145 0 0);
169
+ --foreground: oklch(0.985 0 0);
170
+ --card: oklch(0.205 0 0);
171
+ --card-foreground: oklch(0.985 0 0);
172
+ --popover: oklch(0.205 0 0);
173
+ --popover-foreground: oklch(0.985 0 0);
174
+ --primary: oklch(0.922 0 0);
175
+ --primary-foreground: oklch(0.205 0 0);
176
+ --secondary: oklch(0.269 0 0);
177
+ --secondary-foreground: oklch(0.985 0 0);
178
+ --muted: oklch(0.269 0 0);
179
+ --muted-foreground: oklch(0.708 0 0);
180
+ --accent: oklch(0.269 0 0);
181
+ --accent-foreground: oklch(0.985 0 0);
182
+ --destructive: oklch(0.704 0.191 22.216);
183
+ --border: oklch(1 0 0 / 10%);
184
+ --input: oklch(1 0 0 / 15%);
185
+ --ring: oklch(0.556 0 0);
186
+ --chart-1: oklch(0.87 0 0);
187
+ --chart-2: oklch(0.556 0 0);
188
+ --chart-3: oklch(0.439 0 0);
189
+ --chart-4: oklch(0.371 0 0);
190
+ --chart-5: oklch(0.269 0 0);
191
+ --sidebar: oklch(0.205 0 0);
192
+ --sidebar-foreground: oklch(0.985 0 0);
193
+ --sidebar-primary: oklch(0.488 0.243 264.376);
194
+ --sidebar-primary-foreground: oklch(0.985 0 0);
195
+ --sidebar-accent: oklch(0.269 0 0);
196
+ --sidebar-accent-foreground: oklch(0.985 0 0);
197
+ --sidebar-border: oklch(1 0 0 / 10%);
198
+ --sidebar-ring: oklch(0.556 0 0);
199
+ }
@@ -1,3 +1,4 @@
1
+ import tailwindcss from '@tailwindcss/vite'
1
2
  import { defineConfig, loadEnv, lazyPlugins } from 'vite-plus'
2
3
  import react, { reactCompilerPreset } from '@vitejs/plugin-react'
3
4
 
@@ -7,12 +8,12 @@ const env = loadEnv(mode, process.cwd(), '')
7
8
  // https://vite.dev/config/
8
9
  export default defineConfig({
9
10
  fmt: { semi: false, singleQuote: true },
11
+ lint: { options: { typeAware: true, typeCheck: true } },
10
12
  staged: {
11
13
  '*': 'vp check --fix',
12
14
  },
13
- lint: { options: { typeAware: true, typeCheck: true } },
14
15
  plugins: [
15
- /* VITE_PLUS_PLUGINS */
16
+ tailwindcss(),
16
17
  react(),
17
18
  // lazyPlugins 返回类型与 Vite plugins 字段类型不兼容
18
19
  // 上游 issue: vitejs/vite#22085,官方确认为已知问题
@@ -38,6 +39,7 @@ export default defineConfig({
38
39
  if (id.includes('react-dom')) return 'vendor-react-dom'
39
40
  if (id.includes('react-router')) return 'vendor-router'
40
41
  if (id.includes('@tanstack')) return 'vendor-query'
42
+ if (id.includes('radix-ui')) return 'vendor-radix'
41
43
  if (id.includes('zustand')) return 'vendor-state'
42
44
  if (id.includes('jotai')) return 'vendor-atom'
43
45
  return 'vendor'
File without changes
File without changes