@pyreon/styler 0.11.4 → 0.11.6
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 +27 -23
- package/lib/index.d.ts +9 -2
- package/lib/index.js +47 -4
- package/package.json +22 -22
- package/src/ThemeProvider.ts +10 -3
- package/src/__tests__/ThemeProvider.test.ts +21 -21
- package/src/__tests__/benchmark.bench.ts +56 -45
- package/src/__tests__/composition-chain.test.ts +200 -151
- package/src/__tests__/forward.test.ts +122 -122
- package/src/__tests__/globalStyle.test.ts +18 -18
- package/src/__tests__/hash.test.ts +27 -27
- package/src/__tests__/hybrid-injection.test.ts +83 -59
- package/src/__tests__/index.ts +10 -10
- package/src/__tests__/insertion-effect.test.ts +45 -32
- package/src/__tests__/integration.test.ts +81 -51
- package/src/__tests__/keyframes.test.ts +13 -13
- package/src/__tests__/memory-growth.test.ts +21 -21
- package/src/__tests__/p3-features.test.ts +162 -104
- package/src/__tests__/shared.test.ts +51 -33
- package/src/__tests__/sheet-advanced.test.ts +227 -227
- package/src/__tests__/sheet-split-atrules.test.ts +85 -85
- package/src/__tests__/sheet.test.ts +69 -69
- package/src/__tests__/styled-ssr.test.ts +36 -28
- package/src/__tests__/styled.test.ts +214 -145
- package/src/__tests__/theme.test.ts +11 -11
- package/src/__tests__/useCSS.test.ts +89 -59
- package/src/css.ts +1 -1
- package/src/forward.ts +187 -187
- package/src/globalStyle.ts +5 -5
- package/src/index.ts +15 -15
- package/src/keyframes.ts +3 -3
- package/src/resolve.ts +14 -14
- package/src/shared.ts +2 -2
- package/src/sheet.ts +26 -26
- package/src/styled.tsx +145 -100
- package/src/useCSS.ts +4 -4
package/src/globalStyle.ts
CHANGED
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
* *, *::before, *::after { box-sizing: border-box; }
|
|
11
11
|
* `
|
|
12
12
|
*/
|
|
13
|
-
import type { ComponentFn } from
|
|
14
|
-
import { type Interpolation, normalizeCSS, resolve } from
|
|
15
|
-
import { isDynamic } from
|
|
16
|
-
import { sheet } from
|
|
17
|
-
import { useTheme } from
|
|
13
|
+
import type { ComponentFn } from '@pyreon/core'
|
|
14
|
+
import { type Interpolation, normalizeCSS, resolve } from './resolve'
|
|
15
|
+
import { isDynamic } from './shared'
|
|
16
|
+
import { sheet } from './sheet'
|
|
17
|
+
import { useTheme } from './ThemeProvider'
|
|
18
18
|
|
|
19
19
|
export const createGlobalStyle = (
|
|
20
20
|
strings: TemplateStringsArray,
|
package/src/index.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
export { css } from
|
|
2
|
-
export { buildProps, filterProps } from
|
|
3
|
-
export { createGlobalStyle } from
|
|
4
|
-
export { HASH_INIT, hash, hashFinalize, hashUpdate } from
|
|
5
|
-
export { keyframes } from
|
|
6
|
-
export type { CSSResult, Interpolation } from
|
|
7
|
-
export { clearNormCache, normalizeCSS, resolve, resolveValue } from
|
|
8
|
-
export { isDynamic } from
|
|
9
|
-
export type { StyleSheetOptions } from
|
|
10
|
-
export { createSheet, StyleSheet, sheet } from
|
|
11
|
-
export type { StyledFunction, StyledOptions } from
|
|
12
|
-
export { styled } from
|
|
13
|
-
export type { DefaultTheme } from
|
|
14
|
-
export { ThemeContext, ThemeProvider, useTheme } from
|
|
15
|
-
export { useCSS } from
|
|
1
|
+
export { css } from './css'
|
|
2
|
+
export { buildProps, filterProps } from './forward'
|
|
3
|
+
export { createGlobalStyle } from './globalStyle'
|
|
4
|
+
export { HASH_INIT, hash, hashFinalize, hashUpdate } from './hash'
|
|
5
|
+
export { keyframes } from './keyframes'
|
|
6
|
+
export type { CSSResult, Interpolation } from './resolve'
|
|
7
|
+
export { clearNormCache, normalizeCSS, resolve, resolveValue } from './resolve'
|
|
8
|
+
export { isDynamic } from './shared'
|
|
9
|
+
export type { StyleSheetOptions } from './sheet'
|
|
10
|
+
export { createSheet, StyleSheet, sheet } from './sheet'
|
|
11
|
+
export type { StyledFunction, StyledOptions } from './styled'
|
|
12
|
+
export { styled } from './styled'
|
|
13
|
+
export type { DefaultTheme } from './ThemeProvider'
|
|
14
|
+
export { ThemeContext, ThemeProvider, useTheme } from './ThemeProvider'
|
|
15
|
+
export { useCSS } from './useCSS'
|
package/src/keyframes.ts
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
* `
|
|
10
10
|
* // fadeIn === "pyr-kf-abc123" (deterministic, hash-based)
|
|
11
11
|
*/
|
|
12
|
-
import { hash } from
|
|
13
|
-
import { type Interpolation, normalizeCSS, resolve } from
|
|
14
|
-
import { sheet } from
|
|
12
|
+
import { hash } from './hash'
|
|
13
|
+
import { type Interpolation, normalizeCSS, resolve } from './resolve'
|
|
14
|
+
import { sheet } from './sheet'
|
|
15
15
|
|
|
16
16
|
class KeyframesResult {
|
|
17
17
|
readonly name: string
|
package/src/resolve.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* primitive values.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { DefaultTheme } from
|
|
7
|
+
import type { DefaultTheme } from './ThemeProvider'
|
|
8
8
|
|
|
9
9
|
export type Interpolation =
|
|
10
10
|
| string
|
|
@@ -47,19 +47,19 @@ export const resolve = (
|
|
|
47
47
|
const v = values[i]
|
|
48
48
|
const s = strings[i + 1] as string
|
|
49
49
|
// Inline the most common value types to avoid function call overhead.
|
|
50
|
-
if (typeof v ===
|
|
50
|
+
if (typeof v === 'function') {
|
|
51
51
|
const r = v(props)
|
|
52
52
|
result +=
|
|
53
|
-
(typeof r ===
|
|
53
|
+
(typeof r === 'string'
|
|
54
54
|
? r
|
|
55
55
|
: r == null || r === false || r === true
|
|
56
|
-
?
|
|
56
|
+
? ''
|
|
57
57
|
: resolveValue(r as Interpolation, props)) + s
|
|
58
58
|
} else if (v == null || v === false || v === true) {
|
|
59
59
|
result += s
|
|
60
|
-
} else if (typeof v ===
|
|
60
|
+
} else if (typeof v === 'string') {
|
|
61
61
|
result += v + s
|
|
62
|
-
} else if (typeof v ===
|
|
62
|
+
} else if (typeof v === 'number') {
|
|
63
63
|
result += v + s
|
|
64
64
|
} else {
|
|
65
65
|
result += resolveValue(v, props) + s
|
|
@@ -87,7 +87,7 @@ export const normalizeCSS = (css: string): string => {
|
|
|
87
87
|
if (cached !== undefined) return cached
|
|
88
88
|
|
|
89
89
|
const len = css.length
|
|
90
|
-
let out =
|
|
90
|
+
let out = ''
|
|
91
91
|
let space = false // pending space to emit before next non-whitespace char
|
|
92
92
|
let last = 0 // charCode of last char written to output (0 = nothing yet)
|
|
93
93
|
|
|
@@ -96,7 +96,7 @@ export const normalizeCSS = (css: string): string => {
|
|
|
96
96
|
|
|
97
97
|
// /* block comment */
|
|
98
98
|
if (c === 47 /* / */ && css.charCodeAt(i + 1) === 42 /* * */) {
|
|
99
|
-
const end = css.indexOf(
|
|
99
|
+
const end = css.indexOf('*/', i + 2)
|
|
100
100
|
i = end === -1 ? len : end + 1
|
|
101
101
|
space = true
|
|
102
102
|
continue
|
|
@@ -104,7 +104,7 @@ export const normalizeCSS = (css: string): string => {
|
|
|
104
104
|
|
|
105
105
|
// // line comment (but not :// in URLs)
|
|
106
106
|
if (c === 47 /* / */ && css.charCodeAt(i + 1) === 47 /* / */ && last !== 58 /* : */) {
|
|
107
|
-
const nl = css.indexOf(
|
|
107
|
+
const nl = css.indexOf('\n', i + 2)
|
|
108
108
|
i = nl === -1 ? len : nl
|
|
109
109
|
space = true
|
|
110
110
|
continue
|
|
@@ -122,13 +122,13 @@ export const normalizeCSS = (css: string): string => {
|
|
|
122
122
|
continue
|
|
123
123
|
}
|
|
124
124
|
space = false
|
|
125
|
-
out +=
|
|
125
|
+
out += ';'
|
|
126
126
|
last = 59
|
|
127
127
|
continue
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
// Regular char — emit pending space (but not at start of output)
|
|
131
|
-
if (space && last !== 0) out +=
|
|
131
|
+
if (space && last !== 0) out += ' '
|
|
132
132
|
space = false
|
|
133
133
|
|
|
134
134
|
out += css[i]
|
|
@@ -151,17 +151,17 @@ export const normalizeCSS = (css: string): string => {
|
|
|
151
151
|
|
|
152
152
|
export const resolveValue = (value: Interpolation, props: Record<string, any>): string => {
|
|
153
153
|
// null, undefined, false, true → empty (enables conditional: ${cond && css`...`})
|
|
154
|
-
if (value == null || value === false || value === true) return
|
|
154
|
+
if (value == null || value === false || value === true) return ''
|
|
155
155
|
|
|
156
156
|
// function interpolation → call with props/theme context, resolve result
|
|
157
|
-
if (typeof value ===
|
|
157
|
+
if (typeof value === 'function') return resolveValue(value(props) as Interpolation, props)
|
|
158
158
|
|
|
159
159
|
// nested CSSResult → recursively resolve
|
|
160
160
|
if (value instanceof CSSResult) return resolve(value.strings, value.values, props)
|
|
161
161
|
|
|
162
162
|
// array of results (e.g. from makeItResponsive's breakpoints.map())
|
|
163
163
|
if (Array.isArray(value)) {
|
|
164
|
-
let arrayResult =
|
|
164
|
+
let arrayResult = ''
|
|
165
165
|
for (let i = 0; i < value.length; i++) {
|
|
166
166
|
arrayResult += resolveValue(value[i], props)
|
|
167
167
|
}
|
package/src/shared.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared utilities used across multiple modules.
|
|
3
3
|
*/
|
|
4
|
-
import { CSSResult, type Interpolation } from
|
|
4
|
+
import { CSSResult, type Interpolation } from './resolve'
|
|
5
5
|
|
|
6
6
|
/** Check if an interpolation value is dynamic (contains functions or nested dynamic CSSResults). */
|
|
7
7
|
export const isDynamic = (v: Interpolation): boolean => {
|
|
8
|
-
if (typeof v ===
|
|
8
|
+
if (typeof v === 'function') return true
|
|
9
9
|
if (Array.isArray(v)) return v.some(isDynamic)
|
|
10
10
|
if (v instanceof CSSResult) return v.values.some(isDynamic)
|
|
11
11
|
return false
|
package/src/sheet.ts
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
* Media queries (@media), @supports, and @container blocks nested inside
|
|
6
6
|
* component CSS are automatically extracted into separate top-level rules.
|
|
7
7
|
*/
|
|
8
|
-
import { hash } from
|
|
9
|
-
import { clearNormCache } from
|
|
8
|
+
import { hash } from './hash'
|
|
9
|
+
import { clearNormCache } from './resolve'
|
|
10
10
|
|
|
11
|
-
const PREFIX =
|
|
12
|
-
const ATTR =
|
|
11
|
+
const PREFIX = 'pyr'
|
|
12
|
+
const ATTR = 'data-pyreon-styler'
|
|
13
13
|
const DEFAULT_MAX_CACHE_SIZE = 10000
|
|
14
14
|
|
|
15
15
|
export interface StyleSheetOptions {
|
|
@@ -31,7 +31,7 @@ export class StyleSheet {
|
|
|
31
31
|
constructor(options: StyleSheetOptions = {}) {
|
|
32
32
|
this.maxCacheSize = options.maxCacheSize ?? DEFAULT_MAX_CACHE_SIZE
|
|
33
33
|
this.layer = options.layer
|
|
34
|
-
this.isSSR = typeof document ===
|
|
34
|
+
this.isSSR = typeof document === 'undefined'
|
|
35
35
|
if (!this.isSSR) this.mount()
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -43,8 +43,8 @@ export class StyleSheet {
|
|
|
43
43
|
this.sheet = existing.sheet ?? null
|
|
44
44
|
this.hydrateFromTag(existing)
|
|
45
45
|
} else {
|
|
46
|
-
const el = document.createElement(
|
|
47
|
-
el.setAttribute(ATTR,
|
|
46
|
+
const el = document.createElement('style')
|
|
47
|
+
el.setAttribute(ATTR, '')
|
|
48
48
|
document.head.appendChild(el)
|
|
49
49
|
this.sheet = el.sheet ?? null
|
|
50
50
|
}
|
|
@@ -61,8 +61,8 @@ export class StyleSheet {
|
|
|
61
61
|
|
|
62
62
|
/** Extract className from a selector like ".pyr-abc" or ".pyr-abc.pyr-abc" → "pyr-abc" */
|
|
63
63
|
private extractClassName(selectorText: string): string | null {
|
|
64
|
-
if (selectorText[0] !==
|
|
65
|
-
const dotIdx = selectorText.indexOf(
|
|
64
|
+
if (selectorText[0] !== '.') return null
|
|
65
|
+
const dotIdx = selectorText.indexOf('.', 1)
|
|
66
66
|
return dotIdx > 0 ? selectorText.slice(1, dotIdx) : selectorText.slice(1)
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -81,7 +81,7 @@ export class StyleSheet {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
// Handle split @media rules that wrap our selectors
|
|
84
|
-
if (typeof CSSMediaRule !==
|
|
84
|
+
if (typeof CSSMediaRule !== 'undefined' && rule instanceof CSSMediaRule) {
|
|
85
85
|
for (let j = 0; j < rule.cssRules.length; j++) {
|
|
86
86
|
const inner = rule.cssRules[j]
|
|
87
87
|
if (inner instanceof CSSStyleRule) {
|
|
@@ -114,7 +114,7 @@ export class StyleSheet {
|
|
|
114
114
|
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: complex logic is inherent to this function
|
|
115
115
|
private splitAtRules(cssText: string, selector: string): { base: string; atRules: string[] } {
|
|
116
116
|
// Fast path: no at-rules to split
|
|
117
|
-
if (cssText.indexOf(
|
|
117
|
+
if (cssText.indexOf('@') === -1) return { base: cssText, atRules: [] }
|
|
118
118
|
|
|
119
119
|
const atRules: string[] = []
|
|
120
120
|
const baseParts: string[] = []
|
|
@@ -125,13 +125,13 @@ export class StyleSheet {
|
|
|
125
125
|
for (let i = 0; i < cssText.length; i++) {
|
|
126
126
|
const ch = cssText[i]
|
|
127
127
|
|
|
128
|
-
if (ch ===
|
|
128
|
+
if (ch === '{') {
|
|
129
129
|
depth++
|
|
130
|
-
} else if (ch ===
|
|
130
|
+
} else if (ch === '}') {
|
|
131
131
|
depth--
|
|
132
132
|
if (depth === 0 && atStart >= 0) {
|
|
133
133
|
// End of a tracked at-rule block — extract and wrap with selector
|
|
134
|
-
const openBrace = cssText.indexOf(
|
|
134
|
+
const openBrace = cssText.indexOf('{', atStart)
|
|
135
135
|
const atPrefix = cssText.slice(atStart, openBrace).trim()
|
|
136
136
|
const innerCSS = cssText.slice(openBrace + 1, i).trim()
|
|
137
137
|
if (innerCSS) {
|
|
@@ -140,7 +140,7 @@ export class StyleSheet {
|
|
|
140
140
|
atStart = -1
|
|
141
141
|
lastBase = i + 1
|
|
142
142
|
}
|
|
143
|
-
} else if (depth === 0 && ch ===
|
|
143
|
+
} else if (depth === 0 && ch === '@' && atStart < 0) {
|
|
144
144
|
// Check if this starts a splittable at-rule (not @keyframes, @font-face, etc.)
|
|
145
145
|
const remaining = cssText.slice(i, i + 20)
|
|
146
146
|
if (/^@(?:media|supports|container)\b/.test(remaining)) {
|
|
@@ -161,7 +161,7 @@ export class StyleSheet {
|
|
|
161
161
|
// If no at-rules were found, return original unchanged
|
|
162
162
|
if (atRules.length === 0) return { base: cssText, atRules: [] }
|
|
163
163
|
|
|
164
|
-
return { base: baseParts.join(
|
|
164
|
+
return { base: baseParts.join(' '), atRules }
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
/**
|
|
@@ -221,9 +221,9 @@ export class StyleSheet {
|
|
|
221
221
|
try {
|
|
222
222
|
this.sheet.insertRule(rule, this.sheet.cssRules.length)
|
|
223
223
|
} catch (_e) {
|
|
224
|
-
if (process.env.NODE_ENV !==
|
|
224
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
225
225
|
// biome-ignore lint/suspicious/noConsole: dev-only CSS rule insertion warning
|
|
226
|
-
console.warn(
|
|
226
|
+
console.warn('[styler] Failed to insert CSS rule:', rule, _e)
|
|
227
227
|
}
|
|
228
228
|
}
|
|
229
229
|
}
|
|
@@ -248,7 +248,7 @@ export class StyleSheet {
|
|
|
248
248
|
try {
|
|
249
249
|
this.sheet.insertRule(rule, this.sheet.cssRules.length)
|
|
250
250
|
} catch (_e) {
|
|
251
|
-
if (process.env.NODE_ENV !==
|
|
251
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
252
252
|
// silently ignore invalid CSS rules in production
|
|
253
253
|
}
|
|
254
254
|
}
|
|
@@ -266,8 +266,8 @@ export class StyleSheet {
|
|
|
266
266
|
|
|
267
267
|
for (let i = 0; i < cssText.length; i++) {
|
|
268
268
|
const ch = cssText[i]
|
|
269
|
-
if (ch ===
|
|
270
|
-
else if (ch ===
|
|
269
|
+
if (ch === '{') depth++
|
|
270
|
+
else if (ch === '}') {
|
|
271
271
|
depth--
|
|
272
272
|
if (depth === 0) {
|
|
273
273
|
const rule = cssText.slice(start, i + 1).trim()
|
|
@@ -298,9 +298,9 @@ export class StyleSheet {
|
|
|
298
298
|
try {
|
|
299
299
|
this.sheet.insertRule(rule, this.sheet.cssRules.length)
|
|
300
300
|
} catch (_e) {
|
|
301
|
-
if (process.env.NODE_ENV !==
|
|
301
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
302
302
|
// biome-ignore lint/suspicious/noConsole: dev-only CSS rule insertion warning
|
|
303
|
-
console.warn(
|
|
303
|
+
console.warn('[styler] Failed to insert global CSS rule:', rule, _e)
|
|
304
304
|
}
|
|
305
305
|
}
|
|
306
306
|
}
|
|
@@ -309,13 +309,13 @@ export class StyleSheet {
|
|
|
309
309
|
|
|
310
310
|
/** Returns collected CSS for SSR as a complete `<style>` tag string. */
|
|
311
311
|
getStyleTag(): string {
|
|
312
|
-
const css = this.ssrBuffer.join(
|
|
312
|
+
const css = this.ssrBuffer.join('').replace(/<\/style/gi, '<\\/style')
|
|
313
313
|
return `<style ${ATTR}="">${css}</style>`
|
|
314
314
|
}
|
|
315
315
|
|
|
316
316
|
/** Returns collected CSS rules as a raw string (useful for streaming SSR). */
|
|
317
317
|
getStyles(): string {
|
|
318
|
-
return this.ssrBuffer.join(
|
|
318
|
+
return this.ssrBuffer.join('')
|
|
319
319
|
}
|
|
320
320
|
|
|
321
321
|
/** Reset SSR buffer and cache (call between server requests). */
|
|
@@ -363,7 +363,7 @@ export class StyleSheet {
|
|
|
363
363
|
|
|
364
364
|
const finalRules = this.layer ? allRules.map((r) => `@layer ${this.layer}{${r}}`) : allRules
|
|
365
365
|
|
|
366
|
-
return { className, rules: finalRules.join(
|
|
366
|
+
return { className, rules: finalRules.join('') }
|
|
367
367
|
}
|
|
368
368
|
|
|
369
369
|
/** Check if a className is already in the cache. O(1) Map lookup. */
|
package/src/styled.tsx
CHANGED
|
@@ -15,13 +15,14 @@
|
|
|
15
15
|
* through without transformation, so `&:hover`, `&::before`, etc. work
|
|
16
16
|
* as-is in browsers supporting CSS Nesting (all modern browsers).
|
|
17
17
|
*/
|
|
18
|
-
import type { ComponentFn, VNode } from
|
|
19
|
-
import { h } from
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
18
|
+
import type { ComponentFn, VNode } from '@pyreon/core'
|
|
19
|
+
import { h } from '@pyreon/core'
|
|
20
|
+
import { effect } from '@pyreon/reactivity'
|
|
21
|
+
import { buildProps } from './forward'
|
|
22
|
+
import { type Interpolation, normalizeCSS, resolve } from './resolve'
|
|
23
|
+
import { isDynamic } from './shared'
|
|
24
|
+
import { sheet } from './sheet'
|
|
25
|
+
import { useTheme } from './ThemeProvider'
|
|
25
26
|
|
|
26
27
|
type Tag = string | ComponentFn<any>
|
|
27
28
|
|
|
@@ -37,9 +38,9 @@ export interface StyledOptions {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
const getDisplayName = (tag: Tag): string =>
|
|
40
|
-
typeof tag ===
|
|
41
|
+
typeof tag === 'string'
|
|
41
42
|
? tag
|
|
42
|
-
: (tag as ComponentFn<any> & { displayName?: string }).displayName || tag.name ||
|
|
43
|
+
: (tag as ComponentFn<any> & { displayName?: string }).displayName || tag.name || 'Component'
|
|
43
44
|
|
|
44
45
|
// Component cache: same template literal + tag + no options → same component.
|
|
45
46
|
// WeakMap on `strings` (TemplateStringsArray is object-identity per source location).
|
|
@@ -86,11 +87,11 @@ const createStyledComponent = (
|
|
|
86
87
|
const cssText = normalizeCSS(raw)
|
|
87
88
|
const hasCss = cssText.length > 0
|
|
88
89
|
|
|
89
|
-
const staticClassName = hasCss ? sheet.insert(cssText, boost) :
|
|
90
|
+
const staticClassName = hasCss ? sheet.insert(cssText, boost) : ''
|
|
90
91
|
|
|
91
92
|
const StaticStyled: ComponentFn = (rawProps: Record<string, any>): VNode | null => {
|
|
92
93
|
const finalTag = rawProps.as || tag
|
|
93
|
-
const isDOM = typeof finalTag ===
|
|
94
|
+
const isDOM = typeof finalTag === 'string'
|
|
94
95
|
const finalProps = buildProps(rawProps, staticClassName, isDOM, customFilter)
|
|
95
96
|
|
|
96
97
|
return h(
|
|
@@ -124,17 +125,61 @@ const createStyledComponent = (
|
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
// DYNAMIC PATH: resolve CSS on every render with theme/props.
|
|
128
|
+
// When $rocketstyle is a function accessor (from rocketstyle's
|
|
129
|
+
// EnhancedComponent), we resolve it initially for the first class,
|
|
130
|
+
// then set up an effect to reactively swap classes when mode changes.
|
|
131
|
+
// This avoids VNode remounting — only classList updates on the DOM element.
|
|
127
132
|
const DynamicStyled: ComponentFn = (rawProps: Record<string, any>): VNode | null => {
|
|
128
133
|
const theme = useTheme()
|
|
129
|
-
const
|
|
130
|
-
const
|
|
134
|
+
const $rs = rawProps.$rocketstyle
|
|
135
|
+
const isReactiveRS = typeof $rs === 'function'
|
|
131
136
|
|
|
132
|
-
|
|
137
|
+
// Resolve initial $rocketstyle value
|
|
138
|
+
const resolvedRS = isReactiveRS ? $rs() : $rs
|
|
139
|
+
const initialProps = isReactiveRS ? { ...rawProps, $rocketstyle: resolvedRS } : rawProps
|
|
140
|
+
const cssText = normalizeCSS(resolve(strings, values, { ...initialProps, theme }))
|
|
141
|
+
const initialClassName = cssText.length > 0 ? sheet.insert(cssText, boost) : ''
|
|
133
142
|
|
|
134
143
|
const finalTag = rawProps.as || tag
|
|
135
|
-
const isDOM = typeof finalTag ===
|
|
136
|
-
const finalProps = buildProps(rawProps, className, isDOM, customFilter)
|
|
144
|
+
const isDOM = typeof finalTag === 'string'
|
|
137
145
|
|
|
146
|
+
// Track mounted element for reactive class updates
|
|
147
|
+
let el: Element | null = null
|
|
148
|
+
let currentClassName = initialClassName
|
|
149
|
+
|
|
150
|
+
const originalRef = rawProps.ref
|
|
151
|
+
const refCallback = (node: Element | null) => {
|
|
152
|
+
el = node
|
|
153
|
+
if (originalRef) {
|
|
154
|
+
if (typeof originalRef === 'function') originalRef(node)
|
|
155
|
+
else if (originalRef && typeof originalRef === 'object') originalRef.current = node
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const finalProps = buildProps(
|
|
160
|
+
{ ...rawProps, ref: isReactiveRS ? refCallback : rawProps.ref },
|
|
161
|
+
initialClassName,
|
|
162
|
+
isDOM,
|
|
163
|
+
customFilter,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
// Set up reactive class swap when $rocketstyle is a function accessor
|
|
167
|
+
if (isReactiveRS) {
|
|
168
|
+
effect(() => {
|
|
169
|
+
const newRS = $rs() // reactive read — tracks mode dependency
|
|
170
|
+
const newResolvedProps = { ...rawProps, $rocketstyle: newRS }
|
|
171
|
+
const newCss = normalizeCSS(resolve(strings, values, { ...newResolvedProps, theme }))
|
|
172
|
+
const newClass = newCss.length > 0 ? sheet.insert(newCss, boost) : ''
|
|
173
|
+
|
|
174
|
+
if (el && newClass !== currentClassName) {
|
|
175
|
+
if (currentClassName) el.classList.remove(currentClassName)
|
|
176
|
+
if (newClass) el.classList.add(newClass)
|
|
177
|
+
currentClassName = newClass
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// STATIC VNode — created once, never remounted on mode change
|
|
138
183
|
return h(
|
|
139
184
|
finalTag as string,
|
|
140
185
|
finalProps,
|
|
@@ -171,89 +216,89 @@ const proxyCache = new Map<string, (...args: any[]) => any>()
|
|
|
171
216
|
type TagTemplateFn = (strings: TemplateStringsArray, ...values: Interpolation[]) => ComponentFn
|
|
172
217
|
|
|
173
218
|
type HtmlTags =
|
|
174
|
-
|
|
|
175
|
-
|
|
|
176
|
-
|
|
|
177
|
-
|
|
|
178
|
-
|
|
|
179
|
-
|
|
|
180
|
-
|
|
|
181
|
-
|
|
|
182
|
-
|
|
|
183
|
-
|
|
|
184
|
-
|
|
|
185
|
-
|
|
|
186
|
-
|
|
|
187
|
-
|
|
|
188
|
-
|
|
|
189
|
-
|
|
|
190
|
-
|
|
|
191
|
-
|
|
|
192
|
-
|
|
|
193
|
-
|
|
|
194
|
-
|
|
|
195
|
-
|
|
|
196
|
-
|
|
|
197
|
-
|
|
|
198
|
-
|
|
|
199
|
-
|
|
|
200
|
-
|
|
|
201
|
-
|
|
|
202
|
-
|
|
|
203
|
-
|
|
|
204
|
-
|
|
|
205
|
-
|
|
|
206
|
-
|
|
|
207
|
-
|
|
|
208
|
-
|
|
|
209
|
-
|
|
|
210
|
-
|
|
|
211
|
-
|
|
|
212
|
-
|
|
|
213
|
-
|
|
|
214
|
-
|
|
|
215
|
-
|
|
|
216
|
-
|
|
|
217
|
-
|
|
|
218
|
-
|
|
|
219
|
-
|
|
|
220
|
-
|
|
|
221
|
-
|
|
|
222
|
-
|
|
|
223
|
-
|
|
|
224
|
-
|
|
|
225
|
-
|
|
|
226
|
-
|
|
|
227
|
-
|
|
|
228
|
-
|
|
|
229
|
-
|
|
|
230
|
-
|
|
|
231
|
-
|
|
|
232
|
-
|
|
|
233
|
-
|
|
|
234
|
-
|
|
|
235
|
-
|
|
|
236
|
-
|
|
|
237
|
-
|
|
|
238
|
-
|
|
|
239
|
-
|
|
|
240
|
-
|
|
|
241
|
-
|
|
|
242
|
-
|
|
|
243
|
-
|
|
|
244
|
-
|
|
|
245
|
-
|
|
|
246
|
-
|
|
|
247
|
-
|
|
|
248
|
-
|
|
|
249
|
-
|
|
|
250
|
-
|
|
|
251
|
-
|
|
|
252
|
-
|
|
|
253
|
-
|
|
|
254
|
-
|
|
|
255
|
-
|
|
|
256
|
-
|
|
|
219
|
+
| 'a'
|
|
220
|
+
| 'abbr'
|
|
221
|
+
| 'address'
|
|
222
|
+
| 'article'
|
|
223
|
+
| 'aside'
|
|
224
|
+
| 'audio'
|
|
225
|
+
| 'b'
|
|
226
|
+
| 'blockquote'
|
|
227
|
+
| 'body'
|
|
228
|
+
| 'br'
|
|
229
|
+
| 'button'
|
|
230
|
+
| 'canvas'
|
|
231
|
+
| 'caption'
|
|
232
|
+
| 'code'
|
|
233
|
+
| 'col'
|
|
234
|
+
| 'colgroup'
|
|
235
|
+
| 'dd'
|
|
236
|
+
| 'details'
|
|
237
|
+
| 'div'
|
|
238
|
+
| 'dl'
|
|
239
|
+
| 'dt'
|
|
240
|
+
| 'em'
|
|
241
|
+
| 'fieldset'
|
|
242
|
+
| 'figcaption'
|
|
243
|
+
| 'figure'
|
|
244
|
+
| 'footer'
|
|
245
|
+
| 'form'
|
|
246
|
+
| 'h1'
|
|
247
|
+
| 'h2'
|
|
248
|
+
| 'h3'
|
|
249
|
+
| 'h4'
|
|
250
|
+
| 'h5'
|
|
251
|
+
| 'h6'
|
|
252
|
+
| 'head'
|
|
253
|
+
| 'header'
|
|
254
|
+
| 'hr'
|
|
255
|
+
| 'html'
|
|
256
|
+
| 'i'
|
|
257
|
+
| 'iframe'
|
|
258
|
+
| 'img'
|
|
259
|
+
| 'input'
|
|
260
|
+
| 'label'
|
|
261
|
+
| 'legend'
|
|
262
|
+
| 'li'
|
|
263
|
+
| 'link'
|
|
264
|
+
| 'main'
|
|
265
|
+
| 'mark'
|
|
266
|
+
| 'menu'
|
|
267
|
+
| 'meta'
|
|
268
|
+
| 'nav'
|
|
269
|
+
| 'ol'
|
|
270
|
+
| 'optgroup'
|
|
271
|
+
| 'option'
|
|
272
|
+
| 'output'
|
|
273
|
+
| 'p'
|
|
274
|
+
| 'picture'
|
|
275
|
+
| 'pre'
|
|
276
|
+
| 'progress'
|
|
277
|
+
| 'q'
|
|
278
|
+
| 'section'
|
|
279
|
+
| 'select'
|
|
280
|
+
| 'small'
|
|
281
|
+
| 'source'
|
|
282
|
+
| 'span'
|
|
283
|
+
| 'strong'
|
|
284
|
+
| 'style'
|
|
285
|
+
| 'sub'
|
|
286
|
+
| 'summary'
|
|
287
|
+
| 'sup'
|
|
288
|
+
| 'svg'
|
|
289
|
+
| 'table'
|
|
290
|
+
| 'tbody'
|
|
291
|
+
| 'td'
|
|
292
|
+
| 'template'
|
|
293
|
+
| 'textarea'
|
|
294
|
+
| 'tfoot'
|
|
295
|
+
| 'th'
|
|
296
|
+
| 'thead'
|
|
297
|
+
| 'time'
|
|
298
|
+
| 'tr'
|
|
299
|
+
| 'u'
|
|
300
|
+
| 'ul'
|
|
301
|
+
| 'video'
|
|
257
302
|
|
|
258
303
|
export type StyledFunction = ((tag: Tag, options?: StyledOptions) => TagTemplateFn) & {
|
|
259
304
|
[K in HtmlTags]: TagTemplateFn
|
|
@@ -264,7 +309,7 @@ export type StyledFunction = ((tag: Tag, options?: StyledOptions) => TagTemplate
|
|
|
264
309
|
// Proxy target uses `as any` because TS can't resolve Proxy<StyledFunction> with mapped types
|
|
265
310
|
export const styled: StyledFunction = new Proxy(styledFactory as any, {
|
|
266
311
|
get(_target: unknown, prop: string) {
|
|
267
|
-
if (prop ===
|
|
312
|
+
if (prop === 'prototype' || prop === '$$typeof') return undefined
|
|
268
313
|
// styled.div`...`, styled.span`...`, etc.
|
|
269
314
|
let fn = proxyCache.get(prop)
|
|
270
315
|
if (!fn) {
|
package/src/useCSS.ts
CHANGED
|
@@ -5,16 +5,16 @@
|
|
|
5
5
|
* Use this when you need computed CSS class names on plain elements
|
|
6
6
|
* without the overhead of a styled component layer.
|
|
7
7
|
*/
|
|
8
|
-
import { type CSSResult, normalizeCSS, resolve } from
|
|
9
|
-
import { sheet } from
|
|
10
|
-
import { useTheme } from
|
|
8
|
+
import { type CSSResult, normalizeCSS, resolve } from './resolve'
|
|
9
|
+
import { sheet } from './sheet'
|
|
10
|
+
import { useTheme } from './ThemeProvider'
|
|
11
11
|
|
|
12
12
|
export function useCSS(template: CSSResult, props?: Record<string, any>, boost?: boolean): string {
|
|
13
13
|
const theme = useTheme()
|
|
14
14
|
const allProps = theme ? { ...props, theme } : (props ?? {})
|
|
15
15
|
const cssText = normalizeCSS(resolve(template.strings, template.values, allProps))
|
|
16
16
|
|
|
17
|
-
if (!cssText.trim()) return
|
|
17
|
+
if (!cssText.trim()) return ''
|
|
18
18
|
|
|
19
19
|
return sheet.insert(cssText, boost)
|
|
20
20
|
}
|