Kolory motywu (dark / light)
View on GitHubJak działa przełączanie motywu
Przeczytaj to przed dotknięciem czegokolwiek innego w tej kategorii — pozostałe przewodniki zakładają, że wiesz, jak theme, resolvedTheme i data-theme się ze sobą łączą.
Trzy wartości ThemeMode vs. dwa prawdziwe motywy
Plik: resources/js/types/Theme.ts
export type ThemeMode = 'dark' | 'light' | 'system';
export type ResolvedTheme = 'dark' | 'light';
ThemeMode to to, co wybrał użytkownik (w Ustawieniach konta → Preferencje → Motyw interfejsu, tablica themeOptions w AccountSettingsPreferencesTab.tsx zasilająca AccountSettingsThemeCard). ResolvedTheme to to, co faktycznie się stosuje — są tylko dwa prawdziwe motywy; system nigdy nie jest stosowany bezpośrednio, zawsze rozstrzyga się do jednego z pozostałych dwóch.
ThemeProvider, krok po kroku
Plik: resources/js/context/ThemeContext.tsx
const resolveTheme = (theme: ThemeMode): ResolvedTheme => {
if (theme === 'system') {
return typeof window !== 'undefined' &&
window.matchMedia(LIGHT_MEDIA_QUERY).matches
? 'light'
: 'dark';
}
return theme;
};
(LIGHT_MEDIA_QUERY to '(prefers-color-scheme: light)' — zauważ, że domyślne ustawienie systemowe jest traktowane jako dark: jeśli media query nie pasuje — w tym w środowisku testowym/SSR bez matchMedia — system rozstrzyga się do 'dark', nie 'light'.)
Przy montowaniu, theme jest odczytywany z localStorage (THEME_STORAGE_KEY = 'theme'), domyślnie 'dark', jeśli nic nie jest zapisane albo zapisana wartość nie jest poprawnym ThemeMode (zabezpiecza to isThemeMode()). Trzy efekty wykonują faktyczną pracę:
- Za każdym razem, gdy zmienia się
theme, przelicz na noworesolvedThemeprzezresolveTheme(theme). - Tylko gdy
theme === 'system': zasubskrybuj się do eventuchangemedia query i rozstrzygaj na żywo — to właśnie sprawia, że przełączenie wyglądu systemu operacyjnego natychmiast aktualizuje Orbit, bez przeładowania, wyłącznie dla użytkowników, którzy wybrali "System sync." Ktoś, kto wybrał jawniedark/light, nie jest dotknięty zmianami systemowymi. - Za każdym razem, gdy zmienia się
resolvedTheme:document.documentElement.setAttribute('data-theme', resolvedTheme)— ta linia to most między stanem Reacta a CSS wglobal.cssdla każdego renderu po zamontowaniu aplikacji.
setTheme(next) aktualizuje stan i odzwierciedla go w localStorage w tym samym wywołaniu — nie ma osobnego kroku "zapisz", każdy wybór jest persystowany natychmiast.
Drugie miejsce, gdzie ustawiany jest data-theme: zanim React w ogóle się zamontuje
Plik: resources/views/app.blade.php
Inline <script> w <head> — zwykły JS, bez bundla, bez czytającego localStorage Reacta jeszcze — reimplementuje uproszczoną wersję powyższego resolveTheme() i sam ustawia data-theme, czysto po to, żeby sam pierwszy render miał już właściwy motyw zamiast błysnąć dark (wartością domyślną) dla użytkownika z light/system rozstrzygającym się do light, zanim efekt ThemeProvider zdąży się uruchomić:
<script>
(function () {
try {
var stored = localStorage.getItem('theme');
var mode = (stored === 'light' || stored === 'dark' || stored === 'system') ? stored : 'dark';
var resolved = mode === 'system'
? (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark')
: mode;
document.documentElement.setAttribute('data-theme', resolved);
// ... (accent color handling follows — see ../accent-colors/01-add-a-new-accent-color.md step 5)
} catch (e) {}
})();
</script>
To duplikat logiki resolveTheme(), nie współdzielony import — Blade nie może zaimportować funkcji TypeScript. Jeśli kiedykolwiek zmieni się reguła rozstrzygania system w resolveTheme(), zaktualizuj kopię w tym skrypcie w tym samym commicie, inaczej sam pierwszy render i każdy render po zamontowaniu ThemeProvider będą przez chwilę się różnić.
Co faktycznie się zmienia, gdy motyw się przełącza
Tylko atrybut data-theme na <html>. Kolory każdego komponentu aktualizują się natychmiast i automatycznie, ponieważ odwołują się do zmiennych CSS, nie dlatego, że jakikolwiek komponent renderuje się ponownie z powodu zmiany motywu — konsumentami ThemeContext są sam ThemeProvider (dla efektu data-theme) oraz cokolwiek renderuje UI wyboru motywu (theme/setTheme z useTheme() dla stanu zaznaczenia przycisków radio). Komponent, który używa tylko var(--text-color) itp. w swoich klasach Tailwinda, potrzebuje zerowej świadomości motywu — zobacz przewodnik 3.
Testy
resources/js/context/ThemeContext.test.tsx— już pokrywa rozstrzyganiesystemwzględem zamockowanegomatchMedia, rundę zapisu/odczytulocalStorageoraz efekt atrybutudata-theme. Żadne zmiany nie są potrzebne, chyba że dodasz nowyThemeMode(nie pokryty przez żaden przewodnik tutaj, ponieważ dziś nie ma trzeciego prawdziwego motywu —systemto strategia rozstrzygania, nie paleta).
