app/src/composables/useTheme.tstypescript
import { ref, computed, watch, type Ref, type ComputedRef } from "vue";

const STORAGE_KEY = "theme-preference";

function getStoredTheme(): string | null {
  return localStorage.getItem(STORAGE_KEY);
}

function setStoredTheme(theme: string): void {
  localStorage.setItem(STORAGE_KEY, theme);
}

function getPreferredTheme(): string {
  const stored = getStoredTheme();
  if (stored) return stored;
  return window.matchMedia("(prefers-color-scheme: dark)").matches
    ? "dark"
    : "light";
}

function setTheme(theme: string): void {
  document.documentElement.setAttribute("data-bs-theme", theme);
}

interface ThemeInstance {
  theme: Ref<string>;
  isDark: ComputedRef<boolean>;
  toggleTheme: () => void;
}

let instance: ThemeInstance | null = null;

/**
 * Singleton composable for managing light/dark color theme.
 *
 * Follows Bootstrap 5.3's official color mode toggler pattern:
 * https://getbootstrap.com/docs/5.3/customize/color-modes/#javascript
 *
 * The inline <script> in index.html sets data-bs-theme before render
 * (no flash). This composable takes over after mount for reactivity.
 */
export function useTheme(): ThemeInstance {
  if (!instance) {
    const theme: Ref<string> = ref(getPreferredTheme());

    const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
    const onOsChange = () => {
      if (!getStoredTheme()) {
        theme.value = getPreferredTheme();
      }
    };
    mediaQuery.addEventListener("change", onOsChange);

    watch(
      theme,
      (val) => {
        setTheme(val);
        setStoredTheme(val);
      },
      { immediate: true }
    );

    const isDark: ComputedRef<boolean> = computed(() => theme.value === "dark");

    function toggleTheme(): void {
      theme.value = isDark.value ? "light" : "dark";
    }

    instance = { theme, isDark, toggleTheme };
  }
  return instance;
}