@phsa.tec/design-system-react 0.1.3 → 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.
@@ -16,8 +16,28 @@ concurrency:
16
16
  cancel-in-progress: false
17
17
 
18
18
  jobs:
19
+ test:
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - name: Checkout
23
+ uses: actions/checkout@v4
24
+
25
+ - name: Setup Node
26
+ uses: actions/setup-node@v4
27
+ with:
28
+ node-version: "20"
29
+ cache: "yarn"
30
+
31
+ - name: Install dependencies
32
+ run: yarn install --frozen-lockfile
33
+
34
+ - name: Run tests
35
+ run: yarn test
36
+
19
37
  build:
20
38
  runs-on: ubuntu-latest
39
+ needs: test
40
+ if: github.event_name == 'push' && github.ref == 'refs/heads/master'
21
41
  steps:
22
42
  - name: Checkout
23
43
  uses: actions/checkout@v4
@@ -48,6 +68,7 @@ jobs:
48
68
  url: ${{ steps.deployment.outputs.page_url }}
49
69
  runs-on: ubuntu-latest
50
70
  needs: build
71
+ if: github.event_name == 'push' && github.ref == 'refs/heads/master'
51
72
  steps:
52
73
  - name: Deploy to GitHub Pages
53
74
  id: deployment
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phsa.tec/design-system-react",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "private": false,
5
5
  "main": "./src/index.ts",
6
6
  "scripts": {
@@ -11,7 +11,8 @@
11
11
  "storybook": "storybook dev -p 6006",
12
12
  "build-storybook": "storybook build",
13
13
  "deploy-storybook": "storybook build && touch storybook-static/.nojekyll",
14
- "test": "jest"
14
+ "test": "jest",
15
+ "prepare": "husky install"
15
16
  },
16
17
  "dependencies": {
17
18
  "@hookform/resolvers": "^3.9.1",
@@ -75,6 +76,7 @@
75
76
  "eslint": "^8",
76
77
  "eslint-config-next": "15.0.3",
77
78
  "eslint-plugin-storybook": "^9.0.15",
79
+ "husky": "^9.1.7",
78
80
  "jest": "^30.0.4",
79
81
  "jest-environment-jsdom": "^29.7.0",
80
82
  "postcss": "^8",
@@ -91,6 +93,33 @@
91
93
  "type": "git",
92
94
  "url": "https://github.com/henriques4nti4go/phsa-design-system"
93
95
  },
96
+ "keywords": [
97
+ "react",
98
+ "design-system",
99
+ "ui-components",
100
+ "typescript",
101
+ "tailwindcss",
102
+ "radix-ui",
103
+ "storybook",
104
+ "nextjs",
105
+ "form-components",
106
+ "data-table",
107
+ "theme-support",
108
+ "dark-mode",
109
+ "light-mode",
110
+ "responsive",
111
+ "accessible",
112
+ "reusable-components",
113
+ "react-hook-form",
114
+ "zod-validation",
115
+ "tanstack-query",
116
+ "zustand",
117
+ "lucide-react",
118
+ "shadcn-ui",
119
+ "component-library",
120
+ "frontend",
121
+ "ui-kit"
122
+ ],
94
123
  "homepage": "https://github.com/henriques4nti4go/phsa-design-system#readme",
95
124
  "bugs": {
96
125
  "url": "https://github.com/henriques4nti4go/phsa-design-system/issues"
@@ -8,6 +8,8 @@ import { cn } from "@/lib/utils";
8
8
  export const Input = ({
9
9
  "data-testid": dataTestId,
10
10
  withoutForm = false,
11
+ extraElement,
12
+ containerClassName,
11
13
  ...props
12
14
  }: InputProps) => {
13
15
  const formData = useConditionalController({
@@ -29,15 +31,18 @@ export const Input = ({
29
31
  required={props.required}
30
32
  data-testid={dataTestId}
31
33
  >
32
- <InputUI
33
- {...inputProps}
34
- data-testid={dataTestId}
35
- className={cn(
36
- props.className,
37
- props.error &&
38
- "border-destructive focus:border-destructive focus-visible:ring-0"
39
- )}
40
- />
34
+ <div className={cn("flex items-center gap-2", containerClassName)}>
35
+ <InputUI
36
+ {...inputProps}
37
+ data-testid={dataTestId}
38
+ className={cn(
39
+ props.className,
40
+ props.error &&
41
+ "border-destructive focus:border-destructive focus-visible:ring-0"
42
+ )}
43
+ />
44
+ {extraElement}
45
+ </div>
41
46
  </InputBase>
42
47
  );
43
48
  };
@@ -10,4 +10,6 @@ export type InputProps = Omit<InputBaseProps, "children"> & {
10
10
  helperText?: string;
11
11
  floatingLabel?: boolean;
12
12
  "data-testid"?: string;
13
+ extraElement?: React.ReactNode;
14
+ containerClassName?: string;
13
15
  };
@@ -1,11 +1,12 @@
1
1
  import "@testing-library/jest-dom";
2
- import { fireEvent, render, screen } from "@testing-library/react";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
3
4
  import { MaskInput } from "../mask-input";
4
5
 
5
6
  describe("MaskInput", () => {
6
7
  it("should render label", () => {
7
8
  render(
8
- <MaskInput label="test" data-testid="mask-input" mask="000.000.000-00" />
9
+ <MaskInput label="test" data-testid="mask-input" mask="999.999.999-99" />
9
10
  );
10
11
  const label = screen.getByTestId("mask-input-label");
11
12
  expect(label).toBeInTheDocument();
@@ -14,7 +15,7 @@ describe("MaskInput", () => {
14
15
 
15
16
  it("should render error", () => {
16
17
  render(
17
- <MaskInput error="test" data-testid="mask-input" mask="000.000.000-00" />
18
+ <MaskInput error="test" data-testid="mask-input" mask="999.999.999-99" />
18
19
  );
19
20
  const error = screen.getByTestId("mask-input-error-label");
20
21
  expect(error).toBeInTheDocument();
@@ -23,7 +24,7 @@ describe("MaskInput", () => {
23
24
 
24
25
  it("should be disabled", () => {
25
26
  render(
26
- <MaskInput disabled data-testid="mask-input" mask="000.000.000-00" />
27
+ <MaskInput disabled data-testid="mask-input" mask="999.999.999-99" />
27
28
  );
28
29
  const input = screen.getByTestId("mask-input");
29
30
  expect(input).toBeDisabled();
@@ -35,7 +36,7 @@ describe("MaskInput", () => {
35
36
  required
36
37
  data-testid="mask-input"
37
38
  label="test"
38
- mask="000.000.000-00"
39
+ mask="999.999.999-99"
39
40
  />
40
41
  );
41
42
  const label = screen.getByTestId("mask-input-label");
@@ -43,11 +44,11 @@ describe("MaskInput", () => {
43
44
  });
44
45
 
45
46
  it("should apply mask to input value", async () => {
46
- render(<MaskInput data-testid="mask-input" mask="000.000.000-00" />);
47
+ const user = userEvent.setup();
48
+ render(<MaskInput data-testid="mask-input" mask="999.999.999-99" />);
47
49
  const input = screen.getByTestId("mask-input");
48
50
 
49
- fireEvent.focus(input);
50
- fireEvent.input(input, { target: { value: "12345678901" } });
51
+ await user.type(input, "12345678901");
51
52
 
52
53
  expect(input).toHaveValue("123.456.789-01");
53
54
  });
@@ -56,7 +57,7 @@ describe("MaskInput", () => {
56
57
  render(
57
58
  <MaskInput
58
59
  data-testid="mask-input"
59
- mask="000.000.000-00"
60
+ mask="999.999.999-99"
60
61
  placeholder="Digite seu CPF"
61
62
  />
62
63
  );
@@ -67,7 +68,7 @@ describe("MaskInput", () => {
67
68
 
68
69
  it("should handle name attribute", () => {
69
70
  render(
70
- <MaskInput data-testid="mask-input" mask="000.000.000-00" name="cpf" />
71
+ <MaskInput data-testid="mask-input" mask="999.999.999-99" name="cpf" />
71
72
  );
72
73
  const input = screen.getByTestId("mask-input");
73
74
 
@@ -19,31 +19,47 @@ export const Default: Story = {
19
19
  args: {
20
20
  placeholder: "99999-999",
21
21
  label: "CEP",
22
- mask: "00000-000",
22
+ mask: "99999-999",
23
23
  },
24
24
  };
25
25
 
26
26
  export const CPF: Story = {
27
27
  args: {
28
- placeholder: "000.000.000-00",
28
+ placeholder: "999.999.999-99",
29
29
  label: "CPF",
30
- mask: "000.000.000-00",
30
+ mask: "999.999.999-99",
31
31
  },
32
32
  };
33
33
 
34
34
  export const Phone: Story = {
35
35
  args: {
36
- placeholder: "(00) 00000-0000",
36
+ placeholder: "(99) 99999-9999",
37
37
  label: "Telefone",
38
- mask: "(00) 00000-0000",
38
+ mask: "(99) 99999-9999",
39
39
  },
40
40
  };
41
41
 
42
42
  export const Date: Story = {
43
43
  args: {
44
- placeholder: "00/00/0000",
44
+ placeholder: "99/99/9999",
45
45
  label: "Data",
46
- mask: "00/00/0000",
46
+ mask: "99/99/9999",
47
+ },
48
+ };
49
+
50
+ export const LettersExample: Story = {
51
+ args: {
52
+ placeholder: "AAA-9999",
53
+ label: "Código com Letras",
54
+ mask: "AAA-9999",
55
+ },
56
+ };
57
+
58
+ export const MixedExample: Story = {
59
+ args: {
60
+ placeholder: "A99-AAA-999",
61
+ label: "Código Misto",
62
+ mask: "A99-AAA-999",
47
63
  },
48
64
  };
49
65
 
@@ -60,8 +76,8 @@ export const WithForm = () => {
60
76
  <MaskInput
61
77
  name="cpf"
62
78
  label="CPF"
63
- placeholder="000.000.000-00"
64
- mask="000.000.000-00"
79
+ placeholder="999.999.999-99"
80
+ mask="999.999.999-99"
65
81
  />
66
82
  </form>
67
83
  </Form>
@@ -1,15 +1,20 @@
1
1
  "use client";
2
2
  import * as React from "react";
3
- import { useIMask, ReactMaskOpts } from "react-imask";
4
3
  import { Input } from "../Input";
5
4
  import { useConditionalController } from "@/hooks/use-conditional-controller";
6
5
  import { InputProps } from "../Input/types";
6
+ import { useMask } from "@/hooks/use-mask";
7
+ import { useRef } from "react";
7
8
 
8
- export type MaskInputProps = ReactMaskOpts & InputProps;
9
+ export type MaskInputProps = {
10
+ mask?: string;
11
+ } & InputProps;
9
12
 
10
13
  export const MaskInput = ({
11
14
  "data-testid": dataTestId,
12
15
  withoutForm = false,
16
+ extraElement,
17
+ mask = "",
13
18
  ...props
14
19
  }: MaskInputProps) => {
15
20
  const formData = useConditionalController({
@@ -17,38 +22,52 @@ export const MaskInput = ({
17
22
  withoutForm,
18
23
  });
19
24
 
20
- const {
21
- label,
22
- error,
23
- required,
24
- name,
25
- className,
26
- placeholder,
27
- ...imaskProps
28
- } = props;
29
-
30
- const { ref, value } = useIMask({
31
- ...imaskProps,
32
- });
25
+ const inputProps = React.useMemo(() => {
26
+ return {
27
+ ...formData,
28
+ ...props,
29
+ };
30
+ }, [formData, props]);
31
+
32
+ const { applyMask } = useMask({ mask });
33
33
 
34
- React.useEffect(() => {
35
- if (formData.value !== undefined && formData.value !== value) {
36
- formData.onChange(value);
37
- }
38
- }, [formData, value]);
34
+ const inputRef = useRef<HTMLInputElement>(null);
35
+
36
+ const applyMaskToInput = React.useCallback(
37
+ (value: string) => {
38
+ if (inputRef.current && mask) {
39
+ const maskedValue = applyMask(value);
40
+ inputRef.current.value = maskedValue;
41
+ }
42
+ },
43
+ [applyMask, mask]
44
+ );
45
+
46
+ const handleChange = React.useCallback(
47
+ (e: React.ChangeEvent<HTMLInputElement>) => {
48
+ const maskedValue = mask ? applyMask(e.target.value) : e.target.value;
49
+
50
+ const response = {
51
+ ...e,
52
+ target: {
53
+ ...e.target,
54
+ value: maskedValue,
55
+ },
56
+ };
57
+ applyMaskToInput(maskedValue);
58
+ return inputProps?.onChange?.(response);
59
+ },
60
+ [applyMask, inputProps, mask, applyMaskToInput]
61
+ );
39
62
 
40
63
  return (
41
64
  <Input
42
- {...props}
43
- ref={ref as React.RefObject<HTMLInputElement>}
65
+ {...inputProps}
66
+ ref={inputRef}
67
+ onChange={handleChange}
68
+ extraElement={extraElement}
44
69
  data-testid={dataTestId}
45
70
  withoutForm={true}
46
- label={label}
47
- error={error}
48
- required={required}
49
- className={className}
50
- placeholder={placeholder}
51
- name={name}
52
71
  />
53
72
  );
54
73
  };
@@ -1,36 +1,105 @@
1
1
  import { Button } from "../../../../actions/Button";
2
- import { Input, InputProps } from "../Input";
3
- import { MultipleInputBase } from "./MultipleInputBase";
4
2
  import { Icon } from "../../../../dataDisplay/Icon";
3
+ import { useCallback, useMemo, useState } from "react";
4
+ import { useConditionalController } from "@/hooks/use-conditional-controller";
5
+ import { MaskInput, MaskInputProps } from "../MaskInput";
5
6
 
6
- export type MultipleInputProps = InputProps & {
7
+ export type MultipleInputProps = MaskInputProps & {
7
8
  data?: string[];
8
- onChangeData?: (data: string[]) => void;
9
+ onAdd?: (data: string) => void;
10
+ onRemove?: (position: number) => void;
9
11
  name: string;
12
+ defaultValue?: string;
13
+ "data-testid"?: string;
10
14
  };
11
15
 
12
16
  export const MultipleInput = ({
13
17
  data = [],
14
- mask,
18
+ onAdd = () => {},
19
+ defaultValue = "",
20
+ onRemove = () => {},
21
+ withoutForm = false,
15
22
  ...props
16
23
  }: MultipleInputProps) => {
24
+ const [inputValue, setInputValue] = useState(defaultValue);
25
+
26
+ const formData = useConditionalController({
27
+ name: props.name || "",
28
+ withoutForm,
29
+ });
30
+
31
+ const inputItems = useMemo(() => {
32
+ if (formData?.value) {
33
+ return formData.value;
34
+ }
35
+ return data;
36
+ }, [data, formData]);
37
+
38
+ const onAddData = useCallback(() => {
39
+ const trimmedValue = inputValue.trim();
40
+ if (trimmedValue) {
41
+ if (formData?.onChange) {
42
+ formData.onChange([...inputItems, trimmedValue]);
43
+ } else {
44
+ onAdd(trimmedValue);
45
+ }
46
+ setInputValue("");
47
+ }
48
+ }, [inputValue, formData, onAdd, inputItems]);
49
+
50
+ const onRemoveData = useCallback(
51
+ (index: number) => {
52
+ if (formData?.onChange) {
53
+ const newData = inputItems.filter(
54
+ (_: string, i: number) => i !== index
55
+ );
56
+ formData.onChange(newData);
57
+ } else {
58
+ onRemove(index);
59
+ }
60
+ },
61
+ [formData, inputItems, onRemove]
62
+ );
63
+
64
+ const renderItens = useCallback(() => {
65
+ return inputItems?.map((item: string, index: number) => {
66
+ return (
67
+ <div key={item} className="flex justify-between">
68
+ {item}
69
+ <Button
70
+ variant="ghost"
71
+ className="text-destructive"
72
+ size={"icon"}
73
+ onClick={() => onRemoveData(index)}
74
+ >
75
+ <Icon name="MdDelete" />
76
+ </Button>
77
+ </div>
78
+ );
79
+ });
80
+ }, [inputItems, onRemoveData]);
81
+
82
+ const extraElement = useMemo(
83
+ () => (
84
+ <Button type="button" onClick={onAddData} disabled={!inputValue}>
85
+ <Icon name="MdAdd" />
86
+ </Button>
87
+ ),
88
+ [onAddData, inputValue]
89
+ );
90
+
17
91
  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>
92
+ <div className="flex flex-col gap-2">
93
+ <MaskInput
94
+ {...props}
95
+ withoutForm
96
+ extraElement={extraElement}
97
+ onChange={(e) => {
98
+ setInputValue(e.target.value);
99
+ }}
100
+ value={inputValue}
101
+ />
102
+ {renderItens()}
103
+ </div>
35
104
  );
36
105
  };
@@ -0,0 +1,152 @@
1
+ import "@testing-library/jest-dom";
2
+ import { fireEvent, render, screen } from "@testing-library/react";
3
+ import { MultipleInput } from "../MultipleInput";
4
+
5
+ describe("MultipleInput", () => {
6
+ it("should render label", () => {
7
+ render(
8
+ <MultipleInput
9
+ label="test"
10
+ name="test-input"
11
+ data-testid="multiple-input"
12
+ />
13
+ );
14
+ const label = screen.getByTestId("multiple-input-label");
15
+ expect(label).toBeInTheDocument();
16
+ expect(label).toHaveTextContent("test");
17
+ });
18
+
19
+ it("should render error", () => {
20
+ render(
21
+ <MultipleInput
22
+ error="test"
23
+ name="test-input"
24
+ data-testid="multiple-input"
25
+ />
26
+ );
27
+ const error = screen.getByTestId("multiple-input-error-label");
28
+ expect(error).toBeInTheDocument();
29
+ expect(error).toHaveTextContent("test");
30
+ });
31
+
32
+ it("should be disabled", () => {
33
+ render(
34
+ <MultipleInput disabled name="test-input" data-testid="multiple-input" />
35
+ );
36
+ const input = screen.getByTestId("multiple-input");
37
+ expect(input).toBeDisabled();
38
+ });
39
+
40
+ it("should be required", () => {
41
+ render(
42
+ <MultipleInput
43
+ required
44
+ name="test-input"
45
+ label="test"
46
+ data-testid="multiple-input"
47
+ />
48
+ );
49
+ const label = screen.getByTestId("multiple-input-label");
50
+ expect(label).toHaveTextContent("test *");
51
+ });
52
+
53
+ it("should change value", () => {
54
+ render(<MultipleInput name="test-input" data-testid="multiple-input" />);
55
+ const input = screen.getByTestId("multiple-input");
56
+ fireEvent.change(input, { target: { value: "test" } });
57
+ expect(input).toHaveValue("test");
58
+ });
59
+
60
+ it("should render add button", () => {
61
+ render(<MultipleInput name="test-input" data-testid="multiple-input" />);
62
+ const addButton = screen.getByRole("button");
63
+ expect(addButton).toBeInTheDocument();
64
+ });
65
+
66
+ it("should add item when add button is clicked", () => {
67
+ const mockOnAdd = jest.fn();
68
+ render(
69
+ <MultipleInput
70
+ name="test-input"
71
+ data-testid="multiple-input"
72
+ onAdd={mockOnAdd}
73
+ withoutForm
74
+ />
75
+ );
76
+
77
+ const input = screen.getByTestId("multiple-input");
78
+ const addButton = screen.getByRole("button");
79
+
80
+ fireEvent.change(input, { target: { value: "test item" } });
81
+ fireEvent.click(addButton);
82
+
83
+ expect(mockOnAdd).toHaveBeenCalledWith("test item");
84
+ });
85
+
86
+ it("should render with initial data", () => {
87
+ const initialData = ["item 1", "item 2"];
88
+
89
+ render(
90
+ <MultipleInput
91
+ name="test-input"
92
+ data={initialData}
93
+ data-testid="multiple-input"
94
+ withoutForm
95
+ />
96
+ );
97
+
98
+ expect(screen.getByText("item 1")).toBeInTheDocument();
99
+ expect(screen.getByText("item 2")).toBeInTheDocument();
100
+ });
101
+
102
+ it("should handle mask prop", () => {
103
+ render(
104
+ <MultipleInput
105
+ name="test-input"
106
+ mask="(99) 99999-9999"
107
+ data-testid="multiple-input"
108
+ />
109
+ );
110
+
111
+ const input = screen.getByTestId("multiple-input");
112
+ expect(input).toBeInTheDocument();
113
+ });
114
+
115
+ it("should remove item when remove button is clicked", () => {
116
+ const mockOnRemove = jest.fn();
117
+ const initialData = ["item 1", "item 2"];
118
+
119
+ render(
120
+ <MultipleInput
121
+ name="test-input"
122
+ data={initialData}
123
+ onRemove={mockOnRemove}
124
+ data-testid="multiple-input"
125
+ withoutForm
126
+ />
127
+ );
128
+
129
+ const removeButtons = screen.getAllByRole("button");
130
+ const removeButton = removeButtons.find((button) =>
131
+ button.querySelector('[data-icon="MdDelete"]')
132
+ );
133
+
134
+ if (removeButton) {
135
+ fireEvent.click(removeButton);
136
+ expect(mockOnRemove).toHaveBeenCalledWith(0);
137
+ }
138
+ });
139
+
140
+ it("should disable add button when input is empty", () => {
141
+ render(
142
+ <MultipleInput
143
+ name="test-input"
144
+ data-testid="multiple-input"
145
+ withoutForm
146
+ />
147
+ );
148
+
149
+ const addButton = screen.getByRole("button");
150
+ expect(addButton).toBeDisabled();
151
+ });
152
+ });
@@ -21,8 +21,12 @@ export const Default: Story = {
21
21
  name: "multipleInput",
22
22
  data: ["Item 1", "Item 2", "Item 3"],
23
23
  placeholder: "Multiple Input",
24
- onChangeData: (data: string[]) => {
25
- console.log(data);
24
+ withoutForm: true,
25
+ onAdd: (data: string) => {
26
+ console.log("Added:", data);
27
+ },
28
+ onRemove: (index: number) => {
29
+ console.log("Removed index:", index);
26
30
  },
27
31
  },
28
32
  };
@@ -34,30 +38,51 @@ export const WithError: Story = {
34
38
  error: "This is an error",
35
39
  data: ["Item 1", "Item 2", "Item 3"],
36
40
  placeholder: "Multiple Input",
41
+ withoutForm: true,
42
+ onAdd: (data: string) => {
43
+ console.log("Added:", data);
44
+ },
45
+ onRemove: (index: number) => {
46
+ console.log("Removed index:", index);
47
+ },
37
48
  },
38
49
  };
39
50
 
40
51
  export const WithForm: Story = {
41
52
  args: {
42
53
  label: "Multiple Input",
43
- name: "client.data",
54
+ name: "client",
44
55
  placeholder: "Multiple Input",
45
- mask: "999.999.999-99",
46
56
  },
47
57
  decorators: [
48
58
  (Story: React.ComponentType) => {
49
59
  const form = useForm({
50
60
  defaultValues: {
51
- client: {
52
- data: ["Item 1", "Item 2", "Item 3"],
53
- },
61
+ client: ["Item 1", "Item 2", "Item 3"],
54
62
  },
55
- errors: {
56
- client: {
57
- data: {
58
- message: "This is an error",
59
- },
60
- },
63
+ });
64
+
65
+ return (
66
+ <Form {...form}>
67
+ <Story />
68
+ </Form>
69
+ );
70
+ },
71
+ ],
72
+ };
73
+
74
+ export const WithMaskAndForm: Story = {
75
+ args: {
76
+ label: "Phone Numbers",
77
+ name: "phoneNumbers",
78
+ placeholder: "Enter phone number",
79
+ mask: "(99) 99999-9999",
80
+ },
81
+ decorators: [
82
+ (Story: React.ComponentType) => {
83
+ const form = useForm({
84
+ defaultValues: {
85
+ phoneNumbers: ["(11) 99999-9999", "(21) 88888-8888"],
61
86
  },
62
87
  });
63
88
 
@@ -0,0 +1,116 @@
1
+ import { useState, useCallback } from "react";
2
+
3
+ export type UseMaskProps = {
4
+ mask: string;
5
+ placeholder?: string; // Placeholder que mostra a máscara
6
+ showMask?: boolean; // Se deve mostrar a máscara mesmo sem valor
7
+ allowEmpty?: boolean; // Se permite valor vazio
8
+ transform?: "uppercase" | "lowercase" | "capitalize"; // Transformação do texto
9
+ };
10
+
11
+ export const useMask = ({ mask, ...options }: UseMaskProps) => {
12
+ const [value, setValue] = useState("");
13
+
14
+ const applyMask = useCallback(
15
+ (inputValue: string) => {
16
+ if (!inputValue || !mask) return "";
17
+
18
+ // Remove todos os caracteres que não são letras ou números
19
+ const cleanValue = inputValue.replace(/[^a-zA-Z0-9]/g, "");
20
+
21
+ let maskedValue = "";
22
+ let cleanIndex = 0;
23
+
24
+ for (let i = 0; i < mask.length && cleanIndex < cleanValue.length; i++) {
25
+ const maskChar = mask[i];
26
+ const inputChar = cleanValue[cleanIndex];
27
+
28
+ if (maskChar === "9") {
29
+ // Só aceita números
30
+ if (/\d/.test(inputChar)) {
31
+ maskedValue += inputChar;
32
+ cleanIndex++;
33
+ } else {
34
+ // Se não for número, pula esse caractere do input
35
+ cleanIndex++;
36
+ i--; // Volta um passo na máscara para tentar novamente
37
+ }
38
+ } else if (maskChar === "A") {
39
+ // Só aceita letras
40
+ if (/[a-zA-Z]/.test(inputChar)) {
41
+ let char = inputChar;
42
+ // Aplica transformação se especificada
43
+ if (options.transform === "uppercase") char = char.toUpperCase();
44
+ else if (options.transform === "lowercase")
45
+ char = char.toLowerCase();
46
+ maskedValue += char;
47
+ cleanIndex++;
48
+ } else {
49
+ // Se não for letra, pula esse caractere do input
50
+ cleanIndex++;
51
+ i--; // Volta um passo na máscara para tentar novamente
52
+ }
53
+ } else {
54
+ // Caractere literal da máscara (parênteses, hífen, etc.)
55
+ maskedValue += maskChar;
56
+ }
57
+ }
58
+
59
+ return maskedValue;
60
+ },
61
+ [mask, options.transform]
62
+ );
63
+
64
+ const handleSetValue = useCallback(
65
+ (newValue: string) => {
66
+ const maskedValue = applyMask(newValue);
67
+ setValue(maskedValue);
68
+ },
69
+ [applyMask]
70
+ );
71
+
72
+ // Função para obter valor sem máscara (raw)
73
+ const getRawValue = useCallback(() => {
74
+ return value.replace(/[^a-zA-Z0-9]/g, "");
75
+ }, [value]);
76
+
77
+ // Função para verificar se está completo
78
+ const isComplete = useCallback(() => {
79
+ return value.length === mask.length;
80
+ }, [value, mask]);
81
+
82
+ // Função para verificar se é válido
83
+ const isValid = useCallback(() => {
84
+ // Implementar validação específica
85
+ return isComplete() && value.length > 0;
86
+ }, [isComplete, value]);
87
+
88
+ // Função para limpar o valor
89
+ const clear = useCallback(() => {
90
+ setValue("");
91
+ }, []);
92
+
93
+ // Compatibilidade com react-hook-form
94
+ const getFormProps = useCallback(
95
+ () => ({
96
+ value,
97
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
98
+ handleSetValue(e.target.value);
99
+ },
100
+ placeholder: options.placeholder,
101
+ }),
102
+ [value, handleSetValue, options.placeholder]
103
+ );
104
+
105
+ return {
106
+ value,
107
+ setValue: handleSetValue,
108
+ rawValue: getRawValue(),
109
+ isComplete: isComplete(),
110
+ isValid: isValid(),
111
+ applyMask,
112
+ clear,
113
+ placeholder: options.placeholder || mask.replace(/[9A]/g, "_"),
114
+ formProps: getFormProps(),
115
+ };
116
+ };
@@ -0,0 +1,132 @@
1
+ # PHSA Design System
2
+
3
+ Welcome to the **PHSA Design System** - a modern and accessible React component library built with TypeScript, Tailwind CSS, and Radix UI.
4
+
5
+ ## 🎯 About the Project
6
+
7
+ The PHSA Design System is a collection of reusable components that follow best practices in design and accessibility. Our mission is to provide a solid foundation for building consistent and professional user interfaces.
8
+
9
+ ## 🚀 Technologies
10
+
11
+ - **React 19** - JavaScript library for user interfaces
12
+ - **TypeScript** - Static typing for JavaScript
13
+ - **Tailwind CSS** - Utility-first CSS framework
14
+ - **Radix UI** - Accessible primitive components
15
+ - **Lucide React** - Modern and consistent icons
16
+ - **React Hook Form** - Form management
17
+ - **Zod** - Schema validation
18
+
19
+ ## 📦 Available Components
20
+
21
+ ### Forms
22
+
23
+ - Input, Textarea, Select
24
+ - Checkbox, Switch, Radio
25
+ - DatePicker, TimePicker
26
+ - Form validation with Zod
27
+
28
+ ### Navigation
29
+
30
+ - Button, Link
31
+ - Breadcrumb
32
+ - Pagination
33
+ - Tabs
34
+
35
+ ### Feedback
36
+
37
+ - Alert, Toast
38
+ - Modal, Dialog
39
+ - Tooltip, Popover
40
+ - Loading states
41
+
42
+ ### Layout
43
+
44
+ - Card, Container
45
+ - Grid, Flex
46
+ - Divider, Spacer
47
+
48
+ ### Data
49
+
50
+ - Table
51
+ - DataTable
52
+ - Charts (in development)
53
+
54
+ ## 🎨 Theme and Customization
55
+
56
+ The design system supports light and dark themes with full customization through Tailwind CSS. All components follow a consistent design system with:
57
+
58
+ - **Colors**: Semantic and accessible color palette
59
+ - **Typography**: Clear text hierarchy
60
+ - **Spacing**: Consistent spacing system
61
+ - **Borders**: Standardized border radius
62
+ - **Shadows**: Elevation system
63
+
64
+ ## ♿ Accessibility
65
+
66
+ All components are built with accessibility in mind:
67
+
68
+ - Full keyboard navigation support
69
+ - Screen reader compatibility
70
+ - Adequate color contrast
71
+ - Visible focus states
72
+ - Appropriate ARIA labels
73
+
74
+ ## 🛠️ How to Use
75
+
76
+ ### Installation
77
+
78
+ ```bash
79
+ npm install @phsa.tec/design-system-react
80
+ # or
81
+ yarn add @phsa.tec/design-system-react
82
+ ```
83
+
84
+ ### Basic Setup
85
+
86
+ ```tsx
87
+ import { Button, Input, Card } from "@phsa.tec/design-system-react";
88
+
89
+ function App() {
90
+ return (
91
+ <div>
92
+ <Card>
93
+ <Input placeholder="Type something..." />
94
+ <Button>Click here</Button>
95
+ </Card>
96
+ </div>
97
+ );
98
+ }
99
+ ```
100
+
101
+ ### Theme Configuration
102
+
103
+ ```tsx
104
+ import { ThemeProvider } from "@phsa.tec/design-system-react";
105
+
106
+ function App() {
107
+ return (
108
+ <ThemeProvider defaultTheme="light" storageKey="phsa-theme">
109
+ {/* Your components here */}
110
+ </ThemeProvider>
111
+ );
112
+ }
113
+ ```
114
+
115
+ ## 📚 Documentation
116
+
117
+ - **Components**: Explore all available components
118
+ - **Examples**: See practical usage examples
119
+ - **API**: Complete documentation of props and methods
120
+ - **Style Guide**: Design patterns and best practices
121
+
122
+ ## 🤝 Contributing
123
+
124
+ Contributions are welcome! Please read our contribution guide before submitting pull requests.
125
+
126
+ ## 📄 License
127
+
128
+ This project is licensed under the MIT License.
129
+
130
+ ---
131
+
132
+ **Need help?** Open an issue on GitHub or contact our team.