@xyhp915/slack-base-ui 0.0.4 → 0.0.6

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 CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@xyhp915/slack-base-ui",
3
3
  "main": "libs/index.js",
4
4
  "types": "libs/index.d.ts",
5
- "version": "0.0.4",
5
+ "version": "0.0.6",
6
6
  "type": "module",
7
7
  "scripts": {
8
8
  "dev": "vite",
@@ -10,7 +10,7 @@ import React, {
10
10
  import { X } from 'lucide-react'
11
11
  import { Button } from './Button'
12
12
 
13
- export type DialogSize = 'sm' | 'md' | 'lg' | 'xl';
13
+ export type DialogSize = 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl';
14
14
 
15
15
  export interface DialogProps {
16
16
  open?: boolean;
@@ -29,7 +29,7 @@ export const Dialog = ({
29
29
  title,
30
30
  description,
31
31
  children,
32
- size = 'md',
32
+ size = 'xl',
33
33
  showCloseButton = true,
34
34
  className,
35
35
  }: DialogProps) => {
@@ -38,6 +38,9 @@ export const Dialog = ({
38
38
  md: 'max-w-md',
39
39
  lg: 'max-w-lg',
40
40
  xl: 'max-w-xl',
41
+ '2xl': 'max-w-2xl',
42
+ '3xl': 'max-w-3xl',
43
+ '4xl': 'max-w-4xl',
41
44
  }
42
45
 
43
46
  return (
@@ -242,27 +245,27 @@ export const DialogProvider: React.FC<{ children: React.ReactNode }> = ({ childr
242
245
  }, [])
243
246
 
244
247
  const value = useMemo<UseDialogReturn>(
245
- () => ({ show, confirm, alert }),
246
- [show, confirm, alert],
248
+ () => ({ show, confirm, alert }),
249
+ [show, confirm, alert],
247
250
  )
248
251
 
249
252
  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>
253
+ <DialogImperativeContext.Provider value={value}>
254
+ {children}
255
+ {dialogs.map(dialog => (
256
+ <ImperativeDialogItem
257
+ key={dialog.id}
258
+ dialog={dialog}
259
+ onRemove={() => removeDialog(dialog.id)}
260
+ />
261
+ ))}
262
+ </DialogImperativeContext.Provider>
260
263
  )
261
264
  }
262
265
 
263
266
  DialogProvider.displayName = 'DialogProvider'
264
267
 
265
- function ImperativeDialogItem({
268
+ function ImperativeDialogItem ({
266
269
  dialog,
267
270
  onRemove,
268
271
  }: {
@@ -272,13 +275,13 @@ function ImperativeDialogItem({
272
275
  const [open, setOpen] = useState(true)
273
276
 
274
277
  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],
278
+ (result: boolean) => {
279
+ setOpen(false)
280
+ dialog.resolve(result)
281
+ // Allow close animation to finish before unmounting
282
+ setTimeout(onRemove, 300)
283
+ },
284
+ [dialog, onRemove],
282
285
  )
283
286
 
284
287
  const commonProps = {
@@ -291,55 +294,55 @@ function ImperativeDialogItem({
291
294
 
292
295
  if (dialog.type === 'show') {
293
296
  return (
294
- <Dialog
295
- {...commonProps}
296
- showCloseButton={dialog.showCloseButton ?? true}
297
- onOpenChange={o => !o && handleClose(false)}
298
- >
299
- {dialog.content}
300
- </Dialog>
297
+ <Dialog
298
+ {...commonProps}
299
+ showCloseButton={dialog.showCloseButton ?? true}
300
+ onOpenChange={o => !o && handleClose(false)}
301
+ >
302
+ {dialog.content}
303
+ </Dialog>
301
304
  )
302
305
  }
303
306
 
304
307
  if (dialog.type === 'confirm') {
305
308
  return (
309
+ <Dialog
310
+ {...commonProps}
311
+ size={dialog.size ?? 'sm'}
312
+ showCloseButton={false}
313
+ onOpenChange={o => !o && handleClose(false)}
314
+ >
315
+ {dialog.content}
316
+ <DialogFooter>
317
+ <Button variant="secondary" onClick={() => handleClose(false)}>
318
+ {dialog.cancelLabel ?? 'Cancel'}
319
+ </Button>
320
+ <Button
321
+ variant={dialog.confirmVariant ?? 'primary'}
322
+ onClick={() => handleClose(true)}
323
+ >
324
+ {dialog.confirmLabel ?? 'Confirm'}
325
+ </Button>
326
+ </DialogFooter>
327
+ </Dialog>
328
+ )
329
+ }
330
+
331
+ // alert
332
+ return (
306
333
  <Dialog
307
- {...commonProps}
308
- size={dialog.size ?? 'sm'}
309
- showCloseButton={false}
310
- onOpenChange={o => !o && handleClose(false)}
334
+ {...commonProps}
335
+ size={dialog.size ?? 'sm'}
336
+ showCloseButton={false}
337
+ onOpenChange={o => !o && handleClose(true)}
311
338
  >
312
339
  {dialog.content}
313
340
  <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'}
341
+ <Button variant="primary" onClick={() => handleClose(true)}>
342
+ {dialog.confirmLabel ?? 'OK'}
322
343
  </Button>
323
344
  </DialogFooter>
324
345
  </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
346
  )
344
347
  }
345
348
 
@@ -363,7 +366,7 @@ function ImperativeDialogItem({
363
366
  * await alert({ title: 'Error', description: 'Something went wrong.' })
364
367
  * ```
365
368
  */
366
- export function useDialog(): UseDialogReturn {
369
+ export function useDialog (): UseDialogReturn {
367
370
  const ctx = useContext(DialogImperativeContext)
368
371
  if (!ctx) throw new Error('useDialog must be used within <DialogProvider>')
369
372
  return ctx
@@ -1,4 +1,4 @@
1
- import React from 'react'
1
+ import React, { createContext, useContext, useState } from 'react'
2
2
  import { Toast as BaseToast } from '@base-ui/react'
3
3
  import clsx from 'clsx'
4
4
  import { X, Info, CheckCircle, AlertTriangle, XCircle } from 'lucide-react'
@@ -7,27 +7,71 @@ import { X, Info, CheckCircle, AlertTriangle, XCircle } from 'lucide-react'
7
7
 
8
8
  export type ToastType = 'default' | 'info' | 'success' | 'warning' | 'error'
9
9
 
10
+ export type ToastPosition =
11
+ | 'top-left'
12
+ | 'top-center'
13
+ | 'top-right'
14
+ | 'bottom-left'
15
+ | 'bottom-center'
16
+ | 'bottom-right'
17
+
18
+ // ── Position context ──────────────────────────────────────────────────────────
19
+
20
+ interface ToastPositionState {
21
+ position: ToastPosition
22
+ setPosition: (p: ToastPosition) => void
23
+ }
24
+
25
+ const ToastPositionContext = createContext<ToastPositionState>({
26
+ position: 'bottom-right',
27
+ setPosition: () => {},
28
+ })
29
+
30
+ const positionClasses: Record<ToastPosition, string> = {
31
+ 'top-left': 'top-4 left-4 flex-col',
32
+ 'top-center': 'top-4 left-1/2 -translate-x-1/2 flex-col',
33
+ 'top-right': 'top-4 right-4 flex-col',
34
+ 'bottom-left': 'bottom-4 left-4 flex-col-reverse',
35
+ 'bottom-center':'bottom-4 left-1/2 -translate-x-1/2 flex-col-reverse',
36
+ 'bottom-right': 'bottom-4 right-4 flex-col-reverse',
37
+ }
38
+
10
39
  // ── ToastProvider ─────────────────────────────────────────────────────────────
11
40
 
12
41
  export interface ToastProviderProps {
13
42
  children: React.ReactNode
14
43
  timeout?: number
15
44
  limit?: number
45
+ /** Where toasts appear on screen. Defaults to `'bottom-right'`. */
46
+ position?: ToastPosition
16
47
  }
17
48
 
18
49
  /**
19
50
  * Wrap your app (or part of it) with `ToastProvider` to enable toasts.
20
51
  * Then use the `useToast()` hook inside to add toasts.
52
+ *
53
+ * @example
54
+ * // Positon can be set at provider level:
55
+ * <ToastProvider position="top-center">...</ToastProvider>
56
+ *
57
+ * // Or changed at runtime via the hook:
58
+ * const { setPosition } = useToast()
59
+ * setPosition('top-right')
21
60
  */
22
61
  export const ToastProvider: React.FC<ToastProviderProps> = ({
23
62
  children,
24
63
  timeout = 5000,
25
64
  limit = 5,
65
+ position: defaultPosition = 'bottom-right',
26
66
  }) => {
67
+ const [position, setPosition] = useState<ToastPosition>(defaultPosition)
68
+
27
69
  return (
28
70
  <BaseToast.Provider timeout={timeout} limit={limit}>
29
- {children}
30
- <ToastViewport />
71
+ <ToastPositionContext.Provider value={{ position, setPosition }}>
72
+ {children}
73
+ <ToastViewport />
74
+ </ToastPositionContext.Provider>
31
75
  </BaseToast.Provider>
32
76
  )
33
77
  }
@@ -48,11 +92,14 @@ export interface ToastOptions {
48
92
 
49
93
  /**
50
94
  * Hook to imperatively add, close and update toasts.
95
+ * Also exposes `position` / `setPosition` for runtime position changes.
51
96
  *
52
97
  * Must be used inside `<ToastProvider>`.
53
98
  */
54
99
  export function useToast() {
55
100
  const manager = BaseToast.useToastManager()
101
+ const { position, setPosition } = useContext(ToastPositionContext)
102
+
56
103
  return {
57
104
  toast: (options: ToastOptions) =>
58
105
  manager.add({
@@ -65,6 +112,10 @@ export function useToast() {
65
112
  : undefined,
66
113
  }),
67
114
  dismiss: manager.close,
115
+ /** Current toast position */
116
+ position,
117
+ /** Dynamically change where toasts appear */
118
+ setPosition,
68
119
  }
69
120
  }
70
121
 
@@ -72,9 +123,15 @@ export function useToast() {
72
123
 
73
124
  function ToastViewport() {
74
125
  const { toasts } = BaseToast.useToastManager()
126
+ const { position } = useContext(ToastPositionContext)
75
127
 
76
128
  return (
77
- <BaseToast.Viewport className="fixed bottom-4 right-4 z-[100] flex w-80 flex-col-reverse gap-2 outline-none">
129
+ <BaseToast.Viewport
130
+ className={clsx(
131
+ 'fixed z-[100] flex w-80 gap-2 outline-none',
132
+ positionClasses[position],
133
+ )}
134
+ >
78
135
  {toasts.map((toast) => (
79
136
  <ToastItem key={toast.id} toast={toast} />
80
137
  ))}
@@ -84,49 +141,94 @@ function ToastViewport() {
84
141
 
85
142
  // ── ToastItem (internal) ──────────────────────────────────────────────────────
86
143
 
87
- const typeStyles: Record<ToastType, { icon: React.ReactNode; color: string }> = {
88
- default: { icon: null, color: 'border-(--border-light)' },
144
+ /**
145
+ * Per-type config — icon colors adapt to theme via dark: modifier.
146
+ * Accent strip colors stay the same in both modes.
147
+ */
148
+ const typeConfig: Record<
149
+ ToastType,
150
+ {
151
+ icon: React.ReactNode
152
+ titleClass: string
153
+ surfaceClass: string
154
+ actionClass: string
155
+ closeClass: string
156
+ }
157
+ > = {
158
+ default: {
159
+ icon: null,
160
+ titleClass: 'text-(--text-primary) dark:text-white',
161
+ surfaceClass: 'bg-(--bg-primary) border-(--border-light) dark:bg-[#2c2f33] dark:border-transparent',
162
+ actionClass: 'text-(--accent) hover:underline dark:text-white/85 dark:hover:text-white',
163
+ closeClass: 'text-(--text-muted) hover:bg-(--bg-hover) hover:text-(--text-primary) dark:text-white/40 dark:hover:bg-white/10 dark:hover:text-white',
164
+ },
89
165
  info: {
90
- icon: <Info size={16} className="shrink-0 text-blue-500" />,
91
- color: 'border-blue-200 dark:border-blue-900',
166
+ icon: <Info size={14} className="text-[#1164A3] dark:text-[#5ba4cf]" />,
167
+ titleClass: 'text-[#1164A3] dark:text-[#5ba4cf]',
168
+ surfaceClass: 'bg-[#F6FAFE] border-[#E3EFF9] dark:bg-[#102633] dark:border-[#1A4256]',
169
+ actionClass: 'text-[#0F5A94] hover:underline hover:text-[#0B4673] dark:text-[#7AB7DE] dark:hover:text-[#A7D1EC]',
170
+ closeClass: 'text-[#7C9BB4] hover:bg-[#E8F2FB] hover:text-[#1164A3] dark:text-[#8AA7BD] dark:hover:bg-[#173547] dark:hover:text-[#7AB7DE]',
92
171
  },
93
172
  success: {
94
- icon: <CheckCircle size={16} className="shrink-0 text-(--slack-green)" />,
95
- color: 'border-green-200 dark:border-green-900',
173
+ icon: <CheckCircle size={14} className="text-[#007a5a] dark:text-[#39c088]" />,
174
+ titleClass: 'text-[#007a5a] dark:text-[#39c088]',
175
+ surfaceClass: 'bg-[#F4FBF7] border-[#DFF1E8] dark:bg-[#102A20] dark:border-[#1A4B39]',
176
+ actionClass: 'text-[#0A6B51] hover:underline hover:text-[#08533F] dark:text-[#6FD4A3] dark:hover:text-[#96E6BB]',
177
+ closeClass: 'text-[#7A9E91] hover:bg-[#E7F6EE] hover:text-[#007A5A] dark:text-[#89A99E] dark:hover:bg-[#18372C] dark:hover:text-[#6FD4A3]',
96
178
  },
97
179
  warning: {
98
- icon: <AlertTriangle size={16} className="shrink-0 text-amber-500" />,
99
- color: 'border-amber-200 dark:border-amber-900',
180
+ icon: <AlertTriangle size={14} className="text-amber-500 dark:text-amber-400" />,
181
+ titleClass: 'text-amber-500 dark:text-amber-400',
182
+ surfaceClass: 'bg-[#FFFBF0] border-[#F7E9BF] dark:bg-[#3A2A09] dark:border-[#5C4514]',
183
+ actionClass: 'text-[#B26A00] hover:underline hover:text-[#8B5300] dark:text-[#F0C46B] dark:hover:text-[#F5D697]',
184
+ closeClass: 'text-[#AA9B73] hover:bg-[#FFF3D6] hover:text-[#B26A00] dark:text-[#A99668] dark:hover:bg-[#4B3810] dark:hover:text-[#F0C46B]',
100
185
  },
101
186
  error: {
102
- icon: <XCircle size={16} className="shrink-0 text-(--danger)" />,
103
- color: 'border-red-200 dark:border-red-900',
187
+ icon: <XCircle size={14} className="text-[#E01E5A] dark:text-[#e06082]" />,
188
+ titleClass: 'text-[#E01E5A] dark:text-[#e06082]',
189
+ surfaceClass: 'bg-[#FEF6F8] border-[#F8E2E8] dark:bg-[#3A1822] dark:border-[#5E2735]',
190
+ actionClass: 'text-[#C61B50] hover:underline hover:text-[#9E153F] dark:text-[#F08CA8] dark:hover:text-[#F5B0C2]',
191
+ closeClass: 'text-[#B0929C] hover:bg-[#FBE8EE] hover:text-[#E01E5A] dark:text-[#A88E96] dark:hover:bg-[#4A1F2C] dark:hover:text-[#F08CA8]',
104
192
  },
105
193
  }
106
194
 
107
195
  function ToastItem({ toast }: { toast: BaseToast.Root.ToastObject }) {
108
196
  const type = (toast.type ?? 'default') as ToastType
109
- const style = typeStyles[type] ?? typeStyles.default
197
+ const config = typeConfig[type] ?? typeConfig.default
110
198
 
111
199
  return (
112
200
  <BaseToast.Root
113
201
  toast={toast}
202
+ swipeDirection={[]}
114
203
  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]',
204
+ 'group relative flex w-full items-start gap-2.5 overflow-hidden',
205
+ 'rounded-lg pl-3 pr-3 pt-3 pb-3',
206
+ 'border border-(--border-light)',
207
+ 'shadow-[0_2px_16px_rgba(0,0,0,0.08),0_1px_4px_rgba(0,0,0,0.05)]',
208
+ 'dark:shadow-[0_4px_24px_rgba(0,0,0,0.45)]',
209
+ 'data-[starting]:animate-[toast-slide-in_240ms_cubic-bezier(0.16,1,0.3,1)]',
210
+ 'data-[ending]:animate-[toast-slide-out_180ms_ease-in_forwards]',
211
+ config.surfaceClass,
118
212
  )}
119
213
  >
120
- {style.icon && <div className="pt-0.5">{style.icon}</div>}
214
+ {/* ── Icon ─────────────────────────────────────────── */}
215
+ {config.icon && (
216
+ <div className="flex h-5 shrink-0 items-center">{config.icon}</div>
217
+ )}
121
218
 
122
- <div className="flex flex-1 flex-col gap-0.5 min-w-0">
219
+ {/* ── Content ──────────────────────────────────────── */}
220
+ <div className="flex min-w-0 flex-1 flex-col gap-0.5">
123
221
  {toast.title && (
124
- <BaseToast.Title className="text-[14px] font-semibold text-(--text-primary) leading-snug">
222
+ <BaseToast.Title
223
+ className={clsx('text-[14px] font-semibold leading-snug', config.titleClass)}
224
+ >
125
225
  {toast.title}
126
226
  </BaseToast.Title>
127
227
  )}
128
228
  {toast.description && (
129
- <BaseToast.Description className="text-[13px] text-(--text-secondary) leading-snug">
229
+ <BaseToast.Description
230
+ className="text-[13px] leading-snug text-(--text-secondary) dark:text-white/65"
231
+ >
130
232
  {toast.description}
131
233
  </BaseToast.Description>
132
234
  )}
@@ -134,15 +236,23 @@ function ToastItem({ toast }: { toast: BaseToast.Root.ToastObject }) {
134
236
  <button
135
237
  {...toast.actionProps}
136
238
  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',
239
+ 'mt-1.5 self-start rounded text-[13px] font-semibold outline-none transition-colors',
240
+ config.actionClass,
241
+ 'focus-visible:ring-1 focus-visible:ring-(--focus-ring) dark:focus-visible:ring-white/50',
139
242
  )}
140
243
  />
141
244
  )}
142
245
  </div>
143
246
 
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} />
247
+ {/* ── Close button ─────────────────────────────────── */}
248
+ <BaseToast.Close
249
+ className={clsx(
250
+ 'ml-0.5 shrink-0 rounded p-1 outline-none transition-colors',
251
+ config.closeClass,
252
+ 'focus-visible:ring-1 focus-visible:ring-(--focus-ring) dark:focus-visible:ring-white/50',
253
+ )}
254
+ >
255
+ <X size={13} />
146
256
  </BaseToast.Close>
147
257
  </BaseToast.Root>
148
258
  )
@@ -174,7 +174,7 @@ export type { ProgressProps } from './Progress'
174
174
 
175
175
  // Toast Components
176
176
  export { ToastProvider, useToast } from './Toast'
177
- export type { ToastProviderProps, ToastOptions, ToastType } from './Toast'
177
+ export type { ToastProviderProps, ToastOptions, ToastType, ToastPosition } from './Toast'
178
178
 
179
179
  // Loading Component
180
180
  export { Loading } from './Loading'
package/src/index.css CHANGED
@@ -75,6 +75,28 @@
75
75
  }
76
76
  }
77
77
 
78
+ @keyframes toast-slide-in {
79
+ from {
80
+ opacity: 0;
81
+ transform: translateY(10px) scale(0.96);
82
+ }
83
+ to {
84
+ opacity: 1;
85
+ transform: translateY(0) scale(1);
86
+ }
87
+ }
88
+
89
+ @keyframes toast-slide-out {
90
+ from {
91
+ opacity: 1;
92
+ transform: scale(1);
93
+ }
94
+ to {
95
+ opacity: 0;
96
+ transform: scale(0.94);
97
+ }
98
+ }
99
+
78
100
  :root {
79
101
  /* Colors - Slack Brand */
80
102
  --slack-aubergine: #3F0E40;