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 -}} + {{ $name }} +{{- 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)" -}} + + + {{ $initials }} + +{{- 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 }} - {{ $name }} - {{- 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") + -}} + + +{{ 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(); }); })();