stackkit 0.3.4 → 0.3.6

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 (203) hide show
  1. package/README.md +50 -42
  2. package/dist/cli/add.js +122 -56
  3. package/dist/cli/create.d.ts +2 -0
  4. package/dist/cli/create.js +271 -95
  5. package/dist/cli/doctor.js +1 -0
  6. package/dist/cli/list.d.ts +1 -1
  7. package/dist/cli/list.js +6 -4
  8. package/dist/index.js +234 -191
  9. package/dist/lib/constants.d.ts +4 -0
  10. package/dist/lib/constants.js +4 -0
  11. package/dist/lib/discovery/module-discovery.d.ts +4 -0
  12. package/dist/lib/discovery/module-discovery.js +56 -0
  13. package/dist/lib/generation/code-generator.d.ts +11 -2
  14. package/dist/lib/generation/code-generator.js +42 -3
  15. package/dist/lib/generation/generator-utils.js +3 -1
  16. package/dist/lib/pm/package-manager.js +16 -13
  17. package/dist/lib/ui/logger.js +3 -2
  18. package/dist/lib/utils/path-resolver.d.ts +2 -0
  19. package/dist/lib/utils/path-resolver.js +8 -0
  20. package/dist/meta.json +8312 -0
  21. package/modules/auth/better-auth/files/{shared → express}/config/env.ts +48 -52
  22. package/modules/auth/better-auth/files/express/middlewares/authorize.ts +20 -1
  23. package/modules/auth/better-auth/files/express/modules/auth.controller.ts +349 -0
  24. package/modules/auth/better-auth/files/express/modules/{auth/auth.route.ts → auth.route.ts} +12 -7
  25. package/modules/auth/better-auth/files/express/modules/auth.service.ts +664 -0
  26. package/modules/auth/better-auth/files/express/modules/{auth/auth.type.ts → auth.type.ts} +22 -9
  27. package/modules/auth/better-auth/files/{shared/mongoose/auth/constants.ts → express/mongo-modules/auth.constants.ts} +0 -1
  28. package/modules/auth/better-auth/files/{shared/mongoose/auth/helper.ts → express/mongo-modules/auth.helper.ts} +11 -1
  29. package/modules/auth/better-auth/files/express/types/express.d.ts +11 -0
  30. package/modules/auth/better-auth/files/nextjs/api-route.ts +74 -0
  31. package/modules/auth/better-auth/files/nextjs/dashboard/pages/(user)/page.tsx +6 -0
  32. package/modules/auth/better-auth/files/nextjs/dashboard/pages/admin/page.tsx +6 -0
  33. package/modules/auth/better-auth/files/nextjs/dashboard/pages/layout.tsx +48 -0
  34. package/modules/auth/better-auth/files/nextjs/dashboard/pages/my-profile/page.tsx +5 -0
  35. package/modules/auth/better-auth/files/nextjs/features/services/auth.service.ts +102 -0
  36. package/modules/auth/better-auth/files/nextjs/layout/layout.tsx +13 -0
  37. package/modules/auth/better-auth/files/nextjs/lib/axios/http.ts +158 -0
  38. package/modules/auth/better-auth/files/nextjs/lib/env.ts +35 -0
  39. package/modules/auth/better-auth/files/nextjs/lib/utils/auth.ts +75 -0
  40. package/modules/auth/better-auth/files/nextjs/lib/utils/cookie.ts +29 -0
  41. package/modules/auth/better-auth/files/nextjs/lib/utils/jwt.ts +28 -0
  42. package/modules/auth/better-auth/files/nextjs/lib/utils/token.ts +49 -0
  43. package/modules/auth/better-auth/files/nextjs/pages/forgot-password/page.tsx +5 -0
  44. package/modules/auth/better-auth/files/nextjs/pages/layout.tsx +11 -0
  45. package/modules/auth/better-auth/files/nextjs/pages/login/page.tsx +9 -0
  46. package/modules/auth/better-auth/files/nextjs/pages/register/page.tsx +5 -0
  47. package/modules/auth/better-auth/files/nextjs/pages/reset-password/page.tsx +10 -0
  48. package/modules/auth/better-auth/files/nextjs/pages/verify-email/page.tsx +10 -0
  49. package/modules/auth/better-auth/files/nextjs/proxy.ts +157 -22
  50. package/modules/auth/better-auth/files/nextjs/theme/providers/theme-provider.tsx +11 -0
  51. package/modules/auth/better-auth/files/nextjs/types/api.types.ts +18 -0
  52. package/modules/auth/better-auth/files/react/components/protected-route.tsx +39 -0
  53. package/modules/auth/better-auth/files/react/components/route-guards.tsx +13 -0
  54. package/modules/auth/better-auth/files/react/dashboard/admin/pages/overview.tsx +3 -0
  55. package/modules/auth/better-auth/files/react/dashboard/pages/overview.tsx +3 -0
  56. package/modules/auth/better-auth/files/react/features/pages/forgot-password.tsx +5 -0
  57. package/modules/auth/better-auth/files/react/features/pages/login.tsx +5 -0
  58. package/modules/auth/better-auth/files/react/features/pages/my-profile.tsx +5 -0
  59. package/modules/auth/better-auth/files/react/features/pages/oauth-callback.tsx +59 -0
  60. package/modules/auth/better-auth/files/react/features/pages/register.tsx +5 -0
  61. package/modules/auth/better-auth/files/react/features/pages/reset-password.tsx +10 -0
  62. package/modules/auth/better-auth/files/react/features/pages/verify-email.tsx +10 -0
  63. package/modules/auth/better-auth/files/react/layout/dashboard-layout.tsx +54 -0
  64. package/modules/auth/better-auth/files/react/lib/axios/http.ts +68 -0
  65. package/modules/auth/better-auth/files/react/lib/env.ts +25 -0
  66. package/modules/auth/better-auth/files/react/router.tsx +73 -0
  67. package/modules/auth/better-auth/files/react/theme/components/providers/theme-provider-context.ts +13 -0
  68. package/modules/auth/better-auth/files/react/theme/components/providers/theme-provider.tsx +51 -0
  69. package/modules/auth/better-auth/files/react/theme/hooks/use-theme.ts +8 -0
  70. package/modules/auth/better-auth/files/shared/features/components/change-password-dialog.tsx +113 -0
  71. package/modules/auth/better-auth/files/shared/features/components/forgot-password-form.tsx +84 -0
  72. package/modules/auth/better-auth/files/shared/features/components/login-form.tsx +134 -0
  73. package/modules/auth/better-auth/files/shared/features/components/my-profile.tsx +147 -0
  74. package/modules/auth/better-auth/files/shared/features/components/profile-form.tsx +205 -0
  75. package/modules/auth/better-auth/files/shared/features/components/register-form.tsx +100 -0
  76. package/modules/auth/better-auth/files/shared/features/components/reset-password-form.tsx +111 -0
  77. package/modules/auth/better-auth/files/shared/features/components/social-login-buttons.tsx +47 -0
  78. package/modules/auth/better-auth/files/shared/features/components/user-profile-menu.tsx +106 -0
  79. package/modules/auth/better-auth/files/shared/features/components/verify-email-form.tsx +110 -0
  80. package/modules/auth/better-auth/files/shared/features/queries/auth.mutations.tsx +312 -0
  81. package/modules/auth/better-auth/files/shared/features/queries/auth.querie.ts +19 -0
  82. package/modules/auth/better-auth/files/shared/features/services/auth.api.ts +81 -0
  83. package/modules/auth/better-auth/files/shared/features/types/auth.type.ts +47 -0
  84. package/modules/auth/better-auth/files/shared/features/validators/change-password.validator.ts +18 -0
  85. package/modules/auth/better-auth/files/shared/features/validators/forgot.validator.ts +7 -0
  86. package/modules/auth/better-auth/files/shared/features/validators/login.validator.ts +14 -0
  87. package/modules/auth/better-auth/files/shared/features/validators/profile.validator.ts +8 -0
  88. package/modules/auth/better-auth/files/shared/features/validators/register.validator.ts +9 -0
  89. package/modules/auth/better-auth/files/shared/features/validators/reset.validator.ts +9 -0
  90. package/modules/auth/better-auth/files/shared/features/validators/verify.validator.ts +8 -0
  91. package/modules/auth/better-auth/files/shared/lib/auth-client.ts +2 -1
  92. package/modules/auth/better-auth/files/shared/lib/auth.ts +10 -29
  93. package/modules/auth/better-auth/files/shared/lib/constant/dashboard.ts +90 -0
  94. package/modules/auth/better-auth/files/shared/prisma/enums.prisma +0 -1
  95. package/modules/auth/better-auth/files/shared/theme/mode-toggle.tsx +30 -0
  96. package/modules/auth/better-auth/files/shared/ui/shadcn/components/dashboard/dashboard-header.tsx +94 -0
  97. package/modules/auth/better-auth/files/shared/ui/shadcn/components/dashboard/dashboard-sidebar.tsx +255 -0
  98. package/modules/auth/better-auth/files/shared/ui/shadcn/components/footer.tsx +35 -0
  99. package/modules/auth/better-auth/files/shared/ui/shadcn/components/navbar.tsx +145 -0
  100. package/modules/auth/better-auth/files/shared/ui/shadcn/form-field/input-field.tsx +440 -0
  101. package/modules/auth/better-auth/files/shared/utils/email.ts +20 -18
  102. package/modules/auth/better-auth/generator.json +174 -53
  103. package/modules/auth/better-auth/module.json +2 -2
  104. package/modules/components/files/shared/hooks/use-file-upload.ts +412 -0
  105. package/modules/components/files/shared/lib/utils/url-helpers.ts +110 -0
  106. package/modules/components/files/shared/shadcn/dashboard/data-table-column-selector.tsx +52 -0
  107. package/modules/components/files/shared/shadcn/dashboard/data-table-footer.tsx +156 -0
  108. package/modules/components/files/shared/shadcn/dashboard/data-table.tsx +405 -0
  109. package/modules/components/files/shared/shadcn/global/form-field/input-field.tsx +440 -0
  110. package/modules/components/files/shared/shadcn/global/form-field/media-uploader-field.tsx +745 -0
  111. package/modules/components/files/shared/shadcn/global/form-field/multi-select-field.tsx +207 -0
  112. package/modules/components/files/shared/shadcn/global/form-field/select-field.tsx +247 -0
  113. package/modules/components/files/shared/shadcn/global/form-field/textarea-field.tsx +277 -0
  114. package/modules/components/files/shared/shadcn/global/form-field/tiptap-editor-field.tsx +35 -0
  115. package/modules/components/files/shared/shadcn/global/no-results.tsx +41 -0
  116. package/modules/components/files/shared/shadcn/tiptap-editor/editor-menu-bar.tsx +217 -0
  117. package/modules/components/files/shared/shadcn/tiptap-editor/tiptap-editor.tsx +104 -0
  118. package/modules/components/files/shared/url/load-more.tsx +93 -0
  119. package/modules/components/files/shared/url/search-bar.tsx +131 -0
  120. package/modules/components/files/shared/url/sort-select.tsx +118 -0
  121. package/modules/components/files/shared/url/url-tabs.tsx +77 -0
  122. package/modules/components/generator.json +109 -0
  123. package/modules/components/module.json +11 -0
  124. package/modules/database/mongoose/generator.json +3 -14
  125. package/modules/database/mongoose/module.json +2 -2
  126. package/modules/database/prisma/generator.json +6 -12
  127. package/modules/database/prisma/module.json +2 -2
  128. package/modules/storage/cloudinary/files/express/config/env.ts +65 -0
  129. package/modules/storage/cloudinary/files/express/config/media.ts +103 -0
  130. package/modules/storage/cloudinary/files/express/modules/media/media.controller.ts +59 -0
  131. package/modules/storage/cloudinary/files/express/modules/media/media.route.ts +29 -0
  132. package/modules/storage/cloudinary/files/express/modules/media/media.service.ts +113 -0
  133. package/modules/storage/cloudinary/files/express/modules/media/media.type.ts +32 -0
  134. package/modules/storage/cloudinary/generator.json +34 -0
  135. package/modules/storage/cloudinary/module.json +11 -0
  136. package/modules/ui/shadcn/generator.json +21 -0
  137. package/modules/ui/shadcn/module.json +11 -0
  138. package/package.json +24 -26
  139. package/templates/express/README.md +11 -16
  140. package/templates/express/src/config/env.ts +7 -5
  141. package/templates/nextjs/README.md +13 -18
  142. package/templates/nextjs/app/favicon.ico +0 -0
  143. package/templates/nextjs/app/layout.tsx +6 -4
  144. package/templates/nextjs/components/providers/query-provider.tsx +3 -0
  145. package/templates/nextjs/env.example +3 -1
  146. package/templates/nextjs/lib/axios/http.ts +23 -0
  147. package/templates/nextjs/lib/env.ts +7 -5
  148. package/templates/nextjs/package.json +2 -1
  149. package/templates/nextjs/template.json +1 -2
  150. package/templates/react/README.md +9 -14
  151. package/templates/react/index.html +1 -1
  152. package/templates/react/package.json +1 -1
  153. package/templates/react/src/assets/favicon.ico +0 -0
  154. package/templates/react/src/components/providers/query-provider.tsx +38 -0
  155. package/templates/react/src/{shared/components → components}/seo.tsx +4 -8
  156. package/templates/react/src/lib/axios/http.ts +24 -0
  157. package/templates/react/src/main.tsx +8 -11
  158. package/templates/react/src/{features/about/pages → pages}/about.tsx +1 -1
  159. package/templates/react/src/{features/home/pages → pages}/home.tsx +1 -1
  160. package/templates/react/src/router.tsx +6 -6
  161. package/templates/react/src/vite-env.d.ts +2 -1
  162. package/templates/react/template.json +0 -1
  163. package/templates/react/tsconfig.app.json +6 -0
  164. package/templates/react/tsconfig.json +7 -1
  165. package/templates/react/vite.config.ts +12 -0
  166. package/modules/auth/authjs/files/nextjs/api/auth/[...nextauth]/route.ts +0 -3
  167. package/modules/auth/authjs/files/nextjs/proxy.ts +0 -1
  168. package/modules/auth/authjs/files/shared/lib/auth.ts +0 -119
  169. package/modules/auth/authjs/files/shared/prisma/schema.prisma +0 -61
  170. package/modules/auth/authjs/generator.json +0 -64
  171. package/modules/auth/authjs/module.json +0 -13
  172. package/modules/auth/better-auth/files/express/modules/auth/auth.controller.ts +0 -264
  173. package/modules/auth/better-auth/files/express/modules/auth/auth.service.ts +0 -537
  174. package/modules/auth/better-auth/files/express/templates/google-redirect.ejs +0 -24
  175. package/modules/auth/better-auth/files/nextjs/api/auth/[...all]/route.ts +0 -4
  176. package/modules/auth/better-auth/files/nextjs/lib/auth/auth-guards.ts +0 -41
  177. package/modules/auth/better-auth/files/nextjs/templates/email-otp.tsx +0 -74
  178. package/templates/express/node_modules/.bin/acorn +0 -17
  179. package/templates/express/node_modules/.bin/eslint +0 -17
  180. package/templates/express/node_modules/.bin/tsc +0 -17
  181. package/templates/express/node_modules/.bin/tsserver +0 -17
  182. package/templates/express/node_modules/.bin/tsx +0 -17
  183. package/templates/nextjs/lib/api/http.ts +0 -40
  184. package/templates/nextjs/next-env.d.ts +0 -6
  185. package/templates/react/dist/assets/index-D4AHT4dU.js +0 -193
  186. package/templates/react/dist/assets/index-rpwj5ZOX.css +0 -1
  187. package/templates/react/dist/index.html +0 -14
  188. package/templates/react/dist/vite.svg +0 -1
  189. package/templates/react/public/vite.svg +0 -1
  190. package/templates/react/src/app/layouts/dashboard-layout.tsx +0 -8
  191. package/templates/react/src/app/layouts/public-layout.tsx +0 -5
  192. package/templates/react/src/app/providers.tsx +0 -20
  193. package/templates/react/src/app/router.tsx +0 -21
  194. package/templates/react/src/assets/react.svg +0 -1
  195. package/templates/react/src/shared/api/http.ts +0 -39
  196. package/templates/react/src/shared/components/loading.tsx +0 -8
  197. package/templates/react/src/shared/lib/query-client.ts +0 -12
  198. package/templates/react/src/utils/storage.ts +0 -35
  199. package/templates/react/src/utils/utils.ts +0 -3
  200. /package/templates/nextjs/app/{page.tsx → (public)/(root)/page.tsx} +0 -0
  201. /package/templates/react/src/{shared/components → components}/error-boundary.tsx +0 -0
  202. /package/templates/react/src/{shared/components → components}/layout.tsx +0 -0
  203. /package/templates/react/src/{shared/pages → pages}/not-found.tsx +0 -0
@@ -0,0 +1,440 @@
1
+ "use client";
2
+
3
+ import { Button } from "@/components/ui/button";
4
+ import { Checkbox } from "@/components/ui/checkbox";
5
+ import {
6
+ Field,
7
+ FieldContent,
8
+ FieldDescription,
9
+ FieldError,
10
+ FieldLabel,
11
+ } from "@/components/ui/field";
12
+ import {
13
+ InputGroup,
14
+ InputGroupAddon,
15
+ InputGroupInput,
16
+ } from "@/components/ui/input-group";
17
+ import { CloudUpload, Eye, EyeOff, type LucideIcon } from "lucide-react";
18
+ import type { ChangeEvent, HTMLInputTypeAttribute } from "react";
19
+ import * as React from "react";
20
+ import type { FieldPath, FieldValues } from "react-hook-form";
21
+ import { Controller } from "react-hook-form";
22
+
23
+ // Types
24
+ type ButtonVariant =
25
+ | "default"
26
+ | "outline"
27
+ | "destructive"
28
+ | "secondary"
29
+ | "ghost"
30
+ | "link";
31
+ type ButtonSize = "default" | "sm" | "lg" | "icon";
32
+
33
+ /** Base props shared across standalone & RHF modes */
34
+ type BaseProps = Omit<
35
+ React.InputHTMLAttributes<HTMLInputElement>,
36
+ "onChange" | "type" | "value"
37
+ > & {
38
+ icon?: LucideIcon;
39
+ startIcon?: LucideIcon;
40
+ label?: React.ReactNode;
41
+ labelNode?: React.ReactNode;
42
+ disable?: boolean;
43
+ isDisabled?: boolean;
44
+ value?: string | number | boolean;
45
+ defaultValue?: string | number | boolean;
46
+ requiredMark?: boolean;
47
+ hint?: React.ReactNode;
48
+ onChange?: React.ChangeEventHandler<HTMLInputElement>;
49
+ type?: HTMLInputTypeAttribute;
50
+ parseNumber?: boolean;
51
+ preventWheelChange?: boolean;
52
+ id?: string;
53
+ className?: string;
54
+ inputClassName?: string;
55
+ multiple?: boolean;
56
+ accept?: string;
57
+ onFilesChange?: (files: File[] | null) => void;
58
+ fileIcon?: LucideIcon;
59
+ fileButtonLabel?: string;
60
+ fileButtonVariant?: ButtonVariant;
61
+ fileButtonSize?: ButtonSize;
62
+ };
63
+
64
+ /** Props for generic + optional RHF name */
65
+ export type InputFieldProps<
66
+ TFieldValues extends FieldValues = FieldValues,
67
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
68
+ > = BaseProps & {
69
+ name?: TName;
70
+ };
71
+
72
+ // Helpers
73
+ function toTextInputValue(v: unknown): string | number {
74
+ if (v === null || v === undefined) return "";
75
+ if (typeof v === "boolean") return v ? "1" : "";
76
+ if (typeof v === "string" || typeof v === "number") return v;
77
+ return "";
78
+ }
79
+
80
+ function makeSyntheticCheckboxChange(
81
+ checked: boolean,
82
+ ): React.ChangeEvent<HTMLInputElement> {
83
+ const input = document.createElement("input");
84
+ input.type = "checkbox";
85
+ input.checked = checked;
86
+ const evt = new Event("change", { bubbles: true });
87
+ Object.defineProperty(evt, "target", { writable: false, value: input });
88
+ return evt as unknown as React.ChangeEvent<HTMLInputElement>;
89
+ }
90
+
91
+ // Component
92
+ export default function InputField<
93
+ TFieldValues extends FieldValues = FieldValues,
94
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
95
+ >({
96
+ name,
97
+ icon: LabelIcon,
98
+ startIcon,
99
+ label,
100
+ labelNode,
101
+ placeholder,
102
+ type = "text",
103
+ disabled,
104
+ isDisabled,
105
+ disable,
106
+ className,
107
+ inputClassName,
108
+ value,
109
+ defaultValue,
110
+ requiredMark,
111
+ hint,
112
+ onChange,
113
+ id,
114
+ parseNumber,
115
+ preventWheelChange,
116
+ multiple,
117
+ accept,
118
+ onFilesChange,
119
+ fileIcon: FileIcon,
120
+ fileButtonLabel = "Import",
121
+ fileButtonVariant = "outline",
122
+ fileButtonSize = "default",
123
+ ...rest
124
+ }: InputFieldProps<TFieldValues, TName>) {
125
+ const effectiveDisabled = disabled ?? isDisabled ?? disable ?? false;
126
+ const isFile = type === "file";
127
+ const isCheckbox = type === "checkbox";
128
+ const shouldParseNumber = !isCheckbox && (parseNumber ?? type === "number");
129
+ const shouldPreventWheel =
130
+ !isCheckbox && (preventWheelChange ?? shouldParseNumber);
131
+ const labelContent = labelNode ?? label;
132
+
133
+ const hiddenFileRef = React.useRef<HTMLInputElement | null>(null);
134
+ const chooseFile = React.useCallback(
135
+ () => hiddenFileRef.current?.click(),
136
+ [],
137
+ );
138
+
139
+ const [internal, setInternal] = React.useState<
140
+ string | number | boolean | undefined
141
+ >(defaultValue);
142
+
143
+ const [showPassword, setShowPassword] = React.useState(false);
144
+ const isPassword = type === "password";
145
+ const effectiveType =
146
+ isPassword && showPassword ? ("text" as HTMLInputTypeAttribute) : type;
147
+
148
+ const inFormMode = Boolean(name);
149
+
150
+ const toNumberIfNeeded = (raw: string): string | number => {
151
+ if (!shouldParseNumber) return raw;
152
+ return raw === "" ? "" : Number(raw);
153
+ };
154
+
155
+ const LabelIconEffective: LucideIcon | undefined = startIcon ?? LabelIcon;
156
+ const FileButtonIcon: LucideIcon = FileIcon ?? CloudUpload;
157
+
158
+ if (inFormMode) {
159
+ // RHF mode using Controller + Field components
160
+ return (
161
+ <Controller<TFieldValues, TName>
162
+ name={name as TName}
163
+ render={({ field, fieldState }) => {
164
+ const inputId = id ?? (field.name as string);
165
+
166
+ const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
167
+ const fl = e.target.files;
168
+ field.onChange(fl);
169
+ onFilesChange?.(fl ? Array.from(fl) : null);
170
+ };
171
+
172
+ const handleChangeTextOrNumber = (
173
+ e: ChangeEvent<HTMLInputElement>,
174
+ ) => {
175
+ const raw = e.target.value;
176
+ const val = toNumberIfNeeded(raw);
177
+ field.onChange(val);
178
+ onChange?.(e);
179
+ };
180
+
181
+ const assignRefs = (el: HTMLInputElement | null) => {
182
+ hiddenFileRef.current = el;
183
+ const ref = field.ref as
184
+ | ((instance: HTMLInputElement | null) => void)
185
+ | React.MutableRefObject<HTMLInputElement | null>
186
+ | undefined;
187
+ if (typeof ref === "function") ref(el);
188
+ else if (ref && "current" in ref) ref.current = el;
189
+ };
190
+
191
+ return (
192
+ <Field className={className} data-invalid={fieldState.invalid}>
193
+ {!isCheckbox && labelContent ? (
194
+ <FieldLabel
195
+ className="mb-2 inline-flex items-center gap-2"
196
+ htmlFor={inputId}
197
+ >
198
+ {LabelIcon ? (
199
+ <LabelIcon className="h-4 w-4" />
200
+ ) : LabelIconEffective ? (
201
+ <LabelIconEffective className="h-4 w-4" />
202
+ ) : null}
203
+ {labelContent}
204
+ {requiredMark ? (
205
+ <span className="ml-0.5 text-destructive">*</span>
206
+ ) : null}
207
+ </FieldLabel>
208
+ ) : null}
209
+
210
+ <FieldContent>
211
+ {isFile ? (
212
+ <div className="flex items-center gap-2">
213
+ <input
214
+ id={inputId}
215
+ ref={assignRefs}
216
+ type="file"
217
+ className="hidden"
218
+ disabled={effectiveDisabled}
219
+ multiple={multiple}
220
+ accept={accept}
221
+ onChange={handleFileChange}
222
+ {...rest}
223
+ />
224
+ <Button
225
+ type="button"
226
+ variant={fileButtonVariant}
227
+ size={fileButtonSize}
228
+ onClick={chooseFile}
229
+ disabled={effectiveDisabled}
230
+ className="gap-2"
231
+ >
232
+ <FileButtonIcon className="h-4 w-4" />
233
+ {fileButtonLabel}
234
+ </Button>
235
+ </div>
236
+ ) : isCheckbox ? (
237
+ <div className="flex items-center gap-2">
238
+ <Checkbox
239
+ id={inputId}
240
+ checked={Boolean(field.value)}
241
+ onCheckedChange={(v) => field.onChange(Boolean(v))}
242
+ disabled={effectiveDisabled}
243
+ />
244
+ {labelContent ? (
245
+ <FieldLabel
246
+ htmlFor={inputId}
247
+ className="cursor-pointer select-none"
248
+ >
249
+ {labelContent}
250
+ {requiredMark ? (
251
+ <span className="ml-0.5 text-destructive">*</span>
252
+ ) : null}
253
+ </FieldLabel>
254
+ ) : null}
255
+ </div>
256
+ ) : (
257
+ <InputGroup>
258
+ <InputGroupInput
259
+ id={inputId}
260
+ type={effectiveType}
261
+ placeholder={placeholder}
262
+ disabled={effectiveDisabled}
263
+ value={toTextInputValue(field.value)}
264
+ onChange={handleChangeTextOrNumber}
265
+ onWheel={
266
+ shouldPreventWheel
267
+ ? (e) => (e.currentTarget as HTMLInputElement).blur()
268
+ : undefined
269
+ }
270
+ className={inputClassName}
271
+ ref={field.ref}
272
+ {...rest}
273
+ />
274
+ {isPassword ? (
275
+ <InputGroupAddon align="inline-end">
276
+ <Button
277
+ type="button"
278
+ variant="ghost"
279
+ size="icon"
280
+ className="h-full px-3 py-2 hover:bg-transparent"
281
+ onClick={() => setShowPassword(!showPassword)}
282
+ disabled={effectiveDisabled}
283
+ >
284
+ {showPassword ? (
285
+ <EyeOff className="h-4 w-4" />
286
+ ) : (
287
+ <Eye className="h-4 w-4" />
288
+ )}
289
+ </Button>
290
+ </InputGroupAddon>
291
+ ) : null}
292
+ </InputGroup>
293
+ )}
294
+ </FieldContent>
295
+
296
+ {hint ? <FieldDescription>{hint}</FieldDescription> : null}
297
+ {fieldState.invalid && <FieldError errors={[fieldState.error]} />}
298
+ </Field>
299
+ );
300
+ }}
301
+ />
302
+ );
303
+ }
304
+
305
+ // Standalone mode
306
+ const isControlled = value !== undefined;
307
+ const currentValue = isControlled ? value : internal;
308
+
309
+ const inputId =
310
+ id ??
311
+ (typeof label === "string"
312
+ ? `input-${label.toLowerCase().replace(/\s+/g, "-")}`
313
+ : undefined);
314
+
315
+ const handleStandaloneChange: React.ChangeEventHandler<HTMLInputElement> = (
316
+ e,
317
+ ) => {
318
+ const raw = e.target.value;
319
+ const val = toNumberIfNeeded(raw);
320
+ if (!isControlled) setInternal(val);
321
+ onChange?.(e);
322
+ };
323
+
324
+ const handleStandaloneFileChange: React.ChangeEventHandler<
325
+ HTMLInputElement
326
+ > = (e) => {
327
+ const fl = e.target.files;
328
+ onFilesChange?.(fl ? Array.from(fl) : null);
329
+ onChange?.(e);
330
+ };
331
+
332
+ const handleStandaloneCheckboxChange = (checked: boolean) => {
333
+ if (!isControlled) setInternal(checked);
334
+ if (onChange) onChange(makeSyntheticCheckboxChange(checked));
335
+ };
336
+
337
+ return (
338
+ <div className={className}>
339
+ {!isCheckbox && labelContent ? (
340
+ <FieldLabel
341
+ className="mb-2 inline-flex items-center gap-2"
342
+ htmlFor={inputId}
343
+ >
344
+ {LabelIcon ? <LabelIcon className="h-4 w-4" /> : null}
345
+ {labelContent}
346
+ {requiredMark ? (
347
+ <span className="ml-0.5 text-destructive">*</span>
348
+ ) : null}
349
+ </FieldLabel>
350
+ ) : null}
351
+
352
+ {isFile ? (
353
+ <div className="flex items-center gap-2">
354
+ <input
355
+ id={inputId}
356
+ ref={hiddenFileRef}
357
+ type="file"
358
+ className="hidden"
359
+ disabled={effectiveDisabled}
360
+ multiple={multiple}
361
+ accept={accept}
362
+ onChange={handleStandaloneFileChange}
363
+ {...rest}
364
+ />
365
+ <Button
366
+ type="button"
367
+ variant={fileButtonVariant}
368
+ size={fileButtonSize}
369
+ onClick={chooseFile}
370
+ disabled={effectiveDisabled}
371
+ className="gap-2"
372
+ >
373
+ <FileButtonIcon className="h-4 w-4" />
374
+ {fileButtonLabel}
375
+ </Button>
376
+ </div>
377
+ ) : isCheckbox ? (
378
+ <div className="flex items-center gap-2">
379
+ <Checkbox
380
+ id={inputId}
381
+ checked={Boolean(currentValue)}
382
+ onCheckedChange={(v) => handleStandaloneCheckboxChange(Boolean(v))}
383
+ disabled={effectiveDisabled}
384
+ />
385
+ {labelContent ? (
386
+ <FieldLabel
387
+ htmlFor={inputId}
388
+ className="cursor-pointer select-none"
389
+ >
390
+ {labelContent}
391
+ {requiredMark ? (
392
+ <span className="ml-0.5 text-destructive">*</span>
393
+ ) : null}
394
+ </FieldLabel>
395
+ ) : null}
396
+ </div>
397
+ ) : (
398
+ <InputGroup>
399
+ <InputGroupInput
400
+ id={inputId}
401
+ type={effectiveType}
402
+ placeholder={placeholder}
403
+ disabled={effectiveDisabled}
404
+ value={toTextInputValue(currentValue)}
405
+ onChange={handleStandaloneChange}
406
+ onWheel={
407
+ shouldPreventWheel
408
+ ? (e) => (e.currentTarget as HTMLInputElement).blur()
409
+ : undefined
410
+ }
411
+ className={inputClassName}
412
+ {...rest}
413
+ />
414
+ {isPassword ? (
415
+ <InputGroupAddon align="inline-end">
416
+ <Button
417
+ type="button"
418
+ variant="ghost"
419
+ size="icon"
420
+ className="h-full px-3 py-2 hover:bg-transparent"
421
+ onClick={() => setShowPassword(!showPassword)}
422
+ disabled={effectiveDisabled}
423
+ >
424
+ {showPassword ? (
425
+ <EyeOff className="h-4 w-4" />
426
+ ) : (
427
+ <Eye className="h-4 w-4" />
428
+ )}
429
+ </Button>
430
+ </InputGroupAddon>
431
+ ) : null}
432
+ </InputGroup>
433
+ )}
434
+
435
+ {hint ? (
436
+ <p className="mt-1 text-xs text-muted-foreground">{hint}</p>
437
+ ) : null}
438
+ </div>
439
+ );
440
+ }