react-snackbar-glass 1.2.0 → 1.4.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,46 @@ 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.4.0] - 2026-10-28
9
+
10
+ ### Added
11
+
12
+ - Surface presets via `tone`: `paper` (opaque matte, light/dark aware),
13
+ `graphite` (dark charcoal in both themes) and `neon` (near-black with an
14
+ accent glow), next to the existing `glass` and `solid`. Per-snackbar, on the
15
+ provider and in store config; no new prop to learn.
16
+ - Demo: the playground's tone switch is now a five-way surface picker with
17
+ swatches.
18
+
19
+ ### Changed
20
+
21
+ - README is now a full API reference: layouts, surfaces, sizes, animations,
22
+ every `SnackbarOptions` field, close reasons, the store surface and the
23
+ theming tokens.
24
+
25
+ ### Fixed
26
+
27
+ - Title-less cards no longer pin their message to the top edge: content is
28
+ centred against the icon when there is no title.
29
+
30
+ ## [1.3.0] - 2026-10-28
31
+
32
+ ### Added
33
+
34
+ - `layout` option: `'card' | 'compact' | 'inline'`, available per snackbar,
35
+ on `<SnackbarProvider layout="…">` and in the store config. `compact: true`
36
+ keeps working as sugar for `layout: 'compact'`.
37
+ - `toast.single(message, options)` / `useSnackbar().single()`: message-only
38
+ status line with a hard one-line contract (ellipsis instead of wrapping),
39
+ no title, no hairline, and no close button while timed.
40
+ - Animation presets: `animation`, `enterAnimation` and `exitAnimation`
41
+ (`'slide' | 'fade' | 'scale' | 'pop' | 'flip' | 'none'`).
42
+ - `--prs-enter-duration` / `--prs-exit-duration` and `--prs-radius-inline`
43
+ CSS variables for pacing and shaping the new modes.
44
+ - Demo: replaced the showcase page with an API-first homepage — hero install
45
+ command, 3-step quickstart, live playground, method/option tables and theming
46
+ notes (`demo/src/ApiHome.tsx`).
47
+
8
48
  ## [1.2.0] - 2026-10-28
9
49
 
10
50
  ### Added
package/README.md CHANGED
@@ -1,14 +1,16 @@
1
1
  # react-snackbar-glass
2
2
 
3
- A glassmorphic snackbar system for React — queueing, promise-aware
4
- states, swipe-to-dismiss and token-driven theming with **zero runtime
3
+ Queueing, promise-aware snackbars for React — five ready-made surfaces, three
4
+ densities, swipe-to-dismiss and token-driven theming with **zero runtime
5
5
  dependencies**.
6
6
 
7
- - 🪟 **Glass or solid** surfaces, `xs` → `lg` sizes, six screen corners
7
+ - 🪟 **Five surfaces** — `glass`, `paper`, `solid`, `graphite`, `neon`
8
+ - 📐 **Three layouts** — `card`, `compact`, `inline`; `toast.single()` for one-liners
8
9
  - 🌍 **Global `toast`** — call it from anywhere, no hook required
9
10
  - ⏳ **Promise aware** — one call moves a snackbar through loading → success → error
10
11
  - 👆 **Swipe to dismiss**, hover/focus/document-hidden pause, countdown hairline
11
- - 📦 **Compact + minimize** — one-line pills, or collapse the whole stack
12
+ - 📦 **Compact + minimize** — one-line pills, or collapse the whole stack into one
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
@@ -34,7 +36,7 @@ import 'react-snackbar-glass/styles.css';
34
36
 
35
37
  export default function App() {
36
38
  return (
37
- <SnackbarProvider position="bottom-right" theme="auto">
39
+ <SnackbarProvider position="bottom-right" theme="auto" tone="glass">
38
40
  <Dashboard />
39
41
  </SnackbarProvider>
40
42
  );
@@ -51,25 +53,43 @@ function Dashboard() {
51
53
  }
52
54
  ```
53
55
 
56
+ That is the whole setup: one provider, one stylesheet, one hook. Everything else
57
+ below is optional.
58
+
54
59
  ## API
55
60
 
61
+ Two entry points render the same store, so pick whichever fits the call site:
62
+
63
+ | Import | Use it when |
64
+ | --- | --- |
65
+ | `useSnackbar()` | Inside a component — the idiomatic path. |
66
+ | `toast` | Anywhere else: data layers, API clients, event handlers, `setTimeout`. |
67
+
56
68
  ### `useSnackbar()`
57
69
 
70
+ ```ts
71
+ const snackbar = useSnackbar();
72
+ ```
73
+
58
74
  | Method | Description |
59
75
  | --- | --- |
60
76
  | `show(options)` | Queue a fully customised snackbar, returns its `id`. |
61
- | `update(id, options)` | Patch an existing snackbar in place; timers restart if `duration` changes. |
77
+ | `update(id, options)` | Patch an existing snackbar in place; timers restart when `duration` changes. |
62
78
  | `dismiss(id?, reason?)` | Close one snackbar (or the oldest) and report a close reason. |
63
79
  | `dismissAll(reason?)` | Close every visible snackbar. |
64
80
  | `success / error / warning / info / neutral / loading / custom(message, options?)` | Variant presets. |
65
- | `promise(promise, messages, options?)` | Drive a single snackbar through loading, success and error. |
81
+ | `single(message, options?)` | Message-only status line (`layout: 'inline'`). |
82
+ | `promise(promise, messages, options?)` | Drive one snackbar through loading → success → error. |
83
+ | `minimize()` / `expand()` / `isMinimized()` | Collapse the stack into a summary pill and bring it back. |
66
84
  | `store` | Escape hatch: the underlying store for subscriptions and advanced updates. |
67
85
 
86
+ Every method returns the snackbar `id` (except `update`, `dismiss*` and the
87
+ minimize helpers), so you can update or dismiss it later.
88
+
68
89
  ### `toast` — global API
69
90
 
70
- Import `toast` when you are outside a component: data layers, event
71
- emitters, API clients, `setTimeout` callbacks. It targets the same store
72
- that `<SnackbarProvider>` renders, so no hook is needed.
91
+ Import `toast` when you are outside a component. It talks to the same store that
92
+ `<SnackbarProvider>` renders, so no hook is needed.
73
93
 
74
94
  ```ts
75
95
  import { toast } from 'react-snackbar-glass';
@@ -77,22 +97,115 @@ import { toast } from 'react-snackbar-glass';
77
97
  toast('Draft saved');
78
98
  toast.success('Deployment complete.', { title: 'v1.4.0 is live' });
79
99
  toast.error('Payment declined', { action: { label: 'Retry', onClick: retry } });
80
- ```
100
+ toast.single('Position set to top-center');
81
101
 
82
- It mirrors `useSnackbar()` one-to-one — `show`, `update`, `dismiss`,
83
- `dismissAll`, every variant preset, `promise`, and `store`:
84
-
85
- ```ts
86
102
  const id = toast.loading('Saving…');
87
103
  await save();
88
104
  toast.update(id, { variant: 'success', message: 'Saved', duration: 3000 });
89
105
  ```
90
106
 
107
+ It mirrors `useSnackbar()` one-to-one — `show`, `update`, `dismiss`,
108
+ `dismissAll`, every variant preset, `single`, `promise`, `minimize`, `expand`,
109
+ `isMinimized` and `store`.
110
+
91
111
  `<SnackbarProvider>` renders the shared store by default. Mount
92
112
  `<SnackbarHost store={getDefaultStore()} />` if you need the host without a
93
- provider, and `setDefaultStore(store)` to point `toast` at a store of your
94
- own (handy in tests or multi-app setups). On the server, pass an explicit
95
- `store` so requests stay isolated.
113
+ provider, and `setDefaultStore(store)` to point `toast` at a store of your own
114
+ (handy in tests or multi-app setups). On the server, pass an explicit `store` so
115
+ requests stay isolated.
116
+
117
+ ### Layouts
118
+
119
+ `layout` is the density axis. It can be set per snackbar, on the provider or in
120
+ the store config, and it is the only thing that changes between the three shapes:
121
+
122
+ | `layout` | Shape | Use it for |
123
+ | --- | --- | --- |
124
+ | `card` | Full card: title, message, action, hairline. Default. | Results that deserve a headline and an action. |
125
+ | `compact` | Fully rounded single-line pill. | High-frequency messages that should stay quiet. |
126
+ | `inline` | Message-only status line, content width, ~38 px tall. | Instant feedback such as “Copied” or “Position set”. |
127
+
128
+ ```ts
129
+ toast.success('Draft saved', { layout: 'compact' });
130
+ snackbar.show({ message: 'Synced', layout: 'inline' });
131
+ <SnackbarProvider layout="compact" />;
132
+ ```
133
+
134
+ `compact: true` still works and is sugar for `layout: 'compact'`.
135
+
136
+ **Single-line status** — `toast.single()` is the `inline` preset with a hard
137
+ one-line contract: long text truncates with an ellipsis instead of wrapping, and
138
+ a timed notice has no title, no countdown hairline and no close button.
139
+
140
+ ```ts
141
+ toast.single('Position set to top-center', { variant: 'success' });
142
+ toast.single('Could not reach the server', { variant: 'error', duration: 8000 });
143
+ ```
144
+
145
+ ### Surfaces
146
+
147
+ Five presets ship with the stylesheet. Pick one per snackbar or as the default:
148
+
149
+ ```tsx
150
+ <SnackbarProvider tone="paper" />
151
+
152
+ // or per snackbar
153
+ toast.info('Backup finished.', { tone: 'graphite' });
154
+ ```
155
+
156
+ | `tone` | Look |
157
+ | --- | --- |
158
+ | `glass` | Frosted and translucent — the page shows through. Default. |
159
+ | `paper` | Opaque, matte, hairline border, no blur. Follows light/dark. |
160
+ | `solid` | The variant accent painted as the surface, white text. |
161
+ | `graphite` | Dark matte charcoal with light text, in both themes. |
162
+ | `neon` | Near-black with an accent glow ring, in both themes. |
163
+
164
+ All five keep the same layout, icon tinting, hairline and motion, and every one
165
+ is driven by the `--prs-*` tokens, so you can restyle a preset without forking
166
+ the CSS.
167
+
168
+ ### Sizes and positions
169
+
170
+ | `size` | Width (relative to `--prs-width`, default `384px`) |
171
+ | --- | --- |
172
+ | `xs` | 72 % |
173
+ | `sm` | 86 % |
174
+ | `md` | 100 % — default |
175
+ | `lg` | 116 % |
176
+
177
+ Positions are the six screen corners: `top-left`, `top-center`, `top-right`,
178
+ `bottom-left`, `bottom-center`, `bottom-right` (default).
179
+
180
+ ```ts
181
+ toast.success('Saved', { size: 'sm', position: 'top-center' });
182
+ ```
183
+
184
+ `maxVisible` (default `3`) caps how many cards a corner shows at once; older
185
+ ones stay queued and take the next free slot.
186
+
187
+ ### Animations
188
+
189
+ One preset drives both phases; `enterAnimation` / `exitAnimation` override a
190
+ single phase:
191
+
192
+ ```ts
193
+ toast.success('Saved', { animation: 'fade' });
194
+ toast.error('Failed', { animation: 'scale', exitAnimation: 'none' });
195
+ <SnackbarProvider animation="pop" />;
196
+ ```
197
+
198
+ | Preset | Feel |
199
+ | --- | --- |
200
+ | `slide` | Spring slide from the nearest edge with a soft blur (default). |
201
+ | `fade` | Pure opacity cross-fade. |
202
+ | `scale` | Zoom from the center. |
203
+ | `pop` | Bouncy overshoot. |
204
+ | `flip` | 3D flip on the X axis. |
205
+ | `none` | Instant, no motion. |
206
+
207
+ Pace them with `--prs-enter-duration` / `--prs-exit-duration` (defaults
208
+ `440ms` / `190ms`); `prefers-reduced-motion` still disables everything.
96
209
 
97
210
  ### Promise-aware flows
98
211
 
@@ -110,12 +223,13 @@ snackbar.promise(deploy(), {
110
223
  });
111
224
  ```
112
225
 
113
- `success` and `error` can be plain content, full options, or a mapper that
114
- receives the resolved value / rejection reason.
226
+ `loading`, `success` and `error` accept plain content, full options, or a mapper
227
+ that receives the resolved value / rejection reason. The returned promise
228
+ resolves with the original value, so it can be awaited inline.
115
229
 
116
230
  ### Sticky snackbars with in-place updates
117
231
 
118
- ```tsx
232
+ ```ts
119
233
  const id = snackbar.show({
120
234
  variant: 'loading',
121
235
  title: 'Uploading assets',
@@ -132,9 +246,12 @@ snackbar.update(id, {
132
246
  });
133
247
  ```
134
248
 
249
+ Reusing an `id` (in `show()` or `update()`) updates or revives the existing
250
+ snackbar instead of stacking a duplicate — ideal for progress-style flows.
251
+
135
252
  ### Actions and undo windows
136
253
 
137
- ```tsx
254
+ ```ts
138
255
  snackbar.show({
139
256
  message: 'Project archived.',
140
257
  duration: 7000,
@@ -145,23 +262,76 @@ snackbar.show({
145
262
  });
146
263
  ```
147
264
 
148
- Action handlers may return a promise: the button shows a pending state and
149
- the snackbar only closes after the handler settles (`closeOnClick`, default
150
- `true`).
265
+ Action handlers may return a promise: the button shows a pending state and the
266
+ snackbar only closes after the handler settles (`closeOnClick`, default `true`).
267
+ Set `closeOnClick: false` to keep the snackbar open for a second step.
268
+
269
+ ### Close reasons and lifecycle
270
+
271
+ `onClose` receives **why** a snackbar went away, plus its id:
272
+
273
+ ```ts
274
+ snackbar.show({
275
+ message: 'Draft saved',
276
+ onOpen: (id) => track('shown', id),
277
+ onClose: (reason) => track('closed', reason),
278
+ });
279
+ ```
280
+
281
+ | Reason | Fired by |
282
+ | --- | --- |
283
+ | `timeout` | The duration elapsed. |
284
+ | `manual` | The close button (or `dismiss()` without a reason). |
285
+ | `action` | The action button, after its handler settled. |
286
+ | `swipe` | A drag past `swipeThreshold`. |
287
+ | `escape` | The user pressed <kbd>Esc</kbd> while a snackbar had focus. |
288
+ | `dismissAll` | `dismissAll()` cleared the stack. |
289
+
290
+ `onOpen` runs once the snackbar is actually mounted in a visible slot — not when
291
+ it is queued.
292
+
293
+ ## `SnackbarOptions`
294
+
295
+ | Option | Type | Default | Notes |
296
+ | --- | --- | --- | --- |
297
+ | `id` | `string` | generated | Stable key; reusing it updates instead of stacking. |
298
+ | `title` | `ReactNode` | — | Small, strong headline above the message. |
299
+ | `message` | `ReactNode` | — | Main content. |
300
+ | `variant` | `'neutral' \| 'success' \| 'error' \| 'warning' \| 'info' \| 'loading' \| 'custom'` | `'neutral'` | Drives accent, icon and live-region politeness. |
301
+ | `duration` | `number \| null` | `5000` | `null`, `0` or `Infinity` = persistent. |
302
+ | `position` | `SnackbarPosition` | provider | Corner for this snackbar. |
303
+ | `icon` | `ReactNode \| false` | variant icon | `false` hides the icon. |
304
+ | `action` | `{ label, onClick?, closeOnClick? } \| null` | `null` | One call-to-action. |
305
+ | `dismissible` | `boolean` | `true` | Show the close button. |
306
+ | `progress` | `boolean` | `true` | Countdown hairline. |
307
+ | `layout` | `'card' \| 'compact' \| 'inline'` | `'card'` | Density. |
308
+ | `compact` | `boolean` | `false` | Sugar for `layout: 'compact'`. |
309
+ | `animation` | `SnackbarAnimation` | `'slide'` | Both phases. |
310
+ | `enterAnimation` / `exitAnimation` | `SnackbarAnimation` | — | Per-phase override. |
311
+ | `tone` | `'glass' \| 'paper' \| 'solid' \| 'graphite' \| 'neon'` | `'glass'` | Surface preset. |
312
+ | `size` | `'xs' \| 'sm' \| 'md' \| 'lg'` | `'md'` | Density/width. |
313
+ | `className` / `style` | — | — | Per-snackbar styling hooks. |
314
+ | `onOpen` | `(id) => void` | — | Mounted and visible. |
315
+ | `onClose` | `(reason, id) => void` | — | Dismissal started. |
151
316
 
152
317
  ## `<SnackbarProvider>` props
153
318
 
319
+ Every default below can be overridden per snackbar in `SnackbarOptions`.
320
+
154
321
  | Prop | Type | Default | Description |
155
322
  | --- | --- | --- | --- |
156
323
  | `position` | `SnackbarPosition` | `'bottom-right'` | Default corner for new snackbars. |
157
324
  | `duration` | `number \| null` | `5000` | Auto-dismiss delay in ms. `null`/`0`/`Infinity` = persistent. |
158
- | `tone` | `'glass' \| 'solid'` | `'glass'` | Surface treatment. |
325
+ | `tone` | `'glass' \| 'paper' \| 'solid' \| 'graphite' \| 'neon'` | `'glass'` | Surface preset. |
159
326
  | `size` | `'xs' \| 'sm' \| 'md' \| 'lg'` | `'md'` | Card density/width. |
327
+ | `layout` | `'card' \| 'compact' \| 'inline'` | `'card'` | Default layout. |
328
+ | `compact` | `boolean` | `false` | Sugar for `layout: 'compact'`. |
329
+ | `animation` | `SnackbarAnimation` | `'slide'` | Enter + exit preset. |
330
+ | `enterAnimation` / `exitAnimation` | `SnackbarAnimation` | — | Per-phase override. |
160
331
  | `theme` | `'light' \| 'dark' \| 'auto'` | `'auto'` | Color scheme. |
161
332
  | `maxVisible` | `number` | `3` | Simultaneously visible cards per corner. |
162
333
  | `dismissible` | `boolean` | `true` | Show the close button. |
163
334
  | `progress` | `boolean` | `true` | Show the countdown hairline. |
164
- | `compact` | `boolean` | `false` | Single-line pill for every snackbar. |
165
335
  | `minimizedLabel` | `(count: number) => ReactNode` | `"3 notifications"` | Text on the collapsed pill. |
166
336
  | `pauseOnHover` | `boolean` | `true` | Freeze the timer while hovered. |
167
337
  | `pauseOnFocus` | `boolean` | `true` | Freeze the timer while focus is inside. |
@@ -174,15 +344,7 @@ the snackbar only closes after the handler settles (`closeOnClick`, default
174
344
  | `container` | `HTMLElement \| () => HTMLElement \| null` | `document.body` | Portal target. |
175
345
  | `dir` | `'ltr' \| 'rtl'` | inherited | Text direction. |
176
346
  | `zIndex` | `number` | `2147483000` | Host stacking context. |
177
-
178
- ## `SnackbarOptions`
179
-
180
- `id`, `title`, `message`, `variant`, `duration`, `position`, `icon`,
181
- `action`, `dismissible`, `progress`, `compact`, `tone`, `size`, `className`,
182
- `style`, `onOpen`, `onClose`.
183
-
184
- Reusing an `id` updates (or revives) the existing snackbar instead of stacking
185
- a duplicate — ideal for progress-style flows.
347
+ | `className` / `style` | — | — | Classes/styles for the host root. |
186
348
 
187
349
  ## Compact and minimized stacks
188
350
 
@@ -202,9 +364,9 @@ Set it once for every snackbar with `<SnackbarProvider compact />` or
202
364
  (`"3 notifications"`) that expands on click:
203
365
 
204
366
  ```ts
205
- toast.minimize(); // stack → one pill
206
- toast.expand(); // back to full cards
207
- toast.isMinimized(); // boolean
367
+ toast.minimize(); // stack → one pill
368
+ toast.expand(); // back to full cards
369
+ toast.isMinimized(); // boolean
208
370
  ```
209
371
 
210
372
  `useSnackbar()` exposes the same three methods, and the store has
@@ -213,13 +375,14 @@ toast.isMinimized(); // boolean
213
375
 
214
376
  ## Theming
215
377
 
216
- Colors, radii, blur and motion are CSS custom properties on `.prs-root`
217
- (and per-card inline styles). Override globally:
378
+ Colors, radii, blur and motion are CSS custom properties on `.prs-root` (and
379
+ per-card inline styles).
218
380
 
219
381
  ```css
220
382
  :root {
221
383
  --prs-accent: #8b5cf6;
222
384
  --prs-radius: 16px;
385
+ --prs-width: 400px;
223
386
  }
224
387
  ```
225
388
 
@@ -231,12 +394,32 @@ snackbar.custom('Added to collection.', {
231
394
  });
232
395
  ```
233
396
 
397
+ | Token | Default | Controls |
398
+ | --- | --- | --- |
399
+ | `--prs-accent` | variant color | Accent for icons, actions and links. |
400
+ | `--prs-accent-soft` | 13 % accent | Icon chip / action background. |
401
+ | `--prs-accent-ring` | 30 % accent | Icon ring and hover border. |
402
+ | `--prs-solid` | accent | Surface color of the `solid` tone. |
403
+ | `--prs-radius` | `18px` | Card radius. |
404
+ | `--prs-width` | `384px` | Base card width; `size` scales it. |
405
+ | `--prs-offset` | `20px` | Distance from the viewport edge. |
406
+ | `--prs-gap` | `12px` | Space between stacked cards. |
407
+ | `--prs-font` | Inter/system stack | Font family. |
408
+ | `--prs-blur` | `blur(22px) saturate(1.55)` | Glass backdrop filter. |
409
+ | `--prs-shadow` / `--prs-shadow-hover` | layered | Card shadow. |
410
+ | `--prs-enter-duration` / `--prs-exit-duration` | `440ms` / `190ms` | Motion pacing. |
411
+ | `--prs-radius-inline` | pill radius | Radius of `inline` notices. |
412
+ | `--prs-z-index` | `2147483000` | Host stacking context. |
413
+
234
414
  ## Store-only usage
235
415
 
416
+ If you want to own the store — or render the host somewhere specific — skip the
417
+ provider:
418
+
236
419
  ```tsx
237
420
  import { createSnackbarStore, SnackbarHost } from 'react-snackbar-glass';
238
421
 
239
- const store = createSnackbarStore({ position: 'top-center' });
422
+ const store = createSnackbarStore({ position: 'top-center', tone: 'paper' });
240
423
 
241
424
  function App() {
242
425
  return (
@@ -248,11 +431,27 @@ function App() {
248
431
  }
249
432
  ```
250
433
 
434
+ ```ts
435
+ store.setConfig({ duration: 3000, layout: 'compact' }); // live default changes
436
+ const stop = store.subscribe(() => console.log(store.getSnapshot()));
437
+ ```
438
+
439
+ | Member | Description |
440
+ | --- | --- |
441
+ | `show / update / dismiss / dismissAll / promise` | Same semantics as the hook. |
442
+ | `subscribe(fn)` | Listen for any change; returns an unsubscribe function. |
443
+ | `getSnapshot()` / `getServerSnapshot()` | Current records (stable for `useSyncExternalStore`). |
444
+ | `getConfig()` / `setConfig(partial)` | Read or change defaults at runtime. |
445
+ | `pause(id, reason?)` / `resume(id, reason?)` | Reference-counted timer pause (hover, drag, hidden tab). |
446
+ | `minimize / expand / getMinimized` | Stack collapse state. |
447
+ | `notifyShown(id)` / `notifyExited(id)` | Lifecycle hooks the viewport calls. |
448
+ | `setPageHidden(hidden)` | Forward `document.visibilityState` changes. |
449
+
251
450
  ## Server-side rendering
252
451
 
253
452
  `SnackbarProvider` and `SnackbarHost` render nothing until mounted, and the
254
- store returns a stable server snapshot. No `window` access during render, so
255
- the components are safe in Next.js, Remix and other SSR frameworks.
453
+ store returns a stable server snapshot. No `window` access during render, so the
454
+ components are safe in Next.js, Remix and other SSR frameworks.
256
455
 
257
456
  ## Requirements
258
457
 
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,