diff --git a/src/App.vue b/src/App.vue index 6d704e5..c629bd6 100644 --- a/src/App.vue +++ b/src/App.vue @@ -21,7 +21,9 @@ const canInstall = ref(false); const installDismissed = ref(false); const isCoarsePointer = ref(false); const isStandalone = ref(false); +const themePreference = ref('system'); let displayModeMedia = null; +let prefersColorSchemeMedia = null; const installLabel = computed(() => { return isCoarsePointer.value ? t('pwa.installMobile') : t('pwa.installDesktop'); @@ -60,12 +62,46 @@ const handleInstall = async () => { } }; +const resolveSystemTheme = () => { + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; +}; + +const applyTheme = () => { + const nextTheme = themePreference.value === 'system' ? resolveSystemTheme() : themePreference.value; + document.documentElement.dataset.theme = nextTheme; +}; + +const setThemePreference = (value) => { + themePreference.value = value; + if (typeof localStorage !== 'undefined') { + localStorage.setItem('theme', value); + } + applyTheme(); +}; + +const handleSystemThemeChange = () => { + if (themePreference.value === 'system') { + applyTheme(); + } +}; + onMounted(() => { if (!store.loadState()) { store.initGame(); // Inicjalizacja domyślnej gry jeśli brak zapisu } if (typeof window !== 'undefined') { isCoarsePointer.value = window.matchMedia('(pointer: coarse)').matches; + const storedTheme = typeof localStorage !== 'undefined' ? localStorage.getItem('theme') : null; + if (storedTheme === 'light' || storedTheme === 'dark' || storedTheme === 'system') { + themePreference.value = storedTheme; + } + applyTheme(); + prefersColorSchemeMedia = window.matchMedia('(prefers-color-scheme: dark)'); + if (prefersColorSchemeMedia?.addEventListener) { + prefersColorSchemeMedia.addEventListener('change', handleSystemThemeChange); + } else if (prefersColorSchemeMedia?.addListener) { + prefersColorSchemeMedia.addListener(handleSystemThemeChange); + } updateStandalone(); window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt); window.addEventListener('appinstalled', handleAppInstalled); @@ -82,6 +118,11 @@ onUnmounted(() => { if (typeof window === 'undefined') return; window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt); window.removeEventListener('appinstalled', handleAppInstalled); + if (prefersColorSchemeMedia?.removeEventListener) { + prefersColorSchemeMedia.removeEventListener('change', handleSystemThemeChange); + } else if (prefersColorSchemeMedia?.removeListener) { + prefersColorSchemeMedia.removeListener(handleSystemThemeChange); + } if (displayModeMedia?.removeEventListener) { displayModeMedia.removeEventListener('change', updateStandalone); } else if (displayModeMedia?.removeListener) { @@ -96,9 +137,23 @@ onUnmounted(() => {

{{ t('app.title') }}

-
- - +
+
+ + +
+
+ {{ t('theme.label') }} + + + +
@@ -166,8 +221,8 @@ h1 { margin: 0; letter-spacing: 5px; font-weight: 300; - color: #fff; - text-shadow: 0 0 20px rgba(0, 255, 255, 0.2); + color: var(--text-strong); + text-shadow: 0 0 20px var(--title-glow); } .underline { @@ -178,21 +233,29 @@ h1 { box-shadow: 0 0 10px var(--primary-accent); } +.header-toggles { + display: flex; + flex-wrap: wrap; + gap: 10px; + align-items: center; + justify-content: center; + margin-top: 12px; +} + .lang-toggle { display: inline-flex; gap: 8px; - margin-top: 12px; padding: 6px 10px; border-radius: 999px; - background: rgba(255, 255, 255, 0.08); - border: 1px solid rgba(255, 255, 255, 0.2); - box-shadow: 0 0 15px rgba(0, 0, 0, 0.2); + background: var(--toggle-bg); + border: 1px solid var(--toggle-border); + box-shadow: var(--toggle-shadow); } .lang-btn { background: transparent; - border: 1px solid rgba(255, 255, 255, 0.2); - color: #fff; + border: 1px solid var(--toggle-btn-border); + color: var(--text-strong); padding: 4px 10px; border-radius: 999px; font-size: 0.8rem; @@ -203,11 +266,50 @@ h1 { .lang-btn.active { border-color: var(--primary-accent); - box-shadow: 0 0 10px rgba(0, 242, 255, 0.3); + box-shadow: var(--toggle-active-shadow); } .lang-btn:hover { - border-color: #fff; + border-color: var(--toggle-hover-border); +} + +.theme-toggle { + display: inline-flex; + gap: 8px; + align-items: center; + padding: 6px 10px; + border-radius: 999px; + background: var(--toggle-bg); + border: 1px solid var(--toggle-border); + box-shadow: var(--toggle-shadow); +} + +.theme-label { + font-size: 0.75rem; + letter-spacing: 1px; + text-transform: uppercase; + color: var(--text-muted); +} + +.theme-btn { + background: transparent; + border: 1px solid var(--toggle-btn-border); + color: var(--text-strong); + padding: 4px 10px; + border-radius: 999px; + font-size: 0.75rem; + letter-spacing: 1px; + cursor: pointer; + transition: all 0.2s ease; +} + +.theme-btn.active { + border-color: var(--primary-accent); + box-shadow: var(--toggle-active-shadow); +} + +.theme-btn:hover { + border-color: var(--toggle-hover-border); } .game-layout { @@ -226,15 +328,15 @@ h1 { gap: 16px; padding: 12px 16px; border-radius: 16px; - background: rgba(0, 0, 0, 0.35); - border: 1px solid rgba(0, 242, 254, 0.35); - box-shadow: 0 8px 30px rgba(0, 0, 0, 0.25); + background: var(--banner-bg); + border: 1px solid var(--banner-border); + box-shadow: var(--banner-shadow); width: min(680px, 92vw); margin: -10px 0 20px; } .install-text { - color: rgba(255, 255, 255, 0.9); + color: var(--text-secondary); font-size: 0.95rem; letter-spacing: 0.5px; } @@ -252,8 +354,8 @@ h1 { .install-close { background: transparent; - border: 1px solid rgba(255, 255, 255, 0.2); - color: #fff; + border: 1px solid var(--toggle-btn-border); + color: var(--text-strong); width: 32px; height: 32px; border-radius: 999px; @@ -267,7 +369,7 @@ h1 { } .install-close:hover { - border-color: #fff; + border-color: var(--toggle-hover-border); } /* Remove old glass panel style from game-layout since we split it */ diff --git a/src/components/CustomGameModal.vue b/src/components/CustomGameModal.vue index 1b4061c..43ad9f7 100644 --- a/src/components/CustomGameModal.vue +++ b/src/components/CustomGameModal.vue @@ -70,7 +70,7 @@ const confirm = () => { left: 0; width: 100vw; height: 100vh; - background: rgba(0, 0, 0, 0.7); + background: var(--modal-overlay); backdrop-filter: blur(5px); display: flex; justify-content: center; @@ -114,9 +114,9 @@ p { min-width: 64px; padding: 6px 12px; border-radius: 999px; - background: rgba(0, 0, 0, 0.3); - border: 1px solid var(--glass-border); - color: #fff; + background: var(--panel-bg-strong); + border: 1px solid var(--panel-border); + color: var(--text-strong); font-size: 1.1rem; text-align: center; } @@ -128,7 +128,7 @@ input[type="range"] { border-radius: 999px; background: linear-gradient(90deg, var(--accent-cyan), var(--accent-purple)); outline: none; - border: 1px solid rgba(255, 255, 255, 0.2); + border: 1px solid var(--panel-border); box-shadow: 0 0 10px rgba(0, 242, 255, 0.2); } @@ -137,7 +137,7 @@ input[type="range"]::-webkit-slider-thumb { width: 22px; height: 22px; border-radius: 50%; - background: #fff; + background: var(--text-strong); border: 2px solid var(--accent-cyan); box-shadow: 0 0 12px rgba(0, 242, 255, 0.5); cursor: pointer; @@ -147,7 +147,7 @@ input[type="range"]::-moz-range-thumb { width: 22px; height: 22px; border-radius: 50%; - background: #fff; + background: var(--text-strong); border: 2px solid var(--accent-cyan); box-shadow: 0 0 12px rgba(0, 242, 255, 0.5); cursor: pointer; @@ -157,7 +157,7 @@ input[type="range"]::-moz-range-thumb { width: min(300px, 70vw); display: flex; justify-content: space-between; - color: rgba(255, 255, 255, 0.7); + color: var(--text-muted); font-size: 0.85rem; } diff --git a/src/components/FixedBar.vue b/src/components/FixedBar.vue index ed424dd..f23656c 100644 --- a/src/components/FixedBar.vue +++ b/src/components/FixedBar.vue @@ -62,16 +62,16 @@ const toggleVisibility = () => { left: 0; width: 100%; height: 60px; - background: rgba(0, 0, 0, 0.85); + background: var(--fixed-bar-bg); backdrop-filter: blur(10px); z-index: 1000; display: flex; align-items: center; justify-content: center; - border-bottom: 1px solid rgba(255, 255, 255, 0.1); + border-bottom: 1px solid var(--fixed-bar-border); transform: translateY(-100%); transition: transform 0.3s ease; - box-shadow: 0 4px 20px rgba(0,0,0,0.5); + box-shadow: var(--fixed-bar-shadow); } #fixed-bar.visible { @@ -95,13 +95,13 @@ const toggleVisibility = () => { display: flex; gap: 10px; align-items: center; - color: #fff; + color: var(--text-strong); } .fixed-stat span:first-child { - opacity: 0.7; font-size: 0.9rem; text-transform: uppercase; + color: var(--text-muted); } .progress-line-container { @@ -110,7 +110,7 @@ const toggleVisibility = () => { left: 0; width: 100%; height: 3px; - background: rgba(255, 255, 255, 0.1); + background: var(--progress-track-bg); } .progress-line-fill { diff --git a/src/components/GameActions.vue b/src/components/GameActions.vue index 290d00e..ca4b7cc 100644 --- a/src/components/GameActions.vue +++ b/src/components/GameActions.vue @@ -34,15 +34,15 @@ function handleNewRandom() { } .btn-neon.secondary { - border-color: rgba(255, 255, 255, 0.3); - color: rgba(255, 255, 255, 0.8); + border-color: var(--button-secondary-border); + color: var(--button-secondary-text); font-size: 0.9rem; padding: 10px 25px; } .btn-neon.secondary:hover { - background: rgba(255, 255, 255, 0.1); - border-color: #fff; - color: #fff; + background: var(--button-secondary-hover-bg); + border-color: var(--button-secondary-hover-border); + color: var(--button-secondary-hover-text); } diff --git a/src/components/Hints.vue b/src/components/Hints.vue index f170b24..433d89e 100644 --- a/src/components/Hints.vue +++ b/src/components/Hints.vue @@ -68,8 +68,8 @@ defineProps({ display: flex; justify-content: flex-end; align-items: center; - background: rgba(255, 255, 255, 0.05); - border: 1px solid rgba(255, 255, 255, 0.1); + background: var(--hint-bg); + border: 1px solid var(--hint-border); border-radius: 4px; transition: all 0.3s ease; width: 100%; @@ -89,7 +89,7 @@ defineProps({ .hint-num { font-size: 0.85rem; - color: #fff; + color: var(--text-strong); font-weight: bold; padding: 2px; } @@ -101,7 +101,7 @@ defineProps({ /* Hover effect for readability */ .hint-group:hover { - background: rgba(255, 255, 255, 0.1); + background: var(--hint-hover-bg); border-color: var(--accent-cyan); } diff --git a/src/components/StatusPanel.vue b/src/components/StatusPanel.vue index 236ba3a..3ad5274 100644 --- a/src/components/StatusPanel.vue +++ b/src/components/StatusPanel.vue @@ -41,10 +41,10 @@ const progressText = computed(() => `${store.progressPercentage.toFixed(3)}%`); align-items: center; padding: 20px 40px; border-radius: 15px; - background: rgba(255, 255, 255, 0.1); + background: var(--panel-bg); backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.1); - box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); + border: 1px solid var(--panel-border); + box-shadow: var(--panel-shadow); margin-bottom: 30px; width: 100%; max-width: 600px; @@ -60,13 +60,13 @@ const progressText = computed(() => `${store.progressPercentage.toFixed(3)}%`); .label { font-size: 0.8rem; text-transform: uppercase; - color: rgba(255, 255, 255, 0.7); + color: var(--text-muted); letter-spacing: 1px; } .value { font-size: 1.8rem; - color: #fff; + color: var(--text-strong); font-weight: 300; font-family: 'Courier New', monospace; } diff --git a/src/components/WinModal.vue b/src/components/WinModal.vue index 075aeaa..9a8c53d 100644 --- a/src/components/WinModal.vue +++ b/src/components/WinModal.vue @@ -308,7 +308,7 @@ onUnmounted(() => { left: 0; width: 100vw; height: 100vh; - background: rgba(0, 0, 0, 0.7); + background: var(--modal-overlay); backdrop-filter: blur(5px); display: flex; justify-content: center; @@ -355,7 +355,7 @@ p { .stats { margin-bottom: 30px; padding: 20px; - background: rgba(0, 0, 0, 0.3); + background: var(--panel-bg-strong); border-radius: 8px; } @@ -364,7 +364,7 @@ p { } .stat strong { - color: #fff; + color: var(--text-strong); margin-left: 10px; } @@ -379,7 +379,7 @@ p { font-size: 0.95rem; letter-spacing: 1px; text-transform: uppercase; - color: rgba(255, 255, 255, 0.7); + color: var(--text-muted); } .share-buttons { diff --git a/src/composables/useI18n.js b/src/composables/useI18n.js index 9613528..de0e1d2 100644 --- a/src/composables/useI18n.js +++ b/src/composables/useI18n.js @@ -48,7 +48,11 @@ const messages = { 'win.shareDownload': 'Pobierz zrzut', 'pwa.installTitle': 'Zainstaluj aplikację i graj offline', 'pwa.installMobile': 'Dodaj do ekranu głównego', - 'pwa.installDesktop': 'Zainstaluj na komputerze' + 'pwa.installDesktop': 'Zainstaluj na komputerze', + 'theme.label': 'Motyw', + 'theme.system': 'System', + 'theme.light': 'Jasny', + 'theme.dark': 'Ciemny' }, en: { 'app.title': 'Nonograms', @@ -90,7 +94,11 @@ const messages = { 'win.shareDownload': 'Download screenshot', 'pwa.installTitle': 'Install the app and play offline', 'pwa.installMobile': 'Add to home screen', - 'pwa.installDesktop': 'Install on desktop' + 'pwa.installDesktop': 'Install on desktop', + 'theme.label': 'Theme', + 'theme.system': 'System', + 'theme.light': 'Light', + 'theme.dark': 'Dark' } }; diff --git a/src/styles/main.css b/src/styles/main.css index 43f206c..67dabc8 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -5,12 +5,53 @@ --glass-border: rgba(255, 255, 255, 0.2); --glass-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37); --text-color: #ffffff; + --text-strong: #ffffff; + --text-secondary: rgba(255, 255, 255, 0.85); + --text-muted: rgba(255, 255, 255, 0.7); --accent-cyan: #00f2fe; --accent-purple: #4facfe; + --primary-accent: #00f2fe; --cell-empty: rgba(255, 255, 255, 0.05); --cell-hover: rgba(255, 255, 255, 0.2); --cell-filled-gradient: linear-gradient(45deg, #00f2fe, #4facfe); --cell-x-color: rgba(255, 255, 255, 0.4); + --title-glow: rgba(0, 255, 255, 0.2); + --toggle-bg: rgba(255, 255, 255, 0.08); + --toggle-border: rgba(255, 255, 255, 0.2); + --toggle-shadow: 0 0 15px rgba(0, 0, 0, 0.2); + --toggle-btn-border: rgba(255, 255, 255, 0.2); + --toggle-hover-border: #ffffff; + --toggle-active-shadow: 0 0 10px rgba(0, 242, 255, 0.3); + --banner-bg: rgba(0, 0, 0, 0.35); + --banner-border: rgba(0, 242, 254, 0.35); + --banner-shadow: 0 8px 30px rgba(0, 0, 0, 0.25); + --panel-bg: rgba(255, 255, 255, 0.1); + --panel-border: rgba(255, 255, 255, 0.1); + --panel-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); + --panel-bg-strong: rgba(0, 0, 0, 0.3); + --modal-overlay: rgba(0, 0, 0, 0.7); + --fixed-bar-bg: rgba(0, 0, 0, 0.85); + --fixed-bar-border: rgba(255, 255, 255, 0.1); + --fixed-bar-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); + --progress-track-bg: rgba(255, 255, 255, 0.1); + --hint-bg: rgba(255, 255, 255, 0.05); + --hint-border: rgba(255, 255, 255, 0.1); + --hint-hover-bg: rgba(255, 255, 255, 0.1); + --button-bg: rgba(255, 255, 255, 0.1); + --button-border: rgba(255, 255, 255, 0.2); + --button-text: #ffffff; + --button-hover-bg: rgba(255, 255, 255, 0.25); + --button-hover-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); + --button-active-shadow: 0 0 20px rgba(79, 172, 254, 0.4); + --button-secondary-bg: rgba(0, 0, 0, 0.2); + --button-secondary-border: rgba(255, 255, 255, 0.1); + --button-secondary-text: rgba(255, 255, 255, 0.8); + --button-secondary-hover-bg: rgba(255, 255, 255, 0.1); + --button-secondary-hover-border: #ffffff; + --button-secondary-hover-text: #ffffff; + --scroll-track: rgba(0, 0, 0, 0.1); + --scroll-thumb: rgba(255, 255, 255, 0.2); + --scroll-thumb-hover: var(--accent-cyan); /* Rozmiary */ --cell-size: 30px; @@ -19,6 +60,61 @@ --grid-padding: 5px; } +:root[data-theme="light"] { + --bg-gradient: linear-gradient(135deg, #f7fbff 0%, #dde7ff 100%); + --glass-bg: rgba(255, 255, 255, 0.75); + --glass-border: rgba(15, 23, 42, 0.12); + --glass-shadow: 0 8px 32px 0 rgba(15, 23, 42, 0.12); + --text-color: #0f172a; + --text-strong: #0f172a; + --text-secondary: rgba(15, 23, 42, 0.7); + --text-muted: rgba(15, 23, 42, 0.6); + --accent-cyan: #0ea5e9; + --accent-purple: #6366f1; + --primary-accent: #0ea5e9; + --cell-empty: rgba(15, 23, 42, 0.05); + --cell-hover: rgba(15, 23, 42, 0.12); + --cell-filled-gradient: linear-gradient(45deg, #0ea5e9, #6366f1); + --cell-x-color: rgba(15, 23, 42, 0.35); + --title-glow: rgba(14, 165, 233, 0.35); + --toggle-bg: rgba(255, 255, 255, 0.85); + --toggle-border: rgba(15, 23, 42, 0.12); + --toggle-shadow: 0 8px 20px rgba(15, 23, 42, 0.12); + --toggle-btn-border: rgba(15, 23, 42, 0.18); + --toggle-hover-border: rgba(15, 23, 42, 0.5); + --toggle-active-shadow: 0 0 12px rgba(14, 165, 233, 0.25); + --banner-bg: rgba(255, 255, 255, 0.8); + --banner-border: rgba(14, 165, 233, 0.35); + --banner-shadow: 0 12px 28px rgba(15, 23, 42, 0.16); + --panel-bg: rgba(255, 255, 255, 0.7); + --panel-border: rgba(15, 23, 42, 0.12); + --panel-shadow: 0 12px 24px rgba(15, 23, 42, 0.12); + --panel-bg-strong: rgba(15, 23, 42, 0.08); + --modal-overlay: rgba(15, 23, 42, 0.45); + --fixed-bar-bg: rgba(248, 250, 255, 0.95); + --fixed-bar-border: rgba(15, 23, 42, 0.12); + --fixed-bar-shadow: 0 8px 24px rgba(15, 23, 42, 0.18); + --progress-track-bg: rgba(15, 23, 42, 0.08); + --hint-bg: rgba(255, 255, 255, 0.7); + --hint-border: rgba(15, 23, 42, 0.12); + --hint-hover-bg: rgba(15, 23, 42, 0.06); + --button-bg: rgba(255, 255, 255, 0.85); + --button-border: rgba(15, 23, 42, 0.16); + --button-text: #0f172a; + --button-hover-bg: rgba(255, 255, 255, 1); + --button-hover-shadow: 0 6px 18px rgba(15, 23, 42, 0.18); + --button-active-shadow: 0 0 18px rgba(14, 165, 233, 0.25); + --button-secondary-bg: rgba(15, 23, 42, 0.06); + --button-secondary-border: rgba(15, 23, 42, 0.2); + --button-secondary-text: rgba(15, 23, 42, 0.8); + --button-secondary-hover-bg: rgba(15, 23, 42, 0.12); + --button-secondary-hover-border: rgba(15, 23, 42, 0.5); + --button-secondary-hover-text: #0f172a; + --scroll-track: rgba(15, 23, 42, 0.08); + --scroll-thumb: rgba(15, 23, 42, 0.2); + --scroll-thumb-hover: #0ea5e9; +} + * { box-sizing: border-box; user-select: none; @@ -60,9 +156,9 @@ body { /* Button Styles */ button.btn-neon { - background: rgba(255, 255, 255, 0.1); - border: 1px solid rgba(255, 255, 255, 0.2); - color: #fff; + background: var(--button-bg); + border: 1px solid var(--button-border); + color: var(--button-text); padding: 12px 24px; font-size: 0.95rem; border-radius: 30px; @@ -79,21 +175,21 @@ button.btn-neon { } button.btn-neon:hover { - background: rgba(255, 255, 255, 0.25); + background: var(--button-hover-bg); transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(0,0,0,0.2); + box-shadow: var(--button-hover-shadow); } button.btn-neon.active { background: linear-gradient(90deg, var(--accent-cyan), var(--accent-purple)); border-color: transparent; - box-shadow: 0 0 20px rgba(79, 172, 254, 0.4); + box-shadow: var(--button-active-shadow); font-weight: 700; } button.btn-neon.secondary { - border-color: rgba(255, 255, 255, 0.1); - background: rgba(0,0,0,0.2); + border-color: var(--button-secondary-border); + background: var(--button-secondary-bg); } /* Scrollbar */ @@ -101,14 +197,14 @@ button.btn-neon.secondary { width: 8px; } ::-webkit-scrollbar-track { - background: rgba(0,0,0,0.1); + background: var(--scroll-track); } ::-webkit-scrollbar-thumb { - background: rgba(255,255,255,0.2); + background: var(--scroll-thumb); border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { - background: var(--accent-cyan); + background: var(--scroll-thumb-hover); } /* Animations */