From 922ca197df5dd4edf46ef7d4ad42df95f3fd88a6 Mon Sep 17 00:00:00 2001 From: Will Eastcott Date: Fri, 12 Jun 2026 12:01:27 +0100 Subject: [PATCH 1/2] Redesign landing page to match TypeDoc 0.28 look and feel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The landing page hand-mimicked an old (~v0.22-era) TypeDoc theme and had drifted visually from the TypeDoc 0.28 output of the product references. Rather than duplicating theme CSS again, the page now reuses the actual generated TypeDoc stylesheet (copied to docs/assets/ at build time) plus a small landing.css, so it restyles itself automatically when product repos bump TypeDoc. Landing page: - Mirrors TypeDoc 0.28 markup (toolbar, container grid, sidebar with Settings accordion, footer) and tri-state os/light/dark theme that never writes to localStorage on load (the old page force-wrote 'dark', overriding the user's OS preference across all doc pages) - Cards grouped into "PlayCanvas Development" (Engine, Editor, React, Web Components) and "Foundational Libraries" (PCUI, PCUI Graph, Observer, Splat Transform), with build-injected version badges - Sidebar nav mirrors the card grid so they cannot drift apart - Fixes: footer now shows the real build date (was client-side new Date(), i.e. the visitor's date); mobile menu button works (reuses TypeDoc's has-menu drawer); removed the card-filter search Build pipeline (build.mjs): - generateLandingPage() injects {{BUILD_DATE}} and {{VERSION:*}} tokens from each cloned repo's package.json - copySharedAssets() copies TypeDoc's style.css/icons.svg for the landing page to share - postProcessProductDocs() makes the route back to the landing page obvious on every product page: toolbar title becomes a breadcrumb (Home / ) and the sidebar "Home" link is renamed to "← All API References" - New --landing-only flag (npm run build:landing) regenerates just the landing page against an existing docs/ for fast local iteration Co-Authored-By: Claude Fable 5 --- assets/landing.css | 101 +++++ build.mjs | 212 +++++++-- index.html | 1029 +++++++------------------------------------- package.json | 1 + 4 files changed, 433 insertions(+), 910 deletions(-) create mode 100644 assets/landing.css diff --git a/assets/landing.css b/assets/landing.css new file mode 100644 index 0000000..68846fc --- /dev/null +++ b/assets/landing.css @@ -0,0 +1,101 @@ +/* + * Landing page styles, layered on top of TypeDoc's generated style.css + * (copied to assets/style.css at build time). TypeDoc wraps its rules in + * @layer typedoc, so these unlayered rules win without specificity hacks. + * Only TypeDoc's unprefixed --color-* variables are used, so the landing + * page tracks the docs' light/dark/os theming automatically. + */ + +.landing-cards { + list-style: none; + margin: 1rem 0 0; + padding: 0; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1rem; +} + +.landing-card { + display: flex; + flex-direction: column; + padding: 1rem; + background-color: var(--color-background-secondary); + border: 1px solid var(--color-accent); + border-radius: 0.5rem; +} + +.landing-card h3 { + margin: 0 0 0.75rem; + font-size: 1rem; +} + +.landing-card-version { + display: inline-block; + margin-left: 0.25rem; + padding: 0 0.4rem; + border: 1px solid var(--color-accent); + border-radius: 0.25rem; + color: var(--color-text-aside); + font-size: 0.75rem; + font-weight: 400; + line-height: 1.25rem; + vertical-align: middle; + white-space: nowrap; +} + +.landing-card p { + flex: 1; + margin: 0 0 1rem; + color: var(--color-text-aside); + font-size: 0.875rem; +} + +.landing-card-links { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.landing-link { + display: inline-block; + padding: 0.375rem 0.75rem; + border: 1px solid transparent; + border-radius: 0.25rem; + background-color: var(--color-link); + color: var(--color-background); + font-size: 0.875rem; +} + +.landing-link:hover { + filter: brightness(1.1); +} + +.landing-link.legacy { + background-color: transparent; + border-color: var(--color-accent); + color: var(--color-text-aside); +} + +/* Single ↗ indicator in the button's text color; suppress TypeDoc's + black/white a.external[target="_blank"] background icon */ +.landing-link.external { + background-image: none; + padding-right: 0.75rem; +} + +.landing-link.external::after { + content: " ↗"; +} + +.landing-nav-category { + margin: 1rem 0 0.25rem; + color: var(--color-text-aside); + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.site-menu .landing-nav-category:first-child { + margin-top: 0; +} diff --git a/build.mjs b/build.mjs index fd38743..51c8700 100644 --- a/build.mjs +++ b/build.mjs @@ -25,9 +25,12 @@ try { */ function parseBranchArgs() { const args = process.argv.slice(2); - + // Process arguments in the format: repo=branch (e.g., engine=dev) for (const arg of args) { + if (arg === '--landing-only') { + continue; + } const match = arg.match(/^([^=]+)=(.+)$/); if (match) { const [, repoName, branchName] = match; @@ -96,6 +99,149 @@ function copyDirContents(src, dest) { } } +/** + * Read the version of each cloned repository from its package.json + */ +function getRepoVersions() { + const versions = {}; + + for (const repo of REPOS) { + const packagePath = path.join('repos', repo.name, 'package.json'); + try { + const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8')); + versions[repo.name] = packageJson.version || ''; + } catch (error) { + console.warn(`Warning: Could not read version for ${repo.name}: ${error.message}`); + versions[repo.name] = ''; + } + } + + return versions; +} + +/** + * Generate the landing page from the index.html template, injecting the + * build date and per-repository version numbers + */ +function generateLandingPage(versions) { + const sourceIndexPath = path.join(__dirname, 'index.html'); + if (!fs.existsSync(sourceIndexPath)) { + throw new Error(`Source index.html not found: ${sourceIndexPath}`); + } + + let html = fs.readFileSync(sourceIndexPath, 'utf8'); + + const buildDate = new Date().toLocaleDateString('en-US', { + year: 'numeric', month: 'long', day: 'numeric' + }); + html = html.replace(/\{\{BUILD_DATE\}\}/g, buildDate); + + html = html.replace(/v\{\{VERSION:([\w-]+)\}\}/g, (match, name) => { + return versions[name] ? `v${versions[name]}` : ''; + }); + + // Remove version badges left empty by missing versions + html = html.replace(/\s*\s*<\/span>/g, ''); + + fs.writeFileSync(path.join('docs', 'index.html'), html); + console.log('Generated landing page with build date and versions'); +} + +/** + * Copy TypeDoc's generated stylesheet and icons into docs/assets so the + * landing page shares the exact theme of the product references + */ +function copySharedAssets() { + const engineAssets = path.join('docs', 'engine', 'assets'); + const stylePath = path.join(engineAssets, 'style.css'); + if (!fs.existsSync(stylePath)) { + throw new Error(`TypeDoc stylesheet not found: ${stylePath}. Run a full build first.`); + } + + ensureDir(path.join('docs', 'assets')); + fs.copyFileSync(stylePath, path.join('docs', 'assets', 'style.css')); + fs.copyFileSync(path.join(engineAssets, 'icons.svg'), path.join('docs', 'assets', 'icons.svg')); + console.log('Copied shared TypeDoc assets (style.css, icons.svg)'); +} + +/** + * Post-process the generated TypeDoc pages so the way back to the landing + * page is obvious: turn the toolbar title into a breadcrumb (PlayCanvas / + * ) and rename the sidebar "Home" link. All replacements are + * idempotent so this can re-run over already-processed docs. + */ +function postProcessProductDocs() { + const breadcrumb = 'Home'; + const breadcrumbCss = ` +/* api-reference: toolbar breadcrumb back to the landing page */ +.tsd-toolbar-contents > .title-home { + font-weight: bold; + white-space: nowrap; +} +.tsd-toolbar-contents > .title-sep { + margin: 0 0.5rem; + color: var(--color-text-aside); +} +@media (max-width: 769px) { + .tsd-toolbar-contents > .title-home, + .tsd-toolbar-contents > .title-sep { + display: none; + } +} +`; + + const processHtml = (file) => { + const html = fs.readFileSync(file, 'utf8'); + if (html.includes('class="title-home"')) { + return false; + } + + const updated = html + .replace(//, match => breadcrumb + match) + .replace( + '