@keepkit/ui 0.6.0 → 0.8.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,122 @@ 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
- );
134
+ // src/KeepBulkActions.tsx
135
+ import { Fragment, jsx as jsx4, jsxs } from "react/jsx-runtime";
136
+ function isAllSelected(items, selectedIds) {
137
+ if (items.length === 0) return false;
138
+ const selected = new Set(selectedIds);
139
+ return items.every((item) => selected.has(item.id));
146
140
  }
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
- );
141
+ function toggleSelectAll(items, selectedIds) {
142
+ const visibleIds = new Set(items.map((item) => item.id));
143
+ if (isAllSelected(items, selectedIds)) return selectedIds.filter((id) => !visibleIds.has(id));
144
+ const nextIds = [...selectedIds];
145
+ for (const item of items) {
146
+ if (!nextIds.includes(item.id)) nextIds.push(item.id);
147
+ }
148
+ return nextIds;
183
149
  }
184
- function KeepPagination({
185
- totalCount,
186
- pageSize,
187
- page = 1,
188
- maxPageButtons = 7,
189
- onPageChange,
150
+ function KeepBulkActions({
151
+ query,
152
+ selectedIds: controlledSelectedIds,
153
+ defaultSelectedIds = [],
154
+ onSelectedIdsChange,
155
+ renderItem,
156
+ onCompleted,
190
157
  render,
158
+ children,
191
159
  ...props
192
160
  }) {
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);
161
+ const selectItemsLabel = useUiLabel("selectItems");
162
+ const selectedCountLabel = useUiLabel("selectedCount");
163
+ const deleteSelectedLabel = useUiLabel("deleteSelected");
164
+ const tagsLabel = useUiLabel("tagsToApply");
165
+ const applyTagsLabel = useUiLabel("applyTags");
166
+ const list = useKeepList(query);
167
+ const [uncontrolledSelectedIds, setUncontrolledSelectedIds] = useState(defaultSelectedIds);
168
+ const selectedIds = controlledSelectedIds ?? uncontrolledSelectedIds;
169
+ const selected = new Set(selectedIds);
170
+ const [tagsInput, setTagsInput] = useState("");
171
+ const setSelectedIds = (ids) => {
172
+ if (controlledSelectedIds === void 0) setUncontrolledSelectedIds(ids);
173
+ onSelectedIdsChange?.(ids);
202
174
  };
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(
175
+ const toggle = (id) => setSelectedIds(selected.has(id) ? selectedIds.filter((current) => current !== id) : [...selectedIds, id]);
176
+ const allSelected = isAllSelected(list.items, selectedIds);
177
+ const toggleAll = () => setSelectedIds(toggleSelectAll(list.items, selectedIds));
178
+ const remove = async () => {
179
+ const ids = [...selectedIds];
180
+ await list.removeBatch(ids);
181
+ onCompleted?.("remove", ids);
182
+ setSelectedIds([]);
183
+ };
184
+ const updateTags = async () => {
185
+ const ids = [...selectedIds];
186
+ await list.updateTagsBatch(ids, normalizeUiTags(tagsInput.split(",")));
187
+ onCompleted?.("tags", ids);
188
+ setSelectedIds([]);
189
+ };
190
+ const state = {
191
+ items: list.items,
192
+ selectedIds,
193
+ selectedCount: selectedIds.length,
194
+ isAllSelected: allSelected,
195
+ allSelected,
196
+ tagsInput,
197
+ setTagsInput,
198
+ toggle,
199
+ toggleSelectAll: toggleAll,
200
+ toggleAll,
201
+ remove,
202
+ updateTags,
203
+ isMutating: list.isMutating
204
+ };
205
+ const body = render ? render(state) : typeof children === "function" ? children(state) : children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
206
+ /* @__PURE__ */ jsxs("fieldset", { children: [
207
+ /* @__PURE__ */ jsx4("legend", { children: selectItemsLabel }),
208
+ /* @__PURE__ */ jsxs("label", { children: [
209
+ /* @__PURE__ */ jsx4("input", { type: "checkbox", checked: allSelected, onChange: toggleAll, "aria-label": selectItemsLabel }),
210
+ selectedIds.length,
211
+ " ",
212
+ selectedCountLabel
213
+ ] }),
214
+ list.items.map((item) => /* @__PURE__ */ jsxs("span", { children: [
215
+ /* @__PURE__ */ jsx4(
216
+ KeepItemCheckbox,
217
+ {
218
+ item,
219
+ checked: selected.has(item.id),
220
+ onCheckedChange: () => toggle(item.id)
221
+ }
222
+ ),
223
+ renderItem ? renderItem(item, selected.has(item.id)) : getMetaTitle(item.meta) ?? item.id
224
+ ] }, item.id))
225
+ ] }),
226
+ /* @__PURE__ */ jsx4("button", { type: "button", onClick: () => void remove(), disabled: selectedIds.length === 0 || list.isMutating, children: deleteSelectedLabel }),
227
+ /* @__PURE__ */ jsxs("label", { children: [
228
+ tagsLabel,
229
+ /* @__PURE__ */ jsx4("input", { value: tagsInput, onChange: (event) => setTagsInput(event.currentTarget.value) })
230
+ ] }),
231
+ /* @__PURE__ */ jsx4(
209
232
  "button",
210
233
  {
211
234
  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 })
235
+ onClick: () => void updateTags(),
236
+ disabled: selectedIds.length === 0 || list.isMutating,
237
+ children: applyTagsLabel
238
+ }
239
+ )
220
240
  ] });
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);
241
+ return /* @__PURE__ */ jsx4("section", { ...props, "aria-busy": list.isMutating || props["aria-busy"], children: body });
227
242
  }
228
243
 
229
- // src/index.tsx
230
- import { KeepProvider, useKeepContext as useKeepContext2, useKeepItem as useKeepItem2, useKeepList as useKeepList2, useKeepShortcut } from "@keepkit/core/react";
244
+ // src/KeepButton.tsx
231
245
  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
- }
246
+ KeepButton as CoreKeepButton,
247
+ useKeepItem
248
+ } from "@keepkit/core/react";
249
+ import { Fragment as Fragment2, jsx as jsx5 } from "react/jsx-runtime";
286
250
  function KeepButton({ labels, ...props }) {
287
251
  const saveLabel = useUiLabel("save", typeof labels?.unsaved === "string" ? labels.unsaved : void 0);
288
252
  const savedLabel = useUiLabel("saved", typeof labels?.saved === "string" ? labels.saved : void 0);
@@ -297,22 +261,6 @@ function KeepButton({ labels, ...props }) {
297
261
  if (props.children !== void 0) return props.children;
298
262
  return state.isSaved ? labels?.saved ?? savedLabel : labels?.unsaved ?? saveLabel;
299
263
  };
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
264
  const sharedProps = {
317
265
  ...props,
318
266
  "aria-busy": props["aria-busy"] ?? (buttonState.isLoading || buttonState.isMutating),
@@ -321,10 +269,22 @@ function KeepButton({ labels, ...props }) {
321
269
  savedAriaLabel: labels?.savedAriaLabel ?? props.savedAriaLabel,
322
270
  unsavedAriaLabel: labels?.unsavedAriaLabel ?? props.unsavedAriaLabel
323
271
  };
324
- if (!customStateLabel) return /* @__PURE__ */ jsx4(CoreKeepButton, { ...sharedProps });
325
- const statefulProps = { ...sharedProps, children: (state) => getStateContent(state) };
326
- return /* @__PURE__ */ jsx4(CoreKeepButton, { ...statefulProps });
272
+ if (!customStateLabel) return /* @__PURE__ */ jsx5(CoreKeepButton, { ...sharedProps });
273
+ return /* @__PURE__ */ jsx5(CoreKeepButton, { ...sharedProps, children: (state) => /* @__PURE__ */ jsx5(Fragment2, { children: getStateContent(state) }) });
327
274
  }
275
+
276
+ // src/KeepCollection.tsx
277
+ import { useKeepList as useKeepList4 } from "@keepkit/core/react";
278
+ import { useMemo as useMemo3, useState as useState4 } from "react";
279
+
280
+ // src/KeepList.tsx
281
+ import { useKeepList as useKeepList2 } from "@keepkit/core/react";
282
+ import { isValidElement as isValidElement3 } from "react";
283
+
284
+ // src/KeepItemCard.tsx
285
+ import { useKeepItem as useKeepItem2 } from "@keepkit/core/react";
286
+ import { isValidElement as isValidElement2 } from "react";
287
+ import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs2 } from "react/jsx-runtime";
328
288
  function KeepItemCard({
329
289
  item,
330
290
  title,
@@ -346,8 +306,8 @@ function KeepItemCard({
346
306
  }) {
347
307
  const saveActionLabel = useUiLabel("save");
348
308
  const removeActionLabel = useUiLabel("remove");
349
- const itemState = useKeepItem(item);
350
- const contentChildren = asChild && isValidElement(children) ? void 0 : children;
309
+ const itemState = useKeepItem2(item);
310
+ const contentChildren = asChild && isValidElement2(children) ? void 0 : children;
351
311
  const state = {
352
312
  item,
353
313
  isSaved: itemState.isSaved,
@@ -365,10 +325,10 @@ function KeepItemCard({
365
325
  onRemoveError?.(error);
366
326
  }
367
327
  }
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(
328
+ const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? /* @__PURE__ */ jsxs2(Fragment3, { children: [
329
+ 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,
330
+ /* @__PURE__ */ jsx6("h3", { children: resolvedTitle }),
331
+ showSaveButton ? /* @__PURE__ */ jsx6(
372
332
  KeepButton,
373
333
  {
374
334
  item: toKeepButtonItem(item),
@@ -376,16 +336,19 @@ function KeepItemCard({
376
336
  getAriaLabel: (buttonState) => `${buttonState.isSaved ? removeActionLabel : saveActionLabel} ${String(resolvedTitle)}`
377
337
  }
378
338
  ) : null,
379
- /* @__PURE__ */ jsx4("button", { type: "button", onClick: () => void handleRemove(), disabled: itemState.isMutating, children: removeLabel ?? removeActionLabel })
339
+ /* @__PURE__ */ jsx6("button", { type: "button", onClick: () => void handleRemove(), disabled: itemState.isMutating, children: removeLabel ?? removeActionLabel })
380
340
  ] });
381
341
  return renderRoot(
382
342
  asChild,
383
- isValidElement(children) ? children : void 0,
343
+ isValidElement2(children) ? children : void 0,
384
344
  { ...rootProps, className, "aria-busy": itemState.isMutating || rootProps["aria-busy"] },
385
345
  body,
386
346
  "KeepItemCard"
387
347
  );
388
348
  }
349
+
350
+ // src/KeepList.tsx
351
+ import { jsx as jsx7 } from "react/jsx-runtime";
389
352
  function KeepList({
390
353
  query,
391
354
  children,
@@ -401,7 +364,7 @@ function KeepList({
401
364
  const defaultLoading = useUiLabel("loadingItems");
402
365
  const defaultEmpty = useUiLabel("noItems");
403
366
  const defaultError = useUiLabel("errorItems");
404
- const state = useKeepList(query);
367
+ const state = useKeepList2(query);
405
368
  const body = getListBody(state, {
406
369
  children,
407
370
  renderItem,
@@ -410,10 +373,9 @@ function KeepList({
410
373
  error: errorContent ?? defaultError,
411
374
  itemCardProps
412
375
  });
413
- const root = asChild && isValidElement(children) ? children : void 0;
414
376
  return renderRoot(
415
377
  asChild,
416
- root,
378
+ asChild && isValidElement3(children) ? children : void 0,
417
379
  { ...rootProps, className, "aria-busy": state.isLoading || rootProps["aria-busy"] },
418
380
  body,
419
381
  "KeepList"
@@ -424,12 +386,196 @@ function getListBody(state, options) {
424
386
  if (state.isLoading && !state.isHydrated) return resolveContent(options.loading, state);
425
387
  if (state.isHydrated && state.items.length === 0) return resolveContent(options.empty, state);
426
388
  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)
389
+ if (options.children !== void 0 && !isValidElement3(options.children)) return options.children;
390
+ return /* @__PURE__ */ jsx7("ul", { children: state.items.map(
391
+ (item) => options.renderItem ? options.renderItem(item, state) : /* @__PURE__ */ jsx7("li", { children: /* @__PURE__ */ jsx7(KeepItemCard, { item, ...options.itemCardProps }) }, item.id)
392
+ ) });
393
+ }
394
+
395
+ // src/KeepTagFilter.tsx
396
+ import { useKeepList as useKeepList3 } from "@keepkit/core/react";
397
+ import { isValidElement as isValidElement4, useCallback, useMemo as useMemo2, useState as useState2 } from "react";
398
+ import { jsx as jsx8, jsxs as jsxs3 } from "react/jsx-runtime";
399
+ function KeepTagFilter({
400
+ query,
401
+ value: controlledValue,
402
+ defaultValue,
403
+ onChange,
404
+ onValueChange,
405
+ allLabel,
406
+ ariaLabel,
407
+ renderTag,
408
+ render,
409
+ children,
410
+ asChild = false,
411
+ className,
412
+ ...rootProps
413
+ }) {
414
+ const uiAllLabel = useUiLabel("allTags");
415
+ const uiAriaLabel = useUiLabel("filterTags");
416
+ const [uncontrolledValue, setUncontrolledValue] = useState2(defaultValue);
417
+ const resolvedValue = controlledValue ?? uncontrolledValue;
418
+ const list = useKeepList3({
419
+ ...query,
420
+ tags: resolvedValue ? [...query?.tags ?? [], resolvedValue] : query?.tags
421
+ });
422
+ const select = useCallback(
423
+ (tag) => {
424
+ if (controlledValue === void 0) setUncontrolledValue(tag);
425
+ onChange?.(tag);
426
+ onValueChange?.(tag);
427
+ },
428
+ [controlledValue, onChange, onValueChange]
429
+ );
430
+ const state = useMemo2(
431
+ () => ({ tags: list.tags, tagCounts: list.tagCounts, value: resolvedValue, select }),
432
+ [list.tagCounts, list.tags, resolvedValue, select]
433
+ );
434
+ const contentChildren = asChild && isValidElement4(children) ? void 0 : children;
435
+ const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? /* @__PURE__ */ jsxs3("fieldset", { children: [
436
+ /* @__PURE__ */ jsx8("legend", { children: ariaLabel ?? uiAriaLabel }),
437
+ /* @__PURE__ */ jsx8("button", { type: "button", "aria-pressed": resolvedValue === void 0, onClick: () => select(), children: allLabel ?? uiAllLabel }),
438
+ list.tags.map((tag) => /* @__PURE__ */ jsxs3("button", { type: "button", "aria-pressed": resolvedValue === tag, onClick: () => select(tag), children: [
439
+ renderTag ? renderTag(tag, list.tagCounts[tag] ?? 0, resolvedValue === tag) : tag,
440
+ /* @__PURE__ */ jsxs3("span", { children: [
441
+ " (",
442
+ list.tagCounts[tag] ?? 0,
443
+ ")"
444
+ ] })
445
+ ] }, tag))
446
+ ] });
447
+ return renderRoot(
448
+ asChild,
449
+ isValidElement4(children) ? children : void 0,
450
+ { ...rootProps, className },
451
+ body,
452
+ "KeepTagFilter"
453
+ );
454
+ }
455
+
456
+ // src/query-controls.tsx
457
+ import { useEffect, useState as useState3 } from "react";
458
+ import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs4 } from "react/jsx-runtime";
459
+ function KeepSearchInput({
460
+ value: controlledValue,
461
+ defaultValue = "",
462
+ debounceMs = 300,
463
+ onValueChange,
464
+ "aria-label": ariaLabel,
465
+ placeholder,
466
+ ...props
467
+ }) {
468
+ const label = useUiLabel("search");
469
+ const [uncontrolledValue, setUncontrolledValue] = useState3(defaultValue);
470
+ const value = controlledValue ?? uncontrolledValue;
471
+ useEffect(() => {
472
+ if (!onValueChange) return;
473
+ if (debounceMs <= 0) {
474
+ onValueChange(value);
475
+ return;
476
+ }
477
+ const timer = window.setTimeout(() => onValueChange(value), debounceMs);
478
+ return () => window.clearTimeout(timer);
479
+ }, [debounceMs, onValueChange, value]);
480
+ return /* @__PURE__ */ jsx9(
481
+ "input",
482
+ {
483
+ ...props,
484
+ type: "search",
485
+ value,
486
+ "aria-label": ariaLabel ?? label,
487
+ placeholder: placeholder ?? label,
488
+ onChange: (event) => {
489
+ const nextValue = event.currentTarget.value;
490
+ if (controlledValue === void 0) setUncontrolledValue(nextValue);
491
+ }
492
+ }
493
+ );
494
+ }
495
+ function KeepSortSelect({
496
+ value: controlledValue,
497
+ defaultValue = "updatedAt:desc",
498
+ onValueChange,
499
+ "aria-label": ariaLabel,
500
+ children,
501
+ ...props
502
+ }) {
503
+ const label = useUiLabel("sort");
504
+ const updatedNewestLabel = useUiLabel("updatedNewest");
505
+ const updatedOldestLabel = useUiLabel("updatedOldest");
506
+ const savedNewestLabel = useUiLabel("savedNewest");
507
+ const savedOldestLabel = useUiLabel("savedOldest");
508
+ const [uncontrolledValue, setUncontrolledValue] = useState3(defaultValue);
509
+ const value = controlledValue ?? uncontrolledValue;
510
+ const options = children ?? /* @__PURE__ */ jsxs4(Fragment4, { children: [
511
+ /* @__PURE__ */ jsx9("option", { value: "updatedAt:desc", children: updatedNewestLabel }),
512
+ /* @__PURE__ */ jsx9("option", { value: "updatedAt:asc", children: updatedOldestLabel }),
513
+ /* @__PURE__ */ jsx9("option", { value: "savedAt:desc", children: savedNewestLabel }),
514
+ /* @__PURE__ */ jsx9("option", { value: "savedAt:asc", children: savedOldestLabel })
515
+ ] });
516
+ return /* @__PURE__ */ jsx9(
517
+ "select",
518
+ {
519
+ ...props,
520
+ value,
521
+ "aria-label": ariaLabel ?? label,
522
+ onChange: (event) => {
523
+ const nextValue = event.currentTarget.value;
524
+ if (controlledValue === void 0) setUncontrolledValue(nextValue);
525
+ const [by, direction] = nextValue.split(":");
526
+ onValueChange?.(nextValue, { by, direction });
527
+ },
528
+ children: options
529
+ }
430
530
  );
431
- return /* @__PURE__ */ jsx4("ul", { children: items });
432
531
  }
532
+ function KeepPagination({
533
+ totalCount,
534
+ pageSize,
535
+ page = 1,
536
+ maxPageButtons = 7,
537
+ onPageChange,
538
+ render,
539
+ ...props
540
+ }) {
541
+ const previousPageLabel = useUiLabel("previousPage");
542
+ const nextPageLabel = useUiLabel("nextPage");
543
+ const pageLabel = useUiLabel("page");
544
+ const paginationLabel = useUiLabel("pagination");
545
+ const pageCount = Math.max(1, Math.ceil(totalCount / Math.max(1, pageSize)));
546
+ const currentPage = Math.min(Math.max(1, page), pageCount);
547
+ const goToPage = (nextPage) => {
548
+ const next = Math.min(Math.max(1, nextPage), pageCount);
549
+ onPageChange?.(next, (next - 1) * pageSize);
550
+ };
551
+ const navProps = { ...props, "aria-label": props["aria-label"] ?? paginationLabel };
552
+ if (render) return /* @__PURE__ */ jsx9("nav", { ...navProps, children: render({ page: currentPage, pageCount, goToPage }) });
553
+ const visiblePages = getVisiblePages(currentPage, pageCount, Math.max(1, maxPageButtons));
554
+ return /* @__PURE__ */ jsxs4("nav", { ...navProps, children: [
555
+ /* @__PURE__ */ jsx9("button", { type: "button", onClick: () => goToPage(currentPage - 1), disabled: currentPage <= 1, children: previousPageLabel }),
556
+ visiblePages.map((nextPage) => /* @__PURE__ */ jsx9(
557
+ "button",
558
+ {
559
+ type: "button",
560
+ "aria-current": nextPage === currentPage ? "page" : void 0,
561
+ "aria-label": `${pageLabel} ${nextPage}`,
562
+ onClick: () => goToPage(nextPage),
563
+ children: nextPage
564
+ },
565
+ nextPage
566
+ )),
567
+ /* @__PURE__ */ jsx9("button", { type: "button", onClick: () => goToPage(currentPage + 1), disabled: currentPage >= pageCount, children: nextPageLabel })
568
+ ] });
569
+ }
570
+ function getVisiblePages(currentPage, pageCount, maxPageButtons) {
571
+ if (pageCount <= maxPageButtons) return Array.from({ length: pageCount }, (_, index) => index + 1);
572
+ const half = Math.floor(maxPageButtons / 2);
573
+ const start = Math.min(Math.max(1, currentPage - half), pageCount - maxPageButtons + 1);
574
+ return Array.from({ length: maxPageButtons }, (_, index) => start + index);
575
+ }
576
+
577
+ // src/KeepCollection.tsx
578
+ import { jsx as jsx10, jsxs as jsxs5 } from "react/jsx-runtime";
433
579
  function KeepCollection({
434
580
  query = {},
435
581
  pageSize = 20,
@@ -450,12 +596,12 @@ function KeepCollection({
450
596
  bulkActions: false,
451
597
  ...features
452
598
  };
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);
599
+ const [searchValue, setSearchValue] = useState4(query.search?.query ?? "");
600
+ const [sort, setSort] = useState4(query.sort ?? { by: "updatedAt", direction: "desc" });
601
+ const [tag, setTag] = useState4(query.tags?.[0]);
602
+ const [page, setPage] = useState4(query.pagination?.page ?? 1);
457
603
  const resolvedPageSize = query.pagination?.pageSize ?? pageSize;
458
- const resolvedQuery = useMemo2(
604
+ const resolvedQuery = useMemo3(
459
605
  () => ({
460
606
  ...query,
461
607
  search: enabled.search ? { ...query.search, query: searchValue } : query.search,
@@ -465,25 +611,48 @@ function KeepCollection({
465
611
  }),
466
612
  [enabled.pagination, enabled.search, enabled.sort, page, query, resolvedPageSize, searchValue, sort, tag]
467
613
  );
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(
614
+ const list = useKeepList4(resolvedQuery);
615
+ return /* @__PURE__ */ jsxs5(
475
616
  "section",
476
617
  {
477
618
  ...rootProps,
478
619
  className,
479
620
  "aria-busy": list.isLoading || list.isMutating || rootProps["aria-busy"],
480
621
  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
622
+ /* @__PURE__ */ jsxs5("div", { children: [
623
+ enabled.search ? /* @__PURE__ */ jsx10(
624
+ KeepSearchInput,
625
+ {
626
+ value: searchValue,
627
+ onValueChange: (value) => {
628
+ setSearchValue(value);
629
+ setPage(1);
630
+ }
631
+ }
632
+ ) : null,
633
+ enabled.sort ? /* @__PURE__ */ jsx10(
634
+ KeepSortSelect,
635
+ {
636
+ value: sortToValue(sort),
637
+ onValueChange: (_value, nextSort) => {
638
+ setSort(nextSort);
639
+ setPage(1);
640
+ }
641
+ }
642
+ ) : null,
643
+ enabled.tagFilter ? /* @__PURE__ */ jsx10(
644
+ KeepTagFilter,
645
+ {
646
+ query,
647
+ value: tag,
648
+ onValueChange: (value) => {
649
+ setTag(value);
650
+ setPage(1);
651
+ }
652
+ }
653
+ ) : null
485
654
  ] }),
486
- /* @__PURE__ */ jsx4(
655
+ /* @__PURE__ */ jsx10(
487
656
  KeepList,
488
657
  {
489
658
  query: resolvedQuery,
@@ -494,80 +663,37 @@ function KeepCollection({
494
663
  error
495
664
  }
496
665
  ),
497
- enabled.pagination ? /* @__PURE__ */ jsx4(
666
+ enabled.pagination ? /* @__PURE__ */ jsx10(
498
667
  KeepPagination,
499
668
  {
500
669
  totalCount: list.totalCount,
501
670
  pageSize: resolvedPageSize,
502
671
  page: list.page,
503
- onPageChange: (_nextPage, nextPageOffset) => setPage(Math.floor(nextPageOffset / resolvedPageSize) + 1)
672
+ onPageChange: (nextPage) => setPage(nextPage)
504
673
  }
505
674
  ) : null,
506
- enabled.bulkActions ? /* @__PURE__ */ jsx4(KeepBulkActions, { query: resolvedQuery }) : null
675
+ enabled.bulkActions ? /* @__PURE__ */ jsx10(KeepBulkActions, { query: resolvedQuery }) : null
507
676
  ]
508
677
  }
509
678
  );
510
679
  }
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
- }
680
+
681
+ // src/KeepNoteEditor.tsx
682
+ import { useKeepItem as useKeepItem3 } from "@keepkit/core/react";
683
+ import {
684
+ isValidElement as isValidElement5,
685
+ useCallback as useCallback2,
686
+ useEffect as useEffect2,
687
+ useRef,
688
+ useState as useState5
689
+ } from "react";
690
+ import { Fragment as Fragment5, jsx as jsx11, jsxs as jsxs6 } from "react/jsx-runtime";
566
691
  function KeepNoteEditor({
567
692
  item,
568
693
  label,
569
694
  saveLabel,
570
695
  placeholder,
696
+ debounceMs = 300,
571
697
  onSaved,
572
698
  onSaveError,
573
699
  render,
@@ -578,68 +704,89 @@ function KeepNoteEditor({
578
704
  }) {
579
705
  const defaultLabel = useUiLabel("note");
580
706
  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 () => {
707
+ const itemState = useKeepItem3(item);
708
+ const { error, isMutating, item: savedItem, updateNote } = itemState;
709
+ const contentChildren = asChild && isValidElement5(children) ? void 0 : children;
710
+ const [note, setNote] = useState5(item.note ?? "");
711
+ const baselineNote = savedItem?.note ?? item.note ?? "";
712
+ const isDirty = note !== baselineNote;
713
+ const lastSavedNoteRef = useRef(void 0);
714
+ useEffect2(() => setNote(baselineNote), [baselineNote]);
715
+ const save = useCallback2(async () => {
586
716
  const nextNote = note.trim() || void 0;
587
717
  try {
588
- await itemState.updateNote(nextNote);
718
+ await updateNote(nextNote);
719
+ lastSavedNoteRef.current = note;
589
720
  onSaved?.(nextNote);
590
- } catch (error) {
591
- onSaveError?.(error);
592
- throw error;
721
+ } catch (error2) {
722
+ onSaveError?.(error2);
723
+ throw error2;
593
724
  }
594
- }, [itemState, note, onSaveError, onSaved]);
725
+ }, [note, onSaveError, onSaved, updateNote]);
726
+ useEffect2(() => {
727
+ if (!isDirty || debounceMs <= 0 || lastSavedNoteRef.current === note) return;
728
+ const timer = window.setTimeout(() => void save().catch(() => void 0), debounceMs);
729
+ return () => window.clearTimeout(timer);
730
+ }, [debounceMs, isDirty, note, save]);
595
731
  const state = {
596
732
  item,
597
733
  note,
598
734
  setNote,
599
- isDirty: note !== (itemState.item?.note ?? item.note ?? ""),
600
- isSaving: itemState.isMutating,
601
- error: itemState.error,
735
+ isDirty,
736
+ isSaving: isMutating,
737
+ error,
602
738
  save
603
739
  };
604
- const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? /* @__PURE__ */ jsxs2(Fragment2, { children: [
605
- /* @__PURE__ */ jsxs2("label", { children: [
740
+ const body = render ? render(state) : typeof contentChildren === "function" ? contentChildren(state) : contentChildren ?? /* @__PURE__ */ jsxs6(Fragment5, { children: [
741
+ /* @__PURE__ */ jsxs6("label", { children: [
606
742
  label ?? defaultLabel,
607
- /* @__PURE__ */ jsx4(
743
+ /* @__PURE__ */ jsx11(
608
744
  "textarea",
609
745
  {
610
746
  value: note,
611
747
  onChange: (event) => setNote(event.currentTarget.value),
612
748
  placeholder,
613
- disabled: itemState.isMutating
749
+ disabled: isMutating,
750
+ onKeyDown: (event) => {
751
+ if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
752
+ event.preventDefault();
753
+ void save().catch(() => void 0);
754
+ }
755
+ }
614
756
  }
615
757
  )
616
758
  ] }),
617
- /* @__PURE__ */ jsx4("button", { type: "submit", disabled: itemState.isMutating, "aria-busy": itemState.isMutating, children: saveLabel ?? defaultSaveLabel })
759
+ /* @__PURE__ */ jsx11("button", { type: "submit", disabled: isMutating, "aria-busy": isMutating, children: saveLabel ?? defaultSaveLabel })
618
760
  ] });
619
761
  const handleSubmit = (event) => {
620
762
  event.preventDefault();
621
763
  void save().catch(() => void 0);
622
764
  };
623
765
  if (!asChild) {
624
- return /* @__PURE__ */ jsx4(
766
+ return /* @__PURE__ */ jsx11(
625
767
  "form",
626
768
  {
627
769
  ...formProps,
628
770
  className,
629
771
  onSubmit: handleSubmit,
630
- "aria-busy": itemState.isMutating || formProps["aria-busy"],
772
+ "aria-busy": isMutating || formProps["aria-busy"],
631
773
  children: body
632
774
  }
633
775
  );
634
776
  }
635
777
  return renderRoot(
636
778
  true,
637
- isValidElement(children) ? children : void 0,
638
- { ...formProps, className, onSubmit: handleSubmit, "aria-busy": itemState.isMutating || formProps["aria-busy"] },
779
+ isValidElement5(children) ? children : void 0,
780
+ { ...formProps, className, onSubmit: handleSubmit, "aria-busy": isMutating || formProps["aria-busy"] },
639
781
  body,
640
782
  "KeepNoteEditor"
641
783
  );
642
784
  }
785
+
786
+ // src/KeepTagEditor.tsx
787
+ import { useKeepItem as useKeepItem4 } from "@keepkit/core/react";
788
+ import { useCallback as useCallback3, useEffect as useEffect3, useState as useState6 } from "react";
789
+ import { Fragment as Fragment6, jsx as jsx12, jsxs as jsxs7 } from "react/jsx-runtime";
643
790
  function KeepTagEditor({
644
791
  item,
645
792
  availableTags = [],
@@ -651,11 +798,11 @@ function KeepTagEditor({
651
798
  const tagsLabel = useUiLabel("tagsToApply");
652
799
  const removeLabel = useUiLabel("remove");
653
800
  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 () => {
801
+ const itemState = useKeepItem4(item);
802
+ const [tags, setTags] = useState6(item.tags ?? []);
803
+ const [input, setInput] = useState6("");
804
+ useEffect3(() => setTags(itemState.item?.tags ?? item.tags ?? []), [item.tags, itemState.item?.tags]);
805
+ const save = useCallback3(async () => {
659
806
  const nextTags = normalizeUiTags(tags);
660
807
  try {
661
808
  await itemState.updateTags(nextTags);
@@ -667,14 +814,13 @@ function KeepTagEditor({
667
814
  }
668
815
  }, [itemState, onSaveError, onSaved, tags]);
669
816
  const addTag = (tag) => {
670
- const next = normalizeUiTags([...tags, tag]);
671
- setTags(next);
817
+ setTags(normalizeUiTags([...tags, tag]));
672
818
  setInput("");
673
819
  };
674
- const body = render ? render({ tags, setTags, save, isSaving: itemState.isMutating }) : /* @__PURE__ */ jsxs2(Fragment2, { children: [
675
- /* @__PURE__ */ jsxs2("label", { children: [
820
+ const body = render ? render({ tags, setTags, save, isSaving: itemState.isMutating }) : /* @__PURE__ */ jsxs7(Fragment6, { children: [
821
+ /* @__PURE__ */ jsxs7("label", { children: [
676
822
  tagsLabel,
677
- /* @__PURE__ */ jsx4(
823
+ /* @__PURE__ */ jsx12(
678
824
  "input",
679
825
  {
680
826
  value: input,
@@ -682,6 +828,7 @@ function KeepTagEditor({
682
828
  onChange: (event) => setInput(event.currentTarget.value),
683
829
  onKeyDown: (event) => {
684
830
  if (event.key === "Enter") {
831
+ if (event.nativeEvent.isComposing) return;
685
832
  event.preventDefault();
686
833
  if (input.trim()) addTag(input);
687
834
  } else if (event.key === "Backspace" && input.length === 0 && tags.length > 0) {
@@ -692,14 +839,14 @@ function KeepTagEditor({
692
839
  }
693
840
  )
694
841
  ] }),
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: [
842
+ availableTags.length > 0 ? /* @__PURE__ */ jsx12("datalist", { id: `keep-tags-${item.id}`, children: availableTags.map((tag) => /* @__PURE__ */ jsx12("option", { value: tag }, tag)) }) : null,
843
+ /* @__PURE__ */ jsx12("ul", { "aria-label": tagsLabel, children: tags.map((tag) => /* @__PURE__ */ jsxs7("li", { children: [
697
844
  tag,
698
- /* @__PURE__ */ jsx4("button", { type: "button", onClick: () => setTags(tags.filter((current) => current !== tag)), children: removeLabel })
845
+ /* @__PURE__ */ jsx12("button", { type: "button", onClick: () => setTags(tags.filter((current) => current !== tag)), children: removeLabel })
699
846
  ] }, tag)) }),
700
- /* @__PURE__ */ jsx4("button", { type: "submit", disabled: itemState.isMutating, "aria-busy": itemState.isMutating, children: applyTagsLabel })
847
+ /* @__PURE__ */ jsx12("button", { type: "submit", disabled: itemState.isMutating, "aria-busy": itemState.isMutating, children: applyTagsLabel })
701
848
  ] });
702
- return /* @__PURE__ */ jsx4(
849
+ return /* @__PURE__ */ jsx12(
703
850
  "form",
704
851
  {
705
852
  ...props,
@@ -712,96 +859,11 @@ function KeepTagEditor({
712
859
  }
713
860
  );
714
861
  }
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
- }
862
+
863
+ // src/status.tsx
864
+ import { useKeepContext } from "@keepkit/core/react";
865
+ import { isValidElement as isValidElement6, useEffect as useEffect4, useRef as useRef2, useState as useState7 } from "react";
866
+ import { Fragment as Fragment7, jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
805
867
  function KeepEmptyState({
806
868
  title,
807
869
  description,
@@ -812,10 +874,10 @@ function KeepEmptyState({
812
874
  ...rootProps
813
875
  }) {
814
876
  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,
877
+ const contentChildren = asChild && isValidElement6(children) ? void 0 : children;
878
+ const body = contentChildren ?? /* @__PURE__ */ jsxs8(Fragment7, { children: [
879
+ /* @__PURE__ */ jsx13("h2", { children: title ?? defaultTitle }),
880
+ description ? /* @__PURE__ */ jsx13("p", { children: description }) : null,
819
881
  action
820
882
  ] });
821
883
  return renderRoot(asChild, children, { ...rootProps, className }, body, "KeepEmptyState");
@@ -830,7 +892,7 @@ function KeepStatus({
830
892
  ...rootProps
831
893
  }) {
832
894
  const context = useKeepContext();
833
- const contentChildren = asChild && isValidElement(children) ? void 0 : children;
895
+ const contentChildren = asChild && isValidElement6(children) ? void 0 : children;
834
896
  const resolvedStatus = status ?? getDerivedStatus(context);
835
897
  const defaultLabel = useUiLabel(getStatusLabelKey(resolvedStatus));
836
898
  const state = {
@@ -843,7 +905,7 @@ function KeepStatus({
843
905
  const role = rootProps.role ?? (resolvedStatus === "error" ? "alert" : "status");
844
906
  return renderRoot(
845
907
  asChild,
846
- isValidElement(children) ? children : void 0,
908
+ isValidElement6(children) ? children : void 0,
847
909
  { ...rootProps, className, role, "aria-live": rootProps["aria-live"] ?? "polite" },
848
910
  body,
849
911
  "KeepStatus"
@@ -854,9 +916,9 @@ function KeepAnnouncements({ messages, ...props }) {
854
916
  const savedMessage = useUiLabel("savedMessage", messages?.save);
855
917
  const removedMessage = useUiLabel("removedMessage", messages?.remove);
856
918
  const noteSavedMessage = useUiLabel("noteSavedMessage", messages?.note);
857
- const [message, setMessage] = useState2("");
858
- const lastChangeRef = useRef(void 0);
859
- useEffect2(() => {
919
+ const [message, setMessage] = useState7("");
920
+ const lastChangeRef = useRef2(void 0);
921
+ useEffect4(() => {
860
922
  const change = context.lastChange;
861
923
  if (!change || change === lastChangeRef.current) return;
862
924
  lastChangeRef.current = change;
@@ -864,8 +926,9 @@ function KeepAnnouncements({ messages, ...props }) {
864
926
  else if (change.action === "remove" || change.action === "removeBatch") setMessage(removedMessage);
865
927
  else if (change.action === "updateNote") setMessage(noteSavedMessage);
866
928
  }, [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 });
929
+ return /* @__PURE__ */ jsx13("div", { ...props, role: props.role ?? "status", "aria-live": props["aria-live"] ?? "polite", "aria-atomic": "true", children: message });
868
930
  }
931
+ var KeepAnnouncer = KeepAnnouncements;
869
932
  function getDerivedStatus(context) {
870
933
  if (context.error) return "error";
871
934
  if (context.syncState.status === "pending" || context.syncState.status === "syncing") return "syncing";
@@ -882,41 +945,70 @@ function getStatusLabelKey(status) {
882
945
  if (status === "syncing") return "syncing";
883
946
  return "saved";
884
947
  }
885
- function resolveContent(content, state) {
886
- return typeof content === "function" ? content(state) : content;
948
+
949
+ // src/index.tsx
950
+ import { KeepProvider, useKeepContext as useKeepContext2, useKeepItem as useKeepItem5, useKeepList as useKeepList5, useKeepShortcut } from "@keepkit/core/react";
951
+ import {
952
+ createBrowserStorageAdapter,
953
+ createStorageAdapter,
954
+ FallbackStorageAdapter,
955
+ IndexedDBAdapter,
956
+ IndexedDBSyncQueueAdapter,
957
+ LocalStorageAdapter,
958
+ LocalStorageSyncQueueAdapter,
959
+ SyncStorageAdapter
960
+ } from "@keepkit/core/storage";
961
+ import { jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
962
+ function KeepKitProvider({
963
+ labels,
964
+ locale,
965
+ labelResolver,
966
+ children,
967
+ ...providerProps
968
+ }) {
969
+ return /* @__PURE__ */ jsx14(KeepUiProvider, { labels, locale, labelResolver, children: /* @__PURE__ */ jsxs9(CoreKeepProvider, { ...providerProps, children: [
970
+ children,
971
+ /* @__PURE__ */ jsx14(KeepAnnouncements, {})
972
+ ] }) });
887
973
  }
888
- function toKeepButtonItem(item) {
974
+ function createKeepKit(options = {}) {
975
+ const { labels, locale, labelResolver, getTitle, getImageProps, ...coreOptions } = options;
976
+ const coreKit = createCoreKeepKit(coreOptions);
889
977
  return {
890
- id: item.id,
891
- meta: item.meta,
892
- targetType: item.targetType,
893
- note: item.note,
894
- tags: item.tags
978
+ Provider: (props) => /* @__PURE__ */ jsx14(
979
+ KeepKitProvider,
980
+ {
981
+ ...coreOptions,
982
+ labels,
983
+ locale,
984
+ labelResolver,
985
+ ...props
986
+ }
987
+ ),
988
+ Button: (props) => /* @__PURE__ */ jsx14(KeepButton, { ...props }),
989
+ Collection: (props) => /* @__PURE__ */ jsx14(
990
+ KeepCollection,
991
+ {
992
+ ...props,
993
+ itemCardProps: {
994
+ ...getTitle ? { getTitle } : {},
995
+ ...getImageProps ? { getImageProps } : {},
996
+ ...props.itemCardProps
997
+ }
998
+ }
999
+ ),
1000
+ useContext: () => coreKit.useContext(),
1001
+ useItem: (item) => coreKit.useItem(item),
1002
+ useList: (query) => coreKit.useList(query),
1003
+ useShortcut: (shortcutOptions) => coreKit.useShortcut(shortcutOptions)
895
1004
  };
896
1005
  }
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
1006
  export {
916
1007
  FallbackStorageAdapter,
917
1008
  IndexedDBAdapter,
918
1009
  IndexedDBSyncQueueAdapter,
919
1010
  KeepAnnouncements,
1011
+ KeepAnnouncer,
920
1012
  KeepBulkActions,
921
1013
  KeepButton,
922
1014
  KeepCollection,
@@ -940,9 +1032,11 @@ export {
940
1032
  createBrowserStorageAdapter,
941
1033
  createKeepKit,
942
1034
  createStorageAdapter,
1035
+ isAllSelected,
1036
+ toggleSelectAll,
943
1037
  useKeepContext2 as useKeepContext,
944
- useKeepItem2 as useKeepItem,
945
- useKeepList2 as useKeepList,
1038
+ useKeepItem5 as useKeepItem,
1039
+ useKeepList5 as useKeepList,
946
1040
  useKeepShortcut,
947
1041
  useKeepUiLabels
948
1042
  };