@keepkit/ui 0.6.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,22 +2,13 @@
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";
12
- import {
13
- cloneElement,
14
- isValidElement,
15
- useCallback,
16
- useEffect as useEffect2,
17
- useMemo as useMemo2,
18
- useRef,
19
- useState as useState2
20
- } from "react";
8
+
9
+ // src/KeepBulkActions.tsx
10
+ import { useKeepList } from "@keepkit/core/react";
11
+ import { useState } from "react";
21
12
 
22
13
  // src/KeepItemCheckbox.tsx
23
14
  import { jsx } from "react/jsx-runtime";
@@ -48,12 +39,46 @@ function getItemLabel(item) {
48
39
  return typeof title === "string" && title.trim() ? title.trim() : void 0;
49
40
  }
50
41
 
51
- // src/query-controls.tsx
52
- import { useEffect, useState } from "react";
42
+ // src/shared.tsx
43
+ import {
44
+ cloneElement,
45
+ isValidElement
46
+ } from "react";
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
+ }
53
78
 
54
79
  // src/ui-context.tsx
55
80
  import { createContext, useContext, useMemo } from "react";
56
- import { jsx as jsx2 } from "react/jsx-runtime";
81
+ import { jsx as jsx3 } from "react/jsx-runtime";
57
82
  var DEFAULT_LABELS = {
58
83
  save: "Save",
59
84
  saved: "Saved",
@@ -96,7 +121,7 @@ function KeepUiProvider({ labels, locale, labelResolver, children }) {
96
121
  const resolved = { ...DEFAULT_LABELS, ...labels };
97
122
  return { locale, labels: resolved, labelResolver };
98
123
  }, [labelResolver, labels, locale]);
99
- return /* @__PURE__ */ jsx2(KeepUiLabelsContext.Provider, { value, children });
124
+ return /* @__PURE__ */ jsx3(KeepUiLabelsContext.Provider, { value, children });
100
125
  }
101
126
  function useKeepUiLabels() {
102
127
  return useContext(KeepUiLabelsContext);
@@ -106,183 +131,106 @@ function useUiLabel(key, override) {
106
131
  return override ?? context.labelResolver?.(key, { locale: context.locale }) ?? context.labels[key];
107
132
  }
108
133
 
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,
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,
190
143
  render,
144
+ children,
191
145
  ...props
192
146
  }) {
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);
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);
202
160
  };
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(
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(
209
216
  "button",
210
217
  {
211
218
  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 })
219
+ onClick: () => void updateTags(),
220
+ disabled: selectedIds.length === 0 || list.isMutating,
221
+ children: applyTagsLabel
222
+ }
223
+ )
220
224
  ] });
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);
225
+ return /* @__PURE__ */ jsx4("section", { ...props, "aria-busy": list.isMutating || props["aria-busy"], children: body });
227
226
  }
228
227
 
229
- // src/index.tsx
230
- import { KeepProvider, useKeepContext as useKeepContext2, useKeepItem as useKeepItem2, useKeepList as useKeepList2, useKeepShortcut } from "@keepkit/core/react";
228
+ // src/KeepButton.tsx
231
229
  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
- }
230
+ KeepButton as CoreKeepButton,
231
+ useKeepItem
232
+ } from "@keepkit/core/react";
233
+ import { Fragment as Fragment2, jsx as jsx5 } from "react/jsx-runtime";
286
234
  function KeepButton({ labels, ...props }) {
287
235
  const saveLabel = useUiLabel("save", typeof labels?.unsaved === "string" ? labels.unsaved : void 0);
288
236
  const savedLabel = useUiLabel("saved", typeof labels?.saved === "string" ? labels.saved : void 0);
@@ -297,22 +245,6 @@ function KeepButton({ labels, ...props }) {
297
245
  if (props.children !== void 0) return props.children;
298
246
  return state.isSaved ? labels?.saved ?? savedLabel : labels?.unsaved ?? saveLabel;
299
247
  };
300
- if (props.asChild === true) {
301
- const sharedProps2 = {
302
- ...props,
303
- "aria-busy": props["aria-busy"] ?? (buttonState.isLoading || buttonState.isMutating),
304
- savedLabel: labels?.saved ?? props.savedLabel ?? savedLabel,
305
- unsavedLabel: labels?.unsaved ?? props.unsavedLabel ?? saveLabel,
306
- savedAriaLabel: labels?.savedAriaLabel ?? props.savedAriaLabel,
307
- unsavedAriaLabel: labels?.unsavedAriaLabel ?? props.unsavedAriaLabel
308
- };
309
- if (!customStateLabel) return /* @__PURE__ */ jsx4(CoreKeepButton, { ...sharedProps2 });
310
- const statefulProps2 = {
311
- ...sharedProps2,
312
- children: (state) => /* @__PURE__ */ jsx4(Fragment2, { children: getStateContent(state) })
313
- };
314
- return /* @__PURE__ */ jsx4(CoreKeepButton, { ...statefulProps2 });
315
- }
316
248
  const sharedProps = {
317
249
  ...props,
318
250
  "aria-busy": props["aria-busy"] ?? (buttonState.isLoading || buttonState.isMutating),
@@ -321,10 +253,22 @@ function KeepButton({ labels, ...props }) {
321
253
  savedAriaLabel: labels?.savedAriaLabel ?? props.savedAriaLabel,
322
254
  unsavedAriaLabel: labels?.unsavedAriaLabel ?? props.unsavedAriaLabel
323
255
  };
324
- if (!customStateLabel) return /* @__PURE__ */ jsx4(CoreKeepButton, { ...sharedProps });
325
- const statefulProps = { ...sharedProps, children: (state) => getStateContent(state) };
326
- return /* @__PURE__ */ jsx4(CoreKeepButton, { ...statefulProps });
256
+ if (!customStateLabel) return /* @__PURE__ */ jsx5(CoreKeepButton, { ...sharedProps });
257
+ return /* @__PURE__ */ jsx5(CoreKeepButton, { ...sharedProps, children: (state) => /* @__PURE__ */ jsx5(Fragment2, { children: getStateContent(state) }) });
327
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";
328
272
  function KeepItemCard({
329
273
  item,
330
274
  title,
@@ -346,8 +290,8 @@ function KeepItemCard({
346
290
  }) {
347
291
  const saveActionLabel = useUiLabel("save");
348
292
  const removeActionLabel = useUiLabel("remove");
349
- const itemState = useKeepItem(item);
350
- const contentChildren = asChild && isValidElement(children) ? void 0 : children;
293
+ const itemState = useKeepItem2(item);
294
+ const contentChildren = asChild && isValidElement2(children) ? void 0 : children;
351
295
  const state = {
352
296
  item,
353
297
  isSaved: itemState.isSaved,
@@ -365,10 +309,10 @@ function KeepItemCard({
365
309
  onRemoveError?.(error);
366
310
  }
367
311
  }
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(
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(
372
316
  KeepButton,
373
317
  {
374
318
  item: toKeepButtonItem(item),
@@ -376,16 +320,19 @@ function KeepItemCard({
376
320
  getAriaLabel: (buttonState) => `${buttonState.isSaved ? removeActionLabel : saveActionLabel} ${String(resolvedTitle)}`
377
321
  }
378
322
  ) : null,
379
- /* @__PURE__ */ jsx4("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 })
380
324
  ] });
381
325
  return renderRoot(
382
326
  asChild,
383
- isValidElement(children) ? children : void 0,
327
+ isValidElement2(children) ? children : void 0,
384
328
  { ...rootProps, className, "aria-busy": itemState.isMutating || rootProps["aria-busy"] },
385
329
  body,
386
330
  "KeepItemCard"
387
331
  );
388
332
  }
333
+
334
+ // src/KeepList.tsx
335
+ import { jsx as jsx7 } from "react/jsx-runtime";
389
336
  function KeepList({
390
337
  query,
391
338
  children,
@@ -401,7 +348,7 @@ function KeepList({
401
348
  const defaultLoading = useUiLabel("loadingItems");
402
349
  const defaultEmpty = useUiLabel("noItems");
403
350
  const defaultError = useUiLabel("errorItems");
404
- const state = useKeepList(query);
351
+ const state = useKeepList2(query);
405
352
  const body = getListBody(state, {
406
353
  children,
407
354
  renderItem,
@@ -410,26 +357,209 @@ function KeepList({
410
357
  error: errorContent ?? defaultError,
411
358
  itemCardProps
412
359
  });
413
- const root = asChild && isValidElement(children) ? children : void 0;
414
360
  return renderRoot(
415
361
  asChild,
416
- root,
362
+ asChild && isValidElement3(children) ? children : void 0,
417
363
  { ...rootProps, className, "aria-busy": state.isLoading || rootProps["aria-busy"] },
418
364
  body,
419
365
  "KeepList"
420
366
  );
421
367
  }
422
- function getListBody(state, options) {
423
- if (state.error && state.items.length === 0) return resolveContent(options.error, state);
424
- if (state.isLoading && !state.isHydrated) return resolveContent(options.loading, state);
425
- if (state.isHydrated && state.items.length === 0) return resolveContent(options.empty, state);
426
- if (typeof options.children === "function") return options.children(state);
427
- if (options.children !== void 0 && !isValidElement(options.children)) return options.children;
428
- const items = state.items.map(
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 });
368
+ function getListBody(state, options) {
369
+ if (state.error && state.items.length === 0) return resolveContent(options.error, state);
370
+ if (state.isLoading && !state.isHydrated) return resolveContent(options.loading, state);
371
+ if (state.isHydrated && state.items.length === 0) return resolveContent(options.empty, state);
372
+ if (typeof options.children === "function") return options.children(state);
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"
437
+ );
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);
432
559
  }
560
+
561
+ // src/KeepCollection.tsx
562
+ import { jsx as jsx10, jsxs as jsxs5 } from "react/jsx-runtime";
433
563
  function KeepCollection({
434
564
  query = {},
435
565
  pageSize = 20,
@@ -450,12 +580,12 @@ function KeepCollection({
450
580
  bulkActions: false,
451
581
  ...features
452
582
  };
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);
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);
457
587
  const resolvedPageSize = query.pagination?.pageSize ?? pageSize;
458
- const resolvedQuery = useMemo2(
588
+ const resolvedQuery = useMemo3(
459
589
  () => ({
460
590
  ...query,
461
591
  search: enabled.search ? { ...query.search, query: searchValue } : query.search,
@@ -465,25 +595,48 @@ function KeepCollection({
465
595
  }),
466
596
  [enabled.pagination, enabled.search, enabled.sort, page, query, resolvedPageSize, searchValue, sort, tag]
467
597
  );
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(
598
+ const list = useKeepList4(resolvedQuery);
599
+ return /* @__PURE__ */ jsxs5(
475
600
  "section",
476
601
  {
477
602
  ...rootProps,
478
603
  className,
479
604
  "aria-busy": list.isLoading || list.isMutating || rootProps["aria-busy"],
480
605
  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
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
485
638
  ] }),
486
- /* @__PURE__ */ jsx4(
639
+ /* @__PURE__ */ jsx10(
487
640
  KeepList,
488
641
  {
489
642
  query: resolvedQuery,
@@ -494,80 +647,37 @@ function KeepCollection({
494
647
  error
495
648
  }
496
649
  ),
497
- enabled.pagination ? /* @__PURE__ */ jsx4(
650
+ enabled.pagination ? /* @__PURE__ */ jsx10(
498
651
  KeepPagination,
499
652
  {
500
653
  totalCount: list.totalCount,
501
654
  pageSize: resolvedPageSize,
502
655
  page: list.page,
503
- onPageChange: (_nextPage, nextPageOffset) => setPage(Math.floor(nextPageOffset / resolvedPageSize) + 1)
656
+ onPageChange: (nextPage) => setPage(nextPage)
504
657
  }
505
658
  ) : null,
506
- enabled.bulkActions ? /* @__PURE__ */ jsx4(KeepBulkActions, { query: resolvedQuery }) : null
659
+ enabled.bulkActions ? /* @__PURE__ */ jsx10(KeepBulkActions, { query: resolvedQuery }) : null
507
660
  ]
508
661
  }
509
662
  );
510
663
  }
511
- function KeepTagFilter({
512
- query,
513
- value: controlledValue,
514
- defaultValue,
515
- onChange,
516
- onValueChange,
517
- allLabel,
518
- ariaLabel,
519
- renderTag,
520
- render,
521
- children,
522
- asChild = false,
523
- className,
524
- ...rootProps
525
- }) {
526
- const uiAllLabel = useUiLabel("allTags");
527
- const uiAriaLabel = useUiLabel("filterTags");
528
- const defaultAllLabel = allLabel ?? uiAllLabel;
529
- const defaultAriaLabel = ariaLabel ?? uiAriaLabel;
530
- const contentChildren = asChild && isValidElement(children) ? void 0 : children;
531
- const [uncontrolledValue, setUncontrolledValue] = useState2(defaultValue);
532
- const value = controlledValue ?? uncontrolledValue;
533
- const list = useKeepList({ ...query, tags: value ? [...query?.tags ?? [], value] : query?.tags });
534
- const select = useCallback(
535
- (tag) => {
536
- if (controlledValue === void 0) setUncontrolledValue(tag);
537
- onChange?.(tag);
538
- onValueChange?.(tag);
539
- },
540
- [controlledValue, onChange, onValueChange]
541
- );
542
- const state = useMemo2(
543
- () => ({ tags: list.tags, tagCounts: list.tagCounts, value, select }),
544
- [list.tagCounts, list.tags, select, value]
545
- );
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: [
550
- renderTag ? renderTag(tag, list.tagCounts[tag] ?? 0, value === tag) : tag,
551
- /* @__PURE__ */ jsxs2("span", { children: [
552
- " (",
553
- list.tagCounts[tag] ?? 0,
554
- ")"
555
- ] })
556
- ] }, tag))
557
- ] });
558
- return renderRoot(
559
- asChild,
560
- isValidElement(children) ? children : void 0,
561
- { ...rootProps, className },
562
- body,
563
- "KeepTagFilter"
564
- );
565
- }
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";
566
675
  function KeepNoteEditor({
567
676
  item,
568
677
  label,
569
678
  saveLabel,
570
679
  placeholder,
680
+ debounceMs = 300,
571
681
  onSaved,
572
682
  onSaveError,
573
683
  render,
@@ -578,68 +688,89 @@ function KeepNoteEditor({
578
688
  }) {
579
689
  const defaultLabel = useUiLabel("note");
580
690
  const defaultSaveLabel = useUiLabel("saveNote");
581
- const itemState = useKeepItem(item);
582
- const contentChildren = asChild && isValidElement(children) ? void 0 : children;
583
- const [note, setNote] = useState2(item.note ?? "");
584
- useEffect2(() => setNote(itemState.item?.note ?? item.note ?? ""), [item.note, itemState.item?.note]);
585
- 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 () => {
586
700
  const nextNote = note.trim() || void 0;
587
701
  try {
588
- await itemState.updateNote(nextNote);
702
+ await updateNote(nextNote);
703
+ lastSavedNoteRef.current = note;
589
704
  onSaved?.(nextNote);
590
- } catch (error) {
591
- onSaveError?.(error);
592
- throw error;
705
+ } catch (error2) {
706
+ onSaveError?.(error2);
707
+ throw error2;
593
708
  }
594
- }, [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]);
595
715
  const state = {
596
716
  item,
597
717
  note,
598
718
  setNote,
599
- isDirty: note !== (itemState.item?.note ?? item.note ?? ""),
600
- isSaving: itemState.isMutating,
601
- error: itemState.error,
719
+ isDirty,
720
+ isSaving: isMutating,
721
+ error,
602
722
  save
603
723
  };
604
- const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? /* @__PURE__ */ jsxs2(Fragment2, { children: [
605
- /* @__PURE__ */ jsxs2("label", { children: [
724
+ const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? /* @__PURE__ */ jsxs6(Fragment5, { children: [
725
+ /* @__PURE__ */ jsxs6("label", { children: [
606
726
  label ?? defaultLabel,
607
- /* @__PURE__ */ jsx4(
727
+ /* @__PURE__ */ jsx11(
608
728
  "textarea",
609
729
  {
610
730
  value: note,
611
731
  onChange: (event) => setNote(event.currentTarget.value),
612
732
  placeholder,
613
- 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
+ }
614
740
  }
615
741
  )
616
742
  ] }),
617
- /* @__PURE__ */ jsx4("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 })
618
744
  ] });
619
745
  const handleSubmit = (event) => {
620
746
  event.preventDefault();
621
747
  void save().catch(() => void 0);
622
748
  };
623
749
  if (!asChild) {
624
- return /* @__PURE__ */ jsx4(
750
+ return /* @__PURE__ */ jsx11(
625
751
  "form",
626
752
  {
627
753
  ...formProps,
628
754
  className,
629
755
  onSubmit: handleSubmit,
630
- "aria-busy": itemState.isMutating || formProps["aria-busy"],
756
+ "aria-busy": isMutating || formProps["aria-busy"],
631
757
  children: body
632
758
  }
633
759
  );
634
760
  }
635
761
  return renderRoot(
636
762
  true,
637
- isValidElement(children) ? children : void 0,
638
- { ...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"] },
639
765
  body,
640
766
  "KeepNoteEditor"
641
767
  );
642
768
  }
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";
643
774
  function KeepTagEditor({
644
775
  item,
645
776
  availableTags = [],
@@ -651,11 +782,11 @@ function KeepTagEditor({
651
782
  const tagsLabel = useUiLabel("tagsToApply");
652
783
  const removeLabel = useUiLabel("remove");
653
784
  const applyTagsLabel = useUiLabel("applyTags");
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]);
658
- 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 () => {
659
790
  const nextTags = normalizeUiTags(tags);
660
791
  try {
661
792
  await itemState.updateTags(nextTags);
@@ -667,14 +798,13 @@ function KeepTagEditor({
667
798
  }
668
799
  }, [itemState, onSaveError, onSaved, tags]);
669
800
  const addTag = (tag) => {
670
- const next = normalizeUiTags([...tags, tag]);
671
- setTags(next);
801
+ setTags(normalizeUiTags([...tags, tag]));
672
802
  setInput("");
673
803
  };
674
- const body = render ? render({ tags, setTags, save, isSaving: itemState.isMutating }) : /* @__PURE__ */ jsxs2(Fragment2, { children: [
675
- /* @__PURE__ */ jsxs2("label", { children: [
804
+ const body = render ? render({ tags, setTags, save, isSaving: itemState.isMutating }) : /* @__PURE__ */ jsxs7(Fragment6, { children: [
805
+ /* @__PURE__ */ jsxs7("label", { children: [
676
806
  tagsLabel,
677
- /* @__PURE__ */ jsx4(
807
+ /* @__PURE__ */ jsx12(
678
808
  "input",
679
809
  {
680
810
  value: input,
@@ -692,14 +822,14 @@ function KeepTagEditor({
692
822
  }
693
823
  )
694
824
  ] }),
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: [
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: [
697
827
  tag,
698
- /* @__PURE__ */ jsx4("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 })
699
829
  ] }, tag)) }),
700
- /* @__PURE__ */ jsx4("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 })
701
831
  ] });
702
- return /* @__PURE__ */ jsx4(
832
+ return /* @__PURE__ */ jsx12(
703
833
  "form",
704
834
  {
705
835
  ...props,
@@ -712,96 +842,11 @@ function KeepTagEditor({
712
842
  }
713
843
  );
714
844
  }
715
- function KeepBulkActions({
716
- query,
717
- selectedIds: controlledSelectedIds,
718
- defaultSelectedIds = [],
719
- onSelectedIdsChange,
720
- renderItem,
721
- onCompleted,
722
- render,
723
- children,
724
- ...props
725
- }) {
726
- const selectItemsLabel = useUiLabel("selectItems");
727
- const selectedCountLabel = useUiLabel("selectedCount");
728
- const deleteSelectedLabel = useUiLabel("deleteSelected");
729
- const tagsLabel = useUiLabel("tagsToApply");
730
- const applyTagsLabel = useUiLabel("applyTags");
731
- const list = useKeepList(query);
732
- const [uncontrolledSelectedIds, setUncontrolledSelectedIds] = useState2(defaultSelectedIds);
733
- const selectedIds = controlledSelectedIds ?? uncontrolledSelectedIds;
734
- const selected = new Set(selectedIds);
735
- const [tagsInput, setTagsInput] = useState2("");
736
- const setSelectedIds = (ids) => {
737
- if (controlledSelectedIds === void 0) setUncontrolledSelectedIds(ids);
738
- onSelectedIdsChange?.(ids);
739
- };
740
- const toggle = (id) => setSelectedIds(selected.has(id) ? selectedIds.filter((current) => current !== id) : [...selectedIds, id]);
741
- const allSelected = list.items.length > 0 && list.items.every((item) => selected.has(item.id));
742
- const toggleAll = () => setSelectedIds(allSelected ? [] : list.items.map((item) => item.id));
743
- const remove = async () => {
744
- await list.removeBatch(selectedIds);
745
- onCompleted?.("remove", selectedIds);
746
- setSelectedIds([]);
747
- };
748
- const updateTags = async () => {
749
- const tags = normalizeUiTags(tagsInput.split(","));
750
- await list.updateTagsBatch(selectedIds, tags);
751
- onCompleted?.("tags", selectedIds);
752
- setSelectedIds([]);
753
- };
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 }),
772
- selectedIds.length,
773
- " ",
774
- selectedCountLabel
775
- ] }),
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
- ),
785
- renderItem ? renderItem(item, selected.has(item.id)) : getMetaTitle(item.meta) ?? item.id
786
- ] }, item.id))
787
- ] }),
788
- /* @__PURE__ */ jsx4("button", { type: "button", onClick: () => void remove(), disabled: selectedIds.length === 0 || list.isMutating, children: deleteSelectedLabel }),
789
- /* @__PURE__ */ jsxs2("label", { children: [
790
- tagsLabel,
791
- /* @__PURE__ */ jsx4("input", { value: tagsInput, onChange: (event) => setTagsInput(event.currentTarget.value) })
792
- ] }),
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
- )
802
- ] });
803
- return /* @__PURE__ */ jsx4("section", { ...props, "aria-busy": list.isMutating || props["aria-busy"], children: body });
804
- }
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";
805
850
  function KeepEmptyState({
806
851
  title,
807
852
  description,
@@ -812,10 +857,10 @@ function KeepEmptyState({
812
857
  ...rootProps
813
858
  }) {
814
859
  const defaultTitle = useUiLabel("noItems").replace(/\.$/, "");
815
- const contentChildren = asChild && isValidElement(children) ? void 0 : children;
816
- const body = contentChildren ?? /* @__PURE__ */ jsxs2(Fragment2, { children: [
817
- /* @__PURE__ */ jsx4("h2", { children: title ?? defaultTitle }),
818
- description ? /* @__PURE__ */ jsx4("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,
819
864
  action
820
865
  ] });
821
866
  return renderRoot(asChild, children, { ...rootProps, className }, body, "KeepEmptyState");
@@ -830,7 +875,7 @@ function KeepStatus({
830
875
  ...rootProps
831
876
  }) {
832
877
  const context = useKeepContext();
833
- const contentChildren = asChild && isValidElement(children) ? void 0 : children;
878
+ const contentChildren = asChild && isValidElement6(children) ? void 0 : children;
834
879
  const resolvedStatus = status ?? getDerivedStatus(context);
835
880
  const defaultLabel = useUiLabel(getStatusLabelKey(resolvedStatus));
836
881
  const state = {
@@ -843,7 +888,7 @@ function KeepStatus({
843
888
  const role = rootProps.role ?? (resolvedStatus === "error" ? "alert" : "status");
844
889
  return renderRoot(
845
890
  asChild,
846
- isValidElement(children) ? children : void 0,
891
+ isValidElement6(children) ? children : void 0,
847
892
  { ...rootProps, className, role, "aria-live": rootProps["aria-live"] ?? "polite" },
848
893
  body,
849
894
  "KeepStatus"
@@ -854,9 +899,9 @@ function KeepAnnouncements({ messages, ...props }) {
854
899
  const savedMessage = useUiLabel("savedMessage", messages?.save);
855
900
  const removedMessage = useUiLabel("removedMessage", messages?.remove);
856
901
  const noteSavedMessage = useUiLabel("noteSavedMessage", messages?.note);
857
- const [message, setMessage] = useState2("");
858
- const lastChangeRef = useRef(void 0);
859
- useEffect2(() => {
902
+ const [message, setMessage] = useState7("");
903
+ const lastChangeRef = useRef2(void 0);
904
+ useEffect4(() => {
860
905
  const change = context.lastChange;
861
906
  if (!change || change === lastChangeRef.current) return;
862
907
  lastChangeRef.current = change;
@@ -864,8 +909,9 @@ function KeepAnnouncements({ messages, ...props }) {
864
909
  else if (change.action === "remove" || change.action === "removeBatch") setMessage(removedMessage);
865
910
  else if (change.action === "updateNote") setMessage(noteSavedMessage);
866
911
  }, [context.lastChange, noteSavedMessage, removedMessage, savedMessage]);
867
- return /* @__PURE__ */ jsx4("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 });
868
913
  }
914
+ var KeepAnnouncer = KeepAnnouncements;
869
915
  function getDerivedStatus(context) {
870
916
  if (context.error) return "error";
871
917
  if (context.syncState.status === "pending" || context.syncState.status === "syncing") return "syncing";
@@ -882,41 +928,70 @@ function getStatusLabelKey(status) {
882
928
  if (status === "syncing") return "syncing";
883
929
  return "saved";
884
930
  }
885
- function resolveContent(content, state) {
886
- 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
+ ] }) });
887
956
  }
888
- function toKeepButtonItem(item) {
957
+ function createKeepKit(options = {}) {
958
+ const { labels, locale, labelResolver, getTitle, getImageProps, ...coreOptions } = options;
959
+ const coreKit = createCoreKeepKit(coreOptions);
889
960
  return {
890
- id: item.id,
891
- meta: item.meta,
892
- targetType: item.targetType,
893
- note: item.note,
894
- 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)
895
987
  };
896
988
  }
897
- function getMetaTitle(meta) {
898
- if (typeof meta !== "object" || meta === null || !("title" in meta)) return void 0;
899
- const title = meta.title;
900
- return typeof title === "string" && title.trim() ? title.trim() : void 0;
901
- }
902
- function normalizeUiTags(tags) {
903
- return [...new Set(tags.map((tag) => tag.trim()).filter(Boolean))];
904
- }
905
- function sortToValue(sort) {
906
- return `${sort?.by ?? "updatedAt"}:${sort?.direction ?? "desc"}`;
907
- }
908
- function renderRoot(asChild, child, props, body, componentName) {
909
- if (asChild) {
910
- if (!isValidElement(child)) throw new Error(`${componentName} with asChild requires a single React element child.`);
911
- return cloneElement(child, { ...props, children: body });
912
- }
913
- return /* @__PURE__ */ jsx4("div", { ...props, children: body });
914
- }
915
989
  export {
916
990
  FallbackStorageAdapter,
917
991
  IndexedDBAdapter,
918
992
  IndexedDBSyncQueueAdapter,
919
993
  KeepAnnouncements,
994
+ KeepAnnouncer,
920
995
  KeepBulkActions,
921
996
  KeepButton,
922
997
  KeepCollection,
@@ -941,8 +1016,8 @@ export {
941
1016
  createKeepKit,
942
1017
  createStorageAdapter,
943
1018
  useKeepContext2 as useKeepContext,
944
- useKeepItem2 as useKeepItem,
945
- useKeepList2 as useKeepList,
1019
+ useKeepItem5 as useKeepItem,
1020
+ useKeepList5 as useKeepList,
946
1021
  useKeepShortcut,
947
1022
  useKeepUiLabels
948
1023
  };