@luanthnh/cntt-ui 0.1.5

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 (255) hide show
  1. package/.storybook/globals.d.ts +1 -0
  2. package/.storybook/main.ts +29 -0
  3. package/.storybook/preview.ts +32 -0
  4. package/README.md +86 -0
  5. package/assets/fonts/Montserrat-Black.eot +0 -0
  6. package/assets/fonts/Montserrat-Black.ttf +0 -0
  7. package/assets/fonts/Montserrat-Black.woff +0 -0
  8. package/assets/fonts/Montserrat-Black.woff2 +0 -0
  9. package/assets/fonts/Montserrat-BlackItalic.eot +0 -0
  10. package/assets/fonts/Montserrat-BlackItalic.ttf +0 -0
  11. package/assets/fonts/Montserrat-BlackItalic.woff +0 -0
  12. package/assets/fonts/Montserrat-BlackItalic.woff2 +0 -0
  13. package/assets/fonts/Montserrat-Bold.eot +0 -0
  14. package/assets/fonts/Montserrat-Bold.ttf +0 -0
  15. package/assets/fonts/Montserrat-Bold.woff +0 -0
  16. package/assets/fonts/Montserrat-Bold.woff2 +0 -0
  17. package/assets/fonts/Montserrat-BoldItalic.eot +0 -0
  18. package/assets/fonts/Montserrat-BoldItalic.ttf +0 -0
  19. package/assets/fonts/Montserrat-BoldItalic.woff +0 -0
  20. package/assets/fonts/Montserrat-BoldItalic.woff2 +0 -0
  21. package/assets/fonts/Montserrat-ExtraBold.eot +0 -0
  22. package/assets/fonts/Montserrat-ExtraBold.ttf +0 -0
  23. package/assets/fonts/Montserrat-ExtraBold.woff +0 -0
  24. package/assets/fonts/Montserrat-ExtraBold.woff2 +0 -0
  25. package/assets/fonts/Montserrat-ExtraBoldItalic.eot +0 -0
  26. package/assets/fonts/Montserrat-ExtraBoldItalic.ttf +0 -0
  27. package/assets/fonts/Montserrat-ExtraBoldItalic.woff +0 -0
  28. package/assets/fonts/Montserrat-ExtraBoldItalic.woff2 +0 -0
  29. package/assets/fonts/Montserrat-ExtraLight.eot +0 -0
  30. package/assets/fonts/Montserrat-ExtraLight.ttf +0 -0
  31. package/assets/fonts/Montserrat-ExtraLight.woff +0 -0
  32. package/assets/fonts/Montserrat-ExtraLight.woff2 +0 -0
  33. package/assets/fonts/Montserrat-ExtraLightItalic.eot +0 -0
  34. package/assets/fonts/Montserrat-ExtraLightItalic.ttf +0 -0
  35. package/assets/fonts/Montserrat-ExtraLightItalic.woff +0 -0
  36. package/assets/fonts/Montserrat-ExtraLightItalic.woff2 +0 -0
  37. package/assets/fonts/Montserrat-Italic.eot +0 -0
  38. package/assets/fonts/Montserrat-Italic.ttf +0 -0
  39. package/assets/fonts/Montserrat-Italic.woff +0 -0
  40. package/assets/fonts/Montserrat-Italic.woff2 +0 -0
  41. package/assets/fonts/Montserrat-Light.eot +0 -0
  42. package/assets/fonts/Montserrat-Light.ttf +0 -0
  43. package/assets/fonts/Montserrat-Light.woff +0 -0
  44. package/assets/fonts/Montserrat-Light.woff2 +0 -0
  45. package/assets/fonts/Montserrat-LightItalic.eot +0 -0
  46. package/assets/fonts/Montserrat-LightItalic.ttf +0 -0
  47. package/assets/fonts/Montserrat-LightItalic.woff +0 -0
  48. package/assets/fonts/Montserrat-LightItalic.woff2 +0 -0
  49. package/assets/fonts/Montserrat-Medium.eot +0 -0
  50. package/assets/fonts/Montserrat-Medium.ttf +0 -0
  51. package/assets/fonts/Montserrat-Medium.woff +0 -0
  52. package/assets/fonts/Montserrat-Medium.woff2 +0 -0
  53. package/assets/fonts/Montserrat-MediumItalic.eot +0 -0
  54. package/assets/fonts/Montserrat-MediumItalic.ttf +0 -0
  55. package/assets/fonts/Montserrat-MediumItalic.woff +0 -0
  56. package/assets/fonts/Montserrat-MediumItalic.woff2 +0 -0
  57. package/assets/fonts/Montserrat-Regular.eot +0 -0
  58. package/assets/fonts/Montserrat-Regular.ttf +0 -0
  59. package/assets/fonts/Montserrat-Regular.woff +0 -0
  60. package/assets/fonts/Montserrat-Regular.woff2 +0 -0
  61. package/assets/fonts/Montserrat-SemiBold.eot +0 -0
  62. package/assets/fonts/Montserrat-SemiBold.ttf +0 -0
  63. package/assets/fonts/Montserrat-SemiBold.woff +0 -0
  64. package/assets/fonts/Montserrat-SemiBold.woff2 +0 -0
  65. package/assets/fonts/Montserrat-SemiBoldItalic.eot +0 -0
  66. package/assets/fonts/Montserrat-SemiBoldItalic.ttf +0 -0
  67. package/assets/fonts/Montserrat-SemiBoldItalic.woff +0 -0
  68. package/assets/fonts/Montserrat-SemiBoldItalic.woff2 +0 -0
  69. package/assets/fonts/Montserrat-Thin.eot +0 -0
  70. package/assets/fonts/Montserrat-Thin.ttf +0 -0
  71. package/assets/fonts/Montserrat-Thin.woff +0 -0
  72. package/assets/fonts/Montserrat-Thin.woff2 +0 -0
  73. package/assets/fonts/Montserrat-ThinItalic.eot +0 -0
  74. package/assets/fonts/Montserrat-ThinItalic.ttf +0 -0
  75. package/assets/fonts/Montserrat-ThinItalic.woff +0 -0
  76. package/assets/fonts/Montserrat-ThinItalic.woff2 +0 -0
  77. package/assets/fonts/Montserrat-Variable.eot +0 -0
  78. package/assets/fonts/Montserrat-Variable.ttf +0 -0
  79. package/assets/fonts/Montserrat-Variable.woff +0 -0
  80. package/assets/fonts/Montserrat-Variable.woff2 +0 -0
  81. package/assets/fonts/Montserrat-VariableItalic.eot +0 -0
  82. package/assets/fonts/Montserrat-VariableItalic.ttf +0 -0
  83. package/assets/fonts/Montserrat-VariableItalic.woff +0 -0
  84. package/assets/fonts/Montserrat-VariableItalic.woff2 +0 -0
  85. package/assets/icons/arrow-left.svg +1 -0
  86. package/assets/icons/file.svg +1 -0
  87. package/assets/icons/globe.svg +1 -0
  88. package/assets/icons/logo-line.svg +1 -0
  89. package/assets/icons/next.svg +1 -0
  90. package/assets/icons/panel-left-expand.svg +1 -0
  91. package/assets/icons/placeholder.svg +57 -0
  92. package/assets/icons/vercel.svg +1 -0
  93. package/assets/icons/window.svg +1 -0
  94. package/assets/lotties/error-404.json +19642 -0
  95. package/assets/lotties/error.json +2414 -0
  96. package/assets/lotties/loader.json +305 -0
  97. package/components/Welcome.mdx +74 -0
  98. package/components/lenis/index.tsx +48 -0
  99. package/components/motion/auto-height.tsx +56 -0
  100. package/components/motion/cursor.tsx +108 -0
  101. package/components/motion/highlight.tsx +605 -0
  102. package/components/motion/number-ticker.tsx +55 -0
  103. package/components/motion/slot.tsx +106 -0
  104. package/components/motion/waves.tsx +417 -0
  105. package/components/primitives/tabs.tsx +174 -0
  106. package/components/ui/Accordion/index.stories.tsx +39 -0
  107. package/components/ui/Accordion/index.tsx +170 -0
  108. package/components/ui/Alert/index.stories.tsx +39 -0
  109. package/components/ui/Alert/index.tsx +60 -0
  110. package/components/ui/AlertDialog/index.stories.tsx +47 -0
  111. package/components/ui/AlertDialog/index.tsx +172 -0
  112. package/components/ui/AspectRatio/index.stories.tsx +40 -0
  113. package/components/ui/AspectRatio/index.tsx +9 -0
  114. package/components/ui/Avatar/index.stories.tsx +39 -0
  115. package/components/ui/Avatar/index.tsx +44 -0
  116. package/components/ui/Badge/index.stories.tsx +64 -0
  117. package/components/ui/Badge/index.tsx +46 -0
  118. package/components/ui/Breadcrumb/index.stories.tsx +64 -0
  119. package/components/ui/Breadcrumb/index.tsx +102 -0
  120. package/components/ui/Button/index.stories.tsx +232 -0
  121. package/components/ui/Button/index.tsx +114 -0
  122. package/components/ui/Calendar/index.stories.tsx +20 -0
  123. package/components/ui/Calendar/index.tsx +149 -0
  124. package/components/ui/Card/index.stories.tsx +39 -0
  125. package/components/ui/Card/index.tsx +65 -0
  126. package/components/ui/Carousel/index.stories.tsx +37 -0
  127. package/components/ui/Carousel/index.tsx +242 -0
  128. package/components/ui/Chart/index.stories.tsx +53 -0
  129. package/components/ui/Chart/index.tsx +322 -0
  130. package/components/ui/Checkbox/index.stories.tsx +56 -0
  131. package/components/ui/Checkbox/index.tsx +167 -0
  132. package/components/ui/CircleProcess/index.stories.tsx +29 -0
  133. package/components/ui/CircleProcess/index.tsx +50 -0
  134. package/components/ui/Collapsible/index.stories.tsx +33 -0
  135. package/components/ui/Collapsible/index.tsx +124 -0
  136. package/components/ui/Command/index.stories.tsx +65 -0
  137. package/components/ui/Command/index.tsx +161 -0
  138. package/components/ui/Container/index.stories.tsx +22 -0
  139. package/components/ui/Container/index.tsx +30 -0
  140. package/components/ui/ContextMenu/index.stories.tsx +51 -0
  141. package/components/ui/ContextMenu/index.tsx +224 -0
  142. package/components/ui/Dialog/index.stories.tsx +44 -0
  143. package/components/ui/Dialog/index.tsx +156 -0
  144. package/components/ui/Drawer/index.stories.tsx +54 -0
  145. package/components/ui/Drawer/index.tsx +124 -0
  146. package/components/ui/DropdownMenu/index.stories.tsx +83 -0
  147. package/components/ui/DropdownMenu/index.tsx +231 -0
  148. package/components/ui/Dropzone/index.stories.tsx +18 -0
  149. package/components/ui/Dropzone/index.tsx +47 -0
  150. package/components/ui/Form/date-field.tsx +77 -0
  151. package/components/ui/Form/index.stories.tsx +67 -0
  152. package/components/ui/Form/index.tsx +188 -0
  153. package/components/ui/Form/select-field.tsx +55 -0
  154. package/components/ui/Form/text-area-field.tsx +37 -0
  155. package/components/ui/Form/text-field.tsx +72 -0
  156. package/components/ui/HStack/index.stories.tsx +48 -0
  157. package/components/ui/HStack/index.tsx +73 -0
  158. package/components/ui/HoverCard/index.stories.tsx +38 -0
  159. package/components/ui/HoverCard/index.tsx +38 -0
  160. package/components/ui/Icons/index.stories.tsx +27 -0
  161. package/components/ui/Icons/index.tsx +33 -0
  162. package/components/ui/ImageWithFallback/index.stories.tsx +32 -0
  163. package/components/ui/ImageWithFallback/index.tsx +34 -0
  164. package/components/ui/Input/index.stories.tsx +47 -0
  165. package/components/ui/Input/index.tsx +21 -0
  166. package/components/ui/InputOtp/index.stories.tsx +35 -0
  167. package/components/ui/InputOtp/index.tsx +70 -0
  168. package/components/ui/Label/index.stories.tsx +18 -0
  169. package/components/ui/Label/index.tsx +21 -0
  170. package/components/ui/Marquee/index.stories.tsx +71 -0
  171. package/components/ui/Marquee/index.tsx +65 -0
  172. package/components/ui/Menubar/index.stories.tsx +116 -0
  173. package/components/ui/Menubar/index.tsx +252 -0
  174. package/components/ui/NavigationMenu/index.stories.tsx +112 -0
  175. package/components/ui/NavigationMenu/index.tsx +185 -0
  176. package/components/ui/NoData/index.stories.tsx +24 -0
  177. package/components/ui/NoData/index.tsx +19 -0
  178. package/components/ui/Pagination/index.stories.tsx +53 -0
  179. package/components/ui/Pagination/index.tsx +114 -0
  180. package/components/ui/Popover/index.stories.tsx +31 -0
  181. package/components/ui/Popover/index.tsx +42 -0
  182. package/components/ui/Progress/index.stories.tsx +35 -0
  183. package/components/ui/Progress/index.tsx +28 -0
  184. package/components/ui/RadioGroup/index.stories.tsx +28 -0
  185. package/components/ui/RadioGroup/index.tsx +45 -0
  186. package/components/ui/Resizable/index.stories.tsx +44 -0
  187. package/components/ui/Resizable/index.tsx +54 -0
  188. package/components/ui/ScrollArea/index.stories.tsx +31 -0
  189. package/components/ui/ScrollArea/index.tsx +56 -0
  190. package/components/ui/Select/index.stories.tsx +64 -0
  191. package/components/ui/Select/index.tsx +170 -0
  192. package/components/ui/Separator/index.stories.tsx +31 -0
  193. package/components/ui/Separator/index.tsx +28 -0
  194. package/components/ui/Sheet/index.stories.tsx +45 -0
  195. package/components/ui/Sheet/index.tsx +130 -0
  196. package/components/ui/Sidebar/index.stories.tsx +82 -0
  197. package/components/ui/Sidebar/index.tsx +676 -0
  198. package/components/ui/Skeleton/index.stories.tsx +36 -0
  199. package/components/ui/Skeleton/index.tsx +13 -0
  200. package/components/ui/Slider/index.stories.tsx +48 -0
  201. package/components/ui/Slider/index.tsx +82 -0
  202. package/components/ui/Slot/index.stories.tsx +29 -0
  203. package/components/ui/Slot/index.tsx +106 -0
  204. package/components/ui/Sonner/index.stories.tsx +36 -0
  205. package/components/ui/Sonner/index.tsx +31 -0
  206. package/components/ui/Switch/index.stories.tsx +33 -0
  207. package/components/ui/Switch/index.tsx +28 -0
  208. package/components/ui/Table/index.stories.tsx +74 -0
  209. package/components/ui/Table/index.tsx +95 -0
  210. package/components/ui/Tabs/index.stories.tsx +38 -0
  211. package/components/ui/Tabs/index.tsx +78 -0
  212. package/components/ui/Text/index.stories.tsx +53 -0
  213. package/components/ui/Text/index.tsx +138 -0
  214. package/components/ui/Textarea/index.stories.tsx +25 -0
  215. package/components/ui/Textarea/index.tsx +18 -0
  216. package/components/ui/Toggle/index.stories.tsx +52 -0
  217. package/components/ui/Toggle/index.tsx +46 -0
  218. package/components/ui/ToggleGroup/index.stories.tsx +52 -0
  219. package/components/ui/ToggleGroup/index.tsx +69 -0
  220. package/components/ui/Tooltip/index.stories.tsx +29 -0
  221. package/components/ui/Tooltip/index.tsx +35 -0
  222. package/components/ui/VStack/index.stories.tsx +45 -0
  223. package/components/ui/VStack/index.tsx +69 -0
  224. package/components/ui/colors.stories.tsx +148 -0
  225. package/dist/arrow-left-46B4CAEY.svg +1 -0
  226. package/dist/file-4IXBJF4J.svg +1 -0
  227. package/dist/globe-KVAXBN2U.svg +1 -0
  228. package/dist/index.cjs +6001 -0
  229. package/dist/index.cjs.map +1 -0
  230. package/dist/index.d.cts +693 -0
  231. package/dist/index.d.ts +693 -0
  232. package/dist/index.js +5714 -0
  233. package/dist/index.js.map +1 -0
  234. package/dist/logo-line-QLUD5DAV.svg +1 -0
  235. package/dist/next-HOXZBJQP.svg +1 -0
  236. package/dist/panel-left-expand-SIPFBG4J.svg +1 -0
  237. package/dist/placeholder-H3V4XYVI.svg +57 -0
  238. package/dist/vercel-KFYFHF3A.svg +1 -0
  239. package/dist/window-JNUL4Q2E.svg +1 -0
  240. package/eslint.config.js +10 -0
  241. package/globals.css +994 -0
  242. package/hooks/index.ts +3 -0
  243. package/hooks/use-auto-height.tsx +99 -0
  244. package/hooks/use-controlled-state.tsx +32 -0
  245. package/hooks/use-mobile.ts +19 -0
  246. package/index.ts +58 -0
  247. package/lib/get-strict-context.ts +15 -0
  248. package/lib/utils.ts +10 -0
  249. package/package.json +107 -0
  250. package/scripts/generate-exports.ts +32 -0
  251. package/tsconfig.json +12 -0
  252. package/tsconfig.tsbuildinfo +1 -0
  253. package/tsup.config.ts +11 -0
  254. package/types/svg.d.ts +10 -0
  255. package/vercel.json +5 -0
@@ -0,0 +1,18 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+
3
+ import { Dropzone } from './index';
4
+
5
+ const meta: Meta<typeof Dropzone> = {
6
+ title: 'UI/Dropzone',
7
+ component: Dropzone,
8
+ tags: ['autodocs'],
9
+ };
10
+
11
+ export default meta;
12
+ type Story = StoryObj<typeof Dropzone>;
13
+
14
+ export const Default: Story = {
15
+ args: {
16
+ onFile: (file) => console.log('File dropped:', file),
17
+ },
18
+ };
@@ -0,0 +1,47 @@
1
+ 'use client';
2
+
3
+ import { useCallback } from 'react';
4
+ import { UploadCloud } from 'lucide-react';
5
+ import { useDropzone } from 'react-dropzone';
6
+
7
+ import { cn } from '@/lib/utils';
8
+
9
+ interface Props {
10
+ onFile: (file: File) => void;
11
+ className?: string;
12
+ }
13
+
14
+ export function Dropzone({ onFile, className }: Props) {
15
+ const onDrop = useCallback(
16
+ (acceptedFiles: File[]) => {
17
+ if (acceptedFiles[0]) onFile(acceptedFiles[0]);
18
+ },
19
+ [onFile],
20
+ );
21
+
22
+ const { getRootProps, getInputProps, isDragActive } = useDropzone({
23
+ onDrop,
24
+ accept: { 'image/*': [] },
25
+ multiple: false,
26
+ });
27
+
28
+ return (
29
+ <div
30
+ {...getRootProps()}
31
+ className={cn(
32
+ 'group border-muted-foreground/25 hover:border-muted-foreground/50 relative grid h-32 w-full cursor-pointer place-items-center rounded-lg border-2 border-dashed px-6 py-4 text-center transition',
33
+ isDragActive && 'border-primary',
34
+ className,
35
+ )}
36
+ >
37
+ <input {...getInputProps()} />
38
+ <div className="text-muted-foreground flex flex-col items-center gap-1 text-sm">
39
+ <UploadCloud className="h-6 w-6" />
40
+ <span className="font-medium">
41
+ {isDragActive ? 'Thả ảnh vào đây' : 'Kéo thả hoặc click để chọn'}
42
+ </span>
43
+ <span className="text-xs">PNG, JPG, GIF tối đa 5 MB</span>
44
+ </div>
45
+ </div>
46
+ );
47
+ }
@@ -0,0 +1,77 @@
1
+ 'use client';
2
+
3
+ import type * as React from 'react';
4
+ import { format } from 'date-fns';
5
+ import { CalendarIcon } from 'lucide-react';
6
+ import { ControllerProps, FieldPath, FieldValues } from 'react-hook-form';
7
+
8
+ import { cn } from '@/lib/utils';
9
+ import { Button } from '@/components/ui/Button';
10
+ import { Calendar } from '@/components/ui/Calendar';
11
+ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover';
12
+
13
+ import { FormControl, FormField, FormItem, FormLabel, FormMessage } from './index';
14
+
15
+ interface Props<
16
+ TFieldValues extends FieldValues = FieldValues,
17
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
18
+ > extends Omit<React.ComponentProps<'button'>, 'name' | 'type' | 'defaultValue'> {
19
+ name: TName;
20
+ control: ControllerProps<TFieldValues, TName>['control'];
21
+ label?: string;
22
+ isRequired?: boolean;
23
+ }
24
+
25
+ export function DateField<
26
+ TFieldValues extends FieldValues = FieldValues,
27
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
28
+ >({ name, control, label, isRequired, ...props }: Props<TFieldValues, TName>) {
29
+ return (
30
+ <FormField
31
+ name={name}
32
+ control={control}
33
+ render={({ field }) => (
34
+ <FormItem className="flex flex-col gap-2">
35
+ {label && <FormLabel isRequired={isRequired}>{label}</FormLabel>}
36
+ <FormControl>
37
+ <Popover>
38
+ <PopoverTrigger asChild>
39
+ <Button
40
+ variant="outline"
41
+ className={cn(
42
+ 'border-primary-100 h-11 w-full justify-start text-left font-normal text-black shadow-xs transition-[color,box-shadow]',
43
+ !field.value && 'text-muted-foreground',
44
+ props.className,
45
+ )}
46
+ {...props}
47
+ >
48
+ <CalendarIcon className="mr-2 h-4 w-4" />
49
+ {field.value ? (
50
+ typeof field.value === 'string' ? (
51
+ format(new Date(field.value), 'dd/MM/yyyy')
52
+ ) : (
53
+ format(field.value, 'dd/MM/yyyy')
54
+ )
55
+ ) : (
56
+ <span>Chọn ngày</span>
57
+ )}
58
+ </Button>
59
+ </PopoverTrigger>
60
+ <PopoverContent className="w-auto p-0" align="start">
61
+ <Calendar
62
+ mode="single"
63
+ captionLayout="dropdown"
64
+ selected={field.value ? new Date(field.value) : undefined}
65
+ onSelect={field.onChange}
66
+ fromYear={1900}
67
+ toYear={new Date().getFullYear() + 10}
68
+ />
69
+ </PopoverContent>
70
+ </Popover>
71
+ </FormControl>
72
+ <FormMessage />
73
+ </FormItem>
74
+ )}
75
+ />
76
+ );
77
+ }
@@ -0,0 +1,67 @@
1
+ import { zodResolver } from '@hookform/resolvers/zod';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { useForm } from 'react-hook-form';
4
+ import { z } from 'zod';
5
+
6
+ import { Button } from '@/components/ui/Button';
7
+ import { Input } from '@/components/ui/Input';
8
+
9
+ import {
10
+ Form,
11
+ FormControl,
12
+ FormDescription,
13
+ FormField,
14
+ FormItem,
15
+ FormLabel,
16
+ FormMessage,
17
+ } from './index';
18
+
19
+ const meta: Meta<typeof Form> = {
20
+ title: 'UI/Form',
21
+ component: Form,
22
+ tags: ['autodocs'],
23
+ };
24
+
25
+ export default meta;
26
+
27
+ const formSchema = z.object({
28
+ username: z.string().min(2, {
29
+ message: 'Username must be at least 2 characters.',
30
+ }),
31
+ });
32
+
33
+ export const Default: StoryObj = {
34
+ render: () => {
35
+ // eslint-disable-next-line react-hooks/rules-of-hooks
36
+ const form = useForm<z.infer<typeof formSchema>>({
37
+ resolver: zodResolver(formSchema),
38
+ defaultValues: {
39
+ username: '',
40
+ },
41
+ });
42
+
43
+ function onSubmit(values: z.infer<typeof formSchema>) {
44
+ alert(JSON.stringify(values, null, 2));
45
+ }
46
+
47
+ return (
48
+ <Form form={form} onSubmit={onSubmit} className="space-y-8">
49
+ <FormField
50
+ control={form.control}
51
+ name="username"
52
+ render={({ field }) => (
53
+ <FormItem>
54
+ <FormLabel>Username</FormLabel>
55
+ <FormControl>
56
+ <Input placeholder="shadcn" {...field} />
57
+ </FormControl>
58
+ <FormDescription>This is your public display name.</FormDescription>
59
+ <FormMessage />
60
+ </FormItem>
61
+ )}
62
+ />
63
+ <Button type="submit">Submit</Button>
64
+ </Form>
65
+ );
66
+ },
67
+ };
@@ -0,0 +1,188 @@
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
+ SubmitHandler,
10
+ useFormContext,
11
+ UseFormReturn,
12
+ useFormState,
13
+ type ControllerProps,
14
+ type FieldPath,
15
+ type FieldValues,
16
+ } from 'react-hook-form';
17
+
18
+ import { cn } from '@/lib/utils';
19
+ import { Label } from '@/components/ui/Label';
20
+
21
+ export interface FormProps<T extends FieldValues> {
22
+ form: UseFormReturn<T, unknown>;
23
+ onSubmit?: SubmitHandler<T>;
24
+ children: React.ReactNode;
25
+ id?: string;
26
+ className?: string;
27
+ }
28
+
29
+ const Form = <T extends FieldValues>({
30
+ form,
31
+ onSubmit,
32
+ children,
33
+ id = 'form',
34
+ className,
35
+ }: FormProps<T>) => {
36
+ return (
37
+ <FormProvider {...form}>
38
+ <form
39
+ id={id}
40
+ className={className}
41
+ onSubmit={form.handleSubmit(onSubmit as SubmitHandler<T>)}
42
+ noValidate
43
+ >
44
+ {children}
45
+ </form>
46
+ </FormProvider>
47
+ );
48
+ };
49
+
50
+ type FormFieldContextValue<
51
+ TFieldValues extends FieldValues = FieldValues,
52
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
53
+ > = {
54
+ name: TName;
55
+ };
56
+
57
+ const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);
58
+
59
+ const FormField = <
60
+ TFieldValues extends FieldValues = FieldValues,
61
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
62
+ >({
63
+ ...props
64
+ }: ControllerProps<TFieldValues, TName>) => {
65
+ return (
66
+ <FormFieldContext.Provider value={{ name: props.name }}>
67
+ <Controller {...props} />
68
+ </FormFieldContext.Provider>
69
+ );
70
+ };
71
+
72
+ const useFormField = () => {
73
+ const fieldContext = React.useContext(FormFieldContext);
74
+ const itemContext = React.useContext(FormItemContext);
75
+ const { getFieldState } = useFormContext();
76
+ const formState = useFormState({ name: fieldContext.name });
77
+ const fieldState = getFieldState(fieldContext.name, formState);
78
+
79
+ if (!fieldContext) {
80
+ throw new Error('useFormField should be used within <FormField>');
81
+ }
82
+
83
+ const { id } = itemContext;
84
+
85
+ return {
86
+ id,
87
+ name: fieldContext.name,
88
+ formItemId: `${id}-form-item`,
89
+ formDescriptionId: `${id}-form-item-description`,
90
+ formMessageId: `${id}-form-item-message`,
91
+ ...fieldState,
92
+ };
93
+ };
94
+
95
+ type FormItemContextValue = {
96
+ id: string;
97
+ };
98
+
99
+ const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);
100
+
101
+ function FormItem({ className, ...props }: React.ComponentProps<'div'>) {
102
+ const id = React.useId();
103
+
104
+ return (
105
+ <FormItemContext.Provider value={{ id }}>
106
+ <div data-slot="form-item" className={cn('grid gap-2', className)} {...props} />
107
+ </FormItemContext.Provider>
108
+ );
109
+ }
110
+
111
+ function FormLabel({
112
+ className,
113
+ isRequired,
114
+ children,
115
+ ...props
116
+ }: React.ComponentProps<typeof LabelPrimitive.Root> & { isRequired?: boolean }) {
117
+ const { error, formItemId } = useFormField();
118
+ return (
119
+ <Label
120
+ data-slot="form-label"
121
+ data-error={!!error}
122
+ className={cn('data-[error=true]:text-destructive', className)}
123
+ htmlFor={formItemId}
124
+ {...props}
125
+ >
126
+ {children}
127
+ {isRequired && <span className="text-destructive">*</span>}
128
+ </Label>
129
+ );
130
+ }
131
+
132
+ function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
133
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
134
+
135
+ return (
136
+ <Slot
137
+ data-slot="form-control"
138
+ id={formItemId}
139
+ aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
140
+ aria-invalid={!!error}
141
+ {...props}
142
+ />
143
+ );
144
+ }
145
+
146
+ function FormDescription({ className, ...props }: React.ComponentProps<'p'>) {
147
+ const { formDescriptionId } = useFormField();
148
+
149
+ return (
150
+ <p
151
+ data-slot="form-description"
152
+ id={formDescriptionId}
153
+ className={cn('text-muted-foreground text-sm', className)}
154
+ {...props}
155
+ />
156
+ );
157
+ }
158
+
159
+ function FormMessage({ className, ...props }: React.ComponentProps<'p'>) {
160
+ const { error, formMessageId } = useFormField();
161
+ const body = error ? String(error?.message ?? '') : props.children;
162
+
163
+ if (!body) {
164
+ return null;
165
+ }
166
+
167
+ return (
168
+ <p
169
+ data-slot="form-message"
170
+ id={formMessageId}
171
+ className={cn('text-destructive text-sm', className)}
172
+ {...props}
173
+ >
174
+ {body}
175
+ </p>
176
+ );
177
+ }
178
+
179
+ export {
180
+ useFormField,
181
+ Form,
182
+ FormItem,
183
+ FormLabel,
184
+ FormControl,
185
+ FormDescription,
186
+ FormMessage,
187
+ FormField,
188
+ };
@@ -0,0 +1,55 @@
1
+ import { ControllerProps, FieldPath, FieldValues } from 'react-hook-form';
2
+
3
+ import {
4
+ Select,
5
+ SelectContent,
6
+ SelectItem,
7
+ SelectTrigger,
8
+ SelectValue,
9
+ } from '@/components/ui/Select';
10
+
11
+ import { FormControl, FormField, FormItem, FormLabel, FormMessage } from './index';
12
+
13
+ export interface SelectFieldProps<
14
+ TFieldValues extends FieldValues = FieldValues,
15
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
16
+ > {
17
+ name: TName;
18
+ control: ControllerProps<TFieldValues, TName>['control'];
19
+ label?: string;
20
+ placeholder?: string;
21
+ options: { value: string; label: string }[];
22
+ isRequired?: boolean;
23
+ }
24
+
25
+ export function SelectField<
26
+ TFieldValues extends FieldValues = FieldValues,
27
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
28
+ >({ label, placeholder, options, isRequired, ...rest }: SelectFieldProps<TFieldValues, TName>) {
29
+ return (
30
+ <FormField
31
+ name={rest.name}
32
+ control={rest.control}
33
+ render={({ field }) => (
34
+ <FormItem>
35
+ {label && <FormLabel isRequired={isRequired}>{label}</FormLabel>}
36
+ <Select onValueChange={field.onChange} defaultValue={field.value}>
37
+ <FormControl>
38
+ <SelectTrigger>
39
+ <SelectValue placeholder={placeholder} />
40
+ </SelectTrigger>
41
+ </FormControl>
42
+ <SelectContent>
43
+ {options.map((o) => (
44
+ <SelectItem key={o.value} value={o.value}>
45
+ {o.label}
46
+ </SelectItem>
47
+ ))}
48
+ </SelectContent>
49
+ </Select>
50
+ <FormMessage />
51
+ </FormItem>
52
+ )}
53
+ />
54
+ );
55
+ }
@@ -0,0 +1,37 @@
1
+ import type * as React from 'react';
2
+ import { ControllerProps, FieldPath, FieldValues } from 'react-hook-form';
3
+
4
+ import { Textarea } from '@/components/ui/Textarea';
5
+
6
+ import { FormControl, FormField, FormItem, FormLabel, FormMessage } from './index';
7
+
8
+ export interface TextAreaFieldProps<
9
+ TFieldValues extends FieldValues = FieldValues,
10
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
11
+ > extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'name'> {
12
+ name: TName;
13
+ control: ControllerProps<TFieldValues, TName>['control'];
14
+ label?: string;
15
+ isRequired?: boolean;
16
+ }
17
+
18
+ export function TextAreaField<
19
+ TFieldValues extends FieldValues = FieldValues,
20
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
21
+ >({ label, isRequired, ...textareaProps }: TextAreaFieldProps<TFieldValues, TName>) {
22
+ return (
23
+ <FormField
24
+ name={textareaProps.name}
25
+ control={textareaProps.control}
26
+ render={({ field }) => (
27
+ <FormItem>
28
+ {label && <FormLabel isRequired={isRequired}>{label}</FormLabel>}
29
+ <FormControl>
30
+ <Textarea {...field} {...textareaProps} className="min-h-37.5" />
31
+ </FormControl>
32
+ <FormMessage />
33
+ </FormItem>
34
+ )}
35
+ />
36
+ );
37
+ }
@@ -0,0 +1,72 @@
1
+ import * as React from 'react';
2
+ import { Eye, EyeOff } from 'lucide-react';
3
+ import { ControllerProps, FieldPath, FieldValues } from 'react-hook-form';
4
+
5
+ import { Input } from '@/components/ui/Input';
6
+
7
+ import { FormField, FormItem, FormLabel, FormMessage } from './index';
8
+
9
+ export interface TextFieldProps<
10
+ TFieldValues extends FieldValues = FieldValues,
11
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
12
+ > extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name'> {
13
+ name: TName;
14
+ control: ControllerProps<TFieldValues, TName>['control'];
15
+ label?: string;
16
+ isPassword?: boolean;
17
+ isRequired?: boolean;
18
+ }
19
+
20
+ export function TextField<
21
+ TFieldValues extends FieldValues = FieldValues,
22
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
23
+ >({
24
+ label,
25
+ type = 'text',
26
+ isPassword,
27
+ isRequired,
28
+ ...inputProps
29
+ }: TextFieldProps<TFieldValues, TName>) {
30
+ const [show, setShow] = React.useState(false);
31
+ const toggle = () => setShow((prev) => !prev);
32
+ const isNum = type === 'number';
33
+ const renderedType = isPassword ? (show ? 'text' : 'password') : 'text';
34
+
35
+ return (
36
+ <FormField
37
+ name={inputProps.name}
38
+ control={inputProps.control}
39
+ render={({ field }) => (
40
+ <FormItem>
41
+ {label && <FormLabel isRequired={isRequired}>{label}</FormLabel>}
42
+ <div className="relative">
43
+ <FormItem>
44
+ <Input
45
+ {...field}
46
+ {...inputProps}
47
+ type={renderedType}
48
+ inputMode={isNum ? 'decimal' : undefined}
49
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
50
+ let val = e.target.value;
51
+ if (isNum) val = val.replace(/[^0-9]/g, '');
52
+ field.onChange(val);
53
+ }}
54
+ />
55
+ </FormItem>
56
+ {isPassword && (
57
+ <button
58
+ type="button"
59
+ tabIndex={-1}
60
+ onClick={toggle}
61
+ className="text-muted-foreground hover:text-foreground absolute top-1/2 right-3 -translate-y-1/2 cursor-pointer"
62
+ >
63
+ {show ? <EyeOff size={16} /> : <Eye size={16} />}
64
+ </button>
65
+ )}
66
+ </div>
67
+ <FormMessage />
68
+ </FormItem>
69
+ )}
70
+ />
71
+ );
72
+ }
@@ -0,0 +1,48 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+
3
+ import { HStack } from './index';
4
+
5
+ const meta: Meta<typeof HStack> = {
6
+ title: 'UI/Layout/HStack',
7
+ component: HStack,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ spacing: {
11
+ control: 'select',
12
+ options: [0, 2, 4, 6, 8, 12, 16, 20, 24, 32, 40, 48, 64, 'none'],
13
+ },
14
+ justify: {
15
+ control: 'select',
16
+ options: ['left', 'right', 'center', 'between', 'around', 'evenly'],
17
+ },
18
+ align: {
19
+ control: 'select',
20
+ options: ['default', 'center', 'start', 'end', 'baseline'],
21
+ },
22
+ noWrap: {
23
+ control: 'boolean',
24
+ },
25
+ },
26
+ };
27
+
28
+ export default meta;
29
+ type Story = StoryObj<typeof HStack>;
30
+
31
+ export const Default: Story = {
32
+ args: {
33
+ children: (
34
+ <>
35
+ <div className="bg-primary flex h-10 w-10 items-center justify-center rounded text-white">
36
+ 1
37
+ </div>
38
+ <div className="bg-primary flex h-10 w-10 items-center justify-center rounded text-white">
39
+ 2
40
+ </div>
41
+ <div className="bg-primary flex h-10 w-10 items-center justify-center rounded text-white">
42
+ 3
43
+ </div>
44
+ </>
45
+ ),
46
+ spacing: 16,
47
+ },
48
+ };
@@ -0,0 +1,73 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { cva, type VariantProps } from 'class-variance-authority';
5
+ import { motion, type HTMLMotionProps } from 'motion/react';
6
+
7
+ import { cn } from '@/lib/utils';
8
+
9
+ const hStackVariants = cva('flex flex-wrap items-center', {
10
+ variants: {
11
+ justify: {
12
+ left: 'justify-start',
13
+ right: 'justify-end',
14
+ center: 'justify-center',
15
+ between: 'justify-between',
16
+ around: 'justify-around',
17
+ evenly: 'justify-evenly',
18
+ },
19
+ align: {
20
+ default: 'items-stretch',
21
+ center: 'items-center',
22
+ start: 'items-start',
23
+ end: 'items-end',
24
+ baseline: 'items-baseline',
25
+ },
26
+ spacing: {
27
+ 0: 'gap-0',
28
+ 2: 'gap-0.5',
29
+ 4: 'gap-1',
30
+ 6: 'gap-1.5',
31
+ 8: 'gap-2',
32
+ 12: 'gap-3',
33
+ 16: 'gap-4',
34
+ 20: 'gap-5',
35
+ 24: 'gap-6',
36
+ 32: 'gap-8',
37
+ 40: 'gap-10',
38
+ 48: 'gap-12',
39
+ 64: 'gap-16',
40
+ none: 'gap-0',
41
+ },
42
+ noWrap: {
43
+ true: 'flex-nowrap',
44
+ },
45
+ },
46
+ defaultVariants: {
47
+ spacing: 16,
48
+ justify: 'left',
49
+ align: 'default',
50
+ },
51
+ });
52
+
53
+ export interface HStackProps extends VariantProps<typeof hStackVariants>, HTMLMotionProps<'div'> {
54
+ className?: string;
55
+ }
56
+
57
+ const HStack = React.forwardRef<HTMLDivElement, HStackProps>(
58
+ ({ className, noWrap, justify, align, spacing, children, ...motionProps }, ref) => {
59
+ return (
60
+ <motion.div
61
+ ref={ref}
62
+ className={cn(hStackVariants({ spacing, align, justify, noWrap }), className)}
63
+ {...motionProps}
64
+ >
65
+ {children}
66
+ </motion.div>
67
+ );
68
+ },
69
+ );
70
+
71
+ HStack.displayName = 'HStack';
72
+
73
+ export { HStack, hStackVariants };