Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
5 changes: 3 additions & 2 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4610,7 +4610,8 @@ export default function App({ showLabPanels = false }: { showLabPanels?: boolean
</div>
<div className="login-controls">
<button className="btn-primary" onClick={() => {
const returnUrl = window.location.pathname + window.location.search;
const returnUrl = returnUrlFromLocation();
rememberOidcReturnUrl(returnUrl);
void auth.signinRedirect({ state: { returnUrl } });
}}>
{t("Log in")}
Expand All @@ -4620,7 +4621,7 @@ export default function App({ showLabPanels = false }: { showLabPanels?: boolean
<small>Enterprise SSO Authentication</small>
</div>
</div>
{destination === "admin" ? <AdminPanel currentBrandName={brandName} onBrandNameChange={setBrandName} accessToken={accessToken} /> : null}
{destination === "admin" && accessToken ? <AdminPanel currentBrandName={brandName} onBrandNameChange={setBrandName} accessToken={accessToken} /> : null}
</main>
<footer className="app-footer" role="contentinfo">
<div className="app-footer-title">
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/styles/tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
--color-header-bg: #ffffff;
--color-header-border: #d8d8d8;
--color-footer-bg: #f9f9fa;
--color-footer-text: #898c8e;
--color-footer-text: #6b6375;
--color-footer-border: #e5e4e7;

/* Focus Ring (§3.1.4 – cursor input form) */
Expand Down
35 changes: 35 additions & 0 deletions frontend/src/styles/tokens.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,42 @@ const RETIRED_LIGHT_ONLY_HEX = [
"#721c24",
];

// WCAG 2.x relative luminance / contrast ratio (SC 1.4.3).
function relativeLuminance(hex: string): number {
const rgb = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);
const [r, g, b] = rgb.map((c) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4));
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}

function contrastRatio(hexA: string, hexB: string): number {
const lA = relativeLuminance(hexA);
const lB = relativeLuminance(hexB);
const [lighter, darker] = lA > lB ? [lA, lB] : [lB, lA];
return (lighter + 0.05) / (darker + 0.05);
}

function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

function readToken(block: string, token: string): string {
// Safe regex: `token` is always a hardcoded CSS custom-property name
// literal passed by the call sites below (e.g. "--color-footer-bg"), and
// escapeRegExp() neutralizes any regex metacharacters before
// interpolation, so no caller-controlled input reaches RegExp unescaped.
const match = block.match(new RegExp(`${escapeRegExp(token)}:\\s*(#[0-9a-fA-F]{6})`)); // nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp
if (!match) throw new Error(`${token} not found as a hex value`);
return match[1];
}
Comment thread
seonghobae marked this conversation as resolved.

describe("design tokens", () => {
it("keeps footer text at or above the WCAG AA 4.5:1 contrast minimum (SC 1.4.3)", () => {
const footerBg = readToken(lightBlock, "--color-footer-bg");
const footerText = readToken(lightBlock, "--color-footer-text");
expect(contrastRatio(footerBg, footerText)).toBeGreaterThanOrEqual(4.5);
});
Comment thread
seonghobae marked this conversation as resolved.


it("defines every badge/accent token in both the light and dark blocks", () => {
for (const token of BADGE_AND_ACCENT_TOKENS) {
expect(lightBlock, `${token} missing from the light :root block`).toContain(`${token}:`);
Expand Down
Loading