@keepkit/core 0.16.0 → 0.18.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/react.js CHANGED
@@ -2,23 +2,34 @@
2
2
  import {
3
3
  KeepStore,
4
4
  exportItems,
5
+ getKeepNavigationState,
5
6
  importItems,
6
7
  isKeepItemMetadataStale,
8
+ moveKeepItem,
9
+ orderKeepItems,
7
10
  queryKeepItems,
11
+ reorderKeepItems,
8
12
  revalidateKeepItems
9
- } from "./chunk-MDTL5L64.js";
13
+ } from "./chunk-62I2YZYI.js";
10
14
  import {
11
15
  parseKeepMeta
12
- } from "./chunk-THZ3ACR2.js";
16
+ } from "./chunk-5W4QSJHV.js";
13
17
  import {
14
18
  createBrowserStorageAdapter,
15
19
  normalizeKeepTags
16
- } from "./chunk-VJDO3GFH.js";
20
+ } from "./chunk-PNP7OALR.js";
17
21
 
18
- // src/hooks/useKeepItem.ts
22
+ // src/react/components/KeepButton.tsx
23
+ import {
24
+ Children,
25
+ cloneElement,
26
+ isValidElement
27
+ } from "react";
28
+
29
+ // src/react/hooks/useKeepItem.ts
19
30
  import { useCallback as useCallback3 } from "react";
20
31
 
21
- // src/KeepProvider.tsx
32
+ // src/react/components/KeepProvider.tsx
22
33
  import {
23
34
  createContext,
24
35
  useCallback,
@@ -29,7 +40,7 @@ import {
29
40
  useSyncExternalStore
30
41
  } from "react";
31
42
 
32
- // src/KeepErrorBoundary.tsx
43
+ // src/react/components/KeepErrorBoundary.tsx
33
44
  import { Component } from "react";
34
45
  var KeepErrorBoundary = class extends Component {
35
46
  constructor() {
@@ -55,7 +66,7 @@ var KeepErrorBoundary = class extends Component {
55
66
  }
56
67
  };
57
68
 
58
- // src/KeepProvider.tsx
69
+ // src/react/components/KeepProvider.tsx
59
70
  import { jsx } from "react/jsx-runtime";
60
71
  var defaultStorage = createBrowserStorageAdapter();
61
72
  var KeepContext = createContext(null);
@@ -331,17 +342,77 @@ function KeepProviderContent({
331
342
  reportError(cause, { action: "save", id: item.id });
332
343
  throw cause;
333
344
  }
334
- await runMutation("save", normalizedItem.id, (previous) => ({
335
- next: [...previous.filter((current) => current.id !== normalizedItem.id), normalizedItem].sort(
336
- (a, b) => b.updatedAt - a.updatedAt
337
- ),
338
- persist: () => storage.set(normalizedItem),
339
- onSuccess: () => handlersRef.current.onSave?.(normalizedItem),
340
- pluginContext: { action: "save", id: normalizedItem.id, item: normalizedItem }
341
- }));
345
+ await runMutation("save", normalizedItem.id, (previous) => {
346
+ const current = previous.find((item2) => item2.id === normalizedItem.id);
347
+ const hasCustomOrder = previous.some((item2) => item2.order !== void 0);
348
+ const nextItem = {
349
+ ...normalizedItem,
350
+ ...normalizedItem.order === void 0 && current?.order !== void 0 ? { order: current.order } : {},
351
+ ...normalizedItem.order === void 0 && !current && hasCustomOrder ? { order: previous.reduce((max, item2) => Math.max(max, item2.order ?? -1), -1) + 1 } : {}
352
+ };
353
+ const withoutCurrent = previous.filter((item2) => item2.id !== nextItem.id);
354
+ const next = hasCustomOrder ? reorderKeepItems(
355
+ [...withoutCurrent, nextItem],
356
+ orderKeepItems(previous).map((item2) => item2.id === nextItem.id ? nextItem.id : item2.id)
357
+ ) : [...withoutCurrent, nextItem].sort((a, b) => b.updatedAt - a.updatedAt);
358
+ return {
359
+ next,
360
+ persist: () => storage.set(nextItem),
361
+ onSuccess: () => handlersRef.current.onSave?.(nextItem),
362
+ pluginContext: { action: "save", id: nextItem.id, item: nextItem }
363
+ };
364
+ });
342
365
  },
343
366
  [reportError, runMutation, storage]
344
367
  );
368
+ const reorderItems = useCallback(
369
+ async (orderedIds) => {
370
+ await runMutation("reorder", void 0, (previous) => {
371
+ const next = reorderKeepItems(previous, orderedIds);
372
+ const changedItems = next.filter(
373
+ (item, index) => item.id !== previous[index]?.id || item.order !== previous[index]?.order
374
+ );
375
+ return {
376
+ next,
377
+ persist: async () => {
378
+ try {
379
+ if (storage.setMany) await storage.setMany(changedItems);
380
+ else for (const item of changedItems) await storage.set(item);
381
+ } catch (cause) {
382
+ await restoreItems(storage, previous);
383
+ throw cause;
384
+ }
385
+ },
386
+ pluginContext: { action: "reorder", items: changedItems }
387
+ };
388
+ });
389
+ },
390
+ [runMutation, storage]
391
+ );
392
+ const moveItem = useCallback(
393
+ async (id, targetIndex) => {
394
+ await runMutation("reorder", id, (previous) => {
395
+ const next = moveKeepItem(previous, id, targetIndex);
396
+ const changedItems = next.filter(
397
+ (item, index) => item.id !== previous[index]?.id || item.order !== previous[index]?.order
398
+ );
399
+ return {
400
+ next,
401
+ persist: async () => {
402
+ try {
403
+ if (storage.setMany) await storage.setMany(changedItems);
404
+ else for (const item of changedItems) await storage.set(item);
405
+ } catch (cause) {
406
+ await restoreItems(storage, previous);
407
+ throw cause;
408
+ }
409
+ },
410
+ pluginContext: { action: "reorder", id, items: changedItems }
411
+ };
412
+ });
413
+ },
414
+ [runMutation, storage]
415
+ );
345
416
  const updateNote = useCallback(
346
417
  async (id, note) => {
347
418
  const nextNote = note?.trim() || void 0;
@@ -501,7 +572,8 @@ function KeepProviderContent({
501
572
  const rememberUndo = useCallback(
502
573
  (removedItems) => {
503
574
  undoRef.current?.timer && clearTimeout(undoRef.current.timer);
504
- const expiresAt = Date.now() + Math.max(0, undoTimeoutMs);
575
+ const startedAt = Date.now();
576
+ const expiresAt = startedAt + Math.max(0, undoTimeoutMs);
505
577
  const timer = setTimeout(
506
578
  () => {
507
579
  undoRef.current = void 0;
@@ -510,7 +582,7 @@ function KeepProviderContent({
510
582
  Math.max(0, undoTimeoutMs)
511
583
  );
512
584
  undoRef.current = { items: removedItems, expiresAt, timer };
513
- store.setState({ undo: { canUndo: true, ids: removedItems.map((item) => item.id), expiresAt } });
585
+ store.setState({ undo: { canUndo: true, ids: removedItems.map((item) => item.id), startedAt, expiresAt } });
514
586
  },
515
587
  [store, undoTimeoutMs]
516
588
  );
@@ -729,6 +801,8 @@ function KeepProviderContent({
729
801
  resolveSyncConflict,
730
802
  refreshItemMetadata,
731
803
  revalidateItems,
804
+ reorderItems,
805
+ moveItem,
732
806
  exportBackup,
733
807
  importBackup
734
808
  }),
@@ -758,6 +832,8 @@ function KeepProviderContent({
758
832
  removeItems,
759
833
  refreshItemMetadata,
760
834
  revalidateItems,
835
+ reorderItems,
836
+ moveItem,
761
837
  exportBackup,
762
838
  importBackup
763
839
  ]
@@ -778,7 +854,9 @@ function KeepProviderContent({
778
854
  clear,
779
855
  refresh,
780
856
  refreshItemMetadata,
781
- revalidateItems
857
+ revalidateItems,
858
+ reorderItems,
859
+ moveItem
782
860
  }),
783
861
  [
784
862
  addTagsBatch,
@@ -793,6 +871,8 @@ function KeepProviderContent({
793
871
  updateTagsBatch,
794
872
  refreshItemMetadata,
795
873
  revalidateItems,
874
+ reorderItems,
875
+ moveItem,
796
876
  removeItemWithUndo,
797
877
  removeItemsWithUndo,
798
878
  undoLastRemoval
@@ -838,7 +918,7 @@ function useKeepStore() {
838
918
  return context;
839
919
  }
840
920
 
841
- // src/hooks/useKeepStoreSelector.ts
921
+ // src/react/hooks/useKeepStoreSelector.ts
842
922
  import { useCallback as useCallback2, useRef as useRef2, useSyncExternalStore as useSyncExternalStore2 } from "react";
843
923
  function useKeepStoreSelector(store, selector) {
844
924
  const cacheRef = useRef2(null);
@@ -853,7 +933,7 @@ function useKeepStoreSelector(store, selector) {
853
933
  return useSyncExternalStore2(store.subscribe, getSelectedSnapshot, getSelectedSnapshot);
854
934
  }
855
935
 
856
- // src/hooks/useKeepItem.ts
936
+ // src/react/hooks/useKeepItem.ts
857
937
  function useKeepItem(input) {
858
938
  const { store, actions } = useKeepStore();
859
939
  const id = input?.id ?? "";
@@ -873,15 +953,17 @@ function useKeepItem(input) {
873
953
  store,
874
954
  useCallback3((state) => state.error, [])
875
955
  );
956
+ const currentOrder = item?.order;
876
957
  const save = useCallback3(async () => {
877
958
  if (!input) throw new Error("An item input is required to save an item.");
878
959
  const now = Date.now();
879
960
  await actions.saveItem({
880
961
  ...input,
962
+ ...currentOrder === void 0 ? {} : { order: currentOrder },
881
963
  savedAt: item?.savedAt ?? now,
882
964
  updatedAt: now
883
965
  });
884
- }, [actions, input, item?.savedAt]);
966
+ }, [actions, currentOrder, input, item?.savedAt]);
885
967
  const remove = useCallback3(() => actions.removeItem(id), [actions, id]);
886
968
  const removeWithUndo = useCallback3(() => actions.removeItemWithUndo(id), [actions, id]);
887
969
  const toggle = useCallback3(() => item ? remove() : save(), [item, remove, save]);
@@ -908,7 +990,96 @@ function useKeepItem(input) {
908
990
  };
909
991
  }
910
992
 
911
- // src/hooks/useKeepList.ts
993
+ // src/react/components/KeepButton.tsx
994
+ import { jsx as jsx2 } from "react/jsx-runtime";
995
+ function KeepButton({
996
+ item,
997
+ children,
998
+ savedLabel = "Saved",
999
+ unsavedLabel = "Save",
1000
+ savedAriaLabel,
1001
+ unsavedAriaLabel,
1002
+ getAriaLabel,
1003
+ asChild = false,
1004
+ onToggleError,
1005
+ onClick,
1006
+ disabled,
1007
+ ...buttonProps
1008
+ }) {
1009
+ const state = useKeepItem(item);
1010
+ const { isSaved, toggle } = state;
1011
+ const isDisabled = disabled ?? state.isMutating;
1012
+ async function handleClick(event) {
1013
+ if (isDisabled) return;
1014
+ if (asChild) {
1015
+ onClick?.(event);
1016
+ } else {
1017
+ onClick?.(
1018
+ event
1019
+ );
1020
+ }
1021
+ if (event.defaultPrevented) return;
1022
+ try {
1023
+ await toggle();
1024
+ } catch (error) {
1025
+ onToggleError?.(error);
1026
+ }
1027
+ }
1028
+ const content = typeof children === "function" ? children(state) : children ?? (isSaved ? savedLabel : unsavedLabel);
1029
+ function handleElementClick(event) {
1030
+ if (isDisabled) return;
1031
+ if (asChild && isValidElement(content)) {
1032
+ content.props.onClick?.(event);
1033
+ }
1034
+ if (!event.defaultPrevented) void handleClick(event);
1035
+ }
1036
+ function handleKeyDown(event) {
1037
+ if (asChild && isValidElement(content)) {
1038
+ content.props.onKeyDown?.(event);
1039
+ }
1040
+ buttonProps.onKeyDown?.(event);
1041
+ if (!event.defaultPrevented && !isDisabled && asChild && (event.key === "Enter" || event.key === " ")) {
1042
+ void handleClick(event);
1043
+ event.preventDefault();
1044
+ }
1045
+ }
1046
+ const child = asChild ? Children.only(content) : void 0;
1047
+ if (asChild && !isValidElement(child)) {
1048
+ throw new Error("KeepButton with asChild requires a single React element child.");
1049
+ }
1050
+ const commonProps = {
1051
+ ...buttonProps,
1052
+ "aria-pressed": isSaved,
1053
+ "data-state": state.error ? "error" : isSaved ? "saved" : "unsaved",
1054
+ "data-loading": state.isLoading || state.isMutating ? "true" : void 0,
1055
+ "data-disabled": isDisabled ? "true" : void 0,
1056
+ "aria-label": ("aria-label" in buttonProps ? buttonProps["aria-label"] : void 0) ?? getAriaLabel?.(state) ?? (isSaved ? savedAriaLabel : unsavedAriaLabel) ?? getAccessibleLabel(isSaved, item, asChild),
1057
+ ...asChild ? {
1058
+ "aria-disabled": isDisabled,
1059
+ role: buttonProps.role ?? "button",
1060
+ tabIndex: isDisabled ? -1 : buttonProps.tabIndex ?? 0
1061
+ } : { disabled: isDisabled },
1062
+ onClick: handleElementClick,
1063
+ onKeyDown: handleKeyDown
1064
+ };
1065
+ if (asChild) {
1066
+ return cloneElement(child, commonProps);
1067
+ }
1068
+ return /* @__PURE__ */ jsx2("button", { ...commonProps, type: "type" in buttonProps ? buttonProps.type ?? "button" : "button", children: content });
1069
+ }
1070
+ function getAccessibleLabel(isSaved, item, asChild) {
1071
+ if (!asChild) return isSaved ? "Remove saved item" : "Save item";
1072
+ const title = getMetaTitle(item.meta);
1073
+ const subject = title ? `${item.targetType ?? "item"}: ${title}` : item.targetType ?? "item";
1074
+ return `${isSaved ? "Remove" : "Save"} ${subject}`;
1075
+ }
1076
+ function getMetaTitle(meta) {
1077
+ if (typeof meta !== "object" || meta === null || !("title" in meta)) return void 0;
1078
+ const title = meta.title;
1079
+ return typeof title === "string" && title.trim() ? title.trim() : void 0;
1080
+ }
1081
+
1082
+ // src/react/hooks/useKeepList.ts
912
1083
  import { useCallback as useCallback4, useMemo as useMemo2 } from "react";
913
1084
  function useKeepList(query = {}) {
914
1085
  const { store, actions } = useKeepStore();
@@ -989,6 +1160,8 @@ function useKeepList(query = {}) {
989
1160
  updateTagsBatch,
990
1161
  addTagsBatch,
991
1162
  removeTagsBatch,
1163
+ reorder: actions.reorderItems,
1164
+ move: actions.moveItem,
992
1165
  clear: actions.clear,
993
1166
  refresh: actions.refresh,
994
1167
  revalidate: actions.revalidateItems
@@ -1006,7 +1179,63 @@ function sameCounts(left, right) {
1006
1179
  });
1007
1180
  }
1008
1181
 
1009
- // src/hooks/useKeepShortcut.ts
1182
+ // src/react/hooks/useKeepNavigator.ts
1183
+ import { useCallback as useCallback5, useMemo as useMemo3, useState } from "react";
1184
+ function useKeepNavigator(options = {}) {
1185
+ const { store } = useKeepStore();
1186
+ const { currentId, initialIndex = 0, query } = options;
1187
+ const [activeIndex, setActiveIndex] = useState(() => Math.max(0, Math.floor(initialIndex)));
1188
+ const queryOptions = useMemo3(() => ({ ...query, pagination: void 0 }), [query]);
1189
+ const selector = useMemo3(() => {
1190
+ let previousSource;
1191
+ let previousItems;
1192
+ let previousPointer;
1193
+ let previousResult;
1194
+ return (state) => {
1195
+ if (previousSource !== state.items) {
1196
+ previousSource = state.items;
1197
+ previousItems = queryKeepItems(state.items, queryOptions).items;
1198
+ previousResult = void 0;
1199
+ }
1200
+ const items = previousItems ?? [];
1201
+ const pointer = currentId ?? activeIndex;
1202
+ if (previousResult && previousPointer === pointer) return previousResult;
1203
+ previousPointer = pointer;
1204
+ previousResult = getKeepNavigationState(items, pointer);
1205
+ return previousResult;
1206
+ };
1207
+ }, [activeIndex, currentId, queryOptions]);
1208
+ const navigation = useKeepStoreSelector(store, selector);
1209
+ const goToIndex = useCallback5(
1210
+ (index) => {
1211
+ const item = navigation.items[index];
1212
+ if (!item) return null;
1213
+ setActiveIndex(index);
1214
+ return item;
1215
+ },
1216
+ [navigation.items]
1217
+ );
1218
+ const goToNext = useCallback5(() => {
1219
+ if (!navigation.nextItem) return null;
1220
+ setActiveIndex(navigation.currentIndex + 1);
1221
+ return navigation.nextItem;
1222
+ }, [navigation.currentIndex, navigation.nextItem]);
1223
+ const goToPrev = useCallback5(() => {
1224
+ if (!navigation.prevItem) return null;
1225
+ setActiveIndex(navigation.currentIndex - 1);
1226
+ return navigation.prevItem;
1227
+ }, [navigation.currentIndex, navigation.prevItem]);
1228
+ const goToItem = useCallback5(
1229
+ (id) => {
1230
+ const index = navigation.items.findIndex((item) => item.id === id);
1231
+ return index < 0 ? null : goToIndex(index);
1232
+ },
1233
+ [goToIndex, navigation.items]
1234
+ );
1235
+ return { ...navigation, goToNext, goToPrev, goToIndex, goToItem };
1236
+ }
1237
+
1238
+ // src/react/hooks/useKeepShortcut.ts
1010
1239
  import { useEffect as useEffect2 } from "react";
1011
1240
  function useKeepShortcut(options) {
1012
1241
  const item = useKeepItem(options.item);
@@ -1062,101 +1291,7 @@ function isEditableTarget(target) {
1062
1291
  return target.isContentEditable || target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT";
1063
1292
  }
1064
1293
 
1065
- // src/KeepButton.tsx
1066
- import {
1067
- Children,
1068
- cloneElement,
1069
- isValidElement
1070
- } from "react";
1071
- import { jsx as jsx2 } from "react/jsx-runtime";
1072
- function KeepButton({
1073
- item,
1074
- children,
1075
- savedLabel = "Saved",
1076
- unsavedLabel = "Save",
1077
- savedAriaLabel,
1078
- unsavedAriaLabel,
1079
- getAriaLabel,
1080
- asChild = false,
1081
- onToggleError,
1082
- onClick,
1083
- disabled,
1084
- ...buttonProps
1085
- }) {
1086
- const state = useKeepItem(item);
1087
- const { isSaved, toggle } = state;
1088
- const isDisabled = disabled ?? state.isMutating;
1089
- async function handleClick(event) {
1090
- if (isDisabled) return;
1091
- if (asChild) {
1092
- onClick?.(event);
1093
- } else {
1094
- onClick?.(
1095
- event
1096
- );
1097
- }
1098
- if (event.defaultPrevented) return;
1099
- try {
1100
- await toggle();
1101
- } catch (error) {
1102
- onToggleError?.(error);
1103
- }
1104
- }
1105
- const content = typeof children === "function" ? children(state) : children ?? (isSaved ? savedLabel : unsavedLabel);
1106
- function handleElementClick(event) {
1107
- if (isDisabled) return;
1108
- if (asChild && isValidElement(content)) {
1109
- content.props.onClick?.(event);
1110
- }
1111
- if (!event.defaultPrevented) void handleClick(event);
1112
- }
1113
- function handleKeyDown(event) {
1114
- if (asChild && isValidElement(content)) {
1115
- content.props.onKeyDown?.(event);
1116
- }
1117
- buttonProps.onKeyDown?.(event);
1118
- if (!event.defaultPrevented && !isDisabled && asChild && (event.key === "Enter" || event.key === " ")) {
1119
- void handleClick(event);
1120
- event.preventDefault();
1121
- }
1122
- }
1123
- const child = asChild ? Children.only(content) : void 0;
1124
- if (asChild && !isValidElement(child)) {
1125
- throw new Error("KeepButton with asChild requires a single React element child.");
1126
- }
1127
- const commonProps = {
1128
- ...buttonProps,
1129
- "aria-pressed": isSaved,
1130
- "data-state": state.error ? "error" : isSaved ? "saved" : "unsaved",
1131
- "data-loading": state.isLoading || state.isMutating ? "true" : void 0,
1132
- "data-disabled": isDisabled ? "true" : void 0,
1133
- "aria-label": ("aria-label" in buttonProps ? buttonProps["aria-label"] : void 0) ?? getAriaLabel?.(state) ?? (isSaved ? savedAriaLabel : unsavedAriaLabel) ?? getAccessibleLabel(isSaved, item, asChild),
1134
- ...asChild ? {
1135
- "aria-disabled": isDisabled,
1136
- role: buttonProps.role ?? "button",
1137
- tabIndex: isDisabled ? -1 : buttonProps.tabIndex ?? 0
1138
- } : { disabled: isDisabled },
1139
- onClick: handleElementClick,
1140
- onKeyDown: handleKeyDown
1141
- };
1142
- if (asChild) {
1143
- return cloneElement(child, commonProps);
1144
- }
1145
- return /* @__PURE__ */ jsx2("button", { ...commonProps, type: "type" in buttonProps ? buttonProps.type ?? "button" : "button", children: content });
1146
- }
1147
- function getAccessibleLabel(isSaved, item, asChild) {
1148
- if (!asChild) return isSaved ? "Remove saved item" : "Save item";
1149
- const title = getMetaTitle(item.meta);
1150
- const subject = title ? `${item.targetType ?? "item"}: ${title}` : item.targetType ?? "item";
1151
- return `${isSaved ? "Remove" : "Save"} ${subject}`;
1152
- }
1153
- function getMetaTitle(meta) {
1154
- if (typeof meta !== "object" || meta === null || !("title" in meta)) return void 0;
1155
- const title = meta.title;
1156
- return typeof title === "string" && title.trim() ? title.trim() : void 0;
1157
- }
1158
-
1159
- // src/createKeepKit.tsx
1294
+ // src/react/createKeepKit.tsx
1160
1295
  import { jsx as jsx3 } from "react/jsx-runtime";
1161
1296
  function createKeepKit(options = {}) {
1162
1297
  return {
@@ -1165,6 +1300,7 @@ function createKeepKit(options = {}) {
1165
1300
  useContext: () => useKeepContext(),
1166
1301
  useItem: (item) => useKeepItem(item),
1167
1302
  useList: (query) => useKeepList(query),
1303
+ useNavigator: (navigatorOptions) => useKeepNavigator(navigatorOptions),
1168
1304
  useShortcut: (shortcutOptions) => useKeepShortcut(shortcutOptions)
1169
1305
  };
1170
1306
  }
@@ -1177,6 +1313,7 @@ export {
1177
1313
  useKeepContext,
1178
1314
  useKeepItem,
1179
1315
  useKeepList,
1316
+ useKeepNavigator,
1180
1317
  useKeepShortcut,
1181
1318
  useKeepStore
1182
1319
  };