@keepkit/ui 0.5.0 → 0.7.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
@@ -2,36 +2,83 @@
2
2
 
3
3
  // src/index.tsx
4
4
  import {
5
- KeepButton as CoreKeepButton,
6
5
  KeepProvider as CoreKeepProvider,
7
- createKeepKit as createCoreKeepKit,
8
- useKeepContext,
9
- useKeepItem,
10
- useKeepList
6
+ createKeepKit as createCoreKeepKit
11
7
  } from "@keepkit/core/react";
8
+
9
+ // src/KeepBulkActions.tsx
10
+ import { useKeepList } from "@keepkit/core/react";
11
+ import { useState } from "react";
12
+
13
+ // src/KeepItemCheckbox.tsx
14
+ import { jsx } from "react/jsx-runtime";
15
+ function KeepItemCheckbox({
16
+ item,
17
+ checked = false,
18
+ label,
19
+ onCheckedChange,
20
+ "aria-label": ariaLabel,
21
+ ...props
22
+ }) {
23
+ const itemLabel = getItemLabel(item) ?? item.id;
24
+ const accessibleLabel = ariaLabel ?? (typeof label === "string" ? label : itemLabel);
25
+ return /* @__PURE__ */ jsx(
26
+ "input",
27
+ {
28
+ ...props,
29
+ type: "checkbox",
30
+ checked,
31
+ "aria-label": accessibleLabel,
32
+ onChange: (event) => onCheckedChange?.(event.currentTarget.checked)
33
+ }
34
+ );
35
+ }
36
+ function getItemLabel(item) {
37
+ if (typeof item.meta !== "object" || item.meta === null || !("title" in item.meta)) return void 0;
38
+ const title = item.meta.title;
39
+ return typeof title === "string" && title.trim() ? title.trim() : void 0;
40
+ }
41
+
42
+ // src/shared.tsx
12
43
  import {
13
44
  cloneElement,
14
- createContext,
15
- isValidElement,
16
- useCallback,
17
- useContext,
18
- useEffect,
19
- useMemo,
20
- useRef,
21
- useState
45
+ isValidElement
22
46
  } from "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";
34
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
47
+ import { jsx as jsx2 } from "react/jsx-runtime";
48
+ function toKeepButtonItem(item) {
49
+ return {
50
+ id: item.id,
51
+ meta: item.meta,
52
+ targetType: item.targetType,
53
+ note: item.note,
54
+ tags: item.tags
55
+ };
56
+ }
57
+ function getMetaTitle(meta) {
58
+ if (typeof meta !== "object" || meta === null || !("title" in meta)) return void 0;
59
+ const title = meta.title;
60
+ return typeof title === "string" && title.trim() ? title.trim() : void 0;
61
+ }
62
+ function normalizeUiTags(tags) {
63
+ return [...new Set(tags.map((tag) => tag.trim()).filter(Boolean))];
64
+ }
65
+ function sortToValue(sort) {
66
+ return `${sort?.by ?? "updatedAt"}:${sort?.direction ?? "desc"}`;
67
+ }
68
+ function resolveContent(content, state) {
69
+ return typeof content === "function" ? content(state) : content;
70
+ }
71
+ function renderRoot(asChild, child, props, body, componentName) {
72
+ if (asChild) {
73
+ if (!isValidElement(child)) throw new Error(`${componentName} with asChild requires a single React element child.`);
74
+ return cloneElement(child, { ...props, children: body });
75
+ }
76
+ return /* @__PURE__ */ jsx2("div", { ...props, children: body });
77
+ }
78
+
79
+ // src/ui-context.tsx
80
+ import { createContext, useContext, useMemo } from "react";
81
+ import { jsx as jsx3 } from "react/jsx-runtime";
35
82
  var DEFAULT_LABELS = {
36
83
  save: "Save",
37
84
  saved: "Saved",
@@ -51,6 +98,10 @@ var DEFAULT_LABELS = {
51
98
  sort: "Sort saved items",
52
99
  newest: "Newest first",
53
100
  oldest: "Oldest first",
101
+ savedNewest: "Saved newest first",
102
+ savedOldest: "Saved oldest first",
103
+ updatedNewest: "Updated newest first",
104
+ updatedOldest: "Updated oldest first",
54
105
  previousPage: "Previous page",
55
106
  nextPage: "Next page",
56
107
  pagination: "Pagination",
@@ -70,16 +121,7 @@ function KeepUiProvider({ labels, locale, labelResolver, children }) {
70
121
  const resolved = { ...DEFAULT_LABELS, ...labels };
71
122
  return { locale, labels: resolved, labelResolver };
72
123
  }, [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 }) });
124
+ return /* @__PURE__ */ jsx3(KeepUiLabelsContext.Provider, { value, children });
83
125
  }
84
126
  function useKeepUiLabels() {
85
127
  return useContext(KeepUiLabelsContext);
@@ -88,38 +130,107 @@ function useUiLabel(key, override) {
88
130
  const context = useKeepUiLabels();
89
131
  return override ?? context.labelResolver?.(key, { locale: context.locale }) ?? context.labels[key];
90
132
  }
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,
133
+
134
+ // src/KeepBulkActions.tsx
135
+ import { Fragment, jsx as jsx4, jsxs } from "react/jsx-runtime";
136
+ function KeepBulkActions({
137
+ query,
138
+ selectedIds: controlledSelectedIds,
139
+ defaultSelectedIds = [],
140
+ onSelectedIdsChange,
141
+ renderItem,
142
+ onCompleted,
143
+ render,
144
+ children,
145
+ ...props
146
+ }) {
147
+ const selectItemsLabel = useUiLabel("selectItems");
148
+ const selectedCountLabel = useUiLabel("selectedCount");
149
+ const deleteSelectedLabel = useUiLabel("deleteSelected");
150
+ const tagsLabel = useUiLabel("tagsToApply");
151
+ const applyTagsLabel = useUiLabel("applyTags");
152
+ const list = useKeepList(query);
153
+ const [uncontrolledSelectedIds, setUncontrolledSelectedIds] = useState(defaultSelectedIds);
154
+ const selectedIds = controlledSelectedIds ?? uncontrolledSelectedIds;
155
+ const selected = new Set(selectedIds);
156
+ const [tagsInput, setTagsInput] = useState("");
157
+ const setSelectedIds = (ids) => {
158
+ if (controlledSelectedIds === void 0) setUncontrolledSelectedIds(ids);
159
+ onSelectedIdsChange?.(ids);
160
+ };
161
+ const toggle = (id) => setSelectedIds(selected.has(id) ? selectedIds.filter((current) => current !== id) : [...selectedIds, id]);
162
+ const allSelected = list.items.length > 0 && list.items.every((item) => selected.has(item.id));
163
+ const toggleAll = () => setSelectedIds(allSelected ? [] : list.items.map((item) => item.id));
164
+ const remove = async () => {
165
+ const ids = [...selectedIds];
166
+ await list.removeBatch(ids);
167
+ onCompleted?.("remove", ids);
168
+ setSelectedIds([]);
169
+ };
170
+ const updateTags = async () => {
171
+ const ids = [...selectedIds];
172
+ await list.updateTagsBatch(ids, normalizeUiTags(tagsInput.split(",")));
173
+ onCompleted?.("tags", ids);
174
+ setSelectedIds([]);
175
+ };
176
+ const state = {
177
+ items: list.items,
178
+ selectedIds,
179
+ selectedCount: selectedIds.length,
180
+ allSelected,
181
+ tagsInput,
182
+ setTagsInput,
183
+ toggle,
184
+ toggleAll,
185
+ remove,
186
+ updateTags,
187
+ isMutating: list.isMutating
188
+ };
189
+ const body = render ? render(state) : typeof children === "function" ? children(state) : children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
190
+ /* @__PURE__ */ jsxs("fieldset", { children: [
191
+ /* @__PURE__ */ jsx4("legend", { children: selectItemsLabel }),
192
+ /* @__PURE__ */ jsxs("label", { children: [
193
+ /* @__PURE__ */ jsx4("input", { type: "checkbox", checked: allSelected, onChange: toggleAll, "aria-label": selectItemsLabel }),
194
+ selectedIds.length,
195
+ " ",
196
+ selectedCountLabel
197
+ ] }),
198
+ list.items.map((item) => /* @__PURE__ */ jsxs("span", { children: [
199
+ /* @__PURE__ */ jsx4(
200
+ KeepItemCheckbox,
201
+ {
202
+ item,
203
+ checked: selected.has(item.id),
204
+ onCheckedChange: () => toggle(item.id)
205
+ }
206
+ ),
207
+ renderItem ? renderItem(item, selected.has(item.id)) : getMetaTitle(item.meta) ?? item.id
208
+ ] }, item.id))
209
+ ] }),
210
+ /* @__PURE__ */ jsx4("button", { type: "button", onClick: () => void remove(), disabled: selectedIds.length === 0 || list.isMutating, children: deleteSelectedLabel }),
211
+ /* @__PURE__ */ jsxs("label", { children: [
212
+ tagsLabel,
213
+ /* @__PURE__ */ jsx4("input", { value: tagsInput, onChange: (event) => setTagsInput(event.currentTarget.value) })
214
+ ] }),
215
+ /* @__PURE__ */ jsx4(
216
+ "button",
97
217
  {
98
- ...coreOptions,
99
- labels,
100
- locale,
101
- labelResolver,
102
- ...props
218
+ type: "button",
219
+ onClick: () => void updateTags(),
220
+ disabled: selectedIds.length === 0 || list.isMutating,
221
+ children: applyTagsLabel
103
222
  }
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
- };
223
+ )
224
+ ] });
225
+ return /* @__PURE__ */ jsx4("section", { ...props, "aria-busy": list.isMutating || props["aria-busy"], children: body });
122
226
  }
227
+
228
+ // src/KeepButton.tsx
229
+ import {
230
+ KeepButton as CoreKeepButton,
231
+ useKeepItem
232
+ } from "@keepkit/core/react";
233
+ import { Fragment as Fragment2, jsx as jsx5 } from "react/jsx-runtime";
123
234
  function KeepButton({ labels, ...props }) {
124
235
  const saveLabel = useUiLabel("save", typeof labels?.unsaved === "string" ? labels.unsaved : void 0);
125
236
  const savedLabel = useUiLabel("saved", typeof labels?.saved === "string" ? labels.saved : void 0);
@@ -134,22 +245,6 @@ function KeepButton({ labels, ...props }) {
134
245
  if (props.children !== void 0) return props.children;
135
246
  return state.isSaved ? labels?.saved ?? savedLabel : labels?.unsaved ?? saveLabel;
136
247
  };
137
- if (props.asChild === true) {
138
- const sharedProps2 = {
139
- ...props,
140
- "aria-busy": props["aria-busy"] ?? (buttonState.isLoading || buttonState.isMutating),
141
- savedLabel: labels?.saved ?? props.savedLabel ?? savedLabel,
142
- unsavedLabel: labels?.unsaved ?? props.unsavedLabel ?? saveLabel,
143
- savedAriaLabel: labels?.savedAriaLabel ?? props.savedAriaLabel,
144
- unsavedAriaLabel: labels?.unsavedAriaLabel ?? props.unsavedAriaLabel
145
- };
146
- if (!customStateLabel) return /* @__PURE__ */ jsx(CoreKeepButton, { ...sharedProps2 });
147
- const statefulProps2 = {
148
- ...sharedProps2,
149
- children: (state) => /* @__PURE__ */ jsx(Fragment, { children: getStateContent(state) })
150
- };
151
- return /* @__PURE__ */ jsx(CoreKeepButton, { ...statefulProps2 });
152
- }
153
248
  const sharedProps = {
154
249
  ...props,
155
250
  "aria-busy": props["aria-busy"] ?? (buttonState.isLoading || buttonState.isMutating),
@@ -158,10 +253,22 @@ function KeepButton({ labels, ...props }) {
158
253
  savedAriaLabel: labels?.savedAriaLabel ?? props.savedAriaLabel,
159
254
  unsavedAriaLabel: labels?.unsavedAriaLabel ?? props.unsavedAriaLabel
160
255
  };
161
- if (!customStateLabel) return /* @__PURE__ */ jsx(CoreKeepButton, { ...sharedProps });
162
- const statefulProps = { ...sharedProps, children: (state) => getStateContent(state) };
163
- return /* @__PURE__ */ jsx(CoreKeepButton, { ...statefulProps });
256
+ if (!customStateLabel) return /* @__PURE__ */ jsx5(CoreKeepButton, { ...sharedProps });
257
+ return /* @__PURE__ */ jsx5(CoreKeepButton, { ...sharedProps, children: (state) => /* @__PURE__ */ jsx5(Fragment2, { children: getStateContent(state) }) });
164
258
  }
259
+
260
+ // src/KeepCollection.tsx
261
+ import { useKeepList as useKeepList4 } from "@keepkit/core/react";
262
+ import { useMemo as useMemo3, useState as useState4 } from "react";
263
+
264
+ // src/KeepList.tsx
265
+ import { useKeepList as useKeepList2 } from "@keepkit/core/react";
266
+ import { isValidElement as isValidElement3 } from "react";
267
+
268
+ // src/KeepItemCard.tsx
269
+ import { useKeepItem as useKeepItem2 } from "@keepkit/core/react";
270
+ import { isValidElement as isValidElement2 } from "react";
271
+ import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs2 } from "react/jsx-runtime";
165
272
  function KeepItemCard({
166
273
  item,
167
274
  title,
@@ -183,8 +290,8 @@ function KeepItemCard({
183
290
  }) {
184
291
  const saveActionLabel = useUiLabel("save");
185
292
  const removeActionLabel = useUiLabel("remove");
186
- const itemState = useKeepItem(item);
187
- const contentChildren = asChild && isValidElement(children) ? void 0 : children;
293
+ const itemState = useKeepItem2(item);
294
+ const contentChildren = asChild && isValidElement2(children) ? void 0 : children;
188
295
  const state = {
189
296
  item,
190
297
  isSaved: itemState.isSaved,
@@ -202,10 +309,10 @@ function KeepItemCard({
202
309
  onRemoveError?.(error);
203
310
  }
204
311
  }
205
- const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? /* @__PURE__ */ jsxs(Fragment, { children: [
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,
207
- /* @__PURE__ */ jsx("h3", { children: resolvedTitle }),
208
- showSaveButton ? /* @__PURE__ */ jsx(
312
+ const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? /* @__PURE__ */ jsxs2(Fragment3, { children: [
313
+ imageProps ? renderImage?.({ ...imageProps, alt: imageAlt ?? imageProps.alt }, item) ?? (ImageComponent ? /* @__PURE__ */ jsx6(ImageComponent, { ...imageProps, alt: imageAlt ?? imageProps.alt }) : /* @__PURE__ */ jsx6("img", { ...imageProps, alt: imageAlt ?? imageProps.alt })) : null,
314
+ /* @__PURE__ */ jsx6("h3", { children: resolvedTitle }),
315
+ showSaveButton ? /* @__PURE__ */ jsx6(
209
316
  KeepButton,
210
317
  {
211
318
  item: toKeepButtonItem(item),
@@ -213,16 +320,19 @@ function KeepItemCard({
213
320
  getAriaLabel: (buttonState) => `${buttonState.isSaved ? removeActionLabel : saveActionLabel} ${String(resolvedTitle)}`
214
321
  }
215
322
  ) : null,
216
- /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void handleRemove(), disabled: itemState.isMutating, children: removeLabel ?? removeActionLabel })
323
+ /* @__PURE__ */ jsx6("button", { type: "button", onClick: () => void handleRemove(), disabled: itemState.isMutating, children: removeLabel ?? removeActionLabel })
217
324
  ] });
218
325
  return renderRoot(
219
326
  asChild,
220
- isValidElement(children) ? children : void 0,
327
+ isValidElement2(children) ? children : void 0,
221
328
  { ...rootProps, className, "aria-busy": itemState.isMutating || rootProps["aria-busy"] },
222
329
  body,
223
330
  "KeepItemCard"
224
331
  );
225
332
  }
333
+
334
+ // src/KeepList.tsx
335
+ import { jsx as jsx7 } from "react/jsx-runtime";
226
336
  function KeepList({
227
337
  query,
228
338
  children,
@@ -238,7 +348,7 @@ function KeepList({
238
348
  const defaultLoading = useUiLabel("loadingItems");
239
349
  const defaultEmpty = useUiLabel("noItems");
240
350
  const defaultError = useUiLabel("errorItems");
241
- const state = useKeepList(query);
351
+ const state = useKeepList2(query);
242
352
  const body = getListBody(state, {
243
353
  children,
244
354
  renderItem,
@@ -247,10 +357,9 @@ function KeepList({
247
357
  error: errorContent ?? defaultError,
248
358
  itemCardProps
249
359
  });
250
- const root = asChild && isValidElement(children) ? children : void 0;
251
360
  return renderRoot(
252
361
  asChild,
253
- root,
362
+ asChild && isValidElement3(children) ? children : void 0,
254
363
  { ...rootProps, className, "aria-busy": state.isLoading || rootProps["aria-busy"] },
255
364
  body,
256
365
  "KeepList"
@@ -261,12 +370,196 @@ function getListBody(state, options) {
261
370
  if (state.isLoading && !state.isHydrated) return resolveContent(options.loading, state);
262
371
  if (state.isHydrated && state.items.length === 0) return resolveContent(options.empty, state);
263
372
  if (typeof options.children === "function") return options.children(state);
264
- if (options.children !== void 0 && !isValidElement(options.children)) return options.children;
265
- const items = state.items.map(
266
- (item) => options.renderItem ? options.renderItem(item, state) : /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(KeepItemCard, { item, ...options.itemCardProps }) }, item.id)
373
+ if (options.children !== void 0 && !isValidElement3(options.children)) return options.children;
374
+ return /* @__PURE__ */ jsx7("ul", { children: state.items.map(
375
+ (item) => options.renderItem ? options.renderItem(item, state) : /* @__PURE__ */ jsx7("li", { children: /* @__PURE__ */ jsx7(KeepItemCard, { item, ...options.itemCardProps }) }, item.id)
376
+ ) });
377
+ }
378
+
379
+ // src/KeepTagFilter.tsx
380
+ import { useKeepList as useKeepList3 } from "@keepkit/core/react";
381
+ import { isValidElement as isValidElement4, useCallback, useMemo as useMemo2, useState as useState2 } from "react";
382
+ import { jsx as jsx8, jsxs as jsxs3 } from "react/jsx-runtime";
383
+ function KeepTagFilter({
384
+ query,
385
+ value: controlledValue,
386
+ defaultValue,
387
+ onChange,
388
+ onValueChange,
389
+ allLabel,
390
+ ariaLabel,
391
+ renderTag,
392
+ render,
393
+ children,
394
+ asChild = false,
395
+ className,
396
+ ...rootProps
397
+ }) {
398
+ const uiAllLabel = useUiLabel("allTags");
399
+ const uiAriaLabel = useUiLabel("filterTags");
400
+ const [uncontrolledValue, setUncontrolledValue] = useState2(defaultValue);
401
+ const resolvedValue = controlledValue ?? uncontrolledValue;
402
+ const list = useKeepList3({
403
+ ...query,
404
+ tags: resolvedValue ? [...query?.tags ?? [], resolvedValue] : query?.tags
405
+ });
406
+ const select = useCallback(
407
+ (tag) => {
408
+ if (controlledValue === void 0) setUncontrolledValue(tag);
409
+ onChange?.(tag);
410
+ onValueChange?.(tag);
411
+ },
412
+ [controlledValue, onChange, onValueChange]
413
+ );
414
+ const state = useMemo2(
415
+ () => ({ tags: list.tags, tagCounts: list.tagCounts, value: resolvedValue, select }),
416
+ [list.tagCounts, list.tags, resolvedValue, select]
417
+ );
418
+ const contentChildren = asChild && isValidElement4(children) ? void 0 : children;
419
+ const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? /* @__PURE__ */ jsxs3("fieldset", { children: [
420
+ /* @__PURE__ */ jsx8("legend", { children: ariaLabel ?? uiAriaLabel }),
421
+ /* @__PURE__ */ jsx8("button", { type: "button", "aria-pressed": resolvedValue === void 0, onClick: () => select(), children: allLabel ?? uiAllLabel }),
422
+ list.tags.map((tag) => /* @__PURE__ */ jsxs3("button", { type: "button", "aria-pressed": resolvedValue === tag, onClick: () => select(tag), children: [
423
+ renderTag ? renderTag(tag, list.tagCounts[tag] ?? 0, resolvedValue === tag) : tag,
424
+ /* @__PURE__ */ jsxs3("span", { children: [
425
+ " (",
426
+ list.tagCounts[tag] ?? 0,
427
+ ")"
428
+ ] })
429
+ ] }, tag))
430
+ ] });
431
+ return renderRoot(
432
+ asChild,
433
+ isValidElement4(children) ? children : void 0,
434
+ { ...rootProps, className },
435
+ body,
436
+ "KeepTagFilter"
267
437
  );
268
- return /* @__PURE__ */ jsx("ul", { children: items });
269
438
  }
439
+
440
+ // src/query-controls.tsx
441
+ import { useEffect, useState as useState3 } from "react";
442
+ import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs4 } from "react/jsx-runtime";
443
+ function KeepSearchInput({
444
+ value: controlledValue,
445
+ defaultValue = "",
446
+ debounceMs = 300,
447
+ onValueChange,
448
+ "aria-label": ariaLabel,
449
+ placeholder,
450
+ ...props
451
+ }) {
452
+ const label = useUiLabel("search");
453
+ const [uncontrolledValue, setUncontrolledValue] = useState3(defaultValue);
454
+ const value = controlledValue ?? uncontrolledValue;
455
+ useEffect(() => {
456
+ if (!onValueChange) return;
457
+ if (debounceMs <= 0) {
458
+ onValueChange(value);
459
+ return;
460
+ }
461
+ const timer = window.setTimeout(() => onValueChange(value), debounceMs);
462
+ return () => window.clearTimeout(timer);
463
+ }, [debounceMs, onValueChange, value]);
464
+ return /* @__PURE__ */ jsx9(
465
+ "input",
466
+ {
467
+ ...props,
468
+ type: "search",
469
+ value,
470
+ "aria-label": ariaLabel ?? label,
471
+ placeholder: placeholder ?? label,
472
+ onChange: (event) => {
473
+ const nextValue = event.currentTarget.value;
474
+ if (controlledValue === void 0) setUncontrolledValue(nextValue);
475
+ }
476
+ }
477
+ );
478
+ }
479
+ function KeepSortSelect({
480
+ value: controlledValue,
481
+ defaultValue = "updatedAt:desc",
482
+ onValueChange,
483
+ "aria-label": ariaLabel,
484
+ children,
485
+ ...props
486
+ }) {
487
+ const label = useUiLabel("sort");
488
+ const updatedNewestLabel = useUiLabel("updatedNewest");
489
+ const updatedOldestLabel = useUiLabel("updatedOldest");
490
+ const savedNewestLabel = useUiLabel("savedNewest");
491
+ const savedOldestLabel = useUiLabel("savedOldest");
492
+ const [uncontrolledValue, setUncontrolledValue] = useState3(defaultValue);
493
+ const value = controlledValue ?? uncontrolledValue;
494
+ const options = children ?? /* @__PURE__ */ jsxs4(Fragment4, { children: [
495
+ /* @__PURE__ */ jsx9("option", { value: "updatedAt:desc", children: updatedNewestLabel }),
496
+ /* @__PURE__ */ jsx9("option", { value: "updatedAt:asc", children: updatedOldestLabel }),
497
+ /* @__PURE__ */ jsx9("option", { value: "savedAt:desc", children: savedNewestLabel }),
498
+ /* @__PURE__ */ jsx9("option", { value: "savedAt:asc", children: savedOldestLabel })
499
+ ] });
500
+ return /* @__PURE__ */ jsx9(
501
+ "select",
502
+ {
503
+ ...props,
504
+ value,
505
+ "aria-label": ariaLabel ?? label,
506
+ onChange: (event) => {
507
+ const nextValue = event.currentTarget.value;
508
+ if (controlledValue === void 0) setUncontrolledValue(nextValue);
509
+ const [by, direction] = nextValue.split(":");
510
+ onValueChange?.(nextValue, { by, direction });
511
+ },
512
+ children: options
513
+ }
514
+ );
515
+ }
516
+ function KeepPagination({
517
+ totalCount,
518
+ pageSize,
519
+ page = 1,
520
+ maxPageButtons = 7,
521
+ onPageChange,
522
+ render,
523
+ ...props
524
+ }) {
525
+ const previousPageLabel = useUiLabel("previousPage");
526
+ const nextPageLabel = useUiLabel("nextPage");
527
+ const pageLabel = useUiLabel("page");
528
+ const paginationLabel = useUiLabel("pagination");
529
+ const pageCount = Math.max(1, Math.ceil(totalCount / Math.max(1, pageSize)));
530
+ const currentPage = Math.min(Math.max(1, page), pageCount);
531
+ const goToPage = (nextPage) => {
532
+ const next = Math.min(Math.max(1, nextPage), pageCount);
533
+ onPageChange?.(next, (next - 1) * pageSize);
534
+ };
535
+ const navProps = { ...props, "aria-label": props["aria-label"] ?? paginationLabel };
536
+ if (render) return /* @__PURE__ */ jsx9("nav", { ...navProps, children: render({ page: currentPage, pageCount, goToPage }) });
537
+ const visiblePages = getVisiblePages(currentPage, pageCount, Math.max(1, maxPageButtons));
538
+ return /* @__PURE__ */ jsxs4("nav", { ...navProps, children: [
539
+ /* @__PURE__ */ jsx9("button", { type: "button", onClick: () => goToPage(currentPage - 1), disabled: currentPage <= 1, children: previousPageLabel }),
540
+ visiblePages.map((nextPage) => /* @__PURE__ */ jsx9(
541
+ "button",
542
+ {
543
+ type: "button",
544
+ "aria-current": nextPage === currentPage ? "page" : void 0,
545
+ "aria-label": `${pageLabel} ${nextPage}`,
546
+ onClick: () => goToPage(nextPage),
547
+ children: nextPage
548
+ },
549
+ nextPage
550
+ )),
551
+ /* @__PURE__ */ jsx9("button", { type: "button", onClick: () => goToPage(currentPage + 1), disabled: currentPage >= pageCount, children: nextPageLabel })
552
+ ] });
553
+ }
554
+ function getVisiblePages(currentPage, pageCount, maxPageButtons) {
555
+ if (pageCount <= maxPageButtons) return Array.from({ length: pageCount }, (_, index) => index + 1);
556
+ const half = Math.floor(maxPageButtons / 2);
557
+ const start = Math.min(Math.max(1, currentPage - half), pageCount - maxPageButtons + 1);
558
+ return Array.from({ length: maxPageButtons }, (_, index) => start + index);
559
+ }
560
+
561
+ // src/KeepCollection.tsx
562
+ import { jsx as jsx10, jsxs as jsxs5 } from "react/jsx-runtime";
270
563
  function KeepCollection({
271
564
  query = {},
272
565
  pageSize = 20,
@@ -287,12 +580,12 @@ function KeepCollection({
287
580
  bulkActions: false,
288
581
  ...features
289
582
  };
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);
583
+ const [searchValue, setSearchValue] = useState4(query.search?.query ?? "");
584
+ const [sort, setSort] = useState4(query.sort ?? { by: "updatedAt", direction: "desc" });
585
+ const [tag, setTag] = useState4(query.tags?.[0]);
586
+ const [page, setPage] = useState4(query.pagination?.page ?? 1);
294
587
  const resolvedPageSize = query.pagination?.pageSize ?? pageSize;
295
- const resolvedQuery = useMemo(
588
+ const resolvedQuery = useMemo3(
296
589
  () => ({
297
590
  ...query,
298
591
  search: enabled.search ? { ...query.search, query: searchValue } : query.search,
@@ -302,25 +595,48 @@ function KeepCollection({
302
595
  }),
303
596
  [enabled.pagination, enabled.search, enabled.sort, page, query, resolvedPageSize, searchValue, sort, tag]
304
597
  );
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(
598
+ const list = useKeepList4(resolvedQuery);
599
+ return /* @__PURE__ */ jsxs5(
312
600
  "section",
313
601
  {
314
602
  ...rootProps,
315
603
  className,
316
604
  "aria-busy": list.isLoading || list.isMutating || rootProps["aria-busy"],
317
605
  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
606
+ /* @__PURE__ */ jsxs5("div", { children: [
607
+ enabled.search ? /* @__PURE__ */ jsx10(
608
+ KeepSearchInput,
609
+ {
610
+ value: searchValue,
611
+ onValueChange: (value) => {
612
+ setSearchValue(value);
613
+ setPage(1);
614
+ }
615
+ }
616
+ ) : null,
617
+ enabled.sort ? /* @__PURE__ */ jsx10(
618
+ KeepSortSelect,
619
+ {
620
+ value: sortToValue(sort),
621
+ onValueChange: (_value, nextSort) => {
622
+ setSort(nextSort);
623
+ setPage(1);
624
+ }
625
+ }
626
+ ) : null,
627
+ enabled.tagFilter ? /* @__PURE__ */ jsx10(
628
+ KeepTagFilter,
629
+ {
630
+ query,
631
+ value: tag,
632
+ onValueChange: (value) => {
633
+ setTag(value);
634
+ setPage(1);
635
+ }
636
+ }
637
+ ) : null
322
638
  ] }),
323
- /* @__PURE__ */ jsx(
639
+ /* @__PURE__ */ jsx10(
324
640
  KeepList,
325
641
  {
326
642
  query: resolvedQuery,
@@ -331,81 +647,37 @@ function KeepCollection({
331
647
  error
332
648
  }
333
649
  ),
334
- enabled.pagination ? /* @__PURE__ */ jsx(
650
+ enabled.pagination ? /* @__PURE__ */ jsx10(
335
651
  KeepPagination,
336
652
  {
337
653
  totalCount: list.totalCount,
338
654
  pageSize: resolvedPageSize,
339
655
  page: list.page,
340
- onPageChange: (_nextPage, nextPageOffset) => setPage(Math.floor(nextPageOffset / resolvedPageSize) + 1)
656
+ onPageChange: (nextPage) => setPage(nextPage)
341
657
  }
342
658
  ) : null,
343
- enabled.bulkActions ? /* @__PURE__ */ jsx(KeepBulkActions, { query: resolvedQuery }) : null,
344
- /* @__PURE__ */ jsx(KeepAnnouncements, {})
659
+ enabled.bulkActions ? /* @__PURE__ */ jsx10(KeepBulkActions, { query: resolvedQuery }) : null
345
660
  ]
346
661
  }
347
662
  );
348
663
  }
349
- function KeepTagFilter({
350
- query,
351
- value: controlledValue,
352
- defaultValue,
353
- onChange,
354
- onValueChange,
355
- allLabel,
356
- ariaLabel,
357
- renderTag,
358
- render,
359
- children,
360
- asChild = false,
361
- className,
362
- ...rootProps
363
- }) {
364
- const uiAllLabel = useUiLabel("allTags");
365
- const uiAriaLabel = useUiLabel("filterTags");
366
- const defaultAllLabel = allLabel ?? uiAllLabel;
367
- const defaultAriaLabel = ariaLabel ?? uiAriaLabel;
368
- const contentChildren = asChild && isValidElement(children) ? void 0 : children;
369
- const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
370
- const value = controlledValue ?? uncontrolledValue;
371
- const list = useKeepList({ ...query, tags: value ? [...query?.tags ?? [], value] : query?.tags });
372
- const select = useCallback(
373
- (tag) => {
374
- if (controlledValue === void 0) setUncontrolledValue(tag);
375
- onChange?.(tag);
376
- onValueChange?.(tag);
377
- },
378
- [controlledValue, onChange, onValueChange]
379
- );
380
- const state = useMemo(
381
- () => ({ tags: list.tags, tagCounts: list.tagCounts, value, select }),
382
- [list.tagCounts, list.tags, select, value]
383
- );
384
- const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? /* @__PURE__ */ jsxs("fieldset", { children: [
385
- /* @__PURE__ */ jsx("legend", { children: defaultAriaLabel }),
386
- /* @__PURE__ */ jsx("button", { type: "button", "aria-pressed": value === void 0, onClick: () => select(), children: defaultAllLabel }),
387
- list.tags.map((tag) => /* @__PURE__ */ jsxs("button", { type: "button", "aria-pressed": value === tag, onClick: () => select(tag), children: [
388
- renderTag ? renderTag(tag, list.tagCounts[tag] ?? 0, value === tag) : tag,
389
- /* @__PURE__ */ jsxs("span", { children: [
390
- " (",
391
- list.tagCounts[tag] ?? 0,
392
- ")"
393
- ] })
394
- ] }, tag))
395
- ] });
396
- return renderRoot(
397
- asChild,
398
- isValidElement(children) ? children : void 0,
399
- { ...rootProps, className },
400
- body,
401
- "KeepTagFilter"
402
- );
403
- }
664
+
665
+ // src/KeepNoteEditor.tsx
666
+ import { useKeepItem as useKeepItem3 } from "@keepkit/core/react";
667
+ import {
668
+ isValidElement as isValidElement5,
669
+ useCallback as useCallback2,
670
+ useEffect as useEffect2,
671
+ useRef,
672
+ useState as useState5
673
+ } from "react";
674
+ import { Fragment as Fragment5, jsx as jsx11, jsxs as jsxs6 } from "react/jsx-runtime";
404
675
  function KeepNoteEditor({
405
676
  item,
406
677
  label,
407
678
  saveLabel,
408
679
  placeholder,
680
+ debounceMs = 300,
409
681
  onSaved,
410
682
  onSaveError,
411
683
  render,
@@ -416,162 +688,89 @@ function KeepNoteEditor({
416
688
  }) {
417
689
  const defaultLabel = useUiLabel("note");
418
690
  const defaultSaveLabel = useUiLabel("saveNote");
419
- const itemState = useKeepItem(item);
420
- const contentChildren = asChild && isValidElement(children) ? void 0 : children;
421
- const [note, setNote] = useState(item.note ?? "");
422
- useEffect(() => setNote(itemState.item?.note ?? item.note ?? ""), [item.note, itemState.item?.note]);
423
- const save = useCallback(async () => {
691
+ const itemState = useKeepItem3(item);
692
+ const { error, isMutating, item: savedItem, updateNote } = itemState;
693
+ const contentChildren = asChild && isValidElement5(children) ? void 0 : children;
694
+ const [note, setNote] = useState5(item.note ?? "");
695
+ const baselineNote = savedItem?.note ?? item.note ?? "";
696
+ const isDirty = note !== baselineNote;
697
+ const lastSavedNoteRef = useRef(void 0);
698
+ useEffect2(() => setNote(baselineNote), [baselineNote]);
699
+ const save = useCallback2(async () => {
424
700
  const nextNote = note.trim() || void 0;
425
701
  try {
426
- await itemState.updateNote(nextNote);
702
+ await updateNote(nextNote);
703
+ lastSavedNoteRef.current = note;
427
704
  onSaved?.(nextNote);
428
- } catch (error) {
429
- onSaveError?.(error);
430
- throw error;
705
+ } catch (error2) {
706
+ onSaveError?.(error2);
707
+ throw error2;
431
708
  }
432
- }, [itemState, note, onSaveError, onSaved]);
709
+ }, [note, onSaveError, onSaved, updateNote]);
710
+ useEffect2(() => {
711
+ if (!isDirty || debounceMs <= 0 || lastSavedNoteRef.current === note) return;
712
+ const timer = window.setTimeout(() => void save().catch(() => void 0), debounceMs);
713
+ return () => window.clearTimeout(timer);
714
+ }, [debounceMs, isDirty, note, save]);
433
715
  const state = {
434
716
  item,
435
717
  note,
436
718
  setNote,
437
- isDirty: note !== (itemState.item?.note ?? item.note ?? ""),
438
- isSaving: itemState.isMutating,
439
- error: itemState.error,
719
+ isDirty,
720
+ isSaving: isMutating,
721
+ error,
440
722
  save
441
723
  };
442
- const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? /* @__PURE__ */ jsxs(Fragment, { children: [
443
- /* @__PURE__ */ jsxs("label", { children: [
724
+ const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? /* @__PURE__ */ jsxs6(Fragment5, { children: [
725
+ /* @__PURE__ */ jsxs6("label", { children: [
444
726
  label ?? defaultLabel,
445
- /* @__PURE__ */ jsx(
727
+ /* @__PURE__ */ jsx11(
446
728
  "textarea",
447
729
  {
448
730
  value: note,
449
731
  onChange: (event) => setNote(event.currentTarget.value),
450
732
  placeholder,
451
- disabled: itemState.isMutating
733
+ disabled: isMutating,
734
+ onKeyDown: (event) => {
735
+ if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
736
+ event.preventDefault();
737
+ void save().catch(() => void 0);
738
+ }
739
+ }
452
740
  }
453
741
  )
454
742
  ] }),
455
- /* @__PURE__ */ jsx("button", { type: "submit", disabled: itemState.isMutating, "aria-busy": itemState.isMutating, children: saveLabel ?? defaultSaveLabel })
743
+ /* @__PURE__ */ jsx11("button", { type: "submit", disabled: isMutating, "aria-busy": isMutating, children: saveLabel ?? defaultSaveLabel })
456
744
  ] });
457
745
  const handleSubmit = (event) => {
458
746
  event.preventDefault();
459
747
  void save().catch(() => void 0);
460
748
  };
461
749
  if (!asChild) {
462
- return /* @__PURE__ */ jsx(
750
+ return /* @__PURE__ */ jsx11(
463
751
  "form",
464
752
  {
465
753
  ...formProps,
466
754
  className,
467
755
  onSubmit: handleSubmit,
468
- "aria-busy": itemState.isMutating || formProps["aria-busy"],
756
+ "aria-busy": isMutating || formProps["aria-busy"],
469
757
  children: body
470
758
  }
471
759
  );
472
760
  }
473
761
  return renderRoot(
474
762
  true,
475
- isValidElement(children) ? children : void 0,
476
- { ...formProps, className, onSubmit: handleSubmit, "aria-busy": itemState.isMutating || formProps["aria-busy"] },
763
+ isValidElement5(children) ? children : void 0,
764
+ { ...formProps, className, onSubmit: handleSubmit, "aria-busy": isMutating || formProps["aria-busy"] },
477
765
  body,
478
766
  "KeepNoteEditor"
479
767
  );
480
768
  }
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
- }
769
+
770
+ // src/KeepTagEditor.tsx
771
+ import { useKeepItem as useKeepItem4 } from "@keepkit/core/react";
772
+ import { useCallback as useCallback3, useEffect as useEffect3, useState as useState6 } from "react";
773
+ import { Fragment as Fragment6, jsx as jsx12, jsxs as jsxs7 } from "react/jsx-runtime";
575
774
  function KeepTagEditor({
576
775
  item,
577
776
  availableTags = [],
@@ -583,11 +782,11 @@ function KeepTagEditor({
583
782
  const tagsLabel = useUiLabel("tagsToApply");
584
783
  const removeLabel = useUiLabel("remove");
585
784
  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 () => {
785
+ const itemState = useKeepItem4(item);
786
+ const [tags, setTags] = useState6(item.tags ?? []);
787
+ const [input, setInput] = useState6("");
788
+ useEffect3(() => setTags(itemState.item?.tags ?? item.tags ?? []), [item.tags, itemState.item?.tags]);
789
+ const save = useCallback3(async () => {
591
790
  const nextTags = normalizeUiTags(tags);
592
791
  try {
593
792
  await itemState.updateTags(nextTags);
@@ -599,14 +798,13 @@ function KeepTagEditor({
599
798
  }
600
799
  }, [itemState, onSaveError, onSaved, tags]);
601
800
  const addTag = (tag) => {
602
- const next = normalizeUiTags([...tags, tag]);
603
- setTags(next);
801
+ setTags(normalizeUiTags([...tags, tag]));
604
802
  setInput("");
605
803
  };
606
- const body = render ? render({ tags, setTags, save, isSaving: itemState.isMutating }) : /* @__PURE__ */ jsxs(Fragment, { children: [
607
- /* @__PURE__ */ jsxs("label", { children: [
804
+ const body = render ? render({ tags, setTags, save, isSaving: itemState.isMutating }) : /* @__PURE__ */ jsxs7(Fragment6, { children: [
805
+ /* @__PURE__ */ jsxs7("label", { children: [
608
806
  tagsLabel,
609
- /* @__PURE__ */ jsx(
807
+ /* @__PURE__ */ jsx12(
610
808
  "input",
611
809
  {
612
810
  value: input,
@@ -616,19 +814,22 @@ function KeepTagEditor({
616
814
  if (event.key === "Enter") {
617
815
  event.preventDefault();
618
816
  if (input.trim()) addTag(input);
817
+ } else if (event.key === "Backspace" && input.length === 0 && tags.length > 0) {
818
+ event.preventDefault();
819
+ setTags(tags.slice(0, -1));
619
820
  }
620
821
  }
621
822
  }
622
823
  )
623
824
  ] }),
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: [
825
+ availableTags.length > 0 ? /* @__PURE__ */ jsx12("datalist", { id: `keep-tags-${item.id}`, children: availableTags.map((tag) => /* @__PURE__ */ jsx12("option", { value: tag }, tag)) }) : null,
826
+ /* @__PURE__ */ jsx12("ul", { "aria-label": tagsLabel, children: tags.map((tag) => /* @__PURE__ */ jsxs7("li", { children: [
626
827
  tag,
627
- /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setTags(tags.filter((current) => current !== tag)), children: removeLabel })
828
+ /* @__PURE__ */ jsx12("button", { type: "button", onClick: () => setTags(tags.filter((current) => current !== tag)), children: removeLabel })
628
829
  ] }, tag)) }),
629
- /* @__PURE__ */ jsx("button", { type: "submit", disabled: itemState.isMutating, "aria-busy": itemState.isMutating, children: applyTagsLabel })
830
+ /* @__PURE__ */ jsx12("button", { type: "submit", disabled: itemState.isMutating, "aria-busy": itemState.isMutating, children: applyTagsLabel })
630
831
  ] });
631
- return /* @__PURE__ */ jsx(
832
+ return /* @__PURE__ */ jsx12(
632
833
  "form",
633
834
  {
634
835
  ...props,
@@ -641,65 +842,11 @@ function KeepTagEditor({
641
842
  }
642
843
  );
643
844
  }
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
- }
845
+
846
+ // src/status.tsx
847
+ import { useKeepContext } from "@keepkit/core/react";
848
+ import { isValidElement as isValidElement6, useEffect as useEffect4, useRef as useRef2, useState as useState7 } from "react";
849
+ import { Fragment as Fragment7, jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
703
850
  function KeepEmptyState({
704
851
  title,
705
852
  description,
@@ -710,10 +857,10 @@ function KeepEmptyState({
710
857
  ...rootProps
711
858
  }) {
712
859
  const defaultTitle = useUiLabel("noItems").replace(/\.$/, "");
713
- const contentChildren = asChild && isValidElement(children) ? void 0 : children;
714
- const body = contentChildren ?? /* @__PURE__ */ jsxs(Fragment, { children: [
715
- /* @__PURE__ */ jsx("h2", { children: title ?? defaultTitle }),
716
- description ? /* @__PURE__ */ jsx("p", { children: description }) : null,
860
+ const contentChildren = asChild && isValidElement6(children) ? void 0 : children;
861
+ const body = contentChildren ?? /* @__PURE__ */ jsxs8(Fragment7, { children: [
862
+ /* @__PURE__ */ jsx13("h2", { children: title ?? defaultTitle }),
863
+ description ? /* @__PURE__ */ jsx13("p", { children: description }) : null,
717
864
  action
718
865
  ] });
719
866
  return renderRoot(asChild, children, { ...rootProps, className }, body, "KeepEmptyState");
@@ -728,7 +875,7 @@ function KeepStatus({
728
875
  ...rootProps
729
876
  }) {
730
877
  const context = useKeepContext();
731
- const contentChildren = asChild && isValidElement(children) ? void 0 : children;
878
+ const contentChildren = asChild && isValidElement6(children) ? void 0 : children;
732
879
  const resolvedStatus = status ?? getDerivedStatus(context);
733
880
  const defaultLabel = useUiLabel(getStatusLabelKey(resolvedStatus));
734
881
  const state = {
@@ -741,7 +888,7 @@ function KeepStatus({
741
888
  const role = rootProps.role ?? (resolvedStatus === "error" ? "alert" : "status");
742
889
  return renderRoot(
743
890
  asChild,
744
- isValidElement(children) ? children : void 0,
891
+ isValidElement6(children) ? children : void 0,
745
892
  { ...rootProps, className, role, "aria-live": rootProps["aria-live"] ?? "polite" },
746
893
  body,
747
894
  "KeepStatus"
@@ -752,9 +899,9 @@ function KeepAnnouncements({ messages, ...props }) {
752
899
  const savedMessage = useUiLabel("savedMessage", messages?.save);
753
900
  const removedMessage = useUiLabel("removedMessage", messages?.remove);
754
901
  const noteSavedMessage = useUiLabel("noteSavedMessage", messages?.note);
755
- const [message, setMessage] = useState("");
756
- const lastChangeRef = useRef(void 0);
757
- useEffect(() => {
902
+ const [message, setMessage] = useState7("");
903
+ const lastChangeRef = useRef2(void 0);
904
+ useEffect4(() => {
758
905
  const change = context.lastChange;
759
906
  if (!change || change === lastChangeRef.current) return;
760
907
  lastChangeRef.current = change;
@@ -762,8 +909,9 @@ function KeepAnnouncements({ messages, ...props }) {
762
909
  else if (change.action === "remove" || change.action === "removeBatch") setMessage(removedMessage);
763
910
  else if (change.action === "updateNote") setMessage(noteSavedMessage);
764
911
  }, [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 });
912
+ return /* @__PURE__ */ jsx13("div", { ...props, role: props.role ?? "status", "aria-live": props["aria-live"] ?? "polite", "aria-atomic": "true", children: message });
766
913
  }
914
+ var KeepAnnouncer = KeepAnnouncements;
767
915
  function getDerivedStatus(context) {
768
916
  if (context.error) return "error";
769
917
  if (context.syncState.status === "pending" || context.syncState.status === "syncing") return "syncing";
@@ -780,46 +928,76 @@ function getStatusLabelKey(status) {
780
928
  if (status === "syncing") return "syncing";
781
929
  return "saved";
782
930
  }
783
- function resolveContent(content, state) {
784
- return typeof content === "function" ? content(state) : content;
931
+
932
+ // src/index.tsx
933
+ import { KeepProvider, useKeepContext as useKeepContext2, useKeepItem as useKeepItem5, useKeepList as useKeepList5, useKeepShortcut } from "@keepkit/core/react";
934
+ import {
935
+ createBrowserStorageAdapter,
936
+ createStorageAdapter,
937
+ FallbackStorageAdapter,
938
+ IndexedDBAdapter,
939
+ IndexedDBSyncQueueAdapter,
940
+ LocalStorageAdapter,
941
+ LocalStorageSyncQueueAdapter,
942
+ SyncStorageAdapter
943
+ } from "@keepkit/core/storage";
944
+ import { jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
945
+ function KeepKitProvider({
946
+ labels,
947
+ locale,
948
+ labelResolver,
949
+ children,
950
+ ...providerProps
951
+ }) {
952
+ return /* @__PURE__ */ jsx14(KeepUiProvider, { labels, locale, labelResolver, children: /* @__PURE__ */ jsxs9(CoreKeepProvider, { ...providerProps, children: [
953
+ children,
954
+ /* @__PURE__ */ jsx14(KeepAnnouncements, {})
955
+ ] }) });
785
956
  }
786
- function toKeepButtonItem(item) {
957
+ function createKeepKit(options = {}) {
958
+ const { labels, locale, labelResolver, getTitle, getImageProps, ...coreOptions } = options;
959
+ const coreKit = createCoreKeepKit(coreOptions);
787
960
  return {
788
- id: item.id,
789
- meta: item.meta,
790
- targetType: item.targetType,
791
- note: item.note,
792
- tags: item.tags
961
+ Provider: (props) => /* @__PURE__ */ jsx14(
962
+ KeepKitProvider,
963
+ {
964
+ ...coreOptions,
965
+ labels,
966
+ locale,
967
+ labelResolver,
968
+ ...props
969
+ }
970
+ ),
971
+ Button: (props) => /* @__PURE__ */ jsx14(KeepButton, { ...props }),
972
+ Collection: (props) => /* @__PURE__ */ jsx14(
973
+ KeepCollection,
974
+ {
975
+ ...props,
976
+ itemCardProps: {
977
+ ...getTitle ? { getTitle } : {},
978
+ ...getImageProps ? { getImageProps } : {},
979
+ ...props.itemCardProps
980
+ }
981
+ }
982
+ ),
983
+ useContext: () => coreKit.useContext(),
984
+ useItem: (item) => coreKit.useItem(item),
985
+ useList: (query) => coreKit.useList(query),
986
+ useShortcut: (shortcutOptions) => coreKit.useShortcut(shortcutOptions)
793
987
  };
794
988
  }
795
- function getMetaTitle(meta) {
796
- if (typeof meta !== "object" || meta === null || !("title" in meta)) return void 0;
797
- const title = meta.title;
798
- return typeof title === "string" && title.trim() ? title.trim() : void 0;
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
- }
806
- function renderRoot(asChild, child, props, body, componentName) {
807
- if (asChild) {
808
- if (!isValidElement(child)) throw new Error(`${componentName} with asChild requires a single React element child.`);
809
- return cloneElement(child, { ...props, children: body });
810
- }
811
- return /* @__PURE__ */ jsx("div", { ...props, children: body });
812
- }
813
989
  export {
814
990
  FallbackStorageAdapter,
815
991
  IndexedDBAdapter,
816
992
  IndexedDBSyncQueueAdapter,
817
993
  KeepAnnouncements,
994
+ KeepAnnouncer,
818
995
  KeepBulkActions,
819
996
  KeepButton,
820
997
  KeepCollection,
821
998
  KeepEmptyState,
822
999
  KeepItemCard,
1000
+ KeepItemCheckbox,
823
1001
  KeepKitProvider,
824
1002
  KeepList,
825
1003
  KeepNoteEditor,
@@ -838,8 +1016,8 @@ export {
838
1016
  createKeepKit,
839
1017
  createStorageAdapter,
840
1018
  useKeepContext2 as useKeepContext,
841
- useKeepItem2 as useKeepItem,
842
- useKeepList2 as useKeepList,
1019
+ useKeepItem5 as useKeepItem,
1020
+ useKeepList5 as useKeepList,
843
1021
  useKeepShortcut,
844
1022
  useKeepUiLabels
845
1023
  };