@mugenlabs/logtape-devtools 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,110 +1,93 @@
1
- // src/store.ts
2
- function createLogStore(maxSize = 1e3) {
3
- let records = [];
4
- let max = maxSize;
5
- const listeners = /* @__PURE__ */ new Set();
6
- function notify() {
7
- for (const listener of listeners) {
8
- listener();
9
- }
10
- }
11
- return {
12
- getSnapshot: () => records,
13
- addRecord: (record) => {
14
- records = [...records, record];
15
- if (records.length > max) {
16
- records = records.slice(records.length - max);
17
- }
18
- notify();
19
- },
20
- clear: () => {
21
- records = [];
22
- notify();
23
- },
24
- subscribe: (listener) => {
25
- listeners.add(listener);
26
- return () => {
27
- listeners.delete(listener);
28
- };
29
- },
30
- setMaxSize: (size) => {
31
- max = size;
32
- if (records.length > max) {
33
- records = records.slice(records.length - max);
34
- notify();
35
- }
36
- }
37
- };
38
- }
39
- var defaultLogStore = createLogStore();
1
+ import {
2
+ LOG_LEVELS,
3
+ createDevtoolsSink,
4
+ createLogStore,
5
+ defaultLogStore,
6
+ safeStringify
7
+ } from "./chunk-FDWUISCJ.mjs";
40
8
 
41
9
  // src/plugin/logtape-devtools-plugin.tsx
42
- import { useCallback as useCallback3, useMemo as useMemo2, useState as useState2, useSyncExternalStore } from "react";
10
+ import { useCallback as useCallback4, useMemo as useMemo2, useState as useState2, useSyncExternalStore } from "react";
43
11
 
44
12
  // src/plugin/components/log-list.tsx
45
- import { useEffect, useRef } from "react";
13
+ import { useVirtualizer } from "@tanstack/react-virtual";
14
+ import { useCallback as useCallback2, useEffect, useRef } from "react";
46
15
 
47
16
  // src/plugin/theme.ts
48
17
  var theme = {
49
18
  colors: {
19
+ accent: "#6366f1",
20
+ accentHover: "#818cf8",
50
21
  background: "#1e1e1e",
51
- surface: "#1a1a1a",
52
- surfaceHover: "#2a2a2a",
53
- surfaceSelected: "#2a2a3a",
54
22
  border: "#333",
55
- borderInput: "#444",
56
23
  borderFocus: "#6366f1",
57
- textPrimary: "#e0e0e0",
58
- textSecondary: "#888",
59
- textMuted: "#666",
60
- textDimmed: "#555",
61
- white: "#fff",
62
- accent: "#6366f1",
63
- accentHover: "#818cf8",
24
+ borderInput: "#444",
64
25
  levels: {
65
- trace: { bg: "#1a1a2e", color: "#8888bb", badge: "#5555aa" },
66
- debug: { bg: "#1a2a1a", color: "#88bb88", badge: "#55aa55" },
67
- info: { bg: "#1a2a3a", color: "#60a5fa", badge: "#3b82f6" },
68
- warning: { bg: "#2a2a1a", color: "#facc15", badge: "#eab308" },
69
- error: { bg: "#2a1a1a", color: "#ef4444", badge: "#dc2626" },
70
- fatal: { bg: "#3a0a0a", color: "#ff6b6b", badge: "#b91c1c" }
26
+ // Badge colours keep at least 4.5:1 contrast against white badge text.
27
+ debug: { badge: "#2f7a2f", bg: "#1a2a1a", color: "#88bb88" },
28
+ error: { badge: "#dc2626", bg: "#2a1a1a", color: "#ef4444" },
29
+ fatal: { badge: "#b91c1c", bg: "#3a0a0a", color: "#ff6b6b" },
30
+ info: { badge: "#2563eb", bg: "#1a2a3a", color: "#60a5fa" },
31
+ trace: { badge: "#5555aa", bg: "#1a1a2e", color: "#8888bb" },
32
+ warning: { badge: "#eab308", bg: "#2a2a1a", color: "#facc15" }
71
33
  },
72
34
  scrollbar: "#444",
73
- scrollbarHover: "#555"
35
+ scrollbarHover: "#555",
36
+ surface: "#1a1a1a",
37
+ surfaceHover: "#2a2a2a",
38
+ surfaceSelected: "#2a2a3a",
39
+ textDimmed: "#555",
40
+ textMuted: "#666",
41
+ textPrimary: "#e0e0e0",
42
+ textSecondary: "#888",
43
+ white: "#fff"
74
44
  },
75
45
  fontFamily: {
76
46
  mono: 'ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, monospace',
77
47
  sans: 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif'
78
48
  },
79
49
  fontSize: {
80
- xs: "9px",
81
- sm: "10px",
82
- md: "11px",
83
50
  base: "12px",
84
- lg: "13px"
51
+ lg: "13px",
52
+ md: "11px",
53
+ sm: "10px",
54
+ xs: "9px"
85
55
  },
86
56
  radius: {
87
- sm: "3px",
88
- md: "4px",
89
57
  lg: "6px",
90
- pill: "10px"
58
+ md: "4px",
59
+ pill: "10px",
60
+ sm: "3px"
91
61
  },
92
62
  spacing: {
93
- xs: "2px",
94
- sm: "4px",
95
- md: "6px",
96
63
  lg: "8px",
64
+ md: "6px",
65
+ sm: "4px",
97
66
  xl: "12px",
67
+ xs: "2px",
98
68
  xxl: "16px"
99
69
  }
100
70
  };
71
+ var getLevelColors = (level) => theme.colors.levels[level] ?? theme.colors.levels.info;
101
72
 
102
73
  // src/plugin/components/log-row.tsx
103
- import { useCallback } from "react";
74
+ import { memo, useCallback } from "react";
75
+
76
+ // src/plugin/format.ts
77
+ var timeFormatter;
78
+ function formatTime(ts) {
79
+ timeFormatter ??= new Intl.DateTimeFormat(void 0, {
80
+ hour: "2-digit",
81
+ hour12: false,
82
+ minute: "2-digit",
83
+ second: "2-digit"
84
+ });
85
+ return timeFormatter.format(new Date(ts));
86
+ }
104
87
 
105
88
  // src/plugin/icons.tsx
106
89
  import { jsx, jsxs } from "react/jsx-runtime";
107
- var defaults = { size: 14, color: "currentColor" };
90
+ var defaults = { color: "currentColor", size: 14 };
108
91
  var Svg = ({
109
92
  size = defaults.size,
110
93
  color = defaults.color,
@@ -159,15 +142,16 @@ var Trash2Icon = (props) => /* @__PURE__ */ jsxs(Svg, { ...props, children: [
159
142
  // src/plugin/components/level-badge.tsx
160
143
  import { jsx as jsx2 } from "react/jsx-runtime";
161
144
  var LEVEL_LABELS = {
162
- trace: "TRC",
163
145
  debug: "DBG",
164
- info: "INF",
165
- warning: "WRN",
166
146
  error: "ERR",
167
- fatal: "FTL"
147
+ fatal: "FTL",
148
+ info: "INF",
149
+ trace: "TRC",
150
+ warning: "WRN"
168
151
  };
169
152
  var LevelBadge = ({ level }) => {
170
- const colors = theme.colors.levels[level];
153
+ const colors = getLevelColors(level);
154
+ const label = LEVEL_LABELS[level] ?? level.toUpperCase();
171
155
  return /* @__PURE__ */ jsx2(
172
156
  "span",
173
157
  {
@@ -185,100 +169,129 @@ var LevelBadge = ({ level }) => {
185
169
  textAlign: "center",
186
170
  textTransform: "uppercase"
187
171
  },
188
- children: LEVEL_LABELS[level]
172
+ children: label
189
173
  }
190
174
  );
191
175
  };
192
176
 
193
177
  // src/plugin/components/log-detail.tsx
194
178
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
195
- function formatTimestamp(ts) {
196
- const d = new Date(ts);
197
- return d.toLocaleTimeString(void 0, {
198
- hour12: false,
199
- hour: "2-digit",
200
- minute: "2-digit",
201
- second: "2-digit"
202
- });
203
- }
204
- function safeStringify(value, indent = 2) {
205
- try {
206
- return JSON.stringify(value, null, indent);
207
- } catch {
208
- return String(value);
209
- }
210
- }
211
- var DetailRow = ({ label, children }) => /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: theme.spacing.lg, marginBottom: theme.spacing.md }, children: [
212
- /* @__PURE__ */ jsx3(
213
- "span",
214
- {
215
- style: {
216
- color: theme.colors.textMuted,
217
- flexShrink: 0,
218
- fontSize: theme.fontSize.md,
219
- minWidth: "72px",
220
- textAlign: "right"
221
- },
222
- children: label
223
- }
224
- ),
225
- /* @__PURE__ */ jsx3("span", { style: { fontSize: theme.fontSize.base, wordBreak: "break-all" }, children })
179
+ var detailRowStyle = {
180
+ display: "flex",
181
+ gap: theme.spacing.lg,
182
+ marginBottom: theme.spacing.md
183
+ };
184
+ var detailLabelStyle = {
185
+ color: theme.colors.textMuted,
186
+ flexShrink: 0,
187
+ fontSize: theme.fontSize.md,
188
+ minWidth: "72px",
189
+ textAlign: "right"
190
+ };
191
+ var detailValueStyle = {
192
+ fontSize: theme.fontSize.base,
193
+ wordBreak: "break-all"
194
+ };
195
+ var containerStyle = {
196
+ borderTop: `1px solid ${theme.colors.border}`,
197
+ padding: `${theme.spacing.lg} ${theme.spacing.xl}`
198
+ };
199
+ var categoryStyle = {
200
+ color: theme.colors.accent,
201
+ fontFamily: theme.fontFamily.mono
202
+ };
203
+ var callerStyle = {
204
+ color: theme.colors.textSecondary,
205
+ fontFamily: theme.fontFamily.mono
206
+ };
207
+ var messageStyle = { color: theme.colors.textPrimary };
208
+ var propertiesStyle = {
209
+ background: "rgba(0,0,0,0.3)",
210
+ borderRadius: theme.radius.md,
211
+ color: theme.colors.textPrimary,
212
+ fontFamily: theme.fontFamily.mono,
213
+ fontSize: theme.fontSize.md,
214
+ margin: 0,
215
+ maxHeight: "200px",
216
+ overflow: "auto",
217
+ padding: theme.spacing.lg,
218
+ whiteSpace: "pre-wrap",
219
+ wordBreak: "break-all"
220
+ };
221
+ var DetailRow = ({ label, children }) => /* @__PURE__ */ jsxs2("div", { style: detailRowStyle, children: [
222
+ /* @__PURE__ */ jsx3("span", { style: detailLabelStyle, children: label }),
223
+ /* @__PURE__ */ jsx3("span", { style: detailValueStyle, children })
226
224
  ] });
227
225
  var LogDetail = ({ record }) => {
228
226
  const levelColors = theme.colors.levels[record.level];
229
227
  const hasProperties = Object.keys(record.properties).length > 0;
230
- return /* @__PURE__ */ jsxs2(
231
- "div",
232
- {
233
- "data-testid": "log-detail",
234
- style: {
235
- background: levelColors.bg,
236
- borderTop: `1px solid ${theme.colors.border}`,
237
- padding: `${theme.spacing.lg} ${theme.spacing.xl}`
238
- },
239
- children: [
240
- /* @__PURE__ */ jsx3(DetailRow, { label: "Time", children: formatTimestamp(record.timestamp) }),
241
- /* @__PURE__ */ jsx3(DetailRow, { label: "Level", children: /* @__PURE__ */ jsx3("span", { style: { color: levelColors.color, fontWeight: 600 }, children: record.level }) }),
242
- /* @__PURE__ */ jsx3(DetailRow, { label: "Category", children: /* @__PURE__ */ jsx3("span", { style: { color: theme.colors.accent, fontFamily: theme.fontFamily.mono }, children: record.category.join(".") }) }),
243
- record.caller && /* @__PURE__ */ jsx3(DetailRow, { label: "Caller", children: /* @__PURE__ */ jsx3("span", { style: { color: theme.colors.textSecondary, fontFamily: theme.fontFamily.mono }, children: record.caller }) }),
244
- /* @__PURE__ */ jsx3(DetailRow, { label: "Message", children: /* @__PURE__ */ jsx3("span", { style: { color: theme.colors.textPrimary }, children: record.messageText }) }),
245
- hasProperties && /* @__PURE__ */ jsx3(DetailRow, { label: "Data", children: /* @__PURE__ */ jsx3(
246
- "pre",
247
- {
248
- style: {
249
- background: "rgba(0,0,0,0.3)",
250
- borderRadius: theme.radius.md,
251
- color: theme.colors.textPrimary,
252
- fontFamily: theme.fontFamily.mono,
253
- fontSize: theme.fontSize.md,
254
- margin: 0,
255
- maxHeight: "200px",
256
- overflow: "auto",
257
- padding: theme.spacing.lg,
258
- whiteSpace: "pre-wrap",
259
- wordBreak: "break-all"
260
- },
261
- children: safeStringify(record.properties)
262
- }
263
- ) })
264
- ]
265
- }
266
- );
228
+ return /* @__PURE__ */ jsxs2("div", { "data-testid": "log-detail", style: { ...containerStyle, background: levelColors.bg }, children: [
229
+ /* @__PURE__ */ jsx3(DetailRow, { label: "Time", children: formatTime(record.timestamp) }),
230
+ /* @__PURE__ */ jsx3(DetailRow, { label: "Level", children: /* @__PURE__ */ jsx3("span", { style: { color: levelColors.color, fontWeight: 600 }, children: record.level }) }),
231
+ /* @__PURE__ */ jsx3(DetailRow, { label: "Category", children: /* @__PURE__ */ jsx3("span", { style: categoryStyle, children: record.category.join(".") }) }),
232
+ record.caller ? /* @__PURE__ */ jsx3(DetailRow, { label: "Caller", children: /* @__PURE__ */ jsx3("span", { style: callerStyle, children: record.caller }) }) : null,
233
+ /* @__PURE__ */ jsx3(DetailRow, { label: "Message", children: /* @__PURE__ */ jsx3("span", { style: messageStyle, children: record.messageText }) }),
234
+ hasProperties && /* @__PURE__ */ jsx3(DetailRow, { label: "Data", children: /* @__PURE__ */ jsx3("pre", { style: propertiesStyle, children: safeStringify(record.properties, 2) }) })
235
+ ] });
267
236
  };
268
237
 
269
238
  // src/plugin/components/log-row.tsx
270
239
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
271
- function formatTime(ts) {
272
- const d = new Date(ts);
273
- return d.toLocaleTimeString(void 0, {
274
- hour12: false,
275
- hour: "2-digit",
276
- minute: "2-digit",
277
- second: "2-digit"
278
- });
279
- }
280
- var LogRow = ({ record, expanded, onToggle }) => {
281
- const levelColors = theme.colors.levels[record.level];
240
+ var rowStyle = {
241
+ alignItems: "center",
242
+ border: "none",
243
+ borderBottom: `1px solid ${theme.colors.border}`,
244
+ cursor: "pointer",
245
+ display: "flex",
246
+ gap: theme.spacing.lg,
247
+ padding: `${theme.spacing.md} ${theme.spacing.xl}`,
248
+ textAlign: "left",
249
+ transition: "background 0.1s, filter 0.1s",
250
+ width: "100%"
251
+ };
252
+ var timestampStyle = {
253
+ color: theme.colors.textDimmed,
254
+ flexShrink: 0,
255
+ fontFamily: theme.fontFamily.mono,
256
+ fontSize: theme.fontSize.md
257
+ };
258
+ var categoryStyle2 = {
259
+ color: theme.colors.accent,
260
+ flexShrink: 0,
261
+ fontFamily: theme.fontFamily.mono,
262
+ fontSize: theme.fontSize.md,
263
+ maxWidth: "180px",
264
+ overflow: "hidden",
265
+ textOverflow: "ellipsis",
266
+ whiteSpace: "nowrap"
267
+ };
268
+ var messageStyle2 = {
269
+ color: theme.colors.textPrimary,
270
+ flex: 1,
271
+ fontSize: theme.fontSize.base,
272
+ overflow: "hidden",
273
+ textOverflow: "ellipsis",
274
+ whiteSpace: "nowrap"
275
+ };
276
+ var callerStyle2 = {
277
+ color: theme.colors.textDimmed,
278
+ flexShrink: 0,
279
+ fontFamily: theme.fontFamily.mono,
280
+ fontSize: theme.fontSize.sm,
281
+ maxWidth: "200px",
282
+ opacity: 0.7,
283
+ overflow: "hidden",
284
+ textOverflow: "ellipsis",
285
+ whiteSpace: "nowrap"
286
+ };
287
+ var chevronStyle = {
288
+ color: theme.colors.textDimmed,
289
+ display: "flex",
290
+ flexShrink: 0,
291
+ transition: "transform 0.15s"
292
+ };
293
+ var LogRowComponent = ({ record, expanded, onToggle }) => {
294
+ const levelColors = getLevelColors(record.level);
282
295
  const handleClick = useCallback(() => {
283
296
  onToggle(expanded ? null : record.id);
284
297
  }, [expanded, onToggle, record.id]);
@@ -290,173 +303,119 @@ var LogRow = ({ record, expanded, onToggle }) => {
290
303
  "data-lt-interactive": "",
291
304
  "data-testid": "log-row",
292
305
  onClick: handleClick,
293
- style: {
294
- alignItems: "center",
295
- background: expanded ? levelColors.bg : "transparent",
296
- border: "none",
297
- borderBottom: `1px solid ${theme.colors.border}`,
298
- cursor: "pointer",
299
- display: "flex",
300
- gap: theme.spacing.lg,
301
- padding: `${theme.spacing.md} ${theme.spacing.xl}`,
302
- textAlign: "left",
303
- transition: "background 0.1s, filter 0.1s",
304
- width: "100%"
305
- },
306
+ style: { ...rowStyle, background: expanded ? levelColors.bg : "transparent" },
306
307
  type: "button",
307
308
  children: [
308
- /* @__PURE__ */ jsx4(
309
- "span",
310
- {
311
- style: {
312
- color: theme.colors.textDimmed,
313
- flexShrink: 0,
314
- fontFamily: theme.fontFamily.mono,
315
- fontSize: theme.fontSize.md
316
- },
317
- children: formatTime(record.timestamp)
318
- }
319
- ),
309
+ /* @__PURE__ */ jsx4("span", { style: timestampStyle, children: formatTime(record.timestamp) }),
320
310
  /* @__PURE__ */ jsx4(LevelBadge, { level: record.level }),
321
- /* @__PURE__ */ jsx4(
322
- "span",
323
- {
324
- style: {
325
- color: theme.colors.accent,
326
- flexShrink: 0,
327
- fontFamily: theme.fontFamily.mono,
328
- fontSize: theme.fontSize.md,
329
- maxWidth: "180px",
330
- overflow: "hidden",
331
- textOverflow: "ellipsis",
332
- whiteSpace: "nowrap"
333
- },
334
- children: record.category.join(".")
335
- }
336
- ),
337
- /* @__PURE__ */ jsx4(
338
- "span",
339
- {
340
- style: {
341
- color: theme.colors.textPrimary,
342
- flex: 1,
343
- fontSize: theme.fontSize.base,
344
- overflow: "hidden",
345
- textOverflow: "ellipsis",
346
- whiteSpace: "nowrap"
347
- },
348
- children: record.messageText
349
- }
350
- ),
351
- record.caller && /* @__PURE__ */ jsx4(
352
- "span",
353
- {
354
- style: {
355
- color: theme.colors.textDimmed,
356
- flexShrink: 0,
357
- fontFamily: theme.fontFamily.mono,
358
- fontSize: theme.fontSize.sm,
359
- maxWidth: "200px",
360
- opacity: 0.7,
361
- overflow: "hidden",
362
- textOverflow: "ellipsis",
363
- whiteSpace: "nowrap"
364
- },
365
- children: record.caller
366
- }
367
- ),
368
- /* @__PURE__ */ jsx4(
369
- "span",
370
- {
371
- style: {
372
- color: theme.colors.textDimmed,
373
- display: "flex",
374
- flexShrink: 0,
375
- transform: expanded ? "rotate(90deg)" : "rotate(0deg)",
376
- transition: "transform 0.15s"
377
- },
378
- children: /* @__PURE__ */ jsx4(ChevronRightIcon, { size: 12 })
379
- }
380
- )
311
+ /* @__PURE__ */ jsx4("span", { style: categoryStyle2, children: record.category.join(".") }),
312
+ /* @__PURE__ */ jsx4("span", { style: messageStyle2, children: record.messageText }),
313
+ record.caller ? /* @__PURE__ */ jsx4("span", { style: callerStyle2, children: record.caller }) : null,
314
+ /* @__PURE__ */ jsx4("span", { style: { ...chevronStyle, transform: expanded ? "rotate(90deg)" : "rotate(0deg)" }, children: /* @__PURE__ */ jsx4(ChevronRightIcon, { size: 12 }) })
381
315
  ]
382
316
  }
383
317
  ),
384
- expanded && /* @__PURE__ */ jsx4(LogDetail, { record })
318
+ expanded ? /* @__PURE__ */ jsx4(LogDetail, { record }) : null
385
319
  ] });
386
320
  };
321
+ var LogRow = memo(LogRowComponent);
387
322
 
388
323
  // src/plugin/components/log-list.tsx
389
324
  import { jsx as jsx5 } from "react/jsx-runtime";
325
+ var ESTIMATED_ROW_HEIGHT = 28;
326
+ var OVERSCAN = 10;
327
+ var STICK_THRESHOLD = 40;
328
+ var emptyStyle = {
329
+ alignItems: "center",
330
+ color: theme.colors.textMuted,
331
+ display: "flex",
332
+ flex: 1,
333
+ fontSize: theme.fontSize.lg,
334
+ justifyContent: "center"
335
+ };
336
+ var containerStyle2 = {
337
+ flex: 1,
338
+ overflow: "auto",
339
+ scrollbarColor: `${theme.colors.scrollbar} transparent`,
340
+ scrollbarWidth: "thin"
341
+ };
390
342
  var LogList = ({ records, expandedId, onToggle, autoScroll }) => {
391
343
  const containerRef = useRef(null);
392
344
  const isAtBottomRef = useRef(true);
345
+ const virtualizer = useVirtualizer({
346
+ count: records.length,
347
+ estimateSize: () => ESTIMATED_ROW_HEIGHT,
348
+ // The virtualizer can ask for keys of indices from a previous, longer list
349
+ // (after clear or a narrowing filter), so never assume the index exists.
350
+ getItemKey: (index) => records[index]?.id ?? index,
351
+ getScrollElement: () => containerRef.current,
352
+ overscan: OVERSCAN
353
+ });
393
354
  const recordCount = records.length;
355
+ const lastRecordId = records[recordCount - 1]?.id;
394
356
  useEffect(() => {
395
- const el = containerRef.current;
396
- if (!(el && autoScroll && isAtBottomRef.current)) {
357
+ if (lastRecordId === void 0 || !(autoScroll && isAtBottomRef.current)) {
397
358
  return;
398
359
  }
399
- el.scrollTop = el.scrollHeight;
400
- }, [autoScroll, recordCount]);
401
- const handleScroll = () => {
360
+ virtualizer.scrollToIndex(recordCount - 1, { align: "end" });
361
+ }, [autoScroll, lastRecordId, recordCount, virtualizer]);
362
+ useEffect(() => {
363
+ if (recordCount === 0) {
364
+ isAtBottomRef.current = true;
365
+ }
366
+ }, [recordCount]);
367
+ const handleScroll = useCallback2(() => {
402
368
  const el = containerRef.current;
403
369
  if (!el) {
404
370
  return;
405
371
  }
406
- isAtBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 40;
407
- };
372
+ isAtBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < STICK_THRESHOLD;
373
+ }, []);
408
374
  if (records.length === 0) {
409
- return /* @__PURE__ */ jsx5(
410
- "div",
411
- {
412
- "data-testid": "log-list-empty",
413
- style: {
414
- alignItems: "center",
415
- color: theme.colors.textMuted,
416
- display: "flex",
417
- flex: 1,
418
- fontSize: theme.fontSize.lg,
419
- justifyContent: "center"
420
- },
421
- children: "No logs yet"
422
- }
423
- );
375
+ return /* @__PURE__ */ jsx5("div", { "data-testid": "log-list-empty", style: emptyStyle, children: "No logs yet" });
424
376
  }
425
- return /* @__PURE__ */ jsx5(
377
+ const virtualItems = virtualizer.getVirtualItems();
378
+ return /* @__PURE__ */ jsx5("div", { "data-testid": "log-list", onScroll: handleScroll, ref: containerRef, style: containerStyle2, children: /* @__PURE__ */ jsx5(
426
379
  "div",
427
380
  {
428
- "data-testid": "log-list",
429
- onScroll: handleScroll,
430
- ref: containerRef,
431
381
  style: {
432
- flex: 1,
433
- overflow: "auto",
434
- scrollbarColor: `${theme.colors.scrollbar} transparent`,
435
- scrollbarWidth: "thin"
382
+ height: `${virtualizer.getTotalSize()}px`,
383
+ position: "relative",
384
+ width: "100%"
436
385
  },
437
- children: records.map((record) => /* @__PURE__ */ jsx5(
438
- LogRow,
386
+ children: /* @__PURE__ */ jsx5(
387
+ "div",
439
388
  {
440
- expanded: expandedId === record.id,
441
- onToggle,
442
- record
443
- },
444
- record.id
445
- ))
389
+ style: {
390
+ left: 0,
391
+ position: "absolute",
392
+ top: 0,
393
+ transform: `translateY(${virtualItems[0]?.start ?? 0}px)`,
394
+ width: "100%"
395
+ },
396
+ children: virtualItems.map((virtualItem) => {
397
+ const record = records[virtualItem.index];
398
+ return /* @__PURE__ */ jsx5(
399
+ "div",
400
+ {
401
+ "data-index": virtualItem.index,
402
+ ref: virtualizer.measureElement,
403
+ children: /* @__PURE__ */ jsx5(LogRow, { expanded: expandedId === record.id, onToggle, record })
404
+ },
405
+ virtualItem.key
406
+ );
407
+ })
408
+ }
409
+ )
446
410
  }
447
- );
411
+ ) });
448
412
  };
449
413
 
450
414
  // src/plugin/components/toolbar.tsx
451
415
  import { Combobox } from "@base-ui-components/react/combobox";
452
416
  import { Popover } from "@base-ui-components/react/popover";
453
417
  import { Debouncer } from "@tanstack/pacer";
454
- import { useCallback as useCallback2, useEffect as useEffect2, useMemo, useState } from "react";
455
-
456
- // src/types.ts
457
- var LOG_LEVELS = ["trace", "debug", "info", "warning", "error", "fatal"];
458
-
459
- // src/plugin/components/toolbar.tsx
418
+ import { useCallback as useCallback3, useEffect as useEffect2, useMemo, useState } from "react";
460
419
  import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
461
420
  var HOVER_STYLE_ID = "logtape-devtools-hover";
462
421
  function useHoverStyles() {
@@ -473,6 +432,11 @@ function useHoverStyles() {
473
432
  [data-lt-interactive]:active {
474
433
  filter: brightness(0.9);
475
434
  }
435
+ [data-lt-interactive]:focus-visible,
436
+ [data-lt-input]:focus-visible {
437
+ outline: 2px solid ${theme.colors.accent};
438
+ outline-offset: 1px;
439
+ }
476
440
  [data-lt-clear-btn]:hover {
477
441
  color: ${theme.colors.textPrimary} !important;
478
442
  }
@@ -518,14 +482,15 @@ var sectionLabelStyle = {
518
482
  var LevelToggle = ({
519
483
  level,
520
484
  active,
485
+ pressed,
521
486
  onToggle
522
487
  }) => {
523
488
  const colors = theme.colors.levels[level];
524
- const handleClick = useCallback2(() => onToggle(level), [level, onToggle]);
489
+ const handleClick = useCallback3(() => onToggle(level), [level, onToggle]);
525
490
  return /* @__PURE__ */ jsx6(
526
491
  "button",
527
492
  {
528
- "aria-pressed": active,
493
+ "aria-pressed": pressed,
529
494
  "data-lt-interactive": "",
530
495
  "data-testid": `level-toggle-${level}`,
531
496
  onClick: handleClick,
@@ -561,32 +526,26 @@ var Toolbar = ({
561
526
  categories,
562
527
  categoryFilter,
563
528
  onCategoryFilterChange,
564
- searchText,
565
529
  onSearchTextChange,
566
530
  totalCount,
567
531
  filteredCount
568
532
  }) => {
569
533
  useHoverStyles();
570
- const [localSearch, setLocalSearch] = useState(searchText);
534
+ const [search, setSearch] = useState("");
571
535
  const debouncedSearch = useMemo(
572
536
  () => new Debouncer(onSearchTextChange, { wait: 200 }),
573
537
  [onSearchTextChange]
574
538
  );
575
539
  useEffect2(() => () => debouncedSearch.cancel(), [debouncedSearch]);
576
- const handleSearchChange = useCallback2(
540
+ const handleSearchChange = useCallback3(
577
541
  (e) => {
578
- const value = e.target.value;
579
- setLocalSearch(value);
542
+ const { value } = e.target;
543
+ setSearch(value);
580
544
  debouncedSearch.maybeExecute(value);
581
545
  },
582
546
  [debouncedSearch]
583
547
  );
584
- useEffect2(() => {
585
- if (searchText === "" && localSearch !== "") {
586
- setLocalSearch("");
587
- }
588
- }, [searchText, localSearch]);
589
- const handleLevelToggle = useCallback2(
548
+ const handleLevelToggle = useCallback3(
590
549
  (level) => {
591
550
  const next = new Set(levelFilter);
592
551
  if (next.has(level)) {
@@ -699,7 +658,8 @@ var Toolbar = ({
699
658
  {
700
659
  active: levelFilter.size === 0 || levelFilter.has(level),
701
660
  level,
702
- onToggle: handleLevelToggle
661
+ onToggle: handleLevelToggle,
662
+ pressed: levelFilter.has(level)
703
663
  },
704
664
  level
705
665
  ))
@@ -835,6 +795,8 @@ var Toolbar = ({
835
795
  /* @__PURE__ */ jsx6(
836
796
  Combobox.Input,
837
797
  {
798
+ "aria-label": "Filter by category",
799
+ "data-lt-input": "",
838
800
  "data-testid": "category-input",
839
801
  placeholder: categoryFilter.length === 0 ? "Category\u2026" : "",
840
802
  style: {
@@ -901,12 +863,14 @@ var Toolbar = ({
901
863
  /* @__PURE__ */ jsx6(
902
864
  "input",
903
865
  {
866
+ "aria-label": "Search logs",
867
+ "data-lt-input": "",
904
868
  "data-testid": "search-input",
905
869
  onChange: handleSearchChange,
906
870
  placeholder: "Search\u2026",
907
871
  style: inputStyle,
908
872
  type: "text",
909
- value: localSearch
873
+ value: search
910
874
  }
911
875
  )
912
876
  ] })
@@ -917,6 +881,61 @@ var Toolbar = ({
917
881
 
918
882
  // src/plugin/logtape-devtools-plugin.tsx
919
883
  import { Fragment, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
884
+ var rootStyle = {
885
+ background: theme.colors.background,
886
+ color: theme.colors.textPrimary,
887
+ display: "flex",
888
+ flexDirection: "row",
889
+ fontFamily: theme.fontFamily.sans,
890
+ height: "100%",
891
+ overflow: "hidden"
892
+ };
893
+ var sidebarStyle = {
894
+ flexShrink: 0,
895
+ overflow: "hidden",
896
+ transition: "width 0.2s ease, opacity 0.15s ease"
897
+ };
898
+ var mainStyle = {
899
+ display: "flex",
900
+ flex: 1,
901
+ flexDirection: "column",
902
+ overflow: "hidden"
903
+ };
904
+ var topBarStyle = {
905
+ alignItems: "center",
906
+ borderBottom: `1px solid ${theme.colors.border}`,
907
+ display: "flex",
908
+ flexShrink: 0,
909
+ gap: theme.spacing.lg,
910
+ padding: `${theme.spacing.sm} ${theme.spacing.lg}`
911
+ };
912
+ var sidebarToggleStyle = {
913
+ background: "none",
914
+ border: "none",
915
+ color: theme.colors.textMuted,
916
+ cursor: "pointer",
917
+ fontFamily: theme.fontFamily.sans,
918
+ fontSize: theme.fontSize.md,
919
+ padding: `${theme.spacing.xs} ${theme.spacing.sm}`,
920
+ transition: "color 0.1s"
921
+ };
922
+ var iconLabelStyle = { alignItems: "center", display: "inline-flex", gap: "6px" };
923
+ var countStyle = {
924
+ color: theme.colors.textMuted,
925
+ fontFamily: theme.fontFamily.mono,
926
+ fontSize: theme.fontSize.sm
927
+ };
928
+ var compactButtonStyle = {
929
+ background: theme.colors.surfaceHover,
930
+ border: `1px solid ${theme.colors.border}`,
931
+ borderRadius: theme.radius.sm,
932
+ color: theme.colors.textPrimary,
933
+ cursor: "pointer",
934
+ fontFamily: theme.fontFamily.sans,
935
+ fontSize: theme.fontSize.sm,
936
+ padding: `${theme.spacing.xs} ${theme.spacing.sm}`,
937
+ transition: "filter 0.1s"
938
+ };
920
939
  var LogTapeDevtoolsPlugin = ({ store }) => {
921
940
  const allRecords = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
922
941
  const [paused, setPaused] = useState2(false);
@@ -926,15 +945,15 @@ var LogTapeDevtoolsPlugin = ({ store }) => {
926
945
  const [searchText, setSearchText] = useState2("");
927
946
  const [expandedId, setExpandedId] = useState2(null);
928
947
  const [sidebarOpen, setSidebarOpen] = useState2(true);
929
- const handlePause = useCallback3(() => {
948
+ const handlePause = useCallback4(() => {
930
949
  setPaused(true);
931
950
  setPausedRecords(store.getSnapshot());
932
951
  }, [store]);
933
- const handleResume = useCallback3(() => {
952
+ const handleResume = useCallback4(() => {
934
953
  setPaused(false);
935
954
  setPausedRecords([]);
936
955
  }, []);
937
- const handleClear = useCallback3(() => {
956
+ const handleClear = useCallback4(() => {
938
957
  store.clear();
939
958
  setPausedRecords([]);
940
959
  setExpandedId(null);
@@ -947,8 +966,8 @@ var LogTapeDevtoolsPlugin = ({ store }) => {
947
966
  }
948
967
  return Array.from(seen).sort();
949
968
  }, [records]);
950
- const filteredRecords = useMemo2(() => {
951
- return records.filter((r) => {
969
+ const filteredRecords = useMemo2(
970
+ () => records.filter((r) => {
952
971
  if (levelFilter.size > 0 && !levelFilter.has(r.level)) {
953
972
  return false;
954
973
  }
@@ -962,160 +981,96 @@ var LogTapeDevtoolsPlugin = ({ store }) => {
962
981
  }
963
982
  }
964
983
  return true;
965
- });
966
- }, [records, levelFilter, categoryFilter, searchText]);
967
- return /* @__PURE__ */ jsxs5(
968
- "div",
969
- {
970
- style: {
971
- background: theme.colors.background,
972
- color: theme.colors.textPrimary,
973
- display: "flex",
974
- flexDirection: "row",
975
- fontFamily: theme.fontFamily.sans,
976
- height: "100%",
977
- overflow: "hidden"
978
- },
979
- children: [
984
+ }),
985
+ [records, levelFilter, categoryFilter, searchText]
986
+ );
987
+ return /* @__PURE__ */ jsxs5("div", { style: rootStyle, children: [
988
+ /* @__PURE__ */ jsx7(
989
+ "div",
990
+ {
991
+ style: {
992
+ ...sidebarStyle,
993
+ borderRight: sidebarOpen ? `1px solid ${theme.colors.border}` : "none",
994
+ opacity: sidebarOpen ? 1 : 0,
995
+ width: sidebarOpen ? "200px" : "0px"
996
+ },
997
+ children: /* @__PURE__ */ jsx7(
998
+ Toolbar,
999
+ {
1000
+ categories: uniqueCategories,
1001
+ categoryFilter,
1002
+ filteredCount: filteredRecords.length,
1003
+ levelFilter,
1004
+ onCategoryFilterChange: setCategoryFilter,
1005
+ onClear: handleClear,
1006
+ onLevelFilterChange: setLevelFilter,
1007
+ onPause: handlePause,
1008
+ onResume: handleResume,
1009
+ onSearchTextChange: setSearchText,
1010
+ paused,
1011
+ totalCount: records.length
1012
+ }
1013
+ )
1014
+ }
1015
+ ),
1016
+ /* @__PURE__ */ jsxs5("div", { style: mainStyle, children: [
1017
+ /* @__PURE__ */ jsxs5("div", { style: topBarStyle, children: [
980
1018
  /* @__PURE__ */ jsx7(
981
- "div",
1019
+ "button",
982
1020
  {
983
- style: {
984
- borderRight: sidebarOpen ? `1px solid ${theme.colors.border}` : "none",
985
- flexShrink: 0,
986
- opacity: sidebarOpen ? 1 : 0,
987
- overflow: "hidden",
988
- transition: "width 0.2s ease, opacity 0.15s ease",
989
- width: sidebarOpen ? "200px" : "0px"
990
- },
991
- children: /* @__PURE__ */ jsx7(
992
- Toolbar,
993
- {
994
- categories: uniqueCategories,
995
- categoryFilter,
996
- filteredCount: filteredRecords.length,
997
- levelFilter,
998
- onCategoryFilterChange: setCategoryFilter,
999
- onClear: handleClear,
1000
- onLevelFilterChange: setLevelFilter,
1001
- onPause: handlePause,
1002
- onResume: handleResume,
1003
- onSearchTextChange: setSearchText,
1004
- paused,
1005
- searchText,
1006
- totalCount: records.length
1007
- }
1008
- )
1021
+ "aria-expanded": sidebarOpen,
1022
+ "data-lt-clear-btn": "",
1023
+ onClick: () => setSidebarOpen((v) => !v),
1024
+ style: sidebarToggleStyle,
1025
+ title: sidebarOpen ? "Collapse sidebar" : "Expand sidebar",
1026
+ type: "button",
1027
+ children: /* @__PURE__ */ jsxs5("span", { style: iconLabelStyle, children: [
1028
+ sidebarOpen ? /* @__PURE__ */ jsx7(PanelLeftCloseIcon, { size: 14 }) : /* @__PURE__ */ jsx7(PanelLeftOpenIcon, { size: 14 }),
1029
+ sidebarOpen ? "Hide controls" : "Show controls"
1030
+ ] })
1009
1031
  }
1010
1032
  ),
1011
- /* @__PURE__ */ jsxs5("div", { style: { display: "flex", flex: 1, flexDirection: "column", overflow: "hidden" }, children: [
1012
- /* @__PURE__ */ jsxs5(
1013
- "div",
1033
+ !sidebarOpen && /* @__PURE__ */ jsxs5(Fragment, { children: [
1034
+ /* @__PURE__ */ jsx7("span", { style: countStyle, children: filteredRecords.length === records.length ? `${records.length} logs` : `${filteredRecords.length} / ${records.length} logs` }),
1035
+ /* @__PURE__ */ jsx7(
1036
+ "button",
1014
1037
  {
1038
+ "data-lt-interactive": "",
1039
+ onClick: paused ? handleResume : handlePause,
1015
1040
  style: {
1016
- alignItems: "center",
1017
- borderBottom: `1px solid ${theme.colors.border}`,
1018
- display: "flex",
1019
- flexShrink: 0,
1020
- gap: theme.spacing.lg,
1021
- padding: `${theme.spacing.sm} ${theme.spacing.lg}`
1041
+ ...compactButtonStyle,
1042
+ background: paused ? theme.colors.levels.warning.badge : compactButtonStyle.background,
1043
+ color: paused ? theme.colors.white : compactButtonStyle.color
1022
1044
  },
1023
- children: [
1024
- /* @__PURE__ */ jsx7(
1025
- "button",
1026
- {
1027
- "aria-expanded": sidebarOpen,
1028
- "data-lt-clear-btn": "",
1029
- onClick: () => setSidebarOpen((v) => !v),
1030
- style: {
1031
- background: "none",
1032
- border: "none",
1033
- color: theme.colors.textMuted,
1034
- cursor: "pointer",
1035
- fontFamily: theme.fontFamily.sans,
1036
- fontSize: theme.fontSize.md,
1037
- padding: `${theme.spacing.xs} ${theme.spacing.sm}`,
1038
- transition: "color 0.1s"
1039
- },
1040
- title: sidebarOpen ? "Collapse sidebar" : "Expand sidebar",
1041
- type: "button",
1042
- children: /* @__PURE__ */ jsxs5("span", { style: { alignItems: "center", display: "inline-flex", gap: "6px" }, children: [
1043
- sidebarOpen ? /* @__PURE__ */ jsx7(PanelLeftCloseIcon, { size: 14 }) : /* @__PURE__ */ jsx7(PanelLeftOpenIcon, { size: 14 }),
1044
- sidebarOpen ? "Hide controls" : "Show controls"
1045
- ] })
1046
- }
1047
- ),
1048
- !sidebarOpen && /* @__PURE__ */ jsxs5(Fragment, { children: [
1049
- /* @__PURE__ */ jsx7(
1050
- "span",
1051
- {
1052
- style: {
1053
- color: theme.colors.textMuted,
1054
- fontFamily: theme.fontFamily.mono,
1055
- fontSize: theme.fontSize.sm
1056
- },
1057
- children: filteredRecords.length === records.length ? `${records.length} logs` : `${filteredRecords.length} / ${records.length} logs`
1058
- }
1059
- ),
1060
- /* @__PURE__ */ jsx7(
1061
- "button",
1062
- {
1063
- "data-lt-interactive": "",
1064
- onClick: paused ? handleResume : handlePause,
1065
- style: {
1066
- background: paused ? theme.colors.levels.warning.badge : theme.colors.surfaceHover,
1067
- border: `1px solid ${theme.colors.border}`,
1068
- borderRadius: theme.radius.sm,
1069
- color: paused ? theme.colors.white : theme.colors.textPrimary,
1070
- cursor: "pointer",
1071
- fontFamily: theme.fontFamily.sans,
1072
- fontSize: theme.fontSize.sm,
1073
- padding: `${theme.spacing.xs} ${theme.spacing.sm}`,
1074
- transition: "filter 0.1s"
1075
- },
1076
- title: paused ? "Resume" : "Pause",
1077
- type: "button",
1078
- children: paused ? /* @__PURE__ */ jsx7(PlayIcon, { size: 12 }) : /* @__PURE__ */ jsx7(PauseIcon, { size: 12 })
1079
- }
1080
- ),
1081
- /* @__PURE__ */ jsx7(
1082
- "button",
1083
- {
1084
- "data-lt-interactive": "",
1085
- onClick: handleClear,
1086
- style: {
1087
- background: theme.colors.surfaceHover,
1088
- border: `1px solid ${theme.colors.border}`,
1089
- borderRadius: theme.radius.sm,
1090
- color: theme.colors.textPrimary,
1091
- cursor: "pointer",
1092
- fontFamily: theme.fontFamily.sans,
1093
- fontSize: theme.fontSize.sm,
1094
- padding: `${theme.spacing.xs} ${theme.spacing.sm}`,
1095
- transition: "filter 0.1s"
1096
- },
1097
- title: "Clear",
1098
- type: "button",
1099
- children: /* @__PURE__ */ jsx7(Trash2Icon, { size: 12 })
1100
- }
1101
- )
1102
- ] })
1103
- ]
1045
+ title: paused ? "Resume" : "Pause",
1046
+ type: "button",
1047
+ children: paused ? /* @__PURE__ */ jsx7(PlayIcon, { size: 12 }) : /* @__PURE__ */ jsx7(PauseIcon, { size: 12 })
1104
1048
  }
1105
1049
  ),
1106
1050
  /* @__PURE__ */ jsx7(
1107
- LogList,
1051
+ "button",
1108
1052
  {
1109
- autoScroll: !paused,
1110
- expandedId,
1111
- onToggle: setExpandedId,
1112
- records: filteredRecords
1053
+ "data-lt-interactive": "",
1054
+ onClick: handleClear,
1055
+ style: compactButtonStyle,
1056
+ title: "Clear",
1057
+ type: "button",
1058
+ children: /* @__PURE__ */ jsx7(Trash2Icon, { size: 12 })
1113
1059
  }
1114
1060
  )
1115
1061
  ] })
1116
- ]
1117
- }
1118
- );
1062
+ ] }),
1063
+ /* @__PURE__ */ jsx7(
1064
+ LogList,
1065
+ {
1066
+ autoScroll: !paused,
1067
+ expandedId,
1068
+ onToggle: setExpandedId,
1069
+ records: filteredRecords
1070
+ }
1071
+ )
1072
+ ] })
1073
+ ] });
1119
1074
  };
1120
1075
 
1121
1076
  // src/plugin/create-logtape-devtools-plugin.tsx
@@ -1127,113 +1082,19 @@ var createLogTapeDevtoolsPlugin = (options) => ({
1127
1082
  render: /* @__PURE__ */ jsx8(LogTapeDevtoolsPlugin, { store: options?.store ?? defaultLogStore })
1128
1083
  });
1129
1084
 
1130
- // src/sink.ts
1131
- function renderMessage(message) {
1132
- return message.map((part) => {
1133
- if (typeof part === "string") {
1134
- return part;
1135
- }
1136
- if (part === null || part === void 0) {
1137
- return String(part);
1138
- }
1139
- try {
1140
- return JSON.stringify(part);
1141
- } catch {
1142
- return String(part);
1143
- }
1144
- }).join("");
1145
- }
1146
- function safeCloneProperties(properties) {
1147
- try {
1148
- return JSON.parse(JSON.stringify(properties));
1149
- } catch {
1150
- const result = {};
1151
- for (const [key, value] of Object.entries(properties)) {
1152
- try {
1153
- result[key] = JSON.parse(JSON.stringify(value));
1154
- } catch {
1155
- result[key] = String(value);
1156
- }
1157
- }
1158
- return result;
1159
- }
1160
- }
1161
- var v8FrameRe = /at\s+(?:.*?\s+\()?(.+?):(\d+):(\d+)\)?$/;
1162
- var smFrameRe = /(?:^|@)(.+?):(\d+):(\d+)$/;
1163
- function captureCallerInfo() {
1164
- const stack = new Error().stack;
1165
- if (!stack) {
1166
- return void 0;
1167
- }
1168
- const lines = stack.split("\n");
1169
- for (const line of lines) {
1170
- const trimmed = line.trim();
1171
- if (!trimmed || trimmed === "Error") {
1172
- continue;
1173
- }
1174
- if (/logtape/i.test(trimmed)) {
1175
- continue;
1176
- }
1177
- const match = v8FrameRe.exec(trimmed) ?? smFrameRe.exec(trimmed);
1178
- if (!match) {
1179
- continue;
1180
- }
1181
- const [, filePath, lineNo, colNo] = match;
1182
- const fileName = filePath.replace(/[?#].*$/, "").split("/").pop() ?? filePath;
1183
- return `${fileName}:${lineNo}:${colNo}`;
1184
- }
1185
- return void 0;
1186
- }
1187
- function normalizeRecord(record, captureStack, id) {
1188
- const normalized = {
1189
- id,
1190
- timestamp: record.timestamp,
1191
- level: record.level,
1192
- category: [...record.category],
1193
- message: [...record.message],
1194
- messageText: renderMessage(record.message),
1195
- properties: safeCloneProperties(record.properties)
1196
- };
1197
- if (captureStack) {
1198
- const caller = captureCallerInfo();
1199
- if (caller) {
1200
- normalized.caller = caller;
1201
- }
1202
- }
1203
- return normalized;
1204
- }
1205
- function isProductionEnv() {
1206
- try {
1207
- if (typeof process !== "undefined" && process.env?.NODE_ENV === "production") {
1208
- return true;
1209
- }
1210
- } catch {
1211
- }
1212
- return false;
1213
- }
1214
- function createDevtoolsSink(options) {
1085
+ // src/create-logtape-devtools.ts
1086
+ function createLogTapeDevtools(options) {
1215
1087
  const store = options?.store ?? defaultLogStore;
1216
- const wantsStack = options?.experimentalCaptureStackTrace ?? true;
1217
- const captureStack = wantsStack && (options?.forceStackTrace || !isProductionEnv());
1218
- const sinkId = Math.random().toString(36).slice(2, 8);
1219
- let counter = 0;
1220
- return (record) => {
1221
- try {
1222
- const normalized = normalizeRecord(
1223
- record,
1224
- captureStack,
1225
- `log-${sinkId}-${++counter}-${record.timestamp}`
1226
- );
1227
- store.addRecord(normalized);
1228
- } catch {
1229
- }
1088
+ return {
1089
+ plugin: createLogTapeDevtoolsPlugin({ ...options?.plugin, store }),
1090
+ sink: createDevtoolsSink({ ...options?.sink, store })
1230
1091
  };
1231
1092
  }
1232
1093
  export {
1233
1094
  LOG_LEVELS,
1234
- LogTapeDevtoolsPlugin,
1235
1095
  createDevtoolsSink,
1236
1096
  createLogStore,
1097
+ createLogTapeDevtools,
1237
1098
  createLogTapeDevtoolsPlugin,
1238
1099
  defaultLogStore
1239
1100
  };