@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,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
|
+
}
|