Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2633ddf
Add a comparison with nmap, Angry IP Scanner and Advanced IP Scanner …
fstubner Sep 24, 2026
9dd581c
Outline netscli's comparison column instead of tinting it
fstubner Sep 24, 2026
e7c0e0b
WIP: comparison table, 8 rows, no lead, body-coloured column text
fstubner Sep 24, 2026
0edb098
Rebuild the comparison around the questions people choosing a scanner…
fstubner Sep 24, 2026
1df11fc
Make the comparison a tick table
fstubner Sep 24, 2026
8e49817
Fade the comparison's dashes, and announce them as 'Not offered'
fstubner Sep 24, 2026
937281f
Split the comparison's detection row into UDP, versions and OS
fstubner Sep 25, 2026
bc38e57
Comparison: netscli's versions cover common services, OS gets hints
fstubner Sep 25, 2026
42f7148
Comparison: drop the JSON row, add plugins, date the table; say TCP a…
fstubner Sep 26, 2026
4675d8c
Comparison: call the extension row Add-ons, so it doesn't read as uns…
fstubner Sep 26, 2026
bf99dc5
Add Compare to the site nav
fstubner Sep 26, 2026
6ad12b9
Install: give the Linux .deb a download button, not a command chip
fstubner Sep 26, 2026
db9a94d
Install: put the .deb button on the right, level with the AppImage one
fstubner Sep 26, 2026
61b76a1
Install: the Windows installer is signed; label the .deb row, not its…
fstubner Sep 26, 2026
1ddc8c1
Install: drop the Windows installer's signing note; it is signed
fstubner Sep 26, 2026
412d953
Install: macOS 15 removed right-click Open; give the Open Anyway route
fstubner Sep 26, 2026
f6d5d0e
Install: warn Homebrew cask users too; only binaries and installers a…
fstubner Sep 26, 2026
b98bc2f
Give the landing and docs bars one set of nav spacing
fstubner Sep 27, 2026
fc88d88
Move the nav spacing tokens to their own file; tokens.css is at the s…
fstubner Sep 27, 2026
4a6246d
Centre the nav separator between FAQ and Docs
fstubner Sep 27, 2026
7b55ce9
Docs: anchors land 24px under the header, one shaded surface for head…
fstubner Sep 27, 2026
ba81774
Docs TOC bar: drop the light theme's white box behind 'On this page'
fstubner Sep 27, 2026
a62f774
Theme menu and search: focus without scrolling the page
fstubner Sep 28, 2026
4328c06
Docs header: search button sits a link-gap from Features at mid widths
fstubner Sep 28, 2026
94e063c
Docs header: search box sits a link-gap from Features on wide screens…
fstubner Sep 28, 2026
30c5692
Docs header: search joins the theme control after the links
fstubner Sep 28, 2026
8d8ad04
Docs: Copy page rides in the contents bar on narrow screens, sentence…
fstubner Sep 29, 2026
4d36228
toc-bar.css back under the size guard
fstubner Sep 29, 2026
5ba175b
toc-bar.css under the size guard
fstubner Sep 29, 2026
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
1 change: 1 addition & 0 deletions site/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export default defineConfig({
// scripts/css-regions.mjs and css-shadowing.mjs hold this shape.
customCss: [
'./src/styles/tokens.css',
'./src/styles/nav-spacing.css',
'./src/styles/docs/theme.css',
'./src/styles/docs/base.css',
'./src/styles/docs/shell.css',
Expand Down
155 changes: 155 additions & 0 deletions site/src/components/Compare.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
---
import {
compareColumns,
compareCopy,
compareNoteHtml,
compareRows,
} from '../data/site-content/compare';

const highlightIndex = compareColumns.findIndex((c) => c.highlight);
---

<section id="compare">
<div class="w">
<h2>{compareCopy.heading}</h2>
{/* Optional: netscli's table stands on its heading, and a lead that only
listed what was compared and which versions was filler. */}
{compareCopy.leadHtml && <p class="lead" set:html={compareCopy.leadHtml} />}

<!-- Focusable because it scrolls: six columns do not fit a phone, and axe
wants a scrollable region reachable by keyboard. A table of text has no
focusable descendant of its own. -->
<div class="compare-scroll" tabindex="0">
<table class="compare-table">
<thead>
<tr>
<th scope="col">Feature</th>
{compareColumns.map((c, i) => (
<th scope="col" class={`col${i === highlightIndex ? ' mine' : ''}`}>{c.name}</th>
))}
</tr>
</thead>
<tbody>
{compareRows.map((row) => (
<tr>
<th scope="row">{row.feature}</th>
{row.cells.map((cell, i) => (
<td class={`col${i === highlightIndex ? ' mine' : ''}${cell === '—' ? ' none' : ''}`}>
{cell === '—' ? (
<>
<span aria-hidden="true">—</span>
<span class="sr-only">Not offered</span>
</>
) : (
cell
)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>

{compareNoteHtml && <p class="compare-note" set:html={compareNoteHtml} />}
</div>
</section>

<style is:global>
/* ─── COMPARISON MATRIX ─── */

/* Drawn the way Supabase and Linear draw one: hairlines, no boxes, no capsules.
The compared column is outlined in the accent rather than tinted: netscli
tried the tint (nvx's version) on 2026-09-24 and found it read as a
highlighted row of cells, not as the product the table is about. */
.compare-scroll{overflow-x:auto;margin-bottom:14px}
.compare-table{
width:100%;
border-collapse:collapse;
font-size:.8125rem;
min-width:44rem;
}

.compare-table th,
.compare-table td{
text-align:left;
padding:12px 18px 12px 0;
border-bottom:1px solid rgba(var(--ui-hairline-rgb),.14);
vertical-align:top;
}
/* Headers stay on one line; answer cells wrap. netscli's answers are short
phrases ("CLI, terminal UI, desktop app, AI agents"), and unwrapped they
pushed the table past a 1024px window. */
.compare-table thead th{white-space:nowrap}
.compare-table thead th{
padding-bottom:10px;
border-bottom:1px solid rgba(var(--ui-hairline-rgb),.26);
font-size:.6875rem;font-weight:700;
letter-spacing:.07em;text-transform:uppercase;
color:var(--ui-text-muted);
}
.compare-table tbody tr:last-child th,
.compare-table tbody tr:last-child td{border-bottom:0}

/* The feature column carries the row's name and is the only wrapping cell. */
.compare-table tbody th{
font-weight:400;
color:var(--ui-text);
white-space:normal;
padding-right:28px;
min-width:11rem;
}

.compare-table th.col,
.compare-table td.col{
padding-left:18px;
border-left:1px solid rgba(var(--ui-hairline-rgb),.14);
color:var(--ui-text);
}

/* A dash is an absence, so it recedes rather than competing with the answers. */
/* A dash only marks an empty cell, so it sits well back: the ticks and the
words are what a reader scans for. It is aria-hidden and screen readers get
"Not offered" instead, so the faint glyph carries no information on its
own -- which is also why it can go below the text contrast floor. */
.compare-table td.none{color:var(--ui-text-muted)}
.compare-table td.none [aria-hidden]{opacity:.4}

/* The column's cells read in the body colour; only its header is green, in
--ui-accent-bright rather than --ui-accent, which is too dim for text on
the page ground in the dark theme. The outline is
four borders on the column's cells: border-collapse lets the left cell's
border win a tie, so its right edge is not lost to the next column's
hairline. */
.compare-table th.mine,
.compare-table td.mine{
--compare-outline:1px solid rgba(var(--ui-accent-rgb),.7);
border-left:var(--compare-outline);
border-right:var(--compare-outline);
padding-right:18px;
color:var(--ui-text);
font-weight:600;
}
.compare-table thead th.mine{border-top:var(--compare-outline);padding-top:10px}
.compare-table tbody tr:last-child td.mine{border-bottom:var(--compare-outline)}
.compare-table thead th.mine{color:var(--ui-accent-bright)}
.compare-table td.mine.none{color:var(--ui-text-muted);font-weight:400}

.compare-note{
font-size:.75rem;
line-height:1.6;
color:var(--ui-text-muted);
margin:0;
/* No max-width. The template's 62ch was sized for a one-sentence note; a
longer one wrapped into a narrow column under a full-width table. nvx
dropped it for the same reason. */
}

@media(max-width:720px){
.compare-table th,
.compare-table td{padding:10px 12px 10px 0}
.compare-table th.col,
.compare-table td.col{padding-left:12px}
.compare-table tbody th{min-width:9rem}
}
</style>
28 changes: 23 additions & 5 deletions site/src/components/Install.astro
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,21 @@ const groups = [
return (
<div class="install-group">
<h3 class="install-group-title">{title}</h3>
<div class="reco">
<div class={`reco${recommended.command ? '' : ' reco-download'}`}>
<div class="reco-meta">{recommended.label}</div>
{recommended.command ? (
<div class="cmd big has-copy always-show">{recommended.command}</div>
) : (
<a class="cmd big install-download" href={recommended.href} download>
Download {recommended.label}
/* A button, not a command chip. It was drawn in the
`.cmd big` box the other tabs use for their
recommended command, so it read as a command that
said "Download Debian / Ubuntu" -- the label above
it, repeated, in a box made for copying. The label
names the package type; the button just says
Download, like the AppImage row's, and is styled like
it (not green: see `a.install-download` below). */
<a class="install-download" href={recommended.href} download>
Download →
</a>
)}
{recommended.hint && <p class="install-hint">{recommended.hint}</p>}
Expand Down Expand Up @@ -217,6 +225,16 @@ html[data-theme='light'] #install .reco{
box-shadow:var(--ui-lift-sm);
}
.reco-meta{font-size:.85rem;font-weight:500;color:var(--ui-text);margin-bottom:8px}
/* A recommended download (Linux's .deb) lays out like the download rows
under it: label left, button on the right edge. Stacked, the button sat
under the label on the left while the AppImage button below it sat on the
right, so the two buttons in one group pointed in different directions.
16px sides match `.alt`, so labels and buttons line up down the group. */
.reco-download{
display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;
gap:8px 16px;padding-inline:16px;
}
.reco-download .reco-meta{margin-bottom:0}
.cmd.big{font-size:1.05rem;padding-right:74px}
/* The container is the divider: the rows sit on it with a 1px gap, so its
background shows through as the hairline between them. Chip tokens, so the
Expand Down Expand Up @@ -267,8 +285,8 @@ html[data-theme='light'] #install .reco{
*
* This was accent text on an accent tint inside an accent border -- the
* loudest thing in the section, on the row that is the *least* recommended
* route: a direct download is unsigned, unverified and unmanaged, while the
* package-manager rows above it check a hash. The green was pointing at the
* route: a direct download is unmanaged and nothing checks it on the way
* in, while the package-manager rows above it check a hash. The green was pointing at the
* wrong option. It reads as one route among several now, and keeps the
* accent for hover and focus so it is still obviously a control. */
a.install-download{
Expand Down
6 changes: 3 additions & 3 deletions site/src/components/Nav.astro
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ html[data-theme='light'] nav[data-site-nav]::after{
border-color:rgba(var(--ui-accent-rgb),.35);
outline:none;
}
.nlinks{display:flex;gap:20px;font-size:.8125rem;flex-wrap:wrap;justify-content:flex-end}
.nlinks{display:flex;gap:var(--ui-nav-link-gap);font-size:var(--ui-nav-font-size);flex-wrap:wrap;justify-content:flex-end}
.nlinks a{
color:var(--ui-text-muted);text-decoration:none;
display:inline-flex;align-items:center;
Expand Down Expand Up @@ -235,12 +235,12 @@ html[data-theme='light'] nav[data-site-nav]::after{
starlight/Header.astro, where the docs page shows it. */
.nlinks a.nav-group-start{
position:relative;
margin-inline-start:32px;
margin-inline-start:var(--ui-nav-group-gap);
}
.nlinks a.nav-group-start::before{
content:"";
position:absolute;
inset-inline-start:-16px;
inset-inline-start:calc((var(--ui-nav-group-gap) + var(--ui-nav-link-gap) + 1px) / -2);
top:50%;
width:1px;
height:18px;
Expand Down
120 changes: 120 additions & 0 deletions site/src/components/starlight/CopyPage.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
---
/**
* The "Copy page" button: copies this page's markdown source to the
* clipboard. Rendered twice on a docs page -- above the content on wide
* screens (MarkdownContent.astro), and at the end of the collapsed "On this
* page" bar on narrow ones (PageSidebar.astro), where it stays in reach as the
* page scrolls. CSS shows one or the other; the script wires every copy.
*/
const { entry } = Astro.locals.starlightRoute;
// 'docs/cli' -> 'cli'. The section index arrives as the bare id 'docs',
// which the strip does not match -- see the same mapping in
// src/pages/docs/[...slug].md.ts, which serves these files.
const slug = entry.id === 'docs' ? 'index' : entry.id.replace(/^docs\//, '');
const markdownHref = `/docs/${slug}.md`;
---

<button
type="button"
class="copy-page"
hidden
data-markdown-href={markdownHref}
aria-label="Copy this page as markdown"
title="Copy this page as markdown"
>
<svg aria-hidden="true" viewBox="0 0 16 16" focusable="false">
<path d="M5.5 2.5h7v9h-7z"></path>
<path d="M3.5 4.5h-1v9h7v-1"></path>
</svg>
<span class="copy-page-label">Copy page</span>
</button>

<script>
// Progressive enhancement: the button ships `hidden` and is revealed only
// once this has wired it. A visible control that does nothing is worse
// than no control.
document.querySelectorAll<HTMLButtonElement>('.copy-page').forEach((button) => {
const href = button.dataset.markdownHref;
if (!href || !navigator.clipboard) return;
button.hidden = false;

const label = button.querySelector('.copy-page-label');
const original = label?.textContent ?? 'Copy page';
let resetAt: ReturnType<typeof setTimeout> | undefined;

button.addEventListener('click', async () => {
const say = (text: string, state: string) => {
if (label) label.textContent = text;
button.dataset.state = state;
clearTimeout(resetAt);
resetAt = setTimeout(() => {
if (label) label.textContent = original;
delete button.dataset.state;
}, 2000);
};

try {
const response = await fetch(href);
if (!response.ok) throw new Error(String(response.status));
await navigator.clipboard.writeText(await response.text());
say('Copied', 'copied');
} catch {
// Say so rather than looking like it worked. A silent failure leaves
// the reader pasting whatever was on the clipboard before.
say('Failed', 'failed');
}
});
});
</script>

<style>
@layer starlight.core {
.copy-page {
display: inline-flex;
align-items: center;
gap: 0.4em;
padding: 0.35rem 0.7rem;
border: 1px solid var(--sl-color-gray-5);
border-radius: 7px;
background: transparent;
color: var(--sl-color-gray-2);
font: inherit;
font-size: 0.8125rem;
line-height: 1;
cursor: pointer;
transition:
color 140ms ease,
border-color 140ms ease,
background 140ms ease;
}

.copy-page:hover,
.copy-page:focus-visible {
color: var(--sl-color-white);
border-color: var(--sl-color-gray-4);
background: var(--sl-color-gray-6);
}

/* The site's accent, not Starlight's green -- they are different hues and
everything else here that means "good" uses --ui-accent. */
.copy-page[data-state='copied'] {
color: var(--ui-accent-bright);
border-color: rgba(var(--ui-accent-rgb), 0.48);
}

.copy-page[data-state='failed'] {
color: var(--sl-color-red-high);
border-color: var(--sl-color-red);
}

.copy-page svg {
width: 14px;
height: 14px;
fill: none;
stroke: currentColor;
stroke-width: 1.6;
stroke-linecap: round;
stroke-linejoin: round;
}
}
</style>
Loading
Loading