react-snackbar-glass 1.0.0 → 1.2.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,39 @@ 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.2.0] - 2026-10-28
9
+
10
+ ### Added
11
+
12
+ - `compact` snackbars: single-line pill, no title or countdown hairline.
13
+ Available per toast (`compact: true`), on the provider
14
+ (`<SnackbarProvider compact />`) and in the store config.
15
+ - Minimize API: `toast.minimize()`, `toast.expand()`, `toast.isMinimized()`
16
+ (also on `useSnackbar()`), collapsing the whole stack into one summary pill
17
+ that expands on click.
18
+ - `minimizedLabel` host prop to customise the collapsed pill text.
19
+
20
+ ### Changed
21
+
22
+ - Demo homepage now defaults to the light theme.
23
+
24
+ ## [1.1.0] - 2026-10-28
25
+
26
+ ### Added
27
+
28
+ - Global `toast` API: callable (`toast('Saved')`), option objects
29
+ (`toast({ message, variant })`), every variant preset, plus `show`,
30
+ `update`, `dismiss`, `dismissAll`, `promise` and a `store` accessor.
31
+ - `getDefaultStore()` / `setDefaultStore()` to share or swap the store behind
32
+ the global API.
33
+ - `<SnackbarProvider>` now renders the shared default store when no `store`
34
+ prop is given, so `toast` works with zero wiring.
35
+
36
+ ### Changed
37
+
38
+ - Copy and docs rebranded from "premium" to "glass".
39
+ - Added `repository`, `homepage` and `bugs` metadata.
40
+
8
41
  ## [1.0.0] - 2026-10-28
9
42
 
10
43
  ### Added
package/README.md CHANGED
@@ -1,12 +1,14 @@
1
1
  # react-snackbar-glass
2
2
 
3
- A premium, glassmorphic snackbar system for React — queueing, promise-aware
3
+ A glassmorphic snackbar system for React — queueing, promise-aware
4
4
  states, swipe-to-dismiss and token-driven theming with **zero runtime
5
5
  dependencies**.
6
6
 
7
7
  - 🪟 **Glass or solid** surfaces, `xs` → `lg` sizes, six screen corners
8
+ - 🌍 **Global `toast`** — call it from anywhere, no hook required
8
9
  - ⏳ **Promise aware** — one call moves a snackbar through loading → success → error
9
10
  - 👆 **Swipe to dismiss**, hover/focus/document-hidden pause, countdown hairline
11
+ - 📦 **Compact + minimize** — one-line pills, or collapse the whole stack
10
12
  - ♿ **Accessible** — polite/assertive live regions, keyboard reachable actions, `prefers-reduced-motion`
11
13
  - 🧩 **Typed** — written in TypeScript, ships ESM + CJS + `.d.ts`
12
14
  - 🪶 **Tiny** — React is the only peer dependency
@@ -63,6 +65,35 @@ function Dashboard() {
63
65
  | `promise(promise, messages, options?)` | Drive a single snackbar through loading, success and error. |
64
66
  | `store` | Escape hatch: the underlying store for subscriptions and advanced updates. |
65
67
 
68
+ ### `toast` — global API
69
+
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.
73
+
74
+ ```ts
75
+ import { toast } from 'react-snackbar-glass';
76
+
77
+ toast('Draft saved');
78
+ toast.success('Deployment complete.', { title: 'v1.4.0 is live' });
79
+ toast.error('Payment declined', { action: { label: 'Retry', onClick: retry } });
80
+ ```
81
+
82
+ It mirrors `useSnackbar()` one-to-one — `show`, `update`, `dismiss`,
83
+ `dismissAll`, every variant preset, `promise`, and `store`:
84
+
85
+ ```ts
86
+ const id = toast.loading('Saving…');
87
+ await save();
88
+ toast.update(id, { variant: 'success', message: 'Saved', duration: 3000 });
89
+ ```
90
+
91
+ `<SnackbarProvider>` renders the shared store by default. Mount
92
+ `<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.
96
+
66
97
  ### Promise-aware flows
67
98
 
68
99
  ```tsx
@@ -130,6 +161,8 @@ the snackbar only closes after the handler settles (`closeOnClick`, default
130
161
  | `maxVisible` | `number` | `3` | Simultaneously visible cards per corner. |
131
162
  | `dismissible` | `boolean` | `true` | Show the close button. |
132
163
  | `progress` | `boolean` | `true` | Show the countdown hairline. |
164
+ | `compact` | `boolean` | `false` | Single-line pill for every snackbar. |
165
+ | `minimizedLabel` | `(count: number) => ReactNode` | `"3 notifications"` | Text on the collapsed pill. |
133
166
  | `pauseOnHover` | `boolean` | `true` | Freeze the timer while hovered. |
134
167
  | `pauseOnFocus` | `boolean` | `true` | Freeze the timer while focus is inside. |
135
168
  | `pauseOnDocumentHidden` | `boolean` | `true` | Pause when the tab is hidden. |
@@ -145,12 +178,39 @@ the snackbar only closes after the handler settles (`closeOnClick`, default
145
178
  ## `SnackbarOptions`
146
179
 
147
180
  `id`, `title`, `message`, `variant`, `duration`, `position`, `icon`,
148
- `action`, `dismissible`, `progress`, `tone`, `size`, `className`, `style`,
149
- `onOpen`, `onClose`.
181
+ `action`, `dismissible`, `progress`, `compact`, `tone`, `size`, `className`,
182
+ `style`, `onOpen`, `onClose`.
150
183
 
151
184
  Reusing an `id` updates (or revives) the existing snackbar instead of stacking
152
185
  a duplicate — ideal for progress-style flows.
153
186
 
187
+ ## Compact and minimized stacks
188
+
189
+ Two ways to reclaim screen space when snackbars would otherwise pile up.
190
+
191
+ **Compact** — a single-line pill per snackbar (no title, no hairline):
192
+
193
+ ```ts
194
+ toast.success('Draft saved', { compact: true });
195
+ snackbar.show({ message: 'Synced', compact: true });
196
+ ```
197
+
198
+ Set it once for every snackbar with `<SnackbarProvider compact />` or
199
+ `createSnackbarStore({ compact: true })`.
200
+
201
+ **Minimized** — collapse the whole stack into one summary pill
202
+ (`"3 notifications"`) that expands on click:
203
+
204
+ ```ts
205
+ toast.minimize(); // stack → one pill
206
+ toast.expand(); // back to full cards
207
+ toast.isMinimized(); // boolean
208
+ ```
209
+
210
+ `useSnackbar()` exposes the same three methods, and the store has
211
+ `setMinimized(true | false)` + `getMinimized()`. Customize the pill text with
212
+ `minimizedLabel={(count) => `${count} neue Meldungen`}`.
213
+
154
214
  ## Theming
155
215
 
156
216
  Colors, radii, blur and motion are CSS custom properties on `.prs-root`
package/dist/index.cjs CHANGED
@@ -11,6 +11,7 @@ var DEFAULT_CONFIG = {
11
11
  duration: 5e3,
12
12
  dismissible: true,
13
13
  progress: true,
14
+ compact: false,
14
15
  tone: "glass",
15
16
  size: "md",
16
17
  maxVisible: 3
@@ -50,6 +51,7 @@ function createSnackbarStore(initialConfig = {}) {
50
51
  } : null,
51
52
  ...initialConfig.dismissible !== void 0 ? { dismissible: initialConfig.dismissible } : null,
52
53
  ...initialConfig.progress !== void 0 ? { progress: initialConfig.progress } : null,
54
+ ...initialConfig.compact !== void 0 ? { compact: initialConfig.compact } : null,
53
55
  ...initialConfig.tone !== void 0 ? { tone: initialConfig.tone } : null,
54
56
  ...initialConfig.size !== void 0 ? { size: initialConfig.size } : null,
55
57
  ...initialConfig.maxVisible !== void 0 ? { maxVisible: Math.max(1, Math.floor(initialConfig.maxVisible)) } : null
@@ -60,6 +62,7 @@ function createSnackbarStore(initialConfig = {}) {
60
62
  const shown = /* @__PURE__ */ new Set();
61
63
  const pauseReasons = /* @__PURE__ */ new Map();
62
64
  let pageHidden = false;
65
+ let minimized = false;
63
66
  const emit = () => {
64
67
  for (const listener of listeners) listener();
65
68
  };
@@ -233,6 +236,7 @@ function createSnackbarStore(initialConfig = {}) {
233
236
  action: options.action ?? null,
234
237
  dismissible: options.dismissible ?? config.dismissible,
235
238
  progress: options.progress ?? config.progress,
239
+ compact: options.compact ?? config.compact,
236
240
  className: options.className,
237
241
  style: options.style,
238
242
  createdAt: Date.now(),
@@ -261,6 +265,7 @@ function createSnackbarStore(initialConfig = {}) {
261
265
  ...options.action !== void 0 ? { action: options.action } : null,
262
266
  ...options.dismissible !== void 0 ? { dismissible: options.dismissible } : null,
263
267
  ...options.progress !== void 0 ? { progress: options.progress } : null,
268
+ ...options.compact !== void 0 ? { compact: options.compact } : null,
264
269
  ...options.className !== void 0 ? { className: options.className } : null,
265
270
  ...options.style !== void 0 ? { style: options.style } : null,
266
271
  ...options.onOpen !== void 0 ? { onOpen: options.onOpen } : null,
@@ -318,6 +323,7 @@ function createSnackbarStore(initialConfig = {}) {
318
323
  } : null,
319
324
  ...patch.dismissible !== void 0 ? { dismissible: patch.dismissible } : null,
320
325
  ...patch.progress !== void 0 ? { progress: patch.progress } : null,
326
+ ...patch.compact !== void 0 ? { compact: patch.compact } : null,
321
327
  ...patch.tone !== void 0 ? { tone: patch.tone } : null,
322
328
  ...patch.size !== void 0 ? { size: patch.size } : null,
323
329
  ...patch.maxVisible !== void 0 ? { maxVisible: Math.max(1, Math.floor(patch.maxVisible)) } : null
@@ -372,9 +378,26 @@ function createSnackbarStore(initialConfig = {}) {
372
378
  resume,
373
379
  notifyShown,
374
380
  notifyExited,
375
- setPageHidden
381
+ setPageHidden,
382
+ getMinimized: () => minimized,
383
+ getServerMinimized: () => false,
384
+ setMinimized(next) {
385
+ if (minimized === next) return;
386
+ minimized = next;
387
+ emit();
388
+ }
376
389
  };
377
390
  }
391
+
392
+ // src/core/defaultStore.ts
393
+ var defaultStore = null;
394
+ function getDefaultStore() {
395
+ if (!defaultStore) defaultStore = createSnackbarStore();
396
+ return defaultStore;
397
+ }
398
+ function setDefaultStore(store) {
399
+ defaultStore = store;
400
+ }
378
401
  var strokeProps = {
379
402
  fill: "none",
380
403
  stroke: "currentColor",
@@ -607,7 +630,7 @@ function SnackbarItem({
607
630
  };
608
631
  const resolvedIcon = record.icon === void 0 ? icons?.[variant] ?? defaultIcons[variant] : record.icon;
609
632
  const showIcon = record.icon !== false;
610
- const showProgress = record.progress && Number.isFinite(duration) && duration > 0;
633
+ const showProgress = !record.compact && record.progress && Number.isFinite(duration) && duration > 0;
611
634
  const style = {
612
635
  ...record.style,
613
636
  "--prs-drag-x": `${dragX}px`,
@@ -627,6 +650,7 @@ function SnackbarItem({
627
650
  "data-dragging": dragging ? "true" : "false",
628
651
  "data-has-icon": showIcon ? "true" : "false",
629
652
  "data-dismissible": record.dismissible ? "true" : "false",
653
+ "data-compact": record.compact ? "true" : "false",
630
654
  "data-prs-id": record.id,
631
655
  "data-position": record.position,
632
656
  style,
@@ -726,6 +750,9 @@ var POSITIONS = [
726
750
  "bottom-center",
727
751
  "bottom-right"
728
752
  ];
753
+ function defaultMinimizedLabel(count) {
754
+ return `${count} notification${count === 1 ? "" : "s"}`;
755
+ }
729
756
  function SnackbarHost({
730
757
  store,
731
758
  maxVisible = 3,
@@ -743,6 +770,7 @@ function SnackbarHost({
743
770
  closeIcon,
744
771
  closeLabel = "Dismiss notification",
745
772
  regionLabel = "Notifications",
773
+ minimizedLabel = defaultMinimizedLabel,
746
774
  className,
747
775
  style,
748
776
  zIndex = 2147483e3,
@@ -753,6 +781,11 @@ function SnackbarHost({
753
781
  store.getSnapshot,
754
782
  store.getServerSnapshot
755
783
  );
784
+ const minimized = react.useSyncExternalStore(
785
+ store.subscribe,
786
+ store.getMinimized,
787
+ store.getServerMinimized
788
+ );
756
789
  const [portalNode, setPortalNode] = react.useState(null);
757
790
  const regionRefs = react.useRef(/* @__PURE__ */ new Map());
758
791
  const topsRef = react.useRef(/* @__PURE__ */ new Map());
@@ -816,6 +849,7 @@ function SnackbarHost({
816
849
  else byPosition.set(record.position, [record]);
817
850
  }
818
851
  const limit = Math.max(1, Math.floor(maxVisible));
852
+ const minimizedPosition = records[0]?.position ?? null;
819
853
  const rootStyle = {
820
854
  "--prs-width": `${width}px`,
821
855
  "--prs-offset": `${offset}px`,
@@ -834,6 +868,32 @@ function SnackbarHost({
834
868
  children: POSITIONS.map((position) => {
835
869
  const items = byPosition.get(position);
836
870
  if (!items || items.length === 0) return null;
871
+ if (minimized) {
872
+ if (position !== minimizedPosition) return null;
873
+ return /* @__PURE__ */ jsxRuntime.jsx(
874
+ "section",
875
+ {
876
+ className: "prs-region prs-region--minimized",
877
+ "data-position": position,
878
+ "aria-label": regionLabel,
879
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
880
+ "button",
881
+ {
882
+ type: "button",
883
+ className: "prs-minimized",
884
+ "data-count": items.length,
885
+ onClick: () => store.setMinimized(false),
886
+ title: "Expand notifications",
887
+ children: [
888
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "prs-minimized__dot", "aria-hidden": "true" }),
889
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "prs-minimized__count", children: minimizedLabel(items.length) })
890
+ ]
891
+ }
892
+ )
893
+ },
894
+ position
895
+ );
896
+ }
837
897
  const visible = items.slice(0, limit);
838
898
  return /* @__PURE__ */ jsxRuntime.jsx(
839
899
  "section",
@@ -877,20 +937,18 @@ function SnackbarProvider({
877
937
  duration,
878
938
  dismissible,
879
939
  progress,
940
+ compact,
880
941
  tone,
881
942
  size,
882
943
  maxVisible,
883
944
  ...hostProps
884
945
  }) {
885
946
  const store = react.useMemo(
886
- () => providedStore ?? createSnackbarStore({
887
- position,
888
- duration,
889
- dismissible,
890
- progress,
891
- tone,
892
- size
893
- }),
947
+ () => (
948
+ // Falling back to the shared default store is what makes the
949
+ // module-level `toast` API render without extra wiring.
950
+ providedStore ?? getDefaultStore()
951
+ ),
894
952
  // The store is intentionally created once per provider instance.
895
953
  // Subsequent config changes are pushed below.
896
954
  [providedStore]
@@ -901,31 +959,45 @@ function SnackbarProvider({
901
959
  duration,
902
960
  dismissible,
903
961
  progress,
962
+ compact,
904
963
  tone,
905
964
  size,
906
965
  maxVisible
907
966
  });
908
- }, [store, position, duration, dismissible, progress, tone, size, maxVisible]);
967
+ }, [
968
+ store,
969
+ position,
970
+ duration,
971
+ dismissible,
972
+ progress,
973
+ compact,
974
+ tone,
975
+ size,
976
+ maxVisible
977
+ ]);
909
978
  const api = react.useMemo(() => {
910
- const preset = (variant) => (message, options = {}) => store.show({ ...options, message, variant });
979
+ const preset2 = (variant) => (message, options = {}) => store.show({ ...options, message, variant });
911
980
  return {
912
981
  show: store.show,
913
982
  update: store.update,
914
983
  dismiss: store.dismiss,
915
984
  dismissAll: store.dismissAll,
916
985
  promise: store.promise,
917
- success: preset("success"),
918
- error: preset("error"),
919
- warning: preset("warning"),
920
- info: preset("info"),
921
- neutral: preset("neutral"),
922
- custom: preset("custom"),
986
+ success: preset2("success"),
987
+ error: preset2("error"),
988
+ warning: preset2("warning"),
989
+ info: preset2("info"),
990
+ neutral: preset2("neutral"),
991
+ custom: preset2("custom"),
923
992
  loading: (message, options = {}) => store.show({
924
993
  ...options,
925
994
  message,
926
995
  variant: "loading",
927
996
  duration: options.duration !== void 0 ? options.duration : null
928
997
  }),
998
+ minimize: () => store.setMinimized(true),
999
+ expand: () => store.setMinimized(false),
1000
+ isMinimized: () => store.getMinimized(),
929
1001
  store
930
1002
  };
931
1003
  }, [store]);
@@ -957,10 +1029,56 @@ function useSnackbar() {
957
1029
  function useSnackbarStore() {
958
1030
  return useSnackbarContext().store;
959
1031
  }
1032
+ function isOptionsObject(value) {
1033
+ return typeof value === "object" && value !== null && !Array.isArray(value) && !react.isValidElement(value) && !("$$typeof" in value);
1034
+ }
1035
+ function preset(variant) {
1036
+ return (message, options = {}) => getDefaultStore().show({ ...options, message, variant });
1037
+ }
1038
+ function createToast() {
1039
+ const toast2 = ((content, options) => {
1040
+ if (options === void 0 && isOptionsObject(content)) {
1041
+ return getDefaultStore().show(content);
1042
+ }
1043
+ return getDefaultStore().show({
1044
+ ...options,
1045
+ message: content
1046
+ });
1047
+ });
1048
+ toast2.show = (options) => getDefaultStore().show(options);
1049
+ toast2.update = (id, options) => getDefaultStore().update(id, options);
1050
+ toast2.dismiss = (id, reason) => getDefaultStore().dismiss(id, reason);
1051
+ toast2.dismissAll = (reason) => getDefaultStore().dismissAll(reason);
1052
+ toast2.success = preset("success");
1053
+ toast2.error = preset("error");
1054
+ toast2.warning = preset("warning");
1055
+ toast2.info = preset("info");
1056
+ toast2.neutral = preset("neutral");
1057
+ toast2.custom = preset("custom");
1058
+ toast2.loading = (message, options = {}) => getDefaultStore().show({
1059
+ ...options,
1060
+ message,
1061
+ variant: "loading",
1062
+ duration: options.duration !== void 0 ? options.duration : null
1063
+ });
1064
+ toast2.promise = (input, messages, options) => getDefaultStore().promise(input, messages, options);
1065
+ toast2.minimize = () => getDefaultStore().setMinimized(true);
1066
+ toast2.expand = () => getDefaultStore().setMinimized(false);
1067
+ toast2.isMinimized = () => getDefaultStore().getMinimized();
1068
+ Object.defineProperty(toast2, "store", {
1069
+ get: getDefaultStore,
1070
+ enumerable: true
1071
+ });
1072
+ return toast2;
1073
+ }
1074
+ var toast = createToast();
960
1075
 
961
1076
  exports.SnackbarHost = SnackbarHost;
962
1077
  exports.SnackbarProvider = SnackbarProvider;
963
1078
  exports.createSnackbarStore = createSnackbarStore;
1079
+ exports.getDefaultStore = getDefaultStore;
1080
+ exports.setDefaultStore = setDefaultStore;
1081
+ exports.toast = toast;
964
1082
  exports.useSnackbar = useSnackbar;
965
1083
  exports.useSnackbarStore = useSnackbarStore;
966
1084
  //# sourceMappingURL=index.cjs.map