fix: Prettier and less invasive toasts (#7758)

* Toast update

* ToastType

* nit adjustements

* nit smaller toast
This commit is contained in:
Diego Imbert
2026-02-03 19:18:21 +01:00
committed by GitHub
parent 4901e3cbf0
commit 64f970e976
5 changed files with 112 additions and 76 deletions

View File

@@ -36,21 +36,23 @@
})
}
}
export type ToastType = AlertType
</script>
<script lang="ts">
import { toast } from '@zerodevx/svelte-toast'
import { CheckCircle2, XCircleIcon } from 'lucide-svelte'
import Button from './common/button/Button.svelte'
import { type ToastAction } from '$lib/toast'
import { processMessage } from './toast'
import { onDestroy, untrack } from 'svelte'
import { twMerge } from 'tailwind-merge'
import { classes, icons, type AlertType } from '$lib/components/common/alert/model'
interface Props {
message: string
toastId: string
error?: boolean
type?: ToastType
actions?: ToastAction[]
errorMessage?: string | undefined
duration?: number
@@ -59,7 +61,7 @@
let {
message,
toastId,
error = false,
type = 'success',
actions = [],
errorMessage = undefined,
duration = 5000
@@ -75,81 +77,103 @@
onDestroy(() => {
delete toastStates[toastId]
})
let state = $derived.by(() => toastStates[toastId] as ToastState | undefined)
let _state = $derived.by(() => toastStates[toastId] as ToastState | undefined)
$effect(() => {
if (!state) {
if (!_state) {
toast.pop(toastId)
}
})
let color = error
? { text: 'text-red-400', bg: 'bg-red-400' }
: { text: 'text-green-400', bg: 'bg-green-300' }
let color = classes[type]
let hover = $derived(Object.values(toastStates).some((state) => state.hover))
let containerClass = {
success: 'toast-success',
error: 'toast-error',
info: 'toast-info',
warning: 'toast-warning'
}[type]
let Icon = $derived(icons[type])
let showMore = $state(false)
const MAX_MSG_LEN = 160
let isLongMessage = $derived(message.length > MAX_MSG_LEN)
let displayMessage = $derived(
isLongMessage && !showMore ? message.slice(0, MAX_MSG_LEN) + '... ' : message
)
// let hover = $derived(Object.values(toastStates).some((state) => state.hover))
</script>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class={twMerge(
'pointer-events-auto w-full max-w-sm overflow-hidden bg-surface-tertiary drop-shadow-base shadow-lg ring-1 ring-black ring-opacity-5 border rounded-md',
error ? 'toast-error' : 'toast-success'
'pointer-events-auto w-full overflow-hidden rounded-md relative flex items-center bg-surface',
containerClass
)}
onmouseenter={() => state && (state.hover = true)}
onmouseleave={() => state && (state.hover = false)}
onmouseenter={() => _state && (_state.hover = true)}
onmouseleave={() => _state && (_state.hover = false)}
>
<div class="p-2 min-h-[60px] flex flex-col">
<div class="flex items-start w-full">
<div class="flex-shrink-0 mt-0.5">
{#if error}
<XCircleIcon class="h-4 w-4 {color.text}" />
{:else}
<CheckCircle2 class="h-4 w-4 {color.text}" />
{/if}
</div>
<div class="ml-3 flex-1 w-0">
<p class="text-sm text-primary break-words">{@html processMessage(message)}</p>
{#if errorMessage}
<p class="text-xs {color.text} w-full overflow-auto mt-2">
{errorMessage}
</p>
{/if}
</div>
<div class="ml-4 flex flex-shrink-0">
<button
type="button"
onclick={handleClose}
class="inline-flex rounded-md text-gray-400 hover:text-primary focus:outline-none"
>
<span class="sr-only">Close</span>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
<div
class="flex items-center h-full w-full min-h-10 rounded-md px-2 py-1 {color.descriptionClass} {color.bgClass}"
>
<div class="flex-shrink-0 mt-0.5">
<Icon class="h-4 w-4 {color.iconClass}" />
</div>
<div class="mt-2 flex flex-col gap-2 w-full items-center">
<div class="ml-3 flex-1 w-0">
<p class="text-xs break-words">
{@html processMessage(displayMessage)}
{#if isLongMessage && !showMore}
<button
type="button"
class="ml-1 {color.descriptionClass} font-medium hover:underline focus:outline-none"
onclick={() => (showMore = true)}
>
Show more
</button>
{/if}
</p>
{#if errorMessage}
<p class="text-xs {color.descriptionClass} w-full overflow-auto mt-2">
{errorMessage}
</p>
{/if}
</div>
<div class="flex justify-center ml-2">
{#each actions as action, index (index)}
<Button
variant={action.buttonType ?? 'default'}
variant={action.buttonType ?? 'subtle'}
unifiedSize="sm"
onClick={() => {
action.callback()
toast.pop(toastId)
}}
wrapperClasses="w-full"
btnClasses="{color.descriptionClass} font-medium"
>
{action.label}
</Button>
{/each}
</div>
<div class="ml-4 flex flex-shrink-0">
<button
type="button"
onclick={handleClose}
class="inline-flex rounded-md hover:text-primary focus:outline-none"
>
<span class="sr-only">Close</span>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
</div>
<!-- Duration indicator -->
<div
class="h-0.5 transition-colors {hover ? 'bg-gray-300' : color.bg}"
style="width: {Math.max(0, 1 - (state?.elapsed ?? duration) / duration) * 100}%"
class="h-[1px] absolute bottom-0 transition-colors bg-current {color.iconClass} opacity-60"
style="width: {Math.max(0, 1 - (_state?.elapsed ?? duration) / duration) * 100}%"
>
</div>
</div>

View File

@@ -1,14 +1,7 @@
<script lang="ts">
import { type AlertType, classes } from './model'
import { type AlertType, classes, icons } from './model'
import Tooltip from '$lib/components/Tooltip.svelte'
import {
AlertCircle,
AlertTriangle,
CheckCircle2,
Info,
ChevronDown,
ChevronUp
} from 'lucide-svelte'
import { ChevronDown, ChevronUp } from 'lucide-svelte'
import { slide } from 'svelte/transition'
import { twMerge } from 'tailwind-merge'
@@ -54,13 +47,6 @@
children
}: Props = $props()
const icons: Record<AlertType, any> = {
info: Info,
warning: AlertCircle,
error: AlertTriangle,
success: CheckCircle2
}
function toggleCollapse() {
if (collapsible) {
isCollapsed = !isCollapsed

View File

@@ -1,3 +1,5 @@
import { AlertCircle, AlertTriangle, CheckCircle2, Info } from 'lucide-svelte'
export type AlertType = 'success' | 'error' | 'warning' | 'info'
export const classes: Record<AlertType, Record<string, string>> = {
@@ -27,3 +29,10 @@ export const classes: Record<AlertType, Record<string, string>> = {
descriptionClass: 'text-green-700 dark:text-green-100/90'
}
}
export const icons: Record<AlertType, any> = {
info: Info,
warning: AlertCircle,
error: AlertTriangle,
success: CheckCircle2
}

View File

@@ -1,4 +1,4 @@
import Toast from '$lib/components/Toast.svelte'
import Toast, { type ToastType } from '$lib/components/Toast.svelte'
import { toast } from '@zerodevx/svelte-toast'
import type { ComponentProps } from 'svelte'
import type { Button } from './components/common'
@@ -11,15 +11,18 @@ export type ToastAction = {
export function sendUserToast(
message: string,
error: boolean = false,
_type: boolean | ToastType = 'success',
actions: ToastAction[] = [],
errorMessage: string | undefined = undefined,
duration: number = 5000
): void {
const type = typeof _type === 'boolean' ? (_type ? 'error' : 'success') : _type
const error = type === 'error'
if (globalThis.windmillToast) {
globalThis.windmillToast({
message,
error,
type,
actions,
errorMessage,
duration
@@ -34,7 +37,7 @@ export function sendUserToast(
src: Toast,
props: {
message,
error,
type,
actions,
errorMessage,
duration
@@ -47,7 +50,10 @@ export function sendUserToast(
'--toastPadding': '0',
'--toastMsgPadding': '0',
'--toastBackground': '#00000000',
'--toastBorderRadius': '0.4rem'
'--toastBorderRadius': '0.4rem',
'--toastWidth': '34rem',
'--toastMinHeight': '1rem',
'--toastBoxShadow': 'none'
}
})
}

View File

@@ -16,8 +16,8 @@
next: 0, // next progress value
pausable: true, // pause progress bar tween on mouse hover
dismissable: true, // allow dismiss with close button
reversed: false, // insert new toast to bottom of stack
intro: { x: 256 }, // toast intro fly animation settings
reversed: true, // insert new toast to bottom of stack
intro: { y: -32 }, // toast intro fly animation settings
theme: {} // css var overrides
}
@@ -43,11 +43,22 @@
<style>
.wrap {
display: contents;
display: flex;
font-family: 'Inter', sans-serif;
font-size: 0.875rem;
width: 100%;
height: 100%;
justify-content: center;
--toastContainerRight: auto;
--toastContainerTop: auto;
--toastContainerBottom: 1rem;
}
.wrap :global(strong) {
font-weight: 600;
.wrap :global(._toastContainer) {
height: 20rem;
display: flex;
flex-direction: column-reverse;
overflow-y: auto;
overflow-x: hidden;
}
</style>