Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,30 @@
(function () {
var lightColor = '#ffffff'
var darkColor = '#1c1c1e'
var oledColor = '#000000'
var themeBackgrounds = {
notion: { light: '#fafafa', dark: '#191919' },
one: { light: '#fbfbff', dark: '#1f2433' },
proof: { light: '#f8f7f2', dark: '#18231f' },
raycast: { light: '#ffffff', dark: '#201617' },
'rose-pine': { light: '#fffaf3', dark: '#191724' },
solarized: { light: '#fdf6e3', dark: '#002b36' },
vercel: { light: '#ffffff', dark: '#000000' },
'vs-code-plus': { light: '#ffffff', dark: '#1e1e1e' },
xcode: { light: '#f7f9ff', dark: '#1f2024' },
linear: { light: '#f7f7fb', dark: '#08090d' },
lobster: { light: '#fff7f4', dark: '#161821' },
material: { light: '#fffbfe', dark: '#1c1b1f' },
matrix: { light: '#f5fff7', dark: '#030806' },
monokai: { light: '#f8f7ef', dark: '#272822' },
'night-owl': { light: '#fbfdff', dark: '#011627' },
nord: { light: '#eceff4', dark: '#2e3440' },
}

function getInitialScheme() {
try {
var appearance = localStorage.getItem('hapi-appearance')
if (appearance === 'dark' || appearance === 'light') return appearance
if (appearance === 'dark' || appearance === 'light' || appearance === 'oled') return appearance
} catch (error) {
// Ignore storage errors.
}
Expand All @@ -56,15 +75,29 @@
}

var scheme = getInitialScheme()
var color = scheme === 'dark' ? darkColor : lightColor
var colorTheme = 'default'
try {
var storedColorTheme = localStorage.getItem('hapi-color-theme')
if (storedColorTheme && themeBackgrounds[storedColorTheme]) colorTheme = storedColorTheme
} catch (error) {
// Ignore storage errors.
}
var paletteScheme = scheme === 'light' ? 'light' : 'dark'
var color = colorTheme === 'default'
? (scheme === 'oled' ? oledColor : (scheme === 'dark' ? darkColor : lightColor))
: themeBackgrounds[colorTheme][paletteScheme]
document.documentElement.setAttribute('data-theme', scheme)
document.documentElement.setAttribute('data-color-theme', colorTheme)
document.documentElement.style.backgroundColor = color

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MINOR] This inline backgroundColor is set during boot, but the runtime theme path never clears or updates document.documentElement.style.backgroundColor; applyTheme() only updates data-theme, preset CSS variables, and the meta tag. Since inline style outranks the later html[data-theme] rules, the document canvas can retain the launch appearance/preset color after a user changes appearance or color theme.

Suggested fix:

function applyTheme(scheme: ColorScheme): void {
    document.documentElement.style.removeProperty("background-color")
    document.documentElement.setAttribute("data-theme", scheme)
    applyColorTheme(getStoredColorTheme(), scheme)
    applyBrowserThemeColor(scheme)
}

document.querySelector('meta[name="theme-color"]').setAttribute('content', color)
})()
</script>
<!-- Prevent flash of mismatched background on app launch -->
<style>
html { background: #fff }
html[data-theme="dark"] { background: #1c1c1e; color-scheme: dark }
html[data-theme="oled"] { background: #000; color-scheme: dark }
html[data-color-theme]:not([data-color-theme="default"]) { background: inherit }
</style>

<title>HAPI</title>
Expand Down
68 changes: 68 additions & 0 deletions web/src/hooks/useColorTheme.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { act, renderHook } from '@testing-library/react'
import { beforeEach, describe, expect, it } from 'vitest'
import {
applyColorTheme,
getColorThemeStorageKey,
getStoredColorTheme,
parseColorTheme,
useColorTheme,
} from './useColorTheme'

const THEME_VARS = ['--app-bg', '--app-fg', '--app-link', '--app-button', '--app-secondary-bg']

describe('useColorTheme', () => {
beforeEach(() => {
localStorage.clear()
document.documentElement.removeAttribute('data-color-theme')
document.documentElement.setAttribute('data-theme', 'light')
for (const name of THEME_VARS) document.documentElement.style.removeProperty(name)
})

it('parses invalid or missing stored values as default', () => {
expect(parseColorTheme(null)).toBe('default')
expect(parseColorTheme('unknown')).toBe('default')
expect(parseColorTheme('notion')).toBe('notion')
})

it('reads the stored color theme preference', () => {
localStorage.setItem(getColorThemeStorageKey(), 'rose-pine')
expect(getStoredColorTheme()).toBe('rose-pine')
})

it('applies a preset palette to the document css variables', () => {
applyColorTheme('one', 'light')
expect(document.documentElement).toHaveAttribute('data-color-theme', 'one')
expect(document.documentElement.style.getPropertyValue('--app-bg')).toBe('#fbfbff')
expect(document.documentElement.style.getPropertyValue('--app-link')).toBe('#526fff')
})

it('removes palette css variables when reset to default', () => {
applyColorTheme('one', 'dark')
applyColorTheme('default', 'dark')

expect(document.documentElement).toHaveAttribute('data-color-theme', 'default')
expect(document.documentElement.style.getPropertyValue('--app-bg')).toBe('')
expect(document.documentElement.style.getPropertyValue('--app-link')).toBe('')
})

it('persists non-default selections and removes the key for default', () => {
const { result } = renderHook(() => useColorTheme())

act(() => result.current.setColorTheme('night-owl'))
expect(localStorage.getItem(getColorThemeStorageKey())).toBe('night-owl')
expect(document.documentElement.style.getPropertyValue('--app-bg')).toBe('#fbfdff')

act(() => result.current.setColorTheme('default'))
expect(localStorage.getItem(getColorThemeStorageKey())).toBeNull()
expect(document.documentElement.style.getPropertyValue('--app-bg')).toBe('')
})

it('uses the current dark scheme when applying from the hook', () => {
document.documentElement.setAttribute('data-theme', 'dark')
const { result } = renderHook(() => useColorTheme())

act(() => result.current.setColorTheme('notion'))
expect(document.documentElement.style.getPropertyValue('--app-bg')).toBe('#191919')
expect(document.documentElement.style.getPropertyValue('--app-fg')).toBe('#d9d9d8')
})
})
Loading
Loading