@sinlungtech/push-client 0.1.1 → 0.1.3

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/index.d.mts CHANGED
@@ -86,6 +86,28 @@ interface PushProviderProps {
86
86
  autoSubscribe?: boolean;
87
87
  children: React.ReactNode;
88
88
  }
89
+ interface PushStatusIndicatorProps {
90
+ /**
91
+ * Optional title shown in the indicator.
92
+ * Default: "Notifications"
93
+ */
94
+ title?: string;
95
+ /**
96
+ * Show indicator even when notifications are fully enabled.
97
+ * Default: true
98
+ */
99
+ showWhenEnabled?: boolean;
100
+ /**
101
+ * Corner placement for the floating indicator.
102
+ * Default: "bottom-right"
103
+ */
104
+ position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
105
+ /**
106
+ * Optional z-index override.
107
+ * Default: 2147483000
108
+ */
109
+ zIndex?: number;
110
+ }
89
111
 
90
112
  interface PushContextValue {
91
113
  /** Whether the browser supports Web Push */
@@ -131,6 +153,8 @@ interface PushContextValue {
131
153
  declare function PushProvider({ vapidKey, serviceWorkerPath, onSubscribe, onUnsubscribe, chatServerUrl, getToken, subscriptionsEndpoint, live, onLiveNotification, showInPageAlerts, inPageAlertDurationMs, autoSubscribe, children, }: PushProviderProps): react_jsx_runtime.JSX.Element;
132
154
  declare function usePush(): PushContextValue;
133
155
 
156
+ declare function PushStatusIndicator({ title, showWhenEnabled, position, zIndex, }: PushStatusIndicatorProps): react_jsx_runtime.JSX.Element | null;
157
+
134
158
  declare function urlBase64ToUint8Array(base64String: string): ArrayBuffer;
135
159
 
136
- export { type PushPayload, PushProvider, type PushProviderProps, type PushSubscription, urlBase64ToUint8Array, usePush };
160
+ export { type PushPayload, PushProvider, type PushProviderProps, PushStatusIndicator, type PushStatusIndicatorProps, type PushSubscription, urlBase64ToUint8Array, usePush };
package/dist/index.d.ts CHANGED
@@ -86,6 +86,28 @@ interface PushProviderProps {
86
86
  autoSubscribe?: boolean;
87
87
  children: React.ReactNode;
88
88
  }
89
+ interface PushStatusIndicatorProps {
90
+ /**
91
+ * Optional title shown in the indicator.
92
+ * Default: "Notifications"
93
+ */
94
+ title?: string;
95
+ /**
96
+ * Show indicator even when notifications are fully enabled.
97
+ * Default: true
98
+ */
99
+ showWhenEnabled?: boolean;
100
+ /**
101
+ * Corner placement for the floating indicator.
102
+ * Default: "bottom-right"
103
+ */
104
+ position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
105
+ /**
106
+ * Optional z-index override.
107
+ * Default: 2147483000
108
+ */
109
+ zIndex?: number;
110
+ }
89
111
 
90
112
  interface PushContextValue {
91
113
  /** Whether the browser supports Web Push */
@@ -131,6 +153,8 @@ interface PushContextValue {
131
153
  declare function PushProvider({ vapidKey, serviceWorkerPath, onSubscribe, onUnsubscribe, chatServerUrl, getToken, subscriptionsEndpoint, live, onLiveNotification, showInPageAlerts, inPageAlertDurationMs, autoSubscribe, children, }: PushProviderProps): react_jsx_runtime.JSX.Element;
132
154
  declare function usePush(): PushContextValue;
133
155
 
156
+ declare function PushStatusIndicator({ title, showWhenEnabled, position, zIndex, }: PushStatusIndicatorProps): react_jsx_runtime.JSX.Element | null;
157
+
134
158
  declare function urlBase64ToUint8Array(base64String: string): ArrayBuffer;
135
159
 
136
- export { type PushPayload, PushProvider, type PushProviderProps, type PushSubscription, urlBase64ToUint8Array, usePush };
160
+ export { type PushPayload, PushProvider, type PushProviderProps, PushStatusIndicator, type PushStatusIndicatorProps, type PushSubscription, urlBase64ToUint8Array, usePush };
package/dist/index.js CHANGED
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  PushProvider: () => PushProvider,
34
+ PushStatusIndicator: () => PushStatusIndicator,
34
35
  urlBase64ToUint8Array: () => urlBase64ToUint8Array,
35
36
  usePush: () => usePush
36
37
  });
@@ -317,8 +318,8 @@ function PushProvider({
317
318
  tag: payload.tag
318
319
  };
319
320
  onLiveNotification?.(normalized);
320
- if (!onLiveNotification && showInPageAlerts && normalized.body) {
321
- pushInPageAlert(normalized.title, normalized.body);
321
+ if (!onLiveNotification && showInPageAlerts) {
322
+ pushInPageAlert(normalized.title, normalized.body || "You have a new notification.");
322
323
  }
323
324
  if (showSystem && typeof window !== "undefined" && Notification.permission === "granted") {
324
325
  new Notification(normalized.title, {
@@ -383,9 +384,147 @@ function PushProvider({
383
384
  function usePush() {
384
385
  return (0, import_react.useContext)(PushContext);
385
386
  }
387
+
388
+ // src/PushStatusIndicator.tsx
389
+ var import_react2 = require("react");
390
+ var import_jsx_runtime2 = require("react/jsx-runtime");
391
+ var POSITION_STYLES = {
392
+ "bottom-right": { right: 16, bottom: 16 },
393
+ "bottom-left": { left: 16, bottom: 16 },
394
+ "top-right": { right: 16, top: 16 },
395
+ "top-left": { left: 16, top: 16 }
396
+ };
397
+ function PushStatusIndicator({
398
+ title = "Notifications",
399
+ showWhenEnabled = true,
400
+ position = "bottom-right",
401
+ zIndex = 2147483e3
402
+ }) {
403
+ const { isSupported: isSupported2, permission, isSubscribed, subscribe, unsubscribe, error } = usePush();
404
+ const [busy, setBusy] = (0, import_react2.useState)(false);
405
+ const state = (0, import_react2.useMemo)(() => {
406
+ if (!isSupported2) return { label: "Not supported", tone: "#6b7280" };
407
+ if (permission === "denied") return { label: "Blocked in browser", tone: "#dc2626" };
408
+ if (permission === "granted" && isSubscribed) return { label: "Enabled", tone: "#16a34a" };
409
+ return { label: "Not enabled", tone: "#d97706" };
410
+ }, [isSupported2, permission, isSubscribed]);
411
+ if (!isSupported2) return null;
412
+ if (!showWhenEnabled && permission === "granted" && isSubscribed) return null;
413
+ const canEnable = permission !== "granted" || !isSubscribed;
414
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
415
+ "div",
416
+ {
417
+ style: {
418
+ ...POSITION_STYLES[position],
419
+ position: "fixed",
420
+ zIndex,
421
+ width: 320,
422
+ maxWidth: "calc(100vw - 32px)",
423
+ borderRadius: 12,
424
+ border: "1px solid #e5e7eb",
425
+ background: "#ffffff",
426
+ boxShadow: "0 10px 30px rgba(0, 0, 0, 0.16)",
427
+ padding: 12,
428
+ fontFamily: "system-ui, sans-serif"
429
+ },
430
+ children: [
431
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }, children: [
432
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: { margin: 0, fontSize: 13, fontWeight: 700, color: "#111827" }, children: title }),
433
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
434
+ "span",
435
+ {
436
+ style: {
437
+ display: "inline-flex",
438
+ alignItems: "center",
439
+ gap: 6,
440
+ fontSize: 11,
441
+ color: "#374151",
442
+ background: "#f9fafb",
443
+ border: "1px solid #e5e7eb",
444
+ borderRadius: 999,
445
+ padding: "2px 8px"
446
+ },
447
+ children: [
448
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
449
+ "span",
450
+ {
451
+ style: {
452
+ width: 8,
453
+ height: 8,
454
+ borderRadius: 999,
455
+ display: "inline-block",
456
+ background: state.tone
457
+ }
458
+ }
459
+ ),
460
+ state.label
461
+ ]
462
+ }
463
+ )
464
+ ] }),
465
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: { margin: "8px 0 0", fontSize: 12, color: "#6b7280", lineHeight: 1.35 }, children: permission === "denied" ? "Notifications are blocked. Re-enable in browser site settings, then click Retry." : "Enable notifications to receive alerts even when this tab is not active." }),
466
+ error && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: { margin: "8px 0 0", fontSize: 12, color: "#dc2626", lineHeight: 1.35 }, children: error.message || "Push setup failed" }),
467
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { marginTop: 10, display: "flex", gap: 8 }, children: canEnable ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
468
+ "button",
469
+ {
470
+ type: "button",
471
+ disabled: busy,
472
+ onClick: async () => {
473
+ setBusy(true);
474
+ try {
475
+ await subscribe();
476
+ } finally {
477
+ setBusy(false);
478
+ }
479
+ },
480
+ style: {
481
+ border: "none",
482
+ borderRadius: 8,
483
+ padding: "7px 10px",
484
+ fontSize: 12,
485
+ fontWeight: 600,
486
+ color: "#fff",
487
+ background: "#111827",
488
+ cursor: busy ? "not-allowed" : "pointer",
489
+ opacity: busy ? 0.7 : 1
490
+ },
491
+ children: busy ? "Working..." : permission === "denied" ? "Retry" : "Enable"
492
+ }
493
+ ) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
494
+ "button",
495
+ {
496
+ type: "button",
497
+ disabled: busy,
498
+ onClick: async () => {
499
+ setBusy(true);
500
+ try {
501
+ await unsubscribe();
502
+ } finally {
503
+ setBusy(false);
504
+ }
505
+ },
506
+ style: {
507
+ border: "1px solid #e5e7eb",
508
+ borderRadius: 8,
509
+ padding: "7px 10px",
510
+ fontSize: 12,
511
+ fontWeight: 600,
512
+ color: "#374151",
513
+ background: "#fff",
514
+ cursor: busy ? "not-allowed" : "pointer",
515
+ opacity: busy ? 0.7 : 1
516
+ },
517
+ children: busy ? "Working..." : "Disable"
518
+ }
519
+ ) })
520
+ ]
521
+ }
522
+ );
523
+ }
386
524
  // Annotate the CommonJS export names for ESM import in node:
387
525
  0 && (module.exports = {
388
526
  PushProvider,
527
+ PushStatusIndicator,
389
528
  urlBase64ToUint8Array,
390
529
  usePush
391
530
  });
package/dist/index.mjs CHANGED
@@ -279,8 +279,8 @@ function PushProvider({
279
279
  tag: payload.tag
280
280
  };
281
281
  onLiveNotification?.(normalized);
282
- if (!onLiveNotification && showInPageAlerts && normalized.body) {
283
- pushInPageAlert(normalized.title, normalized.body);
282
+ if (!onLiveNotification && showInPageAlerts) {
283
+ pushInPageAlert(normalized.title, normalized.body || "You have a new notification.");
284
284
  }
285
285
  if (showSystem && typeof window !== "undefined" && Notification.permission === "granted") {
286
286
  new Notification(normalized.title, {
@@ -345,8 +345,146 @@ function PushProvider({
345
345
  function usePush() {
346
346
  return useContext(PushContext);
347
347
  }
348
+
349
+ // src/PushStatusIndicator.tsx
350
+ import { useMemo, useState as useState2 } from "react";
351
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
352
+ var POSITION_STYLES = {
353
+ "bottom-right": { right: 16, bottom: 16 },
354
+ "bottom-left": { left: 16, bottom: 16 },
355
+ "top-right": { right: 16, top: 16 },
356
+ "top-left": { left: 16, top: 16 }
357
+ };
358
+ function PushStatusIndicator({
359
+ title = "Notifications",
360
+ showWhenEnabled = true,
361
+ position = "bottom-right",
362
+ zIndex = 2147483e3
363
+ }) {
364
+ const { isSupported: isSupported2, permission, isSubscribed, subscribe, unsubscribe, error } = usePush();
365
+ const [busy, setBusy] = useState2(false);
366
+ const state = useMemo(() => {
367
+ if (!isSupported2) return { label: "Not supported", tone: "#6b7280" };
368
+ if (permission === "denied") return { label: "Blocked in browser", tone: "#dc2626" };
369
+ if (permission === "granted" && isSubscribed) return { label: "Enabled", tone: "#16a34a" };
370
+ return { label: "Not enabled", tone: "#d97706" };
371
+ }, [isSupported2, permission, isSubscribed]);
372
+ if (!isSupported2) return null;
373
+ if (!showWhenEnabled && permission === "granted" && isSubscribed) return null;
374
+ const canEnable = permission !== "granted" || !isSubscribed;
375
+ return /* @__PURE__ */ jsxs2(
376
+ "div",
377
+ {
378
+ style: {
379
+ ...POSITION_STYLES[position],
380
+ position: "fixed",
381
+ zIndex,
382
+ width: 320,
383
+ maxWidth: "calc(100vw - 32px)",
384
+ borderRadius: 12,
385
+ border: "1px solid #e5e7eb",
386
+ background: "#ffffff",
387
+ boxShadow: "0 10px 30px rgba(0, 0, 0, 0.16)",
388
+ padding: 12,
389
+ fontFamily: "system-ui, sans-serif"
390
+ },
391
+ children: [
392
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }, children: [
393
+ /* @__PURE__ */ jsx2("p", { style: { margin: 0, fontSize: 13, fontWeight: 700, color: "#111827" }, children: title }),
394
+ /* @__PURE__ */ jsxs2(
395
+ "span",
396
+ {
397
+ style: {
398
+ display: "inline-flex",
399
+ alignItems: "center",
400
+ gap: 6,
401
+ fontSize: 11,
402
+ color: "#374151",
403
+ background: "#f9fafb",
404
+ border: "1px solid #e5e7eb",
405
+ borderRadius: 999,
406
+ padding: "2px 8px"
407
+ },
408
+ children: [
409
+ /* @__PURE__ */ jsx2(
410
+ "span",
411
+ {
412
+ style: {
413
+ width: 8,
414
+ height: 8,
415
+ borderRadius: 999,
416
+ display: "inline-block",
417
+ background: state.tone
418
+ }
419
+ }
420
+ ),
421
+ state.label
422
+ ]
423
+ }
424
+ )
425
+ ] }),
426
+ /* @__PURE__ */ jsx2("p", { style: { margin: "8px 0 0", fontSize: 12, color: "#6b7280", lineHeight: 1.35 }, children: permission === "denied" ? "Notifications are blocked. Re-enable in browser site settings, then click Retry." : "Enable notifications to receive alerts even when this tab is not active." }),
427
+ error && /* @__PURE__ */ jsx2("p", { style: { margin: "8px 0 0", fontSize: 12, color: "#dc2626", lineHeight: 1.35 }, children: error.message || "Push setup failed" }),
428
+ /* @__PURE__ */ jsx2("div", { style: { marginTop: 10, display: "flex", gap: 8 }, children: canEnable ? /* @__PURE__ */ jsx2(
429
+ "button",
430
+ {
431
+ type: "button",
432
+ disabled: busy,
433
+ onClick: async () => {
434
+ setBusy(true);
435
+ try {
436
+ await subscribe();
437
+ } finally {
438
+ setBusy(false);
439
+ }
440
+ },
441
+ style: {
442
+ border: "none",
443
+ borderRadius: 8,
444
+ padding: "7px 10px",
445
+ fontSize: 12,
446
+ fontWeight: 600,
447
+ color: "#fff",
448
+ background: "#111827",
449
+ cursor: busy ? "not-allowed" : "pointer",
450
+ opacity: busy ? 0.7 : 1
451
+ },
452
+ children: busy ? "Working..." : permission === "denied" ? "Retry" : "Enable"
453
+ }
454
+ ) : /* @__PURE__ */ jsx2(
455
+ "button",
456
+ {
457
+ type: "button",
458
+ disabled: busy,
459
+ onClick: async () => {
460
+ setBusy(true);
461
+ try {
462
+ await unsubscribe();
463
+ } finally {
464
+ setBusy(false);
465
+ }
466
+ },
467
+ style: {
468
+ border: "1px solid #e5e7eb",
469
+ borderRadius: 8,
470
+ padding: "7px 10px",
471
+ fontSize: 12,
472
+ fontWeight: 600,
473
+ color: "#374151",
474
+ background: "#fff",
475
+ cursor: busy ? "not-allowed" : "pointer",
476
+ opacity: busy ? 0.7 : 1
477
+ },
478
+ children: busy ? "Working..." : "Disable"
479
+ }
480
+ ) })
481
+ ]
482
+ }
483
+ );
484
+ }
348
485
  export {
349
486
  PushProvider,
487
+ PushStatusIndicator,
350
488
  urlBase64ToUint8Array,
351
489
  usePush
352
490
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sinlungtech/push-client",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Web Push notification client for Next.js — service worker registration, permission handling, and subscription management",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",