diff --git a/src/components/FooterProvenance.tsx b/src/components/FooterProvenance.tsx index 5e13cf11..56fa37e5 100644 --- a/src/components/FooterProvenance.tsx +++ b/src/components/FooterProvenance.tsx @@ -5,35 +5,37 @@ const REPO_URL = 'https://github.com/bradsaucier/opsnormal'; export function FooterProvenance() { return (

Provenance

-
-
-
Build
-
- v{__APP_VERSION__} -
-
-
-
License
-
MIT
-
-
- - - Source - +
+
+
+
Build
+
+ v{__APP_VERSION__} +
+
+
+
License
+
MIT
+
+
+ + + Source + +
); } diff --git a/src/styles/index.css b/src/styles/index.css index d7f0c226..b83a3d5c 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -379,7 +379,7 @@ h3, .ops-headline-h1 { font-size: 2.5rem; line-height: 1.1; - letter-spacing: 0; + letter-spacing: -0.015em; } .ops-headline-h2 { @@ -1176,6 +1176,7 @@ h3, .ops-telemetry-horizon-label { border-left: 2px solid var(--color-ops-accent-deep); + box-shadow: -4px 0 12px rgba(46, 143, 106, 0.18); } .ops-surface-fault-card { @@ -1510,6 +1511,7 @@ h3, bottom: -1px; width: 2px; height: 16px; + border-radius: 1px; background: var(--color-ops-accent); box-shadow: 0 0 10px rgba(110, 231, 183, 0.2); } @@ -1707,7 +1709,8 @@ h3, box-shadow: inset 0 2px 0 var(--ops-status-nominal-border), inset 1px 0 0 var(--ops-today-rail), - inset -1px 0 0 var(--ops-today-rail); + inset -1px 0 0 var(--ops-today-rail), + 0 0 10px rgba(110, 231, 183, 0.07); } .ops-history-today-cell { @@ -1729,7 +1732,8 @@ h3, box-shadow: inset 2px 0 0 var(--ops-today-rail), inset -1px 0 0 var(--ops-today-rail), - inset 0 2px 0 var(--ops-status-nominal-border); + inset 0 2px 0 var(--ops-status-nominal-border), + 0 0 10px rgba(110, 231, 183, 0.07); } .ops-history-today-cell.ops-history-week-boundary { @@ -2318,6 +2322,10 @@ h3, box-shadow: none; } + .ops-telemetry-horizon-label { + box-shadow: none; + } + .ops-section-frame, .ops-frame-emphasis-primary, .ops-frame-emphasis-standard, @@ -2431,7 +2439,8 @@ h3, --notch: var(--ops-notch-chip); min-height: 28px; min-width: 2rem; - width: 100%; + width: auto; + flex-shrink: 0; justify-content: center; padding: 0 0.75rem; gap: 0.5rem; @@ -2439,34 +2448,20 @@ h3, letter-spacing: 0.2em; text-transform: uppercase; color: var(--color-ops-text-secondary); + white-space: nowrap; } @media (min-width: 1024px) { .ops-provenance-facts { grid-auto-flow: column; grid-auto-columns: max-content; - justify-content: end; + justify-content: start; column-gap: 1.5rem; } .ops-provenance-facts > div { grid-template-columns: auto auto; } - - .ops-provenance-facts dd { - text-align: right; - } - - .ops-provenance-source { - justify-self: end; - } -} - -@media (min-width: 1024px) and (max-width: 1535px) { - .ops-provenance-source { - width: 2rem; - padding: 0; - } } @media (max-width: 640px) { @@ -2481,10 +2476,3 @@ h3, .ops-provenance-source:hover { color: var(--color-ops-text-primary); } - -@media (min-width: 640px) { - .ops-provenance-source { - width: auto; - align-self: center; - } -} diff --git a/tests/e2e/footer-provenance.spec.ts b/tests/e2e/footer-provenance.spec.ts index b334b956..356eaed7 100644 --- a/tests/e2e/footer-provenance.spec.ts +++ b/tests/e2e/footer-provenance.spec.ts @@ -1,5 +1,25 @@ import { expect, test } from '@playwright/test'; +interface Box { + height: number; + width: number; + x: number; + y: number; +} + +function assertBox(box: Box | null, label: string): asserts box is Box { + expect(box, `${label} should have a layout box`).not.toBeNull(); +} + +function boxesOverlap(first: Box, second: Box) { + const horizontalOverlap = + first.x < second.x + second.width && first.x + first.width > second.x; + const verticalOverlap = + first.y < second.y + second.height && first.y + first.height > second.y; + + return horizontalOverlap && verticalOverlap; +} + test('footer exposes build metadata and the public source link', async ({ page, }) => { @@ -23,3 +43,35 @@ test('footer exposes build metadata and the public source link', async ({ page.getByText('OpsNormal is a personal status tracking tool.'), ).toBeVisible(); }); + +for (const width of [1024, 1280, 1536, 1920]) { + test(`footer provenance does not overflow or overlap at ${width}px`, async ({ + page, + }) => { + await page.setViewportSize({ width, height: 900 }); + await page.goto('/'); + + const provenance = page.getByTestId('footer-provenance'); + const label = provenance.getByText('Provenance'); + const facts = provenance.locator('dl'); + const source = page.getByTestId('footer-provenance-source'); + + const provenanceBox = await provenance.boundingBox(); + const labelBox = await label.boundingBox(); + const factsBox = await facts.boundingBox(); + const sourceBox = await source.boundingBox(); + + assertBox(provenanceBox, 'footer provenance'); + assertBox(labelBox, 'footer provenance label'); + assertBox(factsBox, 'footer provenance facts'); + assertBox(sourceBox, 'footer provenance source'); + + expect(sourceBox.x + sourceBox.width).toBeLessThanOrEqual( + provenanceBox.x + provenanceBox.width + 1, + ); + expect(factsBox.y).toBeGreaterThanOrEqual(labelBox.y + labelBox.height - 1); + expect(boxesOverlap(labelBox, factsBox)).toBe(false); + expect(boxesOverlap(labelBox, sourceBox)).toBe(false); + expect(boxesOverlap(factsBox, sourceBox)).toBe(false); + }); +} diff --git a/tests/unit/footerProvenance.test.tsx b/tests/unit/footerProvenance.test.tsx index dbae34ae..6794edf5 100644 --- a/tests/unit/footerProvenance.test.tsx +++ b/tests/unit/footerProvenance.test.tsx @@ -10,6 +10,7 @@ describe('FooterProvenance', () => { expect(screen.getByText('Build')).toBeInTheDocument(); expect(screen.getByText('License')).toBeInTheDocument(); expect(screen.getByText('MIT')).toBeInTheDocument(); + expect(screen.getByText('Source')).toBeInTheDocument(); expect(screen.getByText(/^v\d+\.\d+\.\d+/)).toBeInTheDocument(); const link = screen.getByTestId('footer-provenance-source'); diff --git a/tests/unit/historyGrid.test.tsx b/tests/unit/historyGrid.test.tsx index fab73e01..778e670b 100644 --- a/tests/unit/historyGrid.test.tsx +++ b/tests/unit/historyGrid.test.tsx @@ -249,7 +249,9 @@ describe('history helpers and grid behavior', () => { const previousWeekButton = screen.getByRole('button', { name: /previous week/i, }); - const nextWeekButton = screen.getByRole('button', { name: /next week/i }); + const nextWeekButton = screen.getByRole('button', { + name: /next week/i, + }); const weekStatus = screen.getByTestId('mobile-history-week-status'); const dailyBriefHeading = screen.getByRole('heading', { level: 3, @@ -282,7 +284,7 @@ describe('history helpers and grid behavior', () => { expect( screen.getByRole('heading', { level: 3, name: /sat, mar 28, 2026/i }), ).toBeVisible(); - }); + }, 30000); it('renders a first-run empty state before any history entries exist', () => { const dateKeys = getTrailingDateKeys(30, new Date(2026, 2, 28)); @@ -329,7 +331,7 @@ describe('history helpers and grid behavior', () => { await waitFor(() => { expect(selectedCellSurface).toHaveClass('ops-sector-spine-degraded'); }); - }); + }, 30000); it('applies the state spine to the mobile daily brief surface', async () => { const user = userEvent.setup({ delay: null }); @@ -365,7 +367,7 @@ describe('history helpers and grid behavior', () => { ); expect((await axe(container)).violations).toEqual([]); - }, 15000); + }, 30000); it('has no accessibility violations in the desktop history view', async () => { const dateKeys = getTrailingDateKeys(30, new Date(2026, 2, 28)); @@ -376,7 +378,7 @@ describe('history helpers and grid behavior', () => { ); expect((await axe(container)).violations).toEqual([]); - }, 15000); + }, 30000); it('keeps a single tabbable desktop gridcell and updates the selected-cell brief during keyboard traversal', async () => { const user = userEvent.setup({ delay: null }); diff --git a/tests/unit/todayPanel.test.tsx b/tests/unit/todayPanel.test.tsx index 68557de4..4bc211ce 100644 --- a/tests/unit/todayPanel.test.tsx +++ b/tests/unit/todayPanel.test.tsx @@ -43,7 +43,7 @@ describe('TodayPanel', () => { expect((await axe(container)).violations).toEqual([]); vi.useFakeTimers(); - }); + }, 15000); it('renders direct-select radio controls for each sector', () => { render();