Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1c678bd
feat(web): dashboard design-system base (MetricInfo, fullscreen, toke…
StanislavBG Jun 28, 2026
24af313
fix(web): restore bids+page cache-key params (CWE-349 #56 guard)
StanislavBG Jun 28, 2026
6ccae13
fix(web): restore CWE-349 cache-key drift guard + risk-box styles (re…
StanislavBG Jun 29, 2026
cf06f21
test(web): restore behavioral cache-key assert for keyed params
StanislavBG Jul 2, 2026
957ad75
fix(web): metric-info popover text can never overflow the card
StanislavBG Jul 3, 2026
7c7e56a
fix(web): SSR-safe layout effect, live popover clamp, aria-expanded sync
StanislavBG Jul 10, 2026
9f2f4b1
fix(web): address PR review feedback on MetricInfo, cache-key, overru…
StanislavBG Jul 11, 2026
a03348e
fix(web): run prettier on files flagged by CI lint check
StanislavBG Jul 11, 2026
3e253bd
Merge remote-tracking branch 'origin/main' into pr/dash-base
StanislavBG Jul 11, 2026
eb7a2e1
fix(web): idempotent popover clamp, cover hover/focus reveal (PR #169…
StanislavBG Jul 11, 2026
bc45f82
fix(web): address round-3 ydimitrof review threads on PR #169
StanislavBG Jul 18, 2026
bf76e45
Merge remote-tracking branch 'origin/main' into pr/dash-base
StanislavBG Jul 18, 2026
ffd9d84
fix(web): drop stray aria-expanded + passive scroll listener in Metri…
StanislavBG Jul 20, 2026
cf53ad4
fix(web): drop invalid passive option from removeEventListener in Met…
StanislavBG Jul 21, 2026
bc790e9
fix(web): drop cheater test, correct index-coverage comment, name cla…
StanislavBG Jul 21, 2026
f2f5c41
build(deps): bump sharp to ^0.35.0 (GHSA-f88m-g3jw-g9cj)
StanislavBG Jul 22, 2026
3619625
style(web): prettier-format metric-info-clamp.ts
StanislavBG Jul 22, 2026
94b6fe8
test(web): assert stale allow-list entries, not just log them
StanislavBG Jul 26, 2026
f960f4e
build(deps): patch postcss/valibot CVEs, suppress unrelated react-rou…
StanislavBG Jul 27, 2026
d6202aa
build: merge origin/main into pr/dash-base, resolve conflicts
StanislavBG Jul 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions apps/web/app/components/FullscreenButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { useCallback, useEffect, useRef, useState } from 'react';

/**
* Toggle the native Fullscreen API on a container ref. SSR-safe: the listener and the
* `document` reads only run in the browser effect. `requestFullscreen` is feature-detected,
* so the button no-ops gracefully where the API is unavailable.
*/
export function useFullscreen<T extends HTMLElement>() {
Comment thread
StanislavBG marked this conversation as resolved.
Outdated
Comment thread
StanislavBG marked this conversation as resolved.
Outdated
const ref = useRef<T>(null);
const [isFullscreen, setIsFullscreen] = useState(false);

useEffect(() => {
const onChange = () => setIsFullscreen(document.fullscreenElement === ref.current);
document.addEventListener('fullscreenchange', onChange);
return () => document.removeEventListener('fullscreenchange', onChange);
}, []);

const toggle = useCallback(() => {
const el = ref.current;
if (!el) return;
if (document.fullscreenElement) {
document.exitFullscreen?.();
} else {
el.requestFullscreen?.().catch(() => {});
Comment thread
StanislavBG marked this conversation as resolved.
Outdated
}
}, []);

return { ref, isFullscreen, toggle };
}

export function FullscreenButton({ active, onToggle }: { active: boolean; onToggle: () => void }) {
Comment thread
StanislavBG marked this conversation as resolved.
Comment thread
StanislavBG marked this conversation as resolved.
return (
<button
type="button"
className="fs-btn"
onClick={onToggle}
aria-pressed={active}
aria-label={active ? 'Изход от цял екран' : 'Разгледай графиката на цял екран'}
title={active ? 'Изход от цял екран' : 'На цял екран'}
>
<svg
aria-hidden="true"
width="13"
height="13"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
>
{active ? (
<>
<path d="M6 2v4H2" />
<path d="M10 2v4h4" />
<path d="M6 14v-4H2" />
<path d="M10 14v-4h4" />
</>
) : (
<>
<path d="M2 6V2h4" />
<path d="M14 6V2h-4" />
<path d="M2 10v4h4" />
<path d="M14 10v4h-4" />
</>
)}
</svg>
<span>{active ? 'Изход' : 'Цял екран'}</span>
</button>
);
}
89 changes: 89 additions & 0 deletions apps/web/app/components/MetricInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { useEffect, useLayoutEffect, useRef, useState } from 'react';

// A small ⓘ affordance next to a metric label. For pointer users it reveals an elegant popover on
// hover or keyboard focus (pure CSS `:hover` / `:focus-within`). Because hover does not exist on
// touch, a click also toggles the popover open via an `is-open` class — and an outside-click or Esc
// closes it again. The button carries the full text as its aria-label, so screen-reader users get the
// same information without the visual popover (which is aria-hidden). SSR-safe: the initial render is
// closed and the toggle/effects only run on the client.
export function MetricInfo({
Comment thread
StanislavBG marked this conversation as resolved.
title,
summary,
readout,
align = 'start',
}: {
title: string;
summary: string;
// Plain string so the readout is always reflected verbatim into the aria-label (all callers pass a
// string — the screen-reader text must never silently drop a non-string interpretation).
readout?: string;
// Which edge the popover anchors to — use 'end' for right-most metrics so it doesn't clip.
align?: 'start' | 'end';
}) {
const aria = readout ? `${title}. ${summary} ${readout}`.trim() : `${title}. ${summary}`;
const [open, setOpen] = useState(false);
const ref = useRef<HTMLSpanElement>(null);
const popRef = useRef<HTMLSpanElement>(null);
// Horizontal shift (px) that keeps the click-opened popover inside the viewport on small screens
// (mobile audit: at 320px the fixed-width popover clips off-screen for edge-column metrics).
const [shift, setShift] = useState(0);

useLayoutEffect(() => {
Comment thread
StanislavBG marked this conversation as resolved.
Outdated
if (!open) {
setShift(0);
return;
}
const pop = popRef.current;
if (!pop) return;
const rect = pop.getBoundingClientRect();
const vw = document.documentElement.clientWidth;
let dx = 0;
if (rect.right > vw - 8) dx = vw - 8 - rect.right;
if (rect.left + dx < 8) dx = 8 - rect.left;
setShift(Math.round(dx));
}, [open]);
Comment thread
StanislavBG marked this conversation as resolved.
Outdated
Comment thread
StanislavBG marked this conversation as resolved.
Outdated

// Close on outside-click / Esc while open (touch path — pointer users rely on CSS hover/focus).
useEffect(() => {
if (!open) return;
const onPointer = (e: PointerEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
};
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') setOpen(false);
};
document.addEventListener('pointerdown', onPointer);
document.addEventListener('keydown', onKey);
return () => {
document.removeEventListener('pointerdown', onPointer);
document.removeEventListener('keydown', onKey);
};
}, [open]);

return (
<span className={`metric-info${open ? ' is-open' : ''}`} ref={ref}>
<button
type="button"
className="metric-info-btn"
Comment thread
StanislavBG marked this conversation as resolved.
aria-label={aria}
aria-expanded={open}
Comment thread
StanislavBG marked this conversation as resolved.
Outdated
onClick={() => setOpen((v) => !v)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Клик за затваряне може да не скрие popover-а визуално в реален браузър. Кликването върху <button> в Chrome/Firefox дава фокус на бутона, така че .metric-info:focus-within .metric-info-pop (components.css) остава активно дори след като is-open бъде премахнат — popover-ът остава видим, докато фокусът не напусне (напр. отместване на мишката извън елемента при все още фокусиран бутон). При touch устройства (iOS Safari) tap обикновено не фокусира бутон, така че там работи, но на desktop поведението е непоследователно. Обмислете при затваряне (когато преминава от open→closed) също да се извика blur на бутона, или да се синхронизира видимостта изцяло през JS състояние вместо да се разчита на :focus-within.

>
<span className="metric-info-glyph" aria-hidden="true">
</span>
</button>
<span
className={`metric-info-pop${align === 'end' ? ' is-end' : ''}`}
aria-hidden="true"
ref={popRef}
// `translate` composes with the CSS `transform` reveal transition instead of replacing it
style={shift !== 0 ? { translate: `${shift}px 0` } : undefined}
>
<span className="metric-info-title">{title}</span>
<span className="metric-info-summary">{summary}</span>
{readout ? <span className="metric-info-readout">{readout}</span> : null}
</span>
</span>
);
}
Loading