Skip to content

Commit 58eaa89

Browse files
committed
feat: Legacy and GG EZ themes with structural CSS overrides
Add two themes with embedded CSS that goes beyond color variables: - Legacy: beveled surfaces, sunken inputs, navy titlebars, raised buttons, styled scrollbars/sliders/checkboxes, and listbox selection patterns for a retro desktop aesthetic - GG EZ: animated rainbow gradient borders on windows via CSS @Property gradient rotation Theme CSS is stored in ThemeDef.css, injected via <style> tag, and scoped with html[data-theme-style] for specificity. Custom themes can clone and edit CSS in the theme editor. Split monolithic themes.ts into src/themes/ subdirectory. Add --scene-shadow var so 3D label text shadows stay dark in light themes.
1 parent a9427a0 commit 58eaa89

16 files changed

Lines changed: 1184 additions & 244 deletions

index.html

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,36 @@
5454
#loading-screen .bar-outer { width: 80%; max-width: 280px; height: 22px; }
5555
#loading-screen .msg { font-size: 15px; }
5656
}
57+
/* Legacy loading screen */
58+
html[data-theme-style="legacy"] #loading-screen .bar-outer {
59+
border: none;
60+
box-shadow:
61+
inset 1px 1px 0 0 #808080,
62+
inset -1px -1px 0 0 #ffffff,
63+
inset 2px 2px 0 0 #000000,
64+
inset -2px -2px 0 0 #dfdfdf;
65+
}
66+
html[data-theme-style="legacy"] #loading-screen .bar-inner {
67+
background: #000080;
68+
}
69+
/* RGB loading screen */
70+
@property --rgb-angle {
71+
syntax: '<angle>';
72+
inherits: false;
73+
initial-value: 0deg;
74+
}
75+
html[data-theme-style="rgb"] #loading-screen .bar-outer {
76+
border: 2px solid;
77+
border-image: linear-gradient(var(--rgb-angle, 0deg), #ff0000, #ff8800, #ffff00, #00ff00, #0088ff, #8800ff, #ff0088, #ff0000) 1;
78+
animation: rgb-border-rotate 4s linear infinite;
79+
box-shadow: 0 0 12px rgba(100,0,255,0.15), 0 0 30px rgba(0,100,255,0.08);
80+
}
81+
html[data-theme-style="rgb"] #loading-screen .bar-inner {
82+
background: linear-gradient(90deg, #ff0000, #ff8800, #ffff00, #00ff00, #0088ff, #8800ff, #ff0088);
83+
}
84+
@keyframes rgb-border-rotate {
85+
to { --rgb-angle: 360deg; }
86+
}
5787
</style>
5888
<script>
5989
// Apply saved theme before first paint to prevent flash
@@ -70,6 +100,8 @@
70100
'everforest': { '--bg': '#272e33', '--ui-bg': '#2e383c', '--text': '#d3c6aa', '--text-muted': '#9da9a0' },
71101
'tokyo-night': { '--bg': '#1a1b26', '--ui-bg': '#292e42', '--text': '#c0caf5', '--text-muted': '#9aa5ce' },
72102
'dracula': { '--bg': '#282a36', '--ui-bg': '#363848', '--text': '#f8f8f2', '--text-muted': '#bfc0c0' },
103+
'legacy': { '--bg': '#c0c0c0', '--ui-bg': '#c0c0c0', '--text': '#000000', '--text-muted': '#222222', '--border': '#808080', '--warning': '#cc6600' },
104+
'gg-ez': { '--bg': '#050505', '--ui-bg': '#0a0a0a', '--text': '#ffffff', '--text-muted': '#bbbbbb' },
73105
};
74106
if (minimal[id]) {
75107
vars = minimal[id];
@@ -89,9 +121,13 @@
89121
var m = document.querySelector('meta[name="theme-color"]');
90122
if (m) m.setAttribute('content', vars['--bg']);
91123
}
92-
if (id === 'light' || (vars && vars['--bg'] && parseInt(vars['--bg'].replace('#','').slice(0,2), 16) > 128)) {
124+
if (id === 'light' || id === 'legacy' || (vars && vars['--bg'] && parseInt(vars['--bg'].replace('#','').slice(0,2), 16) > 128)) {
93125
document.documentElement.style.setProperty('color-scheme', 'light');
94126
}
127+
var styles = { 'legacy': 'legacy', 'gg-ez': 'rgb' };
128+
if (styles[id]) {
129+
document.documentElement.dataset.themeStyle = styles[id];
130+
}
95131
} catch(e) {}
96132
})();
97133
</script>

src/scene/map-renderer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ export class MapRenderer {
286286
allMarkers.push({ groupId: group.id, mapX, mapY, color });
287287

288288
const label = document.createElement('div');
289-
label.style.cssText = `position:absolute;font-size:11px;color:${group.color};pointer-events:none;white-space:nowrap;display:none;text-shadow:-1px -1px 0 var(--bg),1px -1px 0 var(--bg),-1px 1px 0 var(--bg),1px 1px 0 var(--bg);`;
289+
label.style.cssText = `position:absolute;font-size:11px;color:${group.color};pointer-events:none;white-space:nowrap;display:none;text-shadow:-1px -1px 0 var(--scene-shadow),1px -1px 0 var(--scene-shadow),-1px 1px 0 var(--scene-shadow),1px 1px 0 var(--scene-shadow);`;
290290
label.textContent = m.name;
291291
overlay.appendChild(label);
292292
this.markerLabels2d.push({ div: label, groupId: group.id, mapX, mapY });
@@ -352,7 +352,7 @@ export class MapRenderer {
352352
this.markerData2d.push({ groupId, mapX, mapY, color: c });
353353

354354
const label = document.createElement('div');
355-
label.style.cssText = `position:absolute;font-size:11px;color:${color};pointer-events:none;white-space:nowrap;display:none;text-shadow:-1px -1px 0 var(--bg),1px -1px 0 var(--bg),-1px 1px 0 var(--bg),1px 1px 0 var(--bg);`;
355+
label.style.cssText = `position:absolute;font-size:11px;color:${color};pointer-events:none;white-space:nowrap;display:none;text-shadow:-1px -1px 0 var(--scene-shadow),1px -1px 0 var(--scene-shadow),-1px 1px 0 var(--scene-shadow),1px 1px 0 var(--scene-shadow);`;
356356
label.textContent = m.name;
357357
overlay.appendChild(label);
358358
this.markerLabels2d.push({ div: label, groupId, mapX, mapY });

src/scene/marker-manager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export class MarkerManager {
124124
private createLabel(name: string, colorStr: string): HTMLDivElement {
125125
const label = document.createElement('div');
126126
label.className = 'scene-label';
127-
label.style.cssText = `position:absolute;left:0;top:0;font-size:11px;color:${colorStr};pointer-events:none;white-space:nowrap;display:none;will-change:transform;text-shadow:-1px -1px 0 var(--bg),1px -1px 0 var(--bg),-1px 1px 0 var(--bg),1px 1px 0 var(--bg);`;
127+
label.style.cssText = `position:absolute;left:0;top:0;font-size:11px;color:${colorStr};pointer-events:none;white-space:nowrap;display:none;will-change:transform;text-shadow:-1px -1px 0 var(--scene-shadow),1px -1px 0 var(--scene-shadow),-1px 1px 0 var(--scene-shadow),1px 1px 0 var(--scene-shadow);`;
128128
label.textContent = name;
129129
this.overlay.appendChild(label);
130130
return label;

src/scene/sky-grid-renderer.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export class SkyGridRenderer {
8484
el.style.cssText = `
8585
position: absolute; pointer-events: none; font-size: 11px;
8686
font-family: 'Overpass Mono', monospace; color: var(--sky-grid-label);
87-
text-shadow: 0 0 3px var(--bg); transform: translate(-50%, -50%);
87+
text-shadow: 0 0 3px var(--scene-shadow); transform: translate(-50%, -50%);
8888
white-space: nowrap; display: none; z-index: 5;
8989
`;
9090
overlay.appendChild(el);
@@ -102,7 +102,7 @@ export class SkyGridRenderer {
102102
el.style.cssText = `
103103
position: absolute; pointer-events: none; font-size: 10px;
104104
font-family: 'Overpass Mono', monospace; color: var(--sky-grid-label);
105-
text-shadow: 0 0 3px var(--bg); transform: translate(-50%, -50%);
105+
text-shadow: 0 0 3px var(--scene-shadow); transform: translate(-50%, -50%);
106106
white-space: nowrap; display: none; z-index: 5; opacity: 0.7;
107107
`;
108108
overlay.appendChild(el);
@@ -120,7 +120,7 @@ export class SkyGridRenderer {
120120
el.style.cssText = `
121121
position: absolute; pointer-events: none; font-size: 10px;
122122
font-family: 'Overpass Mono', monospace; color: var(--sky-grid-label);
123-
text-shadow: 0 0 3px var(--bg); transform: translate(-50%, -50%);
123+
text-shadow: 0 0 3px var(--scene-shadow); transform: translate(-50%, -50%);
124124
white-space: nowrap; display: none; z-index: 5;
125125
`;
126126
overlay.appendChild(el);

src/stores/theme.svelte.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { refreshTheme } from '../ui/shared/theme';
66

77
const ACTIVE_KEY = 'satvisor_theme_active';
88
const CUSTOM_KEY = 'satvisor_themes_custom';
9+
const STYLE_EL_ID = 'satvisor-theme-css';
910

1011
class ThemeStore {
1112
activeId = $state(DEFAULT_THEME_ID);
@@ -44,6 +45,12 @@ class ThemeStore {
4445
for (const [prop, value] of Object.entries(theme.vars)) {
4546
root.style.setProperty(prop, value);
4647
}
48+
if (theme.themeStyle) {
49+
root.dataset.themeStyle = theme.themeStyle;
50+
} else {
51+
delete root.dataset.themeStyle;
52+
}
53+
this.injectCss(theme.css);
4754
const meta = document.querySelector('meta[name="theme-color"]');
4855
if (meta) meta.setAttribute('content', theme.vars['--bg']);
4956
refreshTheme();
@@ -69,6 +76,8 @@ class ThemeStore {
6976
builtin: false,
7077
colorScheme: source.colorScheme,
7178
vars: { ...source.vars },
79+
themeStyle: source.themeStyle,
80+
css: source.css,
7281
};
7382
this.customThemes = [...this.customThemes, clone];
7483
this.persistCustom();
@@ -86,6 +95,15 @@ class ThemeStore {
8695
this.persistCustom();
8796
}
8897

98+
/** Update the custom CSS of the active custom theme (live preview) */
99+
updateCss(css: string) {
100+
const theme = this.activeTheme;
101+
if (theme.builtin) return;
102+
theme.css = css || undefined;
103+
this.injectCss(theme.css);
104+
this.persistCustom();
105+
}
106+
89107
/** Update a custom theme's name */
90108
renameTheme(id: string, newName: string) {
91109
this.customThemes = this.customThemes.map(t =>
@@ -122,6 +140,8 @@ class ThemeStore {
122140
name: theme.name,
123141
colorScheme: theme.colorScheme,
124142
vars: theme.vars,
143+
...(theme.themeStyle ? { themeStyle: theme.themeStyle } : {}),
144+
...(theme.css ? { css: theme.css } : {}),
125145
}, null, 2);
126146
}
127147

@@ -138,6 +158,8 @@ class ThemeStore {
138158
builtin: false,
139159
colorScheme: data.colorScheme === 'light' ? 'light' : 'dark',
140160
vars: { ...BUILTIN_THEMES[0].vars, ...data.vars },
161+
themeStyle: data.themeStyle,
162+
css: data.css,
141163
};
142164
this.customThemes = [...this.customThemes, theme];
143165
this.persistCustom();
@@ -147,6 +169,21 @@ class ThemeStore {
147169
}
148170
}
149171

172+
/** Inject or remove the theme's custom CSS <style> tag */
173+
private injectCss(css: string | undefined) {
174+
let el = document.getElementById(STYLE_EL_ID) as HTMLStyleElement | null;
175+
if (css) {
176+
if (!el) {
177+
el = document.createElement('style');
178+
el.id = STYLE_EL_ID;
179+
document.head.appendChild(el);
180+
}
181+
el.textContent = css;
182+
} else {
183+
el?.remove();
184+
}
185+
}
186+
150187
private persistCustom() {
151188
try {
152189
localStorage.setItem(CUSTOM_KEY, JSON.stringify(this.customThemes));

src/styles/global.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
/* ── Scene overlay text (always over dark 3D scene) ── */
7878
--scene-text: rgba(255,255,255,0.75);
7979
--scene-text-dim: rgba(255,255,255,0.45);
80+
--scene-shadow: #000000;
8081

8182
/* ── Misc ── */
8283
--snap-guide: rgba(100,150,255,0.3);

0 commit comments

Comments
 (0)