feat(site): phase 1 — the foundation
Some checks failed
PR checks / checks (pull_request) Failing after 4m19s
Some checks failed
PR checks / checks (pull_request) Failing after 4m19s
Astro 7 with the Node adapter, Starlight mounted at /docs, the token file, both self-hosted typefaces, the layout shell, and the two build-time checks from §12. The palette's gold and cyan are sampled from runic-emblem.png rather than guessed, per §11: 494,059 opaque pixels binned by hue, each value annotated with its measured contrast against the ground, and restricted rather than brightened where a ratio fails. - checkTokens.mjs fails the build on any colour literal outside tokens.css, which is what keeps §7's "recolouring is a file copy" promise true. - checkFacts.mjs re-reads all 14 externally-sourced facts from their authorities over the Gitea API and fails on disagreement. It also enforces D13: no email address in the source outside brand-default/brand.json. - Both were negative-tested; neither has ever been allowed to pass by default. §6 asks for output:'server' with per-page prerender=true. Astro 7 expresses the same runtime shape as output:'static' with an adapter, opting individual routes out — so the default is static rather than accidentally server-rendered. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
54
src/components/DocsSiteTitle.astro
Normal file
54
src/components/DocsSiteTitle.astro
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
import Mark from '../assets/placeholder-mark.svg?raw';
|
||||
import { brand } from '../lib/brand.mjs';
|
||||
|
||||
/**
|
||||
* Overrides Starlight's `SiteTitle` so the documentation header carries the same lockup as
|
||||
* the marketing header. One product, two chromes, one mark.
|
||||
*
|
||||
* It exists because Starlight's `logo` option renders an `<img>`, and our mark is an
|
||||
* inline-only asset: it is drawn in `currentColor` so it inherits `--gold` and follows a
|
||||
* bind-mounted `theme.css` for free (§7). An SVG loaded through `<img>` is an independent
|
||||
* document — `currentColor` has nothing to inherit from there, and the mark renders black
|
||||
* on black. Inlining it is what makes the token reach the artwork.
|
||||
*
|
||||
* Phase 2 replaces the placeholder with the real emblem derivatives; this component keeps
|
||||
* working, because what it needs is markup rather than a file.
|
||||
*/
|
||||
const { siteTitle, siteTitleHref } = Astro.locals.starlightRoute;
|
||||
---
|
||||
|
||||
<a href={siteTitleHref} class="site-title sl-flex">
|
||||
<span class="docs-mark" set:html={Mark} aria-hidden="true" />
|
||||
<span translate="no">{siteTitle || brand.siteName}</span>
|
||||
</a>
|
||||
|
||||
<style>
|
||||
/* Layout only. The brand of this element — display face, weight, letter-spacing and
|
||||
colour — is set once in src/styles/starlight.css, next to the rest of the docs
|
||||
theming, so there is one place to change it. */
|
||||
.site-title {
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
font-size: var(--sl-text-h4);
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.docs-mark {
|
||||
display: inline-flex;
|
||||
flex: none;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
color: var(--gold);
|
||||
}
|
||||
|
||||
:global(:root[data-theme='light']) .docs-mark {
|
||||
color: var(--light-gold);
|
||||
}
|
||||
|
||||
span:last-child {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
92
src/components/Footer.astro
Normal file
92
src/components/Footer.astro
Normal file
@@ -0,0 +1,92 @@
|
||||
---
|
||||
import { brand } from '../lib/brand.mjs';
|
||||
import platform from '../data/platform.json';
|
||||
|
||||
/**
|
||||
* The footer is on every page, so it is the one place that must never quote a fact from
|
||||
* memory. The version chip reads `platform.json` (§12); the contact address and the links
|
||||
* read `brand.json` (§7, D13).
|
||||
*
|
||||
* `/privacy` and `/terms` are linked from every page (§9) — those pages land in phase 6,
|
||||
* which is why they are the only two entries deliberately left out of the columns below
|
||||
* until then.
|
||||
*/
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
heading: 'Product',
|
||||
links: [
|
||||
{ href: '/features/', label: 'Features' },
|
||||
{ href: '/architecture/', label: 'Architecture' },
|
||||
{ href: '/modules/', label: 'Modules' },
|
||||
{ href: '/app/', label: 'Android app' },
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: 'Documentation',
|
||||
links: [
|
||||
{ href: '/docs/', label: 'Getting started' },
|
||||
{ href: '/docs/', label: 'Administration' },
|
||||
{ href: '/docs/', label: 'Building a module' },
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: 'Project',
|
||||
links: [
|
||||
{ href: brand.giteaOrg, label: 'Source' },
|
||||
{ href: brand.discordInvite, label: 'Discord' },
|
||||
{ href: '/community/', label: 'Community' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const isExternal = (href: string) => href.startsWith('http');
|
||||
---
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="page">
|
||||
<div class="site-footer__cols">
|
||||
{
|
||||
columns.map((column) => (
|
||||
<section>
|
||||
<h2>{column.heading}</h2>
|
||||
<ul>
|
||||
{column.links.map((link) => (
|
||||
<li>
|
||||
<a
|
||||
href={link.href}
|
||||
rel={isExternal(link.href) ? 'noopener noreferrer' : undefined}
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="site-footer__legal">
|
||||
<p>
|
||||
{brand.siteName} is free software under the{' '}
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0.html" rel="noopener noreferrer"
|
||||
>GPL-3.0-or-later</a
|
||||
>. © {year}.
|
||||
</p>
|
||||
<p class="site-footer__meta">
|
||||
<span class="chip chip--version">Protocol {platform.protocol}</span>
|
||||
<span class="chip chip--version">Bundle {platform.bundle.tag}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
.site-footer__meta {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
49
src/components/Header.astro
Normal file
49
src/components/Header.astro
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
import { brand } from '../lib/brand.mjs';
|
||||
import Mark from '../assets/placeholder-mark.svg?raw';
|
||||
|
||||
/**
|
||||
* The marketing header. The docs get Starlight's own header, themed to match in
|
||||
* `src/styles/starlight.css` — one site, two chromes, the same lockup.
|
||||
*
|
||||
* The nav names the routes §10 specifies. Phase 3 onwards fills them in; a link added
|
||||
* here before its page exists fails `checkLinks.mjs`, which is the order we want.
|
||||
*/
|
||||
const { pathname } = Astro.url;
|
||||
|
||||
const links = [
|
||||
{ href: '/features/', label: 'Features' },
|
||||
{ href: '/docs/', label: 'Docs' },
|
||||
{ href: '/app/', label: 'App' },
|
||||
{ href: '/community/', label: 'Community' },
|
||||
];
|
||||
|
||||
const isCurrent = (href: string) =>
|
||||
href === '/' ? pathname === '/' : pathname.startsWith(href);
|
||||
---
|
||||
|
||||
<header class="site-header">
|
||||
<div class="page site-header__inner">
|
||||
<a class="brand-lockup" href="/">
|
||||
<span class="brand-lockup__mark" set:html={Mark} />
|
||||
<span class="brand-lockup__name">{brand.siteName}</span>
|
||||
</a>
|
||||
|
||||
<nav class="site-nav" aria-label="Primary">
|
||||
{
|
||||
links.map((link) => (
|
||||
<a href={link.href} aria-current={isCurrent(link.href) ? 'page' : undefined}>
|
||||
{link.label}
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
.brand-lockup__mark {
|
||||
display: inline-flex;
|
||||
color: var(--gold);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user