diff --git a/build.mjs b/build.mjs
index af8e426..5fed98a 100644
--- a/build.mjs
+++ b/build.mjs
@@ -10,6 +10,7 @@ import { Marked } from 'marked';
import { docShell, esc, GITHUB_URL, highlightTokens, SITE_URL } from './src/layout.mjs';
import { renderLanding } from './src/landing.mjs';
import { renderImpressum } from './src/impressum.mjs';
+import { renderLicense } from './src/license.mjs';
const root = path.dirname(fileURLToPath(import.meta.url));
const dist = path.join(root, 'dist');
@@ -32,6 +33,15 @@ function locateDocs() {
const docsDir = locateDocs();
+// --- license: raw text from the tool repo root, next to docs/ ---------------
+
+const licensePath = path.join(docsDir, '..', 'LICENSE');
+if (!existsSync(licensePath)) {
+ console.error(`error: LICENSE not found at ${licensePath}`);
+ process.exit(1);
+}
+const licenseText = readFileSync(licensePath, 'utf8');
+
// --- version: release tag from env, else the tool repo's package.json ------
function readVersion() {
@@ -174,13 +184,21 @@ writeFileSync(path.join(dist, 'index.html'), renderLanding({ version, gitRef }))
mkdirSync(path.join(dist, 'impressum'), { recursive: true });
writeFileSync(path.join(dist, 'impressum', 'index.html'), renderImpressum({ version }));
+mkdirSync(path.join(dist, 'license'), { recursive: true });
+writeFileSync(path.join(dist, 'license', 'index.html'), renderLicense({ version, text: licenseText }));
+
for (const page of pages) {
const dir = page.slug ? path.join(dist, 'docs', page.slug) : path.join(dist, 'docs');
mkdirSync(dir, { recursive: true });
writeFileSync(path.join(dir, 'index.html'), renderDoc(page));
}
-const sitePaths = ['/', ...pages.map((p) => (p.slug ? `/docs/${p.slug}/` : '/docs/')), '/impressum/'];
+const sitePaths = [
+ '/',
+ ...pages.map((p) => (p.slug ? `/docs/${p.slug}/` : '/docs/')),
+ '/license/',
+ '/impressum/',
+];
writeFileSync(
path.join(dist, 'sitemap.xml'),
`
@@ -194,4 +212,4 @@ cpSync(path.join(root, 'public'), dist, { recursive: true });
cpSync(path.join(root, 'src', 'styles', 'site.css'), path.join(dist, 'site.css'));
cpSync(path.join(root, 'src', 'scripts', 'site.js'), path.join(dist, 'site.js'));
-console.log(`built ${pages.length + 2} pages into dist/ (flashtrace ${version || 'unknown version'})`);
+console.log(`built ${pages.length + 3} pages into dist/ (flashtrace ${version || 'unknown version'})`);
diff --git a/src/layout.mjs b/src/layout.mjs
index b97e930..b4937ce 100644
--- a/src/layout.mjs
+++ b/src/layout.mjs
@@ -75,7 +75,7 @@ function footer({ version }) {
Docs
GitHub
Discussions
- License
+ License
Legal Notice
diff --git a/src/license.mjs b/src/license.mjs
new file mode 100644
index 0000000..6fd569f
--- /dev/null
+++ b/src/license.mjs
@@ -0,0 +1,66 @@
+// License page - renders the tool repo's LICENSE (plain text, pre-formatted
+// with spaces for centering) verbatim inside the site shell. When the text is
+// recognized as Apache 2.0, a GitHub-style summary panel is shown above it;
+// an unrecognized license falls back to the raw text alone rather than
+// risking a wrong summary.
+import { esc, pageShell } from './layout.mjs';
+
+// Octicons (MIT): law, check, x, info.
+const lawIcon = ` `;
+const checkIcon = ` `;
+const xIcon = ` `;
+const infoIcon = ` `;
+
+// Summary as shown by GitHub's license banner (sourced from choosealicense.com).
+function apacheSummaryPanel() {
+ const col = (label, cls, icon, items) => `
+
${label}
+
${items.map((it) => `${icon}${esc(it)} `).join('\n')}
+
`;
+ return `
+
+${lawIcon}
+
+
flashtrace is licensed under the
+
Apache License 2.0
+
+
+A permissive license whose main conditions require preservation of copyright and license notices. Contributors provide an express grant of patent rights. Licensed works, modifications, and larger works may be distributed under different terms and without source code.
+
+${col('Permissions', 'lic-ok', checkIcon, ['Commercial use', 'Modification', 'Distribution', 'Patent use', 'Private use'])}
+${col('Limitations', 'lic-no', xIcon, ['Trademark use', 'Liability', 'Warranty'])}
+${col('Conditions', 'lic-info', infoIcon, ['License and copyright notice', 'State changes'])}
+
+This is not legal advice. Learn more about the Apache License 2.0 .
+ `;
+}
+
+// Apache 2.0 starts with "Apache License" / "Version 2.0, January 2004" as
+// its first two non-blank lines. Checking only the header (not the whole
+// text) avoids a false match on a future license that merely mentions it.
+function isApache2(text) {
+ const [l1 = '', l2 = ''] = text
+ .split(/\r?\n/, 20)
+ .map((l) => l.trim())
+ .filter(Boolean);
+ return l1 === 'Apache License' && l2.startsWith('Version 2.0');
+}
+
+export function renderLicense({ version, text }) {
+ const body = `
+
+
License
+${isApache2(text) ? `${apacheSummaryPanel()}\n
Full license text
` : ''}
+
${esc(text)}
+
+ `;
+ return pageShell({
+ title: 'License ยท flashtrace',
+ description: 'The Apache License, Version 2.0 that flashtrace is distributed under.',
+ path: '/license/',
+ version,
+ active: '',
+ body,
+ bodyClass: 'page-legal',
+ });
+}
diff --git a/src/styles/site.css b/src/styles/site.css
index dfc08cf..c42c66c 100644
--- a/src/styles/site.css
+++ b/src/styles/site.css
@@ -18,6 +18,9 @@
--tk-id: #b45309;
--tk-kw: #92400e;
--tk-arrow: #0e7490;
+ --ok: #15803d;
+ --no: #b91c1c;
+ --info: #0e7490;
--shadow: 0 1px 3px rgb(0 0 0 / 0.08), 0 8px 24px rgb(0 0 0 / 0.06);
--topbar-h: 3.5rem;
color-scheme: light;
@@ -39,6 +42,9 @@
--tk-id: #f9c21d;
--tk-kw: #e8964a;
--tk-arrow: #56b6c2;
+ --ok: #3fb950;
+ --no: #f85149;
+ --info: #56b6c2;
--shadow: 0 1px 3px rgb(0 0 0 / 0.5), 0 8px 24px rgb(0 0 0 / 0.35);
color-scheme: dark;
}
@@ -61,6 +67,9 @@
--tk-id: #f9c21d;
--tk-kw: #e8964a;
--tk-arrow: #56b6c2;
+ --ok: #3fb950;
+ --no: #f85149;
+ --info: #56b6c2;
color-scheme: dark;
}
}
@@ -993,12 +1002,150 @@ h3:hover .heading-anchor,
padding-bottom: 3.5rem;
}
+.page-legal .doc-content {
+ margin-inline: auto;
+}
+
.legal address {
font-style: normal;
line-height: 1.7;
color: var(--text);
}
+/* license: the tool repo's raw LICENSE text. Shrink-wrap the block and center
+ it so the text's own space-centered header reads as intended. */
+.license-doc {
+ max-width: 52rem;
+}
+
+.license-doc h1 {
+ text-align: center;
+}
+
+.license-doc pre {
+ width: fit-content;
+ max-width: 100%;
+ margin: 1.75rem auto 0;
+ padding: 2rem 2.5rem;
+ font-size: 0.9rem;
+}
+
+/* GitHub-style summary panel above the raw license text */
+.license-summary {
+ margin-top: 1.75rem;
+ padding: 1.5rem 1.75rem;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: var(--bg-raised);
+ box-shadow: var(--shadow);
+}
+
+.license-summary-head {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ color: var(--muted);
+}
+
+.license-summary-kicker {
+ margin: 0;
+ font-size: 0.85rem;
+ color: var(--muted);
+}
+
+.license-summary-name {
+ margin: 0;
+ font-size: 1.15rem;
+ font-weight: 650;
+ line-height: 1.3;
+ color: var(--text);
+}
+
+.license-summary-desc {
+ margin: 1rem 0 0;
+ font-size: 0.9rem;
+ color: var(--muted);
+}
+
+.license-summary-cols {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 1rem 1.5rem;
+ margin-top: 1.5rem;
+}
+
+.license-col-label {
+ margin: 0 0 0.4rem;
+ font-size: 0.75rem;
+ font-weight: 650;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.license-col ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.license-col li {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.15rem 0;
+ font-size: 0.85rem;
+}
+
+.license-col li svg {
+ flex-shrink: 0;
+}
+
+.lic-ok svg {
+ color: var(--ok);
+}
+
+.lic-no svg {
+ color: var(--no);
+}
+
+.lic-info svg {
+ color: var(--info);
+}
+
+.license-summary-note {
+ margin: 1.5rem 0 0;
+ padding-top: 1rem;
+ border-top: 1px solid var(--border);
+ font-size: 0.8rem;
+ color: var(--muted);
+}
+
+/* labeled divider between the summary panel and the raw text */
+.license-divider {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ margin: 3rem 0 0;
+ color: var(--muted);
+ font-size: 0.75rem;
+ font-weight: 650;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+}
+
+.license-divider::before,
+.license-divider::after {
+ content: '';
+ flex: 1;
+ border-top: 1px solid var(--border);
+}
+
+@media (max-width: 640px) {
+ .license-summary-cols {
+ grid-template-columns: 1fr;
+ }
+}
+
/* ---------- responsive ---------- */
@media (max-width: 1100px) {