@marianmeres/stuic 3.176.0 → 3.177.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.
@@ -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.177.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",