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
50 changes: 26 additions & 24 deletions src/components/FooterProvenance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,37 @@ const REPO_URL = 'https://github.com/bradsaucier/opsnormal';
export function FooterProvenance() {
return (
<div
className="grid min-w-0 gap-3 sm:w-full sm:self-end sm:grid-cols-[auto_minmax(0,1fr)_auto] sm:items-center sm:gap-4 2xl:gap-8"
className="grid min-w-0 gap-2 sm:w-full sm:gap-3"
data-testid="footer-provenance"
>
<p className="ops-eyebrow-strong font-semibold text-ops-text-primary">
Provenance
</p>
<dl className="ops-provenance-facts">
<div>
<dt>Build</dt>
<dd className="ops-numeric text-ops-text-primary">
v{__APP_VERSION__}
</dd>
</div>
<div>
<dt>License</dt>
<dd>MIT</dd>
</div>
</dl>
<a
className="ops-action-button ops-action-button-sm ops-action-button-subtle ops-provenance-source"
href={REPO_URL}
target="_blank"
rel="noopener noreferrer"
aria-label="View OpsNormal source on GitHub. Opens in a new tab."
data-testid="footer-provenance-source"
>
<GitHubMark />
<span className="lg:sr-only 2xl:not-sr-only">Source</span>
</a>
<div className="flex flex-wrap items-center gap-3 sm:gap-4">
<dl className="ops-provenance-facts">
<div>
<dt>Build</dt>
<dd className="ops-numeric text-ops-text-primary">
v{__APP_VERSION__}
</dd>
</div>
<div>
<dt>License</dt>
<dd>MIT</dd>
</div>
</dl>
<a
className="ops-action-button ops-action-button-sm ops-action-button-subtle ops-provenance-source ml-auto"
href={REPO_URL}
target="_blank"
rel="noopener noreferrer"
aria-label="View OpsNormal source on GitHub. Opens in a new tab."
data-testid="footer-provenance-source"
>
<GitHubMark />
<span>Source</span>
</a>
</div>
</div>
);
}
42 changes: 15 additions & 27 deletions src/styles/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -2431,42 +2439,29 @@ 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;
font-size: 11px;
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) {
Expand All @@ -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;
}
}
52 changes: 52 additions & 0 deletions tests/e2e/footer-provenance.spec.ts
Original file line number Diff line number Diff line change
@@ -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,
}) => {
Expand All @@ -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);
});
}
1 change: 1 addition & 0 deletions tests/unit/footerProvenance.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
12 changes: 7 additions & 5 deletions tests/unit/historyGrid.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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 });
Expand Down Expand Up @@ -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));
Expand All @@ -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 });
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/todayPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<TodayPanel todayKey="2026-03-27" />);
Expand Down
Loading