@keepkit/ui 0.14.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
 
@@ -1309,28 +1309,14 @@ function useUiLabel(key, override) {
1309
1309
  return override ?? context.customLabels?.[key] ?? context.labelResolver?.(key, { locale: context.locale }) ?? context.labels[key];
1310
1310
  }
1311
1311
 
1312
- // src/KeepBackup.tsx
1313
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
1314
- function KeepBackup({
1315
- filename = "keepkit-backup.json",
1316
- onExport,
1317
- onImported,
1318
- ...props
1319
- }) {
1312
+ // src/hooks/useKeepBackup.ts
1313
+ function useKeepBackup({ filename, onExport, onImported }) {
1320
1314
  const context = useKeepContext();
1321
- const exportLabel = useUiLabel("exportData");
1322
- const importLabel = useUiLabel("importData");
1323
- const importModeLabel = useUiLabel("importMode");
1324
- const mergeLabel = useUiLabel("merge");
1325
- const replaceLabel = useUiLabel("replace");
1326
- const importedCountLabel = useUiLabel("importedCount");
1327
- const failedCountLabel = useUiLabel("failedCount");
1328
- const quotaErrorLabel = useUiLabel("storageQuotaError");
1329
1315
  const inputRef = useRef(null);
1330
1316
  const [mode, setMode] = useState("merge");
1331
1317
  const [result, setResult] = useState();
1332
1318
  const [error, setError] = useState();
1333
- async function handleExport() {
1319
+ async function exportBackup() {
1334
1320
  setError(void 0);
1335
1321
  try {
1336
1322
  const data = await context.exportBackup();
@@ -1346,7 +1332,7 @@ function KeepBackup({
1346
1332
  setError(cause);
1347
1333
  }
1348
1334
  }
1349
- async function handleImport(event) {
1335
+ async function importBackup(event) {
1350
1336
  const file = event.currentTarget.files?.[0];
1351
1337
  event.currentTarget.value = "";
1352
1338
  if (!file) return;
@@ -1360,43 +1346,93 @@ function KeepBackup({
1360
1346
  setError(cause);
1361
1347
  }
1362
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 });
1363
1381
  return /* @__PURE__ */ jsxs(
1364
1382
  "section",
1365
1383
  {
1366
1384
  ...props,
1367
1385
  "data-keepkit": "backup",
1368
- "data-state": error ? "error" : result ? "complete" : "idle",
1369
- "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,
1370
1388
  children: [
1371
- /* @__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
+ ),
1372
1399
  /* @__PURE__ */ jsxs("label", { children: [
1373
- importModeLabel,
1374
- /* @__PURE__ */ jsxs("select", { value: mode, onChange: (event) => setMode(event.currentTarget.value), children: [
1375
- /* @__PURE__ */ jsx2("option", { value: "merge", children: mergeLabel }),
1376
- /* @__PURE__ */ jsx2("option", { value: "replace", children: replaceLabel })
1377
- ] })
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
+ )
1378
1413
  ] }),
1379
- /* @__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 }),
1380
1415
  /* @__PURE__ */ jsx2(
1381
1416
  "input",
1382
1417
  {
1383
- ref: inputRef,
1418
+ ref: view.inputRef,
1384
1419
  type: "file",
1420
+ "data-keep-action": "select-backup-file",
1385
1421
  accept: "application/json,.json",
1386
- "aria-label": importLabel,
1387
- onChange: (event) => void handleImport(event)
1422
+ "aria-label": view.labels.import,
1423
+ onChange: (event) => void view.importBackup(event)
1388
1424
  }
1389
1425
  ),
1390
- result ? /* @__PURE__ */ jsxs("p", { role: "status", children: [
1391
- result.imported,
1426
+ view.result ? /* @__PURE__ */ jsxs("p", { role: "status", children: [
1427
+ view.result.imported,
1392
1428
  " ",
1393
- importedCountLabel,
1429
+ view.labels.importedCount,
1394
1430
  "; ",
1395
- result.failed,
1431
+ view.result.failed,
1396
1432
  " ",
1397
- failedCountLabel
1433
+ view.labels.failedCount
1398
1434
  ] }) : null,
1399
- 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
1400
1436
  ]
1401
1437
  }
1402
1438
  );
@@ -1410,48 +1446,16 @@ function getErrorMessage(error) {
1410
1446
  return error instanceof Error ? error.message : "Something went wrong.";
1411
1447
  }
1412
1448
 
1413
- // src/KeepBulkActions.tsx
1449
+ // src/hooks/useKeepBulkActions.ts
1414
1450
  import { useKeepList } from "@keepkit/core/react";
1415
1451
  import { useState as useState2 } from "react";
1416
1452
 
1417
- // src/KeepItemCheckbox.tsx
1418
- import { jsx as jsx3 } from "react/jsx-runtime";
1419
- function KeepItemCheckbox({
1420
- item,
1421
- checked = false,
1422
- label,
1423
- onCheckedChange,
1424
- "aria-label": ariaLabel,
1425
- ...props
1426
- }) {
1427
- const itemLabel = getItemLabel(item) ?? item.id;
1428
- const accessibleLabel = ariaLabel ?? (typeof label === "string" ? label : itemLabel);
1429
- return /* @__PURE__ */ jsx3(
1430
- "input",
1431
- {
1432
- ...props,
1433
- type: "checkbox",
1434
- checked,
1435
- "data-keepkit": "item-checkbox",
1436
- "data-state": checked ? "checked" : "unchecked",
1437
- "data-disabled": props.disabled ? "true" : void 0,
1438
- "aria-label": accessibleLabel,
1439
- onChange: (event) => onCheckedChange?.(event.currentTarget.checked)
1440
- }
1441
- );
1442
- }
1443
- function getItemLabel(item) {
1444
- if (typeof item.meta !== "object" || item.meta === null || !("title" in item.meta)) return void 0;
1445
- const title = item.meta.title;
1446
- return typeof title === "string" && title.trim() ? title.trim() : void 0;
1447
- }
1448
-
1449
1453
  // src/shared.tsx
1450
1454
  import {
1451
1455
  cloneElement,
1452
1456
  isValidElement
1453
1457
  } from "react";
1454
- import { jsx as jsx4 } from "react/jsx-runtime";
1458
+ import { jsx as jsx3 } from "react/jsx-runtime";
1455
1459
  function toKeepButtonItem(item) {
1456
1460
  return {
1457
1461
  id: item.id,
@@ -1480,11 +1484,10 @@ function renderRoot(asChild, child, props, body, componentName) {
1480
1484
  if (!isValidElement(child)) throw new Error(`${componentName} with asChild requires a single React element child.`);
1481
1485
  return cloneElement(child, { ...props, children: body });
1482
1486
  }
1483
- return /* @__PURE__ */ jsx4("div", { ...props, children: body });
1487
+ return /* @__PURE__ */ jsx3("div", { ...props, children: body });
1484
1488
  }
1485
1489
 
1486
- // src/KeepBulkActions.tsx
1487
- import { Fragment, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
1490
+ // src/hooks/useKeepBulkActions.ts
1488
1491
  function isAllSelected(items, selectedIds) {
1489
1492
  if (items.length === 0) return false;
1490
1493
  const selected = new Set(selectedIds);
@@ -1499,42 +1502,30 @@ function toggleSelectAll(items, selectedIds) {
1499
1502
  }
1500
1503
  return nextIds;
1501
1504
  }
1502
- function KeepBulkActions({
1503
- query,
1504
- selectedIds: controlledSelectedIds,
1505
- defaultSelectedIds = [],
1506
- onSelectedIdsChange,
1507
- renderItem,
1508
- onCompleted,
1509
- selectionScope: controlledScope,
1510
- onSelectionScopeChange,
1511
- render,
1512
- children,
1513
- ...props
1514
- }) {
1515
- const selectItemsLabel = useUiLabel("selectItems");
1516
- const selectedCountLabel = useUiLabel("selectedCount");
1517
- const deleteSelectedLabel = useUiLabel("deleteSelected");
1518
- const tagsLabel = useUiLabel("tagsToApply");
1519
- const applyTagsLabel = useUiLabel("applyTags");
1520
- const selectionScopeLabel = useUiLabel("selectionScope");
1521
- const currentPageLabel = useUiLabel("currentPage");
1522
- const searchResultsLabel = useUiLabel("searchResults");
1523
- const allItemsLabel = useUiLabel("allItems");
1505
+ function useKeepBulkActions(options) {
1506
+ const {
1507
+ query,
1508
+ controlledSelectedIds,
1509
+ defaultSelectedIds,
1510
+ onSelectedIdsChange,
1511
+ onCompleted,
1512
+ controlledScope,
1513
+ onSelectionScopeChange
1514
+ } = options;
1524
1515
  const list = useKeepList(query);
1525
1516
  const queryList = useKeepList(query ? { ...query, pagination: void 0 } : { pagination: void 0 });
1526
1517
  const allList = useKeepList({ pagination: void 0 });
1527
1518
  const [uncontrolledScope, setUncontrolledScope] = useState2(controlledScope ?? "page");
1528
1519
  const selectionScope = controlledScope ?? uncontrolledScope;
1529
- const setSelectionScope = (scope) => {
1530
- if (controlledScope === void 0) setUncontrolledScope(scope);
1531
- onSelectionScopeChange?.(scope);
1532
- };
1533
1520
  const targetItems = selectionScope === "page" ? list.items : selectionScope === "query" ? queryList.items : allList.items;
1534
1521
  const [uncontrolledSelectedIds, setUncontrolledSelectedIds] = useState2(defaultSelectedIds);
1535
1522
  const selectedIds = controlledSelectedIds ?? uncontrolledSelectedIds;
1536
1523
  const selected = new Set(selectedIds);
1537
1524
  const [tagsInput, setTagsInput] = useState2("");
1525
+ const setSelectionScope = (scope) => {
1526
+ if (controlledScope === void 0) setUncontrolledScope(scope);
1527
+ onSelectionScopeChange?.(scope);
1528
+ };
1538
1529
  const setSelectedIds = (ids) => {
1539
1530
  if (controlledSelectedIds === void 0) setUncontrolledSelectedIds(ids);
1540
1531
  onSelectedIdsChange?.(ids);
@@ -1571,54 +1562,156 @@ function KeepBulkActions({
1571
1562
  selectionScope,
1572
1563
  setSelectionScope
1573
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
+ });
1574
1640
  const body = render ? render(state) : typeof children === "function" ? children(state) : children ?? /* @__PURE__ */ jsxs2(Fragment, { children: [
1575
1641
  /* @__PURE__ */ jsxs2("fieldset", { children: [
1576
- /* @__PURE__ */ jsx5("legend", { children: selectItemsLabel }),
1642
+ /* @__PURE__ */ jsx5("legend", { children: labels.selectItems }),
1577
1643
  /* @__PURE__ */ jsxs2("label", { children: [
1578
- selectionScopeLabel,
1644
+ labels.selectionScope,
1579
1645
  /* @__PURE__ */ jsxs2(
1580
1646
  "select",
1581
1647
  {
1582
- value: selectionScope,
1583
- onChange: (event) => setSelectionScope(event.currentTarget.value),
1648
+ "data-keep-action": "select-scope",
1649
+ value: state.selectionScope,
1650
+ onChange: (event) => state.setSelectionScope(event.currentTarget.value),
1584
1651
  children: [
1585
- /* @__PURE__ */ jsx5("option", { value: "page", children: currentPageLabel }),
1586
- /* @__PURE__ */ jsx5("option", { value: "query", children: searchResultsLabel }),
1587
- /* @__PURE__ */ jsx5("option", { value: "all", children: allItemsLabel })
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 })
1588
1655
  ]
1589
1656
  }
1590
1657
  )
1591
1658
  ] }),
1592
1659
  /* @__PURE__ */ jsxs2("label", { children: [
1593
- /* @__PURE__ */ jsx5("input", { type: "checkbox", checked: allSelected, onChange: toggleAll, "aria-label": selectItemsLabel }),
1594
- 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,
1595
1671
  " ",
1596
- selectedCountLabel
1672
+ labels.selectedCount
1597
1673
  ] }),
1598
- targetItems.map((item) => /* @__PURE__ */ jsxs2("span", { children: [
1674
+ state.items.map((item) => /* @__PURE__ */ jsxs2("span", { children: [
1599
1675
  /* @__PURE__ */ jsx5(
1600
1676
  KeepItemCheckbox,
1601
1677
  {
1602
1678
  item,
1603
1679
  checked: selected.has(item.id),
1604
- onCheckedChange: () => toggle(item.id)
1680
+ onCheckedChange: () => state.toggle(item.id)
1605
1681
  }
1606
1682
  ),
1607
1683
  renderItem ? renderItem(item, selected.has(item.id)) : getMetaTitle(item.meta) ?? item.id
1608
1684
  ] }, item.id))
1609
1685
  ] }),
1610
- /* @__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
+ ),
1611
1696
  /* @__PURE__ */ jsxs2("label", { children: [
1612
- tagsLabel,
1613
- /* @__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
+ )
1614
1706
  ] }),
1615
1707
  /* @__PURE__ */ jsx5(
1616
1708
  "button",
1617
1709
  {
1618
1710
  type: "button",
1619
- onClick: () => void updateTags(),
1620
- disabled: selectedIds.length === 0 || list.isMutating,
1621
- 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
1622
1715
  }
1623
1716
  )
1624
1717
  ] });
@@ -1627,9 +1720,9 @@ function KeepBulkActions({
1627
1720
  {
1628
1721
  ...props,
1629
1722
  "data-keepkit": "bulk-actions",
1630
- "aria-busy": list.isMutating || props["aria-busy"],
1631
- "data-state": selectedIds.length > 0 ? "selected" : "idle",
1632
- "data-loading": list.isLoading || list.isMutating ? "true" : void 0,
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,
1633
1726
  children: body
1634
1727
  }
1635
1728
  );
@@ -1637,10 +1730,26 @@ function KeepBulkActions({
1637
1730
 
1638
1731
  // src/KeepButton.tsx
1639
1732
  import {
1640
- KeepButton as CoreKeepButton,
1641
- useKeepItem
1733
+ KeepButton as CoreKeepButton
1642
1734
  } from "@keepkit/core/react";
1643
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
1644
1753
  import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
1645
1754
  function KeepButton({
1646
1755
  labels,
@@ -1650,16 +1759,11 @@ function KeepButton({
1650
1759
  iconClassName,
1651
1760
  ...props
1652
1761
  }) {
1653
- const saveLabel = useUiLabel("save", typeof labels?.unsaved === "string" ? labels.unsaved : void 0);
1654
- const savedLabel = useUiLabel("saved", typeof labels?.saved === "string" ? labels.saved : void 0);
1655
- const loadingLabel = useUiLabel("loading", typeof labels?.loading === "string" ? labels.loading : void 0);
1656
- const errorLabel = useUiLabel("error", typeof labels?.error === "string" ? labels.error : void 0);
1657
- const buttonState = useKeepItem(props.item);
1658
- const customStateLabel = labels?.loading !== void 0 || labels?.error !== void 0 || icons !== void 0 && props.children === void 0;
1762
+ const view = useKeepButton({ item: props.item, labels, icons, children: props.children });
1659
1763
  const getStateContent = (state) => {
1660
1764
  if (typeof props.children === "function") return props.children(state);
1661
1765
  if (props.children !== void 0) return props.children;
1662
- const label = state.error ? labels?.error ?? errorLabel : state.isMutating ? labels?.loading ?? loadingLabel : state.isSaved ? labels?.saved ?? savedLabel : labels?.unsaved ?? saveLabel;
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;
1663
1767
  if (!icons) return label;
1664
1768
  const icon = state.error ? icons.error : state.isMutating ? icons.loading : state.isSaved ? icons.remove ?? icons.saved : icons.save;
1665
1769
  return /* @__PURE__ */ jsxs3(Fragment2, { children: [
@@ -1670,14 +1774,16 @@ function KeepButton({
1670
1774
  const sharedProps = {
1671
1775
  ...props,
1672
1776
  "data-keepkit": "button",
1777
+ "data-keep-action": "toggle-save",
1778
+ "data-has-custom-icon": icons ? "true" : void 0,
1673
1779
  "data-icon-only": iconOnly ? "true" : void 0,
1674
- "aria-busy": props["aria-busy"] ?? (buttonState.isLoading || buttonState.isMutating),
1675
- savedLabel: labels?.saved ?? props.savedLabel ?? savedLabel,
1676
- unsavedLabel: labels?.unsaved ?? props.unsavedLabel ?? saveLabel,
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,
1677
1783
  savedAriaLabel: labels?.savedAriaLabel ?? props.savedAriaLabel,
1678
1784
  unsavedAriaLabel: labels?.unsavedAriaLabel ?? props.unsavedAriaLabel
1679
1785
  };
1680
- if (!customStateLabel) return /* @__PURE__ */ jsx6(CoreKeepButton, { ...sharedProps });
1786
+ if (!view.customStateLabel) return /* @__PURE__ */ jsx6(CoreKeepButton, { ...sharedProps });
1681
1787
  return /* @__PURE__ */ jsx6(CoreKeepButton, { ...sharedProps, children: (state) => /* @__PURE__ */ jsx6(Fragment2, { children: getStateContent(state) }) });
1682
1788
  }
1683
1789
  function renderIcon(icon, className) {
@@ -1686,100 +1792,284 @@ function renderIcon(icon, className) {
1686
1792
  }
1687
1793
 
1688
1794
  // src/KeepCollection.tsx
1689
- import { KeepErrorBoundary as KeepErrorBoundary2, useKeepList as useKeepList4 } from "@keepkit/core/react";
1690
- import { useMemo as useMemo3, useState as useState6 } from "react";
1795
+ import { KeepErrorBoundary as KeepErrorBoundary2 } from "@keepkit/core/react";
1691
1796
 
1692
- // src/KeepList.tsx
1693
- import {
1694
- KeepErrorBoundary,
1695
- useKeepList as useKeepList2
1696
- } from "@keepkit/core/react";
1697
- 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";
1698
1800
 
1699
- // src/KeepItemCard.tsx
1700
- import { useKeepItem as useKeepItem2 } from "@keepkit/core/react";
1801
+ // src/url-sync.tsx
1701
1802
  import {
1702
- isValidElement as isValidElement2
1703
- } from "react";
1704
-
1705
- // src/KeepStaleNotice.tsx
1706
- import { useKeepContext as useKeepContext2 } from "@keepkit/core/react";
1707
- import { useState as useState3 } from "react";
1708
-
1709
- // src/KeepItemStatusBadge.tsx
1710
- import { jsx as jsx7 } from "react/jsx-runtime";
1711
- function KeepItemStatusBadge({ status = "available", label, className, ...props }) {
1712
- const resolvedStatus = getDisplayStatus(status);
1713
- const statusLabel = useUiLabel(getStatusLabelKey(status));
1714
- return /* @__PURE__ */ jsx7(
1715
- "span",
1716
- {
1717
- ...props,
1718
- className,
1719
- "data-keepkit": "status-badge",
1720
- "data-status": status,
1721
- "data-item-status": resolvedStatus,
1722
- children: label ?? statusLabel
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 });
1723
1818
  }
1724
- );
1725
- }
1726
- function getStatusLabelKey(status) {
1727
- switch (status) {
1728
- case "available":
1729
- return "statusAvailable";
1730
- case "expired":
1731
- return "statusExpired";
1732
- case "removed":
1733
- return "statusRemoved";
1734
- case "deleted":
1735
- return "statusDeleted";
1736
- case "private":
1737
- return "statusPrivate";
1738
- case "unknown":
1739
- return "statusUnknown";
1740
- case "restricted":
1741
- return "statusPrivate";
1742
- }
1743
- }
1744
- function getDisplayStatus(status) {
1745
- if (status === "available") return "available";
1746
- if (status === "expired") return "expired";
1747
- if (status === "removed") return "removed";
1748
- return "restricted";
1819
+ };
1749
1820
  }
1750
-
1751
- // src/KeepStaleNotice.tsx
1752
- import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
1753
- function KeepStaleNotice({
1754
- item,
1755
- onRetry,
1756
- onRemoved,
1757
- retryLabel,
1758
- removeLabel,
1759
- children,
1760
- className,
1761
- ...props
1821
+ function useKeepUrlSync({
1822
+ enabled = true,
1823
+ query,
1824
+ onQueryChange,
1825
+ options = {},
1826
+ adapter: providedAdapter
1762
1827
  }) {
1763
- const context = useKeepContext2();
1764
- const retryText = useUiLabel("retry");
1765
- const removeText = useUiLabel("removeFromList");
1766
- const errorText = useUiLabel("error");
1767
- const [isRetrying, setIsRetrying] = useState3(false);
1768
- const [error, setError] = useState3(null);
1769
- const status = item.status && item.status !== "available" ? item.status : "unknown";
1770
- async function handleRetry() {
1771
- setError(null);
1772
- setIsRetrying(true);
1773
- try {
1774
- if (onRetry) await onRetry(item);
1775
- else await context.revalidateItems();
1776
- } catch (cause) {
1777
- setError(cause);
1778
- } finally {
1779
- setIsRetrying(false);
1780
- }
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";
1781
2044
  }
1782
- async function handleRemove() {
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() {
1783
2073
  try {
1784
2074
  await context.removeItemWithUndo(item.id);
1785
2075
  onRemoved?.(item);
@@ -1787,14 +2077,120 @@ function KeepStaleNotice({
1787
2077
  setError(cause);
1788
2078
  }
1789
2079
  }
1790
- return /* @__PURE__ */ jsxs4("aside", { ...props, className, "data-keepkit": "stale-notice", "data-state": error ? "error" : "stale", children: [
1791
- /* @__PURE__ */ jsx8(KeepItemStatusBadge, { status }),
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 }),
1792
2170
  children ?? (item.statusReason ? /* @__PURE__ */ jsx8("p", { children: item.statusReason }) : null),
1793
2171
  /* @__PURE__ */ jsxs4("div", { children: [
1794
- /* @__PURE__ */ jsx8("button", { type: "button", onClick: () => void handleRetry(), disabled: isRetrying || context.isMutating, children: retryLabel ?? retryText }),
1795
- /* @__PURE__ */ jsx8("button", { type: "button", onClick: () => void handleRemove(), disabled: context.isMutating, children: removeLabel ?? removeText })
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
+ )
1796
2192
  ] }),
1797
- error ? /* @__PURE__ */ jsx8("p", { role: "alert", children: error instanceof Error ? error.message : errorText }) : null
2193
+ view.error ? /* @__PURE__ */ jsx8("p", { role: "alert", children: view.error instanceof Error ? view.error.message : view.labels.error }) : null
1798
2194
  ] });
1799
2195
  }
1800
2196
  function KeepPruneStaleButton({
@@ -1805,14 +2201,7 @@ function KeepPruneStaleButton({
1805
2201
  disabled,
1806
2202
  ...props
1807
2203
  }) {
1808
- const context = useKeepContext2();
1809
- const defaultLabel = useUiLabel("pruneStale");
1810
- const staleIds = context.items.filter((item) => item.status && statuses.includes(item.status)).map((item) => item.id);
1811
- async function handlePrune() {
1812
- const ids = [...staleIds];
1813
- await context.removeItemsWithUndo(ids);
1814
- onPruned?.(ids);
1815
- }
2204
+ const view = useKeepPruneStale({ statuses, onPruned });
1816
2205
  return /* @__PURE__ */ jsx8(
1817
2206
  "button",
1818
2207
  {
@@ -1820,10 +2209,11 @@ function KeepPruneStaleButton({
1820
2209
  type: "button",
1821
2210
  className: props.className,
1822
2211
  "data-keepkit": "prune-stale",
1823
- "data-state": staleIds.length > 0 ? "available" : "empty",
1824
- disabled: staleIds.length === 0 || context.isMutating || disabled,
1825
- onClick: () => void handlePrune(),
1826
- children: children ?? label ?? `${defaultLabel} (${staleIds.length})`
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})`
1827
2217
  }
1828
2218
  );
1829
2219
  }
@@ -1859,76 +2249,63 @@ function KeepItemCard({
1859
2249
  className,
1860
2250
  ...rootProps
1861
2251
  }) {
1862
- const saveActionLabel = useUiLabel("save");
1863
- const savedAtLabel = useUiLabel("saved");
1864
- const errorLabel = useUiLabel("error");
1865
- const removeActionLabel = useUiLabel("remove");
1866
- const itemState = useKeepItem2(item);
1867
- const contentChildren = asChild && isValidElement2(children) ? void 0 : children;
1868
- const state = {
2252
+ const view = useKeepItemCard({
1869
2253
  item,
1870
- isSaved: itemState.isSaved,
1871
- isMutating: itemState.isMutating,
1872
- error: itemState.error,
1873
- remove: itemState.remove,
1874
- status: item.status
1875
- };
1876
- const resolvedTitle = typeof title === "function" ? title(item) : title ?? getTitle?.(item) ?? getMetaTitle(item.meta) ?? item.id;
1877
- const imageProps = getImageProps?.(item, resolvedTitle);
1878
- const href = typeof hrefOption === "function" ? hrefOption(item) : hrefOption;
1879
- const isAvailable = item.status === void 0 || item.status === "available";
1880
- const displayStatus = getDisplayStatus2(item.status);
1881
- const isExternalLink = href ? isExternalHref(href) : false;
1882
- const resolvedLinkTarget = linkTargetAttribute ?? (isExternalLink ? "_blank" : void 0);
1883
- const resolvedLinkRel = linkRel ?? (isExternalLink ? "noreferrer" : void 0);
1884
- const statusLabelKey = item.status && item.status !== "available" ? getStatusLabelKey2(item.status) : "statusUnknown";
1885
- const unavailableLabel = useUiLabel(statusLabelKey);
1886
- const tagsLabel = useUiLabel("tags");
1887
- const statusLabel = item.status && item.status !== "available" ? unavailableLabel : void 0;
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;
1888
2264
  function renderLink(content) {
1889
- if (!href || !isAvailable) return content;
2265
+ if (!view.href || !view.isAvailable) return content;
1890
2266
  const linkProps = {
1891
- href,
1892
- target: resolvedLinkTarget,
1893
- rel: resolvedLinkRel,
2267
+ href: view.href,
2268
+ target: view.resolvedLinkTarget,
2269
+ rel: view.resolvedLinkRel,
1894
2270
  onClick: (event) => onOpen?.(item, event),
1895
2271
  children: content
1896
2272
  };
1897
2273
  return LinkComponent ? /* @__PURE__ */ jsx9(LinkComponent, { ...linkProps }) : /* @__PURE__ */ jsx9("a", { ...linkProps });
1898
2274
  }
1899
- async function handleRemove() {
1900
- try {
1901
- await itemState.remove();
1902
- onRemoved?.(item);
1903
- } catch (error) {
1904
- onRemoveError?.(error);
1905
- }
1906
- }
1907
- const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? /* @__PURE__ */ jsxs5(Fragment3, { children: [
1908
- imageProps ? renderImage?.({ ...imageProps, alt: imageAlt ?? imageProps.alt }, item) ?? (ImageComponent ? /* @__PURE__ */ jsx9(ImageComponent, { ...imageProps, alt: imageAlt ?? imageProps.alt }) : /* @__PURE__ */ jsx9("img", { ...imageProps, alt: imageAlt ?? imageProps.alt })) : null,
1909
- /* @__PURE__ */ jsx9("h3", { children: linkTarget === "title" ? isAvailable ? renderLink(resolvedTitle) : href ? /* @__PURE__ */ jsx9("span", { "aria-disabled": "true", "data-link-disabled": "true", children: resolvedTitle }) : resolvedTitle : resolvedTitle }),
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 }),
1910
2278
  showSavedAt ? /* @__PURE__ */ jsxs5("div", { "data-card-meta": true, children: [
1911
2279
  /* @__PURE__ */ jsxs5("span", { children: [
1912
- savedAtLabel,
2280
+ view.labels.savedAt,
1913
2281
  ":"
1914
2282
  ] }),
1915
2283
  " ",
1916
2284
  /* @__PURE__ */ jsx9("time", { dateTime: new Date(item.savedAt).toISOString(), children: formatSavedAt(item.savedAt) })
1917
2285
  ] }) : null,
1918
- showTags && (item.tags?.length ?? 0) > 0 ? renderTags ? renderTags(item.tags ?? [], item) : /* @__PURE__ */ jsx9("ul", { "aria-label": tagsLabel, children: item.tags?.map((tag) => /* @__PURE__ */ jsx9("li", { children: tag }, tag)) }) : null,
1919
- itemState.error ? /* @__PURE__ */ jsx9("p", { role: "alert", children: getErrorMessage2(itemState.error, errorLabel) }) : null,
1920
- statusLabel ? /* @__PURE__ */ jsx9(KeepStaleNotice, { item, onRetry, onRemoved }) : null,
1921
- showSaveButton && !statusLabel ? /* @__PURE__ */ jsx9(
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(
1922
2290
  KeepButton,
1923
2291
  {
1924
2292
  item: toKeepButtonItem(item),
1925
2293
  labels: saveButtonLabels,
1926
- getAriaLabel: (buttonState) => `${buttonState.isSaved ? removeActionLabel : saveActionLabel} ${String(resolvedTitle)}`
2294
+ getAriaLabel: (buttonState) => `${buttonState.isSaved ? view.labels.remove : view.labels.save} ${String(view.resolvedTitle)}`
1927
2295
  }
1928
2296
  ) : null,
1929
- !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
1930
2307
  ] });
1931
- const linkedBody = linkTarget === "card" && isAvailable ? renderLink(body) : body;
2308
+ const linkedBody = linkTarget === "card" && view.isAvailable ? renderLink(body) : body;
1932
2309
  return renderRoot(
1933
2310
  asChild,
1934
2311
  isValidElement2(children) ? children : void 0,
@@ -1936,40 +2313,17 @@ function KeepItemCard({
1936
2313
  ...rootProps,
1937
2314
  className,
1938
2315
  "data-keepkit": "card",
1939
- "aria-busy": itemState.isMutating || rootProps["aria-busy"],
1940
- "aria-disabled": rootProps["aria-disabled"] ?? (!isAvailable ? "true" : void 0),
1941
- "data-state": itemState.error ? "error" : itemState.isSaved ? "saved" : "unsaved",
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",
1942
2319
  "data-status": item.status ?? "available",
1943
- "data-item-status": displayStatus,
1944
- "data-loading": itemState.isMutating ? "true" : void 0
2320
+ "data-item-status": view.displayStatus,
2321
+ "data-loading": view.itemState.isMutating ? "true" : void 0
1945
2322
  },
1946
2323
  linkedBody,
1947
2324
  "KeepItemCard"
1948
2325
  );
1949
2326
  }
1950
- function getStatusLabelKey2(status) {
1951
- switch (status) {
1952
- case "expired":
1953
- return "statusExpired";
1954
- case "removed":
1955
- return "statusRemoved";
1956
- case "deleted":
1957
- return "statusDeleted";
1958
- case "private":
1959
- return "statusPrivate";
1960
- default:
1961
- return "statusUnknown";
1962
- }
1963
- }
1964
- function getDisplayStatus2(status) {
1965
- if (status === void 0 || status === "available") return "available";
1966
- if (status === "expired") return "expired";
1967
- if (status === "removed") return "removed";
1968
- return "restricted";
1969
- }
1970
- function isExternalHref(href) {
1971
- return /^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(href);
1972
- }
1973
2327
  function formatSavedAt(timestamp) {
1974
2328
  return new Date(timestamp).toISOString().slice(0, 10);
1975
2329
  }
@@ -1998,16 +2352,14 @@ function KeepListContent({
1998
2352
  className,
1999
2353
  ...rootProps
2000
2354
  }) {
2001
- const defaultLoading = useUiLabel("loadingItems");
2002
- const defaultEmpty = useUiLabel("noItems");
2003
- const defaultError = useUiLabel("errorItems");
2004
- const state = useKeepList2(query);
2355
+ const view = useKeepListView(query);
2356
+ const { state } = view;
2005
2357
  const body = getListBody(state, {
2006
2358
  children,
2007
2359
  renderItem,
2008
- loading: loading ?? defaultLoading,
2009
- empty: empty ?? defaultEmpty,
2010
- error: errorContent ?? defaultError,
2360
+ loading: loading ?? view.labels.loading,
2361
+ empty: empty ?? view.labels.empty,
2362
+ error: errorContent ?? view.labels.error,
2011
2363
  itemCardProps,
2012
2364
  layout
2013
2365
  });
@@ -2045,8 +2397,39 @@ function getListBody(state, options) {
2045
2397
  }
2046
2398
 
2047
2399
  // src/KeepTagFilter.tsx
2048
- import { useKeepList as useKeepList3 } from "@keepkit/core/react";
2049
- import { isValidElement as isValidElement4, useCallback, useMemo as useMemo2, useState as useState4 } from "react";
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]
2424
+ );
2425
+ return {
2426
+ state,
2427
+ isLoading: list.isLoading,
2428
+ labels: { all: useUiLabel("allTags"), aria: useUiLabel("filterTags") }
2429
+ };
2430
+ }
2431
+
2432
+ // src/KeepTagFilter.tsx
2050
2433
  import { jsx as jsx11, jsxs as jsxs6 } from "react/jsx-runtime";
2051
2434
  function KeepTagFilter({
2052
2435
  query,
@@ -2063,38 +2446,38 @@ function KeepTagFilter({
2063
2446
  className,
2064
2447
  ...rootProps
2065
2448
  }) {
2066
- const uiAllLabel = useUiLabel("allTags");
2067
- const uiAriaLabel = useUiLabel("filterTags");
2068
- const [uncontrolledValue, setUncontrolledValue] = useState4(defaultValue);
2069
- const resolvedValue = controlledValue ?? uncontrolledValue;
2070
- const list = useKeepList3({
2071
- ...query,
2072
- tags: resolvedValue ? [...query?.tags ?? [], resolvedValue] : query?.tags
2073
- });
2074
- const select = useCallback(
2075
- (tag) => {
2076
- if (controlledValue === void 0) setUncontrolledValue(tag);
2077
- onChange?.(tag);
2078
- onValueChange?.(tag);
2079
- },
2080
- [controlledValue, onChange, onValueChange]
2081
- );
2082
- const state = useMemo2(
2083
- () => ({ tags: list.tags, tagCounts: list.tagCounts, value: resolvedValue, select }),
2084
- [list.tagCounts, list.tags, resolvedValue, select]
2085
- );
2449
+ const view = useKeepTagFilter({ query, controlledValue, defaultValue, onChange, onValueChange });
2086
2450
  const contentChildren = asChild && isValidElement4(children) ? void 0 : children;
2087
- const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? /* @__PURE__ */ jsxs6("fieldset", { children: [
2088
- /* @__PURE__ */ jsx11("legend", { children: ariaLabel ?? uiAriaLabel }),
2089
- /* @__PURE__ */ jsx11("button", { type: "button", "aria-pressed": resolvedValue === void 0, onClick: () => select(), children: allLabel ?? uiAllLabel }),
2090
- list.tags.map((tag) => /* @__PURE__ */ jsxs6("button", { type: "button", "aria-pressed": resolvedValue === tag, onClick: () => select(tag), children: [
2091
- renderTag ? renderTag(tag, list.tagCounts[tag] ?? 0, resolvedValue === tag) : tag,
2092
- /* @__PURE__ */ jsxs6("span", { children: [
2093
- " (",
2094
- list.tagCounts[tag] ?? 0,
2095
- ")"
2096
- ] })
2097
- ] }, tag))
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
+ ))
2098
2481
  ] });
2099
2482
  return renderRoot(
2100
2483
  asChild,
@@ -2103,16 +2486,87 @@ function KeepTagFilter({
2103
2486
  ...rootProps,
2104
2487
  className,
2105
2488
  "data-keepkit": "tag-filter",
2106
- "data-state": resolvedValue === void 0 ? "all" : "filtered",
2107
- "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
2108
2491
  },
2109
2492
  body,
2110
2493
  "KeepTagFilter"
2111
2494
  );
2112
2495
  }
2113
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
+
2114
2569
  // src/query-controls.tsx
2115
- import { useEffect, useState as useState5 } from "react";
2116
2570
  import { Fragment as Fragment4, jsx as jsx12, jsxs as jsxs7 } from "react/jsx-runtime";
2117
2571
  function KeepSearchInput({
2118
2572
  value: controlledValue,
@@ -2123,33 +2577,20 @@ function KeepSearchInput({
2123
2577
  placeholder,
2124
2578
  ...props
2125
2579
  }) {
2126
- const label = useUiLabel("search");
2127
- const [uncontrolledValue, setUncontrolledValue] = useState5(defaultValue);
2128
- const value = controlledValue ?? uncontrolledValue;
2129
- useEffect(() => {
2130
- if (!onValueChange) return;
2131
- if (debounceMs <= 0) {
2132
- onValueChange(value);
2133
- return;
2134
- }
2135
- const timer = window.setTimeout(() => onValueChange(value), debounceMs);
2136
- return () => window.clearTimeout(timer);
2137
- }, [debounceMs, onValueChange, value]);
2580
+ const view = useKeepSearchInput({ controlledValue, defaultValue, debounceMs, onValueChange });
2138
2581
  return /* @__PURE__ */ jsx12(
2139
2582
  "input",
2140
2583
  {
2141
2584
  ...props,
2142
2585
  "data-keepkit": "search-input",
2586
+ "data-keep-action": "search",
2143
2587
  type: "search",
2144
- value,
2145
- "data-state": value ? "active" : "idle",
2588
+ value: view.value,
2589
+ "data-state": view.value ? "active" : "idle",
2146
2590
  "data-disabled": props.disabled ? "true" : void 0,
2147
- "aria-label": ariaLabel ?? label,
2148
- placeholder: placeholder ?? label,
2149
- onChange: (event) => {
2150
- const nextValue = event.currentTarget.value;
2151
- if (controlledValue === void 0) setUncontrolledValue(nextValue);
2152
- }
2591
+ "aria-label": ariaLabel ?? view.label,
2592
+ placeholder: placeholder ?? view.label,
2593
+ onChange: view.change
2153
2594
  }
2154
2595
  );
2155
2596
  }
@@ -2161,34 +2602,24 @@ function KeepSortSelect({
2161
2602
  children,
2162
2603
  ...props
2163
2604
  }) {
2164
- const label = useUiLabel("sort");
2165
- const updatedNewestLabel = useUiLabel("updatedNewest");
2166
- const updatedOldestLabel = useUiLabel("updatedOldest");
2167
- const savedNewestLabel = useUiLabel("savedNewest");
2168
- const savedOldestLabel = useUiLabel("savedOldest");
2169
- const [uncontrolledValue, setUncontrolledValue] = useState5(defaultValue);
2170
- const value = controlledValue ?? uncontrolledValue;
2605
+ const view = useKeepSortSelect({ controlledValue, defaultValue, onValueChange });
2171
2606
  const options = children ?? /* @__PURE__ */ jsxs7(Fragment4, { children: [
2172
- /* @__PURE__ */ jsx12("option", { value: "updatedAt:desc", children: updatedNewestLabel }),
2173
- /* @__PURE__ */ jsx12("option", { value: "updatedAt:asc", children: updatedOldestLabel }),
2174
- /* @__PURE__ */ jsx12("option", { value: "savedAt:desc", children: savedNewestLabel }),
2175
- /* @__PURE__ */ jsx12("option", { value: "savedAt:asc", children: savedOldestLabel })
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 })
2176
2611
  ] });
2177
2612
  return /* @__PURE__ */ jsx12(
2178
2613
  "select",
2179
2614
  {
2180
2615
  ...props,
2181
2616
  "data-keepkit": "sort-select",
2182
- value,
2617
+ "data-keep-action": "sort",
2618
+ value: view.value,
2183
2619
  "data-state": "selected",
2184
2620
  "data-disabled": props.disabled ? "true" : void 0,
2185
- "aria-label": ariaLabel ?? label,
2186
- onChange: (event) => {
2187
- const nextValue = event.currentTarget.value;
2188
- if (controlledValue === void 0) setUncontrolledValue(nextValue);
2189
- const [by, direction] = nextValue.split(":");
2190
- onValueChange?.(nextValue, { by, direction });
2191
- },
2621
+ "aria-label": ariaLabel ?? view.labels.sort,
2622
+ onChange: view.change,
2192
2623
  children: options
2193
2624
  }
2194
2625
  );
@@ -2202,128 +2633,50 @@ function KeepPagination({
2202
2633
  render,
2203
2634
  ...props
2204
2635
  }) {
2205
- const previousPageLabel = useUiLabel("previousPage");
2206
- const nextPageLabel = useUiLabel("nextPage");
2207
- const pageLabel = useUiLabel("page");
2208
- const paginationLabel = useUiLabel("pagination");
2209
- const pageCount = Math.max(1, Math.ceil(totalCount / Math.max(1, pageSize)));
2210
- const currentPage = Math.min(Math.max(1, page), pageCount);
2211
- const goToPage = (nextPage) => {
2212
- const next = Math.min(Math.max(1, nextPage), pageCount);
2213
- onPageChange?.(next, (next - 1) * pageSize);
2214
- };
2636
+ const view = useKeepPagination({ totalCount, pageSize, page, maxPageButtons, onPageChange });
2215
2637
  const navProps = {
2216
2638
  ...props,
2217
2639
  "data-keepkit": "pagination",
2218
- "aria-label": props["aria-label"] ?? paginationLabel,
2219
- "data-state": pageCount > 1 ? "active" : "idle"
2640
+ "aria-label": props["aria-label"] ?? view.labels.pagination,
2641
+ "data-state": view.pageCount > 1 ? "active" : "idle"
2220
2642
  };
2221
- if (render) return /* @__PURE__ */ jsx12("nav", { ...navProps, children: render({ page: currentPage, pageCount, goToPage }) });
2222
- 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 }) });
2223
2645
  return /* @__PURE__ */ jsxs7("nav", { ...navProps, children: [
2224
- /* @__PURE__ */ jsx12("button", { type: "button", onClick: () => goToPage(currentPage - 1), disabled: currentPage <= 1, children: previousPageLabel }),
2225
- visiblePages.map((nextPage) => /* @__PURE__ */ jsx12(
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(
2226
2657
  "button",
2227
2658
  {
2228
2659
  type: "button",
2229
- "aria-current": nextPage === currentPage ? "page" : void 0,
2230
- "aria-label": `${pageLabel} ${nextPage}`,
2231
- 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),
2232
2664
  children: nextPage
2233
2665
  },
2234
2666
  nextPage
2235
2667
  )),
2236
- /* @__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
+ )
2237
2678
  ] });
2238
2679
  }
2239
- function getVisiblePages(currentPage, pageCount, maxPageButtons) {
2240
- if (pageCount <= maxPageButtons) return Array.from({ length: pageCount }, (_, index) => index + 1);
2241
- const half = Math.floor(maxPageButtons / 2);
2242
- const start = Math.min(Math.max(1, currentPage - half), pageCount - maxPageButtons + 1);
2243
- return Array.from({ length: maxPageButtons }, (_, index) => start + index);
2244
- }
2245
-
2246
- // src/url-sync.tsx
2247
- import {
2248
- DEFAULT_KEEP_URL_PARAMS,
2249
- decodeKeepListQuery,
2250
- encodeKeepListQuery
2251
- } from "@keepkit/core/core";
2252
- import { useEffect as useEffect2, useRef as useRef2 } from "react";
2253
- function createNextPagesRouterAdapter(router) {
2254
- const getUrl = () => router.asPath ?? (typeof window === "undefined" ? "/" : window.location.href);
2255
- return {
2256
- getUrl,
2257
- subscribe: router.events ? (listener) => {
2258
- router.events?.on("routeChangeComplete", listener);
2259
- return () => router.events?.off("routeChangeComplete", listener);
2260
- } : void 0,
2261
- navigate: (url, mode) => {
2262
- void router[mode](url, void 0, { shallow: true });
2263
- }
2264
- };
2265
- }
2266
- function useKeepUrlSync({
2267
- enabled = true,
2268
- query,
2269
- onQueryChange,
2270
- options = {},
2271
- adapter: providedAdapter
2272
- }) {
2273
- const browserAdapterRef = useRef2(getBrowserAdapter());
2274
- const adapter = providedAdapter ?? browserAdapterRef.current;
2275
- const onQueryChangeRef = useRef2(onQueryChange);
2276
- onQueryChangeRef.current = onQueryChange;
2277
- const skipWriteRef = useRef2(true);
2278
- const params = options.params;
2279
- useEffect2(() => {
2280
- if (!enabled) return;
2281
- const read = () => {
2282
- const url = adapter.getUrl();
2283
- const decoded = decodeKeepListQuery(url, { params });
2284
- skipWriteRef.current = true;
2285
- onQueryChangeRef.current((previousQuery) => ({
2286
- ...previousQuery,
2287
- ...decoded.search ? { search: decoded.search } : { search: void 0 },
2288
- ...decoded.tags ? { tags: decoded.tags } : { tags: void 0 },
2289
- ...decoded.sort ? { sort: decoded.sort } : {},
2290
- ...decoded.pagination ? { pagination: { ...previousQuery.pagination, ...decoded.pagination } } : {}
2291
- }));
2292
- };
2293
- read();
2294
- return adapter.subscribe?.(read);
2295
- }, [adapter, enabled, params]);
2296
- useEffect2(() => {
2297
- if (!enabled) return;
2298
- if (skipWriteRef.current) {
2299
- skipWriteRef.current = false;
2300
- return;
2301
- }
2302
- const currentUrl = new URL(adapter.getUrl(), "http://keepkit.invalid");
2303
- const urlParams = { ...DEFAULT_KEEP_URL_PARAMS, ...params };
2304
- for (const key of Object.values(urlParams)) currentUrl.searchParams.delete(key);
2305
- const nextParams = encodeKeepListQuery(query, { params });
2306
- nextParams.forEach((value, key) => {
2307
- currentUrl.searchParams.append(key, value);
2308
- });
2309
- const nextUrl = `${currentUrl.pathname}${currentUrl.search}${currentUrl.hash}`;
2310
- adapter.navigate(nextUrl, options.history ?? "push");
2311
- }, [adapter, enabled, options.history, params, query]);
2312
- }
2313
- function getBrowserAdapter() {
2314
- return {
2315
- getUrl: () => typeof window === "undefined" ? "/" : window.location.href,
2316
- subscribe: (listener) => {
2317
- if (typeof window === "undefined") return () => void 0;
2318
- window.addEventListener("popstate", listener);
2319
- return () => window.removeEventListener("popstate", listener);
2320
- },
2321
- navigate: (url, mode) => {
2322
- if (typeof window === "undefined") return;
2323
- window.history[mode === "push" ? "pushState" : "replaceState"]({}, "", url);
2324
- }
2325
- };
2326
- }
2327
2680
 
2328
2681
  // src/KeepCollection.tsx
2329
2682
  import { jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
@@ -2348,43 +2701,7 @@ function KeepCollectionContent({
2348
2701
  className,
2349
2702
  ...rootProps
2350
2703
  }) {
2351
- const enabled = {
2352
- search: true,
2353
- sort: true,
2354
- pagination: true,
2355
- tagFilter: false,
2356
- bulkActions: false,
2357
- ...features
2358
- };
2359
- const [searchValue, setSearchValue] = useState6(query.search?.query ?? "");
2360
- const [sort, setSort] = useState6(query.sort ?? { by: "updatedAt", direction: "desc" });
2361
- const [tag, setTag] = useState6(query.tags?.[0]);
2362
- const [page, setPage] = useState6(query.pagination?.page ?? 1);
2363
- const resolvedPageSize = query.pagination?.pageSize ?? pageSize;
2364
- const resolvedQuery = useMemo3(
2365
- () => ({
2366
- ...query,
2367
- search: enabled.search ? { ...query.search, query: searchValue } : query.search,
2368
- sort: enabled.sort ? sort : query.sort,
2369
- tags: tag ? [.../* @__PURE__ */ new Set([...query.tags ?? [], tag])] : query.tags,
2370
- pagination: enabled.pagination ? { ...query.pagination, page, pageSize: resolvedPageSize } : query.pagination
2371
- }),
2372
- [enabled.pagination, enabled.search, enabled.sort, page, query, resolvedPageSize, searchValue, sort, tag]
2373
- );
2374
- useKeepUrlSync({
2375
- enabled: Boolean(urlSync),
2376
- query: resolvedQuery,
2377
- onQueryChange: (nextOrUpdater) => {
2378
- const next = typeof nextOrUpdater === "function" ? nextOrUpdater(resolvedQuery) : nextOrUpdater;
2379
- setSearchValue(next.search?.query ?? "");
2380
- setSort(next.sort ?? { by: "updatedAt", direction: "desc" });
2381
- setTag(next.tags?.[0]);
2382
- setPage(next.pagination?.page ?? 1);
2383
- },
2384
- options: typeof urlSync === "object" ? urlSync : {},
2385
- adapter: urlAdapter
2386
- });
2387
- const list = useKeepList4(resolvedQuery);
2704
+ const view = useKeepCollection({ query, pageSize, urlSync, urlAdapter, features });
2388
2705
  return /* @__PURE__ */ jsxs8(
2389
2706
  "section",
2390
2707
  {
@@ -2392,47 +2709,19 @@ function KeepCollectionContent({
2392
2709
  className,
2393
2710
  "data-keepkit": "collection",
2394
2711
  "data-layout": layout,
2395
- "aria-busy": list.isLoading || list.isMutating || rootProps["aria-busy"],
2396
- "data-state": getCollectionState(list),
2397
- "data-loading": list.isLoading || list.isMutating ? "true" : void 0,
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,
2398
2715
  children: [
2399
2716
  /* @__PURE__ */ jsxs8("div", { children: [
2400
- enabled.search ? /* @__PURE__ */ jsx13(
2401
- KeepSearchInput,
2402
- {
2403
- value: searchValue,
2404
- onValueChange: (value) => {
2405
- setSearchValue(value);
2406
- setPage(1);
2407
- }
2408
- }
2409
- ) : null,
2410
- enabled.sort ? /* @__PURE__ */ jsx13(
2411
- KeepSortSelect,
2412
- {
2413
- value: sortToValue(sort),
2414
- onValueChange: (_value, nextSort) => {
2415
- setSort(nextSort);
2416
- setPage(1);
2417
- }
2418
- }
2419
- ) : null,
2420
- enabled.tagFilter ? /* @__PURE__ */ jsx13(
2421
- KeepTagFilter,
2422
- {
2423
- query,
2424
- value: tag,
2425
- onValueChange: (value) => {
2426
- setTag(value);
2427
- setPage(1);
2428
- }
2429
- }
2430
- ) : null
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
2431
2720
  ] }),
2432
2721
  /* @__PURE__ */ jsx13(
2433
2722
  KeepList,
2434
2723
  {
2435
- query: resolvedQuery,
2724
+ query: view.resolvedQuery,
2436
2725
  renderItem,
2437
2726
  itemCardProps,
2438
2727
  layout,
@@ -2441,16 +2730,16 @@ function KeepCollectionContent({
2441
2730
  error
2442
2731
  }
2443
2732
  ),
2444
- enabled.pagination ? /* @__PURE__ */ jsx13(
2733
+ view.enabled.pagination ? /* @__PURE__ */ jsx13(
2445
2734
  KeepPagination,
2446
2735
  {
2447
- totalCount: list.totalCount,
2448
- pageSize: resolvedPageSize,
2449
- page: list.page,
2450
- onPageChange: (nextPage) => setPage(nextPage)
2736
+ totalCount: view.list.totalCount,
2737
+ pageSize: view.resolvedPageSize,
2738
+ page: view.list.page,
2739
+ onPageChange: view.setPage
2451
2740
  }
2452
2741
  ) : null,
2453
- enabled.bulkActions ? /* @__PURE__ */ jsx13(KeepBulkActions, { query: resolvedQuery }) : null
2742
+ view.enabled.bulkActions ? /* @__PURE__ */ jsx13(KeepBulkActions, { query: view.resolvedQuery }) : null
2454
2743
  ]
2455
2744
  }
2456
2745
  );
@@ -2459,45 +2748,24 @@ function getCollectionState(list) {
2459
2748
  if (list.error && list.items.length === 0) return "error";
2460
2749
  if (list.isLoading && !list.isHydrated) return "loading";
2461
2750
  if (list.isHydrated && list.items.length === 0) return "empty";
2462
- return "ready";
2463
- }
2464
-
2465
- // src/KeepLayout.tsx
2466
- import { jsx as jsx14 } from "react/jsx-runtime";
2467
- function KeepLayout({ layout = "list", children, ...props }) {
2468
- return /* @__PURE__ */ jsx14("div", { ...props, "data-keepkit": "layout", "data-layout": layout, children });
2469
- }
2470
-
2471
- // src/KeepNoteEditor.tsx
2472
- import { useKeepItem as useKeepItem3 } from "@keepkit/core/react";
2473
- import {
2474
- isValidElement as isValidElement5,
2475
- useCallback as useCallback2,
2476
- useEffect as useEffect3,
2477
- useRef as useRef3,
2478
- useState as useState7
2479
- } from "react";
2480
- import { Fragment as Fragment5, jsx as jsx15, jsxs as jsxs9 } from "react/jsx-runtime";
2481
- function KeepNoteEditor({
2482
- item,
2483
- label,
2484
- saveLabel,
2485
- placeholder,
2486
- debounceMs = 300,
2487
- onSaved,
2488
- onSaveError,
2489
- render,
2490
- children,
2491
- asChild = false,
2492
- className,
2493
- ...formProps
2494
- }) {
2495
- const defaultLabel = useUiLabel("note");
2496
- const defaultSaveLabel = useUiLabel("saveNote");
2497
- const errorLabel = useUiLabel("error");
2751
+ return "ready";
2752
+ }
2753
+
2754
+ // src/KeepLayout.tsx
2755
+ import { jsx as jsx14 } from "react/jsx-runtime";
2756
+ function KeepLayout({ layout = "list", children, ...props }) {
2757
+ return /* @__PURE__ */ jsx14("div", { ...props, "data-keepkit": "layout", "data-layout": layout, children });
2758
+ }
2759
+
2760
+ // src/KeepNoteEditor.tsx
2761
+ import { isValidElement as isValidElement5 } from "react";
2762
+
2763
+ // src/hooks/useKeepNoteEditor.ts
2764
+ import { useKeepItem as useKeepItem3 } from "@keepkit/core/react";
2765
+ import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef3, useState as useState7 } from "react";
2766
+ function useKeepNoteEditor({ item, debounceMs, onSaved, onSaveError }) {
2498
2767
  const itemState = useKeepItem3(item);
2499
2768
  const { error, isMutating, item: savedItem, updateNote } = itemState;
2500
- const contentChildren = asChild && isValidElement5(children) ? void 0 : children;
2501
2769
  const [note, setNote] = useState7(item.note ?? "");
2502
2770
  const baselineNote = savedItem?.note ?? item.note ?? "";
2503
2771
  const isDirty = note !== baselineNote;
@@ -2509,9 +2777,9 @@ function KeepNoteEditor({
2509
2777
  await updateNote(nextNote);
2510
2778
  lastSavedNoteRef.current = note;
2511
2779
  onSaved?.(nextNote);
2512
- } catch (error2) {
2513
- onSaveError?.(error2);
2514
- throw error2;
2780
+ } catch (cause) {
2781
+ onSaveError?.(cause);
2782
+ throw cause;
2515
2783
  }
2516
2784
  }, [note, onSaveError, onSaved, updateNote]);
2517
2785
  useEffect3(() => {
@@ -2528,31 +2796,62 @@ function KeepNoteEditor({
2528
2796
  error,
2529
2797
  save
2530
2798
  };
2531
- 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: [
2532
2839
  /* @__PURE__ */ jsxs9("label", { children: [
2533
- label ?? defaultLabel,
2840
+ label ?? view.labels.note,
2534
2841
  /* @__PURE__ */ jsx15(
2535
2842
  "textarea",
2536
2843
  {
2844
+ "data-keep-action": "edit-note",
2537
2845
  value: note,
2538
2846
  onChange: (event) => setNote(event.currentTarget.value),
2539
2847
  placeholder,
2540
- disabled: isMutating,
2541
- onKeyDown: (event) => {
2542
- if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
2543
- event.preventDefault();
2544
- void save().catch(() => void 0);
2545
- }
2546
- }
2848
+ disabled: isSaving,
2849
+ onKeyDown: view.handleKeyDown
2547
2850
  }
2548
2851
  )
2549
2852
  ] }),
2550
- /* @__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 })
2551
2854
  ] });
2552
- const handleSubmit = (event) => {
2553
- event.preventDefault();
2554
- void save().catch(() => void 0);
2555
- };
2556
2855
  if (!asChild) {
2557
2856
  return /* @__PURE__ */ jsxs9(
2558
2857
  "form",
@@ -2560,14 +2859,14 @@ function KeepNoteEditor({
2560
2859
  ...formProps,
2561
2860
  className,
2562
2861
  "data-keepkit": "note-editor",
2563
- onSubmit: handleSubmit,
2564
- "aria-busy": isMutating || formProps["aria-busy"],
2862
+ onSubmit: view.submit,
2863
+ "aria-busy": isSaving || formProps["aria-busy"],
2565
2864
  "data-state": error ? "error" : isDirty ? "dirty" : "clean",
2566
- "data-loading": isMutating ? "true" : void 0,
2567
- "data-disabled": isMutating ? "true" : void 0,
2865
+ "data-loading": isSaving ? "true" : void 0,
2866
+ "data-disabled": isSaving ? "true" : void 0,
2568
2867
  children: [
2569
2868
  body,
2570
- 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
2571
2870
  ]
2572
2871
  }
2573
2872
  );
@@ -2579,11 +2878,11 @@ function KeepNoteEditor({
2579
2878
  ...formProps,
2580
2879
  className,
2581
2880
  "data-keepkit": "note-editor",
2582
- onSubmit: handleSubmit,
2583
- "aria-busy": isMutating || formProps["aria-busy"],
2881
+ onSubmit: view.submit,
2882
+ "aria-busy": isSaving || formProps["aria-busy"],
2584
2883
  "data-state": error ? "error" : isDirty ? "dirty" : "clean",
2585
- "data-loading": isMutating ? "true" : void 0,
2586
- "data-disabled": isMutating ? "true" : void 0
2884
+ "data-loading": isSaving ? "true" : void 0,
2885
+ "data-disabled": isSaving ? "true" : void 0
2587
2886
  },
2588
2887
  body,
2589
2888
  "KeepNoteEditor"
@@ -2593,9 +2892,62 @@ function getErrorMessage3(error, fallback) {
2593
2892
  return error instanceof Error ? error.message : fallback;
2594
2893
  }
2595
2894
 
2596
- // src/KeepSyncRecoveryDialog.tsx
2895
+ // src/hooks/useKeepSyncRecoveryDialog.ts
2597
2896
  import { useKeepContext as useKeepContext3 } from "@keepkit/core/react";
2598
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
2599
2951
  import { jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
2600
2952
  function KeepSyncRecoveryDialog({
2601
2953
  open,
@@ -2609,48 +2961,8 @@ function KeepSyncRecoveryDialog({
2609
2961
  className,
2610
2962
  ...props
2611
2963
  }) {
2612
- const context = useKeepContext3();
2613
- const closeLabel = useUiLabel("close");
2614
- const defaultDialogTitle = useUiLabel("resolveSync");
2615
- const dialogTitle = title ?? defaultDialogTitle;
2616
- const conflictLabel = useUiLabel("syncConflict");
2617
- const keepLocalLabel = useUiLabel("keepLocal");
2618
- const useServerLabel = useUiLabel("useServer");
2619
- const manualMergeLabel = useUiLabel("manualMerge");
2620
- const localVersionLabel = useUiLabel("localVersion");
2621
- const remoteVersionLabel = useUiLabel("remoteVersion");
2622
- const updatedAtLabel = useUiLabel("updatedAt");
2623
- const noteLabel = useUiLabel("note");
2624
- const backupRecoveryLabel = useUiLabel("backupRecovery");
2625
- const backupRecoveryDescription = useUiLabel("backupRecoveryDescription");
2626
- const errorLabel = useUiLabel("error");
2627
- const conflictList = conflicts ?? context.syncState.conflicts ?? [];
2628
- const hasRecovery = conflictList.length > 0 || context.syncState.status === "error" || Boolean(context.error);
2629
- const [dismissed, setDismissed] = useState8(false);
2630
- const [busyId, setBusyId] = useState8();
2631
- const [error, setError] = useState8();
2632
- useEffect4(() => {
2633
- if (hasRecovery) setDismissed(false);
2634
- }, [hasRecovery]);
2635
- const isOpen = open ?? (hasRecovery && !dismissed);
2636
- if (!isOpen) return null;
2637
- const close = () => {
2638
- setDismissed(true);
2639
- onOpenChange?.(false);
2640
- };
2641
- async function resolve(conflict, resolution) {
2642
- setError(void 0);
2643
- setBusyId(conflict.id);
2644
- try {
2645
- const merged = resolution === "manual" ? await onManualMerge?.(conflict) : void 0;
2646
- if (resolution === "manual" && !merged) throw new Error("A manual merge result is required.");
2647
- await context.resolveSyncConflict(conflict.id, resolution, merged);
2648
- } catch (cause) {
2649
- setError(cause);
2650
- } finally {
2651
- setBusyId(void 0);
2652
- }
2653
- }
2964
+ const view = useKeepSyncRecoveryDialog({ open, onOpenChange, conflicts, onManualMerge });
2965
+ if (!view.isOpen) return null;
2654
2966
  return /* @__PURE__ */ jsxs10(
2655
2967
  "section",
2656
2968
  {
@@ -2659,29 +2971,29 @@ function KeepSyncRecoveryDialog({
2659
2971
  role: "dialog",
2660
2972
  "aria-modal": "true",
2661
2973
  "aria-labelledby": "keepkit-sync-recovery-title",
2662
- "aria-describedby": error ? "keepkit-sync-recovery-error" : void 0,
2663
- "aria-busy": busyId !== void 0,
2974
+ "aria-describedby": view.error ? "keepkit-sync-recovery-error" : void 0,
2975
+ "aria-busy": view.busyId !== void 0,
2664
2976
  "data-keepkit": "sync-recovery",
2665
- "data-state": conflictList.length > 0 ? "conflict" : "error",
2666
- "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,
2667
2979
  children: [
2668
2980
  /* @__PURE__ */ jsxs10("header", { children: [
2669
- /* @__PURE__ */ jsx16("h2", { id: "keepkit-sync-recovery-title", children: dialogTitle }),
2670
- /* @__PURE__ */ jsx16("button", { type: "button", onClick: close, "aria-label": closeLabel, children: closeLabel })
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 })
2671
2983
  ] }),
2672
2984
  children,
2673
- conflictList.length > 0 ? /* @__PURE__ */ jsxs10("div", { children: [
2674
- /* @__PURE__ */ jsx16("p", { children: conflictLabel }),
2675
- conflictList.map((conflict) => /* @__PURE__ */ jsxs10("article", { "data-conflict-id": conflict.id, children: [
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: [
2676
2988
  /* @__PURE__ */ jsx16("h3", { children: getMetaTitle(conflict.operation.item?.meta) ?? conflict.id }),
2677
2989
  /* @__PURE__ */ jsxs10("div", { "data-conflict-preview": true, children: [
2678
2990
  /* @__PURE__ */ jsx16(
2679
2991
  ConflictPreview,
2680
2992
  {
2681
2993
  item: conflict.operation.item,
2682
- heading: localVersionLabel,
2683
- updatedAtLabel,
2684
- noteLabel,
2994
+ heading: view.labels.localVersion,
2995
+ updatedAtLabel: view.labels.updatedAt,
2996
+ noteLabel: view.labels.note,
2685
2997
  side: "local"
2686
2998
  }
2687
2999
  ),
@@ -2689,34 +3001,53 @@ function KeepSyncRecoveryDialog({
2689
3001
  ConflictPreview,
2690
3002
  {
2691
3003
  item: conflict.remote,
2692
- heading: remoteVersionLabel,
2693
- updatedAtLabel,
2694
- noteLabel,
3004
+ heading: view.labels.remoteVersion,
3005
+ updatedAtLabel: view.labels.updatedAt,
3006
+ noteLabel: view.labels.note,
2695
3007
  side: "remote"
2696
3008
  }
2697
3009
  )
2698
3010
  ] }),
2699
3011
  /* @__PURE__ */ jsxs10("div", { children: [
2700
- /* @__PURE__ */ jsx16("button", { type: "button", onClick: () => void resolve(conflict, "local"), disabled: busyId !== void 0, children: keepLocalLabel }),
2701
- /* @__PURE__ */ jsx16("button", { type: "button", onClick: () => void resolve(conflict, "remote"), disabled: busyId !== void 0, children: useServerLabel }),
2702
3012
  /* @__PURE__ */ jsx16(
2703
3013
  "button",
2704
3014
  {
2705
3015
  type: "button",
2706
- onClick: () => void resolve(conflict, "manual"),
2707
- disabled: busyId !== void 0 || !onManualMerge,
2708
- 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
2709
3040
  }
2710
3041
  )
2711
3042
  ] })
2712
3043
  ] }, conflict.id))
2713
3044
  ] }) : null,
2714
- context.syncState.status === "error" || context.error ? /* @__PURE__ */ jsxs10("section", { "data-recovery": "backup", children: [
2715
- /* @__PURE__ */ jsx16("h3", { children: backupRecoveryLabel }),
2716
- /* @__PURE__ */ jsx16("p", { children: backupRecoveryDescription }),
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 }),
2717
3048
  backup ?? (showBackupControls ? /* @__PURE__ */ jsx16(KeepBackup, {}) : null)
2718
3049
  ] }) : null,
2719
- error ? /* @__PURE__ */ jsx16("p", { id: "keepkit-sync-recovery-error", role: "alert", "aria-live": "assertive", 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
2720
3051
  ]
2721
3052
  }
2722
3053
  );
@@ -2746,16 +3077,9 @@ function formatConflictDate(timestamp) {
2746
3077
  return new Date(timestamp).toISOString().slice(0, 10);
2747
3078
  }
2748
3079
 
2749
- // src/KeepSyncStatusBanner.tsx
3080
+ // src/hooks/useKeepSyncStatusBanner.ts
2750
3081
  import { useKeepContext as useKeepContext4 } from "@keepkit/core/react";
2751
- import { jsx as jsx17, jsxs as jsxs11 } from "react/jsx-runtime";
2752
- function KeepSyncStatusBanner({
2753
- onRetry,
2754
- onResolveConflicts,
2755
- children,
2756
- className,
2757
- ...props
2758
- }) {
3082
+ function useKeepSyncStatusBanner({ onRetry, children }) {
2759
3083
  const context = useKeepContext4();
2760
3084
  const retryLabel = useUiLabel("retrySync");
2761
3085
  const resolveLabel = useUiLabel("resolveSync");
@@ -2764,53 +3088,80 @@ function KeepSyncStatusBanner({
2764
3088
  const syncedLabel = useUiLabel("syncSynced");
2765
3089
  const status = context.syncState.status;
2766
3090
  const hasConflicts = (context.syncState.conflicts?.length ?? 0) > 0 || context.syncState.conflictIds.length > 0;
2767
- if (status === "idle" && !hasConflicts) return null;
2768
3091
  const message = children ?? (status === "error" ? getErrorMessage4(context.syncState.error) : status === "conflict" || hasConflicts ? conflictLabel : status === "pending" || status === "syncing" ? pendingLabel : syncedLabel);
2769
- const role = status === "error" || status === "conflict" || hasConflicts ? "alert" : "status";
2770
- const retry = async () => {
2771
- if (onRetry) {
2772
- await onRetry();
2773
- return;
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();
2774
3107
  }
2775
- await context.flushSync();
2776
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;
2777
3125
  return /* @__PURE__ */ jsxs11(
2778
3126
  "aside",
2779
3127
  {
2780
3128
  ...props,
2781
3129
  className,
2782
- role: props.role ?? role,
3130
+ role: props.role ?? view.role,
2783
3131
  "aria-live": props["aria-live"] ?? "polite",
2784
3132
  "data-keepkit": "sync-status",
2785
- "data-state": status,
3133
+ "data-state": view.status,
2786
3134
  children: [
2787
- /* @__PURE__ */ jsx17("p", { children: message }),
2788
- status === "error" || status === "pending" || status === "syncing" ? /* @__PURE__ */ jsx17("button", { type: "button", onClick: () => void retry(), disabled: context.isMutating, children: retryLabel }) : null,
2789
- hasConflicts ? /* @__PURE__ */ jsx17("button", { type: "button", onClick: onResolveConflicts, disabled: !onResolveConflicts, children: resolveLabel }) : null
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
2790
3156
  ]
2791
3157
  }
2792
3158
  );
2793
3159
  }
2794
- function getErrorMessage4(error) {
2795
- return error instanceof Error ? error.message : "Sync failed.";
2796
- }
2797
3160
 
2798
- // src/KeepTagEditor.tsx
3161
+ // src/hooks/useKeepTagEditor.ts
2799
3162
  import { useKeepItem as useKeepItem4 } from "@keepkit/core/react";
2800
3163
  import { useCallback as useCallback3, useEffect as useEffect5, useState as useState9 } from "react";
2801
- import { Fragment as Fragment6, jsx as jsx18, jsxs as jsxs12 } from "react/jsx-runtime";
2802
- function KeepTagEditor({
2803
- item,
2804
- availableTags = [],
2805
- onSaved,
2806
- onSaveError,
2807
- render,
2808
- ...props
2809
- }) {
2810
- const tagsLabel = useUiLabel("tagsToApply");
2811
- const removeLabel = useUiLabel("remove");
2812
- const applyTagsLabel = useUiLabel("applyTags");
2813
- const errorLabel = useUiLabel("error");
3164
+ function useKeepTagEditor({ item, onSaved, onSaveError }) {
2814
3165
  const itemState = useKeepItem4(item);
2815
3166
  const [tags, setTags] = useState9(item.tags ?? []);
2816
3167
  const [input, setInput] = useState9("");
@@ -2821,60 +3172,91 @@ function KeepTagEditor({
2821
3172
  await itemState.updateTags(nextTags);
2822
3173
  setTags(nextTags);
2823
3174
  onSaved?.(nextTags);
2824
- } catch (error) {
2825
- onSaveError?.(error);
2826
- throw error;
3175
+ } catch (cause) {
3176
+ onSaveError?.(cause);
3177
+ throw cause;
2827
3178
  }
2828
3179
  }, [itemState, onSaveError, onSaved, tags]);
2829
3180
  const addTag = (tag) => {
2830
3181
  setTags(normalizeUiTags([...tags, tag]));
2831
3182
  setInput("");
2832
3183
  };
2833
- 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: [
2834
3227
  /* @__PURE__ */ jsxs12("label", { children: [
2835
- tagsLabel,
3228
+ view.labels.tags,
2836
3229
  /* @__PURE__ */ jsx18(
2837
3230
  "input",
2838
3231
  {
2839
- value: input,
3232
+ "data-keep-action": "edit-tags",
3233
+ value: view.input,
2840
3234
  list: availableTags.length > 0 ? `keep-tags-${item.id}` : void 0,
2841
- onChange: (event) => setInput(event.currentTarget.value),
2842
- onKeyDown: (event) => {
2843
- if (event.key === "Enter") {
2844
- if (event.nativeEvent.isComposing) return;
2845
- event.preventDefault();
2846
- if (input.trim()) addTag(input);
2847
- } else if (event.key === "Backspace" && input.length === 0 && tags.length > 0) {
2848
- event.preventDefault();
2849
- setTags(tags.slice(0, -1));
2850
- }
2851
- }
3235
+ onChange: (event) => view.setInput(event.currentTarget.value),
3236
+ onKeyDown: view.handleInputKeyDown
2852
3237
  }
2853
3238
  )
2854
3239
  ] }),
2855
3240
  availableTags.length > 0 ? /* @__PURE__ */ jsx18("datalist", { id: `keep-tags-${item.id}`, children: availableTags.map((tag) => /* @__PURE__ */ jsx18("option", { value: tag }, tag)) }) : null,
2856
- /* @__PURE__ */ jsx18("ul", { "aria-label": tagsLabel, children: tags.map((tag) => /* @__PURE__ */ jsxs12("li", { children: [
3241
+ /* @__PURE__ */ jsx18("ul", { "aria-label": view.labels.tags, children: tags.map((tag) => /* @__PURE__ */ jsxs12("li", { children: [
2857
3242
  tag,
2858
- /* @__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 })
2859
3244
  ] }, tag)) }),
2860
- /* @__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 })
2861
3246
  ] });
2862
3247
  return /* @__PURE__ */ jsxs12(
2863
3248
  "form",
2864
3249
  {
2865
3250
  ...props,
2866
- onSubmit: (event) => {
2867
- event.preventDefault();
2868
- void save().catch(() => void 0);
2869
- },
2870
- "aria-busy": itemState.isMutating || props["aria-busy"],
3251
+ onSubmit: view.submit,
3252
+ "aria-busy": isSaving || props["aria-busy"],
2871
3253
  "data-keepkit": "tag-editor",
2872
- "data-state": itemState.error ? "error" : itemState.isMutating ? "saving" : "idle",
2873
- "data-loading": itemState.isMutating ? "true" : void 0,
2874
- "data-disabled": itemState.isMutating ? "true" : void 0,
3254
+ "data-state": view.error ? "error" : isSaving ? "saving" : "idle",
3255
+ "data-loading": isSaving ? "true" : void 0,
3256
+ "data-disabled": isSaving ? "true" : void 0,
2875
3257
  children: [
2876
3258
  body,
2877
- 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
2878
3260
  ]
2879
3261
  }
2880
3262
  );
@@ -2883,23 +3265,84 @@ function getErrorMessage5(error, fallback) {
2883
3265
  return error instanceof Error ? error.message : fallback;
2884
3266
  }
2885
3267
 
2886
- // src/KeepUndo.tsx
3268
+ // src/hooks/useKeepUndo.ts
2887
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
2888
3281
  import { jsx as jsx19, jsxs as jsxs13 } from "react/jsx-runtime";
2889
3282
  function KeepUndo({ children, label, ...props }) {
2890
- const context = useKeepContext5();
2891
- const message = useUiLabel("undoAvailable");
2892
- const undoLabel = useUiLabel("undo");
2893
- if (!context.undo.canUndo) return null;
3283
+ const view = useKeepUndo();
3284
+ if (!view.canUndo) return null;
2894
3285
  return /* @__PURE__ */ jsxs13("div", { ...props, role: "status", "aria-live": "polite", "data-keepkit": "undo", "data-state": "available", children: [
2895
- children ?? message,
2896
- /* @__PURE__ */ jsx19("button", { type: "button", onClick: () => void context.undoLastRemoval(), children: label ?? undoLabel })
3286
+ children ?? view.message,
3287
+ /* @__PURE__ */ jsx19("button", { type: "button", "data-keep-action": "undo", onClick: () => void view.undo(), children: label ?? view.label })
2897
3288
  ] });
2898
3289
  }
2899
3290
 
2900
3291
  // src/status.tsx
3292
+ import { isValidElement as isValidElement6 } from "react";
3293
+
3294
+ // src/hooks/useStatusViews.ts
2901
3295
  import { useKeepContext as useKeepContext6 } from "@keepkit/core/react";
2902
- 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
2903
3346
  import { Fragment as Fragment7, jsx as jsx20, jsxs as jsxs14 } from "react/jsx-runtime";
2904
3347
  function KeepEmptyState({
2905
3348
  title,
@@ -2910,7 +3353,7 @@ function KeepEmptyState({
2910
3353
  className,
2911
3354
  ...rootProps
2912
3355
  }) {
2913
- const defaultTitle = useUiLabel("noItems").replace(/\.$/, "");
3356
+ const defaultTitle = useKeepEmptyState();
2914
3357
  const contentChildren = asChild && isValidElement6(children) ? void 0 : children;
2915
3358
  const body = contentChildren ?? /* @__PURE__ */ jsxs14(Fragment7, { children: [
2916
3359
  /* @__PURE__ */ jsx20("h2", { children: title ?? defaultTitle }),
@@ -2934,18 +3377,10 @@ function KeepStatus({
2934
3377
  className,
2935
3378
  ...rootProps
2936
3379
  }) {
2937
- const context = useKeepContext6();
3380
+ const view = useKeepStatus(status);
2938
3381
  const contentChildren = asChild && isValidElement6(children) ? void 0 : children;
2939
- const resolvedStatus = status ?? getDerivedStatus(context);
2940
- const defaultLabel = useUiLabel(getStatusLabelKey3(resolvedStatus));
2941
- const state = {
2942
- status: resolvedStatus,
2943
- error: context.error,
2944
- pendingCount: context.syncState.pendingCount,
2945
- items: context.items
2946
- };
2947
- const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? labels?.[resolvedStatus] ?? defaultLabel;
2948
- const role = rootProps.role ?? (resolvedStatus === "error" ? "alert" : "status");
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");
2949
3384
  return renderRoot(
2950
3385
  asChild,
2951
3386
  isValidElement6(children) ? children : void 0,
@@ -2955,28 +3390,15 @@ function KeepStatus({
2955
3390
  "data-keepkit": "status",
2956
3391
  role,
2957
3392
  "aria-live": rootProps["aria-live"] ?? "polite",
2958
- "data-state": resolvedStatus,
2959
- "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
2960
3395
  },
2961
3396
  body,
2962
3397
  "KeepStatus"
2963
3398
  );
2964
3399
  }
2965
3400
  function KeepAnnouncements({ messages, ...props }) {
2966
- const context = useKeepContext6();
2967
- const savedMessage = useUiLabel("savedMessage", messages?.save);
2968
- const removedMessage = useUiLabel("removedMessage", messages?.remove);
2969
- const noteSavedMessage = useUiLabel("noteSavedMessage", messages?.note);
2970
- const [message, setMessage] = useState10("");
2971
- const lastChangeRef = useRef4(void 0);
2972
- useEffect6(() => {
2973
- const change = context.lastChange;
2974
- if (!change || change === lastChangeRef.current) return;
2975
- lastChangeRef.current = change;
2976
- if (change.action === "save") setMessage(savedMessage);
2977
- else if (change.action === "remove" || change.action === "removeBatch") setMessage(removedMessage);
2978
- else if (change.action === "updateNote") setMessage(noteSavedMessage);
2979
- }, [context.lastChange, noteSavedMessage, removedMessage, savedMessage]);
3401
+ const message = useKeepAnnouncements(messages);
2980
3402
  return /* @__PURE__ */ jsx20(
2981
3403
  "div",
2982
3404
  {
@@ -2991,26 +3413,22 @@ function KeepAnnouncements({ messages, ...props }) {
2991
3413
  );
2992
3414
  }
2993
3415
  var KeepAnnouncer = KeepAnnouncements;
2994
- function getDerivedStatus(context) {
2995
- if (context.error) return "error";
2996
- if (context.syncState.status === "pending" || context.syncState.status === "syncing") return "syncing";
2997
- if (context.isMutating) return "saving";
2998
- if (context.isLoading && !context.isHydrated) return "loading";
2999
- if (context.isHydrated && context.items.length === 0) return "empty";
3000
- return "idle";
3001
- }
3002
- function getStatusLabelKey3(status) {
3003
- if (status === "empty") return "noItems";
3004
- if (status === "loading") return "loadingItems";
3005
- if (status === "error") return "error";
3006
- if (status === "saving") return "saving";
3007
- if (status === "syncing") return "syncing";
3008
- return "saved";
3009
- }
3010
3416
 
3011
3417
  // src/theme.tsx
3012
3418
  import { cloneElement as cloneElement2, isValidElement as isValidElement7 } from "react";
3013
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
+ ];
3014
3432
  function KeepThemeProvider({
3015
3433
  children,
3016
3434
  theme = "default",
@@ -3223,6 +3641,7 @@ export {
3223
3641
  createStorageAdapter,
3224
3642
  getKeepLocaleLabels,
3225
3643
  isAllSelected,
3644
+ keepThemeNames,
3226
3645
  toggleSelectAll,
3227
3646
  useKeepContext7 as useKeepContext,
3228
3647
  useKeepItem5 as useKeepItem,