commit
590b2554c0
@ -1,10 +1,10 @@ |
||||
.link { |
||||
color: var(--blue-2); |
||||
color: var(--primary-accent); |
||||
transition-duration: 0.3s; |
||||
text-decoration: none; |
||||
white-space: normal; |
||||
} |
||||
|
||||
.link:hover { |
||||
color: var(--teal-2); |
||||
color: var(--secondary-accent); |
||||
} |
||||
|
@ -0,0 +1,178 @@ |
||||
import React, { |
||||
createContext, |
||||
ReactElement, |
||||
useContext, |
||||
useEffect, |
||||
useState, |
||||
} from "react"; |
||||
|
||||
type BuiltInThemes = "light" | "dark"; |
||||
|
||||
export interface Theme { |
||||
name: BuiltInThemes | "custom"; |
||||
palette: Palette; |
||||
} |
||||
|
||||
export type SetThemeInput = BuiltInThemes | Partial<Palette>; |
||||
|
||||
export const PALETTE_NAMES = [ |
||||
"--primary-background", |
||||
"--secondary-background", |
||||
|
||||
"--primary-accent", |
||||
"--primary-accent-soft", |
||||
"--primary-accent-light", |
||||
"--primary-accent-dim", |
||||
|
||||
"--secondary-accent", |
||||
"--secondary-accent-light", |
||||
|
||||
"--primary-heading", |
||||
"--secondary-heading", |
||||
|
||||
"--text", |
||||
|
||||
"--form-invalid", |
||||
|
||||
"--input-background", |
||||
"--input-placeholder-text", |
||||
"--input-text", |
||||
|
||||
"--navbar-page-overlay", |
||||
] as const; |
||||
|
||||
export const emptyPalette = PALETTE_NAMES.reduce( |
||||
(partial, varName) => ({ |
||||
...partial, |
||||
[varName]: "#c4e0f8", |
||||
}), |
||||
{} as Palette |
||||
); |
||||
|
||||
const ThemeContext = |
||||
createContext< |
||||
| { |
||||
theme: Theme; |
||||
setTheme(input: SetThemeInput): void; |
||||
save(): void; |
||||
clearSaved(): void; |
||||
} |
||||
| undefined |
||||
>(undefined); |
||||
|
||||
export interface Props { |
||||
children?: ReactElement; |
||||
} |
||||
|
||||
export function ThemeProvider(props: Props) { |
||||
const update = useForcedUpdate(); |
||||
const [themeName, setThemeName] = |
||||
useState<Theme["name"] | undefined>(undefined); |
||||
|
||||
const setTheme = (input: SetThemeInput) => { |
||||
if (typeof input === "string") { |
||||
PALETTE_NAMES.forEach((name) => |
||||
document.body.style.setProperty(name, "") |
||||
); |
||||
|
||||
if (input === "light") { |
||||
document.body.classList.remove("dark"); |
||||
} else if (input === "dark") { |
||||
document.body.classList.add("dark"); |
||||
} |
||||
|
||||
setThemeName(input); |
||||
} else { |
||||
const properties = Object.keys(input) as PaletteNames[]; |
||||
|
||||
properties.forEach((property) => |
||||
document.body.style.setProperty( |
||||
property, |
||||
input[property]?.trim() ?? null |
||||
) |
||||
); |
||||
|
||||
setThemeName("custom"); |
||||
} |
||||
|
||||
update(); |
||||
}; |
||||
|
||||
useEffect(() => { |
||||
const customPalette = getSavedPalette(); |
||||
|
||||
if (customPalette == null) { |
||||
setThemeName("light"); |
||||
} else { |
||||
setTheme(customPalette); |
||||
setThemeName("custom"); |
||||
} |
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []); |
||||
|
||||
return ( |
||||
<ThemeContext.Provider |
||||
value={ |
||||
themeName == null |
||||
? undefined |
||||
: { |
||||
theme: { |
||||
name: themeName, |
||||
get palette() { |
||||
return getCurrentPalette(); |
||||
}, |
||||
}, |
||||
setTheme, |
||||
save: () => savePalette(getCurrentPalette()), |
||||
clearSaved: clearSavedPalette, |
||||
} |
||||
} |
||||
> |
||||
{props.children} |
||||
</ThemeContext.Provider> |
||||
); |
||||
} |
||||
|
||||
export function useThemeContext() { |
||||
return useContext(ThemeContext); |
||||
} |
||||
|
||||
export type PaletteNames = typeof PALETTE_NAMES[number]; |
||||
|
||||
export type Palette = { |
||||
[key in PaletteNames]: string; |
||||
}; |
||||
|
||||
function getCurrentPalette() { |
||||
const styles = getComputedStyle(document.body); |
||||
|
||||
return PALETTE_NAMES.reduce( |
||||
(partial, varName) => ({ |
||||
...partial, |
||||
[varName]: styles.getPropertyValue(varName).trim(), |
||||
}), |
||||
{} as Palette |
||||
); |
||||
} |
||||
|
||||
function useForcedUpdate() { |
||||
const [fakeState, setFakeState] = useState(true); |
||||
|
||||
return () => setFakeState(!fakeState); |
||||
} |
||||
|
||||
const STORAGE_KEY = "csc-theme-palette"; |
||||
|
||||
function getSavedPalette() { |
||||
const raw = localStorage.getItem(STORAGE_KEY); |
||||
|
||||
return raw == null ? undefined : (JSON.parse(raw) as Palette); |
||||
} |
||||
|
||||
function savePalette(palette: Palette) { |
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(palette)); |
||||
} |
||||
|
||||
function clearSavedPalette() { |
||||
localStorage.removeItem(STORAGE_KEY); |
||||
} |
@ -1,36 +0,0 @@ |
||||
import React, { createContext, ReactElement, useEffect, useState } from "react"; |
||||
|
||||
type Theme = "light" | "dark"; |
||||
|
||||
const ThemeContext = createContext<{ |
||||
theme: Theme; |
||||
setTheme(newTheme: Theme): void; |
||||
}>({ |
||||
theme: "light", |
||||
setTheme: () => { |
||||
throw new Error("Use ThemeProvider instead."); |
||||
}, |
||||
}); |
||||
|
||||
interface Props { |
||||
theme: Theme; |
||||
children?: ReactElement; |
||||
} |
||||
|
||||
export function ThemeProvider({ children, theme }: Props) { |
||||
const [currentTheme, setTheme] = useState(theme); |
||||
|
||||
useEffect(() => { |
||||
if (currentTheme === "light") { |
||||
document.body.classList.remove("dark"); |
||||
} else if (currentTheme === "dark") { |
||||
document.body.classList.add("dark"); |
||||
} |
||||
}, [currentTheme]); |
||||
|
||||
return ( |
||||
<ThemeContext.Provider value={{ theme: currentTheme, setTheme }}> |
||||
{children} |
||||
</ThemeContext.Provider> |
||||
); |
||||
} |
@ -0,0 +1,7 @@ |
||||
#!/bin/bash |
||||
export NEXT_PUBLIC_BASE_PATH="/~$USER/website-demo" |
||||
rm -rf ~/www/website-demo |
||||
npm install |
||||
npm run build |
||||
npm run export |
||||
mv out ~/www/website-demo |
File diff suppressed because it is too large
Load Diff