@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.
- package/.eslintrc.json +7 -0
- package/.storybook/main.ts +16 -0
- package/.storybook/preview.ts +15 -0
- package/README.md +36 -0
- package/components.json +21 -0
- package/jest.config.ts +25 -0
- package/next.config.ts +7 -0
- package/package.json +88 -0
- package/postcss.config.mjs +8 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/columns.tsx +178 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/fonts/GeistMonoVF.woff +0 -0
- package/src/app/fonts/GeistVF.woff +0 -0
- package/src/app/globals.css +94 -0
- package/src/app/layout.tsx +35 -0
- package/src/app/page.tsx +7 -0
- package/src/components/actions/AlertDialog/AlertDialog.tsx +45 -0
- package/src/components/actions/AlertDialog/alert-dialog.stories.tsx +21 -0
- package/src/components/actions/AlertDialog/index.ts +1 -0
- package/src/components/actions/Button/Button.stories.ts +38 -0
- package/src/components/actions/Button/Button.tsx +23 -0
- package/src/components/actions/Button/index.ts +1 -0
- package/src/components/actions/Collapsible/index.ts +1 -0
- package/src/components/actions/Dialog/Dialog.stories.tsx +70 -0
- package/src/components/actions/Dialog/Dialog.tsx +87 -0
- package/src/components/actions/Dialog/components/DialogWithActions/index.tsx +40 -0
- package/src/components/actions/Dialog/index.ts +1 -0
- package/src/components/actions/Steps/Steps.stories.tsx +25 -0
- package/src/components/actions/Steps/Steps.tsx +51 -0
- package/src/components/actions/Steps/index.ts +1 -0
- package/src/components/actions/index.ts +5 -0
- package/src/components/dataDisplay/Avatar/Avatar.stories.tsx +22 -0
- package/src/components/dataDisplay/Avatar/Avatar.tsx +21 -0
- package/src/components/dataDisplay/Avatar/index.ts +2 -0
- package/src/components/dataDisplay/Badge/Badge.stories.tsx +36 -0
- package/src/components/dataDisplay/Badge/index.ts +1 -0
- package/src/components/dataDisplay/Card/Card.stories.tsx +24 -0
- package/src/components/dataDisplay/Card/Card.tsx +34 -0
- package/src/components/dataDisplay/Card/index.ts +1 -0
- package/src/components/dataDisplay/DataPairList/DataPairList.tsx +56 -0
- package/src/components/dataDisplay/DataPairList/data-pair-list.stories.tsx +87 -0
- package/src/components/dataDisplay/DataPairList/index.ts +2 -0
- package/src/components/dataDisplay/DataPairList/types.ts +10 -0
- package/src/components/dataDisplay/DropDownMenu/index.ts +1 -0
- package/src/components/dataDisplay/ErrorMessage/ErrorMessage.tsx +6 -0
- package/src/components/dataDisplay/ErrorMessage/index.ts +1 -0
- package/src/components/dataDisplay/Icon/Icon.stories.tsx +21 -0
- package/src/components/dataDisplay/Icon/Icon.tsx +47 -0
- package/src/components/dataDisplay/Icon/index.ts +1 -0
- package/src/components/dataDisplay/Icon/types.ts +6 -0
- package/src/components/dataDisplay/Label/Label.stories.tsx +21 -0
- package/src/components/dataDisplay/Label/Label.tsx +10 -0
- package/src/components/dataDisplay/Label/index.ts +1 -0
- package/src/components/dataDisplay/Table/Table.tsx +173 -0
- package/src/components/dataDisplay/Table/columns.tsx +223 -0
- package/src/components/dataDisplay/Table/components/DynamicTable/data-table-column-header.tsx +72 -0
- package/src/components/dataDisplay/Table/components/DynamicTable/data-table-pagination.tsx +91 -0
- package/src/components/dataDisplay/Table/components/DynamicTable/data-table-toolbar.tsx +17 -0
- package/src/components/dataDisplay/Table/components/DynamicTable/data-table-view-options.tsx +58 -0
- package/src/components/dataDisplay/Table/components/DynamicTable/data-table.stories.tsx +118 -0
- package/src/components/dataDisplay/Table/components/DynamicTable/index.tsx +136 -0
- package/src/components/dataDisplay/Table/components/DynamicTable/types.ts +43 -0
- package/src/components/dataDisplay/Table/custom/CustomTable/data-table-column-header.tsx +71 -0
- package/src/components/dataDisplay/Table/custom/CustomTable/data-table-faceted-filter.tsx +147 -0
- package/src/components/dataDisplay/Table/custom/CustomTable/data-table-pagination.tsx +97 -0
- package/src/components/dataDisplay/Table/custom/CustomTable/data-table-row-actions.tsx +78 -0
- package/src/components/dataDisplay/Table/custom/CustomTable/data-table-toolbar.tsx +60 -0
- package/src/components/dataDisplay/Table/custom/CustomTable/data-table-view-options.tsx +59 -0
- package/src/components/dataDisplay/Table/custom/CustomTable/data-table.tsx +145 -0
- package/src/components/dataDisplay/Table/custom/CustomTable/data.ts +71 -0
- package/src/components/dataDisplay/Table/custom/CustomTable/index.tsx +34 -0
- package/src/components/dataDisplay/Table/custom/CustomTable/schema.ts +11 -0
- package/src/components/dataDisplay/Table/index.ts +2 -0
- package/src/components/dataDisplay/Table/table.stories.tsx +147 -0
- package/src/components/dataDisplay/Table/types.ts +15 -0
- package/src/components/dataDisplay/Tabs/Tabs.stories.tsx +34 -0
- package/src/components/dataDisplay/Tabs/Tabs.tsx +53 -0
- package/src/components/dataDisplay/Tabs/index.ts +1 -0
- package/src/components/dataDisplay/Text/Text.stories.tsx +66 -0
- package/src/components/dataDisplay/Text/Text.tsx +56 -0
- package/src/components/dataDisplay/Text/index.ts +1 -0
- package/src/components/dataDisplay/index.ts +8 -0
- package/src/components/dataInput/Input/components/Input/Input.stories.tsx +99 -0
- package/src/components/dataInput/Input/components/Input/InputBase.tsx +50 -0
- package/src/components/dataInput/Input/components/Input/__tests__/Input.test.tsx +100 -0
- package/src/components/dataInput/Input/components/Input/index.tsx +257 -0
- package/src/components/dataInput/Input/components/InputBase/__tests__/InputBase.test.tsx +120 -0
- package/src/components/dataInput/Input/components/InputBase/index.tsx +89 -0
- package/src/components/dataInput/Input/components/MaskInput/__tests__/mask-input.test.tsx +67 -0
- package/src/components/dataInput/Input/components/MaskInput/index.ts +1 -0
- package/src/components/dataInput/Input/components/MaskInput/mask-input.stories.tsx +59 -0
- package/src/components/dataInput/Input/components/MaskInput/mask-input.tsx +43 -0
- package/src/components/dataInput/Input/components/MultipleInput/MultipleInput.tsx +36 -0
- package/src/components/dataInput/Input/components/MultipleInput/MultipleInputBase.tsx +100 -0
- package/src/components/dataInput/Input/components/MultipleInput/MultipleMaskInput.tsx +35 -0
- package/src/components/dataInput/Input/components/MultipleInput/MultipleNumberInput.tsx +35 -0
- package/src/components/dataInput/Input/components/MultipleInput/index.ts +2 -0
- package/src/components/dataInput/Input/components/MultipleInput/multiple-input.stories.tsx +71 -0
- package/src/components/dataInput/Input/components/NumberInput/__tests__/number-input.test.tsx +95 -0
- package/src/components/dataInput/Input/components/NumberInput/index.ts +1 -0
- package/src/components/dataInput/Input/components/NumberInput/number-input.stories.tsx +76 -0
- package/src/components/dataInput/Input/components/NumberInput/number-input.tsx +68 -0
- package/src/components/dataInput/Input/index.ts +4 -0
- package/src/components/dataInput/Select/MultiSelect/MultiSelect.stories.tsx +119 -0
- package/src/components/dataInput/Select/MultiSelect/MultiSelectBase.tsx +135 -0
- package/src/components/dataInput/Select/MultiSelect/index.tsx +75 -0
- package/src/components/dataInput/Select/Select.stories.tsx +61 -0
- package/src/components/dataInput/Select/Select.tsx +73 -0
- package/src/components/dataInput/Select/SelectBase.tsx +58 -0
- package/src/components/dataInput/Select/index.ts +2 -0
- package/src/components/dataInput/Switch/Switch.stories.tsx +75 -0
- package/src/components/dataInput/Switch/Switch.tsx +52 -0
- package/src/components/dataInput/Switch/index.ts +1 -0
- package/src/components/dataInput/checkbox/Checkbox.tsx +57 -0
- package/src/components/dataInput/checkbox/Checkbox_old.tsx +58 -0
- package/src/components/dataInput/checkbox/Checkout.stories.tsx +62 -0
- package/src/components/dataInput/checkbox/index.ts +1 -0
- package/src/components/dataInput/form/Form.tsx +47 -0
- package/src/components/dataInput/form/index.ts +3 -0
- package/src/components/dataInput/index.ts +5 -0
- package/src/components/feedback/Spinner/index.ts +1 -0
- package/src/components/feedback/Toast/Toast.stories.tsx +45 -0
- package/src/components/feedback/Toast/index.ts +2 -0
- package/src/components/feedback/index.ts +2 -0
- package/src/components/index.ts +6 -0
- package/src/components/layout/Crud/components/Table/index.tsx +183 -0
- package/src/components/layout/Crud/components/Table/types.ts +15 -0
- package/src/components/layout/Crud/crud.stories.tsx +317 -0
- package/src/components/layout/Crud/hook/useCrudLayout/index.tsx +94 -0
- package/src/components/layout/Crud/hook/useRequest/index.tsx +156 -0
- package/src/components/layout/Crud/index.tsx +295 -0
- package/src/components/layout/Crud/store/CrudLayoutStore.ts +75 -0
- package/src/components/layout/Crud/types.ts +14 -0
- package/src/components/layout/Drawer/CustomDrawer/index.tsx +33 -0
- package/src/components/layout/Drawer/Drawer.stories.tsx +80 -0
- package/src/components/layout/Drawer/index.ts +2 -0
- package/src/components/layout/PageLayout/PageLayout.stories.tsx +42 -0
- package/src/components/layout/PageLayout/index.tsx +28 -0
- package/src/components/layout/Separator/index.ts +1 -0
- package/src/components/layout/Sheet/Sheet.stories.tsx +28 -0
- package/src/components/layout/Sheet/Sheet.tsx +22 -0
- package/src/components/layout/Sheet/index.ts +1 -0
- package/src/components/layout/Sidebar/Sidebar.stories.tsx +116 -0
- package/src/components/layout/Sidebar/Sidebar.tsx +50 -0
- package/src/components/layout/Sidebar/components/app-sidebar.tsx +203 -0
- package/src/components/layout/Sidebar/components/footer-sidebar.tsx +17 -0
- package/src/components/layout/Sidebar/components/header-sidebar.tsx +90 -0
- package/src/components/layout/Sidebar/components/menus.tsx +55 -0
- package/src/components/layout/Sidebar/components/nav-projects.tsx +88 -0
- package/src/components/layout/Sidebar/components/nav-user.tsx +114 -0
- package/src/components/layout/Sidebar/components/team-switcher.tsx +85 -0
- package/src/components/layout/Sidebar/index.ts +2 -0
- package/src/components/layout/Sidebar/provider/index.tsx +51 -0
- package/src/components/layout/Tabs/Tabs.tsx +51 -0
- package/src/components/layout/Tabs/index.ts +1 -0
- package/src/components/layout/Tabs/tabs.stories.tsx +57 -0
- package/src/components/layout/index.ts +6 -0
- package/src/components/navigation/Breadcrumbs/Breadcrumbs.tsx +66 -0
- package/src/components/navigation/Breadcrumbs/index.ts +2 -0
- package/src/components/navigation/index.ts +1 -0
- package/src/components/ui/alert-dialog.tsx +141 -0
- package/src/components/ui/alert.tsx +59 -0
- package/src/components/ui/avatar.tsx +50 -0
- package/src/components/ui/badge.tsx +40 -0
- package/src/components/ui/breadcrumb.tsx +115 -0
- package/src/components/ui/button.tsx +57 -0
- package/src/components/ui/card.tsx +83 -0
- package/src/components/ui/checkbox.tsx +34 -0
- package/src/components/ui/collapsible.tsx +11 -0
- package/src/components/ui/command.tsx +153 -0
- package/src/components/ui/dialog.tsx +124 -0
- package/src/components/ui/drawer.tsx +117 -0
- package/src/components/ui/dropdown-menu.tsx +201 -0
- package/src/components/ui/form.tsx +179 -0
- package/src/components/ui/input.tsx +24 -0
- package/src/components/ui/label.tsx +30 -0
- package/src/components/ui/popover.tsx +33 -0
- package/src/components/ui/select.tsx +161 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/sheet.tsx +140 -0
- package/src/components/ui/sidebar.tsx +763 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/sonner.tsx +31 -0
- package/src/components/ui/spinner.tsx +54 -0
- package/src/components/ui/switch.tsx +33 -0
- package/src/components/ui/table.tsx +120 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/toast.tsx +130 -0
- package/src/components/ui/toaster.tsx +35 -0
- package/src/components/ui/tooltip.tsx +32 -0
- package/src/hooks/use-mobile.tsx +19 -0
- package/src/hooks/use-toast.ts +191 -0
- package/src/index.ts +1 -0
- package/src/lib/utils.ts +6 -0
- package/tailwind.config.ts +83 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Tabs as TabsShadcn,
|
|
3
|
+
TabsContent,
|
|
4
|
+
TabsList,
|
|
5
|
+
TabsTrigger,
|
|
6
|
+
} from "../../../components/ui/tabs";
|
|
7
|
+
import { useCallback } from "react";
|
|
8
|
+
|
|
9
|
+
export type TabsProps = {
|
|
10
|
+
data: {
|
|
11
|
+
label: string;
|
|
12
|
+
value: string;
|
|
13
|
+
content: JSX.Element;
|
|
14
|
+
}[];
|
|
15
|
+
defaultValue: string;
|
|
16
|
+
className?: string;
|
|
17
|
+
onSelectTab?: (value: string) => void;
|
|
18
|
+
value?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function Tabs({
|
|
22
|
+
data,
|
|
23
|
+
className,
|
|
24
|
+
value,
|
|
25
|
+
onSelectTab = () => {},
|
|
26
|
+
}: TabsProps) {
|
|
27
|
+
const renderTabsTrigger = useCallback(() => {
|
|
28
|
+
return data.map(({ label, value }, index) => (
|
|
29
|
+
<TabsTrigger key={index} value={value} onClick={() => onSelectTab(value)}>
|
|
30
|
+
{label}
|
|
31
|
+
</TabsTrigger>
|
|
32
|
+
));
|
|
33
|
+
}, [data, onSelectTab]);
|
|
34
|
+
|
|
35
|
+
const renderTabsContent = useCallback(() => {
|
|
36
|
+
return data.map(({ value, content }, index) => (
|
|
37
|
+
<TabsContent key={index} value={value}>
|
|
38
|
+
{content}
|
|
39
|
+
</TabsContent>
|
|
40
|
+
));
|
|
41
|
+
}, [data]);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<TabsShadcn defaultValue={value} className={className}>
|
|
45
|
+
{data?.length && (
|
|
46
|
+
<TabsList className="grid w-full grid-cols-2">
|
|
47
|
+
{renderTabsTrigger()}
|
|
48
|
+
</TabsList>
|
|
49
|
+
)}
|
|
50
|
+
{data?.length && renderTabsContent()}
|
|
51
|
+
</TabsShadcn>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./Tabs";
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/nextjs";
|
|
2
|
+
import { Text } from "./Text";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Text> = {
|
|
5
|
+
title: "DataDisplay/Text",
|
|
6
|
+
component: Text,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: "centered",
|
|
9
|
+
},
|
|
10
|
+
tags: ["autodocs"],
|
|
11
|
+
argTypes: {
|
|
12
|
+
variant: {
|
|
13
|
+
control: "select",
|
|
14
|
+
options: ["display", "title", "subtitle", "body", "caption", "muted"],
|
|
15
|
+
},
|
|
16
|
+
align: {
|
|
17
|
+
control: "select",
|
|
18
|
+
options: ["left", "center", "right", "justify"],
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default meta;
|
|
24
|
+
type Story = StoryObj<typeof Text>;
|
|
25
|
+
|
|
26
|
+
export const Display: Story = {
|
|
27
|
+
args: {
|
|
28
|
+
variant: "display",
|
|
29
|
+
children: "Cabeçalho Principal (Display)",
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const Title: Story = {
|
|
34
|
+
args: {
|
|
35
|
+
variant: "title",
|
|
36
|
+
children: "Título da Seção",
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const Subtitle: Story = {
|
|
41
|
+
args: {
|
|
42
|
+
variant: "subtitle",
|
|
43
|
+
children: "Subtítulo explicativo",
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const Normal: Story = {
|
|
48
|
+
args: {
|
|
49
|
+
variant: "normal",
|
|
50
|
+
children: "Texto de corpo padrão",
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const Caption: Story = {
|
|
55
|
+
args: {
|
|
56
|
+
variant: "caption",
|
|
57
|
+
children: "Texto de legenda",
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const Muted: Story = {
|
|
62
|
+
args: {
|
|
63
|
+
variant: "muted",
|
|
64
|
+
children: "Texto desativado ou informativo",
|
|
65
|
+
},
|
|
66
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
2
|
+
import { cn } from "../../..//lib/utils";
|
|
3
|
+
|
|
4
|
+
// Definição das variantes com pesos adequados
|
|
5
|
+
const textVariants = cva("", {
|
|
6
|
+
variants: {
|
|
7
|
+
variant: {
|
|
8
|
+
display: "text-5xl font-extrabold tracking-tight",
|
|
9
|
+
title: "text-3xl font-bold tracking-tight",
|
|
10
|
+
subtitle: "text-2xl font-semibold",
|
|
11
|
+
normal: "text-base font-normal",
|
|
12
|
+
caption: "text-sm font-medium",
|
|
13
|
+
muted: "text-sm font-normal text-muted-foreground",
|
|
14
|
+
},
|
|
15
|
+
align: {
|
|
16
|
+
left: "text-left",
|
|
17
|
+
center: "text-center",
|
|
18
|
+
right: "text-right",
|
|
19
|
+
justify: "text-justify",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
defaultVariants: {
|
|
23
|
+
variant: "normal",
|
|
24
|
+
align: "left",
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Mapeia variantes para as tags HTML corretas
|
|
29
|
+
const tagMap = {
|
|
30
|
+
display: "h1",
|
|
31
|
+
title: "h2",
|
|
32
|
+
subtitle: "h3",
|
|
33
|
+
normal: "p",
|
|
34
|
+
caption: "small",
|
|
35
|
+
muted: "p",
|
|
36
|
+
} as const;
|
|
37
|
+
|
|
38
|
+
type TextProps = VariantProps<typeof textVariants> & {
|
|
39
|
+
className?: string;
|
|
40
|
+
children: React.ReactNode;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export function Text({
|
|
44
|
+
variant = "normal",
|
|
45
|
+
align,
|
|
46
|
+
className,
|
|
47
|
+
children,
|
|
48
|
+
}: TextProps) {
|
|
49
|
+
const Tag = tagMap[variant || "normal"] || "p";
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Tag className={cn(textVariants({ variant, align }), className)}>
|
|
53
|
+
{children}
|
|
54
|
+
</Tag>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./Text";
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/nextjs";
|
|
2
|
+
import { Input } from "./index";
|
|
3
|
+
import { useForm } from "react-hook-form";
|
|
4
|
+
import { Form } from "@/components/ui/form";
|
|
5
|
+
import { Button } from "@/components/actions";
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof Input> = {
|
|
8
|
+
title: "Data Input/Input/Input",
|
|
9
|
+
component: Input,
|
|
10
|
+
tags: ["autodocs"],
|
|
11
|
+
parameters: {
|
|
12
|
+
layout: "centered",
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default meta;
|
|
17
|
+
type Story = StoryObj<typeof Input>;
|
|
18
|
+
|
|
19
|
+
export const Default: Story = {
|
|
20
|
+
args: {
|
|
21
|
+
label: "Default Input",
|
|
22
|
+
placeholder: "Type something...",
|
|
23
|
+
name: "default",
|
|
24
|
+
mask: "999.999.999-99",
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const Required: Story = {
|
|
29
|
+
args: {
|
|
30
|
+
label: "Required Input",
|
|
31
|
+
placeholder: "This field is required",
|
|
32
|
+
name: "required",
|
|
33
|
+
required: true,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const WithForm: Story = {
|
|
38
|
+
args: {
|
|
39
|
+
label: "Input with Form",
|
|
40
|
+
placeholder: "Standalone input",
|
|
41
|
+
name: "test",
|
|
42
|
+
},
|
|
43
|
+
decorators: [
|
|
44
|
+
(Story: React.ComponentType) => {
|
|
45
|
+
const form = useForm();
|
|
46
|
+
return (
|
|
47
|
+
<Form {...form}>
|
|
48
|
+
<Story />
|
|
49
|
+
</Form>
|
|
50
|
+
);
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const WithFormMask: Story = {
|
|
56
|
+
args: {
|
|
57
|
+
label: "Input with Form",
|
|
58
|
+
placeholder: "Standalone input",
|
|
59
|
+
name: "standalone",
|
|
60
|
+
mask: "999.999.999-99",
|
|
61
|
+
},
|
|
62
|
+
decorators: [
|
|
63
|
+
(Story: React.ComponentType) => {
|
|
64
|
+
const form = useForm();
|
|
65
|
+
return (
|
|
66
|
+
<Form {...form}>
|
|
67
|
+
<Story />
|
|
68
|
+
</Form>
|
|
69
|
+
);
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const Disabled: Story = {
|
|
75
|
+
args: {
|
|
76
|
+
label: "Disabled Input",
|
|
77
|
+
placeholder: "You cannot type here",
|
|
78
|
+
name: "disabled",
|
|
79
|
+
disabled: true,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const Error: Story = {
|
|
84
|
+
args: {
|
|
85
|
+
label: "Error Input",
|
|
86
|
+
placeholder: "This field is required",
|
|
87
|
+
name: "error",
|
|
88
|
+
error: "This is an error message",
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const WithButton: Story = {
|
|
93
|
+
args: {
|
|
94
|
+
label: "Input with Button",
|
|
95
|
+
placeholder: "Type something...",
|
|
96
|
+
name: "default",
|
|
97
|
+
component: <Button>Click me</Button>,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React, { forwardRef, useCallback, useImperativeHandle } from "react";
|
|
2
|
+
import { Input, InputProps } from "../../../../ui/input";
|
|
3
|
+
import { mask as remask } from "remask";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
export type InputBaseProps = InputProps & {
|
|
7
|
+
onChangeText?: (text: string) => void;
|
|
8
|
+
mask?: string | string[];
|
|
9
|
+
"data-testid"?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const InputBase = forwardRef<HTMLInputElement, InputBaseProps>(
|
|
13
|
+
({ mask, className, ...props }, ref) => {
|
|
14
|
+
const { onChangeText = () => {}, ...rest } = props;
|
|
15
|
+
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
16
|
+
|
|
17
|
+
const setMask = useCallback(
|
|
18
|
+
(value?: string | number | readonly string[] | undefined) => {
|
|
19
|
+
const inputValue = value || "";
|
|
20
|
+
if (!mask) return String(inputValue);
|
|
21
|
+
return remask(String(inputValue), mask);
|
|
22
|
+
},
|
|
23
|
+
[mask]
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
useImperativeHandle(ref, () => inputRef.current!);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Input
|
|
30
|
+
{...rest}
|
|
31
|
+
ref={inputRef}
|
|
32
|
+
className={cn(
|
|
33
|
+
"border-0 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0",
|
|
34
|
+
"p-0 h-auto bg-transparent",
|
|
35
|
+
className
|
|
36
|
+
)}
|
|
37
|
+
onChange={(e) => {
|
|
38
|
+
const value = setMask(e.target.value);
|
|
39
|
+
if (inputRef.current) {
|
|
40
|
+
inputRef.current.value = value;
|
|
41
|
+
}
|
|
42
|
+
props.onChange?.(e);
|
|
43
|
+
onChangeText(value);
|
|
44
|
+
}}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
InputBase.displayName = "InputBase";
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { Input } from "../index";
|
|
5
|
+
import { FormProvider, useForm } from "react-hook-form";
|
|
6
|
+
|
|
7
|
+
// Componente wrapper para testar o Input dentro de um formulário
|
|
8
|
+
const FormWrapper = ({ children }: { children: React.ReactNode }) => {
|
|
9
|
+
const methods = useForm();
|
|
10
|
+
return <FormProvider {...methods}>{children}</FormProvider>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
describe("Componente Input", () => {
|
|
14
|
+
it("deve renderizar com label", () => {
|
|
15
|
+
render(
|
|
16
|
+
<FormWrapper>
|
|
17
|
+
<Input label="Nome" name="name" />
|
|
18
|
+
</FormWrapper>
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
expect(screen.getByLabelText("Nome")).toBeInTheDocument();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("deve renderizar sem formulário quando withoutForm é true", () => {
|
|
25
|
+
render(<Input label="Email" name="email" withoutForm={true} />);
|
|
26
|
+
|
|
27
|
+
expect(screen.getByLabelText("Email")).toBeInTheDocument();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("deve exibir mensagem de erro quando fornecida", () => {
|
|
31
|
+
render(
|
|
32
|
+
<FormWrapper>
|
|
33
|
+
<Input label="Senha" name="password" error="Senha é obrigatória" />
|
|
34
|
+
</FormWrapper>
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
expect(screen.getByText("Senha é obrigatória")).toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("deve aplicar data-testid quando fornecido", () => {
|
|
41
|
+
render(
|
|
42
|
+
<FormWrapper>
|
|
43
|
+
<Input label="Telefone" name="phone" data-testid="phone-input" />
|
|
44
|
+
</FormWrapper>
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(screen.getByTestId("phone-input")).toBeInTheDocument();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("deve lidar corretamente com a entrada do usuário", async () => {
|
|
51
|
+
const user = userEvent.setup();
|
|
52
|
+
|
|
53
|
+
render(
|
|
54
|
+
<FormWrapper>
|
|
55
|
+
<Input label="Usuário" name="username" />
|
|
56
|
+
</FormWrapper>
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const input = screen.getByLabelText("Usuário");
|
|
60
|
+
await user.type(input, "testuser");
|
|
61
|
+
|
|
62
|
+
expect(input).toHaveValue("testuser");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("deve passar props adicionais para o elemento input", () => {
|
|
66
|
+
render(
|
|
67
|
+
<FormWrapper>
|
|
68
|
+
<Input
|
|
69
|
+
label="Código"
|
|
70
|
+
name="code"
|
|
71
|
+
placeholder="Digite o código"
|
|
72
|
+
maxLength={6}
|
|
73
|
+
/>
|
|
74
|
+
</FormWrapper>
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const input = screen.getByLabelText("Código");
|
|
78
|
+
expect(input).toHaveAttribute("placeholder", "Digite o código");
|
|
79
|
+
expect(input).toHaveAttribute("maxLength", "6");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("deve chamar o onChange quando usado sem formulário", async () => {
|
|
83
|
+
const handleChange = jest.fn();
|
|
84
|
+
const user = userEvent.setup();
|
|
85
|
+
|
|
86
|
+
render(
|
|
87
|
+
<Input
|
|
88
|
+
label="Comentário"
|
|
89
|
+
name="comment"
|
|
90
|
+
withoutForm={true}
|
|
91
|
+
onChange={handleChange}
|
|
92
|
+
/>
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const input = screen.getByLabelText("Comentário");
|
|
96
|
+
await user.type(input, "a");
|
|
97
|
+
|
|
98
|
+
expect(handleChange).toHaveBeenCalled();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Label } from "../../../../ui/label";
|
|
3
|
+
import { useFormContext } from "react-hook-form";
|
|
4
|
+
import {
|
|
5
|
+
FormControl,
|
|
6
|
+
FormField,
|
|
7
|
+
FormItem,
|
|
8
|
+
FormLabel,
|
|
9
|
+
FormMessage,
|
|
10
|
+
} from "../../../../../components/ui/form";
|
|
11
|
+
import { InputBase, InputBaseProps } from "./InputBase";
|
|
12
|
+
import { ErrorMessage } from "../../../../../components/dataDisplay/ErrorMessage";
|
|
13
|
+
import { cn } from "@/lib/utils";
|
|
14
|
+
|
|
15
|
+
export type InputProps = Omit<InputBaseProps, "children"> & {
|
|
16
|
+
component?: React.ReactNode;
|
|
17
|
+
error?: string;
|
|
18
|
+
withoutForm?: boolean;
|
|
19
|
+
label?: string;
|
|
20
|
+
leftIcon?: React.ReactNode;
|
|
21
|
+
rightIcon?: React.ReactNode;
|
|
22
|
+
helperText?: string;
|
|
23
|
+
floatingLabel?: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const Input = ({
|
|
27
|
+
label,
|
|
28
|
+
withoutForm,
|
|
29
|
+
component,
|
|
30
|
+
name,
|
|
31
|
+
error,
|
|
32
|
+
leftIcon,
|
|
33
|
+
rightIcon,
|
|
34
|
+
helperText,
|
|
35
|
+
floatingLabel = false,
|
|
36
|
+
"data-testid": testId,
|
|
37
|
+
className,
|
|
38
|
+
required,
|
|
39
|
+
placeholder,
|
|
40
|
+
...props
|
|
41
|
+
}: InputProps) => {
|
|
42
|
+
const form = useFormContext();
|
|
43
|
+
const hasForm = !withoutForm && !!form && !!name;
|
|
44
|
+
const [isFocused, setIsFocused] = React.useState(false);
|
|
45
|
+
const [hasValue, setHasValue] = React.useState(false);
|
|
46
|
+
|
|
47
|
+
const containerClasses = cn("relative w-full", className);
|
|
48
|
+
|
|
49
|
+
if (!hasForm)
|
|
50
|
+
return (
|
|
51
|
+
<div className={containerClasses}>
|
|
52
|
+
<div className="relative">
|
|
53
|
+
{/* Floating Label ou Label Normal */}
|
|
54
|
+
{label && !floatingLabel && (
|
|
55
|
+
<Label
|
|
56
|
+
htmlFor={name || "input"}
|
|
57
|
+
className={cn(
|
|
58
|
+
"block text-sm font-medium mb-1.5 transition-colors duration-200",
|
|
59
|
+
error ? "text-destructive" : "text-foreground"
|
|
60
|
+
)}
|
|
61
|
+
>
|
|
62
|
+
{label}
|
|
63
|
+
{required && <span className="text-destructive ml-1">*</span>}
|
|
64
|
+
</Label>
|
|
65
|
+
)}
|
|
66
|
+
|
|
67
|
+
{/* Input Container */}
|
|
68
|
+
<div className="relative">
|
|
69
|
+
{leftIcon && (
|
|
70
|
+
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 z-10">
|
|
71
|
+
{leftIcon}
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
<InputBase
|
|
76
|
+
{...props}
|
|
77
|
+
placeholder={floatingLabel ? " " : placeholder}
|
|
78
|
+
className={cn(
|
|
79
|
+
"w-full px-4 py-3 border border-border rounded-xl",
|
|
80
|
+
"bg-background transition-all duration-200",
|
|
81
|
+
"focus:border-primary focus:ring-4 focus:ring-primary/20",
|
|
82
|
+
"placeholder:text-muted-foreground",
|
|
83
|
+
"disabled:bg-muted disabled:text-muted-foreground",
|
|
84
|
+
leftIcon && "pl-10",
|
|
85
|
+
rightIcon && "pr-10",
|
|
86
|
+
error &&
|
|
87
|
+
"border-destructive focus:border-destructive focus:ring-destructive/20",
|
|
88
|
+
floatingLabel && "pt-6 pb-2"
|
|
89
|
+
)}
|
|
90
|
+
onFocus={(e) => {
|
|
91
|
+
setIsFocused(true);
|
|
92
|
+
props.onFocus?.(e);
|
|
93
|
+
}}
|
|
94
|
+
onBlur={(e) => {
|
|
95
|
+
setIsFocused(false);
|
|
96
|
+
setHasValue(!!e.target.value);
|
|
97
|
+
props.onBlur?.(e);
|
|
98
|
+
}}
|
|
99
|
+
/>
|
|
100
|
+
|
|
101
|
+
{/* Floating Label */}
|
|
102
|
+
{label && floatingLabel && (
|
|
103
|
+
<Label
|
|
104
|
+
htmlFor={name || "input"}
|
|
105
|
+
className={cn(
|
|
106
|
+
"absolute left-4 transition-all duration-200 pointer-events-none",
|
|
107
|
+
"text-muted-foreground",
|
|
108
|
+
isFocused || hasValue
|
|
109
|
+
? "top-2 text-xs font-medium"
|
|
110
|
+
: "top-1/2 transform -translate-y-1/2 text-base",
|
|
111
|
+
isFocused && !error && "text-primary",
|
|
112
|
+
error && "text-destructive"
|
|
113
|
+
)}
|
|
114
|
+
>
|
|
115
|
+
{label}
|
|
116
|
+
{required && <span className="text-destructive ml-1">*</span>}
|
|
117
|
+
</Label>
|
|
118
|
+
)}
|
|
119
|
+
|
|
120
|
+
{rightIcon && (
|
|
121
|
+
<div className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 z-10">
|
|
122
|
+
{rightIcon}
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
|
|
126
|
+
{component && (
|
|
127
|
+
<div className="absolute right-3 top-1/2 transform -translate-y-1/2 z-10">
|
|
128
|
+
{component}
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
{/* Helper Text */}
|
|
134
|
+
{helperText && !error && (
|
|
135
|
+
<p className="mt-2 text-xs text-muted-foreground">{helperText}</p>
|
|
136
|
+
)}
|
|
137
|
+
|
|
138
|
+
{/* Error Message */}
|
|
139
|
+
{error && (
|
|
140
|
+
<div className="mt-2">
|
|
141
|
+
<ErrorMessage>{error}</ErrorMessage>
|
|
142
|
+
</div>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<FormField
|
|
150
|
+
control={form.control}
|
|
151
|
+
name={name}
|
|
152
|
+
render={({ field: { onChange, value, ...rest } }) => (
|
|
153
|
+
<FormItem
|
|
154
|
+
className={containerClasses}
|
|
155
|
+
data-testid={testId ? `form-item-${testId}` : undefined}
|
|
156
|
+
>
|
|
157
|
+
<div className="relative">
|
|
158
|
+
{/* Label Normal */}
|
|
159
|
+
{label && !floatingLabel && (
|
|
160
|
+
<FormLabel
|
|
161
|
+
htmlFor={name}
|
|
162
|
+
className={cn(
|
|
163
|
+
"block text-sm font-medium mb-1.5 transition-colors duration-200",
|
|
164
|
+
"text-foreground data-[invalid]:text-destructive"
|
|
165
|
+
)}
|
|
166
|
+
data-testid={testId ? `form-label-${testId}` : undefined}
|
|
167
|
+
>
|
|
168
|
+
{label}
|
|
169
|
+
{required && <span className="text-destructive ml-1">*</span>}
|
|
170
|
+
</FormLabel>
|
|
171
|
+
)}
|
|
172
|
+
|
|
173
|
+
<FormControl>
|
|
174
|
+
<div className="relative">
|
|
175
|
+
{leftIcon && (
|
|
176
|
+
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 z-10">
|
|
177
|
+
{leftIcon}
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
|
|
181
|
+
<InputBase
|
|
182
|
+
{...props}
|
|
183
|
+
{...rest}
|
|
184
|
+
value={value}
|
|
185
|
+
onChangeText={onChange}
|
|
186
|
+
placeholder={floatingLabel ? " " : placeholder}
|
|
187
|
+
className={cn(
|
|
188
|
+
"w-full px-4 py-3 border border-border rounded-xl",
|
|
189
|
+
"bg-background transition-all duration-200",
|
|
190
|
+
"focus:border-primary focus:ring-4 focus:ring-primary/20",
|
|
191
|
+
"placeholder:text-muted-foreground",
|
|
192
|
+
"disabled:bg-muted disabled:text-muted-foreground",
|
|
193
|
+
leftIcon && "pl-10",
|
|
194
|
+
rightIcon && "pr-10",
|
|
195
|
+
floatingLabel && "pt-6 pb-2"
|
|
196
|
+
)}
|
|
197
|
+
onFocus={(e) => {
|
|
198
|
+
setIsFocused(true);
|
|
199
|
+
props.onFocus?.(e);
|
|
200
|
+
}}
|
|
201
|
+
onBlur={(e) => {
|
|
202
|
+
setIsFocused(false);
|
|
203
|
+
setHasValue(!!value);
|
|
204
|
+
props.onBlur?.(e);
|
|
205
|
+
}}
|
|
206
|
+
/>
|
|
207
|
+
|
|
208
|
+
{/* Floating Label */}
|
|
209
|
+
{label && floatingLabel && (
|
|
210
|
+
<FormLabel
|
|
211
|
+
htmlFor={name}
|
|
212
|
+
className={cn(
|
|
213
|
+
"absolute left-4 transition-all duration-200 pointer-events-none",
|
|
214
|
+
"text-muted-foreground",
|
|
215
|
+
isFocused || hasValue || value
|
|
216
|
+
? "top-2 text-xs font-medium"
|
|
217
|
+
: "top-1/2 transform -translate-y-1/2 text-base",
|
|
218
|
+
isFocused && "text-primary"
|
|
219
|
+
)}
|
|
220
|
+
>
|
|
221
|
+
{label}
|
|
222
|
+
{required && (
|
|
223
|
+
<span className="text-destructive ml-1">*</span>
|
|
224
|
+
)}
|
|
225
|
+
</FormLabel>
|
|
226
|
+
)}
|
|
227
|
+
|
|
228
|
+
{rightIcon && (
|
|
229
|
+
<div className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 z-10">
|
|
230
|
+
{rightIcon}
|
|
231
|
+
</div>
|
|
232
|
+
)}
|
|
233
|
+
|
|
234
|
+
{component && (
|
|
235
|
+
<div className="absolute right-3 top-1/2 transform -translate-y-1/2 z-10">
|
|
236
|
+
{component}
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
</FormControl>
|
|
241
|
+
|
|
242
|
+
{/* Helper Text */}
|
|
243
|
+
{helperText && (
|
|
244
|
+
<p className="mt-2 text-xs text-muted-foreground">{helperText}</p>
|
|
245
|
+
)}
|
|
246
|
+
|
|
247
|
+
<FormMessage
|
|
248
|
+
role="alert"
|
|
249
|
+
className="mt-2"
|
|
250
|
+
data-testid={testId ? `form-message-${testId}` : undefined}
|
|
251
|
+
/>
|
|
252
|
+
</div>
|
|
253
|
+
</FormItem>
|
|
254
|
+
)}
|
|
255
|
+
/>
|
|
256
|
+
);
|
|
257
|
+
};
|