Skip to content
Merged
49 changes: 49 additions & 0 deletions frontend/src/__tests__/typography.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Typography foundation (issue #741): body line-height, form controls that
* inherit the page font, a base heading scale on the --cudly-fs-* tokens, and
* no literal sizes or weights that duplicate a token.
*/
import fs from 'fs';
import path from 'path';

const stylesDir = path.join(__dirname, '..', 'styles');
const read = (file: string): string => fs.readFileSync(path.join(stylesDir, file), 'utf8');
const files = fs.readdirSync(stylesDir).filter(file => file.endsWith('.css'));

/** Body of the first rule whose selector list matches `selector` exactly. */
function ruleBody(css: string, selector: RegExp): string {
const match = new RegExp(`(?:^|\\})\\s*${selector.source}\\s*\\{([^}]*)\\}`, 'm').exec(css);
if (!match) throw new Error(`no rule for ${selector.source}`);
return match[1]!;
}

describe('Typography foundation (issue #741)', () => {
const base = read('base.css');

test('body sets the normal line-height token', () => {
expect(ruleBody(base, /body/)).toMatch(/line-height:\s*var\(--cudly-lh-normal\)/);
});

test('form controls inherit the page font at the button size', () => {
const body = ruleBody(base, /button,\s*input,\s*select,\s*textarea/);
expect(body).toMatch(/font:\s*inherit/);
expect(body).toMatch(/font-size:\s*var\(--cudly-fs-sm\)/);
});

test.each([
['h1', 'fs-2xl'],
['h2', 'fs-xl'],
['h3', 'fs-lg'],
['h4', 'fs-md'],
['h5, h6', 'fs-sm'],
])('%s has a zero-specificity base size on --cudly-%s', (tags, token) => {
const selector = new RegExp(`:where\\(${tags.replace(/ /g, '\\s*')}\\)`);
expect(ruleBody(base, selector)).toContain(`var(--cudly-${token})`);
});

test.each(files)('%s has no literal font size or weight that duplicates a token', file => {
const css = read(file);
expect(css).not.toMatch(/font-size:\s*(0\.75|0\.875|1|1\.125|1\.375|1\.75|2\.25)rem\s*;/);
expect(css).not.toMatch(/font-weight:\s*(bold|400|500|600|700)\s*;/);
});
});
33 changes: 32 additions & 1 deletion frontend/src/styles/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,41 @@ html {

body {
font-family: var(--cudly-font-base);
line-height: var(--cudly-lh-normal);
background: var(--cudly-bg);
color: var(--cudly-text);
}

/* Form controls do not inherit the page font by default (inputs fall back
* to 13.33px system text next to 14px buttons). Inherit family and weight,
* then pin the size to the button size and keep the line-height tight so
* control heights stay as they were. Element selectors, so the existing
* `select`, `button` and class rules later in the cascade still win. */
button,
input,
select,
textarea {
font: inherit;
font-size: var(--cudly-fs-sm);
line-height: var(--cudly-lh-tight);
}

/* Base heading scale on the type tokens, close to the browser defaults it
* replaces (h1 32px -> 28px, h2 24px -> 22px, h5/h6 grow to 14px). Weight
* stays the browser's bold so headings that only set a size keep their
* look. :where() keeps it at zero specificity so every contextual heading
* rule (.card h3, .page-hero h2, ...) still overrides it. */
:where(h1, h2, h3, h4, h5, h6) {
line-height: var(--cudly-lh-tight);
font-weight: var(--cudly-fw-bold);
}

:where(h1) { font-size: var(--cudly-fs-2xl); }
:where(h2) { font-size: var(--cudly-fs-xl); }
:where(h3) { font-size: var(--cudly-fs-lg); }
:where(h4) { font-size: var(--cudly-fs-md); }
:where(h5, h6) { font-size: var(--cudly-fs-sm); }

/* Default keyboard focus ring. Zero specificity (:where) so a component that
* draws its own focus style keeps it; everything else gets a visible ring. */
:where(a[href], button, input, select, textarea, summary, [tabindex]):focus-visible {
Expand Down Expand Up @@ -234,7 +265,7 @@ body {
/* Help text */
.help-text {
color: var(--cudly-text-muted);
font-size: 0.875rem;
font-size: var(--cudly-fs-sm);
margin-bottom: 0.75rem;
}

Expand Down
4 changes: 2 additions & 2 deletions frontend/src/styles/charts.css
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,12 @@
.ladder-timeline-controls .form-group label {
margin-bottom: var(--cudly-sp-1);
font-size: var(--cudly-fs-xs);
font-weight: 500;
font-weight: var(--cudly-fw-medium);
color: var(--cudly-text-muted);
}
.ladder-timeline-controls input[data-date] { width: 11.5rem; font-variant-numeric: tabular-nums; }
.ladder-timeline-state { margin: 0 0 var(--cudly-sp-3); }
.ladder-timeline-stat { margin: var(--cudly-sp-3) 0 var(--cudly-sp-1); font-weight: 600; }
.ladder-timeline-stat { margin: var(--cudly-sp-3) 0 var(--cudly-sp-1); font-weight: var(--cudly-fw-semibold); }
.ladder-timeline-note { margin: 0 0 var(--cudly-sp-2); font-size: var(--cudly-fs-sm); color: var(--cudly-text-muted); }
.ladder-timeline-chart { height: 320px; margin: 1rem 0; }
#ladder-purchase-timeline [data-table] { overflow-x: auto; }
Expand Down
62 changes: 31 additions & 31 deletions frontend/src/styles/components.css
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,13 @@

.card h3 {
color: var(--cudly-text-muted);
font-size: 0.875rem;
font-size: var(--cudly-fs-sm);
margin-bottom: 0.5rem;
}

.card .value {
font-size: 1.75rem;
font-weight: bold;
font-size: var(--cudly-fs-2xl);
font-weight: var(--cudly-fw-bold);
}

.card .monthly {
Expand Down Expand Up @@ -123,7 +123,7 @@ button {
background: var(--cudly-surface);
color: var(--cudly-text);
cursor: pointer;
font-size: 0.875rem;
font-size: var(--cudly-fs-sm);
transition: background var(--cudly-dur) var(--cudly-ease), border-color var(--cudly-dur) var(--cudly-ease);
}

Expand Down Expand Up @@ -195,7 +195,7 @@ button.secondary:hover {
background: var(--cudly-surface);
color: var(--cudly-text);
cursor: pointer;
font-size: 0.875rem;
font-size: var(--cudly-fs-sm);
transition: background var(--cudly-dur) var(--cudly-ease), border-color var(--cudly-dur) var(--cudly-ease);
}

Expand Down Expand Up @@ -389,7 +389,7 @@ button.secondary:hover {
.toast--error { border-left-color: var(--cudly-error-fg); }

.toast-icon {
font-weight: bold;
font-weight: var(--cudly-fw-bold);
font-size: 1.1rem;
line-height: 1.2;
}
Expand Down Expand Up @@ -454,7 +454,7 @@ button.secondary:hover {

.btn-download .download-icon {
margin-right: 0.4rem;
font-weight: bold;
font-weight: var(--cudly-fw-bold);
color: var(--cudly-primary);
}

Expand Down Expand Up @@ -559,7 +559,7 @@ button.secondary:hover {

.accounts-overview-chip-count {
font-size: 1.4rem;
font-weight: 700;
font-weight: var(--cudly-fw-bold);
color: var(--cudly-primary);
}

Expand Down Expand Up @@ -598,7 +598,7 @@ button.secondary:hover {
background: var(--cudly-info-bg);
border-color: #4a90e2;
color: #1a56c4;
font-weight: 600;
font-weight: var(--cudly-fw-semibold);
}

/* Per-provider accounts table (P3). Replaces the prior inline-row
Expand All @@ -622,7 +622,7 @@ button.secondary:hover {
.accounts-table th {
font-size: 0.85rem;
color: var(--cudly-text-muted);
font-weight: 500;
font-weight: var(--cudly-fw-medium);
background: #fafafa;
}

Expand Down Expand Up @@ -714,7 +714,7 @@ th .column-filter-btn:hover {
}
th .column-filter-btn.active {
color: var(--cudly-primary);
font-weight: 700;
font-weight: var(--cudly-fw-bold);
}

/* Detached popover (lives in document.body, positioned absolutely). */
Expand All @@ -733,7 +733,7 @@ th .column-filter-btn.active {
.column-filter-popover .column-filter-heading {
margin: 0 0 0.5rem 0;
font-size: 0.9rem;
font-weight: 600;
font-weight: var(--cudly-fw-semibold);
}
.column-filter-popover .column-filter-all {
display: flex;
Expand All @@ -742,7 +742,7 @@ th .column-filter-btn.active {
padding: 0.25rem 0;
border-bottom: 1px solid var(--cudly-border);
margin-bottom: 0.5rem;
font-weight: 500;
font-weight: var(--cudly-fw-medium);
}
.column-filter-popover .column-filter-list {
max-height: 240px;
Expand Down Expand Up @@ -959,7 +959,7 @@ tr.recommendation-row:hover {
.column-visibility-popover .column-visibility-title {
margin: 0 0 0.4rem;
font-size: 0.8em;
font-weight: 600;
font-weight: var(--cudly-fw-semibold);
color: #444;
text-transform: uppercase;
letter-spacing: 0.04em;
Expand Down Expand Up @@ -1043,7 +1043,7 @@ tr.rec-cell-summary-row:hover {
box-shadow: var(--cudly-focus-ring);
}
.rec-cell-identity {
font-weight: 500;
font-weight: var(--cudly-fw-medium);
font-size: 0.9em;
}
.rec-cell-range {
Expand Down Expand Up @@ -1142,7 +1142,7 @@ tr.rec-variant-row:hover td:not(.checkbox-col) {
}

.account-delete-pending-total {
font-weight: 600;
font-weight: var(--cudly-fw-semibold);
}

/* Approval-details modal (issue #374): when the confirmDialog body
Expand Down Expand Up @@ -1187,8 +1187,8 @@ tr.rec-variant-row:hover td:not(.checkbox-col) {
}

.approval-details-stat-value {
font-size: 1rem;
font-weight: 600;
font-size: var(--cudly-fs-md);
font-weight: var(--cudly-fw-semibold);
color: #222;
}

Expand Down Expand Up @@ -1217,7 +1217,7 @@ tr.rec-variant-row:hover td:not(.checkbox-col) {
position: sticky;
top: 0;
background: var(--cudly-surface-muted);
font-weight: 600;
font-weight: var(--cudly-fw-semibold);
color: var(--cudly-text-muted, #444);
font-size: 0.78rem;
text-transform: uppercase;
Expand All @@ -1239,8 +1239,8 @@ tr.rec-variant-row:hover td:not(.checkbox-col) {
display: inline-block;
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
font-size: var(--cudly-fs-xs);
font-weight: var(--cudly-fw-semibold);
/* No text-transform: let the HTML control casing. Acronyms (AWS,
* GCP) render as-written; brand names (Azure) keep their native
* title case rather than being SHOUT-CASED by CSS. */
Expand All @@ -1266,7 +1266,7 @@ tr.rec-variant-row:hover td:not(.checkbox-col) {
display: inline-block;
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-size: var(--cudly-fs-xs);
background: var(--cudly-info-bg);
color: var(--cudly-primary);
}
Expand All @@ -1276,7 +1276,7 @@ tr.rec-variant-row:hover td:not(.checkbox-col) {
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 600;
font-weight: var(--cudly-fw-semibold);
}

.status-badge.active {
Expand Down Expand Up @@ -1395,8 +1395,8 @@ tr.rec-variant-row:hover td:not(.checkbox-col) {

/* Role badge */
.role-badge {
font-size: 0.75rem;
font-weight: 600;
font-size: var(--cudly-fs-xs);
font-weight: var(--cudly-fw-semibold);
padding: 0.15rem 0.5rem;
margin-left: 0.4rem;
border-radius: 999px;
Expand All @@ -1410,7 +1410,7 @@ tr.rec-variant-row:hover td:not(.checkbox-col) {
.badge-admin {
display: inline-block;
font-size: 0.8rem;
font-weight: 600;
font-weight: var(--cudly-fw-semibold);
padding: 0.2rem 0.5rem;
border-radius: 4px;
background: var(--cudly-info-bg);
Expand All @@ -1420,7 +1420,7 @@ tr.rec-variant-row:hover td:not(.checkbox-col) {
.badge-user {
display: inline-block;
font-size: 0.8rem;
font-weight: 600;
font-weight: var(--cudly-fw-semibold);
padding: 0.2rem 0.5rem;
border-radius: 4px;
background: var(--cudly-border);
Expand All @@ -1430,7 +1430,7 @@ tr.rec-variant-row:hover td:not(.checkbox-col) {
.badge-readonly {
display: inline-block;
font-size: 0.8rem;
font-weight: 600;
font-weight: var(--cudly-fw-semibold);
padding: 0.2rem 0.5rem;
border-radius: 4px;
background: var(--cudly-bg);
Expand All @@ -1445,7 +1445,7 @@ tr.rec-variant-row:hover td:not(.checkbox-col) {
.badge {
display: inline-block;
font-size: 0.8rem;
font-weight: 600;
font-weight: var(--cudly-fw-semibold);
padding: 0.2rem 0.55rem;
border-radius: 999px;
letter-spacing: 0.01em;
Expand Down Expand Up @@ -1498,7 +1498,7 @@ tr.rec-variant-row:hover td:not(.checkbox-col) {

.history-approver strong {
color: var(--cudly-text);
font-weight: 600;
font-weight: var(--cudly-fw-semibold);
}

/* Upfront Cost cell when the amortize toggle folds the upfront into the
Expand Down Expand Up @@ -1549,7 +1549,7 @@ tr.rec-variant-row:hover td:not(.checkbox-col) {
}

.copy-btn .copy-icon {
font-size: 1rem;
font-size: var(--cudly-fs-md);
}

.copy-btn.copied {
Expand Down
Loading
Loading