@vitus-labs/styler 2.0.0-alpha.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/LICENSE +21 -0
- package/README.md +324 -0
- package/lib/index.d.ts +160 -0
- package/lib/index.js +691 -0
- package/package.json +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023-present Vit Bokisch
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
# @vitus-labs/styler
|
|
2
|
+
|
|
3
|
+
A lightweight CSS-in-JS engine for React. Drop-in replacement for `styled-components` at a fraction of the size.
|
|
4
|
+
|
|
5
|
+
**3.06 KB** gzipped | **React 18+** | **SSR ready** | **TypeScript strict**
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @vitus-labs/styler
|
|
11
|
+
# or
|
|
12
|
+
bun add @vitus-labs/styler
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
React 18+ is required as a peer dependency.
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import { styled, css, ThemeProvider } from '@vitus-labs/styler'
|
|
21
|
+
|
|
22
|
+
const Button = styled('button')`
|
|
23
|
+
display: inline-flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
padding: 8px 16px;
|
|
26
|
+
border-radius: 4px;
|
|
27
|
+
background: ${({ theme }) => theme.colors.primary};
|
|
28
|
+
color: white;
|
|
29
|
+
cursor: pointer;
|
|
30
|
+
|
|
31
|
+
&:hover {
|
|
32
|
+
opacity: 0.9;
|
|
33
|
+
}
|
|
34
|
+
`
|
|
35
|
+
|
|
36
|
+
function App() {
|
|
37
|
+
return (
|
|
38
|
+
<ThemeProvider theme={{ colors: { primary: '#3b82f6' } }}>
|
|
39
|
+
<Button>Click me</Button>
|
|
40
|
+
</ThemeProvider>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## API
|
|
46
|
+
|
|
47
|
+
### `styled(tag)`
|
|
48
|
+
|
|
49
|
+
Creates a styled React component from an HTML tag or another component.
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
// HTML tag
|
|
53
|
+
const Box = styled('div')`
|
|
54
|
+
display: flex;
|
|
55
|
+
`
|
|
56
|
+
|
|
57
|
+
// Shorthand (via Proxy)
|
|
58
|
+
const Box = styled.div`
|
|
59
|
+
display: flex;
|
|
60
|
+
`
|
|
61
|
+
|
|
62
|
+
// Wrapping a component
|
|
63
|
+
const StyledLink = styled(Link)`
|
|
64
|
+
color: blue;
|
|
65
|
+
text-decoration: none;
|
|
66
|
+
`
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
#### Dynamic interpolations
|
|
70
|
+
|
|
71
|
+
Function interpolations receive all props plus the current `theme`:
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
const Text = styled('p')`
|
|
75
|
+
color: ${({ theme }) => theme.colors.text};
|
|
76
|
+
font-size: ${(props) => props.$size || '16px'};
|
|
77
|
+
`
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
#### Polymorphic `as` prop
|
|
81
|
+
|
|
82
|
+
Render as a different element at runtime:
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
const Box = styled('div')`padding: 16px;`
|
|
86
|
+
|
|
87
|
+
<Box as="section">Renders as a <section></Box>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
#### Ref forwarding
|
|
91
|
+
|
|
92
|
+
All styled components forward refs via `React.forwardRef`:
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
const Input = styled('input')`border: 1px solid #ccc;`
|
|
96
|
+
|
|
97
|
+
const ref = useRef<HTMLInputElement>(null)
|
|
98
|
+
<Input ref={ref} />
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
#### Transient props
|
|
102
|
+
|
|
103
|
+
Props prefixed with `$` are not forwarded to the DOM:
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
const Box = styled('div')`
|
|
107
|
+
color: ${(p) => p.$active ? 'blue' : 'gray'};
|
|
108
|
+
`
|
|
109
|
+
|
|
110
|
+
// $active is used for styling but won't appear on the <div>
|
|
111
|
+
<Box $active />
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### Custom prop filtering
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
const Box = styled('div', {
|
|
118
|
+
shouldForwardProp: (prop) => prop !== 'size',
|
|
119
|
+
})`
|
|
120
|
+
font-size: ${(p) => p.size}px;
|
|
121
|
+
`
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### `css`
|
|
125
|
+
|
|
126
|
+
Tagged template for composable CSS fragments:
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
const flexCenter = css`
|
|
130
|
+
display: flex;
|
|
131
|
+
align-items: center;
|
|
132
|
+
justify-content: center;
|
|
133
|
+
`
|
|
134
|
+
|
|
135
|
+
const Card = styled('div')`
|
|
136
|
+
${flexCenter};
|
|
137
|
+
padding: 16px;
|
|
138
|
+
`
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Supports conditional patterns:
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
const Box = styled('div')`
|
|
145
|
+
display: flex;
|
|
146
|
+
${(props) => props.$bordered && css`
|
|
147
|
+
border: 1px solid #e0e0e0;
|
|
148
|
+
border-radius: 4px;
|
|
149
|
+
`};
|
|
150
|
+
`
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### `keyframes`
|
|
154
|
+
|
|
155
|
+
Creates `@keyframes` animations:
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
const fadeIn = keyframes`
|
|
159
|
+
from { opacity: 0; }
|
|
160
|
+
to { opacity: 1; }
|
|
161
|
+
`
|
|
162
|
+
|
|
163
|
+
const FadeBox = styled('div')`
|
|
164
|
+
animation: ${fadeIn} 300ms ease-in;
|
|
165
|
+
`
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### `createGlobalStyle`
|
|
169
|
+
|
|
170
|
+
Injects global CSS rules (not scoped to a class):
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
const GlobalStyle = createGlobalStyle`
|
|
174
|
+
*, *::before, *::after {
|
|
175
|
+
box-sizing: border-box;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
body {
|
|
179
|
+
margin: 0;
|
|
180
|
+
font-family: ${({ theme }) => theme.font};
|
|
181
|
+
}
|
|
182
|
+
`
|
|
183
|
+
|
|
184
|
+
// Renders nothing, injects CSS when mounted
|
|
185
|
+
<GlobalStyle />
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### `ThemeProvider` & `useTheme`
|
|
189
|
+
|
|
190
|
+
Provides a theme object to all nested styled components via React context:
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
const theme = {
|
|
194
|
+
colors: { primary: '#3b82f6', text: '#111' },
|
|
195
|
+
spacing: (n: number) => `${n * 4}px`,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
<ThemeProvider theme={theme}>
|
|
199
|
+
<App />
|
|
200
|
+
</ThemeProvider>
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Access the theme from any component:
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
const MyComponent = () => {
|
|
207
|
+
const theme = useTheme()
|
|
208
|
+
return <div style={{ color: theme.colors.primary }} />
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
#### TypeScript theme augmentation
|
|
213
|
+
|
|
214
|
+
Extend `DefaultTheme` for strict typing across your app:
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
declare module '@vitus-labs/styler' {
|
|
218
|
+
interface DefaultTheme {
|
|
219
|
+
colors: { primary: string; text: string }
|
|
220
|
+
spacing: (n: number) => string
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### `sheet` & `createSheet`
|
|
226
|
+
|
|
227
|
+
The singleton `sheet` manages CSS rule injection. For SSR, use `createSheet` for per-request isolation:
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
import { createSheet } from '@vitus-labs/styler'
|
|
231
|
+
|
|
232
|
+
// Server-side rendering
|
|
233
|
+
const sheet = createSheet()
|
|
234
|
+
const html = renderToString(<App />)
|
|
235
|
+
const styleTags = sheet.getStyleTag() // <style data-vl="">...</style>
|
|
236
|
+
sheet.reset()
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
#### `@layer` support
|
|
240
|
+
|
|
241
|
+
Wrap all scoped rules in a CSS Cascade Layer:
|
|
242
|
+
|
|
243
|
+
```tsx
|
|
244
|
+
import { createSheet } from '@vitus-labs/styler'
|
|
245
|
+
|
|
246
|
+
const sheet = createSheet({ layer: 'components' })
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
#### HMR cleanup
|
|
250
|
+
|
|
251
|
+
```tsx
|
|
252
|
+
if (import.meta.hot) {
|
|
253
|
+
import.meta.hot.accept(() => {
|
|
254
|
+
sheet.clearAll()
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## How It Works
|
|
260
|
+
|
|
261
|
+
### Static path (zero runtime cost)
|
|
262
|
+
|
|
263
|
+
Templates with no function interpolations are resolved **once at component creation time**. The CSS class is computed and injected immediately, and the React component is a thin `forwardRef` wrapper with no hooks.
|
|
264
|
+
|
|
265
|
+
```tsx
|
|
266
|
+
// Class computed once at import time, not on every render
|
|
267
|
+
const Box = styled('div')`
|
|
268
|
+
display: flex;
|
|
269
|
+
padding: 16px;
|
|
270
|
+
`
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Dynamic path
|
|
274
|
+
|
|
275
|
+
Templates with function interpolations resolve on every render. The engine caches the last CSS string and skips re-hashing and re-injection when props haven't changed. Style injection uses `useInsertionEffect` for concurrent-mode safety.
|
|
276
|
+
|
|
277
|
+
### CSS Nesting
|
|
278
|
+
|
|
279
|
+
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.
|
|
280
|
+
|
|
281
|
+
```tsx
|
|
282
|
+
const Card = styled('div')`
|
|
283
|
+
padding: 16px;
|
|
284
|
+
|
|
285
|
+
&:hover {
|
|
286
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
& > h2 {
|
|
290
|
+
margin: 0 0 8px;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
@media (min-width: 768px) {
|
|
294
|
+
padding: 24px;
|
|
295
|
+
}
|
|
296
|
+
`
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Migrating from styled-components
|
|
300
|
+
|
|
301
|
+
The API is intentionally compatible. Most code works by changing the import:
|
|
302
|
+
|
|
303
|
+
```diff
|
|
304
|
+
- import styled, { css, keyframes, createGlobalStyle, ThemeProvider } from 'styled-components'
|
|
305
|
+
+ import { styled, css, keyframes, createGlobalStyle, ThemeProvider } from '@vitus-labs/styler'
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Key differences:
|
|
309
|
+
|
|
310
|
+
| Feature | styled-components | @vitus-labs/styler |
|
|
311
|
+
|---------|------------------|-------------------|
|
|
312
|
+
| Bundle size | ~16 KB gz | **3.06 KB gz** |
|
|
313
|
+
| `styled.div` shorthand | Yes | Yes |
|
|
314
|
+
| `as` prop | Yes | Yes |
|
|
315
|
+
| Ref forwarding | Yes | Yes |
|
|
316
|
+
| Transient `$` props | Yes | Yes |
|
|
317
|
+
| `shouldForwardProp` | `.withConfig()` | Second argument |
|
|
318
|
+
| SSR | `ServerStyleSheet` | `createSheet()` |
|
|
319
|
+
| CSS nesting | Preprocessed | Native (no transform) |
|
|
320
|
+
| `attrs()` | Yes | Use `@vitus-labs/attrs` |
|
|
321
|
+
|
|
322
|
+
## License
|
|
323
|
+
|
|
324
|
+
MIT
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import * as react from "react";
|
|
2
|
+
import { ComponentType, FC, ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/resolve.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Interpolation resolver: converts tagged template strings + values into a
|
|
7
|
+
* final CSS string. Handles nested CSSResults, arrays, functions, and
|
|
8
|
+
* primitive values.
|
|
9
|
+
*/
|
|
10
|
+
type Interpolation = string | number | boolean | null | undefined | CSSResult | Interpolation[] | ((props: any) => Interpolation);
|
|
11
|
+
/**
|
|
12
|
+
* Lazy representation of a `css` tagged template. Stores the raw template
|
|
13
|
+
* strings and interpolation values without resolving them. Resolution is
|
|
14
|
+
* deferred until a styled component renders (or until explicitly resolved).
|
|
15
|
+
*/
|
|
16
|
+
declare class CSSResult {
|
|
17
|
+
readonly strings: TemplateStringsArray;
|
|
18
|
+
readonly values: Interpolation[];
|
|
19
|
+
constructor(strings: TemplateStringsArray, values: Interpolation[]);
|
|
20
|
+
/** Resolve with empty props — useful for static templates, testing, and debugging. */
|
|
21
|
+
toString(): string;
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/css.d.ts
|
|
25
|
+
/**
|
|
26
|
+
* Tagged template function for CSS. Captures the template strings and
|
|
27
|
+
* interpolation values as a lazy CSSResult — resolution is deferred
|
|
28
|
+
* until a styled component renders.
|
|
29
|
+
*
|
|
30
|
+
* Works as both a tagged template (`css\`...\``) and a regular function
|
|
31
|
+
* call (`css(...args)`) since tagged templates are syntactic sugar for
|
|
32
|
+
* function calls with (TemplateStringsArray, ...values).
|
|
33
|
+
*/
|
|
34
|
+
declare const css: (strings: TemplateStringsArray, ...values: Interpolation[]) => CSSResult;
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/globalStyle.d.ts
|
|
37
|
+
declare const createGlobalStyle: (strings: TemplateStringsArray, ...values: Interpolation[]) => {
|
|
38
|
+
(props: Record<string, any>): null;
|
|
39
|
+
displayName: string;
|
|
40
|
+
};
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/keyframes.d.ts
|
|
43
|
+
declare class KeyframesResult {
|
|
44
|
+
readonly name: string;
|
|
45
|
+
constructor(strings: TemplateStringsArray, values: Interpolation[]);
|
|
46
|
+
/** Returns the animation name when used in string context. */
|
|
47
|
+
toString(): string;
|
|
48
|
+
}
|
|
49
|
+
declare const keyframes: (strings: TemplateStringsArray, ...values: Interpolation[]) => KeyframesResult;
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/sheet.d.ts
|
|
52
|
+
interface StyleSheetOptions {
|
|
53
|
+
/** Maximum number of cached rules before eviction (default: 10000). */
|
|
54
|
+
maxCacheSize?: number;
|
|
55
|
+
/** CSS @layer name to wrap scoped rules in. */
|
|
56
|
+
layer?: string;
|
|
57
|
+
}
|
|
58
|
+
declare class StyleSheet {
|
|
59
|
+
private cache;
|
|
60
|
+
private sheet;
|
|
61
|
+
private ssrBuffer;
|
|
62
|
+
private isSSR;
|
|
63
|
+
private maxCacheSize;
|
|
64
|
+
private layer;
|
|
65
|
+
constructor(options?: StyleSheetOptions);
|
|
66
|
+
private mount;
|
|
67
|
+
/** Parse existing rules from SSR-rendered <style> tag into cache. */
|
|
68
|
+
private hydrateFromTag;
|
|
69
|
+
/** Evict oldest entries when cache exceeds max size. */
|
|
70
|
+
private evictIfNeeded;
|
|
71
|
+
/**
|
|
72
|
+
* Compute a className from CSS text without injecting (pure function for render phase).
|
|
73
|
+
* Used with useInsertionEffect pattern: compute class during render, inject in effect.
|
|
74
|
+
*/
|
|
75
|
+
getClassName(cssText: string): string;
|
|
76
|
+
/**
|
|
77
|
+
* Insert a CSS rule. Returns the class name (deterministic, hash-based).
|
|
78
|
+
* Deduplicates: same CSS text always produces the same class name and
|
|
79
|
+
* the rule is only injected once.
|
|
80
|
+
*/
|
|
81
|
+
insert(cssText: string): string;
|
|
82
|
+
/** Insert a @keyframes rule. Deduplicates by animation name. */
|
|
83
|
+
insertKeyframes(name: string, body: string): void;
|
|
84
|
+
/** Insert a global CSS rule (no wrapper selector). Deduplicates by hash. */
|
|
85
|
+
insertGlobal(cssText: string): void;
|
|
86
|
+
/** Returns collected CSS for SSR as a complete `<style>` tag string. */
|
|
87
|
+
getStyleTag(): string;
|
|
88
|
+
/** Returns collected CSS rules as a raw string (useful for streaming SSR). */
|
|
89
|
+
getStyles(): string;
|
|
90
|
+
/** Reset SSR buffer (for concurrent server requests). */
|
|
91
|
+
reset(): void;
|
|
92
|
+
/** Clear the dedup cache. Useful for HMR / dev-time reloads. */
|
|
93
|
+
clearCache(): void;
|
|
94
|
+
/**
|
|
95
|
+
* Full cleanup: clear cache and remove all CSS rules from the DOM.
|
|
96
|
+
* Intended for HMR / dev-time reloads where stale styles must be purged.
|
|
97
|
+
*
|
|
98
|
+
* if (import.meta.hot) {
|
|
99
|
+
* import.meta.hot.accept(() => sheet.clearAll())
|
|
100
|
+
* }
|
|
101
|
+
*/
|
|
102
|
+
clearAll(): void;
|
|
103
|
+
/** Check if a className is already in the cache. O(1) Map lookup. */
|
|
104
|
+
has(className: string): boolean;
|
|
105
|
+
/** Current number of cached rules. */
|
|
106
|
+
get cacheSize(): number;
|
|
107
|
+
}
|
|
108
|
+
/** Default singleton sheet for client-side use. */
|
|
109
|
+
declare const sheet: StyleSheet;
|
|
110
|
+
/**
|
|
111
|
+
* Factory for creating isolated StyleSheet instances.
|
|
112
|
+
* Use in SSR to get per-request isolation:
|
|
113
|
+
*
|
|
114
|
+
* const sheet = createSheet()
|
|
115
|
+
* // render with this sheet...
|
|
116
|
+
* const html = sheet.getStyleTag()
|
|
117
|
+
* sheet.reset()
|
|
118
|
+
*/
|
|
119
|
+
declare const createSheet: (options?: StyleSheetOptions) => StyleSheet;
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/styled.d.ts
|
|
122
|
+
type Tag = string | ComponentType<any>;
|
|
123
|
+
interface StyledOptions {
|
|
124
|
+
/** Custom prop filter. Return true to forward the prop to the DOM element. */
|
|
125
|
+
shouldForwardProp?: (prop: string) => boolean;
|
|
126
|
+
}
|
|
127
|
+
/** Factory function: styled(tag) returns a tagged template function. */
|
|
128
|
+
declare const styledFactory: (tag: Tag, options?: StyledOptions) => (strings: TemplateStringsArray, ...values: Interpolation[]) => react.ForwardRefExoticComponent<Omit<Record<string, any>, "ref"> & react.RefAttributes<unknown>>;
|
|
129
|
+
/**
|
|
130
|
+
* Main styled export. Supports both calling conventions:
|
|
131
|
+
* - `styled('div')` or `styled(Component)` → returns tagged template function
|
|
132
|
+
* - `styled('div', { shouldForwardProp })` → with custom prop filtering
|
|
133
|
+
* - `styled.div` → shorthand via Proxy (no options)
|
|
134
|
+
*/
|
|
135
|
+
declare const styled: typeof styledFactory & Record<string, (strings: TemplateStringsArray, ...values: Interpolation[]) => any>;
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/ThemeProvider.d.ts
|
|
138
|
+
/**
|
|
139
|
+
* Extensible theme interface. Consumers can augment this via module
|
|
140
|
+
* declaration merging for full strict types:
|
|
141
|
+
*
|
|
142
|
+
* declare module '@vitus-labs/styler' {
|
|
143
|
+
* interface DefaultTheme {
|
|
144
|
+
* colors: { primary: string; secondary: string }
|
|
145
|
+
* spacing: (n: number) => string
|
|
146
|
+
* }
|
|
147
|
+
* }
|
|
148
|
+
*/
|
|
149
|
+
type DefaultTheme = {};
|
|
150
|
+
type Theme = DefaultTheme & Record<string, unknown>;
|
|
151
|
+
/** Hook to read the current theme from the nearest ThemeProvider. */
|
|
152
|
+
declare const useTheme: <T extends Theme = Theme>() => T;
|
|
153
|
+
/** Provides a theme object to all nested styled components via React context. */
|
|
154
|
+
declare const ThemeProvider: FC<{
|
|
155
|
+
theme: Theme;
|
|
156
|
+
children: ReactNode;
|
|
157
|
+
}>;
|
|
158
|
+
//#endregion
|
|
159
|
+
export { type CSSResult, type DefaultTheme, type Interpolation, type StyleSheetOptions, type StyledOptions, ThemeProvider, createGlobalStyle, createSheet, css, keyframes, sheet, styled, useTheme };
|
|
160
|
+
//# sourceMappingURL=index2.d.ts.map
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
import { Fragment, createContext, createElement, forwardRef, useContext, useInsertionEffect, useRef } from "react";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
|
|
4
|
+
//#region src/resolve.ts
|
|
5
|
+
/**
|
|
6
|
+
* Lazy representation of a `css` tagged template. Stores the raw template
|
|
7
|
+
* strings and interpolation values without resolving them. Resolution is
|
|
8
|
+
* deferred until a styled component renders (or until explicitly resolved).
|
|
9
|
+
*/
|
|
10
|
+
var CSSResult = class {
|
|
11
|
+
constructor(strings, values) {
|
|
12
|
+
this.strings = strings;
|
|
13
|
+
this.values = values;
|
|
14
|
+
}
|
|
15
|
+
/** Resolve with empty props — useful for static templates, testing, and debugging. */
|
|
16
|
+
toString() {
|
|
17
|
+
return resolve(this.strings, this.values, {});
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
/** Resolve a tagged template's strings + values into a final CSS string. */
|
|
21
|
+
const resolve = (strings, values, props) => {
|
|
22
|
+
let result = strings[0] ?? "";
|
|
23
|
+
for (let i = 0; i < values.length; i++) result += resolveValue(values[i], props) + (strings[i + 1] ?? "");
|
|
24
|
+
return result;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Normalize resolved CSS text for strict `insertRule` compatibility.
|
|
28
|
+
* Removes double semicolons, empty declarations, and excess whitespace
|
|
29
|
+
* that arise from template interpolation (conditional expressions,
|
|
30
|
+
* empty CSSResult values, etc.).
|
|
31
|
+
*/
|
|
32
|
+
const normalizeCSS = (css) => {
|
|
33
|
+
let s = css.replace(/\s+/g, " ").trim();
|
|
34
|
+
while (s.includes("; ;") || s.includes(";;")) s = s.replace(/;(\s*;)+/g, ";");
|
|
35
|
+
s = s.replace(/\{\s*;/g, "{").replace(/}\s*;/g, "}");
|
|
36
|
+
s = s.replace(/^\s*;/, "").trim();
|
|
37
|
+
return s;
|
|
38
|
+
};
|
|
39
|
+
const resolveValue = (value, props) => {
|
|
40
|
+
if (value == null || value === false || value === true) return "";
|
|
41
|
+
if (typeof value === "function") return resolveValue(value(props), props);
|
|
42
|
+
if (value instanceof CSSResult) return resolve(value.strings, value.values, props);
|
|
43
|
+
if (Array.isArray(value)) {
|
|
44
|
+
let arrayResult = "";
|
|
45
|
+
for (let i = 0; i < value.length; i++) arrayResult += resolveValue(value[i], props);
|
|
46
|
+
return arrayResult;
|
|
47
|
+
}
|
|
48
|
+
return String(value);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/css.ts
|
|
53
|
+
/**
|
|
54
|
+
* Tagged template function for CSS. Captures the template strings and
|
|
55
|
+
* interpolation values as a lazy CSSResult — resolution is deferred
|
|
56
|
+
* until a styled component renders.
|
|
57
|
+
*
|
|
58
|
+
* Works as both a tagged template (`css\`...\``) and a regular function
|
|
59
|
+
* call (`css(...args)`) since tagged templates are syntactic sugar for
|
|
60
|
+
* function calls with (TemplateStringsArray, ...values).
|
|
61
|
+
*/
|
|
62
|
+
const css = (strings, ...values) => new CSSResult(strings, values);
|
|
63
|
+
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/shared.ts
|
|
66
|
+
/**
|
|
67
|
+
* Shared utilities used across multiple modules.
|
|
68
|
+
*/
|
|
69
|
+
/** Check if an interpolation value is dynamic (contains functions or nested dynamic CSSResults). */
|
|
70
|
+
const isDynamic = (v) => {
|
|
71
|
+
if (typeof v === "function") return true;
|
|
72
|
+
if (Array.isArray(v)) return v.some(isDynamic);
|
|
73
|
+
if (v instanceof CSSResult) return v.values.some(isDynamic);
|
|
74
|
+
return false;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/hash.ts
|
|
79
|
+
/**
|
|
80
|
+
* Fast FNV-1a non-cryptographic hash. Returns base-36 string for compact class names.
|
|
81
|
+
*
|
|
82
|
+
* 32-bit hash space → ~4.3 billion unique values. Collision probability is
|
|
83
|
+
* negligible for typical applications (< 10,000 unique CSS rules). If a collision
|
|
84
|
+
* did occur, two different CSS strings would share the same class name and only
|
|
85
|
+
* the first would be injected (dedup). In practice this is a non-issue — CSS
|
|
86
|
+
* strings are rarely similar enough to collide under FNV-1a's avalanche properties.
|
|
87
|
+
*/
|
|
88
|
+
/** FNV-1a offset basis — starting state for streaming hash. */
|
|
89
|
+
const HASH_INIT = 2166136261;
|
|
90
|
+
const FNV_PRIME = 16777619;
|
|
91
|
+
/**
|
|
92
|
+
* Feed a string segment into the running hash state.
|
|
93
|
+
* Streaming: hashUpdate(hashUpdate(HASH_INIT, 'ab'), 'cd') === hash('abcd').
|
|
94
|
+
*/
|
|
95
|
+
const hashUpdate = (h, str) => {
|
|
96
|
+
for (let i = 0; i < str.length; i++) {
|
|
97
|
+
h ^= str.charCodeAt(i);
|
|
98
|
+
h = Math.imul(h, FNV_PRIME);
|
|
99
|
+
}
|
|
100
|
+
return h;
|
|
101
|
+
};
|
|
102
|
+
/** Finalize a hash state into a base-36 class name suffix. */
|
|
103
|
+
const hashFinalize = (h) => (h >>> 0).toString(36);
|
|
104
|
+
/** Hash a complete string in one shot. Returns base-36 string. */
|
|
105
|
+
const hash = (str) => hashFinalize(hashUpdate(HASH_INIT, str));
|
|
106
|
+
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region src/sheet.ts
|
|
109
|
+
/**
|
|
110
|
+
* StyleSheet manager. Handles CSS rule injection, hash-based deduplication,
|
|
111
|
+
* SSR buffering, client-side hydration, bounded cache, and @layer support.
|
|
112
|
+
*/
|
|
113
|
+
const PREFIX = "vl";
|
|
114
|
+
const ATTR = `data-${PREFIX}`;
|
|
115
|
+
const DEFAULT_MAX_CACHE_SIZE = 1e4;
|
|
116
|
+
var StyleSheet = class {
|
|
117
|
+
cache = /* @__PURE__ */ new Map();
|
|
118
|
+
sheet = null;
|
|
119
|
+
ssrBuffer = [];
|
|
120
|
+
isSSR;
|
|
121
|
+
maxCacheSize;
|
|
122
|
+
layer;
|
|
123
|
+
constructor(options = {}) {
|
|
124
|
+
this.maxCacheSize = options.maxCacheSize ?? DEFAULT_MAX_CACHE_SIZE;
|
|
125
|
+
this.layer = options.layer;
|
|
126
|
+
this.isSSR = typeof document === "undefined";
|
|
127
|
+
if (!this.isSSR) this.mount();
|
|
128
|
+
}
|
|
129
|
+
mount() {
|
|
130
|
+
const existing = document.querySelector(`style[${ATTR}]`);
|
|
131
|
+
if (existing) {
|
|
132
|
+
this.sheet = existing.sheet ?? null;
|
|
133
|
+
this.hydrateFromTag(existing);
|
|
134
|
+
} else {
|
|
135
|
+
const el = document.createElement("style");
|
|
136
|
+
el.setAttribute(ATTR, "");
|
|
137
|
+
document.head.appendChild(el);
|
|
138
|
+
this.sheet = el.sheet ?? null;
|
|
139
|
+
}
|
|
140
|
+
if (this.layer && this.sheet) try {
|
|
141
|
+
this.sheet.insertRule(`@layer ${this.layer};`, 0);
|
|
142
|
+
} catch {}
|
|
143
|
+
}
|
|
144
|
+
/** Parse existing rules from SSR-rendered <style> tag into cache. */
|
|
145
|
+
hydrateFromTag(el) {
|
|
146
|
+
const sheet = el.sheet;
|
|
147
|
+
if (!sheet) return;
|
|
148
|
+
for (let i = 0; i < sheet.cssRules.length; i++) {
|
|
149
|
+
const rule = sheet.cssRules[i];
|
|
150
|
+
if (rule instanceof CSSStyleRule) {
|
|
151
|
+
const className = rule.selectorText.slice(1);
|
|
152
|
+
this.cache.set(className, className);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/** Evict oldest entries when cache exceeds max size. */
|
|
157
|
+
evictIfNeeded() {
|
|
158
|
+
if (this.cache.size <= this.maxCacheSize) return;
|
|
159
|
+
const toDelete = Math.floor(this.maxCacheSize * .1);
|
|
160
|
+
let count = 0;
|
|
161
|
+
for (const key of this.cache.keys()) {
|
|
162
|
+
if (count >= toDelete) break;
|
|
163
|
+
this.cache.delete(key);
|
|
164
|
+
count++;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Compute a className from CSS text without injecting (pure function for render phase).
|
|
169
|
+
* Used with useInsertionEffect pattern: compute class during render, inject in effect.
|
|
170
|
+
*/
|
|
171
|
+
getClassName(cssText) {
|
|
172
|
+
return `${PREFIX}-${hash(cssText)}`;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Insert a CSS rule. Returns the class name (deterministic, hash-based).
|
|
176
|
+
* Deduplicates: same CSS text always produces the same class name and
|
|
177
|
+
* the rule is only injected once.
|
|
178
|
+
*/
|
|
179
|
+
insert(cssText) {
|
|
180
|
+
const className = `${PREFIX}-${hash(cssText)}`;
|
|
181
|
+
if (this.cache.has(className)) return className;
|
|
182
|
+
this.evictIfNeeded();
|
|
183
|
+
this.cache.set(className, className);
|
|
184
|
+
const baseRule = `.${className}{${cssText}}`;
|
|
185
|
+
const rule = this.layer ? `@layer ${this.layer}{${baseRule}}` : baseRule;
|
|
186
|
+
if (this.isSSR) this.ssrBuffer.push(rule);
|
|
187
|
+
else if (this.sheet) try {
|
|
188
|
+
this.sheet.insertRule(rule, this.sheet.cssRules.length);
|
|
189
|
+
} catch (e) {
|
|
190
|
+
if (process.env.NODE_ENV !== "production") console.warn(`[styler] Failed to insert CSS rule for .${className}:`, e.message, "\nCSS:", cssText.slice(0, 200));
|
|
191
|
+
}
|
|
192
|
+
return className;
|
|
193
|
+
}
|
|
194
|
+
/** Insert a @keyframes rule. Deduplicates by animation name. */
|
|
195
|
+
insertKeyframes(name, body) {
|
|
196
|
+
if (this.cache.has(name)) return;
|
|
197
|
+
this.evictIfNeeded();
|
|
198
|
+
this.cache.set(name, name);
|
|
199
|
+
const rule = `@keyframes ${name}{${body}}`;
|
|
200
|
+
if (this.isSSR) this.ssrBuffer.push(rule);
|
|
201
|
+
else if (this.sheet) try {
|
|
202
|
+
this.sheet.insertRule(rule, this.sheet.cssRules.length);
|
|
203
|
+
} catch (e) {
|
|
204
|
+
if (process.env.NODE_ENV !== "production") console.warn(`[styler] Failed to insert @keyframes "${name}":`, e.message);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/** Insert a global CSS rule (no wrapper selector). Deduplicates by hash. */
|
|
208
|
+
insertGlobal(cssText) {
|
|
209
|
+
const key = `global-${hash(cssText)}`;
|
|
210
|
+
if (this.cache.has(key)) return;
|
|
211
|
+
this.evictIfNeeded();
|
|
212
|
+
this.cache.set(key, key);
|
|
213
|
+
if (this.isSSR) this.ssrBuffer.push(cssText);
|
|
214
|
+
else if (this.sheet) try {
|
|
215
|
+
this.sheet.insertRule(cssText, this.sheet.cssRules.length);
|
|
216
|
+
} catch (e) {
|
|
217
|
+
if (process.env.NODE_ENV !== "production") console.warn("[styler] Failed to insert global CSS rule:", e.message, "\nCSS:", cssText.slice(0, 200));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/** Returns collected CSS for SSR as a complete `<style>` tag string. */
|
|
221
|
+
getStyleTag() {
|
|
222
|
+
return `<style ${ATTR}="">${this.ssrBuffer.join("")}</style>`;
|
|
223
|
+
}
|
|
224
|
+
/** Returns collected CSS rules as a raw string (useful for streaming SSR). */
|
|
225
|
+
getStyles() {
|
|
226
|
+
return this.ssrBuffer.join("");
|
|
227
|
+
}
|
|
228
|
+
/** Reset SSR buffer (for concurrent server requests). */
|
|
229
|
+
reset() {
|
|
230
|
+
this.ssrBuffer = [];
|
|
231
|
+
}
|
|
232
|
+
/** Clear the dedup cache. Useful for HMR / dev-time reloads. */
|
|
233
|
+
clearCache() {
|
|
234
|
+
this.cache.clear();
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Full cleanup: clear cache and remove all CSS rules from the DOM.
|
|
238
|
+
* Intended for HMR / dev-time reloads where stale styles must be purged.
|
|
239
|
+
*
|
|
240
|
+
* if (import.meta.hot) {
|
|
241
|
+
* import.meta.hot.accept(() => sheet.clearAll())
|
|
242
|
+
* }
|
|
243
|
+
*/
|
|
244
|
+
clearAll() {
|
|
245
|
+
this.cache.clear();
|
|
246
|
+
this.ssrBuffer = [];
|
|
247
|
+
if (this.sheet) while (this.sheet.cssRules.length > 0) this.sheet.deleteRule(0);
|
|
248
|
+
}
|
|
249
|
+
/** Check if a className is already in the cache. O(1) Map lookup. */
|
|
250
|
+
has(className) {
|
|
251
|
+
return this.cache.has(className);
|
|
252
|
+
}
|
|
253
|
+
/** Current number of cached rules. */
|
|
254
|
+
get cacheSize() {
|
|
255
|
+
return this.cache.size;
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
/** Default singleton sheet for client-side use. */
|
|
259
|
+
const sheet = new StyleSheet();
|
|
260
|
+
/**
|
|
261
|
+
* Factory for creating isolated StyleSheet instances.
|
|
262
|
+
* Use in SSR to get per-request isolation:
|
|
263
|
+
*
|
|
264
|
+
* const sheet = createSheet()
|
|
265
|
+
* // render with this sheet...
|
|
266
|
+
* const html = sheet.getStyleTag()
|
|
267
|
+
* sheet.reset()
|
|
268
|
+
*/
|
|
269
|
+
const createSheet = (options) => new StyleSheet(options);
|
|
270
|
+
|
|
271
|
+
//#endregion
|
|
272
|
+
//#region src/ThemeProvider.tsx
|
|
273
|
+
const ThemeContext = createContext({});
|
|
274
|
+
/** Hook to read the current theme from the nearest ThemeProvider. */
|
|
275
|
+
const useTheme = () => useContext(ThemeContext);
|
|
276
|
+
/** Provides a theme object to all nested styled components via React context. */
|
|
277
|
+
const ThemeProvider = ({ theme, children }) => /* @__PURE__ */ jsx(ThemeContext.Provider, {
|
|
278
|
+
value: theme,
|
|
279
|
+
children
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
//#endregion
|
|
283
|
+
//#region src/globalStyle.ts
|
|
284
|
+
/**
|
|
285
|
+
* createGlobalStyle() — tagged template function that injects global CSS
|
|
286
|
+
* rules (not scoped to a class name). Returns a React component that
|
|
287
|
+
* injects styles when mounted and supports dynamic interpolations via
|
|
288
|
+
* props/theme.
|
|
289
|
+
*
|
|
290
|
+
* Usage:
|
|
291
|
+
* const GlobalStyle = createGlobalStyle`
|
|
292
|
+
* body { margin: 0; font-family: ${({ theme }) => theme.font}; }
|
|
293
|
+
* *, *::before, *::after { box-sizing: border-box; }
|
|
294
|
+
* `
|
|
295
|
+
* // <GlobalStyle /> — renders nothing, injects global CSS
|
|
296
|
+
*
|
|
297
|
+
* Approach inspired by Goober's glob() + styled-components' API:
|
|
298
|
+
* - Static templates: inject once at component creation (no React overhead)
|
|
299
|
+
* - Dynamic templates: re-inject on prop/theme change via useInsertionEffect
|
|
300
|
+
* - Hash-based dedup prevents duplicate injection
|
|
301
|
+
*/
|
|
302
|
+
const createGlobalStyle = (strings, ...values) => {
|
|
303
|
+
if (!values.some(isDynamic)) {
|
|
304
|
+
const cssText = normalizeCSS(resolve(strings, values, {}));
|
|
305
|
+
if (cssText.trim()) sheet.insertGlobal(cssText);
|
|
306
|
+
const StaticGlobal = () => null;
|
|
307
|
+
StaticGlobal.displayName = "GlobalStyle";
|
|
308
|
+
return StaticGlobal;
|
|
309
|
+
}
|
|
310
|
+
const DynamicGlobal = (props) => {
|
|
311
|
+
const theme = useTheme();
|
|
312
|
+
const cssText = normalizeCSS(resolve(strings, values, {
|
|
313
|
+
...props,
|
|
314
|
+
theme
|
|
315
|
+
}));
|
|
316
|
+
const prevCssRef = useRef("");
|
|
317
|
+
useInsertionEffect(() => {
|
|
318
|
+
if (cssText !== prevCssRef.current) {
|
|
319
|
+
prevCssRef.current = cssText;
|
|
320
|
+
if (cssText.trim()) sheet.insertGlobal(cssText);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
return null;
|
|
324
|
+
};
|
|
325
|
+
DynamicGlobal.displayName = "GlobalStyle";
|
|
326
|
+
return DynamicGlobal;
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
//#endregion
|
|
330
|
+
//#region src/keyframes.ts
|
|
331
|
+
/**
|
|
332
|
+
* keyframes() tagged template function. Creates a CSS @keyframes rule,
|
|
333
|
+
* injects it into the stylesheet, and returns the generated animation name.
|
|
334
|
+
*
|
|
335
|
+
* Usage:
|
|
336
|
+
* const fadeIn = keyframes`
|
|
337
|
+
* from { opacity: 0; }
|
|
338
|
+
* to { opacity: 1; }
|
|
339
|
+
* `
|
|
340
|
+
* // fadeIn === "vl-kf-abc123" (deterministic, hash-based)
|
|
341
|
+
*
|
|
342
|
+
* Supports interpolation values (strings, numbers) but NOT function
|
|
343
|
+
* interpolations — keyframes are static by nature.
|
|
344
|
+
*/
|
|
345
|
+
var KeyframesResult = class {
|
|
346
|
+
name;
|
|
347
|
+
constructor(strings, values) {
|
|
348
|
+
const body = normalizeCSS(resolve(strings, values, {}));
|
|
349
|
+
this.name = `vl-kf-${hash(body)}`;
|
|
350
|
+
sheet.insertKeyframes(this.name, body);
|
|
351
|
+
}
|
|
352
|
+
/** Returns the animation name when used in string context. */
|
|
353
|
+
toString() {
|
|
354
|
+
return this.name;
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
const keyframes = (strings, ...values) => new KeyframesResult(strings, values);
|
|
358
|
+
|
|
359
|
+
//#endregion
|
|
360
|
+
//#region src/forward.ts
|
|
361
|
+
/**
|
|
362
|
+
* HTML prop filtering. Prevents unknown props from being forwarded to DOM
|
|
363
|
+
* elements (which causes React warnings). Props starting with `$` are
|
|
364
|
+
* transient (styling-only) and are always filtered out.
|
|
365
|
+
*/
|
|
366
|
+
const HTML_PROPS = new Set([
|
|
367
|
+
"children",
|
|
368
|
+
"className",
|
|
369
|
+
"dangerouslySetInnerHTML",
|
|
370
|
+
"htmlFor",
|
|
371
|
+
"id",
|
|
372
|
+
"key",
|
|
373
|
+
"ref",
|
|
374
|
+
"style",
|
|
375
|
+
"tabIndex",
|
|
376
|
+
"role",
|
|
377
|
+
"onAbort",
|
|
378
|
+
"onAnimationEnd",
|
|
379
|
+
"onAnimationIteration",
|
|
380
|
+
"onAnimationStart",
|
|
381
|
+
"onBlur",
|
|
382
|
+
"onChange",
|
|
383
|
+
"onClick",
|
|
384
|
+
"onCompositionEnd",
|
|
385
|
+
"onCompositionStart",
|
|
386
|
+
"onCompositionUpdate",
|
|
387
|
+
"onContextMenu",
|
|
388
|
+
"onCopy",
|
|
389
|
+
"onCut",
|
|
390
|
+
"onDoubleClick",
|
|
391
|
+
"onDrag",
|
|
392
|
+
"onDragEnd",
|
|
393
|
+
"onDragEnter",
|
|
394
|
+
"onDragLeave",
|
|
395
|
+
"onDragOver",
|
|
396
|
+
"onDragStart",
|
|
397
|
+
"onDrop",
|
|
398
|
+
"onError",
|
|
399
|
+
"onFocus",
|
|
400
|
+
"onInput",
|
|
401
|
+
"onKeyDown",
|
|
402
|
+
"onKeyPress",
|
|
403
|
+
"onKeyUp",
|
|
404
|
+
"onLoad",
|
|
405
|
+
"onMouseDown",
|
|
406
|
+
"onMouseEnter",
|
|
407
|
+
"onMouseLeave",
|
|
408
|
+
"onMouseMove",
|
|
409
|
+
"onMouseOut",
|
|
410
|
+
"onMouseOver",
|
|
411
|
+
"onMouseUp",
|
|
412
|
+
"onPaste",
|
|
413
|
+
"onPointerCancel",
|
|
414
|
+
"onPointerDown",
|
|
415
|
+
"onPointerEnter",
|
|
416
|
+
"onPointerLeave",
|
|
417
|
+
"onPointerMove",
|
|
418
|
+
"onPointerOut",
|
|
419
|
+
"onPointerOver",
|
|
420
|
+
"onPointerUp",
|
|
421
|
+
"onScroll",
|
|
422
|
+
"onSelect",
|
|
423
|
+
"onSubmit",
|
|
424
|
+
"onTouchCancel",
|
|
425
|
+
"onTouchEnd",
|
|
426
|
+
"onTouchMove",
|
|
427
|
+
"onTouchStart",
|
|
428
|
+
"onTransitionEnd",
|
|
429
|
+
"onWheel",
|
|
430
|
+
"accept",
|
|
431
|
+
"acceptCharset",
|
|
432
|
+
"accessKey",
|
|
433
|
+
"action",
|
|
434
|
+
"allow",
|
|
435
|
+
"allowFullScreen",
|
|
436
|
+
"alt",
|
|
437
|
+
"as",
|
|
438
|
+
"async",
|
|
439
|
+
"autoCapitalize",
|
|
440
|
+
"autoComplete",
|
|
441
|
+
"autoCorrect",
|
|
442
|
+
"autoFocus",
|
|
443
|
+
"autoPlay",
|
|
444
|
+
"capture",
|
|
445
|
+
"cellPadding",
|
|
446
|
+
"cellSpacing",
|
|
447
|
+
"charSet",
|
|
448
|
+
"checked",
|
|
449
|
+
"cite",
|
|
450
|
+
"cols",
|
|
451
|
+
"colSpan",
|
|
452
|
+
"content",
|
|
453
|
+
"contentEditable",
|
|
454
|
+
"controls",
|
|
455
|
+
"controlsList",
|
|
456
|
+
"coords",
|
|
457
|
+
"crossOrigin",
|
|
458
|
+
"dateTime",
|
|
459
|
+
"decoding",
|
|
460
|
+
"default",
|
|
461
|
+
"defaultChecked",
|
|
462
|
+
"defaultValue",
|
|
463
|
+
"defer",
|
|
464
|
+
"dir",
|
|
465
|
+
"disabled",
|
|
466
|
+
"disablePictureInPicture",
|
|
467
|
+
"disableRemotePlayback",
|
|
468
|
+
"download",
|
|
469
|
+
"draggable",
|
|
470
|
+
"encType",
|
|
471
|
+
"enterKeyHint",
|
|
472
|
+
"fetchPriority",
|
|
473
|
+
"form",
|
|
474
|
+
"formAction",
|
|
475
|
+
"formEncType",
|
|
476
|
+
"formMethod",
|
|
477
|
+
"formNoValidate",
|
|
478
|
+
"formTarget",
|
|
479
|
+
"frameBorder",
|
|
480
|
+
"headers",
|
|
481
|
+
"height",
|
|
482
|
+
"hidden",
|
|
483
|
+
"high",
|
|
484
|
+
"href",
|
|
485
|
+
"hrefLang",
|
|
486
|
+
"httpEquiv",
|
|
487
|
+
"inputMode",
|
|
488
|
+
"integrity",
|
|
489
|
+
"is",
|
|
490
|
+
"label",
|
|
491
|
+
"lang",
|
|
492
|
+
"list",
|
|
493
|
+
"loading",
|
|
494
|
+
"loop",
|
|
495
|
+
"low",
|
|
496
|
+
"max",
|
|
497
|
+
"maxLength",
|
|
498
|
+
"media",
|
|
499
|
+
"method",
|
|
500
|
+
"min",
|
|
501
|
+
"minLength",
|
|
502
|
+
"multiple",
|
|
503
|
+
"muted",
|
|
504
|
+
"name",
|
|
505
|
+
"noModule",
|
|
506
|
+
"noValidate",
|
|
507
|
+
"nonce",
|
|
508
|
+
"open",
|
|
509
|
+
"optimum",
|
|
510
|
+
"pattern",
|
|
511
|
+
"placeholder",
|
|
512
|
+
"playsInline",
|
|
513
|
+
"poster",
|
|
514
|
+
"preload",
|
|
515
|
+
"readOnly",
|
|
516
|
+
"referrerPolicy",
|
|
517
|
+
"rel",
|
|
518
|
+
"required",
|
|
519
|
+
"reversed",
|
|
520
|
+
"rows",
|
|
521
|
+
"rowSpan",
|
|
522
|
+
"sandbox",
|
|
523
|
+
"scope",
|
|
524
|
+
"scoped",
|
|
525
|
+
"scrolling",
|
|
526
|
+
"selected",
|
|
527
|
+
"shape",
|
|
528
|
+
"size",
|
|
529
|
+
"sizes",
|
|
530
|
+
"slot",
|
|
531
|
+
"span",
|
|
532
|
+
"spellCheck",
|
|
533
|
+
"src",
|
|
534
|
+
"srcDoc",
|
|
535
|
+
"srcLang",
|
|
536
|
+
"srcSet",
|
|
537
|
+
"start",
|
|
538
|
+
"step",
|
|
539
|
+
"summary",
|
|
540
|
+
"target",
|
|
541
|
+
"title",
|
|
542
|
+
"translate",
|
|
543
|
+
"type",
|
|
544
|
+
"useMap",
|
|
545
|
+
"value",
|
|
546
|
+
"width",
|
|
547
|
+
"wrap"
|
|
548
|
+
]);
|
|
549
|
+
/**
|
|
550
|
+
* Filters props for HTML elements. Keeps valid HTML attrs, data-*, aria-*.
|
|
551
|
+
* Rejects unknown props and $-prefixed transient props.
|
|
552
|
+
*/
|
|
553
|
+
const filterProps = (props) => {
|
|
554
|
+
const filtered = {};
|
|
555
|
+
for (const key in props) {
|
|
556
|
+
if (key.charCodeAt(0) === 36) continue;
|
|
557
|
+
if (key === "as") continue;
|
|
558
|
+
if (key.startsWith("data-") || key.startsWith("aria-")) {
|
|
559
|
+
filtered[key] = props[key];
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
if (HTML_PROPS.has(key)) filtered[key] = props[key];
|
|
563
|
+
}
|
|
564
|
+
return filtered;
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
//#endregion
|
|
568
|
+
//#region src/styled.ts
|
|
569
|
+
/**
|
|
570
|
+
* styled() component factory. Creates React components that inject CSS
|
|
571
|
+
* class names from tagged template literals.
|
|
572
|
+
*
|
|
573
|
+
* Supports:
|
|
574
|
+
* - styled('div')`...` and styled(Component)`...`
|
|
575
|
+
* - styled.div`...` (via Proxy)
|
|
576
|
+
* - `as` prop for polymorphic rendering
|
|
577
|
+
* - forwardRef for ref forwarding
|
|
578
|
+
* - $-prefixed transient props (not forwarded to DOM)
|
|
579
|
+
* - Custom shouldForwardProp for per-component prop filtering
|
|
580
|
+
* - Static path optimization (templates with no dynamic interpolations)
|
|
581
|
+
* - useInsertionEffect for safe concurrent-mode style injection
|
|
582
|
+
*
|
|
583
|
+
* CSS nesting (`&` selectors) works natively — the resolver passes CSS
|
|
584
|
+
* through without transformation, so `&:hover`, `&::before`, etc. work
|
|
585
|
+
* as-is in browsers supporting CSS Nesting (all modern browsers).
|
|
586
|
+
*/
|
|
587
|
+
const IS_SERVER = typeof document === "undefined";
|
|
588
|
+
const getDisplayName = (tag) => typeof tag === "string" ? tag : tag.displayName || tag.name || "Component";
|
|
589
|
+
const mergeClassNames = (generated, user) => {
|
|
590
|
+
if (!user) return generated;
|
|
591
|
+
if (!generated) return user;
|
|
592
|
+
return `${generated} ${user}`;
|
|
593
|
+
};
|
|
594
|
+
const applyPropFilter = (props, customFilter) => {
|
|
595
|
+
if (customFilter) {
|
|
596
|
+
const filtered = {};
|
|
597
|
+
for (const key in props) if (customFilter(key)) filtered[key] = props[key];
|
|
598
|
+
return filtered;
|
|
599
|
+
}
|
|
600
|
+
return filterProps(props);
|
|
601
|
+
};
|
|
602
|
+
const createStyledComponent = (tag, strings, values, options) => {
|
|
603
|
+
const hasDynamicValues = values.some(isDynamic);
|
|
604
|
+
const customFilter = options?.shouldForwardProp;
|
|
605
|
+
if (!hasDynamicValues) {
|
|
606
|
+
const cssText = normalizeCSS(resolve(strings, values, {}));
|
|
607
|
+
const staticClassName = cssText.trim() ? sheet.insert(cssText) : "";
|
|
608
|
+
const staticRule = staticClassName ? `.${staticClassName}{${cssText}}` : "";
|
|
609
|
+
const StaticStyled = forwardRef(({ as: asProp, className: userCls, ...props }, ref) => {
|
|
610
|
+
const finalTag = asProp || tag;
|
|
611
|
+
const finalCls = mergeClassNames(staticClassName, userCls);
|
|
612
|
+
const el = createElement(finalTag, {
|
|
613
|
+
...typeof finalTag === "string" ? applyPropFilter(props, customFilter) : props,
|
|
614
|
+
className: finalCls || void 0,
|
|
615
|
+
ref
|
|
616
|
+
});
|
|
617
|
+
if (staticRule) return createElement(Fragment, null, createElement("style", {
|
|
618
|
+
"data-vl": "",
|
|
619
|
+
dangerouslySetInnerHTML: { __html: staticRule }
|
|
620
|
+
}), el);
|
|
621
|
+
return el;
|
|
622
|
+
});
|
|
623
|
+
StaticStyled.displayName = `styled(${getDisplayName(tag)})`;
|
|
624
|
+
return StaticStyled;
|
|
625
|
+
}
|
|
626
|
+
const DynamicStyled = forwardRef(({ as: asProp, className: userCls, ...props }, ref) => {
|
|
627
|
+
const theme = useTheme();
|
|
628
|
+
const cssText = normalizeCSS(resolve(strings, values, {
|
|
629
|
+
...props,
|
|
630
|
+
theme
|
|
631
|
+
}));
|
|
632
|
+
const lastCssRef = useRef("");
|
|
633
|
+
const lastClsRef = useRef("");
|
|
634
|
+
const insertedRef = useRef(false);
|
|
635
|
+
let className;
|
|
636
|
+
if (cssText === lastCssRef.current) className = lastClsRef.current;
|
|
637
|
+
else {
|
|
638
|
+
className = cssText.trim() ? sheet.getClassName(cssText) : "";
|
|
639
|
+
lastCssRef.current = cssText;
|
|
640
|
+
lastClsRef.current = className;
|
|
641
|
+
insertedRef.current = false;
|
|
642
|
+
}
|
|
643
|
+
if (IS_SERVER) {
|
|
644
|
+
if (!insertedRef.current && cssText.trim()) {
|
|
645
|
+
sheet.insert(cssText);
|
|
646
|
+
insertedRef.current = true;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
const mountedRef = useRef(false);
|
|
650
|
+
useInsertionEffect(() => {
|
|
651
|
+
mountedRef.current = true;
|
|
652
|
+
if (!insertedRef.current && lastCssRef.current.trim()) {
|
|
653
|
+
sheet.insert(lastCssRef.current);
|
|
654
|
+
insertedRef.current = true;
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
const finalTag = asProp || tag;
|
|
658
|
+
const finalCls = mergeClassNames(className, userCls);
|
|
659
|
+
const el = createElement(finalTag, {
|
|
660
|
+
...typeof finalTag === "string" ? applyPropFilter(props, customFilter) : props,
|
|
661
|
+
className: finalCls || void 0,
|
|
662
|
+
ref
|
|
663
|
+
});
|
|
664
|
+
if (!mountedRef.current && className) return createElement(Fragment, null, createElement("style", {
|
|
665
|
+
"data-vl": "",
|
|
666
|
+
dangerouslySetInnerHTML: { __html: `.${className}{${cssText}}` }
|
|
667
|
+
}), el);
|
|
668
|
+
return el;
|
|
669
|
+
});
|
|
670
|
+
DynamicStyled.displayName = `styled(${getDisplayName(tag)})`;
|
|
671
|
+
return DynamicStyled;
|
|
672
|
+
};
|
|
673
|
+
/** Factory function: styled(tag) returns a tagged template function. */
|
|
674
|
+
const styledFactory = (tag, options) => {
|
|
675
|
+
const templateFn = (strings, ...values) => createStyledComponent(tag, strings, values, options);
|
|
676
|
+
return templateFn;
|
|
677
|
+
};
|
|
678
|
+
/**
|
|
679
|
+
* Main styled export. Supports both calling conventions:
|
|
680
|
+
* - `styled('div')` or `styled(Component)` → returns tagged template function
|
|
681
|
+
* - `styled('div', { shouldForwardProp })` → with custom prop filtering
|
|
682
|
+
* - `styled.div` → shorthand via Proxy (no options)
|
|
683
|
+
*/
|
|
684
|
+
const styled = new Proxy(styledFactory, { get(_target, prop) {
|
|
685
|
+
if (prop === "prototype" || prop === "$$typeof") return void 0;
|
|
686
|
+
return (strings, ...values) => createStyledComponent(prop, strings, values);
|
|
687
|
+
} });
|
|
688
|
+
|
|
689
|
+
//#endregion
|
|
690
|
+
export { ThemeProvider, createGlobalStyle, createSheet, css, keyframes, sheet, styled, useTheme };
|
|
691
|
+
//# sourceMappingURL=index.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vitus-labs/styler",
|
|
3
|
+
"version": "2.0.0-alpha.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"author": "Vit Bokisch <vit@bokisch.cz>",
|
|
6
|
+
"maintainers": [
|
|
7
|
+
"Vit Bokisch <vit@bokisch.cz>"
|
|
8
|
+
],
|
|
9
|
+
"type": "module",
|
|
10
|
+
"sideEffects": false,
|
|
11
|
+
"exports": {
|
|
12
|
+
"source": "./src/index.ts",
|
|
13
|
+
"import": "./lib/index.js",
|
|
14
|
+
"types": "./lib/index.d.ts"
|
|
15
|
+
},
|
|
16
|
+
"types": "./lib/index.d.ts",
|
|
17
|
+
"files": [
|
|
18
|
+
"lib",
|
|
19
|
+
"!lib/**/*.map",
|
|
20
|
+
"!lib/analysis"
|
|
21
|
+
],
|
|
22
|
+
"homepage": "https://github.com/vitus-labs/ui-system/tree/master/packages/styler",
|
|
23
|
+
"description": "Lightweight CSS-in-JS engine for vitus-labs packages",
|
|
24
|
+
"keywords": [
|
|
25
|
+
"vitus-labs",
|
|
26
|
+
"css-in-js",
|
|
27
|
+
"styled-components",
|
|
28
|
+
"css",
|
|
29
|
+
"styling"
|
|
30
|
+
],
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/vitus-labs/ui-system",
|
|
37
|
+
"directory": "packages/styler"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/vitus-labs/ui-system/issues"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">= 18"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"prepublish": "bun run build",
|
|
47
|
+
"build": "bun run vl_rolldown_build",
|
|
48
|
+
"build:watch": "bun run vl_rolldown_build-watch",
|
|
49
|
+
"lint": "biome check src/",
|
|
50
|
+
"test": "vitest run",
|
|
51
|
+
"test:coverage": "vitest run --coverage",
|
|
52
|
+
"test:watch": "vitest",
|
|
53
|
+
"typecheck": "tsc --noEmit"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"react": ">= 18"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@emotion/react": "^11.14.0",
|
|
60
|
+
"@emotion/styled": "^11.14.1",
|
|
61
|
+
"@vitus-labs/tools-rolldown": "^1.6.0",
|
|
62
|
+
"@vitus-labs/tools-typescript": "^1.6.0",
|
|
63
|
+
"goober": "^2.1.18",
|
|
64
|
+
"styled-components": "^6.3.9"
|
|
65
|
+
},
|
|
66
|
+
"gitHead": "f2221c6fe2db3e39bfe1c30dc7ff0c01e2c66dc5"
|
|
67
|
+
}
|