@keepkit/ui 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6,12 +6,32 @@ import {
6
6
  createKeepKit as createCoreKeepKit
7
7
  } from "@keepkit/core/react";
8
8
 
9
+ // src/hooks/useKeepToastFeedback.ts
10
+ import { useCallback } from "react";
11
+ function useKeepToastFeedback(showToast) {
12
+ return useCallback(
13
+ (event) => {
14
+ if (!("undo" in event)) {
15
+ showToast(event.message);
16
+ return;
17
+ }
18
+ showToast(event.message, {
19
+ action: {
20
+ label: event.undoLabel,
21
+ onClick: () => void event.undo()
22
+ }
23
+ });
24
+ },
25
+ [showToast]
26
+ );
27
+ }
28
+
9
29
  // src/hooks/useKeepBackup.ts
10
30
  import { useKeepContext } from "@keepkit/core/react";
11
31
  import { useRef, useState } from "react";
12
32
 
13
33
  // src/ui-context.tsx
14
- import { createContext, useContext, useMemo } from "react";
34
+ import { createContext, useCallback as useCallback2, useContext, useMemo } from "react";
15
35
 
16
36
  // src/locales/de.ts
17
37
  var DE_LABELS = {
@@ -49,6 +69,9 @@ var DE_LABELS = {
49
69
  applyTags: "Tags anwenden",
50
70
  savedMessage: "Element gespeichert.",
51
71
  removedMessage: "Element entfernt.",
72
+ restoredMessage: "Element wiederhergestellt.",
73
+ syncFailedMessage: "Synchronisierung fehlgeschlagen.",
74
+ stalePrunedMessage: "Nicht verf\xFCgbare Elemente entfernt.",
52
75
  undo: "R\xFCckg\xE4ngig",
53
76
  undoAvailable: "Element entfernt. R\xFCckg\xE4ngig machen?",
54
77
  selectionScope: "Auswahlbereich",
@@ -125,6 +148,9 @@ var EN_LABELS = {
125
148
  applyTags: "Apply tags",
126
149
  savedMessage: "Item saved.",
127
150
  removedMessage: "Item removed.",
151
+ restoredMessage: "Item restored.",
152
+ syncFailedMessage: "Sync failed.",
153
+ stalePrunedMessage: "Unavailable items removed.",
128
154
  undo: "Undo",
129
155
  undoAvailable: "Item removed. Undo?",
130
156
  selectionScope: "Selection scope",
@@ -201,6 +227,9 @@ var ES_LABELS = {
201
227
  applyTags: "Aplicar etiquetas",
202
228
  savedMessage: "Elemento guardado.",
203
229
  removedMessage: "Elemento eliminado.",
230
+ restoredMessage: "Elemento restaurado.",
231
+ syncFailedMessage: "Error de sincronizaci\xF3n.",
232
+ stalePrunedMessage: "Elementos no disponibles eliminados.",
204
233
  undo: "Deshacer",
205
234
  undoAvailable: "Elemento eliminado. \xBFDeshacer?",
206
235
  selectionScope: "\xC1mbito de selecci\xF3n",
@@ -277,6 +306,9 @@ var FIL_LABELS = {
277
306
  applyTags: "Ilapat ang mga tag",
278
307
  savedMessage: "Na-save ang item.",
279
308
  removedMessage: "Inalis ang item.",
309
+ restoredMessage: "Naibalik ang item.",
310
+ syncFailedMessage: "Hindi nagtagumpay ang pag-sync.",
311
+ stalePrunedMessage: "Inalis ang mga hindi available na item.",
280
312
  undo: "I-undo",
281
313
  undoAvailable: "Inalis ang item. I-undo?",
282
314
  selectionScope: "Saklaw ng pagpili",
@@ -353,6 +385,9 @@ var FR_LABELS = {
353
385
  applyTags: "Appliquer les \xE9tiquettes",
354
386
  savedMessage: "\xC9l\xE9ment enregistr\xE9.",
355
387
  removedMessage: "\xC9l\xE9ment supprim\xE9.",
388
+ restoredMessage: "\xC9l\xE9ment restaur\xE9.",
389
+ syncFailedMessage: "\xC9chec de la synchronisation.",
390
+ stalePrunedMessage: "\xC9l\xE9ments indisponibles supprim\xE9s.",
356
391
  undo: "Annuler",
357
392
  undoAvailable: "\xC9l\xE9ment supprim\xE9. Annuler ?",
358
393
  selectionScope: "Port\xE9e de la s\xE9lection",
@@ -429,6 +464,9 @@ var ID_LABELS = {
429
464
  applyTags: "Terapkan tag",
430
465
  savedMessage: "Item tersimpan.",
431
466
  removedMessage: "Item dihapus.",
467
+ restoredMessage: "Item dipulihkan.",
468
+ syncFailedMessage: "Sinkronisasi gagal.",
469
+ stalePrunedMessage: "Item yang tidak tersedia dihapus.",
432
470
  undo: "Urungkan",
433
471
  undoAvailable: "Item dihapus. Urungkan?",
434
472
  selectionScope: "Cakupan pilihan",
@@ -505,6 +543,9 @@ var IT_LABELS = {
505
543
  applyTags: "Applica tag",
506
544
  savedMessage: "Elemento salvato.",
507
545
  removedMessage: "Elemento rimosso.",
546
+ restoredMessage: "Elemento ripristinato.",
547
+ syncFailedMessage: "Sincronizzazione non riuscita.",
548
+ stalePrunedMessage: "Elementi non disponibili rimossi.",
508
549
  undo: "Annulla",
509
550
  undoAvailable: "Elemento rimosso. Annullare?",
510
551
  selectionScope: "Ambito della selezione",
@@ -581,6 +622,9 @@ var JA_LABELS = {
581
622
  applyTags: "\u30BF\u30B0\u3092\u9069\u7528",
582
623
  savedMessage: "\u30A2\u30A4\u30C6\u30E0\u3092\u4FDD\u5B58\u3057\u307E\u3057\u305F\u3002",
583
624
  removedMessage: "\u30A2\u30A4\u30C6\u30E0\u3092\u524A\u9664\u3057\u307E\u3057\u305F\u3002",
625
+ restoredMessage: "\u30A2\u30A4\u30C6\u30E0\u3092\u5FA9\u5143\u3057\u307E\u3057\u305F\u3002",
626
+ syncFailedMessage: "\u540C\u671F\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002",
627
+ stalePrunedMessage: "\u5229\u7528\u3067\u304D\u306A\u3044\u30A2\u30A4\u30C6\u30E0\u3092\u524A\u9664\u3057\u307E\u3057\u305F\u3002",
584
628
  undo: "\u5143\u306B\u623B\u3059",
585
629
  undoAvailable: "\u30A2\u30A4\u30C6\u30E0\u3092\u524A\u9664\u3057\u307E\u3057\u305F\u3002\u5143\u306B\u623B\u3057\u307E\u3059\u304B\uFF1F",
586
630
  selectionScope: "\u9078\u629E\u7BC4\u56F2",
@@ -657,6 +701,9 @@ var KO_LABELS = {
657
701
  applyTags: "\uD0DC\uADF8 \uC801\uC6A9",
658
702
  savedMessage: "\uD56D\uBAA9\uC744 \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4.",
659
703
  removedMessage: "\uD56D\uBAA9\uC744 \uC0AD\uC81C\uD588\uC2B5\uB2C8\uB2E4.",
704
+ restoredMessage: "\uD56D\uBAA9\uC744 \uBCF5\uC6D0\uD588\uC2B5\uB2C8\uB2E4.",
705
+ syncFailedMessage: "\uB3D9\uAE30\uD654\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.",
706
+ stalePrunedMessage: "\uC0AC\uC6A9\uD560 \uC218 \uC5C6\uB294 \uD56D\uBAA9\uC744 \uC0AD\uC81C\uD588\uC2B5\uB2C8\uB2E4.",
660
707
  undo: "\uC2E4\uD589 \uCDE8\uC18C",
661
708
  undoAvailable: "\uD56D\uBAA9\uC744 \uC0AD\uC81C\uD588\uC2B5\uB2C8\uB2E4. \uC2E4\uD589 \uCDE8\uC18C\uD560\uAE4C\uC694?",
662
709
  selectionScope: "\uC120\uD0DD \uBC94\uC704",
@@ -733,6 +780,9 @@ var MS_LABELS = {
733
780
  applyTags: "Gunakan tag",
734
781
  savedMessage: "Item disimpan.",
735
782
  removedMessage: "Item dibuang.",
783
+ restoredMessage: "Item dipulihkan.",
784
+ syncFailedMessage: "Penyegerakan gagal.",
785
+ stalePrunedMessage: "Item yang tidak tersedia dibuang.",
736
786
  undo: "Buat asal",
737
787
  undoAvailable: "Item dibuang. Buat asal?",
738
788
  selectionScope: "Skop pilihan",
@@ -809,6 +859,9 @@ var PT_BR_LABELS = {
809
859
  applyTags: "Aplicar tags",
810
860
  savedMessage: "Item salvo.",
811
861
  removedMessage: "Item removido.",
862
+ restoredMessage: "Item restaurado.",
863
+ syncFailedMessage: "Falha na sincroniza\xE7\xE3o.",
864
+ stalePrunedMessage: "Itens indispon\xEDveis removidos.",
812
865
  undo: "Desfazer",
813
866
  undoAvailable: "Item removido. Desfazer?",
814
867
  selectionScope: "Escopo da sele\xE7\xE3o",
@@ -885,6 +938,9 @@ var RU_LABELS = {
885
938
  applyTags: "\u041F\u0440\u0438\u043C\u0435\u043D\u0438\u0442\u044C \u0442\u0435\u0433\u0438",
886
939
  savedMessage: "\u042D\u043B\u0435\u043C\u0435\u043D\u0442 \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D.",
887
940
  removedMessage: "\u042D\u043B\u0435\u043C\u0435\u043D\u0442 \u0443\u0434\u0430\u043B\u0451\u043D.",
941
+ restoredMessage: "\u042D\u043B\u0435\u043C\u0435\u043D\u0442 \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D.",
942
+ syncFailedMessage: "\u0421\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0430\u0446\u0438\u044F \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D\u0430.",
943
+ stalePrunedMessage: "\u041D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B \u0443\u0434\u0430\u043B\u0435\u043D\u044B.",
888
944
  undo: "\u041E\u0442\u043C\u0435\u043D\u0438\u0442\u044C",
889
945
  undoAvailable: "\u042D\u043B\u0435\u043C\u0435\u043D\u0442 \u0443\u0434\u0430\u043B\u0451\u043D. \u041E\u0442\u043C\u0435\u043D\u0438\u0442\u044C?",
890
946
  selectionScope: "\u041E\u0431\u043B\u0430\u0441\u0442\u044C \u0432\u044B\u0431\u043E\u0440\u0430",
@@ -961,6 +1017,9 @@ var TH_LABELS = {
961
1017
  applyTags: "\u0E43\u0E0A\u0E49\u0E41\u0E17\u0E47\u0E01",
962
1018
  savedMessage: "\u0E1A\u0E31\u0E19\u0E17\u0E36\u0E01\u0E23\u0E32\u0E22\u0E01\u0E32\u0E23\u0E41\u0E25\u0E49\u0E27",
963
1019
  removedMessage: "\u0E25\u0E1A\u0E23\u0E32\u0E22\u0E01\u0E32\u0E23\u0E41\u0E25\u0E49\u0E27",
1020
+ restoredMessage: "\u0E01\u0E39\u0E49\u0E04\u0E37\u0E19\u0E23\u0E32\u0E22\u0E01\u0E32\u0E23\u0E41\u0E25\u0E49\u0E27",
1021
+ syncFailedMessage: "\u0E0B\u0E34\u0E07\u0E04\u0E4C\u0E44\u0E21\u0E48\u0E2A\u0E33\u0E40\u0E23\u0E47\u0E08",
1022
+ stalePrunedMessage: "\u0E25\u0E1A\u0E23\u0E32\u0E22\u0E01\u0E32\u0E23\u0E17\u0E35\u0E48\u0E43\u0E0A\u0E49\u0E44\u0E21\u0E48\u0E44\u0E14\u0E49\u0E41\u0E25\u0E49\u0E27",
964
1023
  undo: "\u0E40\u0E25\u0E34\u0E01\u0E17\u0E33",
965
1024
  undoAvailable: "\u0E25\u0E1A\u0E23\u0E32\u0E22\u0E01\u0E32\u0E23\u0E41\u0E25\u0E49\u0E27 \u0E15\u0E49\u0E2D\u0E07\u0E01\u0E32\u0E23\u0E40\u0E25\u0E34\u0E01\u0E17\u0E33\u0E2B\u0E23\u0E37\u0E2D\u0E44\u0E21\u0E48",
966
1025
  selectionScope: "\u0E02\u0E2D\u0E1A\u0E40\u0E02\u0E15\u0E01\u0E32\u0E23\u0E40\u0E25\u0E37\u0E2D\u0E01",
@@ -1037,6 +1096,9 @@ var VI_LABELS = {
1037
1096
  applyTags: "\xC1p d\u1EE5ng th\u1EBB",
1038
1097
  savedMessage: "\u0110\xE3 l\u01B0u m\u1EE5c.",
1039
1098
  removedMessage: "\u0110\xE3 x\xF3a m\u1EE5c.",
1099
+ restoredMessage: "\u0110\xE3 kh\xF4i ph\u1EE5c m\u1EE5c.",
1100
+ syncFailedMessage: "\u0110\u1ED3ng b\u1ED9 kh\xF4ng th\xE0nh c\xF4ng.",
1101
+ stalePrunedMessage: "\u0110\xE3 x\xF3a c\xE1c m\u1EE5c kh\xF4ng kh\u1EA3 d\u1EE5ng.",
1040
1102
  undo: "Ho\xE0n t\xE1c",
1041
1103
  undoAvailable: "\u0110\xE3 x\xF3a m\u1EE5c. Ho\xE0n t\xE1c?",
1042
1104
  selectionScope: "Ph\u1EA1m vi l\u1EF1a ch\u1ECDn",
@@ -1113,6 +1175,9 @@ var ZH_HANS_LABELS = {
1113
1175
  applyTags: "\u5E94\u7528\u6807\u7B7E",
1114
1176
  savedMessage: "\u9879\u76EE\u5DF2\u4FDD\u5B58\u3002",
1115
1177
  removedMessage: "\u9879\u76EE\u5DF2\u5220\u9664\u3002",
1178
+ restoredMessage: "\u9879\u76EE\u5DF2\u6062\u590D\u3002",
1179
+ syncFailedMessage: "\u540C\u6B65\u5931\u8D25\u3002",
1180
+ stalePrunedMessage: "\u4E0D\u53EF\u7528\u9879\u76EE\u5DF2\u5220\u9664\u3002",
1116
1181
  undo: "\u64A4\u9500",
1117
1182
  undoAvailable: "\u9879\u76EE\u5DF2\u5220\u9664\u3002\u8981\u64A4\u9500\u5417\uFF1F",
1118
1183
  selectionScope: "\u9009\u62E9\u8303\u56F4",
@@ -1189,6 +1254,9 @@ var ZH_HANT_LABELS = {
1189
1254
  applyTags: "\u5957\u7528\u6A19\u7C64",
1190
1255
  savedMessage: "\u9805\u76EE\u5DF2\u5132\u5B58\u3002",
1191
1256
  removedMessage: "\u9805\u76EE\u5DF2\u522A\u9664\u3002",
1257
+ restoredMessage: "\u9805\u76EE\u5DF2\u9084\u539F\u3002",
1258
+ syncFailedMessage: "\u540C\u6B65\u5931\u6557\u3002",
1259
+ stalePrunedMessage: "\u7121\u6CD5\u4F7F\u7528\u7684\u9805\u76EE\u5DF2\u522A\u9664\u3002",
1192
1260
  undo: "\u5FA9\u539F",
1193
1261
  undoAvailable: "\u9805\u76EE\u5DF2\u522A\u9664\u3002\u8981\u5FA9\u539F\u55CE\uFF1F",
1194
1262
  selectionScope: "\u9078\u53D6\u7BC4\u570D",
@@ -1291,14 +1359,29 @@ function getKeepLocaleLabels(locale) {
1291
1359
  // src/ui-context.tsx
1292
1360
  import { jsx } from "react/jsx-runtime";
1293
1361
  var DEFAULT_LABELS = KEEP_LOCALE_LABELS.en;
1294
- var KeepUiLabelsContext = createContext({ labels: DEFAULT_LABELS });
1295
- function KeepUiProvider({ labels, locale, labelResolver, children }) {
1362
+ var KeepUiLabelsContext = createContext({
1363
+ labels: DEFAULT_LABELS,
1364
+ emitFeedback: () => void 0
1365
+ });
1366
+ function KeepUiProvider({
1367
+ labels,
1368
+ locale,
1369
+ labelResolver,
1370
+ onFeedback,
1371
+ children
1372
+ }) {
1373
+ const emitFeedback = useCallback2(
1374
+ (event) => {
1375
+ onFeedback?.(event);
1376
+ },
1377
+ [onFeedback]
1378
+ );
1296
1379
  const value = useMemo(() => {
1297
1380
  const resolvedLocale = normalizeKeepLocale(locale);
1298
1381
  const dictionary = getKeepLocaleLabels(resolvedLocale);
1299
1382
  const resolved = { ...DEFAULT_LABELS, ...dictionary, ...labels };
1300
- return { locale: locale ?? resolvedLocale, labels: resolved, customLabels: labels, labelResolver };
1301
- }, [labelResolver, labels, locale]);
1383
+ return { locale: locale ?? resolvedLocale, labels: resolved, customLabels: labels, labelResolver, emitFeedback };
1384
+ }, [emitFeedback, labelResolver, labels, locale]);
1302
1385
  return /* @__PURE__ */ jsx(KeepUiLabelsContext.Provider, { value, children });
1303
1386
  }
1304
1387
  function useKeepUiLabels() {
@@ -1308,6 +1391,10 @@ function useUiLabel(key, override) {
1308
1391
  const context = useKeepUiLabels();
1309
1392
  return override ?? context.customLabels?.[key] ?? context.labelResolver?.(key, { locale: context.locale }) ?? context.labels[key];
1310
1393
  }
1394
+ function useKeepUiFeedback() {
1395
+ const { emitFeedback } = useKeepUiLabels();
1396
+ return useCallback2((event) => emitFeedback(event), [emitFeedback]);
1397
+ }
1311
1398
 
1312
1399
  // src/hooks/useKeepBackup.ts
1313
1400
  function useKeepBackup({ filename, onExport, onImported }) {
@@ -1453,9 +1540,49 @@ import { useState as useState2 } from "react";
1453
1540
  // src/shared.tsx
1454
1541
  import {
1455
1542
  cloneElement,
1456
- isValidElement
1543
+ createContext as createContext2,
1544
+ isValidElement,
1545
+ useContext as useContext2
1457
1546
  } from "react";
1458
- import { jsx as jsx3 } from "react/jsx-runtime";
1547
+ import { Fragment, jsx as jsx3 } from "react/jsx-runtime";
1548
+ var KeepSearchQueryContext = createContext2(void 0);
1549
+ function KeepSearchQueryProvider({ query, children }) {
1550
+ return /* @__PURE__ */ jsx3(KeepSearchQueryContext.Provider, { value: query, children });
1551
+ }
1552
+ function useKeepSearchQuery() {
1553
+ return useContext2(KeepSearchQueryContext);
1554
+ }
1555
+ function KeepHighlight({ children, query }) {
1556
+ const contextQuery = useKeepSearchQuery();
1557
+ const resolvedQuery = query ?? contextQuery;
1558
+ if (typeof children !== "string" || !resolvedQuery?.trim()) return children ?? null;
1559
+ return highlightText(children, resolvedQuery);
1560
+ }
1561
+ function highlightText(text, query) {
1562
+ const normalizedQuery = query.trim();
1563
+ if (!normalizedQuery) return text;
1564
+ const matcher = new RegExp(escapeRegExp(normalizedQuery), "gi");
1565
+ const parts = [];
1566
+ let lastIndex = 0;
1567
+ let matchIndex = 0;
1568
+ while (true) {
1569
+ const match = matcher.exec(text);
1570
+ if (match === null) break;
1571
+ const index = match.index;
1572
+ if (index > lastIndex) parts.push(text.slice(lastIndex, index));
1573
+ parts.push(
1574
+ /* @__PURE__ */ jsx3("mark", { className: "keep-highlight", "data-highlight": "true", children: match[0] }, `highlight-${matchIndex}`)
1575
+ );
1576
+ lastIndex = index + match[0].length;
1577
+ matchIndex += 1;
1578
+ }
1579
+ if (lastIndex === 0) return text;
1580
+ if (lastIndex < text.length) parts.push(text.slice(lastIndex));
1581
+ return /* @__PURE__ */ jsx3(Fragment, { children: parts });
1582
+ }
1583
+ function escapeRegExp(value) {
1584
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1585
+ }
1459
1586
  function toKeepButtonItem(item) {
1460
1587
  return {
1461
1588
  id: item.id,
@@ -1614,7 +1741,7 @@ function getItemLabel(item) {
1614
1741
  }
1615
1742
 
1616
1743
  // src/KeepBulkActions.tsx
1617
- import { Fragment, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
1744
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
1618
1745
  function KeepBulkActions({
1619
1746
  query,
1620
1747
  selectedIds: controlledSelectedIds,
@@ -1637,7 +1764,7 @@ function KeepBulkActions({
1637
1764
  controlledScope,
1638
1765
  onSelectionScopeChange
1639
1766
  });
1640
- const body = render ? render(state) : typeof children === "function" ? children(state) : children ?? /* @__PURE__ */ jsxs2(Fragment, { children: [
1767
+ const body = render ? render(state) : typeof children === "function" ? children(state) : children ?? /* @__PURE__ */ jsxs2(Fragment2, { children: [
1641
1768
  /* @__PURE__ */ jsxs2("fieldset", { children: [
1642
1769
  /* @__PURE__ */ jsx5("legend", { children: labels.selectItems }),
1643
1770
  /* @__PURE__ */ jsxs2("label", { children: [
@@ -1732,47 +1859,91 @@ function KeepBulkActions({
1732
1859
  import {
1733
1860
  KeepButton as CoreKeepButton
1734
1861
  } from "@keepkit/core/react";
1735
- import { createElement } from "react";
1862
+ import { createElement, useEffect, useRef as useRef2 } from "react";
1736
1863
 
1737
1864
  // src/hooks/useKeepButton.ts
1738
1865
  import { useKeepItem } from "@keepkit/core/react";
1739
1866
  function useKeepButton({ item, labels, icons, children }) {
1740
1867
  return {
1741
1868
  buttonState: useKeepItem(item),
1869
+ emitFeedback: useKeepUiFeedback(),
1742
1870
  customStateLabel: labels?.loading !== void 0 || labels?.error !== void 0 || icons !== void 0 && children === void 0,
1743
1871
  labels: {
1744
1872
  save: useUiLabel("save", typeof labels?.unsaved === "string" ? labels.unsaved : void 0),
1745
1873
  saved: useUiLabel("saved", typeof labels?.saved === "string" ? labels.saved : void 0),
1746
1874
  loading: useUiLabel("loading", typeof labels?.loading === "string" ? labels.loading : void 0),
1747
- error: useUiLabel("error", typeof labels?.error === "string" ? labels.error : void 0)
1875
+ error: useUiLabel("error", typeof labels?.error === "string" ? labels.error : void 0),
1876
+ savedMessage: useUiLabel("savedMessage"),
1877
+ removedMessage: useUiLabel("removedMessage"),
1878
+ restoredMessage: useUiLabel("restoredMessage"),
1879
+ undo: useUiLabel("undo")
1748
1880
  }
1749
1881
  };
1750
1882
  }
1751
1883
 
1752
1884
  // src/KeepButton.tsx
1753
- import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
1885
+ import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
1754
1886
  function KeepButton({
1755
1887
  labels,
1756
1888
  icons,
1757
1889
  iconOnly = false,
1758
1890
  showLabel = true,
1759
1891
  iconClassName,
1892
+ onClick,
1893
+ onToggleError,
1760
1894
  ...props
1761
1895
  }) {
1762
1896
  const view = useKeepButton({ item: props.item, labels, icons, children: props.children });
1897
+ const pendingToggle = useRef2(null);
1898
+ useEffect(() => {
1899
+ const pending = pendingToggle.current;
1900
+ if (!pending || view.buttonState.isMutating || pending.wasSaved === view.buttonState.isSaved) return;
1901
+ pendingToggle.current = null;
1902
+ if (view.buttonState.isSaved && view.buttonState.item) {
1903
+ view.emitFeedback({ type: "item-saved", item: view.buttonState.item, message: view.labels.savedMessage });
1904
+ return;
1905
+ }
1906
+ if (!pending.item) return;
1907
+ const removedItem = pending.item;
1908
+ view.emitFeedback({
1909
+ type: "item-removed",
1910
+ item: removedItem,
1911
+ message: view.labels.removedMessage,
1912
+ undoLabel: view.labels.undo,
1913
+ undo: async () => {
1914
+ await view.buttonState.save();
1915
+ view.emitFeedback({
1916
+ type: "item-restored",
1917
+ item: removedItem,
1918
+ items: [removedItem],
1919
+ message: view.labels.restoredMessage
1920
+ });
1921
+ }
1922
+ });
1923
+ }, [view]);
1763
1924
  const getStateContent = (state) => {
1764
1925
  if (typeof props.children === "function") return props.children(state);
1765
1926
  if (props.children !== void 0) return props.children;
1766
1927
  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;
1767
1928
  if (!icons) return label;
1768
1929
  const icon = state.error ? icons.error : state.isMutating ? icons.loading : state.isSaved ? icons.remove ?? icons.saved : icons.save;
1769
- return /* @__PURE__ */ jsxs3(Fragment2, { children: [
1930
+ return /* @__PURE__ */ jsxs3(Fragment3, { children: [
1770
1931
  renderIcon(icon, iconClassName),
1771
1932
  showLabel && !iconOnly ? label : null
1772
1933
  ] });
1773
1934
  };
1774
1935
  const sharedProps = {
1775
1936
  ...props,
1937
+ onClick: (event) => {
1938
+ onClick?.(event);
1939
+ if (!event.defaultPrevented) {
1940
+ pendingToggle.current = { wasSaved: view.buttonState.isSaved, item: view.buttonState.item };
1941
+ }
1942
+ },
1943
+ onToggleError: (error) => {
1944
+ pendingToggle.current = null;
1945
+ onToggleError?.(error);
1946
+ },
1776
1947
  "data-keepkit": "button",
1777
1948
  "data-keep-action": "toggle-save",
1778
1949
  "data-has-custom-icon": icons ? "true" : void 0,
@@ -1784,7 +1955,7 @@ function KeepButton({
1784
1955
  unsavedAriaLabel: labels?.unsavedAriaLabel ?? props.unsavedAriaLabel
1785
1956
  };
1786
1957
  if (!view.customStateLabel) return /* @__PURE__ */ jsx6(CoreKeepButton, { ...sharedProps });
1787
- return /* @__PURE__ */ jsx6(CoreKeepButton, { ...sharedProps, children: (state) => /* @__PURE__ */ jsx6(Fragment2, { children: getStateContent(state) }) });
1958
+ return /* @__PURE__ */ jsx6(CoreKeepButton, { ...sharedProps, children: (state) => /* @__PURE__ */ jsx6(Fragment3, { children: getStateContent(state) }) });
1788
1959
  }
1789
1960
  function renderIcon(icon, className) {
1790
1961
  if (typeof icon === "function") return createElement(icon, { "aria-hidden": true, className });
@@ -1804,7 +1975,7 @@ import {
1804
1975
  decodeKeepListQuery,
1805
1976
  encodeKeepListQuery
1806
1977
  } from "@keepkit/core/core";
1807
- import { useEffect, useRef as useRef2 } from "react";
1978
+ import { useEffect as useEffect2, useRef as useRef3 } from "react";
1808
1979
  function createNextPagesRouterAdapter(router) {
1809
1980
  const getUrl = () => router.asPath ?? (typeof window === "undefined" ? "/" : window.location.href);
1810
1981
  return {
@@ -1825,13 +1996,13 @@ function useKeepUrlSync({
1825
1996
  options = {},
1826
1997
  adapter: providedAdapter
1827
1998
  }) {
1828
- const browserAdapterRef = useRef2(getBrowserAdapter());
1999
+ const browserAdapterRef = useRef3(getBrowserAdapter());
1829
2000
  const adapter = providedAdapter ?? browserAdapterRef.current;
1830
- const onQueryChangeRef = useRef2(onQueryChange);
2001
+ const onQueryChangeRef = useRef3(onQueryChange);
1831
2002
  onQueryChangeRef.current = onQueryChange;
1832
- const skipWriteRef = useRef2(true);
2003
+ const skipWriteRef = useRef3(true);
1833
2004
  const params = options.params;
1834
- useEffect(() => {
2005
+ useEffect2(() => {
1835
2006
  if (!enabled) return;
1836
2007
  const read = () => {
1837
2008
  const url = adapter.getUrl();
@@ -1848,7 +2019,7 @@ function useKeepUrlSync({
1848
2019
  read();
1849
2020
  return adapter.subscribe?.(read);
1850
2021
  }, [adapter, enabled, params]);
1851
- useEffect(() => {
2022
+ useEffect2(() => {
1852
2023
  if (!enabled) return;
1853
2024
  if (skipWriteRef.current) {
1854
2025
  skipWriteRef.current = false;
@@ -1966,9 +2137,79 @@ function useKeepListView(query) {
1966
2137
  };
1967
2138
  }
1968
2139
 
2140
+ // src/hooks/useRovingTabIndex.ts
2141
+ import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef4 } from "react";
2142
+ function useRovingTabIndex() {
2143
+ const ref = useRef4(null);
2144
+ const getItems = useCallback3(() => {
2145
+ const root = ref.current;
2146
+ if (!root) return [];
2147
+ return Array.from(root.querySelectorAll('[data-keepkit="card"]')).filter(
2148
+ (item) => item.getAttribute("aria-hidden") !== "true" && item.getAttribute("data-roving-disabled") !== "true"
2149
+ );
2150
+ }, []);
2151
+ const syncTabIndices = useCallback3(() => {
2152
+ const items = getItems();
2153
+ if (items.length === 0) return;
2154
+ const activeElement = document.activeElement;
2155
+ const activeItem = items.find((item) => item === activeElement || item.contains(activeElement));
2156
+ const activeIndex = activeItem ? items.indexOf(activeItem) : 0;
2157
+ items.forEach((item, index) => {
2158
+ item.tabIndex = index === activeIndex ? 0 : -1;
2159
+ });
2160
+ }, [getItems]);
2161
+ useEffect3(() => {
2162
+ const root = ref.current;
2163
+ if (!root) return;
2164
+ syncTabIndices();
2165
+ const observer = new MutationObserver(syncTabIndices);
2166
+ observer.observe(root, { childList: true, subtree: true });
2167
+ return () => observer.disconnect();
2168
+ }, [syncTabIndices]);
2169
+ const onFocusCapture = useCallback3(
2170
+ (event) => {
2171
+ if (!(event.target instanceof HTMLElement)) return;
2172
+ const item = event.target.closest('[data-keepkit="card"]');
2173
+ if (!item || !ref.current?.contains(item)) return;
2174
+ getItems().forEach((candidate) => {
2175
+ candidate.tabIndex = candidate === item ? 0 : -1;
2176
+ });
2177
+ },
2178
+ [getItems]
2179
+ );
2180
+ const onKeyDown = useCallback3(
2181
+ (event) => {
2182
+ if (event.defaultPrevented) return;
2183
+ const items = getItems();
2184
+ if (!(event.target instanceof HTMLElement)) return;
2185
+ const current = event.target.closest('[data-keepkit="card"]');
2186
+ if (!current || current !== event.target || !ref.current?.contains(current)) return;
2187
+ const currentIndex = items.indexOf(current);
2188
+ if (currentIndex < 0) return;
2189
+ let nextIndex;
2190
+ if (event.key === "Home") nextIndex = 0;
2191
+ if (event.key === "End") nextIndex = items.length - 1;
2192
+ if (event.key === "ArrowRight" || event.key === "ArrowDown")
2193
+ nextIndex = Math.min(currentIndex + 1, items.length - 1);
2194
+ if (event.key === "ArrowLeft" || event.key === "ArrowUp") nextIndex = Math.max(currentIndex - 1, 0);
2195
+ if (nextIndex === void 0) return;
2196
+ event.preventDefault();
2197
+ if (nextIndex === currentIndex) return;
2198
+ items[nextIndex]?.focus();
2199
+ },
2200
+ [getItems]
2201
+ );
2202
+ return { ref, onKeyDown, onFocusCapture };
2203
+ }
2204
+
1969
2205
  // src/KeepItemCard.tsx
1970
2206
  import {
1971
- isValidElement as isValidElement2
2207
+ createContext as createContext3,
2208
+ createElement as createElement2,
2209
+ isValidElement as isValidElement2,
2210
+ useContext as useContext3,
2211
+ useEffect as useEffect4,
2212
+ useState as useState5
1972
2213
  } from "react";
1973
2214
 
1974
2215
  // src/hooks/useKeepItemCard.ts
@@ -1986,6 +2227,10 @@ function useKeepItemCard(options) {
1986
2227
  onRemoved
1987
2228
  } = options;
1988
2229
  const itemState = useKeepItem2(item);
2230
+ const emitFeedback = useKeepUiFeedback();
2231
+ const removedMessage = useUiLabel("removedMessage");
2232
+ const restoredMessage = useUiLabel("restoredMessage");
2233
+ const undoLabel = useUiLabel("undo");
1989
2234
  const resolvedTitle = typeof title === "function" ? title(item) : title ?? getTitle?.(item) ?? getMetaTitle(item.meta) ?? item.id;
1990
2235
  const imageProps = getImageProps?.(item, resolvedTitle);
1991
2236
  const href = typeof hrefOption === "function" ? hrefOption(item) : hrefOption;
@@ -1993,12 +2238,32 @@ function useKeepItemCard(options) {
1993
2238
  const isExternalLink = href ? /^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(href) : false;
1994
2239
  const statusLabelKey = item.status && item.status !== "available" ? getStatusLabelKey(item.status) : "statusUnknown";
1995
2240
  const unavailableLabel = useUiLabel(statusLabelKey);
2241
+ const remove = async () => {
2242
+ const wasSaved = itemState.isSaved;
2243
+ try {
2244
+ await itemState.removeWithUndo();
2245
+ onRemoved?.(item);
2246
+ if (!wasSaved) return;
2247
+ emitFeedback({
2248
+ type: "item-removed",
2249
+ item,
2250
+ message: removedMessage,
2251
+ undoLabel,
2252
+ undo: async () => {
2253
+ await itemState.undo();
2254
+ emitFeedback({ type: "item-restored", item, items: [item], message: restoredMessage });
2255
+ }
2256
+ });
2257
+ } catch (cause) {
2258
+ onRemoveError?.(cause);
2259
+ }
2260
+ };
1996
2261
  const state = {
1997
2262
  item,
1998
2263
  isSaved: itemState.isSaved,
1999
2264
  isMutating: itemState.isMutating,
2000
2265
  error: itemState.error,
2001
- remove: itemState.remove,
2266
+ remove,
2002
2267
  status: item.status
2003
2268
  };
2004
2269
  return {
@@ -2012,14 +2277,7 @@ function useKeepItemCard(options) {
2012
2277
  resolvedLinkTarget: linkTargetAttribute ?? (isExternalLink ? "_blank" : void 0),
2013
2278
  resolvedLinkRel: linkRel ?? (isExternalLink ? "noreferrer" : void 0),
2014
2279
  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
- },
2280
+ remove,
2023
2281
  labels: {
2024
2282
  save: useUiLabel("save"),
2025
2283
  savedAt: useUiLabel("saved"),
@@ -2055,6 +2313,10 @@ import { useKeepContext as useKeepContext2 } from "@keepkit/core/react";
2055
2313
  import { useState as useState4 } from "react";
2056
2314
  function useKeepStaleNotice({ item, onRetry, onRemoved }) {
2057
2315
  const context = useKeepContext2();
2316
+ const emitFeedback = useKeepUiFeedback();
2317
+ const removedMessage = useUiLabel("removedMessage");
2318
+ const restoredMessage = useUiLabel("restoredMessage");
2319
+ const undoLabel = useUiLabel("undo");
2058
2320
  const [isRetrying, setIsRetrying] = useState4(false);
2059
2321
  const [error, setError] = useState4(null);
2060
2322
  async function retry() {
@@ -2073,6 +2335,16 @@ function useKeepStaleNotice({ item, onRetry, onRemoved }) {
2073
2335
  try {
2074
2336
  await context.removeItemWithUndo(item.id);
2075
2337
  onRemoved?.(item);
2338
+ emitFeedback({
2339
+ type: "item-removed",
2340
+ item,
2341
+ message: removedMessage,
2342
+ undoLabel,
2343
+ undo: async () => {
2344
+ await context.undoLastRemoval();
2345
+ emitFeedback({ type: "item-restored", item, items: [item], message: restoredMessage });
2346
+ }
2347
+ });
2076
2348
  } catch (cause) {
2077
2349
  setError(cause);
2078
2350
  }
@@ -2093,15 +2365,34 @@ function useKeepStaleNotice({ item, onRetry, onRemoved }) {
2093
2365
  }
2094
2366
  function useKeepPruneStale({ statuses, onPruned }) {
2095
2367
  const context = useKeepContext2();
2096
- const staleIds = context.items.filter((item) => item.status && statuses.includes(item.status)).map((item) => item.id);
2368
+ const emitFeedback = useKeepUiFeedback();
2369
+ const staleItems = context.items.filter((item) => item.status && statuses.includes(item.status));
2370
+ const staleIds = staleItems.map((item) => item.id);
2371
+ const restoredMessage = useUiLabel("restoredMessage");
2372
+ const prunedMessage = useUiLabel("stalePrunedMessage");
2373
+ const undoLabel = useUiLabel("undo");
2097
2374
  return {
2098
2375
  staleIds,
2099
2376
  isMutating: context.isMutating,
2100
2377
  label: useUiLabel("pruneStale"),
2101
2378
  prune: async () => {
2102
2379
  const ids = [...staleIds];
2380
+ const items = [...staleItems];
2103
2381
  await context.removeItemsWithUndo(ids);
2104
2382
  onPruned?.(ids);
2383
+ if (items.length > 0) {
2384
+ emitFeedback({
2385
+ type: "stale-pruned",
2386
+ item: items[0],
2387
+ items,
2388
+ message: prunedMessage,
2389
+ undoLabel,
2390
+ undo: async () => {
2391
+ await context.undoLastRemoval();
2392
+ emitFeedback({ type: "item-restored", item: items[0], items, message: restoredMessage });
2393
+ }
2394
+ });
2395
+ }
2105
2396
  }
2106
2397
  };
2107
2398
  }
@@ -2219,8 +2510,14 @@ function KeepPruneStaleButton({
2219
2510
  }
2220
2511
 
2221
2512
  // src/KeepItemCard.tsx
2222
- import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
2223
- function KeepItemCard({
2513
+ import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
2514
+ var KeepItemCardContext = createContext3(null);
2515
+ function useKeepItemCardCompound(part) {
2516
+ const context = useContext3(KeepItemCardContext);
2517
+ if (!context) throw new Error(`KeepItemCard.${part} must be rendered inside KeepItemCard.`);
2518
+ return context;
2519
+ }
2520
+ function KeepItemCardRoot({
2224
2521
  item,
2225
2522
  title,
2226
2523
  getTitle,
@@ -2246,6 +2543,7 @@ function KeepItemCard({
2246
2543
  linkComponent: LinkComponent,
2247
2544
  linkTargetAttribute,
2248
2545
  linkRel,
2546
+ highlightQuery,
2249
2547
  className,
2250
2548
  ...rootProps
2251
2549
  }) {
@@ -2260,6 +2558,8 @@ function KeepItemCard({
2260
2558
  onRemoveError,
2261
2559
  onRemoved
2262
2560
  });
2561
+ const contextQuery = useKeepSearchQuery();
2562
+ const searchQuery = highlightQuery ?? contextQuery;
2263
2563
  const contentChildren = asChild && isValidElement2(children) ? void 0 : children;
2264
2564
  function renderLink(content) {
2265
2565
  if (!view.href || !view.isAvailable) return content;
@@ -2272,21 +2572,47 @@ function KeepItemCard({
2272
2572
  };
2273
2573
  return LinkComponent ? /* @__PURE__ */ jsx9(LinkComponent, { ...linkProps }) : /* @__PURE__ */ jsx9("a", { ...linkProps });
2274
2574
  }
2275
- const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? /* @__PURE__ */ jsxs5(Fragment3, { children: [
2276
- view.imageProps ? renderImage?.({ ...view.imageProps, alt: imageAlt ?? view.imageProps.alt }, item) ?? (ImageComponent ? /* @__PURE__ */ jsx9(ImageComponent, { ...view.imageProps, alt: imageAlt ?? view.imageProps.alt }) : /* @__PURE__ */ jsx9("img", { ...view.imageProps, alt: imageAlt ?? view.imageProps.alt })) : null,
2277
- /* @__PURE__ */ jsx9("h3", { children: linkTarget === "title" ? view.isAvailable ? renderLink(view.resolvedTitle) : view.href ? /* @__PURE__ */ jsx9("span", { "aria-disabled": "true", "data-link-disabled": "true", children: view.resolvedTitle }) : view.resolvedTitle : view.resolvedTitle }),
2278
- showSavedAt ? /* @__PURE__ */ jsxs5("div", { "data-card-meta": true, children: [
2279
- /* @__PURE__ */ jsxs5("span", { children: [
2280
- view.labels.savedAt,
2281
- ":"
2282
- ] }),
2283
- " ",
2284
- /* @__PURE__ */ jsx9("time", { dateTime: new Date(item.savedAt).toISOString(), children: formatSavedAt(item.savedAt) })
2285
- ] }) : null,
2286
- showTags && (item.tags?.length ?? 0) > 0 ? renderTags ? renderTags(item.tags ?? [], item) : /* @__PURE__ */ jsx9("ul", { "aria-label": view.labels.tags, children: item.tags?.map((tag) => /* @__PURE__ */ jsx9("li", { children: tag }, tag)) }) : null,
2287
- view.itemState.error ? /* @__PURE__ */ jsx9("p", { role: "alert", children: getErrorMessage2(view.itemState.error, view.labels.error) }) : null,
2288
- view.statusLabel ? /* @__PURE__ */ jsx9(KeepStaleNotice, { item, onRetry, onRemoved }) : null,
2289
- showSaveButton && !view.statusLabel ? /* @__PURE__ */ jsx9(
2575
+ function renderTitle(content) {
2576
+ if (linkTarget !== "title") return content;
2577
+ if (view.isAvailable) return renderLink(content);
2578
+ return view.href ? /* @__PURE__ */ jsx9("span", { "aria-disabled": "true", "data-link-disabled": "true", children: content }) : content;
2579
+ }
2580
+ const resolvedImageProps = view.imageProps ? { ...view.imageProps, alt: imageAlt ?? view.imageProps.alt } : void 0;
2581
+ const imageSource = resolvedImageProps?.src;
2582
+ const [imageStatus, setImageStatus] = useState5(imageSource ? "loading" : "error");
2583
+ useEffect4(() => {
2584
+ setImageStatus(imageSource ? "loading" : "error");
2585
+ }, [imageSource]);
2586
+ let image = null;
2587
+ if (resolvedImageProps) {
2588
+ const imagePropsWithHandlers = {
2589
+ ...resolvedImageProps,
2590
+ src: resolvedImageProps.src,
2591
+ alt: imageAlt ?? resolvedImageProps.alt,
2592
+ onLoad: (event) => {
2593
+ resolvedImageProps.onLoad?.(event);
2594
+ setImageStatus("loaded");
2595
+ },
2596
+ onError: (event) => {
2597
+ resolvedImageProps.onError?.(event);
2598
+ setImageStatus("error");
2599
+ }
2600
+ };
2601
+ image = renderImage?.(imagePropsWithHandlers, item) ?? (ImageComponent ? /* @__PURE__ */ jsx9(ImageComponent, { ...imagePropsWithHandlers }) : /* @__PURE__ */ jsx9("img", { ...imagePropsWithHandlers, alt: imagePropsWithHandlers.alt }));
2602
+ }
2603
+ const tags = showTags ? item.tags ?? [] : [];
2604
+ const renderedTags = showTags && tags.length > 0 ? renderTags?.(tags, item) ?? null : null;
2605
+ const meta = showSavedAt ? /* @__PURE__ */ jsxs5("div", { "data-card-meta": true, children: [
2606
+ /* @__PURE__ */ jsxs5("span", { children: [
2607
+ view.labels.savedAt,
2608
+ ":"
2609
+ ] }),
2610
+ " ",
2611
+ /* @__PURE__ */ jsx9("time", { dateTime: new Date(item.savedAt).toISOString(), children: formatSavedAt(item.savedAt) })
2612
+ ] }) : null;
2613
+ const error = view.itemState.error ? /* @__PURE__ */ jsx9("p", { role: "alert", children: getErrorMessage2(view.itemState.error, view.labels.error) }) : null;
2614
+ const actions = view.statusLabel ? /* @__PURE__ */ jsx9(KeepStaleNotice, { item, onRetry, onRemoved }) : /* @__PURE__ */ jsxs5(Fragment4, { children: [
2615
+ showSaveButton ? /* @__PURE__ */ jsx9(
2290
2616
  KeepButton,
2291
2617
  {
2292
2618
  item: toKeepButtonItem(item),
@@ -2294,7 +2620,7 @@ function KeepItemCard({
2294
2620
  getAriaLabel: (buttonState) => `${buttonState.isSaved ? view.labels.remove : view.labels.save} ${String(view.resolvedTitle)}`
2295
2621
  }
2296
2622
  ) : null,
2297
- !view.statusLabel ? /* @__PURE__ */ jsx9(
2623
+ /* @__PURE__ */ jsx9(
2298
2624
  "button",
2299
2625
  {
2300
2626
  type: "button",
@@ -2303,10 +2629,30 @@ function KeepItemCard({
2303
2629
  disabled: view.itemState.isMutating,
2304
2630
  children: removeLabel ?? view.labels.remove
2305
2631
  }
2306
- ) : null
2632
+ )
2633
+ ] });
2634
+ const compoundValue = {
2635
+ resolvedTitle: view.resolvedTitle,
2636
+ renderTitle,
2637
+ image: imageStatus === "error" ? null : image,
2638
+ imageStatus,
2639
+ fallbackLabel: String(view.resolvedTitle),
2640
+ tags,
2641
+ tagsLabel: view.labels.tags,
2642
+ renderedTags,
2643
+ meta,
2644
+ error,
2645
+ actions,
2646
+ renderText: (content) => /* @__PURE__ */ jsx9(KeepHighlight, { query: searchQuery, children: content })
2647
+ };
2648
+ const defaultBody = /* @__PURE__ */ jsxs5(Fragment4, { children: [
2649
+ /* @__PURE__ */ jsx9(KeepItemCardMedia, {}),
2650
+ /* @__PURE__ */ jsx9(KeepItemCardContent, {}),
2651
+ /* @__PURE__ */ jsx9(KeepItemCardActions, {})
2307
2652
  ] });
2653
+ const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? defaultBody;
2308
2654
  const linkedBody = linkTarget === "card" && view.isAvailable ? renderLink(body) : body;
2309
- return renderRoot(
2655
+ const root = renderRoot(
2310
2656
  asChild,
2311
2657
  isValidElement2(children) ? children : void 0,
2312
2658
  {
@@ -2323,6 +2669,68 @@ function KeepItemCard({
2323
2669
  linkedBody,
2324
2670
  "KeepItemCard"
2325
2671
  );
2672
+ return /* @__PURE__ */ jsx9(KeepItemCardContext.Provider, { value: compoundValue, children: root });
2673
+ }
2674
+ function KeepItemCardMedia({ children, fallback, ...props }) {
2675
+ const context = useKeepItemCardCompound("Media");
2676
+ return /* @__PURE__ */ jsx9("div", { ...props, "data-keep-card-part": "media", "data-media-status": context.imageStatus, children: children ?? context.image ?? /* @__PURE__ */ jsx9("span", { role: "img", "aria-label": context.fallbackLabel, "data-keep-card-fallback": "true", children: fallback ?? /* @__PURE__ */ jsx9(KeepMediaPlaceholderIcon, {}) }) });
2677
+ }
2678
+ function KeepItemCardContent({ children, ...props }) {
2679
+ const context = useKeepItemCardCompound("Content");
2680
+ return /* @__PURE__ */ jsx9("div", { ...props, "data-keep-card-part": "content", children: children === void 0 ? /* @__PURE__ */ jsxs5(Fragment4, { children: [
2681
+ /* @__PURE__ */ jsx9(KeepItemCardTitle, {}),
2682
+ context.meta,
2683
+ /* @__PURE__ */ jsx9(KeepItemCardTags, {}),
2684
+ context.error
2685
+ ] }) : context.renderText(children) });
2686
+ }
2687
+ function KeepItemCardTitle({ as = "h3", children, ...props }) {
2688
+ const context = useKeepItemCardCompound("Title");
2689
+ return createElement2(
2690
+ as,
2691
+ { ...props, "data-keep-card-part": "title" },
2692
+ context.renderTitle(context.renderText(children ?? context.resolvedTitle))
2693
+ );
2694
+ }
2695
+ function KeepMediaPlaceholderIcon() {
2696
+ return /* @__PURE__ */ jsxs5("svg", { "data-media-fallback-icon": "true", viewBox: "0 0 24 24", "aria-hidden": "true", children: [
2697
+ /* @__PURE__ */ jsx9(
2698
+ "path",
2699
+ {
2700
+ d: "M4 5.5A1.5 1.5 0 0 1 5.5 4h13A1.5 1.5 0 0 1 20 5.5v13a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 4 18.5v-13Z",
2701
+ fill: "none",
2702
+ stroke: "currentColor"
2703
+ }
2704
+ ),
2705
+ /* @__PURE__ */ jsx9("circle", { cx: "9", cy: "9", r: "1.5", fill: "currentColor" }),
2706
+ /* @__PURE__ */ jsx9("path", { d: "m5.5 18 4.5-4.5 3 3 2-2L19 18", fill: "none", stroke: "currentColor" })
2707
+ ] });
2708
+ }
2709
+ function KeepItemCardTags({ children, ...props }) {
2710
+ const context = useKeepItemCardCompound("Tags");
2711
+ if (children === void 0 && context.renderedTags) return context.renderedTags;
2712
+ if (children === void 0 && context.tags.length === 0) return null;
2713
+ return /* @__PURE__ */ jsx9("ul", { ...props, "aria-label": props["aria-label"] ?? context.tagsLabel, "data-keep-card-part": "tags", children: children ?? context.tags.map((tag) => /* @__PURE__ */ jsx9("li", { children: tag }, tag)) });
2714
+ }
2715
+ function KeepItemCardActions({ children, ...props }) {
2716
+ const context = useKeepItemCardCompound("Actions");
2717
+ return /* @__PURE__ */ jsx9("div", { ...props, "data-keep-card-part": "actions", children: children ?? context.actions });
2718
+ }
2719
+ var KeepItemCard = Object.assign(KeepItemCardRoot, {
2720
+ Media: KeepItemCardMedia,
2721
+ Content: KeepItemCardContent,
2722
+ Title: KeepItemCardTitle,
2723
+ Tags: KeepItemCardTags,
2724
+ Actions: KeepItemCardActions
2725
+ });
2726
+ function KeepItemCardSkeleton({ layout = "list", ...props }) {
2727
+ return /* @__PURE__ */ jsxs5("article", { ...props, "aria-hidden": "true", "data-keepkit": "card-skeleton", "data-layout": layout, "data-state": "loading", children: [
2728
+ /* @__PURE__ */ jsx9("span", { "data-skeleton-part": "media" }),
2729
+ /* @__PURE__ */ jsx9("span", { "data-skeleton-part": "title" }),
2730
+ /* @__PURE__ */ jsx9("span", { "data-skeleton-part": "meta" }),
2731
+ /* @__PURE__ */ jsx9("span", { "data-skeleton-part": "tag" }),
2732
+ /* @__PURE__ */ jsx9("span", { "data-skeleton-part": "tag" })
2733
+ ] });
2326
2734
  }
2327
2735
  function formatSavedAt(timestamp) {
2328
2736
  return new Date(timestamp).toISOString().slice(0, 10);
@@ -2332,7 +2740,7 @@ function getErrorMessage2(error, fallback) {
2332
2740
  }
2333
2741
 
2334
2742
  // src/KeepList.tsx
2335
- import { jsx as jsx10 } from "react/jsx-runtime";
2743
+ import { Fragment as Fragment5, jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
2336
2744
  function KeepList(props) {
2337
2745
  const { fallback, onBoundaryError, boundaryResetKey, ...listProps } = props;
2338
2746
  const content = /* @__PURE__ */ jsx10(KeepListContent, { ...listProps });
@@ -2344,20 +2752,27 @@ function KeepListContent({
2344
2752
  children,
2345
2753
  renderItem,
2346
2754
  loading,
2755
+ renderLoading,
2756
+ loadingCount = 6,
2347
2757
  empty,
2348
2758
  error: errorContent,
2349
2759
  itemCardProps,
2350
2760
  layout = "list",
2351
2761
  asChild = false,
2762
+ onKeyDown,
2763
+ onFocusCapture,
2352
2764
  className,
2353
2765
  ...rootProps
2354
2766
  }) {
2355
2767
  const view = useKeepListView(query);
2768
+ const roving = useRovingTabIndex();
2356
2769
  const { state } = view;
2357
2770
  const body = getListBody(state, {
2358
2771
  children,
2359
2772
  renderItem,
2360
- loading: loading ?? view.labels.loading,
2773
+ loading: renderLoading !== void 0 ? renderLoading : loading,
2774
+ loadingCount,
2775
+ loadingLabel: view.labels.loading,
2361
2776
  empty: empty ?? view.labels.empty,
2362
2777
  error: errorContent ?? view.labels.error,
2363
2778
  itemCardProps,
@@ -2373,9 +2788,20 @@ function KeepListContent({
2373
2788
  "data-layout": layout,
2374
2789
  "aria-busy": state.isLoading || rootProps["aria-busy"],
2375
2790
  "data-state": getListState(state),
2376
- "data-loading": state.isLoading ? "true" : void 0
2791
+ "data-loading": state.isLoading ? "true" : void 0,
2792
+ "data-roving-tabindex": "true",
2793
+ role: rootProps.role ?? "group",
2794
+ ref: roving.ref,
2795
+ onKeyDown: (event) => {
2796
+ onKeyDown?.(event);
2797
+ if (!event.defaultPrevented) roving.onKeyDown(event);
2798
+ },
2799
+ onFocusCapture: (event) => {
2800
+ onFocusCapture?.(event);
2801
+ if (!event.defaultPrevented) roving.onFocusCapture(event);
2802
+ }
2377
2803
  },
2378
- body,
2804
+ /* @__PURE__ */ jsx10(KeepSearchQueryProvider, { query: query?.search?.query, children: body }),
2379
2805
  "KeepList"
2380
2806
  );
2381
2807
  }
@@ -2387,7 +2813,17 @@ function getListState(state) {
2387
2813
  }
2388
2814
  function getListBody(state, options) {
2389
2815
  if (state.error && state.items.length === 0) return resolveContent(options.error, state);
2390
- if (state.isLoading && !state.isHydrated) return resolveContent(options.loading, state);
2816
+ if (state.isLoading && !state.isHydrated) {
2817
+ if (options.loading !== void 0) return resolveContent(options.loading, state);
2818
+ const count = Number.isFinite(options.loadingCount) ? Math.max(0, Math.floor(options.loadingCount)) : 6;
2819
+ return /* @__PURE__ */ jsxs6(Fragment5, { children: [
2820
+ /* @__PURE__ */ jsx10("span", { role: "status", "data-keepkit": "loading-label", children: options.loadingLabel }),
2821
+ /* @__PURE__ */ jsx10("ul", { "data-keepkit": "skeleton-list", "data-layout": options.layout, children: Array.from({ length: count }, (_, index) => (
2822
+ // biome-ignore lint/suspicious/noArrayIndexKey: Static loading placeholders never reorder.
2823
+ /* @__PURE__ */ jsx10("li", { children: /* @__PURE__ */ jsx10(KeepItemCardSkeleton, { layout: options.layout }) }, index)
2824
+ )) })
2825
+ ] });
2826
+ }
2391
2827
  if (state.isHydrated && state.items.length === 0) return resolveContent(options.empty, state);
2392
2828
  if (typeof options.children === "function") return options.children(state);
2393
2829
  if (options.children !== void 0 && !isValidElement3(options.children)) return options.children;
@@ -2401,16 +2837,16 @@ import { isValidElement as isValidElement4 } from "react";
2401
2837
 
2402
2838
  // src/hooks/useKeepTagFilter.ts
2403
2839
  import { useKeepList as useKeepList4 } from "@keepkit/core/react";
2404
- import { useCallback, useMemo as useMemo3, useState as useState5 } from "react";
2840
+ import { useCallback as useCallback4, useMemo as useMemo3, useState as useState6 } from "react";
2405
2841
  function useKeepTagFilter(options) {
2406
2842
  const { query, controlledValue, defaultValue, onChange, onValueChange } = options;
2407
- const [uncontrolledValue, setUncontrolledValue] = useState5(defaultValue);
2843
+ const [uncontrolledValue, setUncontrolledValue] = useState6(defaultValue);
2408
2844
  const resolvedValue = controlledValue ?? uncontrolledValue;
2409
2845
  const list = useKeepList4({
2410
2846
  ...query,
2411
2847
  tags: resolvedValue ? [...query?.tags ?? [], resolvedValue] : query?.tags
2412
2848
  });
2413
- const select = useCallback(
2849
+ const select = useCallback4(
2414
2850
  (tag) => {
2415
2851
  if (controlledValue === void 0) setUncontrolledValue(tag);
2416
2852
  onChange?.(tag);
@@ -2430,7 +2866,7 @@ function useKeepTagFilter(options) {
2430
2866
  }
2431
2867
 
2432
2868
  // src/KeepTagFilter.tsx
2433
- import { jsx as jsx11, jsxs as jsxs6 } from "react/jsx-runtime";
2869
+ import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
2434
2870
  function KeepTagFilter({
2435
2871
  query,
2436
2872
  value: controlledValue,
@@ -2448,7 +2884,7 @@ function KeepTagFilter({
2448
2884
  }) {
2449
2885
  const view = useKeepTagFilter({ query, controlledValue, defaultValue, onChange, onValueChange });
2450
2886
  const contentChildren = asChild && isValidElement4(children) ? void 0 : children;
2451
- const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? /* @__PURE__ */ jsxs6("fieldset", { children: [
2887
+ const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? /* @__PURE__ */ jsxs7("fieldset", { children: [
2452
2888
  /* @__PURE__ */ jsx11("legend", { children: ariaLabel ?? view.labels.aria }),
2453
2889
  /* @__PURE__ */ jsx11(
2454
2890
  "button",
@@ -2460,7 +2896,7 @@ function KeepTagFilter({
2460
2896
  children: allLabel ?? view.labels.all
2461
2897
  }
2462
2898
  ),
2463
- view.state.tags.map((tag) => /* @__PURE__ */ jsxs6(
2899
+ view.state.tags.map((tag) => /* @__PURE__ */ jsxs7(
2464
2900
  "button",
2465
2901
  {
2466
2902
  type: "button",
@@ -2469,7 +2905,7 @@ function KeepTagFilter({
2469
2905
  onClick: () => view.state.select(tag),
2470
2906
  children: [
2471
2907
  renderTag ? renderTag(tag, view.state.tagCounts[tag] ?? 0, view.state.value === tag) : tag,
2472
- /* @__PURE__ */ jsxs6("span", { children: [
2908
+ /* @__PURE__ */ jsxs7("span", { children: [
2473
2909
  " (",
2474
2910
  view.state.tagCounts[tag] ?? 0,
2475
2911
  ")"
@@ -2495,12 +2931,12 @@ function KeepTagFilter({
2495
2931
  }
2496
2932
 
2497
2933
  // src/hooks/useQueryControls.ts
2498
- import { useEffect as useEffect2, useState as useState6 } from "react";
2934
+ import { useEffect as useEffect5, useState as useState7 } from "react";
2499
2935
  function useKeepSearchInput(options) {
2500
2936
  const { controlledValue, defaultValue, debounceMs, onValueChange } = options;
2501
- const [uncontrolledValue, setUncontrolledValue] = useState6(defaultValue);
2937
+ const [uncontrolledValue, setUncontrolledValue] = useState7(defaultValue);
2502
2938
  const value = controlledValue ?? uncontrolledValue;
2503
- useEffect2(() => {
2939
+ useEffect5(() => {
2504
2940
  if (!onValueChange) return;
2505
2941
  if (debounceMs <= 0) {
2506
2942
  onValueChange(value);
@@ -2519,7 +2955,7 @@ function useKeepSearchInput(options) {
2519
2955
  }
2520
2956
  function useKeepSortSelect(options) {
2521
2957
  const { controlledValue, defaultValue, onValueChange } = options;
2522
- const [uncontrolledValue, setUncontrolledValue] = useState6(defaultValue);
2958
+ const [uncontrolledValue, setUncontrolledValue] = useState7(defaultValue);
2523
2959
  const value = controlledValue ?? uncontrolledValue;
2524
2960
  return {
2525
2961
  value,
@@ -2567,7 +3003,7 @@ function getVisiblePages(currentPage, pageCount, maxPageButtons) {
2567
3003
  }
2568
3004
 
2569
3005
  // src/query-controls.tsx
2570
- import { Fragment as Fragment4, jsx as jsx12, jsxs as jsxs7 } from "react/jsx-runtime";
3006
+ import { Fragment as Fragment6, jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
2571
3007
  function KeepSearchInput({
2572
3008
  value: controlledValue,
2573
3009
  defaultValue = "",
@@ -2603,7 +3039,7 @@ function KeepSortSelect({
2603
3039
  ...props
2604
3040
  }) {
2605
3041
  const view = useKeepSortSelect({ controlledValue, defaultValue, onValueChange });
2606
- const options = children ?? /* @__PURE__ */ jsxs7(Fragment4, { children: [
3042
+ const options = children ?? /* @__PURE__ */ jsxs8(Fragment6, { children: [
2607
3043
  /* @__PURE__ */ jsx12("option", { value: "updatedAt:desc", children: view.labels.updatedNewest }),
2608
3044
  /* @__PURE__ */ jsx12("option", { value: "updatedAt:asc", children: view.labels.updatedOldest }),
2609
3045
  /* @__PURE__ */ jsx12("option", { value: "savedAt:desc", children: view.labels.savedNewest }),
@@ -2642,7 +3078,7 @@ function KeepPagination({
2642
3078
  };
2643
3079
  if (render)
2644
3080
  return /* @__PURE__ */ jsx12("nav", { ...navProps, children: render({ page: view.currentPage, pageCount: view.pageCount, goToPage: view.goToPage }) });
2645
- return /* @__PURE__ */ jsxs7("nav", { ...navProps, children: [
3081
+ return /* @__PURE__ */ jsxs8("nav", { ...navProps, children: [
2646
3082
  /* @__PURE__ */ jsx12(
2647
3083
  "button",
2648
3084
  {
@@ -2679,7 +3115,7 @@ function KeepPagination({
2679
3115
  }
2680
3116
 
2681
3117
  // src/KeepCollection.tsx
2682
- import { jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
3118
+ import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
2683
3119
  function KeepCollection(props) {
2684
3120
  const { fallback, onBoundaryError, boundaryResetKey, ...collectionProps } = props;
2685
3121
  const content = /* @__PURE__ */ jsx13(KeepCollectionContent, { ...collectionProps });
@@ -2696,13 +3132,15 @@ function KeepCollectionContent({
2696
3132
  renderItem,
2697
3133
  itemCardProps,
2698
3134
  loading,
3135
+ renderLoading,
3136
+ loadingCount,
2699
3137
  empty,
2700
3138
  error,
2701
3139
  className,
2702
3140
  ...rootProps
2703
3141
  }) {
2704
3142
  const view = useKeepCollection({ query, pageSize, urlSync, urlAdapter, features });
2705
- return /* @__PURE__ */ jsxs8(
3143
+ return /* @__PURE__ */ jsxs9(
2706
3144
  "section",
2707
3145
  {
2708
3146
  ...rootProps,
@@ -2713,7 +3151,7 @@ function KeepCollectionContent({
2713
3151
  "data-state": getCollectionState(view.list),
2714
3152
  "data-loading": view.list.isLoading || view.list.isMutating ? "true" : void 0,
2715
3153
  children: [
2716
- /* @__PURE__ */ jsxs8("div", { children: [
3154
+ /* @__PURE__ */ jsxs9("div", { children: [
2717
3155
  view.enabled.search ? /* @__PURE__ */ jsx13(KeepSearchInput, { value: view.searchValue, onValueChange: view.setSearchValue }) : null,
2718
3156
  view.enabled.sort ? /* @__PURE__ */ jsx13(KeepSortSelect, { value: view.sortValue, onValueChange: view.setSortValue }) : null,
2719
3157
  view.enabled.tagFilter ? /* @__PURE__ */ jsx13(KeepTagFilter, { query, value: view.tag, onValueChange: view.setTag }) : null
@@ -2726,6 +3164,8 @@ function KeepCollectionContent({
2726
3164
  itemCardProps,
2727
3165
  layout,
2728
3166
  loading,
3167
+ renderLoading,
3168
+ loadingCount,
2729
3169
  empty,
2730
3170
  error
2731
3171
  }
@@ -2753,8 +3193,31 @@ function getCollectionState(list) {
2753
3193
 
2754
3194
  // src/KeepLayout.tsx
2755
3195
  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 });
3196
+ function KeepLayout({ layout = "list", children, onKeyDown, onFocusCapture, ...props }) {
3197
+ const roving = useRovingTabIndex();
3198
+ return (
3199
+ // biome-ignore lint/a11y/noStaticElementInteractions: The group manages keyboard focus for descendant cards.
3200
+ /* @__PURE__ */ jsx14(
3201
+ "div",
3202
+ {
3203
+ ...props,
3204
+ ref: roving.ref,
3205
+ "data-keepkit": "layout",
3206
+ "data-layout": layout,
3207
+ "data-roving-tabindex": "true",
3208
+ role: props.role ?? "group",
3209
+ onKeyDown: (event) => {
3210
+ onKeyDown?.(event);
3211
+ if (!event.defaultPrevented) roving.onKeyDown(event);
3212
+ },
3213
+ onFocusCapture: (event) => {
3214
+ onFocusCapture?.(event);
3215
+ if (!event.defaultPrevented) roving.onFocusCapture(event);
3216
+ },
3217
+ children
3218
+ }
3219
+ )
3220
+ );
2758
3221
  }
2759
3222
 
2760
3223
  // src/KeepNoteEditor.tsx
@@ -2762,16 +3225,16 @@ import { isValidElement as isValidElement5 } from "react";
2762
3225
 
2763
3226
  // src/hooks/useKeepNoteEditor.ts
2764
3227
  import { useKeepItem as useKeepItem3 } from "@keepkit/core/react";
2765
- import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef3, useState as useState7 } from "react";
3228
+ import { useCallback as useCallback5, useEffect as useEffect6, useRef as useRef5, useState as useState8 } from "react";
2766
3229
  function useKeepNoteEditor({ item, debounceMs, onSaved, onSaveError }) {
2767
3230
  const itemState = useKeepItem3(item);
2768
3231
  const { error, isMutating, item: savedItem, updateNote } = itemState;
2769
- const [note, setNote] = useState7(item.note ?? "");
3232
+ const [note, setNote] = useState8(item.note ?? "");
2770
3233
  const baselineNote = savedItem?.note ?? item.note ?? "";
2771
3234
  const isDirty = note !== baselineNote;
2772
- const lastSavedNoteRef = useRef3(void 0);
2773
- useEffect3(() => setNote(baselineNote), [baselineNote]);
2774
- const save = useCallback2(async () => {
3235
+ const lastSavedNoteRef = useRef5(void 0);
3236
+ useEffect6(() => setNote(baselineNote), [baselineNote]);
3237
+ const save = useCallback5(async () => {
2775
3238
  const nextNote = note.trim() || void 0;
2776
3239
  try {
2777
3240
  await updateNote(nextNote);
@@ -2782,7 +3245,7 @@ function useKeepNoteEditor({ item, debounceMs, onSaved, onSaveError }) {
2782
3245
  throw cause;
2783
3246
  }
2784
3247
  }, [note, onSaveError, onSaved, updateNote]);
2785
- useEffect3(() => {
3248
+ useEffect6(() => {
2786
3249
  if (!isDirty || debounceMs <= 0 || lastSavedNoteRef.current === note) return;
2787
3250
  const timer = window.setTimeout(() => void save().catch(() => void 0), debounceMs);
2788
3251
  return () => window.clearTimeout(timer);
@@ -2817,7 +3280,7 @@ function useKeepNoteEditor({ item, debounceMs, onSaved, onSaveError }) {
2817
3280
  }
2818
3281
 
2819
3282
  // src/KeepNoteEditor.tsx
2820
- import { Fragment as Fragment5, jsx as jsx15, jsxs as jsxs9 } from "react/jsx-runtime";
3283
+ import { Fragment as Fragment7, jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
2821
3284
  function KeepNoteEditor({
2822
3285
  item,
2823
3286
  label,
@@ -2835,8 +3298,8 @@ function KeepNoteEditor({
2835
3298
  const view = useKeepNoteEditor({ item, debounceMs, onSaved, onSaveError });
2836
3299
  const { error, isDirty, isSaving, note, setNote } = view.state;
2837
3300
  const contentChildren = asChild && isValidElement5(children) ? void 0 : children;
2838
- const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? /* @__PURE__ */ jsxs9(Fragment5, { children: [
2839
- /* @__PURE__ */ jsxs9("label", { children: [
3301
+ const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? /* @__PURE__ */ jsxs10(Fragment7, { children: [
3302
+ /* @__PURE__ */ jsxs10("label", { children: [
2840
3303
  label ?? view.labels.note,
2841
3304
  /* @__PURE__ */ jsx15(
2842
3305
  "textarea",
@@ -2853,7 +3316,7 @@ function KeepNoteEditor({
2853
3316
  /* @__PURE__ */ jsx15("button", { type: "submit", "data-keep-action": "save-note", disabled: isSaving, "aria-busy": isSaving, children: saveLabel ?? view.labels.save })
2854
3317
  ] });
2855
3318
  if (!asChild) {
2856
- return /* @__PURE__ */ jsxs9(
3319
+ return /* @__PURE__ */ jsxs10(
2857
3320
  "form",
2858
3321
  {
2859
3322
  ...formProps,
@@ -2892,18 +3355,119 @@ function getErrorMessage3(error, fallback) {
2892
3355
  return error instanceof Error ? error.message : fallback;
2893
3356
  }
2894
3357
 
2895
- // src/hooks/useKeepSyncRecoveryDialog.ts
3358
+ // src/KeepReorderableList.tsx
3359
+ import { useState as useState9 } from "react";
3360
+ import { jsx as jsx16 } from "react/jsx-runtime";
3361
+ function KeepReorderableList({
3362
+ items,
3363
+ onReorder,
3364
+ renderItem,
3365
+ itemLabel = (_item, index) => `Move item ${index + 1}`,
3366
+ ...props
3367
+ }) {
3368
+ const [draggedId, setDraggedId] = useState9(null);
3369
+ const ids = items.map((item) => item.id);
3370
+ function commit(nextIds) {
3371
+ void onReorder(nextIds);
3372
+ }
3373
+ function move(index, targetIndex) {
3374
+ if (targetIndex < 0 || targetIndex >= ids.length || targetIndex === index) return;
3375
+ const next = [...ids];
3376
+ const [id] = next.splice(index, 1);
3377
+ if (id === void 0) return;
3378
+ next.splice(targetIndex, 0, id);
3379
+ commit(next);
3380
+ }
3381
+ return /* @__PURE__ */ jsx16("ul", { ...props, "data-keepkit": "reorderable-list", children: items.map((item, index) => {
3382
+ const moveUp = () => move(index, index - 1);
3383
+ const moveDown = () => move(index, index + 1);
3384
+ const onHandleKeyDown = (event) => {
3385
+ if (event.key === "ArrowUp") {
3386
+ event.preventDefault();
3387
+ moveUp();
3388
+ } else if (event.key === "ArrowDown") {
3389
+ event.preventDefault();
3390
+ moveDown();
3391
+ }
3392
+ };
3393
+ const state = {
3394
+ index,
3395
+ isDragging: draggedId === item.id,
3396
+ moveUp,
3397
+ moveDown,
3398
+ dragHandleProps: {
3399
+ role: "button",
3400
+ tabIndex: 0,
3401
+ draggable: true,
3402
+ "aria-label": itemLabel(item, index),
3403
+ "aria-grabbed": draggedId === item.id,
3404
+ onDragStart: () => setDraggedId(item.id),
3405
+ onDragEnd: () => setDraggedId(null),
3406
+ onKeyDown: onHandleKeyDown
3407
+ }
3408
+ };
3409
+ return /* @__PURE__ */ jsx16(
3410
+ "li",
3411
+ {
3412
+ "data-reorder-index": index,
3413
+ "data-dragging": draggedId === item.id ? "true" : void 0,
3414
+ onDragOver: (event) => {
3415
+ if (draggedId && draggedId !== item.id) event.preventDefault();
3416
+ },
3417
+ onDrop: (event) => {
3418
+ event.preventDefault();
3419
+ if (!draggedId || draggedId === item.id) return;
3420
+ const sourceIndex = ids.indexOf(draggedId);
3421
+ setDraggedId(null);
3422
+ move(sourceIndex, index);
3423
+ },
3424
+ children: renderItem(item, state)
3425
+ },
3426
+ item.id
3427
+ );
3428
+ }) });
3429
+ }
3430
+
3431
+ // src/hooks/useKeepSyncFeedback.ts
2896
3432
  import { useKeepContext as useKeepContext3 } from "@keepkit/core/react";
2897
- import { useEffect as useEffect4, useState as useState8 } from "react";
3433
+ import { useEffect as useEffect7, useRef as useRef6 } from "react";
3434
+ function useKeepSyncFeedback() {
3435
+ const { syncState } = useKeepContext3();
3436
+ const emitFeedback = useKeepUiFeedback();
3437
+ const completedMessage = useUiLabel("syncSynced");
3438
+ const failedMessage = useUiLabel("syncFailedMessage");
3439
+ const previousStatus = useRef6("idle");
3440
+ useEffect7(() => {
3441
+ const previous = previousStatus.current;
3442
+ previousStatus.current = syncState.status;
3443
+ if (syncState.status === "error" && previous !== "error") {
3444
+ emitFeedback({ type: "sync-failed", error: syncState.error, message: failedMessage });
3445
+ return;
3446
+ }
3447
+ if (syncState.status === "synced" && (previous === "pending" || previous === "syncing" || previous === "conflict" || previous === "error")) {
3448
+ emitFeedback({ type: "sync-completed", message: completedMessage });
3449
+ }
3450
+ }, [completedMessage, emitFeedback, failedMessage, syncState.error, syncState.status]);
3451
+ }
3452
+
3453
+ // src/KeepSyncFeedbackObserver.tsx
3454
+ function KeepSyncFeedbackObserver() {
3455
+ useKeepSyncFeedback();
3456
+ return null;
3457
+ }
3458
+
3459
+ // src/hooks/useKeepSyncRecoveryDialog.ts
3460
+ import { useKeepContext as useKeepContext4 } from "@keepkit/core/react";
3461
+ import { useEffect as useEffect8, useState as useState10 } from "react";
2898
3462
  function useKeepSyncRecoveryDialog(options) {
2899
3463
  const { open, onOpenChange, conflicts, onManualMerge } = options;
2900
- const context = useKeepContext3();
3464
+ const context = useKeepContext4();
2901
3465
  const conflictList = conflicts ?? context.syncState.conflicts ?? [];
2902
3466
  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(() => {
3467
+ const [dismissed, setDismissed] = useState10(false);
3468
+ const [busyId, setBusyId] = useState10();
3469
+ const [error, setError] = useState10();
3470
+ useEffect8(() => {
2907
3471
  if (hasRecovery) setDismissed(false);
2908
3472
  }, [hasRecovery]);
2909
3473
  return {
@@ -2948,7 +3512,7 @@ function useKeepSyncRecoveryDialog(options) {
2948
3512
  }
2949
3513
 
2950
3514
  // src/KeepSyncRecoveryDialog.tsx
2951
- import { jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
3515
+ import { jsx as jsx17, jsxs as jsxs11 } from "react/jsx-runtime";
2952
3516
  function KeepSyncRecoveryDialog({
2953
3517
  open,
2954
3518
  onOpenChange,
@@ -2963,7 +3527,7 @@ function KeepSyncRecoveryDialog({
2963
3527
  }) {
2964
3528
  const view = useKeepSyncRecoveryDialog({ open, onOpenChange, conflicts, onManualMerge });
2965
3529
  if (!view.isOpen) return null;
2966
- return /* @__PURE__ */ jsxs10(
3530
+ return /* @__PURE__ */ jsxs11(
2967
3531
  "section",
2968
3532
  {
2969
3533
  ...props,
@@ -2977,17 +3541,17 @@ function KeepSyncRecoveryDialog({
2977
3541
  "data-state": view.conflictList.length > 0 ? "conflict" : "error",
2978
3542
  "data-loading": view.busyId !== void 0 ? "true" : void 0,
2979
3543
  children: [
2980
- /* @__PURE__ */ jsxs10("header", { children: [
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 })
3544
+ /* @__PURE__ */ jsxs11("header", { children: [
3545
+ /* @__PURE__ */ jsx17("h2", { id: "keepkit-sync-recovery-title", children: title ?? view.labels.title }),
3546
+ /* @__PURE__ */ jsx17("button", { type: "button", "data-keep-action": "close-dialog", onClick: view.close, "aria-label": view.labels.close, children: view.labels.close })
2983
3547
  ] }),
2984
3548
  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: [
2988
- /* @__PURE__ */ jsx16("h3", { children: getMetaTitle(conflict.operation.item?.meta) ?? conflict.id }),
2989
- /* @__PURE__ */ jsxs10("div", { "data-conflict-preview": true, children: [
2990
- /* @__PURE__ */ jsx16(
3549
+ view.conflictList.length > 0 ? /* @__PURE__ */ jsxs11("div", { children: [
3550
+ /* @__PURE__ */ jsx17("p", { children: view.labels.conflict }),
3551
+ view.conflictList.map((conflict) => /* @__PURE__ */ jsxs11("article", { "data-conflict-id": conflict.id, children: [
3552
+ /* @__PURE__ */ jsx17("h3", { children: getMetaTitle(conflict.operation.item?.meta) ?? conflict.id }),
3553
+ /* @__PURE__ */ jsxs11("div", { "data-conflict-preview": true, children: [
3554
+ /* @__PURE__ */ jsx17(
2991
3555
  ConflictPreview,
2992
3556
  {
2993
3557
  item: conflict.operation.item,
@@ -2997,7 +3561,7 @@ function KeepSyncRecoveryDialog({
2997
3561
  side: "local"
2998
3562
  }
2999
3563
  ),
3000
- /* @__PURE__ */ jsx16(
3564
+ /* @__PURE__ */ jsx17(
3001
3565
  ConflictPreview,
3002
3566
  {
3003
3567
  item: conflict.remote,
@@ -3008,8 +3572,8 @@ function KeepSyncRecoveryDialog({
3008
3572
  }
3009
3573
  )
3010
3574
  ] }),
3011
- /* @__PURE__ */ jsxs10("div", { children: [
3012
- /* @__PURE__ */ jsx16(
3575
+ /* @__PURE__ */ jsxs11("div", { children: [
3576
+ /* @__PURE__ */ jsx17(
3013
3577
  "button",
3014
3578
  {
3015
3579
  type: "button",
@@ -3019,7 +3583,7 @@ function KeepSyncRecoveryDialog({
3019
3583
  children: view.labels.keepLocal
3020
3584
  }
3021
3585
  ),
3022
- /* @__PURE__ */ jsx16(
3586
+ /* @__PURE__ */ jsx17(
3023
3587
  "button",
3024
3588
  {
3025
3589
  type: "button",
@@ -3029,7 +3593,7 @@ function KeepSyncRecoveryDialog({
3029
3593
  children: view.labels.useServer
3030
3594
  }
3031
3595
  ),
3032
- /* @__PURE__ */ jsx16(
3596
+ /* @__PURE__ */ jsx17(
3033
3597
  "button",
3034
3598
  {
3035
3599
  type: "button",
@@ -3042,12 +3606,12 @@ function KeepSyncRecoveryDialog({
3042
3606
  ] })
3043
3607
  ] }, conflict.id))
3044
3608
  ] }) : null,
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 }),
3048
- backup ?? (showBackupControls ? /* @__PURE__ */ jsx16(KeepBackup, {}) : null)
3609
+ view.showBackupRecovery ? /* @__PURE__ */ jsxs11("section", { "data-recovery": "backup", children: [
3610
+ /* @__PURE__ */ jsx17("h3", { children: view.labels.backupRecovery }),
3611
+ /* @__PURE__ */ jsx17("p", { children: view.labels.backupRecoveryDescription }),
3612
+ backup ?? (showBackupControls ? /* @__PURE__ */ jsx17(KeepBackup, {}) : null)
3049
3613
  ] }) : 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
3614
+ view.error ? /* @__PURE__ */ jsx17("p", { id: "keepkit-sync-recovery-error", role: "alert", "aria-live": "assertive", children: view.error instanceof Error ? view.error.message : view.labels.error }) : null
3051
3615
  ]
3052
3616
  }
3053
3617
  );
@@ -3059,16 +3623,16 @@ function ConflictPreview({
3059
3623
  noteLabel,
3060
3624
  side
3061
3625
  }) {
3062
- return /* @__PURE__ */ jsxs10("article", { "data-conflict-version": side, "aria-label": heading, children: [
3063
- /* @__PURE__ */ jsx16("h4", { children: heading }),
3064
- /* @__PURE__ */ jsxs10("dl", { children: [
3065
- /* @__PURE__ */ jsxs10("div", { children: [
3066
- /* @__PURE__ */ jsx16("dt", { children: updatedAtLabel }),
3067
- /* @__PURE__ */ jsx16("dd", { children: item ? /* @__PURE__ */ jsx16("time", { dateTime: new Date(item.updatedAt).toISOString(), children: formatConflictDate(item.updatedAt) }) : "\u2014" })
3626
+ return /* @__PURE__ */ jsxs11("article", { "data-conflict-version": side, "aria-label": heading, children: [
3627
+ /* @__PURE__ */ jsx17("h4", { children: heading }),
3628
+ /* @__PURE__ */ jsxs11("dl", { children: [
3629
+ /* @__PURE__ */ jsxs11("div", { children: [
3630
+ /* @__PURE__ */ jsx17("dt", { children: updatedAtLabel }),
3631
+ /* @__PURE__ */ jsx17("dd", { children: item ? /* @__PURE__ */ jsx17("time", { dateTime: new Date(item.updatedAt).toISOString(), children: formatConflictDate(item.updatedAt) }) : "\u2014" })
3068
3632
  ] }),
3069
- /* @__PURE__ */ jsxs10("div", { children: [
3070
- /* @__PURE__ */ jsx16("dt", { children: noteLabel }),
3071
- /* @__PURE__ */ jsx16("dd", { children: item?.note || "\u2014" })
3633
+ /* @__PURE__ */ jsxs11("div", { children: [
3634
+ /* @__PURE__ */ jsx17("dt", { children: noteLabel }),
3635
+ /* @__PURE__ */ jsx17("dd", { children: item?.note || "\u2014" })
3072
3636
  ] })
3073
3637
  ] })
3074
3638
  ] });
@@ -3078,9 +3642,9 @@ function formatConflictDate(timestamp) {
3078
3642
  }
3079
3643
 
3080
3644
  // src/hooks/useKeepSyncStatusBanner.ts
3081
- import { useKeepContext as useKeepContext4 } from "@keepkit/core/react";
3645
+ import { useKeepContext as useKeepContext5 } from "@keepkit/core/react";
3082
3646
  function useKeepSyncStatusBanner({ onRetry, children }) {
3083
- const context = useKeepContext4();
3647
+ const context = useKeepContext5();
3084
3648
  const retryLabel = useUiLabel("retrySync");
3085
3649
  const resolveLabel = useUiLabel("resolveSync");
3086
3650
  const conflictLabel = useUiLabel("syncConflict");
@@ -3112,7 +3676,7 @@ function getErrorMessage4(error) {
3112
3676
  }
3113
3677
 
3114
3678
  // src/KeepSyncStatusBanner.tsx
3115
- import { jsx as jsx17, jsxs as jsxs11 } from "react/jsx-runtime";
3679
+ import { jsx as jsx18, jsxs as jsxs12 } from "react/jsx-runtime";
3116
3680
  function KeepSyncStatusBanner({
3117
3681
  onRetry,
3118
3682
  onResolveConflicts,
@@ -3122,7 +3686,7 @@ function KeepSyncStatusBanner({
3122
3686
  }) {
3123
3687
  const view = useKeepSyncStatusBanner({ onRetry, children });
3124
3688
  if (view.status === "idle" && !view.hasConflicts) return null;
3125
- return /* @__PURE__ */ jsxs11(
3689
+ return /* @__PURE__ */ jsxs12(
3126
3690
  "aside",
3127
3691
  {
3128
3692
  ...props,
@@ -3132,8 +3696,8 @@ function KeepSyncStatusBanner({
3132
3696
  "data-keepkit": "sync-status",
3133
3697
  "data-state": view.status,
3134
3698
  children: [
3135
- /* @__PURE__ */ jsx17("p", { children: view.message }),
3136
- view.showRetry ? /* @__PURE__ */ jsx17(
3699
+ /* @__PURE__ */ jsx18("p", { children: view.message }),
3700
+ view.showRetry ? /* @__PURE__ */ jsx18(
3137
3701
  "button",
3138
3702
  {
3139
3703
  type: "button",
@@ -3143,7 +3707,7 @@ function KeepSyncStatusBanner({
3143
3707
  children: view.retryLabel
3144
3708
  }
3145
3709
  ) : null,
3146
- view.hasConflicts ? /* @__PURE__ */ jsx17(
3710
+ view.hasConflicts ? /* @__PURE__ */ jsx18(
3147
3711
  "button",
3148
3712
  {
3149
3713
  type: "button",
@@ -3160,13 +3724,13 @@ function KeepSyncStatusBanner({
3160
3724
 
3161
3725
  // src/hooks/useKeepTagEditor.ts
3162
3726
  import { useKeepItem as useKeepItem4 } from "@keepkit/core/react";
3163
- import { useCallback as useCallback3, useEffect as useEffect5, useState as useState9 } from "react";
3727
+ import { useCallback as useCallback6, useEffect as useEffect9, useState as useState11 } from "react";
3164
3728
  function useKeepTagEditor({ item, onSaved, onSaveError }) {
3165
3729
  const itemState = useKeepItem4(item);
3166
- const [tags, setTags] = useState9(item.tags ?? []);
3167
- const [input, setInput] = useState9("");
3168
- useEffect5(() => setTags(itemState.item?.tags ?? item.tags ?? []), [item.tags, itemState.item?.tags]);
3169
- const save = useCallback3(async () => {
3730
+ const [tags, setTags] = useState11(item.tags ?? []);
3731
+ const [input, setInput] = useState11("");
3732
+ useEffect9(() => setTags(itemState.item?.tags ?? item.tags ?? []), [item.tags, itemState.item?.tags]);
3733
+ const save = useCallback6(async () => {
3170
3734
  const nextTags = normalizeUiTags(tags);
3171
3735
  try {
3172
3736
  await itemState.updateTags(nextTags);
@@ -3212,7 +3776,7 @@ function useKeepTagEditor({ item, onSaved, onSaveError }) {
3212
3776
  }
3213
3777
 
3214
3778
  // src/KeepTagEditor.tsx
3215
- import { Fragment as Fragment6, jsx as jsx18, jsxs as jsxs12 } from "react/jsx-runtime";
3779
+ import { Fragment as Fragment8, jsx as jsx19, jsxs as jsxs13 } from "react/jsx-runtime";
3216
3780
  function KeepTagEditor({
3217
3781
  item,
3218
3782
  availableTags = [],
@@ -3223,10 +3787,10 @@ function KeepTagEditor({
3223
3787
  }) {
3224
3788
  const view = useKeepTagEditor({ item, onSaved, onSaveError });
3225
3789
  const { isSaving, tags } = view.state;
3226
- const body = render ? render(view.state) : /* @__PURE__ */ jsxs12(Fragment6, { children: [
3227
- /* @__PURE__ */ jsxs12("label", { children: [
3790
+ const body = render ? render(view.state) : /* @__PURE__ */ jsxs13(Fragment8, { children: [
3791
+ /* @__PURE__ */ jsxs13("label", { children: [
3228
3792
  view.labels.tags,
3229
- /* @__PURE__ */ jsx18(
3793
+ /* @__PURE__ */ jsx19(
3230
3794
  "input",
3231
3795
  {
3232
3796
  "data-keep-action": "edit-tags",
@@ -3237,14 +3801,14 @@ function KeepTagEditor({
3237
3801
  }
3238
3802
  )
3239
3803
  ] }),
3240
- availableTags.length > 0 ? /* @__PURE__ */ jsx18("datalist", { id: `keep-tags-${item.id}`, children: availableTags.map((tag) => /* @__PURE__ */ jsx18("option", { value: tag }, tag)) }) : null,
3241
- /* @__PURE__ */ jsx18("ul", { "aria-label": view.labels.tags, children: tags.map((tag) => /* @__PURE__ */ jsxs12("li", { children: [
3804
+ availableTags.length > 0 ? /* @__PURE__ */ jsx19("datalist", { id: `keep-tags-${item.id}`, children: availableTags.map((tag) => /* @__PURE__ */ jsx19("option", { value: tag }, tag)) }) : null,
3805
+ /* @__PURE__ */ jsx19("ul", { "aria-label": view.labels.tags, children: tags.map((tag) => /* @__PURE__ */ jsxs13("li", { children: [
3242
3806
  tag,
3243
- /* @__PURE__ */ jsx18("button", { type: "button", "data-keep-action": "remove-tag", onClick: () => view.removeTag(tag), children: view.labels.remove })
3807
+ /* @__PURE__ */ jsx19("button", { type: "button", "data-keep-action": "remove-tag", onClick: () => view.removeTag(tag), children: view.labels.remove })
3244
3808
  ] }, tag)) }),
3245
- /* @__PURE__ */ jsx18("button", { type: "submit", "data-keep-action": "apply-tags", disabled: isSaving, "aria-busy": isSaving, children: view.labels.apply })
3809
+ /* @__PURE__ */ jsx19("button", { type: "submit", "data-keep-action": "apply-tags", disabled: isSaving, "aria-busy": isSaving, children: view.labels.apply })
3246
3810
  ] });
3247
- return /* @__PURE__ */ jsxs12(
3811
+ return /* @__PURE__ */ jsxs13(
3248
3812
  "form",
3249
3813
  {
3250
3814
  ...props,
@@ -3256,7 +3820,7 @@ function KeepTagEditor({
3256
3820
  "data-disabled": isSaving ? "true" : void 0,
3257
3821
  children: [
3258
3822
  body,
3259
- view.error ? /* @__PURE__ */ jsx18("p", { role: "alert", children: getErrorMessage5(view.error, view.labels.error) }) : null
3823
+ view.error ? /* @__PURE__ */ jsx19("p", { role: "alert", children: getErrorMessage5(view.error, view.labels.error) }) : null
3260
3824
  ]
3261
3825
  }
3262
3826
  );
@@ -3265,26 +3829,145 @@ function getErrorMessage5(error, fallback) {
3265
3829
  return error instanceof Error ? error.message : fallback;
3266
3830
  }
3267
3831
 
3832
+ // src/KeepTourBar.tsx
3833
+ import { useKeepNavigator } from "@keepkit/core/react";
3834
+
3835
+ // src/hooks/useKeepTourShortcuts.ts
3836
+ import { useEffect as useEffect10 } from "react";
3837
+ function useKeepTourShortcuts({
3838
+ onNext,
3839
+ onPrev,
3840
+ enabled = true,
3841
+ allowInEditable = false,
3842
+ preventDefault = true,
3843
+ nextKeys = ["j", "]"],
3844
+ prevKeys = ["k", "["],
3845
+ onError
3846
+ }) {
3847
+ useEffect10(() => {
3848
+ if (!enabled) return;
3849
+ const handleKeyDown = (event) => {
3850
+ if (!allowInEditable && isEditableTarget(event.target)) return;
3851
+ if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return;
3852
+ const key = event.key.toLocaleLowerCase();
3853
+ const action = nextKeys.some((candidate) => candidate.toLocaleLowerCase() === key) ? onNext : prevKeys.some((candidate) => candidate.toLocaleLowerCase() === key) ? onPrev : void 0;
3854
+ if (!action) return;
3855
+ if (preventDefault) event.preventDefault();
3856
+ void Promise.resolve(action()).catch((error) => onError?.(error));
3857
+ };
3858
+ window.addEventListener("keydown", handleKeyDown);
3859
+ return () => window.removeEventListener("keydown", handleKeyDown);
3860
+ }, [allowInEditable, enabled, nextKeys, onError, onNext, onPrev, preventDefault, prevKeys]);
3861
+ }
3862
+ function isEditableTarget(target) {
3863
+ if (!(target instanceof HTMLElement)) return false;
3864
+ return target.isContentEditable || target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT";
3865
+ }
3866
+
3867
+ // src/KeepTourBar.tsx
3868
+ import { jsx as jsx20, jsxs as jsxs14 } from "react/jsx-runtime";
3869
+ function KeepTourBar({
3870
+ navigation: providedNavigation,
3871
+ currentId,
3872
+ initialIndex,
3873
+ showProgress = true,
3874
+ prevHref,
3875
+ nextHref,
3876
+ backHref,
3877
+ onPrev,
3878
+ onNext,
3879
+ onBack,
3880
+ prevLabel,
3881
+ nextLabel,
3882
+ backLabel,
3883
+ keyboardShortcuts = false,
3884
+ shortcutOptions,
3885
+ progress,
3886
+ ...props
3887
+ }) {
3888
+ const ownNavigation = useKeepNavigator({ currentId, initialIndex });
3889
+ const navigation = providedNavigation ?? ownNavigation;
3890
+ const previousLabel = useUiLabel("previousPage", prevLabel);
3891
+ const nextItemLabel = useUiLabel("nextPage", nextLabel);
3892
+ const listLabel = useUiLabel("allItems", backLabel);
3893
+ const { labels } = useKeepUiLabels();
3894
+ const resolvedPrev = onPrev ?? (() => navigateTo(navigation.goToPrev(), prevHref));
3895
+ const resolvedNext = onNext ?? (() => navigateTo(navigation.goToNext(), nextHref));
3896
+ useKeepTourShortcuts({
3897
+ ...shortcutOptions,
3898
+ enabled: keyboardShortcuts && (shortcutOptions?.enabled ?? true),
3899
+ onNext: () => {
3900
+ return resolvedNext();
3901
+ },
3902
+ onPrev: () => {
3903
+ return resolvedPrev();
3904
+ }
3905
+ });
3906
+ return /* @__PURE__ */ jsxs14("nav", { ...props, "data-keepkit": "tour-bar", "aria-label": props["aria-label"] ?? labels.pagination, children: [
3907
+ showProgress ? /* @__PURE__ */ jsx20("span", { "data-keepkit": "tour-progress", "aria-live": "polite", children: progress ?? `${navigation.currentPosition ?? 0} / ${navigation.items.length}` }) : null,
3908
+ /* @__PURE__ */ jsx20(
3909
+ TourAction,
3910
+ {
3911
+ href: prevHref,
3912
+ disabled: !navigation.hasPrev,
3913
+ onClick: prevHref ? onPrev : resolvedPrev,
3914
+ "data-keep-action": "tour-prev",
3915
+ children: previousLabel
3916
+ }
3917
+ ),
3918
+ /* @__PURE__ */ jsx20(
3919
+ TourAction,
3920
+ {
3921
+ href: nextHref,
3922
+ disabled: !navigation.hasNext,
3923
+ onClick: nextHref ? onNext : resolvedNext,
3924
+ "data-keep-action": "tour-next",
3925
+ children: nextItemLabel
3926
+ }
3927
+ ),
3928
+ backHref || onBack ? /* @__PURE__ */ jsx20(TourAction, { href: backHref, onClick: onBack, "data-keep-action": "tour-back", children: listLabel }) : null
3929
+ ] });
3930
+ }
3931
+ function TourAction({ href, disabled = false, onClick, children, ...props }) {
3932
+ if (href && !disabled) {
3933
+ return /* @__PURE__ */ jsx20("a", { ...props, href, onClick: () => void onClick?.(), children });
3934
+ }
3935
+ return /* @__PURE__ */ jsx20("button", { ...props, type: "button", disabled, onClick: () => void onClick?.(), children });
3936
+ }
3937
+ var KeepNavigator = KeepTourBar;
3938
+ function navigateTo(item, href) {
3939
+ if (!item || !href || typeof window === "undefined") return;
3940
+ window.location.assign(href);
3941
+ }
3942
+
3268
3943
  // src/hooks/useKeepUndo.ts
3269
- import { useKeepContext as useKeepContext5 } from "@keepkit/core/react";
3944
+ import { useKeepContext as useKeepContext6 } from "@keepkit/core/react";
3270
3945
  function useKeepUndo() {
3271
- const context = useKeepContext5();
3946
+ const context = useKeepContext6();
3947
+ const emitFeedback = useKeepUiFeedback();
3948
+ const restoredMessage = useUiLabel("restoredMessage");
3272
3949
  return {
3273
3950
  canUndo: context.undo.canUndo,
3274
- undo: () => context.undoLastRemoval(),
3951
+ undo: async () => {
3952
+ const items = context.lastChange?.items ?? (context.lastChange?.item ? [context.lastChange.item] : []);
3953
+ await context.undoLastRemoval();
3954
+ if (items.length > 0) {
3955
+ emitFeedback({ type: "item-restored", item: items[0], items, message: restoredMessage });
3956
+ }
3957
+ },
3275
3958
  message: useUiLabel("undoAvailable"),
3276
3959
  label: useUiLabel("undo")
3277
3960
  };
3278
3961
  }
3279
3962
 
3280
3963
  // src/KeepUndo.tsx
3281
- import { jsx as jsx19, jsxs as jsxs13 } from "react/jsx-runtime";
3964
+ import { jsx as jsx21, jsxs as jsxs15 } from "react/jsx-runtime";
3282
3965
  function KeepUndo({ children, label, ...props }) {
3283
3966
  const view = useKeepUndo();
3284
3967
  if (!view.canUndo) return null;
3285
- return /* @__PURE__ */ jsxs13("div", { ...props, role: "status", "aria-live": "polite", "data-keepkit": "undo", "data-state": "available", children: [
3968
+ return /* @__PURE__ */ jsxs15("div", { ...props, role: "status", "aria-live": "polite", "data-keepkit": "undo", "data-state": "available", children: [
3286
3969
  children ?? view.message,
3287
- /* @__PURE__ */ jsx19("button", { type: "button", "data-keep-action": "undo", onClick: () => void view.undo(), children: label ?? view.label })
3970
+ /* @__PURE__ */ jsx21("button", { type: "button", "data-keep-action": "undo", onClick: () => void view.undo(), children: label ?? view.label })
3288
3971
  ] });
3289
3972
  }
3290
3973
 
@@ -3292,13 +3975,13 @@ function KeepUndo({ children, label, ...props }) {
3292
3975
  import { isValidElement as isValidElement6 } from "react";
3293
3976
 
3294
3977
  // src/hooks/useStatusViews.ts
3295
- import { useKeepContext as useKeepContext6 } from "@keepkit/core/react";
3296
- import { useEffect as useEffect6, useRef as useRef4, useState as useState10 } from "react";
3978
+ import { useKeepContext as useKeepContext7 } from "@keepkit/core/react";
3979
+ import { useEffect as useEffect11, useRef as useRef7, useState as useState12 } from "react";
3297
3980
  function useKeepEmptyState() {
3298
3981
  return useUiLabel("noItems").replace(/\.$/, "");
3299
3982
  }
3300
3983
  function useKeepStatus(status) {
3301
- const context = useKeepContext6();
3984
+ const context = useKeepContext7();
3302
3985
  const resolvedStatus = status ?? getDerivedStatus(context);
3303
3986
  const state = {
3304
3987
  status: resolvedStatus,
@@ -3309,13 +3992,13 @@ function useKeepStatus(status) {
3309
3992
  return { state, defaultLabel: useUiLabel(getStatusLabelKey3(resolvedStatus)) };
3310
3993
  }
3311
3994
  function useKeepAnnouncements(messages) {
3312
- const context = useKeepContext6();
3995
+ const context = useKeepContext7();
3313
3996
  const savedMessage = useUiLabel("savedMessage", messages?.save);
3314
3997
  const removedMessage = useUiLabel("removedMessage", messages?.remove);
3315
3998
  const noteSavedMessage = useUiLabel("noteSavedMessage", messages?.note);
3316
- const [message, setMessage] = useState10("");
3317
- const lastChangeRef = useRef4(void 0);
3318
- useEffect6(() => {
3999
+ const [message, setMessage] = useState12("");
4000
+ const lastChangeRef = useRef7(void 0);
4001
+ useEffect11(() => {
3319
4002
  const change = context.lastChange;
3320
4003
  if (!change || change === lastChangeRef.current) return;
3321
4004
  lastChangeRef.current = change;
@@ -3343,7 +4026,7 @@ function getStatusLabelKey3(status) {
3343
4026
  }
3344
4027
 
3345
4028
  // src/status.tsx
3346
- import { Fragment as Fragment7, jsx as jsx20, jsxs as jsxs14 } from "react/jsx-runtime";
4029
+ import { Fragment as Fragment9, jsx as jsx22, jsxs as jsxs16 } from "react/jsx-runtime";
3347
4030
  function KeepEmptyState({
3348
4031
  title,
3349
4032
  description,
@@ -3355,9 +4038,9 @@ function KeepEmptyState({
3355
4038
  }) {
3356
4039
  const defaultTitle = useKeepEmptyState();
3357
4040
  const contentChildren = asChild && isValidElement6(children) ? void 0 : children;
3358
- const body = contentChildren ?? /* @__PURE__ */ jsxs14(Fragment7, { children: [
3359
- /* @__PURE__ */ jsx20("h2", { children: title ?? defaultTitle }),
3360
- description ? /* @__PURE__ */ jsx20("p", { children: description }) : null,
4041
+ const body = contentChildren ?? /* @__PURE__ */ jsxs16(Fragment9, { children: [
4042
+ /* @__PURE__ */ jsx22("h2", { children: title ?? defaultTitle }),
4043
+ description ? /* @__PURE__ */ jsx22("p", { children: description }) : null,
3361
4044
  action
3362
4045
  ] });
3363
4046
  return renderRoot(
@@ -3399,7 +4082,7 @@ function KeepStatus({
3399
4082
  }
3400
4083
  function KeepAnnouncements({ messages, ...props }) {
3401
4084
  const message = useKeepAnnouncements(messages);
3402
- return /* @__PURE__ */ jsx20(
4085
+ return /* @__PURE__ */ jsx22(
3403
4086
  "div",
3404
4087
  {
3405
4088
  ...props,
@@ -3416,7 +4099,7 @@ var KeepAnnouncer = KeepAnnouncements;
3416
4099
 
3417
4100
  // src/theme.tsx
3418
4101
  import { cloneElement as cloneElement2, isValidElement as isValidElement7 } from "react";
3419
- import { jsx as jsx21 } from "react/jsx-runtime";
4102
+ import { jsx as jsx23 } from "react/jsx-runtime";
3420
4103
  var keepThemeNames = [
3421
4104
  "default",
3422
4105
  "ocean",
@@ -3475,7 +4158,7 @@ function KeepThemeProvider({
3475
4158
  throw new Error("KeepThemeProvider with asChild requires a single React element child.");
3476
4159
  return cloneElement2(children, rootProps);
3477
4160
  }
3478
- return /* @__PURE__ */ jsx21("div", { ...rootProps, children });
4161
+ return /* @__PURE__ */ jsx23("div", { ...rootProps, children });
3479
4162
  }
3480
4163
 
3481
4164
  // src/index.tsx
@@ -3483,9 +4166,10 @@ import { createAuthenticatedSyncKit } from "@keepkit/core/core";
3483
4166
  import {
3484
4167
  KeepErrorBoundary as KeepErrorBoundary3,
3485
4168
  KeepProvider,
3486
- useKeepContext as useKeepContext7,
4169
+ useKeepContext as useKeepContext8,
3487
4170
  useKeepItem as useKeepItem5,
3488
4171
  useKeepList as useKeepList5,
4172
+ useKeepNavigator as useKeepNavigator2,
3489
4173
  useKeepShortcut
3490
4174
  } from "@keepkit/core/react";
3491
4175
  import {
@@ -3498,11 +4182,12 @@ import {
3498
4182
  LocalStorageSyncQueueAdapter,
3499
4183
  SyncStorageAdapter
3500
4184
  } from "@keepkit/core/storage";
3501
- import { jsx as jsx22, jsxs as jsxs15 } from "react/jsx-runtime";
4185
+ import { jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
3502
4186
  function KeepKitProvider({
3503
4187
  labels,
3504
4188
  locale,
3505
4189
  labelResolver,
4190
+ onFeedback,
3506
4191
  theme,
3507
4192
  mode,
3508
4193
  density,
@@ -3517,7 +4202,7 @@ function KeepKitProvider({
3517
4202
  children,
3518
4203
  ...providerProps
3519
4204
  }) {
3520
- return /* @__PURE__ */ jsx22(KeepUiProvider, { labels, locale, labelResolver, children: /* @__PURE__ */ jsx22(
4205
+ return /* @__PURE__ */ jsx24(KeepUiProvider, { labels, locale, labelResolver, onFeedback, children: /* @__PURE__ */ jsx24(
3521
4206
  KeepThemeProvider,
3522
4207
  {
3523
4208
  theme,
@@ -3531,9 +4216,10 @@ function KeepKitProvider({
3531
4216
  className: themeClassName,
3532
4217
  style: themeStyle,
3533
4218
  asChild: themeAsChild,
3534
- children: /* @__PURE__ */ jsxs15(CoreKeepProvider, { ...providerProps, children: [
4219
+ children: /* @__PURE__ */ jsxs17(CoreKeepProvider, { ...providerProps, children: [
4220
+ /* @__PURE__ */ jsx24(KeepSyncFeedbackObserver, {}),
3535
4221
  children,
3536
- /* @__PURE__ */ jsx22(KeepAnnouncements, {})
4222
+ /* @__PURE__ */ jsx24(KeepAnnouncements, {})
3537
4223
  ] })
3538
4224
  }
3539
4225
  ) });
@@ -3543,6 +4229,7 @@ function createKeepKit(options = {}) {
3543
4229
  labels,
3544
4230
  locale,
3545
4231
  labelResolver,
4232
+ onFeedback,
3546
4233
  theme,
3547
4234
  mode,
3548
4235
  density,
@@ -3559,13 +4246,14 @@ function createKeepKit(options = {}) {
3559
4246
  } = options;
3560
4247
  const coreKit = createCoreKeepKit(coreOptions);
3561
4248
  return {
3562
- Provider: (props) => /* @__PURE__ */ jsx22(
4249
+ Provider: (props) => /* @__PURE__ */ jsx24(
3563
4250
  KeepKitProvider,
3564
4251
  {
3565
4252
  ...coreOptions,
3566
4253
  labels,
3567
4254
  locale,
3568
4255
  labelResolver,
4256
+ onFeedback,
3569
4257
  theme,
3570
4258
  mode,
3571
4259
  density,
@@ -3579,9 +4267,9 @@ function createKeepKit(options = {}) {
3579
4267
  ...props
3580
4268
  }
3581
4269
  ),
3582
- Button: (props) => /* @__PURE__ */ jsx22(KeepButton, { ...props }),
3583
- Backup: (props) => /* @__PURE__ */ jsx22(KeepBackup, { ...props }),
3584
- Collection: (props) => /* @__PURE__ */ jsx22(
4270
+ Button: (props) => /* @__PURE__ */ jsx24(KeepButton, { ...props }),
4271
+ Backup: (props) => /* @__PURE__ */ jsx24(KeepBackup, { ...props }),
4272
+ Collection: (props) => /* @__PURE__ */ jsx24(
3585
4273
  KeepCollection,
3586
4274
  {
3587
4275
  ...props,
@@ -3595,6 +4283,7 @@ function createKeepKit(options = {}) {
3595
4283
  useContext: () => coreKit.useContext(),
3596
4284
  useItem: (item) => coreKit.useItem(item),
3597
4285
  useList: (query) => coreKit.useList(query),
4286
+ useNavigator: (navigatorOptions) => coreKit.useNavigator(navigatorOptions),
3598
4287
  useShortcut: (shortcutOptions) => coreKit.useShortcut(shortcutOptions)
3599
4288
  };
3600
4289
  }
@@ -3610,16 +4299,20 @@ export {
3610
4299
  KeepCollection,
3611
4300
  KeepEmptyState,
3612
4301
  KeepErrorBoundary3 as KeepErrorBoundary,
4302
+ KeepHighlight,
3613
4303
  KeepItemCard,
4304
+ KeepItemCardSkeleton,
3614
4305
  KeepItemCheckbox,
3615
4306
  KeepItemStatusBadge,
3616
4307
  KeepKitProvider,
3617
4308
  KeepLayout,
3618
4309
  KeepList,
4310
+ KeepNavigator,
3619
4311
  KeepNoteEditor,
3620
4312
  KeepPagination,
3621
4313
  KeepProvider,
3622
4314
  KeepPruneStaleButton,
4315
+ KeepReorderableList,
3623
4316
  KeepSearchInput,
3624
4317
  KeepSortSelect,
3625
4318
  KeepStaleNotice,
@@ -3629,6 +4322,7 @@ export {
3629
4322
  KeepTagEditor,
3630
4323
  KeepTagFilter,
3631
4324
  KeepThemeProvider,
4325
+ KeepTourBar,
3632
4326
  KeepUiProvider,
3633
4327
  KeepUndo,
3634
4328
  LocalStorageAdapter,
@@ -3640,13 +4334,17 @@ export {
3640
4334
  createNextPagesRouterAdapter,
3641
4335
  createStorageAdapter,
3642
4336
  getKeepLocaleLabels,
4337
+ highlightText,
3643
4338
  isAllSelected,
3644
4339
  keepThemeNames,
3645
4340
  toggleSelectAll,
3646
- useKeepContext7 as useKeepContext,
4341
+ useKeepContext8 as useKeepContext,
3647
4342
  useKeepItem5 as useKeepItem,
3648
4343
  useKeepList5 as useKeepList,
4344
+ useKeepNavigator2 as useKeepNavigator,
3649
4345
  useKeepShortcut,
4346
+ useKeepToastFeedback,
4347
+ useKeepTourShortcuts,
3650
4348
  useKeepUiLabels,
3651
4349
  useKeepUrlSync
3652
4350
  };