From 1706128f864707ea809d50530bce36535aef9b42 Mon Sep 17 00:00:00 2001 From: Vivek Date: Sun, 2 Aug 2026 23:30:47 +0530 Subject: [PATCH 1/2] style: enhance dark theme aesthetics and refine landing page titles (closes #1212) --- .review-stream-chunks.mjs | 36 ++++ website/app/docs/layout.ts | 4 +- website/app/layout.ts | 38 ++-- website/app/page.ts | 20 +- website/app/what-is-webjs/page.ts | 4 +- website/components/ui/accordion.ts | 79 ++++++++ website/components/ui/aspect-ratio.ts | 24 +++ website/components/ui/breadcrumb.ts | 50 +++++ website/components/ui/card.ts | 90 +++++++++ website/components/ui/collapsible.ts | 62 ++++++ website/components/ui/kbd.ts | 27 +++ website/components/ui/label.ts | 25 +++ website/components/ui/popover.ts | 268 ++++++++++++++++++++++++++ website/components/ui/progress.ts | 53 +++++ website/components/ui/skeleton.ts | 23 +++ website/components/ui/table.ts | 65 +++++++ website/lib/design/tokens.ts | 16 +- website/lib/samples.ts | 13 +- website/lib/ui/dom.ts | 18 ++ website/lib/ui/utils.ts | 211 ++++++++++++++++++++ 20 files changed, 1079 insertions(+), 47 deletions(-) create mode 100644 .review-stream-chunks.mjs create mode 100644 website/components/ui/accordion.ts create mode 100644 website/components/ui/aspect-ratio.ts create mode 100644 website/components/ui/breadcrumb.ts create mode 100644 website/components/ui/card.ts create mode 100644 website/components/ui/collapsible.ts create mode 100644 website/components/ui/kbd.ts create mode 100644 website/components/ui/label.ts create mode 100644 website/components/ui/popover.ts create mode 100644 website/components/ui/progress.ts create mode 100644 website/components/ui/skeleton.ts create mode 100644 website/components/ui/table.ts create mode 100644 website/lib/ui/dom.ts create mode 100644 website/lib/ui/utils.ts diff --git a/.review-stream-chunks.mjs b/.review-stream-chunks.mjs new file mode 100644 index 000000000..97dc554b0 --- /dev/null +++ b/.review-stream-chunks.mjs @@ -0,0 +1,36 @@ +// Count chunks + measure progressive delivery of renderToStream(ssr:false) +import { html, renderToStream, Suspense } from '@webjsdev/core/server'; + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +async function collect(label, stream) { + const reader = stream.getReader(); + const chunks = []; + const t0 = Date.now(); + while (true) { + const { done, value } = await reader.read(); + if (done) break; + chunks.push({ at: Date.now() - t0, len: value.length, head: value.slice(0, 40) }); + } + console.log(`\n== ${label} ==`); + for (const c of chunks) console.log(` +${c.at}ms len=${c.len} ${JSON.stringify(c.head)}`); + console.log(` total chunks: ${chunks.length}`); +} + +// 1. Template with slow async text holes: progressive flushing? +const slow = sleep(150).then(() => 'SLOW'); +await collect( + 'async text holes', + renderToStream(html`
A

${slow}

B

${sleep(300).then(() => 'SLOWER')}

`, { ssr: false }), +); + +// 2. Suspense inside a template hole (ssr:false raw streaming path) +const boundary = Suspense({ + fallback: html`loading…`, + children: sleep(200).then(() => html`resolved`), +}); +const { createSuspenseCtx } = await import('@webjsdev/core/server').catch(() => ({})); +await collect( + 'suspense in template', + renderToStream(html`
${boundary}
`, { ssr: false }), +); diff --git a/website/app/docs/layout.ts b/website/app/docs/layout.ts index db8fd2d55..1dad871b1 100644 --- a/website/app/docs/layout.ts +++ b/website/app/docs/layout.ts @@ -36,7 +36,7 @@ const NAV_SECTIONS = [ ], }, { - title: 'Core Concepts', + title: 'Full Stack', items: [ { href: '/docs/routing', label: 'Routing' }, { href: '/docs/components', label: 'Components' }, @@ -53,7 +53,7 @@ const NAV_SECTIONS = [ ], }, { - title: 'Data & Backend', + title: 'Backend', items: [ { href: '/docs/server-actions', label: 'Server Actions' }, { href: '/docs/api-routes', label: 'API Routes' }, diff --git a/website/app/layout.ts b/website/app/layout.ts index 8393afefb..a7d19048a 100644 --- a/website/app/layout.ts +++ b/website/app/layout.ts @@ -293,14 +293,14 @@ export default function RootLayout({ children }: LayoutProps) { @media (prefers-color-scheme: dark) { :root:not([data-theme='light']) { --heart: oklch(0.74 0.18 6); - --fg: oklch(0.98 0 0); --fg-muted: oklch(0.80 0 0); --fg-subtle: oklch(0.68 0 0); - --bg: oklch(0 0 0); --bg-elev: oklch(0.12 0 0); --bg-subtle: oklch(0.08 0 0); --bg-sunken: oklch(0 0 0); - --border: oklch(0.26 0 0 / 0.9); --border-strong: oklch(0.38 0 0 / 0.95); + --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: transparent; - --glow-strength: 0; - --cta-surface: transparent; + --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); @@ -309,14 +309,14 @@ export default function RootLayout({ children }: LayoutProps) { :root[data-theme='dark'] { color-scheme: dark; --heart: oklch(0.74 0.18 6); - --fg: oklch(0.98 0 0); --fg-muted: oklch(0.80 0 0); --fg-subtle: oklch(0.68 0 0); - --bg: oklch(0 0 0); --bg-elev: oklch(0.12 0 0); --bg-subtle: oklch(0.08 0 0); --bg-sunken: oklch(0 0 0); - --border: oklch(0.26 0 0 / 0.9); --border-strong: oklch(0.38 0 0 / 0.95); + --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: transparent; - --glow-strength: 0; - --cta-surface: transparent; + --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); @@ -356,18 +356,14 @@ export default function RootLayout({ children }: LayoutProps) { selector outranks the border-* utilities that also set border-right-color, so cascade order does not matter. */ .site-top > header { border-right: var(--wj-scrollbar-compensation, 0px) solid transparent; } - /* The page backdrop is deliberately UNCHANGED from what webjs.dev - serves: one fixed layer, two radials off --glow-a, no animation. A - dot-grid texture and a third magenta radial were tried here and - reverted. They tinted every page, which is a site-wide change, and the - glow that was actually wanted belongs to the closing CTA panel, where - it comes from --shadow-glow on the panel itself. */ + /* Background glow layer with soft radial ambient glow */ .glow-layer { position: fixed; inset: 0; z-index: 0; pointer-events: none; } .glow-layer::before { content: ''; position: absolute; inset: 0; background: - radial-gradient(58% 44% at 50% -4%, color-mix(in oklch, var(--glow-a) calc(var(--glow-strength) * 100%), transparent), transparent 72%), - radial-gradient(40% 36% at 88% 8%, color-mix(in oklch, var(--glow-a) calc(var(--glow-strength) * 60%), transparent), transparent 70%); + radial-gradient(60% 48% at 50% -5%, color-mix(in oklch, var(--glow-a) calc(var(--glow-strength) * 100%), transparent), transparent 75%), + radial-gradient(45% 40% at 85% 10%, color-mix(in oklch, var(--glow-a) calc(var(--glow-strength) * 60%), transparent), transparent 70%), + radial-gradient(50% 50% at 15% 85%, color-mix(in oklch, var(--glow-a) calc(var(--glow-strength) * 40%), transparent), transparent 70%); } .scroll-thin { scrollbar-width: thin; scrollbar-color: transparent transparent; transition: scrollbar-color var(--t); } .scroll-thin:hover { scrollbar-color: color-mix(in oklch, var(--fg-subtle) 70%, transparent) transparent; } @@ -400,7 +396,7 @@ export default function RootLayout({ children }: LayoutProps) {
-
+
${brandLockup('hdr', { height: 26 })} diff --git a/website/app/page.ts b/website/app/page.ts index da727bc23..08e40643f 100644 --- a/website/app/page.ts +++ b/website/app/page.ts @@ -1,7 +1,7 @@ import { html } from '@webjsdev/core'; import '#components/copy-cmd.ts'; import '#components/like-button.ts'; -import { COMPONENT_SAMPLE, ACTION_SAMPLE, PAGE_SAMPLE, USAGE_SAMPLE } from '#lib/samples.ts'; +import { COMPONENT_SAMPLE, TOGGLE_SAMPLE, ACTION_SAMPLE, PAGE_SAMPLE, USAGE_SAMPLE } from '#lib/samples.ts'; import { DOCS_PATH, DOCS_START_PATH, GH_URL, NEW_TAB } from '#lib/links.ts'; // highlight() runs only at SSR (codeWindow renders its output into the served // HTML), but it does ship to the client as a small dead module: the page loads @@ -246,16 +246,12 @@ export default function LandingPage() {
-
+
-

The whole stack, in three files

-

A component, a server action, and a page. No build, no boilerplate, all web standards.

+

Server action & SSR page

+

A server action and a page. No build, no boilerplate, all web standards.

-
-
-

Interactive component

- ${codeWindow('components/like-button.ts', COMPONENT_SAMPLE)} -
+

Server action (RPC)

${codeWindow('actions/get-post.server.ts', ACTION_SAMPLE)} @@ -381,8 +377,7 @@ export default function LandingPage() {
- Full-stack -

Pages + API + components

+

Full Stack

SSR pages, web components, server actions, Drizzle, streaming, and a browsable feature gallery. Auth (login, sessions, a protected route) ships as a gallery card. The default.

app/page.ts
 components/counter.ts
@@ -390,8 +385,7 @@ actions/posts.server.ts
npm create webjs@latest my-app
- Backend (API) -

Route handlers + Database

+

Backend

A backend-only app, no UI or SSR. File-based route handlers, modules, middleware, rate limiting, WebSockets, a database, and a backend-features gallery.

app/api/users/route.ts
 app/api/chat/route.ts
diff --git a/website/app/what-is-webjs/page.ts b/website/app/what-is-webjs/page.ts
index 3c56cc85b..7b2989be1 100644
--- a/website/app/what-is-webjs/page.ts
+++ b/website/app/what-is-webjs/page.ts
@@ -250,7 +250,7 @@ export default function WhatIsWebJs() {
           

What a WebJs app looks like

- A component, a server function, and a page. These are ordinary files in your project, + A component, a server action, and a page. These are ordinary files in your project, served as written. There is no compile step between what you see here and what the browser receives.

@@ -261,7 +261,7 @@ export default function WhatIsWebJs() { ${codeWindow('components/like-button.ts', COMPONENT_SAMPLE, 'A WebJs web component with a signal')}
-

A server function

+

A server action

${codeWindow('actions/get-post.server.ts', ACTION_SAMPLE, 'A WebJs server action reading from the database')}
diff --git a/website/components/ui/accordion.ts b/website/components/ui/accordion.ts new file mode 100644 index 000000000..968bc1ea4 --- /dev/null +++ b/website/components/ui/accordion.ts @@ -0,0 +1,79 @@ +/** + * Accordion: vertical collapsible list built on native
/. + * + * Tier-1 (no custom element). Exclusive open behaviour (Radix's + * `type="single"`) comes from giving every
the same + * `name="..."` attribute. Independent open (`type="multiple"`) is the + * default when `name` is omitted. Both modes give `collapsible` for + * free: clicking the open closes it. + * + * shadcn parity: + *
wrapping + *
items + * → same, omit `name` + *
+ * + *
+ * + * Initial state: add `open` on the
that should render expanded + * on first paint. Programmatic toggling: `el.open = true | false`. + * + * `
` is the platform's exclusive-accordion primitive: + * Chrome 120+, Safari 17.2+, Firefox 130+. Migrated from the prior + * custom element set. + * + * Design tokens used: --border, --ring, --foreground. + * + * @example + * ```html + *
+ *
+ * + * Is it accessible? + * + * + *
Yes, it uses a native disclosure widget.
+ *
+ *
+ * + * Is it styled? + * + * + *
Yes, with the shadcn design tokens.
+ *
+ *
+ * ``` + */ + +/** Root wrapper. Holds the column-of-items rhythm; no display: rules. */ +export const accordionClass = (): string => 'w-full'; + +/** + * Item: each
. The `group` utility lets the trigger's chevron + * rotate on open via `group-open:rotate-180`. `last:border-b-0` cleans + * the trailing edge. + */ +export const accordionItemClass = (): string => 'group border-b last:border-b-0'; + +/** + * Trigger: applied to . Hides the native disclosure triangle so + * authors can compose their own chevron icon (typical pattern: trailing + * lucide chevron with `group-open:rotate-180`). + * + * `disabled: true` returns the visual disabled state (greyed out, + * not-allowed cursor, no pointer events). For true keyboard prevention + *, the native disabled-disclosure-widget gap, add the standard + * `inert` attribute to the
element. shadcn's React `disabled` + * prop combines both; native HTML has no `disabled` on
. + */ +export const accordionTriggerClass = (opts: { disabled?: boolean } = {}): string => { + const base = 'flex w-full cursor-pointer list-none items-center justify-between gap-4 py-4 text-left text-sm font-medium outline-none transition-all hover:underline focus-visible:ring-2 focus-visible:ring-ring/50 marker:hidden [&::-webkit-details-marker]:hidden'; + if (opts.disabled) return `${base} pointer-events-none cursor-not-allowed opacity-50`; + return base; +}; + +/** + * Content:
hides this entirely when not [open], so all we add + * is the typography rhythm matching shadcn (bottom padding, small text). + */ +export const accordionContentClass = (): string => 'pb-4 text-sm'; diff --git a/website/components/ui/aspect-ratio.ts b/website/components/ui/aspect-ratio.ts new file mode 100644 index 000000000..122f80d4c --- /dev/null +++ b/website/components/ui/aspect-ratio.ts @@ -0,0 +1,24 @@ +/** + * AspectRatio: preserve a width:height ratio for its child. Tier-1 + * class helper over the modern CSS `aspect-ratio` property (Baseline + * 2022). No JS, no custom element. + * + * shadcn parity: + * AspectRatio (Radix primitive) → aspectRatioClass() + inline `aspect-ratio` style + * + * Design tokens used: none (layout only). + * + * @example + * ```html + *
+ * Team offsite + *
+ * + * + *
+ * Team offsite + *
+ * ``` + */ + +export const aspectRatioClass = (): string => 'relative w-full'; diff --git a/website/components/ui/breadcrumb.ts b/website/components/ui/breadcrumb.ts new file mode 100644 index 000000000..3f5dc4422 --- /dev/null +++ b/website/components/ui/breadcrumb.ts @@ -0,0 +1,50 @@ +/** + * Breadcrumb: semantic nav with breadcrumb list. Tier-1 class helpers; + * compose with native `