diff --git a/lib/custom_html.typ b/lib/custom_html.typ index 3892db7..6f033bb 100644 --- a/lib/custom_html.typ +++ b/lib/custom_html.typ @@ -5,3 +5,9 @@ #let site-wrapper = html.elem.with("site-wrapper") #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) +} diff --git a/lib/html_body.typ b/lib/html_body.typ index 2c1086e..bd5dc37 100644 --- a/lib/html_body.typ +++ b/lib/html_body.typ @@ -1,4 +1,5 @@ #import "./custom_html.typ" as chtml +#import "./theme_picker.typ": theme_picker /// Make the body of the webpage #let html_body( @@ -10,9 +11,43 @@ logo_alt, 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({ chtml.site-wrapper({ chtml.site-container({ + html.nav({ + theme_picker() + }) html.header(style: "display: flex; flex-wrap: wrap-reverse;", { html.hgroup({ header diff --git a/lib/html_head.typ b/lib/html_head.typ index 11a0efc..3fee2c2 100644 --- a/lib/html_head.typ +++ b/lib/html_head.typ @@ -1,4 +1,5 @@ #import "./custom_html.typ" as chtml +#import "./html_utils.typ": get-css /// Generate the html
element for the page. #let html_head( @@ -76,28 +77,12 @@ } --color-border: light-dark(#414868, #414868); - } - site-wrapper { - display: flex; - flex-wrap: wrap; - width: 100%; - justify-content: center; - 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; - } - } + --color-button-shadow: light-dark(#888, #000); + --color-button-focus: light-dark(#000, #FFF); } ``` + html.style(get-css()) + }) } diff --git a/lib/html_utils.typ b/lib/html_utils.typ index 7314573..b1e1369 100644 --- a/lib/html_utils.typ +++ b/lib/html_utils.typ @@ -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) = { - show raw.where(lang: "raw-css"): it => html.style(it.text) + show raw.where(lang: "raw-css"): it => add-css(it.text) body } diff --git a/lib/theme_picker.typ b/lib/theme_picker.typ new file mode 100644 index 0000000..42f95bc --- /dev/null +++ b/lib/theme_picker.typ @@ -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 unecessaryelements */ + 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 + }) + } + ) +}