quackscore 0.1.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.
Files changed (139) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +158 -0
  3. package/dist/commands/create.d.ts +6 -0
  4. package/dist/commands/create.d.ts.map +1 -0
  5. package/dist/commands/create.js +165 -0
  6. package/dist/commands/create.js.map +1 -0
  7. package/dist/commands/init.d.ts +8 -0
  8. package/dist/commands/init.d.ts.map +1 -0
  9. package/dist/commands/init.js +62 -0
  10. package/dist/commands/init.js.map +1 -0
  11. package/dist/commands/leaderboard.d.ts +2 -0
  12. package/dist/commands/leaderboard.d.ts.map +1 -0
  13. package/dist/commands/leaderboard.js +36 -0
  14. package/dist/commands/leaderboard.js.map +1 -0
  15. package/dist/commands/mock-report.d.ts +4 -0
  16. package/dist/commands/mock-report.d.ts.map +1 -0
  17. package/dist/commands/mock-report.js +311 -0
  18. package/dist/commands/mock-report.js.map +1 -0
  19. package/dist/commands/show.d.ts +2 -0
  20. package/dist/commands/show.d.ts.map +1 -0
  21. package/dist/commands/show.js +17 -0
  22. package/dist/commands/show.js.map +1 -0
  23. package/dist/commands/update.d.ts +3 -0
  24. package/dist/commands/update.d.ts.map +1 -0
  25. package/dist/commands/update.js +175 -0
  26. package/dist/commands/update.js.map +1 -0
  27. package/dist/config/index.d.ts +4 -0
  28. package/dist/config/index.d.ts.map +1 -0
  29. package/dist/config/index.js +3 -0
  30. package/dist/config/index.js.map +1 -0
  31. package/dist/config/manager.d.ts +7 -0
  32. package/dist/config/manager.d.ts.map +1 -0
  33. package/dist/config/manager.js +29 -0
  34. package/dist/config/manager.js.map +1 -0
  35. package/dist/config/types.d.ts +14 -0
  36. package/dist/config/types.d.ts.map +1 -0
  37. package/dist/config/types.js +21 -0
  38. package/dist/config/types.js.map +1 -0
  39. package/dist/github/client.d.ts +8 -0
  40. package/dist/github/client.d.ts.map +1 -0
  41. package/dist/github/client.js +241 -0
  42. package/dist/github/client.js.map +1 -0
  43. package/dist/github/index.d.ts +3 -0
  44. package/dist/github/index.d.ts.map +1 -0
  45. package/dist/github/index.js +2 -0
  46. package/dist/github/index.js.map +1 -0
  47. package/dist/index.d.ts +3 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +93 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/llm/analyzer.d.ts +7 -0
  52. package/dist/llm/analyzer.d.ts.map +1 -0
  53. package/dist/llm/analyzer.js +249 -0
  54. package/dist/llm/analyzer.js.map +1 -0
  55. package/dist/llm/character.d.ts +16 -0
  56. package/dist/llm/character.d.ts.map +1 -0
  57. package/dist/llm/character.js +245 -0
  58. package/dist/llm/character.js.map +1 -0
  59. package/dist/llm/client.d.ts +5 -0
  60. package/dist/llm/client.d.ts.map +1 -0
  61. package/dist/llm/client.js +98 -0
  62. package/dist/llm/client.js.map +1 -0
  63. package/dist/llm/index.d.ts +4 -0
  64. package/dist/llm/index.d.ts.map +1 -0
  65. package/dist/llm/index.js +4 -0
  66. package/dist/llm/index.js.map +1 -0
  67. package/dist/llm/prompts.d.ts +4 -0
  68. package/dist/llm/prompts.d.ts.map +1 -0
  69. package/dist/llm/prompts.js +109 -0
  70. package/dist/llm/prompts.js.map +1 -0
  71. package/dist/llm/providers.d.ts +10 -0
  72. package/dist/llm/providers.d.ts.map +1 -0
  73. package/dist/llm/providers.js +70 -0
  74. package/dist/llm/providers.js.map +1 -0
  75. package/dist/report/generate.d.ts +3 -0
  76. package/dist/report/generate.d.ts.map +1 -0
  77. package/dist/report/generate.js +1540 -0
  78. package/dist/report/generate.js.map +1 -0
  79. package/dist/report/index.d.ts +2 -0
  80. package/dist/report/index.d.ts.map +1 -0
  81. package/dist/report/index.js +2 -0
  82. package/dist/report/index.js.map +1 -0
  83. package/dist/scoring/index.d.ts +3 -0
  84. package/dist/scoring/index.d.ts.map +1 -0
  85. package/dist/scoring/index.js +3 -0
  86. package/dist/scoring/index.js.map +1 -0
  87. package/dist/scoring/points.d.ts +8 -0
  88. package/dist/scoring/points.d.ts.map +1 -0
  89. package/dist/scoring/points.js +28 -0
  90. package/dist/scoring/points.js.map +1 -0
  91. package/dist/scoring/stats.d.ts +3 -0
  92. package/dist/scoring/stats.d.ts.map +1 -0
  93. package/dist/scoring/stats.js +80 -0
  94. package/dist/scoring/stats.js.map +1 -0
  95. package/dist/shared/diagnostics.d.ts +8 -0
  96. package/dist/shared/diagnostics.d.ts.map +1 -0
  97. package/dist/shared/diagnostics.js +99 -0
  98. package/dist/shared/diagnostics.js.map +1 -0
  99. package/dist/shared/pr-summary.d.ts +5 -0
  100. package/dist/shared/pr-summary.d.ts.map +1 -0
  101. package/dist/shared/pr-summary.js +272 -0
  102. package/dist/shared/pr-summary.js.map +1 -0
  103. package/dist/shared/profile-summary.d.ts +5 -0
  104. package/dist/shared/profile-summary.d.ts.map +1 -0
  105. package/dist/shared/profile-summary.js +262 -0
  106. package/dist/shared/profile-summary.js.map +1 -0
  107. package/dist/shared/technologies.d.ts +5 -0
  108. package/dist/shared/technologies.d.ts.map +1 -0
  109. package/dist/shared/technologies.js +211 -0
  110. package/dist/shared/technologies.js.map +1 -0
  111. package/dist/shared/types.d.ts +61 -0
  112. package/dist/shared/types.d.ts.map +1 -0
  113. package/dist/shared/types.js +14 -0
  114. package/dist/shared/types.js.map +1 -0
  115. package/dist/shared/ui.d.ts +7 -0
  116. package/dist/shared/ui.d.ts.map +1 -0
  117. package/dist/shared/ui.js +24 -0
  118. package/dist/shared/ui.js.map +1 -0
  119. package/dist/storage/index.d.ts +4 -0
  120. package/dist/storage/index.d.ts.map +1 -0
  121. package/dist/storage/index.js +4 -0
  122. package/dist/storage/index.js.map +1 -0
  123. package/dist/storage/leaderboard.d.ts +4 -0
  124. package/dist/storage/leaderboard.d.ts.map +1 -0
  125. package/dist/storage/leaderboard.js +37 -0
  126. package/dist/storage/leaderboard.js.map +1 -0
  127. package/dist/storage/paths.d.ts +6 -0
  128. package/dist/storage/paths.d.ts.map +1 -0
  129. package/dist/storage/paths.js +19 -0
  130. package/dist/storage/paths.js.map +1 -0
  131. package/dist/storage/report.d.ts +3 -0
  132. package/dist/storage/report.d.ts.map +1 -0
  133. package/dist/storage/report.js +21 -0
  134. package/dist/storage/report.js.map +1 -0
  135. package/dist/storage/user.d.ts +4 -0
  136. package/dist/storage/user.d.ts.map +1 -0
  137. package/dist/storage/user.js +44 -0
  138. package/dist/storage/user.js.map +1 -0
  139. package/package.json +52 -0
@@ -0,0 +1,1540 @@
1
+ import { LEVEL_THRESHOLDS } from '../scoring/index.js';
2
+ import { diagLog } from '../shared/diagnostics.js';
3
+ import { buildManagerProfileSummary } from '../shared/profile-summary.js';
4
+ import { normalizeTechnologies } from '../shared/technologies.js';
5
+ import { buildFallbackPRSummary, getLowValuePRSummaryReason } from '../shared/pr-summary.js';
6
+ const DISPLAY_LABELS = {
7
+ ai: 'AI',
8
+ ai_coding_optimizations: 'AI Coding',
9
+ api: 'API',
10
+ cpp: 'C++',
11
+ ci_cd: 'CI/CD',
12
+ css: 'CSS',
13
+ devops: 'DevOps',
14
+ dotnet: '.NET',
15
+ game_dev: 'Game Dev',
16
+ html: 'HTML',
17
+ javascript: 'JavaScript',
18
+ new_development: 'New Development',
19
+ nodejs: 'Node.js',
20
+ qa: 'QA',
21
+ sre: 'SRE',
22
+ sql: 'SQL',
23
+ typescript: 'TypeScript',
24
+ web_frontend: 'Web Frontend',
25
+ mobile_frontend: 'Mobile Frontend',
26
+ };
27
+ const TECH_PALETTE = ['#f59e0b', '#10b981', '#3b82f6', '#8b5cf6', '#ef4444', '#ec4899'];
28
+ const TYPE_COLORS = ['#10b981', '#f59e0b', '#ef4444', '#3b82f6'];
29
+ const RADAR_FALLBACK_KEYS = ['backend', 'web_frontend', 'devops', 'security', 'ai', 'qa'];
30
+ function pct(value, total) {
31
+ if (total === 0)
32
+ return '0.0';
33
+ return ((value / total) * 100).toFixed(1);
34
+ }
35
+ function escapeHtml(value) {
36
+ return value
37
+ .replace(/&/g, '&')
38
+ .replace(/</g, '&lt;')
39
+ .replace(/>/g, '&gt;')
40
+ .replace(/"/g, '&quot;')
41
+ .replace(/'/g, '&#39;');
42
+ }
43
+ function formatLabel(value) {
44
+ return DISPLAY_LABELS[value] ?? value.split('_').map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(' ');
45
+ }
46
+ function formatDate(value) {
47
+ return new Date(value).toLocaleDateString('en-US', { day: '2-digit', month: 'short', year: 'numeric' });
48
+ }
49
+ function getRankTitle(level) {
50
+ if (level >= 12)
51
+ return 'Distinguished Principal';
52
+ if (level >= 10)
53
+ return 'Principal Contributor';
54
+ if (level >= 8)
55
+ return 'Lead Contributor';
56
+ if (level >= 6)
57
+ return 'Senior Contributor';
58
+ if (level >= 4)
59
+ return 'Contributor II';
60
+ if (level >= 2)
61
+ return 'Contributor I';
62
+ return 'Emerging Contributor';
63
+ }
64
+ const LOW_VALUE_CHARACTER_PATTERNS = [
65
+ /^mostly takes ownership of\b/i,
66
+ /\ba good example is\b/i,
67
+ /\borg-scoped report\b/i,
68
+ /\bstrongest in\b/i,
69
+ /\barmed with\b/i,
70
+ /\banalyzed prs?\b/i,
71
+ /\bpull requests?\b/i,
72
+ /\blevel\s+\d+\b/i,
73
+ /\b\d[\d,]*\s*xp\b/i,
74
+ ];
75
+ function isLowValueCharacterSummary(summary) {
76
+ if (!summary)
77
+ return true;
78
+ const normalized = summary.replace(/\s+/g, ' ').trim();
79
+ if (!normalized)
80
+ return true;
81
+ return LOW_VALUE_CHARACTER_PATTERNS.some((pattern) => pattern.test(normalized));
82
+ }
83
+ function isLowValuePRSummary(summary) {
84
+ return getLowValuePRSummaryReason(summary) !== null;
85
+ }
86
+ function getDisplayPRSummary(pr) {
87
+ const analysis = pr.analysis;
88
+ if (!analysis || analysis.skipped) {
89
+ return analysis?.skipReason ?? 'No analysis available for this PR.';
90
+ }
91
+ return isLowValuePRSummary(analysis.summary)
92
+ ? buildFallbackPRSummary(pr)
93
+ : analysis.summary;
94
+ }
95
+ function compactText(value, maxLength) {
96
+ const normalized = value.replace(/\s+/g, ' ').trim();
97
+ if (normalized.length <= maxLength)
98
+ return normalized;
99
+ const clipped = normalized.slice(0, maxLength - 3).trimEnd();
100
+ const breakIndex = Math.max(clipped.lastIndexOf('. '), clipped.lastIndexOf('; '), clipped.lastIndexOf(', '), clipped.lastIndexOf(' '));
101
+ const safeIndex = breakIndex > 120 ? breakIndex : clipped.length;
102
+ return `${clipped.slice(0, safeIndex).trimEnd()}...`;
103
+ }
104
+ function renderPillList(items, emptyLabel) {
105
+ if (items.length === 0)
106
+ return `<span class="detail-pill muted">${escapeHtml(emptyLabel)}</span>`;
107
+ return items.map((item) => `<span class="detail-pill">${escapeHtml(formatLabel(item))}</span>`).join('');
108
+ }
109
+ function getInitials(name) {
110
+ const parts = name.trim().split(/\s+/).filter(Boolean).slice(0, 2);
111
+ if (parts.length === 0)
112
+ return 'QS';
113
+ return parts.map((part) => part.charAt(0).toUpperCase()).join('');
114
+ }
115
+ function renderPlayerAvatar(name, avatarUrl) {
116
+ if (avatarUrl) {
117
+ return `<div class="avatar"><img class="avatar-image" src="${escapeHtml(avatarUrl)}" alt="${escapeHtml(name)} avatar" /></div>`;
118
+ }
119
+ return `<div class="avatar avatar-fallback" aria-hidden="true">${escapeHtml(getInitials(name))}</div>`;
120
+ }
121
+ function renderPlayerName(name) {
122
+ const [primary, scope, ...rest] = name.split(/\s+@\s+/);
123
+ if (!scope || rest.length > 0) {
124
+ return `<h2 class="player-name">${escapeHtml(name)}</h2>`;
125
+ }
126
+ return `<div class="player-name player-name-scoped"><span class="player-name-main">${escapeHtml(primary)}</span><span class="player-name-scope-mark" aria-hidden="true">@</span><span class="player-name-scope">${escapeHtml(scope)}</span></div>`;
127
+ }
128
+ function polarPoint(angle, radius, cx, cy) {
129
+ return { x: cx + Math.cos(angle) * radius, y: cy + Math.sin(angle) * radius };
130
+ }
131
+ function renderRadarChart(areaEntries, areaTotal) {
132
+ const map = new Map(areaEntries);
133
+ const keys = Array.from(new Set([...areaEntries.map(([key]) => key), ...RADAR_FALLBACK_KEYS])).slice(0, 6);
134
+ const items = keys.map((key) => ({ label: formatLabel(key), share: areaTotal === 0 ? 0 : Number(pct(map.get(key) ?? 0, areaTotal)) }));
135
+ const cx = 160;
136
+ const cy = 138;
137
+ const radius = 92;
138
+ const labelRadius = 118;
139
+ const grid = [0.25, 0.5, 0.75, 1]
140
+ .map((level) => `<polygon points="${items.map((_, index) => {
141
+ const point = polarPoint(-Math.PI / 2 + (index * Math.PI * 2) / items.length, radius * level, cx, cy);
142
+ return `${point.x.toFixed(2)},${point.y.toFixed(2)}`;
143
+ }).join(' ')}" fill="none" stroke="rgba(255,255,255,0.12)" stroke-width="1" />`)
144
+ .join('');
145
+ const axes = items
146
+ .map((item, index) => {
147
+ const angle = -Math.PI / 2 + (index * Math.PI * 2) / items.length;
148
+ const axisEnd = polarPoint(angle, radius, cx, cy);
149
+ const label = polarPoint(angle, labelRadius, cx, cy);
150
+ const anchor = label.x < cx - 8 ? 'end' : label.x > cx + 8 ? 'start' : 'middle';
151
+ return `<line x1="${cx}" y1="${cy}" x2="${axisEnd.x.toFixed(2)}" y2="${axisEnd.y.toFixed(2)}" stroke="rgba(255,255,255,0.12)" stroke-width="1" /><text x="${label.x.toFixed(2)}" y="${label.y.toFixed(2)}" fill="#a3a3a3" font-size="12" font-family="'JetBrains Mono', monospace" text-anchor="${anchor}" dominant-baseline="middle">${escapeHtml(item.label)}</text>`;
152
+ })
153
+ .join('');
154
+ const polygon = items
155
+ .map((item, index) => {
156
+ const point = polarPoint(-Math.PI / 2 + (index * Math.PI * 2) / items.length, radius * (item.share / 100), cx, cy);
157
+ return `${point.x.toFixed(2)},${point.y.toFixed(2)}`;
158
+ })
159
+ .join(' ');
160
+ const markers = items
161
+ .map((item, index) => {
162
+ const point = polarPoint(-Math.PI / 2 + (index * Math.PI * 2) / items.length, radius * (item.share / 100), cx, cy);
163
+ return `<circle cx="${point.x.toFixed(2)}" cy="${point.y.toFixed(2)}" r="3.5" fill="#f59e0b" stroke="#171717" stroke-width="2"><title>${escapeHtml(item.label)}: ${item.share.toFixed(1)}%</title></circle>`;
164
+ })
165
+ .join('');
166
+ return `<svg viewBox="0 0 320 280" role="img" aria-label="Specialization radar chart">${grid}${axes}<polygon points="${polygon}" fill="rgba(245,158,11,0.35)" stroke="#f59e0b" stroke-width="2" />${markers}</svg>`;
167
+ }
168
+ function renderTechDonut(topTechs) {
169
+ if (topTechs.length === 0)
170
+ return '<div class="chart-empty">No technology data available yet.</div>';
171
+ const total = topTechs.reduce((sum, [, value]) => sum + value, 0);
172
+ let deg = 0;
173
+ const gradient = topTechs.map(([, value], index) => {
174
+ const start = deg;
175
+ deg += (value / total) * 360;
176
+ return `${TECH_PALETTE[index % TECH_PALETTE.length]} ${start.toFixed(2)}deg ${deg.toFixed(2)}deg`;
177
+ }).join(', ');
178
+ const legend = topTechs.map(([key, value], index) => `<div class="tech-legend-row"><div class="tech-legend-label"><span class="tech-legend-dot" style="background:${TECH_PALETTE[index % TECH_PALETTE.length]}"></span><span>${escapeHtml(formatLabel(key))}</span></div><span class="tech-legend-value">${pct(value, total)}%</span></div>`).join('');
179
+ return `<div class="donut-layout"><div class="donut-chart" style="background:conic-gradient(${gradient})"><div class="donut-hole" aria-hidden="true"></div></div><div class="tech-legend">${legend}</div></div>`;
180
+ }
181
+ function renderContributionTypes(typeEntries, typeTotal) {
182
+ if (typeEntries.length === 0)
183
+ return '<p class="empty">No contribution data available yet.</p>';
184
+ return typeEntries.map(([key, value], index) => {
185
+ const share = pct(value, typeTotal);
186
+ return `<div class="type-row"><div class="type-row-head"><span class="type-label">${escapeHtml(formatLabel(key))}</span><span class="type-value">${share}%</span></div><div class="type-track"><div class="type-fill" style="width:${share}%;background:${TYPE_COLORS[index % TYPE_COLORS.length]}"></div></div></div>`;
187
+ }).join('');
188
+ }
189
+ function renderPRCard(pr, initiallyOpen) {
190
+ const analysis = pr.analysis;
191
+ const skipped = !analysis || analysis.skipped;
192
+ const summary = getDisplayPRSummary(pr);
193
+ const techs = skipped ? [] : normalizeTechnologies(analysis.technologies);
194
+ const types = skipped ? [] : analysis.types;
195
+ return `<details class="pr-entry${skipped ? ' is-skipped' : ''}"${initiallyOpen ? ' open' : ''}><summary class="pr-toggle"><div class="pr-head"><div class="pr-main-head"><div class="pr-icon-box" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none"><circle cx="4" cy="3" r="2" stroke="#a855f7" stroke-width="1.4"/><circle cx="12" cy="6" r="2" stroke="#a855f7" stroke-width="1.4"/><circle cx="9" cy="13" r="2" stroke="#a855f7" stroke-width="1.4"/><path d="M5.4 4.1l5.1 1.3M5.1 4.7l3.1 6" stroke="#a855f7" stroke-width="1.4" stroke-linecap="round"/></svg></div><div class="pr-title-wrap"><div class="pr-title-row"><strong>${escapeHtml(pr.title)}</strong><span class="repo-chip">${escapeHtml(pr.repo)}</span></div><div class="pr-meta-row"><span class="meta-date">${escapeHtml(formatDate(pr.mergedAt))}</span><span class="meta-plus">+ ${pr.additions.toLocaleString()}</span><span class="meta-minus">- ${pr.deletions.toLocaleString()}</span>${types.map((type) => `<span class="inline-chip subtle">${escapeHtml(formatLabel(type))}</span>`).join('')}</div></div></div><div class="pr-head-side"><div class="pr-tech-preview">${techs.slice(0, 3).map((tech) => `<span class="inline-chip tech">${escapeHtml(formatLabel(tech))}</span>`).join('')}</div><div class="pr-score"><div class="pr-points">${skipped ? 'Skipped' : `+${(pr.points ?? 0).toLocaleString()} XP`}</div><span class="pr-chevron" aria-hidden="true"></span></div></div></div></summary><div class="pr-body"><div class="pr-body-grid"><div class="pr-body-main"><div class="body-block"><h4>Quest Summary</h4><p>${escapeHtml(summary)}</p></div><div class="body-block"><h4>Tech Stack</h4><div class="pr-detail-pills">${renderPillList(techs, 'No technology tags')}</div></div></div><div class="pr-body-side"><div class="pr-stat"><span>Complexity Rating</span><strong>${skipped ? 'N/A' : `${analysis.complexity} / 10`}</strong><div class="mini-progress"><div class="mini-progress-fill" style="width:${skipped ? '0' : `${analysis.complexity * 10}`}%;"></div></div></div><a class="pr-link" href="${escapeHtml(pr.url)}" target="_blank" rel="noopener noreferrer">View Original PR <span aria-hidden="true">&#8599;</span></a></div></div></div></details>`;
196
+ }
197
+ export function generateHTML(data) {
198
+ const { username, stats, character, prs } = data;
199
+ const { totalPoints, level, pointsToNextLevel, areas, technologies, types, weeklyContributions, analyzedPRs, totalPRs } = stats;
200
+ diagLog('Generating HTML report', {
201
+ username,
202
+ totalPoints,
203
+ level,
204
+ pointsToNextLevel,
205
+ totalPRs,
206
+ analyzedPRs,
207
+ activeWeeks: weeklyContributions.filter((entry) => entry.points > 0).length,
208
+ prCount: prs.length,
209
+ characterPresent: Boolean(character),
210
+ });
211
+ const sortedPRs = [...prs].sort((left, right) => Date.parse(right.mergedAt) - Date.parse(left.mergedAt));
212
+ const topTechs = Object.entries(technologies).sort(([, left], [, right]) => right - left).slice(0, TECH_PALETTE.length);
213
+ const areaEntries = Object.entries(areas).sort(([, left], [, right]) => right - left);
214
+ const typeEntries = Object.entries(types).sort(([, left], [, right]) => right - left);
215
+ const areaTotal = areaEntries.reduce((sum, [, value]) => sum + value, 0);
216
+ const typeTotal = typeEntries.reduce((sum, [, value]) => sum + value, 0);
217
+ const weekRangeStart = weeklyContributions[0]?.week.slice(5) ?? 'N/A';
218
+ const weekRangeEnd = weeklyContributions[weeklyContributions.length - 1]?.week.slice(5) ?? 'N/A';
219
+ const activeWeeks = weeklyContributions.filter((entry) => entry.points > 0).length;
220
+ const primaryArea = areaEntries[0]?.[0];
221
+ const totalLinesModified = sortedPRs.reduce((sum, pr) => sum + pr.additions + pr.deletions, 0);
222
+ const scoredPRs = sortedPRs.filter((pr) => pr.analysis && !pr.analysis.skipped);
223
+ const averageComplexity = scoredPRs.length === 0 ? 'N/A' : (scoredPRs.reduce((sum, pr) => sum + (pr.analysis?.complexity ?? 0), 0) / scoredPRs.length).toFixed(1);
224
+ const profileSummary = analyzedPRs === 0
225
+ ? 'No merged pull requests were found for this profile scope, so there are currently no scored contributions to summarize.'
226
+ : buildManagerProfileSummary(stats, sortedPRs);
227
+ const playerRank = character?.title ?? getRankTitle(level);
228
+ const playerDescription = compactText(isLowValueCharacterSummary(character?.summary) ? profileSummary : character?.summary ?? profileSummary, 600);
229
+ const currentLevelFloor = LEVEL_THRESHOLDS[level - 1] ?? 0;
230
+ const nextLevelThreshold = LEVEL_THRESHOLDS[level] ?? LEVEL_THRESHOLDS[LEVEL_THRESHOLDS.length - 1] + LEVEL_THRESHOLDS[LEVEL_THRESHOLDS.length - 2];
231
+ const currentLevelXP = Math.max(0, totalPoints - currentLevelFloor);
232
+ const currentLevelSpan = Math.max(1, nextLevelThreshold - currentLevelFloor);
233
+ const levelProgress = Math.max(0, Math.min(100, Math.round((currentLevelXP / currentLevelSpan) * 100)));
234
+ const weeklyChartWidth = 860;
235
+ const weeklyChartHeight = 260;
236
+ const paddingTop = 18;
237
+ const paddingRight = 8;
238
+ const paddingBottom = 42;
239
+ const paddingLeft = 42;
240
+ const innerWidth = weeklyChartWidth - paddingLeft - paddingRight;
241
+ const innerHeight = weeklyChartHeight - paddingTop - paddingBottom;
242
+ const weeklyMax = Math.max(1, ...weeklyContributions.map((entry) => entry.points));
243
+ const weeklyStep = innerWidth / Math.max(weeklyContributions.length, 1);
244
+ const weeklyBarWidth = Math.max(8, Math.min(18, weeklyStep * 0.62));
245
+ const weeklyTicks = [...new Set([weeklyMax, Math.round(weeklyMax / 2), 0])].sort((left, right) => right - left);
246
+ const weeklyGrid = weeklyTicks.map((tick) => {
247
+ const y = paddingTop + innerHeight - (tick / weeklyMax) * innerHeight;
248
+ return `<line x1="${paddingLeft}" y1="${y.toFixed(2)}" x2="${weeklyChartWidth - paddingRight}" y2="${y.toFixed(2)}" stroke="rgba(255,255,255,0.1)" stroke-width="1" /><text x="${paddingLeft - 8}" y="${(y + 4).toFixed(2)}" fill="#737373" font-size="10" font-family="'JetBrains Mono', monospace" text-anchor="end">${tick.toLocaleString()}</text>`;
249
+ }).join('');
250
+ const weeklyBars = weeklyContributions.map((entry, index) => {
251
+ const x = paddingLeft + index * weeklyStep + (weeklyStep - weeklyBarWidth) / 2;
252
+ const rawHeight = (entry.points / weeklyMax) * innerHeight;
253
+ const barHeight = entry.points === 0 ? 2 : Math.max(8, rawHeight);
254
+ const y = paddingTop + innerHeight - barHeight;
255
+ const label = index % 5 === 0 || index === weeklyContributions.length - 1 ? `<text x="${(x + weeklyBarWidth / 2).toFixed(2)}" y="${weeklyChartHeight - 16}" fill="#737373" font-size="10" font-family="'JetBrains Mono', monospace" text-anchor="middle">${escapeHtml(entry.week.slice(5))}</text>` : '';
256
+ return `<g><rect x="${x.toFixed(2)}" y="${y.toFixed(2)}" width="${weeklyBarWidth.toFixed(2)}" height="${barHeight.toFixed(2)}" rx="4" fill="${entry.points === 0 ? 'rgba(255,255,255,0.12)' : '#f59e0b'}"><title>${escapeHtml(entry.week.slice(5))}: ${entry.points.toLocaleString()} pts</title></rect>${label}</g>`;
257
+ }).join('');
258
+ const weeklyChartSvg = `<svg viewBox="0 0 ${weeklyChartWidth} ${weeklyChartHeight}" role="img" aria-label="Weekly XP gains chart">${weeklyGrid}<line x1="${paddingLeft}" y1="${paddingTop + innerHeight}" x2="${weeklyChartWidth - paddingRight}" y2="${paddingTop + innerHeight}" stroke="rgba(255,255,255,0.14)" stroke-width="1" />${weeklyBars}</svg>`;
259
+ const PRS_PER_PAGE = 10;
260
+ const prPageCount = Math.ceil(sortedPRs.length / PRS_PER_PAGE);
261
+ const prPages = Array.from({ length: prPageCount }, (_, pageIndex) => {
262
+ const pagePRs = sortedPRs.slice(pageIndex * PRS_PER_PAGE, (pageIndex + 1) * PRS_PER_PAGE);
263
+ return `<div class="pr-page"${pageIndex === 0 ? '' : ' hidden'} data-pr-page="${pageIndex + 1}">${pagePRs.map((pr, entryIndex) => renderPRCard(pr, pageIndex === 0 && entryIndex === 0)).join('')}</div>`;
264
+ }).join('');
265
+ const prPaginationControls = prPageCount > 1 ? `<div class="pr-pagination" aria-label="PR pagination"><button class="pr-pagination-btn" type="button" data-pr-nav="prev">Previous</button><div class="pr-pagination-status" data-pr-pagination-status>Page <strong>1</strong> of ${prPageCount}</div><button class="pr-pagination-btn" type="button" data-pr-nav="next">Next</button></div>` : '';
266
+ const styles = String.raw `
267
+ @import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700;800&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;700&display=swap');
268
+
269
+ :root {
270
+ color-scheme: dark;
271
+ --bg: #050505;
272
+ --panel: rgba(14, 14, 14, 0.94);
273
+ --panel-soft: rgba(22, 22, 22, 0.72);
274
+ --border: rgba(245, 158, 11, 0.22);
275
+ --border-soft: rgba(255, 255, 255, 0.08);
276
+ --text: #f5f5f5;
277
+ --muted: #8a8a8a;
278
+ --muted-strong: #b6b6b6;
279
+ --amber: #f5b331;
280
+ --amber-soft: rgba(245, 179, 49, 0.15);
281
+ --purple: #9333ea;
282
+ --blue: #6f89ff;
283
+ --green: #4ade80;
284
+ --danger: #f87171;
285
+ --shadow: 0 24px 80px rgba(0, 0, 0, 0.45);
286
+ }
287
+
288
+ * { box-sizing: border-box; }
289
+
290
+ html { background: var(--bg); }
291
+
292
+ body {
293
+ margin: 0;
294
+ min-height: 100vh;
295
+ font-family: "Inter", ui-sans-serif, system-ui, sans-serif;
296
+ color: var(--text);
297
+ background:
298
+ radial-gradient(circle at 15% 20%, rgba(245, 179, 49, 0.08), transparent 28%),
299
+ radial-gradient(circle at 85% 5%, rgba(111, 137, 255, 0.06), transparent 20%),
300
+ linear-gradient(180deg, #060606 0%, #090909 100%);
301
+ }
302
+
303
+ a { color: inherit; }
304
+
305
+ .shell {
306
+ width: min(1320px, calc(100vw - 32px));
307
+ margin: 0 auto;
308
+ padding: 26px 0 40px;
309
+ }
310
+
311
+ .header {
312
+ display: flex;
313
+ align-items: flex-start;
314
+ justify-content: space-between;
315
+ gap: 24px;
316
+ margin-bottom: 28px;
317
+ }
318
+
319
+ .brand {
320
+ margin: 0 0 6px;
321
+ font-family: "Cinzel", ui-serif, Georgia, serif;
322
+ font-size: clamp(2rem, 2.8vw, 3rem);
323
+ line-height: 1;
324
+ letter-spacing: 0.02em;
325
+ display: inline-block;
326
+ color: #ffbe1b;
327
+ text-shadow:
328
+ 0 0 10px rgba(255, 190, 27, 0.45),
329
+ 0 0 24px rgba(255, 174, 0, 0.28),
330
+ 0 2px 0 rgba(84, 44, 0, 0.6);
331
+ }
332
+
333
+ .brand-note,
334
+ .mono {
335
+ font-family: "JetBrains Mono", ui-monospace, monospace;
336
+ text-transform: uppercase;
337
+ letter-spacing: 0.24em;
338
+ font-size: 0.72rem;
339
+ color: var(--muted);
340
+ }
341
+
342
+ .header-cards {
343
+ display: flex;
344
+ flex-wrap: wrap;
345
+ gap: 16px;
346
+ justify-content: flex-end;
347
+ }
348
+
349
+ .info-card {
350
+ min-width: 180px;
351
+ padding: 14px 18px;
352
+ border-radius: 12px;
353
+ border: 1px solid var(--border-soft);
354
+ background: linear-gradient(180deg, rgba(20, 20, 20, 0.95), rgba(12, 12, 12, 0.95));
355
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
356
+ }
357
+
358
+ .info-content {
359
+ display: flex;
360
+ align-items: center;
361
+ gap: 12px;
362
+ }
363
+
364
+ .info-icon {
365
+ width: 18px;
366
+ height: 18px;
367
+ display: inline-flex;
368
+ align-items: center;
369
+ justify-content: center;
370
+ flex: 0 0 auto;
371
+ }
372
+
373
+ .info-icon svg {
374
+ width: 100%;
375
+ height: 100%;
376
+ }
377
+
378
+ .info-copy {
379
+ display: grid;
380
+ gap: 2px;
381
+ }
382
+
383
+ .info-label {
384
+ font-family: "JetBrains Mono", ui-monospace, monospace;
385
+ font-size: 0.76rem;
386
+ text-transform: uppercase;
387
+ letter-spacing: 0.22em;
388
+ color: #707070;
389
+ }
390
+
391
+ .info-value {
392
+ font-size: 1.9rem;
393
+ line-height: 1;
394
+ font-weight: 700;
395
+ }
396
+
397
+ .info-value.active {
398
+ font-size: 1.15rem;
399
+ color: var(--green);
400
+ }
401
+
402
+ .info-value.prs {
403
+ font-size: 1.15rem;
404
+ color: #b056ff;
405
+ }
406
+
407
+ .dashboard {
408
+ display: grid;
409
+ grid-template-columns: minmax(340px, 430px) minmax(0, 1fr);
410
+ gap: 28px;
411
+ align-items: start;
412
+ }
413
+
414
+ .player-col {
415
+ position: sticky;
416
+ top: 18px;
417
+ }
418
+
419
+ .player-shell {
420
+ position: relative;
421
+ }
422
+
423
+ .player-glow {
424
+ position: absolute;
425
+ inset: 14px 12px auto;
426
+ height: 85%;
427
+ border-radius: 30px;
428
+ background:
429
+ radial-gradient(circle at 50% 0%, rgba(255, 255, 255, 0.06), transparent 42%),
430
+ radial-gradient(circle at 50% 45%, rgba(245, 179, 49, 0.12), transparent 58%);
431
+ filter: blur(26px);
432
+ opacity: 0.45;
433
+ pointer-events: none;
434
+ }
435
+
436
+ .player-frame {
437
+ position: relative;
438
+ overflow: hidden;
439
+ border-radius: 28px;
440
+ border: 1px solid var(--border);
441
+ background:
442
+ radial-gradient(circle at 50% 12%, rgba(255, 255, 255, 0.06), transparent 30%),
443
+ linear-gradient(180deg, rgba(44, 44, 40, 0.96), rgba(16, 16, 18, 0.98));
444
+ box-shadow:
445
+ 0 0 0 1px rgba(245, 179, 49, 0.08),
446
+ 0 0 28px rgba(245, 179, 49, 0.12),
447
+ 0 0 64px rgba(0, 0, 0, 0.34),
448
+ var(--shadow),
449
+ inset 0 1px 0 rgba(255, 255, 255, 0.04);
450
+ }
451
+
452
+ .player-frame::before {
453
+ content: "";
454
+ position: absolute;
455
+ inset: -22% -16% auto auto;
456
+ width: 320px;
457
+ height: 320px;
458
+ border-radius: 999px;
459
+ background:
460
+ radial-gradient(circle, rgba(245, 179, 49, 0.16) 0%, rgba(245, 179, 49, 0.06) 32%, transparent 68%);
461
+ filter: blur(12px);
462
+ opacity: 0.5;
463
+ pointer-events: none;
464
+ animation: player-ambient-drift 9s ease-in-out infinite;
465
+ }
466
+
467
+ .player-frame::after {
468
+ content: "";
469
+ position: absolute;
470
+ inset: 0;
471
+ background:
472
+ radial-gradient(circle at 50% 18%, rgba(255, 255, 255, 0.05), transparent 30%),
473
+ radial-gradient(circle at 24% 72%, rgba(245, 179, 49, 0.08), transparent 24%),
474
+ radial-gradient(circle at 78% 66%, rgba(255, 239, 178, 0.05), transparent 22%),
475
+ linear-gradient(120deg, transparent 18%, rgba(245, 179, 49, 0.05) 44%, transparent 72%);
476
+ background-size: 100% 100%, 100% 100%, 100% 100%, 190% 190%;
477
+ background-position: center, center, center, 0% 50%;
478
+ opacity: 0.44;
479
+ pointer-events: none;
480
+ animation: player-rune-shimmer 10s ease-in-out infinite;
481
+ }
482
+
483
+ .player {
484
+ position: relative;
485
+ z-index: 1;
486
+ padding: 26px 28px 24px;
487
+ }
488
+
489
+ .player::before {
490
+ content: "";
491
+ position: absolute;
492
+ inset: 12% auto auto -16%;
493
+ width: 220px;
494
+ height: 220px;
495
+ border-radius: 999px;
496
+ background: radial-gradient(circle, rgba(255, 244, 206, 0.06) 0%, rgba(245, 179, 49, 0.025) 38%, transparent 72%);
497
+ filter: blur(8px);
498
+ opacity: 0.34;
499
+ pointer-events: none;
500
+ animation: player-orbital-glow 8.5s ease-in-out infinite;
501
+ }
502
+
503
+ .player::after {
504
+ content: "";
505
+ position: absolute;
506
+ inset: 0;
507
+ border-radius: inherit;
508
+ background: linear-gradient(108deg, transparent 16%, rgba(255, 232, 148, 0.09) 29%, transparent 43%, transparent 58%, rgba(255, 232, 148, 0.06) 72%, transparent 86%);
509
+ background-size: 220% 220%;
510
+ background-position: 0% 50%;
511
+ mix-blend-mode: screen;
512
+ opacity: 0.42;
513
+ pointer-events: none;
514
+ animation: player-sheen 7.2s linear infinite;
515
+ }
516
+
517
+ .player-top {
518
+ display: flex;
519
+ justify-content: space-between;
520
+ align-items: flex-start;
521
+ margin-bottom: 28px;
522
+ }
523
+
524
+ .level {
525
+ display: block;
526
+ margin-bottom: 8px;
527
+ font-family: "Cinzel", ui-serif, Georgia, serif;
528
+ font-size: 3rem;
529
+ line-height: 0.9;
530
+ font-weight: 800;
531
+ color: var(--amber);
532
+ }
533
+
534
+ .cplx {
535
+ text-align: right;
536
+ }
537
+
538
+ .cplx strong {
539
+ display: block;
540
+ margin-bottom: 8px;
541
+ font-size: 2rem;
542
+ line-height: 0.9;
543
+ font-weight: 700;
544
+ }
545
+
546
+ .avatar {
547
+ width: 120px;
548
+ height: 120px;
549
+ margin: 0 auto 18px;
550
+ overflow: hidden;
551
+ border-radius: 999px;
552
+ border: 1px solid rgba(255, 255, 255, 0.08);
553
+ box-shadow:
554
+ inset 0 0 0 3px rgba(255, 255, 255, 0.04),
555
+ 0 16px 36px rgba(0, 0, 0, 0.34);
556
+ background: linear-gradient(180deg, rgba(54, 54, 56, 0.95), rgba(12, 12, 14, 0.98));
557
+ }
558
+
559
+ .avatar-fallback {
560
+ display: grid;
561
+ place-items: center;
562
+ font-family: "Cinzel", ui-serif, Georgia, serif;
563
+ font-size: 2.9rem;
564
+ font-weight: 700;
565
+ letter-spacing: 0.04em;
566
+ }
567
+
568
+ .avatar-image {
569
+ width: 100%;
570
+ height: 100%;
571
+ display: block;
572
+ object-fit: cover;
573
+ }
574
+
575
+ .player-name {
576
+ margin: 0 auto 12px;
577
+ max-width: 320px;
578
+ font-family: "Cinzel", ui-serif, Georgia, serif;
579
+ font-size: clamp(1.55rem, 2.45vw, 2.35rem);
580
+ line-height: 1.08;
581
+ text-transform: uppercase;
582
+ letter-spacing: 0.03em;
583
+ text-align: center;
584
+ word-break: normal;
585
+ overflow-wrap: normal;
586
+ }
587
+
588
+ .player-name-scoped {
589
+ display: grid;
590
+ gap: 4px;
591
+ justify-items: center;
592
+ line-height: 1;
593
+ }
594
+
595
+ .player-name-main,
596
+ .player-name-scope,
597
+ .player-name-scope-mark {
598
+ display: block;
599
+ }
600
+
601
+ .player-name-main {
602
+ font-size: clamp(1.325rem, 2vw, 1.925rem);
603
+ line-height: 1.02;
604
+ font-weight: 800;
605
+ }
606
+
607
+ .player-name-scope-mark {
608
+ font-size: clamp(1.1rem, 1.5vw, 1.45rem);
609
+ line-height: 0.95;
610
+ opacity: 0.92;
611
+ }
612
+
613
+ .player-name-scope {
614
+ max-width: 100%;
615
+ font-size: clamp(1.175rem, 1.8vw, 1.775rem);
616
+ line-height: 1.02;
617
+ font-weight: 800;
618
+ }
619
+
620
+ .player-title {
621
+ display: inline-flex;
622
+ align-items: center;
623
+ gap: 10px;
624
+ padding: 8px 14px;
625
+ border-radius: 999px;
626
+ border: 1px solid rgba(245, 179, 49, 0.28);
627
+ background: rgba(245, 179, 49, 0.08);
628
+ font-family: "Inter", ui-sans-serif, sans-serif;
629
+ font-size: 0.92rem;
630
+ font-weight: 700;
631
+ text-transform: uppercase;
632
+ letter-spacing: 0.04em;
633
+ color: #e7bd43;
634
+ }
635
+
636
+ .player-title::before {
637
+ content: "\2605";
638
+ font-size: 0.78rem;
639
+ line-height: 1;
640
+ }
641
+
642
+ .player-stats {
643
+ display: grid;
644
+ grid-template-columns: repeat(2, minmax(0, 1fr));
645
+ gap: 14px;
646
+ margin: 22px 0 18px;
647
+ padding: 22px 0;
648
+ border-top: 1px solid rgba(245, 179, 49, 0.18);
649
+ border-bottom: 1px solid rgba(245, 179, 49, 0.18);
650
+ }
651
+
652
+ .mini {
653
+ display: flex;
654
+ align-items: center;
655
+ gap: 12px;
656
+ }
657
+
658
+ .mini-icon {
659
+ width: 46px;
660
+ height: 46px;
661
+ display: grid;
662
+ place-items: center;
663
+ border-radius: 12px;
664
+ border: 1px solid rgba(255, 255, 255, 0.1);
665
+ background: linear-gradient(180deg, rgba(58, 58, 60, 0.72), rgba(38, 38, 40, 0.42));
666
+ font-family: "JetBrains Mono", ui-monospace, monospace;
667
+ font-size: 1.05rem;
668
+ font-weight: 700;
669
+ }
670
+
671
+ .mini-icon svg {
672
+ width: 21px;
673
+ height: 21px;
674
+ }
675
+
676
+ .mini-icon.lines svg {
677
+ stroke: #78e0ae;
678
+ }
679
+
680
+ .mini-icon.prs svg {
681
+ stroke: #8ca5ff;
682
+ }
683
+
684
+ .mini span {
685
+ display: block;
686
+ margin-bottom: 6px;
687
+ }
688
+
689
+ .mini strong {
690
+ display: block;
691
+ font-size: 1.45rem;
692
+ line-height: 1;
693
+ font-weight: 700;
694
+ }
695
+
696
+ .player-copy {
697
+ margin: 0;
698
+ color: #bfbfbf;
699
+ text-align: center;
700
+ font-family: "Cinzel", ui-serif, Georgia, serif;
701
+ font-style: italic;
702
+ font-size: 1rem;
703
+ line-height: 1.85;
704
+ }
705
+
706
+ .player-xp-row,
707
+ .player-xp-meta {
708
+ display: flex;
709
+ align-items: center;
710
+ justify-content: space-between;
711
+ gap: 12px;
712
+ }
713
+
714
+ .player-xp-row {
715
+ margin-top: 10px;
716
+ margin-bottom: 8px;
717
+ }
718
+
719
+ .player-xp {
720
+ color: var(--amber);
721
+ font-family: "JetBrains Mono", ui-monospace, monospace;
722
+ font-weight: 700;
723
+ }
724
+
725
+ .player-next {
726
+ color: #888;
727
+ font-family: "JetBrains Mono", ui-monospace, monospace;
728
+ font-size: 0.84rem;
729
+ }
730
+
731
+ .track {
732
+ height: 8px;
733
+ border-radius: 999px;
734
+ overflow: hidden;
735
+ background: rgba(255, 255, 255, 0.08);
736
+ border: 1px solid rgba(255, 255, 255, 0.1);
737
+ }
738
+
739
+ .fill {
740
+ height: 100%;
741
+ border-radius: inherit;
742
+ background: linear-gradient(90deg, #e5ab2f 0%, #f1cb56 100%);
743
+ box-shadow: 0 0 18px rgba(245, 179, 49, 0.35);
744
+ }
745
+
746
+ .main {
747
+ display: grid;
748
+ gap: 28px;
749
+ }
750
+
751
+ .top-panels {
752
+ display: grid;
753
+ grid-template-columns: repeat(2, minmax(0, 1fr));
754
+ gap: 28px;
755
+ }
756
+
757
+ .panel {
758
+ position: relative;
759
+ overflow: hidden;
760
+ border-radius: 24px;
761
+ border: 1px solid rgba(255, 255, 255, 0.07);
762
+ background: linear-gradient(180deg, rgba(16, 16, 16, 0.94), rgba(10, 10, 10, 0.98));
763
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
764
+ padding: 22px 22px 24px;
765
+ }
766
+
767
+ .visual-panel::before {
768
+ content: "";
769
+ position: absolute;
770
+ inset: -25% auto auto 48%;
771
+ width: 260px;
772
+ height: 260px;
773
+ border-radius: 999px;
774
+ background: radial-gradient(circle, rgba(111, 137, 255, 0.09) 0%, rgba(111, 137, 255, 0.03) 38%, transparent 72%);
775
+ filter: blur(8px);
776
+ opacity: 0.55;
777
+ pointer-events: none;
778
+ animation: ambient-drift 10s ease-in-out infinite;
779
+ }
780
+
781
+ .visual-panel::after {
782
+ content: "";
783
+ position: absolute;
784
+ inset: auto auto -90px -70px;
785
+ width: 220px;
786
+ height: 220px;
787
+ border-radius: 999px;
788
+ background: radial-gradient(circle, rgba(245, 179, 49, 0.09) 0%, rgba(245, 179, 49, 0.02) 42%, transparent 72%);
789
+ filter: blur(10px);
790
+ opacity: 0.45;
791
+ pointer-events: none;
792
+ animation: ambient-drift 12s ease-in-out infinite reverse;
793
+ }
794
+
795
+ .panel-head,
796
+ .panel-chart,
797
+ .panel-note,
798
+ .weekly-note {
799
+ position: relative;
800
+ z-index: 1;
801
+ }
802
+
803
+ .panel-ornament {
804
+ position: absolute;
805
+ top: 20px;
806
+ right: 20px;
807
+ z-index: 0;
808
+ pointer-events: none;
809
+ font-family: "JetBrains Mono", ui-monospace, monospace;
810
+ font-size: 5rem;
811
+ line-height: 1;
812
+ letter-spacing: -0.12em;
813
+ color: rgba(255, 255, 255, 0.07);
814
+ opacity: 0.9;
815
+ animation: ornament-float 7s ease-in-out infinite;
816
+ }
817
+
818
+ .panel-ornament.tech {
819
+ color: rgba(255, 255, 255, 0.08);
820
+ }
821
+
822
+ .panel-head,
823
+ .pr-headline {
824
+ display: flex;
825
+ align-items: flex-start;
826
+ justify-content: space-between;
827
+ gap: 16px;
828
+ margin-bottom: 18px;
829
+ }
830
+
831
+ .panel-head.compact {
832
+ margin-bottom: 24px;
833
+ }
834
+
835
+ .panel-title-row {
836
+ display: flex;
837
+ align-items: center;
838
+ gap: 10px;
839
+ }
840
+
841
+ .panel-icon {
842
+ font-size: 1.35rem;
843
+ line-height: 1;
844
+ }
845
+
846
+ .panel-icon.radar {
847
+ color: var(--amber);
848
+ }
849
+
850
+ .panel-icon.tech {
851
+ color: #6480ff;
852
+ font-family: "JetBrains Mono", ui-monospace, monospace;
853
+ }
854
+
855
+ .panel-title,
856
+ .pr-title {
857
+ margin: 0;
858
+ font-family: "Cinzel", ui-serif, Georgia, serif;
859
+ font-size: 1.65rem;
860
+ }
861
+
862
+ .panel-note,
863
+ .weekly-note,
864
+ .pr-total {
865
+ color: var(--muted);
866
+ font-family: "JetBrains Mono", ui-monospace, monospace;
867
+ font-size: 0.85rem;
868
+ }
869
+
870
+ .panel-chart {
871
+ min-height: 288px;
872
+ display: flex;
873
+ align-items: center;
874
+ justify-content: center;
875
+ }
876
+
877
+ .panel-chart svg {
878
+ width: 100%;
879
+ height: auto;
880
+ max-height: 288px;
881
+ }
882
+
883
+ .donut-layout {
884
+ width: 100%;
885
+ display: grid;
886
+ grid-template-columns: minmax(140px, 220px) minmax(160px, 1fr);
887
+ align-items: center;
888
+ gap: 18px;
889
+ }
890
+
891
+ .donut-chart {
892
+ width: min(230px, 100%);
893
+ aspect-ratio: 1;
894
+ margin: 0 auto;
895
+ border-radius: 50%;
896
+ position: relative;
897
+ }
898
+
899
+ .donut-hole {
900
+ position: absolute;
901
+ inset: 22%;
902
+ border-radius: 50%;
903
+ background: #0f0f0f;
904
+ border: 1px solid rgba(255, 255, 255, 0.05);
905
+ }
906
+
907
+ .tech-legend {
908
+ display: grid;
909
+ gap: 10px;
910
+ }
911
+
912
+ .tech-legend-row,
913
+ .tech-legend-label {
914
+ display: flex;
915
+ align-items: center;
916
+ justify-content: space-between;
917
+ gap: 14px;
918
+ }
919
+
920
+ .tech-legend-row {
921
+ font-family: "JetBrains Mono", ui-monospace, monospace;
922
+ font-size: 0.95rem;
923
+ }
924
+
925
+ .tech-legend-label {
926
+ justify-content: flex-start;
927
+ color: #c8cffd;
928
+ }
929
+
930
+ .tech-legend-dot {
931
+ width: 12px;
932
+ height: 12px;
933
+ border-radius: 0;
934
+ }
935
+
936
+ .tech-legend-value {
937
+ color: var(--muted);
938
+ font-size: 0.82rem;
939
+ }
940
+
941
+ .chart-empty,
942
+ .empty {
943
+ color: var(--muted);
944
+ }
945
+
946
+ .type-list {
947
+ display: grid;
948
+ gap: 18px;
949
+ }
950
+
951
+ .type-row {
952
+ display: grid;
953
+ gap: 10px;
954
+ }
955
+
956
+ .type-row-head {
957
+ display: flex;
958
+ align-items: center;
959
+ justify-content: space-between;
960
+ gap: 12px;
961
+ }
962
+
963
+ .type-label {
964
+ font-size: 1rem;
965
+ font-weight: 600;
966
+ color: #d3d3d3;
967
+ }
968
+
969
+ .type-value {
970
+ color: #8f8f8f;
971
+ font-family: "JetBrains Mono", ui-monospace, monospace;
972
+ }
973
+
974
+ .type-track,
975
+ .mini-progress {
976
+ height: 8px;
977
+ border-radius: 999px;
978
+ overflow: hidden;
979
+ background: #2a2a2a;
980
+ }
981
+
982
+ .type-fill,
983
+ .mini-progress-fill {
984
+ display: block;
985
+ height: 100%;
986
+ border-radius: inherit;
987
+ }
988
+
989
+ .type-fill {
990
+ min-width: 36px;
991
+ box-shadow: 0 0 14px rgba(255, 255, 255, 0.06);
992
+ }
993
+
994
+ .weekly-chart svg {
995
+ width: 100%;
996
+ height: auto;
997
+ }
998
+
999
+ .pr-panel {
1000
+ margin-top: 28px;
1001
+ padding-top: 18px;
1002
+ }
1003
+
1004
+ .pr-list {
1005
+ display: grid;
1006
+ gap: 12px;
1007
+ }
1008
+
1009
+ .pr-entry {
1010
+ border-radius: 16px;
1011
+ border: 1px solid rgba(255, 255, 255, 0.09);
1012
+ background: rgba(20, 20, 20, 0.96);
1013
+ overflow: hidden;
1014
+ transition: border-color 160ms ease, background 160ms ease, box-shadow 160ms ease;
1015
+ }
1016
+
1017
+ .pr-entry[open] {
1018
+ border-color: rgba(245, 179, 49, 0.42);
1019
+ background: rgba(35, 35, 35, 0.97);
1020
+ box-shadow: inset 0 0 0 1px rgba(245, 179, 49, 0.04);
1021
+ }
1022
+
1023
+ .pr-toggle {
1024
+ list-style: none;
1025
+ cursor: pointer;
1026
+ padding: 14px 16px;
1027
+ }
1028
+
1029
+ .pr-toggle::-webkit-details-marker,
1030
+ .pr-toggle::marker {
1031
+ display: none;
1032
+ }
1033
+
1034
+ .pr-main-head {
1035
+ min-width: 0;
1036
+ display: flex;
1037
+ align-items: flex-start;
1038
+ gap: 12px;
1039
+ flex: 1 1 auto;
1040
+ }
1041
+
1042
+ .pr-icon-box {
1043
+ width: 28px;
1044
+ height: 28px;
1045
+ flex: 0 0 auto;
1046
+ display: grid;
1047
+ place-items: center;
1048
+ border-radius: 8px;
1049
+ border: 1px solid rgba(168, 85, 247, 0.28);
1050
+ background: rgba(168, 85, 247, 0.08);
1051
+ margin-top: 1px;
1052
+ }
1053
+
1054
+ .pr-icon-box svg {
1055
+ width: 15px;
1056
+ height: 15px;
1057
+ }
1058
+
1059
+ .pr-head {
1060
+ display: flex;
1061
+ align-items: flex-start;
1062
+ justify-content: space-between;
1063
+ gap: 18px;
1064
+ }
1065
+
1066
+ .pr-title-wrap,
1067
+ .pr-score,
1068
+ .pr-body-main,
1069
+ .pr-body-side {
1070
+ display: grid;
1071
+ gap: 10px;
1072
+ }
1073
+
1074
+ .pr-title-row,
1075
+ .pr-meta-row,
1076
+ .pr-head-side,
1077
+ .pr-pagination,
1078
+ .pr-detail-pills {
1079
+ display: flex;
1080
+ align-items: center;
1081
+ gap: 10px;
1082
+ flex-wrap: wrap;
1083
+ }
1084
+
1085
+ .pr-title-row strong {
1086
+ font-size: 0.98rem;
1087
+ line-height: 1.35;
1088
+ }
1089
+
1090
+ .repo-chip,
1091
+ .inline-chip,
1092
+ .detail-pill {
1093
+ display: inline-flex;
1094
+ align-items: center;
1095
+ border-radius: 999px;
1096
+ padding: 4px 9px;
1097
+ font-size: 0.72rem;
1098
+ font-family: "JetBrains Mono", ui-monospace, monospace;
1099
+ }
1100
+
1101
+ .repo-chip {
1102
+ color: #9b9b9b;
1103
+ border: 1px solid rgba(255, 255, 255, 0.08);
1104
+ background: rgba(255, 255, 255, 0.04);
1105
+ }
1106
+
1107
+ .inline-chip.subtle,
1108
+ .detail-pill {
1109
+ background: rgba(255, 255, 255, 0.06);
1110
+ color: #c9c9c9;
1111
+ }
1112
+
1113
+ .inline-chip.tech {
1114
+ background: rgba(111, 137, 255, 0.11);
1115
+ color: #9db0ff;
1116
+ border: 1px solid rgba(111, 137, 255, 0.24);
1117
+ }
1118
+
1119
+ .detail-pill.muted {
1120
+ color: var(--muted);
1121
+ }
1122
+
1123
+ .pr-meta-row {
1124
+ color: #888;
1125
+ font-family: "JetBrains Mono", ui-monospace, monospace;
1126
+ font-size: 0.76rem;
1127
+ gap: 8px;
1128
+ }
1129
+
1130
+ .meta-date::before {
1131
+ content: "\1F4C5";
1132
+ margin-right: 6px;
1133
+ opacity: 0.7;
1134
+ }
1135
+
1136
+ .meta-plus {
1137
+ color: #62d497;
1138
+ }
1139
+
1140
+ .meta-minus {
1141
+ color: #f08484;
1142
+ }
1143
+
1144
+ .pr-head-side {
1145
+ justify-content: flex-end;
1146
+ align-items: flex-start;
1147
+ min-width: 0;
1148
+ }
1149
+
1150
+ .pr-score {
1151
+ justify-items: end;
1152
+ align-content: start;
1153
+ gap: 8px;
1154
+ }
1155
+
1156
+ .pr-points {
1157
+ font-family: "JetBrains Mono", ui-monospace, monospace;
1158
+ font-size: 1.05rem;
1159
+ font-weight: 700;
1160
+ color: var(--amber);
1161
+ }
1162
+
1163
+ .pr-chevron {
1164
+ width: 10px;
1165
+ height: 10px;
1166
+ display: inline-block;
1167
+ border-right: 1.5px solid #868686;
1168
+ border-bottom: 1.5px solid #868686;
1169
+ transform: rotate(45deg);
1170
+ transition: transform 160ms ease, border-color 160ms ease;
1171
+ }
1172
+
1173
+ .pr-entry[open] .pr-chevron {
1174
+ transform: rotate(-135deg);
1175
+ border-color: #9f9f9f;
1176
+ }
1177
+
1178
+ .pr-body {
1179
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
1180
+ padding: 16px;
1181
+ }
1182
+
1183
+ .pr-body-grid {
1184
+ display: grid;
1185
+ grid-template-columns: minmax(0, 2fr) minmax(300px, 0.95fr);
1186
+ gap: 20px;
1187
+ }
1188
+
1189
+ .body-block h4 {
1190
+ margin: 0 0 8px;
1191
+ font-family: "JetBrains Mono", ui-monospace, monospace;
1192
+ text-transform: uppercase;
1193
+ letter-spacing: 0.14em;
1194
+ font-size: 0.76rem;
1195
+ color: var(--muted);
1196
+ }
1197
+
1198
+ .body-block p {
1199
+ margin: 0;
1200
+ color: #d3d3d3;
1201
+ line-height: 1.7;
1202
+ }
1203
+
1204
+ .pr-body-main {
1205
+ gap: 18px;
1206
+ }
1207
+
1208
+ .pr-body-side {
1209
+ align-content: start;
1210
+ }
1211
+
1212
+ .pr-stat {
1213
+ padding: 14px;
1214
+ border-radius: 14px;
1215
+ background: rgba(0, 0, 0, 0.14);
1216
+ border: 1px solid rgba(255, 255, 255, 0.05);
1217
+ }
1218
+
1219
+ .pr-stat span {
1220
+ display: block;
1221
+ margin-bottom: 8px;
1222
+ color: var(--muted);
1223
+ font-size: 0.77rem;
1224
+ text-transform: uppercase;
1225
+ letter-spacing: 0.14em;
1226
+ }
1227
+
1228
+ .pr-stat strong {
1229
+ display: block;
1230
+ margin-bottom: 10px;
1231
+ font-size: 1.15rem;
1232
+ color: var(--amber);
1233
+ }
1234
+
1235
+ .mini-progress-fill {
1236
+ background: linear-gradient(90deg, #e5ab2f 0%, #f1cb56 100%);
1237
+ }
1238
+
1239
+ .pr-link,
1240
+ .pr-pagination-btn {
1241
+ border: 1px solid rgba(255, 255, 255, 0.08);
1242
+ background: rgba(255, 255, 255, 0.03);
1243
+ color: var(--text);
1244
+ border-radius: 12px;
1245
+ text-decoration: none;
1246
+ cursor: pointer;
1247
+ transition: background 160ms ease, border-color 160ms ease, color 160ms ease;
1248
+ }
1249
+
1250
+ .pr-link {
1251
+ display: inline-flex;
1252
+ align-items: center;
1253
+ justify-content: center;
1254
+ gap: 8px;
1255
+ padding: 12px 14px;
1256
+ font-weight: 600;
1257
+ }
1258
+
1259
+ .pr-headline {
1260
+ align-items: center;
1261
+ margin-bottom: 20px;
1262
+ }
1263
+
1264
+ .pr-heading-wrap {
1265
+ display: flex;
1266
+ align-items: center;
1267
+ gap: 10px;
1268
+ }
1269
+
1270
+ .pr-heading-icon {
1271
+ color: var(--amber);
1272
+ font-size: 1.1rem;
1273
+ }
1274
+
1275
+ .pr-title {
1276
+ font-size: 1.8rem;
1277
+ }
1278
+
1279
+ .pr-total strong {
1280
+ color: var(--amber);
1281
+ }
1282
+
1283
+ @keyframes ambient-drift {
1284
+ 0%, 100% {
1285
+ transform: translate3d(0, 0, 0) scale(1);
1286
+ }
1287
+ 50% {
1288
+ transform: translate3d(-10px, 8px, 0) scale(1.08);
1289
+ }
1290
+ }
1291
+
1292
+ @keyframes player-ambient-drift {
1293
+ 0%, 100% {
1294
+ transform: translate3d(0, 0, 0) scale(1);
1295
+ opacity: 0.34;
1296
+ }
1297
+ 50% {
1298
+ transform: translate3d(-18px, 14px, 0) scale(1.12);
1299
+ opacity: 0.62;
1300
+ }
1301
+ }
1302
+
1303
+ @keyframes player-rune-shimmer {
1304
+ 0%, 100% {
1305
+ background-position: center, center, center, 0% 50%;
1306
+ opacity: 0.34;
1307
+ }
1308
+ 50% {
1309
+ background-position: center, center, center, 100% 50%;
1310
+ opacity: 0.56;
1311
+ }
1312
+ }
1313
+
1314
+ @keyframes player-orbital-glow {
1315
+ 0%, 100% {
1316
+ transform: translate3d(0, 0, 0) scale(1);
1317
+ opacity: 0.22;
1318
+ }
1319
+ 50% {
1320
+ transform: translate3d(18px, -10px, 0) scale(1.14);
1321
+ opacity: 0.4;
1322
+ }
1323
+ }
1324
+
1325
+ @keyframes player-sheen {
1326
+ 0% {
1327
+ background-position: 220% 50%;
1328
+ opacity: 0.18;
1329
+ }
1330
+ 50% {
1331
+ opacity: 0.42;
1332
+ }
1333
+ 100% {
1334
+ background-position: -20% 50%;
1335
+ opacity: 0.18;
1336
+ }
1337
+ }
1338
+
1339
+ @keyframes ornament-float {
1340
+ 0%, 100% {
1341
+ transform: translate3d(0, 0, 0);
1342
+ opacity: 0.78;
1343
+ }
1344
+ 50% {
1345
+ transform: translate3d(-4px, 6px, 0);
1346
+ opacity: 1;
1347
+ }
1348
+ }
1349
+
1350
+ .pr-pagination {
1351
+ align-items: center;
1352
+ justify-content: space-between;
1353
+ margin-top: 20px;
1354
+ padding-top: 18px;
1355
+ border-top: 1px solid rgba(255, 255, 255, 0.06);
1356
+ gap: 18px;
1357
+ }
1358
+
1359
+ .pr-pagination-btn {
1360
+ min-width: 96px;
1361
+ justify-content: center;
1362
+ padding: 10px 18px;
1363
+ font-family: "JetBrains Mono", ui-monospace, monospace;
1364
+ }
1365
+
1366
+ .pr-pagination-status {
1367
+ flex: 1 1 auto;
1368
+ text-align: center;
1369
+ color: #8b8b8b;
1370
+ font-family: "JetBrains Mono", ui-monospace, monospace;
1371
+ font-size: 0.95rem;
1372
+ }
1373
+
1374
+ .pr-pagination-status strong {
1375
+ color: var(--amber);
1376
+ }
1377
+
1378
+ .pr-pagination-btn:disabled {
1379
+ opacity: 0.45;
1380
+ cursor: not-allowed;
1381
+ }
1382
+
1383
+ .page-footer {
1384
+ margin-top: 22px;
1385
+ color: #777;
1386
+ text-align: center;
1387
+ font-family: "JetBrains Mono", ui-monospace, monospace;
1388
+ font-size: 0.8rem;
1389
+ letter-spacing: 0.14em;
1390
+ text-transform: uppercase;
1391
+ }
1392
+
1393
+ @media (max-width: 1080px) {
1394
+ .header,
1395
+ .dashboard,
1396
+ .top-panels,
1397
+ .pr-body-grid,
1398
+ .donut-layout {
1399
+ grid-template-columns: 1fr;
1400
+ }
1401
+
1402
+ .header,
1403
+ .pr-head,
1404
+ .panel-head,
1405
+ .pr-headline {
1406
+ flex-direction: column;
1407
+ }
1408
+
1409
+ .player-col {
1410
+ position: static;
1411
+ }
1412
+ }
1413
+
1414
+ @media (max-width: 720px) {
1415
+ .shell {
1416
+ width: min(100vw - 20px, 1320px);
1417
+ padding-top: 18px;
1418
+ }
1419
+
1420
+ .player,
1421
+ .panel {
1422
+ padding-left: 18px;
1423
+ padding-right: 18px;
1424
+ }
1425
+
1426
+ .player-stats {
1427
+ grid-template-columns: 1fr;
1428
+ }
1429
+
1430
+ .player-name {
1431
+ font-size: 1.85rem;
1432
+ }
1433
+
1434
+ .mini strong {
1435
+ font-size: 1.3rem;
1436
+ }
1437
+ }
1438
+
1439
+ @media (prefers-reduced-motion: reduce) {
1440
+ *,
1441
+ *::before,
1442
+ *::after {
1443
+ scroll-behavior: auto !important;
1444
+ animation: none !important;
1445
+ transition: none !important;
1446
+ }
1447
+ }`;
1448
+ return `<!DOCTYPE html>
1449
+ <html lang="en">
1450
+ <head>
1451
+ <meta charset="UTF-8">
1452
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1453
+ <title>${escapeHtml(username)} - Quackscore Report</title>
1454
+ <style>
1455
+ ${styles}
1456
+ </style>
1457
+ </head>
1458
+ <body>
1459
+ <div class="shell">
1460
+ <header class="header">
1461
+ <div>
1462
+ <h1 class="brand">&#129414; Quackscore</h1>
1463
+ <p class="brand-note">Player Profile Analysis</p>
1464
+ </div>
1465
+ <div class="header-cards">
1466
+ <div class="info-card"><div class="info-content"><span class="info-icon" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none"><path d="M1 9.5h2.2l1.6-5 2.5 8 2.1-6h2.1l1.5 3H15" stroke="#4ade80" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg></span><div class="info-copy"><span class="info-label">Status</span><strong class="info-value active">Active</strong></div></div></div>
1467
+ <div class="info-card"><div class="info-content"><span class="info-icon" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none"><circle cx="3" cy="3" r="2" stroke="#a855f7" stroke-width="1.5"/><circle cx="13" cy="6" r="2" stroke="#a855f7" stroke-width="1.5"/><circle cx="10" cy="13" r="2" stroke="#a855f7" stroke-width="1.5"/><path d="M4.5 4.2l7 1.7M4.2 4.7l4.6 6.1" stroke="#a855f7" stroke-width="1.5" stroke-linecap="round"/></svg></span><div class="info-copy"><span class="info-label">Analyzed PRs</span><strong class="info-value prs">${analyzedPRs}</strong></div></div></div>
1468
+ </div>
1469
+ </header>
1470
+ <section class="dashboard">
1471
+ <aside class="player-col">
1472
+ <div class="player-shell">
1473
+ <div class="player-glow" aria-hidden="true"></div>
1474
+ <section class="player-frame">
1475
+ <div class="player">
1476
+ <div class="player-top"><div><span class="level">${level}</span><span class="mono">Level</span></div><div class="cplx"><strong>${escapeHtml(averageComplexity)}</strong><span class="mono">CPLX</span></div></div>
1477
+ ${renderPlayerAvatar(username, character?.avatarUrl)}
1478
+ <div style="text-align:center">${renderPlayerName(username)}<span class="player-title">${escapeHtml(playerRank)}</span></div>
1479
+ <div class="player-stats"><div class="mini"><div class="mini-icon lines" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none"><path d="M3 13L13 3M6 13l-3-3M13 10l-3 3" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></div><div><span class="mono">Lines</span><strong>${totalLinesModified.toLocaleString()}</strong></div></div><div class="mini"><div class="mini-icon prs" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none"><path d="M8 2l4 1.5v4.2c0 2.2-1.4 4.2-4 5.8-2.6-1.6-4-3.6-4-5.8V3.5L8 2z" stroke-width="1.5" stroke-linejoin="round"/></svg></div><div><span class="mono">PRs</span><strong>${analyzedPRs}</strong></div></div></div>
1480
+ <p class="player-copy">"${escapeHtml(playerDescription)}"</p>
1481
+ <div class="player-xp-row"><span class="player-xp">${totalPoints.toLocaleString()} XP</span><span class="player-next">${nextLevelThreshold.toLocaleString()} XP</span></div>
1482
+ <div class="track"><div class="fill" style="width:${levelProgress}%"></div></div>
1483
+ </div>
1484
+ </section>
1485
+ </div>
1486
+ </aside>
1487
+ <div class="main">
1488
+ <div class="top-panels">
1489
+ <section class="panel visual-panel"><div class="panel-head"><div class="panel-title-row"><span class="panel-icon radar">&#9889;</span><h2 class="panel-title">Specialization Radar</h2></div></div><div class="panel-chart">${renderRadarChart(areaEntries, areaTotal)}</div></section>
1490
+ <section class="panel visual-panel"><span class="panel-ornament tech" aria-hidden="true">&#60;/&#62;</span><div class="panel-head"><div class="panel-title-row"><span class="panel-icon tech">&#60;/&#62;</span><h2 class="panel-title">Tech Stack</h2></div></div><div class="panel-chart">${renderTechDonut(topTechs)}</div></section>
1491
+ </div>
1492
+ <section class="panel"><div class="panel-head compact"><div><h2 class="panel-title">Contribution Types</h2></div></div><div class="type-list">${renderContributionTypes(typeEntries, typeTotal)}</div></section>
1493
+ <section class="panel"><div class="panel-head"><div><div class="mono">Activity</div><h2 class="panel-title">Weekly XP Gains</h2></div><div class="weekly-note">${activeWeeks} active weeks between ${escapeHtml(weekRangeStart)} and ${escapeHtml(weekRangeEnd)}</div></div><div class="weekly-chart">${weeklyChartSvg}</div></section>
1494
+ </div>
1495
+ </section>
1496
+ <section class="panel pr-panel"><div class="pr-headline"><div class="pr-heading-wrap"><span class="pr-heading-icon" aria-hidden="true">&#9876;</span><h2 class="pr-title">Quest Log (Pull Requests)</h2></div><div class="pr-total">Total Quests: <strong>${prs.length}</strong></div></div><div class="pr-list">${prPages || '<p class="empty">No pull requests available.</p>'}</div>${prPaginationControls}</section>
1497
+ <div class="page-footer">Quackscore report . ${analyzedPRs} PRs analyzed . generated from local profile data</div>
1498
+ </div>
1499
+ <script>
1500
+ (function() {
1501
+ const pages = Array.from(document.querySelectorAll('[data-pr-page]'));
1502
+ if (pages.length <= 1) return;
1503
+
1504
+ const prevButton = document.querySelector('[data-pr-nav="prev"]');
1505
+ const nextButton = document.querySelector('[data-pr-nav="next"]');
1506
+ const status = document.querySelector('[data-pr-pagination-status]');
1507
+ let currentPage = 0;
1508
+
1509
+ const render = () => {
1510
+ pages.forEach((page, index) => {
1511
+ page.hidden = index !== currentPage;
1512
+ });
1513
+ if (prevButton) prevButton.disabled = currentPage === 0;
1514
+ if (nextButton) nextButton.disabled = currentPage === pages.length - 1;
1515
+ if (status) status.innerHTML = 'Page <strong>' + (currentPage + 1) + '</strong> of ' + pages.length;
1516
+ };
1517
+
1518
+ if (prevButton) {
1519
+ prevButton.addEventListener('click', () => {
1520
+ if (currentPage === 0) return;
1521
+ currentPage -= 1;
1522
+ render();
1523
+ });
1524
+ }
1525
+
1526
+ if (nextButton) {
1527
+ nextButton.addEventListener('click', () => {
1528
+ if (currentPage >= pages.length - 1) return;
1529
+ currentPage += 1;
1530
+ render();
1531
+ });
1532
+ }
1533
+
1534
+ render();
1535
+ })();
1536
+ </script>
1537
+ </body>
1538
+ </html>`;
1539
+ }
1540
+ //# sourceMappingURL=generate.js.map