@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.
Files changed (54) hide show
  1. package/README.md +220 -4
  2. package/agents/slack-base-ui/SKILL.md +137 -0
  3. package/agents/slack-base-ui/checklists/style-review.md +56 -0
  4. package/agents/slack-base-ui/templates/consumer-setup.md +109 -0
  5. package/agents/slack-base-ui/templates/slack-theme.css +152 -0
  6. package/libs/Dialog.d.ts +73 -0
  7. package/libs/Dialog.d.ts.map +1 -1
  8. package/libs/Popover.d.ts +69 -0
  9. package/libs/Popover.d.ts.map +1 -1
  10. package/libs/index.d.ts +4 -4
  11. package/libs/index.d.ts.map +1 -1
  12. package/libs/index.js +2885 -2718
  13. package/package.json +1 -1
  14. package/src/App.css +7 -0
  15. package/src/App.tsx +18 -0
  16. package/src/assets/react.svg +1 -0
  17. package/src/components/AlertDialog.tsx +185 -0
  18. package/src/components/AutoComplete.tsx +311 -0
  19. package/src/components/Avatar.tsx +70 -0
  20. package/src/components/Badge.tsx +48 -0
  21. package/src/components/Button.tsx +53 -0
  22. package/src/components/Checkbox.tsx +109 -0
  23. package/src/components/ContextMenu.tsx +393 -0
  24. package/src/components/Dialog.tsx +371 -0
  25. package/src/components/Form.tsx +409 -0
  26. package/src/components/IconButton.tsx +49 -0
  27. package/src/components/Input.tsx +56 -0
  28. package/src/components/Loading.tsx +123 -0
  29. package/src/components/Menu.tsx +368 -0
  30. package/src/components/Popover.tsx +367 -0
  31. package/src/components/Progress.tsx +89 -0
  32. package/src/components/Radio.tsx +137 -0
  33. package/src/components/Select.tsx +177 -0
  34. package/src/components/Switch.tsx +116 -0
  35. package/src/components/Tabs.tsx +128 -0
  36. package/src/components/Toast.tsx +149 -0
  37. package/src/components/Tooltip.tsx +46 -0
  38. package/src/components/index.ts +186 -0
  39. package/src/context/ThemeContext.tsx +53 -0
  40. package/src/context/useTheme.ts +11 -0
  41. package/src/examples/slack-clone/SlackApp.tsx +94 -0
  42. package/src/examples/slack-clone/components/ChannelHeader.tsx +34 -0
  43. package/src/examples/slack-clone/components/Composer.tsx +42 -0
  44. package/src/examples/slack-clone/components/Message.tsx +97 -0
  45. package/src/examples/slack-clone/components/UserProfile.tsx +78 -0
  46. package/src/examples/slack-clone/layout/Layout.tsx +27 -0
  47. package/src/examples/slack-clone/layout/Sidebar.tsx +67 -0
  48. package/src/examples/slack-clone/layout/SidebarItem.tsx +57 -0
  49. package/src/examples/slack-clone/layout/TopBar.tsx +30 -0
  50. package/src/index.css +240 -0
  51. package/src/main.tsx +22 -0
  52. package/src/pages/ComponentShowcase.tsx +1964 -0
  53. package/src/pages/Dashboard.tsx +87 -0
  54. package/src/pages/QuickStartDemo.tsx +262 -0
@@ -0,0 +1,367 @@
1
+ import React, { createContext, useCallback, useContext, useMemo, useState } 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
+ // ─── Imperative Popover API ───────────────────────────────────────────────────
198
+
199
+ export interface ImperativePopoverOptions {
200
+ /** Content rendered inside the popover */
201
+ content: React.ReactNode
202
+ side?: 'top' | 'right' | 'bottom' | 'left'
203
+ align?: 'start' | 'center' | 'end'
204
+ sideOffset?: number
205
+ alignOffset?: number
206
+ className?: string
207
+ }
208
+
209
+ export interface UseImperativePopoverReturn {
210
+ /**
211
+ * Show the popover anchored to the given element.
212
+ * If already open on the same anchor, calling `show` again replaces the content.
213
+ */
214
+ show: (anchor: HTMLElement | null, options: ImperativePopoverOptions) => void
215
+ /** Hide the currently open popover. */
216
+ hide: () => void
217
+ /**
218
+ * Toggle the popover for the given anchor.
219
+ * Closes it if the same anchor is already open; otherwise opens on the new anchor.
220
+ */
221
+ toggle: (anchor: HTMLElement | null, options: ImperativePopoverOptions) => void
222
+ /** Whether the imperative popover is currently open. */
223
+ isOpen: boolean
224
+ }
225
+
226
+ interface ImperativePopoverState {
227
+ open: boolean
228
+ anchor: HTMLElement | null
229
+ options: ImperativePopoverOptions
230
+ }
231
+
232
+ const ImperativePopoverContext = createContext<UseImperativePopoverReturn | null>(null)
233
+
234
+ /**
235
+ * Provides the imperative popover API to all descendant components.
236
+ * Only one popover is shown at a time (singleton).
237
+ * Must wrap any component that calls `useImperativePopover()`.
238
+ *
239
+ * @example
240
+ * ```tsx
241
+ * // main.tsx
242
+ * <ImperativePopoverProvider>
243
+ * <App />
244
+ * </ImperativePopoverProvider>
245
+ *
246
+ * // Any component
247
+ * const popover = useImperativePopover()
248
+ *
249
+ * <button onClick={e => popover.show(e.currentTarget, { content: <MyPanel /> })}>
250
+ * Open
251
+ * </button>
252
+ * ```
253
+ */
254
+ export const ImperativePopoverProvider: React.FC<{ children: React.ReactNode }> = ({
255
+ children,
256
+ }) => {
257
+ const [state, setState] = useState<ImperativePopoverState>({
258
+ open: false,
259
+ anchor: null,
260
+ options: { content: null },
261
+ })
262
+
263
+ const show = useCallback(
264
+ (anchor: HTMLElement | null, options: ImperativePopoverOptions) => {
265
+ setState({ open: true, anchor, options })
266
+ },
267
+ [],
268
+ )
269
+
270
+ const hide = useCallback(() => {
271
+ setState(prev => ({ ...prev, open: false }))
272
+ }, [])
273
+
274
+ const toggle = useCallback(
275
+ (anchor: HTMLElement | null, options: ImperativePopoverOptions) => {
276
+ setState(prev => {
277
+ if (prev.open && prev.anchor === anchor) {
278
+ return { ...prev, open: false }
279
+ }
280
+ return { open: true, anchor, options }
281
+ })
282
+ },
283
+ [],
284
+ )
285
+
286
+ const value = useMemo<UseImperativePopoverReturn>(
287
+ () => ({ show, hide, toggle, isOpen: state.open }),
288
+ [show, hide, toggle, state.open],
289
+ )
290
+
291
+ return (
292
+ <ImperativePopoverContext.Provider value={value}>
293
+ {children}
294
+ <ImperativePopoverPortal state={state} onClose={hide} />
295
+ </ImperativePopoverContext.Provider>
296
+ )
297
+ }
298
+
299
+ ImperativePopoverProvider.displayName = 'ImperativePopoverProvider'
300
+
301
+ function ImperativePopoverPortal({
302
+ state,
303
+ onClose,
304
+ }: {
305
+ state: ImperativePopoverState
306
+ onClose: () => void
307
+ }) {
308
+ if (!state.open || !state.anchor) return null
309
+
310
+ const { options } = state
311
+
312
+ return (
313
+ <BasePopover.Root open={state.open} onOpenChange={open => !open && onClose()}>
314
+ <BasePopover.Portal>
315
+ <BasePopover.Positioner
316
+ anchor={state.anchor}
317
+ side={options.side ?? 'bottom'}
318
+ align={options.align ?? 'center'}
319
+ sideOffset={options.sideOffset ?? 8}
320
+ alignOffset={options.alignOffset ?? 0}
321
+ >
322
+ <BasePopover.Popup
323
+ className={clsx(
324
+ 'z-50 min-w-50 max-w-90 rounded-lg border border-(--border-light) bg-(--bg-primary) shadow-lg',
325
+ options.className,
326
+ )}
327
+ >
328
+ {options.content}
329
+ <BasePopover.Arrow className="fill-(--bg-primary) stroke-(--border-light)" />
330
+ </BasePopover.Popup>
331
+ </BasePopover.Positioner>
332
+ </BasePopover.Portal>
333
+ </BasePopover.Root>
334
+ )
335
+ }
336
+
337
+ /**
338
+ * Returns imperative methods for showing a popover anchored to any element.
339
+ *
340
+ * Must be used inside `<ImperativePopoverProvider>`.
341
+ *
342
+ * @example
343
+ * ```tsx
344
+ * const popover = useImperativePopover()
345
+ *
346
+ * // Show on button click
347
+ * <button onClick={e => popover.show(e.currentTarget, { content: <ProfileCard />, side: 'bottom' })}>
348
+ * Profile
349
+ * </button>
350
+ *
351
+ * // Toggle
352
+ * <button onClick={e => popover.toggle(e.currentTarget, { content: <Settings /> })}>
353
+ * Settings
354
+ * </button>
355
+ * ```
356
+ */
357
+ export function useImperativePopover(): UseImperativePopoverReturn {
358
+ const ctx = useContext(ImperativePopoverContext)
359
+ if (!ctx) throw new Error('useImperativePopover must be used within <ImperativePopoverProvider>')
360
+ return ctx
361
+ }
362
+
363
+
364
+
365
+
366
+
367
+
@@ -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'
@@ -0,0 +1,137 @@
1
+ import React from 'react'
2
+ import { Radio as BaseRadio, RadioGroup as BaseRadioGroup } from '@base-ui/react'
3
+ import clsx from 'clsx'
4
+
5
+ // ── RadioGroup ────────────────────────────────────────────────────────────────
6
+
7
+ export interface RadioGroupProps {
8
+ children: React.ReactNode
9
+ value?: string
10
+ defaultValue?: string
11
+ onValueChange?: (value: string) => void
12
+ disabled?: boolean
13
+ required?: boolean
14
+ /** Group label shown above the radios */
15
+ label?: string
16
+ /** Error message */
17
+ error?: string
18
+ /** Arrange items horizontally instead of vertically */
19
+ orientation?: 'horizontal' | 'vertical'
20
+ className?: string
21
+ }
22
+
23
+ export const RadioGroup = React.forwardRef<HTMLDivElement, RadioGroupProps>(
24
+ (
25
+ {
26
+ children,
27
+ value,
28
+ defaultValue,
29
+ onValueChange,
30
+ disabled,
31
+ required,
32
+ label,
33
+ error,
34
+ orientation = 'vertical',
35
+ className,
36
+ },
37
+ ref,
38
+ ) => {
39
+ return (
40
+ <div className={clsx('flex flex-col gap-1.5', className)}>
41
+ {label && (
42
+ <span className="text-[14px] font-semibold text-(--text-primary)">
43
+ {label}
44
+ {required && <span className="ml-0.5 text-(--danger)">*</span>}
45
+ </span>
46
+ )}
47
+
48
+ <BaseRadioGroup
49
+ ref={ref}
50
+ value={value}
51
+ defaultValue={defaultValue}
52
+ onValueChange={onValueChange}
53
+ disabled={disabled}
54
+ required={required}
55
+ className={clsx(
56
+ 'flex gap-3',
57
+ orientation === 'vertical' ? 'flex-col' : 'flex-row flex-wrap',
58
+ )}
59
+ >
60
+ {children}
61
+ </BaseRadioGroup>
62
+
63
+ {error && (
64
+ <span className="flex items-center gap-1 text-[12px] font-medium leading-tight text-(--danger)">
65
+ ⚠️ {error}
66
+ </span>
67
+ )}
68
+ </div>
69
+ )
70
+ },
71
+ )
72
+
73
+ RadioGroup.displayName = 'RadioGroup'
74
+
75
+ // ── Radio ─────────────────────────────────────────────────────────────────────
76
+
77
+ export interface RadioProps {
78
+ value: string
79
+ disabled?: boolean
80
+ /** Label text shown next to the radio */
81
+ label?: string
82
+ /** Helper text shown below the label */
83
+ description?: string
84
+ id?: string
85
+ className?: string
86
+ }
87
+
88
+ export const Radio = React.forwardRef<HTMLButtonElement, RadioProps>(
89
+ ({ value, disabled, label, description, id, className }, ref) => {
90
+ const generatedId = React.useId()
91
+ const radioId = id ?? generatedId
92
+
93
+ const radioEl = (
94
+ <BaseRadio.Root
95
+ ref={ref}
96
+ id={radioId}
97
+ value={value}
98
+ disabled={disabled}
99
+ className={clsx(
100
+ 'relative flex h-4 w-4 shrink-0 items-center justify-center rounded-full',
101
+ 'border-2 border-(--border-gray) bg-(--bg-primary)',
102
+ 'transition-[background-color,border-color] outline-none',
103
+ 'focus-visible:ring-2 focus-visible:ring-(--focus-ring) focus-visible:ring-offset-1',
104
+ 'data-[checked]:border-(--accent)',
105
+ 'disabled:cursor-not-allowed disabled:opacity-50',
106
+ className,
107
+ )}
108
+ >
109
+ <BaseRadio.Indicator className="h-2 w-2 rounded-full bg-(--accent) data-[unchecked]:hidden" />
110
+ </BaseRadio.Root>
111
+ )
112
+
113
+ if (!label) return radioEl
114
+
115
+ return (
116
+ <div className="flex items-start gap-2">
117
+ {radioEl}
118
+ <div className="flex flex-col">
119
+ <label
120
+ htmlFor={radioId}
121
+ className={clsx(
122
+ 'text-[14px] leading-none text-(--text-primary) select-none',
123
+ disabled && 'opacity-50 cursor-not-allowed',
124
+ )}
125
+ >
126
+ {label}
127
+ </label>
128
+ {description && (
129
+ <span className="mt-0.5 text-[12px] text-(--text-muted)">{description}</span>
130
+ )}
131
+ </div>
132
+ </div>
133
+ )
134
+ },
135
+ )
136
+
137
+ Radio.displayName = 'Radio'