@keepkit/core 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,190 +1,18 @@
1
+ "use client";
1
2
  import {
2
- DEFAULT_INDEXEDDB_DATABASE,
3
- DEFAULT_INDEXEDDB_STORE,
4
- DEFAULT_STORAGE_KEY,
5
- DEFAULT_SYNC_QUEUE_DATABASE,
6
- DEFAULT_SYNC_QUEUE_KEY,
7
- DEFAULT_SYNC_QUEUE_STORE,
8
- IndexedDBAdapter,
9
- IndexedDBSyncQueueAdapter,
10
- KeepStorageAccessError,
11
- KeepStorageError,
12
- KeepStorageParseError,
13
- KeepStorageQuotaError,
14
- LocalStorageAdapter,
15
- LocalStorageSyncQueueAdapter,
16
- SyncStorageAdapter,
17
- createStorageAdapter,
3
+ KeepStore,
4
+ isKeepItemMetadataStale,
5
+ queryKeepItems,
6
+ revalidateKeepItems
7
+ } from "./chunk-J7NKIOXV.js";
8
+ import {
9
+ parseKeepMeta
10
+ } from "./chunk-THZ3ACR2.js";
11
+ import "./chunk-5QSZP6MT.js";
12
+ import {
13
+ createBrowserStorageAdapter,
18
14
  normalizeKeepTags
19
- } from "./chunk-H4A322SZ.js";
20
-
21
- // src/migration.ts
22
- async function mergeKeepItems(localItems, target) {
23
- if (target.merge) return target.merge(localItems);
24
- const remoteItems = await target.getAll();
25
- const byId = new Map(remoteItems.map((item) => [item.id, item]));
26
- for (const localItem of localItems) {
27
- const remoteItem = byId.get(localItem.id);
28
- if (!remoteItem || localItem.updatedAt > remoteItem.updatedAt) {
29
- byId.set(localItem.id, localItem);
30
- }
31
- }
32
- const merged = [...byId.values()].sort((a, b) => b.updatedAt - a.updatedAt);
33
- await Promise.all(merged.map((item) => target.set(item)));
34
- return merged;
35
- }
36
- async function migrateKeepItems(source, target) {
37
- const localItems = await source.getAll();
38
- const merged = await mergeKeepItems(localItems, target);
39
- await source.clear();
40
- return merged;
41
- }
42
-
43
- // src/schema.ts
44
- var KeepSchemaValidationError = class extends Error {
45
- constructor(message, options = {}) {
46
- super(message);
47
- this.name = "KeepSchemaValidationError";
48
- this.cause = options.cause;
49
- this.itemId = options.itemId;
50
- }
51
- };
52
- async function parseKeepMeta(schema, value) {
53
- try {
54
- if ("parse" in schema) return await schema.parse(value);
55
- if ("safeParse" in schema) {
56
- const result2 = await schema.safeParse(value);
57
- if (result2.success) return result2.data;
58
- throw new KeepSchemaValidationError("KeepKit metadata did not match the configured schema.", {
59
- cause: result2.error
60
- });
61
- }
62
- const result = await schema["~standard"].validate(value);
63
- if (!result.issues && "value" in result) return result.value;
64
- throw new KeepSchemaValidationError("KeepKit metadata did not match the configured schema.", {
65
- cause: result.issues
66
- });
67
- } catch (cause) {
68
- if (cause instanceof KeepSchemaValidationError) throw cause;
69
- throw new KeepSchemaValidationError("KeepKit metadata did not match the configured schema.", {
70
- cause
71
- });
72
- }
73
- }
74
- async function validateKeepItem(item, schema) {
75
- return { ...item, meta: await parseKeepMeta(schema, item.meta) };
76
- }
77
-
78
- // src/backup.ts
79
- var KEEP_BACKUP_FORMAT = "keepkit";
80
- var KEEP_BACKUP_VERSION = 1;
81
- var KeepBackupParseError = class extends Error {
82
- constructor(message, options) {
83
- super(message);
84
- this.name = "KeepBackupParseError";
85
- if (options?.cause !== void 0) this.cause = options.cause;
86
- }
87
- };
88
- var KeepBackupImportError = class extends Error {
89
- constructor(message, options) {
90
- super(message);
91
- this.name = "KeepBackupImportError";
92
- this.mode = options.mode;
93
- this.imported = options.imported;
94
- this.failed = options.failed;
95
- if (options.cause !== void 0) this.cause = options.cause;
96
- }
97
- };
98
- async function exportItems(adapter) {
99
- const backup = {
100
- format: KEEP_BACKUP_FORMAT,
101
- version: KEEP_BACKUP_VERSION,
102
- exportedAt: Date.now(),
103
- items: await adapter.getAll()
104
- };
105
- return JSON.stringify(backup, null, 2);
106
- }
107
- async function importItems(adapter, data, options = {}) {
108
- const backup = parseBackup(data);
109
- const mode = options.mode ?? "merge";
110
- const validItems = [];
111
- let failed = 0;
112
- for (const item of backup.items) {
113
- if (!options.schema) {
114
- validItems.push(item);
115
- continue;
116
- }
117
- try {
118
- validItems.push(await validateKeepItem(item, options.schema));
119
- } catch (cause) {
120
- options.onInvalidItem?.(cause, item);
121
- if ((options.invalidItemPolicy ?? "error") === "drop") {
122
- failed += 1;
123
- continue;
124
- }
125
- throw cause;
126
- }
127
- }
128
- let items;
129
- if (mode === "merge") {
130
- try {
131
- items = await mergeKeepItems(validItems, adapter);
132
- } catch (cause) {
133
- throw new KeepBackupImportError("KeepKit could not merge the backup.", {
134
- mode,
135
- imported: 0,
136
- failed: validItems.length + failed,
137
- cause
138
- });
139
- }
140
- } else {
141
- let imported = 0;
142
- try {
143
- await adapter.clear();
144
- for (const item of validItems) {
145
- await adapter.set(item);
146
- imported += 1;
147
- }
148
- items = await adapter.getAll();
149
- } catch (cause) {
150
- throw new KeepBackupImportError("KeepKit could not replace the stored items.", {
151
- mode,
152
- imported,
153
- failed: validItems.length + failed - imported,
154
- cause
155
- });
156
- }
157
- }
158
- return { mode, imported: validItems.length, failed, total: items.length, items };
159
- }
160
- function parseBackup(data) {
161
- let value = data;
162
- if (typeof data === "string") {
163
- try {
164
- value = JSON.parse(data);
165
- } catch (cause) {
166
- throw new KeepBackupParseError("KeepKit backup is not valid JSON.", { cause });
167
- }
168
- }
169
- if (!isRecord(value)) throw new KeepBackupParseError("KeepKit backup must be an object.");
170
- if (value.format !== KEEP_BACKUP_FORMAT || value.version !== KEEP_BACKUP_VERSION) {
171
- throw new KeepBackupParseError("KeepKit backup format or version is unsupported.");
172
- }
173
- if (typeof value.exportedAt !== "number" || !Number.isFinite(value.exportedAt)) {
174
- throw new KeepBackupParseError("KeepKit backup has an invalid export timestamp.");
175
- }
176
- if (!Array.isArray(value.items) || !value.items.every(isKeepItem)) {
177
- throw new KeepBackupParseError("KeepKit backup contains invalid items.");
178
- }
179
- return value;
180
- }
181
- function isKeepItem(value) {
182
- if (!isRecord(value)) return false;
183
- return typeof value.id === "string" && typeof value.savedAt === "number" && Number.isFinite(value.savedAt) && typeof value.updatedAt === "number" && Number.isFinite(value.updatedAt) && "meta" in value && (value.targetType === void 0 || typeof value.targetType === "string") && (value.note === void 0 || typeof value.note === "string") && (value.schemaVersion === void 0 || typeof value.schemaVersion === "number" && Number.isFinite(value.schemaVersion)) && (value.revision === void 0 || typeof value.revision === "string") && (value.tags === void 0 || Array.isArray(value.tags) && value.tags.every((tag) => typeof tag === "string"));
184
- }
185
- function isRecord(value) {
186
- return typeof value === "object" && value !== null;
187
- }
15
+ } from "./chunk-X4UVKTBK.js";
188
16
 
189
17
  // src/hooks/useKeepItem.ts
190
18
  import { useCallback as useCallback3 } from "react";
@@ -199,39 +27,13 @@ import {
199
27
  useRef,
200
28
  useSyncExternalStore
201
29
  } from "react";
202
-
203
- // src/store.ts
204
- var KeepStore = class {
205
- constructor(initialState) {
206
- this.listeners = /* @__PURE__ */ new Set();
207
- this.getSnapshot = () => this.state;
208
- this.subscribe = (listener) => {
209
- this.listeners.add(listener);
210
- return () => this.listeners.delete(listener);
211
- };
212
- this.state = initialState;
213
- }
214
- setState(next) {
215
- let changed = false;
216
- for (const key of Object.keys(next)) {
217
- if (!Object.is(this.state[key], next[key])) {
218
- changed = true;
219
- break;
220
- }
221
- }
222
- if (!changed) return;
223
- this.state = { ...this.state, ...next };
224
- for (const listener of this.listeners) listener();
225
- }
226
- };
227
-
228
- // src/KeepProvider.tsx
229
30
  import { jsx } from "react/jsx-runtime";
230
- var defaultStorage = new LocalStorageAdapter();
31
+ var defaultStorage = createBrowserStorageAdapter();
231
32
  var KeepContext = createContext(null);
232
33
  var KeepStoreContext = createContext(null);
233
34
  function KeepProvider({
234
35
  storage = defaultStorage,
36
+ initialItems,
235
37
  onSave,
236
38
  onRemove,
237
39
  onNoteUpdate,
@@ -249,7 +51,7 @@ function KeepProvider({
249
51
  const storeRef = useRef(null);
250
52
  if (!storeRef.current) {
251
53
  storeRef.current = new KeepStore({
252
- items: [],
54
+ items: initialItems ? [...initialItems] : [],
253
55
  isLoading: true,
254
56
  isHydrated: false,
255
57
  isMutating: false,
@@ -276,10 +78,7 @@ function KeepProvider({
276
78
  const pendingRefreshesRef = useRef(0);
277
79
  const pendingMutationsRef = useRef(0);
278
80
  const syncStorage = isSyncCapableStorage(storage) ? storage : void 0;
279
- const getSyncState = useCallback(
280
- () => syncStorage?.getSyncState() ?? IDLE_SYNC_STATE,
281
- [syncStorage]
282
- );
81
+ const getSyncState = useCallback(() => syncStorage?.getSyncState() ?? IDLE_SYNC_STATE, [syncStorage]);
283
82
  const subscribeSync = useCallback(
284
83
  (listener) => syncStorage?.subscribeSync(listener) ?? (() => void 0),
285
84
  [syncStorage]
@@ -300,19 +99,13 @@ function KeepProvider({
300
99
  },
301
100
  [store]
302
101
  );
303
- const runBeforePlugins = useCallback(
304
- async (context) => {
305
- for (const plugin of pluginsRef.current) await plugin.before?.(context);
306
- return context;
307
- },
308
- []
309
- );
310
- const runAfterPlugins = useCallback(
311
- async (context) => {
312
- for (const plugin of pluginsRef.current) await plugin.after?.(context);
313
- },
314
- []
315
- );
102
+ const runBeforePlugins = useCallback(async (context) => {
103
+ for (const plugin of pluginsRef.current) await plugin.before?.(context);
104
+ return context;
105
+ }, []);
106
+ const runAfterPlugins = useCallback(async (context) => {
107
+ for (const plugin of pluginsRef.current) await plugin.after?.(context);
108
+ }, []);
316
109
  const enqueueOperation = useCallback((operation) => {
317
110
  const run = operationTailRef.current.then(operation, operation);
318
111
  operationTailRef.current = run.then(
@@ -434,10 +227,9 @@ function KeepProvider({
434
227
  throw cause;
435
228
  }
436
229
  await runMutation("save", normalizedItem.id, (previous) => ({
437
- next: [
438
- ...previous.filter((current) => current.id !== normalizedItem.id),
439
- normalizedItem
440
- ].sort((a, b) => b.updatedAt - a.updatedAt),
230
+ next: [...previous.filter((current) => current.id !== normalizedItem.id), normalizedItem].sort(
231
+ (a, b) => b.updatedAt - a.updatedAt
232
+ ),
441
233
  persist: () => storage.set(normalizedItem),
442
234
  onSuccess: () => handlersRef.current.onSave?.(normalizedItem),
443
235
  pluginContext: { action: "save", id: normalizedItem.id, item: normalizedItem }
@@ -533,9 +325,7 @@ function KeepProvider({
533
325
  const idSet = new Set(ids);
534
326
  const currentItems = itemsRef.current.filter((item) => idSet.has(item.id));
535
327
  await Promise.all(
536
- currentItems.map(
537
- (item) => updateTags(item.id, normalizeKeepTags([...item.tags ?? [], ...additions]))
538
- )
328
+ currentItems.map((item) => updateTags(item.id, normalizeKeepTags([...item.tags ?? [], ...additions])))
539
329
  );
540
330
  },
541
331
  [updateTags]
@@ -547,10 +337,7 @@ function KeepProvider({
547
337
  const currentItems = itemsRef.current.filter((item) => idSet.has(item.id));
548
338
  await Promise.all(
549
339
  currentItems.map(
550
- (item) => updateTags(
551
- item.id,
552
- normalizeKeepTags((item.tags ?? []).filter((tag) => !removals.has(tag)))
553
- )
340
+ (item) => updateTags(item.id, normalizeKeepTags((item.tags ?? []).filter((tag) => !removals.has(tag))))
554
341
  )
555
342
  );
556
343
  },
@@ -614,10 +401,65 @@ function KeepProvider({
614
401
  })),
615
402
  [runMutation, storage]
616
403
  );
617
- const flushSync = useCallback(
618
- () => syncStorage ? syncStorage.flushSync() : Promise.resolve(),
619
- [syncStorage]
404
+ const revalidateItems = useCallback(
405
+ async (revalidator, options = {}) => {
406
+ pendingMutationsRef.current += 1;
407
+ store.setState({ isMutating: true });
408
+ const run = enqueueOperation(async () => {
409
+ const previous = itemsRef.current;
410
+ let persistenceStarted = false;
411
+ try {
412
+ const summary = await revalidateKeepItems(previous, revalidator, options);
413
+ const pluginContext = { action: "revalidate", items: summary.updatedItems };
414
+ await runBeforePlugins(pluginContext);
415
+ if (summary.updatedItems.length > 0) {
416
+ persistenceStarted = true;
417
+ if (storage.setMany) await storage.setMany(summary.updatedItems);
418
+ else for (const item of summary.updatedItems) await storage.set(item);
419
+ }
420
+ if (summary.removedIds.length > 0) {
421
+ persistenceStarted = true;
422
+ if (storage.removeMany) await storage.removeMany(summary.removedIds);
423
+ else for (const id of summary.removedIds) await storage.remove(id);
424
+ }
425
+ setItems(summary.items);
426
+ store.setState({ error: null });
427
+ const removedIdSet = new Set(summary.removedIds);
428
+ for (const result of summary.results) {
429
+ if (removedIdSet.has(result.item.id)) handlersRef.current.onRemove?.(result.item);
430
+ }
431
+ await runAfterPlugins(pluginContext);
432
+ void Promise.resolve(handlersRef.current.onChange?.({ ...pluginContext, phase: "local" })).catch(
433
+ (cause) => reportError(cause, { action: "revalidate" })
434
+ );
435
+ return summary;
436
+ } catch (cause) {
437
+ if (persistenceStarted) await restoreItems(storage, previous);
438
+ setItems(previous);
439
+ reportError(cause, { action: "revalidate" });
440
+ throw cause;
441
+ }
442
+ });
443
+ return run.finally(() => {
444
+ pendingMutationsRef.current -= 1;
445
+ if (pendingMutationsRef.current === 0) store.setState({ isMutating: false });
446
+ });
447
+ },
448
+ [enqueueOperation, reportError, runAfterPlugins, runBeforePlugins, setItems, storage, store]
449
+ );
450
+ const refreshItemMetadata = useCallback(
451
+ async (id, refresh2) => {
452
+ if (!itemsRef.current.some((item) => item.id === id)) {
453
+ throw new Error(`Cannot refresh metadata for missing item "${id}".`);
454
+ }
455
+ await revalidateItems(async (item) => {
456
+ if (item.id !== id) return "available";
457
+ return { status: "available", meta: await refresh2(item) };
458
+ });
459
+ },
460
+ [revalidateItems]
620
461
  );
462
+ const flushSync = useCallback(() => syncStorage ? syncStorage.flushSync() : Promise.resolve(), [syncStorage]);
621
463
  const value = useMemo(
622
464
  () => ({
623
465
  items,
@@ -636,7 +478,9 @@ function KeepProvider({
636
478
  removeItems,
637
479
  clear,
638
480
  refresh,
639
- flushSync
481
+ flushSync,
482
+ refreshItemMetadata,
483
+ revalidateItems
640
484
  }),
641
485
  [
642
486
  clear,
@@ -655,7 +499,9 @@ function KeepProvider({
655
499
  updateTagsBatch,
656
500
  addTagsBatch,
657
501
  removeTagsBatch,
658
- removeItems
502
+ removeItems,
503
+ refreshItemMetadata,
504
+ revalidateItems
659
505
  ]
660
506
  );
661
507
  const actions = useMemo(
@@ -669,7 +515,9 @@ function KeepProvider({
669
515
  removeItem,
670
516
  removeItems,
671
517
  clear,
672
- refresh
518
+ refresh,
519
+ refreshItemMetadata,
520
+ revalidateItems
673
521
  }),
674
522
  [
675
523
  addTagsBatch,
@@ -681,7 +529,9 @@ function KeepProvider({
681
529
  saveItem,
682
530
  updateNote,
683
531
  updateTags,
684
- updateTagsBatch
532
+ updateTagsBatch,
533
+ refreshItemMetadata,
534
+ revalidateItems
685
535
  ]
686
536
  );
687
537
  const storeAccess = useMemo(() => ({ store, actions }), [actions, store]);
@@ -698,6 +548,16 @@ function isSyncCapableStorage(storage) {
698
548
  async function parseKeepMetaItem(item, schema) {
699
549
  return { ...item, meta: await parseKeepMeta(schema, item.meta) };
700
550
  }
551
+ async function restoreItems(storage, items) {
552
+ try {
553
+ if (storage.setMany) {
554
+ await storage.setMany(items);
555
+ return;
556
+ }
557
+ for (const item of items) await storage.set(item);
558
+ } catch {
559
+ }
560
+ }
701
561
  function useKeepContext() {
702
562
  const context = useContext(KeepContext);
703
563
  if (!context) throw new Error("Keep hooks must be used inside a KeepProvider");
@@ -759,6 +619,10 @@ function useKeepItem(id, itemPayload) {
759
619
  const toggle = useCallback3(() => item ? remove() : save(), [item, remove, save]);
760
620
  const updateNote = useCallback3((note) => actions.updateNote(id, note), [actions, id]);
761
621
  const updateTags = useCallback3((tags) => actions.updateTags(id, tags), [actions, id]);
622
+ const refreshMetadata = useCallback3(
623
+ (refresh) => actions.refreshItemMetadata(id, refresh),
624
+ [actions, id]
625
+ );
762
626
  return {
763
627
  item,
764
628
  isSaved: Boolean(item),
@@ -769,7 +633,8 @@ function useKeepItem(id, itemPayload) {
769
633
  remove,
770
634
  toggle,
771
635
  updateNote,
772
- updateTags
636
+ updateTags,
637
+ refreshMetadata
773
638
  };
774
639
  }
775
640
 
@@ -873,14 +738,8 @@ function useKeepList(options = {}) {
873
738
  (ids, tags2) => actions.updateTagsBatch(ids, tags2),
874
739
  [actions]
875
740
  );
876
- const addTagsBatch = useCallback4(
877
- (ids, tags2) => actions.addTagsBatch(ids, tags2),
878
- [actions]
879
- );
880
- const removeTagsBatch = useCallback4(
881
- (ids, tags2) => actions.removeTagsBatch(ids, tags2),
882
- [actions]
883
- );
741
+ const addTagsBatch = useCallback4((ids, tags2) => actions.addTagsBatch(ids, tags2), [actions]);
742
+ const removeTagsBatch = useCallback4((ids, tags2) => actions.removeTagsBatch(ids, tags2), [actions]);
884
743
  return {
885
744
  items,
886
745
  totalCount,
@@ -896,55 +755,10 @@ function useKeepList(options = {}) {
896
755
  addTagsBatch,
897
756
  removeTagsBatch,
898
757
  clear: actions.clear,
899
- refresh: actions.refresh
758
+ refresh: actions.refresh,
759
+ revalidate: actions.revalidateItems
900
760
  };
901
761
  }
902
- function queryKeepItems(source, options = {}) {
903
- const filtered = source.filter((item) => {
904
- const [from, to] = options.savedBetween ?? [];
905
- const savedAt = item.savedAt;
906
- const lowerBound = from === void 0 ? void 0 : toTimestamp(from);
907
- const upperBound = to === void 0 ? void 0 : toTimestamp(to);
908
- return (options.targetType === void 0 || item.targetType === options.targetType) && (options.tag === void 0 || item.tags?.includes(options.tag) === true) && (options.tags === void 0 || options.tags.every((tag) => item.tags?.includes(tag))) && (lowerBound === void 0 || savedAt >= lowerBound) && (upperBound === void 0 || savedAt <= upperBound) && matchesSearch(item, options.searchQuery, options.search) && (options.filter?.(item) ?? true) && (options.filterFn?.(item) ?? true);
909
- });
910
- const tagCounts = getTagCounts(filtered);
911
- const sortBy = options.sortBy ?? options.sort?.by;
912
- const direction = (options.order ?? options.sort?.direction) === "asc" ? 1 : -1;
913
- const sorted = sortBy ? [...filtered].sort((a, b) => (a[sortBy] - b[sortBy]) * direction) : filtered;
914
- const offset = Math.max(0, options.offset ?? 0);
915
- const items = options.limit === void 0 ? sorted.slice(offset) : sorted.slice(offset, offset + Math.max(0, options.limit));
916
- return { items, totalCount: sorted.length, tagCounts };
917
- }
918
- function getTagCounts(items) {
919
- const counts = {};
920
- for (const item of items) {
921
- for (const tag of item.tags ?? []) counts[tag] = (counts[tag] ?? 0) + 1;
922
- }
923
- return Object.fromEntries(Object.entries(counts).sort(([a], [b]) => a.localeCompare(b)));
924
- }
925
- function matchesSearch(item, searchQuery, search) {
926
- const query = search?.query ?? searchQuery;
927
- if (!query?.trim()) return true;
928
- const fields = search?.fields ?? ["note", "meta", "tags"];
929
- const values = fields.map((field) => {
930
- if (field === "note") return item.note ?? "";
931
- if (field === "tags") return (item.tags ?? []).join(" ");
932
- try {
933
- return JSON.stringify(item.meta) ?? "";
934
- } catch {
935
- return String(item.meta);
936
- }
937
- });
938
- const text = values.join(" ").toLocaleLowerCase();
939
- if (!search) return text.includes(query.trim().toLocaleLowerCase());
940
- const normalized = query.trim().toLocaleLowerCase();
941
- const needles = search.tokenize === false ? [normalized] : normalized.split(/\s+/).filter(Boolean);
942
- const matches = needles.map((needle) => text.includes(needle));
943
- return search.mode === "or" ? matches.some(Boolean) : matches.every(Boolean);
944
- }
945
- function toTimestamp(value) {
946
- return value instanceof Date ? value.getTime() : value;
947
- }
948
762
  function sameItems(left, right) {
949
763
  return left.length === right.length && left.every((item, index) => item === right[index]);
950
764
  }
@@ -957,6 +771,62 @@ function sameCounts(left, right) {
957
771
  });
958
772
  }
959
773
 
774
+ // src/hooks/useKeepShortcut.ts
775
+ import { useEffect as useEffect2 } from "react";
776
+ function useKeepShortcut(options) {
777
+ const item = useKeepItem(options.id ?? "", options.itemPayload);
778
+ const {
779
+ action = "toggle",
780
+ allowInEditable = false,
781
+ enabled = true,
782
+ key,
783
+ modifier,
784
+ onError,
785
+ onTrigger,
786
+ preventDefault = true
787
+ } = options;
788
+ useEffect2(() => {
789
+ if (!enabled) return;
790
+ const handleKeyDown = (event) => {
791
+ if (!allowInEditable && isEditableTarget(event.target)) return;
792
+ if (!matchesShortcut(event, key, modifier)) return;
793
+ if (preventDefault) event.preventDefault();
794
+ const run = onTrigger ? onTrigger(event) : options.id ? action === "save" ? item.save() : action === "remove" ? item.remove() : item.toggle() : void 0;
795
+ if (run) void Promise.resolve(run).catch((error) => onError?.(error));
796
+ };
797
+ window.addEventListener("keydown", handleKeyDown);
798
+ return () => window.removeEventListener("keydown", handleKeyDown);
799
+ }, [
800
+ action,
801
+ allowInEditable,
802
+ enabled,
803
+ item.remove,
804
+ item.save,
805
+ item.toggle,
806
+ key,
807
+ modifier,
808
+ onError,
809
+ onTrigger,
810
+ options.id,
811
+ preventDefault
812
+ ]);
813
+ }
814
+ function matchesShortcut(event, key, modifier) {
815
+ if (event.key.toLocaleLowerCase() !== key.toLocaleLowerCase()) return false;
816
+ const modifiers = {
817
+ meta: event.metaKey,
818
+ ctrl: event.ctrlKey,
819
+ alt: event.altKey,
820
+ shift: event.shiftKey
821
+ };
822
+ if (modifier ? !modifiers[modifier] : Object.values(modifiers).some(Boolean)) return false;
823
+ return Object.entries(modifiers).every(([name, pressed]) => name === modifier || !pressed);
824
+ }
825
+ function isEditableTarget(target) {
826
+ if (!(target instanceof HTMLElement)) return false;
827
+ return target.isContentEditable || target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT";
828
+ }
829
+
960
830
  // src/KeepButton.tsx
961
831
  import {
962
832
  Children,
@@ -969,6 +839,9 @@ function KeepButton({
969
839
  children,
970
840
  savedLabel = "Saved",
971
841
  unsavedLabel = "Save",
842
+ savedAriaLabel,
843
+ unsavedAriaLabel,
844
+ getAriaLabel,
972
845
  asChild = false,
973
846
  onToggleError,
974
847
  onClick,
@@ -984,6 +857,7 @@ function KeepButton({
984
857
  const { isSaved, toggle } = state;
985
858
  const isDisabled = disabled ?? state.isMutating;
986
859
  async function handleClick(event) {
860
+ if (isDisabled) return;
987
861
  if (asChild) {
988
862
  onClick?.(event);
989
863
  } else {
@@ -1000,33 +874,53 @@ function KeepButton({
1000
874
  }
1001
875
  const content = typeof children === "function" ? children(state) : children ?? (isSaved ? savedLabel : unsavedLabel);
1002
876
  function handleElementClick(event) {
877
+ if (isDisabled) return;
1003
878
  if (asChild && isValidElement(content)) {
1004
879
  content.props.onClick?.(event);
1005
880
  }
1006
881
  if (!event.defaultPrevented) void handleClick(event);
1007
882
  }
883
+ function handleKeyDown(event) {
884
+ if (asChild && isValidElement(content)) {
885
+ content.props.onKeyDown?.(event);
886
+ }
887
+ buttonProps.onKeyDown?.(event);
888
+ if (!event.defaultPrevented && !isDisabled && asChild && (event.key === "Enter" || event.key === " ")) {
889
+ void handleClick(event);
890
+ event.preventDefault();
891
+ }
892
+ }
893
+ const child = asChild ? Children.only(content) : void 0;
894
+ if (asChild && !isValidElement(child)) {
895
+ throw new Error("KeepButton with asChild requires a single React element child.");
896
+ }
1008
897
  const commonProps = {
1009
898
  ...buttonProps,
1010
899
  "aria-pressed": isSaved,
1011
- "aria-label": ("aria-label" in buttonProps ? buttonProps["aria-label"] : void 0) ?? (isSaved ? "Remove saved item" : "Save item"),
1012
- disabled: isDisabled,
1013
- onClick: handleElementClick
900
+ "aria-label": ("aria-label" in buttonProps ? buttonProps["aria-label"] : void 0) ?? getAriaLabel?.(state) ?? (isSaved ? savedAriaLabel : unsavedAriaLabel) ?? getAccessibleLabel(isSaved, item, asChild),
901
+ ...asChild ? {
902
+ "aria-disabled": isDisabled,
903
+ role: buttonProps.role ?? "button",
904
+ tabIndex: isDisabled ? -1 : buttonProps.tabIndex ?? 0
905
+ } : { disabled: isDisabled },
906
+ onClick: handleElementClick,
907
+ onKeyDown: handleKeyDown
1014
908
  };
1015
909
  if (asChild) {
1016
- const child = Children.only(content);
1017
- if (!isValidElement(child)) {
1018
- throw new Error("KeepButton with asChild requires a single React element child.");
1019
- }
1020
910
  return cloneElement(child, commonProps);
1021
911
  }
1022
- return /* @__PURE__ */ jsx2(
1023
- "button",
1024
- {
1025
- ...commonProps,
1026
- type: "type" in buttonProps ? buttonProps.type ?? "button" : "button",
1027
- children: content
1028
- }
1029
- );
912
+ return /* @__PURE__ */ jsx2("button", { ...commonProps, type: "type" in buttonProps ? buttonProps.type ?? "button" : "button", children: content });
913
+ }
914
+ function getAccessibleLabel(isSaved, item, asChild) {
915
+ if (!asChild) return isSaved ? "Remove saved item" : "Save item";
916
+ const title = getMetaTitle(item.meta);
917
+ const subject = title ? `${item.targetType ?? "item"}: ${title}` : item.targetType ?? "item";
918
+ return `${isSaved ? "Remove" : "Save"} ${subject}`;
919
+ }
920
+ function getMetaTitle(meta) {
921
+ if (typeof meta !== "object" || meta === null || !("title" in meta)) return void 0;
922
+ const title = meta.title;
923
+ return typeof title === "string" && title.trim() ? title.trim() : void 0;
1030
924
  }
1031
925
 
1032
926
  // src/createKeepKit.tsx
@@ -1037,57 +931,19 @@ function createKeepKit(options = {}) {
1037
931
  KeepButton: (props) => /* @__PURE__ */ jsx3(KeepButton, { ...props }),
1038
932
  useKeepContext: () => useKeepContext(),
1039
933
  useKeepItem: (id, itemPayload) => useKeepItem(id, itemPayload),
1040
- useKeepList: (options2) => useKeepList(options2)
1041
- };
1042
- }
1043
-
1044
- // src/integrations.ts
1045
- function createKeepInvalidationPlugin(options) {
1046
- return {
1047
- name: options.name ?? "keepkit-cache-invalidation",
1048
- after: async (context) => {
1049
- const keys = typeof options.queryKeys === "function" ? options.queryKeys(context) : [options.queryKeys];
1050
- await Promise.all(keys.map((queryKey) => options.invalidate(queryKey, context)));
1051
- }
934
+ useKeepList: (options2) => useKeepList(options2),
935
+ useKeepShortcut: (shortcutOptions) => useKeepShortcut(shortcutOptions)
1052
936
  };
1053
937
  }
1054
938
  export {
1055
- DEFAULT_INDEXEDDB_DATABASE,
1056
- DEFAULT_INDEXEDDB_STORE,
1057
- DEFAULT_STORAGE_KEY,
1058
- DEFAULT_SYNC_QUEUE_DATABASE,
1059
- DEFAULT_SYNC_QUEUE_KEY,
1060
- DEFAULT_SYNC_QUEUE_STORE,
1061
- IndexedDBAdapter,
1062
- IndexedDBSyncQueueAdapter,
1063
- KEEP_BACKUP_FORMAT,
1064
- KEEP_BACKUP_VERSION,
1065
- KeepBackupImportError,
1066
- KeepBackupParseError,
1067
939
  KeepButton,
1068
940
  KeepProvider,
1069
- KeepSchemaValidationError,
1070
- KeepStorageAccessError,
1071
- KeepStorageError,
1072
- KeepStorageParseError,
1073
- KeepStorageQuotaError,
1074
- LocalStorageAdapter,
1075
- LocalStorageSyncQueueAdapter,
1076
- SyncStorageAdapter,
1077
- createKeepInvalidationPlugin,
1078
941
  createKeepKit,
1079
- createStorageAdapter,
1080
- exportItems,
1081
- getTagCounts,
1082
- importItems,
1083
- mergeKeepItems,
1084
- migrateKeepItems,
1085
- normalizeKeepTags,
1086
- parseKeepMeta,
1087
- queryKeepItems,
942
+ isKeepItemMetadataStale,
1088
943
  useKeepContext,
1089
944
  useKeepItem,
1090
945
  useKeepList,
1091
- validateKeepItem
946
+ useKeepShortcut,
947
+ useKeepStore
1092
948
  };
1093
- //# sourceMappingURL=index.js.map
949
+ //# sourceMappingURL=react.js.map