@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.
Files changed (2) hide show
  1. package/dist/eruda/plugin.js +615 -206
  2. package/package.json +1 -1
@@ -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
- // When filter expanded: icons | diff | ns | handle | content
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
- .gg-log-header {
355
- display: contents;
356
- }
357
- .gg-log-icons,
358
- .gg-log-diff,
359
- .gg-log-ns,
360
- .gg-log-handle,
361
- .gg-log-content {
362
- min-width: 0;
363
- align-self: start !important;
364
- border-top: 1px solid rgba(0,0,0,0.05);
365
- }
366
- .gg-log-icons {
367
- display: flex;
368
- gap: 2px;
369
- padding: 0 4px 0 0;
370
- white-space: nowrap;
371
- align-self: stretch !important;
372
- }
373
- .gg-log-icons button {
374
- all: unset;
375
- cursor: pointer;
376
- opacity: 0.35;
377
- padding: 4px 10px;
378
- line-height: 1;
379
- display: flex;
380
- align-items: center;
381
- }
382
- .gg-log-icons button:hover {
383
- opacity: 1;
384
- background: rgba(0,0,0,0.05);
385
- }
386
- .gg-solo-target {
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
- /* Only show segment hover styling when in filter mode */
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
- .gg-log-ns {
443
- font-weight: bold;
444
- white-space: nowrap;
445
- overflow: hidden;
446
- text-overflow: ellipsis;
447
- padding: 4px 8px 4px 0;
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
- .gg-log-content {
469
- word-break: break-word;
470
- padding: 4px 0;
471
- position: relative;
472
- -webkit-user-select: text !important;
473
- user-select: text !important;
474
- cursor: text;
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
- /* Remove double borders on mobile - only border on entry wrapper */
780
- .gg-log-entry:not(:first-child) {
781
- border-top: 1px solid rgba(0,0,0,0.05);
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
- <div class="gg-filter-checkboxes">
992
- <label class="gg-filter-checkbox" style="font-weight: bold;">
993
- <input type="checkbox" class="gg-all-checkbox" ${allChecked ? 'checked' : ''}>
994
- <span>ALL</span>
995
- </label>
996
- ${displayedNamespaces
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
- <label class="gg-filter-checkbox">
1003
- <input type="checkbox" class="gg-ns-checkbox" data-namespace="${escapeHtml(ns)}" ${checked ? 'checked' : ''}>
1004
- <span>${escapeHtml(ns)} (${count})</span>
1005
- </label>
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
- ${otherTotalCount > 0
1112
+ ${otherTotalCount > 0
1010
1113
  ? `
1011
- <label class="gg-filter-checkbox" style="opacity: 0.7;">
1012
- <input type="checkbox" class="gg-other-checkbox" ${otherChecked ? 'checked' : ''} data-other-namespaces='${JSON.stringify(otherNamespaces)}'>
1013
- <span>other (${otherCount})</span>
1014
- </label>
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
- </div>
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 namespace to open in editor (when filter collapsed)
1406
- if (target?.classList?.contains('gg-log-ns') &&
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 filter icon clicks (hide / solo)
1413
- if (target?.classList?.contains('gg-icon-hide') ||
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
- soloNamespace(namespace);
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 countSpan = $el.find('.gg-count');
1571
- if (!logContainer.length || !countSpan.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
- ? `${entries.length} entries`
1579
- : `${entries.length} / ${allEntries.length} entries`;
1580
- countSpan.html(countText);
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
- logContainer.html('<div style="padding: 20px; text-align: center; opacity: 0.5;">No logs captured yet. Call gg() to see output here.</div>');
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
- 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}</span>`;
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
- // Filter icons column (only when expanded)
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
- return (`<div class="gg-log-entry${levelClass}">` +
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
- iconsCol +
1725
- `<div class="gg-log-diff${soloClass}" style="color: ${color};"${soloAttr}>${diff}</div>` +
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"${!entry.level && entry.src?.trim() && !/^['"`]/.test(entry.src) ? ` data-src="${escapeHtml(entry.src)}"` : ''}>${argsHTML}${stackHTML}</div>` +
2138
+ `<div class="gg-log-content"${hasSrcExpr ? ` data-src="${escapeHtml(entry.src)}"` : ''}>${argsHTML}${inlineExprForPrimitives}${stackHTML}</div>` +
1730
2139
  detailsHTML +
1731
2140
  `</div>`);
1732
2141
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leftium/gg",
3
- "version": "0.0.40",
3
+ "version": "0.0.41",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/Leftium/gg.git"