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. */