diff --git a/examples/blog/public/favicon.png b/examples/blog/public/favicon.png index ebcc07205..db822447b 100644 Binary files a/examples/blog/public/favicon.png and b/examples/blog/public/favicon.png differ diff --git a/examples/blog/public/favicon.svg b/examples/blog/public/favicon.svg index 830c28833..89ee6a6d1 100644 --- a/examples/blog/public/favicon.svg +++ b/examples/blog/public/favicon.svg @@ -1,16 +1,23 @@ - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + diff --git a/packages/ui/AGENTS.md b/packages/ui/AGENTS.md index 2d4e5ef73..8108381fe 100644 --- a/packages/ui/AGENTS.md +++ b/packages/ui/AGENTS.md @@ -134,10 +134,10 @@ website/ the marketing site, at the repo root app/ui/registry/index.json/route.ts GET /ui/registry/index.json, flat list app/ui/registry/[name]/route.ts GET /ui/registry/.json (the CLI fetches here) modules/ui/queries/registry.server.ts composes registry JSON on demand from THIS package - components/ui/, lib/ui/ GITIGNORED mirror of ../registry/, written by scripts/copy-registry.mjs + modules/ui/components/, lib/utils/{cn,dom}.ts GITIGNORED mirror of ../registry/, written by scripts/copy-registry.mjs ``` -### ⚠️ Mirror footgun : do NOT hand-write files into the marketing site's `components/ui/` +### ⚠️ Mirror footgun : do NOT hand-write files into the marketing site's `modules/ui/components/` The marketing site is a consumer of this kit (its `/ui` pages import the components to render live previews), so `website/components/ui/` and diff --git a/scripts/generate-favicon.mjs b/scripts/generate-favicon.mjs index 2ed1b7f23..02fae955a 100644 --- a/scripts/generate-favicon.mjs +++ b/scripts/generate-favicon.mjs @@ -1,15 +1,25 @@ #!/usr/bin/env node /** - * Generates a single favicon.png (and matching SVG source) that mirrors - * the brand logo used in each app's header: a small rounded square with - * the accent-orange gradient + subtle inner highlight. + * Propagates the brand favicon to every app that serves HTML. * - * Writes into website/public, examples/blog/public, and the UI site. + * The SOURCE OF TRUTH is the authored monogram at + * website/public/brand/webjs-monogram.svg (the Velocity W on its dark tile). + * This script no longer draws a mark of its own: an earlier version inlined + * the retired square-gradient logo here, so re-running it silently reverted + * the favicon to the old brand. It now reads the brand asset, copies it out + * as each app's favicon.svg, and bakes the PNG fallbacks from the same bytes, + * so the only way to change the favicon is to change the brand asset. + * + * Writes favicon.svg + favicon.png (512) into website/public and + * examples/blog/public, plus favicon.ico (16+32+48) for the website, which + * serves it at the origin root for crawlers that read no markup. + * Requires ImageMagick for the .ico. * * node scripts/generate-favicon.mjs */ import puppeteer from 'puppeteer-core'; -import { writeFile } from 'node:fs/promises'; +import { execFileSync } from 'node:child_process'; +import { writeFile, readFile } from 'node:fs/promises'; import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -25,31 +35,9 @@ const APPS = [ resolve(root, 'examples/blog/public'), ]; -// SVG that matches the header logo mark in website/app/layout.ts: a rounded -// square with the accent-orange gradient (--logo-from to --logo-to) plus a -// subtle inner highlight ring. 512x512 so it down-scales cleanly to any size. -// -// The mark is theme-adaptive, exactly like the navbar: the default stops are -// the LIGHT-theme --logo-from/--logo-to, and an embedded -// @media (prefers-color-scheme: dark) swaps in the DARK-theme stops. This is -// the standards-based way to ship one favicon that reads on both light and -// dark browser chrome (a single SVG whose own style adapts, no second file). -const svg = ` - - - - - - - - - -`; +// The authored mark. Opaque dark tile, so it reads on light and dark browser +// chrome alike with no media-query variant. +const svg = await readFile(resolve(root, 'website/public/brand/webjs-monogram.svg'), 'utf8'); const browser = await puppeteer.launch({ executablePath: process.env.CHROMIUM_PATH || '/usr/bin/chromium', @@ -57,13 +45,8 @@ const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'], }); const page = await browser.newPage(); -// Bake the single PNG fallback from the DARK-theme stops: a raster cannot -// carry the @media swap, and the bright dark-navbar orange reads on both -// light and dark tab bars. Emulate dark so the SVG's own media query resolves -// to the dark stops before we screenshot it. -await page.emulateMediaFeatures([{ name: 'prefers-color-scheme', value: 'dark' }]); await page.setViewport({ width: 512, height: 512, deviceScaleFactor: 1 }); -await page.setContent(`${svg}`, { waitUntil: 'load' }); +await page.setContent(`${svg.replace('viewBox="0 0 120 120"', 'viewBox="0 0 120 120" width="512" height="512"')}`, { waitUntil: 'load' }); const png = await page.screenshot({ type: 'png', omitBackground: true }); await browser.close(); @@ -72,3 +55,12 @@ for (const pub of APPS) { await writeFile(resolve(pub, 'favicon.png'), png); console.log('wrote', pub + '/favicon.{svg,png}', `(png: ${Math.round(png.length / 1024)} kB)`); } + +// The .ico multi-resolution fallback, website only (it serves /favicon.ico at +// the origin root). ImageMagick downscales the 512 PNG; -background none +// keeps the tile's rounded corners transparent. +const site = resolve(root, 'website/public'); +await writeFile(resolve(site, '.favicon-512.tmp.png'), png); +execFileSync('magick', [resolve(site, '.favicon-512.tmp.png'), '-background', 'none', '-define', 'icon:auto-resize=48,32,16', resolve(site, 'favicon.ico')]); +execFileSync('rm', [resolve(site, '.favicon-512.tmp.png')]); +console.log('wrote', site + '/favicon.ico (48+32+16)'); diff --git a/website/.gitignore b/website/.gitignore index 1830719fe..139c68baa 100644 --- a/website/.gitignore +++ b/website/.gitignore @@ -4,5 +4,6 @@ # import the components for live previews. Hand-written site components # and lib modules live directly in components/*.ts and lib/*.ts, which # stay tracked; only these generated subdirectories are ignored. -/components/ui/ -/lib/ui/ +/modules/ui/components/ +/lib/utils/cn.ts +/lib/utils/dom.ts diff --git a/website/AGENTS.md b/website/AGENTS.md index 5ccc4fd7b..b8531e23c 100644 --- a/website/AGENTS.md +++ b/website/AGENTS.md @@ -38,7 +38,7 @@ website/ docs/ /docs/, the reference documentation (#1098 moved it here from docs.webjs.dev). layout.ts holds the nav tree + docs-scoped metadata; the shell itself is shared, - see lib/docs-shell.ts. + see lib/ui/docs-shell.ts. ui/ /ui, the @webjsdev/ui component gallery (#1099 moved it here from ui.webjs.dev). page.ts is the introduction, [name]/page.ts one page per component, layout.ts the @@ -54,21 +54,40 @@ website/ copy-cmd.ts click-to-copy command line (light DOM, always-on button) doc-search.ts the docs sidebar search field preview-tabs.ts Preview / Code toggle around a gallery demo - ui/ GITIGNORED mirror of the @webjsdev/ui registry sources, - written by scripts/copy-registry.mjs. NEVER hand-write - here: it is wiped every dev cycle and never reaches the - deploy. Hand-written components go in components/ itself. + (components/ui/ is intentionally EMPTY here, left free + for `webjs ui add` to own, exactly as the scaffold + expects. The gallery's preview copies live in + modules/ui/components/ instead, see below.) lib/ - highlight.ts SSR syntax highlighter for the code samples - frontmatter.ts parse changelog/blog markdown frontmatter - faq.ts parse a `## FAQ` markdown section into FAQPage JSON-LD - docs-shell.ts the sidebar + drawer + .prose-docs typography, SHARED by + design/ the design system, one subsystem in one folder + recipes.ts class recipes + the scale (BTN_*, EYEBROW, layout widths) + brand.ts the logo lockup and monogram fragments + tokens.ts the palette as data, painted by /brand + ui/ composed page fragments, one per file. SSR-time functions + returning `html`; nothing here registers a custom element + page-header.ts hub eyebrow + title + lede + cta-panel.ts the closing call to action + site-footer.ts the footer, rendered by the root layout on every page + docs-shell.ts the sidebar + drawer + .prose-docs typography, SHARED by /docs and /ui so the two sections cannot drift apart - docs-llms.server.ts enumerates the doc pages on disk (sitemap, llms.txt) + utils/ pure helpers (compute, never render) + highlight.ts SSR syntax highlighter for the code samples + frontmatter.ts parse changelog/blog markdown frontmatter + faq.ts parse a `## FAQ` markdown section into FAQPage JSON-LD + cn.ts dom.ts GITIGNORED. The kit's helpers, mirrored to the exact + path `webjs ui add` writes them to in a real app links.ts cross-app URLs + in-app paths for the header and footer - site-footer.ts the footer, rendered by the root layout on every page - ui/ GITIGNORED, same as components/ui/ (the kit's cn helper) + samples.ts the code samples shown on the marketing pages + docs-llms.server.ts enumerates the doc pages on disk (sitemap, llms.txt) modules/ + ui/components/ GITIGNORED mirror of the @webjsdev/ui registry sources, + written by scripts/copy-registry.mjs. NEVER hand-write + here: it is wiped every dev cycle. It lives under the + gallery's own module rather than in components/ui/ + because it is gallery INFRASTRUCTURE (live previews), + not this site's UI kit. That keeps components/ui/ free + so `webjs ui add` works here the same way it does in a + scaffolded app. ui/queries/registry.server.ts composes the registry JSON on demand from packages/ui/packages/registry/ (the source of truth). This is what /ui/registry/** serves. @@ -76,13 +95,25 @@ website/ scripts/ manual dev tools, NOT part of build/deploy fetch-fonts.mjs download the self-hosted variable woff2 fonts generate-og.mjs regenerate the OG social card (needs playwright + ImageMagick) - copy-registry.mjs mirror the kit sources into components/ui/ + lib/ui/. + copy-registry.mjs mirror the kit sources into modules/ui/components/ + + lib/utils/{cn,dom}.ts. Runs via webjs.dev.before / webjs.start.before and is baked into the deploy image (#526), so a component page never boots without its imports. public/ favicon, og image, self-hosted fonts, static assets ``` +## How lib/ grows + +The scaffold starts flat (a lone `lib/utils/ui.ts` and nothing else), and this +site follows the same rule it grew by: a file stays loose at `lib/*.ts` while +it is a standalone app-wide value, and a SUBSYSTEM gets its own `lib//` +folder once it reaches about three related files. The design system +(`lib/design/`) and the composed page fragments (`lib/ui/`) both crossed that +bar; `links.ts` and `samples.ts` have not, so they stay loose. Fragments in +`lib/ui/` are one file per fragment for the same reason the framework keeps +one action per file: the filename is the index. + The site is intentionally one page in long-form scroll. When you edit copy, find the section in `app/page.ts` (search for the visible text that needs to change) and update inline. @@ -117,7 +148,7 @@ is split by editorial intent, which is what decides where a piece goes: an article owns the general term, a blog post owns the WebJs-specific angle. - **FAQ convention.** End an article or comparison body with a `## FAQ` section, each question a `### ` heading followed by its answer - paragraph. `lib/faq.ts` (`parseFaq`) turns that into a `FAQPage` JSON-LD + paragraph. `lib/utils/faq.ts` (`parseFaq`) turns that into a `FAQPage` JSON-LD block. The FAQ is BOTH rendered (normal markdown) and emitted as schema, so the two never drift (Google discounts FAQ schema that is not visible on the page). Blog posts do NOT use FAQ. @@ -185,10 +216,7 @@ the stylesheet against a stale mirror silently omits the utility classes the gallery previews need. In prod, `npm start` and `webjs start` are equivalent too, via `webjs.start.before`. -Set `EXAMPLE_BLOG_URL` to point the "Demo" link at the live example-blog app -when deploying; locally, `.env` in this directory sets it to the sibling app's -localhost port. Everything else in the nav is an in-app route and needs no env -var: Blog, Changelog, Docs, and the UI gallery. (Docs and the gallery used to +Every nav entry is an in-app route and needs no env var: Blog, Changelog, Docs, and the UI gallery. (Docs and the gallery used to need one each. They were separate `docs.webjs.dev` and `ui.webjs.dev` apps until #1098 and #1099 moved them here under `app/docs/` and `app/ui/`, so `DOCS_URL` and `UI_URL` are both gone.) diff --git a/website/app/articles/[slug]/page.ts b/website/app/articles/[slug]/page.ts index 80c95292b..aa703f988 100644 --- a/website/app/articles/[slug]/page.ts +++ b/website/app/articles/[slug]/page.ts @@ -1,7 +1,7 @@ import { html, unsafeHTML, notFound } from '@webjsdev/core'; import { getArticle } from '#modules/articles/queries/get-article.server.ts'; import { renderPostBody } from '#modules/blog/utils/render-post.ts'; -import { parseFaq, faqJsonLd } from '#lib/faq.ts'; +import { parseFaq, faqJsonLd } from '#lib/utils/faq.ts'; /** * /articles/[slug] @@ -77,21 +77,21 @@ export default async function ArticlePage({ params }: { params: { slug: string } if (!a) notFound(); return html` -
-