@leftium/gg 0.0.21 → 0.0.24

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.
@@ -0,0 +1,615 @@
1
+ import { LogBuffer } from './buffer.js';
2
+ /**
3
+ * Creates the gg Eruda plugin
4
+ *
5
+ * Uses Eruda's plugin API where $el is a jQuery-like (licia) wrapper.
6
+ * Methods: $el.html(), $el.show(), $el.hide(), $el.find(), $el.on()
7
+ */
8
+ export function createGgPlugin(options, gg) {
9
+ const buffer = new LogBuffer(options.maxEntries ?? 2000);
10
+ // The licia jQuery-like wrapper Eruda passes to init()
11
+ let $el = null;
12
+ let expanderAttached = false;
13
+ let resizeAttached = false;
14
+ // null = auto (fit content), number = user-dragged px width
15
+ let nsColWidth = null;
16
+ // Filter UI state
17
+ let filterExpanded = false;
18
+ let filterPattern = '';
19
+ const enabledNamespaces = new Set();
20
+ const plugin = {
21
+ name: 'GG',
22
+ init($container) {
23
+ $el = $container;
24
+ // Register the capture hook on gg
25
+ if (gg) {
26
+ gg._onLog = (entry) => {
27
+ buffer.push(entry);
28
+ // Add new namespace to enabledNamespaces if it matches the current pattern
29
+ const effectivePattern = filterPattern || 'gg:*';
30
+ if (namespaceMatchesPattern(entry.namespace, effectivePattern)) {
31
+ enabledNamespaces.add(entry.namespace);
32
+ }
33
+ // Update filter UI if expanded (new namespace may have appeared)
34
+ if (filterExpanded) {
35
+ renderFilterUI();
36
+ }
37
+ renderLogs();
38
+ };
39
+ }
40
+ // Load initial filter state
41
+ filterPattern = localStorage.getItem('debug') || '';
42
+ // Render initial UI
43
+ $el.html(buildHTML());
44
+ wireUpButtons();
45
+ wireUpExpanders();
46
+ wireUpResize();
47
+ wireUpFilterUI();
48
+ renderLogs();
49
+ },
50
+ show() {
51
+ if ($el) {
52
+ $el.show();
53
+ renderLogs();
54
+ }
55
+ },
56
+ hide() {
57
+ if ($el) {
58
+ $el.hide();
59
+ }
60
+ },
61
+ destroy() {
62
+ if (gg) {
63
+ gg._onLog = null;
64
+ }
65
+ buffer.clear();
66
+ }
67
+ };
68
+ function toggleNamespace(namespace, enable) {
69
+ const currentPattern = filterPattern || 'gg:*';
70
+ const ns = namespace.trim();
71
+ // Split into parts, manipulate, rejoin (avoids fragile regex on complex namespace strings)
72
+ const parts = currentPattern
73
+ .split(',')
74
+ .map((p) => p.trim())
75
+ .filter(Boolean);
76
+ if (enable) {
77
+ // Remove any exclusion for this namespace
78
+ const filtered = parts.filter((p) => p !== `-${ns}`);
79
+ filterPattern = filtered.join(',');
80
+ }
81
+ else {
82
+ // Add exclusion
83
+ parts.push(`-${ns}`);
84
+ filterPattern = parts.join(',');
85
+ }
86
+ // Simplify pattern
87
+ filterPattern = simplifyPattern(filterPattern);
88
+ // Sync enabledNamespaces from the NEW pattern (don't re-read localStorage)
89
+ const allNamespaces = getAllCapturedNamespaces();
90
+ enabledNamespaces.clear();
91
+ const effectivePattern = filterPattern || 'gg:*';
92
+ allNamespaces.forEach((ns) => {
93
+ if (namespaceMatchesPattern(ns, effectivePattern)) {
94
+ enabledNamespaces.add(ns);
95
+ }
96
+ });
97
+ }
98
+ function simplifyPattern(pattern) {
99
+ if (!pattern)
100
+ return '';
101
+ // Remove empty parts
102
+ let parts = pattern
103
+ .split(',')
104
+ .map((p) => p.trim())
105
+ .filter(Boolean);
106
+ // Remove duplicates
107
+ parts = Array.from(new Set(parts));
108
+ // Clean up trailing/leading commas
109
+ return parts.join(',');
110
+ }
111
+ function getAllCapturedNamespaces() {
112
+ const entries = buffer.getEntries();
113
+ const nsSet = new Set();
114
+ entries.forEach((e) => nsSet.add(e.namespace));
115
+ return Array.from(nsSet).sort();
116
+ }
117
+ function namespaceMatchesPattern(namespace, pattern) {
118
+ if (!pattern)
119
+ return true; // Empty pattern = show all
120
+ // Split by comma for OR logic
121
+ const parts = pattern.split(',').map((p) => p.trim());
122
+ let included = false;
123
+ let excluded = false;
124
+ for (const part of parts) {
125
+ if (part.startsWith('-')) {
126
+ // Exclusion pattern
127
+ const excludePattern = part.slice(1);
128
+ if (matchesGlob(namespace, excludePattern)) {
129
+ excluded = true;
130
+ }
131
+ }
132
+ else {
133
+ // Inclusion pattern
134
+ if (matchesGlob(namespace, part)) {
135
+ included = true;
136
+ }
137
+ }
138
+ }
139
+ // If no inclusion patterns, default to included
140
+ const hasInclusions = parts.some((p) => !p.startsWith('-'));
141
+ if (!hasInclusions)
142
+ included = true;
143
+ return included && !excluded;
144
+ }
145
+ function matchesGlob(str, pattern) {
146
+ // Trim both for comparison (namespaces may have trailing spaces from padEnd)
147
+ const s = str.trim();
148
+ const p = pattern.trim();
149
+ // Convert glob pattern to regex
150
+ const regexPattern = p
151
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape special chars
152
+ .replace(/\*/g, '.*'); // * becomes .*
153
+ const regex = new RegExp(`^${regexPattern}$`);
154
+ return regex.test(s);
155
+ }
156
+ function isSimplePattern(pattern) {
157
+ if (!pattern)
158
+ return true;
159
+ // Simple patterns:
160
+ // 1. 'gg:*' with optional exclusions
161
+ // 2. Explicit comma-separated list of exact namespaces
162
+ const parts = pattern.split(',').map((p) => p.trim());
163
+ // Check if it's 'gg:*' based (with exclusions)
164
+ const hasWildcardBase = parts.some((p) => p === 'gg:*' || p === '*');
165
+ if (hasWildcardBase) {
166
+ // All other parts must be exclusions starting with '-gg:'
167
+ const otherParts = parts.filter((p) => p !== 'gg:*' && p !== '*');
168
+ return otherParts.every((p) => p.startsWith('-') && !p.includes('*', 1));
169
+ }
170
+ // Check if it's an explicit list (no wildcards)
171
+ return parts.every((p) => !p.includes('*') && !p.startsWith('-'));
172
+ }
173
+ function gridColumns() {
174
+ const ns = nsColWidth !== null ? `${nsColWidth}px` : 'auto';
175
+ if (filterExpanded) {
176
+ // [×] | diff | ns | handle | content
177
+ return `24px auto ${ns} 4px 1fr`;
178
+ }
179
+ else {
180
+ // diff | ns | handle | content
181
+ return `auto ${ns} 4px 1fr`;
182
+ }
183
+ }
184
+ function buildHTML() {
185
+ return `
186
+ <style>
187
+ .gg-log-grid {
188
+ display: grid;
189
+ grid-template-columns: ${gridColumns()};
190
+ column-gap: 0;
191
+ align-items: start !important;
192
+ }
193
+ .gg-log-grid > * {
194
+ min-width: 0;
195
+ border-top: 1px solid rgba(0,0,0,0.05);
196
+ align-self: start !important;
197
+ }
198
+ .gg-details {
199
+ align-self: stretch !important;
200
+ border-bottom: none;
201
+ }
202
+ .gg-log-diff {
203
+ text-align: right;
204
+ padding: 4px 8px 4px 0;
205
+ white-space: pre;
206
+ }
207
+ .gg-log-ns {
208
+ font-weight: bold;
209
+ white-space: nowrap;
210
+ overflow: hidden;
211
+ text-overflow: ellipsis;
212
+ padding: 4px 8px 4px 0;
213
+ }
214
+ .gg-log-handle {
215
+ width: 4px;
216
+ cursor: col-resize;
217
+ align-self: stretch !important;
218
+ background: transparent;
219
+ position: relative;
220
+ padding: 0 8px 0 0;
221
+ }
222
+ /* Wider invisible hit area */
223
+ .gg-log-handle::before {
224
+ content: '';
225
+ position: absolute;
226
+ top: 0; bottom: 0;
227
+ left: -4px; right: -4px;
228
+ }
229
+ .gg-log-handle:hover,
230
+ .gg-log-handle.gg-dragging {
231
+ background: rgba(0,0,0,0.15);
232
+ }
233
+ .gg-log-content {
234
+ word-break: break-word;
235
+ padding: 4px 0;
236
+ }
237
+ .gg-row-filter {
238
+ text-align: center;
239
+ padding: 4px 8px 4px 0;
240
+ cursor: pointer;
241
+ user-select: none;
242
+ opacity: 0.6;
243
+ font-size: 14px;
244
+ align-self: start;
245
+ }
246
+ .gg-row-filter:hover {
247
+ opacity: 1;
248
+ background: rgba(0,0,0,0.05);
249
+ }
250
+ .gg-filter-panel {
251
+ background: #f5f5f5;
252
+ padding: 10px;
253
+ margin-bottom: 8px;
254
+ border-radius: 4px;
255
+ flex-shrink: 0;
256
+ display: none;
257
+ }
258
+ .gg-filter-panel.expanded {
259
+ display: block;
260
+ }
261
+ .gg-filter-pattern {
262
+ width: 100%;
263
+ padding: 4px 8px;
264
+ font-family: monospace;
265
+ font-size: 12px;
266
+ margin-bottom: 8px;
267
+ }
268
+ .gg-filter-checkboxes {
269
+ display: flex;
270
+ flex-wrap: wrap;
271
+ gap: 8px;
272
+ margin: 8px 0;
273
+ max-height: 100px;
274
+ overflow-y: auto;
275
+ }
276
+ .gg-filter-checkbox {
277
+ display: flex;
278
+ align-items: center;
279
+ gap: 4px;
280
+ font-size: 11px;
281
+ font-family: monospace;
282
+ white-space: nowrap;
283
+ }
284
+ </style>
285
+ <div class="eruda-gg" style="padding: 10px; height: 100%; display: flex; flex-direction: column; font-size: 14px;">
286
+ <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px; flex-shrink: 0;">
287
+ <button class="gg-clear-btn" style="padding: 4px 10px; cursor: pointer;">Clear</button>
288
+ <button class="gg-copy-btn" style="padding: 4px 10px; cursor: pointer;">Copy</button>
289
+ <button class="gg-filter-btn" style="padding: 4px 10px; cursor: pointer; flex: 1; min-width: 0; text-align: left; font-family: monospace; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">⚙️ Filters: <span class="gg-filter-summary"></span></button>
290
+ <span class="gg-count" style="opacity: 0.6; white-space: nowrap;"></span>
291
+ </div>
292
+ <div class="gg-filter-panel"></div>
293
+ <div class="gg-log-container" style="flex: 1; overflow-y: auto; font-family: monospace; font-size: 12px;"></div>
294
+ </div>
295
+ `;
296
+ }
297
+ function applyPatternFromInput(value) {
298
+ filterPattern = value;
299
+ localStorage.setItem('debug', filterPattern);
300
+ // Sync enabledNamespaces from the new pattern
301
+ const allNamespaces = getAllCapturedNamespaces();
302
+ enabledNamespaces.clear();
303
+ const effectivePattern = filterPattern || 'gg:*';
304
+ allNamespaces.forEach((ns) => {
305
+ if (namespaceMatchesPattern(ns, effectivePattern)) {
306
+ enabledNamespaces.add(ns);
307
+ }
308
+ });
309
+ renderFilterUI();
310
+ renderLogs();
311
+ }
312
+ function wireUpFilterUI() {
313
+ if (!$el)
314
+ return;
315
+ const filterBtn = $el.find('.gg-filter-btn').get(0);
316
+ const filterPanel = $el.find('.gg-filter-panel').get(0);
317
+ if (!filterBtn || !filterPanel)
318
+ return;
319
+ renderFilterUI();
320
+ // Wire up button toggle
321
+ filterBtn.addEventListener('click', () => {
322
+ filterExpanded = !filterExpanded;
323
+ renderFilterUI();
324
+ renderLogs(); // Re-render to update grid columns
325
+ });
326
+ // Wire up pattern input - apply on blur or Enter
327
+ filterPanel.addEventListener('blur', (e) => {
328
+ const target = e.target;
329
+ if (target.classList.contains('gg-filter-pattern')) {
330
+ applyPatternFromInput(target.value);
331
+ }
332
+ }, true); // useCapture for blur (doesn't bubble)
333
+ filterPanel.addEventListener('keydown', (e) => {
334
+ const target = e.target;
335
+ if (target.classList.contains('gg-filter-pattern') && e.key === 'Enter') {
336
+ applyPatternFromInput(target.value);
337
+ target.blur();
338
+ }
339
+ });
340
+ // Wire up checkboxes
341
+ filterPanel.addEventListener('change', (e) => {
342
+ const target = e.target;
343
+ if (target.classList.contains('gg-ns-checkbox')) {
344
+ const namespace = target.getAttribute('data-namespace');
345
+ if (!namespace)
346
+ return;
347
+ // Toggle namespace in pattern
348
+ toggleNamespace(namespace, target.checked);
349
+ // Re-render to update UI
350
+ renderFilterUI();
351
+ renderLogs();
352
+ }
353
+ });
354
+ }
355
+ function renderFilterUI() {
356
+ if (!$el)
357
+ return;
358
+ // Update button summary
359
+ const filterSummary = $el.find('.gg-filter-summary').get(0);
360
+ if (filterSummary) {
361
+ const summary = filterPattern || 'gg:*';
362
+ filterSummary.textContent = summary;
363
+ }
364
+ // Update panel
365
+ const filterPanel = $el.find('.gg-filter-panel').get(0);
366
+ if (!filterPanel)
367
+ return;
368
+ if (filterExpanded) {
369
+ // Show panel
370
+ filterPanel.classList.add('expanded');
371
+ // Render expanded view
372
+ const allNamespaces = getAllCapturedNamespaces();
373
+ const simple = isSimplePattern(filterPattern);
374
+ const effectivePattern = filterPattern || 'gg:*';
375
+ let checkboxesHTML = '';
376
+ if (simple && allNamespaces.length > 0) {
377
+ checkboxesHTML = `
378
+ <div class="gg-filter-checkboxes">
379
+ ${allNamespaces
380
+ .map((ns) => {
381
+ // Check if namespace matches the current pattern
382
+ const checked = namespaceMatchesPattern(ns, effectivePattern);
383
+ return `
384
+ <label class="gg-filter-checkbox">
385
+ <input type="checkbox" class="gg-ns-checkbox" data-namespace="${escapeHtml(ns)}" ${checked ? 'checked' : ''}>
386
+ <span>${escapeHtml(ns)}</span>
387
+ </label>
388
+ `;
389
+ })
390
+ .join('')}
391
+ </div>
392
+ `;
393
+ }
394
+ else if (!simple) {
395
+ checkboxesHTML = `<div style="opacity: 0.6; font-size: 11px; margin: 8px 0;">⚠️ Complex pattern - edit manually (quick filters disabled)</div>`;
396
+ }
397
+ filterPanel.innerHTML = `
398
+ <div style="margin-bottom: 8px;">
399
+ <input type="text" class="gg-filter-pattern" value="${escapeHtml(filterPattern)}" placeholder="gg:*" style="width: 100%;">
400
+ </div>
401
+ ${checkboxesHTML}
402
+ `;
403
+ }
404
+ else {
405
+ // Hide panel
406
+ filterPanel.classList.remove('expanded');
407
+ }
408
+ }
409
+ function wireUpButtons() {
410
+ if (!$el)
411
+ return;
412
+ $el.find('.gg-clear-btn').on('click', () => {
413
+ buffer.clear();
414
+ renderLogs();
415
+ });
416
+ $el.find('.gg-copy-btn').on('click', async () => {
417
+ const allEntries = buffer.getEntries();
418
+ // Apply same filtering as renderLogs() - only copy visible entries
419
+ const entries = allEntries.filter((entry) => enabledNamespaces.has(entry.namespace));
420
+ const text = entries
421
+ .map((e) => {
422
+ const timestamp = new Date(e.timestamp).toISOString();
423
+ // Format args: stringify objects, keep primitives as-is
424
+ const argsStr = e.args
425
+ .map((arg) => {
426
+ if (typeof arg === 'object' && arg !== null) {
427
+ return JSON.stringify(arg, null, 2);
428
+ }
429
+ return String(arg);
430
+ })
431
+ .join(' ');
432
+ return `[${timestamp}] ${e.namespace} ${argsStr}`;
433
+ })
434
+ .join('\n');
435
+ try {
436
+ await navigator.clipboard.writeText(text);
437
+ }
438
+ catch {
439
+ // Fallback: select and copy
440
+ const textarea = document.createElement('textarea');
441
+ textarea.value = text;
442
+ document.body.appendChild(textarea);
443
+ textarea.select();
444
+ document.execCommand('copy');
445
+ document.body.removeChild(textarea);
446
+ }
447
+ });
448
+ }
449
+ function wireUpExpanders() {
450
+ if (!$el || expanderAttached)
451
+ return;
452
+ // Use native event delegation on the actual DOM element.
453
+ // Licia's .on() doesn't delegate to children replaced by .html().
454
+ const containerEl = $el.find('.gg-log-container').get(0);
455
+ if (!containerEl)
456
+ return;
457
+ containerEl.addEventListener('click', (e) => {
458
+ const target = e.target;
459
+ // Handle expand/collapse
460
+ if (target?.classList?.contains('gg-expand')) {
461
+ const index = target.getAttribute('data-index');
462
+ if (!index)
463
+ return;
464
+ const details = containerEl.querySelector(`.gg-details[data-index="${index}"]`);
465
+ if (details) {
466
+ details.style.display = details.style.display === 'none' ? 'block' : 'none';
467
+ }
468
+ return;
469
+ }
470
+ // Handle row filter button
471
+ if (target?.classList?.contains('gg-row-filter')) {
472
+ const namespace = target.getAttribute('data-namespace');
473
+ if (!namespace)
474
+ return;
475
+ // Toggle this namespace off
476
+ toggleNamespace(namespace, false);
477
+ // Save to localStorage and re-render
478
+ localStorage.setItem('debug', filterPattern);
479
+ renderFilterUI();
480
+ renderLogs();
481
+ }
482
+ });
483
+ expanderAttached = true;
484
+ }
485
+ function wireUpResize() {
486
+ if (!$el || resizeAttached)
487
+ return;
488
+ const containerEl = $el.find('.gg-log-container').get(0);
489
+ if (!containerEl)
490
+ return;
491
+ let dragging = false;
492
+ let startX = 0;
493
+ let startWidth = 0;
494
+ function onPointerDown(e) {
495
+ const target = e.target;
496
+ if (!target?.classList?.contains('gg-log-handle'))
497
+ return;
498
+ e.preventDefault();
499
+ dragging = true;
500
+ target.classList.add('gg-dragging');
501
+ target.setPointerCapture(e.pointerId);
502
+ startX = e.clientX;
503
+ // Measure current namespace column width from a sibling .gg-log-ns
504
+ const grid = containerEl.querySelector('.gg-log-grid');
505
+ const nsCell = grid?.querySelector('.gg-log-ns');
506
+ startWidth = nsCell ? nsCell.getBoundingClientRect().width : 200;
507
+ }
508
+ function onPointerMove(e) {
509
+ if (!dragging)
510
+ return;
511
+ const delta = e.clientX - startX;
512
+ const newWidth = Math.max(40, startWidth + delta);
513
+ nsColWidth = newWidth;
514
+ // Update grid template on the live element (no full re-render)
515
+ const grid = containerEl.querySelector('.gg-log-grid');
516
+ if (grid) {
517
+ grid.style.gridTemplateColumns = gridColumns();
518
+ }
519
+ }
520
+ function onPointerUp(e) {
521
+ if (!dragging)
522
+ return;
523
+ dragging = false;
524
+ const target = e.target;
525
+ target?.classList?.remove('gg-dragging');
526
+ }
527
+ containerEl.addEventListener('pointerdown', onPointerDown);
528
+ containerEl.addEventListener('pointermove', onPointerMove);
529
+ containerEl.addEventListener('pointerup', onPointerUp);
530
+ resizeAttached = true;
531
+ }
532
+ function renderLogs() {
533
+ if (!$el)
534
+ return;
535
+ const logContainer = $el.find('.gg-log-container');
536
+ const countSpan = $el.find('.gg-count');
537
+ if (!logContainer.length || !countSpan.length)
538
+ return;
539
+ const allEntries = buffer.getEntries();
540
+ // Apply filtering
541
+ const entries = allEntries.filter((entry) => enabledNamespaces.has(entry.namespace));
542
+ const countText = entries.length === allEntries.length
543
+ ? `${entries.length} entries`
544
+ : `${entries.length} / ${allEntries.length} entries`;
545
+ countSpan.html(countText);
546
+ if (entries.length === 0) {
547
+ logContainer.html('<div style="padding: 20px; text-align: center; opacity: 0.5;">No logs captured yet. Call gg() to see output here.</div>');
548
+ return;
549
+ }
550
+ const logsHTML = `<div class="gg-log-grid" style="grid-template-columns: ${gridColumns()};">${entries
551
+ .map((entry, index) => {
552
+ const color = entry.color || '#0066cc';
553
+ const diff = `+${humanize(entry.diff)}`;
554
+ const ns = escapeHtml(entry.namespace);
555
+ // Format each arg individually - objects are expandable
556
+ let argsHTML = '';
557
+ let detailsHTML = '';
558
+ if (entry.args.length > 0) {
559
+ argsHTML = entry.args
560
+ .map((arg, argIdx) => {
561
+ if (typeof arg === 'object' && arg !== null) {
562
+ // Show expandable object
563
+ const preview = Array.isArray(arg) ? `Array(${arg.length})` : 'Object';
564
+ const jsonStr = escapeHtml(JSON.stringify(arg, null, 2));
565
+ const uniqueId = `${index}-${argIdx}`;
566
+ // Store details separately to render after the row
567
+ detailsHTML += `<div class="gg-details" data-index="${uniqueId}" style="display: none; grid-column: 1 / -1; margin: 4px 0 8px 0; padding: 8px; background: #f8f8f8; border-left: 3px solid ${color}; font-size: 11px; overflow-x: auto;"><pre style="margin: 0;">${jsonStr}</pre></div>`;
568
+ return `<span style="color: #888; cursor: pointer; text-decoration: underline;" class="gg-expand" data-index="${uniqueId}">${preview}</span>`;
569
+ }
570
+ else {
571
+ return `<span>${escapeHtml(String(arg))}</span>`;
572
+ }
573
+ })
574
+ .join(' ');
575
+ }
576
+ // Add filter button if expanded
577
+ const filterBtn = filterExpanded
578
+ ? `<div class="gg-row-filter" data-namespace="${ns}" title="Hide this namespace">×</div>`
579
+ : '';
580
+ return (filterBtn +
581
+ `<div class="gg-log-diff" style="color: ${color};">${diff}</div>` +
582
+ `<div class="gg-log-ns" style="color: ${color};">${ns}</div>` +
583
+ `<div class="gg-log-handle"></div>` +
584
+ `<div class="gg-log-content">${argsHTML}</div>` +
585
+ detailsHTML);
586
+ })
587
+ .join('')}</div>`;
588
+ logContainer.html(logsHTML);
589
+ // Re-wire expanders after rendering
590
+ wireUpExpanders();
591
+ // Auto-scroll to bottom
592
+ const el = logContainer.get(0);
593
+ if (el)
594
+ el.scrollTop = el.scrollHeight;
595
+ }
596
+ /** Format ms like debug's `ms` package: 0ms, 500ms, 5s, 2m, 1h, 3d */
597
+ function humanize(ms) {
598
+ const abs = Math.abs(ms);
599
+ if (abs >= 86400000)
600
+ return Math.round(ms / 86400000) + 'd ';
601
+ if (abs >= 3600000)
602
+ return Math.round(ms / 3600000) + 'h ';
603
+ if (abs >= 60000)
604
+ return Math.round(ms / 60000) + 'm ';
605
+ if (abs >= 1000)
606
+ return Math.round(ms / 1000) + 's ';
607
+ return ms + 'ms';
608
+ }
609
+ function escapeHtml(text) {
610
+ const div = document.createElement('div');
611
+ div.textContent = text;
612
+ return div.innerHTML;
613
+ }
614
+ return plugin;
615
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Options for initializing the gg Eruda plugin
3
+ */
4
+ export interface GgErudaOptions {
5
+ /**
6
+ * How to load in production
7
+ * @default ['url-param', 'gesture']
8
+ */
9
+ prod?: Array<'url-param' | 'localStorage' | 'gesture'> | 'url-param' | 'localStorage' | 'gesture' | false;
10
+ /**
11
+ * Max captured log entries (ring buffer)
12
+ * @default 2000
13
+ */
14
+ maxEntries?: number;
15
+ /**
16
+ * Auto-enable localStorage.debug = 'gg:*' if unset
17
+ * @default true
18
+ */
19
+ autoEnable?: boolean;
20
+ /**
21
+ * Additional Eruda options passed to eruda.init()
22
+ * @default {}
23
+ */
24
+ erudaOptions?: Record<string, unknown>;
25
+ }
26
+ /**
27
+ * A captured log entry from gg()
28
+ */
29
+ export interface CapturedEntry {
30
+ /** Namespace (e.g., "gg:routes/+page.svelte@handleClick") */
31
+ namespace: string;
32
+ /** Color assigned by the debug library (e.g., "#CC3366") */
33
+ color: string;
34
+ /** Millisecond diff from previous log (e.g., 0, 123) */
35
+ diff: number;
36
+ /** Formatted message string */
37
+ message: string;
38
+ /** Raw arguments for expandable view */
39
+ args: unknown[];
40
+ /** Timestamp */
41
+ timestamp: number;
42
+ }
43
+ /**
44
+ * Eruda plugin interface
45
+ */
46
+ export interface ErudaPlugin {
47
+ name: string;
48
+ init($el: HTMLElement): void;
49
+ show?(): void;
50
+ hide?(): void;
51
+ destroy?(): void;
52
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/gg.d.ts CHANGED
@@ -1,4 +1,16 @@
1
1
  import ErrorStackParser from 'error-stack-parser';
2
+ /**
3
+ * Hook for capturing gg() output (used by Eruda plugin)
4
+ */
5
+ interface CapturedEntry {
6
+ namespace: string;
7
+ color: string;
8
+ diff: number;
9
+ message: string;
10
+ args: unknown[];
11
+ timestamp: number;
12
+ }
13
+ type OnLogCallback = (entry: CapturedEntry) => void;
2
14
  export declare function gg(): {
3
15
  fileName: string;
4
16
  functionName: string;
@@ -9,4 +21,7 @@ export declare function gg<T>(arg: T, ...args: unknown[]): T;
9
21
  export declare namespace gg {
10
22
  var disable: () => void;
11
23
  var enable: (namespaces: string) => void;
24
+ var clearPersist: () => void;
25
+ var _onLog: OnLogCallback | null;
12
26
  }
27
+ export {};