2 additions.
Add a new declarative theming surface on ThemeProvider plus the pieces
needed to eliminate the "flash of default accent" on static-export apps.
New exports from @oztix/roadie-components:
DEFAULT_ACCENT_COLOR — the Oztix blue default, previously
module-local.InvalidColorError / isValidHexColor — validation primitives.
Consumers can guard untrusted hex at the fetch boundary instead of
reinventing a zod schema.getAccentStyleTagSync(hex) — synchronous sibling of
getAccentStyleTag. Returns a full <style> tag with
--accent-hue and --accent-chroma, ready for framework-agnostic
<head> injection. Uses the new sync sRGB→OKLCH converter in core.getAccentStyleSync(hex) — returns just the inner CSS body
(:root{--accent-hue:...;--accent-chroma:...}), for React consumers
that want to wrap it in a real <style> element via
dangerouslySetInnerHTML.getBootstrapScript(opts) — re-exported from @oztix/roadie-core.
Composes getThemeScript + an optional accent style tag into one
head injection for apps that want to do the whole bootstrap in one
line.Controlled accentColor prop on ThemeProvider:
<ThemeProvider accentColor={collection?.themeColour ?? null}>
{children}
</ThemeProvider>undefined (or omit the prop) to stay uncontrolled — the old
defaultAccentColor-seeded behaviour is unchanged.setAccentColor calls become no-ops with
a dev warning, and there's no effect sync or cleanup to wire up.null to opt into controlled mode while falling back to
defaultAccentColor (ideal for collection?.themeColour ?? null).setAccentColor now throws synchronously with InvalidColorError
when the argument isn't a valid hex. Previously the call "succeeded"
and threw inside the async accent effect with no handler path. If your
app validates at the boundary (or uses the new isValidHexColor
helper), there's nothing to change.
Why this matters. Consumer apps that theme from async data (per-tenant branding, promoter-coloured collection pages, feature flags) can now drop their bespoke effect-based accent sync, their hex validator, and their hardcoded default constant. The imperative API remains for simple cases like in-app colour pickers.
New sync colour utilities in @oztix/roadie-core/colors:
hexToOklch(hex) — convert a hex string to { l, c, h } using
Björn Ottosson's reference sRGB→Oklab→OKLCH pipeline. Zero
dependencies (the existing async helpers still use colorjs.io).getOklchHueSync(hex) / getOklchChromaSync(hex) — synchronous
siblings of the existing async helpers. Match the async output to
four decimal places across a 20-hex representative palette.Oklch type export.These unblock pre-hydration accent bootstrap: the hot path that the
ThemeProvider accent effect uses (setting just --accent-hue and
--accent-chroma) no longer needs an async colorjs.io import, so
consumers can inject the accent style tag before the first paint.
The existing async generateAccentScale / generateNeutralScale
pipeline is unchanged — those still use colorjs.io for the full
14-step hex fallback output.
New getBootstrapScript helper in @oztix/roadie-core/theme:
import { getBootstrapScript } from '@oztix/roadie-core/theme'
const html = getBootstrapScript({
followSystem: true,
accentColor: collection?.themeColour // optional
})Returns a single HTML string combining the theme script (dark-mode
flash prevention) and an optional accent style tag. Framework-agnostic
— drop it into <head> via dangerouslySetInnerHTML, Astro's
set:html, or a plain HTML template. When accentColor is omitted
or null, only the theme script is emitted. Invalid hex input throws
synchronously with a clear error message.
Extended radius scale:
Tailwind v4's default radius scale stops at rounded-4xl (2rem), so
rounded-5xl and beyond silently resolved to 0px. Roadie now adds
three extended tiers via @theme inline in tokens.css:
--radius-5xl: 2.5rem (40px) — hero cards, collection headers--radius-6xl: 3rem (48px) — feature banners--radius-7xl: 3.5rem (56px) — edge-to-edge promotional layoutsSee the shape foundation page for usage
guidance and the new foundations/theming page for the full
dynamic-theming walkthrough.