@pyreon/styler 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -120
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
# @pyreon/styler
|
|
2
2
|
|
|
3
|
-
Lightweight CSS-in-JS engine
|
|
3
|
+
Lightweight CSS-in-JS engine — `styled` / `css` / `keyframes` / theme, ~3.8KB gzipped.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`@pyreon/styler` is the CSS-in-JS layer that powers `@pyreon/rocketstyle`, `@pyreon/elements`, and every other rocketstyle-derived component. Singleton `StyleSheet` with FNV-1a class hashing and dedup cache. **Static templates resolve once at module load** (zero per-render cost); dynamic interpolations re-resolve on theme/prop change with class-cache dedup. `ThemeContext` is a **reactive** Pyreon context — whole-theme swaps (user-preference theme switching) propagate through the resolver effect in `styled()` and re-resolve CSS + swap class names WITHOUT remounting the VNode. SSR-isolated via `createSheet()`. CSS Nesting passes through to the browser unchanged.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Install
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
bun add @pyreon/styler
|
|
10
|
+
bun add @pyreon/styler @pyreon/core @pyreon/reactivity
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
## Quick
|
|
13
|
+
## Quick start
|
|
14
14
|
|
|
15
|
-
```
|
|
16
|
-
import { styled, css,
|
|
17
|
-
import { useContext, pushContext, popContext, onUnmount } from '@pyreon/core'
|
|
15
|
+
```tsx
|
|
16
|
+
import { styled, css, keyframes, createGlobalStyle, ThemeProvider } from '@pyreon/styler'
|
|
18
17
|
|
|
19
18
|
const Button = styled('button')`
|
|
20
19
|
display: inline-flex;
|
|
@@ -25,34 +24,30 @@ const Button = styled('button')`
|
|
|
25
24
|
color: white;
|
|
26
25
|
cursor: pointer;
|
|
27
26
|
|
|
28
|
-
&:hover {
|
|
29
|
-
opacity: 0.9;
|
|
30
|
-
}
|
|
27
|
+
&:hover { opacity: 0.9; }
|
|
31
28
|
`
|
|
29
|
+
|
|
30
|
+
const GlobalStyle = createGlobalStyle`
|
|
31
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
32
|
+
body { margin: 0; font-family: ${({ theme }) => theme.font}; }
|
|
33
|
+
`
|
|
34
|
+
|
|
35
|
+
<ThemeProvider theme={{ colors: { primary: '#0d6efd' }, font: 'Inter, sans-serif' }}>
|
|
36
|
+
<GlobalStyle />
|
|
37
|
+
<Button>Click me</Button>
|
|
38
|
+
</ThemeProvider>
|
|
32
39
|
```
|
|
33
40
|
|
|
34
41
|
## API
|
|
35
42
|
|
|
36
|
-
### `styled(tag)`
|
|
43
|
+
### `styled(tag, options?)`
|
|
37
44
|
|
|
38
|
-
Creates a styled Pyreon component from an HTML tag or
|
|
45
|
+
Creates a styled Pyreon component from an HTML tag, another component, or a styled component.
|
|
39
46
|
|
|
40
47
|
```ts
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
`
|
|
45
|
-
|
|
46
|
-
// Shorthand (via Proxy)
|
|
47
|
-
const Box = styled.div`
|
|
48
|
-
display: flex;
|
|
49
|
-
`
|
|
50
|
-
|
|
51
|
-
// Wrapping a component
|
|
52
|
-
const StyledLink = styled(Link)`
|
|
53
|
-
color: blue;
|
|
54
|
-
text-decoration: none;
|
|
55
|
-
`
|
|
48
|
+
const Box = styled('div')`display: flex;`
|
|
49
|
+
const StyledLink = styled(Link)`color: blue;`
|
|
50
|
+
const Wider = styled(Box)`padding: 24px;` // wrap an existing styled
|
|
56
51
|
```
|
|
57
52
|
|
|
58
53
|
#### Dynamic interpolations
|
|
@@ -68,28 +63,15 @@ const Text = styled('p')`
|
|
|
68
63
|
|
|
69
64
|
#### Polymorphic `as` prop
|
|
70
65
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
```ts
|
|
74
|
-
const Box = styled('div')`
|
|
75
|
-
padding: 16px;
|
|
76
|
-
`
|
|
77
|
-
|
|
78
|
-
// Renders as a <section>
|
|
79
|
-
Box({ as: 'section', children: 'Content' })
|
|
66
|
+
```tsx
|
|
67
|
+
<Box as="section">Renders as a section</Box>
|
|
80
68
|
```
|
|
81
69
|
|
|
82
|
-
#### Transient props
|
|
83
|
-
|
|
84
|
-
Props prefixed with `$` are not forwarded to the DOM:
|
|
70
|
+
#### Transient props (`$`-prefixed)
|
|
85
71
|
|
|
86
|
-
```
|
|
87
|
-
const Box = styled('div')`
|
|
88
|
-
|
|
89
|
-
`
|
|
90
|
-
|
|
91
|
-
// $active is used for styling but won't appear on the <div>
|
|
92
|
-
Box({ $active: true })
|
|
72
|
+
```tsx
|
|
73
|
+
const Box = styled('div')`color: ${(p) => (p.$active ? 'blue' : 'gray')};`
|
|
74
|
+
<Box $active>$active is used for styling but does NOT reach the DOM.</Box>
|
|
93
75
|
```
|
|
94
76
|
|
|
95
77
|
#### Custom prop filtering
|
|
@@ -104,7 +86,7 @@ const Box = styled('div', {
|
|
|
104
86
|
|
|
105
87
|
### `css`
|
|
106
88
|
|
|
107
|
-
Tagged template for composable CSS fragments
|
|
89
|
+
Tagged template for composable CSS fragments — lazy `CSSResult`, resolved on use.
|
|
108
90
|
|
|
109
91
|
```ts
|
|
110
92
|
const flexCenter = css`
|
|
@@ -117,26 +99,19 @@ const Card = styled('div')`
|
|
|
117
99
|
${flexCenter};
|
|
118
100
|
padding: 16px;
|
|
119
101
|
`
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
Supports conditional patterns:
|
|
123
102
|
|
|
124
|
-
|
|
103
|
+
// Conditional fragments
|
|
125
104
|
const Box = styled('div')`
|
|
126
105
|
display: flex;
|
|
127
|
-
${(props) =>
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
border-radius: 4px;
|
|
132
|
-
`};
|
|
106
|
+
${(props) => props.$bordered && css`
|
|
107
|
+
border: 1px solid #e0e0e0;
|
|
108
|
+
border-radius: 4px;
|
|
109
|
+
`};
|
|
133
110
|
`
|
|
134
111
|
```
|
|
135
112
|
|
|
136
113
|
### `keyframes`
|
|
137
114
|
|
|
138
|
-
Creates `@keyframes` animations:
|
|
139
|
-
|
|
140
115
|
```ts
|
|
141
116
|
const fadeIn = keyframes`
|
|
142
117
|
from { opacity: 0; }
|
|
@@ -150,47 +125,33 @@ const FadeBox = styled('div')`
|
|
|
150
125
|
|
|
151
126
|
### `createGlobalStyle`
|
|
152
127
|
|
|
153
|
-
|
|
128
|
+
Global, non-scoped rules:
|
|
154
129
|
|
|
155
130
|
```ts
|
|
156
131
|
const GlobalStyle = createGlobalStyle`
|
|
157
|
-
|
|
158
|
-
box-sizing: border-box;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
body {
|
|
162
|
-
margin: 0;
|
|
163
|
-
font-family: ${({ theme }) => theme.font};
|
|
164
|
-
}
|
|
132
|
+
body { margin: 0; }
|
|
165
133
|
`
|
|
166
134
|
```
|
|
167
135
|
|
|
168
|
-
### `
|
|
136
|
+
### `ThemeProvider` / `useTheme` / `useThemeAccessor`
|
|
169
137
|
|
|
170
|
-
|
|
138
|
+
`ThemeContext` is a Pyreon **reactive** context — whole-theme swaps (e.g. user preference dark→light) re-resolve every styled-component's CSS and swap classes in place, no VNode remount.
|
|
171
139
|
|
|
172
|
-
```
|
|
173
|
-
import {
|
|
174
|
-
import { pushContext, onUnmount, popContext } from '@pyreon/core'
|
|
175
|
-
|
|
176
|
-
// Provide theme
|
|
177
|
-
pushContext(new Map([[ThemeContext.id, myTheme]]))
|
|
178
|
-
onUnmount(() => popContext())
|
|
179
|
-
```
|
|
140
|
+
```tsx
|
|
141
|
+
import { ThemeProvider, useTheme, useThemeAccessor } from '@pyreon/styler'
|
|
180
142
|
|
|
181
|
-
|
|
143
|
+
<ThemeProvider theme={{ colors: { primary: '#0d6efd' } }}>
|
|
144
|
+
<App />
|
|
145
|
+
</ThemeProvider>
|
|
182
146
|
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
147
|
+
// Inside a component:
|
|
148
|
+
const theme = useTheme() // snapshot at call time
|
|
149
|
+
const themeFn = useThemeAccessor() // () => Theme — track inside effects/computeds
|
|
150
|
+
effect(() => console.log(themeFn().colors))
|
|
188
151
|
```
|
|
189
152
|
|
|
190
153
|
#### TypeScript theme augmentation
|
|
191
154
|
|
|
192
|
-
Extend `DefaultTheme` for strict typing across your app:
|
|
193
|
-
|
|
194
155
|
```ts
|
|
195
156
|
declare module '@pyreon/styler' {
|
|
196
157
|
interface DefaultTheme {
|
|
@@ -200,62 +161,73 @@ declare module '@pyreon/styler' {
|
|
|
200
161
|
}
|
|
201
162
|
```
|
|
202
163
|
|
|
203
|
-
### `sheet`
|
|
164
|
+
### `sheet` / `createSheet`
|
|
204
165
|
|
|
205
|
-
The singleton `sheet` manages CSS
|
|
166
|
+
The singleton `sheet` manages CSS-rule injection. Use `createSheet()` for per-request SSR isolation:
|
|
206
167
|
|
|
207
168
|
```ts
|
|
208
|
-
import { createSheet } from '@pyreon/styler'
|
|
169
|
+
import { sheet, createSheet } from '@pyreon/styler'
|
|
209
170
|
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
const
|
|
213
|
-
|
|
171
|
+
// SSR
|
|
172
|
+
const requestSheet = createSheet()
|
|
173
|
+
const html = renderToString(<App />)
|
|
174
|
+
const styleTags = requestSheet.getStyleTag()
|
|
175
|
+
requestSheet.reset()
|
|
214
176
|
```
|
|
215
177
|
|
|
216
178
|
#### `@layer` support
|
|
217
179
|
|
|
218
|
-
Wrap all scoped rules in a CSS Cascade Layer:
|
|
219
|
-
|
|
220
180
|
```ts
|
|
221
181
|
const sheet = createSheet({ layer: 'components' })
|
|
182
|
+
// All scoped rules emitted inside @layer components { ... }
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### `useCSS(cssResult)`
|
|
186
|
+
|
|
187
|
+
Read-only hook for retrieving the resolved class name of a `CSSResult` — useful for hand-managed JSX paths that need the class without `styled()`.
|
|
188
|
+
|
|
189
|
+
### Low-level
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
import {
|
|
193
|
+
resolve, resolveValue, normalizeCSS, clearNormCache,
|
|
194
|
+
hash, hashUpdate, hashFinalize, HASH_INIT,
|
|
195
|
+
buildProps, filterProps, isDynamic,
|
|
196
|
+
} from '@pyreon/styler'
|
|
222
197
|
```
|
|
223
198
|
|
|
224
|
-
|
|
199
|
+
`buildProps` / `filterProps` are the prop-forwarding helpers `styled()` uses internally — exported for HOC authors who need to recreate the same filter contract.
|
|
200
|
+
|
|
201
|
+
## How it works
|
|
225
202
|
|
|
226
|
-
### Static path
|
|
203
|
+
### Static path — zero runtime cost
|
|
227
204
|
|
|
228
|
-
Templates with no function interpolations
|
|
205
|
+
Templates with no function interpolations resolve **once at module evaluation**. The CSS class, rules, and `<style>` element are pre-computed and cached.
|
|
229
206
|
|
|
230
207
|
### Dynamic path
|
|
231
208
|
|
|
232
|
-
Templates with function interpolations resolve on every render. A cache
|
|
209
|
+
Templates with function interpolations resolve on every render. A class-cache keyed by `($rocketstyle, $rocketstate)` (rocketstyle path) or by `$element` bundle identity (Element path) skips the resolver pipeline entirely on cache hits. Companion `injectRules(rules, key)` is the idempotent entry point the compile-time-collapse path uses to ship pre-resolved CSS without re-hashing.
|
|
233
210
|
|
|
234
|
-
###
|
|
211
|
+
### Reactive theme swaps
|
|
235
212
|
|
|
236
|
-
|
|
213
|
+
`ThemeProvider` wires the theme through `createReactiveContext` — `styled()` reads via the accessor inside a `renderEffect`, so flipping the provider's theme re-resolves CSS and patches `className` on the same node. No remount.
|
|
214
|
+
|
|
215
|
+
### CSS nesting passes through
|
|
216
|
+
|
|
217
|
+
Native CSS nesting is forwarded unchanged — `&:hover`, `&::before`, nested selectors, and `@media` queries work as-is in modern browsers.
|
|
237
218
|
|
|
238
219
|
```ts
|
|
239
220
|
const Card = styled('div')`
|
|
240
221
|
padding: 16px;
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
& > h2 {
|
|
247
|
-
margin: 0 0 8px;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
@media (min-width: 768px) {
|
|
251
|
-
padding: 24px;
|
|
252
|
-
}
|
|
222
|
+
&:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
|
223
|
+
& > h2 { margin: 0 0 8px; }
|
|
224
|
+
@media (min-width: 768px) { padding: 24px; }
|
|
253
225
|
`
|
|
254
226
|
```
|
|
255
227
|
|
|
256
228
|
## Benchmarks
|
|
257
229
|
|
|
258
|
-
### Bundle
|
|
230
|
+
### Bundle size
|
|
259
231
|
|
|
260
232
|
| Library | Minified | Gzipped |
|
|
261
233
|
| ----------------------- | -----------: | ----------: |
|
|
@@ -275,12 +247,17 @@ const Card = styled('div')`
|
|
|
275
247
|
| SSR renderToString | **307K** | 69K | 192K | 18K |
|
|
276
248
|
| styled() factory | **17.3M** | 109K | 933K | 18.2M |
|
|
277
249
|
|
|
278
|
-
##
|
|
250
|
+
## Gotchas
|
|
251
|
+
|
|
252
|
+
- **Theme swaps re-resolve CSS but do NOT remount.** A whole-theme swap (`<ThemeProvider theme={B}>` → `<ThemeProvider theme={C}>`) updates the className in place. Identity preservation: pass a STABLE theme object via signal/computed if you want maximum cache reuse.
|
|
253
|
+
- **`useTheme()` returns a snapshot.** Inside effects/computeds, use `useThemeAccessor()` to subscribe to live updates.
|
|
254
|
+
- **`ThemeProvider` requires `nativeCompat` if used in a compat-layer app** — it's already marked. User code in compat-mode apps inheriting from `ThemeProvider` should preserve that contract.
|
|
255
|
+
- **`@layer` is opt-in**, not the default. The singleton `sheet` does not wrap in `@layer`.
|
|
256
|
+
- **Failed `insertRule` in production used to be silently swallowed.** Current code uses bare `process.env.NODE_ENV !== 'production'` — bundler-agnostic; do not regress to `import.meta.env.DEV` or `typeof process` guards.
|
|
257
|
+
|
|
258
|
+
## Documentation
|
|
279
259
|
|
|
280
|
-
|
|
281
|
-
| ------------------ | -------- |
|
|
282
|
-
| @pyreon/core | >= 0.0.1 |
|
|
283
|
-
| @pyreon/reactivity | >= 0.0.1 |
|
|
260
|
+
Full docs: [docs.pyreon.dev/docs/styler](https://docs.pyreon.dev/docs/styler) (or `docs/docs/styler.md` in this repo).
|
|
284
261
|
|
|
285
262
|
## License
|
|
286
263
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/styler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Lightweight CSS-in-JS engine for Pyreon",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -43,16 +43,16 @@
|
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@pyreon/manifest": "0.13.1",
|
|
46
|
-
"@pyreon/test-utils": "^0.13.
|
|
47
|
-
"@pyreon/typescript": "^0.
|
|
46
|
+
"@pyreon/test-utils": "^0.13.10",
|
|
47
|
+
"@pyreon/typescript": "^0.23.0",
|
|
48
48
|
"@vitest/browser-playwright": "^4.1.4",
|
|
49
|
-
"@vitus-labs/tools-rolldown": "^2.
|
|
49
|
+
"@vitus-labs/tools-rolldown": "^2.4.0"
|
|
50
50
|
},
|
|
51
51
|
"engines": {
|
|
52
52
|
"node": ">= 22"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@pyreon/core": "^0.
|
|
56
|
-
"@pyreon/reactivity": "^0.
|
|
55
|
+
"@pyreon/core": "^0.23.0",
|
|
56
|
+
"@pyreon/reactivity": "^0.23.0"
|
|
57
57
|
}
|
|
58
58
|
}
|