import { DesktopDuotoneIcon, DesktopRegularIcon, } from "alouette-icons/phosphor-icons/Desktop"; import { MoonDuotoneIcon, MoonRegularIcon, } from "alouette-icons/phosphor-icons/Moon"; import { SunDuotoneIcon, SunRegularIcon, } from "alouette-icons/phosphor-icons/Sun"; import type { ReactNode } from "react"; import type { Accent, AlouetteModeTheme, ColorModePreference, } from "../../core/AlouetteConfig"; import { useControllableValue } from "../../core/useControllableValue"; import { useSystemColorMode } from "../../core/useSystemColorMode"; import type { SVGIconElement } from "../primitives/Icon"; import { RadioButton } from "./RadioButton"; import { RadioButtonGroup } from "./RadioButtonGroup"; /** * `system-lock` (the default) offers light and dark only, and folds `system` * into the chip it resolves to: while the preference is `system` that chip keeps * its sun or moon and adds the desktop badge, and pressing it locks the mode * (pressing it again follows the system again). Coming back to it from the other * chip follows the system instead of locking the same mode by hand. The one the * system does not supply has nothing to follow, so it only ever locks. * * `with-system` offers light, dark and system as three chips instead. */ export type ColorModePickerVariant = "system-lock" | "with-system"; interface ColorModeOptionProps { mode: AlouetteModeTheme; label: string; /** Badge marking this chip as the one the system supplies. */ indicator?: SVGIconElement; onPress: () => void; } function ColorModeOption({ mode, label, indicator, onPress, }: ColorModeOptionProps): ReactNode { if (mode === "dark") { return ( } activeIcon={} indicator={indicator} onPress={onPress} /> ); } return ( } activeIcon={} indicator={indicator} onPress={onPress} /> ); } interface SystemColorModeOptionProps { /** `system` in `with-system`; the mode the system supplies in `system-lock`. */ value: ColorModePreference; label: string; onPress: () => void; } function SystemColorModeOption({ value, label, onPress, }: SystemColorModeOptionProps): ReactNode { return ( } activeIcon={} onPress={onPress} /> ); } interface ColorModeLockOptionProps { mode: AlouetteModeTheme; label: string; /** This chip is the one the system supplies and the preference is `system`. */ followsSystem: boolean; followingSystemLabel: (modeLabel: string) => string; onPress: () => void; } function ColorModeLockOption({ mode, label, followsSystem, followingSystemLabel, onPress, }: ColorModeLockOptionProps): ReactNode { // The glyph never changes: following the system only adds the badge, and says // so in the accessible name — the badge itself is decorative. return ( : undefined} onPress={onPress} /> ); } interface NextLockPreferenceParams { mode: AlouetteModeTheme; preference: ColorModePreference; resolvedMode: AlouetteModeTheme; systemMode: AlouetteModeTheme; } function nextLockPreference({ mode, preference, resolvedMode, systemMode, }: NextLockPreferenceParams): ColorModePreference { // Switching to the other chip: the one the system supplies follows it rather // than locking the same mode by hand — locking it is what a second press is // for. The other chip locks, having nothing to follow. if (mode !== resolvedMode) return mode === systemMode ? "system" : mode; // Pressing the chip already in effect toggles the lock, and only on the one // the system supplies — the other has nothing to follow. if (mode !== systemMode) return mode; return preference === "system" ? mode : "system"; } export interface ColorModePickerProps { /** Controlled preference. */ value?: ColorModePreference; /** Initial preference for uncontrolled usage. Defaults to `system`. */ defaultValue?: ColorModePreference; onValueChange?: (value: ColorModePreference) => void; variant?: ColorModePickerVariant; accent?: Accent; disabled?: boolean; /** Accessible name of the group. */ "aria-label"?: string; lightLabel?: string; darkLabel?: string; /** Name of the system chip, in `with-system`. */ systemLabel?: string; /** Name of the chip the system currently supplies, in `system-lock`. */ followingSystemLabel?: (modeLabel: string) => string; } /** * Light/dark picker: a pill of icon-only chips over the stored * `ColorModePreference`. It only reports the choice — the app applies it, by * passing the preference to a `ScopedTheme`. */ export function ColorModePicker({ value, defaultValue = "system", onValueChange, variant = "system-lock", accent, disabled, "aria-label": ariaLabel = "Color mode", lightLabel = "Light", darkLabel = "Dark", systemLabel = "System", followingSystemLabel = (modeLabel) => `${modeLabel} (system)`, }: ColorModePickerProps): ReactNode { const [preference = "system", setPreference] = useControllableValue({ value, defaultValue, onValueChange, }); const systemMode = useSystemColorMode(); if (variant === "system-lock") { const resolvedMode = preference === "system" ? systemMode : preference; const pressLock = (mode: AlouetteModeTheme) => () => { setPreference( nextLockPreference({ mode, preference, resolvedMode, systemMode }), ); }; return ( // The group tracks the mode in effect, not the preference: `system` has no // chip of its own here, it shows as the chip it resolves to. ); } return ( { setPreference("light"); }} /> { setPreference("dark"); }} /> { setPreference("system"); }} /> ); }