next-style 1.1.2 → 1.1.4

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 CHANGED
@@ -3,7 +3,9 @@
3
3
  **next-style** is a lightweight runtime CSS-in-JS engine designed for React, Next.js, and Bun.
4
4
 
5
5
  It is intentionally designed to be **page-scoped and component-scoped**, ensuring predictable
6
- style isolation with zero global leakage between pages.
6
+ style isolation with zero global leakage between pages or routes.
7
+
8
+ The library is framework-agnostic at its core and does not depend on React internally.
7
9
 
8
10
  ---
9
11
 
@@ -13,11 +15,12 @@ style isolation with zero global leakage between pages.
13
15
  - No global singleton style registry
14
16
  - No cross-page or cross-route CSS leakage
15
17
  - Each page owns its own styles
16
- - Explicit style injection via `StyleProvider`
18
+ - Explicit style injection via a `<style>` tag
19
+ - No implicit DOM side effects
17
20
 
18
21
  Because of this design:
19
22
  - `new NextStyle()` must be created inside the page or component scope
20
- - `StyleProvider` must be rendered **on every page where styles are used**
23
+ - Styles must be injected manually using `ns.StyleText`
21
24
 
22
25
  ---
23
26
 
@@ -27,12 +30,12 @@ Because of this design:
27
30
  - Deterministic hashing (same styles always produce the same class name)
28
31
  - Nested pseudo selectors (`_hover`, `_focus`, `_active`)
29
32
  - Built-in responsive media queries
30
- - Global styles (page-scoped)
33
+ - Page-scoped global styles
31
34
  - `@keyframes` support
32
35
  - `@font-face` support
33
36
  - PostCSS + Autoprefixer integration
34
- - Automatic rule deduplication (within a page)
35
- - Single `<style>` injection per page
37
+ - Automatic rule deduplication (per instance)
38
+ - Single `<style>` injection per page or component
36
39
  - No side effects (`sideEffects: false`)
37
40
 
38
41
  ---
@@ -82,17 +85,17 @@ You may optionally provide a **custom prefix** to control generated class names.
82
85
  import { NextStyle } from "next-style"
83
86
 
84
87
  export default function Page() {
85
- const style = new NextStyle("home")
86
- const button = style.css({
88
+ const ns = new NextStyle("home")
89
+ const btn = ns.css({
87
90
  padding: "8px 16px",
88
91
  backgroundColor: "black",
89
92
  color: "white",
90
- borderRadius: 6
93
+ borderRadius: "6px"
91
94
  })
92
95
  return (
93
96
  <>
94
- <style.StyleProvider />
95
- <button className={ button }>Click me</button>
97
+ <style>{ ns.StyleText }</style>
98
+ <button className={ btn }>Click me</button>
96
99
  </>
97
100
  )
98
101
  }
@@ -106,25 +109,10 @@ home_ab12cd3
106
109
 
107
110
  Notes:
108
111
  - The prefix is optional
109
- - If omitted, the default prefix is used
112
+ - If omitted, a default prefix is used
110
113
  - Prefixes help identify styles per page or component
111
114
  - Each page or component should create its own `NextStyle` instance
112
115
 
113
-
114
- ## Why StyleProvider Is Required Per Page
115
-
116
- `next-style` does not use a global style registry.
117
-
118
- Each `NextStyle` instance:
119
- - Collects styles locally
120
- - Injects a single `<style>` tag
121
- - Is destroyed when the page/component unmounts
122
-
123
- This ensures:
124
- - No stale CSS after route changes
125
- - No style conflicts between pages
126
- - Predictable SSR and CSR behavior
127
-
128
116
  ---
129
117
 
130
118
  ## Pseudo Selectors
@@ -132,7 +120,7 @@ This ensures:
132
120
  Pseudo selectors are defined using keys prefixed with `_`.
133
121
 
134
122
  ```ts
135
- const card = style.css({
123
+ const card = ns.css({
136
124
  backgroundColor: "#fff",
137
125
  transition: "0.2s ease",
138
126
  _hover: {
@@ -162,7 +150,7 @@ Built-in breakpoints:
162
150
  - `_xxl` → min-width: 1536px
163
151
 
164
152
  ```ts
165
- const box = style.css({
153
+ const box = ns.css({
166
154
  width: 100,
167
155
  _md: {
168
156
  width: 200
@@ -179,16 +167,16 @@ Media queries can be nested and are automatically merged.
179
167
 
180
168
  ## Global Styles (Page Scoped)
181
169
 
182
- Global styles are **scoped to the current page**.
170
+ Global styles are scoped to the current page or component instance.
183
171
 
184
172
  ```ts
185
- style.global("body", {
173
+ ns.global("body", {
186
174
  margin: 0,
187
175
  fontFamily: "Inter, sans-serif"
188
176
  })
189
177
  ```
190
178
 
191
- These styles are removed automatically when the page unmounts.
179
+ These styles exist only for the lifetime of the page or component.
192
180
 
193
181
  ---
194
182
 
@@ -197,14 +185,16 @@ These styles are removed automatically when the page unmounts.
197
185
  Create animations using `keyframes()`:
198
186
 
199
187
  ```ts
200
- const fadeIn = style.keyframes({
188
+ const fadeIn = ns.keyframes({
201
189
  from: { opacity: 0 },
202
190
  to: { opacity: 1 }
203
191
  })
204
192
  ```
205
193
 
194
+ Use the animation in styles:
195
+
206
196
  ```ts
207
- const modal = style.css({
197
+ const modal = ns.css({
208
198
  animation: `${ fadeIn } 0.3s ease-out`
209
199
  })
210
200
  ```
@@ -218,7 +208,7 @@ Keyframes are scoped to the current `NextStyle` instance.
218
208
  Declare fonts using `fontFace()`:
219
209
 
220
210
  ```ts
221
- style.fontFace({
211
+ ns.fontFace({
222
212
  fontFamily: "Inter",
223
213
  src: "url(/fonts/inter.woff2) format('woff2')",
224
214
  fontWeight: 400,
@@ -227,7 +217,7 @@ style.fontFace({
227
217
  })
228
218
  ```
229
219
 
230
- Font-face rules are injected only for the current page.
220
+ Font-face rules are injected only for the current page or component.
231
221
 
232
222
  ---
233
223
 
@@ -240,12 +230,31 @@ Font-face rules are injected only for the current page.
240
230
 
241
231
  ---
242
232
 
243
- ## Performance
233
+ ## Server-Side Rendering (SSR)
234
+
235
+ `next-style` is SSR-safe.
236
+
237
+ Because the core API only generates strings:
238
+ - No JSX is exported from the library
239
+ - No React runtime is required
240
+ - No side effects occur during render
241
+
242
+ You can safely inject styles during SSR:
243
+
244
+ ```tsx
245
+ <style>{ ns.StyleText }</style>
246
+ ```
247
+
248
+ The same output will be produced on both the server and the client.
249
+
250
+ ---
251
+
252
+ ## Performance Characteristics
244
253
 
245
- - CSS rules are generated and injected only once per page
254
+ - CSS rules are generated and deduplicated per instance
246
255
  - PostCSS and Autoprefixer results are cached
247
- - One `<style>` tag per page
248
- - No global runtime mutation
256
+ - Only one `<style>` tag is required per page or component
257
+ - No global runtime mutations
249
258
 
250
259
  Ideal for:
251
260
  - Next.js App Router
@@ -254,6 +263,15 @@ Ideal for:
254
263
 
255
264
  ---
256
265
 
266
+ ## Common Gotchas
267
+
268
+ - Do not create a shared `NextStyle` instance across pages
269
+ - Do not treat `next-style` as a global style manager
270
+ - Always inject `ns.StyleText` before elements that use generated class names
271
+ - Avoid calling `ns.css()` conditionally with different order between renders
272
+
273
+ ---
274
+
257
275
  ## Package Information
258
276
 
259
277
  - Name: `next-style`
@@ -275,8 +293,8 @@ https://github.com/kingslimes/next-style/issues
275
293
  - `&` selector nesting
276
294
  - `_dark` / `_light` helpers
277
295
  - `@layer` support
278
- - SSR collection utilities
279
- - Framework adapters (Solid, Preact)
296
+ - Optional React helpers
297
+ - Dev-time warnings for incorrect usage
280
298
 
281
299
  ---
282
300
 
package/dist/index.d.ts CHANGED
@@ -31,6 +31,6 @@ export declare class NextStyle {
31
31
  global: (selector: string, style: NextStyleProperties) => void;
32
32
  keyframes: (frames: KeyframesObject) => string;
33
33
  fontFace: (font: FontFaceObject) => void;
34
- StyleProvider: () => import("react").JSX.Element | null;
34
+ get StyleText(): string | null;
35
35
  }
36
36
  export {};
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import postcss from"postcss";import autoprefixer from"autoprefixer";var processor=postcss([autoprefixer({overrideBrowserslist:[">0.2%","not dead","not op_mini all"]})]),postcssCache=new Map;function postcssTransform(cssText){let cached=postcssCache.get(cssText);if(cached)return cached;let result=processor.process(cssText,{from:void 0}).css;return postcssCache.set(cssText,result),result}function stableStringify(value){if(value==null||typeof value!=="object")return JSON.stringify(value);if(Array.isArray(value))return`[${value.map(stableStringify).join(",")}]`;return`{${Object.keys(value).sort().map((k)=>`"${k}":${stableStringify(value[k])}`).join(",")}}`}function createHashName(seed){let hash=BigInt("0xcbf29ce484222325"),prime=BigInt("0x100000001b3");for(let i=0;i<seed.length;i++)hash^=BigInt(seed.charCodeAt(i)),hash*=prime,hash&=BigInt("0xffffffffffffffff");return hash.toString(36).slice(0,9)}function toKebabCase(prop){return prop.replace(/[A-Z]/g,(m)=>`-${m.toLowerCase()}`)}var MEDIA_MAP={_sm:"(min-width:640px)",_md:"(min-width:768px)",_lg:"(min-width:1024px)",_xl:"(min-width:1280px)",_xxl:"(min-width:1536px)"};function mergeMedia(parent,current){if(!parent)return current;if(!current)return parent;return`${parent} and ${current}`}function serializeNested(style,ctx){let css="",base="";for(let key in style){let value=style[key];if(value==null||typeof value==="object"||key.startsWith("_"))continue;base+=`${toKebabCase(key)}:${value};`}if(base){let rule=`${ctx.selector}{${base}}`;css+=ctx.media?`@media ${ctx.media}{${rule}}`:rule}for(let pseudo of["_hover","_focus","_active"]){let value=style[pseudo];if(!value)continue;css+=serializeNested(value,{selector:`${ctx.selector}:${pseudo.slice(1)}`,media:ctx.media})}for(let key in MEDIA_MAP){let mediaKey=key,value=style[mediaKey];if(!value)continue;css+=serializeNested(value,{selector:ctx.selector,media:mergeMedia(ctx.media,MEDIA_MAP[mediaKey])})}return css}class NextStyle{prefix;rules=new Map;constructor(prefix="next"){this.prefix=prefix}css=(style)=>{let seed=stableStringify(style),hash=createHashName(seed),className=`${this.prefix}_${hash}`,key=`class:${className}`;if(!this.rules.has(key)){let raw=serializeNested(style,{selector:`.${className}`}),cssText=postcssTransform(raw);this.rules.set(key,cssText)}return className};global=(selector,style)=>{let key=`global:${selector}`;if(!this.rules.has(key)){let raw=serializeNested(style,{selector}),cssText=postcssTransform(raw);this.rules.set(key,cssText)}};keyframes=(frames)=>{let seed=stableStringify(frames),hash=createHashName(seed),name=`${this.prefix}_${hash}`,key=`@keyframes:${name}`;if(!this.rules.has(key)){let body="";for(let step in frames){let props="",frame=frames[step];for(let prop in frame)props+=`${toKebabCase(prop)}:${frame[prop]};`;body+=`${step}{${props}}`}let cssText=postcssTransform(`@keyframes ${name}{${body}}`);this.rules.set(key,cssText)}return name};fontFace=(font)=>{let seed=stableStringify(font),key=`@font-face:${createHashName(seed)}`;if(!this.rules.has(key)){let body="";for(let prop in font)body+=`${toKebabCase(prop)}:${font[prop]};`;let cssText=postcssTransform(`@font-face{${body}}`);this.rules.set(key,cssText)}};get StyleText(){if(this.rules.size===0)return null;let cssText="";for(let rule of this.rules.values())cssText+=rule+`
2
+ `;return cssText}}export{NextStyle};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-style",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "Lightweight runtime CSS-in-JS engine with nested pseudo selectors, media queries, global styles, keyframes, and font-face support.",
5
5
  "author": {
6
6
  "name": "kingslimes",
@@ -16,11 +16,11 @@
16
16
  "LICENSE",
17
17
  "README.md"
18
18
  ],
19
- "main": "./dist/index.jsx",
19
+ "main": "./dist/index.js",
20
20
  "types": "./dist/index.d.ts",
21
21
  "exports": {
22
22
  ".": {
23
- "import": "./dist/index.jsx",
23
+ "import": "./dist/index.js",
24
24
  "types": "./dist/index.d.ts"
25
25
  }
26
26
  },
@@ -52,7 +52,6 @@
52
52
  "font-face"
53
53
  ],
54
54
  "peerDependencies": {
55
- "react": ">=18",
56
55
  "postcss": "^8",
57
56
  "autoprefixer": "^10"
58
57
  },
@@ -60,7 +59,9 @@
60
59
  "csstype": "latest"
61
60
  },
62
61
  "engines": {
63
- "bun": ">=1.0.0",
64
62
  "node": ">=18"
63
+ },
64
+ "publishConfig": {
65
+ "access": "public"
65
66
  }
66
67
  }
package/dist/index.jsx DELETED
@@ -1,148 +0,0 @@
1
- import postcss from "postcss";
2
- import autoprefixer from "autoprefixer";
3
- const processor = postcss([autoprefixer({
4
- overrideBrowserslist: [">0.2%", "not dead", "not op_mini all"]
5
- })]);
6
- const postcssCache = new Map();
7
- function postcssTransform(cssText) {
8
- const cached = postcssCache.get(cssText);
9
- if (cached) return cached;
10
- const result = processor.process(cssText, {
11
- from: undefined
12
- }).css;
13
- postcssCache.set(cssText, result);
14
- return result;
15
- }
16
- function stableStringify(value) {
17
- if (value == null || typeof value !== "object") return JSON.stringify(value);
18
- if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
19
- const keys = Object.keys(value).sort();
20
- return `{${keys.map(k => `"${k}":${stableStringify(value[k])}`).join(",")}}`;
21
- }
22
- function createHashName(seed) {
23
- let hash = BigInt("0xcbf29ce484222325");
24
- const prime = BigInt("0x100000001b3");
25
- for (let i = 0; i < seed.length; i++) {
26
- hash ^= BigInt(seed.charCodeAt(i));
27
- hash *= prime;
28
- hash &= BigInt("0xffffffffffffffff");
29
- }
30
- return hash.toString(36).slice(0, 9);
31
- }
32
- function toKebabCase(prop) {
33
- return prop.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`);
34
- }
35
- const MEDIA_MAP = {
36
- _sm: "( min-width: 640px )",
37
- _md: "( min-width: 768px )",
38
- _lg: "( min-width: 1024px )",
39
- _xl: "( min-width: 1280px )",
40
- _xxl: "( min-width: 1536px )"
41
- };
42
- function mergeMedia(parent, current) {
43
- if (!parent) return current;
44
- if (!current) return parent;
45
- return `${parent} and ${current}`;
46
- }
47
- function serializeNested(style, ctx) {
48
- let css = "";
49
- let base = "";
50
- for (const key in style) {
51
- const value = style[key];
52
- if (value == null || typeof value === "object" || key.startsWith("_")) continue;
53
- base += `${toKebabCase(key)}:${value};`;
54
- }
55
- if (base) {
56
- const rule = `${ctx.selector}{${base}}`;
57
- css += ctx.media ? `@media ${ctx.media}{${rule}}` : rule;
58
- }
59
- for (const pseudo of ["_hover", "_focus", "_active"]) {
60
- const value = style[pseudo];
61
- if (!value) continue;
62
- css += serializeNested(value, {
63
- selector: `${ctx.selector}:${pseudo.slice(1)}`,
64
- media: ctx.media
65
- });
66
- }
67
- for (const key in MEDIA_MAP) {
68
- const mediaKey = key;
69
- const value = style[mediaKey];
70
- if (!value) continue;
71
- css += serializeNested(value, {
72
- selector: ctx.selector,
73
- media: mergeMedia(ctx.media, MEDIA_MAP[mediaKey])
74
- });
75
- }
76
- return css;
77
- }
78
- export class NextStyle {
79
- rules = new Map();
80
- constructor(prefix = "next") {
81
- this.prefix = prefix;
82
- }
83
- css = style => {
84
- const seed = stableStringify(style);
85
- const hash = createHashName(seed);
86
- const className = `${this.prefix}_${hash}`;
87
- const key = `class:${className}`;
88
- if (!this.rules.has(key)) {
89
- const raw = serializeNested(style, {
90
- selector: `.${className}`
91
- });
92
- const cssText = postcssTransform(raw);
93
- this.rules.set(key, cssText);
94
- }
95
- return className;
96
- };
97
- global = (selector, style) => {
98
- const key = `global:${selector}`;
99
- if (!this.rules.has(key)) {
100
- const raw = serializeNested(style, {
101
- selector
102
- });
103
- const cssText = postcssTransform(raw);
104
- this.rules.set(key, cssText);
105
- }
106
- };
107
- keyframes = frames => {
108
- const seed = stableStringify(frames);
109
- const hash = createHashName(seed);
110
- const name = `${this.prefix}_${hash}`;
111
- const key = `@keyframes:${name}`;
112
- if (!this.rules.has(key)) {
113
- let body = "";
114
- for (const step in frames) {
115
- let props = "";
116
- const frame = frames[step];
117
- for (const prop in frame) {
118
- props += `${toKebabCase(prop)}:${frame[prop]};`;
119
- }
120
- body += `${step}{${props}}`;
121
- }
122
- const cssText = postcssTransform(`@keyframes ${name}{${body}}`);
123
- this.rules.set(key, cssText);
124
- }
125
- return name;
126
- };
127
- fontFace = font => {
128
- const seed = stableStringify(font);
129
- const hash = createHashName(seed);
130
- const key = `@font-face:${hash}`;
131
- if (!this.rules.has(key)) {
132
- let body = "";
133
- for (const prop in font) {
134
- body += `${toKebabCase(prop)}:${font[prop]};`;
135
- }
136
- const cssText = postcssTransform(`@font-face{${body}}`);
137
- this.rules.set(key, cssText);
138
- }
139
- };
140
- StyleProvider = () => {
141
- if (this.rules.size === 0) return null;
142
- let cssText = "";
143
- for (const rule of this.rules.values()) {
144
- cssText += rule + "\n";
145
- }
146
- return <style>{cssText}</style>;
147
- };
148
- }