@pyreon/rocketstyle
v0.53.0
Published
Multi-dimensional style composition for Pyreon components
Readme
@pyreon/rocketstyle
Multi-dimensional component styling — states, sizes, variants, themes, light/dark, all cached.
@pyreon/rocketstyle is the styling layer Pyreon's UI system builds on. Organize styles by named DIMENSIONS — state (primary / danger / success), size (sm / md / lg), variant, plus any custom dimension you declare — instead of flat boolean props. Each dimension is a chainable method (.states({...}), .sizes({...})); per-dimension values are themed via .theme() callbacks that receive (theme, mode, css), with light/dark mode threaded through and pseudo-states (hover / focus / pressed / active / disabled) auto-detected. Built on @pyreon/attrs + @pyreon/styler. Per-definition WeakMap caches make per-mount cost near zero for same-definition components — verified 73% reduction in styler.resolve calls on real-app benchmarks.
Install
bun add @pyreon/rocketstyle @pyreon/core @pyreon/reactivity @pyreon/ui-core @pyreon/stylerQuick start
import rocketstyle from '@pyreon/rocketstyle'
import { Element } from '@pyreon/elements'
const Button = rocketstyle()({ name: 'Button', component: Element })
.attrs({ tag: 'button' })
.theme({
fontSize: 16,
paddingX: 16,
paddingY: 8,
borderRadius: 4,
color: '#fff',
backgroundColor: '#0d6efd',
hover: { backgroundColor: '#0b5ed7' },
})
.states({
primary: { backgroundColor: '#0d6efd', hover: { backgroundColor: '#0b5ed7' } },
danger: { backgroundColor: '#dc3545', hover: { backgroundColor: '#bb2d3b' } },
success: { backgroundColor: '#198754', hover: { backgroundColor: '#157347' } },
})
.sizes({
sm: { fontSize: 14, paddingX: 12, paddingY: 6 },
md: { fontSize: 16, paddingX: 16, paddingY: 8 },
lg: { fontSize: 18, paddingX: 20, paddingY: 10 },
})
<Button state="danger" size="lg">Delete</Button>Core concepts
Dimensions
A dimension is a named axis of style variation. Defaults ship four:
| Dimension | Prop name | Multi? | Example |
| ---------- | --------- | ------ | ------------------------------ |
| states | state | no | primary, danger, success |
| sizes | size | no | sm, md, lg |
| variants | variant | no | outlined, filled |
| multiple | — | yes | rounded, shadow |
Each declared dimension creates a chain method AND a corresponding prop on the component. Multi-dimensions accept multiple active values at once.
Default: useBooleans: false (string prop values)
<Button state="primary" size="lg">
Save
</Button>Boolean shorthand (<Button primary lg>Save</Button>) is opt-in via rocketstyle({ useBooleans: true }). Important: before April 2026 the type default was true but the runtime was false — boolean props typechecked but were silently dropped at runtime. Fixed in rocketstyle/init.ts; new code should not rely on the historical behaviour.
Theme + pseudo-states
.theme({
color: '#333',
fontSize: 16,
hover: { color: '#000' },
focus: { outline: '2px solid blue' },
active: { transform: 'scale(0.98)' },
disabled: { opacity: 0.5 },
})Pseudo-state keys nest directly. Bases (@pyreon/elements) generate :hover / :focus-visible / :active / :disabled CSS from the nested objects. :hover is unconditional — applied to EVERY component with hover theme; only cursor: pointer is gated on onClick / href.
Styles callback
.styles((css) => css`
cursor: pointer;
border: none;
transition: all 0.2s;
${({ $rocketstyle, $rocketstate }) => {
// $rocketstyle — computed theme (base + active dimension values merged)
// $rocketstate — { hover, focus, pressed, active, disabled, pseudo }
return /* css string */
}}
`)$rocketstyle is identity-cached — same dimension-prop combo produces the same object identity, which lets the styler's classCache skip resolve work entirely on cache hits.
API
rocketstyle(options?)({ name, component })
Factory initializer. Returns a function that accepts component configuration.
const factory = rocketstyle({
dimensions: {/* custom dimensions */},
useBooleans: true,
})
const Button = factory({ name: 'Button', component: Element })factory.withTheme<Tokens>() — typed theme callbacks
Binds the theme TYPE every .theme() and dimension callback receives, so t is
inferred and checked. It is type-only (it returns the same factory) and needs no
global declare module '@pyreon/rocketstyle' augmentation — the right choice for
a library, where a global augmentation would merge into every other package's
theme type.
interface Tokens {
accent: string
surface: string
}
const rs = rocketstyle({ useBooleans: false }).withTheme<Tokens>()
const Card = rs({ name: 'Card', component: Element })
.theme((t) => ({ backgroundColor: t.surface })) // t: Tokens
.states((t) => ({ active: { color: t.accent } }))
// .theme((t) => ({ color: t.nope })) ❌ Property 'nope' does not existWithout it, t is the augmentable ThemeDefault (empty unless an app augments
it). An annotation such as (t: Tokens) => is checked against that, so bind the
factory rather than annotating each callback.
.attrs(props | callback, options?)
Same as @pyreon/attrs — accumulate defaults, supports callback / priority / filter.
.theme(values | callback)
Base theme applied to every instance.
.theme({ fontSize: 16, color: '#fff', hover: { opacity: 0.9 } })
.theme((theme, mode, css) => ({
fontSize: 16,
// `mode` is a helper: mode(lightValue, darkValue)
color: mode('#333', '#fff'),
})).states() / .sizes() / .variants() / .multiple()
Define per-dimension values.
.states({ primary: { backgroundColor: '#0d6efd' }, danger: { backgroundColor: '#dc3545' } })
.sizes({ sm: { fontSize: 14, paddingX: 8 }, lg: { fontSize: 18, paddingX: 20 } })
.multiple({ rounded: { borderRadius: 999 }, shadow: { boxShadow: '0 2px 8px rgba(0,0,0,0.15)' } })
// Callback form — receives (theme, mode, css)
.states((theme) => ({
primary: { backgroundColor: theme.colors?.primary ?? '#0d6efd' },
})).styles(callback)
CSS template using @pyreon/styler's css tagged template.
.config(options)
Button.config({
name: 'PrimaryButton',
component: NewBase, // swap base — resets prop chains
provider: true, // make this a context provider for children
consumer: (ctx) => …, // consume parent component context
inversed: true, // invert theme mode for subtree
DEBUG: true,
}).compose(hocs) / .statics(metadata)
Same API as @pyreon/attrs.
isRocketComponent(value) / resolveTheme(value)
Runtime guard and theme accessor for use inside styled-component interpolations:
import { isRocketComponent, resolveTheme } from '@pyreon/rocketstyle'
isRocketComponent(Button) // true
styled(Component)`
color: ${(props) => resolveTheme(props.$rocketstyle).color};
`resolveTheme handles both function-accessor (reactive) and plain-object $rocketstyle shapes.
Custom dimensions
const rocketButton = rocketstyle({
dimensions: {
intent: 'intent', // prop: intent="primary"
size: 'size',
appearance: { propName: 'appearance', multi: true },
},
})Creates .intent(), .size(), .appearance() chain methods.
Transform dimensions
Mark transform: true to make a dimension receive the accumulated theme from all prior dimensions — ideal for modifiers like outlined that derive from the active state.
const rocketButton = rocketstyle({
dimensions: {
states: 'state',
modifiers: { propName: 'modifier', multi: true, transform: true },
},
})
const Button = rocketButton({ name: 'Button', component: Element })
.theme({ backgroundColor: '#0d6efd', color: '#fff' })
.states({ danger: { backgroundColor: '#dc3545', color: '#fff' } })
.modifiers({
outlined: (theme) => ({
color: theme.backgroundColor, // receives merged theme from prior dimensions
backgroundColor: 'transparent',
}),
})
<Button state="danger" modifier="outlined" /> // outlined sees danger's red, becomes red-on-transparentProvider / Consumer — parent-child state propagation
// Parent provides its state
const ButtonGroup = Button.config({ provider: true })
// Child consumes parent state
const ButtonIcon = rocketstyle()({ name: 'ButtonIcon', component: Element })
.config({
consumer: (ctx) => ctx(({ pseudo }) => ({
state: pseudo.hover ? 'active' : 'default',
})),
})
.states({
default: { color: '#666' },
active: { color: '#fff' },
})
<ButtonGroup state="primary">
<ButtonIcon />
Label
</ButtonGroup>Light / dark mode
Theme + dimension callbacks receive (theme, mode, css). mode === 'light' | 'dark'. Use inversed: true on .config() to flip the mode for a subtree.
Button.theme((theme, mode) => ({
color: mode === 'dark' ? '#fff' : '#1a1a1a',
backgroundColor: mode === 'dark' ? '#333' : '#fff',
}))Performance
Per-definition WeakMap caches keep per-mount cost flat as instance count grows:
_dimensionsCache—getDimensionsMapresult keyed on dimension-themes identity_reservedKeysCache—Object.keys(reservedPropNames)keyed on keywords identity_omitSetCache— pre-builtSet<string>foromit()(avoids per-mount Set allocation)LocalThemeManager— WeakMap tiers for baseTheme, dimensionThemes, and per-mode resolved themes_rsMemo— dimension-prop memo keyed bymode|dimensionPropTuple|pseudoState, LRU-bounded at 128 entries per theme. Hit returns identity-stable{ rocketstyle, rocketstate }so the downstream stylerclassCacheskips resolve entirely. Real-app E2 benchmark: 200 Buttons × 5 runs, baseline dropped from 8.80ms to 4.80ms (-45%); per-Buttonstyler.resolvefrom 22 to 6 (-73%).
Real apps MUST mount one shared <PyreonUI> provider for the memo to span instances — each provider mount creates a fresh enrichedTheme via computed(), which produces a different WeakMap key.
For a 150-component page with 8 dimensions each: ~1,350 Set allocations, ~300 array spreads, and ~150 map rebuilds eliminated vs naive implementation.
Gotchas
.config({ component: NewBase })resetsattrs/priorityAttrs/filterAttrs/composechains — they were tailored to the previous component's prop shape.theme/styles/ dimension chains are preserved. Re-chain shared attrs explicitly if you swap the base.useBooleans: falseis the default (since April 2026 alignment fix). String props are the idiomatic surface.- Cache keys are downstream of normalization. Under
useBooleans: true, the memo correctly keys by the resolved dimension (not the raw boolean prop) — otherwise every boolean variant would collide on the first cached entry. - Dimension props don't accept function accessors directly.
state={() => signal()}is wrong — writestate={signal()}and let the compiler emit reactive_rp()wrapping. Caught by the Reactivity Lens. provider: true+consumer:on the same component is a legal but rare shape. Most apps separate the two for clarity.
Documentation
Full docs: pyreon.dev/docs/rocketstyle (or docs/src/content/docs/rocketstyle.md in this repo).
License
MIT
Native compiler plugin
This package owns its native compilation frontend at @pyreon/rocketstyle/native-plugin. The native CLI discovers that tooling entry through pyreon.native.plugin when the application imports the package. Web runtime imports do not load compiler tooling, and the compiler is an optional peer.
For programmatic compilation, pass the entry's default export to createCompiler({ plugins: [plugin] }). Load the owning plugins for every package the source uses. The compiler supplies generic state, JSX, theme tokens and native style operations; this package supplies its import vocabulary and recognition rules.
