Tibi Svelte Starter
diff --git a/frontend/src/lib/actions/portal.ts b/frontend/src/lib/actions/portal.ts
new file mode 100644
index 0000000..67ebd0d
--- /dev/null
+++ b/frontend/src/lib/actions/portal.ts
@@ -0,0 +1,18 @@
+/**
+ * Portal action — teleports an element to document.body.
+ * Usage:
…
+ * SSR-safe: only runs when document is available.
+ */
+export function portal(node: HTMLElement) {
+ if (typeof document === "undefined") return
+
+ document.body.appendChild(node)
+
+ return {
+ destroy() {
+ if (node.parentNode) {
+ node.parentNode.removeChild(node)
+ }
+ },
+ }
+}
diff --git a/frontend/src/lib/toast.ts b/frontend/src/lib/toast.ts
new file mode 100644
index 0000000..35ad367
--- /dev/null
+++ b/frontend/src/lib/toast.ts
@@ -0,0 +1,85 @@
+/**
+ * Toast Notification System
+ * Provides a simple toast notification API with auto-dismiss and responsive positioning.
+ */
+import { writable, derived } from "svelte/store"
+
+export type ToastType = "success" | "error" | "warning" | "info"
+
+export interface Toast {
+ id: string
+ message: string
+ type: ToastType
+ duration: number
+ createdAt: number
+}
+
+interface ToastStore {
+ toasts: Toast[]
+}
+
+const DEFAULT_DURATION = 3000 // 3 seconds
+
+// Create the toast store
+const toastStore = writable
({ toasts: [] })
+
+/**
+ * Generate unique toast ID
+ */
+function generateToastId(): string {
+ return `toast-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
+}
+
+/**
+ * Add a new toast notification
+ */
+export function addToast(message: string, type: ToastType = "info", duration: number = DEFAULT_DURATION): string {
+ const id = generateToastId()
+
+ const toast: Toast = {
+ id,
+ message,
+ type,
+ duration,
+ createdAt: Date.now(),
+ }
+
+ toastStore.update((store) => ({
+ toasts: [...store.toasts, toast],
+ }))
+
+ // Auto-remove after duration
+ if (duration > 0) {
+ setTimeout(() => {
+ removeToast(id)
+ }, duration)
+ }
+
+ return id
+}
+
+/**
+ * Remove a toast by ID
+ */
+export function removeToast(id: string): void {
+ toastStore.update((store) => ({
+ toasts: store.toasts.filter((t) => t.id !== id),
+ }))
+}
+
+/**
+ * Clear all toasts
+ */
+export function clearToasts(): void {
+ toastStore.set({ toasts: [] })
+}
+
+// Export derived readable store for reactive access
+export const toasts = derived(toastStore, ($store) => $store.toasts)
+
+// Export convenience object
+export const toast = {
+ add: addToast,
+ remove: removeToast,
+ clear: clearToasts,
+}
diff --git a/frontend/src/widgets/LoadingBar.svelte b/frontend/src/widgets/LoadingBar.svelte
new file mode 100644
index 0000000..98eb845
--- /dev/null
+++ b/frontend/src/widgets/LoadingBar.svelte
@@ -0,0 +1,63 @@
+
+
+
+
+
diff --git a/frontend/src/widgets/ToastContainer.svelte b/frontend/src/widgets/ToastContainer.svelte
new file mode 100644
index 0000000..5b512ea
--- /dev/null
+++ b/frontend/src/widgets/ToastContainer.svelte
@@ -0,0 +1,145 @@
+
+
+{#if $toasts.length > 0}
+
+
+ {#each $toasts as t (t.id)}
+
+
+
{t.message}
+
+
+ {/each}
+
+
+
+
+ {#each $toasts as t (t.id)}
+
+
+
{t.message}
+
+
+ {/each}
+
+{/if}
+
+