@pyreon/styler 0.21.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.
Files changed (2) hide show
  1. package/README.md +97 -120
  2. 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 for Pyreon.
3
+ Lightweight CSS-in-JS engine `styled` / `css` / `keyframes` / theme, ~3.8KB gzipped.
4
4
 
5
- **3.81 KB** gzipped | **SSR & static export ready** | **TypeScript strict**
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
- ## Installation
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 Start
13
+ ## Quick start
14
14
 
15
- ```ts
16
- import { styled, css, ThemeContext } from '@pyreon/styler'
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 another component.
45
+ Creates a styled Pyreon component from an HTML tag, another component, or a styled component.
39
46
 
40
47
  ```ts
41
- // HTML tag
42
- const Box = styled('div')`
43
- display: flex;
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
- Render as a different element at runtime:
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
- ```ts
87
- const Box = styled('div')`
88
- color: ${(p) => (p.$active ? 'blue' : 'gray')};
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
- ```ts
103
+ // Conditional fragments
125
104
  const Box = styled('div')`
126
105
  display: flex;
127
- ${(props) =>
128
- props.$bordered &&
129
- css`
130
- border: 1px solid #e0e0e0;
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
- Injects global CSS rules (not scoped to a class):
128
+ Global, non-scoped rules:
154
129
 
155
130
  ```ts
156
131
  const GlobalStyle = createGlobalStyle`
157
- *, *::before, *::after {
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
- ### `ThemeContext` & `useTheme`
136
+ ### `ThemeProvider` / `useTheme` / `useThemeAccessor`
169
137
 
170
- Provides a theme object to all nested styled components via Pyreon's context system:
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
- ```ts
173
- import { ThemeContext, useTheme } from '@pyreon/styler'
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
- Access the theme from any component:
143
+ <ThemeProvider theme={{ colors: { primary: '#0d6efd' } }}>
144
+ <App />
145
+ </ThemeProvider>
182
146
 
183
- ```ts
184
- const MyComponent = () => {
185
- const theme = useTheme()
186
- // use theme values
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` & `createSheet`
164
+ ### `sheet` / `createSheet`
204
165
 
205
- The singleton `sheet` manages CSS rule injection. For SSR, use `createSheet` for per-request isolation:
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
- const sheet = createSheet()
211
- const html = renderToString(App({}))
212
- const styleTags = sheet.getStyleTag()
213
- sheet.reset()
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
- ## How It Works
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 (zero runtime cost)
203
+ ### Static path zero runtime cost
227
204
 
228
- Templates with no function interpolations are resolved **once at component creation time**. The CSS class, rules, and `<style>` element are pre-computed and cached.
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 skips `sheet.prepare()` and `<style>` element creation when the resolved CSS text hasn't changed.
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
- ### CSS Nesting
211
+ ### Reactive theme swaps
235
212
 
236
- Native CSS nesting is supported out of the box. The engine passes CSS through without transformation, so `&:hover`, `&::before`, nested selectors, and `@media` queries work as-is in all modern browsers.
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
- &:hover {
243
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
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 Size
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
- ## Peer Dependencies
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
- | Package | Version |
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.21.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.8",
47
- "@pyreon/typescript": "^0.21.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.3.0"
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.21.0",
56
- "@pyreon/reactivity": "^0.21.0"
55
+ "@pyreon/core": "^0.23.0",
56
+ "@pyreon/reactivity": "^0.23.0"
57
57
  }
58
58
  }