Skip to content

Commit 0be9303

Browse files
authored
improvement(toast): match notification styling with countdown ring and consistent design (#3688)
* improvement(toast): match notification styling with countdown ring and consistent design * fix(toast): add success variant indicator dot
1 parent fa181f0 commit 0be9303

File tree

1 file changed

+63
-34
lines changed
  • apps/sim/components/emcn/components/toast

1 file changed

+63
-34
lines changed

apps/sim/components/emcn/components/toast/toast.tsx

Lines changed: 63 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ const AUTO_DISMISS_MS = 0
1818
const EXIT_ANIMATION_MS = 200
1919
const MAX_VISIBLE = 20
2020

21+
const RING_RADIUS = 5.5
22+
const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS
23+
2124
type ToastVariant = 'default' | 'success' | 'error'
2225

2326
interface ToastAction {
@@ -28,15 +31,13 @@ interface ToastAction {
2831
interface ToastData {
2932
id: string
3033
message: string
31-
description?: string
3234
variant: ToastVariant
3335
action?: ToastAction
3436
duration: number
3537
}
3638

3739
type ToastInput = {
3840
message: string
39-
description?: string
4041
variant?: ToastVariant
4142
action?: ToastAction
4243
duration?: number
@@ -90,12 +91,31 @@ export function useToast() {
9091
return ctx
9192
}
9293

93-
const VARIANT_STYLES: Record<ToastVariant, string> = {
94-
default: 'border-[var(--border)] bg-[var(--bg)] text-[var(--text-primary)]',
95-
success:
96-
'border-emerald-200 bg-emerald-50 text-emerald-900 dark:border-emerald-800/40 dark:bg-emerald-950/30 dark:text-emerald-200',
97-
error:
98-
'border-red-200 bg-red-50 text-red-900 dark:border-red-800/40 dark:bg-red-950/30 dark:text-red-200',
94+
function CountdownRing({ duration }: { duration: number }) {
95+
return (
96+
<svg
97+
width='14'
98+
height='14'
99+
viewBox='0 0 16 16'
100+
fill='none'
101+
xmlns='http://www.w3.org/2000/svg'
102+
style={{ transform: 'rotate(-90deg) scaleX(-1)' }}
103+
>
104+
<circle cx='8' cy='8' r={RING_RADIUS} stroke='var(--border)' strokeWidth='1.5' />
105+
<circle
106+
cx='8'
107+
cy='8'
108+
r={RING_RADIUS}
109+
stroke='var(--text-icon)'
110+
strokeWidth='1.5'
111+
strokeLinecap='round'
112+
strokeDasharray={RING_CIRCUMFERENCE}
113+
style={{
114+
animation: `notification-countdown ${duration}ms linear forwards`,
115+
}}
116+
/>
117+
</svg>
118+
)
99119
}
100120

101121
function ToastItem({ toast: t, onDismiss }: { toast: ToastData; onDismiss: (id: string) => void }) {
@@ -117,38 +137,48 @@ function ToastItem({ toast: t, onDismiss }: { toast: ToastData; onDismiss: (id:
117137
return (
118138
<div
119139
className={cn(
120-
'pointer-events-auto flex w-[320px] items-start gap-[8px] rounded-[8px] border px-[12px] py-[10px] shadow-md transition-all',
121-
VARIANT_STYLES[t.variant],
140+
'pointer-events-auto w-[240px] overflow-hidden rounded-[8px] border border-[var(--border)] bg-[var(--bg)] shadow-sm',
122141
exiting
123142
? 'animate-[toast-exit_200ms_ease-in_forwards]'
124143
: 'animate-[toast-enter_200ms_ease-out_forwards]'
125144
)}
126145
>
127-
<div className='min-w-0 flex-1'>
128-
<p className='font-medium text-[13px] leading-[18px]'>{t.message}</p>
129-
{t.description && (
130-
<p className='mt-[2px] text-[12px] leading-[16px] opacity-80'>{t.description}</p>
146+
<div className='flex flex-col gap-[8px] p-[8px]'>
147+
<div className='flex items-start gap-[8px]'>
148+
<div className='line-clamp-2 min-w-0 flex-1 font-medium text-[12px] text-[var(--text-body)]'>
149+
{t.variant === 'error' && (
150+
<span className='mr-[8px] mb-[2px] inline-block h-[8px] w-[8px] rounded-[2px] bg-[var(--text-error)] align-middle' />
151+
)}
152+
{t.variant === 'success' && (
153+
<span className='mr-[8px] mb-[2px] inline-block h-[8px] w-[8px] rounded-[2px] bg-[var(--text-success)] align-middle' />
154+
)}
155+
{t.message}
156+
</div>
157+
<div className='flex shrink-0 items-start gap-[2px]'>
158+
{t.duration > 0 && <CountdownRing duration={t.duration} />}
159+
<button
160+
type='button'
161+
onClick={dismiss}
162+
aria-label='Dismiss notification'
163+
className='-m-[2px] shrink-0 rounded-[5px] p-[4px] hover:bg-[var(--surface-active)]'
164+
>
165+
<X className='h-[14px] w-[14px] text-[var(--text-icon)]' />
166+
</button>
167+
</div>
168+
</div>
169+
{t.action && (
170+
<button
171+
type='button'
172+
onClick={() => {
173+
t.action!.onClick()
174+
dismiss()
175+
}}
176+
className='w-full rounded-[5px] bg-[var(--surface-active)] px-[8px] py-[4px] font-medium text-[12px] hover:bg-[var(--surface-hover)]'
177+
>
178+
{t.action.label}
179+
</button>
131180
)}
132181
</div>
133-
{t.action && (
134-
<button
135-
type='button'
136-
onClick={() => {
137-
t.action!.onClick()
138-
dismiss()
139-
}}
140-
className='shrink-0 font-medium text-[13px] underline underline-offset-2 opacity-90 hover:opacity-100'
141-
>
142-
{t.action.label}
143-
</button>
144-
)}
145-
<button
146-
type='button'
147-
onClick={dismiss}
148-
className='shrink-0 rounded-[4px] p-[2px] opacity-60 hover:opacity-100'
149-
>
150-
<X className='h-[14px] w-[14px]' />
151-
</button>
152182
</div>
153183
)
154184
}
@@ -175,7 +205,6 @@ export function ToastProvider({ children }: { children?: ReactNode }) {
175205
const data: ToastData = {
176206
id,
177207
message: input.message,
178-
description: input.description,
179208
variant: input.variant ?? 'default',
180209
action: input.action,
181210
duration: input.duration ?? AUTO_DISMISS_MS,

0 commit comments

Comments
 (0)