@leftium/gg 0.0.21 → 0.0.23

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