commit
817f413c31
@ -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 |
@ -1,86 +1,3 @@ |
||||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
||||
|
||||
declare module "*.event.mdx" { |
||||
import { ComponentType } from "react"; |
||||
|
||||
interface EventMetadata { |
||||
name: string; |
||||
short: string; |
||||
date: Date; |
||||
online: boolean; |
||||
location: string; |
||||
poster?: string; |
||||
registerLink?: string; |
||||
} |
||||
|
||||
const ReactComponent: ComponentType; |
||||
|
||||
export const metadata: EventMetadata; |
||||
export default ReactComponent; |
||||
} |
||||
|
||||
declare module "*.news.mdx" { |
||||
import { ComponentType } from "react"; |
||||
|
||||
interface NewsMetadata { |
||||
author: string; |
||||
date: Date; |
||||
} |
||||
|
||||
const ReactComponent: ComponentType; |
||||
|
||||
export const metadata: NewsMetadata; |
||||
export default ReactComponent; |
||||
} |
||||
|
||||
declare module "*.team-member.mdx" { |
||||
import { ComponentType } from "react"; |
||||
|
||||
interface TeamMemberMetadata { |
||||
name: string; |
||||
role: string; |
||||
image?: string; |
||||
} |
||||
|
||||
const ReactComponent: ComponentType; |
||||
|
||||
export const metadata: TeamMemberMetadata; |
||||
export default ReactComponent; |
||||
} |
||||
|
||||
declare module "*.section.mdx" { |
||||
import { ComponentType } from "react"; |
||||
|
||||
interface SectionMetadata { |
||||
title: string; |
||||
id: string; |
||||
} |
||||
|
||||
const ReactComponent: ComponentType; |
||||
|
||||
export const metadata: SectionMetadata; |
||||
export default ReactComponent; |
||||
} |
||||
|
||||
declare module "*.talk.mdx" { |
||||
import { ComponentType } from "react"; |
||||
|
||||
interface TalkMetadata { |
||||
name: string; |
||||
short: string; |
||||
poster?: string; |
||||
} |
||||
|
||||
const ReactComponent: ComponentType; |
||||
|
||||
export const metadata: TalkMetadata; |
||||
export default ReactComponent; |
||||
} |
||||
declare module "*.mdx" { |
||||
import { ComponentType } from "react"; |
||||
|
||||
const ReactComponent: ComponentType; |
||||
|
||||
export default ReactComponent; |
||||
} |
||||
/// <reference types="next/image-types/global" />
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,38 @@ |
||||
.page { |
||||
margin-bottom: calc(60rem / 16); |
||||
} |
||||
|
||||
.controls { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
} |
||||
|
||||
.controls > *:first-child { |
||||
margin-right: calc(20rem / 16); |
||||
} |
||||
|
||||
.palette { |
||||
display: grid; |
||||
grid-template-columns: repeat(auto-fill, minmax(calc(150rem / 16), 1fr)); |
||||
row-gap: calc(20rem / 16); |
||||
column-gap: calc(40rem / 16); |
||||
} |
||||
|
||||
.palette > * { |
||||
display: flex; |
||||
flex-direction: column; |
||||
align-items: center; |
||||
} |
||||
|
||||
.colorSelector { |
||||
height: calc(75rem / 16); |
||||
} |
||||
|
||||
.colorReset { |
||||
margin-bottom: calc(10rem / 16); |
||||
transition-duration: unset; |
||||
} |
||||
|
||||
.colorName { |
||||
text-align: center; |
||||
} |
@ -0,0 +1,78 @@ |
||||
import React from "react"; |
||||
import { useThemeContext, emptyPalette } from "components/Theme"; |
||||
import { Input } from "components/Input"; |
||||
import { Button } from "components/Button"; |
||||
|
||||
import styles from "./themer.module.css"; |
||||
|
||||
export default function Themer() { |
||||
const context = useThemeContext(); |
||||
const palette = context?.theme.palette ?? emptyPalette; |
||||
|
||||
return ( |
||||
<main className={styles.page}> |
||||
<h1>Themer</h1> |
||||
<form onSubmit={(event) => event.preventDefault()}> |
||||
<div className={styles.controls}> |
||||
<Button |
||||
type="reset" |
||||
onClick={() => { |
||||
context?.clearSaved(); |
||||
context?.setTheme("light"); |
||||
}} |
||||
> |
||||
Reset to light mode |
||||
</Button> |
||||
<Button type="submit" onClick={() => context?.save()}> |
||||
Save |
||||
</Button> |
||||
</div> |
||||
<div className={styles.palette}> |
||||
{Object.entries(palette).map(([key, value]) => { |
||||
const color = |
||||
value.length === 4 && value.startsWith("#") |
||||
? `#${value[1].repeat(2)}${value[2].repeat(2)}${value[3].repeat( |
||||
2 |
||||
)}` |
||||
: value; |
||||
|
||||
const isValidColor = color.startsWith("#") && color.length === 7; |
||||
|
||||
return ( |
||||
<div key={key}> |
||||
<Input |
||||
id={`color-${key}`} |
||||
type="color" |
||||
className={styles.colorSelector} |
||||
value={color} |
||||
onChange={(event) => |
||||
context?.setTheme({ [key]: event.target.value }) |
||||
} |
||||
/> |
||||
<Button |
||||
type="button" |
||||
className={styles.colorReset} |
||||
size="small" |
||||
onClick={() => context?.setTheme({ [key]: "" })} |
||||
> |
||||
Reset |
||||
</Button> |
||||
<code className={styles.colorName}> |
||||
<label htmlFor={`color-${key}`}> |
||||
{!isValidColor && "🚧 "} |
||||
{key |
||||
.slice(2) |
||||
.split("-") |
||||
.map((word) => word[0].toUpperCase() + word.slice(1)) |
||||
.join(" ")} |
||||
{!isValidColor && " 🚧"} |
||||
</label> |
||||
</code> |
||||
</div> |
||||
); |
||||
})} |
||||
</div>{" "} |
||||
</form> |
||||
</main> |
||||
); |
||||
} |
@ -0,0 +1,84 @@ |
||||
declare module "*.event.mdx" { |
||||
import { ComponentType } from "react"; |
||||
|
||||
interface EventMetadata { |
||||
name: string; |
||||
short: string; |
||||
date: Date; |
||||
online: boolean; |
||||
location: string; |
||||
poster?: string; |
||||
registerLink?: string; |
||||
} |
||||
|
||||
const ReactComponent: ComponentType; |
||||
|
||||
export const metadata: EventMetadata; |
||||
export default ReactComponent; |
||||
} |
||||
|
||||
declare module "*.news.mdx" { |
||||
import { ComponentType } from "react"; |
||||
|
||||
interface NewsMetadata { |
||||
author: string; |
||||
date: Date; |
||||
} |
||||
|
||||
const ReactComponent: ComponentType; |
||||
|
||||
export const metadata: NewsMetadata; |
||||
export default ReactComponent; |
||||
} |
||||
|
||||
declare module "*.team-member.mdx" { |
||||
import { ComponentType } from "react"; |
||||
|
||||
interface TeamMemberMetadata { |
||||
name: string; |
||||
role: string; |
||||
image?: string; |
||||
} |
||||
|
||||
const ReactComponent: ComponentType; |
||||
|
||||
export const metadata: TeamMemberMetadata; |
||||
export default ReactComponent; |
||||
} |
||||
|
||||
declare module "*.section.mdx" { |
||||
import { ComponentType } from "react"; |
||||
|
||||
interface SectionMetadata { |
||||
title: string; |
||||
id: string; |
||||
} |
||||
|
||||
const ReactComponent: ComponentType; |
||||