next-style 1.1.1 → 1.1.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/README.md CHANGED
@@ -1,227 +1,285 @@
1
- # NextStyle
1
+ # next-style
2
2
 
3
- A lightweight runtime CSS-in-JS engine designed for React and Next.js.
4
- It supports nested pseudo selectors, responsive media queries, and automatic vendor prefixing via PostCSS — all at runtime with zero build-time CSS extraction.
3
+ **next-style** is a lightweight runtime CSS-in-JS engine designed for React, Next.js, and Bun.
5
4
 
6
- NextStyle focuses on **runtime flexibility**, **type safety**, and **minimal abstraction**, making it suitable for design systems, theming, and dynamic UI styling.
5
+ It is intentionally designed to be **page-scoped and component-scoped**, ensuring predictable
6
+ style isolation with zero global leakage between pages.
7
+
8
+ ---
9
+
10
+ ## Core Design Principles
11
+
12
+ - Styles are scoped to a **single page or component**
13
+ - No global singleton style registry
14
+ - No cross-page or cross-route CSS leakage
15
+ - Each page owns its own styles
16
+ - Explicit style injection via `StyleProvider`
17
+
18
+ Because of this design:
19
+ - `new NextStyle()` must be created inside the page or component scope
20
+ - `StyleProvider` must be rendered **on every page where styles are used**
7
21
 
8
22
  ---
9
23
 
10
24
  ## Features
11
25
 
12
- - Runtime CSS generation
13
- - Strongly typed style objects (powered by `csstype`)
26
+ - Object-based styling with strong TypeScript support
27
+ - Deterministic hashing (same styles always produce the same class name)
14
28
  - Nested pseudo selectors (`_hover`, `_focus`, `_active`)
15
- - Responsive media queries (`_sm`, `_md`, `_lg`, `_xl`, `_xxl`)
16
- - Automatic vendor prefixing (PostCSS + Autoprefixer)
17
- - Deterministic hashed class names
18
- - React `<style>` provider component
19
- - No bundler or build-step CSS required
29
+ - Built-in responsive media queries
30
+ - Global styles (page-scoped)
31
+ - `@keyframes` support
32
+ - `@font-face` support
33
+ - PostCSS + Autoprefixer integration
34
+ - Automatic rule deduplication (within a page)
35
+ - Single `<style>` injection per page
36
+ - No side effects (`sideEffects: false`)
20
37
 
21
38
  ---
22
39
 
23
40
  ## Installation
24
41
 
25
- Install the package along with its peer dependencies:
42
+ ### npm
26
43
 
27
- ```bash
28
- npm install next-style postcss autoprefixer react
44
+ ```sh
45
+ npm install next-style
29
46
  ```
30
47
 
31
- or with Bun:
48
+ ### Bun
32
49
 
33
- ```bash
34
- bun add next-style postcss autoprefixer react
50
+ ```sh
51
+ bun add next-style
35
52
  ```
36
53
 
37
54
  ---
38
55
 
39
- ## Basic Usage
56
+ ## Peer Dependencies
40
57
 
41
- ```tsx
42
- import { NextStyle } from "next-style"
58
+ For most **Next.js applications**, you **do not need to install these manually**.
43
59
 
44
- const style = new NextStyle('home') // output className = home_{hash} default "next_{hash}"
60
+ - **React** is already included with Next.js
61
+ - **PostCSS** and **Autoprefixer** are bundled and used internally by Next.js
45
62
 
46
- const className = style.css({
47
- display: "flex",
48
- alignItems: "center",
49
- padding: "12px",
50
- backgroundColor: "#111",
51
- color: "white",
52
- _hover: {
53
- backgroundColor: "#222"
54
- },
55
- _md: {
56
- padding: "16px"
57
- }
58
- })
63
+ This section mainly applies if you are using **next-style outside of Next.js**, such as:
64
+ - Custom React setups
65
+ - Bun + React
66
+ - Vite or other non-Next runtimes
67
+
68
+ If required, install them manually:
69
+
70
+ ```sh
71
+ npm install react postcss autoprefixer
59
72
  ```
60
73
 
61
- Apply the generated class name to your component:
74
+ ---
75
+
76
+ ## Basic Usage (Page Scoped)
77
+
78
+ Create a `NextStyle` instance **inside the page or component**.
79
+ You may optionally provide a **custom prefix** to control generated class names.
62
80
 
63
- ```tsx
64
- export default function App() {
81
+ ```ts
82
+ import { NextStyle } from "next-style"
83
+
84
+ export default function Page() {
85
+ const style = new NextStyle("home")
86
+ const button = style.css({
87
+ padding: "8px 16px",
88
+ backgroundColor: "black",
89
+ color: "white",
90
+ borderRadius: 6
91
+ })
65
92
  return (
66
93
  <>
67
- <div className={className}>
68
- Hello NextStyle
69
- </div>
70
- <style.Provider />
94
+ <style.StyleProvider />
95
+ <button className={ button }>Click me</button>
71
96
  </>
72
97
  )
73
98
  }
74
99
  ```
75
100
 
76
- ---
101
+ Generated class names will look like:
77
102
 
78
- ## Style Object API
103
+ ```txt
104
+ home_ab12cd3
105
+ ```
79
106
 
80
- ### Base Properties
107
+ Notes:
108
+ - The prefix is optional
109
+ - If omitted, the default prefix is used
110
+ - Prefixes help identify styles per page or component
111
+ - Each page or component should create its own `NextStyle` instance
81
112
 
82
- All standard CSS properties are supported and fully typed:
83
113
 
84
- ```ts
85
- {
86
- margin: "8px",
87
- fontSize: "14px",
88
- backgroundColor: "#000"
89
- }
90
- ```
114
+ ## Why StyleProvider Is Required Per Page
91
115
 
92
- Types are derived from `csstype`, ensuring correctness and editor autocomplete.
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
93
127
 
94
128
  ---
95
129
 
96
- ### Pseudo Selectors
130
+ ## Pseudo Selectors
97
131
 
98
- Use underscored keys for pseudo selectors:
132
+ Pseudo selectors are defined using keys prefixed with `_`.
99
133
 
100
134
  ```ts
101
- {
135
+ const card = style.css({
136
+ backgroundColor: "#fff",
137
+ transition: "0.2s ease",
102
138
  _hover: {
103
- opacity: 0.8
104
- },
105
- _focus: {
106
- outline: "2px solid blue"
139
+ backgroundColor: "#f5f5f5"
107
140
  },
108
141
  _active: {
109
142
  transform: "scale(0.98)"
110
143
  }
111
- }
144
+ })
112
145
  ```
113
146
 
114
- Supported pseudos:
147
+ Supported pseudo selectors:
115
148
  - `_hover`
116
149
  - `_focus`
117
150
  - `_active`
118
151
 
119
152
  ---
120
153
 
121
- ### Responsive Media Queries
122
-
123
- NextStyle provides built-in responsive keys:
154
+ ## Responsive Styles (Media Queries)
124
155
 
125
- | Key | Media Query |
126
- |------|-------------|
127
- | `_sm` | `(min-width: 640px)` |
128
- | `_md` | `(min-width: 768px)` |
129
- | `_lg` | `(min-width: 1024px)` |
130
- | `_xl` | `(min-width: 1280px)` |
131
- | `_xxl` | `(min-width: 1536px)` |
156
+ Built-in breakpoints:
132
157
 
133
- Example:
158
+ - `_sm` → min-width: 640px
159
+ - `_md` → min-width: 768px
160
+ - `_lg` → min-width: 1024px
161
+ - `_xl` → min-width: 1280px
162
+ - `_xxl` → min-width: 1536px
134
163
 
135
164
  ```ts
136
- {
137
- fontSize: "14px",
165
+ const box = style.css({
166
+ width: 100,
167
+ _md: {
168
+ width: 200
169
+ },
138
170
  _lg: {
139
- fontSize: "18px"
171
+ width: 300
140
172
  }
141
- }
173
+ })
142
174
  ```
143
175
 
144
- ---
145
-
146
- ## `<style.Provider />`
176
+ Media queries can be nested and are automatically merged.
147
177
 
148
- The `Provider` component injects all generated CSS rules into a `<style>` tag.
178
+ ---
149
179
 
150
- Important notes:
180
+ ## Global Styles (Page Scoped)
151
181
 
152
- - It must be rendered **once per style instance**
153
- - It should be rendered **after** calling `css(...)`
154
- - It performs no side effects if no styles were generated
182
+ Global styles are **scoped to the current page**.
155
183
 
156
- ```tsx
157
- <style.Provider />
184
+ ```ts
185
+ style.global("body", {
186
+ margin: 0,
187
+ fontFamily: "Inter, sans-serif"
188
+ })
158
189
  ```
159
190
 
191
+ These styles are removed automatically when the page unmounts.
192
+
160
193
  ---
161
194
 
162
- ## Hashing Strategy
195
+ ## Keyframes
163
196
 
164
- Class names are generated using a deterministic hash based on the style object:
197
+ Create animations using `keyframes()`:
165
198
 
166
- ```text
167
- next_x9k3a2m1
199
+ ```ts
200
+ const fadeIn = style.keyframes({
201
+ from: { opacity: 0 },
202
+ to: { opacity: 1 }
203
+ })
168
204
  ```
169
205
 
170
- This ensures:
171
- - Stable class names
172
- - No duplicates
173
- - Automatic caching of generated rules
206
+ ```ts
207
+ const modal = style.css({
208
+ animation: `${ fadeIn } 0.3s ease-out`
209
+ })
210
+ ```
211
+
212
+ Keyframes are scoped to the current `NextStyle` instance.
174
213
 
175
214
  ---
176
215
 
177
- ## Runtime Behavior
216
+ ## Font Face
178
217
 
179
- - CSS is generated lazily on first usage
180
- - Rules are cached in memory
181
- - PostCSS transformation runs only once per unique rule
182
- - No DOM mutation outside React rendering
218
+ Declare fonts using `fontFace()`:
183
219
 
184
- ---
220
+ ```ts
221
+ style.fontFace({
222
+ fontFamily: "Inter",
223
+ src: "url(/fonts/inter.woff2) format('woff2')",
224
+ fontWeight: 400,
225
+ fontStyle: "normal",
226
+ fontDisplay: "swap"
227
+ })
228
+ ```
185
229
 
186
- ## Type Safety
230
+ Font-face rules are injected only for the current page.
187
231
 
188
- NextStyle provides full TypeScript support:
232
+ ---
189
233
 
190
- - All CSS properties are typed
191
- - Invalid properties are caught at compile time
192
- - Media and pseudo keys are strictly controlled
234
+ ## Deterministic Hashing
193
235
 
194
- Types are generated via `.d.ts` and require no runtime dependency on `csstype`.
236
+ - Styles are hashed using a stable algorithm
237
+ - Object keys are sorted before hashing
238
+ - Semantically identical styles always produce the same class name
239
+ - Prevents unnecessary class regeneration
195
240
 
196
241
  ---
197
242
 
198
- ## Requirements
243
+ ## Performance
199
244
 
200
- ### Peer Dependencies
245
+ - CSS rules are generated and injected only once per page
246
+ - PostCSS and Autoprefixer results are cached
247
+ - One `<style>` tag per page
248
+ - No global runtime mutation
201
249
 
202
- - React >= 18
203
- - PostCSS >= 8
204
- - Autoprefixer >= 10
250
+ Ideal for:
251
+ - Next.js App Router
252
+ - Page-level isolation
253
+ - Component-driven design systems
205
254
 
206
- ### Runtime Environment
255
+ ---
207
256
 
208
- - Node.js >= 18
209
- - Bun >= 1.0.0
257
+ ## Package Information
210
258
 
211
- ---
259
+ - Name: `next-style`
260
+ - Version: `1.1.2`
261
+ - License: MIT
262
+ - Module type: ESM
263
+ - Side effects: false
212
264
 
213
- ## License
265
+ Repository:
266
+ https://github.com/kingslimes/next-style
214
267
 
215
- MIT © kingslimes
268
+ Issues:
269
+ https://github.com/kingslimes/next-style/issues
216
270
 
217
271
  ---
218
272
 
219
- ## Philosophy
273
+ ## Roadmap
220
274
 
221
- NextStyle is intentionally minimal.
275
+ - `&` selector nesting
276
+ - `_dark` / `_light` helpers
277
+ - `@layer` support
278
+ - SSR collection utilities
279
+ - Framework adapters (Solid, Preact)
222
280
 
223
- - No build-time CSS extraction
224
- - No global runtime side effects
225
- - No magic conventions
281
+ ---
226
282
 
227
- Just predictable runtime styling with strong types and explicit behavior.
283
+ ## License
284
+
285
+ MIT © kingslimes
package/dist/index.d.ts CHANGED
@@ -12,11 +12,25 @@ export type NextStyleProperties = {
12
12
  _xxl?: NextStyleObject;
13
13
  };
14
14
  type NextStyleObject = Omit<NextStyleProperties, "_sm" | "_md" | "_lg" | "_xl" | "_xxl">;
15
+ type KeyframesObject = {
16
+ [step: string]: Properties<string | number>;
17
+ };
18
+ type FontFaceObject = {
19
+ fontFamily: string;
20
+ src: string;
21
+ fontWeight?: string | number;
22
+ fontStyle?: string;
23
+ fontDisplay?: string;
24
+ unicodeRange?: string;
25
+ };
15
26
  export declare class NextStyle {
16
27
  private prefix;
17
28
  private rules;
18
29
  constructor(prefix?: string);
19
30
  css: (style: NextStyleProperties) => string;
20
- Provider: () => import("react").JSX.Element | null;
31
+ global: (selector: string, style: NextStyleProperties) => void;
32
+ keyframes: (frames: KeyframesObject) => string;
33
+ fontFace: (font: FontFaceObject) => void;
34
+ StyleProvider: () => import("react").JSX.Element | null;
21
35
  }
22
36
  export {};
package/dist/index.jsx CHANGED
@@ -13,6 +13,12 @@ function postcssTransform(cssText) {
13
13
  postcssCache.set(cssText, result);
14
14
  return result;
15
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
+ }
16
22
  function createHashName(seed) {
17
23
  let hash = BigInt("0xcbf29ce484222325");
18
24
  const prime = BigInt("0x100000001b3");
@@ -33,6 +39,11 @@ const MEDIA_MAP = {
33
39
  _xl: "( min-width: 1280px )",
34
40
  _xxl: "( min-width: 1536px )"
35
41
  };
42
+ function mergeMedia(parent, current) {
43
+ if (!parent) return current;
44
+ if (!current) return parent;
45
+ return `${parent} and ${current}`;
46
+ }
36
47
  function serializeNested(style, ctx) {
37
48
  let css = "";
38
49
  let base = "";
@@ -43,14 +54,14 @@ function serializeNested(style, ctx) {
43
54
  }
44
55
  if (base) {
45
56
  const rule = `${ctx.selector}{${base}}`;
46
- css += ctx.media ? `${ctx.media}{${rule}}` : rule;
57
+ css += ctx.media ? `@media ${ctx.media}{${rule}}` : rule;
47
58
  }
48
59
  for (const pseudo of ["_hover", "_focus", "_active"]) {
49
60
  const value = style[pseudo];
50
61
  if (!value) continue;
51
62
  css += serializeNested(value, {
52
- ...ctx,
53
- selector: `${ctx.selector}:${pseudo.slice(1)}`
63
+ selector: `${ctx.selector}:${pseudo.slice(1)}`,
64
+ media: ctx.media
54
65
  });
55
66
  }
56
67
  for (const key in MEDIA_MAP) {
@@ -58,8 +69,8 @@ function serializeNested(style, ctx) {
58
69
  const value = style[mediaKey];
59
70
  if (!value) continue;
60
71
  css += serializeNested(value, {
61
- ...ctx,
62
- media: `@media ${MEDIA_MAP[mediaKey]}`
72
+ selector: ctx.selector,
73
+ media: mergeMedia(ctx.media, MEDIA_MAP[mediaKey])
63
74
  });
64
75
  }
65
76
  return css;
@@ -70,18 +81,63 @@ export class NextStyle {
70
81
  this.prefix = prefix;
71
82
  }
72
83
  css = style => {
73
- const hash = createHashName(JSON.stringify(style));
84
+ const seed = stableStringify(style);
85
+ const hash = createHashName(seed);
74
86
  const className = `${this.prefix}_${hash}`;
75
- if (!this.rules.has(className)) {
87
+ const key = `class:${className}`;
88
+ if (!this.rules.has(key)) {
76
89
  const raw = serializeNested(style, {
77
90
  selector: `.${className}`
78
91
  });
79
92
  const cssText = postcssTransform(raw);
80
- this.rules.set(className, cssText);
93
+ this.rules.set(key, cssText);
81
94
  }
82
95
  return className;
83
96
  };
84
- Provider = () => {
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 = () => {
85
141
  if (this.rules.size === 0) return null;
86
142
  let cssText = "";
87
143
  for (const rule of this.rules.values()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-style",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
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",
@@ -26,12 +26,12 @@
26
26
  },
27
27
  "repository": {
28
28
  "type": "git",
29
- "url": "https://github.com/kingslimes/zed-style.git"
29
+ "url": "https://github.com/kingslimes/next-style.git"
30
30
  },
31
31
  "bugs": {
32
- "url": "https://github.com/kingslimes/zed-style/issues"
32
+ "url": "https://github.com/kingslimes/next-style/issues"
33
33
  },
34
- "homepage": "https://github.com/kingslimes/zed-style#readme",
34
+ "homepage": "https://github.com/kingslimes/next-style#readme",
35
35
  "keywords": [
36
36
  "css",
37
37
  "css-in-js",
@@ -57,7 +57,7 @@
57
57
  "autoprefixer": "^10"
58
58
  },
59
59
  "devDependencies": {
60
- "csstype": "^3"
60
+ "csstype": "latest"
61
61
  },
62
62
  "engines": {
63
63
  "bun": ">=1.0.0",