feat: add toast component
This commit is contained in:
@@ -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>
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user