@mugenlabs/logtape-devtools 0.2.1 → 0.3.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,48 +1,17 @@
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-CYXOBB6A.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 = {
@@ -102,6 +71,17 @@ var theme = {
102
71
  // src/plugin/components/log-row.tsx
103
72
  import { useCallback } from "react";
104
73
 
74
+ // src/plugin/format.ts
75
+ function formatTime(ts) {
76
+ const d = new Date(ts);
77
+ return d.toLocaleTimeString(void 0, {
78
+ hour12: false,
79
+ hour: "2-digit",
80
+ minute: "2-digit",
81
+ second: "2-digit"
82
+ });
83
+ }
84
+
105
85
  // src/plugin/icons.tsx
106
86
  import { jsx, jsxs } from "react/jsx-runtime";
107
87
  var defaults = { size: 14, color: "currentColor" };
@@ -192,91 +172,120 @@ var LevelBadge = ({ level }) => {
192
172
 
193
173
  // src/plugin/components/log-detail.tsx
194
174
  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 })
175
+ var detailRowStyle = {
176
+ display: "flex",
177
+ gap: theme.spacing.lg,
178
+ marginBottom: theme.spacing.md
179
+ };
180
+ var detailLabelStyle = {
181
+ color: theme.colors.textMuted,
182
+ flexShrink: 0,
183
+ fontSize: theme.fontSize.md,
184
+ minWidth: "72px",
185
+ textAlign: "right"
186
+ };
187
+ var detailValueStyle = {
188
+ fontSize: theme.fontSize.base,
189
+ wordBreak: "break-all"
190
+ };
191
+ var containerStyle = {
192
+ borderTop: `1px solid ${theme.colors.border}`,
193
+ padding: `${theme.spacing.lg} ${theme.spacing.xl}`
194
+ };
195
+ var categoryStyle = {
196
+ color: theme.colors.accent,
197
+ fontFamily: theme.fontFamily.mono
198
+ };
199
+ var callerStyle = {
200
+ color: theme.colors.textSecondary,
201
+ fontFamily: theme.fontFamily.mono
202
+ };
203
+ var messageStyle = { color: theme.colors.textPrimary };
204
+ var propertiesStyle = {
205
+ background: "rgba(0,0,0,0.3)",
206
+ borderRadius: theme.radius.md,
207
+ color: theme.colors.textPrimary,
208
+ fontFamily: theme.fontFamily.mono,
209
+ fontSize: theme.fontSize.md,
210
+ margin: 0,
211
+ maxHeight: "200px",
212
+ overflow: "auto",
213
+ padding: theme.spacing.lg,
214
+ whiteSpace: "pre-wrap",
215
+ wordBreak: "break-all"
216
+ };
217
+ var DetailRow = ({ label, children }) => /* @__PURE__ */ jsxs2("div", { style: detailRowStyle, children: [
218
+ /* @__PURE__ */ jsx3("span", { style: detailLabelStyle, children: label }),
219
+ /* @__PURE__ */ jsx3("span", { style: detailValueStyle, children })
226
220
  ] });
227
221
  var LogDetail = ({ record }) => {
228
222
  const levelColors = theme.colors.levels[record.level];
229
223
  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
- );
224
+ return /* @__PURE__ */ jsxs2("div", { "data-testid": "log-detail", style: { ...containerStyle, background: levelColors.bg }, children: [
225
+ /* @__PURE__ */ jsx3(DetailRow, { label: "Time", children: formatTime(record.timestamp) }),
226
+ /* @__PURE__ */ jsx3(DetailRow, { label: "Level", children: /* @__PURE__ */ jsx3("span", { style: { color: levelColors.color, fontWeight: 600 }, children: record.level }) }),
227
+ /* @__PURE__ */ jsx3(DetailRow, { label: "Category", children: /* @__PURE__ */ jsx3("span", { style: categoryStyle, children: record.category.join(".") }) }),
228
+ record.caller && /* @__PURE__ */ jsx3(DetailRow, { label: "Caller", children: /* @__PURE__ */ jsx3("span", { style: callerStyle, children: record.caller }) }),
229
+ /* @__PURE__ */ jsx3(DetailRow, { label: "Message", children: /* @__PURE__ */ jsx3("span", { style: messageStyle, children: record.messageText }) }),
230
+ hasProperties && /* @__PURE__ */ jsx3(DetailRow, { label: "Data", children: /* @__PURE__ */ jsx3("pre", { style: propertiesStyle, children: safeStringify(record.properties, 2) }) })
231
+ ] });
267
232
  };
268
233
 
269
234
  // src/plugin/components/log-row.tsx
270
235
  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
- }
236
+ var rowStyle = {
237
+ alignItems: "center",
238
+ border: "none",
239
+ borderBottom: `1px solid ${theme.colors.border}`,
240
+ cursor: "pointer",
241
+ display: "flex",
242
+ gap: theme.spacing.lg,
243
+ padding: `${theme.spacing.md} ${theme.spacing.xl}`,
244
+ textAlign: "left",
245
+ transition: "background 0.1s, filter 0.1s",
246
+ width: "100%"
247
+ };
248
+ var timestampStyle = {
249
+ color: theme.colors.textDimmed,
250
+ flexShrink: 0,
251
+ fontFamily: theme.fontFamily.mono,
252
+ fontSize: theme.fontSize.md
253
+ };
254
+ var categoryStyle2 = {
255
+ color: theme.colors.accent,
256
+ flexShrink: 0,
257
+ fontFamily: theme.fontFamily.mono,
258
+ fontSize: theme.fontSize.md,
259
+ maxWidth: "180px",
260
+ overflow: "hidden",
261
+ textOverflow: "ellipsis",
262
+ whiteSpace: "nowrap"
263
+ };
264
+ var messageStyle2 = {
265
+ color: theme.colors.textPrimary,
266
+ flex: 1,
267
+ fontSize: theme.fontSize.base,
268
+ overflow: "hidden",
269
+ textOverflow: "ellipsis",
270
+ whiteSpace: "nowrap"
271
+ };
272
+ var callerStyle2 = {
273
+ color: theme.colors.textDimmed,
274
+ flexShrink: 0,
275
+ fontFamily: theme.fontFamily.mono,
276
+ fontSize: theme.fontSize.sm,
277
+ maxWidth: "200px",
278
+ opacity: 0.7,
279
+ overflow: "hidden",
280
+ textOverflow: "ellipsis",
281
+ whiteSpace: "nowrap"
282
+ };
283
+ var chevronStyle = {
284
+ color: theme.colors.textDimmed,
285
+ display: "flex",
286
+ flexShrink: 0,
287
+ transition: "transform 0.15s"
288
+ };
280
289
  var LogRow = ({ record, expanded, onToggle }) => {
281
290
  const levelColors = theme.colors.levels[record.level];
282
291
  const handleClick = useCallback(() => {
@@ -290,94 +299,15 @@ var LogRow = ({ record, expanded, onToggle }) => {
290
299
  "data-lt-interactive": "",
291
300
  "data-testid": "log-row",
292
301
  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
- },
302
+ style: { ...rowStyle, background: expanded ? levelColors.bg : "transparent" },
306
303
  type: "button",
307
304
  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
- ),
305
+ /* @__PURE__ */ jsx4("span", { style: timestampStyle, children: formatTime(record.timestamp) }),
320
306
  /* @__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
- )
307
+ /* @__PURE__ */ jsx4("span", { style: categoryStyle2, children: record.category.join(".") }),
308
+ /* @__PURE__ */ jsx4("span", { style: messageStyle2, children: record.messageText }),
309
+ record.caller && /* @__PURE__ */ jsx4("span", { style: callerStyle2, children: record.caller }),
310
+ /* @__PURE__ */ jsx4("span", { style: { ...chevronStyle, transform: expanded ? "rotate(90deg)" : "rotate(0deg)" }, children: /* @__PURE__ */ jsx4(ChevronRightIcon, { size: 12 }) })
381
311
  ]
382
312
  }
383
313
  ),
@@ -387,76 +317,92 @@ var LogRow = ({ record, expanded, onToggle }) => {
387
317
 
388
318
  // src/plugin/components/log-list.tsx
389
319
  import { jsx as jsx5 } from "react/jsx-runtime";
320
+ var ESTIMATED_ROW_HEIGHT = 28;
321
+ var OVERSCAN = 10;
322
+ var STICK_THRESHOLD = 40;
323
+ var emptyStyle = {
324
+ alignItems: "center",
325
+ color: theme.colors.textMuted,
326
+ display: "flex",
327
+ flex: 1,
328
+ fontSize: theme.fontSize.lg,
329
+ justifyContent: "center"
330
+ };
331
+ var containerStyle2 = {
332
+ flex: 1,
333
+ overflow: "auto",
334
+ scrollbarColor: `${theme.colors.scrollbar} transparent`,
335
+ scrollbarWidth: "thin"
336
+ };
390
337
  var LogList = ({ records, expandedId, onToggle, autoScroll }) => {
391
338
  const containerRef = useRef(null);
392
339
  const isAtBottomRef = useRef(true);
340
+ const virtualizer = useVirtualizer({
341
+ count: records.length,
342
+ estimateSize: () => ESTIMATED_ROW_HEIGHT,
343
+ getItemKey: (index) => records[index].id,
344
+ getScrollElement: () => containerRef.current,
345
+ overscan: OVERSCAN
346
+ });
393
347
  const recordCount = records.length;
394
348
  useEffect(() => {
395
- const el = containerRef.current;
396
- if (!(el && autoScroll && isAtBottomRef.current)) {
349
+ if (recordCount === 0 || !(autoScroll && isAtBottomRef.current)) {
397
350
  return;
398
351
  }
399
- el.scrollTop = el.scrollHeight;
400
- }, [autoScroll, recordCount]);
401
- const handleScroll = () => {
352
+ virtualizer.scrollToIndex(recordCount - 1, { align: "end" });
353
+ }, [autoScroll, recordCount, virtualizer]);
354
+ const handleScroll = useCallback2(() => {
402
355
  const el = containerRef.current;
403
356
  if (!el) {
404
357
  return;
405
358
  }
406
- isAtBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 40;
407
- };
359
+ isAtBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < STICK_THRESHOLD;
360
+ }, []);
408
361
  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
- );
362
+ return /* @__PURE__ */ jsx5("div", { "data-testid": "log-list-empty", style: emptyStyle, children: "No logs yet" });
424
363
  }
425
- return /* @__PURE__ */ jsx5(
364
+ const virtualItems = virtualizer.getVirtualItems();
365
+ return /* @__PURE__ */ jsx5("div", { "data-testid": "log-list", onScroll: handleScroll, ref: containerRef, style: containerStyle2, children: /* @__PURE__ */ jsx5(
426
366
  "div",
427
367
  {
428
- "data-testid": "log-list",
429
- onScroll: handleScroll,
430
- ref: containerRef,
431
368
  style: {
432
- flex: 1,
433
- overflow: "auto",
434
- scrollbarColor: `${theme.colors.scrollbar} transparent`,
435
- scrollbarWidth: "thin"
369
+ height: `${virtualizer.getTotalSize()}px`,
370
+ position: "relative",
371
+ width: "100%"
436
372
  },
437
- children: records.map((record) => /* @__PURE__ */ jsx5(
438
- LogRow,
373
+ children: /* @__PURE__ */ jsx5(
374
+ "div",
439
375
  {
440
- expanded: expandedId === record.id,
441
- onToggle,
442
- record
443
- },
444
- record.id
445
- ))
376
+ style: {
377
+ left: 0,
378
+ position: "absolute",
379
+ top: 0,
380
+ transform: `translateY(${virtualItems[0]?.start ?? 0}px)`,
381
+ width: "100%"
382
+ },
383
+ children: virtualItems.map((virtualItem) => {
384
+ const record = records[virtualItem.index];
385
+ return /* @__PURE__ */ jsx5(
386
+ "div",
387
+ {
388
+ "data-index": virtualItem.index,
389
+ ref: virtualizer.measureElement,
390
+ children: /* @__PURE__ */ jsx5(LogRow, { expanded: expandedId === record.id, onToggle, record })
391
+ },
392
+ virtualItem.key
393
+ );
394
+ })
395
+ }
396
+ )
446
397
  }
447
- );
398
+ ) });
448
399
  };
449
400
 
450
401
  // src/plugin/components/toolbar.tsx
451
402
  import { Combobox } from "@base-ui-components/react/combobox";
452
403
  import { Popover } from "@base-ui-components/react/popover";
453
404
  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
405
+ import { useCallback as useCallback3, useEffect as useEffect2, useMemo, useState } from "react";
460
406
  import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
461
407
  var HOVER_STYLE_ID = "logtape-devtools-hover";
462
408
  function useHoverStyles() {
@@ -521,7 +467,7 @@ var LevelToggle = ({
521
467
  onToggle
522
468
  }) => {
523
469
  const colors = theme.colors.levels[level];
524
- const handleClick = useCallback2(() => onToggle(level), [level, onToggle]);
470
+ const handleClick = useCallback3(() => onToggle(level), [level, onToggle]);
525
471
  return /* @__PURE__ */ jsx6(
526
472
  "button",
527
473
  {
@@ -561,32 +507,26 @@ var Toolbar = ({
561
507
  categories,
562
508
  categoryFilter,
563
509
  onCategoryFilterChange,
564
- searchText,
565
510
  onSearchTextChange,
566
511
  totalCount,
567
512
  filteredCount
568
513
  }) => {
569
514
  useHoverStyles();
570
- const [localSearch, setLocalSearch] = useState(searchText);
515
+ const [search, setSearch] = useState("");
571
516
  const debouncedSearch = useMemo(
572
517
  () => new Debouncer(onSearchTextChange, { wait: 200 }),
573
518
  [onSearchTextChange]
574
519
  );
575
520
  useEffect2(() => () => debouncedSearch.cancel(), [debouncedSearch]);
576
- const handleSearchChange = useCallback2(
521
+ const handleSearchChange = useCallback3(
577
522
  (e) => {
578
523
  const value = e.target.value;
579
- setLocalSearch(value);
524
+ setSearch(value);
580
525
  debouncedSearch.maybeExecute(value);
581
526
  },
582
527
  [debouncedSearch]
583
528
  );
584
- useEffect2(() => {
585
- if (searchText === "" && localSearch !== "") {
586
- setLocalSearch("");
587
- }
588
- }, [searchText, localSearch]);
589
- const handleLevelToggle = useCallback2(
529
+ const handleLevelToggle = useCallback3(
590
530
  (level) => {
591
531
  const next = new Set(levelFilter);
592
532
  if (next.has(level)) {
@@ -906,7 +846,7 @@ var Toolbar = ({
906
846
  placeholder: "Search\u2026",
907
847
  style: inputStyle,
908
848
  type: "text",
909
- value: localSearch
849
+ value: search
910
850
  }
911
851
  )
912
852
  ] })
@@ -917,6 +857,61 @@ var Toolbar = ({
917
857
 
918
858
  // src/plugin/logtape-devtools-plugin.tsx
919
859
  import { Fragment, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
860
+ var rootStyle = {
861
+ background: theme.colors.background,
862
+ color: theme.colors.textPrimary,
863
+ display: "flex",
864
+ flexDirection: "row",
865
+ fontFamily: theme.fontFamily.sans,
866
+ height: "100%",
867
+ overflow: "hidden"
868
+ };
869
+ var sidebarStyle = {
870
+ flexShrink: 0,
871
+ overflow: "hidden",
872
+ transition: "width 0.2s ease, opacity 0.15s ease"
873
+ };
874
+ var mainStyle = {
875
+ display: "flex",
876
+ flex: 1,
877
+ flexDirection: "column",
878
+ overflow: "hidden"
879
+ };
880
+ var topBarStyle = {
881
+ alignItems: "center",
882
+ borderBottom: `1px solid ${theme.colors.border}`,
883
+ display: "flex",
884
+ flexShrink: 0,
885
+ gap: theme.spacing.lg,
886
+ padding: `${theme.spacing.sm} ${theme.spacing.lg}`
887
+ };
888
+ var sidebarToggleStyle = {
889
+ background: "none",
890
+ border: "none",
891
+ color: theme.colors.textMuted,
892
+ cursor: "pointer",
893
+ fontFamily: theme.fontFamily.sans,
894
+ fontSize: theme.fontSize.md,
895
+ padding: `${theme.spacing.xs} ${theme.spacing.sm}`,
896
+ transition: "color 0.1s"
897
+ };
898
+ var iconLabelStyle = { alignItems: "center", display: "inline-flex", gap: "6px" };
899
+ var countStyle = {
900
+ color: theme.colors.textMuted,
901
+ fontFamily: theme.fontFamily.mono,
902
+ fontSize: theme.fontSize.sm
903
+ };
904
+ var compactButtonStyle = {
905
+ background: theme.colors.surfaceHover,
906
+ border: `1px solid ${theme.colors.border}`,
907
+ borderRadius: theme.radius.sm,
908
+ color: theme.colors.textPrimary,
909
+ cursor: "pointer",
910
+ fontFamily: theme.fontFamily.sans,
911
+ fontSize: theme.fontSize.sm,
912
+ padding: `${theme.spacing.xs} ${theme.spacing.sm}`,
913
+ transition: "filter 0.1s"
914
+ };
920
915
  var LogTapeDevtoolsPlugin = ({ store }) => {
921
916
  const allRecords = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
922
917
  const [paused, setPaused] = useState2(false);
@@ -926,15 +921,15 @@ var LogTapeDevtoolsPlugin = ({ store }) => {
926
921
  const [searchText, setSearchText] = useState2("");
927
922
  const [expandedId, setExpandedId] = useState2(null);
928
923
  const [sidebarOpen, setSidebarOpen] = useState2(true);
929
- const handlePause = useCallback3(() => {
924
+ const handlePause = useCallback4(() => {
930
925
  setPaused(true);
931
926
  setPausedRecords(store.getSnapshot());
932
927
  }, [store]);
933
- const handleResume = useCallback3(() => {
928
+ const handleResume = useCallback4(() => {
934
929
  setPaused(false);
935
930
  setPausedRecords([]);
936
931
  }, []);
937
- const handleClear = useCallback3(() => {
932
+ const handleClear = useCallback4(() => {
938
933
  store.clear();
939
934
  setPausedRecords([]);
940
935
  setExpandedId(null);
@@ -964,158 +959,93 @@ var LogTapeDevtoolsPlugin = ({ store }) => {
964
959
  return true;
965
960
  });
966
961
  }, [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: [
962
+ return /* @__PURE__ */ jsxs5("div", { style: rootStyle, children: [
963
+ /* @__PURE__ */ jsx7(
964
+ "div",
965
+ {
966
+ style: {
967
+ ...sidebarStyle,
968
+ borderRight: sidebarOpen ? `1px solid ${theme.colors.border}` : "none",
969
+ opacity: sidebarOpen ? 1 : 0,
970
+ width: sidebarOpen ? "200px" : "0px"
971
+ },
972
+ children: /* @__PURE__ */ jsx7(
973
+ Toolbar,
974
+ {
975
+ categories: uniqueCategories,
976
+ categoryFilter,
977
+ filteredCount: filteredRecords.length,
978
+ levelFilter,
979
+ onCategoryFilterChange: setCategoryFilter,
980
+ onClear: handleClear,
981
+ onLevelFilterChange: setLevelFilter,
982
+ onPause: handlePause,
983
+ onResume: handleResume,
984
+ onSearchTextChange: setSearchText,
985
+ paused,
986
+ totalCount: records.length
987
+ }
988
+ )
989
+ }
990
+ ),
991
+ /* @__PURE__ */ jsxs5("div", { style: mainStyle, children: [
992
+ /* @__PURE__ */ jsxs5("div", { style: topBarStyle, children: [
980
993
  /* @__PURE__ */ jsx7(
981
- "div",
994
+ "button",
982
995
  {
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
- )
996
+ "aria-expanded": sidebarOpen,
997
+ "data-lt-clear-btn": "",
998
+ onClick: () => setSidebarOpen((v) => !v),
999
+ style: sidebarToggleStyle,
1000
+ title: sidebarOpen ? "Collapse sidebar" : "Expand sidebar",
1001
+ type: "button",
1002
+ children: /* @__PURE__ */ jsxs5("span", { style: iconLabelStyle, children: [
1003
+ sidebarOpen ? /* @__PURE__ */ jsx7(PanelLeftCloseIcon, { size: 14 }) : /* @__PURE__ */ jsx7(PanelLeftOpenIcon, { size: 14 }),
1004
+ sidebarOpen ? "Hide controls" : "Show controls"
1005
+ ] })
1009
1006
  }
1010
1007
  ),
1011
- /* @__PURE__ */ jsxs5("div", { style: { display: "flex", flex: 1, flexDirection: "column", overflow: "hidden" }, children: [
1012
- /* @__PURE__ */ jsxs5(
1013
- "div",
1008
+ !sidebarOpen && /* @__PURE__ */ jsxs5(Fragment, { children: [
1009
+ /* @__PURE__ */ jsx7("span", { style: countStyle, children: filteredRecords.length === records.length ? `${records.length} logs` : `${filteredRecords.length} / ${records.length} logs` }),
1010
+ /* @__PURE__ */ jsx7(
1011
+ "button",
1014
1012
  {
1013
+ "data-lt-interactive": "",
1014
+ onClick: paused ? handleResume : handlePause,
1015
1015
  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}`
1016
+ ...compactButtonStyle,
1017
+ background: paused ? theme.colors.levels.warning.badge : compactButtonStyle.background,
1018
+ color: paused ? theme.colors.white : compactButtonStyle.color
1022
1019
  },
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
- ]
1020
+ title: paused ? "Resume" : "Pause",
1021
+ type: "button",
1022
+ children: paused ? /* @__PURE__ */ jsx7(PlayIcon, { size: 12 }) : /* @__PURE__ */ jsx7(PauseIcon, { size: 12 })
1104
1023
  }
1105
1024
  ),
1106
1025
  /* @__PURE__ */ jsx7(
1107
- LogList,
1026
+ "button",
1108
1027
  {
1109
- autoScroll: !paused,
1110
- expandedId,
1111
- onToggle: setExpandedId,
1112
- records: filteredRecords
1028
+ "data-lt-interactive": "",
1029
+ onClick: handleClear,
1030
+ style: compactButtonStyle,
1031
+ title: "Clear",
1032
+ type: "button",
1033
+ children: /* @__PURE__ */ jsx7(Trash2Icon, { size: 12 })
1113
1034
  }
1114
1035
  )
1115
1036
  ] })
1116
- ]
1117
- }
1118
- );
1037
+ ] }),
1038
+ /* @__PURE__ */ jsx7(
1039
+ LogList,
1040
+ {
1041
+ autoScroll: !paused,
1042
+ expandedId,
1043
+ onToggle: setExpandedId,
1044
+ records: filteredRecords
1045
+ }
1046
+ )
1047
+ ] })
1048
+ ] });
1119
1049
  };
1120
1050
 
1121
1051
  // src/plugin/create-logtape-devtools-plugin.tsx
@@ -1127,113 +1057,19 @@ var createLogTapeDevtoolsPlugin = (options) => ({
1127
1057
  render: /* @__PURE__ */ jsx8(LogTapeDevtoolsPlugin, { store: options?.store ?? defaultLogStore })
1128
1058
  });
1129
1059
 
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) {
1060
+ // src/create-logtape-devtools.ts
1061
+ function createLogTapeDevtools(options) {
1215
1062
  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
- }
1063
+ return {
1064
+ plugin: createLogTapeDevtoolsPlugin({ ...options?.plugin, store }),
1065
+ sink: createDevtoolsSink({ ...options?.sink, store })
1230
1066
  };
1231
1067
  }
1232
1068
  export {
1233
1069
  LOG_LEVELS,
1234
- LogTapeDevtoolsPlugin,
1235
1070
  createDevtoolsSink,
1236
1071
  createLogStore,
1072
+ createLogTapeDevtools,
1237
1073
  createLogTapeDevtoolsPlugin,
1238
1074
  defaultLogStore
1239
1075
  };