@keepkit/ui 0.4.0 → 0.6.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
@@ -3,23 +3,57 @@
3
3
  // src/index.tsx
4
4
  import {
5
5
  KeepButton as CoreKeepButton,
6
+ KeepProvider as CoreKeepProvider,
7
+ createKeepKit as createCoreKeepKit,
6
8
  useKeepContext,
7
9
  useKeepItem,
8
10
  useKeepList
9
11
  } from "@keepkit/core/react";
10
12
  import {
11
13
  cloneElement,
12
- createContext,
13
14
  isValidElement,
14
15
  useCallback,
15
- useContext,
16
- useEffect,
17
- useMemo,
16
+ useEffect as useEffect2,
17
+ useMemo as useMemo2,
18
18
  useRef,
19
- useState
19
+ useState as useState2
20
20
  } from "react";
21
- import { KeepProvider, useKeepContext as useKeepContext2, useKeepItem as useKeepItem2, useKeepList as useKeepList2 } from "@keepkit/core/react";
22
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
21
+
22
+ // src/KeepItemCheckbox.tsx
23
+ import { jsx } from "react/jsx-runtime";
24
+ function KeepItemCheckbox({
25
+ item,
26
+ checked = false,
27
+ label,
28
+ onCheckedChange,
29
+ "aria-label": ariaLabel,
30
+ ...props
31
+ }) {
32
+ const itemLabel = getItemLabel(item) ?? item.id;
33
+ const accessibleLabel = ariaLabel ?? (typeof label === "string" ? label : itemLabel);
34
+ return /* @__PURE__ */ jsx(
35
+ "input",
36
+ {
37
+ ...props,
38
+ type: "checkbox",
39
+ checked,
40
+ "aria-label": accessibleLabel,
41
+ onChange: (event) => onCheckedChange?.(event.currentTarget.checked)
42
+ }
43
+ );
44
+ }
45
+ function getItemLabel(item) {
46
+ if (typeof item.meta !== "object" || item.meta === null || !("title" in item.meta)) return void 0;
47
+ const title = item.meta.title;
48
+ return typeof title === "string" && title.trim() ? title.trim() : void 0;
49
+ }
50
+
51
+ // src/query-controls.tsx
52
+ import { useEffect, useState } from "react";
53
+
54
+ // src/ui-context.tsx
55
+ import { createContext, useContext, useMemo } from "react";
56
+ import { jsx as jsx2 } from "react/jsx-runtime";
23
57
  var DEFAULT_LABELS = {
24
58
  save: "Save",
25
59
  saved: "Saved",
@@ -39,6 +73,10 @@ var DEFAULT_LABELS = {
39
73
  sort: "Sort saved items",
40
74
  newest: "Newest first",
41
75
  oldest: "Oldest first",
76
+ savedNewest: "Saved newest first",
77
+ savedOldest: "Saved oldest first",
78
+ updatedNewest: "Updated newest first",
79
+ updatedOldest: "Updated oldest first",
42
80
  previousPage: "Previous page",
43
81
  nextPage: "Next page",
44
82
  pagination: "Pagination",
@@ -58,7 +96,7 @@ function KeepUiProvider({ labels, locale, labelResolver, children }) {
58
96
  const resolved = { ...DEFAULT_LABELS, ...labels };
59
97
  return { locale, labels: resolved, labelResolver };
60
98
  }, [labelResolver, labels, locale]);
61
- return /* @__PURE__ */ jsx(KeepUiLabelsContext.Provider, { value, children });
99
+ return /* @__PURE__ */ jsx2(KeepUiLabelsContext.Provider, { value, children });
62
100
  }
63
101
  function useKeepUiLabels() {
64
102
  return useContext(KeepUiLabelsContext);
@@ -67,17 +105,190 @@ function useUiLabel(key, override) {
67
105
  const context = useKeepUiLabels();
68
106
  return override ?? context.labelResolver?.(key, { locale: context.locale }) ?? context.labels[key];
69
107
  }
108
+
109
+ // src/query-controls.tsx
110
+ import { Fragment, jsx as jsx3, jsxs } from "react/jsx-runtime";
111
+ function KeepSearchInput({
112
+ value: controlledValue,
113
+ defaultValue = "",
114
+ debounceMs = 300,
115
+ onValueChange,
116
+ "aria-label": ariaLabel,
117
+ placeholder,
118
+ ...props
119
+ }) {
120
+ const label = useUiLabel("search");
121
+ const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
122
+ const value = controlledValue ?? uncontrolledValue;
123
+ useEffect(() => {
124
+ if (!onValueChange) return;
125
+ if (debounceMs <= 0) {
126
+ onValueChange(value);
127
+ return;
128
+ }
129
+ const timer = window.setTimeout(() => onValueChange(value), debounceMs);
130
+ return () => window.clearTimeout(timer);
131
+ }, [debounceMs, onValueChange, value]);
132
+ return /* @__PURE__ */ jsx3(
133
+ "input",
134
+ {
135
+ ...props,
136
+ type: "search",
137
+ value,
138
+ "aria-label": ariaLabel ?? label,
139
+ placeholder: placeholder ?? label,
140
+ onChange: (event) => {
141
+ const nextValue = event.currentTarget.value;
142
+ if (controlledValue === void 0) setUncontrolledValue(nextValue);
143
+ }
144
+ }
145
+ );
146
+ }
147
+ function KeepSortSelect({
148
+ value: controlledValue,
149
+ defaultValue = "updatedAt:desc",
150
+ onValueChange,
151
+ "aria-label": ariaLabel,
152
+ children,
153
+ ...props
154
+ }) {
155
+ const label = useUiLabel("sort");
156
+ const updatedNewestLabel = useUiLabel("updatedNewest");
157
+ const updatedOldestLabel = useUiLabel("updatedOldest");
158
+ const savedNewestLabel = useUiLabel("savedNewest");
159
+ const savedOldestLabel = useUiLabel("savedOldest");
160
+ const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
161
+ const value = controlledValue ?? uncontrolledValue;
162
+ const options = children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
163
+ /* @__PURE__ */ jsx3("option", { value: "updatedAt:desc", children: updatedNewestLabel }),
164
+ /* @__PURE__ */ jsx3("option", { value: "updatedAt:asc", children: updatedOldestLabel }),
165
+ /* @__PURE__ */ jsx3("option", { value: "savedAt:desc", children: savedNewestLabel }),
166
+ /* @__PURE__ */ jsx3("option", { value: "savedAt:asc", children: savedOldestLabel })
167
+ ] });
168
+ return /* @__PURE__ */ jsx3(
169
+ "select",
170
+ {
171
+ ...props,
172
+ value,
173
+ "aria-label": ariaLabel ?? label,
174
+ onChange: (event) => {
175
+ const nextValue = event.currentTarget.value;
176
+ if (controlledValue === void 0) setUncontrolledValue(nextValue);
177
+ const [by, direction] = nextValue.split(":");
178
+ onValueChange?.(nextValue, { by, direction });
179
+ },
180
+ children: options
181
+ }
182
+ );
183
+ }
184
+ function KeepPagination({
185
+ totalCount,
186
+ pageSize,
187
+ page = 1,
188
+ maxPageButtons = 7,
189
+ onPageChange,
190
+ render,
191
+ ...props
192
+ }) {
193
+ const previousPageLabel = useUiLabel("previousPage");
194
+ const nextPageLabel = useUiLabel("nextPage");
195
+ const pageLabel = useUiLabel("page");
196
+ const paginationLabel = useUiLabel("pagination");
197
+ const pageCount = Math.max(1, Math.ceil(totalCount / Math.max(1, pageSize)));
198
+ const currentPage = Math.min(Math.max(1, page), pageCount);
199
+ const goToPage = (nextPage) => {
200
+ const next = Math.min(Math.max(1, nextPage), pageCount);
201
+ onPageChange?.(next, (next - 1) * pageSize);
202
+ };
203
+ const navProps = { ...props, "aria-label": props["aria-label"] ?? paginationLabel };
204
+ if (render) return /* @__PURE__ */ jsx3("nav", { ...navProps, children: render({ page: currentPage, pageCount, goToPage }) });
205
+ const visiblePages = getVisiblePages(currentPage, pageCount, Math.max(1, maxPageButtons));
206
+ return /* @__PURE__ */ jsxs("nav", { ...navProps, children: [
207
+ /* @__PURE__ */ jsx3("button", { type: "button", onClick: () => goToPage(currentPage - 1), disabled: currentPage <= 1, children: previousPageLabel }),
208
+ visiblePages.map((nextPage) => /* @__PURE__ */ jsx3(
209
+ "button",
210
+ {
211
+ type: "button",
212
+ "aria-current": nextPage === currentPage ? "page" : void 0,
213
+ "aria-label": `${pageLabel} ${nextPage}`,
214
+ onClick: () => goToPage(nextPage),
215
+ children: nextPage
216
+ },
217
+ nextPage
218
+ )),
219
+ /* @__PURE__ */ jsx3("button", { type: "button", onClick: () => goToPage(currentPage + 1), disabled: currentPage >= pageCount, children: nextPageLabel })
220
+ ] });
221
+ }
222
+ function getVisiblePages(currentPage, pageCount, maxPageButtons) {
223
+ if (pageCount <= maxPageButtons) return Array.from({ length: pageCount }, (_, index) => index + 1);
224
+ const half = Math.floor(maxPageButtons / 2);
225
+ const start = Math.min(Math.max(1, currentPage - half), pageCount - maxPageButtons + 1);
226
+ return Array.from({ length: maxPageButtons }, (_, index) => start + index);
227
+ }
228
+
229
+ // src/index.tsx
230
+ import { KeepProvider, useKeepContext as useKeepContext2, useKeepItem as useKeepItem2, useKeepList as useKeepList2, useKeepShortcut } from "@keepkit/core/react";
231
+ import {
232
+ createBrowserStorageAdapter,
233
+ createStorageAdapter,
234
+ FallbackStorageAdapter,
235
+ IndexedDBAdapter,
236
+ IndexedDBSyncQueueAdapter,
237
+ LocalStorageAdapter,
238
+ LocalStorageSyncQueueAdapter,
239
+ SyncStorageAdapter
240
+ } from "@keepkit/core/storage";
241
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
242
+ function KeepKitProvider({
243
+ labels,
244
+ locale,
245
+ labelResolver,
246
+ children,
247
+ ...providerProps
248
+ }) {
249
+ return /* @__PURE__ */ jsx4(KeepUiProvider, { labels, locale, labelResolver, children: /* @__PURE__ */ jsxs2(CoreKeepProvider, { ...providerProps, children: [
250
+ children,
251
+ /* @__PURE__ */ jsx4(KeepAnnouncements, {})
252
+ ] }) });
253
+ }
254
+ function createKeepKit(options = {}) {
255
+ const { labels, locale, labelResolver, getTitle, getImageProps, ...coreOptions } = options;
256
+ const coreKit = createCoreKeepKit(coreOptions);
257
+ return {
258
+ Provider: (props) => /* @__PURE__ */ jsx4(
259
+ KeepKitProvider,
260
+ {
261
+ ...coreOptions,
262
+ labels,
263
+ locale,
264
+ labelResolver,
265
+ ...props
266
+ }
267
+ ),
268
+ Button: (props) => /* @__PURE__ */ jsx4(KeepButton, { ...props }),
269
+ Collection: (props) => /* @__PURE__ */ jsx4(
270
+ KeepCollection,
271
+ {
272
+ ...props,
273
+ itemCardProps: {
274
+ ...getTitle ? { getTitle } : {},
275
+ ...getImageProps ? { getImageProps } : {},
276
+ ...props.itemCardProps
277
+ }
278
+ }
279
+ ),
280
+ useContext: () => coreKit.useContext(),
281
+ useItem: (item) => coreKit.useItem(item),
282
+ useList: (query) => coreKit.useList(query),
283
+ useShortcut: (shortcutOptions) => coreKit.useShortcut(shortcutOptions)
284
+ };
285
+ }
70
286
  function KeepButton({ labels, ...props }) {
71
287
  const saveLabel = useUiLabel("save", typeof labels?.unsaved === "string" ? labels.unsaved : void 0);
72
288
  const savedLabel = useUiLabel("saved", typeof labels?.saved === "string" ? labels.saved : void 0);
73
289
  const loadingLabel = useUiLabel("loading", typeof labels?.loading === "string" ? labels.loading : void 0);
74
290
  const errorLabel = useUiLabel("error", typeof labels?.error === "string" ? labels.error : void 0);
75
- const buttonState = useKeepItem(props.item.id, {
76
- meta: props.item.meta,
77
- targetType: props.item.targetType,
78
- note: props.item.note,
79
- tags: props.item.tags
80
- });
291
+ const buttonState = useKeepItem(props.item);
81
292
  const customStateLabel = labels?.loading !== void 0 || labels?.error !== void 0;
82
293
  const getStateContent = (state) => {
83
294
  if (state.error) return labels?.error ?? errorLabel;
@@ -90,33 +301,34 @@ function KeepButton({ labels, ...props }) {
90
301
  const sharedProps2 = {
91
302
  ...props,
92
303
  "aria-busy": props["aria-busy"] ?? (buttonState.isLoading || buttonState.isMutating),
93
- savedLabel: labels?.saved ?? savedLabel,
94
- unsavedLabel: labels?.unsaved ?? saveLabel,
304
+ savedLabel: labels?.saved ?? props.savedLabel ?? savedLabel,
305
+ unsavedLabel: labels?.unsaved ?? props.unsavedLabel ?? saveLabel,
95
306
  savedAriaLabel: labels?.savedAriaLabel ?? props.savedAriaLabel,
96
307
  unsavedAriaLabel: labels?.unsavedAriaLabel ?? props.unsavedAriaLabel
97
308
  };
98
- if (!customStateLabel) return /* @__PURE__ */ jsx(CoreKeepButton, { ...sharedProps2 });
309
+ if (!customStateLabel) return /* @__PURE__ */ jsx4(CoreKeepButton, { ...sharedProps2 });
99
310
  const statefulProps2 = {
100
311
  ...sharedProps2,
101
- children: (state) => /* @__PURE__ */ jsx(Fragment, { children: getStateContent(state) })
312
+ children: (state) => /* @__PURE__ */ jsx4(Fragment2, { children: getStateContent(state) })
102
313
  };
103
- return /* @__PURE__ */ jsx(CoreKeepButton, { ...statefulProps2 });
314
+ return /* @__PURE__ */ jsx4(CoreKeepButton, { ...statefulProps2 });
104
315
  }
105
316
  const sharedProps = {
106
317
  ...props,
107
318
  "aria-busy": props["aria-busy"] ?? (buttonState.isLoading || buttonState.isMutating),
108
- savedLabel: labels?.saved ?? savedLabel,
109
- unsavedLabel: labels?.unsaved ?? saveLabel,
319
+ savedLabel: labels?.saved ?? props.savedLabel ?? savedLabel,
320
+ unsavedLabel: labels?.unsaved ?? props.unsavedLabel ?? saveLabel,
110
321
  savedAriaLabel: labels?.savedAriaLabel ?? props.savedAriaLabel,
111
322
  unsavedAriaLabel: labels?.unsavedAriaLabel ?? props.unsavedAriaLabel
112
323
  };
113
- if (!customStateLabel) return /* @__PURE__ */ jsx(CoreKeepButton, { ...sharedProps });
324
+ if (!customStateLabel) return /* @__PURE__ */ jsx4(CoreKeepButton, { ...sharedProps });
114
325
  const statefulProps = { ...sharedProps, children: (state) => getStateContent(state) };
115
- return /* @__PURE__ */ jsx(CoreKeepButton, { ...statefulProps });
326
+ return /* @__PURE__ */ jsx4(CoreKeepButton, { ...statefulProps });
116
327
  }
117
328
  function KeepItemCard({
118
329
  item,
119
330
  title,
331
+ getTitle,
120
332
  getImageProps,
121
333
  imageComponent: ImageComponent,
122
334
  renderImage,
@@ -134,7 +346,7 @@ function KeepItemCard({
134
346
  }) {
135
347
  const saveActionLabel = useUiLabel("save");
136
348
  const removeActionLabel = useUiLabel("remove");
137
- const itemState = useKeepItem(item.id);
349
+ const itemState = useKeepItem(item);
138
350
  const contentChildren = asChild && isValidElement(children) ? void 0 : children;
139
351
  const state = {
140
352
  item,
@@ -143,7 +355,7 @@ function KeepItemCard({
143
355
  error: itemState.error,
144
356
  remove: itemState.remove
145
357
  };
146
- const resolvedTitle = typeof title === "function" ? title(item) : title ?? getMetaTitle(item.meta) ?? item.id;
358
+ const resolvedTitle = typeof title === "function" ? title(item) : title ?? getTitle?.(item) ?? getMetaTitle(item.meta) ?? item.id;
147
359
  const imageProps = getImageProps?.(item, resolvedTitle);
148
360
  async function handleRemove() {
149
361
  try {
@@ -153,10 +365,10 @@ function KeepItemCard({
153
365
  onRemoveError?.(error);
154
366
  }
155
367
  }
156
- const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? /* @__PURE__ */ jsxs(Fragment, { children: [
157
- imageProps ? renderImage?.({ ...imageProps, alt: imageAlt ?? imageProps.alt }, item) ?? (ImageComponent ? /* @__PURE__ */ jsx(ImageComponent, { ...imageProps, alt: imageAlt ?? imageProps.alt }) : /* @__PURE__ */ jsx("img", { ...imageProps, alt: imageAlt ?? imageProps.alt })) : null,
158
- /* @__PURE__ */ jsx("h3", { children: resolvedTitle }),
159
- showSaveButton ? /* @__PURE__ */ jsx(
368
+ const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? /* @__PURE__ */ jsxs2(Fragment2, { children: [
369
+ imageProps ? renderImage?.({ ...imageProps, alt: imageAlt ?? imageProps.alt }, item) ?? (ImageComponent ? /* @__PURE__ */ jsx4(ImageComponent, { ...imageProps, alt: imageAlt ?? imageProps.alt }) : /* @__PURE__ */ jsx4("img", { ...imageProps, alt: imageAlt ?? imageProps.alt })) : null,
370
+ /* @__PURE__ */ jsx4("h3", { children: resolvedTitle }),
371
+ showSaveButton ? /* @__PURE__ */ jsx4(
160
372
  KeepButton,
161
373
  {
162
374
  item: toKeepButtonItem(item),
@@ -164,7 +376,7 @@ function KeepItemCard({
164
376
  getAriaLabel: (buttonState) => `${buttonState.isSaved ? removeActionLabel : saveActionLabel} ${String(resolvedTitle)}`
165
377
  }
166
378
  ) : null,
167
- /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void handleRemove(), disabled: itemState.isMutating, children: removeLabel ?? removeActionLabel })
379
+ /* @__PURE__ */ jsx4("button", { type: "button", onClick: () => void handleRemove(), disabled: itemState.isMutating, children: removeLabel ?? removeActionLabel })
168
380
  ] });
169
381
  return renderRoot(
170
382
  asChild,
@@ -175,7 +387,7 @@ function KeepItemCard({
175
387
  );
176
388
  }
177
389
  function KeepList({
178
- options,
390
+ query,
179
391
  children,
180
392
  renderItem,
181
393
  loading,
@@ -189,7 +401,7 @@ function KeepList({
189
401
  const defaultLoading = useUiLabel("loadingItems");
190
402
  const defaultEmpty = useUiLabel("noItems");
191
403
  const defaultError = useUiLabel("errorItems");
192
- const state = useKeepList(options);
404
+ const state = useKeepList(query);
193
405
  const body = getListBody(state, {
194
406
  children,
195
407
  renderItem,
@@ -214,12 +426,90 @@ function getListBody(state, options) {
214
426
  if (typeof options.children === "function") return options.children(state);
215
427
  if (options.children !== void 0 && !isValidElement(options.children)) return options.children;
216
428
  const items = state.items.map(
217
- (item) => options.renderItem ? options.renderItem(item, state) : /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(KeepItemCard, { item, ...options.itemCardProps }) }, item.id)
429
+ (item) => options.renderItem ? options.renderItem(item, state) : /* @__PURE__ */ jsx4("li", { children: /* @__PURE__ */ jsx4(KeepItemCard, { item, ...options.itemCardProps }) }, item.id)
430
+ );
431
+ return /* @__PURE__ */ jsx4("ul", { children: items });
432
+ }
433
+ function KeepCollection({
434
+ query = {},
435
+ pageSize = 20,
436
+ features,
437
+ renderItem,
438
+ itemCardProps,
439
+ loading,
440
+ empty,
441
+ error,
442
+ className,
443
+ ...rootProps
444
+ }) {
445
+ const enabled = {
446
+ search: true,
447
+ sort: true,
448
+ pagination: true,
449
+ tagFilter: false,
450
+ bulkActions: false,
451
+ ...features
452
+ };
453
+ const [searchValue, setSearchValue] = useState2(query.search?.query ?? "");
454
+ const [sort, setSort] = useState2(query.sort ?? { by: "updatedAt", direction: "desc" });
455
+ const [tag, setTag] = useState2(query.tags?.[0]);
456
+ const [page, setPage] = useState2(query.pagination?.page ?? 1);
457
+ const resolvedPageSize = query.pagination?.pageSize ?? pageSize;
458
+ const resolvedQuery = useMemo2(
459
+ () => ({
460
+ ...query,
461
+ search: enabled.search ? { ...query.search, query: searchValue } : query.search,
462
+ sort: enabled.sort ? sort : query.sort,
463
+ tags: tag ? [.../* @__PURE__ */ new Set([...query.tags ?? [], tag])] : query.tags,
464
+ pagination: enabled.pagination ? { ...query.pagination, page, pageSize: resolvedPageSize } : query.pagination
465
+ }),
466
+ [enabled.pagination, enabled.search, enabled.sort, page, query, resolvedPageSize, searchValue, sort, tag]
467
+ );
468
+ const list = useKeepList(resolvedQuery);
469
+ useEffect2(() => {
470
+ if (searchValue !== void 0 || sort.by !== void 0 || sort.direction !== void 0 || tag !== void 0) {
471
+ setPage(1);
472
+ }
473
+ }, [searchValue, sort.by, sort.direction, tag]);
474
+ return /* @__PURE__ */ jsxs2(
475
+ "section",
476
+ {
477
+ ...rootProps,
478
+ className,
479
+ "aria-busy": list.isLoading || list.isMutating || rootProps["aria-busy"],
480
+ children: [
481
+ /* @__PURE__ */ jsxs2("div", { children: [
482
+ enabled.search ? /* @__PURE__ */ jsx4(KeepSearchInput, { value: searchValue, onValueChange: setSearchValue }) : null,
483
+ enabled.sort ? /* @__PURE__ */ jsx4(KeepSortSelect, { value: sortToValue(sort), onValueChange: (_value, nextSort) => setSort(nextSort) }) : null,
484
+ enabled.tagFilter ? /* @__PURE__ */ jsx4(KeepTagFilter, { query, value: tag, onValueChange: setTag }) : null
485
+ ] }),
486
+ /* @__PURE__ */ jsx4(
487
+ KeepList,
488
+ {
489
+ query: resolvedQuery,
490
+ renderItem,
491
+ itemCardProps,
492
+ loading,
493
+ empty,
494
+ error
495
+ }
496
+ ),
497
+ enabled.pagination ? /* @__PURE__ */ jsx4(
498
+ KeepPagination,
499
+ {
500
+ totalCount: list.totalCount,
501
+ pageSize: resolvedPageSize,
502
+ page: list.page,
503
+ onPageChange: (_nextPage, nextPageOffset) => setPage(Math.floor(nextPageOffset / resolvedPageSize) + 1)
504
+ }
505
+ ) : null,
506
+ enabled.bulkActions ? /* @__PURE__ */ jsx4(KeepBulkActions, { query: resolvedQuery }) : null
507
+ ]
508
+ }
218
509
  );
219
- return /* @__PURE__ */ jsx("ul", { children: items });
220
510
  }
221
511
  function KeepTagFilter({
222
- listOptions,
512
+ query,
223
513
  value: controlledValue,
224
514
  defaultValue,
225
515
  onChange,
@@ -237,10 +527,10 @@ function KeepTagFilter({
237
527
  const uiAriaLabel = useUiLabel("filterTags");
238
528
  const defaultAllLabel = allLabel ?? uiAllLabel;
239
529
  const defaultAriaLabel = ariaLabel ?? uiAriaLabel;
240
- const list = useKeepList(listOptions);
241
530
  const contentChildren = asChild && isValidElement(children) ? void 0 : children;
242
- const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
531
+ const [uncontrolledValue, setUncontrolledValue] = useState2(defaultValue);
243
532
  const value = controlledValue ?? uncontrolledValue;
533
+ const list = useKeepList({ ...query, tags: value ? [...query?.tags ?? [], value] : query?.tags });
244
534
  const select = useCallback(
245
535
  (tag) => {
246
536
  if (controlledValue === void 0) setUncontrolledValue(tag);
@@ -249,16 +539,16 @@ function KeepTagFilter({
249
539
  },
250
540
  [controlledValue, onChange, onValueChange]
251
541
  );
252
- const state = useMemo(
542
+ const state = useMemo2(
253
543
  () => ({ tags: list.tags, tagCounts: list.tagCounts, value, select }),
254
544
  [list.tagCounts, list.tags, select, value]
255
545
  );
256
- const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? /* @__PURE__ */ jsxs("fieldset", { children: [
257
- /* @__PURE__ */ jsx("legend", { children: defaultAriaLabel }),
258
- /* @__PURE__ */ jsx("button", { type: "button", "aria-pressed": value === void 0, onClick: () => select(), children: defaultAllLabel }),
259
- list.tags.map((tag) => /* @__PURE__ */ jsxs("button", { type: "button", "aria-pressed": value === tag, onClick: () => select(tag), children: [
546
+ const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? /* @__PURE__ */ jsxs2("fieldset", { children: [
547
+ /* @__PURE__ */ jsx4("legend", { children: defaultAriaLabel }),
548
+ /* @__PURE__ */ jsx4("button", { type: "button", "aria-pressed": value === void 0, onClick: () => select(), children: defaultAllLabel }),
549
+ list.tags.map((tag) => /* @__PURE__ */ jsxs2("button", { type: "button", "aria-pressed": value === tag, onClick: () => select(tag), children: [
260
550
  renderTag ? renderTag(tag, list.tagCounts[tag] ?? 0, value === tag) : tag,
261
- /* @__PURE__ */ jsxs("span", { children: [
551
+ /* @__PURE__ */ jsxs2("span", { children: [
262
552
  " (",
263
553
  list.tagCounts[tag] ?? 0,
264
554
  ")"
@@ -288,10 +578,10 @@ function KeepNoteEditor({
288
578
  }) {
289
579
  const defaultLabel = useUiLabel("note");
290
580
  const defaultSaveLabel = useUiLabel("saveNote");
291
- const itemState = useKeepItem(item.id);
581
+ const itemState = useKeepItem(item);
292
582
  const contentChildren = asChild && isValidElement(children) ? void 0 : children;
293
- const [note, setNote] = useState(item.note ?? "");
294
- useEffect(() => setNote(itemState.item?.note ?? item.note ?? ""), [item.note, itemState.item?.note]);
583
+ const [note, setNote] = useState2(item.note ?? "");
584
+ useEffect2(() => setNote(itemState.item?.note ?? item.note ?? ""), [item.note, itemState.item?.note]);
295
585
  const save = useCallback(async () => {
296
586
  const nextNote = note.trim() || void 0;
297
587
  try {
@@ -311,10 +601,10 @@ function KeepNoteEditor({
311
601
  error: itemState.error,
312
602
  save
313
603
  };
314
- const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? /* @__PURE__ */ jsxs(Fragment, { children: [
315
- /* @__PURE__ */ jsxs("label", { children: [
604
+ const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? /* @__PURE__ */ jsxs2(Fragment2, { children: [
605
+ /* @__PURE__ */ jsxs2("label", { children: [
316
606
  label ?? defaultLabel,
317
- /* @__PURE__ */ jsx(
607
+ /* @__PURE__ */ jsx4(
318
608
  "textarea",
319
609
  {
320
610
  value: note,
@@ -324,14 +614,14 @@ function KeepNoteEditor({
324
614
  }
325
615
  )
326
616
  ] }),
327
- /* @__PURE__ */ jsx("button", { type: "submit", disabled: itemState.isMutating, "aria-busy": itemState.isMutating, children: saveLabel ?? defaultSaveLabel })
617
+ /* @__PURE__ */ jsx4("button", { type: "submit", disabled: itemState.isMutating, "aria-busy": itemState.isMutating, children: saveLabel ?? defaultSaveLabel })
328
618
  ] });
329
619
  const handleSubmit = (event) => {
330
620
  event.preventDefault();
331
621
  void save().catch(() => void 0);
332
622
  };
333
623
  if (!asChild) {
334
- return /* @__PURE__ */ jsx(
624
+ return /* @__PURE__ */ jsx4(
335
625
  "form",
336
626
  {
337
627
  ...formProps,
@@ -350,100 +640,6 @@ function KeepNoteEditor({
350
640
  "KeepNoteEditor"
351
641
  );
352
642
  }
353
- function KeepSearchInput({
354
- value: controlledValue,
355
- defaultValue = "",
356
- onValueChange,
357
- "aria-label": ariaLabel,
358
- placeholder,
359
- ...props
360
- }) {
361
- const label = useUiLabel("search");
362
- const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
363
- const value = controlledValue ?? uncontrolledValue;
364
- return /* @__PURE__ */ jsx(
365
- "input",
366
- {
367
- ...props,
368
- type: "search",
369
- value,
370
- "aria-label": ariaLabel ?? label,
371
- placeholder: placeholder ?? label,
372
- onChange: (event) => {
373
- const nextValue = event.currentTarget.value;
374
- if (controlledValue === void 0) setUncontrolledValue(nextValue);
375
- onValueChange?.(nextValue);
376
- }
377
- }
378
- );
379
- }
380
- function KeepSortSelect({
381
- value: controlledValue,
382
- defaultValue = "updatedAt:desc",
383
- onValueChange,
384
- "aria-label": ariaLabel,
385
- children,
386
- ...props
387
- }) {
388
- const label = useUiLabel("sort");
389
- const newestLabel = useUiLabel("newest");
390
- const savedLabel = useUiLabel("saved");
391
- const oldestLabel = useUiLabel("oldest");
392
- const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
393
- const value = controlledValue ?? uncontrolledValue;
394
- const options = children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
395
- /* @__PURE__ */ jsx("option", { value: "updatedAt:desc", children: newestLabel }),
396
- /* @__PURE__ */ jsx("option", { value: "savedAt:desc", children: savedLabel }),
397
- /* @__PURE__ */ jsx("option", { value: "updatedAt:asc", children: oldestLabel }),
398
- /* @__PURE__ */ jsx("option", { value: "savedAt:asc", children: oldestLabel })
399
- ] });
400
- return /* @__PURE__ */ jsx(
401
- "select",
402
- {
403
- ...props,
404
- value,
405
- "aria-label": ariaLabel ?? label,
406
- onChange: (event) => {
407
- const nextValue = event.currentTarget.value;
408
- if (controlledValue === void 0) setUncontrolledValue(nextValue);
409
- const [by, direction] = nextValue.split(":");
410
- onValueChange?.(nextValue, { by, direction });
411
- },
412
- children: options
413
- }
414
- );
415
- }
416
- function KeepPagination({
417
- totalCount,
418
- pageSize,
419
- page = 1,
420
- onPageChange,
421
- render,
422
- ...props
423
- }) {
424
- const previousPageLabel = useUiLabel("previousPage");
425
- const nextPageLabel = useUiLabel("nextPage");
426
- const pageLabel = useUiLabel("page");
427
- const paginationLabel = useUiLabel("pagination");
428
- const pageCount = Math.max(1, Math.ceil(totalCount / Math.max(1, pageSize)));
429
- const currentPage = Math.min(Math.max(1, page), pageCount);
430
- const goToPage = (nextPage) => {
431
- const next = Math.min(Math.max(1, nextPage), pageCount);
432
- onPageChange?.(next, (next - 1) * pageSize);
433
- };
434
- if (render) return /* @__PURE__ */ jsx("nav", { ...props, children: render({ page: currentPage, pageCount, goToPage }) });
435
- return /* @__PURE__ */ jsxs("nav", { ...props, "aria-label": props["aria-label"] ?? paginationLabel, children: [
436
- /* @__PURE__ */ jsx("button", { type: "button", onClick: () => goToPage(currentPage - 1), disabled: currentPage <= 1, children: previousPageLabel }),
437
- /* @__PURE__ */ jsxs("span", { "aria-current": "page", children: [
438
- pageLabel,
439
- " ",
440
- currentPage,
441
- " / ",
442
- pageCount
443
- ] }),
444
- /* @__PURE__ */ jsx("button", { type: "button", onClick: () => goToPage(currentPage + 1), disabled: currentPage >= pageCount, children: nextPageLabel })
445
- ] });
446
- }
447
643
  function KeepTagEditor({
448
644
  item,
449
645
  availableTags = [],
@@ -455,10 +651,10 @@ function KeepTagEditor({
455
651
  const tagsLabel = useUiLabel("tagsToApply");
456
652
  const removeLabel = useUiLabel("remove");
457
653
  const applyTagsLabel = useUiLabel("applyTags");
458
- const itemState = useKeepItem(item.id);
459
- const [tags, setTags] = useState(item.tags ?? []);
460
- const [input, setInput] = useState("");
461
- useEffect(() => setTags(itemState.item?.tags ?? item.tags ?? []), [item.tags, itemState.item?.tags]);
654
+ const itemState = useKeepItem(item);
655
+ const [tags, setTags] = useState2(item.tags ?? []);
656
+ const [input, setInput] = useState2("");
657
+ useEffect2(() => setTags(itemState.item?.tags ?? item.tags ?? []), [item.tags, itemState.item?.tags]);
462
658
  const save = useCallback(async () => {
463
659
  const nextTags = normalizeUiTags(tags);
464
660
  try {
@@ -475,10 +671,10 @@ function KeepTagEditor({
475
671
  setTags(next);
476
672
  setInput("");
477
673
  };
478
- const body = render ? render({ tags, setTags, save, isSaving: itemState.isMutating }) : /* @__PURE__ */ jsxs(Fragment, { children: [
479
- /* @__PURE__ */ jsxs("label", { children: [
674
+ const body = render ? render({ tags, setTags, save, isSaving: itemState.isMutating }) : /* @__PURE__ */ jsxs2(Fragment2, { children: [
675
+ /* @__PURE__ */ jsxs2("label", { children: [
480
676
  tagsLabel,
481
- /* @__PURE__ */ jsx(
677
+ /* @__PURE__ */ jsx4(
482
678
  "input",
483
679
  {
484
680
  value: input,
@@ -488,19 +684,22 @@ function KeepTagEditor({
488
684
  if (event.key === "Enter") {
489
685
  event.preventDefault();
490
686
  if (input.trim()) addTag(input);
687
+ } else if (event.key === "Backspace" && input.length === 0 && tags.length > 0) {
688
+ event.preventDefault();
689
+ setTags(tags.slice(0, -1));
491
690
  }
492
691
  }
493
692
  }
494
693
  )
495
694
  ] }),
496
- availableTags.length > 0 ? /* @__PURE__ */ jsx("datalist", { id: `keep-tags-${item.id}`, children: availableTags.map((tag) => /* @__PURE__ */ jsx("option", { value: tag }, tag)) }) : null,
497
- /* @__PURE__ */ jsx("ul", { "aria-label": tagsLabel, children: tags.map((tag) => /* @__PURE__ */ jsxs("li", { children: [
695
+ availableTags.length > 0 ? /* @__PURE__ */ jsx4("datalist", { id: `keep-tags-${item.id}`, children: availableTags.map((tag) => /* @__PURE__ */ jsx4("option", { value: tag }, tag)) }) : null,
696
+ /* @__PURE__ */ jsx4("ul", { "aria-label": tagsLabel, children: tags.map((tag) => /* @__PURE__ */ jsxs2("li", { children: [
498
697
  tag,
499
- /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setTags(tags.filter((current) => current !== tag)), children: removeLabel })
698
+ /* @__PURE__ */ jsx4("button", { type: "button", onClick: () => setTags(tags.filter((current) => current !== tag)), children: removeLabel })
500
699
  ] }, tag)) }),
501
- /* @__PURE__ */ jsx("button", { type: "submit", disabled: itemState.isMutating, "aria-busy": itemState.isMutating, children: applyTagsLabel })
700
+ /* @__PURE__ */ jsx4("button", { type: "submit", disabled: itemState.isMutating, "aria-busy": itemState.isMutating, children: applyTagsLabel })
502
701
  ] });
503
- return /* @__PURE__ */ jsx(
702
+ return /* @__PURE__ */ jsx4(
504
703
  "form",
505
704
  {
506
705
  ...props,
@@ -514,12 +713,14 @@ function KeepTagEditor({
514
713
  );
515
714
  }
516
715
  function KeepBulkActions({
517
- listOptions,
716
+ query,
518
717
  selectedIds: controlledSelectedIds,
519
718
  defaultSelectedIds = [],
520
719
  onSelectedIdsChange,
521
720
  renderItem,
522
721
  onCompleted,
722
+ render,
723
+ children,
523
724
  ...props
524
725
  }) {
525
726
  const selectItemsLabel = useUiLabel("selectItems");
@@ -527,11 +728,11 @@ function KeepBulkActions({
527
728
  const deleteSelectedLabel = useUiLabel("deleteSelected");
528
729
  const tagsLabel = useUiLabel("tagsToApply");
529
730
  const applyTagsLabel = useUiLabel("applyTags");
530
- const list = useKeepList(listOptions);
531
- const [uncontrolledSelectedIds, setUncontrolledSelectedIds] = useState(defaultSelectedIds);
731
+ const list = useKeepList(query);
732
+ const [uncontrolledSelectedIds, setUncontrolledSelectedIds] = useState2(defaultSelectedIds);
532
733
  const selectedIds = controlledSelectedIds ?? uncontrolledSelectedIds;
533
734
  const selected = new Set(selectedIds);
534
- const [tagsInput, setTagsInput] = useState("");
735
+ const [tagsInput, setTagsInput] = useState2("");
535
736
  const setSelectedIds = (ids) => {
536
737
  if (controlledSelectedIds === void 0) setUncontrolledSelectedIds(ids);
537
738
  onSelectedIdsChange?.(ids);
@@ -550,27 +751,56 @@ function KeepBulkActions({
550
751
  onCompleted?.("tags", selectedIds);
551
752
  setSelectedIds([]);
552
753
  };
553
- return /* @__PURE__ */ jsxs("section", { ...props, "aria-busy": list.isMutating || props["aria-busy"], children: [
554
- /* @__PURE__ */ jsxs("fieldset", { children: [
555
- /* @__PURE__ */ jsx("legend", { children: selectItemsLabel }),
556
- /* @__PURE__ */ jsxs("label", { children: [
557
- /* @__PURE__ */ jsx("input", { type: "checkbox", checked: allSelected, onChange: toggleAll }),
754
+ const state = {
755
+ items: list.items,
756
+ selectedIds,
757
+ selectedCount: selectedIds.length,
758
+ allSelected,
759
+ tagsInput,
760
+ setTagsInput,
761
+ toggle,
762
+ toggleAll,
763
+ remove,
764
+ updateTags,
765
+ isMutating: list.isMutating
766
+ };
767
+ const body = render ? render(state) : typeof children === "function" ? children(state) : children ?? /* @__PURE__ */ jsxs2(Fragment2, { children: [
768
+ /* @__PURE__ */ jsxs2("fieldset", { children: [
769
+ /* @__PURE__ */ jsx4("legend", { children: selectItemsLabel }),
770
+ /* @__PURE__ */ jsxs2("label", { children: [
771
+ /* @__PURE__ */ jsx4("input", { type: "checkbox", checked: allSelected, onChange: toggleAll, "aria-label": selectItemsLabel }),
558
772
  selectedIds.length,
559
773
  " ",
560
774
  selectedCountLabel
561
775
  ] }),
562
- list.items.map((item) => /* @__PURE__ */ jsxs("label", { children: [
563
- /* @__PURE__ */ jsx("input", { type: "checkbox", checked: selected.has(item.id), onChange: () => toggle(item.id) }),
776
+ list.items.map((item) => /* @__PURE__ */ jsxs2("span", { children: [
777
+ /* @__PURE__ */ jsx4(
778
+ KeepItemCheckbox,
779
+ {
780
+ item,
781
+ checked: selected.has(item.id),
782
+ onCheckedChange: () => toggle(item.id)
783
+ }
784
+ ),
564
785
  renderItem ? renderItem(item, selected.has(item.id)) : getMetaTitle(item.meta) ?? item.id
565
786
  ] }, item.id))
566
787
  ] }),
567
- /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void remove(), disabled: selectedIds.length === 0 || list.isMutating, children: deleteSelectedLabel }),
568
- /* @__PURE__ */ jsxs("label", { children: [
788
+ /* @__PURE__ */ jsx4("button", { type: "button", onClick: () => void remove(), disabled: selectedIds.length === 0 || list.isMutating, children: deleteSelectedLabel }),
789
+ /* @__PURE__ */ jsxs2("label", { children: [
569
790
  tagsLabel,
570
- /* @__PURE__ */ jsx("input", { value: tagsInput, onChange: (event) => setTagsInput(event.currentTarget.value) })
791
+ /* @__PURE__ */ jsx4("input", { value: tagsInput, onChange: (event) => setTagsInput(event.currentTarget.value) })
571
792
  ] }),
572
- /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void updateTags(), disabled: selectedIds.length === 0 || list.isMutating, children: applyTagsLabel })
793
+ /* @__PURE__ */ jsx4(
794
+ "button",
795
+ {
796
+ type: "button",
797
+ onClick: () => void updateTags(),
798
+ disabled: selectedIds.length === 0 || list.isMutating,
799
+ children: applyTagsLabel
800
+ }
801
+ )
573
802
  ] });
803
+ return /* @__PURE__ */ jsx4("section", { ...props, "aria-busy": list.isMutating || props["aria-busy"], children: body });
574
804
  }
575
805
  function KeepEmptyState({
576
806
  title,
@@ -583,9 +813,9 @@ function KeepEmptyState({
583
813
  }) {
584
814
  const defaultTitle = useUiLabel("noItems").replace(/\.$/, "");
585
815
  const contentChildren = asChild && isValidElement(children) ? void 0 : children;
586
- const body = contentChildren ?? /* @__PURE__ */ jsxs(Fragment, { children: [
587
- /* @__PURE__ */ jsx("h2", { children: title ?? defaultTitle }),
588
- description ? /* @__PURE__ */ jsx("p", { children: description }) : null,
816
+ const body = contentChildren ?? /* @__PURE__ */ jsxs2(Fragment2, { children: [
817
+ /* @__PURE__ */ jsx4("h2", { children: title ?? defaultTitle }),
818
+ description ? /* @__PURE__ */ jsx4("p", { children: description }) : null,
589
819
  action
590
820
  ] });
591
821
  return renderRoot(asChild, children, { ...rootProps, className }, body, "KeepEmptyState");
@@ -624,9 +854,9 @@ function KeepAnnouncements({ messages, ...props }) {
624
854
  const savedMessage = useUiLabel("savedMessage", messages?.save);
625
855
  const removedMessage = useUiLabel("removedMessage", messages?.remove);
626
856
  const noteSavedMessage = useUiLabel("noteSavedMessage", messages?.note);
627
- const [message, setMessage] = useState("");
857
+ const [message, setMessage] = useState2("");
628
858
  const lastChangeRef = useRef(void 0);
629
- useEffect(() => {
859
+ useEffect2(() => {
630
860
  const change = context.lastChange;
631
861
  if (!change || change === lastChangeRef.current) return;
632
862
  lastChangeRef.current = change;
@@ -634,7 +864,7 @@ function KeepAnnouncements({ messages, ...props }) {
634
864
  else if (change.action === "remove" || change.action === "removeBatch") setMessage(removedMessage);
635
865
  else if (change.action === "updateNote") setMessage(noteSavedMessage);
636
866
  }, [context.lastChange, noteSavedMessage, removedMessage, savedMessage]);
637
- return /* @__PURE__ */ jsx("div", { ...props, role: props.role ?? "status", "aria-live": props["aria-live"] ?? "polite", "aria-atomic": "true", children: message });
867
+ return /* @__PURE__ */ jsx4("div", { ...props, role: props.role ?? "status", "aria-live": props["aria-live"] ?? "polite", "aria-atomic": "true", children: message });
638
868
  }
639
869
  function getDerivedStatus(context) {
640
870
  if (context.error) return "error";
@@ -672,19 +902,28 @@ function getMetaTitle(meta) {
672
902
  function normalizeUiTags(tags) {
673
903
  return [...new Set(tags.map((tag) => tag.trim()).filter(Boolean))];
674
904
  }
905
+ function sortToValue(sort) {
906
+ return `${sort?.by ?? "updatedAt"}:${sort?.direction ?? "desc"}`;
907
+ }
675
908
  function renderRoot(asChild, child, props, body, componentName) {
676
909
  if (asChild) {
677
910
  if (!isValidElement(child)) throw new Error(`${componentName} with asChild requires a single React element child.`);
678
911
  return cloneElement(child, { ...props, children: body });
679
912
  }
680
- return /* @__PURE__ */ jsx("div", { ...props, children: body });
913
+ return /* @__PURE__ */ jsx4("div", { ...props, children: body });
681
914
  }
682
915
  export {
916
+ FallbackStorageAdapter,
917
+ IndexedDBAdapter,
918
+ IndexedDBSyncQueueAdapter,
683
919
  KeepAnnouncements,
684
920
  KeepBulkActions,
685
921
  KeepButton,
922
+ KeepCollection,
686
923
  KeepEmptyState,
687
924
  KeepItemCard,
925
+ KeepItemCheckbox,
926
+ KeepKitProvider,
688
927
  KeepList,
689
928
  KeepNoteEditor,
690
929
  KeepPagination,
@@ -695,9 +934,16 @@ export {
695
934
  KeepTagEditor,
696
935
  KeepTagFilter,
697
936
  KeepUiProvider,
937
+ LocalStorageAdapter,
938
+ LocalStorageSyncQueueAdapter,
939
+ SyncStorageAdapter,
940
+ createBrowserStorageAdapter,
941
+ createKeepKit,
942
+ createStorageAdapter,
698
943
  useKeepContext2 as useKeepContext,
699
944
  useKeepItem2 as useKeepItem,
700
945
  useKeepList2 as useKeepList,
946
+ useKeepShortcut,
701
947
  useKeepUiLabels
702
948
  };
703
949
  //# sourceMappingURL=index.js.map