@spaethtech/svelte-ui 0.21.5-dev.147.eb2f000 → 0.21.5-dev.149.8ed1480

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,104 @@
1
+ <script lang="ts">
2
+ /**
3
+ * One toast row: a filled Alert plus (optionally) an action button and a close button. Owns its own
4
+ * auto-dismiss countdown so it can PAUSE while the pointer is over it or keyboard focus is inside it
5
+ * (a `holds` counter handles pointer + focus overlapping) — that keeps the `action` reachable by
6
+ * mouse AND keyboard. `timeout: 0` is sticky (no countdown). Controls are our <Button>, never raw
7
+ * <button>, per the composition standard.
8
+ */
9
+ import { onMount } from "svelte";
10
+ import Alert from "../Alert.svelte";
11
+ import Button from "../Button.svelte";
12
+ import IconClose from "~icons/mdi/close";
13
+ import { uiI18n } from "../../i18n.js";
14
+ import { dismissToast, type ToastItem } from "./toast.svelte.js";
15
+ import type { Variant } from "../../types/variants.js";
16
+ import type { Responsive } from "../../types/responsive.js";
17
+ import type { Size } from "../../types/sizes.js";
18
+
19
+ const i18n = uiI18n();
20
+
21
+ let {
22
+ item,
23
+ fallbackVariant,
24
+ fallbackSize,
25
+ }: {
26
+ item: ToastItem;
27
+ /** Toaster-level defaults for toasts that didn't set their own. */
28
+ fallbackVariant?: Variant;
29
+ fallbackSize?: Responsive<Size>;
30
+ } = $props();
31
+
32
+ // Auto-dismiss countdown with pause/resume. `remaining` ms is decremented by whatever has elapsed
33
+ // each time we pause; while `holds > 0` (hovered and/or focused) no timer runs.
34
+ let remaining = item.timeout;
35
+ let startedAt = 0;
36
+ let handle: ReturnType<typeof setTimeout> | undefined;
37
+ let holds = 0;
38
+
39
+ function clear() {
40
+ if (handle !== undefined) {
41
+ clearTimeout(handle);
42
+ handle = undefined;
43
+ }
44
+ }
45
+ function run() {
46
+ clear();
47
+ if (item.timeout <= 0 || typeof window === "undefined") return;
48
+ startedAt = Date.now();
49
+ handle = window.setTimeout(() => dismissToast(item.id), remaining);
50
+ }
51
+ function hold() {
52
+ if (item.timeout <= 0) return;
53
+ if (handle !== undefined) remaining -= Date.now() - startedAt; // bank elapsed time once
54
+ clear();
55
+ holds++;
56
+ }
57
+ function release() {
58
+ if (item.timeout <= 0) return;
59
+ holds = Math.max(0, holds - 1);
60
+ if (holds > 0) return;
61
+ if (remaining > 0) run();
62
+ else dismissToast(item.id);
63
+ }
64
+
65
+ onMount(() => {
66
+ run();
67
+ return clear;
68
+ });
69
+
70
+ function runAction() {
71
+ // Run the handler, then dismiss — unless the action opted out with `dismiss: false` (keep open).
72
+ item.action?.onclick();
73
+ if (item.action?.dismiss !== false) dismissToast(item.id);
74
+ }
75
+ </script>
76
+
77
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
78
+ <div
79
+ class="pointer-events-auto flex min-w-56 items-start gap-1 rounded shadow-lg [background-color:var(--ui-color-background)]"
80
+ role="status"
81
+ onpointerenter={hold}
82
+ onpointerleave={release}
83
+ onfocusin={hold}
84
+ onfocusout={release}
85
+ >
86
+ <Alert variant={item.variant ?? fallbackVariant} size={item.size ?? fallbackSize} filled class="grow"
87
+ >{item.message}</Alert
88
+ >
89
+ {#if item.action}
90
+ <Button variant="ghost" size="sm" class="shrink-0 self-center" onclick={runAction}>
91
+ {item.action.label}
92
+ </Button>
93
+ {/if}
94
+ <Button
95
+ variant="ghost"
96
+ size="sm"
97
+ class="shrink-0 self-center"
98
+ aria-label={i18n.messages.dismiss}
99
+ title={i18n.messages.dismiss}
100
+ onclick={() => dismissToast(item.id)}
101
+ >
102
+ {#snippet icon()}<IconClose />{/snippet}
103
+ </Button>
104
+ </div>
@@ -0,0 +1,13 @@
1
+ import { type ToastItem } from "./toast.svelte.js";
2
+ import type { Variant } from "../../types/variants.js";
3
+ import type { Responsive } from "../../types/responsive.js";
4
+ import type { Size } from "../../types/sizes.js";
5
+ type $$ComponentProps = {
6
+ item: ToastItem;
7
+ /** Toaster-level defaults for toasts that didn't set their own. */
8
+ fallbackVariant?: Variant;
9
+ fallbackSize?: Responsive<Size>;
10
+ };
11
+ declare const ToastRow: import("svelte").Component<$$ComponentProps, {}, "">;
12
+ type ToastRow = ReturnType<typeof ToastRow>;
13
+ export default ToastRow;
@@ -1,13 +1,11 @@
1
1
  <script lang="ts">
2
- import { uiI18n } from "../../i18n.js";
3
- const i18n = uiI18n();
4
2
  /**
5
3
  * Renders the toast store's items in a fixed, top-layer stack. Mount ONCE at the app root; push with
6
- * `toast.*` from anywhere. Each toast is an Alert (filled) honouring its variant + size.
4
+ * `toast.*` from anywhere. Each toast is a <ToastRow> (filled Alert + optional action + close),
5
+ * which owns its own pausable auto-dismiss countdown.
7
6
  */
8
- import Alert from "../Alert.svelte";
9
- import IconClose from "~icons/mdi/close";
10
- import { getToasts, dismissToast } from "./toast.svelte.js";
7
+ import ToastRow from "./ToastRow.svelte";
8
+ import { getToasts } from "./toast.svelte.js";
11
9
  import type { Size } from "../../types/sizes.js";
12
10
  import type { Responsive } from "../../types/responsive.js";
13
11
  import type { Variant } from "../../types/variants.js";
@@ -35,19 +33,7 @@
35
33
  {#if items.length}
36
34
  <div class="pointer-events-none fixed z-[100] flex max-w-[90vw] flex-col gap-2 {posClass}">
37
35
  {#each items as t (t.id)}
38
- <div
39
- class="pointer-events-auto flex min-w-56 items-start gap-2 rounded shadow-lg [background-color:var(--ui-color-background)]"
40
- >
41
- <Alert variant={t.variant ?? variant} size={t.size ?? size} filled class="grow"
42
- >{t.message}</Alert
43
- >
44
- <button
45
- type="button"
46
- aria-label={i18n.messages.dismiss}
47
- class="shrink-0 self-center pr-2 opacity-60 hover:opacity-100"
48
- onclick={() => dismissToast(t.id)}><IconClose class="size-4" /></button
49
- >
50
- </div>
36
+ <ToastRow item={t} fallbackVariant={variant} fallbackSize={size} />
51
37
  {/each}
52
38
  </div>
53
39
  {/if}
@@ -5,16 +5,31 @@ import type { Size } from "../../types/sizes.js";
5
5
  * shared Variant (status subset) + Size axes. Auto-dismisses after `timeout` ms (0 = sticky).
6
6
  */
7
7
  export type ToastVariant = "danger" | "success" | "warning" | "info" | "primary";
8
+ /**
9
+ * An optional action rendered as a small button inside the toast (e.g. "Undo"). Clicking it runs
10
+ * `onclick`, then the toast is dismissed. Set `dismiss: false` to KEEP the toast open instead (e.g.
11
+ * to show progress) and close it later via the id returned from `toast.*` + `dismissToast`.
12
+ */
13
+ export type ToastAction = {
14
+ /** Button label, e.g. "Undo". */
15
+ label: string;
16
+ /** Click handler (its return value is ignored). */
17
+ onclick: () => void;
18
+ /** Dismiss the toast after `onclick` runs. Default `true`; `false` keeps it open. */
19
+ dismiss?: boolean;
20
+ };
8
21
  export type ToastItem = {
9
22
  id: number;
10
23
  variant: ToastVariant;
11
24
  message: string;
12
25
  size: Size;
13
26
  timeout: number;
27
+ action?: ToastAction;
14
28
  };
15
29
  export type ToastOptions = {
16
30
  timeout?: number;
17
31
  size?: Size;
32
+ action?: ToastAction;
18
33
  };
19
34
  /** The live toast list (read in <Toaster>; mutations are reactive). */
20
35
  export declare function getToasts(): ToastItem[];
@@ -9,12 +9,13 @@ export function dismissToast(id) {
9
9
  if (i >= 0)
10
10
  items.splice(i, 1);
11
11
  }
12
+ // NOTE: auto-dismiss timing lives in <Toaster> (per toast row), NOT here — the row can then PAUSE the
13
+ // countdown while the pointer is over it or keyboard focus is inside it, so an `action` button stays
14
+ // reachable. `push` only records `timeout` (0 = sticky); the row reads it and owns the timer.
12
15
  function push(variant, message, opts = {}) {
13
16
  const id = ++seq;
14
17
  const timeout = opts.timeout ?? 4000;
15
- items.push({ id, variant, message, size: opts.size ?? "md", timeout });
16
- if (timeout > 0 && typeof window !== "undefined")
17
- window.setTimeout(() => dismissToast(id), timeout);
18
+ items.push({ id, variant, message, size: opts.size ?? "md", timeout, action: opts.action });
18
19
  return id;
19
20
  }
20
21
  export const toast = {
package/dist/index.d.ts CHANGED
@@ -15,7 +15,7 @@ export { Stat } from "./components/Stat/index.js";
15
15
  export { Timeline } from "./components/Timeline/index.js";
16
16
  export type { TimelineItem } from "./components/Timeline/index.js";
17
17
  export { default as Toaster } from "./components/Toast/Toaster.svelte";
18
- export { toast, getToasts, dismissToast, type ToastVariant, type ToastItem, } from "./components/Toast/toast.svelte.js";
18
+ export { toast, getToasts, dismissToast, type ToastVariant, type ToastItem, type ToastOptions, type ToastAction, } from "./components/Toast/toast.svelte.js";
19
19
  export { default as Dialog } from "./components/Dialog.svelte";
20
20
  export { Drawer } from "./components/Drawer/index.js";
21
21
  export { default as ConfirmDialog } from "./components/ConfirmDialog.svelte";
package/dist/utils.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { toast, getToasts, dismissToast, type ToastVariant, type ToastItem, } from "./components/Toast/toast.svelte.js";
1
+ export { toast, getToasts, dismissToast, type ToastVariant, type ToastItem, type ToastOptions, type ToastAction, } from "./components/Toast/toast.svelte.js";
2
2
  export { anchored, computePlacement, resolveBoundary } from "./positioning/anchored.js";
3
3
  export type { Side, Align, Boundary, PlaceOptions, Placement, AnchoredParams, } from "./positioning/anchored.js";
4
4
  export { tooltip } from "./positioning/tooltip.js";
package/docs/usage.md CHANGED
@@ -1169,6 +1169,49 @@ reads `GridState.group` (`{ key, dir }`) and does `ORDER BY <group>, <sort>`.
1169
1169
  client-side counterpart to a remote `query()`; `matchesRow(row, ast)` is the underlying AST evaluator if
1170
1170
  you need to filter an array yourself. See the live demo for the full DataTable + Query walkthrough.
1171
1171
 
1172
+ ### Toasts
1173
+
1174
+ Mount a single `<Toaster />` once at the app root, then push from anywhere with `toast.*`. The push
1175
+ functions (`toast`, `dismissToast`, `getToasts`) are component-free values, so import them from the
1176
+ dedicated **`@spaethtech/svelte-ui/toast`** subpath (or `/utils`, or the root barrel) to stay barrel-free.
1177
+
1178
+ ```svelte
1179
+ <script>
1180
+ import { Toaster } from "@spaethtech/svelte-ui/toaster"; // mount once (e.g. +layout.svelte)
1181
+ import { toast } from "@spaethtech/svelte-ui/toast"; // push from anywhere
1182
+ </script>
1183
+
1184
+ <Toaster position="bottom-right" />
1185
+
1186
+ <!-- status variants; each returns an id you can toast.dismiss(id) -->
1187
+ toast.success("Saved");
1188
+ toast.info("Heads up", { timeout: 0 }); // 0 = sticky (dismiss with the × button)
1189
+ toast.warning("Almost full", { size: "lg" });
1190
+ ```
1191
+
1192
+ **Action button (e.g. Undo).** Pass an `action`; it renders a small button that runs `onclick` and then
1193
+ dismisses the toast. The auto-dismiss countdown pauses while the pointer is over the toast or keyboard
1194
+ focus is inside it, so the action stays reachable. Set `dismiss: false` to keep the toast open instead
1195
+ (close it later by id).
1196
+
1197
+ ```svelte
1198
+ let archived = $state(false);
1199
+
1200
+ function archive() {
1201
+ archived = true;
1202
+ toast.info("Archived “Breaking Bad”", {
1203
+ action: { label: "Undo", onclick: () => (archived = false) },
1204
+ });
1205
+ }
1206
+
1207
+ // Keep-open variant (show progress, then dismiss yourself):
1208
+ const id = toast.info("Uploading…", {
1209
+ timeout: 0,
1210
+ action: { label: "Cancel", onclick: cancelUpload, dismiss: false },
1211
+ });
1212
+ // … later: toast.dismiss(id);
1213
+ ```
1214
+
1172
1215
  ## Theme Integration
1173
1216
 
1174
1217
  ### Using CSS Variables
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spaethtech/svelte-ui",
3
- "version": "0.21.5-dev.147.eb2f000",
3
+ "version": "0.21.5-dev.149.8ed1480",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/spaethtech/svelte-ui.git"
@@ -109,6 +109,11 @@
109
109
  "types": "./dist/components/Toast/Toaster.svelte.d.ts",
110
110
  "svelte": "./dist/components/Toast/Toaster.svelte"
111
111
  },
112
+ "./toast": {
113
+ "types": "./dist/components/Toast/toast.svelte.d.ts",
114
+ "svelte": "./dist/components/Toast/toast.svelte.js",
115
+ "default": "./dist/components/Toast/toast.svelte.js"
116
+ },
112
117
  "./dialog": {
113
118
  "types": "./dist/components/Dialog.svelte.d.ts",
114
119
  "svelte": "./dist/components/Dialog.svelte"