@pyreon/styler 0.24.5 → 0.24.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/package.json +5 -7
- package/src/ThemeProvider.ts +0 -65
- package/src/__tests__/ThemeProvider.test.ts +0 -67
- package/src/__tests__/benchmark.bench.ts +0 -200
- package/src/__tests__/composition-chain.test.ts +0 -537
- package/src/__tests__/css.test.ts +0 -70
- package/src/__tests__/dev-gate-treeshake.test.ts +0 -85
- package/src/__tests__/forward.test.ts +0 -282
- package/src/__tests__/globalStyle.test.ts +0 -72
- package/src/__tests__/hash.test.ts +0 -70
- package/src/__tests__/hybrid-injection.test.ts +0 -225
- package/src/__tests__/index.ts +0 -14
- package/src/__tests__/inject-rules.browser.test.ts +0 -40
- package/src/__tests__/insertion-effect.test.ts +0 -119
- package/src/__tests__/integration-dom.test.ts +0 -58
- package/src/__tests__/integration.test.ts +0 -179
- package/src/__tests__/keyframes.test.ts +0 -68
- package/src/__tests__/memory-growth.test.ts +0 -220
- package/src/__tests__/native-marker.test.ts +0 -9
- package/src/__tests__/p3-features.test.ts +0 -316
- package/src/__tests__/resolve-cache.test.ts +0 -94
- package/src/__tests__/resolve.test.ts +0 -308
- package/src/__tests__/shared.test.ts +0 -133
- package/src/__tests__/sheet-advanced.test.ts +0 -659
- package/src/__tests__/sheet-split-atrules.test.ts +0 -410
- package/src/__tests__/sheet.test.ts +0 -250
- package/src/__tests__/static-styler-resolve-cost.test.ts +0 -160
- package/src/__tests__/styled-reactive.test.ts +0 -74
- package/src/__tests__/styled-ssr.test.ts +0 -75
- package/src/__tests__/styled.test.ts +0 -511
- package/src/__tests__/styler.browser.test.tsx +0 -194
- package/src/__tests__/theme.test.ts +0 -33
- package/src/__tests__/useCSS.test.ts +0 -172
- package/src/css.ts +0 -13
- package/src/env.d.ts +0 -6
- package/src/forward.ts +0 -308
- package/src/globalStyle.ts +0 -53
- package/src/hash.ts +0 -28
- package/src/index.ts +0 -15
- package/src/keyframes.ts +0 -36
- package/src/manifest.ts +0 -332
- package/src/resolve.ts +0 -225
- package/src/shared.ts +0 -22
- package/src/sheet.ts +0 -635
- package/src/styled.tsx +0 -503
- package/src/tests/manifest-snapshot.test.ts +0 -51
- package/src/useCSS.ts +0 -20
package/src/styled.tsx
DELETED
|
@@ -1,503 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* styled() component factory. Creates Pyreon components that inject CSS
|
|
3
|
-
* class names from tagged template literals.
|
|
4
|
-
*
|
|
5
|
-
* Supports:
|
|
6
|
-
* - styled('div')`...` and styled(Component)`...`
|
|
7
|
-
* - styled.div`...` (via Proxy)
|
|
8
|
-
* - `as` prop for polymorphic rendering
|
|
9
|
-
* - $-prefixed transient props (not forwarded to DOM)
|
|
10
|
-
* - Custom shouldForwardProp for per-component prop filtering
|
|
11
|
-
* - Static path optimization (templates with no dynamic interpolations)
|
|
12
|
-
* - Boost specificity via doubled selector
|
|
13
|
-
*
|
|
14
|
-
* CSS nesting (`&` selectors) works natively — the resolver passes CSS
|
|
15
|
-
* through without transformation, so `&:hover`, `&::before`, etc. work
|
|
16
|
-
* as-is in browsers supporting CSS Nesting (all modern browsers).
|
|
17
|
-
*/
|
|
18
|
-
import type { ComponentFn, VNode } from '@pyreon/core'
|
|
19
|
-
import { h } from '@pyreon/core'
|
|
20
|
-
import { computed, renderEffect, runUntracked } from '@pyreon/reactivity'
|
|
21
|
-
import { buildProps } from './forward'
|
|
22
|
-
import { type Interpolation, normalizeCSS, resolve } from './resolve'
|
|
23
|
-
import { isDynamic } from './shared'
|
|
24
|
-
import { onSheetClear, sheet } from './sheet'
|
|
25
|
-
import { useThemeAccessor } from './ThemeProvider'
|
|
26
|
-
|
|
27
|
-
// Dev-time counter sink — see packages/internals/perf-harness/COUNTERS.md.
|
|
28
|
-
const _countSink = globalThis as { __pyreon_count__?: (name: string, n?: number) => void }
|
|
29
|
-
|
|
30
|
-
type Tag = string | ComponentFn<any>
|
|
31
|
-
|
|
32
|
-
export interface StyledOptions {
|
|
33
|
-
/** Custom prop filter. Return true to forward the prop to the DOM element. */
|
|
34
|
-
shouldForwardProp?: (prop: string) => boolean
|
|
35
|
-
/**
|
|
36
|
-
* CSS @layer name. Rules are wrapped in `@layer <name> { ... }`.
|
|
37
|
-
* Framework CSS uses two layers with explicit ordering:
|
|
38
|
-
* `@layer elements, rocketstyle;`
|
|
39
|
-
* Elements (base layout) use `'elements'`, rocketstyle themes use
|
|
40
|
-
* `'rocketstyle'`. The ordering ensures themes always override base
|
|
41
|
-
* styles regardless of source order.
|
|
42
|
-
*/
|
|
43
|
-
layer?: string
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const getDisplayName = (tag: Tag): string =>
|
|
47
|
-
typeof tag === 'string'
|
|
48
|
-
? tag
|
|
49
|
-
: (tag as ComponentFn<any> & { displayName?: string }).displayName || tag.name || 'Component'
|
|
50
|
-
|
|
51
|
-
// Component cache: same template literal + tag + no options → same component.
|
|
52
|
-
// WeakMap on `strings` (TemplateStringsArray is object-identity per source location).
|
|
53
|
-
// `let` so `sheet.clearAll()` (HMR / dev reload) can drop stale entries by
|
|
54
|
-
// swapping the WeakMap reference — WeakMap has no `.clear()` method, and stale
|
|
55
|
-
// `StaticStyled` ComponentFns left behind would keep returning class names the
|
|
56
|
-
// sheet just deleted from the DOM.
|
|
57
|
-
let staticComponentCache = new WeakMap<TemplateStringsArray, Map<Tag, ComponentFn>>()
|
|
58
|
-
|
|
59
|
-
// Single-entry hot cache — 3 reference comparisons, no Map/WeakMap overhead.
|
|
60
|
-
// All 3 fields move atomically (consolidated into one object so `clearAll`
|
|
61
|
-
// resets them together — pre-fix, partial state was possible if a reset
|
|
62
|
-
// path forgot one field).
|
|
63
|
-
const _hotCache: {
|
|
64
|
-
strings: TemplateStringsArray | null
|
|
65
|
-
tag: Tag | null
|
|
66
|
-
component: ComponentFn | null
|
|
67
|
-
} = { strings: null, tag: null, component: null }
|
|
68
|
-
|
|
69
|
-
// Subscribe to `sheet.clearAll()` (HMR / dev-time reset). Drops both the
|
|
70
|
-
// WeakMap and the hot-cache slots so subsequent `styled()` calls produce
|
|
71
|
-
// fresh components with up-to-date class names. Static class names emitted
|
|
72
|
-
// before `clearAll` are stale by the time the user observes them — the rule
|
|
73
|
-
// they pointed at has been deleted from the DOM.
|
|
74
|
-
onSheetClear(() => {
|
|
75
|
-
staticComponentCache = new WeakMap()
|
|
76
|
-
_hotCache.strings = null
|
|
77
|
-
_hotCache.tag = null
|
|
78
|
-
_hotCache.component = null
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
const createStyledComponent = (
|
|
82
|
-
tag: Tag,
|
|
83
|
-
strings: TemplateStringsArray,
|
|
84
|
-
values: Interpolation[],
|
|
85
|
-
options?: StyledOptions,
|
|
86
|
-
): ComponentFn => {
|
|
87
|
-
// Ultra-fast hot cache: 3 reference comparisons → return immediately
|
|
88
|
-
if (values.length === 0 && !options) {
|
|
89
|
-
if (strings === _hotCache.strings && tag === _hotCache.tag)
|
|
90
|
-
return _hotCache.component as ComponentFn
|
|
91
|
-
|
|
92
|
-
// WeakMap fallback for alternating patterns
|
|
93
|
-
const tagMap = staticComponentCache.get(strings)
|
|
94
|
-
if (tagMap) {
|
|
95
|
-
const cached = tagMap.get(tag)
|
|
96
|
-
if (cached) {
|
|
97
|
-
_hotCache.strings = strings
|
|
98
|
-
_hotCache.tag = tag
|
|
99
|
-
_hotCache.component = cached
|
|
100
|
-
return cached
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Fast check: no values means no dynamic interpolations — avoids .some() scan
|
|
106
|
-
const hasDynamicValues = values.length > 0 && values.some(isDynamic)
|
|
107
|
-
const customFilter = options ? options.shouldForwardProp : undefined
|
|
108
|
-
const insertLayer = options?.layer
|
|
109
|
-
|
|
110
|
-
// STATIC FAST PATH: no function interpolations → compute class once at creation time
|
|
111
|
-
if (!hasDynamicValues) {
|
|
112
|
-
// Inline resolve for the common no-values case
|
|
113
|
-
const raw = values.length === 0 ? (strings[0] as string) : resolve(strings, values, {})
|
|
114
|
-
const cssText = normalizeCSS(raw)
|
|
115
|
-
const hasCss = cssText.length > 0
|
|
116
|
-
|
|
117
|
-
const staticClassName = hasCss ? sheet.insert(cssText, false, insertLayer) : ''
|
|
118
|
-
|
|
119
|
-
// Hoisted out of the render fn: `tag` is known at component-creation time,
|
|
120
|
-
// and `tag` matches `rawProps.as ?? tag` whenever rawProps is empty (the
|
|
121
|
-
// common case for `<MyStyled />` without any props). The DOM-ness check
|
|
122
|
-
// doesn't change between renders for the same `tag`.
|
|
123
|
-
const tagIsDOM = typeof tag === 'string'
|
|
124
|
-
|
|
125
|
-
// Pre-built VNode for the no-extra-props hot path (`<MyStyled />`). Same
|
|
126
|
-
// shape `h(tag, { class })` would produce per render, but allocated once
|
|
127
|
-
// at component-creation time. Mount.ts spreads `vnode.props` into a new
|
|
128
|
-
// object before invoking the component (mount.ts:404-418 doesn't mutate
|
|
129
|
-
// the source vnode), so sharing the same VNode across mount sites is
|
|
130
|
-
// safe. `vnode.children` is empty here because the empty-rawProps branch
|
|
131
|
-
// also implies no children were passed — `rawProps.children` would be
|
|
132
|
-
// `undefined` and the `Array.isArray ? : ?? : []` chain produces `[]`.
|
|
133
|
-
//
|
|
134
|
-
// **Cache lifetime**: this VNode references `staticClassName`, which is
|
|
135
|
-
// the className the sheet just inserted. If `sheet.clearAll()` runs
|
|
136
|
-
// (HMR / dev reload), the className becomes stale BUT the outer
|
|
137
|
-
// `staticComponentCache` (and `_hot*` caches) ALSO survive that path —
|
|
138
|
-
// so consumers continue to receive the stale className regardless. The
|
|
139
|
-
// companion fix to wire `onSheetClear` and reset both caches is tracked
|
|
140
|
-
// separately (see PR #561). This optimization is correct under the
|
|
141
|
-
// existing cache lifetime contract; the HMR-staleness issue is broader
|
|
142
|
-
// than the VNode cache.
|
|
143
|
-
const cachedEmptyVNode = h(
|
|
144
|
-
tag as string,
|
|
145
|
-
staticClassName ? { class: staticClassName } : {},
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
const StaticStyled: ComponentFn = (rawProps: Record<string, any>): VNode | null => {
|
|
149
|
-
// Hot path: no extra props beyond what's empty AND no `ref` / `as`.
|
|
150
|
-
// `for ... in` over an empty object is O(0); the `break` exits on the
|
|
151
|
-
// first key. Skipping the cache when `ref` is present is necessary
|
|
152
|
-
// because the user expects their callback to fire on the mounted DOM
|
|
153
|
-
// node — the pre-built VNode has no `ref` in its props.
|
|
154
|
-
let hasExtraProps = false
|
|
155
|
-
for (const _k in rawProps) {
|
|
156
|
-
hasExtraProps = true
|
|
157
|
-
break
|
|
158
|
-
}
|
|
159
|
-
if (!hasExtraProps && rawProps.ref == null) {
|
|
160
|
-
if (process.env.NODE_ENV !== 'production')
|
|
161
|
-
_countSink.__pyreon_count__?.('styler.staticVNode.hit')
|
|
162
|
-
return cachedEmptyVNode
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const finalTag = rawProps.as || tag
|
|
166
|
-
// Fast `isDOM` when the user didn't pass `as` — reuses the closure-time
|
|
167
|
-
// check. Only `typeof` is needed when `as` overrides the tag.
|
|
168
|
-
const isDOM = finalTag === tag ? tagIsDOM : typeof finalTag === 'string'
|
|
169
|
-
const finalProps = buildProps(rawProps, staticClassName, isDOM, customFilter)
|
|
170
|
-
|
|
171
|
-
return h(
|
|
172
|
-
finalTag as string,
|
|
173
|
-
finalProps,
|
|
174
|
-
...(Array.isArray(rawProps.children)
|
|
175
|
-
? rawProps.children
|
|
176
|
-
: rawProps.children != null
|
|
177
|
-
? [rawProps.children]
|
|
178
|
-
: []),
|
|
179
|
-
)
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
;(StaticStyled as ComponentFn & { displayName?: string }).displayName =
|
|
183
|
-
`styled(${getDisplayName(tag)})`
|
|
184
|
-
|
|
185
|
-
// Store in component cache + hot cache for future reuse
|
|
186
|
-
if (!options && values.length === 0) {
|
|
187
|
-
let tagMap = staticComponentCache.get(strings)
|
|
188
|
-
if (!tagMap) {
|
|
189
|
-
tagMap = new Map()
|
|
190
|
-
staticComponentCache.set(strings, tagMap)
|
|
191
|
-
}
|
|
192
|
-
tagMap.set(tag, StaticStyled)
|
|
193
|
-
_hotCache.strings = strings
|
|
194
|
-
_hotCache.tag = tag
|
|
195
|
-
_hotCache.component = StaticStyled
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return StaticStyled
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// ─── Tier 2: Per-definition class cache ───────────────────────────────────
|
|
202
|
-
// Two-level WeakMap: $rocketstyle → $rocketstate → className.
|
|
203
|
-
// 50 identical Items with the same resolved theme → 1 resolve + 49 hits.
|
|
204
|
-
const classCache = new WeakMap<object, WeakMap<object, string>>()
|
|
205
|
-
// Single-key cache for non-rocketstyle styled components (e.g. Element's
|
|
206
|
-
// Wrapper, which depends on `$element` + `$childFix`). The key is the
|
|
207
|
-
// `$element` object identity; `$childFix` is folded into a `Map<bool,
|
|
208
|
-
// string>` per `$element` to avoid wrong-cache hits when childFix differs.
|
|
209
|
-
// Element-layer interning (see `@pyreon/elements` Element/component.tsx)
|
|
210
|
-
// gives `$element` stable identity across mounts, which is what makes this
|
|
211
|
-
// cache fire — analogous to PR #344's rocketstyle dimension memo.
|
|
212
|
-
const elClassCache = new WeakMap<object, Map<unknown, string>>()
|
|
213
|
-
|
|
214
|
-
// DYNAMIC PATH: uses computed() for reactive class derivation.
|
|
215
|
-
//
|
|
216
|
-
// Architecture:
|
|
217
|
-
// - $rocketstyle/$rocketstate may be function ACCESSORS (from rocketstyle)
|
|
218
|
-
// or plain objects (from direct styled() usage).
|
|
219
|
-
// - When they're accessors, a computed() tracks them so mode/dimension
|
|
220
|
-
// signal changes produce a new CSS class reactively.
|
|
221
|
-
// - The resolve() itself runs UNTRACKED inside the computed to prevent
|
|
222
|
-
// exponential cascade from theme deep-reads in interpolation functions.
|
|
223
|
-
// - The computed memoizes by string equality — same CSS class = no DOM update.
|
|
224
|
-
// - Pyreon's built-in renderEffect handles the DOM class attribute update
|
|
225
|
-
// when the computed value changes.
|
|
226
|
-
//
|
|
227
|
-
// This gives reactive mode/dimension switching WITHOUT per-component effect().
|
|
228
|
-
const DynamicStyled: ComponentFn = (rawProps: Record<string, any>): VNode | null => {
|
|
229
|
-
const themeAccessor = useThemeAccessor()
|
|
230
|
-
const theme = themeAccessor() // snapshot for initial + static path
|
|
231
|
-
const $rs = rawProps.$rocketstyle
|
|
232
|
-
const $rsState = rawProps.$rocketstate
|
|
233
|
-
const isReactiveRS = typeof $rs === 'function'
|
|
234
|
-
const isReactiveState = typeof $rsState === 'function'
|
|
235
|
-
|
|
236
|
-
// Helper: resolve CSS + cache result
|
|
237
|
-
const doResolve = (rs: any, rsState: any, t: any): string => {
|
|
238
|
-
// Tier 2 cache: skip resolve if same object identity seen before
|
|
239
|
-
if (rs && typeof rs === 'object' && rsState && typeof rsState === 'object') {
|
|
240
|
-
const inner = classCache.get(rs)
|
|
241
|
-
if (inner) {
|
|
242
|
-
const cached = inner.get(rsState)
|
|
243
|
-
if (cached !== undefined) return cached
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Element-layer cache (no rocketstyle props, but $element is present
|
|
248
|
-
// and an object). Fires only when the rocketstyle path didn't apply
|
|
249
|
-
// — they're mutually exclusive in practice.
|
|
250
|
-
const $el = rawProps.$element
|
|
251
|
-
const $childFix = rawProps.$childFix
|
|
252
|
-
const useElCache =
|
|
253
|
-
(!rs || typeof rs !== 'object' || !rsState || typeof rsState !== 'object') &&
|
|
254
|
-
$el &&
|
|
255
|
-
typeof $el === 'object'
|
|
256
|
-
if (useElCache) {
|
|
257
|
-
const inner = elClassCache.get($el as object)
|
|
258
|
-
if (inner) {
|
|
259
|
-
const cached = inner.get($childFix)
|
|
260
|
-
if (cached !== undefined) {
|
|
261
|
-
if (process.env.NODE_ENV !== 'production')
|
|
262
|
-
_countSink.__pyreon_count__?.('styler.elClassCache.hit')
|
|
263
|
-
return cached
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const resolveProps = {
|
|
269
|
-
...rawProps,
|
|
270
|
-
...(isReactiveRS ? { $rocketstyle: rs } : {}),
|
|
271
|
-
...(isReactiveState ? { $rocketstate: rsState } : {}),
|
|
272
|
-
theme: t,
|
|
273
|
-
}
|
|
274
|
-
const cssText = normalizeCSS(resolve(strings, values, resolveProps))
|
|
275
|
-
const className = cssText.length > 0 ? sheet.insert(cssText, false, insertLayer) : ''
|
|
276
|
-
|
|
277
|
-
if (rs && typeof rs === 'object' && rsState && typeof rsState === 'object') {
|
|
278
|
-
let inner = classCache.get(rs)
|
|
279
|
-
if (!inner) {
|
|
280
|
-
inner = new WeakMap()
|
|
281
|
-
classCache.set(rs, inner)
|
|
282
|
-
}
|
|
283
|
-
inner.set(rsState, className)
|
|
284
|
-
} else if (useElCache) {
|
|
285
|
-
let inner = elClassCache.get($el as object)
|
|
286
|
-
if (!inner) {
|
|
287
|
-
inner = new Map()
|
|
288
|
-
elClassCache.set($el as object, inner)
|
|
289
|
-
}
|
|
290
|
-
inner.set($childFix, className)
|
|
291
|
-
}
|
|
292
|
-
return className
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// If any axis is reactive, wrap in computed that tracks all three:
|
|
296
|
-
// 1. $rocketstyle accessor (mode + dimension signals)
|
|
297
|
-
// 2. $rocketstate accessor (state descriptor)
|
|
298
|
-
// 3. themeAccessor (user-preference theme swap)
|
|
299
|
-
// The resolve itself runs UNTRACKED to prevent exponential cascade.
|
|
300
|
-
const hasReactive = isReactiveRS || isReactiveState
|
|
301
|
-
const cssClass = hasReactive
|
|
302
|
-
? computed(() => {
|
|
303
|
-
// TRACKED reads:
|
|
304
|
-
const rs = isReactiveRS ? $rs() : $rs
|
|
305
|
-
const rsState = isReactiveState ? $rsState() : $rsState
|
|
306
|
-
const t = themeAccessor() // TRACKED — theme swap
|
|
307
|
-
|
|
308
|
-
// UNTRACKED: resolve + sheet insert
|
|
309
|
-
return runUntracked(() => doResolve(rs, rsState, t))
|
|
310
|
-
}, { equals: (a, b) => a === b })
|
|
311
|
-
: null
|
|
312
|
-
|
|
313
|
-
const finalTag = rawProps.as || tag
|
|
314
|
-
const isDOM = typeof finalTag === 'string'
|
|
315
|
-
|
|
316
|
-
// Initial class: computed (reactive) or direct resolve (static)
|
|
317
|
-
const className = cssClass
|
|
318
|
-
? cssClass()
|
|
319
|
-
: doResolve($rs, $rsState, theme)
|
|
320
|
-
const finalProps = buildProps(rawProps, className, isDOM, customFilter)
|
|
321
|
-
|
|
322
|
-
// Reactive path: lightweight renderEffect that reads the pre-computed
|
|
323
|
-
// class string and toggles classList. This is NOT the old PR #258
|
|
324
|
-
// approach — the expensive resolve() already happened inside the
|
|
325
|
-
// computed. This renderEffect only does: read string → compare → toggle.
|
|
326
|
-
if (cssClass) {
|
|
327
|
-
let el: Element | null = null
|
|
328
|
-
let currentClassName = className
|
|
329
|
-
|
|
330
|
-
const originalRef = finalProps.ref
|
|
331
|
-
finalProps.ref = (node: Element | null) => {
|
|
332
|
-
el = node
|
|
333
|
-
if (originalRef) {
|
|
334
|
-
if (typeof originalRef === 'function') originalRef(node)
|
|
335
|
-
else if (originalRef && typeof originalRef === 'object') (originalRef as any).current = node
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
renderEffect(() => {
|
|
340
|
-
const newClass = cssClass() // reads computed — O(1), just string
|
|
341
|
-
if (el && newClass !== currentClassName) {
|
|
342
|
-
if (currentClassName) el.classList.remove(currentClassName)
|
|
343
|
-
if (newClass) el.classList.add(newClass)
|
|
344
|
-
currentClassName = newClass
|
|
345
|
-
}
|
|
346
|
-
})
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
return h(
|
|
350
|
-
finalTag as string,
|
|
351
|
-
finalProps,
|
|
352
|
-
...(Array.isArray(rawProps.children)
|
|
353
|
-
? rawProps.children
|
|
354
|
-
: rawProps.children != null
|
|
355
|
-
? [rawProps.children]
|
|
356
|
-
: []),
|
|
357
|
-
)
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
;(DynamicStyled as ComponentFn & { displayName?: string }).displayName =
|
|
361
|
-
`styled(${getDisplayName(tag)})`
|
|
362
|
-
return DynamicStyled
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/** Factory function: styled(tag) returns a tagged template function. */
|
|
366
|
-
const styledFactory = (tag: Tag, options?: StyledOptions) => {
|
|
367
|
-
const templateFn = (strings: TemplateStringsArray, ...values: Interpolation[]) =>
|
|
368
|
-
createStyledComponent(tag, strings, values, options)
|
|
369
|
-
|
|
370
|
-
return templateFn
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Main styled export. Supports both calling conventions:
|
|
375
|
-
* - `styled('div')` or `styled(Component)` → returns tagged template function
|
|
376
|
-
* - `styled('div', { shouldForwardProp })` → with custom prop filtering
|
|
377
|
-
* - `styled.div` → shorthand via Proxy (no options)
|
|
378
|
-
*/
|
|
379
|
-
// Cache template functions per tag to avoid closure allocation on every Proxy get
|
|
380
|
-
const proxyCache = new Map<string, (...args: any[]) => any>()
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* Generic tagged template function returned by `styled(tag)` and `styled.tag`.
|
|
384
|
-
*
|
|
385
|
-
* Accepts an optional type parameter `<P>` for consumer-defined props
|
|
386
|
-
* (typically transient $-prefixed props that aren't forwarded to the DOM).
|
|
387
|
-
*
|
|
388
|
-
* @example
|
|
389
|
-
* const Box = styled('div')<{ $color: string }>`
|
|
390
|
-
* background: ${(props) => props.$color};
|
|
391
|
-
* `
|
|
392
|
-
* <Box $color="red" /> // $color is required and typed
|
|
393
|
-
*/
|
|
394
|
-
type TagTemplateFn = <P extends object = Record<string, unknown>>(
|
|
395
|
-
strings: TemplateStringsArray,
|
|
396
|
-
...values: Interpolation<P>[]
|
|
397
|
-
) => ComponentFn<P & Record<string, unknown>>
|
|
398
|
-
|
|
399
|
-
type HtmlTags =
|
|
400
|
-
| 'a'
|
|
401
|
-
| 'abbr'
|
|
402
|
-
| 'address'
|
|
403
|
-
| 'article'
|
|
404
|
-
| 'aside'
|
|
405
|
-
| 'audio'
|
|
406
|
-
| 'b'
|
|
407
|
-
| 'blockquote'
|
|
408
|
-
| 'body'
|
|
409
|
-
| 'br'
|
|
410
|
-
| 'button'
|
|
411
|
-
| 'canvas'
|
|
412
|
-
| 'caption'
|
|
413
|
-
| 'code'
|
|
414
|
-
| 'col'
|
|
415
|
-
| 'colgroup'
|
|
416
|
-
| 'dd'
|
|
417
|
-
| 'details'
|
|
418
|
-
| 'div'
|
|
419
|
-
| 'dl'
|
|
420
|
-
| 'dt'
|
|
421
|
-
| 'em'
|
|
422
|
-
| 'fieldset'
|
|
423
|
-
| 'figcaption'
|
|
424
|
-
| 'figure'
|
|
425
|
-
| 'footer'
|
|
426
|
-
| 'form'
|
|
427
|
-
| 'h1'
|
|
428
|
-
| 'h2'
|
|
429
|
-
| 'h3'
|
|
430
|
-
| 'h4'
|
|
431
|
-
| 'h5'
|
|
432
|
-
| 'h6'
|
|
433
|
-
| 'head'
|
|
434
|
-
| 'header'
|
|
435
|
-
| 'hr'
|
|
436
|
-
| 'html'
|
|
437
|
-
| 'i'
|
|
438
|
-
| 'iframe'
|
|
439
|
-
| 'img'
|
|
440
|
-
| 'input'
|
|
441
|
-
| 'label'
|
|
442
|
-
| 'legend'
|
|
443
|
-
| 'li'
|
|
444
|
-
| 'link'
|
|
445
|
-
| 'main'
|
|
446
|
-
| 'mark'
|
|
447
|
-
| 'menu'
|
|
448
|
-
| 'meta'
|
|
449
|
-
| 'nav'
|
|
450
|
-
| 'ol'
|
|
451
|
-
| 'optgroup'
|
|
452
|
-
| 'option'
|
|
453
|
-
| 'output'
|
|
454
|
-
| 'p'
|
|
455
|
-
| 'picture'
|
|
456
|
-
| 'pre'
|
|
457
|
-
| 'progress'
|
|
458
|
-
| 'q'
|
|
459
|
-
| 'section'
|
|
460
|
-
| 'select'
|
|
461
|
-
| 'small'
|
|
462
|
-
| 'source'
|
|
463
|
-
| 'span'
|
|
464
|
-
| 'strong'
|
|
465
|
-
| 'style'
|
|
466
|
-
| 'sub'
|
|
467
|
-
| 'summary'
|
|
468
|
-
| 'sup'
|
|
469
|
-
| 'svg'
|
|
470
|
-
| 'table'
|
|
471
|
-
| 'tbody'
|
|
472
|
-
| 'td'
|
|
473
|
-
| 'template'
|
|
474
|
-
| 'textarea'
|
|
475
|
-
| 'tfoot'
|
|
476
|
-
| 'th'
|
|
477
|
-
| 'thead'
|
|
478
|
-
| 'time'
|
|
479
|
-
| 'tr'
|
|
480
|
-
| 'u'
|
|
481
|
-
| 'ul'
|
|
482
|
-
| 'video'
|
|
483
|
-
|
|
484
|
-
export type StyledFunction = ((tag: Tag, options?: StyledOptions) => TagTemplateFn) & {
|
|
485
|
-
[K in HtmlTags]: TagTemplateFn
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// Proxy is needed to support styled.div`...` syntax; the cast bridges
|
|
489
|
-
// styledFactory's call signature to StyledFunction which adds HTML tag properties.
|
|
490
|
-
// Proxy target uses `as any` because TS can't resolve Proxy<StyledFunction> with mapped types
|
|
491
|
-
export const styled: StyledFunction = new Proxy(styledFactory as any, {
|
|
492
|
-
get(_target: unknown, prop: string) {
|
|
493
|
-
if (prop === 'prototype' || prop === '$$typeof') return undefined
|
|
494
|
-
// styled.div`...`, styled.span`...`, etc.
|
|
495
|
-
let fn = proxyCache.get(prop)
|
|
496
|
-
if (!fn) {
|
|
497
|
-
fn = (strings: TemplateStringsArray, ...values: Interpolation[]) =>
|
|
498
|
-
createStyledComponent(prop, strings, values)
|
|
499
|
-
proxyCache.set(prop, fn)
|
|
500
|
-
}
|
|
501
|
-
return fn
|
|
502
|
-
},
|
|
503
|
-
})
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
renderApiReferenceEntries,
|
|
3
|
-
renderLlmsFullSection,
|
|
4
|
-
renderLlmsTxtLine,
|
|
5
|
-
} from '@pyreon/manifest'
|
|
6
|
-
import manifest from '../manifest'
|
|
7
|
-
|
|
8
|
-
describe('gen-docs — styler snapshot', () => {
|
|
9
|
-
it('renders a llms.txt bullet starting with the package prefix', () => {
|
|
10
|
-
const line = renderLlmsTxtLine(manifest)
|
|
11
|
-
expect(line.startsWith('- @pyreon/styler —')).toBe(true)
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
it('renders a llms-full.txt section with the right header', () => {
|
|
15
|
-
const section = renderLlmsFullSection(manifest)
|
|
16
|
-
expect(section.startsWith('## @pyreon/styler —')).toBe(true)
|
|
17
|
-
expect(section).toContain('```typescript')
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
it('renders MCP api-reference entries for every api[] item', () => {
|
|
21
|
-
const record = renderApiReferenceEntries(manifest)
|
|
22
|
-
expect(Object.keys(record).sort()).toEqual([
|
|
23
|
-
'styler/StyleSheet',
|
|
24
|
-
'styler/ThemeContext',
|
|
25
|
-
'styler/ThemeProvider',
|
|
26
|
-
'styler/buildProps',
|
|
27
|
-
'styler/clearNormCache',
|
|
28
|
-
'styler/createGlobalStyle',
|
|
29
|
-
'styler/createSheet',
|
|
30
|
-
'styler/css',
|
|
31
|
-
'styler/filterProps',
|
|
32
|
-
'styler/isDynamic',
|
|
33
|
-
'styler/keyframes',
|
|
34
|
-
'styler/normalizeCSS',
|
|
35
|
-
'styler/resolve',
|
|
36
|
-
'styler/resolveValue',
|
|
37
|
-
'styler/sheet',
|
|
38
|
-
'styler/styled',
|
|
39
|
-
'styler/useCSS',
|
|
40
|
-
'styler/useTheme',
|
|
41
|
-
'styler/useThemeAccessor',
|
|
42
|
-
])
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it('carries the CSS-in-JS foot-gun catalog into MCP mistakes for flagship APIs', () => {
|
|
46
|
-
const r = renderApiReferenceEntries(manifest)
|
|
47
|
-
expect(r['styler/styled']?.mistakes).toContain('transient')
|
|
48
|
-
expect(r['styler/css']?.mistakes).toContain('lazy')
|
|
49
|
-
expect(r['styler/useTheme']?.mistakes).toContain('snapshot')
|
|
50
|
-
})
|
|
51
|
-
})
|
package/src/useCSS.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hook that resolves a CSSResult template with props, injects CSS
|
|
3
|
-
* into the shared stylesheet, and returns the className.
|
|
4
|
-
*
|
|
5
|
-
* Use this when you need computed CSS class names on plain elements
|
|
6
|
-
* without the overhead of a styled component layer.
|
|
7
|
-
*/
|
|
8
|
-
import { type CSSResult, normalizeCSS, resolve } from './resolve'
|
|
9
|
-
import { sheet } from './sheet'
|
|
10
|
-
import { useTheme } from './ThemeProvider'
|
|
11
|
-
|
|
12
|
-
export function useCSS(template: CSSResult, props?: Record<string, any>, boost?: boolean): string {
|
|
13
|
-
const theme = useTheme()
|
|
14
|
-
const allProps = theme ? { ...props, theme } : (props ?? {})
|
|
15
|
-
const cssText = normalizeCSS(resolve(template.strings, template.values, allProps))
|
|
16
|
-
|
|
17
|
-
if (!cssText.trim()) return ''
|
|
18
|
-
|
|
19
|
-
return sheet.insert(cssText, boost)
|
|
20
|
-
}
|