@zentrades-ui/theme 0.1.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 +187 -0
- package/package.json +26 -0
- package/src/ThemeProvider.tsx +143 -0
- package/src/index.ts +7 -0
- package/src/theme.css.ts +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# @zentrades-ui/theme
|
|
2
|
+
|
|
3
|
+
React theme provider that converts Zen UI tokens into CSS custom properties.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @zentrades-ui/theme @zentrades-ui/tokens
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
Wrap your application with `ThemeProvider`:
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { ThemeProvider } from "@zentrades-ui/theme";
|
|
17
|
+
import { defaultTokens } from "@zentrades-ui/tokens";
|
|
18
|
+
|
|
19
|
+
function App() {
|
|
20
|
+
return (
|
|
21
|
+
<ThemeProvider mode="light" tokens={defaultTokens}>
|
|
22
|
+
<YourApp />
|
|
23
|
+
</ThemeProvider>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Theme Modes
|
|
29
|
+
|
|
30
|
+
The provider supports `light` and `dark` modes:
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
// Light mode (default)
|
|
34
|
+
<ThemeProvider mode="light" tokens={defaultTokens}>
|
|
35
|
+
|
|
36
|
+
// Dark mode
|
|
37
|
+
<ThemeProvider mode="dark" tokens={defaultTokens}>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Dynamic Theme Switching
|
|
41
|
+
|
|
42
|
+
Use the `useTheme` hook to access and change the current mode:
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import { useTheme } from "@zentrades-ui/theme";
|
|
46
|
+
|
|
47
|
+
function ThemeToggle() {
|
|
48
|
+
const { mode, setMode } = useTheme();
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<button onClick={() => setMode(mode === "light" ? "dark" : "light")}>
|
|
52
|
+
Current: {mode}
|
|
53
|
+
</button>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## CSS Variables
|
|
59
|
+
|
|
60
|
+
The provider generates CSS variables on the target element (defaults to `document.documentElement`). All variable names are normalized to lowercase with hyphens.
|
|
61
|
+
|
|
62
|
+
### Color Variables
|
|
63
|
+
|
|
64
|
+
```css
|
|
65
|
+
/* Content colors */
|
|
66
|
+
var(--zen-color-contentprimary)
|
|
67
|
+
var(--zen-color-contentsecondary)
|
|
68
|
+
var(--zen-color-contenttertiary)
|
|
69
|
+
var(--zen-color-contentbrand)
|
|
70
|
+
var(--zen-color-contentinverse)
|
|
71
|
+
|
|
72
|
+
/* Background colors */
|
|
73
|
+
var(--zen-color-backgroundprimary)
|
|
74
|
+
var(--zen-color-backgroundsecondary)
|
|
75
|
+
var(--zen-color-backgroundbrand)
|
|
76
|
+
|
|
77
|
+
/* Border colors */
|
|
78
|
+
var(--zen-color-borderprimary)
|
|
79
|
+
var(--zen-color-bordersecondary)
|
|
80
|
+
|
|
81
|
+
/* Surface levels */
|
|
82
|
+
var(--zen-color-surfacel0)
|
|
83
|
+
var(--zen-color-surfacel1)
|
|
84
|
+
var(--zen-color-surfacel2)
|
|
85
|
+
/* ... up to surfacel6 */
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Spacing Variables
|
|
89
|
+
|
|
90
|
+
```css
|
|
91
|
+
var(--zen-space-1) /* 1px */
|
|
92
|
+
var(--zen-space-2) /* 2px */
|
|
93
|
+
var(--zen-space-4) /* 4px */
|
|
94
|
+
var(--zen-space-6) /* 6px */
|
|
95
|
+
var(--zen-space-8) /* 8px */
|
|
96
|
+
/* ... and more */
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Typography Variables
|
|
100
|
+
|
|
101
|
+
```css
|
|
102
|
+
/* Font families */
|
|
103
|
+
var(--zen-font-geist)
|
|
104
|
+
var(--zen-font-mono)
|
|
105
|
+
|
|
106
|
+
/* Font sizes */
|
|
107
|
+
var(--zen-font-size-xs)
|
|
108
|
+
var(--zen-font-size-s)
|
|
109
|
+
var(--zen-font-size-m)
|
|
110
|
+
var(--zen-font-size-l)
|
|
111
|
+
/* ... up to 8xl */
|
|
112
|
+
|
|
113
|
+
/* Font weights */
|
|
114
|
+
var(--zen-font-weight-regular)
|
|
115
|
+
var(--zen-font-weight-medium)
|
|
116
|
+
var(--zen-font-weight-semibold)
|
|
117
|
+
var(--zen-font-weight-bold)
|
|
118
|
+
|
|
119
|
+
/* Line heights */
|
|
120
|
+
var(--zen-line-height-xs)
|
|
121
|
+
var(--zen-line-height-s)
|
|
122
|
+
/* ... */
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Border Variables
|
|
126
|
+
|
|
127
|
+
```css
|
|
128
|
+
/* Border radius */
|
|
129
|
+
var(--zen-radius-xs)
|
|
130
|
+
var(--zen-radius-sm)
|
|
131
|
+
var(--zen-radius-md)
|
|
132
|
+
var(--zen-radius-lg)
|
|
133
|
+
var(--zen-radius-xl)
|
|
134
|
+
var(--zen-radius-pill)
|
|
135
|
+
var(--zen-radius-circle)
|
|
136
|
+
|
|
137
|
+
/* Border width */
|
|
138
|
+
var(--zen-border-none)
|
|
139
|
+
var(--zen-border-xs)
|
|
140
|
+
var(--zen-border-s)
|
|
141
|
+
var(--zen-border-m)
|
|
142
|
+
var(--zen-border-l)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Shadow Variables
|
|
146
|
+
|
|
147
|
+
```css
|
|
148
|
+
var(--zen-shadow-l0)
|
|
149
|
+
var(--zen-shadow-l1)
|
|
150
|
+
var(--zen-shadow-l2)
|
|
151
|
+
/* ... up to l7 */
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Custom Target Element
|
|
155
|
+
|
|
156
|
+
Apply theme variables to a specific element instead of the document root:
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
160
|
+
|
|
161
|
+
<ThemeProvider mode="light" tokens={defaultTokens} target={containerRef.current}>
|
|
162
|
+
<div ref={containerRef}>
|
|
163
|
+
{/* Theme variables only available within this container */}
|
|
164
|
+
</div>
|
|
165
|
+
</ThemeProvider>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Example: Using Variables in Styles
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
function MyComponent() {
|
|
172
|
+
return (
|
|
173
|
+
<div
|
|
174
|
+
style={{
|
|
175
|
+
background: "var(--zen-color-backgroundprimary)",
|
|
176
|
+
color: "var(--zen-color-contentprimary)",
|
|
177
|
+
padding: "var(--zen-space-4)",
|
|
178
|
+
borderRadius: "var(--zen-radius-md)",
|
|
179
|
+
fontFamily: "var(--zen-font-geist)",
|
|
180
|
+
fontSize: "var(--zen-font-size-m)",
|
|
181
|
+
}}
|
|
182
|
+
>
|
|
183
|
+
Styled with Zen UI tokens
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zentrades-ui/theme",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Theme runtime for Zen UI.",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"src"
|
|
10
|
+
],
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"sideEffects": false,
|
|
15
|
+
"peerDependencies": {
|
|
16
|
+
"react": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@vanilla-extract/css": "^1.17.1",
|
|
20
|
+
"@zentrades-ui/tokens": "0.1.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/react": "^18.3.12",
|
|
24
|
+
"typescript": "^5.9.3"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
} from "react";
|
|
10
|
+
import { darkTheme, lightTheme, type ThemeMode } from "./theme.css";
|
|
11
|
+
|
|
12
|
+
type AllowedThemeOptions = "light" | "dark" | "system";
|
|
13
|
+
|
|
14
|
+
type ThemeContextValue = {
|
|
15
|
+
theme: AllowedThemeOptions;
|
|
16
|
+
effectiveTheme: ThemeMode;
|
|
17
|
+
setTheme: (mode: AllowedThemeOptions) => void;
|
|
18
|
+
toggleTheme: () => void;
|
|
19
|
+
themeClassName: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
|
|
23
|
+
|
|
24
|
+
const themeClassMap: Record<ThemeMode, string> = {
|
|
25
|
+
light: lightTheme,
|
|
26
|
+
dark: darkTheme,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type ThemeProviderProps = {
|
|
30
|
+
children: ReactNode;
|
|
31
|
+
theme: AllowedThemeOptions;
|
|
32
|
+
effectiveTheme?: ThemeMode;
|
|
33
|
+
/**
|
|
34
|
+
* Handler invoked when `useTheme().setTheme` or `useTheme().toggleTheme` is called.
|
|
35
|
+
* Provide this to fully control the theme from the host application.
|
|
36
|
+
*/
|
|
37
|
+
onThemeChange?: (mode: AllowedThemeOptions, specific?: ThemeMode) => void;
|
|
38
|
+
/**
|
|
39
|
+
* Optional element that should receive the theme class.
|
|
40
|
+
* Defaults to document.body when available.
|
|
41
|
+
*/
|
|
42
|
+
target?: HTMLElement | null;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function getSystemTheme(): ThemeMode {
|
|
46
|
+
if (typeof window === "undefined") return "light";
|
|
47
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
48
|
+
? "dark"
|
|
49
|
+
: "light";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function ThemeProvider({
|
|
53
|
+
children,
|
|
54
|
+
theme,
|
|
55
|
+
onThemeChange,
|
|
56
|
+
target,
|
|
57
|
+
}: ThemeProviderProps) {
|
|
58
|
+
const previousClassRef = useRef<string | null>(null);
|
|
59
|
+
|
|
60
|
+
const effectiveTheme: ThemeMode =
|
|
61
|
+
theme === "system" ? getSystemTheme() : theme;
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (typeof document === "undefined") return;
|
|
65
|
+
|
|
66
|
+
const host = target ?? document.body;
|
|
67
|
+
if (!host) return;
|
|
68
|
+
|
|
69
|
+
const nextClass = themeClassMap[effectiveTheme];
|
|
70
|
+
const prev = previousClassRef.current;
|
|
71
|
+
|
|
72
|
+
if (prev && prev !== nextClass) {
|
|
73
|
+
host.classList.remove(prev);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
host.classList.add(nextClass);
|
|
77
|
+
host.setAttribute("data-theme", effectiveTheme);
|
|
78
|
+
previousClassRef.current = nextClass;
|
|
79
|
+
|
|
80
|
+
return () => {
|
|
81
|
+
host.classList.remove(nextClass);
|
|
82
|
+
if (host.getAttribute("data-theme") === effectiveTheme) {
|
|
83
|
+
host.removeAttribute("data-theme");
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}, [effectiveTheme, target]);
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (theme !== "system") return;
|
|
90
|
+
if (typeof window === "undefined") return;
|
|
91
|
+
|
|
92
|
+
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
93
|
+
const handler = () => {
|
|
94
|
+
onThemeChange?.("system", getSystemTheme());
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
mq.addEventListener("change", handler);
|
|
98
|
+
return () => mq.removeEventListener("change", handler);
|
|
99
|
+
}, [theme, onThemeChange]);
|
|
100
|
+
|
|
101
|
+
const setTheme = useCallback(
|
|
102
|
+
(mode: AllowedThemeOptions) => {
|
|
103
|
+
if (onThemeChange) {
|
|
104
|
+
onThemeChange(mode, effectiveTheme);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
console.warn(
|
|
108
|
+
"ThemeProvider: `setTheme` called but no `onThemeChange` handler was provided. The theme remains unchanged."
|
|
109
|
+
);
|
|
110
|
+
},
|
|
111
|
+
[onThemeChange, effectiveTheme]
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const toggleTheme = useCallback(() => {
|
|
115
|
+
setTheme(theme === "light" ? "dark" : "light");
|
|
116
|
+
}, [setTheme, theme]);
|
|
117
|
+
|
|
118
|
+
const value = useMemo<ThemeContextValue>(
|
|
119
|
+
() => ({
|
|
120
|
+
theme,
|
|
121
|
+
effectiveTheme,
|
|
122
|
+
setTheme,
|
|
123
|
+
toggleTheme,
|
|
124
|
+
themeClassName: themeClassMap[effectiveTheme],
|
|
125
|
+
}),
|
|
126
|
+
[theme, effectiveTheme, setTheme, toggleTheme]
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function useTheme(): ThemeContextValue {
|
|
135
|
+
const context = useContext(ThemeContext);
|
|
136
|
+
if (!context) {
|
|
137
|
+
throw new Error("useTheme must be used within a ThemeProvider");
|
|
138
|
+
}
|
|
139
|
+
return context;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Re-export types
|
|
143
|
+
export type { ThemeMode, AllowedThemeOptions };
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// ThemeProvider and hook
|
|
2
|
+
export { ThemeProvider, useTheme } from "./ThemeProvider";
|
|
3
|
+
export type { ThemeProviderProps, ThemeMode, AllowedThemeOptions } from "./ThemeProvider";
|
|
4
|
+
|
|
5
|
+
// Vanilla Extract theme exports
|
|
6
|
+
export { colorVars, lightTheme, darkTheme } from "./theme.css";
|
|
7
|
+
export type { ColorVarName, Theme } from "./theme.css";
|
package/src/theme.css.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createTheme, createThemeContract } from "@vanilla-extract/css";
|
|
2
|
+
import { themedColorVars } from "@zentrades-ui/tokens";
|
|
3
|
+
|
|
4
|
+
// Get all themed color keys
|
|
5
|
+
const themedColorKeys = Object.keys(themedColorVars) as Array<
|
|
6
|
+
keyof typeof themedColorVars
|
|
7
|
+
>;
|
|
8
|
+
|
|
9
|
+
// Create the contract shape (all values null for contract definition)
|
|
10
|
+
const themedContractShape = Object.fromEntries(
|
|
11
|
+
themedColorKeys.map((token) => [token, null])
|
|
12
|
+
) as { [K in keyof typeof themedColorVars]: null };
|
|
13
|
+
|
|
14
|
+
// Create the theme contract - this defines the CSS variable structure
|
|
15
|
+
export const colorVars = createThemeContract({
|
|
16
|
+
...themedContractShape,
|
|
17
|
+
transparent: null,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export type ThemeMode = "light" | "dark";
|
|
21
|
+
export type ColorVarName = keyof typeof colorVars;
|
|
22
|
+
|
|
23
|
+
type ThemedColorName = keyof typeof themedColorVars;
|
|
24
|
+
type ThemedColorVariant = (typeof themedColorVars)[ThemedColorName];
|
|
25
|
+
|
|
26
|
+
const themedEntries = themedColorKeys.map(
|
|
27
|
+
(token) =>
|
|
28
|
+
[token, themedColorVars[token]] as [ThemedColorName, ThemedColorVariant]
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// Build theme values for a given mode
|
|
32
|
+
const buildThemeValues = (mode: ThemeMode) =>
|
|
33
|
+
({
|
|
34
|
+
...Object.fromEntries(
|
|
35
|
+
themedEntries.map(([token, variants]) => [token, variants[mode]])
|
|
36
|
+
),
|
|
37
|
+
transparent: "transparent",
|
|
38
|
+
}) as Record<ColorVarName, string>;
|
|
39
|
+
|
|
40
|
+
// Create light and dark theme classes
|
|
41
|
+
export const lightTheme = createTheme(colorVars, buildThemeValues("light"));
|
|
42
|
+
export const darkTheme = createTheme(colorVars, buildThemeValues("dark"));
|
|
43
|
+
|
|
44
|
+
export type Theme = Record<ColorVarName, string>;
|