@keepkit/ui 0.14.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/KeepBackup.tsx
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
+
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,29 +1391,19 @@ 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
- // src/KeepBackup.tsx
1313
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
1314
- function KeepBackup({
1315
- filename = "keepkit-backup.json",
1316
- onExport,
1317
- onImported,
1318
- ...props
1319
- }) {
1399
+ // src/hooks/useKeepBackup.ts
1400
+ function useKeepBackup({ filename, onExport, onImported }) {
1320
1401
  const context = useKeepContext();
1321
- const exportLabel = useUiLabel("exportData");
1322
- const importLabel = useUiLabel("importData");
1323
- const importModeLabel = useUiLabel("importMode");
1324
- const mergeLabel = useUiLabel("merge");
1325
- const replaceLabel = useUiLabel("replace");
1326
- const importedCountLabel = useUiLabel("importedCount");
1327
- const failedCountLabel = useUiLabel("failedCount");
1328
- const quotaErrorLabel = useUiLabel("storageQuotaError");
1329
1402
  const inputRef = useRef(null);
1330
1403
  const [mode, setMode] = useState("merge");
1331
1404
  const [result, setResult] = useState();
1332
1405
  const [error, setError] = useState();
1333
- async function handleExport() {
1406
+ async function exportBackup() {
1334
1407
  setError(void 0);
1335
1408
  try {
1336
1409
  const data = await context.exportBackup();
@@ -1346,7 +1419,7 @@ function KeepBackup({
1346
1419
  setError(cause);
1347
1420
  }
1348
1421
  }
1349
- async function handleImport(event) {
1422
+ async function importBackup(event) {
1350
1423
  const file = event.currentTarget.files?.[0];
1351
1424
  event.currentTarget.value = "";
1352
1425
  if (!file) return;
@@ -1360,43 +1433,93 @@ function KeepBackup({
1360
1433
  setError(cause);
1361
1434
  }
1362
1435
  }
1436
+ return {
1437
+ inputRef,
1438
+ mode,
1439
+ setMode,
1440
+ result,
1441
+ error,
1442
+ isMutating: context.isMutating,
1443
+ exportBackup,
1444
+ importBackup,
1445
+ openFilePicker: () => inputRef.current?.click(),
1446
+ labels: {
1447
+ export: useUiLabel("exportData"),
1448
+ import: useUiLabel("importData"),
1449
+ importMode: useUiLabel("importMode"),
1450
+ merge: useUiLabel("merge"),
1451
+ replace: useUiLabel("replace"),
1452
+ importedCount: useUiLabel("importedCount"),
1453
+ failedCount: useUiLabel("failedCount"),
1454
+ quotaError: useUiLabel("storageQuotaError")
1455
+ }
1456
+ };
1457
+ }
1458
+
1459
+ // src/KeepBackup.tsx
1460
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
1461
+ function KeepBackup({
1462
+ filename = "keepkit-backup.json",
1463
+ onExport,
1464
+ onImported,
1465
+ ...props
1466
+ }) {
1467
+ const view = useKeepBackup({ filename, onExport, onImported });
1363
1468
  return /* @__PURE__ */ jsxs(
1364
1469
  "section",
1365
1470
  {
1366
1471
  ...props,
1367
1472
  "data-keepkit": "backup",
1368
- "data-state": error ? "error" : result ? "complete" : "idle",
1369
- "data-loading": context.isMutating ? "true" : void 0,
1473
+ "data-state": view.error ? "error" : view.result ? "complete" : "idle",
1474
+ "data-loading": view.isMutating ? "true" : void 0,
1370
1475
  children: [
1371
- /* @__PURE__ */ jsx2("button", { type: "button", onClick: () => void handleExport(), disabled: context.isMutating, children: exportLabel }),
1476
+ /* @__PURE__ */ jsx2(
1477
+ "button",
1478
+ {
1479
+ type: "button",
1480
+ "data-keep-action": "export-backup",
1481
+ onClick: () => void view.exportBackup(),
1482
+ disabled: view.isMutating,
1483
+ children: view.labels.export
1484
+ }
1485
+ ),
1372
1486
  /* @__PURE__ */ jsxs("label", { children: [
1373
- importModeLabel,
1374
- /* @__PURE__ */ jsxs("select", { value: mode, onChange: (event) => setMode(event.currentTarget.value), children: [
1375
- /* @__PURE__ */ jsx2("option", { value: "merge", children: mergeLabel }),
1376
- /* @__PURE__ */ jsx2("option", { value: "replace", children: replaceLabel })
1377
- ] })
1487
+ view.labels.importMode,
1488
+ /* @__PURE__ */ jsxs(
1489
+ "select",
1490
+ {
1491
+ "data-keep-action": "select-import-mode",
1492
+ value: view.mode,
1493
+ onChange: (event) => view.setMode(event.currentTarget.value),
1494
+ children: [
1495
+ /* @__PURE__ */ jsx2("option", { value: "merge", children: view.labels.merge }),
1496
+ /* @__PURE__ */ jsx2("option", { value: "replace", children: view.labels.replace })
1497
+ ]
1498
+ }
1499
+ )
1378
1500
  ] }),
1379
- /* @__PURE__ */ jsx2("button", { type: "button", onClick: () => inputRef.current?.click(), disabled: context.isMutating, children: importLabel }),
1501
+ /* @__PURE__ */ jsx2("button", { type: "button", "data-keep-action": "import-backup", onClick: view.openFilePicker, disabled: view.isMutating, children: view.labels.import }),
1380
1502
  /* @__PURE__ */ jsx2(
1381
1503
  "input",
1382
1504
  {
1383
- ref: inputRef,
1505
+ ref: view.inputRef,
1384
1506
  type: "file",
1507
+ "data-keep-action": "select-backup-file",
1385
1508
  accept: "application/json,.json",
1386
- "aria-label": importLabel,
1387
- onChange: (event) => void handleImport(event)
1509
+ "aria-label": view.labels.import,
1510
+ onChange: (event) => void view.importBackup(event)
1388
1511
  }
1389
1512
  ),
1390
- result ? /* @__PURE__ */ jsxs("p", { role: "status", children: [
1391
- result.imported,
1513
+ view.result ? /* @__PURE__ */ jsxs("p", { role: "status", children: [
1514
+ view.result.imported,
1392
1515
  " ",
1393
- importedCountLabel,
1516
+ view.labels.importedCount,
1394
1517
  "; ",
1395
- result.failed,
1518
+ view.result.failed,
1396
1519
  " ",
1397
- failedCountLabel
1520
+ view.labels.failedCount
1398
1521
  ] }) : null,
1399
- error ? /* @__PURE__ */ jsx2("p", { role: "alert", children: isQuotaError(error) ? quotaErrorLabel : getErrorMessage(error) }) : null
1522
+ view.error ? /* @__PURE__ */ jsx2("p", { role: "alert", children: isQuotaError(view.error) ? view.labels.quotaError : getErrorMessage(view.error) }) : null
1400
1523
  ]
1401
1524
  }
1402
1525
  );
@@ -1410,48 +1533,16 @@ function getErrorMessage(error) {
1410
1533
  return error instanceof Error ? error.message : "Something went wrong.";
1411
1534
  }
1412
1535
 
1413
- // src/KeepBulkActions.tsx
1536
+ // src/hooks/useKeepBulkActions.ts
1414
1537
  import { useKeepList } from "@keepkit/core/react";
1415
1538
  import { useState as useState2 } from "react";
1416
1539
 
1417
- // src/KeepItemCheckbox.tsx
1418
- import { jsx as jsx3 } from "react/jsx-runtime";
1419
- function KeepItemCheckbox({
1420
- item,
1421
- checked = false,
1422
- label,
1423
- onCheckedChange,
1424
- "aria-label": ariaLabel,
1425
- ...props
1426
- }) {
1427
- const itemLabel = getItemLabel(item) ?? item.id;
1428
- const accessibleLabel = ariaLabel ?? (typeof label === "string" ? label : itemLabel);
1429
- return /* @__PURE__ */ jsx3(
1430
- "input",
1431
- {
1432
- ...props,
1433
- type: "checkbox",
1434
- checked,
1435
- "data-keepkit": "item-checkbox",
1436
- "data-state": checked ? "checked" : "unchecked",
1437
- "data-disabled": props.disabled ? "true" : void 0,
1438
- "aria-label": accessibleLabel,
1439
- onChange: (event) => onCheckedChange?.(event.currentTarget.checked)
1440
- }
1441
- );
1442
- }
1443
- function getItemLabel(item) {
1444
- if (typeof item.meta !== "object" || item.meta === null || !("title" in item.meta)) return void 0;
1445
- const title = item.meta.title;
1446
- return typeof title === "string" && title.trim() ? title.trim() : void 0;
1447
- }
1448
-
1449
1540
  // src/shared.tsx
1450
1541
  import {
1451
1542
  cloneElement,
1452
1543
  isValidElement
1453
1544
  } from "react";
1454
- import { jsx as jsx4 } from "react/jsx-runtime";
1545
+ import { jsx as jsx3 } from "react/jsx-runtime";
1455
1546
  function toKeepButtonItem(item) {
1456
1547
  return {
1457
1548
  id: item.id,
@@ -1480,11 +1571,10 @@ function renderRoot(asChild, child, props, body, componentName) {
1480
1571
  if (!isValidElement(child)) throw new Error(`${componentName} with asChild requires a single React element child.`);
1481
1572
  return cloneElement(child, { ...props, children: body });
1482
1573
  }
1483
- return /* @__PURE__ */ jsx4("div", { ...props, children: body });
1574
+ return /* @__PURE__ */ jsx3("div", { ...props, children: body });
1484
1575
  }
1485
1576
 
1486
- // src/KeepBulkActions.tsx
1487
- import { Fragment, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
1577
+ // src/hooks/useKeepBulkActions.ts
1488
1578
  function isAllSelected(items, selectedIds) {
1489
1579
  if (items.length === 0) return false;
1490
1580
  const selected = new Set(selectedIds);
@@ -1499,42 +1589,30 @@ function toggleSelectAll(items, selectedIds) {
1499
1589
  }
1500
1590
  return nextIds;
1501
1591
  }
1502
- function KeepBulkActions({
1503
- query,
1504
- selectedIds: controlledSelectedIds,
1505
- defaultSelectedIds = [],
1506
- onSelectedIdsChange,
1507
- renderItem,
1508
- onCompleted,
1509
- selectionScope: controlledScope,
1510
- onSelectionScopeChange,
1511
- render,
1512
- children,
1513
- ...props
1514
- }) {
1515
- const selectItemsLabel = useUiLabel("selectItems");
1516
- const selectedCountLabel = useUiLabel("selectedCount");
1517
- const deleteSelectedLabel = useUiLabel("deleteSelected");
1518
- const tagsLabel = useUiLabel("tagsToApply");
1519
- const applyTagsLabel = useUiLabel("applyTags");
1520
- const selectionScopeLabel = useUiLabel("selectionScope");
1521
- const currentPageLabel = useUiLabel("currentPage");
1522
- const searchResultsLabel = useUiLabel("searchResults");
1523
- const allItemsLabel = useUiLabel("allItems");
1592
+ function useKeepBulkActions(options) {
1593
+ const {
1594
+ query,
1595
+ controlledSelectedIds,
1596
+ defaultSelectedIds,
1597
+ onSelectedIdsChange,
1598
+ onCompleted,
1599
+ controlledScope,
1600
+ onSelectionScopeChange
1601
+ } = options;
1524
1602
  const list = useKeepList(query);
1525
1603
  const queryList = useKeepList(query ? { ...query, pagination: void 0 } : { pagination: void 0 });
1526
1604
  const allList = useKeepList({ pagination: void 0 });
1527
1605
  const [uncontrolledScope, setUncontrolledScope] = useState2(controlledScope ?? "page");
1528
1606
  const selectionScope = controlledScope ?? uncontrolledScope;
1529
- const setSelectionScope = (scope) => {
1530
- if (controlledScope === void 0) setUncontrolledScope(scope);
1531
- onSelectionScopeChange?.(scope);
1532
- };
1533
1607
  const targetItems = selectionScope === "page" ? list.items : selectionScope === "query" ? queryList.items : allList.items;
1534
1608
  const [uncontrolledSelectedIds, setUncontrolledSelectedIds] = useState2(defaultSelectedIds);
1535
1609
  const selectedIds = controlledSelectedIds ?? uncontrolledSelectedIds;
1536
1610
  const selected = new Set(selectedIds);
1537
1611
  const [tagsInput, setTagsInput] = useState2("");
1612
+ const setSelectionScope = (scope) => {
1613
+ if (controlledScope === void 0) setUncontrolledScope(scope);
1614
+ onSelectionScopeChange?.(scope);
1615
+ };
1538
1616
  const setSelectedIds = (ids) => {
1539
1617
  if (controlledSelectedIds === void 0) setUncontrolledSelectedIds(ids);
1540
1618
  onSelectedIdsChange?.(ids);
@@ -1571,54 +1649,156 @@ function KeepBulkActions({
1571
1649
  selectionScope,
1572
1650
  setSelectionScope
1573
1651
  };
1652
+ return {
1653
+ state,
1654
+ selected,
1655
+ isLoading: list.isLoading,
1656
+ labels: {
1657
+ selectItems: useUiLabel("selectItems"),
1658
+ selectedCount: useUiLabel("selectedCount"),
1659
+ deleteSelected: useUiLabel("deleteSelected"),
1660
+ tags: useUiLabel("tagsToApply"),
1661
+ applyTags: useUiLabel("applyTags"),
1662
+ selectionScope: useUiLabel("selectionScope"),
1663
+ currentPage: useUiLabel("currentPage"),
1664
+ searchResults: useUiLabel("searchResults"),
1665
+ allItems: useUiLabel("allItems")
1666
+ }
1667
+ };
1668
+ }
1669
+
1670
+ // src/KeepItemCheckbox.tsx
1671
+ import { jsx as jsx4 } from "react/jsx-runtime";
1672
+ function KeepItemCheckbox({
1673
+ item,
1674
+ checked = false,
1675
+ label,
1676
+ onCheckedChange,
1677
+ "aria-label": ariaLabel,
1678
+ ...props
1679
+ }) {
1680
+ const itemLabel = getItemLabel(item) ?? item.id;
1681
+ const accessibleLabel = ariaLabel ?? (typeof label === "string" ? label : itemLabel);
1682
+ return /* @__PURE__ */ jsx4(
1683
+ "input",
1684
+ {
1685
+ ...props,
1686
+ type: "checkbox",
1687
+ checked,
1688
+ "data-keepkit": "item-checkbox",
1689
+ "data-keep-action": "select-item",
1690
+ "data-state": checked ? "checked" : "unchecked",
1691
+ "data-disabled": props.disabled ? "true" : void 0,
1692
+ "aria-label": accessibleLabel,
1693
+ onChange: (event) => onCheckedChange?.(event.currentTarget.checked)
1694
+ }
1695
+ );
1696
+ }
1697
+ function getItemLabel(item) {
1698
+ if (typeof item.meta !== "object" || item.meta === null || !("title" in item.meta)) return void 0;
1699
+ const title = item.meta.title;
1700
+ return typeof title === "string" && title.trim() ? title.trim() : void 0;
1701
+ }
1702
+
1703
+ // src/KeepBulkActions.tsx
1704
+ import { Fragment, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
1705
+ function KeepBulkActions({
1706
+ query,
1707
+ selectedIds: controlledSelectedIds,
1708
+ defaultSelectedIds = [],
1709
+ onSelectedIdsChange,
1710
+ renderItem,
1711
+ onCompleted,
1712
+ selectionScope: controlledScope,
1713
+ onSelectionScopeChange,
1714
+ render,
1715
+ children,
1716
+ ...props
1717
+ }) {
1718
+ const { state, selected, isLoading, labels } = useKeepBulkActions({
1719
+ query,
1720
+ controlledSelectedIds,
1721
+ defaultSelectedIds,
1722
+ onSelectedIdsChange,
1723
+ onCompleted,
1724
+ controlledScope,
1725
+ onSelectionScopeChange
1726
+ });
1574
1727
  const body = render ? render(state) : typeof children === "function" ? children(state) : children ?? /* @__PURE__ */ jsxs2(Fragment, { children: [
1575
1728
  /* @__PURE__ */ jsxs2("fieldset", { children: [
1576
- /* @__PURE__ */ jsx5("legend", { children: selectItemsLabel }),
1729
+ /* @__PURE__ */ jsx5("legend", { children: labels.selectItems }),
1577
1730
  /* @__PURE__ */ jsxs2("label", { children: [
1578
- selectionScopeLabel,
1731
+ labels.selectionScope,
1579
1732
  /* @__PURE__ */ jsxs2(
1580
1733
  "select",
1581
1734
  {
1582
- value: selectionScope,
1583
- onChange: (event) => setSelectionScope(event.currentTarget.value),
1735
+ "data-keep-action": "select-scope",
1736
+ value: state.selectionScope,
1737
+ onChange: (event) => state.setSelectionScope(event.currentTarget.value),
1584
1738
  children: [
1585
- /* @__PURE__ */ jsx5("option", { value: "page", children: currentPageLabel }),
1586
- /* @__PURE__ */ jsx5("option", { value: "query", children: searchResultsLabel }),
1587
- /* @__PURE__ */ jsx5("option", { value: "all", children: allItemsLabel })
1739
+ /* @__PURE__ */ jsx5("option", { value: "page", children: labels.currentPage }),
1740
+ /* @__PURE__ */ jsx5("option", { value: "query", children: labels.searchResults }),
1741
+ /* @__PURE__ */ jsx5("option", { value: "all", children: labels.allItems })
1588
1742
  ]
1589
1743
  }
1590
1744
  )
1591
1745
  ] }),
1592
1746
  /* @__PURE__ */ jsxs2("label", { children: [
1593
- /* @__PURE__ */ jsx5("input", { type: "checkbox", checked: allSelected, onChange: toggleAll, "aria-label": selectItemsLabel }),
1594
- selectedIds.length,
1747
+ /* @__PURE__ */ jsx5(
1748
+ "input",
1749
+ {
1750
+ type: "checkbox",
1751
+ "data-keep-action": "select-all",
1752
+ checked: state.allSelected,
1753
+ onChange: state.toggleAll,
1754
+ "aria-label": labels.selectItems
1755
+ }
1756
+ ),
1757
+ state.selectedIds.length,
1595
1758
  " ",
1596
- selectedCountLabel
1759
+ labels.selectedCount
1597
1760
  ] }),
1598
- targetItems.map((item) => /* @__PURE__ */ jsxs2("span", { children: [
1761
+ state.items.map((item) => /* @__PURE__ */ jsxs2("span", { children: [
1599
1762
  /* @__PURE__ */ jsx5(
1600
1763
  KeepItemCheckbox,
1601
1764
  {
1602
1765
  item,
1603
1766
  checked: selected.has(item.id),
1604
- onCheckedChange: () => toggle(item.id)
1767
+ onCheckedChange: () => state.toggle(item.id)
1605
1768
  }
1606
1769
  ),
1607
1770
  renderItem ? renderItem(item, selected.has(item.id)) : getMetaTitle(item.meta) ?? item.id
1608
1771
  ] }, item.id))
1609
1772
  ] }),
1610
- /* @__PURE__ */ jsx5("button", { type: "button", onClick: () => void remove(), disabled: selectedIds.length === 0 || list.isMutating, children: deleteSelectedLabel }),
1773
+ /* @__PURE__ */ jsx5(
1774
+ "button",
1775
+ {
1776
+ type: "button",
1777
+ "data-keep-action": "delete-selected",
1778
+ onClick: () => void state.remove(),
1779
+ disabled: state.selectedIds.length === 0 || state.isMutating,
1780
+ children: labels.deleteSelected
1781
+ }
1782
+ ),
1611
1783
  /* @__PURE__ */ jsxs2("label", { children: [
1612
- tagsLabel,
1613
- /* @__PURE__ */ jsx5("input", { value: tagsInput, onChange: (event) => setTagsInput(event.currentTarget.value) })
1784
+ labels.tags,
1785
+ /* @__PURE__ */ jsx5(
1786
+ "input",
1787
+ {
1788
+ "data-keep-action": "edit-tags",
1789
+ value: state.tagsInput,
1790
+ onChange: (event) => state.setTagsInput(event.currentTarget.value)
1791
+ }
1792
+ )
1614
1793
  ] }),
1615
1794
  /* @__PURE__ */ jsx5(
1616
1795
  "button",
1617
1796
  {
1618
1797
  type: "button",
1619
- onClick: () => void updateTags(),
1620
- disabled: selectedIds.length === 0 || list.isMutating,
1621
- children: applyTagsLabel
1798
+ "data-keep-action": "apply-tags",
1799
+ onClick: () => void state.updateTags(),
1800
+ disabled: state.selectedIds.length === 0 || state.isMutating,
1801
+ children: labels.applyTags
1622
1802
  }
1623
1803
  )
1624
1804
  ] });
@@ -1627,9 +1807,9 @@ function KeepBulkActions({
1627
1807
  {
1628
1808
  ...props,
1629
1809
  "data-keepkit": "bulk-actions",
1630
- "aria-busy": list.isMutating || props["aria-busy"],
1631
- "data-state": selectedIds.length > 0 ? "selected" : "idle",
1632
- "data-loading": list.isLoading || list.isMutating ? "true" : void 0,
1810
+ "aria-busy": state.isMutating || props["aria-busy"],
1811
+ "data-state": state.selectedIds.length > 0 ? "selected" : "idle",
1812
+ "data-loading": isLoading || state.isMutating ? "true" : void 0,
1633
1813
  children: body
1634
1814
  }
1635
1815
  );
@@ -1637,10 +1817,31 @@ function KeepBulkActions({
1637
1817
 
1638
1818
  // src/KeepButton.tsx
1639
1819
  import {
1640
- KeepButton as CoreKeepButton,
1641
- useKeepItem
1820
+ KeepButton as CoreKeepButton
1642
1821
  } from "@keepkit/core/react";
1643
- import { createElement } from "react";
1822
+ import { createElement, useEffect, useRef as useRef2 } from "react";
1823
+
1824
+ // src/hooks/useKeepButton.ts
1825
+ import { useKeepItem } from "@keepkit/core/react";
1826
+ function useKeepButton({ item, labels, icons, children }) {
1827
+ return {
1828
+ buttonState: useKeepItem(item),
1829
+ emitFeedback: useKeepUiFeedback(),
1830
+ customStateLabel: labels?.loading !== void 0 || labels?.error !== void 0 || icons !== void 0 && children === void 0,
1831
+ labels: {
1832
+ save: useUiLabel("save", typeof labels?.unsaved === "string" ? labels.unsaved : void 0),
1833
+ saved: useUiLabel("saved", typeof labels?.saved === "string" ? labels.saved : void 0),
1834
+ loading: useUiLabel("loading", typeof labels?.loading === "string" ? labels.loading : 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")
1840
+ }
1841
+ };
1842
+ }
1843
+
1844
+ // src/KeepButton.tsx
1644
1845
  import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
1645
1846
  function KeepButton({
1646
1847
  labels,
@@ -1648,18 +1849,42 @@ function KeepButton({
1648
1849
  iconOnly = false,
1649
1850
  showLabel = true,
1650
1851
  iconClassName,
1852
+ onClick,
1853
+ onToggleError,
1651
1854
  ...props
1652
1855
  }) {
1653
- const saveLabel = useUiLabel("save", typeof labels?.unsaved === "string" ? labels.unsaved : void 0);
1654
- const savedLabel = useUiLabel("saved", typeof labels?.saved === "string" ? labels.saved : void 0);
1655
- const loadingLabel = useUiLabel("loading", typeof labels?.loading === "string" ? labels.loading : void 0);
1656
- const errorLabel = useUiLabel("error", typeof labels?.error === "string" ? labels.error : void 0);
1657
- const buttonState = useKeepItem(props.item);
1658
- const customStateLabel = labels?.loading !== void 0 || labels?.error !== void 0 || icons !== void 0 && props.children === void 0;
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]);
1659
1884
  const getStateContent = (state) => {
1660
1885
  if (typeof props.children === "function") return props.children(state);
1661
1886
  if (props.children !== void 0) return props.children;
1662
- const label = state.error ? labels?.error ?? errorLabel : state.isMutating ? labels?.loading ?? loadingLabel : state.isSaved ? labels?.saved ?? savedLabel : labels?.unsaved ?? saveLabel;
1887
+ const label = state.error ? labels?.error ?? view.labels.error : state.isMutating ? labels?.loading ?? view.labels.loading : state.isSaved ? labels?.saved ?? view.labels.saved : labels?.unsaved ?? view.labels.save;
1663
1888
  if (!icons) return label;
1664
1889
  const icon = state.error ? icons.error : state.isMutating ? icons.loading : state.isSaved ? icons.remove ?? icons.saved : icons.save;
1665
1890
  return /* @__PURE__ */ jsxs3(Fragment2, { children: [
@@ -1669,15 +1894,27 @@ function KeepButton({
1669
1894
  };
1670
1895
  const sharedProps = {
1671
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
+ },
1672
1907
  "data-keepkit": "button",
1908
+ "data-keep-action": "toggle-save",
1909
+ "data-has-custom-icon": icons ? "true" : void 0,
1673
1910
  "data-icon-only": iconOnly ? "true" : void 0,
1674
- "aria-busy": props["aria-busy"] ?? (buttonState.isLoading || buttonState.isMutating),
1675
- savedLabel: labels?.saved ?? props.savedLabel ?? savedLabel,
1676
- unsavedLabel: labels?.unsaved ?? props.unsavedLabel ?? saveLabel,
1911
+ "aria-busy": props["aria-busy"] ?? (view.buttonState.isLoading || view.buttonState.isMutating),
1912
+ savedLabel: labels?.saved ?? props.savedLabel ?? view.labels.saved,
1913
+ unsavedLabel: labels?.unsaved ?? props.unsavedLabel ?? view.labels.save,
1677
1914
  savedAriaLabel: labels?.savedAriaLabel ?? props.savedAriaLabel,
1678
1915
  unsavedAriaLabel: labels?.unsavedAriaLabel ?? props.unsavedAriaLabel
1679
1916
  };
1680
- if (!customStateLabel) return /* @__PURE__ */ jsx6(CoreKeepButton, { ...sharedProps });
1917
+ if (!view.customStateLabel) return /* @__PURE__ */ jsx6(CoreKeepButton, { ...sharedProps });
1681
1918
  return /* @__PURE__ */ jsx6(CoreKeepButton, { ...sharedProps, children: (state) => /* @__PURE__ */ jsx6(Fragment2, { children: getStateContent(state) }) });
1682
1919
  }
1683
1920
  function renderIcon(icon, className) {
@@ -1686,88 +1923,296 @@ function renderIcon(icon, className) {
1686
1923
  }
1687
1924
 
1688
1925
  // src/KeepCollection.tsx
1689
- import { KeepErrorBoundary as KeepErrorBoundary2, useKeepList as useKeepList4 } from "@keepkit/core/react";
1690
- import { useMemo as useMemo3, useState as useState6 } from "react";
1926
+ import { KeepErrorBoundary as KeepErrorBoundary2 } from "@keepkit/core/react";
1691
1927
 
1692
- // src/KeepList.tsx
1693
- import {
1694
- KeepErrorBoundary,
1695
- useKeepList as useKeepList2
1696
- } from "@keepkit/core/react";
1697
- import { isValidElement as isValidElement3 } from "react";
1928
+ // src/hooks/useKeepCollection.ts
1929
+ import { useKeepList as useKeepList2 } from "@keepkit/core/react";
1930
+ import { useMemo as useMemo2, useState as useState3 } from "react";
1698
1931
 
1699
- // src/KeepItemCard.tsx
1700
- import { useKeepItem as useKeepItem2 } from "@keepkit/core/react";
1932
+ // src/url-sync.tsx
1701
1933
  import {
1702
- isValidElement as isValidElement2
1703
- } from "react";
1704
-
1705
- // src/KeepStaleNotice.tsx
1706
- import { useKeepContext as useKeepContext2 } from "@keepkit/core/react";
1707
- import { useState as useState3 } from "react";
1708
-
1709
- // src/KeepItemStatusBadge.tsx
1710
- import { jsx as jsx7 } from "react/jsx-runtime";
1711
- function KeepItemStatusBadge({ status = "available", label, className, ...props }) {
1712
- const resolvedStatus = getDisplayStatus(status);
1713
- const statusLabel = useUiLabel(getStatusLabelKey(status));
1714
- return /* @__PURE__ */ jsx7(
1715
- "span",
1716
- {
1717
- ...props,
1718
- className,
1719
- "data-keepkit": "status-badge",
1720
- "data-status": status,
1721
- "data-item-status": resolvedStatus,
1722
- children: label ?? statusLabel
1934
+ DEFAULT_KEEP_URL_PARAMS,
1935
+ decodeKeepListQuery,
1936
+ encodeKeepListQuery
1937
+ } from "@keepkit/core/core";
1938
+ import { useEffect as useEffect2, useRef as useRef3 } from "react";
1939
+ function createNextPagesRouterAdapter(router) {
1940
+ const getUrl = () => router.asPath ?? (typeof window === "undefined" ? "/" : window.location.href);
1941
+ return {
1942
+ getUrl,
1943
+ subscribe: router.events ? (listener) => {
1944
+ router.events?.on("routeChangeComplete", listener);
1945
+ return () => router.events?.off("routeChangeComplete", listener);
1946
+ } : void 0,
1947
+ navigate: (url, mode) => {
1948
+ void router[mode](url, void 0, { shallow: true });
1723
1949
  }
1724
- );
1725
- }
1726
- function getStatusLabelKey(status) {
1727
- switch (status) {
1728
- case "available":
1729
- return "statusAvailable";
1730
- case "expired":
1731
- return "statusExpired";
1732
- case "removed":
1733
- return "statusRemoved";
1734
- case "deleted":
1735
- return "statusDeleted";
1736
- case "private":
1737
- return "statusPrivate";
1738
- case "unknown":
1739
- return "statusUnknown";
1740
- case "restricted":
1741
- return "statusPrivate";
1742
- }
1743
- }
1744
- function getDisplayStatus(status) {
1745
- if (status === "available") return "available";
1746
- if (status === "expired") return "expired";
1747
- if (status === "removed") return "removed";
1748
- return "restricted";
1950
+ };
1749
1951
  }
1750
-
1751
- // src/KeepStaleNotice.tsx
1752
- import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
1753
- function KeepStaleNotice({
1754
- item,
1755
- onRetry,
1756
- onRemoved,
1757
- retryLabel,
1758
- removeLabel,
1759
- children,
1760
- className,
1761
- ...props
1952
+ function useKeepUrlSync({
1953
+ enabled = true,
1954
+ query,
1955
+ onQueryChange,
1956
+ options = {},
1957
+ adapter: providedAdapter
1958
+ }) {
1959
+ const browserAdapterRef = useRef3(getBrowserAdapter());
1960
+ const adapter = providedAdapter ?? browserAdapterRef.current;
1961
+ const onQueryChangeRef = useRef3(onQueryChange);
1962
+ onQueryChangeRef.current = onQueryChange;
1963
+ const skipWriteRef = useRef3(true);
1964
+ const params = options.params;
1965
+ useEffect2(() => {
1966
+ if (!enabled) return;
1967
+ const read = () => {
1968
+ const url = adapter.getUrl();
1969
+ const decoded = decodeKeepListQuery(url, { params });
1970
+ skipWriteRef.current = true;
1971
+ onQueryChangeRef.current((previousQuery) => ({
1972
+ ...previousQuery,
1973
+ ...decoded.search ? { search: decoded.search } : { search: void 0 },
1974
+ ...decoded.tags ? { tags: decoded.tags } : { tags: void 0 },
1975
+ ...decoded.sort ? { sort: decoded.sort } : {},
1976
+ ...decoded.pagination ? { pagination: { ...previousQuery.pagination, ...decoded.pagination } } : {}
1977
+ }));
1978
+ };
1979
+ read();
1980
+ return adapter.subscribe?.(read);
1981
+ }, [adapter, enabled, params]);
1982
+ useEffect2(() => {
1983
+ if (!enabled) return;
1984
+ if (skipWriteRef.current) {
1985
+ skipWriteRef.current = false;
1986
+ return;
1987
+ }
1988
+ const currentUrl = new URL(adapter.getUrl(), "http://keepkit.invalid");
1989
+ const urlParams = { ...DEFAULT_KEEP_URL_PARAMS, ...params };
1990
+ for (const key of Object.values(urlParams)) currentUrl.searchParams.delete(key);
1991
+ const nextParams = encodeKeepListQuery(query, { params });
1992
+ nextParams.forEach((value, key) => {
1993
+ currentUrl.searchParams.append(key, value);
1994
+ });
1995
+ const nextUrl = `${currentUrl.pathname}${currentUrl.search}${currentUrl.hash}`;
1996
+ adapter.navigate(nextUrl, options.history ?? "push");
1997
+ }, [adapter, enabled, options.history, params, query]);
1998
+ }
1999
+ function getBrowserAdapter() {
2000
+ return {
2001
+ getUrl: () => typeof window === "undefined" ? "/" : window.location.href,
2002
+ subscribe: (listener) => {
2003
+ if (typeof window === "undefined") return () => void 0;
2004
+ window.addEventListener("popstate", listener);
2005
+ return () => window.removeEventListener("popstate", listener);
2006
+ },
2007
+ navigate: (url, mode) => {
2008
+ if (typeof window === "undefined") return;
2009
+ window.history[mode === "push" ? "pushState" : "replaceState"]({}, "", url);
2010
+ }
2011
+ };
2012
+ }
2013
+
2014
+ // src/hooks/useKeepCollection.ts
2015
+ function useKeepCollection({
2016
+ query,
2017
+ pageSize,
2018
+ urlSync,
2019
+ urlAdapter,
2020
+ features
1762
2021
  }) {
2022
+ const enabled = {
2023
+ search: true,
2024
+ sort: true,
2025
+ pagination: true,
2026
+ tagFilter: false,
2027
+ bulkActions: false,
2028
+ ...features
2029
+ };
2030
+ const [searchValue, setSearchValue] = useState3(query.search?.query ?? "");
2031
+ const [sort, setSort] = useState3(query.sort ?? { by: "updatedAt", direction: "desc" });
2032
+ const [tag, setTag] = useState3(query.tags?.[0]);
2033
+ const [page, setPage] = useState3(query.pagination?.page ?? 1);
2034
+ const resolvedPageSize = query.pagination?.pageSize ?? pageSize;
2035
+ const resolvedQuery = useMemo2(
2036
+ () => ({
2037
+ ...query,
2038
+ search: enabled.search ? { ...query.search, query: searchValue } : query.search,
2039
+ sort: enabled.sort ? sort : query.sort,
2040
+ tags: tag ? [.../* @__PURE__ */ new Set([...query.tags ?? [], tag])] : query.tags,
2041
+ pagination: enabled.pagination ? { ...query.pagination, page, pageSize: resolvedPageSize } : query.pagination
2042
+ }),
2043
+ [enabled.pagination, enabled.search, enabled.sort, page, query, resolvedPageSize, searchValue, sort, tag]
2044
+ );
2045
+ useKeepUrlSync({
2046
+ enabled: Boolean(urlSync),
2047
+ query: resolvedQuery,
2048
+ onQueryChange: (nextOrUpdater) => {
2049
+ const next = typeof nextOrUpdater === "function" ? nextOrUpdater(resolvedQuery) : nextOrUpdater;
2050
+ setSearchValue(next.search?.query ?? "");
2051
+ setSort(next.sort ?? { by: "updatedAt", direction: "desc" });
2052
+ setTag(next.tags?.[0]);
2053
+ setPage(next.pagination?.page ?? 1);
2054
+ },
2055
+ options: typeof urlSync === "object" ? urlSync : {},
2056
+ adapter: urlAdapter
2057
+ });
2058
+ const list = useKeepList2(resolvedQuery);
2059
+ return {
2060
+ enabled,
2061
+ searchValue,
2062
+ sortValue: sortToValue(sort),
2063
+ tag,
2064
+ resolvedPageSize,
2065
+ resolvedQuery,
2066
+ list,
2067
+ setSearchValue: (value) => {
2068
+ setSearchValue(value);
2069
+ setPage(1);
2070
+ },
2071
+ setSortValue: (_value, nextSort) => {
2072
+ setSort(nextSort);
2073
+ setPage(1);
2074
+ },
2075
+ setTag: (value) => {
2076
+ setTag(value);
2077
+ setPage(1);
2078
+ },
2079
+ setPage
2080
+ };
2081
+ }
2082
+
2083
+ // src/KeepList.tsx
2084
+ import { KeepErrorBoundary } from "@keepkit/core/react";
2085
+ import { isValidElement as isValidElement3 } from "react";
2086
+
2087
+ // src/hooks/useKeepListView.ts
2088
+ import { useKeepList as useKeepList3 } from "@keepkit/core/react";
2089
+ function useKeepListView(query) {
2090
+ return {
2091
+ state: useKeepList3(query),
2092
+ labels: {
2093
+ loading: useUiLabel("loadingItems"),
2094
+ empty: useUiLabel("noItems"),
2095
+ error: useUiLabel("errorItems")
2096
+ }
2097
+ };
2098
+ }
2099
+
2100
+ // src/KeepItemCard.tsx
2101
+ import {
2102
+ createContext as createContext2,
2103
+ createElement as createElement2,
2104
+ isValidElement as isValidElement2,
2105
+ useContext as useContext2
2106
+ } from "react";
2107
+
2108
+ // src/hooks/useKeepItemCard.ts
2109
+ import { useKeepItem as useKeepItem2 } from "@keepkit/core/react";
2110
+ function useKeepItemCard(options) {
2111
+ const {
2112
+ item,
2113
+ title,
2114
+ getTitle,
2115
+ getImageProps,
2116
+ href: hrefOption,
2117
+ linkTargetAttribute,
2118
+ linkRel,
2119
+ onRemoveError,
2120
+ onRemoved
2121
+ } = options;
2122
+ const itemState = useKeepItem2(item);
2123
+ const emitFeedback = useKeepUiFeedback();
2124
+ const removedMessage = useUiLabel("removedMessage");
2125
+ const restoredMessage = useUiLabel("restoredMessage");
2126
+ const undoLabel = useUiLabel("undo");
2127
+ const resolvedTitle = typeof title === "function" ? title(item) : title ?? getTitle?.(item) ?? getMetaTitle(item.meta) ?? item.id;
2128
+ const imageProps = getImageProps?.(item, resolvedTitle);
2129
+ const href = typeof hrefOption === "function" ? hrefOption(item) : hrefOption;
2130
+ const isAvailable = item.status === void 0 || item.status === "available";
2131
+ const isExternalLink = href ? /^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(href) : false;
2132
+ const statusLabelKey = item.status && item.status !== "available" ? getStatusLabelKey(item.status) : "statusUnknown";
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
+ };
2154
+ const state = {
2155
+ item,
2156
+ isSaved: itemState.isSaved,
2157
+ isMutating: itemState.isMutating,
2158
+ error: itemState.error,
2159
+ remove,
2160
+ status: item.status
2161
+ };
2162
+ return {
2163
+ itemState,
2164
+ state,
2165
+ resolvedTitle,
2166
+ imageProps,
2167
+ href,
2168
+ isAvailable,
2169
+ displayStatus: getDisplayStatus(item.status),
2170
+ resolvedLinkTarget: linkTargetAttribute ?? (isExternalLink ? "_blank" : void 0),
2171
+ resolvedLinkRel: linkRel ?? (isExternalLink ? "noreferrer" : void 0),
2172
+ statusLabel: item.status && item.status !== "available" ? unavailableLabel : void 0,
2173
+ remove,
2174
+ labels: {
2175
+ save: useUiLabel("save"),
2176
+ savedAt: useUiLabel("saved"),
2177
+ error: useUiLabel("error"),
2178
+ remove: useUiLabel("remove"),
2179
+ tags: useUiLabel("tags")
2180
+ }
2181
+ };
2182
+ }
2183
+ function getStatusLabelKey(status) {
2184
+ switch (status) {
2185
+ case "expired":
2186
+ return "statusExpired";
2187
+ case "removed":
2188
+ return "statusRemoved";
2189
+ case "deleted":
2190
+ return "statusDeleted";
2191
+ case "private":
2192
+ return "statusPrivate";
2193
+ default:
2194
+ return "statusUnknown";
2195
+ }
2196
+ }
2197
+ function getDisplayStatus(status) {
2198
+ if (status === void 0 || status === "available") return "available";
2199
+ if (status === "expired") return "expired";
2200
+ if (status === "removed") return "removed";
2201
+ return "restricted";
2202
+ }
2203
+
2204
+ // src/hooks/useKeepStaleNotice.ts
2205
+ import { useKeepContext as useKeepContext2 } from "@keepkit/core/react";
2206
+ import { useState as useState4 } from "react";
2207
+ function useKeepStaleNotice({ item, onRetry, onRemoved }) {
1763
2208
  const context = useKeepContext2();
1764
- const retryText = useUiLabel("retry");
1765
- const removeText = useUiLabel("removeFromList");
1766
- const errorText = useUiLabel("error");
1767
- const [isRetrying, setIsRetrying] = useState3(false);
1768
- const [error, setError] = useState3(null);
1769
- const status = item.status && item.status !== "available" ? item.status : "unknown";
1770
- async function handleRetry() {
2209
+ const emitFeedback = useKeepUiFeedback();
2210
+ const removedMessage = useUiLabel("removedMessage");
2211
+ const restoredMessage = useUiLabel("restoredMessage");
2212
+ const undoLabel = useUiLabel("undo");
2213
+ const [isRetrying, setIsRetrying] = useState4(false);
2214
+ const [error, setError] = useState4(null);
2215
+ async function retry() {
1771
2216
  setError(null);
1772
2217
  setIsRetrying(true);
1773
2218
  try {
@@ -1779,22 +2224,157 @@ function KeepStaleNotice({
1779
2224
  setIsRetrying(false);
1780
2225
  }
1781
2226
  }
1782
- async function handleRemove() {
2227
+ async function remove() {
1783
2228
  try {
1784
2229
  await context.removeItemWithUndo(item.id);
1785
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
+ });
1786
2241
  } catch (cause) {
1787
2242
  setError(cause);
1788
2243
  }
1789
2244
  }
1790
- return /* @__PURE__ */ jsxs4("aside", { ...props, className, "data-keepkit": "stale-notice", "data-state": error ? "error" : "stale", children: [
1791
- /* @__PURE__ */ jsx8(KeepItemStatusBadge, { status }),
2245
+ return {
2246
+ status: item.status && item.status !== "available" ? item.status : "unknown",
2247
+ error,
2248
+ isRetrying,
2249
+ isMutating: context.isMutating,
2250
+ retry,
2251
+ remove,
2252
+ labels: {
2253
+ retry: useUiLabel("retry"),
2254
+ remove: useUiLabel("removeFromList"),
2255
+ error: useUiLabel("error")
2256
+ }
2257
+ };
2258
+ }
2259
+ function useKeepPruneStale({ statuses, onPruned }) {
2260
+ const context = useKeepContext2();
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");
2267
+ return {
2268
+ staleIds,
2269
+ isMutating: context.isMutating,
2270
+ label: useUiLabel("pruneStale"),
2271
+ prune: async () => {
2272
+ const ids = [...staleIds];
2273
+ const items = [...staleItems];
2274
+ await context.removeItemsWithUndo(ids);
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
+ }
2289
+ }
2290
+ };
2291
+ }
2292
+
2293
+ // src/hooks/useKeepItemStatusBadge.ts
2294
+ function useKeepItemStatusBadge(status) {
2295
+ return { resolvedStatus: getDisplayStatus2(status), statusLabel: useUiLabel(getStatusLabelKey2(status)) };
2296
+ }
2297
+ function getStatusLabelKey2(status) {
2298
+ switch (status) {
2299
+ case "available":
2300
+ return "statusAvailable";
2301
+ case "expired":
2302
+ return "statusExpired";
2303
+ case "removed":
2304
+ return "statusRemoved";
2305
+ case "deleted":
2306
+ return "statusDeleted";
2307
+ case "private":
2308
+ return "statusPrivate";
2309
+ case "unknown":
2310
+ return "statusUnknown";
2311
+ case "restricted":
2312
+ return "statusPrivate";
2313
+ }
2314
+ }
2315
+ function getDisplayStatus2(status) {
2316
+ if (status === "available") return "available";
2317
+ if (status === "expired") return "expired";
2318
+ if (status === "removed") return "removed";
2319
+ return "restricted";
2320
+ }
2321
+
2322
+ // src/KeepItemStatusBadge.tsx
2323
+ import { jsx as jsx7 } from "react/jsx-runtime";
2324
+ function KeepItemStatusBadge({ status = "available", label, className, ...props }) {
2325
+ const view = useKeepItemStatusBadge(status);
2326
+ return /* @__PURE__ */ jsx7(
2327
+ "span",
2328
+ {
2329
+ ...props,
2330
+ className,
2331
+ "data-keepkit": "status-badge",
2332
+ "data-status": status,
2333
+ "data-item-status": view.resolvedStatus,
2334
+ children: label ?? view.statusLabel
2335
+ }
2336
+ );
2337
+ }
2338
+
2339
+ // src/KeepStaleNotice.tsx
2340
+ import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
2341
+ function KeepStaleNotice({
2342
+ item,
2343
+ onRetry,
2344
+ onRemoved,
2345
+ retryLabel,
2346
+ removeLabel,
2347
+ children,
2348
+ className,
2349
+ ...props
2350
+ }) {
2351
+ const view = useKeepStaleNotice({ item, onRetry, onRemoved });
2352
+ return /* @__PURE__ */ jsxs4("aside", { ...props, className, "data-keepkit": "stale-notice", "data-state": view.error ? "error" : "stale", children: [
2353
+ /* @__PURE__ */ jsx8(KeepItemStatusBadge, { status: view.status }),
1792
2354
  children ?? (item.statusReason ? /* @__PURE__ */ jsx8("p", { children: item.statusReason }) : null),
1793
2355
  /* @__PURE__ */ jsxs4("div", { children: [
1794
- /* @__PURE__ */ jsx8("button", { type: "button", onClick: () => void handleRetry(), disabled: isRetrying || context.isMutating, children: retryLabel ?? retryText }),
1795
- /* @__PURE__ */ jsx8("button", { type: "button", onClick: () => void handleRemove(), disabled: context.isMutating, children: removeLabel ?? removeText })
2356
+ /* @__PURE__ */ jsx8(
2357
+ "button",
2358
+ {
2359
+ type: "button",
2360
+ "data-keep-action": "retry-item",
2361
+ onClick: () => void view.retry(),
2362
+ disabled: view.isRetrying || view.isMutating,
2363
+ children: retryLabel ?? view.labels.retry
2364
+ }
2365
+ ),
2366
+ /* @__PURE__ */ jsx8(
2367
+ "button",
2368
+ {
2369
+ type: "button",
2370
+ "data-keep-action": "remove-item",
2371
+ onClick: () => void view.remove(),
2372
+ disabled: view.isMutating,
2373
+ children: removeLabel ?? view.labels.remove
2374
+ }
2375
+ )
1796
2376
  ] }),
1797
- error ? /* @__PURE__ */ jsx8("p", { role: "alert", children: error instanceof Error ? error.message : errorText }) : null
2377
+ view.error ? /* @__PURE__ */ jsx8("p", { role: "alert", children: view.error instanceof Error ? view.error.message : view.labels.error }) : null
1798
2378
  ] });
1799
2379
  }
1800
2380
  function KeepPruneStaleButton({
@@ -1805,14 +2385,7 @@ function KeepPruneStaleButton({
1805
2385
  disabled,
1806
2386
  ...props
1807
2387
  }) {
1808
- const context = useKeepContext2();
1809
- const defaultLabel = useUiLabel("pruneStale");
1810
- const staleIds = context.items.filter((item) => item.status && statuses.includes(item.status)).map((item) => item.id);
1811
- async function handlePrune() {
1812
- const ids = [...staleIds];
1813
- await context.removeItemsWithUndo(ids);
1814
- onPruned?.(ids);
1815
- }
2388
+ const view = useKeepPruneStale({ statuses, onPruned });
1816
2389
  return /* @__PURE__ */ jsx8(
1817
2390
  "button",
1818
2391
  {
@@ -1820,17 +2393,24 @@ function KeepPruneStaleButton({
1820
2393
  type: "button",
1821
2394
  className: props.className,
1822
2395
  "data-keepkit": "prune-stale",
1823
- "data-state": staleIds.length > 0 ? "available" : "empty",
1824
- disabled: staleIds.length === 0 || context.isMutating || disabled,
1825
- onClick: () => void handlePrune(),
1826
- children: children ?? label ?? `${defaultLabel} (${staleIds.length})`
2396
+ "data-keep-action": "prune-stale",
2397
+ "data-state": view.staleIds.length > 0 ? "available" : "empty",
2398
+ disabled: view.staleIds.length === 0 || view.isMutating || disabled,
2399
+ onClick: () => void view.prune(),
2400
+ children: children ?? label ?? `${view.label} (${view.staleIds.length})`
1827
2401
  }
1828
2402
  );
1829
2403
  }
1830
2404
 
1831
2405
  // src/KeepItemCard.tsx
1832
2406
  import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
1833
- 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({
1834
2414
  item,
1835
2415
  title,
1836
2416
  getTitle,
@@ -1859,116 +2439,151 @@ function KeepItemCard({
1859
2439
  className,
1860
2440
  ...rootProps
1861
2441
  }) {
1862
- const saveActionLabel = useUiLabel("save");
1863
- const savedAtLabel = useUiLabel("saved");
1864
- const errorLabel = useUiLabel("error");
1865
- const removeActionLabel = useUiLabel("remove");
1866
- const itemState = useKeepItem2(item);
1867
- const contentChildren = asChild && isValidElement2(children) ? void 0 : children;
1868
- const state = {
2442
+ const view = useKeepItemCard({
1869
2443
  item,
1870
- isSaved: itemState.isSaved,
1871
- isMutating: itemState.isMutating,
1872
- error: itemState.error,
1873
- remove: itemState.remove,
1874
- status: item.status
1875
- };
1876
- const resolvedTitle = typeof title === "function" ? title(item) : title ?? getTitle?.(item) ?? getMetaTitle(item.meta) ?? item.id;
1877
- const imageProps = getImageProps?.(item, resolvedTitle);
1878
- const href = typeof hrefOption === "function" ? hrefOption(item) : hrefOption;
1879
- const isAvailable = item.status === void 0 || item.status === "available";
1880
- const displayStatus = getDisplayStatus2(item.status);
1881
- const isExternalLink = href ? isExternalHref(href) : false;
1882
- const resolvedLinkTarget = linkTargetAttribute ?? (isExternalLink ? "_blank" : void 0);
1883
- const resolvedLinkRel = linkRel ?? (isExternalLink ? "noreferrer" : void 0);
1884
- const statusLabelKey = item.status && item.status !== "available" ? getStatusLabelKey2(item.status) : "statusUnknown";
1885
- const unavailableLabel = useUiLabel(statusLabelKey);
1886
- const tagsLabel = useUiLabel("tags");
1887
- const statusLabel = item.status && item.status !== "available" ? unavailableLabel : void 0;
2444
+ title,
2445
+ getTitle,
2446
+ getImageProps,
2447
+ href: hrefOption,
2448
+ linkTargetAttribute,
2449
+ linkRel,
2450
+ onRemoveError,
2451
+ onRemoved
2452
+ });
2453
+ const contentChildren = asChild && isValidElement2(children) ? void 0 : children;
1888
2454
  function renderLink(content) {
1889
- if (!href || !isAvailable) return content;
2455
+ if (!view.href || !view.isAvailable) return content;
1890
2456
  const linkProps = {
1891
- href,
1892
- target: resolvedLinkTarget,
1893
- rel: resolvedLinkRel,
2457
+ href: view.href,
2458
+ target: view.resolvedLinkTarget,
2459
+ rel: view.resolvedLinkRel,
1894
2460
  onClick: (event) => onOpen?.(item, event),
1895
2461
  children: content
1896
2462
  };
1897
2463
  return LinkComponent ? /* @__PURE__ */ jsx9(LinkComponent, { ...linkProps }) : /* @__PURE__ */ jsx9("a", { ...linkProps });
1898
2464
  }
1899
- async function handleRemove() {
1900
- try {
1901
- await itemState.remove();
1902
- onRemoved?.(item);
1903
- } catch (error) {
1904
- onRemoveError?.(error);
1905
- }
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;
1906
2469
  }
1907
- const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? /* @__PURE__ */ jsxs5(Fragment3, { children: [
1908
- imageProps ? renderImage?.({ ...imageProps, alt: imageAlt ?? imageProps.alt }, item) ?? (ImageComponent ? /* @__PURE__ */ jsx9(ImageComponent, { ...imageProps, alt: imageAlt ?? imageProps.alt }) : /* @__PURE__ */ jsx9("img", { ...imageProps, alt: imageAlt ?? imageProps.alt })) : null,
1909
- /* @__PURE__ */ jsx9("h3", { children: linkTarget === "title" ? isAvailable ? renderLink(resolvedTitle) : href ? /* @__PURE__ */ jsx9("span", { "aria-disabled": "true", "data-link-disabled": "true", children: resolvedTitle }) : resolvedTitle : resolvedTitle }),
1910
- showSavedAt ? /* @__PURE__ */ jsxs5("div", { "data-card-meta": true, children: [
1911
- /* @__PURE__ */ jsxs5("span", { children: [
1912
- savedAtLabel,
1913
- ":"
1914
- ] }),
1915
- " ",
1916
- /* @__PURE__ */ jsx9("time", { dateTime: new Date(item.savedAt).toISOString(), children: formatSavedAt(item.savedAt) })
1917
- ] }) : null,
1918
- showTags && (item.tags?.length ?? 0) > 0 ? renderTags ? renderTags(item.tags ?? [], item) : /* @__PURE__ */ jsx9("ul", { "aria-label": tagsLabel, children: item.tags?.map((tag) => /* @__PURE__ */ jsx9("li", { children: tag }, tag)) }) : null,
1919
- itemState.error ? /* @__PURE__ */ jsx9("p", { role: "alert", children: getErrorMessage2(itemState.error, errorLabel) }) : null,
1920
- statusLabel ? /* @__PURE__ */ jsx9(KeepStaleNotice, { item, onRetry, onRemoved }) : null,
1921
- showSaveButton && !statusLabel ? /* @__PURE__ */ jsx9(
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(
1922
2485
  KeepButton,
1923
2486
  {
1924
2487
  item: toKeepButtonItem(item),
1925
2488
  labels: saveButtonLabels,
1926
- getAriaLabel: (buttonState) => `${buttonState.isSaved ? removeActionLabel : saveActionLabel} ${String(resolvedTitle)}`
2489
+ getAriaLabel: (buttonState) => `${buttonState.isSaved ? view.labels.remove : view.labels.save} ${String(view.resolvedTitle)}`
1927
2490
  }
1928
2491
  ) : null,
1929
- !statusLabel ? /* @__PURE__ */ jsx9("button", { type: "button", onClick: () => void handleRemove(), disabled: itemState.isMutating, children: removeLabel ?? removeActionLabel }) : null
2492
+ /* @__PURE__ */ jsx9(
2493
+ "button",
2494
+ {
2495
+ type: "button",
2496
+ "data-keep-action": "remove-item",
2497
+ onClick: () => void view.remove(),
2498
+ disabled: view.itemState.isMutating,
2499
+ children: removeLabel ?? view.labels.remove
2500
+ }
2501
+ )
1930
2502
  ] });
1931
- const linkedBody = linkTarget === "card" && isAvailable ? renderLink(body) : body;
1932
- return renderRoot(
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, {})
2519
+ ] });
2520
+ const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? defaultBody;
2521
+ const linkedBody = linkTarget === "card" && view.isAvailable ? renderLink(body) : body;
2522
+ const root = renderRoot(
1933
2523
  asChild,
1934
2524
  isValidElement2(children) ? children : void 0,
1935
2525
  {
1936
2526
  ...rootProps,
1937
2527
  className,
1938
2528
  "data-keepkit": "card",
1939
- "aria-busy": itemState.isMutating || rootProps["aria-busy"],
1940
- "aria-disabled": rootProps["aria-disabled"] ?? (!isAvailable ? "true" : void 0),
1941
- "data-state": itemState.error ? "error" : itemState.isSaved ? "saved" : "unsaved",
2529
+ "aria-busy": view.itemState.isMutating || rootProps["aria-busy"],
2530
+ "aria-disabled": rootProps["aria-disabled"] ?? (!view.isAvailable ? "true" : void 0),
2531
+ "data-state": view.itemState.error ? "error" : view.itemState.isSaved ? "saved" : "unsaved",
1942
2532
  "data-status": item.status ?? "available",
1943
- "data-item-status": displayStatus,
1944
- "data-loading": itemState.isMutating ? "true" : void 0
2533
+ "data-item-status": view.displayStatus,
2534
+ "data-loading": view.itemState.isMutating ? "true" : void 0
1945
2535
  },
1946
2536
  linkedBody,
1947
2537
  "KeepItemCard"
1948
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
+ );
1949
2561
  }
1950
- function getStatusLabelKey2(status) {
1951
- switch (status) {
1952
- case "expired":
1953
- return "statusExpired";
1954
- case "removed":
1955
- return "statusRemoved";
1956
- case "deleted":
1957
- return "statusDeleted";
1958
- case "private":
1959
- return "statusPrivate";
1960
- default:
1961
- return "statusUnknown";
1962
- }
1963
- }
1964
- function getDisplayStatus2(status) {
1965
- if (status === void 0 || status === "available") return "available";
1966
- if (status === "expired") return "expired";
1967
- if (status === "removed") return "removed";
1968
- return "restricted";
1969
- }
1970
- function isExternalHref(href) {
1971
- return /^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(href);
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
+ ] });
1972
2587
  }
1973
2588
  function formatSavedAt(timestamp) {
1974
2589
  return new Date(timestamp).toISOString().slice(0, 10);
@@ -1978,7 +2593,7 @@ function getErrorMessage2(error, fallback) {
1978
2593
  }
1979
2594
 
1980
2595
  // src/KeepList.tsx
1981
- import { jsx as jsx10 } from "react/jsx-runtime";
2596
+ import { Fragment as Fragment4, jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
1982
2597
  function KeepList(props) {
1983
2598
  const { fallback, onBoundaryError, boundaryResetKey, ...listProps } = props;
1984
2599
  const content = /* @__PURE__ */ jsx10(KeepListContent, { ...listProps });
@@ -1990,6 +2605,8 @@ function KeepListContent({
1990
2605
  children,
1991
2606
  renderItem,
1992
2607
  loading,
2608
+ renderLoading,
2609
+ loadingCount = 6,
1993
2610
  empty,
1994
2611
  error: errorContent,
1995
2612
  itemCardProps,
@@ -1998,16 +2615,16 @@ function KeepListContent({
1998
2615
  className,
1999
2616
  ...rootProps
2000
2617
  }) {
2001
- const defaultLoading = useUiLabel("loadingItems");
2002
- const defaultEmpty = useUiLabel("noItems");
2003
- const defaultError = useUiLabel("errorItems");
2004
- const state = useKeepList2(query);
2618
+ const view = useKeepListView(query);
2619
+ const { state } = view;
2005
2620
  const body = getListBody(state, {
2006
2621
  children,
2007
2622
  renderItem,
2008
- loading: loading ?? defaultLoading,
2009
- empty: empty ?? defaultEmpty,
2010
- error: errorContent ?? defaultError,
2623
+ loading: renderLoading !== void 0 ? renderLoading : loading,
2624
+ loadingCount,
2625
+ loadingLabel: view.labels.loading,
2626
+ empty: empty ?? view.labels.empty,
2627
+ error: errorContent ?? view.labels.error,
2011
2628
  itemCardProps,
2012
2629
  layout
2013
2630
  });
@@ -2035,7 +2652,17 @@ function getListState(state) {
2035
2652
  }
2036
2653
  function getListBody(state, options) {
2037
2654
  if (state.error && state.items.length === 0) return resolveContent(options.error, state);
2038
- 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
+ }
2039
2666
  if (state.isHydrated && state.items.length === 0) return resolveContent(options.empty, state);
2040
2667
  if (typeof options.children === "function") return options.children(state);
2041
2668
  if (options.children !== void 0 && !isValidElement3(options.children)) return options.children;
@@ -2045,9 +2672,40 @@ function getListBody(state, options) {
2045
2672
  }
2046
2673
 
2047
2674
  // src/KeepTagFilter.tsx
2048
- import { useKeepList as useKeepList3 } from "@keepkit/core/react";
2049
- import { isValidElement as isValidElement4, useCallback, useMemo as useMemo2, useState as useState4 } from "react";
2050
- import { jsx as jsx11, jsxs as jsxs6 } from "react/jsx-runtime";
2675
+ import { isValidElement as isValidElement4 } from "react";
2676
+
2677
+ // src/hooks/useKeepTagFilter.ts
2678
+ import { useKeepList as useKeepList4 } from "@keepkit/core/react";
2679
+ import { useCallback as useCallback3, useMemo as useMemo3, useState as useState5 } from "react";
2680
+ function useKeepTagFilter(options) {
2681
+ const { query, controlledValue, defaultValue, onChange, onValueChange } = options;
2682
+ const [uncontrolledValue, setUncontrolledValue] = useState5(defaultValue);
2683
+ const resolvedValue = controlledValue ?? uncontrolledValue;
2684
+ const list = useKeepList4({
2685
+ ...query,
2686
+ tags: resolvedValue ? [...query?.tags ?? [], resolvedValue] : query?.tags
2687
+ });
2688
+ const select = useCallback3(
2689
+ (tag) => {
2690
+ if (controlledValue === void 0) setUncontrolledValue(tag);
2691
+ onChange?.(tag);
2692
+ onValueChange?.(tag);
2693
+ },
2694
+ [controlledValue, onChange, onValueChange]
2695
+ );
2696
+ const state = useMemo3(
2697
+ () => ({ tags: list.tags, tagCounts: list.tagCounts, value: resolvedValue, select }),
2698
+ [list.tagCounts, list.tags, resolvedValue, select]
2699
+ );
2700
+ return {
2701
+ state,
2702
+ isLoading: list.isLoading,
2703
+ labels: { all: useUiLabel("allTags"), aria: useUiLabel("filterTags") }
2704
+ };
2705
+ }
2706
+
2707
+ // src/KeepTagFilter.tsx
2708
+ import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
2051
2709
  function KeepTagFilter({
2052
2710
  query,
2053
2711
  value: controlledValue,
@@ -2063,38 +2721,38 @@ function KeepTagFilter({
2063
2721
  className,
2064
2722
  ...rootProps
2065
2723
  }) {
2066
- const uiAllLabel = useUiLabel("allTags");
2067
- const uiAriaLabel = useUiLabel("filterTags");
2068
- const [uncontrolledValue, setUncontrolledValue] = useState4(defaultValue);
2069
- const resolvedValue = controlledValue ?? uncontrolledValue;
2070
- const list = useKeepList3({
2071
- ...query,
2072
- tags: resolvedValue ? [...query?.tags ?? [], resolvedValue] : query?.tags
2073
- });
2074
- const select = useCallback(
2075
- (tag) => {
2076
- if (controlledValue === void 0) setUncontrolledValue(tag);
2077
- onChange?.(tag);
2078
- onValueChange?.(tag);
2079
- },
2080
- [controlledValue, onChange, onValueChange]
2081
- );
2082
- const state = useMemo2(
2083
- () => ({ tags: list.tags, tagCounts: list.tagCounts, value: resolvedValue, select }),
2084
- [list.tagCounts, list.tags, resolvedValue, select]
2085
- );
2724
+ const view = useKeepTagFilter({ query, controlledValue, defaultValue, onChange, onValueChange });
2086
2725
  const contentChildren = asChild && isValidElement4(children) ? void 0 : children;
2087
- const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? /* @__PURE__ */ jsxs6("fieldset", { children: [
2088
- /* @__PURE__ */ jsx11("legend", { children: ariaLabel ?? uiAriaLabel }),
2089
- /* @__PURE__ */ jsx11("button", { type: "button", "aria-pressed": resolvedValue === void 0, onClick: () => select(), children: allLabel ?? uiAllLabel }),
2090
- list.tags.map((tag) => /* @__PURE__ */ jsxs6("button", { type: "button", "aria-pressed": resolvedValue === tag, onClick: () => select(tag), children: [
2091
- renderTag ? renderTag(tag, list.tagCounts[tag] ?? 0, resolvedValue === tag) : tag,
2092
- /* @__PURE__ */ jsxs6("span", { children: [
2093
- " (",
2094
- list.tagCounts[tag] ?? 0,
2095
- ")"
2096
- ] })
2097
- ] }, tag))
2726
+ const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? /* @__PURE__ */ jsxs7("fieldset", { children: [
2727
+ /* @__PURE__ */ jsx11("legend", { children: ariaLabel ?? view.labels.aria }),
2728
+ /* @__PURE__ */ jsx11(
2729
+ "button",
2730
+ {
2731
+ type: "button",
2732
+ "data-keep-action": "filter-all-tags",
2733
+ "aria-pressed": view.state.value === void 0,
2734
+ onClick: () => view.state.select(),
2735
+ children: allLabel ?? view.labels.all
2736
+ }
2737
+ ),
2738
+ view.state.tags.map((tag) => /* @__PURE__ */ jsxs7(
2739
+ "button",
2740
+ {
2741
+ type: "button",
2742
+ "data-keep-action": "filter-tag",
2743
+ "aria-pressed": view.state.value === tag,
2744
+ onClick: () => view.state.select(tag),
2745
+ children: [
2746
+ renderTag ? renderTag(tag, view.state.tagCounts[tag] ?? 0, view.state.value === tag) : tag,
2747
+ /* @__PURE__ */ jsxs7("span", { children: [
2748
+ " (",
2749
+ view.state.tagCounts[tag] ?? 0,
2750
+ ")"
2751
+ ] })
2752
+ ]
2753
+ },
2754
+ tag
2755
+ ))
2098
2756
  ] });
2099
2757
  return renderRoot(
2100
2758
  asChild,
@@ -2103,17 +2761,88 @@ function KeepTagFilter({
2103
2761
  ...rootProps,
2104
2762
  className,
2105
2763
  "data-keepkit": "tag-filter",
2106
- "data-state": resolvedValue === void 0 ? "all" : "filtered",
2107
- "data-loading": list.isLoading ? "true" : void 0
2764
+ "data-state": view.state.value === void 0 ? "all" : "filtered",
2765
+ "data-loading": view.isLoading ? "true" : void 0
2108
2766
  },
2109
2767
  body,
2110
2768
  "KeepTagFilter"
2111
2769
  );
2112
2770
  }
2113
2771
 
2772
+ // src/hooks/useQueryControls.ts
2773
+ import { useEffect as useEffect3, useState as useState6 } from "react";
2774
+ function useKeepSearchInput(options) {
2775
+ const { controlledValue, defaultValue, debounceMs, onValueChange } = options;
2776
+ const [uncontrolledValue, setUncontrolledValue] = useState6(defaultValue);
2777
+ const value = controlledValue ?? uncontrolledValue;
2778
+ useEffect3(() => {
2779
+ if (!onValueChange) return;
2780
+ if (debounceMs <= 0) {
2781
+ onValueChange(value);
2782
+ return;
2783
+ }
2784
+ const timer = window.setTimeout(() => onValueChange(value), debounceMs);
2785
+ return () => window.clearTimeout(timer);
2786
+ }, [debounceMs, onValueChange, value]);
2787
+ return {
2788
+ value,
2789
+ label: useUiLabel("search"),
2790
+ change: (event) => {
2791
+ if (controlledValue === void 0) setUncontrolledValue(event.currentTarget.value);
2792
+ }
2793
+ };
2794
+ }
2795
+ function useKeepSortSelect(options) {
2796
+ const { controlledValue, defaultValue, onValueChange } = options;
2797
+ const [uncontrolledValue, setUncontrolledValue] = useState6(defaultValue);
2798
+ const value = controlledValue ?? uncontrolledValue;
2799
+ return {
2800
+ value,
2801
+ change: (event) => {
2802
+ const nextValue = event.currentTarget.value;
2803
+ if (controlledValue === void 0) setUncontrolledValue(nextValue);
2804
+ const [by, direction] = nextValue.split(":");
2805
+ onValueChange?.(nextValue, { by, direction });
2806
+ },
2807
+ labels: {
2808
+ sort: useUiLabel("sort"),
2809
+ updatedNewest: useUiLabel("updatedNewest"),
2810
+ updatedOldest: useUiLabel("updatedOldest"),
2811
+ savedNewest: useUiLabel("savedNewest"),
2812
+ savedOldest: useUiLabel("savedOldest")
2813
+ }
2814
+ };
2815
+ }
2816
+ function useKeepPagination(options) {
2817
+ const { totalCount, pageSize, page, maxPageButtons, onPageChange } = options;
2818
+ const pageCount = Math.max(1, Math.ceil(totalCount / Math.max(1, pageSize)));
2819
+ const currentPage = Math.min(Math.max(1, page), pageCount);
2820
+ const goToPage = (nextPage) => {
2821
+ const next = Math.min(Math.max(1, nextPage), pageCount);
2822
+ onPageChange?.(next, (next - 1) * pageSize);
2823
+ };
2824
+ return {
2825
+ pageCount,
2826
+ currentPage,
2827
+ goToPage,
2828
+ visiblePages: getVisiblePages(currentPage, pageCount, Math.max(1, maxPageButtons)),
2829
+ labels: {
2830
+ previous: useUiLabel("previousPage"),
2831
+ next: useUiLabel("nextPage"),
2832
+ page: useUiLabel("page"),
2833
+ pagination: useUiLabel("pagination")
2834
+ }
2835
+ };
2836
+ }
2837
+ function getVisiblePages(currentPage, pageCount, maxPageButtons) {
2838
+ if (pageCount <= maxPageButtons) return Array.from({ length: pageCount }, (_, index) => index + 1);
2839
+ const half = Math.floor(maxPageButtons / 2);
2840
+ const start = Math.min(Math.max(1, currentPage - half), pageCount - maxPageButtons + 1);
2841
+ return Array.from({ length: maxPageButtons }, (_, index) => start + index);
2842
+ }
2843
+
2114
2844
  // src/query-controls.tsx
2115
- import { useEffect, useState as useState5 } from "react";
2116
- 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";
2117
2846
  function KeepSearchInput({
2118
2847
  value: controlledValue,
2119
2848
  defaultValue = "",
@@ -2123,33 +2852,20 @@ function KeepSearchInput({
2123
2852
  placeholder,
2124
2853
  ...props
2125
2854
  }) {
2126
- const label = useUiLabel("search");
2127
- const [uncontrolledValue, setUncontrolledValue] = useState5(defaultValue);
2128
- const value = controlledValue ?? uncontrolledValue;
2129
- useEffect(() => {
2130
- if (!onValueChange) return;
2131
- if (debounceMs <= 0) {
2132
- onValueChange(value);
2133
- return;
2134
- }
2135
- const timer = window.setTimeout(() => onValueChange(value), debounceMs);
2136
- return () => window.clearTimeout(timer);
2137
- }, [debounceMs, onValueChange, value]);
2855
+ const view = useKeepSearchInput({ controlledValue, defaultValue, debounceMs, onValueChange });
2138
2856
  return /* @__PURE__ */ jsx12(
2139
2857
  "input",
2140
2858
  {
2141
2859
  ...props,
2142
2860
  "data-keepkit": "search-input",
2861
+ "data-keep-action": "search",
2143
2862
  type: "search",
2144
- value,
2145
- "data-state": value ? "active" : "idle",
2863
+ value: view.value,
2864
+ "data-state": view.value ? "active" : "idle",
2146
2865
  "data-disabled": props.disabled ? "true" : void 0,
2147
- "aria-label": ariaLabel ?? label,
2148
- placeholder: placeholder ?? label,
2149
- onChange: (event) => {
2150
- const nextValue = event.currentTarget.value;
2151
- if (controlledValue === void 0) setUncontrolledValue(nextValue);
2152
- }
2866
+ "aria-label": ariaLabel ?? view.label,
2867
+ placeholder: placeholder ?? view.label,
2868
+ onChange: view.change
2153
2869
  }
2154
2870
  );
2155
2871
  }
@@ -2161,34 +2877,24 @@ function KeepSortSelect({
2161
2877
  children,
2162
2878
  ...props
2163
2879
  }) {
2164
- const label = useUiLabel("sort");
2165
- const updatedNewestLabel = useUiLabel("updatedNewest");
2166
- const updatedOldestLabel = useUiLabel("updatedOldest");
2167
- const savedNewestLabel = useUiLabel("savedNewest");
2168
- const savedOldestLabel = useUiLabel("savedOldest");
2169
- const [uncontrolledValue, setUncontrolledValue] = useState5(defaultValue);
2170
- const value = controlledValue ?? uncontrolledValue;
2171
- const options = children ?? /* @__PURE__ */ jsxs7(Fragment4, { children: [
2172
- /* @__PURE__ */ jsx12("option", { value: "updatedAt:desc", children: updatedNewestLabel }),
2173
- /* @__PURE__ */ jsx12("option", { value: "updatedAt:asc", children: updatedOldestLabel }),
2174
- /* @__PURE__ */ jsx12("option", { value: "savedAt:desc", children: savedNewestLabel }),
2175
- /* @__PURE__ */ jsx12("option", { value: "savedAt:asc", children: savedOldestLabel })
2880
+ const view = useKeepSortSelect({ controlledValue, defaultValue, onValueChange });
2881
+ const options = children ?? /* @__PURE__ */ jsxs8(Fragment5, { children: [
2882
+ /* @__PURE__ */ jsx12("option", { value: "updatedAt:desc", children: view.labels.updatedNewest }),
2883
+ /* @__PURE__ */ jsx12("option", { value: "updatedAt:asc", children: view.labels.updatedOldest }),
2884
+ /* @__PURE__ */ jsx12("option", { value: "savedAt:desc", children: view.labels.savedNewest }),
2885
+ /* @__PURE__ */ jsx12("option", { value: "savedAt:asc", children: view.labels.savedOldest })
2176
2886
  ] });
2177
2887
  return /* @__PURE__ */ jsx12(
2178
2888
  "select",
2179
2889
  {
2180
2890
  ...props,
2181
2891
  "data-keepkit": "sort-select",
2182
- value,
2892
+ "data-keep-action": "sort",
2893
+ value: view.value,
2183
2894
  "data-state": "selected",
2184
2895
  "data-disabled": props.disabled ? "true" : void 0,
2185
- "aria-label": ariaLabel ?? label,
2186
- onChange: (event) => {
2187
- const nextValue = event.currentTarget.value;
2188
- if (controlledValue === void 0) setUncontrolledValue(nextValue);
2189
- const [by, direction] = nextValue.split(":");
2190
- onValueChange?.(nextValue, { by, direction });
2191
- },
2896
+ "aria-label": ariaLabel ?? view.labels.sort,
2897
+ onChange: view.change,
2192
2898
  children: options
2193
2899
  }
2194
2900
  );
@@ -2202,131 +2908,53 @@ function KeepPagination({
2202
2908
  render,
2203
2909
  ...props
2204
2910
  }) {
2205
- const previousPageLabel = useUiLabel("previousPage");
2206
- const nextPageLabel = useUiLabel("nextPage");
2207
- const pageLabel = useUiLabel("page");
2208
- const paginationLabel = useUiLabel("pagination");
2209
- const pageCount = Math.max(1, Math.ceil(totalCount / Math.max(1, pageSize)));
2210
- const currentPage = Math.min(Math.max(1, page), pageCount);
2211
- const goToPage = (nextPage) => {
2212
- const next = Math.min(Math.max(1, nextPage), pageCount);
2213
- onPageChange?.(next, (next - 1) * pageSize);
2214
- };
2911
+ const view = useKeepPagination({ totalCount, pageSize, page, maxPageButtons, onPageChange });
2215
2912
  const navProps = {
2216
2913
  ...props,
2217
2914
  "data-keepkit": "pagination",
2218
- "aria-label": props["aria-label"] ?? paginationLabel,
2219
- "data-state": pageCount > 1 ? "active" : "idle"
2915
+ "aria-label": props["aria-label"] ?? view.labels.pagination,
2916
+ "data-state": view.pageCount > 1 ? "active" : "idle"
2220
2917
  };
2221
- if (render) return /* @__PURE__ */ jsx12("nav", { ...navProps, children: render({ page: currentPage, pageCount, goToPage }) });
2222
- const visiblePages = getVisiblePages(currentPage, pageCount, Math.max(1, maxPageButtons));
2223
- return /* @__PURE__ */ jsxs7("nav", { ...navProps, children: [
2224
- /* @__PURE__ */ jsx12("button", { type: "button", onClick: () => goToPage(currentPage - 1), disabled: currentPage <= 1, children: previousPageLabel }),
2225
- visiblePages.map((nextPage) => /* @__PURE__ */ jsx12(
2918
+ if (render)
2919
+ return /* @__PURE__ */ jsx12("nav", { ...navProps, children: render({ page: view.currentPage, pageCount: view.pageCount, goToPage: view.goToPage }) });
2920
+ return /* @__PURE__ */ jsxs8("nav", { ...navProps, children: [
2921
+ /* @__PURE__ */ jsx12(
2922
+ "button",
2923
+ {
2924
+ type: "button",
2925
+ "data-keep-action": "previous-page",
2926
+ onClick: () => view.goToPage(view.currentPage - 1),
2927
+ disabled: view.currentPage <= 1,
2928
+ children: view.labels.previous
2929
+ }
2930
+ ),
2931
+ view.visiblePages.map((nextPage) => /* @__PURE__ */ jsx12(
2226
2932
  "button",
2227
2933
  {
2228
2934
  type: "button",
2229
- "aria-current": nextPage === currentPage ? "page" : void 0,
2230
- "aria-label": `${pageLabel} ${nextPage}`,
2231
- onClick: () => goToPage(nextPage),
2935
+ "data-keep-action": "select-page",
2936
+ "aria-current": nextPage === view.currentPage ? "page" : void 0,
2937
+ "aria-label": `${view.labels.page} ${nextPage}`,
2938
+ onClick: () => view.goToPage(nextPage),
2232
2939
  children: nextPage
2233
2940
  },
2234
2941
  nextPage
2235
2942
  )),
2236
- /* @__PURE__ */ jsx12("button", { type: "button", onClick: () => goToPage(currentPage + 1), disabled: currentPage >= pageCount, children: nextPageLabel })
2943
+ /* @__PURE__ */ jsx12(
2944
+ "button",
2945
+ {
2946
+ type: "button",
2947
+ "data-keep-action": "next-page",
2948
+ onClick: () => view.goToPage(view.currentPage + 1),
2949
+ disabled: view.currentPage >= view.pageCount,
2950
+ children: view.labels.next
2951
+ }
2952
+ )
2237
2953
  ] });
2238
2954
  }
2239
- function getVisiblePages(currentPage, pageCount, maxPageButtons) {
2240
- if (pageCount <= maxPageButtons) return Array.from({ length: pageCount }, (_, index) => index + 1);
2241
- const half = Math.floor(maxPageButtons / 2);
2242
- const start = Math.min(Math.max(1, currentPage - half), pageCount - maxPageButtons + 1);
2243
- return Array.from({ length: maxPageButtons }, (_, index) => start + index);
2244
- }
2245
-
2246
- // src/url-sync.tsx
2247
- import {
2248
- DEFAULT_KEEP_URL_PARAMS,
2249
- decodeKeepListQuery,
2250
- encodeKeepListQuery
2251
- } from "@keepkit/core/core";
2252
- import { useEffect as useEffect2, useRef as useRef2 } from "react";
2253
- function createNextPagesRouterAdapter(router) {
2254
- const getUrl = () => router.asPath ?? (typeof window === "undefined" ? "/" : window.location.href);
2255
- return {
2256
- getUrl,
2257
- subscribe: router.events ? (listener) => {
2258
- router.events?.on("routeChangeComplete", listener);
2259
- return () => router.events?.off("routeChangeComplete", listener);
2260
- } : void 0,
2261
- navigate: (url, mode) => {
2262
- void router[mode](url, void 0, { shallow: true });
2263
- }
2264
- };
2265
- }
2266
- function useKeepUrlSync({
2267
- enabled = true,
2268
- query,
2269
- onQueryChange,
2270
- options = {},
2271
- adapter: providedAdapter
2272
- }) {
2273
- const browserAdapterRef = useRef2(getBrowserAdapter());
2274
- const adapter = providedAdapter ?? browserAdapterRef.current;
2275
- const onQueryChangeRef = useRef2(onQueryChange);
2276
- onQueryChangeRef.current = onQueryChange;
2277
- const skipWriteRef = useRef2(true);
2278
- const params = options.params;
2279
- useEffect2(() => {
2280
- if (!enabled) return;
2281
- const read = () => {
2282
- const url = adapter.getUrl();
2283
- const decoded = decodeKeepListQuery(url, { params });
2284
- skipWriteRef.current = true;
2285
- onQueryChangeRef.current((previousQuery) => ({
2286
- ...previousQuery,
2287
- ...decoded.search ? { search: decoded.search } : { search: void 0 },
2288
- ...decoded.tags ? { tags: decoded.tags } : { tags: void 0 },
2289
- ...decoded.sort ? { sort: decoded.sort } : {},
2290
- ...decoded.pagination ? { pagination: { ...previousQuery.pagination, ...decoded.pagination } } : {}
2291
- }));
2292
- };
2293
- read();
2294
- return adapter.subscribe?.(read);
2295
- }, [adapter, enabled, params]);
2296
- useEffect2(() => {
2297
- if (!enabled) return;
2298
- if (skipWriteRef.current) {
2299
- skipWriteRef.current = false;
2300
- return;
2301
- }
2302
- const currentUrl = new URL(adapter.getUrl(), "http://keepkit.invalid");
2303
- const urlParams = { ...DEFAULT_KEEP_URL_PARAMS, ...params };
2304
- for (const key of Object.values(urlParams)) currentUrl.searchParams.delete(key);
2305
- const nextParams = encodeKeepListQuery(query, { params });
2306
- nextParams.forEach((value, key) => {
2307
- currentUrl.searchParams.append(key, value);
2308
- });
2309
- const nextUrl = `${currentUrl.pathname}${currentUrl.search}${currentUrl.hash}`;
2310
- adapter.navigate(nextUrl, options.history ?? "push");
2311
- }, [adapter, enabled, options.history, params, query]);
2312
- }
2313
- function getBrowserAdapter() {
2314
- return {
2315
- getUrl: () => typeof window === "undefined" ? "/" : window.location.href,
2316
- subscribe: (listener) => {
2317
- if (typeof window === "undefined") return () => void 0;
2318
- window.addEventListener("popstate", listener);
2319
- return () => window.removeEventListener("popstate", listener);
2320
- },
2321
- navigate: (url, mode) => {
2322
- if (typeof window === "undefined") return;
2323
- window.history[mode === "push" ? "pushState" : "replaceState"]({}, "", url);
2324
- }
2325
- };
2326
- }
2327
2955
 
2328
2956
  // src/KeepCollection.tsx
2329
- import { jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
2957
+ import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
2330
2958
  function KeepCollection(props) {
2331
2959
  const { fallback, onBoundaryError, boundaryResetKey, ...collectionProps } = props;
2332
2960
  const content = /* @__PURE__ */ jsx13(KeepCollectionContent, { ...collectionProps });
@@ -2343,114 +2971,54 @@ function KeepCollectionContent({
2343
2971
  renderItem,
2344
2972
  itemCardProps,
2345
2973
  loading,
2974
+ renderLoading,
2975
+ loadingCount,
2346
2976
  empty,
2347
2977
  error,
2348
2978
  className,
2349
2979
  ...rootProps
2350
2980
  }) {
2351
- const enabled = {
2352
- search: true,
2353
- sort: true,
2354
- pagination: true,
2355
- tagFilter: false,
2356
- bulkActions: false,
2357
- ...features
2358
- };
2359
- const [searchValue, setSearchValue] = useState6(query.search?.query ?? "");
2360
- const [sort, setSort] = useState6(query.sort ?? { by: "updatedAt", direction: "desc" });
2361
- const [tag, setTag] = useState6(query.tags?.[0]);
2362
- const [page, setPage] = useState6(query.pagination?.page ?? 1);
2363
- const resolvedPageSize = query.pagination?.pageSize ?? pageSize;
2364
- const resolvedQuery = useMemo3(
2365
- () => ({
2366
- ...query,
2367
- search: enabled.search ? { ...query.search, query: searchValue } : query.search,
2368
- sort: enabled.sort ? sort : query.sort,
2369
- tags: tag ? [.../* @__PURE__ */ new Set([...query.tags ?? [], tag])] : query.tags,
2370
- pagination: enabled.pagination ? { ...query.pagination, page, pageSize: resolvedPageSize } : query.pagination
2371
- }),
2372
- [enabled.pagination, enabled.search, enabled.sort, page, query, resolvedPageSize, searchValue, sort, tag]
2373
- );
2374
- useKeepUrlSync({
2375
- enabled: Boolean(urlSync),
2376
- query: resolvedQuery,
2377
- onQueryChange: (nextOrUpdater) => {
2378
- const next = typeof nextOrUpdater === "function" ? nextOrUpdater(resolvedQuery) : nextOrUpdater;
2379
- setSearchValue(next.search?.query ?? "");
2380
- setSort(next.sort ?? { by: "updatedAt", direction: "desc" });
2381
- setTag(next.tags?.[0]);
2382
- setPage(next.pagination?.page ?? 1);
2383
- },
2384
- options: typeof urlSync === "object" ? urlSync : {},
2385
- adapter: urlAdapter
2386
- });
2387
- const list = useKeepList4(resolvedQuery);
2388
- return /* @__PURE__ */ jsxs8(
2981
+ const view = useKeepCollection({ query, pageSize, urlSync, urlAdapter, features });
2982
+ return /* @__PURE__ */ jsxs9(
2389
2983
  "section",
2390
2984
  {
2391
2985
  ...rootProps,
2392
2986
  className,
2393
2987
  "data-keepkit": "collection",
2394
2988
  "data-layout": layout,
2395
- "aria-busy": list.isLoading || list.isMutating || rootProps["aria-busy"],
2396
- "data-state": getCollectionState(list),
2397
- "data-loading": list.isLoading || list.isMutating ? "true" : void 0,
2989
+ "aria-busy": view.list.isLoading || view.list.isMutating || rootProps["aria-busy"],
2990
+ "data-state": getCollectionState(view.list),
2991
+ "data-loading": view.list.isLoading || view.list.isMutating ? "true" : void 0,
2398
2992
  children: [
2399
- /* @__PURE__ */ jsxs8("div", { children: [
2400
- enabled.search ? /* @__PURE__ */ jsx13(
2401
- KeepSearchInput,
2402
- {
2403
- value: searchValue,
2404
- onValueChange: (value) => {
2405
- setSearchValue(value);
2406
- setPage(1);
2407
- }
2408
- }
2409
- ) : null,
2410
- enabled.sort ? /* @__PURE__ */ jsx13(
2411
- KeepSortSelect,
2412
- {
2413
- value: sortToValue(sort),
2414
- onValueChange: (_value, nextSort) => {
2415
- setSort(nextSort);
2416
- setPage(1);
2417
- }
2418
- }
2419
- ) : null,
2420
- enabled.tagFilter ? /* @__PURE__ */ jsx13(
2421
- KeepTagFilter,
2422
- {
2423
- query,
2424
- value: tag,
2425
- onValueChange: (value) => {
2426
- setTag(value);
2427
- setPage(1);
2428
- }
2429
- }
2430
- ) : null
2993
+ /* @__PURE__ */ jsxs9("div", { children: [
2994
+ view.enabled.search ? /* @__PURE__ */ jsx13(KeepSearchInput, { value: view.searchValue, onValueChange: view.setSearchValue }) : null,
2995
+ view.enabled.sort ? /* @__PURE__ */ jsx13(KeepSortSelect, { value: view.sortValue, onValueChange: view.setSortValue }) : null,
2996
+ view.enabled.tagFilter ? /* @__PURE__ */ jsx13(KeepTagFilter, { query, value: view.tag, onValueChange: view.setTag }) : null
2431
2997
  ] }),
2432
2998
  /* @__PURE__ */ jsx13(
2433
2999
  KeepList,
2434
3000
  {
2435
- query: resolvedQuery,
3001
+ query: view.resolvedQuery,
2436
3002
  renderItem,
2437
3003
  itemCardProps,
2438
3004
  layout,
2439
3005
  loading,
3006
+ renderLoading,
3007
+ loadingCount,
2440
3008
  empty,
2441
3009
  error
2442
3010
  }
2443
3011
  ),
2444
- enabled.pagination ? /* @__PURE__ */ jsx13(
3012
+ view.enabled.pagination ? /* @__PURE__ */ jsx13(
2445
3013
  KeepPagination,
2446
3014
  {
2447
- totalCount: list.totalCount,
2448
- pageSize: resolvedPageSize,
2449
- page: list.page,
2450
- onPageChange: (nextPage) => setPage(nextPage)
3015
+ totalCount: view.list.totalCount,
3016
+ pageSize: view.resolvedPageSize,
3017
+ page: view.list.page,
3018
+ onPageChange: view.setPage
2451
3019
  }
2452
3020
  ) : null,
2453
- enabled.bulkActions ? /* @__PURE__ */ jsx13(KeepBulkActions, { query: resolvedQuery }) : null
3021
+ view.enabled.bulkActions ? /* @__PURE__ */ jsx13(KeepBulkActions, { query: view.resolvedQuery }) : null
2454
3022
  ]
2455
3023
  }
2456
3024
  );
@@ -2469,52 +3037,31 @@ function KeepLayout({ layout = "list", children, ...props }) {
2469
3037
  }
2470
3038
 
2471
3039
  // src/KeepNoteEditor.tsx
2472
- import { useKeepItem as useKeepItem3 } from "@keepkit/core/react";
2473
- import {
2474
- isValidElement as isValidElement5,
2475
- useCallback as useCallback2,
2476
- useEffect as useEffect3,
2477
- useRef as useRef3,
2478
- useState as useState7
2479
- } from "react";
2480
- import { Fragment as Fragment5, jsx as jsx15, jsxs as jsxs9 } from "react/jsx-runtime";
2481
- function KeepNoteEditor({
2482
- item,
2483
- label,
2484
- saveLabel,
2485
- placeholder,
2486
- debounceMs = 300,
2487
- onSaved,
2488
- onSaveError,
2489
- render,
2490
- children,
2491
- asChild = false,
2492
- className,
2493
- ...formProps
2494
- }) {
2495
- const defaultLabel = useUiLabel("note");
2496
- const defaultSaveLabel = useUiLabel("saveNote");
2497
- const errorLabel = useUiLabel("error");
3040
+ import { isValidElement as isValidElement5 } from "react";
3041
+
3042
+ // src/hooks/useKeepNoteEditor.ts
3043
+ import { useKeepItem as useKeepItem3 } from "@keepkit/core/react";
3044
+ import { useCallback as useCallback4, useEffect as useEffect4, useRef as useRef4, useState as useState7 } from "react";
3045
+ function useKeepNoteEditor({ item, debounceMs, onSaved, onSaveError }) {
2498
3046
  const itemState = useKeepItem3(item);
2499
3047
  const { error, isMutating, item: savedItem, updateNote } = itemState;
2500
- const contentChildren = asChild && isValidElement5(children) ? void 0 : children;
2501
3048
  const [note, setNote] = useState7(item.note ?? "");
2502
3049
  const baselineNote = savedItem?.note ?? item.note ?? "";
2503
3050
  const isDirty = note !== baselineNote;
2504
- const lastSavedNoteRef = useRef3(void 0);
2505
- useEffect3(() => setNote(baselineNote), [baselineNote]);
2506
- const save = useCallback2(async () => {
3051
+ const lastSavedNoteRef = useRef4(void 0);
3052
+ useEffect4(() => setNote(baselineNote), [baselineNote]);
3053
+ const save = useCallback4(async () => {
2507
3054
  const nextNote = note.trim() || void 0;
2508
3055
  try {
2509
3056
  await updateNote(nextNote);
2510
3057
  lastSavedNoteRef.current = note;
2511
3058
  onSaved?.(nextNote);
2512
- } catch (error2) {
2513
- onSaveError?.(error2);
2514
- throw error2;
3059
+ } catch (cause) {
3060
+ onSaveError?.(cause);
3061
+ throw cause;
2515
3062
  }
2516
3063
  }, [note, onSaveError, onSaved, updateNote]);
2517
- useEffect3(() => {
3064
+ useEffect4(() => {
2518
3065
  if (!isDirty || debounceMs <= 0 || lastSavedNoteRef.current === note) return;
2519
3066
  const timer = window.setTimeout(() => void save().catch(() => void 0), debounceMs);
2520
3067
  return () => window.clearTimeout(timer);
@@ -2528,46 +3075,77 @@ function KeepNoteEditor({
2528
3075
  error,
2529
3076
  save
2530
3077
  };
2531
- const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? /* @__PURE__ */ jsxs9(Fragment5, { children: [
2532
- /* @__PURE__ */ jsxs9("label", { children: [
2533
- label ?? defaultLabel,
3078
+ const submit = (event) => {
3079
+ event.preventDefault();
3080
+ void save().catch(() => void 0);
3081
+ };
3082
+ return {
3083
+ state,
3084
+ submit,
3085
+ handleKeyDown: (event) => {
3086
+ if (event.key !== "Enter" || !event.ctrlKey && !event.metaKey) return;
3087
+ event.preventDefault();
3088
+ void save().catch(() => void 0);
3089
+ },
3090
+ labels: {
3091
+ note: useUiLabel("note"),
3092
+ save: useUiLabel("saveNote"),
3093
+ error: useUiLabel("error")
3094
+ }
3095
+ };
3096
+ }
3097
+
3098
+ // src/KeepNoteEditor.tsx
3099
+ import { Fragment as Fragment6, jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
3100
+ function KeepNoteEditor({
3101
+ item,
3102
+ label,
3103
+ saveLabel,
3104
+ placeholder,
3105
+ debounceMs = 300,
3106
+ onSaved,
3107
+ onSaveError,
3108
+ render,
3109
+ children,
3110
+ asChild = false,
3111
+ className,
3112
+ ...formProps
3113
+ }) {
3114
+ const view = useKeepNoteEditor({ item, debounceMs, onSaved, onSaveError });
3115
+ const { error, isDirty, isSaving, note, setNote } = view.state;
3116
+ const contentChildren = asChild && isValidElement5(children) ? void 0 : children;
3117
+ const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? /* @__PURE__ */ jsxs10(Fragment6, { children: [
3118
+ /* @__PURE__ */ jsxs10("label", { children: [
3119
+ label ?? view.labels.note,
2534
3120
  /* @__PURE__ */ jsx15(
2535
3121
  "textarea",
2536
3122
  {
3123
+ "data-keep-action": "edit-note",
2537
3124
  value: note,
2538
3125
  onChange: (event) => setNote(event.currentTarget.value),
2539
3126
  placeholder,
2540
- disabled: isMutating,
2541
- onKeyDown: (event) => {
2542
- if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
2543
- event.preventDefault();
2544
- void save().catch(() => void 0);
2545
- }
2546
- }
3127
+ disabled: isSaving,
3128
+ onKeyDown: view.handleKeyDown
2547
3129
  }
2548
3130
  )
2549
3131
  ] }),
2550
- /* @__PURE__ */ jsx15("button", { type: "submit", disabled: isMutating, "aria-busy": isMutating, children: saveLabel ?? defaultSaveLabel })
3132
+ /* @__PURE__ */ jsx15("button", { type: "submit", "data-keep-action": "save-note", disabled: isSaving, "aria-busy": isSaving, children: saveLabel ?? view.labels.save })
2551
3133
  ] });
2552
- const handleSubmit = (event) => {
2553
- event.preventDefault();
2554
- void save().catch(() => void 0);
2555
- };
2556
3134
  if (!asChild) {
2557
- return /* @__PURE__ */ jsxs9(
3135
+ return /* @__PURE__ */ jsxs10(
2558
3136
  "form",
2559
3137
  {
2560
3138
  ...formProps,
2561
3139
  className,
2562
3140
  "data-keepkit": "note-editor",
2563
- onSubmit: handleSubmit,
2564
- "aria-busy": isMutating || formProps["aria-busy"],
3141
+ onSubmit: view.submit,
3142
+ "aria-busy": isSaving || formProps["aria-busy"],
2565
3143
  "data-state": error ? "error" : isDirty ? "dirty" : "clean",
2566
- "data-loading": isMutating ? "true" : void 0,
2567
- "data-disabled": isMutating ? "true" : void 0,
3144
+ "data-loading": isSaving ? "true" : void 0,
3145
+ "data-disabled": isSaving ? "true" : void 0,
2568
3146
  children: [
2569
3147
  body,
2570
- error ? /* @__PURE__ */ jsx15("p", { role: "alert", children: getErrorMessage3(error, errorLabel) }) : null
3148
+ error ? /* @__PURE__ */ jsx15("p", { role: "alert", children: getErrorMessage3(error, view.labels.error) }) : null
2571
3149
  ]
2572
3150
  }
2573
3151
  );
@@ -2579,11 +3157,11 @@ function KeepNoteEditor({
2579
3157
  ...formProps,
2580
3158
  className,
2581
3159
  "data-keepkit": "note-editor",
2582
- onSubmit: handleSubmit,
2583
- "aria-busy": isMutating || formProps["aria-busy"],
3160
+ onSubmit: view.submit,
3161
+ "aria-busy": isSaving || formProps["aria-busy"],
2584
3162
  "data-state": error ? "error" : isDirty ? "dirty" : "clean",
2585
- "data-loading": isMutating ? "true" : void 0,
2586
- "data-disabled": isMutating ? "true" : void 0
3163
+ "data-loading": isSaving ? "true" : void 0,
3164
+ "data-disabled": isSaving ? "true" : void 0
2587
3165
  },
2588
3166
  body,
2589
3167
  "KeepNoteEditor"
@@ -2593,10 +3171,91 @@ function getErrorMessage3(error, fallback) {
2593
3171
  return error instanceof Error ? error.message : fallback;
2594
3172
  }
2595
3173
 
2596
- // src/KeepSyncRecoveryDialog.tsx
3174
+ // src/hooks/useKeepSyncFeedback.ts
2597
3175
  import { useKeepContext as useKeepContext3 } from "@keepkit/core/react";
2598
- import { useEffect as useEffect4, useState as useState8 } from "react";
2599
- import { jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
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";
3205
+ function useKeepSyncRecoveryDialog(options) {
3206
+ const { open, onOpenChange, conflicts, onManualMerge } = options;
3207
+ const context = useKeepContext4();
3208
+ const conflictList = conflicts ?? context.syncState.conflicts ?? [];
3209
+ const hasRecovery = conflictList.length > 0 || context.syncState.status === "error" || Boolean(context.error);
3210
+ const [dismissed, setDismissed] = useState8(false);
3211
+ const [busyId, setBusyId] = useState8();
3212
+ const [error, setError] = useState8();
3213
+ useEffect6(() => {
3214
+ if (hasRecovery) setDismissed(false);
3215
+ }, [hasRecovery]);
3216
+ return {
3217
+ conflictList,
3218
+ isOpen: open ?? (hasRecovery && !dismissed),
3219
+ busyId,
3220
+ error,
3221
+ showBackupRecovery: context.syncState.status === "error" || Boolean(context.error),
3222
+ close: () => {
3223
+ setDismissed(true);
3224
+ onOpenChange?.(false);
3225
+ },
3226
+ resolve: async (conflict, resolution) => {
3227
+ setError(void 0);
3228
+ setBusyId(conflict.id);
3229
+ try {
3230
+ const merged = resolution === "manual" ? await onManualMerge?.(conflict) : void 0;
3231
+ if (resolution === "manual" && !merged) throw new Error("A manual merge result is required.");
3232
+ await context.resolveSyncConflict(conflict.id, resolution, merged);
3233
+ } catch (cause) {
3234
+ setError(cause);
3235
+ } finally {
3236
+ setBusyId(void 0);
3237
+ }
3238
+ },
3239
+ labels: {
3240
+ close: useUiLabel("close"),
3241
+ title: useUiLabel("resolveSync"),
3242
+ conflict: useUiLabel("syncConflict"),
3243
+ keepLocal: useUiLabel("keepLocal"),
3244
+ useServer: useUiLabel("useServer"),
3245
+ manualMerge: useUiLabel("manualMerge"),
3246
+ localVersion: useUiLabel("localVersion"),
3247
+ remoteVersion: useUiLabel("remoteVersion"),
3248
+ updatedAt: useUiLabel("updatedAt"),
3249
+ note: useUiLabel("note"),
3250
+ backupRecovery: useUiLabel("backupRecovery"),
3251
+ backupRecoveryDescription: useUiLabel("backupRecoveryDescription"),
3252
+ error: useUiLabel("error")
3253
+ }
3254
+ };
3255
+ }
3256
+
3257
+ // src/KeepSyncRecoveryDialog.tsx
3258
+ import { jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
2600
3259
  function KeepSyncRecoveryDialog({
2601
3260
  open,
2602
3261
  onOpenChange,
@@ -2609,49 +3268,9 @@ function KeepSyncRecoveryDialog({
2609
3268
  className,
2610
3269
  ...props
2611
3270
  }) {
2612
- const context = useKeepContext3();
2613
- const closeLabel = useUiLabel("close");
2614
- const defaultDialogTitle = useUiLabel("resolveSync");
2615
- const dialogTitle = title ?? defaultDialogTitle;
2616
- const conflictLabel = useUiLabel("syncConflict");
2617
- const keepLocalLabel = useUiLabel("keepLocal");
2618
- const useServerLabel = useUiLabel("useServer");
2619
- const manualMergeLabel = useUiLabel("manualMerge");
2620
- const localVersionLabel = useUiLabel("localVersion");
2621
- const remoteVersionLabel = useUiLabel("remoteVersion");
2622
- const updatedAtLabel = useUiLabel("updatedAt");
2623
- const noteLabel = useUiLabel("note");
2624
- const backupRecoveryLabel = useUiLabel("backupRecovery");
2625
- const backupRecoveryDescription = useUiLabel("backupRecoveryDescription");
2626
- const errorLabel = useUiLabel("error");
2627
- const conflictList = conflicts ?? context.syncState.conflicts ?? [];
2628
- const hasRecovery = conflictList.length > 0 || context.syncState.status === "error" || Boolean(context.error);
2629
- const [dismissed, setDismissed] = useState8(false);
2630
- const [busyId, setBusyId] = useState8();
2631
- const [error, setError] = useState8();
2632
- useEffect4(() => {
2633
- if (hasRecovery) setDismissed(false);
2634
- }, [hasRecovery]);
2635
- const isOpen = open ?? (hasRecovery && !dismissed);
2636
- if (!isOpen) return null;
2637
- const close = () => {
2638
- setDismissed(true);
2639
- onOpenChange?.(false);
2640
- };
2641
- async function resolve(conflict, resolution) {
2642
- setError(void 0);
2643
- setBusyId(conflict.id);
2644
- try {
2645
- const merged = resolution === "manual" ? await onManualMerge?.(conflict) : void 0;
2646
- if (resolution === "manual" && !merged) throw new Error("A manual merge result is required.");
2647
- await context.resolveSyncConflict(conflict.id, resolution, merged);
2648
- } catch (cause) {
2649
- setError(cause);
2650
- } finally {
2651
- setBusyId(void 0);
2652
- }
2653
- }
2654
- return /* @__PURE__ */ jsxs10(
3271
+ const view = useKeepSyncRecoveryDialog({ open, onOpenChange, conflicts, onManualMerge });
3272
+ if (!view.isOpen) return null;
3273
+ return /* @__PURE__ */ jsxs11(
2655
3274
  "section",
2656
3275
  {
2657
3276
  ...props,
@@ -2659,29 +3278,29 @@ function KeepSyncRecoveryDialog({
2659
3278
  role: "dialog",
2660
3279
  "aria-modal": "true",
2661
3280
  "aria-labelledby": "keepkit-sync-recovery-title",
2662
- "aria-describedby": error ? "keepkit-sync-recovery-error" : void 0,
2663
- "aria-busy": busyId !== void 0,
3281
+ "aria-describedby": view.error ? "keepkit-sync-recovery-error" : void 0,
3282
+ "aria-busy": view.busyId !== void 0,
2664
3283
  "data-keepkit": "sync-recovery",
2665
- "data-state": conflictList.length > 0 ? "conflict" : "error",
2666
- "data-loading": busyId !== void 0 ? "true" : void 0,
3284
+ "data-state": view.conflictList.length > 0 ? "conflict" : "error",
3285
+ "data-loading": view.busyId !== void 0 ? "true" : void 0,
2667
3286
  children: [
2668
- /* @__PURE__ */ jsxs10("header", { children: [
2669
- /* @__PURE__ */ jsx16("h2", { id: "keepkit-sync-recovery-title", children: dialogTitle }),
2670
- /* @__PURE__ */ jsx16("button", { type: "button", onClick: close, "aria-label": closeLabel, children: closeLabel })
3287
+ /* @__PURE__ */ jsxs11("header", { children: [
3288
+ /* @__PURE__ */ jsx16("h2", { id: "keepkit-sync-recovery-title", children: title ?? view.labels.title }),
3289
+ /* @__PURE__ */ jsx16("button", { type: "button", "data-keep-action": "close-dialog", onClick: view.close, "aria-label": view.labels.close, children: view.labels.close })
2671
3290
  ] }),
2672
3291
  children,
2673
- conflictList.length > 0 ? /* @__PURE__ */ jsxs10("div", { children: [
2674
- /* @__PURE__ */ jsx16("p", { children: conflictLabel }),
2675
- conflictList.map((conflict) => /* @__PURE__ */ jsxs10("article", { "data-conflict-id": conflict.id, children: [
3292
+ view.conflictList.length > 0 ? /* @__PURE__ */ jsxs11("div", { children: [
3293
+ /* @__PURE__ */ jsx16("p", { children: view.labels.conflict }),
3294
+ view.conflictList.map((conflict) => /* @__PURE__ */ jsxs11("article", { "data-conflict-id": conflict.id, children: [
2676
3295
  /* @__PURE__ */ jsx16("h3", { children: getMetaTitle(conflict.operation.item?.meta) ?? conflict.id }),
2677
- /* @__PURE__ */ jsxs10("div", { "data-conflict-preview": true, children: [
3296
+ /* @__PURE__ */ jsxs11("div", { "data-conflict-preview": true, children: [
2678
3297
  /* @__PURE__ */ jsx16(
2679
3298
  ConflictPreview,
2680
3299
  {
2681
3300
  item: conflict.operation.item,
2682
- heading: localVersionLabel,
2683
- updatedAtLabel,
2684
- noteLabel,
3301
+ heading: view.labels.localVersion,
3302
+ updatedAtLabel: view.labels.updatedAt,
3303
+ noteLabel: view.labels.note,
2685
3304
  side: "local"
2686
3305
  }
2687
3306
  ),
@@ -2689,34 +3308,53 @@ function KeepSyncRecoveryDialog({
2689
3308
  ConflictPreview,
2690
3309
  {
2691
3310
  item: conflict.remote,
2692
- heading: remoteVersionLabel,
2693
- updatedAtLabel,
2694
- noteLabel,
3311
+ heading: view.labels.remoteVersion,
3312
+ updatedAtLabel: view.labels.updatedAt,
3313
+ noteLabel: view.labels.note,
2695
3314
  side: "remote"
2696
3315
  }
2697
3316
  )
2698
3317
  ] }),
2699
- /* @__PURE__ */ jsxs10("div", { children: [
2700
- /* @__PURE__ */ jsx16("button", { type: "button", onClick: () => void resolve(conflict, "local"), disabled: busyId !== void 0, children: keepLocalLabel }),
2701
- /* @__PURE__ */ jsx16("button", { type: "button", onClick: () => void resolve(conflict, "remote"), disabled: busyId !== void 0, children: useServerLabel }),
3318
+ /* @__PURE__ */ jsxs11("div", { children: [
3319
+ /* @__PURE__ */ jsx16(
3320
+ "button",
3321
+ {
3322
+ type: "button",
3323
+ "data-keep-action": "keep-local",
3324
+ onClick: () => void view.resolve(conflict, "local"),
3325
+ disabled: view.busyId !== void 0,
3326
+ children: view.labels.keepLocal
3327
+ }
3328
+ ),
2702
3329
  /* @__PURE__ */ jsx16(
2703
3330
  "button",
2704
3331
  {
2705
3332
  type: "button",
2706
- onClick: () => void resolve(conflict, "manual"),
2707
- disabled: busyId !== void 0 || !onManualMerge,
2708
- children: manualMergeLabel
3333
+ "data-keep-action": "use-server",
3334
+ onClick: () => void view.resolve(conflict, "remote"),
3335
+ disabled: view.busyId !== void 0,
3336
+ children: view.labels.useServer
3337
+ }
3338
+ ),
3339
+ /* @__PURE__ */ jsx16(
3340
+ "button",
3341
+ {
3342
+ type: "button",
3343
+ "data-keep-action": "manual-merge",
3344
+ onClick: () => void view.resolve(conflict, "manual"),
3345
+ disabled: view.busyId !== void 0 || !onManualMerge,
3346
+ children: view.labels.manualMerge
2709
3347
  }
2710
3348
  )
2711
3349
  ] })
2712
3350
  ] }, conflict.id))
2713
3351
  ] }) : null,
2714
- context.syncState.status === "error" || context.error ? /* @__PURE__ */ jsxs10("section", { "data-recovery": "backup", children: [
2715
- /* @__PURE__ */ jsx16("h3", { children: backupRecoveryLabel }),
2716
- /* @__PURE__ */ jsx16("p", { children: backupRecoveryDescription }),
3352
+ view.showBackupRecovery ? /* @__PURE__ */ jsxs11("section", { "data-recovery": "backup", children: [
3353
+ /* @__PURE__ */ jsx16("h3", { children: view.labels.backupRecovery }),
3354
+ /* @__PURE__ */ jsx16("p", { children: view.labels.backupRecoveryDescription }),
2717
3355
  backup ?? (showBackupControls ? /* @__PURE__ */ jsx16(KeepBackup, {}) : null)
2718
3356
  ] }) : null,
2719
- error ? /* @__PURE__ */ jsx16("p", { id: "keepkit-sync-recovery-error", role: "alert", "aria-live": "assertive", children: error instanceof Error ? error.message : errorLabel }) : null
3357
+ view.error ? /* @__PURE__ */ jsx16("p", { id: "keepkit-sync-recovery-error", role: "alert", "aria-live": "assertive", children: view.error instanceof Error ? view.error.message : view.labels.error }) : null
2720
3358
  ]
2721
3359
  }
2722
3360
  );
@@ -2728,14 +3366,14 @@ function ConflictPreview({
2728
3366
  noteLabel,
2729
3367
  side
2730
3368
  }) {
2731
- 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: [
2732
3370
  /* @__PURE__ */ jsx16("h4", { children: heading }),
2733
- /* @__PURE__ */ jsxs10("dl", { children: [
2734
- /* @__PURE__ */ jsxs10("div", { children: [
3371
+ /* @__PURE__ */ jsxs11("dl", { children: [
3372
+ /* @__PURE__ */ jsxs11("div", { children: [
2735
3373
  /* @__PURE__ */ jsx16("dt", { children: updatedAtLabel }),
2736
3374
  /* @__PURE__ */ jsx16("dd", { children: item ? /* @__PURE__ */ jsx16("time", { dateTime: new Date(item.updatedAt).toISOString(), children: formatConflictDate(item.updatedAt) }) : "\u2014" })
2737
3375
  ] }),
2738
- /* @__PURE__ */ jsxs10("div", { children: [
3376
+ /* @__PURE__ */ jsxs11("div", { children: [
2739
3377
  /* @__PURE__ */ jsx16("dt", { children: noteLabel }),
2740
3378
  /* @__PURE__ */ jsx16("dd", { children: item?.note || "\u2014" })
2741
3379
  ] })
@@ -2746,17 +3384,10 @@ function formatConflictDate(timestamp) {
2746
3384
  return new Date(timestamp).toISOString().slice(0, 10);
2747
3385
  }
2748
3386
 
2749
- // src/KeepSyncStatusBanner.tsx
2750
- import { useKeepContext as useKeepContext4 } from "@keepkit/core/react";
2751
- import { jsx as jsx17, jsxs as jsxs11 } from "react/jsx-runtime";
2752
- function KeepSyncStatusBanner({
2753
- onRetry,
2754
- onResolveConflicts,
2755
- children,
2756
- className,
2757
- ...props
2758
- }) {
2759
- const context = useKeepContext4();
3387
+ // src/hooks/useKeepSyncStatusBanner.ts
3388
+ import { useKeepContext as useKeepContext5 } from "@keepkit/core/react";
3389
+ function useKeepSyncStatusBanner({ onRetry, children }) {
3390
+ const context = useKeepContext5();
2760
3391
  const retryLabel = useUiLabel("retrySync");
2761
3392
  const resolveLabel = useUiLabel("resolveSync");
2762
3393
  const conflictLabel = useUiLabel("syncConflict");
@@ -2764,117 +3395,175 @@ function KeepSyncStatusBanner({
2764
3395
  const syncedLabel = useUiLabel("syncSynced");
2765
3396
  const status = context.syncState.status;
2766
3397
  const hasConflicts = (context.syncState.conflicts?.length ?? 0) > 0 || context.syncState.conflictIds.length > 0;
2767
- if (status === "idle" && !hasConflicts) return null;
2768
3398
  const message = children ?? (status === "error" ? getErrorMessage4(context.syncState.error) : status === "conflict" || hasConflicts ? conflictLabel : status === "pending" || status === "syncing" ? pendingLabel : syncedLabel);
2769
- const role = status === "error" || status === "conflict" || hasConflicts ? "alert" : "status";
2770
- const retry = async () => {
2771
- if (onRetry) {
2772
- await onRetry();
2773
- return;
3399
+ return {
3400
+ status,
3401
+ hasConflicts,
3402
+ message,
3403
+ isMutating: context.isMutating,
3404
+ role: status === "error" || status === "conflict" || hasConflicts ? "alert" : "status",
3405
+ showRetry: status === "error" || status === "pending" || status === "syncing",
3406
+ retryLabel,
3407
+ resolveLabel,
3408
+ retry: async () => {
3409
+ if (onRetry) {
3410
+ await onRetry();
3411
+ return;
3412
+ }
3413
+ await context.flushSync();
2774
3414
  }
2775
- await context.flushSync();
2776
3415
  };
2777
- return /* @__PURE__ */ jsxs11(
3416
+ }
3417
+ function getErrorMessage4(error) {
3418
+ return error instanceof Error ? error.message : "Sync failed.";
3419
+ }
3420
+
3421
+ // src/KeepSyncStatusBanner.tsx
3422
+ import { jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
3423
+ function KeepSyncStatusBanner({
3424
+ onRetry,
3425
+ onResolveConflicts,
3426
+ children,
3427
+ className,
3428
+ ...props
3429
+ }) {
3430
+ const view = useKeepSyncStatusBanner({ onRetry, children });
3431
+ if (view.status === "idle" && !view.hasConflicts) return null;
3432
+ return /* @__PURE__ */ jsxs12(
2778
3433
  "aside",
2779
3434
  {
2780
3435
  ...props,
2781
3436
  className,
2782
- role: props.role ?? role,
3437
+ role: props.role ?? view.role,
2783
3438
  "aria-live": props["aria-live"] ?? "polite",
2784
3439
  "data-keepkit": "sync-status",
2785
- "data-state": status,
3440
+ "data-state": view.status,
2786
3441
  children: [
2787
- /* @__PURE__ */ jsx17("p", { children: message }),
2788
- status === "error" || status === "pending" || status === "syncing" ? /* @__PURE__ */ jsx17("button", { type: "button", onClick: () => void retry(), disabled: context.isMutating, children: retryLabel }) : null,
2789
- hasConflicts ? /* @__PURE__ */ jsx17("button", { type: "button", onClick: onResolveConflicts, disabled: !onResolveConflicts, children: resolveLabel }) : null
3442
+ /* @__PURE__ */ jsx17("p", { children: view.message }),
3443
+ view.showRetry ? /* @__PURE__ */ jsx17(
3444
+ "button",
3445
+ {
3446
+ type: "button",
3447
+ "data-keep-action": "retry-sync",
3448
+ onClick: () => void view.retry(),
3449
+ disabled: view.isMutating,
3450
+ children: view.retryLabel
3451
+ }
3452
+ ) : null,
3453
+ view.hasConflicts ? /* @__PURE__ */ jsx17(
3454
+ "button",
3455
+ {
3456
+ type: "button",
3457
+ "data-keep-action": "resolve-conflicts",
3458
+ onClick: onResolveConflicts,
3459
+ disabled: !onResolveConflicts,
3460
+ children: view.resolveLabel
3461
+ }
3462
+ ) : null
2790
3463
  ]
2791
3464
  }
2792
3465
  );
2793
3466
  }
2794
- function getErrorMessage4(error) {
2795
- return error instanceof Error ? error.message : "Sync failed.";
2796
- }
2797
3467
 
2798
- // src/KeepTagEditor.tsx
3468
+ // src/hooks/useKeepTagEditor.ts
2799
3469
  import { useKeepItem as useKeepItem4 } from "@keepkit/core/react";
2800
- import { useCallback as useCallback3, useEffect as useEffect5, useState as useState9 } from "react";
2801
- import { Fragment as Fragment6, jsx as jsx18, jsxs as jsxs12 } from "react/jsx-runtime";
2802
- function KeepTagEditor({
2803
- item,
2804
- availableTags = [],
2805
- onSaved,
2806
- onSaveError,
2807
- render,
2808
- ...props
2809
- }) {
2810
- const tagsLabel = useUiLabel("tagsToApply");
2811
- const removeLabel = useUiLabel("remove");
2812
- const applyTagsLabel = useUiLabel("applyTags");
2813
- const errorLabel = useUiLabel("error");
3470
+ import { useCallback as useCallback5, useEffect as useEffect7, useState as useState9 } from "react";
3471
+ function useKeepTagEditor({ item, onSaved, onSaveError }) {
2814
3472
  const itemState = useKeepItem4(item);
2815
3473
  const [tags, setTags] = useState9(item.tags ?? []);
2816
3474
  const [input, setInput] = useState9("");
2817
- useEffect5(() => setTags(itemState.item?.tags ?? item.tags ?? []), [item.tags, itemState.item?.tags]);
2818
- const save = useCallback3(async () => {
3475
+ useEffect7(() => setTags(itemState.item?.tags ?? item.tags ?? []), [item.tags, itemState.item?.tags]);
3476
+ const save = useCallback5(async () => {
2819
3477
  const nextTags = normalizeUiTags(tags);
2820
3478
  try {
2821
3479
  await itemState.updateTags(nextTags);
2822
3480
  setTags(nextTags);
2823
3481
  onSaved?.(nextTags);
2824
- } catch (error) {
2825
- onSaveError?.(error);
2826
- throw error;
3482
+ } catch (cause) {
3483
+ onSaveError?.(cause);
3484
+ throw cause;
2827
3485
  }
2828
3486
  }, [itemState, onSaveError, onSaved, tags]);
2829
3487
  const addTag = (tag) => {
2830
3488
  setTags(normalizeUiTags([...tags, tag]));
2831
3489
  setInput("");
2832
3490
  };
2833
- const body = render ? render({ tags, setTags, save, isSaving: itemState.isMutating }) : /* @__PURE__ */ jsxs12(Fragment6, { children: [
2834
- /* @__PURE__ */ jsxs12("label", { children: [
2835
- tagsLabel,
3491
+ const state = { tags, setTags, save, isSaving: itemState.isMutating };
3492
+ return {
3493
+ state,
3494
+ input,
3495
+ setInput,
3496
+ error: itemState.error,
3497
+ handleInputKeyDown: (event) => {
3498
+ if (event.key === "Enter") {
3499
+ if (event.nativeEvent.isComposing) return;
3500
+ event.preventDefault();
3501
+ if (input.trim()) addTag(input);
3502
+ } else if (event.key === "Backspace" && input.length === 0 && tags.length > 0) {
3503
+ event.preventDefault();
3504
+ setTags(tags.slice(0, -1));
3505
+ }
3506
+ },
3507
+ removeTag: (tag) => setTags(tags.filter((current) => current !== tag)),
3508
+ submit: (event) => {
3509
+ event.preventDefault();
3510
+ void save().catch(() => void 0);
3511
+ },
3512
+ labels: {
3513
+ tags: useUiLabel("tagsToApply"),
3514
+ remove: useUiLabel("remove"),
3515
+ apply: useUiLabel("applyTags"),
3516
+ error: useUiLabel("error")
3517
+ }
3518
+ };
3519
+ }
3520
+
3521
+ // src/KeepTagEditor.tsx
3522
+ import { Fragment as Fragment7, jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
3523
+ function KeepTagEditor({
3524
+ item,
3525
+ availableTags = [],
3526
+ onSaved,
3527
+ onSaveError,
3528
+ render,
3529
+ ...props
3530
+ }) {
3531
+ const view = useKeepTagEditor({ item, onSaved, onSaveError });
3532
+ const { isSaving, tags } = view.state;
3533
+ const body = render ? render(view.state) : /* @__PURE__ */ jsxs13(Fragment7, { children: [
3534
+ /* @__PURE__ */ jsxs13("label", { children: [
3535
+ view.labels.tags,
2836
3536
  /* @__PURE__ */ jsx18(
2837
3537
  "input",
2838
3538
  {
2839
- value: input,
3539
+ "data-keep-action": "edit-tags",
3540
+ value: view.input,
2840
3541
  list: availableTags.length > 0 ? `keep-tags-${item.id}` : void 0,
2841
- onChange: (event) => setInput(event.currentTarget.value),
2842
- onKeyDown: (event) => {
2843
- if (event.key === "Enter") {
2844
- if (event.nativeEvent.isComposing) return;
2845
- event.preventDefault();
2846
- if (input.trim()) addTag(input);
2847
- } else if (event.key === "Backspace" && input.length === 0 && tags.length > 0) {
2848
- event.preventDefault();
2849
- setTags(tags.slice(0, -1));
2850
- }
2851
- }
3542
+ onChange: (event) => view.setInput(event.currentTarget.value),
3543
+ onKeyDown: view.handleInputKeyDown
2852
3544
  }
2853
3545
  )
2854
3546
  ] }),
2855
3547
  availableTags.length > 0 ? /* @__PURE__ */ jsx18("datalist", { id: `keep-tags-${item.id}`, children: availableTags.map((tag) => /* @__PURE__ */ jsx18("option", { value: tag }, tag)) }) : null,
2856
- /* @__PURE__ */ jsx18("ul", { "aria-label": tagsLabel, 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: [
2857
3549
  tag,
2858
- /* @__PURE__ */ jsx18("button", { type: "button", onClick: () => setTags(tags.filter((current) => current !== tag)), children: removeLabel })
3550
+ /* @__PURE__ */ jsx18("button", { type: "button", "data-keep-action": "remove-tag", onClick: () => view.removeTag(tag), children: view.labels.remove })
2859
3551
  ] }, tag)) }),
2860
- /* @__PURE__ */ jsx18("button", { type: "submit", disabled: itemState.isMutating, "aria-busy": itemState.isMutating, children: applyTagsLabel })
3552
+ /* @__PURE__ */ jsx18("button", { type: "submit", "data-keep-action": "apply-tags", disabled: isSaving, "aria-busy": isSaving, children: view.labels.apply })
2861
3553
  ] });
2862
- return /* @__PURE__ */ jsxs12(
3554
+ return /* @__PURE__ */ jsxs13(
2863
3555
  "form",
2864
3556
  {
2865
3557
  ...props,
2866
- onSubmit: (event) => {
2867
- event.preventDefault();
2868
- void save().catch(() => void 0);
2869
- },
2870
- "aria-busy": itemState.isMutating || props["aria-busy"],
3558
+ onSubmit: view.submit,
3559
+ "aria-busy": isSaving || props["aria-busy"],
2871
3560
  "data-keepkit": "tag-editor",
2872
- "data-state": itemState.error ? "error" : itemState.isMutating ? "saving" : "idle",
2873
- "data-loading": itemState.isMutating ? "true" : void 0,
2874
- "data-disabled": itemState.isMutating ? "true" : void 0,
3561
+ "data-state": view.error ? "error" : isSaving ? "saving" : "idle",
3562
+ "data-loading": isSaving ? "true" : void 0,
3563
+ "data-disabled": isSaving ? "true" : void 0,
2875
3564
  children: [
2876
3565
  body,
2877
- itemState.error ? /* @__PURE__ */ jsx18("p", { role: "alert", children: getErrorMessage5(itemState.error, errorLabel) }) : null
3566
+ view.error ? /* @__PURE__ */ jsx18("p", { role: "alert", children: getErrorMessage5(view.error, view.labels.error) }) : null
2878
3567
  ]
2879
3568
  }
2880
3569
  );
@@ -2883,24 +3572,93 @@ function getErrorMessage5(error, fallback) {
2883
3572
  return error instanceof Error ? error.message : fallback;
2884
3573
  }
2885
3574
 
3575
+ // src/hooks/useKeepUndo.ts
3576
+ import { useKeepContext as useKeepContext6 } from "@keepkit/core/react";
3577
+ function useKeepUndo() {
3578
+ const context = useKeepContext6();
3579
+ const emitFeedback = useKeepUiFeedback();
3580
+ const restoredMessage = useUiLabel("restoredMessage");
3581
+ return {
3582
+ canUndo: context.undo.canUndo,
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
+ },
3590
+ message: useUiLabel("undoAvailable"),
3591
+ label: useUiLabel("undo")
3592
+ };
3593
+ }
3594
+
2886
3595
  // src/KeepUndo.tsx
2887
- import { useKeepContext as useKeepContext5 } from "@keepkit/core/react";
2888
- import { jsx as jsx19, jsxs as jsxs13 } from "react/jsx-runtime";
3596
+ import { jsx as jsx19, jsxs as jsxs14 } from "react/jsx-runtime";
2889
3597
  function KeepUndo({ children, label, ...props }) {
2890
- const context = useKeepContext5();
2891
- const message = useUiLabel("undoAvailable");
2892
- const undoLabel = useUiLabel("undo");
2893
- if (!context.undo.canUndo) return null;
2894
- return /* @__PURE__ */ jsxs13("div", { ...props, role: "status", "aria-live": "polite", "data-keepkit": "undo", "data-state": "available", children: [
2895
- children ?? message,
2896
- /* @__PURE__ */ jsx19("button", { type: "button", onClick: () => void context.undoLastRemoval(), children: label ?? undoLabel })
3598
+ const view = useKeepUndo();
3599
+ if (!view.canUndo) return null;
3600
+ return /* @__PURE__ */ jsxs14("div", { ...props, role: "status", "aria-live": "polite", "data-keepkit": "undo", "data-state": "available", children: [
3601
+ children ?? view.message,
3602
+ /* @__PURE__ */ jsx19("button", { type: "button", "data-keep-action": "undo", onClick: () => void view.undo(), children: label ?? view.label })
2897
3603
  ] });
2898
3604
  }
2899
3605
 
2900
3606
  // src/status.tsx
2901
- import { useKeepContext as useKeepContext6 } from "@keepkit/core/react";
2902
- import { isValidElement as isValidElement6, useEffect as useEffect6, useRef as useRef4, useState as useState10 } from "react";
2903
- import { Fragment as Fragment7, jsx as jsx20, jsxs as jsxs14 } from "react/jsx-runtime";
3607
+ import { isValidElement as isValidElement6 } from "react";
3608
+
3609
+ // src/hooks/useStatusViews.ts
3610
+ import { useKeepContext as useKeepContext7 } from "@keepkit/core/react";
3611
+ import { useEffect as useEffect8, useRef as useRef6, useState as useState10 } from "react";
3612
+ function useKeepEmptyState() {
3613
+ return useUiLabel("noItems").replace(/\.$/, "");
3614
+ }
3615
+ function useKeepStatus(status) {
3616
+ const context = useKeepContext7();
3617
+ const resolvedStatus = status ?? getDerivedStatus(context);
3618
+ const state = {
3619
+ status: resolvedStatus,
3620
+ error: context.error,
3621
+ pendingCount: context.syncState.pendingCount,
3622
+ items: context.items
3623
+ };
3624
+ return { state, defaultLabel: useUiLabel(getStatusLabelKey3(resolvedStatus)) };
3625
+ }
3626
+ function useKeepAnnouncements(messages) {
3627
+ const context = useKeepContext7();
3628
+ const savedMessage = useUiLabel("savedMessage", messages?.save);
3629
+ const removedMessage = useUiLabel("removedMessage", messages?.remove);
3630
+ const noteSavedMessage = useUiLabel("noteSavedMessage", messages?.note);
3631
+ const [message, setMessage] = useState10("");
3632
+ const lastChangeRef = useRef6(void 0);
3633
+ useEffect8(() => {
3634
+ const change = context.lastChange;
3635
+ if (!change || change === lastChangeRef.current) return;
3636
+ lastChangeRef.current = change;
3637
+ if (change.action === "save") setMessage(savedMessage);
3638
+ else if (change.action === "remove" || change.action === "removeBatch") setMessage(removedMessage);
3639
+ else if (change.action === "updateNote") setMessage(noteSavedMessage);
3640
+ }, [context.lastChange, noteSavedMessage, removedMessage, savedMessage]);
3641
+ return message;
3642
+ }
3643
+ function getDerivedStatus(context) {
3644
+ if (context.error) return "error";
3645
+ if (context.syncState.status === "pending" || context.syncState.status === "syncing") return "syncing";
3646
+ if (context.isMutating) return "saving";
3647
+ if (context.isLoading && !context.isHydrated) return "loading";
3648
+ if (context.isHydrated && context.items.length === 0) return "empty";
3649
+ return "idle";
3650
+ }
3651
+ function getStatusLabelKey3(status) {
3652
+ if (status === "empty") return "noItems";
3653
+ if (status === "loading") return "loadingItems";
3654
+ if (status === "error") return "error";
3655
+ if (status === "saving") return "saving";
3656
+ if (status === "syncing") return "syncing";
3657
+ return "saved";
3658
+ }
3659
+
3660
+ // src/status.tsx
3661
+ import { Fragment as Fragment8, jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
2904
3662
  function KeepEmptyState({
2905
3663
  title,
2906
3664
  description,
@@ -2910,9 +3668,9 @@ function KeepEmptyState({
2910
3668
  className,
2911
3669
  ...rootProps
2912
3670
  }) {
2913
- const defaultTitle = useUiLabel("noItems").replace(/\.$/, "");
3671
+ const defaultTitle = useKeepEmptyState();
2914
3672
  const contentChildren = asChild && isValidElement6(children) ? void 0 : children;
2915
- const body = contentChildren ?? /* @__PURE__ */ jsxs14(Fragment7, { children: [
3673
+ const body = contentChildren ?? /* @__PURE__ */ jsxs15(Fragment8, { children: [
2916
3674
  /* @__PURE__ */ jsx20("h2", { children: title ?? defaultTitle }),
2917
3675
  description ? /* @__PURE__ */ jsx20("p", { children: description }) : null,
2918
3676
  action
@@ -2934,18 +3692,10 @@ function KeepStatus({
2934
3692
  className,
2935
3693
  ...rootProps
2936
3694
  }) {
2937
- const context = useKeepContext6();
3695
+ const view = useKeepStatus(status);
2938
3696
  const contentChildren = asChild && isValidElement6(children) ? void 0 : children;
2939
- const resolvedStatus = status ?? getDerivedStatus(context);
2940
- const defaultLabel = useUiLabel(getStatusLabelKey3(resolvedStatus));
2941
- const state = {
2942
- status: resolvedStatus,
2943
- error: context.error,
2944
- pendingCount: context.syncState.pendingCount,
2945
- items: context.items
2946
- };
2947
- const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? labels?.[resolvedStatus] ?? defaultLabel;
2948
- const role = rootProps.role ?? (resolvedStatus === "error" ? "alert" : "status");
3697
+ const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? labels?.[view.state.status] ?? view.defaultLabel;
3698
+ const role = rootProps.role ?? (view.state.status === "error" ? "alert" : "status");
2949
3699
  return renderRoot(
2950
3700
  asChild,
2951
3701
  isValidElement6(children) ? children : void 0,
@@ -2955,28 +3705,15 @@ function KeepStatus({
2955
3705
  "data-keepkit": "status",
2956
3706
  role,
2957
3707
  "aria-live": rootProps["aria-live"] ?? "polite",
2958
- "data-state": resolvedStatus,
2959
- "data-loading": resolvedStatus === "loading" || resolvedStatus === "saving" || resolvedStatus === "syncing" ? "true" : void 0
3708
+ "data-state": view.state.status,
3709
+ "data-loading": view.state.status === "loading" || view.state.status === "saving" || view.state.status === "syncing" ? "true" : void 0
2960
3710
  },
2961
3711
  body,
2962
3712
  "KeepStatus"
2963
3713
  );
2964
3714
  }
2965
3715
  function KeepAnnouncements({ messages, ...props }) {
2966
- const context = useKeepContext6();
2967
- const savedMessage = useUiLabel("savedMessage", messages?.save);
2968
- const removedMessage = useUiLabel("removedMessage", messages?.remove);
2969
- const noteSavedMessage = useUiLabel("noteSavedMessage", messages?.note);
2970
- const [message, setMessage] = useState10("");
2971
- const lastChangeRef = useRef4(void 0);
2972
- useEffect6(() => {
2973
- const change = context.lastChange;
2974
- if (!change || change === lastChangeRef.current) return;
2975
- lastChangeRef.current = change;
2976
- if (change.action === "save") setMessage(savedMessage);
2977
- else if (change.action === "remove" || change.action === "removeBatch") setMessage(removedMessage);
2978
- else if (change.action === "updateNote") setMessage(noteSavedMessage);
2979
- }, [context.lastChange, noteSavedMessage, removedMessage, savedMessage]);
3716
+ const message = useKeepAnnouncements(messages);
2980
3717
  return /* @__PURE__ */ jsx20(
2981
3718
  "div",
2982
3719
  {
@@ -2991,26 +3728,22 @@ function KeepAnnouncements({ messages, ...props }) {
2991
3728
  );
2992
3729
  }
2993
3730
  var KeepAnnouncer = KeepAnnouncements;
2994
- function getDerivedStatus(context) {
2995
- if (context.error) return "error";
2996
- if (context.syncState.status === "pending" || context.syncState.status === "syncing") return "syncing";
2997
- if (context.isMutating) return "saving";
2998
- if (context.isLoading && !context.isHydrated) return "loading";
2999
- if (context.isHydrated && context.items.length === 0) return "empty";
3000
- return "idle";
3001
- }
3002
- function getStatusLabelKey3(status) {
3003
- if (status === "empty") return "noItems";
3004
- if (status === "loading") return "loadingItems";
3005
- if (status === "error") return "error";
3006
- if (status === "saving") return "saving";
3007
- if (status === "syncing") return "syncing";
3008
- return "saved";
3009
- }
3010
3731
 
3011
3732
  // src/theme.tsx
3012
3733
  import { cloneElement as cloneElement2, isValidElement as isValidElement7 } from "react";
3013
3734
  import { jsx as jsx21 } from "react/jsx-runtime";
3735
+ var keepThemeNames = [
3736
+ "default",
3737
+ "ocean",
3738
+ "forest",
3739
+ "sunset",
3740
+ "lavender",
3741
+ "compact",
3742
+ "minimal",
3743
+ "rounded",
3744
+ "high-contrast",
3745
+ "dark"
3746
+ ];
3014
3747
  function KeepThemeProvider({
3015
3748
  children,
3016
3749
  theme = "default",
@@ -3065,7 +3798,7 @@ import { createAuthenticatedSyncKit } from "@keepkit/core/core";
3065
3798
  import {
3066
3799
  KeepErrorBoundary as KeepErrorBoundary3,
3067
3800
  KeepProvider,
3068
- useKeepContext as useKeepContext7,
3801
+ useKeepContext as useKeepContext8,
3069
3802
  useKeepItem as useKeepItem5,
3070
3803
  useKeepList as useKeepList5,
3071
3804
  useKeepShortcut
@@ -3080,11 +3813,12 @@ import {
3080
3813
  LocalStorageSyncQueueAdapter,
3081
3814
  SyncStorageAdapter
3082
3815
  } from "@keepkit/core/storage";
3083
- import { jsx as jsx22, jsxs as jsxs15 } from "react/jsx-runtime";
3816
+ import { jsx as jsx22, jsxs as jsxs16 } from "react/jsx-runtime";
3084
3817
  function KeepKitProvider({
3085
3818
  labels,
3086
3819
  locale,
3087
3820
  labelResolver,
3821
+ onFeedback,
3088
3822
  theme,
3089
3823
  mode,
3090
3824
  density,
@@ -3099,7 +3833,7 @@ function KeepKitProvider({
3099
3833
  children,
3100
3834
  ...providerProps
3101
3835
  }) {
3102
- return /* @__PURE__ */ jsx22(KeepUiProvider, { labels, locale, labelResolver, children: /* @__PURE__ */ jsx22(
3836
+ return /* @__PURE__ */ jsx22(KeepUiProvider, { labels, locale, labelResolver, onFeedback, children: /* @__PURE__ */ jsx22(
3103
3837
  KeepThemeProvider,
3104
3838
  {
3105
3839
  theme,
@@ -3113,7 +3847,8 @@ function KeepKitProvider({
3113
3847
  className: themeClassName,
3114
3848
  style: themeStyle,
3115
3849
  asChild: themeAsChild,
3116
- children: /* @__PURE__ */ jsxs15(CoreKeepProvider, { ...providerProps, children: [
3850
+ children: /* @__PURE__ */ jsxs16(CoreKeepProvider, { ...providerProps, children: [
3851
+ /* @__PURE__ */ jsx22(KeepSyncFeedbackObserver, {}),
3117
3852
  children,
3118
3853
  /* @__PURE__ */ jsx22(KeepAnnouncements, {})
3119
3854
  ] })
@@ -3125,6 +3860,7 @@ function createKeepKit(options = {}) {
3125
3860
  labels,
3126
3861
  locale,
3127
3862
  labelResolver,
3863
+ onFeedback,
3128
3864
  theme,
3129
3865
  mode,
3130
3866
  density,
@@ -3148,6 +3884,7 @@ function createKeepKit(options = {}) {
3148
3884
  labels,
3149
3885
  locale,
3150
3886
  labelResolver,
3887
+ onFeedback,
3151
3888
  theme,
3152
3889
  mode,
3153
3890
  density,
@@ -3193,6 +3930,7 @@ export {
3193
3930
  KeepEmptyState,
3194
3931
  KeepErrorBoundary3 as KeepErrorBoundary,
3195
3932
  KeepItemCard,
3933
+ KeepItemCardSkeleton,
3196
3934
  KeepItemCheckbox,
3197
3935
  KeepItemStatusBadge,
3198
3936
  KeepKitProvider,
@@ -3223,11 +3961,13 @@ export {
3223
3961
  createStorageAdapter,
3224
3962
  getKeepLocaleLabels,
3225
3963
  isAllSelected,
3964
+ keepThemeNames,
3226
3965
  toggleSelectAll,
3227
- useKeepContext7 as useKeepContext,
3966
+ useKeepContext8 as useKeepContext,
3228
3967
  useKeepItem5 as useKeepItem,
3229
3968
  useKeepList5 as useKeepList,
3230
3969
  useKeepShortcut,
3970
+ useKeepToastFeedback,
3231
3971
  useKeepUiLabels,
3232
3972
  useKeepUrlSync
3233
3973
  };