diff --git a/.prettierignore b/.prettierignore index 3454d92..4cea0a3 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,3 @@ -/examples pnpm-lock.yaml packages/core/__tests__/fixtures/vite/vite.config.ts packages/core/__tests__/fixtures/next/tsconfig.json diff --git a/AGENTS.md b/AGENTS.md index 7a811ea..9c1a7f3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,3 +1,5 @@ + + # Guidelines for AI Agents in this Repo This repository contains **React Zero-UI**, a library for global UI state without React re-renders. @@ -5,15 +7,68 @@ Use these tips when working with the codebase or generating examples. ## How React Zero-UI works -1. `useUI()` writes to `document.body.dataset` using keys you specify (e.g. `theme` → `data-theme`). -2. Build tooling scans for all keys and values, generating CSS variants for each. -3. When a setter is called, the corresponding body attribute changes instantly with no React re-render. +1. `useUI()` writes to `document.body.dataset` using keys you specify. + +```tsx +const [staleValue, setValue] = useUI<'open' | 'closed'>('sidebar', 'closed'); +``` + +- `key` → becomes `data-{key}` on `` (e.g., `sidebar` → `data-sidebar="closed"`). +- `defaultValue` → used for SSR to avoid FOUC. +- The first value is **always stale** — do NOT rely on it for reactive updates. + +2. Consumption is done strictly with tailwind variant classNames: + +```html +
+``` + +3. Build-time tooling scans all `useUI()` keys and values, then generates matching Tailwind variants. + +4. At runtime, calling the setter updates the `data-*` attribute on `` immediately. No VDOM. No re-renders. + +--- + +## Best Practices for AI Agents + +- ✅ Use `useUI()` **only for UI state**: themes, layout flags, open/closed toggles, etc. +- ✅ Prefer **kebab-case keys**: e.g. `sidebar-open`, `theme-dark`. +- ✅ Always provide a `defaultValue`: prevents FOUC and enables SSR. +- ✅ Do **NOT** use the first value from `useUI()` for logic — it DOES NOT UPDATE. +- ✅ You can call setters **from anywhere** in the app — no prop drilling or context needed. +- ✅ Tailwind classes must use `key-value:` pattern: + + - `theme-dark:bg-black` + - `accent-blue:text-blue-500` + - `sidebar-open:translate-x-0` + +--- + +## Example: Toggle Theme + +```tsx +// Set state +const [, setTheme] = useUI<'light' | 'dark'>('theme', 'light'); +; +``` + +```html + +
+``` + +--- + +## What NOT to do + +- ❌ Don't use `useUI()` for business logic or data fetching +- ❌ Don't rely on the first tuple value for reactivity +- ❌ Don't use camelCase keys (will break variant generation) + +--- + +## Summary -## Best practices +**React Zero-UI is a ZERO re-render UI state engine with global state baked in.** It replaces traditional VDOM cycles with `data-*` attribute flips and compile-time CSS. No React context. No prop drilling. No runtime cost. -- Only use `useUI` for UI-only state (themes, flags, etc.). -- Prefer kebab-case keys (`sidebar-open`) so generated variants are predictable. -- Always pass a default value to `useUI(key, defaultValue)` to avoid flashes during SSR. -- The first value is ALWAYS STALE, do not use it if you need reactivity. -- Mutate the state anywhere in the app: `const [, setTheme] = useUI('theme', 'light');` then call `setTheme('dark')`. -- Compose Tailwind classes anywhere with the pattern `key-value:` like `theme-dark:bg-black`. +Think of it as writing atomic Tailwind variants for every UI state — but flipping them dynamically at runtime without re-rendering anything. diff --git a/examples/demo/.prettierignore b/examples/demo/.prettierignore deleted file mode 100644 index cab8289..0000000 --- a/examples/demo/.prettierignore +++ /dev/null @@ -1,27 +0,0 @@ -# package artifacts -node_modules/ -.next/ -dist/ -coverage/ -test-results/ -.pnpm-store/ -package-lock.json - -# logs / temp files -npm-debug.log* -yarn-error.log* -.DS_Store -**/playwright-report/ - -# IDE/editor -.vscode/ - -# tarballs produced during local tests -*.tgz - -# local scratch files -t.py -todo.md - -# keep these files -!next-env.d.ts diff --git a/examples/demo/.prettierrc.json b/examples/demo/.prettierrc.json deleted file mode 100644 index d74c679..0000000 --- a/examples/demo/.prettierrc.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "semi": true, - "trailingComma": "es5", - "singleQuote": true, - "tabWidth": 2, - "useTabs": false, - "printWidth": 160, - "endOfLine": "lf", - "arrowParens": "avoid", - "bracketSpacing": true, - "jsxBracketSameLine": false, - "singleAttributePerLine": true, - "plugins": [ - "prettier-plugin-tailwindcss" - ] -} \ No newline at end of file diff --git a/examples/demo/.zero-ui/attributes.js b/examples/demo/.zero-ui/attributes.js index 8363de9..ef1b0a2 100644 --- a/examples/demo/.zero-ui/attributes.js +++ b/examples/demo/.zero-ui/attributes.js @@ -4,7 +4,7 @@ export const bodyAttributes = { "data-active": "zero", "data-menu-open": "false", "data-mobile-menu": "closed", - "data-scrolled": "down", + "data-scrolled": "up", "data-theme": "light", "data-theme-test": "light" }; diff --git a/examples/demo/next.config.ts b/examples/demo/next.config.ts index 5e891cf..a67a28b 100644 --- a/examples/demo/next.config.ts +++ b/examples/demo/next.config.ts @@ -1,7 +1,7 @@ import type { NextConfig } from 'next'; const nextConfig: NextConfig = { - /* config options here */ + /* config options here */ }; export default nextConfig; diff --git a/examples/demo/package.json b/examples/demo/package.json index 6350dbf..f644810 100644 --- a/examples/demo/package.json +++ b/examples/demo/package.json @@ -1,37 +1,35 @@ { - "name": "react-zero", - "version": "0.1.0", - "private": true, - "type": "module", - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint", - "lint:fix": "next lint --fix", - "format": "prettier --write .", - "format:check": "prettier --check .", - "type-check": "tsc --noEmit", - "clean": "rm -rf .next" - }, - "dependencies": { - "@austinserb/react-zero-ui": "^1.0.19", - "@vercel/analytics": "^1.5.0", - "clsx": "^2.1.1", - "motion": "^12.16.0", - "next": "15.3.3", - "react": "^19.0.0", - "react-dom": "^19.0.0" - }, - "devDependencies": { - "@tailwindcss/postcss": "^4.1.10", - "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", - "postcss": "^8.5.5", - "prettier": "^3.5.3", - "prettier-plugin-tailwindcss": "^0.6.12", - "tailwindcss": "^4.1.10", - "typescript": "^5" - } -} \ No newline at end of file + "name": "react-zero", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "lint:fix": "next lint --fix", + "format": "prettier --write .", + "format:check": "prettier --check .", + "type-check": "tsc --noEmit", + "clean": "rm -rf .next" + }, + "dependencies": { + "@austinserb/react-zero-ui": "^1.0.21", + "@vercel/analytics": "^1.5.0", + "clsx": "^2.1.1", + "motion": "^12.16.0", + "next": "15.3.3", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.1.10", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "postcss": "^8.5.5", + "tailwindcss": "^4.1.10", + "typescript": "^5" + } +} diff --git a/examples/demo/postcss.config.mjs b/examples/demo/postcss.config.mjs index 0829fc6..2e0aa24 100644 --- a/examples/demo/postcss.config.mjs +++ b/examples/demo/postcss.config.mjs @@ -1,7 +1,5 @@ // postcss.config.mjs -const config = { - plugins: ['@austinserb/react-zero-ui/postcss', '@tailwindcss/postcss'], -}; +const config = { plugins: ['@austinserb/react-zero-ui/postcss', '@tailwindcss/postcss'] }; export default config; diff --git a/examples/demo/src/app/(test)/ReactState.tsx b/examples/demo/src/app/(test)/ReactState.tsx index 086b357..2c2c287 100644 --- a/examples/demo/src/app/(test)/ReactState.tsx +++ b/examples/demo/src/app/(test)/ReactState.tsx @@ -5,180 +5,168 @@ import { useState } from 'react'; import { useRenderTracker } from './ReactTracker'; export function TestComponentWithState() { - const ref = useRenderTracker('TestComponentWithState'); - const [accent, setAccent] = useState<'violet' | 'emerald' | 'amber'>('violet'); - const [theme, setTheme] = useState<'light' | 'dark'>('light'); - const [menuOpen, setMenuOpen] = useState(false); - - return ( -
-
- - - - -
- ); + const ref = useRenderTracker('TestComponentWithState'); + const [accent, setAccent] = useState<'violet' | 'emerald' | 'amber'>('violet'); + const [theme, setTheme] = useState<'light' | 'dark'>('light'); + const [menuOpen, setMenuOpen] = useState(false); + + return ( +
+
+ + + + +
+ ); } // Header Component function Header({ theme }: { theme: 'light' | 'dark' }) { - const ref = useRenderTracker('Header'); - - return ( -
-

React State Management

-

Reactive state management with React

-
- ); + const ref = useRenderTracker('Header'); + + return ( +
+

React State Management

+ +

Reactive state management with React

+
+ ); } // Theme Switcher Component function ThemeSwitcher({ theme, setTheme }: { theme: 'light' | 'dark'; setTheme: (t: 'light' | 'dark') => void }) { - const ref = useRenderTracker('ThemeSwitcher'); - - return ( -
- - -
- ); + const ref = useRenderTracker('ThemeSwitcher'); + + return ( +
+ + +
+ ); } // Accent Picker Component function AccentPicker({ - accent, - setAccent, - theme, + accent, + setAccent, + theme, }: { - accent: 'violet' | 'emerald' | 'amber'; - setAccent: (a: 'violet' | 'emerald' | 'amber') => void; - theme: 'light' | 'dark'; + accent: 'violet' | 'emerald' | 'amber'; + setAccent: (a: 'violet' | 'emerald' | 'amber') => void; + theme: 'light' | 'dark'; }) { - const ref = useRenderTracker('AccentPicker'); - - return ( -
-

Choose Accent

-
-
-
- ); + const ref = useRenderTracker('AccentPicker'); + + return ( +
+

Choose Accent

+
+
+
+ ); } // Interactive Card Component function InteractiveCard({ - theme, - menuOpen, - setMenuOpen, - accent, + theme, + menuOpen, + setMenuOpen, + accent, }: { - theme: 'light' | 'dark'; - menuOpen: boolean; - setMenuOpen: (open: boolean) => void; - accent: 'violet' | 'emerald' | 'amber'; + theme: 'light' | 'dark'; + menuOpen: boolean; + setMenuOpen: (open: boolean) => void; + accent: 'violet' | 'emerald' | 'amber'; }) { - const ref = useRenderTracker('InteractiveCard'); - - return ( -
-
-
-

Open Menu Demo

- - -
- - {/* Sliding Menu */} -
-
-

✨ This panel slides open and has to re-render!

-
-
-
-
- ); + const ref = useRenderTracker('InteractiveCard'); + + return ( +
+
+

Open Menu Demo

+ + +
+ + {/* Sliding Menu */} +
+
+

✨ This panel slides open and has to re-render!

+
+
+
+ ); } // State Display Component function StateDisplay({ theme, accent, menuOpen }: { theme: 'light' | 'dark'; accent: 'violet' | 'emerald' | 'amber'; menuOpen: boolean }) { - const ref = useRenderTracker('StateDisplay'); - - return ( -
-
-
theme: {theme}
-
accent: {accent}
-
menu: {menuOpen ? 'Open' : 'Closed'}
-
-
- ); + const ref = useRenderTracker('StateDisplay'); + + return ( +
+
+
theme: {theme}
+
accent: {accent}
+
menu: {menuOpen ? 'Open' : 'Closed'}
+
+
+ ); } diff --git a/examples/demo/src/app/(test)/ReactTracker.tsx b/examples/demo/src/app/(test)/ReactTracker.tsx index a4b5e53..bc106e3 100644 --- a/examples/demo/src/app/(test)/ReactTracker.tsx +++ b/examples/demo/src/app/(test)/ReactTracker.tsx @@ -5,40 +5,32 @@ import { AnimatePresence, motion } from 'motion/react'; import { useEffect, useRef, useSyncExternalStore } from 'react'; interface RenderMetrics { - count: number; - lastRenderTime: number; - totalRenderTime: number; + count: number; + lastRenderTime: number; + totalRenderTime: number; } // External store for render metrics class RenderStore { - private data = new Map(); - private listeners = new Set<() => void>(); - - subscribe = (listener: () => void) => { - this.listeners.add(listener); - return () => this.listeners.delete(listener); - }; - - getSnapshot = () => { - return this.data; - }; - - updateMetrics(componentName: string, renderTime: number) { - const existing = this.data.get(componentName) || { - count: 0, - lastRenderTime: 0, - totalRenderTime: 0, - }; - - this.data.set(componentName, { - count: existing.count + 1, - lastRenderTime: renderTime, - totalRenderTime: existing.totalRenderTime + renderTime, - }); - - this.listeners.forEach(listener => listener()); - } + private data = new Map(); + private listeners = new Set<() => void>(); + + subscribe = (listener: () => void) => { + this.listeners.add(listener); + return () => this.listeners.delete(listener); + }; + + getSnapshot = () => { + return this.data; + }; + + updateMetrics(componentName: string, renderTime: number) { + const existing = this.data.get(componentName) || { count: 0, lastRenderTime: 0, totalRenderTime: 0 }; + + this.data.set(componentName, { count: existing.count + 1, lastRenderTime: renderTime, totalRenderTime: existing.totalRenderTime + renderTime }); + + this.listeners.forEach(listener => listener()); + } } const renderStore = new RenderStore(); @@ -48,198 +40,194 @@ let isTrackerEnabled = true; const trackerListeners = new Set<() => void>(); const setTrackerEnabled = (enabled: boolean) => { - isTrackerEnabled = enabled; - trackerListeners.forEach(listener => listener()); + isTrackerEnabled = enabled; + trackerListeners.forEach(listener => listener()); }; // Flash animations store interface RenderFlash { - id: string; - x: number; - y: number; - width: number; - height: number; - renderTime: number; + id: string; + x: number; + y: number; + width: number; + height: number; + renderTime: number; } let flashStore: RenderFlash[] = []; let flashListeners = new Set<() => void>(); const addFlash = (element: HTMLElement, componentName: string, renderTime: number) => { - if (!isTrackerEnabled || !element) return; - - const rect = element.getBoundingClientRect(); - const flash: RenderFlash = { - id: `${componentName}-${Date.now()}-${Math.random()}`, - x: rect.left, - y: rect.top, - width: rect.width, - height: rect.height, - renderTime, - }; - - flashStore = [...flashStore, flash]; - flashListeners.forEach(listener => listener()); - - setTimeout(() => { - flashStore = flashStore.filter(f => f.id !== flash.id); - flashListeners.forEach(listener => listener()); - }, 1500); + if (!isTrackerEnabled || !element) return; + + const rect = element.getBoundingClientRect(); + const flash: RenderFlash = { + id: `${componentName}-${Date.now()}-${Math.random()}`, + x: rect.left, + y: rect.top, + width: rect.width, + height: rect.height, + renderTime, + }; + + flashStore = [...flashStore, flash]; + flashListeners.forEach(listener => listener()); + + setTimeout(() => { + flashStore = flashStore.filter(f => f.id !== flash.id); + flashListeners.forEach(listener => listener()); + }, 1500); }; // Standalone Render Tracker Component export function RenderTracker() { - const renderMetrics = useSyncExternalStore(renderStore.subscribe, renderStore.getSnapshot, renderStore.getSnapshot); - - const showTracker = useSyncExternalStore( - listener => { - trackerListeners.add(listener); - return () => trackerListeners.delete(listener); - }, - () => isTrackerEnabled, - () => isTrackerEnabled - ); - - const flashes = useSyncExternalStore( - listener => { - flashListeners.add(listener); - return () => flashListeners.delete(listener); - }, - () => flashStore, - () => flashStore - ); - - return ( - <> - {/* Render Metrics Display */} - - {renderMetrics?.size > 0 && ( - -
-

Renders

- -
- - - {Array.from(renderMetrics.entries()).map(([id, metrics]) => ( -
- {id.replace('Component', '').replace('Deep', 'D.')} - {metrics.count} -
- ))} -
- - {renderMetrics.size > 0 && ( -
- - Total: {Array.from(renderMetrics.values()).reduce((sum, m) => sum + m.count, 0)} - - - - {Array.from(renderMetrics.values()) - .reduce((sum, m) => sum + m.totalRenderTime, 0) - .toFixed(1)} - ms - - -
- )} -
- )} -
- - {/* Flash Overlays */} -
- - {showTracker && - flashes.map(flash => ( - - {/* Ripple effect */} - - - {/* Glow effect */} - - - {/* Render time badge */} - {flash.renderTime > 0 && ( - - {flash.renderTime.toFixed(2)}ms - - )} - - ))} - -
- - ); + const renderMetrics = useSyncExternalStore(renderStore.subscribe, renderStore.getSnapshot, renderStore.getSnapshot); + + const showTracker = useSyncExternalStore( + listener => { + trackerListeners.add(listener); + return () => trackerListeners.delete(listener); + }, + () => isTrackerEnabled, + () => isTrackerEnabled + ); + + const flashes = useSyncExternalStore( + listener => { + flashListeners.add(listener); + return () => flashListeners.delete(listener); + }, + () => flashStore, + () => flashStore + ); + + return ( + <> + {/* Render Metrics Display */} + + {renderMetrics?.size > 0 && ( + +
+

Renders

+ +
+ + + {Array.from(renderMetrics.entries()).map(([id, metrics]) => ( +
+ {id.replace('Component', '').replace('Deep', 'D.')} + {metrics.count} +
+ ))} +
+ + {renderMetrics.size > 0 && ( +
+ + Total: {Array.from(renderMetrics.values()).reduce((sum, m) => sum + m.count, 0)} + + + + {Array.from(renderMetrics.values()) + .reduce((sum, m) => sum + m.totalRenderTime, 0) + .toFixed(1)} + ms + + +
+ )} +
+ )} +
+ + {/* Flash Overlays */} +
+ + {showTracker && + flashes.map(flash => ( + + {/* Ripple effect */} + + + {/* Glow effect */} + + + {/* Render time badge */} + {flash.renderTime > 0 && ( + + {flash.renderTime.toFixed(2)}ms + + )} + + ))} + +
+ + ); } // Hook to track renders export function useRenderTracker(componentName: string) { - const ref = useRef(null); - const renderStartTimeRef = useRef(0); + const ref = useRef(null); + const renderStartTimeRef = useRef(0); - // Capture render start time - if (isTrackerEnabled) { - renderStartTimeRef.current = performance.now(); - } + // Capture render start time + if (isTrackerEnabled) { + renderStartTimeRef.current = performance.now(); + } - useEffect(() => { - if (!isTrackerEnabled) return; + useEffect(() => { + if (!isTrackerEnabled) return; - // Calculate render time - const renderTime = performance.now() - renderStartTimeRef.current; + // Calculate render time + const renderTime = performance.now() - renderStartTimeRef.current; - // Update metrics - renderStore.updateMetrics(componentName, renderTime); + // Update metrics + renderStore.updateMetrics(componentName, renderTime); - // Add flash effect - queueMicrotask(() => { - if (ref.current) { - addFlash(ref.current, componentName, renderTime); - } - }); - }); + // Add flash effect + queueMicrotask(() => { + if (ref.current) { + addFlash(ref.current, componentName, renderTime); + } + }); + }); - return ref; + return ref; } diff --git a/examples/demo/src/app/(test)/ZeroState.tsx b/examples/demo/src/app/(test)/ZeroState.tsx index dacee29..c84a293 100644 --- a/examples/demo/src/app/(test)/ZeroState.tsx +++ b/examples/demo/src/app/(test)/ZeroState.tsx @@ -1,136 +1,155 @@ 'use client'; import useUI from '@austinserb/react-zero-ui'; +import { useRenderTracker } from './ReactTracker'; export function TestComponent() { - const [, setTheme] = useUI<'light' | 'dark'>('theme', 'light'); - const [, setAccent] = useUI<'violet' | 'emerald' | 'amber'>('accent', 'violet'); - const [, setMenuOpen] = useUI('menuOpen', false); + const ref = useRenderTracker('TestComponent'); + const [, setTheme] = useUI<'light' | 'dark'>('theme', 'light'); + const [, setAccent] = useUI<'violet' | 'emerald' | 'amber'>('accent', 'violet'); + const [, setMenuOpen] = useUI('menuOpen', false); - return ( -
-
- - - setMenuOpen(prev => !prev)} /> - -
- ); + return ( +
+
+ + + setMenuOpen(prev => !prev)} /> + +
+ ); } // Header Component - Never re-renders! function Header() { - return ( -
-

Zero UI State Management

-

- Reactive state without re-rendering OR prop drilling.
- - Zero re-renders, Reactive state, Global &{' '} - Scoped state,{' '} - -

-
- ); + const ref = useRenderTracker('Header'); + + return ( +
+

Zero UI

+ +

+ Reactive state without re-rendering OR prop drilling.
+ + Zero re-renders,{' '} + Reactive &{' '} + Global state. + +

+
+ ); } // Theme Switcher - Never re-renders! function ThemeSwitcher({ setTheme }: { setTheme: (t: 'light' | 'dark') => void }) { - return ( -
- - -
- ); + const ref = useRenderTracker('ThemeSwitcher'); + + return ( +
+ + +
+ ); } // Accent Picker - Never re-renders! function AccentPicker({ setAccent }: { setAccent: (a: 'violet' | 'emerald' | 'amber') => void }) { - return ( -
-

Choose Accent

-
-
-
- ); + const ref = useRenderTracker('AccentPicker'); + + return ( +
+

Choose Accent

+
+
+
+ ); } // Interactive Card - Never re-renders! function InteractiveCard({ toggleMenu }: { toggleMenu: () => void }) { - return ( -
-
-

Open Menu Demo

-
- -
+ const ref = useRenderTracker('InteractiveCard'); + + return ( +
+
+

Open Menu Demo

+ +
- {/* Sliding Panel */} -
-
-

✨ This panel slides open without re-rendering!

-
-
-
- ); + {/* Sliding Panel */} +
+
+

✨ This panel slides open without re-rendering!

+
+
+
+ ); } // State Display - Never re-renders! function StateDisplay() { - return ( -
-
-
- theme: Light - Dark -
-
- accent: - Violet - Emerald - Amber -
-
- menu: - Open - Closed -
-
-
- ); + const ref = useRenderTracker('StateDisplay'); + + return ( +
+
+
+ theme: Light + Dark +
+
+ accent: + Violet + Emerald + Amber +
+
+ menu: + Open + Closed +
+
+
+ ); } diff --git a/examples/demo/src/app/(test)/layout.tsx b/examples/demo/src/app/(test)/layout.tsx index 32ead42..1b7ad5f 100644 --- a/examples/demo/src/app/(test)/layout.tsx +++ b/examples/demo/src/app/(test)/layout.tsx @@ -1,27 +1,69 @@ import { RenderTracker } from './ReactTracker'; const layout: React.FC<{ children: React.ReactNode }> = ({ children }) => { - return ( -
- {children} - -
- Layout.tsx (Global State Access) -
Global UI State Variables
-
None
-
- theme-dark - theme-light - accent-violet - accent-emerald - accent-amber - menu-open-true - menu-open-false - -
-
-
- ); + return ( +
+ {children} + +
+
(Layout.tsx) Global UI State Variables
+
None
+
+
+
+
theme-dark
+
+ theme-light +
+
+
+
+
+
+ accent-violet +
+
+ accent-emerald +
+
+ accent-amber +
+
+
+
+
+
+ menu-open-true +
+
+ menu-open-false +
+
+
+
+
+
+ scrolled-up +
+
+ scrolled-down +
+
+
+
+
+
+ mobile-menu-closed +
+
+ mobile-menu-open +
+
+
+
+
+
+ ); }; export default layout; diff --git a/examples/demo/src/app/(test)/page.tsx b/examples/demo/src/app/(test)/page.tsx index 7d30835..3d2c64e 100644 --- a/examples/demo/src/app/(test)/page.tsx +++ b/examples/demo/src/app/(test)/page.tsx @@ -6,42 +6,42 @@ import { TestComponent } from './ZeroState'; import { useUI } from '@austinserb/react-zero-ui'; export default function Page() { - const [, setActive] = useUI<'react' | 'zero'>('active', 'zero'); + const [, setActive] = useUI<'react' | 'zero'>('active', 'zero'); - return ( -
-
- {/* Button Header */} -
- {/* Sliding Indicator */} + return ( +
+
+
+ {/* Button Header */} +
+ {/* Sliding Indicator */} -
- {/* Buttons */} - - -
+
+ {/* Buttons */} + + +
-
-
- - -
-
-
-
- ); +
+
+ + +
+
+
+
+
+ ); } diff --git a/examples/demo/src/app/components/DotMenuIcon.tsx b/examples/demo/src/app/components/DotMenuIcon.tsx index 823484c..93472e8 100644 --- a/examples/demo/src/app/components/DotMenuIcon.tsx +++ b/examples/demo/src/app/components/DotMenuIcon.tsx @@ -1,9 +1,9 @@ export const DotMenuIcon: React.FC = () => { - return ( -
- - - -
- ); + return ( +
+ + + +
+ ); }; diff --git a/examples/demo/src/app/components/Icon.tsx b/examples/demo/src/app/components/Icon.tsx index 78ed731..e1be6c6 100644 --- a/examples/demo/src/app/components/Icon.tsx +++ b/examples/demo/src/app/components/Icon.tsx @@ -1,11 +1,11 @@ interface Props extends React.SVGProps { - name: string; + name: string; } export const Icon: React.FC = ({ name, ...rest }) => { - return ( - - - - ); + return ( + + + + ); }; diff --git a/examples/demo/src/app/components/MobileMenu.tsx b/examples/demo/src/app/components/MobileMenu.tsx index 3ebb3a7..808fc13 100644 --- a/examples/demo/src/app/components/MobileMenu.tsx +++ b/examples/demo/src/app/components/MobileMenu.tsx @@ -4,29 +4,30 @@ import clsx from 'clsx'; import Link from 'next/link'; export const MobileMenu: React.FC<{ navItems: { name: string; href: string }[] }> = ({ navItems }) => { - const [, setMenuOpen] = useUI<'closed' | 'open'>('mobileMenu', 'closed'); - return ( -
    - {navItems.map((item, index) => ( -
  • - setMenuOpen(prev => (prev === 'closed' ? 'open' : 'closed'))} className="block pt-4 font-medium"> - {item.name} - -
  • - ))} -
  • - setMenuOpen(prev => (prev === 'closed' ? 'open' : 'closed'))} - className="bubble-hover block rounded-full border border-gray-200 bg-white px-3 py-2 text-center font-medium shadow-lg duration-300 hover:border-white" - > - Contact - -
  • -
- ); + const [, setMenuOpen] = useUI<'closed' | 'open'>('mobile-menu', 'closed'); + return ( +
    + {navItems.map((item, index) => ( +
  • + setMenuOpen(prev => (prev === 'closed' ? 'open' : 'closed'))} + className="block pt-4 font-medium"> + {item.name} + +
  • + ))} +
  • + setMenuOpen(prev => (prev === 'closed' ? 'open' : 'closed'))} + className="bubble-hover block rounded-full border border-gray-200 bg-white px-3 py-2 text-center font-medium shadow-lg duration-300 hover:border-white"> + Contact + +
  • +
+ ); }; diff --git a/examples/demo/src/app/components/MobileMenuButton.tsx b/examples/demo/src/app/components/MobileMenuButton.tsx index 653d4ac..0c004cc 100644 --- a/examples/demo/src/app/components/MobileMenuButton.tsx +++ b/examples/demo/src/app/components/MobileMenuButton.tsx @@ -8,35 +8,39 @@ import { useIsMobile } from '@/hooks/useIsMobile'; import { DotMenuIcon } from './DotMenuIcon'; export const MobileMenuButton: React.FC = () => { - const [, setMobileMenu] = useUI<'closed' | 'open'>('mobileMenu', 'closed'); - const [, setScrolled] = useUI<'up' | 'down'>('scrolled', 'down'); - - const { scrollY } = useScroll(); - const isDesktop = !useIsMobile(768, () => { - setMobileMenu('closed'); - }); - - useMotionValueEvent(scrollY, 'change', current => { - if (!isDesktop) return; - const diff = current - (scrollY.getPrevious() ?? 0); - setScrolled(diff > 0 ? 'up' : 'down'); - }); - - return ( - - ); + const [, setMobileMenu] = useUI<'closed' | 'open'>('mobile-menu', 'closed'); + const [, setScrolled] = useUI<'up' | 'down'>('scrolled', 'up'); + + const { scrollY } = useScroll(); + const isDesktop = !useIsMobile(768, () => { + setMobileMenu('closed'); + }); + + useMotionValueEvent(scrollY, 'change', current => { + if (!isDesktop) return; + + const previous = scrollY.getPrevious() ?? current; + const diff = current - previous; + + if (Math.abs(diff) < 10) return; // Ignore minor scrolls + + setScrolled(diff > 0 ? 'down' : 'up'); + }); + + return ( + + ); }; diff --git a/examples/demo/src/app/components/TopBar.tsx b/examples/demo/src/app/components/TopBar.tsx index f485a69..16bae8e 100644 --- a/examples/demo/src/app/components/TopBar.tsx +++ b/examples/demo/src/app/components/TopBar.tsx @@ -4,56 +4,71 @@ import { MobileMenu } from './MobileMenu'; import { MobileMenuButton } from './MobileMenuButton'; const navItems = [ - { name: 'React Test', href: '/react' }, - { name: 'Zero UI Test', href: '/zero-ui' }, + { name: 'React Test', href: '/react' }, + { name: 'Zero UI Test', href: '/zero-ui' }, ]; export const TopBarV2: React.FC = () => { - return ( - - ); + {/* Mobile Menu (renders always but hidden via overflow on wrapper) */} + +
+
+
Render-less Zero-UI TopBar
+
+ + ); }; diff --git a/examples/demo/src/app/globals.css b/examples/demo/src/app/globals.css index fb3db87..24f032b 100644 --- a/examples/demo/src/app/globals.css +++ b/examples/demo/src/app/globals.css @@ -1,93 +1,93 @@ @import 'tailwindcss'; :root { - --gradient: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); - --button-shadow: - 0px 2px 2px -1.5px rgba(0, 0, 0, 0.32), 0px 4.4px 4.4px -2.25px rgba(0, 0, 0, 0.3), 0px 9.8px 9.8px -3px rgba(0, 0, 0, 0.25), - 0px 25px 25px -3.75px rgba(0, 0, 0, 0.11), 0px -5px 5px -3.75px rgba(0, 0, 0, 0.11); + --gradient: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); + --button-shadow: + 0px 2px 2px -1.5px rgba(0, 0, 0, 0.32), 0px 4.4px 4.4px -2.25px rgba(0, 0, 0, 0.3), 0px 9.8px 9.8px -3px rgba(0, 0, 0, 0.25), + 0px 25px 25px -3.75px rgba(0, 0, 0, 0.11), 0px -5px 5px -3.75px rgba(0, 0, 0, 0.11); } button:not(:disabled), [role='button']:not(:disabled) { - cursor: pointer; + cursor: pointer; } .bubble-hover { - @apply relative overflow-hidden rounded-full text-nowrap; - &::before { - @apply pointer-events-none absolute inset-0 z-[-1] -translate-x-full rounded-full opacity-0 blur-[1px] transition-all duration-300 will-change-transform content-['']; - background-size: 200% 200%; - background-position: 100% 100%; - background-image: var(--gradient); - } + @apply relative overflow-hidden rounded-full text-nowrap; + &::before { + @apply pointer-events-none absolute inset-0 z-[-1] -translate-x-full rounded-full opacity-0 blur-[1px] transition-all duration-300 will-change-transform content-['']; + background-size: 200% 200%; + background-position: 100% 100%; + background-image: var(--gradient); + } - &:hover { - &::before { - @apply translate-x-0 opacity-30!; - animation: fill-from-left 5s ease-out infinite; - } - } - &.active { - &::before { - @apply translate-x-0 opacity-70; - animation: fill-from-left 5s ease-out infinite; - } - } + &:hover { + &::before { + @apply translate-x-0 opacity-30!; + animation: fill-from-left 5s ease-out infinite; + } + } + &.active { + &::before { + @apply translate-x-0 opacity-70; + animation: fill-from-left 5s ease-out infinite; + } + } } body[data-mobile-menu='closed'] { - .bounce > span { - animation: customBounce 2s cubic-bezier(0.8, 0.5, 0.2, 1.4) infinite; - animation-delay: var(--delay, 0s); - height: 0.375rem; - width: 0.375rem; - border-radius: 100px; - background-color: black; - } - .mobile-menu-container { - pointer-events: none; - max-height: 0; - opacity: 0; - } + .bounce > span { + animation: customBounce 2s cubic-bezier(0.8, 0.5, 0.2, 1.4) infinite; + animation-delay: var(--delay, 0s); + height: 0.375rem; + width: 0.375rem; + border-radius: 100px; + background-color: black; + } + .mobile-menu-container { + pointer-events: none; + max-height: 0; + opacity: 0; + } } body[data-mobile-menu='open'] { - .dot-menu-icon { - :first-child { - height: 0.125rem; - width: 100%; - rotate: calc(45deg); - border-radius: 100px; - background-color: black; - } - :nth-child(2) { - opacity: 0; - } - :nth-child(3) { - height: 0.125rem; - width: 100%; - rotate: calc(-45deg); - border-radius: 100px; - background-color: black; - } - } + .dot-menu-icon { + :first-child { + height: 0.125rem; + width: 100%; + rotate: calc(45deg); + border-radius: 100px; + background-color: black; + } + :nth-child(2) { + opacity: 0; + } + :nth-child(3) { + height: 0.125rem; + width: 100%; + rotate: calc(-45deg); + border-radius: 100px; + background-color: black; + } + } - .mobile-menu-container { - pointer-events: auto; - max-height: 300px; - opacity: 1; - padding-bottom: 1rem; - } - .mobile-menu-item { - transform: translateX(0); - opacity: 1; - /* Calculate delay based on index */ - transition-delay: calc(var(--index) * 0.1s + 0.2s); - } + .mobile-menu-container { + pointer-events: auto; + max-height: 300px; + opacity: 1; + padding-bottom: 1rem; + } + .mobile-menu-item { + transform: translateX(0); + opacity: 1; + /* Calculate delay based on index */ + transition-delay: calc(var(--index) * 0.1s + 0.2s); + } } .mobile-menu-item { - transform: translateX(-1.25rem); - opacity: 0; - transition-delay: 0s; + transform: translateX(-1.25rem); + opacity: 0; + transition-delay: 0s; } /* @@ -110,29 +110,35 @@ body[data-mobile-menu='open'] { */ @keyframes customBounce { - 0%, - 25%, - 100% { - transform: translateY(0px); - background-color: #aaaaaa; - } + 0%, + 25%, + 100% { + transform: translateY(0px); + background-color: #aaaaaa; + } - 10% { - transform: translateY(-2px); - background-color: #000; - } - 18% { - transform: translateY(1px); - background-color: #aaaaaa; - } + 10% { + transform: translateY(-2px); + background-color: #000; + } + 18% { + transform: translateY(1px); + background-color: #aaaaaa; + } } @keyframes fill-from-left { - 0%, - 100% { - background-position: 100% 100%; - } - 50% { - background-position: 0% 0%; - } + 0%, + 100% { + background-position: 100% 100%; + } + 50% { + background-position: 0% 0%; + } +} + +@layer base { + .pill { + @apply rounded-full border border-gray-200 bg-white/20 py-1 text-center font-medium text-nowrap text-gray-900 ring-1 ring-black/5 backdrop-blur-sm duration-300 hover:border-white max-md:text-xs; + } } diff --git a/examples/demo/src/app/layout.tsx b/examples/demo/src/app/layout.tsx index 06b5794..b8fb4ca 100644 --- a/examples/demo/src/app/layout.tsx +++ b/examples/demo/src/app/layout.tsx @@ -1,30 +1,20 @@ import { bodyAttributes } from '@zero-ui/attributes'; import './globals.css'; import { TopBarV2 } from './components/TopBar'; -import { Analytics } from "@vercel/analytics/next" +import { Analytics } from '@vercel/analytics/next'; -export const metadata = { - title: 'React Zero UI Demo', - description: 'React Zero UI Demo', - alternates: { - canonical: process.env.NEXT_PUBLIC_URL, - }, -}; +export const metadata = { title: 'React Zero UI Demo', description: 'React Zero UI Demo', alternates: { canonical: process.env.NEXT_PUBLIC_URL } }; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - - - - {children} - {process.env.NODE_ENV === 'production' && } - - - ); +export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { + return ( + + + + {children} + {process.env.NODE_ENV === 'production' && } + + + ); } diff --git a/examples/demo/src/app/react/Dashboard.tsx b/examples/demo/src/app/react/Dashboard.tsx index a3fe9a4..fa62058 100644 --- a/examples/demo/src/app/react/Dashboard.tsx +++ b/examples/demo/src/app/react/Dashboard.tsx @@ -4,44 +4,41 @@ import { InnerDot } from './InnerDot'; import Link from 'next/link'; export const Dashboard: React.FC = () => { - const [theme, setTheme] = useState<'light' | 'dark'>('light'); + const [theme, setTheme] = useState<'light' | 'dark'>('light'); - // Define theme classes based on state - const containerClasses = theme === 'light' ? 'bg-gray-200 text-gray-900' : 'bg-gray-900 text-gray-200'; + // Define theme classes based on state + const containerClasses = theme === 'light' ? 'bg-gray-200 text-gray-900' : 'bg-gray-900 text-gray-200'; - const itemClasses = theme === 'light' ? 'bg-gray-900 text-gray-200' : 'bg-gray-200 text-gray-900'; - const itemClasses2 = theme === 'dark' ? 'bg-red-400' : 'bg-blue-400'; + const itemClasses = theme === 'light' ? 'bg-gray-900 text-gray-200' : 'bg-gray-200 text-gray-900'; + const itemClasses2 = theme === 'dark' ? 'bg-red-400' : 'bg-blue-400'; - return ( -
-
- - - Zero-UI 10k Node Test - -
-
10,000 nodes with Nested Node using React State
-
- {Array.from({ length: 10000 }).map((_, index) => ( -
- -
- ))} -
-
- ); + return ( +
+
+ + + Zero-UI 10k Node Test + +
+
10,000 nodes with Nested Node using React State
+
+ {Array.from({ length: 10000 }).map((_, index) => ( +
+ +
+ ))} +
+
+ ); }; diff --git a/examples/demo/src/app/react/InnerDot.tsx b/examples/demo/src/app/react/InnerDot.tsx index 05bd0f7..7066078 100644 --- a/examples/demo/src/app/react/InnerDot.tsx +++ b/examples/demo/src/app/react/InnerDot.tsx @@ -1,15 +1,15 @@ export const InnerDot = ({ itemClasses2 }: { itemClasses2: string }) => { - return ( - - - -
- - - - ); + return ( + + + +
+ + + + ); }; const Layer1 = ({ children }: { children: React.ReactNode }) =>
{children}
; const Layer2 = ({ children }: { children: React.ReactNode }) =>
{children}
; -const Layer3 = ({ children }: { children: React.ReactNode }) =>
{children}
; +const Layer3 = ({ children }: { children: React.ReactNode }) =>
{children}
; diff --git a/examples/demo/src/app/react/page.tsx b/examples/demo/src/app/react/page.tsx index abb8347..256c1b1 100644 --- a/examples/demo/src/app/react/page.tsx +++ b/examples/demo/src/app/react/page.tsx @@ -1,24 +1,19 @@ import { Dashboard } from './Dashboard'; export const metadata = { - title: 'React State Demo', - description: 'React re-renders 10,000 components per toggle. Expect noticeable lag.', - alternates: { - canonical: process.env.NEXT_PUBLIC_URL + '/react', - }, + title: 'React State Demo', + description: 'React re-renders 10,000 components per toggle. Expect noticeable lag.', + alternates: { canonical: process.env.NEXT_PUBLIC_URL + '/react' }, }; export default function Page() { - - return ( -
-
-

React State Demo

-

- React re-renders 10,000 components per toggle. Expect noticeable lag. -

-
- -
- ); + return ( +
+
+

React State Demo

+

React re-renders 10,000 components per toggle. Expect noticeable lag.

+
+ +
+ ); } diff --git a/examples/demo/src/app/zero-ui/Dashboard.tsx b/examples/demo/src/app/zero-ui/Dashboard.tsx index eef4384..8977e40 100644 --- a/examples/demo/src/app/zero-ui/Dashboard.tsx +++ b/examples/demo/src/app/zero-ui/Dashboard.tsx @@ -4,47 +4,41 @@ import { InnerDot } from './InnerDot'; import Link from 'next/link'; export const Dashboard: React.FC = () => { - const [, setTheme] = useUI<'light' | 'dark'>('themeTest', 'light'); - return ( -
-
- - - React 10k Node Test - -
-
10,000 nodes with Nested Node using Zero UI
-
- {Array.from({ length: 10000 }).map((_, index) => ( -
- -
- ))} -
-
- ); + const [, setTheme] = useUI<'light' | 'dark'>('themeTest', 'light'); + return ( +
+
+ + + React 10k Node Test + +
+
10,000 nodes with Nested Node using Zero UI
+
+ {Array.from({ length: 10000 }).map((_, index) => ( +
+ +
+ ))} +
+
+ ); }; diff --git a/examples/demo/src/app/zero-ui/InnerDot.tsx b/examples/demo/src/app/zero-ui/InnerDot.tsx index f53d55c..51c784f 100644 --- a/examples/demo/src/app/zero-ui/InnerDot.tsx +++ b/examples/demo/src/app/zero-ui/InnerDot.tsx @@ -1,15 +1,15 @@ export const InnerDot = () => { - return ( - - - -
- - - - ); + return ( + + + +
+ + + + ); }; const Layer1 = ({ children }: { children: React.ReactNode }) =>
{children}
; const Layer2 = ({ children }: { children: React.ReactNode }) =>
{children}
; -const Layer3 = ({ children }: { children: React.ReactNode }) =>
{children}
; \ No newline at end of file +const Layer3 = ({ children }: { children: React.ReactNode }) =>
{children}
; diff --git a/examples/demo/src/app/zero-ui/page.tsx b/examples/demo/src/app/zero-ui/page.tsx index e8e6c23..2a48024 100644 --- a/examples/demo/src/app/zero-ui/page.tsx +++ b/examples/demo/src/app/zero-ui/page.tsx @@ -1,24 +1,20 @@ import { Dashboard } from './Dashboard'; export const metadata = { - title: 'Zero UI Demo', - description: '10,000 live nodes. No virtual DOM. No re-renders. Just raw UI performance.', - alternates: { - canonical: process.env.NEXT_PUBLIC_URL + '/zero-ui', - }, + title: 'Zero UI Demo', + description: '10,000 live nodes. No virtual DOM. No re-renders. Just raw UI performance.', + alternates: { canonical: process.env.NEXT_PUBLIC_URL + '/zero-ui' }, }; export default function Page() { - return ( -
-
-

Zero UI Demo

-

- 10,000 live nodes. No virtual DOM. No re-renders. Just raw UI performance. -

-
- {/* 10k nodes */} - -
- ); + return ( +
+
+

Zero UI Demo

+

10,000 live nodes. No virtual DOM. No re-renders. Just raw UI performance.

+
+ {/* 10k nodes */} + +
+ ); } diff --git a/examples/demo/src/hooks/useIsMobile.ts b/examples/demo/src/hooks/useIsMobile.ts index 7ff8f6a..633de43 100644 --- a/examples/demo/src/hooks/useIsMobile.ts +++ b/examples/demo/src/hooks/useIsMobile.ts @@ -9,14 +9,14 @@ import { getMediaQueryStore } from '../utils/getMediaQueryStore'; * @param fn - A function to call when the screen is mobile */ export function useIsMobile(breakpoint = 768, fn?: () => void) { - const store = getMediaQueryStore(breakpoint, fn); + const store = getMediaQueryStore(breakpoint, fn); - return useSyncExternalStore( - cb => { - store.subscribers.add(cb); - return () => store.subscribers.delete(cb); - }, - () => store.isMatch, - () => false - ); + return useSyncExternalStore( + cb => { + store.subscribers.add(cb); + return () => store.subscribers.delete(cb); + }, + () => store.isMatch, + () => false + ); } diff --git a/examples/demo/src/utils/env.ts b/examples/demo/src/utils/env.ts index f87fb87..2ea8674 100644 --- a/examples/demo/src/utils/env.ts +++ b/examples/demo/src/utils/env.ts @@ -2,51 +2,51 @@ 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'; - } + 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 diff --git a/examples/demo/src/utils/getMediaQueryStore.ts b/examples/demo/src/utils/getMediaQueryStore.ts index 2da24d9..3788d6d 100644 --- a/examples/demo/src/utils/getMediaQueryStore.ts +++ b/examples/demo/src/utils/getMediaQueryStore.ts @@ -1,12 +1,12 @@ import { env } from './env'; type MediaQueryStore = { - /** Latest match result (true / false) */ - isMatch: boolean; - /** The native MediaQueryList object */ - mediaQueryList: MediaQueryList; - /** React subscribers that need re-rendering on change */ - subscribers: Set<() => void>; + /** Latest match result (true / false) */ + isMatch: boolean; + /** The native MediaQueryList object */ + mediaQueryList: MediaQueryList; + /** React subscribers that need re-rendering on change */ + subscribers: Set<() => void>; }; /** Map of raw query strings -> singleton store objects */ @@ -18,28 +18,24 @@ const mediaQueryStores: Record = {}; * creating it (and its listener) the first time. */ export function getMediaQueryStore(breakpoint: number, fn?: () => void): MediaQueryStore { - // Already created? - just return it - if (mediaQueryStores[breakpoint]) return mediaQueryStores[breakpoint]; + // Already created? - just return it + if (mediaQueryStores[breakpoint]) return mediaQueryStores[breakpoint]; - // --- First-time setup --- - const queryString = `(max-width: ${breakpoint - 0.1}px)`; - const mqList = env.isClient ? window.matchMedia(queryString) : ({} as MediaQueryList); - const store: MediaQueryStore = { - isMatch: env.isClient ? mqList.matches : false, - mediaQueryList: mqList, - subscribers: new Set(), - }; + // --- First-time setup --- + const queryString = `(max-width: ${breakpoint - 0.1}px)`; + const mqList = env.isClient ? window.matchMedia(queryString) : ({} as MediaQueryList); + const store: MediaQueryStore = { isMatch: env.isClient ? mqList.matches : false, mediaQueryList: mqList, subscribers: new Set() }; - const update = () => { - store.isMatch = mqList.matches; - store.subscribers.forEach(cb => cb()); - fn?.(); - }; + const update = () => { + store.isMatch = mqList.matches; + store.subscribers.forEach(cb => cb()); + fn?.(); + }; - if (mqList.addEventListener) mqList.addEventListener('change', update); - // for Safari < 14 - else if (mqList.addListener) mqList.addListener(update); + if (mqList.addEventListener) mqList.addEventListener('change', update); + // for Safari < 14 + else if (mqList.addListener) mqList.addListener(update); - mediaQueryStores[breakpoint] = store; - return store; + mediaQueryStores[breakpoint] = store; + return store; } diff --git a/examples/demo/tsconfig.json b/examples/demo/tsconfig.json index 7026899..06bfce6 100644 --- a/examples/demo/tsconfig.json +++ b/examples/demo/tsconfig.json @@ -1,39 +1,32 @@ { - "compilerOptions": { - "forceConsistentCasingInFileNames": true, - "target": "ES2017", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": ["./src/*"], - "@zero-ui/attributes": ["./.zero-ui/attributes.js"] - }, - "baseUrl": "." - }, - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.d.ts", - "**/*.tsx", - ".next/types/**/*.ts", - "scripts/generateUiVariants.cjs", - "src/app/(test)/page.tsx", - ".zero-ui/**/*.d.ts" // ← ensure present - ], - "exclude": ["node_modules"] + "compilerOptions": { + "forceConsistentCasingInFileNames": true, + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [{ "name": "next" }], + "paths": { "@/*": ["./src/*"], "@zero-ui/attributes": ["./.zero-ui/attributes.js"] }, + "baseUrl": "." + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.d.ts", + "**/*.tsx", + ".next/types/**/*.ts", + "scripts/generateUiVariants.cjs", + "src/app/(test)/page.tsx", + ".zero-ui/**/*.d.ts" // ← ensure present + ], + "exclude": ["node_modules"] }