ci / check (push) Successful in 23m54s
- UI: settings tab with sections (server, API keys, Prometheus, audit, migrate, about); server settings moved there; master keys table rework (menu, chips, version, compact phone cards); three-state node version chip - I1: random per-server template (DNS / QUIC Initial / STUN) kept in users.db, GET/POST /api/masking, change from the master keys banner - Hub rejects nodes older than 0.5.0 (close 4026), /ui/nodes reports outdated - install.sh: spinner for long commands (full output in the log), step progress bar, cards; installs older than 0.5.0 require a full reinstall with data reset - Bump to Beta 0.5.0
48 lines
2.1 KiB
TypeScript
48 lines
2.1 KiB
TypeScript
// Copyright (c) 2026 Ivan Vasilev
|
|
// This source code is licensed under the MIT license found in the
|
|
// LICENSE file in the root directory of this source tree.
|
|
// Тема панели: светлая, тёмная или как в системе. Одно хранилище на всё приложение —
|
|
// переключатель в шапке и вкладка «Настройки» читают и меняют его через useTheme().
|
|
import { useSyncExternalStore } from 'react';
|
|
import { THEME_KEY } from './shared';
|
|
|
|
export type ThemeMode = 'light' | 'dark' | 'system';
|
|
export type Theme = 'light' | 'dark';
|
|
|
|
const dark = window.matchMedia('(prefers-color-scheme: dark)');
|
|
const listeners = new Set<() => void>();
|
|
|
|
function readMode(): ThemeMode {
|
|
try {
|
|
const v = localStorage.getItem(THEME_KEY);
|
|
if (v === 'dark' || v === 'system') return v;
|
|
} catch { /* не критично */ }
|
|
return 'light'; // без сохранённого выбора — светлая, как было до режима «как в системе»
|
|
}
|
|
|
|
let mode = readMode();
|
|
const resolved = (): Theme => (mode === 'system' ? (dark.matches ? 'dark' : 'light') : mode);
|
|
|
|
function apply() {
|
|
document.documentElement.setAttribute('data-theme', resolved());
|
|
listeners.forEach(l => l());
|
|
}
|
|
// «Как в системе» следует за сменой темы ОС на лету
|
|
dark.addEventListener('change', () => { if (mode === 'system') apply(); });
|
|
apply();
|
|
|
|
export function setThemeMode(next: ThemeMode) {
|
|
mode = next;
|
|
try { localStorage.setItem(THEME_KEY, next); } catch { /* не критично */ }
|
|
apply();
|
|
}
|
|
|
|
const subscribe = (l: () => void) => { listeners.add(l); return () => { listeners.delete(l); }; };
|
|
|
|
// mode — выбор пользователя, theme — что реально применено.
|
|
export function useTheme(): { mode: ThemeMode; theme: Theme } {
|
|
const snap = useSyncExternalStore(subscribe, () => `${mode}:${resolved()}`);
|
|
const [m, t] = snap.split(':') as [ThemeMode, Theme];
|
|
return { mode: m, theme: t };
|
|
}
|