@ryuzaki13/react-foundation-lib 1.5.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/hooks/index.js +81 -75
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/useDndSortableSensors.d.ts +15 -2
- package/dist/hooks/useDndSortableSensors.d.ts.map +1 -1
- package/dist/query-client/index.d.ts +1 -0
- package/dist/query-client/index.d.ts.map +1 -1
- package/dist/query-client/index.js +86 -46
- package/dist/query-client/index.js.map +1 -1
- package/dist/query-client/invalidationBroadcast.d.ts +20 -0
- package/dist/query-client/invalidationBroadcast.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/hooks/README.mdx +29 -20
package/README.md
CHANGED
|
@@ -655,6 +655,7 @@ Service worker runtime helpers живут в [`pwa/serviceWorker.ts`](./src/pwa/
|
|
|
655
655
|
| `persistedQueryMeta`, `shouldPersistQuery`, `createReactQueryPersister`, `createIndexedDbQueryStorage` | Opt-in persistence справочников в IndexedDB. |
|
|
656
656
|
| `configureQueryPersistenceProjectAdapter`, `getQueryPersistenceProjectAdapter` | Host namespace и buster для persistent cache. |
|
|
657
657
|
| `installReactQueryBroadcast`, `broadcastCacheEvent`, `setBroadcastFn` | Sync query cache и cache events между вкладками. |
|
|
658
|
+
| `installQueryInvalidationBroadcast` | Scoped invalidation между вкладками без передачи Query data. |
|
|
658
659
|
| `sessionScopedQueryMeta`, `resetSessionScopedQueries`, `installSessionScopedQueryReset` | Очистка auth/session-dependent query локально и между вкладками. |
|
|
659
660
|
| `onMutateOptimistic`, `onErrorOptimistic`, `onSuccessOptimistic`, `onSettledOptimistic` | Helpers для точечного optimistic update. |
|
|
660
661
|
|
package/dist/hooks/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createLazyComponent as e, findFirstEnabledIndex as t, findLastEnabledIndex as n, findNextEnabledIndex as r, handleKeyboardActivation as i } from "../utils/index.js";
|
|
2
2
|
import { useCallback as a, useEffect as o, useLayoutEffect as s, useMemo as c, useRef as l, useState as u } from "react";
|
|
3
|
-
import { KeyboardSensor as d,
|
|
4
|
-
import { sortableKeyboardCoordinates as
|
|
5
|
-
import { autoPlacement as
|
|
6
|
-
import { useLocation as
|
|
3
|
+
import { KeyboardSensor as d, MouseSensor as f, PointerSensor as p, TouchSensor as m, useSensor as h, useSensors as g } from "@dnd-kit/core";
|
|
4
|
+
import { sortableKeyboardCoordinates as _ } from "@dnd-kit/sortable";
|
|
5
|
+
import { autoPlacement as ee, autoUpdate as te, flip as ne, offset as v, shift as re, size as ie, useDismiss as ae, useFloating as y, useInteractions as b, useRole as x } from "@floating-ui/react";
|
|
6
|
+
import { useLocation as S, useNavigate as C, useSearch as w } from "@tanstack/react-router";
|
|
7
7
|
//#region src/hooks/useBlockBrowserZoom.ts
|
|
8
|
-
function
|
|
8
|
+
function T(e) {
|
|
9
9
|
o(() => {
|
|
10
10
|
if (!e) return;
|
|
11
11
|
let t = (e) => {
|
|
@@ -16,7 +16,7 @@ function x(e) {
|
|
|
16
16
|
}
|
|
17
17
|
//#endregion
|
|
18
18
|
//#region src/hooks/useDebounce.ts
|
|
19
|
-
function
|
|
19
|
+
function E(e, t) {
|
|
20
20
|
let [n, r] = u(e);
|
|
21
21
|
return o(() => {
|
|
22
22
|
let n = setTimeout(() => {
|
|
@@ -27,7 +27,7 @@ function S(e, t) {
|
|
|
27
27
|
}
|
|
28
28
|
//#endregion
|
|
29
29
|
//#region src/hooks/useDebounceCallback.ts
|
|
30
|
-
function
|
|
30
|
+
function D(e, t, n) {
|
|
31
31
|
let r = l(e);
|
|
32
32
|
o(() => {
|
|
33
33
|
r.current = e;
|
|
@@ -56,13 +56,19 @@ function C(e, t, n) {
|
|
|
56
56
|
}
|
|
57
57
|
//#endregion
|
|
58
58
|
//#region src/hooks/useDndSortableSensors.ts
|
|
59
|
-
function
|
|
60
|
-
let t = typeof e == "number" ? e : e.activationDistance ?? 6
|
|
61
|
-
|
|
59
|
+
function O(e = 6) {
|
|
60
|
+
let t = typeof e == "number" ? { activationDistance: e } : e, n = t.activationDistance ?? 6, r = h(p, { activationConstraint: { distance: n } }), i = h(f, { activationConstraint: { distance: n } }), a = h(m, { activationConstraint: {
|
|
61
|
+
delay: t.touchActivation?.delay ?? 0,
|
|
62
|
+
tolerance: t.touchActivation?.tolerance ?? n
|
|
63
|
+
} }), o = h(d, {
|
|
64
|
+
coordinateGetter: t.keyboardCoordinateGetter ?? _,
|
|
65
|
+
keyboardCodes: t.keyboardCodes
|
|
66
|
+
});
|
|
67
|
+
return g(t.touchActivation ? i : r, t.touchActivation ? a : void 0, o);
|
|
62
68
|
}
|
|
63
69
|
//#endregion
|
|
64
70
|
//#region src/hooks/useFloatingListbox.ts
|
|
65
|
-
var
|
|
71
|
+
var k = /* @__PURE__ */ new WeakMap(), A = /* @__PURE__ */ new WeakMap(), j = [
|
|
66
72
|
"bottom-start",
|
|
67
73
|
"bottom-end",
|
|
68
74
|
"top-start",
|
|
@@ -72,32 +78,32 @@ var T = /* @__PURE__ */ new WeakMap(), E = /* @__PURE__ */ new WeakMap(), D = [
|
|
|
72
78
|
"right-start",
|
|
73
79
|
"right-end"
|
|
74
80
|
];
|
|
75
|
-
function
|
|
76
|
-
let
|
|
77
|
-
allowedPlacements:
|
|
81
|
+
function M({ options: e, selectedIndex: a, open: c, onOpenChange: d, getOptionDisabled: f, onSelect: p, disabled: m, placement: h = "bottom-start", placementStrategy: g = "flip", closeOnSelect: _ = !0, focusFloatingOnOpen: S = !0, allowOpenWithoutOptions: C = !1, restoreFocusOnClose: w = !0, resolveFloatingSize: T }) {
|
|
82
|
+
let E = l([]), [D, O] = u(!1), M = c ?? D, N = d ?? O, [P, F] = u(a >= 0 ? a : t(e, f)), [I] = u(() => ({})), L = (n) => e.length === 0 ? -1 : n >= 0 && n < e.length && !f?.(e[n]) ? n : a >= 0 && a < e.length && !f?.(e[a]) ? a : t(e, f), R = L(P), oe = g === "auto" ? ee({
|
|
83
|
+
allowedPlacements: j,
|
|
78
84
|
crossAxis: !1,
|
|
79
85
|
padding: 8
|
|
80
|
-
}) :
|
|
81
|
-
open:
|
|
82
|
-
onOpenChange:
|
|
86
|
+
}) : ne({ padding: 8 }), { refs: z, floatingStyles: se, context: B, update: V } = y({
|
|
87
|
+
open: M,
|
|
88
|
+
onOpenChange: N,
|
|
83
89
|
placement: h,
|
|
84
90
|
transform: !1,
|
|
85
91
|
strategy: "fixed",
|
|
86
92
|
middleware: [
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
93
|
+
v(4),
|
|
94
|
+
oe,
|
|
95
|
+
re({ padding: 8 }),
|
|
96
|
+
ie({
|
|
91
97
|
padding: 8,
|
|
92
98
|
apply({ availableWidth: e, availableHeight: t, rects: n, elements: r }) {
|
|
93
|
-
let i = r.floating.ownerDocument.documentElement, a = r.floating.style, o =
|
|
99
|
+
let i = r.floating.ownerDocument.documentElement, a = r.floating.style, o = A.get(I)?.({
|
|
94
100
|
availableWidth: e,
|
|
95
101
|
availableHeight: t,
|
|
96
102
|
viewportWidth: i.clientWidth,
|
|
97
103
|
viewportHeight: i.clientHeight,
|
|
98
104
|
referenceWidth: n.reference.width,
|
|
99
105
|
referenceHeight: n.reference.height
|
|
100
|
-
}), s =
|
|
106
|
+
}), s = k.get(r.floating);
|
|
101
107
|
for (let e of s?.customProperties ?? []) a.removeProperty(e);
|
|
102
108
|
for (let [e, t] of s?.standardProperties ?? []) a[e] = t;
|
|
103
109
|
a.minWidth = `${n.reference.width}px`, a.maxHeight = `${Math.max(t, 120)}px`;
|
|
@@ -113,93 +119,93 @@ function O({ options: e, selectedIndex: a, open: c, onOpenChange: d, getOptionDi
|
|
|
113
119
|
l.set(n, a[n]), a[n] = t;
|
|
114
120
|
}
|
|
115
121
|
}
|
|
116
|
-
|
|
122
|
+
k.set(r.floating, {
|
|
117
123
|
customProperties: c,
|
|
118
124
|
standardProperties: l
|
|
119
125
|
});
|
|
120
126
|
}
|
|
121
127
|
})
|
|
122
128
|
],
|
|
123
|
-
whileElementsMounted:
|
|
124
|
-
}), H =
|
|
125
|
-
s(() => (
|
|
126
|
-
|
|
127
|
-
}), [
|
|
128
|
-
!
|
|
129
|
+
whileElementsMounted: te
|
|
130
|
+
}), H = T !== void 0;
|
|
131
|
+
s(() => (T ? A.set(I, T) : A.delete(I), () => {
|
|
132
|
+
A.delete(I);
|
|
133
|
+
}), [I, T]), s(() => {
|
|
134
|
+
!M || !H || V();
|
|
129
135
|
}, [
|
|
130
136
|
H,
|
|
131
137
|
e.length,
|
|
132
|
-
|
|
138
|
+
M,
|
|
133
139
|
V
|
|
134
140
|
]);
|
|
135
|
-
let U =
|
|
141
|
+
let U = ae(B), W = x(B, { role: "listbox" }), { getFloatingProps: G } = b([U, W]), ce = C || e.length > 0;
|
|
136
142
|
o(() => {
|
|
137
|
-
!
|
|
138
|
-
}, [
|
|
139
|
-
!
|
|
140
|
-
|
|
143
|
+
!M || R < 0 || E.current[R]?.scrollIntoView({ block: "nearest" });
|
|
144
|
+
}, [R, M]), o(() => {
|
|
145
|
+
!M || !S || requestAnimationFrame(() => {
|
|
146
|
+
z.floating.current?.focus();
|
|
141
147
|
});
|
|
142
148
|
}, [
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
149
|
+
S,
|
|
150
|
+
M,
|
|
151
|
+
z
|
|
146
152
|
]);
|
|
147
153
|
let K = () => {
|
|
148
|
-
if (
|
|
149
|
-
let e =
|
|
154
|
+
if (N(!1), w) {
|
|
155
|
+
let e = z.domReference.current;
|
|
150
156
|
e instanceof HTMLElement && e.focus();
|
|
151
157
|
}
|
|
152
158
|
}, q = (t) => {
|
|
153
|
-
m || !ce || (e.length > 0 &&
|
|
159
|
+
m || !ce || (e.length > 0 && F(L(t)), N(!0));
|
|
154
160
|
}, J = () => {
|
|
155
161
|
q(a >= 0 ? a : t(e, f));
|
|
156
162
|
}, le = () => {
|
|
157
|
-
if (
|
|
163
|
+
if (M) {
|
|
158
164
|
K();
|
|
159
165
|
return;
|
|
160
166
|
}
|
|
161
167
|
J();
|
|
162
168
|
}, Y = (e) => {
|
|
163
|
-
f?.(e) || (p?.(e),
|
|
169
|
+
f?.(e) || (p?.(e), _ && K());
|
|
164
170
|
}, X = () => {
|
|
165
|
-
if (
|
|
166
|
-
let t = e[
|
|
171
|
+
if (R < 0) return;
|
|
172
|
+
let t = e[R];
|
|
167
173
|
t !== void 0 && Y(t);
|
|
168
174
|
}, Z = () => {
|
|
169
|
-
|
|
175
|
+
F(t(e, f));
|
|
170
176
|
}, Q = () => {
|
|
171
|
-
|
|
177
|
+
F(n(e, f));
|
|
172
178
|
}, ue = (i) => {
|
|
173
179
|
if (!m) {
|
|
174
180
|
if (i.key === "ArrowDown") {
|
|
175
|
-
if (i.preventDefault(), !
|
|
181
|
+
if (i.preventDefault(), !M) {
|
|
176
182
|
q(a >= 0 ? a : t(e, f));
|
|
177
183
|
return;
|
|
178
184
|
}
|
|
179
|
-
|
|
185
|
+
F((t) => r(e, L(t), 1, f));
|
|
180
186
|
return;
|
|
181
187
|
}
|
|
182
188
|
if (i.key === "ArrowUp") {
|
|
183
|
-
if (i.preventDefault(), !
|
|
189
|
+
if (i.preventDefault(), !M) {
|
|
184
190
|
q(a >= 0 ? a : n(e, f));
|
|
185
191
|
return;
|
|
186
192
|
}
|
|
187
|
-
|
|
193
|
+
F((t) => r(e, L(t), -1, f));
|
|
188
194
|
return;
|
|
189
195
|
}
|
|
190
|
-
if (i.key === "Home" &&
|
|
196
|
+
if (i.key === "Home" && M) {
|
|
191
197
|
i.preventDefault(), Z();
|
|
192
198
|
return;
|
|
193
199
|
}
|
|
194
|
-
if (i.key === "End" &&
|
|
200
|
+
if (i.key === "End" && M) {
|
|
195
201
|
i.preventDefault(), Q();
|
|
196
202
|
return;
|
|
197
203
|
}
|
|
198
|
-
if (i.key === "Escape" &&
|
|
204
|
+
if (i.key === "Escape" && M) {
|
|
199
205
|
i.preventDefault(), K();
|
|
200
206
|
return;
|
|
201
207
|
}
|
|
202
|
-
i.key === "Tab" &&
|
|
208
|
+
i.key === "Tab" && M && K();
|
|
203
209
|
}
|
|
204
210
|
}, de = (t) => {
|
|
205
211
|
if (t.target !== t.currentTarget) {
|
|
@@ -207,11 +213,11 @@ function O({ options: e, selectedIndex: a, open: c, onOpenChange: d, getOptionDi
|
|
|
207
213
|
return;
|
|
208
214
|
}
|
|
209
215
|
if (t.key === "ArrowDown") {
|
|
210
|
-
t.preventDefault(),
|
|
216
|
+
t.preventDefault(), F((t) => r(e, L(t), 1, f));
|
|
211
217
|
return;
|
|
212
218
|
}
|
|
213
219
|
if (t.key === "ArrowUp") {
|
|
214
|
-
t.preventDefault(),
|
|
220
|
+
t.preventDefault(), F((t) => r(e, L(t), -1, f));
|
|
215
221
|
return;
|
|
216
222
|
}
|
|
217
223
|
if (t.key === "Home") {
|
|
@@ -231,17 +237,17 @@ function O({ options: e, selectedIndex: a, open: c, onOpenChange: d, getOptionDi
|
|
|
231
237
|
}
|
|
232
238
|
}, $ = (e, t) => `${e}-option-${t}`;
|
|
233
239
|
return {
|
|
234
|
-
open:
|
|
235
|
-
setOpen:
|
|
236
|
-
activeIndex:
|
|
237
|
-
refs:
|
|
240
|
+
open: M,
|
|
241
|
+
setOpen: N,
|
|
242
|
+
activeIndex: R,
|
|
243
|
+
refs: z,
|
|
238
244
|
context: B,
|
|
239
|
-
floatingStyles:
|
|
245
|
+
floatingStyles: se,
|
|
240
246
|
getFloatingProps: G,
|
|
241
|
-
setReference:
|
|
242
|
-
setFloating:
|
|
247
|
+
setReference: z.setReference,
|
|
248
|
+
setFloating: z.setFloating,
|
|
243
249
|
setOptionRef: (e, t) => {
|
|
244
|
-
|
|
250
|
+
E.current[e] = t;
|
|
245
251
|
},
|
|
246
252
|
close: K,
|
|
247
253
|
openList: J,
|
|
@@ -252,19 +258,19 @@ function O({ options: e, selectedIndex: a, open: c, onOpenChange: d, getOptionDi
|
|
|
252
258
|
handleReferenceKeyDown: ue,
|
|
253
259
|
handleFloatingKeyDown: de,
|
|
254
260
|
getOptionId: $,
|
|
255
|
-
getActiveOptionId: (e) =>
|
|
261
|
+
getActiveOptionId: (e) => R >= 0 ? $(e, R) : void 0
|
|
256
262
|
};
|
|
257
263
|
}
|
|
258
264
|
//#endregion
|
|
259
265
|
//#region src/hooks/useLazyComponent.ts
|
|
260
|
-
var
|
|
266
|
+
var N = (t, n, r) => c(() => e(t, n, r), [
|
|
261
267
|
r,
|
|
262
268
|
t,
|
|
263
269
|
n
|
|
264
270
|
]);
|
|
265
271
|
//#endregion
|
|
266
272
|
//#region src/hooks/useReferenceTrace.ts
|
|
267
|
-
function
|
|
273
|
+
function P(e, t) {
|
|
268
274
|
let n = l(t);
|
|
269
275
|
o(() => {
|
|
270
276
|
if (!__DEV__) return;
|
|
@@ -278,8 +284,8 @@ function A(e, t) {
|
|
|
278
284
|
}
|
|
279
285
|
//#endregion
|
|
280
286
|
//#region src/hooks/useSearchLink.ts
|
|
281
|
-
function
|
|
282
|
-
let e =
|
|
287
|
+
function F() {
|
|
288
|
+
let e = S();
|
|
283
289
|
return { buildLink: (t) => {
|
|
284
290
|
let n = new URLSearchParams(e.searchStr);
|
|
285
291
|
t ||= [];
|
|
@@ -293,8 +299,8 @@ function j() {
|
|
|
293
299
|
}
|
|
294
300
|
//#endregion
|
|
295
301
|
//#region src/hooks/useSearchParams.ts
|
|
296
|
-
function
|
|
297
|
-
let t =
|
|
302
|
+
function I(e) {
|
|
303
|
+
let t = w({ strict: !1 }), n = C();
|
|
298
304
|
return {
|
|
299
305
|
params: t,
|
|
300
306
|
setParams: (e) => n({
|
|
@@ -310,7 +316,7 @@ function M(e) {
|
|
|
310
316
|
}
|
|
311
317
|
//#endregion
|
|
312
318
|
//#region src/hooks/useThrottledCallback.ts
|
|
313
|
-
function
|
|
319
|
+
function L(e, t, n) {
|
|
314
320
|
let r = l(e);
|
|
315
321
|
o(() => {
|
|
316
322
|
r.current = e;
|
|
@@ -348,6 +354,6 @@ function N(e, t, n) {
|
|
|
348
354
|
};
|
|
349
355
|
}
|
|
350
356
|
//#endregion
|
|
351
|
-
export {
|
|
357
|
+
export { T as useBlockBrowserZoom, E as useDebounce, D as useDebouncedCallback, O as useDndSortableSensors, M as useFloatingListbox, N as useLazyComponent, P as useReferenceTrace, F as useSearchLink, I as useSearchParams, L as useThrottledCallback };
|
|
352
358
|
|
|
353
359
|
//# sourceMappingURL=index.js.map
|
package/dist/hooks/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/hooks/useBlockBrowserZoom.ts","../../src/hooks/useDebounce.ts","../../src/hooks/useDebounceCallback.ts","../../src/hooks/useDndSortableSensors.ts","../../src/hooks/useFloatingListbox.ts","../../src/hooks/useLazyComponent.ts","../../src/hooks/useReferenceTrace.ts","../../src/hooks/useSearchLink.ts","../../src/hooks/useSearchParams.ts","../../src/hooks/useThrottledCallback.ts"],"sourcesContent":["import { useEffect } from \"react\";\n\n/**\n * Глобально отключает браузерный zoom (ctrl+wheel).\n *\n * @param enabled если true — блокируем нативный зум страницы\n */\nexport function useBlockBrowserZoom(enabled: boolean) {\n\tuseEffect(() => {\n\t\tif (!enabled) return;\n\n\t\tconst handler = (e: WheelEvent) => {\n\t\t\tif (e.ctrlKey) {\n\t\t\t\te.preventDefault();\n\t\t\t}\n\t\t};\n\n\t\twindow.addEventListener(\"wheel\", handler, { passive: false });\n\t\treturn () => window.removeEventListener(\"wheel\", handler);\n\t}, [enabled]);\n}\n","import { useEffect, useState } from \"react\";\n\nexport function useDebounce<T>(value: T, delay: number): T {\n\tconst [debouncedValue, setDebouncedValue] = useState<T>(value);\n\n\tuseEffect(() => {\n\t\tconst timeoutId = setTimeout(() => {\n\t\t\tsetDebouncedValue(value);\n\t\t}, delay);\n\n\t\treturn () => clearTimeout(timeoutId);\n\t}, [value, delay]);\n\n\treturn debouncedValue;\n}\n","import { useCallback, useEffect, useRef } from \"react\";\n\n/**\n * Дебаунсит вызов колбэка: частые вызовы `call(value)` не вызывают `cb` сразу,\n * а откладывают его на `delayMs` после последнего вызова.\n *\n * Важно:\n * - Всегда сохраняется *последнее* значение (last-write-wins).\n * - `flush()` немедленно вызывает cb с последним сохранённым значением (если оно есть).\n * - `cancel()` отменяет таймер и забывает последнее значение.\n * - На unmount автоматически вызывается `cancel()` чтобы не было setState после размонтирования.\n *\n * Типовой кейс:\n * - onNodesChange -> call(snapshot)\n * - pointerup / blur / unmount -> flush() (или cancel(), если не надо сохранять)\n */\nexport function useDebouncedCallback<T>(cb: (arg: T) => void, delayMs: number, options?: { flushOnUnmount?: boolean }) {\n\t/**\n\t * Храним актуальную версию cb в ref, чтобы:\n\t * - не пересоздавать дебаунсер при каждом рендере,\n\t * - не ловить устаревший замыканием cb (stale closure),\n\t * - при этом всегда вызывать самую свежую функцию.\n\t */\n\tconst cbRef = useRef<(arg: T) => void>(cb);\n\n\t/**\n\t * Обновляем актуальную версию cb после commit'а\n\t */\n\tuseEffect(() => {\n\t\tcbRef.current = cb;\n\t}, [cb]);\n\n\t/**\n\t * id таймера. null означает что таймер не активен.\n\t */\n\tconst tRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n\t/**\n\t * Последнее значение, которое мы хотим \"закоммитить\" в cb.\n\t * Используем стратегию last-write-wins.\n\t */\n\tconst lastRef = useRef<T | null>(null);\n\n\t/**\n\t * cancel():\n\t * - останавливаем таймер, если он был,\n\t * - очищаем последнее значение (ничего не будет вызвано).\n\t */\n\tconst cancel = useCallback(() => {\n\t\tif (tRef.current) {\n\t\t\tclearTimeout(tRef.current);\n\t\t\ttRef.current = null;\n\t\t}\n\t\tlastRef.current = null;\n\t}, []);\n\n\t/**\n\t * flush():\n\t * - если есть отложенное значение, вызываем cb немедленно,\n\t * - сбрасываем таймер и lastRef.\n\t *\n\t * Используется, когда нужно гарантированно сохранить \"последнее\" прямо сейчас:\n\t * - unmount\n\t * - visibilitychange/blur\n\t * - pointerup/mouseup (конец взаимодействия)\n\t */\n\tconst flush = useCallback(() => {\n\t\tconst v = lastRef.current;\n\t\tif (v === null) return;\n\n\t\t// Сбрасываем before-call, чтобы если cb вызовет call() рекурсивно,\n\t\t// мы не держали старое значение.\n\t\tlastRef.current = null;\n\n\t\tif (tRef.current) {\n\t\t\tclearTimeout(tRef.current);\n\t\t\ttRef.current = null;\n\t\t}\n\n\t\tcbRef.current(v);\n\t}, []);\n\n\t/**\n\t * call(arg):\n\t * - сохраняем arg как \"последнее значение\"\n\t * - перезапускаем таймер\n\t * - по истечении delayMs вызываем flush()\n\t */\n\tconst call = useCallback(\n\t\t(arg: T) => {\n\t\t\tlastRef.current = arg;\n\n\t\t\t// Перезапуск таймера -> классический debounce\n\t\t\tif (tRef.current) clearTimeout(tRef.current);\n\n\t\t\ttRef.current = setTimeout(() => {\n\t\t\t\ttRef.current = null;\n\t\t\t\tflush();\n\t\t\t}, delayMs);\n\t\t},\n\t\t[delayMs, flush]\n\t);\n\n\t/**\n\t * Авто-cancel на unmount:\n\t * - убирает pending timeout\n\t * - предотвращает вызовы cb после размонтирования компонента\n\t */\n\tuseEffect(() => {\n\t\treturn () => {\n\t\t\tif (options?.flushOnUnmount) {\n\t\t\t\tflush();\n\t\t\t} else {\n\t\t\t\tcancel();\n\t\t\t}\n\t\t};\n\t}, [cancel, flush, options?.flushOnUnmount]);\n\n\treturn { call, flush, cancel };\n}\n","import { KeyboardSensor, PointerSensor, useSensor, useSensors } from \"@dnd-kit/core\";\nimport { sortableKeyboardCoordinates } from \"@dnd-kit/sortable\";\n\nexport interface UseDndSortableSensorsOptions {\n\tactivationDistance?: number;\n}\n\n/**\n * Стандартная конфигурация DnD-сенсоров для сортируемых списков.\n *\n * Зачем: исключить дублирование одинаковой настройки PointerSensor + KeyboardSensor\n * в каждом хуке персонализации (колонки, сортировки, группировки).\n *\n * @param distance — минимальное смещение указателя (px) для начала перетаскивания (по умолчанию 6).\n */\nexport function useDndSortableSensors(distanceOrOptions: number | UseDndSortableSensorsOptions = 6) {\n\tconst distance = typeof distanceOrOptions === \"number\" ? distanceOrOptions : (distanceOrOptions.activationDistance ?? 6);\n\n\treturn useSensors(\n\t\tuseSensor(PointerSensor, { activationConstraint: { distance } }),\n\t\tuseSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })\n\t);\n}\n","import { useEffect, useLayoutEffect, useRef, useState } from \"react\";\n\nimport {\n\tautoPlacement,\n\tautoUpdate,\n\tflip,\n\toffset,\n\tPlacement,\n\tshift,\n\tsize as floatingSize,\n\tuseDismiss,\n\tuseFloating,\n\tuseInteractions,\n\tuseRole\n} from \"@floating-ui/react\";\n\nimport { findFirstEnabledIndex, findLastEnabledIndex, findNextEnabledIndex, handleKeyboardActivation } from \"../utils\";\n\n/** Геометрия, которую Floating UI вычислил для доступной стороны listbox. */\nexport type FloatingListboxSizeContext = {\n\tavailableWidth: number;\n\tavailableHeight: number;\n\tviewportWidth: number;\n\tviewportHeight: number;\n\treferenceWidth: number;\n\treferenceHeight: number;\n};\n\n/**\n * Размеры floating-элемента и CSS custom properties, которые специализированный\n * picker может вычислить из доступного viewport без собственного middleware.\n */\nexport type FloatingListboxSizeStyle = {\n\twidth?: string;\n\tminWidth?: string;\n\tmaxWidth?: string;\n\tmaxHeight?: string;\n} & {\n\t[customProperty: `--${string}`]: string | undefined;\n};\n\nexport type FloatingListboxSizeResolver = (context: FloatingListboxSizeContext) => FloatingListboxSizeStyle;\n\n/** Стратегия выбора стороны для floating-элемента относительно reference. */\nexport type FloatingListboxPlacementStrategy = \"flip\" | \"auto\";\n\ntype FloatingListboxStandardSizeProperty = \"width\" | \"minWidth\" | \"maxWidth\" | \"maxHeight\";\n\ntype FloatingListboxAppliedSizeState = {\n\tcustomProperties: Set<`--${string}`>;\n\tstandardProperties: Map<FloatingListboxStandardSizeProperty, string>;\n};\n\n/** Хранит свойства и их исходные значения до императивного применения resolver. */\nconst floatingListboxAppliedSizeState = new WeakMap<HTMLElement, FloatingListboxAppliedSizeState>();\n\n/** Хранит актуальный resolver отдельно от callback, который Floating UI может переиспользовать. */\nconst floatingListboxSizeResolvers = new WeakMap<object, FloatingListboxSizeResolver>();\n\n/** Полный набор выровненных сторон для поиска наиболее просторного положения. */\nconst FLOATING_LISTBOX_AUTO_PLACEMENTS: Placement[] = [\n\t\"bottom-start\",\n\t\"bottom-end\",\n\t\"top-start\",\n\t\"top-end\",\n\t\"left-start\",\n\t\"left-end\",\n\t\"right-start\",\n\t\"right-end\"\n];\n\ninterface UseFloatingListboxParams<T> {\n\toptions: readonly T[];\n\tselectedIndex: number;\n\topen?: boolean;\n\tonOpenChange?: (open: boolean) => void;\n\tgetOptionDisabled?: (option: T) => boolean;\n\tonSelect?: (option: T) => void;\n\tdisabled?: boolean;\n\tplacement?: Placement;\n\t/** `auto` сравнивает свободное место сверху, снизу, слева и справа. */\n\tplacementStrategy?: FloatingListboxPlacementStrategy;\n\tcloseOnSelect?: boolean;\n\tfocusFloatingOnOpen?: boolean;\n\tallowOpenWithoutOptions?: boolean;\n\trestoreFocusOnClose?: boolean;\n\tresolveFloatingSize?: FloatingListboxSizeResolver;\n}\n\nexport function useFloatingListbox<T>({\n\toptions,\n\tselectedIndex,\n\topen,\n\tonOpenChange,\n\tgetOptionDisabled,\n\tonSelect,\n\tdisabled,\n\tplacement = \"bottom-start\",\n\tplacementStrategy = \"flip\",\n\tcloseOnSelect = true,\n\tfocusFloatingOnOpen = true,\n\tallowOpenWithoutOptions = false,\n\trestoreFocusOnClose = true,\n\tresolveFloatingSize\n}: UseFloatingListboxParams<T>) {\n\tconst optionRefs = useRef<Array<HTMLElement | null>>([]);\n\tconst [internalOpen, setInternalOpen] = useState(false);\n\tconst resolvedOpen = open ?? internalOpen;\n\tconst setResolvedOpen = onOpenChange ?? setInternalOpen;\n\tconst [currentActiveIndex, setActiveIndex] = useState(\n\t\tselectedIndex >= 0 ? selectedIndex : findFirstEnabledIndex(options, getOptionDisabled)\n\t);\n\tconst [floatingSizeResolverOwner] = useState<object>(() => ({}));\n\n\t/**\n\t * Поддерживает активный индекс в валидном состоянии при изменении набора опций\n\t * или выбранного элемента, чтобы `aria-activedescendant` всегда ссылался на\n\t * существующую и доступную опцию.\n\t */\n\tconst resolveActiveIndex = (activeIndex: number) => {\n\t\tif (options.length === 0) {\n\t\t\treturn -1;\n\t\t}\n\n\t\tif (activeIndex >= 0 && activeIndex < options.length && !getOptionDisabled?.(options[activeIndex])) {\n\t\t\treturn activeIndex;\n\t\t}\n\n\t\tif (selectedIndex >= 0 && selectedIndex < options.length && !getOptionDisabled?.(options[selectedIndex])) {\n\t\t\treturn selectedIndex;\n\t\t}\n\n\t\treturn findFirstEnabledIndex(options, getOptionDisabled);\n\t};\n\tconst activeIndex = resolveActiveIndex(currentActiveIndex);\n\tconst placementMiddleware =\n\t\tplacementStrategy === \"auto\"\n\t\t\t? autoPlacement({\n\t\t\t\t\tallowedPlacements: FLOATING_LISTBOX_AUTO_PLACEMENTS,\n\t\t\t\t\t/*\n\t\t\t\t\t * Свободную сторону сравниваем по main axis. Варианты start/end при\n\t\t\t\t\t * этом всё равно исключаются autoPlacement, если не помещаются по\n\t\t\t\t\t * cross axis.\n\t\t\t\t\t */\n\t\t\t\t\tcrossAxis: false,\n\t\t\t\t\tpadding: 8\n\t\t\t\t})\n\t\t\t: flip({ padding: 8 });\n\n\tconst { refs, floatingStyles, context, update } = useFloating({\n\t\topen: resolvedOpen,\n\t\tonOpenChange: setResolvedOpen,\n\t\tplacement,\n\t\ttransform: false,\n\t\tstrategy: \"fixed\",\n\t\tmiddleware: [\n\t\t\toffset(4),\n\t\t\tplacementMiddleware,\n\t\t\tshift({ padding: 8 }),\n\t\t\tfloatingSize({\n\t\t\t\tpadding: 8,\n\t\t\t\tapply({ availableWidth, availableHeight, rects, elements }) {\n\t\t\t\t\tconst viewport = elements.floating.ownerDocument.documentElement;\n\t\t\t\t\tconst floatingStyle = elements.floating.style;\n\t\t\t\t\tconst resolvedSize = floatingListboxSizeResolvers.get(floatingSizeResolverOwner)?.({\n\t\t\t\t\t\tavailableWidth,\n\t\t\t\t\t\tavailableHeight,\n\t\t\t\t\t\tviewportWidth: viewport.clientWidth,\n\t\t\t\t\t\tviewportHeight: viewport.clientHeight,\n\t\t\t\t\t\treferenceWidth: rects.reference.width,\n\t\t\t\t\t\treferenceHeight: rects.reference.height\n\t\t\t\t\t});\n\n\t\t\t\t\t/*\n\t\t\t\t\t * Сначала сбрасываем размеры предыдущего resolver: Floating UI сохраняет\n\t\t\t\t\t * один DOM-узел между режимами, поэтому старые width и CSS-переменные\n\t\t\t\t\t * иначе продолжат влиять на следующий вариант отображения.\n\t\t\t\t\t */\n\t\t\t\t\tconst previousAppliedState = floatingListboxAppliedSizeState.get(elements.floating);\n\t\t\t\t\tfor (const property of previousAppliedState?.customProperties ?? []) {\n\t\t\t\t\t\tfloatingStyle.removeProperty(property);\n\t\t\t\t\t}\n\t\t\t\t\tfor (const [property, previousValue] of previousAppliedState?.standardProperties ?? []) {\n\t\t\t\t\t\tfloatingStyle[property] = previousValue;\n\t\t\t\t\t}\n\n\t\t\t\t\tfloatingStyle.minWidth = `${rects.reference.width}px`;\n\t\t\t\t\tfloatingStyle.maxHeight = `${Math.max(availableHeight, 120)}px`;\n\n\t\t\t\t\tconst nextCustomProperties = new Set<`--${string}`>();\n\t\t\t\t\tconst nextStandardProperties = new Map<FloatingListboxStandardSizeProperty, string>();\n\t\t\t\t\tfor (const [property, value] of Object.entries(resolvedSize ?? {})) {\n\t\t\t\t\t\tif (property.startsWith(\"--\")) {\n\t\t\t\t\t\t\tconst customProperty = property as `--${string}`;\n\t\t\t\t\t\t\tif (value !== undefined) {\n\t\t\t\t\t\t\t\tfloatingStyle.setProperty(customProperty, value);\n\t\t\t\t\t\t\t\tnextCustomProperties.add(customProperty);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (value !== undefined) {\n\t\t\t\t\t\t\tconst standardProperty = property as FloatingListboxStandardSizeProperty;\n\t\t\t\t\t\t\tnextStandardProperties.set(standardProperty, floatingStyle[standardProperty]);\n\t\t\t\t\t\t\tfloatingStyle[standardProperty] = value;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tfloatingListboxAppliedSizeState.set(elements.floating, {\n\t\t\t\t\t\tcustomProperties: nextCustomProperties,\n\t\t\t\t\t\tstandardProperties: nextStandardProperties\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t})\n\t\t],\n\t\twhileElementsMounted: autoUpdate\n\t});\n\tconst hasFloatingSizeResolver = resolveFloatingSize !== undefined;\n\n\t/*\n\t * Floating UI сравнивает middleware структурно и может сохранить первый\n\t * size.apply. WeakMap позволяет этому callback читать актуальный resolver после\n\t * загрузки OData-узлов, не пересоздавая конкурирующие middleware.\n\t */\n\tuseLayoutEffect(() => {\n\t\tif (resolveFloatingSize) {\n\t\t\tfloatingListboxSizeResolvers.set(floatingSizeResolverOwner, resolveFloatingSize);\n\t\t} else {\n\t\t\tfloatingListboxSizeResolvers.delete(floatingSizeResolverOwner);\n\t\t}\n\n\t\treturn () => {\n\t\t\tfloatingListboxSizeResolvers.delete(floatingSizeResolverOwner);\n\t\t};\n\t}, [floatingSizeResolverOwner, resolveFloatingSize]);\n\n\t/**\n\t * Асинхронно загруженные или отфильтрованные опции могут не изменить внешний\n\t * rect popup: grid с прежним числом строк остаётся того же размера. Явный\n\t * update повторно запускает placement и size middleware после commit списка.\n\t */\n\tuseLayoutEffect(() => {\n\t\tif (!resolvedOpen || !hasFloatingSizeResolver) {\n\t\t\treturn;\n\t\t}\n\n\t\tvoid update();\n\t}, [hasFloatingSizeResolver, options.length, resolvedOpen, update]);\n\n\tconst dismiss = useDismiss(context);\n\tconst role = useRole(context, { role: \"listbox\" });\n\tconst { getFloatingProps } = useInteractions([dismiss, role]);\n\n\tconst canOpen = allowOpenWithoutOptions || options.length > 0;\n\n\tuseEffect(() => {\n\t\tif (!resolvedOpen || activeIndex < 0) {\n\t\t\treturn;\n\t\t}\n\n\t\toptionRefs.current[activeIndex]?.scrollIntoView({ block: \"nearest\" });\n\t}, [activeIndex, resolvedOpen]);\n\n\tuseEffect(() => {\n\t\tif (!resolvedOpen || !focusFloatingOnOpen) {\n\t\t\treturn;\n\t\t}\n\n\t\trequestAnimationFrame(() => {\n\t\t\trefs.floating.current?.focus();\n\t\t});\n\t}, [focusFloatingOnOpen, resolvedOpen, refs]);\n\n\tconst close = () => {\n\t\tsetResolvedOpen(false);\n\n\t\tif (restoreFocusOnClose) {\n\t\t\tconst referenceElement = refs.domReference.current;\n\t\t\tif (referenceElement instanceof HTMLElement) {\n\t\t\t\treferenceElement.focus();\n\t\t\t}\n\t\t}\n\t};\n\n\tconst openWithIndex = (nextIndex: number) => {\n\t\tif (disabled || !canOpen) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (options.length > 0) {\n\t\t\tsetActiveIndex(resolveActiveIndex(nextIndex));\n\t\t}\n\n\t\tsetResolvedOpen(true);\n\t};\n\n\tconst openList = () => {\n\t\topenWithIndex(selectedIndex >= 0 ? selectedIndex : findFirstEnabledIndex(options, getOptionDisabled));\n\t};\n\n\tconst toggleOpen = () => {\n\t\tif (resolvedOpen) {\n\t\t\tclose();\n\t\t\treturn;\n\t\t}\n\n\t\topenList();\n\t};\n\n\tconst selectOption = (option: T) => {\n\t\tif (getOptionDisabled?.(option)) {\n\t\t\treturn;\n\t\t}\n\n\t\tonSelect?.(option);\n\n\t\tif (closeOnSelect) {\n\t\t\tclose();\n\t\t}\n\t};\n\n\tconst selectActiveOption = () => {\n\t\tif (activeIndex < 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst activeOption = options[activeIndex];\n\t\tif (activeOption !== undefined) {\n\t\t\tselectOption(activeOption);\n\t\t}\n\t};\n\n\tconst moveActiveToFirst = () => {\n\t\tsetActiveIndex(findFirstEnabledIndex(options, getOptionDisabled));\n\t};\n\n\tconst moveActiveToLast = () => {\n\t\tsetActiveIndex(findLastEnabledIndex(options, getOptionDisabled));\n\t};\n\n\tconst handleReferenceKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {\n\t\tif (disabled) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.key === \"ArrowDown\") {\n\t\t\tevent.preventDefault();\n\n\t\t\tif (!resolvedOpen) {\n\t\t\t\topenWithIndex(selectedIndex >= 0 ? selectedIndex : findFirstEnabledIndex(options, getOptionDisabled));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tsetActiveIndex((currentIndex) => findNextEnabledIndex(options, resolveActiveIndex(currentIndex), 1, getOptionDisabled));\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.key === \"ArrowUp\") {\n\t\t\tevent.preventDefault();\n\n\t\t\tif (!resolvedOpen) {\n\t\t\t\topenWithIndex(selectedIndex >= 0 ? selectedIndex : findLastEnabledIndex(options, getOptionDisabled));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tsetActiveIndex((currentIndex) => findNextEnabledIndex(options, resolveActiveIndex(currentIndex), -1, getOptionDisabled));\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.key === \"Home\" && resolvedOpen) {\n\t\t\tevent.preventDefault();\n\t\t\tmoveActiveToFirst();\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.key === \"End\" && resolvedOpen) {\n\t\t\tevent.preventDefault();\n\t\t\tmoveActiveToLast();\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.key === \"Escape\" && resolvedOpen) {\n\t\t\tevent.preventDefault();\n\t\t\tclose();\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.key === \"Tab\" && resolvedOpen) {\n\t\t\tclose();\n\t\t}\n\t};\n\n\tconst handleFloatingKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {\n\t\tif (event.target !== event.currentTarget) {\n\t\t\tif (event.key === \"Escape\") {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tclose();\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.key === \"ArrowDown\") {\n\t\t\tevent.preventDefault();\n\t\t\tsetActiveIndex((currentIndex) => findNextEnabledIndex(options, resolveActiveIndex(currentIndex), 1, getOptionDisabled));\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.key === \"ArrowUp\") {\n\t\t\tevent.preventDefault();\n\t\t\tsetActiveIndex((currentIndex) => findNextEnabledIndex(options, resolveActiveIndex(currentIndex), -1, getOptionDisabled));\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.key === \"Home\") {\n\t\t\tevent.preventDefault();\n\t\t\tmoveActiveToFirst();\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.key === \"End\") {\n\t\t\tevent.preventDefault();\n\t\t\tmoveActiveToLast();\n\t\t\treturn;\n\t\t}\n\n\t\tif (handleKeyboardActivation(event, selectActiveOption)) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.key === \"Escape\") {\n\t\t\tevent.preventDefault();\n\t\t\tclose();\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.key === \"Tab\") {\n\t\t\tclose();\n\t\t}\n\t};\n\n\tconst getOptionId = (listId: string, index: number) => `${listId}-option-${index}`;\n\tconst getActiveOptionId = (listId: string) => (activeIndex >= 0 ? getOptionId(listId, activeIndex) : undefined);\n\n\treturn {\n\t\topen: resolvedOpen,\n\t\tsetOpen: setResolvedOpen,\n\t\tactiveIndex,\n\t\trefs,\n\t\tcontext,\n\t\tfloatingStyles,\n\t\tgetFloatingProps,\n\t\tsetReference: refs.setReference,\n\t\tsetFloating: refs.setFloating,\n\t\tsetOptionRef: (index: number, node: HTMLElement | null) => {\n\t\t\toptionRefs.current[index] = node;\n\t\t},\n\t\tclose,\n\t\topenList,\n\t\topenWithIndex,\n\t\ttoggleOpen,\n\t\tselectOption,\n\t\tselectActiveOption,\n\t\thandleReferenceKeyDown,\n\t\thandleFloatingKeyDown,\n\t\tgetOptionId,\n\t\tgetActiveOptionId\n\t};\n}\n","import { type ComponentType, useMemo } from \"react\";\n\nimport { createLazyComponent } from \"../utils/createLazyComponent\";\n\n/**\n * Хук для создания ленивой компоненты, которая будет загружена только при первом рендере.\n *\n * @example\n *\n * ```tsx\n * const MyComponent = () => {\n * const TextEditor = useLazyComponent(\n * () => import(\"@components/content/article/editor/TextEditor\"),\n * \"TextEditor\"\n * );\n *\n * return (\n * <Suspense fallback={<div>Загрузка...</div>}>\n * <TextEditor />\n * </Suspense>\n * );\n * };\n * ```\n *\n * @param importFn - функция, которая возвращает промис, который резолвится в объект с компонентами\n * @param componentName - имя компонента, который нужно загрузить\n * @returns\n */\nconst useLazyComponent = <TProps extends object>(\n\timportFn: () => Promise<Record<string, ComponentType<TProps>> | null>,\n\tcomponentName: string,\n\tcacheKey?: string\n) => {\n\treturn useMemo(() => createLazyComponent(importFn, componentName, cacheKey), [cacheKey, importFn, componentName]);\n};\n\nexport { useLazyComponent };\n","import { useEffect, useRef } from \"react\";\n\nexport function useReferenceTrace<T>(name: string, value: T) {\n\tconst previousRef = useRef(value);\n\n\tuseEffect(() => {\n\t\tif (!__DEV__) return;\n\n\t\tconst referenceChanged = !Object.is(previousRef.current, value);\n\n\t\tconsole.debug(`[render trace] ${name}`, { referenceChanged, previous: previousRef.current, current: value });\n\n\t\tpreviousRef.current = value;\n\t});\n}\n","import { useLocation } from \"@tanstack/react-router\";\n\nimport { SearchParams } from \"./types\";\n\nexport function useSearchLink<K extends PropertyKey>() {\n\tconst location = useLocation();\n\n\tconst buildLink = (params: SearchParams<K>) => {\n\t\tconst next = new URLSearchParams(location.searchStr);\n\n\t\tparams = params || [];\n\n\t\tfor (const param in params) {\n\t\t\tconst value = params[param];\n\n\t\t\tif (value) {\n\t\t\t\tnext.set(param, String(value));\n\t\t\t} else {\n\t\t\t\tnext.delete(param);\n\t\t\t}\n\t\t}\n\n\t\tconst searchString = next.toString();\n\t\treturn `${location.pathname}${searchString ? `?${searchString}` : \"\"}${location.hash}`;\n\t};\n\n\treturn { buildLink };\n}\n","import { useMemo } from \"react\";\n\nimport { AnyRouter, useNavigate, useSearch } from \"@tanstack/react-router\";\n\nimport { SearchParams, SearchParamsUpdater } from \"./types\";\n\nexport function useSearchParams<K extends PropertyKey>(allowedKeys?: (keyof K)[]) {\n\tconst params = useSearch<AnyRouter, string, false, true, SearchParams<K>>({ strict: false });\n\tconst navigate = useNavigate();\n\n\t// Фильтруем параметры, оставляя только разрешенные ключи\n\tconst filteredParams = useMemo(() => {\n\t\tif (!allowedKeys) return params;\n\n\t\treturn Object.fromEntries(Object.entries(params).filter(([key]) => allowedKeys.includes(key as keyof K))) as SearchParams<K>;\n\t}, [params, allowedKeys]);\n\n\tconst setParams = (next: SearchParamsUpdater<K>) =>\n\t\tnavigate({\n\t\t\tto: \".\",\n\t\t\tsearch: (prev: SearchParams<K>) => ({\n\t\t\t\t...prev,\n\t\t\t\t...next\n\t\t\t}),\n\t\t\treplace: true\n\t\t});\n\n\treturn { params, setParams, filteredParams };\n}\n","import { useCallback, useEffect, useRef } from \"react\";\n\nexport type ThrottledCallback<T> = {\n\t/**\n\t * Основной вход: вызывай на каждом частом событии (например, onNodesChange).\n\t * Колбэк `cb` будет вызываться не чаще, чем 1 раз в `intervalMs`.\n\t *\n\t * Стратегия: last-write-wins — если за интервал пришло много значений,\n\t * по окончании интервала будет применено последнее.\n\t */\n\tcall: (arg: T) => void;\n\n\t/**\n\t * Немедленно применить последнее накопленное значение (если оно есть),\n\t * отменив запланированный вызов (если он был).\n\t * Полезно на:\n\t * - pointerup/mouseup (конец перетаскивания)\n\t * - blur/visibilitychange/beforeunload\n\t * - unmount (если хочешь гарантированно сохранить финальное состояние)\n\t */\n\tflush: () => void;\n\n\t/**\n\t * Отменить запланированный вызов и забыть накопленное значение.\n\t * Полезно, если ты переключаешь редактор/граф и не хочешь “долетов” старых данных.\n\t */\n\tcancel: () => void;\n};\n\n/**\n * useThrottledCallback (React 19 safe)\n *\n * Проблема:\n * - В xyflow onNodesChange/onEdgesChange может сыпаться очень часто.\n * - Если каждый раз писать “сериализуемый снапшот” в zustand → лишние ререндеры/CPU/GC.\n *\n * Решение:\n * - throttle: вызывать cb не чаще, чем 1 раз в intervalMs.\n * - last-write-wins: если за интервал пришло 50 обновлений — применим последнее.\n *\n * React 19 нюанс:\n * - нельзя мутировать ref.current во время render (линтер/правило).\n * - поэтому cbRef обновляем в useEffect.\n */\nexport function useThrottledCallback<T>(\n\tcb: (arg: T) => void,\n\tintervalMs: number,\n\toptions?: { flushOnUnmount?: boolean }\n): ThrottledCallback<T> {\n\t/**\n\t * Актуальная версия cb (обновляется только после commit’а).\n\t * Это решает \"stale closure\": call/flush/cancel стабильны,\n\t * но всегда дергают самый свежий cb.\n\t */\n\tconst cbRef = useRef<(arg: T) => void>(cb);\n\n\tuseEffect(() => {\n\t\tcbRef.current = cb;\n\t}, [cb]);\n\n\t/**\n\t * Таймер отложенного выполнения.\n\t * Он нужен, когда call() пришёл раньше, чем завершился текущий throttle-интервал:\n\t * мы планируем “выполнить в конце окна”.\n\t */\n\tconst tRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n\t/**\n\t * Накопленное “последнее значение”.\n\t * Всегда хранится последний arg, пришедший через call().\n\t */\n\tconst lastRef = useRef<T | null>(null);\n\n\t/**\n\t * Время (timestamp) последнего РЕАЛЬНОГО вызова cb.\n\t * Нужно, чтобы вычислять: можно ли выполнить сейчас или нужно подождать.\n\t */\n\tconst lastInvokeTsRef = useRef<number>(0);\n\n\t/**\n\t * cancel():\n\t * - отменяем отложенный таймер,\n\t * - забываем накопленное значение.\n\t * Важно: это не вызывает cb.\n\t */\n\tconst cancel = useCallback(() => {\n\t\tif (tRef.current) {\n\t\t\tclearTimeout(tRef.current);\n\t\t\ttRef.current = null;\n\t\t}\n\t\tlastRef.current = null;\n\t}, []);\n\n\t/**\n\t * flush():\n\t * - если есть накопленное значение, вызываем cb немедленно,\n\t * - чистим таймер,\n\t * - фиксируем lastInvokeTsRef (чтобы окно throttle считалось от flush тоже).\n\t */\n\tconst flush = useCallback(() => {\n\t\tconst v = lastRef.current;\n\t\tif (v === null) return;\n\n\t\t// Сбрасываем накопленное ДО вызова cb на случай,\n\t\t// если cb внутри синхронно вызовет call() снова.\n\t\tlastRef.current = null;\n\n\t\tif (tRef.current) {\n\t\t\tclearTimeout(tRef.current);\n\t\t\ttRef.current = null;\n\t\t}\n\n\t\tlastInvokeTsRef.current = Date.now();\n\t\tcbRef.current(v);\n\t}, []);\n\n\t/**\n\t * call(arg):\n\t * 1) сохраняем arg как последнее значение (last-write-wins)\n\t * 2) если с момента последнего вызова прошло достаточно времени → вызываем сразу\n\t * 3) иначе — планируем flush ровно в момент, когда throttle-окно закончится\n\t *\n\t * При этом мы гарантируем:\n\t * - не более одного cb вызова за intervalMs\n\t * - и что “последнее значение за интервал” будет применено в конце.\n\t */\n\tconst call = useCallback(\n\t\t(arg: T) => {\n\t\t\tlastRef.current = arg;\n\n\t\t\tconst now = Date.now();\n\t\t\tconst elapsed = now - lastInvokeTsRef.current;\n\n\t\t\t// Если окно уже прошло — можно выполнить немедленно.\n\t\t\tif (elapsed >= intervalMs) {\n\t\t\t\t// Немедленный вызов = “leading edge”.\n\t\t\t\t// Берём последнее значение, очищаем lastRef и вызываем cb.\n\t\t\t\tconst v = lastRef.current;\n\t\t\t\tlastRef.current = null;\n\n\t\t\t\t// На всякий случай прибираем таймер, если он был.\n\t\t\t\tif (tRef.current) {\n\t\t\t\t\tclearTimeout(tRef.current);\n\t\t\t\t\ttRef.current = null;\n\t\t\t\t}\n\n\t\t\t\tlastInvokeTsRef.current = now;\n\t\t\t\tif (v !== null) cbRef.current(v);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Иначе — “trailing edge”: планируем выполнение в конце окна.\n\t\t\t// Важно: если таймер уже запланирован — НЕ пересоздаём,\n\t\t\t// потому что trailing-вызов должен случиться в конкретный момент (конец окна),\n\t\t\t// а значение мы и так обновляем через lastRef.current.\n\t\t\tif (!tRef.current) {\n\t\t\t\tconst wait = intervalMs - elapsed;\n\n\t\t\t\ttRef.current = setTimeout(() => {\n\t\t\t\t\ttRef.current = null;\n\t\t\t\t\t// В конце окна применяем последнее накопленное значение\n\t\t\t\t\tflush();\n\t\t\t\t}, wait);\n\t\t\t}\n\t\t},\n\t\t[intervalMs, flush]\n\t);\n\n\t/**\n\t * Cleanup на unmount:\n\t * - по умолчанию cancel (не делаем побочных эффектов при размонтировании)\n\t * - при flushOnUnmount=true — пытаемся сохранить последнее значение.\n\t *\n\t * Для твоего кейса (сериализация финального snapshot) чаще полезно flushOnUnmount=true.\n\t */\n\tuseEffect(() => {\n\t\treturn () => {\n\t\t\tif (options?.flushOnUnmount) flush();\n\t\t\telse cancel();\n\t\t};\n\t}, [cancel, flush, options?.flushOnUnmount]);\n\n\treturn { call, flush, cancel };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/hooks/useBlockBrowserZoom.ts","../../src/hooks/useDebounce.ts","../../src/hooks/useDebounceCallback.ts","../../src/hooks/useDndSortableSensors.ts","../../src/hooks/useFloatingListbox.ts","../../src/hooks/useLazyComponent.ts","../../src/hooks/useReferenceTrace.ts","../../src/hooks/useSearchLink.ts","../../src/hooks/useSearchParams.ts","../../src/hooks/useThrottledCallback.ts"],"sourcesContent":["import { useEffect } from \"react\";\n\n/**\n * Глобально отключает браузерный zoom (ctrl+wheel).\n *\n * @param enabled если true — блокируем нативный зум страницы\n */\nexport function useBlockBrowserZoom(enabled: boolean) {\n\tuseEffect(() => {\n\t\tif (!enabled) return;\n\n\t\tconst handler = (e: WheelEvent) => {\n\t\t\tif (e.ctrlKey) {\n\t\t\t\te.preventDefault();\n\t\t\t}\n\t\t};\n\n\t\twindow.addEventListener(\"wheel\", handler, { passive: false });\n\t\treturn () => window.removeEventListener(\"wheel\", handler);\n\t}, [enabled]);\n}\n","import { useEffect, useState } from \"react\";\n\nexport function useDebounce<T>(value: T, delay: number): T {\n\tconst [debouncedValue, setDebouncedValue] = useState<T>(value);\n\n\tuseEffect(() => {\n\t\tconst timeoutId = setTimeout(() => {\n\t\t\tsetDebouncedValue(value);\n\t\t}, delay);\n\n\t\treturn () => clearTimeout(timeoutId);\n\t}, [value, delay]);\n\n\treturn debouncedValue;\n}\n","import { useCallback, useEffect, useRef } from \"react\";\n\n/**\n * Дебаунсит вызов колбэка: частые вызовы `call(value)` не вызывают `cb` сразу,\n * а откладывают его на `delayMs` после последнего вызова.\n *\n * Важно:\n * - Всегда сохраняется *последнее* значение (last-write-wins).\n * - `flush()` немедленно вызывает cb с последним сохранённым значением (если оно есть).\n * - `cancel()` отменяет таймер и забывает последнее значение.\n * - На unmount автоматически вызывается `cancel()` чтобы не было setState после размонтирования.\n *\n * Типовой кейс:\n * - onNodesChange -> call(snapshot)\n * - pointerup / blur / unmount -> flush() (или cancel(), если не надо сохранять)\n */\nexport function useDebouncedCallback<T>(cb: (arg: T) => void, delayMs: number, options?: { flushOnUnmount?: boolean }) {\n\t/**\n\t * Храним актуальную версию cb в ref, чтобы:\n\t * - не пересоздавать дебаунсер при каждом рендере,\n\t * - не ловить устаревший замыканием cb (stale closure),\n\t * - при этом всегда вызывать самую свежую функцию.\n\t */\n\tconst cbRef = useRef<(arg: T) => void>(cb);\n\n\t/**\n\t * Обновляем актуальную версию cb после commit'а\n\t */\n\tuseEffect(() => {\n\t\tcbRef.current = cb;\n\t}, [cb]);\n\n\t/**\n\t * id таймера. null означает что таймер не активен.\n\t */\n\tconst tRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n\t/**\n\t * Последнее значение, которое мы хотим \"закоммитить\" в cb.\n\t * Используем стратегию last-write-wins.\n\t */\n\tconst lastRef = useRef<T | null>(null);\n\n\t/**\n\t * cancel():\n\t * - останавливаем таймер, если он был,\n\t * - очищаем последнее значение (ничего не будет вызвано).\n\t */\n\tconst cancel = useCallback(() => {\n\t\tif (tRef.current) {\n\t\t\tclearTimeout(tRef.current);\n\t\t\ttRef.current = null;\n\t\t}\n\t\tlastRef.current = null;\n\t}, []);\n\n\t/**\n\t * flush():\n\t * - если есть отложенное значение, вызываем cb немедленно,\n\t * - сбрасываем таймер и lastRef.\n\t *\n\t * Используется, когда нужно гарантированно сохранить \"последнее\" прямо сейчас:\n\t * - unmount\n\t * - visibilitychange/blur\n\t * - pointerup/mouseup (конец взаимодействия)\n\t */\n\tconst flush = useCallback(() => {\n\t\tconst v = lastRef.current;\n\t\tif (v === null) return;\n\n\t\t// Сбрасываем before-call, чтобы если cb вызовет call() рекурсивно,\n\t\t// мы не держали старое значение.\n\t\tlastRef.current = null;\n\n\t\tif (tRef.current) {\n\t\t\tclearTimeout(tRef.current);\n\t\t\ttRef.current = null;\n\t\t}\n\n\t\tcbRef.current(v);\n\t}, []);\n\n\t/**\n\t * call(arg):\n\t * - сохраняем arg как \"последнее значение\"\n\t * - перезапускаем таймер\n\t * - по истечении delayMs вызываем flush()\n\t */\n\tconst call = useCallback(\n\t\t(arg: T) => {\n\t\t\tlastRef.current = arg;\n\n\t\t\t// Перезапуск таймера -> классический debounce\n\t\t\tif (tRef.current) clearTimeout(tRef.current);\n\n\t\t\ttRef.current = setTimeout(() => {\n\t\t\t\ttRef.current = null;\n\t\t\t\tflush();\n\t\t\t}, delayMs);\n\t\t},\n\t\t[delayMs, flush]\n\t);\n\n\t/**\n\t * Авто-cancel на unmount:\n\t * - убирает pending timeout\n\t * - предотвращает вызовы cb после размонтирования компонента\n\t */\n\tuseEffect(() => {\n\t\treturn () => {\n\t\t\tif (options?.flushOnUnmount) {\n\t\t\t\tflush();\n\t\t\t} else {\n\t\t\t\tcancel();\n\t\t\t}\n\t\t};\n\t}, [cancel, flush, options?.flushOnUnmount]);\n\n\treturn { call, flush, cancel };\n}\n","import {\n\tKeyboardSensor,\n\tMouseSensor,\n\tPointerSensor,\n\tTouchSensor,\n\tuseSensor,\n\tuseSensors,\n\ttype KeyboardCodes,\n\ttype KeyboardCoordinateGetter\n} from \"@dnd-kit/core\";\nimport { sortableKeyboardCoordinates } from \"@dnd-kit/sortable\";\n\nexport interface UseDndSortableSensorsOptions {\n\tactivationDistance?: number;\n\t/**\n\t * Отдельная touch-конфигурация сохраняет нативную прокрутку до long press.\n\t * Без неё hook продолжает использовать единый PointerSensor.\n\t */\n\ttouchActivation?: {\n\t\treadonly delay: number;\n\t\treadonly tolerance?: number;\n\t};\n\t/** Позволяет предметной матрице выбирать только допустимые координаты вместо порядка sortable-list. */\n\tkeyboardCoordinateGetter?: KeyboardCoordinateGetter;\n\t/** Переопределяет клавиши начала/завершения, когда Enter занят primary action компонента. */\n\tkeyboardCodes?: KeyboardCodes;\n}\n\n/**\n * Стандартная конфигурация DnD-сенсоров для сортируемых списков и матриц.\n *\n * Зачем: исключить дублирование одинаковой настройки PointerSensor + KeyboardSensor\n * в каждом хуке персонализации (колонки, сортировки, группировки).\n *\n * @param distanceOrOptions — минимальное смещение указателя или расширенные настройки mouse/touch/keyboard.\n */\nexport function useDndSortableSensors(distanceOrOptions: number | UseDndSortableSensorsOptions = 6) {\n\tconst options = typeof distanceOrOptions === \"number\" ? { activationDistance: distanceOrOptions } : distanceOrOptions;\n\tconst distance = options.activationDistance ?? 6;\n\tconst pointerSensor = useSensor(PointerSensor, { activationConstraint: { distance } });\n\tconst mouseSensor = useSensor(MouseSensor, { activationConstraint: { distance } });\n\tconst touchSensor = useSensor(TouchSensor, {\n\t\tactivationConstraint: {\n\t\t\tdelay: options.touchActivation?.delay ?? 0,\n\t\t\ttolerance: options.touchActivation?.tolerance ?? distance\n\t\t}\n\t});\n\tconst keyboardSensor = useSensor(KeyboardSensor, {\n\t\tcoordinateGetter: options.keyboardCoordinateGetter ?? sortableKeyboardCoordinates,\n\t\tkeyboardCodes: options.keyboardCodes\n\t});\n\n\treturn useSensors(\n\t\toptions.touchActivation ? mouseSensor : pointerSensor,\n\t\toptions.touchActivation ? touchSensor : undefined,\n\t\tkeyboardSensor\n\t);\n}\n","import { useEffect, useLayoutEffect, useRef, useState } from \"react\";\n\nimport {\n\tautoPlacement,\n\tautoUpdate,\n\tflip,\n\toffset,\n\tPlacement,\n\tshift,\n\tsize as floatingSize,\n\tuseDismiss,\n\tuseFloating,\n\tuseInteractions,\n\tuseRole\n} from \"@floating-ui/react\";\n\nimport { findFirstEnabledIndex, findLastEnabledIndex, findNextEnabledIndex, handleKeyboardActivation } from \"../utils\";\n\n/** Геометрия, которую Floating UI вычислил для доступной стороны listbox. */\nexport type FloatingListboxSizeContext = {\n\tavailableWidth: number;\n\tavailableHeight: number;\n\tviewportWidth: number;\n\tviewportHeight: number;\n\treferenceWidth: number;\n\treferenceHeight: number;\n};\n\n/**\n * Размеры floating-элемента и CSS custom properties, которые специализированный\n * picker может вычислить из доступного viewport без собственного middleware.\n */\nexport type FloatingListboxSizeStyle = {\n\twidth?: string;\n\tminWidth?: string;\n\tmaxWidth?: string;\n\tmaxHeight?: string;\n} & {\n\t[customProperty: `--${string}`]: string | undefined;\n};\n\nexport type FloatingListboxSizeResolver = (context: FloatingListboxSizeContext) => FloatingListboxSizeStyle;\n\n/** Стратегия выбора стороны для floating-элемента относительно reference. */\nexport type FloatingListboxPlacementStrategy = \"flip\" | \"auto\";\n\ntype FloatingListboxStandardSizeProperty = \"width\" | \"minWidth\" | \"maxWidth\" | \"maxHeight\";\n\ntype FloatingListboxAppliedSizeState = {\n\tcustomProperties: Set<`--${string}`>;\n\tstandardProperties: Map<FloatingListboxStandardSizeProperty, string>;\n};\n\n/** Хранит свойства и их исходные значения до императивного применения resolver. */\nconst floatingListboxAppliedSizeState = new WeakMap<HTMLElement, FloatingListboxAppliedSizeState>();\n\n/** Хранит актуальный resolver отдельно от callback, который Floating UI может переиспользовать. */\nconst floatingListboxSizeResolvers = new WeakMap<object, FloatingListboxSizeResolver>();\n\n/** Полный набор выровненных сторон для поиска наиболее просторного положения. */\nconst FLOATING_LISTBOX_AUTO_PLACEMENTS: Placement[] = [\n\t\"bottom-start\",\n\t\"bottom-end\",\n\t\"top-start\",\n\t\"top-end\",\n\t\"left-start\",\n\t\"left-end\",\n\t\"right-start\",\n\t\"right-end\"\n];\n\ninterface UseFloatingListboxParams<T> {\n\toptions: readonly T[];\n\tselectedIndex: number;\n\topen?: boolean;\n\tonOpenChange?: (open: boolean) => void;\n\tgetOptionDisabled?: (option: T) => boolean;\n\tonSelect?: (option: T) => void;\n\tdisabled?: boolean;\n\tplacement?: Placement;\n\t/** `auto` сравнивает свободное место сверху, снизу, слева и справа. */\n\tplacementStrategy?: FloatingListboxPlacementStrategy;\n\tcloseOnSelect?: boolean;\n\tfocusFloatingOnOpen?: boolean;\n\tallowOpenWithoutOptions?: boolean;\n\trestoreFocusOnClose?: boolean;\n\tresolveFloatingSize?: FloatingListboxSizeResolver;\n}\n\nexport function useFloatingListbox<T>({\n\toptions,\n\tselectedIndex,\n\topen,\n\tonOpenChange,\n\tgetOptionDisabled,\n\tonSelect,\n\tdisabled,\n\tplacement = \"bottom-start\",\n\tplacementStrategy = \"flip\",\n\tcloseOnSelect = true,\n\tfocusFloatingOnOpen = true,\n\tallowOpenWithoutOptions = false,\n\trestoreFocusOnClose = true,\n\tresolveFloatingSize\n}: UseFloatingListboxParams<T>) {\n\tconst optionRefs = useRef<Array<HTMLElement | null>>([]);\n\tconst [internalOpen, setInternalOpen] = useState(false);\n\tconst resolvedOpen = open ?? internalOpen;\n\tconst setResolvedOpen = onOpenChange ?? setInternalOpen;\n\tconst [currentActiveIndex, setActiveIndex] = useState(\n\t\tselectedIndex >= 0 ? selectedIndex : findFirstEnabledIndex(options, getOptionDisabled)\n\t);\n\tconst [floatingSizeResolverOwner] = useState<object>(() => ({}));\n\n\t/**\n\t * Поддерживает активный индекс в валидном состоянии при изменении набора опций\n\t * или выбранного элемента, чтобы `aria-activedescendant` всегда ссылался на\n\t * существующую и доступную опцию.\n\t */\n\tconst resolveActiveIndex = (activeIndex: number) => {\n\t\tif (options.length === 0) {\n\t\t\treturn -1;\n\t\t}\n\n\t\tif (activeIndex >= 0 && activeIndex < options.length && !getOptionDisabled?.(options[activeIndex])) {\n\t\t\treturn activeIndex;\n\t\t}\n\n\t\tif (selectedIndex >= 0 && selectedIndex < options.length && !getOptionDisabled?.(options[selectedIndex])) {\n\t\t\treturn selectedIndex;\n\t\t}\n\n\t\treturn findFirstEnabledIndex(options, getOptionDisabled);\n\t};\n\tconst activeIndex = resolveActiveIndex(currentActiveIndex);\n\tconst placementMiddleware =\n\t\tplacementStrategy === \"auto\"\n\t\t\t? autoPlacement({\n\t\t\t\t\tallowedPlacements: FLOATING_LISTBOX_AUTO_PLACEMENTS,\n\t\t\t\t\t/*\n\t\t\t\t\t * Свободную сторону сравниваем по main axis. Варианты start/end при\n\t\t\t\t\t * этом всё равно исключаются autoPlacement, если не помещаются по\n\t\t\t\t\t * cross axis.\n\t\t\t\t\t */\n\t\t\t\t\tcrossAxis: false,\n\t\t\t\t\tpadding: 8\n\t\t\t\t})\n\t\t\t: flip({ padding: 8 });\n\n\tconst { refs, floatingStyles, context, update } = useFloating({\n\t\topen: resolvedOpen,\n\t\tonOpenChange: setResolvedOpen,\n\t\tplacement,\n\t\ttransform: false,\n\t\tstrategy: \"fixed\",\n\t\tmiddleware: [\n\t\t\toffset(4),\n\t\t\tplacementMiddleware,\n\t\t\tshift({ padding: 8 }),\n\t\t\tfloatingSize({\n\t\t\t\tpadding: 8,\n\t\t\t\tapply({ availableWidth, availableHeight, rects, elements }) {\n\t\t\t\t\tconst viewport = elements.floating.ownerDocument.documentElement;\n\t\t\t\t\tconst floatingStyle = elements.floating.style;\n\t\t\t\t\tconst resolvedSize = floatingListboxSizeResolvers.get(floatingSizeResolverOwner)?.({\n\t\t\t\t\t\tavailableWidth,\n\t\t\t\t\t\tavailableHeight,\n\t\t\t\t\t\tviewportWidth: viewport.clientWidth,\n\t\t\t\t\t\tviewportHeight: viewport.clientHeight,\n\t\t\t\t\t\treferenceWidth: rects.reference.width,\n\t\t\t\t\t\treferenceHeight: rects.reference.height\n\t\t\t\t\t});\n\n\t\t\t\t\t/*\n\t\t\t\t\t * Сначала сбрасываем размеры предыдущего resolver: Floating UI сохраняет\n\t\t\t\t\t * один DOM-узел между режимами, поэтому старые width и CSS-переменные\n\t\t\t\t\t * иначе продолжат влиять на следующий вариант отображения.\n\t\t\t\t\t */\n\t\t\t\t\tconst previousAppliedState = floatingListboxAppliedSizeState.get(elements.floating);\n\t\t\t\t\tfor (const property of previousAppliedState?.customProperties ?? []) {\n\t\t\t\t\t\tfloatingStyle.removeProperty(property);\n\t\t\t\t\t}\n\t\t\t\t\tfor (const [property, previousValue] of previousAppliedState?.standardProperties ?? []) {\n\t\t\t\t\t\tfloatingStyle[property] = previousValue;\n\t\t\t\t\t}\n\n\t\t\t\t\tfloatingStyle.minWidth = `${rects.reference.width}px`;\n\t\t\t\t\tfloatingStyle.maxHeight = `${Math.max(availableHeight, 120)}px`;\n\n\t\t\t\t\tconst nextCustomProperties = new Set<`--${string}`>();\n\t\t\t\t\tconst nextStandardProperties = new Map<FloatingListboxStandardSizeProperty, string>();\n\t\t\t\t\tfor (const [property, value] of Object.entries(resolvedSize ?? {})) {\n\t\t\t\t\t\tif (property.startsWith(\"--\")) {\n\t\t\t\t\t\t\tconst customProperty = property as `--${string}`;\n\t\t\t\t\t\t\tif (value !== undefined) {\n\t\t\t\t\t\t\t\tfloatingStyle.setProperty(customProperty, value);\n\t\t\t\t\t\t\t\tnextCustomProperties.add(customProperty);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (value !== undefined) {\n\t\t\t\t\t\t\tconst standardProperty = property as FloatingListboxStandardSizeProperty;\n\t\t\t\t\t\t\tnextStandardProperties.set(standardProperty, floatingStyle[standardProperty]);\n\t\t\t\t\t\t\tfloatingStyle[standardProperty] = value;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tfloatingListboxAppliedSizeState.set(elements.floating, {\n\t\t\t\t\t\tcustomProperties: nextCustomProperties,\n\t\t\t\t\t\tstandardProperties: nextStandardProperties\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t})\n\t\t],\n\t\twhileElementsMounted: autoUpdate\n\t});\n\tconst hasFloatingSizeResolver = resolveFloatingSize !== undefined;\n\n\t/*\n\t * Floating UI сравнивает middleware структурно и может сохранить первый\n\t * size.apply. WeakMap позволяет этому callback читать актуальный resolver после\n\t * загрузки OData-узлов, не пересоздавая конкурирующие middleware.\n\t */\n\tuseLayoutEffect(() => {\n\t\tif (resolveFloatingSize) {\n\t\t\tfloatingListboxSizeResolvers.set(floatingSizeResolverOwner, resolveFloatingSize);\n\t\t} else {\n\t\t\tfloatingListboxSizeResolvers.delete(floatingSizeResolverOwner);\n\t\t}\n\n\t\treturn () => {\n\t\t\tfloatingListboxSizeResolvers.delete(floatingSizeResolverOwner);\n\t\t};\n\t}, [floatingSizeResolverOwner, resolveFloatingSize]);\n\n\t/**\n\t * Асинхронно загруженные или отфильтрованные опции могут не изменить внешний\n\t * rect popup: grid с прежним числом строк остаётся того же размера. Явный\n\t * update повторно запускает placement и size middleware после commit списка.\n\t */\n\tuseLayoutEffect(() => {\n\t\tif (!resolvedOpen || !hasFloatingSizeResolver) {\n\t\t\treturn;\n\t\t}\n\n\t\tvoid update();\n\t}, [hasFloatingSizeResolver, options.length, resolvedOpen, update]);\n\n\tconst dismiss = useDismiss(context);\n\tconst role = useRole(context, { role: \"listbox\" });\n\tconst { getFloatingProps } = useInteractions([dismiss, role]);\n\n\tconst canOpen = allowOpenWithoutOptions || options.length > 0;\n\n\tuseEffect(() => {\n\t\tif (!resolvedOpen || activeIndex < 0) {\n\t\t\treturn;\n\t\t}\n\n\t\toptionRefs.current[activeIndex]?.scrollIntoView({ block: \"nearest\" });\n\t}, [activeIndex, resolvedOpen]);\n\n\tuseEffect(() => {\n\t\tif (!resolvedOpen || !focusFloatingOnOpen) {\n\t\t\treturn;\n\t\t}\n\n\t\trequestAnimationFrame(() => {\n\t\t\trefs.floating.current?.focus();\n\t\t});\n\t}, [focusFloatingOnOpen, resolvedOpen, refs]);\n\n\tconst close = () => {\n\t\tsetResolvedOpen(false);\n\n\t\tif (restoreFocusOnClose) {\n\t\t\tconst referenceElement = refs.domReference.current;\n\t\t\tif (referenceElement instanceof HTMLElement) {\n\t\t\t\treferenceElement.focus();\n\t\t\t}\n\t\t}\n\t};\n\n\tconst openWithIndex = (nextIndex: number) => {\n\t\tif (disabled || !canOpen) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (options.length > 0) {\n\t\t\tsetActiveIndex(resolveActiveIndex(nextIndex));\n\t\t}\n\n\t\tsetResolvedOpen(true);\n\t};\n\n\tconst openList = () => {\n\t\topenWithIndex(selectedIndex >= 0 ? selectedIndex : findFirstEnabledIndex(options, getOptionDisabled));\n\t};\n\n\tconst toggleOpen = () => {\n\t\tif (resolvedOpen) {\n\t\t\tclose();\n\t\t\treturn;\n\t\t}\n\n\t\topenList();\n\t};\n\n\tconst selectOption = (option: T) => {\n\t\tif (getOptionDisabled?.(option)) {\n\t\t\treturn;\n\t\t}\n\n\t\tonSelect?.(option);\n\n\t\tif (closeOnSelect) {\n\t\t\tclose();\n\t\t}\n\t};\n\n\tconst selectActiveOption = () => {\n\t\tif (activeIndex < 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst activeOption = options[activeIndex];\n\t\tif (activeOption !== undefined) {\n\t\t\tselectOption(activeOption);\n\t\t}\n\t};\n\n\tconst moveActiveToFirst = () => {\n\t\tsetActiveIndex(findFirstEnabledIndex(options, getOptionDisabled));\n\t};\n\n\tconst moveActiveToLast = () => {\n\t\tsetActiveIndex(findLastEnabledIndex(options, getOptionDisabled));\n\t};\n\n\tconst handleReferenceKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {\n\t\tif (disabled) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.key === \"ArrowDown\") {\n\t\t\tevent.preventDefault();\n\n\t\t\tif (!resolvedOpen) {\n\t\t\t\topenWithIndex(selectedIndex >= 0 ? selectedIndex : findFirstEnabledIndex(options, getOptionDisabled));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tsetActiveIndex((currentIndex) => findNextEnabledIndex(options, resolveActiveIndex(currentIndex), 1, getOptionDisabled));\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.key === \"ArrowUp\") {\n\t\t\tevent.preventDefault();\n\n\t\t\tif (!resolvedOpen) {\n\t\t\t\topenWithIndex(selectedIndex >= 0 ? selectedIndex : findLastEnabledIndex(options, getOptionDisabled));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tsetActiveIndex((currentIndex) => findNextEnabledIndex(options, resolveActiveIndex(currentIndex), -1, getOptionDisabled));\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.key === \"Home\" && resolvedOpen) {\n\t\t\tevent.preventDefault();\n\t\t\tmoveActiveToFirst();\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.key === \"End\" && resolvedOpen) {\n\t\t\tevent.preventDefault();\n\t\t\tmoveActiveToLast();\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.key === \"Escape\" && resolvedOpen) {\n\t\t\tevent.preventDefault();\n\t\t\tclose();\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.key === \"Tab\" && resolvedOpen) {\n\t\t\tclose();\n\t\t}\n\t};\n\n\tconst handleFloatingKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {\n\t\tif (event.target !== event.currentTarget) {\n\t\t\tif (event.key === \"Escape\") {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tclose();\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.key === \"ArrowDown\") {\n\t\t\tevent.preventDefault();\n\t\t\tsetActiveIndex((currentIndex) => findNextEnabledIndex(options, resolveActiveIndex(currentIndex), 1, getOptionDisabled));\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.key === \"ArrowUp\") {\n\t\t\tevent.preventDefault();\n\t\t\tsetActiveIndex((currentIndex) => findNextEnabledIndex(options, resolveActiveIndex(currentIndex), -1, getOptionDisabled));\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.key === \"Home\") {\n\t\t\tevent.preventDefault();\n\t\t\tmoveActiveToFirst();\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.key === \"End\") {\n\t\t\tevent.preventDefault();\n\t\t\tmoveActiveToLast();\n\t\t\treturn;\n\t\t}\n\n\t\tif (handleKeyboardActivation(event, selectActiveOption)) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.key === \"Escape\") {\n\t\t\tevent.preventDefault();\n\t\t\tclose();\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.key === \"Tab\") {\n\t\t\tclose();\n\t\t}\n\t};\n\n\tconst getOptionId = (listId: string, index: number) => `${listId}-option-${index}`;\n\tconst getActiveOptionId = (listId: string) => (activeIndex >= 0 ? getOptionId(listId, activeIndex) : undefined);\n\n\treturn {\n\t\topen: resolvedOpen,\n\t\tsetOpen: setResolvedOpen,\n\t\tactiveIndex,\n\t\trefs,\n\t\tcontext,\n\t\tfloatingStyles,\n\t\tgetFloatingProps,\n\t\tsetReference: refs.setReference,\n\t\tsetFloating: refs.setFloating,\n\t\tsetOptionRef: (index: number, node: HTMLElement | null) => {\n\t\t\toptionRefs.current[index] = node;\n\t\t},\n\t\tclose,\n\t\topenList,\n\t\topenWithIndex,\n\t\ttoggleOpen,\n\t\tselectOption,\n\t\tselectActiveOption,\n\t\thandleReferenceKeyDown,\n\t\thandleFloatingKeyDown,\n\t\tgetOptionId,\n\t\tgetActiveOptionId\n\t};\n}\n","import { type ComponentType, useMemo } from \"react\";\n\nimport { createLazyComponent } from \"../utils/createLazyComponent\";\n\n/**\n * Хук для создания ленивой компоненты, которая будет загружена только при первом рендере.\n *\n * @example\n *\n * ```tsx\n * const MyComponent = () => {\n * const TextEditor = useLazyComponent(\n * () => import(\"@components/content/article/editor/TextEditor\"),\n * \"TextEditor\"\n * );\n *\n * return (\n * <Suspense fallback={<div>Загрузка...</div>}>\n * <TextEditor />\n * </Suspense>\n * );\n * };\n * ```\n *\n * @param importFn - функция, которая возвращает промис, который резолвится в объект с компонентами\n * @param componentName - имя компонента, который нужно загрузить\n * @returns\n */\nconst useLazyComponent = <TProps extends object>(\n\timportFn: () => Promise<Record<string, ComponentType<TProps>> | null>,\n\tcomponentName: string,\n\tcacheKey?: string\n) => {\n\treturn useMemo(() => createLazyComponent(importFn, componentName, cacheKey), [cacheKey, importFn, componentName]);\n};\n\nexport { useLazyComponent };\n","import { useEffect, useRef } from \"react\";\n\nexport function useReferenceTrace<T>(name: string, value: T) {\n\tconst previousRef = useRef(value);\n\n\tuseEffect(() => {\n\t\tif (!__DEV__) return;\n\n\t\tconst referenceChanged = !Object.is(previousRef.current, value);\n\n\t\tconsole.debug(`[render trace] ${name}`, { referenceChanged, previous: previousRef.current, current: value });\n\n\t\tpreviousRef.current = value;\n\t});\n}\n","import { useLocation } from \"@tanstack/react-router\";\n\nimport { SearchParams } from \"./types\";\n\nexport function useSearchLink<K extends PropertyKey>() {\n\tconst location = useLocation();\n\n\tconst buildLink = (params: SearchParams<K>) => {\n\t\tconst next = new URLSearchParams(location.searchStr);\n\n\t\tparams = params || [];\n\n\t\tfor (const param in params) {\n\t\t\tconst value = params[param];\n\n\t\t\tif (value) {\n\t\t\t\tnext.set(param, String(value));\n\t\t\t} else {\n\t\t\t\tnext.delete(param);\n\t\t\t}\n\t\t}\n\n\t\tconst searchString = next.toString();\n\t\treturn `${location.pathname}${searchString ? `?${searchString}` : \"\"}${location.hash}`;\n\t};\n\n\treturn { buildLink };\n}\n","import { useMemo } from \"react\";\n\nimport { AnyRouter, useNavigate, useSearch } from \"@tanstack/react-router\";\n\nimport { SearchParams, SearchParamsUpdater } from \"./types\";\n\nexport function useSearchParams<K extends PropertyKey>(allowedKeys?: (keyof K)[]) {\n\tconst params = useSearch<AnyRouter, string, false, true, SearchParams<K>>({ strict: false });\n\tconst navigate = useNavigate();\n\n\t// Фильтруем параметры, оставляя только разрешенные ключи\n\tconst filteredParams = useMemo(() => {\n\t\tif (!allowedKeys) return params;\n\n\t\treturn Object.fromEntries(Object.entries(params).filter(([key]) => allowedKeys.includes(key as keyof K))) as SearchParams<K>;\n\t}, [params, allowedKeys]);\n\n\tconst setParams = (next: SearchParamsUpdater<K>) =>\n\t\tnavigate({\n\t\t\tto: \".\",\n\t\t\tsearch: (prev: SearchParams<K>) => ({\n\t\t\t\t...prev,\n\t\t\t\t...next\n\t\t\t}),\n\t\t\treplace: true\n\t\t});\n\n\treturn { params, setParams, filteredParams };\n}\n","import { useCallback, useEffect, useRef } from \"react\";\n\nexport type ThrottledCallback<T> = {\n\t/**\n\t * Основной вход: вызывай на каждом частом событии (например, onNodesChange).\n\t * Колбэк `cb` будет вызываться не чаще, чем 1 раз в `intervalMs`.\n\t *\n\t * Стратегия: last-write-wins — если за интервал пришло много значений,\n\t * по окончании интервала будет применено последнее.\n\t */\n\tcall: (arg: T) => void;\n\n\t/**\n\t * Немедленно применить последнее накопленное значение (если оно есть),\n\t * отменив запланированный вызов (если он был).\n\t * Полезно на:\n\t * - pointerup/mouseup (конец перетаскивания)\n\t * - blur/visibilitychange/beforeunload\n\t * - unmount (если хочешь гарантированно сохранить финальное состояние)\n\t */\n\tflush: () => void;\n\n\t/**\n\t * Отменить запланированный вызов и забыть накопленное значение.\n\t * Полезно, если ты переключаешь редактор/граф и не хочешь “долетов” старых данных.\n\t */\n\tcancel: () => void;\n};\n\n/**\n * useThrottledCallback (React 19 safe)\n *\n * Проблема:\n * - В xyflow onNodesChange/onEdgesChange может сыпаться очень часто.\n * - Если каждый раз писать “сериализуемый снапшот” в zustand → лишние ререндеры/CPU/GC.\n *\n * Решение:\n * - throttle: вызывать cb не чаще, чем 1 раз в intervalMs.\n * - last-write-wins: если за интервал пришло 50 обновлений — применим последнее.\n *\n * React 19 нюанс:\n * - нельзя мутировать ref.current во время render (линтер/правило).\n * - поэтому cbRef обновляем в useEffect.\n */\nexport function useThrottledCallback<T>(\n\tcb: (arg: T) => void,\n\tintervalMs: number,\n\toptions?: { flushOnUnmount?: boolean }\n): ThrottledCallback<T> {\n\t/**\n\t * Актуальная версия cb (обновляется только после commit’а).\n\t * Это решает \"stale closure\": call/flush/cancel стабильны,\n\t * но всегда дергают самый свежий cb.\n\t */\n\tconst cbRef = useRef<(arg: T) => void>(cb);\n\n\tuseEffect(() => {\n\t\tcbRef.current = cb;\n\t}, [cb]);\n\n\t/**\n\t * Таймер отложенного выполнения.\n\t * Он нужен, когда call() пришёл раньше, чем завершился текущий throttle-интервал:\n\t * мы планируем “выполнить в конце окна”.\n\t */\n\tconst tRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n\t/**\n\t * Накопленное “последнее значение”.\n\t * Всегда хранится последний arg, пришедший через call().\n\t */\n\tconst lastRef = useRef<T | null>(null);\n\n\t/**\n\t * Время (timestamp) последнего РЕАЛЬНОГО вызова cb.\n\t * Нужно, чтобы вычислять: можно ли выполнить сейчас или нужно подождать.\n\t */\n\tconst lastInvokeTsRef = useRef<number>(0);\n\n\t/**\n\t * cancel():\n\t * - отменяем отложенный таймер,\n\t * - забываем накопленное значение.\n\t * Важно: это не вызывает cb.\n\t */\n\tconst cancel = useCallback(() => {\n\t\tif (tRef.current) {\n\t\t\tclearTimeout(tRef.current);\n\t\t\ttRef.current = null;\n\t\t}\n\t\tlastRef.current = null;\n\t}, []);\n\n\t/**\n\t * flush():\n\t * - если есть накопленное значение, вызываем cb немедленно,\n\t * - чистим таймер,\n\t * - фиксируем lastInvokeTsRef (чтобы окно throttle считалось от flush тоже).\n\t */\n\tconst flush = useCallback(() => {\n\t\tconst v = lastRef.current;\n\t\tif (v === null) return;\n\n\t\t// Сбрасываем накопленное ДО вызова cb на случай,\n\t\t// если cb внутри синхронно вызовет call() снова.\n\t\tlastRef.current = null;\n\n\t\tif (tRef.current) {\n\t\t\tclearTimeout(tRef.current);\n\t\t\ttRef.current = null;\n\t\t}\n\n\t\tlastInvokeTsRef.current = Date.now();\n\t\tcbRef.current(v);\n\t}, []);\n\n\t/**\n\t * call(arg):\n\t * 1) сохраняем arg как последнее значение (last-write-wins)\n\t * 2) если с момента последнего вызова прошло достаточно времени → вызываем сразу\n\t * 3) иначе — планируем flush ровно в момент, когда throttle-окно закончится\n\t *\n\t * При этом мы гарантируем:\n\t * - не более одного cb вызова за intervalMs\n\t * - и что “последнее значение за интервал” будет применено в конце.\n\t */\n\tconst call = useCallback(\n\t\t(arg: T) => {\n\t\t\tlastRef.current = arg;\n\n\t\t\tconst now = Date.now();\n\t\t\tconst elapsed = now - lastInvokeTsRef.current;\n\n\t\t\t// Если окно уже прошло — можно выполнить немедленно.\n\t\t\tif (elapsed >= intervalMs) {\n\t\t\t\t// Немедленный вызов = “leading edge”.\n\t\t\t\t// Берём последнее значение, очищаем lastRef и вызываем cb.\n\t\t\t\tconst v = lastRef.current;\n\t\t\t\tlastRef.current = null;\n\n\t\t\t\t// На всякий случай прибираем таймер, если он был.\n\t\t\t\tif (tRef.current) {\n\t\t\t\t\tclearTimeout(tRef.current);\n\t\t\t\t\ttRef.current = null;\n\t\t\t\t}\n\n\t\t\t\tlastInvokeTsRef.current = now;\n\t\t\t\tif (v !== null) cbRef.current(v);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Иначе — “trailing edge”: планируем выполнение в конце окна.\n\t\t\t// Важно: если таймер уже запланирован — НЕ пересоздаём,\n\t\t\t// потому что trailing-вызов должен случиться в конкретный момент (конец окна),\n\t\t\t// а значение мы и так обновляем через lastRef.current.\n\t\t\tif (!tRef.current) {\n\t\t\t\tconst wait = intervalMs - elapsed;\n\n\t\t\t\ttRef.current = setTimeout(() => {\n\t\t\t\t\ttRef.current = null;\n\t\t\t\t\t// В конце окна применяем последнее накопленное значение\n\t\t\t\t\tflush();\n\t\t\t\t}, wait);\n\t\t\t}\n\t\t},\n\t\t[intervalMs, flush]\n\t);\n\n\t/**\n\t * Cleanup на unmount:\n\t * - по умолчанию cancel (не делаем побочных эффектов при размонтировании)\n\t * - при flushOnUnmount=true — пытаемся сохранить последнее значение.\n\t *\n\t * Для твоего кейса (сериализация финального snapshot) чаще полезно flushOnUnmount=true.\n\t */\n\tuseEffect(() => {\n\t\treturn () => {\n\t\t\tif (options?.flushOnUnmount) flush();\n\t\t\telse cancel();\n\t\t};\n\t}, [cancel, flush, options?.flushOnUnmount]);\n\n\treturn { call, flush, cancel };\n}\n"],"mappings":"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"}
|
|
@@ -1,13 +1,26 @@
|
|
|
1
|
+
import { type KeyboardCodes, type KeyboardCoordinateGetter } from "@dnd-kit/core";
|
|
1
2
|
export interface UseDndSortableSensorsOptions {
|
|
2
3
|
activationDistance?: number;
|
|
4
|
+
/**
|
|
5
|
+
* Отдельная touch-конфигурация сохраняет нативную прокрутку до long press.
|
|
6
|
+
* Без неё hook продолжает использовать единый PointerSensor.
|
|
7
|
+
*/
|
|
8
|
+
touchActivation?: {
|
|
9
|
+
readonly delay: number;
|
|
10
|
+
readonly tolerance?: number;
|
|
11
|
+
};
|
|
12
|
+
/** Позволяет предметной матрице выбирать только допустимые координаты вместо порядка sortable-list. */
|
|
13
|
+
keyboardCoordinateGetter?: KeyboardCoordinateGetter;
|
|
14
|
+
/** Переопределяет клавиши начала/завершения, когда Enter занят primary action компонента. */
|
|
15
|
+
keyboardCodes?: KeyboardCodes;
|
|
3
16
|
}
|
|
4
17
|
/**
|
|
5
|
-
* Стандартная конфигурация DnD-сенсоров для сортируемых
|
|
18
|
+
* Стандартная конфигурация DnD-сенсоров для сортируемых списков и матриц.
|
|
6
19
|
*
|
|
7
20
|
* Зачем: исключить дублирование одинаковой настройки PointerSensor + KeyboardSensor
|
|
8
21
|
* в каждом хуке персонализации (колонки, сортировки, группировки).
|
|
9
22
|
*
|
|
10
|
-
* @param
|
|
23
|
+
* @param distanceOrOptions — минимальное смещение указателя или расширенные настройки mouse/touch/keyboard.
|
|
11
24
|
*/
|
|
12
25
|
export declare function useDndSortableSensors(distanceOrOptions?: number | UseDndSortableSensorsOptions): import("@dnd-kit/core").SensorDescriptor<import("@dnd-kit/core").SensorOptions>[];
|
|
13
26
|
//# sourceMappingURL=useDndSortableSensors.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useDndSortableSensors.d.ts","sourceRoot":"","sources":["../../src/hooks/useDndSortableSensors.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useDndSortableSensors.d.ts","sourceRoot":"","sources":["../../src/hooks/useDndSortableSensors.ts"],"names":[],"mappings":"AAAA,OAAO,EAON,KAAK,aAAa,EAClB,KAAK,wBAAwB,EAC7B,MAAM,eAAe,CAAC;AAGvB,MAAM,WAAW,4BAA4B;IAC5C,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,eAAe,CAAC,EAAE;QACjB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;KAC5B,CAAC;IACF,uGAAuG;IACvG,wBAAwB,CAAC,EAAE,wBAAwB,CAAC;IACpD,6FAA6F;IAC7F,aAAa,CAAC,EAAE,aAAa,CAAC;CAC9B;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,iBAAiB,GAAE,MAAM,GAAG,4BAAgC,qFAqBjG"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/query-client/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,2BAA2B,CAAC;AAC1C,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/query-client/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,yBAAyB,CAAC;AACxC,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,2BAA2B,CAAC;AAC1C,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC"}
|
|
@@ -69,43 +69,83 @@ function f(t) {
|
|
|
69
69
|
return e(t) ? t.type === "invalidate" ? Array.isArray(t.keys) ? t.refetchType === void 0 || t.refetchType === "none" || t.refetchType === "active" || t.refetchType === "inactive" || t.refetchType === "all" : !1 : t.type === "setQueryData" && "key" in t && "data" in t : !1;
|
|
70
70
|
}
|
|
71
71
|
//#endregion
|
|
72
|
+
//#region src/query-client/invalidationBroadcast.ts
|
|
73
|
+
function p(t) {
|
|
74
|
+
return e(t) && t.type === "query.invalidate" && typeof t.senderId == "string" && Array.isArray(t.queryKey);
|
|
75
|
+
}
|
|
76
|
+
function m(e, n) {
|
|
77
|
+
if (typeof window > "u" || typeof BroadcastChannel > "u") return {
|
|
78
|
+
broadcast: () => void 0,
|
|
79
|
+
cleanup: () => void 0
|
|
80
|
+
};
|
|
81
|
+
let r = t(), i = new BroadcastChannel(n.channelName), a = !1, o = !1, s = (e) => {
|
|
82
|
+
o || !n.matchesQuery(e) || i.postMessage({
|
|
83
|
+
type: "query.invalidate",
|
|
84
|
+
senderId: r,
|
|
85
|
+
queryKey: e
|
|
86
|
+
});
|
|
87
|
+
}, c = e.getQueryCache().subscribe((e) => {
|
|
88
|
+
a || e.type !== "updated" || e.action.type !== "invalidate" || s(e.query.queryKey);
|
|
89
|
+
});
|
|
90
|
+
return i.onmessage = (t) => {
|
|
91
|
+
let i = t.data;
|
|
92
|
+
if (!(o || !p(i) || i.senderId === r || !n.matchesQuery(i.queryKey))) {
|
|
93
|
+
a = !0;
|
|
94
|
+
try {
|
|
95
|
+
e.invalidateQueries({
|
|
96
|
+
queryKey: i.queryKey,
|
|
97
|
+
exact: !0,
|
|
98
|
+
refetchType: n.refetchType ?? "active"
|
|
99
|
+
});
|
|
100
|
+
} finally {
|
|
101
|
+
a = !1;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}, {
|
|
105
|
+
broadcast: s,
|
|
106
|
+
cleanup() {
|
|
107
|
+
o || (o = !0, c(), i.close());
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
72
112
|
//#region src/query-client/queryPersistenceAdapter.ts
|
|
73
|
-
var
|
|
74
|
-
function
|
|
75
|
-
|
|
113
|
+
var h = {};
|
|
114
|
+
function g(e) {
|
|
115
|
+
h = e;
|
|
76
116
|
}
|
|
77
|
-
function
|
|
78
|
-
return
|
|
117
|
+
function _() {
|
|
118
|
+
return h;
|
|
79
119
|
}
|
|
80
120
|
//#endregion
|
|
81
121
|
//#region src/query-client/persistence.ts
|
|
82
|
-
var
|
|
83
|
-
function
|
|
122
|
+
var v = "queries", y = __REACT_QUERY_PERSISTENCE_BUSTER__, b = 7776e6;
|
|
123
|
+
function x(e) {
|
|
84
124
|
if (e) return e;
|
|
85
125
|
if (globalThis.indexedDB !== void 0) return globalThis.indexedDB;
|
|
86
126
|
}
|
|
87
|
-
function
|
|
127
|
+
function S(e) {
|
|
88
128
|
return new Promise((t, n) => {
|
|
89
129
|
e.onsuccess = () => t(e.result), e.onerror = () => n(e.error);
|
|
90
130
|
});
|
|
91
131
|
}
|
|
92
|
-
function
|
|
132
|
+
function C() {
|
|
93
133
|
if (__DEV__) return `dev:${__APP_ID__}`;
|
|
94
|
-
let e =
|
|
134
|
+
let e = _().resolveSystemIdentifier?.();
|
|
95
135
|
if (e) return e;
|
|
96
136
|
let t = typeof window > "u" ? "server" : window.location.hostname;
|
|
97
137
|
return `${__APP_ID__}:${t}`;
|
|
98
138
|
}
|
|
99
|
-
function
|
|
100
|
-
return `${
|
|
139
|
+
function w() {
|
|
140
|
+
return `${C()}`;
|
|
101
141
|
}
|
|
102
|
-
function
|
|
142
|
+
function T(e) {
|
|
103
143
|
return e.meta?.persist === !0 && e.meta.sessionScoped !== !0;
|
|
104
144
|
}
|
|
105
|
-
function
|
|
106
|
-
let t =
|
|
145
|
+
function E(e = {}) {
|
|
146
|
+
let t = x(e.indexedDB);
|
|
107
147
|
if (!t) return;
|
|
108
|
-
let n = t, r = e.dbName ??
|
|
148
|
+
let n = t, r = e.dbName ?? C(), i = e.storeName ?? v, a = null;
|
|
109
149
|
function o() {
|
|
110
150
|
return a || (a = new Promise((e, t) => {
|
|
111
151
|
let a = n.open(r, 1);
|
|
@@ -116,7 +156,7 @@ function w(e = {}) {
|
|
|
116
156
|
}), a);
|
|
117
157
|
}
|
|
118
158
|
async function s(e, t) {
|
|
119
|
-
return
|
|
159
|
+
return S(t((await o()).transaction(i, e).objectStore(i)));
|
|
120
160
|
}
|
|
121
161
|
async function c(e, t) {
|
|
122
162
|
let n = await o();
|
|
@@ -165,26 +205,26 @@ function w(e = {}) {
|
|
|
165
205
|
}
|
|
166
206
|
};
|
|
167
207
|
}
|
|
168
|
-
function
|
|
169
|
-
let e =
|
|
208
|
+
function D() {
|
|
209
|
+
let e = E();
|
|
170
210
|
if (e) return r({
|
|
171
211
|
storage: e,
|
|
172
212
|
serialize: (e) => e,
|
|
173
213
|
deserialize: (e) => e,
|
|
174
|
-
prefix:
|
|
175
|
-
buster:
|
|
176
|
-
maxAge:
|
|
214
|
+
prefix: w(),
|
|
215
|
+
buster: _().persistenceBuster ?? y,
|
|
216
|
+
maxAge: b,
|
|
177
217
|
refetchOnRestore: !0,
|
|
178
|
-
filters: { predicate:
|
|
218
|
+
filters: { predicate: T }
|
|
179
219
|
});
|
|
180
220
|
}
|
|
181
221
|
//#endregion
|
|
182
222
|
//#region src/query-client/queryMeta.ts
|
|
183
|
-
var
|
|
223
|
+
var O = { persist: !0 }, k = { sessionScoped: !0 };
|
|
184
224
|
//#endregion
|
|
185
225
|
//#region src/query-client/queryClient.ts
|
|
186
|
-
function
|
|
187
|
-
let n =
|
|
226
|
+
function A({ onQueryError: e, onMutationError: t }) {
|
|
227
|
+
let n = D();
|
|
188
228
|
return new o({
|
|
189
229
|
queryCache: new a({ onError: e }),
|
|
190
230
|
mutationCache: new i({ onError: t }),
|
|
@@ -203,35 +243,35 @@ function O({ onQueryError: e, onMutationError: t }) {
|
|
|
203
243
|
}
|
|
204
244
|
});
|
|
205
245
|
}
|
|
206
|
-
async function
|
|
246
|
+
async function j(e, t, n) {
|
|
207
247
|
await e.cancelQueries({ queryKey: t });
|
|
208
248
|
let r = e.getQueryData(t);
|
|
209
249
|
return e.setQueryData(t, n), { previous: r };
|
|
210
250
|
}
|
|
211
|
-
function
|
|
251
|
+
function M(e, t, n) {
|
|
212
252
|
n?.previous !== void 0 && e.setQueryData(t, n.previous);
|
|
213
253
|
}
|
|
214
|
-
function
|
|
254
|
+
function N(e, t, n, r) {
|
|
215
255
|
e.setQueryData(t, (e) => r(e, n));
|
|
216
256
|
}
|
|
217
|
-
async function
|
|
218
|
-
let r = n?.refetchType ?? "none", i =
|
|
257
|
+
async function P(e, t, n) {
|
|
258
|
+
let r = n?.refetchType ?? "none", i = F(t) ? t : [t];
|
|
219
259
|
await Promise.all(i.map((t) => e.invalidateQueries({
|
|
220
260
|
queryKey: t,
|
|
221
261
|
refetchType: r
|
|
222
262
|
})));
|
|
223
263
|
}
|
|
224
|
-
function
|
|
264
|
+
function F(e) {
|
|
225
265
|
return Array.isArray(e) ? Array.isArray(e[0]) : !1;
|
|
226
266
|
}
|
|
227
267
|
//#endregion
|
|
228
268
|
//#region src/query-client/sessionScoped.ts
|
|
229
|
-
var
|
|
230
|
-
function
|
|
269
|
+
var I = "session-scoped-query-cache-reset", L;
|
|
270
|
+
function R(e) {
|
|
231
271
|
return e.meta?.sessionScoped === !0;
|
|
232
272
|
}
|
|
233
|
-
async function
|
|
234
|
-
let n = { predicate:
|
|
273
|
+
async function z(e, t = {}) {
|
|
274
|
+
let n = { predicate: R };
|
|
235
275
|
await e.cancelQueries(n);
|
|
236
276
|
let r = e.getQueryCache().findAll(n);
|
|
237
277
|
for (let t of r) {
|
|
@@ -245,26 +285,26 @@ async function L(e, t = {}) {
|
|
|
245
285
|
});
|
|
246
286
|
}
|
|
247
287
|
await e.refetchQueries({
|
|
248
|
-
predicate: (e) =>
|
|
288
|
+
predicate: (e) => R(e) && e.getObserversCount() > 0,
|
|
249
289
|
type: "active"
|
|
250
|
-
}, { cancelRefetch: !0 }), t.broadcast !== !1 &&
|
|
290
|
+
}, { cancelRefetch: !0 }), t.broadcast !== !1 && L?.();
|
|
251
291
|
}
|
|
252
|
-
function
|
|
292
|
+
function B(e, t) {
|
|
253
293
|
if (typeof window > "u" || typeof BroadcastChannel > "u") return { cleanup: () => void 0 };
|
|
254
294
|
let n = new BroadcastChannel(t.channelName), r = () => {
|
|
255
|
-
let e = { type:
|
|
295
|
+
let e = { type: I };
|
|
256
296
|
n.postMessage(e);
|
|
257
297
|
};
|
|
258
|
-
return
|
|
259
|
-
|
|
298
|
+
return L = r, n.onmessage = (t) => {
|
|
299
|
+
V(t.data) && z(e, { broadcast: !1 });
|
|
260
300
|
}, { cleanup: () => {
|
|
261
|
-
n.onmessage = null, n.close(),
|
|
301
|
+
n.onmessage = null, n.close(), L === r && (L = void 0);
|
|
262
302
|
} };
|
|
263
303
|
}
|
|
264
|
-
function
|
|
265
|
-
return typeof e == "object" && !!e && "type" in e && e.type ===
|
|
304
|
+
function V(e) {
|
|
305
|
+
return typeof e == "object" && !!e && "type" in e && e.type === I;
|
|
266
306
|
}
|
|
267
307
|
//#endregion
|
|
268
|
-
export {
|
|
308
|
+
export { y as REACT_QUERY_PERSISTENCE_BUSTER, b as REACT_QUERY_PERSISTENCE_MAX_AGE, l as broadcastCacheEvent, g as configureQueryPersistenceProjectAdapter, E as createIndexedDbQueryStorage, A as createQueryClient, D as createReactQueryPersister, _ as getQueryPersistenceProjectAdapter, m as installQueryInvalidationBroadcast, u as installReactQueryBroadcast, B as installSessionScopedQueryReset, R as isSessionScopedQuery, M as onErrorOptimistic, j as onMutateOptimistic, P as onSettledOptimistic, N as onSuccessOptimistic, O as persistedQueryMeta, z as resetSessionScopedQueries, k as sessionScopedQueryMeta, c as setBroadcastFn, T as shouldPersistQuery };
|
|
269
309
|
|
|
270
310
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/query-client/broadcast.ts","../../src/query-client/queryPersistenceAdapter.ts","../../src/query-client/persistence.ts","../../src/query-client/queryMeta.ts","../../src/query-client/queryClient.ts","../../src/query-client/sessionScoped.ts"],"sourcesContent":["import { broadcastQueryClient } from \"@tanstack/query-broadcast-client-experimental\";\n\nimport { uuidv4 } from \"../crypto\";\nimport { isRecord } from \"../validators\";\n\nimport type { QueryClient, QueryKey } from \"@tanstack/react-query\";\n\ntype CacheSyncEvent =\n\t| { type: \"invalidate\"; keys: QueryKey[]; refetchType?: \"none\" | \"all\" | \"active\" | \"inactive\" }\n\t| { type: \"setQueryData\"; key: QueryKey; data: unknown };\n\ntype InstallBroadcastOptions = {\n\tqueriesChannel: string;\n\teventsChannel: string;\n};\n\ntype InstalledReactQueryBroadcast = {\n\tbroadcast: (event: CacheSyncEvent) => void;\n\tcleanup: () => void;\n};\n\ntype WireMessage = {\n\ttabId: string;\n\tevent: CacheSyncEvent;\n};\n\nlet broadcastFn: ((event: CacheSyncEvent) => void) | null = null;\n\nexport function setBroadcastFn(fn: (event: CacheSyncEvent) => void) {\n\tbroadcastFn = fn;\n}\n\nexport function broadcastCacheEvent(event: CacheSyncEvent) {\n\tbroadcastFn?.(event);\n}\n\nexport function installReactQueryBroadcast(queryClient: QueryClient, opts: InstallBroadcastOptions): InstalledReactQueryBroadcast {\n\tif (typeof window === \"undefined\") {\n\t\treturn { broadcast: () => {}, cleanup: () => {} };\n\t}\n\n\tlet isApplyingRemoteEvent = false;\n\n\t// 1) Sync query cache (data/state)\n\tconst cleanupQueryBroadcast = broadcastQueryClient({\n\t\tqueryClient,\n\t\tbroadcastChannel: opts.queriesChannel\n\t});\n\n\t// 2) Sync \"events\" (invalidate/setQueryData)\n\tconst tabId = uuidv4();\n\tconst bc = new BroadcastChannel(opts.eventsChannel);\n\tconst unsubscribeInvalidationBroadcast = queryClient.getQueryCache().subscribe((queryEvent) => {\n\t\tif (isApplyingRemoteEvent) return;\n\t\tif (queryEvent.type !== \"updated\" || queryEvent.action.type !== \"invalidate\") return;\n\n\t\tbroadcast({\n\t\t\ttype: \"invalidate\",\n\t\t\tkeys: [queryEvent.query.queryKey],\n\t\t\trefetchType: \"none\"\n\t\t});\n\t});\n\n\tbc.onmessage = (ev: MessageEvent) => {\n\t\t// MessageEvent.data: unknown -> делаем безопасное сужение без any\n\t\tconst data: unknown = ev.data;\n\t\tif (!isWireMessage(data)) return;\n\t\tif (data.tabId === tabId) return;\n\n\t\tconst e = data.event;\n\t\tisApplyingRemoteEvent = true;\n\n\t\ttry {\n\t\t\tif (e.type === \"invalidate\") {\n\t\t\t\tconst refetchType = e.refetchType ?? \"none\";\n\t\t\t\tfor (const key of e.keys) {\n\t\t\t\t\tqueryClient.invalidateQueries({ queryKey: key, refetchType });\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (e.type === \"setQueryData\") {\n\t\t\t\tqueryClient.setQueryData(e.key, e.data);\n\t\t\t\treturn;\n\t\t\t}\n\t\t} finally {\n\t\t\tisApplyingRemoteEvent = false;\n\t\t}\n\t};\n\n\tfunction broadcast(event: CacheSyncEvent) {\n\t\tconst msg: WireMessage = { tabId, event };\n\t\tbc.postMessage(msg);\n\t}\n\n\treturn {\n\t\tbroadcast,\n\t\tcleanup: () => {\n\t\t\tunsubscribeInvalidationBroadcast();\n\t\t\tcleanupQueryBroadcast();\n\t\t\tbc.close();\n\t\t}\n\t};\n}\n\nfunction isWireMessage(v: unknown): v is WireMessage {\n\tif (!isRecord(v)) return false;\n\tif (typeof v.tabId !== \"string\") return false;\n\tif (!(\"event\" in v)) return false;\n\treturn isCacheSyncEvent(v.event);\n}\n\nfunction isCacheSyncEvent(v: unknown): v is CacheSyncEvent {\n\tif (!isRecord(v)) return false;\n\tif (v.type === \"invalidate\") {\n\t\t// keys: QueryKey[] — runtime мы не сможем валидировать “идеально”,\n\t\t// но проверим что это массив, а refetchType — допустимое значение\n\t\tif (!Array.isArray(v.keys)) return false;\n\t\tif (v.refetchType === undefined) return true;\n\t\treturn v.refetchType === \"none\" || v.refetchType === \"active\" || v.refetchType === \"inactive\" || v.refetchType === \"all\";\n\t}\n\tif (v.type === \"setQueryData\") {\n\t\t// key должен быть массивом или строкой — QueryKey в рантайме обычно массив\n\t\t// проверим хотя бы наличие поля\n\t\treturn \"key\" in v && \"data\" in v;\n\t}\n\treturn false;\n}\n","export type QueryPersistenceProjectAdapter = {\n\tpersistenceBuster?: string;\n\tresolveSystemIdentifier?: () => string | null | undefined;\n};\n\nlet queryPersistenceProjectAdapter: QueryPersistenceProjectAdapter = {};\n\nexport function configureQueryPersistenceProjectAdapter(adapter: QueryPersistenceProjectAdapter) {\n\tqueryPersistenceProjectAdapter = adapter;\n}\n\nexport function getQueryPersistenceProjectAdapter() {\n\treturn queryPersistenceProjectAdapter;\n}\n","import { experimental_createQueryPersister, type AsyncStorage, type PersistedQuery } from \"@tanstack/query-persist-client-core\";\nimport { type Query } from \"@tanstack/react-query\";\n\nimport { getQueryPersistenceProjectAdapter } from \"./queryPersistenceAdapter\";\n\nconst STORE_NAME = \"queries\";\n\nexport const REACT_QUERY_PERSISTENCE_BUSTER = __REACT_QUERY_PERSISTENCE_BUSTER__;\nexport const REACT_QUERY_PERSISTENCE_MAX_AGE = 90 * 24 * 60 * 60 * 1000;\n\ntype IndexedDbQueryStorageOptions = {\n\tdbName?: string;\n\tstoreName?: string;\n\tindexedDB?: IDBFactory;\n};\n\n/**\n * Описывает итог синхронного преобразования записи внутри одной IndexedDB-транзакции.\n * Отдельный result позволяет вернуть вызывающему коду вычисленный снимок только после commit.\n */\nexport type IndexedDbAtomicUpdateDecision<TStorageValue, TResult> =\n\t| { readonly action: \"keep\"; readonly result: TResult }\n\t| { readonly action: \"set\"; readonly value: TStorageValue; readonly result: TResult }\n\t| { readonly action: \"remove\"; readonly result: TResult };\n\nexport type IndexedDbQueryStorage<TStorageValue> = AsyncStorage<TStorageValue> & {\n\t/**\n\t * Читает и изменяет одну запись в общей readwrite-транзакции, поэтому параллельные\n\t * вкладки не могут потерять уже зафиксированное изменение между get и set.\n\t * Callback должен быть синхронным: IndexedDB закрывает неактивную транзакцию до\n\t * завершения произвольной асинхронной работы.\n\t */\n\treadonly updateItem: <TResult>(\n\t\tkey: string,\n\t\tupdate: (value: TStorageValue | undefined) => IndexedDbAtomicUpdateDecision<TStorageValue, TResult>\n\t) => Promise<TResult>;\n};\n\nfunction resolveIndexedDbFactory(factory?: IDBFactory) {\n\tif (factory) return factory;\n\tif (typeof globalThis.indexedDB === \"undefined\") return undefined;\n\treturn globalThis.indexedDB;\n}\n\nfunction requestToPromise<T>(request: IDBRequest<T>) {\n\treturn new Promise<T>((resolve, reject) => {\n\t\trequest.onsuccess = () => resolve(request.result);\n\t\trequest.onerror = () => reject(request.error);\n\t});\n}\n\nfunction getSystemIdentifier() {\n\tif (__DEV__) {\n\t\treturn `dev:${__APP_ID__}`;\n\t}\n\n\tconst projectIdentifier = getQueryPersistenceProjectAdapter().resolveSystemIdentifier?.();\n\tif (projectIdentifier) return projectIdentifier;\n\n\tconst hostname = typeof window === \"undefined\" ? \"server\" : window.location.hostname;\n\treturn `${__APP_ID__}:${hostname}`;\n}\n\nfunction getPersistencePrefix() {\n\tconst system = getSystemIdentifier();\n\treturn `${system}`;\n}\n\nexport function shouldPersistQuery(query: Pick<Query, \"meta\">) {\n\treturn query.meta?.persist === true && query.meta.sessionScoped !== true;\n}\n\nexport function createIndexedDbQueryStorage<TStorageValue = unknown>(\n\toptions: IndexedDbQueryStorageOptions = {}\n): IndexedDbQueryStorage<TStorageValue> | undefined {\n\tconst factory = resolveIndexedDbFactory(options.indexedDB);\n\tif (!factory) return undefined;\n\n\tconst idb = factory;\n\tconst dbName = options.dbName ?? getSystemIdentifier();\n\tconst storeName = options.storeName ?? STORE_NAME;\n\tlet dbPromise: Promise<IDBDatabase> | null = null;\n\n\tfunction openDb() {\n\t\tif (dbPromise) return dbPromise;\n\n\t\tdbPromise = new Promise<IDBDatabase>((resolve, reject) => {\n\t\t\tconst request = idb.open(dbName, 1);\n\n\t\t\trequest.onupgradeneeded = () => {\n\t\t\t\tconst db = request.result;\n\t\t\t\tif (!db.objectStoreNames.contains(storeName)) {\n\t\t\t\t\tdb.createObjectStore(storeName);\n\t\t\t\t}\n\t\t\t};\n\n\t\t\trequest.onsuccess = () => resolve(request.result);\n\t\t\trequest.onerror = () => reject(request.error);\n\t\t\trequest.onblocked = () => reject(request.error ?? new Error(\"Открытие IndexedDB заблокировано\"));\n\t\t});\n\n\t\treturn dbPromise;\n\t}\n\n\tasync function runTransaction<T>(mode: IDBTransactionMode, action: (store: IDBObjectStore) => IDBRequest<T>) {\n\t\tconst db = await openDb();\n\t\tconst tx = db.transaction(storeName, mode);\n\t\tconst store = tx.objectStore(storeName);\n\t\tconst request = action(store);\n\t\treturn requestToPromise(request);\n\t}\n\n\tasync function runWrite(mode: IDBTransactionMode, action: (store: IDBObjectStore) => void) {\n\t\tconst db = await openDb();\n\n\t\treturn new Promise<void>((resolve, reject) => {\n\t\t\tconst tx = db.transaction(storeName, mode);\n\t\t\tconst store = tx.objectStore(storeName);\n\n\t\t\ttx.oncomplete = () => resolve();\n\t\t\ttx.onerror = () => reject(tx.error);\n\t\t\ttx.onabort = () => reject(tx.error);\n\n\t\t\ttry {\n\t\t\t\taction(store);\n\t\t\t} catch (error) {\n\t\t\t\ttx.abort();\n\t\t\t\treject(error);\n\t\t\t}\n\t\t});\n\t}\n\n\tasync function updateItem<TResult>(\n\t\tkey: string,\n\t\tupdate: (value: TStorageValue | undefined) => IndexedDbAtomicUpdateDecision<TStorageValue, TResult>\n\t) {\n\t\tconst db = await openDb();\n\t\treturn new Promise<TResult>((resolve, reject) => {\n\t\t\tconst tx = db.transaction(storeName, \"readwrite\");\n\t\t\tconst store = tx.objectStore(storeName);\n\t\t\tconst request = store.get(key);\n\t\t\tlet result: TResult;\n\t\t\tlet hasResult = false;\n\n\t\t\ttx.oncomplete = () => {\n\t\t\t\tif (hasResult) resolve(result);\n\t\t\t\telse reject(new Error(\"IndexedDB-транзакция завершилась без результата обновления\"));\n\t\t\t};\n\t\t\ttx.onerror = () => reject(tx.error);\n\t\t\ttx.onabort = () => reject(tx.error);\n\t\t\trequest.onerror = () => {\n\t\t\t\ttx.abort();\n\t\t\t\treject(request.error);\n\t\t\t};\n\t\t\trequest.onsuccess = () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst decision = update(request.result as TStorageValue | undefined);\n\t\t\t\t\tresult = decision.result;\n\t\t\t\t\thasResult = true;\n\t\t\t\t\tif (decision.action === \"set\") store.put(decision.value, key);\n\t\t\t\t\telse if (decision.action === \"remove\") store.delete(key);\n\t\t\t\t} catch (error) {\n\t\t\t\t\ttx.abort();\n\t\t\t\t\treject(error);\n\t\t\t\t}\n\t\t\t};\n\t\t});\n\t}\n\n\treturn {\n\t\tgetItem: (key) => runTransaction<TStorageValue | undefined>(\"readonly\", (store) => store.get(key)),\n\t\tsetItem: (key, value) => runWrite(\"readwrite\", (store) => store.put(value, key)),\n\t\tremoveItem: (key) => runWrite(\"readwrite\", (store) => store.delete(key)),\n\t\tupdateItem,\n\t\tentries: async () => {\n\t\t\tconst db = await openDb();\n\n\t\t\treturn new Promise<Array<[key: string, value: TStorageValue]>>((resolve, reject) => {\n\t\t\t\tconst tx = db.transaction(storeName, \"readonly\");\n\t\t\t\tconst store = tx.objectStore(storeName);\n\t\t\t\tconst request = store.openCursor();\n\t\t\t\tconst entries: Array<[key: string, value: TStorageValue]> = [];\n\n\t\t\t\ttx.oncomplete = () => resolve(entries);\n\t\t\t\ttx.onerror = () => reject(tx.error);\n\t\t\t\ttx.onabort = () => reject(tx.error);\n\n\t\t\t\trequest.onsuccess = () => {\n\t\t\t\t\tconst cursor = request.result;\n\t\t\t\t\tif (!cursor) return;\n\n\t\t\t\t\tentries.push([String(cursor.key), cursor.value as TStorageValue]);\n\t\t\t\t\tcursor.continue();\n\t\t\t\t};\n\t\t\t\trequest.onerror = () => reject(request.error);\n\t\t\t});\n\t\t}\n\t};\n}\n\nexport function createReactQueryPersister() {\n\tconst storage = createIndexedDbQueryStorage<PersistedQuery>();\n\tif (!storage) return undefined;\n\n\treturn experimental_createQueryPersister<PersistedQuery>({\n\t\tstorage,\n\t\tserialize: (query) => query,\n\t\tdeserialize: (query) => query,\n\t\tprefix: getPersistencePrefix(),\n\t\tbuster: getQueryPersistenceProjectAdapter().persistenceBuster ?? REACT_QUERY_PERSISTENCE_BUSTER,\n\t\tmaxAge: REACT_QUERY_PERSISTENCE_MAX_AGE,\n\t\trefetchOnRestore: true,\n\t\tfilters: { predicate: shouldPersistQuery }\n\t});\n}\n","import { type QueryMeta } from \"@tanstack/react-query\";\n\n/**\n * Общие признаки query, которые управляют инфраструктурой клиентского кеша.\n *\n * `persist` разрешает долговременное хранение публичных справочных данных,\n * а `sessionScoped` помечает данные, доступность или содержимое которых зависит\n * от текущей серверной сессии. Эти признаки нельзя совмещать: session-scoped\n * query никогда не должна переживать смену пользователя или роли.\n */\nexport type AppQueryMeta = {\n\treadonly persist?: boolean;\n\treadonly sessionScoped?: boolean;\n} & Record<string, unknown>;\n\ndeclare module \"@tanstack/react-query\" {\n\tinterface Register {\n\t\tqueryMeta: AppQueryMeta;\n\t}\n}\n\n/** Разрешает сохранять публичную query в долговременном хранилище. */\nexport const persistedQueryMeta = { persist: true } as const satisfies QueryMeta;\n\n/** Помечает query для сброса при изменении серверной сессии. */\nexport const sessionScopedQueryMeta = { sessionScoped: true } as const satisfies QueryMeta;\n","import {\n\tDefaultError,\n\tMutation,\n\tMutationCache,\n\tMutationFunctionContext,\n\tQuery,\n\tQueryCache,\n\tQueryClient,\n\tQueryKey\n} from \"@tanstack/react-query\";\n\nimport { createReactQueryPersister } from \"./persistence\";\n\nexport type QueryBroadcastOptions = {\n\tonQueryError?: (error: DefaultError, query: Query<unknown, unknown, unknown>) => void;\n\tonMutationError?: (\n\t\terror: DefaultError,\n\t\tvariables: unknown,\n\t\tonMutateResult: unknown,\n\t\tmutation: Mutation<unknown, unknown, unknown>,\n\t\tcontext: MutationFunctionContext\n\t) => Promise<unknown> | unknown;\n};\n\n/**\n * Создаёт настроенный QueryClient. Singleton lifecycle обеспечивает host-приложение.\n */\nexport function createQueryClient({ onQueryError, onMutationError }: QueryBroadcastOptions) {\n\tconst queryPersister = createReactQueryPersister();\n\n\tconst queryClient = new QueryClient({\n\t\tqueryCache: new QueryCache({ onError: onQueryError }),\n\t\tmutationCache: new MutationCache({ onError: onMutationError }),\n\t\tdefaultOptions: {\n\t\t\t// TODO: Vite env\n\t\t\tqueries: {\n\t\t\t\tstaleTime: 10 * 60 * 1000,\n\t\t\t\tgcTime: 30 * 60 * 1000,\n\t\t\t\tthrowOnError: false,\n\t\t\t\tretry: false,\n\t\t\t\t...(queryPersister ? { persister: queryPersister.persisterFn } : {})\n\t\t\t},\n\t\t\tmutations: {\n\t\t\t\tthrowOnError: false,\n\t\t\t\tretry: false\n\t\t\t}\n\t\t}\n\t});\n\n\treturn queryClient;\n}\n\n/**\n * Контекст, возвращаемый из `onMutateOptimistic`.\n * Содержит снапшот кеша до оптимистичного обновления.\n * Передаётся в `onError`, `onSuccess`, `onSettled` через TanStack Query.\n */\nexport type OptimisticContext<TQueryCacheData> = {\n\tprevious: TQueryCacheData | undefined;\n};\n\n/**\n * _Оптимистичное обновление — это когда мы моментально меняем локальный кэш (и UI) до ответа сервера,\n * чтобы интерфейс был отзывчивым. Если сервер вернул ошибку — откатываем изменения.\n * Если сервер подтвердил — либо принимаем ответ сервера,\n * либо оставляем оптимистичное состояние (в зависимости от ситуации)._\n *\n * Обработчик `onMutate` для оптимистичного обновления.\n *\n * Выполняет три шага:\n * 1. Отменяет активные запросы по ключу — предотвращает гонку с сервером.\n * 2. Сохраняет снапшот текущего состояния кеша.\n * 3. Применяет оптимистичный патч через `patcher`.\n *\n * Возвращает {@link OptimisticContext} — передаётся в `onError` для отката.\n *\n * @example\n * // Обновление точечных данных в списке объектов\n * useMutation({\n * mutationFn: (data: SomeType) => { ... },\n * onMutate: async (data) =>\n * onMutateOptimistic(queryClient, someQueryKey, (old) => {\n * if (!old) return old;\n * return old.map((item) =>\n * item.id === data.id ? { ...item, visible: data.visible } : item\n * );\n * }),\n * onError: (_err, _vars, context) =>\n * onErrorOptimistic(queryClient, someQueryKey, context),\n * onSettled: () =>\n * onSettledOptimistic(queryClient, someQueryKey),\n * })\n */\nexport async function onMutateOptimistic<TQueryCacheData>(\n\tqc: QueryClient,\n\tqueryKey: QueryKey,\n\tpatcher: (oldCache: TQueryCacheData | undefined) => TQueryCacheData | undefined\n): Promise<OptimisticContext<TQueryCacheData>> {\n\t// 1) Отменяем запросы по ключу, чтобы не было гонки с сервером\n\tawait qc.cancelQueries({ queryKey });\n\n\t// 2) Сохраняем снапшот кеша для возможного отката\n\tconst previous = qc.getQueryData<TQueryCacheData>(queryKey);\n\n\t// 3) Применяем оптимистичный патч\n\tqc.setQueryData<TQueryCacheData>(queryKey, patcher);\n\n\treturn { previous };\n}\n\n/**\n * Обработчик `onError` для оптимистичного обновления.\n *\n * Откатывает кеш к снапшоту, сохранённому в {@link OptimisticContext}.\n * Если `context` не определён (например, `onMutate` сам выбросил ошибку) — безопасно ничего не делает.\n *\n * @example\n * onError: (_err, _vars, context) =>\n * onErrorOptimistic(queryClient, someQueryKey, context),\n */\nexport function onErrorOptimistic<TQueryCacheData>(\n\tqc: QueryClient,\n\tqueryKey: QueryKey,\n\tcontext: OptimisticContext<TQueryCacheData> | undefined\n): void {\n\tif (context?.previous !== undefined) {\n\t\tqc.setQueryData<TQueryCacheData>(queryKey, context.previous);\n\t}\n}\n\n/**\n * Обработчик `onSuccess` для оптимистичного обновления — применяет данные сервера в кеш.\n *\n * Используется, когда сервер вернул авторитетные данные, которые нужно слить\n * с текущим состоянием кеша (или заменить его целиком).\n *\n * Если сервер вернул `void` — эту функцию вызывать не нужно:\n * оптимистичное состояние в кеше остаётся без изменений.\n *\n * @param qc - экземпляр `QueryClient`\n * @param queryKey - ключ кеша\n * @param serverData - данные, вернувшиеся от сервера\n * @param merger - функция слияния: получает текущий кеш и ответ сервера, возвращает новое состояние\n *\n * @example\n * // Полная замена кеша ответом сервера\n * onSuccess: (data) =>\n * onSuccessOptimistic(queryClient, someQueryKey, data, (_cache, serverData) => serverData),\n *\n * @example\n * // Точечное обновление одного элемента в списке\n * onSuccess: (updatedItem) =>\n * onSuccessOptimistic(queryClient, someQueryKey, updatedItem,\n * (cache, item) => cache?.map((i) => (i.id === item.id ? item : i))\n * ),\n */\nexport function onSuccessOptimistic<TQueryCacheData, TServerData>(\n\tqc: QueryClient,\n\tqueryKey: QueryKey,\n\tserverData: TServerData,\n\tmerger: (cache: TQueryCacheData | undefined, serverData: TServerData) => TQueryCacheData | undefined\n): void {\n\tqc.setQueryData<TQueryCacheData>(queryKey, (old) => merger(old, serverData));\n}\n\n/**\n * Параметры для {@link onSettledOptimistic}.\n */\nexport type OnSettledOptimisticOptions = {\n\t/**\n\t * Тип рефетча при инвалидации.\n\t * - `\"none\"` (по умолчанию) — помечает кеш устаревшим, но не запускает рефетч немедленно.\n\t * - `\"active\"` — немедленно перезапрашивает активные подписчики.\n\t * - `\"inactive\"` — перезапрашивает неактивные (фоновые) запросы.\n\t * - `\"all\"` — перезапрашивает все подходящие запросы.\n\t */\n\trefetchType?: \"none\" | \"active\" | \"inactive\" | \"all\";\n};\n\n/**\n * Обработчик `onSettled` для оптимистичного обновления.\n *\n * Инвалидирует один или несколько query-ключей после завершения мутации\n * (выполняется и при успехе, и при ошибке).\n *\n * По умолчанию использует `refetchType: \"none\"` — помечает кеш устаревшим,\n * но не запускает немедленный рефетч. Это безопасный вариант при оптимистике:\n * данные в кеше уже актуальны (подтверждены или откатаны), а рефетч произойдёт\n * при следующем монтировании или явном запросе.\n *\n * Broadcast в другие вкладки — ответственность вызывающего кода через `broadcastCacheEvent`.\n *\n * @example\n * onSettled: () =>\n * onSettledOptimistic(queryClient, someQueryKey),\n *\n * @example\n * // С принудительным рефетчем активных подписчиков\n * onSettled: () =>\n * onSettledOptimistic(queryClient, [queryKeyA, queryKeyB], { refetchType: \"active\" }),\n */\nexport async function onSettledOptimistic(\n\tqc: QueryClient,\n\tqueryKeys: QueryKey | QueryKey[],\n\toptions?: OnSettledOptimisticOptions\n): Promise<void> {\n\tconst refetchType = options?.refetchType ?? \"none\";\n\t// Нормализуем одиночный ключ к массиву для единообразной обработки\n\tconst keys: QueryKey[] = isNestedQueryKey(queryKeys) ? queryKeys : [queryKeys];\n\n\tawait Promise.all(keys.map((queryKey) => qc.invalidateQueries({ queryKey, refetchType })));\n}\n\n/**\n * Определяет, является ли значение массивом query-ключей (QueryKey[]),\n * а не одиночным QueryKey.\n *\n * QueryKey в TanStack Query — это `string | readonly unknown[]`.\n * Чтобы отличить `QueryKey[]` от `QueryKey` типа `readonly unknown[]`,\n * проверяем, содержит ли массив хотя бы один вложенный массив или строку на верхнем уровне.\n */\nfunction isNestedQueryKey(value: QueryKey | QueryKey[]): value is QueryKey[] {\n\tif (!Array.isArray(value)) return false;\n\t// Если первый элемент сам является массивом — это QueryKey[]\n\treturn Array.isArray(value[0]);\n}\n","import { type Query, type QueryClient } from \"@tanstack/react-query\";\n\nconst SESSION_SCOPED_QUERY_CACHE_RESET_EVENT = \"session-scoped-query-cache-reset\";\n\ntype SessionScopedQueryCacheResetEvent = {\n\treadonly type: typeof SESSION_SCOPED_QUERY_CACHE_RESET_EVENT;\n};\n\nexport type ResetSessionScopedQueriesOptions = {\n\t/**\n\t * Передавать ли команду сброса другим вкладкам. При обработке входящего\n\t * сообщения значение отключается, чтобы вкладки не создавали цикл событий.\n\t */\n\treadonly broadcast?: boolean;\n};\n\nexport type InstallSessionScopedQueryResetOptions = {\n\t/** Уникальное для приложения имя служебного BroadcastChannel. */\n\treadonly channelName: string;\n};\n\nexport type InstalledSessionScopedQueryReset = {\n\t/** Освобождает канал и прекращает межвкладочную синхронизацию. */\n\treadonly cleanup: () => void;\n};\n\nlet broadcastSessionScopedQueryReset: (() => void) | undefined;\n\n/** Проверяет, относится ли query к данным текущей серверной сессии. */\nexport function isSessionScopedQuery(query: Pick<Query, \"meta\">): boolean {\n\treturn query.meta?.sessionScoped === true;\n}\n\n/**\n * Сбрасывает все session-scoped query после входа, выхода, обновления сессии\n * или потери авторизации.\n *\n * Сначала отменяются незавершённые запросы старой сессии, затем TanStack Query\n * очищает их состояние и повторно запрашивает только активные query. Обычные\n * публичные данные при этом остаются в кеше. По умолчанию отдельная команда\n * сброса отправляется другим вкладкам без передачи содержимого кеша или токенов.\n */\nexport async function resetSessionScopedQueries(queryClient: QueryClient, options: ResetSessionScopedQueriesOptions = {}): Promise<void> {\n\tconst filters = { predicate: isSessionScopedQuery } as const;\n\n\tawait queryClient.cancelQueries(filters);\n\n\tconst matchedQueries = queryClient.getQueryCache().findAll(filters);\n\tfor (const query of matchedQueries) {\n\t\tif (query.getObserversCount() === 0) {\n\t\t\tqueryClient.getQueryCache().remove(query);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Hydrated query хранит SSR-данные как initial state, поэтому одного\n\t\t// query.reset() недостаточно: старая сессия могла бы вернуться в UI.\n\t\tquery.reset();\n\t\tquery.setState({ data: undefined, dataUpdatedAt: 0 });\n\t}\n\n\tawait queryClient.refetchQueries(\n\t\t{\n\t\t\tpredicate: (query) => isSessionScopedQuery(query) && query.getObserversCount() > 0,\n\t\t\ttype: \"active\"\n\t\t},\n\t\t{ cancelRefetch: true }\n\t);\n\n\tif (options.broadcast !== false) {\n\t\tbroadcastSessionScopedQueryReset?.();\n\t}\n}\n\n/**\n * Подключает безопасную межвкладочную синхронизацию смены сессии.\n *\n * Канал передаёт только команду сброса. В отличие от полной синхронизации\n * Query cache, он не публикует данные query и потому подходит для auth-зависимых\n * ответов. На сервере и в браузерах без BroadcastChannel возвращается no-op.\n */\nexport function installSessionScopedQueryReset(\n\tqueryClient: QueryClient,\n\toptions: InstallSessionScopedQueryResetOptions\n): InstalledSessionScopedQueryReset {\n\tif (typeof window === \"undefined\" || typeof BroadcastChannel === \"undefined\") {\n\t\treturn { cleanup: () => undefined };\n\t}\n\n\tconst channel = new BroadcastChannel(options.channelName);\n\tconst broadcast = () => {\n\t\tconst event: SessionScopedQueryCacheResetEvent = {\n\t\t\ttype: SESSION_SCOPED_QUERY_CACHE_RESET_EVENT\n\t\t};\n\t\tchannel.postMessage(event);\n\t};\n\n\tbroadcastSessionScopedQueryReset = broadcast;\n\tchannel.onmessage = (event: MessageEvent<unknown>) => {\n\t\tif (!isSessionScopedQueryCacheResetEvent(event.data)) return;\n\t\tvoid resetSessionScopedQueries(queryClient, { broadcast: false });\n\t};\n\n\treturn {\n\t\tcleanup: () => {\n\t\t\tchannel.onmessage = null;\n\t\t\tchannel.close();\n\t\t\tif (broadcastSessionScopedQueryReset === broadcast) {\n\t\t\t\tbroadcastSessionScopedQueryReset = undefined;\n\t\t\t}\n\t\t}\n\t};\n}\n\n/** Защищает обработчик канала от посторонних или устаревших сообщений. */\nfunction isSessionScopedQueryCacheResetEvent(value: unknown): value is SessionScopedQueryCacheResetEvent {\n\treturn typeof value === \"object\" && value !== null && \"type\" in value && value.type === SESSION_SCOPED_QUERY_CACHE_RESET_EVENT;\n}\n"],"mappings":";;;;;;AA0BA,IAAI,IAAwD;AAE5D,SAAgB,EAAe,GAAqC;CACnE,IAAc;AACf;AAEA,SAAgB,EAAoB,GAAuB;CAC1D,IAAc,CAAK;AACpB;AAEA,SAAgB,EAA2B,GAA0B,GAA6D;CACjI,IAAI,OAAO,SAAW,KACrB,OAAO;EAAE,iBAAiB,CAAC;EAAG,eAAe,CAAC;CAAE;CAGjD,IAAI,IAAwB,IAGtB,IAAwB,EAAqB;EAClD;EACA,kBAAkB,EAAK;CACxB,CAAC,GAGK,IAAQ,EAAO,GACf,IAAK,IAAI,iBAAiB,EAAK,aAAa,GAC5C,IAAmC,EAAY,cAAc,CAAC,CAAC,WAAW,MAAe;EAC1F,KACA,EAAW,SAAS,aAAa,EAAW,OAAO,SAAS,gBAEhE,EAAU;GACT,MAAM;GACN,MAAM,CAAC,EAAW,MAAM,QAAQ;GAChC,aAAa;EACd,CAAC;CACF,CAAC;CAED,EAAG,aAAa,MAAqB;EAEpC,IAAM,IAAgB,EAAG;EAEzB,IADI,CAAC,EAAc,CAAI,KACnB,EAAK,UAAU,GAAO;EAE1B,IAAM,IAAI,EAAK;EACf,IAAwB;EAExB,IAAI;GACH,IAAI,EAAE,SAAS,cAAc;IAC5B,IAAM,IAAc,EAAE,eAAe;IACrC,KAAK,IAAM,KAAO,EAAE,MACnB,EAAY,kBAAkB;KAAE,UAAU;KAAK;IAAY,CAAC;IAE7D;GACD;GAEA,IAAI,EAAE,SAAS,gBAAgB;IAC9B,EAAY,aAAa,EAAE,KAAK,EAAE,IAAI;IACtC;GACD;EACD,UAAU;GACT,IAAwB;EACzB;CACD;CAEA,SAAS,EAAU,GAAuB;EACzC,IAAM,IAAmB;GAAE;GAAO;EAAM;EACxC,EAAG,YAAY,CAAG;CACnB;CAEA,OAAO;EACN;EACA,eAAe;GAGd,AAFA,EAAiC,GACjC,EAAsB,GACtB,EAAG,MAAM;EACV;CACD;AACD;AAEA,SAAS,EAAc,GAA8B;CAIpD,OAHI,CAAC,EAAS,CAAC,KACX,OAAO,EAAE,SAAU,YACnB,EAAE,WAAW,KAAW,KACrB,EAAiB,EAAE,KAAK;AAChC;AAEA,SAAS,EAAiB,GAAiC;CAc1D,OAbK,EAAS,CAAC,IACX,EAAE,SAAS,eAGT,MAAM,QAAQ,EAAE,IAAI,IACrB,EAAE,gBAAgB,KAAA,KACf,EAAE,gBAAgB,UAAU,EAAE,gBAAgB,YAAY,EAAE,gBAAgB,cAAc,EAAE,gBAAgB,QAFhF,KAIhC,EAAE,SAAS,kBAGP,SAAS,KAAK,UAAU,IAXP;AAc1B;;;AC1HA,IAAI,IAAiE,CAAC;AAEtE,SAAgB,EAAwC,GAAyC;CAChG,IAAiC;AAClC;AAEA,SAAgB,IAAoC;CACnD,OAAO;AACR;;;ACRA,IAAM,IAAa,WAEN,IAAiC,oCACjC,IAAkC;AA8B/C,SAAS,EAAwB,GAAsB;CACtD,IAAI,GAAS,OAAO;CAChB,IAAO,WAAW,cAAc,QACpC,OAAO,WAAW;AACnB;AAEA,SAAS,EAAoB,GAAwB;CACpD,OAAO,IAAI,SAAY,GAAS,MAAW;EAE1C,AADA,EAAQ,kBAAkB,EAAQ,EAAQ,MAAM,GAChD,EAAQ,gBAAgB,EAAO,EAAQ,KAAK;CAC7C,CAAC;AACF;AAEA,SAAS,IAAsB;CAC9B,IAAI,SACH,OAAO,OAAO;CAGf,IAAM,IAAoB,EAAkC,CAAC,CAAC,0BAA0B;CACxF,IAAI,GAAmB,OAAO;CAE9B,IAAM,IAAW,OAAO,SAAW,MAAc,WAAW,OAAO,SAAS;CAC5E,OAAO,GAAG,WAAW,GAAG;AACzB;AAEA,SAAS,IAAuB;CAE/B,OAAO,GADQ,EACL;AACX;AAEA,SAAgB,EAAmB,GAA4B;CAC9D,OAAO,EAAM,MAAM,YAAY,MAAQ,EAAM,KAAK,kBAAkB;AACrE;AAEA,SAAgB,EACf,IAAwC,CAAC,GACU;CACnD,IAAM,IAAU,EAAwB,EAAQ,SAAS;CACzD,IAAI,CAAC,GAAS;CAEd,IAAM,IAAM,GACN,IAAS,EAAQ,UAAU,EAAoB,GAC/C,IAAY,EAAQ,aAAa,GACnC,IAAyC;CAE7C,SAAS,IAAS;EAkBjB,OAjBI,MAEJ,IAAY,IAAI,SAAsB,GAAS,MAAW;GACzD,IAAM,IAAU,EAAI,KAAK,GAAQ,CAAC;GAWlC,AATA,EAAQ,wBAAwB;IAC/B,IAAM,IAAK,EAAQ;IACnB,AAAK,EAAG,iBAAiB,SAAS,CAAS,KAC1C,EAAG,kBAAkB,CAAS;GAEhC,GAEA,EAAQ,kBAAkB,EAAQ,EAAQ,MAAM,GAChD,EAAQ,gBAAgB,EAAO,EAAQ,KAAK,GAC5C,EAAQ,kBAAkB,EAAO,EAAQ,SAAS,gBAAI,MAAM,kCAAkC,CAAC;EAChG,CAAC,GAEM;CACR;CAEA,eAAe,EAAkB,GAA0B,GAAkD;EAK5G,OAAO,EADS,GAFL,MADM,EAAO,EAAA,CACV,YAAY,GAAW,CACvB,CAAA,CAAG,YAAY,CACN,CACC,CAAO;CAChC;CAEA,eAAe,EAAS,GAA0B,GAAyC;EAC1F,IAAM,IAAK,MAAM,EAAO;EAExB,OAAO,IAAI,SAAe,GAAS,MAAW;GAC7C,IAAM,IAAK,EAAG,YAAY,GAAW,CAAI,GACnC,IAAQ,EAAG,YAAY,CAAS;GAItC,AAFA,EAAG,mBAAmB,EAAQ,GAC9B,EAAG,gBAAgB,EAAO,EAAG,KAAK,GAClC,EAAG,gBAAgB,EAAO,EAAG,KAAK;GAElC,IAAI;IACH,EAAO,CAAK;GACb,SAAS,GAAO;IAEf,AADA,EAAG,MAAM,GACT,EAAO,CAAK;GACb;EACD,CAAC;CACF;CAEA,eAAe,EACd,GACA,GACC;EACD,IAAM,IAAK,MAAM,EAAO;EACxB,OAAO,IAAI,SAAkB,GAAS,MAAW;GAChD,IAAM,IAAK,EAAG,YAAY,GAAW,WAAW,GAC1C,IAAQ,EAAG,YAAY,CAAS,GAChC,IAAU,EAAM,IAAI,CAAG,GACzB,GACA,IAAY;GAYhB,AAVA,EAAG,mBAAmB;IACrB,AAAI,IAAW,EAAQ,CAAM,IACxB,EAAO,gBAAI,MAAM,4DAA4D,CAAC;GACpF,GACA,EAAG,gBAAgB,EAAO,EAAG,KAAK,GAClC,EAAG,gBAAgB,EAAO,EAAG,KAAK,GAClC,EAAQ,gBAAgB;IAEvB,AADA,EAAG,MAAM,GACT,EAAO,EAAQ,KAAK;GACrB,GACA,EAAQ,kBAAkB;IACzB,IAAI;KACH,IAAM,IAAW,EAAO,EAAQ,MAAmC;KAGnE,AAFA,IAAS,EAAS,QAClB,IAAY,IACR,EAAS,WAAW,QAAO,EAAM,IAAI,EAAS,OAAO,CAAG,IACnD,EAAS,WAAW,YAAU,EAAM,OAAO,CAAG;IACxD,SAAS,GAAO;KAEf,AADA,EAAG,MAAM,GACT,EAAO,CAAK;IACb;GACD;EACD,CAAC;CACF;CAEA,OAAO;EACN,UAAU,MAAQ,EAA0C,aAAa,MAAU,EAAM,IAAI,CAAG,CAAC;EACjG,UAAU,GAAK,MAAU,EAAS,cAAc,MAAU,EAAM,IAAI,GAAO,CAAG,CAAC;EAC/E,aAAa,MAAQ,EAAS,cAAc,MAAU,EAAM,OAAO,CAAG,CAAC;EACvE;EACA,SAAS,YAAY;GACpB,IAAM,IAAK,MAAM,EAAO;GAExB,OAAO,IAAI,SAAqD,GAAS,MAAW;IACnF,IAAM,IAAK,EAAG,YAAY,GAAW,UAAU,GAEzC,IADQ,EAAG,YAAY,CACb,CAAA,CAAM,WAAW,GAC3B,IAAsD,CAAC;IAa7D,AAXA,EAAG,mBAAmB,EAAQ,CAAO,GACrC,EAAG,gBAAgB,EAAO,EAAG,KAAK,GAClC,EAAG,gBAAgB,EAAO,EAAG,KAAK,GAElC,EAAQ,kBAAkB;KACzB,IAAM,IAAS,EAAQ;KAClB,MAEL,EAAQ,KAAK,CAAC,OAAO,EAAO,GAAG,GAAG,EAAO,KAAsB,CAAC,GAChE,EAAO,SAAS;IACjB,GACA,EAAQ,gBAAgB,EAAO,EAAQ,KAAK;GAC7C,CAAC;EACF;CACD;AACD;AAEA,SAAgB,IAA4B;CAC3C,IAAM,IAAU,EAA4C;CACvD,OAEL,OAAO,EAAkD;EACxD;EACA,YAAY,MAAU;EACtB,cAAc,MAAU;EACxB,QAAQ,EAAqB;EAC7B,QAAQ,EAAkC,CAAC,CAAC,qBAAqB;EACjE,QAAQ;EACR,kBAAkB;EAClB,SAAS,EAAE,WAAW,EAAmB;CAC1C,CAAC;AACF;;;AChMA,IAAa,IAAqB,EAAE,SAAS,GAAK,GAGrC,IAAyB,EAAE,eAAe,GAAK;;;ACE5D,SAAgB,EAAkB,EAAE,iBAAc,sBAA0C;CAC3F,IAAM,IAAiB,EAA0B;CAqBjD,OAAO,IAnBiB,EAAY;EACnC,YAAY,IAAI,EAAW,EAAE,SAAS,EAAa,CAAC;EACpD,eAAe,IAAI,EAAc,EAAE,SAAS,EAAgB,CAAC;EAC7D,gBAAgB;GAEf,SAAS;IACR,WAAW;IACX,QAAQ;IACR,cAAc;IACd,OAAO;IACP,GAAI,IAAiB,EAAE,WAAW,EAAe,YAAY,IAAI,CAAC;GACnE;GACA,WAAW;IACV,cAAc;IACd,OAAO;GACR;EACD;CACD,CAEO;AACR;AA2CA,eAAsB,EACrB,GACA,GACA,GAC8C;CAE9C,MAAM,EAAG,cAAc,EAAE,YAAS,CAAC;CAGnC,IAAM,IAAW,EAAG,aAA8B,CAAQ;CAK1D,OAFA,EAAG,aAA8B,GAAU,CAAO,GAE3C,EAAE,YAAS;AACnB;AAYA,SAAgB,EACf,GACA,GACA,GACO;CACP,AAAI,GAAS,aAAa,KAAA,KACzB,EAAG,aAA8B,GAAU,EAAQ,QAAQ;AAE7D;AA4BA,SAAgB,EACf,GACA,GACA,GACA,GACO;CACP,EAAG,aAA8B,IAAW,MAAQ,EAAO,GAAK,CAAU,CAAC;AAC5E;AAsCA,eAAsB,EACrB,GACA,GACA,GACgB;CAChB,IAAM,IAAc,GAAS,eAAe,QAEtC,IAAmB,EAAiB,CAAS,IAAI,IAAY,CAAC,CAAS;CAE7E,MAAM,QAAQ,IAAI,EAAK,KAAK,MAAa,EAAG,kBAAkB;EAAE;EAAU;CAAY,CAAC,CAAC,CAAC;AAC1F;AAUA,SAAS,EAAiB,GAAmD;CAG5E,OAFK,MAAM,QAAQ,CAAK,IAEjB,MAAM,QAAQ,EAAM,EAAE,IAFK;AAGnC;;;AC/NA,IAAM,IAAyC,oCAwB3C;AAGJ,SAAgB,EAAqB,GAAqC;CACzE,OAAO,EAAM,MAAM,kBAAkB;AACtC;AAWA,eAAsB,EAA0B,GAA0B,IAA4C,CAAC,GAAkB;CACxI,IAAM,IAAU,EAAE,WAAW,EAAqB;CAElD,MAAM,EAAY,cAAc,CAAO;CAEvC,IAAM,IAAiB,EAAY,cAAc,CAAC,CAAC,QAAQ,CAAO;CAClE,KAAK,IAAM,KAAS,GAAgB;EACnC,IAAI,EAAM,kBAAkB,MAAM,GAAG;GACpC,EAAY,cAAc,CAAC,CAAC,OAAO,CAAK;GACxC;EACD;EAKA,AADA,EAAM,MAAM,GACZ,EAAM,SAAS;GAAE,MAAM,KAAA;GAAW,eAAe;EAAE,CAAC;CACrD;CAUA,AARA,MAAM,EAAY,eACjB;EACC,YAAY,MAAU,EAAqB,CAAK,KAAK,EAAM,kBAAkB,IAAI;EACjF,MAAM;CACP,GACA,EAAE,eAAe,GAAK,CACvB,GAEI,EAAQ,cAAc,MACzB,IAAmC;AAErC;AASA,SAAgB,EACf,GACA,GACmC;CACnC,IAAI,OAAO,SAAW,OAAe,OAAO,mBAAqB,KAChE,OAAO,EAAE,eAAe,KAAA,EAAU;CAGnC,IAAM,IAAU,IAAI,iBAAiB,EAAQ,WAAW,GAClD,UAAkB;EACvB,IAAM,IAA2C,EAChD,MAAM,EACP;EACA,EAAQ,YAAY,CAAK;CAC1B;CAQA,OANA,IAAmC,GACnC,EAAQ,aAAa,MAAiC;EAChD,EAAoC,EAAM,IAAI,KACnD,EAA+B,GAAa,EAAE,WAAW,GAAM,CAAC;CACjE,GAEO,EACN,eAAe;EAGd,AAFA,EAAQ,YAAY,MACpB,EAAQ,MAAM,GACV,MAAqC,MACxC,IAAmC,KAAA;CAErC,EACD;AACD;AAGA,SAAS,EAAoC,GAA4D;CACxG,OAAO,OAAO,KAAU,cAAY,KAAkB,UAAU,KAAS,EAAM,SAAS;AACzF"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/query-client/broadcast.ts","../../src/query-client/invalidationBroadcast.ts","../../src/query-client/queryPersistenceAdapter.ts","../../src/query-client/persistence.ts","../../src/query-client/queryMeta.ts","../../src/query-client/queryClient.ts","../../src/query-client/sessionScoped.ts"],"sourcesContent":["import { broadcastQueryClient } from \"@tanstack/query-broadcast-client-experimental\";\n\nimport { uuidv4 } from \"../crypto\";\nimport { isRecord } from \"../validators\";\n\nimport type { QueryClient, QueryKey } from \"@tanstack/react-query\";\n\ntype CacheSyncEvent =\n\t| { type: \"invalidate\"; keys: QueryKey[]; refetchType?: \"none\" | \"all\" | \"active\" | \"inactive\" }\n\t| { type: \"setQueryData\"; key: QueryKey; data: unknown };\n\ntype InstallBroadcastOptions = {\n\tqueriesChannel: string;\n\teventsChannel: string;\n};\n\ntype InstalledReactQueryBroadcast = {\n\tbroadcast: (event: CacheSyncEvent) => void;\n\tcleanup: () => void;\n};\n\ntype WireMessage = {\n\ttabId: string;\n\tevent: CacheSyncEvent;\n};\n\nlet broadcastFn: ((event: CacheSyncEvent) => void) | null = null;\n\nexport function setBroadcastFn(fn: (event: CacheSyncEvent) => void) {\n\tbroadcastFn = fn;\n}\n\nexport function broadcastCacheEvent(event: CacheSyncEvent) {\n\tbroadcastFn?.(event);\n}\n\nexport function installReactQueryBroadcast(queryClient: QueryClient, opts: InstallBroadcastOptions): InstalledReactQueryBroadcast {\n\tif (typeof window === \"undefined\") {\n\t\treturn { broadcast: () => {}, cleanup: () => {} };\n\t}\n\n\tlet isApplyingRemoteEvent = false;\n\n\t// 1) Sync query cache (data/state)\n\tconst cleanupQueryBroadcast = broadcastQueryClient({\n\t\tqueryClient,\n\t\tbroadcastChannel: opts.queriesChannel\n\t});\n\n\t// 2) Sync \"events\" (invalidate/setQueryData)\n\tconst tabId = uuidv4();\n\tconst bc = new BroadcastChannel(opts.eventsChannel);\n\tconst unsubscribeInvalidationBroadcast = queryClient.getQueryCache().subscribe((queryEvent) => {\n\t\tif (isApplyingRemoteEvent) return;\n\t\tif (queryEvent.type !== \"updated\" || queryEvent.action.type !== \"invalidate\") return;\n\n\t\tbroadcast({\n\t\t\ttype: \"invalidate\",\n\t\t\tkeys: [queryEvent.query.queryKey],\n\t\t\trefetchType: \"none\"\n\t\t});\n\t});\n\n\tbc.onmessage = (ev: MessageEvent) => {\n\t\t// MessageEvent.data: unknown -> делаем безопасное сужение без any\n\t\tconst data: unknown = ev.data;\n\t\tif (!isWireMessage(data)) return;\n\t\tif (data.tabId === tabId) return;\n\n\t\tconst e = data.event;\n\t\tisApplyingRemoteEvent = true;\n\n\t\ttry {\n\t\t\tif (e.type === \"invalidate\") {\n\t\t\t\tconst refetchType = e.refetchType ?? \"none\";\n\t\t\t\tfor (const key of e.keys) {\n\t\t\t\t\tqueryClient.invalidateQueries({ queryKey: key, refetchType });\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (e.type === \"setQueryData\") {\n\t\t\t\tqueryClient.setQueryData(e.key, e.data);\n\t\t\t\treturn;\n\t\t\t}\n\t\t} finally {\n\t\t\tisApplyingRemoteEvent = false;\n\t\t}\n\t};\n\n\tfunction broadcast(event: CacheSyncEvent) {\n\t\tconst msg: WireMessage = { tabId, event };\n\t\tbc.postMessage(msg);\n\t}\n\n\treturn {\n\t\tbroadcast,\n\t\tcleanup: () => {\n\t\t\tunsubscribeInvalidationBroadcast();\n\t\t\tcleanupQueryBroadcast();\n\t\t\tbc.close();\n\t\t}\n\t};\n}\n\nfunction isWireMessage(v: unknown): v is WireMessage {\n\tif (!isRecord(v)) return false;\n\tif (typeof v.tabId !== \"string\") return false;\n\tif (!(\"event\" in v)) return false;\n\treturn isCacheSyncEvent(v.event);\n}\n\nfunction isCacheSyncEvent(v: unknown): v is CacheSyncEvent {\n\tif (!isRecord(v)) return false;\n\tif (v.type === \"invalidate\") {\n\t\t// keys: QueryKey[] — runtime мы не сможем валидировать “идеально”,\n\t\t// но проверим что это массив, а refetchType — допустимое значение\n\t\tif (!Array.isArray(v.keys)) return false;\n\t\tif (v.refetchType === undefined) return true;\n\t\treturn v.refetchType === \"none\" || v.refetchType === \"active\" || v.refetchType === \"inactive\" || v.refetchType === \"all\";\n\t}\n\tif (v.type === \"setQueryData\") {\n\t\t// key должен быть массивом или строкой — QueryKey в рантайме обычно массив\n\t\t// проверим хотя бы наличие поля\n\t\treturn \"key\" in v && \"data\" in v;\n\t}\n\treturn false;\n}\n","import { uuidv4 } from \"../crypto\";\nimport { isRecord } from \"../validators\";\n\nimport type { QueryClient, QueryKey } from \"@tanstack/react-query\";\n\nexport type QueryInvalidationBroadcastOptions = {\n\t/** Уникальное в пределах origin имя канала приложения. */\n\treadonly channelName: string;\n\t/** Ограничивает как исходящие, так и входящие Query keys. */\n\treadonly matchesQuery: (queryKey: QueryKey) => boolean;\n\t/** Определяет, какие queries получающей вкладки будут перезапрошены сразу. */\n\treadonly refetchType?: \"none\" | \"active\" | \"inactive\" | \"all\";\n};\n\nexport type InstalledQueryInvalidationBroadcast = {\n\t/** Явно передаёт точечную инвалидацию без изменения локального QueryClient. */\n\treadonly broadcast: (queryKey: QueryKey) => void;\n\treadonly cleanup: () => void;\n};\n\ntype QueryInvalidationWireMessage = {\n\treadonly type: \"query.invalidate\";\n\treadonly senderId: string;\n\treadonly queryKey: QueryKey;\n};\n\nfunction isQueryInvalidationWireMessage(value: unknown): value is QueryInvalidationWireMessage {\n\treturn isRecord(value) && value.type === \"query.invalidate\" && typeof value.senderId === \"string\" && Array.isArray(value.queryKey);\n}\n\n/**\n * Передаёт между вкладками только факт инвалидации выбранного Query key.\n * Server data остаётся у QueryClient каждой вкладки и заново читается её queryFn.\n */\nexport function installQueryInvalidationBroadcast(\n\tqueryClient: QueryClient,\n\toptions: QueryInvalidationBroadcastOptions\n): InstalledQueryInvalidationBroadcast {\n\tif (typeof window === \"undefined\" || typeof BroadcastChannel === \"undefined\") {\n\t\treturn { broadcast: () => undefined, cleanup: () => undefined };\n\t}\n\n\tconst senderId = uuidv4();\n\tconst channel = new BroadcastChannel(options.channelName);\n\tlet applyingRemoteInvalidation = false;\n\tlet closed = false;\n\n\tconst broadcast = (queryKey: QueryKey) => {\n\t\tif (closed || !options.matchesQuery(queryKey)) return;\n\t\tchannel.postMessage({ type: \"query.invalidate\", senderId, queryKey } satisfies QueryInvalidationWireMessage);\n\t};\n\n\tconst unsubscribe = queryClient.getQueryCache().subscribe((event) => {\n\t\tif (applyingRemoteInvalidation || event.type !== \"updated\" || event.action.type !== \"invalidate\") return;\n\t\tbroadcast(event.query.queryKey);\n\t});\n\n\tchannel.onmessage = (event: MessageEvent<unknown>) => {\n\t\tconst message = event.data;\n\t\tif (closed || !isQueryInvalidationWireMessage(message) || message.senderId === senderId || !options.matchesQuery(message.queryKey))\n\t\t\treturn;\n\n\t\tapplyingRemoteInvalidation = true;\n\t\ttry {\n\t\t\tvoid queryClient.invalidateQueries({\n\t\t\t\tqueryKey: message.queryKey,\n\t\t\t\texact: true,\n\t\t\t\trefetchType: options.refetchType ?? \"active\"\n\t\t\t});\n\t\t} finally {\n\t\t\tapplyingRemoteInvalidation = false;\n\t\t}\n\t};\n\n\treturn {\n\t\tbroadcast,\n\t\tcleanup() {\n\t\t\tif (closed) return;\n\t\t\tclosed = true;\n\t\t\tunsubscribe();\n\t\t\tchannel.close();\n\t\t}\n\t};\n}\n","export type QueryPersistenceProjectAdapter = {\n\tpersistenceBuster?: string;\n\tresolveSystemIdentifier?: () => string | null | undefined;\n};\n\nlet queryPersistenceProjectAdapter: QueryPersistenceProjectAdapter = {};\n\nexport function configureQueryPersistenceProjectAdapter(adapter: QueryPersistenceProjectAdapter) {\n\tqueryPersistenceProjectAdapter = adapter;\n}\n\nexport function getQueryPersistenceProjectAdapter() {\n\treturn queryPersistenceProjectAdapter;\n}\n","import { experimental_createQueryPersister, type AsyncStorage, type PersistedQuery } from \"@tanstack/query-persist-client-core\";\nimport { type Query } from \"@tanstack/react-query\";\n\nimport { getQueryPersistenceProjectAdapter } from \"./queryPersistenceAdapter\";\n\nconst STORE_NAME = \"queries\";\n\nexport const REACT_QUERY_PERSISTENCE_BUSTER = __REACT_QUERY_PERSISTENCE_BUSTER__;\nexport const REACT_QUERY_PERSISTENCE_MAX_AGE = 90 * 24 * 60 * 60 * 1000;\n\ntype IndexedDbQueryStorageOptions = {\n\tdbName?: string;\n\tstoreName?: string;\n\tindexedDB?: IDBFactory;\n};\n\n/**\n * Описывает итог синхронного преобразования записи внутри одной IndexedDB-транзакции.\n * Отдельный result позволяет вернуть вызывающему коду вычисленный снимок только после commit.\n */\nexport type IndexedDbAtomicUpdateDecision<TStorageValue, TResult> =\n\t| { readonly action: \"keep\"; readonly result: TResult }\n\t| { readonly action: \"set\"; readonly value: TStorageValue; readonly result: TResult }\n\t| { readonly action: \"remove\"; readonly result: TResult };\n\nexport type IndexedDbQueryStorage<TStorageValue> = AsyncStorage<TStorageValue> & {\n\t/**\n\t * Читает и изменяет одну запись в общей readwrite-транзакции, поэтому параллельные\n\t * вкладки не могут потерять уже зафиксированное изменение между get и set.\n\t * Callback должен быть синхронным: IndexedDB закрывает неактивную транзакцию до\n\t * завершения произвольной асинхронной работы.\n\t */\n\treadonly updateItem: <TResult>(\n\t\tkey: string,\n\t\tupdate: (value: TStorageValue | undefined) => IndexedDbAtomicUpdateDecision<TStorageValue, TResult>\n\t) => Promise<TResult>;\n};\n\nfunction resolveIndexedDbFactory(factory?: IDBFactory) {\n\tif (factory) return factory;\n\tif (typeof globalThis.indexedDB === \"undefined\") return undefined;\n\treturn globalThis.indexedDB;\n}\n\nfunction requestToPromise<T>(request: IDBRequest<T>) {\n\treturn new Promise<T>((resolve, reject) => {\n\t\trequest.onsuccess = () => resolve(request.result);\n\t\trequest.onerror = () => reject(request.error);\n\t});\n}\n\nfunction getSystemIdentifier() {\n\tif (__DEV__) {\n\t\treturn `dev:${__APP_ID__}`;\n\t}\n\n\tconst projectIdentifier = getQueryPersistenceProjectAdapter().resolveSystemIdentifier?.();\n\tif (projectIdentifier) return projectIdentifier;\n\n\tconst hostname = typeof window === \"undefined\" ? \"server\" : window.location.hostname;\n\treturn `${__APP_ID__}:${hostname}`;\n}\n\nfunction getPersistencePrefix() {\n\tconst system = getSystemIdentifier();\n\treturn `${system}`;\n}\n\nexport function shouldPersistQuery(query: Pick<Query, \"meta\">) {\n\treturn query.meta?.persist === true && query.meta.sessionScoped !== true;\n}\n\nexport function createIndexedDbQueryStorage<TStorageValue = unknown>(\n\toptions: IndexedDbQueryStorageOptions = {}\n): IndexedDbQueryStorage<TStorageValue> | undefined {\n\tconst factory = resolveIndexedDbFactory(options.indexedDB);\n\tif (!factory) return undefined;\n\n\tconst idb = factory;\n\tconst dbName = options.dbName ?? getSystemIdentifier();\n\tconst storeName = options.storeName ?? STORE_NAME;\n\tlet dbPromise: Promise<IDBDatabase> | null = null;\n\n\tfunction openDb() {\n\t\tif (dbPromise) return dbPromise;\n\n\t\tdbPromise = new Promise<IDBDatabase>((resolve, reject) => {\n\t\t\tconst request = idb.open(dbName, 1);\n\n\t\t\trequest.onupgradeneeded = () => {\n\t\t\t\tconst db = request.result;\n\t\t\t\tif (!db.objectStoreNames.contains(storeName)) {\n\t\t\t\t\tdb.createObjectStore(storeName);\n\t\t\t\t}\n\t\t\t};\n\n\t\t\trequest.onsuccess = () => resolve(request.result);\n\t\t\trequest.onerror = () => reject(request.error);\n\t\t\trequest.onblocked = () => reject(request.error ?? new Error(\"Открытие IndexedDB заблокировано\"));\n\t\t});\n\n\t\treturn dbPromise;\n\t}\n\n\tasync function runTransaction<T>(mode: IDBTransactionMode, action: (store: IDBObjectStore) => IDBRequest<T>) {\n\t\tconst db = await openDb();\n\t\tconst tx = db.transaction(storeName, mode);\n\t\tconst store = tx.objectStore(storeName);\n\t\tconst request = action(store);\n\t\treturn requestToPromise(request);\n\t}\n\n\tasync function runWrite(mode: IDBTransactionMode, action: (store: IDBObjectStore) => void) {\n\t\tconst db = await openDb();\n\n\t\treturn new Promise<void>((resolve, reject) => {\n\t\t\tconst tx = db.transaction(storeName, mode);\n\t\t\tconst store = tx.objectStore(storeName);\n\n\t\t\ttx.oncomplete = () => resolve();\n\t\t\ttx.onerror = () => reject(tx.error);\n\t\t\ttx.onabort = () => reject(tx.error);\n\n\t\t\ttry {\n\t\t\t\taction(store);\n\t\t\t} catch (error) {\n\t\t\t\ttx.abort();\n\t\t\t\treject(error);\n\t\t\t}\n\t\t});\n\t}\n\n\tasync function updateItem<TResult>(\n\t\tkey: string,\n\t\tupdate: (value: TStorageValue | undefined) => IndexedDbAtomicUpdateDecision<TStorageValue, TResult>\n\t) {\n\t\tconst db = await openDb();\n\t\treturn new Promise<TResult>((resolve, reject) => {\n\t\t\tconst tx = db.transaction(storeName, \"readwrite\");\n\t\t\tconst store = tx.objectStore(storeName);\n\t\t\tconst request = store.get(key);\n\t\t\tlet result: TResult;\n\t\t\tlet hasResult = false;\n\n\t\t\ttx.oncomplete = () => {\n\t\t\t\tif (hasResult) resolve(result);\n\t\t\t\telse reject(new Error(\"IndexedDB-транзакция завершилась без результата обновления\"));\n\t\t\t};\n\t\t\ttx.onerror = () => reject(tx.error);\n\t\t\ttx.onabort = () => reject(tx.error);\n\t\t\trequest.onerror = () => {\n\t\t\t\ttx.abort();\n\t\t\t\treject(request.error);\n\t\t\t};\n\t\t\trequest.onsuccess = () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst decision = update(request.result as TStorageValue | undefined);\n\t\t\t\t\tresult = decision.result;\n\t\t\t\t\thasResult = true;\n\t\t\t\t\tif (decision.action === \"set\") store.put(decision.value, key);\n\t\t\t\t\telse if (decision.action === \"remove\") store.delete(key);\n\t\t\t\t} catch (error) {\n\t\t\t\t\ttx.abort();\n\t\t\t\t\treject(error);\n\t\t\t\t}\n\t\t\t};\n\t\t});\n\t}\n\n\treturn {\n\t\tgetItem: (key) => runTransaction<TStorageValue | undefined>(\"readonly\", (store) => store.get(key)),\n\t\tsetItem: (key, value) => runWrite(\"readwrite\", (store) => store.put(value, key)),\n\t\tremoveItem: (key) => runWrite(\"readwrite\", (store) => store.delete(key)),\n\t\tupdateItem,\n\t\tentries: async () => {\n\t\t\tconst db = await openDb();\n\n\t\t\treturn new Promise<Array<[key: string, value: TStorageValue]>>((resolve, reject) => {\n\t\t\t\tconst tx = db.transaction(storeName, \"readonly\");\n\t\t\t\tconst store = tx.objectStore(storeName);\n\t\t\t\tconst request = store.openCursor();\n\t\t\t\tconst entries: Array<[key: string, value: TStorageValue]> = [];\n\n\t\t\t\ttx.oncomplete = () => resolve(entries);\n\t\t\t\ttx.onerror = () => reject(tx.error);\n\t\t\t\ttx.onabort = () => reject(tx.error);\n\n\t\t\t\trequest.onsuccess = () => {\n\t\t\t\t\tconst cursor = request.result;\n\t\t\t\t\tif (!cursor) return;\n\n\t\t\t\t\tentries.push([String(cursor.key), cursor.value as TStorageValue]);\n\t\t\t\t\tcursor.continue();\n\t\t\t\t};\n\t\t\t\trequest.onerror = () => reject(request.error);\n\t\t\t});\n\t\t}\n\t};\n}\n\nexport function createReactQueryPersister() {\n\tconst storage = createIndexedDbQueryStorage<PersistedQuery>();\n\tif (!storage) return undefined;\n\n\treturn experimental_createQueryPersister<PersistedQuery>({\n\t\tstorage,\n\t\tserialize: (query) => query,\n\t\tdeserialize: (query) => query,\n\t\tprefix: getPersistencePrefix(),\n\t\tbuster: getQueryPersistenceProjectAdapter().persistenceBuster ?? REACT_QUERY_PERSISTENCE_BUSTER,\n\t\tmaxAge: REACT_QUERY_PERSISTENCE_MAX_AGE,\n\t\trefetchOnRestore: true,\n\t\tfilters: { predicate: shouldPersistQuery }\n\t});\n}\n","import { type QueryMeta } from \"@tanstack/react-query\";\n\n/**\n * Общие признаки query, которые управляют инфраструктурой клиентского кеша.\n *\n * `persist` разрешает долговременное хранение публичных справочных данных,\n * а `sessionScoped` помечает данные, доступность или содержимое которых зависит\n * от текущей серверной сессии. Эти признаки нельзя совмещать: session-scoped\n * query никогда не должна переживать смену пользователя или роли.\n */\nexport type AppQueryMeta = {\n\treadonly persist?: boolean;\n\treadonly sessionScoped?: boolean;\n} & Record<string, unknown>;\n\ndeclare module \"@tanstack/react-query\" {\n\tinterface Register {\n\t\tqueryMeta: AppQueryMeta;\n\t}\n}\n\n/** Разрешает сохранять публичную query в долговременном хранилище. */\nexport const persistedQueryMeta = { persist: true } as const satisfies QueryMeta;\n\n/** Помечает query для сброса при изменении серверной сессии. */\nexport const sessionScopedQueryMeta = { sessionScoped: true } as const satisfies QueryMeta;\n","import {\n\tDefaultError,\n\tMutation,\n\tMutationCache,\n\tMutationFunctionContext,\n\tQuery,\n\tQueryCache,\n\tQueryClient,\n\tQueryKey\n} from \"@tanstack/react-query\";\n\nimport { createReactQueryPersister } from \"./persistence\";\n\nexport type QueryBroadcastOptions = {\n\tonQueryError?: (error: DefaultError, query: Query<unknown, unknown, unknown>) => void;\n\tonMutationError?: (\n\t\terror: DefaultError,\n\t\tvariables: unknown,\n\t\tonMutateResult: unknown,\n\t\tmutation: Mutation<unknown, unknown, unknown>,\n\t\tcontext: MutationFunctionContext\n\t) => Promise<unknown> | unknown;\n};\n\n/**\n * Создаёт настроенный QueryClient. Singleton lifecycle обеспечивает host-приложение.\n */\nexport function createQueryClient({ onQueryError, onMutationError }: QueryBroadcastOptions) {\n\tconst queryPersister = createReactQueryPersister();\n\n\tconst queryClient = new QueryClient({\n\t\tqueryCache: new QueryCache({ onError: onQueryError }),\n\t\tmutationCache: new MutationCache({ onError: onMutationError }),\n\t\tdefaultOptions: {\n\t\t\t// TODO: Vite env\n\t\t\tqueries: {\n\t\t\t\tstaleTime: 10 * 60 * 1000,\n\t\t\t\tgcTime: 30 * 60 * 1000,\n\t\t\t\tthrowOnError: false,\n\t\t\t\tretry: false,\n\t\t\t\t...(queryPersister ? { persister: queryPersister.persisterFn } : {})\n\t\t\t},\n\t\t\tmutations: {\n\t\t\t\tthrowOnError: false,\n\t\t\t\tretry: false\n\t\t\t}\n\t\t}\n\t});\n\n\treturn queryClient;\n}\n\n/**\n * Контекст, возвращаемый из `onMutateOptimistic`.\n * Содержит снапшот кеша до оптимистичного обновления.\n * Передаётся в `onError`, `onSuccess`, `onSettled` через TanStack Query.\n */\nexport type OptimisticContext<TQueryCacheData> = {\n\tprevious: TQueryCacheData | undefined;\n};\n\n/**\n * _Оптимистичное обновление — это когда мы моментально меняем локальный кэш (и UI) до ответа сервера,\n * чтобы интерфейс был отзывчивым. Если сервер вернул ошибку — откатываем изменения.\n * Если сервер подтвердил — либо принимаем ответ сервера,\n * либо оставляем оптимистичное состояние (в зависимости от ситуации)._\n *\n * Обработчик `onMutate` для оптимистичного обновления.\n *\n * Выполняет три шага:\n * 1. Отменяет активные запросы по ключу — предотвращает гонку с сервером.\n * 2. Сохраняет снапшот текущего состояния кеша.\n * 3. Применяет оптимистичный патч через `patcher`.\n *\n * Возвращает {@link OptimisticContext} — передаётся в `onError` для отката.\n *\n * @example\n * // Обновление точечных данных в списке объектов\n * useMutation({\n * mutationFn: (data: SomeType) => { ... },\n * onMutate: async (data) =>\n * onMutateOptimistic(queryClient, someQueryKey, (old) => {\n * if (!old) return old;\n * return old.map((item) =>\n * item.id === data.id ? { ...item, visible: data.visible } : item\n * );\n * }),\n * onError: (_err, _vars, context) =>\n * onErrorOptimistic(queryClient, someQueryKey, context),\n * onSettled: () =>\n * onSettledOptimistic(queryClient, someQueryKey),\n * })\n */\nexport async function onMutateOptimistic<TQueryCacheData>(\n\tqc: QueryClient,\n\tqueryKey: QueryKey,\n\tpatcher: (oldCache: TQueryCacheData | undefined) => TQueryCacheData | undefined\n): Promise<OptimisticContext<TQueryCacheData>> {\n\t// 1) Отменяем запросы по ключу, чтобы не было гонки с сервером\n\tawait qc.cancelQueries({ queryKey });\n\n\t// 2) Сохраняем снапшот кеша для возможного отката\n\tconst previous = qc.getQueryData<TQueryCacheData>(queryKey);\n\n\t// 3) Применяем оптимистичный патч\n\tqc.setQueryData<TQueryCacheData>(queryKey, patcher);\n\n\treturn { previous };\n}\n\n/**\n * Обработчик `onError` для оптимистичного обновления.\n *\n * Откатывает кеш к снапшоту, сохранённому в {@link OptimisticContext}.\n * Если `context` не определён (например, `onMutate` сам выбросил ошибку) — безопасно ничего не делает.\n *\n * @example\n * onError: (_err, _vars, context) =>\n * onErrorOptimistic(queryClient, someQueryKey, context),\n */\nexport function onErrorOptimistic<TQueryCacheData>(\n\tqc: QueryClient,\n\tqueryKey: QueryKey,\n\tcontext: OptimisticContext<TQueryCacheData> | undefined\n): void {\n\tif (context?.previous !== undefined) {\n\t\tqc.setQueryData<TQueryCacheData>(queryKey, context.previous);\n\t}\n}\n\n/**\n * Обработчик `onSuccess` для оптимистичного обновления — применяет данные сервера в кеш.\n *\n * Используется, когда сервер вернул авторитетные данные, которые нужно слить\n * с текущим состоянием кеша (или заменить его целиком).\n *\n * Если сервер вернул `void` — эту функцию вызывать не нужно:\n * оптимистичное состояние в кеше остаётся без изменений.\n *\n * @param qc - экземпляр `QueryClient`\n * @param queryKey - ключ кеша\n * @param serverData - данные, вернувшиеся от сервера\n * @param merger - функция слияния: получает текущий кеш и ответ сервера, возвращает новое состояние\n *\n * @example\n * // Полная замена кеша ответом сервера\n * onSuccess: (data) =>\n * onSuccessOptimistic(queryClient, someQueryKey, data, (_cache, serverData) => serverData),\n *\n * @example\n * // Точечное обновление одного элемента в списке\n * onSuccess: (updatedItem) =>\n * onSuccessOptimistic(queryClient, someQueryKey, updatedItem,\n * (cache, item) => cache?.map((i) => (i.id === item.id ? item : i))\n * ),\n */\nexport function onSuccessOptimistic<TQueryCacheData, TServerData>(\n\tqc: QueryClient,\n\tqueryKey: QueryKey,\n\tserverData: TServerData,\n\tmerger: (cache: TQueryCacheData | undefined, serverData: TServerData) => TQueryCacheData | undefined\n): void {\n\tqc.setQueryData<TQueryCacheData>(queryKey, (old) => merger(old, serverData));\n}\n\n/**\n * Параметры для {@link onSettledOptimistic}.\n */\nexport type OnSettledOptimisticOptions = {\n\t/**\n\t * Тип рефетча при инвалидации.\n\t * - `\"none\"` (по умолчанию) — помечает кеш устаревшим, но не запускает рефетч немедленно.\n\t * - `\"active\"` — немедленно перезапрашивает активные подписчики.\n\t * - `\"inactive\"` — перезапрашивает неактивные (фоновые) запросы.\n\t * - `\"all\"` — перезапрашивает все подходящие запросы.\n\t */\n\trefetchType?: \"none\" | \"active\" | \"inactive\" | \"all\";\n};\n\n/**\n * Обработчик `onSettled` для оптимистичного обновления.\n *\n * Инвалидирует один или несколько query-ключей после завершения мутации\n * (выполняется и при успехе, и при ошибке).\n *\n * По умолчанию использует `refetchType: \"none\"` — помечает кеш устаревшим,\n * но не запускает немедленный рефетч. Это безопасный вариант при оптимистике:\n * данные в кеше уже актуальны (подтверждены или откатаны), а рефетч произойдёт\n * при следующем монтировании или явном запросе.\n *\n * Broadcast в другие вкладки — ответственность вызывающего кода через `broadcastCacheEvent`.\n *\n * @example\n * onSettled: () =>\n * onSettledOptimistic(queryClient, someQueryKey),\n *\n * @example\n * // С принудительным рефетчем активных подписчиков\n * onSettled: () =>\n * onSettledOptimistic(queryClient, [queryKeyA, queryKeyB], { refetchType: \"active\" }),\n */\nexport async function onSettledOptimistic(\n\tqc: QueryClient,\n\tqueryKeys: QueryKey | QueryKey[],\n\toptions?: OnSettledOptimisticOptions\n): Promise<void> {\n\tconst refetchType = options?.refetchType ?? \"none\";\n\t// Нормализуем одиночный ключ к массиву для единообразной обработки\n\tconst keys: QueryKey[] = isNestedQueryKey(queryKeys) ? queryKeys : [queryKeys];\n\n\tawait Promise.all(keys.map((queryKey) => qc.invalidateQueries({ queryKey, refetchType })));\n}\n\n/**\n * Определяет, является ли значение массивом query-ключей (QueryKey[]),\n * а не одиночным QueryKey.\n *\n * QueryKey в TanStack Query — это `string | readonly unknown[]`.\n * Чтобы отличить `QueryKey[]` от `QueryKey` типа `readonly unknown[]`,\n * проверяем, содержит ли массив хотя бы один вложенный массив или строку на верхнем уровне.\n */\nfunction isNestedQueryKey(value: QueryKey | QueryKey[]): value is QueryKey[] {\n\tif (!Array.isArray(value)) return false;\n\t// Если первый элемент сам является массивом — это QueryKey[]\n\treturn Array.isArray(value[0]);\n}\n","import { type Query, type QueryClient } from \"@tanstack/react-query\";\n\nconst SESSION_SCOPED_QUERY_CACHE_RESET_EVENT = \"session-scoped-query-cache-reset\";\n\ntype SessionScopedQueryCacheResetEvent = {\n\treadonly type: typeof SESSION_SCOPED_QUERY_CACHE_RESET_EVENT;\n};\n\nexport type ResetSessionScopedQueriesOptions = {\n\t/**\n\t * Передавать ли команду сброса другим вкладкам. При обработке входящего\n\t * сообщения значение отключается, чтобы вкладки не создавали цикл событий.\n\t */\n\treadonly broadcast?: boolean;\n};\n\nexport type InstallSessionScopedQueryResetOptions = {\n\t/** Уникальное для приложения имя служебного BroadcastChannel. */\n\treadonly channelName: string;\n};\n\nexport type InstalledSessionScopedQueryReset = {\n\t/** Освобождает канал и прекращает межвкладочную синхронизацию. */\n\treadonly cleanup: () => void;\n};\n\nlet broadcastSessionScopedQueryReset: (() => void) | undefined;\n\n/** Проверяет, относится ли query к данным текущей серверной сессии. */\nexport function isSessionScopedQuery(query: Pick<Query, \"meta\">): boolean {\n\treturn query.meta?.sessionScoped === true;\n}\n\n/**\n * Сбрасывает все session-scoped query после входа, выхода, обновления сессии\n * или потери авторизации.\n *\n * Сначала отменяются незавершённые запросы старой сессии, затем TanStack Query\n * очищает их состояние и повторно запрашивает только активные query. Обычные\n * публичные данные при этом остаются в кеше. По умолчанию отдельная команда\n * сброса отправляется другим вкладкам без передачи содержимого кеша или токенов.\n */\nexport async function resetSessionScopedQueries(queryClient: QueryClient, options: ResetSessionScopedQueriesOptions = {}): Promise<void> {\n\tconst filters = { predicate: isSessionScopedQuery } as const;\n\n\tawait queryClient.cancelQueries(filters);\n\n\tconst matchedQueries = queryClient.getQueryCache().findAll(filters);\n\tfor (const query of matchedQueries) {\n\t\tif (query.getObserversCount() === 0) {\n\t\t\tqueryClient.getQueryCache().remove(query);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Hydrated query хранит SSR-данные как initial state, поэтому одного\n\t\t// query.reset() недостаточно: старая сессия могла бы вернуться в UI.\n\t\tquery.reset();\n\t\tquery.setState({ data: undefined, dataUpdatedAt: 0 });\n\t}\n\n\tawait queryClient.refetchQueries(\n\t\t{\n\t\t\tpredicate: (query) => isSessionScopedQuery(query) && query.getObserversCount() > 0,\n\t\t\ttype: \"active\"\n\t\t},\n\t\t{ cancelRefetch: true }\n\t);\n\n\tif (options.broadcast !== false) {\n\t\tbroadcastSessionScopedQueryReset?.();\n\t}\n}\n\n/**\n * Подключает безопасную межвкладочную синхронизацию смены сессии.\n *\n * Канал передаёт только команду сброса. В отличие от полной синхронизации\n * Query cache, он не публикует данные query и потому подходит для auth-зависимых\n * ответов. На сервере и в браузерах без BroadcastChannel возвращается no-op.\n */\nexport function installSessionScopedQueryReset(\n\tqueryClient: QueryClient,\n\toptions: InstallSessionScopedQueryResetOptions\n): InstalledSessionScopedQueryReset {\n\tif (typeof window === \"undefined\" || typeof BroadcastChannel === \"undefined\") {\n\t\treturn { cleanup: () => undefined };\n\t}\n\n\tconst channel = new BroadcastChannel(options.channelName);\n\tconst broadcast = () => {\n\t\tconst event: SessionScopedQueryCacheResetEvent = {\n\t\t\ttype: SESSION_SCOPED_QUERY_CACHE_RESET_EVENT\n\t\t};\n\t\tchannel.postMessage(event);\n\t};\n\n\tbroadcastSessionScopedQueryReset = broadcast;\n\tchannel.onmessage = (event: MessageEvent<unknown>) => {\n\t\tif (!isSessionScopedQueryCacheResetEvent(event.data)) return;\n\t\tvoid resetSessionScopedQueries(queryClient, { broadcast: false });\n\t};\n\n\treturn {\n\t\tcleanup: () => {\n\t\t\tchannel.onmessage = null;\n\t\t\tchannel.close();\n\t\t\tif (broadcastSessionScopedQueryReset === broadcast) {\n\t\t\t\tbroadcastSessionScopedQueryReset = undefined;\n\t\t\t}\n\t\t}\n\t};\n}\n\n/** Защищает обработчик канала от посторонних или устаревших сообщений. */\nfunction isSessionScopedQueryCacheResetEvent(value: unknown): value is SessionScopedQueryCacheResetEvent {\n\treturn typeof value === \"object\" && value !== null && \"type\" in value && value.type === SESSION_SCOPED_QUERY_CACHE_RESET_EVENT;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { QueryClient, QueryKey } from "@tanstack/react-query";
|
|
2
|
+
export type QueryInvalidationBroadcastOptions = {
|
|
3
|
+
/** Уникальное в пределах origin имя канала приложения. */
|
|
4
|
+
readonly channelName: string;
|
|
5
|
+
/** Ограничивает как исходящие, так и входящие Query keys. */
|
|
6
|
+
readonly matchesQuery: (queryKey: QueryKey) => boolean;
|
|
7
|
+
/** Определяет, какие queries получающей вкладки будут перезапрошены сразу. */
|
|
8
|
+
readonly refetchType?: "none" | "active" | "inactive" | "all";
|
|
9
|
+
};
|
|
10
|
+
export type InstalledQueryInvalidationBroadcast = {
|
|
11
|
+
/** Явно передаёт точечную инвалидацию без изменения локального QueryClient. */
|
|
12
|
+
readonly broadcast: (queryKey: QueryKey) => void;
|
|
13
|
+
readonly cleanup: () => void;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Передаёт между вкладками только факт инвалидации выбранного Query key.
|
|
17
|
+
* Server data остаётся у QueryClient каждой вкладки и заново читается её queryFn.
|
|
18
|
+
*/
|
|
19
|
+
export declare function installQueryInvalidationBroadcast(queryClient: QueryClient, options: QueryInvalidationBroadcastOptions): InstalledQueryInvalidationBroadcast;
|
|
20
|
+
//# sourceMappingURL=invalidationBroadcast.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invalidationBroadcast.d.ts","sourceRoot":"","sources":["../../src/query-client/invalidationBroadcast.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEnE,MAAM,MAAM,iCAAiC,GAAG;IAC/C,0DAA0D;IAC1D,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,6DAA6D;IAC7D,QAAQ,CAAC,YAAY,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,OAAO,CAAC;IACvD,8EAA8E;IAC9E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,UAAU,GAAG,KAAK,CAAC;CAC9D,CAAC;AAEF,MAAM,MAAM,mCAAmC,GAAG;IACjD,+EAA+E;IAC/E,QAAQ,CAAC,SAAS,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,IAAI,CAAC;IACjD,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC;CAC7B,CAAC;AAYF;;;GAGG;AACH,wBAAgB,iCAAiC,CAChD,WAAW,EAAE,WAAW,EACxB,OAAO,EAAE,iCAAiC,GACxC,mCAAmC,CA8CrC"}
|
package/package.json
CHANGED
package/src/hooks/README.mdx
CHANGED
|
@@ -147,7 +147,7 @@ function Toolbar() {
|
|
|
147
147
|
| Значение после паузы во вводе | `useDebounce` |
|
|
148
148
|
| Callback после паузы от последнего события | `useDebouncedCallback` |
|
|
149
149
|
| Ограничение частоты с leading и trailing вызовами | `useThrottledCallback` |
|
|
150
|
-
| Pointer и Keyboard sensors DnD Kit
|
|
150
|
+
| Pointer/Mouse, Touch и Keyboard sensors DnD Kit | `useDndSortableSensors` |
|
|
151
151
|
| Собственный доступный floating listbox | `useFloatingListbox` |
|
|
152
152
|
| Чтение и replace-navigation search state | `useSearchParams` |
|
|
153
153
|
| Построение `href` без navigation | `useSearchLink` |
|
|
@@ -397,9 +397,17 @@ Hook создаёт стандартный набор DnD Kit sensors:
|
|
|
397
397
|
- `PointerSensor` с activation distance;
|
|
398
398
|
- `KeyboardSensor` с `sortableKeyboardCoordinates`.
|
|
399
399
|
|
|
400
|
+
Опциональная `touchActivation` заменяет общий pointer-путь на отдельные
|
|
401
|
+
`MouseSensor` и `TouchSensor`: mouse сохраняет distance, а touch ждёт long press
|
|
402
|
+
и отменяет DnD при движении больше tolerance, оставляя нативную прокрутку доступной.
|
|
403
|
+
Предметная матрица также может передать собственные keyboard coordinates/codes.
|
|
404
|
+
|
|
400
405
|
```ts
|
|
401
406
|
interface UseDndSortableSensorsOptions {
|
|
402
407
|
activationDistance?: number;
|
|
408
|
+
touchActivation?: { delay: number; tolerance?: number };
|
|
409
|
+
keyboardCoordinateGetter?: KeyboardCoordinateGetter;
|
|
410
|
+
keyboardCodes?: KeyboardCodes;
|
|
403
411
|
}
|
|
404
412
|
|
|
405
413
|
function useDndSortableSensors(distanceOrOptions?: number | UseDndSortableSensorsOptions): SensorDescriptor<SensorOptions>[];
|
|
@@ -414,7 +422,8 @@ import { useDndSortableSensors } from "@ryuzaki13/react-foundation-lib/hooks";
|
|
|
414
422
|
|
|
415
423
|
function SortableArea({ children }: { children: React.ReactNode }) {
|
|
416
424
|
const sensors = useDndSortableSensors({
|
|
417
|
-
activationDistance:
|
|
425
|
+
activationDistance: 10,
|
|
426
|
+
touchActivation: { delay: 250, tolerance: 10 }
|
|
418
427
|
});
|
|
419
428
|
|
|
420
429
|
return <DndContext sensors={sensors}>{children}</DndContext>;
|
|
@@ -952,24 +961,24 @@ Repository test покрывает auto placement, update после измен
|
|
|
952
961
|
|
|
953
962
|
## API-справочник
|
|
954
963
|
|
|
955
|
-
| Экспорт | Contract
|
|
956
|
-
| ---------------------------------- |
|
|
957
|
-
| `useBlockBrowserZoom` | Global `Ctrl + wheel` prevention.
|
|
958
|
-
| `useDebounce` | Debounced value.
|
|
959
|
-
| `useDebouncedCallback` | Trailing debounce.
|
|
960
|
-
| `useDndSortableSensors` | Pointer/Keyboard sensors.
|
|
961
|
-
| `useFloatingListbox` | Headless listbox.
|
|
962
|
-
| `useLazyComponent` | Memoized lazy factory.
|
|
963
|
-
| `useReferenceTrace` | Reference diagnostics.
|
|
964
|
-
| `useSearchLink` | Link builder.
|
|
965
|
-
| `useSearchParams` | Search read/replace navigation.
|
|
966
|
-
| `useThrottledCallback` | Leading/trailing throttle.
|
|
967
|
-
| `FloatingListboxPlacementStrategy` | `"flip" \| "auto"`.
|
|
968
|
-
| `FloatingListboxSizeContext` | Геометрия popup/reference/viewport.
|
|
969
|
-
| `FloatingListboxSizeResolver` | Context → style.
|
|
970
|
-
| `FloatingListboxSizeStyle` | Size fields и CSS properties.
|
|
971
|
-
| `ThrottledCallback` | `call/flush/cancel`.
|
|
972
|
-
| `UseDndSortableSensorsOptions` | `activationDistance`.
|
|
964
|
+
| Экспорт | Contract |
|
|
965
|
+
| ---------------------------------- | ------------------------------------- |
|
|
966
|
+
| `useBlockBrowserZoom` | Global `Ctrl + wheel` prevention. |
|
|
967
|
+
| `useDebounce` | Debounced value. |
|
|
968
|
+
| `useDebouncedCallback` | Trailing debounce. |
|
|
969
|
+
| `useDndSortableSensors` | Pointer/Mouse/Touch/Keyboard sensors. |
|
|
970
|
+
| `useFloatingListbox` | Headless listbox. |
|
|
971
|
+
| `useLazyComponent` | Memoized lazy factory. |
|
|
972
|
+
| `useReferenceTrace` | Reference diagnostics. |
|
|
973
|
+
| `useSearchLink` | Link builder. |
|
|
974
|
+
| `useSearchParams` | Search read/replace navigation. |
|
|
975
|
+
| `useThrottledCallback` | Leading/trailing throttle. |
|
|
976
|
+
| `FloatingListboxPlacementStrategy` | `"flip" \| "auto"`. |
|
|
977
|
+
| `FloatingListboxSizeContext` | Геометрия popup/reference/viewport. |
|
|
978
|
+
| `FloatingListboxSizeResolver` | Context → style. |
|
|
979
|
+
| `FloatingListboxSizeStyle` | Size fields и CSS properties. |
|
|
980
|
+
| `ThrottledCallback` | `call/flush/cancel`. |
|
|
981
|
+
| `UseDndSortableSensorsOptions` | `activationDistance`. |
|
|
973
982
|
|
|
974
983
|
## Что модуль не делает
|
|
975
984
|
|