@pyreon/styler 0.0.2

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-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,283 @@
1
+ # @pyreon/styler
2
+
3
+ Lightweight CSS-in-JS engine for Pyreon.
4
+
5
+ **3.81 KB** gzipped | **SSR & static export ready** | **TypeScript strict**
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ bun add @pyreon/styler
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```ts
16
+ import { styled, css, ThemeContext } from '@pyreon/styler'
17
+ import { useContext, pushContext, popContext, onUnmount } from '@pyreon/core'
18
+
19
+ const Button = styled('button')`
20
+ display: inline-flex;
21
+ align-items: center;
22
+ padding: 8px 16px;
23
+ border-radius: 4px;
24
+ background: ${({ theme }) => theme.colors.primary};
25
+ color: white;
26
+ cursor: pointer;
27
+
28
+ &:hover {
29
+ opacity: 0.9;
30
+ }
31
+ `
32
+ ```
33
+
34
+ ## API
35
+
36
+ ### `styled(tag)`
37
+
38
+ Creates a styled Pyreon component from an HTML tag or another component.
39
+
40
+ ```ts
41
+ // HTML tag
42
+ const Box = styled('div')`
43
+ display: flex;
44
+ `
45
+
46
+ // Shorthand (via Proxy)
47
+ const Box = styled.div`
48
+ display: flex;
49
+ `
50
+
51
+ // Wrapping a component
52
+ const StyledLink = styled(Link)`
53
+ color: blue;
54
+ text-decoration: none;
55
+ `
56
+ ```
57
+
58
+ #### Dynamic interpolations
59
+
60
+ Function interpolations receive all props plus the current `theme`:
61
+
62
+ ```ts
63
+ const Text = styled('p')`
64
+ color: ${({ theme }) => theme.colors.text};
65
+ font-size: ${(props) => props.$size || '16px'};
66
+ `
67
+ ```
68
+
69
+ #### Polymorphic `as` prop
70
+
71
+ Render as a different element at runtime:
72
+
73
+ ```ts
74
+ const Box = styled('div')`padding: 16px;`
75
+
76
+ // Renders as a <section>
77
+ Box({ as: 'section', children: 'Content' })
78
+ ```
79
+
80
+ #### Transient props
81
+
82
+ Props prefixed with `$` are not forwarded to the DOM:
83
+
84
+ ```ts
85
+ const Box = styled('div')`
86
+ color: ${(p) => p.$active ? 'blue' : 'gray'};
87
+ `
88
+
89
+ // $active is used for styling but won't appear on the <div>
90
+ Box({ $active: true })
91
+ ```
92
+
93
+ #### Custom prop filtering
94
+
95
+ ```ts
96
+ const Box = styled('div', {
97
+ shouldForwardProp: (prop) => prop !== 'size',
98
+ })`
99
+ font-size: ${(p) => p.size}px;
100
+ `
101
+ ```
102
+
103
+ ### `css`
104
+
105
+ Tagged template for composable CSS fragments:
106
+
107
+ ```ts
108
+ const flexCenter = css`
109
+ display: flex;
110
+ align-items: center;
111
+ justify-content: center;
112
+ `
113
+
114
+ const Card = styled('div')`
115
+ ${flexCenter};
116
+ padding: 16px;
117
+ `
118
+ ```
119
+
120
+ Supports conditional patterns:
121
+
122
+ ```ts
123
+ const Box = styled('div')`
124
+ display: flex;
125
+ ${(props) => props.$bordered && css`
126
+ border: 1px solid #e0e0e0;
127
+ border-radius: 4px;
128
+ `};
129
+ `
130
+ ```
131
+
132
+ ### `keyframes`
133
+
134
+ Creates `@keyframes` animations:
135
+
136
+ ```ts
137
+ const fadeIn = keyframes`
138
+ from { opacity: 0; }
139
+ to { opacity: 1; }
140
+ `
141
+
142
+ const FadeBox = styled('div')`
143
+ animation: ${fadeIn} 300ms ease-in;
144
+ `
145
+ ```
146
+
147
+ ### `createGlobalStyle`
148
+
149
+ Injects global CSS rules (not scoped to a class):
150
+
151
+ ```ts
152
+ const GlobalStyle = createGlobalStyle`
153
+ *, *::before, *::after {
154
+ box-sizing: border-box;
155
+ }
156
+
157
+ body {
158
+ margin: 0;
159
+ font-family: ${({ theme }) => theme.font};
160
+ }
161
+ `
162
+ ```
163
+
164
+ ### `ThemeContext` & `useTheme`
165
+
166
+ Provides a theme object to all nested styled components via Pyreon's context system:
167
+
168
+ ```ts
169
+ import { ThemeContext, useTheme } from '@pyreon/styler'
170
+ import { pushContext, onUnmount, popContext } from '@pyreon/core'
171
+
172
+ // Provide theme
173
+ pushContext(new Map([[ThemeContext.id, myTheme]]))
174
+ onUnmount(() => popContext())
175
+ ```
176
+
177
+ Access the theme from any component:
178
+
179
+ ```ts
180
+ const MyComponent = () => {
181
+ const theme = useTheme()
182
+ // use theme values
183
+ }
184
+ ```
185
+
186
+ #### TypeScript theme augmentation
187
+
188
+ Extend `DefaultTheme` for strict typing across your app:
189
+
190
+ ```ts
191
+ declare module '@pyreon/styler' {
192
+ interface DefaultTheme {
193
+ colors: { primary: string; text: string }
194
+ spacing: (n: number) => string
195
+ }
196
+ }
197
+ ```
198
+
199
+ ### `sheet` & `createSheet`
200
+
201
+ The singleton `sheet` manages CSS rule injection. For SSR, use `createSheet` for per-request isolation:
202
+
203
+ ```ts
204
+ import { createSheet } from '@pyreon/styler'
205
+
206
+ const sheet = createSheet()
207
+ const html = renderToString(App({}))
208
+ const styleTags = sheet.getStyleTag()
209
+ sheet.reset()
210
+ ```
211
+
212
+ #### `@layer` support
213
+
214
+ Wrap all scoped rules in a CSS Cascade Layer:
215
+
216
+ ```ts
217
+ const sheet = createSheet({ layer: 'components' })
218
+ ```
219
+
220
+ ## How It Works
221
+
222
+ ### Static path (zero runtime cost)
223
+
224
+ Templates with no function interpolations are resolved **once at component creation time**. The CSS class, rules, and `<style>` element are pre-computed and cached.
225
+
226
+ ### Dynamic path
227
+
228
+ Templates with function interpolations resolve on every render. A cache skips `sheet.prepare()` and `<style>` element creation when the resolved CSS text hasn't changed.
229
+
230
+ ### CSS Nesting
231
+
232
+ 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.
233
+
234
+ ```ts
235
+ const Card = styled('div')`
236
+ padding: 16px;
237
+
238
+ &:hover {
239
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
240
+ }
241
+
242
+ & > h2 {
243
+ margin: 0 0 8px;
244
+ }
245
+
246
+ @media (min-width: 768px) {
247
+ padding: 24px;
248
+ }
249
+ `
250
+ ```
251
+
252
+ ## Benchmarks
253
+
254
+ ### Bundle Size
255
+
256
+ | Library | Minified | Gzipped |
257
+ |---------|--------:|--------:|
258
+ | goober | 2.32 KB | 1.31 KB |
259
+ | **@pyreon/styler** | **10.13 KB** | **3.81 KB** |
260
+ | styled-components | 44.93 KB | 17.89 KB |
261
+ | @emotion/react + styled | 48.26 KB | 16.59 KB |
262
+
263
+ ### Performance (ops/sec, higher is better)
264
+
265
+ | Benchmark | styler | styled-components | @emotion | goober |
266
+ |-----------|-------:|-------------------:|---------:|-------:|
267
+ | css() creation | **25.2M** | 9.0M | 2.2M | 26K |
268
+ | css() with interpolations | **24.9M** | 5.6M | 2.3M | 28K |
269
+ | Template resolution | **21.4M** | 3.9M | — | — |
270
+ | Nested composition | **8.3M** | 2.2M | 1.4M | 8K |
271
+ | SSR renderToString | **307K** | 69K | 192K | 18K |
272
+ | styled() factory | **17.3M** | 109K | 933K | 18.2M |
273
+
274
+ ## Peer Dependencies
275
+
276
+ | Package | Version |
277
+ | ------- | ------- |
278
+ | @pyreon/core | >= 0.0.1 |
279
+ | @pyreon/reactivity | >= 0.0.1 |
280
+
281
+ ## License
282
+
283
+ MIT
package/lib/index.d.ts ADDED
@@ -0,0 +1,212 @@
1
+ import * as _pyreon_core0 from "@pyreon/core";
2
+ import { ComponentFn, VNode, VNodeChild } from "@pyreon/core";
3
+
4
+ //#region src/ThemeProvider.d.ts
5
+ interface DefaultTheme {}
6
+ type Theme = DefaultTheme & Record<string, unknown>;
7
+ declare const ThemeContext: _pyreon_core0.Context<Theme>;
8
+ /** Hook to read the current theme from the nearest ThemeProvider. */
9
+ declare const useTheme: <T extends object = Theme>() => T;
10
+ /** Provides a theme object to all nested styled components via Pyreon context. */
11
+ declare function ThemeProvider({
12
+ theme,
13
+ children
14
+ }: {
15
+ theme: Theme;
16
+ children?: VNodeChild;
17
+ }): VNode | null;
18
+ //#endregion
19
+ //#region src/resolve.d.ts
20
+ type Interpolation = string | number | boolean | null | undefined | CSSResult | Interpolation[] | ((props: {
21
+ theme?: DefaultTheme & Record<string, any>;
22
+ [key: string]: any;
23
+ }) => Interpolation);
24
+ /**
25
+ * Lazy representation of a `css` tagged template. Stores the raw template
26
+ * strings and interpolation values without resolving them. Resolution is
27
+ * deferred until a styled component renders (or until explicitly resolved).
28
+ */
29
+ declare class CSSResult {
30
+ readonly strings: TemplateStringsArray;
31
+ readonly values: Interpolation[];
32
+ constructor(strings: TemplateStringsArray, values: Interpolation[]);
33
+ /** Resolve with empty props — useful for static templates, testing, and debugging. */
34
+ toString(): string;
35
+ }
36
+ /** Resolve a tagged template's strings + values into a final CSS string. */
37
+ declare const resolve: (strings: TemplateStringsArray, values: Interpolation[], props: Record<string, any>) => string;
38
+ /** Clear the normalizeCSS cache (called during HMR cleanup). */
39
+ declare const clearNormCache: () => void;
40
+ declare const normalizeCSS: (css: string) => string;
41
+ declare const resolveValue: (value: Interpolation, props: Record<string, any>) => string;
42
+ //#endregion
43
+ //#region src/css.d.ts
44
+ /**
45
+ * Tagged template function for CSS. Captures the template strings and
46
+ * interpolation values as a lazy CSSResult — resolution is deferred
47
+ * until a styled component renders.
48
+ *
49
+ * Works as both a tagged template (`css\`...\``) and a regular function
50
+ * call (`css(...args)`) since tagged templates are syntactic sugar for
51
+ * function calls with (TemplateStringsArray, ...values).
52
+ */
53
+ declare const css: (strings: TemplateStringsArray, ...values: Interpolation[]) => CSSResult;
54
+ //#endregion
55
+ //#region src/forward.d.ts
56
+ /**
57
+ * HTML prop filtering. Prevents unknown props from being forwarded to DOM
58
+ * elements (which causes warnings). Props starting with `$` are
59
+ * transient (styling-only) and are always filtered out.
60
+ */
61
+ /**
62
+ * Filters props for HTML elements. Keeps valid HTML attrs, data-*, aria-*.
63
+ * Rejects unknown props and $-prefixed transient props.
64
+ */
65
+ declare const filterProps: (props: Record<string, unknown>) => Record<string, unknown>;
66
+ /**
67
+ * Build final props for a styled component in a single pass.
68
+ * Combines className merging, ref injection, and prop filtering into one
69
+ * allocation and one iteration.
70
+ */
71
+ declare const buildProps: (rawProps: Record<string, any>, generatedCls: string, isDOM: boolean, customFilter?: (prop: string) => boolean) => Record<string, any>;
72
+ //#endregion
73
+ //#region src/globalStyle.d.ts
74
+ declare const createGlobalStyle: (strings: TemplateStringsArray, ...values: Interpolation[]) => ComponentFn;
75
+ //#endregion
76
+ //#region src/hash.d.ts
77
+ /**
78
+ * Fast FNV-1a non-cryptographic hash. Returns base-36 string for compact class names.
79
+ *
80
+ * 32-bit hash space → ~4.3 billion unique values. Collision probability is
81
+ * negligible for typical applications (< 10,000 unique CSS rules).
82
+ */
83
+ /** FNV-1a offset basis — starting state for streaming hash. */
84
+ declare const HASH_INIT = 2166136261;
85
+ /**
86
+ * Feed a string segment into the running hash state.
87
+ * Streaming: hashUpdate(hashUpdate(HASH_INIT, 'ab'), 'cd') === hash('abcd').
88
+ */
89
+ declare const hashUpdate: (init: number, str: string) => number;
90
+ /** Finalize a hash state into a base-36 class name suffix. */
91
+ declare const hashFinalize: (h: number) => string;
92
+ /** Hash a complete string in one shot. Returns base-36 string. */
93
+ declare const hash: (str: string) => string;
94
+ //#endregion
95
+ //#region src/keyframes.d.ts
96
+ declare class KeyframesResult {
97
+ readonly name: string;
98
+ constructor(strings: TemplateStringsArray, values: Interpolation[]);
99
+ /** Returns the animation name when used in string context. */
100
+ toString(): string;
101
+ }
102
+ declare const keyframes: (strings: TemplateStringsArray, ...values: Interpolation[]) => KeyframesResult;
103
+ //#endregion
104
+ //#region src/shared.d.ts
105
+ /** Check if an interpolation value is dynamic (contains functions or nested dynamic CSSResults). */
106
+ declare const isDynamic: (v: Interpolation) => boolean;
107
+ //#endregion
108
+ //#region src/sheet.d.ts
109
+ interface StyleSheetOptions {
110
+ /** Maximum number of cached rules before eviction (default: 10000). */
111
+ maxCacheSize?: number;
112
+ /** CSS @layer name to wrap scoped rules in. */
113
+ layer?: string;
114
+ }
115
+ declare class StyleSheet {
116
+ private cache;
117
+ private insertCache;
118
+ private sheet;
119
+ private ssrBuffer;
120
+ private isSSR;
121
+ private maxCacheSize;
122
+ private layer;
123
+ constructor(options?: StyleSheetOptions);
124
+ private mount;
125
+ /** Extract className from a selector like ".pyr-abc" or ".pyr-abc.pyr-abc" → "pyr-abc" */
126
+ private extractClassName;
127
+ /** Parse existing rules from SSR-rendered <style> tag into cache. */
128
+ private hydrateFromTag;
129
+ /** Evict oldest entries when cache exceeds max size. */
130
+ private evictIfNeeded;
131
+ /**
132
+ * Extract nested at-rules (@media, @supports, @container) from CSS text
133
+ * and wrap their content in the given selector as separate top-level rules.
134
+ */
135
+ private splitAtRules;
136
+ /**
137
+ * Compute a className from CSS text without injecting (pure function).
138
+ */
139
+ getClassName(cssText: string): string;
140
+ /**
141
+ * Insert CSS rules for a component. Returns the class name (deterministic, hash-based).
142
+ * Deduplicates: same CSS text always produces the same class name and
143
+ * the rules are only injected once.
144
+ *
145
+ * When `boost` is true, the selector is doubled (`.pyr-abc.pyr-abc`)
146
+ * to raise specificity from (0,1,0) to (0,2,0).
147
+ */
148
+ insert(cssText: string, boost?: boolean): string;
149
+ /** Insert a @keyframes rule. Deduplicates by animation name. */
150
+ insertKeyframes(name: string, body: string): void;
151
+ /**
152
+ * Split CSS text into individual top-level rules.
153
+ * CSSStyleSheet.insertRule() only accepts one rule at a time.
154
+ */
155
+ private splitRules;
156
+ /** Insert global CSS rules (no wrapper selector). Deduplicates by hash. */
157
+ insertGlobal(cssText: string): void;
158
+ /** Returns collected CSS for SSR as a complete `<style>` tag string. */
159
+ getStyleTag(): string;
160
+ /** Returns collected CSS rules as a raw string (useful for streaming SSR). */
161
+ getStyles(): string;
162
+ /** Reset SSR buffer and cache (call between server requests). */
163
+ reset(): void;
164
+ /** Clear the dedup cache. Useful for HMR / dev-time reloads. */
165
+ clearCache(): void;
166
+ /**
167
+ * Full cleanup: clear cache and remove all CSS rules from the DOM.
168
+ * Intended for HMR / dev-time reloads where stale styles must be purged.
169
+ */
170
+ clearAll(): void;
171
+ /**
172
+ * Compute className and full CSS rule text without injecting.
173
+ */
174
+ prepare(cssText: string, boost?: boolean): {
175
+ className: string;
176
+ rules: string;
177
+ };
178
+ /** Check if a className is already in the cache. O(1) Map lookup. */
179
+ has(className: string): boolean;
180
+ /** Current number of cached rules. */
181
+ get cacheSize(): number;
182
+ }
183
+ /** Default singleton sheet for client-side use. */
184
+ declare const sheet: StyleSheet;
185
+ /**
186
+ * Factory for creating isolated StyleSheet instances.
187
+ * Use in SSR to get per-request isolation.
188
+ */
189
+ declare const createSheet: (options?: StyleSheetOptions) => StyleSheet;
190
+ //#endregion
191
+ //#region src/styled.d.ts
192
+ type Tag = string | ComponentFn<any>;
193
+ interface StyledOptions {
194
+ /** Custom prop filter. Return true to forward the prop to the DOM element. */
195
+ shouldForwardProp?: (prop: string) => boolean;
196
+ /**
197
+ * Double the class selector to raise specificity from (0,1,0) to (0,2,0).
198
+ * Ensures this component's styles override inner library components
199
+ * regardless of CSS source order.
200
+ */
201
+ boost?: boolean;
202
+ }
203
+ type TagTemplateFn = (strings: TemplateStringsArray, ...values: Interpolation[]) => ComponentFn;
204
+ type HtmlTags = "a" | "abbr" | "address" | "article" | "aside" | "audio" | "b" | "blockquote" | "body" | "br" | "button" | "canvas" | "caption" | "code" | "col" | "colgroup" | "dd" | "details" | "div" | "dl" | "dt" | "em" | "fieldset" | "figcaption" | "figure" | "footer" | "form" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "head" | "header" | "hr" | "html" | "i" | "iframe" | "img" | "input" | "label" | "legend" | "li" | "link" | "main" | "mark" | "menu" | "meta" | "nav" | "ol" | "optgroup" | "option" | "output" | "p" | "picture" | "pre" | "progress" | "q" | "section" | "select" | "small" | "source" | "span" | "strong" | "style" | "sub" | "summary" | "sup" | "svg" | "table" | "tbody" | "td" | "template" | "textarea" | "tfoot" | "th" | "thead" | "time" | "tr" | "u" | "ul" | "video";
205
+ type StyledFunction = ((tag: Tag, options?: StyledOptions) => TagTemplateFn) & { [K in HtmlTags]: TagTemplateFn };
206
+ declare const styled: StyledFunction;
207
+ //#endregion
208
+ //#region src/useCSS.d.ts
209
+ declare function useCSS(template: CSSResult, props?: Record<string, any>, boost?: boolean): string;
210
+ //#endregion
211
+ export { type CSSResult, type DefaultTheme, HASH_INIT, type Interpolation, StyleSheet, type StyleSheetOptions, type StyledFunction, type StyledOptions, ThemeContext, ThemeProvider, buildProps, clearNormCache, createGlobalStyle, createSheet, css, filterProps, hash, hashFinalize, hashUpdate, isDynamic, keyframes, normalizeCSS, resolve, resolveValue, sheet, styled, useCSS, useTheme };
212
+ //# sourceMappingURL=index2.d.ts.map
package/lib/index.js ADDED
@@ -0,0 +1,850 @@
1
+ import { createContext, h, onUnmount, popContext, pushContext, useContext } from "@pyreon/core";
2
+
3
+ //#region src/resolve.ts
4
+ /**
5
+ * Lazy representation of a `css` tagged template. Stores the raw template
6
+ * strings and interpolation values without resolving them. Resolution is
7
+ * deferred until a styled component renders (or until explicitly resolved).
8
+ */
9
+ var CSSResult = class {
10
+ constructor(strings, values) {
11
+ this.strings = strings;
12
+ this.values = values;
13
+ }
14
+ /** Resolve with empty props — useful for static templates, testing, and debugging. */
15
+ toString() {
16
+ return resolve(this.strings, this.values, {});
17
+ }
18
+ };
19
+ /** Resolve a tagged template's strings + values into a final CSS string. */
20
+ const resolve = (strings, values, props) => {
21
+ let result = strings[0];
22
+ for (let i = 0; i < values.length; i++) {
23
+ const v = values[i];
24
+ const s = strings[i + 1];
25
+ if (typeof v === "function") {
26
+ const r = v(props);
27
+ result += (typeof r === "string" ? r : r == null || r === false || r === true ? "" : resolveValue(r, props)) + s;
28
+ } else if (v == null || v === false || v === true) result += s;
29
+ else if (typeof v === "string") result += v + s;
30
+ else if (typeof v === "number") result += v + s;
31
+ else result += resolveValue(v, props) + s;
32
+ }
33
+ return result;
34
+ };
35
+ /**
36
+ * Normalize resolved CSS text for strict `insertRule` compatibility.
37
+ *
38
+ * Single-pass scanner that handles all cleanup in one traversal:
39
+ * - Strips block comments and line comments (preserves :// in URLs)
40
+ * - Collapses whitespace to single spaces
41
+ * - Removes redundant semicolons
42
+ * - Trims leading/trailing whitespace
43
+ */
44
+ const normCache = /* @__PURE__ */ new Map();
45
+ /** Clear the normalizeCSS cache (called during HMR cleanup). */
46
+ const clearNormCache = () => normCache.clear();
47
+ const normalizeCSS = (css) => {
48
+ const cached = normCache.get(css);
49
+ if (cached !== void 0) return cached;
50
+ const len = css.length;
51
+ let out = "";
52
+ let space = false;
53
+ let last = 0;
54
+ for (let i = 0; i < len; i++) {
55
+ const c = css.charCodeAt(i);
56
+ if (c === 47 && css.charCodeAt(i + 1) === 42) {
57
+ const end = css.indexOf("*/", i + 2);
58
+ i = end === -1 ? len : end + 1;
59
+ space = true;
60
+ continue;
61
+ }
62
+ if (c === 47 && css.charCodeAt(i + 1) === 47 && last !== 58) {
63
+ const nl = css.indexOf("\n", i + 2);
64
+ i = nl === -1 ? len : nl;
65
+ space = true;
66
+ continue;
67
+ }
68
+ if (c === 32 || c === 9 || c === 10 || c === 13 || c === 12) {
69
+ space = true;
70
+ continue;
71
+ }
72
+ if (c === 59) {
73
+ if (last === 0 || last === 123 || last === 125 || last === 59) continue;
74
+ space = false;
75
+ out += ";";
76
+ last = 59;
77
+ continue;
78
+ }
79
+ if (space && last !== 0) out += " ";
80
+ space = false;
81
+ out += css[i];
82
+ last = c;
83
+ }
84
+ if (normCache.size > 2e3) {
85
+ let count = 0;
86
+ for (const key of normCache.keys()) {
87
+ if (count >= 200) break;
88
+ normCache.delete(key);
89
+ count++;
90
+ }
91
+ }
92
+ normCache.set(css, out);
93
+ return out;
94
+ };
95
+ const resolveValue = (value, props) => {
96
+ if (value == null || value === false || value === true) return "";
97
+ if (typeof value === "function") return resolveValue(value(props), props);
98
+ if (value instanceof CSSResult) return resolve(value.strings, value.values, props);
99
+ if (Array.isArray(value)) {
100
+ let arrayResult = "";
101
+ for (let i = 0; i < value.length; i++) arrayResult += resolveValue(value[i], props);
102
+ return arrayResult;
103
+ }
104
+ return String(value);
105
+ };
106
+
107
+ //#endregion
108
+ //#region src/css.ts
109
+ /**
110
+ * Tagged template function for CSS. Captures the template strings and
111
+ * interpolation values as a lazy CSSResult — resolution is deferred
112
+ * until a styled component renders.
113
+ *
114
+ * Works as both a tagged template (`css\`...\``) and a regular function
115
+ * call (`css(...args)`) since tagged templates are syntactic sugar for
116
+ * function calls with (TemplateStringsArray, ...values).
117
+ */
118
+ const css = (strings, ...values) => new CSSResult(strings, values);
119
+
120
+ //#endregion
121
+ //#region src/forward.ts
122
+ /**
123
+ * HTML prop filtering. Prevents unknown props from being forwarded to DOM
124
+ * elements (which causes warnings). Props starting with `$` are
125
+ * transient (styling-only) and are always filtered out.
126
+ */
127
+ const HTML_PROPS = new Set([
128
+ "children",
129
+ "className",
130
+ "class",
131
+ "dangerouslySetInnerHTML",
132
+ "htmlFor",
133
+ "id",
134
+ "key",
135
+ "ref",
136
+ "style",
137
+ "tabIndex",
138
+ "role",
139
+ "onAbort",
140
+ "onAnimationEnd",
141
+ "onAnimationIteration",
142
+ "onAnimationStart",
143
+ "onBlur",
144
+ "onChange",
145
+ "onClick",
146
+ "onCompositionEnd",
147
+ "onCompositionStart",
148
+ "onCompositionUpdate",
149
+ "onContextMenu",
150
+ "onCopy",
151
+ "onCut",
152
+ "onDoubleClick",
153
+ "onDrag",
154
+ "onDragEnd",
155
+ "onDragEnter",
156
+ "onDragLeave",
157
+ "onDragOver",
158
+ "onDragStart",
159
+ "onDrop",
160
+ "onError",
161
+ "onFocus",
162
+ "onInput",
163
+ "onKeyDown",
164
+ "onKeyPress",
165
+ "onKeyUp",
166
+ "onLoad",
167
+ "onMouseDown",
168
+ "onMouseEnter",
169
+ "onMouseLeave",
170
+ "onMouseMove",
171
+ "onMouseOut",
172
+ "onMouseOver",
173
+ "onMouseUp",
174
+ "onPaste",
175
+ "onPointerCancel",
176
+ "onPointerDown",
177
+ "onPointerEnter",
178
+ "onPointerLeave",
179
+ "onPointerMove",
180
+ "onPointerOut",
181
+ "onPointerOver",
182
+ "onPointerUp",
183
+ "onScroll",
184
+ "onSelect",
185
+ "onSubmit",
186
+ "onTouchCancel",
187
+ "onTouchEnd",
188
+ "onTouchMove",
189
+ "onTouchStart",
190
+ "onTransitionEnd",
191
+ "onWheel",
192
+ "accept",
193
+ "acceptCharset",
194
+ "accessKey",
195
+ "action",
196
+ "allow",
197
+ "allowFullScreen",
198
+ "alt",
199
+ "as",
200
+ "async",
201
+ "autoCapitalize",
202
+ "autoComplete",
203
+ "autoCorrect",
204
+ "autoFocus",
205
+ "autoPlay",
206
+ "capture",
207
+ "cellPadding",
208
+ "cellSpacing",
209
+ "charSet",
210
+ "checked",
211
+ "cite",
212
+ "cols",
213
+ "colSpan",
214
+ "content",
215
+ "contentEditable",
216
+ "controls",
217
+ "controlsList",
218
+ "coords",
219
+ "crossOrigin",
220
+ "dateTime",
221
+ "decoding",
222
+ "default",
223
+ "defaultChecked",
224
+ "defaultValue",
225
+ "defer",
226
+ "dir",
227
+ "disabled",
228
+ "disablePictureInPicture",
229
+ "disableRemotePlayback",
230
+ "download",
231
+ "draggable",
232
+ "encType",
233
+ "enterKeyHint",
234
+ "fetchPriority",
235
+ "form",
236
+ "formAction",
237
+ "formEncType",
238
+ "formMethod",
239
+ "formNoValidate",
240
+ "formTarget",
241
+ "frameBorder",
242
+ "headers",
243
+ "height",
244
+ "hidden",
245
+ "high",
246
+ "href",
247
+ "hrefLang",
248
+ "httpEquiv",
249
+ "inputMode",
250
+ "integrity",
251
+ "is",
252
+ "label",
253
+ "lang",
254
+ "list",
255
+ "loading",
256
+ "loop",
257
+ "low",
258
+ "max",
259
+ "maxLength",
260
+ "media",
261
+ "method",
262
+ "min",
263
+ "minLength",
264
+ "multiple",
265
+ "muted",
266
+ "name",
267
+ "noModule",
268
+ "noValidate",
269
+ "nonce",
270
+ "open",
271
+ "optimum",
272
+ "pattern",
273
+ "placeholder",
274
+ "playsInline",
275
+ "poster",
276
+ "preload",
277
+ "readOnly",
278
+ "referrerPolicy",
279
+ "rel",
280
+ "required",
281
+ "reversed",
282
+ "rows",
283
+ "rowSpan",
284
+ "sandbox",
285
+ "scope",
286
+ "scoped",
287
+ "scrolling",
288
+ "selected",
289
+ "shape",
290
+ "size",
291
+ "sizes",
292
+ "slot",
293
+ "span",
294
+ "spellCheck",
295
+ "src",
296
+ "srcDoc",
297
+ "srcLang",
298
+ "srcSet",
299
+ "start",
300
+ "step",
301
+ "summary",
302
+ "target",
303
+ "title",
304
+ "translate",
305
+ "type",
306
+ "useMap",
307
+ "value",
308
+ "width",
309
+ "wrap"
310
+ ]);
311
+ /**
312
+ * Filters props for HTML elements. Keeps valid HTML attrs, data-*, aria-*.
313
+ * Rejects unknown props and $-prefixed transient props.
314
+ */
315
+ const filterProps = (props) => {
316
+ const filtered = {};
317
+ for (const key in props) {
318
+ if (key.charCodeAt(0) === 36) continue;
319
+ if (key === "as") continue;
320
+ if (key.startsWith("data-") || key.startsWith("aria-")) {
321
+ filtered[key] = props[key];
322
+ continue;
323
+ }
324
+ if (HTML_PROPS.has(key)) filtered[key] = props[key];
325
+ }
326
+ return filtered;
327
+ };
328
+ /**
329
+ * Build final props for a styled component in a single pass.
330
+ * Combines className merging, ref injection, and prop filtering into one
331
+ * allocation and one iteration.
332
+ */
333
+ const buildProps = (rawProps, generatedCls, isDOM, customFilter) => {
334
+ const result = {};
335
+ const userCls = rawProps.class || rawProps.className;
336
+ if (generatedCls) result.class = userCls ? `${generatedCls} ${userCls}` : generatedCls;
337
+ else if (userCls) result.class = userCls;
338
+ if (!isDOM) {
339
+ for (const key in rawProps) {
340
+ if (key === "as" || key === "className" || key === "class") continue;
341
+ if (key.charCodeAt(0) === 36) continue;
342
+ result[key] = rawProps[key];
343
+ }
344
+ return result;
345
+ }
346
+ if (customFilter) {
347
+ for (const key in rawProps) {
348
+ if (key === "as" || key === "className" || key === "class") continue;
349
+ if (customFilter(key)) result[key] = rawProps[key];
350
+ }
351
+ return result;
352
+ }
353
+ for (const key in rawProps) {
354
+ if (key === "as" || key === "className" || key === "class") continue;
355
+ if (key.charCodeAt(0) === 36) continue;
356
+ if (key.startsWith("data-") || key.startsWith("aria-")) {
357
+ result[key] = rawProps[key];
358
+ continue;
359
+ }
360
+ if (HTML_PROPS.has(key)) result[key] = rawProps[key];
361
+ }
362
+ return result;
363
+ };
364
+
365
+ //#endregion
366
+ //#region src/shared.ts
367
+ /**
368
+ * Shared utilities used across multiple modules.
369
+ */
370
+ /** Check if an interpolation value is dynamic (contains functions or nested dynamic CSSResults). */
371
+ const isDynamic = (v) => {
372
+ if (typeof v === "function") return true;
373
+ if (Array.isArray(v)) return v.some(isDynamic);
374
+ if (v instanceof CSSResult) return v.values.some(isDynamic);
375
+ return false;
376
+ };
377
+
378
+ //#endregion
379
+ //#region src/hash.ts
380
+ /**
381
+ * Fast FNV-1a non-cryptographic hash. Returns base-36 string for compact class names.
382
+ *
383
+ * 32-bit hash space → ~4.3 billion unique values. Collision probability is
384
+ * negligible for typical applications (< 10,000 unique CSS rules).
385
+ */
386
+ /** FNV-1a offset basis — starting state for streaming hash. */
387
+ const HASH_INIT = 2166136261;
388
+ const FNV_PRIME = 16777619;
389
+ /**
390
+ * Feed a string segment into the running hash state.
391
+ * Streaming: hashUpdate(hashUpdate(HASH_INIT, 'ab'), 'cd') === hash('abcd').
392
+ */
393
+ const hashUpdate = (init, str) => {
394
+ let h = init;
395
+ for (let i = 0; i < str.length; i++) {
396
+ h ^= str.charCodeAt(i);
397
+ h = Math.imul(h, FNV_PRIME);
398
+ }
399
+ return h;
400
+ };
401
+ /** Finalize a hash state into a base-36 class name suffix. */
402
+ const hashFinalize = (h) => (h >>> 0).toString(36);
403
+ /** Hash a complete string in one shot. Returns base-36 string. */
404
+ const hash = (str) => hashFinalize(hashUpdate(HASH_INIT, str));
405
+
406
+ //#endregion
407
+ //#region src/sheet.ts
408
+ /**
409
+ * StyleSheet manager. Handles CSS rule injection, hash-based deduplication,
410
+ * SSR buffering, client-side hydration, bounded cache, and @layer support.
411
+ *
412
+ * Media queries (@media), @supports, and @container blocks nested inside
413
+ * component CSS are automatically extracted into separate top-level rules.
414
+ */
415
+ const PREFIX = "pyr";
416
+ const ATTR = "data-pyreon-styler";
417
+ const DEFAULT_MAX_CACHE_SIZE = 1e4;
418
+ var StyleSheet = class {
419
+ cache = /* @__PURE__ */ new Map();
420
+ insertCache = /* @__PURE__ */ new Map();
421
+ sheet = null;
422
+ ssrBuffer = [];
423
+ isSSR;
424
+ maxCacheSize;
425
+ layer;
426
+ constructor(options = {}) {
427
+ this.maxCacheSize = options.maxCacheSize ?? DEFAULT_MAX_CACHE_SIZE;
428
+ this.layer = options.layer;
429
+ this.isSSR = typeof document === "undefined";
430
+ if (!this.isSSR) this.mount();
431
+ }
432
+ mount() {
433
+ const existing = document.querySelector(`style[${ATTR}]`);
434
+ if (existing) {
435
+ this.sheet = existing.sheet ?? null;
436
+ this.hydrateFromTag(existing);
437
+ } else {
438
+ const el = document.createElement("style");
439
+ el.setAttribute(ATTR, "");
440
+ document.head.appendChild(el);
441
+ this.sheet = el.sheet ?? null;
442
+ }
443
+ if (this.layer && this.sheet) try {
444
+ this.sheet.insertRule(`@layer ${this.layer};`, 0);
445
+ } catch {}
446
+ }
447
+ /** Extract className from a selector like ".pyr-abc" or ".pyr-abc.pyr-abc" → "pyr-abc" */
448
+ extractClassName(selectorText) {
449
+ if (selectorText[0] !== ".") return null;
450
+ const dotIdx = selectorText.indexOf(".", 1);
451
+ return dotIdx > 0 ? selectorText.slice(1, dotIdx) : selectorText.slice(1);
452
+ }
453
+ /** Parse existing rules from SSR-rendered <style> tag into cache. */
454
+ hydrateFromTag(el) {
455
+ const sheet = el.sheet;
456
+ if (!sheet) return;
457
+ for (let i = 0; i < sheet.cssRules.length; i++) {
458
+ const rule = sheet.cssRules[i];
459
+ if (rule instanceof CSSStyleRule) {
460
+ const className = this.extractClassName(rule.selectorText);
461
+ if (className) this.cache.set(className, className);
462
+ }
463
+ if (typeof CSSMediaRule !== "undefined" && rule instanceof CSSMediaRule) for (let j = 0; j < rule.cssRules.length; j++) {
464
+ const inner = rule.cssRules[j];
465
+ if (inner instanceof CSSStyleRule) {
466
+ const className = this.extractClassName(inner.selectorText);
467
+ if (className) this.cache.set(className, className);
468
+ }
469
+ }
470
+ }
471
+ }
472
+ /** Evict oldest entries when cache exceeds max size. */
473
+ evictIfNeeded() {
474
+ if (this.cache.size <= this.maxCacheSize) return;
475
+ const toDelete = Math.floor(this.maxCacheSize * .1);
476
+ let count = 0;
477
+ for (const key of this.cache.keys()) {
478
+ if (count >= toDelete) break;
479
+ this.cache.delete(key);
480
+ count++;
481
+ }
482
+ }
483
+ /**
484
+ * Extract nested at-rules (@media, @supports, @container) from CSS text
485
+ * and wrap their content in the given selector as separate top-level rules.
486
+ */
487
+ splitAtRules(cssText, selector) {
488
+ if (cssText.indexOf("@") === -1) return {
489
+ base: cssText,
490
+ atRules: []
491
+ };
492
+ const atRules = [];
493
+ const baseParts = [];
494
+ let depth = 0;
495
+ let atStart = -1;
496
+ let lastBase = 0;
497
+ for (let i = 0; i < cssText.length; i++) {
498
+ const ch = cssText[i];
499
+ if (ch === "{") depth++;
500
+ else if (ch === "}") {
501
+ depth--;
502
+ if (depth === 0 && atStart >= 0) {
503
+ const openBrace = cssText.indexOf("{", atStart);
504
+ const atPrefix = cssText.slice(atStart, openBrace).trim();
505
+ const innerCSS = cssText.slice(openBrace + 1, i).trim();
506
+ if (innerCSS) atRules.push(`${atPrefix}{${selector}{${innerCSS}}}`);
507
+ atStart = -1;
508
+ lastBase = i + 1;
509
+ }
510
+ } else if (depth === 0 && ch === "@" && atStart < 0) {
511
+ const remaining = cssText.slice(i, i + 20);
512
+ if (/^@(?:media|supports|container)\b/.test(remaining)) {
513
+ const baseBefore = cssText.slice(lastBase, i).trim();
514
+ if (baseBefore) baseParts.push(baseBefore);
515
+ atStart = i;
516
+ }
517
+ }
518
+ }
519
+ if (lastBase < cssText.length && atStart < 0) {
520
+ const remaining = cssText.slice(lastBase).trim();
521
+ if (remaining) baseParts.push(remaining);
522
+ }
523
+ if (atRules.length === 0) return {
524
+ base: cssText,
525
+ atRules: []
526
+ };
527
+ return {
528
+ base: baseParts.join(" "),
529
+ atRules
530
+ };
531
+ }
532
+ /**
533
+ * Compute a className from CSS text without injecting (pure function).
534
+ */
535
+ getClassName(cssText) {
536
+ const cached = this.insertCache.get(cssText);
537
+ if (cached) return cached;
538
+ return `${PREFIX}-${hash(cssText)}`;
539
+ }
540
+ /**
541
+ * Insert CSS rules for a component. Returns the class name (deterministic, hash-based).
542
+ * Deduplicates: same CSS text always produces the same class name and
543
+ * the rules are only injected once.
544
+ *
545
+ * When `boost` is true, the selector is doubled (`.pyr-abc.pyr-abc`)
546
+ * to raise specificity from (0,1,0) to (0,2,0).
547
+ */
548
+ insert(cssText, boost = false) {
549
+ const icKey = boost ? `${cssText}\0` : cssText;
550
+ const icHit = this.insertCache.get(icKey);
551
+ if (icHit) return icHit;
552
+ const className = `${PREFIX}-${hash(cssText)}`;
553
+ if (this.cache.has(className)) {
554
+ this.insertCache.set(icKey, className);
555
+ return className;
556
+ }
557
+ this.evictIfNeeded();
558
+ this.cache.set(className, className);
559
+ const selector = boost ? `.${className}.${className}` : `.${className}`;
560
+ const { base, atRules } = this.splitAtRules(cssText, selector);
561
+ const rules = [];
562
+ if (base) rules.push(`${selector}{${base}}`);
563
+ rules.push(...atRules);
564
+ const finalRules = this.layer ? rules.map((r) => `@layer ${this.layer}{${r}}`) : rules;
565
+ if (this.isSSR) for (const rule of finalRules) this.ssrBuffer.push(rule);
566
+ else if (this.sheet) for (const rule of finalRules) try {
567
+ this.sheet.insertRule(rule, this.sheet.cssRules.length);
568
+ } catch (_e) {
569
+ if (process.env.NODE_ENV !== "production") console.warn("[styler] Failed to insert CSS rule:", rule, _e);
570
+ }
571
+ this.insertCache.set(icKey, className);
572
+ return className;
573
+ }
574
+ /** Insert a @keyframes rule. Deduplicates by animation name. */
575
+ insertKeyframes(name, body) {
576
+ if (this.cache.has(name)) return;
577
+ this.evictIfNeeded();
578
+ this.cache.set(name, name);
579
+ const rule = `@keyframes ${name}{${body}}`;
580
+ if (this.isSSR) this.ssrBuffer.push(rule);
581
+ else if (this.sheet) try {
582
+ this.sheet.insertRule(rule, this.sheet.cssRules.length);
583
+ } catch (_e) {
584
+ if (process.env.NODE_ENV !== "production") {}
585
+ }
586
+ }
587
+ /**
588
+ * Split CSS text into individual top-level rules.
589
+ * CSSStyleSheet.insertRule() only accepts one rule at a time.
590
+ */
591
+ splitRules(cssText) {
592
+ const rules = [];
593
+ let depth = 0;
594
+ let start = 0;
595
+ for (let i = 0; i < cssText.length; i++) {
596
+ const ch = cssText[i];
597
+ if (ch === "{") depth++;
598
+ else if (ch === "}") {
599
+ depth--;
600
+ if (depth === 0) {
601
+ const rule = cssText.slice(start, i + 1).trim();
602
+ if (rule) rules.push(rule);
603
+ start = i + 1;
604
+ }
605
+ }
606
+ }
607
+ return rules;
608
+ }
609
+ /** Insert global CSS rules (no wrapper selector). Deduplicates by hash. */
610
+ insertGlobal(cssText) {
611
+ const key = `global-${hash(cssText)}`;
612
+ if (this.cache.has(key)) return;
613
+ this.evictIfNeeded();
614
+ this.cache.set(key, key);
615
+ if (this.isSSR) this.ssrBuffer.push(cssText);
616
+ else if (this.sheet) {
617
+ const rules = this.splitRules(cssText);
618
+ for (const rule of rules) try {
619
+ this.sheet.insertRule(rule, this.sheet.cssRules.length);
620
+ } catch (_e) {
621
+ if (process.env.NODE_ENV !== "production") console.warn("[styler] Failed to insert global CSS rule:", rule, _e);
622
+ }
623
+ }
624
+ }
625
+ /** Returns collected CSS for SSR as a complete `<style>` tag string. */
626
+ getStyleTag() {
627
+ return `<style ${ATTR}="">${this.ssrBuffer.join("").replace(/<\/style/gi, "<\\/style")}</style>`;
628
+ }
629
+ /** Returns collected CSS rules as a raw string (useful for streaming SSR). */
630
+ getStyles() {
631
+ return this.ssrBuffer.join("");
632
+ }
633
+ /** Reset SSR buffer and cache (call between server requests). */
634
+ reset() {
635
+ this.ssrBuffer = [];
636
+ this.cache.clear();
637
+ this.insertCache.clear();
638
+ }
639
+ /** Clear the dedup cache. Useful for HMR / dev-time reloads. */
640
+ clearCache() {
641
+ this.cache.clear();
642
+ this.insertCache.clear();
643
+ clearNormCache();
644
+ }
645
+ /**
646
+ * Full cleanup: clear cache and remove all CSS rules from the DOM.
647
+ * Intended for HMR / dev-time reloads where stale styles must be purged.
648
+ */
649
+ clearAll() {
650
+ this.cache.clear();
651
+ this.insertCache.clear();
652
+ clearNormCache();
653
+ this.ssrBuffer = [];
654
+ if (this.sheet) while (this.sheet.cssRules.length > 0) this.sheet.deleteRule(0);
655
+ }
656
+ /**
657
+ * Compute className and full CSS rule text without injecting.
658
+ */
659
+ prepare(cssText, boost = false) {
660
+ const className = `${PREFIX}-${hash(cssText)}`;
661
+ const selector = boost ? `.${className}.${className}` : `.${className}`;
662
+ const { base, atRules } = this.splitAtRules(cssText, selector);
663
+ const allRules = [];
664
+ if (base) allRules.push(`${selector}{${base}}`);
665
+ allRules.push(...atRules);
666
+ return {
667
+ className,
668
+ rules: (this.layer ? allRules.map((r) => `@layer ${this.layer}{${r}}`) : allRules).join("")
669
+ };
670
+ }
671
+ /** Check if a className is already in the cache. O(1) Map lookup. */
672
+ has(className) {
673
+ return this.cache.has(className);
674
+ }
675
+ /** Current number of cached rules. */
676
+ get cacheSize() {
677
+ return this.cache.size;
678
+ }
679
+ };
680
+ /** Default singleton sheet for client-side use. */
681
+ const sheet = new StyleSheet();
682
+ /**
683
+ * Factory for creating isolated StyleSheet instances.
684
+ * Use in SSR to get per-request isolation.
685
+ */
686
+ const createSheet = (options) => new StyleSheet(options);
687
+
688
+ //#endregion
689
+ //#region src/ThemeProvider.ts
690
+ const ThemeContext = createContext({});
691
+ /** Hook to read the current theme from the nearest ThemeProvider. */
692
+ const useTheme = () => useContext(ThemeContext);
693
+ /** Provides a theme object to all nested styled components via Pyreon context. */
694
+ function ThemeProvider({ theme, children }) {
695
+ pushContext(new Map([[ThemeContext.id, theme]]));
696
+ onUnmount(() => popContext());
697
+ return children ?? null;
698
+ }
699
+
700
+ //#endregion
701
+ //#region src/globalStyle.ts
702
+ const createGlobalStyle = (strings, ...values) => {
703
+ if (!values.some(isDynamic)) {
704
+ const cssText = normalizeCSS(resolve(strings, values, {}));
705
+ if (cssText.trim()) sheet.insertGlobal(cssText);
706
+ const StaticGlobal = () => null;
707
+ return StaticGlobal;
708
+ }
709
+ const DynamicGlobal = (props) => {
710
+ const theme = useTheme();
711
+ const cssText = normalizeCSS(resolve(strings, values, {
712
+ ...props,
713
+ theme
714
+ }));
715
+ if (cssText.trim()) sheet.insertGlobal(cssText);
716
+ return null;
717
+ };
718
+ return DynamicGlobal;
719
+ };
720
+
721
+ //#endregion
722
+ //#region src/keyframes.ts
723
+ /**
724
+ * keyframes() tagged template function. Creates a CSS @keyframes rule,
725
+ * injects it into the stylesheet, and returns the generated animation name.
726
+ *
727
+ * Usage:
728
+ * const fadeIn = keyframes`
729
+ * from { opacity: 0; }
730
+ * to { opacity: 1; }
731
+ * `
732
+ * // fadeIn === "pyr-kf-abc123" (deterministic, hash-based)
733
+ */
734
+ var KeyframesResult = class {
735
+ name;
736
+ constructor(strings, values) {
737
+ const body = normalizeCSS(resolve(strings, values, {}));
738
+ this.name = `pyr-kf-${hash(body)}`;
739
+ sheet.insertKeyframes(this.name, body);
740
+ }
741
+ /** Returns the animation name when used in string context. */
742
+ toString() {
743
+ return this.name;
744
+ }
745
+ };
746
+ const keyframes = (strings, ...values) => new KeyframesResult(strings, values);
747
+
748
+ //#endregion
749
+ //#region src/styled.tsx
750
+ const getDisplayName = (tag) => typeof tag === "string" ? tag : tag.displayName || tag.name || "Component";
751
+ const staticComponentCache = /* @__PURE__ */ new WeakMap();
752
+ let _hotStrings = null;
753
+ let _hotTag = null;
754
+ let _hotComponent = null;
755
+ const createStyledComponent = (tag, strings, values, options) => {
756
+ if (values.length === 0 && !options) {
757
+ if (strings === _hotStrings && tag === _hotTag) return _hotComponent;
758
+ const tagMap = staticComponentCache.get(strings);
759
+ if (tagMap) {
760
+ const cached = tagMap.get(tag);
761
+ if (cached) {
762
+ _hotStrings = strings;
763
+ _hotTag = tag;
764
+ _hotComponent = cached;
765
+ return cached;
766
+ }
767
+ }
768
+ }
769
+ const hasDynamicValues = values.length > 0 && values.some(isDynamic);
770
+ const customFilter = options ? options.shouldForwardProp : void 0;
771
+ const boost = options ? options.boost ?? false : false;
772
+ if (!hasDynamicValues) {
773
+ const cssText = normalizeCSS(values.length === 0 ? strings[0] : resolve(strings, values, {}));
774
+ const staticClassName = cssText.length > 0 ? sheet.insert(cssText, boost) : "";
775
+ const StaticStyled = (rawProps) => {
776
+ const finalTag = rawProps.as || tag;
777
+ return h(finalTag, buildProps(rawProps, staticClassName, typeof finalTag === "string", customFilter), ...Array.isArray(rawProps.children) ? rawProps.children : rawProps.children != null ? [rawProps.children] : []);
778
+ };
779
+ StaticStyled.displayName = `styled(${getDisplayName(tag)})`;
780
+ if (!options && values.length === 0) {
781
+ let tagMap = staticComponentCache.get(strings);
782
+ if (!tagMap) {
783
+ tagMap = /* @__PURE__ */ new Map();
784
+ staticComponentCache.set(strings, tagMap);
785
+ }
786
+ tagMap.set(tag, StaticStyled);
787
+ _hotStrings = strings;
788
+ _hotTag = tag;
789
+ _hotComponent = StaticStyled;
790
+ }
791
+ return StaticStyled;
792
+ }
793
+ const DynamicStyled = (rawProps) => {
794
+ const theme = useTheme();
795
+ const cssText = normalizeCSS(resolve(strings, values, {
796
+ ...rawProps,
797
+ theme
798
+ }));
799
+ const className = cssText.length > 0 ? sheet.insert(cssText, boost) : "";
800
+ const finalTag = rawProps.as || tag;
801
+ return h(finalTag, buildProps(rawProps, className, typeof finalTag === "string", customFilter), ...Array.isArray(rawProps.children) ? rawProps.children : rawProps.children != null ? [rawProps.children] : []);
802
+ };
803
+ DynamicStyled.displayName = `styled(${getDisplayName(tag)})`;
804
+ return DynamicStyled;
805
+ };
806
+ /** Factory function: styled(tag) returns a tagged template function. */
807
+ const styledFactory = (tag, options) => {
808
+ const templateFn = (strings, ...values) => createStyledComponent(tag, strings, values, options);
809
+ return templateFn;
810
+ };
811
+ /**
812
+ * Main styled export. Supports both calling conventions:
813
+ * - `styled('div')` or `styled(Component)` → returns tagged template function
814
+ * - `styled('div', { shouldForwardProp })` → with custom prop filtering
815
+ * - `styled.div` → shorthand via Proxy (no options)
816
+ */
817
+ const proxyCache = /* @__PURE__ */ new Map();
818
+ const styled = new Proxy(styledFactory, { get(_target, prop) {
819
+ if (prop === "prototype" || prop === "$$typeof") return void 0;
820
+ let fn = proxyCache.get(prop);
821
+ if (!fn) {
822
+ fn = (strings, ...values) => createStyledComponent(prop, strings, values);
823
+ proxyCache.set(prop, fn);
824
+ }
825
+ return fn;
826
+ } });
827
+
828
+ //#endregion
829
+ //#region src/useCSS.ts
830
+ /**
831
+ * Hook that resolves a CSSResult template with props, injects CSS
832
+ * into the shared stylesheet, and returns the className.
833
+ *
834
+ * Use this when you need computed CSS class names on plain elements
835
+ * without the overhead of a styled component layer.
836
+ */
837
+ function useCSS(template, props, boost) {
838
+ const theme = useTheme();
839
+ const allProps = theme ? {
840
+ ...props,
841
+ theme
842
+ } : props ?? {};
843
+ const cssText = normalizeCSS(resolve(template.strings, template.values, allProps));
844
+ if (!cssText.trim()) return "";
845
+ return sheet.insert(cssText, boost);
846
+ }
847
+
848
+ //#endregion
849
+ export { HASH_INIT, StyleSheet, ThemeContext, ThemeProvider, buildProps, clearNormCache, createGlobalStyle, createSheet, css, filterProps, hash, hashFinalize, hashUpdate, isDynamic, keyframes, normalizeCSS, resolve, resolveValue, sheet, styled, useCSS, useTheme };
850
+ //# sourceMappingURL=index.js.map
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@pyreon/styler",
3
+ "version": "0.0.2",
4
+ "description": "Lightweight CSS-in-JS engine for Pyreon",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "exports": {
9
+ "source": "./src/index.ts",
10
+ "import": "./lib/index.js",
11
+ "types": "./lib/index.d.ts"
12
+ },
13
+ "types": "./lib/index.d.ts",
14
+ "main": "./lib/index.js",
15
+ "files": [
16
+ "lib",
17
+ "!lib/**/*.map",
18
+ "!lib/analysis",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "engines": {
23
+ "node": ">= 18"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "scripts": {
29
+ "prepublish": "bun run build",
30
+ "build": "bun run vl_rolldown_build",
31
+ "build:watch": "bun run vl_rolldown_build-watch",
32
+ "lint": "biome check src/",
33
+ "test": "vitest run",
34
+ "test:coverage": "vitest run --coverage",
35
+ "test:watch": "vitest",
36
+ "typecheck": "tsc --noEmit"
37
+ },
38
+ "peerDependencies": {
39
+ "@pyreon/core": ">=0.3.0",
40
+ "@pyreon/reactivity": ">=0.3.0"
41
+ },
42
+ "devDependencies": {
43
+ "@vitus-labs/tools-rolldown": "^1.15.0",
44
+ "@vitus-labs/tools-typescript": "^1.15.0"
45
+ }
46
+ }