@keepkit/ui 0.13.0 → 0.15.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,7 +6,7 @@ import {
6
6
  createKeepKit as createCoreKeepKit
7
7
  } from "@keepkit/core/react";
8
8
 
9
- // src/KeepBackup.tsx
9
+ // src/hooks/useKeepBackup.ts
10
10
  import { useKeepContext } from "@keepkit/core/react";
11
11
  import { useRef, useState } from "react";
12
12
 
@@ -55,6 +55,9 @@ var DE_LABELS = {
55
55
  currentPage: "Aktuelle Seite",
56
56
  searchResults: "Aktuelle Suchergebnisse",
57
57
  allItems: "Alle gespeicherten Elemente",
58
+ localVersion: "Lokal",
59
+ remoteVersion: "Server",
60
+ updatedAt: "Aktualisiert",
58
61
  statusAvailable: "Verf\xFCgbar",
59
62
  noteSavedMessage: "Notiz gespeichert.",
60
63
  exportData: "JSON exportieren",
@@ -128,6 +131,9 @@ var EN_LABELS = {
128
131
  currentPage: "Current page",
129
132
  searchResults: "Current search results",
130
133
  allItems: "All saved items",
134
+ localVersion: "Local",
135
+ remoteVersion: "Remote",
136
+ updatedAt: "Updated",
131
137
  statusAvailable: "Available",
132
138
  noteSavedMessage: "Note saved.",
133
139
  exportData: "Export JSON",
@@ -201,6 +207,9 @@ var ES_LABELS = {
201
207
  currentPage: "P\xE1gina actual",
202
208
  searchResults: "Resultados de b\xFAsqueda actuales",
203
209
  allItems: "Todos los elementos guardados",
210
+ localVersion: "Local",
211
+ remoteVersion: "Servidor",
212
+ updatedAt: "Actualizado",
204
213
  statusAvailable: "Disponible",
205
214
  noteSavedMessage: "Nota guardada.",
206
215
  exportData: "Exportar JSON",
@@ -274,6 +283,9 @@ var FIL_LABELS = {
274
283
  currentPage: "Kasalukuyang pahina",
275
284
  searchResults: "Kasalukuyang resulta ng paghahanap",
276
285
  allItems: "Lahat ng naka-save na item",
286
+ localVersion: "Lokal",
287
+ remoteVersion: "Remote",
288
+ updatedAt: "Na-update",
277
289
  statusAvailable: "Available",
278
290
  noteSavedMessage: "Na-save ang tala.",
279
291
  exportData: "I-export ang JSON",
@@ -347,6 +359,9 @@ var FR_LABELS = {
347
359
  currentPage: "Page actuelle",
348
360
  searchResults: "R\xE9sultats de recherche actuels",
349
361
  allItems: "Tous les \xE9l\xE9ments enregistr\xE9s",
362
+ localVersion: "Local",
363
+ remoteVersion: "Distant",
364
+ updatedAt: "Mis \xE0 jour",
350
365
  statusAvailable: "Disponible",
351
366
  noteSavedMessage: "Note enregistr\xE9e.",
352
367
  exportData: "Exporter JSON",
@@ -420,6 +435,9 @@ var ID_LABELS = {
420
435
  currentPage: "Halaman saat ini",
421
436
  searchResults: "Hasil pencarian saat ini",
422
437
  allItems: "Semua item tersimpan",
438
+ localVersion: "Lokal",
439
+ remoteVersion: "Server",
440
+ updatedAt: "Diperbarui",
423
441
  statusAvailable: "Tersedia",
424
442
  noteSavedMessage: "Catatan tersimpan.",
425
443
  exportData: "Ekspor JSON",
@@ -493,6 +511,9 @@ var IT_LABELS = {
493
511
  currentPage: "Pagina corrente",
494
512
  searchResults: "Risultati di ricerca correnti",
495
513
  allItems: "Tutti gli elementi salvati",
514
+ localVersion: "Locale",
515
+ remoteVersion: "Server",
516
+ updatedAt: "Aggiornato",
496
517
  statusAvailable: "Disponibile",
497
518
  noteSavedMessage: "Nota salvata.",
498
519
  exportData: "Esporta JSON",
@@ -566,6 +587,9 @@ var JA_LABELS = {
566
587
  currentPage: "\u8868\u793A\u4E2D\u306E\u30DA\u30FC\u30B8",
567
588
  searchResults: "\u73FE\u5728\u306E\u691C\u7D22\u7D50\u679C\u5168\u4F53",
568
589
  allItems: "\u3059\u3079\u3066\u306E\u4FDD\u5B58\u30A2\u30A4\u30C6\u30E0",
590
+ localVersion: "\u30ED\u30FC\u30AB\u30EB",
591
+ remoteVersion: "\u30EA\u30E2\u30FC\u30C8",
592
+ updatedAt: "\u66F4\u65B0\u65E5\u6642",
569
593
  statusAvailable: "\u5229\u7528\u53EF\u80FD",
570
594
  noteSavedMessage: "\u30E1\u30E2\u3092\u4FDD\u5B58\u3057\u307E\u3057\u305F\u3002",
571
595
  exportData: "JSON\u3092\u30A8\u30AF\u30B9\u30DD\u30FC\u30C8",
@@ -639,6 +663,9 @@ var KO_LABELS = {
639
663
  currentPage: "\uD604\uC7AC \uD398\uC774\uC9C0",
640
664
  searchResults: "\uD604\uC7AC \uAC80\uC0C9 \uACB0\uACFC \uC804\uCCB4",
641
665
  allItems: "\uBAA8\uB4E0 \uC800\uC7A5 \uD56D\uBAA9",
666
+ localVersion: "\uB85C\uCEEC",
667
+ remoteVersion: "\uC6D0\uACA9",
668
+ updatedAt: "\uC5C5\uB370\uC774\uD2B8 \uC2DC\uAC04",
642
669
  statusAvailable: "\uC0AC\uC6A9 \uAC00\uB2A5",
643
670
  noteSavedMessage: "\uBA54\uBAA8\uB97C \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4.",
644
671
  exportData: "JSON \uB0B4\uBCF4\uB0B4\uAE30",
@@ -712,6 +739,9 @@ var MS_LABELS = {
712
739
  currentPage: "Halaman semasa",
713
740
  searchResults: "Hasil carian semasa",
714
741
  allItems: "Semua item yang disimpan",
742
+ localVersion: "Tempatan",
743
+ remoteVersion: "Pelayan",
744
+ updatedAt: "Dikemas kini",
715
745
  statusAvailable: "Tersedia",
716
746
  noteSavedMessage: "Nota disimpan.",
717
747
  exportData: "Eksport JSON",
@@ -785,6 +815,9 @@ var PT_BR_LABELS = {
785
815
  currentPage: "P\xE1gina atual",
786
816
  searchResults: "Resultados da pesquisa atual",
787
817
  allItems: "Todos os itens salvos",
818
+ localVersion: "Local",
819
+ remoteVersion: "Servidor",
820
+ updatedAt: "Atualizado",
788
821
  statusAvailable: "Dispon\xEDvel",
789
822
  noteSavedMessage: "Nota salva.",
790
823
  exportData: "Exportar JSON",
@@ -858,6 +891,9 @@ var RU_LABELS = {
858
891
  currentPage: "\u0422\u0435\u043A\u0443\u0449\u0430\u044F \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0430",
859
892
  searchResults: "\u0422\u0435\u043A\u0443\u0449\u0438\u0435 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u044B \u043F\u043E\u0438\u0441\u043A\u0430",
860
893
  allItems: "\u0412\u0441\u0435 \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D\u043D\u044B\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B",
894
+ localVersion: "\u041B\u043E\u043A\u0430\u043B\u044C\u043D\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F",
895
+ remoteVersion: "\u0421\u0435\u0440\u0432\u0435\u0440\u043D\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F",
896
+ updatedAt: "\u041E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u043E",
861
897
  statusAvailable: "\u0414\u043E\u0441\u0442\u0443\u043F\u0435\u043D",
862
898
  noteSavedMessage: "\u0417\u0430\u043C\u0435\u0442\u043A\u0430 \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0430.",
863
899
  exportData: "\u042D\u043A\u0441\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C JSON",
@@ -931,6 +967,9 @@ var TH_LABELS = {
931
967
  currentPage: "\u0E2B\u0E19\u0E49\u0E32\u0E1B\u0E31\u0E08\u0E08\u0E38\u0E1A\u0E31\u0E19",
932
968
  searchResults: "\u0E1C\u0E25\u0E01\u0E32\u0E23\u0E04\u0E49\u0E19\u0E2B\u0E32\u0E1B\u0E31\u0E08\u0E08\u0E38\u0E1A\u0E31\u0E19",
933
969
  allItems: "\u0E23\u0E32\u0E22\u0E01\u0E32\u0E23\u0E17\u0E35\u0E48\u0E1A\u0E31\u0E19\u0E17\u0E36\u0E01\u0E17\u0E31\u0E49\u0E07\u0E2B\u0E21\u0E14",
970
+ localVersion: "\u0E43\u0E19\u0E40\u0E04\u0E23\u0E37\u0E48\u0E2D\u0E07",
971
+ remoteVersion: "\u0E23\u0E30\u0E22\u0E30\u0E44\u0E01\u0E25",
972
+ updatedAt: "\u0E2D\u0E31\u0E1B\u0E40\u0E14\u0E15\u0E40\u0E21\u0E37\u0E48\u0E2D",
934
973
  statusAvailable: "\u0E1E\u0E23\u0E49\u0E2D\u0E21\u0E43\u0E0A\u0E49\u0E07\u0E32\u0E19",
935
974
  noteSavedMessage: "\u0E1A\u0E31\u0E19\u0E17\u0E36\u0E01\u0E22\u0E48\u0E2D\u0E41\u0E25\u0E49\u0E27",
936
975
  exportData: "\u0E2A\u0E48\u0E07\u0E2D\u0E2D\u0E01 JSON",
@@ -1004,6 +1043,9 @@ var VI_LABELS = {
1004
1043
  currentPage: "Trang hi\u1EC7n t\u1EA1i",
1005
1044
  searchResults: "K\u1EBFt qu\u1EA3 t\xECm ki\u1EBFm hi\u1EC7n t\u1EA1i",
1006
1045
  allItems: "T\u1EA5t c\u1EA3 m\u1EE5c \u0111\xE3 l\u01B0u",
1046
+ localVersion: "C\u1EE5c b\u1ED9",
1047
+ remoteVersion: "M\xE1y ch\u1EE7",
1048
+ updatedAt: "\u0110\xE3 c\u1EADp nh\u1EADt",
1007
1049
  statusAvailable: "Kh\u1EA3 d\u1EE5ng",
1008
1050
  noteSavedMessage: "\u0110\xE3 l\u01B0u ghi ch\xFA.",
1009
1051
  exportData: "Xu\u1EA5t JSON",
@@ -1077,6 +1119,9 @@ var ZH_HANS_LABELS = {
1077
1119
  currentPage: "\u5F53\u524D\u9875",
1078
1120
  searchResults: "\u5F53\u524D\u5168\u90E8\u641C\u7D22\u7ED3\u679C",
1079
1121
  allItems: "\u5168\u90E8\u5DF2\u4FDD\u5B58\u9879\u76EE",
1122
+ localVersion: "\u672C\u5730",
1123
+ remoteVersion: "\u8FDC\u7A0B",
1124
+ updatedAt: "\u66F4\u65B0\u65F6\u95F4",
1080
1125
  statusAvailable: "\u53EF\u7528",
1081
1126
  noteSavedMessage: "\u5907\u6CE8\u5DF2\u4FDD\u5B58\u3002",
1082
1127
  exportData: "\u5BFC\u51FAJSON",
@@ -1150,6 +1195,9 @@ var ZH_HANT_LABELS = {
1150
1195
  currentPage: "\u76EE\u524D\u9801\u9762",
1151
1196
  searchResults: "\u76EE\u524D\u5168\u90E8\u641C\u5C0B\u7D50\u679C",
1152
1197
  allItems: "\u6240\u6709\u5DF2\u5132\u5B58\u9805\u76EE",
1198
+ localVersion: "\u672C\u6A5F",
1199
+ remoteVersion: "\u9060\u7AEF",
1200
+ updatedAt: "\u66F4\u65B0\u6642\u9593",
1153
1201
  statusAvailable: "\u53EF\u7528",
1154
1202
  noteSavedMessage: "\u5099\u8A3B\u5DF2\u5132\u5B58\u3002",
1155
1203
  exportData: "\u532F\u51FAJSON",
@@ -1261,28 +1309,14 @@ function useUiLabel(key, override) {
1261
1309
  return override ?? context.customLabels?.[key] ?? context.labelResolver?.(key, { locale: context.locale }) ?? context.labels[key];
1262
1310
  }
1263
1311
 
1264
- // src/KeepBackup.tsx
1265
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
1266
- function KeepBackup({
1267
- filename = "keepkit-backup.json",
1268
- onExport,
1269
- onImported,
1270
- ...props
1271
- }) {
1312
+ // src/hooks/useKeepBackup.ts
1313
+ function useKeepBackup({ filename, onExport, onImported }) {
1272
1314
  const context = useKeepContext();
1273
- const exportLabel = useUiLabel("exportData");
1274
- const importLabel = useUiLabel("importData");
1275
- const importModeLabel = useUiLabel("importMode");
1276
- const mergeLabel = useUiLabel("merge");
1277
- const replaceLabel = useUiLabel("replace");
1278
- const importedCountLabel = useUiLabel("importedCount");
1279
- const failedCountLabel = useUiLabel("failedCount");
1280
- const quotaErrorLabel = useUiLabel("storageQuotaError");
1281
1315
  const inputRef = useRef(null);
1282
1316
  const [mode, setMode] = useState("merge");
1283
1317
  const [result, setResult] = useState();
1284
1318
  const [error, setError] = useState();
1285
- async function handleExport() {
1319
+ async function exportBackup() {
1286
1320
  setError(void 0);
1287
1321
  try {
1288
1322
  const data = await context.exportBackup();
@@ -1298,7 +1332,7 @@ function KeepBackup({
1298
1332
  setError(cause);
1299
1333
  }
1300
1334
  }
1301
- async function handleImport(event) {
1335
+ async function importBackup(event) {
1302
1336
  const file = event.currentTarget.files?.[0];
1303
1337
  event.currentTarget.value = "";
1304
1338
  if (!file) return;
@@ -1312,43 +1346,93 @@ function KeepBackup({
1312
1346
  setError(cause);
1313
1347
  }
1314
1348
  }
1349
+ return {
1350
+ inputRef,
1351
+ mode,
1352
+ setMode,
1353
+ result,
1354
+ error,
1355
+ isMutating: context.isMutating,
1356
+ exportBackup,
1357
+ importBackup,
1358
+ openFilePicker: () => inputRef.current?.click(),
1359
+ labels: {
1360
+ export: useUiLabel("exportData"),
1361
+ import: useUiLabel("importData"),
1362
+ importMode: useUiLabel("importMode"),
1363
+ merge: useUiLabel("merge"),
1364
+ replace: useUiLabel("replace"),
1365
+ importedCount: useUiLabel("importedCount"),
1366
+ failedCount: useUiLabel("failedCount"),
1367
+ quotaError: useUiLabel("storageQuotaError")
1368
+ }
1369
+ };
1370
+ }
1371
+
1372
+ // src/KeepBackup.tsx
1373
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
1374
+ function KeepBackup({
1375
+ filename = "keepkit-backup.json",
1376
+ onExport,
1377
+ onImported,
1378
+ ...props
1379
+ }) {
1380
+ const view = useKeepBackup({ filename, onExport, onImported });
1315
1381
  return /* @__PURE__ */ jsxs(
1316
1382
  "section",
1317
1383
  {
1318
1384
  ...props,
1319
1385
  "data-keepkit": "backup",
1320
- "data-state": error ? "error" : result ? "complete" : "idle",
1321
- "data-loading": context.isMutating ? "true" : void 0,
1386
+ "data-state": view.error ? "error" : view.result ? "complete" : "idle",
1387
+ "data-loading": view.isMutating ? "true" : void 0,
1322
1388
  children: [
1323
- /* @__PURE__ */ jsx2("button", { type: "button", onClick: () => void handleExport(), disabled: context.isMutating, children: exportLabel }),
1389
+ /* @__PURE__ */ jsx2(
1390
+ "button",
1391
+ {
1392
+ type: "button",
1393
+ "data-keep-action": "export-backup",
1394
+ onClick: () => void view.exportBackup(),
1395
+ disabled: view.isMutating,
1396
+ children: view.labels.export
1397
+ }
1398
+ ),
1324
1399
  /* @__PURE__ */ jsxs("label", { children: [
1325
- importModeLabel,
1326
- /* @__PURE__ */ jsxs("select", { value: mode, onChange: (event) => setMode(event.currentTarget.value), children: [
1327
- /* @__PURE__ */ jsx2("option", { value: "merge", children: mergeLabel }),
1328
- /* @__PURE__ */ jsx2("option", { value: "replace", children: replaceLabel })
1329
- ] })
1400
+ view.labels.importMode,
1401
+ /* @__PURE__ */ jsxs(
1402
+ "select",
1403
+ {
1404
+ "data-keep-action": "select-import-mode",
1405
+ value: view.mode,
1406
+ onChange: (event) => view.setMode(event.currentTarget.value),
1407
+ children: [
1408
+ /* @__PURE__ */ jsx2("option", { value: "merge", children: view.labels.merge }),
1409
+ /* @__PURE__ */ jsx2("option", { value: "replace", children: view.labels.replace })
1410
+ ]
1411
+ }
1412
+ )
1330
1413
  ] }),
1331
- /* @__PURE__ */ jsx2("button", { type: "button", onClick: () => inputRef.current?.click(), disabled: context.isMutating, children: importLabel }),
1414
+ /* @__PURE__ */ jsx2("button", { type: "button", "data-keep-action": "import-backup", onClick: view.openFilePicker, disabled: view.isMutating, children: view.labels.import }),
1332
1415
  /* @__PURE__ */ jsx2(
1333
1416
  "input",
1334
1417
  {
1335
- ref: inputRef,
1418
+ ref: view.inputRef,
1336
1419
  type: "file",
1420
+ "data-keep-action": "select-backup-file",
1337
1421
  accept: "application/json,.json",
1338
- "aria-label": importLabel,
1339
- onChange: (event) => void handleImport(event)
1422
+ "aria-label": view.labels.import,
1423
+ onChange: (event) => void view.importBackup(event)
1340
1424
  }
1341
1425
  ),
1342
- result ? /* @__PURE__ */ jsxs("p", { role: "status", children: [
1343
- result.imported,
1426
+ view.result ? /* @__PURE__ */ jsxs("p", { role: "status", children: [
1427
+ view.result.imported,
1344
1428
  " ",
1345
- importedCountLabel,
1429
+ view.labels.importedCount,
1346
1430
  "; ",
1347
- result.failed,
1431
+ view.result.failed,
1348
1432
  " ",
1349
- failedCountLabel
1433
+ view.labels.failedCount
1350
1434
  ] }) : null,
1351
- error ? /* @__PURE__ */ jsx2("p", { role: "alert", children: isQuotaError(error) ? quotaErrorLabel : getErrorMessage(error) }) : null
1435
+ view.error ? /* @__PURE__ */ jsx2("p", { role: "alert", children: isQuotaError(view.error) ? view.labels.quotaError : getErrorMessage(view.error) }) : null
1352
1436
  ]
1353
1437
  }
1354
1438
  );
@@ -1362,48 +1446,16 @@ function getErrorMessage(error) {
1362
1446
  return error instanceof Error ? error.message : "Something went wrong.";
1363
1447
  }
1364
1448
 
1365
- // src/KeepBulkActions.tsx
1449
+ // src/hooks/useKeepBulkActions.ts
1366
1450
  import { useKeepList } from "@keepkit/core/react";
1367
1451
  import { useState as useState2 } from "react";
1368
1452
 
1369
- // src/KeepItemCheckbox.tsx
1370
- import { jsx as jsx3 } from "react/jsx-runtime";
1371
- function KeepItemCheckbox({
1372
- item,
1373
- checked = false,
1374
- label,
1375
- onCheckedChange,
1376
- "aria-label": ariaLabel,
1377
- ...props
1378
- }) {
1379
- const itemLabel = getItemLabel(item) ?? item.id;
1380
- const accessibleLabel = ariaLabel ?? (typeof label === "string" ? label : itemLabel);
1381
- return /* @__PURE__ */ jsx3(
1382
- "input",
1383
- {
1384
- ...props,
1385
- type: "checkbox",
1386
- checked,
1387
- "data-keepkit": "item-checkbox",
1388
- "data-state": checked ? "checked" : "unchecked",
1389
- "data-disabled": props.disabled ? "true" : void 0,
1390
- "aria-label": accessibleLabel,
1391
- onChange: (event) => onCheckedChange?.(event.currentTarget.checked)
1392
- }
1393
- );
1394
- }
1395
- function getItemLabel(item) {
1396
- if (typeof item.meta !== "object" || item.meta === null || !("title" in item.meta)) return void 0;
1397
- const title = item.meta.title;
1398
- return typeof title === "string" && title.trim() ? title.trim() : void 0;
1399
- }
1400
-
1401
1453
  // src/shared.tsx
1402
1454
  import {
1403
1455
  cloneElement,
1404
1456
  isValidElement
1405
1457
  } from "react";
1406
- import { jsx as jsx4 } from "react/jsx-runtime";
1458
+ import { jsx as jsx3 } from "react/jsx-runtime";
1407
1459
  function toKeepButtonItem(item) {
1408
1460
  return {
1409
1461
  id: item.id,
@@ -1432,11 +1484,10 @@ function renderRoot(asChild, child, props, body, componentName) {
1432
1484
  if (!isValidElement(child)) throw new Error(`${componentName} with asChild requires a single React element child.`);
1433
1485
  return cloneElement(child, { ...props, children: body });
1434
1486
  }
1435
- return /* @__PURE__ */ jsx4("div", { ...props, children: body });
1487
+ return /* @__PURE__ */ jsx3("div", { ...props, children: body });
1436
1488
  }
1437
1489
 
1438
- // src/KeepBulkActions.tsx
1439
- import { Fragment, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
1490
+ // src/hooks/useKeepBulkActions.ts
1440
1491
  function isAllSelected(items, selectedIds) {
1441
1492
  if (items.length === 0) return false;
1442
1493
  const selected = new Set(selectedIds);
@@ -1451,42 +1502,30 @@ function toggleSelectAll(items, selectedIds) {
1451
1502
  }
1452
1503
  return nextIds;
1453
1504
  }
1454
- function KeepBulkActions({
1455
- query,
1456
- selectedIds: controlledSelectedIds,
1457
- defaultSelectedIds = [],
1458
- onSelectedIdsChange,
1459
- renderItem,
1460
- onCompleted,
1461
- selectionScope: controlledScope,
1462
- onSelectionScopeChange,
1463
- render,
1464
- children,
1465
- ...props
1466
- }) {
1467
- const selectItemsLabel = useUiLabel("selectItems");
1468
- const selectedCountLabel = useUiLabel("selectedCount");
1469
- const deleteSelectedLabel = useUiLabel("deleteSelected");
1470
- const tagsLabel = useUiLabel("tagsToApply");
1471
- const applyTagsLabel = useUiLabel("applyTags");
1472
- const selectionScopeLabel = useUiLabel("selectionScope");
1473
- const currentPageLabel = useUiLabel("currentPage");
1474
- const searchResultsLabel = useUiLabel("searchResults");
1475
- const allItemsLabel = useUiLabel("allItems");
1505
+ function useKeepBulkActions(options) {
1506
+ const {
1507
+ query,
1508
+ controlledSelectedIds,
1509
+ defaultSelectedIds,
1510
+ onSelectedIdsChange,
1511
+ onCompleted,
1512
+ controlledScope,
1513
+ onSelectionScopeChange
1514
+ } = options;
1476
1515
  const list = useKeepList(query);
1477
1516
  const queryList = useKeepList(query ? { ...query, pagination: void 0 } : { pagination: void 0 });
1478
1517
  const allList = useKeepList({ pagination: void 0 });
1479
1518
  const [uncontrolledScope, setUncontrolledScope] = useState2(controlledScope ?? "page");
1480
1519
  const selectionScope = controlledScope ?? uncontrolledScope;
1481
- const setSelectionScope = (scope) => {
1482
- if (controlledScope === void 0) setUncontrolledScope(scope);
1483
- onSelectionScopeChange?.(scope);
1484
- };
1485
1520
  const targetItems = selectionScope === "page" ? list.items : selectionScope === "query" ? queryList.items : allList.items;
1486
1521
  const [uncontrolledSelectedIds, setUncontrolledSelectedIds] = useState2(defaultSelectedIds);
1487
1522
  const selectedIds = controlledSelectedIds ?? uncontrolledSelectedIds;
1488
1523
  const selected = new Set(selectedIds);
1489
1524
  const [tagsInput, setTagsInput] = useState2("");
1525
+ const setSelectionScope = (scope) => {
1526
+ if (controlledScope === void 0) setUncontrolledScope(scope);
1527
+ onSelectionScopeChange?.(scope);
1528
+ };
1490
1529
  const setSelectedIds = (ids) => {
1491
1530
  if (controlledSelectedIds === void 0) setUncontrolledSelectedIds(ids);
1492
1531
  onSelectedIdsChange?.(ids);
@@ -1523,54 +1562,156 @@ function KeepBulkActions({
1523
1562
  selectionScope,
1524
1563
  setSelectionScope
1525
1564
  };
1565
+ return {
1566
+ state,
1567
+ selected,
1568
+ isLoading: list.isLoading,
1569
+ labels: {
1570
+ selectItems: useUiLabel("selectItems"),
1571
+ selectedCount: useUiLabel("selectedCount"),
1572
+ deleteSelected: useUiLabel("deleteSelected"),
1573
+ tags: useUiLabel("tagsToApply"),
1574
+ applyTags: useUiLabel("applyTags"),
1575
+ selectionScope: useUiLabel("selectionScope"),
1576
+ currentPage: useUiLabel("currentPage"),
1577
+ searchResults: useUiLabel("searchResults"),
1578
+ allItems: useUiLabel("allItems")
1579
+ }
1580
+ };
1581
+ }
1582
+
1583
+ // src/KeepItemCheckbox.tsx
1584
+ import { jsx as jsx4 } from "react/jsx-runtime";
1585
+ function KeepItemCheckbox({
1586
+ item,
1587
+ checked = false,
1588
+ label,
1589
+ onCheckedChange,
1590
+ "aria-label": ariaLabel,
1591
+ ...props
1592
+ }) {
1593
+ const itemLabel = getItemLabel(item) ?? item.id;
1594
+ const accessibleLabel = ariaLabel ?? (typeof label === "string" ? label : itemLabel);
1595
+ return /* @__PURE__ */ jsx4(
1596
+ "input",
1597
+ {
1598
+ ...props,
1599
+ type: "checkbox",
1600
+ checked,
1601
+ "data-keepkit": "item-checkbox",
1602
+ "data-keep-action": "select-item",
1603
+ "data-state": checked ? "checked" : "unchecked",
1604
+ "data-disabled": props.disabled ? "true" : void 0,
1605
+ "aria-label": accessibleLabel,
1606
+ onChange: (event) => onCheckedChange?.(event.currentTarget.checked)
1607
+ }
1608
+ );
1609
+ }
1610
+ function getItemLabel(item) {
1611
+ if (typeof item.meta !== "object" || item.meta === null || !("title" in item.meta)) return void 0;
1612
+ const title = item.meta.title;
1613
+ return typeof title === "string" && title.trim() ? title.trim() : void 0;
1614
+ }
1615
+
1616
+ // src/KeepBulkActions.tsx
1617
+ import { Fragment, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
1618
+ function KeepBulkActions({
1619
+ query,
1620
+ selectedIds: controlledSelectedIds,
1621
+ defaultSelectedIds = [],
1622
+ onSelectedIdsChange,
1623
+ renderItem,
1624
+ onCompleted,
1625
+ selectionScope: controlledScope,
1626
+ onSelectionScopeChange,
1627
+ render,
1628
+ children,
1629
+ ...props
1630
+ }) {
1631
+ const { state, selected, isLoading, labels } = useKeepBulkActions({
1632
+ query,
1633
+ controlledSelectedIds,
1634
+ defaultSelectedIds,
1635
+ onSelectedIdsChange,
1636
+ onCompleted,
1637
+ controlledScope,
1638
+ onSelectionScopeChange
1639
+ });
1526
1640
  const body = render ? render(state) : typeof children === "function" ? children(state) : children ?? /* @__PURE__ */ jsxs2(Fragment, { children: [
1527
1641
  /* @__PURE__ */ jsxs2("fieldset", { children: [
1528
- /* @__PURE__ */ jsx5("legend", { children: selectItemsLabel }),
1642
+ /* @__PURE__ */ jsx5("legend", { children: labels.selectItems }),
1529
1643
  /* @__PURE__ */ jsxs2("label", { children: [
1530
- selectionScopeLabel,
1644
+ labels.selectionScope,
1531
1645
  /* @__PURE__ */ jsxs2(
1532
1646
  "select",
1533
1647
  {
1534
- value: selectionScope,
1535
- onChange: (event) => setSelectionScope(event.currentTarget.value),
1648
+ "data-keep-action": "select-scope",
1649
+ value: state.selectionScope,
1650
+ onChange: (event) => state.setSelectionScope(event.currentTarget.value),
1536
1651
  children: [
1537
- /* @__PURE__ */ jsx5("option", { value: "page", children: currentPageLabel }),
1538
- /* @__PURE__ */ jsx5("option", { value: "query", children: searchResultsLabel }),
1539
- /* @__PURE__ */ jsx5("option", { value: "all", children: allItemsLabel })
1652
+ /* @__PURE__ */ jsx5("option", { value: "page", children: labels.currentPage }),
1653
+ /* @__PURE__ */ jsx5("option", { value: "query", children: labels.searchResults }),
1654
+ /* @__PURE__ */ jsx5("option", { value: "all", children: labels.allItems })
1540
1655
  ]
1541
1656
  }
1542
1657
  )
1543
1658
  ] }),
1544
1659
  /* @__PURE__ */ jsxs2("label", { children: [
1545
- /* @__PURE__ */ jsx5("input", { type: "checkbox", checked: allSelected, onChange: toggleAll, "aria-label": selectItemsLabel }),
1546
- selectedIds.length,
1660
+ /* @__PURE__ */ jsx5(
1661
+ "input",
1662
+ {
1663
+ type: "checkbox",
1664
+ "data-keep-action": "select-all",
1665
+ checked: state.allSelected,
1666
+ onChange: state.toggleAll,
1667
+ "aria-label": labels.selectItems
1668
+ }
1669
+ ),
1670
+ state.selectedIds.length,
1547
1671
  " ",
1548
- selectedCountLabel
1672
+ labels.selectedCount
1549
1673
  ] }),
1550
- targetItems.map((item) => /* @__PURE__ */ jsxs2("span", { children: [
1674
+ state.items.map((item) => /* @__PURE__ */ jsxs2("span", { children: [
1551
1675
  /* @__PURE__ */ jsx5(
1552
1676
  KeepItemCheckbox,
1553
1677
  {
1554
1678
  item,
1555
1679
  checked: selected.has(item.id),
1556
- onCheckedChange: () => toggle(item.id)
1680
+ onCheckedChange: () => state.toggle(item.id)
1557
1681
  }
1558
1682
  ),
1559
1683
  renderItem ? renderItem(item, selected.has(item.id)) : getMetaTitle(item.meta) ?? item.id
1560
1684
  ] }, item.id))
1561
1685
  ] }),
1562
- /* @__PURE__ */ jsx5("button", { type: "button", onClick: () => void remove(), disabled: selectedIds.length === 0 || list.isMutating, children: deleteSelectedLabel }),
1686
+ /* @__PURE__ */ jsx5(
1687
+ "button",
1688
+ {
1689
+ type: "button",
1690
+ "data-keep-action": "delete-selected",
1691
+ onClick: () => void state.remove(),
1692
+ disabled: state.selectedIds.length === 0 || state.isMutating,
1693
+ children: labels.deleteSelected
1694
+ }
1695
+ ),
1563
1696
  /* @__PURE__ */ jsxs2("label", { children: [
1564
- tagsLabel,
1565
- /* @__PURE__ */ jsx5("input", { value: tagsInput, onChange: (event) => setTagsInput(event.currentTarget.value) })
1697
+ labels.tags,
1698
+ /* @__PURE__ */ jsx5(
1699
+ "input",
1700
+ {
1701
+ "data-keep-action": "edit-tags",
1702
+ value: state.tagsInput,
1703
+ onChange: (event) => state.setTagsInput(event.currentTarget.value)
1704
+ }
1705
+ )
1566
1706
  ] }),
1567
1707
  /* @__PURE__ */ jsx5(
1568
1708
  "button",
1569
1709
  {
1570
1710
  type: "button",
1571
- onClick: () => void updateTags(),
1572
- disabled: selectedIds.length === 0 || list.isMutating,
1573
- children: applyTagsLabel
1711
+ "data-keep-action": "apply-tags",
1712
+ onClick: () => void state.updateTags(),
1713
+ disabled: state.selectedIds.length === 0 || state.isMutating,
1714
+ children: labels.applyTags
1574
1715
  }
1575
1716
  )
1576
1717
  ] });
@@ -1579,9 +1720,9 @@ function KeepBulkActions({
1579
1720
  {
1580
1721
  ...props,
1581
1722
  "data-keepkit": "bulk-actions",
1582
- "aria-busy": list.isMutating || props["aria-busy"],
1583
- "data-state": selectedIds.length > 0 ? "selected" : "idle",
1584
- "data-loading": list.isLoading || list.isMutating ? "true" : void 0,
1723
+ "aria-busy": state.isMutating || props["aria-busy"],
1724
+ "data-state": state.selectedIds.length > 0 ? "selected" : "idle",
1725
+ "data-loading": isLoading || state.isMutating ? "true" : void 0,
1585
1726
  children: body
1586
1727
  }
1587
1728
  );
@@ -1589,10 +1730,26 @@ function KeepBulkActions({
1589
1730
 
1590
1731
  // src/KeepButton.tsx
1591
1732
  import {
1592
- KeepButton as CoreKeepButton,
1593
- useKeepItem
1733
+ KeepButton as CoreKeepButton
1594
1734
  } from "@keepkit/core/react";
1595
1735
  import { createElement } from "react";
1736
+
1737
+ // src/hooks/useKeepButton.ts
1738
+ import { useKeepItem } from "@keepkit/core/react";
1739
+ function useKeepButton({ item, labels, icons, children }) {
1740
+ return {
1741
+ buttonState: useKeepItem(item),
1742
+ customStateLabel: labels?.loading !== void 0 || labels?.error !== void 0 || icons !== void 0 && children === void 0,
1743
+ labels: {
1744
+ save: useUiLabel("save", typeof labels?.unsaved === "string" ? labels.unsaved : void 0),
1745
+ saved: useUiLabel("saved", typeof labels?.saved === "string" ? labels.saved : void 0),
1746
+ loading: useUiLabel("loading", typeof labels?.loading === "string" ? labels.loading : void 0),
1747
+ error: useUiLabel("error", typeof labels?.error === "string" ? labels.error : void 0)
1748
+ }
1749
+ };
1750
+ }
1751
+
1752
+ // src/KeepButton.tsx
1596
1753
  import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
1597
1754
  function KeepButton({
1598
1755
  labels,
@@ -1602,16 +1759,11 @@ function KeepButton({
1602
1759
  iconClassName,
1603
1760
  ...props
1604
1761
  }) {
1605
- const saveLabel = useUiLabel("save", typeof labels?.unsaved === "string" ? labels.unsaved : void 0);
1606
- const savedLabel = useUiLabel("saved", typeof labels?.saved === "string" ? labels.saved : void 0);
1607
- const loadingLabel = useUiLabel("loading", typeof labels?.loading === "string" ? labels.loading : void 0);
1608
- const errorLabel = useUiLabel("error", typeof labels?.error === "string" ? labels.error : void 0);
1609
- const buttonState = useKeepItem(props.item);
1610
- const customStateLabel = labels?.loading !== void 0 || labels?.error !== void 0 || icons !== void 0 && props.children === void 0;
1762
+ const view = useKeepButton({ item: props.item, labels, icons, children: props.children });
1611
1763
  const getStateContent = (state) => {
1612
1764
  if (typeof props.children === "function") return props.children(state);
1613
1765
  if (props.children !== void 0) return props.children;
1614
- const label = state.error ? labels?.error ?? errorLabel : state.isMutating ? labels?.loading ?? loadingLabel : state.isSaved ? labels?.saved ?? savedLabel : labels?.unsaved ?? saveLabel;
1766
+ 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;
1615
1767
  if (!icons) return label;
1616
1768
  const icon = state.error ? icons.error : state.isMutating ? icons.loading : state.isSaved ? icons.remove ?? icons.saved : icons.save;
1617
1769
  return /* @__PURE__ */ jsxs3(Fragment2, { children: [
@@ -1622,14 +1774,16 @@ function KeepButton({
1622
1774
  const sharedProps = {
1623
1775
  ...props,
1624
1776
  "data-keepkit": "button",
1777
+ "data-keep-action": "toggle-save",
1778
+ "data-has-custom-icon": icons ? "true" : void 0,
1625
1779
  "data-icon-only": iconOnly ? "true" : void 0,
1626
- "aria-busy": props["aria-busy"] ?? (buttonState.isLoading || buttonState.isMutating),
1627
- savedLabel: labels?.saved ?? props.savedLabel ?? savedLabel,
1628
- unsavedLabel: labels?.unsaved ?? props.unsavedLabel ?? saveLabel,
1780
+ "aria-busy": props["aria-busy"] ?? (view.buttonState.isLoading || view.buttonState.isMutating),
1781
+ savedLabel: labels?.saved ?? props.savedLabel ?? view.labels.saved,
1782
+ unsavedLabel: labels?.unsaved ?? props.unsavedLabel ?? view.labels.save,
1629
1783
  savedAriaLabel: labels?.savedAriaLabel ?? props.savedAriaLabel,
1630
1784
  unsavedAriaLabel: labels?.unsavedAriaLabel ?? props.unsavedAriaLabel
1631
1785
  };
1632
- if (!customStateLabel) return /* @__PURE__ */ jsx6(CoreKeepButton, { ...sharedProps });
1786
+ if (!view.customStateLabel) return /* @__PURE__ */ jsx6(CoreKeepButton, { ...sharedProps });
1633
1787
  return /* @__PURE__ */ jsx6(CoreKeepButton, { ...sharedProps, children: (state) => /* @__PURE__ */ jsx6(Fragment2, { children: getStateContent(state) }) });
1634
1788
  }
1635
1789
  function renderIcon(icon, className) {
@@ -1638,114 +1792,416 @@ function renderIcon(icon, className) {
1638
1792
  }
1639
1793
 
1640
1794
  // src/KeepCollection.tsx
1641
- import { KeepErrorBoundary as KeepErrorBoundary2, useKeepList as useKeepList4 } from "@keepkit/core/react";
1642
- import { useMemo as useMemo3, useState as useState6 } from "react";
1795
+ import { KeepErrorBoundary as KeepErrorBoundary2 } from "@keepkit/core/react";
1643
1796
 
1644
- // src/KeepList.tsx
1645
- import {
1646
- KeepErrorBoundary,
1647
- useKeepList as useKeepList2
1648
- } from "@keepkit/core/react";
1649
- import { isValidElement as isValidElement3 } from "react";
1797
+ // src/hooks/useKeepCollection.ts
1798
+ import { useKeepList as useKeepList2 } from "@keepkit/core/react";
1799
+ import { useMemo as useMemo2, useState as useState3 } from "react";
1650
1800
 
1651
- // src/KeepItemCard.tsx
1652
- import { useKeepItem as useKeepItem2 } from "@keepkit/core/react";
1801
+ // src/url-sync.tsx
1653
1802
  import {
1654
- isValidElement as isValidElement2
1655
- } from "react";
1656
-
1657
- // src/KeepStaleNotice.tsx
1658
- import { useKeepContext as useKeepContext2 } from "@keepkit/core/react";
1659
- import { useState as useState3 } from "react";
1660
-
1661
- // src/KeepItemStatusBadge.tsx
1662
- import { jsx as jsx7 } from "react/jsx-runtime";
1663
- function KeepItemStatusBadge({ status = "available", label, className, ...props }) {
1664
- const resolvedStatus = status === "available" ? "available" : status;
1665
- const statusLabel = useUiLabel(getStatusLabelKey(resolvedStatus));
1666
- return /* @__PURE__ */ jsx7("span", { ...props, className, "data-keepkit": "status-badge", "data-status": resolvedStatus, children: label ?? statusLabel });
1667
- }
1668
- function getStatusLabelKey(status) {
1669
- switch (status) {
1670
- case "available":
1671
- return "statusAvailable";
1672
- case "expired":
1673
- return "statusExpired";
1674
- case "removed":
1675
- return "statusRemoved";
1676
- case "deleted":
1677
- return "statusDeleted";
1678
- case "private":
1679
- case "unknown":
1680
- return status === "private" ? "statusPrivate" : "statusUnknown";
1681
- }
1682
- }
1683
-
1684
- // src/KeepStaleNotice.tsx
1685
- import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
1686
- function KeepStaleNotice({
1687
- item,
1688
- onRetry,
1689
- onRemoved,
1690
- retryLabel,
1691
- removeLabel,
1692
- children,
1693
- className,
1694
- ...props
1695
- }) {
1696
- const context = useKeepContext2();
1697
- const retryText = useUiLabel("retry");
1698
- const removeText = useUiLabel("removeFromList");
1699
- const errorText = useUiLabel("error");
1700
- const [isRetrying, setIsRetrying] = useState3(false);
1701
- const [error, setError] = useState3(null);
1702
- const status = item.status && item.status !== "available" ? item.status : "unknown";
1703
- async function handleRetry() {
1704
- setError(null);
1705
- setIsRetrying(true);
1706
- try {
1707
- if (onRetry) await onRetry(item);
1708
- else await context.revalidateItems();
1709
- } catch (cause) {
1710
- setError(cause);
1711
- } finally {
1712
- setIsRetrying(false);
1713
- }
1714
- }
1715
- async function handleRemove() {
1716
- try {
1717
- await context.removeItemWithUndo(item.id);
1718
- onRemoved?.(item);
1719
- } catch (cause) {
1720
- setError(cause);
1803
+ DEFAULT_KEEP_URL_PARAMS,
1804
+ decodeKeepListQuery,
1805
+ encodeKeepListQuery
1806
+ } from "@keepkit/core/core";
1807
+ import { useEffect, useRef as useRef2 } from "react";
1808
+ function createNextPagesRouterAdapter(router) {
1809
+ const getUrl = () => router.asPath ?? (typeof window === "undefined" ? "/" : window.location.href);
1810
+ return {
1811
+ getUrl,
1812
+ subscribe: router.events ? (listener) => {
1813
+ router.events?.on("routeChangeComplete", listener);
1814
+ return () => router.events?.off("routeChangeComplete", listener);
1815
+ } : void 0,
1816
+ navigate: (url, mode) => {
1817
+ void router[mode](url, void 0, { shallow: true });
1721
1818
  }
1722
- }
1723
- return /* @__PURE__ */ jsxs4("aside", { ...props, className, "data-keepkit": "stale-notice", "data-state": error ? "error" : "stale", children: [
1724
- /* @__PURE__ */ jsx8(KeepItemStatusBadge, { status }),
1725
- children ?? (item.statusReason ? /* @__PURE__ */ jsx8("p", { children: item.statusReason }) : null),
1726
- /* @__PURE__ */ jsxs4("div", { children: [
1727
- /* @__PURE__ */ jsx8("button", { type: "button", onClick: () => void handleRetry(), disabled: isRetrying || context.isMutating, children: retryLabel ?? retryText }),
1728
- /* @__PURE__ */ jsx8("button", { type: "button", onClick: () => void handleRemove(), disabled: context.isMutating, children: removeLabel ?? removeText })
1729
- ] }),
1730
- error ? /* @__PURE__ */ jsx8("p", { role: "alert", children: error instanceof Error ? error.message : errorText }) : null
1731
- ] });
1819
+ };
1732
1820
  }
1733
- function KeepPruneStaleButton({
1734
- statuses = ["expired", "removed", "deleted", "private", "unknown"],
1735
- label,
1736
- onPruned,
1737
- children,
1738
- disabled,
1739
- ...props
1740
- }) {
1741
- const context = useKeepContext2();
1742
- const defaultLabel = useUiLabel("pruneStale");
1743
- const staleIds = context.items.filter((item) => item.status && statuses.includes(item.status)).map((item) => item.id);
1744
- async function handlePrune() {
1745
- const ids = [...staleIds];
1746
- await context.removeItemsWithUndo(ids);
1747
- onPruned?.(ids);
1748
- }
1821
+ function useKeepUrlSync({
1822
+ enabled = true,
1823
+ query,
1824
+ onQueryChange,
1825
+ options = {},
1826
+ adapter: providedAdapter
1827
+ }) {
1828
+ const browserAdapterRef = useRef2(getBrowserAdapter());
1829
+ const adapter = providedAdapter ?? browserAdapterRef.current;
1830
+ const onQueryChangeRef = useRef2(onQueryChange);
1831
+ onQueryChangeRef.current = onQueryChange;
1832
+ const skipWriteRef = useRef2(true);
1833
+ const params = options.params;
1834
+ useEffect(() => {
1835
+ if (!enabled) return;
1836
+ const read = () => {
1837
+ const url = adapter.getUrl();
1838
+ const decoded = decodeKeepListQuery(url, { params });
1839
+ skipWriteRef.current = true;
1840
+ onQueryChangeRef.current((previousQuery) => ({
1841
+ ...previousQuery,
1842
+ ...decoded.search ? { search: decoded.search } : { search: void 0 },
1843
+ ...decoded.tags ? { tags: decoded.tags } : { tags: void 0 },
1844
+ ...decoded.sort ? { sort: decoded.sort } : {},
1845
+ ...decoded.pagination ? { pagination: { ...previousQuery.pagination, ...decoded.pagination } } : {}
1846
+ }));
1847
+ };
1848
+ read();
1849
+ return adapter.subscribe?.(read);
1850
+ }, [adapter, enabled, params]);
1851
+ useEffect(() => {
1852
+ if (!enabled) return;
1853
+ if (skipWriteRef.current) {
1854
+ skipWriteRef.current = false;
1855
+ return;
1856
+ }
1857
+ const currentUrl = new URL(adapter.getUrl(), "http://keepkit.invalid");
1858
+ const urlParams = { ...DEFAULT_KEEP_URL_PARAMS, ...params };
1859
+ for (const key of Object.values(urlParams)) currentUrl.searchParams.delete(key);
1860
+ const nextParams = encodeKeepListQuery(query, { params });
1861
+ nextParams.forEach((value, key) => {
1862
+ currentUrl.searchParams.append(key, value);
1863
+ });
1864
+ const nextUrl = `${currentUrl.pathname}${currentUrl.search}${currentUrl.hash}`;
1865
+ adapter.navigate(nextUrl, options.history ?? "push");
1866
+ }, [adapter, enabled, options.history, params, query]);
1867
+ }
1868
+ function getBrowserAdapter() {
1869
+ return {
1870
+ getUrl: () => typeof window === "undefined" ? "/" : window.location.href,
1871
+ subscribe: (listener) => {
1872
+ if (typeof window === "undefined") return () => void 0;
1873
+ window.addEventListener("popstate", listener);
1874
+ return () => window.removeEventListener("popstate", listener);
1875
+ },
1876
+ navigate: (url, mode) => {
1877
+ if (typeof window === "undefined") return;
1878
+ window.history[mode === "push" ? "pushState" : "replaceState"]({}, "", url);
1879
+ }
1880
+ };
1881
+ }
1882
+
1883
+ // src/hooks/useKeepCollection.ts
1884
+ function useKeepCollection({
1885
+ query,
1886
+ pageSize,
1887
+ urlSync,
1888
+ urlAdapter,
1889
+ features
1890
+ }) {
1891
+ const enabled = {
1892
+ search: true,
1893
+ sort: true,
1894
+ pagination: true,
1895
+ tagFilter: false,
1896
+ bulkActions: false,
1897
+ ...features
1898
+ };
1899
+ const [searchValue, setSearchValue] = useState3(query.search?.query ?? "");
1900
+ const [sort, setSort] = useState3(query.sort ?? { by: "updatedAt", direction: "desc" });
1901
+ const [tag, setTag] = useState3(query.tags?.[0]);
1902
+ const [page, setPage] = useState3(query.pagination?.page ?? 1);
1903
+ const resolvedPageSize = query.pagination?.pageSize ?? pageSize;
1904
+ const resolvedQuery = useMemo2(
1905
+ () => ({
1906
+ ...query,
1907
+ search: enabled.search ? { ...query.search, query: searchValue } : query.search,
1908
+ sort: enabled.sort ? sort : query.sort,
1909
+ tags: tag ? [.../* @__PURE__ */ new Set([...query.tags ?? [], tag])] : query.tags,
1910
+ pagination: enabled.pagination ? { ...query.pagination, page, pageSize: resolvedPageSize } : query.pagination
1911
+ }),
1912
+ [enabled.pagination, enabled.search, enabled.sort, page, query, resolvedPageSize, searchValue, sort, tag]
1913
+ );
1914
+ useKeepUrlSync({
1915
+ enabled: Boolean(urlSync),
1916
+ query: resolvedQuery,
1917
+ onQueryChange: (nextOrUpdater) => {
1918
+ const next = typeof nextOrUpdater === "function" ? nextOrUpdater(resolvedQuery) : nextOrUpdater;
1919
+ setSearchValue(next.search?.query ?? "");
1920
+ setSort(next.sort ?? { by: "updatedAt", direction: "desc" });
1921
+ setTag(next.tags?.[0]);
1922
+ setPage(next.pagination?.page ?? 1);
1923
+ },
1924
+ options: typeof urlSync === "object" ? urlSync : {},
1925
+ adapter: urlAdapter
1926
+ });
1927
+ const list = useKeepList2(resolvedQuery);
1928
+ return {
1929
+ enabled,
1930
+ searchValue,
1931
+ sortValue: sortToValue(sort),
1932
+ tag,
1933
+ resolvedPageSize,
1934
+ resolvedQuery,
1935
+ list,
1936
+ setSearchValue: (value) => {
1937
+ setSearchValue(value);
1938
+ setPage(1);
1939
+ },
1940
+ setSortValue: (_value, nextSort) => {
1941
+ setSort(nextSort);
1942
+ setPage(1);
1943
+ },
1944
+ setTag: (value) => {
1945
+ setTag(value);
1946
+ setPage(1);
1947
+ },
1948
+ setPage
1949
+ };
1950
+ }
1951
+
1952
+ // src/KeepList.tsx
1953
+ import { KeepErrorBoundary } from "@keepkit/core/react";
1954
+ import { isValidElement as isValidElement3 } from "react";
1955
+
1956
+ // src/hooks/useKeepListView.ts
1957
+ import { useKeepList as useKeepList3 } from "@keepkit/core/react";
1958
+ function useKeepListView(query) {
1959
+ return {
1960
+ state: useKeepList3(query),
1961
+ labels: {
1962
+ loading: useUiLabel("loadingItems"),
1963
+ empty: useUiLabel("noItems"),
1964
+ error: useUiLabel("errorItems")
1965
+ }
1966
+ };
1967
+ }
1968
+
1969
+ // src/KeepItemCard.tsx
1970
+ import {
1971
+ isValidElement as isValidElement2
1972
+ } from "react";
1973
+
1974
+ // src/hooks/useKeepItemCard.ts
1975
+ import { useKeepItem as useKeepItem2 } from "@keepkit/core/react";
1976
+ function useKeepItemCard(options) {
1977
+ const {
1978
+ item,
1979
+ title,
1980
+ getTitle,
1981
+ getImageProps,
1982
+ href: hrefOption,
1983
+ linkTargetAttribute,
1984
+ linkRel,
1985
+ onRemoveError,
1986
+ onRemoved
1987
+ } = options;
1988
+ const itemState = useKeepItem2(item);
1989
+ const resolvedTitle = typeof title === "function" ? title(item) : title ?? getTitle?.(item) ?? getMetaTitle(item.meta) ?? item.id;
1990
+ const imageProps = getImageProps?.(item, resolvedTitle);
1991
+ const href = typeof hrefOption === "function" ? hrefOption(item) : hrefOption;
1992
+ const isAvailable = item.status === void 0 || item.status === "available";
1993
+ const isExternalLink = href ? /^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(href) : false;
1994
+ const statusLabelKey = item.status && item.status !== "available" ? getStatusLabelKey(item.status) : "statusUnknown";
1995
+ const unavailableLabel = useUiLabel(statusLabelKey);
1996
+ const state = {
1997
+ item,
1998
+ isSaved: itemState.isSaved,
1999
+ isMutating: itemState.isMutating,
2000
+ error: itemState.error,
2001
+ remove: itemState.remove,
2002
+ status: item.status
2003
+ };
2004
+ return {
2005
+ itemState,
2006
+ state,
2007
+ resolvedTitle,
2008
+ imageProps,
2009
+ href,
2010
+ isAvailable,
2011
+ displayStatus: getDisplayStatus(item.status),
2012
+ resolvedLinkTarget: linkTargetAttribute ?? (isExternalLink ? "_blank" : void 0),
2013
+ resolvedLinkRel: linkRel ?? (isExternalLink ? "noreferrer" : void 0),
2014
+ statusLabel: item.status && item.status !== "available" ? unavailableLabel : void 0,
2015
+ remove: async () => {
2016
+ try {
2017
+ await itemState.remove();
2018
+ onRemoved?.(item);
2019
+ } catch (cause) {
2020
+ onRemoveError?.(cause);
2021
+ }
2022
+ },
2023
+ labels: {
2024
+ save: useUiLabel("save"),
2025
+ savedAt: useUiLabel("saved"),
2026
+ error: useUiLabel("error"),
2027
+ remove: useUiLabel("remove"),
2028
+ tags: useUiLabel("tags")
2029
+ }
2030
+ };
2031
+ }
2032
+ function getStatusLabelKey(status) {
2033
+ switch (status) {
2034
+ case "expired":
2035
+ return "statusExpired";
2036
+ case "removed":
2037
+ return "statusRemoved";
2038
+ case "deleted":
2039
+ return "statusDeleted";
2040
+ case "private":
2041
+ return "statusPrivate";
2042
+ default:
2043
+ return "statusUnknown";
2044
+ }
2045
+ }
2046
+ function getDisplayStatus(status) {
2047
+ if (status === void 0 || status === "available") return "available";
2048
+ if (status === "expired") return "expired";
2049
+ if (status === "removed") return "removed";
2050
+ return "restricted";
2051
+ }
2052
+
2053
+ // src/hooks/useKeepStaleNotice.ts
2054
+ import { useKeepContext as useKeepContext2 } from "@keepkit/core/react";
2055
+ import { useState as useState4 } from "react";
2056
+ function useKeepStaleNotice({ item, onRetry, onRemoved }) {
2057
+ const context = useKeepContext2();
2058
+ const [isRetrying, setIsRetrying] = useState4(false);
2059
+ const [error, setError] = useState4(null);
2060
+ async function retry() {
2061
+ setError(null);
2062
+ setIsRetrying(true);
2063
+ try {
2064
+ if (onRetry) await onRetry(item);
2065
+ else await context.revalidateItems();
2066
+ } catch (cause) {
2067
+ setError(cause);
2068
+ } finally {
2069
+ setIsRetrying(false);
2070
+ }
2071
+ }
2072
+ async function remove() {
2073
+ try {
2074
+ await context.removeItemWithUndo(item.id);
2075
+ onRemoved?.(item);
2076
+ } catch (cause) {
2077
+ setError(cause);
2078
+ }
2079
+ }
2080
+ return {
2081
+ status: item.status && item.status !== "available" ? item.status : "unknown",
2082
+ error,
2083
+ isRetrying,
2084
+ isMutating: context.isMutating,
2085
+ retry,
2086
+ remove,
2087
+ labels: {
2088
+ retry: useUiLabel("retry"),
2089
+ remove: useUiLabel("removeFromList"),
2090
+ error: useUiLabel("error")
2091
+ }
2092
+ };
2093
+ }
2094
+ function useKeepPruneStale({ statuses, onPruned }) {
2095
+ const context = useKeepContext2();
2096
+ const staleIds = context.items.filter((item) => item.status && statuses.includes(item.status)).map((item) => item.id);
2097
+ return {
2098
+ staleIds,
2099
+ isMutating: context.isMutating,
2100
+ label: useUiLabel("pruneStale"),
2101
+ prune: async () => {
2102
+ const ids = [...staleIds];
2103
+ await context.removeItemsWithUndo(ids);
2104
+ onPruned?.(ids);
2105
+ }
2106
+ };
2107
+ }
2108
+
2109
+ // src/hooks/useKeepItemStatusBadge.ts
2110
+ function useKeepItemStatusBadge(status) {
2111
+ return { resolvedStatus: getDisplayStatus2(status), statusLabel: useUiLabel(getStatusLabelKey2(status)) };
2112
+ }
2113
+ function getStatusLabelKey2(status) {
2114
+ switch (status) {
2115
+ case "available":
2116
+ return "statusAvailable";
2117
+ case "expired":
2118
+ return "statusExpired";
2119
+ case "removed":
2120
+ return "statusRemoved";
2121
+ case "deleted":
2122
+ return "statusDeleted";
2123
+ case "private":
2124
+ return "statusPrivate";
2125
+ case "unknown":
2126
+ return "statusUnknown";
2127
+ case "restricted":
2128
+ return "statusPrivate";
2129
+ }
2130
+ }
2131
+ function getDisplayStatus2(status) {
2132
+ if (status === "available") return "available";
2133
+ if (status === "expired") return "expired";
2134
+ if (status === "removed") return "removed";
2135
+ return "restricted";
2136
+ }
2137
+
2138
+ // src/KeepItemStatusBadge.tsx
2139
+ import { jsx as jsx7 } from "react/jsx-runtime";
2140
+ function KeepItemStatusBadge({ status = "available", label, className, ...props }) {
2141
+ const view = useKeepItemStatusBadge(status);
2142
+ return /* @__PURE__ */ jsx7(
2143
+ "span",
2144
+ {
2145
+ ...props,
2146
+ className,
2147
+ "data-keepkit": "status-badge",
2148
+ "data-status": status,
2149
+ "data-item-status": view.resolvedStatus,
2150
+ children: label ?? view.statusLabel
2151
+ }
2152
+ );
2153
+ }
2154
+
2155
+ // src/KeepStaleNotice.tsx
2156
+ import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
2157
+ function KeepStaleNotice({
2158
+ item,
2159
+ onRetry,
2160
+ onRemoved,
2161
+ retryLabel,
2162
+ removeLabel,
2163
+ children,
2164
+ className,
2165
+ ...props
2166
+ }) {
2167
+ const view = useKeepStaleNotice({ item, onRetry, onRemoved });
2168
+ return /* @__PURE__ */ jsxs4("aside", { ...props, className, "data-keepkit": "stale-notice", "data-state": view.error ? "error" : "stale", children: [
2169
+ /* @__PURE__ */ jsx8(KeepItemStatusBadge, { status: view.status }),
2170
+ children ?? (item.statusReason ? /* @__PURE__ */ jsx8("p", { children: item.statusReason }) : null),
2171
+ /* @__PURE__ */ jsxs4("div", { children: [
2172
+ /* @__PURE__ */ jsx8(
2173
+ "button",
2174
+ {
2175
+ type: "button",
2176
+ "data-keep-action": "retry-item",
2177
+ onClick: () => void view.retry(),
2178
+ disabled: view.isRetrying || view.isMutating,
2179
+ children: retryLabel ?? view.labels.retry
2180
+ }
2181
+ ),
2182
+ /* @__PURE__ */ jsx8(
2183
+ "button",
2184
+ {
2185
+ type: "button",
2186
+ "data-keep-action": "remove-item",
2187
+ onClick: () => void view.remove(),
2188
+ disabled: view.isMutating,
2189
+ children: removeLabel ?? view.labels.remove
2190
+ }
2191
+ )
2192
+ ] }),
2193
+ view.error ? /* @__PURE__ */ jsx8("p", { role: "alert", children: view.error instanceof Error ? view.error.message : view.labels.error }) : null
2194
+ ] });
2195
+ }
2196
+ function KeepPruneStaleButton({
2197
+ statuses = ["expired", "removed", "deleted", "private", "unknown"],
2198
+ label,
2199
+ onPruned,
2200
+ children,
2201
+ disabled,
2202
+ ...props
2203
+ }) {
2204
+ const view = useKeepPruneStale({ statuses, onPruned });
1749
2205
  return /* @__PURE__ */ jsx8(
1750
2206
  "button",
1751
2207
  {
@@ -1753,10 +2209,11 @@ function KeepPruneStaleButton({
1753
2209
  type: "button",
1754
2210
  className: props.className,
1755
2211
  "data-keepkit": "prune-stale",
1756
- "data-state": staleIds.length > 0 ? "available" : "empty",
1757
- disabled: staleIds.length === 0 || context.isMutating || disabled,
1758
- onClick: () => void handlePrune(),
1759
- children: children ?? label ?? `${defaultLabel} (${staleIds.length})`
2212
+ "data-keep-action": "prune-stale",
2213
+ "data-state": view.staleIds.length > 0 ? "available" : "empty",
2214
+ disabled: view.staleIds.length === 0 || view.isMutating || disabled,
2215
+ onClick: () => void view.prune(),
2216
+ children: children ?? label ?? `${view.label} (${view.staleIds.length})`
1760
2217
  }
1761
2218
  );
1762
2219
  }
@@ -1792,72 +2249,63 @@ function KeepItemCard({
1792
2249
  className,
1793
2250
  ...rootProps
1794
2251
  }) {
1795
- const saveActionLabel = useUiLabel("save");
1796
- const savedAtLabel = useUiLabel("saved");
1797
- const errorLabel = useUiLabel("error");
1798
- const removeActionLabel = useUiLabel("remove");
1799
- const itemState = useKeepItem2(item);
1800
- const contentChildren = asChild && isValidElement2(children) ? void 0 : children;
1801
- const state = {
2252
+ const view = useKeepItemCard({
1802
2253
  item,
1803
- isSaved: itemState.isSaved,
1804
- isMutating: itemState.isMutating,
1805
- error: itemState.error,
1806
- remove: itemState.remove,
1807
- status: item.status
1808
- };
1809
- const resolvedTitle = typeof title === "function" ? title(item) : title ?? getTitle?.(item) ?? getMetaTitle(item.meta) ?? item.id;
1810
- const imageProps = getImageProps?.(item, resolvedTitle);
1811
- const href = typeof hrefOption === "function" ? hrefOption(item) : hrefOption;
1812
- const isAvailable = item.status === void 0 || item.status === "available";
1813
- const statusLabelKey = item.status && item.status !== "available" ? getStatusLabelKey2(item.status) : "statusUnknown";
1814
- const unavailableLabel = useUiLabel(statusLabelKey);
1815
- const tagsLabel = useUiLabel("tags");
1816
- const statusLabel = item.status && item.status !== "available" ? unavailableLabel : void 0;
2254
+ title,
2255
+ getTitle,
2256
+ getImageProps,
2257
+ href: hrefOption,
2258
+ linkTargetAttribute,
2259
+ linkRel,
2260
+ onRemoveError,
2261
+ onRemoved
2262
+ });
2263
+ const contentChildren = asChild && isValidElement2(children) ? void 0 : children;
1817
2264
  function renderLink(content) {
1818
- if (!href || !isAvailable) return content;
2265
+ if (!view.href || !view.isAvailable) return content;
1819
2266
  const linkProps = {
1820
- href,
1821
- target: linkTargetAttribute,
1822
- rel: linkRel,
2267
+ href: view.href,
2268
+ target: view.resolvedLinkTarget,
2269
+ rel: view.resolvedLinkRel,
1823
2270
  onClick: (event) => onOpen?.(item, event),
1824
2271
  children: content
1825
2272
  };
1826
2273
  return LinkComponent ? /* @__PURE__ */ jsx9(LinkComponent, { ...linkProps }) : /* @__PURE__ */ jsx9("a", { ...linkProps });
1827
2274
  }
1828
- async function handleRemove() {
1829
- try {
1830
- await itemState.remove();
1831
- onRemoved?.(item);
1832
- } catch (error) {
1833
- onRemoveError?.(error);
1834
- }
1835
- }
1836
- const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? /* @__PURE__ */ jsxs5(Fragment3, { children: [
1837
- 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,
1838
- /* @__PURE__ */ jsx9("h3", { children: linkTarget === "title" ? renderLink(resolvedTitle) : resolvedTitle }),
2275
+ const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? /* @__PURE__ */ jsxs5(Fragment3, { children: [
2276
+ view.imageProps ? renderImage?.({ ...view.imageProps, alt: imageAlt ?? view.imageProps.alt }, item) ?? (ImageComponent ? /* @__PURE__ */ jsx9(ImageComponent, { ...view.imageProps, alt: imageAlt ?? view.imageProps.alt }) : /* @__PURE__ */ jsx9("img", { ...view.imageProps, alt: imageAlt ?? view.imageProps.alt })) : null,
2277
+ /* @__PURE__ */ jsx9("h3", { children: linkTarget === "title" ? view.isAvailable ? renderLink(view.resolvedTitle) : view.href ? /* @__PURE__ */ jsx9("span", { "aria-disabled": "true", "data-link-disabled": "true", children: view.resolvedTitle }) : view.resolvedTitle : view.resolvedTitle }),
1839
2278
  showSavedAt ? /* @__PURE__ */ jsxs5("div", { "data-card-meta": true, children: [
1840
2279
  /* @__PURE__ */ jsxs5("span", { children: [
1841
- savedAtLabel,
2280
+ view.labels.savedAt,
1842
2281
  ":"
1843
2282
  ] }),
1844
2283
  " ",
1845
2284
  /* @__PURE__ */ jsx9("time", { dateTime: new Date(item.savedAt).toISOString(), children: formatSavedAt(item.savedAt) })
1846
2285
  ] }) : null,
1847
- 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,
1848
- itemState.error ? /* @__PURE__ */ jsx9("p", { role: "alert", children: getErrorMessage2(itemState.error, errorLabel) }) : null,
1849
- statusLabel ? /* @__PURE__ */ jsx9(KeepStaleNotice, { item, onRetry, onRemoved }) : null,
1850
- showSaveButton && !statusLabel ? /* @__PURE__ */ jsx9(
2286
+ showTags && (item.tags?.length ?? 0) > 0 ? renderTags ? renderTags(item.tags ?? [], item) : /* @__PURE__ */ jsx9("ul", { "aria-label": view.labels.tags, children: item.tags?.map((tag) => /* @__PURE__ */ jsx9("li", { children: tag }, tag)) }) : null,
2287
+ view.itemState.error ? /* @__PURE__ */ jsx9("p", { role: "alert", children: getErrorMessage2(view.itemState.error, view.labels.error) }) : null,
2288
+ view.statusLabel ? /* @__PURE__ */ jsx9(KeepStaleNotice, { item, onRetry, onRemoved }) : null,
2289
+ showSaveButton && !view.statusLabel ? /* @__PURE__ */ jsx9(
1851
2290
  KeepButton,
1852
2291
  {
1853
2292
  item: toKeepButtonItem(item),
1854
2293
  labels: saveButtonLabels,
1855
- getAriaLabel: (buttonState) => `${buttonState.isSaved ? removeActionLabel : saveActionLabel} ${String(resolvedTitle)}`
2294
+ getAriaLabel: (buttonState) => `${buttonState.isSaved ? view.labels.remove : view.labels.save} ${String(view.resolvedTitle)}`
1856
2295
  }
1857
2296
  ) : null,
1858
- !statusLabel ? /* @__PURE__ */ jsx9("button", { type: "button", onClick: () => void handleRemove(), disabled: itemState.isMutating, children: removeLabel ?? removeActionLabel }) : null
2297
+ !view.statusLabel ? /* @__PURE__ */ jsx9(
2298
+ "button",
2299
+ {
2300
+ type: "button",
2301
+ "data-keep-action": "remove-item",
2302
+ onClick: () => void view.remove(),
2303
+ disabled: view.itemState.isMutating,
2304
+ children: removeLabel ?? view.labels.remove
2305
+ }
2306
+ ) : null
1859
2307
  ] });
1860
- const linkedBody = linkTarget === "card" ? renderLink(body) : body;
2308
+ const linkedBody = linkTarget === "card" && view.isAvailable ? renderLink(body) : body;
1861
2309
  return renderRoot(
1862
2310
  asChild,
1863
2311
  isValidElement2(children) ? children : void 0,
@@ -1865,29 +2313,17 @@ function KeepItemCard({
1865
2313
  ...rootProps,
1866
2314
  className,
1867
2315
  "data-keepkit": "card",
1868
- "aria-busy": itemState.isMutating || rootProps["aria-busy"],
1869
- "data-state": itemState.error ? "error" : itemState.isSaved ? "saved" : "unsaved",
2316
+ "aria-busy": view.itemState.isMutating || rootProps["aria-busy"],
2317
+ "aria-disabled": rootProps["aria-disabled"] ?? (!view.isAvailable ? "true" : void 0),
2318
+ "data-state": view.itemState.error ? "error" : view.itemState.isSaved ? "saved" : "unsaved",
1870
2319
  "data-status": item.status ?? "available",
1871
- "data-loading": itemState.isMutating ? "true" : void 0
2320
+ "data-item-status": view.displayStatus,
2321
+ "data-loading": view.itemState.isMutating ? "true" : void 0
1872
2322
  },
1873
2323
  linkedBody,
1874
2324
  "KeepItemCard"
1875
2325
  );
1876
2326
  }
1877
- function getStatusLabelKey2(status) {
1878
- switch (status) {
1879
- case "expired":
1880
- return "statusExpired";
1881
- case "removed":
1882
- return "statusRemoved";
1883
- case "deleted":
1884
- return "statusDeleted";
1885
- case "private":
1886
- return "statusPrivate";
1887
- default:
1888
- return "statusUnknown";
1889
- }
1890
- }
1891
2327
  function formatSavedAt(timestamp) {
1892
2328
  return new Date(timestamp).toISOString().slice(0, 10);
1893
2329
  }
@@ -1916,16 +2352,14 @@ function KeepListContent({
1916
2352
  className,
1917
2353
  ...rootProps
1918
2354
  }) {
1919
- const defaultLoading = useUiLabel("loadingItems");
1920
- const defaultEmpty = useUiLabel("noItems");
1921
- const defaultError = useUiLabel("errorItems");
1922
- const state = useKeepList2(query);
2355
+ const view = useKeepListView(query);
2356
+ const { state } = view;
1923
2357
  const body = getListBody(state, {
1924
2358
  children,
1925
2359
  renderItem,
1926
- loading: loading ?? defaultLoading,
1927
- empty: empty ?? defaultEmpty,
1928
- error: errorContent ?? defaultError,
2360
+ loading: loading ?? view.labels.loading,
2361
+ empty: empty ?? view.labels.empty,
2362
+ error: errorContent ?? view.labels.error,
1929
2363
  itemCardProps,
1930
2364
  layout
1931
2365
  });
@@ -1936,6 +2370,7 @@ function KeepListContent({
1936
2370
  ...rootProps,
1937
2371
  className,
1938
2372
  "data-keepkit": "list",
2373
+ "data-layout": layout,
1939
2374
  "aria-busy": state.isLoading || rootProps["aria-busy"],
1940
2375
  "data-state": getListState(state),
1941
2376
  "data-loading": state.isLoading ? "true" : void 0
@@ -1956,21 +2391,45 @@ function getListBody(state, options) {
1956
2391
  if (state.isHydrated && state.items.length === 0) return resolveContent(options.empty, state);
1957
2392
  if (typeof options.children === "function") return options.children(state);
1958
2393
  if (options.children !== void 0 && !isValidElement3(options.children)) return options.children;
1959
- return /* @__PURE__ */ jsx10(
1960
- "ul",
1961
- {
1962
- "data-layout": options.layout,
1963
- style: options.layout === "grid" ? { display: "grid", gap: "1rem", gridTemplateColumns: "repeat(auto-fit, minmax(16rem, 1fr))" } : options.layout === "compact" ? { display: "grid", gap: "0.5rem", gridTemplateColumns: "repeat(auto-fit, minmax(12rem, 1fr))" } : { display: "flex", flexDirection: "column", gap: "1rem" },
1964
- children: state.items.map(
1965
- (item) => options.renderItem ? options.renderItem(item, state) : /* @__PURE__ */ jsx10("li", { children: /* @__PURE__ */ jsx10(KeepItemCard, { item, ...options.itemCardProps }) }, item.id)
1966
- )
1967
- }
2394
+ return /* @__PURE__ */ jsx10("ul", { "data-layout": options.layout, children: state.items.map(
2395
+ (item) => options.renderItem ? options.renderItem(item, state) : /* @__PURE__ */ jsx10("li", { children: /* @__PURE__ */ jsx10(KeepItemCard, { item, ...options.itemCardProps }) }, item.id)
2396
+ ) });
2397
+ }
2398
+
2399
+ // src/KeepTagFilter.tsx
2400
+ import { isValidElement as isValidElement4 } from "react";
2401
+
2402
+ // src/hooks/useKeepTagFilter.ts
2403
+ import { useKeepList as useKeepList4 } from "@keepkit/core/react";
2404
+ import { useCallback, useMemo as useMemo3, useState as useState5 } from "react";
2405
+ function useKeepTagFilter(options) {
2406
+ const { query, controlledValue, defaultValue, onChange, onValueChange } = options;
2407
+ const [uncontrolledValue, setUncontrolledValue] = useState5(defaultValue);
2408
+ const resolvedValue = controlledValue ?? uncontrolledValue;
2409
+ const list = useKeepList4({
2410
+ ...query,
2411
+ tags: resolvedValue ? [...query?.tags ?? [], resolvedValue] : query?.tags
2412
+ });
2413
+ const select = useCallback(
2414
+ (tag) => {
2415
+ if (controlledValue === void 0) setUncontrolledValue(tag);
2416
+ onChange?.(tag);
2417
+ onValueChange?.(tag);
2418
+ },
2419
+ [controlledValue, onChange, onValueChange]
2420
+ );
2421
+ const state = useMemo3(
2422
+ () => ({ tags: list.tags, tagCounts: list.tagCounts, value: resolvedValue, select }),
2423
+ [list.tagCounts, list.tags, resolvedValue, select]
1968
2424
  );
2425
+ return {
2426
+ state,
2427
+ isLoading: list.isLoading,
2428
+ labels: { all: useUiLabel("allTags"), aria: useUiLabel("filterTags") }
2429
+ };
1969
2430
  }
1970
2431
 
1971
2432
  // src/KeepTagFilter.tsx
1972
- import { useKeepList as useKeepList3 } from "@keepkit/core/react";
1973
- import { isValidElement as isValidElement4, useCallback, useMemo as useMemo2, useState as useState4 } from "react";
1974
2433
  import { jsx as jsx11, jsxs as jsxs6 } from "react/jsx-runtime";
1975
2434
  function KeepTagFilter({
1976
2435
  query,
@@ -1987,38 +2446,38 @@ function KeepTagFilter({
1987
2446
  className,
1988
2447
  ...rootProps
1989
2448
  }) {
1990
- const uiAllLabel = useUiLabel("allTags");
1991
- const uiAriaLabel = useUiLabel("filterTags");
1992
- const [uncontrolledValue, setUncontrolledValue] = useState4(defaultValue);
1993
- const resolvedValue = controlledValue ?? uncontrolledValue;
1994
- const list = useKeepList3({
1995
- ...query,
1996
- tags: resolvedValue ? [...query?.tags ?? [], resolvedValue] : query?.tags
1997
- });
1998
- const select = useCallback(
1999
- (tag) => {
2000
- if (controlledValue === void 0) setUncontrolledValue(tag);
2001
- onChange?.(tag);
2002
- onValueChange?.(tag);
2003
- },
2004
- [controlledValue, onChange, onValueChange]
2005
- );
2006
- const state = useMemo2(
2007
- () => ({ tags: list.tags, tagCounts: list.tagCounts, value: resolvedValue, select }),
2008
- [list.tagCounts, list.tags, resolvedValue, select]
2009
- );
2449
+ const view = useKeepTagFilter({ query, controlledValue, defaultValue, onChange, onValueChange });
2010
2450
  const contentChildren = asChild && isValidElement4(children) ? void 0 : children;
2011
- const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? /* @__PURE__ */ jsxs6("fieldset", { children: [
2012
- /* @__PURE__ */ jsx11("legend", { children: ariaLabel ?? uiAriaLabel }),
2013
- /* @__PURE__ */ jsx11("button", { type: "button", "aria-pressed": resolvedValue === void 0, onClick: () => select(), children: allLabel ?? uiAllLabel }),
2014
- list.tags.map((tag) => /* @__PURE__ */ jsxs6("button", { type: "button", "aria-pressed": resolvedValue === tag, onClick: () => select(tag), children: [
2015
- renderTag ? renderTag(tag, list.tagCounts[tag] ?? 0, resolvedValue === tag) : tag,
2016
- /* @__PURE__ */ jsxs6("span", { children: [
2017
- " (",
2018
- list.tagCounts[tag] ?? 0,
2019
- ")"
2020
- ] })
2021
- ] }, tag))
2451
+ const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? /* @__PURE__ */ jsxs6("fieldset", { children: [
2452
+ /* @__PURE__ */ jsx11("legend", { children: ariaLabel ?? view.labels.aria }),
2453
+ /* @__PURE__ */ jsx11(
2454
+ "button",
2455
+ {
2456
+ type: "button",
2457
+ "data-keep-action": "filter-all-tags",
2458
+ "aria-pressed": view.state.value === void 0,
2459
+ onClick: () => view.state.select(),
2460
+ children: allLabel ?? view.labels.all
2461
+ }
2462
+ ),
2463
+ view.state.tags.map((tag) => /* @__PURE__ */ jsxs6(
2464
+ "button",
2465
+ {
2466
+ type: "button",
2467
+ "data-keep-action": "filter-tag",
2468
+ "aria-pressed": view.state.value === tag,
2469
+ onClick: () => view.state.select(tag),
2470
+ children: [
2471
+ renderTag ? renderTag(tag, view.state.tagCounts[tag] ?? 0, view.state.value === tag) : tag,
2472
+ /* @__PURE__ */ jsxs6("span", { children: [
2473
+ " (",
2474
+ view.state.tagCounts[tag] ?? 0,
2475
+ ")"
2476
+ ] })
2477
+ ]
2478
+ },
2479
+ tag
2480
+ ))
2022
2481
  ] });
2023
2482
  return renderRoot(
2024
2483
  asChild,
@@ -2027,16 +2486,87 @@ function KeepTagFilter({
2027
2486
  ...rootProps,
2028
2487
  className,
2029
2488
  "data-keepkit": "tag-filter",
2030
- "data-state": resolvedValue === void 0 ? "all" : "filtered",
2031
- "data-loading": list.isLoading ? "true" : void 0
2489
+ "data-state": view.state.value === void 0 ? "all" : "filtered",
2490
+ "data-loading": view.isLoading ? "true" : void 0
2032
2491
  },
2033
2492
  body,
2034
2493
  "KeepTagFilter"
2035
2494
  );
2036
2495
  }
2037
2496
 
2497
+ // src/hooks/useQueryControls.ts
2498
+ import { useEffect as useEffect2, useState as useState6 } from "react";
2499
+ function useKeepSearchInput(options) {
2500
+ const { controlledValue, defaultValue, debounceMs, onValueChange } = options;
2501
+ const [uncontrolledValue, setUncontrolledValue] = useState6(defaultValue);
2502
+ const value = controlledValue ?? uncontrolledValue;
2503
+ useEffect2(() => {
2504
+ if (!onValueChange) return;
2505
+ if (debounceMs <= 0) {
2506
+ onValueChange(value);
2507
+ return;
2508
+ }
2509
+ const timer = window.setTimeout(() => onValueChange(value), debounceMs);
2510
+ return () => window.clearTimeout(timer);
2511
+ }, [debounceMs, onValueChange, value]);
2512
+ return {
2513
+ value,
2514
+ label: useUiLabel("search"),
2515
+ change: (event) => {
2516
+ if (controlledValue === void 0) setUncontrolledValue(event.currentTarget.value);
2517
+ }
2518
+ };
2519
+ }
2520
+ function useKeepSortSelect(options) {
2521
+ const { controlledValue, defaultValue, onValueChange } = options;
2522
+ const [uncontrolledValue, setUncontrolledValue] = useState6(defaultValue);
2523
+ const value = controlledValue ?? uncontrolledValue;
2524
+ return {
2525
+ value,
2526
+ change: (event) => {
2527
+ const nextValue = event.currentTarget.value;
2528
+ if (controlledValue === void 0) setUncontrolledValue(nextValue);
2529
+ const [by, direction] = nextValue.split(":");
2530
+ onValueChange?.(nextValue, { by, direction });
2531
+ },
2532
+ labels: {
2533
+ sort: useUiLabel("sort"),
2534
+ updatedNewest: useUiLabel("updatedNewest"),
2535
+ updatedOldest: useUiLabel("updatedOldest"),
2536
+ savedNewest: useUiLabel("savedNewest"),
2537
+ savedOldest: useUiLabel("savedOldest")
2538
+ }
2539
+ };
2540
+ }
2541
+ function useKeepPagination(options) {
2542
+ const { totalCount, pageSize, page, maxPageButtons, onPageChange } = options;
2543
+ const pageCount = Math.max(1, Math.ceil(totalCount / Math.max(1, pageSize)));
2544
+ const currentPage = Math.min(Math.max(1, page), pageCount);
2545
+ const goToPage = (nextPage) => {
2546
+ const next = Math.min(Math.max(1, nextPage), pageCount);
2547
+ onPageChange?.(next, (next - 1) * pageSize);
2548
+ };
2549
+ return {
2550
+ pageCount,
2551
+ currentPage,
2552
+ goToPage,
2553
+ visiblePages: getVisiblePages(currentPage, pageCount, Math.max(1, maxPageButtons)),
2554
+ labels: {
2555
+ previous: useUiLabel("previousPage"),
2556
+ next: useUiLabel("nextPage"),
2557
+ page: useUiLabel("page"),
2558
+ pagination: useUiLabel("pagination")
2559
+ }
2560
+ };
2561
+ }
2562
+ function getVisiblePages(currentPage, pageCount, maxPageButtons) {
2563
+ if (pageCount <= maxPageButtons) return Array.from({ length: pageCount }, (_, index) => index + 1);
2564
+ const half = Math.floor(maxPageButtons / 2);
2565
+ const start = Math.min(Math.max(1, currentPage - half), pageCount - maxPageButtons + 1);
2566
+ return Array.from({ length: maxPageButtons }, (_, index) => start + index);
2567
+ }
2568
+
2038
2569
  // src/query-controls.tsx
2039
- import { useEffect, useState as useState5 } from "react";
2040
2570
  import { Fragment as Fragment4, jsx as jsx12, jsxs as jsxs7 } from "react/jsx-runtime";
2041
2571
  function KeepSearchInput({
2042
2572
  value: controlledValue,
@@ -2047,33 +2577,20 @@ function KeepSearchInput({
2047
2577
  placeholder,
2048
2578
  ...props
2049
2579
  }) {
2050
- const label = useUiLabel("search");
2051
- const [uncontrolledValue, setUncontrolledValue] = useState5(defaultValue);
2052
- const value = controlledValue ?? uncontrolledValue;
2053
- useEffect(() => {
2054
- if (!onValueChange) return;
2055
- if (debounceMs <= 0) {
2056
- onValueChange(value);
2057
- return;
2058
- }
2059
- const timer = window.setTimeout(() => onValueChange(value), debounceMs);
2060
- return () => window.clearTimeout(timer);
2061
- }, [debounceMs, onValueChange, value]);
2580
+ const view = useKeepSearchInput({ controlledValue, defaultValue, debounceMs, onValueChange });
2062
2581
  return /* @__PURE__ */ jsx12(
2063
2582
  "input",
2064
2583
  {
2065
2584
  ...props,
2066
2585
  "data-keepkit": "search-input",
2586
+ "data-keep-action": "search",
2067
2587
  type: "search",
2068
- value,
2069
- "data-state": value ? "active" : "idle",
2588
+ value: view.value,
2589
+ "data-state": view.value ? "active" : "idle",
2070
2590
  "data-disabled": props.disabled ? "true" : void 0,
2071
- "aria-label": ariaLabel ?? label,
2072
- placeholder: placeholder ?? label,
2073
- onChange: (event) => {
2074
- const nextValue = event.currentTarget.value;
2075
- if (controlledValue === void 0) setUncontrolledValue(nextValue);
2076
- }
2591
+ "aria-label": ariaLabel ?? view.label,
2592
+ placeholder: placeholder ?? view.label,
2593
+ onChange: view.change
2077
2594
  }
2078
2595
  );
2079
2596
  }
@@ -2085,34 +2602,24 @@ function KeepSortSelect({
2085
2602
  children,
2086
2603
  ...props
2087
2604
  }) {
2088
- const label = useUiLabel("sort");
2089
- const updatedNewestLabel = useUiLabel("updatedNewest");
2090
- const updatedOldestLabel = useUiLabel("updatedOldest");
2091
- const savedNewestLabel = useUiLabel("savedNewest");
2092
- const savedOldestLabel = useUiLabel("savedOldest");
2093
- const [uncontrolledValue, setUncontrolledValue] = useState5(defaultValue);
2094
- const value = controlledValue ?? uncontrolledValue;
2605
+ const view = useKeepSortSelect({ controlledValue, defaultValue, onValueChange });
2095
2606
  const options = children ?? /* @__PURE__ */ jsxs7(Fragment4, { children: [
2096
- /* @__PURE__ */ jsx12("option", { value: "updatedAt:desc", children: updatedNewestLabel }),
2097
- /* @__PURE__ */ jsx12("option", { value: "updatedAt:asc", children: updatedOldestLabel }),
2098
- /* @__PURE__ */ jsx12("option", { value: "savedAt:desc", children: savedNewestLabel }),
2099
- /* @__PURE__ */ jsx12("option", { value: "savedAt:asc", children: savedOldestLabel })
2607
+ /* @__PURE__ */ jsx12("option", { value: "updatedAt:desc", children: view.labels.updatedNewest }),
2608
+ /* @__PURE__ */ jsx12("option", { value: "updatedAt:asc", children: view.labels.updatedOldest }),
2609
+ /* @__PURE__ */ jsx12("option", { value: "savedAt:desc", children: view.labels.savedNewest }),
2610
+ /* @__PURE__ */ jsx12("option", { value: "savedAt:asc", children: view.labels.savedOldest })
2100
2611
  ] });
2101
2612
  return /* @__PURE__ */ jsx12(
2102
2613
  "select",
2103
2614
  {
2104
2615
  ...props,
2105
2616
  "data-keepkit": "sort-select",
2106
- value,
2617
+ "data-keep-action": "sort",
2618
+ value: view.value,
2107
2619
  "data-state": "selected",
2108
2620
  "data-disabled": props.disabled ? "true" : void 0,
2109
- "aria-label": ariaLabel ?? label,
2110
- onChange: (event) => {
2111
- const nextValue = event.currentTarget.value;
2112
- if (controlledValue === void 0) setUncontrolledValue(nextValue);
2113
- const [by, direction] = nextValue.split(":");
2114
- onValueChange?.(nextValue, { by, direction });
2115
- },
2621
+ "aria-label": ariaLabel ?? view.labels.sort,
2622
+ onChange: view.change,
2116
2623
  children: options
2117
2624
  }
2118
2625
  );
@@ -2126,128 +2633,50 @@ function KeepPagination({
2126
2633
  render,
2127
2634
  ...props
2128
2635
  }) {
2129
- const previousPageLabel = useUiLabel("previousPage");
2130
- const nextPageLabel = useUiLabel("nextPage");
2131
- const pageLabel = useUiLabel("page");
2132
- const paginationLabel = useUiLabel("pagination");
2133
- const pageCount = Math.max(1, Math.ceil(totalCount / Math.max(1, pageSize)));
2134
- const currentPage = Math.min(Math.max(1, page), pageCount);
2135
- const goToPage = (nextPage) => {
2136
- const next = Math.min(Math.max(1, nextPage), pageCount);
2137
- onPageChange?.(next, (next - 1) * pageSize);
2138
- };
2636
+ const view = useKeepPagination({ totalCount, pageSize, page, maxPageButtons, onPageChange });
2139
2637
  const navProps = {
2140
2638
  ...props,
2141
2639
  "data-keepkit": "pagination",
2142
- "aria-label": props["aria-label"] ?? paginationLabel,
2143
- "data-state": pageCount > 1 ? "active" : "idle"
2640
+ "aria-label": props["aria-label"] ?? view.labels.pagination,
2641
+ "data-state": view.pageCount > 1 ? "active" : "idle"
2144
2642
  };
2145
- if (render) return /* @__PURE__ */ jsx12("nav", { ...navProps, children: render({ page: currentPage, pageCount, goToPage }) });
2146
- const visiblePages = getVisiblePages(currentPage, pageCount, Math.max(1, maxPageButtons));
2643
+ if (render)
2644
+ return /* @__PURE__ */ jsx12("nav", { ...navProps, children: render({ page: view.currentPage, pageCount: view.pageCount, goToPage: view.goToPage }) });
2147
2645
  return /* @__PURE__ */ jsxs7("nav", { ...navProps, children: [
2148
- /* @__PURE__ */ jsx12("button", { type: "button", onClick: () => goToPage(currentPage - 1), disabled: currentPage <= 1, children: previousPageLabel }),
2149
- visiblePages.map((nextPage) => /* @__PURE__ */ jsx12(
2646
+ /* @__PURE__ */ jsx12(
2647
+ "button",
2648
+ {
2649
+ type: "button",
2650
+ "data-keep-action": "previous-page",
2651
+ onClick: () => view.goToPage(view.currentPage - 1),
2652
+ disabled: view.currentPage <= 1,
2653
+ children: view.labels.previous
2654
+ }
2655
+ ),
2656
+ view.visiblePages.map((nextPage) => /* @__PURE__ */ jsx12(
2150
2657
  "button",
2151
2658
  {
2152
2659
  type: "button",
2153
- "aria-current": nextPage === currentPage ? "page" : void 0,
2154
- "aria-label": `${pageLabel} ${nextPage}`,
2155
- onClick: () => goToPage(nextPage),
2660
+ "data-keep-action": "select-page",
2661
+ "aria-current": nextPage === view.currentPage ? "page" : void 0,
2662
+ "aria-label": `${view.labels.page} ${nextPage}`,
2663
+ onClick: () => view.goToPage(nextPage),
2156
2664
  children: nextPage
2157
2665
  },
2158
2666
  nextPage
2159
2667
  )),
2160
- /* @__PURE__ */ jsx12("button", { type: "button", onClick: () => goToPage(currentPage + 1), disabled: currentPage >= pageCount, children: nextPageLabel })
2668
+ /* @__PURE__ */ jsx12(
2669
+ "button",
2670
+ {
2671
+ type: "button",
2672
+ "data-keep-action": "next-page",
2673
+ onClick: () => view.goToPage(view.currentPage + 1),
2674
+ disabled: view.currentPage >= view.pageCount,
2675
+ children: view.labels.next
2676
+ }
2677
+ )
2161
2678
  ] });
2162
2679
  }
2163
- function getVisiblePages(currentPage, pageCount, maxPageButtons) {
2164
- if (pageCount <= maxPageButtons) return Array.from({ length: pageCount }, (_, index) => index + 1);
2165
- const half = Math.floor(maxPageButtons / 2);
2166
- const start = Math.min(Math.max(1, currentPage - half), pageCount - maxPageButtons + 1);
2167
- return Array.from({ length: maxPageButtons }, (_, index) => start + index);
2168
- }
2169
-
2170
- // src/url-sync.tsx
2171
- import {
2172
- DEFAULT_KEEP_URL_PARAMS,
2173
- decodeKeepListQuery,
2174
- encodeKeepListQuery
2175
- } from "@keepkit/core/core";
2176
- import { useEffect as useEffect2, useRef as useRef2 } from "react";
2177
- function createNextPagesRouterAdapter(router) {
2178
- const getUrl = () => router.asPath ?? (typeof window === "undefined" ? "/" : window.location.href);
2179
- return {
2180
- getUrl,
2181
- subscribe: router.events ? (listener) => {
2182
- router.events?.on("routeChangeComplete", listener);
2183
- return () => router.events?.off("routeChangeComplete", listener);
2184
- } : void 0,
2185
- navigate: (url, mode) => {
2186
- void router[mode](url, void 0, { shallow: true });
2187
- }
2188
- };
2189
- }
2190
- function useKeepUrlSync({
2191
- enabled = true,
2192
- query,
2193
- onQueryChange,
2194
- options = {},
2195
- adapter: providedAdapter
2196
- }) {
2197
- const browserAdapterRef = useRef2(getBrowserAdapter());
2198
- const adapter = providedAdapter ?? browserAdapterRef.current;
2199
- const onQueryChangeRef = useRef2(onQueryChange);
2200
- onQueryChangeRef.current = onQueryChange;
2201
- const skipWriteRef = useRef2(true);
2202
- const params = options.params;
2203
- useEffect2(() => {
2204
- if (!enabled) return;
2205
- const read = () => {
2206
- const url = adapter.getUrl();
2207
- const decoded = decodeKeepListQuery(url, { params });
2208
- skipWriteRef.current = true;
2209
- onQueryChangeRef.current((previousQuery) => ({
2210
- ...previousQuery,
2211
- ...decoded.search ? { search: decoded.search } : { search: void 0 },
2212
- ...decoded.tags ? { tags: decoded.tags } : { tags: void 0 },
2213
- ...decoded.sort ? { sort: decoded.sort } : {},
2214
- ...decoded.pagination ? { pagination: { ...previousQuery.pagination, ...decoded.pagination } } : {}
2215
- }));
2216
- };
2217
- read();
2218
- return adapter.subscribe?.(read);
2219
- }, [adapter, enabled, params]);
2220
- useEffect2(() => {
2221
- if (!enabled) return;
2222
- if (skipWriteRef.current) {
2223
- skipWriteRef.current = false;
2224
- return;
2225
- }
2226
- const currentUrl = new URL(adapter.getUrl(), "http://keepkit.invalid");
2227
- const urlParams = { ...DEFAULT_KEEP_URL_PARAMS, ...params };
2228
- for (const key of Object.values(urlParams)) currentUrl.searchParams.delete(key);
2229
- const nextParams = encodeKeepListQuery(query, { params });
2230
- nextParams.forEach((value, key) => {
2231
- currentUrl.searchParams.append(key, value);
2232
- });
2233
- const nextUrl = `${currentUrl.pathname}${currentUrl.search}${currentUrl.hash}`;
2234
- adapter.navigate(nextUrl, options.history ?? "push");
2235
- }, [adapter, enabled, options.history, params, query]);
2236
- }
2237
- function getBrowserAdapter() {
2238
- return {
2239
- getUrl: () => typeof window === "undefined" ? "/" : window.location.href,
2240
- subscribe: (listener) => {
2241
- if (typeof window === "undefined") return () => void 0;
2242
- window.addEventListener("popstate", listener);
2243
- return () => window.removeEventListener("popstate", listener);
2244
- },
2245
- navigate: (url, mode) => {
2246
- if (typeof window === "undefined") return;
2247
- window.history[mode === "push" ? "pushState" : "replaceState"]({}, "", url);
2248
- }
2249
- };
2250
- }
2251
2680
 
2252
2681
  // src/KeepCollection.tsx
2253
2682
  import { jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
@@ -2272,90 +2701,27 @@ function KeepCollectionContent({
2272
2701
  className,
2273
2702
  ...rootProps
2274
2703
  }) {
2275
- const enabled = {
2276
- search: true,
2277
- sort: true,
2278
- pagination: true,
2279
- tagFilter: false,
2280
- bulkActions: false,
2281
- ...features
2282
- };
2283
- const [searchValue, setSearchValue] = useState6(query.search?.query ?? "");
2284
- const [sort, setSort] = useState6(query.sort ?? { by: "updatedAt", direction: "desc" });
2285
- const [tag, setTag] = useState6(query.tags?.[0]);
2286
- const [page, setPage] = useState6(query.pagination?.page ?? 1);
2287
- const resolvedPageSize = query.pagination?.pageSize ?? pageSize;
2288
- const resolvedQuery = useMemo3(
2289
- () => ({
2290
- ...query,
2291
- search: enabled.search ? { ...query.search, query: searchValue } : query.search,
2292
- sort: enabled.sort ? sort : query.sort,
2293
- tags: tag ? [.../* @__PURE__ */ new Set([...query.tags ?? [], tag])] : query.tags,
2294
- pagination: enabled.pagination ? { ...query.pagination, page, pageSize: resolvedPageSize } : query.pagination
2295
- }),
2296
- [enabled.pagination, enabled.search, enabled.sort, page, query, resolvedPageSize, searchValue, sort, tag]
2297
- );
2298
- useKeepUrlSync({
2299
- enabled: Boolean(urlSync),
2300
- query: resolvedQuery,
2301
- onQueryChange: (nextOrUpdater) => {
2302
- const next = typeof nextOrUpdater === "function" ? nextOrUpdater(resolvedQuery) : nextOrUpdater;
2303
- setSearchValue(next.search?.query ?? "");
2304
- setSort(next.sort ?? { by: "updatedAt", direction: "desc" });
2305
- setTag(next.tags?.[0]);
2306
- setPage(next.pagination?.page ?? 1);
2307
- },
2308
- options: typeof urlSync === "object" ? urlSync : {},
2309
- adapter: urlAdapter
2310
- });
2311
- const list = useKeepList4(resolvedQuery);
2704
+ const view = useKeepCollection({ query, pageSize, urlSync, urlAdapter, features });
2312
2705
  return /* @__PURE__ */ jsxs8(
2313
2706
  "section",
2314
2707
  {
2315
2708
  ...rootProps,
2316
2709
  className,
2317
2710
  "data-keepkit": "collection",
2318
- "aria-busy": list.isLoading || list.isMutating || rootProps["aria-busy"],
2319
- "data-state": getCollectionState(list),
2320
- "data-loading": list.isLoading || list.isMutating ? "true" : void 0,
2711
+ "data-layout": layout,
2712
+ "aria-busy": view.list.isLoading || view.list.isMutating || rootProps["aria-busy"],
2713
+ "data-state": getCollectionState(view.list),
2714
+ "data-loading": view.list.isLoading || view.list.isMutating ? "true" : void 0,
2321
2715
  children: [
2322
2716
  /* @__PURE__ */ jsxs8("div", { children: [
2323
- enabled.search ? /* @__PURE__ */ jsx13(
2324
- KeepSearchInput,
2325
- {
2326
- value: searchValue,
2327
- onValueChange: (value) => {
2328
- setSearchValue(value);
2329
- setPage(1);
2330
- }
2331
- }
2332
- ) : null,
2333
- enabled.sort ? /* @__PURE__ */ jsx13(
2334
- KeepSortSelect,
2335
- {
2336
- value: sortToValue(sort),
2337
- onValueChange: (_value, nextSort) => {
2338
- setSort(nextSort);
2339
- setPage(1);
2340
- }
2341
- }
2342
- ) : null,
2343
- enabled.tagFilter ? /* @__PURE__ */ jsx13(
2344
- KeepTagFilter,
2345
- {
2346
- query,
2347
- value: tag,
2348
- onValueChange: (value) => {
2349
- setTag(value);
2350
- setPage(1);
2351
- }
2352
- }
2353
- ) : null
2717
+ view.enabled.search ? /* @__PURE__ */ jsx13(KeepSearchInput, { value: view.searchValue, onValueChange: view.setSearchValue }) : null,
2718
+ view.enabled.sort ? /* @__PURE__ */ jsx13(KeepSortSelect, { value: view.sortValue, onValueChange: view.setSortValue }) : null,
2719
+ view.enabled.tagFilter ? /* @__PURE__ */ jsx13(KeepTagFilter, { query, value: view.tag, onValueChange: view.setTag }) : null
2354
2720
  ] }),
2355
2721
  /* @__PURE__ */ jsx13(
2356
2722
  KeepList,
2357
2723
  {
2358
- query: resolvedQuery,
2724
+ query: view.resolvedQuery,
2359
2725
  renderItem,
2360
2726
  itemCardProps,
2361
2727
  layout,
@@ -2364,16 +2730,16 @@ function KeepCollectionContent({
2364
2730
  error
2365
2731
  }
2366
2732
  ),
2367
- enabled.pagination ? /* @__PURE__ */ jsx13(
2733
+ view.enabled.pagination ? /* @__PURE__ */ jsx13(
2368
2734
  KeepPagination,
2369
2735
  {
2370
- totalCount: list.totalCount,
2371
- pageSize: resolvedPageSize,
2372
- page: list.page,
2373
- onPageChange: (nextPage) => setPage(nextPage)
2736
+ totalCount: view.list.totalCount,
2737
+ pageSize: view.resolvedPageSize,
2738
+ page: view.list.page,
2739
+ onPageChange: view.setPage
2374
2740
  }
2375
2741
  ) : null,
2376
- enabled.bulkActions ? /* @__PURE__ */ jsx13(KeepBulkActions, { query: resolvedQuery }) : null
2742
+ view.enabled.bulkActions ? /* @__PURE__ */ jsx13(KeepBulkActions, { query: view.resolvedQuery }) : null
2377
2743
  ]
2378
2744
  }
2379
2745
  );
@@ -2387,41 +2753,19 @@ function getCollectionState(list) {
2387
2753
 
2388
2754
  // src/KeepLayout.tsx
2389
2755
  import { jsx as jsx14 } from "react/jsx-runtime";
2390
- function KeepLayout({ layout = "list", children, style, ...props }) {
2391
- const layoutStyle = layout === "grid" ? { display: "grid", gap: "1rem", gridTemplateColumns: "repeat(auto-fit, minmax(16rem, 1fr))" } : layout === "compact" ? { display: "grid", gap: "0.5rem", gridTemplateColumns: "repeat(auto-fit, minmax(12rem, 1fr))" } : { display: "flex", flexDirection: "column", gap: "1rem" };
2392
- return /* @__PURE__ */ jsx14("div", { ...props, style: { ...layoutStyle, ...style }, "data-keepkit": "layout", "data-layout": layout, children });
2756
+ function KeepLayout({ layout = "list", children, ...props }) {
2757
+ return /* @__PURE__ */ jsx14("div", { ...props, "data-keepkit": "layout", "data-layout": layout, children });
2393
2758
  }
2394
2759
 
2395
2760
  // src/KeepNoteEditor.tsx
2761
+ import { isValidElement as isValidElement5 } from "react";
2762
+
2763
+ // src/hooks/useKeepNoteEditor.ts
2396
2764
  import { useKeepItem as useKeepItem3 } from "@keepkit/core/react";
2397
- import {
2398
- isValidElement as isValidElement5,
2399
- useCallback as useCallback2,
2400
- useEffect as useEffect3,
2401
- useRef as useRef3,
2402
- useState as useState7
2403
- } from "react";
2404
- import { Fragment as Fragment5, jsx as jsx15, jsxs as jsxs9 } from "react/jsx-runtime";
2405
- function KeepNoteEditor({
2406
- item,
2407
- label,
2408
- saveLabel,
2409
- placeholder,
2410
- debounceMs = 300,
2411
- onSaved,
2412
- onSaveError,
2413
- render,
2414
- children,
2415
- asChild = false,
2416
- className,
2417
- ...formProps
2418
- }) {
2419
- const defaultLabel = useUiLabel("note");
2420
- const defaultSaveLabel = useUiLabel("saveNote");
2421
- const errorLabel = useUiLabel("error");
2765
+ import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef3, useState as useState7 } from "react";
2766
+ function useKeepNoteEditor({ item, debounceMs, onSaved, onSaveError }) {
2422
2767
  const itemState = useKeepItem3(item);
2423
2768
  const { error, isMutating, item: savedItem, updateNote } = itemState;
2424
- const contentChildren = asChild && isValidElement5(children) ? void 0 : children;
2425
2769
  const [note, setNote] = useState7(item.note ?? "");
2426
2770
  const baselineNote = savedItem?.note ?? item.note ?? "";
2427
2771
  const isDirty = note !== baselineNote;
@@ -2433,9 +2777,9 @@ function KeepNoteEditor({
2433
2777
  await updateNote(nextNote);
2434
2778
  lastSavedNoteRef.current = note;
2435
2779
  onSaved?.(nextNote);
2436
- } catch (error2) {
2437
- onSaveError?.(error2);
2438
- throw error2;
2780
+ } catch (cause) {
2781
+ onSaveError?.(cause);
2782
+ throw cause;
2439
2783
  }
2440
2784
  }, [note, onSaveError, onSaved, updateNote]);
2441
2785
  useEffect3(() => {
@@ -2452,31 +2796,62 @@ function KeepNoteEditor({
2452
2796
  error,
2453
2797
  save
2454
2798
  };
2455
- const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? /* @__PURE__ */ jsxs9(Fragment5, { children: [
2799
+ const submit = (event) => {
2800
+ event.preventDefault();
2801
+ void save().catch(() => void 0);
2802
+ };
2803
+ return {
2804
+ state,
2805
+ submit,
2806
+ handleKeyDown: (event) => {
2807
+ if (event.key !== "Enter" || !event.ctrlKey && !event.metaKey) return;
2808
+ event.preventDefault();
2809
+ void save().catch(() => void 0);
2810
+ },
2811
+ labels: {
2812
+ note: useUiLabel("note"),
2813
+ save: useUiLabel("saveNote"),
2814
+ error: useUiLabel("error")
2815
+ }
2816
+ };
2817
+ }
2818
+
2819
+ // src/KeepNoteEditor.tsx
2820
+ import { Fragment as Fragment5, jsx as jsx15, jsxs as jsxs9 } from "react/jsx-runtime";
2821
+ function KeepNoteEditor({
2822
+ item,
2823
+ label,
2824
+ saveLabel,
2825
+ placeholder,
2826
+ debounceMs = 300,
2827
+ onSaved,
2828
+ onSaveError,
2829
+ render,
2830
+ children,
2831
+ asChild = false,
2832
+ className,
2833
+ ...formProps
2834
+ }) {
2835
+ const view = useKeepNoteEditor({ item, debounceMs, onSaved, onSaveError });
2836
+ const { error, isDirty, isSaving, note, setNote } = view.state;
2837
+ const contentChildren = asChild && isValidElement5(children) ? void 0 : children;
2838
+ const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? /* @__PURE__ */ jsxs9(Fragment5, { children: [
2456
2839
  /* @__PURE__ */ jsxs9("label", { children: [
2457
- label ?? defaultLabel,
2840
+ label ?? view.labels.note,
2458
2841
  /* @__PURE__ */ jsx15(
2459
2842
  "textarea",
2460
2843
  {
2844
+ "data-keep-action": "edit-note",
2461
2845
  value: note,
2462
2846
  onChange: (event) => setNote(event.currentTarget.value),
2463
2847
  placeholder,
2464
- disabled: isMutating,
2465
- onKeyDown: (event) => {
2466
- if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
2467
- event.preventDefault();
2468
- void save().catch(() => void 0);
2469
- }
2470
- }
2848
+ disabled: isSaving,
2849
+ onKeyDown: view.handleKeyDown
2471
2850
  }
2472
2851
  )
2473
2852
  ] }),
2474
- /* @__PURE__ */ jsx15("button", { type: "submit", disabled: isMutating, "aria-busy": isMutating, children: saveLabel ?? defaultSaveLabel })
2853
+ /* @__PURE__ */ jsx15("button", { type: "submit", "data-keep-action": "save-note", disabled: isSaving, "aria-busy": isSaving, children: saveLabel ?? view.labels.save })
2475
2854
  ] });
2476
- const handleSubmit = (event) => {
2477
- event.preventDefault();
2478
- void save().catch(() => void 0);
2479
- };
2480
2855
  if (!asChild) {
2481
2856
  return /* @__PURE__ */ jsxs9(
2482
2857
  "form",
@@ -2484,14 +2859,14 @@ function KeepNoteEditor({
2484
2859
  ...formProps,
2485
2860
  className,
2486
2861
  "data-keepkit": "note-editor",
2487
- onSubmit: handleSubmit,
2488
- "aria-busy": isMutating || formProps["aria-busy"],
2862
+ onSubmit: view.submit,
2863
+ "aria-busy": isSaving || formProps["aria-busy"],
2489
2864
  "data-state": error ? "error" : isDirty ? "dirty" : "clean",
2490
- "data-loading": isMutating ? "true" : void 0,
2491
- "data-disabled": isMutating ? "true" : void 0,
2865
+ "data-loading": isSaving ? "true" : void 0,
2866
+ "data-disabled": isSaving ? "true" : void 0,
2492
2867
  children: [
2493
2868
  body,
2494
- error ? /* @__PURE__ */ jsx15("p", { role: "alert", children: getErrorMessage3(error, errorLabel) }) : null
2869
+ error ? /* @__PURE__ */ jsx15("p", { role: "alert", children: getErrorMessage3(error, view.labels.error) }) : null
2495
2870
  ]
2496
2871
  }
2497
2872
  );
@@ -2503,11 +2878,11 @@ function KeepNoteEditor({
2503
2878
  ...formProps,
2504
2879
  className,
2505
2880
  "data-keepkit": "note-editor",
2506
- onSubmit: handleSubmit,
2507
- "aria-busy": isMutating || formProps["aria-busy"],
2881
+ onSubmit: view.submit,
2882
+ "aria-busy": isSaving || formProps["aria-busy"],
2508
2883
  "data-state": error ? "error" : isDirty ? "dirty" : "clean",
2509
- "data-loading": isMutating ? "true" : void 0,
2510
- "data-disabled": isMutating ? "true" : void 0
2884
+ "data-loading": isSaving ? "true" : void 0,
2885
+ "data-disabled": isSaving ? "true" : void 0
2511
2886
  },
2512
2887
  body,
2513
2888
  "KeepNoteEditor"
@@ -2517,9 +2892,62 @@ function getErrorMessage3(error, fallback) {
2517
2892
  return error instanceof Error ? error.message : fallback;
2518
2893
  }
2519
2894
 
2520
- // src/KeepSyncRecoveryDialog.tsx
2895
+ // src/hooks/useKeepSyncRecoveryDialog.ts
2521
2896
  import { useKeepContext as useKeepContext3 } from "@keepkit/core/react";
2522
2897
  import { useEffect as useEffect4, useState as useState8 } from "react";
2898
+ function useKeepSyncRecoveryDialog(options) {
2899
+ const { open, onOpenChange, conflicts, onManualMerge } = options;
2900
+ const context = useKeepContext3();
2901
+ const conflictList = conflicts ?? context.syncState.conflicts ?? [];
2902
+ const hasRecovery = conflictList.length > 0 || context.syncState.status === "error" || Boolean(context.error);
2903
+ const [dismissed, setDismissed] = useState8(false);
2904
+ const [busyId, setBusyId] = useState8();
2905
+ const [error, setError] = useState8();
2906
+ useEffect4(() => {
2907
+ if (hasRecovery) setDismissed(false);
2908
+ }, [hasRecovery]);
2909
+ return {
2910
+ conflictList,
2911
+ isOpen: open ?? (hasRecovery && !dismissed),
2912
+ busyId,
2913
+ error,
2914
+ showBackupRecovery: context.syncState.status === "error" || Boolean(context.error),
2915
+ close: () => {
2916
+ setDismissed(true);
2917
+ onOpenChange?.(false);
2918
+ },
2919
+ resolve: async (conflict, resolution) => {
2920
+ setError(void 0);
2921
+ setBusyId(conflict.id);
2922
+ try {
2923
+ const merged = resolution === "manual" ? await onManualMerge?.(conflict) : void 0;
2924
+ if (resolution === "manual" && !merged) throw new Error("A manual merge result is required.");
2925
+ await context.resolveSyncConflict(conflict.id, resolution, merged);
2926
+ } catch (cause) {
2927
+ setError(cause);
2928
+ } finally {
2929
+ setBusyId(void 0);
2930
+ }
2931
+ },
2932
+ labels: {
2933
+ close: useUiLabel("close"),
2934
+ title: useUiLabel("resolveSync"),
2935
+ conflict: useUiLabel("syncConflict"),
2936
+ keepLocal: useUiLabel("keepLocal"),
2937
+ useServer: useUiLabel("useServer"),
2938
+ manualMerge: useUiLabel("manualMerge"),
2939
+ localVersion: useUiLabel("localVersion"),
2940
+ remoteVersion: useUiLabel("remoteVersion"),
2941
+ updatedAt: useUiLabel("updatedAt"),
2942
+ note: useUiLabel("note"),
2943
+ backupRecovery: useUiLabel("backupRecovery"),
2944
+ backupRecoveryDescription: useUiLabel("backupRecoveryDescription"),
2945
+ error: useUiLabel("error")
2946
+ }
2947
+ };
2948
+ }
2949
+
2950
+ // src/KeepSyncRecoveryDialog.tsx
2523
2951
  import { jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
2524
2952
  function KeepSyncRecoveryDialog({
2525
2953
  open,
@@ -2533,44 +2961,8 @@ function KeepSyncRecoveryDialog({
2533
2961
  className,
2534
2962
  ...props
2535
2963
  }) {
2536
- const context = useKeepContext3();
2537
- const closeLabel = useUiLabel("close");
2538
- const defaultDialogTitle = useUiLabel("resolveSync");
2539
- const dialogTitle = title ?? defaultDialogTitle;
2540
- const conflictLabel = useUiLabel("syncConflict");
2541
- const keepLocalLabel = useUiLabel("keepLocal");
2542
- const useServerLabel = useUiLabel("useServer");
2543
- const manualMergeLabel = useUiLabel("manualMerge");
2544
- const backupRecoveryLabel = useUiLabel("backupRecovery");
2545
- const backupRecoveryDescription = useUiLabel("backupRecoveryDescription");
2546
- const errorLabel = useUiLabel("error");
2547
- const conflictList = conflicts ?? context.syncState.conflicts ?? [];
2548
- const hasRecovery = conflictList.length > 0 || context.syncState.status === "error" || Boolean(context.error);
2549
- const [dismissed, setDismissed] = useState8(false);
2550
- const [busyId, setBusyId] = useState8();
2551
- const [error, setError] = useState8();
2552
- useEffect4(() => {
2553
- if (hasRecovery) setDismissed(false);
2554
- }, [hasRecovery]);
2555
- const isOpen = open ?? (hasRecovery && !dismissed);
2556
- if (!isOpen) return null;
2557
- const close = () => {
2558
- setDismissed(true);
2559
- onOpenChange?.(false);
2560
- };
2561
- async function resolve(conflict, resolution) {
2562
- setError(void 0);
2563
- setBusyId(conflict.id);
2564
- try {
2565
- const merged = resolution === "manual" ? await onManualMerge?.(conflict) : void 0;
2566
- if (resolution === "manual" && !merged) throw new Error("A manual merge result is required.");
2567
- await context.resolveSyncConflict(conflict.id, resolution, merged);
2568
- } catch (cause) {
2569
- setError(cause);
2570
- } finally {
2571
- setBusyId(void 0);
2572
- }
2573
- }
2964
+ const view = useKeepSyncRecoveryDialog({ open, onOpenChange, conflicts, onManualMerge });
2965
+ if (!view.isOpen) return null;
2574
2966
  return /* @__PURE__ */ jsxs10(
2575
2967
  "section",
2576
2968
  {
@@ -2579,55 +2971,115 @@ function KeepSyncRecoveryDialog({
2579
2971
  role: "dialog",
2580
2972
  "aria-modal": "true",
2581
2973
  "aria-labelledby": "keepkit-sync-recovery-title",
2974
+ "aria-describedby": view.error ? "keepkit-sync-recovery-error" : void 0,
2975
+ "aria-busy": view.busyId !== void 0,
2582
2976
  "data-keepkit": "sync-recovery",
2583
- "data-state": conflictList.length > 0 ? "conflict" : "error",
2584
- "data-loading": busyId !== void 0 ? "true" : void 0,
2977
+ "data-state": view.conflictList.length > 0 ? "conflict" : "error",
2978
+ "data-loading": view.busyId !== void 0 ? "true" : void 0,
2585
2979
  children: [
2586
2980
  /* @__PURE__ */ jsxs10("header", { children: [
2587
- /* @__PURE__ */ jsx16("h2", { id: "keepkit-sync-recovery-title", children: dialogTitle }),
2588
- /* @__PURE__ */ jsx16("button", { type: "button", onClick: close, "aria-label": closeLabel, children: closeLabel })
2981
+ /* @__PURE__ */ jsx16("h2", { id: "keepkit-sync-recovery-title", children: title ?? view.labels.title }),
2982
+ /* @__PURE__ */ jsx16("button", { type: "button", "data-keep-action": "close-dialog", onClick: view.close, "aria-label": view.labels.close, children: view.labels.close })
2589
2983
  ] }),
2590
2984
  children,
2591
- conflictList.length > 0 ? /* @__PURE__ */ jsxs10("div", { children: [
2592
- /* @__PURE__ */ jsx16("p", { children: conflictLabel }),
2593
- conflictList.map((conflict) => /* @__PURE__ */ jsxs10("article", { "data-conflict-id": conflict.id, children: [
2985
+ view.conflictList.length > 0 ? /* @__PURE__ */ jsxs10("div", { children: [
2986
+ /* @__PURE__ */ jsx16("p", { children: view.labels.conflict }),
2987
+ view.conflictList.map((conflict) => /* @__PURE__ */ jsxs10("article", { "data-conflict-id": conflict.id, children: [
2594
2988
  /* @__PURE__ */ jsx16("h3", { children: getMetaTitle(conflict.operation.item?.meta) ?? conflict.id }),
2989
+ /* @__PURE__ */ jsxs10("div", { "data-conflict-preview": true, children: [
2990
+ /* @__PURE__ */ jsx16(
2991
+ ConflictPreview,
2992
+ {
2993
+ item: conflict.operation.item,
2994
+ heading: view.labels.localVersion,
2995
+ updatedAtLabel: view.labels.updatedAt,
2996
+ noteLabel: view.labels.note,
2997
+ side: "local"
2998
+ }
2999
+ ),
3000
+ /* @__PURE__ */ jsx16(
3001
+ ConflictPreview,
3002
+ {
3003
+ item: conflict.remote,
3004
+ heading: view.labels.remoteVersion,
3005
+ updatedAtLabel: view.labels.updatedAt,
3006
+ noteLabel: view.labels.note,
3007
+ side: "remote"
3008
+ }
3009
+ )
3010
+ ] }),
2595
3011
  /* @__PURE__ */ jsxs10("div", { children: [
2596
- /* @__PURE__ */ jsx16("button", { type: "button", onClick: () => void resolve(conflict, "local"), disabled: busyId !== void 0, children: keepLocalLabel }),
2597
- /* @__PURE__ */ jsx16("button", { type: "button", onClick: () => void resolve(conflict, "remote"), disabled: busyId !== void 0, children: useServerLabel }),
2598
3012
  /* @__PURE__ */ jsx16(
2599
3013
  "button",
2600
3014
  {
2601
3015
  type: "button",
2602
- onClick: () => void resolve(conflict, "manual"),
2603
- disabled: busyId !== void 0 || !onManualMerge,
2604
- children: manualMergeLabel
3016
+ "data-keep-action": "keep-local",
3017
+ onClick: () => void view.resolve(conflict, "local"),
3018
+ disabled: view.busyId !== void 0,
3019
+ children: view.labels.keepLocal
3020
+ }
3021
+ ),
3022
+ /* @__PURE__ */ jsx16(
3023
+ "button",
3024
+ {
3025
+ type: "button",
3026
+ "data-keep-action": "use-server",
3027
+ onClick: () => void view.resolve(conflict, "remote"),
3028
+ disabled: view.busyId !== void 0,
3029
+ children: view.labels.useServer
3030
+ }
3031
+ ),
3032
+ /* @__PURE__ */ jsx16(
3033
+ "button",
3034
+ {
3035
+ type: "button",
3036
+ "data-keep-action": "manual-merge",
3037
+ onClick: () => void view.resolve(conflict, "manual"),
3038
+ disabled: view.busyId !== void 0 || !onManualMerge,
3039
+ children: view.labels.manualMerge
2605
3040
  }
2606
3041
  )
2607
3042
  ] })
2608
3043
  ] }, conflict.id))
2609
3044
  ] }) : null,
2610
- context.syncState.status === "error" || context.error ? /* @__PURE__ */ jsxs10("section", { "data-recovery": "backup", children: [
2611
- /* @__PURE__ */ jsx16("h3", { children: backupRecoveryLabel }),
2612
- /* @__PURE__ */ jsx16("p", { children: backupRecoveryDescription }),
3045
+ view.showBackupRecovery ? /* @__PURE__ */ jsxs10("section", { "data-recovery": "backup", children: [
3046
+ /* @__PURE__ */ jsx16("h3", { children: view.labels.backupRecovery }),
3047
+ /* @__PURE__ */ jsx16("p", { children: view.labels.backupRecoveryDescription }),
2613
3048
  backup ?? (showBackupControls ? /* @__PURE__ */ jsx16(KeepBackup, {}) : null)
2614
3049
  ] }) : null,
2615
- error ? /* @__PURE__ */ jsx16("p", { role: "alert", children: error instanceof Error ? error.message : errorLabel }) : null
3050
+ 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
2616
3051
  ]
2617
3052
  }
2618
3053
  );
2619
3054
  }
3055
+ function ConflictPreview({
3056
+ item,
3057
+ heading,
3058
+ updatedAtLabel,
3059
+ noteLabel,
3060
+ side
3061
+ }) {
3062
+ return /* @__PURE__ */ jsxs10("article", { "data-conflict-version": side, "aria-label": heading, children: [
3063
+ /* @__PURE__ */ jsx16("h4", { children: heading }),
3064
+ /* @__PURE__ */ jsxs10("dl", { children: [
3065
+ /* @__PURE__ */ jsxs10("div", { children: [
3066
+ /* @__PURE__ */ jsx16("dt", { children: updatedAtLabel }),
3067
+ /* @__PURE__ */ jsx16("dd", { children: item ? /* @__PURE__ */ jsx16("time", { dateTime: new Date(item.updatedAt).toISOString(), children: formatConflictDate(item.updatedAt) }) : "\u2014" })
3068
+ ] }),
3069
+ /* @__PURE__ */ jsxs10("div", { children: [
3070
+ /* @__PURE__ */ jsx16("dt", { children: noteLabel }),
3071
+ /* @__PURE__ */ jsx16("dd", { children: item?.note || "\u2014" })
3072
+ ] })
3073
+ ] })
3074
+ ] });
3075
+ }
3076
+ function formatConflictDate(timestamp) {
3077
+ return new Date(timestamp).toISOString().slice(0, 10);
3078
+ }
2620
3079
 
2621
- // src/KeepSyncStatusBanner.tsx
3080
+ // src/hooks/useKeepSyncStatusBanner.ts
2622
3081
  import { useKeepContext as useKeepContext4 } from "@keepkit/core/react";
2623
- import { jsx as jsx17, jsxs as jsxs11 } from "react/jsx-runtime";
2624
- function KeepSyncStatusBanner({
2625
- onRetry,
2626
- onResolveConflicts,
2627
- children,
2628
- className,
2629
- ...props
2630
- }) {
3082
+ function useKeepSyncStatusBanner({ onRetry, children }) {
2631
3083
  const context = useKeepContext4();
2632
3084
  const retryLabel = useUiLabel("retrySync");
2633
3085
  const resolveLabel = useUiLabel("resolveSync");
@@ -2636,53 +3088,80 @@ function KeepSyncStatusBanner({
2636
3088
  const syncedLabel = useUiLabel("syncSynced");
2637
3089
  const status = context.syncState.status;
2638
3090
  const hasConflicts = (context.syncState.conflicts?.length ?? 0) > 0 || context.syncState.conflictIds.length > 0;
2639
- if (status === "idle" && !hasConflicts) return null;
2640
3091
  const message = children ?? (status === "error" ? getErrorMessage4(context.syncState.error) : status === "conflict" || hasConflicts ? conflictLabel : status === "pending" || status === "syncing" ? pendingLabel : syncedLabel);
2641
- const role = status === "error" || status === "conflict" || hasConflicts ? "alert" : "status";
2642
- const retry = async () => {
2643
- if (onRetry) {
2644
- await onRetry();
2645
- return;
3092
+ return {
3093
+ status,
3094
+ hasConflicts,
3095
+ message,
3096
+ isMutating: context.isMutating,
3097
+ role: status === "error" || status === "conflict" || hasConflicts ? "alert" : "status",
3098
+ showRetry: status === "error" || status === "pending" || status === "syncing",
3099
+ retryLabel,
3100
+ resolveLabel,
3101
+ retry: async () => {
3102
+ if (onRetry) {
3103
+ await onRetry();
3104
+ return;
3105
+ }
3106
+ await context.flushSync();
2646
3107
  }
2647
- await context.flushSync();
2648
3108
  };
3109
+ }
3110
+ function getErrorMessage4(error) {
3111
+ return error instanceof Error ? error.message : "Sync failed.";
3112
+ }
3113
+
3114
+ // src/KeepSyncStatusBanner.tsx
3115
+ import { jsx as jsx17, jsxs as jsxs11 } from "react/jsx-runtime";
3116
+ function KeepSyncStatusBanner({
3117
+ onRetry,
3118
+ onResolveConflicts,
3119
+ children,
3120
+ className,
3121
+ ...props
3122
+ }) {
3123
+ const view = useKeepSyncStatusBanner({ onRetry, children });
3124
+ if (view.status === "idle" && !view.hasConflicts) return null;
2649
3125
  return /* @__PURE__ */ jsxs11(
2650
3126
  "aside",
2651
3127
  {
2652
3128
  ...props,
2653
3129
  className,
2654
- role: props.role ?? role,
3130
+ role: props.role ?? view.role,
2655
3131
  "aria-live": props["aria-live"] ?? "polite",
2656
3132
  "data-keepkit": "sync-status",
2657
- "data-state": status,
3133
+ "data-state": view.status,
2658
3134
  children: [
2659
- /* @__PURE__ */ jsx17("p", { children: message }),
2660
- status === "error" || status === "pending" || status === "syncing" ? /* @__PURE__ */ jsx17("button", { type: "button", onClick: () => void retry(), disabled: context.isMutating, children: retryLabel }) : null,
2661
- hasConflicts ? /* @__PURE__ */ jsx17("button", { type: "button", onClick: onResolveConflicts, disabled: !onResolveConflicts, children: resolveLabel }) : null
3135
+ /* @__PURE__ */ jsx17("p", { children: view.message }),
3136
+ view.showRetry ? /* @__PURE__ */ jsx17(
3137
+ "button",
3138
+ {
3139
+ type: "button",
3140
+ "data-keep-action": "retry-sync",
3141
+ onClick: () => void view.retry(),
3142
+ disabled: view.isMutating,
3143
+ children: view.retryLabel
3144
+ }
3145
+ ) : null,
3146
+ view.hasConflicts ? /* @__PURE__ */ jsx17(
3147
+ "button",
3148
+ {
3149
+ type: "button",
3150
+ "data-keep-action": "resolve-conflicts",
3151
+ onClick: onResolveConflicts,
3152
+ disabled: !onResolveConflicts,
3153
+ children: view.resolveLabel
3154
+ }
3155
+ ) : null
2662
3156
  ]
2663
3157
  }
2664
3158
  );
2665
3159
  }
2666
- function getErrorMessage4(error) {
2667
- return error instanceof Error ? error.message : "Sync failed.";
2668
- }
2669
3160
 
2670
- // src/KeepTagEditor.tsx
3161
+ // src/hooks/useKeepTagEditor.ts
2671
3162
  import { useKeepItem as useKeepItem4 } from "@keepkit/core/react";
2672
3163
  import { useCallback as useCallback3, useEffect as useEffect5, useState as useState9 } from "react";
2673
- import { Fragment as Fragment6, jsx as jsx18, jsxs as jsxs12 } from "react/jsx-runtime";
2674
- function KeepTagEditor({
2675
- item,
2676
- availableTags = [],
2677
- onSaved,
2678
- onSaveError,
2679
- render,
2680
- ...props
2681
- }) {
2682
- const tagsLabel = useUiLabel("tagsToApply");
2683
- const removeLabel = useUiLabel("remove");
2684
- const applyTagsLabel = useUiLabel("applyTags");
2685
- const errorLabel = useUiLabel("error");
3164
+ function useKeepTagEditor({ item, onSaved, onSaveError }) {
2686
3165
  const itemState = useKeepItem4(item);
2687
3166
  const [tags, setTags] = useState9(item.tags ?? []);
2688
3167
  const [input, setInput] = useState9("");
@@ -2693,60 +3172,91 @@ function KeepTagEditor({
2693
3172
  await itemState.updateTags(nextTags);
2694
3173
  setTags(nextTags);
2695
3174
  onSaved?.(nextTags);
2696
- } catch (error) {
2697
- onSaveError?.(error);
2698
- throw error;
3175
+ } catch (cause) {
3176
+ onSaveError?.(cause);
3177
+ throw cause;
2699
3178
  }
2700
3179
  }, [itemState, onSaveError, onSaved, tags]);
2701
3180
  const addTag = (tag) => {
2702
3181
  setTags(normalizeUiTags([...tags, tag]));
2703
3182
  setInput("");
2704
3183
  };
2705
- const body = render ? render({ tags, setTags, save, isSaving: itemState.isMutating }) : /* @__PURE__ */ jsxs12(Fragment6, { children: [
3184
+ const state = { tags, setTags, save, isSaving: itemState.isMutating };
3185
+ return {
3186
+ state,
3187
+ input,
3188
+ setInput,
3189
+ error: itemState.error,
3190
+ handleInputKeyDown: (event) => {
3191
+ if (event.key === "Enter") {
3192
+ if (event.nativeEvent.isComposing) return;
3193
+ event.preventDefault();
3194
+ if (input.trim()) addTag(input);
3195
+ } else if (event.key === "Backspace" && input.length === 0 && tags.length > 0) {
3196
+ event.preventDefault();
3197
+ setTags(tags.slice(0, -1));
3198
+ }
3199
+ },
3200
+ removeTag: (tag) => setTags(tags.filter((current) => current !== tag)),
3201
+ submit: (event) => {
3202
+ event.preventDefault();
3203
+ void save().catch(() => void 0);
3204
+ },
3205
+ labels: {
3206
+ tags: useUiLabel("tagsToApply"),
3207
+ remove: useUiLabel("remove"),
3208
+ apply: useUiLabel("applyTags"),
3209
+ error: useUiLabel("error")
3210
+ }
3211
+ };
3212
+ }
3213
+
3214
+ // src/KeepTagEditor.tsx
3215
+ import { Fragment as Fragment6, jsx as jsx18, jsxs as jsxs12 } from "react/jsx-runtime";
3216
+ function KeepTagEditor({
3217
+ item,
3218
+ availableTags = [],
3219
+ onSaved,
3220
+ onSaveError,
3221
+ render,
3222
+ ...props
3223
+ }) {
3224
+ const view = useKeepTagEditor({ item, onSaved, onSaveError });
3225
+ const { isSaving, tags } = view.state;
3226
+ const body = render ? render(view.state) : /* @__PURE__ */ jsxs12(Fragment6, { children: [
2706
3227
  /* @__PURE__ */ jsxs12("label", { children: [
2707
- tagsLabel,
3228
+ view.labels.tags,
2708
3229
  /* @__PURE__ */ jsx18(
2709
3230
  "input",
2710
3231
  {
2711
- value: input,
3232
+ "data-keep-action": "edit-tags",
3233
+ value: view.input,
2712
3234
  list: availableTags.length > 0 ? `keep-tags-${item.id}` : void 0,
2713
- onChange: (event) => setInput(event.currentTarget.value),
2714
- onKeyDown: (event) => {
2715
- if (event.key === "Enter") {
2716
- if (event.nativeEvent.isComposing) return;
2717
- event.preventDefault();
2718
- if (input.trim()) addTag(input);
2719
- } else if (event.key === "Backspace" && input.length === 0 && tags.length > 0) {
2720
- event.preventDefault();
2721
- setTags(tags.slice(0, -1));
2722
- }
2723
- }
3235
+ onChange: (event) => view.setInput(event.currentTarget.value),
3236
+ onKeyDown: view.handleInputKeyDown
2724
3237
  }
2725
3238
  )
2726
3239
  ] }),
2727
3240
  availableTags.length > 0 ? /* @__PURE__ */ jsx18("datalist", { id: `keep-tags-${item.id}`, children: availableTags.map((tag) => /* @__PURE__ */ jsx18("option", { value: tag }, tag)) }) : null,
2728
- /* @__PURE__ */ jsx18("ul", { "aria-label": tagsLabel, children: tags.map((tag) => /* @__PURE__ */ jsxs12("li", { children: [
3241
+ /* @__PURE__ */ jsx18("ul", { "aria-label": view.labels.tags, children: tags.map((tag) => /* @__PURE__ */ jsxs12("li", { children: [
2729
3242
  tag,
2730
- /* @__PURE__ */ jsx18("button", { type: "button", onClick: () => setTags(tags.filter((current) => current !== tag)), children: removeLabel })
3243
+ /* @__PURE__ */ jsx18("button", { type: "button", "data-keep-action": "remove-tag", onClick: () => view.removeTag(tag), children: view.labels.remove })
2731
3244
  ] }, tag)) }),
2732
- /* @__PURE__ */ jsx18("button", { type: "submit", disabled: itemState.isMutating, "aria-busy": itemState.isMutating, children: applyTagsLabel })
3245
+ /* @__PURE__ */ jsx18("button", { type: "submit", "data-keep-action": "apply-tags", disabled: isSaving, "aria-busy": isSaving, children: view.labels.apply })
2733
3246
  ] });
2734
3247
  return /* @__PURE__ */ jsxs12(
2735
3248
  "form",
2736
3249
  {
2737
3250
  ...props,
2738
- onSubmit: (event) => {
2739
- event.preventDefault();
2740
- void save().catch(() => void 0);
2741
- },
2742
- "aria-busy": itemState.isMutating || props["aria-busy"],
3251
+ onSubmit: view.submit,
3252
+ "aria-busy": isSaving || props["aria-busy"],
2743
3253
  "data-keepkit": "tag-editor",
2744
- "data-state": itemState.error ? "error" : itemState.isMutating ? "saving" : "idle",
2745
- "data-loading": itemState.isMutating ? "true" : void 0,
2746
- "data-disabled": itemState.isMutating ? "true" : void 0,
3254
+ "data-state": view.error ? "error" : isSaving ? "saving" : "idle",
3255
+ "data-loading": isSaving ? "true" : void 0,
3256
+ "data-disabled": isSaving ? "true" : void 0,
2747
3257
  children: [
2748
3258
  body,
2749
- itemState.error ? /* @__PURE__ */ jsx18("p", { role: "alert", children: getErrorMessage5(itemState.error, errorLabel) }) : null
3259
+ view.error ? /* @__PURE__ */ jsx18("p", { role: "alert", children: getErrorMessage5(view.error, view.labels.error) }) : null
2750
3260
  ]
2751
3261
  }
2752
3262
  );
@@ -2755,23 +3265,84 @@ function getErrorMessage5(error, fallback) {
2755
3265
  return error instanceof Error ? error.message : fallback;
2756
3266
  }
2757
3267
 
2758
- // src/KeepUndo.tsx
3268
+ // src/hooks/useKeepUndo.ts
2759
3269
  import { useKeepContext as useKeepContext5 } from "@keepkit/core/react";
3270
+ function useKeepUndo() {
3271
+ const context = useKeepContext5();
3272
+ return {
3273
+ canUndo: context.undo.canUndo,
3274
+ undo: () => context.undoLastRemoval(),
3275
+ message: useUiLabel("undoAvailable"),
3276
+ label: useUiLabel("undo")
3277
+ };
3278
+ }
3279
+
3280
+ // src/KeepUndo.tsx
2760
3281
  import { jsx as jsx19, jsxs as jsxs13 } from "react/jsx-runtime";
2761
3282
  function KeepUndo({ children, label, ...props }) {
2762
- const context = useKeepContext5();
2763
- const message = useUiLabel("undoAvailable");
2764
- const undoLabel = useUiLabel("undo");
2765
- if (!context.undo.canUndo) return null;
3283
+ const view = useKeepUndo();
3284
+ if (!view.canUndo) return null;
2766
3285
  return /* @__PURE__ */ jsxs13("div", { ...props, role: "status", "aria-live": "polite", "data-keepkit": "undo", "data-state": "available", children: [
2767
- children ?? message,
2768
- /* @__PURE__ */ jsx19("button", { type: "button", onClick: () => void context.undoLastRemoval(), children: label ?? undoLabel })
3286
+ children ?? view.message,
3287
+ /* @__PURE__ */ jsx19("button", { type: "button", "data-keep-action": "undo", onClick: () => void view.undo(), children: label ?? view.label })
2769
3288
  ] });
2770
3289
  }
2771
3290
 
2772
3291
  // src/status.tsx
3292
+ import { isValidElement as isValidElement6 } from "react";
3293
+
3294
+ // src/hooks/useStatusViews.ts
2773
3295
  import { useKeepContext as useKeepContext6 } from "@keepkit/core/react";
2774
- import { isValidElement as isValidElement6, useEffect as useEffect6, useRef as useRef4, useState as useState10 } from "react";
3296
+ import { useEffect as useEffect6, useRef as useRef4, useState as useState10 } from "react";
3297
+ function useKeepEmptyState() {
3298
+ return useUiLabel("noItems").replace(/\.$/, "");
3299
+ }
3300
+ function useKeepStatus(status) {
3301
+ const context = useKeepContext6();
3302
+ const resolvedStatus = status ?? getDerivedStatus(context);
3303
+ const state = {
3304
+ status: resolvedStatus,
3305
+ error: context.error,
3306
+ pendingCount: context.syncState.pendingCount,
3307
+ items: context.items
3308
+ };
3309
+ return { state, defaultLabel: useUiLabel(getStatusLabelKey3(resolvedStatus)) };
3310
+ }
3311
+ function useKeepAnnouncements(messages) {
3312
+ const context = useKeepContext6();
3313
+ const savedMessage = useUiLabel("savedMessage", messages?.save);
3314
+ const removedMessage = useUiLabel("removedMessage", messages?.remove);
3315
+ const noteSavedMessage = useUiLabel("noteSavedMessage", messages?.note);
3316
+ const [message, setMessage] = useState10("");
3317
+ const lastChangeRef = useRef4(void 0);
3318
+ useEffect6(() => {
3319
+ const change = context.lastChange;
3320
+ if (!change || change === lastChangeRef.current) return;
3321
+ lastChangeRef.current = change;
3322
+ if (change.action === "save") setMessage(savedMessage);
3323
+ else if (change.action === "remove" || change.action === "removeBatch") setMessage(removedMessage);
3324
+ else if (change.action === "updateNote") setMessage(noteSavedMessage);
3325
+ }, [context.lastChange, noteSavedMessage, removedMessage, savedMessage]);
3326
+ return message;
3327
+ }
3328
+ function getDerivedStatus(context) {
3329
+ if (context.error) return "error";
3330
+ if (context.syncState.status === "pending" || context.syncState.status === "syncing") return "syncing";
3331
+ if (context.isMutating) return "saving";
3332
+ if (context.isLoading && !context.isHydrated) return "loading";
3333
+ if (context.isHydrated && context.items.length === 0) return "empty";
3334
+ return "idle";
3335
+ }
3336
+ function getStatusLabelKey3(status) {
3337
+ if (status === "empty") return "noItems";
3338
+ if (status === "loading") return "loadingItems";
3339
+ if (status === "error") return "error";
3340
+ if (status === "saving") return "saving";
3341
+ if (status === "syncing") return "syncing";
3342
+ return "saved";
3343
+ }
3344
+
3345
+ // src/status.tsx
2775
3346
  import { Fragment as Fragment7, jsx as jsx20, jsxs as jsxs14 } from "react/jsx-runtime";
2776
3347
  function KeepEmptyState({
2777
3348
  title,
@@ -2782,7 +3353,7 @@ function KeepEmptyState({
2782
3353
  className,
2783
3354
  ...rootProps
2784
3355
  }) {
2785
- const defaultTitle = useUiLabel("noItems").replace(/\.$/, "");
3356
+ const defaultTitle = useKeepEmptyState();
2786
3357
  const contentChildren = asChild && isValidElement6(children) ? void 0 : children;
2787
3358
  const body = contentChildren ?? /* @__PURE__ */ jsxs14(Fragment7, { children: [
2788
3359
  /* @__PURE__ */ jsx20("h2", { children: title ?? defaultTitle }),
@@ -2806,18 +3377,10 @@ function KeepStatus({
2806
3377
  className,
2807
3378
  ...rootProps
2808
3379
  }) {
2809
- const context = useKeepContext6();
3380
+ const view = useKeepStatus(status);
2810
3381
  const contentChildren = asChild && isValidElement6(children) ? void 0 : children;
2811
- const resolvedStatus = status ?? getDerivedStatus(context);
2812
- const defaultLabel = useUiLabel(getStatusLabelKey3(resolvedStatus));
2813
- const state = {
2814
- status: resolvedStatus,
2815
- error: context.error,
2816
- pendingCount: context.syncState.pendingCount,
2817
- items: context.items
2818
- };
2819
- const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? labels?.[resolvedStatus] ?? defaultLabel;
2820
- const role = rootProps.role ?? (resolvedStatus === "error" ? "alert" : "status");
3382
+ const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? labels?.[view.state.status] ?? view.defaultLabel;
3383
+ const role = rootProps.role ?? (view.state.status === "error" ? "alert" : "status");
2821
3384
  return renderRoot(
2822
3385
  asChild,
2823
3386
  isValidElement6(children) ? children : void 0,
@@ -2827,28 +3390,15 @@ function KeepStatus({
2827
3390
  "data-keepkit": "status",
2828
3391
  role,
2829
3392
  "aria-live": rootProps["aria-live"] ?? "polite",
2830
- "data-state": resolvedStatus,
2831
- "data-loading": resolvedStatus === "loading" || resolvedStatus === "saving" || resolvedStatus === "syncing" ? "true" : void 0
3393
+ "data-state": view.state.status,
3394
+ "data-loading": view.state.status === "loading" || view.state.status === "saving" || view.state.status === "syncing" ? "true" : void 0
2832
3395
  },
2833
3396
  body,
2834
3397
  "KeepStatus"
2835
3398
  );
2836
3399
  }
2837
3400
  function KeepAnnouncements({ messages, ...props }) {
2838
- const context = useKeepContext6();
2839
- const savedMessage = useUiLabel("savedMessage", messages?.save);
2840
- const removedMessage = useUiLabel("removedMessage", messages?.remove);
2841
- const noteSavedMessage = useUiLabel("noteSavedMessage", messages?.note);
2842
- const [message, setMessage] = useState10("");
2843
- const lastChangeRef = useRef4(void 0);
2844
- useEffect6(() => {
2845
- const change = context.lastChange;
2846
- if (!change || change === lastChangeRef.current) return;
2847
- lastChangeRef.current = change;
2848
- if (change.action === "save") setMessage(savedMessage);
2849
- else if (change.action === "remove" || change.action === "removeBatch") setMessage(removedMessage);
2850
- else if (change.action === "updateNote") setMessage(noteSavedMessage);
2851
- }, [context.lastChange, noteSavedMessage, removedMessage, savedMessage]);
3401
+ const message = useKeepAnnouncements(messages);
2852
3402
  return /* @__PURE__ */ jsx20(
2853
3403
  "div",
2854
3404
  {
@@ -2863,26 +3413,22 @@ function KeepAnnouncements({ messages, ...props }) {
2863
3413
  );
2864
3414
  }
2865
3415
  var KeepAnnouncer = KeepAnnouncements;
2866
- function getDerivedStatus(context) {
2867
- if (context.error) return "error";
2868
- if (context.syncState.status === "pending" || context.syncState.status === "syncing") return "syncing";
2869
- if (context.isMutating) return "saving";
2870
- if (context.isLoading && !context.isHydrated) return "loading";
2871
- if (context.isHydrated && context.items.length === 0) return "empty";
2872
- return "idle";
2873
- }
2874
- function getStatusLabelKey3(status) {
2875
- if (status === "empty") return "noItems";
2876
- if (status === "loading") return "loadingItems";
2877
- if (status === "error") return "error";
2878
- if (status === "saving") return "saving";
2879
- if (status === "syncing") return "syncing";
2880
- return "saved";
2881
- }
2882
3416
 
2883
3417
  // src/theme.tsx
2884
3418
  import { cloneElement as cloneElement2, isValidElement as isValidElement7 } from "react";
2885
3419
  import { jsx as jsx21 } from "react/jsx-runtime";
3420
+ var keepThemeNames = [
3421
+ "default",
3422
+ "ocean",
3423
+ "forest",
3424
+ "sunset",
3425
+ "lavender",
3426
+ "compact",
3427
+ "minimal",
3428
+ "rounded",
3429
+ "high-contrast",
3430
+ "dark"
3431
+ ];
2886
3432
  function KeepThemeProvider({
2887
3433
  children,
2888
3434
  theme = "default",
@@ -3095,6 +3641,7 @@ export {
3095
3641
  createStorageAdapter,
3096
3642
  getKeepLocaleLabels,
3097
3643
  isAllSelected,
3644
+ keepThemeNames,
3098
3645
  toggleSelectAll,
3099
3646
  useKeepContext7 as useKeepContext,
3100
3647
  useKeepItem5 as useKeepItem,