@casoon/pages-theme 0.0.0-stage → 0.12.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +279 -2
- package/assets/casoon-logo-dark.svg +70 -0
- package/assets/casoon-logo.svg +71 -0
- package/assets/favicon.svg +1 -0
- package/components/AnchorGrid.astro +135 -0
- package/components/ApiEntry.astro +136 -0
- package/components/Badge.astro +31 -0
- package/components/Badges.astro +10 -0
- package/components/BuildOutput.astro +84 -0
- package/components/Button.astro +55 -0
- package/components/CallToAction.astro +64 -0
- package/components/Callout.astro +66 -0
- package/components/CodeBlock.astro +84 -0
- package/components/Demo.astro +80 -0
- package/components/ExamplePanel.astro +153 -0
- package/components/Facts.astro +112 -0
- package/components/Faq.astro +123 -0
- package/components/Features.astro +139 -0
- package/components/FileSet.astro +258 -0
- package/components/Footer.astro +160 -0
- package/components/Header.astro +278 -0
- package/components/Hero.astro +318 -0
- package/components/Install.astro +154 -0
- package/components/Integrations.astro +125 -0
- package/components/Keymap.astro +102 -0
- package/components/Quickstart.astro +95 -0
- package/components/ReleaseHighlights.astro +115 -0
- package/components/Spotlight.astro +177 -0
- package/components/Swatch.astro +45 -0
- package/components/Tabs.astro +90 -0
- package/components/Terminal.astro +79 -0
- package/components/index.ts +25 -0
- package/content.ts +23 -0
- package/fonts/OFL.txt +95 -0
- package/fonts/inter-latin-400-normal.woff2 +0 -0
- package/fonts/inter-latin-500-normal.woff2 +0 -0
- package/fonts/inter-latin-600-normal.woff2 +0 -0
- package/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/fonts/jetbrains-mono-latin-500-normal.woff2 +0 -0
- package/fonts/onest-latin-wght-normal.woff2 +0 -0
- package/index.ts +204 -0
- package/layouts/Base.astro +131 -0
- package/layouts/Docs.astro +286 -0
- package/lib/ansi.ts +168 -0
- package/lib/changelog.ts +85 -0
- package/lib/docs.ts +54 -0
- package/lib/i18n.ts +136 -0
- package/lib/og.ts +91 -0
- package/lib/showcase.ts +48 -0
- package/lib/url.ts +16 -0
- package/lib/version.ts +50 -0
- package/package.json +71 -4
- package/routes/404.astro +49 -0
- package/routes/changelog.astro +129 -0
- package/routes/docs/[...slug].astro +39 -0
- package/routes/showcase/[slug].astro +108 -0
- package/routes/showcase/index.astro +195 -0
- package/styles/base.css +307 -0
- package/styles/fonts.css +52 -0
- package/styles/tokens.css +136 -0
- package/virtual.d.ts +12 -0
package/lib/i18n.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/** UI strings the theme emits itself. Project-supplied props and content stay untranslated. */
|
|
2
|
+
|
|
3
|
+
export type Lang = 'en' | 'de';
|
|
4
|
+
|
|
5
|
+
const en = {
|
|
6
|
+
dateLocale: 'en-GB',
|
|
7
|
+
skipLink: 'Skip to content',
|
|
8
|
+
mainNav: 'Main',
|
|
9
|
+
docs: 'Docs',
|
|
10
|
+
showcase: 'Showcase',
|
|
11
|
+
changelog: 'Changelog',
|
|
12
|
+
darkTheme: 'Dark theme',
|
|
13
|
+
copy: 'Copy',
|
|
14
|
+
copied: 'Copied',
|
|
15
|
+
footerProject: 'Project',
|
|
16
|
+
documentation: 'Documentation',
|
|
17
|
+
footerPackages: 'Packages',
|
|
18
|
+
website: (name: string) => `${name} website`,
|
|
19
|
+
catalogEntry: (name: string) => `${name} on casoon.dev`,
|
|
20
|
+
allProjects: 'All open-source projects by CASOON',
|
|
21
|
+
footerBy: 'A CASOON project',
|
|
22
|
+
imprint: 'Imprint',
|
|
23
|
+
privacyPolicy: 'Privacy Policy',
|
|
24
|
+
license: (id: string) => `${id} License`,
|
|
25
|
+
docsNav: 'Documentation',
|
|
26
|
+
breadcrumb: 'Breadcrumb',
|
|
27
|
+
pagerDocs: 'Previous and next page',
|
|
28
|
+
pagerShowcase: 'Previous and next example',
|
|
29
|
+
previous: 'Previous',
|
|
30
|
+
next: 'Next',
|
|
31
|
+
editPage: 'Edit this page on GitHub',
|
|
32
|
+
docsFor: (version: string) => `Docs for v${version}`,
|
|
33
|
+
onThisPage: 'On this page',
|
|
34
|
+
heroEyebrow: 'CASOON Open Source',
|
|
35
|
+
installLabel: 'Package manager',
|
|
36
|
+
callout: { note: 'Note', tip: 'Tip', caution: 'Caution', deprecated: 'Deprecated' },
|
|
37
|
+
buildOutputHeading: 'Generated at build time',
|
|
38
|
+
allExamples: 'All examples',
|
|
39
|
+
featuresHeading: 'What it does',
|
|
40
|
+
overview: 'Overview',
|
|
41
|
+
quickstartTitle: 'Quickstart',
|
|
42
|
+
quickstartLead:
|
|
43
|
+
'From install to a first result. The full walkthrough lives in the documentation.',
|
|
44
|
+
readDocs: 'Read the documentation',
|
|
45
|
+
parameters: 'Parameters',
|
|
46
|
+
detail: 'Detail',
|
|
47
|
+
input: 'Input',
|
|
48
|
+
output: 'Output',
|
|
49
|
+
keyFacts: 'Key facts',
|
|
50
|
+
themeDependent: 'theme-dependent',
|
|
51
|
+
tabsLabel: 'Variants',
|
|
52
|
+
filesLabel: 'Generated files',
|
|
53
|
+
notFoundTitle: 'Page not found',
|
|
54
|
+
notFoundHeading: 'This page does not exist.',
|
|
55
|
+
notFoundBody: 'The address may be outdated, or the page moved when the docs were restructured.',
|
|
56
|
+
backToStart: 'Back to the start page',
|
|
57
|
+
openDocs: 'Open the documentation',
|
|
58
|
+
changelogDescription: (name: string) => `Release history of ${name}.`,
|
|
59
|
+
changelogLeadBefore: 'Generated from',
|
|
60
|
+
changelogLeadAfter: 'in the project repository. Never maintained twice.',
|
|
61
|
+
showcaseDescription: (name: string) => `Real input and output of ${name}.`,
|
|
62
|
+
showcaseLead: (name: string) =>
|
|
63
|
+
`Each example shows real output produced by ${name} itself, from files in the project repository. Input on the left, output on the right.`,
|
|
64
|
+
faqHeading: 'Frequently asked questions',
|
|
65
|
+
newIn: (version: string) => `New in v${version}`,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type Strings = typeof en;
|
|
69
|
+
|
|
70
|
+
const de: Strings = {
|
|
71
|
+
dateLocale: 'de-DE',
|
|
72
|
+
skipLink: 'Zum Inhalt springen',
|
|
73
|
+
mainNav: 'Hauptnavigation',
|
|
74
|
+
docs: 'Doku',
|
|
75
|
+
showcase: 'Beispiele',
|
|
76
|
+
changelog: 'Änderungen',
|
|
77
|
+
darkTheme: 'Dunkles Design',
|
|
78
|
+
copy: 'Kopieren',
|
|
79
|
+
copied: 'Kopiert',
|
|
80
|
+
footerProject: 'Projekt',
|
|
81
|
+
documentation: 'Dokumentation',
|
|
82
|
+
footerPackages: 'Pakete',
|
|
83
|
+
website: (name) => `Website von ${name}`,
|
|
84
|
+
catalogEntry: (name) => `${name} auf casoon.dev`,
|
|
85
|
+
allProjects: 'Alle Open-Source-Projekte von CASOON',
|
|
86
|
+
footerBy: 'Ein Projekt von CASOON',
|
|
87
|
+
imprint: 'Impressum',
|
|
88
|
+
privacyPolicy: 'Datenschutz',
|
|
89
|
+
license: (id) => `${id}-Lizenz`,
|
|
90
|
+
docsNav: 'Dokumentation',
|
|
91
|
+
breadcrumb: 'Navigationspfad',
|
|
92
|
+
pagerDocs: 'Vorherige und nächste Seite',
|
|
93
|
+
pagerShowcase: 'Vorheriges und nächstes Beispiel',
|
|
94
|
+
previous: 'Zurück',
|
|
95
|
+
next: 'Weiter',
|
|
96
|
+
editPage: 'Diese Seite auf GitHub bearbeiten',
|
|
97
|
+
docsFor: (version) => `Doku für v${version}`,
|
|
98
|
+
onThisPage: 'Auf dieser Seite',
|
|
99
|
+
heroEyebrow: 'CASOON Open Source',
|
|
100
|
+
installLabel: 'Paketmanager',
|
|
101
|
+
callout: { note: 'Hinweis', tip: 'Tipp', caution: 'Achtung', deprecated: 'Veraltet' },
|
|
102
|
+
buildOutputHeading: 'Beim Build erzeugt',
|
|
103
|
+
allExamples: 'Alle Beispiele',
|
|
104
|
+
featuresHeading: 'Was es kann',
|
|
105
|
+
overview: 'Überblick',
|
|
106
|
+
quickstartTitle: 'Schnellstart',
|
|
107
|
+
quickstartLead:
|
|
108
|
+
'Von der Installation zum ersten Ergebnis. Die ausführliche Anleitung steht in der Dokumentation.',
|
|
109
|
+
readDocs: 'Zur Dokumentation',
|
|
110
|
+
parameters: 'Parameter',
|
|
111
|
+
detail: 'Details',
|
|
112
|
+
input: 'Eingabe',
|
|
113
|
+
output: 'Ausgabe',
|
|
114
|
+
keyFacts: 'Eckdaten',
|
|
115
|
+
themeDependent: 'je nach Farbschema',
|
|
116
|
+
tabsLabel: 'Varianten',
|
|
117
|
+
filesLabel: 'Erzeugte Dateien',
|
|
118
|
+
notFoundTitle: 'Seite nicht gefunden',
|
|
119
|
+
notFoundHeading: 'Diese Seite gibt es nicht.',
|
|
120
|
+
notFoundBody:
|
|
121
|
+
'Die Adresse ist vielleicht veraltet, oder die Seite ist beim Umbau der Doku umgezogen.',
|
|
122
|
+
backToStart: 'Zur Startseite',
|
|
123
|
+
openDocs: 'Zur Dokumentation',
|
|
124
|
+
changelogDescription: (name) => `Versionsgeschichte von ${name}.`,
|
|
125
|
+
changelogLeadBefore: 'Erzeugt aus',
|
|
126
|
+
changelogLeadAfter: 'im Projekt-Repository. Wird nie doppelt gepflegt.',
|
|
127
|
+
showcaseDescription: (name) => `Echte Ein- und Ausgaben von ${name}.`,
|
|
128
|
+
showcaseLead: (name) =>
|
|
129
|
+
`Jedes Beispiel zeigt echte Ausgabe, die ${name} selbst erzeugt hat, aus Dateien im Projekt-Repository. Links die Eingabe, rechts die Ausgabe.`,
|
|
130
|
+
faqHeading: 'Häufige Fragen',
|
|
131
|
+
newIn: (version) => `Neu in v${version}`,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const strings: Record<Lang, Strings> = { en, de };
|
|
135
|
+
|
|
136
|
+
export const t = (lang: Lang): Strings => strings[lang];
|
package/lib/og.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import sharp from 'sharp';
|
|
4
|
+
import type { ResolvedConfig } from '../index';
|
|
5
|
+
|
|
6
|
+
/** Dark theme tokens (styles/tokens.css): the card looks like the site at night. */
|
|
7
|
+
const BG = '#101010';
|
|
8
|
+
const HEADING = '#f4f4f4';
|
|
9
|
+
const TEXT = '#c3c7c7';
|
|
10
|
+
const MUTED = '#8c9090';
|
|
11
|
+
const ACCENT = 'rgb(58 206 222)';
|
|
12
|
+
|
|
13
|
+
const WIDTH = 1200;
|
|
14
|
+
const HEIGHT = 630;
|
|
15
|
+
const X = 88;
|
|
16
|
+
|
|
17
|
+
const asset = (path: string) => fileURLToPath(new URL(path, import.meta.url));
|
|
18
|
+
const SANS = asset('../fonts/onest-latin-wght-normal.woff2');
|
|
19
|
+
const MONO = asset('../fonts/jetbrains-mono-latin-400-normal.woff2');
|
|
20
|
+
|
|
21
|
+
const escapeMarkup = (text: string) => text.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
22
|
+
|
|
23
|
+
/** Cut at a word boundary so the text keeps to about three lines on the card. */
|
|
24
|
+
const clip = (text: string, max: number) =>
|
|
25
|
+
text.length <= max ? text : `${text.slice(0, text.lastIndexOf(' ', max - 1))} …`;
|
|
26
|
+
|
|
27
|
+
/** One text block via Pango, in a font file of the theme: no system fonts involved. */
|
|
28
|
+
const text = (markup: string, font: string, fontfile: string, width: number) =>
|
|
29
|
+
sharp({ text: { text: markup, font, fontfile, width, wrap: 'word', rgba: true } })
|
|
30
|
+
.png()
|
|
31
|
+
.toBuffer();
|
|
32
|
+
|
|
33
|
+
/** Largest name size whose single line fits the card, so a long name shrinks instead of wrapping. */
|
|
34
|
+
async function nameBlock(name: string, width: number): Promise<Buffer> {
|
|
35
|
+
const markup = `<span foreground="${HEADING}" weight="600">${escapeMarkup(name)}</span>`;
|
|
36
|
+
const sizes = [96, 80, 68, 58, 50];
|
|
37
|
+
for (const size of sizes) {
|
|
38
|
+
const block = await text(markup, `Onest ${size}`, SANS, width * 4);
|
|
39
|
+
if (((await sharp(block).metadata()).width ?? 0) <= width) return block;
|
|
40
|
+
}
|
|
41
|
+
return text(markup, `Onest ${sizes[sizes.length - 1]}`, SANS, width);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The project's 1200×630 Open Graph image: name, tagline and description, address. */
|
|
45
|
+
export async function renderOgImage(config: ResolvedConfig, site: URL): Promise<Buffer> {
|
|
46
|
+
const accent = config.accent?.dark ?? ACCENT;
|
|
47
|
+
const background = Buffer.from(
|
|
48
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${WIDTH}" height="${HEIGHT}">` +
|
|
49
|
+
`<rect width="100%" height="100%" fill="${BG}"/>` +
|
|
50
|
+
`<rect width="12" height="100%" fill="${accent}"/>` +
|
|
51
|
+
`<rect x="${X}" y="${HEIGHT - 128}" width="${WIDTH - 2 * X}" height="1" fill="${MUTED}" opacity="0.4"/>` +
|
|
52
|
+
'</svg>'
|
|
53
|
+
);
|
|
54
|
+
const logo = await sharp(await readFile(asset('../assets/casoon-logo-dark.svg')), {
|
|
55
|
+
density: 300,
|
|
56
|
+
})
|
|
57
|
+
.resize({ width: 176 })
|
|
58
|
+
.png()
|
|
59
|
+
.toBuffer();
|
|
60
|
+
const address = `${site.host}${site.pathname}`.replace(/\/$/, '');
|
|
61
|
+
const span = (color: string, content: string) =>
|
|
62
|
+
`<span foreground="${color}">${escapeMarkup(content)}</span>`;
|
|
63
|
+
|
|
64
|
+
const [eyebrow, name, tagline, description, host] = await Promise.all([
|
|
65
|
+
text(span(MUTED, 'CASOON Open Source'), 'JetBrains Mono 26', MONO, 1000),
|
|
66
|
+
nameBlock(config.name, WIDTH - 2 * X),
|
|
67
|
+
config.tagline && text(span(HEADING, config.tagline), 'Onest 44', SANS, WIDTH - 2 * X),
|
|
68
|
+
text(
|
|
69
|
+
span(TEXT, clip(config.description, config.tagline ? 150 : 140)),
|
|
70
|
+
config.tagline ? 'Onest 30' : 'Onest 40',
|
|
71
|
+
SANS,
|
|
72
|
+
WIDTH - 2 * X
|
|
73
|
+
),
|
|
74
|
+
text(span(MUTED, address), 'JetBrains Mono 24', MONO, 640),
|
|
75
|
+
]);
|
|
76
|
+
const size = async (image: Buffer) => sharp(image).metadata();
|
|
77
|
+
const nameBottom = 128 + ((await size(name)).height ?? 0) + 16;
|
|
78
|
+
const taglineHeight = tagline ? ((await size(tagline)).height ?? 0) + 24 : 0;
|
|
79
|
+
|
|
80
|
+
return sharp(background)
|
|
81
|
+
.composite([
|
|
82
|
+
{ input: eyebrow, left: X, top: 76 },
|
|
83
|
+
{ input: name, left: X, top: 128 },
|
|
84
|
+
...(tagline ? [{ input: tagline, left: X, top: nameBottom }] : []),
|
|
85
|
+
{ input: description, left: X, top: nameBottom + taglineHeight },
|
|
86
|
+
{ input: logo, left: X, top: HEIGHT - 92 },
|
|
87
|
+
{ input: host, left: WIDTH - X - ((await size(host)).width ?? 0), top: HEIGHT - 80 },
|
|
88
|
+
])
|
|
89
|
+
.png()
|
|
90
|
+
.toBuffer();
|
|
91
|
+
}
|
package/lib/showcase.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface ShowcaseExample {
|
|
2
|
+
/** URL segment of the detail page. */
|
|
3
|
+
slug: string;
|
|
4
|
+
title: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
/** Source file in the project repository, e.g. "examples/report.ansi". */
|
|
7
|
+
file: string;
|
|
8
|
+
tags?: string[];
|
|
9
|
+
/** What goes in: source, fixture, configuration. */
|
|
10
|
+
input: { code: string; lang?: string };
|
|
11
|
+
/**
|
|
12
|
+
* What comes out: HTML produced at build time by the project's own package.
|
|
13
|
+
* "terminal" renders on the dark terminal surface, "panel" on the page surface (SVG, tables …).
|
|
14
|
+
*/
|
|
15
|
+
output: { html: string; kind?: 'terminal' | 'panel' };
|
|
16
|
+
/**
|
|
17
|
+
* Heading the example is listed under on the overview. Examples sharing a group appear together,
|
|
18
|
+
* groups in the order of their first example, with a jump list above them.
|
|
19
|
+
*/
|
|
20
|
+
group?: string;
|
|
21
|
+
/**
|
|
22
|
+
* A small preview for the overview, typically a lazily loaded image. With it the overview shows
|
|
23
|
+
* a compact card (title, description, preview, link); input and output stay on the detail page.
|
|
24
|
+
* Suits showcases with many examples.
|
|
25
|
+
*/
|
|
26
|
+
preview?: { html: string };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const escapeHtml = (text: string) =>
|
|
30
|
+
text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A description as HTML: text escaped, `backtick spans` as `<code>`, no other markup.
|
|
34
|
+
* Text without backticks comes out as the escaped text alone.
|
|
35
|
+
*/
|
|
36
|
+
export function descriptionHtml(text: string): string {
|
|
37
|
+
return text
|
|
38
|
+
.split(/(`[^`]+`)/)
|
|
39
|
+
.map((part, i) =>
|
|
40
|
+
i % 2 === 1 ? `<code>${escapeHtml(part.slice(1, -1))}</code>` : escapeHtml(part)
|
|
41
|
+
)
|
|
42
|
+
.join('');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A description as plain text (meta description and other text-only places): backticks of code spans removed. */
|
|
46
|
+
export function descriptionText(text: string): string {
|
|
47
|
+
return text.replace(/`([^`]+)`/g, '$1');
|
|
48
|
+
}
|
package/lib/url.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Lang } from './i18n';
|
|
2
|
+
|
|
3
|
+
const base = import.meta.env.BASE_URL.replace(/\/?$/, '/');
|
|
4
|
+
|
|
5
|
+
/** Prefix a site-internal path with the Pages base path ("/<repo>/"). */
|
|
6
|
+
export function url(path = ''): string {
|
|
7
|
+
return base + path.replace(/^\//, '');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** casoon.de, canonical host. */
|
|
11
|
+
export const CASOON_URL = 'https://www.casoon.de/';
|
|
12
|
+
|
|
13
|
+
/** The project catalog on casoon.dev (canonical host), or one project's entry in it. */
|
|
14
|
+
export function catalogUrl(lang: Lang, slug = ''): string {
|
|
15
|
+
return `https://casoon.dev/${lang === 'en' ? 'en/' : ''}${slug && `#projekt-${slug}`}`;
|
|
16
|
+
}
|
package/lib/version.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The project's current release, read from the manifest next to site/ so it is never
|
|
6
|
+
* maintained twice. npm projects keep it in package.json, Rust projects in Cargo.toml
|
|
7
|
+
* (also as `[workspace.package]`, which a crate inherits with `version.workspace = true`).
|
|
8
|
+
*
|
|
9
|
+
* A wrong number on the page is worse than none: anything this cannot read with certainty
|
|
10
|
+
* yields '', and the project passes `version` itself.
|
|
11
|
+
*/
|
|
12
|
+
export function projectVersion(repoRoot: string): string {
|
|
13
|
+
return fromPackageJson(repoRoot) || fromCargoToml(repoRoot) || '';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function read(path: string): string | null {
|
|
17
|
+
try {
|
|
18
|
+
return readFileSync(path, 'utf8');
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function fromPackageJson(repoRoot: string): string {
|
|
25
|
+
const raw = read(join(repoRoot, 'package.json'));
|
|
26
|
+
if (raw === null) return '';
|
|
27
|
+
try {
|
|
28
|
+
const { version } = JSON.parse(raw) as { version?: unknown };
|
|
29
|
+
return typeof version === 'string' ? version : '';
|
|
30
|
+
} catch {
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function fromCargoToml(repoRoot: string): string {
|
|
36
|
+
const raw = read(join(repoRoot, 'Cargo.toml'));
|
|
37
|
+
if (raw === null) return '';
|
|
38
|
+
return section(raw, 'package') || section(raw, 'workspace.package');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** First `version = "…"` inside [<name>], stopping at the next table header. */
|
|
42
|
+
function section(toml: string, name: string): string {
|
|
43
|
+
const start = toml.search(new RegExp(`^\\[${name.replaceAll('.', '\\.')}\\]\\s*$`, 'm'));
|
|
44
|
+
if (start === -1) return '';
|
|
45
|
+
const rest = toml.slice(start);
|
|
46
|
+
const next = rest.indexOf('\n[', 1);
|
|
47
|
+
const body = next === -1 ? rest : rest.slice(0, next);
|
|
48
|
+
const match = body.match(/^version\s*=\s*"([^"]+)"/m);
|
|
49
|
+
return match ? match[1] : '';
|
|
50
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,73 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@casoon/pages-theme",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.12.8",
|
|
4
|
+
"description": "Shared Astro theme for the GitHub Pages sites of CASOON open-source projects",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Jörn Seidel <joern.seidel@casoon.de> (https://www.casoon.de)",
|
|
8
|
+
"homepage": "https://github.com/casoon/pages-theme#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/casoon/pages-theme.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/casoon/pages-theme/issues"
|
|
15
|
+
},
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./index.ts",
|
|
18
|
+
"./content": "./content.ts",
|
|
19
|
+
"./components": "./components/index.ts",
|
|
20
|
+
"./components/*": "./components/*",
|
|
21
|
+
"./ansi": "./lib/ansi.ts",
|
|
22
|
+
"./url": "./lib/url.ts",
|
|
23
|
+
"./showcase": "./lib/showcase.ts",
|
|
24
|
+
"./layouts/*": "./layouts/*",
|
|
25
|
+
"./lib/*": "./lib/*.ts",
|
|
26
|
+
"./routes/*": "./routes/*",
|
|
27
|
+
"./styles/*": "./styles/*"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"index.ts",
|
|
31
|
+
"content.ts",
|
|
32
|
+
"virtual.d.ts",
|
|
33
|
+
"assets",
|
|
34
|
+
"components",
|
|
35
|
+
"fonts",
|
|
36
|
+
"layouts",
|
|
37
|
+
"lib",
|
|
38
|
+
"routes",
|
|
39
|
+
"styles"
|
|
40
|
+
],
|
|
41
|
+
"keywords": [
|
|
42
|
+
"astro-integration",
|
|
43
|
+
"astro-theme",
|
|
44
|
+
"withastro",
|
|
45
|
+
"casoon"
|
|
46
|
+
],
|
|
47
|
+
"scripts": {
|
|
48
|
+
"dev": "pnpm --filter starter-site dev",
|
|
49
|
+
"build": "pnpm --filter starter-site build",
|
|
50
|
+
"preview": "pnpm --filter starter-site preview",
|
|
51
|
+
"check": "biome check .",
|
|
52
|
+
"check:fix": "biome check --write .",
|
|
53
|
+
"type-check": "pnpm --filter starter-site type-check",
|
|
54
|
+
"test:accent": "node scripts/test-accent.mjs"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"astro": "^7.3.2"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"@astrojs/mdx": "^8.1.2",
|
|
61
|
+
"sharp": "^0.35.5"
|
|
62
|
+
},
|
|
63
|
+
"publishConfig": {
|
|
64
|
+
"access": "public",
|
|
65
|
+
"provenance": true
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@axe-core/playwright": "^4.14.0",
|
|
69
|
+
"@biomejs/biome": "^2.6.4",
|
|
70
|
+
"@playwright/test": "^1.64.1"
|
|
71
|
+
},
|
|
72
|
+
"packageManager": "pnpm@10.26.0"
|
|
73
|
+
}
|
package/routes/404.astro
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
import config from 'virtual:casoon-pages/config';
|
|
3
|
+
import Button from '../components/Button.astro';
|
|
4
|
+
import Base from '../layouts/Base.astro';
|
|
5
|
+
import { t } from '../lib/i18n';
|
|
6
|
+
import { url } from '../lib/url';
|
|
7
|
+
|
|
8
|
+
const s = t(config.lang);
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<Base title={s.notFoundTitle}>
|
|
12
|
+
<div class="page">
|
|
13
|
+
<p class="code">404</p>
|
|
14
|
+
<h1>{s.notFoundHeading}</h1>
|
|
15
|
+
<p>{s.notFoundBody}</p>
|
|
16
|
+
<div class="actions">
|
|
17
|
+
<Button href={url()}>{s.backToStart}</Button>
|
|
18
|
+
<Button href={url('docs/')} variant="secondary">{s.openDocs}</Button>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
</Base>
|
|
22
|
+
|
|
23
|
+
<style>
|
|
24
|
+
.page {
|
|
25
|
+
max-width: 640px;
|
|
26
|
+
margin: 0 auto;
|
|
27
|
+
padding: 96px 24px;
|
|
28
|
+
}
|
|
29
|
+
.code {
|
|
30
|
+
margin: 0 0 12px;
|
|
31
|
+
color: var(--brand);
|
|
32
|
+
font-family: var(--font-mono);
|
|
33
|
+
font-size: 14px;
|
|
34
|
+
}
|
|
35
|
+
h1 {
|
|
36
|
+
margin: 0 0 14px;
|
|
37
|
+
color: var(--heading);
|
|
38
|
+
font-size: 38px;
|
|
39
|
+
font-weight: 600;
|
|
40
|
+
letter-spacing: -0.025em;
|
|
41
|
+
line-height: 1.15;
|
|
42
|
+
}
|
|
43
|
+
.actions {
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-wrap: wrap;
|
|
46
|
+
gap: 12px;
|
|
47
|
+
margin-top: 28px;
|
|
48
|
+
}
|
|
49
|
+
</style>
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import config from 'virtual:casoon-pages/config';
|
|
4
|
+
import Base from '../layouts/Base.astro';
|
|
5
|
+
import { formatDate, parseChangelog } from '../lib/changelog';
|
|
6
|
+
import { t } from '../lib/i18n';
|
|
7
|
+
|
|
8
|
+
const s = t(config.lang);
|
|
9
|
+
|
|
10
|
+
const releases = parseChangelog(await readFile(config.changelog as string, 'utf8'));
|
|
11
|
+
const tone = (kind: string) =>
|
|
12
|
+
['breaking', 'removed', 'security'].includes(kind)
|
|
13
|
+
? 'brand'
|
|
14
|
+
: kind === 'fixed'
|
|
15
|
+
? 'accent'
|
|
16
|
+
: 'plain';
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
<Base title={s.changelog} description={s.changelogDescription(config.name)}>
|
|
20
|
+
<div class="page">
|
|
21
|
+
<h1>{s.changelog}</h1>
|
|
22
|
+
<p class="lead">
|
|
23
|
+
{s.changelogLeadBefore} <code>CHANGELOG.md</code> {s.changelogLeadAfter}
|
|
24
|
+
</p>
|
|
25
|
+
{
|
|
26
|
+
releases.map((release) => (
|
|
27
|
+
<section>
|
|
28
|
+
<div>
|
|
29
|
+
<h2>{release.version}</h2>
|
|
30
|
+
{release.date && <p class="date">{formatDate(release.date, s.dateLocale)}</p>}
|
|
31
|
+
</div>
|
|
32
|
+
<ul>
|
|
33
|
+
{release.changes.map((change) => (
|
|
34
|
+
<li>
|
|
35
|
+
<span class:list={['kind', tone(change.kind)]}>{change.kind}</span>
|
|
36
|
+
<span set:html={change.html} />
|
|
37
|
+
</li>
|
|
38
|
+
))}
|
|
39
|
+
</ul>
|
|
40
|
+
</section>
|
|
41
|
+
))
|
|
42
|
+
}
|
|
43
|
+
</div>
|
|
44
|
+
</Base>
|
|
45
|
+
|
|
46
|
+
<style>
|
|
47
|
+
.page {
|
|
48
|
+
max-width: 820px;
|
|
49
|
+
margin: 0 auto;
|
|
50
|
+
padding: 56px 24px 96px;
|
|
51
|
+
}
|
|
52
|
+
h1 {
|
|
53
|
+
margin: 0 0 12px;
|
|
54
|
+
color: var(--heading);
|
|
55
|
+
font-size: 38px;
|
|
56
|
+
font-weight: 600;
|
|
57
|
+
letter-spacing: -0.025em;
|
|
58
|
+
}
|
|
59
|
+
.lead {
|
|
60
|
+
margin: 0 0 44px;
|
|
61
|
+
font-size: 17.5px;
|
|
62
|
+
}
|
|
63
|
+
.lead code,
|
|
64
|
+
li :global(code) {
|
|
65
|
+
padding: 1px 6px;
|
|
66
|
+
border-radius: 5px;
|
|
67
|
+
background: var(--code-bg);
|
|
68
|
+
font-size: 0.86em;
|
|
69
|
+
}
|
|
70
|
+
li :global(a) {
|
|
71
|
+
text-decoration: underline;
|
|
72
|
+
text-underline-offset: 2px;
|
|
73
|
+
text-decoration-thickness: 1px;
|
|
74
|
+
}
|
|
75
|
+
section {
|
|
76
|
+
display: grid;
|
|
77
|
+
grid-template-columns: 150px 1fr;
|
|
78
|
+
gap: 24px;
|
|
79
|
+
padding: 26px 0;
|
|
80
|
+
border-top: 1px solid var(--border);
|
|
81
|
+
}
|
|
82
|
+
h2 {
|
|
83
|
+
margin: 0;
|
|
84
|
+
color: var(--heading);
|
|
85
|
+
font-family: var(--font-mono);
|
|
86
|
+
font-size: 15px;
|
|
87
|
+
font-weight: 500;
|
|
88
|
+
}
|
|
89
|
+
.date {
|
|
90
|
+
margin: 0;
|
|
91
|
+
color: var(--muted);
|
|
92
|
+
font-size: 13px;
|
|
93
|
+
}
|
|
94
|
+
ul {
|
|
95
|
+
margin: 0;
|
|
96
|
+
padding: 0;
|
|
97
|
+
list-style: none;
|
|
98
|
+
}
|
|
99
|
+
li {
|
|
100
|
+
display: flex;
|
|
101
|
+
align-items: baseline;
|
|
102
|
+
gap: 12px;
|
|
103
|
+
margin-bottom: 8px;
|
|
104
|
+
font-size: 14.5px;
|
|
105
|
+
}
|
|
106
|
+
.kind {
|
|
107
|
+
flex-shrink: 0;
|
|
108
|
+
min-width: 76px;
|
|
109
|
+
font-family: var(--font-mono);
|
|
110
|
+
font-size: 12px;
|
|
111
|
+
letter-spacing: 0.05em;
|
|
112
|
+
text-transform: uppercase;
|
|
113
|
+
}
|
|
114
|
+
.plain {
|
|
115
|
+
color: var(--heading);
|
|
116
|
+
}
|
|
117
|
+
.accent {
|
|
118
|
+
color: var(--accent);
|
|
119
|
+
}
|
|
120
|
+
.brand {
|
|
121
|
+
color: var(--brand);
|
|
122
|
+
}
|
|
123
|
+
@media (max-width: 560px) {
|
|
124
|
+
section {
|
|
125
|
+
grid-template-columns: 1fr;
|
|
126
|
+
gap: 12px;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
</style>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { type CollectionEntry, getCollection, render } from 'astro:content';
|
|
3
|
+
import config from 'virtual:casoon-pages/config';
|
|
4
|
+
import { components as projectComponents } from 'virtual:casoon-pages/mdx-components';
|
|
5
|
+
import * as components from '../../components/index';
|
|
6
|
+
import Docs from '../../layouts/Docs.astro';
|
|
7
|
+
import { buildDocsNav, docSlug } from '../../lib/docs';
|
|
8
|
+
|
|
9
|
+
export async function getStaticPaths() {
|
|
10
|
+
const entries = await getCollection('docs');
|
|
11
|
+
const paths = entries.map((entry) => ({ params: { slug: docSlug(entry.id) }, props: { entry } }));
|
|
12
|
+
// Without docs/index.md, /docs/ shows the first page of the sidebar.
|
|
13
|
+
if (entries.length > 0 && !entries.some((entry) => entry.id === 'index')) {
|
|
14
|
+
const first = buildDocsNav(entries, config.docsGroups)[0].items[0];
|
|
15
|
+
const entry = entries.find((e) => e.id === first.id) as CollectionEntry<'docs'>;
|
|
16
|
+
paths.push({ params: { slug: undefined }, props: { entry } });
|
|
17
|
+
}
|
|
18
|
+
return paths;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { entry } = Astro.props;
|
|
22
|
+
const { Content, headings } = await render(entry);
|
|
23
|
+
const groups = buildDocsNav(await getCollection('docs'), config.docsGroups);
|
|
24
|
+
// filePath is relative to site/; the repository root is one level up.
|
|
25
|
+
const sourcePath = (entry.filePath ?? '').replace(/^(\.\.\/)+/, '');
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
<Docs
|
|
29
|
+
id={entry.id}
|
|
30
|
+
title={entry.data.title}
|
|
31
|
+
description={entry.data.description}
|
|
32
|
+
sourcePath={sourcePath}
|
|
33
|
+
groups={groups}
|
|
34
|
+
headings={headings}
|
|
35
|
+
>
|
|
36
|
+
{/* docs/ sits outside site/ and cannot import packages; theme components are provided
|
|
37
|
+
here, and the project's own (option `mdxComponents`) on top of them. */}
|
|
38
|
+
<Content components={{ ...components, ...projectComponents }} />
|
|
39
|
+
</Docs>
|