Skip to content
Open
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
28 changes: 28 additions & 0 deletions apps/petrinaut-docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,34 @@ output, which would otherwise reach the root-hoisted `cookie@0.7.2` that
`express` pins and fail on a missing `parseCookie` export. Nesting keeps Astro on
its own `cookie@2.x` without changing hoisting for the rest of the monorepo.

## Chrome overrides

[`src/styles/chrome.css`](src/styles/chrome.css), registered as Starlight's
`customCss`, narrows both side panels to give the content column more width,
tones down the header, and rounds the corners on markdown images so the embedded
diagrams match the bordered cards beside them. The left nav takes its own
`--pnd-sidebar-width` because Starlight sizes both panels from
`--sl-sidebar-width`, and collapsing the nav has to zero one of them without
flattening the other. The collapse toggle and resize handle come from
`components.SiteTitle`, the header's leftmost slot, next to the rail they act
on β€” and the fixed header is the one piece of chrome still on screen once the
nav is gone. Both
controls remember their state in `localStorage`, restored by a `head` script so
a collapsed sidebar does not render open and then jump.

## The code font

Code renders in JetBrains Mono, requested through Astro's font support in
[`astro.config.mjs`](astro.config.mjs) and emitted by `<Font>` in
[`src/components/Head.astro`](src/components/Head.astro).

A build downloads one variable file covering weights 400 to 700, subsets it to
latin, and writes it beside the other assets, so a reader makes no request to a
font host. The head carries a `preload` link and `font-display: swap`, and Astro
generates a metric-matched fallback, which is what keeps text from shifting when
the file arrives. The downloaded originals are cached under `.astro/`, which is
ignored.

## Deployment

Vercel builds the site from [`vercel.json`](vercel.json), which runs
Expand Down
41 changes: 39 additions & 2 deletions apps/petrinaut-docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";

import react from "@astrojs/react";
import starlight from "@astrojs/starlight";
import { defineConfig } from "astro/config";
import { defineConfig, fontProviders } from "astro/config";

/**
* Renders the architecture bundle produced by `@local/petrinaut-arch-docs`.
Expand Down Expand Up @@ -213,12 +213,29 @@ export default defineConfig({
trailingSlash: "never",
build: { format: "file" },

/*
* One variable file covers every weight the code blocks use, downloaded and
* subset at build time so a reader makes no request to a font host. Italic is
* left out: no code style in the docs uses it.
*/
fonts: [
{
provider: fontProviders.fontsource(),
name: "JetBrains Mono",
cssVariable: "--pnd-font-mono",
weights: ["400 700"],
styles: ["normal"],
subsets: ["latin"],
fallbacks: ["ui-monospace", "SFMono-Regular", "Menlo", "monospace"],
},
],

integrations: [
// Generated layer pages import the bundle's facts and relations cards, and
// authored pages may import its diagram components; both are React.
react(),
starlight({
title: "Architecture Docs",
title: "Docs",
description:
"How the Petrinaut packages fit together β€” generated from annotations in the source.",
// The helmet carries the Petrinaut identity, so the title beside it names
Expand All @@ -229,6 +246,26 @@ export default defineConfig({
replacesTitle: false,
},
favicon: "/favicon.ico",
// Narrows both side panels and rounds the images, and styles the collapse
// controls that `components.SiteTitle` renders. See `chrome.css`.
customCss: ["./src/styles/chrome.css"],
components: {
Head: "./src/components/Head.astro",
SiteTitle: "./src/components/SiteTitle.astro",
},
// Restores the collapsed state before first paint. In the component's own
// script the sidebar would render at full width and then jump.
head: [
{
tag: "script",
content: `try {
const stored = localStorage.getItem("pnd:sidebar");
const width = localStorage.getItem("pnd:sidebar-width");
if (stored === "collapsed") document.documentElement.dataset.pndSidebar = stored;
if (width) document.documentElement.style.setProperty("--pnd-sidebar-open-width", width);
} catch {}`,
},
],
social: [
{
icon: "github",
Expand Down
14 changes: 14 additions & 0 deletions apps/petrinaut-docs/src/components/Head.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
/**
* Starlight's head, plus the font faces and preload links for the code font.
*
* Astro's `<Font>` emits both, so the family has to be requested from a
* component in the head rather than from configuration alone.
*/

import Default from "@astrojs/starlight/components/Head.astro";
import { Font } from "astro:assets";
---

<Default><slot /></Default>
<Font cssVariable="--pnd-font-mono" preload />
157 changes: 157 additions & 0 deletions apps/petrinaut-docs/src/components/SiteTitle.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
---
import Default from "@astrojs/starlight/components/SiteTitle.astro";

/**
* Adds the desktop sidebar collapse toggle and resize handle beside the title.
*
* They hang off `SiteTitle` because it is the header's leftmost slot, next to
* the rail's edge where the controls act, and the fixed header is the only
* chrome still on screen once the sidebar pane itself is collapsed away. Both
* are display: none unless the page has a sidebar (see `chrome.css`), so the
* splash-template 404 page does not grow controls with nothing to control.
*/
---

<button
class="pnd-sidebar-toggle"
type="button"
aria-controls="starlight__sidebar"
aria-expanded="true"
aria-label="Toggle sidebar"
>
<svg
aria-hidden="true"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linejoin="round"
>
<rect x="3" y="3" width="18" height="18" rx="2"></rect>
<path d="M9 3v18"></path>
</svg>
</button>

<Default><slot /></Default>

<div
class="pnd-sidebar-resize"
role="separator"
aria-orientation="vertical"
aria-label="Resize sidebar"
tabindex="0"
>
</div>

<script is:inline>
(() => {
const root = document.documentElement;
const toggle = /** @type {HTMLButtonElement | null} */ (
document.querySelector(".pnd-sidebar-toggle")
);
const handle = /** @type {HTMLElement | null} */ (
document.querySelector(".pnd-sidebar-resize")
);

if (!toggle || !handle) {
return;
}

/**
* @param {string} key
* @param {string} value
*/
const remember = (key, value) => {
try {
localStorage.setItem(key, value);
} catch {
// Private browsing denies storage; the control still works this session.
}
};

toggle.setAttribute(
"aria-expanded",
String(root.dataset.pndSidebar !== "collapsed"),
);

toggle.addEventListener("click", () => {
const collapsed = root.dataset.pndSidebar !== "collapsed";

if (collapsed) {
root.dataset.pndSidebar = "collapsed";
} else {
delete root.dataset.pndSidebar;
}

toggle.setAttribute("aria-expanded", String(!collapsed));
remember("pnd:sidebar", collapsed ? "collapsed" : "expanded");
});

/** @param {number} edge Where the pane's trailing edge should land, in px. */
const resizeTo = (edge) => {
const width = `${Math.round(Math.min(Math.max(edge, 224), 480))}px`;

root.style.setProperty("--pnd-sidebar-open-width", width);
remember("pnd:sidebar-width", width);
};

handle.addEventListener("pointerdown", (event) => {
handle.setPointerCapture(event.pointerId);

/** @param {PointerEvent} move */
const onMove = (move) => resizeTo(move.clientX);

handle.addEventListener("pointermove", onMove);
handle.addEventListener(
"pointerup",
() => handle.removeEventListener("pointermove", onMove),
{ once: true },
);
});
Comment thread
cursor[bot] marked this conversation as resolved.

handle.addEventListener("keydown", (event) => {
const step =
event.key === "ArrowLeft" ? -16 : event.key === "ArrowRight" ? 16 : 0;

if (step !== 0) {
event.preventDefault();
resizeTo(handle.getBoundingClientRect().left + 3 + step);
}
});

// A gradient cannot ask whether the text fits, so the labels that do not
// fit are marked here and the fade keys off the mark. Deferred because this
// script is in the header, which the browser parses before the sidebar.
const markLabels = () => {
const pane = document.querySelector(".sidebar-content");

if (!pane) {
return;
}

const labels = pane.querySelectorAll("a > span, .group-label > span");

const markClipped = () => {
for (const label of labels) {
label.toggleAttribute(
"data-pnd-clipped",
label.scrollWidth > label.clientWidth + 1,
);
}
};

markClipped();
// Fires on the pane's own resize and when expanding a group changes it.
new ResizeObserver(markClipped).observe(pane);
pane.addEventListener("toggle", markClipped, { capture: true });
};

if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", markLabels, { once: true });
} else {
markLabels();
}
})();
</script>
Loading
Loading