Franknoirot/live system theme (#358)

* Only show the Replay Onboarding button in file settings
Resolves #351. Eventually we will implement more sophisticated
logic for which settings should be shown where.

Signed-off-by: Frank Noirot <frank@kittycad.io>

* Remove unnecessary console.log

Signed-off-by: Frank Noirot <frank@kittycad.io>

* Respond to system theme changes in real-time
If the user has their "theme" setting to "system".
I tried to use the [XState invoked callback approach](https://xstate.js.org/docs/guides/communication.html#invoking-callbacks),
but I could not find any way to respond to the latest context/state values within the
media listener; I kept receiving stale state.

Signed-off-by: Frank Noirot <frank@kittycad.io>

---------

Signed-off-by: Frank Noirot <frank@kittycad.io>
This commit is contained in:
Frank Noirot
2023-08-31 09:34:13 -04:00
committed by GitHub
parent 9cbc088ba3
commit 798cbe968a
5 changed files with 42 additions and 14 deletions

View File

@ -15,7 +15,7 @@ import {
settingsMachine, settingsMachine,
} from 'machines/settingsMachine' } from 'machines/settingsMachine'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { setThemeClass } from 'lib/theme' import { setThemeClass, Themes } from 'lib/theme'
import { import {
AnyStateMachine, AnyStateMachine,
ContextFrom, ContextFrom,
@ -87,10 +87,21 @@ export const GlobalStateProvider = ({
commandBarMeta: settingsCommandBarMeta, commandBarMeta: settingsCommandBarMeta,
}) })
useEffect( // Listen for changes to the system theme and update the app theme accordingly
() => setThemeClass(settingsState.context.theme), // This is only done if the theme setting is set to 'system'.
[settingsState.context.theme] // It can't be done in XState (in an invoked callback, for example)
) // because there doesn't seem to be a good way to listen to
// events outside of the machine that also depend on the machine's context
useEffect(() => {
const matcher = window.matchMedia('(prefers-color-scheme: dark)')
const listener = (e: MediaQueryListEvent) => {
if (settingsState.context.theme !== 'system') return
setThemeClass(e.matches ? Themes.Dark : Themes.Light)
}
matcher.addEventListener('change', listener)
return () => matcher.removeEventListener('change', listener)
}, [settingsState.context])
// Auth machine setup // Auth machine setup
const [authState, authSend] = useMachine(authMachine, { const [authState, authSend] = useMachine(authMachine, {

View File

@ -73,8 +73,6 @@ export function createMachineCommand<T extends AnyStateMachine>({
arg.defaultValue as keyof typeof state.context arg.defaultValue as keyof typeof state.context
] as string | undefined ] as string | undefined
console.log(arg.name, { defaultValueFromContext })
const options = const options =
arg.options instanceof Array arg.options instanceof Array
? arg.options.map((o) => ({ ? arg.options.map((o) => ({

View File

@ -4,17 +4,18 @@ export enum Themes {
System = 'system', System = 'system',
} }
// Get the theme from the system settings manually
export function getSystemTheme(): Exclude<Themes, 'system'> { export function getSystemTheme(): Exclude<Themes, 'system'> {
return typeof window !== 'undefined' && return typeof window !== 'undefined' && 'matchMedia' in window
'matchMedia' in window && ? window.matchMedia('(prefers-color-scheme: dark)').matches
window.matchMedia('(prefers-color-scheme: dark)').matches
? Themes.Dark ? Themes.Dark
: Themes.Light : Themes.Light
: Themes.Light
} }
// Set the theme class on the body element
export function setThemeClass(theme: Themes) { export function setThemeClass(theme: Themes) {
const systemTheme = theme === Themes.System && getSystemTheme() if (theme === Themes.Dark) {
if (theme === Themes.Dark || systemTheme === Themes.Dark) {
document.body.classList.add('dark') document.body.classList.add('dark')
} else { } else {
document.body.classList.remove('dark') document.body.classList.remove('dark')

View File

@ -1,7 +1,7 @@
import { assign, createMachine } from 'xstate' import { assign, createMachine } from 'xstate'
import { BaseUnit, baseUnitsUnion } from '../useStore' import { BaseUnit, baseUnitsUnion } from '../useStore'
import { CommandBarMeta } from '../lib/commands' import { CommandBarMeta } from '../lib/commands'
import { Themes } from '../lib/theme' import { Themes, getSystemTheme, setThemeClass } from '../lib/theme'
export const SETTINGS_PERSIST_KEY = 'SETTINGS_PERSIST_KEY' export const SETTINGS_PERSIST_KEY = 'SETTINGS_PERSIST_KEY'
@ -79,6 +79,7 @@ export const settingsMachine = createMachine(
initial: 'idle', initial: 'idle',
states: { states: {
idle: { idle: {
entry: ['setThemeClass'],
on: { on: {
'Set Theme': { 'Set Theme': {
actions: [ actions: [
@ -87,6 +88,7 @@ export const settingsMachine = createMachine(
}), }),
'persistSettings', 'persistSettings',
'toastSuccess', 'toastSuccess',
'setThemeClass',
], ],
target: 'idle', target: 'idle',
internal: true, internal: true,
@ -188,6 +190,13 @@ export const settingsMachine = createMachine(
console.error(e) console.error(e)
} }
}, },
setThemeClass: (context, event) => {
const currentTheme =
event.type === 'Set Theme' ? event.data.theme : context.theme
setThemeClass(
currentTheme === Themes.System ? getSystemTheme() : currentTheme
)
},
}, },
} }
) )

View File

@ -21,6 +21,15 @@ export interface Typegen0 {
| 'Set Theme' | 'Set Theme'
| 'Set Unit System' | 'Set Unit System'
| 'Toggle Debug Panel' | 'Toggle Debug Panel'
setThemeClass:
| 'Set Base Unit'
| 'Set Default Directory'
| 'Set Default Project Name'
| 'Set Onboarding Status'
| 'Set Theme'
| 'Set Unit System'
| 'Toggle Debug Panel'
| 'xstate.init'
toastSuccess: toastSuccess:
| 'Set Base Unit' | 'Set Base Unit'
| 'Set Default Directory' | 'Set Default Directory'