@keepkit/ui 0.3.0 → 0.5.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,41 +3,143 @@
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,
14
+ createContext,
12
15
  isValidElement,
13
16
  useCallback,
17
+ useContext,
14
18
  useEffect,
15
19
  useMemo,
20
+ useRef,
16
21
  useState
17
22
  } from "react";
18
- import { KeepProvider, useKeepContext as useKeepContext2, useKeepItem as useKeepItem2, useKeepList as useKeepList2 } from "@keepkit/core/react";
23
+ import { KeepProvider, useKeepContext as useKeepContext2, useKeepItem as useKeepItem2, useKeepList as useKeepList2, useKeepShortcut } from "@keepkit/core/react";
24
+ import {
25
+ createBrowserStorageAdapter,
26
+ createStorageAdapter,
27
+ FallbackStorageAdapter,
28
+ IndexedDBAdapter,
29
+ IndexedDBSyncQueueAdapter,
30
+ LocalStorageAdapter,
31
+ LocalStorageSyncQueueAdapter,
32
+ SyncStorageAdapter
33
+ } from "@keepkit/core/storage";
19
34
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
35
+ var DEFAULT_LABELS = {
36
+ save: "Save",
37
+ saved: "Saved",
38
+ remove: "Remove",
39
+ loading: "Loading\u2026",
40
+ saving: "Saving\u2026",
41
+ syncing: "Syncing\u2026",
42
+ error: "Something went wrong.",
43
+ allTags: "All",
44
+ filterTags: "Filter saved items by tag",
45
+ note: "Note",
46
+ saveNote: "Save note",
47
+ noItems: "No saved items.",
48
+ loadingItems: "Loading saved items\u2026",
49
+ errorItems: "Could not load saved items.",
50
+ search: "Search saved items",
51
+ sort: "Sort saved items",
52
+ newest: "Newest first",
53
+ oldest: "Oldest first",
54
+ previousPage: "Previous page",
55
+ nextPage: "Next page",
56
+ pagination: "Pagination",
57
+ page: "Page",
58
+ selectItems: "Select saved items",
59
+ selectedCount: "selected",
60
+ deleteSelected: "Delete selected",
61
+ tagsToApply: "Tags to apply",
62
+ applyTags: "Apply tags",
63
+ savedMessage: "Item saved.",
64
+ removedMessage: "Item removed.",
65
+ noteSavedMessage: "Note saved."
66
+ };
67
+ var KeepUiLabelsContext = createContext({ labels: DEFAULT_LABELS });
68
+ function KeepUiProvider({ labels, locale, labelResolver, children }) {
69
+ const value = useMemo(() => {
70
+ const resolved = { ...DEFAULT_LABELS, ...labels };
71
+ return { locale, labels: resolved, labelResolver };
72
+ }, [labelResolver, labels, locale]);
73
+ return /* @__PURE__ */ jsx(KeepUiLabelsContext.Provider, { value, children });
74
+ }
75
+ function KeepKitProvider({
76
+ labels,
77
+ locale,
78
+ labelResolver,
79
+ children,
80
+ ...providerProps
81
+ }) {
82
+ return /* @__PURE__ */ jsx(KeepUiProvider, { labels, locale, labelResolver, children: /* @__PURE__ */ jsx(CoreKeepProvider, { ...providerProps, children }) });
83
+ }
84
+ function useKeepUiLabels() {
85
+ return useContext(KeepUiLabelsContext);
86
+ }
87
+ function useUiLabel(key, override) {
88
+ const context = useKeepUiLabels();
89
+ return override ?? context.labelResolver?.(key, { locale: context.locale }) ?? context.labels[key];
90
+ }
91
+ function createKeepKit(options = {}) {
92
+ const { labels, locale, labelResolver, getTitle, getImageProps, ...coreOptions } = options;
93
+ const coreKit = createCoreKeepKit(coreOptions);
94
+ return {
95
+ Provider: (props) => /* @__PURE__ */ jsx(
96
+ KeepKitProvider,
97
+ {
98
+ ...coreOptions,
99
+ labels,
100
+ locale,
101
+ labelResolver,
102
+ ...props
103
+ }
104
+ ),
105
+ Button: (props) => /* @__PURE__ */ jsx(KeepButton, { ...props }),
106
+ Collection: (props) => /* @__PURE__ */ jsx(
107
+ KeepCollection,
108
+ {
109
+ ...props,
110
+ itemCardProps: {
111
+ ...getTitle ? { getTitle } : {},
112
+ ...getImageProps ? { getImageProps } : {},
113
+ ...props.itemCardProps
114
+ }
115
+ }
116
+ ),
117
+ useContext: () => coreKit.useContext(),
118
+ useItem: (item) => coreKit.useItem(item),
119
+ useList: (query) => coreKit.useList(query),
120
+ useShortcut: (shortcutOptions) => coreKit.useShortcut(shortcutOptions)
121
+ };
122
+ }
20
123
  function KeepButton({ labels, ...props }) {
21
- const buttonState = useKeepItem(props.item.id, {
22
- meta: props.item.meta,
23
- targetType: props.item.targetType,
24
- note: props.item.note,
25
- tags: props.item.tags
26
- });
124
+ const saveLabel = useUiLabel("save", typeof labels?.unsaved === "string" ? labels.unsaved : void 0);
125
+ const savedLabel = useUiLabel("saved", typeof labels?.saved === "string" ? labels.saved : void 0);
126
+ const loadingLabel = useUiLabel("loading", typeof labels?.loading === "string" ? labels.loading : void 0);
127
+ const errorLabel = useUiLabel("error", typeof labels?.error === "string" ? labels.error : void 0);
128
+ const buttonState = useKeepItem(props.item);
27
129
  const customStateLabel = labels?.loading !== void 0 || labels?.error !== void 0;
28
130
  const getStateContent = (state) => {
29
- if (state.error && labels?.error !== void 0) return labels.error;
30
- if (state.isMutating && labels?.loading !== void 0) return labels.loading;
131
+ if (state.error) return labels?.error ?? errorLabel;
132
+ if (state.isMutating) return labels?.loading ?? loadingLabel;
31
133
  if (typeof props.children === "function") return props.children(state);
32
134
  if (props.children !== void 0) return props.children;
33
- return state.isSaved ? labels?.saved ?? "Saved" : labels?.unsaved ?? "Save";
135
+ return state.isSaved ? labels?.saved ?? savedLabel : labels?.unsaved ?? saveLabel;
34
136
  };
35
137
  if (props.asChild === true) {
36
138
  const sharedProps2 = {
37
139
  ...props,
38
140
  "aria-busy": props["aria-busy"] ?? (buttonState.isLoading || buttonState.isMutating),
39
- savedLabel: labels?.saved ?? props.savedLabel,
40
- unsavedLabel: labels?.unsaved ?? props.unsavedLabel,
141
+ savedLabel: labels?.saved ?? props.savedLabel ?? savedLabel,
142
+ unsavedLabel: labels?.unsaved ?? props.unsavedLabel ?? saveLabel,
41
143
  savedAriaLabel: labels?.savedAriaLabel ?? props.savedAriaLabel,
42
144
  unsavedAriaLabel: labels?.unsavedAriaLabel ?? props.unsavedAriaLabel
43
145
  };
@@ -51,8 +153,8 @@ function KeepButton({ labels, ...props }) {
51
153
  const sharedProps = {
52
154
  ...props,
53
155
  "aria-busy": props["aria-busy"] ?? (buttonState.isLoading || buttonState.isMutating),
54
- savedLabel: labels?.saved ?? props.savedLabel,
55
- unsavedLabel: labels?.unsaved ?? props.unsavedLabel,
156
+ savedLabel: labels?.saved ?? props.savedLabel ?? savedLabel,
157
+ unsavedLabel: labels?.unsaved ?? props.unsavedLabel ?? saveLabel,
56
158
  savedAriaLabel: labels?.savedAriaLabel ?? props.savedAriaLabel,
57
159
  unsavedAriaLabel: labels?.unsavedAriaLabel ?? props.unsavedAriaLabel
58
160
  };
@@ -63,11 +165,14 @@ function KeepButton({ labels, ...props }) {
63
165
  function KeepItemCard({
64
166
  item,
65
167
  title,
66
- getImageUrl,
168
+ getTitle,
169
+ getImageProps,
170
+ imageComponent: ImageComponent,
171
+ renderImage,
67
172
  imageAlt,
68
173
  render,
69
174
  children,
70
- removeLabel = "Remove",
175
+ removeLabel,
71
176
  onRemoveError,
72
177
  onRemoved,
73
178
  showSaveButton = true,
@@ -76,7 +181,9 @@ function KeepItemCard({
76
181
  className,
77
182
  ...rootProps
78
183
  }) {
79
- const itemState = useKeepItem(item.id);
184
+ const saveActionLabel = useUiLabel("save");
185
+ const removeActionLabel = useUiLabel("remove");
186
+ const itemState = useKeepItem(item);
80
187
  const contentChildren = asChild && isValidElement(children) ? void 0 : children;
81
188
  const state = {
82
189
  item,
@@ -85,8 +192,8 @@ function KeepItemCard({
85
192
  error: itemState.error,
86
193
  remove: itemState.remove
87
194
  };
88
- const resolvedTitle = typeof title === "function" ? title(item) : title ?? getMetaTitle(item.meta) ?? item.id;
89
- const imageUrl = getImageUrl?.(item);
195
+ const resolvedTitle = typeof title === "function" ? title(item) : title ?? getTitle?.(item) ?? getMetaTitle(item.meta) ?? item.id;
196
+ const imageProps = getImageProps?.(item, resolvedTitle);
90
197
  async function handleRemove() {
91
198
  try {
92
199
  await itemState.remove();
@@ -96,17 +203,17 @@ function KeepItemCard({
96
203
  }
97
204
  }
98
205
  const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? /* @__PURE__ */ jsxs(Fragment, { children: [
99
- imageUrl ? /* @__PURE__ */ jsx("img", { src: imageUrl, alt: imageAlt ?? String(resolvedTitle) }) : null,
206
+ 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,
100
207
  /* @__PURE__ */ jsx("h3", { children: resolvedTitle }),
101
208
  showSaveButton ? /* @__PURE__ */ jsx(
102
209
  KeepButton,
103
210
  {
104
211
  item: toKeepButtonItem(item),
105
212
  labels: saveButtonLabels,
106
- getAriaLabel: (buttonState) => `${buttonState.isSaved ? "Remove" : "Save"} ${String(resolvedTitle)}`
213
+ getAriaLabel: (buttonState) => `${buttonState.isSaved ? removeActionLabel : saveActionLabel} ${String(resolvedTitle)}`
107
214
  }
108
215
  ) : null,
109
- /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void handleRemove(), disabled: itemState.isMutating, children: removeLabel })
216
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void handleRemove(), disabled: itemState.isMutating, children: removeLabel ?? removeActionLabel })
110
217
  ] });
111
218
  return renderRoot(
112
219
  asChild,
@@ -117,19 +224,29 @@ function KeepItemCard({
117
224
  );
118
225
  }
119
226
  function KeepList({
120
- options,
227
+ query,
121
228
  children,
122
229
  renderItem,
123
- loading = "Loading saved items\u2026",
124
- empty = "No saved items.",
125
- error: errorContent = "Could not load saved items.",
230
+ loading,
231
+ empty,
232
+ error: errorContent,
126
233
  itemCardProps,
127
234
  asChild = false,
128
235
  className,
129
236
  ...rootProps
130
237
  }) {
131
- const state = useKeepList(options);
132
- const body = getListBody(state, { children, renderItem, loading, empty, error: errorContent, itemCardProps });
238
+ const defaultLoading = useUiLabel("loadingItems");
239
+ const defaultEmpty = useUiLabel("noItems");
240
+ const defaultError = useUiLabel("errorItems");
241
+ const state = useKeepList(query);
242
+ const body = getListBody(state, {
243
+ children,
244
+ renderItem,
245
+ loading: loading ?? defaultLoading,
246
+ empty: empty ?? defaultEmpty,
247
+ error: errorContent ?? defaultError,
248
+ itemCardProps
249
+ });
133
250
  const root = asChild && isValidElement(children) ? children : void 0;
134
251
  return renderRoot(
135
252
  asChild,
@@ -150,14 +267,93 @@ function getListBody(state, options) {
150
267
  );
151
268
  return /* @__PURE__ */ jsx("ul", { children: items });
152
269
  }
270
+ function KeepCollection({
271
+ query = {},
272
+ pageSize = 20,
273
+ features,
274
+ renderItem,
275
+ itemCardProps,
276
+ loading,
277
+ empty,
278
+ error,
279
+ className,
280
+ ...rootProps
281
+ }) {
282
+ const enabled = {
283
+ search: true,
284
+ sort: true,
285
+ pagination: true,
286
+ tagFilter: false,
287
+ bulkActions: false,
288
+ ...features
289
+ };
290
+ const [searchValue, setSearchValue] = useState(query.search?.query ?? "");
291
+ const [sort, setSort] = useState(query.sort ?? { by: "updatedAt", direction: "desc" });
292
+ const [tag, setTag] = useState(query.tags?.[0]);
293
+ const [page, setPage] = useState(query.pagination?.page ?? 1);
294
+ const resolvedPageSize = query.pagination?.pageSize ?? pageSize;
295
+ const resolvedQuery = useMemo(
296
+ () => ({
297
+ ...query,
298
+ search: enabled.search ? { ...query.search, query: searchValue } : query.search,
299
+ sort: enabled.sort ? sort : query.sort,
300
+ tags: tag ? [.../* @__PURE__ */ new Set([...query.tags ?? [], tag])] : query.tags,
301
+ pagination: enabled.pagination ? { ...query.pagination, page, pageSize: resolvedPageSize } : query.pagination
302
+ }),
303
+ [enabled.pagination, enabled.search, enabled.sort, page, query, resolvedPageSize, searchValue, sort, tag]
304
+ );
305
+ const list = useKeepList(resolvedQuery);
306
+ useEffect(() => {
307
+ if (searchValue !== void 0 || sort.by !== void 0 || sort.direction !== void 0 || tag !== void 0) {
308
+ setPage(1);
309
+ }
310
+ }, [searchValue, sort.by, sort.direction, tag]);
311
+ return /* @__PURE__ */ jsxs(
312
+ "section",
313
+ {
314
+ ...rootProps,
315
+ className,
316
+ "aria-busy": list.isLoading || list.isMutating || rootProps["aria-busy"],
317
+ children: [
318
+ /* @__PURE__ */ jsxs("div", { children: [
319
+ enabled.search ? /* @__PURE__ */ jsx(KeepSearchInput, { value: searchValue, onValueChange: setSearchValue }) : null,
320
+ enabled.sort ? /* @__PURE__ */ jsx(KeepSortSelect, { value: sortToValue(sort), onValueChange: (_value, nextSort) => setSort(nextSort) }) : null,
321
+ enabled.tagFilter ? /* @__PURE__ */ jsx(KeepTagFilter, { query, value: tag, onValueChange: setTag }) : null
322
+ ] }),
323
+ /* @__PURE__ */ jsx(
324
+ KeepList,
325
+ {
326
+ query: resolvedQuery,
327
+ renderItem,
328
+ itemCardProps,
329
+ loading,
330
+ empty,
331
+ error
332
+ }
333
+ ),
334
+ enabled.pagination ? /* @__PURE__ */ jsx(
335
+ KeepPagination,
336
+ {
337
+ totalCount: list.totalCount,
338
+ pageSize: resolvedPageSize,
339
+ page: list.page,
340
+ onPageChange: (_nextPage, nextPageOffset) => setPage(Math.floor(nextPageOffset / resolvedPageSize) + 1)
341
+ }
342
+ ) : null,
343
+ enabled.bulkActions ? /* @__PURE__ */ jsx(KeepBulkActions, { query: resolvedQuery }) : null,
344
+ /* @__PURE__ */ jsx(KeepAnnouncements, {})
345
+ ]
346
+ }
347
+ );
348
+ }
153
349
  function KeepTagFilter({
154
- listOptions,
350
+ query,
155
351
  value: controlledValue,
156
352
  defaultValue,
157
353
  onChange,
158
354
  onValueChange,
159
- allLabel = "All",
160
- ariaLabel = "Filter saved items by tag",
355
+ allLabel,
356
+ ariaLabel,
161
357
  renderTag,
162
358
  render,
163
359
  children,
@@ -165,10 +361,14 @@ function KeepTagFilter({
165
361
  className,
166
362
  ...rootProps
167
363
  }) {
168
- const list = useKeepList(listOptions);
364
+ const uiAllLabel = useUiLabel("allTags");
365
+ const uiAriaLabel = useUiLabel("filterTags");
366
+ const defaultAllLabel = allLabel ?? uiAllLabel;
367
+ const defaultAriaLabel = ariaLabel ?? uiAriaLabel;
169
368
  const contentChildren = asChild && isValidElement(children) ? void 0 : children;
170
369
  const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
171
370
  const value = controlledValue ?? uncontrolledValue;
371
+ const list = useKeepList({ ...query, tags: value ? [...query?.tags ?? [], value] : query?.tags });
172
372
  const select = useCallback(
173
373
  (tag) => {
174
374
  if (controlledValue === void 0) setUncontrolledValue(tag);
@@ -182,8 +382,8 @@ function KeepTagFilter({
182
382
  [list.tagCounts, list.tags, select, value]
183
383
  );
184
384
  const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? /* @__PURE__ */ jsxs("fieldset", { children: [
185
- /* @__PURE__ */ jsx("legend", { children: ariaLabel }),
186
- /* @__PURE__ */ jsx("button", { type: "button", "aria-pressed": value === void 0, onClick: () => select(), children: allLabel }),
385
+ /* @__PURE__ */ jsx("legend", { children: defaultAriaLabel }),
386
+ /* @__PURE__ */ jsx("button", { type: "button", "aria-pressed": value === void 0, onClick: () => select(), children: defaultAllLabel }),
187
387
  list.tags.map((tag) => /* @__PURE__ */ jsxs("button", { type: "button", "aria-pressed": value === tag, onClick: () => select(tag), children: [
188
388
  renderTag ? renderTag(tag, list.tagCounts[tag] ?? 0, value === tag) : tag,
189
389
  /* @__PURE__ */ jsxs("span", { children: [
@@ -203,8 +403,8 @@ function KeepTagFilter({
203
403
  }
204
404
  function KeepNoteEditor({
205
405
  item,
206
- label = "Note",
207
- saveLabel = "Save note",
406
+ label,
407
+ saveLabel,
208
408
  placeholder,
209
409
  onSaved,
210
410
  onSaveError,
@@ -214,7 +414,9 @@ function KeepNoteEditor({
214
414
  className,
215
415
  ...formProps
216
416
  }) {
217
- const itemState = useKeepItem(item.id);
417
+ const defaultLabel = useUiLabel("note");
418
+ const defaultSaveLabel = useUiLabel("saveNote");
419
+ const itemState = useKeepItem(item);
218
420
  const contentChildren = asChild && isValidElement(children) ? void 0 : children;
219
421
  const [note, setNote] = useState(item.note ?? "");
220
422
  useEffect(() => setNote(itemState.item?.note ?? item.note ?? ""), [item.note, itemState.item?.note]);
@@ -239,7 +441,7 @@ function KeepNoteEditor({
239
441
  };
240
442
  const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? /* @__PURE__ */ jsxs(Fragment, { children: [
241
443
  /* @__PURE__ */ jsxs("label", { children: [
242
- label,
444
+ label ?? defaultLabel,
243
445
  /* @__PURE__ */ jsx(
244
446
  "textarea",
245
447
  {
@@ -250,22 +452,256 @@ function KeepNoteEditor({
250
452
  }
251
453
  )
252
454
  ] }),
253
- /* @__PURE__ */ jsx("button", { type: "submit", disabled: itemState.isMutating, "aria-busy": itemState.isMutating, children: saveLabel })
455
+ /* @__PURE__ */ jsx("button", { type: "submit", disabled: itemState.isMutating, "aria-busy": itemState.isMutating, children: saveLabel ?? defaultSaveLabel })
254
456
  ] });
255
457
  const handleSubmit = (event) => {
256
458
  event.preventDefault();
257
459
  void save().catch(() => void 0);
258
460
  };
461
+ if (!asChild) {
462
+ return /* @__PURE__ */ jsx(
463
+ "form",
464
+ {
465
+ ...formProps,
466
+ className,
467
+ onSubmit: handleSubmit,
468
+ "aria-busy": itemState.isMutating || formProps["aria-busy"],
469
+ children: body
470
+ }
471
+ );
472
+ }
259
473
  return renderRoot(
260
- asChild,
474
+ true,
261
475
  isValidElement(children) ? children : void 0,
262
476
  { ...formProps, className, onSubmit: handleSubmit, "aria-busy": itemState.isMutating || formProps["aria-busy"] },
263
477
  body,
264
478
  "KeepNoteEditor"
265
479
  );
266
480
  }
481
+ function KeepSearchInput({
482
+ value: controlledValue,
483
+ defaultValue = "",
484
+ onValueChange,
485
+ "aria-label": ariaLabel,
486
+ placeholder,
487
+ ...props
488
+ }) {
489
+ const label = useUiLabel("search");
490
+ const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
491
+ const value = controlledValue ?? uncontrolledValue;
492
+ return /* @__PURE__ */ jsx(
493
+ "input",
494
+ {
495
+ ...props,
496
+ type: "search",
497
+ value,
498
+ "aria-label": ariaLabel ?? label,
499
+ placeholder: placeholder ?? label,
500
+ onChange: (event) => {
501
+ const nextValue = event.currentTarget.value;
502
+ if (controlledValue === void 0) setUncontrolledValue(nextValue);
503
+ onValueChange?.(nextValue);
504
+ }
505
+ }
506
+ );
507
+ }
508
+ function KeepSortSelect({
509
+ value: controlledValue,
510
+ defaultValue = "updatedAt:desc",
511
+ onValueChange,
512
+ "aria-label": ariaLabel,
513
+ children,
514
+ ...props
515
+ }) {
516
+ const label = useUiLabel("sort");
517
+ const newestLabel = useUiLabel("newest");
518
+ const savedLabel = useUiLabel("saved");
519
+ const oldestLabel = useUiLabel("oldest");
520
+ const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
521
+ const value = controlledValue ?? uncontrolledValue;
522
+ const options = children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
523
+ /* @__PURE__ */ jsx("option", { value: "updatedAt:desc", children: newestLabel }),
524
+ /* @__PURE__ */ jsx("option", { value: "savedAt:desc", children: savedLabel }),
525
+ /* @__PURE__ */ jsx("option", { value: "updatedAt:asc", children: oldestLabel }),
526
+ /* @__PURE__ */ jsx("option", { value: "savedAt:asc", children: oldestLabel })
527
+ ] });
528
+ return /* @__PURE__ */ jsx(
529
+ "select",
530
+ {
531
+ ...props,
532
+ value,
533
+ "aria-label": ariaLabel ?? label,
534
+ onChange: (event) => {
535
+ const nextValue = event.currentTarget.value;
536
+ if (controlledValue === void 0) setUncontrolledValue(nextValue);
537
+ const [by, direction] = nextValue.split(":");
538
+ onValueChange?.(nextValue, { by, direction });
539
+ },
540
+ children: options
541
+ }
542
+ );
543
+ }
544
+ function KeepPagination({
545
+ totalCount,
546
+ pageSize,
547
+ page = 1,
548
+ onPageChange,
549
+ render,
550
+ ...props
551
+ }) {
552
+ const previousPageLabel = useUiLabel("previousPage");
553
+ const nextPageLabel = useUiLabel("nextPage");
554
+ const pageLabel = useUiLabel("page");
555
+ const paginationLabel = useUiLabel("pagination");
556
+ const pageCount = Math.max(1, Math.ceil(totalCount / Math.max(1, pageSize)));
557
+ const currentPage = Math.min(Math.max(1, page), pageCount);
558
+ const goToPage = (nextPage) => {
559
+ const next = Math.min(Math.max(1, nextPage), pageCount);
560
+ onPageChange?.(next, (next - 1) * pageSize);
561
+ };
562
+ if (render) return /* @__PURE__ */ jsx("nav", { ...props, children: render({ page: currentPage, pageCount, goToPage }) });
563
+ return /* @__PURE__ */ jsxs("nav", { ...props, "aria-label": props["aria-label"] ?? paginationLabel, children: [
564
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => goToPage(currentPage - 1), disabled: currentPage <= 1, children: previousPageLabel }),
565
+ /* @__PURE__ */ jsxs("span", { "aria-current": "page", children: [
566
+ pageLabel,
567
+ " ",
568
+ currentPage,
569
+ " / ",
570
+ pageCount
571
+ ] }),
572
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => goToPage(currentPage + 1), disabled: currentPage >= pageCount, children: nextPageLabel })
573
+ ] });
574
+ }
575
+ function KeepTagEditor({
576
+ item,
577
+ availableTags = [],
578
+ onSaved,
579
+ onSaveError,
580
+ render,
581
+ ...props
582
+ }) {
583
+ const tagsLabel = useUiLabel("tagsToApply");
584
+ const removeLabel = useUiLabel("remove");
585
+ const applyTagsLabel = useUiLabel("applyTags");
586
+ const itemState = useKeepItem(item);
587
+ const [tags, setTags] = useState(item.tags ?? []);
588
+ const [input, setInput] = useState("");
589
+ useEffect(() => setTags(itemState.item?.tags ?? item.tags ?? []), [item.tags, itemState.item?.tags]);
590
+ const save = useCallback(async () => {
591
+ const nextTags = normalizeUiTags(tags);
592
+ try {
593
+ await itemState.updateTags(nextTags);
594
+ setTags(nextTags);
595
+ onSaved?.(nextTags);
596
+ } catch (error) {
597
+ onSaveError?.(error);
598
+ throw error;
599
+ }
600
+ }, [itemState, onSaveError, onSaved, tags]);
601
+ const addTag = (tag) => {
602
+ const next = normalizeUiTags([...tags, tag]);
603
+ setTags(next);
604
+ setInput("");
605
+ };
606
+ const body = render ? render({ tags, setTags, save, isSaving: itemState.isMutating }) : /* @__PURE__ */ jsxs(Fragment, { children: [
607
+ /* @__PURE__ */ jsxs("label", { children: [
608
+ tagsLabel,
609
+ /* @__PURE__ */ jsx(
610
+ "input",
611
+ {
612
+ value: input,
613
+ list: availableTags.length > 0 ? `keep-tags-${item.id}` : void 0,
614
+ onChange: (event) => setInput(event.currentTarget.value),
615
+ onKeyDown: (event) => {
616
+ if (event.key === "Enter") {
617
+ event.preventDefault();
618
+ if (input.trim()) addTag(input);
619
+ }
620
+ }
621
+ }
622
+ )
623
+ ] }),
624
+ availableTags.length > 0 ? /* @__PURE__ */ jsx("datalist", { id: `keep-tags-${item.id}`, children: availableTags.map((tag) => /* @__PURE__ */ jsx("option", { value: tag }, tag)) }) : null,
625
+ /* @__PURE__ */ jsx("ul", { "aria-label": tagsLabel, children: tags.map((tag) => /* @__PURE__ */ jsxs("li", { children: [
626
+ tag,
627
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setTags(tags.filter((current) => current !== tag)), children: removeLabel })
628
+ ] }, tag)) }),
629
+ /* @__PURE__ */ jsx("button", { type: "submit", disabled: itemState.isMutating, "aria-busy": itemState.isMutating, children: applyTagsLabel })
630
+ ] });
631
+ return /* @__PURE__ */ jsx(
632
+ "form",
633
+ {
634
+ ...props,
635
+ onSubmit: (event) => {
636
+ event.preventDefault();
637
+ void save().catch(() => void 0);
638
+ },
639
+ "aria-busy": itemState.isMutating || props["aria-busy"],
640
+ children: body
641
+ }
642
+ );
643
+ }
644
+ function KeepBulkActions({
645
+ query,
646
+ selectedIds: controlledSelectedIds,
647
+ defaultSelectedIds = [],
648
+ onSelectedIdsChange,
649
+ renderItem,
650
+ onCompleted,
651
+ ...props
652
+ }) {
653
+ const selectItemsLabel = useUiLabel("selectItems");
654
+ const selectedCountLabel = useUiLabel("selectedCount");
655
+ const deleteSelectedLabel = useUiLabel("deleteSelected");
656
+ const tagsLabel = useUiLabel("tagsToApply");
657
+ const applyTagsLabel = useUiLabel("applyTags");
658
+ const list = useKeepList(query);
659
+ const [uncontrolledSelectedIds, setUncontrolledSelectedIds] = useState(defaultSelectedIds);
660
+ const selectedIds = controlledSelectedIds ?? uncontrolledSelectedIds;
661
+ const selected = new Set(selectedIds);
662
+ const [tagsInput, setTagsInput] = useState("");
663
+ const setSelectedIds = (ids) => {
664
+ if (controlledSelectedIds === void 0) setUncontrolledSelectedIds(ids);
665
+ onSelectedIdsChange?.(ids);
666
+ };
667
+ const toggle = (id) => setSelectedIds(selected.has(id) ? selectedIds.filter((current) => current !== id) : [...selectedIds, id]);
668
+ const allSelected = list.items.length > 0 && list.items.every((item) => selected.has(item.id));
669
+ const toggleAll = () => setSelectedIds(allSelected ? [] : list.items.map((item) => item.id));
670
+ const remove = async () => {
671
+ await list.removeBatch(selectedIds);
672
+ onCompleted?.("remove", selectedIds);
673
+ setSelectedIds([]);
674
+ };
675
+ const updateTags = async () => {
676
+ const tags = normalizeUiTags(tagsInput.split(","));
677
+ await list.updateTagsBatch(selectedIds, tags);
678
+ onCompleted?.("tags", selectedIds);
679
+ setSelectedIds([]);
680
+ };
681
+ return /* @__PURE__ */ jsxs("section", { ...props, "aria-busy": list.isMutating || props["aria-busy"], children: [
682
+ /* @__PURE__ */ jsxs("fieldset", { children: [
683
+ /* @__PURE__ */ jsx("legend", { children: selectItemsLabel }),
684
+ /* @__PURE__ */ jsxs("label", { children: [
685
+ /* @__PURE__ */ jsx("input", { type: "checkbox", checked: allSelected, onChange: toggleAll }),
686
+ selectedIds.length,
687
+ " ",
688
+ selectedCountLabel
689
+ ] }),
690
+ list.items.map((item) => /* @__PURE__ */ jsxs("label", { children: [
691
+ /* @__PURE__ */ jsx("input", { type: "checkbox", checked: selected.has(item.id), onChange: () => toggle(item.id) }),
692
+ renderItem ? renderItem(item, selected.has(item.id)) : getMetaTitle(item.meta) ?? item.id
693
+ ] }, item.id))
694
+ ] }),
695
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void remove(), disabled: selectedIds.length === 0 || list.isMutating, children: deleteSelectedLabel }),
696
+ /* @__PURE__ */ jsxs("label", { children: [
697
+ tagsLabel,
698
+ /* @__PURE__ */ jsx("input", { value: tagsInput, onChange: (event) => setTagsInput(event.currentTarget.value) })
699
+ ] }),
700
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void updateTags(), disabled: selectedIds.length === 0 || list.isMutating, children: applyTagsLabel })
701
+ ] });
702
+ }
267
703
  function KeepEmptyState({
268
- title = "No saved items",
704
+ title,
269
705
  description,
270
706
  action,
271
707
  children,
@@ -273,9 +709,10 @@ function KeepEmptyState({
273
709
  className,
274
710
  ...rootProps
275
711
  }) {
712
+ const defaultTitle = useUiLabel("noItems").replace(/\.$/, "");
276
713
  const contentChildren = asChild && isValidElement(children) ? void 0 : children;
277
714
  const body = contentChildren ?? /* @__PURE__ */ jsxs(Fragment, { children: [
278
- /* @__PURE__ */ jsx("h2", { children: title }),
715
+ /* @__PURE__ */ jsx("h2", { children: title ?? defaultTitle }),
279
716
  description ? /* @__PURE__ */ jsx("p", { children: description }) : null,
280
717
  action
281
718
  ] });
@@ -293,13 +730,14 @@ function KeepStatus({
293
730
  const context = useKeepContext();
294
731
  const contentChildren = asChild && isValidElement(children) ? void 0 : children;
295
732
  const resolvedStatus = status ?? getDerivedStatus(context);
733
+ const defaultLabel = useUiLabel(getStatusLabelKey(resolvedStatus));
296
734
  const state = {
297
735
  status: resolvedStatus,
298
736
  error: context.error,
299
737
  pendingCount: context.syncState.pendingCount,
300
738
  items: context.items
301
739
  };
302
- const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? labels?.[resolvedStatus] ?? getDefaultStatusLabel(resolvedStatus);
740
+ const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? labels?.[resolvedStatus] ?? defaultLabel;
303
741
  const role = rootProps.role ?? (resolvedStatus === "error" ? "alert" : "status");
304
742
  return renderRoot(
305
743
  asChild,
@@ -309,6 +747,23 @@ function KeepStatus({
309
747
  "KeepStatus"
310
748
  );
311
749
  }
750
+ function KeepAnnouncements({ messages, ...props }) {
751
+ const context = useKeepContext();
752
+ const savedMessage = useUiLabel("savedMessage", messages?.save);
753
+ const removedMessage = useUiLabel("removedMessage", messages?.remove);
754
+ const noteSavedMessage = useUiLabel("noteSavedMessage", messages?.note);
755
+ const [message, setMessage] = useState("");
756
+ const lastChangeRef = useRef(void 0);
757
+ useEffect(() => {
758
+ const change = context.lastChange;
759
+ if (!change || change === lastChangeRef.current) return;
760
+ lastChangeRef.current = change;
761
+ if (change.action === "save") setMessage(savedMessage);
762
+ else if (change.action === "remove" || change.action === "removeBatch") setMessage(removedMessage);
763
+ else if (change.action === "updateNote") setMessage(noteSavedMessage);
764
+ }, [context.lastChange, noteSavedMessage, removedMessage, savedMessage]);
765
+ return /* @__PURE__ */ jsx("div", { ...props, role: props.role ?? "status", "aria-live": props["aria-live"] ?? "polite", "aria-atomic": "true", children: message });
766
+ }
312
767
  function getDerivedStatus(context) {
313
768
  if (context.error) return "error";
314
769
  if (context.syncState.status === "pending" || context.syncState.status === "syncing") return "syncing";
@@ -317,13 +772,13 @@ function getDerivedStatus(context) {
317
772
  if (context.isHydrated && context.items.length === 0) return "empty";
318
773
  return "idle";
319
774
  }
320
- function getDefaultStatusLabel(status) {
321
- if (status === "empty") return "No saved items.";
322
- if (status === "loading") return "Loading saved items\u2026";
323
- if (status === "saving") return "Saving\u2026";
324
- if (status === "syncing") return "Syncing\u2026";
325
- if (status === "error") return "Something went wrong.";
326
- return "";
775
+ function getStatusLabelKey(status) {
776
+ if (status === "empty") return "noItems";
777
+ if (status === "loading") return "loadingItems";
778
+ if (status === "error") return "error";
779
+ if (status === "saving") return "saving";
780
+ if (status === "syncing") return "syncing";
781
+ return "saved";
327
782
  }
328
783
  function resolveContent(content, state) {
329
784
  return typeof content === "function" ? content(state) : content;
@@ -342,6 +797,12 @@ function getMetaTitle(meta) {
342
797
  const title = meta.title;
343
798
  return typeof title === "string" && title.trim() ? title.trim() : void 0;
344
799
  }
800
+ function normalizeUiTags(tags) {
801
+ return [...new Set(tags.map((tag) => tag.trim()).filter(Boolean))];
802
+ }
803
+ function sortToValue(sort) {
804
+ return `${sort?.by ?? "updatedAt"}:${sort?.direction ?? "desc"}`;
805
+ }
345
806
  function renderRoot(asChild, child, props, body, componentName) {
346
807
  if (asChild) {
347
808
  if (!isValidElement(child)) throw new Error(`${componentName} with asChild requires a single React element child.`);
@@ -350,16 +811,36 @@ function renderRoot(asChild, child, props, body, componentName) {
350
811
  return /* @__PURE__ */ jsx("div", { ...props, children: body });
351
812
  }
352
813
  export {
814
+ FallbackStorageAdapter,
815
+ IndexedDBAdapter,
816
+ IndexedDBSyncQueueAdapter,
817
+ KeepAnnouncements,
818
+ KeepBulkActions,
353
819
  KeepButton,
820
+ KeepCollection,
354
821
  KeepEmptyState,
355
822
  KeepItemCard,
823
+ KeepKitProvider,
356
824
  KeepList,
357
825
  KeepNoteEditor,
826
+ KeepPagination,
358
827
  KeepProvider,
828
+ KeepSearchInput,
829
+ KeepSortSelect,
359
830
  KeepStatus,
831
+ KeepTagEditor,
360
832
  KeepTagFilter,
833
+ KeepUiProvider,
834
+ LocalStorageAdapter,
835
+ LocalStorageSyncQueueAdapter,
836
+ SyncStorageAdapter,
837
+ createBrowserStorageAdapter,
838
+ createKeepKit,
839
+ createStorageAdapter,
361
840
  useKeepContext2 as useKeepContext,
362
841
  useKeepItem2 as useKeepItem,
363
- useKeepList2 as useKeepList
842
+ useKeepList2 as useKeepList,
843
+ useKeepShortcut,
844
+ useKeepUiLabels
364
845
  };
365
846
  //# sourceMappingURL=index.js.map