diff --git a/index.html b/index.html
index f1d9519c..35010f95 100644
--- a/index.html
+++ b/index.html
@@ -8,6 +8,22 @@
+
diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx
index a21e8cfe..0737359a 100644
--- a/src/components/AppShell.tsx
+++ b/src/components/AppShell.tsx
@@ -1,4 +1,4 @@
-import { useState } from "react";
+import { useState, useEffect } from "react";
import { DndContext, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
import { ObjectPalette } from "./Palette/ObjectPalette";
import { LabelCanvas } from "./Canvas/LabelCanvas";
@@ -27,6 +27,8 @@ import {
PaperAirplaneIcon,
GlobeAltIcon,
XMarkIcon,
+ SunIcon,
+ MoonIcon,
} from "@heroicons/react/16/solid";
import { useLabelStore, useHistory } from "../store/labelStore";
import { localeNames } from "../locales";
@@ -46,6 +48,14 @@ export function AppShell() {
const addPage = useLabelStore((s) => s.addPage);
const locale = useLabelStore((s) => s.locale);
const setLocale = useLabelStore((s) => s.setLocale);
+ const theme = useLabelStore((s) => s.theme);
+ const setTheme = useLabelStore((s) => s.setTheme);
+
+ // Bridge the theme preference to so the CSS variables in
+ // index.css pick it up.
+ useEffect(() => {
+ document.documentElement.setAttribute('data-theme', theme);
+ }, [theme]);
const { undo, redo, pastStates, futureStates } = useHistory();
const canvasSettings = useLabelStore((s) => s.canvasSettings);
const setCanvasSettings = useLabelStore((s) => s.setCanvasSettings);
@@ -116,6 +126,19 @@ export function AppShell() {
+
+
}
maxHeight="260px"
diff --git a/src/index.css b/src/index.css
index 7f0e1f04..aa31c616 100644
--- a/src/index.css
+++ b/src/index.css
@@ -15,18 +15,20 @@
--font-mono: 'IBM Plex Mono', ui-monospace, monospace;
}
-@media (prefers-color-scheme: light) {
- :root {
- --color-bg: #f4f4f8;
- --color-surface: #ffffff;
- --color-surface-2: #ececf3;
- --color-border: #dcdce8;
- --color-border-2: #c8c8d8;
- --color-text: #1a1a2e;
- --color-muted: #8080a8;
- --color-accent: #d97706;
- --color-accent-dim:#fef3c7;
- }
+/* Light palette: applied via `data-theme="light"`. The store seeds the
+ initial theme from prefers-color-scheme on first load and persists the
+ user's explicit choice afterwards, so an OS media-query fallback would
+ only fight with the cached preference. */
+:root[data-theme="light"] {
+ --color-bg: #f4f4f8;
+ --color-surface: #ffffff;
+ --color-surface-2: #ececf3;
+ --color-border: #dcdce8;
+ --color-border-2: #c8c8d8;
+ --color-text: #1a1a2e;
+ --color-muted: #8080a8;
+ --color-accent: #d97706;
+ --color-accent-dim:#fef3c7;
}
*, *::before, *::after {
diff --git a/src/lib/useColorScheme.ts b/src/lib/useColorScheme.ts
index f80afb85..4389211e 100644
--- a/src/lib/useColorScheme.ts
+++ b/src/lib/useColorScheme.ts
@@ -1,4 +1,4 @@
-import { useEffect, useState } from 'react';
+import { useLabelStore } from '../store/labelStore';
export interface CanvasColors {
canvasBg: string;
@@ -40,16 +40,6 @@ export const LIGHT_COLORS: CanvasColors = {
};
export function useColorScheme(): CanvasColors {
- const [isDark, setIsDark] = useState(
- () => window.matchMedia('(prefers-color-scheme: dark)').matches,
- );
-
- useEffect(() => {
- const mql = window.matchMedia('(prefers-color-scheme: dark)');
- const handler = (e: MediaQueryListEvent) => setIsDark(e.matches);
- mql.addEventListener('change', handler);
- return () => mql.removeEventListener('change', handler);
- }, []);
-
- return isDark ? DARK_COLORS : LIGHT_COLORS;
+ const theme = useLabelStore((s) => s.theme);
+ return theme === 'dark' ? DARK_COLORS : LIGHT_COLORS;
}
diff --git a/src/locales/ar.ts b/src/locales/ar.ts
index e473a0e9..2e86692e 100644
--- a/src/locales/ar.ts
+++ b/src/locales/ar.ts
@@ -104,6 +104,9 @@ const ar = {
propertiesTab: 'الخصائص',
layersTab: 'الطبقات',
fontsTab: 'الخطوط',
+ themeToggle: 'تبديل المظهر',
+ themeLight: 'مظهر فاتح',
+ themeDark: 'مظهر داكن',
},
output: {
diff --git a/src/locales/bg.ts b/src/locales/bg.ts
index 62c433bb..0111685b 100644
--- a/src/locales/bg.ts
+++ b/src/locales/bg.ts
@@ -104,6 +104,9 @@ const bg = {
propertiesTab: 'Свойства',
layersTab: 'Слоеве',
fontsTab: 'Шрифтове',
+ themeToggle: 'Превключване на темата',
+ themeLight: 'Светла тема',
+ themeDark: 'Тъмна тема',
},
output: {
diff --git a/src/locales/cs.ts b/src/locales/cs.ts
index 5e255b05..cf8c3aca 100644
--- a/src/locales/cs.ts
+++ b/src/locales/cs.ts
@@ -104,6 +104,9 @@ const cs = {
propertiesTab: 'Vlastnosti',
layersTab: 'Vrstvy',
fontsTab: 'Písma',
+ themeToggle: 'Přepnout motiv',
+ themeLight: 'Světlý motiv',
+ themeDark: 'Tmavý motiv',
},
output: {
diff --git a/src/locales/da.ts b/src/locales/da.ts
index c1e43bb9..6fa8e006 100644
--- a/src/locales/da.ts
+++ b/src/locales/da.ts
@@ -104,6 +104,9 @@ const da = {
propertiesTab: 'Egenskaber',
layersTab: 'Lag',
fontsTab: 'Skrifttyper',
+ themeToggle: 'Skift tema',
+ themeLight: 'Lyst tema',
+ themeDark: 'Mørkt tema',
},
output: {
diff --git a/src/locales/de.ts b/src/locales/de.ts
index 8d333c3c..536b13f7 100644
--- a/src/locales/de.ts
+++ b/src/locales/de.ts
@@ -104,6 +104,9 @@ const de = {
propertiesTab: 'Eigenschaften',
layersTab: 'Ebenen',
fontsTab: 'Schriften',
+ themeToggle: 'Design wechseln',
+ themeLight: 'Helles Design',
+ themeDark: 'Dunkles Design',
},
zebraPrint: {
diff --git a/src/locales/el.ts b/src/locales/el.ts
index f4a27b88..3562a154 100644
--- a/src/locales/el.ts
+++ b/src/locales/el.ts
@@ -104,6 +104,9 @@ const el = {
propertiesTab: 'Ιδιότητες',
layersTab: 'Επίπεδα',
fontsTab: 'Γραμματοσειρές',
+ themeToggle: 'Εναλλαγή θέματος',
+ themeLight: 'Ανοιχτό θέμα',
+ themeDark: 'Σκοτεινό θέμα',
},
output: {
diff --git a/src/locales/en.ts b/src/locales/en.ts
index e76b60d6..c8f319f9 100644
--- a/src/locales/en.ts
+++ b/src/locales/en.ts
@@ -104,6 +104,9 @@ const en = {
propertiesTab: 'Properties',
layersTab: 'Layers',
fontsTab: 'Fonts',
+ themeToggle: 'Toggle theme',
+ themeLight: 'Light theme',
+ themeDark: 'Dark theme',
},
zebraPrint: {
diff --git a/src/locales/es.ts b/src/locales/es.ts
index 18e553d9..f90984ba 100644
--- a/src/locales/es.ts
+++ b/src/locales/es.ts
@@ -104,6 +104,9 @@ const es = {
propertiesTab: 'Propiedades',
layersTab: 'Capas',
fontsTab: 'Fuentes',
+ themeToggle: 'Cambiar tema',
+ themeLight: 'Tema claro',
+ themeDark: 'Tema oscuro',
},
output: {
diff --git a/src/locales/et.ts b/src/locales/et.ts
index 02e19bd8..7e25a0d8 100644
--- a/src/locales/et.ts
+++ b/src/locales/et.ts
@@ -104,6 +104,9 @@ const et = {
propertiesTab: 'Omadused',
layersTab: 'Kihid',
fontsTab: 'Fondid',
+ themeToggle: 'Vaheta teemat',
+ themeLight: 'Hele teema',
+ themeDark: 'Tume teema',
},
output: {
diff --git a/src/locales/fa.ts b/src/locales/fa.ts
index 75b1d6c2..f994a9dd 100644
--- a/src/locales/fa.ts
+++ b/src/locales/fa.ts
@@ -104,6 +104,9 @@ const fa = {
propertiesTab: 'ویژگیها',
layersTab: 'لایهها',
fontsTab: 'فونتها',
+ themeToggle: 'تغییر تم',
+ themeLight: 'تم روشن',
+ themeDark: 'تم تیره',
},
output: {
diff --git a/src/locales/fi.ts b/src/locales/fi.ts
index 7f40c9ef..2a9b4685 100644
--- a/src/locales/fi.ts
+++ b/src/locales/fi.ts
@@ -104,6 +104,9 @@ const fi = {
propertiesTab: 'Ominaisuudet',
layersTab: 'Tasot',
fontsTab: 'Fontit',
+ themeToggle: 'Vaihda teema',
+ themeLight: 'Vaalea teema',
+ themeDark: 'Tumma teema',
},
output: {
diff --git a/src/locales/fr.ts b/src/locales/fr.ts
index 8dbe5eec..bccc3b38 100644
--- a/src/locales/fr.ts
+++ b/src/locales/fr.ts
@@ -104,6 +104,9 @@ const fr = {
propertiesTab: 'Propriétés',
layersTab: 'Calques',
fontsTab: 'Polices',
+ themeToggle: 'Changer de thème',
+ themeLight: 'Thème clair',
+ themeDark: 'Thème sombre',
},
output: {
diff --git a/src/locales/he.ts b/src/locales/he.ts
index 29f663dd..1efa18cf 100644
--- a/src/locales/he.ts
+++ b/src/locales/he.ts
@@ -104,6 +104,9 @@ const he = {
propertiesTab: 'מאפיינים',
layersTab: 'שכבות',
fontsTab: 'גופנים',
+ themeToggle: 'החלפת ערכת נושא',
+ themeLight: 'ערכת נושא בהירה',
+ themeDark: 'ערכת נושא כהה',
},
output: {
diff --git a/src/locales/hr.ts b/src/locales/hr.ts
index e89ab4ad..d5b5244e 100644
--- a/src/locales/hr.ts
+++ b/src/locales/hr.ts
@@ -104,6 +104,9 @@ const hr = {
propertiesTab: 'Svojstva',
layersTab: 'Slojevi',
fontsTab: 'Fontovi',
+ themeToggle: 'Promijeni temu',
+ themeLight: 'Svijetla tema',
+ themeDark: 'Tamna tema',
},
output: {
diff --git a/src/locales/hu.ts b/src/locales/hu.ts
index cfe52eed..f87c9cab 100644
--- a/src/locales/hu.ts
+++ b/src/locales/hu.ts
@@ -104,6 +104,9 @@ const hu = {
propertiesTab: 'Tulajdonságok',
layersTab: 'Rétegek',
fontsTab: 'Betűtípusok',
+ themeToggle: 'Téma váltása',
+ themeLight: 'Világos téma',
+ themeDark: 'Sötét téma',
},
output: {
diff --git a/src/locales/it.ts b/src/locales/it.ts
index 876eb1d6..0db72e7f 100644
--- a/src/locales/it.ts
+++ b/src/locales/it.ts
@@ -104,6 +104,9 @@ const it = {
propertiesTab: 'Proprietà',
layersTab: 'Livelli',
fontsTab: 'Caratteri',
+ themeToggle: 'Cambia tema',
+ themeLight: 'Tema chiaro',
+ themeDark: 'Tema scuro',
},
output: {
diff --git a/src/locales/ja.ts b/src/locales/ja.ts
index 8b425201..4ead9c31 100644
--- a/src/locales/ja.ts
+++ b/src/locales/ja.ts
@@ -104,6 +104,9 @@ const ja = {
propertiesTab: 'プロパティ',
layersTab: 'レイヤー',
fontsTab: 'フォント',
+ themeToggle: 'テーマを切り替える',
+ themeLight: 'ライトテーマ',
+ themeDark: 'ダークテーマ',
},
output: {
diff --git a/src/locales/ko.ts b/src/locales/ko.ts
index 9cc07df7..b5361322 100644
--- a/src/locales/ko.ts
+++ b/src/locales/ko.ts
@@ -104,6 +104,9 @@ const ko = {
propertiesTab: '속성',
layersTab: '레이어',
fontsTab: '글꼴',
+ themeToggle: '테마 전환',
+ themeLight: '라이트 테마',
+ themeDark: '다크 테마',
},
output: {
diff --git a/src/locales/lt.ts b/src/locales/lt.ts
index 90589cd1..cb573169 100644
--- a/src/locales/lt.ts
+++ b/src/locales/lt.ts
@@ -104,6 +104,9 @@ const lt = {
propertiesTab: 'Savybės',
layersTab: 'Sluoksniai',
fontsTab: 'Šriftai',
+ themeToggle: 'Perjungti temą',
+ themeLight: 'Šviesi tema',
+ themeDark: 'Tamsi tema',
},
output: {
diff --git a/src/locales/lv.ts b/src/locales/lv.ts
index 3aa5c6d3..f7612a79 100644
--- a/src/locales/lv.ts
+++ b/src/locales/lv.ts
@@ -104,6 +104,9 @@ const lv = {
propertiesTab: 'Īpašības',
layersTab: 'Slāņi',
fontsTab: 'Fonti',
+ themeToggle: 'Pārslēgt motīvu',
+ themeLight: 'Gaišais motīvs',
+ themeDark: 'Tumšais motīvs',
},
output: {
diff --git a/src/locales/nl.ts b/src/locales/nl.ts
index 03e4d08d..4f5966f4 100644
--- a/src/locales/nl.ts
+++ b/src/locales/nl.ts
@@ -104,6 +104,9 @@ const nl = {
propertiesTab: 'Eigenschappen',
layersTab: 'Lagen',
fontsTab: 'Lettertypen',
+ themeToggle: 'Thema wisselen',
+ themeLight: 'Licht thema',
+ themeDark: 'Donker thema',
},
output: {
diff --git a/src/locales/no.ts b/src/locales/no.ts
index fcfbd43c..51581d68 100644
--- a/src/locales/no.ts
+++ b/src/locales/no.ts
@@ -104,6 +104,9 @@ const no = {
propertiesTab: 'Egenskaper',
layersTab: 'Lag',
fontsTab: 'Skrifter',
+ themeToggle: 'Bytt tema',
+ themeLight: 'Lyst tema',
+ themeDark: 'Mørkt tema',
},
output: {
diff --git a/src/locales/pl.ts b/src/locales/pl.ts
index 62990b3b..e116f2ff 100644
--- a/src/locales/pl.ts
+++ b/src/locales/pl.ts
@@ -104,6 +104,9 @@ const pl = {
propertiesTab: 'Właściwości',
layersTab: 'Warstwy',
fontsTab: 'Czcionki',
+ themeToggle: 'Przełącz motyw',
+ themeLight: 'Jasny motyw',
+ themeDark: 'Ciemny motyw',
},
output: {
diff --git a/src/locales/pt.ts b/src/locales/pt.ts
index 5c95768a..20e05dc8 100644
--- a/src/locales/pt.ts
+++ b/src/locales/pt.ts
@@ -104,6 +104,9 @@ const pt = {
propertiesTab: 'Propriedades',
layersTab: 'Camadas',
fontsTab: 'Fontes',
+ themeToggle: 'Alternar tema',
+ themeLight: 'Tema claro',
+ themeDark: 'Tema escuro',
},
output: {
diff --git a/src/locales/ro.ts b/src/locales/ro.ts
index 1b6fa222..fa6f8c85 100644
--- a/src/locales/ro.ts
+++ b/src/locales/ro.ts
@@ -104,6 +104,9 @@ const ro = {
propertiesTab: 'Proprietăți',
layersTab: 'Straturi',
fontsTab: 'Fonturi',
+ themeToggle: 'Comutare temă',
+ themeLight: 'Temă luminoasă',
+ themeDark: 'Temă întunecată',
},
output: {
diff --git a/src/locales/sk.ts b/src/locales/sk.ts
index 89c0e306..af84af8b 100644
--- a/src/locales/sk.ts
+++ b/src/locales/sk.ts
@@ -104,6 +104,9 @@ const sk = {
propertiesTab: 'Vlastnosti',
layersTab: 'Vrstvy',
fontsTab: 'Písma',
+ themeToggle: 'Prepnúť motív',
+ themeLight: 'Svetlý motív',
+ themeDark: 'Tmavý motív',
},
output: {
diff --git a/src/locales/sl.ts b/src/locales/sl.ts
index 1674d821..26fe5218 100644
--- a/src/locales/sl.ts
+++ b/src/locales/sl.ts
@@ -104,6 +104,9 @@ const sl = {
propertiesTab: 'Lastnosti',
layersTab: 'Plasti',
fontsTab: 'Pisave',
+ themeToggle: 'Preklopi temo',
+ themeLight: 'Svetla tema',
+ themeDark: 'Temna tema',
},
output: {
diff --git a/src/locales/sr.ts b/src/locales/sr.ts
index 49449463..1990c3dc 100644
--- a/src/locales/sr.ts
+++ b/src/locales/sr.ts
@@ -104,6 +104,9 @@ const sr = {
propertiesTab: 'Својства',
layersTab: 'Слојеви',
fontsTab: 'Фонтови',
+ themeToggle: 'Промени тему',
+ themeLight: 'Светла тема',
+ themeDark: 'Тамна тема',
},
output: {
diff --git a/src/locales/sv.ts b/src/locales/sv.ts
index 6c8c3b4a..548746da 100644
--- a/src/locales/sv.ts
+++ b/src/locales/sv.ts
@@ -104,6 +104,9 @@ const sv = {
propertiesTab: 'Egenskaper',
layersTab: 'Lager',
fontsTab: 'Typsnitt',
+ themeToggle: 'Växla tema',
+ themeLight: 'Ljust tema',
+ themeDark: 'Mörkt tema',
},
output: {
diff --git a/src/locales/tr.ts b/src/locales/tr.ts
index 41b580b6..844635b8 100644
--- a/src/locales/tr.ts
+++ b/src/locales/tr.ts
@@ -104,6 +104,9 @@ const tr = {
propertiesTab: 'Özellikler',
layersTab: 'Katmanlar',
fontsTab: 'Yazı Tipleri',
+ themeToggle: 'Temayı değiştir',
+ themeLight: 'Açık tema',
+ themeDark: 'Koyu tema',
},
output: {
diff --git a/src/locales/zh-hans.ts b/src/locales/zh-hans.ts
index 859974c7..a39fff8e 100644
--- a/src/locales/zh-hans.ts
+++ b/src/locales/zh-hans.ts
@@ -104,6 +104,9 @@ const zhHans = {
propertiesTab: '属性',
layersTab: '图层',
fontsTab: '字体',
+ themeToggle: '切换主题',
+ themeLight: '浅色主题',
+ themeDark: '深色主题',
},
output: {
diff --git a/src/locales/zh-hant.ts b/src/locales/zh-hant.ts
index fb1ad34e..7153c9ba 100644
--- a/src/locales/zh-hant.ts
+++ b/src/locales/zh-hant.ts
@@ -104,6 +104,9 @@ const zhHant = {
propertiesTab: '屬性',
layersTab: '圖層',
fontsTab: '字體',
+ themeToggle: '切換主題',
+ themeLight: '淺色主題',
+ themeDark: '深色主題',
},
output: {
diff --git a/src/store/labelStore.ts b/src/store/labelStore.ts
index d42e4ca5..33e497a3 100644
--- a/src/store/labelStore.ts
+++ b/src/store/labelStore.ts
@@ -30,6 +30,12 @@ function detectLocale(): LocaleCode {
return (lang in locales ? lang : 'en') as LocaleCode;
}
+function detectInitialTheme(): 'light' | 'dark' {
+ return window.matchMedia('(prefers-color-scheme: dark)').matches
+ ? 'dark'
+ : 'light';
+}
+
export interface CanvasSettings {
showGrid: boolean;
snapEnabled: boolean;
@@ -39,12 +45,17 @@ export interface CanvasSettings {
viewRotation: ViewRotation;
}
+export type ThemePreference = 'light' | 'dark';
+
interface LabelState {
label: LabelConfig;
pages: Page[];
currentPageIndex: number;
selectedIds: string[];
locale: LocaleCode;
+ /** UI theme. Initial value seeded from prefers-color-scheme; once toggled
+ * the explicit choice persists. */
+ theme: ThemePreference;
canvasSettings: CanvasSettings;
clipboard: LabelObject[];
@@ -65,6 +76,7 @@ interface LabelState {
removeSelectedObjects: () => void;
setLabelConfig: (config: Partial) => void;
setLocale: (locale: LocaleCode) => void;
+ setTheme: (theme: ThemePreference) => void;
setCanvasSettings: (settings: Partial) => void;
loadDesign: (label: LabelConfig, pages: Page[]) => void;
moveObjectForward: (id: string) => void;
@@ -127,6 +139,7 @@ export const useLabelStore = create()(
pasteCount: 0,
duplicateCount: 0,
locale: detectLocale(),
+ theme: detectInitialTheme(),
canvasSettings: { showGrid: false, snapEnabled: false, snapSizeMm: 1, zoom: 1, unit: 'mm', viewRotation: 0 },
addObject: (type, position = { x: 50, y: 50 }) => {
@@ -347,6 +360,8 @@ export const useLabelStore = create()(
setLocale: (locale) => set({ locale }),
+ setTheme: (theme) => set({ theme }),
+
setCanvasSettings: (settings) =>
set((state) => ({ canvasSettings: { ...state.canvasSettings, ...settings } })),
@@ -425,6 +440,7 @@ export const useLabelStore = create()(
pages: state.pages,
currentPageIndex: state.currentPageIndex,
locale: state.locale,
+ theme: state.theme,
canvasSettings: state.canvasSettings,
}),
}
diff --git a/src/test/setup.ts b/src/test/setup.ts
index 8b977eac..f2cfa7c9 100644
--- a/src/test/setup.ts
+++ b/src/test/setup.ts
@@ -45,6 +45,18 @@ Object.defineProperty(globalThis, 'navigator', {
value: { language: 'en-US' } as Partial,
});
+// ── window (matchMedia is read at store init for the initial theme) ──────────
+Object.defineProperty(globalThis, 'window', {
+ configurable: true,
+ value: {
+ matchMedia: () => ({
+ matches: false,
+ addEventListener: () => { /* noop */ },
+ removeEventListener: () => { /* noop */ },
+ }),
+ } as unknown as Window,
+});
+
// ── document (canvas stub – used by ^GFA parser path) ────────────────────────
/** Minimal ImageData stub for canvas operations in Node. */