Remove uploading css functionality. Themes are hard-coded. Change the code

This commit is contained in:
Hugh Kaznowski
2026-09-07 01:02:41 +01:00
parent 901dcf0a3f
commit 82fa0dfeb4
24 changed files with 578 additions and 1702 deletions
+41 -117
View File
@@ -2,75 +2,49 @@ import { useCallback, useEffect, useState } from 'react'
import {
activateProfile,
createProfile,
cssContentUrl,
deleteCssEntry,
deleteProfile,
fetchCssEntries,
fetchProfiles,
fetchSettings,
importCss,
linkCss,
fetchThemes,
themeCssUrl,
updateSettings,
type ColorScheme,
type CssEntry,
type ProfileSummary,
type Settings,
type Theme,
} from './api'
// `data-*` marker for the single injected `<link>` for the active custom theme, if any.
const INJECTED_THEME_ATTR = 'data-ottyl-theme-css-id'
// `data-*` marker for the single injected `<link>` for the active theme's stylesheet.
const INJECTED_THEME_ATTR = 'data-ottyl-theme-css'
// Themes selectable independently of `system`, each backed by a `html.<name>` block in `index.css`.
const THEME_CLASSES = [
'light',
'dark',
'red',
'green',
'blue',
'yellow',
'pink',
'orange',
'light_green',
'light_red',
'light_blue',
'light_yellow',
'light_pink',
'light_orange',
] as const
// `"system"` isn't an embedded theme file — it resolves to one of these two at apply time, based
// on the OS light/dark preference.
const SYSTEM_LIGHT_THEME = 'light'
const SYSTEM_DARK_THEME = 'dark'
// Reflects `colorScheme` onto `<html>` as a class + `data-color-scheme`; when a custom theme is active, no built-in class is added at all, since a class selector would out-specificity its injected `:root` override (see `applyCustomThemeCss`) and mask it.
function applyColorScheme(colorScheme: ColorScheme, customThemeActive: boolean) {
const root = document.documentElement
root.classList.remove(...THEME_CLASSES)
if (customThemeActive) {
root.dataset.colorScheme = 'custom'
return
}
function resolveTheme(theme: Theme): string {
if (theme !== 'system') return theme
const prefersDark = window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? true
const effective = colorScheme === 'system' ? (prefersDark ? 'dark' : 'light') : colorScheme
root.classList.add(effective)
root.dataset.colorScheme = colorScheme
return prefersDark ? SYSTEM_DARK_THEME : SYSTEM_LIGHT_THEME
}
// Injects a `<link>` for the active custom theme entry, if any (ticket 020: every CSS entry is
// always a selectable theme now, so this is the only injection path — there's no more
// always-on-overlay mode). `entry` null just removes any previously-injected tag.
function applyCustomThemeCss(entry: CssEntry | null) {
// Injects a `<link>` for the resolved theme's stylesheet, replacing any previously-injected one.
// `webapp/src/index.css`'s `:root` block is the only styling until this loads, so there's no
// flash of unstyled content on first paint.
function applyTheme(theme: Theme) {
const resolved = resolveTheme(theme)
document.querySelectorAll(`link[${INJECTED_THEME_ATTR}]`).forEach((el) => el.remove())
if (!entry) return
const href = entry.kind === 'imported' ? cssContentUrl(entry.id) : entry.path_or_url
if (!href) return
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = href
link.setAttribute(INJECTED_THEME_ATTR, entry.id)
link.href = themeCssUrl(resolved)
link.setAttribute(INJECTED_THEME_ATTR, resolved)
document.head.appendChild(link)
}
// Owns fetching + mutating settings/CSS/profile state for the NavMenu panels, and applies colour scheme / imported CSS to the document as a side effect so changes are visible immediately, with no reload.
// Owns fetching + mutating settings/theme/profile state for the NavMenu panels, and applies the
// selected theme to the document as a side effect so changes are visible immediately, with no reload.
export function useSettingsPanel() {
const [settings, setSettings] = useState<Settings | null>(null)
const [cssEntries, setCssEntries] = useState<CssEntry[]>([])
const [themes, setThemes] = useState<string[]>([])
const [profiles, setProfiles] = useState<ProfileSummary[]>([])
const [error, setError] = useState<string | null>(null)
@@ -80,9 +54,9 @@ export function useSettingsPanel() {
return next
}, [])
const refreshCss = useCallback(async () => {
const next = await fetchCssEntries()
setCssEntries(next)
const refreshThemes = useCallback(async () => {
const next = await fetchThemes()
setThemes(next)
return next
}, [])
@@ -94,21 +68,20 @@ export function useSettingsPanel() {
useEffect(() => {
refreshSettings().catch(() => {})
refreshCss().catch(() => {})
refreshThemes().catch(() => {})
refreshProfiles().catch(() => {})
}, [refreshSettings, refreshCss, refreshProfiles])
const activeCustomTheme = settings?.custom_theme_id
? cssEntries.find((entry) => entry.id === settings.custom_theme_id) ?? null
: null
}, [refreshSettings, refreshThemes, refreshProfiles])
useEffect(() => {
if (settings) applyColorScheme(settings.color_scheme, activeCustomTheme !== null)
}, [settings, activeCustomTheme])
useEffect(() => {
applyCustomThemeCss(activeCustomTheme)
}, [activeCustomTheme])
if (!settings) return
applyTheme(settings.theme)
if (settings.theme !== 'system') return
// Re-resolve if the OS preference changes while "system" is selected.
const media = window.matchMedia?.('(prefers-color-scheme: dark)')
const onChange = () => applyTheme(settings.theme)
media?.addEventListener('change', onChange)
return () => media?.removeEventListener('change', onChange)
}, [settings])
const guarded = useCallback((action: () => Promise<void>) => {
return async () => {
@@ -121,20 +94,10 @@ export function useSettingsPanel() {
}
}, [])
const setColorScheme = useCallback(
(colorScheme: ColorScheme) =>
const setTheme = useCallback(
(theme: Theme) =>
guarded(async () => {
// Selecting a built-in scheme always deselects any custom theme — the two are mutually exclusive (see `applyColorScheme`).
const next = await updateSettings({ colorScheme, customThemeId: null })
setSettings(next)
})(),
[guarded],
)
const setCustomTheme = useCallback(
(id: string) =>
guarded(async () => {
const next = await updateSettings({ customThemeId: id })
const next = await updateSettings({ theme })
setSettings(next)
})(),
[guarded],
@@ -158,40 +121,6 @@ export function useSettingsPanel() {
[guarded],
)
// Every entry is always a selectable style, on equal footing with the built-in colour schemes
// (ticket 020 unified the data model; ticket 047 unified the UI), so there's a single import path.
const importCssEntry = useCallback(
(filename: string, content: string) =>
guarded(async () => {
await importCss(filename, content)
await refreshCss()
})(),
[guarded, refreshCss],
)
const linkCssEntry = useCallback(
(pathOrUrl: string) =>
guarded(async () => {
await linkCss(pathOrUrl)
await refreshCss()
})(),
[guarded, refreshCss],
)
const removeCssEntry = useCallback(
(id: string) =>
guarded(async () => {
await deleteCssEntry(id)
await refreshCss()
// The removed entry may have been the active custom theme — fall back to `color_scheme` rather than leaving a dangling selection.
if (settings?.custom_theme_id === id) {
const next = await updateSettings({ customThemeId: null })
setSettings(next)
}
})(),
[guarded, refreshCss, settings],
)
const addProfile = useCallback(
(name: string) =>
guarded(async () => {
@@ -221,17 +150,12 @@ export function useSettingsPanel() {
return {
settings,
cssEntries,
activeCustomTheme,
themes,
profiles,
error,
setColorScheme,
setCustomTheme,
setTheme,
setMaxLogs,
setNtfySink,
importCssEntry,
linkCssEntry,
removeCssEntry,
addProfile,
removeProfile,
switchProfile,