@keepkit/ui 0.15.0 → 0.16.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
@@ -6,12 +6,32 @@ import {
6
6
  createKeepKit as createCoreKeepKit
7
7
  } from "@keepkit/core/react";
8
8
 
9
+ // src/hooks/useKeepToastFeedback.ts
10
+ import { useCallback } from "react";
11
+ function useKeepToastFeedback(showToast) {
12
+ return useCallback(
13
+ (event) => {
14
+ if (!("undo" in event)) {
15
+ showToast(event.message);
16
+ return;
17
+ }
18
+ showToast(event.message, {
19
+ action: {
20
+ label: event.undoLabel,
21
+ onClick: () => void event.undo()
22
+ }
23
+ });
24
+ },
25
+ [showToast]
26
+ );
27
+ }
28
+
9
29
  // src/hooks/useKeepBackup.ts
10
30
  import { useKeepContext } from "@keepkit/core/react";
11
31
  import { useRef, useState } from "react";
12
32
 
13
33
  // src/ui-context.tsx
14
- import { createContext, useContext, useMemo } from "react";
34
+ import { createContext, useCallback as useCallback2, useContext, useMemo } from "react";
15
35
 
16
36
  // src/locales/de.ts
17
37
  var DE_LABELS = {
@@ -49,6 +69,9 @@ var DE_LABELS = {
49
69
  applyTags: "Tags anwenden",
50
70
  savedMessage: "Element gespeichert.",
51
71
  removedMessage: "Element entfernt.",
72
+ restoredMessage: "Element wiederhergestellt.",
73
+ syncFailedMessage: "Synchronisierung fehlgeschlagen.",
74
+ stalePrunedMessage: "Nicht verf\xFCgbare Elemente entfernt.",
52
75
  undo: "R\xFCckg\xE4ngig",
53
76
  undoAvailable: "Element entfernt. R\xFCckg\xE4ngig machen?",
54
77
  selectionScope: "Auswahlbereich",
@@ -125,6 +148,9 @@ var EN_LABELS = {
125
148
  applyTags: "Apply tags",
126
149
  savedMessage: "Item saved.",
127
150
  removedMessage: "Item removed.",
151
+ restoredMessage: "Item restored.",
152
+ syncFailedMessage: "Sync failed.",
153
+ stalePrunedMessage: "Unavailable items removed.",
128
154
  undo: "Undo",
129
155
  undoAvailable: "Item removed. Undo?",
130
156
  selectionScope: "Selection scope",
@@ -201,6 +227,9 @@ var ES_LABELS = {
201
227
  applyTags: "Aplicar etiquetas",
202
228
  savedMessage: "Elemento guardado.",
203
229
  removedMessage: "Elemento eliminado.",
230
+ restoredMessage: "Elemento restaurado.",
231
+ syncFailedMessage: "Error de sincronizaci\xF3n.",
232
+ stalePrunedMessage: "Elementos no disponibles eliminados.",
204
233
  undo: "Deshacer",
205
234
  undoAvailable: "Elemento eliminado. \xBFDeshacer?",
206
235
  selectionScope: "\xC1mbito de selecci\xF3n",
@@ -277,6 +306,9 @@ var FIL_LABELS = {
277
306
  applyTags: "Ilapat ang mga tag",
278
307
  savedMessage: "Na-save ang item.",
279
308
  removedMessage: "Inalis ang item.",
309
+ restoredMessage: "Naibalik ang item.",
310
+ syncFailedMessage: "Hindi nagtagumpay ang pag-sync.",
311
+ stalePrunedMessage: "Inalis ang mga hindi available na item.",
280
312
  undo: "I-undo",
281
313
  undoAvailable: "Inalis ang item. I-undo?",
282
314
  selectionScope: "Saklaw ng pagpili",
@@ -353,6 +385,9 @@ var FR_LABELS = {
353
385
  applyTags: "Appliquer les \xE9tiquettes",
354
386
  savedMessage: "\xC9l\xE9ment enregistr\xE9.",
355
387
  removedMessage: "\xC9l\xE9ment supprim\xE9.",
388
+ restoredMessage: "\xC9l\xE9ment restaur\xE9.",
389
+ syncFailedMessage: "\xC9chec de la synchronisation.",
390
+ stalePrunedMessage: "\xC9l\xE9ments indisponibles supprim\xE9s.",
356
391
  undo: "Annuler",
357
392
  undoAvailable: "\xC9l\xE9ment supprim\xE9. Annuler ?",
358
393
  selectionScope: "Port\xE9e de la s\xE9lection",
@@ -429,6 +464,9 @@ var ID_LABELS = {
429
464
  applyTags: "Terapkan tag",
430
465
  savedMessage: "Item tersimpan.",
431
466
  removedMessage: "Item dihapus.",
467
+ restoredMessage: "Item dipulihkan.",
468
+ syncFailedMessage: "Sinkronisasi gagal.",
469
+ stalePrunedMessage: "Item yang tidak tersedia dihapus.",
432
470
  undo: "Urungkan",
433
471
  undoAvailable: "Item dihapus. Urungkan?",
434
472
  selectionScope: "Cakupan pilihan",
@@ -505,6 +543,9 @@ var IT_LABELS = {
505
543
  applyTags: "Applica tag",
506
544
  savedMessage: "Elemento salvato.",
507
545
  removedMessage: "Elemento rimosso.",
546
+ restoredMessage: "Elemento ripristinato.",
547
+ syncFailedMessage: "Sincronizzazione non riuscita.",
548
+ stalePrunedMessage: "Elementi non disponibili rimossi.",
508
549
  undo: "Annulla",
509
550
  undoAvailable: "Elemento rimosso. Annullare?",
510
551
  selectionScope: "Ambito della selezione",
@@ -581,6 +622,9 @@ var JA_LABELS = {
581
622
  applyTags: "\u30BF\u30B0\u3092\u9069\u7528",
582
623
  savedMessage: "\u30A2\u30A4\u30C6\u30E0\u3092\u4FDD\u5B58\u3057\u307E\u3057\u305F\u3002",
583
624
  removedMessage: "\u30A2\u30A4\u30C6\u30E0\u3092\u524A\u9664\u3057\u307E\u3057\u305F\u3002",
625
+ restoredMessage: "\u30A2\u30A4\u30C6\u30E0\u3092\u5FA9\u5143\u3057\u307E\u3057\u305F\u3002",
626
+ syncFailedMessage: "\u540C\u671F\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002",
627
+ stalePrunedMessage: "\u5229\u7528\u3067\u304D\u306A\u3044\u30A2\u30A4\u30C6\u30E0\u3092\u524A\u9664\u3057\u307E\u3057\u305F\u3002",
584
628
  undo: "\u5143\u306B\u623B\u3059",
585
629
  undoAvailable: "\u30A2\u30A4\u30C6\u30E0\u3092\u524A\u9664\u3057\u307E\u3057\u305F\u3002\u5143\u306B\u623B\u3057\u307E\u3059\u304B\uFF1F",
586
630
  selectionScope: "\u9078\u629E\u7BC4\u56F2",
@@ -657,6 +701,9 @@ var KO_LABELS = {
657
701
  applyTags: "\uD0DC\uADF8 \uC801\uC6A9",
658
702
  savedMessage: "\uD56D\uBAA9\uC744 \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4.",
659
703
  removedMessage: "\uD56D\uBAA9\uC744 \uC0AD\uC81C\uD588\uC2B5\uB2C8\uB2E4.",
704
+ restoredMessage: "\uD56D\uBAA9\uC744 \uBCF5\uC6D0\uD588\uC2B5\uB2C8\uB2E4.",
705
+ syncFailedMessage: "\uB3D9\uAE30\uD654\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.",
706
+ stalePrunedMessage: "\uC0AC\uC6A9\uD560 \uC218 \uC5C6\uB294 \uD56D\uBAA9\uC744 \uC0AD\uC81C\uD588\uC2B5\uB2C8\uB2E4.",
660
707
  undo: "\uC2E4\uD589 \uCDE8\uC18C",
661
708
  undoAvailable: "\uD56D\uBAA9\uC744 \uC0AD\uC81C\uD588\uC2B5\uB2C8\uB2E4. \uC2E4\uD589 \uCDE8\uC18C\uD560\uAE4C\uC694?",
662
709
  selectionScope: "\uC120\uD0DD \uBC94\uC704",
@@ -733,6 +780,9 @@ var MS_LABELS = {
733
780
  applyTags: "Gunakan tag",
734
781
  savedMessage: "Item disimpan.",
735
782
  removedMessage: "Item dibuang.",
783
+ restoredMessage: "Item dipulihkan.",
784
+ syncFailedMessage: "Penyegerakan gagal.",
785
+ stalePrunedMessage: "Item yang tidak tersedia dibuang.",
736
786
  undo: "Buat asal",
737
787
  undoAvailable: "Item dibuang. Buat asal?",
738
788
  selectionScope: "Skop pilihan",
@@ -809,6 +859,9 @@ var PT_BR_LABELS = {
809
859
  applyTags: "Aplicar tags",
810
860
  savedMessage: "Item salvo.",
811
861
  removedMessage: "Item removido.",
862
+ restoredMessage: "Item restaurado.",
863
+ syncFailedMessage: "Falha na sincroniza\xE7\xE3o.",
864
+ stalePrunedMessage: "Itens indispon\xEDveis removidos.",
812
865
  undo: "Desfazer",
813
866
  undoAvailable: "Item removido. Desfazer?",
814
867
  selectionScope: "Escopo da sele\xE7\xE3o",
@@ -885,6 +938,9 @@ var RU_LABELS = {
885
938
  applyTags: "\u041F\u0440\u0438\u043C\u0435\u043D\u0438\u0442\u044C \u0442\u0435\u0433\u0438",
886
939
  savedMessage: "\u042D\u043B\u0435\u043C\u0435\u043D\u0442 \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D.",
887
940
  removedMessage: "\u042D\u043B\u0435\u043C\u0435\u043D\u0442 \u0443\u0434\u0430\u043B\u0451\u043D.",
941
+ restoredMessage: "\u042D\u043B\u0435\u043C\u0435\u043D\u0442 \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D.",
942
+ syncFailedMessage: "\u0421\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0430\u0446\u0438\u044F \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D\u0430.",
943
+ stalePrunedMessage: "\u041D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B \u0443\u0434\u0430\u043B\u0435\u043D\u044B.",
888
944
  undo: "\u041E\u0442\u043C\u0435\u043D\u0438\u0442\u044C",
889
945
  undoAvailable: "\u042D\u043B\u0435\u043C\u0435\u043D\u0442 \u0443\u0434\u0430\u043B\u0451\u043D. \u041E\u0442\u043C\u0435\u043D\u0438\u0442\u044C?",
890
946
  selectionScope: "\u041E\u0431\u043B\u0430\u0441\u0442\u044C \u0432\u044B\u0431\u043E\u0440\u0430",
@@ -961,6 +1017,9 @@ var TH_LABELS = {
961
1017
  applyTags: "\u0E43\u0E0A\u0E49\u0E41\u0E17\u0E47\u0E01",
962
1018
  savedMessage: "\u0E1A\u0E31\u0E19\u0E17\u0E36\u0E01\u0E23\u0E32\u0E22\u0E01\u0E32\u0E23\u0E41\u0E25\u0E49\u0E27",
963
1019
  removedMessage: "\u0E25\u0E1A\u0E23\u0E32\u0E22\u0E01\u0E32\u0E23\u0E41\u0E25\u0E49\u0E27",
1020
+ restoredMessage: "\u0E01\u0E39\u0E49\u0E04\u0E37\u0E19\u0E23\u0E32\u0E22\u0E01\u0E32\u0E23\u0E41\u0E25\u0E49\u0E27",
1021
+ syncFailedMessage: "\u0E0B\u0E34\u0E07\u0E04\u0E4C\u0E44\u0E21\u0E48\u0E2A\u0E33\u0E40\u0E23\u0E47\u0E08",
1022
+ stalePrunedMessage: "\u0E25\u0E1A\u0E23\u0E32\u0E22\u0E01\u0E32\u0E23\u0E17\u0E35\u0E48\u0E43\u0E0A\u0E49\u0E44\u0E21\u0E48\u0E44\u0E14\u0E49\u0E41\u0E25\u0E49\u0E27",
964
1023
  undo: "\u0E40\u0E25\u0E34\u0E01\u0E17\u0E33",
965
1024
  undoAvailable: "\u0E25\u0E1A\u0E23\u0E32\u0E22\u0E01\u0E32\u0E23\u0E41\u0E25\u0E49\u0E27 \u0E15\u0E49\u0E2D\u0E07\u0E01\u0E32\u0E23\u0E40\u0E25\u0E34\u0E01\u0E17\u0E33\u0E2B\u0E23\u0E37\u0E2D\u0E44\u0E21\u0E48",
966
1025
  selectionScope: "\u0E02\u0E2D\u0E1A\u0E40\u0E02\u0E15\u0E01\u0E32\u0E23\u0E40\u0E25\u0E37\u0E2D\u0E01",
@@ -1037,6 +1096,9 @@ var VI_LABELS = {
1037
1096
  applyTags: "\xC1p d\u1EE5ng th\u1EBB",
1038
1097
  savedMessage: "\u0110\xE3 l\u01B0u m\u1EE5c.",
1039
1098
  removedMessage: "\u0110\xE3 x\xF3a m\u1EE5c.",
1099
+ restoredMessage: "\u0110\xE3 kh\xF4i ph\u1EE5c m\u1EE5c.",
1100
+ syncFailedMessage: "\u0110\u1ED3ng b\u1ED9 kh\xF4ng th\xE0nh c\xF4ng.",
1101
+ stalePrunedMessage: "\u0110\xE3 x\xF3a c\xE1c m\u1EE5c kh\xF4ng kh\u1EA3 d\u1EE5ng.",
1040
1102
  undo: "Ho\xE0n t\xE1c",
1041
1103
  undoAvailable: "\u0110\xE3 x\xF3a m\u1EE5c. Ho\xE0n t\xE1c?",
1042
1104
  selectionScope: "Ph\u1EA1m vi l\u1EF1a ch\u1ECDn",
@@ -1113,6 +1175,9 @@ var ZH_HANS_LABELS = {
1113
1175
  applyTags: "\u5E94\u7528\u6807\u7B7E",
1114
1176
  savedMessage: "\u9879\u76EE\u5DF2\u4FDD\u5B58\u3002",
1115
1177
  removedMessage: "\u9879\u76EE\u5DF2\u5220\u9664\u3002",
1178
+ restoredMessage: "\u9879\u76EE\u5DF2\u6062\u590D\u3002",
1179
+ syncFailedMessage: "\u540C\u6B65\u5931\u8D25\u3002",
1180
+ stalePrunedMessage: "\u4E0D\u53EF\u7528\u9879\u76EE\u5DF2\u5220\u9664\u3002",
1116
1181
  undo: "\u64A4\u9500",
1117
1182
  undoAvailable: "\u9879\u76EE\u5DF2\u5220\u9664\u3002\u8981\u64A4\u9500\u5417\uFF1F",
1118
1183
  selectionScope: "\u9009\u62E9\u8303\u56F4",
@@ -1189,6 +1254,9 @@ var ZH_HANT_LABELS = {
1189
1254
  applyTags: "\u5957\u7528\u6A19\u7C64",
1190
1255
  savedMessage: "\u9805\u76EE\u5DF2\u5132\u5B58\u3002",
1191
1256
  removedMessage: "\u9805\u76EE\u5DF2\u522A\u9664\u3002",
1257
+ restoredMessage: "\u9805\u76EE\u5DF2\u9084\u539F\u3002",
1258
+ syncFailedMessage: "\u540C\u6B65\u5931\u6557\u3002",
1259
+ stalePrunedMessage: "\u7121\u6CD5\u4F7F\u7528\u7684\u9805\u76EE\u5DF2\u522A\u9664\u3002",
1192
1260
  undo: "\u5FA9\u539F",
1193
1261
  undoAvailable: "\u9805\u76EE\u5DF2\u522A\u9664\u3002\u8981\u5FA9\u539F\u55CE\uFF1F",
1194
1262
  selectionScope: "\u9078\u53D6\u7BC4\u570D",
@@ -1291,14 +1359,29 @@ function getKeepLocaleLabels(locale) {
1291
1359
  // src/ui-context.tsx
1292
1360
  import { jsx } from "react/jsx-runtime";
1293
1361
  var DEFAULT_LABELS = KEEP_LOCALE_LABELS.en;
1294
- var KeepUiLabelsContext = createContext({ labels: DEFAULT_LABELS });
1295
- function KeepUiProvider({ labels, locale, labelResolver, children }) {
1362
+ var KeepUiLabelsContext = createContext({
1363
+ labels: DEFAULT_LABELS,
1364
+ emitFeedback: () => void 0
1365
+ });
1366
+ function KeepUiProvider({
1367
+ labels,
1368
+ locale,
1369
+ labelResolver,
1370
+ onFeedback,
1371
+ children
1372
+ }) {
1373
+ const emitFeedback = useCallback2(
1374
+ (event) => {
1375
+ onFeedback?.(event);
1376
+ },
1377
+ [onFeedback]
1378
+ );
1296
1379
  const value = useMemo(() => {
1297
1380
  const resolvedLocale = normalizeKeepLocale(locale);
1298
1381
  const dictionary = getKeepLocaleLabels(resolvedLocale);
1299
1382
  const resolved = { ...DEFAULT_LABELS, ...dictionary, ...labels };
1300
- return { locale: locale ?? resolvedLocale, labels: resolved, customLabels: labels, labelResolver };
1301
- }, [labelResolver, labels, locale]);
1383
+ return { locale: locale ?? resolvedLocale, labels: resolved, customLabels: labels, labelResolver, emitFeedback };
1384
+ }, [emitFeedback, labelResolver, labels, locale]);
1302
1385
  return /* @__PURE__ */ jsx(KeepUiLabelsContext.Provider, { value, children });
1303
1386
  }
1304
1387
  function useKeepUiLabels() {
@@ -1308,6 +1391,10 @@ function useUiLabel(key, override) {
1308
1391
  const context = useKeepUiLabels();
1309
1392
  return override ?? context.customLabels?.[key] ?? context.labelResolver?.(key, { locale: context.locale }) ?? context.labels[key];
1310
1393
  }
1394
+ function useKeepUiFeedback() {
1395
+ const { emitFeedback } = useKeepUiLabels();
1396
+ return useCallback2((event) => emitFeedback(event), [emitFeedback]);
1397
+ }
1311
1398
 
1312
1399
  // src/hooks/useKeepBackup.ts
1313
1400
  function useKeepBackup({ filename, onExport, onImported }) {
@@ -1732,19 +1819,24 @@ function KeepBulkActions({
1732
1819
  import {
1733
1820
  KeepButton as CoreKeepButton
1734
1821
  } from "@keepkit/core/react";
1735
- import { createElement } from "react";
1822
+ import { createElement, useEffect, useRef as useRef2 } from "react";
1736
1823
 
1737
1824
  // src/hooks/useKeepButton.ts
1738
1825
  import { useKeepItem } from "@keepkit/core/react";
1739
1826
  function useKeepButton({ item, labels, icons, children }) {
1740
1827
  return {
1741
1828
  buttonState: useKeepItem(item),
1829
+ emitFeedback: useKeepUiFeedback(),
1742
1830
  customStateLabel: labels?.loading !== void 0 || labels?.error !== void 0 || icons !== void 0 && children === void 0,
1743
1831
  labels: {
1744
1832
  save: useUiLabel("save", typeof labels?.unsaved === "string" ? labels.unsaved : void 0),
1745
1833
  saved: useUiLabel("saved", typeof labels?.saved === "string" ? labels.saved : void 0),
1746
1834
  loading: useUiLabel("loading", typeof labels?.loading === "string" ? labels.loading : void 0),
1747
- error: useUiLabel("error", typeof labels?.error === "string" ? labels.error : void 0)
1835
+ error: useUiLabel("error", typeof labels?.error === "string" ? labels.error : void 0),
1836
+ savedMessage: useUiLabel("savedMessage"),
1837
+ removedMessage: useUiLabel("removedMessage"),
1838
+ restoredMessage: useUiLabel("restoredMessage"),
1839
+ undo: useUiLabel("undo")
1748
1840
  }
1749
1841
  };
1750
1842
  }
@@ -1757,9 +1849,38 @@ function KeepButton({
1757
1849
  iconOnly = false,
1758
1850
  showLabel = true,
1759
1851
  iconClassName,
1852
+ onClick,
1853
+ onToggleError,
1760
1854
  ...props
1761
1855
  }) {
1762
1856
  const view = useKeepButton({ item: props.item, labels, icons, children: props.children });
1857
+ const pendingToggle = useRef2(null);
1858
+ useEffect(() => {
1859
+ const pending = pendingToggle.current;
1860
+ if (!pending || view.buttonState.isMutating || pending.wasSaved === view.buttonState.isSaved) return;
1861
+ pendingToggle.current = null;
1862
+ if (view.buttonState.isSaved && view.buttonState.item) {
1863
+ view.emitFeedback({ type: "item-saved", item: view.buttonState.item, message: view.labels.savedMessage });
1864
+ return;
1865
+ }
1866
+ if (!pending.item) return;
1867
+ const removedItem = pending.item;
1868
+ view.emitFeedback({
1869
+ type: "item-removed",
1870
+ item: removedItem,
1871
+ message: view.labels.removedMessage,
1872
+ undoLabel: view.labels.undo,
1873
+ undo: async () => {
1874
+ await view.buttonState.save();
1875
+ view.emitFeedback({
1876
+ type: "item-restored",
1877
+ item: removedItem,
1878
+ items: [removedItem],
1879
+ message: view.labels.restoredMessage
1880
+ });
1881
+ }
1882
+ });
1883
+ }, [view]);
1763
1884
  const getStateContent = (state) => {
1764
1885
  if (typeof props.children === "function") return props.children(state);
1765
1886
  if (props.children !== void 0) return props.children;
@@ -1773,6 +1894,16 @@ function KeepButton({
1773
1894
  };
1774
1895
  const sharedProps = {
1775
1896
  ...props,
1897
+ onClick: (event) => {
1898
+ onClick?.(event);
1899
+ if (!event.defaultPrevented) {
1900
+ pendingToggle.current = { wasSaved: view.buttonState.isSaved, item: view.buttonState.item };
1901
+ }
1902
+ },
1903
+ onToggleError: (error) => {
1904
+ pendingToggle.current = null;
1905
+ onToggleError?.(error);
1906
+ },
1776
1907
  "data-keepkit": "button",
1777
1908
  "data-keep-action": "toggle-save",
1778
1909
  "data-has-custom-icon": icons ? "true" : void 0,
@@ -1804,7 +1935,7 @@ import {
1804
1935
  decodeKeepListQuery,
1805
1936
  encodeKeepListQuery
1806
1937
  } from "@keepkit/core/core";
1807
- import { useEffect, useRef as useRef2 } from "react";
1938
+ import { useEffect as useEffect2, useRef as useRef3 } from "react";
1808
1939
  function createNextPagesRouterAdapter(router) {
1809
1940
  const getUrl = () => router.asPath ?? (typeof window === "undefined" ? "/" : window.location.href);
1810
1941
  return {
@@ -1825,13 +1956,13 @@ function useKeepUrlSync({
1825
1956
  options = {},
1826
1957
  adapter: providedAdapter
1827
1958
  }) {
1828
- const browserAdapterRef = useRef2(getBrowserAdapter());
1959
+ const browserAdapterRef = useRef3(getBrowserAdapter());
1829
1960
  const adapter = providedAdapter ?? browserAdapterRef.current;
1830
- const onQueryChangeRef = useRef2(onQueryChange);
1961
+ const onQueryChangeRef = useRef3(onQueryChange);
1831
1962
  onQueryChangeRef.current = onQueryChange;
1832
- const skipWriteRef = useRef2(true);
1963
+ const skipWriteRef = useRef3(true);
1833
1964
  const params = options.params;
1834
- useEffect(() => {
1965
+ useEffect2(() => {
1835
1966
  if (!enabled) return;
1836
1967
  const read = () => {
1837
1968
  const url = adapter.getUrl();
@@ -1848,7 +1979,7 @@ function useKeepUrlSync({
1848
1979
  read();
1849
1980
  return adapter.subscribe?.(read);
1850
1981
  }, [adapter, enabled, params]);
1851
- useEffect(() => {
1982
+ useEffect2(() => {
1852
1983
  if (!enabled) return;
1853
1984
  if (skipWriteRef.current) {
1854
1985
  skipWriteRef.current = false;
@@ -1968,7 +2099,10 @@ function useKeepListView(query) {
1968
2099
 
1969
2100
  // src/KeepItemCard.tsx
1970
2101
  import {
1971
- isValidElement as isValidElement2
2102
+ createContext as createContext2,
2103
+ createElement as createElement2,
2104
+ isValidElement as isValidElement2,
2105
+ useContext as useContext2
1972
2106
  } from "react";
1973
2107
 
1974
2108
  // src/hooks/useKeepItemCard.ts
@@ -1986,6 +2120,10 @@ function useKeepItemCard(options) {
1986
2120
  onRemoved
1987
2121
  } = options;
1988
2122
  const itemState = useKeepItem2(item);
2123
+ const emitFeedback = useKeepUiFeedback();
2124
+ const removedMessage = useUiLabel("removedMessage");
2125
+ const restoredMessage = useUiLabel("restoredMessage");
2126
+ const undoLabel = useUiLabel("undo");
1989
2127
  const resolvedTitle = typeof title === "function" ? title(item) : title ?? getTitle?.(item) ?? getMetaTitle(item.meta) ?? item.id;
1990
2128
  const imageProps = getImageProps?.(item, resolvedTitle);
1991
2129
  const href = typeof hrefOption === "function" ? hrefOption(item) : hrefOption;
@@ -1993,12 +2131,32 @@ function useKeepItemCard(options) {
1993
2131
  const isExternalLink = href ? /^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(href) : false;
1994
2132
  const statusLabelKey = item.status && item.status !== "available" ? getStatusLabelKey(item.status) : "statusUnknown";
1995
2133
  const unavailableLabel = useUiLabel(statusLabelKey);
2134
+ const remove = async () => {
2135
+ const wasSaved = itemState.isSaved;
2136
+ try {
2137
+ await itemState.removeWithUndo();
2138
+ onRemoved?.(item);
2139
+ if (!wasSaved) return;
2140
+ emitFeedback({
2141
+ type: "item-removed",
2142
+ item,
2143
+ message: removedMessage,
2144
+ undoLabel,
2145
+ undo: async () => {
2146
+ await itemState.undo();
2147
+ emitFeedback({ type: "item-restored", item, items: [item], message: restoredMessage });
2148
+ }
2149
+ });
2150
+ } catch (cause) {
2151
+ onRemoveError?.(cause);
2152
+ }
2153
+ };
1996
2154
  const state = {
1997
2155
  item,
1998
2156
  isSaved: itemState.isSaved,
1999
2157
  isMutating: itemState.isMutating,
2000
2158
  error: itemState.error,
2001
- remove: itemState.remove,
2159
+ remove,
2002
2160
  status: item.status
2003
2161
  };
2004
2162
  return {
@@ -2012,14 +2170,7 @@ function useKeepItemCard(options) {
2012
2170
  resolvedLinkTarget: linkTargetAttribute ?? (isExternalLink ? "_blank" : void 0),
2013
2171
  resolvedLinkRel: linkRel ?? (isExternalLink ? "noreferrer" : void 0),
2014
2172
  statusLabel: item.status && item.status !== "available" ? unavailableLabel : void 0,
2015
- remove: async () => {
2016
- try {
2017
- await itemState.remove();
2018
- onRemoved?.(item);
2019
- } catch (cause) {
2020
- onRemoveError?.(cause);
2021
- }
2022
- },
2173
+ remove,
2023
2174
  labels: {
2024
2175
  save: useUiLabel("save"),
2025
2176
  savedAt: useUiLabel("saved"),
@@ -2055,6 +2206,10 @@ import { useKeepContext as useKeepContext2 } from "@keepkit/core/react";
2055
2206
  import { useState as useState4 } from "react";
2056
2207
  function useKeepStaleNotice({ item, onRetry, onRemoved }) {
2057
2208
  const context = useKeepContext2();
2209
+ const emitFeedback = useKeepUiFeedback();
2210
+ const removedMessage = useUiLabel("removedMessage");
2211
+ const restoredMessage = useUiLabel("restoredMessage");
2212
+ const undoLabel = useUiLabel("undo");
2058
2213
  const [isRetrying, setIsRetrying] = useState4(false);
2059
2214
  const [error, setError] = useState4(null);
2060
2215
  async function retry() {
@@ -2073,6 +2228,16 @@ function useKeepStaleNotice({ item, onRetry, onRemoved }) {
2073
2228
  try {
2074
2229
  await context.removeItemWithUndo(item.id);
2075
2230
  onRemoved?.(item);
2231
+ emitFeedback({
2232
+ type: "item-removed",
2233
+ item,
2234
+ message: removedMessage,
2235
+ undoLabel,
2236
+ undo: async () => {
2237
+ await context.undoLastRemoval();
2238
+ emitFeedback({ type: "item-restored", item, items: [item], message: restoredMessage });
2239
+ }
2240
+ });
2076
2241
  } catch (cause) {
2077
2242
  setError(cause);
2078
2243
  }
@@ -2093,15 +2258,34 @@ function useKeepStaleNotice({ item, onRetry, onRemoved }) {
2093
2258
  }
2094
2259
  function useKeepPruneStale({ statuses, onPruned }) {
2095
2260
  const context = useKeepContext2();
2096
- const staleIds = context.items.filter((item) => item.status && statuses.includes(item.status)).map((item) => item.id);
2261
+ const emitFeedback = useKeepUiFeedback();
2262
+ const staleItems = context.items.filter((item) => item.status && statuses.includes(item.status));
2263
+ const staleIds = staleItems.map((item) => item.id);
2264
+ const restoredMessage = useUiLabel("restoredMessage");
2265
+ const prunedMessage = useUiLabel("stalePrunedMessage");
2266
+ const undoLabel = useUiLabel("undo");
2097
2267
  return {
2098
2268
  staleIds,
2099
2269
  isMutating: context.isMutating,
2100
2270
  label: useUiLabel("pruneStale"),
2101
2271
  prune: async () => {
2102
2272
  const ids = [...staleIds];
2273
+ const items = [...staleItems];
2103
2274
  await context.removeItemsWithUndo(ids);
2104
2275
  onPruned?.(ids);
2276
+ if (items.length > 0) {
2277
+ emitFeedback({
2278
+ type: "stale-pruned",
2279
+ item: items[0],
2280
+ items,
2281
+ message: prunedMessage,
2282
+ undoLabel,
2283
+ undo: async () => {
2284
+ await context.undoLastRemoval();
2285
+ emitFeedback({ type: "item-restored", item: items[0], items, message: restoredMessage });
2286
+ }
2287
+ });
2288
+ }
2105
2289
  }
2106
2290
  };
2107
2291
  }
@@ -2220,7 +2404,13 @@ function KeepPruneStaleButton({
2220
2404
 
2221
2405
  // src/KeepItemCard.tsx
2222
2406
  import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
2223
- function KeepItemCard({
2407
+ var KeepItemCardContext = createContext2(null);
2408
+ function useKeepItemCardCompound(part) {
2409
+ const context = useContext2(KeepItemCardContext);
2410
+ if (!context) throw new Error(`KeepItemCard.${part} must be rendered inside KeepItemCard.`);
2411
+ return context;
2412
+ }
2413
+ function KeepItemCardRoot({
2224
2414
  item,
2225
2415
  title,
2226
2416
  getTitle,
@@ -2272,21 +2462,26 @@ function KeepItemCard({
2272
2462
  };
2273
2463
  return LinkComponent ? /* @__PURE__ */ jsx9(LinkComponent, { ...linkProps }) : /* @__PURE__ */ jsx9("a", { ...linkProps });
2274
2464
  }
2275
- const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? /* @__PURE__ */ jsxs5(Fragment3, { children: [
2276
- view.imageProps ? renderImage?.({ ...view.imageProps, alt: imageAlt ?? view.imageProps.alt }, item) ?? (ImageComponent ? /* @__PURE__ */ jsx9(ImageComponent, { ...view.imageProps, alt: imageAlt ?? view.imageProps.alt }) : /* @__PURE__ */ jsx9("img", { ...view.imageProps, alt: imageAlt ?? view.imageProps.alt })) : null,
2277
- /* @__PURE__ */ jsx9("h3", { children: linkTarget === "title" ? view.isAvailable ? renderLink(view.resolvedTitle) : view.href ? /* @__PURE__ */ jsx9("span", { "aria-disabled": "true", "data-link-disabled": "true", children: view.resolvedTitle }) : view.resolvedTitle : view.resolvedTitle }),
2278
- showSavedAt ? /* @__PURE__ */ jsxs5("div", { "data-card-meta": true, children: [
2279
- /* @__PURE__ */ jsxs5("span", { children: [
2280
- view.labels.savedAt,
2281
- ":"
2282
- ] }),
2283
- " ",
2284
- /* @__PURE__ */ jsx9("time", { dateTime: new Date(item.savedAt).toISOString(), children: formatSavedAt(item.savedAt) })
2285
- ] }) : null,
2286
- showTags && (item.tags?.length ?? 0) > 0 ? renderTags ? renderTags(item.tags ?? [], item) : /* @__PURE__ */ jsx9("ul", { "aria-label": view.labels.tags, children: item.tags?.map((tag) => /* @__PURE__ */ jsx9("li", { children: tag }, tag)) }) : null,
2287
- view.itemState.error ? /* @__PURE__ */ jsx9("p", { role: "alert", children: getErrorMessage2(view.itemState.error, view.labels.error) }) : null,
2288
- view.statusLabel ? /* @__PURE__ */ jsx9(KeepStaleNotice, { item, onRetry, onRemoved }) : null,
2289
- showSaveButton && !view.statusLabel ? /* @__PURE__ */ jsx9(
2465
+ function renderTitle(content) {
2466
+ if (linkTarget !== "title") return content;
2467
+ if (view.isAvailable) return renderLink(content);
2468
+ return view.href ? /* @__PURE__ */ jsx9("span", { "aria-disabled": "true", "data-link-disabled": "true", children: content }) : content;
2469
+ }
2470
+ const resolvedImageProps = view.imageProps ? { ...view.imageProps, alt: imageAlt ?? view.imageProps.alt } : void 0;
2471
+ const image = resolvedImageProps ? renderImage?.(resolvedImageProps, item) ?? (ImageComponent ? /* @__PURE__ */ jsx9(ImageComponent, { ...resolvedImageProps }) : /* @__PURE__ */ jsx9("img", { ...resolvedImageProps, alt: resolvedImageProps.alt })) : null;
2472
+ const tags = showTags ? item.tags ?? [] : [];
2473
+ const renderedTags = showTags && tags.length > 0 ? renderTags?.(tags, item) ?? null : null;
2474
+ const meta = showSavedAt ? /* @__PURE__ */ jsxs5("div", { "data-card-meta": true, children: [
2475
+ /* @__PURE__ */ jsxs5("span", { children: [
2476
+ view.labels.savedAt,
2477
+ ":"
2478
+ ] }),
2479
+ " ",
2480
+ /* @__PURE__ */ jsx9("time", { dateTime: new Date(item.savedAt).toISOString(), children: formatSavedAt(item.savedAt) })
2481
+ ] }) : null;
2482
+ const error = view.itemState.error ? /* @__PURE__ */ jsx9("p", { role: "alert", children: getErrorMessage2(view.itemState.error, view.labels.error) }) : null;
2483
+ const actions = view.statusLabel ? /* @__PURE__ */ jsx9(KeepStaleNotice, { item, onRetry, onRemoved }) : /* @__PURE__ */ jsxs5(Fragment3, { children: [
2484
+ showSaveButton ? /* @__PURE__ */ jsx9(
2290
2485
  KeepButton,
2291
2486
  {
2292
2487
  item: toKeepButtonItem(item),
@@ -2294,7 +2489,7 @@ function KeepItemCard({
2294
2489
  getAriaLabel: (buttonState) => `${buttonState.isSaved ? view.labels.remove : view.labels.save} ${String(view.resolvedTitle)}`
2295
2490
  }
2296
2491
  ) : null,
2297
- !view.statusLabel ? /* @__PURE__ */ jsx9(
2492
+ /* @__PURE__ */ jsx9(
2298
2493
  "button",
2299
2494
  {
2300
2495
  type: "button",
@@ -2303,10 +2498,28 @@ function KeepItemCard({
2303
2498
  disabled: view.itemState.isMutating,
2304
2499
  children: removeLabel ?? view.labels.remove
2305
2500
  }
2306
- ) : null
2501
+ )
2502
+ ] });
2503
+ const compoundValue = {
2504
+ resolvedTitle: view.resolvedTitle,
2505
+ renderTitle,
2506
+ image,
2507
+ fallbackLabel: String(view.resolvedTitle),
2508
+ tags,
2509
+ tagsLabel: view.labels.tags,
2510
+ renderedTags,
2511
+ meta,
2512
+ error,
2513
+ actions
2514
+ };
2515
+ const defaultBody = /* @__PURE__ */ jsxs5(Fragment3, { children: [
2516
+ /* @__PURE__ */ jsx9(KeepItemCardMedia, {}),
2517
+ /* @__PURE__ */ jsx9(KeepItemCardContent, {}),
2518
+ /* @__PURE__ */ jsx9(KeepItemCardActions, {})
2307
2519
  ] });
2520
+ const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? defaultBody;
2308
2521
  const linkedBody = linkTarget === "card" && view.isAvailable ? renderLink(body) : body;
2309
- return renderRoot(
2522
+ const root = renderRoot(
2310
2523
  asChild,
2311
2524
  isValidElement2(children) ? children : void 0,
2312
2525
  {
@@ -2323,6 +2536,54 @@ function KeepItemCard({
2323
2536
  linkedBody,
2324
2537
  "KeepItemCard"
2325
2538
  );
2539
+ return /* @__PURE__ */ jsx9(KeepItemCardContext.Provider, { value: compoundValue, children: root });
2540
+ }
2541
+ function KeepItemCardMedia({ children, fallback, ...props }) {
2542
+ const context = useKeepItemCardCompound("Media");
2543
+ return /* @__PURE__ */ jsx9("div", { ...props, "data-keep-card-part": "media", children: children ?? context.image ?? /* @__PURE__ */ jsx9("span", { role: "img", "aria-label": context.fallbackLabel, "data-keep-card-fallback": "true", children: fallback ?? context.fallbackLabel.slice(0, 1) }) });
2544
+ }
2545
+ function KeepItemCardContent({ children, ...props }) {
2546
+ const context = useKeepItemCardCompound("Content");
2547
+ return /* @__PURE__ */ jsx9("div", { ...props, "data-keep-card-part": "content", children: children ?? /* @__PURE__ */ jsxs5(Fragment3, { children: [
2548
+ /* @__PURE__ */ jsx9(KeepItemCardTitle, {}),
2549
+ context.meta,
2550
+ /* @__PURE__ */ jsx9(KeepItemCardTags, {}),
2551
+ context.error
2552
+ ] }) });
2553
+ }
2554
+ function KeepItemCardTitle({ as = "h3", children, ...props }) {
2555
+ const context = useKeepItemCardCompound("Title");
2556
+ return createElement2(
2557
+ as,
2558
+ { ...props, "data-keep-card-part": "title" },
2559
+ context.renderTitle(children ?? context.resolvedTitle)
2560
+ );
2561
+ }
2562
+ function KeepItemCardTags({ children, ...props }) {
2563
+ const context = useKeepItemCardCompound("Tags");
2564
+ if (children === void 0 && context.renderedTags) return context.renderedTags;
2565
+ if (children === void 0 && context.tags.length === 0) return null;
2566
+ return /* @__PURE__ */ jsx9("ul", { ...props, "aria-label": props["aria-label"] ?? context.tagsLabel, "data-keep-card-part": "tags", children: children ?? context.tags.map((tag) => /* @__PURE__ */ jsx9("li", { children: tag }, tag)) });
2567
+ }
2568
+ function KeepItemCardActions({ children, ...props }) {
2569
+ const context = useKeepItemCardCompound("Actions");
2570
+ return /* @__PURE__ */ jsx9("div", { ...props, "data-keep-card-part": "actions", children: children ?? context.actions });
2571
+ }
2572
+ var KeepItemCard = Object.assign(KeepItemCardRoot, {
2573
+ Media: KeepItemCardMedia,
2574
+ Content: KeepItemCardContent,
2575
+ Title: KeepItemCardTitle,
2576
+ Tags: KeepItemCardTags,
2577
+ Actions: KeepItemCardActions
2578
+ });
2579
+ function KeepItemCardSkeleton({ layout = "list", ...props }) {
2580
+ return /* @__PURE__ */ jsxs5("article", { ...props, "aria-hidden": "true", "data-keepkit": "card-skeleton", "data-layout": layout, "data-state": "loading", children: [
2581
+ /* @__PURE__ */ jsx9("span", { "data-skeleton-part": "media" }),
2582
+ /* @__PURE__ */ jsx9("span", { "data-skeleton-part": "title" }),
2583
+ /* @__PURE__ */ jsx9("span", { "data-skeleton-part": "meta" }),
2584
+ /* @__PURE__ */ jsx9("span", { "data-skeleton-part": "tag" }),
2585
+ /* @__PURE__ */ jsx9("span", { "data-skeleton-part": "tag" })
2586
+ ] });
2326
2587
  }
2327
2588
  function formatSavedAt(timestamp) {
2328
2589
  return new Date(timestamp).toISOString().slice(0, 10);
@@ -2332,7 +2593,7 @@ function getErrorMessage2(error, fallback) {
2332
2593
  }
2333
2594
 
2334
2595
  // src/KeepList.tsx
2335
- import { jsx as jsx10 } from "react/jsx-runtime";
2596
+ import { Fragment as Fragment4, jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
2336
2597
  function KeepList(props) {
2337
2598
  const { fallback, onBoundaryError, boundaryResetKey, ...listProps } = props;
2338
2599
  const content = /* @__PURE__ */ jsx10(KeepListContent, { ...listProps });
@@ -2344,6 +2605,8 @@ function KeepListContent({
2344
2605
  children,
2345
2606
  renderItem,
2346
2607
  loading,
2608
+ renderLoading,
2609
+ loadingCount = 6,
2347
2610
  empty,
2348
2611
  error: errorContent,
2349
2612
  itemCardProps,
@@ -2357,7 +2620,9 @@ function KeepListContent({
2357
2620
  const body = getListBody(state, {
2358
2621
  children,
2359
2622
  renderItem,
2360
- loading: loading ?? view.labels.loading,
2623
+ loading: renderLoading !== void 0 ? renderLoading : loading,
2624
+ loadingCount,
2625
+ loadingLabel: view.labels.loading,
2361
2626
  empty: empty ?? view.labels.empty,
2362
2627
  error: errorContent ?? view.labels.error,
2363
2628
  itemCardProps,
@@ -2387,7 +2652,17 @@ function getListState(state) {
2387
2652
  }
2388
2653
  function getListBody(state, options) {
2389
2654
  if (state.error && state.items.length === 0) return resolveContent(options.error, state);
2390
- if (state.isLoading && !state.isHydrated) return resolveContent(options.loading, state);
2655
+ if (state.isLoading && !state.isHydrated) {
2656
+ if (options.loading !== void 0) return resolveContent(options.loading, state);
2657
+ const count = Number.isFinite(options.loadingCount) ? Math.max(0, Math.floor(options.loadingCount)) : 6;
2658
+ return /* @__PURE__ */ jsxs6(Fragment4, { children: [
2659
+ /* @__PURE__ */ jsx10("span", { role: "status", "data-keepkit": "loading-label", children: options.loadingLabel }),
2660
+ /* @__PURE__ */ jsx10("ul", { "data-keepkit": "skeleton-list", "data-layout": options.layout, children: Array.from({ length: count }, (_, index) => (
2661
+ // biome-ignore lint/suspicious/noArrayIndexKey: Static loading placeholders never reorder.
2662
+ /* @__PURE__ */ jsx10("li", { children: /* @__PURE__ */ jsx10(KeepItemCardSkeleton, { layout: options.layout }) }, index)
2663
+ )) })
2664
+ ] });
2665
+ }
2391
2666
  if (state.isHydrated && state.items.length === 0) return resolveContent(options.empty, state);
2392
2667
  if (typeof options.children === "function") return options.children(state);
2393
2668
  if (options.children !== void 0 && !isValidElement3(options.children)) return options.children;
@@ -2401,7 +2676,7 @@ import { isValidElement as isValidElement4 } from "react";
2401
2676
 
2402
2677
  // src/hooks/useKeepTagFilter.ts
2403
2678
  import { useKeepList as useKeepList4 } from "@keepkit/core/react";
2404
- import { useCallback, useMemo as useMemo3, useState as useState5 } from "react";
2679
+ import { useCallback as useCallback3, useMemo as useMemo3, useState as useState5 } from "react";
2405
2680
  function useKeepTagFilter(options) {
2406
2681
  const { query, controlledValue, defaultValue, onChange, onValueChange } = options;
2407
2682
  const [uncontrolledValue, setUncontrolledValue] = useState5(defaultValue);
@@ -2410,7 +2685,7 @@ function useKeepTagFilter(options) {
2410
2685
  ...query,
2411
2686
  tags: resolvedValue ? [...query?.tags ?? [], resolvedValue] : query?.tags
2412
2687
  });
2413
- const select = useCallback(
2688
+ const select = useCallback3(
2414
2689
  (tag) => {
2415
2690
  if (controlledValue === void 0) setUncontrolledValue(tag);
2416
2691
  onChange?.(tag);
@@ -2430,7 +2705,7 @@ function useKeepTagFilter(options) {
2430
2705
  }
2431
2706
 
2432
2707
  // src/KeepTagFilter.tsx
2433
- import { jsx as jsx11, jsxs as jsxs6 } from "react/jsx-runtime";
2708
+ import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
2434
2709
  function KeepTagFilter({
2435
2710
  query,
2436
2711
  value: controlledValue,
@@ -2448,7 +2723,7 @@ function KeepTagFilter({
2448
2723
  }) {
2449
2724
  const view = useKeepTagFilter({ query, controlledValue, defaultValue, onChange, onValueChange });
2450
2725
  const contentChildren = asChild && isValidElement4(children) ? void 0 : children;
2451
- const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? /* @__PURE__ */ jsxs6("fieldset", { children: [
2726
+ const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? /* @__PURE__ */ jsxs7("fieldset", { children: [
2452
2727
  /* @__PURE__ */ jsx11("legend", { children: ariaLabel ?? view.labels.aria }),
2453
2728
  /* @__PURE__ */ jsx11(
2454
2729
  "button",
@@ -2460,7 +2735,7 @@ function KeepTagFilter({
2460
2735
  children: allLabel ?? view.labels.all
2461
2736
  }
2462
2737
  ),
2463
- view.state.tags.map((tag) => /* @__PURE__ */ jsxs6(
2738
+ view.state.tags.map((tag) => /* @__PURE__ */ jsxs7(
2464
2739
  "button",
2465
2740
  {
2466
2741
  type: "button",
@@ -2469,7 +2744,7 @@ function KeepTagFilter({
2469
2744
  onClick: () => view.state.select(tag),
2470
2745
  children: [
2471
2746
  renderTag ? renderTag(tag, view.state.tagCounts[tag] ?? 0, view.state.value === tag) : tag,
2472
- /* @__PURE__ */ jsxs6("span", { children: [
2747
+ /* @__PURE__ */ jsxs7("span", { children: [
2473
2748
  " (",
2474
2749
  view.state.tagCounts[tag] ?? 0,
2475
2750
  ")"
@@ -2495,12 +2770,12 @@ function KeepTagFilter({
2495
2770
  }
2496
2771
 
2497
2772
  // src/hooks/useQueryControls.ts
2498
- import { useEffect as useEffect2, useState as useState6 } from "react";
2773
+ import { useEffect as useEffect3, useState as useState6 } from "react";
2499
2774
  function useKeepSearchInput(options) {
2500
2775
  const { controlledValue, defaultValue, debounceMs, onValueChange } = options;
2501
2776
  const [uncontrolledValue, setUncontrolledValue] = useState6(defaultValue);
2502
2777
  const value = controlledValue ?? uncontrolledValue;
2503
- useEffect2(() => {
2778
+ useEffect3(() => {
2504
2779
  if (!onValueChange) return;
2505
2780
  if (debounceMs <= 0) {
2506
2781
  onValueChange(value);
@@ -2567,7 +2842,7 @@ function getVisiblePages(currentPage, pageCount, maxPageButtons) {
2567
2842
  }
2568
2843
 
2569
2844
  // src/query-controls.tsx
2570
- import { Fragment as Fragment4, jsx as jsx12, jsxs as jsxs7 } from "react/jsx-runtime";
2845
+ import { Fragment as Fragment5, jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
2571
2846
  function KeepSearchInput({
2572
2847
  value: controlledValue,
2573
2848
  defaultValue = "",
@@ -2603,7 +2878,7 @@ function KeepSortSelect({
2603
2878
  ...props
2604
2879
  }) {
2605
2880
  const view = useKeepSortSelect({ controlledValue, defaultValue, onValueChange });
2606
- const options = children ?? /* @__PURE__ */ jsxs7(Fragment4, { children: [
2881
+ const options = children ?? /* @__PURE__ */ jsxs8(Fragment5, { children: [
2607
2882
  /* @__PURE__ */ jsx12("option", { value: "updatedAt:desc", children: view.labels.updatedNewest }),
2608
2883
  /* @__PURE__ */ jsx12("option", { value: "updatedAt:asc", children: view.labels.updatedOldest }),
2609
2884
  /* @__PURE__ */ jsx12("option", { value: "savedAt:desc", children: view.labels.savedNewest }),
@@ -2642,7 +2917,7 @@ function KeepPagination({
2642
2917
  };
2643
2918
  if (render)
2644
2919
  return /* @__PURE__ */ jsx12("nav", { ...navProps, children: render({ page: view.currentPage, pageCount: view.pageCount, goToPage: view.goToPage }) });
2645
- return /* @__PURE__ */ jsxs7("nav", { ...navProps, children: [
2920
+ return /* @__PURE__ */ jsxs8("nav", { ...navProps, children: [
2646
2921
  /* @__PURE__ */ jsx12(
2647
2922
  "button",
2648
2923
  {
@@ -2679,7 +2954,7 @@ function KeepPagination({
2679
2954
  }
2680
2955
 
2681
2956
  // src/KeepCollection.tsx
2682
- import { jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
2957
+ import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
2683
2958
  function KeepCollection(props) {
2684
2959
  const { fallback, onBoundaryError, boundaryResetKey, ...collectionProps } = props;
2685
2960
  const content = /* @__PURE__ */ jsx13(KeepCollectionContent, { ...collectionProps });
@@ -2696,13 +2971,15 @@ function KeepCollectionContent({
2696
2971
  renderItem,
2697
2972
  itemCardProps,
2698
2973
  loading,
2974
+ renderLoading,
2975
+ loadingCount,
2699
2976
  empty,
2700
2977
  error,
2701
2978
  className,
2702
2979
  ...rootProps
2703
2980
  }) {
2704
2981
  const view = useKeepCollection({ query, pageSize, urlSync, urlAdapter, features });
2705
- return /* @__PURE__ */ jsxs8(
2982
+ return /* @__PURE__ */ jsxs9(
2706
2983
  "section",
2707
2984
  {
2708
2985
  ...rootProps,
@@ -2713,7 +2990,7 @@ function KeepCollectionContent({
2713
2990
  "data-state": getCollectionState(view.list),
2714
2991
  "data-loading": view.list.isLoading || view.list.isMutating ? "true" : void 0,
2715
2992
  children: [
2716
- /* @__PURE__ */ jsxs8("div", { children: [
2993
+ /* @__PURE__ */ jsxs9("div", { children: [
2717
2994
  view.enabled.search ? /* @__PURE__ */ jsx13(KeepSearchInput, { value: view.searchValue, onValueChange: view.setSearchValue }) : null,
2718
2995
  view.enabled.sort ? /* @__PURE__ */ jsx13(KeepSortSelect, { value: view.sortValue, onValueChange: view.setSortValue }) : null,
2719
2996
  view.enabled.tagFilter ? /* @__PURE__ */ jsx13(KeepTagFilter, { query, value: view.tag, onValueChange: view.setTag }) : null
@@ -2726,6 +3003,8 @@ function KeepCollectionContent({
2726
3003
  itemCardProps,
2727
3004
  layout,
2728
3005
  loading,
3006
+ renderLoading,
3007
+ loadingCount,
2729
3008
  empty,
2730
3009
  error
2731
3010
  }
@@ -2762,16 +3041,16 @@ import { isValidElement as isValidElement5 } from "react";
2762
3041
 
2763
3042
  // src/hooks/useKeepNoteEditor.ts
2764
3043
  import { useKeepItem as useKeepItem3 } from "@keepkit/core/react";
2765
- import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef3, useState as useState7 } from "react";
3044
+ import { useCallback as useCallback4, useEffect as useEffect4, useRef as useRef4, useState as useState7 } from "react";
2766
3045
  function useKeepNoteEditor({ item, debounceMs, onSaved, onSaveError }) {
2767
3046
  const itemState = useKeepItem3(item);
2768
3047
  const { error, isMutating, item: savedItem, updateNote } = itemState;
2769
3048
  const [note, setNote] = useState7(item.note ?? "");
2770
3049
  const baselineNote = savedItem?.note ?? item.note ?? "";
2771
3050
  const isDirty = note !== baselineNote;
2772
- const lastSavedNoteRef = useRef3(void 0);
2773
- useEffect3(() => setNote(baselineNote), [baselineNote]);
2774
- const save = useCallback2(async () => {
3051
+ const lastSavedNoteRef = useRef4(void 0);
3052
+ useEffect4(() => setNote(baselineNote), [baselineNote]);
3053
+ const save = useCallback4(async () => {
2775
3054
  const nextNote = note.trim() || void 0;
2776
3055
  try {
2777
3056
  await updateNote(nextNote);
@@ -2782,7 +3061,7 @@ function useKeepNoteEditor({ item, debounceMs, onSaved, onSaveError }) {
2782
3061
  throw cause;
2783
3062
  }
2784
3063
  }, [note, onSaveError, onSaved, updateNote]);
2785
- useEffect3(() => {
3064
+ useEffect4(() => {
2786
3065
  if (!isDirty || debounceMs <= 0 || lastSavedNoteRef.current === note) return;
2787
3066
  const timer = window.setTimeout(() => void save().catch(() => void 0), debounceMs);
2788
3067
  return () => window.clearTimeout(timer);
@@ -2817,7 +3096,7 @@ function useKeepNoteEditor({ item, debounceMs, onSaved, onSaveError }) {
2817
3096
  }
2818
3097
 
2819
3098
  // src/KeepNoteEditor.tsx
2820
- import { Fragment as Fragment5, jsx as jsx15, jsxs as jsxs9 } from "react/jsx-runtime";
3099
+ import { Fragment as Fragment6, jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
2821
3100
  function KeepNoteEditor({
2822
3101
  item,
2823
3102
  label,
@@ -2835,8 +3114,8 @@ function KeepNoteEditor({
2835
3114
  const view = useKeepNoteEditor({ item, debounceMs, onSaved, onSaveError });
2836
3115
  const { error, isDirty, isSaving, note, setNote } = view.state;
2837
3116
  const contentChildren = asChild && isValidElement5(children) ? void 0 : children;
2838
- const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? /* @__PURE__ */ jsxs9(Fragment5, { children: [
2839
- /* @__PURE__ */ jsxs9("label", { children: [
3117
+ const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? /* @__PURE__ */ jsxs10(Fragment6, { children: [
3118
+ /* @__PURE__ */ jsxs10("label", { children: [
2840
3119
  label ?? view.labels.note,
2841
3120
  /* @__PURE__ */ jsx15(
2842
3121
  "textarea",
@@ -2853,7 +3132,7 @@ function KeepNoteEditor({
2853
3132
  /* @__PURE__ */ jsx15("button", { type: "submit", "data-keep-action": "save-note", disabled: isSaving, "aria-busy": isSaving, children: saveLabel ?? view.labels.save })
2854
3133
  ] });
2855
3134
  if (!asChild) {
2856
- return /* @__PURE__ */ jsxs9(
3135
+ return /* @__PURE__ */ jsxs10(
2857
3136
  "form",
2858
3137
  {
2859
3138
  ...formProps,
@@ -2892,18 +3171,46 @@ function getErrorMessage3(error, fallback) {
2892
3171
  return error instanceof Error ? error.message : fallback;
2893
3172
  }
2894
3173
 
2895
- // src/hooks/useKeepSyncRecoveryDialog.ts
3174
+ // src/hooks/useKeepSyncFeedback.ts
2896
3175
  import { useKeepContext as useKeepContext3 } from "@keepkit/core/react";
2897
- import { useEffect as useEffect4, useState as useState8 } from "react";
3176
+ import { useEffect as useEffect5, useRef as useRef5 } from "react";
3177
+ function useKeepSyncFeedback() {
3178
+ const { syncState } = useKeepContext3();
3179
+ const emitFeedback = useKeepUiFeedback();
3180
+ const completedMessage = useUiLabel("syncSynced");
3181
+ const failedMessage = useUiLabel("syncFailedMessage");
3182
+ const previousStatus = useRef5("idle");
3183
+ useEffect5(() => {
3184
+ const previous = previousStatus.current;
3185
+ previousStatus.current = syncState.status;
3186
+ if (syncState.status === "error" && previous !== "error") {
3187
+ emitFeedback({ type: "sync-failed", error: syncState.error, message: failedMessage });
3188
+ return;
3189
+ }
3190
+ if (syncState.status === "synced" && (previous === "pending" || previous === "syncing" || previous === "conflict" || previous === "error")) {
3191
+ emitFeedback({ type: "sync-completed", message: completedMessage });
3192
+ }
3193
+ }, [completedMessage, emitFeedback, failedMessage, syncState.error, syncState.status]);
3194
+ }
3195
+
3196
+ // src/KeepSyncFeedbackObserver.tsx
3197
+ function KeepSyncFeedbackObserver() {
3198
+ useKeepSyncFeedback();
3199
+ return null;
3200
+ }
3201
+
3202
+ // src/hooks/useKeepSyncRecoveryDialog.ts
3203
+ import { useKeepContext as useKeepContext4 } from "@keepkit/core/react";
3204
+ import { useEffect as useEffect6, useState as useState8 } from "react";
2898
3205
  function useKeepSyncRecoveryDialog(options) {
2899
3206
  const { open, onOpenChange, conflicts, onManualMerge } = options;
2900
- const context = useKeepContext3();
3207
+ const context = useKeepContext4();
2901
3208
  const conflictList = conflicts ?? context.syncState.conflicts ?? [];
2902
3209
  const hasRecovery = conflictList.length > 0 || context.syncState.status === "error" || Boolean(context.error);
2903
3210
  const [dismissed, setDismissed] = useState8(false);
2904
3211
  const [busyId, setBusyId] = useState8();
2905
3212
  const [error, setError] = useState8();
2906
- useEffect4(() => {
3213
+ useEffect6(() => {
2907
3214
  if (hasRecovery) setDismissed(false);
2908
3215
  }, [hasRecovery]);
2909
3216
  return {
@@ -2948,7 +3255,7 @@ function useKeepSyncRecoveryDialog(options) {
2948
3255
  }
2949
3256
 
2950
3257
  // src/KeepSyncRecoveryDialog.tsx
2951
- import { jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
3258
+ import { jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
2952
3259
  function KeepSyncRecoveryDialog({
2953
3260
  open,
2954
3261
  onOpenChange,
@@ -2963,7 +3270,7 @@ function KeepSyncRecoveryDialog({
2963
3270
  }) {
2964
3271
  const view = useKeepSyncRecoveryDialog({ open, onOpenChange, conflicts, onManualMerge });
2965
3272
  if (!view.isOpen) return null;
2966
- return /* @__PURE__ */ jsxs10(
3273
+ return /* @__PURE__ */ jsxs11(
2967
3274
  "section",
2968
3275
  {
2969
3276
  ...props,
@@ -2977,16 +3284,16 @@ function KeepSyncRecoveryDialog({
2977
3284
  "data-state": view.conflictList.length > 0 ? "conflict" : "error",
2978
3285
  "data-loading": view.busyId !== void 0 ? "true" : void 0,
2979
3286
  children: [
2980
- /* @__PURE__ */ jsxs10("header", { children: [
3287
+ /* @__PURE__ */ jsxs11("header", { children: [
2981
3288
  /* @__PURE__ */ jsx16("h2", { id: "keepkit-sync-recovery-title", children: title ?? view.labels.title }),
2982
3289
  /* @__PURE__ */ jsx16("button", { type: "button", "data-keep-action": "close-dialog", onClick: view.close, "aria-label": view.labels.close, children: view.labels.close })
2983
3290
  ] }),
2984
3291
  children,
2985
- view.conflictList.length > 0 ? /* @__PURE__ */ jsxs10("div", { children: [
3292
+ view.conflictList.length > 0 ? /* @__PURE__ */ jsxs11("div", { children: [
2986
3293
  /* @__PURE__ */ jsx16("p", { children: view.labels.conflict }),
2987
- view.conflictList.map((conflict) => /* @__PURE__ */ jsxs10("article", { "data-conflict-id": conflict.id, children: [
3294
+ view.conflictList.map((conflict) => /* @__PURE__ */ jsxs11("article", { "data-conflict-id": conflict.id, children: [
2988
3295
  /* @__PURE__ */ jsx16("h3", { children: getMetaTitle(conflict.operation.item?.meta) ?? conflict.id }),
2989
- /* @__PURE__ */ jsxs10("div", { "data-conflict-preview": true, children: [
3296
+ /* @__PURE__ */ jsxs11("div", { "data-conflict-preview": true, children: [
2990
3297
  /* @__PURE__ */ jsx16(
2991
3298
  ConflictPreview,
2992
3299
  {
@@ -3008,7 +3315,7 @@ function KeepSyncRecoveryDialog({
3008
3315
  }
3009
3316
  )
3010
3317
  ] }),
3011
- /* @__PURE__ */ jsxs10("div", { children: [
3318
+ /* @__PURE__ */ jsxs11("div", { children: [
3012
3319
  /* @__PURE__ */ jsx16(
3013
3320
  "button",
3014
3321
  {
@@ -3042,7 +3349,7 @@ function KeepSyncRecoveryDialog({
3042
3349
  ] })
3043
3350
  ] }, conflict.id))
3044
3351
  ] }) : null,
3045
- view.showBackupRecovery ? /* @__PURE__ */ jsxs10("section", { "data-recovery": "backup", children: [
3352
+ view.showBackupRecovery ? /* @__PURE__ */ jsxs11("section", { "data-recovery": "backup", children: [
3046
3353
  /* @__PURE__ */ jsx16("h3", { children: view.labels.backupRecovery }),
3047
3354
  /* @__PURE__ */ jsx16("p", { children: view.labels.backupRecoveryDescription }),
3048
3355
  backup ?? (showBackupControls ? /* @__PURE__ */ jsx16(KeepBackup, {}) : null)
@@ -3059,14 +3366,14 @@ function ConflictPreview({
3059
3366
  noteLabel,
3060
3367
  side
3061
3368
  }) {
3062
- return /* @__PURE__ */ jsxs10("article", { "data-conflict-version": side, "aria-label": heading, children: [
3369
+ return /* @__PURE__ */ jsxs11("article", { "data-conflict-version": side, "aria-label": heading, children: [
3063
3370
  /* @__PURE__ */ jsx16("h4", { children: heading }),
3064
- /* @__PURE__ */ jsxs10("dl", { children: [
3065
- /* @__PURE__ */ jsxs10("div", { children: [
3371
+ /* @__PURE__ */ jsxs11("dl", { children: [
3372
+ /* @__PURE__ */ jsxs11("div", { children: [
3066
3373
  /* @__PURE__ */ jsx16("dt", { children: updatedAtLabel }),
3067
3374
  /* @__PURE__ */ jsx16("dd", { children: item ? /* @__PURE__ */ jsx16("time", { dateTime: new Date(item.updatedAt).toISOString(), children: formatConflictDate(item.updatedAt) }) : "\u2014" })
3068
3375
  ] }),
3069
- /* @__PURE__ */ jsxs10("div", { children: [
3376
+ /* @__PURE__ */ jsxs11("div", { children: [
3070
3377
  /* @__PURE__ */ jsx16("dt", { children: noteLabel }),
3071
3378
  /* @__PURE__ */ jsx16("dd", { children: item?.note || "\u2014" })
3072
3379
  ] })
@@ -3078,9 +3385,9 @@ function formatConflictDate(timestamp) {
3078
3385
  }
3079
3386
 
3080
3387
  // src/hooks/useKeepSyncStatusBanner.ts
3081
- import { useKeepContext as useKeepContext4 } from "@keepkit/core/react";
3388
+ import { useKeepContext as useKeepContext5 } from "@keepkit/core/react";
3082
3389
  function useKeepSyncStatusBanner({ onRetry, children }) {
3083
- const context = useKeepContext4();
3390
+ const context = useKeepContext5();
3084
3391
  const retryLabel = useUiLabel("retrySync");
3085
3392
  const resolveLabel = useUiLabel("resolveSync");
3086
3393
  const conflictLabel = useUiLabel("syncConflict");
@@ -3112,7 +3419,7 @@ function getErrorMessage4(error) {
3112
3419
  }
3113
3420
 
3114
3421
  // src/KeepSyncStatusBanner.tsx
3115
- import { jsx as jsx17, jsxs as jsxs11 } from "react/jsx-runtime";
3422
+ import { jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
3116
3423
  function KeepSyncStatusBanner({
3117
3424
  onRetry,
3118
3425
  onResolveConflicts,
@@ -3122,7 +3429,7 @@ function KeepSyncStatusBanner({
3122
3429
  }) {
3123
3430
  const view = useKeepSyncStatusBanner({ onRetry, children });
3124
3431
  if (view.status === "idle" && !view.hasConflicts) return null;
3125
- return /* @__PURE__ */ jsxs11(
3432
+ return /* @__PURE__ */ jsxs12(
3126
3433
  "aside",
3127
3434
  {
3128
3435
  ...props,
@@ -3160,13 +3467,13 @@ function KeepSyncStatusBanner({
3160
3467
 
3161
3468
  // src/hooks/useKeepTagEditor.ts
3162
3469
  import { useKeepItem as useKeepItem4 } from "@keepkit/core/react";
3163
- import { useCallback as useCallback3, useEffect as useEffect5, useState as useState9 } from "react";
3470
+ import { useCallback as useCallback5, useEffect as useEffect7, useState as useState9 } from "react";
3164
3471
  function useKeepTagEditor({ item, onSaved, onSaveError }) {
3165
3472
  const itemState = useKeepItem4(item);
3166
3473
  const [tags, setTags] = useState9(item.tags ?? []);
3167
3474
  const [input, setInput] = useState9("");
3168
- useEffect5(() => setTags(itemState.item?.tags ?? item.tags ?? []), [item.tags, itemState.item?.tags]);
3169
- const save = useCallback3(async () => {
3475
+ useEffect7(() => setTags(itemState.item?.tags ?? item.tags ?? []), [item.tags, itemState.item?.tags]);
3476
+ const save = useCallback5(async () => {
3170
3477
  const nextTags = normalizeUiTags(tags);
3171
3478
  try {
3172
3479
  await itemState.updateTags(nextTags);
@@ -3212,7 +3519,7 @@ function useKeepTagEditor({ item, onSaved, onSaveError }) {
3212
3519
  }
3213
3520
 
3214
3521
  // src/KeepTagEditor.tsx
3215
- import { Fragment as Fragment6, jsx as jsx18, jsxs as jsxs12 } from "react/jsx-runtime";
3522
+ import { Fragment as Fragment7, jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
3216
3523
  function KeepTagEditor({
3217
3524
  item,
3218
3525
  availableTags = [],
@@ -3223,8 +3530,8 @@ function KeepTagEditor({
3223
3530
  }) {
3224
3531
  const view = useKeepTagEditor({ item, onSaved, onSaveError });
3225
3532
  const { isSaving, tags } = view.state;
3226
- const body = render ? render(view.state) : /* @__PURE__ */ jsxs12(Fragment6, { children: [
3227
- /* @__PURE__ */ jsxs12("label", { children: [
3533
+ const body = render ? render(view.state) : /* @__PURE__ */ jsxs13(Fragment7, { children: [
3534
+ /* @__PURE__ */ jsxs13("label", { children: [
3228
3535
  view.labels.tags,
3229
3536
  /* @__PURE__ */ jsx18(
3230
3537
  "input",
@@ -3238,13 +3545,13 @@ function KeepTagEditor({
3238
3545
  )
3239
3546
  ] }),
3240
3547
  availableTags.length > 0 ? /* @__PURE__ */ jsx18("datalist", { id: `keep-tags-${item.id}`, children: availableTags.map((tag) => /* @__PURE__ */ jsx18("option", { value: tag }, tag)) }) : null,
3241
- /* @__PURE__ */ jsx18("ul", { "aria-label": view.labels.tags, children: tags.map((tag) => /* @__PURE__ */ jsxs12("li", { children: [
3548
+ /* @__PURE__ */ jsx18("ul", { "aria-label": view.labels.tags, children: tags.map((tag) => /* @__PURE__ */ jsxs13("li", { children: [
3242
3549
  tag,
3243
3550
  /* @__PURE__ */ jsx18("button", { type: "button", "data-keep-action": "remove-tag", onClick: () => view.removeTag(tag), children: view.labels.remove })
3244
3551
  ] }, tag)) }),
3245
3552
  /* @__PURE__ */ jsx18("button", { type: "submit", "data-keep-action": "apply-tags", disabled: isSaving, "aria-busy": isSaving, children: view.labels.apply })
3246
3553
  ] });
3247
- return /* @__PURE__ */ jsxs12(
3554
+ return /* @__PURE__ */ jsxs13(
3248
3555
  "form",
3249
3556
  {
3250
3557
  ...props,
@@ -3266,23 +3573,31 @@ function getErrorMessage5(error, fallback) {
3266
3573
  }
3267
3574
 
3268
3575
  // src/hooks/useKeepUndo.ts
3269
- import { useKeepContext as useKeepContext5 } from "@keepkit/core/react";
3576
+ import { useKeepContext as useKeepContext6 } from "@keepkit/core/react";
3270
3577
  function useKeepUndo() {
3271
- const context = useKeepContext5();
3578
+ const context = useKeepContext6();
3579
+ const emitFeedback = useKeepUiFeedback();
3580
+ const restoredMessage = useUiLabel("restoredMessage");
3272
3581
  return {
3273
3582
  canUndo: context.undo.canUndo,
3274
- undo: () => context.undoLastRemoval(),
3583
+ undo: async () => {
3584
+ const items = context.lastChange?.items ?? (context.lastChange?.item ? [context.lastChange.item] : []);
3585
+ await context.undoLastRemoval();
3586
+ if (items.length > 0) {
3587
+ emitFeedback({ type: "item-restored", item: items[0], items, message: restoredMessage });
3588
+ }
3589
+ },
3275
3590
  message: useUiLabel("undoAvailable"),
3276
3591
  label: useUiLabel("undo")
3277
3592
  };
3278
3593
  }
3279
3594
 
3280
3595
  // src/KeepUndo.tsx
3281
- import { jsx as jsx19, jsxs as jsxs13 } from "react/jsx-runtime";
3596
+ import { jsx as jsx19, jsxs as jsxs14 } from "react/jsx-runtime";
3282
3597
  function KeepUndo({ children, label, ...props }) {
3283
3598
  const view = useKeepUndo();
3284
3599
  if (!view.canUndo) return null;
3285
- return /* @__PURE__ */ jsxs13("div", { ...props, role: "status", "aria-live": "polite", "data-keepkit": "undo", "data-state": "available", children: [
3600
+ return /* @__PURE__ */ jsxs14("div", { ...props, role: "status", "aria-live": "polite", "data-keepkit": "undo", "data-state": "available", children: [
3286
3601
  children ?? view.message,
3287
3602
  /* @__PURE__ */ jsx19("button", { type: "button", "data-keep-action": "undo", onClick: () => void view.undo(), children: label ?? view.label })
3288
3603
  ] });
@@ -3292,13 +3607,13 @@ function KeepUndo({ children, label, ...props }) {
3292
3607
  import { isValidElement as isValidElement6 } from "react";
3293
3608
 
3294
3609
  // src/hooks/useStatusViews.ts
3295
- import { useKeepContext as useKeepContext6 } from "@keepkit/core/react";
3296
- import { useEffect as useEffect6, useRef as useRef4, useState as useState10 } from "react";
3610
+ import { useKeepContext as useKeepContext7 } from "@keepkit/core/react";
3611
+ import { useEffect as useEffect8, useRef as useRef6, useState as useState10 } from "react";
3297
3612
  function useKeepEmptyState() {
3298
3613
  return useUiLabel("noItems").replace(/\.$/, "");
3299
3614
  }
3300
3615
  function useKeepStatus(status) {
3301
- const context = useKeepContext6();
3616
+ const context = useKeepContext7();
3302
3617
  const resolvedStatus = status ?? getDerivedStatus(context);
3303
3618
  const state = {
3304
3619
  status: resolvedStatus,
@@ -3309,13 +3624,13 @@ function useKeepStatus(status) {
3309
3624
  return { state, defaultLabel: useUiLabel(getStatusLabelKey3(resolvedStatus)) };
3310
3625
  }
3311
3626
  function useKeepAnnouncements(messages) {
3312
- const context = useKeepContext6();
3627
+ const context = useKeepContext7();
3313
3628
  const savedMessage = useUiLabel("savedMessage", messages?.save);
3314
3629
  const removedMessage = useUiLabel("removedMessage", messages?.remove);
3315
3630
  const noteSavedMessage = useUiLabel("noteSavedMessage", messages?.note);
3316
3631
  const [message, setMessage] = useState10("");
3317
- const lastChangeRef = useRef4(void 0);
3318
- useEffect6(() => {
3632
+ const lastChangeRef = useRef6(void 0);
3633
+ useEffect8(() => {
3319
3634
  const change = context.lastChange;
3320
3635
  if (!change || change === lastChangeRef.current) return;
3321
3636
  lastChangeRef.current = change;
@@ -3343,7 +3658,7 @@ function getStatusLabelKey3(status) {
3343
3658
  }
3344
3659
 
3345
3660
  // src/status.tsx
3346
- import { Fragment as Fragment7, jsx as jsx20, jsxs as jsxs14 } from "react/jsx-runtime";
3661
+ import { Fragment as Fragment8, jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
3347
3662
  function KeepEmptyState({
3348
3663
  title,
3349
3664
  description,
@@ -3355,7 +3670,7 @@ function KeepEmptyState({
3355
3670
  }) {
3356
3671
  const defaultTitle = useKeepEmptyState();
3357
3672
  const contentChildren = asChild && isValidElement6(children) ? void 0 : children;
3358
- const body = contentChildren ?? /* @__PURE__ */ jsxs14(Fragment7, { children: [
3673
+ const body = contentChildren ?? /* @__PURE__ */ jsxs15(Fragment8, { children: [
3359
3674
  /* @__PURE__ */ jsx20("h2", { children: title ?? defaultTitle }),
3360
3675
  description ? /* @__PURE__ */ jsx20("p", { children: description }) : null,
3361
3676
  action
@@ -3483,7 +3798,7 @@ import { createAuthenticatedSyncKit } from "@keepkit/core/core";
3483
3798
  import {
3484
3799
  KeepErrorBoundary as KeepErrorBoundary3,
3485
3800
  KeepProvider,
3486
- useKeepContext as useKeepContext7,
3801
+ useKeepContext as useKeepContext8,
3487
3802
  useKeepItem as useKeepItem5,
3488
3803
  useKeepList as useKeepList5,
3489
3804
  useKeepShortcut
@@ -3498,11 +3813,12 @@ import {
3498
3813
  LocalStorageSyncQueueAdapter,
3499
3814
  SyncStorageAdapter
3500
3815
  } from "@keepkit/core/storage";
3501
- import { jsx as jsx22, jsxs as jsxs15 } from "react/jsx-runtime";
3816
+ import { jsx as jsx22, jsxs as jsxs16 } from "react/jsx-runtime";
3502
3817
  function KeepKitProvider({
3503
3818
  labels,
3504
3819
  locale,
3505
3820
  labelResolver,
3821
+ onFeedback,
3506
3822
  theme,
3507
3823
  mode,
3508
3824
  density,
@@ -3517,7 +3833,7 @@ function KeepKitProvider({
3517
3833
  children,
3518
3834
  ...providerProps
3519
3835
  }) {
3520
- return /* @__PURE__ */ jsx22(KeepUiProvider, { labels, locale, labelResolver, children: /* @__PURE__ */ jsx22(
3836
+ return /* @__PURE__ */ jsx22(KeepUiProvider, { labels, locale, labelResolver, onFeedback, children: /* @__PURE__ */ jsx22(
3521
3837
  KeepThemeProvider,
3522
3838
  {
3523
3839
  theme,
@@ -3531,7 +3847,8 @@ function KeepKitProvider({
3531
3847
  className: themeClassName,
3532
3848
  style: themeStyle,
3533
3849
  asChild: themeAsChild,
3534
- children: /* @__PURE__ */ jsxs15(CoreKeepProvider, { ...providerProps, children: [
3850
+ children: /* @__PURE__ */ jsxs16(CoreKeepProvider, { ...providerProps, children: [
3851
+ /* @__PURE__ */ jsx22(KeepSyncFeedbackObserver, {}),
3535
3852
  children,
3536
3853
  /* @__PURE__ */ jsx22(KeepAnnouncements, {})
3537
3854
  ] })
@@ -3543,6 +3860,7 @@ function createKeepKit(options = {}) {
3543
3860
  labels,
3544
3861
  locale,
3545
3862
  labelResolver,
3863
+ onFeedback,
3546
3864
  theme,
3547
3865
  mode,
3548
3866
  density,
@@ -3566,6 +3884,7 @@ function createKeepKit(options = {}) {
3566
3884
  labels,
3567
3885
  locale,
3568
3886
  labelResolver,
3887
+ onFeedback,
3569
3888
  theme,
3570
3889
  mode,
3571
3890
  density,
@@ -3611,6 +3930,7 @@ export {
3611
3930
  KeepEmptyState,
3612
3931
  KeepErrorBoundary3 as KeepErrorBoundary,
3613
3932
  KeepItemCard,
3933
+ KeepItemCardSkeleton,
3614
3934
  KeepItemCheckbox,
3615
3935
  KeepItemStatusBadge,
3616
3936
  KeepKitProvider,
@@ -3643,10 +3963,11 @@ export {
3643
3963
  isAllSelected,
3644
3964
  keepThemeNames,
3645
3965
  toggleSelectAll,
3646
- useKeepContext7 as useKeepContext,
3966
+ useKeepContext8 as useKeepContext,
3647
3967
  useKeepItem5 as useKeepItem,
3648
3968
  useKeepList5 as useKeepList,
3649
3969
  useKeepShortcut,
3970
+ useKeepToastFeedback,
3650
3971
  useKeepUiLabels,
3651
3972
  useKeepUrlSync
3652
3973
  };