@lark-apaas/coding-templates 0.1.12 → 0.1.15

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.
Files changed (98) hide show
  1. package/meta.json +6 -0
  2. package/package.json +2 -1
  3. package/template-apex/README.md +294 -0
  4. package/template-apex/_env.local.example +1 -0
  5. package/template-apex/_gitignore +27 -0
  6. package/template-apex/client/index.html +20 -0
  7. package/template-apex/client/public/favicon.svg +1 -0
  8. package/template-apex/client/public/icons.svg +24 -0
  9. package/template-apex/client/src/api/index.ts +39 -0
  10. package/template-apex/client/src/app.tsx +19 -0
  11. package/template-apex/client/src/components/layout.tsx +11 -0
  12. package/template-apex/client/src/components/ui/accordion.tsx +66 -0
  13. package/template-apex/client/src/components/ui/alert-dialog.tsx +157 -0
  14. package/template-apex/client/src/components/ui/alert.tsx +71 -0
  15. package/template-apex/client/src/components/ui/aspect-ratio.tsx +11 -0
  16. package/template-apex/client/src/components/ui/avatar.tsx +53 -0
  17. package/template-apex/client/src/components/ui/badge.tsx +42 -0
  18. package/template-apex/client/src/components/ui/breadcrumb.tsx +109 -0
  19. package/template-apex/client/src/components/ui/button-group.tsx +83 -0
  20. package/template-apex/client/src/components/ui/button.tsx +69 -0
  21. package/template-apex/client/src/components/ui/calendar.tsx +213 -0
  22. package/template-apex/client/src/components/ui/card.tsx +82 -0
  23. package/template-apex/client/src/components/ui/carousel.tsx +241 -0
  24. package/template-apex/client/src/components/ui/chart.tsx +357 -0
  25. package/template-apex/client/src/components/ui/checkbox.tsx +32 -0
  26. package/template-apex/client/src/components/ui/collapsible.tsx +33 -0
  27. package/template-apex/client/src/components/ui/command.tsx +208 -0
  28. package/template-apex/client/src/components/ui/context-menu.tsx +324 -0
  29. package/template-apex/client/src/components/ui/dialog.tsx +143 -0
  30. package/template-apex/client/src/components/ui/drawer.tsx +135 -0
  31. package/template-apex/client/src/components/ui/dropdown-menu.tsx +329 -0
  32. package/template-apex/client/src/components/ui/empty.tsx +104 -0
  33. package/template-apex/client/src/components/ui/field.tsx +248 -0
  34. package/template-apex/client/src/components/ui/form.tsx +167 -0
  35. package/template-apex/client/src/components/ui/hover-card.tsx +44 -0
  36. package/template-apex/client/src/components/ui/image.tsx +183 -0
  37. package/template-apex/client/src/components/ui/input-group.tsx +166 -0
  38. package/template-apex/client/src/components/ui/input-otp.tsx +77 -0
  39. package/template-apex/client/src/components/ui/input.tsx +21 -0
  40. package/template-apex/client/src/components/ui/item.tsx +193 -0
  41. package/template-apex/client/src/components/ui/kbd.tsx +28 -0
  42. package/template-apex/client/src/components/ui/label.tsx +24 -0
  43. package/template-apex/client/src/components/ui/menubar.tsx +348 -0
  44. package/template-apex/client/src/components/ui/native-select.tsx +48 -0
  45. package/template-apex/client/src/components/ui/navigation-menu.tsx +168 -0
  46. package/template-apex/client/src/components/ui/pagination.tsx +127 -0
  47. package/template-apex/client/src/components/ui/popover.tsx +48 -0
  48. package/template-apex/client/src/components/ui/progress.tsx +31 -0
  49. package/template-apex/client/src/components/ui/radio-group.tsx +45 -0
  50. package/template-apex/client/src/components/ui/resizable.tsx +56 -0
  51. package/template-apex/client/src/components/ui/scroll-area.tsx +58 -0
  52. package/template-apex/client/src/components/ui/select.tsx +243 -0
  53. package/template-apex/client/src/components/ui/separator.tsx +28 -0
  54. package/template-apex/client/src/components/ui/sheet.tsx +139 -0
  55. package/template-apex/client/src/components/ui/sidebar.tsx +727 -0
  56. package/template-apex/client/src/components/ui/skeleton.tsx +13 -0
  57. package/template-apex/client/src/components/ui/slider.tsx +87 -0
  58. package/template-apex/client/src/components/ui/sonner.tsx +67 -0
  59. package/template-apex/client/src/components/ui/spinner.tsx +16 -0
  60. package/template-apex/client/src/components/ui/switch.tsx +31 -0
  61. package/template-apex/client/src/components/ui/table.tsx +116 -0
  62. package/template-apex/client/src/components/ui/tabs.tsx +66 -0
  63. package/template-apex/client/src/components/ui/textarea.tsx +18 -0
  64. package/template-apex/client/src/components/ui/toggle-group.tsx +83 -0
  65. package/template-apex/client/src/components/ui/toggle.tsx +47 -0
  66. package/template-apex/client/src/components/ui/tooltip.tsx +61 -0
  67. package/template-apex/client/src/hooks/use-mobile.ts +19 -0
  68. package/template-apex/client/src/index.css +131 -0
  69. package/template-apex/client/src/lib/utils.ts +6 -0
  70. package/template-apex/client/src/main.tsx +11 -0
  71. package/template-apex/client/src/pages/home/index.tsx +20 -0
  72. package/template-apex/client/src/pages/todos/components/todo-form.tsx +40 -0
  73. package/template-apex/client/src/pages/todos/components/todo-list.tsx +43 -0
  74. package/template-apex/client/src/pages/todos/index.tsx +47 -0
  75. package/template-apex/components.json +21 -0
  76. package/template-apex/eslint.config.js +54 -0
  77. package/template-apex/package.json +91 -0
  78. package/template-apex/scripts/build.sh +56 -0
  79. package/template-apex/server/db/index.ts +8 -0
  80. package/template-apex/server/db/schema.ts +8 -0
  81. package/template-apex/server/index.ts +34 -0
  82. package/template-apex/server/routes/index.ts +6 -0
  83. package/template-apex/server/routes/todos.ts +53 -0
  84. package/template-apex/server/types.d.ts +4 -0
  85. package/template-apex/shared/api.interface.ts +23 -0
  86. package/template-apex/shared/types.ts +9 -0
  87. package/template-apex/tsconfig.app.json +33 -0
  88. package/template-apex/tsconfig.json +14 -0
  89. package/template-apex/tsconfig.node.json +31 -0
  90. package/template-apex/tsconfig.server.json +19 -0
  91. package/template-apex/vite.config.ts +18 -0
  92. package/template-vite-react/README.md +23 -8
  93. package/template-vite-react/client/src/app.tsx +2 -2
  94. package/template-vite-react/client/src/components/layout.tsx +2 -4
  95. package/template-vite-react/client/src/lib/utils.ts +3 -3
  96. package/template-vite-react/client/src/pages/{home/index.tsx → HomePage/HomePage.tsx} +1 -1
  97. package/template-vite-react/client/src/pages/NotFoundPage/NotFoundPage.tsx +11 -0
  98. /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 }