next-style 1.1.0 → 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,230 +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 zed-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 zed-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 "zed-style"
58
+ For most **Next.js applications**, you **do not need to install these manually**.
43
59
 
44
- const style = new NextStyle()
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",
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
52
67
 
53
- _hover: {
54
- backgroundColor: "#222"
55
- },
68
+ If required, install them manually:
56
69
 
57
- _md: {
58
- padding: "16px"
59
- }
60
- })
70
+ ```sh
71
+ npm install react postcss autoprefixer
61
72
  ```
62
73
 
63
- Apply the generated class name to your component:
74
+ ---
75
+
76
+ ## Basic Usage (Page Scoped)
64
77
 
65
- ```tsx
66
- export default function App() {
78
+ Create a `NextStyle` instance **inside the page or component**.
79
+ You may optionally provide a **custom prefix** to control generated class names.
80
+
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
+ })
67
92
  return (
68
93
  <>
69
- <div className={className}>
70
- Hello NextStyle
71
- </div>
72
-
73
- <style.Provider />
94
+ <style.StyleProvider />
95
+ <button className={ button }>Click me</button>
74
96
  </>
75
97
  )
76
98
  }
77
99
  ```
78
100
 
79
- ---
101
+ Generated class names will look like:
80
102
 
81
- ## Style Object API
103
+ ```txt
104
+ home_ab12cd3
105
+ ```
82
106
 
83
- ### 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
84
112
 
85
- All standard CSS properties are supported and fully typed:
86
113
 
87
- ```ts
88
- {
89
- margin: "8px",
90
- fontSize: "14px",
91
- backgroundColor: "#000"
92
- }
93
- ```
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
94
122
 
95
- Types are derived from `csstype`, ensuring correctness and editor autocomplete.
123
+ This ensures:
124
+ - No stale CSS after route changes
125
+ - No style conflicts between pages
126
+ - Predictable SSR and CSR behavior
96
127
 
97
128
  ---
98
129
 
99
- ### Pseudo Selectors
130
+ ## Pseudo Selectors
100
131
 
101
- Use underscored keys for pseudo selectors:
132
+ Pseudo selectors are defined using keys prefixed with `_`.
102
133
 
103
134
  ```ts
104
- {
135
+ const card = style.css({
136
+ backgroundColor: "#fff",
137
+ transition: "0.2s ease",
105
138
  _hover: {
106
- opacity: 0.8
107
- },
108
- _focus: {
109
- outline: "2px solid blue"
139
+ backgroundColor: "#f5f5f5"
110
140
  },
111
141
  _active: {
112
142
  transform: "scale(0.98)"
113
143
  }
114
- }
144
+ })
115
145
  ```
116
146
 
117
- Supported pseudos:
147
+ Supported pseudo selectors:
118
148
  - `_hover`
119
149
  - `_focus`
120
150
  - `_active`
121
151
 
122
152
  ---
123
153
 
124
- ### Responsive Media Queries
154
+ ## Responsive Styles (Media Queries)
125
155
 
126
- NextStyle provides built-in responsive keys:
156
+ Built-in breakpoints:
127
157
 
128
- | Key | Media Query |
129
- |------|-------------|
130
- | `_sm` | `(min-width: 640px)` |
131
- | `_md` | `(min-width: 768px)` |
132
- | `_lg` | `(min-width: 1024px)` |
133
- | `_xl` | `(min-width: 1280px)` |
134
- | `_xxl` | `(min-width: 1536px)` |
135
-
136
- 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
137
163
 
138
164
  ```ts
139
- {
140
- fontSize: "14px",
165
+ const box = style.css({
166
+ width: 100,
167
+ _md: {
168
+ width: 200
169
+ },
141
170
  _lg: {
142
- fontSize: "18px"
171
+ width: 300
143
172
  }
144
- }
173
+ })
145
174
  ```
146
175
 
147
- ---
148
-
149
- ## `<style.Provider />`
176
+ Media queries can be nested and are automatically merged.
150
177
 
151
- The `Provider` component injects all generated CSS rules into a `<style>` tag.
178
+ ---
152
179
 
153
- Important notes:
180
+ ## Global Styles (Page Scoped)
154
181
 
155
- - It must be rendered **once per style instance**
156
- - It should be rendered **after** calling `css(...)`
157
- - It performs no side effects if no styles were generated
182
+ Global styles are **scoped to the current page**.
158
183
 
159
- ```tsx
160
- <style.Provider />
184
+ ```ts
185
+ style.global("body", {
186
+ margin: 0,
187
+ fontFamily: "Inter, sans-serif"
188
+ })
161
189
  ```
162
190
 
191
+ These styles are removed automatically when the page unmounts.
192
+
163
193
  ---
164
194
 
165
- ## Hashing Strategy
195
+ ## Keyframes
166
196
 
167
- Class names are generated using a deterministic hash based on the style object:
197
+ Create animations using `keyframes()`:
168
198
 
169
- ```text
170
- zed_x9k3a2m1
199
+ ```ts
200
+ const fadeIn = style.keyframes({
201
+ from: { opacity: 0 },
202
+ to: { opacity: 1 }
203
+ })
171
204
  ```
172
205
 
173
- This ensures:
174
- - Stable class names
175
- - No duplicates
176
- - 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.
177
213
 
178
214
  ---
179
215
 
180
- ## Runtime Behavior
216
+ ## Font Face
181
217
 
182
- - CSS is generated lazily on first usage
183
- - Rules are cached in memory
184
- - PostCSS transformation runs only once per unique rule
185
- - No DOM mutation outside React rendering
218
+ Declare fonts using `fontFace()`:
186
219
 
187
- ---
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
+ ```
188
229
 
189
- ## Type Safety
230
+ Font-face rules are injected only for the current page.
190
231
 
191
- NextStyle provides full TypeScript support:
232
+ ---
192
233
 
193
- - All CSS properties are typed
194
- - Invalid properties are caught at compile time
195
- - Media and pseudo keys are strictly controlled
234
+ ## Deterministic Hashing
196
235
 
197
- 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
198
240
 
199
241
  ---
200
242
 
201
- ## Requirements
243
+ ## Performance
202
244
 
203
- ### 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
204
249
 
205
- - React >= 18
206
- - PostCSS >= 8
207
- - Autoprefixer >= 10
250
+ Ideal for:
251
+ - Next.js App Router
252
+ - Page-level isolation
253
+ - Component-driven design systems
208
254
 
209
- ### Runtime Environment
255
+ ---
210
256
 
211
- - Node.js >= 18
212
- - Bun >= 1.0.0
257
+ ## Package Information
213
258
 
214
- ---
259
+ - Name: `next-style`
260
+ - Version: `1.1.2`
261
+ - License: MIT
262
+ - Module type: ESM
263
+ - Side effects: false
215
264
 
216
- ## License
265
+ Repository:
266
+ https://github.com/kingslimes/next-style
217
267
 
218
- MIT © kingslimes
268
+ Issues:
269
+ https://github.com/kingslimes/next-style/issues
219
270
 
220
271
  ---
221
272
 
222
- ## Philosophy
273
+ ## Roadmap
223
274
 
224
- NextStyle is intentionally minimal.
275
+ - `&` selector nesting
276
+ - `_dark` / `_light` helpers
277
+ - `@layer` support
278
+ - SSR collection utilities
279
+ - Framework adapters (Solid, Preact)
225
280
 
226
- - No build-time CSS extraction
227
- - No global runtime side effects
228
- - No magic conventions
281
+ ---
229
282
 
230
- 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,30 +69,75 @@ 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;
66
77
  }
67
78
  export class NextStyle {
68
79
  rules = new Map();
69
- constructor(prefix = "zed") {
80
+ constructor(prefix = "next") {
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.0",
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",