@qiaolei81/copilot-session-viewer 0.3.9 → 0.4.0

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/views/index.ejs DELETED
@@ -1,827 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Session Viewer</title>
7
- <style>
8
- * { margin: 0; padding: 0; box-sizing: border-box; }
9
- body {
10
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
11
- background: #0d1117;
12
- color: #c9d1d9;
13
- display: flex;
14
- align-items: center;
15
- justify-content: center;
16
- min-height: 100vh;
17
- padding: 20px;
18
- }
19
- .container {
20
- max-width: 1400px;
21
- width: 100%;
22
- text-align: center;
23
- }
24
-
25
- /* Focus indicators for accessibility */
26
- button:focus-visible,
27
- input:focus-visible {
28
- outline: 2px solid #58a6ff;
29
- outline-offset: 2px;
30
- box-shadow: 0 0 0 4px rgba(88, 166, 255, 0.2);
31
- }
32
-
33
- h1 {
34
- font-size: 48px;
35
- margin-bottom: 10px;
36
- color: #58a6ff;
37
- }
38
- .subtitle {
39
- color: #c9d1d9;
40
- margin-bottom: 40px;
41
- font-size: 16px;
42
- }
43
- .input-group {
44
- background: #161b22;
45
- border: 2px solid #30363d;
46
- border-radius: 12px;
47
- padding: 8px;
48
- display: flex;
49
- gap: 8px;
50
- transition: border-color 0.2s;
51
- }
52
- .input-group:focus-within {
53
- border-color: #58a6ff;
54
- }
55
- .session-input {
56
- flex: 1;
57
- padding: 16px 20px;
58
- min-height: 44px;
59
- background: transparent;
60
- border: none;
61
- color: #c9d1d9;
62
- font-size: 16px;
63
- font-family: "SF Mono", Monaco, monospace;
64
- }
65
- .session-input:focus {
66
- outline: none;
67
- }
68
- .session-input::placeholder {
69
- color: #6e7681;
70
- }
71
- .view-btn {
72
- padding: 16px 32px;
73
- min-height: 44px;
74
- background: #238636;
75
- border: none;
76
- border-radius: 8px;
77
- color: #fff;
78
- font-size: 16px;
79
- font-weight: 600;
80
- cursor: pointer;
81
- transition: all 0.2s;
82
- }
83
- .view-btn:hover {
84
- background: #2ea043;
85
- transform: scale(1.02);
86
- }
87
- .view-btn:active {
88
- transform: scale(0.98);
89
- }
90
- .hint {
91
- margin-top: 20px;
92
- color: #c9d1d9;
93
- font-size: 14px;
94
- }
95
- .hint-code {
96
- display: inline-block;
97
- background: #161b22;
98
- padding: 4px 8px;
99
- border-radius: 4px;
100
- font-family: "SF Mono", Monaco, monospace;
101
- font-size: 13px;
102
- color: #58a6ff;
103
- }
104
- .recent-sessions {
105
- margin-top: 40px;
106
- text-align: left;
107
- }
108
- .recent-title {
109
- color: #c9d1d9;
110
- font-size: 14px;
111
- margin-bottom: 12px;
112
- text-transform: uppercase;
113
- letter-spacing: 0.5px;
114
- }
115
- .recent-list {
116
- display: grid;
117
- grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
118
- gap: 16px;
119
- grid-auto-flow: dense;
120
- }
121
- .recent-item {
122
- background: #161b22;
123
- border: 1px solid #30363d;
124
- border-radius: 8px;
125
- padding: 12px 16px;
126
- color: #c9d1d9;
127
- text-decoration: none;
128
- transition: all 0.2s;
129
- display: block;
130
- min-height: 140px;
131
- height: auto;
132
- overflow: hidden;
133
- min-width: 0;
134
- }
135
- .recent-item:hover {
136
- border-color: #58a6ff;
137
- background: #1c2128;
138
- transform: translateY(-2px);
139
- box-shadow: 0 4px 12px rgba(88, 166, 255, 0.2);
140
- }
141
- .session-id {
142
- display: flex;
143
- justify-content: space-between;
144
- align-items: center;
145
- font-family: "SF Mono", Monaco, monospace;
146
- font-size: 11px;
147
- color: #6e7681;
148
- margin-bottom: 12px;
149
- letter-spacing: -0.02em;
150
- opacity: 0.7;
151
- }
152
- .session-id-text {
153
- overflow: hidden;
154
- text-overflow: ellipsis;
155
- white-space: nowrap;
156
- flex: 1;
157
- min-width: 0;
158
- }
159
- .session-badges {
160
- display: flex;
161
- align-items: center;
162
- gap: 4px;
163
- flex-shrink: 0;
164
- }
165
- .session-summary {
166
- color: #e6edf3;
167
- font-size: 15px;
168
- font-weight: 500;
169
- line-height: 1.5;
170
- margin-bottom: 12px;
171
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
172
- word-break: break-word;
173
- overflow-wrap: anywhere;
174
- display: -webkit-box;
175
- -webkit-line-clamp: 3;
176
- -webkit-box-orient: vertical;
177
- overflow: hidden;
178
- cursor: help;
179
- }
180
- .session-divider {
181
- height: 1px;
182
- background: #21262d;
183
- margin: 12px 0 10px 0;
184
- }
185
- .session-info {
186
- display: flex;
187
- align-items: center;
188
- gap: 16px;
189
- flex-wrap: wrap;
190
- font-size: 12px;
191
- font-family: "SF Mono", Monaco, monospace;
192
- min-width: 0;
193
- }
194
- .session-info-item {
195
- display: flex;
196
- align-items: center;
197
- gap: 6px;
198
- min-width: 0;
199
- color: #6e7681;
200
- }
201
- .session-info-item svg {
202
- flex-shrink: 0;
203
- width: 14px;
204
- height: 14px;
205
- opacity: 0.6;
206
- }
207
- .session-info-value {
208
- color: #8b949e;
209
- word-break: break-word;
210
- overflow-wrap: anywhere;
211
- }
212
- .session-info-item.workspace {
213
- flex-basis: 100%;
214
- min-width: 0;
215
- }
216
- .session-info-item.workspace .session-info-value {
217
- font-weight: 500;
218
- }
219
-
220
- /* Session tags */
221
- .session-badges-tags {
222
- display: flex;
223
- flex-wrap: wrap;
224
- align-items: center;
225
- justify-content: flex-start;
226
- gap: 4px;
227
- margin-top: 4px;
228
- }
229
- .session-badges-tags .session-badges {
230
- display: flex;
231
- flex-wrap: wrap;
232
- gap: 4px;
233
- }
234
- .session-badges-tags .session-tags {
235
- display: flex;
236
- flex-wrap: wrap;
237
- gap: 4px;
238
- margin-top: 0;
239
- padding-top: 0;
240
- border-top: none;
241
- }
242
- .session-tags {
243
- display: flex;
244
- flex-wrap: wrap;
245
- gap: 4px;
246
- margin-top: 8px;
247
- padding-top: 8px;
248
- border-top: 1px solid #21262d;
249
- }
250
- .session-tag {
251
- display: inline-block;
252
- padding: 3px 8px;
253
- border-radius: 10px;
254
- font-size: 11px;
255
- font-weight: 500;
256
- color: #fff;
257
- }
258
-
259
- .status-badge {
260
- display: inline-block;
261
- font-size: 14px;
262
- vertical-align: middle;
263
- opacity: 0.8;
264
- transition: opacity 0.2s;
265
- }
266
- .status-badge:hover {
267
- opacity: 1;
268
- }
269
- .status-badge.model {
270
- padding: 2px 8px;
271
- border-radius: 12px;
272
- font-size: 11px;
273
- font-weight: 600;
274
- font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
275
- }
276
- /* Claude - Anthropic orange/brown */
277
- .status-badge.model-claude {
278
- background: rgba(204, 120, 92, 0.15);
279
- color: #e8956f;
280
- }
281
- /* GPT - OpenAI green */
282
- .status-badge.model-gpt {
283
- background: rgba(16, 163, 127, 0.15);
284
- color: #1ec99d;
285
- }
286
- /* Gemini - Google blue */
287
- .status-badge.model-gemini {
288
- background: rgba(66, 133, 244, 0.15);
289
- color: #5e9aff;
290
- }
291
- /* Fallback for other models */
292
- .status-badge.model-other {
293
- background: rgba(139, 92, 246, 0.15);
294
- color: #a78bfa;
295
- }
296
- .status-badge.version {
297
- padding: 2px 8px;
298
- background: rgba(234, 179, 8, 0.15);
299
- color: #fbbf24;
300
- border-radius: 12px;
301
- font-size: 11px;
302
- font-weight: 600;
303
- font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
304
- }
305
- .status-badge.wip {
306
- padding: 2px 8px;
307
- border-radius: 12px;
308
- font-size: 11px;
309
- font-weight: 600;
310
- background: rgba(210, 153, 34, 0.2);
311
- color: #d29922;
312
- border: 1px solid rgba(210, 153, 34, 0.4);
313
- }
314
- .recent-item-wip {
315
- border-color: #d29922;
316
- border-left: 3px solid #d29922;
317
- }
318
- .recent-item-wip:hover {
319
- border-color: #e8b634;
320
- border-left-color: #e8b634;
321
- }
322
- /* Source badges */
323
- .status-badge.source-copilot {
324
- padding: 2px 8px;
325
- background: rgba(88, 166, 255, 0.15);
326
- color: #58a6ff;
327
- border-radius: 12px;
328
- font-size: 11px;
329
- font-weight: 600;
330
- font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
331
- }
332
- .status-badge.source-claude {
333
- padding: 2px 8px;
334
- background: rgba(204, 120, 92, 0.15);
335
- color: #e8956f;
336
- border-radius: 12px;
337
- font-size: 11px;
338
- font-weight: 600;
339
- font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
340
- }
341
- .status-badge.source-pi-mono {
342
- padding: 2px 8px;
343
- background: rgba(138, 102, 204, 0.15);
344
- color: #a78bdb;
345
- border-radius: 12px;
346
- font-size: 11px;
347
- font-weight: 600;
348
- font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
349
- }
350
- .status-badge.source-vscode {
351
- padding: 2px 8px;
352
- background: rgba(0, 122, 204, 0.15);
353
- color: #4fc3f7;
354
- border-radius: 12px;
355
- font-size: 11px;
356
- font-weight: 600;
357
- font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
358
- }
359
- .status-badge.source-modernize {
360
- padding: 2px 8px;
361
- background: rgba(76, 175, 80, 0.15);
362
- color: #66bb6a;
363
- border-radius: 12px;
364
- font-size: 11px;
365
- font-weight: 600;
366
- font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
367
- }
368
-
369
- /* Filter pills */
370
- .filter-pills {
371
- display: flex;
372
- gap: 8px;
373
- margin-bottom: 16px;
374
- flex-wrap: wrap;
375
- }
376
- .filter-pill {
377
- padding: 6px 16px;
378
- background: #21262d;
379
- border: 1px solid #30363d;
380
- border-radius: 20px;
381
- color: #8b949e;
382
- font-size: 13px;
383
- font-weight: 500;
384
- cursor: pointer;
385
- transition: all 0.2s;
386
- min-height: 32px;
387
- }
388
- .filter-pill:hover {
389
- background: #30363d;
390
- border-color: #58a6ff;
391
- color: #c9d1d9;
392
- }
393
- .filter-pill.active {
394
- background: #58a6ff;
395
- border-color: #58a6ff;
396
- color: #fff;
397
- }
398
-
399
- /* Sessions header with import link */
400
- .sessions-header {
401
- display: flex;
402
- align-items: baseline;
403
- gap: 12px;
404
- margin-bottom: 12px;
405
- }
406
- .import-link {
407
- color: #58a6ff;
408
- font-size: 14px;
409
- text-decoration: none;
410
- cursor: pointer;
411
- transition: color 0.2s;
412
- }
413
- .import-link:hover {
414
- color: #79c0ff;
415
- text-decoration: underline;
416
- }
417
- .import-formats-hint {
418
- font-size: 11px;
419
- color: #6e7681;
420
- margin-left: 6px;
421
- vertical-align: middle;
422
- }
423
- .import-status {
424
- margin-bottom: 12px;
425
- padding: 10px 12px;
426
- border-radius: 6px;
427
- font-size: 13px;
428
- display: none;
429
- }
430
- .import-status.success {
431
- display: block;
432
- background: rgba(35, 134, 54, 0.15);
433
- border: 1px solid #238636;
434
- color: #3fb950;
435
- }
436
- .import-status.error {
437
- display: block;
438
- background: rgba(248, 81, 73, 0.15);
439
- border: 1px solid #f85149;
440
- color: #ff7b72;
441
- }
442
- .import-status.loading {
443
- display: block;
444
- background: rgba(88, 166, 255, 0.15);
445
- border: 1px solid #58a6ff;
446
- color: #58a6ff;
447
- }
448
-
449
- /* Responsive adjustments */
450
- @media (max-width: 768px) {
451
- .recent-list {
452
- grid-template-columns: 1fr;
453
- }
454
- .container {
455
- max-width: 100%;
456
- }
457
- }
458
-
459
- @media (min-width: 769px) and (max-width: 1200px) {
460
- .recent-list {
461
- grid-template-columns: repeat(2, 1fr);
462
- }
463
- }
464
-
465
- /* Load more button and loading indicator */
466
- .load-more-btn {
467
- padding: 12px 32px;
468
- background: #21262d;
469
- border: 1px solid #30363d;
470
- border-radius: 8px;
471
- color: #c9d1d9;
472
- font-size: 16px;
473
- font-weight: 500;
474
- cursor: pointer;
475
- transition: all 0.2s;
476
- min-height: 44px;
477
- }
478
- .load-more-btn:hover {
479
- background: #30363d;
480
- border-color: #58a6ff;
481
- color: #58a6ff;
482
- transform: translateY(-1px);
483
- }
484
- .load-more-btn:active {
485
- transform: translateY(0);
486
- }
487
- .loading-spinner {
488
- color: #58a6ff;
489
- font-size: 16px;
490
- display: flex;
491
- align-items: center;
492
- justify-content: center;
493
- gap: 8px;
494
- }
495
- .loading-spinner::before {
496
- content: '⏳';
497
- animation: spin 1s linear infinite;
498
- }
499
- @keyframes spin {
500
- from { transform: rotate(0deg); }
501
- to { transform: rotate(360deg); }
502
- }
503
- </style>
504
-
505
- <%- include('telemetry-snippet') %>
506
- </head>
507
- <body>
508
- <div class="container">
509
- <h1>🤖 Session Viewer</h1>
510
- <p class="subtitle">View session logs from Copilot CLI, Copilot Chat, Claude Code, and Pi-Mono</p>
511
-
512
- <form id="sessionForm">
513
- <div class="input-group">
514
- <input
515
- type="text"
516
- class="session-input"
517
- id="sessionInput"
518
- placeholder="Enter Session ID..."
519
- autofocus
520
- required
521
- >
522
- <button type="submit" class="view-btn">View</button>
523
- </div>
524
- </form>
525
-
526
- <% if (sessions && sessions.length > 0) { %>
527
- <div class="recent-sessions">
528
- <div class="sessions-header">
529
- <div class="recent-title">
530
- Sessions
531
- </div>
532
- <a class="import-link" id="importLink">Import session from zip</a>
533
- <span class="import-formats-hint">Supports: GitHub Copilot, Claude, Pi-Mono</span>
534
- </div>
535
- <div class="filter-pills">
536
- <button class="filter-pill active" data-source="copilot">Copilot CLI</button>
537
- <button class="filter-pill" data-source="vscode">Copilot Chat</button>
538
- <button class="filter-pill" data-source="claude">Claude</button>
539
- <button class="filter-pill" data-source="modernize">Modernize CLI</button>
540
- <button class="filter-pill" data-source="pi-mono">Pi</button>
541
- </div>
542
- <p class="hint source-hint" id="sourceHint"></p>
543
- <input
544
- type="file"
545
- id="fileInput"
546
- name="zipFile"
547
- accept=".zip"
548
- style="display: none;"
549
- >
550
- <div id="importStatus" class="import-status"></div>
551
- <div id="sessions-container"></div>
552
-
553
- <!-- Loading indicator for infinite scroll -->
554
- <div id="loading-indicator" style="text-align: center; margin-top: 20px; display: none;">
555
- <div class="loading-spinner">Loading more sessions...</div>
556
- </div>
557
- </div>
558
- <% } %>
559
- </div>
560
-
561
- <script>
562
- window.__PAGE_DATA = {
563
- sessions: <%- JSON.stringify(sessions || []) %>,
564
- totalSessions: <%= totalSessions || 0 %>,
565
- hasMore: <%= hasMore || false %>,
566
- sourceHints: <%- sourceHints || '{}' %>
567
- };
568
- </script>
569
- <script src="/public/js/homepage.min.js"></script>
570
-
571
- <style>
572
- .date-group-header {
573
- color: #58a6ff;
574
- font-size: 18px;
575
- font-weight: 600;
576
- margin-top: 32px;
577
- margin-bottom: 16px;
578
- padding-bottom: 8px;
579
- }
580
- .date-group-header:first-child {
581
- margin-top: 0;
582
- }
583
-
584
- /* Custom summary tooltip */
585
- .summary-tooltip {
586
- display: none;
587
- position: fixed;
588
- z-index: 9999;
589
- background: #1c2128;
590
- border: 1px solid #30363d;
591
- border-radius: 8px;
592
- padding: 12px 16px;
593
- max-width: 600px;
594
- width: max-content;
595
- max-height: 400px;
596
- overflow-y: auto;
597
- font-size: 13px;
598
- line-height: 1.6;
599
- color: #c9d1d9;
600
- white-space: normal;
601
- word-break: break-word;
602
- box-shadow: 0 8px 24px rgba(0,0,0,0.5);
603
- pointer-events: none;
604
- }
605
- .summary-tooltip.visible {
606
- display: block;
607
- pointer-events: auto;
608
- }
609
- /* Markdown prose styles inside tooltip */
610
- .summary-tooltip h1, .summary-tooltip h2, .summary-tooltip h3,
611
- .summary-tooltip h4, .summary-tooltip h5, .summary-tooltip h6 {
612
- color: #e6edf3; margin: 8px 0 4px; font-weight: 600;
613
- }
614
- .summary-tooltip h1 { font-size: 15px; }
615
- .summary-tooltip h2 { font-size: 14px; }
616
- .summary-tooltip h3, .summary-tooltip h4 { font-size: 13px; }
617
- .summary-tooltip p { margin: 4px 0; }
618
- .summary-tooltip ul, .summary-tooltip ol { margin: 4px 0; padding-left: 18px; }
619
- .summary-tooltip li { margin: 2px 0; }
620
- .summary-tooltip code {
621
- background: #2d333b; border-radius: 3px;
622
- padding: 1px 4px; font-size: 12px; font-family: monospace;
623
- }
624
- .summary-tooltip pre {
625
- background: #2d333b; border-radius: 6px;
626
- padding: 8px 10px; overflow-x: auto; margin: 6px 0;
627
- }
628
- .summary-tooltip pre code { background: none; padding: 0; }
629
- .summary-tooltip strong { color: #e6edf3; }
630
- .summary-tooltip hr { border-color: #30363d; margin: 8px 0; }
631
-
632
- /* Hide hover tooltip on touch devices */
633
- @media (hover: none) {
634
- .summary-tooltip { display: none !important; }
635
- }
636
- /* Bottom sheet for mobile long-press */
637
- .bottom-sheet-overlay {
638
- display: none;
639
- position: fixed;
640
- inset: 0;
641
- background: rgba(0,0,0,0.5);
642
- z-index: 1000;
643
- touch-action: none;
644
- }
645
- .bottom-sheet-overlay.visible { display: block; }
646
- .bottom-sheet {
647
- position: fixed;
648
- bottom: 0;
649
- left: 0;
650
- right: 0;
651
- background: #161b22;
652
- border-top: 1px solid #30363d;
653
- border-radius: 16px 16px 0 0;
654
- padding: 0 16px max(env(safe-area-inset-bottom, 0px), 24px);
655
- max-height: 70vh;
656
- overflow-y: auto;
657
- z-index: 1001;
658
- transform: translateY(100%);
659
- transition: transform 0.28s cubic-bezier(0.32, 0.72, 0, 1);
660
- }
661
- .bottom-sheet.visible { transform: translateY(0); }
662
- .bottom-sheet-handle {
663
- width: 36px;
664
- height: 4px;
665
- background: #444c56;
666
- border-radius: 2px;
667
- margin: 12px auto 16px;
668
- }
669
- .bottom-sheet-content {
670
- font-size: 14px;
671
- line-height: 1.6;
672
- color: #c9d1d9;
673
- }
674
- .bottom-sheet-content h1, .bottom-sheet-content h2, .bottom-sheet-content h3 { color: #e6edf3; margin: 12px 0 6px; }
675
- .bottom-sheet-content h1 { font-size: 16px; }
676
- .bottom-sheet-content h2 { font-size: 15px; }
677
- .bottom-sheet-content h3 { font-size: 14px; }
678
- .bottom-sheet-content p { margin: 6px 0; }
679
- .bottom-sheet-content ul, .bottom-sheet-content ol { margin: 6px 0; padding-left: 20px; }
680
- .bottom-sheet-content li { margin: 3px 0; }
681
- .bottom-sheet-content code { background: #0d1117; padding: 1px 5px; border-radius: 4px; font-size: 13px; }
682
- .bottom-sheet-content pre { background: #0d1117; padding: 10px; border-radius: 6px; overflow-x: auto; }
683
- .bottom-sheet-content pre code { background: none; padding: 0; }
684
- .bottom-sheet-content strong { color: #e6edf3; }
685
- .bottom-sheet-content hr { border-color: #30363d; margin: 10px 0; }
686
- </style>
687
-
688
- <script src="https://cdn.jsdelivr.net/npm/marked@9/marked.min.js"></script>
689
- <div id="summary-tooltip" class="summary-tooltip"></div>
690
-
691
- <script>
692
- const tooltip = document.getElementById('summary-tooltip');
693
- let tooltipTarget = null;
694
- let hideTimer = null;
695
- let showTimer = null;
696
- let pendingEl = null;
697
- let pendingEvent = null;
698
-
699
- function showTooltip(el, e) {
700
- clearTimeout(hideTimer);
701
- tooltipTarget = el;
702
- if (!el.dataset.tooltipText) {
703
- el.dataset.tooltipText = el.getAttribute('title');
704
- el.removeAttribute('title');
705
- }
706
- const md = el.dataset.tooltipText || '';
707
- tooltip.innerHTML = (typeof marked !== 'undefined')
708
- ? marked.parse(md, { breaks: true })
709
- : md.replace(/</g, '&lt;').replace(/\n/g, '<br>');
710
- tooltip.classList.add('visible');
711
- positionTooltip(e);
712
- }
713
-
714
- function scheduleShow(el, e) {
715
- clearTimeout(showTimer);
716
- pendingEl = el;
717
- pendingEvent = e;
718
- showTimer = setTimeout(() => {
719
- if (pendingEl) showTooltip(pendingEl, pendingEvent);
720
- }, 500);
721
- }
722
-
723
- function cancelShow() {
724
- clearTimeout(showTimer);
725
- pendingEl = null;
726
- }
727
-
728
- function scheduleHide() {
729
- cancelShow();
730
- hideTimer = setTimeout(() => {
731
- tooltipTarget = null;
732
- tooltip.classList.remove('visible');
733
- }, 120);
734
- }
735
-
736
- document.addEventListener('mouseover', e => {
737
- const el = e.target.closest('.session-summary');
738
- if (el) { clearTimeout(hideTimer); scheduleShow(el, e); return; }
739
- if (e.target.closest('#summary-tooltip')) { clearTimeout(hideTimer); cancelShow(); return; }
740
- });
741
-
742
- document.addEventListener('mousemove', e => {
743
- if (pendingEl) { pendingEvent = e; }
744
- if (!tooltipTarget) return;
745
- if (e.target.closest('#summary-tooltip')) return;
746
- positionTooltip(e);
747
- });
748
-
749
- document.addEventListener('mouseout', e => {
750
- const toEl = e.relatedTarget;
751
- if (toEl && (toEl.closest('.session-summary') || toEl.closest('#summary-tooltip'))) return;
752
- scheduleHide();
753
- });
754
-
755
- tooltip.addEventListener('mouseleave', () => scheduleHide());
756
-
757
- function positionTooltip(e) {
758
- const pad = 14;
759
- const tw = tooltip.offsetWidth;
760
- const th = tooltip.offsetHeight;
761
- let x = e.clientX + pad;
762
- let y = e.clientY + pad;
763
- if (x + tw > window.innerWidth - 8) x = e.clientX - tw - pad;
764
- if (y + th > window.innerHeight - 8) y = e.clientY - th - pad;
765
- tooltip.style.left = x + 'px';
766
- tooltip.style.top = y + 'px';
767
- }
768
- </script>
769
-
770
- <!-- Bottom sheet for mobile long-press summary -->
771
- <div id="sheet-overlay" class="bottom-sheet-overlay">
772
- <div id="bottom-sheet" class="bottom-sheet">
773
- <div class="bottom-sheet-handle"></div>
774
- <div id="sheet-content" class="bottom-sheet-content"></div>
775
- </div>
776
- </div>
777
-
778
- <script>
779
- const sheetOverlay = document.getElementById('sheet-overlay');
780
- const bottomSheet = document.getElementById('bottom-sheet');
781
- const sheetContent = document.getElementById('sheet-content');
782
-
783
- function openSheet(md) {
784
- sheetContent.innerHTML = (typeof marked !== 'undefined')
785
- ? marked.parse(md, { breaks: true })
786
- : md.replace(/</g, '&lt;').replace(/\n/g, '<br>');
787
- sheetOverlay.classList.add('visible');
788
- requestAnimationFrame(() => bottomSheet.classList.add('visible'));
789
- }
790
-
791
- function closeSheet() {
792
- bottomSheet.classList.remove('visible');
793
- setTimeout(() => sheetOverlay.classList.remove('visible'), 280);
794
- }
795
-
796
- sheetOverlay.addEventListener('click', e => {
797
- if (!bottomSheet.contains(e.target)) closeSheet();
798
- });
799
-
800
- // Long-press detection (500ms)
801
- let lpTimer = null;
802
- let lpMoved = false;
803
-
804
- document.addEventListener('touchstart', e => {
805
- const el = e.target.closest('.session-summary');
806
- if (!el) return;
807
- lpMoved = false;
808
- const md = el.dataset.tooltipText || el.getAttribute('title') || '';
809
- if (!md) return;
810
- lpTimer = setTimeout(() => {
811
- if (!lpMoved) {
812
- e.preventDefault();
813
- openSheet(md);
814
- }
815
- }, 500);
816
- }, { passive: false });
817
-
818
- document.addEventListener('touchmove', () => {
819
- lpMoved = true;
820
- clearTimeout(lpTimer);
821
- }, { passive: true });
822
-
823
- document.addEventListener('touchend', () => clearTimeout(lpTimer), { passive: true });
824
- document.addEventListener('touchcancel', () => clearTimeout(lpTimer), { passive: true });
825
- </script>
826
- </body>
827
- </html>