feat: add toast component

This commit is contained in:
2026-05-07 17:01:33 +02:00
parent 73155dcb46
commit 308626bcdc
4 changed files with 132 additions and 3 deletions
+36
View File
@@ -0,0 +1,36 @@
<script lang="ts">
import { toasts } from './toast.svelte';
const typeClasses: Record<string, string> = {
success: 'border-l-green-500',
error: 'border-l-red-500',
info: 'border-l-active'
};
</script>
<div
class="fixed bottom-4 right-4 flex flex-col gap-2 z-[9999] pointer-events-none"
role="status"
aria-live="polite"
aria-atomic="false"
>
{#each toasts.value as item (item.id)}
<div
class="
bg-layer-2 text-text border border-outline rounded
px-3.5 py-2 text-sm min-w-[180px] max-w-xs
border-l-3 {typeClasses[item.type] ?? 'border-l-outline'}
animate-[slide-in_0.18s_ease]
"
>
{item.message}
</div>
{/each}
</div>
<style>
@keyframes slide-in {
from { opacity: 0; transform: translateX(12px); }
to { opacity: 1; transform: translateX(0); }
}
</style>
+6
View File
@@ -2,14 +2,20 @@ export { default as Input } from './Input.svelte';
export { default as InputCheckbox } from './inputs/InputCheckbox.svelte'; export { default as InputCheckbox } from './inputs/InputCheckbox.svelte';
export { default as InputColor } from './inputs/InputColor.svelte'; export { default as InputColor } from './inputs/InputColor.svelte';
export { default as InputNumber } from './inputs/InputNumber.svelte'; export { default as InputNumber } from './inputs/InputNumber.svelte';
export { default as InputSearch } from './inputs/InputSearch.svelte';
export { default as InputSelect } from './inputs/InputSelect.svelte'; export { default as InputSelect } from './inputs/InputSelect.svelte';
export { default as InputShape } from './inputs/InputShape.svelte'; export { default as InputShape } from './inputs/InputShape.svelte';
export { default as InputVec3 } from './inputs/InputVec3.svelte'; export { default as InputVec3 } from './inputs/InputVec3.svelte';
export { default as SocketTable } from './inputs/SocketTable.svelte'; export { default as SocketTable } from './inputs/SocketTable.svelte';
export { default as Button } from './Button.svelte';
export { default as Details } from './Details.svelte'; export { default as Details } from './Details.svelte';
export { default as JsonViewer } from './JsonViewer.svelte'; export { default as JsonViewer } from './JsonViewer.svelte';
export { default as ShortCut } from './ShortCut.svelte'; export { default as ShortCut } from './ShortCut.svelte';
export { default as Spinner } from './Spinner.svelte';
export { default as Toast } from './Toast.svelte';
export { toast } from './toast.svelte';
export { default as ConfirmDialog } from './ConfirmDialog.svelte';
import Input from './Input.svelte'; import Input from './Input.svelte';
export default Input; export default Input;
+24
View File
@@ -0,0 +1,24 @@
export type ToastType = 'info' | 'success' | 'error';
export type ToastItem = {
id: number;
message: string;
type: ToastType;
};
let _toasts = $state<ToastItem[]>([]);
let _nextId = 0;
export const toasts = {
get value() {
return _toasts;
}
};
export function toast(message: string, type: ToastType = 'info', duration = 3000) {
const id = _nextId++;
_toasts.push({ id, message, type });
setTimeout(() => {
_toasts = _toasts.filter((t) => t.id !== id);
}, duration);
}
+66 -3
View File
@@ -2,15 +2,21 @@
import type { NodeInput } from '@nodarium/types'; import type { NodeInput } from '@nodarium/types';
import '$lib/app.css'; import '$lib/app.css';
import { import {
Button,
ConfirmDialog,
Details, Details,
InputCheckbox, InputCheckbox,
InputColor, InputColor,
InputNumber, InputNumber,
InputSearch,
InputSelect, InputSelect,
InputShape, InputShape,
InputVec3, InputVec3,
JsonViewer, JsonViewer,
ShortCut ShortCut,
Spinner,
Toast,
toast
} from '$lib'; } from '$lib';
import SocketTable from '$lib/inputs/SocketTable.svelte'; import SocketTable from '$lib/inputs/SocketTable.svelte';
import Section from './Section.svelte'; import Section from './Section.svelte';
@@ -68,6 +74,7 @@
let points = $state([]); let points = $state([]);
let theme = $state('dark'); let theme = $state('dark');
let confirmOpen = $state(false);
</script> </script>
<main class="flex flex-col gap-8 py-8"> <main class="flex flex-col gap-8 py-8">
@@ -76,6 +83,17 @@
<ThemeSelector bind:theme /> <ThemeSelector bind:theme />
</div> </div>
<Section title="Button">
<div class="flex flex-wrap gap-3 items-center">
<Button>Default</Button>
<Button variant="primary">Primary</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="ghost">Ghost</Button>
<Button disabled>Disabled</Button>
<Button size="sm">Small</Button>
</div>
</Section>
<Section title="InputNumber"> <Section title="InputNumber">
<Theme /> <Theme />
</Section> </Section>
@@ -95,6 +113,13 @@
<InputVec3 bind:value={vecValue} /> <InputVec3 bind:value={vecValue} />
</Section> </Section>
<Section title="InputSearch" value={options[selectValue]}>
<div class="flex flex-col gap-2">
<p>Searchable select — type to filter</p>
<InputSearch bind:value={selectValue} {options} />
</div>
</Section>
<Section title="Select"> <Section title="Select">
<p> <p>
Select with simple values Select with simple values
@@ -148,12 +173,12 @@
<Section title="JsonViewer"> <Section title="JsonViewer">
{#snippet header()} {#snippet header()}
<button <Button
onclick={() => randomlyUpdateJson()} onclick={() => randomlyUpdateJson()}
class="-mt-1 bg-layer-2 p-1 px-2 rounded-sm cursor-pointer" class="-mt-1 bg-layer-2 p-1 px-2 rounded-sm cursor-pointer"
> >
update update
</button> </Button>
{/snippet} {/snippet}
<div class="w-64 bg-layer-1 p-2 rounded"> <div class="w-64 bg-layer-1 p-2 rounded">
<JsonViewer <JsonViewer
@@ -182,8 +207,46 @@
<ShortCut alt ctrl key="delete" /> <ShortCut alt ctrl key="delete" />
</div> </div>
</Section> </Section>
<Section title="Spinner">
<div class="flex gap-6 items-center">
<Spinner size={16} />
<Spinner size={24} />
<Spinner size={36} />
</div>
</Section>
<Section title="Toast">
<div class="flex gap-3">
<Button onclick={() => toast('Project saved successfully', 'success')}>
Success toast
</Button>
<Button onclick={() => toast('Something went wrong', 'error')}>
Error toast
</Button>
<Button onclick={() => toast('Graph is executing…', 'info')}>
Info toast
</Button>
</div>
</Section>
<Section title="ConfirmDialog">
<Button onclick={() => (confirmOpen = true)}>
Open dialog
</Button>
<ConfirmDialog
bind:open={confirmOpen}
title="Delete project?"
message="This action cannot be undone. The project and all its data will be permanently removed."
confirmLabel="Delete"
cancelLabel="Cancel"
onconfirm={() => toast('Project deleted', 'error')}
/>
</Section>
</main> </main>
<Toast />
<style> <style>
main { main {
max-width: 800px; max-width: 800px;