Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions website/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,10 @@
/modules/ui/components/
/lib/utils/cn.ts
/lib/utils/dom.ts

# components/ui/ belongs to `webjs ui add`, exactly as it does in a scaffolded
# app, so this site keeps it empty and tracks nothing there. It is ignored
# rather than merely left alone because it has already been committed by
# accident once: eleven byte-identical copies of the generated mirror above
# landed here and sat unimported until they were noticed.
/components/ui/
35 changes: 26 additions & 9 deletions website/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,12 @@ website/
preview-tabs.ts Preview / Code toggle around a gallery demo
(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.)
expects, and gitignored so it stays that way. The
gallery's preview copies live in modules/ui/components/
instead, see below. Enforced by
test/ssr/kit-surfaces.test.ts, because eleven copies of
that mirror were once committed here by accident and
sat unimported until someone read the directory.)
lib/
design/ the design system, one subsystem in one folder
recipes.ts class recipes + the scale (BTN_*, EYEBROW, layout widths)
Expand Down Expand Up @@ -163,14 +167,17 @@ is split by editorial intent, which is what decides where a piece goes:
live content queries, so a new article, comparison, or post needs no
edit to those files.

## Announcement banner
## Header

The layout (`app/layout.ts`) renders a top-of-page announcement strip
just above the sticky header: a small utility-class `<div>` with a "New"
badge and a link (currently the `UI_PATH` link, "Introducing the AI-first
component library"). To swap the announcement, edit that `<div>` (its copy
and the link `href`). The banner shows on every page. Remove the `<div>`
to hide it.
`app/layout.ts` renders the site header on every page. It is
`position: fixed`, NOT sticky: a sticky header flickers on iOS WebKit during
a client-router navigation (#610), so the height is reserved on the content
through a `--header-h` offset, measured by the inline script in the layout
head and defaulted on `:root` for no-JS and first paint.

There is no announcement banner. One used to sit above the header, and
`--header-h` is why its removal is not free: re-adding a strip means the
measurement has to cover it too.

## How to update headline / hero copy

Expand All @@ -197,6 +204,16 @@ calling an action), re-enable it and delete the assertion in

- Light DOM, Tailwind utilities, `@theme` tokens from the root layout
(same palette / type scale as the blog and docs).
- **Each per-theme colour is declared ONCE, as `light-dark(LIGHT, DARK)`**,
and the three `color-scheme` declarations in `app/layout.ts` pick the side
(the default follows the OS; the toggle's `[data-theme]` forces one). This
is the rule the framework teaches its own users, in the skill at
`.agents/skills/webjs/references/styling.md`. A token that is NOT a colour
(`--glow-strength`, `--cta-mix`, `--shadow-spread`) cannot ride
`light-dark()`, so it keeps an explicit override pair: the OS media query
plus the attribute rule. Nothing else may. The same rule governs the
`.ui-preview` kit palette in `public/input.css`. Both are pinned by
`test/ssr/design-tokens.test.ts` and `test/ssr/kit-surfaces.test.ts`.
- Each section in `page.ts` is a `<section>` wrapper for predictable
scroll anchors.
- **Code blocks follow three accessibility rules.** `test/ssr/pre-block-a11y.test.ts`
Expand Down
2 changes: 1 addition & 1 deletion website/app/docs/components/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ interface Student { name: string; gpa: number; }
class UserCard extends WebComponent({
count: prop(Number, { reflect: true }), // reflect to the attribute
label: prop(String, { attribute: 'aria-label' }), // renamed attribute
open: prop({ state: true }), // internal, no attribute
open: prop({ state: true }), // internal, no attribute (lit parity; prefer a signal)
student: prop&lt;Student&gt;(Object), // narrowed object type
tags: prop&lt;string[]&gt;(Array), // array-typed: pass Array, not Object
size: prop&lt;'sm' | 'lg'&gt;(String), // narrowed enum type
Expand Down
134 changes: 70 additions & 64 deletions website/app/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,83 +244,89 @@ export default function RootLayout({ children }: LayoutProps) {
CTA, plus live and focus state. It never tints a content panel or a
heading, which is what keeps those few amber surfaces meaningful
instead of decorative. */
/* Every per-theme COLOUR is declared ONCE, as light-dark(LIGHT, DARK),
and the color-scheme declarations below pick the side. This is the
rule the framework teaches its own users (the skill's
references/styling.md, and the palette webjs create generates), and
the site had been the counter-example: the dark half was written
twice, once under the OS media query and once under the toggle's
attribute, so an edit to either block silently drifted the two paths
apart. There is no cascade trick here, just the three color-scheme
declarations at the bottom of this block feeding the one function. */
:root {
color-scheme: light dark;
--fg: oklch(0.20 0.018 60);
--fg-muted: oklch(0.44 0.02 60);
--fg-subtle: oklch(0.50 0.02 65);
--bg: oklch(0.985 0.008 75);
--bg-elev: oklch(1 0 0);
--bg-subtle: oklch(0.96 0.008 75);
--bg-sunken: oklch(0.93 0.01 70);
--border: oklch(0.88 0.012 70 / 0.9);
--border-strong: oklch(0.78 0.014 70 / 0.95);
--accent: oklch(0.54 0.16 52);
--accent-hover: oklch(0.5 0.16 52);
--accent-fg: oklch(1 0 0);
--heart: oklch(0.64 0.22 6);
--accent-live: oklch(0.63 0.17 50);
--glow-a: oklch(0.63 0.17 44);
--accent-tint: color-mix(in oklch, var(--accent-live) 14%, transparent);
--logo-from: oklch(0.63 0.17 50);
--logo-to: oklch(0.44 0.11 52);
--fg: light-dark(oklch(0.20 0.018 60), oklch(0.96 0.01 60));
--fg-muted: light-dark(oklch(0.44 0.02 60), oklch(0.78 0.015 60));
--fg-subtle: light-dark(oklch(0.50 0.02 65), oklch(0.66 0.02 65));
--bg: light-dark(oklch(0.985 0.008 75), oklch(0.08 0.012 60));
--bg-elev: light-dark(oklch(1 0 0), oklch(0.14 0.015 60));
--bg-subtle: light-dark(oklch(0.96 0.008 75), oklch(0.11 0.014 60));
--bg-sunken: light-dark(oklch(0.93 0.01 70), oklch(0.06 0.01 60));
--border: light-dark(oklch(0.88 0.012 70 / 0.9), oklch(0.24 0.015 60 / 0.9));
--border-strong: light-dark(oklch(0.78 0.014 70 / 0.95), oklch(0.36 0.02 60 / 0.95));
--accent: light-dark(oklch(0.54 0.16 52), oklch(0.78 0.18 58));
--accent-hover: light-dark(oklch(0.5 0.16 52), oklch(0.83 0.17 58));
--accent-fg: light-dark(oklch(1 0 0), oklch(0 0 0));
--heart: light-dark(oklch(0.64 0.22 6), oklch(0.74 0.18 6));
--accent-live: light-dark(oklch(0.63 0.17 50), oklch(0.78 0.18 58));
--glow-a: light-dark(oklch(0.63 0.17 44), oklch(0.78 0.18 58));
--logo-from: light-dark(oklch(0.63 0.17 50), oklch(0.82 0.17 58));
--logo-to: light-dark(oklch(0.44 0.11 52), oklch(0.64 0.18 44));
/* Hover lift for nav links and other bare targets. An ALPHA overlay,
not a solid colour, for two reasons. It composes over the header's
translucent blurred background instead of fighting it, and it gives
the same perceived step in both themes: the solid --bg-subtle was a
0.09 lift on a black page, which is real in numbers and invisible to
the eye, while the same token in light was a 0.025 step that read
clearly because the eye is adapted to a bright field. */
--hover-surface: light-dark(oklch(0 0 0 / 0.055), oklch(1 0 0 / 0.09));

/* Derived tokens. Each one reads a light-dark() token above, so it
tracks BOTH themes with no override of its own. */
--accent-text: var(--accent);
--accent-tint: color-mix(in oklch, var(--accent-live) 14%, transparent);
--accent-surface: color-mix(in oklch, var(--accent-live) 12%, transparent);
--accent-border: color-mix(in oklch, var(--accent-live) 28%, transparent);
--shadow-glow: 0 0 0 1px var(--accent-tint), 0 14px 50px color-mix(in oklch, var(--accent-live) 18%, transparent);
/* The closing CTA's fill. Light keeps a faint accent tint, which
separates the panel from the page. Dark pulls the tint back toward
the plain elevated surface: over black the same amount went muddy
rather than warm, so the glow does more of the work there. The two
sides differ by the MIX RATIO, which is a number rather than a
colour, so the ratio is the token that varies. */
--cta-mix: 7%;
--cta-surface: color-mix(in oklch, var(--accent-live) var(--cta-mix), var(--bg-elev));

/* Shadows. --shadow-sm keeps the same geometry in both themes, so its
colour is the only per-theme part and rides light-dark() inline.
--shadow's SPREAD also changes (a wider, softer cast reads as depth
over black, where the light spread disappears), and light-dark() is
colour-only, so the spread is its own token in the block below. */
--shadow-sm: 0 1px 2px light-dark(oklch(0.5 0.06 55 / 0.08), oklch(0 0 0 / 0.4));
--shadow-cast: light-dark(oklch(0.5 0.08 55 / 0.10), oklch(0 0 0 / 0.5));
--shadow-ambient: light-dark(oklch(0.5 0.06 55 / 0.06), oklch(0 0 0 / 0.35));
--shadow-spread: 0 8px 30px;
--shadow: var(--shadow-spread) var(--shadow-cast), 0 2px 6px var(--shadow-ambient);

--glow-strength: 0.16;
--font-display: 'Inter Tight', 'Inter', system-ui, -apple-system, sans-serif;
--font-sans: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
--font-serif: ui-serif, 'Iowan Old Style', 'Palatino Linotype', Palatino, Georgia, Cambria, serif;
--font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
--shadow-sm: 0 1px 2px oklch(0.5 0.06 55 / 0.08);
--shadow: 0 8px 30px oklch(0.5 0.08 55 / 0.10), 0 2px 6px oklch(0.5 0.06 55 / 0.06);
/* The closing CTA's fill. Light keeps a faint accent tint, which
separates the panel from the page. Dark takes the plain elevated
surface: over black the same tint went muddy rather than warm, so
the glow alone carries it there. */
/* Hover lift for nav links and other bare targets. An ALPHA overlay,
not a solid colour, for two reasons. It composes over the header's
translucent blurred background instead of fighting it, and it gives
the same perceived step in both themes: the solid --bg-subtle was a
0.09 lift on a black page, which is real in numbers and invisible to
the eye, while the same token in light was a 0.025 step that read
clearly because the eye is adapted to a bright field. */
--hover-surface: oklch(0 0 0 / 0.055);
--cta-surface: color-mix(in oklch, var(--accent-live) 7%, var(--bg-elev));
--shadow-glow: 0 0 0 1px var(--accent-tint), 0 14px 50px color-mix(in oklch, var(--accent-live) 18%, transparent);
--t: 240ms;

color-scheme: light dark; /* the default: follow the OS */
}
/* The three tokens above that are NOT colours, so light-dark() cannot
carry them. Per the styling reference these keep an explicit pair:
the media query for the OS default, the attribute rule for the
toggle. Colours must never be added here. */
@media (prefers-color-scheme: dark) {
:root:not([data-theme='light']) {
--heart: oklch(0.74 0.18 6);
--fg: oklch(0.96 0.01 60); --fg-muted: oklch(0.78 0.015 60); --fg-subtle: oklch(0.66 0.02 65);
--bg: oklch(0.08 0.012 60); --bg-elev: oklch(0.14 0.015 60); --bg-subtle: oklch(0.11 0.014 60); --bg-sunken: oklch(0.06 0.01 60);
--border: oklch(0.24 0.015 60 / 0.9); --border-strong: oklch(0.36 0.02 60 / 0.95);
--accent: oklch(0.78 0.18 58); --accent-hover: oklch(0.83 0.17 58); --accent-fg: oklch(0 0 0); --logo-from: oklch(0.82 0.17 58); --logo-to: oklch(0.64 0.18 44);
--accent-live: oklch(0.78 0.18 58);
--glow-a: oklch(0.78 0.18 58);
--glow-strength: 0.08;
--cta-surface: color-mix(in oklch, var(--accent-live) 6%, var(--bg-elev));
--hover-surface: oklch(1 0 0 / 0.09);
--shadow-sm: 0 1px 2px oklch(0 0 0 / 0.4);
--shadow: 0 10px 40px oklch(0 0 0 / 0.5), 0 2px 6px oklch(0 0 0 / 0.35);
}
}
:root[data-theme='dark'] {
color-scheme: dark;
--heart: oklch(0.74 0.18 6);
--fg: oklch(0.96 0.01 60); --fg-muted: oklch(0.78 0.015 60); --fg-subtle: oklch(0.66 0.02 65);
--bg: oklch(0.08 0.012 60); --bg-elev: oklch(0.14 0.015 60); --bg-subtle: oklch(0.11 0.014 60); --bg-sunken: oklch(0.06 0.01 60);
--border: oklch(0.24 0.015 60 / 0.9); --border-strong: oklch(0.36 0.02 60 / 0.95);
--accent: oklch(0.78 0.18 58); --accent-hover: oklch(0.83 0.17 58); --accent-fg: oklch(0 0 0); --logo-from: oklch(0.82 0.17 58); --logo-to: oklch(0.64 0.18 44);
--accent-live: oklch(0.78 0.18 58);
--glow-a: oklch(0.78 0.18 58);
--glow-strength: 0.08;
--cta-surface: color-mix(in oklch, var(--accent-live) 6%, var(--bg-elev));
--hover-surface: oklch(1 0 0 / 0.09);
--shadow-sm: 0 1px 2px oklch(0 0 0 / 0.4);
--shadow: 0 10px 40px oklch(0 0 0 / 0.5), 0 2px 6px oklch(0 0 0 / 0.35);
:root:not([data-theme='light']) { --glow-strength: 0.08; --cta-mix: 6%; --shadow-spread: 0 10px 40px; }
}
:root[data-theme='dark'] { --glow-strength: 0.08; --cta-mix: 6%; --shadow-spread: 0 10px 40px; }
/* The toggle forces a scheme, which is what re-points every
light-dark() above. Nothing else needs to change per theme. */
:root[data-theme='dark'] { color-scheme: dark; }
:root[data-theme='light'] { color-scheme: light; }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; scroll-behavior: auto !important; }
Expand Down
3 changes: 2 additions & 1 deletion website/app/llms.txt/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { renderDocsIndexSection } from '#lib/docs-llms.server.ts';
import { loadRegistryIndex } from '#modules/ui/queries/registry.server.ts';
import { splitByTier } from '#modules/ui/utils/tier.ts';
import { UI_PATH, GH_URL } from '#lib/links.ts';
import { siteUrl } from '#lib/env.ts';

/**
* GET /llms.txt
Expand All @@ -30,7 +31,7 @@ import { UI_PATH, GH_URL } from '#lib/links.ts';
* enumerated inline (every page, with its description) rather than reduced
* to a single link at another host. This is the one llms.txt for the site.
*/
const SITE_URL = ((globalThis as any).process?.env?.SITE_URL || 'https://webjs.dev').replace(/\/$/, '');
const SITE_URL = siteUrl();

/** Render a `## <title>` link section, or nothing when it has no items. */
function section(title: string, items: string[]): string[] {
Expand Down
32 changes: 16 additions & 16 deletions website/app/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,18 +125,12 @@ export default function LandingPage() {
--editor-fg: var(--fg);
--editor-gutter-fg: var(--fg-subtle);
--editor-gutter-border: var(--border);
--code-tag: oklch(0.55 0.13 250);
--code-attr: oklch(0.52 0.16 150);
--code-str: oklch(0.55 0.13 145);
--code-tag: light-dark(oklch(0.55 0.13 250), oklch(0.78 0.13 250));
--code-attr: light-dark(oklch(0.52 0.16 150), oklch(0.66 0.16 150));
--code-str: light-dark(oklch(0.55 0.13 145), oklch(0.80 0.15 145));
--code-text: var(--fg);
--code-punc: var(--fg-muted);
}
:root[data-theme='dark'] {
--code-tag: oklch(0.78 0.13 250); --code-attr: oklch(0.66 0.16 150); --code-str: oklch(0.80 0.15 145);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme='light']) { --code-tag: oklch(0.78 0.13 250); --code-attr: oklch(0.66 0.16 150); --code-str: oklch(0.80 0.15 145); }
}
/* Syntax-highlight token colors (.t-kw / .t-str / ...) are defined
globally in public/input.css so every surface (this page and the
blog code fences) shares one palette. */
Expand Down Expand Up @@ -288,14 +282,20 @@ export default function LandingPage() {
<div class="${CARD}">
<div class="mb-6">
<h3 class="font-display font-bold text-base leading-[1.3] tracking-[-0.02em] mt-0 mb-2">Light DOM web components</h3>
<p class="m-0 text-sm leading-[1.6] text-fg-muted">Web components that render to light DOM, so Tailwind and global CSS just work, no shadow plumbing.</p>
<p class="m-0 text-sm leading-[1.6] text-fg-muted">Components render to light DOM by default, so Tailwind and global CSS just work, no shadow plumbing. Scoped styles are one line away when you want them.</p>
</div>
<div class="bg-[var(--editor-sidebar-bg)] border border-[var(--editor-border)] rounded-xl p-3.5 font-mono text-xs leading-[1.5] select-none text-[var(--editor-fg)]">
<div class="text-[var(--code-punc)]">&lt;<span class="text-[var(--code-tag)]">like-button</span>&gt;</div>
<div class="pl-4 text-[var(--code-punc)]">&lt;<span class="text-[var(--code-tag)]">button</span> <span class="text-[var(--code-attr)]">class</span>=<span class="text-[var(--code-str)]">"px-3 rounded bg-accent"</span>&gt;</div>
<div class="pl-8 text-[var(--code-text)]">&hearts; Like</div>
<div class="pl-4 text-[var(--code-punc)]">&lt;/<span class="text-[var(--code-tag)]">button</span>&gt;</div>
<div class="text-[var(--code-punc)]">&lt;/<span class="text-[var(--code-tag)]">like-button</span>&gt;</div>
<!-- Two rows rather than a markup snippet: the point of this card
is which mode is the DEFAULT and how you leave it, and a tag
with Tailwind classes on it shows neither. Same row idiom as
the Built-in essentials card, with the accent border marking
the default and the opt-in stating the real one-line API. -->
<div class="bg-[var(--editor-sidebar-bg)] border border-[var(--editor-border)] rounded-xl p-2.5 flex flex-col gap-1.5 font-mono text-xs text-[var(--editor-fg)] select-none">
<div class="flex justify-between items-center gap-2 px-2 py-1 bg-[var(--editor-bg)] border border-[var(--accent-border)] rounded">
<span>Light DOM</span> <span class="text-[var(--accent-text)] whitespace-nowrap">default</span>
</div>
<div class="flex justify-between items-center gap-2 px-2 py-1 bg-[var(--editor-bg)] border border-[var(--editor-border)] rounded text-fg-subtle">
<span>Shadow DOM</span> <span class="whitespace-nowrap">static shadow = true</span>
</div>
</div>
</div>

Expand Down
3 changes: 2 additions & 1 deletion website/app/robots.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { siteUrl } from '#lib/env.ts';
/**
* /robots.txt
*
Expand Down Expand Up @@ -28,7 +29,7 @@
*
* `SITE_URL` mirrors app/sitemap.ts so the two agree on the origin.
*/
const SITE_URL = ((globalThis as any).process?.env?.SITE_URL || 'https://webjs.dev').replace(/\/$/, '');
const SITE_URL = siteUrl();

// Answer-engine and AI crawlers we explicitly welcome. These are the agents
// that put a citation in front of a developer who asks an assistant what
Expand Down
Loading
Loading