@keepkit/ui 0.10.0 → 0.11.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
@@ -22,6 +22,7 @@ var DEFAULT_LABELS = {
22
22
  syncing: "Syncing\u2026",
23
23
  error: "Something went wrong.",
24
24
  allTags: "All",
25
+ tags: "Tags",
25
26
  filterTags: "Filter saved items by tag",
26
27
  note: "Note",
27
28
  saveNote: "Save note",
@@ -47,6 +48,13 @@ var DEFAULT_LABELS = {
47
48
  applyTags: "Apply tags",
48
49
  savedMessage: "Item saved.",
49
50
  removedMessage: "Item removed.",
51
+ undo: "Undo",
52
+ undoAvailable: "Item removed. Undo?",
53
+ selectionScope: "Selection scope",
54
+ currentPage: "Current page",
55
+ searchResults: "Current search results",
56
+ allItems: "All saved items",
57
+ statusAvailable: "Available",
50
58
  noteSavedMessage: "Note saved.",
51
59
  exportData: "Export JSON",
52
60
  importData: "Import JSON",
@@ -62,11 +70,120 @@ var DEFAULT_LABELS = {
62
70
  statusPrivate: "Private",
63
71
  statusUnknown: "Unavailable"
64
72
  };
73
+ var KEEP_LOCALE_LABELS = {
74
+ ja: {
75
+ save: "\u4FDD\u5B58",
76
+ saved: "\u4FDD\u5B58\u6E08\u307F",
77
+ remove: "\u524A\u9664",
78
+ loading: "\u8AAD\u307F\u8FBC\u307F\u4E2D\u2026",
79
+ saving: "\u4FDD\u5B58\u4E2D\u2026",
80
+ syncing: "\u540C\u671F\u4E2D\u2026",
81
+ allTags: "\u3059\u3079\u3066",
82
+ filterTags: "\u4FDD\u5B58\u30A2\u30A4\u30C6\u30E0\u3092\u30BF\u30B0\u3067\u7D5E\u308A\u8FBC\u3080",
83
+ note: "\u30E1\u30E2",
84
+ saveNote: "\u30E1\u30E2\u3092\u4FDD\u5B58",
85
+ loadingItems: "\u4FDD\u5B58\u30A2\u30A4\u30C6\u30E0\u3092\u8AAD\u307F\u8FBC\u307F\u4E2D\u2026",
86
+ errorItems: "\u4FDD\u5B58\u30A2\u30A4\u30C6\u30E0\u3092\u8AAD\u307F\u8FBC\u3081\u307E\u305B\u3093\u3067\u3057\u305F\u3002",
87
+ noItems: "\u4FDD\u5B58\u3055\u308C\u305F\u30A2\u30A4\u30C6\u30E0\u306F\u3042\u308A\u307E\u305B\u3093\u3002",
88
+ search: "\u4FDD\u5B58\u30A2\u30A4\u30C6\u30E0\u3092\u691C\u7D22",
89
+ sort: "\u4FDD\u5B58\u30A2\u30A4\u30C6\u30E0\u3092\u4E26\u3079\u66FF\u3048",
90
+ newest: "\u65B0\u3057\u3044\u9806",
91
+ oldest: "\u53E4\u3044\u9806",
92
+ savedNewest: "\u4FDD\u5B58\u65E5\u6642\u306E\u65B0\u3057\u3044\u9806",
93
+ savedOldest: "\u4FDD\u5B58\u65E5\u6642\u306E\u53E4\u3044\u9806",
94
+ updatedNewest: "\u66F4\u65B0\u65E5\u6642\u306E\u65B0\u3057\u3044\u9806",
95
+ updatedOldest: "\u66F4\u65B0\u65E5\u6642\u306E\u53E4\u3044\u9806",
96
+ previousPage: "\u524D\u306E\u30DA\u30FC\u30B8",
97
+ nextPage: "\u6B21\u306E\u30DA\u30FC\u30B8",
98
+ pagination: "\u30DA\u30FC\u30B8\u30CD\u30FC\u30B7\u30E7\u30F3",
99
+ page: "\u30DA\u30FC\u30B8",
100
+ selectItems: "\u4FDD\u5B58\u30A2\u30A4\u30C6\u30E0\u3092\u9078\u629E",
101
+ selectedCount: "\u4EF6\u3092\u9078\u629E\u4E2D",
102
+ deleteSelected: "\u9078\u629E\u9805\u76EE\u3092\u524A\u9664",
103
+ undo: "\u5143\u306B\u623B\u3059",
104
+ undoAvailable: "\u30A2\u30A4\u30C6\u30E0\u3092\u524A\u9664\u3057\u307E\u3057\u305F\u3002\u5143\u306B\u623B\u3057\u307E\u3059\u304B\uFF1F",
105
+ selectionScope: "\u9078\u629E\u7BC4\u56F2",
106
+ currentPage: "\u8868\u793A\u4E2D\u306E\u30DA\u30FC\u30B8",
107
+ searchResults: "\u73FE\u5728\u306E\u691C\u7D22\u7D50\u679C\u5168\u4F53",
108
+ allItems: "\u3059\u3079\u3066\u306E\u4FDD\u5B58\u30A2\u30A4\u30C6\u30E0",
109
+ tags: "\u30BF\u30B0",
110
+ statusAvailable: "\u5229\u7528\u53EF\u80FD",
111
+ tagsToApply: "\u9069\u7528\u3059\u308B\u30BF\u30B0",
112
+ applyTags: "\u30BF\u30B0\u3092\u9069\u7528"
113
+ },
114
+ ko: {
115
+ save: "\uC800\uC7A5",
116
+ saved: "\uC800\uC7A5\uB428",
117
+ remove: "\uC0AD\uC81C",
118
+ loading: "\uB85C\uB4DC \uC911\u2026",
119
+ saving: "\uC800\uC7A5 \uC911\u2026",
120
+ syncing: "\uB3D9\uAE30\uD654 \uC911\u2026",
121
+ noItems: "\uC800\uC7A5\uB41C \uD56D\uBAA9\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
122
+ search: "\uC800\uC7A5\uB41C \uD56D\uBAA9 \uAC80\uC0C9",
123
+ sort: "\uC800\uC7A5\uB41C \uD56D\uBAA9 \uC815\uB82C",
124
+ previousPage: "\uC774\uC804 \uD398\uC774\uC9C0",
125
+ nextPage: "\uB2E4\uC74C \uD398\uC774\uC9C0",
126
+ pagination: "\uD398\uC774\uC9C0 \uB9E4\uAE40",
127
+ page: "\uD398\uC774\uC9C0",
128
+ undo: "\uC2E4\uD589 \uCDE8\uC18C",
129
+ undoAvailable: "\uD56D\uBAA9\uC744 \uC0AD\uC81C\uD588\uC2B5\uB2C8\uB2E4. \uC2E4\uD589 \uCDE8\uC18C\uD560\uAE4C\uC694?",
130
+ selectionScope: "\uC120\uD0DD \uBC94\uC704",
131
+ currentPage: "\uD604\uC7AC \uD398\uC774\uC9C0",
132
+ searchResults: "\uD604\uC7AC \uAC80\uC0C9 \uACB0\uACFC \uC804\uCCB4",
133
+ allItems: "\uBAA8\uB4E0 \uC800\uC7A5 \uD56D\uBAA9",
134
+ tags: "\uD0DC\uADF8"
135
+ },
136
+ "zh-CN": {
137
+ save: "\u4FDD\u5B58",
138
+ saved: "\u5DF2\u4FDD\u5B58",
139
+ remove: "\u5220\u9664",
140
+ loading: "\u52A0\u8F7D\u4E2D\u2026",
141
+ saving: "\u4FDD\u5B58\u4E2D\u2026",
142
+ syncing: "\u540C\u6B65\u4E2D\u2026",
143
+ noItems: "\u6CA1\u6709\u5DF2\u4FDD\u5B58\u7684\u9879\u76EE\u3002",
144
+ search: "\u641C\u7D22\u5DF2\u4FDD\u5B58\u9879\u76EE",
145
+ sort: "\u6392\u5E8F\u5DF2\u4FDD\u5B58\u9879\u76EE",
146
+ previousPage: "\u4E0A\u4E00\u9875",
147
+ nextPage: "\u4E0B\u4E00\u9875",
148
+ pagination: "\u5206\u9875",
149
+ page: "\u9875",
150
+ undo: "\u64A4\u9500",
151
+ undoAvailable: "\u9879\u76EE\u5DF2\u5220\u9664\u3002\u8981\u64A4\u9500\u5417\uFF1F",
152
+ selectionScope: "\u9009\u62E9\u8303\u56F4",
153
+ currentPage: "\u5F53\u524D\u9875",
154
+ searchResults: "\u5F53\u524D\u641C\u7D22\u7ED3\u679C",
155
+ allItems: "\u5168\u90E8\u5DF2\u4FDD\u5B58\u9879\u76EE",
156
+ tags: "\u6807\u7B7E"
157
+ },
158
+ "zh-TW": {
159
+ save: "\u5132\u5B58",
160
+ saved: "\u5DF2\u5132\u5B58",
161
+ remove: "\u522A\u9664",
162
+ loading: "\u8F09\u5165\u4E2D\u2026",
163
+ saving: "\u5132\u5B58\u4E2D\u2026",
164
+ syncing: "\u540C\u6B65\u4E2D\u2026",
165
+ noItems: "\u6C92\u6709\u5DF2\u5132\u5B58\u7684\u9805\u76EE\u3002",
166
+ search: "\u641C\u5C0B\u5DF2\u5132\u5B58\u9805\u76EE",
167
+ sort: "\u6392\u5E8F\u5DF2\u5132\u5B58\u9805\u76EE",
168
+ previousPage: "\u4E0A\u4E00\u9801",
169
+ nextPage: "\u4E0B\u4E00\u9801",
170
+ undo: "\u5FA9\u539F",
171
+ undoAvailable: "\u9805\u76EE\u5DF2\u522A\u9664\u3002\u8981\u5FA9\u539F\u55CE\uFF1F",
172
+ selectionScope: "\u9078\u53D6\u7BC4\u570D",
173
+ currentPage: "\u76EE\u524D\u9801\u9762",
174
+ searchResults: "\u76EE\u524D\u641C\u5C0B\u7D50\u679C",
175
+ allItems: "\u6240\u6709\u5DF2\u5132\u5B58\u9805\u76EE",
176
+ tags: "\u6A19\u7C64"
177
+ }
178
+ };
179
+ KEEP_LOCALE_LABELS.zh = KEEP_LOCALE_LABELS["zh-CN"];
65
180
  var KeepUiLabelsContext = createContext({ labels: DEFAULT_LABELS });
66
181
  function KeepUiProvider({ labels, locale, labelResolver, children }) {
67
182
  const value = useMemo(() => {
68
- const resolved = { ...DEFAULT_LABELS, ...labels };
69
- return { locale, labels: resolved, labelResolver };
183
+ const localeKey = locale?.toLowerCase() ?? "en";
184
+ const dictionary = KEEP_LOCALE_LABELS[localeKey] ?? KEEP_LOCALE_LABELS[localeKey.split("-")[0]] ?? {};
185
+ const resolved = { ...DEFAULT_LABELS, ...dictionary, ...labels };
186
+ return { locale, labels: resolved, customLabels: labels, labelResolver };
70
187
  }, [labelResolver, labels, locale]);
71
188
  return /* @__PURE__ */ jsx(KeepUiLabelsContext.Provider, { value, children });
72
189
  }
@@ -75,7 +192,11 @@ function useKeepUiLabels() {
75
192
  }
76
193
  function useUiLabel(key, override) {
77
194
  const context = useKeepUiLabels();
78
- return override ?? context.labelResolver?.(key, { locale: context.locale }) ?? context.labels[key];
195
+ return override ?? context.customLabels?.[key] ?? context.labelResolver?.(key, { locale: context.locale }) ?? context.labels[key];
196
+ }
197
+ function getKeepLocaleLabels(locale) {
198
+ const key = locale?.toLowerCase() ?? "en";
199
+ return { ...KEEP_LOCALE_LABELS[key] ?? KEEP_LOCALE_LABELS[key.split("-")[0]] ?? {} };
79
200
  }
80
201
 
81
202
  // src/KeepBackup.tsx
@@ -265,6 +386,8 @@ function KeepBulkActions({
265
386
  onSelectedIdsChange,
266
387
  renderItem,
267
388
  onCompleted,
389
+ selectionScope: controlledScope,
390
+ onSelectionScopeChange,
268
391
  render,
269
392
  children,
270
393
  ...props
@@ -274,7 +397,20 @@ function KeepBulkActions({
274
397
  const deleteSelectedLabel = useUiLabel("deleteSelected");
275
398
  const tagsLabel = useUiLabel("tagsToApply");
276
399
  const applyTagsLabel = useUiLabel("applyTags");
400
+ const selectionScopeLabel = useUiLabel("selectionScope");
401
+ const currentPageLabel = useUiLabel("currentPage");
402
+ const searchResultsLabel = useUiLabel("searchResults");
403
+ const allItemsLabel = useUiLabel("allItems");
277
404
  const list = useKeepList(query);
405
+ const queryList = useKeepList(query ? { ...query, pagination: void 0 } : { pagination: void 0 });
406
+ const allList = useKeepList({ pagination: void 0 });
407
+ const [uncontrolledScope, setUncontrolledScope] = useState2(controlledScope ?? "page");
408
+ const selectionScope = controlledScope ?? uncontrolledScope;
409
+ const setSelectionScope = (scope) => {
410
+ if (controlledScope === void 0) setUncontrolledScope(scope);
411
+ onSelectionScopeChange?.(scope);
412
+ };
413
+ const targetItems = selectionScope === "page" ? list.items : selectionScope === "query" ? queryList.items : allList.items;
278
414
  const [uncontrolledSelectedIds, setUncontrolledSelectedIds] = useState2(defaultSelectedIds);
279
415
  const selectedIds = controlledSelectedIds ?? uncontrolledSelectedIds;
280
416
  const selected = new Set(selectedIds);
@@ -284,11 +420,11 @@ function KeepBulkActions({
284
420
  onSelectedIdsChange?.(ids);
285
421
  };
286
422
  const toggle = (id) => setSelectedIds(selected.has(id) ? selectedIds.filter((current) => current !== id) : [...selectedIds, id]);
287
- const allSelected = isAllSelected(list.items, selectedIds);
288
- const toggleAll = () => setSelectedIds(toggleSelectAll(list.items, selectedIds));
423
+ const allSelected = isAllSelected(targetItems, selectedIds);
424
+ const toggleAll = () => setSelectedIds(toggleSelectAll(targetItems, selectedIds));
289
425
  const remove = async () => {
290
426
  const ids = [...selectedIds];
291
- await list.removeBatch(ids);
427
+ await list.removeBatchWithUndo(ids);
292
428
  onCompleted?.("remove", ids);
293
429
  setSelectedIds([]);
294
430
  };
@@ -299,7 +435,7 @@ function KeepBulkActions({
299
435
  setSelectedIds([]);
300
436
  };
301
437
  const state = {
302
- items: list.items,
438
+ items: targetItems,
303
439
  selectedIds,
304
440
  selectedCount: selectedIds.length,
305
441
  isAllSelected: allSelected,
@@ -311,18 +447,35 @@ function KeepBulkActions({
311
447
  toggleAll,
312
448
  remove,
313
449
  updateTags,
314
- isMutating: list.isMutating
450
+ isMutating: list.isMutating,
451
+ selectionScope,
452
+ setSelectionScope
315
453
  };
316
454
  const body = render ? render(state) : typeof children === "function" ? children(state) : children ?? /* @__PURE__ */ jsxs2(Fragment, { children: [
317
455
  /* @__PURE__ */ jsxs2("fieldset", { children: [
318
456
  /* @__PURE__ */ jsx5("legend", { children: selectItemsLabel }),
457
+ /* @__PURE__ */ jsxs2("label", { children: [
458
+ selectionScopeLabel,
459
+ /* @__PURE__ */ jsxs2(
460
+ "select",
461
+ {
462
+ value: selectionScope,
463
+ onChange: (event) => setSelectionScope(event.currentTarget.value),
464
+ children: [
465
+ /* @__PURE__ */ jsx5("option", { value: "page", children: currentPageLabel }),
466
+ /* @__PURE__ */ jsx5("option", { value: "query", children: searchResultsLabel }),
467
+ /* @__PURE__ */ jsx5("option", { value: "all", children: allItemsLabel })
468
+ ]
469
+ }
470
+ )
471
+ ] }),
319
472
  /* @__PURE__ */ jsxs2("label", { children: [
320
473
  /* @__PURE__ */ jsx5("input", { type: "checkbox", checked: allSelected, onChange: toggleAll, "aria-label": selectItemsLabel }),
321
474
  selectedIds.length,
322
475
  " ",
323
476
  selectedCountLabel
324
477
  ] }),
325
- list.items.map((item) => /* @__PURE__ */ jsxs2("span", { children: [
478
+ targetItems.map((item) => /* @__PURE__ */ jsxs2("span", { children: [
326
479
  /* @__PURE__ */ jsx5(
327
480
  KeepItemCheckbox,
328
481
  {
@@ -417,6 +570,8 @@ function KeepItemCard({
417
570
  getImageProps,
418
571
  imageComponent: ImageComponent,
419
572
  renderImage,
573
+ renderTags,
574
+ showTags = true,
420
575
  imageAlt,
421
576
  render,
422
577
  children,
@@ -453,6 +608,7 @@ function KeepItemCard({
453
608
  const isAvailable = item.status === void 0 || item.status === "available";
454
609
  const statusLabelKey = item.status && item.status !== "available" ? getStatusLabelKey(item.status) : "statusUnknown";
455
610
  const unavailableLabel = useUiLabel(statusLabelKey);
611
+ const tagsLabel = useUiLabel("tags");
456
612
  const statusLabel = item.status && item.status !== "available" ? unavailableLabel : void 0;
457
613
  function renderLink(content) {
458
614
  if (!href || !isAvailable) return content;
@@ -476,6 +632,7 @@ function KeepItemCard({
476
632
  const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? /* @__PURE__ */ jsxs3(Fragment3, { children: [
477
633
  imageProps ? renderImage?.({ ...imageProps, alt: imageAlt ?? imageProps.alt }, item) ?? (ImageComponent ? /* @__PURE__ */ jsx7(ImageComponent, { ...imageProps, alt: imageAlt ?? imageProps.alt }) : /* @__PURE__ */ jsx7("img", { ...imageProps, alt: imageAlt ?? imageProps.alt })) : null,
478
634
  /* @__PURE__ */ jsx7("h3", { children: linkTarget === "title" ? renderLink(resolvedTitle) : resolvedTitle }),
635
+ showTags && (item.tags?.length ?? 0) > 0 ? renderTags ? renderTags(item.tags ?? [], item) : /* @__PURE__ */ jsx7("ul", { "aria-label": tagsLabel, children: item.tags?.map((tag) => /* @__PURE__ */ jsx7("li", { children: tag }, tag)) }) : null,
479
636
  statusLabel ? /* @__PURE__ */ jsx7("p", { "data-status": item.status, children: statusLabel }) : null,
480
637
  showSaveButton ? /* @__PURE__ */ jsx7(
481
638
  KeepButton,
@@ -534,6 +691,7 @@ function KeepListContent({
534
691
  empty,
535
692
  error: errorContent,
536
693
  itemCardProps,
694
+ layout = "list",
537
695
  asChild = false,
538
696
  className,
539
697
  ...rootProps
@@ -548,7 +706,8 @@ function KeepListContent({
548
706
  loading: loading ?? defaultLoading,
549
707
  empty: empty ?? defaultEmpty,
550
708
  error: errorContent ?? defaultError,
551
- itemCardProps
709
+ itemCardProps,
710
+ layout
552
711
  });
553
712
  return renderRoot(
554
713
  asChild,
@@ -576,9 +735,16 @@ function getListBody(state, options) {
576
735
  if (state.isHydrated && state.items.length === 0) return resolveContent(options.empty, state);
577
736
  if (typeof options.children === "function") return options.children(state);
578
737
  if (options.children !== void 0 && !isValidElement3(options.children)) return options.children;
579
- return /* @__PURE__ */ jsx8("ul", { children: state.items.map(
580
- (item) => options.renderItem ? options.renderItem(item, state) : /* @__PURE__ */ jsx8("li", { children: /* @__PURE__ */ jsx8(KeepItemCard, { item, ...options.itemCardProps }) }, item.id)
581
- ) });
738
+ return /* @__PURE__ */ jsx8(
739
+ "ul",
740
+ {
741
+ "data-layout": options.layout,
742
+ style: options.layout === "grid" ? { display: "grid", gap: "1rem", gridTemplateColumns: "repeat(auto-fit, minmax(16rem, 1fr))" } : options.layout === "compact" ? { display: "grid", gap: "0.5rem", gridTemplateColumns: "repeat(auto-fit, minmax(12rem, 1fr))" } : { display: "flex", flexDirection: "column", gap: "1rem" },
743
+ children: state.items.map(
744
+ (item) => options.renderItem ? options.renderItem(item, state) : /* @__PURE__ */ jsx8("li", { children: /* @__PURE__ */ jsx8(KeepItemCard, { item, ...options.itemCardProps }) }, item.id)
745
+ )
746
+ }
747
+ );
582
748
  }
583
749
 
584
750
  // src/KeepTagFilter.tsx
@@ -776,6 +942,88 @@ function getVisiblePages(currentPage, pageCount, maxPageButtons) {
776
942
  return Array.from({ length: maxPageButtons }, (_, index) => start + index);
777
943
  }
778
944
 
945
+ // src/url-sync.tsx
946
+ import {
947
+ DEFAULT_KEEP_URL_PARAMS,
948
+ decodeKeepListQuery,
949
+ encodeKeepListQuery
950
+ } from "@keepkit/core/core";
951
+ import { useEffect as useEffect2, useRef as useRef2 } from "react";
952
+ function createNextPagesRouterAdapter(router) {
953
+ const getUrl = () => router.asPath ?? (typeof window === "undefined" ? "/" : window.location.href);
954
+ return {
955
+ getUrl,
956
+ subscribe: router.events ? (listener) => {
957
+ router.events?.on("routeChangeComplete", listener);
958
+ return () => router.events?.off("routeChangeComplete", listener);
959
+ } : void 0,
960
+ navigate: (url, mode) => {
961
+ void router[mode](url, void 0, { shallow: true });
962
+ }
963
+ };
964
+ }
965
+ function useKeepUrlSync({
966
+ enabled = true,
967
+ query,
968
+ onQueryChange,
969
+ options = {},
970
+ adapter: providedAdapter
971
+ }) {
972
+ const browserAdapterRef = useRef2(getBrowserAdapter());
973
+ const adapter = providedAdapter ?? browserAdapterRef.current;
974
+ const onQueryChangeRef = useRef2(onQueryChange);
975
+ onQueryChangeRef.current = onQueryChange;
976
+ const skipWriteRef = useRef2(true);
977
+ const params = options.params;
978
+ useEffect2(() => {
979
+ if (!enabled) return;
980
+ const read = () => {
981
+ const url = adapter.getUrl();
982
+ const decoded = decodeKeepListQuery(url, { params });
983
+ skipWriteRef.current = true;
984
+ onQueryChangeRef.current((previousQuery) => ({
985
+ ...previousQuery,
986
+ ...decoded.search ? { search: decoded.search } : { search: void 0 },
987
+ ...decoded.tags ? { tags: decoded.tags } : { tags: void 0 },
988
+ ...decoded.sort ? { sort: decoded.sort } : {},
989
+ ...decoded.pagination ? { pagination: { ...previousQuery.pagination, ...decoded.pagination } } : {}
990
+ }));
991
+ };
992
+ read();
993
+ return adapter.subscribe?.(read);
994
+ }, [adapter, enabled, params]);
995
+ useEffect2(() => {
996
+ if (!enabled) return;
997
+ if (skipWriteRef.current) {
998
+ skipWriteRef.current = false;
999
+ return;
1000
+ }
1001
+ const currentUrl = new URL(adapter.getUrl(), "http://keepkit.invalid");
1002
+ const urlParams = { ...DEFAULT_KEEP_URL_PARAMS, ...params };
1003
+ for (const key of Object.values(urlParams)) currentUrl.searchParams.delete(key);
1004
+ const nextParams = encodeKeepListQuery(query, { params });
1005
+ nextParams.forEach((value, key) => {
1006
+ currentUrl.searchParams.append(key, value);
1007
+ });
1008
+ const nextUrl = `${currentUrl.pathname}${currentUrl.search}${currentUrl.hash}`;
1009
+ adapter.navigate(nextUrl, options.history ?? "push");
1010
+ }, [adapter, enabled, options.history, params, query]);
1011
+ }
1012
+ function getBrowserAdapter() {
1013
+ return {
1014
+ getUrl: () => typeof window === "undefined" ? "/" : window.location.href,
1015
+ subscribe: (listener) => {
1016
+ if (typeof window === "undefined") return () => void 0;
1017
+ window.addEventListener("popstate", listener);
1018
+ return () => window.removeEventListener("popstate", listener);
1019
+ },
1020
+ navigate: (url, mode) => {
1021
+ if (typeof window === "undefined") return;
1022
+ window.history[mode === "push" ? "pushState" : "replaceState"]({}, "", url);
1023
+ }
1024
+ };
1025
+ }
1026
+
779
1027
  // src/KeepCollection.tsx
780
1028
  import { jsx as jsx11, jsxs as jsxs6 } from "react/jsx-runtime";
781
1029
  function KeepCollection(props) {
@@ -787,6 +1035,9 @@ function KeepCollection(props) {
787
1035
  function KeepCollectionContent({
788
1036
  query = {},
789
1037
  pageSize = 20,
1038
+ layout = "list",
1039
+ urlSync = false,
1040
+ urlAdapter,
790
1041
  features,
791
1042
  renderItem,
792
1043
  itemCardProps,
@@ -819,6 +1070,19 @@ function KeepCollectionContent({
819
1070
  }),
820
1071
  [enabled.pagination, enabled.search, enabled.sort, page, query, resolvedPageSize, searchValue, sort, tag]
821
1072
  );
1073
+ useKeepUrlSync({
1074
+ enabled: Boolean(urlSync),
1075
+ query: resolvedQuery,
1076
+ onQueryChange: (nextOrUpdater) => {
1077
+ const next = typeof nextOrUpdater === "function" ? nextOrUpdater(resolvedQuery) : nextOrUpdater;
1078
+ setSearchValue(next.search?.query ?? "");
1079
+ setSort(next.sort ?? { by: "updatedAt", direction: "desc" });
1080
+ setTag(next.tags?.[0]);
1081
+ setPage(next.pagination?.page ?? 1);
1082
+ },
1083
+ options: typeof urlSync === "object" ? urlSync : {},
1084
+ adapter: urlAdapter
1085
+ });
822
1086
  const list = useKeepList4(resolvedQuery);
823
1087
  return /* @__PURE__ */ jsxs6(
824
1088
  "section",
@@ -868,6 +1132,7 @@ function KeepCollectionContent({
868
1132
  query: resolvedQuery,
869
1133
  renderItem,
870
1134
  itemCardProps,
1135
+ layout,
871
1136
  loading,
872
1137
  empty,
873
1138
  error
@@ -894,16 +1159,23 @@ function getCollectionState(list) {
894
1159
  return "ready";
895
1160
  }
896
1161
 
1162
+ // src/KeepLayout.tsx
1163
+ import { jsx as jsx12 } from "react/jsx-runtime";
1164
+ function KeepLayout({ layout = "list", children, style, ...props }) {
1165
+ const layoutStyle = layout === "grid" ? { display: "grid", gap: "1rem", gridTemplateColumns: "repeat(auto-fit, minmax(16rem, 1fr))" } : layout === "compact" ? { display: "grid", gap: "0.5rem", gridTemplateColumns: "repeat(auto-fit, minmax(12rem, 1fr))" } : { display: "flex", flexDirection: "column", gap: "1rem" };
1166
+ return /* @__PURE__ */ jsx12("div", { ...props, style: { ...layoutStyle, ...style }, "data-layout": layout, children });
1167
+ }
1168
+
897
1169
  // src/KeepNoteEditor.tsx
898
1170
  import { useKeepItem as useKeepItem3 } from "@keepkit/core/react";
899
1171
  import {
900
1172
  isValidElement as isValidElement5,
901
1173
  useCallback as useCallback2,
902
- useEffect as useEffect2,
903
- useRef as useRef2,
1174
+ useEffect as useEffect3,
1175
+ useRef as useRef3,
904
1176
  useState as useState6
905
1177
  } from "react";
906
- import { Fragment as Fragment5, jsx as jsx12, jsxs as jsxs7 } from "react/jsx-runtime";
1178
+ import { Fragment as Fragment5, jsx as jsx13, jsxs as jsxs7 } from "react/jsx-runtime";
907
1179
  function KeepNoteEditor({
908
1180
  item,
909
1181
  label,
@@ -926,8 +1198,8 @@ function KeepNoteEditor({
926
1198
  const [note, setNote] = useState6(item.note ?? "");
927
1199
  const baselineNote = savedItem?.note ?? item.note ?? "";
928
1200
  const isDirty = note !== baselineNote;
929
- const lastSavedNoteRef = useRef2(void 0);
930
- useEffect2(() => setNote(baselineNote), [baselineNote]);
1201
+ const lastSavedNoteRef = useRef3(void 0);
1202
+ useEffect3(() => setNote(baselineNote), [baselineNote]);
931
1203
  const save = useCallback2(async () => {
932
1204
  const nextNote = note.trim() || void 0;
933
1205
  try {
@@ -939,7 +1211,7 @@ function KeepNoteEditor({
939
1211
  throw error2;
940
1212
  }
941
1213
  }, [note, onSaveError, onSaved, updateNote]);
942
- useEffect2(() => {
1214
+ useEffect3(() => {
943
1215
  if (!isDirty || debounceMs <= 0 || lastSavedNoteRef.current === note) return;
944
1216
  const timer = window.setTimeout(() => void save().catch(() => void 0), debounceMs);
945
1217
  return () => window.clearTimeout(timer);
@@ -956,7 +1228,7 @@ function KeepNoteEditor({
956
1228
  const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? /* @__PURE__ */ jsxs7(Fragment5, { children: [
957
1229
  /* @__PURE__ */ jsxs7("label", { children: [
958
1230
  label ?? defaultLabel,
959
- /* @__PURE__ */ jsx12(
1231
+ /* @__PURE__ */ jsx13(
960
1232
  "textarea",
961
1233
  {
962
1234
  value: note,
@@ -972,14 +1244,14 @@ function KeepNoteEditor({
972
1244
  }
973
1245
  )
974
1246
  ] }),
975
- /* @__PURE__ */ jsx12("button", { type: "submit", disabled: isMutating, "aria-busy": isMutating, children: saveLabel ?? defaultSaveLabel })
1247
+ /* @__PURE__ */ jsx13("button", { type: "submit", disabled: isMutating, "aria-busy": isMutating, children: saveLabel ?? defaultSaveLabel })
976
1248
  ] });
977
1249
  const handleSubmit = (event) => {
978
1250
  event.preventDefault();
979
1251
  void save().catch(() => void 0);
980
1252
  };
981
1253
  if (!asChild) {
982
- return /* @__PURE__ */ jsx12(
1254
+ return /* @__PURE__ */ jsx13(
983
1255
  "form",
984
1256
  {
985
1257
  ...formProps,
@@ -1012,8 +1284,8 @@ function KeepNoteEditor({
1012
1284
 
1013
1285
  // src/KeepTagEditor.tsx
1014
1286
  import { useKeepItem as useKeepItem4 } from "@keepkit/core/react";
1015
- import { useCallback as useCallback3, useEffect as useEffect3, useState as useState7 } from "react";
1016
- import { Fragment as Fragment6, jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
1287
+ import { useCallback as useCallback3, useEffect as useEffect4, useState as useState7 } from "react";
1288
+ import { Fragment as Fragment6, jsx as jsx14, jsxs as jsxs8 } from "react/jsx-runtime";
1017
1289
  function KeepTagEditor({
1018
1290
  item,
1019
1291
  availableTags = [],
@@ -1028,7 +1300,7 @@ function KeepTagEditor({
1028
1300
  const itemState = useKeepItem4(item);
1029
1301
  const [tags, setTags] = useState7(item.tags ?? []);
1030
1302
  const [input, setInput] = useState7("");
1031
- useEffect3(() => setTags(itemState.item?.tags ?? item.tags ?? []), [item.tags, itemState.item?.tags]);
1303
+ useEffect4(() => setTags(itemState.item?.tags ?? item.tags ?? []), [item.tags, itemState.item?.tags]);
1032
1304
  const save = useCallback3(async () => {
1033
1305
  const nextTags = normalizeUiTags(tags);
1034
1306
  try {
@@ -1047,7 +1319,7 @@ function KeepTagEditor({
1047
1319
  const body = render ? render({ tags, setTags, save, isSaving: itemState.isMutating }) : /* @__PURE__ */ jsxs8(Fragment6, { children: [
1048
1320
  /* @__PURE__ */ jsxs8("label", { children: [
1049
1321
  tagsLabel,
1050
- /* @__PURE__ */ jsx13(
1322
+ /* @__PURE__ */ jsx14(
1051
1323
  "input",
1052
1324
  {
1053
1325
  value: input,
@@ -1066,14 +1338,14 @@ function KeepTagEditor({
1066
1338
  }
1067
1339
  )
1068
1340
  ] }),
1069
- availableTags.length > 0 ? /* @__PURE__ */ jsx13("datalist", { id: `keep-tags-${item.id}`, children: availableTags.map((tag) => /* @__PURE__ */ jsx13("option", { value: tag }, tag)) }) : null,
1070
- /* @__PURE__ */ jsx13("ul", { "aria-label": tagsLabel, children: tags.map((tag) => /* @__PURE__ */ jsxs8("li", { children: [
1341
+ availableTags.length > 0 ? /* @__PURE__ */ jsx14("datalist", { id: `keep-tags-${item.id}`, children: availableTags.map((tag) => /* @__PURE__ */ jsx14("option", { value: tag }, tag)) }) : null,
1342
+ /* @__PURE__ */ jsx14("ul", { "aria-label": tagsLabel, children: tags.map((tag) => /* @__PURE__ */ jsxs8("li", { children: [
1071
1343
  tag,
1072
- /* @__PURE__ */ jsx13("button", { type: "button", onClick: () => setTags(tags.filter((current) => current !== tag)), children: removeLabel })
1344
+ /* @__PURE__ */ jsx14("button", { type: "button", onClick: () => setTags(tags.filter((current) => current !== tag)), children: removeLabel })
1073
1345
  ] }, tag)) }),
1074
- /* @__PURE__ */ jsx13("button", { type: "submit", disabled: itemState.isMutating, "aria-busy": itemState.isMutating, children: applyTagsLabel })
1346
+ /* @__PURE__ */ jsx14("button", { type: "submit", disabled: itemState.isMutating, "aria-busy": itemState.isMutating, children: applyTagsLabel })
1075
1347
  ] });
1076
- return /* @__PURE__ */ jsx13(
1348
+ return /* @__PURE__ */ jsx14(
1077
1349
  "form",
1078
1350
  {
1079
1351
  ...props,
@@ -1090,10 +1362,24 @@ function KeepTagEditor({
1090
1362
  );
1091
1363
  }
1092
1364
 
1093
- // src/status.tsx
1365
+ // src/KeepUndo.tsx
1094
1366
  import { useKeepContext as useKeepContext2 } from "@keepkit/core/react";
1095
- import { isValidElement as isValidElement6, useEffect as useEffect4, useRef as useRef3, useState as useState8 } from "react";
1096
- import { Fragment as Fragment7, jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
1367
+ import { jsx as jsx15, jsxs as jsxs9 } from "react/jsx-runtime";
1368
+ function KeepUndo({ children, label, ...props }) {
1369
+ const context = useKeepContext2();
1370
+ const message = useUiLabel("undoAvailable");
1371
+ const undoLabel = useUiLabel("undo");
1372
+ if (!context.undo.canUndo) return null;
1373
+ return /* @__PURE__ */ jsxs9("div", { ...props, role: "status", "aria-live": "polite", "data-state": "available", children: [
1374
+ children ?? message,
1375
+ /* @__PURE__ */ jsx15("button", { type: "button", onClick: () => void context.undoLastRemoval(), children: label ?? undoLabel })
1376
+ ] });
1377
+ }
1378
+
1379
+ // src/status.tsx
1380
+ import { useKeepContext as useKeepContext3 } from "@keepkit/core/react";
1381
+ import { isValidElement as isValidElement6, useEffect as useEffect5, useRef as useRef4, useState as useState8 } from "react";
1382
+ import { Fragment as Fragment7, jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
1097
1383
  function KeepEmptyState({
1098
1384
  title,
1099
1385
  description,
@@ -1105,9 +1391,9 @@ function KeepEmptyState({
1105
1391
  }) {
1106
1392
  const defaultTitle = useUiLabel("noItems").replace(/\.$/, "");
1107
1393
  const contentChildren = asChild && isValidElement6(children) ? void 0 : children;
1108
- const body = contentChildren ?? /* @__PURE__ */ jsxs9(Fragment7, { children: [
1109
- /* @__PURE__ */ jsx14("h2", { children: title ?? defaultTitle }),
1110
- description ? /* @__PURE__ */ jsx14("p", { children: description }) : null,
1394
+ const body = contentChildren ?? /* @__PURE__ */ jsxs10(Fragment7, { children: [
1395
+ /* @__PURE__ */ jsx16("h2", { children: title ?? defaultTitle }),
1396
+ description ? /* @__PURE__ */ jsx16("p", { children: description }) : null,
1111
1397
  action
1112
1398
  ] });
1113
1399
  return renderRoot(asChild, children, { ...rootProps, className, "data-state": "empty" }, body, "KeepEmptyState");
@@ -1121,7 +1407,7 @@ function KeepStatus({
1121
1407
  className,
1122
1408
  ...rootProps
1123
1409
  }) {
1124
- const context = useKeepContext2();
1410
+ const context = useKeepContext3();
1125
1411
  const contentChildren = asChild && isValidElement6(children) ? void 0 : children;
1126
1412
  const resolvedStatus = status ?? getDerivedStatus(context);
1127
1413
  const defaultLabel = useUiLabel(getStatusLabelKey2(resolvedStatus));
@@ -1149,13 +1435,13 @@ function KeepStatus({
1149
1435
  );
1150
1436
  }
1151
1437
  function KeepAnnouncements({ messages, ...props }) {
1152
- const context = useKeepContext2();
1438
+ const context = useKeepContext3();
1153
1439
  const savedMessage = useUiLabel("savedMessage", messages?.save);
1154
1440
  const removedMessage = useUiLabel("removedMessage", messages?.remove);
1155
1441
  const noteSavedMessage = useUiLabel("noteSavedMessage", messages?.note);
1156
1442
  const [message, setMessage] = useState8("");
1157
- const lastChangeRef = useRef3(void 0);
1158
- useEffect4(() => {
1443
+ const lastChangeRef = useRef4(void 0);
1444
+ useEffect5(() => {
1159
1445
  const change = context.lastChange;
1160
1446
  if (!change || change === lastChangeRef.current) return;
1161
1447
  lastChangeRef.current = change;
@@ -1163,7 +1449,7 @@ function KeepAnnouncements({ messages, ...props }) {
1163
1449
  else if (change.action === "remove" || change.action === "removeBatch") setMessage(removedMessage);
1164
1450
  else if (change.action === "updateNote") setMessage(noteSavedMessage);
1165
1451
  }, [context.lastChange, noteSavedMessage, removedMessage, savedMessage]);
1166
- return /* @__PURE__ */ jsx14(
1452
+ return /* @__PURE__ */ jsx16(
1167
1453
  "div",
1168
1454
  {
1169
1455
  ...props,
@@ -1197,7 +1483,7 @@ function getStatusLabelKey2(status) {
1197
1483
  import {
1198
1484
  KeepErrorBoundary as KeepErrorBoundary3,
1199
1485
  KeepProvider,
1200
- useKeepContext as useKeepContext3,
1486
+ useKeepContext as useKeepContext4,
1201
1487
  useKeepItem as useKeepItem5,
1202
1488
  useKeepList as useKeepList5,
1203
1489
  useKeepShortcut
@@ -1212,7 +1498,7 @@ import {
1212
1498
  LocalStorageSyncQueueAdapter,
1213
1499
  SyncStorageAdapter
1214
1500
  } from "@keepkit/core/storage";
1215
- import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
1501
+ import { jsx as jsx17, jsxs as jsxs11 } from "react/jsx-runtime";
1216
1502
  function KeepKitProvider({
1217
1503
  labels,
1218
1504
  locale,
@@ -1220,16 +1506,16 @@ function KeepKitProvider({
1220
1506
  children,
1221
1507
  ...providerProps
1222
1508
  }) {
1223
- return /* @__PURE__ */ jsx15(KeepUiProvider, { labels, locale, labelResolver, children: /* @__PURE__ */ jsxs10(CoreKeepProvider, { ...providerProps, children: [
1509
+ return /* @__PURE__ */ jsx17(KeepUiProvider, { labels, locale, labelResolver, children: /* @__PURE__ */ jsxs11(CoreKeepProvider, { ...providerProps, children: [
1224
1510
  children,
1225
- /* @__PURE__ */ jsx15(KeepAnnouncements, {})
1511
+ /* @__PURE__ */ jsx17(KeepAnnouncements, {})
1226
1512
  ] }) });
1227
1513
  }
1228
1514
  function createKeepKit(options = {}) {
1229
1515
  const { labels, locale, labelResolver, getTitle, getImageProps, ...coreOptions } = options;
1230
1516
  const coreKit = createCoreKeepKit(coreOptions);
1231
1517
  return {
1232
- Provider: (props) => /* @__PURE__ */ jsx15(
1518
+ Provider: (props) => /* @__PURE__ */ jsx17(
1233
1519
  KeepKitProvider,
1234
1520
  {
1235
1521
  ...coreOptions,
@@ -1239,9 +1525,9 @@ function createKeepKit(options = {}) {
1239
1525
  ...props
1240
1526
  }
1241
1527
  ),
1242
- Button: (props) => /* @__PURE__ */ jsx15(KeepButton, { ...props }),
1243
- Backup: (props) => /* @__PURE__ */ jsx15(KeepBackup, { ...props }),
1244
- Collection: (props) => /* @__PURE__ */ jsx15(
1528
+ Button: (props) => /* @__PURE__ */ jsx17(KeepButton, { ...props }),
1529
+ Backup: (props) => /* @__PURE__ */ jsx17(KeepBackup, { ...props }),
1530
+ Collection: (props) => /* @__PURE__ */ jsx17(
1245
1531
  KeepCollection,
1246
1532
  {
1247
1533
  ...props,
@@ -1273,6 +1559,7 @@ export {
1273
1559
  KeepItemCard,
1274
1560
  KeepItemCheckbox,
1275
1561
  KeepKitProvider,
1562
+ KeepLayout,
1276
1563
  KeepList,
1277
1564
  KeepNoteEditor,
1278
1565
  KeepPagination,
@@ -1283,18 +1570,22 @@ export {
1283
1570
  KeepTagEditor,
1284
1571
  KeepTagFilter,
1285
1572
  KeepUiProvider,
1573
+ KeepUndo,
1286
1574
  LocalStorageAdapter,
1287
1575
  LocalStorageSyncQueueAdapter,
1288
1576
  SyncStorageAdapter,
1289
1577
  createBrowserStorageAdapter,
1290
1578
  createKeepKit,
1579
+ createNextPagesRouterAdapter,
1291
1580
  createStorageAdapter,
1581
+ getKeepLocaleLabels,
1292
1582
  isAllSelected,
1293
1583
  toggleSelectAll,
1294
- useKeepContext3 as useKeepContext,
1584
+ useKeepContext4 as useKeepContext,
1295
1585
  useKeepItem5 as useKeepItem,
1296
1586
  useKeepList5 as useKeepList,
1297
1587
  useKeepShortcut,
1298
- useKeepUiLabels
1588
+ useKeepUiLabels,
1589
+ useKeepUrlSync
1299
1590
  };
1300
1591
  //# sourceMappingURL=index.js.map