@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,138 @@
1
+ import React from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+
4
+ import { Avatar, Field, MessageBubble, Page, ScrollArea, Textarea } from "..";
5
+
6
+ const meta = {
7
+ title: "Components/MessageBubble",
8
+ component: MessageBubble,
9
+ tags: ["autodocs"],
10
+ decorators: [
11
+ (Story) => (
12
+ <Page>
13
+ <Page.Content className="p-medium-30">
14
+ <Story />
15
+ </Page.Content>
16
+ </Page>
17
+ ),
18
+ ],
19
+ } satisfies Meta<typeof MessageBubble>;
20
+ export default meta;
21
+
22
+ type Story = StoryObj<typeof meta>;
23
+
24
+ const MOCK_DATE = new Date("2026-03-17T13:00:00Z");
25
+ const MOCK_MESSAGE = "Hey, how are you doing?";
26
+
27
+ export const Default: Story = {
28
+ args: {
29
+ side: "left",
30
+ raw: false,
31
+ },
32
+ argTypes: {
33
+ side: {
34
+ options: ["left", "right"],
35
+ control: { type: "radio" },
36
+ },
37
+ raw: {
38
+ control: { type: "boolean" },
39
+ },
40
+ },
41
+ render: ({ ...args }) => (
42
+ <MessageBubble.Root>
43
+ <MessageBubble {...args}>
44
+ <MessageBubble.Content>{MOCK_MESSAGE}</MessageBubble.Content>
45
+ <MessageBubble.Meta createdAt={MOCK_DATE} />
46
+ </MessageBubble>
47
+ </MessageBubble.Root>
48
+ ),
49
+ };
50
+
51
+ export const Left: Story = {
52
+ render: () => (
53
+ <MessageBubble.Root>
54
+ <MessageBubble side="left">
55
+ <MessageBubble.Content>{MOCK_MESSAGE}</MessageBubble.Content>
56
+ <MessageBubble.Meta createdAt={MOCK_DATE} />
57
+ </MessageBubble>
58
+ </MessageBubble.Root>
59
+ ),
60
+ };
61
+
62
+ export const Right: Story = {
63
+ render: () => (
64
+ <MessageBubble.Root>
65
+ <MessageBubble side="right">
66
+ <MessageBubble.Content>{MOCK_MESSAGE}</MessageBubble.Content>
67
+ <MessageBubble.Meta createdAt={MOCK_DATE} />
68
+ </MessageBubble>
69
+ </MessageBubble.Root>
70
+ ),
71
+ };
72
+
73
+ export const Conversation: Story = {
74
+ render: () => (
75
+ <ScrollArea className="h-100 w-100 flex flex-column g-medium-30 " scrollbar>
76
+ {(
77
+ [
78
+ {
79
+ variant: "border",
80
+ side: "left",
81
+ message: "Hey, how are you doing?",
82
+ },
83
+ {
84
+ variant: "primary",
85
+ side: "right",
86
+ message: "All good! What about you?",
87
+ },
88
+ {
89
+ variant: "border",
90
+ side: "left",
91
+ message: "Pretty great, thanks for asking",
92
+ },
93
+ {
94
+ variant: "primary",
95
+ side: "right",
96
+ message:
97
+ "Hic dolorum esse magnam sint quibusdam porro reprehenderit, enim, repellendus ipsam, iste est! Deserunt ipsam ullam dolores expedita rem, magni iste eveniet.",
98
+ },
99
+ {
100
+ variant: "hint",
101
+ side: "right",
102
+ message: "Hic dolorum esse magnam sint quibusdam.",
103
+ },
104
+ {
105
+ variant: "meta",
106
+ side: "right",
107
+ message: "Ipsa nisi fugiat doloribus.",
108
+ },
109
+ ] as const
110
+ ).map(({ variant, side, message }, index) => (
111
+ <MessageBubble.Root key={index}>
112
+ <MessageBubble side={side}>
113
+ {side === "left" && (
114
+ <Field.Meta variant="hint">
115
+ <Avatar
116
+ sizing="small"
117
+ alt="foundation-logo"
118
+ src="https://www.untitledui.com/images/avatars/olivia-rhye?fm=webp&q=80"
119
+ shape="smooth"
120
+ >
121
+ <Avatar.Badge
122
+ alt="foundation-logo"
123
+ src="https://www.untitledui.com/logos/images/Layers.jpg"
124
+ />
125
+ </Avatar>
126
+ </Field.Meta>
127
+ )}
128
+
129
+ <MessageBubble.Content variant={variant} className="fs-medium-20">
130
+ {message}
131
+ </MessageBubble.Content>
132
+ <MessageBubble.Meta createdAt={MOCK_DATE} />
133
+ </MessageBubble>
134
+ </MessageBubble.Root>
135
+ ))}
136
+ </ScrollArea>
137
+ ),
138
+ };
@@ -0,0 +1,41 @@
1
+ "use client";
2
+
3
+ import React, { useState, createContext, useContext } from "react";
4
+
5
+ import { IReactChildren, IComponentAPI } from "../../../../../types";
6
+ import { MessageBubbleSide } from "../";
7
+
8
+ const defaultComponentAPI = {
9
+ id: "",
10
+ states: {},
11
+ methods: {},
12
+ };
13
+ const MessageBubbleContext = createContext<IComponentAPI>(defaultComponentAPI);
14
+ export const useMessageBubble = () => useContext(MessageBubbleContext);
15
+
16
+ export const MessageBubbleProvider = ({
17
+ children,
18
+ }: IReactChildren): React.JSX.Element => {
19
+ const context = useMessageBubbleProvider();
20
+
21
+ return (
22
+ <MessageBubbleContext.Provider value={context}>
23
+ {children}
24
+ </MessageBubbleContext.Provider>
25
+ );
26
+ };
27
+
28
+ function useMessageBubbleProvider(): IComponentAPI {
29
+ const [side, setSide] = useState<MessageBubbleSide | null>(null);
30
+ const MessageBubbleId = React.useId();
31
+
32
+ return {
33
+ id: MessageBubbleId,
34
+ states: {
35
+ side,
36
+ },
37
+ methods: {
38
+ applySide: (side: MessageBubbleSide): string | void => setSide(side),
39
+ },
40
+ };
41
+ }
@@ -0,0 +1,171 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { useMessageBubble, MessageBubbleProvider } from "./hooks";
5
+
6
+ import {
7
+ MessageBubbleWrapper,
8
+ MessageBubbleBadge,
9
+ MessageBubbleContentWrapper,
10
+ MessageBubbleMetaWrapper,
11
+ } from "./styles";
12
+
13
+ import {
14
+ ComponentShapeEnum,
15
+ ComponentSizeEnum,
16
+ ComponentVariantEnum,
17
+ IComponentShape,
18
+ IComponentSize,
19
+ IComponentStyling,
20
+ IReactChildren,
21
+ TComponentVariant,
22
+ TComponentVariantExtended,
23
+ } from "../../../../types";
24
+
25
+ export type MessageBubbleSide = "left" | "right";
26
+
27
+ export interface IMessageBubbleContext {
28
+ side: MessageBubbleSide;
29
+ }
30
+
31
+ export interface IMessageBubbleProperties
32
+ extends IComponentStyling, React.HTMLAttributes<HTMLDivElement> {
33
+ side: MessageBubbleSide;
34
+ }
35
+
36
+ export interface IMessageBubbleContentProperties
37
+ extends
38
+ IComponentStyling,
39
+ IComponentShape,
40
+ IComponentSize,
41
+ React.HTMLAttributes<HTMLDivElement> {
42
+ variant?: TComponentVariant | TComponentVariantExtended;
43
+ children: string;
44
+ }
45
+
46
+ export interface IMessageBubbleMetaProperties
47
+ extends IComponentStyling, React.HTMLAttributes<HTMLDivElement> {
48
+ createdAt: Date;
49
+ }
50
+
51
+ const MessageBubbleRoot = ({ children }: IReactChildren) => {
52
+ return <MessageBubbleProvider>{children}</MessageBubbleProvider>;
53
+ };
54
+ MessageBubbleRoot.displayName = "MessageBubble.Root";
55
+
56
+ /**
57
+ * MessageBubble is used to display a chat message with an optional side and raw layout.
58
+ *
59
+ * **Best practices:**
60
+ *
61
+ * - Always wrap MessageBubble inside a MessageBubble.Root to provide the necessary context.
62
+ * - Use `side` to visually distinguish between sent and received messages.
63
+ * - Pair with MessageBubble.Content and MessageBubble.Meta for a complete message layout.
64
+ *
65
+ * @param {IMessageBubbleProperties} props - The props for the MessageBubble component.
66
+ * @param {"left" | "right"} props.side - The side the bubble is aligned to. Propagated to all child compounds via context.
67
+ * @param {boolean} props.raw - When true, removes default styling for custom layouts.
68
+ * @param {ReactNode} props.children - The content to be rendered inside the bubble.
69
+ * @returns {ReactElement} The MessageBubble component.
70
+ */
71
+ const MessageBubble = (props: IMessageBubbleProperties) => {
72
+ const { side, raw, children, ...restProps } = props;
73
+ const { methods } = useMessageBubble();
74
+
75
+ React.useEffect(() => {
76
+ if (side && methods?.applySide) methods.applySide(side);
77
+ }, [side]);
78
+
79
+ return (
80
+ <MessageBubbleWrapper
81
+ data-raw={Boolean(raw)}
82
+ data-side={side}
83
+ aria-label={restProps["aria-label"] ?? `message-bubble-${side}`}
84
+ {...restProps}
85
+ >
86
+ {children}
87
+ </MessageBubbleWrapper>
88
+ );
89
+ };
90
+ MessageBubble.displayName = "MessageBubble";
91
+
92
+ /**
93
+ * MessageBubble.Content is used to display the text or rich content of the message.
94
+ *
95
+ * **Best practices:**
96
+ *
97
+ * - Place MessageBubble.Content inside a MessageBubble to inherit the correct side context.
98
+ * - Avoid nesting interactive elements inside the content that may conflict with bubble focus management.
99
+ *
100
+ * @param {IMessageBubbleContentProperties} props - The props for the MessageBubble.Content component.
101
+ * @param {boolean} props.raw - When true, removes default styling for custom layouts.
102
+ * @param {ReactNode} props.children - The message text or rich content to render.
103
+ * @returns {ReactElement} The MessageBubble.Content component.
104
+ */
105
+ const MessageBubbleContent = (props: IMessageBubbleContentProperties) => {
106
+ const { sizing, shape, variant, children, raw, ...restProps } = props;
107
+ const { id, states } = useMessageBubble();
108
+
109
+ return (
110
+ <MessageBubbleBadge
111
+ data-raw={Boolean(raw)}
112
+ data-side={states?.side}
113
+ variant={variant ?? ComponentVariantEnum.Border}
114
+ shape={shape ?? ComponentShapeEnum.Smooth}
115
+ sizing={sizing ?? ComponentSizeEnum.Medium}
116
+ aria-label={`message-bubble-content-${id}`}
117
+ {...restProps}
118
+ >
119
+ <MessageBubbleContentWrapper>{children}</MessageBubbleContentWrapper>
120
+ </MessageBubbleBadge>
121
+ );
122
+ };
123
+ MessageBubbleContent.displayName = "MessageBubble.Content";
124
+
125
+ /**
126
+ * MessageBubble.Meta is used to display metadata associated with the message, such as its timestamp.
127
+ *
128
+ * **Best practices:**
129
+ *
130
+ * - Always provide a valid `createdAt` date for accurate timestamp display.
131
+ * - Place MessageBubble.Meta after MessageBubble.Content for a natural reading flow.
132
+ * - The side is automatically inherited from context — do not pass it manually.
133
+ *
134
+ * @param {IMessageBubbleMetaProperties} props - The props for the MessageBubble.Meta component.
135
+ * @param {Date} props.createdAt - The date the message was created. Formatted as medium date and short time.
136
+ * @param {boolean} props.raw - When true, removes default styling for custom layouts.
137
+ * @returns {ReactElement} The MessageBubble.Meta component.
138
+ */
139
+ const MessageBubbleMeta = (props: IMessageBubbleMetaProperties) => {
140
+ const { createdAt, raw, ...restProps } = props;
141
+ const { states } = useMessageBubble();
142
+
143
+ const formattedDate = new Intl.DateTimeFormat("en-US", {
144
+ dateStyle: "medium",
145
+ timeStyle: "short",
146
+ }).format(createdAt);
147
+
148
+ return (
149
+ <MessageBubbleMetaWrapper
150
+ data-raw={Boolean(raw)}
151
+ data-side={states?.side}
152
+ aria-label={`message-bubble-meta-${states?.side}`}
153
+ className="fs-small-60 opacity-default-60"
154
+ {...restProps}
155
+ >
156
+ {formattedDate}
157
+ </MessageBubbleMetaWrapper>
158
+ );
159
+ };
160
+ MessageBubbleMeta.displayName = "MessageBubble.Meta";
161
+
162
+ MessageBubble.Root = MessageBubbleRoot;
163
+ MessageBubble.Content = MessageBubbleContent;
164
+ MessageBubble.Meta = MessageBubbleMeta;
165
+
166
+ export {
167
+ MessageBubble,
168
+ MessageBubbleRoot,
169
+ MessageBubbleContent,
170
+ MessageBubbleMeta,
171
+ };
@@ -0,0 +1,58 @@
1
+ import styled from "styled-components";
2
+ import { Badge, IBadgeProperties } from "../../";
3
+
4
+ export const MessageBubbleWrapper = styled.div`
5
+ display: flex;
6
+ flex-direction: column;
7
+ gap: var(--measurement-medium-10);
8
+
9
+ &[data-side="right"] {
10
+ align-items: flex-end;
11
+ }
12
+
13
+ &[data-side="left"] {
14
+ align-items: flex-start;
15
+ }
16
+ `;
17
+
18
+ export const MessageBubbleBadge: React.FC<IBadgeProperties> = styled(Badge)`
19
+ position: relative;
20
+ max-width: var(--measurement-large-90);
21
+ width: 100%;
22
+ justify-self: flex-end;
23
+ padding: var(--measurement-medium-30) var(--measurement-medium-50) !important;
24
+
25
+ &[data-side="left"] {
26
+ border-top-left-radius: 0 !important;
27
+ }
28
+
29
+ &[data-side="right"] {
30
+ border-top-right-radius: 0 !important;
31
+ }
32
+ `;
33
+
34
+ export const MessageBubbleContentWrapper = styled.div`
35
+ line-height: 1.3;
36
+ font-weight: 500;
37
+ word-break: keep-all;
38
+ width: 100%;
39
+
40
+ * {
41
+ font-size: inherit !important;
42
+ }
43
+ `;
44
+
45
+ export const MessageBubbleMetaWrapper = styled.div`
46
+ display: flex;
47
+ align-items: center;
48
+ gap: var(--measurement-medium-10);
49
+ width: 100%;
50
+
51
+ &[data-side="right"] {
52
+ justify-content: flex-end;
53
+ }
54
+
55
+ &[data-side="left"] {
56
+ justify-content: flex-start;
57
+ }
58
+ `;
@@ -1,6 +1,4 @@
1
1
  import React from "react";
2
- import type { Meta, StoryObj } from "@storybook/react";
3
-
4
2
  import { Field, OTPField, Page } from "..";
5
3
 
6
4
  const meta = {
@@ -8,13 +6,17 @@ const meta = {
8
6
  component: OTPField,
9
7
  tags: ["autodocs"],
10
8
  decorators: [
11
- (Story) => (
12
- <div className="m-medium-30">
13
- <Story />
14
- </div>
9
+ (Story: any) => (
10
+ <Page>
11
+ <Page.Content className="p-medium-30">
12
+ <div className="flex flex-column align-center justify-center h-100">
13
+ <Story />
14
+ </div>
15
+ </Page.Content>
16
+ </Page>
15
17
  ),
16
18
  ],
17
- } satisfies Meta<typeof OTPField>;
19
+ };
18
20
  export default meta;
19
21
 
20
22
  export const Default = {
@@ -27,24 +29,20 @@ export const Default = {
27
29
  setValue(value.trim());
28
30
  }, []);
29
31
  return (
30
- <Page>
31
- <Page.Content>
32
- <form aria-label="story-form">
33
- <Field.Wrapper className="w-100">
34
- <Field.Label>Confirmation code</Field.Label>
35
- <OTPField length={6} onComplete={handleComplete}>
36
- <OTPField.Group>
37
- {Array.from({ length: 6 }).map((_, index) => (
38
- <OTPField.Slot key={index} index={index} />
39
- ))}
40
- </OTPField.Group>
41
- </OTPField>
32
+ <form aria-label="story-form" style={{ width: 325 }}>
33
+ <Field.Wrapper className="w-100">
34
+ <Field.Label>Confirmation code</Field.Label>
35
+ <OTPField length={6} onComplete={handleComplete}>
36
+ <OTPField.Group>
37
+ {Array.from({ length: 6 }).map((_, index) => (
38
+ <OTPField.Slot key={index} index={index} />
39
+ ))}
40
+ </OTPField.Group>
41
+ </OTPField>
42
42
 
43
- {value}
44
- </Field.Wrapper>
45
- </form>
46
- </Page.Content>
47
- </Page>
43
+ {value}
44
+ </Field.Wrapper>
45
+ </form>
48
46
  );
49
47
  },
50
48
  };
@@ -1,9 +1,11 @@
1
+ "use client";
2
+
1
3
  import React from "react";
2
4
 
3
5
  import type { OTPFieldContextType } from "../types";
4
6
 
5
7
  export const OTPFieldContext = React.createContext<OTPFieldContextType | null>(
6
- null
8
+ null,
7
9
  );
8
10
  export const useOTPField = () => {
9
11
  const context = React.useContext(OTPFieldContext);
@@ -6,6 +6,11 @@ import { useOTPField, OTPFieldContext } from "./hooks";
6
6
  import { OTPCell } from "./styles";
7
7
 
8
8
  import type { OTPFieldProps, OTPFieldSlotProps } from "./types";
9
+ import {
10
+ ComponentShapeEnum,
11
+ ComponentSizeEnum,
12
+ ComponentVariantEnum,
13
+ } from "../../../../types";
9
14
 
10
15
  export interface IOTPFieldComposition {
11
16
  Slot: typeof OTPFieldSlot;
@@ -26,7 +31,7 @@ const OTPField = ({ children, length, onComplete }: OTPFieldProps) => {
26
31
 
27
32
  const inputRefs = React.useRef<(HTMLInputElement | null)[]>([]);
28
33
  const [otp, setOtp] = React.useState<string[]>(
29
- Array.from({ length: defaultLength }, () => "")
34
+ Array.from({ length: defaultLength }, () => ""),
30
35
  );
31
36
  const [activeIndex, setActiveIndex] = React.useState<number>(0);
32
37
 
@@ -121,7 +126,7 @@ const OTPField = ({ children, length, onComplete }: OTPFieldProps) => {
121
126
  */
122
127
  const timeout = setTimeout(
123
128
  () => inputRefs.current[targetIndex]?.select(),
124
- 0
129
+ 0,
125
130
  );
126
131
 
127
132
  return () => clearTimeout(timeout);
@@ -191,6 +196,8 @@ OTPFieldGroup.displayName = "OTPField.Group";
191
196
  */
192
197
  const OTPFieldSlot = ({
193
198
  index,
199
+ shape,
200
+ raw,
194
201
  ...props
195
202
  }: OTPFieldSlotProps & React.InputHTMLAttributes<HTMLInputElement>) => {
196
203
  const context = useOTPField();
@@ -209,10 +216,14 @@ const OTPFieldSlot = ({
209
216
 
210
217
  return (
211
218
  <OTPCell
212
- ref={(el) => (inputRefs.current[index] = el)}
219
+ ref={(el) => {
220
+ inputRefs.current[index] = el;
221
+ }}
213
222
  type="text"
214
223
  data-testid="otp-field-slot"
215
224
  data-active={activeIndex === index}
225
+ data-shape={shape ?? ComponentShapeEnum.Smooth}
226
+ data-raw={Boolean(raw)}
216
227
  autoComplete="one-time-code"
217
228
  maxLength={1}
218
229
  value={otp[index] || ""}
@@ -1,33 +1,131 @@
1
- import styled from "styled-components";
1
+ import styled, { css } from "styled-components";
2
2
 
3
- export const OTPCell = styled.input`
3
+ const OTPShapeStyles = css`
4
+ &[data-shape="square"] {
5
+ border-radius: 0;
6
+ }
7
+ &[data-shape="smooth"] {
8
+ border-radius: var(--measurement-medium-20);
9
+ }
10
+ &[data-shape="round"] {
11
+ border-radius: var(--measurement-large-90);
12
+ padding-left: var(--measurement-medium-50) !important;
13
+ }
14
+ `;
15
+
16
+ const OTPCellDefaultStyles = css`
17
+ outline: none;
18
+ cursor: text;
19
+ display: flex;
20
+ align-items: center;
21
+ justify-content: center;
22
+ text-align: center;
23
+ box-sizing: border-box;
24
+
25
+ font-size: var(--fontsize-medium-20);
26
+
27
+ padding: 0 var(--measurement-medium-30);
4
28
  width: var(--measurement-medium-90);
5
29
  height: var(--measurement-medium-90);
6
- border: var(--measurement-small-10) solid var(--font-color-alpha-10);
7
30
 
8
- border-radius: var(--measurement-medium-30);
31
+ line-height: 1;
32
+ letter-spacing: calc(
33
+ var(--fontsize-small-10) - ((var(--fontsize-small-10) * 1.066))
34
+ );
35
+
36
+ border: var(--measurement-small-10) solid transparent;
37
+
9
38
  backdrop-filter: blur(var(--measurement-small-10));
39
+ color: var(--font-color-alpha-60);
10
40
 
11
- text-align: center;
12
- font-size: var(--fontsize-medium-10);
13
- font-weight: 500;
41
+ transition: all ease-in-out 0.2s;
14
42
 
15
- color: var(--font-color-alpha-10);
16
- text-shadow: 0 0 0 var(--font-color);
43
+ svg,
44
+ span,
45
+ img {
46
+ opacity: 0.6;
47
+ }
48
+
49
+ &:hover,
50
+ &:focus,
51
+ &:active,
52
+ &:focus-within,
53
+ &:has(:active) {
54
+ color: var(--font-color);
55
+ svg,
56
+ span,
57
+ img {
58
+ opacity: 1;
59
+ }
60
+ }
61
+
62
+ &::placeholder {
63
+ color: var(--font-color-alpha-30);
64
+ }
65
+
66
+ &:disabled,
67
+ &:has(:disabled) {
68
+ cursor: not-allowed;
69
+ opacity: 0.6;
70
+ }
17
71
 
18
72
  background-color: transparent;
19
- transition: all 0.2s ease-in-out;
20
- outline: none;
73
+ border-color: var(--font-color-alpha-10);
21
74
 
22
- &:focus:not(:active) {
23
- background-color: var(--font-color-alpha-10);
75
+ &:hover,
76
+ &:focus,
77
+ &:active,
78
+ &:focus-within,
79
+ &:has(:hover),
80
+ &:has(:active) {
81
+ border-color: var(--font-color-alpha-20);
24
82
  }
25
83
 
26
- &:hover:not(:active) {
84
+ &:focus,
85
+ &:active,
86
+ &:focus-within,
87
+ &:has(:active) {
88
+ box-shadow: 0 0 0 var(--measurement-small-30) var(--alpha-accent-30);
89
+ }
90
+
91
+ background-color: transparent;
92
+ border-color: var(--font-color-alpha-10);
93
+
94
+ &:hover,
95
+ &:focus,
96
+ &:active,
97
+ &:focus-within,
98
+ &:has(:hover),
99
+ &:has(:active) {
27
100
  border-color: var(--font-color-alpha-20);
28
101
  }
29
102
 
30
- &::placeholder {
31
- opacity: var(--opacity-default-10);
103
+ &:focus,
104
+ &:active,
105
+ &:focus-within,
106
+ &:has(:active) {
107
+ box-shadow: 0 0 0 var(--measurement-small-30) var(--font-color-alpha-10);
108
+ }
109
+
110
+ &[data-error="true"] {
111
+ color: var(--color-red);
112
+ border-color: var(--alpha-red-10);
113
+
114
+ &:hover,
115
+ &:focus,
116
+ &:active,
117
+ &:focus-within,
118
+ &:has(:hover),
119
+ &:has(:active) {
120
+ background-color: var(--alpha-red-10);
121
+ box-shadow: 0 0 0 var(--measurement-small-30) var(--alpha-red-10);
122
+ }
123
+ }
124
+ `;
125
+
126
+ export const OTPCell = styled.input`
127
+ &[data-raw="false"] {
128
+ ${OTPCellDefaultStyles}
129
+ ${OTPShapeStyles}
32
130
  }
33
131
  `;