@pyreon/ui-core — API Reference
Generated from
ui-core'ssrc/manifest.ts— the same source that powersllms.txtand MCPget_api. Do not edit this page by hand; edit the manifest. For the conceptual guide, see ui-core.
Foundation layer for the Pyreon UI system. PyreonUI is the single provider replacing the previous theme / mode / config split — it accepts a theme, a mode of "light" | "dark" | "system", and an optional inversed flip, then auto-detects OS preference via prefers-color-scheme when mode="system". useMode() returns the resolved mode as a reactive signal. The package also exposes the init() escape hatch (called internally by PyreonUI but available for SSR / test setups), the static HTML_TAGS / HTML_TEXT_TAGS lists used by the bases, and zero-dep utilities (get, set, merge, pick, omit, throttle, isEmpty, isEqual).
Features
PyreonUI({ theme, mode, inversed }) — single provider replaces 3 separate providers
mode="system" auto-detects OS preference via matchMedia and updates reactively
useMode() returns Signal<"light" | "dark"> resolved against system preference + inversed
init() callable directly for custom environments (tests, SSR without PyreonUI)
init({ cssVariables: true }) — opt-in CSS-variables theming: theme becomes custom properties, dark/light is one attribute write (no re-render)
cssVariablesPrePaintScript() — blocking <head> script that sets the mode attribute on documentElement before first paint (FOUC fix)
enrichTheme() (re-exported from @pyreon/unistyle) merges user theme with defaults
Zero-dep utilities: get, set, merge, pick, omit, throttle, isEmpty, isEqual
HTML_TAGS / HTML_TEXT_TAGS constants drive Element / Text base tag dispatching
Complete example
A full, end-to-end usage of the package:
import { PyreonUI, useMode } from '@pyreon/ui-core'
import { enrichTheme } from '@pyreon/unistyle'
// Single provider — wraps theme, mode, and config in one tree
const theme = enrichTheme({
colors: { primary: '#3b82f6', secondary: '#6366f1' },
fonts: { body: 'Inter, sans-serif' },
})
const App = () => (
<PyreonUI theme={theme} mode="system">
<MyApp />
</PyreonUI>
)
// useMode() reads the resolved mode reactively
function ThemeBadge() {
const mode = useMode()
return <div class={mode() === 'dark' ? 'badge-dark' : 'badge-light'}>{mode()}</div>
}
// inversed flips the resolved mode (light → dark and vice versa)
const InvertedSection = () => (
<PyreonUI inversed>
<Sidebar />
</PyreonUI>
)Exports
| Symbol | Kind | Summary |
|---|---|---|
PyreonUI | component | Unified provider replacing the previous theme / mode / config split (3 nested providers became 1). |
useMode | hook | Returns the currently resolved mode as a reactive signal — 'light' or 'dark'. |
useThemeValue | hook | Deep-reads a dot-path from the styler theme (e.g. |
useRootSize | hook | Reads the styler theme root font size (default 16) and returns it plus pxToRem / remToPx converters. |
useSpacing | hook | Returns a spacing(multiplier) function producing a px string. |
cssVariablesPrePaintScript | function | Build the blocking pre-paint script that sets the CSS-variables mode attribute on document.documentElement BEFORE firs |
API
PyreonUI component
(props: { theme?: Theme; mode?: 'light' | 'dark' | 'system'; inversed?: boolean; children: VNodeChild }) => VNodeChildUnified provider replacing the previous theme / mode / config split (3 nested providers became 1). Accepts an enriched theme object (merge with defaults via enrichTheme()), a mode of 'light' | 'dark' | 'system', and an optional inversed flip. When mode='system', the provider subscribes to matchMedia('(prefers-color-scheme: dark)') and re-resolves the mode reactively. Calls init() internally so consumers don't need to wire it up themselves. Whole-theme swaps (user-preference themes) propagate through the styler resolver and re-resolve CSS without remounting the VNode. Under init({ cssVariables: true }) the provider additionally autogenerates CSS custom properties from the theme (unistyle's themeToCssVars), injects the :root block once, provides a var-leaf theme tree, and renders a layout-neutral display: contents wrapper carrying the mode attribute — a dark/light flip becomes ONE attribute write (zero re-resolution, zero className churn), nested inversed providers scope via the CSS cascade, and SSR ships the right mode server-rendered.
Example
import { PyreonUI } from "@pyreon/ui-core"
import { enrichTheme } from "@pyreon/unistyle"
const theme = enrichTheme({ colors: { primary: "#3b82f6" } })
<PyreonUI theme={theme} mode="system">
<App />
</PyreonUI>
// mode="system" auto-detects OS dark mode via prefers-color-scheme
// inversed flips the resolved mode (light↔dark)Common mistakes
Using
ThemeProvider+ModeProvider+ConfigProviderseparately —PyreonUIis the single replacement covering all threeFlipping
init({ cssVariables })after the first render — the switch is a boot-time contract; theme-resolution caches across the ui-system assume it does not change mid-sessionExpecting
mode(a, b)pairs with NUMBER values to unit-convert undercssVariables— pairs are emitted verbatim into CSS custom properties; pass unit-complete stringsForgetting
enrichTheme()— raw theme objects miss default breakpoints / spacing / unit utilitiesDestructuring
propsinside the provider — components run once; destructuring captures values at setup. Readprops.modelazily inside reactive scopesRe-augmenting the
ThemeDefault/StylesDefaultinterfaces in your app —@pyreon/ui-themealready augments them; double-augmentation throws TS2320
See also: useMode · enrichTheme · init
useMode hook
useMode(): Signal<'light' | 'dark'>Returns the currently resolved mode as a reactive signal — 'light' or 'dark'. When the nearest PyreonUI ancestor uses mode='system', the signal reflects the OS preference and updates when the user changes their system setting. When inversed is true on any ancestor, the mode is flipped before resolution. Component-scoped subscription — readers re-run only when the resolved mode actually changes.
Example
import { useMode } from "@pyreon/ui-core"
const mode = useMode()
// mode() returns "light" or "dark" (resolved, reactive)
// Reflects OS preference when PyreonUI mode="system"Common mistakes
Reading
useMode()without calling it — the value is aSignal; usemode()to readUsing
useMode()outside anyPyreonUIancestor — falls back to a default but loses the reactive system / inversed handling
See also: PyreonUI
useThemeValue hook
useThemeValue<T = unknown>(path: string) => T | undefinedDeep-reads a dot-path from the styler theme (e.g. "colors.primary"), returning the value or undefined. A convenience over useTheme() + manual traversal. Lives in @pyreon/ui-core so the ui-system owns its theme-reader hooks without depending on the @pyreon/hooks fundamentals package.
Example
const primary = useThemeValue<string>('colors.primary')Common mistakes
Returns a PLAIN value captured once — NOT an accessor and NOT reactive; it will not update on a theme swap. For a value that tracks the theme, read
useThemeAccessor()from@pyreon/stylerinside a reactive scope.
See also: useRootSize · useSpacing
useRootSize hook
useRootSize() => { rootSize: number; pxToRem: (px: number) => string; remToPx: (rem: number) => number }Reads the styler theme root font size (default 16) and returns it plus pxToRem / remToPx converters. Requires a theme context (falls back to 16 otherwise). Lives in @pyreon/ui-core — a ui-system theme-reader hook.
Example
const { pxToRem } = useRootSize()
<div style={{ padding: pxToRem(24) }}>…</div>Common mistakes
rootSizeis a plain number captured ONCE at call time — NOT reactive. The converters close over that snapshot, so a later whole-theme swap will not update an already-returned result (re-mount the consumer to pick up a new root size).
See also: useSpacing · useThemeValue
useSpacing hook
useSpacing(base?: number) => (multiplier: number) => stringReturns a spacing(multiplier) function producing a px string. The unit is base ?? rootSize/2 (default 8px), read from the theme via useRootSize. Lives in @pyreon/ui-core — a ui-system theme-reader hook.
Example
const spacing = useSpacing()
<div style={{ gap: spacing(2) }}>…</div> // "16px"Common mistakes
The unit is computed once from a non-reactive
rootSizesnapshot — the returnedspacingfunction is static; a theme change will not affect an already-obtained function.
See also: useRootSize · useThemeValue
cssVariablesPrePaintScript function
cssVariablesPrePaintScript(options?: { attribute?: string; storageKey?: string; fallback?: "light" | "dark" }): stringBuild the blocking pre-paint script that sets the CSS-variables mode attribute on document.documentElement BEFORE first paint — the standard dark-mode FOUC fix for init({ cssVariables: true }). Inject the returned string as a synchronous <script> in <head>: it reads a persisted toggle from localStorage (default key zero-theme), else the OS prefers-color-scheme, else fallback, and writes the attribute at :root — exactly where the var rules cascade from and where the ROOT PyreonUI writes after hydration, so the two agree and there is no flash for mode="system" or a persisted toggle. Self-contained + try/catch-wrapped. (zero apps can use the existing themeScript export, which writes the same attribute.)
Example
import { cssVariablesPrePaintScript } from '@pyreon/ui-core'
// In your document <head>, before the app bundle:
// <script>{cssVariablesPrePaintScript()}</script>Common mistakes
Placing it at end-of-body instead of <head> — it must run before first paint; an in-body script can flash on a streamed/large document
Using it without the ROOT PyreonUI under cssVariables — the script fixes the PRE-hydration paint; the root provider keeps documentElement in sync AFTER hydration. Both are needed
Expecting it to cover a hardcoded
mode="dark"SSR app with no stored preference — the mode lives only in the app JSX; stamp<html data-theme="dark">server-side for that case
See also: PyreonUI
Package-level notes
Provider replacement: The legacy split (separate theme / mode / config providers) is removed.
PyreonUIis the only correct mount; callinginit()directly is the escape hatch for SSR or test environments where the provider tree is unavailable.
System-mode subscription:
mode="system"lazily creates amatchMedia('(prefers-color-scheme: dark)')subscription on first read; the listener stays alive for the document lifetime, so a single subscription handles everyuseMode()consumer.