experimental web port of the Union style engine + adapters
styling
theming
css
solidjs
react
union
kde
1# @union-web/react-plugin
2
3The 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.
4
5> **Note:** This project uses a ratcheting development style. The SolidJS plugin is the
6> currently active runtime. The react-plugin may lag behind in architecture:
7>
8> - **Ancestor state propagation** — Solid's `AncestorElementData` includes `states`;
9> the react-plugin omits it, breaking selectors like `toolbar:disabled > button`.
10> - **Typed style props** — Solid uses `UnionStyleProps` + `StyledRect` (preserves Union's
11> semantic model); the react-plugin uses flattened `styleGroupToCss` → raw CSS objects.
12> - **`unionIR`/`unionLint`** — These Vite plugins have been extracted to
13> `@union-web/vite-plugin` and re-exported from here. They are shared, not duplicated.
14
15## How It Adapts Union's API
16
17The web has no built-in element tree. The plugin recovers hierarchy through **explicit ancestor context**:
18
19| Union Concept | React Plugin Equivalent |
20|-------------|------------------------|
21| `Union::Element` | `createElement(role, opts)` inside `useUnion()` |
22| `Union::ElementQuery` | `resolveElement()` from core |
23| Widget tree (implicit in Qt) | `UnionProvider` chain via React context |
24| `Union::StyleRule` collection | `StyleRule[]` from `UnionIRContext` |
25
26Components 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.
27
28## API Reference
29
30### UseUnionOptions
31
32```ts
33interface UseUnionOptions {
34 role: string;
35 context?: string[] | null;
36 state?: string[];
37 hints?: string[];
38 attributes?: Record<string, string>;
39 ids?: string[];
40 debug?: boolean;
41}
42```
43
44| Option | Description |
45|--------|-------------|
46| `role` | Element type for matching (e.g., `'button'`, `'textarea'`). Required. |
47| `context` | Explicit ancestor roles. If `null`, becomes a root element (no ancestors). If unset, inherits from nearest `UnionProvider`. |
48| `state` | Active states (e.g., `['hovered']`, `['visual-focus']`). |
49| `hints` | Semantic hints for matching (parallels CSS `.hint` selectors). |
50| `attributes` | Key-value attribute pairs for attribute selectors. |
51| `ids` | Element IDs for ID selectors. |
52| `debug` | Enable console group logging of selector matching results. |
53
54### UseUnionResult
55
56```ts
57interface UseUnionResult {
58 styles: Record<string, StylePropertyGroup>;
59 current: StylePropertyGroup;
60 UnionProvider: ({ children }: { children: React.ReactNode }) => React.ReactElement;
61}
62```
63
64| Return | Description |
65|--------|-------------|
66| `styles` | Pre-computed `StylePropertyGroup` for all standard state combinations (base, hovered, pressed, checked, visual-focus, active-focus, disabled, highlighted, plus checked combos). |
67| `current` | The `StylePropertyGroup` for the element's current state. |
68| `UnionProvider` | A component wrapping `UnionAncestorContext.Provider`. Pushes this element into the ancestor chain for descendant `useUnion()` calls. |
69
70### useUnion()
71
72```ts
73function useUnion(options: UseUnionOptions): UseUnionResult
74```
75
76The main hook. It:
77
781. Reads `StyleRule[]` from `UnionIRContext` (set by `UnionIRProvider` at the app root).
792. Reads ancestor chain from `UnionAncestorContext` (set by parent `UnionProvider`s).
803. Pre-computes all standard state combinations via `resolveElement()`.
814. Returns `{ styles, current, UnionProvider }`.
82
83If 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.
84
85### UnionIRProvider
86
87```ts
88function UnionIRProvider({ rules, children }: { rules: StyleRule[]; children: React.ReactNode }): React.ReactElement
89```
90
91Root provider. Supplies the parsed CSS rules to all `useUnion` calls in the tree. Must be placed at the top of the application.
92
93### UnionIRContext / UnionAncestorContext
94
95```ts
96UnionIRContext: React.Context<{ rules: StyleRule[] }> // style rules from theme
97UnionAncestorContext: React.Context<{ elements: AncestorElementData[] }> // ancestor chain
98```
99
100### unionIR (Vite Plugin)
101
102Now lives in `@union-web/vite-plugin`, re-exported from `@union-web/react-plugin/vite`.
103
104```ts
105import { unionIR } from '@union-web/react-plugin/vite';
106// or directly:
107import { unionIR } from '@union-web/vite-plugin/vite';
108```
109
110Build-time plugin that:
111
1121. Loads and parses defaults CSS, theme CSS, and `.colors` file.
1132. Resolves all variable expressions (`var()`, `custom-color()`, `modify-color()`, `mix()`) using `KColorSchemeResolver` and `VariableResolver`.
1143. Provides a `virtual:union-ir` module exporting the resolved `StyleRule[]` and CSS variable map.
1154. Injects resolved CSS variables into HTML `<head>`.
1165. Watches input files for HMR.
117
118```ts
119interface UnionIROptions {
120 theme: string; // path to theme style.css
121 colors: string; // path to .colors file
122 defaults: string; // path to defaults CSS
123}
124```
125
126## Using `UnionProvider`
127
128Render a `UnionProvider` for any component that has descendants that will call `useUnion()`:
129
130```tsx
131function TextArea(props) {
132 const { current, UnionProvider } = useUnion({ role: 'textarea', state: [...] });
133 return (
134 <UnionProvider>
135 <textarea style={styleGroupToReact(current)} />
136 {!value && placeholder && <Placeholder text={placeholder} />}
137 </UnionProvider>
138 );
139}
140
141// Leaf — no UnionProvider needed
142function Placeholder({ text }) {
143 const { current } = useUnion({ role: 'placeholder', hints: ['placeholder'] });
144 return <div style={styleGroupToReact(current)}>{text}</div>;
145}
146```
147
148## Debugging
149
150Pass `debug: true` to see selector matching in the browser console:
151
152```
153[useUnion] textarea (base) ancestors: [applicationwindow, page, columnlayout]
154 ✓ textarea (spec: 0.1.0) { background-color: #2a2a2a; ... }
155 ✓ > .placeholder (spec: 0.2.0) { color: #808080; }
156result: { layout: ..., background: { color: "#2a2a2a" } }
157```