@leftium/gg 0.0.40 → 0.0.41
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/eruda/plugin.js +615 -206
- package/package.json +1 -1
package/dist/eruda/plugin.js
CHANGED
|
@@ -21,10 +21,16 @@ export function createGgPlugin(options, gg) {
|
|
|
21
21
|
const enabledNamespaces = new Set();
|
|
22
22
|
// Last rendered entries (for hover tooltip arg lookup)
|
|
23
23
|
let renderedEntries = [];
|
|
24
|
+
// Toast state for "namespace hidden" feedback
|
|
25
|
+
let lastHiddenPattern = null; // filterPattern before the hide (for undo)
|
|
26
|
+
let hasSeenToastExplanation = false; // first toast auto-expands help text
|
|
24
27
|
// Settings UI state
|
|
25
28
|
let settingsExpanded = false;
|
|
29
|
+
// Expression visibility toggle
|
|
30
|
+
let showExpressions = false;
|
|
26
31
|
// Filter pattern persistence key (independent of localStorage.debug)
|
|
27
32
|
const FILTER_KEY = 'gg-filter';
|
|
33
|
+
const SHOW_EXPRESSIONS_KEY = 'gg-show-expressions';
|
|
28
34
|
// Namespace click action: 'open' uses Vite dev middleware, 'copy' copies formatted string, 'open-url' navigates to URI
|
|
29
35
|
const NS_ACTION_KEY = 'gg-ns-action';
|
|
30
36
|
const EDITOR_BIN_KEY = 'gg-editor-bin';
|
|
@@ -107,6 +113,7 @@ export function createGgPlugin(options, gg) {
|
|
|
107
113
|
// Load filter state BEFORE registering _onLog hook, because setting _onLog
|
|
108
114
|
// triggers replay of earlyLogBuffer and each entry checks filterPattern
|
|
109
115
|
filterPattern = localStorage.getItem(FILTER_KEY) || 'gg:*';
|
|
116
|
+
showExpressions = localStorage.getItem(SHOW_EXPRESSIONS_KEY) === 'true';
|
|
110
117
|
// Register the capture hook on gg
|
|
111
118
|
if (gg) {
|
|
112
119
|
gg._onLog = (entry) => {
|
|
@@ -155,6 +162,7 @@ export function createGgPlugin(options, gg) {
|
|
|
155
162
|
wireUpResize();
|
|
156
163
|
wireUpFilterUI();
|
|
157
164
|
wireUpSettingsUI();
|
|
165
|
+
wireUpToast();
|
|
158
166
|
renderLogs();
|
|
159
167
|
},
|
|
160
168
|
show() {
|
|
@@ -240,20 +248,6 @@ export function createGgPlugin(options, gg) {
|
|
|
240
248
|
// Persist the new pattern
|
|
241
249
|
localStorage.setItem(FILTER_KEY, filterPattern);
|
|
242
250
|
}
|
|
243
|
-
function soloNamespace(namespace) {
|
|
244
|
-
const ns = namespace.trim();
|
|
245
|
-
// Toggle: if already soloed on this namespace, restore all
|
|
246
|
-
if (filterPattern === ns) {
|
|
247
|
-
filterPattern = 'gg:*';
|
|
248
|
-
enabledNamespaces.clear();
|
|
249
|
-
getAllCapturedNamespaces().forEach((n) => enabledNamespaces.add(n));
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
// Solo: show only this namespace
|
|
253
|
-
filterPattern = ns;
|
|
254
|
-
enabledNamespaces.clear();
|
|
255
|
-
enabledNamespaces.add(ns);
|
|
256
|
-
}
|
|
257
251
|
function simplifyPattern(pattern) {
|
|
258
252
|
if (!pattern)
|
|
259
253
|
return '';
|
|
@@ -331,11 +325,7 @@ export function createGgPlugin(options, gg) {
|
|
|
331
325
|
}
|
|
332
326
|
function gridColumns() {
|
|
333
327
|
const ns = nsColWidth !== null ? `${nsColWidth}px` : 'auto';
|
|
334
|
-
//
|
|
335
|
-
// When collapsed: diff | ns | handle | content
|
|
336
|
-
if (filterExpanded) {
|
|
337
|
-
return `auto auto ${ns} 4px 1fr`;
|
|
338
|
-
}
|
|
328
|
+
// Grid columns: diff | ns | handle | content
|
|
339
329
|
return `auto ${ns} 4px 1fr`;
|
|
340
330
|
}
|
|
341
331
|
function buildHTML() {
|
|
@@ -351,76 +341,46 @@ export function createGgPlugin(options, gg) {
|
|
|
351
341
|
.gg-log-entry {
|
|
352
342
|
display: contents;
|
|
353
343
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
cursor: pointer;
|
|
388
|
-
}
|
|
389
|
-
.gg-solo-target:hover {
|
|
390
|
-
background: rgba(0,0,0,0.05);
|
|
391
|
-
}
|
|
392
|
-
/* Clickable namespace cells with file metadata (open-in-editor) */
|
|
393
|
-
.gg-log-ns[data-file] {
|
|
394
|
-
cursor: pointer;
|
|
395
|
-
text-decoration: underline;
|
|
396
|
-
text-decoration-style: dotted;
|
|
397
|
-
text-underline-offset: 3px;
|
|
398
|
-
}
|
|
399
|
-
.gg-log-ns[data-file]:hover {
|
|
400
|
-
text-decoration-style: solid;
|
|
401
|
-
background: rgba(0,0,0,0.05);
|
|
402
|
-
}
|
|
403
|
-
.gg-log-ns[data-file]::after {
|
|
404
|
-
content: ' \u{1F4CB}';
|
|
405
|
-
font-size: 10px;
|
|
406
|
-
opacity: 0;
|
|
407
|
-
transition: opacity 0.1s;
|
|
408
|
-
}
|
|
409
|
-
.gg-action-open .gg-log-ns[data-file]::after {
|
|
410
|
-
content: ' \u{1F517}';
|
|
411
|
-
}
|
|
412
|
-
.gg-log-ns[data-file]:hover::after {
|
|
413
|
-
opacity: 1;
|
|
414
|
-
}
|
|
415
|
-
/* Clickable namespace segments */
|
|
344
|
+
.gg-log-header {
|
|
345
|
+
display: contents;
|
|
346
|
+
}
|
|
347
|
+
.gg-log-diff,
|
|
348
|
+
.gg-log-ns,
|
|
349
|
+
.gg-log-handle,
|
|
350
|
+
.gg-log-content {
|
|
351
|
+
min-width: 0;
|
|
352
|
+
align-self: start !important;
|
|
353
|
+
border-top: 1px solid rgba(0,0,0,0.05);
|
|
354
|
+
}
|
|
355
|
+
.gg-reset-filter-btn:hover {
|
|
356
|
+
background: #1976D2 !important;
|
|
357
|
+
transform: translateY(-1px);
|
|
358
|
+
box-shadow: 0 2px 8px rgba(33, 150, 243, 0.4);
|
|
359
|
+
}
|
|
360
|
+
.gg-reset-filter-btn:active {
|
|
361
|
+
transform: translateY(0);
|
|
362
|
+
}
|
|
363
|
+
/* Clickable time diff with file metadata (open-in-editor) */
|
|
364
|
+
.gg-log-diff[data-file] {
|
|
365
|
+
cursor: pointer;
|
|
366
|
+
text-decoration: underline;
|
|
367
|
+
text-decoration-style: dotted;
|
|
368
|
+
text-underline-offset: 2px;
|
|
369
|
+
opacity: 0.85;
|
|
370
|
+
}
|
|
371
|
+
.gg-log-diff[data-file]:hover {
|
|
372
|
+
text-decoration-style: solid;
|
|
373
|
+
opacity: 1;
|
|
374
|
+
background: rgba(0,0,0,0.05);
|
|
375
|
+
}
|
|
376
|
+
/* Clickable namespace segments - always enabled for filtering */
|
|
416
377
|
.gg-ns-segment {
|
|
417
378
|
cursor: pointer;
|
|
418
379
|
padding: 1px 2px;
|
|
419
380
|
border-radius: 2px;
|
|
420
381
|
transition: background 0.1s;
|
|
421
382
|
}
|
|
422
|
-
|
|
423
|
-
.gg-log-grid.filter-mode .gg-ns-segment:hover {
|
|
383
|
+
.gg-ns-segment:hover {
|
|
424
384
|
background: rgba(0,0,0,0.1);
|
|
425
385
|
text-decoration: underline;
|
|
426
386
|
text-decoration-style: solid;
|
|
@@ -439,13 +399,138 @@ export function createGgPlugin(options, gg) {
|
|
|
439
399
|
padding: 4px 8px 4px 0;
|
|
440
400
|
white-space: pre;
|
|
441
401
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
402
|
+
.gg-log-ns {
|
|
403
|
+
font-weight: bold;
|
|
404
|
+
white-space: nowrap;
|
|
405
|
+
overflow: hidden;
|
|
406
|
+
padding: 4px 8px 4px 0;
|
|
407
|
+
display: flex;
|
|
408
|
+
align-items: center;
|
|
409
|
+
gap: 6px;
|
|
410
|
+
}
|
|
411
|
+
.gg-ns-text {
|
|
412
|
+
overflow: hidden;
|
|
413
|
+
text-overflow: ellipsis;
|
|
414
|
+
min-width: 0;
|
|
415
|
+
}
|
|
416
|
+
.gg-ns-hide {
|
|
417
|
+
all: unset;
|
|
418
|
+
cursor: pointer;
|
|
419
|
+
opacity: 0;
|
|
420
|
+
font-size: 14px;
|
|
421
|
+
font-weight: bold;
|
|
422
|
+
line-height: 1;
|
|
423
|
+
padding: 1px 4px;
|
|
424
|
+
transition: opacity 0.15s;
|
|
425
|
+
flex-shrink: 0;
|
|
426
|
+
}
|
|
427
|
+
.gg-log-ns:hover .gg-ns-hide {
|
|
428
|
+
opacity: 0.4;
|
|
429
|
+
}
|
|
430
|
+
.gg-ns-hide:hover {
|
|
431
|
+
opacity: 1 !important;
|
|
432
|
+
background: rgba(0,0,0,0.08);
|
|
433
|
+
border-radius: 3px;
|
|
434
|
+
}
|
|
435
|
+
/* Toast bar for "namespace hidden" feedback */
|
|
436
|
+
.gg-toast {
|
|
437
|
+
display: none;
|
|
438
|
+
background: #333;
|
|
439
|
+
color: #e0e0e0;
|
|
440
|
+
font-size: 12px;
|
|
441
|
+
font-family: monospace;
|
|
442
|
+
padding: 8px 12px;
|
|
443
|
+
border-radius: 6px 6px 0 0;
|
|
444
|
+
flex-shrink: 0;
|
|
445
|
+
align-items: center;
|
|
446
|
+
gap: 8px;
|
|
447
|
+
margin-top: 4px;
|
|
448
|
+
animation: gg-toast-slide-up 0.2s ease-out;
|
|
449
|
+
}
|
|
450
|
+
.gg-toast.visible {
|
|
451
|
+
display: flex;
|
|
452
|
+
flex-wrap: wrap;
|
|
453
|
+
}
|
|
454
|
+
@keyframes gg-toast-slide-up {
|
|
455
|
+
from { transform: translateY(100%); opacity: 0; }
|
|
456
|
+
to { transform: translateY(0); opacity: 1; }
|
|
457
|
+
}
|
|
458
|
+
.gg-toast-label {
|
|
459
|
+
opacity: 0.7;
|
|
460
|
+
flex-shrink: 0;
|
|
461
|
+
}
|
|
462
|
+
.gg-toast-ns {
|
|
463
|
+
display: inline-flex;
|
|
464
|
+
align-items: center;
|
|
465
|
+
gap: 0;
|
|
466
|
+
}
|
|
467
|
+
.gg-toast-segment {
|
|
468
|
+
cursor: pointer;
|
|
469
|
+
padding: 1px 3px;
|
|
470
|
+
border-radius: 2px;
|
|
471
|
+
color: #bbb;
|
|
472
|
+
text-decoration: line-through;
|
|
473
|
+
transition: background 0.1s, color 0.1s;
|
|
474
|
+
}
|
|
475
|
+
.gg-toast-segment:hover {
|
|
476
|
+
color: #ef5350;
|
|
477
|
+
background: rgba(239, 83, 80, 0.15);
|
|
478
|
+
}
|
|
479
|
+
.gg-toast-delim {
|
|
480
|
+
opacity: 0.5;
|
|
481
|
+
}
|
|
482
|
+
.gg-toast-actions {
|
|
483
|
+
display: flex;
|
|
484
|
+
align-items: center;
|
|
485
|
+
gap: 6px;
|
|
486
|
+
margin-left: auto;
|
|
487
|
+
flex-shrink: 0;
|
|
488
|
+
}
|
|
489
|
+
.gg-toast-btn {
|
|
490
|
+
all: unset;
|
|
491
|
+
cursor: pointer;
|
|
492
|
+
padding: 2px 8px;
|
|
493
|
+
border-radius: 3px;
|
|
494
|
+
font-size: 11px;
|
|
495
|
+
transition: background 0.1s;
|
|
496
|
+
}
|
|
497
|
+
.gg-toast-undo {
|
|
498
|
+
color: #64b5f6;
|
|
499
|
+
font-weight: bold;
|
|
500
|
+
}
|
|
501
|
+
.gg-toast-undo:hover {
|
|
502
|
+
background: rgba(100, 181, 246, 0.2);
|
|
503
|
+
}
|
|
504
|
+
.gg-toast-help {
|
|
505
|
+
color: #999;
|
|
506
|
+
font-size: 13px;
|
|
507
|
+
line-height: 1;
|
|
508
|
+
}
|
|
509
|
+
.gg-toast-help:hover {
|
|
510
|
+
color: #ccc;
|
|
511
|
+
background: rgba(255,255,255,0.1);
|
|
512
|
+
}
|
|
513
|
+
.gg-toast-dismiss {
|
|
514
|
+
color: #999;
|
|
515
|
+
font-size: 14px;
|
|
516
|
+
line-height: 1;
|
|
517
|
+
}
|
|
518
|
+
.gg-toast-dismiss:hover {
|
|
519
|
+
color: #fff;
|
|
520
|
+
background: rgba(255,255,255,0.1);
|
|
521
|
+
}
|
|
522
|
+
.gg-toast-explanation {
|
|
523
|
+
display: none;
|
|
524
|
+
width: 100%;
|
|
525
|
+
font-size: 11px;
|
|
526
|
+
opacity: 0.6;
|
|
527
|
+
padding-top: 4px;
|
|
528
|
+
margin-top: 4px;
|
|
529
|
+
border-top: 1px solid rgba(255,255,255,0.1);
|
|
530
|
+
}
|
|
531
|
+
.gg-toast-explanation.visible {
|
|
532
|
+
display: block;
|
|
533
|
+
}
|
|
449
534
|
.gg-log-handle {
|
|
450
535
|
width: 4px;
|
|
451
536
|
cursor: col-resize;
|
|
@@ -465,14 +550,14 @@ export function createGgPlugin(options, gg) {
|
|
|
465
550
|
.gg-log-handle.gg-dragging {
|
|
466
551
|
background: rgba(0,0,0,0.15);
|
|
467
552
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
553
|
+
.gg-log-content {
|
|
554
|
+
word-break: break-word;
|
|
555
|
+
padding: 4px 0;
|
|
556
|
+
position: relative;
|
|
557
|
+
-webkit-user-select: text !important;
|
|
558
|
+
user-select: text !important;
|
|
559
|
+
cursor: text;
|
|
560
|
+
}
|
|
476
561
|
.gg-log-content * {
|
|
477
562
|
-webkit-user-select: text !important;
|
|
478
563
|
user-select: text !important;
|
|
@@ -523,6 +608,20 @@ export function createGgPlugin(options, gg) {
|
|
|
523
608
|
.gg-log-content[data-src]:not(:has(.gg-expand)):hover::after {
|
|
524
609
|
opacity: 1;
|
|
525
610
|
}
|
|
611
|
+
/* Inline expression label (shown when expression toggle is on) */
|
|
612
|
+
.gg-inline-expr {
|
|
613
|
+
color: #888;
|
|
614
|
+
font-style: italic;
|
|
615
|
+
font-size: 11px;
|
|
616
|
+
}
|
|
617
|
+
/* When expressions are shown inline, suppress the CSS tooltip and magnifying glass on primitives */
|
|
618
|
+
.gg-show-expr .gg-log-content[data-src] {
|
|
619
|
+
cursor: text;
|
|
620
|
+
}
|
|
621
|
+
.gg-show-expr .gg-log-content[data-src]:not(:has(.gg-expand))::before,
|
|
622
|
+
.gg-show-expr .gg-log-content[data-src]:not(:has(.gg-expand))::after {
|
|
623
|
+
display: none;
|
|
624
|
+
}
|
|
526
625
|
/* Expression icon inline with expandable object labels */
|
|
527
626
|
.gg-src-icon {
|
|
528
627
|
font-size: 10px;
|
|
@@ -776,18 +875,17 @@ export function createGgPlugin(options, gg) {
|
|
|
776
875
|
display: block;
|
|
777
876
|
padding: 8px 0;
|
|
778
877
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
}
|
|
783
|
-
.gg-log-icons,
|
|
784
|
-
.gg-log-diff,
|
|
785
|
-
.gg-log-ns,
|
|
786
|
-
.gg-log-handle,
|
|
787
|
-
.gg-log-content,
|
|
788
|
-
.gg-details {
|
|
789
|
-
border-top: none !important;
|
|
878
|
+
/* Remove double borders on mobile - only border on entry wrapper */
|
|
879
|
+
.gg-log-entry:not(:first-child) {
|
|
880
|
+
border-top: 1px solid rgba(0,0,0,0.05);
|
|
790
881
|
}
|
|
882
|
+
.gg-log-diff,
|
|
883
|
+
.gg-log-ns,
|
|
884
|
+
.gg-log-handle,
|
|
885
|
+
.gg-log-content,
|
|
886
|
+
.gg-details {
|
|
887
|
+
border-top: none !important;
|
|
888
|
+
}
|
|
791
889
|
.gg-log-header {
|
|
792
890
|
display: flex;
|
|
793
891
|
align-items: center;
|
|
@@ -824,7 +922,7 @@ export function createGgPlugin(options, gg) {
|
|
|
824
922
|
<div class="eruda-gg${nsClickAction === 'open' || nsClickAction === 'open-url' ? ' gg-action-open' : ''}" style="padding: 10px; height: 100%; display: flex; flex-direction: column; font-size: 14px; touch-action: none; overscroll-behavior: contain;">
|
|
825
923
|
<div class="gg-toolbar">
|
|
826
924
|
<button class="gg-copy-btn">
|
|
827
|
-
<span class="gg-btn-text">Copy</span>
|
|
925
|
+
<span class="gg-btn-text">📋 <span class="gg-copy-count">Copy 0 entries</span></span>
|
|
828
926
|
<span class="gg-btn-icon" title="Copy">📋</span>
|
|
829
927
|
</button>
|
|
830
928
|
<button class="gg-filter-btn" style="text-align: left; white-space: nowrap;">
|
|
@@ -832,11 +930,15 @@ export function createGgPlugin(options, gg) {
|
|
|
832
930
|
<span class="gg-btn-icon">NS: </span>
|
|
833
931
|
<span class="gg-filter-summary"></span>
|
|
834
932
|
</button>
|
|
933
|
+
<button class="gg-expressions-btn" style="background: ${showExpressions ? '#e8f5e9' : 'transparent'};" title="Toggle expression visibility in logs and clipboard">
|
|
934
|
+
<span class="gg-btn-text">\uD83D\uDD0D Expr</span>
|
|
935
|
+
<span class="gg-btn-icon" title="Expressions">\uD83D\uDD0D</span>
|
|
936
|
+
</button>
|
|
937
|
+
<span style="flex: 1;"></span>
|
|
835
938
|
<button class="gg-settings-btn">
|
|
836
939
|
<span class="gg-btn-text">⚙️ Settings</span>
|
|
837
940
|
<span class="gg-btn-icon" title="Settings">⚙️</span>
|
|
838
941
|
</button>
|
|
839
|
-
<span class="gg-count" style="opacity: 0.6; white-space: nowrap; flex: 1; text-align: right;"></span>
|
|
840
942
|
<button class="gg-clear-btn">
|
|
841
943
|
<span class="gg-btn-text">Clear</span>
|
|
842
944
|
<span class="gg-btn-icon" title="Clear">⊘</span>
|
|
@@ -844,7 +946,8 @@ export function createGgPlugin(options, gg) {
|
|
|
844
946
|
</div>
|
|
845
947
|
<div class="gg-filter-panel"></div>
|
|
846
948
|
<div class="gg-settings-panel"></div>
|
|
847
|
-
<div class="gg-log-container" style="flex: 1; overflow-y: auto; font-family: monospace; font-size: 12px; touch-action: pan-y; overscroll-behavior: contain;"></div>
|
|
949
|
+
<div class="gg-log-container" style="flex: 1; overflow-y: auto; overflow-x: hidden; font-family: monospace; font-size: 12px; touch-action: pan-y; overscroll-behavior: contain;"></div>
|
|
950
|
+
<div class="gg-toast"></div>
|
|
848
951
|
<iframe class="gg-editor-iframe" hidden title="open-in-editor"></iframe>
|
|
849
952
|
</div>
|
|
850
953
|
`;
|
|
@@ -988,34 +1091,34 @@ export function createGgPlugin(options, gg) {
|
|
|
988
1091
|
const otherChecked = otherEnabledCount > 0;
|
|
989
1092
|
const otherCount = otherNamespaces.reduce((sum, ns) => sum + (nsCounts.get(ns) || 0), 0);
|
|
990
1093
|
checkboxesHTML = `
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1094
|
+
<div class="gg-filter-checkboxes">
|
|
1095
|
+
<label class="gg-filter-checkbox" style="font-weight: bold;">
|
|
1096
|
+
<input type="checkbox" class="gg-all-checkbox" ${allChecked ? 'checked' : ''}>
|
|
1097
|
+
<span>ALL</span>
|
|
1098
|
+
</label>
|
|
1099
|
+
${displayedNamespaces
|
|
997
1100
|
.map((ns) => {
|
|
998
1101
|
// Check if namespace matches the current pattern
|
|
999
1102
|
const checked = namespaceMatchesPattern(ns, effectivePattern);
|
|
1000
1103
|
const count = nsCounts.get(ns) || 0;
|
|
1001
1104
|
return `
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1105
|
+
<label class="gg-filter-checkbox">
|
|
1106
|
+
<input type="checkbox" class="gg-ns-checkbox" data-namespace="${escapeHtml(ns)}" ${checked ? 'checked' : ''}>
|
|
1107
|
+
<span>${escapeHtml(ns)} (${count})</span>
|
|
1108
|
+
</label>
|
|
1109
|
+
`;
|
|
1007
1110
|
})
|
|
1008
1111
|
.join('')}
|
|
1009
|
-
|
|
1112
|
+
${otherTotalCount > 0
|
|
1010
1113
|
? `
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1114
|
+
<label class="gg-filter-checkbox" style="opacity: 0.7;">
|
|
1115
|
+
<input type="checkbox" class="gg-other-checkbox" ${otherChecked ? 'checked' : ''} data-other-namespaces='${JSON.stringify(otherNamespaces)}'>
|
|
1116
|
+
<span>other (${otherCount})</span>
|
|
1117
|
+
</label>
|
|
1118
|
+
`
|
|
1016
1119
|
: ''}
|
|
1017
|
-
|
|
1018
|
-
|
|
1120
|
+
</div>
|
|
1121
|
+
`;
|
|
1019
1122
|
}
|
|
1020
1123
|
else if (!simple) {
|
|
1021
1124
|
checkboxesHTML = `<div style="opacity: 0.6; font-size: 11px; margin: 8px 0;">⚠️ Complex pattern - edit manually (quick filters disabled)</div>`;
|
|
@@ -1257,6 +1360,9 @@ export function createGgPlugin(options, gg) {
|
|
|
1257
1360
|
const time = new Date(e.timestamp).toISOString().slice(11, 19);
|
|
1258
1361
|
// Trim namespace and strip 'gg:' prefix to save tokens
|
|
1259
1362
|
const ns = e.namespace.trim().replace(/^gg:/, '');
|
|
1363
|
+
// Include expression suffix when toggle is enabled
|
|
1364
|
+
const hasSrcExpr = !e.level && e.src?.trim() && !/^['"`]/.test(e.src);
|
|
1365
|
+
const exprSuffix = showExpressions && hasSrcExpr ? ` \u2039${e.src}\u203A` : '';
|
|
1260
1366
|
// Format args: compact JSON for objects, primitives as-is
|
|
1261
1367
|
const argsStr = e.args
|
|
1262
1368
|
.map((arg) => {
|
|
@@ -1267,7 +1373,7 @@ export function createGgPlugin(options, gg) {
|
|
|
1267
1373
|
return stripAnsi(String(arg));
|
|
1268
1374
|
})
|
|
1269
1375
|
.join(' ');
|
|
1270
|
-
return `${time} ${ns} ${argsStr}`;
|
|
1376
|
+
return `${time} ${ns} ${argsStr}${exprSuffix}`;
|
|
1271
1377
|
})
|
|
1272
1378
|
.join('\n');
|
|
1273
1379
|
try {
|
|
@@ -1283,6 +1389,15 @@ export function createGgPlugin(options, gg) {
|
|
|
1283
1389
|
document.body.removeChild(textarea);
|
|
1284
1390
|
}
|
|
1285
1391
|
});
|
|
1392
|
+
$el.find('.gg-expressions-btn').on('click', () => {
|
|
1393
|
+
showExpressions = !showExpressions;
|
|
1394
|
+
localStorage.setItem(SHOW_EXPRESSIONS_KEY, String(showExpressions));
|
|
1395
|
+
// Update button styling inline (toolbar is not re-rendered by renderLogs)
|
|
1396
|
+
const btn = $el.find('.gg-expressions-btn').get(0);
|
|
1397
|
+
if (btn)
|
|
1398
|
+
btn.style.background = showExpressions ? '#e8f5e9' : 'transparent';
|
|
1399
|
+
renderLogs();
|
|
1400
|
+
});
|
|
1286
1401
|
}
|
|
1287
1402
|
/** Substitute format variables ($ROOT, $FILE, $LINE, $COL) in a format string */
|
|
1288
1403
|
function formatString(format, file, line, col) {
|
|
@@ -1336,6 +1451,161 @@ export function createGgPlugin(options, gg) {
|
|
|
1336
1451
|
});
|
|
1337
1452
|
}
|
|
1338
1453
|
}
|
|
1454
|
+
/** Show toast bar after hiding a namespace via the x button */
|
|
1455
|
+
function showHideToast(namespace, previousPattern) {
|
|
1456
|
+
if (!$el)
|
|
1457
|
+
return;
|
|
1458
|
+
lastHiddenPattern = previousPattern;
|
|
1459
|
+
const toast = $el.find('.gg-toast').get(0);
|
|
1460
|
+
if (!toast)
|
|
1461
|
+
return;
|
|
1462
|
+
// Split namespace into segments with delimiters (same logic as log row rendering)
|
|
1463
|
+
const parts = namespace.split(/([:/@ \-_])/);
|
|
1464
|
+
const segments = [];
|
|
1465
|
+
const delimiters = [];
|
|
1466
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1467
|
+
if (i % 2 === 0) {
|
|
1468
|
+
if (parts[i])
|
|
1469
|
+
segments.push(parts[i]);
|
|
1470
|
+
}
|
|
1471
|
+
else {
|
|
1472
|
+
delimiters.push(parts[i]);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
// Build clickable segment HTML
|
|
1476
|
+
let nsHTML = '';
|
|
1477
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1478
|
+
const segment = escapeHtml(segments[i]);
|
|
1479
|
+
// Build filter pattern for this segment level
|
|
1480
|
+
let segFilter = '';
|
|
1481
|
+
for (let j = 0; j <= i; j++) {
|
|
1482
|
+
segFilter += segments[j];
|
|
1483
|
+
if (j < i) {
|
|
1484
|
+
segFilter += delimiters[j];
|
|
1485
|
+
}
|
|
1486
|
+
else if (j < segments.length - 1) {
|
|
1487
|
+
segFilter += delimiters[j] + '*';
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
nsHTML += `<span class="gg-toast-segment" data-filter="${escapeHtml(segFilter)}">${segment}</span>`;
|
|
1491
|
+
if (i < segments.length - 1) {
|
|
1492
|
+
nsHTML += `<span class="gg-toast-delim">${escapeHtml(delimiters[i])}</span>`;
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
// Auto-expand explanation on first use
|
|
1496
|
+
const showExplanation = !hasSeenToastExplanation;
|
|
1497
|
+
toast.innerHTML =
|
|
1498
|
+
`<button class="gg-toast-btn gg-toast-dismiss" title="Dismiss">\u00d7</button>` +
|
|
1499
|
+
`<span class="gg-toast-label">Hidden:</span>` +
|
|
1500
|
+
`<span class="gg-toast-ns">${nsHTML}</span>` +
|
|
1501
|
+
`<span class="gg-toast-actions">` +
|
|
1502
|
+
`<button class="gg-toast-btn gg-toast-undo">Undo</button>` +
|
|
1503
|
+
`<button class="gg-toast-btn gg-toast-help" title="Toggle help">?</button>` +
|
|
1504
|
+
`</span>` +
|
|
1505
|
+
`<div class="gg-toast-explanation${showExplanation ? ' visible' : ''}">` +
|
|
1506
|
+
`Click a segment above to hide all matching namespaces (e.g. click "api" to hide gg:api:*). ` +
|
|
1507
|
+
`Tip: you can also right-click any segment in the log to hide it directly.` +
|
|
1508
|
+
`</div>`;
|
|
1509
|
+
toast.classList.add('visible');
|
|
1510
|
+
if (showExplanation) {
|
|
1511
|
+
hasSeenToastExplanation = true;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
/** Dismiss the toast bar */
|
|
1515
|
+
function dismissToast() {
|
|
1516
|
+
if (!$el)
|
|
1517
|
+
return;
|
|
1518
|
+
const toast = $el.find('.gg-toast').get(0);
|
|
1519
|
+
if (toast) {
|
|
1520
|
+
toast.classList.remove('visible');
|
|
1521
|
+
}
|
|
1522
|
+
lastHiddenPattern = null;
|
|
1523
|
+
}
|
|
1524
|
+
/** Undo the last namespace hide */
|
|
1525
|
+
function undoHide() {
|
|
1526
|
+
if (!$el || lastHiddenPattern === null)
|
|
1527
|
+
return;
|
|
1528
|
+
// Restore the previous filter pattern
|
|
1529
|
+
filterPattern = lastHiddenPattern;
|
|
1530
|
+
localStorage.setItem(FILTER_KEY, filterPattern);
|
|
1531
|
+
// Sync enabledNamespaces from the restored pattern
|
|
1532
|
+
enabledNamespaces.clear();
|
|
1533
|
+
const effectivePattern = filterPattern || 'gg:*';
|
|
1534
|
+
getAllCapturedNamespaces().forEach((ns) => {
|
|
1535
|
+
if (namespaceMatchesPattern(ns, effectivePattern)) {
|
|
1536
|
+
enabledNamespaces.add(ns);
|
|
1537
|
+
}
|
|
1538
|
+
});
|
|
1539
|
+
dismissToast();
|
|
1540
|
+
renderFilterUI();
|
|
1541
|
+
renderLogs();
|
|
1542
|
+
}
|
|
1543
|
+
/** Wire up toast event handlers (called once after init) */
|
|
1544
|
+
function wireUpToast() {
|
|
1545
|
+
if (!$el)
|
|
1546
|
+
return;
|
|
1547
|
+
const toast = $el.find('.gg-toast').get(0);
|
|
1548
|
+
if (!toast)
|
|
1549
|
+
return;
|
|
1550
|
+
toast.addEventListener('click', (e) => {
|
|
1551
|
+
const target = e.target;
|
|
1552
|
+
// Undo button
|
|
1553
|
+
if (target.classList?.contains('gg-toast-undo')) {
|
|
1554
|
+
undoHide();
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
// Dismiss button
|
|
1558
|
+
if (target.classList?.contains('gg-toast-dismiss')) {
|
|
1559
|
+
dismissToast();
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
// Help toggle
|
|
1563
|
+
if (target.classList?.contains('gg-toast-help')) {
|
|
1564
|
+
const explanation = toast.querySelector('.gg-toast-explanation');
|
|
1565
|
+
if (explanation) {
|
|
1566
|
+
explanation.classList.toggle('visible');
|
|
1567
|
+
}
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
// Segment click: add exclusion for that pattern
|
|
1571
|
+
if (target.classList?.contains('gg-toast-segment')) {
|
|
1572
|
+
const filter = target.getAttribute('data-filter');
|
|
1573
|
+
if (!filter)
|
|
1574
|
+
return;
|
|
1575
|
+
// Add exclusion pattern (same logic as right-click segment)
|
|
1576
|
+
const currentPattern = filterPattern || 'gg:*';
|
|
1577
|
+
const exclusion = `-${filter}`;
|
|
1578
|
+
const parts = currentPattern.split(',').map((p) => p.trim());
|
|
1579
|
+
if (parts.includes(exclusion)) {
|
|
1580
|
+
// Already excluded, toggle off
|
|
1581
|
+
filterPattern = parts.filter((p) => p !== exclusion).join(',') || 'gg:*';
|
|
1582
|
+
}
|
|
1583
|
+
else {
|
|
1584
|
+
const hasInclusion = parts.some((p) => !p.startsWith('-'));
|
|
1585
|
+
if (hasInclusion) {
|
|
1586
|
+
filterPattern = `${currentPattern},${exclusion}`;
|
|
1587
|
+
}
|
|
1588
|
+
else {
|
|
1589
|
+
filterPattern = `gg:*,${exclusion}`;
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
filterPattern = simplifyPattern(filterPattern);
|
|
1593
|
+
// Sync enabledNamespaces
|
|
1594
|
+
enabledNamespaces.clear();
|
|
1595
|
+
const effectivePattern = filterPattern || 'gg:*';
|
|
1596
|
+
getAllCapturedNamespaces().forEach((ns) => {
|
|
1597
|
+
if (namespaceMatchesPattern(ns, effectivePattern)) {
|
|
1598
|
+
enabledNamespaces.add(ns);
|
|
1599
|
+
}
|
|
1600
|
+
});
|
|
1601
|
+
localStorage.setItem(FILTER_KEY, filterPattern);
|
|
1602
|
+
dismissToast();
|
|
1603
|
+
renderFilterUI();
|
|
1604
|
+
renderLogs();
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1339
1609
|
function wireUpExpanders() {
|
|
1340
1610
|
if (!$el || expanderAttached)
|
|
1341
1611
|
return;
|
|
@@ -1346,6 +1616,16 @@ export function createGgPlugin(options, gg) {
|
|
|
1346
1616
|
return;
|
|
1347
1617
|
containerEl.addEventListener('click', (e) => {
|
|
1348
1618
|
const target = e.target;
|
|
1619
|
+
// Handle reset filter button (shown when all logs filtered out)
|
|
1620
|
+
if (target?.classList?.contains('gg-reset-filter-btn')) {
|
|
1621
|
+
filterPattern = 'gg:*';
|
|
1622
|
+
enabledNamespaces.clear();
|
|
1623
|
+
getAllCapturedNamespaces().forEach((ns) => enabledNamespaces.add(ns));
|
|
1624
|
+
localStorage.setItem(FILTER_KEY, filterPattern);
|
|
1625
|
+
renderFilterUI();
|
|
1626
|
+
renderLogs();
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1349
1629
|
// Handle expand/collapse
|
|
1350
1630
|
if (target?.classList?.contains('gg-expand')) {
|
|
1351
1631
|
const index = target.getAttribute('data-index');
|
|
@@ -1370,17 +1650,8 @@ export function createGgPlugin(options, gg) {
|
|
|
1370
1650
|
}
|
|
1371
1651
|
return;
|
|
1372
1652
|
}
|
|
1373
|
-
// Handle clicking namespace segments
|
|
1653
|
+
// Handle clicking namespace segments - always filter
|
|
1374
1654
|
if (target?.classList?.contains('gg-ns-segment')) {
|
|
1375
|
-
// When filter is collapsed, open in editor instead of filtering
|
|
1376
|
-
if (!filterExpanded) {
|
|
1377
|
-
const nsContainer = target.closest('.gg-log-ns');
|
|
1378
|
-
if (nsContainer?.hasAttribute('data-file')) {
|
|
1379
|
-
handleNamespaceClick(nsContainer);
|
|
1380
|
-
}
|
|
1381
|
-
return;
|
|
1382
|
-
}
|
|
1383
|
-
// When filter is expanded, apply hierarchical filtering
|
|
1384
1655
|
const filter = target.getAttribute('data-filter');
|
|
1385
1656
|
if (!filter)
|
|
1386
1657
|
return;
|
|
@@ -1402,40 +1673,24 @@ export function createGgPlugin(options, gg) {
|
|
|
1402
1673
|
renderLogs();
|
|
1403
1674
|
return;
|
|
1404
1675
|
}
|
|
1405
|
-
// Handle clicking
|
|
1406
|
-
if (target?.classList?.contains('gg-log-
|
|
1407
|
-
target.hasAttribute('data-file') &&
|
|
1408
|
-
!target.classList.contains('gg-solo-target')) {
|
|
1676
|
+
// Handle clicking time diff to open in editor
|
|
1677
|
+
if (target?.classList?.contains('gg-log-diff') && target.hasAttribute('data-file')) {
|
|
1409
1678
|
handleNamespaceClick(target);
|
|
1410
1679
|
return;
|
|
1411
1680
|
}
|
|
1412
|
-
// Handle
|
|
1413
|
-
if (target?.classList?.contains('gg-
|
|
1414
|
-
target?.classList?.contains('gg-icon-solo')) {
|
|
1415
|
-
const iconsDiv = target.closest('.gg-log-icons');
|
|
1416
|
-
const namespace = iconsDiv?.getAttribute('data-namespace');
|
|
1417
|
-
if (!namespace)
|
|
1418
|
-
return;
|
|
1419
|
-
if (target.classList.contains('gg-icon-hide')) {
|
|
1420
|
-
toggleNamespace(namespace, false);
|
|
1421
|
-
}
|
|
1422
|
-
else {
|
|
1423
|
-
soloNamespace(namespace);
|
|
1424
|
-
}
|
|
1425
|
-
localStorage.setItem(FILTER_KEY, filterPattern);
|
|
1426
|
-
renderFilterUI();
|
|
1427
|
-
renderLogs();
|
|
1428
|
-
return;
|
|
1429
|
-
}
|
|
1430
|
-
// Handle clicking diff/ns cells to solo (same as 🎯)
|
|
1431
|
-
if (target?.classList?.contains('gg-solo-target')) {
|
|
1681
|
+
// Handle clicking hide button for namespace
|
|
1682
|
+
if (target?.classList?.contains('gg-ns-hide')) {
|
|
1432
1683
|
const namespace = target.getAttribute('data-namespace');
|
|
1433
1684
|
if (!namespace)
|
|
1434
1685
|
return;
|
|
1435
|
-
|
|
1686
|
+
// Save current pattern for undo before hiding
|
|
1687
|
+
const previousPattern = filterPattern;
|
|
1688
|
+
toggleNamespace(namespace, false);
|
|
1436
1689
|
localStorage.setItem(FILTER_KEY, filterPattern);
|
|
1437
1690
|
renderFilterUI();
|
|
1438
1691
|
renderLogs();
|
|
1692
|
+
// Show toast with undo option
|
|
1693
|
+
showHideToast(namespace, previousPattern);
|
|
1439
1694
|
return;
|
|
1440
1695
|
}
|
|
1441
1696
|
// Clicking background (container or grid, not a log element) restores all
|
|
@@ -1450,6 +1705,116 @@ export function createGgPlugin(options, gg) {
|
|
|
1450
1705
|
renderLogs();
|
|
1451
1706
|
}
|
|
1452
1707
|
});
|
|
1708
|
+
// Helper: show confirmation tooltip near target element
|
|
1709
|
+
function showConfirmationTooltip(containerEl, target, text) {
|
|
1710
|
+
const tip = containerEl.querySelector('.gg-hover-tooltip');
|
|
1711
|
+
if (!tip)
|
|
1712
|
+
return;
|
|
1713
|
+
tip.textContent = text;
|
|
1714
|
+
tip.style.display = 'block';
|
|
1715
|
+
const targetRect = target.getBoundingClientRect();
|
|
1716
|
+
let left = targetRect.left;
|
|
1717
|
+
let top = targetRect.bottom + 4;
|
|
1718
|
+
const tipRect = tip.getBoundingClientRect();
|
|
1719
|
+
if (left + tipRect.width > window.innerWidth) {
|
|
1720
|
+
left = window.innerWidth - tipRect.width - 8;
|
|
1721
|
+
}
|
|
1722
|
+
if (left < 4)
|
|
1723
|
+
left = 4;
|
|
1724
|
+
if (top + tipRect.height > window.innerHeight) {
|
|
1725
|
+
top = targetRect.top - tipRect.height - 4;
|
|
1726
|
+
}
|
|
1727
|
+
tip.style.left = `${left}px`;
|
|
1728
|
+
tip.style.top = `${top}px`;
|
|
1729
|
+
setTimeout(() => {
|
|
1730
|
+
tip.style.display = 'none';
|
|
1731
|
+
}, 1500);
|
|
1732
|
+
}
|
|
1733
|
+
// Right-click context actions
|
|
1734
|
+
containerEl.addEventListener('contextmenu', (e) => {
|
|
1735
|
+
const target = e.target;
|
|
1736
|
+
// Right-click namespace segment: hide that pattern
|
|
1737
|
+
if (target?.classList?.contains('gg-ns-segment')) {
|
|
1738
|
+
const filter = target.getAttribute('data-filter');
|
|
1739
|
+
if (!filter)
|
|
1740
|
+
return;
|
|
1741
|
+
e.preventDefault();
|
|
1742
|
+
// Add exclusion pattern: keep current base, add -<pattern>
|
|
1743
|
+
const currentPattern = filterPattern || 'gg:*';
|
|
1744
|
+
const exclusion = `-${filter}`;
|
|
1745
|
+
// Check if already excluded (toggle off)
|
|
1746
|
+
const parts = currentPattern.split(',').map((p) => p.trim());
|
|
1747
|
+
if (parts.includes(exclusion)) {
|
|
1748
|
+
// Remove the exclusion to un-hide
|
|
1749
|
+
filterPattern = parts.filter((p) => p !== exclusion).join(',') || 'gg:*';
|
|
1750
|
+
}
|
|
1751
|
+
else {
|
|
1752
|
+
// Ensure we have a base inclusion pattern
|
|
1753
|
+
const hasInclusion = parts.some((p) => !p.startsWith('-'));
|
|
1754
|
+
if (hasInclusion) {
|
|
1755
|
+
filterPattern = `${currentPattern},${exclusion}`;
|
|
1756
|
+
}
|
|
1757
|
+
else {
|
|
1758
|
+
filterPattern = `gg:*,${exclusion}`;
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
filterPattern = simplifyPattern(filterPattern);
|
|
1762
|
+
// Sync enabledNamespaces from the new pattern
|
|
1763
|
+
enabledNamespaces.clear();
|
|
1764
|
+
const effectivePattern = filterPattern || 'gg:*';
|
|
1765
|
+
getAllCapturedNamespaces().forEach((ns) => {
|
|
1766
|
+
if (namespaceMatchesPattern(ns, effectivePattern)) {
|
|
1767
|
+
enabledNamespaces.add(ns);
|
|
1768
|
+
}
|
|
1769
|
+
});
|
|
1770
|
+
localStorage.setItem(FILTER_KEY, filterPattern);
|
|
1771
|
+
renderFilterUI();
|
|
1772
|
+
renderLogs();
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
// Right-click time diff: copy file location to clipboard
|
|
1776
|
+
if (target?.classList?.contains('gg-log-diff') && target.hasAttribute('data-file')) {
|
|
1777
|
+
e.preventDefault();
|
|
1778
|
+
const file = target.getAttribute('data-file') || '';
|
|
1779
|
+
const line = target.getAttribute('data-line');
|
|
1780
|
+
const col = target.getAttribute('data-col');
|
|
1781
|
+
const formatted = formatString(activeFormat(), file, line, col);
|
|
1782
|
+
navigator.clipboard.writeText(formatted).then(() => {
|
|
1783
|
+
showConfirmationTooltip(containerEl, target, `Copied: ${formatted}`);
|
|
1784
|
+
});
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
// Right-click message area: copy that single message
|
|
1788
|
+
const contentEl = target?.closest?.('.gg-log-content');
|
|
1789
|
+
if (contentEl) {
|
|
1790
|
+
const entryEl = contentEl.closest('.gg-log-entry');
|
|
1791
|
+
const entryIdx = entryEl?.getAttribute('data-entry');
|
|
1792
|
+
if (entryIdx === null || entryIdx === undefined)
|
|
1793
|
+
return;
|
|
1794
|
+
const entry = renderedEntries[Number(entryIdx)];
|
|
1795
|
+
if (!entry)
|
|
1796
|
+
return;
|
|
1797
|
+
e.preventDefault();
|
|
1798
|
+
const time = new Date(entry.timestamp).toISOString().slice(11, 19);
|
|
1799
|
+
const ns = entry.namespace.trim().replace(/^gg:/, '');
|
|
1800
|
+
// Include expression suffix when toggle is enabled
|
|
1801
|
+
const hasSrcExpr = !entry.level && entry.src?.trim() && !/^['"`]/.test(entry.src);
|
|
1802
|
+
const exprSuffix = showExpressions && hasSrcExpr ? ` \u2039${entry.src}\u203A` : '';
|
|
1803
|
+
const argsStr = entry.args
|
|
1804
|
+
.map((arg) => {
|
|
1805
|
+
if (typeof arg === 'object' && arg !== null) {
|
|
1806
|
+
return JSON.stringify(arg);
|
|
1807
|
+
}
|
|
1808
|
+
return stripAnsi(String(arg));
|
|
1809
|
+
})
|
|
1810
|
+
.join(' ');
|
|
1811
|
+
const text = `${time} ${ns} ${argsStr}${exprSuffix}`;
|
|
1812
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
1813
|
+
showConfirmationTooltip(containerEl, contentEl, 'Copied message');
|
|
1814
|
+
});
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
});
|
|
1453
1818
|
// Hover tooltip for expandable objects/arrays.
|
|
1454
1819
|
// The tooltip div is re-created after each renderLogs() call
|
|
1455
1820
|
// since logContainer.html() destroys children. Event listeners query it dynamically.
|
|
@@ -1514,6 +1879,57 @@ export function createGgPlugin(options, gg) {
|
|
|
1514
1879
|
if (tip)
|
|
1515
1880
|
tip.style.display = 'none';
|
|
1516
1881
|
});
|
|
1882
|
+
// Tooltip for time diff (open-in-editor action)
|
|
1883
|
+
containerEl.addEventListener('mouseover', (e) => {
|
|
1884
|
+
const target = e.target;
|
|
1885
|
+
if (!target?.classList?.contains('gg-log-diff'))
|
|
1886
|
+
return;
|
|
1887
|
+
if (!target.hasAttribute('data-file'))
|
|
1888
|
+
return;
|
|
1889
|
+
const file = target.getAttribute('data-file') || '';
|
|
1890
|
+
const line = target.getAttribute('data-line') || '1';
|
|
1891
|
+
const col = target.getAttribute('data-col') || '1';
|
|
1892
|
+
const tip = containerEl.querySelector('.gg-hover-tooltip');
|
|
1893
|
+
if (!tip)
|
|
1894
|
+
return;
|
|
1895
|
+
// Build tooltip content
|
|
1896
|
+
let actionText;
|
|
1897
|
+
if (nsClickAction === 'open' && DEV) {
|
|
1898
|
+
actionText = `Open in editor: ${file}:${line}:${col}`;
|
|
1899
|
+
}
|
|
1900
|
+
else if (nsClickAction === 'open-url') {
|
|
1901
|
+
actionText = `Open URL: ${formatString(activeFormat(), file, line, col)}`;
|
|
1902
|
+
}
|
|
1903
|
+
else {
|
|
1904
|
+
actionText = `Copy: ${formatString(activeFormat(), file, line, col)}`;
|
|
1905
|
+
}
|
|
1906
|
+
tip.textContent = actionText;
|
|
1907
|
+
tip.style.display = 'block';
|
|
1908
|
+
// Position below the target
|
|
1909
|
+
const targetRect = target.getBoundingClientRect();
|
|
1910
|
+
let left = targetRect.left;
|
|
1911
|
+
let top = targetRect.bottom + 4;
|
|
1912
|
+
// Keep tooltip within viewport
|
|
1913
|
+
const tipRect = tip.getBoundingClientRect();
|
|
1914
|
+
if (left + tipRect.width > window.innerWidth) {
|
|
1915
|
+
left = window.innerWidth - tipRect.width - 8;
|
|
1916
|
+
}
|
|
1917
|
+
if (left < 4)
|
|
1918
|
+
left = 4;
|
|
1919
|
+
if (top + tipRect.height > window.innerHeight) {
|
|
1920
|
+
top = targetRect.top - tipRect.height - 4;
|
|
1921
|
+
}
|
|
1922
|
+
tip.style.left = `${left}px`;
|
|
1923
|
+
tip.style.top = `${top}px`;
|
|
1924
|
+
});
|
|
1925
|
+
containerEl.addEventListener('mouseout', (e) => {
|
|
1926
|
+
const target = e.target;
|
|
1927
|
+
if (!target?.classList?.contains('gg-log-diff'))
|
|
1928
|
+
return;
|
|
1929
|
+
const tip = containerEl.querySelector('.gg-hover-tooltip');
|
|
1930
|
+
if (tip)
|
|
1931
|
+
tip.style.display = 'none';
|
|
1932
|
+
});
|
|
1517
1933
|
expanderAttached = true;
|
|
1518
1934
|
}
|
|
1519
1935
|
function wireUpResize() {
|
|
@@ -1567,28 +1983,34 @@ export function createGgPlugin(options, gg) {
|
|
|
1567
1983
|
if (!$el)
|
|
1568
1984
|
return;
|
|
1569
1985
|
const logContainer = $el.find('.gg-log-container');
|
|
1570
|
-
const
|
|
1571
|
-
if (!logContainer.length || !
|
|
1986
|
+
const copyCountSpan = $el.find('.gg-copy-count');
|
|
1987
|
+
if (!logContainer.length || !copyCountSpan.length)
|
|
1572
1988
|
return;
|
|
1573
1989
|
const allEntries = buffer.getEntries();
|
|
1574
1990
|
// Apply filtering
|
|
1575
1991
|
const entries = allEntries.filter((entry) => enabledNamespaces.has(entry.namespace));
|
|
1576
1992
|
renderedEntries = entries;
|
|
1577
1993
|
const countText = entries.length === allEntries.length
|
|
1578
|
-
?
|
|
1579
|
-
:
|
|
1580
|
-
|
|
1994
|
+
? `Copy ${entries.length} ${entries.length === 1 ? 'entry' : 'entries'}`
|
|
1995
|
+
: `Copy ${entries.length} / ${allEntries.length} ${entries.length === 1 ? 'entry' : 'entries'}`;
|
|
1996
|
+
copyCountSpan.html(countText);
|
|
1581
1997
|
if (entries.length === 0) {
|
|
1582
|
-
|
|
1998
|
+
const hasFilteredLogs = allEntries.length > 0;
|
|
1999
|
+
const message = hasFilteredLogs
|
|
2000
|
+
? `All ${allEntries.length} logs filtered out.`
|
|
2001
|
+
: 'No logs captured yet. Call gg() to see output here.';
|
|
2002
|
+
const resetButton = hasFilteredLogs
|
|
2003
|
+
? '<button class="gg-reset-filter-btn" style="margin-top: 12px; padding: 10px 20px; cursor: pointer; border: 1px solid #2196F3; background: #2196F3; color: white; border-radius: 6px; font-size: 13px; font-weight: 500; transition: background 0.2s;">Show all logs (gg:*)</button>'
|
|
2004
|
+
: '';
|
|
2005
|
+
logContainer.html(`<div style="padding: 20px; text-align: center; opacity: 0.5;">${message}<div>${resetButton}</div></div>`);
|
|
1583
2006
|
return;
|
|
1584
2007
|
}
|
|
1585
|
-
const logsHTML = `<div class="gg-log-grid${filterExpanded ? ' filter-mode' : ''}" style="grid-template-columns: ${gridColumns()};">${entries
|
|
2008
|
+
const logsHTML = `<div class="gg-log-grid${filterExpanded ? ' filter-mode' : ''}${showExpressions ? ' gg-show-expr' : ''}" style="grid-template-columns: ${gridColumns()};">${entries
|
|
1586
2009
|
.map((entry, index) => {
|
|
1587
2010
|
const color = entry.color || '#0066cc';
|
|
1588
2011
|
const diff = `+${humanize(entry.diff)}`;
|
|
1589
|
-
const ns = escapeHtml(entry.namespace);
|
|
1590
2012
|
// Split namespace into clickable segments on multiple delimiters: : @ / - _
|
|
1591
|
-
const parts = entry.namespace.split(/([
|
|
2013
|
+
const parts = entry.namespace.split(/([:/@ \-_])/);
|
|
1592
2014
|
const nsSegments = [];
|
|
1593
2015
|
const delimiters = [];
|
|
1594
2016
|
for (let i = 0; i < parts.length; i++) {
|
|
@@ -1644,7 +2066,7 @@ export function createGgPlugin(options, gg) {
|
|
|
1644
2066
|
return `<tr>${cells}</tr>`;
|
|
1645
2067
|
})
|
|
1646
2068
|
.join('');
|
|
1647
|
-
argsHTML = `<table style="border-collapse: collapse; margin: 2px 0; font-family: monospace;"><thead><tr>${headerCells}</tr></thead><tbody>${bodyRowsHtml}</tbody></table>`;
|
|
2069
|
+
argsHTML = `<div style="overflow-x: auto;"><table style="border-collapse: collapse; margin: 2px 0; font-family: monospace;"><thead><tr>${headerCells}</tr></thead><tbody>${bodyRowsHtml}</tbody></table></div>`;
|
|
1648
2070
|
}
|
|
1649
2071
|
else if (entry.args.length > 0) {
|
|
1650
2072
|
argsHTML = entry.args
|
|
@@ -1661,7 +2083,11 @@ export function createGgPlugin(options, gg) {
|
|
|
1661
2083
|
// data-entry/data-arg for hover tooltip lookup, data-src for expression context
|
|
1662
2084
|
const srcAttr = srcExpr ? ` data-src="${srcExpr}"` : '';
|
|
1663
2085
|
const srcIcon = srcExpr ? `<span class="gg-src-icon">\uD83D\uDD0D</span>` : '';
|
|
1664
|
-
|
|
2086
|
+
// Show expression inline after preview when toggle is enabled
|
|
2087
|
+
const inlineExpr = showExpressions && srcExpr
|
|
2088
|
+
? ` <span class="gg-inline-expr">\u2039${srcExpr}\u203A</span>`
|
|
2089
|
+
: '';
|
|
2090
|
+
return `<span style="color: #888; cursor: pointer; text-decoration: underline;" class="gg-expand" data-index="${uniqueId}" data-entry="${index}" data-arg="${argIdx}"${srcAttr}>${srcIcon}${preview}${inlineExpr}</span>`;
|
|
1665
2091
|
}
|
|
1666
2092
|
else {
|
|
1667
2093
|
// Parse ANSI codes first, then convert URLs to clickable links
|
|
@@ -1675,33 +2101,11 @@ export function createGgPlugin(options, gg) {
|
|
|
1675
2101
|
})
|
|
1676
2102
|
.join(' ');
|
|
1677
2103
|
}
|
|
1678
|
-
//
|
|
1679
|
-
const iconsCol = filterExpanded
|
|
1680
|
-
? `<div class="gg-log-icons" data-namespace="${ns}">` +
|
|
1681
|
-
`<button class="gg-icon-hide" title="Hide this namespace">🗑</button>` +
|
|
1682
|
-
`<button class="gg-icon-solo" title="Show only this namespace">🎯</button>` +
|
|
1683
|
-
`</div>`
|
|
1684
|
-
: '';
|
|
1685
|
-
// When filter expanded, diff+ns are clickable (solo) with data-namespace
|
|
1686
|
-
const soloAttr = filterExpanded ? ` data-namespace="${ns}"` : '';
|
|
1687
|
-
const soloClass = filterExpanded ? ' gg-solo-target' : '';
|
|
2104
|
+
// Time diff will be clickable for open-in-editor when file metadata exists
|
|
1688
2105
|
// Open-in-editor data attributes (file, line, col)
|
|
1689
2106
|
const fileAttr = entry.file ? ` data-file="${escapeHtml(entry.file)}"` : '';
|
|
1690
2107
|
const lineAttr = entry.line ? ` data-line="${entry.line}"` : '';
|
|
1691
2108
|
const colAttr = entry.col ? ` data-col="${entry.col}"` : '';
|
|
1692
|
-
let fileTitleText = '';
|
|
1693
|
-
if (entry.file) {
|
|
1694
|
-
if (nsClickAction === 'open' && DEV) {
|
|
1695
|
-
fileTitleText = `Open in editor: ${entry.file}${entry.line ? ':' + entry.line : ''}${entry.col ? ':' + entry.col : ''}`;
|
|
1696
|
-
}
|
|
1697
|
-
else if (nsClickAction === 'open-url') {
|
|
1698
|
-
fileTitleText = `Open URL: ${formatString(activeFormat(), entry.file, String(entry.line || 1), String(entry.col || 1))}`;
|
|
1699
|
-
}
|
|
1700
|
-
else {
|
|
1701
|
-
fileTitleText = `Copy: ${formatString(activeFormat(), entry.file, String(entry.line || 1), String(entry.col || 1))}`;
|
|
1702
|
-
}
|
|
1703
|
-
}
|
|
1704
|
-
const fileTitle = fileTitleText ? ` title="${escapeHtml(fileTitleText)}"` : '';
|
|
1705
2109
|
// Level class for info/warn/error styling
|
|
1706
2110
|
const levelClass = entry.level === 'info'
|
|
1707
2111
|
? ' gg-level-info'
|
|
@@ -1719,14 +2123,19 @@ export function createGgPlugin(options, gg) {
|
|
|
1719
2123
|
`<div class="gg-stack-content" data-stack-id="${stackId}">${escapeHtml(entry.stack)}</div>`;
|
|
1720
2124
|
}
|
|
1721
2125
|
// Desktop: grid layout, Mobile: stacked layout
|
|
1722
|
-
|
|
2126
|
+
// Expression tooltip: skip table entries (tableData) -- expression is just gg.table(...) which isn't useful
|
|
2127
|
+
const hasSrcExpr = !entry.level && !entry.tableData && entry.src?.trim() && !/^['"`]/.test(entry.src);
|
|
2128
|
+
// For primitives-only entries, append inline expression when showExpressions is enabled
|
|
2129
|
+
const inlineExprForPrimitives = showExpressions && hasSrcExpr && !argsHTML.includes('gg-expand')
|
|
2130
|
+
? ` <span class="gg-inline-expr">\u2039${escapeHtml(entry.src)}\u203A</span>`
|
|
2131
|
+
: '';
|
|
2132
|
+
return (`<div class="gg-log-entry${levelClass}" data-entry="${index}">` +
|
|
1723
2133
|
`<div class="gg-log-header">` +
|
|
1724
|
-
|
|
1725
|
-
`<div class="gg-log-
|
|
1726
|
-
`<div class="gg-log-ns${soloClass}" style="color: ${color};" data-namespace="${escapeHtml(entry.namespace)}"${fileAttr}${lineAttr}${colAttr}${fileTitle}>${nsHTML}</div>` +
|
|
2134
|
+
`<div class="gg-log-diff" style="color: ${color};"${fileAttr}${lineAttr}${colAttr}>${diff}</div>` +
|
|
2135
|
+
`<div class="gg-log-ns" style="color: ${color};" data-namespace="${escapeHtml(entry.namespace)}"><span class="gg-ns-text">${nsHTML}</span><button class="gg-ns-hide" data-namespace="${escapeHtml(entry.namespace)}" title="Hide this namespace">\u00d7</button></div>` +
|
|
1727
2136
|
`<div class="gg-log-handle"></div>` +
|
|
1728
2137
|
`</div>` +
|
|
1729
|
-
`<div class="gg-log-content"${
|
|
2138
|
+
`<div class="gg-log-content"${hasSrcExpr ? ` data-src="${escapeHtml(entry.src)}"` : ''}>${argsHTML}${inlineExprForPrimitives}${stackHTML}</div>` +
|
|
1730
2139
|
detailsHTML +
|
|
1731
2140
|
`</div>`);
|
|
1732
2141
|
})
|