@phsa.tec/design-system-react 0.1.0

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 (201) hide show
  1. package/.eslintrc.json +7 -0
  2. package/.storybook/main.ts +16 -0
  3. package/.storybook/preview.ts +15 -0
  4. package/README.md +36 -0
  5. package/components.json +21 -0
  6. package/jest.config.ts +25 -0
  7. package/next.config.ts +7 -0
  8. package/package.json +88 -0
  9. package/postcss.config.mjs +8 -0
  10. package/public/file.svg +1 -0
  11. package/public/globe.svg +1 -0
  12. package/public/next.svg +1 -0
  13. package/public/vercel.svg +1 -0
  14. package/public/window.svg +1 -0
  15. package/src/app/columns.tsx +178 -0
  16. package/src/app/favicon.ico +0 -0
  17. package/src/app/fonts/GeistMonoVF.woff +0 -0
  18. package/src/app/fonts/GeistVF.woff +0 -0
  19. package/src/app/globals.css +94 -0
  20. package/src/app/layout.tsx +35 -0
  21. package/src/app/page.tsx +7 -0
  22. package/src/components/actions/AlertDialog/AlertDialog.tsx +45 -0
  23. package/src/components/actions/AlertDialog/alert-dialog.stories.tsx +21 -0
  24. package/src/components/actions/AlertDialog/index.ts +1 -0
  25. package/src/components/actions/Button/Button.stories.ts +38 -0
  26. package/src/components/actions/Button/Button.tsx +23 -0
  27. package/src/components/actions/Button/index.ts +1 -0
  28. package/src/components/actions/Collapsible/index.ts +1 -0
  29. package/src/components/actions/Dialog/Dialog.stories.tsx +70 -0
  30. package/src/components/actions/Dialog/Dialog.tsx +87 -0
  31. package/src/components/actions/Dialog/components/DialogWithActions/index.tsx +40 -0
  32. package/src/components/actions/Dialog/index.ts +1 -0
  33. package/src/components/actions/Steps/Steps.stories.tsx +25 -0
  34. package/src/components/actions/Steps/Steps.tsx +51 -0
  35. package/src/components/actions/Steps/index.ts +1 -0
  36. package/src/components/actions/index.ts +5 -0
  37. package/src/components/dataDisplay/Avatar/Avatar.stories.tsx +22 -0
  38. package/src/components/dataDisplay/Avatar/Avatar.tsx +21 -0
  39. package/src/components/dataDisplay/Avatar/index.ts +2 -0
  40. package/src/components/dataDisplay/Badge/Badge.stories.tsx +36 -0
  41. package/src/components/dataDisplay/Badge/index.ts +1 -0
  42. package/src/components/dataDisplay/Card/Card.stories.tsx +24 -0
  43. package/src/components/dataDisplay/Card/Card.tsx +34 -0
  44. package/src/components/dataDisplay/Card/index.ts +1 -0
  45. package/src/components/dataDisplay/DataPairList/DataPairList.tsx +56 -0
  46. package/src/components/dataDisplay/DataPairList/data-pair-list.stories.tsx +87 -0
  47. package/src/components/dataDisplay/DataPairList/index.ts +2 -0
  48. package/src/components/dataDisplay/DataPairList/types.ts +10 -0
  49. package/src/components/dataDisplay/DropDownMenu/index.ts +1 -0
  50. package/src/components/dataDisplay/ErrorMessage/ErrorMessage.tsx +6 -0
  51. package/src/components/dataDisplay/ErrorMessage/index.ts +1 -0
  52. package/src/components/dataDisplay/Icon/Icon.stories.tsx +21 -0
  53. package/src/components/dataDisplay/Icon/Icon.tsx +47 -0
  54. package/src/components/dataDisplay/Icon/index.ts +1 -0
  55. package/src/components/dataDisplay/Icon/types.ts +6 -0
  56. package/src/components/dataDisplay/Label/Label.stories.tsx +21 -0
  57. package/src/components/dataDisplay/Label/Label.tsx +10 -0
  58. package/src/components/dataDisplay/Label/index.ts +1 -0
  59. package/src/components/dataDisplay/Table/Table.tsx +173 -0
  60. package/src/components/dataDisplay/Table/columns.tsx +223 -0
  61. package/src/components/dataDisplay/Table/components/DynamicTable/data-table-column-header.tsx +72 -0
  62. package/src/components/dataDisplay/Table/components/DynamicTable/data-table-pagination.tsx +91 -0
  63. package/src/components/dataDisplay/Table/components/DynamicTable/data-table-toolbar.tsx +17 -0
  64. package/src/components/dataDisplay/Table/components/DynamicTable/data-table-view-options.tsx +58 -0
  65. package/src/components/dataDisplay/Table/components/DynamicTable/data-table.stories.tsx +118 -0
  66. package/src/components/dataDisplay/Table/components/DynamicTable/index.tsx +136 -0
  67. package/src/components/dataDisplay/Table/components/DynamicTable/types.ts +43 -0
  68. package/src/components/dataDisplay/Table/custom/CustomTable/data-table-column-header.tsx +71 -0
  69. package/src/components/dataDisplay/Table/custom/CustomTable/data-table-faceted-filter.tsx +147 -0
  70. package/src/components/dataDisplay/Table/custom/CustomTable/data-table-pagination.tsx +97 -0
  71. package/src/components/dataDisplay/Table/custom/CustomTable/data-table-row-actions.tsx +78 -0
  72. package/src/components/dataDisplay/Table/custom/CustomTable/data-table-toolbar.tsx +60 -0
  73. package/src/components/dataDisplay/Table/custom/CustomTable/data-table-view-options.tsx +59 -0
  74. package/src/components/dataDisplay/Table/custom/CustomTable/data-table.tsx +145 -0
  75. package/src/components/dataDisplay/Table/custom/CustomTable/data.ts +71 -0
  76. package/src/components/dataDisplay/Table/custom/CustomTable/index.tsx +34 -0
  77. package/src/components/dataDisplay/Table/custom/CustomTable/schema.ts +11 -0
  78. package/src/components/dataDisplay/Table/index.ts +2 -0
  79. package/src/components/dataDisplay/Table/table.stories.tsx +147 -0
  80. package/src/components/dataDisplay/Table/types.ts +15 -0
  81. package/src/components/dataDisplay/Tabs/Tabs.stories.tsx +34 -0
  82. package/src/components/dataDisplay/Tabs/Tabs.tsx +53 -0
  83. package/src/components/dataDisplay/Tabs/index.ts +1 -0
  84. package/src/components/dataDisplay/Text/Text.stories.tsx +66 -0
  85. package/src/components/dataDisplay/Text/Text.tsx +56 -0
  86. package/src/components/dataDisplay/Text/index.ts +1 -0
  87. package/src/components/dataDisplay/index.ts +8 -0
  88. package/src/components/dataInput/Input/components/Input/Input.stories.tsx +99 -0
  89. package/src/components/dataInput/Input/components/Input/InputBase.tsx +50 -0
  90. package/src/components/dataInput/Input/components/Input/__tests__/Input.test.tsx +100 -0
  91. package/src/components/dataInput/Input/components/Input/index.tsx +257 -0
  92. package/src/components/dataInput/Input/components/InputBase/__tests__/InputBase.test.tsx +120 -0
  93. package/src/components/dataInput/Input/components/InputBase/index.tsx +89 -0
  94. package/src/components/dataInput/Input/components/MaskInput/__tests__/mask-input.test.tsx +67 -0
  95. package/src/components/dataInput/Input/components/MaskInput/index.ts +1 -0
  96. package/src/components/dataInput/Input/components/MaskInput/mask-input.stories.tsx +59 -0
  97. package/src/components/dataInput/Input/components/MaskInput/mask-input.tsx +43 -0
  98. package/src/components/dataInput/Input/components/MultipleInput/MultipleInput.tsx +36 -0
  99. package/src/components/dataInput/Input/components/MultipleInput/MultipleInputBase.tsx +100 -0
  100. package/src/components/dataInput/Input/components/MultipleInput/MultipleMaskInput.tsx +35 -0
  101. package/src/components/dataInput/Input/components/MultipleInput/MultipleNumberInput.tsx +35 -0
  102. package/src/components/dataInput/Input/components/MultipleInput/index.ts +2 -0
  103. package/src/components/dataInput/Input/components/MultipleInput/multiple-input.stories.tsx +71 -0
  104. package/src/components/dataInput/Input/components/NumberInput/__tests__/number-input.test.tsx +95 -0
  105. package/src/components/dataInput/Input/components/NumberInput/index.ts +1 -0
  106. package/src/components/dataInput/Input/components/NumberInput/number-input.stories.tsx +76 -0
  107. package/src/components/dataInput/Input/components/NumberInput/number-input.tsx +68 -0
  108. package/src/components/dataInput/Input/index.ts +4 -0
  109. package/src/components/dataInput/Select/MultiSelect/MultiSelect.stories.tsx +119 -0
  110. package/src/components/dataInput/Select/MultiSelect/MultiSelectBase.tsx +135 -0
  111. package/src/components/dataInput/Select/MultiSelect/index.tsx +75 -0
  112. package/src/components/dataInput/Select/Select.stories.tsx +61 -0
  113. package/src/components/dataInput/Select/Select.tsx +73 -0
  114. package/src/components/dataInput/Select/SelectBase.tsx +58 -0
  115. package/src/components/dataInput/Select/index.ts +2 -0
  116. package/src/components/dataInput/Switch/Switch.stories.tsx +75 -0
  117. package/src/components/dataInput/Switch/Switch.tsx +52 -0
  118. package/src/components/dataInput/Switch/index.ts +1 -0
  119. package/src/components/dataInput/checkbox/Checkbox.tsx +57 -0
  120. package/src/components/dataInput/checkbox/Checkbox_old.tsx +58 -0
  121. package/src/components/dataInput/checkbox/Checkout.stories.tsx +62 -0
  122. package/src/components/dataInput/checkbox/index.ts +1 -0
  123. package/src/components/dataInput/form/Form.tsx +47 -0
  124. package/src/components/dataInput/form/index.ts +3 -0
  125. package/src/components/dataInput/index.ts +5 -0
  126. package/src/components/feedback/Spinner/index.ts +1 -0
  127. package/src/components/feedback/Toast/Toast.stories.tsx +45 -0
  128. package/src/components/feedback/Toast/index.ts +2 -0
  129. package/src/components/feedback/index.ts +2 -0
  130. package/src/components/index.ts +6 -0
  131. package/src/components/layout/Crud/components/Table/index.tsx +183 -0
  132. package/src/components/layout/Crud/components/Table/types.ts +15 -0
  133. package/src/components/layout/Crud/crud.stories.tsx +317 -0
  134. package/src/components/layout/Crud/hook/useCrudLayout/index.tsx +94 -0
  135. package/src/components/layout/Crud/hook/useRequest/index.tsx +156 -0
  136. package/src/components/layout/Crud/index.tsx +295 -0
  137. package/src/components/layout/Crud/store/CrudLayoutStore.ts +75 -0
  138. package/src/components/layout/Crud/types.ts +14 -0
  139. package/src/components/layout/Drawer/CustomDrawer/index.tsx +33 -0
  140. package/src/components/layout/Drawer/Drawer.stories.tsx +80 -0
  141. package/src/components/layout/Drawer/index.ts +2 -0
  142. package/src/components/layout/PageLayout/PageLayout.stories.tsx +42 -0
  143. package/src/components/layout/PageLayout/index.tsx +28 -0
  144. package/src/components/layout/Separator/index.ts +1 -0
  145. package/src/components/layout/Sheet/Sheet.stories.tsx +28 -0
  146. package/src/components/layout/Sheet/Sheet.tsx +22 -0
  147. package/src/components/layout/Sheet/index.ts +1 -0
  148. package/src/components/layout/Sidebar/Sidebar.stories.tsx +116 -0
  149. package/src/components/layout/Sidebar/Sidebar.tsx +50 -0
  150. package/src/components/layout/Sidebar/components/app-sidebar.tsx +203 -0
  151. package/src/components/layout/Sidebar/components/footer-sidebar.tsx +17 -0
  152. package/src/components/layout/Sidebar/components/header-sidebar.tsx +90 -0
  153. package/src/components/layout/Sidebar/components/menus.tsx +55 -0
  154. package/src/components/layout/Sidebar/components/nav-projects.tsx +88 -0
  155. package/src/components/layout/Sidebar/components/nav-user.tsx +114 -0
  156. package/src/components/layout/Sidebar/components/team-switcher.tsx +85 -0
  157. package/src/components/layout/Sidebar/index.ts +2 -0
  158. package/src/components/layout/Sidebar/provider/index.tsx +51 -0
  159. package/src/components/layout/Tabs/Tabs.tsx +51 -0
  160. package/src/components/layout/Tabs/index.ts +1 -0
  161. package/src/components/layout/Tabs/tabs.stories.tsx +57 -0
  162. package/src/components/layout/index.ts +6 -0
  163. package/src/components/navigation/Breadcrumbs/Breadcrumbs.tsx +66 -0
  164. package/src/components/navigation/Breadcrumbs/index.ts +2 -0
  165. package/src/components/navigation/index.ts +1 -0
  166. package/src/components/ui/alert-dialog.tsx +141 -0
  167. package/src/components/ui/alert.tsx +59 -0
  168. package/src/components/ui/avatar.tsx +50 -0
  169. package/src/components/ui/badge.tsx +40 -0
  170. package/src/components/ui/breadcrumb.tsx +115 -0
  171. package/src/components/ui/button.tsx +57 -0
  172. package/src/components/ui/card.tsx +83 -0
  173. package/src/components/ui/checkbox.tsx +34 -0
  174. package/src/components/ui/collapsible.tsx +11 -0
  175. package/src/components/ui/command.tsx +153 -0
  176. package/src/components/ui/dialog.tsx +124 -0
  177. package/src/components/ui/drawer.tsx +117 -0
  178. package/src/components/ui/dropdown-menu.tsx +201 -0
  179. package/src/components/ui/form.tsx +179 -0
  180. package/src/components/ui/input.tsx +24 -0
  181. package/src/components/ui/label.tsx +30 -0
  182. package/src/components/ui/popover.tsx +33 -0
  183. package/src/components/ui/select.tsx +161 -0
  184. package/src/components/ui/separator.tsx +31 -0
  185. package/src/components/ui/sheet.tsx +140 -0
  186. package/src/components/ui/sidebar.tsx +763 -0
  187. package/src/components/ui/skeleton.tsx +15 -0
  188. package/src/components/ui/sonner.tsx +31 -0
  189. package/src/components/ui/spinner.tsx +54 -0
  190. package/src/components/ui/switch.tsx +33 -0
  191. package/src/components/ui/table.tsx +120 -0
  192. package/src/components/ui/tabs.tsx +55 -0
  193. package/src/components/ui/toast.tsx +130 -0
  194. package/src/components/ui/toaster.tsx +35 -0
  195. package/src/components/ui/tooltip.tsx +32 -0
  196. package/src/hooks/use-mobile.tsx +19 -0
  197. package/src/hooks/use-toast.ts +191 -0
  198. package/src/index.ts +1 -0
  199. package/src/lib/utils.ts +6 -0
  200. package/tailwind.config.ts +83 -0
  201. package/tsconfig.json +27 -0
@@ -0,0 +1,120 @@
1
+ import "@testing-library/jest-dom";
2
+ import React from "react";
3
+ import { render, screen } from "@testing-library/react";
4
+ import { InputBase, CustomInputProps } from "..";
5
+ import { Form } from "../../../../../../components/ui/form";
6
+ import { useForm } from "react-hook-form";
7
+
8
+ describe("InputBase", () => {
9
+ const MockInput = (props: CustomInputProps) => (
10
+ <input
11
+ data-testid="mock-input"
12
+ {...props}
13
+ onChange={(e) => props.onChange?.(e.target.value)}
14
+ />
15
+ );
16
+
17
+ it("deve renderizar sem contexto de formulário", () => {
18
+ render(<InputBase data-testid="test">{() => <MockInput />}</InputBase>);
19
+ expect(screen.getByTestId("mock-input")).toBeInTheDocument();
20
+ });
21
+
22
+ it("deve renderizar label e descrição", () => {
23
+ render(
24
+ <InputBase
25
+ label="Test Label"
26
+ description="Test Description"
27
+ data-testid="test"
28
+ >
29
+ {() => <MockInput />}
30
+ </InputBase>
31
+ );
32
+
33
+ expect(screen.getByText("Test Label")).toBeInTheDocument();
34
+ expect(screen.getByText("Test Description")).toBeInTheDocument();
35
+ });
36
+
37
+ it("deve mostrar indicador de obrigatório quando required é true", () => {
38
+ render(
39
+ <InputBase label="Test Label" required data-testid="test">
40
+ {() => <MockInput />}
41
+ </InputBase>
42
+ );
43
+
44
+ expect(screen.getByText("Test Label *")).toBeInTheDocument();
45
+ });
46
+
47
+ it("deve renderizar mensagem de erro", () => {
48
+ render(
49
+ <InputBase error="Test Error" data-testid="test">
50
+ {() => <MockInput />}
51
+ </InputBase>
52
+ );
53
+
54
+ expect(screen.getByText("Test Error")).toBeInTheDocument();
55
+ });
56
+
57
+ it("deve funcionar com contexto de formulário", () => {
58
+ const TestForm = () => {
59
+ const form = useForm();
60
+ return (
61
+ <Form {...form}>
62
+ <InputBase name="test" data-testid="test">
63
+ {() => <MockInput />}
64
+ </InputBase>
65
+ </Form>
66
+ );
67
+ };
68
+
69
+ render(<TestForm />);
70
+ expect(screen.getByTestId("mock-input")).toBeInTheDocument();
71
+ });
72
+
73
+ it("deve aplicar className customizada", () => {
74
+ render(
75
+ <InputBase className="custom-class" data-testid="test">
76
+ {() => <MockInput />}
77
+ </InputBase>
78
+ );
79
+
80
+ const container = screen.getByTestId("input-base-test");
81
+ expect(container).toHaveClass("custom-class");
82
+ });
83
+
84
+ it("deve lidar com estado disabled", () => {
85
+ render(
86
+ <InputBase disabled data-testid="mock-input">
87
+ {(props) => <MockInput {...props} />}
88
+ </InputBase>
89
+ );
90
+
91
+ expect(screen.getByTestId("mock-input")).toBeDisabled();
92
+ });
93
+
94
+ it("deve validar erros do React Hook Form", async () => {
95
+ const TestForm = () => {
96
+ const form = useForm({
97
+ defaultValues: {
98
+ test: "",
99
+ },
100
+ });
101
+
102
+ React.useEffect(() => {
103
+ form.setError("test", { message: "Test Error" });
104
+ }, [form]);
105
+
106
+ return (
107
+ <Form {...form}>
108
+ <InputBase name="test" data-testid="mock-input" required>
109
+ {(props) => <MockInput {...props} />}
110
+ </InputBase>
111
+ </Form>
112
+ );
113
+ };
114
+
115
+ render(<TestForm />);
116
+
117
+ const errorMessage = await screen.findByText("Test Error");
118
+ expect(errorMessage).toBeInTheDocument();
119
+ });
120
+ });
@@ -0,0 +1,89 @@
1
+ import { useFormContext } from "react-hook-form";
2
+ import { InputProps as InputPropsUI } from "../../../../ui/input";
3
+ import { Label } from "../../../../ui/label";
4
+ import {
5
+ FormControl,
6
+ FormField,
7
+ FormItem,
8
+ FormLabel,
9
+ FormMessage,
10
+ } from "../../../../ui/form";
11
+
12
+ export type InputBaseProps = Omit<InputPropsUI, "children"> & {
13
+ label?: string;
14
+ withoutForm?: boolean;
15
+ "data-testid"?: string;
16
+ error?: string;
17
+ children?: (
18
+ props: React.DetailedHTMLProps<
19
+ React.InputHTMLAttributes<HTMLInputElement>,
20
+ HTMLInputElement
21
+ >
22
+ ) => JSX.Element;
23
+ };
24
+
25
+ export const InputBase = ({
26
+ label,
27
+ withoutForm,
28
+ className,
29
+ name,
30
+ required,
31
+ "data-testid": testId,
32
+ error,
33
+ children,
34
+ ...props
35
+ }: InputBaseProps) => {
36
+ const form = useFormContext();
37
+ const hasForm = !withoutForm && !!form && !!name;
38
+
39
+ if (!hasForm)
40
+ return (
41
+ <div className="grid w-full items-center gap-3">
42
+ <Label htmlFor="email">
43
+ {label}
44
+ {required && <span>*</span>}
45
+ </Label>
46
+ {children?.({
47
+ ...props,
48
+ className,
49
+ name,
50
+ required,
51
+ })}
52
+ {error && <p className="text-red-500">{error}</p>}
53
+ </div>
54
+ );
55
+
56
+ return (
57
+ <FormField
58
+ control={form.control}
59
+ name={name}
60
+ render={({ field }) => (
61
+ <FormItem
62
+ className={className}
63
+ data-testid={testId ? `form-item-${testId}` : undefined}
64
+ >
65
+ {label && (
66
+ <FormLabel
67
+ htmlFor={name}
68
+ data-testid={testId ? `form-label-${testId}` : undefined}
69
+ >
70
+ {`${label}${required ? " *" : ""}`}
71
+ </FormLabel>
72
+ )}
73
+ <FormControl>
74
+ {children?.({
75
+ className,
76
+ required,
77
+ ...props,
78
+ ...field,
79
+ })}
80
+ </FormControl>
81
+ <FormMessage
82
+ role="alert"
83
+ data-testid={testId ? `form-message-${testId}` : undefined}
84
+ />
85
+ </FormItem>
86
+ )}
87
+ />
88
+ );
89
+ };
@@ -0,0 +1,67 @@
1
+ import "@testing-library/jest-dom";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { MaskInput } from "../mask-input";
5
+
6
+ describe("MaskInput", () => {
7
+ const defaultProps = {
8
+ options: {
9
+ mask: "000.000.000-00",
10
+ },
11
+ name: "cpf",
12
+ label: "CPF",
13
+ };
14
+
15
+ it("should render correctly", () => {
16
+ render(<MaskInput {...defaultProps} />);
17
+
18
+ expect(screen.getByTestId("input-base-cpf")).toBeInTheDocument();
19
+ expect(screen.getByTestId("input-wrapper-cpf")).toBeInTheDocument();
20
+ expect(screen.getByTestId("input-cpf")).toBeInTheDocument();
21
+ expect(screen.getByText("CPF")).toBeInTheDocument();
22
+ });
23
+
24
+ it("should apply mask when typing", async () => {
25
+ const user = userEvent.setup();
26
+ render(<MaskInput {...defaultProps} />);
27
+
28
+ const input = screen.getByTestId("input-cpf");
29
+ await user.type(input, "12345678900");
30
+
31
+ expect(input).toHaveValue("123.456.789-00");
32
+ });
33
+
34
+ it("should call onChange with masked value", async () => {
35
+ const onChange = jest.fn();
36
+ const user = userEvent.setup();
37
+ render(<MaskInput {...defaultProps} onChange={onChange} />);
38
+
39
+ const input = screen.getByTestId("input-cpf");
40
+ await user.type(input, "12345678900");
41
+
42
+ expect(onChange).toHaveBeenCalled();
43
+ });
44
+
45
+ it("should render with error state", () => {
46
+ render(<MaskInput {...defaultProps} error="Invalid CPF" />);
47
+
48
+ expect(screen.getByText("Invalid CPF")).toBeInTheDocument();
49
+ });
50
+
51
+ it("should render with description", () => {
52
+ render(<MaskInput {...defaultProps} description="Enter your CPF number" />);
53
+
54
+ expect(screen.getByText("Enter your CPF number")).toBeInTheDocument();
55
+ });
56
+
57
+ it("should render with custom component", () => {
58
+ render(
59
+ <MaskInput
60
+ {...defaultProps}
61
+ component={<span data-testid="custom-component">Custom</span>}
62
+ />
63
+ );
64
+
65
+ expect(screen.getByTestId("custom-component")).toBeInTheDocument();
66
+ });
67
+ });
@@ -0,0 +1 @@
1
+ export * from "./mask-input";
@@ -0,0 +1,59 @@
1
+ import type { Meta, StoryObj } from "@storybook/nextjs";
2
+ import { MaskInput } from "./mask-input";
3
+ import { useForm } from "react-hook-form";
4
+ import { Form } from "../../../../../components/ui/form";
5
+
6
+ const meta = {
7
+ title: "Data Input/Input/MaskInput",
8
+ component: MaskInput,
9
+ parameters: {
10
+ layout: "centered",
11
+ },
12
+ tags: ["autodocs"],
13
+ } satisfies Meta<typeof MaskInput>;
14
+
15
+ export default meta;
16
+ type Story = StoryObj<typeof meta>;
17
+
18
+ export const Default: Story = {
19
+ args: {
20
+ placeholder: "000.000.000-00",
21
+ label: "CPF",
22
+ mask: "999.999.999-99",
23
+ },
24
+ };
25
+
26
+ export const Phone: Story = {
27
+ args: {
28
+ placeholder: "(99) 99999-9999",
29
+ label: "Telefone",
30
+ },
31
+ };
32
+
33
+ export const Date: Story = {
34
+ args: {
35
+ placeholder: "99/99/9999",
36
+ label: "Data",
37
+ },
38
+ };
39
+
40
+ export const WithForm = () => {
41
+ const form = useForm({
42
+ defaultValues: {
43
+ cpf: "999.999.999-99",
44
+ },
45
+ });
46
+
47
+ return (
48
+ <Form {...form}>
49
+ <form className="space-y-6">
50
+ <MaskInput
51
+ name="cpf"
52
+ label="CPF"
53
+ placeholder="000.000.000-00"
54
+ mask="999.999.999-99"
55
+ />
56
+ </form>
57
+ </Form>
58
+ );
59
+ };
@@ -0,0 +1,43 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import { Input } from "../../../../ui/input";
4
+ import { InputBase, InputBaseProps } from "../InputBase";
5
+
6
+ export type MaskInputProps = Omit<InputBaseProps, "children"> & {
7
+ placeholder?: string;
8
+ className?: string;
9
+ withoutForm?: boolean;
10
+ component?: React.ReactNode;
11
+ "data-testid"?: string;
12
+ onChange?: (value: string) => void;
13
+ };
14
+
15
+ export const MaskInput = ({
16
+ component,
17
+ "data-testid": testId,
18
+ label,
19
+ name,
20
+ withoutForm,
21
+ ...props
22
+ }: MaskInputProps) => {
23
+ return (
24
+ <InputBase
25
+ label={label}
26
+ data-testid={testId}
27
+ withoutForm={withoutForm}
28
+ name={name}
29
+ >
30
+ {({ ...rest }) => {
31
+ return (
32
+ <div
33
+ className="flex w-full gap-3"
34
+ data-testid={`input-wrapper-${testId}`}
35
+ >
36
+ <Input {...props} {...rest} />
37
+ {component}
38
+ </div>
39
+ );
40
+ }}
41
+ </InputBase>
42
+ );
43
+ };
@@ -0,0 +1,36 @@
1
+ import { Button } from "../../../../actions/Button";
2
+ import { Input, InputProps } from "../Input";
3
+ import { MultipleInputBase } from "./MultipleInputBase";
4
+ import { Icon } from "../../../../dataDisplay/Icon";
5
+
6
+ export type MultipleInputProps = InputProps & {
7
+ data?: string[];
8
+ onChangeData?: (data: string[]) => void;
9
+ name: string;
10
+ };
11
+
12
+ export const MultipleInput = ({
13
+ data = [],
14
+ mask,
15
+ ...props
16
+ }: MultipleInputProps) => {
17
+ return (
18
+ <MultipleInputBase data={data} {...props}>
19
+ {({ onChange, addItem, value, error }) => (
20
+ <Input
21
+ {...props}
22
+ mask={mask}
23
+ value={value}
24
+ onChange={onChange}
25
+ withoutForm
26
+ error={error}
27
+ component={
28
+ <Button type="button" onClick={() => addItem()}>
29
+ <Icon name="MdAdd" />
30
+ </Button>
31
+ }
32
+ />
33
+ )}
34
+ </MultipleInputBase>
35
+ );
36
+ };
@@ -0,0 +1,100 @@
1
+ import { Button } from "../../../../../components/actions";
2
+ import { Icon } from "../../../../../components/dataDisplay";
3
+ import { useCallback, useMemo, useState } from "react";
4
+ import { useFormContext } from "react-hook-form";
5
+ import _ from "lodash";
6
+
7
+ export type MultipleInputBaseProps = {
8
+ children: (params: {
9
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
10
+ addItem: () => void;
11
+ value: string;
12
+ error?: string;
13
+ }) => React.ReactNode;
14
+ data: string[];
15
+ name?: string;
16
+ onChangeData?: (data: string[]) => void;
17
+ error?: string;
18
+ };
19
+
20
+ export const MultipleInputBase = ({
21
+ children,
22
+ data = [],
23
+ name,
24
+ onChangeData = () => {},
25
+ error,
26
+ }: MultipleInputBaseProps) => {
27
+ const form = useFormContext();
28
+ const withForm = !!form && !!name;
29
+ const [inputValue, setInputValue] = useState("");
30
+
31
+ const inputData = useMemo(() => {
32
+ if (withForm) {
33
+ return _.get(form.watch(), name) || [];
34
+ }
35
+ return data;
36
+ }, [withForm, form, name, data]);
37
+
38
+ const errorMessage = useMemo(() => {
39
+ if (withForm) {
40
+ return _.get(form.formState.errors, name)?.message as string;
41
+ }
42
+ return error;
43
+ }, [error, form, name, withForm]);
44
+
45
+ const updateData = useCallback(() => {
46
+ if (!inputValue.length) return;
47
+ if (withForm) {
48
+ form.setValue(name, [...inputData, inputValue]);
49
+ } else {
50
+ onChangeData?.(inputData.concat(inputValue));
51
+ }
52
+ setInputValue("");
53
+ }, [inputData, inputValue, withForm, form, name, onChangeData]);
54
+
55
+ const removeItem = useCallback(
56
+ (index: number) => {
57
+ if (withForm) {
58
+ form.setValue(
59
+ name,
60
+ inputData.filter((_: string, i: number) => i !== index)
61
+ );
62
+ } else {
63
+ onChangeData?.(inputData.filter((_: string, i: number) => i !== index));
64
+ }
65
+ },
66
+ [withForm, form, name, inputData, onChangeData]
67
+ );
68
+
69
+ const renderOptions = useCallback(() => {
70
+ return inputData?.map((item: string, index: number) => {
71
+ return (
72
+ <div key={index} className="flex justify-between mt-2">
73
+ <div>{item}</div>
74
+ <Button
75
+ onClick={() => removeItem(index)}
76
+ variant={"ghost"}
77
+ size={"icon"}
78
+ type="button"
79
+ >
80
+ <Icon name="MdDelete" className="fill-destructive" />
81
+ </Button>
82
+ </div>
83
+ );
84
+ });
85
+ }, [inputData, removeItem]);
86
+
87
+ return (
88
+ <div>
89
+ {children({
90
+ onChange: ({ target: { value } }) => {
91
+ setInputValue(value);
92
+ },
93
+ addItem: updateData,
94
+ value: inputValue,
95
+ error: errorMessage,
96
+ })}
97
+ {renderOptions()}
98
+ </div>
99
+ );
100
+ };
@@ -0,0 +1,35 @@
1
+ import { Button } from "../../../../../components/actions";
2
+ import { MaskInput, MaskInputProps } from "../MaskInput";
3
+ import { MultipleInputBase } from "./MultipleInputBase";
4
+ import { Icon } from "../../../../../components/dataDisplay";
5
+
6
+ export type MultipleMaskInputProps = MaskInputProps & {
7
+ data?: string[];
8
+ onChangeData?: (data: string[]) => void;
9
+ name: string;
10
+ };
11
+
12
+ export const MultipleMaskInput = ({
13
+ data = [],
14
+ ...props
15
+ }: MultipleMaskInputProps) => {
16
+ return (
17
+ <MultipleInputBase data={data} {...props}>
18
+ {({ onChange, addItem, value }) => {
19
+ return (
20
+ <MaskInput
21
+ {...props}
22
+ value={value}
23
+ onChange={onChange}
24
+ withoutForm
25
+ component={
26
+ <Button type="button" onClick={() => addItem()}>
27
+ <Icon name="MdAdd" />
28
+ </Button>
29
+ }
30
+ />
31
+ );
32
+ }}
33
+ </MultipleInputBase>
34
+ );
35
+ };
@@ -0,0 +1,35 @@
1
+ import { Button } from "../../../../actions/Button";
2
+ import { Input, InputProps } from "../Input";
3
+ import { MultipleInputBase } from "./MultipleInputBase";
4
+ import { Icon } from "../../../../dataDisplay/Icon";
5
+
6
+ export type MultipleNumberInputProps = InputProps & {
7
+ data: string[];
8
+ onChangeData: (data: string[]) => void;
9
+ name: string;
10
+ };
11
+
12
+ export const MultipleNumberInput = ({
13
+ data,
14
+ ...props
15
+ }: MultipleNumberInputProps) => {
16
+ return (
17
+ <MultipleInputBase data={data} {...props}>
18
+ {({ onChange, addItem, value }) => (
19
+ <Input
20
+ {...props}
21
+ value={value}
22
+ onChange={(e) => {
23
+ onChange(e.target.value);
24
+ }}
25
+ withoutForm
26
+ component={
27
+ <Button onClick={() => addItem()}>
28
+ <Icon name="MdAdd" />
29
+ </Button>
30
+ }
31
+ />
32
+ )}
33
+ </MultipleInputBase>
34
+ );
35
+ };
@@ -0,0 +1,2 @@
1
+ export * from "./MultipleInput";
2
+ export * from "./MultipleMaskInput";
@@ -0,0 +1,71 @@
1
+ import type { Meta, StoryObj } from "@storybook/nextjs";
2
+ import { MultipleInput } from "./MultipleInput";
3
+ import { Form } from "@/components/ui/form";
4
+ import { useForm } from "react-hook-form";
5
+
6
+ const meta: Meta<typeof MultipleInput> = {
7
+ title: "Data Input/Input/Multiple Input",
8
+ component: MultipleInput,
9
+ tags: ["autodocs"],
10
+ parameters: {
11
+ layout: "centered",
12
+ },
13
+ };
14
+
15
+ export default meta;
16
+ type Story = StoryObj<typeof MultipleInput>;
17
+
18
+ export const Default: Story = {
19
+ args: {
20
+ label: "Multiple Input",
21
+ name: "multipleInput",
22
+ data: ["Item 1", "Item 2", "Item 3"],
23
+ placeholder: "Multiple Input",
24
+ onChangeData: (data: string[]) => {
25
+ console.log(data);
26
+ },
27
+ },
28
+ };
29
+
30
+ export const WithError: Story = {
31
+ args: {
32
+ label: "Multiple Input",
33
+ name: "multipleInput",
34
+ error: "This is an error",
35
+ data: ["Item 1", "Item 2", "Item 3"],
36
+ placeholder: "Multiple Input",
37
+ },
38
+ };
39
+
40
+ export const WithForm: Story = {
41
+ args: {
42
+ label: "Multiple Input",
43
+ name: "client.data",
44
+ placeholder: "Multiple Input",
45
+ mask: "999.999.999-99",
46
+ },
47
+ decorators: [
48
+ (Story: React.ComponentType) => {
49
+ const form = useForm({
50
+ defaultValues: {
51
+ client: {
52
+ data: ["Item 1", "Item 2", "Item 3"],
53
+ },
54
+ },
55
+ errors: {
56
+ client: {
57
+ data: {
58
+ message: "This is an error",
59
+ },
60
+ },
61
+ },
62
+ });
63
+
64
+ return (
65
+ <Form {...form}>
66
+ <Story />
67
+ </Form>
68
+ );
69
+ },
70
+ ],
71
+ };