Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
db243b5
feat(website): near-monochrome design system and a live-component hero
vivek7405 Jul 29, 2026
964963e
refactor(website): share one set of control recipes across the market…
vivek7405 Jul 29, 2026
8804fe4
feat(website): real brand assets and a rebuilt /brand
vivek7405 Jul 29, 2026
32e1fde
feat(website): put the lockup in the footer and link /brand
vivek7405 Jul 29, 2026
5466940
fix(website): regenerate the raster icons from the new mark
vivek7405 Jul 29, 2026
f5b9ac5
fix(website): restore the original Velocity lockup, greyscale
vivek7405 Jul 29, 2026
c155740
docs(website): correct /brand copy that contradicted the greyscale marks
vivek7405 Jul 29, 2026
c9a51d4
feat(website): bring back the amber primary action and the glowing CTA
vivek7405 Jul 29, 2026
e65acb8
fix(website): restore the live dark palette and document the accent o…
vivek7405 Jul 29, 2026
103e717
fix(website): lift the dark accent and glow off brown, restore the co…
vivek7405 Jul 29, 2026
d847fa7
fix(website): make the dark accent vivid rather than pale
vivek7405 Jul 29, 2026
39b93d5
fix(website): drop the dark backdrop tint to near black
vivek7405 Jul 29, 2026
3d35594
fix(website): put the install bar on the control radius
vivek7405 Jul 29, 2026
2fd9d79
fix(website): remove the dark backdrop tint
vivek7405 Jul 29, 2026
e52a609
feat(website): stage flip, monogram light variant, and hover-surface …
vivek7405 Jul 29, 2026
f63e1a5
feat(website): make the buttons full pills
vivek7405 Jul 29, 2026
dd16b3b
refactor(website): compose the shared page furniture instead of repea…
vivek7405 Jul 29, 2026
8cc269a
refactor(website): put the site on Tailwind's scale, per Refactoring UI
vivek7405 Jul 30, 2026
71e0879
refactor(website): name the container measures instead of inlining them
vivek7405 Jul 30, 2026
e6ef4d6
refactor(website): use Tailwind's container scale, drop the custom one
vivek7405 Jul 30, 2026
d4fbac6
refactor(website): drop the last custom type token for Tailwind's own
vivek7405 Jul 30, 2026
0f9e1be
revert(website): restore the long-form column to 840px
vivek7405 Jul 30, 2026
af2f8ac
feat(website): give every hub the amber eyebrow the changelog had
vivek7405 Jul 30, 2026
4388591
feat(website): shrink the tag chips and make them one recipe
vivek7405 Jul 30, 2026
2c55e97
refactor(website): no arbitrary values left in our own markup
vivek7405 Jul 30, 2026
4e83715
refactor(website): remove the inline clamps and rem sizes the px audi…
vivek7405 Jul 30, 2026
73efe2f
refactor(website): move the browser-safe helpers into lib/utils
vivek7405 Jul 30, 2026
e16c8d9
refactor(website): split page composition out of the recipe module
vivek7405 Jul 30, 2026
960c790
refactor(website): one shared page fragment per file
vivek7405 Jul 30, 2026
4d2f761
refactor(website): follow the scaffold's layout, and free components/ui
vivek7405 Jul 30, 2026
a7b12ab
refactor(website): name the lib/ subsystems instead of encoding kind …
vivek7405 Jul 30, 2026
8f38973
fix(website): design-review findings, the serif face and the OG card
vivek7405 Jul 30, 2026
46fae16
docs(website): drop semicolon pauses from /brand prose and the fragme…
vivek7405 Jul 30, 2026
cfff0a4
fix(website): first batch of deep-review findings
vivek7405 Jul 30, 2026
e1d3479
fix(website): review round 1, asset and infrastructure findings
vivek7405 Jul 30, 2026
a75d47f
fix(website): long-form prose reads at full foreground
vivek7405 Jul 30, 2026
a47005a
fix(website): scope the prose contrast bump to dark only
vivek7405 Jul 30, 2026
dfecfdd
Revert "fix(website): scope the prose contrast bump to dark only"
vivek7405 Jul 30, 2026
a48df58
fix(website): dark editor pane, chip contrast, footer alignment, drop…
vivek7405 Jul 30, 2026
c295e11
fix(website): optically center the W in the monogram tile
vivek7405 Jul 30, 2026
48ae64f
fix(website): fill the monogram tile more evenly
vivek7405 Jul 30, 2026
613cac4
Revert "fix(website): fill the monogram tile more evenly"
vivek7405 Jul 30, 2026
30207b4
Revert "fix(website): optically center the W in the monogram tile"
vivek7405 Jul 30, 2026
56e618c
fix(brand): seat the monogram W on its lean, not the bbox centre
vivek7405 Jul 30, 2026
1c14843
fix(brand): centre the monogram W vertically, keep its horizontal lean
vivek7405 Jul 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified examples/blog/public/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 23 additions & 16 deletions examples/blog/public/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions packages/ui/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<name>.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
Expand Down
64 changes: 28 additions & 36 deletions scripts/generate-favicon.mjs
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -25,45 +35,18 @@ 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 = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop class="from" offset="0%" stop-color="oklch(0.63 0.17 50)"/>
<stop class="to" offset="100%" stop-color="oklch(0.44 0.11 52)"/>
</linearGradient>
<style>
@media (prefers-color-scheme: dark) {
.from { stop-color: oklch(0.8 0.16 58); }
.to { stop-color: oklch(0.62 0.18 44); }
}
</style>
</defs>
<rect x="32" y="32" width="448" height="448" rx="142" ry="142" fill="url(#g)"/>
<rect x="32" y="32" width="448" height="448" rx="142" ry="142" fill="none" stroke="oklch(1 0 0 / 0.15)" stroke-width="6"/>
</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',
headless: true,
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(`<!doctype html><html><body style="margin:0;background:transparent">${svg}</body></html>`, { waitUntil: 'load' });
await page.setContent(`<!doctype html><html><body style="margin:0;background:transparent">${svg.replace('viewBox="0 0 120 120"', 'viewBox="0 0 120 120" width="512" height="512"')}</body></html>`, { waitUntil: 'load' });
const png = await page.screenshot({ type: 'png', omitBackground: true });
await browser.close();

Expand All @@ -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)');
5 changes: 3 additions & 2 deletions website/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
64 changes: 46 additions & 18 deletions website/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ website/
docs/ /docs/<topic>, 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
Expand All @@ -54,35 +54,66 @@ 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.
ui/utils/ tier classification, per-component examples + API metadata
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/<name>/`
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.
Expand Down Expand Up @@ -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 `### <question>` 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.
Expand Down Expand Up @@ -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.)
Expand Down
22 changes: 11 additions & 11 deletions website/app/articles/[slug]/page.ts
Original file line number Diff line number Diff line change
@@ -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]
Expand Down Expand Up @@ -77,21 +77,21 @@ export default async function ArticlePage({ params }: { params: { slug: string }
if (!a) notFound();

return html`
<main id="main" tabindex="-1" class="max-w-[840px] mx-auto px-[24px] py-[64px] focus:outline-none">
<nav class="mb-[48px]">
<a href="/articles" class="font-mono text-[12px] text-fg-subtle no-underline hover:text-fg tracking-wide">← All articles</a>
<main id="main" tabindex="-1" class="max-w-210 mx-auto px-6 py-16 focus:outline-none">
<nav class="mb-12">
<a href="/articles" class="font-mono text-xs text-fg-subtle no-underline hover:text-fg tracking-wide">← All articles</a>
</nav>

<header class="mb-[64px]">
<h1 class="font-serif text-[clamp(36px,6vw,56px)] leading-[1.05] tracking-tight text-fg m-0 mb-[24px]">${a.title}</h1>
<p class="text-fg-muted text-[19px] leading-[1.55] m-0 font-serif italic">${a.tagline}</p>
<header class="mb-16">
<h1 class="font-serif text-hero leading-[1.05] tracking-tight text-fg m-0 mb-6">${a.title}</h1>
<p class="text-fg-muted text-lg leading-[1.55] m-0 font-serif italic">${a.tagline}</p>
</header>

<article class="mt-[16px]">${unsafeHTML(renderPostBody(a.body))}</article>
<article class="mt-4">${unsafeHTML(renderPostBody(a.body))}</article>

<footer class="mt-[104px] pt-[36px] border-t border-border flex flex-wrap gap-x-[24px] gap-y-[12px]">
<a href="/articles" class="font-mono text-[12px] text-fg-subtle no-underline hover:text-fg tracking-wide">← All articles</a>
<a href="/compare" class="font-mono text-[12px] text-fg-subtle no-underline hover:text-fg tracking-wide">Compare WebJs →</a>
<footer class="mt-26 pt-9 border-t border-border flex flex-wrap gap-x-6 gap-y-3">
<a href="/articles" class="font-mono text-xs text-fg-subtle no-underline hover:text-fg tracking-wide">← All articles</a>
<a href="/compare" class="font-mono text-xs text-fg-subtle no-underline hover:text-fg tracking-wide">Compare WebJs →</a>
</footer>
</main>
`;
Expand Down
Loading
Loading