Урок 13. Решения

📁 Раздел: Состояния и темы ⏱️ Время изучения: ~50 мин 🎯 Сложность: Средняя

⚡ Кратко

Три решения: карточка с dark:, она же на токенах и код переключателя без мигания.

Решение задания 1

Отчёт готов

Данные обновлены пять минут назад.

<div class="max-w-sm rounded-xl border border-slate-200 bg-white p-5 shadow-md
            dark:border-slate-700 dark:bg-slate-800 dark:shadow-none dark:ring-1 dark:ring-white/10">
  <h3 class="text-sm font-semibold text-slate-900 dark:text-slate-100">Отчёт готов</h3>
  <p class="mt-1 text-sm text-slate-600 dark:text-slate-400">Данные обновлены пять минут назад.</p>
  <button class="mt-4 rounded-md bg-slate-900 px-3 py-2 text-sm font-medium text-white hover:bg-slate-700
                 dark:bg-slate-100 dark:text-slate-900 dark:hover:bg-white">Открыть</button>
</div>

Почему так: в тёмной теме основная кнопка меняется местами по контрасту: светлая кнопка на тёмном фоне. Слепое «сделать её темнее» сделало бы её невидимой.

Решение задания 2

📄 src/index.css
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));

@theme inline {
  --color-surface: var(--surface);
  --color-surface-foreground: var(--surface-foreground);
  --color-subtle: var(--subtle);
  --color-line: var(--line);
}

:root {
  --surface: oklch(1 0 0);
  --surface-foreground: oklch(0.21 0.01 265);
  --subtle: oklch(0.55 0.02 265);
  --line: oklch(0.92 0.01 265);
}

.dark {
  --surface: oklch(0.27 0.01 265);
  --surface-foreground: oklch(0.98 0 0);
  --subtle: oklch(0.72 0.02 265);
  --line: oklch(0.37 0.01 265);
}
<div class="max-w-sm rounded-xl border border-line bg-surface p-5 text-surface-foreground">
  <h3 class="text-sm font-semibold">Отчёт готов</h3>
  <p class="mt-1 text-sm text-subtle">Данные обновлены пять минут назад.</p>
</div>

Почему так: имена описывают роль, а не цвет. Через полгода, когда дизайнер попросит «сделать поверхности чуть теплее», правка будет в четырёх строках, а не в четырёхстах классах.

Решение задания 3

<!-- index.html — до подключения бандла -->
<script>
  (() => {
    const saved = localStorage.getItem('theme') ?? 'system'
    const dark = saved === 'dark' ||
      (saved === 'system' && matchMedia('(prefers-color-scheme: dark)').matches)
    document.documentElement.classList.toggle('dark', dark)
  })()
</script>
// src/theme.ts — переключение темы и запоминание выбора
const KEY = 'theme'

export function applyTheme(theme: 'light' | 'dark' | 'system') {
  const isDark =
    theme === 'dark' ||
    (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)

  document.documentElement.classList.toggle('dark', isDark)
  localStorage.setItem(KEY, theme)
}

// вызывать как можно раньше, до первой отрисовки
applyTheme((localStorage.getItem(KEY) as 'light' | 'dark' | 'system') ?? 'system')

Почему так: синхронный скрипт выполняется до отрисовки, поэтому первый кадр уже правильный. Всё остальное — обычная логика переключателя. Не забудьте подписаться на изменение системной темы, если выбран режим «системная»:

matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
  if ((localStorage.getItem('theme') ?? 'system') === 'system') applyTheme('system')
})