@usefui/components 1.5.1 → 1.5.2

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 (41) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/index.d.mts +230 -9
  3. package/dist/index.d.ts +230 -9
  4. package/dist/index.js +1131 -309
  5. package/dist/index.mjs +1085 -276
  6. package/package.json +6 -6
  7. package/src/avatar/index.tsx +1 -1
  8. package/src/badge/index.tsx +3 -3
  9. package/src/breadcrumb/Breadcrumb.stories.tsx +36 -0
  10. package/src/breadcrumb/index.tsx +117 -0
  11. package/src/breadcrumb/styles/index.ts +19 -0
  12. package/src/button/Button.stories.tsx +80 -8
  13. package/src/button/index.tsx +67 -5
  14. package/src/button/styles/index.ts +82 -10
  15. package/src/card/Card.stories.tsx +57 -0
  16. package/src/card/index.tsx +55 -0
  17. package/src/card/styles/index.ts +72 -0
  18. package/src/copy-button/CopyButton.stories.tsx +29 -0
  19. package/src/copy-button/index.tsx +101 -0
  20. package/src/dialog/index.tsx +1 -1
  21. package/src/dropdown/index.tsx +1 -1
  22. package/src/field/Field.stories.tsx +39 -8
  23. package/src/field/index.tsx +5 -0
  24. package/src/field/styles/index.ts +37 -12
  25. package/src/index.ts +8 -0
  26. package/src/page/index.tsx +1 -1
  27. package/src/privacy-field/PrivacyField.stories.tsx +29 -0
  28. package/src/privacy-field/index.tsx +56 -0
  29. package/src/privacy-field/styles/index.ts +17 -0
  30. package/src/resizable/Resizable.stories.tsx +40 -0
  31. package/src/resizable/index.tsx +108 -0
  32. package/src/resizable/styles/index.ts +65 -0
  33. package/src/skeleton/Skeleton.stories.tsx +32 -0
  34. package/src/skeleton/index.tsx +43 -0
  35. package/src/skeleton/styles/index.ts +56 -0
  36. package/src/spinner/Spinner.stories.tsx +27 -0
  37. package/src/spinner/index.tsx +19 -0
  38. package/src/spinner/styles/index.ts +43 -0
  39. package/src/text-area/Textarea.stories.tsx +27 -0
  40. package/src/text-area/index.tsx +62 -0
  41. package/src/text-area/styles/index.ts +124 -0
@@ -0,0 +1,55 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { CardContainer, CardWrapper, CardsGrid } from "./styles";
5
+
6
+ import type { IComponentSize, TComponentShape } from "../../../../types";
7
+
8
+ export interface CardComposition {
9
+ Body: typeof CardBody;
10
+ Meta: typeof CardMeta;
11
+ Grid: typeof CardGrid;
12
+ }
13
+
14
+ interface CardGridProps extends React.ComponentProps<"div">, IComponentSize {}
15
+ interface CardProps extends React.ComponentProps<"div"> {
16
+ shape?: TComponentShape;
17
+ }
18
+
19
+ const CardGrid = (props: CardGridProps) => {
20
+ const { sizing = "medium", children } = props;
21
+
22
+ return <CardsGrid data-size={sizing}>{children}</CardsGrid>;
23
+ };
24
+ CardGrid.displayName = "Card.Grid";
25
+
26
+ const CardMeta = (props: React.ComponentProps<"div">) => {
27
+ const { children } = props;
28
+
29
+ return (
30
+ <div className="p-y-medium-20 p-x-medium-30" {...props}>
31
+ {children}
32
+ </div>
33
+ );
34
+ };
35
+ CardMeta.displayName = "Card.Meta";
36
+
37
+ const CardBody = (props: CardProps) => {
38
+ const { shape = "smooth", children } = props;
39
+
40
+ return <CardWrapper data-shape={shape}>{children}</CardWrapper>;
41
+ };
42
+ CardBody.displayName = "Card.Body";
43
+
44
+ const Card = (props: CardProps) => {
45
+ const { shape = "smooth", children } = props;
46
+
47
+ return <CardContainer data-shape={shape}> {children}</CardContainer>;
48
+ };
49
+ Card.displayName = "Card";
50
+
51
+ Card.Grid = CardGrid;
52
+ Card.Meta = CardMeta;
53
+ Card.Body = CardBody;
54
+
55
+ export { Card, CardGrid, CardBody, CardMeta };
@@ -0,0 +1,72 @@
1
+ "use client";
2
+
3
+ import styled, { css } from "styled-components";
4
+
5
+ const GridDefaultStyles = css`
6
+ display: grid;
7
+ grid-gap: var(--measurement-medium-30) var(--measurement-medium-30);
8
+ box-sizing: border-box;
9
+
10
+ @media (max-width: 768px) {
11
+ grid-template-columns: repeat(auto-fill, minmax(100%, 1fr));
12
+ }
13
+ `;
14
+
15
+ const GridSizeStyles = css`
16
+ &[data-size="small"] {
17
+ grid-template-columns: repeat(
18
+ auto-fill,
19
+ minmax(var(--measurement-large-80), 1fr)
20
+ );
21
+ }
22
+ &[data-size="medium"] {
23
+ grid-template-columns: repeat(
24
+ auto-fill,
25
+ minmax(var(--measurement-large-90), 1fr)
26
+ );
27
+ }
28
+ &[data-size="large"] {
29
+ grid-template-columns: repeat(
30
+ auto-fill,
31
+ minmax(calc(var(--measurement-large-90) * 1.2), 1fr)
32
+ );
33
+ }
34
+ `;
35
+ const CardShapeStyles = css`
36
+ &[data-shape="square"] {
37
+ border-radius: 0;
38
+ }
39
+ &[data-shape="smooth"] {
40
+ border-radius: var(--measurement-medium-30);
41
+ }
42
+ &[data-shape="round"] {
43
+ border-radius: var(--measurement-medium-60);
44
+ }
45
+ `;
46
+ const CardDefaultStyles = css`
47
+ ${CardShapeStyles}
48
+
49
+ box-sizing: border-box;
50
+ `;
51
+
52
+ export const CardContainer = styled.div`
53
+ width: 100%;
54
+ background-color: var(--font-color-alpha-10);
55
+
56
+ ${CardDefaultStyles}
57
+ `;
58
+ export const CardWrapper = styled.div`
59
+ display: flex;
60
+ flex-direction: column;
61
+ gap: var(--measurement-large-10);
62
+ padding: var(--measurement-medium-60);
63
+ background-color: var(--contrast-color);
64
+ border: var(--measurement-small-10) solid var(--font-color-alpha-10);
65
+
66
+ ${CardDefaultStyles}
67
+ `;
68
+
69
+ export const CardsGrid = styled.div`
70
+ ${GridDefaultStyles}
71
+ ${GridSizeStyles}
72
+ `;
@@ -0,0 +1,29 @@
1
+ import React from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import { Page, CopyButton } from "..";
4
+
5
+ /**
6
+ * CopyButton are used to copy stored values when clicked.
7
+ */
8
+ const meta = {
9
+ title: "Components/CopyButton",
10
+ component: CopyButton,
11
+ tags: ["autodocs"],
12
+ } satisfies Meta<typeof CopyButton>;
13
+ export default meta;
14
+
15
+ type Story = StoryObj<typeof meta>;
16
+ export const Default: Story = {
17
+ args: {
18
+ value: "Hello, world!",
19
+ },
20
+ render: ({ ...args }) => (
21
+ <Page>
22
+ <Page.Content className="p-large-30">
23
+ <CopyButton variant="border" sizing="medium" {...args}>
24
+ Click to copy
25
+ </CopyButton>
26
+ </Page.Content>
27
+ </Page>
28
+ ),
29
+ };
@@ -0,0 +1,101 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+
5
+ import { Tooltip, Button } from "../";
6
+ import type { IButtonProperties } from "../";
7
+
8
+ type TooltipValueProperties = {
9
+ copyLabel?: string;
10
+ copiedLabel?: string;
11
+ };
12
+ interface CopyButtonProperties extends IButtonProperties {
13
+ delay?: number;
14
+ value: string;
15
+ tooltip?: TooltipValueProperties;
16
+ }
17
+
18
+ /**
19
+ * CopyButton are used to copy stored values when clicked.
20
+ *
21
+ * @param {IButtonProperties} props - The props for the CopyButton component.
22
+ * @param {boolean} props.raw - Define whether the component is styled or not.
23
+ * @param {boolean} props.rawicon - Define whether the component is styles its svg children.
24
+ * @param {ComponentSizeEnum} props.sizing - The size of the component. Defaults to `medium`.
25
+ * @param {string} props.variant - The style definition used by the component.
26
+ * @param {string} props.value - The value copied when the CopyButton is clicked.
27
+ * @param {number} props.delay - The delay used to rendered the state change.
28
+ * @param {TooltipValueProperties} props.tooltip - The values used to convey the copy state.
29
+ * @param {ReactNode} props.children - The content to be rendered inside the CopyButton.
30
+ * @returns {ReactElement} The CopyButton component.
31
+ */
32
+ export const CopyButton = ({
33
+ value,
34
+ delay,
35
+ tooltip,
36
+ children,
37
+ ...restProps
38
+ }: CopyButtonProperties) => {
39
+ const timerRef = React.useRef<number | null>(null);
40
+ const [copied, setCopied] = React.useState(false);
41
+
42
+ const tooltipLabels = React.useMemo(() => {
43
+ return {
44
+ copy: tooltip?.copyLabel ?? "Copy",
45
+ copied: tooltip?.copiedLabel ?? "Copied!",
46
+ };
47
+ }, [tooltip]);
48
+
49
+ const copyToClipboard = async () => {
50
+ if (value == null) return;
51
+
52
+ try {
53
+ await navigator.clipboard.writeText(value);
54
+ setCopied(true);
55
+ } catch {
56
+ // Best-effort fallback for older browsers
57
+ const ta = document.createElement("textarea");
58
+ ta.value = value;
59
+ ta.style.position = "fixed";
60
+ ta.style.opacity = "0";
61
+ document.body.appendChild(ta);
62
+ ta.select();
63
+
64
+ try {
65
+ document.execCommand("copy");
66
+ setCopied(true);
67
+ } finally {
68
+ document.body.removeChild(ta);
69
+ }
70
+ }
71
+
72
+ if (timerRef.current != null) window.clearTimeout(timerRef.current);
73
+ timerRef.current = window.setTimeout(() => setCopied(false), delay ?? 1000);
74
+ };
75
+
76
+ React.useEffect(() => {
77
+ return () => {
78
+ if (timerRef.current != null) {
79
+ window.clearTimeout(timerRef.current);
80
+ timerRef.current = null;
81
+ }
82
+ };
83
+ }, []);
84
+
85
+ return (
86
+ <Tooltip content={copied ? tooltipLabels.copied : tooltipLabels.copy}>
87
+ <Button
88
+ data-testId="copy-code"
89
+ aria-label="copy-code"
90
+ disabled={value == null}
91
+ aria-disabled={value == null}
92
+ variant={restProps?.variant ?? "ghost"}
93
+ onClick={copyToClipboard}
94
+ {...restProps}
95
+ >
96
+ {children}
97
+ </Button>
98
+ </Tooltip>
99
+ );
100
+ };
101
+ CopyButton.displayName = "CopyButton";
@@ -38,7 +38,7 @@ export interface IDialogItemProperties
38
38
  *
39
39
  * @param {IDialogItemProperties} props - The props for the Dialog component.
40
40
  * @param {boolean} props.raw - Define whether the component is styled or not.
41
- * @param {ComponentSizeEnum} props.sizing - The size of the component.
41
+ * @param {ComponentSizeEnum} props.sizing - The size of the component. Defaults to "medium".
42
42
  * @param {boolean} props.open - Whether the dialog is open or not.
43
43
  * @param {ReactNode} props.children - The content to be rendered inside the dialog.
44
44
  * @returns {ReactElement} The Dialog component.
@@ -132,7 +132,7 @@ DropdownMenuTrigger.displayName = "DropdownMenu.Trigger";
132
132
  *
133
133
  * @param {IDropdownContentProperties} props - The props for the DropdownMenu.Content component.
134
134
  * @param {boolean} props.raw - Define whether the component is styled or not.
135
- * @param {ComponentSizeEnum} props.sizing - The size of the component.
135
+ * @param {ComponentSizeEnum} props.sizing - The size of the component. Defaults to "medium".
136
136
  * @param {boolean} props.defaultOpen - The initial open state of the dropdown menu. Defaults to false.
137
137
  * @param {ReactNode} props.children - The content to be rendered inside the dropdown menu.
138
138
  * @returns {ReactElement} The DropdownMenu.Content component.
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
  import type { Meta, StoryObj } from "@storybook/react";
3
3
 
4
- import { Field } from "..";
4
+ import { Field, Page } from "..";
5
5
  import { ComponentVariantEnum, ComponentSizeEnum } from "../../../../types";
6
6
 
7
7
  const meta = {
@@ -131,16 +131,47 @@ export const Sizes = {
131
131
  );
132
132
  },
133
133
  };
134
+ export const Shapes = {
135
+ render: ({ ...args }) => {
136
+ return (
137
+ <Page>
138
+ <Page.Content>
139
+ <div className="flex align-center justify-center flex-wrap h-100 g-medium-30">
140
+ {["square", "smooth", "round"].map((item) => (
141
+ <Field.Root key={item}>
142
+ <Field
143
+ shape={item}
144
+ sizing="medium"
145
+ placeholder={item}
146
+ variant="secondary"
147
+ />
148
+ </Field.Root>
149
+ ))}
150
+ </div>
151
+ </Page.Content>
152
+ </Page>
153
+ );
154
+ },
155
+ };
134
156
  export const Variants = {
135
157
  render: ({ ...args }) => {
136
158
  return (
137
- <div className="flex g-medium-30">
138
- {["primary", "secondary", "ghost"].map((item) => (
139
- <Field.Root key={item}>
140
- <Field placeholder={item} variant={item} />
141
- </Field.Root>
142
- ))}
143
- </div>
159
+ <Page>
160
+ <Page.Content>
161
+ <div className="flex align-center justify-center flex-wrap h-100 g-medium-30">
162
+ {["primary", "secondary", "ghost"].map((item) => (
163
+ <Field.Root key={item}>
164
+ <Field
165
+ shape="smooth"
166
+ sizing="medium"
167
+ placeholder={item}
168
+ variant={item}
169
+ />
170
+ </Field.Root>
171
+ ))}
172
+ </div>
173
+ </Page.Content>
174
+ </Page>
144
175
  );
145
176
  },
146
177
  };
@@ -10,6 +10,7 @@ import {
10
10
  IComponentSize,
11
11
  ComponentVariantEnum,
12
12
  IComponentVariant,
13
+ TComponentShape,
13
14
  } from "../../../../types";
14
15
 
15
16
  export enum MetaVariantEnum {
@@ -26,6 +27,7 @@ export interface IField
26
27
  IComponentSize,
27
28
  IComponentVariant,
28
29
  IComponentStyling {
30
+ shape?: TComponentShape;
29
31
  hint?: string;
30
32
  error?: string;
31
33
  }
@@ -58,6 +60,7 @@ export interface IFieldComposition {
58
60
  * @param {boolean} props.raw - Define whether the component is styled or not.
59
61
  * @param {ComponentSizeEnum} props.sizing - The size of the component. Defaults to `medium`.
60
62
  * @param {string} props.variant - The style definition used by the component.
63
+ * @param {TComponentShape} props.shape - The size of the component. Defaults to `smooth`.
61
64
  * @param {string} props.error - The error message to display.
62
65
  * @param {string} props.hint - The hint message to display.
63
66
  * @returns {ReactElement} The Field component.
@@ -67,6 +70,7 @@ const Field = (props: IField) => {
67
70
  raw,
68
71
  sizing = ComponentSizeEnum.Medium,
69
72
  variant = ComponentVariantEnum.Primary,
73
+ shape = "smooth",
70
74
  error,
71
75
  hint,
72
76
  ...restProps
@@ -85,6 +89,7 @@ const Field = (props: IField) => {
85
89
  data-error={Boolean(error)}
86
90
  data-variant={variant}
87
91
  data-size={sizing}
92
+ data-shape={shape}
88
93
  data-raw={Boolean(raw)}
89
94
  {...restProps}
90
95
  />
@@ -1,21 +1,20 @@
1
1
  import styled, { css } from "styled-components";
2
2
 
3
- const FieldDefaultStyles = css`
3
+ export const FieldDefaultStyles = css`
4
4
  outline: none;
5
- cursor: pointer;
5
+ cursor: text;
6
6
  display: flex;
7
7
  align-items: center;
8
8
  justify-content: center;
9
9
 
10
- font-size: var(--fontsize-small-80);
11
- font-weight: 500;
10
+ font-size: var(--fontsize-medium-20);
11
+
12
12
  line-height: 1.1;
13
13
  letter-spacing: calc(
14
14
  var(--fontsize-small-10) - ((var(--fontsize-small-10) * 1.066))
15
15
  );
16
16
 
17
17
  border: var(--measurement-small-10) solid transparent;
18
- border-radius: var(--measurement-medium-30);
19
18
  backdrop-filter: blur(var(--measurement-small-10));
20
19
  color: var(--font-color-alpha-60);
21
20
  width: fit-content;
@@ -33,29 +32,37 @@ const FieldDefaultStyles = css`
33
32
  &:focus,
34
33
  &:active {
35
34
  color: var(--font-color);
36
-
37
35
  svg,
38
36
  span,
39
37
  img {
40
38
  opacity: 1;
41
39
  }
42
40
  }
41
+
43
42
  &::placeholder {
44
43
  color: var(--font-color-alpha-30);
45
44
  }
45
+
46
46
  &:disabled {
47
47
  cursor: not-allowed;
48
48
  opacity: 0.6;
49
49
  }
50
50
  `;
51
- const FieldVariantsStyles = css`
51
+ export const FieldVariantsStyles = css`
52
52
  &[data-variant="primary"] {
53
53
  background-color: var(--font-color-alpha-10);
54
+ border-color: var(--font-color-alpha-10);
55
+
56
+ &:focus,
57
+ &:active {
58
+ box-shadow: 0 0 0 var(--measurement-small-30) var(--font-color-alpha-10);
59
+ }
54
60
 
55
61
  &[data-error="true"] {
56
62
  color: var(--color-red);
57
63
  background-color: var(--alpha-red-10);
58
64
  border-color: var(--alpha-red-10);
65
+ box-shadow: 0 0 0 var(--measurement-small-30) var(--alpha-red-10);
59
66
  }
60
67
  }
61
68
 
@@ -66,7 +73,12 @@ const FieldVariantsStyles = css`
66
73
  &:hover,
67
74
  &:focus,
68
75
  &:active {
69
- background-color: var(--font-color-alpha-10);
76
+ border-color: var(--font-color-alpha-20);
77
+ }
78
+
79
+ &:focus,
80
+ &:active {
81
+ box-shadow: 0 0 0 var(--measurement-small-30) var(--font-color-alpha-10);
70
82
  }
71
83
 
72
84
  &[data-error="true"] {
@@ -77,6 +89,7 @@ const FieldVariantsStyles = css`
77
89
  &:focus,
78
90
  &:active {
79
91
  background-color: var(--alpha-red-10);
92
+ box-shadow: 0 0 0 var(--measurement-small-30) var(--alpha-red-10);
80
93
  }
81
94
  }
82
95
  }
@@ -100,26 +113,37 @@ const FieldVariantsStyles = css`
100
113
  }
101
114
  }
102
115
  `;
103
- const FieldSizeStyles = css`
116
+ export const FieldSizeStyles = css`
104
117
  &[data-size="small"] {
105
- gap: var(--measurement-medium-10);
118
+ font-size: var(--fontsize-small-60);
119
+
106
120
  padding: 0 var(--measurement-medium-30);
107
121
  min-width: var(--measurement-medium-60);
108
122
  min-height: var(--measurement-medium-80);
109
123
  }
110
124
  &[data-size="medium"] {
111
- gap: var(--measurement-medium-30);
112
125
  padding: 0 var(--measurement-medium-30);
113
126
  min-width: var(--measurement-medium-90);
114
127
  min-height: var(--measurement-medium-90);
115
128
  width: fit-content;
116
129
  }
117
130
  &[data-size="large"] {
118
- padding: var(--measurement-medium-20) var(--measurement-medium-40);
131
+ padding: var(--measurement-medium-50);
119
132
  min-width: var(--measurement-medium-90);
120
133
  min-height: var(--measurement-medium-90);
121
134
  }
122
135
  `;
136
+ export const FieldShapeStyles = css`
137
+ &[data-shape="square"] {
138
+ border-radius: 0;
139
+ }
140
+ &[data-shape="smooth"] {
141
+ border-radius: var(--measurement-medium-20);
142
+ }
143
+ &[data-shape="round"] {
144
+ border-radius: var(--measurement-large-90);
145
+ }
146
+ `;
123
147
 
124
148
  export const Fieldset = styled.fieldset<any>`
125
149
  all: unset;
@@ -134,6 +158,7 @@ export const Input = styled.input<any>`
134
158
  ${FieldDefaultStyles}
135
159
  ${FieldVariantsStyles}
136
160
  ${FieldSizeStyles}
161
+ ${FieldShapeStyles}
137
162
 
138
163
  &[data-error="true"] {
139
164
  &::placeholder {
package/src/index.ts CHANGED
@@ -2,8 +2,11 @@ export * from "./accordion";
2
2
  export * from "./avatar";
3
3
  export * from "./badge";
4
4
  export * from "./button";
5
+ export * from "./breadcrumb";
6
+ export * from "./card";
5
7
  export * from "./checkbox";
6
8
  export * from "./collapsible";
9
+ export * from "./copy-button";
7
10
  export * from "./dialog";
8
11
  export * from "./divider";
9
12
  export * from "./dropdown";
@@ -12,11 +15,16 @@ export * from "./otp-field";
12
15
  export * from "./overlay";
13
16
  export * from "./page";
14
17
  export * from "./portal";
18
+ export * from "./privacy-field";
19
+ export * from "./resizable";
15
20
  export * from "./sheet";
16
21
  export * from "./scrollarea";
22
+ export * from "./spinner";
23
+ export * from "./skeleton";
17
24
  export * from "./switch";
18
25
  export * from "./table";
19
26
  export * from "./tabs";
27
+ export * from "./text-area";
20
28
  export * from "./toggle";
21
29
  export * from "./toolbar";
22
30
  export * from "./tooltip";
@@ -73,7 +73,7 @@ PageNavigation.displayName = "Page.Navigation";
73
73
  * @param {string} props.shortcut - The key combination used as keyboard shortcuts to trigger the Page.Toolbar.
74
74
  * @param {string} props.hotkey - The key to use in the key combination for the keyboard shortcuts.
75
75
  * @param {KeyBindingEnum} props.bindkey - The modifier key to use in the key combination.
76
- * @param {ComponentSizeEnum} props.sizing - The size of the Page.Toolbar.
76
+ * @param {ComponentSizeEnum} props.sizing - The size of the Page.Toolbar. Defaults to "medium".
77
77
  * @param {ComponentHeightEnum} props.height - The height definition of the Page.Toolbar.
78
78
  * @param {TComponentSide} props.side - The side of the Page.Toolbar.
79
79
  * @param {boolean} props.defaultOpen - Whether the Page.Toolbar should be open by default.
@@ -0,0 +1,29 @@
1
+ import React from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import { Page, PrivacyField } from "..";
4
+
5
+ /**
6
+ * PrivacyFields are used to hide sensitive values typed by users.
7
+ */
8
+ const meta = {
9
+ title: "Components/PrivacyField",
10
+ component: PrivacyField,
11
+ tags: ["autodocs"],
12
+ } satisfies Meta<typeof PrivacyField>;
13
+ export default meta;
14
+
15
+ type Story = StoryObj<typeof meta>;
16
+ export const Default: Story = {
17
+ args: {
18
+ variant: "secondary",
19
+ textIcon: <p className="fs-small-60">txt</p>,
20
+ passwordIcon: <p className="fs-small-60">pwd</p>,
21
+ },
22
+ render: ({ ...args }) => (
23
+ <Page>
24
+ <Page.Content className="p-large-30">
25
+ <PrivacyField {...args} />
26
+ </Page.Content>
27
+ </Page>
28
+ ),
29
+ };
@@ -0,0 +1,56 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+
5
+ import { Field } from "../";
6
+ import { Wrapper, Trigger } from "./styles";
7
+
8
+ import type { IField } from "../";
9
+
10
+ type PrivacyType = "password" | "text";
11
+ interface PrivacyFieldProps extends IField {
12
+ defaultType?: PrivacyType;
13
+ textIcon: React.ReactNode;
14
+ passwordIcon: React.ReactNode;
15
+ }
16
+
17
+ /**
18
+ * PrivacyFields are used to hide sensitive values typed by users.
19
+ *
20
+ * @param {PrivacyFieldProps} props - The props for the PrivacyField component.
21
+ * @param {boolean} props.raw - Define whether the component is styled or not.
22
+ * @param {ComponentSizeEnum} props.sizing - The size of the component. Defaults to `medium`.
23
+ * @param {string} props.variant - The style definition used by the component.
24
+ * @param {string} props.error - The error message to display.
25
+ * @param {string} props.hint - The hint message to display.
26
+ * @param {string} props.defaultType - The type of the PrivacyField when rendered.
27
+ * @param {ReactElement} props.textIcon - The Icon used to convey the text type information.
28
+ * @param {ReactElement} props.passwordIcon - The Icon used to convey the password type information.
29
+ * @returns {ReactElement} The PrivacyField component.
30
+ */
31
+ export const PrivacyField = ({
32
+ defaultType,
33
+ textIcon,
34
+ passwordIcon,
35
+ ...restProps
36
+ }: PrivacyFieldProps) => {
37
+ const [type, setType] = React.useState<PrivacyType>(
38
+ defaultType ?? "password"
39
+ );
40
+
41
+ const handleChangeType = React.useCallback(() => {
42
+ if (type === "text") setType("password");
43
+ if (type === "password") setType("text");
44
+ }, [type, setType]);
45
+
46
+ return (
47
+ <Wrapper className="flex">
48
+ <Field autoComplete="off" type={type} {...restProps} />
49
+ <Trigger variant="ghost" sizing="small" onClick={handleChangeType}>
50
+ {type === "text" && textIcon}
51
+ {type === "password" && passwordIcon}
52
+ </Trigger>
53
+ </Wrapper>
54
+ );
55
+ };
56
+ PrivacyField.displayName = "PrivacyField";
@@ -0,0 +1,17 @@
1
+ "use client";
2
+
3
+ import styled from "styled-components";
4
+ import { Button, Field } from "../../";
5
+
6
+ export const Wrapper = styled(Field.Wrapper)`
7
+ position: relative;
8
+
9
+ input {
10
+ width: 100% !important;
11
+ }
12
+ `;
13
+ export const Trigger = styled(Button)`
14
+ position: absolute !important;
15
+ right: var(--measurement-medium-50);
16
+ top: calc(var(--measurement-medium-50));
17
+ `;