# @union-web/solid-plugin The Solid plugin provides runtime style resolution for Union themes. Components declare themselves via `useUnion()`, wrap children in `UnionProvider` to extend the ancestor context chain, and render using `StyledRect` (for visual layers) or plain elements (for text/layout). This is the **currently active output runtime**. The react-plugin may lag behind in architecture (e.g., ancestor state propagation, typed `UnionStyleProps` model). ## Architecture The web has no built-in element tree. The plugin recovers hierarchy through **explicit ancestor context**: | Union Concept | Solid Plugin Equivalent | |-------------|------------------------| | `Union::Element` | `createElement(role, opts)` inside `useUnion()` | | `Union::ElementQuery` | `resolveElement()` from core | | Widget tree (implicit in Qt) | `UnionProvider` chain via Solid context | | `Union::StyleRule` collection | `StyleRule[]` from `UnionIRContext` | | Platform renderer applies styles | `styleGroupToUnionStyleProps()` → `UnionStyleProps` → `StyledRect` | ## Key Differences from React Plugin 1. **Ancestor state propagation** — Solid's `AncestorElementData` includes a `states` field (the react-plugin omits it). This enables selectors like `toolbar:disabled > button` to match correctly. 2. **Typed style props** — Solid uses `UnionStyleProps` (typed properties matching Union's property groups) + `StyledRect` component instead of raw style object. This preserves Union's semantic model (alignment, inset vs margin, per-side border/outline, shadow spread+blur). 3. **Solid reactive primitives** — Signals, memos, and `createMemo` replace React's `useState`/`useMemo`. State derivations are reactive. ## API Reference ### useUnion() ```ts function useUnion(options: UseUnionOptions): UseUnionResult ``` **Options:** | Option | Type | Description | |--------|------|-------------| | `role` | `string` | Element type for matching (e.g., `'button'`, `'textarea'`). Required. | | `context` | `string[] \| null` | Explicit ancestor roles. `null` = root. Unset = inherit from nearest `UnionProvider`. | | `state` | `string[] \| (() => string[])` | Active states (e.g., `['hovered']`, `['visual-focus']`). Can be a signal getter. | | `hints` | `string[] \| (() => string[])` | Semantic hints for matching (parallels CSS `.hint` selectors). | | `attributes` | `Record \| (() => Record)` | Key-value attribute pairs for attribute selectors. | | `ids` | `string[] \| (() => string[])` | Element IDs for ID selectors. | | `debug` | `boolean` | Enable console group logging of selector matching. | **Returns:** | 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 (reactive signal). | | `UnionProvider` | A component wrapping `UnionAncestorContext.Provider`. Pushes this element into the ancestor chain for descendant `useUnion()` calls. | The `current` value is a reactive getter (`() => StylePropertyGroup`). It resolves the element's styles using the ancestor chain and current state. State combinations not pre-computed are resolved on the fly. ### StyledRect ```tsx {children} ``` Base component that renders a styled DOM element from `UnionStyleProps`. Maps Union's property groups to CSS: - **Layout** — `width`/`height` → `min-width`/`min-height` (Union's width/height are implicit size hints, not fixed sizes); `spacing` → `gap`; `padding` → `padding` - **Margin** — `margin` → CSS `margin` (sub-element spacing). Union's `inset` property (background-container shrink) is wholly owned by the Positioner — StyledRect never maps it. - **Background** — `backgroundColor` → `background-color` - **Border** — per-side width/color/style → CSS border properties - **Corners** — `borderRadius` → `border-radius` (including per-corner) - **Shadow** → `box-shadow` (offset + blur + spread + color) - **Outline** → native CSS `outline` + `outline-offset` (flattened: per-side widths/ colors/styles are dropped in favor of the first defined side). The full-fidelity pseudo-element approach (negative CSS `inset` + `border` for per-side control) is deferred; see `StyledRect.tsx` TODO. - **Text** — color, font-size, font-weight, font-family, `textWrapMode` **Not all components need `StyledRect`.** Components with only text properties (`Text`, `Heading`) or only layout (`DialogButtonBox`, `ColumnLayout`, `RowLayout`) should render a plain element to avoid `StyledRect`'s `display: flex` base styles. ### Positioner > **Status: Experimental, in-progress — nothing is final.** ```tsx ``` A layout component that manages the positioning of a control's internal children (icon, text, indicator, handle). Corresponds to upstream Union's `PositionerLayout` engine. Children declare their alignment via `useUnion`'s `layoutAlignment*` fields and are automatically sorted into buckets (start/center/end/fill) within three containers (item, background, content). Key ideas: - **Owns `inset`** — the background-container shrink is the Positioner's responsibility. `StyledRect` never maps it. - **Child registration** — children use `PositionerContext` (automatically wired by `useUnion`) to register their alignment and container. - **Three containers** — item (full box), background (shrunk by inset), content (shrunk by padding). Background and content containers render as overlay `
`s with `position: absolute; inset: 0`. This component will be iterated on. See `packages/solid-plugin/src/Positioner.tsx` and the design reference at `packages/positioner-sandbox/README.md`. ### UnionIRProvider ```tsx ``` Root provider. Supplies the parsed CSS rules to all `useUnion` calls in the tree. Must be placed at the top of the application. ### Virtual Module: `virtual:union-ir` ```ts import { rules } from 'virtual:union-ir'; ``` Provided by the Vite plugin (`@union-web/vite-plugin`). Exports the resolved `StyleRule[]` array from the theme CSS. ## Components | Component | `useUnion` role | States | Sub-parts | StyledRect? | |-----------|----------------|--------|-----------|-------------| | `ApplicationWindow` | `applicationwindow` | — | — | No (plain `
`, picks bg/color/padding/spacing) | | `ApplicationHeader` | `applicationheader` | — | — | Yes (background + cond border via `.with-separator`) | | `Page` | `page` | — | — | No (plain `
`, picks bg/color/padding/spacing) | | `ColumnLayout` | `columnlayout` | — | — | No (plain `
`) | | `RowLayout` | `rowlayout` | — | — | No (plain `
`) | | `Button` | `button` | hovered, pressed, disabled | — | Yes (all visual layers) | | `Checkbox` | `checkbox` | hovered, pressed, checked, disabled | indicator (role `indicator`, hints `indicator`) | Yes (main uses StyledRect, indicator sub-element) | | `Switch` | `switch` | hovered, pressed, checked, disabled | indicator (role `indicator`, hints `indicator`), handle (role `handle`, hints `handle`) | Yes (main + indicator + handle all use StyledRect) | | `TextArea` | `textarea` | hovered, visual-focus, disabled | placeholder (separate component, role `placeholder`, hints `placeholder`) | Yes (textarea via StyledRect) + plain div (placeholder) | | `Text` | `text` | — | — | No (plain `
`, text props only) | | `Heading` | `heading` | — | hints: `level-N` (+ `primary`) | No (plain ``, text props only) | | `ItemDelegate` | `itemdelegate` | hovered, pressed, highlighted | — | Yes (background + cond border) | | `Drawer` | `drawer` | — | — | Yes (background) | | `Toolbar` | `toolbar` | — | hints: `header` or `footer` for position | Yes (background + cond border) | | `DialogButtonBox` | `dialogbuttonbox` | — | — | No (plain `
`) | | `Spacer` | none | — | — | No (plain `
`) | ## Using `UnionProvider` Each component that can contain other Union-aware components should render `UnionProvider` to extend the ancestor chain: ```tsx function MyContainer(props) { const { current, UnionProvider } = useUnion({ role: 'container' }); const styleProps = createMemo(() => styleGroupToUnionStyleProps(current())); return ( {props.children} ); } // Leaf — no UnionProvider needed function MyLabel(props) { const { current } = useUnion({ role: 'text' }); // ...render plain element with resolved text styles } ``` **Best practice:** Always render `UnionProvider` unless you are certain the component is a leaf (has no descendants that call `useUnion`). If a leaf later gains Union-aware children, forgetting to add `UnionProvider` will produce a broken ancestor chain. ## Debugging Pass `debug: true` to any `useUnion` call 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" } } ``` ## Application Setup ```tsx import { UnionIRProvider } from '@union-web/solid-plugin'; import { rules } from 'virtual:union-ir'; render(() => ( ), document.getElementById("root")); ``` ### Vite Configuration The `virtual:union-ir` module is provided by the `@union-web/vite-plugin` package. In your `vite.config.ts`: ```ts import { unionIR } from '@union-web/vite-plugin/vite'; export default defineConfig({ plugins: [ unionIR({ theme: path.resolve(__dirname, "input/themes/breeze/style.css"), colors: path.resolve(__dirname, "input/colors/BreezeLight.colors"), defaults: path.resolve(__dirname, "input/defaults/default.css"), }), solidPlugin(), ], }); ``` The `unionIR()` plugin: - Parses the theme CSS and defaults at build time - Resolves all variable expressions (`var()`, `custom-color()`, `modify-color()`, `mix()`) - Provides the `virtual:union-ir` module with resolved `StyleRule[]` - Injects resolved CSS variables into HTML `` - Watches input files for HMR in dev mode