@usefui/components 1.5.3 → 1.7.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 (92) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/index.d.mts +615 -51
  3. package/dist/index.d.ts +615 -51
  4. package/dist/index.js +3154 -660
  5. package/dist/index.mjs +3131 -661
  6. package/package.json +12 -12
  7. package/src/__tests__/Avatar.test.tsx +55 -55
  8. package/src/__tests__/MessageBubble.test.tsx +179 -0
  9. package/src/__tests__/Shimmer.test.tsx +122 -0
  10. package/src/__tests__/Tree.test.tsx +275 -0
  11. package/src/accordion/Accordion.stories.tsx +6 -4
  12. package/src/accordion/hooks/index.tsx +3 -1
  13. package/src/accordion/index.tsx +1 -2
  14. package/src/avatar/Avatar.stories.tsx +37 -7
  15. package/src/avatar/index.tsx +90 -19
  16. package/src/avatar/styles/index.ts +58 -12
  17. package/src/badge/Badge.stories.tsx +27 -5
  18. package/src/badge/index.tsx +21 -14
  19. package/src/badge/styles/index.ts +69 -40
  20. package/src/button/Button.stories.tsx +40 -27
  21. package/src/button/index.tsx +13 -9
  22. package/src/button/styles/index.ts +308 -47
  23. package/src/card/index.tsx +2 -4
  24. package/src/checkbox/Checkbox.stories.tsx +72 -33
  25. package/src/checkbox/hooks/index.tsx +5 -1
  26. package/src/checkbox/index.tsx +8 -6
  27. package/src/checkbox/styles/index.ts +239 -19
  28. package/src/collapsible/Collapsible.stories.tsx +6 -4
  29. package/src/collapsible/hooks/index.tsx +3 -1
  30. package/src/dialog/Dialog.stories.tsx +173 -31
  31. package/src/dialog/hooks/index.tsx +5 -1
  32. package/src/dialog/styles/index.ts +15 -8
  33. package/src/dropdown/Dropdown.stories.tsx +61 -23
  34. package/src/dropdown/hooks/index.tsx +3 -1
  35. package/src/dropdown/index.tsx +51 -40
  36. package/src/dropdown/styles/index.ts +30 -19
  37. package/src/field/Field.stories.tsx +183 -24
  38. package/src/field/hooks/index.tsx +5 -1
  39. package/src/field/index.tsx +930 -13
  40. package/src/field/styles/index.ts +246 -14
  41. package/src/field/types/index.ts +31 -0
  42. package/src/field/utils/index.ts +201 -0
  43. package/src/index.ts +8 -1
  44. package/src/message-bubble/MessageBubble.stories.tsx +138 -0
  45. package/src/message-bubble/hooks/index.tsx +41 -0
  46. package/src/message-bubble/index.tsx +171 -0
  47. package/src/message-bubble/styles/index.ts +58 -0
  48. package/src/otp-field/OTPField.stories.tsx +22 -24
  49. package/src/otp-field/hooks/index.tsx +3 -1
  50. package/src/otp-field/index.tsx +14 -3
  51. package/src/otp-field/styles/index.ts +114 -16
  52. package/src/otp-field/types/index.ts +9 -1
  53. package/src/overlay/styles/index.ts +1 -0
  54. package/src/ruler/Ruler.stories.tsx +43 -0
  55. package/src/ruler/constants/index.ts +3 -0
  56. package/src/ruler/hooks/index.tsx +53 -0
  57. package/src/ruler/index.tsx +239 -0
  58. package/src/ruler/styles/index.tsx +154 -0
  59. package/src/ruler/types/index.ts +17 -0
  60. package/src/select/Select.stories.tsx +91 -0
  61. package/src/select/hooks/index.tsx +71 -0
  62. package/src/select/index.tsx +331 -0
  63. package/src/select/styles/index.tsx +156 -0
  64. package/src/sheet/hooks/index.tsx +5 -1
  65. package/src/shimmer/Shimmer.stories.tsx +97 -0
  66. package/src/shimmer/index.tsx +64 -0
  67. package/src/shimmer/styles/index.ts +33 -0
  68. package/src/skeleton/index.tsx +7 -6
  69. package/src/spinner/Spinner.stories.tsx +29 -4
  70. package/src/spinner/index.tsx +16 -6
  71. package/src/spinner/styles/index.ts +41 -22
  72. package/src/switch/Switch.stories.tsx +46 -17
  73. package/src/switch/hooks/index.tsx +5 -1
  74. package/src/switch/index.tsx +5 -8
  75. package/src/switch/styles/index.ts +45 -45
  76. package/src/tabs/Tabs.stories.tsx +43 -15
  77. package/src/tabs/hooks/index.tsx +5 -1
  78. package/src/text-area/Textarea.stories.tsx +45 -8
  79. package/src/text-area/index.tsx +9 -6
  80. package/src/text-area/styles/index.ts +1 -1
  81. package/src/toggle/Toggle.stories.tsx +6 -4
  82. package/src/toolbar/hooks/index.tsx +5 -1
  83. package/src/tree/Tree.stories.tsx +141 -0
  84. package/src/tree/hooks/tree-node-provider.tsx +50 -0
  85. package/src/tree/hooks/tree-provider.tsx +75 -0
  86. package/src/tree/index.tsx +231 -0
  87. package/src/tree/styles/index.ts +23 -0
  88. package/tsconfig.build.json +20 -0
  89. package/tsconfig.json +1 -3
  90. package/src/privacy-field/PrivacyField.stories.tsx +0 -29
  91. package/src/privacy-field/index.tsx +0 -56
  92. package/src/privacy-field/styles/index.ts +0 -17
@@ -0,0 +1,275 @@
1
+ import React from "react";
2
+
3
+ import { test, vi, afterEach, describe, expect } from "vitest";
4
+ import {
5
+ screen,
6
+ render,
7
+ cleanup,
8
+ waitFor,
9
+ fireEvent,
10
+ } from "@testing-library/react";
11
+ import { axe, toHaveNoViolations } from "jest-axe";
12
+
13
+ import { Tree } from "../../src/tree";
14
+
15
+ afterEach(async () => {
16
+ vi.clearAllMocks();
17
+ vi.resetModules();
18
+ cleanup();
19
+ });
20
+
21
+ expect.extend(toHaveNoViolations);
22
+
23
+ describe("Tree", () => {
24
+ test("Renders without accessibility violation", async () => {
25
+ const { container } = render(
26
+ <Tree.Root>
27
+ <Tree>
28
+ <Tree.Node nodeId="node-1">
29
+ <Tree.Trigger nodeId="node-1">Node 1</Tree.Trigger>
30
+ <Tree.Content nodeId="node-1" defaultOpen>
31
+ <Tree.Node level={1} nodeId="node-1-child">
32
+ <Tree.Trigger nodeId="node-1-child">Node 1 Child</Tree.Trigger>
33
+ </Tree.Node>
34
+ </Tree.Content>
35
+ </Tree.Node>
36
+ </Tree>
37
+ </Tree.Root>,
38
+ );
39
+
40
+ const ComponentContainer = await axe(container);
41
+ expect(ComponentContainer).toHaveNoViolations();
42
+ });
43
+
44
+ // test("Renders with accessibility definition", async () => {
45
+ // render(
46
+ // <Tree.Root>
47
+ // <Tree>
48
+ // <Tree.Node nodeId="node-1">
49
+ // <Tree.Trigger nodeId="node-1">Node 1</Tree.Trigger>
50
+ // <Tree.Content nodeId="node-1">
51
+ // <Tree.Node level={1} nodeId="node-1-child">
52
+ // <Tree.Trigger nodeId="node-1-child">Node 1 Child</Tree.Trigger>
53
+ // </Tree.Node>
54
+ // </Tree.Content>
55
+ // </Tree.Node>
56
+ // </Tree>
57
+ // </Tree.Root>,
58
+ // );
59
+
60
+ // const treeView = screen.getByRole("tree");
61
+ // expect(treeView).toBeDefined();
62
+
63
+ // const treeItems = screen.getAllByRole("treeitem");
64
+ // expect(treeItems.length).toBe(1);
65
+
66
+ // expect(() => screen.getByRole("group")).toThrow();
67
+
68
+ // const trigger = screen.getByLabelText("node-1-action");
69
+ // expect(trigger).toBeDefined();
70
+ // expect(trigger.getAttribute("aria-expanded")).toBe("false");
71
+ // expect(trigger.getAttribute("aria-selected")).toBe("false");
72
+ // expect(trigger.getAttribute("data-state")).toBe("collapsed");
73
+
74
+ // fireEvent.click(trigger);
75
+ // await waitFor(() => {
76
+ // const group = screen.getByRole("group");
77
+ // expect(group).toBeDefined();
78
+ // expect(group.getAttribute("aria-labelledby")).toBeDefined();
79
+ // expect(group.getAttribute("id")).toBeDefined();
80
+ // expect(group.getAttribute("data-nodeid")).toBe("node-1");
81
+
82
+ // expect(trigger.getAttribute("aria-expanded")).toBe("true");
83
+ // expect(trigger.getAttribute("aria-selected")).toBe("true");
84
+ // expect(trigger.getAttribute("data-state")).toBe("expanded");
85
+
86
+ // const childItems = screen.getAllByRole("treeitem");
87
+ // expect(childItems.length).toBe(2);
88
+ // });
89
+ // });
90
+
91
+ test("Renders with correct aria-level on nested nodes", async () => {
92
+ render(
93
+ <Tree.Root>
94
+ <Tree>
95
+ <Tree.Node nodeId="root">
96
+ <Tree.Trigger nodeId="root">Root</Tree.Trigger>
97
+ <Tree.Content nodeId="root" defaultOpen>
98
+ <Tree.Node level={1} nodeId="child">
99
+ <Tree.Trigger nodeId="child">Child</Tree.Trigger>
100
+ <Tree.Content nodeId="child" defaultOpen>
101
+ <Tree.Node level={2} nodeId="grandchild">
102
+ <Tree.Trigger nodeId="grandchild">Grandchild</Tree.Trigger>
103
+ </Tree.Node>
104
+ </Tree.Content>
105
+ </Tree.Node>
106
+ </Tree.Content>
107
+ </Tree.Node>
108
+ </Tree>
109
+ </Tree.Root>,
110
+ );
111
+
112
+ const treeItems = screen.getAllByRole("treeitem");
113
+ expect((treeItems[0] as HTMLElement).getAttribute("aria-level")).toBe("1");
114
+ expect((treeItems[1] as HTMLElement).getAttribute("aria-level")).toBe("2");
115
+ expect((treeItems[2] as HTMLElement).getAttribute("aria-level")).toBe("3");
116
+ });
117
+
118
+ // test("Renders with defaultOpen and expands content on mount", async () => {
119
+ // render(
120
+ // <Tree.Root>
121
+ // <Tree>
122
+ // <Tree.Node nodeId="node-1">
123
+ // <Tree.Trigger nodeId="node-1">Node 1</Tree.Trigger>
124
+ // <Tree.Content nodeId="node-1" defaultOpen>
125
+ // <Tree.Node level={1} nodeId="node-1-child">
126
+ // <Tree.Trigger nodeId="node-1-child">Node 1 Child</Tree.Trigger>
127
+ // </Tree.Node>
128
+ // </Tree.Content>
129
+ // </Tree.Node>
130
+ // </Tree>
131
+ // </Tree.Root>,
132
+ // );
133
+
134
+ // await waitFor(() => {
135
+ // expect(screen.getByRole("group")).toBeDefined();
136
+ // expect(screen.getByText("Node 1 Child")).toBeDefined();
137
+
138
+ // const trigger = screen.getByLabelText("node-1-action");
139
+ // expect(trigger.getAttribute("aria-expanded")).toBe("true");
140
+ // expect(trigger.getAttribute("data-state")).toBe("expanded");
141
+ // });
142
+ // });
143
+
144
+ test("Fires the defined callback and toggles content when trigger is clicked", async () => {
145
+ const onClickCallback = vi.fn();
146
+
147
+ render(
148
+ <Tree.Root>
149
+ <Tree>
150
+ <Tree.Node nodeId="node-1">
151
+ <Tree.Trigger
152
+ nodeId="node-1"
153
+ name="trigger-1"
154
+ onClick={onClickCallback}
155
+ >
156
+ Node 1
157
+ </Tree.Trigger>
158
+ <Tree.Content nodeId="node-1" defaultOpen>
159
+ <Tree.Node level={1} nodeId="node-1-child">
160
+ <Tree.Trigger nodeId="node-1-child">Node 1 Child</Tree.Trigger>
161
+ </Tree.Node>
162
+ </Tree.Content>
163
+ </Tree.Node>
164
+ <Tree.Node nodeId="node-2">
165
+ <Tree.Trigger nodeId="node-2" name="trigger-2">
166
+ Node 2
167
+ </Tree.Trigger>
168
+ <Tree.Content nodeId="node-2">
169
+ <Tree.Node level={1} nodeId="node-2-child">
170
+ <Tree.Trigger nodeId="node-2-child">Node 2 Child</Tree.Trigger>
171
+ </Tree.Node>
172
+ </Tree.Content>
173
+ </Tree.Node>
174
+ </Tree>
175
+ </Tree.Root>,
176
+ );
177
+
178
+ expect(screen.getByText("Node 1 Child")).toBeDefined();
179
+ expect(() => screen.getByText("Node 2 Child")).toThrow();
180
+
181
+ fireEvent.click(screen.getByLabelText("trigger-2-action"));
182
+ await waitFor(() => {
183
+ expect(screen.getByText("Node 2 Child")).toBeDefined();
184
+ });
185
+
186
+ fireEvent.click(screen.getByLabelText("trigger-1-action"));
187
+ await waitFor(() => {
188
+ expect(() => screen.getByText("Node 1 Child")).toThrow();
189
+ expect(onClickCallback).toHaveBeenCalledTimes(1);
190
+ });
191
+
192
+ fireEvent.click(screen.getByLabelText("trigger-1-action"));
193
+ await waitFor(() => {
194
+ expect(screen.getByText("Node 1 Child")).toBeDefined();
195
+ expect(onClickCallback).toHaveBeenCalledTimes(2);
196
+ });
197
+
198
+ fireEvent.click(screen.getByLabelText("trigger-1-action"));
199
+ await waitFor(() => {
200
+ expect(() => screen.getByText("Node 1 Child")).toThrow();
201
+ });
202
+ });
203
+
204
+ test("Fires onSelectionChange callback with selected node ids", async () => {
205
+ const onSelectionChange = vi.fn();
206
+
207
+ render(
208
+ <Tree.Root onSelectionChange={onSelectionChange}>
209
+ <Tree>
210
+ <Tree.Node nodeId="node-1">
211
+ <Tree.Trigger nodeId="node-1" name="trigger-1">
212
+ Node 1
213
+ </Tree.Trigger>
214
+ </Tree.Node>
215
+ <Tree.Node nodeId="node-2">
216
+ <Tree.Trigger nodeId="node-2" name="trigger-2">
217
+ Node 2
218
+ </Tree.Trigger>
219
+ </Tree.Node>
220
+ </Tree>
221
+ </Tree.Root>,
222
+ );
223
+
224
+ fireEvent.click(screen.getByLabelText("trigger-1-action"));
225
+ await waitFor(() => {
226
+ expect(onSelectionChange).toHaveBeenCalledTimes(1);
227
+ expect(onSelectionChange).toHaveBeenCalledWith(["node-1"]);
228
+ });
229
+
230
+ fireEvent.click(screen.getByLabelText("trigger-2-action"));
231
+ await waitFor(() => {
232
+ expect(onSelectionChange).toHaveBeenCalledTimes(2);
233
+ expect(onSelectionChange).toHaveBeenCalledWith(["node-1", "node-2"]);
234
+ });
235
+
236
+ fireEvent.click(screen.getByLabelText("trigger-1-action"));
237
+ await waitFor(() => {
238
+ expect(onSelectionChange).toHaveBeenCalledTimes(3);
239
+ expect(onSelectionChange).toHaveBeenCalledWith(["node-2"]);
240
+ });
241
+ });
242
+
243
+ test("Renders with defaultExpandedIds and expands matching nodes on mount", async () => {
244
+ render(
245
+ <Tree.Root defaultExpandedIds={["node-1", "node-2"]}>
246
+ <Tree>
247
+ <Tree.Node nodeId="node-1">
248
+ <Tree.Trigger nodeId="node-1">Node 1</Tree.Trigger>
249
+ <Tree.Content nodeId="node-1">
250
+ <Tree.Node level={1} nodeId="node-1-child">
251
+ <Tree.Trigger nodeId="node-1-child">Node 1 Child</Tree.Trigger>
252
+ </Tree.Node>
253
+ </Tree.Content>
254
+ </Tree.Node>
255
+ <Tree.Node nodeId="node-2">
256
+ <Tree.Trigger nodeId="node-2">Node 2</Tree.Trigger>
257
+ <Tree.Content nodeId="node-2">
258
+ <Tree.Node level={1} nodeId="node-2-child">
259
+ <Tree.Trigger nodeId="node-2-child">Node 2 Child</Tree.Trigger>
260
+ </Tree.Node>
261
+ </Tree.Content>
262
+ </Tree.Node>
263
+ </Tree>
264
+ </Tree.Root>,
265
+ );
266
+
267
+ await waitFor(() => {
268
+ expect(screen.getByText("Node 1 Child")).toBeDefined();
269
+ expect(screen.getByText("Node 2 Child")).toBeDefined();
270
+
271
+ const groups = screen.getAllByRole("group");
272
+ expect(groups.length).toBe(2);
273
+ });
274
+ });
275
+ });
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
  import type { Meta, StoryObj } from "@storybook/react";
3
3
 
4
- import { Accordion } from "..";
4
+ import { Accordion, Page } from "..";
5
5
  import { ComponentSizeEnum, ComponentVariantEnum } from "../../../../types";
6
6
 
7
7
  const meta = {
@@ -10,9 +10,11 @@ const meta = {
10
10
  tags: ["autodocs"],
11
11
  decorators: [
12
12
  (Story) => (
13
- <div className="m-medium-30">
14
- <Story />
15
- </div>
13
+ <Page>
14
+ <Page.Content className="p-medium-30">
15
+ <Story />
16
+ </Page.Content>
17
+ </Page>
16
18
  ),
17
19
  ],
18
20
  } satisfies Meta<typeof Accordion>;
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import React, { useState, createContext, useContext } from "react";
2
4
  import { IReactChildren, IComponentAPI } from "../../../../../types";
3
5
 
@@ -11,7 +13,7 @@ export const useAccordion = () => useContext(AccordionContext);
11
13
 
12
14
  export const AccordionProvider = ({
13
15
  children,
14
- }: IReactChildren): JSX.Element => {
16
+ }: IReactChildren): React.JSX.Element => {
15
17
  const context = useAccordionProvider();
16
18
 
17
19
  return (
@@ -16,8 +16,7 @@ export interface IAccordionComposition {
16
16
  Content: typeof AccordionContent;
17
17
  }
18
18
  export interface IAccordionProperties
19
- extends IComponentSpacing,
20
- React.ComponentProps<"div"> {}
19
+ extends IComponentSpacing, React.ComponentProps<"div"> {}
21
20
  export interface IAccordionTriggerProperties extends IButtonProperties {
22
21
  value: string;
23
22
  }
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import type { Meta, StoryObj } from "@storybook/react";
3
- import { Avatar } from "..";
3
+ import { Avatar, Page, Tooltip } from "..";
4
4
 
5
5
  const meta = {
6
6
  title: "Components/Avatar",
@@ -8,9 +8,11 @@ const meta = {
8
8
  tags: ["autodocs"],
9
9
  decorators: [
10
10
  (Story) => (
11
- <div className="m-medium-30">
12
- <Story />
13
- </div>
11
+ <Page>
12
+ <Page.Content className="p-medium-30">
13
+ <Story />
14
+ </Page.Content>
15
+ </Page>
14
16
  ),
15
17
  ],
16
18
  } satisfies Meta<typeof Avatar>;
@@ -31,6 +33,25 @@ export const Status: Story = {
31
33
  </div>
32
34
  ),
33
35
  };
36
+ export const Badges: Story = {
37
+ render: ({ ...args }) => (
38
+ <div className="flex g-medium-30">
39
+ {["small", "medium", "large"].map((variant) => (
40
+ <Avatar
41
+ sizing={variant as "small"}
42
+ alt="foundation-logo"
43
+ src="https://www.untitledui.com/images/avatars/olivia-rhye?fm=webp&q=80"
44
+ >
45
+ <Avatar.Badge
46
+ alt="foundation-logo"
47
+ src="https://www.untitledui.com/logos/images/Layers.jpg"
48
+ />
49
+ </Avatar>
50
+ ))}
51
+ </div>
52
+ ),
53
+ };
54
+
34
55
  export const Sizes: Story = {
35
56
  render: ({ ...args }) => (
36
57
  <div className="flex g-medium-30">
@@ -40,22 +61,31 @@ export const Sizes: Story = {
40
61
  </div>
41
62
  ),
42
63
  };
64
+ export const Shapes: Story = {
65
+ render: ({ ...args }) => (
66
+ <div className="flex g-medium-30">
67
+ <Avatar shape="square" status="online" {...args} />
68
+ <Avatar shape="smooth" status="online" {...args} />
69
+ <Avatar shape="round" status="online" {...args} />
70
+ </div>
71
+ ),
72
+ };
43
73
  export const Variants: Story = {
44
74
  render: ({ ...args }) => (
45
75
  <div className="flex g-medium-30">
46
76
  <Avatar />
47
77
  <Avatar
48
78
  alt="foundation-logo"
49
- src="https://avatars.githubusercontent.com/u/153380498?s=200&v=4"
79
+ src="https://www.untitledui.com/images/avatars/olivia-rhye?fm=webp&q=80"
50
80
  />
51
81
  <Avatar>
52
- <b>AZ</b>
82
+ <b className="fs-medium-10">AZ</b>
53
83
  </Avatar>
54
84
  <Avatar
55
85
  style={{ backgroundColor: "var(--color-purple)" }}
56
86
  status="online"
57
87
  >
58
- <small>Acme</small>
88
+ <span className="fs-small-30">Acme</span>
59
89
  </Avatar>
60
90
  </div>
61
91
  ),
@@ -1,11 +1,13 @@
1
1
  "use client";
2
2
 
3
3
  import React from "react";
4
- import { AvatarWrapper, StatusWrapper } from "./styles";
4
+ import { AvatarWrapper, BadgeWrapper, StatusWrapper } from "./styles";
5
5
  import {
6
6
  IComponentStyling,
7
7
  ComponentSizeEnum,
8
8
  IComponentSize,
9
+ ComponentShapeEnum,
10
+ IComponentShape,
9
11
  } from "../../../../types";
10
12
 
11
13
  export enum AvataStatusEnum {
@@ -14,13 +16,25 @@ export enum AvataStatusEnum {
14
16
  Busy = "busy",
15
17
  Offline = "offline",
16
18
  }
19
+ export type AvatarStatusType = "online" | "away" | "busy" | "offline";
17
20
  export interface IAvatarProperties
18
- extends IComponentStyling,
21
+ extends
22
+ IComponentStyling,
19
23
  IComponentSize,
24
+ IComponentShape,
20
25
  React.HTMLAttributes<HTMLDivElement> {
21
26
  src?: string;
22
27
  alt?: string;
23
- status?: "online" | "away" | "busy" | "offline";
28
+ status?: AvatarStatusType;
29
+ }
30
+ export interface IAvatarBadgeProperties extends React.HTMLAttributes<HTMLDivElement> {
31
+ src?: string;
32
+ alt?: string;
33
+ }
34
+
35
+ export interface IAvatarComposition {
36
+ Status: typeof AvatarStatus;
37
+ Badge: typeof AvatarBadge;
24
38
  }
25
39
 
26
40
  /**
@@ -41,11 +55,12 @@ export interface IAvatarProperties
41
55
  * @param {ReactNode} props.children - The content to be rendered inside the avatar.
42
56
  * @returns {ReactElement} The Avatar component.
43
57
  */
44
- export const Avatar = (props: IAvatarProperties) => {
58
+ const Avatar = (props: IAvatarProperties) => {
45
59
  const {
46
60
  raw,
47
61
  sizing = ComponentSizeEnum.Medium,
48
62
  status,
63
+ shape = ComponentShapeEnum.Round,
49
64
  src,
50
65
  alt,
51
66
  children,
@@ -58,10 +73,11 @@ export const Avatar = (props: IAvatarProperties) => {
58
73
  data-raw={Boolean(raw)}
59
74
  data-size={sizing}
60
75
  data-status={status}
76
+ data-shape={shape}
61
77
  aria-label={props["aria-label"] ?? `${sizeLabel}-user-avatar`}
62
78
  {...restProps}
63
79
  >
64
- {!children && src && (
80
+ {src && (
65
81
  <img
66
82
  aria-label={`${sizeLabel}-user-avatar-image`}
67
83
  alt={alt ?? `${sizeLabel}-user-avatar-image`}
@@ -70,21 +86,76 @@ export const Avatar = (props: IAvatarProperties) => {
70
86
  )}
71
87
 
72
88
  {children}
73
- {status && (
74
- <StatusWrapper
75
- role="img"
76
- aria-label={`${sizing}-user-avatar-status`}
77
- aria-labelledby="title desc"
78
- data-status={status}
79
- height="16"
80
- width="16"
81
- >
82
- <title>{"Activity status"}</title>
83
- <desc>{status}</desc>
84
- <circle role="presentation" cx="8" cy="8" r="6" />
85
- </StatusWrapper>
86
- )}
89
+ {status && <Avatar.Status status={status} />}
87
90
  </AvatarWrapper>
88
91
  );
89
92
  };
90
93
  Avatar.displayName = "Avatar";
94
+
95
+ /**
96
+ * Avatar Statuses are used to represents a user activity status on an interface.
97
+ *
98
+ * **Best practices:**
99
+ *
100
+ * - Use the appropriate status to match the context and the importance of the information.
101
+ *
102
+ * @param {string} props.status - The status of the user represented by the avatar.
103
+ * @returns {ReactElement} The Avatar Status component.
104
+ */
105
+ const AvatarStatus = (props: {
106
+ status?: AvatarStatusType;
107
+ }): React.ReactElement => {
108
+ const { status } = props;
109
+
110
+ return (
111
+ <StatusWrapper
112
+ role="img"
113
+ aria-label={`${status}-user-avatar-status`}
114
+ aria-labelledby="title desc"
115
+ data-status={status}
116
+ height="12"
117
+ width="12"
118
+ {...props}
119
+ >
120
+ <title>{"Activity status"}</title>
121
+ <desc>{status}</desc>
122
+ <circle role="presentation" cx="6" cy="6" r="4" />
123
+ </StatusWrapper>
124
+ );
125
+ };
126
+ AvatarStatus.displayName = "Avatar.Status";
127
+
128
+ /**
129
+ * Avatar Badges are used to represents an entity or an information related to the primary subject of the avatar.
130
+ *
131
+ * **Best practices:**
132
+ *
133
+ * - Use the appropriate imgage to display the context and the importance of the information.
134
+ * - Always provide an `alt` attribute for accessibility when using an image.
135
+ *
136
+ * @param {IAvatarBadgeProperties} props - The props for the Avatar Badge component.
137
+ * @param {string} props.src - The source URL of the image to be displayed in the avatar badge.
138
+ * @param {string} props.alt - The alternative text for the image in the avatar badge.
139
+ * @returns {ReactElement} The Avatar Badge component.
140
+ */
141
+ const AvatarBadge = (props: IAvatarBadgeProperties): React.ReactElement => {
142
+ const { src, alt, children } = props;
143
+ return (
144
+ <BadgeWrapper role="img" aria-label="user-avatar-badge-wrapper" {...props}>
145
+ {src && !children && (
146
+ <img
147
+ aria-label="user-avatar-badge"
148
+ alt={alt ?? "user-avatar-badge"}
149
+ src={src}
150
+ />
151
+ )}
152
+ {!src && children && children}
153
+ </BadgeWrapper>
154
+ );
155
+ };
156
+ AvatarBadge.displayName = "Avatar.Badge";
157
+
158
+ Avatar.Status = AvatarStatus;
159
+ Avatar.Badge = AvatarBadge;
160
+
161
+ export { Avatar, AvatarStatus };
@@ -2,19 +2,19 @@ import styled, { css } from "styled-components";
2
2
 
3
3
  const AvatarSizesStyles = css`
4
4
  &[data-size="small"] {
5
+ width: var(--measurement-medium-70);
6
+ height: var(--measurement-medium-70);
7
+ min-width: var(--measurement-medium-70);
8
+ min-height: var(--measurement-medium-70);
9
+ }
10
+
11
+ &[data-size="medium"] {
5
12
  width: var(--measurement-large-10);
6
13
  height: var(--measurement-large-10);
7
14
  min-width: var(--measurement-large-10);
8
15
  min-height: var(--measurement-large-10);
9
16
  }
10
17
 
11
- &[data-size="medium"] {
12
- width: var(--measurement-medium-90);
13
- height: var(--measurement-medium-90);
14
- min-width: var(--measurement-medium-90);
15
- min-height: var(--measurement-medium-90);
16
- }
17
-
18
18
  &[data-size="large"] {
19
19
  width: var(--measurement-large-20);
20
20
  height: var(--measurement-large-20);
@@ -22,6 +22,26 @@ const AvatarSizesStyles = css`
22
22
  min-height: var(--measurement-large-20);
23
23
  }
24
24
  `;
25
+ const AvatarShapesStyles = css`
26
+ &[data-shape="square"] {
27
+ border-radius: 0;
28
+ img {
29
+ border-radius: 0;
30
+ }
31
+ }
32
+ &[data-shape="smooth"] {
33
+ border-radius: var(--measurement-medium-30);
34
+ img {
35
+ border-radius: var(--measurement-medium-30);
36
+ }
37
+ }
38
+ &[data-shape="round"] {
39
+ border-radius: 100%;
40
+ img {
41
+ border-radius: 100%;
42
+ }
43
+ }
44
+ `;
25
45
  const AvatarStatusesStyles = css`
26
46
  &[data-status="online"] {
27
47
  fill: var(--shade-green-10);
@@ -40,7 +60,7 @@ const AvatarStatusesStyles = css`
40
60
 
41
61
  &[data-status="offline"] {
42
62
  fill: var(--body-color);
43
- stroke: var(--font-color-alpha-10);
63
+ stroke: var(--contrast-color);
44
64
  }
45
65
  `;
46
66
 
@@ -51,17 +71,18 @@ export const AvatarWrapper = styled.div`
51
71
  align-items: center;
52
72
  justify-content: center;
53
73
 
54
- background-color: var(--body-color);
55
- border-radius: 100%;
74
+ background-color: var(--font-color-alpha-10);
75
+ border: var(--measurement-small-10) solid var(--font-color-alpha-10);
56
76
 
57
77
  img {
58
78
  width: inherit;
59
79
  height: inherit;
60
80
  min-width: inherit;
61
81
  min-height: inherit;
62
- border-radius: 100%;
82
+ border: var(--measurement-small-10) solid var(--font-color-alpha-10);
63
83
  }
64
84
 
85
+ ${AvatarShapesStyles}
65
86
  ${AvatarSizesStyles}
66
87
  }
67
88
  `;
@@ -71,9 +92,34 @@ export const StatusWrapper = styled.svg`
71
92
  );
72
93
 
73
94
  position: absolute;
74
- stroke-width: var(--measurement-small-30);
95
+ stroke-width: var(--measurement-small-10);
75
96
  bottom: var(--status-position);
76
97
  right: var(--status-position);
77
98
 
78
99
  ${AvatarStatusesStyles}
79
100
  `;
101
+ export const BadgeWrapper = styled.div`
102
+ --status-position: calc(
103
+ var(--measurement-medium-10) - (var(--measurement-medium-10) * 2)
104
+ );
105
+
106
+ position: absolute;
107
+
108
+ bottom: var(--status-position);
109
+ right: var(--status-position);
110
+
111
+ width: var(--measurement-medium-60);
112
+ height: var(--measurement-medium-60);
113
+
114
+ background-color: var(--font-color-alpha-10);
115
+ border-radius: 100%;
116
+
117
+ img {
118
+ width: inherit;
119
+ height: inherit;
120
+ min-width: inherit;
121
+ min-height: inherit;
122
+ border-radius: 100%;
123
+ border: var(--measurement-small-10) solid var(--font-color-alpha-10);
124
+ }
125
+ `;