aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFilip Figiel <figiel.filip@gmail.com>2024-03-06 15:41:04 +0100
committerGitHub <noreply@github.com>2024-03-06 14:41:04 +0000
commitf791b2e6409a16f7436e8a603f44230bd7e33611 (patch)
treecf48fec07861cccf263dd3ce778f07f021c58fae
parentb13ca3f54ee93d2a714073caec4aa4252e8b3f00 (diff)
downloadtour-f791b2e6409a16f7436e8a603f44230bd7e33611.tar.gz
tour-f791b2e6409a16f7436e8a603f44230bd7e33611.zip
Add dark mode support (#26)
-rw-r--r--src/icons.gleam61
-rw-r--r--src/tour.gleam81
-rw-r--r--static/common.css41
-rw-r--r--static/index.js1
-rw-r--r--static/style.css214
5 files changed, 383 insertions, 15 deletions
diff --git a/src/icons.gleam b/src/icons.gleam
new file mode 100644
index 0000000..034d7c3
--- /dev/null
+++ b/src/icons.gleam
@@ -0,0 +1,61 @@
+import htmb.{type Html, h}
+
+pub fn icon_moon() -> Html {
+ h("svg", [#("id", "icon-moon"), #("viewBox", "0 0 24 24")], [
+ h(
+ "path",
+ [
+ #(
+ "d",
+ "M21.996 12.882c0.022-0.233-0.038-0.476-0.188-0.681-0.325-0.446-0.951-0.544-1.397-0.219-0.95 0.693-2.060 1.086-3.188 1.162-1.368 0.092-2.765-0.283-3.95-1.158-1.333-0.985-2.139-2.415-2.367-3.935s0.124-3.124 1.109-4.456c0.142-0.191 0.216-0.435 0.191-0.691-0.053-0.55-0.542-0.952-1.092-0.898-2.258 0.22-4.314 1.18-5.895 2.651-1.736 1.615-2.902 3.847-3.137 6.386-0.254 2.749 0.631 5.343 2.266 7.311s4.022 3.313 6.772 3.567 5.343-0.631 7.311-2.266 3.313-4.022 3.567-6.772zM19.567 14.674c-0.49 1.363-1.335 2.543-2.416 3.441-1.576 1.309-3.648 2.016-5.848 1.813s-4.108-1.278-5.417-2.854-2.016-3.648-1.813-5.848c0.187-2.032 1.117-3.814 2.507-5.106 0.782-0.728 1.71-1.3 2.731-1.672-0.456 1.264-0.577 2.606-0.384 3.899 0.303 2.023 1.38 3.934 3.156 5.247 1.578 1.167 3.448 1.668 5.272 1.545 0.752-0.050 1.496-0.207 2.21-0.465z",
+ ),
+ ],
+ [],
+ ),
+ ])
+}
+
+pub fn icon_sun() -> Html {
+ h("svg", [#("id", "icon-sun"), #("viewBox", "0 0 24 24")], [
+ h(
+ "path",
+ [
+ #(
+ "d",
+ "M18 12c0-1.657-0.673-3.158-1.757-4.243s-2.586-1.757-4.243-1.757-3.158 0.673-4.243 1.757-1.757 2.586-1.757 4.243 0.673 3.158 1.757 4.243 2.586 1.757 4.243 1.757 3.158-0.673 4.243-1.757 1.757-2.586 1.757-4.243zM16 12c0 1.105-0.447 2.103-1.172 2.828s-1.723 1.172-2.828 1.172-2.103-0.447-2.828-1.172-1.172-1.723-1.172-2.828 0.447-2.103 1.172-2.828 1.723-1.172 2.828-1.172 2.103 0.447 2.828 1.172 1.172 1.723 1.172 2.828zM11 1v2c0 0.552 0.448 1 1 1s1-0.448 1-1v-2c0-0.552-0.448-1-1-1s-1 0.448-1 1zM11 21v2c0 0.552 0.448 1 1 1s1-0.448 1-1v-2c0-0.552-0.448-1-1-1s-1 0.448-1 1zM3.513 4.927l1.42 1.42c0.391 0.391 1.024 0.391 1.414 0s0.391-1.024 0-1.414l-1.42-1.42c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414zM17.653 19.067l1.42 1.42c0.391 0.391 1.024 0.391 1.414 0s0.391-1.024 0-1.414l-1.42-1.42c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414zM1 13h2c0.552 0 1-0.448 1-1s-0.448-1-1-1h-2c-0.552 0-1 0.448-1 1s0.448 1 1 1zM21 13h2c0.552 0 1-0.448 1-1s-0.448-1-1-1h-2c-0.552 0-1 0.448-1 1s0.448 1 1 1zM4.927 20.487l1.42-1.42c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0l-1.42 1.42c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0zM19.067 6.347l1.42-1.42c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0l-1.42 1.42c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0z",
+ ),
+ ],
+ [],
+ ),
+ ])
+}
+
+pub fn icon_toggle_left() -> Html {
+ h("svg", [#("id", "icon-toggle-left"), #("viewBox", "0 0 24 24")], [
+ h(
+ "path",
+ [
+ #(
+ "d",
+ "M8 4c-2.209 0-4.21 0.897-5.657 2.343s-2.343 3.448-2.343 5.657 0.897 4.21 2.343 5.657 3.448 2.343 5.657 2.343h8c2.209 0 4.21-0.897 5.657-2.343s2.343-3.448 2.343-5.657-0.897-4.21-2.343-5.657-3.448-2.343-5.657-2.343zM8 6h8c1.657 0 3.156 0.67 4.243 1.757s1.757 2.586 1.757 4.243-0.67 3.156-1.757 4.243-2.586 1.757-4.243 1.757h-8c-1.657 0-3.156-0.67-4.243-1.757s-1.757-2.586-1.757-4.243 0.67-3.156 1.757-4.243 2.586-1.757 4.243-1.757zM12 12c0-1.104-0.449-2.106-1.172-2.828s-1.724-1.172-2.828-1.172-2.106 0.449-2.828 1.172-1.172 1.724-1.172 2.828 0.449 2.106 1.172 2.828 1.724 1.172 2.828 1.172 2.106-0.449 2.828-1.172 1.172-1.724 1.172-2.828zM10 12c0 0.553-0.223 1.051-0.586 1.414s-0.861 0.586-1.414 0.586-1.051-0.223-1.414-0.586-0.586-0.861-0.586-1.414 0.223-1.051 0.586-1.414 0.861-0.586 1.414-0.586 1.051 0.223 1.414 0.586 0.586 0.861 0.586 1.414z",
+ ),
+ ],
+ [],
+ ),
+ ])
+}
+
+pub fn icon_toggle_right() -> Html {
+ h("svg", [#("id", "icon-toggle-right"), #("viewBox", "0 0 24 24")], [
+ h(
+ "path",
+ [
+ #(
+ "d",
+ "M8 4c-2.209 0-4.21 0.897-5.657 2.343s-2.343 3.448-2.343 5.657 0.897 4.21 2.343 5.657 3.448 2.343 5.657 2.343h8c2.209 0 4.21-0.897 5.657-2.343s2.343-3.448 2.343-5.657-0.897-4.21-2.343-5.657-3.448-2.343-5.657-2.343zM8 6h8c1.657 0 3.156 0.67 4.243 1.757s1.757 2.586 1.757 4.243-0.67 3.156-1.757 4.243-2.586 1.757-4.243 1.757h-8c-1.657 0-3.156-0.67-4.243-1.757s-1.757-2.586-1.757-4.243 0.67-3.156 1.757-4.243 2.586-1.757 4.243-1.757zM20 12c0-1.104-0.449-2.106-1.172-2.828s-1.724-1.172-2.828-1.172-2.106 0.449-2.828 1.172-1.172 1.724-1.172 2.828 0.449 2.106 1.172 2.828 1.724 1.172 2.828 1.172 2.106-0.449 2.828-1.172 1.172-1.724 1.172-2.828zM18 12c0 0.553-0.223 1.051-0.586 1.414s-0.861 0.586-1.414 0.586-1.051-0.223-1.414-0.586-0.586-0.861-0.586-1.414 0.223-1.051 0.586-1.414 0.861-0.586 1.414-0.586 1.051 0.223 1.414 0.586 0.586 0.861 0.586 1.414z",
+ ),
+ ],
+ [],
+ ),
+ ])
+}
diff --git a/src/tour.gleam b/src/tour.gleam
index 17386c9..fe163ae 100644
--- a/src/tour.gleam
+++ b/src/tour.gleam
@@ -8,6 +8,7 @@ import gleam/result
import simplifile
import filepath
import snag
+import icons
const static = "static"
@@ -516,7 +517,7 @@ fn lesson_html(page: Lesson) -> String {
let description =
"An interactive introduction and reference to the Gleam programming language. Learn Gleam in your browser!"
- h("html", [#("lang", "en-gb")], [
+ h("html", [#("lang", "en-gb"), #("class", "theme-light")], [
h("head", [], [
h("meta", [#("charset", "utf-8")], []),
h(
@@ -540,6 +541,7 @@ fn lesson_html(page: Lesson) -> String {
metaprop("twitter:description", description),
metaprop("twitter:image", "https://gleam.run/images/og-image.png"),
link("shortcut icon", "https://gleam.run/images/lucy/lucy.svg"),
+ link("stylesheet", "/common.css"),
link("stylesheet", "/style.css"),
h(
"script",
@@ -550,6 +552,11 @@ fn lesson_html(page: Lesson) -> String {
],
[],
),
+ h("script", [#("type", "module")], [
+ htmb.dangerous_unescaped_fragment(string_builder.from_string(
+ theme_picker_js,
+ )),
+ ]),
]),
h("body", [], [
h("nav", [#("class", "navbar")], [
@@ -564,6 +571,32 @@ fn lesson_html(page: Lesson) -> String {
),
text("Gleam Language Tour"),
]),
+ h("div", [#("class", "nav-right")], [
+ h("div", [#("class", "theme-picker")], [
+ h(
+ "button",
+ [
+ #("type", "button"),
+ #("alt", "Switch to light mode"),
+ #("title", "Switch to light mode"),
+ #("class", "theme-button -light"),
+ #("data-light-theme-toggle", ""),
+ ],
+ [icons.icon_moon(), icons.icon_toggle_left()],
+ ),
+ h(
+ "button",
+ [
+ #("type", "button"),
+ #("alt", "Switch to dark mode"),
+ #("title", "Switch to dark mode"),
+ #("class", "theme-button -dark"),
+ #("data-dark-theme-toggle", ""),
+ ],
+ [icons.icon_sun(), icons.icon_toggle_right()],
+ ),
+ ]),
+ ]),
]),
h("article", [#("id", "playground")], [
h("section", [#("id", "left")], [
@@ -595,3 +628,49 @@ fn lesson_html(page: Lesson) -> String {
|> htmb.render_page("html")
|> string_builder.to_string
}
+
+// This script is inlined in the response to avoid FOUC when applying the theme
+const theme_picker_js = "
+const mediaPrefersDarkTheme = window.matchMedia('(prefers-color-scheme: dark)')
+
+function selectTheme(selectedTheme) {
+ // Apply and remember the specified theme.
+ applyTheme(selectedTheme)
+ if ((selectedTheme === 'dark') === mediaPrefersDarkTheme.matches) {
+ // Selected theme is the same as the device's preferred theme, so we can forget this setting.
+ localStorage.removeItem('theme')
+ } else {
+ // Remember the selected theme to apply it on the next visit
+ localStorage.setItem('theme', selectedTheme)
+ }
+}
+
+function applyTheme(theme) {
+ document.documentElement.classList.toggle('theme-dark', theme === 'dark')
+ document.documentElement.classList.toggle('theme-light', theme !== 'dark')
+}
+
+// If user had selected a theme, load it. Otherwise, use device's preferred theme
+const selectedTheme = localStorage.getItem('theme')
+if (selectedTheme) {
+ applyTheme(selectedTheme)
+} else {
+ applyTheme(mediaPrefersDarkTheme.matches ? 'dark' : 'light')
+}
+
+// Watch the device's preferred theme and update theme if user did not select a theme
+mediaPrefersDarkTheme.addEventListener('change', () => {
+ const selectedTheme = localStorage.getItem('theme')
+ if (!selectedTheme) {
+ applyTheme(mediaPrefersDarkTheme.matches ? 'dark' : 'light')
+ }
+})
+
+// Add handlers for theme selection buttons.
+document.querySelector('[data-light-theme-toggle]').addEventListener('click', () => {
+ selectTheme('light')
+})
+document.querySelector('[data-dark-theme-toggle]').addEventListener('click', () => {
+ selectTheme('dark')
+})
+"
diff --git a/static/common.css b/static/common.css
new file mode 100644
index 0000000..2c9a188
--- /dev/null
+++ b/static/common.css
@@ -0,0 +1,41 @@
+/* This file contains design tokens used in core Gleam projects */
+:root {
+ /* Branding */
+ --faff-pink: #ffaff3;
+ --white: #fefefc;
+ --unnamed-blue: #a6f0fc;
+ --aged-plastic-yellow: #fffbe8;
+ --unexpected-aubergine: #584355;
+ --underwater-blue: #292d3e;
+ --charcoal: #2f2f2f;
+ --black: #1e1e1e;
+ --blacker: #151515;
+
+ /* Other greys */
+ --off-white: #f5f5f5;
+
+ /* Other colors */
+ --menthol: #c8ffa7;
+ --caramel: #ffd596;
+ --deep-saffron: #ff9d35;
+ --tomato: #ff6262;
+
+ /* Semantic colors */
+ --brand-success: var(--menthol);
+ --brand-warning: var(--caramel);
+ --brand-error: var(--tomato);
+
+ /* Light theme */
+ --light-theme-background: var(--white);
+ --light-theme-background-dim: var(--off-white);
+ --light-theme-text: var(--black);
+ --light-theme-text-secondary: var(--charcoal);
+ --light-theme-code: var(--black);
+
+ /* Dark theme */
+ --dark-theme-background: var(--underwater-blue);
+ --dark-theme-background-dim: var(--black);
+ --dark-theme-text: var(--white);
+ --dark-theme-text-secondary: var(--aged-plastic-yellow);
+ --dark-theme-code: var(--deep-saffron);
+}
diff --git a/static/index.js b/static/index.js
index 825a4b0..022082d 100644
--- a/static/index.js
+++ b/static/index.js
@@ -52,6 +52,7 @@ function appendOutput(content, className) {
const editor = new CodeFlask("#editor-target", {
language: "gleam",
+ defaultTheme: false
});
editor.addLanguage("gleam", prismGrammar);
editor.updateCode(initialCode);
diff --git a/static/style.css b/static/style.css
index 53c071b..1881ea5 100644
--- a/static/style.css
+++ b/static/style.css
@@ -22,13 +22,34 @@
--font-family-normal: "Outfit", sans-serif;
--font-family-title: "Lexend", sans-serif;
- --color-pink: #ffaff3;
- --color-red: #ff6262;
- --color-orange: #ffd596;
- --color-green: #c8ffa7;
-
--navbar-height: calc(calc(2 * var(--gap)) + 20px);
--gap: 12px;
+
+ --color-navbar-background: var(--faff-pink);
+ --color-navbar-text: var(--light-theme-text);
+ --color-navbar-link: var(--light-theme-text);
+}
+
+html.theme-light {
+ --color-background: var(--light-theme-background);
+ --color-background-dim: var(--light-theme-background-dim);
+ --color-text: var(--light-theme-text);
+ --color-link: var(--light-theme-text);
+ --color-link-decoration: var(--faff-pink);
+ --color-code: var(--light-theme-code);
+ --color-divider: var(--faff-pink);
+ color-scheme: light;
+}
+
+html.theme-dark {
+ --color-background: var(--dark-theme-background);
+ --color-background-dim: var(--dark-theme-background-dim);
+ --color-text: var(--dark-theme-text);
+ --color-link: var(--dark-theme-text);
+ --color-link-decoration: var(--faff-pink);
+ --color-code: var(--dark-theme-code);
+ --color-divider: var(--unexpected-aubergine);
+ color-scheme: dark;
}
* {
@@ -41,12 +62,14 @@ body {
height: 100vh;
display: flex;
flex-direction: column;
+ background-color: var(--color-background);
font-family: var(--font-family-normal);
letter-spacing: 0.01em;
line-height: 1.3;
+ color: var(--color-text);
}
-codeflask__textarea,
+.codeflask__textarea,
pre,
code {
font-weight: normal;
@@ -55,7 +78,8 @@ code {
p code {
padding: 1px 2px;
- background-color: #f5f5f5;
+ color: var(--color-code);
+ background-color: var(--color-background-dim);
}
h1,
@@ -74,7 +98,18 @@ h6 {
align-items: center;
height: var(--navbar-height);
padding: var(--gap);
- background-color: var(--color-pink);
+ background-color: var(--color-navbar-background);
+ color: var(--color-navbar-text);
+ box-shadow: 0 0 5px 5px rgba(0, 0, 0, 0.1)
+}
+
+a {
+ color: var(--color-link);
+ text-decoration-color: var(--color-link-decoration);
+}
+
+a code {
+ color: inherit;
}
.navbar .logo {
@@ -92,7 +127,35 @@ h6 {
.navbar a:visited,
.navbar a {
text-decoration: none;
- color: #000;
+ color: var(--color-navbar-link);
+}
+
+html.theme-dark .theme-button.-dark {
+ display: none;
+}
+
+html.theme-light .theme-button.-light {
+ display: none;
+}
+
+.theme-button {
+ appearance: none;
+ margin: 0;
+ border: 0;
+ padding: 0;
+ background: none;
+ color: inherit;
+ display: flex;
+ gap: 0.25em;
+ font-size: inherit;
+ color: inherit;
+}
+
+.theme-button svg {
+ display: inline-block;
+ fill: currentColor;
+ height: 1em;
+ width: 1em;
}
#left,
@@ -100,7 +163,7 @@ h6 {
#editor {
min-height: 200px;
overflow-y: auto;
- border-bottom: 2px solid var(--color-pink);
+ border-bottom: 2px solid var(--color-divider);
margin: 0;
overflow-wrap: break-word;
}
@@ -131,7 +194,7 @@ h6 {
}
#left {
- border-right: 2px solid var(--color-pink);
+ border-right: 2px solid var(--color-divider);
}
#right {
@@ -159,17 +222,18 @@ h6 {
.error,
.warning {
- border: var(--gap) solid var(--border-color);
+ border-width: var(--gap);
+ border-style: solid;
border-top: 0;
border-bottom: 0;
}
.error {
- --border-color: var(--color-red);
+ border-color: var(--brand-error);
}
.warning {
- --border-color: var(--color-orange);
+ border-color: var(--brand-warning);
}
.prev-next {
@@ -187,3 +251,125 @@ h6 {
.mb-0 {
margin-bottom: 0;
}
+
+
+/*
+ * CodeFlask editor themes based on the Atom One highlight.js theme used in Gleam package docs sites.
+ */
+
+.codeflask .codeflask__textarea {
+ color: var(--color-background); /* Prevents rendering artifacts in dark mode */
+ caret-color: var(--color-text); /* Makes the text input cursor visible in dark mode */
+}
+
+
+/* CodeFlask light theme */
+
+html.theme-light .codeflask {
+ background: var(--color-background);
+ color: var(--color-text);
+}
+
+html.theme-light .codeflask .token.punctuation {
+ color: #383a42;
+}
+
+html.theme-light .codeflask .token.keyword {
+ color: #a626a4;
+}
+
+html.theme-light .codeflask .token.operator {
+ color: #383a42;
+}
+
+html.theme-light .codeflask .token.string {
+ color: #50a14f;
+}
+
+html.theme-light .codeflask .token.comment {
+ color: #a0a1a7;
+}
+
+html.theme-light .codeflask .token.function {
+ color: #986801;
+}
+
+html.theme-light .codeflask .token.boolean {
+ color: #986801;
+}
+
+html.theme-light .codeflask .token.number {
+ color: #986801;
+}
+
+html.theme-light .codeflask .token.selector {
+ color: #986801;
+}
+
+html.theme-light .codeflask .token.property {
+ color: #986801;
+}
+
+html.theme-light .codeflask .token.tag {
+ color: #383a42;
+}
+
+html.theme-light .codeflask .token.attr-value {
+ color: #383a42;
+}
+
+
+/* CodeFlask dark theme */
+
+html.theme-dark .codeflask {
+ background: var(--color-background);
+ color: #d19a66;
+}
+
+html.theme-dark .codeflask .token.punctuation {
+ color: #abb2bf
+}
+
+html.theme-dark .codeflask .token.keyword {
+ color: #c678dd;
+}
+
+html.theme-dark .codeflask .token.operator {
+ color: #abb2bf;
+}
+
+html.theme-dark .codeflask .token.string {
+ color: #98c379;
+}
+
+html.theme-dark .codeflask .token.comment {
+ color: #5c6370;
+}
+
+html.theme-dark .codeflask .token.function {
+ color: #61aeee;
+}
+
+html.theme-dark .codeflask .token.boolean {
+ color: #61aeee;
+}
+
+html.theme-dark .codeflask .token.number {
+ color: #d19a66;
+}
+
+html.theme-dark .codeflask .token.selector {
+ color: #d19a66;
+}
+
+html.theme-dark .codeflask .token.property {
+ color: #d19a66;
+}
+
+html.theme-dark .codeflask .token.tag {
+ color: #abb2bf;
+}
+
+html.theme-dark .codeflask .token.attr-value {
+ color: #abb2bf;
+}