@evolution-james/evolution-theme-engine 1.0.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.
@@ -0,0 +1,159 @@
1
+ /*
2
+ * ============================================================
3
+ * ThemeContext.jsx — Evolution Theme Engine
4
+ * ============================================================
5
+ * This file is the heart of the theme engine. It provides:
6
+ *
7
+ * THEMES — A map of human-readable keys to the exact
8
+ * string values used as the `data-theme`
9
+ * attribute on <html>. These strings must
10
+ * match selectors in themes.css exactly.
11
+ *
12
+ * ThemeProvider — A React context provider that:
13
+ * • Reads the persisted theme from localStorage
14
+ * on first render (no flash on reload).
15
+ * • Sets `data-theme` on <html> whenever the
16
+ * theme changes so CSS kicks in site-wide.
17
+ * • Exposes `theme` and `setTheme` to all
18
+ * descendant components via context.
19
+ *
20
+ * useTheme — Convenience hook. Call inside any component
21
+ * wrapped by ThemeProvider to read or change
22
+ * the active theme.
23
+ *
24
+ * registerTheme — Runtime utility. Injects a new [data-theme]
25
+ * CSS block at runtime so consumers can add
26
+ * custom themes without editing themes.css.
27
+ *
28
+ * Data flow:
29
+ * User picks theme → setTheme() → React state updates
30
+ * → useEffect fires → data-theme attribute set on <html>
31
+ * → CSS [data-theme="..."] block takes effect site-wide.
32
+ * ============================================================
33
+ */
34
+
35
+ import { createContext, useContext, useState, useEffect } from 'react';
36
+
37
+ /*
38
+ * THEMES maps a friendly JS key to the exact string written
39
+ * into the HTML `data-theme` attribute. Add an entry here and
40
+ * a matching [data-theme="..."] block in themes.css to create
41
+ * a new built-in theme.
42
+ */
43
+ export const THEMES = {
44
+ light: 'light',
45
+ dark: 'dark',
46
+ forest: 'forest',
47
+ tron: 'tron',
48
+ midnight: 'midnight',
49
+ };
50
+
51
+ const ThemeContext = createContext({
52
+ theme: THEMES.light,
53
+ setTheme: () => {},
54
+ });
55
+
56
+ /*
57
+ * ThemeProvider wraps your application root (e.g. in index.jsx).
58
+ *
59
+ * It reads the user's last-saved theme from localStorage so the
60
+ * correct theme is applied before the first paint, preventing a
61
+ * flash back to the default Light theme on page refresh.
62
+ *
63
+ * Props:
64
+ * children — React subtree to receive theme context.
65
+ * defaultTheme — (optional) Override the fallback when no
66
+ * localStorage value exists. Defaults to 'light'.
67
+ * storageKey — (optional) localStorage key used to persist
68
+ * the selected theme. Defaults to 'etn-theme'.
69
+ */
70
+ export function ThemeProvider({
71
+ children,
72
+ defaultTheme = THEMES.light,
73
+ storageKey = 'etn-theme',
74
+ }) {
75
+ const [theme, setThemeState] = useState(() => {
76
+ return localStorage.getItem(storageKey) || defaultTheme;
77
+ });
78
+
79
+ /*
80
+ * Sync the `data-theme` attribute on <html> whenever theme changes.
81
+ * This is what triggers the CSS variable overrides in themes.css.
82
+ */
83
+ useEffect(() => {
84
+ document.documentElement.setAttribute('data-theme', theme);
85
+ }, [theme]);
86
+
87
+ /*
88
+ * setTheme updates React state AND persists to localStorage so
89
+ * the selection survives page refreshes and new tabs.
90
+ */
91
+ const setTheme = (newTheme) => {
92
+ setThemeState(newTheme);
93
+ localStorage.setItem(storageKey, newTheme);
94
+ };
95
+
96
+ return (
97
+ <ThemeContext.Provider value={{ theme, setTheme }}>
98
+ {children}
99
+ </ThemeContext.Provider>
100
+ );
101
+ }
102
+
103
+ /*
104
+ * useTheme — convenience hook.
105
+ * Returns { theme, setTheme } from the nearest ThemeProvider.
106
+ * Must be called inside a component that is a descendant of ThemeProvider.
107
+ */
108
+ export function useTheme() {
109
+ return useContext(ThemeContext);
110
+ }
111
+
112
+ /*
113
+ * registerTheme — runtime theme registration.
114
+ *
115
+ * Dynamically injects a new [data-theme="name"] CSS block into the
116
+ * document <head> at runtime. This lets consumers define custom themes
117
+ * in JavaScript without modifying themes.css.
118
+ *
119
+ * Parameters:
120
+ * name (string) — The theme key, e.g. 'ocean'. This value is used
121
+ * as the data-theme attribute value.
122
+ * vars (object) — A plain object mapping CSS variable names to values.
123
+ * Keys should NOT include the leading '--'.
124
+ *
125
+ * Example:
126
+ * registerTheme('ocean', {
127
+ * 'color-bg': '#0a1628',
128
+ * 'color-text': '#e0f0ff',
129
+ * 'color-primary': '#00b4d8',
130
+ * 'color-on-primary': '#0a1628',
131
+ * 'color-card-bg': '#0d2137',
132
+ * 'color-card-border': '#1a3a5c',
133
+ * 'color-divider': '#1a3a5c',
134
+ * 'color-link': '#90e0ef',
135
+ * 'color-hover-bg': 'rgba(0,180,216,0.1)',
136
+ * 'color-code-bg': '#070f1a',
137
+ * 'color-code-text': '#e0f0ff',
138
+ * });
139
+ *
140
+ * After calling registerTheme, add the key to your own THEMES-like object
141
+ * and pass it to <ThemeSelector> via the `themes` prop to surface it in
142
+ * the UI.
143
+ */
144
+ export function registerTheme(name, vars) {
145
+ const existingId = `etn-theme-${name}`;
146
+ const existing = document.getElementById(existingId);
147
+ if (existing) existing.remove();
148
+
149
+ const declarations = Object.entries(vars)
150
+ .map(([key, value]) => ` --${key}: ${value};`)
151
+ .join('\n');
152
+
153
+ const css = `[data-theme="${name}"] {\n${declarations}\n}`;
154
+
155
+ const styleEl = document.createElement('style');
156
+ styleEl.id = existingId;
157
+ styleEl.textContent = css;
158
+ document.head.appendChild(styleEl);
159
+ }
package/src/index.js ADDED
@@ -0,0 +1,20 @@
1
+ /*
2
+ * ============================================================
3
+ * index.js — Evolution Theme Engine public API
4
+ * ============================================================
5
+ * Re-exports every symbol that consumers are expected to use.
6
+ * Import from 'evolution-theme-engine', never from internal paths.
7
+ *
8
+ * Named exports:
9
+ * ThemeProvider — Wrap your app root with this.
10
+ * useTheme — Hook: { theme, setTheme } from context.
11
+ * THEMES — Map of built-in theme keys to data-theme values.
12
+ * registerTheme — Runtime: inject a custom theme at run-time.
13
+ * ThemeSelector — Standalone <select> dropdown component.
14
+ * ThemeNavBar — Barebones navbar with ThemeSelector built in.
15
+ * ============================================================
16
+ */
17
+
18
+ export { ThemeProvider, useTheme, THEMES, registerTheme } from './context/ThemeContext.jsx';
19
+ export { ThemeSelector } from './components/ThemeSelector.jsx';
20
+ export { ThemeNavBar } from './components/ThemeNavBar.jsx';
@@ -0,0 +1,278 @@
1
+ /*
2
+ * ============================================================
3
+ * themes.css — Evolution Theme Engine
4
+ * ============================================================
5
+ * This file contains:
6
+ * 1. All built-in theme variable blocks.
7
+ * 2. Base html/body reset.
8
+ * 3. Component styles for .etn-navbar and .etn-theme-selector.
9
+ *
10
+ * HOW THEMES WORK
11
+ * ───────────────
12
+ * Each theme is a CSS block that overrides the CSS custom
13
+ * properties declared in :root. The active theme is selected
14
+ * by setting a `data-theme` attribute on the <html> element:
15
+ *
16
+ * document.documentElement.setAttribute('data-theme', 'dark');
17
+ *
18
+ * ThemeProvider does this automatically whenever the theme changes.
19
+ *
20
+ * WHY :root MUST COME FIRST
21
+ * ─────────────────────────
22
+ * Both `:root` and `[data-theme="..."]` selectors have equal
23
+ * CSS specificity (0,1,0). When both selectors match the same
24
+ * element, the one declared LATER in the file wins. Therefore
25
+ * :root (the Light/default theme) MUST appear before all
26
+ * [data-theme] blocks so that any active theme can override it.
27
+ *
28
+ * CSS VARIABLE REFERENCE
29
+ * ──────────────────────
30
+ * --color-bg Page / app background
31
+ * --color-text Primary body text
32
+ * --color-card-bg Card / panel surface background
33
+ * --color-card-border Card / panel border colour
34
+ * --color-btn-dark-bg Background for "dark" style buttons
35
+ * --color-btn-dark-text Text on "dark" style buttons
36
+ * --color-btn-light-bg Background for "light" style buttons
37
+ * --color-btn-light-text Text on "light" style buttons
38
+ * --color-divider Horizontal rules / separators
39
+ * --color-primary Primary accent / brand colour
40
+ * --color-on-primary Text drawn on top of --color-primary
41
+ * --color-link Hyperlink colour
42
+ * --color-hover-bg Subtle hover-state background tint
43
+ * --color-code-bg Code block background
44
+ * --color-code-text Code block text colour
45
+ *
46
+ * ADDING A CUSTOM THEME (CSS approach)
47
+ * ─────────────────────────────────────
48
+ * Copy the block below, change the selector to your theme name,
49
+ * and update the variable values. Then add the name to your
50
+ * themes object and pass it to <ThemeSelector>.
51
+ *
52
+ * [data-theme="ocean"] {
53
+ * --color-bg: #0a1628;
54
+ * --color-text: #e0f0ff;
55
+ * --color-primary: #00b4d8;
56
+ * ... etc.
57
+ * }
58
+ *
59
+ * Alternatively, use registerTheme() from ThemeContext.jsx to
60
+ * inject a theme at runtime without touching this file at all.
61
+ * ============================================================
62
+ */
63
+
64
+
65
+ /* ============================================================
66
+ * 1. BUILT-IN THEMES
67
+ * ============================================================ */
68
+
69
+ /* --- Light Theme (default) ---
70
+ * :root MUST appear first in this file so that every
71
+ * [data-theme] block below can override it.
72
+ */
73
+ :root {
74
+ --color-bg: #f8f9fa;
75
+ --color-text: #212529;
76
+ --color-card-bg: #ffffff;
77
+ --color-card-border: #dee2e6;
78
+ --color-btn-dark-bg: #f8f9fa;
79
+ --color-btn-dark-text: #212529;
80
+ --color-btn-light-bg: #ffffff;
81
+ --color-btn-light-text: #212529;
82
+ --color-divider: #dee2e6;
83
+
84
+ --color-primary: #1976d2;
85
+ --color-on-primary: #ffffff;
86
+ --color-link: #a435f0;
87
+ --color-hover-bg: rgba(25, 118, 210, 0.08);
88
+ --color-code-bg: #282c34;
89
+ --color-code-text: #ffffff;
90
+ }
91
+
92
+ /* --- Dark Theme --- */
93
+ [data-theme="dark"] {
94
+ --color-bg: #212529;
95
+ --color-text: #f8f9fa;
96
+ --color-card-bg: #343a40;
97
+ --color-card-border: #444444;
98
+ --color-btn-dark-bg: #f8f9fa;
99
+ --color-btn-dark-text: #212529;
100
+ --color-btn-light-bg: #343a40;
101
+ --color-btn-light-text: #f8f9fa;
102
+ --color-divider: #444444;
103
+
104
+ --color-primary: #90caf9;
105
+ --color-on-primary: #212529;
106
+ --color-link: #a435f0;
107
+ --color-hover-bg: rgba(144, 202, 249, 0.08);
108
+ --color-code-bg: #232b36;
109
+ --color-code-text: #f8f9fa;
110
+ }
111
+
112
+ /* --- Forest Theme ---
113
+ * A comforting deep-green palette with sage text and
114
+ * a natural green primary accent.
115
+ */
116
+ [data-theme="forest"] {
117
+ --color-bg: #1b2e22;
118
+ --color-text: #cde8d4;
119
+ --color-card-bg: #243c2b;
120
+ --color-card-border: #3a5c44;
121
+ --color-btn-dark-bg: #3a5c44;
122
+ --color-btn-dark-text: #cde8d4;
123
+ --color-btn-light-bg: #243c2b;
124
+ --color-btn-light-text: #cde8d4;
125
+ --color-divider: #3a5c44;
126
+
127
+ --color-primary: #4caf70;
128
+ --color-on-primary: #1b2e22;
129
+ --color-link: #7dd8a0;
130
+ --color-hover-bg: rgba(76, 175, 112, 0.12);
131
+ --color-code-bg: #111e17;
132
+ --color-code-text: #cde8d4;
133
+ }
134
+
135
+ /* --- Tron Theme ---
136
+ * Inspired by the Tron: Legacy aesthetic — dark navy background
137
+ * with electric cyan text and sky-blue accents.
138
+ */
139
+ [data-theme="tron"] {
140
+ --color-bg: #0f172a;
141
+ --color-text: #67e8f9;
142
+ --color-card-bg: #1e293b;
143
+ --color-card-border: #67e8f9;
144
+ --color-btn-dark-bg: #0ea5e9;
145
+ --color-btn-dark-text: #0f172a;
146
+ --color-btn-light-bg: #1e293b;
147
+ --color-btn-light-text: #67e8f9;
148
+ --color-divider: #0ea5e9;
149
+
150
+ --color-primary: #0ea5e9;
151
+ --color-on-primary: #0f172a;
152
+ --color-link: #67e8f9;
153
+ --color-hover-bg: rgba(14, 165, 233, 0.08);
154
+ --color-code-bg: #232b36;
155
+ --color-code-text: #67e8f9;
156
+ }
157
+
158
+ /* --- Midnight Theme ---
159
+ * A deep night-sky palette: near-black background, muted
160
+ * blue-grey text, teal (#5ce1b5) primary accent, and
161
+ * light-blue (#8bd4ff) link colour.
162
+ */
163
+ [data-theme="midnight"] {
164
+ --color-bg: #0b1016;
165
+ --color-text: #e7edf2;
166
+ --color-card-bg: #131c28;
167
+ --color-card-border: #263141;
168
+ --color-btn-dark-bg: #263141;
169
+ --color-btn-dark-text: #e7edf2;
170
+ --color-btn-light-bg: #0f151e;
171
+ --color-btn-light-text: #e7edf2;
172
+ --color-divider: #263141;
173
+
174
+ --color-primary: #5ce1b5;
175
+ --color-on-primary: #0b1016;
176
+ --color-link: #8bd4ff;
177
+ --color-hover-bg: rgba(92, 225, 181, 0.12);
178
+ --color-code-bg: #0f151e;
179
+ --color-code-text: #e7edf2;
180
+ }
181
+
182
+
183
+ /* ============================================================
184
+ * 2. BASE RESET
185
+ * Sets the theme background on <html> to prevent a flash of
186
+ * white before React mounts and ThemeProvider runs.
187
+ * ============================================================ */
188
+
189
+ html {
190
+ background-color: var(--color-bg);
191
+ color: var(--color-text);
192
+ transition: background-color 0.3s ease, color 0.3s ease;
193
+ }
194
+
195
+
196
+ /* ============================================================
197
+ * 3. COMPONENT STYLES
198
+ * All classes are prefixed with 'etn-' (Evolution Theme eNgine)
199
+ * to avoid collisions with the consumer application's CSS.
200
+ * ============================================================ */
201
+
202
+ /* --- ThemeNavBar (.etn-navbar) --- */
203
+ .etn-navbar {
204
+ display: flex;
205
+ align-items: center;
206
+ justify-content: space-between;
207
+ padding: 0 24px;
208
+ height: 56px;
209
+ background-color: var(--color-card-bg);
210
+ border-bottom: 1px solid var(--color-card-border);
211
+ gap: 16px;
212
+ /* Stays at the top when used as a sticky header */
213
+ position: sticky;
214
+ top: 0;
215
+ z-index: 1000;
216
+ }
217
+
218
+ .etn-navbar-brand {
219
+ font-size: 1.1rem;
220
+ font-weight: 700;
221
+ color: var(--color-text);
222
+ white-space: nowrap;
223
+ flex-shrink: 0;
224
+ }
225
+
226
+ .etn-navbar-links {
227
+ display: flex;
228
+ align-items: center;
229
+ gap: 20px;
230
+ list-style: none;
231
+ margin: 0;
232
+ padding: 0;
233
+ flex: 1;
234
+ }
235
+
236
+ .etn-navbar-link {
237
+ color: var(--color-text);
238
+ text-decoration: none;
239
+ font-size: 0.95rem;
240
+ opacity: 0.85;
241
+ transition: opacity 0.15s ease, color 0.15s ease;
242
+ }
243
+
244
+ .etn-navbar-link:hover {
245
+ opacity: 1;
246
+ color: var(--color-primary);
247
+ }
248
+
249
+ /* --- ThemeSelector (.etn-theme-selector) --- */
250
+ .etn-theme-selector {
251
+ padding: 6px 10px;
252
+ background-color: var(--color-card-bg);
253
+ color: var(--color-text);
254
+ border: 1px solid var(--color-card-border);
255
+ border-radius: 4px;
256
+ font-size: 0.875rem;
257
+ cursor: pointer;
258
+ /* Prevent the select from shrinking inside a flex navbar */
259
+ flex-shrink: 0;
260
+ transition: border-color 0.15s ease;
261
+ }
262
+
263
+ .etn-theme-selector:hover {
264
+ border-color: var(--color-primary);
265
+ }
266
+
267
+ .etn-theme-selector:focus {
268
+ outline: none;
269
+ border-color: var(--color-primary);
270
+ box-shadow: 0 0 0 3px var(--color-hover-bg);
271
+ }
272
+
273
+ /* Responsive: hide nav links on small screens */
274
+ @media (max-width: 600px) {
275
+ .etn-navbar-links {
276
+ display: none;
277
+ }
278
+ }