@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.
- package/LICENSE.txt +13 -0
- package/README.md +407 -0
- package/dist/components/ThemeNavBar.js +99 -0
- package/dist/components/ThemeSelector.js +86 -0
- package/dist/context/ThemeContext.js +175 -0
- package/dist/index.js +44 -0
- package/dist/styles/themes.css +278 -0
- package/package.json +46 -0
- package/src/components/ThemeNavBar.jsx +86 -0
- package/src/components/ThemeSelector.jsx +68 -0
- package/src/context/ThemeContext.jsx +159 -0
- package/src/index.js +20 -0
- package/src/styles/themes.css +278 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.THEMES = void 0;
|
|
7
|
+
exports.ThemeProvider = ThemeProvider;
|
|
8
|
+
exports.registerTheme = registerTheme;
|
|
9
|
+
exports.useTheme = useTheme;
|
|
10
|
+
var _react = require("react");
|
|
11
|
+
var _jsxRuntime = require("react/jsx-runtime");
|
|
12
|
+
function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
|
|
13
|
+
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
|
|
14
|
+
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
|
|
15
|
+
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
|
|
16
|
+
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
|
|
17
|
+
function _arrayWithHoles(r) { if (Array.isArray(r)) return r; } /*
|
|
18
|
+
* ============================================================
|
|
19
|
+
* ThemeContext.jsx — Evolution Theme Engine
|
|
20
|
+
* ============================================================
|
|
21
|
+
* This file is the heart of the theme engine. It provides:
|
|
22
|
+
*
|
|
23
|
+
* THEMES — A map of human-readable keys to the exact
|
|
24
|
+
* string values used as the `data-theme`
|
|
25
|
+
* attribute on <html>. These strings must
|
|
26
|
+
* match selectors in themes.css exactly.
|
|
27
|
+
*
|
|
28
|
+
* ThemeProvider — A React context provider that:
|
|
29
|
+
* • Reads the persisted theme from localStorage
|
|
30
|
+
* on first render (no flash on reload).
|
|
31
|
+
* • Sets `data-theme` on <html> whenever the
|
|
32
|
+
* theme changes so CSS kicks in site-wide.
|
|
33
|
+
* • Exposes `theme` and `setTheme` to all
|
|
34
|
+
* descendant components via context.
|
|
35
|
+
*
|
|
36
|
+
* useTheme — Convenience hook. Call inside any component
|
|
37
|
+
* wrapped by ThemeProvider to read or change
|
|
38
|
+
* the active theme.
|
|
39
|
+
*
|
|
40
|
+
* registerTheme — Runtime utility. Injects a new [data-theme]
|
|
41
|
+
* CSS block at runtime so consumers can add
|
|
42
|
+
* custom themes without editing themes.css.
|
|
43
|
+
*
|
|
44
|
+
* Data flow:
|
|
45
|
+
* User picks theme → setTheme() → React state updates
|
|
46
|
+
* → useEffect fires → data-theme attribute set on <html>
|
|
47
|
+
* → CSS [data-theme="..."] block takes effect site-wide.
|
|
48
|
+
* ============================================================
|
|
49
|
+
*/ /*
|
|
50
|
+
* THEMES maps a friendly JS key to the exact string written
|
|
51
|
+
* into the HTML `data-theme` attribute. Add an entry here and
|
|
52
|
+
* a matching [data-theme="..."] block in themes.css to create
|
|
53
|
+
* a new built-in theme.
|
|
54
|
+
*/
|
|
55
|
+
var THEMES = exports.THEMES = {
|
|
56
|
+
light: 'light',
|
|
57
|
+
dark: 'dark',
|
|
58
|
+
forest: 'forest',
|
|
59
|
+
tron: 'tron',
|
|
60
|
+
midnight: 'midnight'
|
|
61
|
+
};
|
|
62
|
+
var ThemeContext = /*#__PURE__*/(0, _react.createContext)({
|
|
63
|
+
theme: THEMES.light,
|
|
64
|
+
setTheme: function setTheme() {}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/*
|
|
68
|
+
* ThemeProvider wraps your application root (e.g. in index.jsx).
|
|
69
|
+
*
|
|
70
|
+
* It reads the user's last-saved theme from localStorage so the
|
|
71
|
+
* correct theme is applied before the first paint, preventing a
|
|
72
|
+
* flash back to the default Light theme on page refresh.
|
|
73
|
+
*
|
|
74
|
+
* Props:
|
|
75
|
+
* children — React subtree to receive theme context.
|
|
76
|
+
* defaultTheme — (optional) Override the fallback when no
|
|
77
|
+
* localStorage value exists. Defaults to 'light'.
|
|
78
|
+
* storageKey — (optional) localStorage key used to persist
|
|
79
|
+
* the selected theme. Defaults to 'etn-theme'.
|
|
80
|
+
*/
|
|
81
|
+
function ThemeProvider(_ref) {
|
|
82
|
+
var children = _ref.children,
|
|
83
|
+
_ref$defaultTheme = _ref.defaultTheme,
|
|
84
|
+
defaultTheme = _ref$defaultTheme === void 0 ? THEMES.light : _ref$defaultTheme,
|
|
85
|
+
_ref$storageKey = _ref.storageKey,
|
|
86
|
+
storageKey = _ref$storageKey === void 0 ? 'etn-theme' : _ref$storageKey;
|
|
87
|
+
var _useState = (0, _react.useState)(function () {
|
|
88
|
+
return localStorage.getItem(storageKey) || defaultTheme;
|
|
89
|
+
}),
|
|
90
|
+
_useState2 = _slicedToArray(_useState, 2),
|
|
91
|
+
theme = _useState2[0],
|
|
92
|
+
setThemeState = _useState2[1];
|
|
93
|
+
|
|
94
|
+
/*
|
|
95
|
+
* Sync the `data-theme` attribute on <html> whenever theme changes.
|
|
96
|
+
* This is what triggers the CSS variable overrides in themes.css.
|
|
97
|
+
*/
|
|
98
|
+
(0, _react.useEffect)(function () {
|
|
99
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
100
|
+
}, [theme]);
|
|
101
|
+
|
|
102
|
+
/*
|
|
103
|
+
* setTheme updates React state AND persists to localStorage so
|
|
104
|
+
* the selection survives page refreshes and new tabs.
|
|
105
|
+
*/
|
|
106
|
+
var setTheme = function setTheme(newTheme) {
|
|
107
|
+
setThemeState(newTheme);
|
|
108
|
+
localStorage.setItem(storageKey, newTheme);
|
|
109
|
+
};
|
|
110
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(ThemeContext.Provider, {
|
|
111
|
+
value: {
|
|
112
|
+
theme: theme,
|
|
113
|
+
setTheme: setTheme
|
|
114
|
+
},
|
|
115
|
+
children: children
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/*
|
|
120
|
+
* useTheme — convenience hook.
|
|
121
|
+
* Returns { theme, setTheme } from the nearest ThemeProvider.
|
|
122
|
+
* Must be called inside a component that is a descendant of ThemeProvider.
|
|
123
|
+
*/
|
|
124
|
+
function useTheme() {
|
|
125
|
+
return (0, _react.useContext)(ThemeContext);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/*
|
|
129
|
+
* registerTheme — runtime theme registration.
|
|
130
|
+
*
|
|
131
|
+
* Dynamically injects a new [data-theme="name"] CSS block into the
|
|
132
|
+
* document <head> at runtime. This lets consumers define custom themes
|
|
133
|
+
* in JavaScript without modifying themes.css.
|
|
134
|
+
*
|
|
135
|
+
* Parameters:
|
|
136
|
+
* name (string) — The theme key, e.g. 'ocean'. This value is used
|
|
137
|
+
* as the data-theme attribute value.
|
|
138
|
+
* vars (object) — A plain object mapping CSS variable names to values.
|
|
139
|
+
* Keys should NOT include the leading '--'.
|
|
140
|
+
*
|
|
141
|
+
* Example:
|
|
142
|
+
* registerTheme('ocean', {
|
|
143
|
+
* 'color-bg': '#0a1628',
|
|
144
|
+
* 'color-text': '#e0f0ff',
|
|
145
|
+
* 'color-primary': '#00b4d8',
|
|
146
|
+
* 'color-on-primary': '#0a1628',
|
|
147
|
+
* 'color-card-bg': '#0d2137',
|
|
148
|
+
* 'color-card-border': '#1a3a5c',
|
|
149
|
+
* 'color-divider': '#1a3a5c',
|
|
150
|
+
* 'color-link': '#90e0ef',
|
|
151
|
+
* 'color-hover-bg': 'rgba(0,180,216,0.1)',
|
|
152
|
+
* 'color-code-bg': '#070f1a',
|
|
153
|
+
* 'color-code-text': '#e0f0ff',
|
|
154
|
+
* });
|
|
155
|
+
*
|
|
156
|
+
* After calling registerTheme, add the key to your own THEMES-like object
|
|
157
|
+
* and pass it to <ThemeSelector> via the `themes` prop to surface it in
|
|
158
|
+
* the UI.
|
|
159
|
+
*/
|
|
160
|
+
function registerTheme(name, vars) {
|
|
161
|
+
var existingId = "etn-theme-".concat(name);
|
|
162
|
+
var existing = document.getElementById(existingId);
|
|
163
|
+
if (existing) existing.remove();
|
|
164
|
+
var declarations = Object.entries(vars).map(function (_ref2) {
|
|
165
|
+
var _ref3 = _slicedToArray(_ref2, 2),
|
|
166
|
+
key = _ref3[0],
|
|
167
|
+
value = _ref3[1];
|
|
168
|
+
return " --".concat(key, ": ").concat(value, ";");
|
|
169
|
+
}).join('\n');
|
|
170
|
+
var css = "[data-theme=\"".concat(name, "\"] {\n").concat(declarations, "\n}");
|
|
171
|
+
var styleEl = document.createElement('style');
|
|
172
|
+
styleEl.id = existingId;
|
|
173
|
+
styleEl.textContent = css;
|
|
174
|
+
document.head.appendChild(styleEl);
|
|
175
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
Object.defineProperty(exports, "THEMES", {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: function get() {
|
|
9
|
+
return _ThemeContext.THEMES;
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
Object.defineProperty(exports, "ThemeNavBar", {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: function get() {
|
|
15
|
+
return _ThemeNavBar.ThemeNavBar;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
Object.defineProperty(exports, "ThemeProvider", {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
get: function get() {
|
|
21
|
+
return _ThemeContext.ThemeProvider;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
Object.defineProperty(exports, "ThemeSelector", {
|
|
25
|
+
enumerable: true,
|
|
26
|
+
get: function get() {
|
|
27
|
+
return _ThemeSelector.ThemeSelector;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
Object.defineProperty(exports, "registerTheme", {
|
|
31
|
+
enumerable: true,
|
|
32
|
+
get: function get() {
|
|
33
|
+
return _ThemeContext.registerTheme;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
Object.defineProperty(exports, "useTheme", {
|
|
37
|
+
enumerable: true,
|
|
38
|
+
get: function get() {
|
|
39
|
+
return _ThemeContext.useTheme;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
var _ThemeContext = require("./context/ThemeContext.js");
|
|
43
|
+
var _ThemeSelector = require("./components/ThemeSelector.js");
|
|
44
|
+
var _ThemeNavBar = require("./components/ThemeNavBar.js");
|
|
@@ -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
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@evolution-james/evolution-theme-engine",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A plug-and-play React theme engine with CSS variable-based theming, localStorage persistence, and optional navbar component.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react",
|
|
7
|
+
"theme",
|
|
8
|
+
"dark-mode",
|
|
9
|
+
"css-variables",
|
|
10
|
+
"theme-engine",
|
|
11
|
+
"evolution"
|
|
12
|
+
],
|
|
13
|
+
"author": {
|
|
14
|
+
"name": "James Evolution (Evolution Coding Academy)",
|
|
15
|
+
"url": "https://github.com/james-evolution"
|
|
16
|
+
},
|
|
17
|
+
"license": "SEE LICENSE.txt",
|
|
18
|
+
"//": "BUILD NOTES: Source files are JSX (src/). The build script (build.js) uses Babel to pre-compile them into plain CommonJS JS (dist/) so consumers never need to transpile this package themselves. @babel/preset-react is configured with { runtime: 'automatic' } which uses the new JSX transform — it imports from react/jsx-runtime automatically instead of requiring React to be in scope (the classic runtime would inject React.createElement() calls that fail if React is not globally imported). @babel/preset-env compiles ESM imports/exports and modern syntax to CommonJS-compatible output. The 'exports' field below takes strict precedence over 'main' in webpack 5 and Node 12+. Without it, npm link + webpack symlink resolution can bypass 'main' and resolve to raw JSX source files the consumer's bundler cannot parse.",
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"react": ">=17.0.0",
|
|
21
|
+
"react-dom": ">=17.0.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"react": "^18.0.0",
|
|
25
|
+
"react-dom": "^18.0.0",
|
|
26
|
+
"@babel/cli": "^7.0.0",
|
|
27
|
+
"@babel/core": "^7.0.0",
|
|
28
|
+
"@babel/preset-env": "^7.0.0",
|
|
29
|
+
"@babel/preset-react": "^7.0.0"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"src/",
|
|
33
|
+
"dist/"
|
|
34
|
+
],
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/james-evolution/evolution-theme-engine.git"
|
|
38
|
+
},
|
|
39
|
+
"main": "dist/index.js",
|
|
40
|
+
"exports": {
|
|
41
|
+
".": "./dist/index.js"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "node build.js"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ============================================================
|
|
3
|
+
* ThemeNavBar.jsx — Evolution Theme Engine
|
|
4
|
+
* ============================================================
|
|
5
|
+
* An optional, barebones navigation bar with the ThemeSelector
|
|
6
|
+
* component already rendered inside it.
|
|
7
|
+
*
|
|
8
|
+
* This is the "batteries included" option — import ThemeNavBar
|
|
9
|
+
* if you want a ready-made header without wiring up ThemeSelector
|
|
10
|
+
* yourself. Under the hood it simply renders <ThemeSelector />,
|
|
11
|
+
* so everything still flows through ThemeContext.
|
|
12
|
+
*
|
|
13
|
+
* The navbar is intentionally minimal and styled purely via
|
|
14
|
+
* CSS variables (no Bootstrap, no third-party UI libraries).
|
|
15
|
+
* Customize it by passing props or overriding .etn-navbar CSS
|
|
16
|
+
* classes in your own stylesheet.
|
|
17
|
+
*
|
|
18
|
+
* Props:
|
|
19
|
+
* title (string) — Brand/title text shown on the
|
|
20
|
+
* left side of the navbar.
|
|
21
|
+
* Defaults to 'My App'.
|
|
22
|
+
* links (Array<object>) — Navigation links rendered to
|
|
23
|
+
* the right of the title. Each
|
|
24
|
+
* entry: { label, href }.
|
|
25
|
+
* Defaults to a few placeholder
|
|
26
|
+
* links.
|
|
27
|
+
* themes (object) — Forwarded to <ThemeSelector>.
|
|
28
|
+
* Defaults to all 5 built-in themes.
|
|
29
|
+
* className (string) — Extra class(es) added to the
|
|
30
|
+
* root <nav> element alongside
|
|
31
|
+
* 'etn-navbar'.
|
|
32
|
+
* style (object) — Inline styles for the root <nav>.
|
|
33
|
+
*
|
|
34
|
+
* Usage:
|
|
35
|
+
* import { ThemeNavBar } from 'evolution-theme-engine';
|
|
36
|
+
*
|
|
37
|
+
* <ThemeNavBar
|
|
38
|
+
* title="My Cool App"
|
|
39
|
+
* links={[
|
|
40
|
+
* { label: 'Home', href: '/' },
|
|
41
|
+
* { label: 'About', href: '/about' },
|
|
42
|
+
* ]}
|
|
43
|
+
* />
|
|
44
|
+
* ============================================================
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import { ThemeSelector } from './ThemeSelector.jsx';
|
|
48
|
+
import '../styles/themes.css';
|
|
49
|
+
|
|
50
|
+
const DEFAULT_LINKS = [
|
|
51
|
+
{ label: 'Home', href: '#' },
|
|
52
|
+
{ label: 'About', href: '#' },
|
|
53
|
+
{ label: 'Docs', href: '#' },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
export function ThemeNavBar({
|
|
57
|
+
title = 'My App',
|
|
58
|
+
links = DEFAULT_LINKS,
|
|
59
|
+
themes,
|
|
60
|
+
className = '',
|
|
61
|
+
style = {},
|
|
62
|
+
}) {
|
|
63
|
+
return (
|
|
64
|
+
<nav
|
|
65
|
+
className={`etn-navbar${className ? ` ${className}` : ''}`}
|
|
66
|
+
style={style}
|
|
67
|
+
>
|
|
68
|
+
{/* Left side: brand/title */}
|
|
69
|
+
<span className="etn-navbar-brand">{title}</span>
|
|
70
|
+
|
|
71
|
+
{/* Center: navigation links */}
|
|
72
|
+
<ul className="etn-navbar-links">
|
|
73
|
+
{links.map(({ label, href }) => (
|
|
74
|
+
<li key={label}>
|
|
75
|
+
<a href={href} className="etn-navbar-link">
|
|
76
|
+
{label}
|
|
77
|
+
</a>
|
|
78
|
+
</li>
|
|
79
|
+
))}
|
|
80
|
+
</ul>
|
|
81
|
+
|
|
82
|
+
{/* Right side: ThemeSelector dropdown */}
|
|
83
|
+
<ThemeSelector themes={themes} />
|
|
84
|
+
</nav>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ============================================================
|
|
3
|
+
* ThemeSelector.jsx — Evolution Theme Engine
|
|
4
|
+
* ============================================================
|
|
5
|
+
* A standalone, dependency-free <select> dropdown that lets
|
|
6
|
+
* users switch between themes.
|
|
7
|
+
*
|
|
8
|
+
* This component has NO required props — it reads the current
|
|
9
|
+
* theme and setter from ThemeContext via useTheme(). All you
|
|
10
|
+
* need to do is render it anywhere inside a <ThemeProvider>.
|
|
11
|
+
*
|
|
12
|
+
* Props:
|
|
13
|
+
* themes (object) — Map of { label: themeKey } entries
|
|
14
|
+
* displayed in the dropdown.
|
|
15
|
+
* Defaults to all 5 built-in themes.
|
|
16
|
+
* className (string) — Extra CSS class(es) added to the
|
|
17
|
+
* <select> element alongside the
|
|
18
|
+
* default 'etn-theme-selector' class.
|
|
19
|
+
* style (object) — Inline styles applied to the select.
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* import { ThemeSelector } from 'evolution-theme-engine';
|
|
23
|
+
*
|
|
24
|
+
* // Standalone — render anywhere inside ThemeProvider:
|
|
25
|
+
* <ThemeSelector />
|
|
26
|
+
*
|
|
27
|
+
* // With custom theme list:
|
|
28
|
+
* <ThemeSelector
|
|
29
|
+
* themes={{ 'Light': 'light', 'Dark': 'dark', 'Ocean': 'ocean' }}
|
|
30
|
+
* />
|
|
31
|
+
* ============================================================
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { useTheme } from '../context/ThemeContext.jsx';
|
|
35
|
+
import '../styles/themes.css';
|
|
36
|
+
|
|
37
|
+
/*
|
|
38
|
+
* DEFAULT_THEMES is what shows up in the dropdown when no
|
|
39
|
+
* `themes` prop is passed. Keys are display labels; values
|
|
40
|
+
* are the data-theme attribute strings defined in themes.css.
|
|
41
|
+
*/
|
|
42
|
+
const DEFAULT_THEMES = {
|
|
43
|
+
'Light Theme': 'light',
|
|
44
|
+
'Dark Theme': 'dark',
|
|
45
|
+
'Forest': 'forest',
|
|
46
|
+
'Tron': 'tron',
|
|
47
|
+
'Midnight': 'midnight',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export function ThemeSelector({ themes = DEFAULT_THEMES, className = '', style = {} }) {
|
|
51
|
+
const { theme, setTheme } = useTheme();
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<select
|
|
55
|
+
className={`etn-theme-selector${className ? ` ${className}` : ''}`}
|
|
56
|
+
value={theme}
|
|
57
|
+
onChange={(e) => setTheme(e.target.value)}
|
|
58
|
+
aria-label="Select theme"
|
|
59
|
+
style={style}
|
|
60
|
+
>
|
|
61
|
+
{Object.entries(themes).map(([label, value]) => (
|
|
62
|
+
<option key={value} value={value}>
|
|
63
|
+
{label}
|
|
64
|
+
</option>
|
|
65
|
+
))}
|
|
66
|
+
</select>
|
|
67
|
+
);
|
|
68
|
+
}
|