Skip to content

Commit 42ea9a4

Browse files
committed
Add CSSX provider theming primitives
1 parent de29e05 commit 42ea9a4

39 files changed

Lines changed: 2724 additions & 199 deletions

AGENTS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ CSSX is a monorepo for a CSS-in-JS toolchain. Users write `css`, `styl`, or opti
3232
- Compiled sheets are JSON-serializable IR. Runtime cache/tracking state must stay outside the sheet.
3333
- `part='root'` maps to `style`; other parts map to `{partName}Style`.
3434
- `:hover` and `:active` compile to `hoverStyle` and `activeStyle`.
35+
- Provider/global `:root` custom properties compile to `sheet.rootVariables` and resolve as scoped defaults below runtime `variables` and above `defaultVariables`.
36+
- Component tag selectors such as `Button` and `Button:part(text)` apply only inside components wrapped with `themed('Button', Component)` or explicit resolver tag options.
37+
- `variables` and `defaultVariables` are validating proxies. Direct assignment/deletion works for valid `--name` keys; bulk updates use `.assign()`, `.set()`, and `.clear()`.
3538
- Local JS template interpolation is lowered to synthetic `var(--__cssx_dynamic_N)` slots and passed as `values`.
3639
- `cache: 'teamplay'` remains accepted as a Babel option for compatibility, but runtime caching is owned by `@cssxjs/css-to-rn`, not Teamplay.
3740

architecture.md

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -230,14 +230,21 @@ Supported selectors:
230230
- `.root`
231231
- `.root.active`
232232
- `.root:part(label)`
233+
- `.root::part(label)`
233234
- `.root.active:part(icon)`
234235
- `.root:hover`
235236
- `.root:active`
237+
- `Button`
238+
- `Button.primary`
239+
- `Button:part(text)`
240+
- `Button::part(text)`
241+
- `Button.primary:part(text)`
236242
- `:export`
243+
- bare `:root` custom-property declarations
237244

238-
`:hover` maps to `hoverStyle`; `:active` maps to `activeStyle`. Unsupported selectors are ignored with diagnostics in runtime mode.
245+
`:hover` maps to `hoverStyle`; `:active` maps to `activeStyle`. Tag selectors apply only when the current component tag is provided by `themed(tagName, Component)` or explicit resolver options. Unsupported selectors are ignored with diagnostics in runtime mode.
239246

240-
`:root` custom-property declarations and declaration-level custom properties are intentionally not used as defaults. Use `setDefaultVariables()` for defaults.
247+
Bare `:root` custom-property declarations are compiled into `sheet.rootVariables` and become scoped defaults when the sheet is supplied through `CssxProvider style` or another layer. Declaration-level custom properties outside `:root` are ignored with diagnostics.
241248

242249
### Value Resolution
243250

@@ -246,13 +253,17 @@ Supported selectors:
246253
1. Replace interpolation slots from `values`.
247254
2. Recursively resolve nested `var()`.
248255
3. Resolve `u`, viewport units, and supported `calc()`.
249-
4. Return dependencies for variables and dimensions.
256+
4. Normalize supported modern color functions (`oklch()`, `oklab()`, `color-mix()`) to `rgba(...)`.
257+
5. Return dependencies for variables and dimensions.
250258

251259
Variable priority is:
252260

253-
1. runtime `variables['--name']`
254-
2. `defaultVariables['--name']`
255-
3. inline fallback `var(--name, fallback)`
261+
1. template interpolation values.
262+
2. runtime `variables['--name']`.
263+
3. nearest scoped provider/sheet `:root` variable.
264+
4. outer scoped provider/sheet `:root` variables.
265+
5. `defaultVariables['--name']`.
266+
6. inline fallback `var(--name, fallback)`.
256267

257268
Unresolved variables, cycles, depth limits, invalid interpolations, and unsupported `calc()` invalidate only the containing declaration. Earlier fallback declarations in the same rule still apply.
258269

@@ -288,7 +299,7 @@ Resolver order:
288299

289300
1. Normalize `styleName` with classcat-like semantics.
290301
2. Normalize one or more sheet layers.
291-
3. Match selectors by class set.
302+
3. Match selectors by component tag and class set.
292303
4. Filter inactive media rules.
293304
5. Group by output prop: `style`, `{part}Style`, `hoverStyle`, `activeStyle`.
294305
6. Apply cascade by layer, specificity, and source order.
@@ -307,14 +318,16 @@ Runtime caches are bounded. Static cache keys include sheet identity, style name
307318

308319
Key pieces:
309320

310-
- `store.ts`: `variables`, `defaultVariables`, `setDefaultVariables()`, dimensions/media state, microtask-batched notifications.
321+
- `store.ts`: `variables`, `defaultVariables`, `setDefaultVariables()`, variable bulk methods, dimensions/media state, microtask-batched notifications.
311322
- `tracker.ts`: `TrackedCssxSheet`, committed dependency snapshots, per-tracker cache.
312323
- `cssx.ts`: ergonomic `cssx()` wrapper that delegates to `resolveCssx()` and records dependencies into tracked sheets during render.
313-
- `hooks.ts`: `useCssxSheet()`, `useRuntimeCss()`, `useCssxTemplate()`, `useCssxLayer()`.
314-
- `config.ts`: optional `CssxProvider`, `configureCssx()`, and `useCssxConfig()`.
324+
- `hooks.ts`: `useCssxSheet()`, `useRuntimeCss()`, `useCssxTemplate()`, `useCssxLayer()`, `useCssVariable()`, `useCssVariableRaw()`, `getCssVariable()`, and `getCssVariableRaw()`.
325+
- `config.ts`: optional `CssxProvider`, `configureCssx()`, `useCssxConfig()`, and `themed()`.
315326

316327
`useCssxSheet()` starts a render-local dependency collection before render and commits it in a layout/effect phase. If a render is aborted, for example because a component throws a promise into Suspense, the pending dependencies are not committed and do not leak global subscriptions.
317328

329+
`CssxProvider style` accepts raw CSS strings, compiled sheets, tracked sheets, layer objects, arrays, and falsey values. Provider layers are appended after parent provider layers and before component-local layers. Nested providers append additional `:root` variable scopes, with inner scopes winning over outer scopes. `themed()` adds the current component tag and a render-local dependency tracker so provider/global styles that read variables can update themed components even when they have no local sheet.
330+
318331
Variable writes and deletes notify subscribers once per microtask. Subscribers only rerender when a variable they actually used changes. Viewport-unit subscribers are tied to dimension changes. Media-query dependencies store the match value observed during the committed render; dimension changes and platform media adapter changes only rerender subscribers whose committed media result changed. Browser `matchMedia` is used on web when available, and tests can install a media-query adapter for non-DOM media features such as `prefers-color-scheme`, `hover`, and `pointer`. Web resize uses leading plus trailing debounced updates.
319332

320333
## Loaders And Separate Files

docs/api/css.md

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,27 @@ shorthands, comma-separated value chunks, and nested fallbacks.
132132
}
133133
```
134134

135+
Provider/global CSS can define subtree-scoped variables with `:root`:
136+
137+
```css
138+
:root {
139+
--primary-color: oklch(62% 0.18 250);
140+
}
141+
```
142+
143+
Those variables are scoped by `CssxProvider`, not stored as global defaults.
144+
145+
### Modern Color Functions
146+
147+
CSSX resolves `oklch()`, `oklab()`, and `color-mix()` to legacy `rgba(...)`
148+
strings so the same CSS works on React Native:
149+
150+
```css
151+
.button {
152+
background-color: color-mix(in oklch, var(--brand), white 20%);
153+
}
154+
```
155+
135156
### JavaScript Interpolation
136157

137158
Function-scoped `css` templates support JavaScript interpolation in CSS value
@@ -162,11 +183,30 @@ must use plain CSS text.
162183
color: red;
163184
}
164185

165-
.button:part(text) {
186+
.button::part(text) {
166187
font-weight: bold;
167188
}
168189
```
169190

191+
Both `:part()` and `::part()` are supported.
192+
193+
### Component Tag Selectors
194+
195+
Provider/global CSS can target components wrapped with `themed()` by tag:
196+
197+
```css
198+
Button {
199+
background: var(--button-bg);
200+
}
201+
202+
Button.primary:part(text) {
203+
color: white;
204+
}
205+
```
206+
207+
Tag selectors are intended for global component overrides. Class selectors still
208+
work as utility classes everywhere.
209+
170210
### Hover and Active Styles
171211

172212
CSSX maps `:hover` and `:active` to the same output as `:part(hover)` and

docs/api/index.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@ import {
1414
defaultVariables,
1515
cssx,
1616
useRuntimeCss,
17+
useCssVariable,
18+
useCssVariableRaw,
1719
useCssxSheet,
1820
useCssxTemplate,
1921
CssxProvider,
20-
configureCssx
22+
configureCssx,
23+
themed
2124
} from 'cssxjs'
2225
```
2326

@@ -46,12 +49,15 @@ import {
4649
| `styl` | Template literal / Function | Write styles in Stylus syntax, or apply styles via spread |
4750
| `css` | Template literal | Write styles in plain CSS syntax |
4851
| `pug` | Template literal | Write JSX in Pug syntax, with TypeScript expressions and embedded `style` blocks |
49-
| `variables` | Observable object | Set CSS variable values at runtime |
50-
| `setDefaultVariables` | Function | Set default CSS variable values |
51-
| `defaultVariables` | Object | Read-only default variable values |
52+
| `variables` | Reactive object | Set CSS variable values at runtime; supports `.assign()`, `.set()`, `.clear()` |
53+
| `setDefaultVariables` | Function | Replace default CSS variable values |
54+
| `defaultVariables` | Reactive object | Default variable values; supports `.assign()`, `.set()`, `.clear()` |
5255
| `cssx` | Function | Resolve a runtime sheet and `styleName` to props |
5356
| `useRuntimeCss` | Hook | Compile runtime CSS text into a tracked sheet |
57+
| `useCssVariable` | Hook | Read a CSS variable as an RN-friendly value and subscribe to it |
58+
| `useCssVariableRaw` | Hook | Read a CSS variable as raw resolved CSS text |
5459
| `useCssxSheet` | Hook | Track an already compiled sheet |
5560
| `useCssxTemplate` | Hook | Track a compiled sheet with interpolation values |
56-
| `CssxProvider` | Component | Provide runtime options to a subtree |
61+
| `CssxProvider` | Component | Provide runtime options and global/scoped CSS to a subtree |
62+
| `themed` | Function | Give a component a CSS tag for provider/global component overrides |
5763
| `configureCssx` | Function | Configure global runtime defaults |

docs/api/runtime.md

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,61 @@ Only variables used by the resolved element are tracked. If `--border-color`
120120
changes, elements that used it update. If an unrelated variable changes, they do
121121
not.
122122
123+
## Provider Styles
124+
125+
`CssxProvider` can provide global CSS to a subtree through its `style` prop.
126+
Provider styles can define utility classes, component tag overrides, and scoped
127+
`:root` variables:
128+
129+
```jsx
130+
import { CssxProvider, themed } from 'cssxjs'
131+
132+
const Button = themed('Button', function Button({ children }) {
133+
return (
134+
<Pressable part="root">
135+
<Text part="text">{children}</Text>
136+
</Pressable>
137+
)
138+
})
139+
140+
function App() {
141+
return (
142+
<CssxProvider style={`
143+
:root { --brand: oklch(62% 0.18 250); }
144+
.p2 { padding: 2u; }
145+
Button { background: var(--brand); }
146+
Button:part(text) { color: white; }
147+
`}>
148+
<Button styleName="p2">Save</Button>
149+
</CssxProvider>
150+
)
151+
}
152+
```
153+
154+
Nested providers override outer `:root` variables for their subtree. Runtime
155+
`variables['--name']` still has higher priority than provider `:root` values.
156+
157+
Use `themed(tagName, Component)` for components that should be addressable by
158+
tag selectors in provider/global CSS. Class selectors remain global utilities
159+
and do not require a tag.
160+
161+
## Reading Variables In JS
162+
163+
Use `useCssVariable()` when component logic needs the resolved value:
164+
165+
```jsx
166+
import { useCssVariable } from 'cssxjs'
167+
168+
function Avatar() {
169+
const size = useCssVariable('--avatar-size', '4u') // 32
170+
return <Image style={{ width: size, height: size }} />
171+
}
172+
```
173+
174+
`useCssVariable()` returns an RN-friendly value: `2u` and `16px` become numbers,
175+
percentages stay strings, and modern color functions are normalized. Use
176+
`useCssVariableRaw()` when you need the raw resolved CSS string.
177+
123178
## Media Queries
124179
125180
Runtime CSS can use media queries:
@@ -164,6 +219,9 @@ useCssxTemplate(compiledSheet, values, options?)
164219
useCssxLayer(input, options?)
165220
CssxProvider
166221
configureCssx(options)
222+
themed(tagName, Component)
223+
useCssVariable(name, fallback?)
224+
useCssVariableRaw(name, fallback?)
167225
```
168226
169227
`useCssxSheet()` tracks an already compiled sheet. `useCssxTemplate()` is used by
@@ -172,7 +230,8 @@ accepts strings, compiled sheets, tracked sheets, or layer objects and returns
172230
the tracked equivalent.
173231
174232
`CssxProvider` and `configureCssx()` configure runtime defaults such as target
175-
and dimension debounce behavior.
233+
and dimension debounce behavior. `CssxProvider` also accepts a `style` prop for
234+
subtree-scoped CSS.
176235
177236
## Platform Resolution
178237

docs/api/variables.md

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ CSSX provides a reactive system for CSS variables that works at runtime.
44

55
## variables
66

7-
A reactive object for setting CSS variable values at runtime. Assigning values triggers automatic re-renders in components using those variables.
7+
A reactive object for setting CSS variable values at runtime. Assigning values
8+
triggers automatic re-renders in components using those variables.
89

9-
**Type:** `Record<string, unknown>`
10+
**Type:** `CssxVariableStore`
1011

1112
```jsx
1213
import { variables } from 'cssxjs'
@@ -17,13 +18,24 @@ variables['--primary-color'] = '#007bff'
1718
// Read a variable
1819
console.log(variables['--primary-color'])
1920

20-
// Set multiple variables
21-
Object.assign(variables, {
21+
// Merge multiple variables
22+
variables.assign({
2223
'--primary-color': '#007bff',
2324
'--text-color': '#333'
2425
})
26+
27+
// Replace the whole runtime variable set
28+
variables.set({
29+
'--primary-color': '#007bff'
30+
})
31+
32+
// Clear all runtime variables
33+
variables.clear()
2534
```
2635

36+
Only valid CSS custom property names can be assigned. Names must start with
37+
`--`; invalid names throw.
38+
2739
**Reactivity:**
2840
When you assign to `variables`, components that used those specific variables in
2941
their resolved styles automatically re-render with the new values.
@@ -82,32 +94,57 @@ setDefaultVariables({
8294

8395
## defaultVariables
8496

85-
A reactive object containing the default variable values set by
86-
`setDefaultVariables`.
97+
A reactive object containing default variable values. It supports the same
98+
`.assign()`, `.set()`, and `.clear()` methods as `variables`.
8799

88-
**Type:** `Record<string, string>`
100+
**Type:** `CssxVariableStore`
89101

90102
```jsx
91103
import { defaultVariables } from 'cssxjs'
92104
93105
console.log(defaultVariables['--primary-color']) // '#007bff'
94106
```
95107

108+
`setDefaultVariables(vars)` is an alias for `defaultVariables.set(vars)`.
109+
110+
## Reading Variables In Components
111+
112+
Use `useCssVariable()` when JavaScript needs the resolved value:
113+
114+
```jsx
115+
import { useCssVariable } from 'cssxjs'
116+
117+
function Box() {
118+
const gap = useCssVariable('--gap', '2u') // 16
119+
return <View style={{ gap }} />
120+
}
121+
```
122+
123+
`useCssVariable()` subscribes only to the variables it resolves, including nested
124+
`var()` references. It returns RN-friendly values: `2u` and `16px` become
125+
numbers, percentages remain strings, and modern color functions are normalized.
126+
127+
Use `useCssVariableRaw()` to read raw resolved CSS text. Outside React, use
128+
`getCssVariable()` and `getCssVariableRaw()` for global variables only.
129+
96130
## Variable Resolution Order
97131

98132
CSS variables resolve in this priority (highest first):
99133

100-
1. **Runtime:** `variables['--name']`
101-
2. **Default:** `setDefaultVariables({ '--name': value })`
102-
3. **Inline fallback:** `var(--name, fallback)`
134+
1. **Template interpolation values**
135+
2. **Runtime:** `variables['--name']`
136+
3. **Nearest provider `:root` variable**
137+
4. **Outer provider `:root` variables**
138+
5. **Default:** `defaultVariables['--name']`
139+
6. **Inline fallback:** `var(--name, fallback)`
103140

104141
```jsx
105-
setDefaultVariables({ '--color': 'blue' }) // Priority 2
106-
variables['--color'] = 'red' // Priority 1 (wins)
142+
setDefaultVariables({ '--color': 'blue' })
143+
variables['--color'] = 'red' // wins over provider and defaults
107144
108145
styl`
109146
.box
110-
color var(--color, green) // Will be 'red'
147+
color var(--color, green) // Will be 'red'
111148
`
112149
```
113150

0 commit comments

Comments
 (0)