diff --git a/themes/bonsai/exampleSite/content/themes/index.md b/themes/bonsai/exampleSite/content/themes/index.md
new file mode 100644
index 0000000..48e810f
--- /dev/null
+++ b/themes/bonsai/exampleSite/content/themes/index.md
@@ -0,0 +1,4 @@
+---
+title: "Color themes"
+type: themes
+---
diff --git a/themes/bonsai/exampleSite/hugo.toml b/themes/bonsai/exampleSite/hugo.toml
index eedf5ee..754287f 100644
--- a/themes/bonsai/exampleSite/hugo.toml
+++ b/themes/bonsai/exampleSite/hugo.toml
@@ -11,7 +11,7 @@ disableKinds = ["taxonomy", "term", "RSS", "sitemap", "404"]
tagline = "Software engineer · tending small things with care"
bio = "Building tools, writing code, growing a quiet corner of the internet."
avatar = "/images/avatar.svg"
- themeToggle = false
+ themeToggle = true
footerText = "© 2026 · made with bonsai"
[[params.links]]
diff --git a/themes/bonsai/layouts/_default/baseof.html b/themes/bonsai/layouts/_default/baseof.html
index 856a329..ff5cb8a 100644
--- a/themes/bonsai/layouts/_default/baseof.html
+++ b/themes/bonsai/layouts/_default/baseof.html
@@ -1,5 +1,5 @@
-
+
{{- partial "head.html" . -}}
diff --git a/themes/bonsai/layouts/partials/avatar.html b/themes/bonsai/layouts/partials/avatar.html
new file mode 100644
index 0000000..1027c64
--- /dev/null
+++ b/themes/bonsai/layouts/partials/avatar.html
@@ -0,0 +1,30 @@
+{{- /* Render bio avatar.
+ If params.avatar is set, render
.
+ Otherwise, render an inline SVG circle with initials in accent color.
+ Initials override: params.avatarInitials.
+ Background override: params.avatarBg (defaults to --bonsai-accent). */ -}}
+{{- $name := site.Params.name | default site.Title -}}
+{{- $avatar := site.Params.avatar -}}
+
+{{- if $avatar -}}
+
+{{- else -}}
+ {{- $initials := site.Params.avatarInitials -}}
+ {{- if not $initials -}}
+ {{- $words := split (strings.TrimSpace $name) " " -}}
+ {{- $first := "" -}}
+ {{- $second := "" -}}
+ {{- with index $words 0 -}}{{- $first = substr . 0 1 -}}{{- end -}}
+ {{- if gt (len $words) 1 -}}{{- $second = substr (index $words 1) 0 1 -}}{{- end -}}
+ {{- $initials = upper (printf "%s%s" $first $second) -}}
+ {{- if not $initials -}}{{- $initials = "?" -}}{{- end -}}
+ {{- end -}}
+ {{- $bg := site.Params.avatarBg | default "var(--bonsai-accent)" -}}
+
+{{- end -}}
diff --git a/themes/bonsai/layouts/partials/bio-card.html b/themes/bonsai/layouts/partials/bio-card.html
index 1cf3a20..7e7304f 100644
--- a/themes/bonsai/layouts/partials/bio-card.html
+++ b/themes/bonsai/layouts/partials/bio-card.html
@@ -1,13 +1,10 @@
{{- $name := site.Params.name | default site.Title -}}
{{- $tagline := site.Params.tagline -}}
{{- $bio := site.Params.bio -}}
-{{- $avatar := site.Params.avatar -}}
{{- $links := site.Params.links -}}
- {{- if $avatar }}
-
- {{- end }}
+ {{ partial "avatar.html" . }}
{{ $name }}
diff --git a/themes/bonsai/layouts/partials/footer.html b/themes/bonsai/layouts/partials/footer.html
index c6b761b..010630b 100644
--- a/themes/bonsai/layouts/partials/footer.html
+++ b/themes/bonsai/layouts/partials/footer.html
@@ -5,5 +5,8 @@
{{- else -}}
© {{ now.Year }} {{ site.Params.name | default site.Title }}
{{- end -}}
+ {{- if site.Params.themeToggle -}}
+ {{ partial "theme-toggle-button.html" . }}
+ {{- end -}}
{{- end -}}
diff --git a/themes/bonsai/layouts/partials/head.html b/themes/bonsai/layouts/partials/head.html
index be1e06f..fecbae2 100644
--- a/themes/bonsai/layouts/partials/head.html
+++ b/themes/bonsai/layouts/partials/head.html
@@ -20,6 +20,8 @@
{{- end }}
+{{ partial "schema-person.html" . }}
+
{{- if site.Params.themeToggle }}
diff --git a/themes/bonsai/layouts/partials/schema-person.html b/themes/bonsai/layouts/partials/schema-person.html
new file mode 100644
index 0000000..c9dd5da
--- /dev/null
+++ b/themes/bonsai/layouts/partials/schema-person.html
@@ -0,0 +1,28 @@
+{{- /* Emit JSON-LD ProfilePage describing the bio as a Person.
+ Suppressed when site.Params.schema = false. */ -}}
+{{- if ne (site.Params.schema | default true) false -}}
+{{- $name := site.Params.name | default site.Title -}}
+{{- $sameAs := slice -}}
+{{- range site.Params.links -}}
+ {{- with .url -}}
+ {{- if not (or (strings.HasPrefix . "mailto:") (strings.HasPrefix . "tel:")) -}}
+ {{- $sameAs = $sameAs | append . -}}
+ {{- end -}}
+ {{- end -}}
+{{- end -}}
+{{- $person := dict
+ "@type" "Person"
+ "name" $name
+ "url" (site.BaseURL | absURL)
+-}}
+{{- with site.Params.bio -}}{{- $person = merge $person (dict "description" .) -}}{{- end -}}
+{{- with site.Params.avatar -}}
+ {{- $person = merge $person (dict "image" (. | strings.TrimPrefix "/" | absURL)) -}}
+{{- end -}}
+{{- with site.Params.jobTitle -}}{{- $person = merge $person (dict "jobTitle" .) -}}{{- end -}}
+{{- with site.Params.location -}}{{- $person = merge $person (dict "address" .) -}}{{- end -}}
+{{- with site.Params.email -}}{{- $person = merge $person (dict "email" .) -}}{{- end -}}
+{{- if $sameAs -}}{{- $person = merge $person (dict "sameAs" $sameAs) -}}{{- end -}}
+{{- $payload := dict "@context" "https://schema.org" "@type" "ProfilePage" "mainEntity" $person -}}
+
+{{- end -}}
diff --git a/themes/bonsai/layouts/partials/theme-toggle-button.html b/themes/bonsai/layouts/partials/theme-toggle-button.html
new file mode 100644
index 0000000..02041c2
--- /dev/null
+++ b/themes/bonsai/layouts/partials/theme-toggle-button.html
@@ -0,0 +1,24 @@
+{{- /* Sun/moon theme toggle. Only loaded/rendered when site.Params.themeToggle = true.
+ Sun shown in dark mode (target = light). Moon shown in light mode (target = dark). */ -}}
+
diff --git a/themes/bonsai/layouts/themes/single.html b/themes/bonsai/layouts/themes/single.html
new file mode 100644
index 0000000..1c3061f
--- /dev/null
+++ b/themes/bonsai/layouts/themes/single.html
@@ -0,0 +1,26 @@
+{{ define "main" }}
+
+ Color themes
+
+ Set colorTheme = "bonsai" in your hugo.toml [params] block. Each palette has light + dark variants.
+
+
+
+ {{- $palettes := slice
+ (dict "name" "bonsai" "label" "Bonsai (washi + vermilion)" "accent" "#8b3a2b")
+ (dict "name" "sakura" "label" "Sakura (cherry blossom)" "accent" "#d4456a")
+ (dict "name" "sumi" "label" "Sumi (monochrome ink)" "accent" "#1a1a1a")
+ (dict "name" "koi" "label" "Koi (orange + cream)" "accent" "#c8521e")
+ -}}
+
+
+ {{- range $palettes }}
+
+
{{ .label }}
+
accent {{ .accent }}
+
link button
+
accent
+
+ {{- end }}
+
+{{ end }}
diff --git a/themes/bonsai/static/css/bonsai.css b/themes/bonsai/static/css/bonsai.css
index 4d7cea8..9b52890 100644
--- a/themes/bonsai/static/css/bonsai.css
+++ b/themes/bonsai/static/css/bonsai.css
@@ -1,15 +1,9 @@
/* Bonsai — minimalist link-in-bio Hugo theme
- * Aesthetic: Japanese zen — generous whitespace, calm palette, restrained type. */
+ * Aesthetic: Japanese zen — generous whitespace, calm palette, restrained type.
+ * Palettes: bonsai (default), sakura, sumi, koi. Selected via [data-bonsai-theme]. */
:root {
- --bonsai-bg: #f4efe6; /* washi paper */
- --bonsai-surface: #ffffff;
- --bonsai-text: #2b2b2b; /* sumi ink */
- --bonsai-muted: #6b6b6b;
- --bonsai-accent: #8b3a2b; /* shu-iro / vermilion seal */
- --bonsai-border: #d9d0bf;
--bonsai-shadow: 0 1px 2px rgba(0, 0, 0, .04), 0 8px 24px rgba(0, 0, 0, .04);
-
--bonsai-radius: 14px;
--bonsai-gap: clamp(.75rem, 1.6vw, 1rem);
--bonsai-pad: clamp(1.25rem, 4vw, 2rem);
@@ -19,8 +13,29 @@
--bonsai-font-display: ui-serif, Georgia, "Hiragino Mincho ProN", "Yu Mincho", "Noto Serif JP", "Times New Roman", serif;
}
+/* ============================================================
+ Palettes
+ Each palette defines six color vars in light + dark variants.
+ Toggle order:
+ 1. base block sets light values
+ 2. @media (prefers-color-scheme:dark) overrides to dark
+ 3. [data-theme="dark"] forces dark (manual toggle)
+ 4. [data-theme="light"] forces light (manual toggle)
+ ============================================================ */
+
+/* --- Bonsai (default: washi + vermilion) --- */
+:root,
+[data-bonsai-theme="bonsai"] {
+ --bonsai-bg: #f4efe6;
+ --bonsai-surface: #ffffff;
+ --bonsai-text: #2b2b2b;
+ --bonsai-muted: #6b6b6b;
+ --bonsai-accent: #8b3a2b;
+ --bonsai-border: #d9d0bf;
+}
@media (prefers-color-scheme: dark) {
- :root {
+ :root:not([data-theme="light"]):not([data-bonsai-theme]),
+ [data-bonsai-theme="bonsai"]:not([data-theme="light"]) {
--bonsai-bg: #1a1817;
--bonsai-surface: #232020;
--bonsai-text: #ece6d9;
@@ -30,24 +45,111 @@
--bonsai-shadow: 0 1px 2px rgba(0, 0, 0, .3), 0 8px 24px rgba(0, 0, 0, .25);
}
}
-
-[data-theme="light"] {
- --bonsai-bg: #f4efe6;
- --bonsai-surface: #ffffff;
- --bonsai-text: #2b2b2b;
- --bonsai-muted: #6b6b6b;
- --bonsai-accent: #8b3a2b;
- --bonsai-border: #d9d0bf;
-}
-[data-theme="dark"] {
+[data-theme="dark"]:not([data-bonsai-theme]),
+[data-bonsai-theme="bonsai"][data-theme="dark"] {
--bonsai-bg: #1a1817;
--bonsai-surface: #232020;
--bonsai-text: #ece6d9;
--bonsai-muted: #a09a8d;
--bonsai-accent: #d6856e;
--bonsai-border: #3a3633;
+ --bonsai-shadow: 0 1px 2px rgba(0, 0, 0, .3), 0 8px 24px rgba(0, 0, 0, .25);
}
+/* --- Sakura (cherry blossom) --- */
+[data-bonsai-theme="sakura"] {
+ --bonsai-bg: #fff5f5;
+ --bonsai-surface: #ffffff;
+ --bonsai-text: #3a2228;
+ --bonsai-muted: #876773;
+ --bonsai-accent: #d4456a;
+ --bonsai-border: #f0d4dc;
+}
+@media (prefers-color-scheme: dark) {
+ [data-bonsai-theme="sakura"]:not([data-theme="light"]) {
+ --bonsai-bg: #1f1418;
+ --bonsai-surface: #2a1d22;
+ --bonsai-text: #f7e3e9;
+ --bonsai-muted: #b89aa3;
+ --bonsai-accent: #ec7596;
+ --bonsai-border: #3d2932;
+ --bonsai-shadow: 0 1px 2px rgba(0, 0, 0, .3), 0 8px 24px rgba(0, 0, 0, .25);
+ }
+}
+[data-bonsai-theme="sakura"][data-theme="dark"] {
+ --bonsai-bg: #1f1418;
+ --bonsai-surface: #2a1d22;
+ --bonsai-text: #f7e3e9;
+ --bonsai-muted: #b89aa3;
+ --bonsai-accent: #ec7596;
+ --bonsai-border: #3d2932;
+ --bonsai-shadow: 0 1px 2px rgba(0, 0, 0, .3), 0 8px 24px rgba(0, 0, 0, .25);
+}
+
+/* --- Sumi (monochrome ink) --- */
+[data-bonsai-theme="sumi"] {
+ --bonsai-bg: #fafafa;
+ --bonsai-surface: #ffffff;
+ --bonsai-text: #111111;
+ --bonsai-muted: #666666;
+ --bonsai-accent: #1a1a1a;
+ --bonsai-border: #e0e0e0;
+}
+@media (prefers-color-scheme: dark) {
+ [data-bonsai-theme="sumi"]:not([data-theme="light"]) {
+ --bonsai-bg: #0d0d0d;
+ --bonsai-surface: #1a1a1a;
+ --bonsai-text: #f5f5f5;
+ --bonsai-muted: #a0a0a0;
+ --bonsai-accent: #dddddd;
+ --bonsai-border: #2a2a2a;
+ --bonsai-shadow: 0 1px 2px rgba(0, 0, 0, .3), 0 8px 24px rgba(0, 0, 0, .25);
+ }
+}
+[data-bonsai-theme="sumi"][data-theme="dark"] {
+ --bonsai-bg: #0d0d0d;
+ --bonsai-surface: #1a1a1a;
+ --bonsai-text: #f5f5f5;
+ --bonsai-muted: #a0a0a0;
+ --bonsai-accent: #dddddd;
+ --bonsai-border: #2a2a2a;
+ --bonsai-shadow: 0 1px 2px rgba(0, 0, 0, .3), 0 8px 24px rgba(0, 0, 0, .25);
+}
+
+/* --- Koi (orange + cream) --- */
+[data-bonsai-theme="koi"] {
+ --bonsai-bg: #fef6e4;
+ --bonsai-surface: #ffffff;
+ --bonsai-text: #3a2620;
+ --bonsai-muted: #8a6f5c;
+ --bonsai-accent: #c8521e;
+ --bonsai-border: #f2dfb8;
+}
+@media (prefers-color-scheme: dark) {
+ [data-bonsai-theme="koi"]:not([data-theme="light"]) {
+ --bonsai-bg: #1c1410;
+ --bonsai-surface: #2a1f18;
+ --bonsai-text: #f7e2c4;
+ --bonsai-muted: #c9a988;
+ --bonsai-accent: #ff8b5c;
+ --bonsai-border: #3d2c20;
+ --bonsai-shadow: 0 1px 2px rgba(0, 0, 0, .3), 0 8px 24px rgba(0, 0, 0, .25);
+ }
+}
+[data-bonsai-theme="koi"][data-theme="dark"] {
+ --bonsai-bg: #1c1410;
+ --bonsai-surface: #2a1f18;
+ --bonsai-text: #f7e2c4;
+ --bonsai-muted: #c9a988;
+ --bonsai-accent: #ff8b5c;
+ --bonsai-border: #3d2c20;
+ --bonsai-shadow: 0 1px 2px rgba(0, 0, 0, .3), 0 8px 24px rgba(0, 0, 0, .25);
+}
+
+/* ============================================================
+ Layout & components
+ ============================================================ */
+
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
@@ -85,6 +187,12 @@ body {
margin-bottom: 1.25rem;
}
+.bio__avatar--initials {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+}
+
.bio__name {
font-family: var(--bonsai-font-display);
font-weight: 600;
@@ -168,6 +276,105 @@ body {
padding: 1.5rem var(--bonsai-pad) 2rem;
color: var(--bonsai-muted);
font-size: .8rem;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: .75rem;
+}
+
+.bonsai-footer p { margin: 0; }
+
+/* Theme toggle button */
+.theme-toggle {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 36px;
+ height: 36px;
+ padding: 0;
+ background: var(--bonsai-surface);
+ color: var(--bonsai-accent);
+ border: 1px solid var(--bonsai-border);
+ border-radius: 50%;
+ cursor: pointer;
+ transition: transform .15s ease, border-color .15s ease;
+}
+
+.theme-toggle:hover {
+ transform: translateY(-1px);
+ border-color: var(--bonsai-accent);
+}
+
+.theme-toggle:focus-visible {
+ outline: 2px solid var(--bonsai-accent);
+ outline-offset: 2px;
+}
+
+.theme-toggle svg { display: block; }
+
+/* Show moon in light, sun in dark.
+ The button shows the icon for the mode you'll switch TO. */
+.theme-toggle__sun { display: none; }
+.theme-toggle__moon { display: block; }
+
+@media (prefers-color-scheme: dark) {
+ html:not([data-theme="light"]) .theme-toggle__sun { display: block; }
+ html:not([data-theme="light"]) .theme-toggle__moon { display: none; }
+}
+
+[data-theme="dark"] .theme-toggle__sun { display: block; }
+[data-theme="dark"] .theme-toggle__moon { display: none; }
+[data-theme="light"] .theme-toggle__sun { display: none; }
+[data-theme="light"] .theme-toggle__moon { display: block; }
+
+/* ============================================================
+ Themes gallery (exampleSite/themes/) — 4 mini bio cards
+ ============================================================ */
+
+.themes-gallery {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
+ gap: 1.5rem;
+ max-width: 60rem;
+ margin: 2rem auto;
+}
+
+.themes-gallery__card {
+ background: var(--bonsai-bg);
+ border: 1px solid var(--bonsai-border);
+ border-radius: var(--bonsai-radius);
+ padding: 1.5rem;
+ text-align: center;
+}
+
+.themes-gallery__name {
+ font-family: var(--bonsai-font-display);
+ font-size: 1.1rem;
+ margin: 0 0 .25rem;
+ color: var(--bonsai-text);
+}
+
+.themes-gallery__hex {
+ color: var(--bonsai-muted);
+ font-family: ui-monospace, monospace;
+ font-size: .75rem;
+ margin: 0 0 1rem;
+}
+
+.themes-gallery__chip {
+ display: inline-block;
+ padding: .4rem .75rem;
+ background: var(--bonsai-surface);
+ color: var(--bonsai-text);
+ border: 1px solid var(--bonsai-border);
+ border-radius: var(--bonsai-radius);
+ font-size: .85rem;
+}
+
+.themes-gallery__chip--accent {
+ background: var(--bonsai-accent);
+ color: var(--bonsai-bg);
+ border-color: var(--bonsai-accent);
}
@media (prefers-reduced-motion: reduce) {
diff --git a/themes/bonsai/static/js/theme-toggle.js b/themes/bonsai/static/js/theme-toggle.js
index 48c28d3..165b295 100644
--- a/themes/bonsai/static/js/theme-toggle.js
+++ b/themes/bonsai/static/js/theme-toggle.js
@@ -1,5 +1,5 @@
-// Optional: enable via params.themeToggle = true in hugo.toml
-// Persists user preference and overrides system theme.
+// Loaded only when params.themeToggle = true.
+// Persists user preference and overrides system color-scheme.
(function () {
const KEY = 'bonsai-theme';
const root = document.documentElement;
@@ -9,9 +9,18 @@
const btn = document.querySelector('[data-bonsai-theme-toggle]');
if (!btn) return;
+ function syncPressed() {
+ const isDark = root.dataset.theme === 'dark'
+ || (root.dataset.theme === 'auto' && matchMedia('(prefers-color-scheme: dark)').matches);
+ btn.setAttribute('aria-pressed', String(isDark));
+ }
+
+ syncPressed();
+
btn.addEventListener('click', () => {
- const current = root.dataset.theme === 'dark' ? 'light' : 'dark';
- root.dataset.theme = current;
- localStorage.setItem(KEY, current);
+ const next = root.dataset.theme === 'dark' ? 'light' : 'dark';
+ root.dataset.theme = next;
+ localStorage.setItem(KEY, next);
+ syncPressed();
});
})();