@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.
Files changed (121) hide show
  1. package/.prettierrc.cjs +25 -0
  2. package/.storybook/components/ActionButton.tsx +44 -0
  3. package/.storybook/components/DummyIcons.tsx +47 -0
  4. package/.storybook/components/index.ts +2 -0
  5. package/.storybook/docs/blocks/ImportStatement.tsx +52 -0
  6. package/.storybook/docs/blocks/index.ts +1 -0
  7. package/.storybook/docs/page.tsx +41 -0
  8. package/.storybook/main.ts +21 -0
  9. package/.storybook/postcss.config.cjs +8 -0
  10. package/.storybook/preview-body.html +20 -0
  11. package/.storybook/preview-head.html +6 -0
  12. package/.storybook/preview.tsx +30 -0
  13. package/.storybook/tailwind.css +6 -0
  14. package/.storybook/utils/index.ts +2 -0
  15. package/.storybook/utils/renderAsReact.tsx +30 -0
  16. package/.storybook/utils/renderDocsWithProps.tsx +22 -0
  17. package/@types/markdown.d.ts +4 -0
  18. package/README.md +3 -0
  19. package/eslint.config.js +91 -0
  20. package/package.json +63 -0
  21. package/postcss.config.cjs +8 -0
  22. package/scripts/release.sh +76 -0
  23. package/src/components/Button/Button.stories.tsx +314 -0
  24. package/src/components/Button/Button.tsx +132 -0
  25. package/src/components/Button/index.ts +2 -0
  26. package/src/components/Button/types.ts +19 -0
  27. package/src/components/Checkbox/Checkbox.stories.tsx +152 -0
  28. package/src/components/Checkbox/Checkbox.tsx +90 -0
  29. package/src/components/Checkbox/index.ts +2 -0
  30. package/src/components/Checkbox/types.ts +6 -0
  31. package/src/components/Chip/Chip.stories.tsx +146 -0
  32. package/src/components/Chip/Chip.tsx +59 -0
  33. package/src/components/Chip/index.ts +2 -0
  34. package/src/components/Chip/types.ts +6 -0
  35. package/src/components/Drawer/Drawer.docs.md +88 -0
  36. package/src/components/Drawer/Drawer.stories.tsx +239 -0
  37. package/src/components/Drawer/Drawer.tsx +194 -0
  38. package/src/components/Drawer/index.ts +3 -0
  39. package/src/components/Drawer/types.ts +3 -0
  40. package/src/components/Dropdown/AsyncDropdown.tsx +105 -0
  41. package/src/components/Dropdown/Dropdown.docs.md +33 -0
  42. package/src/components/Dropdown/Dropdown.stories.tsx +419 -0
  43. package/src/components/Dropdown/Dropdown.tsx +104 -0
  44. package/src/components/Dropdown/MultiValue.tsx +19 -0
  45. package/src/components/Dropdown/ValueContainer.tsx +114 -0
  46. package/src/components/Dropdown/index.ts +4 -0
  47. package/src/components/Dropdown/types.ts +29 -0
  48. package/src/components/Dropdown/useDropdown.tsx +257 -0
  49. package/src/components/Logo/Logo.stories.tsx +130 -0
  50. package/src/components/Logo/Logo.tsx +80 -0
  51. package/src/components/Logo/index.ts +2 -0
  52. package/src/components/Modal/Modal.docs.md +94 -0
  53. package/src/components/Modal/Modal.stories.tsx +318 -0
  54. package/src/components/Modal/Modal.tsx +217 -0
  55. package/src/components/Modal/index.ts +1 -0
  56. package/src/components/MultiSelect/AsyncMultiSelect.tsx +47 -0
  57. package/src/components/MultiSelect/MultiSelect.docs.md +37 -0
  58. package/src/components/MultiSelect/MultiSelect.stories.tsx +493 -0
  59. package/src/components/MultiSelect/MultiSelect.tsx +81 -0
  60. package/src/components/MultiSelect/index.ts +2 -0
  61. package/src/components/Notification/Notification.stories.tsx +158 -0
  62. package/src/components/Notification/Notification.tsx +110 -0
  63. package/src/components/Notification/index.ts +1 -0
  64. package/src/components/Notification/types.ts +11 -0
  65. package/src/components/Notifications/Notifications.docs.md +103 -0
  66. package/src/components/Notifications/Notifications.stories.tsx +159 -0
  67. package/src/components/Notifications/Notifications.tsx +90 -0
  68. package/src/components/Notifications/NotificationsContext.tsx +90 -0
  69. package/src/components/Notifications/index.ts +7 -0
  70. package/src/components/Select/Select.stories.tsx +234 -0
  71. package/src/components/Select/Select.tsx +129 -0
  72. package/src/components/Select/index.ts +2 -0
  73. package/src/components/Select/types.ts +1 -0
  74. package/src/components/Spinner/Spinner.stories.tsx +55 -0
  75. package/src/components/Spinner/Spinner.tsx +48 -0
  76. package/src/components/Spinner/index.ts +2 -0
  77. package/src/components/Spinner/types.ts +8 -0
  78. package/src/components/TextArea/TextArea.stories.tsx +243 -0
  79. package/src/components/TextArea/TextArea.tsx +133 -0
  80. package/src/components/TextArea/index.ts +2 -0
  81. package/src/components/TextArea/types.ts +4 -0
  82. package/src/components/TextField/Container.tsx +68 -0
  83. package/src/components/TextField/ErrorMessage.tsx +37 -0
  84. package/src/components/TextField/Icon.tsx +77 -0
  85. package/src/components/TextField/Label.tsx +56 -0
  86. package/src/components/TextField/NotchBorder.tsx +67 -0
  87. package/src/components/TextField/index.ts +14 -0
  88. package/src/components/TextField/types.ts +15 -0
  89. package/src/components/TextField/useInputKeyboardFocus.tsx +63 -0
  90. package/src/components/TextInput/TextInput.stories.tsx +384 -0
  91. package/src/components/TextInput/TextInput.tsx +255 -0
  92. package/src/components/TextInput/index.ts +2 -0
  93. package/src/components/TextInput/types.ts +4 -0
  94. package/src/components/Toggle/Toggle.stories.tsx +142 -0
  95. package/src/components/Toggle/Toggle.tsx +69 -0
  96. package/src/components/Toggle/index.ts +1 -0
  97. package/src/hooks/index.ts +6 -0
  98. package/src/hooks/useCombinedRefs.ts +37 -0
  99. package/src/hooks/useEventListener.ts +87 -0
  100. package/src/hooks/useFocusTrap/createAriaHider.ts +62 -0
  101. package/src/hooks/useFocusTrap/index.ts +1 -0
  102. package/src/hooks/useFocusTrap/scopeTab.ts +46 -0
  103. package/src/hooks/useFocusTrap/tabbable.ts +107 -0
  104. package/src/hooks/useFocusTrap/useFocusTrap.ts +97 -0
  105. package/src/hooks/useIsomorphicLayoutEffect.ts +14 -0
  106. package/src/hooks/useLockBodyScroll.ts +24 -0
  107. package/src/hooks/useOnClickOutside.ts +53 -0
  108. package/src/index.ts +22 -0
  109. package/src/tailwind.css +4 -0
  110. package/src/types/helpers.ts +11 -0
  111. package/src/types/polymorphic.ts +39 -0
  112. package/src/utils/animation/variants.ts +21 -0
  113. package/src/utils/array/index.ts +1 -0
  114. package/src/utils/array/uniqBy.ts +12 -0
  115. package/src/utils/common/index.ts +1 -0
  116. package/src/utils/common/isFunction.ts +17 -0
  117. package/src/utils/react/extractDisplayName.ts +15 -0
  118. package/src/utils/react/index.ts +1 -0
  119. package/tsconfig.json +16 -0
  120. package/tsconfig.production.json +19 -0
  121. 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,2 @@
1
+ export { default, type TextAreaProps } from "./TextArea"
2
+ export { type TextAreaSize, type TextAreaVariant } from "./types"
@@ -0,0 +1,4 @@
1
+ export type {
2
+ TextFieldSize as TextAreaSize,
3
+ TextFieldVariant as TextAreaVariant,
4
+ } from "../TextField"
@@ -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