@xyhp915/slack-base-ui 0.0.1 → 0.0.2
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/package.json +1 -1
- package/src/App.css +7 -0
- package/src/App.tsx +18 -0
- package/src/assets/react.svg +1 -0
- package/src/components/AlertDialog.tsx +185 -0
- package/src/components/AutoComplete.tsx +311 -0
- package/src/components/Avatar.tsx +70 -0
- package/src/components/Badge.tsx +48 -0
- package/src/components/Button.tsx +53 -0
- package/src/components/Checkbox.tsx +109 -0
- package/src/components/ContextMenu.tsx +393 -0
- package/src/components/Dialog.tsx +129 -0
- package/src/components/Form.tsx +409 -0
- package/src/components/IconButton.tsx +49 -0
- package/src/components/Input.tsx +56 -0
- package/src/components/Loading.tsx +123 -0
- package/src/components/Menu.tsx +368 -0
- package/src/components/Popover.tsx +200 -0
- package/src/components/Progress.tsx +89 -0
- package/src/components/Radio.tsx +137 -0
- package/src/components/Select.tsx +177 -0
- package/src/components/Switch.tsx +116 -0
- package/src/components/Tabs.tsx +128 -0
- package/src/components/Toast.tsx +149 -0
- package/src/components/Tooltip.tsx +46 -0
- package/src/components/index.ts +165 -0
- package/src/context/ThemeContext.tsx +53 -0
- package/src/context/useTheme.ts +11 -0
- package/src/examples/slack-clone/SlackApp.tsx +94 -0
- package/src/examples/slack-clone/components/ChannelHeader.tsx +34 -0
- package/src/examples/slack-clone/components/Composer.tsx +42 -0
- package/src/examples/slack-clone/components/Message.tsx +97 -0
- package/src/examples/slack-clone/components/UserProfile.tsx +78 -0
- package/src/examples/slack-clone/layout/Layout.tsx +27 -0
- package/src/examples/slack-clone/layout/Sidebar.tsx +67 -0
- package/src/examples/slack-clone/layout/SidebarItem.tsx +57 -0
- package/src/examples/slack-clone/layout/TopBar.tsx +30 -0
- package/src/index.css +240 -0
- package/src/main.tsx +16 -0
- package/src/pages/ComponentShowcase.tsx +1618 -0
- package/src/pages/Dashboard.tsx +87 -0
- package/src/pages/QuickStartDemo.tsx +262 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Menu as BaseMenu } from '@base-ui/react'
|
|
3
|
+
import clsx from 'clsx'
|
|
4
|
+
import { Check, ChevronRight } from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
export interface MenuProps {
|
|
7
|
+
children: React.ReactNode
|
|
8
|
+
open?: boolean
|
|
9
|
+
defaultOpen?: boolean
|
|
10
|
+
onOpenChange?: (open: boolean) => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface MenuTriggerProps {
|
|
14
|
+
children?: React.ReactNode
|
|
15
|
+
className?: string
|
|
16
|
+
render?: React.ReactElement
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface MenuContentProps {
|
|
20
|
+
children: React.ReactNode
|
|
21
|
+
className?: string
|
|
22
|
+
side?: 'top' | 'right' | 'bottom' | 'left'
|
|
23
|
+
align?: 'start' | 'center' | 'end'
|
|
24
|
+
sideOffset?: number
|
|
25
|
+
alignOffset?: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface MenuItemProps {
|
|
29
|
+
children: React.ReactNode
|
|
30
|
+
className?: string
|
|
31
|
+
disabled?: boolean
|
|
32
|
+
onSelect?: () => void
|
|
33
|
+
destructive?: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface MenuCheckboxItemProps {
|
|
37
|
+
children: React.ReactNode
|
|
38
|
+
className?: string
|
|
39
|
+
checked?: boolean
|
|
40
|
+
onCheckedChange?: (checked: boolean) => void
|
|
41
|
+
disabled?: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface MenuRadioGroupProps {
|
|
45
|
+
children: React.ReactNode
|
|
46
|
+
value?: string
|
|
47
|
+
onValueChange?: (value: string) => void
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface MenuRadioItemProps {
|
|
51
|
+
children: React.ReactNode
|
|
52
|
+
value: string
|
|
53
|
+
className?: string
|
|
54
|
+
disabled?: boolean
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface MenuLabelProps {
|
|
58
|
+
children: React.ReactNode
|
|
59
|
+
className?: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface MenuSeparatorProps {
|
|
63
|
+
className?: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface MenuSubProps {
|
|
67
|
+
children: React.ReactNode
|
|
68
|
+
open?: boolean
|
|
69
|
+
defaultOpen?: boolean
|
|
70
|
+
onOpenChange?: (open: boolean) => void
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface MenuSubTriggerProps {
|
|
74
|
+
children: React.ReactNode
|
|
75
|
+
className?: string
|
|
76
|
+
disabled?: boolean
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface MenuSubContentProps {
|
|
80
|
+
children: React.ReactNode
|
|
81
|
+
className?: string
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const Menu: React.FC<MenuProps> = ({
|
|
85
|
+
children,
|
|
86
|
+
open,
|
|
87
|
+
defaultOpen,
|
|
88
|
+
onOpenChange,
|
|
89
|
+
}) => {
|
|
90
|
+
return (
|
|
91
|
+
<BaseMenu.Root
|
|
92
|
+
open={open}
|
|
93
|
+
defaultOpen={defaultOpen}
|
|
94
|
+
onOpenChange={onOpenChange}
|
|
95
|
+
>
|
|
96
|
+
{children}
|
|
97
|
+
</BaseMenu.Root>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const MenuTrigger = React.forwardRef<HTMLButtonElement, MenuTriggerProps>(
|
|
102
|
+
({ children, className, render }, ref) => {
|
|
103
|
+
if (render) {
|
|
104
|
+
return (
|
|
105
|
+
<span className="contents">
|
|
106
|
+
<BaseMenu.Trigger
|
|
107
|
+
ref={ref}
|
|
108
|
+
className={clsx('outline-none', className)}
|
|
109
|
+
render={render}
|
|
110
|
+
/>
|
|
111
|
+
</span>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<span className="contents">
|
|
117
|
+
<BaseMenu.Trigger
|
|
118
|
+
ref={ref}
|
|
119
|
+
className={clsx('outline-none', className)}
|
|
120
|
+
>
|
|
121
|
+
{children}
|
|
122
|
+
</BaseMenu.Trigger>
|
|
123
|
+
</span>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
MenuTrigger.displayName = 'MenuTrigger'
|
|
129
|
+
|
|
130
|
+
export const MenuContent = React.forwardRef<HTMLDivElement, MenuContentProps>(
|
|
131
|
+
(
|
|
132
|
+
{
|
|
133
|
+
children,
|
|
134
|
+
className,
|
|
135
|
+
side = 'bottom',
|
|
136
|
+
align = 'start',
|
|
137
|
+
sideOffset = 4,
|
|
138
|
+
alignOffset = 0,
|
|
139
|
+
},
|
|
140
|
+
ref
|
|
141
|
+
) => {
|
|
142
|
+
return (
|
|
143
|
+
<BaseMenu.Portal>
|
|
144
|
+
<BaseMenu.Positioner
|
|
145
|
+
side={side}
|
|
146
|
+
align={align}
|
|
147
|
+
sideOffset={sideOffset}
|
|
148
|
+
alignOffset={alignOffset}
|
|
149
|
+
>
|
|
150
|
+
<BaseMenu.Popup
|
|
151
|
+
ref={ref}
|
|
152
|
+
className={clsx(
|
|
153
|
+
'z-50 min-w-45 max-w-80 rounded-md border border-(--border-light) bg-(--bg-primary) shadow-lg',
|
|
154
|
+
'py-1',
|
|
155
|
+
className
|
|
156
|
+
)}
|
|
157
|
+
>
|
|
158
|
+
{children}
|
|
159
|
+
<BaseMenu.Arrow className="fill-(--bg-primary) stroke-(--border-light)" />
|
|
160
|
+
</BaseMenu.Popup>
|
|
161
|
+
</BaseMenu.Positioner>
|
|
162
|
+
</BaseMenu.Portal>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
MenuContent.displayName = 'MenuContent'
|
|
168
|
+
|
|
169
|
+
export const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(
|
|
170
|
+
({ children, className, disabled, onSelect, destructive }, ref) => {
|
|
171
|
+
return (
|
|
172
|
+
<BaseMenu.Item
|
|
173
|
+
ref={ref}
|
|
174
|
+
className={clsx(
|
|
175
|
+
'relative flex items-center gap-2 px-3 py-1.5 text-[15px] outline-none cursor-pointer select-none',
|
|
176
|
+
'text-(--text-primary) hover:bg-(--bg-hover)',
|
|
177
|
+
'data-highlighted:bg-(--bg-hover)',
|
|
178
|
+
'data-disabled:opacity-50 data-disabled:pointer-events-none',
|
|
179
|
+
destructive && 'text-(--danger) hover:bg-red-50 dark:hover:bg-red-950/20',
|
|
180
|
+
className
|
|
181
|
+
)}
|
|
182
|
+
disabled={disabled}
|
|
183
|
+
onSelect={onSelect}
|
|
184
|
+
>
|
|
185
|
+
{children}
|
|
186
|
+
</BaseMenu.Item>
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
MenuItem.displayName = 'MenuItem'
|
|
192
|
+
|
|
193
|
+
export const MenuCheckboxItem = React.forwardRef<
|
|
194
|
+
HTMLDivElement,
|
|
195
|
+
MenuCheckboxItemProps
|
|
196
|
+
>(({ children, className, checked, onCheckedChange, disabled }, ref) => {
|
|
197
|
+
return (
|
|
198
|
+
<BaseMenu.CheckboxItem
|
|
199
|
+
ref={ref}
|
|
200
|
+
className={clsx(
|
|
201
|
+
'relative flex items-center gap-2 px-3 py-1.5 pl-8 text-[15px] outline-none cursor-pointer select-none',
|
|
202
|
+
'text-(--text-primary) hover:bg-(--bg-hover)',
|
|
203
|
+
'data-highlighted:bg-(--bg-hover)',
|
|
204
|
+
'data-disabled:opacity-50 data-disabled:pointer-events-none',
|
|
205
|
+
className
|
|
206
|
+
)}
|
|
207
|
+
checked={checked}
|
|
208
|
+
onCheckedChange={onCheckedChange}
|
|
209
|
+
disabled={disabled}
|
|
210
|
+
>
|
|
211
|
+
<BaseMenu.CheckboxItemIndicator className="absolute left-2 flex items-center justify-center">
|
|
212
|
+
<Check className="w-4 h-4" />
|
|
213
|
+
</BaseMenu.CheckboxItemIndicator>
|
|
214
|
+
{children}
|
|
215
|
+
</BaseMenu.CheckboxItem>
|
|
216
|
+
)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
MenuCheckboxItem.displayName = 'MenuCheckboxItem'
|
|
220
|
+
|
|
221
|
+
export const MenuRadioGroup: React.FC<MenuRadioGroupProps> = ({
|
|
222
|
+
children,
|
|
223
|
+
value,
|
|
224
|
+
onValueChange,
|
|
225
|
+
}) => {
|
|
226
|
+
return (
|
|
227
|
+
<BaseMenu.RadioGroup value={value} onValueChange={onValueChange}>
|
|
228
|
+
{children}
|
|
229
|
+
</BaseMenu.RadioGroup>
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export const MenuRadioItem = React.forwardRef<HTMLDivElement, MenuRadioItemProps>(
|
|
234
|
+
({ children, value, className, disabled }, ref) => {
|
|
235
|
+
return (
|
|
236
|
+
<BaseMenu.RadioItem
|
|
237
|
+
ref={ref}
|
|
238
|
+
value={value}
|
|
239
|
+
className={clsx(
|
|
240
|
+
'relative flex items-center gap-2 px-3 py-1.5 pl-8 text-[15px] outline-none cursor-pointer select-none',
|
|
241
|
+
'text-(--text-primary) hover:bg-(--bg-hover)',
|
|
242
|
+
'data-highlighted:bg-(--bg-hover)',
|
|
243
|
+
'data-disabled:opacity-50 data-disabled:pointer-events-none',
|
|
244
|
+
className
|
|
245
|
+
)}
|
|
246
|
+
disabled={disabled}
|
|
247
|
+
>
|
|
248
|
+
<BaseMenu.RadioItemIndicator className="absolute left-2 flex items-center justify-center">
|
|
249
|
+
<Check className="w-4 h-4" />
|
|
250
|
+
</BaseMenu.RadioItemIndicator>
|
|
251
|
+
{children}
|
|
252
|
+
</BaseMenu.RadioItem>
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
MenuRadioItem.displayName = 'MenuRadioItem'
|
|
258
|
+
|
|
259
|
+
export const MenuLabel: React.FC<MenuLabelProps> = ({ children, className }) => {
|
|
260
|
+
return (
|
|
261
|
+
<div
|
|
262
|
+
className={clsx(
|
|
263
|
+
'px-3 py-2 text-xs font-semibold text-(--text-muted) uppercase tracking-wider',
|
|
264
|
+
className
|
|
265
|
+
)}
|
|
266
|
+
>
|
|
267
|
+
{children}
|
|
268
|
+
</div>
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export const MenuSeparator: React.FC<MenuSeparatorProps> = ({ className }) => {
|
|
273
|
+
return (
|
|
274
|
+
<BaseMenu.Separator
|
|
275
|
+
className={clsx('my-1 h-px bg-(--border-light)', className)}
|
|
276
|
+
/>
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export const MenuSub: React.FC<MenuSubProps> = ({
|
|
281
|
+
children,
|
|
282
|
+
open,
|
|
283
|
+
defaultOpen,
|
|
284
|
+
onOpenChange,
|
|
285
|
+
}) => {
|
|
286
|
+
return (
|
|
287
|
+
<BaseMenu.Root
|
|
288
|
+
open={open}
|
|
289
|
+
defaultOpen={defaultOpen}
|
|
290
|
+
onOpenChange={onOpenChange}
|
|
291
|
+
>
|
|
292
|
+
{children}
|
|
293
|
+
</BaseMenu.Root>
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export const MenuSubTrigger = React.forwardRef<HTMLButtonElement, MenuSubTriggerProps>(
|
|
298
|
+
({ children, className, disabled }, ref) => {
|
|
299
|
+
return (
|
|
300
|
+
<BaseMenu.Trigger
|
|
301
|
+
ref={ref}
|
|
302
|
+
className={clsx(
|
|
303
|
+
'relative flex items-center justify-between gap-2 px-3 py-2 text-[15px] outline-none cursor-pointer select-none',
|
|
304
|
+
'text-(--text-primary) hover:bg-(--bg-hover)',
|
|
305
|
+
'data-highlighted:bg-(--bg-hover)',
|
|
306
|
+
'data-disabled:opacity-50 data-disabled:pointer-events-none w-full',
|
|
307
|
+
className
|
|
308
|
+
)}
|
|
309
|
+
disabled={disabled}
|
|
310
|
+
>
|
|
311
|
+
{children}
|
|
312
|
+
<ChevronRight className="w-4 h-4 ml-auto" />
|
|
313
|
+
</BaseMenu.Trigger>
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
MenuSubTrigger.displayName = 'MenuSubTrigger'
|
|
319
|
+
|
|
320
|
+
export const MenuSubContent = React.forwardRef<HTMLDivElement, MenuSubContentProps>(
|
|
321
|
+
({ children, className }, ref) => {
|
|
322
|
+
return (
|
|
323
|
+
<BaseMenu.Portal>
|
|
324
|
+
<BaseMenu.Positioner side="right" align="start" sideOffset={8}>
|
|
325
|
+
<BaseMenu.Popup
|
|
326
|
+
ref={ref}
|
|
327
|
+
className={clsx(
|
|
328
|
+
'z-50 min-w-45 max-w-80 rounded-md border border-(--border-light) bg-(--bg-primary) shadow-lg',
|
|
329
|
+
'py-1',
|
|
330
|
+
className
|
|
331
|
+
)}
|
|
332
|
+
>
|
|
333
|
+
{children}
|
|
334
|
+
</BaseMenu.Popup>
|
|
335
|
+
</BaseMenu.Positioner>
|
|
336
|
+
</BaseMenu.Portal>
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
MenuSubContent.displayName = 'MenuSubContent'
|
|
342
|
+
|
|
343
|
+
// Convenience component for menu items with icons
|
|
344
|
+
export const MenuItemWithIcon: React.FC<{
|
|
345
|
+
icon?: React.ReactNode
|
|
346
|
+
children: React.ReactNode
|
|
347
|
+
shortcut?: string
|
|
348
|
+
className?: string
|
|
349
|
+
disabled?: boolean
|
|
350
|
+
onSelect?: () => void
|
|
351
|
+
destructive?: boolean
|
|
352
|
+
}> = ({ icon, children, shortcut, className, disabled, onSelect, destructive }) => {
|
|
353
|
+
return (
|
|
354
|
+
<MenuItem
|
|
355
|
+
className={className}
|
|
356
|
+
disabled={disabled}
|
|
357
|
+
onSelect={onSelect}
|
|
358
|
+
destructive={destructive}
|
|
359
|
+
>
|
|
360
|
+
{icon && <span className="shrink-0">{icon}</span>}
|
|
361
|
+
<span className="flex-1">{children}</span>
|
|
362
|
+
{shortcut && (
|
|
363
|
+
<span className="ml-auto text-xs text-(--text-muted)">{shortcut}</span>
|
|
364
|
+
)}
|
|
365
|
+
</MenuItem>
|
|
366
|
+
)
|
|
367
|
+
}
|
|
368
|
+
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Popover as BasePopover } from '@base-ui/react'
|
|
3
|
+
import clsx from 'clsx'
|
|
4
|
+
|
|
5
|
+
export interface PopoverProps {
|
|
6
|
+
children: React.ReactNode
|
|
7
|
+
open?: boolean
|
|
8
|
+
defaultOpen?: boolean
|
|
9
|
+
onOpenChange?: (open: boolean) => void
|
|
10
|
+
modal?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PopoverTriggerProps {
|
|
14
|
+
children?: React.ReactNode
|
|
15
|
+
className?: string
|
|
16
|
+
render?: React.ReactElement
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface PopoverContentProps {
|
|
20
|
+
children: React.ReactNode
|
|
21
|
+
className?: string
|
|
22
|
+
side?: 'top' | 'right' | 'bottom' | 'left'
|
|
23
|
+
align?: 'start' | 'center' | 'end'
|
|
24
|
+
sideOffset?: number
|
|
25
|
+
alignOffset?: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PopoverCloseProps {
|
|
29
|
+
children?: React.ReactNode
|
|
30
|
+
className?: string
|
|
31
|
+
render?: React.ReactElement
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const Popover: React.FC<PopoverProps> = ({
|
|
35
|
+
children,
|
|
36
|
+
open,
|
|
37
|
+
defaultOpen,
|
|
38
|
+
onOpenChange,
|
|
39
|
+
modal = false,
|
|
40
|
+
}) => {
|
|
41
|
+
return (
|
|
42
|
+
<BasePopover.Root
|
|
43
|
+
open={open}
|
|
44
|
+
defaultOpen={defaultOpen}
|
|
45
|
+
onOpenChange={onOpenChange}
|
|
46
|
+
modal={modal}
|
|
47
|
+
>
|
|
48
|
+
{children}
|
|
49
|
+
</BasePopover.Root>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const PopoverTrigger = React.forwardRef<
|
|
54
|
+
HTMLButtonElement,
|
|
55
|
+
PopoverTriggerProps
|
|
56
|
+
>(({ children, className, render }, ref) => {
|
|
57
|
+
// Wrap with `contents` span to isolate the hidden accessibility <span> nodes
|
|
58
|
+
// that @base-ui/react injects as siblings of the trigger. Without this wrapper,
|
|
59
|
+
// those fixed-position spans become siblings in the parent container and break
|
|
60
|
+
// Tailwind's `space-y-*` selectors (`:not(:last-child)`) causing layout shifts.
|
|
61
|
+
if (render) {
|
|
62
|
+
return (
|
|
63
|
+
<span className="contents">
|
|
64
|
+
<BasePopover.Trigger
|
|
65
|
+
ref={ref}
|
|
66
|
+
className={clsx('outline-none', className)}
|
|
67
|
+
render={render}
|
|
68
|
+
/>
|
|
69
|
+
</span>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<span className="contents">
|
|
75
|
+
<BasePopover.Trigger
|
|
76
|
+
ref={ref}
|
|
77
|
+
className={clsx('outline-none', className)}
|
|
78
|
+
>
|
|
79
|
+
{children}
|
|
80
|
+
</BasePopover.Trigger>
|
|
81
|
+
</span>
|
|
82
|
+
)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
PopoverTrigger.displayName = 'PopoverTrigger'
|
|
86
|
+
|
|
87
|
+
export const PopoverContent: React.FC<PopoverContentProps> = ({
|
|
88
|
+
children,
|
|
89
|
+
className,
|
|
90
|
+
side = 'bottom',
|
|
91
|
+
align = 'center',
|
|
92
|
+
sideOffset = 8,
|
|
93
|
+
alignOffset = 0,
|
|
94
|
+
}) => {
|
|
95
|
+
return (
|
|
96
|
+
<BasePopover.Portal>
|
|
97
|
+
<BasePopover.Positioner
|
|
98
|
+
side={side}
|
|
99
|
+
align={align}
|
|
100
|
+
sideOffset={sideOffset}
|
|
101
|
+
alignOffset={alignOffset}
|
|
102
|
+
>
|
|
103
|
+
<BasePopover.Popup
|
|
104
|
+
className={clsx(
|
|
105
|
+
'z-50 min-w-50 max-w-90 rounded-lg border border-(--border-light) bg-(--bg-primary) shadow-lg',
|
|
106
|
+
className
|
|
107
|
+
)}
|
|
108
|
+
>
|
|
109
|
+
{children}
|
|
110
|
+
<BasePopover.Arrow className="fill-(--bg-primary) stroke-(--border-light)" />
|
|
111
|
+
</BasePopover.Popup>
|
|
112
|
+
</BasePopover.Positioner>
|
|
113
|
+
</BasePopover.Portal>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
PopoverContent.displayName = 'PopoverContent'
|
|
118
|
+
|
|
119
|
+
export const PopoverClose = React.forwardRef<
|
|
120
|
+
HTMLButtonElement,
|
|
121
|
+
PopoverCloseProps
|
|
122
|
+
>(({ children, className, render }, ref) => {
|
|
123
|
+
if (render) {
|
|
124
|
+
return (
|
|
125
|
+
<BasePopover.Close
|
|
126
|
+
ref={ref}
|
|
127
|
+
className={clsx('outline-none', className)}
|
|
128
|
+
render={render}
|
|
129
|
+
/>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<BasePopover.Close
|
|
135
|
+
ref={ref}
|
|
136
|
+
className={clsx('outline-none', className)}
|
|
137
|
+
>
|
|
138
|
+
{children}
|
|
139
|
+
</BasePopover.Close>
|
|
140
|
+
)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
PopoverClose.displayName = 'PopoverClose'
|
|
144
|
+
|
|
145
|
+
// Convenience components for common Popover content patterns
|
|
146
|
+
export const PopoverHeader: React.FC<{ children: React.ReactNode; className?: string }> = ({
|
|
147
|
+
children,
|
|
148
|
+
className,
|
|
149
|
+
}) => {
|
|
150
|
+
return (
|
|
151
|
+
<div
|
|
152
|
+
className={clsx(
|
|
153
|
+
'px-4 py-3 border-b border-(--border-light)',
|
|
154
|
+
'font-bold text-[15px] text-(--text-primary)',
|
|
155
|
+
className
|
|
156
|
+
)}
|
|
157
|
+
>
|
|
158
|
+
{children}
|
|
159
|
+
</div>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export const PopoverBody: React.FC<{ children: React.ReactNode; className?: string }> = ({
|
|
164
|
+
children,
|
|
165
|
+
className,
|
|
166
|
+
}) => {
|
|
167
|
+
return (
|
|
168
|
+
<div
|
|
169
|
+
className={clsx(
|
|
170
|
+
'px-4 py-3',
|
|
171
|
+
'text-[15px] text-(--text-primary)',
|
|
172
|
+
className
|
|
173
|
+
)}
|
|
174
|
+
>
|
|
175
|
+
{children}
|
|
176
|
+
</div>
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export const PopoverFooter: React.FC<{ children: React.ReactNode; className?: string }> = ({
|
|
181
|
+
children,
|
|
182
|
+
className,
|
|
183
|
+
}) => {
|
|
184
|
+
return (
|
|
185
|
+
<div
|
|
186
|
+
className={clsx(
|
|
187
|
+
'px-4 py-3 border-t border-(--border-light)',
|
|
188
|
+
'flex items-center justify-end gap-2',
|
|
189
|
+
className
|
|
190
|
+
)}
|
|
191
|
+
>
|
|
192
|
+
{children}
|
|
193
|
+
</div>
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Progress as BaseProgress } from '@base-ui/react'
|
|
3
|
+
import clsx from 'clsx'
|
|
4
|
+
|
|
5
|
+
export type ProgressVariant = 'default' | 'success' | 'warning' | 'danger'
|
|
6
|
+
export type ProgressSize = 'sm' | 'md' | 'lg'
|
|
7
|
+
|
|
8
|
+
export interface ProgressProps {
|
|
9
|
+
/** Current value (0–max). Omit for indeterminate state. */
|
|
10
|
+
value?: number
|
|
11
|
+
/** Maximum value. Defaults to 100. */
|
|
12
|
+
max?: number
|
|
13
|
+
variant?: ProgressVariant
|
|
14
|
+
size?: ProgressSize
|
|
15
|
+
/** Label shown above the bar */
|
|
16
|
+
label?: string
|
|
17
|
+
/** Show percentage text on the right */
|
|
18
|
+
showValue?: boolean
|
|
19
|
+
className?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const variantColor: Record<ProgressVariant, string> = {
|
|
23
|
+
default: 'bg-(--accent)',
|
|
24
|
+
success: 'bg-(--slack-green)',
|
|
25
|
+
warning: 'bg-amber-400',
|
|
26
|
+
danger: 'bg-(--danger)',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const trackSizes: Record<ProgressSize, string> = {
|
|
30
|
+
sm: 'h-1',
|
|
31
|
+
md: 'h-2',
|
|
32
|
+
lg: 'h-3',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
|
|
36
|
+
(
|
|
37
|
+
{
|
|
38
|
+
value,
|
|
39
|
+
max = 100,
|
|
40
|
+
variant = 'default',
|
|
41
|
+
size = 'md',
|
|
42
|
+
label,
|
|
43
|
+
showValue,
|
|
44
|
+
className,
|
|
45
|
+
},
|
|
46
|
+
ref,
|
|
47
|
+
) => {
|
|
48
|
+
return (
|
|
49
|
+
<div className={clsx('flex flex-col gap-1.5', className)}>
|
|
50
|
+
{(label || showValue) && (
|
|
51
|
+
<div className="flex items-center justify-between gap-2">
|
|
52
|
+
{label && (
|
|
53
|
+
<span className="text-[13px] font-medium text-(--text-secondary)">{label}</span>
|
|
54
|
+
)}
|
|
55
|
+
{showValue && value !== undefined && (
|
|
56
|
+
<span className="text-[12px] text-(--text-muted)">
|
|
57
|
+
{Math.round((value / max) * 100)}%
|
|
58
|
+
</span>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
|
|
63
|
+
<BaseProgress.Root
|
|
64
|
+
ref={ref}
|
|
65
|
+
value={value ?? null}
|
|
66
|
+
max={max}
|
|
67
|
+
className={clsx(
|
|
68
|
+
'w-full overflow-hidden rounded-full bg-(--bg-hover)',
|
|
69
|
+
trackSizes[size],
|
|
70
|
+
)}
|
|
71
|
+
>
|
|
72
|
+
<BaseProgress.Track className="relative h-full w-full">
|
|
73
|
+
<BaseProgress.Indicator
|
|
74
|
+
className={clsx(
|
|
75
|
+
'h-full rounded-full transition-[width] duration-300 ease-out',
|
|
76
|
+
variantColor[variant],
|
|
77
|
+
// Indeterminate animation when value is null
|
|
78
|
+
value === undefined &&
|
|
79
|
+
'w-1/3 animate-[progress-indeterminate_1.5s_ease-in-out_infinite]',
|
|
80
|
+
)}
|
|
81
|
+
/>
|
|
82
|
+
</BaseProgress.Track>
|
|
83
|
+
</BaseProgress.Root>
|
|
84
|
+
</div>
|
|
85
|
+
)
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
Progress.displayName = 'Progress'
|