Skip to content

Commit 30d88e0

Browse files
authored
added support to toggle css variables without re-renders (#18)
1 parent 264687e commit 30d88e0

File tree

13 files changed

+116
-57
lines changed

13 files changed

+116
-57
lines changed

examples/demo/.zero-ui/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const zeroSSR = { onClick: <const V extends string[]>(key: string, vals: [...V]) => ({ 'data-ui': `cycle:${key}(${vals.join(',')})` }) as const };
Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,26 @@
11
import { bodyAttributes } from './attributes';
22

33
if (typeof window !== 'undefined') {
4-
const toDatasetKey = (dataKey: string) => dataKey.slice(5).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
4+
const toCamel = (key: string) => key.slice(5).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
55

6-
const act = {
7-
// toggle:theme-test(dark,light)
8-
toggle: (k: string, [on = 'on']: string[]) => {
9-
document.body.dataset[k] = document.body.dataset[k] ? '' : on;
10-
},
11-
// cycle:theme-test(dark,light)
12-
cycle: (k: string, vals: string[]) => {
13-
const cur = document.body.dataset[k] ?? vals[0];
14-
const next = vals[(vals.indexOf(cur) + 1) % vals.length];
15-
document.body.dataset[k] = next;
16-
},
17-
// set:theme-test(dark)
18-
set: (k: string, [v = '']: string[]) => {
19-
document.body.dataset[k] = v;
20-
},
21-
// attr:theme-test(data-theme)
22-
attr: (k: string, [attr]: string[], el: HTMLElement) => {
23-
document.body.dataset[k] = el.getAttribute(attr) ?? '';
24-
},
6+
const cycle = (target: HTMLElement, k: string, vals: string[]) => {
7+
const cur = target.dataset[k] ?? vals[0]; // default = first value
8+
const next = vals[(vals.indexOf(cur) + 1) % vals.length];
9+
target.dataset[k] = next;
2510
};
2611

2712
document.addEventListener('click', (e) => {
2813
const el = (e.target as HTMLElement).closest<HTMLElement>('[data-ui]');
2914
if (!el) return;
3015

31-
const [, cmd, key, raw] = el.dataset.ui!.match(/^(\w+):([\w-]+)(?:\((.*?)\))?$/) || [];
32-
if (!cmd || !(`data-${key}` in bodyAttributes)) return;
16+
const [, key, rawVals = ''] = el.dataset.ui!.match(/^cycle:([\w-]+)(?:\((.*?)\))?$/) || [];
3317

34-
const dsKey = toDatasetKey(`data-${key}`);
35-
console.log('dsKey: ', dsKey);
36-
act[cmd as keyof typeof act]?.(dsKey, raw ? raw.split(',') : [], el);
18+
if (!(`data-${key}` in bodyAttributes)) return; // unknown variant → bail
19+
20+
const vals = rawVals.split(','); // '' → [''] OK for toggle
21+
const dsKey = toCamel(`data-${key}`);
22+
const target = (el.closest(`[data-${key}]`) as HTMLElement) ?? document.body;
23+
24+
cycle(target, dsKey, vals);
3725
});
3826
}

examples/demo/src/app/zero-ui-ssr/Dashboard.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1+
import { zeroSSR } from '../../../.zero-ui';
12
import { InnerDot } from './InnerDot';
23
import Link from 'next/link';
34

45
export const Dashboard: React.FC = () => {
6+
const theme = 'theme-test';
7+
const themeValues = ['dark', 'light'];
58
return (
69
<div className="theme-test-light:bg-gray-200 theme-test-light:text-gray-900 theme-test-dark:bg-gray-900 theme-test-dark:text-gray-200 flex h-screen w-screen flex-col items-center justify-start p-5">
710
<div className="flex flex-row items-center gap-2">
811
<button
912
type="button"
10-
data-ui="cycle:theme-test(dark,light)"
13+
{...zeroSSR.onClick(theme, themeValues)}
1114
className="rounded-md bg-blue-500 px-4 py-2 text-white transition-colors hover:bg-blue-600">
1215
Toggle Theme (Current:{' '}
1316
{

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"reset": "git clean -fdx && pnpm install --frozen-lockfile && pnpm prepack:core && pnpm i-tarball",
2626
"bootstrap": "pnpm install --frozen-lockfile && pnpm build && pnpm prepack:core && pnpm i-tarball",
2727
"build": "cd packages/core && pnpm build",
28-
"test": "cd packages/core && pnpm test:all",
28+
"test": "cd packages/core && pnpm test:all && pnpm smoke",
2929
"prepack:core": "pnpm -F @react-zero-ui/core pack --pack-destination ./dist",
3030
"i-tarball": "node scripts/install-local-tarball.js",
3131
"test:vite": "cd packages/core && pnpm test:vite",

packages/core/__tests__/e2e/next.spec.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,16 @@ test.describe('Zero-UI Next.js Integration Tests', () => {
184184
// Verify other states are preserved
185185
await expect(body).toHaveAttribute('data-theme', 'dark');
186186
await expect(body).toHaveAttribute('data-toggle-boolean', 'false');
187+
188+
// Click global blur toggle
189+
await page.getByTestId('global-toggle').click();
190+
const globalBlur = page.getByTestId('global-blur');
191+
await expect(globalBlur).toHaveCSS('backdrop-filter', 'blur(4px)');
192+
193+
// Click scope blur toggle
194+
await page.getByTestId('toggle-0').click();
195+
const demoBlur = page.getByTestId('demo-0');
196+
await expect(demoBlur).toHaveCSS('filter', 'blur(4px)');
187197
});
188198

189199
test('Tailwind is generated correctly', async ({ page }) => {

packages/core/__tests__/fixtures/next/.zero-ui/attributes.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
/* AUTO-GENERATED - DO NOT EDIT */
22
export declare const bodyAttributes: {
3+
"data-blur": string;
4+
"data-blur-global": string;
35
"data-child": "closed" | "open";
6+
"data-dialog": "closed" | "open";
47
"data-faq": "closed" | "open";
58
"data-mobile": "false" | "true";
69
"data-number": "1" | "2";

packages/core/__tests__/fixtures/next/.zero-ui/attributes.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* AUTO-GENERATED - DO NOT EDIT */
22
export const bodyAttributes = {
3+
"data-blur-global": "0px",
34
"data-child": "closed",
45
"data-number": "1",
56
"data-theme": "light",
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
'use client';
2+
3+
import { useScopedUI, cssVar } from '@react-zero-ui/core';
4+
5+
/**
6+
* CssVarDemo - minimal fixture for Playwright
7+
*
8+
* • Flips a scoped CSS variable `--blur` between "0px" ⇄ "4px"
9+
* • Uses the updater-function pattern (`prev ⇒ next`)
10+
* • Exposes test-ids so your spec can assert both style + text
11+
*/
12+
export default function CssVarDemo({ index = 0 }) {
13+
// 👇 pass `cssVar` flag to switch makeSetter into CSS-var mode
14+
const [blur, setBlur] = useScopedUI<'0px' | '4px'>('blur', '0px', cssVar);
15+
// global test
16+
17+
return (
18+
<div
19+
ref={setBlur.ref} // element that owns --blur
20+
data-testid={`demo-${index}`}
21+
style={{ filter: 'blur(var(--blur, 0px))' }} // read the var
22+
className="m-4 p-6 rounded bg-slate-200 space-y-3">
23+
<button
24+
data-testid={`toggle-${index}`}
25+
onClick={() => setBlur((prev) => (prev === '0px' ? '4px' : '0px'))}
26+
className="px-3 py-1 rounded bg-black text-white">
27+
toggle blur
28+
</button>
29+
30+
{/* expose current value for assertions */}
31+
<p data-testid={`value-${index}`}>{blur}</p>
32+
</div>
33+
);
34+
}

packages/core/__tests__/fixtures/next/app/page.tsx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
'use client';
2-
import { useScopedUI, useUI } from '@react-zero-ui/core';
2+
import { cssVar, useScopedUI, useUI } from '@react-zero-ui/core';
33
import UseEffectComponent from './UseEffectComponent';
44
import FAQ from './FAQ';
55
import { ChildComponent } from './ChildComponent';
66
import { ChildWithoutSetter } from './ChildWithoutSetter';
7+
import CssVarDemo from './CssVarDemo';
78

89
export default function Page() {
910
const [scope, setScope] = useScopedUI<'off' | 'on'>('scope', 'off');
@@ -19,13 +20,15 @@ export default function Page() {
1920

2021
const [, setToggleFunction] = useUI<'white' | 'black'>('toggle-function', 'white');
2122

23+
const [global, setGlobal] = useUI<'0px' | '4px'>('blur-global', '0px', cssVar);
24+
2225
const toggleFunction = () => {
2326
setToggleFunction((prev) => (prev === 'white' ? 'black' : 'white'));
2427
};
2528

2629
return (
2730
<div
28-
className="p-8 theme-light:bg-white theme-dark:bg-white bg-black"
31+
className="p-8 theme-light:bg-white theme-dark:bg-white bg-black relative"
2932
data-testid="page-container">
3033
<h1 className="text-2xl font-bold py-5">Global State</h1>
3134
<hr />
@@ -223,6 +226,28 @@ export default function Page() {
223226
question="Question 3"
224227
answer="Answer 3"
225228
/>
229+
230+
{Array.from({ length: 2 }).map((_, index) => (
231+
<CssVarDemo
232+
key={index}
233+
index={index}
234+
/>
235+
))}
236+
<div
237+
data-testid={`global-blur-container`}
238+
className="m-4 p-6 rounded bg-slate-200 space-y-3">
239+
<button
240+
data-testid={`global-toggle`}
241+
className="bg-blue-500 text-white p-2 rounded-md"
242+
onClick={() => setGlobal((prev) => (prev === '0px' ? '4px' : '0px'))}>
243+
Global blur toggle
244+
</button>
245+
<div
246+
data-testid={`global-blur`}
247+
className="absolute inset-0 z-10 pointer-events-none"
248+
style={{ backdropFilter: 'blur(var(--blur-global, 0px))' }} // read the var
249+
/>
250+
</div>
226251
</div>
227252
);
228253
}

packages/core/src/experimental/index.ts

Whitespace-only changes.

0 commit comments

Comments
 (0)