react-snackbar-glass 1.2.0 → 1.3.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/CHANGELOG.md CHANGED
@@ -5,6 +5,23 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.3.0] - 2026-10-28
9
+
10
+ ### Added
11
+
12
+ - `layout` option: `'card' | 'compact' | 'inline'`, available per snackbar,
13
+ on `<SnackbarProvider layout="…">` and in the store config. `compact: true`
14
+ keeps working as sugar for `layout: 'compact'`.
15
+ - `toast.single(message, options)` / `useSnackbar().single()`: message-only
16
+ status line with a hard one-line contract (ellipsis instead of wrapping),
17
+ no title, no hairline, and no close button while timed.
18
+ - Animation presets: `animation`, `enterAnimation` and `exitAnimation`
19
+ (`'slide' | 'fade' | 'scale' | 'pop' | 'flip' | 'none'`).
20
+ - `--prs-enter-duration` / `--prs-exit-duration` and `--prs-radius-inline`
21
+ CSS variables for pacing and shaping the new modes.
22
+ - Demo: layout and motion selectors in the Studio panel, `toast.single()`
23
+ example, and position buttons now confirm with a single-line toast.
24
+
8
25
  ## [1.2.0] - 2026-10-28
9
26
 
10
27
  ### Added
package/README.md CHANGED
@@ -9,6 +9,8 @@ dependencies**.
9
9
  - ⏳ **Promise aware** — one call moves a snackbar through loading → success → error
10
10
  - 👆 **Swipe to dismiss**, hover/focus/document-hidden pause, countdown hairline
11
11
  - 📦 **Compact + minimize** — one-line pills, or collapse the whole stack
12
+ - ✏️ **Single-line status** — `toast.single()` for instant, title-less feedback
13
+ - 🎞️ **Pick your motion** — slide, fade, scale, pop, flip or none
12
14
  - ♿ **Accessible** — polite/assertive live regions, keyboard reachable actions, `prefers-reduced-motion`
13
15
  - 🧩 **Typed** — written in TypeScript, ships ESM + CJS + `.d.ts`
14
16
  - 🪶 **Tiny** — React is the only peer dependency
@@ -162,6 +164,9 @@ the snackbar only closes after the handler settles (`closeOnClick`, default
162
164
  | `dismissible` | `boolean` | `true` | Show the close button. |
163
165
  | `progress` | `boolean` | `true` | Show the countdown hairline. |
164
166
  | `compact` | `boolean` | `false` | Single-line pill for every snackbar. |
167
+ | `layout` | `'card' \| 'compact' \| 'inline'` | `'card'` | Default layout; `compact` is sugar for `'compact'`. |
168
+ | `animation` | `'slide' \| 'fade' \| 'scale' \| 'pop' \| 'flip' \| 'none'` | `'slide'` | Enter + exit animation preset. |
169
+ | `enterAnimation` / `exitAnimation` | same as `animation` | — | Override a single phase. |
165
170
  | `minimizedLabel` | `(count: number) => ReactNode` | `"3 notifications"` | Text on the collapsed pill. |
166
171
  | `pauseOnHover` | `boolean` | `true` | Freeze the timer while hovered. |
167
172
  | `pauseOnFocus` | `boolean` | `true` | Freeze the timer while focus is inside. |
@@ -178,12 +183,55 @@ the snackbar only closes after the handler settles (`closeOnClick`, default
178
183
  ## `SnackbarOptions`
179
184
 
180
185
  `id`, `title`, `message`, `variant`, `duration`, `position`, `icon`,
181
- `action`, `dismissible`, `progress`, `compact`, `tone`, `size`, `className`,
182
- `style`, `onOpen`, `onClose`.
186
+ `action`, `dismissible`, `progress`, `compact`, `layout`, `animation`,
187
+ `enterAnimation`, `exitAnimation`, `tone`, `size`, `className`, `style`,
188
+ `onOpen`, `onClose`.
183
189
 
184
190
  Reusing an `id` updates (or revives) the existing snackbar instead of stacking
185
191
  a duplicate — ideal for progress-style flows.
186
192
 
193
+ ## Single-line status notices
194
+
195
+ `toast.single()` is the "one line, no ceremony" preset: content width, a hard
196
+ one-line contract (long text truncates with an ellipsis instead of wrapping),
197
+ no title, no countdown hairline and no close button while it is timed.
198
+
199
+ ```ts
200
+ toast.single('Position set to top-center', { variant: 'success' });
201
+ toast.single('Could not reach the server', { variant: 'error', duration: 8000 });
202
+ ```
203
+
204
+ It is `layout: 'inline'` under the hood, so every entry point can do the same:
205
+
206
+ ```ts
207
+ snackbar.show({ message: 'Synced', layout: 'inline' });
208
+ useSnackbar().single('Copied to clipboard');
209
+ <SnackbarProvider layout="inline" />;
210
+ ```
211
+
212
+ ## Animations
213
+
214
+ One preset drives both phases; `enterAnimation` / `exitAnimation` override a
215
+ single phase:
216
+
217
+ ```ts
218
+ toast.success('Saved', { animation: 'fade' });
219
+ toast.error('Failed', { animation: 'scale', exitAnimation: 'none' });
220
+ <SnackbarProvider animation="pop" />;
221
+ ```
222
+
223
+ | Preset | Feel |
224
+ | --- | --- |
225
+ | `slide` | Spring slide from the nearest edge with a soft blur (default). |
226
+ | `fade` | Pure opacity cross-fade. |
227
+ | `scale` | Zoom from the center. |
228
+ | `pop` | Bouncy overshoot. |
229
+ | `flip` | 3D flip on the X axis. |
230
+ | `none` | Instant, no motion. |
231
+
232
+ Pace them with `--prs-enter-duration` / `--prs-exit-duration` (defaults
233
+ `440ms` / `190ms`); `prefers-reduced-motion` still disables everything.
234
+
187
235
  ## Compact and minimized stacks
188
236
 
189
237
  Two ways to reclaim screen space when snackbars would otherwise pile up.
package/dist/index.cjs CHANGED
@@ -11,7 +11,10 @@ var DEFAULT_CONFIG = {
11
11
  duration: 5e3,
12
12
  dismissible: true,
13
13
  progress: true,
14
- compact: false,
14
+ layout: "card",
15
+ animation: "slide",
16
+ enterAnimation: "slide",
17
+ exitAnimation: "slide",
15
18
  tone: "glass",
16
19
  size: "md",
17
20
  maxVisible: 3
@@ -39,23 +42,38 @@ function resolveContent(content, value) {
39
42
  const resolved = typeof content === "function" ? content(value) : content;
40
43
  return contentToOptions(resolved);
41
44
  }
42
- function createSnackbarStore(initialConfig = {}) {
43
- let config = {
44
- ...DEFAULT_CONFIG,
45
- ...initialConfig.position !== void 0 ? { position: initialConfig.position } : null,
46
- ...initialConfig.duration !== void 0 ? {
47
- duration: normalizeDuration(
48
- initialConfig.duration,
49
- DEFAULT_CONFIG.duration
50
- )
51
- } : null,
52
- ...initialConfig.dismissible !== void 0 ? { dismissible: initialConfig.dismissible } : null,
53
- ...initialConfig.progress !== void 0 ? { progress: initialConfig.progress } : null,
54
- ...initialConfig.compact !== void 0 ? { compact: initialConfig.compact } : null,
55
- ...initialConfig.tone !== void 0 ? { tone: initialConfig.tone } : null,
56
- ...initialConfig.size !== void 0 ? { size: initialConfig.size } : null,
57
- ...initialConfig.maxVisible !== void 0 ? { maxVisible: Math.max(1, Math.floor(initialConfig.maxVisible)) } : null
45
+ function resolveLayout(layout, compact, fallback) {
46
+ if (layout !== void 0) return layout;
47
+ if (compact !== void 0) return compact ? "compact" : "card";
48
+ return fallback;
49
+ }
50
+ function resolveLayoutPatch(record, options) {
51
+ if (options.layout === void 0 && options.compact === void 0) return {};
52
+ const layout = resolveLayout(options.layout, options.compact, record.layout);
53
+ return { layout, compact: layout === "compact" };
54
+ }
55
+ function mergeConfig(previous, patch) {
56
+ const layout = resolveLayout(patch.layout, patch.compact, previous.layout);
57
+ return {
58
+ position: patch.position ?? previous.position,
59
+ duration: patch.duration !== void 0 ? normalizeDuration(patch.duration, previous.duration) : previous.duration,
60
+ dismissible: patch.dismissible ?? previous.dismissible,
61
+ progress: patch.progress ?? previous.progress,
62
+ compact: layout === "compact",
63
+ layout,
64
+ animation: patch.animation ?? previous.animation,
65
+ enterAnimation: patch.enterAnimation ?? patch.animation ?? previous.enterAnimation,
66
+ exitAnimation: patch.exitAnimation ?? patch.animation ?? previous.exitAnimation,
67
+ tone: patch.tone ?? previous.tone,
68
+ size: patch.size ?? previous.size,
69
+ maxVisible: patch.maxVisible !== void 0 ? Math.max(1, Math.floor(patch.maxVisible)) : previous.maxVisible
58
70
  };
71
+ }
72
+ function createSnackbarStore(initialConfig = {}) {
73
+ let config = mergeConfig(
74
+ DEFAULT_CONFIG,
75
+ initialConfig
76
+ );
59
77
  let records = EMPTY;
60
78
  const listeners = /* @__PURE__ */ new Set();
61
79
  const timers = /* @__PURE__ */ new Map();
@@ -223,6 +241,7 @@ function createSnackbarStore(initialConfig = {}) {
223
241
  }
224
242
  return id;
225
243
  }
244
+ const layout = resolveLayout(options.layout, options.compact, config.layout);
226
245
  const record = {
227
246
  id,
228
247
  title: options.title,
@@ -236,7 +255,11 @@ function createSnackbarStore(initialConfig = {}) {
236
255
  action: options.action ?? null,
237
256
  dismissible: options.dismissible ?? config.dismissible,
238
257
  progress: options.progress ?? config.progress,
239
- compact: options.compact ?? config.compact,
258
+ compact: layout === "compact",
259
+ layout,
260
+ animation: options.animation ?? config.animation,
261
+ enterAnimation: options.enterAnimation ?? options.animation ?? config.enterAnimation,
262
+ exitAnimation: options.exitAnimation ?? options.animation ?? config.exitAnimation,
240
263
  className: options.className,
241
264
  style: options.style,
242
265
  createdAt: Date.now(),
@@ -265,7 +288,14 @@ function createSnackbarStore(initialConfig = {}) {
265
288
  ...options.action !== void 0 ? { action: options.action } : null,
266
289
  ...options.dismissible !== void 0 ? { dismissible: options.dismissible } : null,
267
290
  ...options.progress !== void 0 ? { progress: options.progress } : null,
268
- ...options.compact !== void 0 ? { compact: options.compact } : null,
291
+ ...resolveLayoutPatch(record, options),
292
+ ...options.animation !== void 0 ? {
293
+ animation: options.animation,
294
+ enterAnimation: options.animation,
295
+ exitAnimation: options.animation
296
+ } : null,
297
+ ...options.enterAnimation !== void 0 ? { enterAnimation: options.enterAnimation } : null,
298
+ ...options.exitAnimation !== void 0 ? { exitAnimation: options.exitAnimation } : null,
269
299
  ...options.className !== void 0 ? { className: options.className } : null,
270
300
  ...options.style !== void 0 ? { style: options.style } : null,
271
301
  ...options.onOpen !== void 0 ? { onOpen: options.onOpen } : null,
@@ -315,19 +345,7 @@ function createSnackbarStore(initialConfig = {}) {
315
345
  }
316
346
  };
317
347
  const setConfig = (patch) => {
318
- config = {
319
- ...config,
320
- ...patch.position !== void 0 ? { position: patch.position } : null,
321
- ...patch.duration !== void 0 ? {
322
- duration: normalizeDuration(patch.duration, config.duration)
323
- } : null,
324
- ...patch.dismissible !== void 0 ? { dismissible: patch.dismissible } : null,
325
- ...patch.progress !== void 0 ? { progress: patch.progress } : null,
326
- ...patch.compact !== void 0 ? { compact: patch.compact } : null,
327
- ...patch.tone !== void 0 ? { tone: patch.tone } : null,
328
- ...patch.size !== void 0 ? { size: patch.size } : null,
329
- ...patch.maxVisible !== void 0 ? { maxVisible: Math.max(1, Math.floor(patch.maxVisible)) } : null
330
- };
348
+ config = mergeConfig(config, patch);
331
349
  };
332
350
  const promise = (input, messages, options = {}) => {
333
351
  const loadingOptions = contentToOptions(messages.loading ?? "Loading\u2026");
@@ -630,7 +648,10 @@ function SnackbarItem({
630
648
  };
631
649
  const resolvedIcon = record.icon === void 0 ? icons?.[variant] ?? defaultIcons[variant] : record.icon;
632
650
  const showIcon = record.icon !== false;
633
- const showProgress = !record.compact && record.progress && Number.isFinite(duration) && duration > 0;
651
+ const isInline = record.layout === "inline";
652
+ const isTimed = Number.isFinite(duration) && duration > 0;
653
+ const showProgress = record.layout === "card" && record.progress && isTimed;
654
+ const showClose = record.dismissible && (!isInline || !isTimed);
634
655
  const style = {
635
656
  ...record.style,
636
657
  "--prs-drag-x": `${dragX}px`,
@@ -651,6 +672,9 @@ function SnackbarItem({
651
672
  "data-has-icon": showIcon ? "true" : "false",
652
673
  "data-dismissible": record.dismissible ? "true" : "false",
653
674
  "data-compact": record.compact ? "true" : "false",
675
+ "data-layout": record.layout,
676
+ "data-enter": record.enterAnimation,
677
+ "data-exit": record.exitAnimation,
654
678
  "data-prs-id": record.id,
655
679
  "data-position": record.position,
656
680
  style,
@@ -699,7 +723,7 @@ function SnackbarItem({
699
723
  className: "prs-item__content",
700
724
  "data-has-title": title !== void 0 && title !== null ? "true" : "false",
701
725
  children: [
702
- title !== void 0 && title !== null ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "prs-item__title", children: title }) : null,
726
+ title !== void 0 && title !== null && !isInline ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "prs-item__title", children: title }) : null,
703
727
  message !== void 0 && message !== null ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "prs-item__message", children: message }) : null,
704
728
  action ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "prs-item__actions", children: /* @__PURE__ */ jsxRuntime.jsxs(
705
729
  "button",
@@ -718,7 +742,7 @@ function SnackbarItem({
718
742
  ]
719
743
  }
720
744
  ),
721
- record.dismissible ? /* @__PURE__ */ jsxRuntime.jsx(
745
+ showClose ? /* @__PURE__ */ jsxRuntime.jsx(
722
746
  "button",
723
747
  {
724
748
  type: "button",
@@ -938,6 +962,10 @@ function SnackbarProvider({
938
962
  dismissible,
939
963
  progress,
940
964
  compact,
965
+ layout,
966
+ animation,
967
+ enterAnimation,
968
+ exitAnimation,
941
969
  tone,
942
970
  size,
943
971
  maxVisible,
@@ -960,6 +988,10 @@ function SnackbarProvider({
960
988
  dismissible,
961
989
  progress,
962
990
  compact,
991
+ layout,
992
+ animation,
993
+ enterAnimation,
994
+ exitAnimation,
963
995
  tone,
964
996
  size,
965
997
  maxVisible
@@ -971,6 +1003,10 @@ function SnackbarProvider({
971
1003
  dismissible,
972
1004
  progress,
973
1005
  compact,
1006
+ layout,
1007
+ animation,
1008
+ enterAnimation,
1009
+ exitAnimation,
974
1010
  tone,
975
1011
  size,
976
1012
  maxVisible
@@ -989,6 +1025,7 @@ function SnackbarProvider({
989
1025
  info: preset2("info"),
990
1026
  neutral: preset2("neutral"),
991
1027
  custom: preset2("custom"),
1028
+ single: (message, options = {}) => store.show({ ...options, message, layout: "inline" }),
992
1029
  loading: (message, options = {}) => store.show({
993
1030
  ...options,
994
1031
  message,
@@ -1055,6 +1092,7 @@ function createToast() {
1055
1092
  toast2.info = preset("info");
1056
1093
  toast2.neutral = preset("neutral");
1057
1094
  toast2.custom = preset("custom");
1095
+ toast2.single = (message, options = {}) => getDefaultStore().show({ ...options, message, layout: "inline" });
1058
1096
  toast2.loading = (message, options = {}) => getDefaultStore().show({
1059
1097
  ...options,
1060
1098
  message,