@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.
- package/dist/components/AlertConfirmPrompt/AlertConfirmPrompt.svelte +3 -1
- package/dist/components/AlertConfirmPrompt/Current.svelte +17 -2
- package/dist/components/AlertConfirmPrompt/README.md +36 -1
- package/dist/components/AlertConfirmPrompt/alert-confirm-prompt-stack.svelte.d.ts +49 -8
- package/dist/components/AlertConfirmPrompt/alert-confirm-prompt-stack.svelte.js +50 -10
- package/dist/components/DismissibleMessage/DismissibleMessage.svelte +27 -3
- package/dist/components/DismissibleMessage/DismissibleMessage.svelte.d.ts +8 -0
- package/dist/components/DismissibleMessage/README.md +75 -9
- package/dist/components/Thc/README.md +1 -1
- package/package.json +5 -5
|
@@ -94,7 +94,9 @@
|
|
|
94
94
|
<ModalDialog
|
|
95
95
|
bind:this={modal}
|
|
96
96
|
preEscapeClose={() => {
|
|
97
|
-
|
|
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={
|
|
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={
|
|
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
|
-
* () =>
|
|
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) =>
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
* () =>
|
|
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) =>
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
8
|
-
| -------------- | --------------------------------------------------- |
|
|
9
|
-
| `message` | `THC \| Error`
|
|
10
|
-
| `intent` | `"destructive" \| "warning" \| "success" \| "info"` | -
|
|
11
|
-
| `forceAsHtml` | `boolean` | `true`
|
|
12
|
-
| `duration` | `number` | `150`
|
|
13
|
-
| `onDismiss` | `(() => void) \| null \| false` | -
|
|
14
|
-
| `
|
|
15
|
-
| `
|
|
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" }}
|
|
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.
|
|
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.
|
|
145
|
-
"@codemirror/view": "^6.43.
|
|
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.
|
|
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.
|
|
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",
|