Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .prettierrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@
"bracketSameLine": true,
"embeddedLanguageFormatting": "auto",
"singleAttributePerLine": true
}
}
29 changes: 23 additions & 6 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// eslint.config.js - ESLint 9 flat-config, JS + CJS
import js from '@eslint/js';
import nodePlugin from 'eslint-plugin-node';
import nodePlugin from 'eslint-plugin-n';
import importPlugin from 'eslint-plugin-import';
import tsParser from '@typescript-eslint/parser';

const nodeGlobals = {
require: 'readonly',
Expand Down Expand Up @@ -29,6 +30,8 @@ const browserGlobals = {
clearTimeout: 'readonly',
clearInterval: 'readonly',
console: 'readonly',
HTMLElement: 'readonly',
Element: 'readonly',
};

export default [
Expand All @@ -41,10 +44,10 @@ export default [
/* 3 - CommonJS (*.cjs) */
{
files: ['**/*.cjs'],
plugins: { node: nodePlugin, import: importPlugin },
plugins: { n: nodePlugin, import: importPlugin },
languageOptions: { ecmaVersion: 'latest', sourceType: 'script', globals: nodeGlobals },
rules: {
'node/no-unsupported-features/es-syntax': 'off',
'n/no-unsupported-features/es-syntax': 'off',
'import/no-unresolved': 'error', // Catch unresolved imports
'import/named': 'error', // Catch missing named exports
'import/default': 'error', // Catch missing default exports
Expand All @@ -55,16 +58,30 @@ export default [
/* 4 - ES-module / browser (*.js) */
{
files: ['**/*.js'],
plugins: { node: nodePlugin, import: importPlugin },
plugins: { n: nodePlugin, import: importPlugin },
languageOptions: { ecmaVersion: 'latest', sourceType: 'module', globals: { ...nodeGlobals, ...browserGlobals } },
rules: {
'node/no-unsupported-features/es-syntax': 'off',
'n/no-unsupported-features/es-syntax': 'off',
'import/no-unresolved': 'error',
'import/named': 'error',
'import/default': 'error',
'import/no-absolute-path': 'error',
},
},

/* 5 - JSX files */
/* 5 - TypeScript files (*.ts, *.tsx) */
{
files: ['**/*.ts', '**/*.tsx'],
plugins: { n: nodePlugin, import: importPlugin },
languageOptions: { parser: tsParser, ecmaVersion: 'latest', sourceType: 'module', globals: { ...nodeGlobals, ...browserGlobals } },
rules: {
'n/no-unsupported-features/es-syntax': 'off',
'import/no-unresolved': 'off', // TypeScript handles this
'import/named': 'off', // TypeScript handles this
'import/default': 'off', // TypeScript handles this
'import/no-absolute-path': 'error',
'no-unused-vars': 'off', // TypeScript handles this better
'no-undef': 'off', // TypeScript handles this
},
},
];
54 changes: 1 addition & 53 deletions examples/demo/src/utils/env.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1 @@
type RenderEnv = 'client' | 'server';
type HandoffState = 'pending' | 'complete';

class Env {
current: RenderEnv = this.detect();
handoffState: HandoffState = 'pending';
currentId = 0;

set(env: RenderEnv): void {
if (this.current === env) return;

this.handoffState = 'pending';
this.currentId = 0;
this.current = env;
}

reset(): void {
this.set(this.detect());
}

nextId() {
return ++this.currentId;
}

get isServer(): boolean {
return this.current === 'server';
}

get isClient(): boolean {
return this.current === 'client';
}

private detect(): RenderEnv {
if (typeof window === 'undefined' || typeof document === 'undefined') {
return 'server';
}

return 'client';
}

handoff(): void {
if (this.handoffState === 'pending') {
this.handoffState = 'complete';
}
}

get isHandoffComplete(): boolean {
return this.handoffState === 'complete';
}
}

// eslint-disable-next-line prefer-const
export let env = new Env();
export const env = { isClient: typeof window !== 'undefined', isServer: typeof window === 'undefined' };
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,15 @@
"devDependencies": {
"@eslint/js": "^9.32.0",
"@types/node": "^24.1.0",
"@typescript-eslint/eslint-plugin": "^8.38.0",
"@typescript-eslint/parser": "^8.38.0",
"esbuild": "^0.25.8",
"eslint": "^9.32.0",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-react-zero-ui": "workspace:*",
"eslint-plugin-n": "^17.21.3",
"prettier": "^3.6.2",
"release-please": "^17.1.1",
"tsx": "^4.20.3",
"typescript": "^5.9.2"
}
}
}
10 changes: 10 additions & 0 deletions packages/core/__tests__/e2e/next.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,16 @@ const scenarios = [
initialText: 'Closed',
toggledText: 'Open',
},
{
name: 'SSR Theme Toggle',
toggle: 'theme-ssr-toggle',
container: 'theme-ssr-container',
attr: 'data-theme-ssr',
initialValue: 'light',
toggledValue: 'dark',
initialText: 'Light',
toggledText: 'Dark',
},
];

test.describe.configure({ mode: 'serial' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@ export declare const bodyAttributes: {
"data-scope": "off" | "on";
"data-theme": "dark" | "light";
"data-theme-2": "dark" | "light";
"data-theme-ssr": "dark" | "light";
"data-theme-three": "dark" | "light";
"data-toggle-boolean": "false" | "true";
"data-toggle-function": "black" | "blue" | "green" | "red" | "white";
"data-use-effect-theme": "dark" | "light";
};

export declare const variantKeyMap: {
[key: string]: true;
};
18 changes: 18 additions & 0 deletions packages/core/__tests__/fixtures/next/.zero-ui/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,26 @@ export const bodyAttributes = {
"data-number": "1",
"data-theme": "light",
"data-theme-2": "light",
"data-theme-ssr": "light",
"data-theme-three": "light",
"data-toggle-boolean": "true",
"data-toggle-function": "white",
"data-use-effect-theme": "light"
};
export const variantKeyMap = {
"data-blur": true,
"data-blur-global": true,
"data-child": true,
"data-dialog": true,
"data-faq": true,
"data-mobile": true,
"data-number": true,
"data-scope": true,
"data-theme": true,
"data-theme-2": true,
"data-theme-ssr": true,
"data-theme-three": true,
"data-toggle-boolean": true,
"data-toggle-function": true,
"data-use-effect-theme": true
};
3 changes: 1 addition & 2 deletions packages/core/__tests__/fixtures/next/app/CssVarDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ export default function CssVarDemo({ index = 0 }) {
// 👇 pass `cssVar` flag to switch makeSetter into CSS-var mode
const [blur, setBlur] = useScopedUI<'0px' | '4px'>('blur', '0px', cssVar);
// global test

return (
return (
<div
ref={setBlur.ref} // element that owns --blur
data-testid={`demo-${index}`}
Expand Down
33 changes: 32 additions & 1 deletion packages/core/__tests__/fixtures/next/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import FAQ from './FAQ';
import { ChildComponent } from './ChildComponent';
import { ChildWithoutSetter } from './ChildWithoutSetter';
import CssVarDemo from './CssVarDemo';

import { zeroSSR } from '@react-zero-ui/core/experimental';
import ZeroUiRuntime from './zero-runtime';


export default function Page() {
const [scope, setScope] = useScopedUI<'off' | 'on'>('scope', 'off');
Expand All @@ -19,8 +23,8 @@ export default function Page() {
const [, setChildOpen] = useUI<'open' | 'closed'>('child', 'closed');

const [, setToggleFunction] = useUI<'white' | 'black'>('toggle-function', 'white');
const [, setGlobal] = useUI<'0px' | '4px'>('blur-global', '0px', cssVar);

const [global, setGlobal] = useUI<'0px' | '4px'>('blur-global', '0px', cssVar);

const toggleFunction = () => {
setToggleFunction((prev) => (prev === 'white' ? 'black' : 'white'));
Expand All @@ -30,6 +34,7 @@ export default function Page() {
<div
className="p-8 theme-light:bg-white theme-dark:bg-white bg-black relative"
data-testid="page-container">
<ZeroUiRuntime />
<h1 className="text-2xl font-bold py-5">Global State</h1>
<hr />
<div className=" space-y-4 border-2">
Expand All @@ -38,6 +43,32 @@ export default function Page() {

<hr className="my-8" />

<div
data-theme-ssr="light"
data-testid="theme-ssr-container"
className="theme-ssr-dark:bg-gray-900 theme-ssr-light:bg-gray-100 theme-ssr-dark:text-white theme-ssr-light:text-black">
<button
data-testid="theme-ssr-toggle"
className="border-2 border-red-500 theme-ssr-light:text-blue-500 theme-ssr-dark:text-red-500"
{...zeroSSR.onClick('theme-ssr', ['light', 'dark'])}>
Toggle SSR SAFE THEME
</button>
<div className="flex gap-2">
Theme:
<span
data-testid="theme-ssr-dark"
className="theme-ssr-dark:block hidden">
Dark
</span>
<span
data-testid="theme-ssr-light"
className="theme-ssr-light:block hidden">
Light
</span>
</div>
</div>
<hr className="my-8" />

<div
className="theme-light:bg-gray-100 theme-dark:bg-gray-900 theme-dark:text-white"
data-testid="theme-container">
Expand Down
13 changes: 13 additions & 0 deletions packages/core/__tests__/fixtures/next/app/zero-runtime.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
'use client';

/* ① import the generated defaults */
import { variantKeyMap } from '../.zero-ui/attributes';

/* ② activate the runtime shipped in the package */
import { activateZeroUiRuntime } from '@react-zero-ui/core/experimental/runtime';

activateZeroUiRuntime(variantKeyMap);

export default function ZeroUiRuntime() {
return null; // this component just runs the side effect
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ export declare const bodyAttributes: {
"data-toggle-function": "black" | "blue" | "green" | "red" | "white";
"data-use-effect-theme": "dark" | "light";
};

export declare const variantKeyMap: {
[key: string]: true;
};
13 changes: 13 additions & 0 deletions packages/core/__tests__/fixtures/vite/.zero-ui/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,16 @@ export const bodyAttributes = {
"data-toggle-function": "white",
"data-use-effect-theme": "light"
};
export const variantKeyMap = {
"data-child": true,
"data-faq": true,
"data-mobile": true,
"data-number": true,
"data-scope": true,
"data-theme": true,
"data-theme-2": true,
"data-theme-three": true,
"data-toggle-boolean": true,
"data-toggle-function": true,
"data-use-effect-theme": true
};
34 changes: 30 additions & 4 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"sideEffects": false,
"sideEffects": [
"./dist/experimental/runtime.js"
],
"files": [
"dist/**/*",
"README.md",
Expand All @@ -30,6 +32,30 @@
"types": "./dist/cli/init.d.ts",
"require": "./dist/cli/init.js",
"import": "./dist/cli/init.js"
},
"./experimental": {
"types": "./dist/experimental/index.d.ts",
"import": "./dist/experimental/index.js"
},
"./experimental/runtime": {
"import": "./dist/experimental/runtime.js"
},
"./experimental/init": {
"types": "./dist/experimental/InitZeroUI.d.ts",
"import": "./dist/experimental/InitZeroUI.js"
}
},
"typesVersions": {
"*": {
"experimental/init": [
"dist/experimental/InitZeroUI.d.ts"
],
"experimental/runtime": [
"dist/experimental/runtime.d.ts"
],
"experimental": [
"dist/experimental/index.d.ts"
]
}
},
"scripts": {
Expand Down Expand Up @@ -74,9 +100,9 @@
},
"peerDependencies": {
"@tailwindcss/postcss": "^4.1.10",
"postcss": "^8.5.5",
"react": ">=16.8.0",
"tailwindcss": "^4.1.10"
"tailwindcss": "^4.1.10",
"postcss": "^8.5.5"
},
"dependencies": {
"@babel/code-frame": "^7.27.1",
Expand All @@ -95,4 +121,4 @@
"@types/react": "^19.1.8",
"tsx": "^4.20.3"
}
}
}
5 changes: 4 additions & 1 deletion packages/core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ export const CONFIG = {
SUPPORTED_EXTENSIONS: { TYPESCRIPT: ['.ts', '.tsx'], JAVASCRIPT: ['.js', '.jsx'] },
HOOK_NAME: 'useUI',
LOCAL_HOOK_NAME: 'useScopedUI',
SSR_HOOK_NAME: 'zeroSSR',
SSR_HOOK_NAME_SCOPED: 'scopedZeroSSR',
IMPORT_NAME: '@react-zero-ui/core',
PLUGIN_NAME: 'postcss-react-zero-ui',
MIN_HOOK_ARGUMENTS: 2,
MAX_HOOK_ARGUMENTS: 2,
HEADER: '/* AUTO-GENERATED - DO NOT EDIT */',
ZERO_UI_DIR: '.zero-ui',
CONTENT: ['src/**/*.{ts,tsx,js,jsx}', 'app/**/*.{ts,tsx,js,jsx}', 'pages/**/*.{ts,tsx,js,jsx}'],
POSTCSS_PLUGIN: '@react-zero-ui/core/postcss',
VITE_PLUGIN: '@react-zero-ui/core/vite',
};
} as const;

export const IGNORE_DIRS = [
'**/node_modules/**',
Expand Down
Loading