@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,371 @@
|
|
|
1
|
+
import clsx from 'clsx'
|
|
2
|
+
import { Dialog as BaseDialog } from '@base-ui/react'
|
|
3
|
+
import React, {
|
|
4
|
+
createContext,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useMemo,
|
|
8
|
+
useState,
|
|
9
|
+
} from 'react'
|
|
10
|
+
import { X } from 'lucide-react'
|
|
11
|
+
import { Button } from './Button'
|
|
12
|
+
|
|
13
|
+
export type DialogSize = 'sm' | 'md' | 'lg' | 'xl';
|
|
14
|
+
|
|
15
|
+
export interface DialogProps {
|
|
16
|
+
open?: boolean;
|
|
17
|
+
onOpenChange?: (open: boolean) => void;
|
|
18
|
+
title?: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
children?: React.ReactNode;
|
|
21
|
+
size?: DialogSize;
|
|
22
|
+
showCloseButton?: boolean;
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const Dialog = ({
|
|
27
|
+
open,
|
|
28
|
+
onOpenChange,
|
|
29
|
+
title,
|
|
30
|
+
description,
|
|
31
|
+
children,
|
|
32
|
+
size = 'md',
|
|
33
|
+
showCloseButton = true,
|
|
34
|
+
className,
|
|
35
|
+
}: DialogProps) => {
|
|
36
|
+
const sizeClasses = {
|
|
37
|
+
sm: 'max-w-sm',
|
|
38
|
+
md: 'max-w-md',
|
|
39
|
+
lg: 'max-w-lg',
|
|
40
|
+
xl: 'max-w-xl',
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<BaseDialog.Root open={open} onOpenChange={onOpenChange}>
|
|
45
|
+
<BaseDialog.Portal>
|
|
46
|
+
{/* Backdrop */}
|
|
47
|
+
<BaseDialog.Backdrop
|
|
48
|
+
className="fixed inset-0 bg-black/50 z-40 transition-opacity duration-200"
|
|
49
|
+
/>
|
|
50
|
+
|
|
51
|
+
{/* Dialog Content */}
|
|
52
|
+
<BaseDialog.Popup
|
|
53
|
+
className={clsx(
|
|
54
|
+
'fixed left-[50%] top-[50%] z-50 translate-x-[-50%] translate-y-[-50%]',
|
|
55
|
+
'w-full',
|
|
56
|
+
sizeClasses[size],
|
|
57
|
+
'bg-(--bg-primary) rounded-xl border border-(--border-gray)',
|
|
58
|
+
'transition-all duration-200',
|
|
59
|
+
'outline-none focus-visible:outline-none',
|
|
60
|
+
className,
|
|
61
|
+
)}
|
|
62
|
+
>
|
|
63
|
+
{/* Header */}
|
|
64
|
+
{(title || showCloseButton) && (
|
|
65
|
+
<div className="flex items-start justify-between p-6 border-b border-(--border-light)">
|
|
66
|
+
<div className="flex-1">
|
|
67
|
+
{title && (
|
|
68
|
+
<BaseDialog.Title className="text-2xl font-black text-(--text-primary) leading-tight">
|
|
69
|
+
{title}
|
|
70
|
+
</BaseDialog.Title>
|
|
71
|
+
)}
|
|
72
|
+
{description && (
|
|
73
|
+
<BaseDialog.Description className="text-(--text-secondary) mt-2 text-sm">
|
|
74
|
+
{description}
|
|
75
|
+
</BaseDialog.Description>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
{showCloseButton && (
|
|
79
|
+
<BaseDialog.Close
|
|
80
|
+
className="ml-4 p-2 rounded hover:bg-(--bg-hover) text-(--text-secondary) hover:text-(--text-primary) transition-colors focus-visible:ring-2 focus-visible:ring-(--slack-blue) focus-visible:ring-offset-2 outline-none"
|
|
81
|
+
aria-label="Close dialog"
|
|
82
|
+
>
|
|
83
|
+
<X className="w-5 h-5"/>
|
|
84
|
+
</BaseDialog.Close>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
{/* Body */}
|
|
90
|
+
<div className="p-6">{children}</div>
|
|
91
|
+
</BaseDialog.Popup>
|
|
92
|
+
</BaseDialog.Portal>
|
|
93
|
+
</BaseDialog.Root>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
Dialog.displayName = 'Dialog'
|
|
98
|
+
|
|
99
|
+
// Sub-components for custom layouts
|
|
100
|
+
export const DialogHeader = ({ children, className }: { children: React.ReactNode; className?: string }) => (
|
|
101
|
+
<div className={clsx('mb-4', className)}>{children}</div>
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
export const DialogBody = ({ children, className }: { children: React.ReactNode; className?: string }) => (
|
|
105
|
+
<div className={clsx('text-(--text-primary)', className)}>{children}</div>
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
export const DialogFooter = ({ children, className }: { children: React.ReactNode; className?: string }) => (
|
|
109
|
+
<div className={clsx('flex items-center justify-end gap-3 mt-6 pt-4 border-t border-(--border-light)', className)}>
|
|
110
|
+
{children}
|
|
111
|
+
</div>
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
// Trigger component for opening dialogs
|
|
115
|
+
export interface DialogTriggerProps extends React.ComponentPropsWithoutRef<'button'> {
|
|
116
|
+
asChild?: boolean;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const DialogTrigger = React.forwardRef<HTMLButtonElement, DialogTriggerProps>(
|
|
120
|
+
({ children, asChild, ...props }, ref) => {
|
|
121
|
+
if (asChild) {
|
|
122
|
+
return <BaseDialog.Trigger ref={ref}>{children}</BaseDialog.Trigger>
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<BaseDialog.Trigger ref={ref} {...props}>
|
|
127
|
+
{children}
|
|
128
|
+
</BaseDialog.Trigger>
|
|
129
|
+
)
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
DialogTrigger.displayName = 'DialogTrigger'
|
|
134
|
+
|
|
135
|
+
// Re-export Close for custom usage
|
|
136
|
+
export const DialogClose = BaseDialog.Close
|
|
137
|
+
|
|
138
|
+
// ─── Imperative Dialog API ────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
export interface ShowDialogOptions {
|
|
141
|
+
title?: string
|
|
142
|
+
description?: string
|
|
143
|
+
/** Custom content rendered inside the dialog body */
|
|
144
|
+
content?: React.ReactNode
|
|
145
|
+
size?: DialogSize
|
|
146
|
+
showCloseButton?: boolean
|
|
147
|
+
className?: string
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface ConfirmDialogOptions {
|
|
151
|
+
title?: string
|
|
152
|
+
description?: string
|
|
153
|
+
content?: React.ReactNode
|
|
154
|
+
size?: DialogSize
|
|
155
|
+
confirmLabel?: string
|
|
156
|
+
cancelLabel?: string
|
|
157
|
+
confirmVariant?: 'primary' | 'danger' | 'secondary'
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface AlertDialogOptions {
|
|
161
|
+
title?: string
|
|
162
|
+
description?: string
|
|
163
|
+
content?: React.ReactNode
|
|
164
|
+
size?: DialogSize
|
|
165
|
+
confirmLabel?: string
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface UseDialogReturn {
|
|
169
|
+
/** Show a generic dialog. Resolves when the dialog is closed. */
|
|
170
|
+
show: (options: ShowDialogOptions) => Promise<void>
|
|
171
|
+
/** Show a confirm dialog. Resolves `true` when confirmed, `false` when cancelled. */
|
|
172
|
+
confirm: (options: ConfirmDialogOptions) => Promise<boolean>
|
|
173
|
+
/** Show an alert dialog with a single OK button. Resolves when dismissed. */
|
|
174
|
+
alert: (options: AlertDialogOptions) => Promise<void>
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
interface DialogEntry {
|
|
178
|
+
id: string
|
|
179
|
+
type: 'show' | 'confirm' | 'alert'
|
|
180
|
+
title?: string
|
|
181
|
+
description?: string
|
|
182
|
+
content?: React.ReactNode
|
|
183
|
+
size?: DialogSize
|
|
184
|
+
showCloseButton?: boolean
|
|
185
|
+
confirmLabel?: string
|
|
186
|
+
cancelLabel?: string
|
|
187
|
+
confirmVariant?: 'primary' | 'danger' | 'secondary'
|
|
188
|
+
className?: string
|
|
189
|
+
resolve: (value: boolean) => void
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const DialogImperativeContext = createContext<UseDialogReturn | null>(null)
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Provides the imperative dialog API to all descendant components.
|
|
196
|
+
* Must wrap any component that calls `useDialog()`.
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* ```tsx
|
|
200
|
+
* // main.tsx
|
|
201
|
+
* <DialogProvider>
|
|
202
|
+
* <App />
|
|
203
|
+
* </DialogProvider>
|
|
204
|
+
*
|
|
205
|
+
* // Any component
|
|
206
|
+
* const { show, confirm, alert } = useDialog()
|
|
207
|
+
* await confirm({ title: 'Delete?', confirmLabel: 'Delete', confirmVariant: 'danger' })
|
|
208
|
+
* ```
|
|
209
|
+
*/
|
|
210
|
+
export const DialogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
211
|
+
const [dialogs, setDialogs] = useState<DialogEntry[]>([])
|
|
212
|
+
|
|
213
|
+
const removeDialog = useCallback((id: string) => {
|
|
214
|
+
setDialogs(prev => prev.filter(d => d.id !== id))
|
|
215
|
+
}, [])
|
|
216
|
+
|
|
217
|
+
const show = useCallback((options: ShowDialogOptions): Promise<void> => {
|
|
218
|
+
return new Promise(resolve => {
|
|
219
|
+
const id = Math.random().toString(36).slice(2, 9)
|
|
220
|
+
setDialogs(prev => [
|
|
221
|
+
...prev,
|
|
222
|
+
{ ...options, id, type: 'show', resolve: () => resolve() },
|
|
223
|
+
])
|
|
224
|
+
})
|
|
225
|
+
}, [])
|
|
226
|
+
|
|
227
|
+
const confirm = useCallback((options: ConfirmDialogOptions): Promise<boolean> => {
|
|
228
|
+
return new Promise(resolve => {
|
|
229
|
+
const id = Math.random().toString(36).slice(2, 9)
|
|
230
|
+
setDialogs(prev => [...prev, { ...options, id, type: 'confirm', resolve }])
|
|
231
|
+
})
|
|
232
|
+
}, [])
|
|
233
|
+
|
|
234
|
+
const alert = useCallback((options: AlertDialogOptions): Promise<void> => {
|
|
235
|
+
return new Promise(resolve => {
|
|
236
|
+
const id = Math.random().toString(36).slice(2, 9)
|
|
237
|
+
setDialogs(prev => [
|
|
238
|
+
...prev,
|
|
239
|
+
{ ...options, id, type: 'alert', resolve: () => resolve() },
|
|
240
|
+
])
|
|
241
|
+
})
|
|
242
|
+
}, [])
|
|
243
|
+
|
|
244
|
+
const value = useMemo<UseDialogReturn>(
|
|
245
|
+
() => ({ show, confirm, alert }),
|
|
246
|
+
[show, confirm, alert],
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<DialogImperativeContext.Provider value={value}>
|
|
251
|
+
{children}
|
|
252
|
+
{dialogs.map(dialog => (
|
|
253
|
+
<ImperativeDialogItem
|
|
254
|
+
key={dialog.id}
|
|
255
|
+
dialog={dialog}
|
|
256
|
+
onRemove={() => removeDialog(dialog.id)}
|
|
257
|
+
/>
|
|
258
|
+
))}
|
|
259
|
+
</DialogImperativeContext.Provider>
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
DialogProvider.displayName = 'DialogProvider'
|
|
264
|
+
|
|
265
|
+
function ImperativeDialogItem({
|
|
266
|
+
dialog,
|
|
267
|
+
onRemove,
|
|
268
|
+
}: {
|
|
269
|
+
dialog: DialogEntry
|
|
270
|
+
onRemove: () => void
|
|
271
|
+
}) {
|
|
272
|
+
const [open, setOpen] = useState(true)
|
|
273
|
+
|
|
274
|
+
const handleClose = useCallback(
|
|
275
|
+
(result: boolean) => {
|
|
276
|
+
setOpen(false)
|
|
277
|
+
dialog.resolve(result)
|
|
278
|
+
// Allow close animation to finish before unmounting
|
|
279
|
+
setTimeout(onRemove, 300)
|
|
280
|
+
},
|
|
281
|
+
[dialog, onRemove],
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
const commonProps = {
|
|
285
|
+
open,
|
|
286
|
+
title: dialog.title,
|
|
287
|
+
description: dialog.description,
|
|
288
|
+
size: dialog.size,
|
|
289
|
+
className: dialog.className,
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (dialog.type === 'show') {
|
|
293
|
+
return (
|
|
294
|
+
<Dialog
|
|
295
|
+
{...commonProps}
|
|
296
|
+
showCloseButton={dialog.showCloseButton ?? true}
|
|
297
|
+
onOpenChange={o => !o && handleClose(false)}
|
|
298
|
+
>
|
|
299
|
+
{dialog.content}
|
|
300
|
+
</Dialog>
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (dialog.type === 'confirm') {
|
|
305
|
+
return (
|
|
306
|
+
<Dialog
|
|
307
|
+
{...commonProps}
|
|
308
|
+
size={dialog.size ?? 'sm'}
|
|
309
|
+
showCloseButton={false}
|
|
310
|
+
onOpenChange={o => !o && handleClose(false)}
|
|
311
|
+
>
|
|
312
|
+
{dialog.content}
|
|
313
|
+
<DialogFooter>
|
|
314
|
+
<Button variant="secondary" onClick={() => handleClose(false)}>
|
|
315
|
+
{dialog.cancelLabel ?? 'Cancel'}
|
|
316
|
+
</Button>
|
|
317
|
+
<Button
|
|
318
|
+
variant={dialog.confirmVariant ?? 'primary'}
|
|
319
|
+
onClick={() => handleClose(true)}
|
|
320
|
+
>
|
|
321
|
+
{dialog.confirmLabel ?? 'Confirm'}
|
|
322
|
+
</Button>
|
|
323
|
+
</DialogFooter>
|
|
324
|
+
</Dialog>
|
|
325
|
+
)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// alert
|
|
329
|
+
return (
|
|
330
|
+
<Dialog
|
|
331
|
+
{...commonProps}
|
|
332
|
+
size={dialog.size ?? 'sm'}
|
|
333
|
+
showCloseButton={false}
|
|
334
|
+
onOpenChange={o => !o && handleClose(true)}
|
|
335
|
+
>
|
|
336
|
+
{dialog.content}
|
|
337
|
+
<DialogFooter>
|
|
338
|
+
<Button variant="primary" onClick={() => handleClose(true)}>
|
|
339
|
+
{dialog.confirmLabel ?? 'OK'}
|
|
340
|
+
</Button>
|
|
341
|
+
</DialogFooter>
|
|
342
|
+
</Dialog>
|
|
343
|
+
)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Returns imperative methods for showing dialogs from anywhere in the component tree.
|
|
348
|
+
*
|
|
349
|
+
* Must be used inside `<DialogProvider>`.
|
|
350
|
+
*
|
|
351
|
+
* @example
|
|
352
|
+
* ```tsx
|
|
353
|
+
* const { show, confirm, alert } = useDialog()
|
|
354
|
+
*
|
|
355
|
+
* // Generic dialog
|
|
356
|
+
* await show({ title: 'Welcome', content: <p>Hello world</p> })
|
|
357
|
+
*
|
|
358
|
+
* // Confirm
|
|
359
|
+
* const ok = await confirm({ title: 'Delete item?', confirmLabel: 'Delete', confirmVariant: 'danger' })
|
|
360
|
+
* if (ok) deleteItem()
|
|
361
|
+
*
|
|
362
|
+
* // Alert
|
|
363
|
+
* await alert({ title: 'Error', description: 'Something went wrong.' })
|
|
364
|
+
* ```
|
|
365
|
+
*/
|
|
366
|
+
export function useDialog(): UseDialogReturn {
|
|
367
|
+
const ctx = useContext(DialogImperativeContext)
|
|
368
|
+
if (!ctx) throw new Error('useDialog must be used within <DialogProvider>')
|
|
369
|
+
return ctx
|
|
370
|
+
}
|
|
371
|
+
|