ReviewOS

also looking at this

stacks/analyticshq

Harden AnalyticsHQ release and dashboard quality

#52
Merged glennmichael123 wants to merge docs/bunpress-product-guide into main
34 files +1236 -340

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

resources/views/dashboard.stxmodified+30-7
Changes to resources/views/dashboard.stx
@@ -2299,13 +2299,17 @@ select.panel:hover { border-color: var(--accent); }
22992299.ga-dot-v { background: var(--accent); }
23002300.ga-dot-u { background: var(--text-3); }
23012301.ga-col:hover .ga-dot { opacity: 1; }
2302.ga-tip { position: absolute; left: 50%; transform: translateX(-50%) translateY(calc(-100% - 12px)); min-width: 124px; padding: 8px 10px; border-radius: 9px; background: var(--panel); border: 1px solid var(--border); box-shadow: 0 8px 24px rgba(0,0,0,0.14); opacity: 0; visibility: hidden; transition: opacity 0.12s ease; pointer-events: none; z-index: 20; }
2303.ga-col:hover .ga-tip { opacity: 1; visibility: visible; }
2302.ga-tip { display: none; position: absolute; left: 50%; transform: translateX(-50%) translateY(calc(-100% - 12px)); min-width: 124px; padding: 8px 10px; border-radius: 9px; background: var(--panel); border: 1px solid var(--border); box-shadow: 0 8px 24px rgba(0,0,0,0.14); pointer-events: none; z-index: 20; }
2303.ga-col:hover .ga-tip { display: block; }
23042304.ga-tip-day { font-size: 11px; color: var(--text-2); margin-bottom: 5px; }
23052305.ga-tip-row { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-2); white-space: nowrap; line-height: 1.7; }
23062306.ga-tip-row b { margin-left: auto; color: var(--text); font-weight: 600; }
23072307.ga-sw { display: inline-block; width: 8px; height: 8px; border-radius: 2px; }
2308@media (prefers-reduced-motion: reduce) { .ga-dot, .ga-tip { transition: none; } }
2308@media (max-width: 1023px) {
2309 .ga-col:hover::before,
2310 .ga-dot,
2311 .ga-tip { display: none; }
2312}
23092313
23102314/* Export CSV pill: hover tightens the border to the accent like the range pills. */
23112315#export-csv:hover { color: var(--text); border-color: var(--accent); }
@@ -2339,6 +2343,25 @@ select.panel:hover { border-color: var(--accent); }
23392343.chrome-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 7px 12px; border-radius: 9px; border: 1px solid var(--border); background: var(--panel); color: var(--text-2); font: 500 13px/1 var(--sans, ui-sans-serif); text-decoration: none; cursor: pointer; transition: color 0.15s ease, border-color 0.15s ease, background-color 0.15s ease; }
23402344.chrome-btn:hover { color: var(--text); border-color: var(--accent); }
23412345.chrome-icon { padding: 7px 10px; font-size: 14px; line-height: 1; }
2346
2347@media (max-width: 767px) {
2348 .site-switcher,
2349 .range-tool,
2350 .live-status,
2351 .chrome-btn,
2352 .range-link,
2353 .ga-row,
2354 .goal-form input,
2355 .goal-form select,
2356 .goal-form button { min-height: 2.75rem; }
2357
2358 .site-switcher { height: 2.75rem; }
2359 .range-link { display: inline-flex; align-items: center; justify-content: center; }
2360 .range-tool { min-width: 2.75rem; }
2361 .ga-row { align-items: center; }
2362 .kpi-label { min-height: 2rem; }
2363 .kpi-value { font-size: clamp(1.375rem, 7.25vw, 1.875rem); line-height: 1.2; white-space: nowrap; }
2364}
23422365/* One glyph at a time. `theme.isDark()` writes data-t, and these pick the matching
23432366 icon -- the same contract bughq's header uses, so the two toggles behave alike. */
23442367.theme-btn .ic-sun, .theme-btn .ic-moon { display: none; }
@@ -2474,8 +2497,8 @@ select.panel:hover { border-color: var(--accent); }
24742497 <section class="grid grid-cols-2 lg:grid-cols-7 mb-6 border-border lg:divide-x panel reveal" aria-label="Key metrics">
24752498 @foreach (kpiCards as kpi)
24762499 <div class="px-5 py-5 border-border">
2477 <div class="mb-2 font-medium text-text-3 text-xs tracking-wider uppercase">{{ kpi.label }}</div>
2478 <div class="font-semibold text-3xl tracking-tight mono">{{ kpi.value }}</div>
2500 <div class="mb-2 font-medium text-text-3 text-xs tracking-wider uppercase kpi-label">{{ kpi.label }}</div>
2501 <div class="font-semibold text-3xl tracking-tight kpi-value mono">{{ kpi.value }}</div>
24792502 @if (kpi.delta !== null)
24802503 <div class="mt-1.5 font-medium text-xs mono" style="color: ">
24812504 {{ kpi.delta >= 0 ? '▲' : '▼' }} {{ Math.abs(kpi.delta) }}% <span class="text-text-3">vs prev</span>
@@ -3303,7 +3326,7 @@ select.panel:hover { border-color: var(--accent); }
33033326 /api/sites/{site}/goals via @submit=addGoal. Pattern is the event NAME for
33043327 event goals or the URL PATH for pageview goals. --}}
33053328 @if (ownsActive)
3306 <form @submit="addGoal" class="flex flex-wrap gap-2 items-center mt-4 pt-4" style="border-top:1px solid var(--border)">
3329 <form @submit="addGoal" class="flex flex-wrap gap-2 items-center mt-4 pt-4 goal-form" style="border-top:1px solid var(--border)">
33073330 <input name="name" required maxlength="255" placeholder="Goal name" aria-label="Goal name" style="flex:1;min-width:110px;padding:6px 9px;border-radius:8px;border:1px solid var(--border);background:var(--bg);color:var(--text);font:500 13px/1.2 var(--sans,ui-sans-serif)">
33083331 <select name="type" aria-label="Goal type" style="padding:6px 9px;border-radius:8px;border:1px solid var(--border);background:var(--bg);color:var(--text);font:500 13px/1.2 var(--sans,ui-sans-serif)">
33093332 <option value="event">Event</option>
@@ -3550,7 +3573,7 @@ select.panel:hover { border-color: var(--accent); }
35503573 dragging: false,
35513574 scrollWheelZoom: false,
35523575 doubleClickZoom: false,
3553 touchZoom: false,
3576 pinchZoom: false,
35543577 keyboard: false,
35553578 })
35563579 layer = new GeoJSON(world, { style: styleFor(tokens()) })
scripts/validate-visual.tsmodified+34-0
Changes to scripts/validate-visual.ts
@@ -14,6 +14,8 @@ interface PageAudit {
1414 focusable: number
1515 imagesWithoutAlt: number
1616 overflow: number
17 undersizedDashboardControls: string[]
18 wrappedDashboardValues: string[]
1719 unresolved: boolean
1820 unnamedControls: string[]
1921}
@@ -182,6 +184,20 @@ const auditExpression = `(() => {
182184 return { r: 255, g: 255, b: 255, a: 1 }
183185 }
184186 const controls = [...document.querySelectorAll('a[href], button, input:not([type="hidden"]), select, textarea')].filter(visible)
187 const dashboardControls = [...document.querySelectorAll('.site-switcher, .range-link, .range-tool, .chrome-btn, .ga-row, .goal-form input, .goal-form select, .goal-form button')].filter(visible)
188 const undersizedDashboardControls = dashboardControls.flatMap(element => {
189 const rect = element.getBoundingClientRect()
190 if (rect.width >= 44 && rect.height >= 44) return []
191 const name = element.getAttribute('aria-label') || element.textContent?.trim().replace(/\\s+/g, ' ').slice(0, 32) || element.tagName.toLowerCase()
192 return [name + ' (' + Math.round(rect.width) + 'x' + Math.round(rect.height) + ')']
193 })
194 const wrappedDashboardValues = [...document.querySelectorAll('.kpi-value')].filter(visible).flatMap(element => {
195 const rect = element.getBoundingClientRect()
196 const lineHeight = Number.parseFloat(getComputedStyle(element).lineHeight)
197 return rect.height > lineHeight * 1.5 || element.scrollWidth > element.clientWidth + 1
198 ? [element.textContent?.trim() || 'metric value']
199 : []
200 })
185201 const unnamedControls = controls.filter(element => !label(element)).map(element => {
186202 const identity = element.id ? '#' + element.id : element.getAttribute('name') ? '[name="' + element.getAttribute('name') + '"]' : ''
187203 return element.tagName.toLowerCase() + identity
@@ -210,11 +226,23 @@ const auditExpression = `(() => {
210226 focusable: controls.length,
211227 imagesWithoutAlt: [...document.images].filter(image => !image.hasAttribute('alt')).length,
212228 overflow: Math.max(0, document.documentElement.scrollWidth - document.documentElement.clientWidth),
229 undersizedDashboardControls,
230 wrappedDashboardValues,
213231 unresolved: document.body.innerText.includes('{{') || document.body.innerText.includes('@if'),
214232 unnamedControls,
215233 }
216234})()`
217235
236const dashboardFixtureExpression = `(() => {
237 const fixture = document.createElement('section')
238 fixture.id = 'visual-dashboard-fixture'
239 fixture.setAttribute('aria-hidden', 'true')
240 fixture.style.cssText = 'position:absolute;left:1rem;top:0;width:calc(100vw - 2rem);opacity:0.001;pointer-events:none;z-index:-1'
241 const columns = Array.from({ length: 90 }, (_, index) => '<div class="relative flex-1 ga-col"><div class="ga-tip">Day ' + index + '</div></div>').join('')
242 fixture.innerHTML = '<select class="panel site-switcher" aria-label="Switch site"><option>example.com</option></select><a class="range-link" href="#">24h</a><button class="range-tool">Export</button><a class="chrome-btn" href="#">Account</a><a class="ga-row" href="#">Page row</a><form class="goal-form"><input aria-label="Goal name"></form><section class="grid grid-cols-2"><div class="px-5"><div class="kpi-label">Average visit time</div><div class="text-3xl kpi-value mono">27m 47s</div></div></section><div class="p-5 panel"><div class="relative h-44 w-full ga-chart"><div class="flex absolute inset-0">' + columns + '</div></div></div>'
243 document.body.appendChild(fixture)
244})()`
245
218246async function main(): Promise<void> {
219247 if (!existsSync(DIST) && !existsSync(SSR_PAGES))
220248 throw new Error(`Built site not found at ${DIST} or ${SSR_OUTPUT}. Run bun run build first.`)
@@ -290,9 +318,15 @@ async function main(): Promise<void> {
290318 document.documentElement.setAttribute('color-mode', theme)
291319 })()`)
292320 await Bun.sleep(500)
321 if (route === '/dashboard') await cdp.evaluate(dashboardFixtureExpression)
293322 const audit = await cdp.evaluate<PageAudit>(auditExpression)
323 if (route === '/dashboard') await cdp.evaluate("document.getElementById('visual-dashboard-fixture')?.remove()")
294324 const key = `${route} at ${viewport.name}/${theme}`
295325 if (audit.overflow > 1) failures.push(`${key}: ${audit.overflow}px horizontal overflow`)
326 if (viewport.mobile && audit.undersizedDashboardControls.length)
327 failures.push(`${key}: undersized dashboard controls ${audit.undersizedDashboardControls.join(', ')}`)
328 if (viewport.mobile && audit.wrappedDashboardValues.length)
329 failures.push(`${key}: wrapped dashboard metric values ${audit.wrappedDashboardValues.join(', ')}`)
296330 if (audit.unresolved) failures.push(`${key}: unresolved template expression is visible`)
297331 if (audit.imagesWithoutAlt) failures.push(`${key}: ${audit.imagesWithoutAlt} image(s) have no alt attribute`)
298332 if (audit.unnamedControls.length) failures.push(`${key}: unnamed controls ${audit.unnamedControls.join(', ')}`)
tests/unit/country-map.test.tsmodified+1-1
Changes to tests/unit/country-map.test.ts
@@ -357,7 +357,7 @@ describe('the map talks to nothing but this origin', () => {
357357
358358 test('interaction is disabled — this is a report, not an explorer', () => {
359359 const s = mapScript()
360 for (const opt of ['dragging', 'scrollWheelZoom', 'doubleClickZoom', 'touchZoom'])
360 for (const opt of ['dragging', 'scrollWheelZoom', 'doubleClickZoom', 'pinchZoom'])
361361 expect({ opt, disabled: new RegExp(`${opt}:\\s*false`).test(s) }).toEqual({ opt, disabled: true })
362362 })
363363