claude-roi 0.5.0 → 0.6.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/README.md +2 -0
- package/package.json +2 -2
- package/src/dashboard.html +672 -2
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Codelens AI
|
|
2
2
|
|
|
3
|
+
**[codelensai-dev.vercel.app](https://codelensai-dev.vercel.app/)**
|
|
4
|
+
|
|
3
5
|
**Agent Productivity-to-Cost Correlator** — Is your AI coding agent actually shipping code?
|
|
4
6
|
|
|
5
7
|
Codelens AI ties Claude Code token usage to actual git output. It reads your local Claude Code session files, correlates them with git commits by timestamp, and serves a dashboard answering: *"Am I getting ROI from my AI coding agent?"*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-roi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Correlate Claude Code token usage with git output to measure AI coding agent ROI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"bugs": {
|
|
32
32
|
"url": "https://github.com/Akshat2634/Codelens-AI/issues"
|
|
33
33
|
},
|
|
34
|
-
"homepage": "https://
|
|
34
|
+
"homepage": "https://codelensai-dev.vercel.app/",
|
|
35
35
|
"engines": {
|
|
36
36
|
"node": ">=18.0.0"
|
|
37
37
|
},
|
package/src/dashboard.html
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Codelens AI — Agent Productivity Dashboard</title>
|
|
7
7
|
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&family=JetBrains+Mono:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
|
8
|
+
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect x='8' y='14' width='4' height='12' rx='1.5' fill='%23e67e22'/%3E%3Crect x='14' y='6' width='4' height='20' rx='1.5' fill='%232ecc71'/%3E%3Crect x='20' y='10' width='4' height='16' rx='1.5' fill='%233498db'/%3E%3C/svg%3E">
|
|
8
9
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
|
9
10
|
<script>
|
|
10
11
|
(function() {
|
|
@@ -206,8 +207,37 @@
|
|
|
206
207
|
background-size: 200% 200%;
|
|
207
208
|
-webkit-background-clip: text;
|
|
208
209
|
-webkit-text-fill-color: transparent;
|
|
210
|
+
background-clip: text;
|
|
209
211
|
animation: gradientMove 8s ease-in-out infinite;
|
|
210
212
|
margin-bottom: 12px;
|
|
213
|
+
display: flex;
|
|
214
|
+
align-items: center;
|
|
215
|
+
justify-content: center;
|
|
216
|
+
gap: 12px;
|
|
217
|
+
}
|
|
218
|
+
.logo-bars {
|
|
219
|
+
display: flex;
|
|
220
|
+
align-items: flex-end;
|
|
221
|
+
gap: 3px;
|
|
222
|
+
-webkit-text-fill-color: initial;
|
|
223
|
+
}
|
|
224
|
+
.logo-bars .bar-orange {
|
|
225
|
+
width: 5px;
|
|
226
|
+
height: 18px;
|
|
227
|
+
border-radius: 2px;
|
|
228
|
+
background: #e67e22;
|
|
229
|
+
}
|
|
230
|
+
.logo-bars .bar-teal {
|
|
231
|
+
width: 5px;
|
|
232
|
+
height: 28px;
|
|
233
|
+
border-radius: 2px;
|
|
234
|
+
background: #2ecc71;
|
|
235
|
+
}
|
|
236
|
+
.logo-bars .bar-blue {
|
|
237
|
+
width: 5px;
|
|
238
|
+
height: 22px;
|
|
239
|
+
border-radius: 2px;
|
|
240
|
+
background: #3498db;
|
|
211
241
|
}
|
|
212
242
|
@keyframes gradientMove {
|
|
213
243
|
0%, 100% { background-position: 0% 50%; }
|
|
@@ -1317,6 +1347,175 @@
|
|
|
1317
1347
|
.container { padding: 16px; }
|
|
1318
1348
|
header h1 { font-size: 1.8rem; }
|
|
1319
1349
|
.hero-legend, .cost-legend, .token-legend { flex-wrap: wrap; }
|
|
1350
|
+
.share-modal { padding: 20px; }
|
|
1351
|
+
.share-actions { flex-direction: column; }
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
/* ── Share Button Glow ─────────────────────── */
|
|
1355
|
+
.share-btn:hover {
|
|
1356
|
+
color: var(--accent-purple);
|
|
1357
|
+
border-color: rgba(168, 85, 247, 0.3);
|
|
1358
|
+
box-shadow: 0 0 12px rgba(168, 85, 247, 0.15);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
/* ── Share Modal ───────────────────────────── */
|
|
1362
|
+
.share-modal-backdrop {
|
|
1363
|
+
position: fixed;
|
|
1364
|
+
inset: 0;
|
|
1365
|
+
z-index: 1000;
|
|
1366
|
+
background: rgba(0, 0, 0, 0.6);
|
|
1367
|
+
backdrop-filter: blur(8px);
|
|
1368
|
+
display: flex;
|
|
1369
|
+
align-items: center;
|
|
1370
|
+
justify-content: center;
|
|
1371
|
+
animation: modalFadeIn 0.25s ease;
|
|
1372
|
+
}
|
|
1373
|
+
@keyframes modalFadeIn {
|
|
1374
|
+
from { opacity: 0; }
|
|
1375
|
+
to { opacity: 1; }
|
|
1376
|
+
}
|
|
1377
|
+
.share-modal {
|
|
1378
|
+
background: linear-gradient(145deg, var(--glass-bg-from), var(--glass-bg-to));
|
|
1379
|
+
backdrop-filter: blur(20px);
|
|
1380
|
+
border: 1px solid var(--glass-border);
|
|
1381
|
+
border-radius: var(--radius);
|
|
1382
|
+
padding: 32px;
|
|
1383
|
+
max-width: 680px;
|
|
1384
|
+
width: 90vw;
|
|
1385
|
+
max-height: 90vh;
|
|
1386
|
+
overflow-y: auto;
|
|
1387
|
+
position: relative;
|
|
1388
|
+
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.4);
|
|
1389
|
+
animation: modalSlideUp 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
|
1390
|
+
}
|
|
1391
|
+
@keyframes modalSlideUp {
|
|
1392
|
+
from { opacity: 0; transform: translateY(20px); }
|
|
1393
|
+
to { opacity: 1; transform: translateY(0); }
|
|
1394
|
+
}
|
|
1395
|
+
.share-modal-title {
|
|
1396
|
+
font-family: var(--font-display);
|
|
1397
|
+
font-size: 1.2rem;
|
|
1398
|
+
font-weight: 700;
|
|
1399
|
+
margin-bottom: 4px;
|
|
1400
|
+
background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple));
|
|
1401
|
+
-webkit-background-clip: text;
|
|
1402
|
+
-webkit-text-fill-color: transparent;
|
|
1403
|
+
background-clip: text;
|
|
1404
|
+
}
|
|
1405
|
+
.share-modal-subtitle {
|
|
1406
|
+
font-size: 0.85rem;
|
|
1407
|
+
color: var(--text-muted);
|
|
1408
|
+
margin-bottom: 24px;
|
|
1409
|
+
}
|
|
1410
|
+
.share-modal-close {
|
|
1411
|
+
position: absolute;
|
|
1412
|
+
top: 16px;
|
|
1413
|
+
right: 16px;
|
|
1414
|
+
width: 32px;
|
|
1415
|
+
height: 32px;
|
|
1416
|
+
border-radius: 50%;
|
|
1417
|
+
border: 1px solid var(--glass-border);
|
|
1418
|
+
background: var(--overlay-medium);
|
|
1419
|
+
color: var(--text-secondary);
|
|
1420
|
+
font-size: 1.2rem;
|
|
1421
|
+
cursor: pointer;
|
|
1422
|
+
display: flex;
|
|
1423
|
+
align-items: center;
|
|
1424
|
+
justify-content: center;
|
|
1425
|
+
transition: background 0.2s, color 0.2s;
|
|
1426
|
+
}
|
|
1427
|
+
.share-modal-close:hover {
|
|
1428
|
+
background: var(--overlay-intense);
|
|
1429
|
+
color: var(--text-primary);
|
|
1430
|
+
}
|
|
1431
|
+
.share-theme-toggle {
|
|
1432
|
+
display: flex;
|
|
1433
|
+
gap: 4px;
|
|
1434
|
+
margin-bottom: 16px;
|
|
1435
|
+
background: var(--bg-hover);
|
|
1436
|
+
border: 1px solid var(--border);
|
|
1437
|
+
border-radius: 20px;
|
|
1438
|
+
padding: 3px;
|
|
1439
|
+
width: fit-content;
|
|
1440
|
+
}
|
|
1441
|
+
.share-theme-toggle button {
|
|
1442
|
+
display: flex;
|
|
1443
|
+
align-items: center;
|
|
1444
|
+
gap: 6px;
|
|
1445
|
+
padding: 6px 16px;
|
|
1446
|
+
border-radius: 16px;
|
|
1447
|
+
border: none;
|
|
1448
|
+
background: transparent;
|
|
1449
|
+
color: var(--text-muted);
|
|
1450
|
+
font-family: var(--font-body);
|
|
1451
|
+
font-size: 0.8rem;
|
|
1452
|
+
font-weight: 500;
|
|
1453
|
+
cursor: pointer;
|
|
1454
|
+
transition: background 0.2s, color 0.2s;
|
|
1455
|
+
}
|
|
1456
|
+
.share-theme-toggle button:hover {
|
|
1457
|
+
color: var(--text-primary);
|
|
1458
|
+
}
|
|
1459
|
+
.share-theme-toggle button.active {
|
|
1460
|
+
background: var(--bg-card);
|
|
1461
|
+
color: var(--text-primary);
|
|
1462
|
+
box-shadow: 0 1px 6px var(--shadow-card);
|
|
1463
|
+
}
|
|
1464
|
+
.share-preview-wrap {
|
|
1465
|
+
display: flex;
|
|
1466
|
+
justify-content: center;
|
|
1467
|
+
margin-bottom: 24px;
|
|
1468
|
+
}
|
|
1469
|
+
.share-preview-wrap canvas {
|
|
1470
|
+
width: 100%;
|
|
1471
|
+
max-width: 420px;
|
|
1472
|
+
aspect-ratio: 3 / 4;
|
|
1473
|
+
border-radius: var(--radius-sm);
|
|
1474
|
+
box-shadow: 0 8px 32px var(--shadow-card);
|
|
1475
|
+
}
|
|
1476
|
+
.share-actions {
|
|
1477
|
+
display: flex;
|
|
1478
|
+
gap: 12px;
|
|
1479
|
+
justify-content: center;
|
|
1480
|
+
}
|
|
1481
|
+
.share-action-btn {
|
|
1482
|
+
display: flex;
|
|
1483
|
+
align-items: center;
|
|
1484
|
+
gap: 8px;
|
|
1485
|
+
padding: 10px 20px;
|
|
1486
|
+
border-radius: var(--radius-sm);
|
|
1487
|
+
border: 1px solid var(--glass-border);
|
|
1488
|
+
background: var(--overlay-medium);
|
|
1489
|
+
color: var(--text-secondary);
|
|
1490
|
+
font-family: var(--font-body);
|
|
1491
|
+
font-size: 0.85rem;
|
|
1492
|
+
cursor: pointer;
|
|
1493
|
+
transition: background 0.2s, border-color 0.2s, color 0.2s, transform 0.15s;
|
|
1494
|
+
}
|
|
1495
|
+
.share-action-btn:hover {
|
|
1496
|
+
background: var(--overlay-intense);
|
|
1497
|
+
border-color: var(--glass-border-hover);
|
|
1498
|
+
color: var(--text-primary);
|
|
1499
|
+
transform: translateY(-1px);
|
|
1500
|
+
}
|
|
1501
|
+
.share-action-btn.primary {
|
|
1502
|
+
background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(168, 85, 247, 0.2));
|
|
1503
|
+
border-color: rgba(59, 130, 246, 0.3);
|
|
1504
|
+
color: var(--text-primary);
|
|
1505
|
+
}
|
|
1506
|
+
.share-action-btn.primary:hover {
|
|
1507
|
+
background: linear-gradient(135deg, rgba(59, 130, 246, 0.3), rgba(168, 85, 247, 0.3));
|
|
1508
|
+
}
|
|
1509
|
+
.share-toast {
|
|
1510
|
+
text-align: center;
|
|
1511
|
+
margin-top: 16px;
|
|
1512
|
+
font-size: 0.8rem;
|
|
1513
|
+
color: var(--accent-green);
|
|
1514
|
+
opacity: 0;
|
|
1515
|
+
transition: opacity 0.3s;
|
|
1516
|
+
}
|
|
1517
|
+
.share-toast.visible {
|
|
1518
|
+
opacity: 1;
|
|
1320
1519
|
}
|
|
1321
1520
|
</style>
|
|
1322
1521
|
</head>
|
|
@@ -1324,6 +1523,11 @@
|
|
|
1324
1523
|
<div class="container">
|
|
1325
1524
|
<header>
|
|
1326
1525
|
<div class="header-actions">
|
|
1526
|
+
<button class="header-action-btn share-btn" id="share-btn" aria-label="Share report card" title="Share AI Report Card">
|
|
1527
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1528
|
+
<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
|
|
1529
|
+
</svg>
|
|
1530
|
+
</button>
|
|
1327
1531
|
<button class="header-action-btn refresh-btn" id="refresh-btn" aria-label="Refresh data" title="Refresh dashboard data">
|
|
1328
1532
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1329
1533
|
<polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
|
|
@@ -1347,7 +1551,7 @@
|
|
|
1347
1551
|
</button>
|
|
1348
1552
|
</div>
|
|
1349
1553
|
</div>
|
|
1350
|
-
<h1>Codelens AI</h1>
|
|
1554
|
+
<h1><span class="logo-bars"><span class="bar-orange"></span><span class="bar-teal"></span><span class="bar-blue"></span></span>Codelens AI</h1>
|
|
1351
1555
|
<p class="tagline">Correlates your AI coding agent's token spend with actual git output — see what shipped, what churned, and what it cost.</p>
|
|
1352
1556
|
<div class="meta-info">
|
|
1353
1557
|
<span class="badge" id="date-range"></span>
|
|
@@ -1364,10 +1568,43 @@
|
|
|
1364
1568
|
</div>
|
|
1365
1569
|
|
|
1366
1570
|
<footer>
|
|
1367
|
-
Made by <a href="https://www.linkedin.com/in/akshat2634/">Akshat</a> · Powered by <a href="https://code.claude.com/docs/en/overview">Claude Code</a> · <a href="https://github.com/Akshat2634/Codelens-AI">GitHub</a>
|
|
1571
|
+
Made by <a href="https://www.linkedin.com/in/akshat2634/">Akshat</a> · Powered by <a href="https://code.claude.com/docs/en/overview">Claude Code</a> · <a href="https://github.com/Akshat2634/Codelens-AI">GitHub</a> · <a href="https://codelensai-dev.vercel.app/">Website</a>
|
|
1368
1572
|
<div style="margin-top:6px;font-size:0.75rem;opacity:0.7;">Open source — contributions, ideas, and feedback welcome! <a href="https://github.com/Akshat2634/Codelens-AI" style="color:var(--accent-blue);">Star the repo</a> if you find it useful.</div>
|
|
1369
1573
|
<div style="margin-top:6px;font-size:0.7rem;opacity:0.4;">Cost estimates are approximate — based on Anthropic's published per-token pricing and may vary from actual billing. Currently supports Claude Code only. Support for Cursor, Codex, Gemini CLI, and more coming soon.</div>
|
|
1370
1574
|
</footer>
|
|
1575
|
+
|
|
1576
|
+
<!-- Share Report Card Modal -->
|
|
1577
|
+
<div class="share-modal-backdrop" id="share-modal" style="display:none;">
|
|
1578
|
+
<div class="share-modal">
|
|
1579
|
+
<button class="share-modal-close" id="share-modal-close" aria-label="Close">×</button>
|
|
1580
|
+
<h2 class="share-modal-title">Your AI Report Card</h2>
|
|
1581
|
+
<p class="share-modal-subtitle">Share your AI coding insights</p>
|
|
1582
|
+
<div class="share-theme-toggle" id="share-theme-toggle">
|
|
1583
|
+
<button data-share-theme="dark" class="active" title="Dark card">
|
|
1584
|
+
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
|
1585
|
+
Dark
|
|
1586
|
+
</button>
|
|
1587
|
+
<button data-share-theme="light" title="Light card">
|
|
1588
|
+
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
|
|
1589
|
+
Light
|
|
1590
|
+
</button>
|
|
1591
|
+
</div>
|
|
1592
|
+
<div class="share-preview-wrap">
|
|
1593
|
+
<canvas id="share-canvas"></canvas>
|
|
1594
|
+
</div>
|
|
1595
|
+
<div class="share-actions">
|
|
1596
|
+
<button class="share-action-btn primary" id="share-copy-btn">
|
|
1597
|
+
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
1598
|
+
Copy to Clipboard
|
|
1599
|
+
</button>
|
|
1600
|
+
<button class="share-action-btn" id="share-download-btn">
|
|
1601
|
+
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
|
1602
|
+
Download PNG
|
|
1603
|
+
</button>
|
|
1604
|
+
</div>
|
|
1605
|
+
<div class="share-toast" id="share-toast"></div>
|
|
1606
|
+
</div>
|
|
1607
|
+
</div>
|
|
1371
1608
|
</div>
|
|
1372
1609
|
|
|
1373
1610
|
<script>
|
|
@@ -2379,6 +2616,439 @@ window.toggleTimelineScale = function() {
|
|
|
2379
2616
|
if (btn) btn.textContent = timelineLogScale ? 'Linear scale' : 'Log scale';
|
|
2380
2617
|
};
|
|
2381
2618
|
|
|
2619
|
+
/* ── Share Report Card ─────────────────────────── */
|
|
2620
|
+
|
|
2621
|
+
const GRADE_HEX = { A: '#22d3a8', B: '#3b82f6', C: '#f59e0b', D: '#f0883e', F: '#ef4444' };
|
|
2622
|
+
const INSIGHT_COLORS = { warning: '#f59e0b', success: '#22d3a8', info: '#3b82f6', tip: '#a855f7' };
|
|
2623
|
+
|
|
2624
|
+
const SHARE_THEME = {
|
|
2625
|
+
dark: {
|
|
2626
|
+
bg1: '#0a0e17', bg2: '#111827',
|
|
2627
|
+
orb1: 'rgba(59, 130, 246, 0.06)', orb2: 'rgba(168, 85, 247, 0.05)', orb3: 'rgba(34, 211, 168, 0.04)',
|
|
2628
|
+
border: 'rgba(255,255,255,0.06)',
|
|
2629
|
+
glassBg: 'rgba(255,255,255,0.03)', glassBorder: 'rgba(255,255,255,0.06)',
|
|
2630
|
+
textPrimary: '#f0f4f8', textSecondary: '#94a3b8', textMuted: '#64748b', textDim: '#475569',
|
|
2631
|
+
barTrack: 'rgba(255,255,255,0.06)', heatmapEmpty: 'rgba(255,255,255,0.04)',
|
|
2632
|
+
separator: 'rgba(255,255,255,0.06)', separatorLight: 'rgba(255,255,255,0.04)',
|
|
2633
|
+
insightText: '#cbd5e1',
|
|
2634
|
+
},
|
|
2635
|
+
light: {
|
|
2636
|
+
bg1: '#f8fafc', bg2: '#e2e8f0',
|
|
2637
|
+
orb1: 'rgba(59, 130, 246, 0.08)', orb2: 'rgba(168, 85, 247, 0.06)', orb3: 'rgba(34, 211, 168, 0.05)',
|
|
2638
|
+
border: 'rgba(0,0,0,0.08)',
|
|
2639
|
+
glassBg: 'rgba(0,0,0,0.03)', glassBorder: 'rgba(0,0,0,0.08)',
|
|
2640
|
+
textPrimary: '#0f172a', textSecondary: '#475569', textMuted: '#64748b', textDim: '#94a3b8',
|
|
2641
|
+
barTrack: 'rgba(0,0,0,0.06)', heatmapEmpty: 'rgba(0,0,0,0.05)',
|
|
2642
|
+
separator: 'rgba(0,0,0,0.08)', separatorLight: 'rgba(0,0,0,0.05)',
|
|
2643
|
+
insightText: '#334155',
|
|
2644
|
+
}
|
|
2645
|
+
};
|
|
2646
|
+
|
|
2647
|
+
function drawRoundRect(ctx, x, y, w, h, r) {
|
|
2648
|
+
ctx.beginPath();
|
|
2649
|
+
ctx.moveTo(x + r, y);
|
|
2650
|
+
ctx.lineTo(x + w - r, y);
|
|
2651
|
+
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
|
|
2652
|
+
ctx.lineTo(x + w, y + h - r);
|
|
2653
|
+
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
|
|
2654
|
+
ctx.lineTo(x + r, y + h);
|
|
2655
|
+
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
|
|
2656
|
+
ctx.lineTo(x, y + r);
|
|
2657
|
+
ctx.quadraticCurveTo(x, y, x + r, y);
|
|
2658
|
+
ctx.closePath();
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
function drawOrb(ctx, x, y, radius, color) {
|
|
2662
|
+
const grad = ctx.createRadialGradient(x, y, 0, x, y, radius);
|
|
2663
|
+
grad.addColorStop(0, color);
|
|
2664
|
+
grad.addColorStop(1, 'transparent');
|
|
2665
|
+
ctx.fillStyle = grad;
|
|
2666
|
+
ctx.fillRect(x - radius, y - radius, radius * 2, radius * 2);
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
function drawGradeCircle(ctx, cx, cy, r, grade, th) {
|
|
2670
|
+
const color = GRADE_HEX[grade] || GRADE_HEX.F;
|
|
2671
|
+
// Background circle
|
|
2672
|
+
ctx.beginPath();
|
|
2673
|
+
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
|
2674
|
+
ctx.strokeStyle = th.barTrack;
|
|
2675
|
+
ctx.lineWidth = 6;
|
|
2676
|
+
ctx.stroke();
|
|
2677
|
+
// Grade arc
|
|
2678
|
+
const pct = { A: 0.95, B: 0.8, C: 0.6, D: 0.4, F: 0.2 }[grade] || 0.2;
|
|
2679
|
+
ctx.beginPath();
|
|
2680
|
+
ctx.arc(cx, cy, r, -Math.PI / 2, -Math.PI / 2 + Math.PI * 2 * pct);
|
|
2681
|
+
ctx.strokeStyle = color;
|
|
2682
|
+
ctx.lineWidth = 6;
|
|
2683
|
+
ctx.lineCap = 'round';
|
|
2684
|
+
ctx.stroke();
|
|
2685
|
+
// Grade letter
|
|
2686
|
+
ctx.font = 'bold 36px "JetBrains Mono", monospace';
|
|
2687
|
+
ctx.fillStyle = color;
|
|
2688
|
+
ctx.textAlign = 'center';
|
|
2689
|
+
ctx.textBaseline = 'middle';
|
|
2690
|
+
ctx.fillText(grade, cx, cy);
|
|
2691
|
+
// Label below
|
|
2692
|
+
ctx.font = '600 10px "JetBrains Mono", monospace';
|
|
2693
|
+
ctx.fillStyle = th.textMuted;
|
|
2694
|
+
ctx.fillText('ROI GRADE', cx, cy + r + 18);
|
|
2695
|
+
ctx.textAlign = 'left';
|
|
2696
|
+
ctx.textBaseline = 'alphabetic';
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
function drawStatBox(ctx, x, y, w, h, value, label, accentColor, th) {
|
|
2700
|
+
// Glass background
|
|
2701
|
+
drawRoundRect(ctx, x, y, w, h, 8);
|
|
2702
|
+
ctx.fillStyle = th.glassBg;
|
|
2703
|
+
ctx.fill();
|
|
2704
|
+
ctx.strokeStyle = th.glassBorder;
|
|
2705
|
+
ctx.lineWidth = 1;
|
|
2706
|
+
ctx.stroke();
|
|
2707
|
+
// Value
|
|
2708
|
+
ctx.font = 'bold 20px "JetBrains Mono", monospace';
|
|
2709
|
+
ctx.fillStyle = accentColor;
|
|
2710
|
+
ctx.fillText(value, x + 14, y + 28);
|
|
2711
|
+
// Label
|
|
2712
|
+
ctx.font = '400 11px "DM Sans", sans-serif';
|
|
2713
|
+
ctx.fillStyle = th.textMuted;
|
|
2714
|
+
ctx.fillText(label, x + 14, y + h - 10);
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
function drawProgressBar(ctx, x, y, w, label, value, suffix, color, th) {
|
|
2718
|
+
const barH = 8;
|
|
2719
|
+
const labelW = 140;
|
|
2720
|
+
const valueW = 50;
|
|
2721
|
+
const barW = w - labelW - valueW - 16;
|
|
2722
|
+
// Label
|
|
2723
|
+
ctx.font = '400 12px "DM Sans", sans-serif';
|
|
2724
|
+
ctx.fillStyle = th.textSecondary;
|
|
2725
|
+
ctx.fillText(label, x, y + 10);
|
|
2726
|
+
// Bar track
|
|
2727
|
+
const bx = x + labelW;
|
|
2728
|
+
drawRoundRect(ctx, bx, y + 3, barW, barH, 4);
|
|
2729
|
+
ctx.fillStyle = th.barTrack;
|
|
2730
|
+
ctx.fill();
|
|
2731
|
+
// Bar fill
|
|
2732
|
+
const fillW = Math.max(0, (value / 100) * barW);
|
|
2733
|
+
if (fillW > 0) {
|
|
2734
|
+
drawRoundRect(ctx, bx, y + 3, fillW, barH, 4);
|
|
2735
|
+
ctx.fillStyle = color;
|
|
2736
|
+
ctx.fill();
|
|
2737
|
+
}
|
|
2738
|
+
// Value text
|
|
2739
|
+
ctx.font = '600 12px "JetBrains Mono", monospace';
|
|
2740
|
+
ctx.fillStyle = color;
|
|
2741
|
+
ctx.textAlign = 'right';
|
|
2742
|
+
ctx.fillText(Math.round(value) + suffix, x + w, y + 11);
|
|
2743
|
+
ctx.textAlign = 'left';
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
function drawMiniHeatmap(ctx, x, y, w, heatmapData, th, isLight) {
|
|
2747
|
+
if (!heatmapData || !heatmapData.length) return;
|
|
2748
|
+
const cols = 24;
|
|
2749
|
+
const rows = 7;
|
|
2750
|
+
const gap = 2;
|
|
2751
|
+
const cellW = (w - (cols - 1) * gap) / cols;
|
|
2752
|
+
const cellH = Math.min(cellW, 10);
|
|
2753
|
+
const maxVal = Math.max(1, ...heatmapData.flat());
|
|
2754
|
+
const dayLabels = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
|
|
2755
|
+
const labelOffset = 14;
|
|
2756
|
+
|
|
2757
|
+
for (let day = 0; day < rows; day++) {
|
|
2758
|
+
// Day label
|
|
2759
|
+
ctx.font = '500 8px "JetBrains Mono", monospace';
|
|
2760
|
+
ctx.fillStyle = th.textDim;
|
|
2761
|
+
ctx.fillText(dayLabels[day], x, y + day * (cellH + gap) + cellH - 1);
|
|
2762
|
+
for (let hour = 0; hour < cols; hour++) {
|
|
2763
|
+
const val = heatmapData[day]?.[hour] || 0;
|
|
2764
|
+
const cx = x + labelOffset + hour * (cellW + gap);
|
|
2765
|
+
const cy = y + day * (cellH + gap);
|
|
2766
|
+
drawRoundRect(ctx, cx, cy, cellW, cellH, 2);
|
|
2767
|
+
if (val === 0) {
|
|
2768
|
+
ctx.fillStyle = th.heatmapEmpty;
|
|
2769
|
+
} else {
|
|
2770
|
+
const intensity = val / maxVal;
|
|
2771
|
+
if (isLight) {
|
|
2772
|
+
ctx.fillStyle = `rgba(5,150,105,${0.25 + intensity * 0.75})`;
|
|
2773
|
+
} else {
|
|
2774
|
+
ctx.fillStyle = `rgba(34,211,168,${0.2 + intensity * 0.8})`;
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
ctx.fill();
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
function drawInsightRow(ctx, x, y, w, insight, th) {
|
|
2783
|
+
const color = INSIGHT_COLORS[insight.type] || INSIGHT_COLORS.info;
|
|
2784
|
+
// Dot indicator
|
|
2785
|
+
ctx.beginPath();
|
|
2786
|
+
ctx.arc(x + 6, y + 8, 4, 0, Math.PI * 2);
|
|
2787
|
+
ctx.fillStyle = color;
|
|
2788
|
+
ctx.fill();
|
|
2789
|
+
// Text (truncate if needed)
|
|
2790
|
+
ctx.font = '400 12px "DM Sans", sans-serif';
|
|
2791
|
+
ctx.fillStyle = th.insightText;
|
|
2792
|
+
let text = insight.text;
|
|
2793
|
+
// Truncate to fit width
|
|
2794
|
+
const maxW = w - 24;
|
|
2795
|
+
while (ctx.measureText(text).width > maxW && text.length > 3) {
|
|
2796
|
+
text = text.slice(0, -4) + '...';
|
|
2797
|
+
}
|
|
2798
|
+
ctx.fillText(text, x + 18, y + 12);
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
function renderShareCard(canvas) {
|
|
2802
|
+
const W = 600, H = 800;
|
|
2803
|
+
canvas.width = W * 2;
|
|
2804
|
+
canvas.height = H * 2;
|
|
2805
|
+
const ctx = canvas.getContext('2d');
|
|
2806
|
+
ctx.scale(2, 2);
|
|
2807
|
+
|
|
2808
|
+
const isLight = shareCardTheme === 'light';
|
|
2809
|
+
const th = isLight ? SHARE_THEME.light : SHARE_THEME.dark;
|
|
2810
|
+
|
|
2811
|
+
const d = DATA;
|
|
2812
|
+
const s = d.summary;
|
|
2813
|
+
const t = d.tokenAnalytics;
|
|
2814
|
+
const ls = d.lineSurvival;
|
|
2815
|
+
|
|
2816
|
+
// ── Background ──
|
|
2817
|
+
const bgGrad = ctx.createLinearGradient(0, 0, W, H);
|
|
2818
|
+
bgGrad.addColorStop(0, th.bg1);
|
|
2819
|
+
bgGrad.addColorStop(0.5, th.bg2);
|
|
2820
|
+
bgGrad.addColorStop(1, th.bg1);
|
|
2821
|
+
ctx.fillStyle = bgGrad;
|
|
2822
|
+
ctx.fillRect(0, 0, W, H);
|
|
2823
|
+
|
|
2824
|
+
// Subtle orbs
|
|
2825
|
+
drawOrb(ctx, W * 0.2, H * 0.12, 200, th.orb1);
|
|
2826
|
+
drawOrb(ctx, W * 0.85, H * 0.35, 180, th.orb2);
|
|
2827
|
+
drawOrb(ctx, W * 0.3, H * 0.85, 160, th.orb3);
|
|
2828
|
+
|
|
2829
|
+
// Border
|
|
2830
|
+
drawRoundRect(ctx, 0, 0, W, H, 16);
|
|
2831
|
+
ctx.strokeStyle = th.border;
|
|
2832
|
+
ctx.lineWidth = 1;
|
|
2833
|
+
ctx.stroke();
|
|
2834
|
+
|
|
2835
|
+
// ── Header with logo bars ──
|
|
2836
|
+
let y = 36;
|
|
2837
|
+
// Draw 3-bar logo
|
|
2838
|
+
const barX = 32;
|
|
2839
|
+
const barW = 5;
|
|
2840
|
+
const barGap = 4;
|
|
2841
|
+
const barBaseY = y + 26;
|
|
2842
|
+
drawRoundRect(ctx, barX, barBaseY - 16, barW, 16, 2);
|
|
2843
|
+
ctx.fillStyle = '#e67e22';
|
|
2844
|
+
ctx.fill();
|
|
2845
|
+
drawRoundRect(ctx, barX + barW + barGap, barBaseY - 26, barW, 26, 2);
|
|
2846
|
+
ctx.fillStyle = '#2ecc71';
|
|
2847
|
+
ctx.fill();
|
|
2848
|
+
drawRoundRect(ctx, barX + (barW + barGap) * 2, barBaseY - 20, barW, 20, 2);
|
|
2849
|
+
ctx.fillStyle = '#3498db';
|
|
2850
|
+
ctx.fill();
|
|
2851
|
+
// Title text after logo
|
|
2852
|
+
const titleX = barX + (barW + barGap) * 3 + 8;
|
|
2853
|
+
ctx.font = 'bold 28px "JetBrains Mono", monospace';
|
|
2854
|
+
const titleGrad = ctx.createLinearGradient(titleX, y, titleX + 240, y + 28);
|
|
2855
|
+
if (isLight) {
|
|
2856
|
+
titleGrad.addColorStop(0, '#2563eb');
|
|
2857
|
+
titleGrad.addColorStop(0.5, '#7c3aed');
|
|
2858
|
+
titleGrad.addColorStop(1, '#0891b2');
|
|
2859
|
+
} else {
|
|
2860
|
+
titleGrad.addColorStop(0, '#3b82f6');
|
|
2861
|
+
titleGrad.addColorStop(0.5, '#a855f7');
|
|
2862
|
+
titleGrad.addColorStop(1, '#06b6d4');
|
|
2863
|
+
}
|
|
2864
|
+
ctx.fillStyle = titleGrad;
|
|
2865
|
+
ctx.fillText('Codelens AI', titleX, y + 26);
|
|
2866
|
+
|
|
2867
|
+
y += 44;
|
|
2868
|
+
ctx.font = '300 13px "DM Sans", sans-serif';
|
|
2869
|
+
ctx.fillStyle = th.textMuted;
|
|
2870
|
+
ctx.fillText('AI Coding Report Card', 32, y);
|
|
2871
|
+
|
|
2872
|
+
y += 20;
|
|
2873
|
+
const fmtDate = iso => {
|
|
2874
|
+
try { return new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); }
|
|
2875
|
+
catch { return iso; }
|
|
2876
|
+
};
|
|
2877
|
+
const dateLabel = (d.meta.startDate && d.meta.endDate)
|
|
2878
|
+
? fmtDate(d.meta.startDate) + ' – ' + fmtDate(d.meta.endDate)
|
|
2879
|
+
: 'Last ' + d.meta.daysAnalyzed + ' days';
|
|
2880
|
+
ctx.font = '500 11px "JetBrains Mono", monospace';
|
|
2881
|
+
ctx.fillStyle = th.textDim;
|
|
2882
|
+
ctx.fillText(dateLabel, 32, y);
|
|
2883
|
+
|
|
2884
|
+
// Thin separator
|
|
2885
|
+
y += 16;
|
|
2886
|
+
ctx.strokeStyle = th.separator;
|
|
2887
|
+
ctx.lineWidth = 1;
|
|
2888
|
+
ctx.beginPath();
|
|
2889
|
+
ctx.moveTo(32, y);
|
|
2890
|
+
ctx.lineTo(W - 32, y);
|
|
2891
|
+
ctx.stroke();
|
|
2892
|
+
|
|
2893
|
+
// ── Grade + Stats Row ──
|
|
2894
|
+
y += 20;
|
|
2895
|
+
drawGradeCircle(ctx, 80, y + 52, 42, s.overallGrade, th);
|
|
2896
|
+
|
|
2897
|
+
const boxW = 152;
|
|
2898
|
+
const boxH = 52;
|
|
2899
|
+
const boxGap = 12;
|
|
2900
|
+
const boxStartX = 160;
|
|
2901
|
+
// Deeper accent colors for light theme so they pop against white bg
|
|
2902
|
+
const cOrange = isLight ? '#d97706' : '#f59e0b';
|
|
2903
|
+
const cGreen = isLight ? '#059669' : '#22d3a8';
|
|
2904
|
+
const cBlue = isLight ? '#2563eb' : '#3b82f6';
|
|
2905
|
+
const cPurple = isLight ? '#7c3aed' : '#a855f7';
|
|
2906
|
+
drawStatBox(ctx, boxStartX, y, boxW, boxH, '$' + s.totalCost.toFixed(2), 'Total Spend', cOrange, th);
|
|
2907
|
+
drawStatBox(ctx, boxStartX + boxW + boxGap, y, boxW, boxH, String(s.totalCommits), 'Commits Shipped', cGreen, th);
|
|
2908
|
+
drawStatBox(ctx, boxStartX, y + boxH + boxGap, boxW, boxH,
|
|
2909
|
+
s.avgCostPerCommit != null ? '$' + s.avgCostPerCommit.toFixed(2) : 'N/A', 'Avg $/Commit', cBlue, th);
|
|
2910
|
+
drawStatBox(ctx, boxStartX + boxW + boxGap, y + boxH + boxGap, boxW, boxH,
|
|
2911
|
+
String(s.totalSessions), 'Sessions', cPurple, th);
|
|
2912
|
+
|
|
2913
|
+
// ── Efficiency Bars ──
|
|
2914
|
+
y += (boxH + boxGap) * 2 + 24;
|
|
2915
|
+
ctx.font = '600 10px "JetBrains Mono", monospace';
|
|
2916
|
+
ctx.fillStyle = th.textDim;
|
|
2917
|
+
ctx.letterSpacing = '0.08em';
|
|
2918
|
+
ctx.fillText('EFFICIENCY', 32, y);
|
|
2919
|
+
ctx.letterSpacing = '0';
|
|
2920
|
+
y += 18;
|
|
2921
|
+
|
|
2922
|
+
// Each bar gets its own accent color for visual variety
|
|
2923
|
+
const effColor = isLight ? '#0d9488' : '#06b6d4'; // cyan/teal
|
|
2924
|
+
const cacheColor = isLight ? '#7c3aed' : '#a855f7'; // purple
|
|
2925
|
+
const survColor = isLight ? '#059669' : '#22d3a8'; // green
|
|
2926
|
+
drawProgressBar(ctx, 32, y, W - 64, 'Token Efficiency', t.tokenEfficiencyRate, '%', effColor, th);
|
|
2927
|
+
y += 34;
|
|
2928
|
+
drawProgressBar(ctx, 32, y, W - 64, 'Cache Hit Rate', t.cacheHitRate, '%', cacheColor, th);
|
|
2929
|
+
y += 34;
|
|
2930
|
+
drawProgressBar(ctx, 32, y, W - 64, 'Line Survival', ls.survivalRate, '%', survColor, th);
|
|
2931
|
+
|
|
2932
|
+
// ── Heatmap ──
|
|
2933
|
+
y += 40;
|
|
2934
|
+
ctx.font = '600 10px "JetBrains Mono", monospace';
|
|
2935
|
+
ctx.fillStyle = th.textDim;
|
|
2936
|
+
ctx.fillText('PRODUCTIVITY HEATMAP', 32, y);
|
|
2937
|
+
y += 14;
|
|
2938
|
+
drawMiniHeatmap(ctx, 32, y, W - 64, d.heatmap.commits, th, isLight);
|
|
2939
|
+
|
|
2940
|
+
// ── Top Insights ──
|
|
2941
|
+
y += 100;
|
|
2942
|
+
const topInsights = (d.insights || []).slice(0, 3);
|
|
2943
|
+
if (topInsights.length > 0) {
|
|
2944
|
+
ctx.font = '600 10px "JetBrains Mono", monospace';
|
|
2945
|
+
ctx.fillStyle = th.textDim;
|
|
2946
|
+
ctx.fillText('KEY INSIGHTS', 32, y);
|
|
2947
|
+
y += 14;
|
|
2948
|
+
for (const insight of topInsights) {
|
|
2949
|
+
drawInsightRow(ctx, 32, y, W - 64, insight, th);
|
|
2950
|
+
y += 28;
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
// ── Footer ──
|
|
2955
|
+
ctx.font = '400 10px "DM Sans", sans-serif';
|
|
2956
|
+
ctx.fillStyle = th.textDim;
|
|
2957
|
+
ctx.fillText('Generated by Codelens AI · codelensai-dev.vercel.app', 32, H - 24);
|
|
2958
|
+
|
|
2959
|
+
// Subtle footer separator
|
|
2960
|
+
ctx.strokeStyle = th.separatorLight;
|
|
2961
|
+
ctx.lineWidth = 1;
|
|
2962
|
+
ctx.beginPath();
|
|
2963
|
+
ctx.moveTo(32, H - 40);
|
|
2964
|
+
ctx.lineTo(W - 32, H - 40);
|
|
2965
|
+
ctx.stroke();
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
let shareCardTheme = 'dark';
|
|
2969
|
+
|
|
2970
|
+
function updateShareThemeToggle() {
|
|
2971
|
+
document.querySelectorAll('#share-theme-toggle button').forEach(btn => {
|
|
2972
|
+
btn.classList.toggle('active', btn.dataset.shareTheme === shareCardTheme);
|
|
2973
|
+
});
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
function openShareModal() {
|
|
2977
|
+
if (!DATA) return;
|
|
2978
|
+
shareCardTheme = getTheme();
|
|
2979
|
+
updateShareThemeToggle();
|
|
2980
|
+
const modal = document.getElementById('share-modal');
|
|
2981
|
+
modal.style.display = '';
|
|
2982
|
+
document.body.style.overflow = 'hidden';
|
|
2983
|
+
const canvas = document.getElementById('share-canvas');
|
|
2984
|
+
document.fonts.ready.then(() => renderShareCard(canvas));
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
function closeShareModal() {
|
|
2988
|
+
const modal = document.getElementById('share-modal');
|
|
2989
|
+
modal.style.display = 'none';
|
|
2990
|
+
document.body.style.overflow = '';
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
async function copyShareCard() {
|
|
2994
|
+
const canvas = document.getElementById('share-canvas');
|
|
2995
|
+
try {
|
|
2996
|
+
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
|
|
2997
|
+
await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
|
|
2998
|
+
showShareToast('Copied to clipboard!');
|
|
2999
|
+
} catch {
|
|
3000
|
+
showShareToast('Clipboard not supported — downloading instead');
|
|
3001
|
+
downloadShareCard();
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
|
|
3005
|
+
function downloadShareCard() {
|
|
3006
|
+
const canvas = document.getElementById('share-canvas');
|
|
3007
|
+
const link = document.createElement('a');
|
|
3008
|
+
link.download = 'codelens-ai-report-card.png';
|
|
3009
|
+
link.href = canvas.toDataURL('image/png');
|
|
3010
|
+
link.click();
|
|
3011
|
+
showShareToast('Downloaded!');
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
function showShareToast(msg) {
|
|
3015
|
+
const toast = document.getElementById('share-toast');
|
|
3016
|
+
toast.textContent = msg;
|
|
3017
|
+
toast.classList.add('visible');
|
|
3018
|
+
setTimeout(() => toast.classList.remove('visible'), 2500);
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
// Wire up share modal events
|
|
3022
|
+
(function initShareModal() {
|
|
3023
|
+
const shareBtn = document.getElementById('share-btn');
|
|
3024
|
+
const shareModal = document.getElementById('share-modal');
|
|
3025
|
+
const closeBtn = document.getElementById('share-modal-close');
|
|
3026
|
+
const copyBtn = document.getElementById('share-copy-btn');
|
|
3027
|
+
const downloadBtn = document.getElementById('share-download-btn');
|
|
3028
|
+
|
|
3029
|
+
if (shareBtn) shareBtn.addEventListener('click', openShareModal);
|
|
3030
|
+
if (closeBtn) closeBtn.addEventListener('click', closeShareModal);
|
|
3031
|
+
if (shareModal) shareModal.addEventListener('click', function(e) { if (e.target === shareModal) closeShareModal(); });
|
|
3032
|
+
if (copyBtn) copyBtn.addEventListener('click', copyShareCard);
|
|
3033
|
+
if (downloadBtn) downloadBtn.addEventListener('click', downloadShareCard);
|
|
3034
|
+
document.addEventListener('keydown', function(e) {
|
|
3035
|
+
if (e.key === 'Escape' && shareModal && shareModal.style.display !== 'none') closeShareModal();
|
|
3036
|
+
});
|
|
3037
|
+
|
|
3038
|
+
// Share card theme toggle
|
|
3039
|
+
const themeToggle = document.getElementById('share-theme-toggle');
|
|
3040
|
+
if (themeToggle) {
|
|
3041
|
+
themeToggle.addEventListener('click', function(e) {
|
|
3042
|
+
const btn = e.target.closest('[data-share-theme]');
|
|
3043
|
+
if (!btn) return;
|
|
3044
|
+
shareCardTheme = btn.dataset.shareTheme;
|
|
3045
|
+
updateShareThemeToggle();
|
|
3046
|
+
const canvas = document.getElementById('share-canvas');
|
|
3047
|
+
renderShareCard(canvas);
|
|
3048
|
+
});
|
|
3049
|
+
}
|
|
3050
|
+
})();
|
|
3051
|
+
|
|
2382
3052
|
</script>
|
|
2383
3053
|
</body>
|
|
2384
3054
|
</html>
|