@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.
Files changed (42) hide show
  1. package/package.json +1 -1
  2. package/src/App.css +7 -0
  3. package/src/App.tsx +18 -0
  4. package/src/assets/react.svg +1 -0
  5. package/src/components/AlertDialog.tsx +185 -0
  6. package/src/components/AutoComplete.tsx +311 -0
  7. package/src/components/Avatar.tsx +70 -0
  8. package/src/components/Badge.tsx +48 -0
  9. package/src/components/Button.tsx +53 -0
  10. package/src/components/Checkbox.tsx +109 -0
  11. package/src/components/ContextMenu.tsx +393 -0
  12. package/src/components/Dialog.tsx +129 -0
  13. package/src/components/Form.tsx +409 -0
  14. package/src/components/IconButton.tsx +49 -0
  15. package/src/components/Input.tsx +56 -0
  16. package/src/components/Loading.tsx +123 -0
  17. package/src/components/Menu.tsx +368 -0
  18. package/src/components/Popover.tsx +200 -0
  19. package/src/components/Progress.tsx +89 -0
  20. package/src/components/Radio.tsx +137 -0
  21. package/src/components/Select.tsx +177 -0
  22. package/src/components/Switch.tsx +116 -0
  23. package/src/components/Tabs.tsx +128 -0
  24. package/src/components/Toast.tsx +149 -0
  25. package/src/components/Tooltip.tsx +46 -0
  26. package/src/components/index.ts +165 -0
  27. package/src/context/ThemeContext.tsx +53 -0
  28. package/src/context/useTheme.ts +11 -0
  29. package/src/examples/slack-clone/SlackApp.tsx +94 -0
  30. package/src/examples/slack-clone/components/ChannelHeader.tsx +34 -0
  31. package/src/examples/slack-clone/components/Composer.tsx +42 -0
  32. package/src/examples/slack-clone/components/Message.tsx +97 -0
  33. package/src/examples/slack-clone/components/UserProfile.tsx +78 -0
  34. package/src/examples/slack-clone/layout/Layout.tsx +27 -0
  35. package/src/examples/slack-clone/layout/Sidebar.tsx +67 -0
  36. package/src/examples/slack-clone/layout/SidebarItem.tsx +57 -0
  37. package/src/examples/slack-clone/layout/TopBar.tsx +30 -0
  38. package/src/index.css +240 -0
  39. package/src/main.tsx +16 -0
  40. package/src/pages/ComponentShowcase.tsx +1618 -0
  41. package/src/pages/Dashboard.tsx +87 -0
  42. package/src/pages/QuickStartDemo.tsx +262 -0
@@ -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'
@@ -0,0 +1,177 @@
1
+ import React from 'react'
2
+ import { Select as BaseSelect } from '@base-ui/react'
3
+ import clsx from 'clsx'
4
+ import { Check, ChevronDown, ChevronUp } from 'lucide-react'
5
+
6
+ export interface SelectOption {
7
+ value: string
8
+ label: string
9
+ disabled?: boolean
10
+ }
11
+
12
+ export interface SelectGroup {
13
+ label: string
14
+ options: SelectOption[]
15
+ }
16
+
17
+ export interface SelectProps {
18
+ /** Option items (flat list or grouped) */
19
+ options?: SelectOption[]
20
+ groups?: SelectGroup[]
21
+ value?: string
22
+ defaultValue?: string
23
+ onValueChange?: (value: string | null) => void
24
+ placeholder?: string
25
+ disabled?: boolean
26
+ required?: boolean
27
+ /** Label shown above the trigger */
28
+ label?: string
29
+ /** Error message shown below the trigger */
30
+ error?: string
31
+ /** Full width */
32
+ fullWidth?: boolean
33
+ className?: string
34
+ id?: string
35
+ }
36
+
37
+ const itemClass = clsx(
38
+ 'relative flex cursor-default select-none items-center rounded px-3 py-1.5 text-[14px] text-(--text-primary)',
39
+ 'data-[highlighted]:bg-(--bg-hover) data-[highlighted]:outline-none',
40
+ 'data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed',
41
+ )
42
+
43
+ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(
44
+ (
45
+ {
46
+ options,
47
+ groups,
48
+ value,
49
+ defaultValue,
50
+ onValueChange,
51
+ placeholder = 'Select an option',
52
+ disabled,
53
+ required,
54
+ label,
55
+ error,
56
+ fullWidth,
57
+ className,
58
+ id,
59
+ },
60
+ ref,
61
+ ) => {
62
+ const generatedId = React.useId()
63
+ const triggerId = id ?? generatedId
64
+
65
+ return (
66
+ <div className={clsx('flex flex-col gap-1.5', fullWidth && 'w-full')}>
67
+ {label && (
68
+ <label
69
+ htmlFor={triggerId}
70
+ className="text-[14px] font-semibold text-(--text-primary)"
71
+ >
72
+ {label}
73
+ {required && <span className="ml-0.5 text-(--danger)">*</span>}
74
+ </label>
75
+ )}
76
+
77
+ <BaseSelect.Root
78
+ value={value}
79
+ defaultValue={defaultValue}
80
+ onValueChange={onValueChange}
81
+ disabled={disabled}
82
+ required={required}
83
+ >
84
+ <BaseSelect.Trigger
85
+ id={triggerId}
86
+ ref={ref}
87
+ className={clsx(
88
+ 'inline-flex h-9 w-full items-center justify-between gap-2 rounded-md border px-3 text-[14px]',
89
+ 'bg-(--bg-primary) text-(--text-primary)',
90
+ 'transition-[border-color,box-shadow] outline-none',
91
+ 'focus:border-(--accent)/70',
92
+ 'disabled:cursor-not-allowed disabled:opacity-50',
93
+ error
94
+ ? 'border-(--danger) focus:shadow-[0_0_0_2px_var(--danger)]'
95
+ : 'border-(--border-light) hover:border-(--text-primary)/30',
96
+ className,
97
+ )}
98
+ >
99
+ <BaseSelect.Value
100
+ className="flex-1 text-left data-[placeholder]:text-(--text-muted)"
101
+ placeholder={placeholder}
102
+ />
103
+ <BaseSelect.Icon className="shrink-0 text-(--text-muted)">
104
+ <ChevronDown size={14} />
105
+ </BaseSelect.Icon>
106
+ </BaseSelect.Trigger>
107
+
108
+ <BaseSelect.Portal>
109
+ <BaseSelect.Positioner sideOffset={4} className="z-50">
110
+ <BaseSelect.ScrollUpArrow className="flex h-5 w-full cursor-default items-center justify-center rounded-t-md bg-(--bg-primary) text-(--text-muted)">
111
+ <ChevronUp size={12} />
112
+ </BaseSelect.ScrollUpArrow>
113
+
114
+ <BaseSelect.Popup
115
+ className={clsx(
116
+ 'min-w-[var(--anchor-width)] overflow-auto rounded-lg border border-(--border-light) bg-(--bg-primary)',
117
+ 'py-1 shadow-lg',
118
+ )}
119
+ >
120
+ {groups
121
+ ? groups.map((group) => (
122
+ <BaseSelect.Group key={group.label}>
123
+ <BaseSelect.GroupLabel className="px-3 py-1 text-[12px] font-semibold text-(--text-muted) uppercase tracking-wider">
124
+ {group.label}
125
+ </BaseSelect.GroupLabel>
126
+ {group.options.map((opt) => (
127
+ <SelectItem key={opt.value} value={opt.value} disabled={opt.disabled}>
128
+ {opt.label}
129
+ </SelectItem>
130
+ ))}
131
+ </BaseSelect.Group>
132
+ ))
133
+ : options?.map((opt) => (
134
+ <SelectItem key={opt.value} value={opt.value} disabled={opt.disabled}>
135
+ {opt.label}
136
+ </SelectItem>
137
+ ))}
138
+ </BaseSelect.Popup>
139
+
140
+ <BaseSelect.ScrollDownArrow className="flex h-5 w-full cursor-default items-center justify-center rounded-b-md bg-(--bg-primary) text-(--text-muted)">
141
+ <ChevronDown size={12} />
142
+ </BaseSelect.ScrollDownArrow>
143
+ </BaseSelect.Positioner>
144
+ </BaseSelect.Portal>
145
+ </BaseSelect.Root>
146
+
147
+ {error && (
148
+ <span className="flex items-center gap-1 text-[12px] font-medium leading-tight text-(--danger)">
149
+ ⚠️ {error}
150
+ </span>
151
+ )}
152
+ </div>
153
+ )
154
+ },
155
+ )
156
+
157
+ Select.displayName = 'Select'
158
+
159
+ // Internal SelectItem helper
160
+ function SelectItem({
161
+ value,
162
+ disabled,
163
+ children,
164
+ }: {
165
+ value: string
166
+ disabled?: boolean
167
+ children: React.ReactNode
168
+ }) {
169
+ return (
170
+ <BaseSelect.Item value={value} disabled={disabled} className={itemClass}>
171
+ <BaseSelect.ItemIndicator className="absolute right-3 flex items-center text-(--accent)">
172
+ <Check size={14} />
173
+ </BaseSelect.ItemIndicator>
174
+ <BaseSelect.ItemText>{children}</BaseSelect.ItemText>
175
+ </BaseSelect.Item>
176
+ )
177
+ }
@@ -0,0 +1,116 @@
1
+ import React from 'react'
2
+ import { Switch as BaseSwitch } from '@base-ui/react'
3
+ import clsx from 'clsx'
4
+
5
+ export type SwitchSize = 'sm' | 'md' | 'lg'
6
+
7
+ export interface SwitchProps {
8
+ checked?: boolean
9
+ defaultChecked?: boolean
10
+ onCheckedChange?: (checked: boolean) => void
11
+ disabled?: boolean
12
+ required?: boolean
13
+ size?: SwitchSize
14
+ /** Label text shown next to the switch */
15
+ label?: string
16
+ /** Helper text shown below the label */
17
+ description?: string
18
+ name?: string
19
+ value?: string
20
+ id?: string
21
+ className?: string
22
+ }
23
+
24
+ const rootSizes: Record<SwitchSize, string> = {
25
+ sm: 'h-4 w-7',
26
+ md: 'h-5 w-9',
27
+ lg: 'h-6 w-11',
28
+ }
29
+
30
+ const thumbSizes: Record<SwitchSize, string> = {
31
+ sm: 'h-3 w-3 data-[checked]:translate-x-3',
32
+ md: 'h-3.5 w-3.5 data-[checked]:translate-x-4',
33
+ lg: 'h-4.5 w-4.5 data-[checked]:translate-x-5',
34
+ }
35
+
36
+ export const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>(
37
+ (
38
+ {
39
+ checked,
40
+ defaultChecked,
41
+ onCheckedChange,
42
+ disabled,
43
+ required,
44
+ size = 'md',
45
+ label,
46
+ description,
47
+ name,
48
+ value,
49
+ id,
50
+ className,
51
+ },
52
+ ref,
53
+ ) => {
54
+ const generatedId = React.useId()
55
+ const switchId = id ?? generatedId
56
+
57
+ const switchEl = (
58
+ <BaseSwitch.Root
59
+ ref={ref}
60
+ id={switchId}
61
+ checked={checked}
62
+ defaultChecked={defaultChecked}
63
+ onCheckedChange={onCheckedChange}
64
+ disabled={disabled}
65
+ required={required}
66
+ name={name}
67
+ value={value}
68
+ className={clsx(
69
+ 'relative inline-flex shrink-0 cursor-pointer items-center rounded-full p-0.5',
70
+ 'border-2 border-transparent outline-none',
71
+ 'transition-colors duration-200',
72
+ 'bg-(--border-gray)',
73
+ 'data-[checked]:bg-(--accent)',
74
+ 'focus-visible:ring-2 focus-visible:ring-(--focus-ring) focus-visible:ring-offset-1',
75
+ 'disabled:cursor-not-allowed disabled:opacity-50',
76
+ rootSizes[size],
77
+ className,
78
+ )}
79
+ >
80
+ <BaseSwitch.Thumb
81
+ className={clsx(
82
+ 'block rounded-full bg-white shadow-sm',
83
+ 'transition-transform duration-200',
84
+ 'translate-x-0',
85
+ thumbSizes[size],
86
+ )}
87
+ />
88
+ </BaseSwitch.Root>
89
+ )
90
+
91
+ if (!label) return switchEl
92
+
93
+ return (
94
+ <div className="flex items-start gap-3">
95
+ {switchEl}
96
+ <div className="flex flex-col">
97
+ <label
98
+ htmlFor={switchId}
99
+ className={clsx(
100
+ 'text-[14px] leading-none text-(--text-primary) select-none',
101
+ disabled && 'opacity-50 cursor-not-allowed',
102
+ )}
103
+ >
104
+ {label}
105
+ {required && <span className="ml-0.5 text-(--danger)">*</span>}
106
+ </label>
107
+ {description && (
108
+ <span className="mt-0.5 text-[12px] text-(--text-muted)">{description}</span>
109
+ )}
110
+ </div>
111
+ </div>
112
+ )
113
+ },
114
+ )
115
+
116
+ Switch.displayName = 'Switch'
@@ -0,0 +1,128 @@
1
+ import React from 'react'
2
+ import { Tabs as BaseTabs } from '@base-ui/react'
3
+ import clsx from 'clsx'
4
+
5
+ // ── Tabs Root ─────────────────────────────────────────────────────────────────
6
+
7
+ export interface TabsProps {
8
+ children: React.ReactNode
9
+ value?: string | number
10
+ defaultValue?: string | number
11
+ onValueChange?: (value: string | number) => void
12
+ orientation?: 'horizontal' | 'vertical'
13
+ className?: string
14
+ }
15
+
16
+ export const Tabs = React.forwardRef<HTMLDivElement, TabsProps>(
17
+ ({ children, value, defaultValue, onValueChange, orientation = 'horizontal', className }, ref) => {
18
+ return (
19
+ <BaseTabs.Root
20
+ ref={ref}
21
+ value={value}
22
+ defaultValue={defaultValue}
23
+ onValueChange={onValueChange}
24
+ orientation={orientation}
25
+ className={clsx(
26
+ 'flex',
27
+ orientation === 'horizontal' ? 'flex-col' : 'flex-row gap-4',
28
+ className,
29
+ )}
30
+ >
31
+ {children}
32
+ </BaseTabs.Root>
33
+ )
34
+ },
35
+ )
36
+
37
+ Tabs.displayName = 'Tabs'
38
+
39
+ // ── TabList ───────────────────────────────────────────────────────────────────
40
+
41
+ export interface TabListProps {
42
+ children: React.ReactNode
43
+ className?: string
44
+ }
45
+
46
+ export const TabList = React.forwardRef<HTMLDivElement, TabListProps>(
47
+ ({ children, className }, ref) => {
48
+ return (
49
+ <BaseTabs.List
50
+ ref={ref}
51
+ className={clsx(
52
+ 'relative flex border-b border-(--border-light)',
53
+ className,
54
+ )}
55
+ >
56
+ {children}
57
+ <BaseTabs.Indicator
58
+ className={clsx(
59
+ 'absolute bottom-0 left-0 h-0.5 bg-(--accent)',
60
+ 'transition-[left,width] duration-200 ease-out',
61
+ )}
62
+ />
63
+ </BaseTabs.List>
64
+ )
65
+ },
66
+ )
67
+
68
+ TabList.displayName = 'TabList'
69
+
70
+ // ── Tab ───────────────────────────────────────────────────────────────────────
71
+
72
+ export interface TabProps {
73
+ children: React.ReactNode
74
+ value: string | number
75
+ disabled?: boolean
76
+ className?: string
77
+ }
78
+
79
+ export const Tab = React.forwardRef<HTMLButtonElement, TabProps>(
80
+ ({ children, value, disabled, className }, ref) => {
81
+ return (
82
+ <BaseTabs.Tab
83
+ ref={ref}
84
+ value={value}
85
+ disabled={disabled}
86
+ className={clsx(
87
+ 'relative px-4 py-2.5 text-[14px] font-medium outline-none',
88
+ 'text-(--text-secondary) transition-colors',
89
+ 'hover:text-(--text-primary)',
90
+ 'data-[selected]:text-(--accent)',
91
+ 'focus-visible:ring-2 focus-visible:ring-(--focus-ring) focus-visible:ring-inset',
92
+ 'disabled:cursor-not-allowed disabled:opacity-50',
93
+ className,
94
+ )}
95
+ >
96
+ {children}
97
+ </BaseTabs.Tab>
98
+ )
99
+ },
100
+ )
101
+
102
+ Tab.displayName = 'Tab'
103
+
104
+ // ── TabPanel ──────────────────────────────────────────────────────────────────
105
+
106
+ export interface TabPanelProps {
107
+ children: React.ReactNode
108
+ value: string | number
109
+ keepMounted?: boolean
110
+ className?: string
111
+ }
112
+
113
+ export const TabPanel = React.forwardRef<HTMLDivElement, TabPanelProps>(
114
+ ({ children, value, keepMounted = false, className }, ref) => {
115
+ return (
116
+ <BaseTabs.Panel
117
+ ref={ref}
118
+ value={value}
119
+ keepMounted={keepMounted}
120
+ className={clsx('pt-4 outline-none', className)}
121
+ >
122
+ {children}
123
+ </BaseTabs.Panel>
124
+ )
125
+ },
126
+ )
127
+
128
+ TabPanel.displayName = 'TabPanel'
@@ -0,0 +1,149 @@
1
+ import React from 'react'
2
+ import { Toast as BaseToast } from '@base-ui/react'
3
+ import clsx from 'clsx'
4
+ import { X, Info, CheckCircle, AlertTriangle, XCircle } from 'lucide-react'
5
+
6
+ // ── Types ─────────────────────────────────────────────────────────────────────
7
+
8
+ export type ToastType = 'default' | 'info' | 'success' | 'warning' | 'error'
9
+
10
+ // ── ToastProvider ─────────────────────────────────────────────────────────────
11
+
12
+ export interface ToastProviderProps {
13
+ children: React.ReactNode
14
+ timeout?: number
15
+ limit?: number
16
+ }
17
+
18
+ /**
19
+ * Wrap your app (or part of it) with `ToastProvider` to enable toasts.
20
+ * Then use the `useToast()` hook inside to add toasts.
21
+ */
22
+ export const ToastProvider: React.FC<ToastProviderProps> = ({
23
+ children,
24
+ timeout = 5000,
25
+ limit = 5,
26
+ }) => {
27
+ return (
28
+ <BaseToast.Provider timeout={timeout} limit={limit}>
29
+ {children}
30
+ <ToastViewport />
31
+ </BaseToast.Provider>
32
+ )
33
+ }
34
+
35
+ // ── useToast hook ─────────────────────────────────────────────────────────────
36
+
37
+ export interface ToastOptions {
38
+ title?: React.ReactNode
39
+ description?: React.ReactNode
40
+ type?: ToastType
41
+ timeout?: number
42
+ /** Action button rendered inside the toast */
43
+ action?: {
44
+ label: string
45
+ onClick: () => void
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Hook to imperatively add, close and update toasts.
51
+ *
52
+ * Must be used inside `<ToastProvider>`.
53
+ */
54
+ export function useToast() {
55
+ const manager = BaseToast.useToastManager()
56
+ return {
57
+ toast: (options: ToastOptions) =>
58
+ manager.add({
59
+ title: options.title,
60
+ description: options.description,
61
+ type: options.type ?? 'default',
62
+ timeout: options.timeout,
63
+ actionProps: options.action
64
+ ? { children: options.action.label, onClick: options.action.onClick }
65
+ : undefined,
66
+ }),
67
+ dismiss: manager.close,
68
+ }
69
+ }
70
+
71
+ // ── ToastViewport (internal) ──────────────────────────────────────────────────
72
+
73
+ function ToastViewport() {
74
+ const { toasts } = BaseToast.useToastManager()
75
+
76
+ return (
77
+ <BaseToast.Viewport className="fixed bottom-4 right-4 z-[100] flex w-80 flex-col-reverse gap-2 outline-none">
78
+ {toasts.map((toast) => (
79
+ <ToastItem key={toast.id} toast={toast} />
80
+ ))}
81
+ </BaseToast.Viewport>
82
+ )
83
+ }
84
+
85
+ // ── ToastItem (internal) ──────────────────────────────────────────────────────
86
+
87
+ const typeStyles: Record<ToastType, { icon: React.ReactNode; color: string }> = {
88
+ default: { icon: null, color: 'border-(--border-light)' },
89
+ info: {
90
+ icon: <Info size={16} className="shrink-0 text-blue-500" />,
91
+ color: 'border-blue-200 dark:border-blue-900',
92
+ },
93
+ success: {
94
+ icon: <CheckCircle size={16} className="shrink-0 text-(--slack-green)" />,
95
+ color: 'border-green-200 dark:border-green-900',
96
+ },
97
+ warning: {
98
+ icon: <AlertTriangle size={16} className="shrink-0 text-amber-500" />,
99
+ color: 'border-amber-200 dark:border-amber-900',
100
+ },
101
+ error: {
102
+ icon: <XCircle size={16} className="shrink-0 text-(--danger)" />,
103
+ color: 'border-red-200 dark:border-red-900',
104
+ },
105
+ }
106
+
107
+ function ToastItem({ toast }: { toast: BaseToast.Root.ToastObject }) {
108
+ const type = (toast.type ?? 'default') as ToastType
109
+ const style = typeStyles[type] ?? typeStyles.default
110
+
111
+ return (
112
+ <BaseToast.Root
113
+ toast={toast}
114
+ className={clsx(
115
+ 'flex w-full items-start gap-3 rounded-lg border bg-(--bg-primary) px-4 py-3 shadow-lg',
116
+ style.color,
117
+ 'data-[ending]:animate-[fade-out_150ms_ease-in] data-[starting]:animate-[zoom-in_150ms_ease-out]',
118
+ )}
119
+ >
120
+ {style.icon && <div className="pt-0.5">{style.icon}</div>}
121
+
122
+ <div className="flex flex-1 flex-col gap-0.5 min-w-0">
123
+ {toast.title && (
124
+ <BaseToast.Title className="text-[14px] font-semibold text-(--text-primary) leading-snug">
125
+ {toast.title}
126
+ </BaseToast.Title>
127
+ )}
128
+ {toast.description && (
129
+ <BaseToast.Description className="text-[13px] text-(--text-secondary) leading-snug">
130
+ {toast.description}
131
+ </BaseToast.Description>
132
+ )}
133
+ {toast.actionProps && (
134
+ <button
135
+ {...toast.actionProps}
136
+ className={clsx(
137
+ 'mt-1.5 self-start text-[13px] font-semibold text-(--accent) hover:underline outline-none',
138
+ 'focus-visible:ring-1 focus-visible:ring-(--focus-ring) rounded',
139
+ )}
140
+ />
141
+ )}
142
+ </div>
143
+
144
+ <BaseToast.Close className="shrink-0 rounded p-0.5 text-(--text-muted) hover:text-(--text-primary) hover:bg-(--bg-hover) outline-none focus-visible:ring-1 focus-visible:ring-(--focus-ring) transition-colors">
145
+ <X size={14} />
146
+ </BaseToast.Close>
147
+ </BaseToast.Root>
148
+ )
149
+ }