@codingfactory/notify-kit-client 0.1.0 → 0.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/dist/index.js CHANGED
@@ -1,6 +1,24 @@
1
1
  import { useNotifyKitStore } from './chunk-7LQBNDRD.js';
2
2
  export { InvalidSnoozeDurationError, NOTIFY_KIT_MODALS_KEY, NOTIFY_KIT_STORE_KEY, provideNotifyKitModals, provideNotifyKitStore, resetNotifyKitStore, useNotifyKitModals, useNotifyKitStore } from './chunk-7LQBNDRD.js';
3
- import { provide, inject, ref, computed, onMounted, onUnmounted, watch } from 'vue';
3
+ import { provide, inject, computed, ref, onMounted, onUnmounted, watch, reactive, readonly } from 'vue';
4
+
5
+ // src/types/modal.ts
6
+ function isModalSpec(value) {
7
+ if (typeof value !== "object" || value === null) {
8
+ return false;
9
+ }
10
+ const obj = value;
11
+ if (typeof obj["enabled"] !== "boolean") return false;
12
+ if (typeof obj["requires_ack"] !== "boolean") return false;
13
+ if (!Array.isArray(obj["snooze_options_minutes"])) return false;
14
+ const snoozeOptions = obj["snooze_options_minutes"];
15
+ for (const option of snoozeOptions) {
16
+ if (typeof option !== "number" || !Number.isInteger(option) || option < 1) {
17
+ return false;
18
+ }
19
+ }
20
+ return true;
21
+ }
4
22
 
5
23
  // src/types/notification.ts
6
24
  var NOTIFICATION_CATEGORIES = [
@@ -46,38 +64,49 @@ function isNotifyKitNotification(value) {
46
64
  return false;
47
65
  }
48
66
  }
49
- if (obj["modal"] !== null && typeof obj["modal"] !== "object") return false;
67
+ if (obj["modal"] !== null && !isModalSpec(obj["modal"])) return false;
50
68
  return true;
51
69
  }
52
70
  function isModalEnabled(notification) {
53
71
  return notification.modal?.enabled ?? false;
54
72
  }
55
73
 
56
- // src/types/modal.ts
57
- function isModalSpec(value) {
74
+ // src/types/preferences.ts
75
+ function isBooleanRecord(value) {
58
76
  if (typeof value !== "object" || value === null) {
59
77
  return false;
60
78
  }
61
- const obj = value;
62
- if (typeof obj["enabled"] !== "boolean") return false;
63
- if (typeof obj["requires_ack"] !== "boolean") return false;
64
- if (!Array.isArray(obj["snooze_options_minutes"])) return false;
65
- const snoozeOptions = obj["snooze_options_minutes"];
66
- for (const option of snoozeOptions) {
67
- if (typeof option !== "number" || !Number.isInteger(option) || option < 1) {
68
- return false;
69
- }
79
+ return Object.values(value).every((entry) => typeof entry === "boolean");
80
+ }
81
+ function isStringRecord(value) {
82
+ if (typeof value !== "object" || value === null) {
83
+ return false;
70
84
  }
71
- return true;
85
+ return Object.values(value).every((entry) => typeof entry === "string");
72
86
  }
73
-
74
- // src/types/preferences.ts
75
- function isChannelPreferences(value) {
87
+ function isStringArray(value) {
88
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
89
+ }
90
+ function isNotifyKitQuietHours(value) {
91
+ if (typeof value !== "object" || value === null) {
92
+ return false;
93
+ }
94
+ const obj = value;
95
+ return typeof obj["start"] === "string" && typeof obj["end"] === "string";
96
+ }
97
+ function isNotifyKitPreferenceSettings(value) {
76
98
  if (typeof value !== "object" || value === null) {
77
99
  return false;
78
100
  }
79
101
  const obj = value;
80
- return typeof obj["database"] === "boolean" && typeof obj["broadcast"] === "boolean" && typeof obj["mail"] === "boolean";
102
+ const digest = obj["digest"];
103
+ if (typeof digest !== "object" || digest === null) {
104
+ return false;
105
+ }
106
+ const digestRecord = digest;
107
+ const quietHours = digestRecord["quiet_hours"];
108
+ const categories = digestRecord["categories"];
109
+ return isBooleanRecord(obj["channels"]) && isStringRecord(obj["channel_modes"]) && isBooleanRecord(obj["topics"]) && isBooleanRecord(obj["email_preferences"]) && isStringArray(obj["keyword_rules"]) && typeof digestRecord["enabled"] === "boolean" && (digestRecord["frequency"] === "daily" || digestRecord["frequency"] === "weekly") && (categories === null || isStringArray(categories)) && (digestRecord["workspace_id"] === null || typeof digestRecord["workspace_id"] === "string") && (digestRecord["timezone"] === null || typeof digestRecord["timezone"] === "string") && typeof digestRecord["hour"] === "number" && (quietHours === null || isNotifyKitQuietHours(quietHours)) && isStringArray(obj["available_channels"]) && isStringArray(obj["available_topics"]) && isStringArray(obj["available_email_preferences"]);
81
110
  }
82
111
 
83
112
  // src/types/api.ts
@@ -103,12 +132,146 @@ function isUnreadCountResponse(value) {
103
132
  return typeof obj["count"] === "number";
104
133
  }
105
134
 
135
+ // src/client/normalizeNotification.ts
136
+ var RESERVED_PAYLOAD_KEYS = /* @__PURE__ */ new Set([
137
+ "id",
138
+ "key",
139
+ "category",
140
+ "level",
141
+ "title",
142
+ "body",
143
+ "action_url",
144
+ "workspace_id",
145
+ "modal",
146
+ "group_key",
147
+ "idempotency_key",
148
+ "read_at",
149
+ "created_at",
150
+ "data"
151
+ ]);
152
+ function isRecord(value) {
153
+ return typeof value === "object" && value !== null;
154
+ }
155
+ function nullableString(value) {
156
+ return typeof value === "string" && value.trim() !== "" ? value : null;
157
+ }
158
+ function stringOrFallback(value, fallback) {
159
+ return typeof value === "string" && value.trim() !== "" ? value : fallback;
160
+ }
161
+ function inferCategory(key) {
162
+ if (key.startsWith("workspace.")) {
163
+ return "workspace";
164
+ }
165
+ if (key.startsWith("security.")) {
166
+ return "security";
167
+ }
168
+ if (key.startsWith("orders.")) {
169
+ return "orders";
170
+ }
171
+ if (key.startsWith("social.") || key.includes("mention") || key.includes("comment")) {
172
+ return "social";
173
+ }
174
+ if (key.startsWith("marketing.")) {
175
+ return "marketing";
176
+ }
177
+ return "system";
178
+ }
179
+ function inferLevel(value) {
180
+ if (isValidLevel(value)) {
181
+ return value;
182
+ }
183
+ return "info";
184
+ }
185
+ function buildUser(payload) {
186
+ const actorId = nullableString(payload["actor_id"]);
187
+ if (actorId === null) {
188
+ return void 0;
189
+ }
190
+ const user = {
191
+ id: actorId,
192
+ name: stringOrFallback(payload["actor_name"], "Someone"),
193
+ handle: nullableString(payload["actor_handle"]) ?? ""
194
+ };
195
+ const avatar = nullableString(payload["actor_avatar"]);
196
+ if (avatar !== null) {
197
+ user["avatar"] = avatar;
198
+ }
199
+ return user;
200
+ }
201
+ function normalizeApiNotification(raw) {
202
+ if (isNotifyKitNotification(raw)) {
203
+ return raw;
204
+ }
205
+ const rawRecord = isRecord(raw) ? {
206
+ id: stringOrFallback(raw["id"], ""),
207
+ type: nullableString(raw["type"]) ?? void 0,
208
+ data: isRecord(raw["data"]) ? raw["data"] : {},
209
+ read_at: nullableString(raw["read_at"]),
210
+ created_at: nullableString(raw["created_at"]) ?? void 0
211
+ } : {
212
+ id: "",
213
+ type: void 0,
214
+ data: {},
215
+ read_at: void 0,
216
+ created_at: void 0
217
+ };
218
+ const payload = isRecord(rawRecord.data) ? rawRecord.data : {};
219
+ const nestedData = isRecord(payload["data"]) ? payload["data"] : {};
220
+ const key = stringOrFallback(payload["key"], stringOrFallback(rawRecord.type, "system.notification"));
221
+ const category = isValidCategory(payload["category"]) ? payload["category"] : inferCategory(key);
222
+ const title = stringOrFallback(payload["title"], stringOrFallback(payload["message"], "Notification"));
223
+ const body = stringOrFallback(payload["body"], stringOrFallback(payload["message"], title));
224
+ const actionUrl = nullableString(payload["action_url"]) ?? nullableString(payload["link"]);
225
+ const user = buildUser(payload);
226
+ const data = {
227
+ ...nestedData
228
+ };
229
+ for (const [entryKey, entryValue] of Object.entries(payload)) {
230
+ if (entryKey === "data" || RESERVED_PAYLOAD_KEYS.has(entryKey)) {
231
+ continue;
232
+ }
233
+ data[entryKey] = entryValue;
234
+ }
235
+ if (typeof payload["key"] === "string") {
236
+ data["key"] = payload["key"];
237
+ }
238
+ if (typeof payload["title"] === "string") {
239
+ data["title"] = payload["title"];
240
+ }
241
+ data["body"] = body;
242
+ data["message"] = body;
243
+ if (actionUrl !== null) {
244
+ data["action_url"] = actionUrl;
245
+ data["link"] = actionUrl;
246
+ }
247
+ if (user !== void 0) {
248
+ data["user"] = user;
249
+ }
250
+ return {
251
+ id: stringOrFallback(rawRecord.id, ""),
252
+ key,
253
+ category,
254
+ level: inferLevel(payload["level"]),
255
+ title,
256
+ body,
257
+ action_url: actionUrl,
258
+ workspace_id: nullableString(payload["workspace_id"]),
259
+ modal: isModalSpec(payload["modal"]) ? payload["modal"] : null,
260
+ group_key: nullableString(payload["group_key"]),
261
+ idempotency_key: nullableString(payload["idempotency_key"]),
262
+ data,
263
+ read_at: rawRecord.read_at ?? null,
264
+ created_at: stringOrFallback(rawRecord.created_at, (/* @__PURE__ */ new Date()).toISOString())
265
+ };
266
+ }
267
+
106
268
  // src/client/NotifyKitClient.ts
107
269
  var NotifyKitApiError = class extends Error {
108
- constructor(message, status, errors) {
270
+ constructor(message, status, errors, headers) {
109
271
  super(message);
110
272
  this.status = status;
111
273
  this.errors = errors;
274
+ this.headers = headers;
112
275
  }
113
276
  name = "NotifyKitApiError";
114
277
  };
@@ -140,7 +303,11 @@ var NotifyKitClient = class {
140
303
  const queryParams = this.buildNotificationParams(params);
141
304
  const queryString = queryParams.toString();
142
305
  const url = queryString.length > 0 ? `/notifications?${queryString}` : "/notifications";
143
- return this.request("GET", url);
306
+ const response = await this.request("GET", url);
307
+ return {
308
+ data: response.data.map((notification) => normalizeApiNotification(notification)),
309
+ meta: response.meta
310
+ };
144
311
  }
145
312
  /**
146
313
  * Get the count of unread notifications.
@@ -151,11 +318,21 @@ var NotifyKitClient = class {
151
318
  "/notifications/unread-count"
152
319
  );
153
320
  }
321
+ /**
322
+ * Get a single notification for the current user.
323
+ */
324
+ async getNotification(id) {
325
+ const response = await this.request(
326
+ "GET",
327
+ `/notifications/${id}`
328
+ );
329
+ return normalizeApiNotification(response.data);
330
+ }
154
331
  /**
155
332
  * Mark a notification as read.
156
333
  */
157
334
  async markRead(id) {
158
- await this.request("PATCH", `/notifications/${id}/read`);
335
+ return this.request("PATCH", `/notifications/${id}/read`);
159
336
  }
160
337
  /**
161
338
  * Mark all notifications as read.
@@ -191,7 +368,11 @@ var NotifyKitClient = class {
191
368
  }
192
369
  const queryString = queryParams.toString();
193
370
  const url = queryString.length > 0 ? `/notifications/groups/${groupKey}?${queryString}` : `/notifications/groups/${groupKey}`;
194
- return this.request("GET", url);
371
+ const response = await this.request("GET", url);
372
+ return {
373
+ data: response.data.map((notification) => normalizeApiNotification(notification)),
374
+ meta: response.meta
375
+ };
195
376
  }
196
377
  // ============================================
197
378
  // Modals
@@ -222,16 +403,159 @@ var NotifyKitClient = class {
222
403
  // Preferences
223
404
  // ============================================
224
405
  /**
225
- * Get the current user's notification preferences.
406
+ * Get the current user's notification preference settings.
407
+ */
408
+ async getPreferenceSettings(query) {
409
+ const queryParams = new URLSearchParams();
410
+ if (query?.workspace_id !== void 0 && query.workspace_id !== null) {
411
+ queryParams.set("workspace_id", query.workspace_id);
412
+ }
413
+ const queryString = queryParams.toString();
414
+ const url = queryString.length > 0 ? `/preference-settings?${queryString}` : "/preference-settings";
415
+ const response = await this.request("GET", url);
416
+ return response.data;
417
+ }
418
+ /**
419
+ * Alias for getPreferenceSettings.
226
420
  */
227
- async getPreferences() {
228
- return this.request("GET", "/preferences");
421
+ async getPreferences(query) {
422
+ return this.getPreferenceSettings(query);
423
+ }
424
+ /**
425
+ * Update the current user's notification preference settings.
426
+ */
427
+ async updatePreferenceSettings(preferences) {
428
+ const response = await this.request(
429
+ "PATCH",
430
+ "/preference-settings",
431
+ preferences
432
+ );
433
+ return response.data;
229
434
  }
230
435
  /**
231
- * Update the current user's notification preferences.
436
+ * Alias for updatePreferenceSettings.
232
437
  */
233
438
  async updatePreferences(preferences) {
234
- await this.request("PUT", "/preferences", preferences);
439
+ return this.updatePreferenceSettings(preferences);
440
+ }
441
+ // ============================================
442
+ // Bulk Actions
443
+ // ============================================
444
+ /**
445
+ * Bulk mark notifications as read.
446
+ */
447
+ async bulkMarkRead(ids) {
448
+ return this.request("POST", "/notifications/bulk/read", {
449
+ notification_ids: ids
450
+ });
451
+ }
452
+ /**
453
+ * Bulk mark all matching notifications as read.
454
+ */
455
+ async bulkMarkAllRead(filters) {
456
+ return this.request("POST", "/notifications/bulk/read", {
457
+ all: true,
458
+ filters
459
+ });
460
+ }
461
+ /**
462
+ * Bulk archive notifications.
463
+ */
464
+ async bulkArchive(ids) {
465
+ return this.request("POST", "/notifications/bulk/archive", {
466
+ notification_ids: ids
467
+ });
468
+ }
469
+ /**
470
+ * Bulk archive all matching notifications.
471
+ */
472
+ async bulkArchiveAll(filters) {
473
+ return this.request("POST", "/notifications/bulk/archive", {
474
+ all: true,
475
+ filters
476
+ });
477
+ }
478
+ /**
479
+ * Bulk delete notifications.
480
+ */
481
+ async bulkDelete(ids) {
482
+ return this.request("POST", "/notifications/bulk/delete", {
483
+ notification_ids: ids
484
+ });
485
+ }
486
+ /**
487
+ * Bulk delete all matching notifications.
488
+ */
489
+ async bulkDeleteAll(filters) {
490
+ return this.request("POST", "/notifications/bulk/delete", {
491
+ all: true,
492
+ filters
493
+ });
494
+ }
495
+ // ============================================
496
+ // Search
497
+ // ============================================
498
+ /**
499
+ * Search notifications by query.
500
+ */
501
+ async search(query, filters, limit) {
502
+ const queryParams = new URLSearchParams();
503
+ queryParams.set("q", query);
504
+ if (filters?.category !== void 0) {
505
+ queryParams.set("category", filters.category);
506
+ }
507
+ if (filters?.unread === true) {
508
+ queryParams.set("unread", "1");
509
+ }
510
+ if (filters?.level !== void 0) {
511
+ queryParams.set("level", filters.level);
512
+ }
513
+ if (limit !== void 0) {
514
+ queryParams.set("limit", String(limit));
515
+ }
516
+ const response = await this.request("GET", `/notifications/search?${queryParams.toString()}`);
517
+ return {
518
+ data: response.data.map((notification) => normalizeApiNotification(notification)),
519
+ total: response.total
520
+ };
521
+ }
522
+ /**
523
+ * Get saved filters (system + custom).
524
+ */
525
+ async getSavedFilters() {
526
+ const response = await this.request("GET", "/notifications/filters");
527
+ return response.data;
528
+ }
529
+ /**
530
+ * Save a custom filter.
531
+ */
532
+ async saveFilter(name, filters) {
533
+ const response = await this.request("POST", "/notifications/filters", {
534
+ name,
535
+ filters
536
+ });
537
+ return response.data;
538
+ }
539
+ /**
540
+ * Delete a custom filter.
541
+ */
542
+ async deleteFilter(filterId) {
543
+ await this.request("DELETE", `/notifications/filters/${filterId}`);
544
+ }
545
+ // ============================================
546
+ // Archive
547
+ // ============================================
548
+ /**
549
+ * Archive a notification.
550
+ */
551
+ async archiveNotification(id) {
552
+ await this.request("POST", `/notifications/${id}/archive`);
553
+ }
554
+ /**
555
+ * Unarchive a notification.
556
+ */
557
+ async unarchiveNotification(id) {
558
+ await this.request("POST", `/notifications/${id}/unarchive`);
235
559
  }
236
560
  // ============================================
237
561
  // Private helpers
@@ -280,11 +604,15 @@ var NotifyKitClient = class {
280
604
  }
281
605
  const response = await this.fetchFn(url, requestInit);
282
606
  if (!response.ok) {
607
+ const responseHeaders = Object.fromEntries(
608
+ Array.from(response.headers.entries()).map(([key, value]) => [key.toLowerCase(), value])
609
+ );
283
610
  const errorData = await response.json();
284
611
  throw new NotifyKitApiError(
285
612
  errorData.message ?? `Request failed with status ${String(response.status)}`,
286
613
  response.status,
287
- errorData.errors
614
+ errorData.errors,
615
+ responseHeaders
288
616
  );
289
617
  }
290
618
  const text = await response.text();
@@ -322,8 +650,9 @@ var ENDPOINTS = {
322
650
  ACK_MODAL: (id) => `/modals/${id}/ack`,
323
651
  /** POST - Snooze a modal notification */
324
652
  SNOOZE_MODAL: (id) => `/modals/${id}/snooze`,
325
- /** GET - Get user's notification preferences */
326
- PREFERENCES: "/preferences"
653
+ /** GET/PATCH - Get or update user's notification preference settings */
654
+ PREFERENCE_SETTINGS: "/preference-settings",
655
+ PREFERENCES: "/preference-settings"
327
656
  };
328
657
  var NOTIFY_KIT_CLIENT_KEY = /* @__PURE__ */ Symbol(
329
658
  "notify-kit-client"
@@ -353,6 +682,7 @@ function useNotifyKitClient(config) {
353
682
  // Notifications
354
683
  listNotifications: (params) => client.listNotifications(params),
355
684
  getUnreadCount: () => client.getUnreadCount(),
685
+ getNotification: (id) => client.getNotification(id),
356
686
  markRead: (id) => client.markRead(id),
357
687
  markAllRead: () => client.markAllRead(),
358
688
  deleteNotification: (id) => client.deleteNotification(id),
@@ -364,10 +694,493 @@ function useNotifyKitClient(config) {
364
694
  ackModal: (id) => client.ackModal(id),
365
695
  snoozeModal: (id, minutes) => client.snoozeModal(id, minutes),
366
696
  // Preferences
367
- getPreferences: () => client.getPreferences(),
697
+ getPreferenceSettings: (query) => client.getPreferenceSettings(query),
698
+ updatePreferenceSettings: (preferences) => client.updatePreferenceSettings(preferences),
699
+ getPreferences: (query) => client.getPreferences(query),
368
700
  updatePreferences: (preferences) => client.updatePreferences(preferences)
369
701
  };
370
702
  }
703
+ var DEFAULT_UNREAD_COUNT_RETRY_AFTER_MS = 3e4;
704
+ var MIN_UNREAD_COUNT_RETRY_AFTER_MS = 5e3;
705
+ var DEFAULT_UNREAD_COUNT_TRANSIENT_RETRY_MS = 1e4;
706
+ var MAX_UNREAD_COUNT_TRANSIENT_RETRY_MS = 12e4;
707
+ var MIN_UNREAD_COUNT_REFRESH_INTERVAL_MS = 5e3;
708
+ var DEFAULT_PER_PAGE = 20;
709
+ function createInitialInboxState() {
710
+ return {
711
+ error: null,
712
+ currentPage: 0,
713
+ lastPage: 1,
714
+ hasMore: true,
715
+ unreadCountRetryAfterUntil: 0,
716
+ unreadCountLastLoadedAt: 0,
717
+ unreadCountTransientFailureCount: 0,
718
+ unreadCountRequest: null
719
+ };
720
+ }
721
+ var inboxState = null;
722
+ function getInboxState() {
723
+ inboxState ??= reactive(createInitialInboxState());
724
+ return inboxState;
725
+ }
726
+ function resetNotifyKitInbox() {
727
+ if (inboxState !== null) {
728
+ Object.assign(inboxState, createInitialInboxState());
729
+ }
730
+ }
731
+ function resolveStatusCode(statusCode) {
732
+ if (typeof statusCode === "number" && Number.isFinite(statusCode)) {
733
+ return statusCode;
734
+ }
735
+ if (typeof statusCode === "string") {
736
+ const parsedStatus = Number.parseInt(statusCode, 10);
737
+ if (Number.isFinite(parsedStatus)) {
738
+ return parsedStatus;
739
+ }
740
+ }
741
+ return void 0;
742
+ }
743
+ function resolveRetryAfterMs(retryAfterHeader) {
744
+ if (typeof retryAfterHeader === "number" && Number.isFinite(retryAfterHeader) && retryAfterHeader > 0) {
745
+ return Math.max(MIN_UNREAD_COUNT_RETRY_AFTER_MS, retryAfterHeader * 1e3);
746
+ }
747
+ if (typeof retryAfterHeader === "string") {
748
+ const parsedSeconds = Number.parseInt(retryAfterHeader, 10);
749
+ if (Number.isFinite(parsedSeconds) && parsedSeconds > 0) {
750
+ return Math.max(MIN_UNREAD_COUNT_RETRY_AFTER_MS, parsedSeconds * 1e3);
751
+ }
752
+ }
753
+ return DEFAULT_UNREAD_COUNT_RETRY_AFTER_MS;
754
+ }
755
+ function isServerFailureStatus(statusCode) {
756
+ return typeof statusCode === "number" && statusCode >= 500 && statusCode < 600;
757
+ }
758
+ function resolveTransientRetryMs(failureCount) {
759
+ const sanitizedFailureCount = Math.max(1, failureCount);
760
+ const exponentialRetryMs = DEFAULT_UNREAD_COUNT_TRANSIENT_RETRY_MS * 2 ** (sanitizedFailureCount - 1);
761
+ return Math.min(MAX_UNREAD_COUNT_TRANSIENT_RETRY_MS, exponentialRetryMs);
762
+ }
763
+ function resolveErrorMessage(error) {
764
+ if (error instanceof Error && error.message.length > 0) {
765
+ return error.message;
766
+ }
767
+ if (typeof error === "string" && error.length > 0) {
768
+ return error;
769
+ }
770
+ return "Request failed";
771
+ }
772
+ function asNotifyKitApiError(error) {
773
+ if (typeof error !== "object" || error === null) {
774
+ return null;
775
+ }
776
+ const status = "status" in error ? error.status : void 0;
777
+ const headers = "headers" in error ? error.headers : void 0;
778
+ const resolvedStatus = typeof status === "number" || typeof status === "string" ? status : void 0;
779
+ const resolvedHeaders = typeof headers === "object" && headers !== null ? headers : void 0;
780
+ if (resolvedStatus === void 0 && resolvedHeaders === void 0) {
781
+ return {};
782
+ }
783
+ return {
784
+ ...resolvedStatus !== void 0 ? { status: resolvedStatus } : {},
785
+ ...resolvedHeaders !== void 0 ? { headers: resolvedHeaders } : {}
786
+ };
787
+ }
788
+ function useNotifyKitInbox(options) {
789
+ const { client, perPage = DEFAULT_PER_PAGE } = options;
790
+ const store = useNotifyKitStore();
791
+ const state = getInboxState();
792
+ const notifications = computed(() => store.state.notifications);
793
+ const unreadCount = computed(() => store.state.unreadCount);
794
+ const loading = computed(() => store.state.notificationsLoading);
795
+ const error = computed(() => state.error);
796
+ const hasMore = computed(() => state.hasMore);
797
+ const nextPage = computed(() => state.hasMore ? state.currentPage + 1 : null);
798
+ const isUnreadCountCoolingDown = computed(
799
+ () => Date.now() < state.unreadCountRetryAfterUntil
800
+ );
801
+ function reconcileUnreadCountForFullyLoadedList() {
802
+ if (state.hasMore) {
803
+ return;
804
+ }
805
+ const resolvedUnreadCount = store.state.notifications.filter(
806
+ (notification) => notification.read_at === null
807
+ ).length;
808
+ store.setUnreadCount(resolvedUnreadCount);
809
+ }
810
+ async function loadUnreadCount() {
811
+ if (Date.now() < state.unreadCountRetryAfterUntil) {
812
+ return false;
813
+ }
814
+ if (state.unreadCountRequest !== null) {
815
+ return await state.unreadCountRequest;
816
+ }
817
+ const now = Date.now();
818
+ if (now - state.unreadCountLastLoadedAt < MIN_UNREAD_COUNT_REFRESH_INTERVAL_MS) {
819
+ return true;
820
+ }
821
+ store.setUnreadCountLoading(true);
822
+ state.unreadCountRequest = (async () => {
823
+ try {
824
+ const response = await client.getUnreadCount();
825
+ store.setUnreadCount(response.count);
826
+ state.unreadCountLastLoadedAt = Date.now();
827
+ state.unreadCountTransientFailureCount = 0;
828
+ state.unreadCountRetryAfterUntil = 0;
829
+ return true;
830
+ } catch (error2) {
831
+ const apiError = asNotifyKitApiError(error2);
832
+ const statusCode = resolveStatusCode(apiError?.status);
833
+ if (statusCode === 429) {
834
+ const retryAfterHeader = apiError?.headers?.["retry-after"];
835
+ state.unreadCountRetryAfterUntil = Date.now() + resolveRetryAfterMs(retryAfterHeader);
836
+ return false;
837
+ }
838
+ if (statusCode === 404) {
839
+ store.setUnreadCount(0);
840
+ state.unreadCountLastLoadedAt = Date.now();
841
+ state.unreadCountTransientFailureCount = 0;
842
+ state.unreadCountRetryAfterUntil = 0;
843
+ return true;
844
+ }
845
+ state.unreadCountTransientFailureCount += 1;
846
+ if (statusCode === void 0 || isServerFailureStatus(statusCode)) {
847
+ state.unreadCountRetryAfterUntil = Date.now() + resolveTransientRetryMs(state.unreadCountTransientFailureCount);
848
+ return false;
849
+ }
850
+ state.unreadCountRetryAfterUntil = Date.now() + DEFAULT_UNREAD_COUNT_TRANSIENT_RETRY_MS;
851
+ return false;
852
+ } finally {
853
+ store.setUnreadCountLoading(false);
854
+ }
855
+ })();
856
+ const wasSuccessful = await state.unreadCountRequest.finally(() => {
857
+ state.unreadCountRequest = null;
858
+ });
859
+ return wasSuccessful;
860
+ }
861
+ async function fetchNotifications(page = null, append = false) {
862
+ if (store.state.notificationsLoading) {
863
+ return void 0;
864
+ }
865
+ store.setNotificationsLoading(true);
866
+ state.error = null;
867
+ try {
868
+ const pageNumber = page !== null ? Number(page) : 1;
869
+ const response = await client.listNotifications({
870
+ per_page: perPage,
871
+ page: pageNumber
872
+ });
873
+ const items = [...response.data];
874
+ if (append) {
875
+ store.setNotifications([...store.state.notifications, ...items]);
876
+ } else {
877
+ store.setNotifications(items);
878
+ }
879
+ store.setNotificationsMeta(response.meta);
880
+ store.setLastNotificationsSync(Date.now());
881
+ state.currentPage = response.meta.current_page;
882
+ state.lastPage = response.meta.last_page;
883
+ state.hasMore = response.meta.current_page < response.meta.last_page;
884
+ reconcileUnreadCountForFullyLoadedList();
885
+ return items;
886
+ } catch (error2) {
887
+ state.error = resolveErrorMessage(error2);
888
+ store.setNotificationsMeta(null);
889
+ state.currentPage = 0;
890
+ state.lastPage = 1;
891
+ state.hasMore = false;
892
+ if (!append) {
893
+ store.setNotifications([]);
894
+ store.setUnreadCount(0);
895
+ }
896
+ throw error2;
897
+ } finally {
898
+ store.setNotificationsLoading(false);
899
+ }
900
+ }
901
+ async function markAsRead(notificationId) {
902
+ const notification = store.state.notifications.find((item) => item.id === notificationId);
903
+ const wasUnread = notification?.read_at === null;
904
+ const response = await client.markRead(notificationId);
905
+ if (wasUnread) {
906
+ store.markNotificationRead(notificationId);
907
+ }
908
+ if (typeof response.unread_count === "number") {
909
+ store.setUnreadCount(Math.max(0, response.unread_count));
910
+ return;
911
+ }
912
+ if (wasUnread) {
913
+ store.decrementUnreadCount();
914
+ }
915
+ }
916
+ async function markAllAsRead() {
917
+ try {
918
+ await client.markAllRead();
919
+ const readTimestamp = (/* @__PURE__ */ new Date()).toISOString();
920
+ const nextNotifications = store.state.notifications.map(
921
+ (notification) => notification.read_at === null ? {
922
+ ...notification,
923
+ read_at: readTimestamp
924
+ } : notification
925
+ );
926
+ store.setNotifications(nextNotifications);
927
+ await loadUnreadCount();
928
+ } catch (error2) {
929
+ state.error = resolveErrorMessage(error2);
930
+ throw error2;
931
+ }
932
+ }
933
+ async function deleteNotification(notificationId) {
934
+ await client.deleteNotification(notificationId);
935
+ const notification = store.state.notifications.find((item) => item.id === notificationId);
936
+ const wasUnread = notification?.read_at === null;
937
+ store.removeNotification(notificationId);
938
+ if (wasUnread) {
939
+ store.decrementUnreadCount();
940
+ }
941
+ }
942
+ async function clearAll() {
943
+ try {
944
+ await client.markAllRead();
945
+ store.setNotifications([]);
946
+ store.setUnreadCount(0);
947
+ } catch (error2) {
948
+ state.error = resolveErrorMessage(error2);
949
+ throw error2;
950
+ }
951
+ }
952
+ async function loadMore() {
953
+ if (!state.hasMore || store.state.notificationsLoading) {
954
+ return;
955
+ }
956
+ await fetchNotifications(state.currentPage + 1, true);
957
+ }
958
+ function reset() {
959
+ store.reset();
960
+ Object.assign(state, createInitialInboxState());
961
+ }
962
+ return {
963
+ notifications,
964
+ unreadCount,
965
+ loading,
966
+ error,
967
+ hasMore,
968
+ nextPage,
969
+ isUnreadCountCoolingDown,
970
+ loadUnreadCount,
971
+ fetchNotifications,
972
+ loadMore,
973
+ markAsRead,
974
+ markAllAsRead,
975
+ deleteNotification,
976
+ clearAll,
977
+ reset
978
+ };
979
+ }
980
+
981
+ // src/composables/useNotifyKitRealtimeState.ts
982
+ function isRecord2(value) {
983
+ return typeof value === "object" && value !== null && !Array.isArray(value);
984
+ }
985
+ function normalizeNullableString(value) {
986
+ if (value === void 0 || value === null) {
987
+ return null;
988
+ }
989
+ return typeof value === "string" ? value : void 0;
990
+ }
991
+ function normalizeModalSpec(value) {
992
+ if (value === void 0 || value === null) {
993
+ return null;
994
+ }
995
+ return isModalSpec(value) ? value : void 0;
996
+ }
997
+ function normalizeRealtimeNotificationPayload(payload) {
998
+ if (!isRecord2(payload)) {
999
+ return null;
1000
+ }
1001
+ const id = payload["id"];
1002
+ const key = payload["key"];
1003
+ const category = payload["category"];
1004
+ const level = payload["level"];
1005
+ const title = payload["title"];
1006
+ const body = payload["body"];
1007
+ const createdAt = payload["created_at"];
1008
+ if (typeof id !== "string" || typeof key !== "string" || !isValidCategory(category) || !isValidLevel(level) || typeof title !== "string" || typeof body !== "string" || typeof createdAt !== "string") {
1009
+ return null;
1010
+ }
1011
+ const data = payload["data"];
1012
+ if (data !== void 0 && !isRecord2(data)) {
1013
+ return null;
1014
+ }
1015
+ const actionUrl = normalizeNullableString(payload["action_url"]);
1016
+ const workspaceId = normalizeNullableString(payload["workspace_id"]);
1017
+ const groupKey = normalizeNullableString(payload["group_key"]);
1018
+ const idempotencyKey = normalizeNullableString(payload["idempotency_key"]);
1019
+ const readAt = normalizeNullableString(payload["read_at"]);
1020
+ const modal = normalizeModalSpec(payload["modal"]);
1021
+ if (actionUrl === void 0 || workspaceId === void 0 || groupKey === void 0 || idempotencyKey === void 0 || readAt === void 0 || modal === void 0) {
1022
+ return null;
1023
+ }
1024
+ return {
1025
+ id,
1026
+ key,
1027
+ category,
1028
+ level,
1029
+ title,
1030
+ body,
1031
+ action_url: actionUrl,
1032
+ workspace_id: workspaceId,
1033
+ modal,
1034
+ group_key: groupKey,
1035
+ idempotency_key: idempotencyKey,
1036
+ data: data ?? {},
1037
+ read_at: readAt,
1038
+ created_at: createdAt
1039
+ };
1040
+ }
1041
+ function useNotifyKitRealtimeState() {
1042
+ const store = useNotifyKitStore();
1043
+ function applyIncomingNotification(notification) {
1044
+ const existingIndex = store.state.notifications.findIndex((item) => item.id === notification.id);
1045
+ if (existingIndex === -1) {
1046
+ store.setNotifications([notification, ...store.state.notifications]);
1047
+ if (notification.read_at === null) {
1048
+ store.incrementUnreadCount();
1049
+ }
1050
+ } else {
1051
+ const existing = store.state.notifications[existingIndex];
1052
+ const wasRead = existing?.read_at !== null;
1053
+ const isNowRead = notification.read_at !== null;
1054
+ const nextNotifications = [...store.state.notifications];
1055
+ nextNotifications.splice(existingIndex, 1);
1056
+ nextNotifications.unshift(notification);
1057
+ store.setNotifications(nextNotifications);
1058
+ if (wasRead && !isNowRead) {
1059
+ store.incrementUnreadCount();
1060
+ } else if (!wasRead && isNowRead) {
1061
+ store.decrementUnreadCount();
1062
+ }
1063
+ }
1064
+ if (isModalEnabled(notification)) {
1065
+ store.enqueueModal(notification);
1066
+ }
1067
+ }
1068
+ function applyIncomingPayload(payload) {
1069
+ const notification = normalizeRealtimeNotificationPayload(payload);
1070
+ if (notification === null) {
1071
+ return null;
1072
+ }
1073
+ applyIncomingNotification(notification);
1074
+ return notification;
1075
+ }
1076
+ function handleNotificationRead(notificationId) {
1077
+ const notification = store.state.notifications.find((item) => item.id === notificationId);
1078
+ if (notification?.read_at === null) {
1079
+ store.markNotificationRead(notificationId);
1080
+ store.decrementUnreadCount();
1081
+ }
1082
+ }
1083
+ function handleNotificationDeleted(notificationId) {
1084
+ const notification = store.state.notifications.find((item) => item.id === notificationId);
1085
+ const wasUnread = notification?.read_at === null;
1086
+ store.removeNotification(notificationId);
1087
+ if (wasUnread) {
1088
+ store.decrementUnreadCount();
1089
+ }
1090
+ }
1091
+ return {
1092
+ applyIncomingPayload,
1093
+ applyIncomingNotification,
1094
+ handleNotificationRead,
1095
+ handleNotificationDeleted
1096
+ };
1097
+ }
1098
+
1099
+ // src/helpers/toastPolicy.ts
1100
+ var DEFAULT_TOAST_CATEGORIES = {
1101
+ workspace: true,
1102
+ security: true,
1103
+ orders: true,
1104
+ social: false,
1105
+ system: true,
1106
+ marketing: true
1107
+ };
1108
+ var DEFAULT_SOUND_LEVELS = ["danger"];
1109
+ var DEFAULT_SOUND_CATEGORIES = ["security"];
1110
+ function isCriticalModal(notification) {
1111
+ return notification.modal !== null && notification.modal.enabled && notification.modal.requires_ack;
1112
+ }
1113
+ function defaultShouldToast(notification, categorySettings) {
1114
+ if (isCriticalModal(notification)) {
1115
+ return false;
1116
+ }
1117
+ return categorySettings[notification.category];
1118
+ }
1119
+ function defaultShouldPlaySound(notification, soundLevels, soundCategories) {
1120
+ if (soundLevels.includes(notification.level)) {
1121
+ return true;
1122
+ }
1123
+ if (soundCategories.includes(notification.category)) {
1124
+ return true;
1125
+ }
1126
+ return false;
1127
+ }
1128
+ var defaultToastPolicy = {
1129
+ shouldToast(notification) {
1130
+ return defaultShouldToast(notification, DEFAULT_TOAST_CATEGORIES);
1131
+ },
1132
+ shouldPlaySound(notification) {
1133
+ return defaultShouldPlaySound(
1134
+ notification,
1135
+ DEFAULT_SOUND_LEVELS,
1136
+ DEFAULT_SOUND_CATEGORIES
1137
+ );
1138
+ }
1139
+ };
1140
+ function createToastPolicy(config) {
1141
+ const categorySettings = {
1142
+ ...DEFAULT_TOAST_CATEGORIES,
1143
+ ...config.toastCategories
1144
+ };
1145
+ const soundLevels = config.soundLevels ?? DEFAULT_SOUND_LEVELS;
1146
+ const soundCategories = config.soundCategories ?? DEFAULT_SOUND_CATEGORIES;
1147
+ return {
1148
+ shouldToast(notification) {
1149
+ if (config.shouldToast !== void 0) {
1150
+ return config.shouldToast(notification);
1151
+ }
1152
+ return defaultShouldToast(notification, categorySettings);
1153
+ },
1154
+ shouldPlaySound(notification) {
1155
+ if (config.shouldPlaySound !== void 0) {
1156
+ return config.shouldPlaySound(notification);
1157
+ }
1158
+ return defaultShouldPlaySound(notification, soundLevels, soundCategories);
1159
+ }
1160
+ };
1161
+ }
1162
+
1163
+ // src/helpers/notifyKitRealtimeChannel.ts
1164
+ async function resolveNotifyKitBroadcastChannel(options) {
1165
+ const { client, channel } = options;
1166
+ if (typeof channel === "string" && channel.length > 0) {
1167
+ return channel;
1168
+ }
1169
+ const meResponse = await client.getMe();
1170
+ return meResponse.broadcast_channel;
1171
+ }
1172
+ function bindNotifyKitBroadcastChannel(options) {
1173
+ const { echo, channel, onNotification } = options;
1174
+ echo.private(channel).notification(onNotification);
1175
+ return {
1176
+ channel,
1177
+ unsubscribe: () => {
1178
+ echo.leave(channel);
1179
+ }
1180
+ };
1181
+ }
1182
+
1183
+ // src/composables/useNotifyKitRealtime.ts
371
1184
  function useNotifyKitRealtime(options) {
372
1185
  const {
373
1186
  echo: echoOption,
@@ -377,6 +1190,7 @@ function useNotifyKitRealtime(options) {
377
1190
  onConnectionChange
378
1191
  } = options;
379
1192
  const store = useNotifyKitStore();
1193
+ const realtimeState = useNotifyKitRealtimeState();
380
1194
  const connectionState = ref("disconnected");
381
1195
  const currentChannel = ref(null);
382
1196
  const isSubscribed = ref(false);
@@ -390,15 +1204,11 @@ function useNotifyKitRealtime(options) {
390
1204
  onConnectionChange?.(state);
391
1205
  }
392
1206
  function handleNotification(payload) {
393
- if (!isNotifyKitNotification(payload)) {
1207
+ const notification = realtimeState.applyIncomingPayload(payload);
1208
+ if (notification === null) {
394
1209
  return;
395
1210
  }
396
- store.addNotification(payload);
397
- store.incrementUnreadCount();
398
- if (isModalEnabled(payload)) {
399
- store.enqueueModal(payload);
400
- }
401
- onNotification?.(payload);
1211
+ onNotification?.(notification);
402
1212
  }
403
1213
  async function connect() {
404
1214
  if (connectionState.value === "connected" || connectionState.value === "connecting") {
@@ -406,12 +1216,15 @@ function useNotifyKitRealtime(options) {
406
1216
  }
407
1217
  setConnectionState("connecting");
408
1218
  try {
409
- const meResponse = await client.getMe();
410
- const channel = meResponse.broadcast_channel;
1219
+ const channel = await resolveNotifyKitBroadcastChannel({ client });
411
1220
  currentChannel.value = channel;
412
1221
  store.setBroadcastChannel(channel);
413
1222
  const echo = getEcho();
414
- echo.private(channel).notification(handleNotification);
1223
+ bindNotifyKitBroadcastChannel({
1224
+ echo,
1225
+ channel,
1226
+ onNotification: handleNotification
1227
+ });
415
1228
  isSubscribed.value = true;
416
1229
  setConnectionState("connected");
417
1230
  } catch (error) {
@@ -537,7 +1350,9 @@ function useNotifyKitFallback(options) {
537
1350
  startPolling();
538
1351
  disconnectTimer = null;
539
1352
  }, config.disconnectThresholdMs);
540
- } else if (state === "connected") {
1353
+ return;
1354
+ }
1355
+ if (state === "connected") {
541
1356
  if (disconnectTimer !== null) {
542
1357
  clearTimeout(disconnectTimer);
543
1358
  disconnectTimer = null;
@@ -566,172 +1381,299 @@ function useNotifyKitFallback(options) {
566
1381
  pollModals
567
1382
  };
568
1383
  }
569
-
570
- // src/helpers/toastPolicy.ts
571
- var DEFAULT_TOAST_CATEGORIES = {
572
- workspace: true,
573
- security: true,
574
- orders: true,
575
- social: false,
576
- system: true,
577
- marketing: true
578
- };
579
- var DEFAULT_SOUND_LEVELS = ["danger"];
580
- var DEFAULT_SOUND_CATEGORIES = ["security"];
581
- function isCriticalModal(notification) {
582
- return notification.modal !== null && notification.modal.enabled && notification.modal.requires_ack;
1384
+ function createInitialState() {
1385
+ return {
1386
+ selectedIds: /* @__PURE__ */ new Set(),
1387
+ isSelectionMode: false,
1388
+ isProcessing: false,
1389
+ error: null
1390
+ };
583
1391
  }
584
- function defaultShouldToast(notification, categorySettings) {
585
- if (isCriticalModal(notification)) {
586
- return false;
1392
+ function useNotifyKitBulkActions(options) {
1393
+ const { client } = options;
1394
+ const store = useNotifyKitStore();
1395
+ const state = reactive(createInitialState());
1396
+ const selectedCount = computed(() => state.selectedIds.size);
1397
+ const hasSelection = computed(() => state.selectedIds.size > 0);
1398
+ const selectedIdsArray = computed(() => Array.from(state.selectedIds));
1399
+ function toggleSelection(id) {
1400
+ if (state.selectedIds.has(id)) {
1401
+ state.selectedIds.delete(id);
1402
+ } else {
1403
+ state.selectedIds.add(id);
1404
+ }
587
1405
  }
588
- return categorySettings[notification.category];
589
- }
590
- function defaultShouldPlaySound(notification, soundLevels, soundCategories) {
591
- if (soundLevels.includes(notification.level)) {
592
- return true;
1406
+ function select(id) {
1407
+ state.selectedIds.add(id);
593
1408
  }
594
- if (soundCategories.includes(notification.category)) {
595
- return true;
1409
+ function deselect(id) {
1410
+ state.selectedIds.delete(id);
596
1411
  }
597
- return false;
598
- }
599
- var defaultToastPolicy = {
600
- shouldToast(notification) {
601
- return defaultShouldToast(notification, DEFAULT_TOAST_CATEGORIES);
602
- },
603
- shouldPlaySound(notification) {
604
- return defaultShouldPlaySound(
605
- notification,
606
- DEFAULT_SOUND_LEVELS,
607
- DEFAULT_SOUND_CATEGORIES
608
- );
1412
+ function selectAll(ids) {
1413
+ for (const id of ids) {
1414
+ state.selectedIds.add(id);
1415
+ }
609
1416
  }
610
- };
611
- function createToastPolicy(config) {
612
- const categorySettings = {
613
- ...DEFAULT_TOAST_CATEGORIES,
614
- ...config.toastCategories
615
- };
616
- const soundLevels = config.soundLevels ?? DEFAULT_SOUND_LEVELS;
617
- const soundCategories = config.soundCategories ?? DEFAULT_SOUND_CATEGORIES;
618
- return {
619
- shouldToast(notification) {
620
- if (config.shouldToast !== void 0) {
621
- return config.shouldToast(notification);
1417
+ function clearSelection() {
1418
+ state.selectedIds.clear();
1419
+ }
1420
+ function enterSelectionMode() {
1421
+ state.isSelectionMode = true;
1422
+ }
1423
+ function exitSelectionMode() {
1424
+ state.isSelectionMode = false;
1425
+ state.selectedIds.clear();
1426
+ }
1427
+ async function markSelectedRead() {
1428
+ if (state.selectedIds.size === 0) {
1429
+ return { affected: 0, failed: 0 };
1430
+ }
1431
+ state.isProcessing = true;
1432
+ state.error = null;
1433
+ try {
1434
+ const ids = Array.from(state.selectedIds);
1435
+ const result = await client.bulkMarkRead(ids);
1436
+ for (const id of ids) {
1437
+ store.markNotificationRead(id);
622
1438
  }
623
- return defaultShouldToast(notification, categorySettings);
624
- },
625
- shouldPlaySound(notification) {
626
- if (config.shouldPlaySound !== void 0) {
627
- return config.shouldPlaySound(notification);
1439
+ if (result.affected > 0) {
1440
+ store.setUnreadCount(Math.max(0, store.state.unreadCount - result.affected));
628
1441
  }
629
- return defaultShouldPlaySound(notification, soundLevels, soundCategories);
1442
+ clearSelection();
1443
+ return result;
1444
+ } catch (error) {
1445
+ state.error = error instanceof Error ? error.message : "Failed to mark as read";
1446
+ throw error;
1447
+ } finally {
1448
+ state.isProcessing = false;
630
1449
  }
631
- };
632
- }
633
-
634
- // src/helpers/modalHelpers.ts
635
- function getModalAriaAttributes(notification, ids) {
636
- const ariaRole = notification.modal?.accessibility?.aria_role ?? "alertdialog";
637
- return {
638
- role: ariaRole,
639
- "aria-modal": "true",
640
- "aria-labelledby": ids.titleId,
641
- "aria-describedby": ids.bodyId
642
- };
643
- }
644
- function getEscapeKeyHandler(notification, callbacks) {
645
- const behavior = notification.modal?.accessibility?.escape_key_behavior ?? "close";
646
- if (behavior === "disabled") {
647
- return null;
648
1450
  }
649
- return (event) => {
650
- if (event.key !== "Escape") return;
651
- switch (behavior) {
652
- case "close":
653
- event.preventDefault();
654
- callbacks.onClose();
655
- break;
656
- case "snooze": {
657
- event.preventDefault();
658
- const minSnooze = notification.modal?.snooze_options_minutes[0] ?? 15;
659
- callbacks.onSnooze(minSnooze);
660
- break;
1451
+ async function markAllRead(filters) {
1452
+ state.isProcessing = true;
1453
+ state.error = null;
1454
+ try {
1455
+ const result = await client.bulkMarkAllRead(filters);
1456
+ if (filters === void 0) {
1457
+ store.setUnreadCount(0);
661
1458
  }
662
- }
663
- };
664
- }
665
- function createFocusTrap(modalElement) {
666
- const focusableSelector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
667
- let previousFocus = null;
668
- let trapHandler = null;
669
- function trapFocus(event) {
670
- if (event.key !== "Tab") return;
671
- const focusable = modalElement.querySelectorAll(focusableSelector);
672
- if (focusable.length === 0) return;
673
- const first = focusable[0];
674
- const last = focusable[focusable.length - 1];
675
- if (first === void 0 || last === void 0) return;
676
- if (event.shiftKey && document.activeElement === first) {
677
- event.preventDefault();
678
- last.focus();
679
- } else if (!event.shiftKey && document.activeElement === last) {
680
- event.preventDefault();
681
- first.focus();
1459
+ clearSelection();
1460
+ return result;
1461
+ } catch (error) {
1462
+ state.error = error instanceof Error ? error.message : "Failed to mark all as read";
1463
+ throw error;
1464
+ } finally {
1465
+ state.isProcessing = false;
682
1466
  }
683
1467
  }
684
- return {
685
- activate() {
686
- previousFocus = document.activeElement;
687
- const focusable = modalElement.querySelectorAll(focusableSelector);
688
- if (focusable.length > 0 && focusable[0] !== void 0) {
689
- focusable[0].focus();
690
- }
691
- trapHandler = trapFocus;
692
- modalElement.addEventListener("keydown", trapHandler);
693
- },
694
- deactivate() {
695
- if (trapHandler !== null) {
696
- modalElement.removeEventListener("keydown", trapHandler);
697
- trapHandler = null;
1468
+ async function archiveSelected() {
1469
+ if (state.selectedIds.size === 0) {
1470
+ return { affected: 0, failed: 0 };
1471
+ }
1472
+ state.isProcessing = true;
1473
+ state.error = null;
1474
+ try {
1475
+ const ids = Array.from(state.selectedIds);
1476
+ const result = await client.bulkArchive(ids);
1477
+ for (const id of ids) {
1478
+ store.removeNotification(id);
698
1479
  }
699
- if (previousFocus !== null) {
700
- previousFocus.focus();
701
- previousFocus = null;
1480
+ clearSelection();
1481
+ return result;
1482
+ } catch (error) {
1483
+ state.error = error instanceof Error ? error.message : "Failed to archive";
1484
+ throw error;
1485
+ } finally {
1486
+ state.isProcessing = false;
1487
+ }
1488
+ }
1489
+ async function archiveAll(filters) {
1490
+ state.isProcessing = true;
1491
+ state.error = null;
1492
+ try {
1493
+ const result = await client.bulkArchiveAll(filters);
1494
+ clearSelection();
1495
+ return result;
1496
+ } catch (error) {
1497
+ state.error = error instanceof Error ? error.message : "Failed to archive all";
1498
+ throw error;
1499
+ } finally {
1500
+ state.isProcessing = false;
1501
+ }
1502
+ }
1503
+ async function deleteSelected() {
1504
+ if (state.selectedIds.size === 0) {
1505
+ return { affected: 0, failed: 0 };
1506
+ }
1507
+ state.isProcessing = true;
1508
+ state.error = null;
1509
+ try {
1510
+ const ids = Array.from(state.selectedIds);
1511
+ const result = await client.bulkDelete(ids);
1512
+ for (const id of ids) {
1513
+ store.removeNotification(id);
702
1514
  }
1515
+ clearSelection();
1516
+ return result;
1517
+ } catch (error) {
1518
+ state.error = error instanceof Error ? error.message : "Failed to delete";
1519
+ throw error;
1520
+ } finally {
1521
+ state.isProcessing = false;
703
1522
  }
1523
+ }
1524
+ async function deleteAll(filters) {
1525
+ state.isProcessing = true;
1526
+ state.error = null;
1527
+ try {
1528
+ const result = await client.bulkDeleteAll(filters);
1529
+ clearSelection();
1530
+ return result;
1531
+ } catch (error) {
1532
+ state.error = error instanceof Error ? error.message : "Failed to delete all";
1533
+ throw error;
1534
+ } finally {
1535
+ state.isProcessing = false;
1536
+ }
1537
+ }
1538
+ function reset() {
1539
+ Object.assign(state, createInitialState());
1540
+ }
1541
+ return {
1542
+ state: readonly(state),
1543
+ selectedCount,
1544
+ hasSelection,
1545
+ selectedIdsArray,
1546
+ toggleSelection,
1547
+ select,
1548
+ deselect,
1549
+ selectAll,
1550
+ clearSelection,
1551
+ enterSelectionMode,
1552
+ exitSelectionMode,
1553
+ markSelectedRead,
1554
+ markAllRead,
1555
+ archiveSelected,
1556
+ archiveAll,
1557
+ deleteSelected,
1558
+ deleteAll,
1559
+ reset
704
1560
  };
705
1561
  }
706
- function getActionButtonAttributes(action, index) {
707
- const attrs = {
708
- type: "button",
709
- "data-action-type": action.type,
710
- "data-action-index": String(index),
711
- "aria-label": action.label
1562
+ function createInitialState2() {
1563
+ return {
1564
+ query: "",
1565
+ results: [],
1566
+ total: 0,
1567
+ isSearching: false,
1568
+ hasSearched: false,
1569
+ filters: null,
1570
+ error: null
712
1571
  };
713
- if (action.primary === true) {
714
- attrs["data-primary"] = true;
715
- }
716
- return attrs;
717
- }
718
- function isCriticalModal2(notification) {
719
- return notification.modal?.enabled === true && notification.modal.requires_ack === true;
720
1572
  }
721
- function shouldCloseOnBackdropClick(notification) {
722
- return !isCriticalModal2(notification);
723
- }
724
- function getMinSnoozeDuration(notification) {
725
- const options = notification.modal?.snooze_options_minutes;
726
- if (options === void 0 || options.length === 0) {
727
- return null;
1573
+ function useNotifyKitSearch(options) {
1574
+ const { client, minQueryLength = 3 } = options;
1575
+ const state = reactive(createInitialState2());
1576
+ const savedFilters = ref([]);
1577
+ const savedFiltersLoading = ref(false);
1578
+ const hasResults = computed(() => state.results.length > 0);
1579
+ const resultCount = computed(() => state.results.length);
1580
+ const isValidQuery = computed(() => state.query.length >= minQueryLength);
1581
+ async function search(query, filters) {
1582
+ state.query = query;
1583
+ if (query.length < minQueryLength) {
1584
+ state.error = `Search query must be at least ${String(minQueryLength)} characters`;
1585
+ return;
1586
+ }
1587
+ state.isSearching = true;
1588
+ state.error = null;
1589
+ try {
1590
+ const effectiveFilters = filters ?? state.filters ?? void 0;
1591
+ const result = await client.search(query, effectiveFilters);
1592
+ state.results = [...result.data];
1593
+ state.total = result.total;
1594
+ state.hasSearched = true;
1595
+ if (filters !== void 0) {
1596
+ state.filters = filters;
1597
+ }
1598
+ } catch (error) {
1599
+ state.error = error instanceof Error ? error.message : "Search failed";
1600
+ state.results = [];
1601
+ state.total = 0;
1602
+ } finally {
1603
+ state.isSearching = false;
1604
+ }
1605
+ }
1606
+ function clearSearch() {
1607
+ state.query = "";
1608
+ state.results = [];
1609
+ state.total = 0;
1610
+ state.hasSearched = false;
1611
+ state.error = null;
1612
+ }
1613
+ function setFilters(filters) {
1614
+ state.filters = filters;
728
1615
  }
729
- return options[0] ?? null;
1616
+ async function loadSavedFilters() {
1617
+ savedFiltersLoading.value = true;
1618
+ try {
1619
+ savedFilters.value = await client.getSavedFilters();
1620
+ } catch {
1621
+ } finally {
1622
+ savedFiltersLoading.value = false;
1623
+ }
1624
+ }
1625
+ async function applyFilter(filter) {
1626
+ const searchFilters = {};
1627
+ const categoryValue = filter.filters["category"];
1628
+ const levelValue = filter.filters["level"];
1629
+ if (typeof categoryValue === "string") {
1630
+ searchFilters.category = categoryValue;
1631
+ }
1632
+ if (filter.filters["unread_only"] === true) {
1633
+ searchFilters.unread = true;
1634
+ }
1635
+ if (typeof levelValue === "string") {
1636
+ searchFilters.level = levelValue;
1637
+ }
1638
+ state.filters = searchFilters;
1639
+ if (state.query.length >= minQueryLength) {
1640
+ await search(state.query, searchFilters);
1641
+ }
1642
+ }
1643
+ async function saveFilter(name, filters) {
1644
+ const createdFilter = await client.saveFilter(name, filters);
1645
+ savedFilters.value = [...savedFilters.value, createdFilter];
1646
+ return createdFilter;
1647
+ }
1648
+ async function deleteFilter(filterId) {
1649
+ await client.deleteFilter(filterId);
1650
+ savedFilters.value = savedFilters.value.filter((filter) => filter.id !== filterId);
1651
+ }
1652
+ function reset() {
1653
+ Object.assign(state, createInitialState2());
1654
+ savedFilters.value = [];
1655
+ }
1656
+ return {
1657
+ state: readonly(state),
1658
+ hasResults,
1659
+ resultCount,
1660
+ isValidQuery,
1661
+ savedFilters,
1662
+ savedFiltersLoading,
1663
+ search,
1664
+ clearSearch,
1665
+ setFilters,
1666
+ loadSavedFilters,
1667
+ applyFilter,
1668
+ saveFilter,
1669
+ deleteFilter,
1670
+ reset
1671
+ };
730
1672
  }
731
1673
 
732
1674
  // src/index.ts
733
1675
  var VERSION = "0.1.0";
734
1676
 
735
- export { ENDPOINTS, NOTIFICATION_CATEGORIES, NOTIFICATION_LEVELS, NOTIFY_KIT_CLIENT_KEY, NotifyKitApiError, NotifyKitClient, VERSION, createFocusTrap, createNotifyKitClient, createToastPolicy, defaultToastPolicy, getActionButtonAttributes, getEscapeKeyHandler, getMinSnoozeDuration, getModalAriaAttributes, isChannelPreferences, isCriticalModal2 as isCriticalModal, isMeResponse, isModalEnabled, isModalSpec, isNotifyKitNotification, isPaginationMeta, isUnreadCountResponse, isValidCategory, isValidLevel, provideNotifyKitClient, shouldCloseOnBackdropClick, useNotifyKitClient, useNotifyKitFallback, useNotifyKitRealtime };
1677
+ export { ENDPOINTS, NOTIFICATION_CATEGORIES, NOTIFICATION_LEVELS, NOTIFY_KIT_CLIENT_KEY, NotifyKitApiError, NotifyKitClient, VERSION, bindNotifyKitBroadcastChannel, createNotifyKitClient, createToastPolicy, defaultToastPolicy, isMeResponse, isModalEnabled, isModalSpec, isNotifyKitNotification, isNotifyKitPreferenceSettings, isNotifyKitQuietHours, isPaginationMeta, isUnreadCountResponse, isValidCategory, isValidLevel, provideNotifyKitClient, resetNotifyKitInbox, resolveNotifyKitBroadcastChannel, useNotifyKitBulkActions, useNotifyKitClient, useNotifyKitFallback, useNotifyKitInbox, useNotifyKitRealtime, useNotifyKitRealtimeState, useNotifyKitSearch };
736
1678
  //# sourceMappingURL=index.js.map
737
1679
  //# sourceMappingURL=index.js.map