@lark-apaas/coding-templates 0.1.11 → 0.1.14
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/meta.json +6 -0
- package/package.json +2 -1
- package/template-apex/README.md +294 -0
- package/template-apex/_env.local.example +1 -0
- package/template-apex/_gitignore +27 -0
- package/template-apex/client/index.html +20 -0
- package/template-apex/client/public/favicon.svg +1 -0
- package/template-apex/client/public/icons.svg +24 -0
- package/template-apex/client/src/api/index.ts +39 -0
- package/template-apex/client/src/app.tsx +21 -0
- package/template-apex/client/src/components/layout.tsx +11 -0
- package/template-apex/client/src/components/ui/accordion.tsx +66 -0
- package/template-apex/client/src/components/ui/alert-dialog.tsx +157 -0
- package/template-apex/client/src/components/ui/alert.tsx +71 -0
- package/template-apex/client/src/components/ui/aspect-ratio.tsx +11 -0
- package/template-apex/client/src/components/ui/avatar.tsx +53 -0
- package/template-apex/client/src/components/ui/badge.tsx +42 -0
- package/template-apex/client/src/components/ui/breadcrumb.tsx +109 -0
- package/template-apex/client/src/components/ui/button-group.tsx +83 -0
- package/template-apex/client/src/components/ui/button.tsx +69 -0
- package/template-apex/client/src/components/ui/calendar.tsx +213 -0
- package/template-apex/client/src/components/ui/card.tsx +82 -0
- package/template-apex/client/src/components/ui/carousel.tsx +241 -0
- package/template-apex/client/src/components/ui/chart.tsx +357 -0
- package/template-apex/client/src/components/ui/checkbox.tsx +32 -0
- package/template-apex/client/src/components/ui/collapsible.tsx +33 -0
- package/template-apex/client/src/components/ui/command.tsx +208 -0
- package/template-apex/client/src/components/ui/context-menu.tsx +324 -0
- package/template-apex/client/src/components/ui/dialog.tsx +143 -0
- package/template-apex/client/src/components/ui/drawer.tsx +135 -0
- package/template-apex/client/src/components/ui/dropdown-menu.tsx +329 -0
- package/template-apex/client/src/components/ui/empty.tsx +104 -0
- package/template-apex/client/src/components/ui/field.tsx +248 -0
- package/template-apex/client/src/components/ui/form.tsx +167 -0
- package/template-apex/client/src/components/ui/hover-card.tsx +44 -0
- package/template-apex/client/src/components/ui/image.tsx +183 -0
- package/template-apex/client/src/components/ui/input-group.tsx +166 -0
- package/template-apex/client/src/components/ui/input-otp.tsx +77 -0
- package/template-apex/client/src/components/ui/input.tsx +21 -0
- package/template-apex/client/src/components/ui/item.tsx +193 -0
- package/template-apex/client/src/components/ui/kbd.tsx +28 -0
- package/template-apex/client/src/components/ui/label.tsx +24 -0
- package/template-apex/client/src/components/ui/menubar.tsx +348 -0
- package/template-apex/client/src/components/ui/native-select.tsx +48 -0
- package/template-apex/client/src/components/ui/navigation-menu.tsx +168 -0
- package/template-apex/client/src/components/ui/pagination.tsx +127 -0
- package/template-apex/client/src/components/ui/popover.tsx +48 -0
- package/template-apex/client/src/components/ui/progress.tsx +31 -0
- package/template-apex/client/src/components/ui/radio-group.tsx +45 -0
- package/template-apex/client/src/components/ui/resizable.tsx +56 -0
- package/template-apex/client/src/components/ui/scroll-area.tsx +58 -0
- package/template-apex/client/src/components/ui/select.tsx +243 -0
- package/template-apex/client/src/components/ui/separator.tsx +28 -0
- package/template-apex/client/src/components/ui/sheet.tsx +139 -0
- package/template-apex/client/src/components/ui/sidebar.tsx +727 -0
- package/template-apex/client/src/components/ui/skeleton.tsx +13 -0
- package/template-apex/client/src/components/ui/slider.tsx +87 -0
- package/template-apex/client/src/components/ui/sonner.tsx +67 -0
- package/template-apex/client/src/components/ui/spinner.tsx +16 -0
- package/template-apex/client/src/components/ui/switch.tsx +31 -0
- package/template-apex/client/src/components/ui/table.tsx +116 -0
- package/template-apex/client/src/components/ui/tabs.tsx +66 -0
- package/template-apex/client/src/components/ui/textarea.tsx +18 -0
- package/template-apex/client/src/components/ui/toggle-group.tsx +83 -0
- package/template-apex/client/src/components/ui/toggle.tsx +47 -0
- package/template-apex/client/src/components/ui/tooltip.tsx +61 -0
- package/template-apex/client/src/hooks/use-mobile.ts +19 -0
- package/template-apex/client/src/index.css +131 -0
- package/template-apex/client/src/lib/utils.ts +6 -0
- package/template-apex/client/src/main.tsx +10 -0
- package/template-apex/client/src/pages/home/index.tsx +20 -0
- package/template-apex/client/src/pages/todos/components/todo-form.tsx +40 -0
- package/template-apex/client/src/pages/todos/components/todo-list.tsx +43 -0
- package/template-apex/client/src/pages/todos/index.tsx +47 -0
- package/template-apex/components.json +21 -0
- package/template-apex/eslint.config.js +54 -0
- package/template-apex/package.json +91 -0
- package/template-apex/scripts/build.sh +51 -0
- package/template-apex/server/db/index.ts +8 -0
- package/template-apex/server/db/schema.ts +8 -0
- package/template-apex/server/index.ts +34 -0
- package/template-apex/server/routes/index.ts +6 -0
- package/template-apex/server/routes/todos.ts +53 -0
- package/template-apex/server/types.d.ts +4 -0
- package/template-apex/shared/api.interface.ts +23 -0
- package/template-apex/shared/types.ts +9 -0
- package/template-apex/tsconfig.app.json +33 -0
- package/template-apex/tsconfig.json +14 -0
- package/template-apex/tsconfig.node.json +31 -0
- package/template-apex/tsconfig.server.json +19 -0
- package/template-apex/vite.config.ts +18 -0
- package/template-vite-react/README.md +23 -8
- package/template-vite-react/client/src/app.tsx +2 -2
- package/template-vite-react/client/src/components/layout.tsx +2 -4
- package/template-vite-react/client/src/lib/utils.ts +3 -3
- package/template-vite-react/client/src/pages/{home/index.tsx → HomePage/HomePage.tsx} +1 -1
- package/template-vite-react/client/src/pages/NotFoundPage/NotFoundPage.tsx +11 -0
- /package/{template-vite-react → template-apex}/client/src/pages/not-found/index.tsx +0 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as LabelPrimitive from "@radix-ui/react-label"
|
|
5
|
+
import { Slot } from "@radix-ui/react-slot"
|
|
6
|
+
import {
|
|
7
|
+
Controller,
|
|
8
|
+
FormProvider,
|
|
9
|
+
useFormContext,
|
|
10
|
+
useFormState,
|
|
11
|
+
type ControllerProps,
|
|
12
|
+
type FieldPath,
|
|
13
|
+
type FieldValues,
|
|
14
|
+
} from "react-hook-form"
|
|
15
|
+
|
|
16
|
+
import { cn } from "@/lib/utils"
|
|
17
|
+
import { Label } from "@/components/ui/label"
|
|
18
|
+
|
|
19
|
+
const Form = FormProvider
|
|
20
|
+
|
|
21
|
+
type FormFieldContextValue<
|
|
22
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
23
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
24
|
+
> = {
|
|
25
|
+
name: TName
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
|
29
|
+
{} as FormFieldContextValue
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
const FormField = <
|
|
33
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
34
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
35
|
+
>({
|
|
36
|
+
...props
|
|
37
|
+
}: ControllerProps<TFieldValues, TName>) => {
|
|
38
|
+
return (
|
|
39
|
+
<FormFieldContext.Provider value={{ name: props.name }}>
|
|
40
|
+
<Controller {...props} />
|
|
41
|
+
</FormFieldContext.Provider>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const useFormField = () => {
|
|
46
|
+
const fieldContext = React.useContext(FormFieldContext)
|
|
47
|
+
const itemContext = React.useContext(FormItemContext)
|
|
48
|
+
const { getFieldState } = useFormContext()
|
|
49
|
+
const formState = useFormState({ name: fieldContext.name })
|
|
50
|
+
const fieldState = getFieldState(fieldContext.name, formState)
|
|
51
|
+
|
|
52
|
+
if (!fieldContext) {
|
|
53
|
+
throw new Error("useFormField should be used within <FormField>")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const { id } = itemContext
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
id,
|
|
60
|
+
name: fieldContext.name,
|
|
61
|
+
formItemId: `${id}-form-item`,
|
|
62
|
+
formDescriptionId: `${id}-form-item-description`,
|
|
63
|
+
formMessageId: `${id}-form-item-message`,
|
|
64
|
+
...fieldState,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type FormItemContextValue = {
|
|
69
|
+
id: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const FormItemContext = React.createContext<FormItemContextValue>(
|
|
73
|
+
{} as FormItemContextValue
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
|
77
|
+
const id = React.useId()
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<FormItemContext.Provider value={{ id }}>
|
|
81
|
+
<div
|
|
82
|
+
data-slot="form-item"
|
|
83
|
+
className={cn("grid gap-2", className)}
|
|
84
|
+
{...props}
|
|
85
|
+
/>
|
|
86
|
+
</FormItemContext.Provider>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function FormLabel({
|
|
91
|
+
className,
|
|
92
|
+
...props
|
|
93
|
+
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
|
94
|
+
const { error, formItemId } = useFormField()
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<Label
|
|
98
|
+
data-slot="form-label"
|
|
99
|
+
data-error={!!error}
|
|
100
|
+
className={cn("data-[error=true]:text-destructive", className)}
|
|
101
|
+
htmlFor={formItemId}
|
|
102
|
+
{...props}
|
|
103
|
+
/>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
|
108
|
+
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<Slot
|
|
112
|
+
data-slot="form-control"
|
|
113
|
+
id={formItemId}
|
|
114
|
+
aria-describedby={
|
|
115
|
+
!error
|
|
116
|
+
? `${formDescriptionId}`
|
|
117
|
+
: `${formDescriptionId} ${formMessageId}`
|
|
118
|
+
}
|
|
119
|
+
aria-invalid={!!error}
|
|
120
|
+
{...props}
|
|
121
|
+
/>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
126
|
+
const { formDescriptionId } = useFormField()
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<p
|
|
130
|
+
data-slot="form-description"
|
|
131
|
+
id={formDescriptionId}
|
|
132
|
+
className={cn("text-muted-foreground text-sm", className)}
|
|
133
|
+
{...props}
|
|
134
|
+
/>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
|
139
|
+
const { error, formMessageId } = useFormField()
|
|
140
|
+
const body = error ? String(error?.message ?? "") : props.children
|
|
141
|
+
|
|
142
|
+
if (!body) {
|
|
143
|
+
return null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<p
|
|
148
|
+
data-slot="form-message"
|
|
149
|
+
id={formMessageId}
|
|
150
|
+
className={cn("text-destructive text-sm", className)}
|
|
151
|
+
{...props}
|
|
152
|
+
>
|
|
153
|
+
{body}
|
|
154
|
+
</p>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export {
|
|
159
|
+
useFormField,
|
|
160
|
+
Form,
|
|
161
|
+
FormItem,
|
|
162
|
+
FormLabel,
|
|
163
|
+
FormControl,
|
|
164
|
+
FormDescription,
|
|
165
|
+
FormMessage,
|
|
166
|
+
FormField,
|
|
167
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
|
|
8
|
+
function HoverCard({
|
|
9
|
+
...props
|
|
10
|
+
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
|
|
11
|
+
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function HoverCardTrigger({
|
|
15
|
+
...props
|
|
16
|
+
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
|
|
17
|
+
return (
|
|
18
|
+
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function HoverCardContent({
|
|
23
|
+
className,
|
|
24
|
+
align = "center",
|
|
25
|
+
sideOffset = 4,
|
|
26
|
+
...props
|
|
27
|
+
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
|
|
28
|
+
return (
|
|
29
|
+
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
|
|
30
|
+
<HoverCardPrimitive.Content
|
|
31
|
+
data-slot="hover-card-content"
|
|
32
|
+
align={align}
|
|
33
|
+
sideOffset={sideOffset}
|
|
34
|
+
className={cn(
|
|
35
|
+
"bg-popover text-popover-foreground 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 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
|
36
|
+
className
|
|
37
|
+
)}
|
|
38
|
+
{...props}
|
|
39
|
+
/>
|
|
40
|
+
</HoverCardPrimitive.Portal>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { cn } from '@/lib/utils';
|
|
5
|
+
type ImageFormat = 'jpg' | 'png' | 'webp' | 'bmp' | 'gif' | 'tiff';
|
|
6
|
+
|
|
7
|
+
type NativeImgProps = React.ComponentPropsWithoutRef<'img'>;
|
|
8
|
+
|
|
9
|
+
export interface ImageProps extends NativeImgProps {
|
|
10
|
+
quality?: number;
|
|
11
|
+
format?: ImageFormat;
|
|
12
|
+
breakpoints?: Array<number>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const DEFAULT_QUALITY = 80;
|
|
16
|
+
const DEFAULT_RESOLUTIONS: number[] = [
|
|
17
|
+
16, 32, 48, 64, 96, 128, 256, 384, 640, 750, 828, 1080, 1200, 1920, 2048,
|
|
18
|
+
3840,
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const SRC_ALLOWLIST = [
|
|
22
|
+
'/runtime/api/v1/storage/object/',
|
|
23
|
+
'/aily/api/v1/feisuda/attachments/',
|
|
24
|
+
'/aily/api/v1/files/static/',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function getClosestResolution(target: number): number {
|
|
28
|
+
return DEFAULT_RESOLUTIONS.reduce((prev, curr) => {
|
|
29
|
+
return Math.abs(curr - target) < Math.abs(prev - target) ? curr : prev;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function applyParamsToUrl(
|
|
34
|
+
src: string,
|
|
35
|
+
params: Record<string, string | number | undefined>,
|
|
36
|
+
): string {
|
|
37
|
+
const search = Object.entries(params)
|
|
38
|
+
.filter(([, v]) => v !== undefined && v !== null && v !== '')
|
|
39
|
+
.map(([k, v]) => {
|
|
40
|
+
return `${k},${v}`;
|
|
41
|
+
})
|
|
42
|
+
.join('/');
|
|
43
|
+
if (!search) return src;
|
|
44
|
+
|
|
45
|
+
const [pathAndQuery = '', hash] = src.split('#');
|
|
46
|
+
const [base, query] = pathAndQuery.split('?');
|
|
47
|
+
const urlParams = new URLSearchParams(query);
|
|
48
|
+
urlParams.set('x-tos-process', `image/${search}`);
|
|
49
|
+
|
|
50
|
+
return `${base}?${urlParams.toString()}${hash ? '#' + hash : ''}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isTargetSrc(originSrc: string) {
|
|
54
|
+
return SRC_ALLOWLIST.some((item) => originSrc.includes(item));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function supportWebp() {
|
|
58
|
+
try {
|
|
59
|
+
return (
|
|
60
|
+
document
|
|
61
|
+
.createElement('canvas')
|
|
62
|
+
.toDataURL('image/webp')
|
|
63
|
+
.indexOf('data:image/webp') === 0
|
|
64
|
+
);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function buildSrcSet(
|
|
71
|
+
src: string,
|
|
72
|
+
widths: number[],
|
|
73
|
+
format: ImageFormat | undefined,
|
|
74
|
+
quality: number,
|
|
75
|
+
width?: number,
|
|
76
|
+
sizes?: string,
|
|
77
|
+
): string | undefined {
|
|
78
|
+
if (!widths || widths.length === 0 || (!width && !sizes)) return undefined;
|
|
79
|
+
const fmt = format;
|
|
80
|
+
if (width) {
|
|
81
|
+
return [1, 2]
|
|
82
|
+
.map((dpr) => {
|
|
83
|
+
const targetWidth = getClosestResolution(width * dpr);
|
|
84
|
+
return `${applyParamsToUrl(src, { resize: `w_${targetWidth}`, quality: `Q_${quality}`, format: fmt })} ${dpr}x`;
|
|
85
|
+
})
|
|
86
|
+
.join(', ');
|
|
87
|
+
}
|
|
88
|
+
return widths
|
|
89
|
+
.map(
|
|
90
|
+
(w) =>
|
|
91
|
+
`${applyParamsToUrl(src, { resize: `w_${w}`, quality: `Q_${quality}`, format: fmt })} ${w}w`,
|
|
92
|
+
)
|
|
93
|
+
.join(', ');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const Image = React.forwardRef<HTMLImageElement, ImageProps>(
|
|
97
|
+
(
|
|
98
|
+
{
|
|
99
|
+
src,
|
|
100
|
+
width,
|
|
101
|
+
height,
|
|
102
|
+
quality = DEFAULT_QUALITY,
|
|
103
|
+
format,
|
|
104
|
+
sizes,
|
|
105
|
+
srcSet: userSrcSet,
|
|
106
|
+
breakpoints = DEFAULT_RESOLUTIONS,
|
|
107
|
+
className,
|
|
108
|
+
loading = 'lazy',
|
|
109
|
+
decoding = 'async',
|
|
110
|
+
...rest
|
|
111
|
+
},
|
|
112
|
+
ref,
|
|
113
|
+
) => {
|
|
114
|
+
const defaultFormat = React.useMemo(
|
|
115
|
+
() => (supportWebp() ? 'webp' : undefined),
|
|
116
|
+
[],
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// 当 src 不在白名单时,直接渲染原生 img,保留所有原生属性
|
|
120
|
+
if (typeof src !== 'string' || !isTargetSrc(src)) {
|
|
121
|
+
return (
|
|
122
|
+
<img
|
|
123
|
+
{...rest}
|
|
124
|
+
ref={ref}
|
|
125
|
+
src={src}
|
|
126
|
+
width={width}
|
|
127
|
+
height={height}
|
|
128
|
+
sizes={sizes}
|
|
129
|
+
srcSet={userSrcSet}
|
|
130
|
+
className={cn(
|
|
131
|
+
'bg-linear-to-b from-gray-50/20 to-gray-200/20',
|
|
132
|
+
className,
|
|
133
|
+
)}
|
|
134
|
+
loading={loading}
|
|
135
|
+
decoding={decoding}
|
|
136
|
+
/>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 只有当 width 是数字类型时才进行 srcSet 优化
|
|
141
|
+
const numericWidth = typeof width === 'number' ? width : undefined;
|
|
142
|
+
|
|
143
|
+
// 用户传入的 srcSet 优先,否则生成优化的 srcSet
|
|
144
|
+
const srcSet =
|
|
145
|
+
userSrcSet ??
|
|
146
|
+
buildSrcSet(
|
|
147
|
+
src,
|
|
148
|
+
breakpoints,
|
|
149
|
+
format ?? (defaultFormat as ImageFormat),
|
|
150
|
+
quality,
|
|
151
|
+
numericWidth,
|
|
152
|
+
sizes,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const baseSrc = applyParamsToUrl(src, {
|
|
156
|
+
resize: numericWidth ? `w_${numericWidth}` : undefined,
|
|
157
|
+
quality: `Q_${quality}`,
|
|
158
|
+
format: format ?? defaultFormat,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<img
|
|
163
|
+
{...rest}
|
|
164
|
+
ref={ref}
|
|
165
|
+
src={baseSrc}
|
|
166
|
+
width={width}
|
|
167
|
+
height={height}
|
|
168
|
+
sizes={sizes}
|
|
169
|
+
srcSet={srcSet}
|
|
170
|
+
className={cn(
|
|
171
|
+
'bg-linear-to-b from-gray-50/20 to-gray-200/20',
|
|
172
|
+
className,
|
|
173
|
+
)}
|
|
174
|
+
loading={loading}
|
|
175
|
+
decoding={decoding}
|
|
176
|
+
/>
|
|
177
|
+
);
|
|
178
|
+
},
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
Image.displayName = 'Image';
|
|
182
|
+
|
|
183
|
+
export default Image;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
import { Button } from "@/components/ui/button"
|
|
8
|
+
import { Input } from "@/components/ui/input"
|
|
9
|
+
import { Textarea } from "@/components/ui/textarea"
|
|
10
|
+
|
|
11
|
+
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
data-slot="input-group"
|
|
15
|
+
role="group"
|
|
16
|
+
className={cn(
|
|
17
|
+
"group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border transition-[color,box-shadow] outline-none",
|
|
18
|
+
"h-9 min-w-0 has-[>textarea]:h-auto",
|
|
19
|
+
|
|
20
|
+
// Variants based on alignment.
|
|
21
|
+
"has-[>[data-align=inline-start]]:[&>input]:pl-2",
|
|
22
|
+
"has-[>[data-align=inline-end]]:[&>input]:pr-2",
|
|
23
|
+
"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
|
|
24
|
+
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
|
|
25
|
+
|
|
26
|
+
// Focus state.
|
|
27
|
+
"has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/20 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]",
|
|
28
|
+
|
|
29
|
+
// Error state.
|
|
30
|
+
"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
|
|
31
|
+
|
|
32
|
+
className
|
|
33
|
+
)}
|
|
34
|
+
{...props}
|
|
35
|
+
/>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const inputGroupAddonVariants = cva(
|
|
40
|
+
"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
|
|
41
|
+
{
|
|
42
|
+
variants: {
|
|
43
|
+
align: {
|
|
44
|
+
"inline-start":
|
|
45
|
+
"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
|
|
46
|
+
"inline-end":
|
|
47
|
+
"order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
|
|
48
|
+
"block-start":
|
|
49
|
+
"order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
|
|
50
|
+
"block-end":
|
|
51
|
+
"order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
defaultVariants: {
|
|
55
|
+
align: "inline-start",
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
function InputGroupAddon({
|
|
61
|
+
className,
|
|
62
|
+
align = "inline-start",
|
|
63
|
+
...props
|
|
64
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
role="group"
|
|
68
|
+
data-slot="input-group-addon"
|
|
69
|
+
data-align={align}
|
|
70
|
+
className={cn(inputGroupAddonVariants({ align }), className)}
|
|
71
|
+
onClick={(e) => {
|
|
72
|
+
if ((e.target as HTMLElement).closest("button")) {
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
e.currentTarget.parentElement?.querySelector("input")?.focus()
|
|
76
|
+
}}
|
|
77
|
+
{...props}
|
|
78
|
+
/>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const inputGroupButtonVariants = cva("text-sm flex gap-2 items-center", {
|
|
83
|
+
variants: {
|
|
84
|
+
size: {
|
|
85
|
+
xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
|
|
86
|
+
sm: "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5",
|
|
87
|
+
"icon-xs": "size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
|
|
88
|
+
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
defaultVariants: {
|
|
92
|
+
size: "xs",
|
|
93
|
+
},
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
function InputGroupButton({
|
|
97
|
+
className,
|
|
98
|
+
type = "button",
|
|
99
|
+
variant = "ghost",
|
|
100
|
+
size = "xs",
|
|
101
|
+
...props
|
|
102
|
+
}: Omit<React.ComponentProps<typeof Button>, "size"> &
|
|
103
|
+
VariantProps<typeof inputGroupButtonVariants>) {
|
|
104
|
+
return (
|
|
105
|
+
<Button
|
|
106
|
+
type={type}
|
|
107
|
+
data-size={size}
|
|
108
|
+
variant={variant}
|
|
109
|
+
className={cn(inputGroupButtonVariants({ size }), className)}
|
|
110
|
+
{...props}
|
|
111
|
+
/>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
|
|
116
|
+
return (
|
|
117
|
+
<span
|
|
118
|
+
className={cn(
|
|
119
|
+
"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
|
120
|
+
className
|
|
121
|
+
)}
|
|
122
|
+
{...props}
|
|
123
|
+
/>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function InputGroupInput({
|
|
128
|
+
className,
|
|
129
|
+
...props
|
|
130
|
+
}: React.ComponentProps<"input">) {
|
|
131
|
+
return (
|
|
132
|
+
<Input
|
|
133
|
+
data-slot="input-group-control"
|
|
134
|
+
className={cn(
|
|
135
|
+
"flex-1 rounded-none border-0 bg-transparent focus-visible:ring-0 dark:bg-transparent",
|
|
136
|
+
className
|
|
137
|
+
)}
|
|
138
|
+
{...props}
|
|
139
|
+
/>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function InputGroupTextarea({
|
|
144
|
+
className,
|
|
145
|
+
...props
|
|
146
|
+
}: React.ComponentProps<"textarea">) {
|
|
147
|
+
return (
|
|
148
|
+
<Textarea
|
|
149
|
+
data-slot="input-group-control"
|
|
150
|
+
className={cn(
|
|
151
|
+
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 focus-visible:ring-0 dark:bg-transparent",
|
|
152
|
+
className
|
|
153
|
+
)}
|
|
154
|
+
{...props}
|
|
155
|
+
/>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export {
|
|
160
|
+
InputGroup,
|
|
161
|
+
InputGroupAddon,
|
|
162
|
+
InputGroupButton,
|
|
163
|
+
InputGroupText,
|
|
164
|
+
InputGroupInput,
|
|
165
|
+
InputGroupTextarea,
|
|
166
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { OTPInput, OTPInputContext } from "input-otp"
|
|
5
|
+
import { MinusIcon } from "lucide-react"
|
|
6
|
+
|
|
7
|
+
import { cn } from "@/lib/utils"
|
|
8
|
+
|
|
9
|
+
function InputOTP({
|
|
10
|
+
className,
|
|
11
|
+
containerClassName,
|
|
12
|
+
...props
|
|
13
|
+
}: React.ComponentProps<typeof OTPInput> & {
|
|
14
|
+
containerClassName?: string
|
|
15
|
+
}) {
|
|
16
|
+
return (
|
|
17
|
+
<OTPInput
|
|
18
|
+
data-slot="input-otp"
|
|
19
|
+
containerClassName={cn(
|
|
20
|
+
"flex items-center gap-2 has-disabled:opacity-50",
|
|
21
|
+
containerClassName
|
|
22
|
+
)}
|
|
23
|
+
className={cn("disabled:cursor-not-allowed", className)}
|
|
24
|
+
{...props}
|
|
25
|
+
/>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
data-slot="input-otp-group"
|
|
33
|
+
className={cn("flex items-center", className)}
|
|
34
|
+
{...props}
|
|
35
|
+
/>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function InputOTPSlot({
|
|
40
|
+
index,
|
|
41
|
+
className,
|
|
42
|
+
...props
|
|
43
|
+
}: React.ComponentProps<"div"> & {
|
|
44
|
+
index: number
|
|
45
|
+
}) {
|
|
46
|
+
const inputOTPContext = React.useContext(OTPInputContext)
|
|
47
|
+
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
data-slot="input-otp-slot"
|
|
52
|
+
data-active={isActive}
|
|
53
|
+
className={cn(
|
|
54
|
+
"data-[active=true]:border-ring data-[active=true]:ring-ring/20 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
|
|
55
|
+
className
|
|
56
|
+
)}
|
|
57
|
+
{...props}
|
|
58
|
+
>
|
|
59
|
+
{char}
|
|
60
|
+
{hasFakeCaret && (
|
|
61
|
+
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
|
62
|
+
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
|
|
70
|
+
return (
|
|
71
|
+
<div data-slot="input-otp-separator" role="separator" {...props}>
|
|
72
|
+
<MinusIcon />
|
|
73
|
+
</div>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/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
|
+
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
12
|
+
"enabled:hover:border-ring focus-visible:border-ring focus-visible:ring-ring/20 focus-visible:ring-[3px]",
|
|
13
|
+
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
14
|
+
className
|
|
15
|
+
)}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { Input }
|