@xyhp915/slack-base-ui 0.0.1 → 0.0.3
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/README.md +220 -4
- package/agents/slack-base-ui/SKILL.md +137 -0
- package/agents/slack-base-ui/checklists/style-review.md +56 -0
- package/agents/slack-base-ui/templates/consumer-setup.md +109 -0
- package/agents/slack-base-ui/templates/slack-theme.css +152 -0
- package/libs/Dialog.d.ts +73 -0
- package/libs/Dialog.d.ts.map +1 -1
- package/libs/Popover.d.ts +69 -0
- package/libs/Popover.d.ts.map +1 -1
- package/libs/index.d.ts +4 -4
- package/libs/index.d.ts.map +1 -1
- package/libs/index.js +2885 -2718
- 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 +371 -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 +367 -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 +186 -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 +22 -0
- package/src/pages/ComponentShowcase.tsx +1964 -0
- package/src/pages/Dashboard.tsx +87 -0
- package/src/pages/QuickStartDemo.tsx +262 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import clsx from 'clsx'
|
|
2
|
+
import { Button as BaseButton } from '@base-ui/react'
|
|
3
|
+
import type { ButtonProps as BaseButtonProps } from '@base-ui/react'
|
|
4
|
+
import React from 'react'
|
|
5
|
+
|
|
6
|
+
// Slack Button Variants
|
|
7
|
+
type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost';
|
|
8
|
+
type ButtonSize = 'sm' | 'md' | 'lg';
|
|
9
|
+
|
|
10
|
+
export interface ButtonProps extends BaseButtonProps {
|
|
11
|
+
variant?: ButtonVariant;
|
|
12
|
+
size?: ButtonSize;
|
|
13
|
+
fullWidth?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
17
|
+
({ className, variant = 'secondary', size = 'md', fullWidth, children, ...props }, ref) => {
|
|
18
|
+
|
|
19
|
+
// Base styles tailored to match Slack
|
|
20
|
+
const baseStyles = 'inline-flex items-center justify-center font-bold transition-all outline-none focus-visible:ring-2 focus-visible:ring-(--focus-ring) focus-visible:ring-offset-2 focus-visible:ring-offset-(--bg-primary) disabled:opacity-50 disabled:cursor-not-allowed select-none'
|
|
21
|
+
|
|
22
|
+
const variants = {
|
|
23
|
+
primary: 'bg-(--accent-action) text-(--accent-contrast) hover:bg-(--accent-action-hover) border border-transparent shadow-sm active:scale-[0.98]',
|
|
24
|
+
secondary: 'bg-(--bg-secondary) text-(--text-primary) border border-(--border-active) hover:bg-(--bg-hover) hover:shadow-sm active:bg-(--bg-secondary)',
|
|
25
|
+
danger: 'bg-(--danger) text-(--accent-contrast) hover:bg-(--danger-hover) border border-transparent shadow-sm',
|
|
26
|
+
ghost: 'bg-transparent text-(--text-secondary) hover:bg-(--bg-hover) hover:text-(--text-primary)',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const sizes = {
|
|
30
|
+
sm: 'h-7 px-3 text-[13px] rounded',
|
|
31
|
+
md: 'h-9 px-4 text-[15px] rounded-md',
|
|
32
|
+
lg: 'h-11 px-6 text-[18px] rounded-lg',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<BaseButton
|
|
37
|
+
ref={ref}
|
|
38
|
+
className={clsx(
|
|
39
|
+
baseStyles,
|
|
40
|
+
variants[variant],
|
|
41
|
+
sizes[size],
|
|
42
|
+
fullWidth && 'w-full',
|
|
43
|
+
className,
|
|
44
|
+
)}
|
|
45
|
+
{...props}
|
|
46
|
+
>
|
|
47
|
+
{children}
|
|
48
|
+
</BaseButton>
|
|
49
|
+
)
|
|
50
|
+
},
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
Button.displayName = 'Button'
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Checkbox as BaseCheckbox } from '@base-ui/react'
|
|
3
|
+
import clsx from 'clsx'
|
|
4
|
+
import { Check, Minus } from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
export interface CheckboxProps {
|
|
7
|
+
checked?: boolean
|
|
8
|
+
defaultChecked?: boolean
|
|
9
|
+
onCheckedChange?: (checked: boolean) => void
|
|
10
|
+
indeterminate?: boolean
|
|
11
|
+
disabled?: boolean
|
|
12
|
+
required?: boolean
|
|
13
|
+
/** Label text shown next to the checkbox */
|
|
14
|
+
label?: string
|
|
15
|
+
/** Helper text shown below the label */
|
|
16
|
+
description?: string
|
|
17
|
+
/** Error message */
|
|
18
|
+
error?: string
|
|
19
|
+
name?: string
|
|
20
|
+
value?: string
|
|
21
|
+
id?: string
|
|
22
|
+
className?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const Checkbox = React.forwardRef<HTMLButtonElement, CheckboxProps>(
|
|
26
|
+
(
|
|
27
|
+
{
|
|
28
|
+
checked,
|
|
29
|
+
defaultChecked,
|
|
30
|
+
onCheckedChange,
|
|
31
|
+
indeterminate,
|
|
32
|
+
disabled,
|
|
33
|
+
required,
|
|
34
|
+
label,
|
|
35
|
+
description,
|
|
36
|
+
error,
|
|
37
|
+
name,
|
|
38
|
+
value,
|
|
39
|
+
id,
|
|
40
|
+
className,
|
|
41
|
+
},
|
|
42
|
+
ref,
|
|
43
|
+
) => {
|
|
44
|
+
const generatedId = React.useId()
|
|
45
|
+
const checkboxId = id ?? generatedId
|
|
46
|
+
|
|
47
|
+
const rootEl = (
|
|
48
|
+
<BaseCheckbox.Root
|
|
49
|
+
ref={ref}
|
|
50
|
+
id={checkboxId}
|
|
51
|
+
checked={checked}
|
|
52
|
+
defaultChecked={defaultChecked}
|
|
53
|
+
onCheckedChange={onCheckedChange}
|
|
54
|
+
indeterminate={indeterminate}
|
|
55
|
+
disabled={disabled}
|
|
56
|
+
required={required}
|
|
57
|
+
name={name}
|
|
58
|
+
value={value}
|
|
59
|
+
className={clsx(
|
|
60
|
+
'relative flex h-4 w-4 shrink-0 items-center justify-center rounded',
|
|
61
|
+
'border-2 border-(--border-gray) bg-(--bg-primary)',
|
|
62
|
+
'transition-[background-color,border-color] outline-none',
|
|
63
|
+
'focus-visible:ring-2 focus-visible:ring-(--focus-ring) focus-visible:ring-offset-1',
|
|
64
|
+
'data-[checked]:border-(--accent) data-[checked]:bg-(--accent)',
|
|
65
|
+
'data-[indeterminate]:border-(--accent) data-[indeterminate]:bg-(--accent)',
|
|
66
|
+
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
67
|
+
error && 'border-(--danger)',
|
|
68
|
+
className,
|
|
69
|
+
)}
|
|
70
|
+
>
|
|
71
|
+
<BaseCheckbox.Indicator className="flex items-center justify-center text-white">
|
|
72
|
+
{indeterminate ? <Minus size={10} strokeWidth={3} /> : <Check size={10} strokeWidth={3} />}
|
|
73
|
+
</BaseCheckbox.Indicator>
|
|
74
|
+
</BaseCheckbox.Root>
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
if (!label) return rootEl
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div className="flex flex-col gap-1">
|
|
81
|
+
<div className="flex items-start gap-2">
|
|
82
|
+
{rootEl}
|
|
83
|
+
<div className="flex flex-col">
|
|
84
|
+
<label
|
|
85
|
+
htmlFor={checkboxId}
|
|
86
|
+
className={clsx(
|
|
87
|
+
'text-[14px] leading-none text-(--text-primary) select-none',
|
|
88
|
+
disabled && 'opacity-50 cursor-not-allowed',
|
|
89
|
+
)}
|
|
90
|
+
>
|
|
91
|
+
{label}
|
|
92
|
+
{required && <span className="ml-0.5 text-(--danger)">*</span>}
|
|
93
|
+
</label>
|
|
94
|
+
{description && (
|
|
95
|
+
<span className="mt-0.5 text-[12px] text-(--text-muted)">{description}</span>
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
{error && (
|
|
100
|
+
<span className="flex items-center gap-1 text-[12px] font-medium leading-tight text-(--danger)">
|
|
101
|
+
⚠️ {error}
|
|
102
|
+
</span>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
)
|
|
106
|
+
},
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
Checkbox.displayName = 'Checkbox'
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Menu as BaseMenu } from '@base-ui/react'
|
|
3
|
+
import clsx from 'clsx'
|
|
4
|
+
import { ChevronRight } from 'lucide-react'
|
|
5
|
+
import { MenuTrigger, MenuItem, MenuCheckboxItem, MenuRadioItem } from './Menu'
|
|
6
|
+
|
|
7
|
+
export interface ContextMenuProps {
|
|
8
|
+
children: React.ReactNode
|
|
9
|
+
open?: boolean
|
|
10
|
+
defaultOpen?: boolean
|
|
11
|
+
onOpenChange?: (open: boolean) => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ContextMenuTriggerProps {
|
|
15
|
+
children: React.ReactNode
|
|
16
|
+
className?: string
|
|
17
|
+
disabled?: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ContextMenuContentProps {
|
|
21
|
+
children: React.ReactNode
|
|
22
|
+
className?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ContextMenuItemProps {
|
|
26
|
+
children: React.ReactNode
|
|
27
|
+
className?: string
|
|
28
|
+
disabled?: boolean
|
|
29
|
+
onSelect?: () => void
|
|
30
|
+
destructive?: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ContextMenuCheckboxItemProps {
|
|
34
|
+
children: React.ReactNode
|
|
35
|
+
className?: string
|
|
36
|
+
checked?: boolean
|
|
37
|
+
onCheckedChange?: (checked: boolean) => void
|
|
38
|
+
disabled?: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ContextMenuRadioGroupProps {
|
|
42
|
+
children: React.ReactNode
|
|
43
|
+
value?: string
|
|
44
|
+
onValueChange?: (value: string) => void
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ContextMenuRadioItemProps {
|
|
48
|
+
children: React.ReactNode
|
|
49
|
+
value: string
|
|
50
|
+
className?: string
|
|
51
|
+
disabled?: boolean
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ContextMenuLabelProps {
|
|
55
|
+
children: React.ReactNode
|
|
56
|
+
className?: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ContextMenuSeparatorProps {
|
|
60
|
+
className?: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ContextMenuSubProps {
|
|
64
|
+
children: React.ReactNode
|
|
65
|
+
open?: boolean
|
|
66
|
+
defaultOpen?: boolean
|
|
67
|
+
onOpenChange?: (open: boolean) => void
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ContextMenuSubTriggerProps {
|
|
71
|
+
children: React.ReactNode
|
|
72
|
+
className?: string
|
|
73
|
+
disabled?: boolean
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ContextMenuSubContentProps {
|
|
77
|
+
children: React.ReactNode
|
|
78
|
+
className?: string
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Context for sharing anchor position between components
|
|
82
|
+
const ContextMenuContext = React.createContext<{
|
|
83
|
+
anchorEl: VirtualElement | null
|
|
84
|
+
setAnchorEl: (el: VirtualElement | null) => void
|
|
85
|
+
} | null>(null)
|
|
86
|
+
|
|
87
|
+
interface VirtualElement {
|
|
88
|
+
getBoundingClientRect: () => DOMRect
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const ContextMenu: React.FC<ContextMenuProps> = ({
|
|
92
|
+
children,
|
|
93
|
+
open: controlledOpen,
|
|
94
|
+
defaultOpen,
|
|
95
|
+
onOpenChange,
|
|
96
|
+
}) => {
|
|
97
|
+
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen ?? false)
|
|
98
|
+
const [anchorEl, setAnchorEl] = React.useState<VirtualElement | null>(null)
|
|
99
|
+
|
|
100
|
+
const open = controlledOpen ?? uncontrolledOpen
|
|
101
|
+
const handleOpenChange = (newOpen: boolean) => {
|
|
102
|
+
setUncontrolledOpen(newOpen)
|
|
103
|
+
onOpenChange?.(newOpen)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<ContextMenuContext.Provider value={{ anchorEl, setAnchorEl }}>
|
|
108
|
+
<BaseMenu.Root
|
|
109
|
+
open={open}
|
|
110
|
+
onOpenChange={handleOpenChange}
|
|
111
|
+
>
|
|
112
|
+
{children}
|
|
113
|
+
</BaseMenu.Root>
|
|
114
|
+
</ContextMenuContext.Provider>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const ContextMenuTrigger = React.forwardRef<
|
|
119
|
+
HTMLDivElement,
|
|
120
|
+
ContextMenuTriggerProps
|
|
121
|
+
>(({ children, className, disabled }, ref) => {
|
|
122
|
+
const context = React.useContext(ContextMenuContext)
|
|
123
|
+
const triggerRef = React.useRef<HTMLButtonElement>(null)
|
|
124
|
+
|
|
125
|
+
const handleContextMenu = (e: React.MouseEvent) => {
|
|
126
|
+
e.preventDefault()
|
|
127
|
+
if (disabled) return
|
|
128
|
+
|
|
129
|
+
// Create a virtual element at the mouse position
|
|
130
|
+
const virtualEl: VirtualElement = {
|
|
131
|
+
getBoundingClientRect: () => ({
|
|
132
|
+
width: 0,
|
|
133
|
+
height: 0,
|
|
134
|
+
x: e.clientX,
|
|
135
|
+
y: e.clientY,
|
|
136
|
+
left: e.clientX,
|
|
137
|
+
right: e.clientX,
|
|
138
|
+
top: e.clientY,
|
|
139
|
+
bottom: e.clientY,
|
|
140
|
+
toJSON: () => ({}),
|
|
141
|
+
} as DOMRect),
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
context?.setAnchorEl(virtualEl)
|
|
145
|
+
|
|
146
|
+
// Programmatically trigger the menu
|
|
147
|
+
if (triggerRef.current) {
|
|
148
|
+
triggerRef.current.click()
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div
|
|
154
|
+
ref={ref}
|
|
155
|
+
className={clsx('outline-none focus:outline-none', className)}
|
|
156
|
+
onContextMenu={handleContextMenu}
|
|
157
|
+
>
|
|
158
|
+
{/* Hidden trigger button — must suppress all focus styles */}
|
|
159
|
+
<MenuTrigger
|
|
160
|
+
ref={triggerRef}
|
|
161
|
+
className="!absolute !w-0 !h-0 !p-0 !m-0 !border-0 !outline-none !shadow-none !opacity-0 overflow-hidden pointer-events-none"
|
|
162
|
+
render={
|
|
163
|
+
<button
|
|
164
|
+
disabled={disabled}
|
|
165
|
+
tabIndex={-1}
|
|
166
|
+
aria-hidden="true"
|
|
167
|
+
/>
|
|
168
|
+
}
|
|
169
|
+
/>
|
|
170
|
+
{children}
|
|
171
|
+
</div>
|
|
172
|
+
)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
ContextMenuTrigger.displayName = 'ContextMenuTrigger'
|
|
176
|
+
|
|
177
|
+
export const ContextMenuContent = React.forwardRef<
|
|
178
|
+
HTMLDivElement,
|
|
179
|
+
ContextMenuContentProps
|
|
180
|
+
>(({ children, className }, ref) => {
|
|
181
|
+
const context = React.useContext(ContextMenuContext)
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<BaseMenu.Portal>
|
|
185
|
+
<BaseMenu.Positioner
|
|
186
|
+
anchor={context?.anchorEl || undefined}
|
|
187
|
+
side="bottom"
|
|
188
|
+
align="start"
|
|
189
|
+
sideOffset={4}
|
|
190
|
+
>
|
|
191
|
+
<BaseMenu.Popup
|
|
192
|
+
ref={ref}
|
|
193
|
+
className={clsx(
|
|
194
|
+
'z-50 min-w-56 rounded-md border border-(--border-light) bg-(--bg-primary) shadow-lg',
|
|
195
|
+
'py-1 outline-0',
|
|
196
|
+
className
|
|
197
|
+
)}
|
|
198
|
+
>
|
|
199
|
+
{children}
|
|
200
|
+
</BaseMenu.Popup>
|
|
201
|
+
</BaseMenu.Positioner>
|
|
202
|
+
</BaseMenu.Portal>
|
|
203
|
+
|
|
204
|
+
)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
ContextMenuContent.displayName = 'ContextMenuContent'
|
|
208
|
+
|
|
209
|
+
export const ContextMenuItem = React.forwardRef<HTMLDivElement, ContextMenuItemProps>(
|
|
210
|
+
({ children, className, disabled, onSelect, destructive }, ref) => {
|
|
211
|
+
return (
|
|
212
|
+
<MenuItem
|
|
213
|
+
ref={ref}
|
|
214
|
+
className={className}
|
|
215
|
+
disabled={disabled}
|
|
216
|
+
onSelect={onSelect}
|
|
217
|
+
destructive={destructive}
|
|
218
|
+
>
|
|
219
|
+
{children}
|
|
220
|
+
</MenuItem>
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
ContextMenuItem.displayName = 'ContextMenuItem'
|
|
226
|
+
|
|
227
|
+
export const ContextMenuCheckboxItem = React.forwardRef<
|
|
228
|
+
HTMLDivElement,
|
|
229
|
+
ContextMenuCheckboxItemProps
|
|
230
|
+
>(({ children, className, checked, onCheckedChange, disabled }, ref) => {
|
|
231
|
+
return (
|
|
232
|
+
<MenuCheckboxItem
|
|
233
|
+
ref={ref}
|
|
234
|
+
className={className}
|
|
235
|
+
checked={checked}
|
|
236
|
+
onCheckedChange={onCheckedChange}
|
|
237
|
+
disabled={disabled}
|
|
238
|
+
>
|
|
239
|
+
{children}
|
|
240
|
+
</MenuCheckboxItem>
|
|
241
|
+
)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
ContextMenuCheckboxItem.displayName = 'ContextMenuCheckboxItem'
|
|
245
|
+
|
|
246
|
+
export const ContextMenuRadioGroup: React.FC<ContextMenuRadioGroupProps> = ({
|
|
247
|
+
children,
|
|
248
|
+
value,
|
|
249
|
+
onValueChange,
|
|
250
|
+
}) => {
|
|
251
|
+
return (
|
|
252
|
+
<BaseMenu.RadioGroup value={value} onValueChange={onValueChange}>
|
|
253
|
+
{children}
|
|
254
|
+
</BaseMenu.RadioGroup>
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export const ContextMenuRadioItem = React.forwardRef<
|
|
259
|
+
HTMLDivElement,
|
|
260
|
+
ContextMenuRadioItemProps
|
|
261
|
+
>(({ children, value, className, disabled }, ref) => {
|
|
262
|
+
return (
|
|
263
|
+
<MenuRadioItem
|
|
264
|
+
ref={ref}
|
|
265
|
+
value={value}
|
|
266
|
+
className={className}
|
|
267
|
+
disabled={disabled}
|
|
268
|
+
>
|
|
269
|
+
{children}
|
|
270
|
+
</MenuRadioItem>
|
|
271
|
+
)
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
ContextMenuRadioItem.displayName = 'ContextMenuRadioItem'
|
|
275
|
+
|
|
276
|
+
export const ContextMenuLabel: React.FC<ContextMenuLabelProps> = ({
|
|
277
|
+
children,
|
|
278
|
+
className,
|
|
279
|
+
}) => {
|
|
280
|
+
return (
|
|
281
|
+
<div
|
|
282
|
+
className={clsx(
|
|
283
|
+
'px-3 py-2 text-xs font-semibold text-(--text-muted) uppercase tracking-wider',
|
|
284
|
+
className
|
|
285
|
+
)}
|
|
286
|
+
>
|
|
287
|
+
{children}
|
|
288
|
+
</div>
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export const ContextMenuSeparator: React.FC<ContextMenuSeparatorProps> = ({
|
|
293
|
+
className,
|
|
294
|
+
}) => {
|
|
295
|
+
return (
|
|
296
|
+
<BaseMenu.Separator
|
|
297
|
+
className={clsx('my-1 h-px bg-(--border-light)', className)}
|
|
298
|
+
/>
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export const ContextMenuSub: React.FC<ContextMenuSubProps> = ({
|
|
303
|
+
children,
|
|
304
|
+
open,
|
|
305
|
+
defaultOpen,
|
|
306
|
+
onOpenChange,
|
|
307
|
+
}) => {
|
|
308
|
+
return (
|
|
309
|
+
<BaseMenu.Root
|
|
310
|
+
open={open}
|
|
311
|
+
defaultOpen={defaultOpen}
|
|
312
|
+
onOpenChange={onOpenChange}
|
|
313
|
+
>
|
|
314
|
+
{children}
|
|
315
|
+
</BaseMenu.Root>
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export const ContextMenuSubTrigger = React.forwardRef<
|
|
320
|
+
HTMLButtonElement,
|
|
321
|
+
ContextMenuSubTriggerProps
|
|
322
|
+
>(({ children, className, disabled }, ref) => {
|
|
323
|
+
return (
|
|
324
|
+
<MenuTrigger
|
|
325
|
+
ref={ref}
|
|
326
|
+
className={clsx(
|
|
327
|
+
'relative flex items-center justify-between gap-2 px-3 py-2 text-[15px] cursor-pointer select-none',
|
|
328
|
+
'text-(--text-primary) hover:bg-(--bg-hover)',
|
|
329
|
+
'data-highlighted:bg-(--bg-hover)',
|
|
330
|
+
'data-disabled:opacity-50 data-disabled:pointer-events-none w-full',
|
|
331
|
+
className
|
|
332
|
+
)}
|
|
333
|
+
render={
|
|
334
|
+
<button disabled={disabled}>
|
|
335
|
+
{children}
|
|
336
|
+
<ChevronRight className="w-4 h-4 ml-auto"/>
|
|
337
|
+
</button>
|
|
338
|
+
}
|
|
339
|
+
/>
|
|
340
|
+
)
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
ContextMenuSubTrigger.displayName = 'ContextMenuSubTrigger'
|
|
344
|
+
|
|
345
|
+
export const ContextMenuSubContent = React.forwardRef<
|
|
346
|
+
HTMLDivElement,
|
|
347
|
+
ContextMenuSubContentProps
|
|
348
|
+
>(({ children, className }, ref) => {
|
|
349
|
+
return (
|
|
350
|
+
<BaseMenu.Portal>
|
|
351
|
+
<BaseMenu.Positioner side="right" align="start" sideOffset={8}>
|
|
352
|
+
<BaseMenu.Popup
|
|
353
|
+
ref={ref}
|
|
354
|
+
className={clsx(
|
|
355
|
+
'z-50 min-w-45 max-w-80 rounded-md border border-(--border-light) bg-(--bg-primary) shadow-lg',
|
|
356
|
+
'py-1',
|
|
357
|
+
className
|
|
358
|
+
)}
|
|
359
|
+
>
|
|
360
|
+
{children}
|
|
361
|
+
</BaseMenu.Popup>
|
|
362
|
+
</BaseMenu.Positioner>
|
|
363
|
+
</BaseMenu.Portal>
|
|
364
|
+
)
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
ContextMenuSubContent.displayName = 'ContextMenuSubContent'
|
|
368
|
+
|
|
369
|
+
// Convenience component for context menu items with icons and shortcuts
|
|
370
|
+
export const ContextMenuItemWithIcon: React.FC<{
|
|
371
|
+
icon?: React.ReactNode
|
|
372
|
+
children: React.ReactNode
|
|
373
|
+
shortcut?: string
|
|
374
|
+
className?: string
|
|
375
|
+
disabled?: boolean
|
|
376
|
+
onSelect?: () => void
|
|
377
|
+
destructive?: boolean
|
|
378
|
+
}> = ({ icon, children, shortcut, className, disabled, onSelect, destructive }) => {
|
|
379
|
+
return (
|
|
380
|
+
<ContextMenuItem
|
|
381
|
+
className={className}
|
|
382
|
+
disabled={disabled}
|
|
383
|
+
onSelect={onSelect}
|
|
384
|
+
destructive={destructive}
|
|
385
|
+
>
|
|
386
|
+
{icon && <span className="shrink-0">{icon}</span>}
|
|
387
|
+
<span className="flex-1">{children}</span>
|
|
388
|
+
{shortcut && (
|
|
389
|
+
<span className="ml-auto text-xs text-(--text-muted)">{shortcut}</span>
|
|
390
|
+
)}
|
|
391
|
+
</ContextMenuItem>
|
|
392
|
+
)
|
|
393
|
+
}
|