north-star / commit
added dark mode
src/app.js | 30 ++++++++++++++++- src/index.html | 1 + src/style.css | 102 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+), 1 deletion(-) diff --git a/src/app.js b/src/app.js index ddab320..7ddb00a 100644 --- a/src/app.js +++ b/src/app.js @@ -14,6 +14,7 @@ const selfColorInput = document.getElementById('self-color-input'); const roomCurrent = document.getElementById('room-current'); const inviteRoomQrList = document.getElementById('invite-room-qr-list'); const calibrateHeadingLink = document.getElementById('calibrate-heading-link'); +const darkModeToggle = document.getElementById('dark-mode-toggle'); const createRoomLink = document.getElementById('create-room-link'); const participantsPanel = document.getElementById('participants-panel'); const participantsToggle = document.getElementById('participants-toggle'); @@ -34,12 +35,27 @@ const normalizeSignedAngle = (angle) => { const RENDER_INTERVAL_MS = 100; const LOCATION_SYNC_INTERVAL_MS = 3000; const HEADING_CORRECTION_STORAGE_KEY = 'north-star-heading-correction-deg'; +const DARK_MODE_STORAGE_KEY = 'north-star-dark-mode'; let headingCorrectionDeg = (() => { const parsed = Number(localStorage.getItem(HEADING_CORRECTION_STORAGE_KEY)); return Number.isFinite(parsed) ? parsed : 0; })(); let isLocalCommit = true; let GIT_COMMIT = 'local-dev'; + +const applyDarkMode = (dark) => { + document.body.classList.toggle('dark', dark); + if (darkModeToggle) darkModeToggle.textContent = dark ? '☀️' : '🌙'; + localStorage.setItem(DARK_MODE_STORAGE_KEY, dark ? '1' : '0'); +}; +const storedDarkMode = localStorage.getItem(DARK_MODE_STORAGE_KEY); +const prefersDark = storedDarkMode !== null + ? storedDarkMode === '1' + : window.matchMedia('(prefers-color-scheme: dark)').matches; +applyDarkMode(prefersDark); +if (darkModeToggle) { + darkModeToggle.addEventListener('click', () => applyDarkMode(!document.body.classList.contains('dark'))); +} let wakeLockSentinel = null; const hasWakeLockSupport = typeof navigator !== 'undefined' && 'wakeLock' in navigator; @@ -534,6 +550,18 @@ const getReadableTextColor = (hexColor) => { return luminance > 145 ? '#0f172a' : '#f8fafc'; }; +// In dark mode, swap colors that are too dark to see on a dark background to white. +const getArrowDisplayColor = (hexColor) => { + if (!document.body.classList.contains('dark')) return hexColor; + const normalized = normalizeColor(hexColor); + if (!normalized) return '#ffffff'; + const r = parseInt(normalized.slice(1, 3), 16) / 255; + const g = parseInt(normalized.slice(3, 5), 16) / 255; + const b = parseInt(normalized.slice(5, 7), 16) / 255; + const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b; + return luminance < 0.2 ? '#ffffff' : hexColor; +}; + const getDeviceHint = async () => { // Try high-entropy UA data first (Chromium only). if (navigator.userAgentData) { @@ -1237,7 +1265,7 @@ const render = () => { existingArrow.currentAngle = currentArrowAngle; existingArrow.style.transform = `rotate(${currentArrowAngle}deg)`; - existingArrow.style.color = target.color; + existingArrow.style.color = getArrowDisplayColor(target.color); const arrowLabel = existingArrow.querySelector('.arrow-label'); if (arrowLabel) { diff --git a/src/index.html b/src/index.html index c5b7aa6..672bae6 100644 --- a/src/index.html +++ b/src/index.html @@ -32,6 +32,7 @@ <form id="name-form" class="name-form" autocomplete="off"> <input id="self-name-input" name="selfName" type="text" maxlength="32" enterkeyhint="done" /> <input id="self-color-input" name="selfColor" type="color" /> + <button id="dark-mode-toggle" class="dark-mode-toggle" type="button" aria-label="Toggle dark mode">🌙</button> </form> <div id="default-arrow" style="display: none;" class="arrow"> <div class="arrow-img"></div> diff --git a/src/style.css b/src/style.css index e9bd433..1d6532b 100644 --- a/src/style.css +++ b/src/style.css @@ -204,6 +204,24 @@ a { transform: translateY(1px); } +.dark-mode-toggle { + width: 44px; + min-width: 44px; + height: 44px; + padding: 2px; + border: 1px solid #64748b; + border-radius: 10px; + background: rgba(248, 250, 252, 0.96); + font-size: 22px; + line-height: 1; + cursor: pointer; +} + +body.dark .dark-mode-toggle { + background: #0f172a; + border-color: #475569; +} + #content { position: relative; width: 100%; @@ -423,6 +441,90 @@ a { color: #94a3b8; } +/* ── Dark mode ─────────────────────────────────────────────────── */ +body.dark { + background: #0f172a; + color: #f1f5f9; +} + +body.dark a { + color: #60a5fa; +} + +body.dark .room-current { + color: #94a3b8; +} + +body.dark .room-qr-card { + background: #1e293b; + border-color: #334155; +} + +body.dark .room-qr-title { + color: #94a3b8; +} + +body.dark .room-qr { + background: #1e293b; + filter: invert(1); +} + +body.dark .name-form { + background: rgba(30, 41, 59, 0.96); + border-color: #334155; +} + +body.dark .name-form label { + color: #94a3b8; +} + +body.dark .name-form input { + background: #0f172a; + border-color: #475569; + color: #f1f5f9; +} + +body.dark .name-form input[type="color"] { + background: #0f172a; + border-color: #475569; +} + +body.dark .participants-toggle { + background: rgba(30, 41, 59, 0.96); + border-color: #334155; + color: #f1f5f9; +} + +body.dark .participants-toggle.compass-error { + background: rgba(124, 45, 18, 0.85); +} + +body.dark .participants-panel { + background: rgba(30, 41, 59, 0.96); + border-color: #334155; +} + +body.dark .participant-drag-handle { + background: #0f172a; + border-color: #475569; + color: #94a3b8; +} + +body.dark #debug { + background: rgba(30, 41, 59, 0.96); + color: #94a3b8; + border-color: #334155; +} + +body.dark .arrow-img { + filter: drop-shadow(0 0 2px #fff) drop-shadow(0 0 6px rgba(255, 255, 255, 0.5)); +} + +body.dark .arrow-label { + -webkit-text-stroke: 0.25px rgba(255, 255, 255, 0.9); + text-shadow: 0 1px 4px rgba(0, 0, 0, 0.95); +} + @media (max-width: 720px) { .panel-actions { left: 6px;