@agility/plenum-ui 2.0.0-rc47 → 2.0.0-rc49

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 (141) hide show
  1. package/.eslintrc.json +6 -6
  2. package/.prettierrc +13 -13
  3. package/.storybook/Layout.jsx +12 -12
  4. package/.storybook/head.tsx +4 -4
  5. package/.storybook/main.ts +18 -18
  6. package/.storybook/manager-head.html +1 -1
  7. package/.storybook/manager.ts +25 -25
  8. package/.storybook/plenumTheme.ts +8 -8
  9. package/.storybook/preview-head.html +3 -3
  10. package/.storybook/preview.tsx +28 -28
  11. package/.vscode/settings.json +3 -3
  12. package/README.md +271 -271
  13. package/app/globals.css +99 -99
  14. package/app/head.tsx +59 -59
  15. package/app/layout.tsx +28 -28
  16. package/app/page.tsx +7 -7
  17. package/build.js +45 -45
  18. package/dist/index.d.ts +232 -230
  19. package/dist/index.js +1 -1
  20. package/dist/index.js.map +3 -3
  21. package/dist/types/stories/molecules/inputs/select/Select.d.ts +2 -0
  22. package/local.sh +100 -100
  23. package/next.config.js +8 -8
  24. package/package.json +82 -82
  25. package/pages/api/hello.ts +13 -13
  26. package/postcss.config.js +6 -6
  27. package/rollup.config.mjs +41 -41
  28. package/scripts/create-component.js +97 -97
  29. package/stories/Introduction.mdx +314 -314
  30. package/stories/assets/stackalt.svg +1 -1
  31. package/stories/atoms/Avatar/Avatar.stories.tsx +96 -96
  32. package/stories/atoms/Avatar/Avatar.tsx +123 -123
  33. package/stories/atoms/Avatar/index.ts +3 -3
  34. package/stories/atoms/badges/Badge.tsx +127 -127
  35. package/stories/atoms/badges/Pill/Pill.stories.tsx +75 -75
  36. package/stories/atoms/badges/Rounded/Rounded.stories.tsx +75 -75
  37. package/stories/atoms/badges/index.ts +3 -3
  38. package/stories/atoms/buttons/Button/Alternative/Alternative.stories.ts +86 -86
  39. package/stories/atoms/buttons/Button/Button.tsx +232 -232
  40. package/stories/atoms/buttons/Button/Danger/Danger.stories.ts +90 -90
  41. package/stories/atoms/buttons/Button/Primary/Primary.stories.ts +97 -97
  42. package/stories/atoms/buttons/Button/Secondary/Secondary.stories.ts +93 -93
  43. package/stories/atoms/buttons/Button/defaultArgs.ts +9 -9
  44. package/stories/atoms/buttons/Button/index.ts +3 -3
  45. package/stories/atoms/buttons/Capsule/Alternative/Alternative.stories.ts +27 -27
  46. package/stories/atoms/buttons/Capsule/Capsule.tsx +88 -88
  47. package/stories/atoms/buttons/Capsule/Danger/Danger.stories.ts +27 -27
  48. package/stories/atoms/buttons/Capsule/Primary/Primary.stories.ts +27 -27
  49. package/stories/atoms/buttons/Capsule/Secondary/Secondary.stories.ts +27 -27
  50. package/stories/atoms/buttons/Capsule/index.ts +3 -3
  51. package/stories/atoms/buttons/FloatingActionButton/FloatingActionButton.stories.tsx +15 -15
  52. package/stories/atoms/buttons/FloatingActionButton/FloatingActionButton.tsx +22 -22
  53. package/stories/atoms/buttons/FloatingActionButton/index.tsx +3 -3
  54. package/stories/atoms/buttons/index.ts +4 -4
  55. package/stories/atoms/crumb/Crumb.stories.tsx +18 -18
  56. package/stories/atoms/crumb/Crumb.tsx +22 -22
  57. package/stories/atoms/crumb/index.tsx +3 -3
  58. package/stories/atoms/icons/DynamicIcon.stories.ts +43 -43
  59. package/stories/atoms/icons/DynamicIcon.tsx +90 -90
  60. package/stories/atoms/icons/IconWithShadow.stories.ts +43 -43
  61. package/stories/atoms/icons/IconWithShadow.tsx +16 -16
  62. package/stories/atoms/icons/TablerIcon.tsx +22 -22
  63. package/stories/atoms/icons/index.tsx +14 -14
  64. package/stories/atoms/icons/tablerIconNames.ts +4336 -4336
  65. package/stories/atoms/index.ts +46 -46
  66. package/stories/atoms/loaders/Loader.stories.ts +15 -15
  67. package/stories/atoms/loaders/Loader.tsx +21 -21
  68. package/stories/atoms/loaders/NProgress/RadialProgress.stories.tsx +19 -19
  69. package/stories/atoms/loaders/NProgress/RadialProgress.tsx +74 -74
  70. package/stories/atoms/loaders/NProgress/index.ts +3 -3
  71. package/stories/atoms/loaders/index.ts +4 -4
  72. package/stories/index.ts +136 -136
  73. package/stories/molecules/index.ts +51 -51
  74. package/stories/molecules/inputs/InputCounter/InputCounter.stories.tsx +18 -18
  75. package/stories/molecules/inputs/InputCounter/InputCounter.tsx +24 -24
  76. package/stories/molecules/inputs/InputCounter/index.tsx +3 -3
  77. package/stories/molecules/inputs/InputField/InputField.stories.tsx +29 -29
  78. package/stories/molecules/inputs/InputField/InputField.tsx +96 -96
  79. package/stories/molecules/inputs/InputField/index.tsx +3 -3
  80. package/stories/molecules/inputs/InputLabel/InputLabel.stories.tsx +19 -19
  81. package/stories/molecules/inputs/InputLabel/InputLabel.tsx +45 -45
  82. package/stories/molecules/inputs/InputLabel/index.tsx +3 -3
  83. package/stories/molecules/inputs/NestedInputButton/NestedInputButton.stories.tsx +52 -52
  84. package/stories/molecules/inputs/NestedInputButton/NestedInputButton.tsx +64 -64
  85. package/stories/molecules/inputs/NestedInputButton/index.tsx +3 -3
  86. package/stories/molecules/inputs/TextInput/TextInput.stories.tsx +32 -32
  87. package/stories/molecules/inputs/TextInput/TextInput.tsx +165 -165
  88. package/stories/molecules/inputs/TextInput/index.tsx +5 -5
  89. package/stories/molecules/inputs/checkbox/Checkbox.stories.ts +23 -23
  90. package/stories/molecules/inputs/checkbox/Checkbox.tsx +98 -98
  91. package/stories/molecules/inputs/checkbox/index.ts +3 -3
  92. package/stories/molecules/inputs/combobox/ComboBox.stories.ts +41 -41
  93. package/stories/molecules/inputs/combobox/ComboBox.tsx +185 -185
  94. package/stories/molecules/inputs/combobox/index.ts +3 -3
  95. package/stories/molecules/inputs/index.ts +38 -38
  96. package/stories/molecules/inputs/radio/Radio.stories.ts +27 -27
  97. package/stories/molecules/inputs/radio/Radio.tsx +92 -92
  98. package/stories/molecules/inputs/radio/index.ts +3 -3
  99. package/stories/molecules/inputs/select/Select.stories.ts +23 -23
  100. package/stories/molecules/inputs/select/Select.tsx +108 -100
  101. package/stories/molecules/inputs/select/index.ts +3 -3
  102. package/stories/molecules/inputs/textArea/TextArea.stories.ts +22 -22
  103. package/stories/molecules/inputs/textArea/TextArea.tsx +158 -158
  104. package/stories/molecules/inputs/textArea/index.ts +3 -3
  105. package/stories/molecules/inputs/toggleSwitch/ToggleSwitch.stories.tsx +118 -118
  106. package/stories/molecules/inputs/toggleSwitch/ToggleSwitch.tsx +81 -81
  107. package/stories/molecules/inputs/toggleSwitch/index.ts +3 -3
  108. package/stories/molecules/tabs/Tabs.stories.tsx +18 -18
  109. package/stories/molecules/tabs/Tabs.tsx +22 -22
  110. package/stories/molecules/tabs/index.tsx +2 -2
  111. package/stories/organisms/AnimatedLabelInput/AnimatedLabelInput.stories.tsx +30 -30
  112. package/stories/organisms/AnimatedLabelInput/AnimatedLabelInput.tsx +66 -66
  113. package/stories/organisms/AnimatedLabelInput/index.tsx +3 -3
  114. package/stories/organisms/AnimatedLabelTextArea/AnimatedLabelTextArea.stories.tsx +26 -26
  115. package/stories/organisms/AnimatedLabelTextArea/AnimatedLabelTextArea.tsx +61 -61
  116. package/stories/organisms/AnimatedLabelTextArea/index.tsx +3 -3
  117. package/stories/organisms/ButtonDropdown/ButtonDropdown.stories.tsx +125 -125
  118. package/stories/organisms/ButtonDropdown/ButtonDropdown.tsx +86 -86
  119. package/stories/organisms/ButtonDropdown/index.tsx +3 -3
  120. package/stories/organisms/DropdownComponent/Dropdown.stories.tsx +73 -73
  121. package/stories/organisms/DropdownComponent/DropdownComponent.tsx +346 -346
  122. package/stories/organisms/DropdownComponent/dropdownItems.ts +122 -122
  123. package/stories/organisms/DropdownComponent/index.ts +4 -4
  124. package/stories/organisms/EmptySectionPlaceholder/EmptySectionPlaceholder.stories.tsx +76 -76
  125. package/stories/organisms/EmptySectionPlaceholder/EmptySectionPlaceholder.tsx +52 -52
  126. package/stories/organisms/EmptySectionPlaceholder/index.tsx +4 -4
  127. package/stories/organisms/FormInputWithAddons/FormInputWithAddons.stories.tsx +29 -29
  128. package/stories/organisms/FormInputWithAddons/FormInputWithAddons.tsx +145 -145
  129. package/stories/organisms/FormInputWithAddons/index.tsx +3 -3
  130. package/stories/organisms/TextInputSelect/InputSelect.tsx +59 -59
  131. package/stories/organisms/TextInputSelect/TextInputSelect.stories.tsx +33 -33
  132. package/stories/organisms/TextInputSelect/TextInputSelect.tsx +186 -186
  133. package/stories/organisms/TextInputSelect/index.tsx +3 -3
  134. package/stories/organisms/index.ts +27 -27
  135. package/tailwind.config.js +192 -192
  136. package/tsconfig.json +29 -29
  137. package/tsconfig.lib.json +25 -25
  138. package/utils/types.d.ts +2 -2
  139. package/utils/types.ts +3 -3
  140. package/utils/useId.d.ts +1 -1
  141. package/utils/useId.tsx +16 -16
@@ -1,346 +1,346 @@
1
- import React, { HTMLAttributes, useEffect, useMemo, useRef, useState } from "react"
2
- import { default as cn } from "classnames"
3
- import {
4
- useFloating,
5
- autoUpdate,
6
- offset,
7
- useDismiss,
8
- useRole,
9
- useClick,
10
- useInteractions,
11
- FloatingFocusManager,
12
- autoPlacement,
13
- shift,
14
- FloatingPortal,
15
- FloatingList,
16
- useTransitionStyles,
17
- Placement,
18
- useListNavigation
19
- } from "@floating-ui/react"
20
-
21
- import { ClassNameWithAutocomplete } from "utils/types"
22
- import { DynamicIcon, IDynamicIconProps, UnifiedIconName } from "@/stories/atoms/icons"
23
- import { list } from "postcss"
24
-
25
- export interface IItemProp {
26
- //Don't think this needs to extend HtmlButton... extends HTMLAttributes<HTMLButtonElement> {
27
- icon?: IDynamicIconProps
28
- iconPosition?: "trailing" | "leading"
29
- label: string | JSX.Element
30
- onClick?(): void
31
- isEmphasized?: boolean
32
- key: React.Key
33
- iconObj?: JSX.Element
34
- }
35
-
36
- export interface IDropdownProps extends HTMLAttributes<HTMLDivElement> {
37
- items: IItemProp[][]
38
- label: string
39
- CustomDropdownTrigger?: React.ReactNode
40
- id: string
41
- groupClassname?: ClassNameWithAutocomplete
42
- itemsClassname?: ClassNameWithAutocomplete
43
- itemClassname?: ClassNameWithAutocomplete
44
- activeItemClassname?: ClassNameWithAutocomplete
45
- buttonClassname?: ClassNameWithAutocomplete
46
- iconClassname?: ClassNameWithAutocomplete
47
- iconSpacingClassname?: ClassNameWithAutocomplete
48
- placement?: Placement
49
- offsetOptions?: Partial<{
50
- mainAxis: number
51
- crossAxis: number
52
- alignmentAxis: number | null
53
- }>
54
- disabled?: boolean
55
- onFocus?: () => void
56
- onBlur?: () => void
57
- }
58
- export const defaultClassNames = {
59
- groupClassname: "flex inline-block text-left",
60
- itemsClassname:
61
- "mt-2 origin-bottom-right rounded bg-white shadow-lg z-[99999] divide-y divide-gray-100 border border-gray-300 ",
62
- itemClassname:
63
- "group flex font-muli cursor-pointer items-center px-4 py-2 text-sm transition-all hover:bg-gray-100 hover:text-gray-900 justify-between gap-4 ",
64
- activeItemClassname: "block px-4 py-2 text-sm text-gray-700 bg-gray-100 hover:bg-gray-200 hover:text-gray-900",
65
- buttonClassname:
66
- "py-[2px] flex items-center rounded outline-purple-500 transition-all text-gray-400 hover:text-gray-600 ",
67
- iconClassname: "ml-1 h-5 w-6",
68
- iconSpacingClassname: "flex items-center gap-x-4"
69
- }
70
-
71
- /** Comment */
72
- const Dropdown: React.FC<IDropdownProps> = ({
73
- items,
74
- id,
75
- label,
76
- groupClassname,
77
- itemsClassname,
78
- itemClassname,
79
- activeItemClassname,
80
- buttonClassname,
81
- iconClassname,
82
- iconSpacingClassname,
83
- CustomDropdownTrigger,
84
- placement = "bottom-start",
85
- offsetOptions,
86
- disabled,
87
- onFocus,
88
- onBlur,
89
- ...props
90
- }: IDropdownProps): JSX.Element | null => {
91
- const [isOpen, setIsOpen] = useState(false)
92
- const [activeItem, setActiveItem] = useState<React.Key | null>(null)
93
- const [activeIndex, setActiveIndex] = useState<number | null>(null)
94
-
95
- const listRef = useRef<(HTMLButtonElement | null)[]>([])
96
-
97
- // Floating UI logic
98
- const { refs, floatingStyles, context } = useFloating({
99
- open: isOpen,
100
- onOpenChange: (bool: boolean) => {
101
- listRef.current = []
102
- setActiveIndex(null)
103
- setIsOpen(bool)
104
- },
105
- placement,
106
- middleware: [
107
- offset(offsetOptions ?? 10),
108
- autoPlacement({
109
- allowedPlacements: [placement, "bottom-start", "bottom-end", "bottom"]
110
- }),
111
- shift({ rootBoundary: "document" })
112
- ],
113
- whileElementsMounted: autoUpdate
114
- })
115
- const click = useClick(context)
116
- const dismiss = useDismiss(context)
117
- const role = useRole(context)
118
- const listNavigation = useListNavigation(context, {
119
- listRef,
120
- activeIndex,
121
- onNavigate: (index: number | null) => {
122
- if (index !== null && listRef.current[index]) {
123
- setActiveIndex(index)
124
- listRef.current[index]?.focus()
125
- }
126
- }
127
- })
128
-
129
- const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([
130
- click,
131
- dismiss,
132
- role,
133
- listNavigation
134
- ])
135
-
136
- useEffect(() => {
137
- if (isOpen) {
138
- onFocus && onFocus()
139
- } else {
140
- onBlur && onBlur()
141
- }
142
- }, [isOpen, onBlur, onFocus])
143
-
144
- const ItemComponents = useMemo(
145
- () =>
146
- items.map((itemStack, stackIndex) => {
147
- return itemStack.map((item, itemIndex) => {
148
- const { key, label, icon, iconObj, iconPosition, isEmphasized, onClick, ...rest } = item
149
- const active = activeItem && activeItem === key
150
- const itemClass = cn(
151
- defaultClassNames.itemClassname,
152
- itemClassname,
153
- "group flex cursor-pointer items-center px-4 py-2 text-sm transition-all",
154
- {
155
- "text-red-500": isEmphasized
156
- },
157
- {
158
- "text-gray-900": !isEmphasized
159
- },
160
- {
161
- "bg-gray-100 text-gray-900": active
162
- },
163
- active ? cn(defaultClassNames.activeItemClassname, activeItemClassname) : "",
164
- {
165
- "bg-gray-100 text-red-500 hover:text-red-500": active && isEmphasized
166
- }
167
- )
168
- return (
169
- <button
170
- {...{
171
- key: key,
172
- id: key.toString(),
173
- className: cn(itemClass, "w-full"),
174
- ...rest,
175
- ...getItemProps(),
176
- onClick: () => {
177
- onClick && onClick()
178
- setTimeout(() => {
179
- //hide the dropdown after click
180
- setIsOpen(false)
181
- }, 150)
182
- }
183
- }}
184
- ref={(node) => {
185
- //If the list ref already contains a node with the same id do nothing, otherwise add it
186
- if (listRef.current.some((item) => item?.id === key)) {
187
- return
188
- }
189
- listRef.current.push(node)
190
- }}
191
- key={key}
192
- >
193
- <div className={cn(defaultClassNames.iconSpacingClassname, iconSpacingClassname)}>
194
- {iconObj && (iconPosition === "leading" || iconPosition === undefined) && (
195
- <>{iconObj}</>
196
- )}
197
- {icon &&
198
- (iconPosition === "leading" || iconPosition === undefined) &&
199
- (typeof icon === "string" ? (
200
- <DynamicIcon
201
- {...{
202
- icon: icon,
203
- className: cn(
204
- {
205
- "text-red-500": isEmphasized
206
- },
207
- "opacity-60 group"
208
- )
209
- }}
210
- />
211
- ) : (
212
- <DynamicIcon
213
- {...{
214
- ...icon,
215
- className: cn(
216
- icon.className,
217
- {
218
- "text-red-500": isEmphasized
219
- },
220
- "opacity-60 group"
221
- )
222
- }}
223
- />
224
- ))}
225
- <div className="whitespace-nowrap">{label}</div>
226
- {iconObj && iconPosition === "trailing" && <>{iconObj}</>}
227
- {icon &&
228
- iconPosition === "trailing" &&
229
- (typeof icon === "string" ? (
230
- <DynamicIcon
231
- {...{
232
- icon: icon,
233
- className: cn(
234
- {
235
- "text-red-500": isEmphasized
236
- },
237
- "opacity-60 group"
238
- )
239
- }}
240
- />
241
- ) : (
242
- <DynamicIcon
243
- {...{
244
- ...icon,
245
- className: cn(
246
- icon.className,
247
- {
248
- "text-red-500": isEmphasized
249
- },
250
- "opacity-60 group"
251
- )
252
- }}
253
- />
254
- ))}
255
- </div>
256
- </button>
257
- )
258
- })
259
- }),
260
- [activeItem, activeItemClassname, getItemProps, iconSpacingClassname, itemClassname, items]
261
- )
262
-
263
- const { isMounted, styles: transitionStyles } = useTransitionStyles(context, {
264
- duration: {
265
- open: 200,
266
- close: 200
267
- },
268
- initial: {
269
- opacity: 0,
270
- scale: 95
271
- },
272
- open: {
273
- opacity: 1,
274
- scale: 100
275
- }
276
- })
277
- return (
278
- <div
279
- {...{
280
- className: cn(defaultClassNames.groupClassname, groupClassname),
281
- role: "combobox",
282
- "aria-owns": `${id}-list`,
283
- "aria-expanded": isOpen,
284
- "aria-haspopup": "listbox",
285
- ...props
286
- }}
287
- >
288
- <button
289
- {...{
290
- ref: refs.setReference,
291
- className: cn(defaultClassNames.buttonClassname, buttonClassname),
292
- onClick: () => {
293
- setIsOpen(!isOpen)
294
- },
295
- type: "button",
296
- disabled: disabled,
297
- ...getReferenceProps()
298
- }}
299
- >
300
- {CustomDropdownTrigger ? (
301
- <span className="">{CustomDropdownTrigger}</span>
302
- ) : (
303
- <>
304
- <span className="pl-1">{label}</span>
305
- <DynamicIcon
306
- icon="IconChevronDown"
307
- className={cn(defaultClassNames.iconClassname, iconClassname)}
308
- />
309
- </>
310
- )}
311
- </button>
312
-
313
- {isMounted && items.length > 0 && isOpen && (
314
- <FloatingList
315
- {...{
316
- elementsRef: listRef
317
- }}
318
- >
319
- <FloatingPortal>
320
- <FloatingFocusManager context={context} modal={true}>
321
- <div
322
- {...getFloatingProps()}
323
- className={cn(defaultClassNames.itemsClassname, itemsClassname)}
324
- ref={refs.setFloating}
325
- aria-labelledby={label}
326
- style={{
327
- position: context.strategy,
328
- top: Math.round(context.y ?? 0),
329
- left: Math.round(context.x ?? 0),
330
- width: "max-content",
331
- maxWidth: "min(calc(100vw - 10px), 25rem)",
332
- ...floatingStyles,
333
- ...transitionStyles
334
- }}
335
- >
336
- {ItemComponents}
337
- </div>
338
- </FloatingFocusManager>
339
- </FloatingPortal>
340
- </FloatingList>
341
- )}
342
- </div>
343
- )
344
- }
345
-
346
- export default Dropdown
1
+ import React, { HTMLAttributes, useEffect, useMemo, useRef, useState } from "react"
2
+ import { default as cn } from "classnames"
3
+ import {
4
+ useFloating,
5
+ autoUpdate,
6
+ offset,
7
+ useDismiss,
8
+ useRole,
9
+ useClick,
10
+ useInteractions,
11
+ FloatingFocusManager,
12
+ autoPlacement,
13
+ shift,
14
+ FloatingPortal,
15
+ FloatingList,
16
+ useTransitionStyles,
17
+ Placement,
18
+ useListNavigation
19
+ } from "@floating-ui/react"
20
+
21
+ import { ClassNameWithAutocomplete } from "utils/types"
22
+ import { DynamicIcon, IDynamicIconProps, UnifiedIconName } from "@/stories/atoms/icons"
23
+ import { list } from "postcss"
24
+
25
+ export interface IItemProp {
26
+ //Don't think this needs to extend HtmlButton... extends HTMLAttributes<HTMLButtonElement> {
27
+ icon?: IDynamicIconProps
28
+ iconPosition?: "trailing" | "leading"
29
+ label: string | JSX.Element
30
+ onClick?(): void
31
+ isEmphasized?: boolean
32
+ key: React.Key
33
+ iconObj?: JSX.Element
34
+ }
35
+
36
+ export interface IDropdownProps extends HTMLAttributes<HTMLDivElement> {
37
+ items: IItemProp[][]
38
+ label: string
39
+ CustomDropdownTrigger?: React.ReactNode
40
+ id: string
41
+ groupClassname?: ClassNameWithAutocomplete
42
+ itemsClassname?: ClassNameWithAutocomplete
43
+ itemClassname?: ClassNameWithAutocomplete
44
+ activeItemClassname?: ClassNameWithAutocomplete
45
+ buttonClassname?: ClassNameWithAutocomplete
46
+ iconClassname?: ClassNameWithAutocomplete
47
+ iconSpacingClassname?: ClassNameWithAutocomplete
48
+ placement?: Placement
49
+ offsetOptions?: Partial<{
50
+ mainAxis: number
51
+ crossAxis: number
52
+ alignmentAxis: number | null
53
+ }>
54
+ disabled?: boolean
55
+ onFocus?: () => void
56
+ onBlur?: () => void
57
+ }
58
+ export const defaultClassNames = {
59
+ groupClassname: "flex inline-block text-left",
60
+ itemsClassname:
61
+ "mt-2 origin-bottom-right rounded bg-white shadow-lg z-[99999] divide-y divide-gray-100 border border-gray-300 ",
62
+ itemClassname:
63
+ "group flex font-muli cursor-pointer items-center px-4 py-2 text-sm transition-all hover:bg-gray-100 hover:text-gray-900 justify-between gap-4 ",
64
+ activeItemClassname: "block px-4 py-2 text-sm text-gray-700 bg-gray-100 hover:bg-gray-200 hover:text-gray-900",
65
+ buttonClassname:
66
+ "py-[2px] flex items-center rounded outline-purple-500 transition-all text-gray-400 hover:text-gray-600 ",
67
+ iconClassname: "ml-1 h-5 w-6",
68
+ iconSpacingClassname: "flex items-center gap-x-4"
69
+ }
70
+
71
+ /** Comment */
72
+ const Dropdown: React.FC<IDropdownProps> = ({
73
+ items,
74
+ id,
75
+ label,
76
+ groupClassname,
77
+ itemsClassname,
78
+ itemClassname,
79
+ activeItemClassname,
80
+ buttonClassname,
81
+ iconClassname,
82
+ iconSpacingClassname,
83
+ CustomDropdownTrigger,
84
+ placement = "bottom-start",
85
+ offsetOptions,
86
+ disabled,
87
+ onFocus,
88
+ onBlur,
89
+ ...props
90
+ }: IDropdownProps): JSX.Element | null => {
91
+ const [isOpen, setIsOpen] = useState(false)
92
+ const [activeItem, setActiveItem] = useState<React.Key | null>(null)
93
+ const [activeIndex, setActiveIndex] = useState<number | null>(null)
94
+
95
+ const listRef = useRef<(HTMLButtonElement | null)[]>([])
96
+
97
+ // Floating UI logic
98
+ const { refs, floatingStyles, context } = useFloating({
99
+ open: isOpen,
100
+ onOpenChange: (bool: boolean) => {
101
+ listRef.current = []
102
+ setActiveIndex(null)
103
+ setIsOpen(bool)
104
+ },
105
+ placement,
106
+ middleware: [
107
+ offset(offsetOptions ?? 10),
108
+ autoPlacement({
109
+ allowedPlacements: [placement, "bottom-start", "bottom-end", "bottom"]
110
+ }),
111
+ shift({ rootBoundary: "document" })
112
+ ],
113
+ whileElementsMounted: autoUpdate
114
+ })
115
+ const click = useClick(context)
116
+ const dismiss = useDismiss(context)
117
+ const role = useRole(context)
118
+ const listNavigation = useListNavigation(context, {
119
+ listRef,
120
+ activeIndex,
121
+ onNavigate: (index: number | null) => {
122
+ if (index !== null && listRef.current[index]) {
123
+ setActiveIndex(index)
124
+ listRef.current[index]?.focus()
125
+ }
126
+ }
127
+ })
128
+
129
+ const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([
130
+ click,
131
+ dismiss,
132
+ role,
133
+ listNavigation
134
+ ])
135
+
136
+ useEffect(() => {
137
+ if (isOpen) {
138
+ onFocus && onFocus()
139
+ } else {
140
+ onBlur && onBlur()
141
+ }
142
+ }, [isOpen, onBlur, onFocus])
143
+
144
+ const ItemComponents = useMemo(
145
+ () =>
146
+ items.map((itemStack, stackIndex) => {
147
+ return itemStack.map((item, itemIndex) => {
148
+ const { key, label, icon, iconObj, iconPosition, isEmphasized, onClick, ...rest } = item
149
+ const active = activeItem && activeItem === key
150
+ const itemClass = cn(
151
+ defaultClassNames.itemClassname,
152
+ itemClassname,
153
+ "group flex cursor-pointer items-center px-4 py-2 text-sm transition-all",
154
+ {
155
+ "text-red-500": isEmphasized
156
+ },
157
+ {
158
+ "text-gray-900": !isEmphasized
159
+ },
160
+ {
161
+ "bg-gray-100 text-gray-900": active
162
+ },
163
+ active ? cn(defaultClassNames.activeItemClassname, activeItemClassname) : "",
164
+ {
165
+ "bg-gray-100 text-red-500 hover:text-red-500": active && isEmphasized
166
+ }
167
+ )
168
+ return (
169
+ <button
170
+ {...{
171
+ key: key,
172
+ id: key.toString(),
173
+ className: cn(itemClass, "w-full"),
174
+ ...rest,
175
+ ...getItemProps(),
176
+ onClick: () => {
177
+ onClick && onClick()
178
+ setTimeout(() => {
179
+ //hide the dropdown after click
180
+ setIsOpen(false)
181
+ }, 150)
182
+ }
183
+ }}
184
+ ref={(node) => {
185
+ //If the list ref already contains a node with the same id do nothing, otherwise add it
186
+ if (listRef.current.some((item) => item?.id === key)) {
187
+ return
188
+ }
189
+ listRef.current.push(node)
190
+ }}
191
+ key={key}
192
+ >
193
+ <div className={cn(defaultClassNames.iconSpacingClassname, iconSpacingClassname)}>
194
+ {iconObj && (iconPosition === "leading" || iconPosition === undefined) && (
195
+ <>{iconObj}</>
196
+ )}
197
+ {icon &&
198
+ (iconPosition === "leading" || iconPosition === undefined) &&
199
+ (typeof icon === "string" ? (
200
+ <DynamicIcon
201
+ {...{
202
+ icon: icon,
203
+ className: cn(
204
+ {
205
+ "text-red-500": isEmphasized
206
+ },
207
+ "opacity-60 group"
208
+ )
209
+ }}
210
+ />
211
+ ) : (
212
+ <DynamicIcon
213
+ {...{
214
+ ...icon,
215
+ className: cn(
216
+ icon.className,
217
+ {
218
+ "text-red-500": isEmphasized
219
+ },
220
+ "opacity-60 group"
221
+ )
222
+ }}
223
+ />
224
+ ))}
225
+ <div className="whitespace-nowrap">{label}</div>
226
+ {iconObj && iconPosition === "trailing" && <>{iconObj}</>}
227
+ {icon &&
228
+ iconPosition === "trailing" &&
229
+ (typeof icon === "string" ? (
230
+ <DynamicIcon
231
+ {...{
232
+ icon: icon,
233
+ className: cn(
234
+ {
235
+ "text-red-500": isEmphasized
236
+ },
237
+ "opacity-60 group"
238
+ )
239
+ }}
240
+ />
241
+ ) : (
242
+ <DynamicIcon
243
+ {...{
244
+ ...icon,
245
+ className: cn(
246
+ icon.className,
247
+ {
248
+ "text-red-500": isEmphasized
249
+ },
250
+ "opacity-60 group"
251
+ )
252
+ }}
253
+ />
254
+ ))}
255
+ </div>
256
+ </button>
257
+ )
258
+ })
259
+ }),
260
+ [activeItem, activeItemClassname, getItemProps, iconSpacingClassname, itemClassname, items]
261
+ )
262
+
263
+ const { isMounted, styles: transitionStyles } = useTransitionStyles(context, {
264
+ duration: {
265
+ open: 200,
266
+ close: 200
267
+ },
268
+ initial: {
269
+ opacity: 0,
270
+ scale: 95
271
+ },
272
+ open: {
273
+ opacity: 1,
274
+ scale: 100
275
+ }
276
+ })
277
+ return (
278
+ <div
279
+ {...{
280
+ className: cn(defaultClassNames.groupClassname, groupClassname),
281
+ role: "combobox",
282
+ "aria-owns": `${id}-list`,
283
+ "aria-expanded": isOpen,
284
+ "aria-haspopup": "listbox",
285
+ ...props
286
+ }}
287
+ >
288
+ <button
289
+ {...{
290
+ ref: refs.setReference,
291
+ className: cn(defaultClassNames.buttonClassname, buttonClassname),
292
+ onClick: () => {
293
+ setIsOpen(!isOpen)
294
+ },
295
+ type: "button",
296
+ disabled: disabled,
297
+ ...getReferenceProps()
298
+ }}
299
+ >
300
+ {CustomDropdownTrigger ? (
301
+ <span className="">{CustomDropdownTrigger}</span>
302
+ ) : (
303
+ <>
304
+ <span className="pl-1">{label}</span>
305
+ <DynamicIcon
306
+ icon="IconChevronDown"
307
+ className={cn(defaultClassNames.iconClassname, iconClassname)}
308
+ />
309
+ </>
310
+ )}
311
+ </button>
312
+
313
+ {isMounted && items.length > 0 && isOpen && (
314
+ <FloatingList
315
+ {...{
316
+ elementsRef: listRef
317
+ }}
318
+ >
319
+ <FloatingPortal>
320
+ <FloatingFocusManager context={context} modal={true}>
321
+ <div
322
+ {...getFloatingProps()}
323
+ className={cn(defaultClassNames.itemsClassname, itemsClassname)}
324
+ ref={refs.setFloating}
325
+ aria-labelledby={label}
326
+ style={{
327
+ position: context.strategy,
328
+ top: Math.round(context.y ?? 0),
329
+ left: Math.round(context.x ?? 0),
330
+ width: "max-content",
331
+ maxWidth: "min(calc(100vw - 10px), 25rem)",
332
+ ...floatingStyles,
333
+ ...transitionStyles
334
+ }}
335
+ >
336
+ {ItemComponents}
337
+ </div>
338
+ </FloatingFocusManager>
339
+ </FloatingPortal>
340
+ </FloatingList>
341
+ )}
342
+ </div>
343
+ )
344
+ }
345
+
346
+ export default Dropdown