add theme picker

This commit is contained in:
Jean-Marie 'Histausse' Mineau 2026-03-16 02:13:11 +01:00
parent a4337b3514
commit 49f457cda7
Signed by: histausse
GPG key ID: B66AEEDA9B645AD2
5 changed files with 122 additions and 21 deletions

View file

@ -5,3 +5,9 @@
#let site-wrapper = html.elem.with("site-wrapper") #let site-wrapper = html.elem.with("site-wrapper")
#let site-container = html.elem.with("site-container") #let site-container = html.elem.with("site-container")
/// Element used to group radio buttons for theme selection.
/// label is the aria-label, for accessibility (ie screen reader)
#let theme-picker(label: "Theme Picker", body) = {
// Role 'radiogroup' is needed because of bug in chromium with screenreaders (https://lyra.horse/blog/2025/08/you-dont-need-js/#fn:10)
html.elem("theme-picker", attrs: (aria-label: label, role: "radiogroup"), body)
}

View file

@ -1,4 +1,5 @@
#import "./custom_html.typ" as chtml #import "./custom_html.typ" as chtml
#import "./theme_picker.typ": theme_picker
/// Make the body of the webpage /// Make the body of the webpage
#let html_body( #let html_body(
@ -10,9 +11,43 @@
logo_alt, logo_alt,
body body
) = { ) = {
```raw-css
site-wrapper {
display: flex;
flex-wrap: wrap;
width: 100%;
justify-content: center;
nav {
display: flex;
theme-picker {
margin-left: auto;
}
}
site-container {
width: 1050px;
padding: 0 20px;
header {
// display: flex
// flex-wrap: wrap;
justify-content: space-between;
align-items: center;
padding: 15px 5px;
border-bottom: solid 1px var(--color-border);
margin-top: 15px;
}
}
}
```
html.body({ html.body({
chtml.site-wrapper({ chtml.site-wrapper({
chtml.site-container({ chtml.site-container({
html.nav({
theme_picker()
})
html.header(style: "display: flex; flex-wrap: wrap-reverse;", { html.header(style: "display: flex; flex-wrap: wrap-reverse;", {
html.hgroup({ html.hgroup({
header header

View file

@ -1,4 +1,5 @@
#import "./custom_html.typ" as chtml #import "./custom_html.typ" as chtml
#import "./html_utils.typ": get-css
/// Generate the html <head> element for the page. /// Generate the html <head> element for the page.
#let html_head( #let html_head(
@ -76,28 +77,12 @@
} }
--color-border: light-dark(#414868, #414868); --color-border: light-dark(#414868, #414868);
}
site-wrapper {
display: flex;
flex-wrap: wrap;
width: 100%;
justify-content: center;
site-container { --color-button-shadow: light-dark(#888, #000);
width: 1050px; --color-button-focus: light-dark(#000, #FFF);
padding: 0 20px;
header {
// display: flex
// flex-wrap: wrap;
justify-content: space-between;
align-items: center;
padding: 15px 5px;
border-bottom: solid 1px var(--color-border);
margin-top: 15px;
}
}
} }
``` ```
html.style(get-css())
}) })
} }

View file

@ -1,4 +1,16 @@
#let css-list = state("css-list", ())
/// Add string `css` to `css-list` if not already present
#let add-css(css) = context {
css-list.update(x => if css in x { x } else { x + (css,) })
}
/// Concatenate all css found in css-list at the end of the document
#let get-css() = context {
css-list.final().join("\n\n")
}
#let html_show(body) = { #let html_show(body) = {
show raw.where(lang: "raw-css"): it => html.style(it.text) show raw.where(lang: "raw-css"): it => add-css(it.text)
body body
} }

63
lib/theme_picker.typ Normal file
View file

@ -0,0 +1,63 @@
#import "./custom_html.typ" as chtml
/// Buttons to chose the theme to use (Light/Dark)
/// label is the aria-label of the html element (for screenreader).
/// label-auto, label-light and label-dark are the content of each button.
#let theme_picker(
label: "Theme Picker",
label-auto: [Auto],
label-light: [Light],
label-dark: [Dark],
) = {
```raw-css
/* Thanks to Lyra Rebane for no-js theme picker implementation: https://lyra.horse/blog/2025/08/you-dont-need-js/ */
theme-picker {
display: flex;
padding: 5px;
label {
transition: background 0.08s;
&:first-child { border-radius: 8px 0 0 8px; }
&:last-child { border-radius: 0 8px 8px 0; }
&:has(input:checked) {
box-shadow: inset 0px 0px 8px 0px var(--color-button-shadow);
}
&:has(input:focus-visible) {
outline: 2px solid var(--color-button-focus);
}
&:hover { background: #0004; }
&:active { background: #0006; }
box-shadow: inset 0px 0px 1.2px 0px #000;
padding: 10px;
cursor: pointer;
background: #0002;
}
input {
/* To allow screen reader to still access these. */
opacity: 0;
position: absolute;
pointer-events: none;
}
/* Typst like to insert unecessary <p> elements */
p {
margin: 0px;
}
}
```
chtml.theme-picker(
label: label,
{
html.label({
html.input(type: "radio", name: "theme", id: "theme-auto", checked: true)
label-auto
})
html.label({
html.input(type: "radio", name: "theme", id: "theme-light")
label-light
})
html.label({
html.input(type: "radio", name: "theme", id: "theme-dark")
label-dark
})
}
)
}