@leftium/gg 0.0.39 → 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 +741 -178
- 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() {
|
|
@@ -205,19 +213,40 @@ export function createGgPlugin(options, gg) {
|
|
|
205
213
|
}
|
|
206
214
|
});
|
|
207
215
|
}
|
|
208
|
-
function
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
216
|
+
function toggleNamespaces(namespaces, enable) {
|
|
217
|
+
const currentPattern = filterPattern || 'gg:*';
|
|
218
|
+
let parts = currentPattern
|
|
219
|
+
.split(',')
|
|
220
|
+
.map((p) => p.trim())
|
|
221
|
+
.filter(Boolean);
|
|
222
|
+
namespaces.forEach((namespace) => {
|
|
223
|
+
const ns = namespace.trim();
|
|
224
|
+
if (enable) {
|
|
225
|
+
// Remove any exclusion for this namespace
|
|
226
|
+
parts = parts.filter((p) => p !== `-${ns}`);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
// Add exclusion if not already present
|
|
230
|
+
const exclusion = `-${ns}`;
|
|
231
|
+
if (!parts.includes(exclusion)) {
|
|
232
|
+
parts.push(exclusion);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
filterPattern = parts.join(',');
|
|
237
|
+
// Simplify pattern
|
|
238
|
+
filterPattern = simplifyPattern(filterPattern);
|
|
239
|
+
// Sync enabledNamespaces from the NEW pattern
|
|
240
|
+
const allNamespaces = getAllCapturedNamespaces();
|
|
219
241
|
enabledNamespaces.clear();
|
|
220
|
-
|
|
242
|
+
const effectivePattern = filterPattern || 'gg:*';
|
|
243
|
+
allNamespaces.forEach((ns) => {
|
|
244
|
+
if (namespaceMatchesPattern(ns, effectivePattern)) {
|
|
245
|
+
enabledNamespaces.add(ns);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
// Persist the new pattern
|
|
249
|
+
localStorage.setItem(FILTER_KEY, filterPattern);
|
|
221
250
|
}
|
|
222
251
|
function simplifyPattern(pattern) {
|
|
223
252
|
if (!pattern)
|
|
@@ -296,11 +325,7 @@ export function createGgPlugin(options, gg) {
|
|
|
296
325
|
}
|
|
297
326
|
function gridColumns() {
|
|
298
327
|
const ns = nsColWidth !== null ? `${nsColWidth}px` : 'auto';
|
|
299
|
-
//
|
|
300
|
-
// When collapsed: diff | ns | handle | content
|
|
301
|
-
if (filterExpanded) {
|
|
302
|
-
return `auto auto ${ns} 4px 1fr`;
|
|
303
|
-
}
|
|
328
|
+
// Grid columns: diff | ns | handle | content
|
|
304
329
|
return `auto ${ns} 4px 1fr`;
|
|
305
330
|
}
|
|
306
331
|
function buildHTML() {
|
|
@@ -316,67 +341,51 @@ export function createGgPlugin(options, gg) {
|
|
|
316
341
|
.gg-log-entry {
|
|
317
342
|
display: contents;
|
|
318
343
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
.gg-log-ns[data-file]:hover {
|
|
365
|
-
text-decoration-style: solid;
|
|
366
|
-
background: rgba(0,0,0,0.05);
|
|
367
|
-
}
|
|
368
|
-
.gg-log-ns[data-file]::after {
|
|
369
|
-
content: ' \u{1F4CB}';
|
|
370
|
-
font-size: 10px;
|
|
371
|
-
opacity: 0;
|
|
372
|
-
transition: opacity 0.1s;
|
|
373
|
-
}
|
|
374
|
-
.gg-action-open .gg-log-ns[data-file]::after {
|
|
375
|
-
content: ' \u{1F517}';
|
|
376
|
-
}
|
|
377
|
-
.gg-log-ns[data-file]:hover::after {
|
|
378
|
-
opacity: 1;
|
|
379
|
-
}
|
|
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 */
|
|
377
|
+
.gg-ns-segment {
|
|
378
|
+
cursor: pointer;
|
|
379
|
+
padding: 1px 2px;
|
|
380
|
+
border-radius: 2px;
|
|
381
|
+
transition: background 0.1s;
|
|
382
|
+
}
|
|
383
|
+
.gg-ns-segment:hover {
|
|
384
|
+
background: rgba(0,0,0,0.1);
|
|
385
|
+
text-decoration: underline;
|
|
386
|
+
text-decoration-style: solid;
|
|
387
|
+
text-underline-offset: 2px;
|
|
388
|
+
}
|
|
380
389
|
.gg-details {
|
|
381
390
|
grid-column: 1 / -1;
|
|
382
391
|
border-top: none;
|
|
@@ -390,13 +399,138 @@ export function createGgPlugin(options, gg) {
|
|
|
390
399
|
padding: 4px 8px 4px 0;
|
|
391
400
|
white-space: pre;
|
|
392
401
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
+
}
|
|
400
534
|
.gg-log-handle {
|
|
401
535
|
width: 4px;
|
|
402
536
|
cursor: col-resize;
|
|
@@ -416,14 +550,14 @@ export function createGgPlugin(options, gg) {
|
|
|
416
550
|
.gg-log-handle.gg-dragging {
|
|
417
551
|
background: rgba(0,0,0,0.15);
|
|
418
552
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
+
}
|
|
427
561
|
.gg-log-content * {
|
|
428
562
|
-webkit-user-select: text !important;
|
|
429
563
|
user-select: text !important;
|
|
@@ -474,6 +608,20 @@ export function createGgPlugin(options, gg) {
|
|
|
474
608
|
.gg-log-content[data-src]:not(:has(.gg-expand)):hover::after {
|
|
475
609
|
opacity: 1;
|
|
476
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
|
+
}
|
|
477
625
|
/* Expression icon inline with expandable object labels */
|
|
478
626
|
.gg-src-icon {
|
|
479
627
|
font-size: 10px;
|
|
@@ -727,18 +875,17 @@ export function createGgPlugin(options, gg) {
|
|
|
727
875
|
display: block;
|
|
728
876
|
padding: 8px 0;
|
|
729
877
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
}
|
|
734
|
-
.gg-log-icons,
|
|
735
|
-
.gg-log-diff,
|
|
736
|
-
.gg-log-ns,
|
|
737
|
-
.gg-log-handle,
|
|
738
|
-
.gg-log-content,
|
|
739
|
-
.gg-details {
|
|
740
|
-
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);
|
|
741
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
|
+
}
|
|
742
889
|
.gg-log-header {
|
|
743
890
|
display: flex;
|
|
744
891
|
align-items: center;
|
|
@@ -775,7 +922,7 @@ export function createGgPlugin(options, gg) {
|
|
|
775
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;">
|
|
776
923
|
<div class="gg-toolbar">
|
|
777
924
|
<button class="gg-copy-btn">
|
|
778
|
-
<span class="gg-btn-text">Copy</span>
|
|
925
|
+
<span class="gg-btn-text">📋 <span class="gg-copy-count">Copy 0 entries</span></span>
|
|
779
926
|
<span class="gg-btn-icon" title="Copy">📋</span>
|
|
780
927
|
</button>
|
|
781
928
|
<button class="gg-filter-btn" style="text-align: left; white-space: nowrap;">
|
|
@@ -783,11 +930,15 @@ export function createGgPlugin(options, gg) {
|
|
|
783
930
|
<span class="gg-btn-icon">NS: </span>
|
|
784
931
|
<span class="gg-filter-summary"></span>
|
|
785
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>
|
|
786
938
|
<button class="gg-settings-btn">
|
|
787
939
|
<span class="gg-btn-text">⚙️ Settings</span>
|
|
788
940
|
<span class="gg-btn-icon" title="Settings">⚙️</span>
|
|
789
941
|
</button>
|
|
790
|
-
<span class="gg-count" style="opacity: 0.6; white-space: nowrap; flex: 1; text-align: right;"></span>
|
|
791
942
|
<button class="gg-clear-btn">
|
|
792
943
|
<span class="gg-btn-text">Clear</span>
|
|
793
944
|
<span class="gg-btn-icon" title="Clear">⊘</span>
|
|
@@ -795,7 +946,8 @@ export function createGgPlugin(options, gg) {
|
|
|
795
946
|
</div>
|
|
796
947
|
<div class="gg-filter-panel"></div>
|
|
797
948
|
<div class="gg-settings-panel"></div>
|
|
798
|
-
<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>
|
|
799
951
|
<iframe class="gg-editor-iframe" hidden title="open-in-editor"></iframe>
|
|
800
952
|
</div>
|
|
801
953
|
`;
|
|
@@ -870,6 +1022,19 @@ export function createGgPlugin(options, gg) {
|
|
|
870
1022
|
renderLogs();
|
|
871
1023
|
return;
|
|
872
1024
|
}
|
|
1025
|
+
// Handle "other" checkbox
|
|
1026
|
+
if (target.classList.contains('gg-other-checkbox')) {
|
|
1027
|
+
const otherNamespacesJson = target.getAttribute('data-other-namespaces');
|
|
1028
|
+
if (!otherNamespacesJson)
|
|
1029
|
+
return;
|
|
1030
|
+
const otherNamespaces = JSON.parse(otherNamespacesJson);
|
|
1031
|
+
// Toggle all "other" namespaces at once
|
|
1032
|
+
toggleNamespaces(otherNamespaces, target.checked);
|
|
1033
|
+
// localStorage already saved in toggleNamespaces()
|
|
1034
|
+
renderFilterUI();
|
|
1035
|
+
renderLogs();
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
873
1038
|
// Handle individual namespace checkboxes
|
|
874
1039
|
if (target.classList.contains('gg-ns-checkbox')) {
|
|
875
1040
|
const namespace = target.getAttribute('data-namespace');
|
|
@@ -908,26 +1073,52 @@ export function createGgPlugin(options, gg) {
|
|
|
908
1073
|
let checkboxesHTML = '';
|
|
909
1074
|
if (simple && allNamespaces.length > 0) {
|
|
910
1075
|
const allChecked = enabledCount === totalCount;
|
|
1076
|
+
// Count frequency of each namespace in the buffer
|
|
1077
|
+
const allEntries = buffer.getEntries();
|
|
1078
|
+
const nsCounts = new Map();
|
|
1079
|
+
allEntries.forEach((entry) => {
|
|
1080
|
+
nsCounts.set(entry.namespace, (nsCounts.get(entry.namespace) || 0) + 1);
|
|
1081
|
+
});
|
|
1082
|
+
// Sort ALL namespaces by frequency (most common first)
|
|
1083
|
+
const sortedAllNamespaces = [...allNamespaces].sort((a, b) => (nsCounts.get(b) || 0) - (nsCounts.get(a) || 0));
|
|
1084
|
+
// Take top 5 most common (regardless of enabled state)
|
|
1085
|
+
const displayedNamespaces = sortedAllNamespaces.slice(0, 5);
|
|
1086
|
+
// Calculate "other" namespaces (not in top 5)
|
|
1087
|
+
const displayedSet = new Set(displayedNamespaces);
|
|
1088
|
+
const otherNamespaces = allNamespaces.filter((ns) => !displayedSet.has(ns));
|
|
1089
|
+
const otherEnabledCount = otherNamespaces.filter((ns) => enabledNamespaces.has(ns)).length;
|
|
1090
|
+
const otherTotalCount = otherNamespaces.length;
|
|
1091
|
+
const otherChecked = otherEnabledCount > 0;
|
|
1092
|
+
const otherCount = otherNamespaces.reduce((sum, ns) => sum + (nsCounts.get(ns) || 0), 0);
|
|
911
1093
|
checkboxesHTML = `
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
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
|
|
918
1100
|
.map((ns) => {
|
|
919
1101
|
// Check if namespace matches the current pattern
|
|
920
1102
|
const checked = namespaceMatchesPattern(ns, effectivePattern);
|
|
1103
|
+
const count = nsCounts.get(ns) || 0;
|
|
921
1104
|
return `
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
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
|
+
`;
|
|
927
1110
|
})
|
|
928
1111
|
.join('')}
|
|
929
|
-
|
|
930
|
-
|
|
1112
|
+
${otherTotalCount > 0
|
|
1113
|
+
? `
|
|
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
|
+
`
|
|
1119
|
+
: ''}
|
|
1120
|
+
</div>
|
|
1121
|
+
`;
|
|
931
1122
|
}
|
|
932
1123
|
else if (!simple) {
|
|
933
1124
|
checkboxesHTML = `<div style="opacity: 0.6; font-size: 11px; margin: 8px 0;">⚠️ Complex pattern - edit manually (quick filters disabled)</div>`;
|
|
@@ -1169,6 +1360,9 @@ export function createGgPlugin(options, gg) {
|
|
|
1169
1360
|
const time = new Date(e.timestamp).toISOString().slice(11, 19);
|
|
1170
1361
|
// Trim namespace and strip 'gg:' prefix to save tokens
|
|
1171
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` : '';
|
|
1172
1366
|
// Format args: compact JSON for objects, primitives as-is
|
|
1173
1367
|
const argsStr = e.args
|
|
1174
1368
|
.map((arg) => {
|
|
@@ -1179,7 +1373,7 @@ export function createGgPlugin(options, gg) {
|
|
|
1179
1373
|
return stripAnsi(String(arg));
|
|
1180
1374
|
})
|
|
1181
1375
|
.join(' ');
|
|
1182
|
-
return `${time} ${ns} ${argsStr}`;
|
|
1376
|
+
return `${time} ${ns} ${argsStr}${exprSuffix}`;
|
|
1183
1377
|
})
|
|
1184
1378
|
.join('\n');
|
|
1185
1379
|
try {
|
|
@@ -1195,6 +1389,15 @@ export function createGgPlugin(options, gg) {
|
|
|
1195
1389
|
document.body.removeChild(textarea);
|
|
1196
1390
|
}
|
|
1197
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
|
+
});
|
|
1198
1401
|
}
|
|
1199
1402
|
/** Substitute format variables ($ROOT, $FILE, $LINE, $COL) in a format string */
|
|
1200
1403
|
function formatString(format, file, line, col) {
|
|
@@ -1248,6 +1451,161 @@ export function createGgPlugin(options, gg) {
|
|
|
1248
1451
|
});
|
|
1249
1452
|
}
|
|
1250
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
|
+
}
|
|
1251
1609
|
function wireUpExpanders() {
|
|
1252
1610
|
if (!$el || expanderAttached)
|
|
1253
1611
|
return;
|
|
@@ -1258,6 +1616,16 @@ export function createGgPlugin(options, gg) {
|
|
|
1258
1616
|
return;
|
|
1259
1617
|
containerEl.addEventListener('click', (e) => {
|
|
1260
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
|
+
}
|
|
1261
1629
|
// Handle expand/collapse
|
|
1262
1630
|
if (target?.classList?.contains('gg-expand')) {
|
|
1263
1631
|
const index = target.getAttribute('data-index');
|
|
@@ -1282,40 +1650,47 @@ export function createGgPlugin(options, gg) {
|
|
|
1282
1650
|
}
|
|
1283
1651
|
return;
|
|
1284
1652
|
}
|
|
1285
|
-
// Handle clicking namespace
|
|
1286
|
-
if (target?.classList?.contains('gg-
|
|
1287
|
-
target.
|
|
1288
|
-
!
|
|
1289
|
-
handleNamespaceClick(target);
|
|
1290
|
-
return;
|
|
1291
|
-
}
|
|
1292
|
-
// Handle filter icon clicks (hide / solo)
|
|
1293
|
-
if (target?.classList?.contains('gg-icon-hide') ||
|
|
1294
|
-
target?.classList?.contains('gg-icon-solo')) {
|
|
1295
|
-
const iconsDiv = target.closest('.gg-log-icons');
|
|
1296
|
-
const namespace = iconsDiv?.getAttribute('data-namespace');
|
|
1297
|
-
if (!namespace)
|
|
1653
|
+
// Handle clicking namespace segments - always filter
|
|
1654
|
+
if (target?.classList?.contains('gg-ns-segment')) {
|
|
1655
|
+
const filter = target.getAttribute('data-filter');
|
|
1656
|
+
if (!filter)
|
|
1298
1657
|
return;
|
|
1299
|
-
if
|
|
1300
|
-
|
|
1658
|
+
// Toggle behavior: if already at this filter, restore all
|
|
1659
|
+
if (filterPattern === filter) {
|
|
1660
|
+
filterPattern = 'gg:*';
|
|
1661
|
+
enabledNamespaces.clear();
|
|
1662
|
+
getAllCapturedNamespaces().forEach((ns) => enabledNamespaces.add(ns));
|
|
1301
1663
|
}
|
|
1302
1664
|
else {
|
|
1303
|
-
|
|
1665
|
+
filterPattern = filter;
|
|
1666
|
+
enabledNamespaces.clear();
|
|
1667
|
+
getAllCapturedNamespaces()
|
|
1668
|
+
.filter((ns) => namespaceMatchesPattern(ns, filter))
|
|
1669
|
+
.forEach((ns) => enabledNamespaces.add(ns));
|
|
1304
1670
|
}
|
|
1305
1671
|
localStorage.setItem(FILTER_KEY, filterPattern);
|
|
1306
1672
|
renderFilterUI();
|
|
1307
1673
|
renderLogs();
|
|
1308
1674
|
return;
|
|
1309
1675
|
}
|
|
1310
|
-
// Handle clicking diff
|
|
1311
|
-
if (target?.classList?.contains('gg-
|
|
1676
|
+
// Handle clicking time diff to open in editor
|
|
1677
|
+
if (target?.classList?.contains('gg-log-diff') && target.hasAttribute('data-file')) {
|
|
1678
|
+
handleNamespaceClick(target);
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
// Handle clicking hide button for namespace
|
|
1682
|
+
if (target?.classList?.contains('gg-ns-hide')) {
|
|
1312
1683
|
const namespace = target.getAttribute('data-namespace');
|
|
1313
1684
|
if (!namespace)
|
|
1314
1685
|
return;
|
|
1315
|
-
|
|
1686
|
+
// Save current pattern for undo before hiding
|
|
1687
|
+
const previousPattern = filterPattern;
|
|
1688
|
+
toggleNamespace(namespace, false);
|
|
1316
1689
|
localStorage.setItem(FILTER_KEY, filterPattern);
|
|
1317
1690
|
renderFilterUI();
|
|
1318
1691
|
renderLogs();
|
|
1692
|
+
// Show toast with undo option
|
|
1693
|
+
showHideToast(namespace, previousPattern);
|
|
1319
1694
|
return;
|
|
1320
1695
|
}
|
|
1321
1696
|
// Clicking background (container or grid, not a log element) restores all
|
|
@@ -1330,6 +1705,116 @@ export function createGgPlugin(options, gg) {
|
|
|
1330
1705
|
renderLogs();
|
|
1331
1706
|
}
|
|
1332
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
|
+
});
|
|
1333
1818
|
// Hover tooltip for expandable objects/arrays.
|
|
1334
1819
|
// The tooltip div is re-created after each renderLogs() call
|
|
1335
1820
|
// since logContainer.html() destroys children. Event listeners query it dynamically.
|
|
@@ -1394,6 +1879,57 @@ export function createGgPlugin(options, gg) {
|
|
|
1394
1879
|
if (tip)
|
|
1395
1880
|
tip.style.display = 'none';
|
|
1396
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
|
+
});
|
|
1397
1933
|
expanderAttached = true;
|
|
1398
1934
|
}
|
|
1399
1935
|
function wireUpResize() {
|
|
@@ -1447,26 +1983,66 @@ export function createGgPlugin(options, gg) {
|
|
|
1447
1983
|
if (!$el)
|
|
1448
1984
|
return;
|
|
1449
1985
|
const logContainer = $el.find('.gg-log-container');
|
|
1450
|
-
const
|
|
1451
|
-
if (!logContainer.length || !
|
|
1986
|
+
const copyCountSpan = $el.find('.gg-copy-count');
|
|
1987
|
+
if (!logContainer.length || !copyCountSpan.length)
|
|
1452
1988
|
return;
|
|
1453
1989
|
const allEntries = buffer.getEntries();
|
|
1454
1990
|
// Apply filtering
|
|
1455
1991
|
const entries = allEntries.filter((entry) => enabledNamespaces.has(entry.namespace));
|
|
1456
1992
|
renderedEntries = entries;
|
|
1457
1993
|
const countText = entries.length === allEntries.length
|
|
1458
|
-
?
|
|
1459
|
-
:
|
|
1460
|
-
|
|
1994
|
+
? `Copy ${entries.length} ${entries.length === 1 ? 'entry' : 'entries'}`
|
|
1995
|
+
: `Copy ${entries.length} / ${allEntries.length} ${entries.length === 1 ? 'entry' : 'entries'}`;
|
|
1996
|
+
copyCountSpan.html(countText);
|
|
1461
1997
|
if (entries.length === 0) {
|
|
1462
|
-
|
|
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>`);
|
|
1463
2006
|
return;
|
|
1464
2007
|
}
|
|
1465
|
-
const logsHTML = `<div class="gg-log-grid" 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
|
|
1466
2009
|
.map((entry, index) => {
|
|
1467
2010
|
const color = entry.color || '#0066cc';
|
|
1468
2011
|
const diff = `+${humanize(entry.diff)}`;
|
|
1469
|
-
|
|
2012
|
+
// Split namespace into clickable segments on multiple delimiters: : @ / - _
|
|
2013
|
+
const parts = entry.namespace.split(/([:/@ \-_])/);
|
|
2014
|
+
const nsSegments = [];
|
|
2015
|
+
const delimiters = [];
|
|
2016
|
+
for (let i = 0; i < parts.length; i++) {
|
|
2017
|
+
if (i % 2 === 0) {
|
|
2018
|
+
// Even indices are segments
|
|
2019
|
+
if (parts[i])
|
|
2020
|
+
nsSegments.push(parts[i]);
|
|
2021
|
+
}
|
|
2022
|
+
else {
|
|
2023
|
+
// Odd indices are delimiters
|
|
2024
|
+
delimiters.push(parts[i]);
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
let nsHTML = '';
|
|
2028
|
+
for (let i = 0; i < nsSegments.length; i++) {
|
|
2029
|
+
const segment = escapeHtml(nsSegments[i]);
|
|
2030
|
+
// Build filter pattern: reconstruct namespace up to this point
|
|
2031
|
+
let filterPattern = '';
|
|
2032
|
+
for (let j = 0; j <= i; j++) {
|
|
2033
|
+
filterPattern += nsSegments[j];
|
|
2034
|
+
if (j < i) {
|
|
2035
|
+
filterPattern += delimiters[j];
|
|
2036
|
+
}
|
|
2037
|
+
else if (j < nsSegments.length - 1) {
|
|
2038
|
+
filterPattern += delimiters[j] + '*';
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
nsHTML += `<span class="gg-ns-segment" data-filter="${escapeHtml(filterPattern)}">${segment}</span>`;
|
|
2042
|
+
if (i < nsSegments.length - 1) {
|
|
2043
|
+
nsHTML += escapeHtml(delimiters[i]);
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
1470
2046
|
// Format each arg individually - objects are expandable
|
|
1471
2047
|
let argsHTML = '';
|
|
1472
2048
|
let detailsHTML = '';
|
|
@@ -1490,7 +2066,7 @@ export function createGgPlugin(options, gg) {
|
|
|
1490
2066
|
return `<tr>${cells}</tr>`;
|
|
1491
2067
|
})
|
|
1492
2068
|
.join('');
|
|
1493
|
-
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>`;
|
|
1494
2070
|
}
|
|
1495
2071
|
else if (entry.args.length > 0) {
|
|
1496
2072
|
argsHTML = entry.args
|
|
@@ -1507,7 +2083,11 @@ export function createGgPlugin(options, gg) {
|
|
|
1507
2083
|
// data-entry/data-arg for hover tooltip lookup, data-src for expression context
|
|
1508
2084
|
const srcAttr = srcExpr ? ` data-src="${srcExpr}"` : '';
|
|
1509
2085
|
const srcIcon = srcExpr ? `<span class="gg-src-icon">\uD83D\uDD0D</span>` : '';
|
|
1510
|
-
|
|
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>`;
|
|
1511
2091
|
}
|
|
1512
2092
|
else {
|
|
1513
2093
|
// Parse ANSI codes first, then convert URLs to clickable links
|
|
@@ -1521,33 +2101,11 @@ export function createGgPlugin(options, gg) {
|
|
|
1521
2101
|
})
|
|
1522
2102
|
.join(' ');
|
|
1523
2103
|
}
|
|
1524
|
-
//
|
|
1525
|
-
const iconsCol = filterExpanded
|
|
1526
|
-
? `<div class="gg-log-icons" data-namespace="${ns}">` +
|
|
1527
|
-
`<button class="gg-icon-hide" title="Hide this namespace">🗑</button>` +
|
|
1528
|
-
`<button class="gg-icon-solo" title="Show only this namespace">🎯</button>` +
|
|
1529
|
-
`</div>`
|
|
1530
|
-
: '';
|
|
1531
|
-
// When filter expanded, diff+ns are clickable (solo) with data-namespace
|
|
1532
|
-
const soloAttr = filterExpanded ? ` data-namespace="${ns}"` : '';
|
|
1533
|
-
const soloClass = filterExpanded ? ' gg-solo-target' : '';
|
|
2104
|
+
// Time diff will be clickable for open-in-editor when file metadata exists
|
|
1534
2105
|
// Open-in-editor data attributes (file, line, col)
|
|
1535
2106
|
const fileAttr = entry.file ? ` data-file="${escapeHtml(entry.file)}"` : '';
|
|
1536
2107
|
const lineAttr = entry.line ? ` data-line="${entry.line}"` : '';
|
|
1537
2108
|
const colAttr = entry.col ? ` data-col="${entry.col}"` : '';
|
|
1538
|
-
let fileTitleText = '';
|
|
1539
|
-
if (entry.file) {
|
|
1540
|
-
if (nsClickAction === 'open' && DEV) {
|
|
1541
|
-
fileTitleText = `Open in editor: ${entry.file}${entry.line ? ':' + entry.line : ''}${entry.col ? ':' + entry.col : ''}`;
|
|
1542
|
-
}
|
|
1543
|
-
else if (nsClickAction === 'open-url') {
|
|
1544
|
-
fileTitleText = `Open URL: ${formatString(activeFormat(), entry.file, String(entry.line || 1), String(entry.col || 1))}`;
|
|
1545
|
-
}
|
|
1546
|
-
else {
|
|
1547
|
-
fileTitleText = `Copy: ${formatString(activeFormat(), entry.file, String(entry.line || 1), String(entry.col || 1))}`;
|
|
1548
|
-
}
|
|
1549
|
-
}
|
|
1550
|
-
const fileTitle = fileTitleText ? ` title="${escapeHtml(fileTitleText)}"` : '';
|
|
1551
2109
|
// Level class for info/warn/error styling
|
|
1552
2110
|
const levelClass = entry.level === 'info'
|
|
1553
2111
|
? ' gg-level-info'
|
|
@@ -1565,14 +2123,19 @@ export function createGgPlugin(options, gg) {
|
|
|
1565
2123
|
`<div class="gg-stack-content" data-stack-id="${stackId}">${escapeHtml(entry.stack)}</div>`;
|
|
1566
2124
|
}
|
|
1567
2125
|
// Desktop: grid layout, Mobile: stacked layout
|
|
1568
|
-
|
|
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}">` +
|
|
1569
2133
|
`<div class="gg-log-header">` +
|
|
1570
|
-
|
|
1571
|
-
`<div class="gg-log-
|
|
1572
|
-
`<div class="gg-log-ns${soloClass}" style="color: ${color};"${soloAttr}${fileAttr}${lineAttr}${colAttr}${fileTitle}>${ns}</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>` +
|
|
1573
2136
|
`<div class="gg-log-handle"></div>` +
|
|
1574
2137
|
`</div>` +
|
|
1575
|
-
`<div class="gg-log-content"${
|
|
2138
|
+
`<div class="gg-log-content"${hasSrcExpr ? ` data-src="${escapeHtml(entry.src)}"` : ''}>${argsHTML}${inlineExprForPrimitives}${stackHTML}</div>` +
|
|
1576
2139
|
detailsHTML +
|
|
1577
2140
|
`</div>`);
|
|
1578
2141
|
})
|