themes.ts 3.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. /**
  2. * Theme registry.
  3. *
  4. * Every theme is a set of CSS custom properties declared in globals.css. The
  5. * two base themes (`light`, `dark`) live on `:root` / `.dark`; the presets add
  6. * a `theme-<id>` class on top of their base mode class, so a dark preset is
  7. * applied as `class="dark theme-midnight"`. Keeping the mode class means the
  8. * `dark:` Tailwind variant and the `.dark`-scoped utilities keep working for
  9. * every preset.
  10. */
  11. export type ThemeId = 'light' | 'dark' | 'graphite' | 'ocean' | 'forest' | 'midnight' | 'carbon'
  12. /** What the user picks: a theme, or "follow the OS". */
  13. export type ThemePreference = ThemeId | 'system'
  14. export interface ThemeDefinition {
  15. id: ThemeId
  16. mode: 'light' | 'dark'
  17. /** Literal colors for the settings preview: [background, surface, primary]. */
  18. swatch: [string, string, string]
  19. }
  20. export const THEMES: ThemeDefinition[] = [
  21. {
  22. id: 'light',
  23. mode: 'light',
  24. swatch: ['oklch(0.98 0.008 85)', 'oklch(0.93 0.04 80)', 'oklch(0.72 0.17 75)'],
  25. },
  26. {
  27. id: 'dark',
  28. mode: 'dark',
  29. swatch: ['oklch(0.12 0.01 70)', 'oklch(0.24 0.03 75)', 'oklch(0.78 0.16 80)'],
  30. },
  31. {
  32. id: 'graphite',
  33. mode: 'light',
  34. swatch: ['oklch(0.98 0.002 250)', 'oklch(0.93 0.02 250)', 'oklch(0.52 0.13 250)'],
  35. },
  36. {
  37. id: 'ocean',
  38. mode: 'light',
  39. swatch: ['oklch(0.98 0.012 220)', 'oklch(0.91 0.045 205)', 'oklch(0.52 0.12 215)'],
  40. },
  41. {
  42. id: 'forest',
  43. mode: 'light',
  44. swatch: ['oklch(0.98 0.008 140)', 'oklch(0.92 0.04 145)', 'oklch(0.5 0.12 150)'],
  45. },
  46. {
  47. id: 'midnight',
  48. mode: 'dark',
  49. swatch: ['oklch(0.16 0.03 260)', 'oklch(0.3 0.05 255)', 'oklch(0.7 0.14 245)'],
  50. },
  51. {
  52. id: 'carbon',
  53. mode: 'dark',
  54. swatch: ['oklch(0.15 0 0)', 'oklch(0.28 0.02 165)', 'oklch(0.72 0.15 165)'],
  55. },
  56. ]
  57. export const THEME_IDS = THEMES.map((t) => t.id)
  58. /** Storage key shared with the pre-hydration script in the root layout. */
  59. export const THEME_STORAGE_KEY = 'torqvoice-theme'
  60. export const DEFAULT_THEME: ThemeId = 'dark'
  61. /** Every class this module may put on <html>, so we can clear them in one go. */
  62. export const ALL_THEME_CLASSES = ['light', 'dark', ...THEMES.map((t) => `theme-${t.id}`)]
  63. export function isThemeId(value: string | null | undefined): value is ThemeId {
  64. return !!value && (THEME_IDS as string[]).includes(value)
  65. }
  66. export function isThemePreference(value: string | null | undefined): value is ThemePreference {
  67. return value === 'system' || isThemeId(value)
  68. }
  69. export function getThemeMode(id: ThemeId): 'light' | 'dark' {
  70. return THEMES.find((t) => t.id === id)?.mode ?? 'dark'
  71. }
  72. /** The classes that make up a theme: its mode, plus a preset class when needed. */
  73. export function getThemeClasses(id: ThemeId): string[] {
  74. const mode = getThemeMode(id)
  75. return id === mode ? [mode] : [mode, `theme-${id}`]
  76. }
  77. /** Swap <html> over to `id`, removing whatever theme was there before. */
  78. export function applyTheme(root: HTMLElement, id: ThemeId) {
  79. root.classList.remove(...ALL_THEME_CLASSES)
  80. root.classList.add(...getThemeClasses(id))
  81. }