Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
23 changes: 17 additions & 6 deletions frontend/src/styles/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,26 @@

/* Text */
--cudly-text: #111827; /* Primary text */
--cudly-text-muted: #6b7280; /* Secondary / hint text */
--cudly-text-subtle: #9ca3af; /* Placeholder / disabled labels */
--cudly-text-muted: #4b5563; /* Secondary / hint text (7.5:1 on white) */
--cudly-text-subtle: #6b7280; /* Placeholder / disabled labels (4.8:1 on white, AA) */

/* Status (semantic) — keep existing palette to avoid surprise drift */
--cudly-success: #34a853;
--cudly-success: #15803d; /* 5.0:1 on white, and white text on it passes AA */
--cudly-success-bg: #e8f5e9;
--cudly-success-fg: #2e7d32;
--cudly-warn: #fbbc04;
--cudly-warn-bg: #fff3e0;
--cudly-warn-fg: #e65100;
--cudly-error: #ea4335;
--cudly-warn-fg: #b45309;
--cudly-error: #d93025; /* white text on it is 4.8:1 */
--cudly-error-bg: #fce8e6;
--cudly-error-fg: #c5221f;
--cudly-info: #2563eb;
--cudly-info-bg: #eff6ff;

/* Keyboard focus: a surface-coloured gap then a primary ring, visible on
* white and on tinted backgrounds. */
--cudly-focus-ring: 0 0 0 2px var(--cudly-surface), 0 0 0 4px var(--cudly-primary);

/* Typography */
--cudly-font-base: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--cudly-fs-xs: 0.75rem; /* 12px */
Expand Down Expand Up @@ -97,6 +101,13 @@ body {
color: var(--cudly-text);
}

/* 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 {
outline: 2px solid var(--cudly-primary);
outline-offset: 2px;
}

/* Generic utilities — prefer these over inline `style="..."` attributes
* (parsed by HTML, blocked by CSP `style-src 'self'`) and over JS
* `element.style.*` mutations (not blocked by CSP but harder to restyle). */
Expand All @@ -105,7 +116,7 @@ body {
}

.text-muted {
color: var(--cudly-text-subtle);
color: var(--cudly-text-muted);
font-size: 0.85rem;
}

Expand Down
25 changes: 14 additions & 11 deletions frontend/src/styles/components.css
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,8 @@ button.success {
}

button.success:hover {
background: #2d9048;
border-color: #2d9048;
background: #166534;
border-color: #166534;
}

button.danger {
Expand All @@ -168,8 +168,8 @@ button.danger {
}

button.danger:hover {
background: #d33426;
border-color: #d33426;
background: var(--cudly-error-fg);
border-color: var(--cudly-error-fg);
}

/* Secondary button style */
Expand Down Expand Up @@ -238,8 +238,8 @@ button.secondary:hover {
}

.btn-small.success:hover {
background: #2d9048;
border-color: #2d9048;
background: #166534;
border-color: #166534;
}

.btn-small.warning {
Expand Down Expand Up @@ -664,7 +664,7 @@ th.sortable:hover {

th.sortable .sort-indicator {
margin-left: 0.35rem;
color: #bbb;
color: var(--cudly-text-subtle);
font-size: 0.75em;
}

Expand All @@ -678,7 +678,7 @@ th .column-filter-btn {
padding: 0 0.25rem;
border: 0;
background: transparent;
color: #bbb;
color: var(--cudly-text-subtle);
cursor: pointer;
font-size: 0.85em;
line-height: 1;
Expand Down Expand Up @@ -1013,6 +1013,9 @@ tr.rec-cell-summary-row:hover {
background: var(--cudly-info-bg, #eaf0fb);
outline: none;
}
.rec-cell-chevron:focus-visible {
box-shadow: var(--cudly-focus-ring);
}
.rec-cell-identity {
font-weight: 500;
font-size: 0.9em;
Expand Down Expand Up @@ -1208,7 +1211,7 @@ tr.rec-variant-row:hover td:not(.checkbox-col) {
}

.provider-badge.gcp {
background: #4285f4;
background: #1a73e8;
color: white;
}

Expand Down Expand Up @@ -1593,7 +1596,7 @@ tr.rec-variant-row:hover td:not(.checkbox-col) {
.chip-select:focus-visible {
outline: none;
border-color: var(--cudly-primary);
box-shadow: 0 0 0 3px var(--cudly-info-bg);
box-shadow: var(--cudly-focus-ring);
}

.chip-select[aria-expanded="true"] {
Expand Down Expand Up @@ -1654,7 +1657,7 @@ tr.rec-variant-row:hover td:not(.checkbox-col) {
.chip-select-search:focus-visible {
outline: none;
border-color: var(--cudly-primary);
box-shadow: 0 0 0 2px var(--cudly-info-bg);
box-shadow: var(--cudly-focus-ring);
}

.chip-select-options {
Expand Down
18 changes: 16 additions & 2 deletions frontend/src/styles/forms.css
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ label {
#history-controls input[type="date"]:focus-visible {
outline: none;
border-color: var(--cudly-primary);
box-shadow: 0 0 0 3px var(--cudly-info-bg);
box-shadow: var(--cudly-focus-ring);
}

/* Tone down the native calendar-picker indicator inside our pill-
Expand Down Expand Up @@ -286,8 +286,22 @@ input:checked + .slider:before {
border-color: var(--cudly-primary);
}

.ramp-option {
position: relative;
}

/* Visually hidden, not display:none, so the radio stays in the tab order. */
.ramp-option input[type="radio"] {
display: none;
position: absolute;
opacity: 0;
width: 1px;
height: 1px;
margin: 0;
pointer-events: none;
}

.ramp-option:has(input:focus-visible) {
box-shadow: var(--cudly-focus-ring);
}

.ramp-option:has(input:checked) {
Expand Down
16 changes: 15 additions & 1 deletion frontend/src/styles/plans.css
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,22 @@
border-color: var(--cudly-primary);
}

.ramp-option {
position: relative;
}

/* Visually hidden, not display:none, so the radio stays in the tab order. */
.ramp-option input[type="radio"] {
display: none;
position: absolute;
opacity: 0;
width: 1px;
height: 1px;
margin: 0;
pointer-events: none;
}

.ramp-option:has(input:focus-visible) {
box-shadow: var(--cudly-focus-ring);
}

.ramp-option:has(input:checked) {
Expand Down
31 changes: 31 additions & 0 deletions frontend/tests-e2e/ramp-focus.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Keyboard access to the plan ramp options (issue #750).
*
* The ramp radios were `display: none`, which removed them from the tab order
* so the cards could only be chosen with a mouse. jsdom has no layout or tab
* order, so this runs in Chromium: Tab must land on a ramp radio and the card
* must draw a visible focus ring.
*/

import { test, expect } from '@playwright/test';
import { mockApi, seedAuth } from './fixtures/recs';

test('ramp option radios are reachable by Tab and show a focus ring', async ({ page }) => {
await seedAuth(page);
await mockApi(page);
await page.goto('/plans');
await page.getByRole('button', { name: 'New Plan' }).click();

const radio = page.locator('.ramp-option input[type="radio"]').first();
await expect(radio).toBeAttached();
for (let i = 0; i < 80; i++) {
if (await radio.evaluate((el) => el === document.activeElement)) break;
await page.keyboard.press('Tab');
}
await expect(radio).toBeFocused();

// box-shadow animates with the card's transition; wait for the settled ring.
await expect
.poll(() => radio.evaluate((el) => getComputedStyle(el.closest('.ramp-option')!).boxShadow))
.toContain('rgb(37, 99, 235)');
});
Loading