master
  1---
  2import { t } from '@utils/i18n';
  3import { Icon } from 'astro-icon/components';
  4import type { HTMLAttributes } from 'astro/types';
  5import Button from './Button.astro';
  6
  7interface Props extends Omit<HTMLAttributes<'button'>, 'onclick'> {
  8  showText?: boolean;
  9  useDefaultBtnClass?: boolean;
 10}
 11
 12const { class: className, showText, useDefaultBtnClass, ...rest } = Astro.props;
 13---
 14
 15<Button
 16  class:list={['darkmode-btn swap swap-rotate', className]}
 17  useDefaultClass={useDefaultBtnClass}
 18  {...rest}
 19  data-text-light={t.button.themeToggle.lightMode()}
 20  data-text-dark={t.button.themeToggle.darkMode()}
 21  data-text-auto={t.button.themeToggle.systemMode()}
 22  role="switch"
 23  aria-label={t.button.themeToggle.title()}
 24>
 25  <input type="checkbox" inert />
 26  <Icon
 27    class="darkmode-icon-light swap-off"
 28    name="material-symbols:light-mode-rounded"
 29    role="presentation"
 30    aria-hidden
 31  />
 32  <Icon
 33    class="darkmode-icon-dark swap-on"
 34    name="material-symbols:dark-mode-rounded"
 35    role="presentation"
 36    aria-hidden
 37  />
 38  <Icon
 39    class="darkmode-icon-auto swap-indeterminate"
 40    name="material-symbols:night-sight-auto-rounded"
 41    role="presentation"
 42    aria-hidden
 43  />
 44  <span class={showText ? 'darkmode-text pl-6' : 'sr-only'}></span>
 45</Button>
 46
 47<script>
 48  import { buildConfig } from '@/config';
 49
 50  function setup() {
 51    const darkmodeBtns = document.querySelectorAll('button.darkmode-btn');
 52
 53    // 获取当前主题模式
 54    function getCurrentMode(): 'system' | 'light' | 'dark' {
 55      if (!('darkMode' in localStorage)) {
 56        return 'system';
 57      }
 58      return localStorage.darkMode === 'true' ? 'dark' : 'light';
 59    }
 60
 61    // 获取下一个主题模式
 62    function getNextMode(
 63      currentMode: 'system' | 'light' | 'dark'
 64    ): 'system' | 'light' | 'dark' {
 65      switch (currentMode) {
 66        case 'system':
 67          return 'light';
 68        case 'light':
 69          return 'dark';
 70        case 'dark':
 71          return 'system';
 72      }
 73    }
 74
 75    darkmodeBtns.forEach((btn) => {
 76      const checkbox = btn.querySelector('input[type="checkbox"]') as HTMLInputElement;
 77      const text = btn.querySelector('.darkmode-text') || btn.querySelector('.sr-only');
 78
 79      // 更新UI状态和文本
 80      function updateUI(mode: 'system' | 'light' | 'dark') {
 81        if (mode === 'system') {
 82          checkbox.indeterminate = true;
 83        } else {
 84          checkbox.indeterminate = false;
 85        }
 86        checkbox.checked = mode === 'dark';
 87
 88        const textContent =
 89          mode === 'system'
 90            ? btn.getAttribute('data-text-auto')
 91            : mode === 'dark'
 92              ? btn.getAttribute('data-text-dark')
 93              : btn.getAttribute('data-text-light');
 94
 95        btn.setAttribute('title', textContent || '');
 96
 97        if (text) {
 98          text.textContent = textContent;
 99        }
100      }
101
102      // 点击处理
103      btn.addEventListener('click', () => {
104        const currentMode = getCurrentMode();
105        const nextMode = getNextMode(currentMode);
106
107        // 更新 localStorage
108        if (nextMode === 'system') {
109          localStorage.removeItem('darkMode');
110        } else {
111          localStorage.darkMode = nextMode === 'dark';
112        }
113
114        // 触发主题变化事件
115        const isDark =
116          nextMode === 'system'
117            ? window.matchMedia('(prefers-color-scheme: dark)').matches
118            : nextMode === 'dark';
119
120        document.dispatchEvent(
121          new CustomEvent('blog:darkmode-change', {
122            detail: { isDark, nextMode },
123          })
124        );
125      });
126
127      document.addEventListener('blog:darkmode-change', (e) => {
128        // @ts-expect-error CustomEvent.detail is not defined in TypeScript
129        const { nextMode } = e.detail;
130        updateUI(nextMode);
131      });
132
133      // 初始化 UI
134      updateUI(getCurrentMode());
135    });
136
137    document.addEventListener('blog:darkmode-change', (e) => {
138      // @ts-expect-error CustomEvent.detail is not defined in TypeScript
139      const { isDark } = e.detail;
140      document.documentElement.setAttribute(
141        'data-theme',
142        isDark ? buildConfig.themeNames.dark : buildConfig.themeNames.light
143      );
144    });
145  }
146
147  document.addEventListener('astro:page-load', setup);
148  setup();
149</script>