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
20 changes: 18 additions & 2 deletions src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@
/* ---------- Small shared helpers ---------- */
.mono { font-family: var(--font-mono); }
.wrap { max-width: 1100px; margin: 0 auto; padding: 0 32px; }
@media (max-width: 640px) { .wrap { padding: 0 16px; } }

/* ---------- Docs content — global so styles reach mdsvex child components ---------- */
.content section { scroll-margin-top: 88px; padding-bottom: 8px; }
Expand All @@ -200,6 +201,13 @@
.content li { font-size: 15px; line-height: 1.66; color: var(--muted); margin-bottom: 7px; }
.content li::marker { color: var(--muted); }

/* Ordered lists (not the custom step list): hanging indent so a wrapped item —
e.g. a long code path that breaks mid-token — is indented under its first
line instead of returning to the margin. Needs an inside marker so the number
flows with the first line rather than sitting in the outside gutter. */
.content ol:not(.steplist) { list-style-position: inside; padding-left: 0; }
.content ol:not(.steplist) > li { padding-left: 1.5rem; text-indent: -1.5rem; }

/* inline code — reaches markup inside mdsvex-rendered content */
.content code {
font-family: var(--font-mono);
Expand All @@ -208,6 +216,10 @@
color: var(--text);
padding: 2px 6px;
border-radius: 5px;
/* Break long tokens (file paths, flags) to stay inside the container, but only
as a last resort — so injected `<wbr>` slash hints win and paths wrap on
segment boundaries. Table code opts back into `anywhere` (see .deftable). */
overflow-wrap: break-word;
}

/* step list */
Expand All @@ -227,10 +239,11 @@
.steplist > li p { margin: 0; }

/* definition / api table */
.deftable-scroll { overflow-x: auto; margin: 0 0 18px; }
.deftable {
width: 100%; border-collapse: collapse;
border: 1px solid var(--border); border-radius: 10px; overflow: hidden;
margin: 0 0 18px; font-size: 14px;
margin: 0; font-size: 14px;
}
.deftable th {
text-align: left; font-family: var(--font-mono);
Expand All @@ -241,7 +254,10 @@
.deftable td { padding: 11px 14px; border-bottom: 1px solid var(--border); color: var(--muted); line-height: 1.5; vertical-align: top; }
.deftable tr:last-child td { border-bottom: 0; }
.deftable td:first-child { color: var(--text); }
.deftable code { white-space: nowrap; }
/* Table code wraps only on word / path-segment boundaries (spaces + injected
`<wbr>` hints), never mid-token. A long command widens its column and the
.deftable-scroll wrapper scrolls, rather than breaking words apart. */
.deftable code { overflow-wrap: normal; }

.method {
font-size: 11px; font-weight: 700; padding: 2px 7px; border-radius: 5px;
Expand Down
7 changes: 4 additions & 3 deletions src/lib/components/Hero.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@
.g1 { top: -160px; left: -120px; width: 460px; height: 460px; background: radial-gradient(circle, var(--ring), transparent 70%); }
.g2 { bottom: -160px; right: -140px; width: 560px; height: 560px; background: radial-gradient(circle, color-mix(in srgb, var(--chart-1) 24%, transparent), transparent 70%); }

.inner { position: relative; display: grid; grid-template-columns: 1.2fr 0.8fr; gap: 48px; align-items: center; padding: 80px 32px 84px; }
.inner { position: relative; display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 0.8fr); gap: 48px; align-items: center; padding: 80px 32px 84px; }

.eyebrow { font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--muted); margin-bottom: 22px; }
.pill { display: inline-flex; align-items: center; gap: 8px; padding: 5px 12px; border: 1px solid var(--border); border-radius: 999px; background: var(--card); }
Expand Down Expand Up @@ -159,7 +159,8 @@
.donut .num b { font-size: 34px; font-weight: 800; color: var(--status-pass); letter-spacing: -0.02em; }
.donut .num b i { font-size: 17px; font-style: normal; color: var(--chart-2); }
.donut .num small { display: block; margin-top: 3px; font-size: 8.5px; letter-spacing: 0.22em; text-transform: uppercase; color: var(--muted); }
.ring-meta .rm-title { font-size: 13.5px; font-weight: 600; }
.ring-meta { min-width: 0; }
.ring-meta .rm-title { font-size: 13.5px; font-weight: 600; overflow-wrap: anywhere; }
.ring-meta .rm-sub { font-size: 12px; color: var(--muted); margin-top: 3px; }
.ring-meta .delta { margin-top: 10px; color: var(--status-pass); }

Expand All @@ -184,7 +185,7 @@
@media (prefers-reduced-motion: reduce) { .cursor { animation: none; } .typer { transition: none; } }

@media (max-width: 880px) {
.inner { grid-template-columns: 1fr; gap: 36px; padding-top: 56px; }
.inner { grid-template-columns: minmax(0, 1fr); gap: 36px; padding-top: 56px; }
.title { font-size: 52px; }
}
</style>
112 changes: 106 additions & 6 deletions src/lib/components/Nav.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,28 @@
];

const isActive = (href: string) => href === '/docs' && $page.url.pathname.startsWith('/docs');

// Mobile menu: shown via the burger at the same width the inline links hide.
const MOBILE_BREAKPOINT = 760;
let menuOpen = $state(false);

function onWindowKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') menuOpen = false;
}

function onWindowResize() {
// The burger is hidden above the breakpoint — don't leave the overlay stuck open.
if (window.innerWidth > MOBILE_BREAKPOINT) menuOpen = false;
}
</script>

<nav>
<svelte:window onkeydown={onWindowKeydown} onresize={onWindowResize} />

<nav class="topbar">
<div class="wrap inner">
<a class="brand mono" href="/">
<BrandMark />
Coverage Tracker
<span class="brand-label">Coverage Tracker</span>
</a>

<div class="navlinks">
Expand All @@ -29,13 +44,39 @@

<div class="navright">
<ThemePicker />
<a class="ghost mono" href="https://github.com/CoverageTracker/coverage-tracker" target="_blank" rel="noreferrer">GitHub ↗</a>
<a class="ghost" href="https://github.com/CoverageTracker/coverage-tracker" target="_blank" rel="noreferrer" aria-label="GitHub repository" title="GitHub repository">
<svg class="octicon" viewBox="0 0 98 96" width="20" height="20" aria-hidden="true"><path d="M41.4395 69.3848C28.8066 67.8535 19.9062 58.7617 19.9062 46.9902C19.9062 42.2051 21.6289 37.0371 24.5 33.5918C23.2559 30.4336 23.4473 23.7344 24.8828 20.959C28.7109 20.4805 33.8789 22.4902 36.9414 25.2656C40.5781 24.1172 44.4062 23.543 49.0957 23.543C53.7852 23.543 57.6133 24.1172 61.0586 25.1699C64.0254 22.4902 69.2891 20.4805 73.1172 20.959C74.457 23.543 74.6484 30.2422 73.4043 33.4961C76.4668 37.1328 78.0937 42.0137 78.0937 46.9902C78.0937 58.7617 69.1934 67.6621 56.3691 69.2891C59.623 71.3945 61.8242 75.9883 61.8242 81.252L61.8242 91.2051C61.8242 94.0762 64.2168 95.7031 67.0879 94.5547C84.4102 87.9512 98 70.6289 98 49.1914C98 22.1074 75.9883 6.69539e-07 48.9043 4.309e-07C21.8203 1.92261e-07 -1.9479e-07 22.1074 -4.3343e-07 49.1914C-6.20631e-07 70.4375 13.4941 88.0469 31.6777 94.6504C34.2617 95.6074 36.75 93.8848 36.75 91.3008L36.75 83.6445C35.4102 84.2188 33.6875 84.6016 32.1562 84.6016C25.8398 84.6016 22.1074 81.1563 19.4277 74.7441C18.375 72.1602 17.2266 70.6289 15.0254 70.3418C13.877 70.2461 13.4941 69.7676 13.4941 69.1934C13.4941 68.0449 15.4082 67.1836 17.3223 67.1836C20.0977 67.1836 22.4902 68.9063 24.9785 72.4473C26.8926 75.2227 28.9023 76.4668 31.2949 76.4668C33.6875 76.4668 35.2187 75.6055 37.4199 73.4043C39.0469 71.7773 40.291 70.3418 41.4395 69.3848Z" /></svg>
</a>
</div>

<button
class="burger"
onclick={() => (menuOpen = !menuOpen)}
aria-label={menuOpen ? 'Close menu' : 'Open menu'}
aria-expanded={menuOpen}
aria-controls="mobile-menu"
>
{#if menuOpen}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M18 6L6 18" /></svg>
{:else}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M3 12h18M3 18h18" /></svg>
{/if}
</button>
</div>
</nav>

{#if menuOpen}
<div class="mobile-menu" id="mobile-menu">
<nav class="mobile-links" aria-label="Primary">
{#each links as l}
<a href={l.href} class:current={isActive(l.href)} onclick={() => (menuOpen = false)}>{l.label}</a>
{/each}
</nav>
</div>
{/if}

<style>
nav {
.topbar {
position: sticky; top: 0; z-index: 40; height: 64px; display: flex; align-items: center;
background: color-mix(in srgb, var(--bg) 86%, transparent);
backdrop-filter: blur(10px);
Expand All @@ -48,7 +89,66 @@
.navlinks a:hover { color: var(--text); }
.navlinks a.current { color: var(--text); }
.navright { margin-left: auto; display: flex; align-items: center; gap: 16px; }
.ghost { font-size: 13px; color: var(--muted); transition: color 120ms ease; }
.ghost { display: inline-flex; align-items: center; color: var(--muted); transition: color 120ms ease, border-color 120ms ease, background 120ms ease; }
.ghost:hover { color: var(--text); }
@media (max-width: 760px) { .navlinks { display: none; } }
/* GitHub brand rules: the Invertocat may only be black/white/gray/green — never
tinted by the site theme. Fill is a fixed gray keyed to light/dark mode only
(not the theme hue), with black/white hover for contrast/affordance. */
.octicon { display: block; fill: #656d76; transition: fill 120ms ease; }
:global([data-mode='dark']) .octicon { fill: #8b949e; }
.ghost:hover .octicon { fill: #1f2328; }
:global([data-mode='dark']) .ghost:hover .octicon { fill: #ffffff; }

/* Burger toggle — hidden on desktop, revealed when the inline links collapse. */
.burger {
display: none;
align-items: center; justify-content: center;
width: 38px; height: 38px; border-radius: 8px;
background: transparent; border: 1px solid var(--border); color: var(--muted);
cursor: pointer; transition: color 120ms ease, border-color 120ms ease, background 120ms ease;
}
.burger svg { width: 20px; height: 20px; }
.burger:hover { color: var(--primary); border-color: var(--primary); background: var(--elevated); }

/* Full-screen overlay menu. z-index sits below the nav (40) so the nav — and
its close (✕) button — stay visible and clickable on top of the overlay. */
.mobile-menu {
position: fixed; inset: 0; z-index: 30;
display: flex; align-items: flex-start; justify-content: center;
padding: 80px 24px 24px;
background: var(--bg);
animation: menufade 140ms ease;
}
.mobile-links { display: flex; flex-direction: column; align-items: stretch; gap: 6px; width: 100%; max-width: 320px; }
.mobile-links a {
font-size: 20px; font-weight: 600; color: var(--muted); text-align: center;
padding: 16px 20px; border-radius: 12px; border: 1px solid transparent;
transition: color 120ms ease, background 120ms ease, border-color 120ms ease;
}
.mobile-links a:hover { color: var(--text); background: var(--card); border-color: var(--border); }
.mobile-links a.current { color: var(--primary); background: var(--accent-fill); }
@keyframes menufade { from { opacity: 0; } to { opacity: 1; } }

@media (max-width: 760px) {
.navlinks { display: none; }
/* Three zones: brand left, controls centered, burger right. */
.inner { display: grid; grid-template-columns: 1fr auto 1fr; }
.brand { justify-self: start; }
.navright { justify-self: center; margin-left: 0; }
.burger { display: inline-flex; justify-self: end; }
/* Give the GitHub link the same outlined-button footprint as the burger. */
.ghost {
width: 38px; height: 38px; justify-content: center;
border: 1px solid var(--border); border-radius: 8px; background: transparent;
}
.ghost:hover { color: var(--primary); border-color: var(--primary); background: var(--elevated); }
}
/* Never leave the overlay visible above the breakpoint (e.g. after a resize). */
@media (min-width: 761px) { .mobile-menu { display: none; } }
@media (max-width: 560px) {
.brand-label { display: none; }
.navright { gap: 10px; }
/* Icon-only nav: match the brand tile to the 38px control buttons. */
.brand :global(svg) { width: 38px; height: 38px; }
}
</style>
33 changes: 32 additions & 1 deletion src/lib/components/ThemePicker.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@
if (root && !root.contains(e.target as Node)) open = false;
}

function onWindowKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') open = false;
}

function pick(id: string) {
setTheme(id);
open = false;
}
</script>

<svelte:window onclick={onWindowClick} />
<svelte:window onclick={onWindowClick} onkeydown={onWindowKeydown} />

<div class="picker" bind:this={root}>
<!-- light/dark toggle -->
Expand Down Expand Up @@ -118,4 +122,31 @@
.row-name { font-size: 13px; font-weight: 600; }
.row-note { font-size: 11px; color: var(--muted); }
.check { width: 16px; height: 16px; margin-left: auto; color: var(--primary); }

/* On small screens the dropdown would render off-screen, so present the scheme
list as a full-screen overlay below the nav (matching the burger menu). The
nav — and this picker's trigger — stay visible above it (top: 64px), so the
trigger toggles it closed; Escape and picking a scheme also close it. */
@media (max-width: 760px) {
.menu {
/* .topbar's backdrop-filter makes it the containing block for this fixed
element, so size with viewport units (immune to the 64px-tall block) and
anchor with top/left/right against the full-width bar at the origin. */
position: fixed; top: 64px; left: 0; right: 0; bottom: auto; width: auto;
height: calc(100dvh - 64px);
display: flex; flex-direction: column; align-items: center; gap: 2px;
padding: 24px; overflow-y: auto;
background: var(--bg); border: 0; border-radius: 0; box-shadow: none;
animation: menufade 140ms ease;
}
.menu-head { width: 100%; max-width: 360px; padding: 6px 10px 10px; }
.row { max-width: 360px; padding: 12px; }
}
@keyframes menufade { from { opacity: 0; } to { opacity: 1; } }

/* On small screens, drop the scheme name and show only the color dots. */
@media (max-width: 560px) {
.trigger .name { display: none; }
.trigger { gap: 8px; padding: 0 10px; }
}
</style>
3 changes: 1 addition & 2 deletions src/lib/components/docs/Callout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<div class="note {variant}">
<div class="body">
<div class="nh mono">{title}</div>
<p>{@render children?.()}</p>
<div class="ct">{@render children?.()}</div>
</div>
</div>

Expand Down Expand Up @@ -38,7 +38,6 @@
}
.note.warn .nh { color: var(--status-alert); }
.body { min-width: 0; }
.note p { margin: 0; font-size: 14px; line-height: 1.6; color: var(--text); }
.note :global(code) {
font-family: var(--font-mono);
font-size: 0.86em;
Expand Down
44 changes: 44 additions & 0 deletions src/lib/docs-content/remark-inline-code-breaks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// @ts-check
import { visit } from 'unist-util-visit';

/**
* Adds `<wbr>` line-break opportunities after slashes in inline code so long
* file paths prefer to wrap on path-segment boundaries (e.g. after `test/`)
* rather than mid-token. `<wbr>` is a hint, not a character — it is not included
* when the text is copied, so the path stays intact on paste.
*
* The code text is emitted as normal `text` nodes (escaped once by the mdsvex
* pipeline, exactly like ordinary inline code) interleaved with raw `<wbr>`
* html nodes — so shell metacharacters like `>` and `&` render correctly rather
* than being double-escaped.
*
* Site-only: the docs export (scripts/export-docs.ts) does not run remark, so
* downstream markdown keeps clean inline code.
*
* @returns {(tree: import('mdast').Root) => void}
*/
export function remarkInlineCodeBreaks() {
return (tree) => {
visit(tree, 'inlineCode', (node, index, parent) => {
if (!parent || typeof index !== 'number') return;
const value = /** @type {any} */ (node).value;
if (typeof value !== 'string' || !value.includes('/')) return;

/** @type {any[]} */
const children = [];
value.split('/').forEach((segment, i) => {
if (i > 0) {
children.push({ type: 'text', value: '/' });
children.push({ type: 'html', value: '<wbr>' });
}
if (segment) children.push({ type: 'text', value: segment });
});

/** @type {any} */ (parent.children)[index] = {
type: 'inlineCodeWithBreaks',
data: { hName: 'code' },
children
};
});
};
}
42 changes: 42 additions & 0 deletions src/lib/docs-content/remark-plugins.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { compile } from 'mdsvex';
import { remarkCallouts } from './remark-callouts.js';
import { remarkCodeBlocks } from './remark-code-blocks.js';
import { remarkTables } from './remark-tables.js';
import { remarkInlineCodeBreaks } from './remark-inline-code-breaks.js';

// ---------------------------------------------------------------------------
// Helpers
Expand Down Expand Up @@ -153,4 +154,45 @@ describe('remarkTables', () => {
const code = await compiled(table, [remarkTables]);
expect(code).toMatch(/<table class="deftable">/);
});

it('wraps pipe tables in a horizontally-scrollable container', async () => {
const code = await compiled(table, [remarkTables]);
expect(code).toMatch(/<div class="deftable-scroll">\s*<table class="deftable">/);
});

it('wraps hand-written HTML deftables in a scroll container', async () => {
const html =
'<table class="deftable">\n <thead><tr><th>A</th></tr></thead>\n <tbody><tr><td>1</td></tr></tbody>\n</table>';
const code = await compiled(html, [remarkTables]);
expect(code).toMatch(/<div class="deftable-scroll"><table class="deftable">/);
});

it('does not double-wrap an already-wrapped HTML table', async () => {
const html =
'<div class="deftable-scroll"><table class="deftable"><tbody><tr><td>1</td></tr></tbody></table></div>';
const code = await compiled(html, [remarkTables]);
expect(code.match(/deftable-scroll/g) ?? []).toHaveLength(1);
});
});

// ---------------------------------------------------------------------------
// remarkInlineCodeBreaks
// ---------------------------------------------------------------------------

describe('remarkInlineCodeBreaks', () => {
it('inserts <wbr> after each slash in inline code paths', async () => {
const code = await compiled('`build/reports/jacoco/test.xml`', [remarkInlineCodeBreaks]);
expect(code).toMatch(/build\/<wbr\s*\/?>reports\/<wbr\s*\/?>jacoco\/<wbr\s*\/?>test\.xml/);
});

it('leaves slash-free inline code untouched', async () => {
const code = await compiled('`coverage.out`', [remarkInlineCodeBreaks]);
expect(code).not.toMatch(/<wbr/);
expect(code).toMatch(/<code>coverage\.out<\/code>/);
});

it('does not double-escape shell metacharacters in a path command', async () => {
const code = await compiled('`gocyclo ./... > out.txt`', [remarkInlineCodeBreaks]);
expect(code).not.toMatch(/&amp;gt;/);
});
});
Loading
Loading