@keepkit/ui 0.16.0 → 0.18.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
@@ -6,32 +6,94 @@ import {
6
6
  createKeepKit as createCoreKeepKit
7
7
  } from "@keepkit/core/react";
8
8
 
9
- // src/hooks/useKeepToastFeedback.ts
10
- import { useCallback } from "react";
11
- function useKeepToastFeedback(showToast) {
12
- return useCallback(
13
- (event) => {
14
- if (!("undo" in event)) {
15
- showToast(event.message);
16
- return;
17
- }
18
- showToast(event.message, {
19
- action: {
20
- label: event.undoLabel,
21
- onClick: () => void event.undo()
22
- }
23
- });
9
+ // src/adapters/url-sync.tsx
10
+ import {
11
+ DEFAULT_KEEP_URL_PARAMS,
12
+ decodeKeepListQuery,
13
+ encodeKeepListQuery
14
+ } from "@keepkit/core/core";
15
+ import { useEffect, useRef } from "react";
16
+ function createNextPagesRouterAdapter(router) {
17
+ const getUrl = () => router.asPath ?? (typeof window === "undefined" ? "/" : window.location.href);
18
+ return {
19
+ getUrl,
20
+ subscribe: router.events ? (listener) => {
21
+ router.events?.on("routeChangeComplete", listener);
22
+ return () => router.events?.off("routeChangeComplete", listener);
23
+ } : void 0,
24
+ navigate: (url, mode) => {
25
+ void router[mode](url, void 0, { shallow: true });
26
+ }
27
+ };
28
+ }
29
+ function useKeepUrlSync({
30
+ enabled = true,
31
+ query,
32
+ onQueryChange,
33
+ options = {},
34
+ adapter: providedAdapter
35
+ }) {
36
+ const browserAdapterRef = useRef(getBrowserAdapter());
37
+ const adapter = providedAdapter ?? browserAdapterRef.current;
38
+ const onQueryChangeRef = useRef(onQueryChange);
39
+ onQueryChangeRef.current = onQueryChange;
40
+ const skipWriteRef = useRef(true);
41
+ const params = options.params;
42
+ useEffect(() => {
43
+ if (!enabled) return;
44
+ const read = () => {
45
+ const url = adapter.getUrl();
46
+ const decoded = decodeKeepListQuery(url, { params });
47
+ skipWriteRef.current = true;
48
+ onQueryChangeRef.current((previousQuery) => ({
49
+ ...previousQuery,
50
+ ...decoded.search ? { search: decoded.search } : { search: void 0 },
51
+ ...decoded.tags ? { tags: decoded.tags } : { tags: void 0 },
52
+ ...decoded.sort ? { sort: decoded.sort } : {},
53
+ ...decoded.pagination ? { pagination: { ...previousQuery.pagination, ...decoded.pagination } } : {}
54
+ }));
55
+ };
56
+ read();
57
+ return adapter.subscribe?.(read);
58
+ }, [adapter, enabled, params]);
59
+ useEffect(() => {
60
+ if (!enabled) return;
61
+ if (skipWriteRef.current) {
62
+ skipWriteRef.current = false;
63
+ return;
64
+ }
65
+ const currentUrl = new URL(adapter.getUrl(), "http://keepkit.invalid");
66
+ const urlParams = { ...DEFAULT_KEEP_URL_PARAMS, ...params };
67
+ for (const key of Object.values(urlParams)) currentUrl.searchParams.delete(key);
68
+ const nextParams = encodeKeepListQuery(query, { params });
69
+ nextParams.forEach((value, key) => {
70
+ currentUrl.searchParams.append(key, value);
71
+ });
72
+ const nextUrl = `${currentUrl.pathname}${currentUrl.search}${currentUrl.hash}`;
73
+ adapter.navigate(nextUrl, options.history ?? "push");
74
+ }, [adapter, enabled, options.history, params, query]);
75
+ }
76
+ function getBrowserAdapter() {
77
+ return {
78
+ getUrl: () => typeof window === "undefined" ? "/" : window.location.href,
79
+ subscribe: (listener) => {
80
+ if (typeof window === "undefined") return () => void 0;
81
+ window.addEventListener("popstate", listener);
82
+ return () => window.removeEventListener("popstate", listener);
24
83
  },
25
- [showToast]
26
- );
84
+ navigate: (url, mode) => {
85
+ if (typeof window === "undefined") return;
86
+ window.history[mode === "push" ? "pushState" : "replaceState"]({}, "", url);
87
+ }
88
+ };
27
89
  }
28
90
 
29
- // src/hooks/useKeepBackup.ts
91
+ // src/features/actions/hooks/useKeepBackup.ts
30
92
  import { useKeepContext } from "@keepkit/core/react";
31
- import { useRef, useState } from "react";
93
+ import { useRef as useRef2, useState } from "react";
32
94
 
33
- // src/ui-context.tsx
34
- import { createContext, useCallback as useCallback2, useContext, useMemo } from "react";
95
+ // src/foundation/ui-context.tsx
96
+ import { createContext, useCallback, useContext, useMemo } from "react";
35
97
 
36
98
  // src/locales/de.ts
37
99
  var DE_LABELS = {
@@ -1356,7 +1418,7 @@ function getKeepLocaleLabels(locale) {
1356
1418
  return { ...KEEP_LOCALE_LABELS[normalizeKeepLocale(locale)] };
1357
1419
  }
1358
1420
 
1359
- // src/ui-context.tsx
1421
+ // src/foundation/ui-context.tsx
1360
1422
  import { jsx } from "react/jsx-runtime";
1361
1423
  var DEFAULT_LABELS = KEEP_LOCALE_LABELS.en;
1362
1424
  var KeepUiLabelsContext = createContext({
@@ -1370,7 +1432,7 @@ function KeepUiProvider({
1370
1432
  onFeedback,
1371
1433
  children
1372
1434
  }) {
1373
- const emitFeedback = useCallback2(
1435
+ const emitFeedback = useCallback(
1374
1436
  (event) => {
1375
1437
  onFeedback?.(event);
1376
1438
  },
@@ -1393,13 +1455,13 @@ function useUiLabel(key, override) {
1393
1455
  }
1394
1456
  function useKeepUiFeedback() {
1395
1457
  const { emitFeedback } = useKeepUiLabels();
1396
- return useCallback2((event) => emitFeedback(event), [emitFeedback]);
1458
+ return useCallback((event) => emitFeedback(event), [emitFeedback]);
1397
1459
  }
1398
1460
 
1399
- // src/hooks/useKeepBackup.ts
1461
+ // src/features/actions/hooks/useKeepBackup.ts
1400
1462
  function useKeepBackup({ filename, onExport, onImported }) {
1401
1463
  const context = useKeepContext();
1402
- const inputRef = useRef(null);
1464
+ const inputRef = useRef2(null);
1403
1465
  const [mode, setMode] = useState("merge");
1404
1466
  const [result, setResult] = useState();
1405
1467
  const [error, setError] = useState();
@@ -1456,7 +1518,7 @@ function useKeepBackup({ filename, onExport, onImported }) {
1456
1518
  };
1457
1519
  }
1458
1520
 
1459
- // src/KeepBackup.tsx
1521
+ // src/features/actions/KeepBackup.tsx
1460
1522
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
1461
1523
  function KeepBackup({
1462
1524
  filename = "keepkit-backup.json",
@@ -1533,16 +1595,52 @@ function getErrorMessage(error) {
1533
1595
  return error instanceof Error ? error.message : "Something went wrong.";
1534
1596
  }
1535
1597
 
1536
- // src/hooks/useKeepBulkActions.ts
1537
- import { useKeepList } from "@keepkit/core/react";
1538
- import { useState as useState2 } from "react";
1539
-
1540
- // src/shared.tsx
1598
+ // src/foundation/shared.tsx
1541
1599
  import {
1542
1600
  cloneElement,
1543
- isValidElement
1601
+ createContext as createContext2,
1602
+ isValidElement,
1603
+ useContext as useContext2
1544
1604
  } from "react";
1545
- import { jsx as jsx3 } from "react/jsx-runtime";
1605
+ import { Fragment, jsx as jsx3 } from "react/jsx-runtime";
1606
+ var KeepSearchQueryContext = createContext2(void 0);
1607
+ function KeepSearchQueryProvider({ query, children }) {
1608
+ return /* @__PURE__ */ jsx3(KeepSearchQueryContext.Provider, { value: query, children });
1609
+ }
1610
+ function useKeepSearchQuery() {
1611
+ return useContext2(KeepSearchQueryContext);
1612
+ }
1613
+ function KeepHighlight({ children, query }) {
1614
+ const contextQuery = useKeepSearchQuery();
1615
+ const resolvedQuery = query ?? contextQuery;
1616
+ if (typeof children !== "string" || !resolvedQuery?.trim()) return children ?? null;
1617
+ return highlightText(children, resolvedQuery);
1618
+ }
1619
+ function highlightText(text, query) {
1620
+ const normalizedQuery = query.trim();
1621
+ if (!normalizedQuery) return text;
1622
+ const matcher = new RegExp(escapeRegExp(normalizedQuery), "gi");
1623
+ const parts = [];
1624
+ let lastIndex = 0;
1625
+ let matchIndex = 0;
1626
+ while (true) {
1627
+ const match = matcher.exec(text);
1628
+ if (match === null) break;
1629
+ const index = match.index;
1630
+ if (index > lastIndex) parts.push(text.slice(lastIndex, index));
1631
+ parts.push(
1632
+ /* @__PURE__ */ jsx3("mark", { className: "keep-highlight", "data-highlight": "true", children: match[0] }, `highlight-${matchIndex}`)
1633
+ );
1634
+ lastIndex = index + match[0].length;
1635
+ matchIndex += 1;
1636
+ }
1637
+ if (lastIndex === 0) return text;
1638
+ if (lastIndex < text.length) parts.push(text.slice(lastIndex));
1639
+ return /* @__PURE__ */ jsx3(Fragment, { children: parts });
1640
+ }
1641
+ function escapeRegExp(value) {
1642
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1643
+ }
1546
1644
  function toKeepButtonItem(item) {
1547
1645
  return {
1548
1646
  id: item.id,
@@ -1574,7 +1672,9 @@ function renderRoot(asChild, child, props, body, componentName) {
1574
1672
  return /* @__PURE__ */ jsx3("div", { ...props, children: body });
1575
1673
  }
1576
1674
 
1577
- // src/hooks/useKeepBulkActions.ts
1675
+ // src/features/actions/hooks/useKeepBulkActions.ts
1676
+ import { useKeepList } from "@keepkit/core/react";
1677
+ import { useState as useState2 } from "react";
1578
1678
  function isAllSelected(items, selectedIds) {
1579
1679
  if (items.length === 0) return false;
1580
1680
  const selected = new Set(selectedIds);
@@ -1667,7 +1767,7 @@ function useKeepBulkActions(options) {
1667
1767
  };
1668
1768
  }
1669
1769
 
1670
- // src/KeepItemCheckbox.tsx
1770
+ // src/features/actions/KeepItemCheckbox.tsx
1671
1771
  import { jsx as jsx4 } from "react/jsx-runtime";
1672
1772
  function KeepItemCheckbox({
1673
1773
  item,
@@ -1700,8 +1800,8 @@ function getItemLabel(item) {
1700
1800
  return typeof title === "string" && title.trim() ? title.trim() : void 0;
1701
1801
  }
1702
1802
 
1703
- // src/KeepBulkActions.tsx
1704
- import { Fragment, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
1803
+ // src/features/actions/KeepBulkActions.tsx
1804
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
1705
1805
  function KeepBulkActions({
1706
1806
  query,
1707
1807
  selectedIds: controlledSelectedIds,
@@ -1724,7 +1824,7 @@ function KeepBulkActions({
1724
1824
  controlledScope,
1725
1825
  onSelectionScopeChange
1726
1826
  });
1727
- const body = render ? render(state) : typeof children === "function" ? children(state) : children ?? /* @__PURE__ */ jsxs2(Fragment, { children: [
1827
+ const body = render ? render(state) : typeof children === "function" ? children(state) : children ?? /* @__PURE__ */ jsxs2(Fragment2, { children: [
1728
1828
  /* @__PURE__ */ jsxs2("fieldset", { children: [
1729
1829
  /* @__PURE__ */ jsx5("legend", { children: labels.selectItems }),
1730
1830
  /* @__PURE__ */ jsxs2("label", { children: [
@@ -1815,13 +1915,13 @@ function KeepBulkActions({
1815
1915
  );
1816
1916
  }
1817
1917
 
1818
- // src/KeepButton.tsx
1918
+ // src/features/actions/KeepButton.tsx
1819
1919
  import {
1820
1920
  KeepButton as CoreKeepButton
1821
1921
  } from "@keepkit/core/react";
1822
- import { createElement, useEffect, useRef as useRef2 } from "react";
1922
+ import { createElement, useEffect as useEffect2, useRef as useRef3 } from "react";
1823
1923
 
1824
- // src/hooks/useKeepButton.ts
1924
+ // src/features/actions/hooks/useKeepButton.ts
1825
1925
  import { useKeepItem } from "@keepkit/core/react";
1826
1926
  function useKeepButton({ item, labels, icons, children }) {
1827
1927
  return {
@@ -1841,8 +1941,8 @@ function useKeepButton({ item, labels, icons, children }) {
1841
1941
  };
1842
1942
  }
1843
1943
 
1844
- // src/KeepButton.tsx
1845
- import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
1944
+ // src/features/actions/KeepButton.tsx
1945
+ import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
1846
1946
  function KeepButton({
1847
1947
  labels,
1848
1948
  icons,
@@ -1854,8 +1954,8 @@ function KeepButton({
1854
1954
  ...props
1855
1955
  }) {
1856
1956
  const view = useKeepButton({ item: props.item, labels, icons, children: props.children });
1857
- const pendingToggle = useRef2(null);
1858
- useEffect(() => {
1957
+ const pendingToggle = useRef3(null);
1958
+ useEffect2(() => {
1859
1959
  const pending = pendingToggle.current;
1860
1960
  if (!pending || view.buttonState.isMutating || pending.wasSaved === view.buttonState.isSaved) return;
1861
1961
  pendingToggle.current = null;
@@ -1887,7 +1987,7 @@ function KeepButton({
1887
1987
  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
1988
  if (!icons) return label;
1889
1989
  const icon = state.error ? icons.error : state.isMutating ? icons.loading : state.isSaved ? icons.remove ?? icons.saved : icons.save;
1890
- return /* @__PURE__ */ jsxs3(Fragment2, { children: [
1990
+ return /* @__PURE__ */ jsxs3(Fragment3, { children: [
1891
1991
  renderIcon(icon, iconClassName),
1892
1992
  showLabel && !iconOnly ? label : null
1893
1993
  ] });
@@ -1915,128 +2015,388 @@ function KeepButton({
1915
2015
  unsavedAriaLabel: labels?.unsavedAriaLabel ?? props.unsavedAriaLabel
1916
2016
  };
1917
2017
  if (!view.customStateLabel) return /* @__PURE__ */ jsx6(CoreKeepButton, { ...sharedProps });
1918
- return /* @__PURE__ */ jsx6(CoreKeepButton, { ...sharedProps, children: (state) => /* @__PURE__ */ jsx6(Fragment2, { children: getStateContent(state) }) });
2018
+ return /* @__PURE__ */ jsx6(CoreKeepButton, { ...sharedProps, children: (state) => /* @__PURE__ */ jsx6(Fragment3, { children: getStateContent(state) }) });
1919
2019
  }
1920
2020
  function renderIcon(icon, className) {
1921
2021
  if (typeof icon === "function") return createElement(icon, { "aria-hidden": true, className });
1922
2022
  return icon ?? null;
1923
2023
  }
1924
2024
 
1925
- // src/KeepCollection.tsx
1926
- import { KeepErrorBoundary as KeepErrorBoundary2 } from "@keepkit/core/react";
1927
-
1928
- // src/hooks/useKeepCollection.ts
1929
- import { useKeepList as useKeepList2 } from "@keepkit/core/react";
1930
- import { useMemo as useMemo2, useState as useState3 } from "react";
1931
-
1932
- // src/url-sync.tsx
1933
- import {
1934
- DEFAULT_KEEP_URL_PARAMS,
1935
- decodeKeepListQuery,
1936
- encodeKeepListQuery
1937
- } from "@keepkit/core/core";
1938
- import { useEffect as useEffect2, useRef as useRef3 } from "react";
1939
- function createNextPagesRouterAdapter(router) {
1940
- const getUrl = () => router.asPath ?? (typeof window === "undefined" ? "/" : window.location.href);
2025
+ // src/features/actions/hooks/useKeepUndo.ts
2026
+ import { useKeepContext as useKeepContext2 } from "@keepkit/core/react";
2027
+ import { useEffect as useEffect3, useState as useState3 } from "react";
2028
+ function useKeepUndo() {
2029
+ const context = useKeepContext2();
2030
+ const emitFeedback = useKeepUiFeedback();
2031
+ const restoredMessage = useUiLabel("restoredMessage");
2032
+ const { canUndo, startedAt, expiresAt } = context.undo;
2033
+ const [now, setNow] = useState3(() => Date.now());
2034
+ useEffect3(() => {
2035
+ if (!canUndo || expiresAt === void 0) return;
2036
+ setNow(Date.now());
2037
+ const timer = setInterval(() => setNow(Date.now()), 250);
2038
+ return () => clearInterval(timer);
2039
+ }, [canUndo, expiresAt]);
2040
+ const duration = Math.max(1, (expiresAt ?? now) - (startedAt ?? now));
2041
+ const remainingMs = Math.max(0, (expiresAt ?? now) - now);
2042
+ const remainingSeconds = Math.max(0, Math.ceil(remainingMs / 1e3));
2043
+ const progress = Math.min(1, Math.max(0, remainingMs / duration));
1941
2044
  return {
1942
- getUrl,
1943
- subscribe: router.events ? (listener) => {
1944
- router.events?.on("routeChangeComplete", listener);
1945
- return () => router.events?.off("routeChangeComplete", listener);
1946
- } : void 0,
1947
- navigate: (url, mode) => {
1948
- void router[mode](url, void 0, { shallow: true });
1949
- }
2045
+ canUndo,
2046
+ expiresAt,
2047
+ remainingMs,
2048
+ remainingSeconds,
2049
+ progress,
2050
+ undo: async () => {
2051
+ const items = context.lastChange?.items ?? (context.lastChange?.item ? [context.lastChange.item] : []);
2052
+ await context.undoLastRemoval();
2053
+ if (items.length > 0) {
2054
+ emitFeedback({ type: "item-restored", item: items[0], items, message: restoredMessage });
2055
+ }
2056
+ },
2057
+ message: useUiLabel("undoAvailable"),
2058
+ label: useUiLabel("undo")
1950
2059
  };
1951
2060
  }
1952
- function useKeepUrlSync({
1953
- enabled = true,
1954
- query,
1955
- onQueryChange,
1956
- options = {},
1957
- adapter: providedAdapter
1958
- }) {
1959
- const browserAdapterRef = useRef3(getBrowserAdapter());
1960
- const adapter = providedAdapter ?? browserAdapterRef.current;
1961
- const onQueryChangeRef = useRef3(onQueryChange);
1962
- onQueryChangeRef.current = onQueryChange;
1963
- const skipWriteRef = useRef3(true);
1964
- const params = options.params;
1965
- useEffect2(() => {
1966
- if (!enabled) return;
1967
- const read = () => {
1968
- const url = adapter.getUrl();
1969
- const decoded = decodeKeepListQuery(url, { params });
1970
- skipWriteRef.current = true;
1971
- onQueryChangeRef.current((previousQuery) => ({
1972
- ...previousQuery,
1973
- ...decoded.search ? { search: decoded.search } : { search: void 0 },
1974
- ...decoded.tags ? { tags: decoded.tags } : { tags: void 0 },
1975
- ...decoded.sort ? { sort: decoded.sort } : {},
1976
- ...decoded.pagination ? { pagination: { ...previousQuery.pagination, ...decoded.pagination } } : {}
1977
- }));
1978
- };
1979
- read();
1980
- return adapter.subscribe?.(read);
1981
- }, [adapter, enabled, params]);
1982
- useEffect2(() => {
1983
- if (!enabled) return;
1984
- if (skipWriteRef.current) {
1985
- skipWriteRef.current = false;
1986
- return;
1987
- }
1988
- const currentUrl = new URL(adapter.getUrl(), "http://keepkit.invalid");
1989
- const urlParams = { ...DEFAULT_KEEP_URL_PARAMS, ...params };
1990
- for (const key of Object.values(urlParams)) currentUrl.searchParams.delete(key);
1991
- const nextParams = encodeKeepListQuery(query, { params });
1992
- nextParams.forEach((value, key) => {
1993
- currentUrl.searchParams.append(key, value);
1994
- });
1995
- const nextUrl = `${currentUrl.pathname}${currentUrl.search}${currentUrl.hash}`;
1996
- adapter.navigate(nextUrl, options.history ?? "push");
1997
- }, [adapter, enabled, options.history, params, query]);
2061
+
2062
+ // src/features/actions/KeepUndo.tsx
2063
+ import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
2064
+ function KeepUndo({ children, label, ...props }) {
2065
+ const view = useKeepUndo();
2066
+ if (!view.canUndo) return null;
2067
+ return /* @__PURE__ */ jsxs4("div", { ...props, role: "status", "aria-live": "polite", "data-keepkit": "undo", "data-state": "available", children: [
2068
+ /* @__PURE__ */ jsx7("span", { "data-keepkit": "undo-message", children: children ?? view.message }),
2069
+ /* @__PURE__ */ jsxs4("span", { "data-keepkit": "undo-countdown", "aria-hidden": "true", children: [
2070
+ view.remainingSeconds,
2071
+ "s"
2072
+ ] }),
2073
+ /* @__PURE__ */ jsx7(
2074
+ "progress",
2075
+ {
2076
+ "data-keepkit": "undo-progress",
2077
+ max: 1,
2078
+ value: view.progress,
2079
+ "aria-label": String(view.label),
2080
+ "aria-valuetext": `${view.remainingSeconds}s`
2081
+ }
2082
+ ),
2083
+ /* @__PURE__ */ jsx7("button", { type: "button", "data-keep-action": "undo", onClick: () => void view.undo(), children: label ?? view.label })
2084
+ ] });
1998
2085
  }
1999
- function getBrowserAdapter() {
2000
- return {
2001
- getUrl: () => typeof window === "undefined" ? "/" : window.location.href,
2002
- subscribe: (listener) => {
2003
- if (typeof window === "undefined") return () => void 0;
2004
- window.addEventListener("popstate", listener);
2005
- return () => window.removeEventListener("popstate", listener);
2086
+
2087
+ // src/features/collection/KeepCollection.tsx
2088
+ import { KeepErrorBoundary as KeepErrorBoundary2 } from "@keepkit/core/react";
2089
+
2090
+ // src/features/query/KeepTagFilter.tsx
2091
+ import { isValidElement as isValidElement2 } from "react";
2092
+
2093
+ // src/features/query/hooks/useKeepTagFilter.ts
2094
+ import { useKeepList as useKeepList2 } from "@keepkit/core/react";
2095
+ import { useCallback as useCallback2, useMemo as useMemo2, useState as useState4 } from "react";
2096
+ function useKeepTagFilter(options) {
2097
+ const { query, controlledValue, defaultValue, onChange, onValueChange } = options;
2098
+ const [uncontrolledValue, setUncontrolledValue] = useState4(defaultValue);
2099
+ const resolvedValue = controlledValue ?? uncontrolledValue;
2100
+ const list = useKeepList2({
2101
+ ...query,
2102
+ tags: resolvedValue ? [...query?.tags ?? [], resolvedValue] : query?.tags
2103
+ });
2104
+ const select = useCallback2(
2105
+ (tag) => {
2106
+ if (controlledValue === void 0) setUncontrolledValue(tag);
2107
+ onChange?.(tag);
2108
+ onValueChange?.(tag);
2006
2109
  },
2007
- navigate: (url, mode) => {
2008
- if (typeof window === "undefined") return;
2009
- window.history[mode === "push" ? "pushState" : "replaceState"]({}, "", url);
2010
- }
2110
+ [controlledValue, onChange, onValueChange]
2111
+ );
2112
+ const state = useMemo2(
2113
+ () => ({ tags: list.tags, tagCounts: list.tagCounts, value: resolvedValue, select }),
2114
+ [list.tagCounts, list.tags, resolvedValue, select]
2115
+ );
2116
+ return {
2117
+ state,
2118
+ isLoading: list.isLoading,
2119
+ labels: { all: useUiLabel("allTags"), aria: useUiLabel("filterTags") }
2011
2120
  };
2012
2121
  }
2013
2122
 
2014
- // src/hooks/useKeepCollection.ts
2015
- function useKeepCollection({
2123
+ // src/features/query/KeepTagFilter.tsx
2124
+ import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
2125
+ function KeepTagFilter({
2016
2126
  query,
2017
- pageSize,
2018
- urlSync,
2019
- urlAdapter,
2020
- features
2127
+ value: controlledValue,
2128
+ defaultValue,
2129
+ onChange,
2130
+ onValueChange,
2131
+ allLabel,
2132
+ ariaLabel,
2133
+ renderTag,
2134
+ render,
2135
+ children,
2136
+ asChild = false,
2137
+ className,
2138
+ ...rootProps
2021
2139
  }) {
2022
- const enabled = {
2023
- search: true,
2024
- sort: true,
2025
- pagination: true,
2026
- tagFilter: false,
2027
- bulkActions: false,
2028
- ...features
2029
- };
2030
- const [searchValue, setSearchValue] = useState3(query.search?.query ?? "");
2031
- const [sort, setSort] = useState3(query.sort ?? { by: "updatedAt", direction: "desc" });
2032
- const [tag, setTag] = useState3(query.tags?.[0]);
2033
- const [page, setPage] = useState3(query.pagination?.page ?? 1);
2034
- const resolvedPageSize = query.pagination?.pageSize ?? pageSize;
2035
- const resolvedQuery = useMemo2(
2036
- () => ({
2037
- ...query,
2038
- search: enabled.search ? { ...query.search, query: searchValue } : query.search,
2039
- sort: enabled.sort ? sort : query.sort,
2140
+ const view = useKeepTagFilter({ query, controlledValue, defaultValue, onChange, onValueChange });
2141
+ const contentChildren = asChild && isValidElement2(children) ? void 0 : children;
2142
+ const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? /* @__PURE__ */ jsxs5("fieldset", { children: [
2143
+ /* @__PURE__ */ jsx8("legend", { children: ariaLabel ?? view.labels.aria }),
2144
+ /* @__PURE__ */ jsx8(
2145
+ "button",
2146
+ {
2147
+ type: "button",
2148
+ "data-keep-action": "filter-all-tags",
2149
+ "aria-pressed": view.state.value === void 0,
2150
+ onClick: () => view.state.select(),
2151
+ children: allLabel ?? view.labels.all
2152
+ }
2153
+ ),
2154
+ view.state.tags.map((tag) => /* @__PURE__ */ jsxs5(
2155
+ "button",
2156
+ {
2157
+ type: "button",
2158
+ "data-keep-action": "filter-tag",
2159
+ "aria-pressed": view.state.value === tag,
2160
+ onClick: () => view.state.select(tag),
2161
+ children: [
2162
+ renderTag ? renderTag(tag, view.state.tagCounts[tag] ?? 0, view.state.value === tag) : tag,
2163
+ /* @__PURE__ */ jsxs5("span", { children: [
2164
+ " (",
2165
+ view.state.tagCounts[tag] ?? 0,
2166
+ ")"
2167
+ ] })
2168
+ ]
2169
+ },
2170
+ tag
2171
+ ))
2172
+ ] });
2173
+ return renderRoot(
2174
+ asChild,
2175
+ isValidElement2(children) ? children : void 0,
2176
+ {
2177
+ ...rootProps,
2178
+ className,
2179
+ "data-keepkit": "tag-filter",
2180
+ "data-state": view.state.value === void 0 ? "all" : "filtered",
2181
+ "data-loading": view.isLoading ? "true" : void 0
2182
+ },
2183
+ body,
2184
+ "KeepTagFilter"
2185
+ );
2186
+ }
2187
+
2188
+ // src/features/query/hooks/useQueryControls.ts
2189
+ import { useEffect as useEffect4, useState as useState5 } from "react";
2190
+ function useKeepSearchInput(options) {
2191
+ const { controlledValue, defaultValue, debounceMs, onValueChange } = options;
2192
+ const [uncontrolledValue, setUncontrolledValue] = useState5(defaultValue);
2193
+ const value = controlledValue ?? uncontrolledValue;
2194
+ useEffect4(() => {
2195
+ if (!onValueChange) return;
2196
+ if (debounceMs <= 0) {
2197
+ onValueChange(value);
2198
+ return;
2199
+ }
2200
+ const timer = window.setTimeout(() => onValueChange(value), debounceMs);
2201
+ return () => window.clearTimeout(timer);
2202
+ }, [debounceMs, onValueChange, value]);
2203
+ return {
2204
+ value,
2205
+ label: useUiLabel("search"),
2206
+ change: (event) => {
2207
+ if (controlledValue === void 0) setUncontrolledValue(event.currentTarget.value);
2208
+ }
2209
+ };
2210
+ }
2211
+ function useKeepSortSelect(options) {
2212
+ const { controlledValue, defaultValue, onValueChange } = options;
2213
+ const [uncontrolledValue, setUncontrolledValue] = useState5(defaultValue);
2214
+ const value = controlledValue ?? uncontrolledValue;
2215
+ return {
2216
+ value,
2217
+ change: (event) => {
2218
+ const nextValue = event.currentTarget.value;
2219
+ if (controlledValue === void 0) setUncontrolledValue(nextValue);
2220
+ const [by, direction] = nextValue.split(":");
2221
+ onValueChange?.(nextValue, { by, direction });
2222
+ },
2223
+ labels: {
2224
+ sort: useUiLabel("sort"),
2225
+ updatedNewest: useUiLabel("updatedNewest"),
2226
+ updatedOldest: useUiLabel("updatedOldest"),
2227
+ savedNewest: useUiLabel("savedNewest"),
2228
+ savedOldest: useUiLabel("savedOldest")
2229
+ }
2230
+ };
2231
+ }
2232
+ function useKeepPagination(options) {
2233
+ const { totalCount, pageSize, page, maxPageButtons, onPageChange } = options;
2234
+ const pageCount = Math.max(1, Math.ceil(totalCount / Math.max(1, pageSize)));
2235
+ const currentPage = Math.min(Math.max(1, page), pageCount);
2236
+ const goToPage = (nextPage) => {
2237
+ const next = Math.min(Math.max(1, nextPage), pageCount);
2238
+ onPageChange?.(next, (next - 1) * pageSize);
2239
+ };
2240
+ return {
2241
+ pageCount,
2242
+ currentPage,
2243
+ goToPage,
2244
+ visiblePages: getVisiblePages(currentPage, pageCount, Math.max(1, maxPageButtons)),
2245
+ labels: {
2246
+ previous: useUiLabel("previousPage"),
2247
+ next: useUiLabel("nextPage"),
2248
+ page: useUiLabel("page"),
2249
+ pagination: useUiLabel("pagination")
2250
+ }
2251
+ };
2252
+ }
2253
+ function getVisiblePages(currentPage, pageCount, maxPageButtons) {
2254
+ if (pageCount <= maxPageButtons) return Array.from({ length: pageCount }, (_, index) => index + 1);
2255
+ const half = Math.floor(maxPageButtons / 2);
2256
+ const start = Math.min(Math.max(1, currentPage - half), pageCount - maxPageButtons + 1);
2257
+ return Array.from({ length: maxPageButtons }, (_, index) => start + index);
2258
+ }
2259
+
2260
+ // src/features/query/query-controls.tsx
2261
+ import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
2262
+ function KeepSearchInput({
2263
+ value: controlledValue,
2264
+ defaultValue = "",
2265
+ debounceMs = 300,
2266
+ onValueChange,
2267
+ "aria-label": ariaLabel,
2268
+ placeholder,
2269
+ ...props
2270
+ }) {
2271
+ const view = useKeepSearchInput({ controlledValue, defaultValue, debounceMs, onValueChange });
2272
+ return /* @__PURE__ */ jsx9(
2273
+ "input",
2274
+ {
2275
+ ...props,
2276
+ "data-keepkit": "search-input",
2277
+ "data-keep-action": "search",
2278
+ type: "search",
2279
+ value: view.value,
2280
+ "data-state": view.value ? "active" : "idle",
2281
+ "data-disabled": props.disabled ? "true" : void 0,
2282
+ "aria-label": ariaLabel ?? view.label,
2283
+ placeholder: placeholder ?? view.label,
2284
+ onChange: view.change
2285
+ }
2286
+ );
2287
+ }
2288
+ function KeepSortSelect({
2289
+ value: controlledValue,
2290
+ defaultValue = "updatedAt:desc",
2291
+ onValueChange,
2292
+ "aria-label": ariaLabel,
2293
+ children,
2294
+ ...props
2295
+ }) {
2296
+ const view = useKeepSortSelect({ controlledValue, defaultValue, onValueChange });
2297
+ const options = children ?? /* @__PURE__ */ jsxs6(Fragment4, { children: [
2298
+ /* @__PURE__ */ jsx9("option", { value: "updatedAt:desc", children: view.labels.updatedNewest }),
2299
+ /* @__PURE__ */ jsx9("option", { value: "updatedAt:asc", children: view.labels.updatedOldest }),
2300
+ /* @__PURE__ */ jsx9("option", { value: "savedAt:desc", children: view.labels.savedNewest }),
2301
+ /* @__PURE__ */ jsx9("option", { value: "savedAt:asc", children: view.labels.savedOldest })
2302
+ ] });
2303
+ return /* @__PURE__ */ jsx9(
2304
+ "select",
2305
+ {
2306
+ ...props,
2307
+ "data-keepkit": "sort-select",
2308
+ "data-keep-action": "sort",
2309
+ value: view.value,
2310
+ "data-state": "selected",
2311
+ "data-disabled": props.disabled ? "true" : void 0,
2312
+ "aria-label": ariaLabel ?? view.labels.sort,
2313
+ onChange: view.change,
2314
+ children: options
2315
+ }
2316
+ );
2317
+ }
2318
+ function KeepPagination({
2319
+ totalCount,
2320
+ pageSize,
2321
+ page = 1,
2322
+ maxPageButtons = 7,
2323
+ onPageChange,
2324
+ render,
2325
+ ...props
2326
+ }) {
2327
+ const view = useKeepPagination({ totalCount, pageSize, page, maxPageButtons, onPageChange });
2328
+ const navProps = {
2329
+ ...props,
2330
+ "data-keepkit": "pagination",
2331
+ "aria-label": props["aria-label"] ?? view.labels.pagination,
2332
+ "data-state": view.pageCount > 1 ? "active" : "idle"
2333
+ };
2334
+ if (render)
2335
+ return /* @__PURE__ */ jsx9("nav", { ...navProps, children: render({ page: view.currentPage, pageCount: view.pageCount, goToPage: view.goToPage }) });
2336
+ return /* @__PURE__ */ jsxs6("nav", { ...navProps, children: [
2337
+ /* @__PURE__ */ jsx9(
2338
+ "button",
2339
+ {
2340
+ type: "button",
2341
+ "data-keep-action": "previous-page",
2342
+ onClick: () => view.goToPage(view.currentPage - 1),
2343
+ disabled: view.currentPage <= 1,
2344
+ children: view.labels.previous
2345
+ }
2346
+ ),
2347
+ view.visiblePages.map((nextPage) => /* @__PURE__ */ jsx9(
2348
+ "button",
2349
+ {
2350
+ type: "button",
2351
+ "data-keep-action": "select-page",
2352
+ "aria-current": nextPage === view.currentPage ? "page" : void 0,
2353
+ "aria-label": `${view.labels.page} ${nextPage}`,
2354
+ onClick: () => view.goToPage(nextPage),
2355
+ children: nextPage
2356
+ },
2357
+ nextPage
2358
+ )),
2359
+ /* @__PURE__ */ jsx9(
2360
+ "button",
2361
+ {
2362
+ type: "button",
2363
+ "data-keep-action": "next-page",
2364
+ onClick: () => view.goToPage(view.currentPage + 1),
2365
+ disabled: view.currentPage >= view.pageCount,
2366
+ children: view.labels.next
2367
+ }
2368
+ )
2369
+ ] });
2370
+ }
2371
+
2372
+ // src/features/collection/hooks/useKeepCollection.ts
2373
+ import { useKeepList as useKeepList3 } from "@keepkit/core/react";
2374
+ import { useMemo as useMemo3, useState as useState6 } from "react";
2375
+ function useKeepCollection({
2376
+ query,
2377
+ pageSize,
2378
+ urlSync,
2379
+ urlAdapter,
2380
+ features
2381
+ }) {
2382
+ const enabled = {
2383
+ search: true,
2384
+ sort: true,
2385
+ pagination: true,
2386
+ tagFilter: false,
2387
+ bulkActions: false,
2388
+ ...features
2389
+ };
2390
+ const [searchValue, setSearchValue] = useState6(query.search?.query ?? "");
2391
+ const [sort, setSort] = useState6(query.sort ?? { by: "updatedAt", direction: "desc" });
2392
+ const [tag, setTag] = useState6(query.tags?.[0]);
2393
+ const [page, setPage] = useState6(query.pagination?.page ?? 1);
2394
+ const resolvedPageSize = query.pagination?.pageSize ?? pageSize;
2395
+ const resolvedQuery = useMemo3(
2396
+ () => ({
2397
+ ...query,
2398
+ search: enabled.search ? { ...query.search, query: searchValue } : query.search,
2399
+ sort: enabled.sort ? sort : query.sort,
2040
2400
  tags: tag ? [.../* @__PURE__ */ new Set([...query.tags ?? [], tag])] : query.tags,
2041
2401
  pagination: enabled.pagination ? { ...query.pagination, page, pageSize: resolvedPageSize } : query.pagination
2042
2402
  }),
@@ -2055,7 +2415,7 @@ function useKeepCollection({
2055
2415
  options: typeof urlSync === "object" ? urlSync : {},
2056
2416
  adapter: urlAdapter
2057
2417
  });
2058
- const list = useKeepList2(resolvedQuery);
2418
+ const list = useKeepList3(resolvedQuery);
2059
2419
  return {
2060
2420
  enabled,
2061
2421
  searchValue,
@@ -2080,108 +2440,38 @@ function useKeepCollection({
2080
2440
  };
2081
2441
  }
2082
2442
 
2083
- // src/KeepList.tsx
2443
+ // src/features/collection/KeepList.tsx
2084
2444
  import { KeepErrorBoundary } from "@keepkit/core/react";
2085
- import { isValidElement as isValidElement3 } from "react";
2086
-
2087
- // src/hooks/useKeepListView.ts
2088
- import { useKeepList as useKeepList3 } from "@keepkit/core/react";
2089
- function useKeepListView(query) {
2090
- return {
2091
- state: useKeepList3(query),
2092
- labels: {
2093
- loading: useUiLabel("loadingItems"),
2094
- empty: useUiLabel("noItems"),
2095
- error: useUiLabel("errorItems")
2096
- }
2097
- };
2098
- }
2445
+ import { isValidElement as isValidElement4 } from "react";
2099
2446
 
2100
- // src/KeepItemCard.tsx
2447
+ // src/features/item/KeepItemCard.tsx
2101
2448
  import {
2102
- createContext as createContext2,
2449
+ createContext as createContext3,
2103
2450
  createElement as createElement2,
2104
- isValidElement as isValidElement2,
2105
- useContext as useContext2
2451
+ isValidElement as isValidElement3,
2452
+ useContext as useContext3,
2453
+ useEffect as useEffect5,
2454
+ useState as useState8
2106
2455
  } from "react";
2107
2456
 
2108
- // src/hooks/useKeepItemCard.ts
2109
- import { useKeepItem as useKeepItem2 } from "@keepkit/core/react";
2110
- function useKeepItemCard(options) {
2111
- const {
2112
- item,
2113
- title,
2114
- getTitle,
2115
- getImageProps,
2116
- href: hrefOption,
2117
- linkTargetAttribute,
2118
- linkRel,
2119
- onRemoveError,
2120
- onRemoved
2121
- } = options;
2122
- const itemState = useKeepItem2(item);
2123
- const emitFeedback = useKeepUiFeedback();
2124
- const removedMessage = useUiLabel("removedMessage");
2125
- const restoredMessage = useUiLabel("restoredMessage");
2126
- const undoLabel = useUiLabel("undo");
2127
- const resolvedTitle = typeof title === "function" ? title(item) : title ?? getTitle?.(item) ?? getMetaTitle(item.meta) ?? item.id;
2128
- const imageProps = getImageProps?.(item, resolvedTitle);
2129
- const href = typeof hrefOption === "function" ? hrefOption(item) : hrefOption;
2130
- const isAvailable = item.status === void 0 || item.status === "available";
2131
- const isExternalLink = href ? /^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(href) : false;
2132
- const statusLabelKey = item.status && item.status !== "available" ? getStatusLabelKey(item.status) : "statusUnknown";
2133
- const unavailableLabel = useUiLabel(statusLabelKey);
2134
- const remove = async () => {
2135
- const wasSaved = itemState.isSaved;
2136
- try {
2137
- await itemState.removeWithUndo();
2138
- onRemoved?.(item);
2139
- if (!wasSaved) return;
2140
- emitFeedback({
2141
- type: "item-removed",
2142
- item,
2143
- message: removedMessage,
2144
- undoLabel,
2145
- undo: async () => {
2146
- await itemState.undo();
2147
- emitFeedback({ type: "item-restored", item, items: [item], message: restoredMessage });
2148
- }
2149
- });
2150
- } catch (cause) {
2151
- onRemoveError?.(cause);
2152
- }
2153
- };
2154
- const state = {
2155
- item,
2156
- isSaved: itemState.isSaved,
2157
- isMutating: itemState.isMutating,
2158
- error: itemState.error,
2159
- remove,
2160
- status: item.status
2161
- };
2457
+ // src/features/item/hooks/useKeepItemStatusBadge.ts
2458
+ function useKeepItemStatusBadge(status) {
2162
2459
  return {
2163
- itemState,
2164
- state,
2165
- resolvedTitle,
2166
- imageProps,
2167
- href,
2168
- isAvailable,
2169
- displayStatus: getDisplayStatus(item.status),
2170
- resolvedLinkTarget: linkTargetAttribute ?? (isExternalLink ? "_blank" : void 0),
2171
- resolvedLinkRel: linkRel ?? (isExternalLink ? "noreferrer" : void 0),
2172
- statusLabel: item.status && item.status !== "available" ? unavailableLabel : void 0,
2173
- remove,
2174
- labels: {
2175
- save: useUiLabel("save"),
2176
- savedAt: useUiLabel("saved"),
2177
- error: useUiLabel("error"),
2178
- remove: useUiLabel("remove"),
2179
- tags: useUiLabel("tags")
2180
- }
2460
+ resolvedStatus: getDisplayStatus(status),
2461
+ statusLabel: useUiLabel(getStatusLabelKey(status)),
2462
+ icon: getStatusIcon(status)
2181
2463
  };
2182
2464
  }
2465
+ function getStatusIcon(status) {
2466
+ if (status === "available") return "check";
2467
+ if (status === "expired") return "clock";
2468
+ if (status === "removed" || status === "deleted") return "ban";
2469
+ return "lock";
2470
+ }
2183
2471
  function getStatusLabelKey(status) {
2184
2472
  switch (status) {
2473
+ case "available":
2474
+ return "statusAvailable";
2185
2475
  case "expired":
2186
2476
  return "statusExpired";
2187
2477
  case "removed":
@@ -2190,28 +2480,85 @@ function getStatusLabelKey(status) {
2190
2480
  return "statusDeleted";
2191
2481
  case "private":
2192
2482
  return "statusPrivate";
2193
- default:
2483
+ case "unknown":
2194
2484
  return "statusUnknown";
2485
+ case "restricted":
2486
+ return "statusPrivate";
2195
2487
  }
2196
2488
  }
2197
2489
  function getDisplayStatus(status) {
2198
- if (status === void 0 || status === "available") return "available";
2490
+ if (status === "available") return "available";
2199
2491
  if (status === "expired") return "expired";
2200
- if (status === "removed") return "removed";
2492
+ if (status === "removed" || status === "deleted") return "removed";
2201
2493
  return "restricted";
2202
2494
  }
2203
2495
 
2204
- // src/hooks/useKeepStaleNotice.ts
2205
- import { useKeepContext as useKeepContext2 } from "@keepkit/core/react";
2206
- import { useState as useState4 } from "react";
2496
+ // src/features/item/KeepItemStatusBadge.tsx
2497
+ import { Fragment as Fragment5, jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
2498
+ function KeepItemStatusBadge({ status = "available", label, className, ...props }) {
2499
+ const view = useKeepItemStatusBadge(status);
2500
+ return /* @__PURE__ */ jsxs7(
2501
+ "span",
2502
+ {
2503
+ ...props,
2504
+ className,
2505
+ role: "img",
2506
+ "aria-label": props["aria-label"] ?? view.statusLabel,
2507
+ "data-keepkit": "status-badge",
2508
+ "data-status": status,
2509
+ "data-item-status": view.resolvedStatus,
2510
+ children: [
2511
+ /* @__PURE__ */ jsx10(StatusIcon, { name: view.icon }),
2512
+ /* @__PURE__ */ jsx10("span", { "data-status-label": "true", children: label ?? view.statusLabel })
2513
+ ]
2514
+ }
2515
+ );
2516
+ }
2517
+ function StatusIcon({ name }) {
2518
+ return /* @__PURE__ */ jsxs7(
2519
+ "svg",
2520
+ {
2521
+ "data-status-icon": name,
2522
+ viewBox: "0 0 24 24",
2523
+ width: "1em",
2524
+ height: "1em",
2525
+ fill: "none",
2526
+ stroke: "currentColor",
2527
+ strokeWidth: "2",
2528
+ strokeLinecap: "round",
2529
+ strokeLinejoin: "round",
2530
+ "aria-hidden": "true",
2531
+ focusable: "false",
2532
+ children: [
2533
+ name === "check" ? /* @__PURE__ */ jsx10("path", { d: "m5 12 4 4L19 6" }) : null,
2534
+ name === "clock" ? /* @__PURE__ */ jsxs7(Fragment5, { children: [
2535
+ /* @__PURE__ */ jsx10("circle", { cx: "12", cy: "12", r: "8" }),
2536
+ /* @__PURE__ */ jsx10("path", { d: "M12 7v5l3 2" })
2537
+ ] }) : null,
2538
+ name === "ban" ? /* @__PURE__ */ jsxs7(Fragment5, { children: [
2539
+ /* @__PURE__ */ jsx10("circle", { cx: "12", cy: "12", r: "8" }),
2540
+ /* @__PURE__ */ jsx10("path", { d: "m6.5 6.5 11 11" })
2541
+ ] }) : null,
2542
+ name === "lock" ? /* @__PURE__ */ jsxs7(Fragment5, { children: [
2543
+ /* @__PURE__ */ jsx10("rect", { x: "5", y: "10", width: "14", height: "10", rx: "2" }),
2544
+ /* @__PURE__ */ jsx10("path", { d: "M8 10V7a4 4 0 0 1 8 0v3" })
2545
+ ] }) : null
2546
+ ]
2547
+ }
2548
+ );
2549
+ }
2550
+
2551
+ // src/features/status/hooks/useKeepStaleNotice.ts
2552
+ import { useKeepContext as useKeepContext3 } from "@keepkit/core/react";
2553
+ import { useState as useState7 } from "react";
2207
2554
  function useKeepStaleNotice({ item, onRetry, onRemoved }) {
2208
- const context = useKeepContext2();
2555
+ const context = useKeepContext3();
2209
2556
  const emitFeedback = useKeepUiFeedback();
2210
2557
  const removedMessage = useUiLabel("removedMessage");
2211
2558
  const restoredMessage = useUiLabel("restoredMessage");
2212
2559
  const undoLabel = useUiLabel("undo");
2213
- const [isRetrying, setIsRetrying] = useState4(false);
2214
- const [error, setError] = useState4(null);
2560
+ const [isRetrying, setIsRetrying] = useState7(false);
2561
+ const [error, setError] = useState7(null);
2215
2562
  async function retry() {
2216
2563
  setError(null);
2217
2564
  setIsRetrying(true);
@@ -2257,7 +2604,7 @@ function useKeepStaleNotice({ item, onRetry, onRemoved }) {
2257
2604
  };
2258
2605
  }
2259
2606
  function useKeepPruneStale({ statuses, onPruned }) {
2260
- const context = useKeepContext2();
2607
+ const context = useKeepContext3();
2261
2608
  const emitFeedback = useKeepUiFeedback();
2262
2609
  const staleItems = context.items.filter((item) => item.status && statuses.includes(item.status));
2263
2610
  const staleIds = staleItems.map((item) => item.id);
@@ -2290,54 +2637,8 @@ function useKeepPruneStale({ statuses, onPruned }) {
2290
2637
  };
2291
2638
  }
2292
2639
 
2293
- // src/hooks/useKeepItemStatusBadge.ts
2294
- function useKeepItemStatusBadge(status) {
2295
- return { resolvedStatus: getDisplayStatus2(status), statusLabel: useUiLabel(getStatusLabelKey2(status)) };
2296
- }
2297
- function getStatusLabelKey2(status) {
2298
- switch (status) {
2299
- case "available":
2300
- return "statusAvailable";
2301
- case "expired":
2302
- return "statusExpired";
2303
- case "removed":
2304
- return "statusRemoved";
2305
- case "deleted":
2306
- return "statusDeleted";
2307
- case "private":
2308
- return "statusPrivate";
2309
- case "unknown":
2310
- return "statusUnknown";
2311
- case "restricted":
2312
- return "statusPrivate";
2313
- }
2314
- }
2315
- function getDisplayStatus2(status) {
2316
- if (status === "available") return "available";
2317
- if (status === "expired") return "expired";
2318
- if (status === "removed") return "removed";
2319
- return "restricted";
2320
- }
2321
-
2322
- // src/KeepItemStatusBadge.tsx
2323
- import { jsx as jsx7 } from "react/jsx-runtime";
2324
- function KeepItemStatusBadge({ status = "available", label, className, ...props }) {
2325
- const view = useKeepItemStatusBadge(status);
2326
- return /* @__PURE__ */ jsx7(
2327
- "span",
2328
- {
2329
- ...props,
2330
- className,
2331
- "data-keepkit": "status-badge",
2332
- "data-status": status,
2333
- "data-item-status": view.resolvedStatus,
2334
- children: label ?? view.statusLabel
2335
- }
2336
- );
2337
- }
2338
-
2339
- // src/KeepStaleNotice.tsx
2340
- import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
2640
+ // src/features/status/KeepStaleNotice.tsx
2641
+ import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
2341
2642
  function KeepStaleNotice({
2342
2643
  item,
2343
2644
  onRetry,
@@ -2349,11 +2650,11 @@ function KeepStaleNotice({
2349
2650
  ...props
2350
2651
  }) {
2351
2652
  const view = useKeepStaleNotice({ item, onRetry, onRemoved });
2352
- return /* @__PURE__ */ jsxs4("aside", { ...props, className, "data-keepkit": "stale-notice", "data-state": view.error ? "error" : "stale", children: [
2353
- /* @__PURE__ */ jsx8(KeepItemStatusBadge, { status: view.status }),
2354
- children ?? (item.statusReason ? /* @__PURE__ */ jsx8("p", { children: item.statusReason }) : null),
2355
- /* @__PURE__ */ jsxs4("div", { children: [
2356
- /* @__PURE__ */ jsx8(
2653
+ return /* @__PURE__ */ jsxs8("aside", { ...props, className, "data-keepkit": "stale-notice", "data-state": view.error ? "error" : "stale", children: [
2654
+ /* @__PURE__ */ jsx11(KeepItemStatusBadge, { status: view.status }),
2655
+ children ?? (item.statusReason ? /* @__PURE__ */ jsx11("p", { children: item.statusReason }) : null),
2656
+ /* @__PURE__ */ jsxs8("div", { children: [
2657
+ /* @__PURE__ */ jsx11(
2357
2658
  "button",
2358
2659
  {
2359
2660
  type: "button",
@@ -2363,7 +2664,7 @@ function KeepStaleNotice({
2363
2664
  children: retryLabel ?? view.labels.retry
2364
2665
  }
2365
2666
  ),
2366
- /* @__PURE__ */ jsx8(
2667
+ /* @__PURE__ */ jsx11(
2367
2668
  "button",
2368
2669
  {
2369
2670
  type: "button",
@@ -2374,7 +2675,7 @@ function KeepStaleNotice({
2374
2675
  }
2375
2676
  )
2376
2677
  ] }),
2377
- view.error ? /* @__PURE__ */ jsx8("p", { role: "alert", children: view.error instanceof Error ? view.error.message : view.labels.error }) : null
2678
+ view.error ? /* @__PURE__ */ jsx11("p", { role: "alert", children: view.error instanceof Error ? view.error.message : view.labels.error }) : null
2378
2679
  ] });
2379
2680
  }
2380
2681
  function KeepPruneStaleButton({
@@ -2386,7 +2687,7 @@ function KeepPruneStaleButton({
2386
2687
  ...props
2387
2688
  }) {
2388
2689
  const view = useKeepPruneStale({ statuses, onPruned });
2389
- return /* @__PURE__ */ jsx8(
2690
+ return /* @__PURE__ */ jsx11(
2390
2691
  "button",
2391
2692
  {
2392
2693
  ...props,
@@ -2402,11 +2703,107 @@ function KeepPruneStaleButton({
2402
2703
  );
2403
2704
  }
2404
2705
 
2405
- // src/KeepItemCard.tsx
2406
- import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
2407
- var KeepItemCardContext = createContext2(null);
2706
+ // src/features/item/hooks/useKeepItemCard.ts
2707
+ import { useKeepItem as useKeepItem2 } from "@keepkit/core/react";
2708
+ function useKeepItemCard(options) {
2709
+ const {
2710
+ item,
2711
+ title,
2712
+ getTitle,
2713
+ getImageProps,
2714
+ href: hrefOption,
2715
+ linkTargetAttribute,
2716
+ linkRel,
2717
+ onRemoveError,
2718
+ onRemoved
2719
+ } = options;
2720
+ const itemState = useKeepItem2(item);
2721
+ const emitFeedback = useKeepUiFeedback();
2722
+ const removedMessage = useUiLabel("removedMessage");
2723
+ const restoredMessage = useUiLabel("restoredMessage");
2724
+ const undoLabel = useUiLabel("undo");
2725
+ const resolvedTitle = typeof title === "function" ? title(item) : title ?? getTitle?.(item) ?? getMetaTitle(item.meta) ?? item.id;
2726
+ const imageProps = getImageProps?.(item, resolvedTitle);
2727
+ const href = typeof hrefOption === "function" ? hrefOption(item) : hrefOption;
2728
+ const isAvailable = item.status === void 0 || item.status === "available";
2729
+ const isExternalLink = href ? /^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(href) : false;
2730
+ const statusLabelKey = item.status && item.status !== "available" ? getStatusLabelKey2(item.status) : "statusUnknown";
2731
+ const unavailableLabel = useUiLabel(statusLabelKey);
2732
+ const remove = async () => {
2733
+ const wasSaved = itemState.isSaved;
2734
+ try {
2735
+ await itemState.removeWithUndo();
2736
+ onRemoved?.(item);
2737
+ if (!wasSaved) return;
2738
+ emitFeedback({
2739
+ type: "item-removed",
2740
+ item,
2741
+ message: removedMessage,
2742
+ undoLabel,
2743
+ undo: async () => {
2744
+ await itemState.undo();
2745
+ emitFeedback({ type: "item-restored", item, items: [item], message: restoredMessage });
2746
+ }
2747
+ });
2748
+ } catch (cause) {
2749
+ onRemoveError?.(cause);
2750
+ }
2751
+ };
2752
+ const state = {
2753
+ item,
2754
+ isSaved: itemState.isSaved,
2755
+ isMutating: itemState.isMutating,
2756
+ error: itemState.error,
2757
+ remove,
2758
+ status: item.status
2759
+ };
2760
+ return {
2761
+ itemState,
2762
+ state,
2763
+ resolvedTitle,
2764
+ imageProps,
2765
+ href,
2766
+ isAvailable,
2767
+ displayStatus: getDisplayStatus2(item.status),
2768
+ resolvedLinkTarget: linkTargetAttribute ?? (isExternalLink ? "_blank" : void 0),
2769
+ resolvedLinkRel: linkRel ?? (isExternalLink ? "noreferrer" : void 0),
2770
+ statusLabel: item.status && item.status !== "available" ? unavailableLabel : void 0,
2771
+ remove,
2772
+ labels: {
2773
+ save: useUiLabel("save"),
2774
+ savedAt: useUiLabel("saved"),
2775
+ error: useUiLabel("error"),
2776
+ remove: useUiLabel("remove"),
2777
+ tags: useUiLabel("tags")
2778
+ }
2779
+ };
2780
+ }
2781
+ function getStatusLabelKey2(status) {
2782
+ switch (status) {
2783
+ case "expired":
2784
+ return "statusExpired";
2785
+ case "removed":
2786
+ return "statusRemoved";
2787
+ case "deleted":
2788
+ return "statusDeleted";
2789
+ case "private":
2790
+ return "statusPrivate";
2791
+ default:
2792
+ return "statusUnknown";
2793
+ }
2794
+ }
2795
+ function getDisplayStatus2(status) {
2796
+ if (status === void 0 || status === "available") return "available";
2797
+ if (status === "expired") return "expired";
2798
+ if (status === "removed") return "removed";
2799
+ return "restricted";
2800
+ }
2801
+
2802
+ // src/features/item/KeepItemCard.tsx
2803
+ import { Fragment as Fragment6, jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
2804
+ var KeepItemCardContext = createContext3(null);
2408
2805
  function useKeepItemCardCompound(part) {
2409
- const context = useContext2(KeepItemCardContext);
2806
+ const context = useContext3(KeepItemCardContext);
2410
2807
  if (!context) throw new Error(`KeepItemCard.${part} must be rendered inside KeepItemCard.`);
2411
2808
  return context;
2412
2809
  }
@@ -2436,6 +2833,7 @@ function KeepItemCardRoot({
2436
2833
  linkComponent: LinkComponent,
2437
2834
  linkTargetAttribute,
2438
2835
  linkRel,
2836
+ highlightQuery,
2439
2837
  className,
2440
2838
  ...rootProps
2441
2839
  }) {
@@ -2450,7 +2848,9 @@ function KeepItemCardRoot({
2450
2848
  onRemoveError,
2451
2849
  onRemoved
2452
2850
  });
2453
- const contentChildren = asChild && isValidElement2(children) ? void 0 : children;
2851
+ const contextQuery = useKeepSearchQuery();
2852
+ const searchQuery = highlightQuery ?? contextQuery;
2853
+ const contentChildren = asChild && isValidElement3(children) ? void 0 : children;
2454
2854
  function renderLink(content) {
2455
2855
  if (!view.href || !view.isAvailable) return content;
2456
2856
  const linkProps = {
@@ -2460,28 +2860,49 @@ function KeepItemCardRoot({
2460
2860
  onClick: (event) => onOpen?.(item, event),
2461
2861
  children: content
2462
2862
  };
2463
- return LinkComponent ? /* @__PURE__ */ jsx9(LinkComponent, { ...linkProps }) : /* @__PURE__ */ jsx9("a", { ...linkProps });
2863
+ return LinkComponent ? /* @__PURE__ */ jsx12(LinkComponent, { ...linkProps }) : /* @__PURE__ */ jsx12("a", { ...linkProps });
2464
2864
  }
2465
2865
  function renderTitle(content) {
2466
2866
  if (linkTarget !== "title") return content;
2467
2867
  if (view.isAvailable) return renderLink(content);
2468
- return view.href ? /* @__PURE__ */ jsx9("span", { "aria-disabled": "true", "data-link-disabled": "true", children: content }) : content;
2868
+ return view.href ? /* @__PURE__ */ jsx12("span", { "aria-disabled": "true", "data-link-disabled": "true", children: content }) : content;
2469
2869
  }
2470
2870
  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;
2871
+ const imageSource = resolvedImageProps?.src;
2872
+ const [imageStatus, setImageStatus] = useState8(imageSource ? "loading" : "error");
2873
+ useEffect5(() => {
2874
+ setImageStatus(imageSource ? "loading" : "error");
2875
+ }, [imageSource]);
2876
+ let image = null;
2877
+ if (resolvedImageProps) {
2878
+ const imagePropsWithHandlers = {
2879
+ ...resolvedImageProps,
2880
+ src: resolvedImageProps.src,
2881
+ alt: imageAlt ?? resolvedImageProps.alt,
2882
+ onLoad: (event) => {
2883
+ resolvedImageProps.onLoad?.(event);
2884
+ setImageStatus("loaded");
2885
+ },
2886
+ onError: (event) => {
2887
+ resolvedImageProps.onError?.(event);
2888
+ setImageStatus("error");
2889
+ }
2890
+ };
2891
+ image = renderImage?.(imagePropsWithHandlers, item) ?? (ImageComponent ? /* @__PURE__ */ jsx12(ImageComponent, { ...imagePropsWithHandlers }) : /* @__PURE__ */ jsx12("img", { ...imagePropsWithHandlers, alt: imagePropsWithHandlers.alt }));
2892
+ }
2472
2893
  const tags = showTags ? item.tags ?? [] : [];
2473
2894
  const renderedTags = showTags && tags.length > 0 ? renderTags?.(tags, item) ?? null : null;
2474
- const meta = showSavedAt ? /* @__PURE__ */ jsxs5("div", { "data-card-meta": true, children: [
2475
- /* @__PURE__ */ jsxs5("span", { children: [
2895
+ const meta = showSavedAt ? /* @__PURE__ */ jsxs9("div", { "data-card-meta": true, children: [
2896
+ /* @__PURE__ */ jsxs9("span", { children: [
2476
2897
  view.labels.savedAt,
2477
2898
  ":"
2478
2899
  ] }),
2479
2900
  " ",
2480
- /* @__PURE__ */ jsx9("time", { dateTime: new Date(item.savedAt).toISOString(), children: formatSavedAt(item.savedAt) })
2901
+ /* @__PURE__ */ jsx12("time", { dateTime: new Date(item.savedAt).toISOString(), children: formatSavedAt(item.savedAt) })
2481
2902
  ] }) : null;
2482
- 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: [
2484
- showSaveButton ? /* @__PURE__ */ jsx9(
2903
+ const error = view.itemState.error ? /* @__PURE__ */ jsx12("p", { role: "alert", children: getErrorMessage2(view.itemState.error, view.labels.error) }) : null;
2904
+ const actions = view.statusLabel ? /* @__PURE__ */ jsx12(KeepStaleNotice, { item, onRetry, onRemoved }) : /* @__PURE__ */ jsxs9(Fragment6, { children: [
2905
+ showSaveButton ? /* @__PURE__ */ jsx12(
2485
2906
  KeepButton,
2486
2907
  {
2487
2908
  item: toKeepButtonItem(item),
@@ -2489,7 +2910,7 @@ function KeepItemCardRoot({
2489
2910
  getAriaLabel: (buttonState) => `${buttonState.isSaved ? view.labels.remove : view.labels.save} ${String(view.resolvedTitle)}`
2490
2911
  }
2491
2912
  ) : null,
2492
- /* @__PURE__ */ jsx9(
2913
+ /* @__PURE__ */ jsx12(
2493
2914
  "button",
2494
2915
  {
2495
2916
  type: "button",
@@ -2503,25 +2924,27 @@ function KeepItemCardRoot({
2503
2924
  const compoundValue = {
2504
2925
  resolvedTitle: view.resolvedTitle,
2505
2926
  renderTitle,
2506
- image,
2927
+ image: imageStatus === "error" ? null : image,
2928
+ imageStatus,
2507
2929
  fallbackLabel: String(view.resolvedTitle),
2508
2930
  tags,
2509
2931
  tagsLabel: view.labels.tags,
2510
2932
  renderedTags,
2511
2933
  meta,
2512
2934
  error,
2513
- actions
2935
+ actions,
2936
+ renderText: (content) => /* @__PURE__ */ jsx12(KeepHighlight, { query: searchQuery, children: content })
2514
2937
  };
2515
- const defaultBody = /* @__PURE__ */ jsxs5(Fragment3, { children: [
2516
- /* @__PURE__ */ jsx9(KeepItemCardMedia, {}),
2517
- /* @__PURE__ */ jsx9(KeepItemCardContent, {}),
2518
- /* @__PURE__ */ jsx9(KeepItemCardActions, {})
2938
+ const defaultBody = /* @__PURE__ */ jsxs9(Fragment6, { children: [
2939
+ /* @__PURE__ */ jsx12(KeepItemCardMedia, {}),
2940
+ /* @__PURE__ */ jsx12(KeepItemCardContent, {}),
2941
+ /* @__PURE__ */ jsx12(KeepItemCardActions, {})
2519
2942
  ] });
2520
2943
  const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? defaultBody;
2521
2944
  const linkedBody = linkTarget === "card" && view.isAvailable ? renderLink(body) : body;
2522
2945
  const root = renderRoot(
2523
2946
  asChild,
2524
- isValidElement2(children) ? children : void 0,
2947
+ isValidElement3(children) ? children : void 0,
2525
2948
  {
2526
2949
  ...rootProps,
2527
2950
  className,
@@ -2536,38 +2959,52 @@ function KeepItemCardRoot({
2536
2959
  linkedBody,
2537
2960
  "KeepItemCard"
2538
2961
  );
2539
- return /* @__PURE__ */ jsx9(KeepItemCardContext.Provider, { value: compoundValue, children: root });
2962
+ return /* @__PURE__ */ jsx12(KeepItemCardContext.Provider, { value: compoundValue, children: root });
2540
2963
  }
2541
2964
  function KeepItemCardMedia({ children, fallback, ...props }) {
2542
2965
  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) }) });
2966
+ return /* @__PURE__ */ jsx12("div", { ...props, "data-keep-card-part": "media", "data-media-status": context.imageStatus, "data-aspect-ratio": "1/1", children: children ?? context.image ?? /* @__PURE__ */ jsx12("span", { role: "img", "aria-label": context.fallbackLabel, "data-keep-card-fallback": "true", children: fallback ?? /* @__PURE__ */ jsx12(KeepMediaPlaceholderIcon, {}) }) });
2544
2967
  }
2545
2968
  function KeepItemCardContent({ children, ...props }) {
2546
2969
  const context = useKeepItemCardCompound("Content");
2547
- return /* @__PURE__ */ jsx9("div", { ...props, "data-keep-card-part": "content", children: children ?? /* @__PURE__ */ jsxs5(Fragment3, { children: [
2548
- /* @__PURE__ */ jsx9(KeepItemCardTitle, {}),
2970
+ return /* @__PURE__ */ jsx12("div", { ...props, "data-keep-card-part": "content", children: children === void 0 ? /* @__PURE__ */ jsxs9(Fragment6, { children: [
2971
+ /* @__PURE__ */ jsx12(KeepItemCardTitle, {}),
2549
2972
  context.meta,
2550
- /* @__PURE__ */ jsx9(KeepItemCardTags, {}),
2973
+ /* @__PURE__ */ jsx12(KeepItemCardTags, {}),
2551
2974
  context.error
2552
- ] }) });
2975
+ ] }) : context.renderText(children) });
2553
2976
  }
2554
2977
  function KeepItemCardTitle({ as = "h3", children, ...props }) {
2555
2978
  const context = useKeepItemCardCompound("Title");
2556
2979
  return createElement2(
2557
2980
  as,
2558
- { ...props, "data-keep-card-part": "title" },
2559
- context.renderTitle(children ?? context.resolvedTitle)
2981
+ { ...props, "data-keep-card-part": "title", "data-line-clamp": "2" },
2982
+ context.renderTitle(context.renderText(children ?? context.resolvedTitle))
2560
2983
  );
2561
2984
  }
2985
+ function KeepMediaPlaceholderIcon() {
2986
+ return /* @__PURE__ */ jsxs9("svg", { "data-media-fallback-icon": "true", viewBox: "0 0 24 24", "aria-hidden": "true", children: [
2987
+ /* @__PURE__ */ jsx12(
2988
+ "path",
2989
+ {
2990
+ 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",
2991
+ fill: "none",
2992
+ stroke: "currentColor"
2993
+ }
2994
+ ),
2995
+ /* @__PURE__ */ jsx12("circle", { cx: "9", cy: "9", r: "1.5", fill: "currentColor" }),
2996
+ /* @__PURE__ */ jsx12("path", { d: "m5.5 18 4.5-4.5 3 3 2-2L19 18", fill: "none", stroke: "currentColor" })
2997
+ ] });
2998
+ }
2562
2999
  function KeepItemCardTags({ children, ...props }) {
2563
3000
  const context = useKeepItemCardCompound("Tags");
2564
3001
  if (children === void 0 && context.renderedTags) return context.renderedTags;
2565
3002
  if (children === void 0 && context.tags.length === 0) return null;
2566
- return /* @__PURE__ */ jsx9("ul", { ...props, "aria-label": props["aria-label"] ?? context.tagsLabel, "data-keep-card-part": "tags", children: children ?? context.tags.map((tag) => /* @__PURE__ */ jsx9("li", { children: tag }, tag)) });
3003
+ return /* @__PURE__ */ jsx12("ul", { ...props, "aria-label": props["aria-label"] ?? context.tagsLabel, "data-keep-card-part": "tags", children: children ?? context.tags.map((tag) => /* @__PURE__ */ jsx12("li", { children: tag }, tag)) });
2567
3004
  }
2568
3005
  function KeepItemCardActions({ children, ...props }) {
2569
3006
  const context = useKeepItemCardCompound("Actions");
2570
- return /* @__PURE__ */ jsx9("div", { ...props, "data-keep-card-part": "actions", children: children ?? context.actions });
3007
+ return /* @__PURE__ */ jsx12("div", { ...props, "data-keep-card-part": "actions", children: children ?? context.actions });
2571
3008
  }
2572
3009
  var KeepItemCard = Object.assign(KeepItemCardRoot, {
2573
3010
  Media: KeepItemCardMedia,
@@ -2577,12 +3014,12 @@ var KeepItemCard = Object.assign(KeepItemCardRoot, {
2577
3014
  Actions: KeepItemCardActions
2578
3015
  });
2579
3016
  function KeepItemCardSkeleton({ layout = "list", ...props }) {
2580
- return /* @__PURE__ */ jsxs5("article", { ...props, "aria-hidden": "true", "data-keepkit": "card-skeleton", "data-layout": layout, "data-state": "loading", children: [
2581
- /* @__PURE__ */ jsx9("span", { "data-skeleton-part": "media" }),
2582
- /* @__PURE__ */ jsx9("span", { "data-skeleton-part": "title" }),
2583
- /* @__PURE__ */ jsx9("span", { "data-skeleton-part": "meta" }),
2584
- /* @__PURE__ */ jsx9("span", { "data-skeleton-part": "tag" }),
2585
- /* @__PURE__ */ jsx9("span", { "data-skeleton-part": "tag" })
3017
+ return /* @__PURE__ */ jsxs9("article", { ...props, "aria-hidden": "true", "data-keepkit": "card-skeleton", "data-layout": layout, "data-state": "loading", children: [
3018
+ /* @__PURE__ */ jsx12("span", { "data-skeleton-part": "media" }),
3019
+ /* @__PURE__ */ jsx12("span", { "data-skeleton-part": "title" }),
3020
+ /* @__PURE__ */ jsx12("span", { "data-skeleton-part": "meta" }),
3021
+ /* @__PURE__ */ jsx12("span", { "data-skeleton-part": "tag" }),
3022
+ /* @__PURE__ */ jsx12("span", { "data-skeleton-part": "tag" })
2586
3023
  ] });
2587
3024
  }
2588
3025
  function formatSavedAt(timestamp) {
@@ -2592,19 +3029,97 @@ function getErrorMessage2(error, fallback) {
2592
3029
  return error instanceof Error ? error.message : fallback;
2593
3030
  }
2594
3031
 
2595
- // src/KeepList.tsx
2596
- import { Fragment as Fragment4, jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
2597
- function KeepList(props) {
2598
- const { fallback, onBoundaryError, boundaryResetKey, ...listProps } = props;
2599
- const content = /* @__PURE__ */ jsx10(KeepListContent, { ...listProps });
2600
- if (fallback === void 0 && onBoundaryError === void 0) return content;
2601
- return /* @__PURE__ */ jsx10(KeepErrorBoundary, { fallback, onError: onBoundaryError, resetKey: boundaryResetKey, children: content });
2602
- }
2603
- function KeepListContent({
2604
- query,
2605
- children,
2606
- renderItem,
2607
- loading,
3032
+ // src/features/collection/hooks/useKeepListView.ts
3033
+ import { useKeepList as useKeepList4 } from "@keepkit/core/react";
3034
+ function useKeepListView(query) {
3035
+ return {
3036
+ state: useKeepList4(query),
3037
+ labels: {
3038
+ loading: useUiLabel("loadingItems"),
3039
+ empty: useUiLabel("noItems"),
3040
+ error: useUiLabel("errorItems")
3041
+ }
3042
+ };
3043
+ }
3044
+
3045
+ // src/features/collection/hooks/useRovingTabIndex.ts
3046
+ import { useCallback as useCallback3, useEffect as useEffect6, useRef as useRef4 } from "react";
3047
+ function useRovingTabIndex() {
3048
+ const ref = useRef4(null);
3049
+ const getItems = useCallback3(() => {
3050
+ const root = ref.current;
3051
+ if (!root) return [];
3052
+ return Array.from(root.querySelectorAll('[data-keepkit="card"]')).filter(
3053
+ (item) => item.getAttribute("aria-hidden") !== "true" && item.getAttribute("data-roving-disabled") !== "true"
3054
+ );
3055
+ }, []);
3056
+ const syncTabIndices = useCallback3(() => {
3057
+ const items = getItems();
3058
+ if (items.length === 0) return;
3059
+ const activeElement = document.activeElement;
3060
+ const activeItem = items.find((item) => item === activeElement || item.contains(activeElement));
3061
+ const activeIndex = activeItem ? items.indexOf(activeItem) : 0;
3062
+ items.forEach((item, index) => {
3063
+ item.tabIndex = index === activeIndex ? 0 : -1;
3064
+ });
3065
+ }, [getItems]);
3066
+ useEffect6(() => {
3067
+ const root = ref.current;
3068
+ if (!root) return;
3069
+ syncTabIndices();
3070
+ const observer = new MutationObserver(syncTabIndices);
3071
+ observer.observe(root, { childList: true, subtree: true });
3072
+ return () => observer.disconnect();
3073
+ }, [syncTabIndices]);
3074
+ const onFocusCapture = useCallback3(
3075
+ (event) => {
3076
+ if (!(event.target instanceof HTMLElement)) return;
3077
+ const item = event.target.closest('[data-keepkit="card"]');
3078
+ if (!item || !ref.current?.contains(item)) return;
3079
+ getItems().forEach((candidate) => {
3080
+ candidate.tabIndex = candidate === item ? 0 : -1;
3081
+ });
3082
+ },
3083
+ [getItems]
3084
+ );
3085
+ const onKeyDown = useCallback3(
3086
+ (event) => {
3087
+ if (event.defaultPrevented) return;
3088
+ const items = getItems();
3089
+ if (!(event.target instanceof HTMLElement)) return;
3090
+ const current = event.target.closest('[data-keepkit="card"]');
3091
+ if (!current || current !== event.target || !ref.current?.contains(current)) return;
3092
+ const currentIndex = items.indexOf(current);
3093
+ if (currentIndex < 0) return;
3094
+ let nextIndex;
3095
+ if (event.key === "Home") nextIndex = 0;
3096
+ if (event.key === "End") nextIndex = items.length - 1;
3097
+ if (event.key === "ArrowRight" || event.key === "ArrowDown")
3098
+ nextIndex = Math.min(currentIndex + 1, items.length - 1);
3099
+ if (event.key === "ArrowLeft" || event.key === "ArrowUp") nextIndex = Math.max(currentIndex - 1, 0);
3100
+ if (nextIndex === void 0) return;
3101
+ event.preventDefault();
3102
+ if (nextIndex === currentIndex) return;
3103
+ items[nextIndex]?.focus();
3104
+ },
3105
+ [getItems]
3106
+ );
3107
+ return { ref, onKeyDown, onFocusCapture };
3108
+ }
3109
+
3110
+ // src/features/collection/KeepList.tsx
3111
+ import { Fragment as Fragment7, jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
3112
+ function KeepList(props) {
3113
+ const { fallback, onBoundaryError, boundaryResetKey, ...listProps } = props;
3114
+ const content = /* @__PURE__ */ jsx13(KeepListContent, { ...listProps });
3115
+ if (fallback === void 0 && onBoundaryError === void 0) return content;
3116
+ return /* @__PURE__ */ jsx13(KeepErrorBoundary, { fallback, onError: onBoundaryError, resetKey: boundaryResetKey, children: content });
3117
+ }
3118
+ function KeepListContent({
3119
+ query,
3120
+ children,
3121
+ renderItem,
3122
+ loading,
2608
3123
  renderLoading,
2609
3124
  loadingCount = 6,
2610
3125
  empty,
@@ -2612,10 +3127,13 @@ function KeepListContent({
2612
3127
  itemCardProps,
2613
3128
  layout = "list",
2614
3129
  asChild = false,
3130
+ onKeyDown,
3131
+ onFocusCapture,
2615
3132
  className,
2616
3133
  ...rootProps
2617
3134
  }) {
2618
3135
  const view = useKeepListView(query);
3136
+ const roving = useRovingTabIndex();
2619
3137
  const { state } = view;
2620
3138
  const body = getListBody(state, {
2621
3139
  children,
@@ -2630,7 +3148,7 @@ function KeepListContent({
2630
3148
  });
2631
3149
  return renderRoot(
2632
3150
  asChild,
2633
- asChild && isValidElement3(children) ? children : void 0,
3151
+ asChild && isValidElement4(children) ? children : void 0,
2634
3152
  {
2635
3153
  ...rootProps,
2636
3154
  className,
@@ -2638,9 +3156,20 @@ function KeepListContent({
2638
3156
  "data-layout": layout,
2639
3157
  "aria-busy": state.isLoading || rootProps["aria-busy"],
2640
3158
  "data-state": getListState(state),
2641
- "data-loading": state.isLoading ? "true" : void 0
3159
+ "data-loading": state.isLoading ? "true" : void 0,
3160
+ "data-roving-tabindex": "true",
3161
+ role: rootProps.role ?? "group",
3162
+ ref: roving.ref,
3163
+ onKeyDown: (event) => {
3164
+ onKeyDown?.(event);
3165
+ if (!event.defaultPrevented) roving.onKeyDown(event);
3166
+ },
3167
+ onFocusCapture: (event) => {
3168
+ onFocusCapture?.(event);
3169
+ if (!event.defaultPrevented) roving.onFocusCapture(event);
3170
+ }
2642
3171
  },
2643
- body,
3172
+ /* @__PURE__ */ jsx13(KeepSearchQueryProvider, { query: query?.search?.query, children: body }),
2644
3173
  "KeepList"
2645
3174
  );
2646
3175
  }
@@ -2655,437 +3184,280 @@ function getListBody(state, options) {
2655
3184
  if (state.isLoading && !state.isHydrated) {
2656
3185
  if (options.loading !== void 0) return resolveContent(options.loading, state);
2657
3186
  const count = Number.isFinite(options.loadingCount) ? Math.max(0, Math.floor(options.loadingCount)) : 6;
2658
- return /* @__PURE__ */ jsxs6(Fragment4, { children: [
2659
- /* @__PURE__ */ jsx10("span", { role: "status", "data-keepkit": "loading-label", children: options.loadingLabel }),
2660
- /* @__PURE__ */ jsx10("ul", { "data-keepkit": "skeleton-list", "data-layout": options.layout, children: Array.from({ length: count }, (_, index) => (
3187
+ return /* @__PURE__ */ jsxs10(Fragment7, { children: [
3188
+ /* @__PURE__ */ jsx13("span", { role: "status", "data-keepkit": "loading-label", children: options.loadingLabel }),
3189
+ /* @__PURE__ */ jsx13("ul", { "data-keepkit": "skeleton-list", "data-layout": options.layout, children: Array.from({ length: count }, (_, index) => (
2661
3190
  // biome-ignore lint/suspicious/noArrayIndexKey: Static loading placeholders never reorder.
2662
- /* @__PURE__ */ jsx10("li", { children: /* @__PURE__ */ jsx10(KeepItemCardSkeleton, { layout: options.layout }) }, index)
3191
+ /* @__PURE__ */ jsx13("li", { children: /* @__PURE__ */ jsx13(KeepItemCardSkeleton, { layout: options.layout }) }, index)
2663
3192
  )) })
2664
3193
  ] });
2665
3194
  }
2666
3195
  if (state.isHydrated && state.items.length === 0) return resolveContent(options.empty, state);
2667
3196
  if (typeof options.children === "function") return options.children(state);
2668
- if (options.children !== void 0 && !isValidElement3(options.children)) return options.children;
2669
- return /* @__PURE__ */ jsx10("ul", { "data-layout": options.layout, children: state.items.map(
2670
- (item) => options.renderItem ? options.renderItem(item, state) : /* @__PURE__ */ jsx10("li", { children: /* @__PURE__ */ jsx10(KeepItemCard, { item, ...options.itemCardProps }) }, item.id)
3197
+ if (options.children !== void 0 && !isValidElement4(options.children)) return options.children;
3198
+ return /* @__PURE__ */ jsx13("ul", { "data-layout": options.layout, children: state.items.map(
3199
+ (item) => options.renderItem ? options.renderItem(item, state) : /* @__PURE__ */ jsx13("li", { children: /* @__PURE__ */ jsx13(KeepItemCard, { item, ...options.itemCardProps }) }, item.id)
2671
3200
  ) });
2672
3201
  }
2673
3202
 
2674
- // src/KeepTagFilter.tsx
2675
- import { isValidElement as isValidElement4 } from "react";
2676
-
2677
- // src/hooks/useKeepTagFilter.ts
2678
- import { useKeepList as useKeepList4 } from "@keepkit/core/react";
2679
- import { useCallback as useCallback3, useMemo as useMemo3, useState as useState5 } from "react";
2680
- function useKeepTagFilter(options) {
2681
- const { query, controlledValue, defaultValue, onChange, onValueChange } = options;
2682
- const [uncontrolledValue, setUncontrolledValue] = useState5(defaultValue);
2683
- const resolvedValue = controlledValue ?? uncontrolledValue;
2684
- const list = useKeepList4({
2685
- ...query,
2686
- tags: resolvedValue ? [...query?.tags ?? [], resolvedValue] : query?.tags
2687
- });
2688
- const select = useCallback3(
2689
- (tag) => {
2690
- if (controlledValue === void 0) setUncontrolledValue(tag);
2691
- onChange?.(tag);
2692
- onValueChange?.(tag);
2693
- },
2694
- [controlledValue, onChange, onValueChange]
2695
- );
2696
- const state = useMemo3(
2697
- () => ({ tags: list.tags, tagCounts: list.tagCounts, value: resolvedValue, select }),
2698
- [list.tagCounts, list.tags, resolvedValue, select]
2699
- );
2700
- return {
2701
- state,
2702
- isLoading: list.isLoading,
2703
- labels: { all: useUiLabel("allTags"), aria: useUiLabel("filterTags") }
2704
- };
3203
+ // src/features/collection/KeepCollection.tsx
3204
+ import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
3205
+ function KeepCollection(props) {
3206
+ const { fallback, onBoundaryError, boundaryResetKey, ...collectionProps } = props;
3207
+ const content = /* @__PURE__ */ jsx14(KeepCollectionContent, { ...collectionProps });
3208
+ if (fallback === void 0 && onBoundaryError === void 0) return content;
3209
+ return /* @__PURE__ */ jsx14(KeepErrorBoundary2, { fallback, onError: onBoundaryError, resetKey: boundaryResetKey, children: content });
2705
3210
  }
2706
-
2707
- // src/KeepTagFilter.tsx
2708
- import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
2709
- function KeepTagFilter({
2710
- query,
2711
- value: controlledValue,
2712
- defaultValue,
2713
- onChange,
2714
- onValueChange,
2715
- allLabel,
2716
- ariaLabel,
2717
- renderTag,
2718
- render,
2719
- children,
2720
- asChild = false,
3211
+ function KeepCollectionContent({
3212
+ query = {},
3213
+ pageSize = 20,
3214
+ layout = "list",
3215
+ urlSync = false,
3216
+ urlAdapter,
3217
+ features,
3218
+ renderItem,
3219
+ itemCardProps,
3220
+ loading,
3221
+ renderLoading,
3222
+ loadingCount,
3223
+ empty,
3224
+ error,
2721
3225
  className,
2722
3226
  ...rootProps
2723
3227
  }) {
2724
- const view = useKeepTagFilter({ query, controlledValue, defaultValue, onChange, onValueChange });
2725
- const contentChildren = asChild && isValidElement4(children) ? void 0 : children;
2726
- const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? /* @__PURE__ */ jsxs7("fieldset", { children: [
2727
- /* @__PURE__ */ jsx11("legend", { children: ariaLabel ?? view.labels.aria }),
2728
- /* @__PURE__ */ jsx11(
2729
- "button",
2730
- {
2731
- type: "button",
2732
- "data-keep-action": "filter-all-tags",
2733
- "aria-pressed": view.state.value === void 0,
2734
- onClick: () => view.state.select(),
2735
- children: allLabel ?? view.labels.all
2736
- }
2737
- ),
2738
- view.state.tags.map((tag) => /* @__PURE__ */ jsxs7(
2739
- "button",
2740
- {
2741
- type: "button",
2742
- "data-keep-action": "filter-tag",
2743
- "aria-pressed": view.state.value === tag,
2744
- onClick: () => view.state.select(tag),
2745
- children: [
2746
- renderTag ? renderTag(tag, view.state.tagCounts[tag] ?? 0, view.state.value === tag) : tag,
2747
- /* @__PURE__ */ jsxs7("span", { children: [
2748
- " (",
2749
- view.state.tagCounts[tag] ?? 0,
2750
- ")"
2751
- ] })
2752
- ]
2753
- },
2754
- tag
2755
- ))
2756
- ] });
2757
- return renderRoot(
2758
- asChild,
2759
- isValidElement4(children) ? children : void 0,
3228
+ const view = useKeepCollection({ query, pageSize, urlSync, urlAdapter, features });
3229
+ return /* @__PURE__ */ jsxs11(
3230
+ "section",
2760
3231
  {
2761
3232
  ...rootProps,
2762
3233
  className,
2763
- "data-keepkit": "tag-filter",
2764
- "data-state": view.state.value === void 0 ? "all" : "filtered",
2765
- "data-loading": view.isLoading ? "true" : void 0
2766
- },
2767
- body,
2768
- "KeepTagFilter"
3234
+ "data-keepkit": "collection",
3235
+ "data-layout": layout,
3236
+ "aria-busy": view.list.isLoading || view.list.isMutating || rootProps["aria-busy"],
3237
+ "data-state": getCollectionState(view.list),
3238
+ "data-loading": view.list.isLoading || view.list.isMutating ? "true" : void 0,
3239
+ children: [
3240
+ /* @__PURE__ */ jsxs11("div", { children: [
3241
+ view.enabled.search ? /* @__PURE__ */ jsx14(KeepSearchInput, { value: view.searchValue, onValueChange: view.setSearchValue }) : null,
3242
+ view.enabled.sort ? /* @__PURE__ */ jsx14(KeepSortSelect, { value: view.sortValue, onValueChange: view.setSortValue }) : null,
3243
+ view.enabled.tagFilter ? /* @__PURE__ */ jsx14(KeepTagFilter, { query, value: view.tag, onValueChange: view.setTag }) : null
3244
+ ] }),
3245
+ /* @__PURE__ */ jsx14(
3246
+ KeepList,
3247
+ {
3248
+ query: view.resolvedQuery,
3249
+ renderItem,
3250
+ itemCardProps,
3251
+ layout,
3252
+ loading,
3253
+ renderLoading,
3254
+ loadingCount,
3255
+ empty,
3256
+ error
3257
+ }
3258
+ ),
3259
+ view.enabled.pagination ? /* @__PURE__ */ jsx14(
3260
+ KeepPagination,
3261
+ {
3262
+ totalCount: view.list.totalCount,
3263
+ pageSize: view.resolvedPageSize,
3264
+ page: view.list.page,
3265
+ onPageChange: view.setPage
3266
+ }
3267
+ ) : null,
3268
+ view.enabled.bulkActions ? /* @__PURE__ */ jsx14(KeepBulkActions, { query: view.resolvedQuery }) : null
3269
+ ]
3270
+ }
3271
+ );
3272
+ }
3273
+ function getCollectionState(list) {
3274
+ if (list.error && list.items.length === 0) return "error";
3275
+ if (list.isLoading && !list.isHydrated) return "loading";
3276
+ if (list.isHydrated && list.items.length === 0) return "empty";
3277
+ return "ready";
3278
+ }
3279
+
3280
+ // src/features/collection/KeepLayout.tsx
3281
+ import { jsx as jsx15 } from "react/jsx-runtime";
3282
+ function KeepLayout({ layout = "list", children, onKeyDown, onFocusCapture, ...props }) {
3283
+ const roving = useRovingTabIndex();
3284
+ return (
3285
+ // biome-ignore lint/a11y/noStaticElementInteractions: The group manages keyboard focus for descendant cards.
3286
+ /* @__PURE__ */ jsx15(
3287
+ "div",
3288
+ {
3289
+ ...props,
3290
+ ref: roving.ref,
3291
+ "data-keepkit": "layout",
3292
+ "data-layout": layout,
3293
+ "data-roving-tabindex": "true",
3294
+ role: props.role ?? "group",
3295
+ onKeyDown: (event) => {
3296
+ onKeyDown?.(event);
3297
+ if (!event.defaultPrevented) roving.onKeyDown(event);
3298
+ },
3299
+ onFocusCapture: (event) => {
3300
+ onFocusCapture?.(event);
3301
+ if (!event.defaultPrevented) roving.onFocusCapture(event);
3302
+ },
3303
+ children
3304
+ }
3305
+ )
2769
3306
  );
2770
3307
  }
2771
3308
 
2772
- // src/hooks/useQueryControls.ts
2773
- import { useEffect as useEffect3, useState as useState6 } from "react";
2774
- function useKeepSearchInput(options) {
2775
- const { controlledValue, defaultValue, debounceMs, onValueChange } = options;
2776
- const [uncontrolledValue, setUncontrolledValue] = useState6(defaultValue);
2777
- const value = controlledValue ?? uncontrolledValue;
2778
- useEffect3(() => {
2779
- if (!onValueChange) return;
2780
- if (debounceMs <= 0) {
2781
- onValueChange(value);
2782
- return;
3309
+ // src/features/collection/KeepReorderableList.tsx
3310
+ import { useState as useState9 } from "react";
3311
+ import { jsx as jsx16 } from "react/jsx-runtime";
3312
+ function KeepReorderableList({
3313
+ items,
3314
+ onReorder,
3315
+ renderItem,
3316
+ itemLabel = (_item, index) => `Move item ${index + 1}`,
3317
+ ...props
3318
+ }) {
3319
+ const [draggedId, setDraggedId] = useState9(null);
3320
+ const [dropTargetIndex, setDropTargetIndex] = useState9(null);
3321
+ const ids = items.map((item) => item.id);
3322
+ function commit(nextIds) {
3323
+ void onReorder(nextIds);
3324
+ }
3325
+ function move(index, targetIndex) {
3326
+ if (targetIndex < 0 || targetIndex >= ids.length || targetIndex === index) return;
3327
+ const next = [...ids];
3328
+ const [id] = next.splice(index, 1);
3329
+ if (id === void 0) return;
3330
+ next.splice(targetIndex, 0, id);
3331
+ commit(next);
3332
+ }
3333
+ function moveToInsertion(index, insertionIndex) {
3334
+ const next = [...ids];
3335
+ const [id] = next.splice(index, 1);
3336
+ if (id === void 0) return;
3337
+ const targetIndex = Math.min(
3338
+ Math.max(0, insertionIndex > index ? insertionIndex - 1 : insertionIndex),
3339
+ next.length
3340
+ );
3341
+ next.splice(targetIndex, 0, id);
3342
+ if (next[index] === id) return;
3343
+ commit(next);
3344
+ }
3345
+ return /* @__PURE__ */ jsx16("ul", { ...props, "data-keepkit": "reorderable-list", children: items.map((item, index) => {
3346
+ const moveUp = () => move(index, index - 1);
3347
+ const moveDown = () => move(index, index + 1);
3348
+ const onHandleKeyDown = (event) => {
3349
+ if (event.key === "ArrowUp") {
3350
+ event.preventDefault();
3351
+ moveUp();
3352
+ } else if (event.key === "ArrowDown") {
3353
+ event.preventDefault();
3354
+ moveDown();
3355
+ }
3356
+ };
3357
+ const state = {
3358
+ index,
3359
+ isDragging: draggedId === item.id,
3360
+ moveUp,
3361
+ moveDown,
3362
+ dragHandleProps: {
3363
+ role: "button",
3364
+ tabIndex: 0,
3365
+ draggable: true,
3366
+ "data-drag-handle": "true",
3367
+ "aria-label": itemLabel(item, index),
3368
+ "aria-grabbed": draggedId === item.id,
3369
+ onDragStart: () => {
3370
+ setDraggedId(item.id);
3371
+ setDropTargetIndex(index);
3372
+ },
3373
+ onDragEnd: () => {
3374
+ setDraggedId(null);
3375
+ setDropTargetIndex(null);
3376
+ },
3377
+ onKeyDown: onHandleKeyDown
3378
+ }
3379
+ };
3380
+ return /* @__PURE__ */ jsx16(
3381
+ "li",
3382
+ {
3383
+ "data-reorder-index": index,
3384
+ "data-dragging": draggedId === item.id ? "true" : void 0,
3385
+ "data-drop-target": draggedId && draggedId !== item.id ? dropTargetIndex === index ? "before" : dropTargetIndex === index + 1 ? "after" : void 0 : void 0,
3386
+ onDragOver: (event) => {
3387
+ if (!draggedId || draggedId === item.id) return;
3388
+ event.preventDefault();
3389
+ const bounds = event.currentTarget.getBoundingClientRect();
3390
+ const insertionIndex = event.clientY > bounds.top + bounds.height / 2 ? index + 1 : index;
3391
+ setDropTargetIndex(insertionIndex);
3392
+ },
3393
+ onDragLeave: (event) => {
3394
+ if (event.currentTarget === event.target) setDropTargetIndex(null);
3395
+ },
3396
+ onDrop: (event) => {
3397
+ event.preventDefault();
3398
+ if (!draggedId || draggedId === item.id) return;
3399
+ const sourceIndex = ids.indexOf(draggedId);
3400
+ setDraggedId(null);
3401
+ setDropTargetIndex(null);
3402
+ moveToInsertion(sourceIndex, dropTargetIndex ?? index);
3403
+ },
3404
+ children: renderItem(item, state)
3405
+ },
3406
+ item.id
3407
+ );
3408
+ }) });
3409
+ }
3410
+
3411
+ // src/features/editor/KeepNoteEditor.tsx
3412
+ import { isValidElement as isValidElement5 } from "react";
3413
+
3414
+ // src/features/editor/hooks/useKeepNoteEditor.ts
3415
+ import { useKeepItem as useKeepItem3 } from "@keepkit/core/react";
3416
+ import { useCallback as useCallback4, useEffect as useEffect7, useRef as useRef5, useState as useState10 } from "react";
3417
+ function useKeepNoteEditor({ item, debounceMs, onSaved, onSaveError }) {
3418
+ const itemState = useKeepItem3(item);
3419
+ const { error, isMutating, item: savedItem, updateNote } = itemState;
3420
+ const [note, setNote] = useState10(item.note ?? "");
3421
+ const baselineNote = savedItem?.note ?? item.note ?? "";
3422
+ const isDirty = note !== baselineNote;
3423
+ const lastSavedNoteRef = useRef5(void 0);
3424
+ useEffect7(() => setNote(baselineNote), [baselineNote]);
3425
+ const save = useCallback4(async () => {
3426
+ const nextNote = note.trim() || void 0;
3427
+ try {
3428
+ await updateNote(nextNote);
3429
+ lastSavedNoteRef.current = note;
3430
+ onSaved?.(nextNote);
3431
+ } catch (cause) {
3432
+ onSaveError?.(cause);
3433
+ throw cause;
2783
3434
  }
2784
- const timer = window.setTimeout(() => onValueChange(value), debounceMs);
3435
+ }, [note, onSaveError, onSaved, updateNote]);
3436
+ useEffect7(() => {
3437
+ if (!isDirty || debounceMs <= 0 || lastSavedNoteRef.current === note) return;
3438
+ const timer = window.setTimeout(() => void save().catch(() => void 0), debounceMs);
2785
3439
  return () => window.clearTimeout(timer);
2786
- }, [debounceMs, onValueChange, value]);
2787
- return {
2788
- value,
2789
- label: useUiLabel("search"),
2790
- change: (event) => {
2791
- if (controlledValue === void 0) setUncontrolledValue(event.currentTarget.value);
2792
- }
3440
+ }, [debounceMs, isDirty, note, save]);
3441
+ const state = {
3442
+ item,
3443
+ note,
3444
+ setNote,
3445
+ isDirty,
3446
+ isSaving: isMutating,
3447
+ error,
3448
+ save
3449
+ };
3450
+ const submit = (event) => {
3451
+ event.preventDefault();
3452
+ void save().catch(() => void 0);
2793
3453
  };
2794
- }
2795
- function useKeepSortSelect(options) {
2796
- const { controlledValue, defaultValue, onValueChange } = options;
2797
- const [uncontrolledValue, setUncontrolledValue] = useState6(defaultValue);
2798
- const value = controlledValue ?? uncontrolledValue;
2799
3454
  return {
2800
- value,
2801
- change: (event) => {
2802
- const nextValue = event.currentTarget.value;
2803
- if (controlledValue === void 0) setUncontrolledValue(nextValue);
2804
- const [by, direction] = nextValue.split(":");
2805
- onValueChange?.(nextValue, { by, direction });
2806
- },
2807
- labels: {
2808
- sort: useUiLabel("sort"),
2809
- updatedNewest: useUiLabel("updatedNewest"),
2810
- updatedOldest: useUiLabel("updatedOldest"),
2811
- savedNewest: useUiLabel("savedNewest"),
2812
- savedOldest: useUiLabel("savedOldest")
2813
- }
2814
- };
2815
- }
2816
- function useKeepPagination(options) {
2817
- const { totalCount, pageSize, page, maxPageButtons, onPageChange } = options;
2818
- const pageCount = Math.max(1, Math.ceil(totalCount / Math.max(1, pageSize)));
2819
- const currentPage = Math.min(Math.max(1, page), pageCount);
2820
- const goToPage = (nextPage) => {
2821
- const next = Math.min(Math.max(1, nextPage), pageCount);
2822
- onPageChange?.(next, (next - 1) * pageSize);
2823
- };
2824
- return {
2825
- pageCount,
2826
- currentPage,
2827
- goToPage,
2828
- visiblePages: getVisiblePages(currentPage, pageCount, Math.max(1, maxPageButtons)),
2829
- labels: {
2830
- previous: useUiLabel("previousPage"),
2831
- next: useUiLabel("nextPage"),
2832
- page: useUiLabel("page"),
2833
- pagination: useUiLabel("pagination")
2834
- }
2835
- };
2836
- }
2837
- function getVisiblePages(currentPage, pageCount, maxPageButtons) {
2838
- if (pageCount <= maxPageButtons) return Array.from({ length: pageCount }, (_, index) => index + 1);
2839
- const half = Math.floor(maxPageButtons / 2);
2840
- const start = Math.min(Math.max(1, currentPage - half), pageCount - maxPageButtons + 1);
2841
- return Array.from({ length: maxPageButtons }, (_, index) => start + index);
2842
- }
2843
-
2844
- // src/query-controls.tsx
2845
- import { Fragment as Fragment5, jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
2846
- function KeepSearchInput({
2847
- value: controlledValue,
2848
- defaultValue = "",
2849
- debounceMs = 300,
2850
- onValueChange,
2851
- "aria-label": ariaLabel,
2852
- placeholder,
2853
- ...props
2854
- }) {
2855
- const view = useKeepSearchInput({ controlledValue, defaultValue, debounceMs, onValueChange });
2856
- return /* @__PURE__ */ jsx12(
2857
- "input",
2858
- {
2859
- ...props,
2860
- "data-keepkit": "search-input",
2861
- "data-keep-action": "search",
2862
- type: "search",
2863
- value: view.value,
2864
- "data-state": view.value ? "active" : "idle",
2865
- "data-disabled": props.disabled ? "true" : void 0,
2866
- "aria-label": ariaLabel ?? view.label,
2867
- placeholder: placeholder ?? view.label,
2868
- onChange: view.change
2869
- }
2870
- );
2871
- }
2872
- function KeepSortSelect({
2873
- value: controlledValue,
2874
- defaultValue = "updatedAt:desc",
2875
- onValueChange,
2876
- "aria-label": ariaLabel,
2877
- children,
2878
- ...props
2879
- }) {
2880
- const view = useKeepSortSelect({ controlledValue, defaultValue, onValueChange });
2881
- const options = children ?? /* @__PURE__ */ jsxs8(Fragment5, { children: [
2882
- /* @__PURE__ */ jsx12("option", { value: "updatedAt:desc", children: view.labels.updatedNewest }),
2883
- /* @__PURE__ */ jsx12("option", { value: "updatedAt:asc", children: view.labels.updatedOldest }),
2884
- /* @__PURE__ */ jsx12("option", { value: "savedAt:desc", children: view.labels.savedNewest }),
2885
- /* @__PURE__ */ jsx12("option", { value: "savedAt:asc", children: view.labels.savedOldest })
2886
- ] });
2887
- return /* @__PURE__ */ jsx12(
2888
- "select",
2889
- {
2890
- ...props,
2891
- "data-keepkit": "sort-select",
2892
- "data-keep-action": "sort",
2893
- value: view.value,
2894
- "data-state": "selected",
2895
- "data-disabled": props.disabled ? "true" : void 0,
2896
- "aria-label": ariaLabel ?? view.labels.sort,
2897
- onChange: view.change,
2898
- children: options
2899
- }
2900
- );
2901
- }
2902
- function KeepPagination({
2903
- totalCount,
2904
- pageSize,
2905
- page = 1,
2906
- maxPageButtons = 7,
2907
- onPageChange,
2908
- render,
2909
- ...props
2910
- }) {
2911
- const view = useKeepPagination({ totalCount, pageSize, page, maxPageButtons, onPageChange });
2912
- const navProps = {
2913
- ...props,
2914
- "data-keepkit": "pagination",
2915
- "aria-label": props["aria-label"] ?? view.labels.pagination,
2916
- "data-state": view.pageCount > 1 ? "active" : "idle"
2917
- };
2918
- if (render)
2919
- return /* @__PURE__ */ jsx12("nav", { ...navProps, children: render({ page: view.currentPage, pageCount: view.pageCount, goToPage: view.goToPage }) });
2920
- return /* @__PURE__ */ jsxs8("nav", { ...navProps, children: [
2921
- /* @__PURE__ */ jsx12(
2922
- "button",
2923
- {
2924
- type: "button",
2925
- "data-keep-action": "previous-page",
2926
- onClick: () => view.goToPage(view.currentPage - 1),
2927
- disabled: view.currentPage <= 1,
2928
- children: view.labels.previous
2929
- }
2930
- ),
2931
- view.visiblePages.map((nextPage) => /* @__PURE__ */ jsx12(
2932
- "button",
2933
- {
2934
- type: "button",
2935
- "data-keep-action": "select-page",
2936
- "aria-current": nextPage === view.currentPage ? "page" : void 0,
2937
- "aria-label": `${view.labels.page} ${nextPage}`,
2938
- onClick: () => view.goToPage(nextPage),
2939
- children: nextPage
2940
- },
2941
- nextPage
2942
- )),
2943
- /* @__PURE__ */ jsx12(
2944
- "button",
2945
- {
2946
- type: "button",
2947
- "data-keep-action": "next-page",
2948
- onClick: () => view.goToPage(view.currentPage + 1),
2949
- disabled: view.currentPage >= view.pageCount,
2950
- children: view.labels.next
2951
- }
2952
- )
2953
- ] });
2954
- }
2955
-
2956
- // src/KeepCollection.tsx
2957
- import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
2958
- function KeepCollection(props) {
2959
- const { fallback, onBoundaryError, boundaryResetKey, ...collectionProps } = props;
2960
- const content = /* @__PURE__ */ jsx13(KeepCollectionContent, { ...collectionProps });
2961
- if (fallback === void 0 && onBoundaryError === void 0) return content;
2962
- return /* @__PURE__ */ jsx13(KeepErrorBoundary2, { fallback, onError: onBoundaryError, resetKey: boundaryResetKey, children: content });
2963
- }
2964
- function KeepCollectionContent({
2965
- query = {},
2966
- pageSize = 20,
2967
- layout = "list",
2968
- urlSync = false,
2969
- urlAdapter,
2970
- features,
2971
- renderItem,
2972
- itemCardProps,
2973
- loading,
2974
- renderLoading,
2975
- loadingCount,
2976
- empty,
2977
- error,
2978
- className,
2979
- ...rootProps
2980
- }) {
2981
- const view = useKeepCollection({ query, pageSize, urlSync, urlAdapter, features });
2982
- return /* @__PURE__ */ jsxs9(
2983
- "section",
2984
- {
2985
- ...rootProps,
2986
- className,
2987
- "data-keepkit": "collection",
2988
- "data-layout": layout,
2989
- "aria-busy": view.list.isLoading || view.list.isMutating || rootProps["aria-busy"],
2990
- "data-state": getCollectionState(view.list),
2991
- "data-loading": view.list.isLoading || view.list.isMutating ? "true" : void 0,
2992
- children: [
2993
- /* @__PURE__ */ jsxs9("div", { children: [
2994
- view.enabled.search ? /* @__PURE__ */ jsx13(KeepSearchInput, { value: view.searchValue, onValueChange: view.setSearchValue }) : null,
2995
- view.enabled.sort ? /* @__PURE__ */ jsx13(KeepSortSelect, { value: view.sortValue, onValueChange: view.setSortValue }) : null,
2996
- view.enabled.tagFilter ? /* @__PURE__ */ jsx13(KeepTagFilter, { query, value: view.tag, onValueChange: view.setTag }) : null
2997
- ] }),
2998
- /* @__PURE__ */ jsx13(
2999
- KeepList,
3000
- {
3001
- query: view.resolvedQuery,
3002
- renderItem,
3003
- itemCardProps,
3004
- layout,
3005
- loading,
3006
- renderLoading,
3007
- loadingCount,
3008
- empty,
3009
- error
3010
- }
3011
- ),
3012
- view.enabled.pagination ? /* @__PURE__ */ jsx13(
3013
- KeepPagination,
3014
- {
3015
- totalCount: view.list.totalCount,
3016
- pageSize: view.resolvedPageSize,
3017
- page: view.list.page,
3018
- onPageChange: view.setPage
3019
- }
3020
- ) : null,
3021
- view.enabled.bulkActions ? /* @__PURE__ */ jsx13(KeepBulkActions, { query: view.resolvedQuery }) : null
3022
- ]
3023
- }
3024
- );
3025
- }
3026
- function getCollectionState(list) {
3027
- if (list.error && list.items.length === 0) return "error";
3028
- if (list.isLoading && !list.isHydrated) return "loading";
3029
- if (list.isHydrated && list.items.length === 0) return "empty";
3030
- return "ready";
3031
- }
3032
-
3033
- // src/KeepLayout.tsx
3034
- 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 });
3037
- }
3038
-
3039
- // src/KeepNoteEditor.tsx
3040
- import { isValidElement as isValidElement5 } from "react";
3041
-
3042
- // src/hooks/useKeepNoteEditor.ts
3043
- import { useKeepItem as useKeepItem3 } from "@keepkit/core/react";
3044
- import { useCallback as useCallback4, useEffect as useEffect4, useRef as useRef4, useState as useState7 } from "react";
3045
- function useKeepNoteEditor({ item, debounceMs, onSaved, onSaveError }) {
3046
- const itemState = useKeepItem3(item);
3047
- const { error, isMutating, item: savedItem, updateNote } = itemState;
3048
- const [note, setNote] = useState7(item.note ?? "");
3049
- const baselineNote = savedItem?.note ?? item.note ?? "";
3050
- const isDirty = note !== baselineNote;
3051
- const lastSavedNoteRef = useRef4(void 0);
3052
- useEffect4(() => setNote(baselineNote), [baselineNote]);
3053
- const save = useCallback4(async () => {
3054
- const nextNote = note.trim() || void 0;
3055
- try {
3056
- await updateNote(nextNote);
3057
- lastSavedNoteRef.current = note;
3058
- onSaved?.(nextNote);
3059
- } catch (cause) {
3060
- onSaveError?.(cause);
3061
- throw cause;
3062
- }
3063
- }, [note, onSaveError, onSaved, updateNote]);
3064
- useEffect4(() => {
3065
- if (!isDirty || debounceMs <= 0 || lastSavedNoteRef.current === note) return;
3066
- const timer = window.setTimeout(() => void save().catch(() => void 0), debounceMs);
3067
- return () => window.clearTimeout(timer);
3068
- }, [debounceMs, isDirty, note, save]);
3069
- const state = {
3070
- item,
3071
- note,
3072
- setNote,
3073
- isDirty,
3074
- isSaving: isMutating,
3075
- error,
3076
- save
3077
- };
3078
- const submit = (event) => {
3079
- event.preventDefault();
3080
- void save().catch(() => void 0);
3081
- };
3082
- return {
3083
- state,
3084
- submit,
3085
- handleKeyDown: (event) => {
3086
- if (event.key !== "Enter" || !event.ctrlKey && !event.metaKey) return;
3087
- event.preventDefault();
3088
- void save().catch(() => void 0);
3455
+ state,
3456
+ submit,
3457
+ handleKeyDown: (event) => {
3458
+ if (event.key !== "Enter" || !event.ctrlKey && !event.metaKey) return;
3459
+ event.preventDefault();
3460
+ void save().catch(() => void 0);
3089
3461
  },
3090
3462
  labels: {
3091
3463
  note: useUiLabel("note"),
@@ -3095,8 +3467,8 @@ function useKeepNoteEditor({ item, debounceMs, onSaved, onSaveError }) {
3095
3467
  };
3096
3468
  }
3097
3469
 
3098
- // src/KeepNoteEditor.tsx
3099
- import { Fragment as Fragment6, jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
3470
+ // src/features/editor/KeepNoteEditor.tsx
3471
+ import { Fragment as Fragment8, jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
3100
3472
  function KeepNoteEditor({
3101
3473
  item,
3102
3474
  label,
@@ -3114,10 +3486,10 @@ function KeepNoteEditor({
3114
3486
  const view = useKeepNoteEditor({ item, debounceMs, onSaved, onSaveError });
3115
3487
  const { error, isDirty, isSaving, note, setNote } = view.state;
3116
3488
  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: [
3118
- /* @__PURE__ */ jsxs10("label", { children: [
3489
+ const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? /* @__PURE__ */ jsxs12(Fragment8, { children: [
3490
+ /* @__PURE__ */ jsxs12("label", { children: [
3119
3491
  label ?? view.labels.note,
3120
- /* @__PURE__ */ jsx15(
3492
+ /* @__PURE__ */ jsx17(
3121
3493
  "textarea",
3122
3494
  {
3123
3495
  "data-keep-action": "edit-note",
@@ -3129,10 +3501,10 @@ function KeepNoteEditor({
3129
3501
  }
3130
3502
  )
3131
3503
  ] }),
3132
- /* @__PURE__ */ jsx15("button", { type: "submit", "data-keep-action": "save-note", disabled: isSaving, "aria-busy": isSaving, children: saveLabel ?? view.labels.save })
3504
+ /* @__PURE__ */ jsx17("button", { type: "submit", "data-keep-action": "save-note", disabled: isSaving, "aria-busy": isSaving, children: saveLabel ?? view.labels.save })
3133
3505
  ] });
3134
3506
  if (!asChild) {
3135
- return /* @__PURE__ */ jsxs10(
3507
+ return /* @__PURE__ */ jsxs12(
3136
3508
  "form",
3137
3509
  {
3138
3510
  ...formProps,
@@ -3145,7 +3517,7 @@ function KeepNoteEditor({
3145
3517
  "data-disabled": isSaving ? "true" : void 0,
3146
3518
  children: [
3147
3519
  body,
3148
- error ? /* @__PURE__ */ jsx15("p", { role: "alert", children: getErrorMessage3(error, view.labels.error) }) : null
3520
+ error ? /* @__PURE__ */ jsx17("p", { role: "alert", children: getErrorMessage3(error, view.labels.error) }) : null
3149
3521
  ]
3150
3522
  }
3151
3523
  );
@@ -3167,20 +3539,422 @@ function KeepNoteEditor({
3167
3539
  "KeepNoteEditor"
3168
3540
  );
3169
3541
  }
3170
- function getErrorMessage3(error, fallback) {
3171
- return error instanceof Error ? error.message : fallback;
3172
- }
3542
+ function getErrorMessage3(error, fallback) {
3543
+ return error instanceof Error ? error.message : fallback;
3544
+ }
3545
+
3546
+ // src/features/editor/hooks/useKeepTagEditor.ts
3547
+ import { useKeepItem as useKeepItem4 } from "@keepkit/core/react";
3548
+ import { useCallback as useCallback5, useEffect as useEffect8, useState as useState11 } from "react";
3549
+ function useKeepTagEditor({ item, onSaved, onSaveError }) {
3550
+ const itemState = useKeepItem4(item);
3551
+ const [tags, setTags] = useState11(item.tags ?? []);
3552
+ const [input, setInput] = useState11("");
3553
+ useEffect8(() => setTags(itemState.item?.tags ?? item.tags ?? []), [item.tags, itemState.item?.tags]);
3554
+ const save = useCallback5(async () => {
3555
+ const nextTags = normalizeUiTags(tags);
3556
+ try {
3557
+ await itemState.updateTags(nextTags);
3558
+ setTags(nextTags);
3559
+ onSaved?.(nextTags);
3560
+ } catch (cause) {
3561
+ onSaveError?.(cause);
3562
+ throw cause;
3563
+ }
3564
+ }, [itemState, onSaveError, onSaved, tags]);
3565
+ const addTag = (tag) => {
3566
+ setTags(normalizeUiTags([...tags, tag]));
3567
+ setInput("");
3568
+ };
3569
+ const state = { tags, setTags, save, isSaving: itemState.isMutating };
3570
+ return {
3571
+ state,
3572
+ input,
3573
+ setInput,
3574
+ error: itemState.error,
3575
+ handleInputKeyDown: (event) => {
3576
+ if (event.key === "Enter") {
3577
+ if (event.nativeEvent.isComposing) return;
3578
+ event.preventDefault();
3579
+ if (input.trim()) addTag(input);
3580
+ } else if (event.key === "Backspace" && input.length === 0 && tags.length > 0) {
3581
+ event.preventDefault();
3582
+ setTags(tags.slice(0, -1));
3583
+ }
3584
+ },
3585
+ removeTag: (tag) => setTags(tags.filter((current) => current !== tag)),
3586
+ submit: (event) => {
3587
+ event.preventDefault();
3588
+ void save().catch(() => void 0);
3589
+ },
3590
+ labels: {
3591
+ tags: useUiLabel("tagsToApply"),
3592
+ remove: useUiLabel("remove"),
3593
+ apply: useUiLabel("applyTags"),
3594
+ error: useUiLabel("error")
3595
+ }
3596
+ };
3597
+ }
3598
+
3599
+ // src/features/editor/KeepTagEditor.tsx
3600
+ import { Fragment as Fragment9, jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
3601
+ function KeepTagEditor({
3602
+ item,
3603
+ availableTags = [],
3604
+ onSaved,
3605
+ onSaveError,
3606
+ render,
3607
+ ...props
3608
+ }) {
3609
+ const view = useKeepTagEditor({ item, onSaved, onSaveError });
3610
+ const { isSaving, tags } = view.state;
3611
+ const body = render ? render(view.state) : /* @__PURE__ */ jsxs13(Fragment9, { children: [
3612
+ /* @__PURE__ */ jsxs13("label", { children: [
3613
+ view.labels.tags,
3614
+ /* @__PURE__ */ jsx18(
3615
+ "input",
3616
+ {
3617
+ "data-keep-action": "edit-tags",
3618
+ value: view.input,
3619
+ list: availableTags.length > 0 ? `keep-tags-${item.id}` : void 0,
3620
+ onChange: (event) => view.setInput(event.currentTarget.value),
3621
+ onKeyDown: view.handleInputKeyDown
3622
+ }
3623
+ )
3624
+ ] }),
3625
+ availableTags.length > 0 ? /* @__PURE__ */ jsx18("datalist", { id: `keep-tags-${item.id}`, children: availableTags.map((tag) => /* @__PURE__ */ jsx18("option", { value: tag }, tag)) }) : null,
3626
+ /* @__PURE__ */ jsx18("ul", { "aria-label": view.labels.tags, children: tags.map((tag) => /* @__PURE__ */ jsxs13("li", { children: [
3627
+ tag,
3628
+ /* @__PURE__ */ jsx18("button", { type: "button", "data-keep-action": "remove-tag", onClick: () => view.removeTag(tag), children: view.labels.remove })
3629
+ ] }, tag)) }),
3630
+ /* @__PURE__ */ jsx18("button", { type: "submit", "data-keep-action": "apply-tags", disabled: isSaving, "aria-busy": isSaving, children: view.labels.apply })
3631
+ ] });
3632
+ return /* @__PURE__ */ jsxs13(
3633
+ "form",
3634
+ {
3635
+ ...props,
3636
+ onSubmit: view.submit,
3637
+ "aria-busy": isSaving || props["aria-busy"],
3638
+ "data-keepkit": "tag-editor",
3639
+ "data-state": view.error ? "error" : isSaving ? "saving" : "idle",
3640
+ "data-loading": isSaving ? "true" : void 0,
3641
+ "data-disabled": isSaving ? "true" : void 0,
3642
+ children: [
3643
+ body,
3644
+ view.error ? /* @__PURE__ */ jsx18("p", { role: "alert", children: getErrorMessage4(view.error, view.labels.error) }) : null
3645
+ ]
3646
+ }
3647
+ );
3648
+ }
3649
+ function getErrorMessage4(error, fallback) {
3650
+ return error instanceof Error ? error.message : fallback;
3651
+ }
3652
+
3653
+ // src/features/feedback/useKeepToastFeedback.ts
3654
+ import { useCallback as useCallback6 } from "react";
3655
+ function useKeepToastFeedback(showToast) {
3656
+ return useCallback6(
3657
+ (event) => {
3658
+ if (!("undo" in event)) {
3659
+ showToast(event.message);
3660
+ return;
3661
+ }
3662
+ showToast(event.message, {
3663
+ action: {
3664
+ label: event.undoLabel,
3665
+ onClick: () => void event.undo()
3666
+ }
3667
+ });
3668
+ },
3669
+ [showToast]
3670
+ );
3671
+ }
3672
+
3673
+ // src/features/navigation/KeepTourBar.tsx
3674
+ import { useKeepNavigator } from "@keepkit/core/react";
3675
+ import { useId } from "react";
3676
+
3677
+ // src/features/navigation/hooks/useKeepTourShortcuts.ts
3678
+ import { useEffect as useEffect9 } from "react";
3679
+ function useKeepTourShortcuts({
3680
+ onNext,
3681
+ onPrev,
3682
+ enabled = true,
3683
+ allowInEditable = false,
3684
+ preventDefault = true,
3685
+ nextKeys = ["j", "]"],
3686
+ prevKeys = ["k", "["],
3687
+ onError
3688
+ }) {
3689
+ useEffect9(() => {
3690
+ if (!enabled) return;
3691
+ const handleKeyDown = (event) => {
3692
+ if (!allowInEditable && isEditableTarget(event.target)) return;
3693
+ if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return;
3694
+ const key = event.key.toLocaleLowerCase();
3695
+ const action = nextKeys.some((candidate) => candidate.toLocaleLowerCase() === key) ? onNext : prevKeys.some((candidate) => candidate.toLocaleLowerCase() === key) ? onPrev : void 0;
3696
+ if (!action) return;
3697
+ if (preventDefault) event.preventDefault();
3698
+ void Promise.resolve(action()).catch((error) => onError?.(error));
3699
+ };
3700
+ window.addEventListener("keydown", handleKeyDown);
3701
+ return () => window.removeEventListener("keydown", handleKeyDown);
3702
+ }, [allowInEditable, enabled, nextKeys, onError, onNext, onPrev, preventDefault, prevKeys]);
3703
+ }
3704
+ function isEditableTarget(target) {
3705
+ if (!(target instanceof HTMLElement)) return false;
3706
+ return target.isContentEditable || target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT";
3707
+ }
3708
+
3709
+ // src/features/navigation/KeepTourBar.tsx
3710
+ import { Fragment as Fragment10, jsx as jsx19, jsxs as jsxs14 } from "react/jsx-runtime";
3711
+ function KeepTourBar({
3712
+ navigation: providedNavigation,
3713
+ currentId,
3714
+ initialIndex,
3715
+ showProgress = true,
3716
+ prevHref,
3717
+ nextHref,
3718
+ backHref,
3719
+ onPrev,
3720
+ onNext,
3721
+ onBack,
3722
+ prevLabel,
3723
+ nextLabel,
3724
+ backLabel,
3725
+ keyboardShortcuts = false,
3726
+ shortcutOptions,
3727
+ progress,
3728
+ getItemTitle = (item) => getMetaTitle(item.meta) ?? item.id,
3729
+ ...props
3730
+ }) {
3731
+ const ownNavigation = useKeepNavigator({ currentId, initialIndex });
3732
+ const navigation = providedNavigation ?? ownNavigation;
3733
+ const previousLabel = useUiLabel("previousPage", prevLabel);
3734
+ const nextItemLabel = useUiLabel("nextPage", nextLabel);
3735
+ const listLabel = useUiLabel("allItems", backLabel);
3736
+ const { labels } = useKeepUiLabels();
3737
+ const resolvedPrev = onPrev ?? (() => navigateTo(navigation.goToPrev(), prevHref));
3738
+ const resolvedNext = onNext ?? (() => navigateTo(navigation.goToNext(), nextHref));
3739
+ useKeepTourShortcuts({
3740
+ ...shortcutOptions,
3741
+ enabled: keyboardShortcuts && (shortcutOptions?.enabled ?? true),
3742
+ onNext: () => {
3743
+ return resolvedNext();
3744
+ },
3745
+ onPrev: () => {
3746
+ return resolvedPrev();
3747
+ }
3748
+ });
3749
+ return /* @__PURE__ */ jsxs14("nav", { ...props, "data-keepkit": "tour-bar", "aria-label": props["aria-label"] ?? labels.pagination, children: [
3750
+ showProgress ? /* @__PURE__ */ jsx19("span", { "data-keepkit": "tour-progress", "aria-live": "polite", children: progress ?? `${navigation.currentPosition ?? 0} / ${navigation.items.length}` }) : null,
3751
+ /* @__PURE__ */ jsx19(
3752
+ TourAction,
3753
+ {
3754
+ href: prevHref,
3755
+ disabled: !navigation.hasPrev,
3756
+ onClick: prevHref ? onPrev : resolvedPrev,
3757
+ "data-keep-action": "tour-prev",
3758
+ preview: navigation.prevItem ? /* @__PURE__ */ jsxs14(Fragment10, { children: [
3759
+ previousLabel,
3760
+ ": ",
3761
+ getItemTitle(navigation.prevItem)
3762
+ ] }) : void 0,
3763
+ children: previousLabel
3764
+ }
3765
+ ),
3766
+ /* @__PURE__ */ jsx19(
3767
+ TourAction,
3768
+ {
3769
+ href: nextHref,
3770
+ disabled: !navigation.hasNext,
3771
+ onClick: nextHref ? onNext : resolvedNext,
3772
+ "data-keep-action": "tour-next",
3773
+ preview: navigation.nextItem ? /* @__PURE__ */ jsxs14(Fragment10, { children: [
3774
+ nextItemLabel,
3775
+ ": ",
3776
+ getItemTitle(navigation.nextItem)
3777
+ ] }) : void 0,
3778
+ children: nextItemLabel
3779
+ }
3780
+ ),
3781
+ backHref || onBack ? /* @__PURE__ */ jsx19(TourAction, { href: backHref, onClick: onBack, "data-keep-action": "tour-back", children: listLabel }) : null
3782
+ ] });
3783
+ }
3784
+ function TourAction({ href, disabled = false, onClick, children, preview, ...props }) {
3785
+ const previewId = useId();
3786
+ const content = /* @__PURE__ */ jsxs14(Fragment10, { children: [
3787
+ /* @__PURE__ */ jsx19("span", { "data-tour-label": "true", children }),
3788
+ preview ? /* @__PURE__ */ jsx19("small", { id: previewId, "data-tour-preview": "true", children: preview }) : null
3789
+ ] });
3790
+ if (href && !disabled) {
3791
+ return /* @__PURE__ */ jsx19(
3792
+ "a",
3793
+ {
3794
+ ...props,
3795
+ href,
3796
+ onClick: () => void onClick?.(),
3797
+ "aria-label": String(children),
3798
+ "aria-describedby": preview ? previewId : void 0,
3799
+ children: content
3800
+ }
3801
+ );
3802
+ }
3803
+ return /* @__PURE__ */ jsx19(
3804
+ "button",
3805
+ {
3806
+ ...props,
3807
+ type: "button",
3808
+ disabled,
3809
+ onClick: () => void onClick?.(),
3810
+ "aria-label": String(children),
3811
+ "aria-describedby": preview ? previewId : void 0,
3812
+ children: content
3813
+ }
3814
+ );
3815
+ }
3816
+ var KeepNavigator = KeepTourBar;
3817
+ function navigateTo(item, href) {
3818
+ if (!item || !href || typeof window === "undefined") return;
3819
+ window.location.assign(href);
3820
+ }
3821
+
3822
+ // src/features/status/status.tsx
3823
+ import { isValidElement as isValidElement6 } from "react";
3824
+
3825
+ // src/features/status/hooks/useStatusViews.ts
3826
+ import { useKeepContext as useKeepContext4 } from "@keepkit/core/react";
3827
+ import { useEffect as useEffect10, useRef as useRef6, useState as useState12 } from "react";
3828
+ function useKeepEmptyState() {
3829
+ return useUiLabel("noItems").replace(/\.$/, "");
3830
+ }
3831
+ function useKeepStatus(status) {
3832
+ const context = useKeepContext4();
3833
+ const resolvedStatus = status ?? getDerivedStatus(context);
3834
+ const state = {
3835
+ status: resolvedStatus,
3836
+ error: context.error,
3837
+ pendingCount: context.syncState.pendingCount,
3838
+ items: context.items
3839
+ };
3840
+ return { state, defaultLabel: useUiLabel(getStatusLabelKey3(resolvedStatus)) };
3841
+ }
3842
+ function useKeepAnnouncements(messages) {
3843
+ const context = useKeepContext4();
3844
+ const savedMessage = useUiLabel("savedMessage", messages?.save);
3845
+ const removedMessage = useUiLabel("removedMessage", messages?.remove);
3846
+ const noteSavedMessage = useUiLabel("noteSavedMessage", messages?.note);
3847
+ const [message, setMessage] = useState12("");
3848
+ const lastChangeRef = useRef6(void 0);
3849
+ useEffect10(() => {
3850
+ const change = context.lastChange;
3851
+ if (!change || change === lastChangeRef.current) return;
3852
+ lastChangeRef.current = change;
3853
+ if (change.action === "save") setMessage(savedMessage);
3854
+ else if (change.action === "remove" || change.action === "removeBatch") setMessage(removedMessage);
3855
+ else if (change.action === "updateNote") setMessage(noteSavedMessage);
3856
+ }, [context.lastChange, noteSavedMessage, removedMessage, savedMessage]);
3857
+ return message;
3858
+ }
3859
+ function getDerivedStatus(context) {
3860
+ if (context.error) return "error";
3861
+ if (context.syncState.status === "pending" || context.syncState.status === "syncing") return "syncing";
3862
+ if (context.isMutating) return "saving";
3863
+ if (context.isLoading && !context.isHydrated) return "loading";
3864
+ if (context.isHydrated && context.items.length === 0) return "empty";
3865
+ return "idle";
3866
+ }
3867
+ function getStatusLabelKey3(status) {
3868
+ if (status === "empty") return "noItems";
3869
+ if (status === "loading") return "loadingItems";
3870
+ if (status === "error") return "error";
3871
+ if (status === "saving") return "saving";
3872
+ if (status === "syncing") return "syncing";
3873
+ return "saved";
3874
+ }
3875
+
3876
+ // src/features/status/status.tsx
3877
+ import { Fragment as Fragment11, jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
3878
+ function KeepEmptyState({
3879
+ title,
3880
+ description,
3881
+ action,
3882
+ children,
3883
+ asChild = false,
3884
+ className,
3885
+ ...rootProps
3886
+ }) {
3887
+ const defaultTitle = useKeepEmptyState();
3888
+ const contentChildren = asChild && isValidElement6(children) ? void 0 : children;
3889
+ const body = contentChildren ?? /* @__PURE__ */ jsxs15(Fragment11, { children: [
3890
+ /* @__PURE__ */ jsx20("h2", { children: title ?? defaultTitle }),
3891
+ description ? /* @__PURE__ */ jsx20("p", { children: description }) : null,
3892
+ action
3893
+ ] });
3894
+ return renderRoot(
3895
+ asChild,
3896
+ children,
3897
+ { ...rootProps, className, "data-keepkit": "empty-state", "data-state": "empty" },
3898
+ body,
3899
+ "KeepEmptyState"
3900
+ );
3901
+ }
3902
+ function KeepStatus({
3903
+ status,
3904
+ labels,
3905
+ children,
3906
+ render,
3907
+ asChild = false,
3908
+ className,
3909
+ ...rootProps
3910
+ }) {
3911
+ const view = useKeepStatus(status);
3912
+ const contentChildren = asChild && isValidElement6(children) ? void 0 : children;
3913
+ const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? labels?.[view.state.status] ?? view.defaultLabel;
3914
+ const role = rootProps.role ?? (view.state.status === "error" ? "alert" : "status");
3915
+ return renderRoot(
3916
+ asChild,
3917
+ isValidElement6(children) ? children : void 0,
3918
+ {
3919
+ ...rootProps,
3920
+ className,
3921
+ "data-keepkit": "status",
3922
+ role,
3923
+ "aria-live": rootProps["aria-live"] ?? "polite",
3924
+ "data-state": view.state.status,
3925
+ "data-loading": view.state.status === "loading" || view.state.status === "saving" || view.state.status === "syncing" ? "true" : void 0
3926
+ },
3927
+ body,
3928
+ "KeepStatus"
3929
+ );
3930
+ }
3931
+ function KeepAnnouncements({ messages, ...props }) {
3932
+ const message = useKeepAnnouncements(messages);
3933
+ return /* @__PURE__ */ jsx20(
3934
+ "div",
3935
+ {
3936
+ ...props,
3937
+ role: props.role ?? "status",
3938
+ "aria-live": props["aria-live"] ?? "polite",
3939
+ "aria-atomic": "true",
3940
+ "data-keepkit": "announcements",
3941
+ "data-state": "announcing",
3942
+ children: message
3943
+ }
3944
+ );
3945
+ }
3946
+ var KeepAnnouncer = KeepAnnouncements;
3173
3947
 
3174
- // src/hooks/useKeepSyncFeedback.ts
3175
- import { useKeepContext as useKeepContext3 } from "@keepkit/core/react";
3176
- import { useEffect as useEffect5, useRef as useRef5 } from "react";
3948
+ // src/features/sync/hooks/useKeepSyncFeedback.ts
3949
+ import { useKeepContext as useKeepContext5 } from "@keepkit/core/react";
3950
+ import { useEffect as useEffect11, useRef as useRef7 } from "react";
3177
3951
  function useKeepSyncFeedback() {
3178
- const { syncState } = useKeepContext3();
3952
+ const { syncState } = useKeepContext5();
3179
3953
  const emitFeedback = useKeepUiFeedback();
3180
3954
  const completedMessage = useUiLabel("syncSynced");
3181
3955
  const failedMessage = useUiLabel("syncFailedMessage");
3182
- const previousStatus = useRef5("idle");
3183
- useEffect5(() => {
3956
+ const previousStatus = useRef7("idle");
3957
+ useEffect11(() => {
3184
3958
  const previous = previousStatus.current;
3185
3959
  previousStatus.current = syncState.status;
3186
3960
  if (syncState.status === "error" && previous !== "error") {
@@ -3193,24 +3967,24 @@ function useKeepSyncFeedback() {
3193
3967
  }, [completedMessage, emitFeedback, failedMessage, syncState.error, syncState.status]);
3194
3968
  }
3195
3969
 
3196
- // src/KeepSyncFeedbackObserver.tsx
3970
+ // src/features/sync/KeepSyncFeedbackObserver.tsx
3197
3971
  function KeepSyncFeedbackObserver() {
3198
3972
  useKeepSyncFeedback();
3199
3973
  return null;
3200
3974
  }
3201
3975
 
3202
- // src/hooks/useKeepSyncRecoveryDialog.ts
3203
- import { useKeepContext as useKeepContext4 } from "@keepkit/core/react";
3204
- import { useEffect as useEffect6, useState as useState8 } from "react";
3976
+ // src/features/sync/hooks/useKeepSyncRecoveryDialog.ts
3977
+ import { useKeepContext as useKeepContext6 } from "@keepkit/core/react";
3978
+ import { useEffect as useEffect12, useState as useState13 } from "react";
3205
3979
  function useKeepSyncRecoveryDialog(options) {
3206
3980
  const { open, onOpenChange, conflicts, onManualMerge } = options;
3207
- const context = useKeepContext4();
3981
+ const context = useKeepContext6();
3208
3982
  const conflictList = conflicts ?? context.syncState.conflicts ?? [];
3209
3983
  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(() => {
3984
+ const [dismissed, setDismissed] = useState13(false);
3985
+ const [busyId, setBusyId] = useState13();
3986
+ const [error, setError] = useState13();
3987
+ useEffect12(() => {
3214
3988
  if (hasRecovery) setDismissed(false);
3215
3989
  }, [hasRecovery]);
3216
3990
  return {
@@ -3254,8 +4028,8 @@ function useKeepSyncRecoveryDialog(options) {
3254
4028
  };
3255
4029
  }
3256
4030
 
3257
- // src/KeepSyncRecoveryDialog.tsx
3258
- import { jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
4031
+ // src/features/sync/KeepSyncRecoveryDialog.tsx
4032
+ import { jsx as jsx21, jsxs as jsxs16 } from "react/jsx-runtime";
3259
4033
  function KeepSyncRecoveryDialog({
3260
4034
  open,
3261
4035
  onOpenChange,
@@ -3270,7 +4044,7 @@ function KeepSyncRecoveryDialog({
3270
4044
  }) {
3271
4045
  const view = useKeepSyncRecoveryDialog({ open, onOpenChange, conflicts, onManualMerge });
3272
4046
  if (!view.isOpen) return null;
3273
- return /* @__PURE__ */ jsxs11(
4047
+ return /* @__PURE__ */ jsxs16(
3274
4048
  "section",
3275
4049
  {
3276
4050
  ...props,
@@ -3284,17 +4058,17 @@ function KeepSyncRecoveryDialog({
3284
4058
  "data-state": view.conflictList.length > 0 ? "conflict" : "error",
3285
4059
  "data-loading": view.busyId !== void 0 ? "true" : void 0,
3286
4060
  children: [
3287
- /* @__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 })
4061
+ /* @__PURE__ */ jsxs16("header", { children: [
4062
+ /* @__PURE__ */ jsx21("h2", { id: "keepkit-sync-recovery-title", children: title ?? view.labels.title }),
4063
+ /* @__PURE__ */ jsx21("button", { type: "button", "data-keep-action": "close-dialog", onClick: view.close, "aria-label": view.labels.close, children: view.labels.close })
3290
4064
  ] }),
3291
4065
  children,
3292
- view.conflictList.length > 0 ? /* @__PURE__ */ jsxs11("div", { children: [
3293
- /* @__PURE__ */ jsx16("p", { children: view.labels.conflict }),
3294
- 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 }),
3296
- /* @__PURE__ */ jsxs11("div", { "data-conflict-preview": true, children: [
3297
- /* @__PURE__ */ jsx16(
4066
+ view.conflictList.length > 0 ? /* @__PURE__ */ jsxs16("div", { children: [
4067
+ /* @__PURE__ */ jsx21("p", { children: view.labels.conflict }),
4068
+ view.conflictList.map((conflict) => /* @__PURE__ */ jsxs16("article", { "data-conflict-id": conflict.id, children: [
4069
+ /* @__PURE__ */ jsx21("h3", { children: getMetaTitle(conflict.operation.item?.meta) ?? conflict.id }),
4070
+ /* @__PURE__ */ jsxs16("div", { "data-conflict-preview": true, children: [
4071
+ /* @__PURE__ */ jsx21(
3298
4072
  ConflictPreview,
3299
4073
  {
3300
4074
  item: conflict.operation.item,
@@ -3304,7 +4078,7 @@ function KeepSyncRecoveryDialog({
3304
4078
  side: "local"
3305
4079
  }
3306
4080
  ),
3307
- /* @__PURE__ */ jsx16(
4081
+ /* @__PURE__ */ jsx21(
3308
4082
  ConflictPreview,
3309
4083
  {
3310
4084
  item: conflict.remote,
@@ -3315,8 +4089,8 @@ function KeepSyncRecoveryDialog({
3315
4089
  }
3316
4090
  )
3317
4091
  ] }),
3318
- /* @__PURE__ */ jsxs11("div", { children: [
3319
- /* @__PURE__ */ jsx16(
4092
+ /* @__PURE__ */ jsxs16("div", { children: [
4093
+ /* @__PURE__ */ jsx21(
3320
4094
  "button",
3321
4095
  {
3322
4096
  type: "button",
@@ -3326,7 +4100,7 @@ function KeepSyncRecoveryDialog({
3326
4100
  children: view.labels.keepLocal
3327
4101
  }
3328
4102
  ),
3329
- /* @__PURE__ */ jsx16(
4103
+ /* @__PURE__ */ jsx21(
3330
4104
  "button",
3331
4105
  {
3332
4106
  type: "button",
@@ -3336,7 +4110,7 @@ function KeepSyncRecoveryDialog({
3336
4110
  children: view.labels.useServer
3337
4111
  }
3338
4112
  ),
3339
- /* @__PURE__ */ jsx16(
4113
+ /* @__PURE__ */ jsx21(
3340
4114
  "button",
3341
4115
  {
3342
4116
  type: "button",
@@ -3349,12 +4123,12 @@ function KeepSyncRecoveryDialog({
3349
4123
  ] })
3350
4124
  ] }, conflict.id))
3351
4125
  ] }) : null,
3352
- 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)
4126
+ view.showBackupRecovery ? /* @__PURE__ */ jsxs16("section", { "data-recovery": "backup", children: [
4127
+ /* @__PURE__ */ jsx21("h3", { children: view.labels.backupRecovery }),
4128
+ /* @__PURE__ */ jsx21("p", { children: view.labels.backupRecoveryDescription }),
4129
+ backup ?? (showBackupControls ? /* @__PURE__ */ jsx21(KeepBackup, {}) : null)
3356
4130
  ] }) : 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
4131
+ view.error ? /* @__PURE__ */ jsx21("p", { id: "keepkit-sync-recovery-error", role: "alert", "aria-live": "assertive", children: view.error instanceof Error ? view.error.message : view.labels.error }) : null
3358
4132
  ]
3359
4133
  }
3360
4134
  );
@@ -3366,16 +4140,16 @@ function ConflictPreview({
3366
4140
  noteLabel,
3367
4141
  side
3368
4142
  }) {
3369
- return /* @__PURE__ */ jsxs11("article", { "data-conflict-version": side, "aria-label": heading, children: [
3370
- /* @__PURE__ */ jsx16("h4", { children: heading }),
3371
- /* @__PURE__ */ jsxs11("dl", { children: [
3372
- /* @__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" })
4143
+ return /* @__PURE__ */ jsxs16("article", { "data-conflict-version": side, "aria-label": heading, children: [
4144
+ /* @__PURE__ */ jsx21("h4", { children: heading }),
4145
+ /* @__PURE__ */ jsxs16("dl", { children: [
4146
+ /* @__PURE__ */ jsxs16("div", { children: [
4147
+ /* @__PURE__ */ jsx21("dt", { children: updatedAtLabel }),
4148
+ /* @__PURE__ */ jsx21("dd", { children: item ? /* @__PURE__ */ jsx21("time", { dateTime: new Date(item.updatedAt).toISOString(), children: formatConflictDate(item.updatedAt) }) : "\u2014" })
3375
4149
  ] }),
3376
- /* @__PURE__ */ jsxs11("div", { children: [
3377
- /* @__PURE__ */ jsx16("dt", { children: noteLabel }),
3378
- /* @__PURE__ */ jsx16("dd", { children: item?.note || "\u2014" })
4150
+ /* @__PURE__ */ jsxs16("div", { children: [
4151
+ /* @__PURE__ */ jsx21("dt", { children: noteLabel }),
4152
+ /* @__PURE__ */ jsx21("dd", { children: item?.note || "\u2014" })
3379
4153
  ] })
3380
4154
  ] })
3381
4155
  ] });
@@ -3384,10 +4158,10 @@ function formatConflictDate(timestamp) {
3384
4158
  return new Date(timestamp).toISOString().slice(0, 10);
3385
4159
  }
3386
4160
 
3387
- // src/hooks/useKeepSyncStatusBanner.ts
3388
- import { useKeepContext as useKeepContext5 } from "@keepkit/core/react";
4161
+ // src/features/sync/hooks/useKeepSyncStatusBanner.ts
4162
+ import { useKeepContext as useKeepContext7 } from "@keepkit/core/react";
3389
4163
  function useKeepSyncStatusBanner({ onRetry, children }) {
3390
- const context = useKeepContext5();
4164
+ const context = useKeepContext7();
3391
4165
  const retryLabel = useUiLabel("retrySync");
3392
4166
  const resolveLabel = useUiLabel("resolveSync");
3393
4167
  const conflictLabel = useUiLabel("syncConflict");
@@ -3395,7 +4169,7 @@ function useKeepSyncStatusBanner({ onRetry, children }) {
3395
4169
  const syncedLabel = useUiLabel("syncSynced");
3396
4170
  const status = context.syncState.status;
3397
4171
  const hasConflicts = (context.syncState.conflicts?.length ?? 0) > 0 || context.syncState.conflictIds.length > 0;
3398
- const message = children ?? (status === "error" ? getErrorMessage4(context.syncState.error) : status === "conflict" || hasConflicts ? conflictLabel : status === "pending" || status === "syncing" ? pendingLabel : syncedLabel);
4172
+ const message = children ?? (status === "error" ? getErrorMessage5(context.syncState.error) : status === "conflict" || hasConflicts ? conflictLabel : status === "pending" || status === "syncing" ? pendingLabel : syncedLabel);
3399
4173
  return {
3400
4174
  status,
3401
4175
  hasConflicts,
@@ -3414,12 +4188,12 @@ function useKeepSyncStatusBanner({ onRetry, children }) {
3414
4188
  }
3415
4189
  };
3416
4190
  }
3417
- function getErrorMessage4(error) {
4191
+ function getErrorMessage5(error) {
3418
4192
  return error instanceof Error ? error.message : "Sync failed.";
3419
4193
  }
3420
4194
 
3421
- // src/KeepSyncStatusBanner.tsx
3422
- import { jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
4195
+ // src/features/sync/KeepSyncStatusBanner.tsx
4196
+ import { jsx as jsx22, jsxs as jsxs17 } from "react/jsx-runtime";
3423
4197
  function KeepSyncStatusBanner({
3424
4198
  onRetry,
3425
4199
  onResolveConflicts,
@@ -3429,7 +4203,7 @@ function KeepSyncStatusBanner({
3429
4203
  }) {
3430
4204
  const view = useKeepSyncStatusBanner({ onRetry, children });
3431
4205
  if (view.status === "idle" && !view.hasConflicts) return null;
3432
- return /* @__PURE__ */ jsxs12(
4206
+ return /* @__PURE__ */ jsxs17(
3433
4207
  "aside",
3434
4208
  {
3435
4209
  ...props,
@@ -3439,8 +4213,8 @@ function KeepSyncStatusBanner({
3439
4213
  "data-keepkit": "sync-status",
3440
4214
  "data-state": view.status,
3441
4215
  children: [
3442
- /* @__PURE__ */ jsx17("p", { children: view.message }),
3443
- view.showRetry ? /* @__PURE__ */ jsx17(
4216
+ /* @__PURE__ */ jsx22("p", { children: view.message }),
4217
+ view.showRetry ? /* @__PURE__ */ jsx22(
3444
4218
  "button",
3445
4219
  {
3446
4220
  type: "button",
@@ -3450,7 +4224,7 @@ function KeepSyncStatusBanner({
3450
4224
  children: view.retryLabel
3451
4225
  }
3452
4226
  ) : null,
3453
- view.hasConflicts ? /* @__PURE__ */ jsx17(
4227
+ view.hasConflicts ? /* @__PURE__ */ jsx22(
3454
4228
  "button",
3455
4229
  {
3456
4230
  type: "button",
@@ -3465,273 +4239,9 @@ function KeepSyncStatusBanner({
3465
4239
  );
3466
4240
  }
3467
4241
 
3468
- // src/hooks/useKeepTagEditor.ts
3469
- import { useKeepItem as useKeepItem4 } from "@keepkit/core/react";
3470
- import { useCallback as useCallback5, useEffect as useEffect7, useState as useState9 } from "react";
3471
- function useKeepTagEditor({ item, onSaved, onSaveError }) {
3472
- 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 () => {
3477
- const nextTags = normalizeUiTags(tags);
3478
- try {
3479
- await itemState.updateTags(nextTags);
3480
- setTags(nextTags);
3481
- onSaved?.(nextTags);
3482
- } catch (cause) {
3483
- onSaveError?.(cause);
3484
- throw cause;
3485
- }
3486
- }, [itemState, onSaveError, onSaved, tags]);
3487
- const addTag = (tag) => {
3488
- setTags(normalizeUiTags([...tags, tag]));
3489
- setInput("");
3490
- };
3491
- const state = { tags, setTags, save, isSaving: itemState.isMutating };
3492
- return {
3493
- state,
3494
- input,
3495
- setInput,
3496
- error: itemState.error,
3497
- handleInputKeyDown: (event) => {
3498
- if (event.key === "Enter") {
3499
- if (event.nativeEvent.isComposing) return;
3500
- event.preventDefault();
3501
- if (input.trim()) addTag(input);
3502
- } else if (event.key === "Backspace" && input.length === 0 && tags.length > 0) {
3503
- event.preventDefault();
3504
- setTags(tags.slice(0, -1));
3505
- }
3506
- },
3507
- removeTag: (tag) => setTags(tags.filter((current) => current !== tag)),
3508
- submit: (event) => {
3509
- event.preventDefault();
3510
- void save().catch(() => void 0);
3511
- },
3512
- labels: {
3513
- tags: useUiLabel("tagsToApply"),
3514
- remove: useUiLabel("remove"),
3515
- apply: useUiLabel("applyTags"),
3516
- error: useUiLabel("error")
3517
- }
3518
- };
3519
- }
3520
-
3521
- // src/KeepTagEditor.tsx
3522
- import { Fragment as Fragment7, jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
3523
- function KeepTagEditor({
3524
- item,
3525
- availableTags = [],
3526
- onSaved,
3527
- onSaveError,
3528
- render,
3529
- ...props
3530
- }) {
3531
- const view = useKeepTagEditor({ item, onSaved, onSaveError });
3532
- const { isSaving, tags } = view.state;
3533
- const body = render ? render(view.state) : /* @__PURE__ */ jsxs13(Fragment7, { children: [
3534
- /* @__PURE__ */ jsxs13("label", { children: [
3535
- view.labels.tags,
3536
- /* @__PURE__ */ jsx18(
3537
- "input",
3538
- {
3539
- "data-keep-action": "edit-tags",
3540
- value: view.input,
3541
- list: availableTags.length > 0 ? `keep-tags-${item.id}` : void 0,
3542
- onChange: (event) => view.setInput(event.currentTarget.value),
3543
- onKeyDown: view.handleInputKeyDown
3544
- }
3545
- )
3546
- ] }),
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: [
3549
- tag,
3550
- /* @__PURE__ */ jsx18("button", { type: "button", "data-keep-action": "remove-tag", onClick: () => view.removeTag(tag), children: view.labels.remove })
3551
- ] }, tag)) }),
3552
- /* @__PURE__ */ jsx18("button", { type: "submit", "data-keep-action": "apply-tags", disabled: isSaving, "aria-busy": isSaving, children: view.labels.apply })
3553
- ] });
3554
- return /* @__PURE__ */ jsxs13(
3555
- "form",
3556
- {
3557
- ...props,
3558
- onSubmit: view.submit,
3559
- "aria-busy": isSaving || props["aria-busy"],
3560
- "data-keepkit": "tag-editor",
3561
- "data-state": view.error ? "error" : isSaving ? "saving" : "idle",
3562
- "data-loading": isSaving ? "true" : void 0,
3563
- "data-disabled": isSaving ? "true" : void 0,
3564
- children: [
3565
- body,
3566
- view.error ? /* @__PURE__ */ jsx18("p", { role: "alert", children: getErrorMessage5(view.error, view.labels.error) }) : null
3567
- ]
3568
- }
3569
- );
3570
- }
3571
- function getErrorMessage5(error, fallback) {
3572
- return error instanceof Error ? error.message : fallback;
3573
- }
3574
-
3575
- // src/hooks/useKeepUndo.ts
3576
- import { useKeepContext as useKeepContext6 } from "@keepkit/core/react";
3577
- function useKeepUndo() {
3578
- const context = useKeepContext6();
3579
- const emitFeedback = useKeepUiFeedback();
3580
- const restoredMessage = useUiLabel("restoredMessage");
3581
- return {
3582
- canUndo: context.undo.canUndo,
3583
- undo: async () => {
3584
- const items = context.lastChange?.items ?? (context.lastChange?.item ? [context.lastChange.item] : []);
3585
- await context.undoLastRemoval();
3586
- if (items.length > 0) {
3587
- emitFeedback({ type: "item-restored", item: items[0], items, message: restoredMessage });
3588
- }
3589
- },
3590
- message: useUiLabel("undoAvailable"),
3591
- label: useUiLabel("undo")
3592
- };
3593
- }
3594
-
3595
- // src/KeepUndo.tsx
3596
- import { jsx as jsx19, jsxs as jsxs14 } from "react/jsx-runtime";
3597
- function KeepUndo({ children, label, ...props }) {
3598
- const view = useKeepUndo();
3599
- if (!view.canUndo) return null;
3600
- return /* @__PURE__ */ jsxs14("div", { ...props, role: "status", "aria-live": "polite", "data-keepkit": "undo", "data-state": "available", children: [
3601
- children ?? view.message,
3602
- /* @__PURE__ */ jsx19("button", { type: "button", "data-keep-action": "undo", onClick: () => void view.undo(), children: label ?? view.label })
3603
- ] });
3604
- }
3605
-
3606
- // src/status.tsx
3607
- import { isValidElement as isValidElement6 } from "react";
3608
-
3609
- // src/hooks/useStatusViews.ts
3610
- import { useKeepContext as useKeepContext7 } from "@keepkit/core/react";
3611
- import { useEffect as useEffect8, useRef as useRef6, useState as useState10 } from "react";
3612
- function useKeepEmptyState() {
3613
- return useUiLabel("noItems").replace(/\.$/, "");
3614
- }
3615
- function useKeepStatus(status) {
3616
- const context = useKeepContext7();
3617
- const resolvedStatus = status ?? getDerivedStatus(context);
3618
- const state = {
3619
- status: resolvedStatus,
3620
- error: context.error,
3621
- pendingCount: context.syncState.pendingCount,
3622
- items: context.items
3623
- };
3624
- return { state, defaultLabel: useUiLabel(getStatusLabelKey3(resolvedStatus)) };
3625
- }
3626
- function useKeepAnnouncements(messages) {
3627
- const context = useKeepContext7();
3628
- const savedMessage = useUiLabel("savedMessage", messages?.save);
3629
- const removedMessage = useUiLabel("removedMessage", messages?.remove);
3630
- const noteSavedMessage = useUiLabel("noteSavedMessage", messages?.note);
3631
- const [message, setMessage] = useState10("");
3632
- const lastChangeRef = useRef6(void 0);
3633
- useEffect8(() => {
3634
- const change = context.lastChange;
3635
- if (!change || change === lastChangeRef.current) return;
3636
- lastChangeRef.current = change;
3637
- if (change.action === "save") setMessage(savedMessage);
3638
- else if (change.action === "remove" || change.action === "removeBatch") setMessage(removedMessage);
3639
- else if (change.action === "updateNote") setMessage(noteSavedMessage);
3640
- }, [context.lastChange, noteSavedMessage, removedMessage, savedMessage]);
3641
- return message;
3642
- }
3643
- function getDerivedStatus(context) {
3644
- if (context.error) return "error";
3645
- if (context.syncState.status === "pending" || context.syncState.status === "syncing") return "syncing";
3646
- if (context.isMutating) return "saving";
3647
- if (context.isLoading && !context.isHydrated) return "loading";
3648
- if (context.isHydrated && context.items.length === 0) return "empty";
3649
- return "idle";
3650
- }
3651
- function getStatusLabelKey3(status) {
3652
- if (status === "empty") return "noItems";
3653
- if (status === "loading") return "loadingItems";
3654
- if (status === "error") return "error";
3655
- if (status === "saving") return "saving";
3656
- if (status === "syncing") return "syncing";
3657
- return "saved";
3658
- }
3659
-
3660
- // src/status.tsx
3661
- import { Fragment as Fragment8, jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
3662
- function KeepEmptyState({
3663
- title,
3664
- description,
3665
- action,
3666
- children,
3667
- asChild = false,
3668
- className,
3669
- ...rootProps
3670
- }) {
3671
- const defaultTitle = useKeepEmptyState();
3672
- 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,
3676
- action
3677
- ] });
3678
- return renderRoot(
3679
- asChild,
3680
- children,
3681
- { ...rootProps, className, "data-keepkit": "empty-state", "data-state": "empty" },
3682
- body,
3683
- "KeepEmptyState"
3684
- );
3685
- }
3686
- function KeepStatus({
3687
- status,
3688
- labels,
3689
- children,
3690
- render,
3691
- asChild = false,
3692
- className,
3693
- ...rootProps
3694
- }) {
3695
- const view = useKeepStatus(status);
3696
- const contentChildren = asChild && isValidElement6(children) ? void 0 : children;
3697
- const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? labels?.[view.state.status] ?? view.defaultLabel;
3698
- const role = rootProps.role ?? (view.state.status === "error" ? "alert" : "status");
3699
- return renderRoot(
3700
- asChild,
3701
- isValidElement6(children) ? children : void 0,
3702
- {
3703
- ...rootProps,
3704
- className,
3705
- "data-keepkit": "status",
3706
- role,
3707
- "aria-live": rootProps["aria-live"] ?? "polite",
3708
- "data-state": view.state.status,
3709
- "data-loading": view.state.status === "loading" || view.state.status === "saving" || view.state.status === "syncing" ? "true" : void 0
3710
- },
3711
- body,
3712
- "KeepStatus"
3713
- );
3714
- }
3715
- function KeepAnnouncements({ messages, ...props }) {
3716
- const message = useKeepAnnouncements(messages);
3717
- return /* @__PURE__ */ jsx20(
3718
- "div",
3719
- {
3720
- ...props,
3721
- role: props.role ?? "status",
3722
- "aria-live": props["aria-live"] ?? "polite",
3723
- "aria-atomic": "true",
3724
- "data-keepkit": "announcements",
3725
- "data-state": "announcing",
3726
- children: message
3727
- }
3728
- );
3729
- }
3730
- var KeepAnnouncer = KeepAnnouncements;
3731
-
3732
- // src/theme.tsx
4242
+ // src/foundation/theme.tsx
3733
4243
  import { cloneElement as cloneElement2, isValidElement as isValidElement7 } from "react";
3734
- import { jsx as jsx21 } from "react/jsx-runtime";
4244
+ import { jsx as jsx23 } from "react/jsx-runtime";
3735
4245
  var keepThemeNames = [
3736
4246
  "default",
3737
4247
  "ocean",
@@ -3790,7 +4300,7 @@ function KeepThemeProvider({
3790
4300
  throw new Error("KeepThemeProvider with asChild requires a single React element child.");
3791
4301
  return cloneElement2(children, rootProps);
3792
4302
  }
3793
- return /* @__PURE__ */ jsx21("div", { ...rootProps, children });
4303
+ return /* @__PURE__ */ jsx23("div", { ...rootProps, children });
3794
4304
  }
3795
4305
 
3796
4306
  // src/index.tsx
@@ -3801,6 +4311,7 @@ import {
3801
4311
  useKeepContext as useKeepContext8,
3802
4312
  useKeepItem as useKeepItem5,
3803
4313
  useKeepList as useKeepList5,
4314
+ useKeepNavigator as useKeepNavigator2,
3804
4315
  useKeepShortcut
3805
4316
  } from "@keepkit/core/react";
3806
4317
  import {
@@ -3813,7 +4324,7 @@ import {
3813
4324
  LocalStorageSyncQueueAdapter,
3814
4325
  SyncStorageAdapter
3815
4326
  } from "@keepkit/core/storage";
3816
- import { jsx as jsx22, jsxs as jsxs16 } from "react/jsx-runtime";
4327
+ import { jsx as jsx24, jsxs as jsxs18 } from "react/jsx-runtime";
3817
4328
  function KeepKitProvider({
3818
4329
  labels,
3819
4330
  locale,
@@ -3833,7 +4344,7 @@ function KeepKitProvider({
3833
4344
  children,
3834
4345
  ...providerProps
3835
4346
  }) {
3836
- return /* @__PURE__ */ jsx22(KeepUiProvider, { labels, locale, labelResolver, onFeedback, children: /* @__PURE__ */ jsx22(
4347
+ return /* @__PURE__ */ jsx24(KeepUiProvider, { labels, locale, labelResolver, onFeedback, children: /* @__PURE__ */ jsx24(
3837
4348
  KeepThemeProvider,
3838
4349
  {
3839
4350
  theme,
@@ -3847,10 +4358,10 @@ function KeepKitProvider({
3847
4358
  className: themeClassName,
3848
4359
  style: themeStyle,
3849
4360
  asChild: themeAsChild,
3850
- children: /* @__PURE__ */ jsxs16(CoreKeepProvider, { ...providerProps, children: [
3851
- /* @__PURE__ */ jsx22(KeepSyncFeedbackObserver, {}),
4361
+ children: /* @__PURE__ */ jsxs18(CoreKeepProvider, { ...providerProps, children: [
4362
+ /* @__PURE__ */ jsx24(KeepSyncFeedbackObserver, {}),
3852
4363
  children,
3853
- /* @__PURE__ */ jsx22(KeepAnnouncements, {})
4364
+ /* @__PURE__ */ jsx24(KeepAnnouncements, {})
3854
4365
  ] })
3855
4366
  }
3856
4367
  ) });
@@ -3877,7 +4388,7 @@ function createKeepKit(options = {}) {
3877
4388
  } = options;
3878
4389
  const coreKit = createCoreKeepKit(coreOptions);
3879
4390
  return {
3880
- Provider: (props) => /* @__PURE__ */ jsx22(
4391
+ Provider: (props) => /* @__PURE__ */ jsx24(
3881
4392
  KeepKitProvider,
3882
4393
  {
3883
4394
  ...coreOptions,
@@ -3898,9 +4409,9 @@ function createKeepKit(options = {}) {
3898
4409
  ...props
3899
4410
  }
3900
4411
  ),
3901
- Button: (props) => /* @__PURE__ */ jsx22(KeepButton, { ...props }),
3902
- Backup: (props) => /* @__PURE__ */ jsx22(KeepBackup, { ...props }),
3903
- Collection: (props) => /* @__PURE__ */ jsx22(
4412
+ Button: (props) => /* @__PURE__ */ jsx24(KeepButton, { ...props }),
4413
+ Backup: (props) => /* @__PURE__ */ jsx24(KeepBackup, { ...props }),
4414
+ Collection: (props) => /* @__PURE__ */ jsx24(
3904
4415
  KeepCollection,
3905
4416
  {
3906
4417
  ...props,
@@ -3914,6 +4425,7 @@ function createKeepKit(options = {}) {
3914
4425
  useContext: () => coreKit.useContext(),
3915
4426
  useItem: (item) => coreKit.useItem(item),
3916
4427
  useList: (query) => coreKit.useList(query),
4428
+ useNavigator: (navigatorOptions) => coreKit.useNavigator(navigatorOptions),
3917
4429
  useShortcut: (shortcutOptions) => coreKit.useShortcut(shortcutOptions)
3918
4430
  };
3919
4431
  }
@@ -3929,6 +4441,7 @@ export {
3929
4441
  KeepCollection,
3930
4442
  KeepEmptyState,
3931
4443
  KeepErrorBoundary3 as KeepErrorBoundary,
4444
+ KeepHighlight,
3932
4445
  KeepItemCard,
3933
4446
  KeepItemCardSkeleton,
3934
4447
  KeepItemCheckbox,
@@ -3936,10 +4449,12 @@ export {
3936
4449
  KeepKitProvider,
3937
4450
  KeepLayout,
3938
4451
  KeepList,
4452
+ KeepNavigator,
3939
4453
  KeepNoteEditor,
3940
4454
  KeepPagination,
3941
4455
  KeepProvider,
3942
4456
  KeepPruneStaleButton,
4457
+ KeepReorderableList,
3943
4458
  KeepSearchInput,
3944
4459
  KeepSortSelect,
3945
4460
  KeepStaleNotice,
@@ -3949,6 +4464,7 @@ export {
3949
4464
  KeepTagEditor,
3950
4465
  KeepTagFilter,
3951
4466
  KeepThemeProvider,
4467
+ KeepTourBar,
3952
4468
  KeepUiProvider,
3953
4469
  KeepUndo,
3954
4470
  LocalStorageAdapter,
@@ -3960,14 +4476,17 @@ export {
3960
4476
  createNextPagesRouterAdapter,
3961
4477
  createStorageAdapter,
3962
4478
  getKeepLocaleLabels,
4479
+ highlightText,
3963
4480
  isAllSelected,
3964
4481
  keepThemeNames,
3965
4482
  toggleSelectAll,
3966
4483
  useKeepContext8 as useKeepContext,
3967
4484
  useKeepItem5 as useKeepItem,
3968
4485
  useKeepList5 as useKeepList,
4486
+ useKeepNavigator2 as useKeepNavigator,
3969
4487
  useKeepShortcut,
3970
4488
  useKeepToastFeedback,
4489
+ useKeepTourShortcuts,
3971
4490
  useKeepUiLabels,
3972
4491
  useKeepUrlSync
3973
4492
  };