themed-styler 0.1.7

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)

```text
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: <value>) 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):

```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):

```yaml
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:

```text
# 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.

```json
{
  "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 <style> 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 <style> 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):

```text
// 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:

```text
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.