@marianmeres/stuic 3.176.0 → 3.178.0

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.
@@ -94,7 +94,9 @@
94
94
  <ModalDialog
95
95
  bind:this={modal}
96
96
  preEscapeClose={() => {
97
- return isPending ? false : acp?.current.onEscape?.();
97
+ // `escape()` runs the current dialog's `onEscape` (defaults to `shift`); the
98
+ // handler owns the close. Refused while the OK handler is still pending.
99
+ return isPending ? false : acp?.escape();
98
100
  }}
99
101
  preClose={() => !acp.length}
100
102
  noClickOutsideClose
@@ -78,6 +78,21 @@
78
78
 
79
79
  let current = $derived(acp?.current!);
80
80
 
81
+ // The prompt field is bound to the dialog object itself (`current.value`). Svelte's
82
+ // `bind:value` on the underlying <input> registers an ASYNC "input" listener that
83
+ // re-reads the binding getter after `await tick()` (to respect validation in
84
+ // accessors), and `createOnClick` below dispatches a synthetic "input" event on OK.
85
+ // If the `onOk` worker shifts the stack within that microtask window (e.g.
86
+ // `Promise.resolve().then(() => acp.shift())`), the deferred read finds `current`
87
+ // already undefined and a plain `bind:value={current.value}` throws
88
+ // "Cannot read properties of undefined (reading 'value')" as an unhandled
89
+ // rejection. So the field binds through these null-safe accessors instead: after
90
+ // the dialog is gone the read yields undefined and the write is dropped.
91
+ const getValue = () => current?.value;
92
+ const setValue = (v: any) => {
93
+ if (current) current.value = v;
94
+ };
95
+
81
96
  // Button config is layered: the dialog object's own value (most specific) wins over
82
97
  // the component level prop - same as CmpButtonOk/Cancel/Custom below.
83
98
 
@@ -200,7 +215,7 @@
200
215
  <div class={twMerge("input-box", "mt-3 p-1", classInputBox)}>
201
216
  {#if current?.promptFieldProps?.options?.length}
202
217
  <FieldSelect
203
- bind:value={current.value}
218
+ bind:value={getValue, setValue}
204
219
  bind:input={inputEl}
205
220
  class={twMerge("input", "m-0", classInput)}
206
221
  options={current.promptFieldProps.options}
@@ -210,7 +225,7 @@
210
225
  />
211
226
  {:else}
212
227
  <FieldInput
213
- bind:value={current.value}
228
+ bind:value={getValue, setValue}
214
229
  bind:input={inputEl}
215
230
  class={twMerge("input", "m-0", classInput)}
216
231
  renderSize="sm"
@@ -37,8 +37,38 @@ A modern, customizable replacement for native browser `alert()`, `confirm()`, an
37
37
  - `alert(options)` - Show an alert dialog
38
38
  - `confirm(onOk, options)` - Show a confirm dialog with callback
39
39
  - `prompt(onOk, options)` - Show a prompt dialog with input field
40
- - `shift()` - Remove current dialog from queue
40
+ - `shift()` - Remove current dialog from queue (revealing the next one, if any)
41
+ - `escape()` - Run the current dialog's `onEscape` handler (defaults to `shift`); no-op on an empty queue
41
42
  - `reset()` - Clear all dialogs
43
+ - `dump()` - Snapshot of the queue (current dialog first)
44
+ - `current` / `length` - The dialog being shown / number of queued dialogs (reactive)
45
+
46
+ ### Closing a dialog: the handler owns the close
47
+
48
+ A dialog leaves the queue only through `shift()`. Each of `onOk`, `onCancel` and `onEscape`
49
+ defaults to `shift` when you do not supply it (so a plain `alert()` closes itself), but as
50
+ soon as you pass your own handler, closing is your job: call `acp.shift()` from it when
51
+ done. The stack deliberately does not close for you, so an async handler can validate,
52
+ stage a second step, or keep a failed action's message on screen. The classic mistake is an
53
+ `onOk` that does its work and never shifts: the work happens, the dialog stays, and the user
54
+ clicks OK again.
55
+
56
+ While the promise returned by the handler is pending, the dialog is in a "pending" state
57
+ (buttons disabled, spinner shown, Escape refused). Return the promise rather than `void`-ing
58
+ it to keep that state for the whole of your work:
59
+
60
+ ```ts
61
+ acp.confirm(
62
+ async () => {
63
+ await deleteItem(); // dialog stays up and disabled until this settles
64
+ acp.shift();
65
+ },
66
+ { title: "Delete?", variant: "warn" }
67
+ );
68
+ ```
69
+
70
+ If all you want is a dialog that closes when the user answers, use the Promise-based
71
+ wrappers (`createAlert`, `createConfirm`, `createPrompt`) below; they shift for you.
42
72
 
43
73
  ### Dialog Options
44
74
 
@@ -87,6 +117,8 @@ A modern, customizable replacement for native browser `alert()`, `confirm()`, an
87
117
 
88
118
  ### Confirm
89
119
 
120
+ Supplying `onOk` makes you responsible for closing (see above), hence the `acp.shift()`:
121
+
90
122
  ```svelte
91
123
  <script lang="ts">
92
124
  acp.confirm(
@@ -105,6 +137,9 @@ A modern, customizable replacement for native browser `alert()`, `confirm()`, an
105
137
 
106
138
  ### Prompt
107
139
 
140
+ Same rule: `onOk` receives the entered value and owns the close. The shift may happen at
141
+ any time, synchronously or after an `await`:
142
+
108
143
  ```svelte
109
144
  <script lang="ts">
110
145
  acp.prompt(
@@ -57,6 +57,23 @@ export interface AlertConfirmPromptObj extends Record<string, any> {
57
57
  * Manages a FIFO queue of dialogs, allowing one dialog to be displayed at a time.
58
58
  * Provides a modern, customizable replacement for native browser dialogs.
59
59
  *
60
+ * **The handler owns the close.** A dialog is removed from the queue only by
61
+ * `shift()`. Each of `onOk`, `onCancel` and `onEscape` defaults to `shift` when the
62
+ * caller does not supply it, so an `alert()` with no callbacks closes itself. As soon
63
+ * as you pass your own handler, closing becomes your job: call `acp.shift()` when you
64
+ * are done. The stack does not close for you, so that an async handler can validate,
65
+ * stage a second step, or keep a failed action's message on screen. (An `onOk` that
66
+ * runs its work but never shifts is the classic mistake: the work happens, the dialog
67
+ * stays, and the user clicks OK again.)
68
+ *
69
+ * While a handler's returned promise is pending, the rendered dialog is in a "pending"
70
+ * state (buttons disabled, spinner shown, Escape refused). Return the promise - do not
71
+ * `void` it - to get that for the duration of your work.
72
+ *
73
+ * If all you want is "a dialog that closes when the user answers", use the
74
+ * Promise-based wrappers {@link createAlert}, {@link createConfirm} and
75
+ * {@link createPrompt} instead; they shift for you.
76
+ *
60
77
  * @example
61
78
  * ```ts
62
79
  * const acp = new AlertConfirmPromptStack({
@@ -64,18 +81,24 @@ export interface AlertConfirmPromptObj extends Record<string, any> {
64
81
  * labelCancel: 'Dismiss'
65
82
  * });
66
83
  *
67
- * // Simple alert
84
+ * // Simple alert (no handler given, so OK/Escape default to `shift`)
68
85
  * acp.alert({ title: 'Notice', content: 'Operation complete' });
69
86
  *
70
- * // Confirm with callback
87
+ * // Confirm with callback. Supplying `onOk` makes YOU responsible for closing.
71
88
  * acp.confirm(
72
- * () => console.log('Confirmed!'),
89
+ * async () => {
90
+ * await deleteItem(); // dialog is pending (OK disabled) until this settles
91
+ * acp.shift();
92
+ * },
73
93
  * { title: 'Delete?', content: 'This cannot be undone', variant: 'warn' }
74
94
  * );
75
95
  *
76
- * // Prompt for input
96
+ * // Prompt for input. Same rule: shift when done.
77
97
  * acp.prompt(
78
- * (value) => console.log('User entered:', value),
98
+ * (value) => {
99
+ * console.log('User entered:', value);
100
+ * acp.shift();
101
+ * },
79
102
  * { title: 'Name', content: 'Enter your name', value: 'Default' }
80
103
  * );
81
104
  * ```
@@ -86,20 +109,38 @@ export declare class AlertConfirmPromptStack {
86
109
  constructor(defaults?: Partial<AlertConfirmPromptObj>);
87
110
  get length(): number;
88
111
  get current(): AlertConfirmPromptObj;
112
+ /** Removes the current dialog from the queue, revealing the next one (if any). */
89
113
  shift: () => AlertConfirmPromptObj | undefined;
114
+ /** Clears the whole queue. */
90
115
  reset: () => void;
116
+ /**
117
+ * Runs the current dialog's `onEscape` handler (which defaults to `shift`). Does
118
+ * nothing on an empty stack. The handler owns the close, exactly as with `onOk`
119
+ * and `onCancel`, so this does NOT shift on its own: a custom `onEscape` that
120
+ * does not shift keeps the dialog open, and one that does shift pops exactly one
121
+ * entry (not the one queued behind it as well).
122
+ */
91
123
  escape: () => void;
124
+ /** Snapshot of the queue (current dialog first). */
92
125
  dump: () => AlertConfirmPromptObj[];
93
126
  /**
94
- * Main api.
127
+ * Queues an alert dialog (OK button only). With no `onOk`/`onEscape` given, both
128
+ * default to `shift`, so a plain alert closes itself. If you supply either, call
129
+ * `shift()` from it yourself.
95
130
  */
96
131
  alert: (o?: Partial<AlertConfirmPromptObj> | string) => void;
97
132
  /**
98
- * Main api.
133
+ * Queues a confirm dialog (Cancel + OK). `onOk` owns the close: call `acp.shift()`
134
+ * from it when done (a returned promise keeps the dialog pending until it settles).
135
+ * `onCancel`/`onEscape` default to `shift`. For a self-closing, Promise-based
136
+ * confirm use {@link createConfirm}.
99
137
  */
100
138
  confirm: (onOk: FnOnOK, o?: Partial<AlertConfirmPromptObj>) => void;
101
139
  /**
102
- * Main api.
140
+ * Queues a prompt dialog (input + Cancel + OK). `onOk` receives the entered value
141
+ * and owns the close: call `acp.shift()` from it when done (a returned promise
142
+ * keeps the dialog pending until it settles). `onCancel`/`onEscape` default to
143
+ * `shift`. For a self-closing, Promise-based prompt use {@link createPrompt}.
103
144
  */
104
145
  prompt: (onOk: FnOnOK, o?: Partial<AlertConfirmPromptObj>) => void;
105
146
  }
@@ -16,6 +16,23 @@ const ucf = (s) => `${s}`[0].toUpperCase() + `${s}`.slice(1);
16
16
  * Manages a FIFO queue of dialogs, allowing one dialog to be displayed at a time.
17
17
  * Provides a modern, customizable replacement for native browser dialogs.
18
18
  *
19
+ * **The handler owns the close.** A dialog is removed from the queue only by
20
+ * `shift()`. Each of `onOk`, `onCancel` and `onEscape` defaults to `shift` when the
21
+ * caller does not supply it, so an `alert()` with no callbacks closes itself. As soon
22
+ * as you pass your own handler, closing becomes your job: call `acp.shift()` when you
23
+ * are done. The stack does not close for you, so that an async handler can validate,
24
+ * stage a second step, or keep a failed action's message on screen. (An `onOk` that
25
+ * runs its work but never shifts is the classic mistake: the work happens, the dialog
26
+ * stays, and the user clicks OK again.)
27
+ *
28
+ * While a handler's returned promise is pending, the rendered dialog is in a "pending"
29
+ * state (buttons disabled, spinner shown, Escape refused). Return the promise - do not
30
+ * `void` it - to get that for the duration of your work.
31
+ *
32
+ * If all you want is "a dialog that closes when the user answers", use the
33
+ * Promise-based wrappers {@link createAlert}, {@link createConfirm} and
34
+ * {@link createPrompt} instead; they shift for you.
35
+ *
19
36
  * @example
20
37
  * ```ts
21
38
  * const acp = new AlertConfirmPromptStack({
@@ -23,18 +40,24 @@ const ucf = (s) => `${s}`[0].toUpperCase() + `${s}`.slice(1);
23
40
  * labelCancel: 'Dismiss'
24
41
  * });
25
42
  *
26
- * // Simple alert
43
+ * // Simple alert (no handler given, so OK/Escape default to `shift`)
27
44
  * acp.alert({ title: 'Notice', content: 'Operation complete' });
28
45
  *
29
- * // Confirm with callback
46
+ * // Confirm with callback. Supplying `onOk` makes YOU responsible for closing.
30
47
  * acp.confirm(
31
- * () => console.log('Confirmed!'),
48
+ * async () => {
49
+ * await deleteItem(); // dialog is pending (OK disabled) until this settles
50
+ * acp.shift();
51
+ * },
32
52
  * { title: 'Delete?', content: 'This cannot be undone', variant: 'warn' }
33
53
  * );
34
54
  *
35
- * // Prompt for input
55
+ * // Prompt for input. Same rule: shift when done.
36
56
  * acp.prompt(
37
- * (value) => console.log('User entered:', value),
57
+ * (value) => {
58
+ * console.log('User entered:', value);
59
+ * acp.shift();
60
+ * },
38
61
  * { title: 'Name', content: 'Enter your name', value: 'Default' }
39
62
  * );
40
63
  * ```
@@ -80,19 +103,30 @@ export class AlertConfirmPromptStack {
80
103
  o._id = Math.random().toString(36).slice(2);
81
104
  this.#stack.push(o);
82
105
  };
106
+ /** Removes the current dialog from the queue, revealing the next one (if any). */
83
107
  shift = () => this.#stack.shift();
108
+ /** Clears the whole queue. */
84
109
  reset = () => {
85
110
  this.#stack = [];
86
111
  };
112
+ /**
113
+ * Runs the current dialog's `onEscape` handler (which defaults to `shift`). Does
114
+ * nothing on an empty stack. The handler owns the close, exactly as with `onOk`
115
+ * and `onCancel`, so this does NOT shift on its own: a custom `onEscape` that
116
+ * does not shift keeps the dialog open, and one that does shift pops exactly one
117
+ * entry (not the one queued behind it as well).
118
+ */
87
119
  escape = () => {
88
- this.#stack?.[0]?.onEscape?.();
89
- this.shift();
120
+ return this.#stack[0]?.onEscape?.();
90
121
  };
122
+ /** Snapshot of the queue (current dialog first). */
91
123
  dump = () => {
92
124
  return [...this.#stack];
93
125
  };
94
126
  /**
95
- * Main api.
127
+ * Queues an alert dialog (OK button only). With no `onOk`/`onEscape` given, both
128
+ * default to `shift`, so a plain alert closes itself. If you supply either, call
129
+ * `shift()` from it yourself.
96
130
  */
97
131
  alert = (o) => {
98
132
  if (typeof o === "string")
@@ -100,13 +134,19 @@ export class AlertConfirmPromptStack {
100
134
  this.#push({ ...(o || {}), type: AlertConfirmPromptType.ALERT });
101
135
  };
102
136
  /**
103
- * Main api.
137
+ * Queues a confirm dialog (Cancel + OK). `onOk` owns the close: call `acp.shift()`
138
+ * from it when done (a returned promise keeps the dialog pending until it settles).
139
+ * `onCancel`/`onEscape` default to `shift`. For a self-closing, Promise-based
140
+ * confirm use {@link createConfirm}.
104
141
  */
105
142
  confirm = (onOk, o) => {
106
143
  this.#push({ onOk, value: false, ...o, type: AlertConfirmPromptType.CONFIRM });
107
144
  };
108
145
  /**
109
- * Main api.
146
+ * Queues a prompt dialog (input + Cancel + OK). `onOk` receives the entered value
147
+ * and owns the close: call `acp.shift()` from it when done (a returned promise
148
+ * keeps the dialog pending until it settles). `onCancel`/`onEscape` default to
149
+ * `shift`. For a self-closing, Promise-based prompt use {@link createPrompt}.
110
150
  */
111
151
  prompt = (onOk, o) => {
112
152
  this.#push({ onOk, value: "", ...o, type: AlertConfirmPromptType.PROMPT });
@@ -7,13 +7,21 @@
7
7
  class?: string;
8
8
  classContent?: string;
9
9
  classIcon?: string;
10
+ /**
11
+ * Message content. Any THC form — a string, `{ text }`, `{ html }`, `{ component }`,
12
+ * `{ snippet }` or a bare snippet — is handed to `Thc` as-is; an `Error` is rendered
13
+ * as `String(error)`. Empty/nullish renders nothing.
14
+ */
10
15
  message: THC | Error | undefined | null;
11
16
  intent?: MessageIntent;
17
+ /** Render a string or `{ text }` message via `{@html}` (snippets/components ignore it). */
12
18
  forceAsHtml?: boolean;
13
19
  duration?: number;
14
20
  onDismiss?: (() => void) | null | false;
15
21
  withIcon?: boolean;
16
22
  iconFn?: (() => string) | false;
23
+ /** Accessible name (`aria-label` + `title`) of the built-in dismiss button. */
24
+ dismissLabel?: string;
17
25
  }
18
26
  </script>
19
27
 
@@ -21,7 +29,7 @@
21
29
  import { untrack } from "svelte";
22
30
  import { slide } from "svelte/transition";
23
31
  import { twMerge } from "../../utils/tw-merge.js";
24
- import Thc, { isTHCNotEmpty } from "../Thc/Thc.svelte";
32
+ import Thc, { getTHCStringContent, isTHCNotEmpty } from "../Thc/Thc.svelte";
25
33
  import Button from "../Button/Button.svelte";
26
34
  import {
27
35
  iconAlertWarning,
@@ -48,22 +56,36 @@
48
56
  onDismiss,
49
57
  withIcon,
50
58
  iconFn,
59
+ dismissLabel = "Dismiss",
51
60
  }: Props = $props();
52
61
 
62
+ // Only the non-THC member of the union needs coercing. Every THC form is passed to
63
+ // `Thc` intact — `String()`-ing the whole union used to flatten the object forms to
64
+ // "[object Object]" and a snippet to its source text.
65
+ let _message: THC = $derived(
66
+ message instanceof Error ? String(message) : (message ?? "")
67
+ );
68
+
53
69
  // Track dismissal in local state instead of mutating the (non-bindable) `message`
54
70
  // prop. Mutating a destructured prop var creates a local shadow that Svelte 5
55
71
  // won't always overwrite when the parent re-passes the same value — so a user
56
72
  // who dismissed an error would never see the SAME error message again, even
57
73
  // after the parent re-set it. Keeping `_dismissed` separate sidesteps that and
58
74
  // makes the dismiss state reset cleanly whenever the message changes.
59
- let _message = $derived(message ? String(message) : "");
60
75
  let _dismissed = $state(false);
61
76
  let _show = $derived(isTHCNotEmpty(_message) && !_dismissed);
62
77
 
63
78
  // Reset the dismissed flag whenever the message changes — a new (or re-set)
64
79
  // message from the parent should re-show, even if the user previously dismissed.
80
+ //
81
+ // Keyed on the string content where there is one (string, Error, `{ text }`,
82
+ // `{ html }`), so an inline object literal — a new object on every parent render —
83
+ // does not re-show a dismissed message on unrelated state changes. Component and
84
+ // snippet forms have no string content and fall back to identity: a re-created
85
+ // snippet is a message rebuilt from new data and re-shows.
86
+ let _resetKey = $derived(getTHCStringContent(_message) || _message);
65
87
  $effect(() => {
66
- void _message;
88
+ void _resetKey;
67
89
  untrack(() => {
68
90
  if (_dismissed) _dismissed = false;
69
91
  });
@@ -110,6 +132,8 @@
110
132
  roundedFull
111
133
  size="sm"
112
134
  type="button"
135
+ title={dismissLabel}
136
+ aria-label={dismissLabel}
113
137
  onclick={() => _onDismiss()}
114
138
  />
115
139
  </div>
@@ -4,13 +4,21 @@ export interface Props {
4
4
  class?: string;
5
5
  classContent?: string;
6
6
  classIcon?: string;
7
+ /**
8
+ * Message content. Any THC form — a string, `{ text }`, `{ html }`, `{ component }`,
9
+ * `{ snippet }` or a bare snippet — is handed to `Thc` as-is; an `Error` is rendered
10
+ * as `String(error)`. Empty/nullish renders nothing.
11
+ */
7
12
  message: THC | Error | undefined | null;
8
13
  intent?: MessageIntent;
14
+ /** Render a string or `{ text }` message via `{@html}` (snippets/components ignore it). */
9
15
  forceAsHtml?: boolean;
10
16
  duration?: number;
11
17
  onDismiss?: (() => void) | null | false;
12
18
  withIcon?: boolean;
13
19
  iconFn?: (() => string) | false;
20
+ /** Accessible name (`aria-label` + `title`) of the built-in dismiss button. */
21
+ dismissLabel?: string;
14
22
  }
15
23
  declare const DismissibleMessage: import("svelte").Component<Props, {}, "">;
16
24
  type DismissibleMessage = ReturnType<typeof DismissibleMessage>;
@@ -4,15 +4,29 @@ A dismissible alert/message component with semantic intents and slide transition
4
4
 
5
5
  ## Props
6
6
 
7
- | Prop | Type | Default | Description |
8
- | -------------- | --------------------------------------------------- | ------- | -------------------------------------------------- |
9
- | `message` | `THC \| Error` | - | Message content (string, HTML, or Error object) |
10
- | `intent` | `"destructive" \| "warning" \| "success" \| "info"` | - | Semantic color intent |
11
- | `forceAsHtml` | `boolean` | `true` | Render message as HTML |
12
- | `duration` | `number` | `150` | Slide transition duration (ms) |
13
- | `onDismiss` | `(() => void) \| null \| false` | - | Dismiss callback (set to `false` to hide X button) |
14
- | `class` | `string` | - | CSS for container |
15
- | `classContent` | `string` | - | CSS for content area |
7
+ | Prop | Type | Default | Description |
8
+ | -------------- | --------------------------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
9
+ | `message` | `THC \| Error \| null \| undefined` | - | Message content: any [THC](../Thc/README.md) form (string, `{ text }`, `{ html }`, `{ component }`, `{ snippet }`, bare snippet) or an `Error`. Empty renders nothing |
10
+ | `intent` | `"destructive" \| "warning" \| "success" \| "info"` | - | Semantic color intent |
11
+ | `forceAsHtml` | `boolean` | `true` | Render a string or `{ text }` message via `{@html}`; snippets and components are unaffected |
12
+ | `duration` | `number` | `150` | Slide transition duration (ms) |
13
+ | `onDismiss` | `(() => void) \| null \| false` | - | Dismiss callback (set to `false` to hide X button) |
14
+ | `dismissLabel` | `string` | `"Dismiss"` | Accessible name (`aria-label` + `title`) of the built-in dismiss button |
15
+ | `withIcon` | `boolean` | - | Show the default icon for the current `intent` |
16
+ | `iconFn` | `(() => string) \| false` | - | Custom icon (returns an SVG string); `false` hides the icon |
17
+ | `class` | `string` | - | CSS for container |
18
+ | `classContent` | `string` | - | CSS for content area |
19
+ | `classIcon` | `string` | - | CSS for icon area |
20
+
21
+ `message` is a THC, so anything `Thc` renders works: a string, `{ text }`, `{ html }`,
22
+ `{ component, props }`, `{ snippet }` or a bare snippet. An `Error` is rendered as
23
+ `String(error)` (i.e. `Error: <message>`).
24
+
25
+ Dismissing hides the message locally; the dismissed state resets when the message changes.
26
+ For string, `{ text }`, `{ html }` and `Error` messages "changes" means different text — an
27
+ inline `{ text: t("saved") }` literal re-created on every parent render does **not** re-show a
28
+ dismissed message. Snippet and component messages have no text to compare, so they are keyed
29
+ on identity: a re-created snippet re-shows.
16
30
 
17
31
  ## Usage
18
32
 
@@ -51,6 +65,58 @@ A dismissible alert/message component with semantic intents and slide transition
51
65
  <DismissibleMessage message="New features are available" intent="info" />
52
66
  ```
53
67
 
68
+ ### Snippet Content (with an action button)
69
+
70
+ Any THC form works as the message, so a banner that needs an action inside the alert is a
71
+ snippet — no need to re-implement the alert markup:
72
+
73
+ ```svelte
74
+ <script lang="ts">
75
+ import { Button, DismissibleMessage } from "@marianmeres/stuic";
76
+
77
+ let sending = $state(false);
78
+
79
+ async function resend() {
80
+ sending = true;
81
+ try {
82
+ await api.resendVerificationEmail();
83
+ } finally {
84
+ sending = false;
85
+ }
86
+ }
87
+ </script>
88
+
89
+ {#snippet body()}
90
+ <span class="flex-1">Your email address is not verified.</span>
91
+ <Button size="sm" variant="outline" disabled={sending} onclick={resend}>Resend</Button>
92
+ {/snippet}
93
+
94
+ <DismissibleMessage
95
+ message={body}
96
+ intent="warning"
97
+ withIcon
98
+ classContent="flex flex-wrap items-center gap-x-4 gap-y-2"
99
+ dismissLabel="Dismiss"
100
+ />
101
+ ```
102
+
103
+ ### Other THC Forms
104
+
105
+ ```svelte
106
+ <!-- explicit text (rendered via {@html} under the default forceAsHtml — pass
107
+ forceAsHtml={false} to escape it) -->
108
+ <DismissibleMessage message={{ text: "Operation completed" }} intent="success" />
109
+
110
+ <!-- html -->
111
+ <DismissibleMessage message={{ html: "<b>Saved.</b> You can close this tab." }} />
112
+
113
+ <!-- component -->
114
+ <DismissibleMessage
115
+ message={{ component: QuotaWarning, props: { used: 95 } }}
116
+ intent="warning"
117
+ />
118
+ ```
119
+
54
120
  ### Non-Dismissible
55
121
 
56
122
  ```svelte
@@ -114,5 +114,5 @@ Many stuic components accept THC for labels and content:
114
114
  description={{ html: "Enter your <strong>unique</strong> username" }}
115
115
  />
116
116
 
117
- <DismissibleMessage message={{ text: "Operation completed" }} theme="green" />
117
+ <DismissibleMessage message={{ text: "Operation completed" }} intent="success" />
118
118
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "3.176.0",
3
+ "version": "3.178.0",
4
4
  "packageManager": "pnpm@11.5.0",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -141,8 +141,8 @@
141
141
  "@codemirror/lang-markdown": "^6.5.2",
142
142
  "@codemirror/language": "^6.12.4",
143
143
  "@codemirror/language-data": "^6.5.2",
144
- "@codemirror/state": "^6.7.2",
145
- "@codemirror/view": "^6.43.10",
144
+ "@codemirror/state": "^6.7.4",
145
+ "@codemirror/view": "^6.43.11",
146
146
  "@eslint/js": "^9.39.5",
147
147
  "@marianmeres/random-human-readable": "^1.10.2",
148
148
  "@marianmeres/trend-chart": "^0.5.0",
@@ -168,7 +168,7 @@
168
168
  "dotenv": "^16.6.1",
169
169
  "eslint": "^9.39.5",
170
170
  "globals": "^16.5.0",
171
- "playwright": "^1.62.1",
171
+ "playwright": "^1.63.0",
172
172
  "prettier": "^3.9.6",
173
173
  "prettier-plugin-svelte": "^3.5.2",
174
174
  "publint": "^0.3.24",
@@ -187,7 +187,7 @@
187
187
  "@marianmeres/clog": "^3.21.0",
188
188
  "@marianmeres/countries": "^1.1.0",
189
189
  "@marianmeres/cron-parser": "^1.0.1",
190
- "@marianmeres/design-tokens": "^1.18.0",
190
+ "@marianmeres/design-tokens": "^1.20.0",
191
191
  "@marianmeres/icons-fns": "^6.0.0",
192
192
  "@marianmeres/item-collection": "^1.4.2",
193
193
  "@marianmeres/paging-store": "^2.1.1",