@bronzelabs/oakma-ui 0.0.1
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/.prettierrc.cjs +25 -0
- package/.storybook/components/ActionButton.tsx +44 -0
- package/.storybook/components/DummyIcons.tsx +47 -0
- package/.storybook/components/index.ts +2 -0
- package/.storybook/docs/blocks/ImportStatement.tsx +52 -0
- package/.storybook/docs/blocks/index.ts +1 -0
- package/.storybook/docs/page.tsx +41 -0
- package/.storybook/main.ts +21 -0
- package/.storybook/postcss.config.cjs +8 -0
- package/.storybook/preview-body.html +20 -0
- package/.storybook/preview-head.html +6 -0
- package/.storybook/preview.tsx +30 -0
- package/.storybook/tailwind.css +6 -0
- package/.storybook/utils/index.ts +2 -0
- package/.storybook/utils/renderAsReact.tsx +30 -0
- package/.storybook/utils/renderDocsWithProps.tsx +22 -0
- package/@types/markdown.d.ts +4 -0
- package/README.md +3 -0
- package/eslint.config.js +91 -0
- package/package.json +63 -0
- package/postcss.config.cjs +8 -0
- package/scripts/release.sh +76 -0
- package/src/components/Button/Button.stories.tsx +314 -0
- package/src/components/Button/Button.tsx +132 -0
- package/src/components/Button/index.ts +2 -0
- package/src/components/Button/types.ts +19 -0
- package/src/components/Checkbox/Checkbox.stories.tsx +152 -0
- package/src/components/Checkbox/Checkbox.tsx +90 -0
- package/src/components/Checkbox/index.ts +2 -0
- package/src/components/Checkbox/types.ts +6 -0
- package/src/components/Chip/Chip.stories.tsx +146 -0
- package/src/components/Chip/Chip.tsx +59 -0
- package/src/components/Chip/index.ts +2 -0
- package/src/components/Chip/types.ts +6 -0
- package/src/components/Drawer/Drawer.docs.md +88 -0
- package/src/components/Drawer/Drawer.stories.tsx +239 -0
- package/src/components/Drawer/Drawer.tsx +194 -0
- package/src/components/Drawer/index.ts +3 -0
- package/src/components/Drawer/types.ts +3 -0
- package/src/components/Dropdown/AsyncDropdown.tsx +105 -0
- package/src/components/Dropdown/Dropdown.docs.md +33 -0
- package/src/components/Dropdown/Dropdown.stories.tsx +419 -0
- package/src/components/Dropdown/Dropdown.tsx +104 -0
- package/src/components/Dropdown/MultiValue.tsx +19 -0
- package/src/components/Dropdown/ValueContainer.tsx +114 -0
- package/src/components/Dropdown/index.ts +4 -0
- package/src/components/Dropdown/types.ts +29 -0
- package/src/components/Dropdown/useDropdown.tsx +257 -0
- package/src/components/Logo/Logo.stories.tsx +130 -0
- package/src/components/Logo/Logo.tsx +80 -0
- package/src/components/Logo/index.ts +2 -0
- package/src/components/Modal/Modal.docs.md +94 -0
- package/src/components/Modal/Modal.stories.tsx +318 -0
- package/src/components/Modal/Modal.tsx +217 -0
- package/src/components/Modal/index.ts +1 -0
- package/src/components/MultiSelect/AsyncMultiSelect.tsx +47 -0
- package/src/components/MultiSelect/MultiSelect.docs.md +37 -0
- package/src/components/MultiSelect/MultiSelect.stories.tsx +493 -0
- package/src/components/MultiSelect/MultiSelect.tsx +81 -0
- package/src/components/MultiSelect/index.ts +2 -0
- package/src/components/Notification/Notification.stories.tsx +158 -0
- package/src/components/Notification/Notification.tsx +110 -0
- package/src/components/Notification/index.ts +1 -0
- package/src/components/Notification/types.ts +11 -0
- package/src/components/Notifications/Notifications.docs.md +103 -0
- package/src/components/Notifications/Notifications.stories.tsx +159 -0
- package/src/components/Notifications/Notifications.tsx +90 -0
- package/src/components/Notifications/NotificationsContext.tsx +90 -0
- package/src/components/Notifications/index.ts +7 -0
- package/src/components/Select/Select.stories.tsx +234 -0
- package/src/components/Select/Select.tsx +129 -0
- package/src/components/Select/index.ts +2 -0
- package/src/components/Select/types.ts +1 -0
- package/src/components/Spinner/Spinner.stories.tsx +55 -0
- package/src/components/Spinner/Spinner.tsx +48 -0
- package/src/components/Spinner/index.ts +2 -0
- package/src/components/Spinner/types.ts +8 -0
- package/src/components/TextArea/TextArea.stories.tsx +243 -0
- package/src/components/TextArea/TextArea.tsx +133 -0
- package/src/components/TextArea/index.ts +2 -0
- package/src/components/TextArea/types.ts +4 -0
- package/src/components/TextField/Container.tsx +68 -0
- package/src/components/TextField/ErrorMessage.tsx +37 -0
- package/src/components/TextField/Icon.tsx +77 -0
- package/src/components/TextField/Label.tsx +56 -0
- package/src/components/TextField/NotchBorder.tsx +67 -0
- package/src/components/TextField/index.ts +14 -0
- package/src/components/TextField/types.ts +15 -0
- package/src/components/TextField/useInputKeyboardFocus.tsx +63 -0
- package/src/components/TextInput/TextInput.stories.tsx +384 -0
- package/src/components/TextInput/TextInput.tsx +255 -0
- package/src/components/TextInput/index.ts +2 -0
- package/src/components/TextInput/types.ts +4 -0
- package/src/components/Toggle/Toggle.stories.tsx +142 -0
- package/src/components/Toggle/Toggle.tsx +69 -0
- package/src/components/Toggle/index.ts +1 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useCombinedRefs.ts +37 -0
- package/src/hooks/useEventListener.ts +87 -0
- package/src/hooks/useFocusTrap/createAriaHider.ts +62 -0
- package/src/hooks/useFocusTrap/index.ts +1 -0
- package/src/hooks/useFocusTrap/scopeTab.ts +46 -0
- package/src/hooks/useFocusTrap/tabbable.ts +107 -0
- package/src/hooks/useFocusTrap/useFocusTrap.ts +97 -0
- package/src/hooks/useIsomorphicLayoutEffect.ts +14 -0
- package/src/hooks/useLockBodyScroll.ts +24 -0
- package/src/hooks/useOnClickOutside.ts +53 -0
- package/src/index.ts +22 -0
- package/src/tailwind.css +4 -0
- package/src/types/helpers.ts +11 -0
- package/src/types/polymorphic.ts +39 -0
- package/src/utils/animation/variants.ts +21 -0
- package/src/utils/array/index.ts +1 -0
- package/src/utils/array/uniqBy.ts +12 -0
- package/src/utils/common/index.ts +1 -0
- package/src/utils/common/isFunction.ts +17 -0
- package/src/utils/react/extractDisplayName.ts +15 -0
- package/src/utils/react/index.ts +1 -0
- package/tsconfig.json +16 -0
- package/tsconfig.production.json +19 -0
- package/tsup.config.ts +16 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
|
|
3
|
+
// Components
|
|
4
|
+
import TextArea from "./TextArea"
|
|
5
|
+
|
|
6
|
+
// Types
|
|
7
|
+
import type { Meta, StoryFn } from "@storybook/react-webpack5"
|
|
8
|
+
import { TEXT_FIELD_SIZES, TEXT_FIELD_VARIANTS } from "../TextField"
|
|
9
|
+
|
|
10
|
+
// Utils
|
|
11
|
+
import { DummyPhoneIcon, DummyChevronDownIcon } from "../../../.storybook/components"
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
type TextAreaStoryProps = React.ComponentProps<typeof TextArea>
|
|
21
|
+
|
|
22
|
+
const meta: Meta<TextAreaStoryProps> = {
|
|
23
|
+
title: "Components/TextArea",
|
|
24
|
+
component: TextArea,
|
|
25
|
+
parameters: {
|
|
26
|
+
docs: {
|
|
27
|
+
description: {
|
|
28
|
+
component: "A labelled multi-line text field with optional error state.",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
controls: {
|
|
32
|
+
exclude: ["className", "ref"],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
argTypes: {
|
|
36
|
+
label: {
|
|
37
|
+
type: "string",
|
|
38
|
+
description: "Visible label rendered above the textarea.",
|
|
39
|
+
table: {
|
|
40
|
+
type: { summary: "string" },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
size: {
|
|
44
|
+
type: "string",
|
|
45
|
+
description: "Determines the size of the textarea.",
|
|
46
|
+
table: {
|
|
47
|
+
type: { summary: TEXT_FIELD_SIZES.map(size => `"${size}"`).join(" | ") },
|
|
48
|
+
defaultValue: { summary: '"md"' },
|
|
49
|
+
},
|
|
50
|
+
options: TEXT_FIELD_SIZES,
|
|
51
|
+
control: { type: "select" },
|
|
52
|
+
},
|
|
53
|
+
variant: {
|
|
54
|
+
type: "string",
|
|
55
|
+
description: "Determines the color scheme of the textarea.",
|
|
56
|
+
table: {
|
|
57
|
+
type: { summary: TEXT_FIELD_VARIANTS.map(variant => `"${variant}"`).join(" | ") },
|
|
58
|
+
defaultValue: { summary: '"dark"' },
|
|
59
|
+
},
|
|
60
|
+
options: TEXT_FIELD_VARIANTS,
|
|
61
|
+
control: { type: "select" },
|
|
62
|
+
},
|
|
63
|
+
placeholder: {
|
|
64
|
+
type: "string",
|
|
65
|
+
description: "Placeholder text shown when the textarea is empty.",
|
|
66
|
+
table: {
|
|
67
|
+
type: { summary: "string" },
|
|
68
|
+
defaultValue: { summary: "undefined" },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
showOptional: {
|
|
72
|
+
type: "boolean",
|
|
73
|
+
description: 'Shows an "Optional" tag next to the label when the field is not required.',
|
|
74
|
+
table: {
|
|
75
|
+
type: { summary: "boolean" },
|
|
76
|
+
defaultValue: { summary: "true" },
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
required: {
|
|
80
|
+
type: "boolean",
|
|
81
|
+
description: 'Marks the field as required and hides the "Optional" tag.',
|
|
82
|
+
table: {
|
|
83
|
+
type: { summary: "boolean" },
|
|
84
|
+
defaultValue: { summary: "false" },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
disabled: {
|
|
88
|
+
type: "boolean",
|
|
89
|
+
description: "Disables the textarea.",
|
|
90
|
+
table: {
|
|
91
|
+
type: { summary: "boolean" },
|
|
92
|
+
defaultValue: { summary: "false" },
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
leadingIcon: {
|
|
96
|
+
description: "Renders an icon inside the field, aligned to the left.",
|
|
97
|
+
table: {
|
|
98
|
+
type: { summary: "{ icon: ReactNode }" },
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
trailingIcon: {
|
|
102
|
+
description:
|
|
103
|
+
"Renders an icon inside the field, aligned to the right. Pass `onClick` to make it a button.",
|
|
104
|
+
table: {
|
|
105
|
+
type: {
|
|
106
|
+
summary:
|
|
107
|
+
"{ icon: ReactNode, onClick?: function, type?: 'button' | 'submit' | 'reset', disabled?: boolean }",
|
|
108
|
+
},
|
|
109
|
+
defaultValue: { summary: "undefined" },
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
error: {
|
|
113
|
+
description: "Renders the field in an error state and displays the provided message.",
|
|
114
|
+
table: {
|
|
115
|
+
type: { summary: "{ message: string }" },
|
|
116
|
+
defaultValue: { summary: "undefined" },
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
args: {
|
|
121
|
+
id: "text-area",
|
|
122
|
+
label: "Label",
|
|
123
|
+
showOptional: true,
|
|
124
|
+
required: false,
|
|
125
|
+
disabled: false,
|
|
126
|
+
rows: 4,
|
|
127
|
+
placeholder: "Enter a description…",
|
|
128
|
+
},
|
|
129
|
+
decorators: [
|
|
130
|
+
Story => (
|
|
131
|
+
<div style={{ width: 380 }}>
|
|
132
|
+
<Story />
|
|
133
|
+
</div>
|
|
134
|
+
),
|
|
135
|
+
],
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const Template: StoryFn<TextAreaStoryProps> = args => <TextArea {...args} />
|
|
139
|
+
|
|
140
|
+
const TrailingIconTemplate: StoryFn<TextAreaStoryProps> = args => (
|
|
141
|
+
<div className="flex flex-col gap-6">
|
|
142
|
+
<TextArea {...args} trailingIcon={{ icon: <DummyChevronDownIcon className="size-5" /> }} />
|
|
143
|
+
<TextArea
|
|
144
|
+
{...args}
|
|
145
|
+
trailingIcon={{
|
|
146
|
+
icon: <DummyChevronDownIcon className="size-5" />,
|
|
147
|
+
onClick: () => alert("Trailing icon clicked!"),
|
|
148
|
+
}}
|
|
149
|
+
/>
|
|
150
|
+
</div>
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
const VariantsTemplate: StoryFn<TextAreaStoryProps> = args => (
|
|
154
|
+
<div className="flex flex-col">
|
|
155
|
+
<div className="bg-[#fff] p-6">
|
|
156
|
+
<TextArea {...args} variant="dark" label="Dark Variant" />
|
|
157
|
+
</div>
|
|
158
|
+
<div className="bg-[#333] p-6">
|
|
159
|
+
<TextArea {...args} className="bg-inherit" variant="light" label="Light Variant" />
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
const Default = Template.bind({})
|
|
165
|
+
const Variants = VariantsTemplate.bind({})
|
|
166
|
+
const WithLeadingIcon = Template.bind({})
|
|
167
|
+
const WithTrailingIcon = TrailingIconTemplate.bind({})
|
|
168
|
+
const Required = Template.bind({})
|
|
169
|
+
const WithError = Template.bind({})
|
|
170
|
+
const Disabled = Template.bind({})
|
|
171
|
+
|
|
172
|
+
// Args
|
|
173
|
+
WithLeadingIcon.args = {
|
|
174
|
+
leadingIcon: { icon: <DummyPhoneIcon className="size-5" /> },
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
WithTrailingIcon.args = {}
|
|
178
|
+
|
|
179
|
+
Required.args = {
|
|
180
|
+
required: true,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
WithError.args = {
|
|
184
|
+
error: { message: "This field is required." },
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
Disabled.args = {
|
|
188
|
+
placeholder: "Disabled",
|
|
189
|
+
disabled: true,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Parameters
|
|
193
|
+
Variants.parameters = {
|
|
194
|
+
docs: {
|
|
195
|
+
description: {
|
|
196
|
+
story: "The `variant` prop changes the color scheme of the textarea.",
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
WithLeadingIcon.parameters = {
|
|
202
|
+
docs: {
|
|
203
|
+
description: {
|
|
204
|
+
story: "The `leadingIcon` prop renders an icon inside the field, aligned to the left.",
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
WithTrailingIcon.parameters = {
|
|
210
|
+
docs: {
|
|
211
|
+
description: {
|
|
212
|
+
story:
|
|
213
|
+
"The `trailingIcon` prop renders an icon inside the field, aligned to the right. Pass `onTrailingIconClick` to make it a button.",
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
Required.parameters = {
|
|
219
|
+
docs: {
|
|
220
|
+
description: {
|
|
221
|
+
story: 'Setting `required` hides the "Optional" tag and marks the textarea as required.',
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
WithError.parameters = {
|
|
227
|
+
docs: {
|
|
228
|
+
description: {
|
|
229
|
+
story: "Pass an `error` object with a `message` string to display an inline error.",
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
Disabled.parameters = {
|
|
235
|
+
docs: {
|
|
236
|
+
description: {
|
|
237
|
+
story: "Setting `disabled` prevents interaction and visually dims the field.",
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export { Default, Variants, WithLeadingIcon, WithTrailingIcon, Required, WithError, Disabled }
|
|
243
|
+
export default meta
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React from "react"
|
|
4
|
+
|
|
5
|
+
// Components
|
|
6
|
+
import { Container, ErrorMessage, Icon, Label, useInputKeyboardFocus } from "../TextField"
|
|
7
|
+
|
|
8
|
+
// Types
|
|
9
|
+
import type { TextAreaVariant, TextAreaSize } from "./types"
|
|
10
|
+
|
|
11
|
+
// Utils
|
|
12
|
+
import clsx from "clsx"
|
|
13
|
+
|
|
14
|
+
// Params
|
|
15
|
+
interface TextAreaProps extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "id"> {
|
|
16
|
+
id: string
|
|
17
|
+
label?: string
|
|
18
|
+
size?: TextAreaSize
|
|
19
|
+
variant?: TextAreaVariant
|
|
20
|
+
leadingIcon?: { icon: React.ReactNode }
|
|
21
|
+
trailingIcon?: {
|
|
22
|
+
icon: React.ReactNode
|
|
23
|
+
onClick?: React.MouseEventHandler<HTMLButtonElement>
|
|
24
|
+
type?: "button" | "submit" | "reset"
|
|
25
|
+
disabled?: boolean
|
|
26
|
+
}
|
|
27
|
+
showOptional?: boolean
|
|
28
|
+
error?: {
|
|
29
|
+
message: string
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/*
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
const TextArea: React.FC<TextAreaProps> = ({
|
|
41
|
+
id,
|
|
42
|
+
label,
|
|
43
|
+
size = "md",
|
|
44
|
+
variant = "dark",
|
|
45
|
+
error,
|
|
46
|
+
leadingIcon,
|
|
47
|
+
trailingIcon,
|
|
48
|
+
showOptional = true,
|
|
49
|
+
className,
|
|
50
|
+
required,
|
|
51
|
+
disabled,
|
|
52
|
+
onFocus,
|
|
53
|
+
onBlur,
|
|
54
|
+
...rest
|
|
55
|
+
}) => {
|
|
56
|
+
const { keyboardFocused, handleMouseDown, handleMouseUp, handleFocus, handleBlur } =
|
|
57
|
+
useInputKeyboardFocus({ onFocus, onBlur })
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div className={className}>
|
|
61
|
+
<Container
|
|
62
|
+
keyboardFocused={keyboardFocused}
|
|
63
|
+
onMouseDown={handleMouseDown}
|
|
64
|
+
onMouseUp={handleMouseUp}
|
|
65
|
+
className="items-start!"
|
|
66
|
+
size={size}
|
|
67
|
+
hasButton={false}
|
|
68
|
+
label={label}
|
|
69
|
+
showOptional={showOptional}
|
|
70
|
+
required={required}
|
|
71
|
+
variant={variant}
|
|
72
|
+
>
|
|
73
|
+
{label && (
|
|
74
|
+
<Label
|
|
75
|
+
inputId={id}
|
|
76
|
+
label={label}
|
|
77
|
+
showOptional={showOptional}
|
|
78
|
+
required={required}
|
|
79
|
+
variant={variant}
|
|
80
|
+
/>
|
|
81
|
+
)}
|
|
82
|
+
|
|
83
|
+
{!!leadingIcon?.icon && (
|
|
84
|
+
<Icon position="leading" disabled={disabled} fieldSize={size} variant={variant}>
|
|
85
|
+
{leadingIcon.icon}
|
|
86
|
+
</Icon>
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
<textarea
|
|
90
|
+
id={id}
|
|
91
|
+
className={clsx(
|
|
92
|
+
"min-w-0 grow resize-none py-3.5 placeholder:text-neutral-30 focus:outline-hidden",
|
|
93
|
+
variant === "dark" && "text-neutral-100",
|
|
94
|
+
variant === "light" && "text-white",
|
|
95
|
+
size === "sm" && [
|
|
96
|
+
"min-h-6 text-sm",
|
|
97
|
+
!!leadingIcon?.icon ? "pl-10" : "pl-3",
|
|
98
|
+
!!trailingIcon?.icon ? "pr-10" : "pr-3",
|
|
99
|
+
],
|
|
100
|
+
size === "md" && [
|
|
101
|
+
"min-h-12 text-base",
|
|
102
|
+
!!leadingIcon?.icon ? "pl-12" : "pl-5",
|
|
103
|
+
!!trailingIcon?.icon ? "pr-12" : "pr-5",
|
|
104
|
+
],
|
|
105
|
+
)}
|
|
106
|
+
aria-describedby={error ? `${id}-error` : undefined}
|
|
107
|
+
aria-invalid={!!error}
|
|
108
|
+
required={required}
|
|
109
|
+
onFocus={handleFocus}
|
|
110
|
+
onBlur={handleBlur}
|
|
111
|
+
disabled={disabled}
|
|
112
|
+
{...rest}
|
|
113
|
+
/>
|
|
114
|
+
|
|
115
|
+
{!!trailingIcon?.icon && (
|
|
116
|
+
<Icon
|
|
117
|
+
position="trailing"
|
|
118
|
+
onClick={trailingIcon?.onClick}
|
|
119
|
+
disabled={trailingIcon?.disabled || disabled}
|
|
120
|
+
fieldSize={size}
|
|
121
|
+
variant={variant}
|
|
122
|
+
>
|
|
123
|
+
{trailingIcon.icon}
|
|
124
|
+
</Icon>
|
|
125
|
+
)}
|
|
126
|
+
</Container>
|
|
127
|
+
{error && <ErrorMessage id={id} error={error} />}
|
|
128
|
+
</div>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export default TextArea
|
|
133
|
+
export type { TextAreaProps }
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
|
|
3
|
+
// Components
|
|
4
|
+
import NotchBorder from "./NotchBorder"
|
|
5
|
+
|
|
6
|
+
// Utils
|
|
7
|
+
import { clsx } from "clsx"
|
|
8
|
+
|
|
9
|
+
// Params
|
|
10
|
+
interface ContainerProps {
|
|
11
|
+
keyboardFocused?: boolean
|
|
12
|
+
onMouseDown?: React.MouseEventHandler<HTMLDivElement>
|
|
13
|
+
onMouseUp?: React.MouseEventHandler<HTMLDivElement>
|
|
14
|
+
size: "sm" | "md"
|
|
15
|
+
variant?: "dark" | "light"
|
|
16
|
+
hasButton: boolean
|
|
17
|
+
label?: string
|
|
18
|
+
showOptional?: boolean
|
|
19
|
+
required?: boolean
|
|
20
|
+
children: React.ReactNode
|
|
21
|
+
className?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/*
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const Container: React.FC<ContainerProps> = ({
|
|
32
|
+
keyboardFocused,
|
|
33
|
+
onMouseDown,
|
|
34
|
+
onMouseUp,
|
|
35
|
+
size = "md",
|
|
36
|
+
variant = "dark",
|
|
37
|
+
hasButton = false,
|
|
38
|
+
label,
|
|
39
|
+
showOptional,
|
|
40
|
+
required,
|
|
41
|
+
className,
|
|
42
|
+
children,
|
|
43
|
+
}) => {
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
className={clsx(
|
|
47
|
+
"group relative mt-2 flex items-center rounded-[1.75rem] transition-colors",
|
|
48
|
+
size === "sm" && "min-h-10",
|
|
49
|
+
size === "md" && "min-h-14",
|
|
50
|
+
hasButton && "pr-1",
|
|
51
|
+
keyboardFocused && "outline-browser",
|
|
52
|
+
className,
|
|
53
|
+
)}
|
|
54
|
+
onMouseDown={onMouseDown}
|
|
55
|
+
onMouseUp={onMouseUp}
|
|
56
|
+
>
|
|
57
|
+
<NotchBorder
|
|
58
|
+
label={label}
|
|
59
|
+
showOptional={showOptional}
|
|
60
|
+
required={required}
|
|
61
|
+
variant={variant}
|
|
62
|
+
/>
|
|
63
|
+
{children}
|
|
64
|
+
</div>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default Container
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
|
|
3
|
+
// Components
|
|
4
|
+
import { ExclamationCircleIcon } from "@heroicons/react/20/solid"
|
|
5
|
+
|
|
6
|
+
// Utils
|
|
7
|
+
import { clsx } from "clsx"
|
|
8
|
+
|
|
9
|
+
// Params
|
|
10
|
+
interface ErrorMessageProps {
|
|
11
|
+
id: string
|
|
12
|
+
error: {
|
|
13
|
+
message: string
|
|
14
|
+
}
|
|
15
|
+
className?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const ErrorMessage: React.FC<ErrorMessageProps> = ({ id, className, error }) => {
|
|
27
|
+
return (
|
|
28
|
+
<div className={clsx("mt-1 flex items-center gap-0.5 text-error-100", className)}>
|
|
29
|
+
<ExclamationCircleIcon className="ml-1.5 size-3.5" />
|
|
30
|
+
<span id={`${id}-error`} className="text-xs">
|
|
31
|
+
{error.message}
|
|
32
|
+
</span>
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default ErrorMessage
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
|
|
3
|
+
// Utils
|
|
4
|
+
import { clsx } from "clsx"
|
|
5
|
+
|
|
6
|
+
// Types
|
|
7
|
+
import type { TextFieldSize, TextFieldVariant } from "./types"
|
|
8
|
+
|
|
9
|
+
// Params
|
|
10
|
+
interface IconProps {
|
|
11
|
+
position: "leading" | "trailing"
|
|
12
|
+
fieldSize: TextFieldSize
|
|
13
|
+
variant: TextFieldVariant
|
|
14
|
+
onClick?: React.MouseEventHandler<HTMLButtonElement>
|
|
15
|
+
disabled?: boolean
|
|
16
|
+
buttonType?: "button" | "submit" | "reset"
|
|
17
|
+
children: React.ReactNode
|
|
18
|
+
className?: string
|
|
19
|
+
name?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const Icon: React.FC<IconProps> = ({
|
|
31
|
+
position,
|
|
32
|
+
fieldSize = "md",
|
|
33
|
+
variant = "dark",
|
|
34
|
+
onClick,
|
|
35
|
+
buttonType,
|
|
36
|
+
disabled,
|
|
37
|
+
children,
|
|
38
|
+
className,
|
|
39
|
+
name,
|
|
40
|
+
}) => {
|
|
41
|
+
const isButton = typeof onClick === "function"
|
|
42
|
+
const Component = (isButton ? "button" : "div") as React.ElementType
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<Component
|
|
46
|
+
className={clsx(
|
|
47
|
+
"absolute flex grow-0 items-center justify-center rounded-full transition-colors ease-out",
|
|
48
|
+
fieldSize === "sm" && [
|
|
49
|
+
"top-2.125 size-6.5",
|
|
50
|
+
// isButton && "w-6.5",
|
|
51
|
+
position === "leading" ? "left-3.75" : "right-3.75",
|
|
52
|
+
],
|
|
53
|
+
fieldSize === "md" && [
|
|
54
|
+
"top-3.125 size-8.5",
|
|
55
|
+
// isButton && "w-8.5",
|
|
56
|
+
position === "leading" ? "left-3.5" : "right-3.5",
|
|
57
|
+
],
|
|
58
|
+
variant === "dark" && "text-neutral-50",
|
|
59
|
+
variant === "light" && "text-neutral-30",
|
|
60
|
+
isButton && [
|
|
61
|
+
"group-has-[:is(input,textarea,select):not(:disabled,:user-invalid,[aria-invalid=true])]:cursor-pointer",
|
|
62
|
+
variant === "dark" &&
|
|
63
|
+
"group-has-[:is(input,textarea,select):not(:disabled,:user-invalid,[aria-invalid=true])]:hover:bg-neutral-50/10 group-has-[:is(input,textarea,select):not(:disabled,:user-invalid,[aria-invalid=true])]:hover:text-neutral-100 focus-visible:bg-neutral-50/10 group-has-[:is(input,textarea,select):not(:disabled,:user-invalid,[aria-invalid=true])]:focus-visible:text-neutral-100 group-has-[:is(input,textarea,select):not(:disabled,:user-invalid,[aria-invalid=true])]:active:bg-neutral-50/20",
|
|
64
|
+
variant === "light" &&
|
|
65
|
+
"group-has-[:is(input,textarea,select):not(:disabled,:user-invalid,[aria-invalid=true])]:hover:bg-white/10 group-has-[:is(input,textarea,select):not(:disabled,:user-invalid,[aria-invalid=true])]:hover:text-white focus-visible:bg-white/10 group-has-[:is(input,textarea,select):not(:disabled,:user-invalid,[aria-invalid=true])]:focus-visible:text-white group-has-[:is(input,textarea,select):not(:disabled,:user-invalid,[aria-invalid=true])]:active:bg-white/20",
|
|
66
|
+
],
|
|
67
|
+
className,
|
|
68
|
+
)}
|
|
69
|
+
{...(!isButton && { "aria-hidden": true })}
|
|
70
|
+
{...(isButton && { name, onClick, disabled, type: buttonType || "button" })}
|
|
71
|
+
>
|
|
72
|
+
{children}
|
|
73
|
+
</Component>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export default Icon
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React from "react"
|
|
4
|
+
|
|
5
|
+
// Utils
|
|
6
|
+
import { clsx } from "clsx"
|
|
7
|
+
|
|
8
|
+
// Params
|
|
9
|
+
interface LabelProps {
|
|
10
|
+
/** Unique identifier for the input element associated with this label */
|
|
11
|
+
inputId: string
|
|
12
|
+
label: string
|
|
13
|
+
variant?: "dark" | "light"
|
|
14
|
+
showOptional?: boolean
|
|
15
|
+
required?: boolean
|
|
16
|
+
className?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const Label: React.FC<LabelProps> = ({
|
|
28
|
+
inputId,
|
|
29
|
+
label,
|
|
30
|
+
variant = "dark",
|
|
31
|
+
showOptional,
|
|
32
|
+
required,
|
|
33
|
+
className,
|
|
34
|
+
}) => {
|
|
35
|
+
return (
|
|
36
|
+
<div
|
|
37
|
+
className={clsx(
|
|
38
|
+
"absolute -top-2 left-4.25 z-10 flex flex-nowrap items-center justify-start gap-1 px-1 font-medium whitespace-nowrap group-has-user-invalid:text-error-100! group-has-aria-invalid:text-error-100!",
|
|
39
|
+
variant === "dark" &&
|
|
40
|
+
"text-neutral-50 group-focus-within:text-neutral-100 group-has-[:is(input,textarea,select):not(:disabled)]:group-hover:text-neutral-100",
|
|
41
|
+
variant === "light" &&
|
|
42
|
+
"text-neutral-20 group-focus-within:text-white group-has-[:is(input,textarea,select):not(:disabled)]:group-hover:text-white",
|
|
43
|
+
className,
|
|
44
|
+
)}
|
|
45
|
+
>
|
|
46
|
+
<label htmlFor={inputId} className="text-sm leading-none">
|
|
47
|
+
{label}
|
|
48
|
+
</label>
|
|
49
|
+
{showOptional && !required && (
|
|
50
|
+
<span className="inline-block text-xs leading-none">(Optional)</span>
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default Label
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React from "react"
|
|
4
|
+
|
|
5
|
+
// Utils
|
|
6
|
+
import { clsx } from "clsx"
|
|
7
|
+
|
|
8
|
+
// Params
|
|
9
|
+
interface NotchBorderProps {
|
|
10
|
+
label?: string
|
|
11
|
+
showOptional?: boolean
|
|
12
|
+
required?: boolean
|
|
13
|
+
variant?: "dark" | "light"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const NotchBorder: React.FC<NotchBorderProps> = ({
|
|
17
|
+
label,
|
|
18
|
+
showOptional,
|
|
19
|
+
required,
|
|
20
|
+
variant = "dark",
|
|
21
|
+
}) => {
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
className={clsx(
|
|
25
|
+
"pointer-events-none absolute inset-0 flex",
|
|
26
|
+
"group-has-user-invalid:[--border-color:var(--color-error-100)]! group-has-aria-invalid:[--border-color:var(--color-error-100)]!",
|
|
27
|
+
variant === "dark" &&
|
|
28
|
+
"[--border-color:var(--color-stroke-dark)] group-has-focus-visible:[--border-color:var(--color-neutral-100)] group-has-[:is(input,textarea,select):not(:disabled)]:group-hover:[--border-color:var(--color-neutral-100)]",
|
|
29
|
+
variant === "light" &&
|
|
30
|
+
"[--border-color:var(--color-stroke-light)] group-has-focus-visible:[--border-color:var(--color-white)] group-has-[:is(input,textarea,select):not(:disabled)]:group-hover:[--border-color:var(--color-white)]",
|
|
31
|
+
)}
|
|
32
|
+
aria-hidden="true"
|
|
33
|
+
>
|
|
34
|
+
{/* Left cap */}
|
|
35
|
+
<div
|
|
36
|
+
className="relative -mr-9.75 w-14 shrink-0 rounded-l-[1.75rem] border-y-[1.5px] border-l-[1.5px] border-(--border-color) transition-colors"
|
|
37
|
+
style={
|
|
38
|
+
label
|
|
39
|
+
? { clipPath: "polygon(0% 0%, 0% 100%, 100% 100%, 100% 50%, 30% 50%, 30% 0%)" }
|
|
40
|
+
: undefined
|
|
41
|
+
}
|
|
42
|
+
/>
|
|
43
|
+
|
|
44
|
+
{/* Notch — no top border; invisible clone auto-sizes the gap to match the label */}
|
|
45
|
+
{label && (
|
|
46
|
+
<div className="relative shrink-0 px-1 transition-colors before:absolute before:right-0 before:bottom-0 before:left-9.75 before:h-[1.5px] before:bg-(--border-color) before:transition-colors">
|
|
47
|
+
<div className="invisible flex items-center gap-1 font-medium whitespace-nowrap">
|
|
48
|
+
<span className="text-sm leading-none">{label}</span>
|
|
49
|
+
{showOptional && !required && (
|
|
50
|
+
<span className="inline-block text-xs leading-none">(Optional)</span>
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
)}
|
|
55
|
+
|
|
56
|
+
{/* Right cap */}
|
|
57
|
+
<div
|
|
58
|
+
className={clsx(
|
|
59
|
+
"flex-1 rounded-r-[1.75rem] border-y-[1.5px] border-r-[1.5px] border-(--border-color) transition-colors",
|
|
60
|
+
!label && "ml-9.75",
|
|
61
|
+
)}
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default NotchBorder
|