@develemit/web-hooks 0.4.0 → 0.5.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/CHANGELOG.md +6 -0
- package/README.md +63 -0
- package/dist/{chunk-TE2FRIPF.js → chunk-IVLRQ23V.js} +2 -135
- package/dist/chunk-JTLQQSLO.js +143 -0
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +5 -3
- package/dist/log-helpers-rkbgYqZW.d.cts +25 -0
- package/dist/log-helpers-rkbgYqZW.d.ts +25 -0
- package/dist/terminal-output.d.cts +2 -25
- package/dist/terminal-output.d.ts +2 -25
- package/dist/terminal-output.js +5 -3
- package/dist/virtual-list.cjs +499 -0
- package/dist/virtual-list.d.cts +116 -0
- package/dist/virtual-list.d.ts +116 -0
- package/dist/virtual-list.js +287 -0
- package/package.json +14 -2
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/virtual-list/index.ts
|
|
22
|
+
var virtual_list_exports = {};
|
|
23
|
+
__export(virtual_list_exports, {
|
|
24
|
+
MAX_REALIGN_FRAMES: () => MAX_REALIGN_FRAMES,
|
|
25
|
+
downloadText: () => downloadText,
|
|
26
|
+
initialVisibleCount: () => initialVisibleCount,
|
|
27
|
+
logToText: () => logToText,
|
|
28
|
+
measuredOrEstimate: () => measuredOrEstimate,
|
|
29
|
+
shouldRealign: () => shouldRealign,
|
|
30
|
+
targetOffset: () => targetOffset,
|
|
31
|
+
useVirtualList: () => useVirtualList,
|
|
32
|
+
useVirtualLog: () => useVirtualLog
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(virtual_list_exports);
|
|
35
|
+
|
|
36
|
+
// src/virtual-list/use-virtual-list.ts
|
|
37
|
+
var import_react = require("react");
|
|
38
|
+
var import_react_virtual = require("@tanstack/react-virtual");
|
|
39
|
+
|
|
40
|
+
// src/virtual-list/measure.ts
|
|
41
|
+
var MAX_REALIGN_FRAMES = 3;
|
|
42
|
+
function measuredOrEstimate(measured, estimate) {
|
|
43
|
+
return measured !== void 0 && measured > 0 ? measured : estimate;
|
|
44
|
+
}
|
|
45
|
+
function targetOffset(align, itemStart, itemSize, viewportSize) {
|
|
46
|
+
if (align === "end") return itemStart + itemSize - viewportSize;
|
|
47
|
+
if (align === "center") return itemStart + itemSize / 2 - viewportSize / 2;
|
|
48
|
+
return itemStart;
|
|
49
|
+
}
|
|
50
|
+
function shouldRealign({ align, itemStart, itemSize, scrollOffset, viewportSize, frame, maxFrames = MAX_REALIGN_FRAMES }) {
|
|
51
|
+
if (frame >= maxFrames) return false;
|
|
52
|
+
if (align === "auto") {
|
|
53
|
+
return itemStart < scrollOffset || itemStart + itemSize > scrollOffset + viewportSize;
|
|
54
|
+
}
|
|
55
|
+
const want = Math.max(0, targetOffset(align, itemStart, itemSize, viewportSize));
|
|
56
|
+
return Math.abs(scrollOffset - want) > 1;
|
|
57
|
+
}
|
|
58
|
+
function initialVisibleCount(count, initialCount) {
|
|
59
|
+
return Math.min(count, Math.max(0, initialCount));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/virtual-list/use-virtual-list.ts
|
|
63
|
+
var ESTIMATED_ROW_FOR_SSR = 30;
|
|
64
|
+
function useVirtualList(options) {
|
|
65
|
+
const {
|
|
66
|
+
count,
|
|
67
|
+
estimateSize,
|
|
68
|
+
scrollRef,
|
|
69
|
+
windowScroll: scrollsWindow = false,
|
|
70
|
+
scrollMargin = 0,
|
|
71
|
+
overscan = 10,
|
|
72
|
+
measure = true,
|
|
73
|
+
getItemKey,
|
|
74
|
+
initialCount = 30
|
|
75
|
+
} = options;
|
|
76
|
+
const frameRef = (0, import_react.useRef)(null);
|
|
77
|
+
const virtualizer = (0, import_react_virtual.useVirtualizer)({
|
|
78
|
+
count,
|
|
79
|
+
estimateSize,
|
|
80
|
+
overscan,
|
|
81
|
+
scrollMargin,
|
|
82
|
+
getScrollElement: () => scrollsWindow ? typeof window === "undefined" ? null : window : scrollRef?.current ?? null,
|
|
83
|
+
...scrollsWindow ? {
|
|
84
|
+
observeElementRect: import_react_virtual.observeWindowRect,
|
|
85
|
+
observeElementOffset: import_react_virtual.observeWindowOffset,
|
|
86
|
+
scrollToFn: import_react_virtual.windowScroll,
|
|
87
|
+
initialOffset: () => typeof window === "undefined" ? 0 : window.scrollY
|
|
88
|
+
} : {},
|
|
89
|
+
// Seeds a viewport so the server render and first paint include rows.
|
|
90
|
+
initialRect: { width: 0, height: initialCount * ESTIMATED_ROW_FOR_SSR },
|
|
91
|
+
...getItemKey ? { getItemKey } : {},
|
|
92
|
+
...measure ? {
|
|
93
|
+
measureElement: (el, entry, instance) => {
|
|
94
|
+
const height = entry?.borderBoxSize?.[0]?.blockSize ?? el.getBoundingClientRect().height;
|
|
95
|
+
const index = Number(el.dataset.index);
|
|
96
|
+
const estimate = Number.isNaN(index) ? 0 : instance.options.estimateSize(index);
|
|
97
|
+
return measuredOrEstimate(height, estimate);
|
|
98
|
+
}
|
|
99
|
+
} : {}
|
|
100
|
+
});
|
|
101
|
+
const cancelRealign = (0, import_react.useCallback)(() => {
|
|
102
|
+
if (frameRef.current !== null && typeof cancelAnimationFrame === "function") cancelAnimationFrame(frameRef.current);
|
|
103
|
+
frameRef.current = null;
|
|
104
|
+
}, []);
|
|
105
|
+
(0, import_react.useEffect)(() => cancelRealign, [cancelRealign]);
|
|
106
|
+
const scrollToIndex = (0, import_react.useCallback)(
|
|
107
|
+
(index, { align = "auto" } = {}) => {
|
|
108
|
+
cancelRealign();
|
|
109
|
+
const last = Math.max(0, count - 1);
|
|
110
|
+
const target = Math.max(0, Math.min(index, last));
|
|
111
|
+
virtualizer.scrollToIndex(target, { align });
|
|
112
|
+
if (typeof requestAnimationFrame !== "function") return;
|
|
113
|
+
let frame = 0;
|
|
114
|
+
const check = () => {
|
|
115
|
+
frameRef.current = null;
|
|
116
|
+
const item = virtualizer.measurementsCache[target];
|
|
117
|
+
const offset = virtualizer.scrollOffset;
|
|
118
|
+
const viewport = virtualizer.scrollRect?.height;
|
|
119
|
+
if (!item || offset === null || !viewport) return;
|
|
120
|
+
const input = { align, itemStart: item.start - scrollMargin, itemSize: item.size, scrollOffset: offset - scrollMargin, viewportSize: viewport, frame };
|
|
121
|
+
if (!shouldRealign(input)) return;
|
|
122
|
+
frame += 1;
|
|
123
|
+
virtualizer.scrollToIndex(target, { align });
|
|
124
|
+
frameRef.current = requestAnimationFrame(check);
|
|
125
|
+
};
|
|
126
|
+
frameRef.current = requestAnimationFrame(check);
|
|
127
|
+
},
|
|
128
|
+
[virtualizer, count, scrollMargin, cancelRealign]
|
|
129
|
+
);
|
|
130
|
+
const scrollToOffset = (0, import_react.useCallback)((offset) => virtualizer.scrollToOffset(offset), [virtualizer]);
|
|
131
|
+
const rendered = virtualizer.getVirtualItems();
|
|
132
|
+
const items = rendered.map((v) => ({ index: v.index, key: v.key, start: v.start - scrollMargin, size: v.size }));
|
|
133
|
+
const fallback = items.length === 0 && count > 0 ? initialVisibleCount(count, initialCount) : 0;
|
|
134
|
+
if (fallback > 0) {
|
|
135
|
+
let start = 0;
|
|
136
|
+
for (let i = 0; i < fallback; i++) {
|
|
137
|
+
const size = estimateSize(i);
|
|
138
|
+
items.push({ index: i, key: getItemKey ? getItemKey(i) : i, start, size });
|
|
139
|
+
start += size;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
items,
|
|
144
|
+
totalSize: virtualizer.getTotalSize(),
|
|
145
|
+
measureRef: measure ? virtualizer.measureElement : noopRef,
|
|
146
|
+
scrollToIndex,
|
|
147
|
+
scrollToOffset,
|
|
148
|
+
virtualizer
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function noopRef() {
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/virtual-list/use-virtual-log.ts
|
|
155
|
+
var import_react3 = require("react");
|
|
156
|
+
|
|
157
|
+
// src/terminal-output/log-helpers.ts
|
|
158
|
+
var escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
159
|
+
function queryMatcher(query) {
|
|
160
|
+
const q = query.trim();
|
|
161
|
+
return q ? new RegExp(escapeRegExp(q), "giu") : null;
|
|
162
|
+
}
|
|
163
|
+
function filterLines(lines, query) {
|
|
164
|
+
const matcher = queryMatcher(query);
|
|
165
|
+
const indexes = [];
|
|
166
|
+
lines.forEach((line, i) => {
|
|
167
|
+
if (!matcher) indexes.push(i);
|
|
168
|
+
else {
|
|
169
|
+
matcher.lastIndex = 0;
|
|
170
|
+
if (matcher.test(line)) indexes.push(i);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
return indexes;
|
|
174
|
+
}
|
|
175
|
+
var FAILURE_PATTERNS = [
|
|
176
|
+
{ tool: "tsc", kind: "error", pattern: /\berror TS\d+/ },
|
|
177
|
+
{ tool: "pnpm", kind: "error", pattern: /\bERR_PNPM_[A-Z_]+/ },
|
|
178
|
+
{ tool: "npm", kind: "error", pattern: /\bnpm (?:ERR!|error\b)/ },
|
|
179
|
+
{ tool: "nx", kind: "failed tasks", pattern: /targets failed, including|Failed tasks/ },
|
|
180
|
+
{ tool: "nx", kind: "task failed", pattern: /^\s*✖\s+nx run / },
|
|
181
|
+
{ tool: "nx", kind: "failed", pattern: /^\s*✖\s+\d+\/\d+ failed/ },
|
|
182
|
+
{ tool: "vitest", kind: "fail", pattern: /^\s*FAIL\s/ },
|
|
183
|
+
{ tool: "vitest", kind: "test", pattern: /^\s*[×✗]\s/ },
|
|
184
|
+
{ tool: "docker", kind: "error", pattern: /^\s*(?:Error response from daemon|ERROR: failed to solve|failed to solve|pull access denied)/ },
|
|
185
|
+
{ tool: "docker", kind: "exit", pattern: /^\s*Container \S+\s+(?:Error|exited with code [1-9]\d*)/ },
|
|
186
|
+
{ tool: "generic", kind: "error", pattern: /\b\w*Error\b(?: \[\w+\])?:/ }
|
|
187
|
+
];
|
|
188
|
+
var STACK_FRAME = /^\s*at\s/;
|
|
189
|
+
function findFailures(lines, patterns = FAILURE_PATTERNS) {
|
|
190
|
+
const failures = [];
|
|
191
|
+
lines.forEach((line, index) => {
|
|
192
|
+
if (STACK_FRAME.test(line)) return;
|
|
193
|
+
const hit = patterns.find((p) => p.pattern.test(line));
|
|
194
|
+
if (hit) failures.push({ index, tool: hit.tool, kind: hit.kind });
|
|
195
|
+
});
|
|
196
|
+
return failures;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/terminal-output/use-follow-tail.ts
|
|
200
|
+
var import_react2 = require("react");
|
|
201
|
+
|
|
202
|
+
// src/terminal-output/follow-tail.ts
|
|
203
|
+
var DEFAULT_TAIL_THRESHOLD = 24;
|
|
204
|
+
function distanceFromEnd({ scrollTop, scrollHeight, clientHeight }) {
|
|
205
|
+
return scrollHeight - scrollTop - clientHeight;
|
|
206
|
+
}
|
|
207
|
+
function isNearEnd(metrics, threshold = DEFAULT_TAIL_THRESHOLD) {
|
|
208
|
+
return distanceFromEnd(metrics) <= threshold;
|
|
209
|
+
}
|
|
210
|
+
function shouldStick(prevAtBottom, metrics, userInitiated, threshold = DEFAULT_TAIL_THRESHOLD) {
|
|
211
|
+
if (!userInitiated) return prevAtBottom;
|
|
212
|
+
return isNearEnd(metrics, threshold);
|
|
213
|
+
}
|
|
214
|
+
function isUserScrollUp({ kind, delta = 0, lastTarget, scrollTop }, threshold = DEFAULT_TAIL_THRESHOLD) {
|
|
215
|
+
if (kind === "wheel") return delta < 0;
|
|
216
|
+
if (kind === "touch") return delta > 0;
|
|
217
|
+
if (kind === "gutter") return true;
|
|
218
|
+
return lastTarget !== null && scrollTop < lastTarget - threshold;
|
|
219
|
+
}
|
|
220
|
+
function unreadDelta(prevCount, nextCount, attached) {
|
|
221
|
+
if (attached) return 0;
|
|
222
|
+
return Math.max(0, nextCount - prevCount);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/terminal-output/use-follow-tail.ts
|
|
226
|
+
var useIsomorphicLayoutEffect = typeof window === "undefined" ? import_react2.useEffect : import_react2.useLayoutEffect;
|
|
227
|
+
function useFollowTail({
|
|
228
|
+
scrollRef,
|
|
229
|
+
itemCount,
|
|
230
|
+
scrollToEnd,
|
|
231
|
+
resetKey,
|
|
232
|
+
threshold = DEFAULT_TAIL_THRESHOLD
|
|
233
|
+
}) {
|
|
234
|
+
const [atBottom, setAtBottom] = (0, import_react2.useState)(true);
|
|
235
|
+
const [unread, setUnread] = (0, import_react2.useState)(0);
|
|
236
|
+
const attached = (0, import_react2.useRef)(true);
|
|
237
|
+
const programmatic = (0, import_react2.useRef)(false);
|
|
238
|
+
const lastTarget = (0, import_react2.useRef)(null);
|
|
239
|
+
const lastTop = (0, import_react2.useRef)(0);
|
|
240
|
+
const frame = (0, import_react2.useRef)(0);
|
|
241
|
+
const prevCount = (0, import_react2.useRef)(itemCount);
|
|
242
|
+
const prevReset = (0, import_react2.useRef)(resetKey);
|
|
243
|
+
const latest = (0, import_react2.useRef)({ scrollToEnd, threshold });
|
|
244
|
+
latest.current = { scrollToEnd, threshold };
|
|
245
|
+
const attach = (0, import_react2.useCallback)((value) => {
|
|
246
|
+
attached.current = value;
|
|
247
|
+
setAtBottom(value);
|
|
248
|
+
if (value) setUnread(0);
|
|
249
|
+
}, []);
|
|
250
|
+
const followEnd = (0, import_react2.useCallback)(() => {
|
|
251
|
+
const el = scrollRef.current;
|
|
252
|
+
if (!el) return;
|
|
253
|
+
programmatic.current = true;
|
|
254
|
+
if (latest.current.scrollToEnd) latest.current.scrollToEnd();
|
|
255
|
+
else el.scrollTop = el.scrollHeight;
|
|
256
|
+
lastTarget.current = el.scrollHeight - el.clientHeight;
|
|
257
|
+
if (typeof requestAnimationFrame === "function") {
|
|
258
|
+
cancelAnimationFrame(frame.current);
|
|
259
|
+
frame.current = requestAnimationFrame(() => {
|
|
260
|
+
programmatic.current = false;
|
|
261
|
+
});
|
|
262
|
+
} else {
|
|
263
|
+
programmatic.current = false;
|
|
264
|
+
}
|
|
265
|
+
}, [scrollRef]);
|
|
266
|
+
useIsomorphicLayoutEffect(() => {
|
|
267
|
+
const count = prevCount.current;
|
|
268
|
+
prevCount.current = itemCount;
|
|
269
|
+
if (prevReset.current !== resetKey) {
|
|
270
|
+
prevReset.current = resetKey;
|
|
271
|
+
attach(true);
|
|
272
|
+
followEnd();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (attached.current) {
|
|
276
|
+
if (itemCount > 0) followEnd();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const added = unreadDelta(count, itemCount, false);
|
|
280
|
+
if (added > 0) setUnread((n) => n + added);
|
|
281
|
+
}, [itemCount, resetKey, attach, followEnd]);
|
|
282
|
+
(0, import_react2.useEffect)(() => {
|
|
283
|
+
const el = scrollRef.current;
|
|
284
|
+
if (!el) return;
|
|
285
|
+
const takeOver = () => {
|
|
286
|
+
programmatic.current = false;
|
|
287
|
+
lastTarget.current = null;
|
|
288
|
+
if (attached.current) attach(false);
|
|
289
|
+
};
|
|
290
|
+
const onScroll = () => {
|
|
291
|
+
const { threshold: limit } = latest.current;
|
|
292
|
+
const movedUp = el.scrollTop < lastTop.current;
|
|
293
|
+
lastTop.current = el.scrollTop;
|
|
294
|
+
const info = { kind: "scroll", lastTarget: movedUp ? lastTarget.current : null, scrollTop: el.scrollTop };
|
|
295
|
+
const user = isUserScrollUp(info, limit);
|
|
296
|
+
if (user) takeOver();
|
|
297
|
+
const next = shouldStick(attached.current, el, user || !programmatic.current, limit);
|
|
298
|
+
if (next !== attached.current) attach(next);
|
|
299
|
+
};
|
|
300
|
+
const canScrollUp = () => el.scrollHeight > el.clientHeight && el.scrollTop > 0;
|
|
301
|
+
const onWheel = (e) => {
|
|
302
|
+
if (canScrollUp() && isUserScrollUp({ kind: "wheel", delta: e.deltaY, lastTarget: null, scrollTop: el.scrollTop })) takeOver();
|
|
303
|
+
};
|
|
304
|
+
let touchY = 0;
|
|
305
|
+
const onTouchStart = (e) => {
|
|
306
|
+
touchY = e.touches[0]?.clientY ?? 0;
|
|
307
|
+
};
|
|
308
|
+
const onTouchMove = (e) => {
|
|
309
|
+
const delta = (e.touches[0]?.clientY ?? touchY) - touchY;
|
|
310
|
+
if (canScrollUp() && isUserScrollUp({ kind: "touch", delta, lastTarget: null, scrollTop: el.scrollTop })) takeOver();
|
|
311
|
+
};
|
|
312
|
+
const onPointerDown = (e) => {
|
|
313
|
+
const gutter = el.offsetWidth - el.clientWidth;
|
|
314
|
+
if (e.target !== el || gutter <= 0) return;
|
|
315
|
+
if (e.clientX >= el.getBoundingClientRect().left + el.clientWidth) {
|
|
316
|
+
if (isUserScrollUp({ kind: "gutter", lastTarget: null, scrollTop: el.scrollTop })) takeOver();
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
el.addEventListener("scroll", onScroll, { passive: true });
|
|
320
|
+
el.addEventListener("wheel", onWheel, { passive: true });
|
|
321
|
+
el.addEventListener("touchstart", onTouchStart, { passive: true });
|
|
322
|
+
el.addEventListener("touchmove", onTouchMove, { passive: true });
|
|
323
|
+
el.addEventListener("pointerdown", onPointerDown, { passive: true });
|
|
324
|
+
return () => {
|
|
325
|
+
el.removeEventListener("scroll", onScroll);
|
|
326
|
+
el.removeEventListener("wheel", onWheel);
|
|
327
|
+
el.removeEventListener("touchstart", onTouchStart);
|
|
328
|
+
el.removeEventListener("touchmove", onTouchMove);
|
|
329
|
+
el.removeEventListener("pointerdown", onPointerDown);
|
|
330
|
+
if (typeof cancelAnimationFrame === "function") cancelAnimationFrame(frame.current);
|
|
331
|
+
};
|
|
332
|
+
}, [scrollRef, attach]);
|
|
333
|
+
const jumpToBottom = (0, import_react2.useCallback)(() => {
|
|
334
|
+
attach(true);
|
|
335
|
+
followEnd();
|
|
336
|
+
}, [attach, followEnd]);
|
|
337
|
+
return { atBottom, unread, jumpToBottom };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// src/virtual-list/log-index.ts
|
|
341
|
+
function emptyScan() {
|
|
342
|
+
return { lineCount: 0, firstLine: void 0, failures: [] };
|
|
343
|
+
}
|
|
344
|
+
function scanFailures(prev, lines) {
|
|
345
|
+
const appendOnly = prev.lineCount > 0 && lines.length >= prev.lineCount && lines[0] === prev.firstLine;
|
|
346
|
+
const from = appendOnly ? prev.lineCount - 1 : 0;
|
|
347
|
+
const kept = appendOnly ? prev.failures.filter((f) => f.index < from) : [];
|
|
348
|
+
const fresh = findFailures(lines.slice(from)).map((f) => ({ ...f, index: f.index + from }));
|
|
349
|
+
return { lineCount: lines.length, firstLine: lines[0], failures: [...kept, ...fresh] };
|
|
350
|
+
}
|
|
351
|
+
function visiblePosition(visible, lineIndex) {
|
|
352
|
+
if (!visible) return lineIndex;
|
|
353
|
+
let lo = 0;
|
|
354
|
+
let hi = visible.length - 1;
|
|
355
|
+
while (lo <= hi) {
|
|
356
|
+
const mid = lo + hi >> 1;
|
|
357
|
+
const value = visible[mid];
|
|
358
|
+
if (value === lineIndex) return mid;
|
|
359
|
+
if (value < lineIndex) lo = mid + 1;
|
|
360
|
+
else hi = mid - 1;
|
|
361
|
+
}
|
|
362
|
+
return -1;
|
|
363
|
+
}
|
|
364
|
+
function stepIndex(current, count, direction) {
|
|
365
|
+
if (count === 0) return -1;
|
|
366
|
+
if (current < 0) return direction === 1 ? 0 : count - 1;
|
|
367
|
+
return (current + direction + count) % count;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/terminal-output/ansi.ts
|
|
371
|
+
var ESCAPE_SOURCE = "\\x1b(?:\\[([0-?]*)([ -/]*)([@-~])|\\]([^\\x07\\x1b]*)(?:\\x07|\\x1b\\\\)|[ -/]+[0-~]|[0-~])";
|
|
372
|
+
function stripAnsi(text) {
|
|
373
|
+
return text.replace(new RegExp(ESCAPE_SOURCE, "g"), "");
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/virtual-list/log-text.ts
|
|
377
|
+
function logToText(lines, options = {}) {
|
|
378
|
+
const rows = options.stripAnsi ? lines.map((line) => stripAnsi(line)) : lines;
|
|
379
|
+
return rows.join("\n");
|
|
380
|
+
}
|
|
381
|
+
function downloadText(filename, text) {
|
|
382
|
+
const url = URL.createObjectURL(new Blob([text], { type: "text/plain" }));
|
|
383
|
+
const a = document.createElement("a");
|
|
384
|
+
a.href = url;
|
|
385
|
+
a.download = filename;
|
|
386
|
+
a.click();
|
|
387
|
+
URL.revokeObjectURL(url);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// src/virtual-list/use-virtual-log.ts
|
|
391
|
+
var DEFAULT_ROW = 18;
|
|
392
|
+
function useVirtualLog({
|
|
393
|
+
lines,
|
|
394
|
+
scrollRef,
|
|
395
|
+
query = "",
|
|
396
|
+
estimateSize,
|
|
397
|
+
overscan = 12,
|
|
398
|
+
resetKey,
|
|
399
|
+
filename = "log.txt"
|
|
400
|
+
}) {
|
|
401
|
+
const deferred = (0, import_react3.useDeferredValue)(query);
|
|
402
|
+
const isFiltering = deferred.trim() !== "";
|
|
403
|
+
const visible = (0, import_react3.useMemo)(() => isFiltering ? filterLines(lines, deferred) : null, [lines, deferred, isFiltering]);
|
|
404
|
+
const shownCount = visible ? visible.length : lines.length;
|
|
405
|
+
const scan = (0, import_react3.useRef)(emptyScan());
|
|
406
|
+
const failures = (0, import_react3.useMemo)(() => {
|
|
407
|
+
scan.current = scanFailures(scan.current, lines);
|
|
408
|
+
return scan.current.failures;
|
|
409
|
+
}, [lines]);
|
|
410
|
+
const list = useVirtualList({
|
|
411
|
+
count: shownCount,
|
|
412
|
+
estimateSize: estimateSize ?? (() => DEFAULT_ROW),
|
|
413
|
+
scrollRef,
|
|
414
|
+
overscan
|
|
415
|
+
});
|
|
416
|
+
const lastIndex = shownCount - 1;
|
|
417
|
+
const follow = useFollowTail({
|
|
418
|
+
scrollRef,
|
|
419
|
+
itemCount: lines.length,
|
|
420
|
+
scrollToEnd: () => {
|
|
421
|
+
if (!isFiltering) list.scrollToIndex(lastIndex, { align: "end" });
|
|
422
|
+
},
|
|
423
|
+
resetKey
|
|
424
|
+
});
|
|
425
|
+
const baseline = (0, import_react3.useRef)(shownCount);
|
|
426
|
+
(0, import_react3.useEffect)(() => {
|
|
427
|
+
baseline.current = shownCount;
|
|
428
|
+
}, [deferred, resetKey]);
|
|
429
|
+
const unread = isFiltering ? Math.max(0, shownCount - baseline.current) : follow.unread;
|
|
430
|
+
(0, import_react3.useEffect)(() => {
|
|
431
|
+
if (isFiltering) list.scrollToOffset(0);
|
|
432
|
+
}, [deferred]);
|
|
433
|
+
const [failurePosition, setFailurePosition] = (0, import_react3.useState)(-1);
|
|
434
|
+
const jumpToFailure = (0, import_react3.useCallback)(
|
|
435
|
+
(n) => {
|
|
436
|
+
const failure = failures[n];
|
|
437
|
+
if (!failure) return false;
|
|
438
|
+
const at = visiblePosition(visible, failure.index);
|
|
439
|
+
if (at < 0) return false;
|
|
440
|
+
setFailurePosition(n);
|
|
441
|
+
list.scrollToIndex(at, { align: "center" });
|
|
442
|
+
return true;
|
|
443
|
+
},
|
|
444
|
+
[failures, visible, list]
|
|
445
|
+
);
|
|
446
|
+
const step = (0, import_react3.useCallback)(
|
|
447
|
+
(direction) => {
|
|
448
|
+
for (let tries = 0, n = failurePosition; tries < failures.length; tries++) {
|
|
449
|
+
n = stepIndex(n, failures.length, direction);
|
|
450
|
+
if (jumpToFailure(n)) return true;
|
|
451
|
+
}
|
|
452
|
+
return false;
|
|
453
|
+
},
|
|
454
|
+
[failurePosition, failures.length, jumpToFailure]
|
|
455
|
+
);
|
|
456
|
+
const nextFailure = (0, import_react3.useCallback)(() => step(1), [step]);
|
|
457
|
+
const previousFailure = (0, import_react3.useCallback)(() => step(-1), [step]);
|
|
458
|
+
const text = (0, import_react3.useCallback)((options) => logToText(lines, options), [lines]);
|
|
459
|
+
const copyText = (0, import_react3.useCallback)(() => logToText(lines, { stripAnsi: true }), [lines]);
|
|
460
|
+
const download = (0, import_react3.useCallback)(
|
|
461
|
+
(name) => downloadText(name ?? filename, logToText(lines, { stripAnsi: true })),
|
|
462
|
+
[lines, filename]
|
|
463
|
+
);
|
|
464
|
+
const rows = list.items.map((item) => ({
|
|
465
|
+
...item,
|
|
466
|
+
lineIndex: visible ? visible[item.index] ?? item.index : item.index
|
|
467
|
+
}));
|
|
468
|
+
return {
|
|
469
|
+
rows,
|
|
470
|
+
totalSize: list.totalSize,
|
|
471
|
+
measureRef: list.measureRef,
|
|
472
|
+
isFiltering,
|
|
473
|
+
shownCount,
|
|
474
|
+
atBottom: follow.atBottom,
|
|
475
|
+
unread,
|
|
476
|
+
jumpToBottom: follow.jumpToBottom,
|
|
477
|
+
failures,
|
|
478
|
+
failurePosition,
|
|
479
|
+
jumpToFailure,
|
|
480
|
+
nextFailure,
|
|
481
|
+
previousFailure,
|
|
482
|
+
text,
|
|
483
|
+
copyText,
|
|
484
|
+
download,
|
|
485
|
+
list
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
489
|
+
0 && (module.exports = {
|
|
490
|
+
MAX_REALIGN_FRAMES,
|
|
491
|
+
downloadText,
|
|
492
|
+
initialVisibleCount,
|
|
493
|
+
logToText,
|
|
494
|
+
measuredOrEstimate,
|
|
495
|
+
shouldRealign,
|
|
496
|
+
targetOffset,
|
|
497
|
+
useVirtualList,
|
|
498
|
+
useVirtualLog
|
|
499
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
import { Virtualizer } from '@tanstack/react-virtual';
|
|
3
|
+
import { a as Failure } from './log-helpers-rkbgYqZW.cjs';
|
|
4
|
+
|
|
5
|
+
type ScrollAlign = 'start' | 'center' | 'end' | 'auto';
|
|
6
|
+
declare const MAX_REALIGN_FRAMES = 3;
|
|
7
|
+
/** A 0 height means "not laid out yet" (jsdom, or a read before first paint), so fall back to the estimate. */
|
|
8
|
+
declare function measuredOrEstimate(measured: number | undefined, estimate: number): number;
|
|
9
|
+
interface RealignInput {
|
|
10
|
+
align: ScrollAlign;
|
|
11
|
+
itemStart: number;
|
|
12
|
+
itemSize: number;
|
|
13
|
+
scrollOffset: number;
|
|
14
|
+
viewportSize: number;
|
|
15
|
+
frame: number;
|
|
16
|
+
maxFrames?: number;
|
|
17
|
+
}
|
|
18
|
+
declare function targetOffset(align: ScrollAlign, itemStart: number, itemSize: number, viewportSize: number): number;
|
|
19
|
+
/** After rows above the target are measured the estimate can be off, so scroll again until the row sits where `align` says. */
|
|
20
|
+
declare function shouldRealign({ align, itemStart, itemSize, scrollOffset, viewportSize, frame, maxFrames }: RealignInput): boolean;
|
|
21
|
+
/** The first `initialCount` rows, used until the browser reports a viewport size. */
|
|
22
|
+
declare function initialVisibleCount(count: number, initialCount: number): number;
|
|
23
|
+
|
|
24
|
+
interface UseVirtualListOptions {
|
|
25
|
+
count: number;
|
|
26
|
+
estimateSize: (index: number) => number;
|
|
27
|
+
/** The overflow:auto element that scrolls. Omit with `windowScroll: true`. */
|
|
28
|
+
scrollRef?: RefObject<Element | null>;
|
|
29
|
+
/** Scroll with the page instead of an inner element. */
|
|
30
|
+
windowScroll?: boolean;
|
|
31
|
+
/** Distance from the top of the page to the list, for window scroll. */
|
|
32
|
+
scrollMargin?: number;
|
|
33
|
+
overscan?: number;
|
|
34
|
+
/** Measure each rendered row. Pass false when the app's own maths relies on `estimateSize`. */
|
|
35
|
+
measure?: boolean;
|
|
36
|
+
getItemKey?: (index: number) => string | number;
|
|
37
|
+
/** Rows rendered before the viewport size is known, so the server render isn't blank. */
|
|
38
|
+
initialCount?: number;
|
|
39
|
+
}
|
|
40
|
+
interface VirtualListItem {
|
|
41
|
+
index: number;
|
|
42
|
+
key: string | number | bigint;
|
|
43
|
+
start: number;
|
|
44
|
+
size: number;
|
|
45
|
+
}
|
|
46
|
+
interface ScrollToIndexOptions {
|
|
47
|
+
align?: ScrollAlign;
|
|
48
|
+
}
|
|
49
|
+
interface UseVirtualListResult {
|
|
50
|
+
items: VirtualListItem[];
|
|
51
|
+
totalSize: number;
|
|
52
|
+
/** Ref for each row element; the row also needs `data-index={item.index}`. */
|
|
53
|
+
measureRef: (el: Element | null) => void;
|
|
54
|
+
scrollToIndex: (index: number, options?: ScrollToIndexOptions) => void;
|
|
55
|
+
scrollToOffset: (offset: number) => void;
|
|
56
|
+
virtualizer: Virtualizer<Element, Element>;
|
|
57
|
+
}
|
|
58
|
+
declare function useVirtualList(options: UseVirtualListOptions): UseVirtualListResult;
|
|
59
|
+
|
|
60
|
+
interface UseVirtualLogOptions {
|
|
61
|
+
/** Plain or raw lines; the last one may still be growing. */
|
|
62
|
+
lines: readonly string[];
|
|
63
|
+
scrollRef: RefObject<HTMLElement | null>;
|
|
64
|
+
/** Show only lines containing this text. Typing stays responsive on large logs. */
|
|
65
|
+
query?: string;
|
|
66
|
+
estimateSize?: (index: number) => number;
|
|
67
|
+
overscan?: number;
|
|
68
|
+
/** Snap back to the end whenever this changes (switching logs). */
|
|
69
|
+
resetKey?: unknown;
|
|
70
|
+
/** Filename used by `download`. */
|
|
71
|
+
filename?: string;
|
|
72
|
+
}
|
|
73
|
+
interface VirtualLogRow {
|
|
74
|
+
/** Position in the shown list. */
|
|
75
|
+
index: number;
|
|
76
|
+
/** Index into `lines`. */
|
|
77
|
+
lineIndex: number;
|
|
78
|
+
key: string | number | bigint;
|
|
79
|
+
start: number;
|
|
80
|
+
size: number;
|
|
81
|
+
}
|
|
82
|
+
interface UseVirtualLogResult {
|
|
83
|
+
rows: VirtualLogRow[];
|
|
84
|
+
totalSize: number;
|
|
85
|
+
measureRef: UseVirtualListResult['measureRef'];
|
|
86
|
+
isFiltering: boolean;
|
|
87
|
+
/** Lines shown: all of them, or the matches. */
|
|
88
|
+
shownCount: number;
|
|
89
|
+
atBottom: boolean;
|
|
90
|
+
unread: number;
|
|
91
|
+
jumpToBottom: () => void;
|
|
92
|
+
failures: readonly Failure[];
|
|
93
|
+
/** Which failure the last jump landed on, or -1. */
|
|
94
|
+
failurePosition: number;
|
|
95
|
+
jumpToFailure: (n: number) => boolean;
|
|
96
|
+
nextFailure: () => boolean;
|
|
97
|
+
previousFailure: () => boolean;
|
|
98
|
+
/** The whole log as text, whatever is rendered. */
|
|
99
|
+
text: (options?: {
|
|
100
|
+
stripAnsi?: boolean;
|
|
101
|
+
}) => string;
|
|
102
|
+
copyText: () => string;
|
|
103
|
+
download: (filename?: string) => void;
|
|
104
|
+
list: UseVirtualListResult;
|
|
105
|
+
}
|
|
106
|
+
declare function useVirtualLog({ lines, scrollRef, query, estimateSize, overscan, resetKey, filename, }: UseVirtualLogOptions): UseVirtualLogResult;
|
|
107
|
+
|
|
108
|
+
interface LogToTextOptions {
|
|
109
|
+
/** Remove escape codes from each line. Default false: lines are joined as given. */
|
|
110
|
+
stripAnsi?: boolean;
|
|
111
|
+
}
|
|
112
|
+
declare function logToText(lines: readonly string[], options?: LogToTextOptions): string;
|
|
113
|
+
/** Saves text as a file. The Blob URL is revoked right after the click. */
|
|
114
|
+
declare function downloadText(filename: string, text: string): void;
|
|
115
|
+
|
|
116
|
+
export { type LogToTextOptions, MAX_REALIGN_FRAMES, type RealignInput, type ScrollAlign, type ScrollToIndexOptions, type UseVirtualListOptions, type UseVirtualListResult, type UseVirtualLogOptions, type UseVirtualLogResult, type VirtualListItem, type VirtualLogRow, downloadText, initialVisibleCount, logToText, measuredOrEstimate, shouldRealign, targetOffset, useVirtualList, useVirtualLog };
|