Skip to content

feat: Geist-based theme system + design standards + routed demo#350

Merged
chaxus merged 36 commits into
mainfrom
redesign-theme-system
Jun 28, 2026
Merged

feat: Geist-based theme system + design standards + routed demo#350
chaxus merged 36 commits into
mainfrom
redesign-theme-system

Conversation

@chaxus

@chaxus chaxus commented Jun 28, 2026

Copy link
Copy Markdown
Owner

Summary

Redesigns ranui's theming around the Geist design system (light/dark only), aligns every component to it, rebuilds the demo as a routed showcase, and codifies the result as standards so future work (human or AI) stays consistent.

Theme system (Geist)

  • Removed all 7 opt-in theme packs (pixel-retro / windows-98 / windows-xp / system-6 / wired / paper / neo-brutalism), dark-overrides/transitions, the wired SVG pipeline, the roughjs dependency, and the setThemePack/getThemePack APIs. Only the base light/dark theme remains.
  • New 3-layer token system: Geist base scales (--ran-gray/gray-alpha/blue/red/amber/green-100..1000, --ran-background-100/200) → semantic tokens (--ran-color-*, incl. -hover/-active state tokens) → component tokens.
  • Dark mode is a single source of truth: theme/dark.less redefines only the base scale via one mixin; semantic tokens flip automatically.
  • Geist radius (6/12/16/full), --ran-space-* scale, shadows (elevated/menu/modal), focus ring, Geist Sans/Mono.

Components

  • Aligned button / input / select / dropdown / modal / link / checkbox / progress / tab to Geist: control radius, hover/active scale stepping, focus rings, menu/modal shadows.
  • Fixed dark-mode bugs from hardcoded colors (dropdown-item, skeleton shimmer, radar canvas label/grid).

Utilities

  • New framework-agnostic i18n core (utils/i18n: createI18n/useI18n/I18nCore) — mirrors the router core/singleton design; SSR-safe.

Demo

  • Rebuilt as a routed multi-page app with ranui's own r-router/r-route/r-link (Overview · Design · Components · Guide), history mode + a Cloudflare Pages _redirects SPA fallback.
  • Design route is a methodology page (color state ladder, spacing rhythm, type roles, motion, copy do/don't, accessibility); EN/中文 switcher via r-select; light/dark toggle; everything token-driven.
  • Applied a Geist design-discipline pass and an accessibility pass (icon-link aria-labels, labelled nav, prefers-reduced-motion).

Standards (so AI follows them automatically)

  • docs/DESIGN.md — an AI-facing, executable design spec (color states, spacing, type roles, motion, copy, accessibility, component application, pre-ship checklist).
  • CLAUDE.md — new top-level "Design Standards" section pointing at DESIGN.md, refreshed theme/i18n references, and pitfalls distilled from this work.

Also included (prior unpushed foundation the demo builds on)

  • r-router JS API + View Transitions, RouterCore unit tests, and true SSG support for r-route.

Verification

  • tsc clean; theme + i18n unit tests pass; LESS compiles; demo builds.
  • Rendered output verified in light and dark, at compact and wide viewports, across all four routes.

🤖 Generated with Claude Code

chaxus and others added 30 commits June 27, 2026 18:11
…cumentation

Implements a module-level RouterCore singleton (createRouter / useRouter) with
navigation guards, SPA/MPA View Transitions support, and SSR-safe browser API
guards. Binds to r-router / r-link components via _bind/_unbind so they pick up
programmatic navigation without an event bus. Ships VitePress docs (EN + CN),
updates ranui/utils READMEs and CLAUDE.md with the full router API reference.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
60 tests covering createRouter/useRouter singleton, push/replace/history
navigation, hash mode, base path stripping, beforeEach guards (allow/cancel/
redirect/order/unsubscribe), afterEach, onRouteChange, component bind/unbind,
destroy cleanup, _buildLocation query parsing, MPA style injection,
onPageSwap/onPageReveal, SPA transition graceful degradation, and SSR window
guards.

Also limits vitest to maxForks: 4 to prevent JavaScript heap OOM when 60+
jsdom workers run in parallel.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…enerateStaticPages

Implements build-time route pre-rendering so static HTML has the correct
r-route visible (no hidden attr) and all others hidden.

Key changes:
- HTMLElementMock: reflect hidden property to/from attributes so _update()
  works in SSR (previously hidden = true only set a JS property, not serialized)
- ssr-registry.defineSSR: stamp _ssrTag on each component prototype so
  HTMLElementMock.serialize() uses the correct custom element tag name when
  components appear as children in a larger tree (previously serialized as <div>)
- utils/router/index.ts: add _ssgPath context (setSSGPath/clearSSGPath/getSSGPath)
  plus RouterCore.matchRoute() and RouterCore.getStaticPaths() for SSG enumeration
- components/route/index.ts: add _preSerialize() hook — called by HTMLElementMock
  before serialization; resolves hidden state from the active SSG path context
- utils/ssg.ts: new module exporting renderStaticPage() and generateStaticPages()
- package.json: add NODE_OPTIONS=--max-old-space-size=4096 to test scripts to
  prevent jsdom OOM on full parallel test run (replaces removed Vitest 4
  poolOptions.forks config which was deprecated)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Deleted transitions.ts, windows-98.less, windows-98.ts, windows-xp.less, windows-xp.ts, wired-assets.less, wired-overlay.ts, wired.less, wired.ts, color.less, and compat.less files.
- Cleaned up theme pack structure by removing unused styles and scripts to streamline the codebase.
- Removed all theme packs and streamlined the theme management to only support light, dark, and system themes.
- Simplified theme utility functions by eliminating theme pack logic and related storage.
- Introduced a new token architecture based on the Geist design system, enhancing color and semantic token definitions.
- Updated demo page to showcase the new token-driven design, including color scales, radius, elevation, and component examples.
- Fixed dark mode bugs by ensuring all colors are token-based for automatic theme adaptation.
- Add utils/i18n core (I18nCore / createI18n / useI18n): t() with locale
  fallback + {param} interpolation, setLocale/onChange, addMessages,
  localStorage persistence, navigator detection; SSR-safe. Mirrors the
  router core/singleton design and is exported from the ranui barrel.
- Split demo locales into demo/locales/{en,zh}.json (resolveJsonModule);
  demo/i18n.ts now consumes the core as a thin data-i18n DOM binding.
- Replace the demo language toggle button with an r-select picker, and
  add GitHub/Issues nav icons (assets/icons/github.svg, issue.svg) that
  inherit currentColor for theme adaptivity.
- Add test/unit/utils.i18n.test.ts (16 cases); update changelogs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Remove docs/SPECIAL_THEME_PACKS_PLAN.md (theme packs were deleted).
- Rewrite docs/THEME_STYLE_SYSTEM_DESIGN.md to describe the current
  Geist-based token system: base scales → semantic tokens → component
  tokens, the single-source dark mixin, runtime API, files, and tests.
- Regenerate docs/style-tokens-{public,parts}.md from current components.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Dotted-grid backdrop, hero accent glow, eyebrow badge, gradient
  heading, and primary/ghost CTA buttons.
- Numbered section eyebrows in Geist Mono (01, 02 …).
- Hover micro-interactions on cards and color swatches.
- New hero i18n keys (badge, CTAs) in en/zh locales.
All chrome stays token-driven and theme-adaptive.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dogfood ranui's own router to split the demo into chapters:
- Routes (history mode): Overview, Design (with Geist design.md /
  design.dark.md references), Components, Guide (install / theming /
  i18n / SSR with code blocks).
- demo/public/_redirects (/* /index.html 200) for Cloudflare Pages
  SPA fallback so deep links and refresh work in history mode.
- routechange highlights the active nav link; the canvas radar
  re-renders when the Components route becomes visible.
- r-link box model injected via `sheet`; per-route section numbering
  via CSS counters; expanded en/zh locales for nav/design/guide.
- Fix radar sample data (scoreRate is 0–100, not 0–1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Fix the generic hero: left-aligned, solid high-contrast heading
  (drop gradient text that lowered contrast) and remove the decorative
  glow; tighten the spacing rhythm.
- Add interaction-state semantic tokens per Geist's 100–1000 model:
  --ran-color-bg-hover/active and --ran-color-border-hover/active.
- Rebuild the Design route into a methodology page modeled on the
  Vercel/Geist spec: color state ladder (each step → a fixed state),
  spacing scale + rhythm, typography roles, motion durations, copy
  do/don't, and accessibility. The page follows its own rules
  (do/don't use ✓/✕ icons + text, not color alone).
- State legend + spacing scale generated in index.ts; expanded en/zh
  locales for the new content.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add docs/DESIGN.md — an executable design spec (color state ladder,
  spacing rhythm, typography roles, radius/elevation, motion, copy,
  accessibility, component application, pre-ship checklist), modeled on
  the Vercel/Geist design methodology.
- Fix hero CTA vertical centering: the inner <a> used height:100% against
  an auto-height host (collapsed to auto) plus inherited line-height; now
  a fixed 42px host height + line-height:1 + box-sizing:border-box.
- Fix mobile navigation: route links were hidden < 820px; they now drop
  to a full-width row (brand tagline hidden) so chapters stay reachable.

Found via a Vercel-Product-Design-skill review pass (verified rendered
output in light/dark and at compact/wide viewports).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add aria-labels to the GitHub/Issues links (they render icon-only on
  mobile, so they had no accessible name) and label the primary <nav>.
- Honor prefers-reduced-motion: disable smooth scroll, transitions, and
  hover transforms — as DESIGN.md §5 requires.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CLAUDE.md is auto-loaded when working in this repo, so the standards
now apply to all future AI work here:

- Add a top-level "Design Standards" section pointing to docs/DESIGN.md
  as authoritative, with the non-negotiables (color state ladder,
  dark-safe fallbacks, spacing/type/motion tokens, copy, a11y, verify
  rendered output).
- Update the stale theme section (theme packs were removed; no more
  setThemePack/RanThemePackName) and document the i18n utility.
- Refresh the token list (Geist scales, space/shadow/radius, trimmed
  skin layer) and the project layout.
- Add 8 pitfalls distilled from this session: dark-safe color
  fallbacks, button vertical centering, icon-only aria-label,
  prefers-reduced-motion, no color-only state, don't hide mobile nav,
  r-link box model via `sheet`, and the Cloudflare _redirects deploy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add docs/DESIGN.md to the `files` whitelist so consumers get the design
spec. CLAUDE.md stays unpublished (repo-internal working instructions).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Large, breaking changes (theme packs + setThemePack API removed, Geist
token system) warrant a minor bump under 0.x semver; switch the
prerelease channel from alpha to beta.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Major bump to 1.0 (still beta): the Geist token system and component
realignment mark the first stable design baseline; theme packs and the
setThemePack API are removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Geist-literal menu/modal shadows were too faint to read, so floating
layers (dropdown, select, modal) looked flat, and message was on the
flat card tier entirely.

- Recalibrate light --ran-shadow-menu / --ran-shadow-modal so overlays
  clearly lift off the page; keep --ran-shadow-elevated (cards) subtle.
- message: box-shadow now uses --ran-shadow-menu (was the card tier).
- Document the principle "elevation = role" (raised / overlay / modal,
  each must be perceptible) in DESIGN.md §4 and CLAUDE.md.

Verified rendered: select dropdown (light) and modal (dark) now read as
elevated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Complete the "elevation = role" standard: alongside the DESIGN.md §4
table and the Design Standards bullet, add the operational gotcha as a
pitfall row (overlays use --ran-shadow-menu, not the card tier; every
tier must be perceptible).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
BREAKING CHANGE: Node-only APIs (Server/WSS/Router/runCommand/…) are no
longer re-exported from the default `ranuts` entry — import them from
`ranuts/node`. Keeps the default bundle browser-safe (no node: builtins).

- Remove the broken `./react` subpath export (no source, never built).
- Expose previously-unreachable subsystems as ESM subpaths: `./visual`,
  `./vnode`, `./wicket`, `./arithmetic`, `./sort`, `./optimize`.
  Add barrels for `vnode` and `sort` (was empty `export {}`).
  `cache` is intentionally NOT exposed (demo server with import-time
  side effects).
- Add smoke tests for the newly-public surface (optimize, visual math,
  vnode h/patch); 139 → 160 tests.
- Lock the new export contract in package-exports.test.ts.
- Add @vitest/coverage-v8 + `test:coverage` script.
- Bump version 0.1.0-alpha-23 → 1.0.0-beta-1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The error/warning state was signalled by color alone. Now it also shows:
- an automatic status icon (CSS-mask SVG tinted by the status token), and
- an optional `message` attribute rendering helper/validation text below
  the field, colored by status.

Demo: the error email field shows the icon + "Enter a valid email
address" (en/zh). Input contract tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Agents (and humans) had no per-component attribute/event reference, so
using a component meant reading source. Add a generator that extracts
each custom element's API and emit docs/COMPONENTS.md.

- bin/generate-component-api.ts → docs/COMPONENTS.md (29 elements):
  attributes (observedAttributes), properties (get/set), events
  (CustomEvent), slots, and ::part() names — all from source, so it
  never drifts. `npm run doc:api`.
- Publish docs/COMPONENTS.md + style-tokens-public.md with the package.
- CLAUDE.md points to it as the element API source of truth.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`get checked()` returned "true"/"false" strings, so `if (el.checked)`
was always truthy (even unchecked). It now returns a boolean, matching
the native checkbox; the setter still accepts boolean or string.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Same wart as checkbox.checked: these returned strings, so `if (el.prop)`
was always truthy. Now return real booleans (setters still accept
boolean|string; boolean attrs reflect as `disabled=""`):
- r-input.disabled, r-input.required
- r-checkbox.disabled (and fix the onChange disabled guard accordingly)

Tests updated; input + checkbox suites pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…gines

- r-input: `change` now fires on native change (blur/commit), not on
  every keystroke; `input` still fires per keystroke. Matches the
  native <input> and stops e.g. writing localStorage on each keypress.
- r-select: listbox items now get `role="option"` (had aria-selected
  only) so screen readers announce them; long selected text ellipsizes
  (the selection item is now width-bounded).
- r-button: `type` is a real observed attribute + property (was only a
  CSS hook), so it's discoverable in docs and settable via JS.
- engines.node lowered >=24.0.0 → >=20.19.0 (fewer engine warnings).

Regenerated docs/COMPONENTS.md. tsc clean; input/select/button/checkbox
suites pass; role="option" verified in-browser.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- r-input field box now defaults to content height (auto + min-height
  32px) instead of 100%. With the new `message` below it, `height:100%`
  re-resolved against the taller host and the box ballooned. Now all
  fields are a consistent height and the message stacks below.
- Demo password field used both `label` and `icon`, which the component
  treats as mutually exclusive (the absolute floating label collided
  with the icon → "lPassword"). Switched it to icon + placeholder so the
  lock icon renders cleanly.

Verified: field heights are [32, 32, 54(=32+message)]; password shows a
clean lock icon. input suite passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The generated component API listed only names, so agents knew an event
existed but not its `detail` shape, nor a property's type. Now extracted
from source:
- Properties show types: `checked: boolean`, `value: string`, …
- Events show detail keys: `change → detail { value, label }` (select),
  `change → detail { checked }` (checkbox), `input/change → { value }`
  (input).

Regenerated docs/COMPONENTS.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each attribute in COMPONENTS.md now shows the type of its matching
property (e.g. `checked: boolean`, `value: string`); attributes without
a typed accessor stay bare, which flags them as markup-only. Makes the
attribute↔property correspondence explicit for agents.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- The API generator now resolves same-file `enum`/`type` aliases, so
  r-loading `name` shows the real icon-name union instead of leaking the
  internal `ICON_NAME_AMP` type name.
- Annotated r-message getters/setters (`type`/`content`: string | null,
  `sheet`: string) so they show types in the docs.

Regenerated docs/COMPONENTS.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract each accessor's `@description` JSDoc (getter preferred) and show
it next to the typed property in COMPONENTS.md, e.g.
`value: string — input 的值`. Properties are rendered as a list when any
has a description, inline otherwise. Descriptions are the source's
(Chinese) JSDoc; events have no JSDoc so they stay name + detail.

Regenerated docs/COMPONENTS.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
chaxus and others added 6 commits June 28, 2026 12:34
The visual engine shipped a PixiJS-style API but only Canvas2D worked
end-to-end. This wires up the GPU backends, unifies the pipeline, and
fixes three latent bugs that made GPU rendering produce nothing.

Backends:
- Add async Application.create() factory that awaits renderer.init(),
  so WebGPU's async device setup actually runs (was never called).
- Rewrite WebGLRenderer to extend BatchRenderer, sharing the
  triangulate→pack pipeline with WebGPU (vertex layout already matched);
  enable OES_element_index_uint for the shared uint32 index buffer.
- Delete the dead, disconnected WebGL BatchPool + Container.renderWebGL
  stubs that never drew anything.

Latent bugs fixed:
- GraphicsGeometry.dirty was never set true on drawShape, so
  buildVerticesAndTriangulate always early-returned → GPU geometry was
  always empty even when wired correctly.
- ObservablePoint.set()/setters invoked the change callback with no
  args, so Transform.onScaleChange/onSkewChange received undefined →
  the whole transform matrix became NaN. Any non-default scale/skew
  silently broke rendering.
- Scenes never rebuilt after the first frame (needBuildArr only ever
  flipped to false). Replace with a per-scene structureVersion bumped
  by addChild/removeChild and Graphics draw/clear; the renderer rebuilds
  the batch arrays when the version changes, else just repacks vertices.
  clear() now fully resets geometry buffers so no stale data lingers.

Hygiene:
- Remove per-node per-frame console.log from hot render paths; gate the
  renderer banner behind options.debug (also unblocks testability).
- needBuildArr/projection state moved off static onto the instance.

Tests + verification:
- Add visual-batch (data-level pipeline, transform-no-NaN,
  add/remove/clear rebuild) and visual-application (create wiring,
  getRenderer selection) suites.
- visual-demo.html: a three-backend switcher with live add/remove,
  used to verify Canvas2D/WebGL/WebGPU all render on real hardware.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… files

- Updated style tokens documentation to enhance formatting and readability.
- Refactored exports in index.ts for cleaner structure.
- Simplified route definitions in router.ssg.test.ts.
- Consolidated import statements in utils.router.test.ts for brevity.
- Streamlined router index.ts regex pattern creation for clarity.
- Optimized SSG utility functions for better readability.
- Enhanced WebGLRenderer class methods for concise code.
- Cleaned up vnode index.ts import statements for consistency.
- Improved visual-math test cases for better readability.
- Reformatted visual-demo.html CSS for improved structure and clarity.
…case

The routed rebuild only carried a subset of components; these five were
dropped by omission, not intent. Add a "More components" section to the
Components route showcasing r-icon, r-colorpicker, r-popover (trigger +
r-content), r-math (katex), and r-player, with en/zh strings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ark-safe

r-select rendered an <r-icon name="arrow-down"> caret but never registered
the icon, so the caret was missing (and logged "icon not registered") unless
the consuming app happened to register arrow-down itself. The caret color was
also hardcoded to #d9d9d9, which does not adapt to dark mode.

- select now imports arrow-down.svg and registers it at module load, making
  the component self-contained for any consumer.
- caret color uses var(--ran-color-text-secondary) so it flips with the theme.

Demo: register the demo's icons via a side-effect module imported first, before
any component module loads, so route content rendered on mount finds its icons
in the registry instead of warning "icon not registered".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The demo was rebuilt into a routed app, so component showcases moved from the
flat root page to the /components route and lost their #component-* anchors.
The visual regression specs still targeted the old root-page structure, so
every section locator failed with "element not found".

- demo: add stable id="component-*" hooks to each showcase block on the
  /components route, and give the tabs semantic r-key values (overview/api/
  theming/disabled) the spec already expects.
- specs: navigate to /components and wait for domcontentloaded instead of
  networkidle (the demo player streams HLS continuously, so networkidle never
  settles and the navigation would time out).
- prettier: reformat demo/index.html (Format check was failing on it).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The panel was visually broken: r-popover portals the panel out of the
component's shadow root into document.body, so all shadow-scoped CSS stopped
applying — the saturation square collapsed to 0px, sliders floated unanchored,
and the inputs were non-functional decoration.

Redesign:
- Panel styles now live in panel.less and are injected as a <style> inside the
  panel subtree, so they travel with the portaled content. Selectors are
  uniquely namespaced and every color is a theme token, so dark mode just works
  (tokens inherit from :root in the light DOM).
- Replace the fragile r-progress sliders with custom hue/alpha sliders using
  percent-based, layout-independent thumb/dot positioning.
- Wire the value input + HEX/RGB format select to actually display and edit the
  color, and emit a `change` event ({ value, hex, rgb, rgba, alpha }).
- Polished, Geist-aligned spacing, radii, preview swatch, and checkerboard
  (token-driven); trigger swatch gets a hover state.

Verified in light and dark; contract tests extended (alpha parsing, currentValue,
change event) and visual e2e specs pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@chaxus chaxus merged commit 128ba89 into main Jun 28, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant