diff --git a/site/astro.config.mjs b/site/astro.config.mjs index fcbd6b2..33f6944 100644 --- a/site/astro.config.mjs +++ b/site/astro.config.mjs @@ -27,11 +27,19 @@ export default defineConfig({ 'Capture, frame and export showcase images of desktop and web apps for READMEs and portfolios.', favicon: '/favicon.svg', logo: { src: './src/assets/mark.svg', alt: '' }, - customCss: ['./src/styles/theme.css'], + // The noctcore theme in its load order: the token contract, the shared + // pieces, the Observatory preset (it may tune a piece), then this site's own rules. + customCss: [ + './src/styles/noctcore/base.css', + './src/styles/noctcore/components.css', + './src/styles/noctcore/presets/observatory.css', + './src/styles/site.css', + ], components: { Head: './src/components/Head.astro', Footer: './src/components/Footer.astro', SiteTitle: './src/components/SiteTitle.astro', + SocialIcons: './src/components/SocialIcons.astro', }, social: [ { icon: 'github', label: 'GitHub', href: 'https://github.com/noctcore/showcase-kit' }, diff --git a/site/bun.lock b/site/bun.lock index 80be035..b18939d 100644 --- a/site/bun.lock +++ b/site/bun.lock @@ -8,7 +8,7 @@ "@astrojs/markdown-satteri": "0.4.1", "@astrojs/starlight": "^0.42.2", "@fontsource-variable/jetbrains-mono": "5.3.0", - "@fontsource-variable/space-grotesk": "5.3.0", + "@fontsource-variable/newsreader": "5.3.0", "astro": "^7.3.3", "satteri": "0.10.5", }, @@ -160,7 +160,7 @@ "@fontsource-variable/jetbrains-mono": ["@fontsource-variable/jetbrains-mono@5.3.0", "", {}, "sha512-F32xpS2NsGYoQi2ADSkKTgpJj7ozajsGgDJ8woTnqjmIB+dxDIqImjl4pXZVEExu8UFZ2ndhmX18EBS/hdz3Lw=="], - "@fontsource-variable/space-grotesk": ["@fontsource-variable/space-grotesk@5.3.0", "", {}, "sha512-2IxmvfB08i9vnGB3Ym/AXvhRE+8XOjWMXIyDum03c+tPwH0FUoMNQfGpU8NXPxjbws0Vvss3AH0Zqt4oJBBAdw=="], + "@fontsource-variable/newsreader": ["@fontsource-variable/newsreader@5.3.0", "", {}, "sha512-rrzYi43qMpbzwuFtf9OkWH8sxAPVPcQQQEwXpPtwaKYeJ8yVg5aLs5kawmo1f2Q1t1M38TLmEKCkGVDsYwgdFw=="], "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], diff --git a/site/ec.config.mjs b/site/ec.config.mjs new file mode 100644 index 0000000..8cd9210 --- /dev/null +++ b/site/ec.config.mjs @@ -0,0 +1,9 @@ +import { defineEcConfig } from '@astrojs/starlight/expressive-code'; + +import { noctcoreCodeConfig } from './src/styles/noctcore/expressive-code.mjs'; + +// Code frames follow the Observatory preset: its dark and light code themes, +// the frame chrome on Starlight's colours, and the code area on --nc-code-bg. +export default defineEcConfig({ + ...noctcoreCodeConfig('observatory'), +}); diff --git a/site/package.json b/site/package.json index 73bae44..85cb862 100644 --- a/site/package.json +++ b/site/package.json @@ -16,7 +16,7 @@ "@astrojs/markdown-satteri": "0.4.1", "@astrojs/starlight": "^0.42.2", "@fontsource-variable/jetbrains-mono": "5.3.0", - "@fontsource-variable/space-grotesk": "5.3.0", + "@fontsource-variable/newsreader": "5.3.0", "astro": "^7.3.3", "satteri": "0.10.5" }, diff --git a/site/scripts/changelog/page.test.ts b/site/scripts/changelog/page.test.ts index 6d3fb81..fffdbf3 100644 --- a/site/scripts/changelog/page.test.ts +++ b/site/scripts/changelog/page.test.ts @@ -31,17 +31,19 @@ describe('renderChangelogPage', () => { expect(headings).toEqual(['## 1.0.0 {#v1.0.0}', '## 0.9.0 {#v0.9.0}']); }); - test('the meta line has the date, npm, the real tag and a compare link across tag formats', () => { + test('the meta line is one .nc-release-meta line: the date, npm, the real tag and a compare link across tag formats', () => { expect(page).toContain( - 'Released · ' + - '[npm](https://www.npmjs.com/package/@scope/pkg/v/1.0.0) · ' + - '[tag `v1.0.0`](https://github.com/o/r/releases/tag/v1.0.0) · ' + - '[changes since 0.9.0](https://github.com/o/r/compare/%40scope%2Fpkg%400.9.0...v1.0.0)', + '## 1.0.0 {#v1.0.0}\n\n

' + + 'Released ' + + 'npm' + + 'tag v1.0.0' + + 'changes since 0.9.0

\n', ); // The oldest release has nothing to compare with. expect(page).toContain( - 'Released · [npm](https://www.npmjs.com/package/@scope/pkg/v/0.9.0) · ' + - '[tag `@scope/pkg@0.9.0`](https://github.com/o/r/releases/tag/%40scope%2Fpkg%400.9.0)\n', + '

Released ' + + 'npm' + + 'tag @scope/pkg@0.9.0

\n', ); }); @@ -49,7 +51,7 @@ describe('renderChangelogPage', () => { const model = fixtureModel(); delete model.releases[0]!.date.tag; const untagged = renderChangelogPage(model); - expect(untagged).toContain('[npm](https://www.npmjs.com/package/@scope/pkg/v/1.0.0) · not tagged yet\n'); + expect(untagged).toContain('npmnot tagged yet

\n'); expect(untagged).not.toContain('/compare/'); }); @@ -62,7 +64,7 @@ describe('renderChangelogPage', () => { test('entries keep their links, thank everyone but the maintainer and keep follow-on lines in the item', () => { expect(page).toContain( [ - '- [#12](https://github.com/o/r/pull/12) [`abc1234`](https://github.com/o/r/commit/abc1234def) Thanks [@guest](https://github.com/guest)! Drops `old()`.', + '- [#12](https://github.com/o/r/pull/12) abc1234 Thanks [@guest](https://github.com/guest)! Drops `old()`.', '', ' Second paragraph with `a | b` and `` a`b `` and a table-like `x|y|z`.', '', @@ -73,8 +75,12 @@ describe('renderChangelogPage', () => { ' :::', ].join('\n'), ); - expect(page).toContain('- [`0123abc`](https://github.com/o/r/commit/0123abc999) A maintainer fix.\n\n :::tip[Tip]\n'); - expect(page).toContain('- [`def5678`](https://github.com/o/r/commit/def5678abc) A fix with a link but no author.\n'); + expect(page).toContain( + '- 0123abc A maintainer fix.\n\n :::tip[Tip]\n', + ); + expect(page).toContain( + '- def5678 A fix with a link but no author.\n', + ); expect(page).toContain('- First release, written by hand.\n'); expect(page).not.toContain('@maint'); }); diff --git a/site/scripts/changelog/page.ts b/site/scripts/changelog/page.ts index f329a97..64fae86 100644 --- a/site/scripts/changelog/page.ts +++ b/site/scripts/changelog/page.ts @@ -11,6 +11,7 @@ import { replaceAlerts, toAside, type Alert } from './alerts'; import type { ChangelogModel, DatedRelease } from './model'; import type { BumpType, Entry, Link } from './parse'; import { nextBump, type PendingChangeset } from './pending'; +import { escapeXml } from './xml'; export const GROUP_TITLES: Readonly> = { major: 'Major changes', @@ -40,12 +41,20 @@ export const compareUrl = (repo: string, from: string, to: string) => `https://github.com/${repo}/compare/${encodeURIComponent(from)}...${encodeURIComponent(to)}`; const link = (text: string, url: string) => `[${text}](${url})`; +/** An HTML link, for markup Markdown cannot carry: a class, or a link inside an HTML block. */ +const htmlLink = (html: string, url: string, className?: string) => + `${html}`; + +/** A commit link as the feed shows it: the short hash as code. */ +const commitCode = (commit: Link) => link(`\`${commit.label}\``, commit.url); +/** A commit link as the changelog page shows it: the theme's quiet .nc-sha. */ +const commitSha = (commit: Link) => htmlLink(escapeXml(commit.label), commit.url, 'nc-sha'); /** The first line of an entry: links, thanks for anyone but the maintainer, the summary. */ -function entryPrefix(entry: Entry, maintainer: string): string { +function entryPrefix(entry: Entry, maintainer: string, commitLink: (commit: Link) => string): string { const parts: string[] = []; if (entry.pr) parts.push(link(entry.pr.label, entry.pr.url)); - if (entry.commit) parts.push(link(`\`${entry.commit.label}\``, entry.commit.url)); + if (entry.commit) parts.push(commitLink(entry.commit)); const thanked = entry.authors.filter((author: Link) => author.label !== maintainer); if (thanked.length > 0) parts.push(`Thanks ${thanked.map((a) => link(`@${a.label}`, a.url)).join(', ')}!`); return parts.map((part) => `${part} `).join(''); @@ -53,15 +62,16 @@ function entryPrefix(entry: Entry, maintainer: string): string { /** * One entry as a Markdown list item, alerts turned into whatever `alert` - * renders. Follow-on lines are indented two spaces to stay in the item. + * renders and the commit link into whatever `commitLink` renders (the short + * hash as code by default). Follow-on lines are indented two spaces to stay in the item. */ export function entryMarkdown( entry: Entry, - options: { maintainer: string; where: string; alert: (alert: Alert) => string[] }, + options: { maintainer: string; where: string; alert: (alert: Alert) => string[]; commitLink?: (commit: Link) => string }, ): string { const body = replaceAlerts(entry.body, options.alert, options.where); const [first = '', ...rest] = body.split('\n'); - return [`- ${entryPrefix(entry, options.maintainer)}${first}`, ...rest.map((line) => (line === '' ? '' : ` ${line}`))].join( + return [`- ${entryPrefix(entry, options.maintainer, options.commitLink ?? commitCode)}${first}`, ...rest.map((line) => (line === '' ? '' : ` ${line}`))].join( '\n', ); } @@ -77,25 +87,28 @@ function groupsMarkdown( '', GROUP_LEGENDS[group.type], '', - ...group.entries.flatMap((entry) => [entryMarkdown(entry, { maintainer, where, alert: toAside }), '']), + ...group.entries.flatMap((entry) => [entryMarkdown(entry, { maintainer, where, alert: toAside, commitLink: commitSha }), '']), ]); } -/** Date, npm, tag and compare links under a version heading. */ +/** + * Date, npm, tag and compare links under a version heading, as one line (the + * theme's .nc-release-meta). An HTML block, so its links are HTML too. + */ function metaLine(model: ChangelogModel, release: DatedRelease, previous: DatedRelease | undefined): string { const parts = [ - `Released `, - link('npm', npmVersionUrl(model.packageName, release.version)), + `Released `, + htmlLink('npm', npmVersionUrl(model.packageName, release.version)), ]; const { tag } = release.date; if (tag) { - parts.push(link(`tag \`${tag}\``, tagUrl(model.repo, tag))); + parts.push(htmlLink(`tag ${escapeXml(tag)}`, tagUrl(model.repo, tag))); const from = previous?.date.tag; - if (from) parts.push(link(`changes since ${previous.version}`, compareUrl(model.repo, from, tag))); + if (from) parts.push(htmlLink(`changes since ${previous.version}`, compareUrl(model.repo, from, tag))); } else { - parts.push('not tagged yet'); + parts.push('not tagged yet'); } - return parts.join(' · '); + return `

${parts.join('')}

`; } function unreleasedMarkdown(pending: readonly PendingChangeset[], maintainer: string): string[] { diff --git a/site/scripts/theme.test.ts b/site/scripts/theme.test.ts new file mode 100644 index 0000000..1b7ba52 --- /dev/null +++ b/site/scripts/theme.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, test } from 'bun:test'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +import { SITE_DIR } from './site'; +import { + DARK_SELECTOR, + LIGHT_SELECTOR, + PRESET_FILE, + SHARED_TOKEN, + blockTokens, + presetContract, + readTheme, + tokensDeclared, + tokensRead, +} from './theme'; + +describe('theme contract', () => { + const preset = readFileSync(PRESET_FILE, 'utf8'); + const contract = presetContract(); + const dark = blockTokens(preset, DARK_SELECTOR); + const light = blockTokens(preset, LIGHT_SELECTOR); + + test('base.css and components.css read preset tokens (the contract is not empty)', () => { + expect(contract).toContain('--nc-bg'); + expect(contract).toContain('--nc-font-body'); + expect(contract).toContain('--nc-motif'); + }); + + test('the preset sets every token base.css and components.css read, in its dark block', () => { + expect(contract.filter((token) => !dark.has(token))).toEqual([]); + }); + + test('the preset sets every colour token again in its light block', () => { + const perMode = contract.filter((token) => !SHARED_TOKEN.test(token)); + expect(perMode.filter((token) => !light.has(token))).toEqual([]); + }); + + test('the preset reads no token it leaves unset', () => { + const own = new Set([...dark, ...light]); + const unset = [...tokensRead(preset)].filter((token) => !own.has(token)); + expect(unset).toEqual([]); + }); + + test("the site's own rules read only tokens the theme sets", () => { + const site = readFileSync(join(SITE_DIR, 'src', 'styles', 'site.css'), 'utf8'); + const theme = readTheme('base.css') + readTheme('components.css'); + const set = new Set([...tokensDeclared(theme), ...dark, ...light]); + expect([...tokensRead(site)].filter((token) => !set.has(token))).toEqual([]); + }); + + test('the preset names no Starlight colour variable: base.css maps them once', () => { + expect(preset).not.toMatch(/--sl-color-[\w-]+\s*:/); + }); + + test('astro.config.mjs loads the theme in its order: base, components, the preset, then the site', () => { + const config = readFileSync(join(SITE_DIR, 'astro.config.mjs'), 'utf8'); + const files = [...config.matchAll(/'\.\/src\/styles\/([^']+\.css)'/g)].map((m) => m[1]); + expect(files).toEqual(['noctcore/base.css', 'noctcore/components.css', 'noctcore/presets/observatory.css', 'site.css']); + }); +}); diff --git a/site/scripts/theme.ts b/site/scripts/theme.ts new file mode 100644 index 0000000..2396ca0 --- /dev/null +++ b/site/scripts/theme.ts @@ -0,0 +1,56 @@ +/** + * Reads the noctcore theme files (src/styles/noctcore/) for the contract test: + * which --nc-* tokens base.css and components.css read, and which ones a + * preset sets for dark and for light. + */ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +import { SITE_DIR } from './site'; + +export const THEME_DIR = join(SITE_DIR, 'src', 'styles', 'noctcore'); + +/** The preset this site ships, as in astro.config.mjs `customCss`. */ +export const PRESET_FILE = join(THEME_DIR, 'presets', 'observatory.css'); + +/** The selectors that open a preset's dark block (Starlight's default) and its light block. */ +export const DARK_SELECTOR = ':root,\n::backdrop'; +export const LIGHT_SELECTOR = ":root[data-theme='light'],\n[data-theme='light'] ::backdrop"; + +/** + * Tokens with one value for both modes (README "Type, shape and labels"): a + * preset sets them once in its dark block. Every other token is a colour or + * the motif and needs a value in each mode. + */ +export const SHARED_TOKEN = /^--nc-(?:font-|label-|radius$|measure$)/; + +export const readTheme = (name: string) => readFileSync(join(THEME_DIR, name), 'utf8'); + +const stripComments = (css: string) => css.replace(/\/\*[\s\S]*?\*\//g, ''); + +/** Every --nc-* custom property a stylesheet reads through var(). */ +export function tokensRead(css: string): Set { + return new Set([...stripComments(css).matchAll(/var\(\s*(--nc-[\w-]+)/g)].map((m) => m[1]!)); +} + +/** Every --nc-* custom property a stylesheet declares, anywhere. */ +export function tokensDeclared(css: string): Set { + return new Set([...stripComments(css).matchAll(/(--nc-[\w-]+)\s*:/g)].map((m) => m[1]!)); +} + +/** The declarations of the rule whose selector list is exactly `selector`, at the top level. */ +export function blockTokens(css: string, selector: string): Set { + const text = stripComments(css); + const start = text.indexOf(`${selector} {`); + if (start === -1) throw new Error(`theme: no "${selector.replace(/\n/g, ' ')}" block`); + const open = text.indexOf('{', start); + const close = text.indexOf('}', open); + return tokensDeclared(text.slice(open + 1, close)); +} + +/** Tokens base.css and components.css read that neither file sets itself, so a preset must. */ +export function presetContract(): string[] { + const shared = readTheme('base.css') + readTheme('components.css'); + const own = tokensDeclared(shared); + return [...tokensRead(shared)].filter((token) => !own.has(token)).sort(); +} diff --git a/site/src/components/GalleryImage.astro b/site/src/components/GalleryImage.astro index 3632abe..6935ac5 100644 --- a/site/src/components/GalleryImage.astro +++ b/site/src/components/GalleryImage.astro @@ -56,10 +56,12 @@ const facts = [ display: block; line-height: 0; } + /* The frame radius of the theme's figures (.nc-frame), so the gallery, the + splash banner and a window-frame figure share one corner. */ .gallery-figure img { width: 100%; height: auto; - border-radius: 0.5rem; + border-radius: calc(var(--nc-radius) * 1.25); } figcaption { display: flex; diff --git a/site/src/components/Head.astro b/site/src/components/Head.astro index 5c6debf..e1a84b0 100644 --- a/site/src/components/Head.astro +++ b/site/src/components/Head.astro @@ -1,9 +1,5 @@ --- import Default from '@astrojs/starlight/components/Head.astro'; -// Space Grotesk for headings and the wordmark, JetBrains Mono for code. Both -// self-hosted, so the site makes no third-party font requests. -import '@fontsource-variable/space-grotesk'; -import '@fontsource-variable/jetbrains-mono'; // The release feed (src/pages/changelog.xml.ts), so feed readers find it from any page. const feed = `${import.meta.env.BASE_URL.replace(/\/$/, '')}/changelog.xml`; diff --git a/site/src/components/ReferenceCliOptions.astro b/site/src/components/ReferenceCliOptions.astro index 51fa0c0..9eae8a0 100644 --- a/site/src/components/ReferenceCliOptions.astro +++ b/site/src/components/ReferenceCliOptions.astro @@ -23,7 +23,7 @@ function flag(option: CliOption): string { ---
- +
@@ -35,11 +35,11 @@ function flag(option: CliOption): string { { options.map((option) => ( - {!command && ( - )} - + )) } @@ -61,16 +63,6 @@ function flag(option: CliOption): string { diff --git a/site/src/components/ReferenceConfigTable.astro b/site/src/components/ReferenceConfigTable.astro index 8f88511..5156f4e 100644 --- a/site/src/components/ReferenceConfigTable.astro +++ b/site/src/components/ReferenceConfigTable.astro @@ -4,6 +4,8 @@ * src/tty/types.ts, with modes from the validator. Nothing in it is typed by * hand: see scripts/reference/config.ts. */ +import { Badge } from '@astrojs/starlight/components'; + import { configTable } from '../../scripts/reference/data'; import type { ConfigMode, ConfigRow } from '../../scripts/reference/types'; import ReferenceText from './ReferenceText.astro'; @@ -42,7 +44,7 @@ function typeText(row: ConfigRow): string {
{ table.kind === 'fields' ? ( -
Option
+ {flag(option)} + {option.exits ? 'any command line: prints, then exits without running a command' : option.global @@ -52,7 +52,9 @@ function flag(option: CliOption): string { ))} {option.description} + {option.description} +
+
@@ -54,11 +56,15 @@ function typeText(row: ConfigRow): string { {table.rows.map((row) => ( - - - - @@ -101,7 +107,7 @@ function typeText(row: ConfigRow): string {
Key
+ {row.key} - {showModes && row.modes.length === 1 && {modeLabel(row.modes)}} + {showModes && row.modes.length === 1 && ( + + + + )} + {row.link ? ( {typeText(row)} @@ -73,7 +79,7 @@ function typeText(row: ConfigRow): string { )} + {row.default?.kind === 'literal' ? ( {breakable(row.default.text).map((part, i) => ( @@ -93,7 +99,7 @@ function typeText(row: ConfigRow): string { 'none' )} +
) : ( - +
@@ -111,10 +117,10 @@ function typeText(row: ConfigRow): string { {table.forms.map((form) => ( - -
Form
+ {form.type} + {form.fields ?.filter((field) => field.description || field.default) @@ -147,16 +153,6 @@ function typeText(row: ConfigRow): string { diff --git a/site/src/components/SiteTitle.astro b/site/src/components/SiteTitle.astro index 83b8bb2..852da44 100644 --- a/site/src/components/SiteTitle.astro +++ b/site/src/components/SiteTitle.astro @@ -1,13 +1,17 @@ --- -import Default from '@astrojs/starlight/components/SiteTitle.astro'; -// The version is the root package's, read at build time, so the pill always -// names the release this build documents. It links to the changelog. -import pkg from '../../../package.json'; +/** + * The site title as the noctcore wordmark, then the product: "noctcore / + * showcase-kit". base.css draws the crescent rule under the wordmark and + * components.css colours the separator and the product. + */ +import mark from '../assets/mark.svg'; -const changelog = `${import.meta.env.BASE_URL.replace(/\/$/, '')}/changelog/`; +const { siteTitle, siteTitleHref } = Astro.locals.starlightRoute; --- - + + + noctcore + + {siteTitle} + diff --git a/site/src/components/SocialIcons.astro b/site/src/components/SocialIcons.astro new file mode 100644 index 0000000..0cc8b3d --- /dev/null +++ b/site/src/components/SocialIcons.astro @@ -0,0 +1,15 @@ +--- +/** + * The version pill before the header's social icons. The version is the root + * package's, read at build time, so the pill always names the release this + * build documents. It links to the changelog. + */ +import Default from '@astrojs/starlight/components/SocialIcons.astro'; + +import pkg from '../../../package.json'; + +const changelog = `${import.meta.env.BASE_URL.replace(/\/$/, '')}/changelog/`; +--- + +v{pkg.version}, see the changelog + diff --git a/site/src/styles/noctcore/base.css b/site/src/styles/noctcore/base.css new file mode 100644 index 0000000..e723b46 --- /dev/null +++ b/site/src/styles/noctcore/base.css @@ -0,0 +1,140 @@ +/* noctcore docs base.css + The token contract. Every preset sets the same --nc-* tokens; this file + maps them onto Starlight 0.42's --sl-* variables once, so a preset never + names a Starlight variable for colour. Starlight's own CSS sits in cascade + layers and customCss is unlayered, so these rules win without !important. + The brand constants are decorative only (the crescent rule, the hero + button, the wordmark): never body text. Load first in customCss. */ + +:root, +::backdrop { + /* Brand constants */ + --nc-violet: #8b7cff; + --nc-cyan: #4dd0e1; + --nc-night: #0b1020; + --nc-gradient: linear-gradient(90deg, var(--nc-violet), var(--nc-cyan)); + + /* Type and measure */ + --sl-font: var(--nc-font-body); + --sl-font-mono: var(--nc-font-mono); + --sl-content-width: var(--nc-measure); + + /* Starlight's grey ramp is named for dark mode; the --nc names say what each step does */ + --sl-color-white: var(--nc-ink); + --sl-color-gray-1: var(--nc-ink-2); + --sl-color-gray-2: var(--nc-text); + --sl-color-gray-3: var(--nc-muted); + --sl-color-gray-4: var(--nc-faint); + --sl-color-gray-5: var(--nc-line); + --sl-color-gray-6: var(--nc-surface); + --sl-color-gray-7: var(--nc-raised); + --sl-color-black: var(--nc-bg); + + /* Accent */ + --sl-color-accent-low: var(--nc-accent-soft); + --sl-color-accent: var(--nc-accent); + --sl-color-accent-high: var(--nc-accent-strong); + + /* Aliases Starlight switches per mode. Mapped once here, so the presets hold all the mode logic */ + --sl-color-text: var(--nc-text); + --sl-color-text-accent: var(--nc-link); + --sl-color-text-invert: var(--nc-on-link); + --sl-color-bg: var(--nc-bg); + --sl-color-bg-nav: var(--nc-surface); + --sl-color-bg-sidebar: var(--nc-surface); + --sl-color-bg-inline-code: var(--nc-code-inline); + --sl-color-bg-accent: var(--nc-link); + --sl-color-hairline-light: var(--nc-line); + --sl-color-hairline: var(--nc-line-soft); + --sl-color-hairline-shade: var(--nc-bg); + + /* Signals: Starlight hues by role (blue note, purple tip, orange caution, red danger, green success) */ + --sl-color-blue-low: var(--nc-note-bg); + --sl-color-blue: var(--nc-note-edge); + --sl-color-blue-high: var(--nc-note-ink); + --sl-color-purple-low: var(--nc-tip-bg); + --sl-color-purple: var(--nc-tip-edge); + --sl-color-purple-high: var(--nc-tip-ink); + --sl-color-orange-low: var(--nc-caution-bg); + --sl-color-orange: var(--nc-caution-edge); + --sl-color-orange-high: var(--nc-caution-ink); + --sl-color-red-low: var(--nc-danger-bg); + --sl-color-red: var(--nc-danger-edge); + --sl-color-red-high: var(--nc-danger-ink); + --sl-color-green-low: var(--nc-success-bg); + --sl-color-green: var(--nc-success-edge); + --sl-color-green-high: var(--nc-success-ink); +} + +/* Shared structure: the wordmark, the crescent rule, the hero button, radius, focus. */ +.site-title { + font-family: var(--sl-font-mono); + font-weight: 600; + letter-spacing: -0.02em; + color: var(--sl-color-white); +} + +.nc-wordmark { + position: relative; +} + +.nc-wordmark::after { + content: ''; + position: absolute; + left: 0; + bottom: -0.3rem; + width: 1.75rem; + height: 2px; + border-radius: 2px; + background: var(--nc-gradient); +} + +h1#_top, +.hero h1, +.sl-markdown-content h2, +.sl-markdown-content h3 { + font-family: var(--nc-font-display); +} + +.hero h1::after { + content: ''; + display: block; + width: 6rem; + height: 0.25rem; + margin-top: 1rem; + border-radius: 999px; + background: var(--nc-gradient); +} + +@media (max-width: 49.99rem) { + .hero h1::after { + margin-inline: auto; + } +} + +.hero .sl-link-button.primary { + background: var(--nc-gradient); + border-color: transparent; + color: var(--nc-night); +} + +.starlight-aside, +.sl-link-card, +.card, +.pagination-links a { + border-radius: var(--nc-radius); +} + +.expressive-code { + --ec-brdRad: var(--nc-radius); +} + +:focus-visible { + outline: 2px solid var(--sl-color-accent); + outline-offset: 2px; +} + +.expressive-code pre, +code { + font-variant-ligatures: none; +} diff --git a/site/src/styles/noctcore/components.css b/site/src/styles/noctcore/components.css new file mode 100644 index 0000000..c7cc6d3 --- /dev/null +++ b/site/src/styles/noctcore/components.css @@ -0,0 +1,787 @@ +/* noctcore docs components.css + The site pieces the noctcore docs sites share. Every colour comes from + Starlight's --sl-* variables or a preset's --nc-* tokens, so each piece + follows both presets in dark and light. No bare element selectors: it + only touches the classes below and Starlight's own component classes. + + Load order in customCss: base.css, components.css, presets/.css, + then the site's own CSS. A preset comes after this file so it can tune a + component (Observatory draws link cards as ruled rows). + Markup for each piece is in COMPONENTS.md. */ + +/* --------------------------------------------------------------------------- + Labels. The small line over facts, stats and stacked table cells. Presets + set the look through --nc-label-* (mono capitals for the atlas presets). +--------------------------------------------------------------------------- */ +.nc-label, +.nc-facts dt, +.nc-stats dt, +.package-card-version { + font-family: var(--nc-label-font); + font-size: var(--nc-label-size); + font-weight: var(--nc-label-weight); + letter-spacing: var(--nc-label-tracking); + text-transform: var(--nc-label-case); + line-height: 1.3; + color: var(--sl-color-gray-3); +} + +/* A page's opening paragraph: the showcase-kit lead, and the rule summary that + sync.ts writes where the doc has its "> summary" blockquote. */ +.sl-markdown-content .nc-lead, +.nc-lead { + font-size: var(--sl-text-lg); + line-height: 1.6; + color: var(--sl-color-gray-2); + text-wrap: pretty; +} + +/* Starlight styles inline code only inside the Markdown body; a lead can sit above it. */ +.nc-lead code { + padding: 0.125rem 0.375rem; + background-color: var(--sl-color-bg-inline-code); + font-family: var(--__sl-font-mono); + font-size: 0.875em; +} + +/* --------------------------------------------------------------------------- + Site title: the wordmark (its crescent rule is in base.css), then the + product. Markup in COMPONENTS.md (a SiteTitle override). +--------------------------------------------------------------------------- */ +.site-title .nc-sep { + color: var(--sl-color-gray-4); + font-weight: 400; +} + +.site-title .nc-product { + overflow: hidden; + color: var(--sl-color-gray-2); + font-weight: 500; + text-overflow: ellipsis; +} + +/* A PageTitle override drops Starlight's scoped h1 styles; these are the same + values, at zero specificity so a preset can still restyle the title. */ +:where(.sl-container > h1#_top) { + margin-top: 1rem; + font-size: var(--sl-text-h1); + line-height: var(--sl-line-height-headings); + font-weight: 600; + color: var(--sl-color-white); +} + +/* --------------------------------------------------------------------------- + Version pill (showcase-kit header, links to the changelog). +--------------------------------------------------------------------------- */ +.nc-version { + display: inline-flex; + align-items: center; + height: 1.6rem; + padding-inline: 0.55rem; + border-radius: calc(var(--nc-radius) * 2 + 0.25rem); + font: 600 0.75rem/1 var(--__sl-font-mono); + color: var(--sl-color-text-accent); + background: var(--sl-color-accent-low); + text-decoration: none; + white-space: nowrap; +} + +.nc-version:hover { + color: var(--sl-color-white); +} + +/* --------------------------------------------------------------------------- + Window-frame figure: a raw capture in the frame showcase-kit draws, on the + preset's --nc-motif backdrop. Sizes are relative to the frame's width, so it + scales like the rendered image. Set --nc-win on .nc-window when a window is + narrower than its frame (the hero stack uses 74). +--------------------------------------------------------------------------- */ +.nc-shot { + margin: 0; +} + +.nc-shot figcaption { + margin-top: 0.75rem; + font-size: var(--sl-text-sm); + line-height: 1.55; + color: var(--sl-color-gray-3); +} + +.nc-frame { + container-type: inline-size; + padding: 4.5%; + border-radius: calc(var(--nc-radius) * 1.25); + background: var(--nc-motif); +} + +.nc-window { + --nc-u: calc(var(--nc-win, 100) * 0.01cqi); + position: relative; + overflow: hidden; + border-radius: calc(var(--nc-u) * 0.97); + background: #0f1017; + box-shadow: + 0 calc(var(--nc-u) * 2.4) calc(var(--nc-u) * 5) calc(var(--nc-u) * -1) rgba(2, 4, 12, 0.65), + 0 0 0 1px rgba(255, 255, 255, 0.07); +} + +.nc-window > img { + display: block; + width: 100%; + height: auto; +} + +/* The title bar showcase-kit draws with frame.theme 'dark'. */ +.nc-titlebar { + position: relative; + display: flex; + align-items: center; + justify-content: center; + height: calc(var(--nc-u) * 2.78); + background: #1b1c26; + border-bottom: 1px solid #0a0a10; +} + +.nc-lights { + position: absolute; + top: 50%; + left: var(--nc-u); + display: flex; + gap: calc(var(--nc-u) * 0.55); + transform: translateY(-50%); +} + +.nc-lights i { + width: calc(var(--nc-u) * 0.85); + height: calc(var(--nc-u) * 0.85); + border-radius: 50%; + background: #ff5f57; +} + +.nc-lights i:nth-child(2) { + background: #febc2e; +} + +.nc-lights i:nth-child(3) { + background: #28c840; +} + +.nc-wtitle { + font: 500 max(calc(var(--nc-u) * 0.95), 5px)/1 var(--sl-font-system, system-ui, sans-serif); + color: #a3a6b8; +} + +/* The hero stack: what `showcase hero` composes, minus its text. */ +.nc-banner { + position: relative; + container-type: inline-size; + width: 100%; + max-width: 34rem; + aspect-ratio: 5 / 4; + margin-inline: auto; + overflow: hidden; + border-radius: calc(var(--nc-radius) * 1.5); + background: var(--nc-motif); +} + +.nc-banner .nc-window { + --nc-win: 74; + position: absolute; + width: 74%; +} + +.nc-banner .nc-window:first-child { + top: 8%; + left: 4%; + transform: rotate(-5deg); +} + +.nc-banner .nc-window:last-child { + right: 3%; + bottom: 9%; + transform: rotate(3.5deg); +} + +/* --------------------------------------------------------------------------- + Cards. Starlight's LinkCard and Card, and the ESLint package cards, share one + surface: --nc-card-bg and --nc-card-border from the preset. +--------------------------------------------------------------------------- */ +.sl-link-card, +.card { + background-color: var(--nc-card-bg); + border-color: var(--nc-card-border); +} + +.package-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(15.5rem, 1fr)); + gap: 0.75rem; + margin: 0; + padding: 0; + list-style: none; +} + +.package-grid .package-card { + margin: 0; +} + +.package-card a { + position: relative; + display: flex; + flex-direction: column; + gap: 0.45rem; + height: 100%; + padding: 1rem 1.1rem 0.95rem; + overflow: hidden; + border: 1px solid var(--nc-card-border); + border-radius: var(--nc-radius); + background: var(--nc-card-bg); + color: var(--sl-color-gray-2); + text-decoration: none; +} + +.package-card a::before { + content: ''; + position: absolute; + inset: 0 0 auto; + height: 2px; + background: var(--nc-gradient); + opacity: 0; +} + +.package-card a:hover, +.package-card a:focus-visible { + border-color: var(--sl-color-accent); +} + +.package-card a:hover::before, +.package-card a:focus-visible::before { + opacity: 1; +} + +.package-card-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 0.75rem; +} + +.package-card-short { + font-family: var(--nc-font-display); + font-size: var(--sl-text-lg); + font-weight: 600; + line-height: 1.2; + color: var(--sl-color-white); +} + +.package-card-name { + font: 500 var(--sl-text-2xs)/1.4 var(--__sl-font-mono); + color: var(--sl-color-gray-3); + overflow-wrap: anywhere; +} + +.package-card-desc { + font-size: var(--sl-text-sm); + line-height: 1.5; +} + +.package-card-meta { + font-size: var(--sl-text-xs); + font-variant-numeric: tabular-nums; + color: var(--sl-color-gray-3); +} + +/* How a package's rules split across its preset: on, registered off, not listed. */ +.nc-split { + display: flex; + gap: 2px; + height: 0.3125rem; + margin-top: auto; + overflow: hidden; + border-radius: 999px; +} + +.nc-split i { + min-width: 3px; + background: var(--sl-color-gray-5); +} + +.nc-split .on { + background: var(--sl-color-green); +} + +.nc-split .off { + background: var(--sl-color-gray-4); +} + +.nc-split .harness { + background: var(--sl-color-accent); +} + +/* --------------------------------------------------------------------------- + Badges. Rule state, as scannable chips: the preset severity, autofix, + suggestions, required options, type information, deprecated. The .pill + classes the ESLint site's tables already emit get the same look. +--------------------------------------------------------------------------- */ +:is(.nc-badge, .pill) { + --nc-badge-bg: transparent; + --nc-badge-ink: var(--sl-color-gray-2); + --nc-badge-edge: var(--sl-color-gray-4); + display: inline-flex; + align-items: center; + gap: 0.3em; + padding: 0.0625rem 0.45rem; + border: 1px solid color-mix(in srgb, var(--nc-badge-edge) 55%, transparent); + border-radius: calc(var(--nc-radius) * 0.5 + 0.1875rem); + background: var(--nc-badge-bg); + color: var(--nc-badge-ink); + font: 600 var(--sl-text-2xs)/1.5 var(--__sl-font-mono); + white-space: nowrap; +} + +:is(.nc-badge--on, .pill-error) { + --nc-badge-bg: var(--sl-color-green-low); + --nc-badge-ink: var(--sl-color-green-high); + --nc-badge-edge: var(--sl-color-green); +} + +:is(.nc-badge--off, .pill-off) { + --nc-badge-ink: var(--sl-color-gray-2); + --nc-badge-edge: var(--sl-color-gray-4); +} + +:is(.nc-badge--out, .pill-not-listed) { + --nc-badge-ink: var(--sl-color-gray-3); + border-style: dashed; +} + +:is(.nc-badge--fix) { + --nc-badge-bg: var(--sl-color-purple-low); + --nc-badge-ink: var(--sl-color-purple-high); + --nc-badge-edge: var(--sl-color-purple); +} + +:is(.nc-badge--suggest) { + --nc-badge-bg: var(--sl-color-blue-low); + --nc-badge-ink: var(--sl-color-blue-high); + --nc-badge-edge: var(--sl-color-blue); +} + +:is(.nc-badge--options) { + --nc-badge-bg: var(--sl-color-orange-low); + --nc-badge-ink: var(--sl-color-orange-high); + --nc-badge-edge: var(--sl-color-orange); +} + +:is(.nc-badge--types) { + --nc-badge-bg: var(--sl-color-blue-low); + --nc-badge-ink: var(--sl-color-blue-high); + --nc-badge-edge: var(--sl-color-blue); +} + +:is(.nc-badge--harness, .pill-harness) { + --nc-badge-bg: var(--sl-color-accent-low); + --nc-badge-ink: var(--sl-color-text-accent); + --nc-badge-edge: var(--sl-color-accent); +} + +:is(.nc-badge--deprecated, .pill-deprecated) { + --nc-badge-bg: var(--sl-color-red-low); + --nc-badge-ink: var(--sl-color-red-high); + --nc-badge-edge: var(--sl-color-red); +} + +/* "opt-in" beside an off or not-listed preset: the rule is yours to turn on. */ +.nc-optin { + margin-inline-start: 0.35rem; + font-size: var(--sl-text-2xs); + color: var(--sl-color-gray-3); + white-space: nowrap; +} + +.deprecated-note { + display: block; + margin-top: 0.25rem; + font-size: var(--sl-text-xs); +} + +/* --------------------------------------------------------------------------- + Rule facts: the strip under a rule's title. sync.ts writes it in place of + the "Recommended preset: ... · Autofix: ..." line. Same six cells on every + rule page, so the eye finds a fact in the same place each time. +--------------------------------------------------------------------------- */ +.sl-markdown-content .nc-facts, +.nc-facts { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(7.25rem, 1fr)); + margin-inline: 0; + border-block: 1px solid var(--sl-color-hairline-light); +} + +.nc-facts > div { + display: grid; + align-content: start; + gap: 0.3rem; + padding: 0.7rem 0.9rem 0.8rem 0; +} + +.nc-facts dt { + margin: 0; +} + +.nc-facts dd { + margin: 0; + padding: 0; + font-size: var(--sl-text-sm); + line-height: 1.4; + color: var(--sl-color-white); +} + +.nc-facts dd.is-no { + color: var(--sl-color-gray-3); +} + +.nc-facts dd code { + font-size: var(--sl-text-xs); +} + +.nc-facts-note { + margin-inline-start: 0.35rem; + white-space: nowrap; + font: 500 var(--sl-text-2xs) var(--__sl-font-mono); + color: var(--sl-color-gray-3); +} + +/* A rule title reads as namespace plus name; the PageTitle override splits it. */ +.nc-title-ns { + display: block; + margin-bottom: 0.2em; + font: 500 max(0.875rem, 0.36em)/1.3 var(--__sl-font-mono); + letter-spacing: 0; + color: var(--sl-color-gray-3); +} + +/* --------------------------------------------------------------------------- + Incorrect and Correct examples. ec.config.mjs puts .verdict-bad or + .verdict-good on the frame. Each frame restyles itself by setting Expressive + Code's own variables: a tinted title bar, a coloured frame edge, a verdict + mark before the title. Three signals, never colour alone. +--------------------------------------------------------------------------- */ +.expressive-code .verdict-bad { + --nc-verdict: var(--sl-color-red); + --nc-verdict-bg: var(--sl-color-red-low); + --nc-verdict-ink: var(--sl-color-red-high); + --nc-verdict-mark: '\2715'; +} + +.expressive-code .verdict-good { + --nc-verdict: var(--sl-color-green); + --nc-verdict-bg: var(--sl-color-green-low); + --nc-verdict-ink: var(--sl-color-green-high); + --nc-verdict-mark: '\2713'; +} + +.expressive-code :is(.verdict-bad, .verdict-good) { + --ec-brdCol: color-mix(in srgb, var(--nc-verdict) 55%, transparent); + --ec-frm-edTabBarBg: var(--nc-verdict-bg); + --ec-frm-edTabBarBrdBtmCol: color-mix(in srgb, var(--nc-verdict) 55%, transparent); + --ec-frm-edActTabBg: transparent; + --ec-frm-edActTabFg: var(--nc-verdict-ink); + --ec-frm-edActTabIndTopCol: transparent; +} + +.expressive-code :is(.verdict-bad, .verdict-good) .header .title { + display: inline-flex; + align-items: center; + gap: 0.55rem; + font-weight: 600; +} + +.expressive-code :is(.verdict-bad, .verdict-good) .header .title::before { + content: var(--nc-verdict-mark); + display: inline-grid; + place-items: center; + width: 1.2rem; + height: 1.2rem; + border-radius: 50%; + background: var(--nc-verdict); + color: var(--sl-color-black); + font-size: 0.6875rem; + font-weight: 800; +} + +.expressive-code :is(.verdict-bad, .verdict-good) pre { + box-shadow: inset 3px 0 0 var(--nc-verdict); +} + +.expressive-code :is(.verdict-bad, .verdict-good) + * { + margin-top: 0.75rem; +} + +/* --------------------------------------------------------------------------- + Tables. The ESLint rule tables and the showcase-kit config reference. Wide + screens get a dense table; below 40rem each row becomes a stacked entry + (name, text, then the badges), labelled from data-label on each cell. +--------------------------------------------------------------------------- */ +.rule-table-wrap { + overflow-x: auto; +} + +.sl-markdown-content :is(.rule-table, .nc-ref) { + display: table; + width: 100%; + font-size: var(--sl-text-sm); + border-collapse: collapse; +} + +.sl-markdown-content :is(.rule-table, .nc-ref) :is(th, td) { + vertical-align: top; +} + +.sl-markdown-content :is(.rule-table, .nc-ref) thead th { + font-family: var(--nc-label-font); + font-size: var(--nc-label-size); + font-weight: var(--nc-label-weight); + letter-spacing: var(--nc-label-tracking); + text-transform: var(--nc-label-case); + color: var(--sl-color-gray-3); + white-space: nowrap; +} + +/* The all-rules index has no table of contents, so it takes a wider column. */ +main:has(.rule-table-all) { + --sl-content-width: 68rem; +} + +.sl-markdown-content .rule-table td:first-child { + width: 30%; +} + +.sl-markdown-content .rule-table td:first-child code { + overflow-wrap: break-word; + background: none; + padding: 0; + font-weight: 600; +} + +/* A rule id as two lines: the namespace, quiet, over the rule's name. Both stay + in one code element, so find-in-page still matches the full id. */ +.nc-id-ns { + display: block; + font-size: 0.85em; + font-weight: 400; + color: var(--sl-color-gray-3); +} + +/* A package's rows start with a group row: the package name and its count. */ +.sl-markdown-content .rule-table .nc-group th { + padding-top: 1.4rem; + border-bottom: 1px solid var(--sl-color-gray-4); + font-family: var(--nc-font-display); + font-size: var(--sl-text-base); + font-weight: 600; + color: var(--sl-color-white); + text-align: start; +} + +.nc-group-count { + margin-inline-start: 0.5rem; + font: 500 var(--sl-text-2xs) var(--__sl-font-mono); + color: var(--sl-color-gray-3); +} + +.sl-markdown-content .nc-ref td:first-child, +.sl-markdown-content .nc-ref td:last-child { + white-space: nowrap; +} + +.sl-markdown-content .nc-ref .sl-badge + .sl-badge { + margin-inline-start: 0.25rem; +} + +@media (max-width: 40rem) { + .rule-table-wrap { + overflow: visible; + } + + .sl-markdown-content :is(.rule-table, .nc-ref), + .sl-markdown-content :is(.rule-table, .nc-ref) :is(tbody, tr) { + display: block; + } + + .sl-markdown-content :is(.rule-table, .nc-ref) thead { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); + } + + .sl-markdown-content :is(.rule-table, .nc-ref) tbody tr { + padding-block: 0.8rem; + border-bottom: 1px solid var(--sl-color-gray-5); + } + + .sl-markdown-content :is(.rule-table, .nc-ref) td { + display: block; + width: auto !important; + padding: 0; + border: 0; + white-space: normal; + } + + .sl-markdown-content :is(.rule-table, .nc-ref) td + td { + margin-top: 0.3rem; + } + + .sl-markdown-content :is(.rule-table, .nc-ref) td:nth-child(n + 3) { + display: inline-flex; + align-items: center; + gap: 0.35rem; + margin: 0.5rem 0.9rem 0 0; + } + + .sl-markdown-content :is(.rule-table, .nc-ref) td:nth-child(n + 3):empty { + display: none; + } + + .sl-markdown-content :is(.rule-table, .nc-ref) td[data-label]:nth-child(n + 3)::before { + content: attr(data-label); + font-family: var(--nc-label-font); + font-size: var(--nc-label-size); + font-weight: var(--nc-label-weight); + letter-spacing: var(--nc-label-tracking); + text-transform: var(--nc-label-case); + color: var(--sl-color-gray-3); + } + + .sl-markdown-content .rule-table .nc-group { + display: block; + border: 0; + } + + .sl-markdown-content .rule-table .nc-group th { + display: block; + padding-inline: 0; + } +} + +.rule-table-legend, +.quickstart-note, +.site-footer-note { + font-size: var(--sl-text-sm); + color: var(--sl-color-gray-3); +} + +/* Summary numbers over the all-rules index. */ +.sl-markdown-content .nc-stats, +.nc-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr)); + gap: 1px; + margin-inline: 0; + overflow: hidden; + border: 1px solid var(--nc-card-border); + border-radius: var(--nc-radius); + background: var(--nc-card-border); +} + +.nc-stats > div { + display: grid; + gap: 0.15rem; + padding: 0.75rem 0.9rem; + background: var(--nc-card-bg); +} + +.nc-stats dd { + order: -1; + margin: 0; + font-family: var(--nc-font-display); + font-size: var(--sl-text-2xl); + font-weight: 600; + line-height: 1.1; + font-variant-numeric: tabular-nums; + color: var(--sl-color-white); +} + +/* --------------------------------------------------------------------------- + Tabs with many entries (the twelve quick-start tabs) wrap into a row of + chips instead of scrolling sideways past the fold. +--------------------------------------------------------------------------- */ +starlight-tabs:has(.tab:nth-child(6)) [role='tablist'] { + flex-wrap: wrap; + gap: 0.35rem; + padding-bottom: 0.75rem; + border-bottom: 1px solid var(--sl-color-hairline-light); +} + +starlight-tabs:has(.tab:nth-child(6)) .tab { + margin-bottom: 0; +} + +starlight-tabs:has(.tab:nth-child(6)) .tab > [role='tab'] { + padding: 0.25rem 0.7rem; + border: 1px solid var(--sl-color-gray-5); + border-radius: calc(var(--nc-radius) * 2 + 0.25rem); + font-family: var(--__sl-font-mono); + font-size: var(--sl-text-xs); +} + +starlight-tabs:has(.tab:nth-child(6)) .tab > [role='tab'][aria-selected='true'] { + border-color: var(--sl-color-text-accent); + background: var(--sl-color-accent-low); + color: var(--sl-color-white); +} + +/* --------------------------------------------------------------------------- + Who a site is for: the two landing-page lists side by side. +--------------------------------------------------------------------------- */ +.sl-markdown-content .nc-fit, +.nc-fit { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); + gap: 0.75rem; +} + +.nc-fit > div { + padding: 1rem 1.1rem; + border: 1px solid var(--nc-card-border); + border-top: 2px solid var(--sl-color-green); + border-radius: var(--nc-radius); + background: var(--nc-card-bg); + font-size: var(--sl-text-sm); +} + +.nc-fit > div + div { + border-top-color: var(--sl-color-orange); +} + +.nc-fit > div > p + p { + margin-top: 0.5rem; +} + +/* --------------------------------------------------------------------------- + Changelog: a release's version and date as one line. +--------------------------------------------------------------------------- */ +.sl-markdown-content .nc-release-meta, +.nc-release-meta { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 0.25rem 0.75rem; + font-family: var(--__sl-font-mono); + font-size: var(--sl-text-sm); + color: var(--sl-color-gray-3); +} + +.nc-release-meta b { + font-size: var(--sl-text-lg); + font-weight: 600; + color: var(--sl-color-white); +} + +.nc-sha { + font-family: var(--__sl-font-mono); + font-size: var(--sl-text-xs); + white-space: nowrap; +} diff --git a/site/src/styles/noctcore/expressive-code.mjs b/site/src/styles/noctcore/expressive-code.mjs new file mode 100644 index 0000000..6d38683 --- /dev/null +++ b/site/src/styles/noctcore/expressive-code.mjs @@ -0,0 +1,102 @@ +/* noctcore docs: Expressive Code settings for the Observatory preset. + + // ec.config.mjs + import { defineEcConfig } from '@astrojs/starlight/expressive-code'; + import { noctcoreCodeConfig } from './src/styles/noctcore/expressive-code.mjs'; + + export default defineEcConfig({ + ...noctcoreCodeConfig('observatory'), + plugins: [], + }); + + What noctcoreCodeConfig sets, and why: + - themes: a dark and a light theme built from the preset's --nc-code-ink and + --nc-syn-* values. Starlight switches between them with its own theme + picker, because one is type 'dark' and the other 'light'. + - useStarlightUiThemeColors: true, so title bars, tabs and borders follow the + preset through Starlight's --sl-* variables. Starlight turns this off by + default once a site passes its own themes. + - customizeTheme: puts the code area on the preset's --nc-code-bg. Starlight + would otherwise use --sl-color-gray-6 in dark mode and -7 in light. + - minSyntaxHighlightingColorContrast: 0. By default Expressive Code nudges + token colours toward 5.5:1 against a fixed grey it uses for that sum, not + against the real code background. These colours are checked against the + real background instead: 4.5:1 or more, see contrast.md. + Keep these values in step with presets/.css when either changes. */ +import { ExpressiveCodeTheme } from '@astrojs/starlight/expressive-code'; + +const SYNTAX = { + "observatory": { + "dark": { + "bg": "#0f1526", + "ink": "#d9deeb", + "keyword": "#c1b8ff", + "string": "#92e0e9", + "number": "#f0c98f", + "function": "#a6e6ee", + "property": "#dfe4f0", + "comment": "#8a93ad", + "punct": "#97a0b8" + }, + "light": { + "bg": "#ffffff", + "ink": "#1e2745", + "keyword": "#4a36c4", + "string": "#0a6873", + "number": "#8a4b00", + "function": "#0c4e58", + "property": "#1e2745", + "comment": "#5c6682", + "punct": "#58617c" + } + } +}; + +/* TextMate scopes per token role, for the TypeScript, JavaScript, shell and + JSON grammars the docs use. */ +const SCOPES = [ + ['comment', ['comment', 'punctuation.definition.comment'], 'italic'], + ['keyword', ['keyword', 'storage', 'storage.type', 'storage.modifier', 'keyword.control', 'constant.language', 'variable.language']], + ['string', ['string', 'string.template', 'punctuation.definition.string', 'punctuation.definition.template-expression']], + ['number', ['constant.numeric', 'constant.other']], + ['function', ['entity.name.function', 'support.function', 'meta.function-call entity.name.function', 'entity.name.type', 'support.type', 'support.class', 'entity.name.class', 'entity.name.command']], + ['property', ['variable.other.property', 'meta.object-literal.key', 'support.type.property-name', 'entity.other.attribute-name']], + ['punct', ['punctuation', 'meta.brace', 'keyword.operator']], +]; + +function theme(preset, type) { + const c = SYNTAX[preset][type]; + return new ExpressiveCodeTheme({ + name: `noctcore-${preset}-${type}`, + type, + colors: { 'editor.background': c.bg, 'editor.foreground': c.ink }, + tokenColors: SCOPES.map(([key, scope, fontStyle]) => ({ + scope, + settings: fontStyle ? { foreground: c[key], fontStyle } : { foreground: c[key] }, + })), + }); +} + +/** The dark and light themes for one preset. */ +export function noctcoreCodeThemes(preset = 'observatory') { + if (!SYNTAX[preset]) throw new Error(`Unknown noctcore preset "${preset}". Use one of: ${Object.keys(SYNTAX).join(', ')}.`); + return [theme(preset, 'dark'), theme(preset, 'light')]; +} + +/** Everything ec.config.mjs needs for one preset; spread it and add the site's plugins. */ +export function noctcoreCodeConfig(preset = 'observatory') { + return { + themes: noctcoreCodeThemes(preset), + useStarlightUiThemeColors: true, + minSyntaxHighlightingColorContrast: 0, + customizeTheme(theme) { + theme.styleOverrides.frames = { + ...theme.styleOverrides.frames, + editorBackground: 'var(--nc-code-bg)', + terminalBackground: 'var(--nc-code-bg)', + editorActiveTabBackground: 'var(--nc-code-bg)', + }; + return theme; + }, + }; +} diff --git a/site/src/styles/noctcore/presets/observatory.css b/site/src/styles/noctcore/presets/observatory.css new file mode 100644 index 0000000..8d71a34 --- /dev/null +++ b/site/src/styles/noctcore/presets/observatory.css @@ -0,0 +1,249 @@ +/* noctcore docs preset: Observatory + An editorial star atlas: a serif with optical sizes, graticule hairlines and one calm column, for guides you read top to bottom beside framed screenshots. + Type: Newsreader for headings, Newsreader for text, JetBrains Mono for code. + Needs: @fontsource-variable/newsreader, @fontsource-variable/jetbrains-mono. + Dark is the default, as in Starlight: :root holds the dark values and + :root[data-theme='light'] overrides them. Load after base.css and + components.css. Contrast: every text pair passes WCAG AA, see contrast.md. */ +@import '@fontsource-variable/newsreader/opsz.css'; +@import '@fontsource-variable/newsreader/opsz-italic.css'; +@import '@fontsource-variable/jetbrains-mono'; + +:root, +::backdrop { + /* Type, shape and labels */ + --nc-font-display: 'Newsreader Variable', 'Newsreader', ui-serif, Georgia, serif; + --nc-font-body: 'Newsreader Variable', 'Newsreader', ui-serif, Georgia, serif; + --nc-font-mono: 'JetBrains Mono Variable', 'JetBrains Mono', ui-monospace, 'SFMono-Regular', Menlo, monospace; + --nc-label-font: var(--nc-font-mono); + --nc-label-case: uppercase; + --nc-label-tracking: 0.12em; + --nc-label-size: 0.6875rem; + --nc-label-weight: 500; + --nc-radius: 0.125rem; + --nc-measure: 42rem; + --sl-text-body: 1.0625rem; + --sl-line-height: 1.7; + --sl-text-h1: clamp(2.5rem, 1.75rem + 2.5vw, 3.25rem); + --sl-text-h2: clamp(1.625rem, 1.25rem + 1.25vw, 2rem); + + /* Surfaces */ + --nc-bg: #0a0f1d; + --nc-surface: #0a0f1d; + --nc-raised: #0f1526; + --nc-line: #242d47; + --nc-line-soft: #161d33; + + /* Ink */ + --nc-ink: #f1f3fa; + --nc-ink-2: #dfe4f0; + --nc-text: #c3cadb; + --nc-muted: #97a0b8; + --nc-faint: #56607d; + + /* Accent */ + --nc-link: #8fdde8; + --nc-on-link: #0a0f1d; + --nc-accent: #a79cff; + --nc-accent-soft: #171a38; + --nc-accent-strong: #8fdde8; + + /* Signals: Starlight asides and badges, the ESLint verdict frames and rule badges */ + --nc-note-bg: #0d1a28; + --nc-note-edge: #4dd0e1; + --nc-note-ink: #a6e6ee; + --nc-tip-bg: #15142f; + --nc-tip-edge: #8b7cff; + --nc-tip-ink: #cdc6ff; + --nc-caution-bg: #1f1a10; + --nc-caution-edge: #e2b04e; + --nc-caution-ink: #f1d49a; + --nc-danger-bg: #231219; + --nc-danger-edge: #f07a7a; + --nc-danger-ink: #f7bcbc; + --nc-success-bg: #0e1f1a; + --nc-success-edge: #56d68f; + --nc-success-ink: #a6e9c3; + + /* Components: cards and package cards */ + --nc-card-bg: #0a0f1d; + --nc-card-border: #242d47; + + /* Code: frames read these; the Expressive Code theme carries the syn-* values */ + --nc-code-inline: #161d33; + --nc-code-bg: #0f1526; + --nc-code-ink: #d9deeb; + --nc-syn-keyword: #c1b8ff; + --nc-syn-string: #92e0e9; + --nc-syn-number: #f0c98f; + --nc-syn-function: #a6e6ee; + --nc-syn-property: #dfe4f0; + --nc-syn-comment: #8a93ad; + --nc-syn-punct: #97a0b8; + + /* Motif: the window-frame backdrop */ + --nc-motif: + radial-gradient(circle at 23% 41%, rgba(241, 243, 250, 0.85) 0 1px, transparent 1.6px) 13px 29px / 113px 97px, + radial-gradient(circle at 71% 13%, rgba(143, 221, 232, 0.7) 0 1px, transparent 1.6px) 41px 7px / 167px 139px, + radial-gradient(circle at 47% 83%, rgba(241, 243, 250, 0.55) 0 0.8px, transparent 1.3px) 5px 53px / 229px 181px, + radial-gradient(circle at 88% 64%, rgba(167, 156, 255, 0.6) 0 0.9px, transparent 1.4px) 71px 23px / 191px 157px, + linear-gradient(160deg, #1b1f4a, #0a0f1d 60%); +} + +:root[data-theme='light'], +[data-theme='light'] ::backdrop { + /* Surfaces */ + --nc-bg: #f6f8fc; + --nc-surface: #f6f8fc; + --nc-raised: #ffffff; + --nc-line: #cdd5e5; + --nc-line-soft: #e3e8f2; + + /* Ink */ + --nc-ink: #0d1430; + --nc-ink-2: #1e2745; + --nc-text: #2e3752; + --nc-muted: #58617c; + --nc-faint: #8a93ad; + + /* Accent */ + --nc-link: #4034b8; + --nc-on-link: #ffffff; + --nc-accent: #5a4ad6; + --nc-accent-soft: #e9e7fb; + --nc-accent-strong: #262066; + + /* Signals: Starlight asides and badges, the ESLint verdict frames and rule badges */ + --nc-note-bg: #e6f3f6; + --nc-note-edge: #177f8c; + --nc-note-ink: #0c4e58; + --nc-tip-bg: #ecebfa; + --nc-tip-edge: #5a4ad6; + --nc-tip-ink: #332a8f; + --nc-caution-bg: #f8efdf; + --nc-caution-edge: #b87a10; + --nc-caution-ink: #6b4207; + --nc-danger-bg: #f9e7e6; + --nc-danger-edge: #c63f36; + --nc-danger-ink: #85211a; + --nc-success-bg: #e3f2e9; + --nc-success-edge: #1d8a50; + --nc-success-ink: #12532f; + + /* Components: cards and package cards */ + --nc-card-bg: #ffffff; + --nc-card-border: #cdd5e5; + + /* Code: frames read these; the Expressive Code theme carries the syn-* values */ + --nc-code-inline: #e8ecf5; + --nc-code-bg: #ffffff; + --nc-code-ink: #1e2745; + --nc-syn-keyword: #4a36c4; + --nc-syn-string: #0a6873; + --nc-syn-number: #8a4b00; + --nc-syn-function: #0c4e58; + --nc-syn-property: #1e2745; + --nc-syn-comment: #5c6682; + --nc-syn-punct: #58617c; + + /* Motif: the window-frame backdrop */ + --nc-motif: + radial-gradient(circle at 23% 41%, rgba(241, 243, 250, 0.85) 0 1px, transparent 1.6px) 13px 29px / 113px 97px, + radial-gradient(circle at 71% 13%, rgba(143, 221, 232, 0.7) 0 1px, transparent 1.6px) 41px 7px / 167px 139px, + radial-gradient(circle at 47% 83%, rgba(241, 243, 250, 0.55) 0 0.8px, transparent 1.3px) 5px 53px / 229px 181px, + radial-gradient(circle at 88% 64%, rgba(167, 156, 255, 0.6) 0 0.9px, transparent 1.4px) 71px 23px / 191px 157px, + linear-gradient(160deg, #262066, #0d1430 65%); +} + +/* Structure */ +:root:not([data-theme='light']) body { + background-image: + linear-gradient(to bottom, transparent 0, var(--sl-color-black) 26rem), + radial-gradient(circle, rgba(241, 243, 250, 0.55) 0 0.5px, transparent 1px), + radial-gradient(circle, rgba(143, 221, 232, 0.45) 0 0.75px, transparent 1.25px); + background-size: 100% 100%, 173px 139px, 257px 211px; + background-repeat: no-repeat, repeat, repeat; +} + +.page > .header { + border-bottom: 0; + background-image: + repeating-linear-gradient(90deg, var(--sl-color-gray-5) 0 1px, transparent 1px 2rem), + linear-gradient(var(--sl-color-gray-5), var(--sl-color-gray-5)); + background-size: 100% 0.3125rem, 100% 1px; + background-position: left bottom; + background-repeat: repeat-x, no-repeat; +} + +.sidebar-pane { + border-inline-end: 1px solid var(--sl-color-hairline-light); +} + +.group-label .large, +starlight-toc h2, +.sl-markdown-content th { + font-family: var(--sl-font-mono); + font-size: 0.6875rem; + font-weight: 500; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--sl-color-gray-3); +} + +.sidebar-content a[aria-current='page'] { + background: transparent; + color: var(--sl-color-white); + box-shadow: inset 2px 0 0 var(--sl-color-text-accent); + border-radius: 0; +} + +h1#_top, +.hero h1 { + font-weight: 400; + font-variation-settings: 'opsz' 72; + letter-spacing: -0.02em; +} + +.sl-markdown-content h2 { + font-weight: 400; + font-variation-settings: 'opsz' 60; + padding-top: 1.75rem; + border-top: 1px solid var(--sl-color-hairline-light); +} + +.sl-markdown-content h2[id]::before { + content: '\00a7\00a0' attr(id); + display: block; + margin-bottom: 0.75rem; + font: 500 0.6875rem/1 var(--sl-font-mono); + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--sl-color-gray-3); +} + +.starlight-aside { + border: 1px solid var(--sl-color-asides-border); + border-inline-start-width: 3px; +} + +.starlight-aside__title { + font-family: var(--sl-font-mono); + font-size: 0.75rem; + letter-spacing: 0.1em; + text-transform: uppercase; +} + +.sl-link-card { + background: transparent; + border-width: 1px 0 0; + border-radius: 0; + padding-inline: 0; + box-shadow: none; +} + +.sl-link-card .title, +.package-card-short { + font-family: var(--nc-font-display); + font-style: italic; + font-weight: 400; +} diff --git a/site/src/styles/site.css b/site/src/styles/site.css new file mode 100644 index 0000000..5456b47 --- /dev/null +++ b/site/src/styles/site.css @@ -0,0 +1,210 @@ +/* showcase-kit docs: the site's own rules. + Loaded last, after the noctcore theme (noctcore/base.css, components.css + and presets/observatory.css), so every colour here is a Starlight --sl-* + variable or an --nc-* token the preset sets for dark and light. */ + +/* --------------------------------------------------------------------------- + Hero. base.css puts the brand gradient on the primary action with + night-navy text; the hover keeps that text instead of Starlight's. +--------------------------------------------------------------------------- */ + +.hero .sl-link-button.primary:hover, +.hero .sl-link-button.primary:focus-visible { + color: var(--nc-night); + filter: brightness(1.08); +} + +/* The tagline is the page's opening paragraph, in the .nc-lead type. */ +.hero .tagline { + max-width: 38rem; + font-size: var(--sl-text-lg); + line-height: 1.6; + color: var(--sl-color-gray-2); + text-wrap: pretty; +} + +/* The splash's hero banner: a real output of the kit, shown as a figure under + the hero rather than in its image slot, because it carries another app's + name. The kit already framed it, so it is a plain image, not an .nc-shot. */ +.splash-shot { + margin: 0 0 2.5rem; +} + +.splash-shot img { + display: block; + width: 100%; + height: auto; + border: 1px solid var(--sl-color-hairline-light); + border-radius: calc(var(--nc-radius) * 1.25); +} + +.splash-shot figcaption { + margin-top: 0.75rem; + font-size: var(--sl-text-sm); + line-height: 1.55; + color: var(--sl-color-gray-3); +} + +/* --------------------------------------------------------------------------- + Site title. The wordmark override (SiteTitle.astro) replaces Starlight's + component, and with it the component's scoped layout: these are its values, + with a tighter gap between the wordmark, the separator and the product. +--------------------------------------------------------------------------- */ + +.site-title { + align-items: center; + gap: 0.5rem; + min-width: 0; + font-size: var(--sl-text-h4); + white-space: nowrap; + text-decoration: none; +} + +.site-title img { + height: calc(var(--sl-nav-height) - 2 * var(--sl-nav-pad-y)); + width: auto; + margin-inline-end: 0.25rem; +} + +/* Below Starlight's 50em breakpoint the header has room for the logo, the + search and menu buttons and about 15rem of title. The wordmark and the + product in monospace at h4 size are too wide for a 390px phone, base size fits. */ +@media (max-width: 50em) { + .site-title { + font-size: var(--sl-text-base); + } +} + +/* --------------------------------------------------------------------------- + Tables. The guides' Markdown tables scroll sideways inside the column; at + phone width their last column ("Meaning") would get squeezed to a word per + line, so it gets a floor. The generated reference tables are .nc-ref + (components.css) and stack into entries instead. +--------------------------------------------------------------------------- */ + +@media (max-width: 40rem) { + .sl-markdown-content table:not(.nc-ref) td:last-child { + min-width: 16rem; + } +} + +.reference-wrap { + overflow-x: auto; +} + +/* components.css keeps an .nc-ref table's first and last columns on one line, + for a key and a column of badges. In the reference the last column is prose + (Meaning, Message) and a union type can be long: both wrap. */ +.sl-markdown-content .nc-ref td.reference-meaning, +.sl-markdown-content .nc-ref td.reference-type { + white-space: normal; +} + +/* Stacked, prose reads as a paragraph under its label, not as a chip beside it. */ +@media (max-width: 40rem) { + .sl-markdown-content .nc-ref td.reference-meaning { + display: block; + margin-inline-end: 0; + } + + .sl-markdown-content .nc-ref td.reference-meaning[data-label]::before { + display: block; + margin-bottom: 0.2rem; + } +} + +/* --------------------------------------------------------------------------- + What one config produces: the splash's cards, one per output, each linking + to its guide (GuideOutputs.astro). On the preset's card surface. +--------------------------------------------------------------------------- */ + +.sl-markdown-content .link-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); + gap: 0.75rem; + padding: 0; + list-style: none; +} + +.sl-markdown-content .link-grid .link-card { + margin: 0; +} + +.link-card a { + position: relative; + display: flex; + flex-direction: column; + gap: 0.4rem; + height: 100%; + padding: 1rem 1.1rem; + overflow: hidden; + border: 1px solid var(--nc-card-border); + border-radius: var(--nc-radius); + background: var(--nc-card-bg); + color: var(--sl-color-gray-2); + text-decoration: none; +} + +.link-card a::before { + content: ''; + position: absolute; + inset: 0 0 auto; + height: 2px; + background: var(--nc-gradient); + opacity: 0; +} + +.link-card a:hover, +.link-card a:focus-visible { + border-color: var(--sl-color-accent); +} + +.link-card a:hover::before, +.link-card a:focus-visible::before { + opacity: 1; +} + +.link-card-name { + font-family: var(--nc-font-display); + font-size: var(--sl-text-lg); + line-height: 1.25; + color: var(--sl-color-white); +} + +.link-card-desc { + font-size: var(--sl-text-sm); + line-height: 1.5; +} + +.link-card-meta { + margin-top: auto; + font: 500 var(--sl-text-xs) / 1.4 var(--__sl-font-mono); + color: var(--sl-color-gray-3); +} + +/* components.css sets the note's size and colour. */ +.site-footer-note { + margin-top: 1.5rem; +} + +/* --------------------------------------------------------------------------- + Section headings. Starlight wraps each Markdown h2 in .sl-heading-wrapper + and sets the heading inline, so the preset's hairline over an h2 only spans + the heading's text. The wrapper carries it instead, across the column. +--------------------------------------------------------------------------- */ + +.sl-markdown-content .sl-heading-wrapper.level-h2 { + padding-top: 1.75rem; + border-top: 1px solid var(--sl-color-hairline-light); +} + +.sl-markdown-content .sl-heading-wrapper.level-h2 > h2 { + padding-top: 0; + border-top: 0; +} + +/* The preset sets table heads in mono capitals. Code in a head is a literal + (a URL, a key), so it keeps its case. */ +.sl-markdown-content th code { + text-transform: none; +} diff --git a/site/src/styles/theme.css b/site/src/styles/theme.css deleted file mode 100644 index 23fdc33..0000000 --- a/site/src/styles/theme.css +++ /dev/null @@ -1,287 +0,0 @@ -/* noctcore docs theme. - The noctcore brand: a night sky (near-black navy, not violet), a crescent moon - that runs from violet (#8b7cff) on one limb to cyan (#4dd0e1) on the other, - and a white wordmark over a short violet-to-cyan rule. Surfaces here are that - navy; the gradient is the signature and cyan does the link and highlight work. - Starlight is dark by default, so :root is the dark theme and - :root[data-theme='light'] overrides it. */ - -:root { - --sl-font: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif; - --sl-font-mono: 'JetBrains Mono Variable', ui-monospace, 'SFMono-Regular', monospace; - --nc-font-display: 'Space Grotesk Variable', ui-sans-serif, system-ui, sans-serif; - - /* Brand constants. Decorative only (gradients, the moon): never body text. */ - --nc-violet: #8b7cff; - --nc-cyan: #4dd0e1; - --nc-gradient: linear-gradient(90deg, var(--nc-violet), var(--nc-cyan)); - --nc-night: #0b1020; - - /* Starlight maps text-accent to accent-high in dark mode: links, the - selected sidebar item and the primary button all use it, so it is the - brand cyan. accent (violet) drives hover borders and focus. */ - --sl-color-accent-low: #1b2450; - --sl-color-accent: #8b7cff; - --sl-color-accent-high: #5fd6e5; - - --sl-color-white: #eef2fb; - --sl-color-gray-1: #dde3f1; - --sl-color-gray-2: #b7c0d8; - --sl-color-gray-3: #939fbf; - --sl-color-gray-4: #5b6a90; - --sl-color-gray-5: #26324f; - --sl-color-gray-6: #141c33; - --sl-color-gray-7: #10172b; - --sl-color-black: #0b1020; - - /* Cyan as text: the card meta line. Same hue family as the links. */ - --nc-glow: #5fd6e5; - --nc-card: #10172b; - --nc-card-hover: #161f3a; - - --sl-content-width: 50rem; -} - -:root[data-theme='light'] { - /* Light mode is genuinely light: white paper, navy ink, the violet darkened - until it holds 4.5:1 as link text, cyan deepened to teal for the same - reason. The gradient constants stay the brand hexes because they only - ever sit under navy text or on their own. */ - --sl-color-accent-low: #e7e5ff; - --sl-color-accent: #5a4ad6; - --sl-color-accent-high: #2a2470; - - --sl-color-white: #0b1020; - --sl-color-gray-1: #232c44; - --sl-color-gray-2: #3a4560; - --sl-color-gray-3: #5a6684; - --sl-color-gray-4: #7b86a4; - --sl-color-gray-5: #c9d1e3; - --sl-color-gray-6: #e9edf7; - --sl-color-gray-7: #f5f7fc; - --sl-color-black: #ffffff; - - --nc-glow: #0b6f7e; - --nc-card: #f5f7fc; - --nc-card-hover: #eef1f9; -} - -/* Night sky. A faint violet and cyan glow at the top of the page in dark - mode, the way the banner lifts from black at the horizon. Light mode gets - none of this: paper stays paper. */ -:root:not([data-theme='light']) body { - background-image: - radial-gradient(70rem 30rem at 20% -8rem, rgba(139, 124, 255, 0.16), transparent 65%), - radial-gradient(50rem 24rem at 90% -10rem, rgba(77, 208, 225, 0.1), transparent 65%); - background-repeat: no-repeat; -} - -/* The wordmark is monospace white over a gradient rule in the banner; the - site title follows it. */ -.site-title { - font-family: var(--sl-font-mono); - font-weight: 600; - letter-spacing: -0.02em; - color: var(--sl-color-white) !important; -} - -.site-title span { - text-overflow: ellipsis; -} - -/* Below Starlight's 50em breakpoint the header has room for the logo, the - search and menu buttons and about 15rem of title. Monospace at h4 size - plus the version pill is too wide for a 390px phone, base size fits. */ -@media (max-width: 50em) { - .site-title { - font-size: var(--sl-text-base); - } -} - -/* Hero: the banner's short violet-to-cyan rule under the title, and the - primary action carries the gradient with night-navy text, which holds AA - against both ends in both modes. */ -.hero h1::after { - content: ''; - display: block; - width: 6rem; - height: 0.25rem; - margin-top: 1rem; - border-radius: 999px; - background: var(--nc-gradient); -} - -/* Starlight centres the hero below 50rem and starts it above; the rule - follows the title. */ -@media (max-width: 49.99rem) { - .hero h1::after { - margin-inline: auto; - } -} - -.hero .sl-link-button.primary { - background: var(--nc-gradient); - border-color: transparent; - color: var(--nc-night); -} - -.hero .sl-link-button.primary:hover, -.hero .sl-link-button.primary:focus-visible { - color: var(--nc-night); - filter: brightness(1.08); -} - -.site-title, -.sl-markdown-content h1, -.sl-markdown-content h2, -h1#_top, -.hero h1 { - font-family: var(--nc-font-display); - letter-spacing: -0.015em; -} - -/* --------------------------------------------------------------------------- - Version pill. - - SiteTitle.astro puts the current release next to the wordmark as a small - pill that links to the changelog. ---------------------------------------------------------------------------- */ - -.site-title-group { - display: flex; - align-items: center; - gap: 0.6rem; - min-width: 0; -} - -.version-pill { - display: inline-block; - padding: 0.05rem 0.5rem; - border-radius: 999px; - font-family: var(--sl-font-mono); - font-size: var(--sl-text-xs); - font-weight: 600; - white-space: nowrap; - text-decoration: none; - color: var(--sl-color-accent-high); - background: var(--sl-color-accent-low); -} - -.version-pill:hover, -.version-pill:focus-visible { - color: var(--sl-color-white); -} - -/* --------------------------------------------------------------------------- - Tables. - - At phone width a key table's last column ("Meaning") gets squeezed to a - word per line. Give it a floor and let the table scroll sideways instead. ---------------------------------------------------------------------------- */ - -@media (max-width: 40rem) { - .sl-markdown-content table td:last-child { - min-width: 16rem; - } -} - -/* --------------------------------------------------------------------------- - Landing page. ---------------------------------------------------------------------------- */ - -.hero .tagline { - max-width: 38rem; -} - -.link-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); - gap: 0.75rem; - padding: 0 !important; - list-style: none; -} - -.link-grid .link-card { - margin: 0 !important; -} - -.link-card a { - position: relative; - overflow: hidden; - display: flex; - flex-direction: column; - gap: 0.4rem; - height: 100%; - padding: 1rem 1.1rem; - border: 1px solid var(--sl-color-gray-5); - border-radius: 0.6rem; - background: var(--nc-card); - color: var(--sl-color-gray-2); - text-decoration: none; - transition: border-color 0.15s ease, background 0.15s ease; -} - -.link-card a::before { - content: ''; - position: absolute; - inset: 0 0 auto 0; - height: 3px; - background: var(--nc-gradient); - opacity: 0; - transition: opacity 0.15s ease; -} - -.link-card a:hover, -.link-card a:focus-visible { - border-color: var(--sl-color-accent); - background: var(--nc-card-hover); -} - -.link-card a:hover::before, -.link-card a:focus-visible::before { - opacity: 1; -} - -.link-card-name { - font-family: var(--sl-font-mono); - font-size: var(--sl-text-sm); - font-weight: 600; - color: var(--sl-color-white); -} - -.link-card-desc { - font-size: var(--sl-text-sm); - line-height: 1.45; -} - -.link-card-meta { - margin-top: auto; - font-size: var(--sl-text-xs); - color: var(--nc-glow); -} - -/* The splash's hero banner: a real output of the kit, shown as a figure under - the hero rather than in its image slot, because it carries another app's name. */ -.splash-shot { - margin: 0 0 2.5rem; -} - -.splash-shot img { - display: block; - width: 100%; - height: auto; - border-radius: 0.75rem; - border: 1px solid var(--sl-color-gray-5); -} - -.splash-shot figcaption { - margin-top: 0.75rem; - font-size: var(--sl-text-sm); - color: var(--sl-color-gray-3); -} - -.site-footer-note { - margin-top: 1.5rem; - font-size: var(--sl-text-xs); - color: var(--sl-color-gray-3); -}