experimental web port of the Union style engine + adapters
styling theming css solidjs react union kde
0

Configure Feed

Select the types of activity you want to include in your feed.

union-web / docs / react-plugin.md
6.3 kB

@union-web/react-plugin#

The React plugin provides runtime style resolution for Union themes. It adapts Union's C++ core concepts (elements, selectors, queries) into React hooks and context, enabling components to resolve their styles on the fly based on role, state, and position in the element tree.

Note: This project uses a ratcheting development style. The SolidJS plugin is the currently active runtime. The react-plugin may lag behind in architecture:

  • Ancestor state propagation — Solid's AncestorElementData includes states; the react-plugin omits it, breaking selectors like toolbar:disabled > button.
  • Typed style props — Solid uses UnionStyleProps + StyledRect (preserves Union's semantic model); the react-plugin uses flattened styleGroupToCss → raw CSS objects.
  • unionIR/unionLint — These Vite plugins have been extracted to @union-web/vite-plugin and re-exported from here. They are shared, not duplicated.

How It Adapts Union's API#

The web has no built-in element tree. The plugin recovers hierarchy through explicit ancestor context:

Union Concept React Plugin Equivalent
Union::Element createElement(role, opts) inside useUnion()
Union::ElementQuery resolveElement() from core
Widget tree (implicit in Qt) UnionProvider chain via React context
Union::StyleRule collection StyleRule[] from UnionIRContext

Components call useUnion() and wrap children in the returned UnionProvider to extend the ancestor chain. This enables selectors like textarea > .placeholder to match because Placeholder can see textarea in its ancestor context. Any third-party component can opt in to Union theming by calling useUnion() with the appropriate role.

API Reference#

UseUnionOptions#

interface UseUnionOptions {
  role: string;
  context?: string[] | null;
  state?: string[];
  hints?: string[];
  attributes?: Record<string, string>;
  ids?: string[];
  debug?: boolean;
}
Option Description
role Element type for matching (e.g., 'button', 'textarea'). Required.
context Explicit ancestor roles. If null, becomes a root element (no ancestors). If unset, inherits from nearest UnionProvider.
state Active states (e.g., ['hovered'], ['visual-focus']).
hints Semantic hints for matching (parallels CSS .hint selectors).
attributes Key-value attribute pairs for attribute selectors.
ids Element IDs for ID selectors.
debug Enable console group logging of selector matching results.

UseUnionResult#

interface UseUnionResult {
  styles: Record<string, StylePropertyGroup>;
  current: StylePropertyGroup;
  UnionProvider: ({ children }: { children: React.ReactNode }) => React.ReactElement;
}
Return Description
styles Pre-computed StylePropertyGroup for all standard state combinations (base, hovered, pressed, checked, visual-focus, active-focus, disabled, highlighted, plus checked combos).
current The StylePropertyGroup for the element's current state.
UnionProvider A component wrapping UnionAncestorContext.Provider. Pushes this element into the ancestor chain for descendant useUnion() calls.

useUnion()#

function useUnion(options: UseUnionOptions): UseUnionResult

The main hook. It:

  1. Reads StyleRule[] from UnionIRContext (set by UnionIRProvider at the app root).
  2. Reads ancestor chain from UnionAncestorContext (set by parent UnionProviders).
  3. Pre-computes all standard state combinations via resolveElement().
  4. Returns { styles, current, UnionProvider }.

If a UnionProvider is not rendered (leaf component), the hook still resolves styles correctly — it just doesn't extend the ancestor chain for any deeper components.

UnionIRProvider#

function UnionIRProvider({ rules, children }: { rules: StyleRule[]; children: React.ReactNode }): React.ReactElement

Root provider. Supplies the parsed CSS rules to all useUnion calls in the tree. Must be placed at the top of the application.

UnionIRContext / UnionAncestorContext#

UnionIRContext: React.Context<{ rules: StyleRule[] }>        // style rules from theme
UnionAncestorContext: React.Context<{ elements: AncestorElementData[] }>  // ancestor chain

unionIR (Vite Plugin)#

Now lives in @union-web/vite-plugin, re-exported from @union-web/react-plugin/vite.

import { unionIR } from '@union-web/react-plugin/vite';
// or directly:
import { unionIR } from '@union-web/vite-plugin/vite';

Build-time plugin that:

  1. Loads and parses defaults CSS, theme CSS, and .colors file.
  2. Resolves all variable expressions (var(), custom-color(), modify-color(), mix()) using KColorSchemeResolver and VariableResolver.
  3. Provides a virtual:union-ir module exporting the resolved StyleRule[] and CSS variable map.
  4. Injects resolved CSS variables into HTML <head>.
  5. Watches input files for HMR.
interface UnionIROptions {
  theme: string;    // path to theme style.css
  colors: string;   // path to .colors file
  defaults: string; // path to defaults CSS
}

Using UnionProvider#

Render a UnionProvider for any component that has descendants that will call useUnion():

function TextArea(props) {
  const { current, UnionProvider } = useUnion({ role: 'textarea', state: [...] });
  return (
    <UnionProvider>
      <textarea style={styleGroupToReact(current)} />
      {!value && placeholder && <Placeholder text={placeholder} />}
    </UnionProvider>
  );
}

// Leaf — no UnionProvider needed
function Placeholder({ text }) {
  const { current } = useUnion({ role: 'placeholder', hints: ['placeholder'] });
  return <div style={styleGroupToReact(current)}>{text}</div>;
}

Debugging#

Pass debug: true to see selector matching in the browser console:

[useUnion] textarea (base) ancestors: [applicationwindow, page, columnlayout]
  ✓ textarea (spec: 0.1.0)    { background-color: #2a2a2a; ... }
  ✓ > .placeholder (spec: 0.2.0)    { color: #808080; }
result: { layout: ..., background: { color: "#2a2a2a" } }