@keepkit/ui 0.16.0 → 0.17.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
@@ -1540,9 +1540,49 @@ import { useState as useState2 } from "react";
1540
1540
  // src/shared.tsx
1541
1541
  import {
1542
1542
  cloneElement,
1543
- isValidElement
1543
+ createContext as createContext2,
1544
+ isValidElement,
1545
+ useContext as useContext2
1544
1546
  } from "react";
1545
- import { jsx as jsx3 } from "react/jsx-runtime";
1547
+ import { Fragment, jsx as jsx3 } from "react/jsx-runtime";
1548
+ var KeepSearchQueryContext = createContext2(void 0);
1549
+ function KeepSearchQueryProvider({ query, children }) {
1550
+ return /* @__PURE__ */ jsx3(KeepSearchQueryContext.Provider, { value: query, children });
1551
+ }
1552
+ function useKeepSearchQuery() {
1553
+ return useContext2(KeepSearchQueryContext);
1554
+ }
1555
+ function KeepHighlight({ children, query }) {
1556
+ const contextQuery = useKeepSearchQuery();
1557
+ const resolvedQuery = query ?? contextQuery;
1558
+ if (typeof children !== "string" || !resolvedQuery?.trim()) return children ?? null;
1559
+ return highlightText(children, resolvedQuery);
1560
+ }
1561
+ function highlightText(text, query) {
1562
+ const normalizedQuery = query.trim();
1563
+ if (!normalizedQuery) return text;
1564
+ const matcher = new RegExp(escapeRegExp(normalizedQuery), "gi");
1565
+ const parts = [];
1566
+ let lastIndex = 0;
1567
+ let matchIndex = 0;
1568
+ while (true) {
1569
+ const match = matcher.exec(text);
1570
+ if (match === null) break;
1571
+ const index = match.index;
1572
+ if (index > lastIndex) parts.push(text.slice(lastIndex, index));
1573
+ parts.push(
1574
+ /* @__PURE__ */ jsx3("mark", { className: "keep-highlight", "data-highlight": "true", children: match[0] }, `highlight-${matchIndex}`)
1575
+ );
1576
+ lastIndex = index + match[0].length;
1577
+ matchIndex += 1;
1578
+ }
1579
+ if (lastIndex === 0) return text;
1580
+ if (lastIndex < text.length) parts.push(text.slice(lastIndex));
1581
+ return /* @__PURE__ */ jsx3(Fragment, { children: parts });
1582
+ }
1583
+ function escapeRegExp(value) {
1584
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1585
+ }
1546
1586
  function toKeepButtonItem(item) {
1547
1587
  return {
1548
1588
  id: item.id,
@@ -1701,7 +1741,7 @@ function getItemLabel(item) {
1701
1741
  }
1702
1742
 
1703
1743
  // src/KeepBulkActions.tsx
1704
- import { Fragment, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
1744
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
1705
1745
  function KeepBulkActions({
1706
1746
  query,
1707
1747
  selectedIds: controlledSelectedIds,
@@ -1724,7 +1764,7 @@ function KeepBulkActions({
1724
1764
  controlledScope,
1725
1765
  onSelectionScopeChange
1726
1766
  });
1727
- const body = render ? render(state) : typeof children === "function" ? children(state) : children ?? /* @__PURE__ */ jsxs2(Fragment, { children: [
1767
+ const body = render ? render(state) : typeof children === "function" ? children(state) : children ?? /* @__PURE__ */ jsxs2(Fragment2, { children: [
1728
1768
  /* @__PURE__ */ jsxs2("fieldset", { children: [
1729
1769
  /* @__PURE__ */ jsx5("legend", { children: labels.selectItems }),
1730
1770
  /* @__PURE__ */ jsxs2("label", { children: [
@@ -1842,7 +1882,7 @@ function useKeepButton({ item, labels, icons, children }) {
1842
1882
  }
1843
1883
 
1844
1884
  // src/KeepButton.tsx
1845
- import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
1885
+ import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
1846
1886
  function KeepButton({
1847
1887
  labels,
1848
1888
  icons,
@@ -1887,7 +1927,7 @@ function KeepButton({
1887
1927
  const label = state.error ? labels?.error ?? view.labels.error : state.isMutating ? labels?.loading ?? view.labels.loading : state.isSaved ? labels?.saved ?? view.labels.saved : labels?.unsaved ?? view.labels.save;
1888
1928
  if (!icons) return label;
1889
1929
  const icon = state.error ? icons.error : state.isMutating ? icons.loading : state.isSaved ? icons.remove ?? icons.saved : icons.save;
1890
- return /* @__PURE__ */ jsxs3(Fragment2, { children: [
1930
+ return /* @__PURE__ */ jsxs3(Fragment3, { children: [
1891
1931
  renderIcon(icon, iconClassName),
1892
1932
  showLabel && !iconOnly ? label : null
1893
1933
  ] });
@@ -1915,7 +1955,7 @@ function KeepButton({
1915
1955
  unsavedAriaLabel: labels?.unsavedAriaLabel ?? props.unsavedAriaLabel
1916
1956
  };
1917
1957
  if (!view.customStateLabel) return /* @__PURE__ */ jsx6(CoreKeepButton, { ...sharedProps });
1918
- return /* @__PURE__ */ jsx6(CoreKeepButton, { ...sharedProps, children: (state) => /* @__PURE__ */ jsx6(Fragment2, { children: getStateContent(state) }) });
1958
+ return /* @__PURE__ */ jsx6(CoreKeepButton, { ...sharedProps, children: (state) => /* @__PURE__ */ jsx6(Fragment3, { children: getStateContent(state) }) });
1919
1959
  }
1920
1960
  function renderIcon(icon, className) {
1921
1961
  if (typeof icon === "function") return createElement(icon, { "aria-hidden": true, className });
@@ -2097,12 +2137,79 @@ function useKeepListView(query) {
2097
2137
  };
2098
2138
  }
2099
2139
 
2140
+ // src/hooks/useRovingTabIndex.ts
2141
+ import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef4 } from "react";
2142
+ function useRovingTabIndex() {
2143
+ const ref = useRef4(null);
2144
+ const getItems = useCallback3(() => {
2145
+ const root = ref.current;
2146
+ if (!root) return [];
2147
+ return Array.from(root.querySelectorAll('[data-keepkit="card"]')).filter(
2148
+ (item) => item.getAttribute("aria-hidden") !== "true" && item.getAttribute("data-roving-disabled") !== "true"
2149
+ );
2150
+ }, []);
2151
+ const syncTabIndices = useCallback3(() => {
2152
+ const items = getItems();
2153
+ if (items.length === 0) return;
2154
+ const activeElement = document.activeElement;
2155
+ const activeItem = items.find((item) => item === activeElement || item.contains(activeElement));
2156
+ const activeIndex = activeItem ? items.indexOf(activeItem) : 0;
2157
+ items.forEach((item, index) => {
2158
+ item.tabIndex = index === activeIndex ? 0 : -1;
2159
+ });
2160
+ }, [getItems]);
2161
+ useEffect3(() => {
2162
+ const root = ref.current;
2163
+ if (!root) return;
2164
+ syncTabIndices();
2165
+ const observer = new MutationObserver(syncTabIndices);
2166
+ observer.observe(root, { childList: true, subtree: true });
2167
+ return () => observer.disconnect();
2168
+ }, [syncTabIndices]);
2169
+ const onFocusCapture = useCallback3(
2170
+ (event) => {
2171
+ if (!(event.target instanceof HTMLElement)) return;
2172
+ const item = event.target.closest('[data-keepkit="card"]');
2173
+ if (!item || !ref.current?.contains(item)) return;
2174
+ getItems().forEach((candidate) => {
2175
+ candidate.tabIndex = candidate === item ? 0 : -1;
2176
+ });
2177
+ },
2178
+ [getItems]
2179
+ );
2180
+ const onKeyDown = useCallback3(
2181
+ (event) => {
2182
+ if (event.defaultPrevented) return;
2183
+ const items = getItems();
2184
+ if (!(event.target instanceof HTMLElement)) return;
2185
+ const current = event.target.closest('[data-keepkit="card"]');
2186
+ if (!current || current !== event.target || !ref.current?.contains(current)) return;
2187
+ const currentIndex = items.indexOf(current);
2188
+ if (currentIndex < 0) return;
2189
+ let nextIndex;
2190
+ if (event.key === "Home") nextIndex = 0;
2191
+ if (event.key === "End") nextIndex = items.length - 1;
2192
+ if (event.key === "ArrowRight" || event.key === "ArrowDown")
2193
+ nextIndex = Math.min(currentIndex + 1, items.length - 1);
2194
+ if (event.key === "ArrowLeft" || event.key === "ArrowUp") nextIndex = Math.max(currentIndex - 1, 0);
2195
+ if (nextIndex === void 0) return;
2196
+ event.preventDefault();
2197
+ if (nextIndex === currentIndex) return;
2198
+ items[nextIndex]?.focus();
2199
+ },
2200
+ [getItems]
2201
+ );
2202
+ return { ref, onKeyDown, onFocusCapture };
2203
+ }
2204
+
2100
2205
  // src/KeepItemCard.tsx
2101
2206
  import {
2102
- createContext as createContext2,
2207
+ createContext as createContext3,
2103
2208
  createElement as createElement2,
2104
2209
  isValidElement as isValidElement2,
2105
- useContext as useContext2
2210
+ useContext as useContext3,
2211
+ useEffect as useEffect4,
2212
+ useState as useState5
2106
2213
  } from "react";
2107
2214
 
2108
2215
  // src/hooks/useKeepItemCard.ts
@@ -2403,10 +2510,10 @@ function KeepPruneStaleButton({
2403
2510
  }
2404
2511
 
2405
2512
  // src/KeepItemCard.tsx
2406
- import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
2407
- var KeepItemCardContext = createContext2(null);
2513
+ import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
2514
+ var KeepItemCardContext = createContext3(null);
2408
2515
  function useKeepItemCardCompound(part) {
2409
- const context = useContext2(KeepItemCardContext);
2516
+ const context = useContext3(KeepItemCardContext);
2410
2517
  if (!context) throw new Error(`KeepItemCard.${part} must be rendered inside KeepItemCard.`);
2411
2518
  return context;
2412
2519
  }
@@ -2436,6 +2543,7 @@ function KeepItemCardRoot({
2436
2543
  linkComponent: LinkComponent,
2437
2544
  linkTargetAttribute,
2438
2545
  linkRel,
2546
+ highlightQuery,
2439
2547
  className,
2440
2548
  ...rootProps
2441
2549
  }) {
@@ -2450,6 +2558,8 @@ function KeepItemCardRoot({
2450
2558
  onRemoveError,
2451
2559
  onRemoved
2452
2560
  });
2561
+ const contextQuery = useKeepSearchQuery();
2562
+ const searchQuery = highlightQuery ?? contextQuery;
2453
2563
  const contentChildren = asChild && isValidElement2(children) ? void 0 : children;
2454
2564
  function renderLink(content) {
2455
2565
  if (!view.href || !view.isAvailable) return content;
@@ -2468,7 +2578,28 @@ function KeepItemCardRoot({
2468
2578
  return view.href ? /* @__PURE__ */ jsx9("span", { "aria-disabled": "true", "data-link-disabled": "true", children: content }) : content;
2469
2579
  }
2470
2580
  const resolvedImageProps = view.imageProps ? { ...view.imageProps, alt: imageAlt ?? view.imageProps.alt } : void 0;
2471
- const image = resolvedImageProps ? renderImage?.(resolvedImageProps, item) ?? (ImageComponent ? /* @__PURE__ */ jsx9(ImageComponent, { ...resolvedImageProps }) : /* @__PURE__ */ jsx9("img", { ...resolvedImageProps, alt: resolvedImageProps.alt })) : null;
2581
+ const imageSource = resolvedImageProps?.src;
2582
+ const [imageStatus, setImageStatus] = useState5(imageSource ? "loading" : "error");
2583
+ useEffect4(() => {
2584
+ setImageStatus(imageSource ? "loading" : "error");
2585
+ }, [imageSource]);
2586
+ let image = null;
2587
+ if (resolvedImageProps) {
2588
+ const imagePropsWithHandlers = {
2589
+ ...resolvedImageProps,
2590
+ src: resolvedImageProps.src,
2591
+ alt: imageAlt ?? resolvedImageProps.alt,
2592
+ onLoad: (event) => {
2593
+ resolvedImageProps.onLoad?.(event);
2594
+ setImageStatus("loaded");
2595
+ },
2596
+ onError: (event) => {
2597
+ resolvedImageProps.onError?.(event);
2598
+ setImageStatus("error");
2599
+ }
2600
+ };
2601
+ image = renderImage?.(imagePropsWithHandlers, item) ?? (ImageComponent ? /* @__PURE__ */ jsx9(ImageComponent, { ...imagePropsWithHandlers }) : /* @__PURE__ */ jsx9("img", { ...imagePropsWithHandlers, alt: imagePropsWithHandlers.alt }));
2602
+ }
2472
2603
  const tags = showTags ? item.tags ?? [] : [];
2473
2604
  const renderedTags = showTags && tags.length > 0 ? renderTags?.(tags, item) ?? null : null;
2474
2605
  const meta = showSavedAt ? /* @__PURE__ */ jsxs5("div", { "data-card-meta": true, children: [
@@ -2480,7 +2611,7 @@ function KeepItemCardRoot({
2480
2611
  /* @__PURE__ */ jsx9("time", { dateTime: new Date(item.savedAt).toISOString(), children: formatSavedAt(item.savedAt) })
2481
2612
  ] }) : null;
2482
2613
  const error = view.itemState.error ? /* @__PURE__ */ jsx9("p", { role: "alert", children: getErrorMessage2(view.itemState.error, view.labels.error) }) : null;
2483
- const actions = view.statusLabel ? /* @__PURE__ */ jsx9(KeepStaleNotice, { item, onRetry, onRemoved }) : /* @__PURE__ */ jsxs5(Fragment3, { children: [
2614
+ const actions = view.statusLabel ? /* @__PURE__ */ jsx9(KeepStaleNotice, { item, onRetry, onRemoved }) : /* @__PURE__ */ jsxs5(Fragment4, { children: [
2484
2615
  showSaveButton ? /* @__PURE__ */ jsx9(
2485
2616
  KeepButton,
2486
2617
  {
@@ -2503,16 +2634,18 @@ function KeepItemCardRoot({
2503
2634
  const compoundValue = {
2504
2635
  resolvedTitle: view.resolvedTitle,
2505
2636
  renderTitle,
2506
- image,
2637
+ image: imageStatus === "error" ? null : image,
2638
+ imageStatus,
2507
2639
  fallbackLabel: String(view.resolvedTitle),
2508
2640
  tags,
2509
2641
  tagsLabel: view.labels.tags,
2510
2642
  renderedTags,
2511
2643
  meta,
2512
2644
  error,
2513
- actions
2645
+ actions,
2646
+ renderText: (content) => /* @__PURE__ */ jsx9(KeepHighlight, { query: searchQuery, children: content })
2514
2647
  };
2515
- const defaultBody = /* @__PURE__ */ jsxs5(Fragment3, { children: [
2648
+ const defaultBody = /* @__PURE__ */ jsxs5(Fragment4, { children: [
2516
2649
  /* @__PURE__ */ jsx9(KeepItemCardMedia, {}),
2517
2650
  /* @__PURE__ */ jsx9(KeepItemCardContent, {}),
2518
2651
  /* @__PURE__ */ jsx9(KeepItemCardActions, {})
@@ -2540,25 +2673,39 @@ function KeepItemCardRoot({
2540
2673
  }
2541
2674
  function KeepItemCardMedia({ children, fallback, ...props }) {
2542
2675
  const context = useKeepItemCardCompound("Media");
2543
- return /* @__PURE__ */ jsx9("div", { ...props, "data-keep-card-part": "media", children: children ?? context.image ?? /* @__PURE__ */ jsx9("span", { role: "img", "aria-label": context.fallbackLabel, "data-keep-card-fallback": "true", children: fallback ?? context.fallbackLabel.slice(0, 1) }) });
2676
+ return /* @__PURE__ */ jsx9("div", { ...props, "data-keep-card-part": "media", "data-media-status": context.imageStatus, children: children ?? context.image ?? /* @__PURE__ */ jsx9("span", { role: "img", "aria-label": context.fallbackLabel, "data-keep-card-fallback": "true", children: fallback ?? /* @__PURE__ */ jsx9(KeepMediaPlaceholderIcon, {}) }) });
2544
2677
  }
2545
2678
  function KeepItemCardContent({ children, ...props }) {
2546
2679
  const context = useKeepItemCardCompound("Content");
2547
- return /* @__PURE__ */ jsx9("div", { ...props, "data-keep-card-part": "content", children: children ?? /* @__PURE__ */ jsxs5(Fragment3, { children: [
2680
+ return /* @__PURE__ */ jsx9("div", { ...props, "data-keep-card-part": "content", children: children === void 0 ? /* @__PURE__ */ jsxs5(Fragment4, { children: [
2548
2681
  /* @__PURE__ */ jsx9(KeepItemCardTitle, {}),
2549
2682
  context.meta,
2550
2683
  /* @__PURE__ */ jsx9(KeepItemCardTags, {}),
2551
2684
  context.error
2552
- ] }) });
2685
+ ] }) : context.renderText(children) });
2553
2686
  }
2554
2687
  function KeepItemCardTitle({ as = "h3", children, ...props }) {
2555
2688
  const context = useKeepItemCardCompound("Title");
2556
2689
  return createElement2(
2557
2690
  as,
2558
2691
  { ...props, "data-keep-card-part": "title" },
2559
- context.renderTitle(children ?? context.resolvedTitle)
2692
+ context.renderTitle(context.renderText(children ?? context.resolvedTitle))
2560
2693
  );
2561
2694
  }
2695
+ function KeepMediaPlaceholderIcon() {
2696
+ return /* @__PURE__ */ jsxs5("svg", { "data-media-fallback-icon": "true", viewBox: "0 0 24 24", "aria-hidden": "true", children: [
2697
+ /* @__PURE__ */ jsx9(
2698
+ "path",
2699
+ {
2700
+ d: "M4 5.5A1.5 1.5 0 0 1 5.5 4h13A1.5 1.5 0 0 1 20 5.5v13a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 4 18.5v-13Z",
2701
+ fill: "none",
2702
+ stroke: "currentColor"
2703
+ }
2704
+ ),
2705
+ /* @__PURE__ */ jsx9("circle", { cx: "9", cy: "9", r: "1.5", fill: "currentColor" }),
2706
+ /* @__PURE__ */ jsx9("path", { d: "m5.5 18 4.5-4.5 3 3 2-2L19 18", fill: "none", stroke: "currentColor" })
2707
+ ] });
2708
+ }
2562
2709
  function KeepItemCardTags({ children, ...props }) {
2563
2710
  const context = useKeepItemCardCompound("Tags");
2564
2711
  if (children === void 0 && context.renderedTags) return context.renderedTags;
@@ -2593,7 +2740,7 @@ function getErrorMessage2(error, fallback) {
2593
2740
  }
2594
2741
 
2595
2742
  // src/KeepList.tsx
2596
- import { Fragment as Fragment4, jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
2743
+ import { Fragment as Fragment5, jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
2597
2744
  function KeepList(props) {
2598
2745
  const { fallback, onBoundaryError, boundaryResetKey, ...listProps } = props;
2599
2746
  const content = /* @__PURE__ */ jsx10(KeepListContent, { ...listProps });
@@ -2612,10 +2759,13 @@ function KeepListContent({
2612
2759
  itemCardProps,
2613
2760
  layout = "list",
2614
2761
  asChild = false,
2762
+ onKeyDown,
2763
+ onFocusCapture,
2615
2764
  className,
2616
2765
  ...rootProps
2617
2766
  }) {
2618
2767
  const view = useKeepListView(query);
2768
+ const roving = useRovingTabIndex();
2619
2769
  const { state } = view;
2620
2770
  const body = getListBody(state, {
2621
2771
  children,
@@ -2638,9 +2788,20 @@ function KeepListContent({
2638
2788
  "data-layout": layout,
2639
2789
  "aria-busy": state.isLoading || rootProps["aria-busy"],
2640
2790
  "data-state": getListState(state),
2641
- "data-loading": state.isLoading ? "true" : void 0
2791
+ "data-loading": state.isLoading ? "true" : void 0,
2792
+ "data-roving-tabindex": "true",
2793
+ role: rootProps.role ?? "group",
2794
+ ref: roving.ref,
2795
+ onKeyDown: (event) => {
2796
+ onKeyDown?.(event);
2797
+ if (!event.defaultPrevented) roving.onKeyDown(event);
2798
+ },
2799
+ onFocusCapture: (event) => {
2800
+ onFocusCapture?.(event);
2801
+ if (!event.defaultPrevented) roving.onFocusCapture(event);
2802
+ }
2642
2803
  },
2643
- body,
2804
+ /* @__PURE__ */ jsx10(KeepSearchQueryProvider, { query: query?.search?.query, children: body }),
2644
2805
  "KeepList"
2645
2806
  );
2646
2807
  }
@@ -2655,7 +2816,7 @@ function getListBody(state, options) {
2655
2816
  if (state.isLoading && !state.isHydrated) {
2656
2817
  if (options.loading !== void 0) return resolveContent(options.loading, state);
2657
2818
  const count = Number.isFinite(options.loadingCount) ? Math.max(0, Math.floor(options.loadingCount)) : 6;
2658
- return /* @__PURE__ */ jsxs6(Fragment4, { children: [
2819
+ return /* @__PURE__ */ jsxs6(Fragment5, { children: [
2659
2820
  /* @__PURE__ */ jsx10("span", { role: "status", "data-keepkit": "loading-label", children: options.loadingLabel }),
2660
2821
  /* @__PURE__ */ jsx10("ul", { "data-keepkit": "skeleton-list", "data-layout": options.layout, children: Array.from({ length: count }, (_, index) => (
2661
2822
  // biome-ignore lint/suspicious/noArrayIndexKey: Static loading placeholders never reorder.
@@ -2676,16 +2837,16 @@ import { isValidElement as isValidElement4 } from "react";
2676
2837
 
2677
2838
  // src/hooks/useKeepTagFilter.ts
2678
2839
  import { useKeepList as useKeepList4 } from "@keepkit/core/react";
2679
- import { useCallback as useCallback3, useMemo as useMemo3, useState as useState5 } from "react";
2840
+ import { useCallback as useCallback4, useMemo as useMemo3, useState as useState6 } from "react";
2680
2841
  function useKeepTagFilter(options) {
2681
2842
  const { query, controlledValue, defaultValue, onChange, onValueChange } = options;
2682
- const [uncontrolledValue, setUncontrolledValue] = useState5(defaultValue);
2843
+ const [uncontrolledValue, setUncontrolledValue] = useState6(defaultValue);
2683
2844
  const resolvedValue = controlledValue ?? uncontrolledValue;
2684
2845
  const list = useKeepList4({
2685
2846
  ...query,
2686
2847
  tags: resolvedValue ? [...query?.tags ?? [], resolvedValue] : query?.tags
2687
2848
  });
2688
- const select = useCallback3(
2849
+ const select = useCallback4(
2689
2850
  (tag) => {
2690
2851
  if (controlledValue === void 0) setUncontrolledValue(tag);
2691
2852
  onChange?.(tag);
@@ -2770,12 +2931,12 @@ function KeepTagFilter({
2770
2931
  }
2771
2932
 
2772
2933
  // src/hooks/useQueryControls.ts
2773
- import { useEffect as useEffect3, useState as useState6 } from "react";
2934
+ import { useEffect as useEffect5, useState as useState7 } from "react";
2774
2935
  function useKeepSearchInput(options) {
2775
2936
  const { controlledValue, defaultValue, debounceMs, onValueChange } = options;
2776
- const [uncontrolledValue, setUncontrolledValue] = useState6(defaultValue);
2937
+ const [uncontrolledValue, setUncontrolledValue] = useState7(defaultValue);
2777
2938
  const value = controlledValue ?? uncontrolledValue;
2778
- useEffect3(() => {
2939
+ useEffect5(() => {
2779
2940
  if (!onValueChange) return;
2780
2941
  if (debounceMs <= 0) {
2781
2942
  onValueChange(value);
@@ -2794,7 +2955,7 @@ function useKeepSearchInput(options) {
2794
2955
  }
2795
2956
  function useKeepSortSelect(options) {
2796
2957
  const { controlledValue, defaultValue, onValueChange } = options;
2797
- const [uncontrolledValue, setUncontrolledValue] = useState6(defaultValue);
2958
+ const [uncontrolledValue, setUncontrolledValue] = useState7(defaultValue);
2798
2959
  const value = controlledValue ?? uncontrolledValue;
2799
2960
  return {
2800
2961
  value,
@@ -2842,7 +3003,7 @@ function getVisiblePages(currentPage, pageCount, maxPageButtons) {
2842
3003
  }
2843
3004
 
2844
3005
  // src/query-controls.tsx
2845
- import { Fragment as Fragment5, jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
3006
+ import { Fragment as Fragment6, jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
2846
3007
  function KeepSearchInput({
2847
3008
  value: controlledValue,
2848
3009
  defaultValue = "",
@@ -2878,7 +3039,7 @@ function KeepSortSelect({
2878
3039
  ...props
2879
3040
  }) {
2880
3041
  const view = useKeepSortSelect({ controlledValue, defaultValue, onValueChange });
2881
- const options = children ?? /* @__PURE__ */ jsxs8(Fragment5, { children: [
3042
+ const options = children ?? /* @__PURE__ */ jsxs8(Fragment6, { children: [
2882
3043
  /* @__PURE__ */ jsx12("option", { value: "updatedAt:desc", children: view.labels.updatedNewest }),
2883
3044
  /* @__PURE__ */ jsx12("option", { value: "updatedAt:asc", children: view.labels.updatedOldest }),
2884
3045
  /* @__PURE__ */ jsx12("option", { value: "savedAt:desc", children: view.labels.savedNewest }),
@@ -3032,8 +3193,31 @@ function getCollectionState(list) {
3032
3193
 
3033
3194
  // src/KeepLayout.tsx
3034
3195
  import { jsx as jsx14 } from "react/jsx-runtime";
3035
- function KeepLayout({ layout = "list", children, ...props }) {
3036
- return /* @__PURE__ */ jsx14("div", { ...props, "data-keepkit": "layout", "data-layout": layout, children });
3196
+ function KeepLayout({ layout = "list", children, onKeyDown, onFocusCapture, ...props }) {
3197
+ const roving = useRovingTabIndex();
3198
+ return (
3199
+ // biome-ignore lint/a11y/noStaticElementInteractions: The group manages keyboard focus for descendant cards.
3200
+ /* @__PURE__ */ jsx14(
3201
+ "div",
3202
+ {
3203
+ ...props,
3204
+ ref: roving.ref,
3205
+ "data-keepkit": "layout",
3206
+ "data-layout": layout,
3207
+ "data-roving-tabindex": "true",
3208
+ role: props.role ?? "group",
3209
+ onKeyDown: (event) => {
3210
+ onKeyDown?.(event);
3211
+ if (!event.defaultPrevented) roving.onKeyDown(event);
3212
+ },
3213
+ onFocusCapture: (event) => {
3214
+ onFocusCapture?.(event);
3215
+ if (!event.defaultPrevented) roving.onFocusCapture(event);
3216
+ },
3217
+ children
3218
+ }
3219
+ )
3220
+ );
3037
3221
  }
3038
3222
 
3039
3223
  // src/KeepNoteEditor.tsx
@@ -3041,16 +3225,16 @@ import { isValidElement as isValidElement5 } from "react";
3041
3225
 
3042
3226
  // src/hooks/useKeepNoteEditor.ts
3043
3227
  import { useKeepItem as useKeepItem3 } from "@keepkit/core/react";
3044
- import { useCallback as useCallback4, useEffect as useEffect4, useRef as useRef4, useState as useState7 } from "react";
3228
+ import { useCallback as useCallback5, useEffect as useEffect6, useRef as useRef5, useState as useState8 } from "react";
3045
3229
  function useKeepNoteEditor({ item, debounceMs, onSaved, onSaveError }) {
3046
3230
  const itemState = useKeepItem3(item);
3047
3231
  const { error, isMutating, item: savedItem, updateNote } = itemState;
3048
- const [note, setNote] = useState7(item.note ?? "");
3232
+ const [note, setNote] = useState8(item.note ?? "");
3049
3233
  const baselineNote = savedItem?.note ?? item.note ?? "";
3050
3234
  const isDirty = note !== baselineNote;
3051
- const lastSavedNoteRef = useRef4(void 0);
3052
- useEffect4(() => setNote(baselineNote), [baselineNote]);
3053
- const save = useCallback4(async () => {
3235
+ const lastSavedNoteRef = useRef5(void 0);
3236
+ useEffect6(() => setNote(baselineNote), [baselineNote]);
3237
+ const save = useCallback5(async () => {
3054
3238
  const nextNote = note.trim() || void 0;
3055
3239
  try {
3056
3240
  await updateNote(nextNote);
@@ -3061,7 +3245,7 @@ function useKeepNoteEditor({ item, debounceMs, onSaved, onSaveError }) {
3061
3245
  throw cause;
3062
3246
  }
3063
3247
  }, [note, onSaveError, onSaved, updateNote]);
3064
- useEffect4(() => {
3248
+ useEffect6(() => {
3065
3249
  if (!isDirty || debounceMs <= 0 || lastSavedNoteRef.current === note) return;
3066
3250
  const timer = window.setTimeout(() => void save().catch(() => void 0), debounceMs);
3067
3251
  return () => window.clearTimeout(timer);
@@ -3096,7 +3280,7 @@ function useKeepNoteEditor({ item, debounceMs, onSaved, onSaveError }) {
3096
3280
  }
3097
3281
 
3098
3282
  // src/KeepNoteEditor.tsx
3099
- import { Fragment as Fragment6, jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
3283
+ import { Fragment as Fragment7, jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
3100
3284
  function KeepNoteEditor({
3101
3285
  item,
3102
3286
  label,
@@ -3114,7 +3298,7 @@ function KeepNoteEditor({
3114
3298
  const view = useKeepNoteEditor({ item, debounceMs, onSaved, onSaveError });
3115
3299
  const { error, isDirty, isSaving, note, setNote } = view.state;
3116
3300
  const contentChildren = asChild && isValidElement5(children) ? void 0 : children;
3117
- const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? /* @__PURE__ */ jsxs10(Fragment6, { children: [
3301
+ const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? /* @__PURE__ */ jsxs10(Fragment7, { children: [
3118
3302
  /* @__PURE__ */ jsxs10("label", { children: [
3119
3303
  label ?? view.labels.note,
3120
3304
  /* @__PURE__ */ jsx15(
@@ -3171,16 +3355,89 @@ function getErrorMessage3(error, fallback) {
3171
3355
  return error instanceof Error ? error.message : fallback;
3172
3356
  }
3173
3357
 
3358
+ // src/KeepReorderableList.tsx
3359
+ import { useState as useState9 } from "react";
3360
+ import { jsx as jsx16 } from "react/jsx-runtime";
3361
+ function KeepReorderableList({
3362
+ items,
3363
+ onReorder,
3364
+ renderItem,
3365
+ itemLabel = (_item, index) => `Move item ${index + 1}`,
3366
+ ...props
3367
+ }) {
3368
+ const [draggedId, setDraggedId] = useState9(null);
3369
+ const ids = items.map((item) => item.id);
3370
+ function commit(nextIds) {
3371
+ void onReorder(nextIds);
3372
+ }
3373
+ function move(index, targetIndex) {
3374
+ if (targetIndex < 0 || targetIndex >= ids.length || targetIndex === index) return;
3375
+ const next = [...ids];
3376
+ const [id] = next.splice(index, 1);
3377
+ if (id === void 0) return;
3378
+ next.splice(targetIndex, 0, id);
3379
+ commit(next);
3380
+ }
3381
+ return /* @__PURE__ */ jsx16("ul", { ...props, "data-keepkit": "reorderable-list", children: items.map((item, index) => {
3382
+ const moveUp = () => move(index, index - 1);
3383
+ const moveDown = () => move(index, index + 1);
3384
+ const onHandleKeyDown = (event) => {
3385
+ if (event.key === "ArrowUp") {
3386
+ event.preventDefault();
3387
+ moveUp();
3388
+ } else if (event.key === "ArrowDown") {
3389
+ event.preventDefault();
3390
+ moveDown();
3391
+ }
3392
+ };
3393
+ const state = {
3394
+ index,
3395
+ isDragging: draggedId === item.id,
3396
+ moveUp,
3397
+ moveDown,
3398
+ dragHandleProps: {
3399
+ role: "button",
3400
+ tabIndex: 0,
3401
+ draggable: true,
3402
+ "aria-label": itemLabel(item, index),
3403
+ "aria-grabbed": draggedId === item.id,
3404
+ onDragStart: () => setDraggedId(item.id),
3405
+ onDragEnd: () => setDraggedId(null),
3406
+ onKeyDown: onHandleKeyDown
3407
+ }
3408
+ };
3409
+ return /* @__PURE__ */ jsx16(
3410
+ "li",
3411
+ {
3412
+ "data-reorder-index": index,
3413
+ "data-dragging": draggedId === item.id ? "true" : void 0,
3414
+ onDragOver: (event) => {
3415
+ if (draggedId && draggedId !== item.id) event.preventDefault();
3416
+ },
3417
+ onDrop: (event) => {
3418
+ event.preventDefault();
3419
+ if (!draggedId || draggedId === item.id) return;
3420
+ const sourceIndex = ids.indexOf(draggedId);
3421
+ setDraggedId(null);
3422
+ move(sourceIndex, index);
3423
+ },
3424
+ children: renderItem(item, state)
3425
+ },
3426
+ item.id
3427
+ );
3428
+ }) });
3429
+ }
3430
+
3174
3431
  // src/hooks/useKeepSyncFeedback.ts
3175
3432
  import { useKeepContext as useKeepContext3 } from "@keepkit/core/react";
3176
- import { useEffect as useEffect5, useRef as useRef5 } from "react";
3433
+ import { useEffect as useEffect7, useRef as useRef6 } from "react";
3177
3434
  function useKeepSyncFeedback() {
3178
3435
  const { syncState } = useKeepContext3();
3179
3436
  const emitFeedback = useKeepUiFeedback();
3180
3437
  const completedMessage = useUiLabel("syncSynced");
3181
3438
  const failedMessage = useUiLabel("syncFailedMessage");
3182
- const previousStatus = useRef5("idle");
3183
- useEffect5(() => {
3439
+ const previousStatus = useRef6("idle");
3440
+ useEffect7(() => {
3184
3441
  const previous = previousStatus.current;
3185
3442
  previousStatus.current = syncState.status;
3186
3443
  if (syncState.status === "error" && previous !== "error") {
@@ -3201,16 +3458,16 @@ function KeepSyncFeedbackObserver() {
3201
3458
 
3202
3459
  // src/hooks/useKeepSyncRecoveryDialog.ts
3203
3460
  import { useKeepContext as useKeepContext4 } from "@keepkit/core/react";
3204
- import { useEffect as useEffect6, useState as useState8 } from "react";
3461
+ import { useEffect as useEffect8, useState as useState10 } from "react";
3205
3462
  function useKeepSyncRecoveryDialog(options) {
3206
3463
  const { open, onOpenChange, conflicts, onManualMerge } = options;
3207
3464
  const context = useKeepContext4();
3208
3465
  const conflictList = conflicts ?? context.syncState.conflicts ?? [];
3209
3466
  const hasRecovery = conflictList.length > 0 || context.syncState.status === "error" || Boolean(context.error);
3210
- const [dismissed, setDismissed] = useState8(false);
3211
- const [busyId, setBusyId] = useState8();
3212
- const [error, setError] = useState8();
3213
- useEffect6(() => {
3467
+ const [dismissed, setDismissed] = useState10(false);
3468
+ const [busyId, setBusyId] = useState10();
3469
+ const [error, setError] = useState10();
3470
+ useEffect8(() => {
3214
3471
  if (hasRecovery) setDismissed(false);
3215
3472
  }, [hasRecovery]);
3216
3473
  return {
@@ -3255,7 +3512,7 @@ function useKeepSyncRecoveryDialog(options) {
3255
3512
  }
3256
3513
 
3257
3514
  // src/KeepSyncRecoveryDialog.tsx
3258
- import { jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
3515
+ import { jsx as jsx17, jsxs as jsxs11 } from "react/jsx-runtime";
3259
3516
  function KeepSyncRecoveryDialog({
3260
3517
  open,
3261
3518
  onOpenChange,
@@ -3285,16 +3542,16 @@ function KeepSyncRecoveryDialog({
3285
3542
  "data-loading": view.busyId !== void 0 ? "true" : void 0,
3286
3543
  children: [
3287
3544
  /* @__PURE__ */ jsxs11("header", { children: [
3288
- /* @__PURE__ */ jsx16("h2", { id: "keepkit-sync-recovery-title", children: title ?? view.labels.title }),
3289
- /* @__PURE__ */ jsx16("button", { type: "button", "data-keep-action": "close-dialog", onClick: view.close, "aria-label": view.labels.close, children: view.labels.close })
3545
+ /* @__PURE__ */ jsx17("h2", { id: "keepkit-sync-recovery-title", children: title ?? view.labels.title }),
3546
+ /* @__PURE__ */ jsx17("button", { type: "button", "data-keep-action": "close-dialog", onClick: view.close, "aria-label": view.labels.close, children: view.labels.close })
3290
3547
  ] }),
3291
3548
  children,
3292
3549
  view.conflictList.length > 0 ? /* @__PURE__ */ jsxs11("div", { children: [
3293
- /* @__PURE__ */ jsx16("p", { children: view.labels.conflict }),
3550
+ /* @__PURE__ */ jsx17("p", { children: view.labels.conflict }),
3294
3551
  view.conflictList.map((conflict) => /* @__PURE__ */ jsxs11("article", { "data-conflict-id": conflict.id, children: [
3295
- /* @__PURE__ */ jsx16("h3", { children: getMetaTitle(conflict.operation.item?.meta) ?? conflict.id }),
3552
+ /* @__PURE__ */ jsx17("h3", { children: getMetaTitle(conflict.operation.item?.meta) ?? conflict.id }),
3296
3553
  /* @__PURE__ */ jsxs11("div", { "data-conflict-preview": true, children: [
3297
- /* @__PURE__ */ jsx16(
3554
+ /* @__PURE__ */ jsx17(
3298
3555
  ConflictPreview,
3299
3556
  {
3300
3557
  item: conflict.operation.item,
@@ -3304,7 +3561,7 @@ function KeepSyncRecoveryDialog({
3304
3561
  side: "local"
3305
3562
  }
3306
3563
  ),
3307
- /* @__PURE__ */ jsx16(
3564
+ /* @__PURE__ */ jsx17(
3308
3565
  ConflictPreview,
3309
3566
  {
3310
3567
  item: conflict.remote,
@@ -3316,7 +3573,7 @@ function KeepSyncRecoveryDialog({
3316
3573
  )
3317
3574
  ] }),
3318
3575
  /* @__PURE__ */ jsxs11("div", { children: [
3319
- /* @__PURE__ */ jsx16(
3576
+ /* @__PURE__ */ jsx17(
3320
3577
  "button",
3321
3578
  {
3322
3579
  type: "button",
@@ -3326,7 +3583,7 @@ function KeepSyncRecoveryDialog({
3326
3583
  children: view.labels.keepLocal
3327
3584
  }
3328
3585
  ),
3329
- /* @__PURE__ */ jsx16(
3586
+ /* @__PURE__ */ jsx17(
3330
3587
  "button",
3331
3588
  {
3332
3589
  type: "button",
@@ -3336,7 +3593,7 @@ function KeepSyncRecoveryDialog({
3336
3593
  children: view.labels.useServer
3337
3594
  }
3338
3595
  ),
3339
- /* @__PURE__ */ jsx16(
3596
+ /* @__PURE__ */ jsx17(
3340
3597
  "button",
3341
3598
  {
3342
3599
  type: "button",
@@ -3350,11 +3607,11 @@ function KeepSyncRecoveryDialog({
3350
3607
  ] }, conflict.id))
3351
3608
  ] }) : null,
3352
3609
  view.showBackupRecovery ? /* @__PURE__ */ jsxs11("section", { "data-recovery": "backup", children: [
3353
- /* @__PURE__ */ jsx16("h3", { children: view.labels.backupRecovery }),
3354
- /* @__PURE__ */ jsx16("p", { children: view.labels.backupRecoveryDescription }),
3355
- backup ?? (showBackupControls ? /* @__PURE__ */ jsx16(KeepBackup, {}) : null)
3610
+ /* @__PURE__ */ jsx17("h3", { children: view.labels.backupRecovery }),
3611
+ /* @__PURE__ */ jsx17("p", { children: view.labels.backupRecoveryDescription }),
3612
+ backup ?? (showBackupControls ? /* @__PURE__ */ jsx17(KeepBackup, {}) : null)
3356
3613
  ] }) : null,
3357
- view.error ? /* @__PURE__ */ jsx16("p", { id: "keepkit-sync-recovery-error", role: "alert", "aria-live": "assertive", children: view.error instanceof Error ? view.error.message : view.labels.error }) : null
3614
+ view.error ? /* @__PURE__ */ jsx17("p", { id: "keepkit-sync-recovery-error", role: "alert", "aria-live": "assertive", children: view.error instanceof Error ? view.error.message : view.labels.error }) : null
3358
3615
  ]
3359
3616
  }
3360
3617
  );
@@ -3367,15 +3624,15 @@ function ConflictPreview({
3367
3624
  side
3368
3625
  }) {
3369
3626
  return /* @__PURE__ */ jsxs11("article", { "data-conflict-version": side, "aria-label": heading, children: [
3370
- /* @__PURE__ */ jsx16("h4", { children: heading }),
3627
+ /* @__PURE__ */ jsx17("h4", { children: heading }),
3371
3628
  /* @__PURE__ */ jsxs11("dl", { children: [
3372
3629
  /* @__PURE__ */ jsxs11("div", { children: [
3373
- /* @__PURE__ */ jsx16("dt", { children: updatedAtLabel }),
3374
- /* @__PURE__ */ jsx16("dd", { children: item ? /* @__PURE__ */ jsx16("time", { dateTime: new Date(item.updatedAt).toISOString(), children: formatConflictDate(item.updatedAt) }) : "\u2014" })
3630
+ /* @__PURE__ */ jsx17("dt", { children: updatedAtLabel }),
3631
+ /* @__PURE__ */ jsx17("dd", { children: item ? /* @__PURE__ */ jsx17("time", { dateTime: new Date(item.updatedAt).toISOString(), children: formatConflictDate(item.updatedAt) }) : "\u2014" })
3375
3632
  ] }),
3376
3633
  /* @__PURE__ */ jsxs11("div", { children: [
3377
- /* @__PURE__ */ jsx16("dt", { children: noteLabel }),
3378
- /* @__PURE__ */ jsx16("dd", { children: item?.note || "\u2014" })
3634
+ /* @__PURE__ */ jsx17("dt", { children: noteLabel }),
3635
+ /* @__PURE__ */ jsx17("dd", { children: item?.note || "\u2014" })
3379
3636
  ] })
3380
3637
  ] })
3381
3638
  ] });
@@ -3419,7 +3676,7 @@ function getErrorMessage4(error) {
3419
3676
  }
3420
3677
 
3421
3678
  // src/KeepSyncStatusBanner.tsx
3422
- import { jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
3679
+ import { jsx as jsx18, jsxs as jsxs12 } from "react/jsx-runtime";
3423
3680
  function KeepSyncStatusBanner({
3424
3681
  onRetry,
3425
3682
  onResolveConflicts,
@@ -3439,8 +3696,8 @@ function KeepSyncStatusBanner({
3439
3696
  "data-keepkit": "sync-status",
3440
3697
  "data-state": view.status,
3441
3698
  children: [
3442
- /* @__PURE__ */ jsx17("p", { children: view.message }),
3443
- view.showRetry ? /* @__PURE__ */ jsx17(
3699
+ /* @__PURE__ */ jsx18("p", { children: view.message }),
3700
+ view.showRetry ? /* @__PURE__ */ jsx18(
3444
3701
  "button",
3445
3702
  {
3446
3703
  type: "button",
@@ -3450,7 +3707,7 @@ function KeepSyncStatusBanner({
3450
3707
  children: view.retryLabel
3451
3708
  }
3452
3709
  ) : null,
3453
- view.hasConflicts ? /* @__PURE__ */ jsx17(
3710
+ view.hasConflicts ? /* @__PURE__ */ jsx18(
3454
3711
  "button",
3455
3712
  {
3456
3713
  type: "button",
@@ -3467,13 +3724,13 @@ function KeepSyncStatusBanner({
3467
3724
 
3468
3725
  // src/hooks/useKeepTagEditor.ts
3469
3726
  import { useKeepItem as useKeepItem4 } from "@keepkit/core/react";
3470
- import { useCallback as useCallback5, useEffect as useEffect7, useState as useState9 } from "react";
3727
+ import { useCallback as useCallback6, useEffect as useEffect9, useState as useState11 } from "react";
3471
3728
  function useKeepTagEditor({ item, onSaved, onSaveError }) {
3472
3729
  const itemState = useKeepItem4(item);
3473
- const [tags, setTags] = useState9(item.tags ?? []);
3474
- const [input, setInput] = useState9("");
3475
- useEffect7(() => setTags(itemState.item?.tags ?? item.tags ?? []), [item.tags, itemState.item?.tags]);
3476
- const save = useCallback5(async () => {
3730
+ const [tags, setTags] = useState11(item.tags ?? []);
3731
+ const [input, setInput] = useState11("");
3732
+ useEffect9(() => setTags(itemState.item?.tags ?? item.tags ?? []), [item.tags, itemState.item?.tags]);
3733
+ const save = useCallback6(async () => {
3477
3734
  const nextTags = normalizeUiTags(tags);
3478
3735
  try {
3479
3736
  await itemState.updateTags(nextTags);
@@ -3519,7 +3776,7 @@ function useKeepTagEditor({ item, onSaved, onSaveError }) {
3519
3776
  }
3520
3777
 
3521
3778
  // src/KeepTagEditor.tsx
3522
- import { Fragment as Fragment7, jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
3779
+ import { Fragment as Fragment8, jsx as jsx19, jsxs as jsxs13 } from "react/jsx-runtime";
3523
3780
  function KeepTagEditor({
3524
3781
  item,
3525
3782
  availableTags = [],
@@ -3530,10 +3787,10 @@ function KeepTagEditor({
3530
3787
  }) {
3531
3788
  const view = useKeepTagEditor({ item, onSaved, onSaveError });
3532
3789
  const { isSaving, tags } = view.state;
3533
- const body = render ? render(view.state) : /* @__PURE__ */ jsxs13(Fragment7, { children: [
3790
+ const body = render ? render(view.state) : /* @__PURE__ */ jsxs13(Fragment8, { children: [
3534
3791
  /* @__PURE__ */ jsxs13("label", { children: [
3535
3792
  view.labels.tags,
3536
- /* @__PURE__ */ jsx18(
3793
+ /* @__PURE__ */ jsx19(
3537
3794
  "input",
3538
3795
  {
3539
3796
  "data-keep-action": "edit-tags",
@@ -3544,12 +3801,12 @@ function KeepTagEditor({
3544
3801
  }
3545
3802
  )
3546
3803
  ] }),
3547
- availableTags.length > 0 ? /* @__PURE__ */ jsx18("datalist", { id: `keep-tags-${item.id}`, children: availableTags.map((tag) => /* @__PURE__ */ jsx18("option", { value: tag }, tag)) }) : null,
3548
- /* @__PURE__ */ jsx18("ul", { "aria-label": view.labels.tags, children: tags.map((tag) => /* @__PURE__ */ jsxs13("li", { children: [
3804
+ availableTags.length > 0 ? /* @__PURE__ */ jsx19("datalist", { id: `keep-tags-${item.id}`, children: availableTags.map((tag) => /* @__PURE__ */ jsx19("option", { value: tag }, tag)) }) : null,
3805
+ /* @__PURE__ */ jsx19("ul", { "aria-label": view.labels.tags, children: tags.map((tag) => /* @__PURE__ */ jsxs13("li", { children: [
3549
3806
  tag,
3550
- /* @__PURE__ */ jsx18("button", { type: "button", "data-keep-action": "remove-tag", onClick: () => view.removeTag(tag), children: view.labels.remove })
3807
+ /* @__PURE__ */ jsx19("button", { type: "button", "data-keep-action": "remove-tag", onClick: () => view.removeTag(tag), children: view.labels.remove })
3551
3808
  ] }, tag)) }),
3552
- /* @__PURE__ */ jsx18("button", { type: "submit", "data-keep-action": "apply-tags", disabled: isSaving, "aria-busy": isSaving, children: view.labels.apply })
3809
+ /* @__PURE__ */ jsx19("button", { type: "submit", "data-keep-action": "apply-tags", disabled: isSaving, "aria-busy": isSaving, children: view.labels.apply })
3553
3810
  ] });
3554
3811
  return /* @__PURE__ */ jsxs13(
3555
3812
  "form",
@@ -3563,7 +3820,7 @@ function KeepTagEditor({
3563
3820
  "data-disabled": isSaving ? "true" : void 0,
3564
3821
  children: [
3565
3822
  body,
3566
- view.error ? /* @__PURE__ */ jsx18("p", { role: "alert", children: getErrorMessage5(view.error, view.labels.error) }) : null
3823
+ view.error ? /* @__PURE__ */ jsx19("p", { role: "alert", children: getErrorMessage5(view.error, view.labels.error) }) : null
3567
3824
  ]
3568
3825
  }
3569
3826
  );
@@ -3572,6 +3829,117 @@ function getErrorMessage5(error, fallback) {
3572
3829
  return error instanceof Error ? error.message : fallback;
3573
3830
  }
3574
3831
 
3832
+ // src/KeepTourBar.tsx
3833
+ import { useKeepNavigator } from "@keepkit/core/react";
3834
+
3835
+ // src/hooks/useKeepTourShortcuts.ts
3836
+ import { useEffect as useEffect10 } from "react";
3837
+ function useKeepTourShortcuts({
3838
+ onNext,
3839
+ onPrev,
3840
+ enabled = true,
3841
+ allowInEditable = false,
3842
+ preventDefault = true,
3843
+ nextKeys = ["j", "]"],
3844
+ prevKeys = ["k", "["],
3845
+ onError
3846
+ }) {
3847
+ useEffect10(() => {
3848
+ if (!enabled) return;
3849
+ const handleKeyDown = (event) => {
3850
+ if (!allowInEditable && isEditableTarget(event.target)) return;
3851
+ if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return;
3852
+ const key = event.key.toLocaleLowerCase();
3853
+ const action = nextKeys.some((candidate) => candidate.toLocaleLowerCase() === key) ? onNext : prevKeys.some((candidate) => candidate.toLocaleLowerCase() === key) ? onPrev : void 0;
3854
+ if (!action) return;
3855
+ if (preventDefault) event.preventDefault();
3856
+ void Promise.resolve(action()).catch((error) => onError?.(error));
3857
+ };
3858
+ window.addEventListener("keydown", handleKeyDown);
3859
+ return () => window.removeEventListener("keydown", handleKeyDown);
3860
+ }, [allowInEditable, enabled, nextKeys, onError, onNext, onPrev, preventDefault, prevKeys]);
3861
+ }
3862
+ function isEditableTarget(target) {
3863
+ if (!(target instanceof HTMLElement)) return false;
3864
+ return target.isContentEditable || target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT";
3865
+ }
3866
+
3867
+ // src/KeepTourBar.tsx
3868
+ import { jsx as jsx20, jsxs as jsxs14 } from "react/jsx-runtime";
3869
+ function KeepTourBar({
3870
+ navigation: providedNavigation,
3871
+ currentId,
3872
+ initialIndex,
3873
+ showProgress = true,
3874
+ prevHref,
3875
+ nextHref,
3876
+ backHref,
3877
+ onPrev,
3878
+ onNext,
3879
+ onBack,
3880
+ prevLabel,
3881
+ nextLabel,
3882
+ backLabel,
3883
+ keyboardShortcuts = false,
3884
+ shortcutOptions,
3885
+ progress,
3886
+ ...props
3887
+ }) {
3888
+ const ownNavigation = useKeepNavigator({ currentId, initialIndex });
3889
+ const navigation = providedNavigation ?? ownNavigation;
3890
+ const previousLabel = useUiLabel("previousPage", prevLabel);
3891
+ const nextItemLabel = useUiLabel("nextPage", nextLabel);
3892
+ const listLabel = useUiLabel("allItems", backLabel);
3893
+ const { labels } = useKeepUiLabels();
3894
+ const resolvedPrev = onPrev ?? (() => navigateTo(navigation.goToPrev(), prevHref));
3895
+ const resolvedNext = onNext ?? (() => navigateTo(navigation.goToNext(), nextHref));
3896
+ useKeepTourShortcuts({
3897
+ ...shortcutOptions,
3898
+ enabled: keyboardShortcuts && (shortcutOptions?.enabled ?? true),
3899
+ onNext: () => {
3900
+ return resolvedNext();
3901
+ },
3902
+ onPrev: () => {
3903
+ return resolvedPrev();
3904
+ }
3905
+ });
3906
+ return /* @__PURE__ */ jsxs14("nav", { ...props, "data-keepkit": "tour-bar", "aria-label": props["aria-label"] ?? labels.pagination, children: [
3907
+ showProgress ? /* @__PURE__ */ jsx20("span", { "data-keepkit": "tour-progress", "aria-live": "polite", children: progress ?? `${navigation.currentPosition ?? 0} / ${navigation.items.length}` }) : null,
3908
+ /* @__PURE__ */ jsx20(
3909
+ TourAction,
3910
+ {
3911
+ href: prevHref,
3912
+ disabled: !navigation.hasPrev,
3913
+ onClick: prevHref ? onPrev : resolvedPrev,
3914
+ "data-keep-action": "tour-prev",
3915
+ children: previousLabel
3916
+ }
3917
+ ),
3918
+ /* @__PURE__ */ jsx20(
3919
+ TourAction,
3920
+ {
3921
+ href: nextHref,
3922
+ disabled: !navigation.hasNext,
3923
+ onClick: nextHref ? onNext : resolvedNext,
3924
+ "data-keep-action": "tour-next",
3925
+ children: nextItemLabel
3926
+ }
3927
+ ),
3928
+ backHref || onBack ? /* @__PURE__ */ jsx20(TourAction, { href: backHref, onClick: onBack, "data-keep-action": "tour-back", children: listLabel }) : null
3929
+ ] });
3930
+ }
3931
+ function TourAction({ href, disabled = false, onClick, children, ...props }) {
3932
+ if (href && !disabled) {
3933
+ return /* @__PURE__ */ jsx20("a", { ...props, href, onClick: () => void onClick?.(), children });
3934
+ }
3935
+ return /* @__PURE__ */ jsx20("button", { ...props, type: "button", disabled, onClick: () => void onClick?.(), children });
3936
+ }
3937
+ var KeepNavigator = KeepTourBar;
3938
+ function navigateTo(item, href) {
3939
+ if (!item || !href || typeof window === "undefined") return;
3940
+ window.location.assign(href);
3941
+ }
3942
+
3575
3943
  // src/hooks/useKeepUndo.ts
3576
3944
  import { useKeepContext as useKeepContext6 } from "@keepkit/core/react";
3577
3945
  function useKeepUndo() {
@@ -3593,13 +3961,13 @@ function useKeepUndo() {
3593
3961
  }
3594
3962
 
3595
3963
  // src/KeepUndo.tsx
3596
- import { jsx as jsx19, jsxs as jsxs14 } from "react/jsx-runtime";
3964
+ import { jsx as jsx21, jsxs as jsxs15 } from "react/jsx-runtime";
3597
3965
  function KeepUndo({ children, label, ...props }) {
3598
3966
  const view = useKeepUndo();
3599
3967
  if (!view.canUndo) return null;
3600
- return /* @__PURE__ */ jsxs14("div", { ...props, role: "status", "aria-live": "polite", "data-keepkit": "undo", "data-state": "available", children: [
3968
+ return /* @__PURE__ */ jsxs15("div", { ...props, role: "status", "aria-live": "polite", "data-keepkit": "undo", "data-state": "available", children: [
3601
3969
  children ?? view.message,
3602
- /* @__PURE__ */ jsx19("button", { type: "button", "data-keep-action": "undo", onClick: () => void view.undo(), children: label ?? view.label })
3970
+ /* @__PURE__ */ jsx21("button", { type: "button", "data-keep-action": "undo", onClick: () => void view.undo(), children: label ?? view.label })
3603
3971
  ] });
3604
3972
  }
3605
3973
 
@@ -3608,7 +3976,7 @@ import { isValidElement as isValidElement6 } from "react";
3608
3976
 
3609
3977
  // src/hooks/useStatusViews.ts
3610
3978
  import { useKeepContext as useKeepContext7 } from "@keepkit/core/react";
3611
- import { useEffect as useEffect8, useRef as useRef6, useState as useState10 } from "react";
3979
+ import { useEffect as useEffect11, useRef as useRef7, useState as useState12 } from "react";
3612
3980
  function useKeepEmptyState() {
3613
3981
  return useUiLabel("noItems").replace(/\.$/, "");
3614
3982
  }
@@ -3628,9 +3996,9 @@ function useKeepAnnouncements(messages) {
3628
3996
  const savedMessage = useUiLabel("savedMessage", messages?.save);
3629
3997
  const removedMessage = useUiLabel("removedMessage", messages?.remove);
3630
3998
  const noteSavedMessage = useUiLabel("noteSavedMessage", messages?.note);
3631
- const [message, setMessage] = useState10("");
3632
- const lastChangeRef = useRef6(void 0);
3633
- useEffect8(() => {
3999
+ const [message, setMessage] = useState12("");
4000
+ const lastChangeRef = useRef7(void 0);
4001
+ useEffect11(() => {
3634
4002
  const change = context.lastChange;
3635
4003
  if (!change || change === lastChangeRef.current) return;
3636
4004
  lastChangeRef.current = change;
@@ -3658,7 +4026,7 @@ function getStatusLabelKey3(status) {
3658
4026
  }
3659
4027
 
3660
4028
  // src/status.tsx
3661
- import { Fragment as Fragment8, jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
4029
+ import { Fragment as Fragment9, jsx as jsx22, jsxs as jsxs16 } from "react/jsx-runtime";
3662
4030
  function KeepEmptyState({
3663
4031
  title,
3664
4032
  description,
@@ -3670,9 +4038,9 @@ function KeepEmptyState({
3670
4038
  }) {
3671
4039
  const defaultTitle = useKeepEmptyState();
3672
4040
  const contentChildren = asChild && isValidElement6(children) ? void 0 : children;
3673
- const body = contentChildren ?? /* @__PURE__ */ jsxs15(Fragment8, { children: [
3674
- /* @__PURE__ */ jsx20("h2", { children: title ?? defaultTitle }),
3675
- description ? /* @__PURE__ */ jsx20("p", { children: description }) : null,
4041
+ const body = contentChildren ?? /* @__PURE__ */ jsxs16(Fragment9, { children: [
4042
+ /* @__PURE__ */ jsx22("h2", { children: title ?? defaultTitle }),
4043
+ description ? /* @__PURE__ */ jsx22("p", { children: description }) : null,
3676
4044
  action
3677
4045
  ] });
3678
4046
  return renderRoot(
@@ -3714,7 +4082,7 @@ function KeepStatus({
3714
4082
  }
3715
4083
  function KeepAnnouncements({ messages, ...props }) {
3716
4084
  const message = useKeepAnnouncements(messages);
3717
- return /* @__PURE__ */ jsx20(
4085
+ return /* @__PURE__ */ jsx22(
3718
4086
  "div",
3719
4087
  {
3720
4088
  ...props,
@@ -3731,7 +4099,7 @@ var KeepAnnouncer = KeepAnnouncements;
3731
4099
 
3732
4100
  // src/theme.tsx
3733
4101
  import { cloneElement as cloneElement2, isValidElement as isValidElement7 } from "react";
3734
- import { jsx as jsx21 } from "react/jsx-runtime";
4102
+ import { jsx as jsx23 } from "react/jsx-runtime";
3735
4103
  var keepThemeNames = [
3736
4104
  "default",
3737
4105
  "ocean",
@@ -3790,7 +4158,7 @@ function KeepThemeProvider({
3790
4158
  throw new Error("KeepThemeProvider with asChild requires a single React element child.");
3791
4159
  return cloneElement2(children, rootProps);
3792
4160
  }
3793
- return /* @__PURE__ */ jsx21("div", { ...rootProps, children });
4161
+ return /* @__PURE__ */ jsx23("div", { ...rootProps, children });
3794
4162
  }
3795
4163
 
3796
4164
  // src/index.tsx
@@ -3801,6 +4169,7 @@ import {
3801
4169
  useKeepContext as useKeepContext8,
3802
4170
  useKeepItem as useKeepItem5,
3803
4171
  useKeepList as useKeepList5,
4172
+ useKeepNavigator as useKeepNavigator2,
3804
4173
  useKeepShortcut
3805
4174
  } from "@keepkit/core/react";
3806
4175
  import {
@@ -3813,7 +4182,7 @@ import {
3813
4182
  LocalStorageSyncQueueAdapter,
3814
4183
  SyncStorageAdapter
3815
4184
  } from "@keepkit/core/storage";
3816
- import { jsx as jsx22, jsxs as jsxs16 } from "react/jsx-runtime";
4185
+ import { jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
3817
4186
  function KeepKitProvider({
3818
4187
  labels,
3819
4188
  locale,
@@ -3833,7 +4202,7 @@ function KeepKitProvider({
3833
4202
  children,
3834
4203
  ...providerProps
3835
4204
  }) {
3836
- return /* @__PURE__ */ jsx22(KeepUiProvider, { labels, locale, labelResolver, onFeedback, children: /* @__PURE__ */ jsx22(
4205
+ return /* @__PURE__ */ jsx24(KeepUiProvider, { labels, locale, labelResolver, onFeedback, children: /* @__PURE__ */ jsx24(
3837
4206
  KeepThemeProvider,
3838
4207
  {
3839
4208
  theme,
@@ -3847,10 +4216,10 @@ function KeepKitProvider({
3847
4216
  className: themeClassName,
3848
4217
  style: themeStyle,
3849
4218
  asChild: themeAsChild,
3850
- children: /* @__PURE__ */ jsxs16(CoreKeepProvider, { ...providerProps, children: [
3851
- /* @__PURE__ */ jsx22(KeepSyncFeedbackObserver, {}),
4219
+ children: /* @__PURE__ */ jsxs17(CoreKeepProvider, { ...providerProps, children: [
4220
+ /* @__PURE__ */ jsx24(KeepSyncFeedbackObserver, {}),
3852
4221
  children,
3853
- /* @__PURE__ */ jsx22(KeepAnnouncements, {})
4222
+ /* @__PURE__ */ jsx24(KeepAnnouncements, {})
3854
4223
  ] })
3855
4224
  }
3856
4225
  ) });
@@ -3877,7 +4246,7 @@ function createKeepKit(options = {}) {
3877
4246
  } = options;
3878
4247
  const coreKit = createCoreKeepKit(coreOptions);
3879
4248
  return {
3880
- Provider: (props) => /* @__PURE__ */ jsx22(
4249
+ Provider: (props) => /* @__PURE__ */ jsx24(
3881
4250
  KeepKitProvider,
3882
4251
  {
3883
4252
  ...coreOptions,
@@ -3898,9 +4267,9 @@ function createKeepKit(options = {}) {
3898
4267
  ...props
3899
4268
  }
3900
4269
  ),
3901
- Button: (props) => /* @__PURE__ */ jsx22(KeepButton, { ...props }),
3902
- Backup: (props) => /* @__PURE__ */ jsx22(KeepBackup, { ...props }),
3903
- Collection: (props) => /* @__PURE__ */ jsx22(
4270
+ Button: (props) => /* @__PURE__ */ jsx24(KeepButton, { ...props }),
4271
+ Backup: (props) => /* @__PURE__ */ jsx24(KeepBackup, { ...props }),
4272
+ Collection: (props) => /* @__PURE__ */ jsx24(
3904
4273
  KeepCollection,
3905
4274
  {
3906
4275
  ...props,
@@ -3914,6 +4283,7 @@ function createKeepKit(options = {}) {
3914
4283
  useContext: () => coreKit.useContext(),
3915
4284
  useItem: (item) => coreKit.useItem(item),
3916
4285
  useList: (query) => coreKit.useList(query),
4286
+ useNavigator: (navigatorOptions) => coreKit.useNavigator(navigatorOptions),
3917
4287
  useShortcut: (shortcutOptions) => coreKit.useShortcut(shortcutOptions)
3918
4288
  };
3919
4289
  }
@@ -3929,6 +4299,7 @@ export {
3929
4299
  KeepCollection,
3930
4300
  KeepEmptyState,
3931
4301
  KeepErrorBoundary3 as KeepErrorBoundary,
4302
+ KeepHighlight,
3932
4303
  KeepItemCard,
3933
4304
  KeepItemCardSkeleton,
3934
4305
  KeepItemCheckbox,
@@ -3936,10 +4307,12 @@ export {
3936
4307
  KeepKitProvider,
3937
4308
  KeepLayout,
3938
4309
  KeepList,
4310
+ KeepNavigator,
3939
4311
  KeepNoteEditor,
3940
4312
  KeepPagination,
3941
4313
  KeepProvider,
3942
4314
  KeepPruneStaleButton,
4315
+ KeepReorderableList,
3943
4316
  KeepSearchInput,
3944
4317
  KeepSortSelect,
3945
4318
  KeepStaleNotice,
@@ -3949,6 +4322,7 @@ export {
3949
4322
  KeepTagEditor,
3950
4323
  KeepTagFilter,
3951
4324
  KeepThemeProvider,
4325
+ KeepTourBar,
3952
4326
  KeepUiProvider,
3953
4327
  KeepUndo,
3954
4328
  LocalStorageAdapter,
@@ -3960,14 +4334,17 @@ export {
3960
4334
  createNextPagesRouterAdapter,
3961
4335
  createStorageAdapter,
3962
4336
  getKeepLocaleLabels,
4337
+ highlightText,
3963
4338
  isAllSelected,
3964
4339
  keepThemeNames,
3965
4340
  toggleSelectAll,
3966
4341
  useKeepContext8 as useKeepContext,
3967
4342
  useKeepItem5 as useKeepItem,
3968
4343
  useKeepList5 as useKeepList,
4344
+ useKeepNavigator2 as useKeepNavigator,
3969
4345
  useKeepShortcut,
3970
4346
  useKeepToastFeedback,
4347
+ useKeepTourShortcuts,
3971
4348
  useKeepUiLabels,
3972
4349
  useKeepUrlSync
3973
4350
  };