@keepkit/ui 0.17.0 → 0.19.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,32 +6,94 @@ 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
- });
9
+ // src/adapters/url-sync.tsx
10
+ import {
11
+ DEFAULT_KEEP_URL_PARAMS,
12
+ decodeKeepListQuery,
13
+ encodeKeepListQuery
14
+ } from "@keepkit/core/core";
15
+ import { useEffect, useRef } from "react";
16
+ function createNextPagesRouterAdapter(router) {
17
+ const getUrl = () => router.asPath ?? (typeof window === "undefined" ? "/" : window.location.href);
18
+ return {
19
+ getUrl,
20
+ subscribe: router.events ? (listener) => {
21
+ router.events?.on("routeChangeComplete", listener);
22
+ return () => router.events?.off("routeChangeComplete", listener);
23
+ } : void 0,
24
+ navigate: (url, mode) => {
25
+ void router[mode](url, void 0, { shallow: true });
26
+ }
27
+ };
28
+ }
29
+ function useKeepUrlSync({
30
+ enabled = true,
31
+ query,
32
+ onQueryChange,
33
+ options = {},
34
+ adapter: providedAdapter
35
+ }) {
36
+ const browserAdapterRef = useRef(getBrowserAdapter());
37
+ const adapter = providedAdapter ?? browserAdapterRef.current;
38
+ const onQueryChangeRef = useRef(onQueryChange);
39
+ onQueryChangeRef.current = onQueryChange;
40
+ const skipWriteRef = useRef(true);
41
+ const params = options.params;
42
+ useEffect(() => {
43
+ if (!enabled) return;
44
+ const read = () => {
45
+ const url = adapter.getUrl();
46
+ const decoded = decodeKeepListQuery(url, { params });
47
+ skipWriteRef.current = true;
48
+ onQueryChangeRef.current((previousQuery) => ({
49
+ ...previousQuery,
50
+ ...decoded.search ? { search: decoded.search } : { search: void 0 },
51
+ ...decoded.tags ? { tags: decoded.tags } : { tags: void 0 },
52
+ ...decoded.sort ? { sort: decoded.sort } : {},
53
+ ...decoded.pagination ? { pagination: { ...previousQuery.pagination, ...decoded.pagination } } : {}
54
+ }));
55
+ };
56
+ read();
57
+ return adapter.subscribe?.(read);
58
+ }, [adapter, enabled, params]);
59
+ useEffect(() => {
60
+ if (!enabled) return;
61
+ if (skipWriteRef.current) {
62
+ skipWriteRef.current = false;
63
+ return;
64
+ }
65
+ const currentUrl = new URL(adapter.getUrl(), "http://keepkit.invalid");
66
+ const urlParams = { ...DEFAULT_KEEP_URL_PARAMS, ...params };
67
+ for (const key of Object.values(urlParams)) currentUrl.searchParams.delete(key);
68
+ const nextParams = encodeKeepListQuery(query, { params });
69
+ nextParams.forEach((value, key) => {
70
+ currentUrl.searchParams.append(key, value);
71
+ });
72
+ const nextUrl = `${currentUrl.pathname}${currentUrl.search}${currentUrl.hash}`;
73
+ adapter.navigate(nextUrl, options.history ?? "push");
74
+ }, [adapter, enabled, options.history, params, query]);
75
+ }
76
+ function getBrowserAdapter() {
77
+ return {
78
+ getUrl: () => typeof window === "undefined" ? "/" : window.location.href,
79
+ subscribe: (listener) => {
80
+ if (typeof window === "undefined") return () => void 0;
81
+ window.addEventListener("popstate", listener);
82
+ return () => window.removeEventListener("popstate", listener);
24
83
  },
25
- [showToast]
26
- );
84
+ navigate: (url, mode) => {
85
+ if (typeof window === "undefined") return;
86
+ window.history[mode === "push" ? "pushState" : "replaceState"]({}, "", url);
87
+ }
88
+ };
27
89
  }
28
90
 
29
- // src/hooks/useKeepBackup.ts
91
+ // src/features/actions/hooks/useKeepBackup.ts
30
92
  import { useKeepContext } from "@keepkit/core/react";
31
- import { useRef, useState } from "react";
93
+ import { useRef as useRef2, useState } from "react";
32
94
 
33
- // src/ui-context.tsx
34
- import { createContext, useCallback as useCallback2, useContext, useMemo } from "react";
95
+ // src/foundation/ui-context.tsx
96
+ import { createContext, useCallback, useContext, useMemo } from "react";
35
97
 
36
98
  // src/locales/de.ts
37
99
  var DE_LABELS = {
@@ -45,6 +107,13 @@ var DE_LABELS = {
45
107
  allTags: "Alle",
46
108
  tags: "Tags",
47
109
  filterTags: "Gespeicherte Elemente nach Tag filtern",
110
+ activeFilters: "Aktive Filter",
111
+ clearFilters: "Filter l\xF6schen",
112
+ clearAllFilters: "Alle Filter l\xF6schen",
113
+ removeFilter: "entfernen",
114
+ noFilteredItems: "Keine passenden Elemente gefunden.",
115
+ noFilteredItemsDescription: "Passe die Suche an oder entferne einen Tag-Filter.",
116
+ emptyStorageDescription: "Speichere ein Element, damit es hier angezeigt wird.",
48
117
  note: "Notiz",
49
118
  saveNote: "Notiz speichern",
50
119
  noItems: "Keine gespeicherten Elemente.",
@@ -124,6 +193,13 @@ var EN_LABELS = {
124
193
  allTags: "All",
125
194
  tags: "Tags",
126
195
  filterTags: "Filter saved items by tag",
196
+ activeFilters: "Active filters",
197
+ clearFilters: "Clear filters",
198
+ clearAllFilters: "Clear all filters",
199
+ removeFilter: "Remove",
200
+ noFilteredItems: "No matching items found.",
201
+ noFilteredItemsDescription: "Try adjusting your search or removing a tag filter.",
202
+ emptyStorageDescription: "Save an item to see it here.",
127
203
  note: "Note",
128
204
  saveNote: "Save note",
129
205
  noItems: "No saved items.",
@@ -203,6 +279,13 @@ var ES_LABELS = {
203
279
  allTags: "Todos",
204
280
  tags: "Etiquetas",
205
281
  filterTags: "Filtrar elementos guardados por etiqueta",
282
+ activeFilters: "Filtros activos",
283
+ clearFilters: "Borrar filtros",
284
+ clearAllFilters: "Borrar todos los filtros",
285
+ removeFilter: "quitar",
286
+ noFilteredItems: "No se encontraron elementos coincidentes.",
287
+ noFilteredItemsDescription: "Ajusta la b\xFAsqueda o quita un filtro de etiqueta.",
288
+ emptyStorageDescription: "Guarda un elemento para verlo aqu\xED.",
206
289
  note: "Nota",
207
290
  saveNote: "Guardar nota",
208
291
  noItems: "No hay elementos guardados.",
@@ -282,6 +365,13 @@ var FIL_LABELS = {
282
365
  allTags: "Lahat",
283
366
  tags: "Mga tag",
284
367
  filterTags: "I-filter ang mga naka-save na item ayon sa tag",
368
+ activeFilters: "Mga aktibong filter",
369
+ clearFilters: "I-clear ang mga filter",
370
+ clearAllFilters: "I-clear ang lahat ng filter",
371
+ removeFilter: "alisin",
372
+ noFilteredItems: "Walang nakitang tumutugmang item.",
373
+ noFilteredItemsDescription: "Baguhin ang paghahanap o alisin ang filter ng tag.",
374
+ emptyStorageDescription: "Mag-save ng item para makita ito rito.",
285
375
  note: "Tala",
286
376
  saveNote: "I-save ang tala",
287
377
  noItems: "Walang naka-save na item.",
@@ -361,6 +451,13 @@ var FR_LABELS = {
361
451
  allTags: "Tous",
362
452
  tags: "\xC9tiquettes",
363
453
  filterTags: "Filtrer les \xE9l\xE9ments enregistr\xE9s par \xE9tiquette",
454
+ activeFilters: "Filtres actifs",
455
+ clearFilters: "Effacer les filtres",
456
+ clearAllFilters: "Effacer tous les filtres",
457
+ removeFilter: "supprimer",
458
+ noFilteredItems: "Aucun \xE9l\xE9ment correspondant trouv\xE9.",
459
+ noFilteredItemsDescription: "Modifiez votre recherche ou supprimez un filtre de tag.",
460
+ emptyStorageDescription: "Enregistrez un \xE9l\xE9ment pour le voir ici.",
364
461
  note: "Note",
365
462
  saveNote: "Enregistrer la note",
366
463
  noItems: "Aucun \xE9l\xE9ment enregistr\xE9.",
@@ -440,6 +537,13 @@ var ID_LABELS = {
440
537
  allTags: "Semua",
441
538
  tags: "Tag",
442
539
  filterTags: "Filter item tersimpan berdasarkan tag",
540
+ activeFilters: "Filter aktif",
541
+ clearFilters: "Hapus filter",
542
+ clearAllFilters: "Hapus semua filter",
543
+ removeFilter: "hapus",
544
+ noFilteredItems: "Tidak ada item yang cocok.",
545
+ noFilteredItemsDescription: "Ubah pencarian atau hapus filter tag.",
546
+ emptyStorageDescription: "Simpan item untuk melihatnya di sini.",
443
547
  note: "Catatan",
444
548
  saveNote: "Simpan catatan",
445
549
  noItems: "Belum ada item tersimpan.",
@@ -519,6 +623,13 @@ var IT_LABELS = {
519
623
  allTags: "Tutti",
520
624
  tags: "Tag",
521
625
  filterTags: "Filtra gli elementi salvati per tag",
626
+ activeFilters: "Filtri attivi",
627
+ clearFilters: "Cancella filtri",
628
+ clearAllFilters: "Cancella tutti i filtri",
629
+ removeFilter: "rimuovi",
630
+ noFilteredItems: "Nessun elemento corrispondente trovato.",
631
+ noFilteredItemsDescription: "Modifica la ricerca o rimuovi un filtro tag.",
632
+ emptyStorageDescription: "Salva un elemento per visualizzarlo qui.",
522
633
  note: "Nota",
523
634
  saveNote: "Salva nota",
524
635
  noItems: "Nessun elemento salvato.",
@@ -598,6 +709,13 @@ var JA_LABELS = {
598
709
  allTags: "\u3059\u3079\u3066",
599
710
  tags: "\u30BF\u30B0",
600
711
  filterTags: "\u4FDD\u5B58\u30A2\u30A4\u30C6\u30E0\u3092\u30BF\u30B0\u3067\u7D5E\u308A\u8FBC\u3080",
712
+ activeFilters: "\u9069\u7528\u4E2D\u306E\u6761\u4EF6",
713
+ clearFilters: "\u30D5\u30A3\u30EB\u30BF\u30FC\u3092\u30AF\u30EA\u30A2",
714
+ clearAllFilters: "\u3059\u3079\u3066\u306E\u6761\u4EF6\u3092\u30AF\u30EA\u30A2",
715
+ removeFilter: "\u3092\u89E3\u9664",
716
+ noFilteredItems: "\u4E00\u81F4\u3059\u308B\u30A2\u30A4\u30C6\u30E0\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3067\u3057\u305F",
717
+ noFilteredItemsDescription: "\u691C\u7D22\u8A9E\u3092\u5909\u66F4\u3059\u308B\u304B\u3001\u30BF\u30B0\u306E\u7D5E\u308A\u8FBC\u307F\u3092\u89E3\u9664\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
718
+ emptyStorageDescription: "\u30A2\u30A4\u30C6\u30E0\u3092\u4FDD\u5B58\u3059\u308B\u3068\u3001\u3053\u3053\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002",
601
719
  note: "\u30E1\u30E2",
602
720
  saveNote: "\u30E1\u30E2\u3092\u4FDD\u5B58",
603
721
  noItems: "\u4FDD\u5B58\u3055\u308C\u305F\u30A2\u30A4\u30C6\u30E0\u306F\u3042\u308A\u307E\u305B\u3093\u3002",
@@ -677,6 +795,13 @@ var KO_LABELS = {
677
795
  allTags: "\uBAA8\uB450",
678
796
  tags: "\uD0DC\uADF8",
679
797
  filterTags: "\uD0DC\uADF8\uB85C \uC800\uC7A5 \uD56D\uBAA9 \uD544\uD130\uB9C1",
798
+ activeFilters: "\uD65C\uC131 \uD544\uD130",
799
+ clearFilters: "\uD544\uD130 \uC9C0\uC6B0\uAE30",
800
+ clearAllFilters: "\uBAA8\uB4E0 \uD544\uD130 \uC9C0\uC6B0\uAE30",
801
+ removeFilter: "\uC81C\uAC70",
802
+ noFilteredItems: "\uC77C\uCE58\uD558\uB294 \uD56D\uBAA9\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
803
+ noFilteredItemsDescription: "\uAC80\uC0C9\uC5B4\uB97C \uBC14\uAFB8\uAC70\uB098 \uD0DC\uADF8 \uD544\uD130\uB97C \uC81C\uAC70\uD574 \uBCF4\uC138\uC694.",
804
+ emptyStorageDescription: "\uD56D\uBAA9\uC744 \uC800\uC7A5\uD558\uBA74 \uC5EC\uAE30\uC5D0 \uD45C\uC2DC\uB429\uB2C8\uB2E4.",
680
805
  note: "\uBA54\uBAA8",
681
806
  saveNote: "\uBA54\uBAA8 \uC800\uC7A5",
682
807
  noItems: "\uC800\uC7A5\uB41C \uD56D\uBAA9\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
@@ -756,6 +881,13 @@ var MS_LABELS = {
756
881
  allTags: "Semua",
757
882
  tags: "Tag",
758
883
  filterTags: "Tapis item yang disimpan mengikut tag",
884
+ activeFilters: "Penapis aktif",
885
+ clearFilters: "Kosongkan penapis",
886
+ clearAllFilters: "Kosongkan semua penapis",
887
+ removeFilter: "alih keluar",
888
+ noFilteredItems: "Tiada item yang sepadan ditemui.",
889
+ noFilteredItemsDescription: "Laraskan carian atau alih keluar penapis tag.",
890
+ emptyStorageDescription: "Simpan item untuk melihatnya di sini.",
759
891
  note: "Nota",
760
892
  saveNote: "Simpan nota",
761
893
  noItems: "Tiada item disimpan.",
@@ -835,6 +967,13 @@ var PT_BR_LABELS = {
835
967
  allTags: "Todos",
836
968
  tags: "Tags",
837
969
  filterTags: "Filtrar itens salvos por tag",
970
+ activeFilters: "Filtros ativos",
971
+ clearFilters: "Limpar filtros",
972
+ clearAllFilters: "Limpar todos os filtros",
973
+ removeFilter: "remover",
974
+ noFilteredItems: "Nenhum item correspondente encontrado.",
975
+ noFilteredItemsDescription: "Ajuste sua busca ou remova um filtro de tag.",
976
+ emptyStorageDescription: "Salve um item para v\xEA-lo aqui.",
838
977
  note: "Nota",
839
978
  saveNote: "Salvar nota",
840
979
  noItems: "Nenhum item salvo.",
@@ -914,6 +1053,13 @@ var RU_LABELS = {
914
1053
  allTags: "\u0412\u0441\u0435",
915
1054
  tags: "\u0422\u0435\u0433\u0438",
916
1055
  filterTags: "\u0424\u0438\u043B\u044C\u0442\u0440\u043E\u0432\u0430\u0442\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D\u043D\u044B\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B \u043F\u043E \u0442\u0435\u0433\u0443",
1056
+ activeFilters: "\u0410\u043A\u0442\u0438\u0432\u043D\u044B\u0435 \u0444\u0438\u043B\u044C\u0442\u0440\u044B",
1057
+ clearFilters: "\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u0444\u0438\u043B\u044C\u0442\u0440\u044B",
1058
+ clearAllFilters: "\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u0432\u0441\u0435 \u0444\u0438\u043B\u044C\u0442\u0440\u044B",
1059
+ removeFilter: "\u0443\u0434\u0430\u043B\u0438\u0442\u044C",
1060
+ noFilteredItems: "\u041F\u043E\u0434\u0445\u043E\u0434\u044F\u0449\u0438\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u044B.",
1061
+ noFilteredItemsDescription: "\u0418\u0437\u043C\u0435\u043D\u0438\u0442\u0435 \u043F\u043E\u0438\u0441\u043A \u0438\u043B\u0438 \u0443\u0434\u0430\u043B\u0438\u0442\u0435 \u0444\u0438\u043B\u044C\u0442\u0440 \u0442\u0435\u0433\u0430.",
1062
+ emptyStorageDescription: "\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442, \u0447\u0442\u043E\u0431\u044B \u0443\u0432\u0438\u0434\u0435\u0442\u044C \u0435\u0433\u043E \u0437\u0434\u0435\u0441\u044C.",
917
1063
  note: "\u0417\u0430\u043C\u0435\u0442\u043A\u0430",
918
1064
  saveNote: "\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u0437\u0430\u043C\u0435\u0442\u043A\u0443",
919
1065
  noItems: "\u041D\u0435\u0442 \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D\u043D\u044B\u0445 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432.",
@@ -993,6 +1139,13 @@ var TH_LABELS = {
993
1139
  allTags: "\u0E17\u0E31\u0E49\u0E07\u0E2B\u0E21\u0E14",
994
1140
  tags: "\u0E41\u0E17\u0E47\u0E01",
995
1141
  filterTags: "\u0E01\u0E23\u0E2D\u0E07\u0E23\u0E32\u0E22\u0E01\u0E32\u0E23\u0E17\u0E35\u0E48\u0E1A\u0E31\u0E19\u0E17\u0E36\u0E01\u0E14\u0E49\u0E27\u0E22\u0E41\u0E17\u0E47\u0E01",
1142
+ activeFilters: "\u0E15\u0E31\u0E27\u0E01\u0E23\u0E2D\u0E07\u0E17\u0E35\u0E48\u0E43\u0E0A\u0E49\u0E07\u0E32\u0E19\u0E2D\u0E22\u0E39\u0E48",
1143
+ clearFilters: "\u0E25\u0E49\u0E32\u0E07\u0E15\u0E31\u0E27\u0E01\u0E23\u0E2D\u0E07",
1144
+ clearAllFilters: "\u0E25\u0E49\u0E32\u0E07\u0E15\u0E31\u0E27\u0E01\u0E23\u0E2D\u0E07\u0E17\u0E31\u0E49\u0E07\u0E2B\u0E21\u0E14",
1145
+ removeFilter: "\u0E19\u0E33\u0E2D\u0E2D\u0E01",
1146
+ noFilteredItems: "\u0E44\u0E21\u0E48\u0E1E\u0E1A\u0E23\u0E32\u0E22\u0E01\u0E32\u0E23\u0E17\u0E35\u0E48\u0E15\u0E23\u0E07\u0E01\u0E31\u0E19",
1147
+ noFilteredItemsDescription: "\u0E25\u0E2D\u0E07\u0E1B\u0E23\u0E31\u0E1A\u0E01\u0E32\u0E23\u0E04\u0E49\u0E19\u0E2B\u0E32\u0E2B\u0E23\u0E37\u0E2D\u0E19\u0E33\u0E15\u0E31\u0E27\u0E01\u0E23\u0E2D\u0E07\u0E41\u0E17\u0E47\u0E01\u0E2D\u0E2D\u0E01",
1148
+ emptyStorageDescription: "\u0E1A\u0E31\u0E19\u0E17\u0E36\u0E01\u0E23\u0E32\u0E22\u0E01\u0E32\u0E23\u0E40\u0E1E\u0E37\u0E48\u0E2D\u0E14\u0E39\u0E17\u0E35\u0E48\u0E19\u0E35\u0E48",
996
1149
  note: "\u0E1A\u0E31\u0E19\u0E17\u0E36\u0E01\u0E22\u0E48\u0E2D",
997
1150
  saveNote: "\u0E1A\u0E31\u0E19\u0E17\u0E36\u0E01\u0E1A\u0E31\u0E19\u0E17\u0E36\u0E01\u0E22\u0E48\u0E2D",
998
1151
  noItems: "\u0E44\u0E21\u0E48\u0E21\u0E35\u0E23\u0E32\u0E22\u0E01\u0E32\u0E23\u0E17\u0E35\u0E48\u0E1A\u0E31\u0E19\u0E17\u0E36\u0E01\u0E44\u0E27\u0E49",
@@ -1072,6 +1225,13 @@ var VI_LABELS = {
1072
1225
  allTags: "T\u1EA5t c\u1EA3",
1073
1226
  tags: "Th\u1EBB",
1074
1227
  filterTags: "L\u1ECDc m\u1EE5c \u0111\xE3 l\u01B0u theo th\u1EBB",
1228
+ activeFilters: "B\u1ED9 l\u1ECDc \u0111ang d\xF9ng",
1229
+ clearFilters: "X\xF3a b\u1ED9 l\u1ECDc",
1230
+ clearAllFilters: "X\xF3a t\u1EA5t c\u1EA3 b\u1ED9 l\u1ECDc",
1231
+ removeFilter: "x\xF3a",
1232
+ noFilteredItems: "Kh\xF4ng t\xECm th\u1EA5y m\u1EE5c ph\xF9 h\u1EE3p.",
1233
+ noFilteredItemsDescription: "H\xE3y \u0111i\u1EC1u ch\u1EC9nh t\xECm ki\u1EBFm ho\u1EB7c x\xF3a b\u1ED9 l\u1ECDc th\u1EBB.",
1234
+ emptyStorageDescription: "L\u01B0u m\u1ED9t m\u1EE5c \u0111\u1EC3 xem m\u1EE5c \u0111\xF3 t\u1EA1i \u0111\xE2y.",
1075
1235
  note: "Ghi ch\xFA",
1076
1236
  saveNote: "L\u01B0u ghi ch\xFA",
1077
1237
  noItems: "Ch\u01B0a c\xF3 m\u1EE5c n\xE0o \u0111\u01B0\u1EE3c l\u01B0u.",
@@ -1151,6 +1311,13 @@ var ZH_HANS_LABELS = {
1151
1311
  allTags: "\u5168\u90E8",
1152
1312
  tags: "\u6807\u7B7E",
1153
1313
  filterTags: "\u6309\u6807\u7B7E\u7B5B\u9009\u5DF2\u4FDD\u5B58\u9879\u76EE",
1314
+ activeFilters: "\u5F53\u524D\u7B5B\u9009\u6761\u4EF6",
1315
+ clearFilters: "\u6E05\u9664\u7B5B\u9009",
1316
+ clearAllFilters: "\u6E05\u9664\u6240\u6709\u7B5B\u9009\u6761\u4EF6",
1317
+ removeFilter: "\u79FB\u9664",
1318
+ noFilteredItems: "\u672A\u627E\u5230\u5339\u914D\u9879\u76EE\u3002",
1319
+ noFilteredItemsDescription: "\u8BF7\u8C03\u6574\u641C\u7D22\u6761\u4EF6\u6216\u79FB\u9664\u6807\u7B7E\u7B5B\u9009\u3002",
1320
+ emptyStorageDescription: "\u4FDD\u5B58\u9879\u76EE\u540E\u4F1A\u663E\u793A\u5728\u8FD9\u91CC\u3002",
1154
1321
  note: "\u5907\u6CE8",
1155
1322
  saveNote: "\u4FDD\u5B58\u5907\u6CE8",
1156
1323
  noItems: "\u6CA1\u6709\u5DF2\u4FDD\u5B58\u7684\u9879\u76EE\u3002",
@@ -1230,6 +1397,13 @@ var ZH_HANT_LABELS = {
1230
1397
  allTags: "\u5168\u90E8",
1231
1398
  tags: "\u6A19\u7C64",
1232
1399
  filterTags: "\u4F9D\u6A19\u7C64\u7BE9\u9078\u5DF2\u5132\u5B58\u9805\u76EE",
1400
+ activeFilters: "\u76EE\u524D\u7BE9\u9078\u689D\u4EF6",
1401
+ clearFilters: "\u6E05\u9664\u7BE9\u9078",
1402
+ clearAllFilters: "\u6E05\u9664\u6240\u6709\u7BE9\u9078\u689D\u4EF6",
1403
+ removeFilter: "\u79FB\u9664",
1404
+ noFilteredItems: "\u627E\u4E0D\u5230\u76F8\u7B26\u9805\u76EE\u3002",
1405
+ noFilteredItemsDescription: "\u8ACB\u8ABF\u6574\u641C\u5C0B\u689D\u4EF6\u6216\u79FB\u9664\u6A19\u7C64\u7BE9\u9078\u3002",
1406
+ emptyStorageDescription: "\u5132\u5B58\u9805\u76EE\u5F8C\u6703\u986F\u793A\u5728\u9019\u88E1\u3002",
1233
1407
  note: "\u5099\u8A3B",
1234
1408
  saveNote: "\u5132\u5B58\u5099\u8A3B",
1235
1409
  noItems: "\u6C92\u6709\u5DF2\u5132\u5B58\u7684\u9805\u76EE\u3002",
@@ -1356,7 +1530,7 @@ function getKeepLocaleLabels(locale) {
1356
1530
  return { ...KEEP_LOCALE_LABELS[normalizeKeepLocale(locale)] };
1357
1531
  }
1358
1532
 
1359
- // src/ui-context.tsx
1533
+ // src/foundation/ui-context.tsx
1360
1534
  import { jsx } from "react/jsx-runtime";
1361
1535
  var DEFAULT_LABELS = KEEP_LOCALE_LABELS.en;
1362
1536
  var KeepUiLabelsContext = createContext({
@@ -1370,7 +1544,7 @@ function KeepUiProvider({
1370
1544
  onFeedback,
1371
1545
  children
1372
1546
  }) {
1373
- const emitFeedback = useCallback2(
1547
+ const emitFeedback = useCallback(
1374
1548
  (event) => {
1375
1549
  onFeedback?.(event);
1376
1550
  },
@@ -1393,13 +1567,13 @@ function useUiLabel(key, override) {
1393
1567
  }
1394
1568
  function useKeepUiFeedback() {
1395
1569
  const { emitFeedback } = useKeepUiLabels();
1396
- return useCallback2((event) => emitFeedback(event), [emitFeedback]);
1570
+ return useCallback((event) => emitFeedback(event), [emitFeedback]);
1397
1571
  }
1398
1572
 
1399
- // src/hooks/useKeepBackup.ts
1573
+ // src/features/actions/hooks/useKeepBackup.ts
1400
1574
  function useKeepBackup({ filename, onExport, onImported }) {
1401
1575
  const context = useKeepContext();
1402
- const inputRef = useRef(null);
1576
+ const inputRef = useRef2(null);
1403
1577
  const [mode, setMode] = useState("merge");
1404
1578
  const [result, setResult] = useState();
1405
1579
  const [error, setError] = useState();
@@ -1456,7 +1630,7 @@ function useKeepBackup({ filename, onExport, onImported }) {
1456
1630
  };
1457
1631
  }
1458
1632
 
1459
- // src/KeepBackup.tsx
1633
+ // src/features/actions/KeepBackup.tsx
1460
1634
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
1461
1635
  function KeepBackup({
1462
1636
  filename = "keepkit-backup.json",
@@ -1533,11 +1707,7 @@ function getErrorMessage(error) {
1533
1707
  return error instanceof Error ? error.message : "Something went wrong.";
1534
1708
  }
1535
1709
 
1536
- // src/hooks/useKeepBulkActions.ts
1537
- import { useKeepList } from "@keepkit/core/react";
1538
- import { useState as useState2 } from "react";
1539
-
1540
- // src/shared.tsx
1710
+ // src/foundation/shared.tsx
1541
1711
  import {
1542
1712
  cloneElement,
1543
1713
  createContext as createContext2,
@@ -1606,15 +1776,69 @@ function sortToValue(sort) {
1606
1776
  function resolveContent(content, state) {
1607
1777
  return typeof content === "function" ? content(state) : content;
1608
1778
  }
1779
+ function chainedFunction(childHandler, parentHandler) {
1780
+ if (!childHandler) return parentHandler;
1781
+ if (!parentHandler) return childHandler;
1782
+ return ((...args) => {
1783
+ childHandler(...args);
1784
+ parentHandler(...args);
1785
+ });
1786
+ }
1787
+ function mergeProps(childProps, parentProps) {
1788
+ const merged = { ...parentProps, ...childProps };
1789
+ const className = mergeClassNames(childProps.className, parentProps.className);
1790
+ if (className) merged.className = className;
1791
+ const style = mergeStyles(childProps.style, parentProps.style);
1792
+ if (style) merged.style = style;
1793
+ for (const key of Object.keys(parentProps)) {
1794
+ if (!isEventProp(key)) continue;
1795
+ const chained = chainedFunction(getHandler(childProps[key]), getHandler(parentProps[key]));
1796
+ if (chained) merged[key] = chained;
1797
+ }
1798
+ for (const [key, value] of Object.entries(merged)) {
1799
+ if (value === void 0 && key.startsWith("aria-") && childProps[key] !== void 0) {
1800
+ merged[key] = childProps[key];
1801
+ }
1802
+ }
1803
+ return merged;
1804
+ }
1805
+ function getHandler(value) {
1806
+ return typeof value === "function" ? value : void 0;
1807
+ }
1808
+ function isEventProp(key) {
1809
+ return key.startsWith("on") && key.length > 2 && key[2] === key[2]?.toUpperCase();
1810
+ }
1811
+ function mergeClassNames(childClassName, parentClassName) {
1812
+ const values = [childClassName, parentClassName].filter(
1813
+ (value) => typeof value === "string" && Boolean(value)
1814
+ );
1815
+ return values.length > 0 ? values.join(" ") : void 0;
1816
+ }
1817
+ function mergeStyles(childStyle, parentStyle) {
1818
+ if (!childStyle && !parentStyle) return void 0;
1819
+ return {
1820
+ ...isObject(childStyle) ? childStyle : {},
1821
+ ...isObject(parentStyle) ? parentStyle : {}
1822
+ };
1823
+ }
1824
+ function isObject(value) {
1825
+ return typeof value === "object" && value !== null;
1826
+ }
1827
+ function createSlot(child, props, body) {
1828
+ const nextProps = body === void 0 ? props : { ...props, children: body };
1829
+ return cloneElement(child, mergeProps(child.props, nextProps));
1830
+ }
1609
1831
  function renderRoot(asChild, child, props, body, componentName) {
1610
1832
  if (asChild) {
1611
1833
  if (!isValidElement(child)) throw new Error(`${componentName} with asChild requires a single React element child.`);
1612
- return cloneElement(child, { ...props, children: body });
1834
+ return createSlot(child, props, body);
1613
1835
  }
1614
1836
  return /* @__PURE__ */ jsx3("div", { ...props, children: body });
1615
1837
  }
1616
1838
 
1617
- // src/hooks/useKeepBulkActions.ts
1839
+ // src/features/actions/hooks/useKeepBulkActions.ts
1840
+ import { useKeepList } from "@keepkit/core/react";
1841
+ import { useState as useState2 } from "react";
1618
1842
  function isAllSelected(items, selectedIds) {
1619
1843
  if (items.length === 0) return false;
1620
1844
  const selected = new Set(selectedIds);
@@ -1707,7 +1931,7 @@ function useKeepBulkActions(options) {
1707
1931
  };
1708
1932
  }
1709
1933
 
1710
- // src/KeepItemCheckbox.tsx
1934
+ // src/features/actions/KeepItemCheckbox.tsx
1711
1935
  import { jsx as jsx4 } from "react/jsx-runtime";
1712
1936
  function KeepItemCheckbox({
1713
1937
  item,
@@ -1740,7 +1964,7 @@ function getItemLabel(item) {
1740
1964
  return typeof title === "string" && title.trim() ? title.trim() : void 0;
1741
1965
  }
1742
1966
 
1743
- // src/KeepBulkActions.tsx
1967
+ // src/features/actions/KeepBulkActions.tsx
1744
1968
  import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
1745
1969
  function KeepBulkActions({
1746
1970
  query,
@@ -1855,13 +2079,13 @@ function KeepBulkActions({
1855
2079
  );
1856
2080
  }
1857
2081
 
1858
- // src/KeepButton.tsx
2082
+ // src/features/actions/KeepButton.tsx
1859
2083
  import {
1860
2084
  KeepButton as CoreKeepButton
1861
2085
  } from "@keepkit/core/react";
1862
- import { createElement, useEffect, useRef as useRef2 } from "react";
2086
+ import { createElement, useEffect as useEffect2, useRef as useRef3 } from "react";
1863
2087
 
1864
- // src/hooks/useKeepButton.ts
2088
+ // src/features/actions/hooks/useKeepButton.ts
1865
2089
  import { useKeepItem } from "@keepkit/core/react";
1866
2090
  function useKeepButton({ item, labels, icons, children }) {
1867
2091
  return {
@@ -1881,7 +2105,7 @@ function useKeepButton({ item, labels, icons, children }) {
1881
2105
  };
1882
2106
  }
1883
2107
 
1884
- // src/KeepButton.tsx
2108
+ // src/features/actions/KeepButton.tsx
1885
2109
  import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
1886
2110
  function KeepButton({
1887
2111
  labels,
@@ -1894,8 +2118,8 @@ function KeepButton({
1894
2118
  ...props
1895
2119
  }) {
1896
2120
  const view = useKeepButton({ item: props.item, labels, icons, children: props.children });
1897
- const pendingToggle = useRef2(null);
1898
- useEffect(() => {
2121
+ const pendingToggle = useRef3(null);
2122
+ useEffect2(() => {
1899
2123
  const pending = pendingToggle.current;
1900
2124
  if (!pending || view.buttonState.isMutating || pending.wasSaved === view.buttonState.isSaved) return;
1901
2125
  pendingToggle.current = null;
@@ -1962,148 +2186,495 @@ function renderIcon(icon, className) {
1962
2186
  return icon ?? null;
1963
2187
  }
1964
2188
 
1965
- // src/KeepCollection.tsx
1966
- import { KeepErrorBoundary as KeepErrorBoundary2 } from "@keepkit/core/react";
1967
-
1968
- // src/hooks/useKeepCollection.ts
1969
- import { useKeepList as useKeepList2 } from "@keepkit/core/react";
1970
- import { useMemo as useMemo2, useState as useState3 } from "react";
1971
-
1972
- // src/url-sync.tsx
1973
- import {
1974
- DEFAULT_KEEP_URL_PARAMS,
1975
- decodeKeepListQuery,
1976
- encodeKeepListQuery
1977
- } from "@keepkit/core/core";
1978
- import { useEffect as useEffect2, useRef as useRef3 } from "react";
1979
- function createNextPagesRouterAdapter(router) {
1980
- const getUrl = () => router.asPath ?? (typeof window === "undefined" ? "/" : window.location.href);
1981
- return {
1982
- getUrl,
1983
- subscribe: router.events ? (listener) => {
1984
- router.events?.on("routeChangeComplete", listener);
1985
- return () => router.events?.off("routeChangeComplete", listener);
1986
- } : void 0,
1987
- navigate: (url, mode) => {
1988
- void router[mode](url, void 0, { shallow: true });
1989
- }
1990
- };
1991
- }
1992
- function useKeepUrlSync({
1993
- enabled = true,
1994
- query,
1995
- onQueryChange,
1996
- options = {},
1997
- adapter: providedAdapter
1998
- }) {
1999
- const browserAdapterRef = useRef3(getBrowserAdapter());
2000
- const adapter = providedAdapter ?? browserAdapterRef.current;
2001
- const onQueryChangeRef = useRef3(onQueryChange);
2002
- onQueryChangeRef.current = onQueryChange;
2003
- const skipWriteRef = useRef3(true);
2004
- const params = options.params;
2005
- useEffect2(() => {
2006
- if (!enabled) return;
2007
- const read = () => {
2008
- const url = adapter.getUrl();
2009
- const decoded = decodeKeepListQuery(url, { params });
2010
- skipWriteRef.current = true;
2011
- onQueryChangeRef.current((previousQuery) => ({
2012
- ...previousQuery,
2013
- ...decoded.search ? { search: decoded.search } : { search: void 0 },
2014
- ...decoded.tags ? { tags: decoded.tags } : { tags: void 0 },
2015
- ...decoded.sort ? { sort: decoded.sort } : {},
2016
- ...decoded.pagination ? { pagination: { ...previousQuery.pagination, ...decoded.pagination } } : {}
2017
- }));
2018
- };
2019
- read();
2020
- return adapter.subscribe?.(read);
2021
- }, [adapter, enabled, params]);
2022
- useEffect2(() => {
2023
- if (!enabled) return;
2024
- if (skipWriteRef.current) {
2025
- skipWriteRef.current = false;
2026
- return;
2027
- }
2028
- const currentUrl = new URL(adapter.getUrl(), "http://keepkit.invalid");
2029
- const urlParams = { ...DEFAULT_KEEP_URL_PARAMS, ...params };
2030
- for (const key of Object.values(urlParams)) currentUrl.searchParams.delete(key);
2031
- const nextParams = encodeKeepListQuery(query, { params });
2032
- nextParams.forEach((value, key) => {
2033
- currentUrl.searchParams.append(key, value);
2034
- });
2035
- const nextUrl = `${currentUrl.pathname}${currentUrl.search}${currentUrl.hash}`;
2036
- adapter.navigate(nextUrl, options.history ?? "push");
2037
- }, [adapter, enabled, options.history, params, query]);
2038
- }
2039
- function getBrowserAdapter() {
2189
+ // src/features/actions/hooks/useKeepUndo.ts
2190
+ import { useKeepContext as useKeepContext2 } from "@keepkit/core/react";
2191
+ import { useEffect as useEffect3, useState as useState3 } from "react";
2192
+ function useKeepUndo() {
2193
+ const context = useKeepContext2();
2194
+ const emitFeedback = useKeepUiFeedback();
2195
+ const restoredMessage = useUiLabel("restoredMessage");
2196
+ const { canUndo, startedAt, expiresAt } = context.undo;
2197
+ const [now, setNow] = useState3(() => Date.now());
2198
+ useEffect3(() => {
2199
+ if (!canUndo || expiresAt === void 0) return;
2200
+ setNow(Date.now());
2201
+ const timer = setInterval(() => setNow(Date.now()), 250);
2202
+ return () => clearInterval(timer);
2203
+ }, [canUndo, expiresAt]);
2204
+ const duration = Math.max(1, (expiresAt ?? now) - (startedAt ?? now));
2205
+ const remainingMs = Math.max(0, (expiresAt ?? now) - now);
2206
+ const remainingSeconds = Math.max(0, Math.ceil(remainingMs / 1e3));
2207
+ const progress = Math.min(1, Math.max(0, remainingMs / duration));
2040
2208
  return {
2041
- getUrl: () => typeof window === "undefined" ? "/" : window.location.href,
2042
- subscribe: (listener) => {
2043
- if (typeof window === "undefined") return () => void 0;
2044
- window.addEventListener("popstate", listener);
2045
- return () => window.removeEventListener("popstate", listener);
2209
+ canUndo,
2210
+ expiresAt,
2211
+ remainingMs,
2212
+ remainingSeconds,
2213
+ progress,
2214
+ undo: async () => {
2215
+ const items = context.lastChange?.items ?? (context.lastChange?.item ? [context.lastChange.item] : []);
2216
+ await context.undoLastRemoval();
2217
+ if (items.length > 0) {
2218
+ emitFeedback({ type: "item-restored", item: items[0], items, message: restoredMessage });
2219
+ }
2046
2220
  },
2047
- navigate: (url, mode) => {
2048
- if (typeof window === "undefined") return;
2049
- window.history[mode === "push" ? "pushState" : "replaceState"]({}, "", url);
2050
- }
2221
+ message: useUiLabel("undoAvailable"),
2222
+ label: useUiLabel("undo")
2051
2223
  };
2052
2224
  }
2053
2225
 
2054
- // src/hooks/useKeepCollection.ts
2055
- function useKeepCollection({
2226
+ // src/features/actions/KeepUndo.tsx
2227
+ import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
2228
+ function KeepUndo({ children, label, ...props }) {
2229
+ const view = useKeepUndo();
2230
+ if (!view.canUndo) return null;
2231
+ return /* @__PURE__ */ jsxs4("div", { ...props, role: "status", "aria-live": "polite", "data-keepkit": "undo", "data-state": "available", children: [
2232
+ /* @__PURE__ */ jsx7("span", { "data-keepkit": "undo-message", children: children ?? view.message }),
2233
+ /* @__PURE__ */ jsxs4("span", { "data-keepkit": "undo-countdown", "aria-hidden": "true", children: [
2234
+ view.remainingSeconds,
2235
+ "s"
2236
+ ] }),
2237
+ /* @__PURE__ */ jsx7(
2238
+ "progress",
2239
+ {
2240
+ "data-keepkit": "undo-progress",
2241
+ max: 1,
2242
+ value: view.progress,
2243
+ "aria-label": String(view.label),
2244
+ "aria-valuetext": `${view.remainingSeconds}s`
2245
+ }
2246
+ ),
2247
+ /* @__PURE__ */ jsx7("button", { type: "button", "data-keep-action": "undo", onClick: () => void view.undo(), children: label ?? view.label })
2248
+ ] });
2249
+ }
2250
+
2251
+ // src/features/collection/KeepCollection.tsx
2252
+ import { KeepErrorBoundary as KeepErrorBoundary2 } from "@keepkit/core/react";
2253
+
2254
+ // src/features/query/KeepActiveFiltersSummary.tsx
2255
+ import { isValidElement as isValidElement2 } from "react";
2256
+ import { Fragment as Fragment4, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
2257
+ function KeepActiveFiltersSummary({
2056
2258
  query,
2057
- pageSize,
2058
- urlSync,
2059
- urlAdapter,
2060
- features
2259
+ search: providedSearch,
2260
+ tags: providedTags,
2261
+ onSearchChange,
2262
+ onTagChange,
2263
+ onClear,
2264
+ children,
2265
+ asChild = false,
2266
+ className,
2267
+ ...rootProps
2061
2268
  }) {
2062
- const enabled = {
2063
- search: true,
2064
- sort: true,
2065
- pagination: true,
2066
- tagFilter: false,
2067
- bulkActions: false,
2068
- ...features
2269
+ const search = (providedSearch ?? query?.search?.query ?? "").trim();
2270
+ const tags = normalizeTags(providedTags ?? query?.tags ?? []);
2271
+ const hasFilters = Boolean(search) || tags.length > 0;
2272
+ const activeFiltersLabel = useUiLabel("activeFilters");
2273
+ const clearAllLabel = useUiLabel("clearAllFilters");
2274
+ const clearLabel = useUiLabel("clearFilters");
2275
+ const removeLabel = useUiLabel("removeFilter");
2276
+ const state = {
2277
+ search,
2278
+ tags,
2279
+ hasFilters,
2280
+ clear: () => onClear?.(),
2281
+ removeSearch: () => onSearchChange?.(""),
2282
+ removeTag: (tag) => onTagChange?.(tag)
2069
2283
  };
2070
- const [searchValue, setSearchValue] = useState3(query.search?.query ?? "");
2071
- const [sort, setSort] = useState3(query.sort ?? { by: "updatedAt", direction: "desc" });
2072
- const [tag, setTag] = useState3(query.tags?.[0]);
2073
- const [page, setPage] = useState3(query.pagination?.page ?? 1);
2074
- const resolvedPageSize = query.pagination?.pageSize ?? pageSize;
2075
- const resolvedQuery = useMemo2(
2076
- () => ({
2077
- ...query,
2078
- search: enabled.search ? { ...query.search, query: searchValue } : query.search,
2079
- sort: enabled.sort ? sort : query.sort,
2080
- tags: tag ? [.../* @__PURE__ */ new Set([...query.tags ?? [], tag])] : query.tags,
2081
- pagination: enabled.pagination ? { ...query.pagination, page, pageSize: resolvedPageSize } : query.pagination
2082
- }),
2083
- [enabled.pagination, enabled.search, enabled.sort, page, query, resolvedPageSize, searchValue, sort, tag]
2284
+ const contentChildren = asChild && isElement(children) ? void 0 : children;
2285
+ const body = typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? (hasFilters ? /* @__PURE__ */ jsxs5(Fragment4, { children: [
2286
+ /* @__PURE__ */ jsx8("span", { "data-active-filters-label": "true", children: activeFiltersLabel }),
2287
+ /* @__PURE__ */ jsxs5("ul", { "data-active-filters-list": "true", children: [
2288
+ search ? /* @__PURE__ */ jsxs5("li", { "data-filter-kind": "search", children: [
2289
+ /* @__PURE__ */ jsx8("span", { "data-filter-value": "true", children: search }),
2290
+ /* @__PURE__ */ jsx8(
2291
+ "button",
2292
+ {
2293
+ type: "button",
2294
+ "data-keep-action": "remove-search-filter",
2295
+ "aria-label": `${search} ${removeLabel}`,
2296
+ onClick: state.removeSearch,
2297
+ children: "\xD7"
2298
+ }
2299
+ )
2300
+ ] }) : null,
2301
+ tags.map((tag) => /* @__PURE__ */ jsxs5("li", { "data-filter-kind": "tag", children: [
2302
+ /* @__PURE__ */ jsx8("span", { "data-filter-value": "true", children: tag }),
2303
+ /* @__PURE__ */ jsx8(
2304
+ "button",
2305
+ {
2306
+ type: "button",
2307
+ "data-keep-action": "remove-tag-filter",
2308
+ "aria-label": `${tag} ${removeLabel}`,
2309
+ onClick: () => state.removeTag(tag),
2310
+ children: "\xD7"
2311
+ }
2312
+ )
2313
+ ] }, tag))
2314
+ ] }),
2315
+ /* @__PURE__ */ jsx8("button", { type: "button", "data-keep-action": "clear-filters", onClick: state.clear, children: clearAllLabel || clearLabel })
2316
+ ] }) : null);
2317
+ if (!hasFilters && !asChild && contentChildren === void 0) return null;
2318
+ return renderRoot(
2319
+ asChild,
2320
+ isElement(children) ? children : void 0,
2321
+ {
2322
+ ...rootProps,
2323
+ className,
2324
+ "data-keepkit": "active-filters",
2325
+ "data-state": hasFilters ? "active" : "idle",
2326
+ "aria-label": rootProps["aria-label"] ?? activeFiltersLabel
2327
+ },
2328
+ body,
2329
+ "KeepActiveFiltersSummary"
2084
2330
  );
2085
- useKeepUrlSync({
2331
+ }
2332
+ function normalizeTags(tags) {
2333
+ return [...new Set(tags.map((tag) => tag.trim()).filter(Boolean))];
2334
+ }
2335
+ function isElement(value) {
2336
+ return isValidElement2(value);
2337
+ }
2338
+
2339
+ // src/features/query/KeepTagFilter.tsx
2340
+ import { isValidElement as isValidElement3 } from "react";
2341
+
2342
+ // src/features/query/hooks/useKeepTagFilter.ts
2343
+ import { useKeepList as useKeepList2 } from "@keepkit/core/react";
2344
+ import { useCallback as useCallback2, useMemo as useMemo2, useState as useState4 } from "react";
2345
+ function useKeepTagFilter(options) {
2346
+ const { query, controlledValue, defaultValue, onChange, onValueChange } = options;
2347
+ const [uncontrolledValue, setUncontrolledValue] = useState4(defaultValue);
2348
+ const resolvedValue = controlledValue ?? uncontrolledValue;
2349
+ const list = useKeepList2({
2350
+ ...query,
2351
+ tags: resolvedValue ? [...query?.tags ?? [], resolvedValue] : query?.tags
2352
+ });
2353
+ const select = useCallback2(
2354
+ (tag) => {
2355
+ if (controlledValue === void 0) setUncontrolledValue(tag);
2356
+ onChange?.(tag);
2357
+ onValueChange?.(tag);
2358
+ },
2359
+ [controlledValue, onChange, onValueChange]
2360
+ );
2361
+ const state = useMemo2(
2362
+ () => ({ tags: list.tags, tagCounts: list.tagCounts, value: resolvedValue, select }),
2363
+ [list.tagCounts, list.tags, resolvedValue, select]
2364
+ );
2365
+ return {
2366
+ state,
2367
+ isLoading: list.isLoading,
2368
+ labels: { all: useUiLabel("allTags"), aria: useUiLabel("filterTags") }
2369
+ };
2370
+ }
2371
+
2372
+ // src/features/query/KeepTagFilter.tsx
2373
+ import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
2374
+ function KeepTagFilter({
2375
+ query,
2376
+ value: controlledValue,
2377
+ defaultValue,
2378
+ onChange,
2379
+ onValueChange,
2380
+ allLabel,
2381
+ ariaLabel,
2382
+ renderTag,
2383
+ render,
2384
+ children,
2385
+ asChild = false,
2386
+ className,
2387
+ ...rootProps
2388
+ }) {
2389
+ const view = useKeepTagFilter({ query, controlledValue, defaultValue, onChange, onValueChange });
2390
+ const contentChildren = asChild && isValidElement3(children) ? void 0 : children;
2391
+ const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? /* @__PURE__ */ jsxs6("fieldset", { children: [
2392
+ /* @__PURE__ */ jsx9("legend", { children: ariaLabel ?? view.labels.aria }),
2393
+ /* @__PURE__ */ jsx9(
2394
+ "button",
2395
+ {
2396
+ type: "button",
2397
+ "data-keep-action": "filter-all-tags",
2398
+ "aria-pressed": view.state.value === void 0,
2399
+ onClick: () => view.state.select(),
2400
+ children: allLabel ?? view.labels.all
2401
+ }
2402
+ ),
2403
+ view.state.tags.map((tag) => /* @__PURE__ */ jsxs6(
2404
+ "button",
2405
+ {
2406
+ type: "button",
2407
+ "data-keep-action": "filter-tag",
2408
+ "aria-pressed": view.state.value === tag,
2409
+ onClick: () => view.state.select(tag),
2410
+ children: [
2411
+ renderTag ? renderTag(tag, view.state.tagCounts[tag] ?? 0, view.state.value === tag) : tag,
2412
+ /* @__PURE__ */ jsxs6("span", { children: [
2413
+ " (",
2414
+ view.state.tagCounts[tag] ?? 0,
2415
+ ")"
2416
+ ] })
2417
+ ]
2418
+ },
2419
+ tag
2420
+ ))
2421
+ ] });
2422
+ return renderRoot(
2423
+ asChild,
2424
+ isValidElement3(children) ? children : void 0,
2425
+ {
2426
+ ...rootProps,
2427
+ className,
2428
+ "data-keepkit": "tag-filter",
2429
+ "data-state": view.state.value === void 0 ? "all" : "filtered",
2430
+ "data-loading": view.isLoading ? "true" : void 0
2431
+ },
2432
+ body,
2433
+ "KeepTagFilter"
2434
+ );
2435
+ }
2436
+
2437
+ // src/features/query/hooks/useQueryControls.ts
2438
+ import { useEffect as useEffect4, useState as useState5 } from "react";
2439
+ function useKeepSearchInput(options) {
2440
+ const { controlledValue, defaultValue, debounceMs, onValueChange } = options;
2441
+ const [uncontrolledValue, setUncontrolledValue] = useState5(defaultValue);
2442
+ const value = controlledValue ?? uncontrolledValue;
2443
+ useEffect4(() => {
2444
+ if (!onValueChange) return;
2445
+ if (debounceMs <= 0) {
2446
+ onValueChange(value);
2447
+ return;
2448
+ }
2449
+ const timer = window.setTimeout(() => onValueChange(value), debounceMs);
2450
+ return () => window.clearTimeout(timer);
2451
+ }, [debounceMs, onValueChange, value]);
2452
+ return {
2453
+ value,
2454
+ label: useUiLabel("search"),
2455
+ change: (event) => {
2456
+ if (controlledValue === void 0) setUncontrolledValue(event.currentTarget.value);
2457
+ }
2458
+ };
2459
+ }
2460
+ function useKeepSortSelect(options) {
2461
+ const { controlledValue, defaultValue, onValueChange } = options;
2462
+ const [uncontrolledValue, setUncontrolledValue] = useState5(defaultValue);
2463
+ const value = controlledValue ?? uncontrolledValue;
2464
+ return {
2465
+ value,
2466
+ change: (event) => {
2467
+ const nextValue = event.currentTarget.value;
2468
+ if (controlledValue === void 0) setUncontrolledValue(nextValue);
2469
+ const [by, direction] = nextValue.split(":");
2470
+ onValueChange?.(nextValue, { by, direction });
2471
+ },
2472
+ labels: {
2473
+ sort: useUiLabel("sort"),
2474
+ updatedNewest: useUiLabel("updatedNewest"),
2475
+ updatedOldest: useUiLabel("updatedOldest"),
2476
+ savedNewest: useUiLabel("savedNewest"),
2477
+ savedOldest: useUiLabel("savedOldest")
2478
+ }
2479
+ };
2480
+ }
2481
+ function useKeepPagination(options) {
2482
+ const { totalCount, pageSize, page, maxPageButtons, onPageChange } = options;
2483
+ const pageCount = Math.max(1, Math.ceil(totalCount / Math.max(1, pageSize)));
2484
+ const currentPage = Math.min(Math.max(1, page), pageCount);
2485
+ const goToPage = (nextPage) => {
2486
+ const next = Math.min(Math.max(1, nextPage), pageCount);
2487
+ onPageChange?.(next, (next - 1) * pageSize);
2488
+ };
2489
+ return {
2490
+ pageCount,
2491
+ currentPage,
2492
+ goToPage,
2493
+ visiblePages: getVisiblePages(currentPage, pageCount, Math.max(1, maxPageButtons)),
2494
+ labels: {
2495
+ previous: useUiLabel("previousPage"),
2496
+ next: useUiLabel("nextPage"),
2497
+ page: useUiLabel("page"),
2498
+ pagination: useUiLabel("pagination")
2499
+ }
2500
+ };
2501
+ }
2502
+ function getVisiblePages(currentPage, pageCount, maxPageButtons) {
2503
+ if (pageCount <= maxPageButtons) return Array.from({ length: pageCount }, (_, index) => index + 1);
2504
+ const half = Math.floor(maxPageButtons / 2);
2505
+ const start = Math.min(Math.max(1, currentPage - half), pageCount - maxPageButtons + 1);
2506
+ return Array.from({ length: maxPageButtons }, (_, index) => start + index);
2507
+ }
2508
+
2509
+ // src/features/query/query-controls.tsx
2510
+ import { Fragment as Fragment5, jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
2511
+ function KeepSearchInput({
2512
+ value: controlledValue,
2513
+ defaultValue = "",
2514
+ debounceMs = 300,
2515
+ onValueChange,
2516
+ "aria-label": ariaLabel,
2517
+ placeholder,
2518
+ ...props
2519
+ }) {
2520
+ const view = useKeepSearchInput({ controlledValue, defaultValue, debounceMs, onValueChange });
2521
+ return /* @__PURE__ */ jsx10(
2522
+ "input",
2523
+ {
2524
+ ...props,
2525
+ "data-keepkit": "search-input",
2526
+ "data-keep-action": "search",
2527
+ type: "search",
2528
+ value: view.value,
2529
+ "data-state": view.value ? "active" : "idle",
2530
+ "data-disabled": props.disabled ? "true" : void 0,
2531
+ "aria-label": ariaLabel ?? view.label,
2532
+ placeholder: placeholder ?? view.label,
2533
+ onChange: view.change
2534
+ }
2535
+ );
2536
+ }
2537
+ function KeepSortSelect({
2538
+ value: controlledValue,
2539
+ defaultValue = "updatedAt:desc",
2540
+ onValueChange,
2541
+ "aria-label": ariaLabel,
2542
+ children,
2543
+ ...props
2544
+ }) {
2545
+ const view = useKeepSortSelect({ controlledValue, defaultValue, onValueChange });
2546
+ const options = children ?? /* @__PURE__ */ jsxs7(Fragment5, { children: [
2547
+ /* @__PURE__ */ jsx10("option", { value: "updatedAt:desc", children: view.labels.updatedNewest }),
2548
+ /* @__PURE__ */ jsx10("option", { value: "updatedAt:asc", children: view.labels.updatedOldest }),
2549
+ /* @__PURE__ */ jsx10("option", { value: "savedAt:desc", children: view.labels.savedNewest }),
2550
+ /* @__PURE__ */ jsx10("option", { value: "savedAt:asc", children: view.labels.savedOldest })
2551
+ ] });
2552
+ return /* @__PURE__ */ jsx10(
2553
+ "select",
2554
+ {
2555
+ ...props,
2556
+ "data-keepkit": "sort-select",
2557
+ "data-keep-action": "sort",
2558
+ value: view.value,
2559
+ "data-state": "selected",
2560
+ "data-disabled": props.disabled ? "true" : void 0,
2561
+ "aria-label": ariaLabel ?? view.labels.sort,
2562
+ onChange: view.change,
2563
+ children: options
2564
+ }
2565
+ );
2566
+ }
2567
+ function KeepPagination({
2568
+ totalCount,
2569
+ pageSize,
2570
+ page = 1,
2571
+ maxPageButtons = 7,
2572
+ onPageChange,
2573
+ render,
2574
+ ...props
2575
+ }) {
2576
+ const view = useKeepPagination({ totalCount, pageSize, page, maxPageButtons, onPageChange });
2577
+ const navProps = {
2578
+ ...props,
2579
+ "data-keepkit": "pagination",
2580
+ "aria-label": props["aria-label"] ?? view.labels.pagination,
2581
+ "data-state": view.pageCount > 1 ? "active" : "idle"
2582
+ };
2583
+ if (render)
2584
+ return /* @__PURE__ */ jsx10("nav", { ...navProps, children: render({ page: view.currentPage, pageCount: view.pageCount, goToPage: view.goToPage }) });
2585
+ return /* @__PURE__ */ jsxs7("nav", { ...navProps, children: [
2586
+ /* @__PURE__ */ jsx10(
2587
+ "button",
2588
+ {
2589
+ type: "button",
2590
+ "data-keep-action": "previous-page",
2591
+ onClick: () => view.goToPage(view.currentPage - 1),
2592
+ disabled: view.currentPage <= 1,
2593
+ children: view.labels.previous
2594
+ }
2595
+ ),
2596
+ view.visiblePages.map((nextPage) => /* @__PURE__ */ jsx10(
2597
+ "button",
2598
+ {
2599
+ type: "button",
2600
+ "data-keep-action": "select-page",
2601
+ "aria-current": nextPage === view.currentPage ? "page" : void 0,
2602
+ "aria-label": `${view.labels.page} ${nextPage}`,
2603
+ onClick: () => view.goToPage(nextPage),
2604
+ children: nextPage
2605
+ },
2606
+ nextPage
2607
+ )),
2608
+ /* @__PURE__ */ jsx10(
2609
+ "button",
2610
+ {
2611
+ type: "button",
2612
+ "data-keep-action": "next-page",
2613
+ onClick: () => view.goToPage(view.currentPage + 1),
2614
+ disabled: view.currentPage >= view.pageCount,
2615
+ children: view.labels.next
2616
+ }
2617
+ )
2618
+ ] });
2619
+ }
2620
+
2621
+ // src/features/collection/hooks/useKeepCollection.ts
2622
+ import { useKeepList as useKeepList3 } from "@keepkit/core/react";
2623
+ import { useMemo as useMemo3, useState as useState6 } from "react";
2624
+ function useKeepCollection({
2625
+ query,
2626
+ pageSize,
2627
+ urlSync,
2628
+ urlAdapter,
2629
+ features
2630
+ }) {
2631
+ const enabled = {
2632
+ search: true,
2633
+ sort: true,
2634
+ pagination: true,
2635
+ tagFilter: false,
2636
+ bulkActions: false,
2637
+ ...features
2638
+ };
2639
+ const [searchValue, setSearchValue] = useState6(query.search?.query ?? "");
2640
+ const [sort, setSort] = useState6(query.sort ?? { by: "updatedAt", direction: "desc" });
2641
+ const [activeTags, setActiveTags] = useState6(query.tags ?? []);
2642
+ const [page, setPage] = useState6(query.pagination?.page ?? 1);
2643
+ const resolvedPageSize = query.pagination?.pageSize ?? pageSize;
2644
+ const resolvedQuery = useMemo3(
2645
+ () => ({
2646
+ ...query,
2647
+ search: enabled.search ? { ...query.search, query: searchValue } : query.search,
2648
+ sort: enabled.sort ? sort : query.sort,
2649
+ tags: activeTags.length > 0 ? activeTags : void 0,
2650
+ pagination: enabled.pagination ? { ...query.pagination, page, pageSize: resolvedPageSize } : query.pagination
2651
+ }),
2652
+ [activeTags, enabled.pagination, enabled.search, enabled.sort, page, query, resolvedPageSize, searchValue, sort]
2653
+ );
2654
+ useKeepUrlSync({
2086
2655
  enabled: Boolean(urlSync),
2087
2656
  query: resolvedQuery,
2088
2657
  onQueryChange: (nextOrUpdater) => {
2089
2658
  const next = typeof nextOrUpdater === "function" ? nextOrUpdater(resolvedQuery) : nextOrUpdater;
2090
2659
  setSearchValue(next.search?.query ?? "");
2091
2660
  setSort(next.sort ?? { by: "updatedAt", direction: "desc" });
2092
- setTag(next.tags?.[0]);
2661
+ setActiveTags(next.tags ?? []);
2093
2662
  setPage(next.pagination?.page ?? 1);
2094
2663
  },
2095
2664
  options: typeof urlSync === "object" ? urlSync : {},
2096
2665
  adapter: urlAdapter
2097
2666
  });
2098
- const list = useKeepList2(resolvedQuery);
2667
+ const list = useKeepList3(resolvedQuery);
2668
+ const allState = useKeepList3({});
2099
2669
  return {
2100
2670
  enabled,
2101
2671
  searchValue,
2102
2672
  sortValue: sortToValue(sort),
2103
- tag,
2673
+ activeTags,
2104
2674
  resolvedPageSize,
2105
2675
  resolvedQuery,
2106
2676
  list,
2677
+ allState,
2107
2678
  setSearchValue: (value) => {
2108
2679
  setSearchValue(value);
2109
2680
  setPage(1);
@@ -2113,182 +2684,54 @@ function useKeepCollection({
2113
2684
  setPage(1);
2114
2685
  },
2115
2686
  setTag: (value) => {
2116
- setTag(value);
2687
+ setActiveTags(value ? [value] : []);
2688
+ setPage(1);
2689
+ },
2690
+ removeTag: (tagToRemove) => {
2691
+ setActiveTags((current) => current.filter((tag) => tag !== tagToRemove));
2692
+ setPage(1);
2693
+ },
2694
+ clearFilters: () => {
2695
+ setSearchValue("");
2696
+ setActiveTags([]);
2117
2697
  setPage(1);
2118
2698
  },
2119
2699
  setPage
2120
2700
  };
2121
2701
  }
2122
2702
 
2123
- // src/KeepList.tsx
2703
+ // src/features/collection/KeepList.tsx
2124
2704
  import { KeepErrorBoundary } from "@keepkit/core/react";
2125
- import { isValidElement as isValidElement3 } from "react";
2126
-
2127
- // src/hooks/useKeepListView.ts
2128
- import { useKeepList as useKeepList3 } from "@keepkit/core/react";
2129
- function useKeepListView(query) {
2130
- return {
2131
- state: useKeepList3(query),
2132
- labels: {
2133
- loading: useUiLabel("loadingItems"),
2134
- empty: useUiLabel("noItems"),
2135
- error: useUiLabel("errorItems")
2136
- }
2137
- };
2138
- }
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
- }
2705
+ import { isValidElement as isValidElement6 } from "react";
2204
2706
 
2205
- // src/KeepItemCard.tsx
2707
+ // src/features/item/KeepItemCard.tsx
2206
2708
  import {
2207
2709
  createContext as createContext3,
2208
2710
  createElement as createElement2,
2209
- isValidElement as isValidElement2,
2711
+ isValidElement as isValidElement4,
2210
2712
  useContext as useContext3,
2211
- useEffect as useEffect4,
2212
- useState as useState5
2713
+ useEffect as useEffect5,
2714
+ useState as useState8
2213
2715
  } from "react";
2214
2716
 
2215
- // src/hooks/useKeepItemCard.ts
2216
- import { useKeepItem as useKeepItem2 } from "@keepkit/core/react";
2217
- function useKeepItemCard(options) {
2218
- const {
2219
- item,
2220
- title,
2221
- getTitle,
2222
- getImageProps,
2223
- href: hrefOption,
2224
- linkTargetAttribute,
2225
- linkRel,
2226
- onRemoveError,
2227
- onRemoved
2228
- } = options;
2229
- const itemState = useKeepItem2(item);
2230
- const emitFeedback = useKeepUiFeedback();
2231
- const removedMessage = useUiLabel("removedMessage");
2232
- const restoredMessage = useUiLabel("restoredMessage");
2233
- const undoLabel = useUiLabel("undo");
2234
- const resolvedTitle = typeof title === "function" ? title(item) : title ?? getTitle?.(item) ?? getMetaTitle(item.meta) ?? item.id;
2235
- const imageProps = getImageProps?.(item, resolvedTitle);
2236
- const href = typeof hrefOption === "function" ? hrefOption(item) : hrefOption;
2237
- const isAvailable = item.status === void 0 || item.status === "available";
2238
- const isExternalLink = href ? /^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(href) : false;
2239
- const statusLabelKey = item.status && item.status !== "available" ? getStatusLabelKey(item.status) : "statusUnknown";
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
- };
2261
- const state = {
2262
- item,
2263
- isSaved: itemState.isSaved,
2264
- isMutating: itemState.isMutating,
2265
- error: itemState.error,
2266
- remove,
2267
- status: item.status
2268
- };
2717
+ // src/features/item/hooks/useKeepItemStatusBadge.ts
2718
+ function useKeepItemStatusBadge(status) {
2269
2719
  return {
2270
- itemState,
2271
- state,
2272
- resolvedTitle,
2273
- imageProps,
2274
- href,
2275
- isAvailable,
2276
- displayStatus: getDisplayStatus(item.status),
2277
- resolvedLinkTarget: linkTargetAttribute ?? (isExternalLink ? "_blank" : void 0),
2278
- resolvedLinkRel: linkRel ?? (isExternalLink ? "noreferrer" : void 0),
2279
- statusLabel: item.status && item.status !== "available" ? unavailableLabel : void 0,
2280
- remove,
2281
- labels: {
2282
- save: useUiLabel("save"),
2283
- savedAt: useUiLabel("saved"),
2284
- error: useUiLabel("error"),
2285
- remove: useUiLabel("remove"),
2286
- tags: useUiLabel("tags")
2287
- }
2720
+ resolvedStatus: getDisplayStatus(status),
2721
+ statusLabel: useUiLabel(getStatusLabelKey(status)),
2722
+ icon: getStatusIcon(status)
2288
2723
  };
2289
2724
  }
2725
+ function getStatusIcon(status) {
2726
+ if (status === "available") return "check";
2727
+ if (status === "expired") return "clock";
2728
+ if (status === "removed" || status === "deleted") return "ban";
2729
+ return "lock";
2730
+ }
2290
2731
  function getStatusLabelKey(status) {
2291
2732
  switch (status) {
2733
+ case "available":
2734
+ return "statusAvailable";
2292
2735
  case "expired":
2293
2736
  return "statusExpired";
2294
2737
  case "removed":
@@ -2297,28 +2740,85 @@ function getStatusLabelKey(status) {
2297
2740
  return "statusDeleted";
2298
2741
  case "private":
2299
2742
  return "statusPrivate";
2300
- default:
2743
+ case "unknown":
2301
2744
  return "statusUnknown";
2745
+ case "restricted":
2746
+ return "statusPrivate";
2302
2747
  }
2303
2748
  }
2304
2749
  function getDisplayStatus(status) {
2305
- if (status === void 0 || status === "available") return "available";
2750
+ if (status === "available") return "available";
2306
2751
  if (status === "expired") return "expired";
2307
- if (status === "removed") return "removed";
2752
+ if (status === "removed" || status === "deleted") return "removed";
2308
2753
  return "restricted";
2309
2754
  }
2310
2755
 
2311
- // src/hooks/useKeepStaleNotice.ts
2312
- import { useKeepContext as useKeepContext2 } from "@keepkit/core/react";
2313
- import { useState as useState4 } from "react";
2756
+ // src/features/item/KeepItemStatusBadge.tsx
2757
+ import { Fragment as Fragment6, jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
2758
+ function KeepItemStatusBadge({ status = "available", label, className, ...props }) {
2759
+ const view = useKeepItemStatusBadge(status);
2760
+ return /* @__PURE__ */ jsxs8(
2761
+ "span",
2762
+ {
2763
+ ...props,
2764
+ className,
2765
+ role: "img",
2766
+ "aria-label": props["aria-label"] ?? view.statusLabel,
2767
+ "data-keepkit": "status-badge",
2768
+ "data-status": status,
2769
+ "data-item-status": view.resolvedStatus,
2770
+ children: [
2771
+ /* @__PURE__ */ jsx11(StatusIcon, { name: view.icon }),
2772
+ /* @__PURE__ */ jsx11("span", { "data-status-label": "true", children: label ?? view.statusLabel })
2773
+ ]
2774
+ }
2775
+ );
2776
+ }
2777
+ function StatusIcon({ name }) {
2778
+ return /* @__PURE__ */ jsxs8(
2779
+ "svg",
2780
+ {
2781
+ "data-status-icon": name,
2782
+ viewBox: "0 0 24 24",
2783
+ width: "1em",
2784
+ height: "1em",
2785
+ fill: "none",
2786
+ stroke: "currentColor",
2787
+ strokeWidth: "2",
2788
+ strokeLinecap: "round",
2789
+ strokeLinejoin: "round",
2790
+ "aria-hidden": "true",
2791
+ focusable: "false",
2792
+ children: [
2793
+ name === "check" ? /* @__PURE__ */ jsx11("path", { d: "m5 12 4 4L19 6" }) : null,
2794
+ name === "clock" ? /* @__PURE__ */ jsxs8(Fragment6, { children: [
2795
+ /* @__PURE__ */ jsx11("circle", { cx: "12", cy: "12", r: "8" }),
2796
+ /* @__PURE__ */ jsx11("path", { d: "M12 7v5l3 2" })
2797
+ ] }) : null,
2798
+ name === "ban" ? /* @__PURE__ */ jsxs8(Fragment6, { children: [
2799
+ /* @__PURE__ */ jsx11("circle", { cx: "12", cy: "12", r: "8" }),
2800
+ /* @__PURE__ */ jsx11("path", { d: "m6.5 6.5 11 11" })
2801
+ ] }) : null,
2802
+ name === "lock" ? /* @__PURE__ */ jsxs8(Fragment6, { children: [
2803
+ /* @__PURE__ */ jsx11("rect", { x: "5", y: "10", width: "14", height: "10", rx: "2" }),
2804
+ /* @__PURE__ */ jsx11("path", { d: "M8 10V7a4 4 0 0 1 8 0v3" })
2805
+ ] }) : null
2806
+ ]
2807
+ }
2808
+ );
2809
+ }
2810
+
2811
+ // src/features/status/hooks/useKeepStaleNotice.ts
2812
+ import { useKeepContext as useKeepContext3 } from "@keepkit/core/react";
2813
+ import { useState as useState7 } from "react";
2314
2814
  function useKeepStaleNotice({ item, onRetry, onRemoved }) {
2315
- const context = useKeepContext2();
2815
+ const context = useKeepContext3();
2316
2816
  const emitFeedback = useKeepUiFeedback();
2317
2817
  const removedMessage = useUiLabel("removedMessage");
2318
2818
  const restoredMessage = useUiLabel("restoredMessage");
2319
2819
  const undoLabel = useUiLabel("undo");
2320
- const [isRetrying, setIsRetrying] = useState4(false);
2321
- const [error, setError] = useState4(null);
2820
+ const [isRetrying, setIsRetrying] = useState7(false);
2821
+ const [error, setError] = useState7(null);
2322
2822
  async function retry() {
2323
2823
  setError(null);
2324
2824
  setIsRetrying(true);
@@ -2364,7 +2864,7 @@ function useKeepStaleNotice({ item, onRetry, onRemoved }) {
2364
2864
  };
2365
2865
  }
2366
2866
  function useKeepPruneStale({ statuses, onPruned }) {
2367
- const context = useKeepContext2();
2867
+ const context = useKeepContext3();
2368
2868
  const emitFeedback = useKeepUiFeedback();
2369
2869
  const staleItems = context.items.filter((item) => item.status && statuses.includes(item.status));
2370
2870
  const staleIds = staleItems.map((item) => item.id);
@@ -2397,54 +2897,8 @@ function useKeepPruneStale({ statuses, onPruned }) {
2397
2897
  };
2398
2898
  }
2399
2899
 
2400
- // src/hooks/useKeepItemStatusBadge.ts
2401
- function useKeepItemStatusBadge(status) {
2402
- return { resolvedStatus: getDisplayStatus2(status), statusLabel: useUiLabel(getStatusLabelKey2(status)) };
2403
- }
2404
- function getStatusLabelKey2(status) {
2405
- switch (status) {
2406
- case "available":
2407
- return "statusAvailable";
2408
- case "expired":
2409
- return "statusExpired";
2410
- case "removed":
2411
- return "statusRemoved";
2412
- case "deleted":
2413
- return "statusDeleted";
2414
- case "private":
2415
- return "statusPrivate";
2416
- case "unknown":
2417
- return "statusUnknown";
2418
- case "restricted":
2419
- return "statusPrivate";
2420
- }
2421
- }
2422
- function getDisplayStatus2(status) {
2423
- if (status === "available") return "available";
2424
- if (status === "expired") return "expired";
2425
- if (status === "removed") return "removed";
2426
- return "restricted";
2427
- }
2428
-
2429
- // src/KeepItemStatusBadge.tsx
2430
- import { jsx as jsx7 } from "react/jsx-runtime";
2431
- function KeepItemStatusBadge({ status = "available", label, className, ...props }) {
2432
- const view = useKeepItemStatusBadge(status);
2433
- return /* @__PURE__ */ jsx7(
2434
- "span",
2435
- {
2436
- ...props,
2437
- className,
2438
- "data-keepkit": "status-badge",
2439
- "data-status": status,
2440
- "data-item-status": view.resolvedStatus,
2441
- children: label ?? view.statusLabel
2442
- }
2443
- );
2444
- }
2445
-
2446
- // src/KeepStaleNotice.tsx
2447
- import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
2900
+ // src/features/status/KeepStaleNotice.tsx
2901
+ import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
2448
2902
  function KeepStaleNotice({
2449
2903
  item,
2450
2904
  onRetry,
@@ -2456,11 +2910,11 @@ function KeepStaleNotice({
2456
2910
  ...props
2457
2911
  }) {
2458
2912
  const view = useKeepStaleNotice({ item, onRetry, onRemoved });
2459
- return /* @__PURE__ */ jsxs4("aside", { ...props, className, "data-keepkit": "stale-notice", "data-state": view.error ? "error" : "stale", children: [
2460
- /* @__PURE__ */ jsx8(KeepItemStatusBadge, { status: view.status }),
2461
- children ?? (item.statusReason ? /* @__PURE__ */ jsx8("p", { children: item.statusReason }) : null),
2462
- /* @__PURE__ */ jsxs4("div", { children: [
2463
- /* @__PURE__ */ jsx8(
2913
+ return /* @__PURE__ */ jsxs9("aside", { ...props, className, "data-keepkit": "stale-notice", "data-state": view.error ? "error" : "stale", children: [
2914
+ /* @__PURE__ */ jsx12(KeepItemStatusBadge, { status: view.status }),
2915
+ children ?? (item.statusReason ? /* @__PURE__ */ jsx12("p", { children: item.statusReason }) : null),
2916
+ /* @__PURE__ */ jsxs9("div", { children: [
2917
+ /* @__PURE__ */ jsx12(
2464
2918
  "button",
2465
2919
  {
2466
2920
  type: "button",
@@ -2470,7 +2924,7 @@ function KeepStaleNotice({
2470
2924
  children: retryLabel ?? view.labels.retry
2471
2925
  }
2472
2926
  ),
2473
- /* @__PURE__ */ jsx8(
2927
+ /* @__PURE__ */ jsx12(
2474
2928
  "button",
2475
2929
  {
2476
2930
  type: "button",
@@ -2481,7 +2935,7 @@ function KeepStaleNotice({
2481
2935
  }
2482
2936
  )
2483
2937
  ] }),
2484
- view.error ? /* @__PURE__ */ jsx8("p", { role: "alert", children: view.error instanceof Error ? view.error.message : view.labels.error }) : null
2938
+ view.error ? /* @__PURE__ */ jsx12("p", { role: "alert", children: view.error instanceof Error ? view.error.message : view.labels.error }) : null
2485
2939
  ] });
2486
2940
  }
2487
2941
  function KeepPruneStaleButton({
@@ -2493,7 +2947,7 @@ function KeepPruneStaleButton({
2493
2947
  ...props
2494
2948
  }) {
2495
2949
  const view = useKeepPruneStale({ statuses, onPruned });
2496
- return /* @__PURE__ */ jsx8(
2950
+ return /* @__PURE__ */ jsx12(
2497
2951
  "button",
2498
2952
  {
2499
2953
  ...props,
@@ -2509,8 +2963,104 @@ function KeepPruneStaleButton({
2509
2963
  );
2510
2964
  }
2511
2965
 
2512
- // src/KeepItemCard.tsx
2513
- import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
2966
+ // src/features/item/hooks/useKeepItemCard.ts
2967
+ import { useKeepItem as useKeepItem2 } from "@keepkit/core/react";
2968
+ function useKeepItemCard(options) {
2969
+ const {
2970
+ item,
2971
+ title,
2972
+ getTitle,
2973
+ getImageProps,
2974
+ href: hrefOption,
2975
+ linkTargetAttribute,
2976
+ linkRel,
2977
+ onRemoveError,
2978
+ onRemoved
2979
+ } = options;
2980
+ const itemState = useKeepItem2(item);
2981
+ const emitFeedback = useKeepUiFeedback();
2982
+ const removedMessage = useUiLabel("removedMessage");
2983
+ const restoredMessage = useUiLabel("restoredMessage");
2984
+ const undoLabel = useUiLabel("undo");
2985
+ const resolvedTitle = typeof title === "function" ? title(item) : title ?? getTitle?.(item) ?? getMetaTitle(item.meta) ?? item.id;
2986
+ const imageProps = getImageProps?.(item, resolvedTitle);
2987
+ const href = typeof hrefOption === "function" ? hrefOption(item) : hrefOption;
2988
+ const isAvailable = item.status === void 0 || item.status === "available";
2989
+ const isExternalLink = href ? /^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(href) : false;
2990
+ const statusLabelKey = item.status && item.status !== "available" ? getStatusLabelKey2(item.status) : "statusUnknown";
2991
+ const unavailableLabel = useUiLabel(statusLabelKey);
2992
+ const remove = async () => {
2993
+ const wasSaved = itemState.isSaved;
2994
+ try {
2995
+ await itemState.removeWithUndo();
2996
+ onRemoved?.(item);
2997
+ if (!wasSaved) return;
2998
+ emitFeedback({
2999
+ type: "item-removed",
3000
+ item,
3001
+ message: removedMessage,
3002
+ undoLabel,
3003
+ undo: async () => {
3004
+ await itemState.undo();
3005
+ emitFeedback({ type: "item-restored", item, items: [item], message: restoredMessage });
3006
+ }
3007
+ });
3008
+ } catch (cause) {
3009
+ onRemoveError?.(cause);
3010
+ }
3011
+ };
3012
+ const state = {
3013
+ item,
3014
+ isSaved: itemState.isSaved,
3015
+ isMutating: itemState.isMutating,
3016
+ error: itemState.error,
3017
+ remove,
3018
+ status: item.status
3019
+ };
3020
+ return {
3021
+ itemState,
3022
+ state,
3023
+ resolvedTitle,
3024
+ imageProps,
3025
+ href,
3026
+ isAvailable,
3027
+ displayStatus: getDisplayStatus2(item.status),
3028
+ resolvedLinkTarget: linkTargetAttribute ?? (isExternalLink ? "_blank" : void 0),
3029
+ resolvedLinkRel: linkRel ?? (isExternalLink ? "noreferrer" : void 0),
3030
+ statusLabel: item.status && item.status !== "available" ? unavailableLabel : void 0,
3031
+ remove,
3032
+ labels: {
3033
+ save: useUiLabel("save"),
3034
+ savedAt: useUiLabel("saved"),
3035
+ error: useUiLabel("error"),
3036
+ remove: useUiLabel("remove"),
3037
+ tags: useUiLabel("tags")
3038
+ }
3039
+ };
3040
+ }
3041
+ function getStatusLabelKey2(status) {
3042
+ switch (status) {
3043
+ case "expired":
3044
+ return "statusExpired";
3045
+ case "removed":
3046
+ return "statusRemoved";
3047
+ case "deleted":
3048
+ return "statusDeleted";
3049
+ case "private":
3050
+ return "statusPrivate";
3051
+ default:
3052
+ return "statusUnknown";
3053
+ }
3054
+ }
3055
+ function getDisplayStatus2(status) {
3056
+ if (status === void 0 || status === "available") return "available";
3057
+ if (status === "expired") return "expired";
3058
+ if (status === "removed") return "removed";
3059
+ return "restricted";
3060
+ }
3061
+
3062
+ // src/features/item/KeepItemCard.tsx
3063
+ import { Fragment as Fragment7, jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
2514
3064
  var KeepItemCardContext = createContext3(null);
2515
3065
  function useKeepItemCardCompound(part) {
2516
3066
  const context = useContext3(KeepItemCardContext);
@@ -2560,7 +3110,7 @@ function KeepItemCardRoot({
2560
3110
  });
2561
3111
  const contextQuery = useKeepSearchQuery();
2562
3112
  const searchQuery = highlightQuery ?? contextQuery;
2563
- const contentChildren = asChild && isValidElement2(children) ? void 0 : children;
3113
+ const contentChildren = asChild && isValidElement4(children) ? void 0 : children;
2564
3114
  function renderLink(content) {
2565
3115
  if (!view.href || !view.isAvailable) return content;
2566
3116
  const linkProps = {
@@ -2570,17 +3120,17 @@ function KeepItemCardRoot({
2570
3120
  onClick: (event) => onOpen?.(item, event),
2571
3121
  children: content
2572
3122
  };
2573
- return LinkComponent ? /* @__PURE__ */ jsx9(LinkComponent, { ...linkProps }) : /* @__PURE__ */ jsx9("a", { ...linkProps });
3123
+ return LinkComponent ? /* @__PURE__ */ jsx13(LinkComponent, { ...linkProps }) : /* @__PURE__ */ jsx13("a", { ...linkProps });
2574
3124
  }
2575
3125
  function renderTitle(content) {
2576
3126
  if (linkTarget !== "title") return content;
2577
3127
  if (view.isAvailable) return renderLink(content);
2578
- return view.href ? /* @__PURE__ */ jsx9("span", { "aria-disabled": "true", "data-link-disabled": "true", children: content }) : content;
3128
+ return view.href ? /* @__PURE__ */ jsx13("span", { "aria-disabled": "true", "data-link-disabled": "true", children: content }) : content;
2579
3129
  }
2580
3130
  const resolvedImageProps = view.imageProps ? { ...view.imageProps, alt: imageAlt ?? view.imageProps.alt } : void 0;
2581
3131
  const imageSource = resolvedImageProps?.src;
2582
- const [imageStatus, setImageStatus] = useState5(imageSource ? "loading" : "error");
2583
- useEffect4(() => {
3132
+ const [imageStatus, setImageStatus] = useState8(imageSource ? "loading" : "error");
3133
+ useEffect5(() => {
2584
3134
  setImageStatus(imageSource ? "loading" : "error");
2585
3135
  }, [imageSource]);
2586
3136
  let image = null;
@@ -2598,21 +3148,21 @@ function KeepItemCardRoot({
2598
3148
  setImageStatus("error");
2599
3149
  }
2600
3150
  };
2601
- image = renderImage?.(imagePropsWithHandlers, item) ?? (ImageComponent ? /* @__PURE__ */ jsx9(ImageComponent, { ...imagePropsWithHandlers }) : /* @__PURE__ */ jsx9("img", { ...imagePropsWithHandlers, alt: imagePropsWithHandlers.alt }));
3151
+ image = renderImage?.(imagePropsWithHandlers, item) ?? (ImageComponent ? /* @__PURE__ */ jsx13(ImageComponent, { ...imagePropsWithHandlers }) : /* @__PURE__ */ jsx13("img", { ...imagePropsWithHandlers, alt: imagePropsWithHandlers.alt }));
2602
3152
  }
2603
3153
  const tags = showTags ? item.tags ?? [] : [];
2604
3154
  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: [
3155
+ const meta = showSavedAt ? /* @__PURE__ */ jsxs10("div", { "data-card-meta": true, children: [
3156
+ /* @__PURE__ */ jsxs10("span", { children: [
2607
3157
  view.labels.savedAt,
2608
3158
  ":"
2609
3159
  ] }),
2610
3160
  " ",
2611
- /* @__PURE__ */ jsx9("time", { dateTime: new Date(item.savedAt).toISOString(), children: formatSavedAt(item.savedAt) })
3161
+ /* @__PURE__ */ jsx13("time", { dateTime: new Date(item.savedAt).toISOString(), children: formatSavedAt(item.savedAt) })
2612
3162
  ] }) : 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(
3163
+ const error = view.itemState.error ? /* @__PURE__ */ jsx13("p", { role: "alert", children: getErrorMessage2(view.itemState.error, view.labels.error) }) : null;
3164
+ const actions = view.statusLabel ? /* @__PURE__ */ jsx13(KeepStaleNotice, { item, onRetry, onRemoved }) : /* @__PURE__ */ jsxs10(Fragment7, { children: [
3165
+ showSaveButton ? /* @__PURE__ */ jsx13(
2616
3166
  KeepButton,
2617
3167
  {
2618
3168
  item: toKeepButtonItem(item),
@@ -2620,7 +3170,7 @@ function KeepItemCardRoot({
2620
3170
  getAriaLabel: (buttonState) => `${buttonState.isSaved ? view.labels.remove : view.labels.save} ${String(view.resolvedTitle)}`
2621
3171
  }
2622
3172
  ) : null,
2623
- /* @__PURE__ */ jsx9(
3173
+ /* @__PURE__ */ jsx13(
2624
3174
  "button",
2625
3175
  {
2626
3176
  type: "button",
@@ -2643,18 +3193,18 @@ function KeepItemCardRoot({
2643
3193
  meta,
2644
3194
  error,
2645
3195
  actions,
2646
- renderText: (content) => /* @__PURE__ */ jsx9(KeepHighlight, { query: searchQuery, children: content })
3196
+ renderText: (content) => /* @__PURE__ */ jsx13(KeepHighlight, { query: searchQuery, children: content })
2647
3197
  };
2648
- const defaultBody = /* @__PURE__ */ jsxs5(Fragment4, { children: [
2649
- /* @__PURE__ */ jsx9(KeepItemCardMedia, {}),
2650
- /* @__PURE__ */ jsx9(KeepItemCardContent, {}),
2651
- /* @__PURE__ */ jsx9(KeepItemCardActions, {})
3198
+ const defaultBody = /* @__PURE__ */ jsxs10(Fragment7, { children: [
3199
+ /* @__PURE__ */ jsx13(KeepItemCardMedia, {}),
3200
+ /* @__PURE__ */ jsx13(KeepItemCardContent, {}),
3201
+ /* @__PURE__ */ jsx13(KeepItemCardActions, {})
2652
3202
  ] });
2653
3203
  const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? defaultBody;
2654
3204
  const linkedBody = linkTarget === "card" && view.isAvailable ? renderLink(body) : body;
2655
3205
  const root = renderRoot(
2656
3206
  asChild,
2657
- isValidElement2(children) ? children : void 0,
3207
+ isValidElement4(children) ? children : void 0,
2658
3208
  {
2659
3209
  ...rootProps,
2660
3210
  className,
@@ -2669,18 +3219,18 @@ function KeepItemCardRoot({
2669
3219
  linkedBody,
2670
3220
  "KeepItemCard"
2671
3221
  );
2672
- return /* @__PURE__ */ jsx9(KeepItemCardContext.Provider, { value: compoundValue, children: root });
3222
+ return /* @__PURE__ */ jsx13(KeepItemCardContext.Provider, { value: compoundValue, children: root });
2673
3223
  }
2674
3224
  function KeepItemCardMedia({ children, fallback, ...props }) {
2675
3225
  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, {}) }) });
3226
+ return /* @__PURE__ */ jsx13("div", { ...props, "data-keep-card-part": "media", "data-media-status": context.imageStatus, "data-aspect-ratio": "1/1", children: children ?? context.image ?? /* @__PURE__ */ jsx13("span", { role: "img", "aria-label": context.fallbackLabel, "data-keep-card-fallback": "true", children: fallback ?? /* @__PURE__ */ jsx13(KeepMediaPlaceholderIcon, {}) }) });
2677
3227
  }
2678
3228
  function KeepItemCardContent({ children, ...props }) {
2679
3229
  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, {}),
3230
+ return /* @__PURE__ */ jsx13("div", { ...props, "data-keep-card-part": "content", children: children === void 0 ? /* @__PURE__ */ jsxs10(Fragment7, { children: [
3231
+ /* @__PURE__ */ jsx13(KeepItemCardTitle, {}),
2682
3232
  context.meta,
2683
- /* @__PURE__ */ jsx9(KeepItemCardTags, {}),
3233
+ /* @__PURE__ */ jsx13(KeepItemCardTags, {}),
2684
3234
  context.error
2685
3235
  ] }) : context.renderText(children) });
2686
3236
  }
@@ -2688,13 +3238,13 @@ function KeepItemCardTitle({ as = "h3", children, ...props }) {
2688
3238
  const context = useKeepItemCardCompound("Title");
2689
3239
  return createElement2(
2690
3240
  as,
2691
- { ...props, "data-keep-card-part": "title" },
3241
+ { ...props, "data-keep-card-part": "title", "data-line-clamp": "2" },
2692
3242
  context.renderTitle(context.renderText(children ?? context.resolvedTitle))
2693
3243
  );
2694
3244
  }
2695
3245
  function KeepMediaPlaceholderIcon() {
2696
- return /* @__PURE__ */ jsxs5("svg", { "data-media-fallback-icon": "true", viewBox: "0 0 24 24", "aria-hidden": "true", children: [
2697
- /* @__PURE__ */ jsx9(
3246
+ return /* @__PURE__ */ jsxs10("svg", { "data-media-fallback-icon": "true", viewBox: "0 0 24 24", "aria-hidden": "true", children: [
3247
+ /* @__PURE__ */ jsx13(
2698
3248
  "path",
2699
3249
  {
2700
3250
  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",
@@ -2702,19 +3252,19 @@ function KeepMediaPlaceholderIcon() {
2702
3252
  stroke: "currentColor"
2703
3253
  }
2704
3254
  ),
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" })
3255
+ /* @__PURE__ */ jsx13("circle", { cx: "9", cy: "9", r: "1.5", fill: "currentColor" }),
3256
+ /* @__PURE__ */ jsx13("path", { d: "m5.5 18 4.5-4.5 3 3 2-2L19 18", fill: "none", stroke: "currentColor" })
2707
3257
  ] });
2708
3258
  }
2709
3259
  function KeepItemCardTags({ children, ...props }) {
2710
3260
  const context = useKeepItemCardCompound("Tags");
2711
3261
  if (children === void 0 && context.renderedTags) return context.renderedTags;
2712
3262
  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)) });
3263
+ return /* @__PURE__ */ jsx13("ul", { ...props, "aria-label": props["aria-label"] ?? context.tagsLabel, "data-keep-card-part": "tags", children: children ?? context.tags.map((tag) => /* @__PURE__ */ jsx13("li", { children: tag }, tag)) });
2714
3264
  }
2715
3265
  function KeepItemCardActions({ children, ...props }) {
2716
3266
  const context = useKeepItemCardCompound("Actions");
2717
- return /* @__PURE__ */ jsx9("div", { ...props, "data-keep-card-part": "actions", children: children ?? context.actions });
3267
+ return /* @__PURE__ */ jsx13("div", { ...props, "data-keep-card-part": "actions", children: children ?? context.actions });
2718
3268
  }
2719
3269
  var KeepItemCard = Object.assign(KeepItemCardRoot, {
2720
3270
  Media: KeepItemCardMedia,
@@ -2722,405 +3272,256 @@ var KeepItemCard = Object.assign(KeepItemCardRoot, {
2722
3272
  Title: KeepItemCardTitle,
2723
3273
  Tags: KeepItemCardTags,
2724
3274
  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
- ] });
2734
- }
2735
- function formatSavedAt(timestamp) {
2736
- return new Date(timestamp).toISOString().slice(0, 10);
2737
- }
2738
- function getErrorMessage2(error, fallback) {
2739
- return error instanceof Error ? error.message : fallback;
2740
- }
2741
-
2742
- // src/KeepList.tsx
2743
- import { Fragment as Fragment5, jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
2744
- function KeepList(props) {
2745
- const { fallback, onBoundaryError, boundaryResetKey, ...listProps } = props;
2746
- const content = /* @__PURE__ */ jsx10(KeepListContent, { ...listProps });
2747
- if (fallback === void 0 && onBoundaryError === void 0) return content;
2748
- return /* @__PURE__ */ jsx10(KeepErrorBoundary, { fallback, onError: onBoundaryError, resetKey: boundaryResetKey, children: content });
2749
- }
2750
- function KeepListContent({
2751
- query,
2752
- children,
2753
- renderItem,
2754
- loading,
2755
- renderLoading,
2756
- loadingCount = 6,
2757
- empty,
2758
- error: errorContent,
2759
- itemCardProps,
2760
- layout = "list",
2761
- asChild = false,
2762
- onKeyDown,
2763
- onFocusCapture,
2764
- className,
2765
- ...rootProps
2766
- }) {
2767
- const view = useKeepListView(query);
2768
- const roving = useRovingTabIndex();
2769
- const { state } = view;
2770
- const body = getListBody(state, {
2771
- children,
2772
- renderItem,
2773
- loading: renderLoading !== void 0 ? renderLoading : loading,
2774
- loadingCount,
2775
- loadingLabel: view.labels.loading,
2776
- empty: empty ?? view.labels.empty,
2777
- error: errorContent ?? view.labels.error,
2778
- itemCardProps,
2779
- layout
2780
- });
2781
- return renderRoot(
2782
- asChild,
2783
- asChild && isValidElement3(children) ? children : void 0,
2784
- {
2785
- ...rootProps,
2786
- className,
2787
- "data-keepkit": "list",
2788
- "data-layout": layout,
2789
- "aria-busy": state.isLoading || rootProps["aria-busy"],
2790
- "data-state": getListState(state),
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
- }
2803
- },
2804
- /* @__PURE__ */ jsx10(KeepSearchQueryProvider, { query: query?.search?.query, children: body }),
2805
- "KeepList"
2806
- );
2807
- }
2808
- function getListState(state) {
2809
- if (state.error && state.items.length === 0) return "error";
2810
- if (state.isLoading && !state.isHydrated) return "loading";
2811
- if (state.isHydrated && state.items.length === 0) return "empty";
2812
- return "ready";
2813
- }
2814
- function getListBody(state, options) {
2815
- if (state.error && state.items.length === 0) return resolveContent(options.error, 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
- }
2827
- if (state.isHydrated && state.items.length === 0) return resolveContent(options.empty, state);
2828
- if (typeof options.children === "function") return options.children(state);
2829
- if (options.children !== void 0 && !isValidElement3(options.children)) return options.children;
2830
- return /* @__PURE__ */ jsx10("ul", { "data-layout": options.layout, children: state.items.map(
2831
- (item) => options.renderItem ? options.renderItem(item, state) : /* @__PURE__ */ jsx10("li", { children: /* @__PURE__ */ jsx10(KeepItemCard, { item, ...options.itemCardProps }) }, item.id)
2832
- ) });
3275
+ });
3276
+ function KeepItemCardSkeleton({ layout = "list", ...props }) {
3277
+ return /* @__PURE__ */ jsxs10("article", { ...props, "aria-hidden": "true", "data-keepkit": "card-skeleton", "data-layout": layout, "data-state": "loading", children: [
3278
+ /* @__PURE__ */ jsx13("span", { "data-skeleton-part": "media" }),
3279
+ /* @__PURE__ */ jsx13("span", { "data-skeleton-part": "title" }),
3280
+ /* @__PURE__ */ jsx13("span", { "data-skeleton-part": "meta" }),
3281
+ /* @__PURE__ */ jsx13("span", { "data-skeleton-part": "tag" }),
3282
+ /* @__PURE__ */ jsx13("span", { "data-skeleton-part": "tag" })
3283
+ ] });
2833
3284
  }
2834
-
2835
- // src/KeepTagFilter.tsx
2836
- import { isValidElement as isValidElement4 } from "react";
2837
-
2838
- // src/hooks/useKeepTagFilter.ts
2839
- import { useKeepList as useKeepList4 } from "@keepkit/core/react";
2840
- import { useCallback as useCallback4, useMemo as useMemo3, useState as useState6 } from "react";
2841
- function useKeepTagFilter(options) {
2842
- const { query, controlledValue, defaultValue, onChange, onValueChange } = options;
2843
- const [uncontrolledValue, setUncontrolledValue] = useState6(defaultValue);
2844
- const resolvedValue = controlledValue ?? uncontrolledValue;
2845
- const list = useKeepList4({
2846
- ...query,
2847
- tags: resolvedValue ? [...query?.tags ?? [], resolvedValue] : query?.tags
2848
- });
2849
- const select = useCallback4(
2850
- (tag) => {
2851
- if (controlledValue === void 0) setUncontrolledValue(tag);
2852
- onChange?.(tag);
2853
- onValueChange?.(tag);
2854
- },
2855
- [controlledValue, onChange, onValueChange]
2856
- );
2857
- const state = useMemo3(
2858
- () => ({ tags: list.tags, tagCounts: list.tagCounts, value: resolvedValue, select }),
2859
- [list.tagCounts, list.tags, resolvedValue, select]
2860
- );
2861
- return {
2862
- state,
2863
- isLoading: list.isLoading,
2864
- labels: { all: useUiLabel("allTags"), aria: useUiLabel("filterTags") }
2865
- };
3285
+ function formatSavedAt(timestamp) {
3286
+ return new Date(timestamp).toISOString().slice(0, 10);
3287
+ }
3288
+ function getErrorMessage2(error, fallback) {
3289
+ return error instanceof Error ? error.message : fallback;
2866
3290
  }
2867
3291
 
2868
- // src/KeepTagFilter.tsx
2869
- import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
2870
- function KeepTagFilter({
2871
- query,
2872
- value: controlledValue,
2873
- defaultValue,
2874
- onChange,
2875
- onValueChange,
2876
- allLabel,
2877
- ariaLabel,
2878
- renderTag,
2879
- render,
3292
+ // src/features/status/KeepEmptyState.tsx
3293
+ import { isValidElement as isValidElement5 } from "react";
3294
+ import { Fragment as Fragment8, jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
3295
+ function KeepEmptyState({
3296
+ title,
3297
+ description,
3298
+ action,
2880
3299
  children,
3300
+ variant,
3301
+ onClearFilters,
2881
3302
  asChild = false,
2882
3303
  className,
2883
3304
  ...rootProps
2884
3305
  }) {
2885
- const view = useKeepTagFilter({ query, controlledValue, defaultValue, onChange, onValueChange });
2886
- const contentChildren = asChild && isValidElement4(children) ? void 0 : children;
2887
- const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? /* @__PURE__ */ jsxs7("fieldset", { children: [
2888
- /* @__PURE__ */ jsx11("legend", { children: ariaLabel ?? view.labels.aria }),
2889
- /* @__PURE__ */ jsx11(
2890
- "button",
2891
- {
2892
- type: "button",
2893
- "data-keep-action": "filter-all-tags",
2894
- "aria-pressed": view.state.value === void 0,
2895
- onClick: () => view.state.select(),
2896
- children: allLabel ?? view.labels.all
2897
- }
2898
- ),
2899
- view.state.tags.map((tag) => /* @__PURE__ */ jsxs7(
2900
- "button",
2901
- {
2902
- type: "button",
2903
- "data-keep-action": "filter-tag",
2904
- "aria-pressed": view.state.value === tag,
2905
- onClick: () => view.state.select(tag),
2906
- children: [
2907
- renderTag ? renderTag(tag, view.state.tagCounts[tag] ?? 0, view.state.value === tag) : tag,
2908
- /* @__PURE__ */ jsxs7("span", { children: [
2909
- " (",
2910
- view.state.tagCounts[tag] ?? 0,
2911
- ")"
2912
- ] })
2913
- ]
2914
- },
2915
- tag
2916
- ))
3306
+ const resolvedVariant = variant ?? "empty-storage";
3307
+ const defaultTitle = useUiLabel(resolvedVariant === "empty-filtered" ? "noFilteredItems" : "noItems");
3308
+ const defaultDescription = useUiLabel(
3309
+ resolvedVariant === "empty-filtered" ? "noFilteredItemsDescription" : "emptyStorageDescription"
3310
+ );
3311
+ const clearLabel = useUiLabel("clearFilters");
3312
+ const contentChildren = asChild && isValidElement5(children) ? void 0 : children;
3313
+ const body = contentChildren ?? /* @__PURE__ */ jsxs11(Fragment8, { children: [
3314
+ /* @__PURE__ */ jsx14("h2", { children: title ?? defaultTitle }),
3315
+ description ?? /* @__PURE__ */ jsx14("p", { children: defaultDescription }),
3316
+ action ?? (resolvedVariant === "empty-filtered" && onClearFilters ? /* @__PURE__ */ jsx14("button", { type: "button", "data-keep-action": "clear-filters", onClick: onClearFilters, children: clearLabel }) : null)
2917
3317
  ] });
2918
3318
  return renderRoot(
2919
3319
  asChild,
2920
- isValidElement4(children) ? children : void 0,
2921
- {
2922
- ...rootProps,
2923
- className,
2924
- "data-keepkit": "tag-filter",
2925
- "data-state": view.state.value === void 0 ? "all" : "filtered",
2926
- "data-loading": view.isLoading ? "true" : void 0
2927
- },
3320
+ children,
3321
+ { ...rootProps, className, "data-keepkit": "empty-state", "data-state": variant ?? "empty" },
2928
3322
  body,
2929
- "KeepTagFilter"
3323
+ "KeepEmptyState"
2930
3324
  );
2931
3325
  }
2932
3326
 
2933
- // src/hooks/useQueryControls.ts
2934
- import { useEffect as useEffect5, useState as useState7 } from "react";
2935
- function useKeepSearchInput(options) {
2936
- const { controlledValue, defaultValue, debounceMs, onValueChange } = options;
2937
- const [uncontrolledValue, setUncontrolledValue] = useState7(defaultValue);
2938
- const value = controlledValue ?? uncontrolledValue;
2939
- useEffect5(() => {
2940
- if (!onValueChange) return;
2941
- if (debounceMs <= 0) {
2942
- onValueChange(value);
2943
- return;
2944
- }
2945
- const timer = window.setTimeout(() => onValueChange(value), debounceMs);
2946
- return () => window.clearTimeout(timer);
2947
- }, [debounceMs, onValueChange, value]);
2948
- return {
2949
- value,
2950
- label: useUiLabel("search"),
2951
- change: (event) => {
2952
- if (controlledValue === void 0) setUncontrolledValue(event.currentTarget.value);
2953
- }
2954
- };
2955
- }
2956
- function useKeepSortSelect(options) {
2957
- const { controlledValue, defaultValue, onValueChange } = options;
2958
- const [uncontrolledValue, setUncontrolledValue] = useState7(defaultValue);
2959
- const value = controlledValue ?? uncontrolledValue;
2960
- return {
2961
- value,
2962
- change: (event) => {
2963
- const nextValue = event.currentTarget.value;
2964
- if (controlledValue === void 0) setUncontrolledValue(nextValue);
2965
- const [by, direction] = nextValue.split(":");
2966
- onValueChange?.(nextValue, { by, direction });
2967
- },
2968
- labels: {
2969
- sort: useUiLabel("sort"),
2970
- updatedNewest: useUiLabel("updatedNewest"),
2971
- updatedOldest: useUiLabel("updatedOldest"),
2972
- savedNewest: useUiLabel("savedNewest"),
2973
- savedOldest: useUiLabel("savedOldest")
2974
- }
2975
- };
2976
- }
2977
- function useKeepPagination(options) {
2978
- const { totalCount, pageSize, page, maxPageButtons, onPageChange } = options;
2979
- const pageCount = Math.max(1, Math.ceil(totalCount / Math.max(1, pageSize)));
2980
- const currentPage = Math.min(Math.max(1, page), pageCount);
2981
- const goToPage = (nextPage) => {
2982
- const next = Math.min(Math.max(1, nextPage), pageCount);
2983
- onPageChange?.(next, (next - 1) * pageSize);
2984
- };
3327
+ // src/features/collection/hooks/useKeepListView.ts
3328
+ import { useKeepList as useKeepList4 } from "@keepkit/core/react";
3329
+ function useKeepListView(query) {
2985
3330
  return {
2986
- pageCount,
2987
- currentPage,
2988
- goToPage,
2989
- visiblePages: getVisiblePages(currentPage, pageCount, Math.max(1, maxPageButtons)),
3331
+ state: useKeepList4(query),
3332
+ allState: useKeepList4({}),
2990
3333
  labels: {
2991
- previous: useUiLabel("previousPage"),
2992
- next: useUiLabel("nextPage"),
2993
- page: useUiLabel("page"),
2994
- pagination: useUiLabel("pagination")
3334
+ loading: useUiLabel("loadingItems"),
3335
+ empty: useUiLabel("noItems"),
3336
+ error: useUiLabel("errorItems")
2995
3337
  }
2996
3338
  };
2997
3339
  }
2998
- function getVisiblePages(currentPage, pageCount, maxPageButtons) {
2999
- if (pageCount <= maxPageButtons) return Array.from({ length: pageCount }, (_, index) => index + 1);
3000
- const half = Math.floor(maxPageButtons / 2);
3001
- const start = Math.min(Math.max(1, currentPage - half), pageCount - maxPageButtons + 1);
3002
- return Array.from({ length: maxPageButtons }, (_, index) => start + index);
3003
- }
3004
3340
 
3005
- // src/query-controls.tsx
3006
- import { Fragment as Fragment6, jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
3007
- function KeepSearchInput({
3008
- value: controlledValue,
3009
- defaultValue = "",
3010
- debounceMs = 300,
3011
- onValueChange,
3012
- "aria-label": ariaLabel,
3013
- placeholder,
3014
- ...props
3015
- }) {
3016
- const view = useKeepSearchInput({ controlledValue, defaultValue, debounceMs, onValueChange });
3017
- return /* @__PURE__ */ jsx12(
3018
- "input",
3019
- {
3020
- ...props,
3021
- "data-keepkit": "search-input",
3022
- "data-keep-action": "search",
3023
- type: "search",
3024
- value: view.value,
3025
- "data-state": view.value ? "active" : "idle",
3026
- "data-disabled": props.disabled ? "true" : void 0,
3027
- "aria-label": ariaLabel ?? view.label,
3028
- placeholder: placeholder ?? view.label,
3029
- onChange: view.change
3030
- }
3341
+ // src/features/collection/hooks/useRovingTabIndex.ts
3342
+ import { useCallback as useCallback3, useEffect as useEffect6, useRef as useRef4 } from "react";
3343
+ function useRovingTabIndex() {
3344
+ const ref = useRef4(null);
3345
+ const getItems = useCallback3(() => {
3346
+ const root = ref.current;
3347
+ if (!root) return [];
3348
+ return Array.from(root.querySelectorAll('[data-keepkit="card"]')).filter(
3349
+ (item) => item.getAttribute("aria-hidden") !== "true" && item.getAttribute("data-roving-disabled") !== "true"
3350
+ );
3351
+ }, []);
3352
+ const syncTabIndices = useCallback3(() => {
3353
+ const items = getItems();
3354
+ if (items.length === 0) return;
3355
+ const activeElement = document.activeElement;
3356
+ const activeItem = items.find((item) => item === activeElement || item.contains(activeElement));
3357
+ const activeIndex = activeItem ? items.indexOf(activeItem) : 0;
3358
+ items.forEach((item, index) => {
3359
+ item.tabIndex = index === activeIndex ? 0 : -1;
3360
+ });
3361
+ }, [getItems]);
3362
+ useEffect6(() => {
3363
+ const root = ref.current;
3364
+ if (!root) return;
3365
+ syncTabIndices();
3366
+ const observer = new MutationObserver(syncTabIndices);
3367
+ observer.observe(root, { childList: true, subtree: true });
3368
+ return () => observer.disconnect();
3369
+ }, [syncTabIndices]);
3370
+ const onFocusCapture = useCallback3(
3371
+ (event) => {
3372
+ if (!(event.target instanceof HTMLElement)) return;
3373
+ const item = event.target.closest('[data-keepkit="card"]');
3374
+ if (!item || !ref.current?.contains(item)) return;
3375
+ getItems().forEach((candidate) => {
3376
+ candidate.tabIndex = candidate === item ? 0 : -1;
3377
+ });
3378
+ },
3379
+ [getItems]
3031
3380
  );
3032
- }
3033
- function KeepSortSelect({
3034
- value: controlledValue,
3035
- defaultValue = "updatedAt:desc",
3036
- onValueChange,
3037
- "aria-label": ariaLabel,
3038
- children,
3039
- ...props
3040
- }) {
3041
- const view = useKeepSortSelect({ controlledValue, defaultValue, onValueChange });
3042
- const options = children ?? /* @__PURE__ */ jsxs8(Fragment6, { children: [
3043
- /* @__PURE__ */ jsx12("option", { value: "updatedAt:desc", children: view.labels.updatedNewest }),
3044
- /* @__PURE__ */ jsx12("option", { value: "updatedAt:asc", children: view.labels.updatedOldest }),
3045
- /* @__PURE__ */ jsx12("option", { value: "savedAt:desc", children: view.labels.savedNewest }),
3046
- /* @__PURE__ */ jsx12("option", { value: "savedAt:asc", children: view.labels.savedOldest })
3047
- ] });
3048
- return /* @__PURE__ */ jsx12(
3049
- "select",
3050
- {
3051
- ...props,
3052
- "data-keepkit": "sort-select",
3053
- "data-keep-action": "sort",
3054
- value: view.value,
3055
- "data-state": "selected",
3056
- "data-disabled": props.disabled ? "true" : void 0,
3057
- "aria-label": ariaLabel ?? view.labels.sort,
3058
- onChange: view.change,
3059
- children: options
3060
- }
3381
+ const onKeyDown = useCallback3(
3382
+ (event) => {
3383
+ if (event.defaultPrevented) return;
3384
+ const items = getItems();
3385
+ if (!(event.target instanceof HTMLElement)) return;
3386
+ const current = event.target.closest('[data-keepkit="card"]');
3387
+ if (!current || current !== event.target || !ref.current?.contains(current)) return;
3388
+ const currentIndex = items.indexOf(current);
3389
+ if (currentIndex < 0) return;
3390
+ let nextIndex;
3391
+ if (event.key === "Home") nextIndex = 0;
3392
+ if (event.key === "End") nextIndex = items.length - 1;
3393
+ if (event.key === "ArrowRight" || event.key === "ArrowDown")
3394
+ nextIndex = Math.min(currentIndex + 1, items.length - 1);
3395
+ if (event.key === "ArrowLeft" || event.key === "ArrowUp") nextIndex = Math.max(currentIndex - 1, 0);
3396
+ if (nextIndex === void 0) return;
3397
+ event.preventDefault();
3398
+ if (nextIndex === currentIndex) return;
3399
+ items[nextIndex]?.focus();
3400
+ },
3401
+ [getItems]
3061
3402
  );
3403
+ return { ref, onKeyDown, onFocusCapture };
3062
3404
  }
3063
- function KeepPagination({
3064
- totalCount,
3065
- pageSize,
3066
- page = 1,
3067
- maxPageButtons = 7,
3068
- onPageChange,
3069
- render,
3070
- ...props
3405
+
3406
+ // src/features/collection/KeepList.tsx
3407
+ import { Fragment as Fragment9, jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
3408
+ function KeepList(props) {
3409
+ const { fallback, onBoundaryError, boundaryResetKey, ...listProps } = props;
3410
+ const content = /* @__PURE__ */ jsx15(KeepListContent, { ...listProps });
3411
+ if (fallback === void 0 && onBoundaryError === void 0) return content;
3412
+ return /* @__PURE__ */ jsx15(KeepErrorBoundary, { fallback, onError: onBoundaryError, resetKey: boundaryResetKey, children: content });
3413
+ }
3414
+ function KeepListContent({
3415
+ query,
3416
+ children,
3417
+ renderItem,
3418
+ loading,
3419
+ renderLoading,
3420
+ loadingCount = 6,
3421
+ empty,
3422
+ error: errorContent,
3423
+ onClearFilters,
3424
+ itemCardProps,
3425
+ layout = "list",
3426
+ asChild = false,
3427
+ onKeyDown,
3428
+ onFocusCapture,
3429
+ className,
3430
+ ...rootProps
3071
3431
  }) {
3072
- const view = useKeepPagination({ totalCount, pageSize, page, maxPageButtons, onPageChange });
3073
- const navProps = {
3074
- ...props,
3075
- "data-keepkit": "pagination",
3076
- "aria-label": props["aria-label"] ?? view.labels.pagination,
3077
- "data-state": view.pageCount > 1 ? "active" : "idle"
3078
- };
3079
- if (render)
3080
- return /* @__PURE__ */ jsx12("nav", { ...navProps, children: render({ page: view.currentPage, pageCount: view.pageCount, goToPage: view.goToPage }) });
3081
- return /* @__PURE__ */ jsxs8("nav", { ...navProps, children: [
3082
- /* @__PURE__ */ jsx12(
3083
- "button",
3084
- {
3085
- type: "button",
3086
- "data-keep-action": "previous-page",
3087
- onClick: () => view.goToPage(view.currentPage - 1),
3088
- disabled: view.currentPage <= 1,
3089
- children: view.labels.previous
3090
- }
3091
- ),
3092
- view.visiblePages.map((nextPage) => /* @__PURE__ */ jsx12(
3093
- "button",
3094
- {
3095
- type: "button",
3096
- "data-keep-action": "select-page",
3097
- "aria-current": nextPage === view.currentPage ? "page" : void 0,
3098
- "aria-label": `${view.labels.page} ${nextPage}`,
3099
- onClick: () => view.goToPage(nextPage),
3100
- children: nextPage
3432
+ const view = useKeepListView(query);
3433
+ const roving = useRovingTabIndex();
3434
+ const { state } = view;
3435
+ const body = getListBody(state, {
3436
+ children,
3437
+ renderItem,
3438
+ loading: renderLoading !== void 0 ? renderLoading : loading,
3439
+ loadingCount,
3440
+ loadingLabel: view.labels.loading,
3441
+ empty,
3442
+ error: errorContent ?? view.labels.error,
3443
+ onClearFilters,
3444
+ allItemCount: view.allState.totalCount,
3445
+ query,
3446
+ itemCardProps,
3447
+ layout
3448
+ });
3449
+ return renderRoot(
3450
+ asChild,
3451
+ asChild && isValidElement6(children) ? children : void 0,
3452
+ {
3453
+ ...rootProps,
3454
+ className,
3455
+ "data-keepkit": "list",
3456
+ "data-layout": layout,
3457
+ "aria-busy": state.isLoading || rootProps["aria-busy"],
3458
+ "data-state": getListState(state),
3459
+ "data-loading": state.isLoading ? "true" : void 0,
3460
+ "data-roving-tabindex": "true",
3461
+ role: rootProps.role ?? "group",
3462
+ ref: roving.ref,
3463
+ onKeyDown: (event) => {
3464
+ onKeyDown?.(event);
3465
+ if (!event.defaultPrevented) roving.onKeyDown(event);
3101
3466
  },
3102
- nextPage
3103
- )),
3104
- /* @__PURE__ */ jsx12(
3105
- "button",
3467
+ onFocusCapture: (event) => {
3468
+ onFocusCapture?.(event);
3469
+ if (!event.defaultPrevented) roving.onFocusCapture(event);
3470
+ }
3471
+ },
3472
+ /* @__PURE__ */ jsx15(KeepSearchQueryProvider, { query: query?.search?.query, children: body }),
3473
+ "KeepList"
3474
+ );
3475
+ }
3476
+ function getListState(state) {
3477
+ if (state.error && state.items.length === 0) return "error";
3478
+ if (state.isLoading && !state.isHydrated) return "loading";
3479
+ if (state.isHydrated && state.items.length === 0) return "empty";
3480
+ return "ready";
3481
+ }
3482
+ function getListBody(state, options) {
3483
+ if (state.error && state.items.length === 0) return resolveContent(options.error, state);
3484
+ if (state.isLoading && !state.isHydrated) {
3485
+ if (options.loading !== void 0) return resolveContent(options.loading, state);
3486
+ const count = Number.isFinite(options.loadingCount) ? Math.max(0, Math.floor(options.loadingCount)) : 6;
3487
+ return /* @__PURE__ */ jsxs12(Fragment9, { children: [
3488
+ /* @__PURE__ */ jsx15("span", { role: "status", "data-keepkit": "loading-label", children: options.loadingLabel }),
3489
+ /* @__PURE__ */ jsx15("ul", { "data-keepkit": "skeleton-list", "data-layout": options.layout, children: Array.from({ length: count }, (_, index) => (
3490
+ // biome-ignore lint/suspicious/noArrayIndexKey: Static loading placeholders never reorder.
3491
+ /* @__PURE__ */ jsx15("li", { children: /* @__PURE__ */ jsx15(KeepItemCardSkeleton, { layout: options.layout }) }, index)
3492
+ )) })
3493
+ ] });
3494
+ }
3495
+ if (state.isHydrated && state.items.length === 0) {
3496
+ if (options.empty !== void 0) return resolveContent(options.empty, state);
3497
+ return /* @__PURE__ */ jsx15(
3498
+ KeepEmptyState,
3106
3499
  {
3107
- type: "button",
3108
- "data-keep-action": "next-page",
3109
- onClick: () => view.goToPage(view.currentPage + 1),
3110
- disabled: view.currentPage >= view.pageCount,
3111
- children: view.labels.next
3500
+ variant: options.allItemCount > 0 && hasActiveQueryFilters(options.query) ? "empty-filtered" : "empty-storage",
3501
+ onClearFilters: options.onClearFilters
3112
3502
  }
3113
- )
3114
- ] });
3503
+ );
3504
+ }
3505
+ if (typeof options.children === "function") return options.children(state);
3506
+ if (options.children !== void 0 && !isValidElement6(options.children)) return options.children;
3507
+ return /* @__PURE__ */ jsx15("ul", { "data-layout": options.layout, children: state.items.map(
3508
+ (item) => options.renderItem ? options.renderItem(item, state) : /* @__PURE__ */ jsx15("li", { children: /* @__PURE__ */ jsx15(KeepItemCard, { item, ...options.itemCardProps }) }, item.id)
3509
+ ) });
3510
+ }
3511
+ function hasActiveQueryFilters(query) {
3512
+ if (!query) return false;
3513
+ return Boolean(
3514
+ query.search?.query?.trim() || query.tags?.some((tag) => tag.trim()) || query.targetType || query.savedBetween || query.filter
3515
+ );
3115
3516
  }
3116
3517
 
3117
- // src/KeepCollection.tsx
3118
- import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
3518
+ // src/features/collection/KeepCollection.tsx
3519
+ import { jsx as jsx16, jsxs as jsxs13 } from "react/jsx-runtime";
3119
3520
  function KeepCollection(props) {
3120
3521
  const { fallback, onBoundaryError, boundaryResetKey, ...collectionProps } = props;
3121
- const content = /* @__PURE__ */ jsx13(KeepCollectionContent, { ...collectionProps });
3522
+ const content = /* @__PURE__ */ jsx16(KeepCollectionContent, { ...collectionProps });
3122
3523
  if (fallback === void 0 && onBoundaryError === void 0) return content;
3123
- return /* @__PURE__ */ jsx13(KeepErrorBoundary2, { fallback, onError: onBoundaryError, resetKey: boundaryResetKey, children: content });
3524
+ return /* @__PURE__ */ jsx16(KeepErrorBoundary2, { fallback, onError: onBoundaryError, resetKey: boundaryResetKey, children: content });
3124
3525
  }
3125
3526
  function KeepCollectionContent({
3126
3527
  query = {},
@@ -3134,13 +3535,14 @@ function KeepCollectionContent({
3134
3535
  loading,
3135
3536
  renderLoading,
3136
3537
  loadingCount,
3538
+ activeFilters,
3137
3539
  empty,
3138
3540
  error,
3139
3541
  className,
3140
3542
  ...rootProps
3141
3543
  }) {
3142
3544
  const view = useKeepCollection({ query, pageSize, urlSync, urlAdapter, features });
3143
- return /* @__PURE__ */ jsxs9(
3545
+ return /* @__PURE__ */ jsxs13(
3144
3546
  "section",
3145
3547
  {
3146
3548
  ...rootProps,
@@ -3151,12 +3553,22 @@ function KeepCollectionContent({
3151
3553
  "data-state": getCollectionState(view.list),
3152
3554
  "data-loading": view.list.isLoading || view.list.isMutating ? "true" : void 0,
3153
3555
  children: [
3154
- /* @__PURE__ */ jsxs9("div", { children: [
3155
- view.enabled.search ? /* @__PURE__ */ jsx13(KeepSearchInput, { value: view.searchValue, onValueChange: view.setSearchValue }) : null,
3156
- view.enabled.sort ? /* @__PURE__ */ jsx13(KeepSortSelect, { value: view.sortValue, onValueChange: view.setSortValue }) : null,
3157
- view.enabled.tagFilter ? /* @__PURE__ */ jsx13(KeepTagFilter, { query, value: view.tag, onValueChange: view.setTag }) : null
3556
+ /* @__PURE__ */ jsxs13("div", { children: [
3557
+ view.enabled.search ? /* @__PURE__ */ jsx16(KeepSearchInput, { value: view.searchValue, onValueChange: view.setSearchValue }) : null,
3558
+ view.enabled.sort ? /* @__PURE__ */ jsx16(KeepSortSelect, { value: view.sortValue, onValueChange: view.setSortValue }) : null,
3559
+ view.enabled.tagFilter ? /* @__PURE__ */ jsx16(KeepTagFilter, { query, value: view.activeTags[0], onValueChange: view.setTag }) : null
3158
3560
  ] }),
3159
- /* @__PURE__ */ jsx13(
3561
+ activeFilters === void 0 ? /* @__PURE__ */ jsx16(
3562
+ KeepActiveFiltersSummary,
3563
+ {
3564
+ search: view.searchValue,
3565
+ tags: view.activeTags,
3566
+ onSearchChange: view.setSearchValue,
3567
+ onTagChange: view.removeTag,
3568
+ onClear: view.clearFilters
3569
+ }
3570
+ ) : resolveContent(activeFilters, view.list),
3571
+ /* @__PURE__ */ jsx16(
3160
3572
  KeepList,
3161
3573
  {
3162
3574
  query: view.resolvedQuery,
@@ -3166,198 +3578,64 @@ function KeepCollectionContent({
3166
3578
  loading,
3167
3579
  renderLoading,
3168
3580
  loadingCount,
3169
- empty,
3581
+ onClearFilters: view.clearFilters,
3582
+ empty: view.list.totalCount === 0 && view.allState.totalCount > 0 ? void 0 : empty,
3170
3583
  error
3171
3584
  }
3172
3585
  ),
3173
- view.enabled.pagination ? /* @__PURE__ */ jsx13(
3586
+ view.enabled.pagination ? /* @__PURE__ */ jsx16(
3174
3587
  KeepPagination,
3175
3588
  {
3176
3589
  totalCount: view.list.totalCount,
3177
3590
  pageSize: view.resolvedPageSize,
3178
3591
  page: view.list.page,
3179
3592
  onPageChange: view.setPage
3180
- }
3181
- ) : null,
3182
- view.enabled.bulkActions ? /* @__PURE__ */ jsx13(KeepBulkActions, { query: view.resolvedQuery }) : null
3183
- ]
3184
- }
3185
- );
3186
- }
3187
- function getCollectionState(list) {
3188
- if (list.error && list.items.length === 0) return "error";
3189
- if (list.isLoading && !list.isHydrated) return "loading";
3190
- if (list.isHydrated && list.items.length === 0) return "empty";
3191
- return "ready";
3192
- }
3193
-
3194
- // src/KeepLayout.tsx
3195
- import { jsx as jsx14 } from "react/jsx-runtime";
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
- );
3221
- }
3222
-
3223
- // src/KeepNoteEditor.tsx
3224
- import { isValidElement as isValidElement5 } from "react";
3225
-
3226
- // src/hooks/useKeepNoteEditor.ts
3227
- import { useKeepItem as useKeepItem3 } from "@keepkit/core/react";
3228
- import { useCallback as useCallback5, useEffect as useEffect6, useRef as useRef5, useState as useState8 } from "react";
3229
- function useKeepNoteEditor({ item, debounceMs, onSaved, onSaveError }) {
3230
- const itemState = useKeepItem3(item);
3231
- const { error, isMutating, item: savedItem, updateNote } = itemState;
3232
- const [note, setNote] = useState8(item.note ?? "");
3233
- const baselineNote = savedItem?.note ?? item.note ?? "";
3234
- const isDirty = note !== baselineNote;
3235
- const lastSavedNoteRef = useRef5(void 0);
3236
- useEffect6(() => setNote(baselineNote), [baselineNote]);
3237
- const save = useCallback5(async () => {
3238
- const nextNote = note.trim() || void 0;
3239
- try {
3240
- await updateNote(nextNote);
3241
- lastSavedNoteRef.current = note;
3242
- onSaved?.(nextNote);
3243
- } catch (cause) {
3244
- onSaveError?.(cause);
3245
- throw cause;
3246
- }
3247
- }, [note, onSaveError, onSaved, updateNote]);
3248
- useEffect6(() => {
3249
- if (!isDirty || debounceMs <= 0 || lastSavedNoteRef.current === note) return;
3250
- const timer = window.setTimeout(() => void save().catch(() => void 0), debounceMs);
3251
- return () => window.clearTimeout(timer);
3252
- }, [debounceMs, isDirty, note, save]);
3253
- const state = {
3254
- item,
3255
- note,
3256
- setNote,
3257
- isDirty,
3258
- isSaving: isMutating,
3259
- error,
3260
- save
3261
- };
3262
- const submit = (event) => {
3263
- event.preventDefault();
3264
- void save().catch(() => void 0);
3265
- };
3266
- return {
3267
- state,
3268
- submit,
3269
- handleKeyDown: (event) => {
3270
- if (event.key !== "Enter" || !event.ctrlKey && !event.metaKey) return;
3271
- event.preventDefault();
3272
- void save().catch(() => void 0);
3273
- },
3274
- labels: {
3275
- note: useUiLabel("note"),
3276
- save: useUiLabel("saveNote"),
3277
- error: useUiLabel("error")
3278
- }
3279
- };
3280
- }
3281
-
3282
- // src/KeepNoteEditor.tsx
3283
- import { Fragment as Fragment7, jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
3284
- function KeepNoteEditor({
3285
- item,
3286
- label,
3287
- saveLabel,
3288
- placeholder,
3289
- debounceMs = 300,
3290
- onSaved,
3291
- onSaveError,
3292
- render,
3293
- children,
3294
- asChild = false,
3295
- className,
3296
- ...formProps
3297
- }) {
3298
- const view = useKeepNoteEditor({ item, debounceMs, onSaved, onSaveError });
3299
- const { error, isDirty, isSaving, note, setNote } = view.state;
3300
- const contentChildren = asChild && isValidElement5(children) ? void 0 : children;
3301
- const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? /* @__PURE__ */ jsxs10(Fragment7, { children: [
3302
- /* @__PURE__ */ jsxs10("label", { children: [
3303
- label ?? view.labels.note,
3304
- /* @__PURE__ */ jsx15(
3305
- "textarea",
3306
- {
3307
- "data-keep-action": "edit-note",
3308
- value: note,
3309
- onChange: (event) => setNote(event.currentTarget.value),
3310
- placeholder,
3311
- disabled: isSaving,
3312
- onKeyDown: view.handleKeyDown
3313
- }
3314
- )
3315
- ] }),
3316
- /* @__PURE__ */ jsx15("button", { type: "submit", "data-keep-action": "save-note", disabled: isSaving, "aria-busy": isSaving, children: saveLabel ?? view.labels.save })
3317
- ] });
3318
- if (!asChild) {
3319
- return /* @__PURE__ */ jsxs10(
3320
- "form",
3593
+ }
3594
+ ) : null,
3595
+ view.enabled.bulkActions ? /* @__PURE__ */ jsx16(KeepBulkActions, { query: view.resolvedQuery }) : null
3596
+ ]
3597
+ }
3598
+ );
3599
+ }
3600
+ function getCollectionState(list) {
3601
+ if (list.error && list.items.length === 0) return "error";
3602
+ if (list.isLoading && !list.isHydrated) return "loading";
3603
+ if (list.isHydrated && list.items.length === 0) return "empty";
3604
+ return "ready";
3605
+ }
3606
+
3607
+ // src/features/collection/KeepLayout.tsx
3608
+ import { jsx as jsx17 } from "react/jsx-runtime";
3609
+ function KeepLayout({ layout = "list", children, onKeyDown, onFocusCapture, ...props }) {
3610
+ const roving = useRovingTabIndex();
3611
+ return (
3612
+ // biome-ignore lint/a11y/noStaticElementInteractions: The group manages keyboard focus for descendant cards.
3613
+ /* @__PURE__ */ jsx17(
3614
+ "div",
3321
3615
  {
3322
- ...formProps,
3323
- className,
3324
- "data-keepkit": "note-editor",
3325
- onSubmit: view.submit,
3326
- "aria-busy": isSaving || formProps["aria-busy"],
3327
- "data-state": error ? "error" : isDirty ? "dirty" : "clean",
3328
- "data-loading": isSaving ? "true" : void 0,
3329
- "data-disabled": isSaving ? "true" : void 0,
3330
- children: [
3331
- body,
3332
- error ? /* @__PURE__ */ jsx15("p", { role: "alert", children: getErrorMessage3(error, view.labels.error) }) : null
3333
- ]
3616
+ ...props,
3617
+ ref: roving.ref,
3618
+ "data-keepkit": "layout",
3619
+ "data-layout": layout,
3620
+ "data-roving-tabindex": "true",
3621
+ role: props.role ?? "group",
3622
+ onKeyDown: (event) => {
3623
+ onKeyDown?.(event);
3624
+ if (!event.defaultPrevented) roving.onKeyDown(event);
3625
+ },
3626
+ onFocusCapture: (event) => {
3627
+ onFocusCapture?.(event);
3628
+ if (!event.defaultPrevented) roving.onFocusCapture(event);
3629
+ },
3630
+ children
3334
3631
  }
3335
- );
3336
- }
3337
- return renderRoot(
3338
- true,
3339
- isValidElement5(children) ? children : void 0,
3340
- {
3341
- ...formProps,
3342
- className,
3343
- "data-keepkit": "note-editor",
3344
- onSubmit: view.submit,
3345
- "aria-busy": isSaving || formProps["aria-busy"],
3346
- "data-state": error ? "error" : isDirty ? "dirty" : "clean",
3347
- "data-loading": isSaving ? "true" : void 0,
3348
- "data-disabled": isSaving ? "true" : void 0
3349
- },
3350
- body,
3351
- "KeepNoteEditor"
3632
+ )
3352
3633
  );
3353
3634
  }
3354
- function getErrorMessage3(error, fallback) {
3355
- return error instanceof Error ? error.message : fallback;
3356
- }
3357
3635
 
3358
- // src/KeepReorderableList.tsx
3636
+ // src/features/collection/KeepReorderableList.tsx
3359
3637
  import { useState as useState9 } from "react";
3360
- import { jsx as jsx16 } from "react/jsx-runtime";
3638
+ import { jsx as jsx18 } from "react/jsx-runtime";
3361
3639
  function KeepReorderableList({
3362
3640
  items,
3363
3641
  onReorder,
@@ -3366,6 +3644,7 @@ function KeepReorderableList({
3366
3644
  ...props
3367
3645
  }) {
3368
3646
  const [draggedId, setDraggedId] = useState9(null);
3647
+ const [dropTargetIndex, setDropTargetIndex] = useState9(null);
3369
3648
  const ids = items.map((item) => item.id);
3370
3649
  function commit(nextIds) {
3371
3650
  void onReorder(nextIds);
@@ -3378,7 +3657,19 @@ function KeepReorderableList({
3378
3657
  next.splice(targetIndex, 0, id);
3379
3658
  commit(next);
3380
3659
  }
3381
- return /* @__PURE__ */ jsx16("ul", { ...props, "data-keepkit": "reorderable-list", children: items.map((item, index) => {
3660
+ function moveToInsertion(index, insertionIndex) {
3661
+ const next = [...ids];
3662
+ const [id] = next.splice(index, 1);
3663
+ if (id === void 0) return;
3664
+ const targetIndex = Math.min(
3665
+ Math.max(0, insertionIndex > index ? insertionIndex - 1 : insertionIndex),
3666
+ next.length
3667
+ );
3668
+ next.splice(targetIndex, 0, id);
3669
+ if (next[index] === id) return;
3670
+ commit(next);
3671
+ }
3672
+ return /* @__PURE__ */ jsx18("ul", { ...props, "data-keepkit": "reorderable-list", children: items.map((item, index) => {
3382
3673
  const moveUp = () => move(index, index - 1);
3383
3674
  const moveDown = () => move(index, index + 1);
3384
3675
  const onHandleKeyDown = (event) => {
@@ -3399,27 +3690,43 @@ function KeepReorderableList({
3399
3690
  role: "button",
3400
3691
  tabIndex: 0,
3401
3692
  draggable: true,
3693
+ "data-drag-handle": "true",
3402
3694
  "aria-label": itemLabel(item, index),
3403
3695
  "aria-grabbed": draggedId === item.id,
3404
- onDragStart: () => setDraggedId(item.id),
3405
- onDragEnd: () => setDraggedId(null),
3696
+ onDragStart: () => {
3697
+ setDraggedId(item.id);
3698
+ setDropTargetIndex(index);
3699
+ },
3700
+ onDragEnd: () => {
3701
+ setDraggedId(null);
3702
+ setDropTargetIndex(null);
3703
+ },
3406
3704
  onKeyDown: onHandleKeyDown
3407
3705
  }
3408
3706
  };
3409
- return /* @__PURE__ */ jsx16(
3707
+ return /* @__PURE__ */ jsx18(
3410
3708
  "li",
3411
3709
  {
3412
3710
  "data-reorder-index": index,
3413
3711
  "data-dragging": draggedId === item.id ? "true" : void 0,
3712
+ "data-drop-target": draggedId && draggedId !== item.id ? dropTargetIndex === index ? "before" : dropTargetIndex === index + 1 ? "after" : void 0 : void 0,
3414
3713
  onDragOver: (event) => {
3415
- if (draggedId && draggedId !== item.id) event.preventDefault();
3714
+ if (!draggedId || draggedId === item.id) return;
3715
+ event.preventDefault();
3716
+ const bounds = event.currentTarget.getBoundingClientRect();
3717
+ const insertionIndex = event.clientY > bounds.top + bounds.height / 2 ? index + 1 : index;
3718
+ setDropTargetIndex(insertionIndex);
3719
+ },
3720
+ onDragLeave: (event) => {
3721
+ if (event.currentTarget === event.target) setDropTargetIndex(null);
3416
3722
  },
3417
3723
  onDrop: (event) => {
3418
3724
  event.preventDefault();
3419
3725
  if (!draggedId || draggedId === item.id) return;
3420
3726
  const sourceIndex = ids.indexOf(draggedId);
3421
3727
  setDraggedId(null);
3422
- move(sourceIndex, index);
3728
+ setDropTargetIndex(null);
3729
+ moveToInsertion(sourceIndex, dropTargetIndex ?? index);
3423
3730
  },
3424
3731
  children: renderItem(item, state)
3425
3732
  },
@@ -3428,309 +3735,170 @@ function KeepReorderableList({
3428
3735
  }) });
3429
3736
  }
3430
3737
 
3431
- // src/hooks/useKeepSyncFeedback.ts
3432
- import { useKeepContext as useKeepContext3 } from "@keepkit/core/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";
3462
- function useKeepSyncRecoveryDialog(options) {
3463
- const { open, onOpenChange, conflicts, onManualMerge } = options;
3464
- const context = useKeepContext4();
3465
- const conflictList = conflicts ?? context.syncState.conflicts ?? [];
3466
- const hasRecovery = conflictList.length > 0 || context.syncState.status === "error" || Boolean(context.error);
3467
- const [dismissed, setDismissed] = useState10(false);
3468
- const [busyId, setBusyId] = useState10();
3469
- const [error, setError] = useState10();
3470
- useEffect8(() => {
3471
- if (hasRecovery) setDismissed(false);
3472
- }, [hasRecovery]);
3473
- return {
3474
- conflictList,
3475
- isOpen: open ?? (hasRecovery && !dismissed),
3476
- busyId,
3477
- error,
3478
- showBackupRecovery: context.syncState.status === "error" || Boolean(context.error),
3479
- close: () => {
3480
- setDismissed(true);
3481
- onOpenChange?.(false);
3482
- },
3483
- resolve: async (conflict, resolution) => {
3484
- setError(void 0);
3485
- setBusyId(conflict.id);
3486
- try {
3487
- const merged = resolution === "manual" ? await onManualMerge?.(conflict) : void 0;
3488
- if (resolution === "manual" && !merged) throw new Error("A manual merge result is required.");
3489
- await context.resolveSyncConflict(conflict.id, resolution, merged);
3490
- } catch (cause) {
3491
- setError(cause);
3492
- } finally {
3493
- setBusyId(void 0);
3494
- }
3495
- },
3496
- labels: {
3497
- close: useUiLabel("close"),
3498
- title: useUiLabel("resolveSync"),
3499
- conflict: useUiLabel("syncConflict"),
3500
- keepLocal: useUiLabel("keepLocal"),
3501
- useServer: useUiLabel("useServer"),
3502
- manualMerge: useUiLabel("manualMerge"),
3503
- localVersion: useUiLabel("localVersion"),
3504
- remoteVersion: useUiLabel("remoteVersion"),
3505
- updatedAt: useUiLabel("updatedAt"),
3506
- note: useUiLabel("note"),
3507
- backupRecovery: useUiLabel("backupRecovery"),
3508
- backupRecoveryDescription: useUiLabel("backupRecoveryDescription"),
3509
- error: useUiLabel("error")
3510
- }
3511
- };
3512
- }
3738
+ // src/features/editor/KeepNoteEditor.tsx
3739
+ import { isValidElement as isValidElement7 } from "react";
3513
3740
 
3514
- // src/KeepSyncRecoveryDialog.tsx
3515
- import { jsx as jsx17, jsxs as jsxs11 } from "react/jsx-runtime";
3516
- function KeepSyncRecoveryDialog({
3517
- open,
3518
- onOpenChange,
3519
- conflicts,
3520
- onManualMerge,
3521
- backup,
3522
- showBackupControls = true,
3523
- title,
3524
- children,
3525
- className,
3526
- ...props
3527
- }) {
3528
- const view = useKeepSyncRecoveryDialog({ open, onOpenChange, conflicts, onManualMerge });
3529
- if (!view.isOpen) return null;
3530
- return /* @__PURE__ */ jsxs11(
3531
- "section",
3532
- {
3533
- ...props,
3534
- className,
3535
- role: "dialog",
3536
- "aria-modal": "true",
3537
- "aria-labelledby": "keepkit-sync-recovery-title",
3538
- "aria-describedby": view.error ? "keepkit-sync-recovery-error" : void 0,
3539
- "aria-busy": view.busyId !== void 0,
3540
- "data-keepkit": "sync-recovery",
3541
- "data-state": view.conflictList.length > 0 ? "conflict" : "error",
3542
- "data-loading": view.busyId !== void 0 ? "true" : void 0,
3543
- children: [
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 })
3547
- ] }),
3548
- children,
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(
3555
- ConflictPreview,
3556
- {
3557
- item: conflict.operation.item,
3558
- heading: view.labels.localVersion,
3559
- updatedAtLabel: view.labels.updatedAt,
3560
- noteLabel: view.labels.note,
3561
- side: "local"
3562
- }
3563
- ),
3564
- /* @__PURE__ */ jsx17(
3565
- ConflictPreview,
3566
- {
3567
- item: conflict.remote,
3568
- heading: view.labels.remoteVersion,
3569
- updatedAtLabel: view.labels.updatedAt,
3570
- noteLabel: view.labels.note,
3571
- side: "remote"
3572
- }
3573
- )
3574
- ] }),
3575
- /* @__PURE__ */ jsxs11("div", { children: [
3576
- /* @__PURE__ */ jsx17(
3577
- "button",
3578
- {
3579
- type: "button",
3580
- "data-keep-action": "keep-local",
3581
- onClick: () => void view.resolve(conflict, "local"),
3582
- disabled: view.busyId !== void 0,
3583
- children: view.labels.keepLocal
3584
- }
3585
- ),
3586
- /* @__PURE__ */ jsx17(
3587
- "button",
3588
- {
3589
- type: "button",
3590
- "data-keep-action": "use-server",
3591
- onClick: () => void view.resolve(conflict, "remote"),
3592
- disabled: view.busyId !== void 0,
3593
- children: view.labels.useServer
3594
- }
3595
- ),
3596
- /* @__PURE__ */ jsx17(
3597
- "button",
3598
- {
3599
- type: "button",
3600
- "data-keep-action": "manual-merge",
3601
- onClick: () => void view.resolve(conflict, "manual"),
3602
- disabled: view.busyId !== void 0 || !onManualMerge,
3603
- children: view.labels.manualMerge
3604
- }
3605
- )
3606
- ] })
3607
- ] }, conflict.id))
3608
- ] }) : 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)
3613
- ] }) : 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
3615
- ]
3616
- }
3617
- );
3618
- }
3619
- function ConflictPreview({
3620
- item,
3621
- heading,
3622
- updatedAtLabel,
3623
- noteLabel,
3624
- side
3625
- }) {
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" })
3632
- ] }),
3633
- /* @__PURE__ */ jsxs11("div", { children: [
3634
- /* @__PURE__ */ jsx17("dt", { children: noteLabel }),
3635
- /* @__PURE__ */ jsx17("dd", { children: item?.note || "\u2014" })
3636
- ] })
3637
- ] })
3638
- ] });
3639
- }
3640
- function formatConflictDate(timestamp) {
3641
- return new Date(timestamp).toISOString().slice(0, 10);
3741
+ // src/features/navigation/KeepShortcutHint.tsx
3742
+ import { jsx as jsx19, jsxs as jsxs14 } from "react/jsx-runtime";
3743
+ function KeepShortcutHint({ shortcut, separator = "+", ...props }) {
3744
+ const keys = typeof shortcut === "string" ? shortcut.split("+") : [...shortcut];
3745
+ const occurrences = /* @__PURE__ */ new Map();
3746
+ const keyedKeys = keys.map((key) => {
3747
+ const occurrence = occurrences.get(key) ?? 0;
3748
+ occurrences.set(key, occurrence + 1);
3749
+ return { id: `${key}-${occurrence}`, value: key };
3750
+ });
3751
+ return /* @__PURE__ */ jsx19("span", { ...props, "data-keepkit": "shortcut-hint", "aria-hidden": props["aria-hidden"] ?? true, children: keyedKeys.map((key, index) => /* @__PURE__ */ jsxs14("span", { children: [
3752
+ index > 0 ? /* @__PURE__ */ jsx19("span", { "data-shortcut-separator": "true", children: separator }) : null,
3753
+ /* @__PURE__ */ jsx19("kbd", { children: key.value.trim() })
3754
+ ] }, key.id)) });
3642
3755
  }
3643
-
3644
- // src/hooks/useKeepSyncStatusBanner.ts
3645
- import { useKeepContext as useKeepContext5 } from "@keepkit/core/react";
3646
- function useKeepSyncStatusBanner({ onRetry, children }) {
3647
- const context = useKeepContext5();
3648
- const retryLabel = useUiLabel("retrySync");
3649
- const resolveLabel = useUiLabel("resolveSync");
3650
- const conflictLabel = useUiLabel("syncConflict");
3651
- const pendingLabel = useUiLabel("syncPending");
3652
- const syncedLabel = useUiLabel("syncSynced");
3653
- const status = context.syncState.status;
3654
- const hasConflicts = (context.syncState.conflicts?.length ?? 0) > 0 || context.syncState.conflictIds.length > 0;
3655
- const message = children ?? (status === "error" ? getErrorMessage4(context.syncState.error) : status === "conflict" || hasConflicts ? conflictLabel : status === "pending" || status === "syncing" ? pendingLabel : syncedLabel);
3756
+
3757
+ // src/features/editor/hooks/useKeepNoteEditor.ts
3758
+ import { useKeepItem as useKeepItem3 } from "@keepkit/core/react";
3759
+ import { useCallback as useCallback4, useEffect as useEffect7, useRef as useRef5, useState as useState10 } from "react";
3760
+ function useKeepNoteEditor({ item, debounceMs, onSaved, onSaveError }) {
3761
+ const itemState = useKeepItem3(item);
3762
+ const { error, isMutating, item: savedItem, updateNote } = itemState;
3763
+ const [note, setNote] = useState10(item.note ?? "");
3764
+ const baselineNote = savedItem?.note ?? item.note ?? "";
3765
+ const isDirty = note !== baselineNote;
3766
+ const lastSavedNoteRef = useRef5(void 0);
3767
+ useEffect7(() => setNote(baselineNote), [baselineNote]);
3768
+ const save = useCallback4(async () => {
3769
+ const nextNote = note.trim() || void 0;
3770
+ try {
3771
+ await updateNote(nextNote);
3772
+ lastSavedNoteRef.current = note;
3773
+ onSaved?.(nextNote);
3774
+ } catch (cause) {
3775
+ onSaveError?.(cause);
3776
+ throw cause;
3777
+ }
3778
+ }, [note, onSaveError, onSaved, updateNote]);
3779
+ useEffect7(() => {
3780
+ if (!isDirty || debounceMs <= 0 || lastSavedNoteRef.current === note) return;
3781
+ const timer = window.setTimeout(() => void save().catch(() => void 0), debounceMs);
3782
+ return () => window.clearTimeout(timer);
3783
+ }, [debounceMs, isDirty, note, save]);
3784
+ const state = {
3785
+ item,
3786
+ note,
3787
+ setNote,
3788
+ isDirty,
3789
+ isSaving: isMutating,
3790
+ error,
3791
+ save
3792
+ };
3793
+ const submit = (event) => {
3794
+ event.preventDefault();
3795
+ void save().catch(() => void 0);
3796
+ };
3656
3797
  return {
3657
- status,
3658
- hasConflicts,
3659
- message,
3660
- isMutating: context.isMutating,
3661
- role: status === "error" || status === "conflict" || hasConflicts ? "alert" : "status",
3662
- showRetry: status === "error" || status === "pending" || status === "syncing",
3663
- retryLabel,
3664
- resolveLabel,
3665
- retry: async () => {
3666
- if (onRetry) {
3667
- await onRetry();
3668
- return;
3669
- }
3670
- await context.flushSync();
3798
+ state,
3799
+ submit,
3800
+ handleKeyDown: (event) => {
3801
+ if (event.key !== "Enter" || !event.ctrlKey && !event.metaKey) return;
3802
+ event.preventDefault();
3803
+ void save().catch(() => void 0);
3804
+ },
3805
+ labels: {
3806
+ note: useUiLabel("note"),
3807
+ save: useUiLabel("saveNote"),
3808
+ error: useUiLabel("error")
3671
3809
  }
3672
3810
  };
3673
3811
  }
3674
- function getErrorMessage4(error) {
3675
- return error instanceof Error ? error.message : "Sync failed.";
3676
- }
3677
3812
 
3678
- // src/KeepSyncStatusBanner.tsx
3679
- import { jsx as jsx18, jsxs as jsxs12 } from "react/jsx-runtime";
3680
- function KeepSyncStatusBanner({
3681
- onRetry,
3682
- onResolveConflicts,
3813
+ // src/features/editor/KeepNoteEditor.tsx
3814
+ import { Fragment as Fragment10, jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
3815
+ function KeepNoteEditor({
3816
+ item,
3817
+ label,
3818
+ saveLabel,
3819
+ placeholder,
3820
+ debounceMs = 300,
3821
+ showShortcutHint = false,
3822
+ onSaved,
3823
+ onSaveError,
3824
+ render,
3683
3825
  children,
3826
+ asChild = false,
3684
3827
  className,
3685
- ...props
3828
+ ...formProps
3686
3829
  }) {
3687
- const view = useKeepSyncStatusBanner({ onRetry, children });
3688
- if (view.status === "idle" && !view.hasConflicts) return null;
3689
- return /* @__PURE__ */ jsxs12(
3690
- "aside",
3830
+ const view = useKeepNoteEditor({ item, debounceMs, onSaved, onSaveError });
3831
+ const { error, isDirty, isSaving, note, setNote } = view.state;
3832
+ const contentChildren = asChild && isValidElement7(children) ? void 0 : children;
3833
+ const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? /* @__PURE__ */ jsxs15(Fragment10, { children: [
3834
+ /* @__PURE__ */ jsxs15("label", { children: [
3835
+ label ?? view.labels.note,
3836
+ /* @__PURE__ */ jsx20(
3837
+ "textarea",
3838
+ {
3839
+ "data-keep-action": "edit-note",
3840
+ value: note,
3841
+ onChange: (event) => setNote(event.currentTarget.value),
3842
+ placeholder,
3843
+ disabled: isSaving,
3844
+ onKeyDown: view.handleKeyDown
3845
+ }
3846
+ )
3847
+ ] }),
3848
+ /* @__PURE__ */ jsxs15("button", { type: "submit", "data-keep-action": "save-note", disabled: isSaving, "aria-busy": isSaving, children: [
3849
+ saveLabel ?? view.labels.save,
3850
+ showShortcutHint ? /* @__PURE__ */ jsx20(KeepShortcutHint, { shortcut: "Ctrl+Enter" }) : null
3851
+ ] })
3852
+ ] });
3853
+ if (!asChild) {
3854
+ return /* @__PURE__ */ jsxs15(
3855
+ "form",
3856
+ {
3857
+ ...formProps,
3858
+ className,
3859
+ "data-keepkit": "note-editor",
3860
+ onSubmit: view.submit,
3861
+ "aria-busy": isSaving || formProps["aria-busy"],
3862
+ "data-state": error ? "error" : isDirty ? "dirty" : "clean",
3863
+ "data-loading": isSaving ? "true" : void 0,
3864
+ "data-disabled": isSaving ? "true" : void 0,
3865
+ children: [
3866
+ body,
3867
+ error ? /* @__PURE__ */ jsx20("p", { role: "alert", children: getErrorMessage3(error, view.labels.error) }) : null
3868
+ ]
3869
+ }
3870
+ );
3871
+ }
3872
+ return renderRoot(
3873
+ true,
3874
+ isValidElement7(children) ? children : void 0,
3691
3875
  {
3692
- ...props,
3876
+ ...formProps,
3693
3877
  className,
3694
- role: props.role ?? view.role,
3695
- "aria-live": props["aria-live"] ?? "polite",
3696
- "data-keepkit": "sync-status",
3697
- "data-state": view.status,
3698
- children: [
3699
- /* @__PURE__ */ jsx18("p", { children: view.message }),
3700
- view.showRetry ? /* @__PURE__ */ jsx18(
3701
- "button",
3702
- {
3703
- type: "button",
3704
- "data-keep-action": "retry-sync",
3705
- onClick: () => void view.retry(),
3706
- disabled: view.isMutating,
3707
- children: view.retryLabel
3708
- }
3709
- ) : null,
3710
- view.hasConflicts ? /* @__PURE__ */ jsx18(
3711
- "button",
3712
- {
3713
- type: "button",
3714
- "data-keep-action": "resolve-conflicts",
3715
- onClick: onResolveConflicts,
3716
- disabled: !onResolveConflicts,
3717
- children: view.resolveLabel
3718
- }
3719
- ) : null
3720
- ]
3721
- }
3878
+ "data-keepkit": "note-editor",
3879
+ onSubmit: view.submit,
3880
+ "aria-busy": isSaving || formProps["aria-busy"],
3881
+ "data-state": error ? "error" : isDirty ? "dirty" : "clean",
3882
+ "data-loading": isSaving ? "true" : void 0,
3883
+ "data-disabled": isSaving ? "true" : void 0
3884
+ },
3885
+ body,
3886
+ "KeepNoteEditor"
3722
3887
  );
3723
3888
  }
3889
+ function getErrorMessage3(error, fallback) {
3890
+ return error instanceof Error ? error.message : fallback;
3891
+ }
3724
3892
 
3725
- // src/hooks/useKeepTagEditor.ts
3893
+ // src/features/editor/hooks/useKeepTagEditor.ts
3726
3894
  import { useKeepItem as useKeepItem4 } from "@keepkit/core/react";
3727
- import { useCallback as useCallback6, useEffect as useEffect9, useState as useState11 } from "react";
3895
+ import { useCallback as useCallback5, useEffect as useEffect8, useState as useState11 } from "react";
3728
3896
  function useKeepTagEditor({ item, onSaved, onSaveError }) {
3729
3897
  const itemState = useKeepItem4(item);
3730
3898
  const [tags, setTags] = useState11(item.tags ?? []);
3731
3899
  const [input, setInput] = useState11("");
3732
- useEffect9(() => setTags(itemState.item?.tags ?? item.tags ?? []), [item.tags, itemState.item?.tags]);
3733
- const save = useCallback6(async () => {
3900
+ useEffect8(() => setTags(itemState.item?.tags ?? item.tags ?? []), [item.tags, itemState.item?.tags]);
3901
+ const save = useCallback5(async () => {
3734
3902
  const nextTags = normalizeUiTags(tags);
3735
3903
  try {
3736
3904
  await itemState.updateTags(nextTags);
@@ -3775,8 +3943,8 @@ function useKeepTagEditor({ item, onSaved, onSaveError }) {
3775
3943
  };
3776
3944
  }
3777
3945
 
3778
- // src/KeepTagEditor.tsx
3779
- import { Fragment as Fragment8, jsx as jsx19, jsxs as jsxs13 } from "react/jsx-runtime";
3946
+ // src/features/editor/KeepTagEditor.tsx
3947
+ import { Fragment as Fragment11, jsx as jsx21, jsxs as jsxs16 } from "react/jsx-runtime";
3780
3948
  function KeepTagEditor({
3781
3949
  item,
3782
3950
  availableTags = [],
@@ -3787,10 +3955,10 @@ function KeepTagEditor({
3787
3955
  }) {
3788
3956
  const view = useKeepTagEditor({ item, onSaved, onSaveError });
3789
3957
  const { isSaving, tags } = view.state;
3790
- const body = render ? render(view.state) : /* @__PURE__ */ jsxs13(Fragment8, { children: [
3791
- /* @__PURE__ */ jsxs13("label", { children: [
3958
+ const body = render ? render(view.state) : /* @__PURE__ */ jsxs16(Fragment11, { children: [
3959
+ /* @__PURE__ */ jsxs16("label", { children: [
3792
3960
  view.labels.tags,
3793
- /* @__PURE__ */ jsx19(
3961
+ /* @__PURE__ */ jsx21(
3794
3962
  "input",
3795
3963
  {
3796
3964
  "data-keep-action": "edit-tags",
@@ -3801,14 +3969,14 @@ function KeepTagEditor({
3801
3969
  }
3802
3970
  )
3803
3971
  ] }),
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: [
3972
+ availableTags.length > 0 ? /* @__PURE__ */ jsx21("datalist", { id: `keep-tags-${item.id}`, children: availableTags.map((tag) => /* @__PURE__ */ jsx21("option", { value: tag }, tag)) }) : null,
3973
+ /* @__PURE__ */ jsx21("ul", { "aria-label": view.labels.tags, children: tags.map((tag) => /* @__PURE__ */ jsxs16("li", { children: [
3806
3974
  tag,
3807
- /* @__PURE__ */ jsx19("button", { type: "button", "data-keep-action": "remove-tag", onClick: () => view.removeTag(tag), children: view.labels.remove })
3975
+ /* @__PURE__ */ jsx21("button", { type: "button", "data-keep-action": "remove-tag", onClick: () => view.removeTag(tag), children: view.labels.remove })
3808
3976
  ] }, tag)) }),
3809
- /* @__PURE__ */ jsx19("button", { type: "submit", "data-keep-action": "apply-tags", disabled: isSaving, "aria-busy": isSaving, children: view.labels.apply })
3977
+ /* @__PURE__ */ jsx21("button", { type: "submit", "data-keep-action": "apply-tags", disabled: isSaving, "aria-busy": isSaving, children: view.labels.apply })
3810
3978
  ] });
3811
- return /* @__PURE__ */ jsxs13(
3979
+ return /* @__PURE__ */ jsxs16(
3812
3980
  "form",
3813
3981
  {
3814
3982
  ...props,
@@ -3820,20 +3988,41 @@ function KeepTagEditor({
3820
3988
  "data-disabled": isSaving ? "true" : void 0,
3821
3989
  children: [
3822
3990
  body,
3823
- view.error ? /* @__PURE__ */ jsx19("p", { role: "alert", children: getErrorMessage5(view.error, view.labels.error) }) : null
3991
+ view.error ? /* @__PURE__ */ jsx21("p", { role: "alert", children: getErrorMessage4(view.error, view.labels.error) }) : null
3824
3992
  ]
3825
3993
  }
3826
3994
  );
3827
3995
  }
3828
- function getErrorMessage5(error, fallback) {
3996
+ function getErrorMessage4(error, fallback) {
3829
3997
  return error instanceof Error ? error.message : fallback;
3830
3998
  }
3831
3999
 
3832
- // src/KeepTourBar.tsx
4000
+ // src/features/feedback/useKeepToastFeedback.ts
4001
+ import { useCallback as useCallback6 } from "react";
4002
+ function useKeepToastFeedback(showToast) {
4003
+ return useCallback6(
4004
+ (event) => {
4005
+ if (!("undo" in event)) {
4006
+ showToast(event.message);
4007
+ return;
4008
+ }
4009
+ showToast(event.message, {
4010
+ action: {
4011
+ label: event.undoLabel,
4012
+ onClick: () => void event.undo()
4013
+ }
4014
+ });
4015
+ },
4016
+ [showToast]
4017
+ );
4018
+ }
4019
+
4020
+ // src/features/navigation/KeepTourBar.tsx
3833
4021
  import { useKeepNavigator } from "@keepkit/core/react";
4022
+ import { useId } from "react";
3834
4023
 
3835
- // src/hooks/useKeepTourShortcuts.ts
3836
- import { useEffect as useEffect10 } from "react";
4024
+ // src/features/navigation/hooks/useKeepTourShortcuts.ts
4025
+ import { useEffect as useEffect9 } from "react";
3837
4026
  function useKeepTourShortcuts({
3838
4027
  onNext,
3839
4028
  onPrev,
@@ -3844,7 +4033,7 @@ function useKeepTourShortcuts({
3844
4033
  prevKeys = ["k", "["],
3845
4034
  onError
3846
4035
  }) {
3847
- useEffect10(() => {
4036
+ useEffect9(() => {
3848
4037
  if (!enabled) return;
3849
4038
  const handleKeyDown = (event) => {
3850
4039
  if (!allowInEditable && isEditableTarget(event.target)) return;
@@ -3864,8 +4053,8 @@ function isEditableTarget(target) {
3864
4053
  return target.isContentEditable || target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT";
3865
4054
  }
3866
4055
 
3867
- // src/KeepTourBar.tsx
3868
- import { jsx as jsx20, jsxs as jsxs14 } from "react/jsx-runtime";
4056
+ // src/features/navigation/KeepTourBar.tsx
4057
+ import { Fragment as Fragment12, jsx as jsx22, jsxs as jsxs17 } from "react/jsx-runtime";
3869
4058
  function KeepTourBar({
3870
4059
  navigation: providedNavigation,
3871
4060
  currentId,
@@ -3881,8 +4070,12 @@ function KeepTourBar({
3881
4070
  nextLabel,
3882
4071
  backLabel,
3883
4072
  keyboardShortcuts = false,
4073
+ showShortcutHint = false,
3884
4074
  shortcutOptions,
3885
4075
  progress,
4076
+ getItemTitle = (item) => getMetaTitle(item.meta) ?? item.id,
4077
+ children,
4078
+ asChild = false,
3886
4079
  ...props
3887
4080
  }) {
3888
4081
  const ownNavigation = useKeepNavigator({ currentId, initialIndex });
@@ -3903,36 +4096,82 @@ function KeepTourBar({
3903
4096
  return resolvedPrev();
3904
4097
  }
3905
4098
  });
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(
4099
+ const body = /* @__PURE__ */ jsxs17(Fragment12, { children: [
4100
+ showProgress ? /* @__PURE__ */ jsx22("span", { "data-keepkit": "tour-progress", "aria-live": "polite", children: progress ?? `${navigation.currentPosition ?? 0} / ${navigation.items.length}` }) : null,
4101
+ /* @__PURE__ */ jsx22(
3909
4102
  TourAction,
3910
4103
  {
3911
4104
  href: prevHref,
3912
4105
  disabled: !navigation.hasPrev,
3913
4106
  onClick: prevHref ? onPrev : resolvedPrev,
3914
4107
  "data-keep-action": "tour-prev",
4108
+ shortcut: showShortcutHint ? shortcutOptions?.prevKeys?.[0] ?? "K" : void 0,
4109
+ preview: navigation.prevItem ? /* @__PURE__ */ jsxs17(Fragment12, { children: [
4110
+ previousLabel,
4111
+ ": ",
4112
+ getItemTitle(navigation.prevItem)
4113
+ ] }) : void 0,
3915
4114
  children: previousLabel
3916
4115
  }
3917
4116
  ),
3918
- /* @__PURE__ */ jsx20(
4117
+ /* @__PURE__ */ jsx22(
3919
4118
  TourAction,
3920
4119
  {
3921
4120
  href: nextHref,
3922
4121
  disabled: !navigation.hasNext,
3923
4122
  onClick: nextHref ? onNext : resolvedNext,
3924
4123
  "data-keep-action": "tour-next",
4124
+ shortcut: showShortcutHint ? shortcutOptions?.nextKeys?.[0] ?? "J" : void 0,
4125
+ preview: navigation.nextItem ? /* @__PURE__ */ jsxs17(Fragment12, { children: [
4126
+ nextItemLabel,
4127
+ ": ",
4128
+ getItemTitle(navigation.nextItem)
4129
+ ] }) : void 0,
3925
4130
  children: nextItemLabel
3926
4131
  }
3927
4132
  ),
3928
- backHref || onBack ? /* @__PURE__ */ jsx20(TourAction, { href: backHref, onClick: onBack, "data-keep-action": "tour-back", children: listLabel }) : null
4133
+ backHref || onBack ? /* @__PURE__ */ jsx22(TourAction, { href: backHref, onClick: onBack, "data-keep-action": "tour-back", children: listLabel }) : null
3929
4134
  ] });
4135
+ return renderRoot(
4136
+ asChild,
4137
+ children,
4138
+ { ...props, "data-keepkit": "tour-bar", "aria-label": props["aria-label"] ?? labels.pagination },
4139
+ body,
4140
+ "KeepTourBar"
4141
+ );
3930
4142
  }
3931
- function TourAction({ href, disabled = false, onClick, children, ...props }) {
4143
+ function TourAction({ href, disabled = false, onClick, children, preview, shortcut, ...props }) {
4144
+ const previewId = useId();
4145
+ const content = /* @__PURE__ */ jsxs17(Fragment12, { children: [
4146
+ /* @__PURE__ */ jsx22("span", { "data-tour-label": "true", children }),
4147
+ shortcut ? /* @__PURE__ */ jsx22(KeepShortcutHint, { shortcut }) : null,
4148
+ preview ? /* @__PURE__ */ jsx22("small", { id: previewId, "data-tour-preview": "true", children: preview }) : null
4149
+ ] });
3932
4150
  if (href && !disabled) {
3933
- return /* @__PURE__ */ jsx20("a", { ...props, href, onClick: () => void onClick?.(), children });
4151
+ return /* @__PURE__ */ jsx22(
4152
+ "a",
4153
+ {
4154
+ ...props,
4155
+ href,
4156
+ onClick: () => void onClick?.(),
4157
+ "aria-label": String(children),
4158
+ "aria-describedby": preview ? previewId : void 0,
4159
+ children: content
4160
+ }
4161
+ );
3934
4162
  }
3935
- return /* @__PURE__ */ jsx20("button", { ...props, type: "button", disabled, onClick: () => void onClick?.(), children });
4163
+ return /* @__PURE__ */ jsx22(
4164
+ "button",
4165
+ {
4166
+ ...props,
4167
+ type: "button",
4168
+ disabled,
4169
+ onClick: () => void onClick?.(),
4170
+ "aria-label": String(children),
4171
+ "aria-describedby": preview ? previewId : void 0,
4172
+ children: content
4173
+ }
4174
+ );
3936
4175
  }
3937
4176
  var KeepNavigator = KeepTourBar;
3938
4177
  function navigateTo(item, href) {
@@ -3940,48 +4179,14 @@ function navigateTo(item, href) {
3940
4179
  window.location.assign(href);
3941
4180
  }
3942
4181
 
3943
- // src/hooks/useKeepUndo.ts
3944
- import { useKeepContext as useKeepContext6 } from "@keepkit/core/react";
3945
- function useKeepUndo() {
3946
- const context = useKeepContext6();
3947
- const emitFeedback = useKeepUiFeedback();
3948
- const restoredMessage = useUiLabel("restoredMessage");
3949
- return {
3950
- canUndo: context.undo.canUndo,
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
- },
3958
- message: useUiLabel("undoAvailable"),
3959
- label: useUiLabel("undo")
3960
- };
3961
- }
3962
-
3963
- // src/KeepUndo.tsx
3964
- import { jsx as jsx21, jsxs as jsxs15 } from "react/jsx-runtime";
3965
- function KeepUndo({ children, label, ...props }) {
3966
- const view = useKeepUndo();
3967
- if (!view.canUndo) return null;
3968
- return /* @__PURE__ */ jsxs15("div", { ...props, role: "status", "aria-live": "polite", "data-keepkit": "undo", "data-state": "available", children: [
3969
- children ?? view.message,
3970
- /* @__PURE__ */ jsx21("button", { type: "button", "data-keep-action": "undo", onClick: () => void view.undo(), children: label ?? view.label })
3971
- ] });
3972
- }
3973
-
3974
- // src/status.tsx
3975
- import { isValidElement as isValidElement6 } from "react";
4182
+ // src/features/status/status.tsx
4183
+ import { isValidElement as isValidElement8 } from "react";
3976
4184
 
3977
- // src/hooks/useStatusViews.ts
3978
- import { useKeepContext as useKeepContext7 } from "@keepkit/core/react";
3979
- import { useEffect as useEffect11, useRef as useRef7, useState as useState12 } from "react";
3980
- function useKeepEmptyState() {
3981
- return useUiLabel("noItems").replace(/\.$/, "");
3982
- }
4185
+ // src/features/status/hooks/useStatusViews.ts
4186
+ import { useKeepContext as useKeepContext4 } from "@keepkit/core/react";
4187
+ import { useEffect as useEffect10, useRef as useRef6, useState as useState12 } from "react";
3983
4188
  function useKeepStatus(status) {
3984
- const context = useKeepContext7();
4189
+ const context = useKeepContext4();
3985
4190
  const resolvedStatus = status ?? getDerivedStatus(context);
3986
4191
  const state = {
3987
4192
  status: resolvedStatus,
@@ -3992,13 +4197,13 @@ function useKeepStatus(status) {
3992
4197
  return { state, defaultLabel: useUiLabel(getStatusLabelKey3(resolvedStatus)) };
3993
4198
  }
3994
4199
  function useKeepAnnouncements(messages) {
3995
- const context = useKeepContext7();
4200
+ const context = useKeepContext4();
3996
4201
  const savedMessage = useUiLabel("savedMessage", messages?.save);
3997
4202
  const removedMessage = useUiLabel("removedMessage", messages?.remove);
3998
4203
  const noteSavedMessage = useUiLabel("noteSavedMessage", messages?.note);
3999
4204
  const [message, setMessage] = useState12("");
4000
- const lastChangeRef = useRef7(void 0);
4001
- useEffect11(() => {
4205
+ const lastChangeRef = useRef6(void 0);
4206
+ useEffect10(() => {
4002
4207
  const change = context.lastChange;
4003
4208
  if (!change || change === lastChangeRef.current) return;
4004
4209
  lastChangeRef.current = change;
@@ -4014,43 +4219,19 @@ function getDerivedStatus(context) {
4014
4219
  if (context.isMutating) return "saving";
4015
4220
  if (context.isLoading && !context.isHydrated) return "loading";
4016
4221
  if (context.isHydrated && context.items.length === 0) return "empty";
4017
- return "idle";
4018
- }
4019
- function getStatusLabelKey3(status) {
4020
- if (status === "empty") return "noItems";
4021
- if (status === "loading") return "loadingItems";
4022
- if (status === "error") return "error";
4023
- if (status === "saving") return "saving";
4024
- if (status === "syncing") return "syncing";
4025
- return "saved";
4026
- }
4027
-
4028
- // src/status.tsx
4029
- import { Fragment as Fragment9, jsx as jsx22, jsxs as jsxs16 } from "react/jsx-runtime";
4030
- function KeepEmptyState({
4031
- title,
4032
- description,
4033
- action,
4034
- children,
4035
- asChild = false,
4036
- className,
4037
- ...rootProps
4038
- }) {
4039
- const defaultTitle = useKeepEmptyState();
4040
- const contentChildren = asChild && isValidElement6(children) ? void 0 : children;
4041
- const body = contentChildren ?? /* @__PURE__ */ jsxs16(Fragment9, { children: [
4042
- /* @__PURE__ */ jsx22("h2", { children: title ?? defaultTitle }),
4043
- description ? /* @__PURE__ */ jsx22("p", { children: description }) : null,
4044
- action
4045
- ] });
4046
- return renderRoot(
4047
- asChild,
4048
- children,
4049
- { ...rootProps, className, "data-keepkit": "empty-state", "data-state": "empty" },
4050
- body,
4051
- "KeepEmptyState"
4052
- );
4222
+ return "idle";
4223
+ }
4224
+ function getStatusLabelKey3(status) {
4225
+ if (status === "empty") return "noItems";
4226
+ if (status === "loading") return "loadingItems";
4227
+ if (status === "error") return "error";
4228
+ if (status === "saving") return "saving";
4229
+ if (status === "syncing") return "syncing";
4230
+ return "saved";
4053
4231
  }
4232
+
4233
+ // src/features/status/status.tsx
4234
+ import { jsx as jsx23 } from "react/jsx-runtime";
4054
4235
  function KeepStatus({
4055
4236
  status,
4056
4237
  labels,
@@ -4061,12 +4242,12 @@ function KeepStatus({
4061
4242
  ...rootProps
4062
4243
  }) {
4063
4244
  const view = useKeepStatus(status);
4064
- const contentChildren = asChild && isValidElement6(children) ? void 0 : children;
4245
+ const contentChildren = asChild && isValidElement8(children) ? void 0 : children;
4065
4246
  const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? labels?.[view.state.status] ?? view.defaultLabel;
4066
4247
  const role = rootProps.role ?? (view.state.status === "error" ? "alert" : "status");
4067
4248
  return renderRoot(
4068
4249
  asChild,
4069
- isValidElement6(children) ? children : void 0,
4250
+ isValidElement8(children) ? children : void 0,
4070
4251
  {
4071
4252
  ...rootProps,
4072
4253
  className,
@@ -4082,7 +4263,7 @@ function KeepStatus({
4082
4263
  }
4083
4264
  function KeepAnnouncements({ messages, ...props }) {
4084
4265
  const message = useKeepAnnouncements(messages);
4085
- return /* @__PURE__ */ jsx22(
4266
+ return /* @__PURE__ */ jsx23(
4086
4267
  "div",
4087
4268
  {
4088
4269
  ...props,
@@ -4097,9 +4278,303 @@ function KeepAnnouncements({ messages, ...props }) {
4097
4278
  }
4098
4279
  var KeepAnnouncer = KeepAnnouncements;
4099
4280
 
4100
- // src/theme.tsx
4101
- import { cloneElement as cloneElement2, isValidElement as isValidElement7 } from "react";
4102
- import { jsx as jsx23 } from "react/jsx-runtime";
4281
+ // src/features/sync/hooks/useKeepSyncFeedback.ts
4282
+ import { useKeepContext as useKeepContext5 } from "@keepkit/core/react";
4283
+ import { useEffect as useEffect11, useRef as useRef7 } from "react";
4284
+ function useKeepSyncFeedback() {
4285
+ const { syncState } = useKeepContext5();
4286
+ const emitFeedback = useKeepUiFeedback();
4287
+ const completedMessage = useUiLabel("syncSynced");
4288
+ const failedMessage = useUiLabel("syncFailedMessage");
4289
+ const previousStatus = useRef7("idle");
4290
+ useEffect11(() => {
4291
+ const previous = previousStatus.current;
4292
+ previousStatus.current = syncState.status;
4293
+ if (syncState.status === "error" && previous !== "error") {
4294
+ emitFeedback({ type: "sync-failed", error: syncState.error, message: failedMessage });
4295
+ return;
4296
+ }
4297
+ if (syncState.status === "synced" && (previous === "pending" || previous === "syncing" || previous === "conflict" || previous === "error")) {
4298
+ emitFeedback({ type: "sync-completed", message: completedMessage });
4299
+ }
4300
+ }, [completedMessage, emitFeedback, failedMessage, syncState.error, syncState.status]);
4301
+ }
4302
+
4303
+ // src/features/sync/KeepSyncFeedbackObserver.tsx
4304
+ function KeepSyncFeedbackObserver() {
4305
+ useKeepSyncFeedback();
4306
+ return null;
4307
+ }
4308
+
4309
+ // src/features/sync/hooks/useKeepSyncRecoveryDialog.ts
4310
+ import { useKeepContext as useKeepContext6 } from "@keepkit/core/react";
4311
+ import { useEffect as useEffect12, useState as useState13 } from "react";
4312
+ function useKeepSyncRecoveryDialog(options) {
4313
+ const { open, onOpenChange, conflicts, onManualMerge } = options;
4314
+ const context = useKeepContext6();
4315
+ const conflictList = conflicts ?? context.syncState.conflicts ?? [];
4316
+ const hasRecovery = conflictList.length > 0 || context.syncState.status === "error" || Boolean(context.error);
4317
+ const [dismissed, setDismissed] = useState13(false);
4318
+ const [busyId, setBusyId] = useState13();
4319
+ const [error, setError] = useState13();
4320
+ useEffect12(() => {
4321
+ if (hasRecovery) setDismissed(false);
4322
+ }, [hasRecovery]);
4323
+ return {
4324
+ conflictList,
4325
+ isOpen: open ?? (hasRecovery && !dismissed),
4326
+ busyId,
4327
+ error,
4328
+ showBackupRecovery: context.syncState.status === "error" || Boolean(context.error),
4329
+ close: () => {
4330
+ setDismissed(true);
4331
+ onOpenChange?.(false);
4332
+ },
4333
+ resolve: async (conflict, resolution) => {
4334
+ setError(void 0);
4335
+ setBusyId(conflict.id);
4336
+ try {
4337
+ const merged = resolution === "manual" ? await onManualMerge?.(conflict) : void 0;
4338
+ if (resolution === "manual" && !merged) throw new Error("A manual merge result is required.");
4339
+ await context.resolveSyncConflict(conflict.id, resolution, merged);
4340
+ } catch (cause) {
4341
+ setError(cause);
4342
+ } finally {
4343
+ setBusyId(void 0);
4344
+ }
4345
+ },
4346
+ labels: {
4347
+ close: useUiLabel("close"),
4348
+ title: useUiLabel("resolveSync"),
4349
+ conflict: useUiLabel("syncConflict"),
4350
+ keepLocal: useUiLabel("keepLocal"),
4351
+ useServer: useUiLabel("useServer"),
4352
+ manualMerge: useUiLabel("manualMerge"),
4353
+ localVersion: useUiLabel("localVersion"),
4354
+ remoteVersion: useUiLabel("remoteVersion"),
4355
+ updatedAt: useUiLabel("updatedAt"),
4356
+ note: useUiLabel("note"),
4357
+ backupRecovery: useUiLabel("backupRecovery"),
4358
+ backupRecoveryDescription: useUiLabel("backupRecoveryDescription"),
4359
+ error: useUiLabel("error")
4360
+ }
4361
+ };
4362
+ }
4363
+
4364
+ // src/features/sync/KeepSyncRecoveryDialog.tsx
4365
+ import { jsx as jsx24, jsxs as jsxs18 } from "react/jsx-runtime";
4366
+ function KeepSyncRecoveryDialog({
4367
+ open,
4368
+ onOpenChange,
4369
+ conflicts,
4370
+ onManualMerge,
4371
+ backup,
4372
+ showBackupControls = true,
4373
+ title,
4374
+ children,
4375
+ className,
4376
+ ...props
4377
+ }) {
4378
+ const view = useKeepSyncRecoveryDialog({ open, onOpenChange, conflicts, onManualMerge });
4379
+ if (!view.isOpen) return null;
4380
+ return /* @__PURE__ */ jsxs18(
4381
+ "section",
4382
+ {
4383
+ ...props,
4384
+ className,
4385
+ role: "dialog",
4386
+ "aria-modal": "true",
4387
+ "aria-labelledby": "keepkit-sync-recovery-title",
4388
+ "aria-describedby": view.error ? "keepkit-sync-recovery-error" : void 0,
4389
+ "aria-busy": view.busyId !== void 0,
4390
+ "data-keepkit": "sync-recovery",
4391
+ "data-state": view.conflictList.length > 0 ? "conflict" : "error",
4392
+ "data-loading": view.busyId !== void 0 ? "true" : void 0,
4393
+ children: [
4394
+ /* @__PURE__ */ jsxs18("header", { children: [
4395
+ /* @__PURE__ */ jsx24("h2", { id: "keepkit-sync-recovery-title", children: title ?? view.labels.title }),
4396
+ /* @__PURE__ */ jsx24("button", { type: "button", "data-keep-action": "close-dialog", onClick: view.close, "aria-label": view.labels.close, children: view.labels.close })
4397
+ ] }),
4398
+ children,
4399
+ view.conflictList.length > 0 ? /* @__PURE__ */ jsxs18("div", { children: [
4400
+ /* @__PURE__ */ jsx24("p", { children: view.labels.conflict }),
4401
+ view.conflictList.map((conflict) => /* @__PURE__ */ jsxs18("article", { "data-conflict-id": conflict.id, children: [
4402
+ /* @__PURE__ */ jsx24("h3", { children: getMetaTitle(conflict.operation.item?.meta) ?? conflict.id }),
4403
+ /* @__PURE__ */ jsxs18("div", { "data-conflict-preview": true, children: [
4404
+ /* @__PURE__ */ jsx24(
4405
+ ConflictPreview,
4406
+ {
4407
+ item: conflict.operation.item,
4408
+ heading: view.labels.localVersion,
4409
+ updatedAtLabel: view.labels.updatedAt,
4410
+ noteLabel: view.labels.note,
4411
+ side: "local"
4412
+ }
4413
+ ),
4414
+ /* @__PURE__ */ jsx24(
4415
+ ConflictPreview,
4416
+ {
4417
+ item: conflict.remote,
4418
+ heading: view.labels.remoteVersion,
4419
+ updatedAtLabel: view.labels.updatedAt,
4420
+ noteLabel: view.labels.note,
4421
+ side: "remote"
4422
+ }
4423
+ )
4424
+ ] }),
4425
+ /* @__PURE__ */ jsxs18("div", { children: [
4426
+ /* @__PURE__ */ jsx24(
4427
+ "button",
4428
+ {
4429
+ type: "button",
4430
+ "data-keep-action": "keep-local",
4431
+ onClick: () => void view.resolve(conflict, "local"),
4432
+ disabled: view.busyId !== void 0,
4433
+ children: view.labels.keepLocal
4434
+ }
4435
+ ),
4436
+ /* @__PURE__ */ jsx24(
4437
+ "button",
4438
+ {
4439
+ type: "button",
4440
+ "data-keep-action": "use-server",
4441
+ onClick: () => void view.resolve(conflict, "remote"),
4442
+ disabled: view.busyId !== void 0,
4443
+ children: view.labels.useServer
4444
+ }
4445
+ ),
4446
+ /* @__PURE__ */ jsx24(
4447
+ "button",
4448
+ {
4449
+ type: "button",
4450
+ "data-keep-action": "manual-merge",
4451
+ onClick: () => void view.resolve(conflict, "manual"),
4452
+ disabled: view.busyId !== void 0 || !onManualMerge,
4453
+ children: view.labels.manualMerge
4454
+ }
4455
+ )
4456
+ ] })
4457
+ ] }, conflict.id))
4458
+ ] }) : null,
4459
+ view.showBackupRecovery ? /* @__PURE__ */ jsxs18("section", { "data-recovery": "backup", children: [
4460
+ /* @__PURE__ */ jsx24("h3", { children: view.labels.backupRecovery }),
4461
+ /* @__PURE__ */ jsx24("p", { children: view.labels.backupRecoveryDescription }),
4462
+ backup ?? (showBackupControls ? /* @__PURE__ */ jsx24(KeepBackup, {}) : null)
4463
+ ] }) : null,
4464
+ view.error ? /* @__PURE__ */ jsx24("p", { id: "keepkit-sync-recovery-error", role: "alert", "aria-live": "assertive", children: view.error instanceof Error ? view.error.message : view.labels.error }) : null
4465
+ ]
4466
+ }
4467
+ );
4468
+ }
4469
+ function ConflictPreview({
4470
+ item,
4471
+ heading,
4472
+ updatedAtLabel,
4473
+ noteLabel,
4474
+ side
4475
+ }) {
4476
+ return /* @__PURE__ */ jsxs18("article", { "data-conflict-version": side, "aria-label": heading, children: [
4477
+ /* @__PURE__ */ jsx24("h4", { children: heading }),
4478
+ /* @__PURE__ */ jsxs18("dl", { children: [
4479
+ /* @__PURE__ */ jsxs18("div", { children: [
4480
+ /* @__PURE__ */ jsx24("dt", { children: updatedAtLabel }),
4481
+ /* @__PURE__ */ jsx24("dd", { children: item ? /* @__PURE__ */ jsx24("time", { dateTime: new Date(item.updatedAt).toISOString(), children: formatConflictDate(item.updatedAt) }) : "\u2014" })
4482
+ ] }),
4483
+ /* @__PURE__ */ jsxs18("div", { children: [
4484
+ /* @__PURE__ */ jsx24("dt", { children: noteLabel }),
4485
+ /* @__PURE__ */ jsx24("dd", { children: item?.note || "\u2014" })
4486
+ ] })
4487
+ ] })
4488
+ ] });
4489
+ }
4490
+ function formatConflictDate(timestamp) {
4491
+ return new Date(timestamp).toISOString().slice(0, 10);
4492
+ }
4493
+
4494
+ // src/features/sync/hooks/useKeepSyncStatusBanner.ts
4495
+ import { useKeepContext as useKeepContext7 } from "@keepkit/core/react";
4496
+ function useKeepSyncStatusBanner({ onRetry, children }) {
4497
+ const context = useKeepContext7();
4498
+ const retryLabel = useUiLabel("retrySync");
4499
+ const resolveLabel = useUiLabel("resolveSync");
4500
+ const conflictLabel = useUiLabel("syncConflict");
4501
+ const pendingLabel = useUiLabel("syncPending");
4502
+ const syncedLabel = useUiLabel("syncSynced");
4503
+ const status = context.syncState.status;
4504
+ const hasConflicts = (context.syncState.conflicts?.length ?? 0) > 0 || context.syncState.conflictIds.length > 0;
4505
+ const message = children ?? (status === "error" ? getErrorMessage5(context.syncState.error) : status === "conflict" || hasConflicts ? conflictLabel : status === "pending" || status === "syncing" ? pendingLabel : syncedLabel);
4506
+ return {
4507
+ status,
4508
+ hasConflicts,
4509
+ message,
4510
+ isMutating: context.isMutating,
4511
+ role: status === "error" || status === "conflict" || hasConflicts ? "alert" : "status",
4512
+ showRetry: status === "error" || status === "pending" || status === "syncing",
4513
+ retryLabel,
4514
+ resolveLabel,
4515
+ retry: async () => {
4516
+ if (onRetry) {
4517
+ await onRetry();
4518
+ return;
4519
+ }
4520
+ await context.flushSync();
4521
+ }
4522
+ };
4523
+ }
4524
+ function getErrorMessage5(error) {
4525
+ return error instanceof Error ? error.message : "Sync failed.";
4526
+ }
4527
+
4528
+ // src/features/sync/KeepSyncStatusBanner.tsx
4529
+ import { jsx as jsx25, jsxs as jsxs19 } from "react/jsx-runtime";
4530
+ function KeepSyncStatusBanner({
4531
+ onRetry,
4532
+ onResolveConflicts,
4533
+ children,
4534
+ className,
4535
+ ...props
4536
+ }) {
4537
+ const view = useKeepSyncStatusBanner({ onRetry, children });
4538
+ if (view.status === "idle" && !view.hasConflicts) return null;
4539
+ return /* @__PURE__ */ jsxs19(
4540
+ "aside",
4541
+ {
4542
+ ...props,
4543
+ className,
4544
+ role: props.role ?? view.role,
4545
+ "aria-live": props["aria-live"] ?? "polite",
4546
+ "data-keepkit": "sync-status",
4547
+ "data-state": view.status,
4548
+ children: [
4549
+ /* @__PURE__ */ jsx25("p", { children: view.message }),
4550
+ view.showRetry ? /* @__PURE__ */ jsx25(
4551
+ "button",
4552
+ {
4553
+ type: "button",
4554
+ "data-keep-action": "retry-sync",
4555
+ onClick: () => void view.retry(),
4556
+ disabled: view.isMutating,
4557
+ children: view.retryLabel
4558
+ }
4559
+ ) : null,
4560
+ view.hasConflicts ? /* @__PURE__ */ jsx25(
4561
+ "button",
4562
+ {
4563
+ type: "button",
4564
+ "data-keep-action": "resolve-conflicts",
4565
+ onClick: onResolveConflicts,
4566
+ disabled: !onResolveConflicts,
4567
+ children: view.resolveLabel
4568
+ }
4569
+ ) : null
4570
+ ]
4571
+ }
4572
+ );
4573
+ }
4574
+
4575
+ // src/foundation/theme.tsx
4576
+ import { isValidElement as isValidElement9 } from "react";
4577
+ import { jsx as jsx26 } from "react/jsx-runtime";
4103
4578
  var keepThemeNames = [
4104
4579
  "default",
4105
4580
  "ocean",
@@ -4154,11 +4629,11 @@ function KeepThemeProvider({
4154
4629
  "data-reduced-motion": reducedMotion ? "true" : void 0
4155
4630
  };
4156
4631
  if (asChild) {
4157
- if (!isValidElement7(children))
4632
+ if (!isValidElement9(children))
4158
4633
  throw new Error("KeepThemeProvider with asChild requires a single React element child.");
4159
- return cloneElement2(children, rootProps);
4634
+ return createSlot(children, rootProps);
4160
4635
  }
4161
- return /* @__PURE__ */ jsx23("div", { ...rootProps, children });
4636
+ return /* @__PURE__ */ jsx26("div", { ...rootProps, children });
4162
4637
  }
4163
4638
 
4164
4639
  // src/index.tsx
@@ -4182,7 +4657,7 @@ import {
4182
4657
  LocalStorageSyncQueueAdapter,
4183
4658
  SyncStorageAdapter
4184
4659
  } from "@keepkit/core/storage";
4185
- import { jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
4660
+ import { jsx as jsx27, jsxs as jsxs20 } from "react/jsx-runtime";
4186
4661
  function KeepKitProvider({
4187
4662
  labels,
4188
4663
  locale,
@@ -4202,7 +4677,7 @@ function KeepKitProvider({
4202
4677
  children,
4203
4678
  ...providerProps
4204
4679
  }) {
4205
- return /* @__PURE__ */ jsx24(KeepUiProvider, { labels, locale, labelResolver, onFeedback, children: /* @__PURE__ */ jsx24(
4680
+ return /* @__PURE__ */ jsx27(KeepUiProvider, { labels, locale, labelResolver, onFeedback, children: /* @__PURE__ */ jsx27(
4206
4681
  KeepThemeProvider,
4207
4682
  {
4208
4683
  theme,
@@ -4216,10 +4691,10 @@ function KeepKitProvider({
4216
4691
  className: themeClassName,
4217
4692
  style: themeStyle,
4218
4693
  asChild: themeAsChild,
4219
- children: /* @__PURE__ */ jsxs17(CoreKeepProvider, { ...providerProps, children: [
4220
- /* @__PURE__ */ jsx24(KeepSyncFeedbackObserver, {}),
4694
+ children: /* @__PURE__ */ jsxs20(CoreKeepProvider, { ...providerProps, children: [
4695
+ /* @__PURE__ */ jsx27(KeepSyncFeedbackObserver, {}),
4221
4696
  children,
4222
- /* @__PURE__ */ jsx24(KeepAnnouncements, {})
4697
+ /* @__PURE__ */ jsx27(KeepAnnouncements, {})
4223
4698
  ] })
4224
4699
  }
4225
4700
  ) });
@@ -4246,7 +4721,7 @@ function createKeepKit(options = {}) {
4246
4721
  } = options;
4247
4722
  const coreKit = createCoreKeepKit(coreOptions);
4248
4723
  return {
4249
- Provider: (props) => /* @__PURE__ */ jsx24(
4724
+ Provider: (props) => /* @__PURE__ */ jsx27(
4250
4725
  KeepKitProvider,
4251
4726
  {
4252
4727
  ...coreOptions,
@@ -4267,9 +4742,9 @@ function createKeepKit(options = {}) {
4267
4742
  ...props
4268
4743
  }
4269
4744
  ),
4270
- Button: (props) => /* @__PURE__ */ jsx24(KeepButton, { ...props }),
4271
- Backup: (props) => /* @__PURE__ */ jsx24(KeepBackup, { ...props }),
4272
- Collection: (props) => /* @__PURE__ */ jsx24(
4745
+ Button: (props) => /* @__PURE__ */ jsx27(KeepButton, { ...props }),
4746
+ Backup: (props) => /* @__PURE__ */ jsx27(KeepBackup, { ...props }),
4747
+ Collection: (props) => /* @__PURE__ */ jsx27(
4273
4748
  KeepCollection,
4274
4749
  {
4275
4750
  ...props,
@@ -4291,6 +4766,7 @@ export {
4291
4766
  FallbackStorageAdapter,
4292
4767
  IndexedDBAdapter,
4293
4768
  IndexedDBSyncQueueAdapter,
4769
+ KeepActiveFiltersSummary,
4294
4770
  KeepAnnouncements,
4295
4771
  KeepAnnouncer,
4296
4772
  KeepBackup,
@@ -4314,6 +4790,7 @@ export {
4314
4790
  KeepPruneStaleButton,
4315
4791
  KeepReorderableList,
4316
4792
  KeepSearchInput,
4793
+ KeepShortcutHint,
4317
4794
  KeepSortSelect,
4318
4795
  KeepStaleNotice,
4319
4796
  KeepStatus,
@@ -4328,15 +4805,18 @@ export {
4328
4805
  LocalStorageAdapter,
4329
4806
  LocalStorageSyncQueueAdapter,
4330
4807
  SyncStorageAdapter,
4808
+ chainedFunction,
4331
4809
  createAuthenticatedSyncKit,
4332
4810
  createBrowserStorageAdapter,
4333
4811
  createKeepKit,
4334
4812
  createNextPagesRouterAdapter,
4813
+ createSlot,
4335
4814
  createStorageAdapter,
4336
4815
  getKeepLocaleLabels,
4337
4816
  highlightText,
4338
4817
  isAllSelected,
4339
4818
  keepThemeNames,
4819
+ mergeProps,
4340
4820
  toggleSelectAll,
4341
4821
  useKeepContext8 as useKeepContext,
4342
4822
  useKeepItem5 as useKeepItem,