mintcss 0.1.0 → 0.2.0

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,8 +1,6 @@
1
1
  # mintcss
2
2
 
3
- Small, dependency-free CSS-in-JS for browser applications. Extracted from
4
- [Salvia](https://github.com/nexryai/rosmarinus/tree/main/salvia), with no React
5
- runtime or type dependency. MIT licensed.
3
+ Small, dependency-free CSS-in-JS with optional Next.js App Router integration.
6
4
 
7
5
  ## Install
8
6
 
@@ -12,7 +10,8 @@ npm install mintcss
12
10
 
13
11
  mintcss is an ESM package with bundled TypeScript declarations. It targets modern
14
12
  browsers with `String.replaceAll` support. Importing it does not touch the DOM;
15
- calling its styling functions requires a browser `document` with a `head`.
13
+ the top-level styling functions require a browser `document` with a `head`.
14
+ For server rendering, use `createStyleRegistry` or the Next.js integration below.
16
15
 
17
16
  ## Usage
18
17
 
@@ -55,6 +54,96 @@ export function Button() {
55
54
  }
56
55
  ```
57
56
 
57
+ ## Next.js App Router
58
+
59
+ The optional `mintcss/next` entry point supports Next.js 14–16 with React 18.2 or
60
+ 19. React and Next.js are optional peer dependencies; the core entry point does
61
+ not import either framework.
62
+
63
+ Wrap the app in `MintStyleRegistry` in your root layout. The layout can remain a
64
+ Server Component:
65
+
66
+ ```tsx
67
+ // app/layout.tsx
68
+ import type { ReactNode } from "react";
69
+ import { MintStyleRegistry } from "mintcss/next";
70
+
71
+ export default function RootLayout({ children }: { children: ReactNode }) {
72
+ return (
73
+ <html lang="en">
74
+ <body>
75
+ <MintStyleRegistry>{children}</MintStyleRegistry>
76
+ </body>
77
+ </html>
78
+ );
79
+ }
80
+ ```
81
+
82
+ In Client Components, obtain the styling functions from `useMintcss` and call
83
+ them during rendering:
84
+
85
+ ```tsx
86
+ // app/button.tsx
87
+ "use client";
88
+
89
+ import { useMintcss } from "mintcss/next";
90
+
91
+ export function Button() {
92
+ const { css, globalCss, keyframes } = useMintcss();
93
+ globalCss({ body: { margin: 0 } });
94
+ const fade = keyframes({ from: { opacity: 0 }, to: { opacity: 1 } });
95
+ const button = css({
96
+ backgroundColor: "#f4bd36",
97
+ borderRadius: 8,
98
+ animation: `${fade} 150ms ease-out`,
99
+ "&:hover": { opacity: 0.8 },
100
+ });
101
+ return <button type="button" className={button}>Hello</button>;
102
+ }
103
+ ```
104
+
105
+ The registry collects CSS separately for each server render and uses Next.js's
106
+ [`useServerInsertedHTML`](https://nextjs.org/docs/app/guides/css-in-js) integration
107
+ to emit new rules as content streams. The browser reuses those rules during
108
+ hydration and inserts new rules for subsequent renders and navigation. The same
109
+ styles produce the same identifiers on the server and client.
110
+
111
+ Use the hook inside Client Components under the registry. Module-level calls to
112
+ the browser-only `css`, `globalCss`, or `keyframes` exports still require a DOM,
113
+ even in files with `"use client"`, because Next.js also renders Client Components
114
+ on the server. Style objects can be declared at module scope, then passed to the
115
+ hook's functions during rendering. Server Components cannot use `useMintcss`.
116
+ The automatic integration targets the App Router; Pages Router applications
117
+ need their own SSR collection using the API below.
118
+
119
+ For a nonce-based CSP, pass the request's nonce as
120
+ `<MintStyleRegistry nonce={nonce}>`. It is applied to server-generated style tags
121
+ and new browser style tags. Configure the corresponding CSP in your app.
122
+
123
+ ## Server rendering
124
+
125
+ `createStyleRegistry({ nonce? })` provides `css`, `globalCss`, and `keyframes`
126
+ without requiring a DOM. Create a separate registry for every server render and
127
+ use its functions throughout that render; do not share a mutable registry
128
+ between requests.
129
+
130
+ ```ts
131
+ import { createStyleRegistry } from "mintcss";
132
+
133
+ const registry = createStyleRegistry();
134
+ const button = registry.css({ color: "red" });
135
+ const styles = registry.getStyles();
136
+ // styles: { ids: string[], css: string }
137
+ // Render a <style data-mintcss={styles.ids.join(" ")}> containing styles.css
138
+ // in the document head alongside the HTML using button as its class name.
139
+ ```
140
+
141
+ `getStyles()` returns all collected rules in insertion order. `flush()` returns
142
+ only the rules collected since the previous flush, retaining deduplication for
143
+ later streaming chunks. Include `data-mintcss` with the emitted identifiers so
144
+ browser calls can reuse the SSR styles. In the browser, registry styling calls
145
+ also insert rules into the document head.
146
+
58
147
  ## API
59
148
 
60
149
  - `css(styles: CSSObject): string` inserts rules and returns a deterministic
@@ -88,60 +177,12 @@ are implementation details and should not be persisted or hard-coded.
88
177
 
89
178
  ## Limits
90
179
 
91
- This is a browser runtime library: there is no SSR stylesheet collection,
92
- hydration API, build-time extraction, automatic vendor prefixing, or CSP nonce
93
- configuration. Styles accumulate for the lifetime of the document; prefer
94
- reusing stable style objects and CSS variables for continuously changing values.
95
- Strict CSP deployments must permit the generated inline stylesheet.
180
+ There is no build-time extraction or automatic vendor prefixing. Styles accumulate
181
+ for the lifetime of the document; prefer reusing stable style objects and CSS
182
+ variables for continuously changing values. Strict CSP deployments must permit
183
+ the generated inline stylesheet, for example with the registry nonce option.
96
184
 
97
185
  The serializer rejects malformed properties and rules, non-finite numeric
98
186
  values, and certain injection-like strings. It is **not an untrusted-input
99
187
  sanitizer**: pass application-authored styles and validate external data before
100
188
  using it, particularly URLs. It does not enforce an external URL policy.
101
-
102
- ## Tests
103
-
104
- Tests with a DOM environment can reset the shared stylesheet between cases:
105
-
106
- ```ts
107
- import { afterEach } from "vitest";
108
- import { __resetCssForTests } from "mintcss/testing";
109
-
110
- afterEach(__resetCssForTests);
111
- ```
112
-
113
- This helper removes all mintcss rules in the current document. Use it only in tests.
114
-
115
- ## Development and publishing
116
-
117
- Use Node.js 22.12 or later:
118
-
119
- ```sh
120
- npm ci
121
- npm run lint
122
- npm run check
123
- npm test
124
- npm run test:package
125
- ```
126
-
127
- `test:package` builds a real npm tarball, installs it into a temporary consumer,
128
- and checks its ESM exports, TypeScript declarations, and shipped files without
129
- React. CI runs these checks on Node.js 22 and 24. The Biome configuration and
130
- version are copied from Salvia; package-internal `#` imports keep the same rule
131
- against relative imports.
132
-
133
- For the first release, verify the package name is still available and sign in
134
- to an npm account authorized to publish it:
135
-
136
- ```sh
137
- npm view mintcss
138
- npm login
139
- npm publish --dry-run
140
- npm publish
141
- ```
142
-
143
- The initial version is `0.1.0`. `prepack` builds `dist`; `prepublishOnly` runs the
144
- checks before publishing. Only compiled JavaScript, declarations, README,
145
- LICENSE, and package metadata are shipped. Later releases must increment the
146
- version before publishing. Publishing is manual; a successful CI run does not
147
- publish a release.
package/dist/core.d.ts CHANGED
@@ -3,7 +3,24 @@ export type CSSObject = {
3
3
  [key: string]: CSSValue | CSSObject | undefined;
4
4
  };
5
5
  export type CSSDeclarations = Record<string, CSSValue | undefined>;
6
- export declare const css: (object: CSSObject) => string;
7
- export declare const keyframes: (frames: Record<string, CSSDeclarations>) => string;
8
- export declare const globalCss: (rules: Record<string, CSSObject>) => void;
6
+ declare const createStyleApi: (insert: (id: string, rule: string) => void) => {
7
+ css: (object: CSSObject) => string;
8
+ keyframes: (frames: Record<string, CSSDeclarations>) => string;
9
+ globalCss: (rules: Record<string, CSSObject>) => void;
10
+ };
11
+ export declare const css: (object: CSSObject) => string, keyframes: (frames: Record<string, CSSDeclarations>) => string, globalCss: (rules: Record<string, CSSObject>) => void;
12
+ export type StyleSnapshot = {
13
+ ids: string[];
14
+ css: string;
15
+ };
16
+ export type StyleApi = ReturnType<typeof createStyleApi>;
17
+ export type StyleRegistry = StyleApi & {
18
+ getStyles: () => StyleSnapshot;
19
+ flush: () => StyleSnapshot;
20
+ };
21
+ /** Create a registry per server render. Browser calls also insert into the DOM. */
22
+ export declare const createStyleRegistry: (options?: {
23
+ nonce?: string;
24
+ }) => StyleRegistry;
9
25
  export declare const __resetCssForTests: () => void;
26
+ export {};
package/dist/core.js CHANGED
@@ -49,6 +49,7 @@ const declaration = (property, value) => {
49
49
  return `${hyphenate(property)}:${serialized};`;
50
50
  };
51
51
  const serializeRule = (selector, object) => {
52
+ assertSafe(selector, "selector");
52
53
  let declarations = "";
53
54
  let nested = "";
54
55
  for (const key of Object.keys(object).sort()) {
@@ -88,7 +89,7 @@ const hash = (value) => {
88
89
  }
89
90
  return (result >>> 0).toString(36);
90
91
  };
91
- const registry = () => {
92
+ const registry = (nonce) => {
92
93
  if (typeof document === "undefined")
93
94
  throw new Error("mintcss requires a browser document");
94
95
  if (styleElement?.isConnected && styleElement.ownerDocument === document)
@@ -97,12 +98,21 @@ const registry = () => {
97
98
  styleElement = existing ?? document.createElement("style");
98
99
  if (!existing) {
99
100
  styleElement.setAttribute(STYLE_ATTRIBUTE, "");
101
+ if (nonce !== undefined)
102
+ styleElement.nonce = nonce;
100
103
  document.head.append(styleElement);
101
104
  }
102
105
  return styleElement;
103
106
  };
104
- const insert = (id, rule) => {
105
- const target = registry();
107
+ const insert = (id, rule, nonce) => {
108
+ if (typeof document === "undefined")
109
+ throw new Error("mintcss requires a browser document; use createStyleRegistry for SSR or useMintcss in Next.js");
110
+ // Streaming SSR can emit several style elements. Consult all of them.
111
+ for (const element of document.head.querySelectorAll(`style[${STYLE_ATTRIBUTE}]`)) {
112
+ if ((element.dataset.mintcss ?? "").split(" ").includes(id))
113
+ return;
114
+ }
115
+ const target = registry(nonce);
106
116
  // The DOM is authoritative across module reloads and stylesheet replacement.
107
117
  const registeredInDOM = new Set((target.dataset.mintcss ?? "").split(" ").filter(Boolean));
108
118
  if (registeredInDOM.has(id))
@@ -111,46 +121,75 @@ const insert = (id, rule) => {
111
121
  target.dataset.mintcss = [...registeredInDOM].join(" ");
112
122
  target.append(document.createTextNode(rule));
113
123
  };
114
- export const css = (object) => {
115
- const id = `mint-${hash(canonicalize(object))}`;
116
- insert(id, serializeRule(`.${id}`, object));
117
- return id;
118
- };
119
- export const keyframes = (frames) => {
120
- const object = frames;
121
- const name = `mint-kf-${hash(canonicalize(object))}`;
122
- const body = Object.entries(object)
123
- .sort(([left], [right]) => {
124
- const position = (step) => (step === "from" ? 0 : step === "to" ? 100 : Number.parseInt(step, 10));
125
- return position(left) - position(right);
126
- })
127
- .map(([step, properties]) => {
128
- if (!/^(from|to|(?:\d|[1-9]\d|100)%)$/.test(step))
129
- throw new Error(`Invalid keyframe step: ${step}`);
130
- return serializeRule(step, properties);
131
- })
132
- .join("");
133
- insert(name, `@keyframes ${name}{${body}}`);
134
- return name;
135
- };
136
- export const globalCss = (rules) => {
137
- const output = Object.entries(rules)
138
- .map(([selector, object]) => {
139
- if (selector.startsWith("@media") || selector.startsWith("@supports")) {
140
- assertConditionalRule(selector);
141
- return `${selector}{${Object.entries(object)
142
- .map(([nestedSelector, nestedObject]) => {
143
- assertSafe(nestedSelector, "selector");
144
- return serializeRule(nestedSelector, nestedObject);
145
- })
146
- .join("")}}`;
147
- }
148
- assertSafe(selector, "selector");
149
- return serializeRule(selector, object);
150
- })
151
- .join("");
152
- const id = `mint-global-${hash(output)}`;
153
- insert(id, output);
124
+ const createStyleApi = (insert) => ({
125
+ css: (object) => {
126
+ const id = `mint-${hash(canonicalize(object))}`;
127
+ insert(id, serializeRule(`.${id}`, object));
128
+ return id;
129
+ },
130
+ keyframes: (frames) => {
131
+ const object = frames;
132
+ const name = `mint-kf-${hash(canonicalize(object))}`;
133
+ const body = Object.entries(object)
134
+ .sort(([left], [right]) => {
135
+ const position = (step) => (step === "from" ? 0 : step === "to" ? 100 : Number.parseInt(step, 10));
136
+ return position(left) - position(right);
137
+ })
138
+ .map(([step, properties]) => {
139
+ if (!/^(from|to|(?:\d|[1-9]\d|100)%)$/.test(step))
140
+ throw new Error(`Invalid keyframe step: ${step}`);
141
+ return serializeRule(step, properties);
142
+ })
143
+ .join("");
144
+ insert(name, `@keyframes ${name}{${body}}`);
145
+ return name;
146
+ },
147
+ globalCss: (rules) => {
148
+ const output = Object.entries(rules)
149
+ .map(([selector, object]) => {
150
+ if (selector.startsWith("@media") || selector.startsWith("@supports")) {
151
+ assertConditionalRule(selector);
152
+ return `${selector}{${Object.entries(object)
153
+ .map(([nestedSelector, nestedObject]) => {
154
+ assertSafe(nestedSelector, "selector");
155
+ return serializeRule(nestedSelector, nestedObject);
156
+ })
157
+ .join("")}}`;
158
+ }
159
+ assertSafe(selector, "selector");
160
+ return serializeRule(selector, object);
161
+ })
162
+ .join("");
163
+ const id = `mint-global-${hash(output)}`;
164
+ insert(id, output);
165
+ },
166
+ });
167
+ export const { css, keyframes, globalCss } = createStyleApi(insert);
168
+ /** Create a registry per server render. Browser calls also insert into the DOM. */
169
+ export const createStyleRegistry = (options = {}) => {
170
+ const rules = new Map();
171
+ const pending = new Map();
172
+ const snapshot = (entries) => ({
173
+ ids: [...entries.keys()],
174
+ // Rules are validated by the serializer, including closing style tags.
175
+ css: [...entries.values()].join(""),
176
+ });
177
+ return {
178
+ ...createStyleApi((id, rule) => {
179
+ if (typeof document !== "undefined")
180
+ insert(id, rule, options.nonce);
181
+ if (rules.has(id))
182
+ return;
183
+ rules.set(id, rule);
184
+ pending.set(id, rule);
185
+ }),
186
+ getStyles: () => snapshot(rules),
187
+ flush: () => {
188
+ const styles = snapshot(pending);
189
+ pending.clear();
190
+ return styles;
191
+ },
192
+ };
154
193
  };
155
194
  export const __resetCssForTests = () => {
156
195
  document.head.querySelector(`style[${STYLE_ATTRIBUTE}]`)?.remove();
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export type { CSSDeclarations, CSSObject, CSSValue } from "#core";
2
- export { css, globalCss, keyframes } from "#core";
1
+ export type { CSSDeclarations, CSSObject, CSSValue, StyleApi, StyleRegistry, StyleSnapshot } from "#core";
2
+ export { createStyleRegistry, css, globalCss, keyframes } from "#core";
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export { css, globalCss, keyframes } from "#core";
1
+ export { createStyleRegistry, css, globalCss, keyframes } from "#core";
package/dist/next.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { type ReactNode } from "react";
2
+ import { type StyleApi, type StyleRegistry } from "#core";
3
+ export type MintStyleRegistryProps = {
4
+ children?: ReactNode;
5
+ nonce?: string;
6
+ };
7
+ /** Collect styles for each server render and flush them before streamed content. */
8
+ export declare function MintStyleRegistry({ children, nonce }: MintStyleRegistryProps): import("react").FunctionComponentElement<import("react").ProviderProps<StyleRegistry | null>>;
9
+ /** Use inside a Client Component wrapped in MintStyleRegistry. */
10
+ export declare function useMintcss(): StyleApi;
package/dist/next.js ADDED
@@ -0,0 +1,27 @@
1
+ "use client";
2
+ import { useServerInsertedHTML } from "next/navigation.js";
3
+ import { createContext, createElement, useContext, useState } from "react";
4
+ import { createStyleRegistry } from "#core";
5
+ const MintContext = createContext(null);
6
+ /** Collect styles for each server render and flush them before streamed content. */
7
+ export function MintStyleRegistry({ children, nonce }) {
8
+ const [registry] = useState(() => createStyleRegistry({ nonce }));
9
+ useServerInsertedHTML(() => {
10
+ const styles = registry.flush();
11
+ if (!styles.ids.length)
12
+ return null;
13
+ return createElement("style", {
14
+ "data-mintcss": styles.ids.join(" "),
15
+ nonce,
16
+ dangerouslySetInnerHTML: { __html: styles.css },
17
+ });
18
+ });
19
+ return createElement(MintContext.Provider, { value: registry }, children);
20
+ }
21
+ /** Use inside a Client Component wrapped in MintStyleRegistry. */
22
+ export function useMintcss() {
23
+ const registry = useContext(MintContext);
24
+ if (!registry)
25
+ throw new Error("useMintcss must be used inside MintStyleRegistry");
26
+ return registry;
27
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mintcss",
3
- "version": "0.1.0",
4
- "description": "Small, dependency-free CSS-in-JS for browser applications, extracted from Salvia.",
3
+ "version": "0.2.0",
4
+ "description": "Small, dependency-free CSS-in-JS styling library for React.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "nexryai",
@@ -25,7 +25,11 @@
25
25
  "types": "./dist/testing.d.ts",
26
26
  "import": "./dist/testing.js"
27
27
  },
28
- "./package.json": "./package.json"
28
+ "./package.json": "./package.json",
29
+ "./next": {
30
+ "types": "./dist/next.d.ts",
31
+ "import": "./dist/next.js"
32
+ }
29
33
  },
30
34
  "files": ["dist", "README.md", "LICENSE"],
31
35
  "sideEffects": false,
@@ -41,18 +45,37 @@
41
45
  "test": "vitest run",
42
46
  "test:package": "node scripts/test-package.mjs",
43
47
  "prepack": "npm run build",
44
- "prepublishOnly": "npm run lint && npm run check && npm test && npm run test:package"
48
+ "prepublishOnly": "npm run lint && npm run check && npm test && npm run test:package && npm run test:next",
49
+ "test:next": "node scripts/test-next.mjs"
45
50
  },
46
51
  "devDependencies": {
47
52
  "@biomejs/biome": "2.5.11",
48
53
  "@types/node": "^24.13.3",
54
+ "@types/react": "^19.3.0",
55
+ "@types/react-dom": "^19.3.0",
49
56
  "jsdom": "30.1.2",
57
+ "next": "^16.4.0",
58
+ "react": "^19.3.0",
59
+ "react-dom": "^19.3.0",
50
60
  "typescript": "~6.0.2",
51
61
  "vitest": "^4.1.11"
52
62
  },
53
63
  "imports": {
54
64
  "#core": "./dist/core.js",
55
65
  "#mintcss": "./dist/index.js",
56
- "#testing": "./dist/testing.js"
66
+ "#testing": "./dist/testing.js",
67
+ "#next": "./dist/next.js"
68
+ },
69
+ "peerDependencies": {
70
+ "next": "^14.0.0 || ^15.0.0 || ^16.0.0",
71
+ "react": "^18.2.0 || ^19.0.0"
72
+ },
73
+ "peerDependenciesMeta": {
74
+ "next": {
75
+ "optional": true
76
+ },
77
+ "react": {
78
+ "optional": true
79
+ }
57
80
  }
58
81
  }