@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.
- package/CHANGELOG.md +12 -0
- package/dist/index.d.mts +615 -51
- package/dist/index.d.ts +615 -51
- package/dist/index.js +3154 -660
- package/dist/index.mjs +3131 -661
- package/package.json +12 -12
- package/src/__tests__/Avatar.test.tsx +55 -55
- package/src/__tests__/MessageBubble.test.tsx +179 -0
- package/src/__tests__/Shimmer.test.tsx +122 -0
- package/src/__tests__/Tree.test.tsx +275 -0
- package/src/accordion/Accordion.stories.tsx +6 -4
- package/src/accordion/hooks/index.tsx +3 -1
- package/src/accordion/index.tsx +1 -2
- package/src/avatar/Avatar.stories.tsx +37 -7
- package/src/avatar/index.tsx +90 -19
- package/src/avatar/styles/index.ts +58 -12
- package/src/badge/Badge.stories.tsx +27 -5
- package/src/badge/index.tsx +21 -14
- package/src/badge/styles/index.ts +69 -40
- package/src/button/Button.stories.tsx +40 -27
- package/src/button/index.tsx +13 -9
- package/src/button/styles/index.ts +308 -47
- package/src/card/index.tsx +2 -4
- package/src/checkbox/Checkbox.stories.tsx +72 -33
- package/src/checkbox/hooks/index.tsx +5 -1
- package/src/checkbox/index.tsx +8 -6
- package/src/checkbox/styles/index.ts +239 -19
- package/src/collapsible/Collapsible.stories.tsx +6 -4
- package/src/collapsible/hooks/index.tsx +3 -1
- package/src/dialog/Dialog.stories.tsx +173 -31
- package/src/dialog/hooks/index.tsx +5 -1
- package/src/dialog/styles/index.ts +15 -8
- package/src/dropdown/Dropdown.stories.tsx +61 -23
- package/src/dropdown/hooks/index.tsx +3 -1
- package/src/dropdown/index.tsx +51 -40
- package/src/dropdown/styles/index.ts +30 -19
- package/src/field/Field.stories.tsx +183 -24
- package/src/field/hooks/index.tsx +5 -1
- package/src/field/index.tsx +930 -13
- package/src/field/styles/index.ts +246 -14
- package/src/field/types/index.ts +31 -0
- package/src/field/utils/index.ts +201 -0
- package/src/index.ts +8 -1
- package/src/message-bubble/MessageBubble.stories.tsx +138 -0
- package/src/message-bubble/hooks/index.tsx +41 -0
- package/src/message-bubble/index.tsx +171 -0
- package/src/message-bubble/styles/index.ts +58 -0
- package/src/otp-field/OTPField.stories.tsx +22 -24
- package/src/otp-field/hooks/index.tsx +3 -1
- package/src/otp-field/index.tsx +14 -3
- package/src/otp-field/styles/index.ts +114 -16
- package/src/otp-field/types/index.ts +9 -1
- package/src/overlay/styles/index.ts +1 -0
- package/src/ruler/Ruler.stories.tsx +43 -0
- package/src/ruler/constants/index.ts +3 -0
- package/src/ruler/hooks/index.tsx +53 -0
- package/src/ruler/index.tsx +239 -0
- package/src/ruler/styles/index.tsx +154 -0
- package/src/ruler/types/index.ts +17 -0
- package/src/select/Select.stories.tsx +91 -0
- package/src/select/hooks/index.tsx +71 -0
- package/src/select/index.tsx +331 -0
- package/src/select/styles/index.tsx +156 -0
- package/src/sheet/hooks/index.tsx +5 -1
- package/src/shimmer/Shimmer.stories.tsx +97 -0
- package/src/shimmer/index.tsx +64 -0
- package/src/shimmer/styles/index.ts +33 -0
- package/src/skeleton/index.tsx +7 -6
- package/src/spinner/Spinner.stories.tsx +29 -4
- package/src/spinner/index.tsx +16 -6
- package/src/spinner/styles/index.ts +41 -22
- package/src/switch/Switch.stories.tsx +46 -17
- package/src/switch/hooks/index.tsx +5 -1
- package/src/switch/index.tsx +5 -8
- package/src/switch/styles/index.ts +45 -45
- package/src/tabs/Tabs.stories.tsx +43 -15
- package/src/tabs/hooks/index.tsx +5 -1
- package/src/text-area/Textarea.stories.tsx +45 -8
- package/src/text-area/index.tsx +9 -6
- package/src/text-area/styles/index.ts +1 -1
- package/src/toggle/Toggle.stories.tsx +6 -4
- package/src/toolbar/hooks/index.tsx +5 -1
- package/src/tree/Tree.stories.tsx +141 -0
- package/src/tree/hooks/tree-node-provider.tsx +50 -0
- package/src/tree/hooks/tree-provider.tsx +75 -0
- package/src/tree/index.tsx +231 -0
- package/src/tree/styles/index.ts +23 -0
- package/tsconfig.build.json +20 -0
- package/tsconfig.json +1 -3
- package/src/privacy-field/PrivacyField.stories.tsx +0 -29
- package/src/privacy-field/index.tsx +0 -56
- 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
|
-
<
|
|
13
|
-
<
|
|
14
|
-
|
|
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
|
-
}
|
|
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
|
-
<
|
|
31
|
-
<
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
<OTPField.
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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);
|
package/src/otp-field/index.tsx
CHANGED
|
@@ -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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
font-size: var(--fontsize-medium-10);
|
|
13
|
-
font-weight: 500;
|
|
41
|
+
transition: all ease-in-out 0.2s;
|
|
14
42
|
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
20
|
-
outline: none;
|
|
73
|
+
border-color: var(--font-color-alpha-10);
|
|
21
74
|
|
|
22
|
-
&:
|
|
23
|
-
|
|
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
|
-
&:
|
|
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
|
-
|
|
31
|
-
|
|
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
|
`;
|