@keepkit/ui 0.17.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,11 +1595,7 @@ 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
1601
  createContext as createContext2,
@@ -1614,7 +1672,9 @@ function renderRoot(asChild, child, props, body, componentName) {
1614
1672
  return /* @__PURE__ */ jsx3("div", { ...props, children: body });
1615
1673
  }
1616
1674
 
1617
- // 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";
1618
1678
  function isAllSelected(items, selectedIds) {
1619
1679
  if (items.length === 0) return false;
1620
1680
  const selected = new Set(selectedIds);
@@ -1707,7 +1767,7 @@ function useKeepBulkActions(options) {
1707
1767
  };
1708
1768
  }
1709
1769
 
1710
- // src/KeepItemCheckbox.tsx
1770
+ // src/features/actions/KeepItemCheckbox.tsx
1711
1771
  import { jsx as jsx4 } from "react/jsx-runtime";
1712
1772
  function KeepItemCheckbox({
1713
1773
  item,
@@ -1740,7 +1800,7 @@ function getItemLabel(item) {
1740
1800
  return typeof title === "string" && title.trim() ? title.trim() : void 0;
1741
1801
  }
1742
1802
 
1743
- // src/KeepBulkActions.tsx
1803
+ // src/features/actions/KeepBulkActions.tsx
1744
1804
  import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
1745
1805
  function KeepBulkActions({
1746
1806
  query,
@@ -1855,13 +1915,13 @@ function KeepBulkActions({
1855
1915
  );
1856
1916
  }
1857
1917
 
1858
- // src/KeepButton.tsx
1918
+ // src/features/actions/KeepButton.tsx
1859
1919
  import {
1860
1920
  KeepButton as CoreKeepButton
1861
1921
  } from "@keepkit/core/react";
1862
- import { createElement, useEffect, useRef as useRef2 } from "react";
1922
+ import { createElement, useEffect as useEffect2, useRef as useRef3 } from "react";
1863
1923
 
1864
- // src/hooks/useKeepButton.ts
1924
+ // src/features/actions/hooks/useKeepButton.ts
1865
1925
  import { useKeepItem } from "@keepkit/core/react";
1866
1926
  function useKeepButton({ item, labels, icons, children }) {
1867
1927
  return {
@@ -1881,7 +1941,7 @@ function useKeepButton({ item, labels, icons, children }) {
1881
1941
  };
1882
1942
  }
1883
1943
 
1884
- // src/KeepButton.tsx
1944
+ // src/features/actions/KeepButton.tsx
1885
1945
  import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
1886
1946
  function KeepButton({
1887
1947
  labels,
@@ -1894,8 +1954,8 @@ function KeepButton({
1894
1954
  ...props
1895
1955
  }) {
1896
1956
  const view = useKeepButton({ item: props.item, labels, icons, children: props.children });
1897
- const pendingToggle = useRef2(null);
1898
- useEffect(() => {
1957
+ const pendingToggle = useRef3(null);
1958
+ useEffect2(() => {
1899
1959
  const pending = pendingToggle.current;
1900
1960
  if (!pending || view.buttonState.isMutating || pending.wasSaved === view.buttonState.isSaved) return;
1901
1961
  pendingToggle.current = null;
@@ -1962,130 +2022,390 @@ function renderIcon(icon, className) {
1962
2022
  return icon ?? null;
1963
2023
  }
1964
2024
 
1965
- // src/KeepCollection.tsx
1966
- import { KeepErrorBoundary as KeepErrorBoundary2 } from "@keepkit/core/react";
1967
-
1968
- // src/hooks/useKeepCollection.ts
1969
- import { useKeepList as useKeepList2 } from "@keepkit/core/react";
1970
- import { useMemo as useMemo2, useState as useState3 } from "react";
1971
-
1972
- // src/url-sync.tsx
1973
- import {
1974
- DEFAULT_KEEP_URL_PARAMS,
1975
- decodeKeepListQuery,
1976
- encodeKeepListQuery
1977
- } from "@keepkit/core/core";
1978
- import { useEffect as useEffect2, useRef as useRef3 } from "react";
1979
- function createNextPagesRouterAdapter(router) {
1980
- 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));
1981
2044
  return {
1982
- getUrl,
1983
- subscribe: router.events ? (listener) => {
1984
- router.events?.on("routeChangeComplete", listener);
1985
- return () => router.events?.off("routeChangeComplete", listener);
1986
- } : void 0,
1987
- navigate: (url, mode) => {
1988
- void router[mode](url, void 0, { shallow: true });
1989
- }
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")
1990
2059
  };
1991
2060
  }
1992
- function useKeepUrlSync({
1993
- enabled = true,
1994
- query,
1995
- onQueryChange,
1996
- options = {},
1997
- adapter: providedAdapter
1998
- }) {
1999
- const browserAdapterRef = useRef3(getBrowserAdapter());
2000
- const adapter = providedAdapter ?? browserAdapterRef.current;
2001
- const onQueryChangeRef = useRef3(onQueryChange);
2002
- onQueryChangeRef.current = onQueryChange;
2003
- const skipWriteRef = useRef3(true);
2004
- const params = options.params;
2005
- useEffect2(() => {
2006
- if (!enabled) return;
2007
- const read = () => {
2008
- const url = adapter.getUrl();
2009
- const decoded = decodeKeepListQuery(url, { params });
2010
- skipWriteRef.current = true;
2011
- onQueryChangeRef.current((previousQuery) => ({
2012
- ...previousQuery,
2013
- ...decoded.search ? { search: decoded.search } : { search: void 0 },
2014
- ...decoded.tags ? { tags: decoded.tags } : { tags: void 0 },
2015
- ...decoded.sort ? { sort: decoded.sort } : {},
2016
- ...decoded.pagination ? { pagination: { ...previousQuery.pagination, ...decoded.pagination } } : {}
2017
- }));
2018
- };
2019
- read();
2020
- return adapter.subscribe?.(read);
2021
- }, [adapter, enabled, params]);
2022
- useEffect2(() => {
2023
- if (!enabled) return;
2024
- if (skipWriteRef.current) {
2025
- skipWriteRef.current = false;
2026
- return;
2027
- }
2028
- const currentUrl = new URL(adapter.getUrl(), "http://keepkit.invalid");
2029
- const urlParams = { ...DEFAULT_KEEP_URL_PARAMS, ...params };
2030
- for (const key of Object.values(urlParams)) currentUrl.searchParams.delete(key);
2031
- const nextParams = encodeKeepListQuery(query, { params });
2032
- nextParams.forEach((value, key) => {
2033
- currentUrl.searchParams.append(key, value);
2034
- });
2035
- const nextUrl = `${currentUrl.pathname}${currentUrl.search}${currentUrl.hash}`;
2036
- adapter.navigate(nextUrl, options.history ?? "push");
2037
- }, [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
+ ] });
2038
2085
  }
2039
- function getBrowserAdapter() {
2040
- return {
2041
- getUrl: () => typeof window === "undefined" ? "/" : window.location.href,
2042
- subscribe: (listener) => {
2043
- if (typeof window === "undefined") return () => void 0;
2044
- window.addEventListener("popstate", listener);
2045
- 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);
2046
2109
  },
2047
- navigate: (url, mode) => {
2048
- if (typeof window === "undefined") return;
2049
- window.history[mode === "push" ? "pushState" : "replaceState"]({}, "", url);
2050
- }
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") }
2051
2120
  };
2052
2121
  }
2053
2122
 
2054
- // src/hooks/useKeepCollection.ts
2055
- function useKeepCollection({
2123
+ // src/features/query/KeepTagFilter.tsx
2124
+ import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
2125
+ function KeepTagFilter({
2056
2126
  query,
2057
- pageSize,
2058
- urlSync,
2059
- urlAdapter,
2060
- 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
2061
2139
  }) {
2062
- const enabled = {
2063
- search: true,
2064
- sort: true,
2065
- pagination: true,
2066
- tagFilter: false,
2067
- bulkActions: false,
2068
- ...features
2069
- };
2070
- const [searchValue, setSearchValue] = useState3(query.search?.query ?? "");
2071
- const [sort, setSort] = useState3(query.sort ?? { by: "updatedAt", direction: "desc" });
2072
- const [tag, setTag] = useState3(query.tags?.[0]);
2073
- const [page, setPage] = useState3(query.pagination?.page ?? 1);
2074
- const resolvedPageSize = query.pagination?.pageSize ?? pageSize;
2075
- const resolvedQuery = useMemo2(
2076
- () => ({
2077
- ...query,
2078
- search: enabled.search ? { ...query.search, query: searchValue } : query.search,
2079
- sort: enabled.sort ? sort : query.sort,
2080
- tags: tag ? [.../* @__PURE__ */ new Set([...query.tags ?? [], tag])] : query.tags,
2081
- pagination: enabled.pagination ? { ...query.pagination, page, pageSize: resolvedPageSize } : query.pagination
2082
- }),
2083
- [enabled.pagination, enabled.search, enabled.sort, page, query, resolvedPageSize, searchValue, sort, tag]
2084
- );
2085
- useKeepUrlSync({
2086
- enabled: Boolean(urlSync),
2087
- query: resolvedQuery,
2088
- onQueryChange: (nextOrUpdater) => {
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,
2400
+ tags: tag ? [.../* @__PURE__ */ new Set([...query.tags ?? [], tag])] : query.tags,
2401
+ pagination: enabled.pagination ? { ...query.pagination, page, pageSize: resolvedPageSize } : query.pagination
2402
+ }),
2403
+ [enabled.pagination, enabled.search, enabled.sort, page, query, resolvedPageSize, searchValue, sort, tag]
2404
+ );
2405
+ useKeepUrlSync({
2406
+ enabled: Boolean(urlSync),
2407
+ query: resolvedQuery,
2408
+ onQueryChange: (nextOrUpdater) => {
2089
2409
  const next = typeof nextOrUpdater === "function" ? nextOrUpdater(resolvedQuery) : nextOrUpdater;
2090
2410
  setSearchValue(next.search?.query ?? "");
2091
2411
  setSort(next.sort ?? { by: "updatedAt", direction: "desc" });
@@ -2095,7 +2415,7 @@ function useKeepCollection({
2095
2415
  options: typeof urlSync === "object" ? urlSync : {},
2096
2416
  adapter: urlAdapter
2097
2417
  });
2098
- const list = useKeepList2(resolvedQuery);
2418
+ const list = useKeepList3(resolvedQuery);
2099
2419
  return {
2100
2420
  enabled,
2101
2421
  searchValue,
@@ -2120,175 +2440,38 @@ function useKeepCollection({
2120
2440
  };
2121
2441
  }
2122
2442
 
2123
- // src/KeepList.tsx
2443
+ // src/features/collection/KeepList.tsx
2124
2444
  import { KeepErrorBoundary } from "@keepkit/core/react";
2125
- import { isValidElement as isValidElement3 } from "react";
2445
+ import { isValidElement as isValidElement4 } from "react";
2126
2446
 
2127
- // src/hooks/useKeepListView.ts
2128
- import { useKeepList as useKeepList3 } from "@keepkit/core/react";
2129
- function useKeepListView(query) {
2447
+ // src/features/item/KeepItemCard.tsx
2448
+ import {
2449
+ createContext as createContext3,
2450
+ createElement as createElement2,
2451
+ isValidElement as isValidElement3,
2452
+ useContext as useContext3,
2453
+ useEffect as useEffect5,
2454
+ useState as useState8
2455
+ } from "react";
2456
+
2457
+ // src/features/item/hooks/useKeepItemStatusBadge.ts
2458
+ function useKeepItemStatusBadge(status) {
2130
2459
  return {
2131
- state: useKeepList3(query),
2132
- labels: {
2133
- loading: useUiLabel("loadingItems"),
2134
- empty: useUiLabel("noItems"),
2135
- error: useUiLabel("errorItems")
2136
- }
2460
+ resolvedStatus: getDisplayStatus(status),
2461
+ statusLabel: useUiLabel(getStatusLabelKey(status)),
2462
+ icon: getStatusIcon(status)
2137
2463
  };
2138
2464
  }
2139
-
2140
- // src/hooks/useRovingTabIndex.ts
2141
- import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef4 } from "react";
2142
- function useRovingTabIndex() {
2143
- const ref = useRef4(null);
2144
- const getItems = useCallback3(() => {
2145
- const root = ref.current;
2146
- if (!root) return [];
2147
- return Array.from(root.querySelectorAll('[data-keepkit="card"]')).filter(
2148
- (item) => item.getAttribute("aria-hidden") !== "true" && item.getAttribute("data-roving-disabled") !== "true"
2149
- );
2150
- }, []);
2151
- const syncTabIndices = useCallback3(() => {
2152
- const items = getItems();
2153
- if (items.length === 0) return;
2154
- const activeElement = document.activeElement;
2155
- const activeItem = items.find((item) => item === activeElement || item.contains(activeElement));
2156
- const activeIndex = activeItem ? items.indexOf(activeItem) : 0;
2157
- items.forEach((item, index) => {
2158
- item.tabIndex = index === activeIndex ? 0 : -1;
2159
- });
2160
- }, [getItems]);
2161
- useEffect3(() => {
2162
- const root = ref.current;
2163
- if (!root) return;
2164
- syncTabIndices();
2165
- const observer = new MutationObserver(syncTabIndices);
2166
- observer.observe(root, { childList: true, subtree: true });
2167
- return () => observer.disconnect();
2168
- }, [syncTabIndices]);
2169
- const onFocusCapture = useCallback3(
2170
- (event) => {
2171
- if (!(event.target instanceof HTMLElement)) return;
2172
- const item = event.target.closest('[data-keepkit="card"]');
2173
- if (!item || !ref.current?.contains(item)) return;
2174
- getItems().forEach((candidate) => {
2175
- candidate.tabIndex = candidate === item ? 0 : -1;
2176
- });
2177
- },
2178
- [getItems]
2179
- );
2180
- const onKeyDown = useCallback3(
2181
- (event) => {
2182
- if (event.defaultPrevented) return;
2183
- const items = getItems();
2184
- if (!(event.target instanceof HTMLElement)) return;
2185
- const current = event.target.closest('[data-keepkit="card"]');
2186
- if (!current || current !== event.target || !ref.current?.contains(current)) return;
2187
- const currentIndex = items.indexOf(current);
2188
- if (currentIndex < 0) return;
2189
- let nextIndex;
2190
- if (event.key === "Home") nextIndex = 0;
2191
- if (event.key === "End") nextIndex = items.length - 1;
2192
- if (event.key === "ArrowRight" || event.key === "ArrowDown")
2193
- nextIndex = Math.min(currentIndex + 1, items.length - 1);
2194
- if (event.key === "ArrowLeft" || event.key === "ArrowUp") nextIndex = Math.max(currentIndex - 1, 0);
2195
- if (nextIndex === void 0) return;
2196
- event.preventDefault();
2197
- if (nextIndex === currentIndex) return;
2198
- items[nextIndex]?.focus();
2199
- },
2200
- [getItems]
2201
- );
2202
- return { ref, onKeyDown, onFocusCapture };
2203
- }
2204
-
2205
- // src/KeepItemCard.tsx
2206
- import {
2207
- createContext as createContext3,
2208
- createElement as createElement2,
2209
- isValidElement as isValidElement2,
2210
- useContext as useContext3,
2211
- useEffect as useEffect4,
2212
- useState as useState5
2213
- } from "react";
2214
-
2215
- // src/hooks/useKeepItemCard.ts
2216
- import { useKeepItem as useKeepItem2 } from "@keepkit/core/react";
2217
- function useKeepItemCard(options) {
2218
- const {
2219
- item,
2220
- title,
2221
- getTitle,
2222
- getImageProps,
2223
- href: hrefOption,
2224
- linkTargetAttribute,
2225
- linkRel,
2226
- onRemoveError,
2227
- onRemoved
2228
- } = options;
2229
- const itemState = useKeepItem2(item);
2230
- const emitFeedback = useKeepUiFeedback();
2231
- const removedMessage = useUiLabel("removedMessage");
2232
- const restoredMessage = useUiLabel("restoredMessage");
2233
- const undoLabel = useUiLabel("undo");
2234
- const resolvedTitle = typeof title === "function" ? title(item) : title ?? getTitle?.(item) ?? getMetaTitle(item.meta) ?? item.id;
2235
- const imageProps = getImageProps?.(item, resolvedTitle);
2236
- const href = typeof hrefOption === "function" ? hrefOption(item) : hrefOption;
2237
- const isAvailable = item.status === void 0 || item.status === "available";
2238
- const isExternalLink = href ? /^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(href) : false;
2239
- const statusLabelKey = item.status && item.status !== "available" ? getStatusLabelKey(item.status) : "statusUnknown";
2240
- const unavailableLabel = useUiLabel(statusLabelKey);
2241
- const remove = async () => {
2242
- const wasSaved = itemState.isSaved;
2243
- try {
2244
- await itemState.removeWithUndo();
2245
- onRemoved?.(item);
2246
- if (!wasSaved) return;
2247
- emitFeedback({
2248
- type: "item-removed",
2249
- item,
2250
- message: removedMessage,
2251
- undoLabel,
2252
- undo: async () => {
2253
- await itemState.undo();
2254
- emitFeedback({ type: "item-restored", item, items: [item], message: restoredMessage });
2255
- }
2256
- });
2257
- } catch (cause) {
2258
- onRemoveError?.(cause);
2259
- }
2260
- };
2261
- const state = {
2262
- item,
2263
- isSaved: itemState.isSaved,
2264
- isMutating: itemState.isMutating,
2265
- error: itemState.error,
2266
- remove,
2267
- status: item.status
2268
- };
2269
- return {
2270
- itemState,
2271
- state,
2272
- resolvedTitle,
2273
- imageProps,
2274
- href,
2275
- isAvailable,
2276
- displayStatus: getDisplayStatus(item.status),
2277
- resolvedLinkTarget: linkTargetAttribute ?? (isExternalLink ? "_blank" : void 0),
2278
- resolvedLinkRel: linkRel ?? (isExternalLink ? "noreferrer" : void 0),
2279
- statusLabel: item.status && item.status !== "available" ? unavailableLabel : void 0,
2280
- remove,
2281
- labels: {
2282
- save: useUiLabel("save"),
2283
- savedAt: useUiLabel("saved"),
2284
- error: useUiLabel("error"),
2285
- remove: useUiLabel("remove"),
2286
- tags: useUiLabel("tags")
2287
- }
2288
- };
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";
2289
2470
  }
2290
2471
  function getStatusLabelKey(status) {
2291
2472
  switch (status) {
2473
+ case "available":
2474
+ return "statusAvailable";
2292
2475
  case "expired":
2293
2476
  return "statusExpired";
2294
2477
  case "removed":
@@ -2297,28 +2480,85 @@ function getStatusLabelKey(status) {
2297
2480
  return "statusDeleted";
2298
2481
  case "private":
2299
2482
  return "statusPrivate";
2300
- default:
2483
+ case "unknown":
2301
2484
  return "statusUnknown";
2485
+ case "restricted":
2486
+ return "statusPrivate";
2302
2487
  }
2303
2488
  }
2304
2489
  function getDisplayStatus(status) {
2305
- if (status === void 0 || status === "available") return "available";
2490
+ if (status === "available") return "available";
2306
2491
  if (status === "expired") return "expired";
2307
- if (status === "removed") return "removed";
2492
+ if (status === "removed" || status === "deleted") return "removed";
2308
2493
  return "restricted";
2309
2494
  }
2310
2495
 
2311
- // src/hooks/useKeepStaleNotice.ts
2312
- import { useKeepContext as useKeepContext2 } from "@keepkit/core/react";
2313
- 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";
2314
2554
  function useKeepStaleNotice({ item, onRetry, onRemoved }) {
2315
- const context = useKeepContext2();
2555
+ const context = useKeepContext3();
2316
2556
  const emitFeedback = useKeepUiFeedback();
2317
2557
  const removedMessage = useUiLabel("removedMessage");
2318
2558
  const restoredMessage = useUiLabel("restoredMessage");
2319
2559
  const undoLabel = useUiLabel("undo");
2320
- const [isRetrying, setIsRetrying] = useState4(false);
2321
- const [error, setError] = useState4(null);
2560
+ const [isRetrying, setIsRetrying] = useState7(false);
2561
+ const [error, setError] = useState7(null);
2322
2562
  async function retry() {
2323
2563
  setError(null);
2324
2564
  setIsRetrying(true);
@@ -2364,7 +2604,7 @@ function useKeepStaleNotice({ item, onRetry, onRemoved }) {
2364
2604
  };
2365
2605
  }
2366
2606
  function useKeepPruneStale({ statuses, onPruned }) {
2367
- const context = useKeepContext2();
2607
+ const context = useKeepContext3();
2368
2608
  const emitFeedback = useKeepUiFeedback();
2369
2609
  const staleItems = context.items.filter((item) => item.status && statuses.includes(item.status));
2370
2610
  const staleIds = staleItems.map((item) => item.id);
@@ -2397,54 +2637,8 @@ function useKeepPruneStale({ statuses, onPruned }) {
2397
2637
  };
2398
2638
  }
2399
2639
 
2400
- // src/hooks/useKeepItemStatusBadge.ts
2401
- function useKeepItemStatusBadge(status) {
2402
- return { resolvedStatus: getDisplayStatus2(status), statusLabel: useUiLabel(getStatusLabelKey2(status)) };
2403
- }
2404
- function getStatusLabelKey2(status) {
2405
- switch (status) {
2406
- case "available":
2407
- return "statusAvailable";
2408
- case "expired":
2409
- return "statusExpired";
2410
- case "removed":
2411
- return "statusRemoved";
2412
- case "deleted":
2413
- return "statusDeleted";
2414
- case "private":
2415
- return "statusPrivate";
2416
- case "unknown":
2417
- return "statusUnknown";
2418
- case "restricted":
2419
- return "statusPrivate";
2420
- }
2421
- }
2422
- function getDisplayStatus2(status) {
2423
- if (status === "available") return "available";
2424
- if (status === "expired") return "expired";
2425
- if (status === "removed") return "removed";
2426
- return "restricted";
2427
- }
2428
-
2429
- // src/KeepItemStatusBadge.tsx
2430
- import { jsx as jsx7 } from "react/jsx-runtime";
2431
- function KeepItemStatusBadge({ status = "available", label, className, ...props }) {
2432
- const view = useKeepItemStatusBadge(status);
2433
- return /* @__PURE__ */ jsx7(
2434
- "span",
2435
- {
2436
- ...props,
2437
- className,
2438
- "data-keepkit": "status-badge",
2439
- "data-status": status,
2440
- "data-item-status": view.resolvedStatus,
2441
- children: label ?? view.statusLabel
2442
- }
2443
- );
2444
- }
2445
-
2446
- // src/KeepStaleNotice.tsx
2447
- 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";
2448
2642
  function KeepStaleNotice({
2449
2643
  item,
2450
2644
  onRetry,
@@ -2456,11 +2650,11 @@ function KeepStaleNotice({
2456
2650
  ...props
2457
2651
  }) {
2458
2652
  const view = useKeepStaleNotice({ item, onRetry, onRemoved });
2459
- return /* @__PURE__ */ jsxs4("aside", { ...props, className, "data-keepkit": "stale-notice", "data-state": view.error ? "error" : "stale", children: [
2460
- /* @__PURE__ */ jsx8(KeepItemStatusBadge, { status: view.status }),
2461
- children ?? (item.statusReason ? /* @__PURE__ */ jsx8("p", { children: item.statusReason }) : null),
2462
- /* @__PURE__ */ jsxs4("div", { children: [
2463
- /* @__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(
2464
2658
  "button",
2465
2659
  {
2466
2660
  type: "button",
@@ -2470,7 +2664,7 @@ function KeepStaleNotice({
2470
2664
  children: retryLabel ?? view.labels.retry
2471
2665
  }
2472
2666
  ),
2473
- /* @__PURE__ */ jsx8(
2667
+ /* @__PURE__ */ jsx11(
2474
2668
  "button",
2475
2669
  {
2476
2670
  type: "button",
@@ -2481,7 +2675,7 @@ function KeepStaleNotice({
2481
2675
  }
2482
2676
  )
2483
2677
  ] }),
2484
- 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
2485
2679
  ] });
2486
2680
  }
2487
2681
  function KeepPruneStaleButton({
@@ -2493,7 +2687,7 @@ function KeepPruneStaleButton({
2493
2687
  ...props
2494
2688
  }) {
2495
2689
  const view = useKeepPruneStale({ statuses, onPruned });
2496
- return /* @__PURE__ */ jsx8(
2690
+ return /* @__PURE__ */ jsx11(
2497
2691
  "button",
2498
2692
  {
2499
2693
  ...props,
@@ -2509,47 +2703,143 @@ function KeepPruneStaleButton({
2509
2703
  );
2510
2704
  }
2511
2705
 
2512
- // src/KeepItemCard.tsx
2513
- import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
2514
- var KeepItemCardContext = createContext3(null);
2515
- function useKeepItemCardCompound(part) {
2516
- const context = useContext3(KeepItemCardContext);
2517
- if (!context) throw new Error(`KeepItemCard.${part} must be rendered inside KeepItemCard.`);
2518
- return context;
2519
- }
2520
- function KeepItemCardRoot({
2521
- item,
2522
- title,
2523
- getTitle,
2524
- getImageProps,
2525
- imageComponent: ImageComponent,
2526
- renderImage,
2527
- renderTags,
2528
- showTags = true,
2529
- showSavedAt = true,
2530
- imageAlt,
2531
- render,
2532
- children,
2533
- removeLabel,
2534
- onRemoveError,
2535
- onRemoved,
2536
- onRetry,
2537
- showSaveButton = true,
2538
- saveButtonLabels,
2539
- asChild = false,
2540
- href: hrefOption,
2541
- onOpen,
2542
- linkTarget = "title",
2543
- linkComponent: LinkComponent,
2544
- linkTargetAttribute,
2545
- linkRel,
2546
- highlightQuery,
2547
- className,
2548
- ...rootProps
2549
- }) {
2550
- const view = useKeepItemCard({
2551
- item,
2552
- title,
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);
2805
+ function useKeepItemCardCompound(part) {
2806
+ const context = useContext3(KeepItemCardContext);
2807
+ if (!context) throw new Error(`KeepItemCard.${part} must be rendered inside KeepItemCard.`);
2808
+ return context;
2809
+ }
2810
+ function KeepItemCardRoot({
2811
+ item,
2812
+ title,
2813
+ getTitle,
2814
+ getImageProps,
2815
+ imageComponent: ImageComponent,
2816
+ renderImage,
2817
+ renderTags,
2818
+ showTags = true,
2819
+ showSavedAt = true,
2820
+ imageAlt,
2821
+ render,
2822
+ children,
2823
+ removeLabel,
2824
+ onRemoveError,
2825
+ onRemoved,
2826
+ onRetry,
2827
+ showSaveButton = true,
2828
+ saveButtonLabels,
2829
+ asChild = false,
2830
+ href: hrefOption,
2831
+ onOpen,
2832
+ linkTarget = "title",
2833
+ linkComponent: LinkComponent,
2834
+ linkTargetAttribute,
2835
+ linkRel,
2836
+ highlightQuery,
2837
+ className,
2838
+ ...rootProps
2839
+ }) {
2840
+ const view = useKeepItemCard({
2841
+ item,
2842
+ title,
2553
2843
  getTitle,
2554
2844
  getImageProps,
2555
2845
  href: hrefOption,
@@ -2560,7 +2850,7 @@ function KeepItemCardRoot({
2560
2850
  });
2561
2851
  const contextQuery = useKeepSearchQuery();
2562
2852
  const searchQuery = highlightQuery ?? contextQuery;
2563
- const contentChildren = asChild && isValidElement2(children) ? void 0 : children;
2853
+ const contentChildren = asChild && isValidElement3(children) ? void 0 : children;
2564
2854
  function renderLink(content) {
2565
2855
  if (!view.href || !view.isAvailable) return content;
2566
2856
  const linkProps = {
@@ -2570,17 +2860,17 @@ function KeepItemCardRoot({
2570
2860
  onClick: (event) => onOpen?.(item, event),
2571
2861
  children: content
2572
2862
  };
2573
- return LinkComponent ? /* @__PURE__ */ jsx9(LinkComponent, { ...linkProps }) : /* @__PURE__ */ jsx9("a", { ...linkProps });
2863
+ return LinkComponent ? /* @__PURE__ */ jsx12(LinkComponent, { ...linkProps }) : /* @__PURE__ */ jsx12("a", { ...linkProps });
2574
2864
  }
2575
2865
  function renderTitle(content) {
2576
2866
  if (linkTarget !== "title") return content;
2577
2867
  if (view.isAvailable) return renderLink(content);
2578
- 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;
2579
2869
  }
2580
2870
  const resolvedImageProps = view.imageProps ? { ...view.imageProps, alt: imageAlt ?? view.imageProps.alt } : void 0;
2581
2871
  const imageSource = resolvedImageProps?.src;
2582
- const [imageStatus, setImageStatus] = useState5(imageSource ? "loading" : "error");
2583
- useEffect4(() => {
2872
+ const [imageStatus, setImageStatus] = useState8(imageSource ? "loading" : "error");
2873
+ useEffect5(() => {
2584
2874
  setImageStatus(imageSource ? "loading" : "error");
2585
2875
  }, [imageSource]);
2586
2876
  let image = null;
@@ -2598,21 +2888,21 @@ function KeepItemCardRoot({
2598
2888
  setImageStatus("error");
2599
2889
  }
2600
2890
  };
2601
- image = renderImage?.(imagePropsWithHandlers, item) ?? (ImageComponent ? /* @__PURE__ */ jsx9(ImageComponent, { ...imagePropsWithHandlers }) : /* @__PURE__ */ jsx9("img", { ...imagePropsWithHandlers, alt: imagePropsWithHandlers.alt }));
2891
+ image = renderImage?.(imagePropsWithHandlers, item) ?? (ImageComponent ? /* @__PURE__ */ jsx12(ImageComponent, { ...imagePropsWithHandlers }) : /* @__PURE__ */ jsx12("img", { ...imagePropsWithHandlers, alt: imagePropsWithHandlers.alt }));
2602
2892
  }
2603
2893
  const tags = showTags ? item.tags ?? [] : [];
2604
2894
  const renderedTags = showTags && tags.length > 0 ? renderTags?.(tags, item) ?? null : null;
2605
- const meta = showSavedAt ? /* @__PURE__ */ jsxs5("div", { "data-card-meta": true, children: [
2606
- /* @__PURE__ */ jsxs5("span", { children: [
2895
+ const meta = showSavedAt ? /* @__PURE__ */ jsxs9("div", { "data-card-meta": true, children: [
2896
+ /* @__PURE__ */ jsxs9("span", { children: [
2607
2897
  view.labels.savedAt,
2608
2898
  ":"
2609
2899
  ] }),
2610
2900
  " ",
2611
- /* @__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) })
2612
2902
  ] }) : null;
2613
- const error = view.itemState.error ? /* @__PURE__ */ jsx9("p", { role: "alert", children: getErrorMessage2(view.itemState.error, view.labels.error) }) : null;
2614
- const actions = view.statusLabel ? /* @__PURE__ */ jsx9(KeepStaleNotice, { item, onRetry, onRemoved }) : /* @__PURE__ */ jsxs5(Fragment4, { children: [
2615
- 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(
2616
2906
  KeepButton,
2617
2907
  {
2618
2908
  item: toKeepButtonItem(item),
@@ -2620,7 +2910,7 @@ function KeepItemCardRoot({
2620
2910
  getAriaLabel: (buttonState) => `${buttonState.isSaved ? view.labels.remove : view.labels.save} ${String(view.resolvedTitle)}`
2621
2911
  }
2622
2912
  ) : null,
2623
- /* @__PURE__ */ jsx9(
2913
+ /* @__PURE__ */ jsx12(
2624
2914
  "button",
2625
2915
  {
2626
2916
  type: "button",
@@ -2643,18 +2933,18 @@ function KeepItemCardRoot({
2643
2933
  meta,
2644
2934
  error,
2645
2935
  actions,
2646
- renderText: (content) => /* @__PURE__ */ jsx9(KeepHighlight, { query: searchQuery, children: content })
2936
+ renderText: (content) => /* @__PURE__ */ jsx12(KeepHighlight, { query: searchQuery, children: content })
2647
2937
  };
2648
- const defaultBody = /* @__PURE__ */ jsxs5(Fragment4, { children: [
2649
- /* @__PURE__ */ jsx9(KeepItemCardMedia, {}),
2650
- /* @__PURE__ */ jsx9(KeepItemCardContent, {}),
2651
- /* @__PURE__ */ jsx9(KeepItemCardActions, {})
2938
+ const defaultBody = /* @__PURE__ */ jsxs9(Fragment6, { children: [
2939
+ /* @__PURE__ */ jsx12(KeepItemCardMedia, {}),
2940
+ /* @__PURE__ */ jsx12(KeepItemCardContent, {}),
2941
+ /* @__PURE__ */ jsx12(KeepItemCardActions, {})
2652
2942
  ] });
2653
2943
  const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? defaultBody;
2654
2944
  const linkedBody = linkTarget === "card" && view.isAvailable ? renderLink(body) : body;
2655
2945
  const root = renderRoot(
2656
2946
  asChild,
2657
- isValidElement2(children) ? children : void 0,
2947
+ isValidElement3(children) ? children : void 0,
2658
2948
  {
2659
2949
  ...rootProps,
2660
2950
  className,
@@ -2669,18 +2959,18 @@ function KeepItemCardRoot({
2669
2959
  linkedBody,
2670
2960
  "KeepItemCard"
2671
2961
  );
2672
- return /* @__PURE__ */ jsx9(KeepItemCardContext.Provider, { value: compoundValue, children: root });
2962
+ return /* @__PURE__ */ jsx12(KeepItemCardContext.Provider, { value: compoundValue, children: root });
2673
2963
  }
2674
2964
  function KeepItemCardMedia({ children, fallback, ...props }) {
2675
2965
  const context = useKeepItemCardCompound("Media");
2676
- return /* @__PURE__ */ jsx9("div", { ...props, "data-keep-card-part": "media", "data-media-status": context.imageStatus, children: children ?? context.image ?? /* @__PURE__ */ jsx9("span", { role: "img", "aria-label": context.fallbackLabel, "data-keep-card-fallback": "true", children: fallback ?? /* @__PURE__ */ jsx9(KeepMediaPlaceholderIcon, {}) }) });
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, {}) }) });
2677
2967
  }
2678
2968
  function KeepItemCardContent({ children, ...props }) {
2679
2969
  const context = useKeepItemCardCompound("Content");
2680
- return /* @__PURE__ */ jsx9("div", { ...props, "data-keep-card-part": "content", children: children === void 0 ? /* @__PURE__ */ jsxs5(Fragment4, { children: [
2681
- /* @__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, {}),
2682
2972
  context.meta,
2683
- /* @__PURE__ */ jsx9(KeepItemCardTags, {}),
2973
+ /* @__PURE__ */ jsx12(KeepItemCardTags, {}),
2684
2974
  context.error
2685
2975
  ] }) : context.renderText(children) });
2686
2976
  }
@@ -2688,13 +2978,13 @@ function KeepItemCardTitle({ as = "h3", children, ...props }) {
2688
2978
  const context = useKeepItemCardCompound("Title");
2689
2979
  return createElement2(
2690
2980
  as,
2691
- { ...props, "data-keep-card-part": "title" },
2981
+ { ...props, "data-keep-card-part": "title", "data-line-clamp": "2" },
2692
2982
  context.renderTitle(context.renderText(children ?? context.resolvedTitle))
2693
2983
  );
2694
2984
  }
2695
2985
  function KeepMediaPlaceholderIcon() {
2696
- return /* @__PURE__ */ jsxs5("svg", { "data-media-fallback-icon": "true", viewBox: "0 0 24 24", "aria-hidden": "true", children: [
2697
- /* @__PURE__ */ jsx9(
2986
+ return /* @__PURE__ */ jsxs9("svg", { "data-media-fallback-icon": "true", viewBox: "0 0 24 24", "aria-hidden": "true", children: [
2987
+ /* @__PURE__ */ jsx12(
2698
2988
  "path",
2699
2989
  {
2700
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",
@@ -2702,19 +2992,19 @@ function KeepMediaPlaceholderIcon() {
2702
2992
  stroke: "currentColor"
2703
2993
  }
2704
2994
  ),
2705
- /* @__PURE__ */ jsx9("circle", { cx: "9", cy: "9", r: "1.5", fill: "currentColor" }),
2706
- /* @__PURE__ */ jsx9("path", { d: "m5.5 18 4.5-4.5 3 3 2-2L19 18", fill: "none", stroke: "currentColor" })
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" })
2707
2997
  ] });
2708
2998
  }
2709
2999
  function KeepItemCardTags({ children, ...props }) {
2710
3000
  const context = useKeepItemCardCompound("Tags");
2711
3001
  if (children === void 0 && context.renderedTags) return context.renderedTags;
2712
3002
  if (children === void 0 && context.tags.length === 0) return null;
2713
- 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)) });
2714
3004
  }
2715
3005
  function KeepItemCardActions({ children, ...props }) {
2716
3006
  const context = useKeepItemCardCompound("Actions");
2717
- 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 });
2718
3008
  }
2719
3009
  var KeepItemCard = Object.assign(KeepItemCardRoot, {
2720
3010
  Media: KeepItemCardMedia,
@@ -2724,12 +3014,12 @@ var KeepItemCard = Object.assign(KeepItemCardRoot, {
2724
3014
  Actions: KeepItemCardActions
2725
3015
  });
2726
3016
  function KeepItemCardSkeleton({ layout = "list", ...props }) {
2727
- return /* @__PURE__ */ jsxs5("article", { ...props, "aria-hidden": "true", "data-keepkit": "card-skeleton", "data-layout": layout, "data-state": "loading", children: [
2728
- /* @__PURE__ */ jsx9("span", { "data-skeleton-part": "media" }),
2729
- /* @__PURE__ */ jsx9("span", { "data-skeleton-part": "title" }),
2730
- /* @__PURE__ */ jsx9("span", { "data-skeleton-part": "meta" }),
2731
- /* @__PURE__ */ jsx9("span", { "data-skeleton-part": "tag" }),
2732
- /* @__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" })
2733
3023
  ] });
2734
3024
  }
2735
3025
  function formatSavedAt(timestamp) {
@@ -2739,388 +3029,184 @@ function getErrorMessage2(error, fallback) {
2739
3029
  return error instanceof Error ? error.message : fallback;
2740
3030
  }
2741
3031
 
2742
- // src/KeepList.tsx
2743
- import { Fragment as Fragment5, jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
2744
- function KeepList(props) {
2745
- const { fallback, onBoundaryError, boundaryResetKey, ...listProps } = props;
2746
- const content = /* @__PURE__ */ jsx10(KeepListContent, { ...listProps });
2747
- if (fallback === void 0 && onBoundaryError === void 0) return content;
2748
- return /* @__PURE__ */ jsx10(KeepErrorBoundary, { fallback, onError: onBoundaryError, resetKey: boundaryResetKey, children: content });
2749
- }
2750
- function KeepListContent({
2751
- query,
2752
- children,
2753
- renderItem,
2754
- loading,
2755
- renderLoading,
2756
- loadingCount = 6,
2757
- empty,
2758
- error: errorContent,
2759
- itemCardProps,
2760
- layout = "list",
2761
- asChild = false,
2762
- onKeyDown,
2763
- onFocusCapture,
2764
- className,
2765
- ...rootProps
2766
- }) {
2767
- const view = useKeepListView(query);
2768
- const roving = useRovingTabIndex();
2769
- const { state } = view;
2770
- const body = getListBody(state, {
2771
- children,
2772
- renderItem,
2773
- loading: renderLoading !== void 0 ? renderLoading : loading,
2774
- loadingCount,
2775
- loadingLabel: view.labels.loading,
2776
- empty: empty ?? view.labels.empty,
2777
- error: errorContent ?? view.labels.error,
2778
- itemCardProps,
2779
- layout
2780
- });
2781
- return renderRoot(
2782
- asChild,
2783
- asChild && isValidElement3(children) ? children : void 0,
2784
- {
2785
- ...rootProps,
2786
- className,
2787
- "data-keepkit": "list",
2788
- "data-layout": layout,
2789
- "aria-busy": state.isLoading || rootProps["aria-busy"],
2790
- "data-state": getListState(state),
2791
- "data-loading": state.isLoading ? "true" : void 0,
2792
- "data-roving-tabindex": "true",
2793
- role: rootProps.role ?? "group",
2794
- ref: roving.ref,
2795
- onKeyDown: (event) => {
2796
- onKeyDown?.(event);
2797
- if (!event.defaultPrevented) roving.onKeyDown(event);
2798
- },
2799
- onFocusCapture: (event) => {
2800
- onFocusCapture?.(event);
2801
- if (!event.defaultPrevented) roving.onFocusCapture(event);
2802
- }
2803
- },
2804
- /* @__PURE__ */ jsx10(KeepSearchQueryProvider, { query: query?.search?.query, children: body }),
2805
- "KeepList"
2806
- );
2807
- }
2808
- function getListState(state) {
2809
- if (state.error && state.items.length === 0) return "error";
2810
- if (state.isLoading && !state.isHydrated) return "loading";
2811
- if (state.isHydrated && state.items.length === 0) return "empty";
2812
- return "ready";
2813
- }
2814
- function getListBody(state, options) {
2815
- if (state.error && state.items.length === 0) return resolveContent(options.error, state);
2816
- if (state.isLoading && !state.isHydrated) {
2817
- if (options.loading !== void 0) return resolveContent(options.loading, state);
2818
- const count = Number.isFinite(options.loadingCount) ? Math.max(0, Math.floor(options.loadingCount)) : 6;
2819
- return /* @__PURE__ */ jsxs6(Fragment5, { children: [
2820
- /* @__PURE__ */ jsx10("span", { role: "status", "data-keepkit": "loading-label", children: options.loadingLabel }),
2821
- /* @__PURE__ */ jsx10("ul", { "data-keepkit": "skeleton-list", "data-layout": options.layout, children: Array.from({ length: count }, (_, index) => (
2822
- // biome-ignore lint/suspicious/noArrayIndexKey: Static loading placeholders never reorder.
2823
- /* @__PURE__ */ jsx10("li", { children: /* @__PURE__ */ jsx10(KeepItemCardSkeleton, { layout: options.layout }) }, index)
2824
- )) })
2825
- ] });
2826
- }
2827
- if (state.isHydrated && state.items.length === 0) return resolveContent(options.empty, state);
2828
- if (typeof options.children === "function") return options.children(state);
2829
- if (options.children !== void 0 && !isValidElement3(options.children)) return options.children;
2830
- return /* @__PURE__ */ jsx10("ul", { "data-layout": options.layout, children: state.items.map(
2831
- (item) => options.renderItem ? options.renderItem(item, state) : /* @__PURE__ */ jsx10("li", { children: /* @__PURE__ */ jsx10(KeepItemCard, { item, ...options.itemCardProps }) }, item.id)
2832
- ) });
2833
- }
2834
-
2835
- // src/KeepTagFilter.tsx
2836
- import { isValidElement as isValidElement4 } from "react";
2837
-
2838
- // src/hooks/useKeepTagFilter.ts
3032
+ // src/features/collection/hooks/useKeepListView.ts
2839
3033
  import { useKeepList as useKeepList4 } from "@keepkit/core/react";
2840
- import { useCallback as useCallback4, useMemo as useMemo3, useState as useState6 } from "react";
2841
- function useKeepTagFilter(options) {
2842
- const { query, controlledValue, defaultValue, onChange, onValueChange } = options;
2843
- const [uncontrolledValue, setUncontrolledValue] = useState6(defaultValue);
2844
- const resolvedValue = controlledValue ?? uncontrolledValue;
2845
- const list = useKeepList4({
2846
- ...query,
2847
- tags: resolvedValue ? [...query?.tags ?? [], resolvedValue] : query?.tags
2848
- });
2849
- const select = useCallback4(
2850
- (tag) => {
2851
- if (controlledValue === void 0) setUncontrolledValue(tag);
2852
- onChange?.(tag);
2853
- onValueChange?.(tag);
2854
- },
2855
- [controlledValue, onChange, onValueChange]
2856
- );
2857
- const state = useMemo3(
2858
- () => ({ tags: list.tags, tagCounts: list.tagCounts, value: resolvedValue, select }),
2859
- [list.tagCounts, list.tags, resolvedValue, select]
2860
- );
3034
+ function useKeepListView(query) {
2861
3035
  return {
2862
- state,
2863
- isLoading: list.isLoading,
2864
- labels: { all: useUiLabel("allTags"), aria: useUiLabel("filterTags") }
3036
+ state: useKeepList4(query),
3037
+ labels: {
3038
+ loading: useUiLabel("loadingItems"),
3039
+ empty: useUiLabel("noItems"),
3040
+ error: useUiLabel("errorItems")
3041
+ }
2865
3042
  };
2866
3043
  }
2867
3044
 
2868
- // src/KeepTagFilter.tsx
2869
- import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
2870
- function KeepTagFilter({
2871
- query,
2872
- value: controlledValue,
2873
- defaultValue,
2874
- onChange,
2875
- onValueChange,
2876
- allLabel,
2877
- ariaLabel,
2878
- renderTag,
2879
- render,
2880
- children,
2881
- asChild = false,
2882
- className,
2883
- ...rootProps
2884
- }) {
2885
- const view = useKeepTagFilter({ query, controlledValue, defaultValue, onChange, onValueChange });
2886
- const contentChildren = asChild && isValidElement4(children) ? void 0 : children;
2887
- const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? /* @__PURE__ */ jsxs7("fieldset", { children: [
2888
- /* @__PURE__ */ jsx11("legend", { children: ariaLabel ?? view.labels.aria }),
2889
- /* @__PURE__ */ jsx11(
2890
- "button",
2891
- {
2892
- type: "button",
2893
- "data-keep-action": "filter-all-tags",
2894
- "aria-pressed": view.state.value === void 0,
2895
- onClick: () => view.state.select(),
2896
- children: allLabel ?? view.labels.all
2897
- }
2898
- ),
2899
- view.state.tags.map((tag) => /* @__PURE__ */ jsxs7(
2900
- "button",
2901
- {
2902
- type: "button",
2903
- "data-keep-action": "filter-tag",
2904
- "aria-pressed": view.state.value === tag,
2905
- onClick: () => view.state.select(tag),
2906
- children: [
2907
- renderTag ? renderTag(tag, view.state.tagCounts[tag] ?? 0, view.state.value === tag) : tag,
2908
- /* @__PURE__ */ jsxs7("span", { children: [
2909
- " (",
2910
- view.state.tagCounts[tag] ?? 0,
2911
- ")"
2912
- ] })
2913
- ]
2914
- },
2915
- tag
2916
- ))
2917
- ] });
2918
- return renderRoot(
2919
- asChild,
2920
- isValidElement4(children) ? children : void 0,
2921
- {
2922
- ...rootProps,
2923
- className,
2924
- "data-keepkit": "tag-filter",
2925
- "data-state": view.state.value === void 0 ? "all" : "filtered",
2926
- "data-loading": view.isLoading ? "true" : void 0
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
+ });
2927
3082
  },
2928
- body,
2929
- "KeepTagFilter"
3083
+ [getItems]
2930
3084
  );
2931
- }
2932
-
2933
- // src/hooks/useQueryControls.ts
2934
- import { useEffect as useEffect5, useState as useState7 } from "react";
2935
- function useKeepSearchInput(options) {
2936
- const { controlledValue, defaultValue, debounceMs, onValueChange } = options;
2937
- const [uncontrolledValue, setUncontrolledValue] = useState7(defaultValue);
2938
- const value = controlledValue ?? uncontrolledValue;
2939
- useEffect5(() => {
2940
- if (!onValueChange) return;
2941
- if (debounceMs <= 0) {
2942
- onValueChange(value);
2943
- return;
2944
- }
2945
- const timer = window.setTimeout(() => onValueChange(value), debounceMs);
2946
- return () => window.clearTimeout(timer);
2947
- }, [debounceMs, onValueChange, value]);
2948
- return {
2949
- value,
2950
- label: useUiLabel("search"),
2951
- change: (event) => {
2952
- if (controlledValue === void 0) setUncontrolledValue(event.currentTarget.value);
2953
- }
2954
- };
2955
- }
2956
- function useKeepSortSelect(options) {
2957
- const { controlledValue, defaultValue, onValueChange } = options;
2958
- const [uncontrolledValue, setUncontrolledValue] = useState7(defaultValue);
2959
- const value = controlledValue ?? uncontrolledValue;
2960
- return {
2961
- value,
2962
- change: (event) => {
2963
- const nextValue = event.currentTarget.value;
2964
- if (controlledValue === void 0) setUncontrolledValue(nextValue);
2965
- const [by, direction] = nextValue.split(":");
2966
- onValueChange?.(nextValue, { by, direction });
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();
2967
3104
  },
2968
- labels: {
2969
- sort: useUiLabel("sort"),
2970
- updatedNewest: useUiLabel("updatedNewest"),
2971
- updatedOldest: useUiLabel("updatedOldest"),
2972
- savedNewest: useUiLabel("savedNewest"),
2973
- savedOldest: useUiLabel("savedOldest")
2974
- }
2975
- };
2976
- }
2977
- function useKeepPagination(options) {
2978
- const { totalCount, pageSize, page, maxPageButtons, onPageChange } = options;
2979
- const pageCount = Math.max(1, Math.ceil(totalCount / Math.max(1, pageSize)));
2980
- const currentPage = Math.min(Math.max(1, page), pageCount);
2981
- const goToPage = (nextPage) => {
2982
- const next = Math.min(Math.max(1, nextPage), pageCount);
2983
- onPageChange?.(next, (next - 1) * pageSize);
2984
- };
2985
- return {
2986
- pageCount,
2987
- currentPage,
2988
- goToPage,
2989
- visiblePages: getVisiblePages(currentPage, pageCount, Math.max(1, maxPageButtons)),
2990
- labels: {
2991
- previous: useUiLabel("previousPage"),
2992
- next: useUiLabel("nextPage"),
2993
- page: useUiLabel("page"),
2994
- pagination: useUiLabel("pagination")
2995
- }
2996
- };
2997
- }
2998
- function getVisiblePages(currentPage, pageCount, maxPageButtons) {
2999
- if (pageCount <= maxPageButtons) return Array.from({ length: pageCount }, (_, index) => index + 1);
3000
- const half = Math.floor(maxPageButtons / 2);
3001
- const start = Math.min(Math.max(1, currentPage - half), pageCount - maxPageButtons + 1);
3002
- return Array.from({ length: maxPageButtons }, (_, index) => start + index);
3003
- }
3004
-
3005
- // src/query-controls.tsx
3006
- import { Fragment as Fragment6, jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
3007
- function KeepSearchInput({
3008
- value: controlledValue,
3009
- defaultValue = "",
3010
- debounceMs = 300,
3011
- onValueChange,
3012
- "aria-label": ariaLabel,
3013
- placeholder,
3014
- ...props
3015
- }) {
3016
- const view = useKeepSearchInput({ controlledValue, defaultValue, debounceMs, onValueChange });
3017
- return /* @__PURE__ */ jsx12(
3018
- "input",
3019
- {
3020
- ...props,
3021
- "data-keepkit": "search-input",
3022
- "data-keep-action": "search",
3023
- type: "search",
3024
- value: view.value,
3025
- "data-state": view.value ? "active" : "idle",
3026
- "data-disabled": props.disabled ? "true" : void 0,
3027
- "aria-label": ariaLabel ?? view.label,
3028
- placeholder: placeholder ?? view.label,
3029
- onChange: view.change
3030
- }
3105
+ [getItems]
3031
3106
  );
3107
+ return { ref, onKeyDown, onFocusCapture };
3032
3108
  }
3033
- function KeepSortSelect({
3034
- value: controlledValue,
3035
- defaultValue = "updatedAt:desc",
3036
- onValueChange,
3037
- "aria-label": ariaLabel,
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,
3038
3120
  children,
3039
- ...props
3121
+ renderItem,
3122
+ loading,
3123
+ renderLoading,
3124
+ loadingCount = 6,
3125
+ empty,
3126
+ error: errorContent,
3127
+ itemCardProps,
3128
+ layout = "list",
3129
+ asChild = false,
3130
+ onKeyDown,
3131
+ onFocusCapture,
3132
+ className,
3133
+ ...rootProps
3040
3134
  }) {
3041
- const view = useKeepSortSelect({ controlledValue, defaultValue, onValueChange });
3042
- const options = children ?? /* @__PURE__ */ jsxs8(Fragment6, { children: [
3043
- /* @__PURE__ */ jsx12("option", { value: "updatedAt:desc", children: view.labels.updatedNewest }),
3044
- /* @__PURE__ */ jsx12("option", { value: "updatedAt:asc", children: view.labels.updatedOldest }),
3045
- /* @__PURE__ */ jsx12("option", { value: "savedAt:desc", children: view.labels.savedNewest }),
3046
- /* @__PURE__ */ jsx12("option", { value: "savedAt:asc", children: view.labels.savedOldest })
3047
- ] });
3048
- return /* @__PURE__ */ jsx12(
3049
- "select",
3135
+ const view = useKeepListView(query);
3136
+ const roving = useRovingTabIndex();
3137
+ const { state } = view;
3138
+ const body = getListBody(state, {
3139
+ children,
3140
+ renderItem,
3141
+ loading: renderLoading !== void 0 ? renderLoading : loading,
3142
+ loadingCount,
3143
+ loadingLabel: view.labels.loading,
3144
+ empty: empty ?? view.labels.empty,
3145
+ error: errorContent ?? view.labels.error,
3146
+ itemCardProps,
3147
+ layout
3148
+ });
3149
+ return renderRoot(
3150
+ asChild,
3151
+ asChild && isValidElement4(children) ? children : void 0,
3050
3152
  {
3051
- ...props,
3052
- "data-keepkit": "sort-select",
3053
- "data-keep-action": "sort",
3054
- value: view.value,
3055
- "data-state": "selected",
3056
- "data-disabled": props.disabled ? "true" : void 0,
3057
- "aria-label": ariaLabel ?? view.labels.sort,
3058
- onChange: view.change,
3059
- children: options
3060
- }
3061
- );
3062
- }
3063
- function KeepPagination({
3064
- totalCount,
3065
- pageSize,
3066
- page = 1,
3067
- maxPageButtons = 7,
3068
- onPageChange,
3069
- render,
3070
- ...props
3071
- }) {
3072
- const view = useKeepPagination({ totalCount, pageSize, page, maxPageButtons, onPageChange });
3073
- const navProps = {
3074
- ...props,
3075
- "data-keepkit": "pagination",
3076
- "aria-label": props["aria-label"] ?? view.labels.pagination,
3077
- "data-state": view.pageCount > 1 ? "active" : "idle"
3078
- };
3079
- if (render)
3080
- return /* @__PURE__ */ jsx12("nav", { ...navProps, children: render({ page: view.currentPage, pageCount: view.pageCount, goToPage: view.goToPage }) });
3081
- return /* @__PURE__ */ jsxs8("nav", { ...navProps, children: [
3082
- /* @__PURE__ */ jsx12(
3083
- "button",
3084
- {
3085
- type: "button",
3086
- "data-keep-action": "previous-page",
3087
- onClick: () => view.goToPage(view.currentPage - 1),
3088
- disabled: view.currentPage <= 1,
3089
- children: view.labels.previous
3090
- }
3091
- ),
3092
- view.visiblePages.map((nextPage) => /* @__PURE__ */ jsx12(
3093
- "button",
3094
- {
3095
- type: "button",
3096
- "data-keep-action": "select-page",
3097
- "aria-current": nextPage === view.currentPage ? "page" : void 0,
3098
- "aria-label": `${view.labels.page} ${nextPage}`,
3099
- onClick: () => view.goToPage(nextPage),
3100
- children: nextPage
3153
+ ...rootProps,
3154
+ className,
3155
+ "data-keepkit": "list",
3156
+ "data-layout": layout,
3157
+ "aria-busy": state.isLoading || rootProps["aria-busy"],
3158
+ "data-state": getListState(state),
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);
3101
3166
  },
3102
- nextPage
3103
- )),
3104
- /* @__PURE__ */ jsx12(
3105
- "button",
3106
- {
3107
- type: "button",
3108
- "data-keep-action": "next-page",
3109
- onClick: () => view.goToPage(view.currentPage + 1),
3110
- disabled: view.currentPage >= view.pageCount,
3111
- children: view.labels.next
3167
+ onFocusCapture: (event) => {
3168
+ onFocusCapture?.(event);
3169
+ if (!event.defaultPrevented) roving.onFocusCapture(event);
3112
3170
  }
3113
- )
3114
- ] });
3171
+ },
3172
+ /* @__PURE__ */ jsx13(KeepSearchQueryProvider, { query: query?.search?.query, children: body }),
3173
+ "KeepList"
3174
+ );
3175
+ }
3176
+ function getListState(state) {
3177
+ if (state.error && state.items.length === 0) return "error";
3178
+ if (state.isLoading && !state.isHydrated) return "loading";
3179
+ if (state.isHydrated && state.items.length === 0) return "empty";
3180
+ return "ready";
3181
+ }
3182
+ function getListBody(state, options) {
3183
+ if (state.error && state.items.length === 0) return resolveContent(options.error, state);
3184
+ if (state.isLoading && !state.isHydrated) {
3185
+ if (options.loading !== void 0) return resolveContent(options.loading, state);
3186
+ const count = Number.isFinite(options.loadingCount) ? Math.max(0, Math.floor(options.loadingCount)) : 6;
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) => (
3190
+ // biome-ignore lint/suspicious/noArrayIndexKey: Static loading placeholders never reorder.
3191
+ /* @__PURE__ */ jsx13("li", { children: /* @__PURE__ */ jsx13(KeepItemCardSkeleton, { layout: options.layout }) }, index)
3192
+ )) })
3193
+ ] });
3194
+ }
3195
+ if (state.isHydrated && state.items.length === 0) return resolveContent(options.empty, state);
3196
+ if (typeof options.children === "function") return options.children(state);
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)
3200
+ ) });
3115
3201
  }
3116
3202
 
3117
- // src/KeepCollection.tsx
3118
- import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
3203
+ // src/features/collection/KeepCollection.tsx
3204
+ import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
3119
3205
  function KeepCollection(props) {
3120
3206
  const { fallback, onBoundaryError, boundaryResetKey, ...collectionProps } = props;
3121
- const content = /* @__PURE__ */ jsx13(KeepCollectionContent, { ...collectionProps });
3207
+ const content = /* @__PURE__ */ jsx14(KeepCollectionContent, { ...collectionProps });
3122
3208
  if (fallback === void 0 && onBoundaryError === void 0) return content;
3123
- return /* @__PURE__ */ jsx13(KeepErrorBoundary2, { fallback, onError: onBoundaryError, resetKey: boundaryResetKey, children: content });
3209
+ return /* @__PURE__ */ jsx14(KeepErrorBoundary2, { fallback, onError: onBoundaryError, resetKey: boundaryResetKey, children: content });
3124
3210
  }
3125
3211
  function KeepCollectionContent({
3126
3212
  query = {},
@@ -3140,7 +3226,7 @@ function KeepCollectionContent({
3140
3226
  ...rootProps
3141
3227
  }) {
3142
3228
  const view = useKeepCollection({ query, pageSize, urlSync, urlAdapter, features });
3143
- return /* @__PURE__ */ jsxs9(
3229
+ return /* @__PURE__ */ jsxs11(
3144
3230
  "section",
3145
3231
  {
3146
3232
  ...rootProps,
@@ -3151,12 +3237,12 @@ function KeepCollectionContent({
3151
3237
  "data-state": getCollectionState(view.list),
3152
3238
  "data-loading": view.list.isLoading || view.list.isMutating ? "true" : void 0,
3153
3239
  children: [
3154
- /* @__PURE__ */ jsxs9("div", { children: [
3155
- view.enabled.search ? /* @__PURE__ */ jsx13(KeepSearchInput, { value: view.searchValue, onValueChange: view.setSearchValue }) : null,
3156
- view.enabled.sort ? /* @__PURE__ */ jsx13(KeepSortSelect, { value: view.sortValue, onValueChange: view.setSortValue }) : null,
3157
- view.enabled.tagFilter ? /* @__PURE__ */ jsx13(KeepTagFilter, { query, value: view.tag, onValueChange: view.setTag }) : null
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
3158
3244
  ] }),
3159
- /* @__PURE__ */ jsx13(
3245
+ /* @__PURE__ */ jsx14(
3160
3246
  KeepList,
3161
3247
  {
3162
3248
  query: view.resolvedQuery,
@@ -3170,7 +3256,7 @@ function KeepCollectionContent({
3170
3256
  error
3171
3257
  }
3172
3258
  ),
3173
- view.enabled.pagination ? /* @__PURE__ */ jsx13(
3259
+ view.enabled.pagination ? /* @__PURE__ */ jsx14(
3174
3260
  KeepPagination,
3175
3261
  {
3176
3262
  totalCount: view.list.totalCount,
@@ -3179,7 +3265,7 @@ function KeepCollectionContent({
3179
3265
  onPageChange: view.setPage
3180
3266
  }
3181
3267
  ) : null,
3182
- view.enabled.bulkActions ? /* @__PURE__ */ jsx13(KeepBulkActions, { query: view.resolvedQuery }) : null
3268
+ view.enabled.bulkActions ? /* @__PURE__ */ jsx14(KeepBulkActions, { query: view.resolvedQuery }) : null
3183
3269
  ]
3184
3270
  }
3185
3271
  );
@@ -3190,172 +3276,37 @@ function getCollectionState(list) {
3190
3276
  if (list.isHydrated && list.items.length === 0) return "empty";
3191
3277
  return "ready";
3192
3278
  }
3193
-
3194
- // src/KeepLayout.tsx
3195
- import { jsx as jsx14 } from "react/jsx-runtime";
3196
- function KeepLayout({ layout = "list", children, onKeyDown, onFocusCapture, ...props }) {
3197
- const roving = useRovingTabIndex();
3198
- return (
3199
- // biome-ignore lint/a11y/noStaticElementInteractions: The group manages keyboard focus for descendant cards.
3200
- /* @__PURE__ */ jsx14(
3201
- "div",
3202
- {
3203
- ...props,
3204
- ref: roving.ref,
3205
- "data-keepkit": "layout",
3206
- "data-layout": layout,
3207
- "data-roving-tabindex": "true",
3208
- role: props.role ?? "group",
3209
- onKeyDown: (event) => {
3210
- onKeyDown?.(event);
3211
- if (!event.defaultPrevented) roving.onKeyDown(event);
3212
- },
3213
- onFocusCapture: (event) => {
3214
- onFocusCapture?.(event);
3215
- if (!event.defaultPrevented) roving.onFocusCapture(event);
3216
- },
3217
- children
3218
- }
3219
- )
3220
- );
3221
- }
3222
-
3223
- // src/KeepNoteEditor.tsx
3224
- import { isValidElement as isValidElement5 } from "react";
3225
-
3226
- // src/hooks/useKeepNoteEditor.ts
3227
- import { useKeepItem as useKeepItem3 } from "@keepkit/core/react";
3228
- import { useCallback as useCallback5, useEffect as useEffect6, useRef as useRef5, useState as useState8 } from "react";
3229
- function useKeepNoteEditor({ item, debounceMs, onSaved, onSaveError }) {
3230
- const itemState = useKeepItem3(item);
3231
- const { error, isMutating, item: savedItem, updateNote } = itemState;
3232
- const [note, setNote] = useState8(item.note ?? "");
3233
- const baselineNote = savedItem?.note ?? item.note ?? "";
3234
- const isDirty = note !== baselineNote;
3235
- const lastSavedNoteRef = useRef5(void 0);
3236
- useEffect6(() => setNote(baselineNote), [baselineNote]);
3237
- const save = useCallback5(async () => {
3238
- const nextNote = note.trim() || void 0;
3239
- try {
3240
- await updateNote(nextNote);
3241
- lastSavedNoteRef.current = note;
3242
- onSaved?.(nextNote);
3243
- } catch (cause) {
3244
- onSaveError?.(cause);
3245
- throw cause;
3246
- }
3247
- }, [note, onSaveError, onSaved, updateNote]);
3248
- useEffect6(() => {
3249
- if (!isDirty || debounceMs <= 0 || lastSavedNoteRef.current === note) return;
3250
- const timer = window.setTimeout(() => void save().catch(() => void 0), debounceMs);
3251
- return () => window.clearTimeout(timer);
3252
- }, [debounceMs, isDirty, note, save]);
3253
- const state = {
3254
- item,
3255
- note,
3256
- setNote,
3257
- isDirty,
3258
- isSaving: isMutating,
3259
- error,
3260
- save
3261
- };
3262
- const submit = (event) => {
3263
- event.preventDefault();
3264
- void save().catch(() => void 0);
3265
- };
3266
- return {
3267
- state,
3268
- submit,
3269
- handleKeyDown: (event) => {
3270
- if (event.key !== "Enter" || !event.ctrlKey && !event.metaKey) return;
3271
- event.preventDefault();
3272
- void save().catch(() => void 0);
3273
- },
3274
- labels: {
3275
- note: useUiLabel("note"),
3276
- save: useUiLabel("saveNote"),
3277
- error: useUiLabel("error")
3278
- }
3279
- };
3280
- }
3281
-
3282
- // src/KeepNoteEditor.tsx
3283
- import { Fragment as Fragment7, jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
3284
- function KeepNoteEditor({
3285
- item,
3286
- label,
3287
- saveLabel,
3288
- placeholder,
3289
- debounceMs = 300,
3290
- onSaved,
3291
- onSaveError,
3292
- render,
3293
- children,
3294
- asChild = false,
3295
- className,
3296
- ...formProps
3297
- }) {
3298
- const view = useKeepNoteEditor({ item, debounceMs, onSaved, onSaveError });
3299
- const { error, isDirty, isSaving, note, setNote } = view.state;
3300
- const contentChildren = asChild && isValidElement5(children) ? void 0 : children;
3301
- const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? /* @__PURE__ */ jsxs10(Fragment7, { children: [
3302
- /* @__PURE__ */ jsxs10("label", { children: [
3303
- label ?? view.labels.note,
3304
- /* @__PURE__ */ jsx15(
3305
- "textarea",
3306
- {
3307
- "data-keep-action": "edit-note",
3308
- value: note,
3309
- onChange: (event) => setNote(event.currentTarget.value),
3310
- placeholder,
3311
- disabled: isSaving,
3312
- onKeyDown: view.handleKeyDown
3313
- }
3314
- )
3315
- ] }),
3316
- /* @__PURE__ */ jsx15("button", { type: "submit", "data-keep-action": "save-note", disabled: isSaving, "aria-busy": isSaving, children: saveLabel ?? view.labels.save })
3317
- ] });
3318
- if (!asChild) {
3319
- return /* @__PURE__ */ jsxs10(
3320
- "form",
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",
3321
3288
  {
3322
- ...formProps,
3323
- className,
3324
- "data-keepkit": "note-editor",
3325
- onSubmit: view.submit,
3326
- "aria-busy": isSaving || formProps["aria-busy"],
3327
- "data-state": error ? "error" : isDirty ? "dirty" : "clean",
3328
- "data-loading": isSaving ? "true" : void 0,
3329
- "data-disabled": isSaving ? "true" : void 0,
3330
- children: [
3331
- body,
3332
- error ? /* @__PURE__ */ jsx15("p", { role: "alert", children: getErrorMessage3(error, view.labels.error) }) : null
3333
- ]
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
3334
3304
  }
3335
- );
3336
- }
3337
- return renderRoot(
3338
- true,
3339
- isValidElement5(children) ? children : void 0,
3340
- {
3341
- ...formProps,
3342
- className,
3343
- "data-keepkit": "note-editor",
3344
- onSubmit: view.submit,
3345
- "aria-busy": isSaving || formProps["aria-busy"],
3346
- "data-state": error ? "error" : isDirty ? "dirty" : "clean",
3347
- "data-loading": isSaving ? "true" : void 0,
3348
- "data-disabled": isSaving ? "true" : void 0
3349
- },
3350
- body,
3351
- "KeepNoteEditor"
3305
+ )
3352
3306
  );
3353
3307
  }
3354
- function getErrorMessage3(error, fallback) {
3355
- return error instanceof Error ? error.message : fallback;
3356
- }
3357
3308
 
3358
- // src/KeepReorderableList.tsx
3309
+ // src/features/collection/KeepReorderableList.tsx
3359
3310
  import { useState as useState9 } from "react";
3360
3311
  import { jsx as jsx16 } from "react/jsx-runtime";
3361
3312
  function KeepReorderableList({
@@ -3366,6 +3317,7 @@ function KeepReorderableList({
3366
3317
  ...props
3367
3318
  }) {
3368
3319
  const [draggedId, setDraggedId] = useState9(null);
3320
+ const [dropTargetIndex, setDropTargetIndex] = useState9(null);
3369
3321
  const ids = items.map((item) => item.id);
3370
3322
  function commit(nextIds) {
3371
3323
  void onReorder(nextIds);
@@ -3378,6 +3330,18 @@ function KeepReorderableList({
3378
3330
  next.splice(targetIndex, 0, id);
3379
3331
  commit(next);
3380
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
+ }
3381
3345
  return /* @__PURE__ */ jsx16("ul", { ...props, "data-keepkit": "reorderable-list", children: items.map((item, index) => {
3382
3346
  const moveUp = () => move(index, index - 1);
3383
3347
  const moveDown = () => move(index, index + 1);
@@ -3398,339 +3362,196 @@ function KeepReorderableList({
3398
3362
  dragHandleProps: {
3399
3363
  role: "button",
3400
3364
  tabIndex: 0,
3401
- draggable: true,
3402
- "aria-label": itemLabel(item, index),
3403
- "aria-grabbed": draggedId === item.id,
3404
- onDragStart: () => setDraggedId(item.id),
3405
- onDragEnd: () => setDraggedId(null),
3406
- onKeyDown: onHandleKeyDown
3407
- }
3408
- };
3409
- return /* @__PURE__ */ jsx16(
3410
- "li",
3411
- {
3412
- "data-reorder-index": index,
3413
- "data-dragging": draggedId === item.id ? "true" : void 0,
3414
- onDragOver: (event) => {
3415
- if (draggedId && draggedId !== item.id) event.preventDefault();
3416
- },
3417
- onDrop: (event) => {
3418
- event.preventDefault();
3419
- if (!draggedId || draggedId === item.id) return;
3420
- const sourceIndex = ids.indexOf(draggedId);
3421
- setDraggedId(null);
3422
- move(sourceIndex, index);
3423
- },
3424
- children: renderItem(item, state)
3425
- },
3426
- item.id
3427
- );
3428
- }) });
3429
- }
3430
-
3431
- // src/hooks/useKeepSyncFeedback.ts
3432
- import { useKeepContext as useKeepContext3 } from "@keepkit/core/react";
3433
- import { useEffect as useEffect7, useRef as useRef6 } from "react";
3434
- function useKeepSyncFeedback() {
3435
- const { syncState } = useKeepContext3();
3436
- const emitFeedback = useKeepUiFeedback();
3437
- const completedMessage = useUiLabel("syncSynced");
3438
- const failedMessage = useUiLabel("syncFailedMessage");
3439
- const previousStatus = useRef6("idle");
3440
- useEffect7(() => {
3441
- const previous = previousStatus.current;
3442
- previousStatus.current = syncState.status;
3443
- if (syncState.status === "error" && previous !== "error") {
3444
- emitFeedback({ type: "sync-failed", error: syncState.error, message: failedMessage });
3445
- return;
3446
- }
3447
- if (syncState.status === "synced" && (previous === "pending" || previous === "syncing" || previous === "conflict" || previous === "error")) {
3448
- emitFeedback({ type: "sync-completed", message: completedMessage });
3449
- }
3450
- }, [completedMessage, emitFeedback, failedMessage, syncState.error, syncState.status]);
3451
- }
3452
-
3453
- // src/KeepSyncFeedbackObserver.tsx
3454
- function KeepSyncFeedbackObserver() {
3455
- useKeepSyncFeedback();
3456
- return null;
3457
- }
3458
-
3459
- // src/hooks/useKeepSyncRecoveryDialog.ts
3460
- import { useKeepContext as useKeepContext4 } from "@keepkit/core/react";
3461
- import { useEffect as useEffect8, useState as useState10 } from "react";
3462
- function useKeepSyncRecoveryDialog(options) {
3463
- const { open, onOpenChange, conflicts, onManualMerge } = options;
3464
- const context = useKeepContext4();
3465
- const conflictList = conflicts ?? context.syncState.conflicts ?? [];
3466
- const hasRecovery = conflictList.length > 0 || context.syncState.status === "error" || Boolean(context.error);
3467
- const [dismissed, setDismissed] = useState10(false);
3468
- const [busyId, setBusyId] = useState10();
3469
- const [error, setError] = useState10();
3470
- useEffect8(() => {
3471
- if (hasRecovery) setDismissed(false);
3472
- }, [hasRecovery]);
3473
- return {
3474
- conflictList,
3475
- isOpen: open ?? (hasRecovery && !dismissed),
3476
- busyId,
3477
- error,
3478
- showBackupRecovery: context.syncState.status === "error" || Boolean(context.error),
3479
- close: () => {
3480
- setDismissed(true);
3481
- onOpenChange?.(false);
3482
- },
3483
- resolve: async (conflict, resolution) => {
3484
- setError(void 0);
3485
- setBusyId(conflict.id);
3486
- try {
3487
- const merged = resolution === "manual" ? await onManualMerge?.(conflict) : void 0;
3488
- if (resolution === "manual" && !merged) throw new Error("A manual merge result is required.");
3489
- await context.resolveSyncConflict(conflict.id, resolution, merged);
3490
- } catch (cause) {
3491
- setError(cause);
3492
- } finally {
3493
- setBusyId(void 0);
3494
- }
3495
- },
3496
- labels: {
3497
- close: useUiLabel("close"),
3498
- title: useUiLabel("resolveSync"),
3499
- conflict: useUiLabel("syncConflict"),
3500
- keepLocal: useUiLabel("keepLocal"),
3501
- useServer: useUiLabel("useServer"),
3502
- manualMerge: useUiLabel("manualMerge"),
3503
- localVersion: useUiLabel("localVersion"),
3504
- remoteVersion: useUiLabel("remoteVersion"),
3505
- updatedAt: useUiLabel("updatedAt"),
3506
- note: useUiLabel("note"),
3507
- backupRecovery: useUiLabel("backupRecovery"),
3508
- backupRecoveryDescription: useUiLabel("backupRecoveryDescription"),
3509
- error: useUiLabel("error")
3510
- }
3511
- };
3512
- }
3513
-
3514
- // src/KeepSyncRecoveryDialog.tsx
3515
- import { jsx as jsx17, jsxs as jsxs11 } from "react/jsx-runtime";
3516
- function KeepSyncRecoveryDialog({
3517
- open,
3518
- onOpenChange,
3519
- conflicts,
3520
- onManualMerge,
3521
- backup,
3522
- showBackupControls = true,
3523
- title,
3524
- children,
3525
- className,
3526
- ...props
3527
- }) {
3528
- const view = useKeepSyncRecoveryDialog({ open, onOpenChange, conflicts, onManualMerge });
3529
- if (!view.isOpen) return null;
3530
- return /* @__PURE__ */ jsxs11(
3531
- "section",
3532
- {
3533
- ...props,
3534
- className,
3535
- role: "dialog",
3536
- "aria-modal": "true",
3537
- "aria-labelledby": "keepkit-sync-recovery-title",
3538
- "aria-describedby": view.error ? "keepkit-sync-recovery-error" : void 0,
3539
- "aria-busy": view.busyId !== void 0,
3540
- "data-keepkit": "sync-recovery",
3541
- "data-state": view.conflictList.length > 0 ? "conflict" : "error",
3542
- "data-loading": view.busyId !== void 0 ? "true" : void 0,
3543
- children: [
3544
- /* @__PURE__ */ jsxs11("header", { children: [
3545
- /* @__PURE__ */ jsx17("h2", { id: "keepkit-sync-recovery-title", children: title ?? view.labels.title }),
3546
- /* @__PURE__ */ jsx17("button", { type: "button", "data-keep-action": "close-dialog", onClick: view.close, "aria-label": view.labels.close, children: view.labels.close })
3547
- ] }),
3548
- children,
3549
- view.conflictList.length > 0 ? /* @__PURE__ */ jsxs11("div", { children: [
3550
- /* @__PURE__ */ jsx17("p", { children: view.labels.conflict }),
3551
- view.conflictList.map((conflict) => /* @__PURE__ */ jsxs11("article", { "data-conflict-id": conflict.id, children: [
3552
- /* @__PURE__ */ jsx17("h3", { children: getMetaTitle(conflict.operation.item?.meta) ?? conflict.id }),
3553
- /* @__PURE__ */ jsxs11("div", { "data-conflict-preview": true, children: [
3554
- /* @__PURE__ */ jsx17(
3555
- ConflictPreview,
3556
- {
3557
- item: conflict.operation.item,
3558
- heading: view.labels.localVersion,
3559
- updatedAtLabel: view.labels.updatedAt,
3560
- noteLabel: view.labels.note,
3561
- side: "local"
3562
- }
3563
- ),
3564
- /* @__PURE__ */ jsx17(
3565
- ConflictPreview,
3566
- {
3567
- item: conflict.remote,
3568
- heading: view.labels.remoteVersion,
3569
- updatedAtLabel: view.labels.updatedAt,
3570
- noteLabel: view.labels.note,
3571
- side: "remote"
3572
- }
3573
- )
3574
- ] }),
3575
- /* @__PURE__ */ jsxs11("div", { children: [
3576
- /* @__PURE__ */ jsx17(
3577
- "button",
3578
- {
3579
- type: "button",
3580
- "data-keep-action": "keep-local",
3581
- onClick: () => void view.resolve(conflict, "local"),
3582
- disabled: view.busyId !== void 0,
3583
- children: view.labels.keepLocal
3584
- }
3585
- ),
3586
- /* @__PURE__ */ jsx17(
3587
- "button",
3588
- {
3589
- type: "button",
3590
- "data-keep-action": "use-server",
3591
- onClick: () => void view.resolve(conflict, "remote"),
3592
- disabled: view.busyId !== void 0,
3593
- children: view.labels.useServer
3594
- }
3595
- ),
3596
- /* @__PURE__ */ jsx17(
3597
- "button",
3598
- {
3599
- type: "button",
3600
- "data-keep-action": "manual-merge",
3601
- onClick: () => void view.resolve(conflict, "manual"),
3602
- disabled: view.busyId !== void 0 || !onManualMerge,
3603
- children: view.labels.manualMerge
3604
- }
3605
- )
3606
- ] })
3607
- ] }, conflict.id))
3608
- ] }) : null,
3609
- view.showBackupRecovery ? /* @__PURE__ */ jsxs11("section", { "data-recovery": "backup", children: [
3610
- /* @__PURE__ */ jsx17("h3", { children: view.labels.backupRecovery }),
3611
- /* @__PURE__ */ jsx17("p", { children: view.labels.backupRecoveryDescription }),
3612
- backup ?? (showBackupControls ? /* @__PURE__ */ jsx17(KeepBackup, {}) : null)
3613
- ] }) : null,
3614
- view.error ? /* @__PURE__ */ jsx17("p", { id: "keepkit-sync-recovery-error", role: "alert", "aria-live": "assertive", children: view.error instanceof Error ? view.error.message : view.labels.error }) : null
3615
- ]
3616
- }
3617
- );
3618
- }
3619
- function ConflictPreview({
3620
- item,
3621
- heading,
3622
- updatedAtLabel,
3623
- noteLabel,
3624
- side
3625
- }) {
3626
- return /* @__PURE__ */ jsxs11("article", { "data-conflict-version": side, "aria-label": heading, children: [
3627
- /* @__PURE__ */ jsx17("h4", { children: heading }),
3628
- /* @__PURE__ */ jsxs11("dl", { children: [
3629
- /* @__PURE__ */ jsxs11("div", { children: [
3630
- /* @__PURE__ */ jsx17("dt", { children: updatedAtLabel }),
3631
- /* @__PURE__ */ jsx17("dd", { children: item ? /* @__PURE__ */ jsx17("time", { dateTime: new Date(item.updatedAt).toISOString(), children: formatConflictDate(item.updatedAt) }) : "\u2014" })
3632
- ] }),
3633
- /* @__PURE__ */ jsxs11("div", { children: [
3634
- /* @__PURE__ */ jsx17("dt", { children: noteLabel }),
3635
- /* @__PURE__ */ jsx17("dd", { children: item?.note || "\u2014" })
3636
- ] })
3637
- ] })
3638
- ] });
3639
- }
3640
- function formatConflictDate(timestamp) {
3641
- return new Date(timestamp).toISOString().slice(0, 10);
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
+ }) });
3642
3409
  }
3643
3410
 
3644
- // src/hooks/useKeepSyncStatusBanner.ts
3645
- import { useKeepContext as useKeepContext5 } from "@keepkit/core/react";
3646
- function useKeepSyncStatusBanner({ onRetry, children }) {
3647
- const context = useKeepContext5();
3648
- const retryLabel = useUiLabel("retrySync");
3649
- const resolveLabel = useUiLabel("resolveSync");
3650
- const conflictLabel = useUiLabel("syncConflict");
3651
- const pendingLabel = useUiLabel("syncPending");
3652
- const syncedLabel = useUiLabel("syncSynced");
3653
- const status = context.syncState.status;
3654
- const hasConflicts = (context.syncState.conflicts?.length ?? 0) > 0 || context.syncState.conflictIds.length > 0;
3655
- const message = children ?? (status === "error" ? getErrorMessage4(context.syncState.error) : status === "conflict" || hasConflicts ? conflictLabel : status === "pending" || status === "syncing" ? pendingLabel : syncedLabel);
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;
3434
+ }
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);
3439
+ return () => window.clearTimeout(timer);
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);
3453
+ };
3656
3454
  return {
3657
- status,
3658
- hasConflicts,
3659
- message,
3660
- isMutating: context.isMutating,
3661
- role: status === "error" || status === "conflict" || hasConflicts ? "alert" : "status",
3662
- showRetry: status === "error" || status === "pending" || status === "syncing",
3663
- retryLabel,
3664
- resolveLabel,
3665
- retry: async () => {
3666
- if (onRetry) {
3667
- await onRetry();
3668
- return;
3669
- }
3670
- await context.flushSync();
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);
3461
+ },
3462
+ labels: {
3463
+ note: useUiLabel("note"),
3464
+ save: useUiLabel("saveNote"),
3465
+ error: useUiLabel("error")
3671
3466
  }
3672
3467
  };
3673
3468
  }
3674
- function getErrorMessage4(error) {
3675
- return error instanceof Error ? error.message : "Sync failed.";
3676
- }
3677
3469
 
3678
- // src/KeepSyncStatusBanner.tsx
3679
- import { jsx as jsx18, jsxs as jsxs12 } from "react/jsx-runtime";
3680
- function KeepSyncStatusBanner({
3681
- onRetry,
3682
- onResolveConflicts,
3470
+ // src/features/editor/KeepNoteEditor.tsx
3471
+ import { Fragment as Fragment8, jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
3472
+ function KeepNoteEditor({
3473
+ item,
3474
+ label,
3475
+ saveLabel,
3476
+ placeholder,
3477
+ debounceMs = 300,
3478
+ onSaved,
3479
+ onSaveError,
3480
+ render,
3683
3481
  children,
3482
+ asChild = false,
3684
3483
  className,
3685
- ...props
3484
+ ...formProps
3686
3485
  }) {
3687
- const view = useKeepSyncStatusBanner({ onRetry, children });
3688
- if (view.status === "idle" && !view.hasConflicts) return null;
3689
- return /* @__PURE__ */ jsxs12(
3690
- "aside",
3486
+ const view = useKeepNoteEditor({ item, debounceMs, onSaved, onSaveError });
3487
+ const { error, isDirty, isSaving, note, setNote } = view.state;
3488
+ const contentChildren = asChild && isValidElement5(children) ? void 0 : children;
3489
+ const body = render ? render(view.state) : typeof contentChildren === "function" ? contentChildren(view.state) : contentChildren ?? /* @__PURE__ */ jsxs12(Fragment8, { children: [
3490
+ /* @__PURE__ */ jsxs12("label", { children: [
3491
+ label ?? view.labels.note,
3492
+ /* @__PURE__ */ jsx17(
3493
+ "textarea",
3494
+ {
3495
+ "data-keep-action": "edit-note",
3496
+ value: note,
3497
+ onChange: (event) => setNote(event.currentTarget.value),
3498
+ placeholder,
3499
+ disabled: isSaving,
3500
+ onKeyDown: view.handleKeyDown
3501
+ }
3502
+ )
3503
+ ] }),
3504
+ /* @__PURE__ */ jsx17("button", { type: "submit", "data-keep-action": "save-note", disabled: isSaving, "aria-busy": isSaving, children: saveLabel ?? view.labels.save })
3505
+ ] });
3506
+ if (!asChild) {
3507
+ return /* @__PURE__ */ jsxs12(
3508
+ "form",
3509
+ {
3510
+ ...formProps,
3511
+ className,
3512
+ "data-keepkit": "note-editor",
3513
+ onSubmit: view.submit,
3514
+ "aria-busy": isSaving || formProps["aria-busy"],
3515
+ "data-state": error ? "error" : isDirty ? "dirty" : "clean",
3516
+ "data-loading": isSaving ? "true" : void 0,
3517
+ "data-disabled": isSaving ? "true" : void 0,
3518
+ children: [
3519
+ body,
3520
+ error ? /* @__PURE__ */ jsx17("p", { role: "alert", children: getErrorMessage3(error, view.labels.error) }) : null
3521
+ ]
3522
+ }
3523
+ );
3524
+ }
3525
+ return renderRoot(
3526
+ true,
3527
+ isValidElement5(children) ? children : void 0,
3691
3528
  {
3692
- ...props,
3529
+ ...formProps,
3693
3530
  className,
3694
- role: props.role ?? view.role,
3695
- "aria-live": props["aria-live"] ?? "polite",
3696
- "data-keepkit": "sync-status",
3697
- "data-state": view.status,
3698
- children: [
3699
- /* @__PURE__ */ jsx18("p", { children: view.message }),
3700
- view.showRetry ? /* @__PURE__ */ jsx18(
3701
- "button",
3702
- {
3703
- type: "button",
3704
- "data-keep-action": "retry-sync",
3705
- onClick: () => void view.retry(),
3706
- disabled: view.isMutating,
3707
- children: view.retryLabel
3708
- }
3709
- ) : null,
3710
- view.hasConflicts ? /* @__PURE__ */ jsx18(
3711
- "button",
3712
- {
3713
- type: "button",
3714
- "data-keep-action": "resolve-conflicts",
3715
- onClick: onResolveConflicts,
3716
- disabled: !onResolveConflicts,
3717
- children: view.resolveLabel
3718
- }
3719
- ) : null
3720
- ]
3721
- }
3531
+ "data-keepkit": "note-editor",
3532
+ onSubmit: view.submit,
3533
+ "aria-busy": isSaving || formProps["aria-busy"],
3534
+ "data-state": error ? "error" : isDirty ? "dirty" : "clean",
3535
+ "data-loading": isSaving ? "true" : void 0,
3536
+ "data-disabled": isSaving ? "true" : void 0
3537
+ },
3538
+ body,
3539
+ "KeepNoteEditor"
3722
3540
  );
3723
3541
  }
3542
+ function getErrorMessage3(error, fallback) {
3543
+ return error instanceof Error ? error.message : fallback;
3544
+ }
3724
3545
 
3725
- // src/hooks/useKeepTagEditor.ts
3546
+ // src/features/editor/hooks/useKeepTagEditor.ts
3726
3547
  import { useKeepItem as useKeepItem4 } from "@keepkit/core/react";
3727
- import { useCallback as useCallback6, useEffect as useEffect9, useState as useState11 } from "react";
3548
+ import { useCallback as useCallback5, useEffect as useEffect8, useState as useState11 } from "react";
3728
3549
  function useKeepTagEditor({ item, onSaved, onSaveError }) {
3729
3550
  const itemState = useKeepItem4(item);
3730
3551
  const [tags, setTags] = useState11(item.tags ?? []);
3731
3552
  const [input, setInput] = useState11("");
3732
- useEffect9(() => setTags(itemState.item?.tags ?? item.tags ?? []), [item.tags, itemState.item?.tags]);
3733
- const save = useCallback6(async () => {
3553
+ useEffect8(() => setTags(itemState.item?.tags ?? item.tags ?? []), [item.tags, itemState.item?.tags]);
3554
+ const save = useCallback5(async () => {
3734
3555
  const nextTags = normalizeUiTags(tags);
3735
3556
  try {
3736
3557
  await itemState.updateTags(nextTags);
@@ -3775,8 +3596,8 @@ function useKeepTagEditor({ item, onSaved, onSaveError }) {
3775
3596
  };
3776
3597
  }
3777
3598
 
3778
- // src/KeepTagEditor.tsx
3779
- import { Fragment as Fragment8, jsx as jsx19, jsxs as jsxs13 } from "react/jsx-runtime";
3599
+ // src/features/editor/KeepTagEditor.tsx
3600
+ import { Fragment as Fragment9, jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
3780
3601
  function KeepTagEditor({
3781
3602
  item,
3782
3603
  availableTags = [],
@@ -3787,10 +3608,10 @@ function KeepTagEditor({
3787
3608
  }) {
3788
3609
  const view = useKeepTagEditor({ item, onSaved, onSaveError });
3789
3610
  const { isSaving, tags } = view.state;
3790
- const body = render ? render(view.state) : /* @__PURE__ */ jsxs13(Fragment8, { children: [
3611
+ const body = render ? render(view.state) : /* @__PURE__ */ jsxs13(Fragment9, { children: [
3791
3612
  /* @__PURE__ */ jsxs13("label", { children: [
3792
3613
  view.labels.tags,
3793
- /* @__PURE__ */ jsx19(
3614
+ /* @__PURE__ */ jsx18(
3794
3615
  "input",
3795
3616
  {
3796
3617
  "data-keep-action": "edit-tags",
@@ -3801,12 +3622,12 @@ function KeepTagEditor({
3801
3622
  }
3802
3623
  )
3803
3624
  ] }),
3804
- availableTags.length > 0 ? /* @__PURE__ */ jsx19("datalist", { id: `keep-tags-${item.id}`, children: availableTags.map((tag) => /* @__PURE__ */ jsx19("option", { value: tag }, tag)) }) : null,
3805
- /* @__PURE__ */ jsx19("ul", { "aria-label": view.labels.tags, children: tags.map((tag) => /* @__PURE__ */ jsxs13("li", { children: [
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: [
3806
3627
  tag,
3807
- /* @__PURE__ */ jsx19("button", { type: "button", "data-keep-action": "remove-tag", onClick: () => view.removeTag(tag), children: view.labels.remove })
3628
+ /* @__PURE__ */ jsx18("button", { type: "button", "data-keep-action": "remove-tag", onClick: () => view.removeTag(tag), children: view.labels.remove })
3808
3629
  ] }, tag)) }),
3809
- /* @__PURE__ */ jsx19("button", { type: "submit", "data-keep-action": "apply-tags", disabled: isSaving, "aria-busy": isSaving, children: view.labels.apply })
3630
+ /* @__PURE__ */ jsx18("button", { type: "submit", "data-keep-action": "apply-tags", disabled: isSaving, "aria-busy": isSaving, children: view.labels.apply })
3810
3631
  ] });
3811
3632
  return /* @__PURE__ */ jsxs13(
3812
3633
  "form",
@@ -3820,20 +3641,41 @@ function KeepTagEditor({
3820
3641
  "data-disabled": isSaving ? "true" : void 0,
3821
3642
  children: [
3822
3643
  body,
3823
- view.error ? /* @__PURE__ */ jsx19("p", { role: "alert", children: getErrorMessage5(view.error, view.labels.error) }) : null
3644
+ view.error ? /* @__PURE__ */ jsx18("p", { role: "alert", children: getErrorMessage4(view.error, view.labels.error) }) : null
3824
3645
  ]
3825
3646
  }
3826
3647
  );
3827
3648
  }
3828
- function getErrorMessage5(error, fallback) {
3649
+ function getErrorMessage4(error, fallback) {
3829
3650
  return error instanceof Error ? error.message : fallback;
3830
3651
  }
3831
3652
 
3832
- // src/KeepTourBar.tsx
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
3833
3674
  import { useKeepNavigator } from "@keepkit/core/react";
3675
+ import { useId } from "react";
3834
3676
 
3835
- // src/hooks/useKeepTourShortcuts.ts
3836
- import { useEffect as useEffect10 } from "react";
3677
+ // src/features/navigation/hooks/useKeepTourShortcuts.ts
3678
+ import { useEffect as useEffect9 } from "react";
3837
3679
  function useKeepTourShortcuts({
3838
3680
  onNext,
3839
3681
  onPrev,
@@ -3844,7 +3686,7 @@ function useKeepTourShortcuts({
3844
3686
  prevKeys = ["k", "["],
3845
3687
  onError
3846
3688
  }) {
3847
- useEffect10(() => {
3689
+ useEffect9(() => {
3848
3690
  if (!enabled) return;
3849
3691
  const handleKeyDown = (event) => {
3850
3692
  if (!allowInEditable && isEditableTarget(event.target)) return;
@@ -3864,8 +3706,8 @@ function isEditableTarget(target) {
3864
3706
  return target.isContentEditable || target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT";
3865
3707
  }
3866
3708
 
3867
- // src/KeepTourBar.tsx
3868
- import { jsx as jsx20, jsxs as jsxs14 } from "react/jsx-runtime";
3709
+ // src/features/navigation/KeepTourBar.tsx
3710
+ import { Fragment as Fragment10, jsx as jsx19, jsxs as jsxs14 } from "react/jsx-runtime";
3869
3711
  function KeepTourBar({
3870
3712
  navigation: providedNavigation,
3871
3713
  currentId,
@@ -3883,6 +3725,7 @@ function KeepTourBar({
3883
3725
  keyboardShortcuts = false,
3884
3726
  shortcutOptions,
3885
3727
  progress,
3728
+ getItemTitle = (item) => getMetaTitle(item.meta) ?? item.id,
3886
3729
  ...props
3887
3730
  }) {
3888
3731
  const ownNavigation = useKeepNavigator({ currentId, initialIndex });
@@ -3904,84 +3747,89 @@ function KeepTourBar({
3904
3747
  }
3905
3748
  });
3906
3749
  return /* @__PURE__ */ jsxs14("nav", { ...props, "data-keepkit": "tour-bar", "aria-label": props["aria-label"] ?? labels.pagination, children: [
3907
- showProgress ? /* @__PURE__ */ jsx20("span", { "data-keepkit": "tour-progress", "aria-live": "polite", children: progress ?? `${navigation.currentPosition ?? 0} / ${navigation.items.length}` }) : null,
3908
- /* @__PURE__ */ jsx20(
3750
+ showProgress ? /* @__PURE__ */ jsx19("span", { "data-keepkit": "tour-progress", "aria-live": "polite", children: progress ?? `${navigation.currentPosition ?? 0} / ${navigation.items.length}` }) : null,
3751
+ /* @__PURE__ */ jsx19(
3909
3752
  TourAction,
3910
3753
  {
3911
3754
  href: prevHref,
3912
3755
  disabled: !navigation.hasPrev,
3913
3756
  onClick: prevHref ? onPrev : resolvedPrev,
3914
3757
  "data-keep-action": "tour-prev",
3758
+ preview: navigation.prevItem ? /* @__PURE__ */ jsxs14(Fragment10, { children: [
3759
+ previousLabel,
3760
+ ": ",
3761
+ getItemTitle(navigation.prevItem)
3762
+ ] }) : void 0,
3915
3763
  children: previousLabel
3916
3764
  }
3917
3765
  ),
3918
- /* @__PURE__ */ jsx20(
3766
+ /* @__PURE__ */ jsx19(
3919
3767
  TourAction,
3920
3768
  {
3921
3769
  href: nextHref,
3922
3770
  disabled: !navigation.hasNext,
3923
3771
  onClick: nextHref ? onNext : resolvedNext,
3924
3772
  "data-keep-action": "tour-next",
3773
+ preview: navigation.nextItem ? /* @__PURE__ */ jsxs14(Fragment10, { children: [
3774
+ nextItemLabel,
3775
+ ": ",
3776
+ getItemTitle(navigation.nextItem)
3777
+ ] }) : void 0,
3925
3778
  children: nextItemLabel
3926
3779
  }
3927
3780
  ),
3928
- backHref || onBack ? /* @__PURE__ */ jsx20(TourAction, { href: backHref, onClick: onBack, "data-keep-action": "tour-back", children: listLabel }) : null
3781
+ backHref || onBack ? /* @__PURE__ */ jsx19(TourAction, { href: backHref, onClick: onBack, "data-keep-action": "tour-back", children: listLabel }) : null
3929
3782
  ] });
3930
3783
  }
3931
- function TourAction({ href, disabled = false, onClick, children, ...props }) {
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
+ ] });
3932
3790
  if (href && !disabled) {
3933
- return /* @__PURE__ */ jsx20("a", { ...props, href, onClick: () => void onClick?.(), children });
3934
- }
3935
- return /* @__PURE__ */ jsx20("button", { ...props, type: "button", disabled, onClick: () => void onClick?.(), children });
3936
- }
3937
- var KeepNavigator = KeepTourBar;
3938
- function navigateTo(item, href) {
3939
- if (!item || !href || typeof window === "undefined") return;
3940
- window.location.assign(href);
3941
- }
3942
-
3943
- // src/hooks/useKeepUndo.ts
3944
- import { useKeepContext as useKeepContext6 } from "@keepkit/core/react";
3945
- function useKeepUndo() {
3946
- const context = useKeepContext6();
3947
- const emitFeedback = useKeepUiFeedback();
3948
- const restoredMessage = useUiLabel("restoredMessage");
3949
- return {
3950
- canUndo: context.undo.canUndo,
3951
- undo: async () => {
3952
- const items = context.lastChange?.items ?? (context.lastChange?.item ? [context.lastChange.item] : []);
3953
- await context.undoLastRemoval();
3954
- if (items.length > 0) {
3955
- emitFeedback({ type: "item-restored", item: items[0], items, message: restoredMessage });
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
3956
3800
  }
3957
- },
3958
- message: useUiLabel("undoAvailable"),
3959
- label: useUiLabel("undo")
3960
- };
3961
- }
3962
-
3963
- // src/KeepUndo.tsx
3964
- import { jsx as jsx21, jsxs as jsxs15 } from "react/jsx-runtime";
3965
- function KeepUndo({ children, label, ...props }) {
3966
- const view = useKeepUndo();
3967
- if (!view.canUndo) return null;
3968
- return /* @__PURE__ */ jsxs15("div", { ...props, role: "status", "aria-live": "polite", "data-keepkit": "undo", "data-state": "available", children: [
3969
- children ?? view.message,
3970
- /* @__PURE__ */ jsx21("button", { type: "button", "data-keep-action": "undo", onClick: () => void view.undo(), children: label ?? view.label })
3971
- ] });
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);
3972
3820
  }
3973
3821
 
3974
- // src/status.tsx
3822
+ // src/features/status/status.tsx
3975
3823
  import { isValidElement as isValidElement6 } from "react";
3976
3824
 
3977
- // src/hooks/useStatusViews.ts
3978
- import { useKeepContext as useKeepContext7 } from "@keepkit/core/react";
3979
- import { useEffect as useEffect11, useRef as useRef7, useState as useState12 } from "react";
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";
3980
3828
  function useKeepEmptyState() {
3981
3829
  return useUiLabel("noItems").replace(/\.$/, "");
3982
3830
  }
3983
3831
  function useKeepStatus(status) {
3984
- const context = useKeepContext7();
3832
+ const context = useKeepContext4();
3985
3833
  const resolvedStatus = status ?? getDerivedStatus(context);
3986
3834
  const state = {
3987
3835
  status: resolvedStatus,
@@ -3992,13 +3840,13 @@ function useKeepStatus(status) {
3992
3840
  return { state, defaultLabel: useUiLabel(getStatusLabelKey3(resolvedStatus)) };
3993
3841
  }
3994
3842
  function useKeepAnnouncements(messages) {
3995
- const context = useKeepContext7();
3843
+ const context = useKeepContext4();
3996
3844
  const savedMessage = useUiLabel("savedMessage", messages?.save);
3997
3845
  const removedMessage = useUiLabel("removedMessage", messages?.remove);
3998
3846
  const noteSavedMessage = useUiLabel("noteSavedMessage", messages?.note);
3999
3847
  const [message, setMessage] = useState12("");
4000
- const lastChangeRef = useRef7(void 0);
4001
- useEffect11(() => {
3848
+ const lastChangeRef = useRef6(void 0);
3849
+ useEffect10(() => {
4002
3850
  const change = context.lastChange;
4003
3851
  if (!change || change === lastChangeRef.current) return;
4004
3852
  lastChangeRef.current = change;
@@ -4025,8 +3873,8 @@ function getStatusLabelKey3(status) {
4025
3873
  return "saved";
4026
3874
  }
4027
3875
 
4028
- // src/status.tsx
4029
- import { Fragment as Fragment9, jsx as jsx22, jsxs as jsxs16 } from "react/jsx-runtime";
3876
+ // src/features/status/status.tsx
3877
+ import { Fragment as Fragment11, jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
4030
3878
  function KeepEmptyState({
4031
3879
  title,
4032
3880
  description,
@@ -4038,9 +3886,9 @@ function KeepEmptyState({
4038
3886
  }) {
4039
3887
  const defaultTitle = useKeepEmptyState();
4040
3888
  const contentChildren = asChild && isValidElement6(children) ? void 0 : children;
4041
- const body = contentChildren ?? /* @__PURE__ */ jsxs16(Fragment9, { children: [
4042
- /* @__PURE__ */ jsx22("h2", { children: title ?? defaultTitle }),
4043
- description ? /* @__PURE__ */ jsx22("p", { children: description }) : null,
3889
+ const body = contentChildren ?? /* @__PURE__ */ jsxs15(Fragment11, { children: [
3890
+ /* @__PURE__ */ jsx20("h2", { children: title ?? defaultTitle }),
3891
+ description ? /* @__PURE__ */ jsx20("p", { children: description }) : null,
4044
3892
  action
4045
3893
  ] });
4046
3894
  return renderRoot(
@@ -4082,7 +3930,7 @@ function KeepStatus({
4082
3930
  }
4083
3931
  function KeepAnnouncements({ messages, ...props }) {
4084
3932
  const message = useKeepAnnouncements(messages);
4085
- return /* @__PURE__ */ jsx22(
3933
+ return /* @__PURE__ */ jsx20(
4086
3934
  "div",
4087
3935
  {
4088
3936
  ...props,
@@ -4097,7 +3945,301 @@ function KeepAnnouncements({ messages, ...props }) {
4097
3945
  }
4098
3946
  var KeepAnnouncer = KeepAnnouncements;
4099
3947
 
4100
- // src/theme.tsx
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";
3951
+ function useKeepSyncFeedback() {
3952
+ const { syncState } = useKeepContext5();
3953
+ const emitFeedback = useKeepUiFeedback();
3954
+ const completedMessage = useUiLabel("syncSynced");
3955
+ const failedMessage = useUiLabel("syncFailedMessage");
3956
+ const previousStatus = useRef7("idle");
3957
+ useEffect11(() => {
3958
+ const previous = previousStatus.current;
3959
+ previousStatus.current = syncState.status;
3960
+ if (syncState.status === "error" && previous !== "error") {
3961
+ emitFeedback({ type: "sync-failed", error: syncState.error, message: failedMessage });
3962
+ return;
3963
+ }
3964
+ if (syncState.status === "synced" && (previous === "pending" || previous === "syncing" || previous === "conflict" || previous === "error")) {
3965
+ emitFeedback({ type: "sync-completed", message: completedMessage });
3966
+ }
3967
+ }, [completedMessage, emitFeedback, failedMessage, syncState.error, syncState.status]);
3968
+ }
3969
+
3970
+ // src/features/sync/KeepSyncFeedbackObserver.tsx
3971
+ function KeepSyncFeedbackObserver() {
3972
+ useKeepSyncFeedback();
3973
+ return null;
3974
+ }
3975
+
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";
3979
+ function useKeepSyncRecoveryDialog(options) {
3980
+ const { open, onOpenChange, conflicts, onManualMerge } = options;
3981
+ const context = useKeepContext6();
3982
+ const conflictList = conflicts ?? context.syncState.conflicts ?? [];
3983
+ const hasRecovery = conflictList.length > 0 || context.syncState.status === "error" || Boolean(context.error);
3984
+ const [dismissed, setDismissed] = useState13(false);
3985
+ const [busyId, setBusyId] = useState13();
3986
+ const [error, setError] = useState13();
3987
+ useEffect12(() => {
3988
+ if (hasRecovery) setDismissed(false);
3989
+ }, [hasRecovery]);
3990
+ return {
3991
+ conflictList,
3992
+ isOpen: open ?? (hasRecovery && !dismissed),
3993
+ busyId,
3994
+ error,
3995
+ showBackupRecovery: context.syncState.status === "error" || Boolean(context.error),
3996
+ close: () => {
3997
+ setDismissed(true);
3998
+ onOpenChange?.(false);
3999
+ },
4000
+ resolve: async (conflict, resolution) => {
4001
+ setError(void 0);
4002
+ setBusyId(conflict.id);
4003
+ try {
4004
+ const merged = resolution === "manual" ? await onManualMerge?.(conflict) : void 0;
4005
+ if (resolution === "manual" && !merged) throw new Error("A manual merge result is required.");
4006
+ await context.resolveSyncConflict(conflict.id, resolution, merged);
4007
+ } catch (cause) {
4008
+ setError(cause);
4009
+ } finally {
4010
+ setBusyId(void 0);
4011
+ }
4012
+ },
4013
+ labels: {
4014
+ close: useUiLabel("close"),
4015
+ title: useUiLabel("resolveSync"),
4016
+ conflict: useUiLabel("syncConflict"),
4017
+ keepLocal: useUiLabel("keepLocal"),
4018
+ useServer: useUiLabel("useServer"),
4019
+ manualMerge: useUiLabel("manualMerge"),
4020
+ localVersion: useUiLabel("localVersion"),
4021
+ remoteVersion: useUiLabel("remoteVersion"),
4022
+ updatedAt: useUiLabel("updatedAt"),
4023
+ note: useUiLabel("note"),
4024
+ backupRecovery: useUiLabel("backupRecovery"),
4025
+ backupRecoveryDescription: useUiLabel("backupRecoveryDescription"),
4026
+ error: useUiLabel("error")
4027
+ }
4028
+ };
4029
+ }
4030
+
4031
+ // src/features/sync/KeepSyncRecoveryDialog.tsx
4032
+ import { jsx as jsx21, jsxs as jsxs16 } from "react/jsx-runtime";
4033
+ function KeepSyncRecoveryDialog({
4034
+ open,
4035
+ onOpenChange,
4036
+ conflicts,
4037
+ onManualMerge,
4038
+ backup,
4039
+ showBackupControls = true,
4040
+ title,
4041
+ children,
4042
+ className,
4043
+ ...props
4044
+ }) {
4045
+ const view = useKeepSyncRecoveryDialog({ open, onOpenChange, conflicts, onManualMerge });
4046
+ if (!view.isOpen) return null;
4047
+ return /* @__PURE__ */ jsxs16(
4048
+ "section",
4049
+ {
4050
+ ...props,
4051
+ className,
4052
+ role: "dialog",
4053
+ "aria-modal": "true",
4054
+ "aria-labelledby": "keepkit-sync-recovery-title",
4055
+ "aria-describedby": view.error ? "keepkit-sync-recovery-error" : void 0,
4056
+ "aria-busy": view.busyId !== void 0,
4057
+ "data-keepkit": "sync-recovery",
4058
+ "data-state": view.conflictList.length > 0 ? "conflict" : "error",
4059
+ "data-loading": view.busyId !== void 0 ? "true" : void 0,
4060
+ children: [
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 })
4064
+ ] }),
4065
+ children,
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(
4072
+ ConflictPreview,
4073
+ {
4074
+ item: conflict.operation.item,
4075
+ heading: view.labels.localVersion,
4076
+ updatedAtLabel: view.labels.updatedAt,
4077
+ noteLabel: view.labels.note,
4078
+ side: "local"
4079
+ }
4080
+ ),
4081
+ /* @__PURE__ */ jsx21(
4082
+ ConflictPreview,
4083
+ {
4084
+ item: conflict.remote,
4085
+ heading: view.labels.remoteVersion,
4086
+ updatedAtLabel: view.labels.updatedAt,
4087
+ noteLabel: view.labels.note,
4088
+ side: "remote"
4089
+ }
4090
+ )
4091
+ ] }),
4092
+ /* @__PURE__ */ jsxs16("div", { children: [
4093
+ /* @__PURE__ */ jsx21(
4094
+ "button",
4095
+ {
4096
+ type: "button",
4097
+ "data-keep-action": "keep-local",
4098
+ onClick: () => void view.resolve(conflict, "local"),
4099
+ disabled: view.busyId !== void 0,
4100
+ children: view.labels.keepLocal
4101
+ }
4102
+ ),
4103
+ /* @__PURE__ */ jsx21(
4104
+ "button",
4105
+ {
4106
+ type: "button",
4107
+ "data-keep-action": "use-server",
4108
+ onClick: () => void view.resolve(conflict, "remote"),
4109
+ disabled: view.busyId !== void 0,
4110
+ children: view.labels.useServer
4111
+ }
4112
+ ),
4113
+ /* @__PURE__ */ jsx21(
4114
+ "button",
4115
+ {
4116
+ type: "button",
4117
+ "data-keep-action": "manual-merge",
4118
+ onClick: () => void view.resolve(conflict, "manual"),
4119
+ disabled: view.busyId !== void 0 || !onManualMerge,
4120
+ children: view.labels.manualMerge
4121
+ }
4122
+ )
4123
+ ] })
4124
+ ] }, conflict.id))
4125
+ ] }) : 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)
4130
+ ] }) : 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
4132
+ ]
4133
+ }
4134
+ );
4135
+ }
4136
+ function ConflictPreview({
4137
+ item,
4138
+ heading,
4139
+ updatedAtLabel,
4140
+ noteLabel,
4141
+ side
4142
+ }) {
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" })
4149
+ ] }),
4150
+ /* @__PURE__ */ jsxs16("div", { children: [
4151
+ /* @__PURE__ */ jsx21("dt", { children: noteLabel }),
4152
+ /* @__PURE__ */ jsx21("dd", { children: item?.note || "\u2014" })
4153
+ ] })
4154
+ ] })
4155
+ ] });
4156
+ }
4157
+ function formatConflictDate(timestamp) {
4158
+ return new Date(timestamp).toISOString().slice(0, 10);
4159
+ }
4160
+
4161
+ // src/features/sync/hooks/useKeepSyncStatusBanner.ts
4162
+ import { useKeepContext as useKeepContext7 } from "@keepkit/core/react";
4163
+ function useKeepSyncStatusBanner({ onRetry, children }) {
4164
+ const context = useKeepContext7();
4165
+ const retryLabel = useUiLabel("retrySync");
4166
+ const resolveLabel = useUiLabel("resolveSync");
4167
+ const conflictLabel = useUiLabel("syncConflict");
4168
+ const pendingLabel = useUiLabel("syncPending");
4169
+ const syncedLabel = useUiLabel("syncSynced");
4170
+ const status = context.syncState.status;
4171
+ const hasConflicts = (context.syncState.conflicts?.length ?? 0) > 0 || context.syncState.conflictIds.length > 0;
4172
+ const message = children ?? (status === "error" ? getErrorMessage5(context.syncState.error) : status === "conflict" || hasConflicts ? conflictLabel : status === "pending" || status === "syncing" ? pendingLabel : syncedLabel);
4173
+ return {
4174
+ status,
4175
+ hasConflicts,
4176
+ message,
4177
+ isMutating: context.isMutating,
4178
+ role: status === "error" || status === "conflict" || hasConflicts ? "alert" : "status",
4179
+ showRetry: status === "error" || status === "pending" || status === "syncing",
4180
+ retryLabel,
4181
+ resolveLabel,
4182
+ retry: async () => {
4183
+ if (onRetry) {
4184
+ await onRetry();
4185
+ return;
4186
+ }
4187
+ await context.flushSync();
4188
+ }
4189
+ };
4190
+ }
4191
+ function getErrorMessage5(error) {
4192
+ return error instanceof Error ? error.message : "Sync failed.";
4193
+ }
4194
+
4195
+ // src/features/sync/KeepSyncStatusBanner.tsx
4196
+ import { jsx as jsx22, jsxs as jsxs17 } from "react/jsx-runtime";
4197
+ function KeepSyncStatusBanner({
4198
+ onRetry,
4199
+ onResolveConflicts,
4200
+ children,
4201
+ className,
4202
+ ...props
4203
+ }) {
4204
+ const view = useKeepSyncStatusBanner({ onRetry, children });
4205
+ if (view.status === "idle" && !view.hasConflicts) return null;
4206
+ return /* @__PURE__ */ jsxs17(
4207
+ "aside",
4208
+ {
4209
+ ...props,
4210
+ className,
4211
+ role: props.role ?? view.role,
4212
+ "aria-live": props["aria-live"] ?? "polite",
4213
+ "data-keepkit": "sync-status",
4214
+ "data-state": view.status,
4215
+ children: [
4216
+ /* @__PURE__ */ jsx22("p", { children: view.message }),
4217
+ view.showRetry ? /* @__PURE__ */ jsx22(
4218
+ "button",
4219
+ {
4220
+ type: "button",
4221
+ "data-keep-action": "retry-sync",
4222
+ onClick: () => void view.retry(),
4223
+ disabled: view.isMutating,
4224
+ children: view.retryLabel
4225
+ }
4226
+ ) : null,
4227
+ view.hasConflicts ? /* @__PURE__ */ jsx22(
4228
+ "button",
4229
+ {
4230
+ type: "button",
4231
+ "data-keep-action": "resolve-conflicts",
4232
+ onClick: onResolveConflicts,
4233
+ disabled: !onResolveConflicts,
4234
+ children: view.resolveLabel
4235
+ }
4236
+ ) : null
4237
+ ]
4238
+ }
4239
+ );
4240
+ }
4241
+
4242
+ // src/foundation/theme.tsx
4101
4243
  import { cloneElement as cloneElement2, isValidElement as isValidElement7 } from "react";
4102
4244
  import { jsx as jsx23 } from "react/jsx-runtime";
4103
4245
  var keepThemeNames = [
@@ -4182,7 +4324,7 @@ import {
4182
4324
  LocalStorageSyncQueueAdapter,
4183
4325
  SyncStorageAdapter
4184
4326
  } from "@keepkit/core/storage";
4185
- import { jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
4327
+ import { jsx as jsx24, jsxs as jsxs18 } from "react/jsx-runtime";
4186
4328
  function KeepKitProvider({
4187
4329
  labels,
4188
4330
  locale,
@@ -4216,7 +4358,7 @@ function KeepKitProvider({
4216
4358
  className: themeClassName,
4217
4359
  style: themeStyle,
4218
4360
  asChild: themeAsChild,
4219
- children: /* @__PURE__ */ jsxs17(CoreKeepProvider, { ...providerProps, children: [
4361
+ children: /* @__PURE__ */ jsxs18(CoreKeepProvider, { ...providerProps, children: [
4220
4362
  /* @__PURE__ */ jsx24(KeepSyncFeedbackObserver, {}),
4221
4363
  children,
4222
4364
  /* @__PURE__ */ jsx24(KeepAnnouncements, {})