+
SLASHED Studio
-
+
v{version}
@@ -73,7 +74,7 @@
{#if overridesCount > 0}
@@ -125,20 +126,20 @@
onclick={onRedo}
disabled={!canRedo}
title="Redo (Ctrl+Shift+Z)"
- class="p-1.5 rounded-lg text-slate-400 hover:text-white hover:bg-white/8 disabled:opacity-25 disabled:pointer-events-none transition-all cursor-pointer"
+ class="p-1.5 rounded-lg text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-black/8 dark:hover:bg-white/8 disabled:opacity-25 disabled:pointer-events-none transition-all cursor-pointer"
>
-
+
+
+
+
+
+ {#if themeState.value === "dark"}
+
+ {:else}
+
+ {/if}
+
-
Reset all overrides?
-
+
+
Reset all overrides?
+
This will clear all {overridesCount} customisation{overridesCount !== 1 ? 's' : ''}.
You can still undo this before saving.
@@ -191,7 +208,7 @@
Cancel
diff --git a/configurator/src/lib/theme.svelte.ts b/configurator/src/lib/theme.svelte.ts
new file mode 100644
index 00000000..aa14cd58
--- /dev/null
+++ b/configurator/src/lib/theme.svelte.ts
@@ -0,0 +1,74 @@
+/**
+ * Studio chrome theme (light/dark) — independent of `previewTheme`, which
+ * only controls which mode the live preview iframe renders in.
+ *
+ * Defaults to the OS preference (prefers-color-scheme) and keeps following
+ * it live until the user explicitly toggles, at which point the choice is
+ * persisted and the OS is no longer consulted.
+ */
+
+export type Theme = "light" | "dark";
+
+const STORAGE_KEY = "slashed-studio-theme";
+
+function readStored(): Theme | null {
+ try {
+ const v = localStorage.getItem(STORAGE_KEY);
+ return v === "light" || v === "dark" ? v : null;
+ } catch {
+ return null;
+ }
+}
+
+function systemTheme(): Theme {
+ return typeof matchMedia !== "undefined" && matchMedia("(prefers-color-scheme: dark)").matches
+ ? "dark"
+ : "light";
+}
+
+const stored = readStored();
+let followSystem = stored === null;
+
+export const themeState = $state<{ value: Theme }>({ value: stored ?? systemTheme() });
+
+let root: HTMLElement | null = null;
+
+function applyToRoot() {
+ root?.classList.toggle("dark", themeState.value === "dark");
+}
+
+/** Register the element whose descendants Tailwind's `dark:` variant keys off. */
+export function bindThemeRoot(el: HTMLElement) {
+ root = el;
+ applyToRoot();
+}
+
+export function setTheme(t: Theme) {
+ followSystem = false;
+ try {
+ localStorage.setItem(STORAGE_KEY, t);
+ } catch {
+ // ignore (private browsing / storage disabled)
+ }
+ themeState.value = t;
+ applyToRoot();
+}
+
+export function toggleTheme() {
+ setTheme(themeState.value === "dark" ? "light" : "dark");
+}
+
+if (typeof matchMedia !== "undefined") {
+ const mql = matchMedia("(prefers-color-scheme: dark)");
+ const onChange = (e: MediaQueryListEvent) => {
+ if (!followSystem) return;
+ themeState.value = e.matches ? "dark" : "light";
+ applyToRoot();
+ };
+ // Safari < 14 only exposes the older addListener/removeListener pair.
+ if (typeof mql.addEventListener === "function") {
+ mql.addEventListener("change", onChange);
+ } else if (typeof mql.addListener === "function") {
+ mql.addListener(onChange);
+ }
+}
diff --git a/configurator/src/main.ts b/configurator/src/main.ts
index 42b6169d..418382ed 100644
--- a/configurator/src/main.ts
+++ b/configurator/src/main.ts
@@ -1,6 +1,7 @@
import { mount } from 'svelte';
import './app.css';
import App from './App.svelte';
+import { bindThemeRoot } from './lib/theme.svelte';
// DOGFOOD: load SLASHED's own token + theme layers at :root so the configurator
// chrome renders with the framework it edits. We load only token/theme layers —
@@ -21,6 +22,9 @@ const target =
let app: ReturnType
| undefined;
if (target) {
target.innerHTML = '';
+ // Apply the persisted/OS-derived theme class before mounting so the first
+ // paint is never wrong-themed (no flash of the other mode).
+ bindThemeRoot(target);
app = mount(App, { target });
}
export default app;