@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 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";
@@ -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>;