themed-styler 0.1.6

Client-side runtime styling engine for web and React Native with theme support and Tailwind subset
Documentation

themed-styler

Client-side runtime styling engine for web and React Native with theme support and Tailwind-style utility classes defined in themes. It stores theme-aware selector styles using CSS property names and, at render time, infers which rules to emit from observed usage (tags, classes, and tag+class pairs). It can output either:

  • Web CSS for currently-used selectors/classes
  • React Native style objects for a selector combined with Tailwind-like utility classes

This crate is designed to be embedded in clients or tooling. A CLI wrapper is available via the hook-transpiler-cli crate.

Features

  • Theme registry with default/current theme switching
  • CSS-attribute-based style storage per selector
  • Per-theme variables and breakpoints (xs, sm, md, lg, xl), with inheritance
  • Tailwind utilities are provided by the theme (no whitelist). The default theme ships with a minimal set and other themes inherit from it.
  • Output:
    • Web: flat CSS string for currently used selectors/classes
    • React Native: camelCased style object with basic unit conversion (e.g., "8px" → 8)
  • Render-time inference of usage (no exact-string selector tracking): emits rules for observed tags, classes, and tag+class pairs without deep hierarchy selectors

Quick start (Rust)

use themed_styler::api::State;
use indexmap::IndexMap;

// 1) Start with a default state
let mut st = State::new_default();

// 2) Register what is currently used in your app (structured usage)
st.register_tags(["body".to_string(), "button".to_string()]);
st.register_tailwind_classes(["p-2".to_string()]);
st.register_tag_class("h1", "text-sm");

// 3) Emit CSS for the web
let css = st.css_for_web();
println!("{}", css);

// 4) Emit RN styles for a specific selector + classes
let rn = st.rn_styles_for("button", &["p-2".into()]);
println!("{}", serde_json::to_string_pretty(&rn).unwrap());

// 5) Themes/variables/breakpoints can be customized
st.set_theme("light").ok();
st.set_variables(IndexMap::from([
    ("primary".into(), "#2563eb".into()),
]));
st.set_breakpoints(IndexMap::from([
    ("md".into(), "768px".into()),
]));

Themed-styler utilities

  • There is no runtime whitelist or generator anymore. Classes used at runtime are matched directly against the active theme’s selectors (for example, the class "p-2" is looked up as ".p-2" in the theme; "hover:p-2" is looked up as ".p-2:hover").
  • The crate bundles a default YAML theme that includes a small subset of utilities as examples; apps can extend it by adding selectors to their own theme(s).

Newly supported dynamic utilities (generated at runtime if not present in the theme):

  • rounded*: border radius utilities
    • rounded, rounded-sm, rounded-md, rounded-lg, rounded-xl, rounded-2xl, rounded-3xl, rounded-full
    • side variants: rounded-t, rounded-r, rounded-b, rounded-l (with optional size suffix, e.g., rounded-t-lg)
  • cursor-*: sets CSS cursor (pointer, default, text, move, wait, not-allowed, etc.)
  • transition*: transition shorthands
    • transition / transition-all → transition-property: all; transition-duration: 150ms; ease-in-out
    • transition-none → disables transitions
    • transition-colors | transition-opacity | transition-transform | transition-shadow → limits transition-property with default duration/ease
  • Width utilities: w-, min-w-, max-w-*
    • Numeric scale: w-2 ⇒ width: 8px (n×4px)
    • Fractions: w-1/2 ⇒ width: 50%
    • Tokens: w-full (100%), w-screen (100vw), w-px (1px); min/max variants mirror the same mapping

Display, flex, hover, and breakpoints

  • Display: block, inline-block, inline, inline-flex, grid, hidden
  • Flex: flex, flex-row, flex-col, flex-1; alignment helpers items-, justify-
  • Pseudo/state: hover: prefix supported anywhere in the token chain (e.g., hover:block, md:hover:flex)
  • Breakpoints: xs:, sm:, md:, lg:, xl: prefixes wrap the rule in @media (min-width: ) using the active theme’s breakpoints
    • Example: md:flex → @media (min-width: 768px) { .flex { display:flex } }
    • Example: md:hover:block → @media (min-width: 768px) { .block:hover { display:block } }

Notes:

  • React Native output will camelCase properties and convert px values to numbers when applicable (e.g., border-radius → borderRadius, width: "8px" → 8). Properties without RN equivalents (e.g., cursor, transition) are harmless and may be ignored on RN.
  • RN ignores hover and breakpoint prefixes at runtime and applies the base class styles (e.g., md:flex → display:flex in RN output).

Theme format and inheritance

  • The default state is defined in YAML and bundled with the crate: crates/themed-styler/theme.yaml.
  • Each theme entry contains selectors, variables, and breakpoints, plus an optional inherits pointing to a parent theme.
  • Inheritance merges default → parent(s) → child, with the child overriding parent/default on conflicts. This lets the default define canonical utility classes and common selectors, while other themes only specify overrides.

Example (YAML):

themes:
  default:
    selectors:
      ".p-2": { padding: "8px" }
    variables:
      text: "#111b26"
      bg: "#ffffff"
    breakpoints:
      xs: "480px"
      md: "768px"
  dark:
    inherits: default
    selectors:
      body: { color: "#f8fafc" }
    variables:
      bg: "#0f172a"
default_theme: default
current_theme: dark

On load and theme switch, themed-styler computes effective selectors, variables, and breakpoints by following the inherits chain and finally the default theme.

Variables and breakpoints (per-theme)

  • Variables and breakpoints live inside each theme under variables and breakpoints.
  • Resolution order for variables: legacy global variables (lowest) → default theme variables → parent theme variables (via inherits chain) → current theme variables (highest). Breakpoints follow the same order.
  • React Native output resolves var tokens too. Supported syntaxes: var(--name), var(name), and $name.

Example YAML (variables and breakpoints):

themes:
  default:
    selectors:
      body:
        background-color: "var(bg)"
        color: "var(text)"
    variables:
      bg: "#ffffff"
      text: "#111b26"
    breakpoints:
      xs: "480px"
      sm: "640px"
default_theme: default
current_theme: default

CLI integration

The hook-transpiler-cli crate embeds themed-styler state management as subcommands. Example workflow:

# Initialize a state file
hook-transpiler-cli style init --file .themed-styler-state.json

# Register usage (selectors/classes)
hook-transpiler-cli style register-selectors --file .themed-styler-state.json body button
hook-transpiler-cli style register-classes   --file .themed-styler-state.json p-2 hover:mx-1

# Output web CSS (stdout)
hook-transpiler-cli style css --file .themed-styler-state.json

# Output RN style object for a selector + classes
hook-transpiler-cli style rn --file .themed-styler-state.json button p-2

State format

State can be serialized/deserialized for tooling. Internally the crate uses Serde and accepts JSON; the built-in default is YAML.

{
  "themes": {
    "default": {
      "selectors": {
        "body": { "background-color": "var(bg)", "color": "var(text)" }
      },
      "variables": { "bg": "#ffffff", "text": "#111b26" },
      "breakpoints": { "xs": "480px", "md": "768px" }
    },
    "dark": {
      "inherits": "default",
      "selectors": { "body": { "color": "#f8fafc" } },
      "variables": { "bg": "#0f172a" }
    }
  },
  "default_theme": "default",
  "current_theme": "default",
  "used_tags": ["body", "button"],
  "used_classes": ["p-2", "hover:mx-1"],
  "used_tag_classes": ["h1|text-sm"]
}

Status & next steps

  • Core theme storage & usage tracking: State captures selectors/styles per theme with dark/light defaults and now records structured runtime usage via register_tags, register_tailwind_classes, and register_tag_class. The runtime wrappers keep the state in sync with components that render and unmount.
  • Theme overrides, variables, and breakpoints: CLI helpers (add_theme, set_vars, set_bps, set_theme) are in place, yet we still need a shared theme file that exposes the default/dark/light combinations and gives consumers a place to override selectors before the runtime or tooling reads them.
  • Web CSS & RN output: css_for_web and rn_styles_for (plus the CLI style css/style rn commands) infer emission at render time from observed tags/classes/tag+class pairs. No exact-string used_selectors filtering is required.
  • Runtime Tailwind support: Tailwind utilities are theme-defined (no whitelist). The default YAML includes a minimal set (e.g., .p-2, .hover\:mx-1:hover) and apps can add more in their themes.
  • Client-web stylesheet updates: The web side should repaint or replace the global stylesheet whenever observed usage changes. A global style manager should call css_for_web() after render to update a single tag.
  • Tailwind/nativewind removal & custom wrapper: The web and RN clients still ship with their existing Tailwind stylesheets and nativewind wiring. We need to delete index.css/globals.css and any Tailwind/nativewind references, then rewire both apps to use the themed-styler binary via the custom styled wrapper.
  • Example theme file & hooks: While State::new_default provides in-memory defaults, there is no filesystem example that can be shared with the template repo. We should add a JSON theme file (with default aliasing dark, plus explicit dark and light) and refactor template-ui/theme.js to use the new hooks to set selectors/themes/values instead of the old boilerplate.
  • React Native selectors: The backend supports selector registration; wrappers should capture the intended tag string used in themes (e.g., div, span, h1) so cross-platform selectors like div.primary remain coherent.
  • CLI commands & tests: The CLI already exposes style init, register-selectors, register-classes, css, and rn, but the requirements also expect exposing theme/variables/breakpoint updates and tailored outputs for selectors. We need new unit tests that exercise set-theme, add-theme, set-vars, etc., to ensure the commands manipulate the state as expected.
  • Tailwind CSS files: Legacy Tailwind CSS imports should be removed from clients; runtime styling comes from themed-styler + theme utilities.

Default theme file

A default YAML state is bundled at crates/themed-styler/theme.yaml and loaded automatically by State::new_default(). You can still manage state via JSON with the CLI if preferred.

Notes about selectors and HTML tag usage

  • Use HTML tags for all selectors in themes (for example div, button, h1). The React Native app will render JSX div into a styled native View at runtime, so keeping HTML tags in themes makes selector logic identical between web and RN.
  • Themes store selectors as literal keys (e.g., h1, .text-sm, h1.text-sm). At runtime, the engine observes tags/classes and infers matches: a usage of h1.text-sm implies both h1 and .text-sm are eligible for emission.

Runtime integration (overview)

  • The next step is adding a small createElement wrapper / hooks in the template/client apps that:
    • preserve the original string tag used in JSX (e.g., div) so selectors like div[type=primary] or div.myClass can match in RN and web alike,
    • call the runtime to register_selectors on mount and unregister on unmount (to keep used_selectors accurate), and
    • on the web, call the style manager to fetch css_for_web() and write it into a single global tag when the used selectors/classes set changes.

We'll implement these runtime helpers in template-ui as the next task and provide concrete JS/TS files and usage examples for both client-web and client-react-native.

Notes

  • Store attributes using CSS property names; RN output will camelCase and convert px to numbers when possible.
  • CSS output is flat by design initially; media queries may be emitted in future iterations.
  • Only styles for registered selectors/classes are emitted to keep output minimal.

License

MIT OR Apache-2.0

Web wrapper example (TSDiv)

The client-web app uses a very small wrapper component named TSDiv that reports usage to the unified themed-styler bridge and renders a normal DOM element. This keeps usage tracking opt-in, instead of wrapping every element globally.

Example (simplified):

// apps/client-web/src/components/TSDiv.tsx
import React, { useEffect } from 'react'
import { unifiedBridge, styleManager } from '@relay/shared'

type DivProps = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
  tag?: string
}

export const TSDiv: React.FC<DivProps> = ({ children, tag = 'div', ...props }) => {
  useEffect(() => {
    try {
      unifiedBridge.registerUsage(tag, props as any)
      styleManager.requestRender()
    } catch {}
  }, [props.className])

  return React.createElement(tag, props, children)
}

Usage in the app:

import { TSDiv } from './components/TSDiv'

// Replace <div> with <TSDiv> and pass className normally
<TSDiv className="flex flex-col w-screen h-screen">
  ...
</TSDiv>

Notes:

  • The wrapper passes the same tag string (default 'div', or via tag prop) to registerUsage so selectors like div.myClass can match across web and RN.
  • Only wrapped elements are registered; this prevents over-collecting and keeps the emitted stylesheet minimal.

Hierarchy selectors

Deep descendant selectors (e.g., div div div span) are not generated. Emission is based on tags, classes, and tag+class pairs only. This keeps CSS lightweight, decoupled from DOM depth, and easier to override.