5 Commits
Author SHA1 Message Date
tiennm99 eff11d99d8 chore(images): regenerate screenshots showing sakura palette + initials avatar 2026-05-03 11:16:59 +07:00
tiennm99 7487aa2c36 fix: theme-toggle CSS specificity + demo uses initials fallback
- .theme-toggle svg { display: block } was beating per-class hide rules
  due to higher specificity. Scope the per-class rules under
  .theme-toggle .theme-toggle__sun/moon so they win.
- Remove avatar from exampleSite so the initials fallback renders, which
  inherits the active palette's accent color (sakura -> pink). The static
  avatar.svg was hardcoded vermilion and looked off in non-bonsai
  palettes.
2026-05-03 11:16:02 +07:00
tiennm99 921390c351 docs: README + CHANGELOG for v0.2.0 + sakura demo
- README: add new params (colorTheme, themeToggle button details, schema,
  jobTitle/location/email, avatarInitials/Bg) to config table; new
  'Color themes' section with live gallery link
- CHANGELOG: 0.2.0 entry under Added/Changed; placeholder for v0.3
- exampleSite/hugo.toml: colorTheme = sakura for distinctive screenshot
  + jobTitle to populate Person.jobTitle in JSON-LD
2026-05-03 11:13:19 +07:00
tiennm99 7468f3afba feat: v0.2 polish & SEO
Bundle of four small additions for the v0.2 release. All strictly
additive — no breaking changes; v0.1 sites upgrade without edits.

Phase 1 — Color theme presets
- Refactor static/css/bonsai.css: palette vars now scoped per
  [data-bonsai-theme]; layout vars (radius/gap/pad/fonts) stay in :root
- Four palettes: bonsai (default, vermilion+washi), sakura (cherry
  blossom), sumi (monochrome ink), koi (orange+cream). Light + dark
  variants each, both prefers-color-scheme and manual data-theme override
- New params.colorTheme (default 'bonsai'); applied via baseof
- Themes gallery at exampleSite /themes/ shows all 4 side-by-side

Phase 2 — schema.org Person markup
- New layouts/partials/schema-person.html emits JSON-LD ProfilePage
  containing a Person (built from name/bio/avatar/links + optional
  jobTitle/location/email)
- mailto: and tel: links excluded from sameAs
- Suppress with params.schema = false
- Wired into head.html

Phase 3 — Theme toggle UI button
- New layouts/partials/theme-toggle-button.html (sun/moon SVGs)
- Rendered in footer when params.themeToggle = true
- aria-pressed reflects current state; updated theme-toggle.js to
  sync on click and on first paint
- Sun shown in dark mode, moon in light (button shows target state)
- exampleSite enables the toggle by default for demo

Phase 4 — Avatar initials fallback
- New layouts/partials/avatar.html: img if params.avatar set,
  inline SVG circle with initials otherwise
- Initials auto-derived from params.name (first letter of up to 2
  words, uppercased); override with params.avatarInitials
- Background overrideable with params.avatarBg; defaults to accent
- bio-card.html simplified to delegate to the partial
2026-05-03 11:11:39 +07:00
tiennm99 ce08594810 docs(journal): v0.1 release retro 2026-05-03 11:11:19 +07:00
17 changed files with 462 additions and 34 deletions

No files matched your search

+18 -1
View File
@@ -5,7 +5,24 @@ All notable changes to this project are documented here. Format follows [Keep a
## [Unreleased]
### Added
- (placeholder for v0.2 — layout variants, color presets, schema.org Person markup)
- (placeholder for v0.3 — layout variants, i18n support)
## [0.2.0] — 2026-05-03
Polish & SEO release. All additions are strictly opt-in; v0.1 sites upgrade with no config edits.
### Added
- **Color theme presets** — four named palettes (`bonsai` default, `sakura`, `sumi`, `koi`); select via new `params.colorTheme`. Each has light + dark variants. Gallery at `/themes/`.
- **schema.org Person markup** — JSON-LD `ProfilePage > Person` injected in `<head>`. Built from existing params + optional `params.jobTitle`, `params.location`, `params.email`. Suppressible via `params.schema = false`. `mailto:` and `tel:` links excluded from `sameAs`.
- **Theme toggle UI button** — sun/moon button rendered in footer when `params.themeToggle = true`. Sun shown in dark mode, moon in light (button shows target). `aria-pressed` reflects state. Existing localStorage persistence preserved.
- **Avatar initials fallback** — when `params.avatar` is unset, theme renders an inline SVG circle with auto-derived initials (first letter of up to 2 words from `params.name`). Override with `params.avatarInitials`. Background overrideable via `params.avatarBg`.
### Changed
- `static/css/bonsai.css` restructured — color vars now scoped per `[data-bonsai-theme]`; layout vars (radius/gap/pad/fonts) stay in `:root`.
- `layouts/_default/baseof.html` emits `data-bonsai-theme` attribute on `<html>`.
- `layouts/partials/bio-card.html` delegates avatar rendering to new `partials/avatar.html`.
- `static/js/theme-toggle.js` syncs `aria-pressed` on first paint and on click.
- `exampleSite/hugo.toml` switches demo to `colorTheme = "sakura"` to showcase a non-default palette.
## [0.1.0] — 2026-05-01
+22 -2
View File
@@ -88,9 +88,16 @@ disableKinds = ["taxonomy", "term", "RSS", "sitemap", "404"]
| `name` | string | site `title` | Display name shown as `<h1>`. |
| `tagline` | string | — | One-liner under the name. |
| `bio` | string (markdown) | — | Short bio paragraph. Markdown supported. |
| `avatar` | string (URL) | — | Avatar image path. Omit to skip. |
| `avatar` | string (URL) | — | Avatar image path. If unset, theme renders an SVG circle with auto-derived initials. |
| `avatarInitials` | string | first letters of `name` | Override the initials when no `avatar` is set. |
| `avatarBg` | string (CSS color) | `var(--bonsai-accent)` | Background color of the initials circle. |
| `favicon` | string (URL) | `/favicon.ico` | Favicon path. |
| `themeToggle` | bool | `false` | Render the light/dark toggle button + load the toggle script. |
| `colorTheme` | string | `bonsai` | Palette: `bonsai`, `sakura`, `sumi`, or `koi`. See [Color themes](#color-themes). |
| `themeToggle` | bool | `false` | Render a sun/moon button in the footer + load the toggle script. |
| `schema` | bool | `true` | Emit schema.org `Person` JSON-LD in `<head>`. Set `false` if you provide your own. |
| `jobTitle` | string | — | Optional `Person.jobTitle` field for JSON-LD. |
| `location` | string | — | Optional `Person.address` field for JSON-LD. |
| `email` | string | — | Optional `Person.email` field for JSON-LD. |
| `footer` | bool | `true` | Show the footer. |
| `footerText` | string (HTML) | `© {year} {name}` | Override footer text. HTML allowed. |
| `links` | array | — | Bio links. See below. |
@@ -103,6 +110,19 @@ disableKinds = ["taxonomy", "term", "RSS", "sitemap", "404"]
| `url` | string | yes | Link target. `mailto:` and `tel:` are rendered without `target=_blank`. |
| `icon` | string | no | Icon name from the available set (see below). Unknown names render a generic external-link glyph. |
## Color themes
Four built-in palettes, each with light + dark variants. Set `colorTheme` in `[params]`:
| Name | Vibe | Accent |
|------|------|--------|
| `bonsai` *(default)* | washi paper + vermilion seal | `#8b3a2b` |
| `sakura` | cherry blossom pink | `#d4456a` |
| `sumi` | monochrome ink | `#1a1a1a` |
| `koi` | orange + cream | `#c8521e` |
Live preview: **[tiennm99.github.io/bonsai/themes/](https://tiennm99.github.io/bonsai/themes/)**.
## Available Icons
<details>
@@ -0,0 +1,59 @@
# Bonsai v0.1: Icon Library Split Forced Mid-Course Correction
**Date**: 2026-05-03 14:30
**Severity**: Medium
**Component**: Icon system, asset paths, demo deployment
**Status**: Resolved
## What Happened
Shipped Bonsai v0.1.0 to Hugo themes catalog in one day: icon system overhaul (Lucide + Simple Icons vendoring), CI/CD pipeline, GitHub Pages demo, docs, submission assets, and PR #708 to gohugoio. All 6 phases executed end-to-end with 11 commits. Live at https://tiennm99.github.io/bonsai/; catalog PR awaiting review.
## The Brutal Truth
Shipping a theme on spec in 15 hours felt clean until we deployed. The first three surprises weren't fatal but exposed how little we knew about Hugo's URL handling under GitHub Pages subpaths and how Chromium renders fonts in CI. Each one cost a fix commit. The Japanese avatar glyph as tofu in a headless browser was funny; the SVGs rendering at 0×0 was not.
## Technical Details
1. **Icon library split enforcement**: Lucide intentionally omits brand/social icons (design principle). Simple Icons provides them but ships SVGs with only `viewBox`, no intrinsic dimensions. First screenshot: brand icons invisible. Fix: `.link__icon svg { width:20px; height:20px }` in `assets/css/style.css`.
2. **Hugo URL function behavior under subpaths**: `relURL` and `absURL` pass through leading-slash inputs unchanged, which bypasses the baseURL subpath. Asset paths broke on `tiennm99.github.io/bonsai/static/*`. Fix: strip leading slash before `relURL`: `strings.TrimPrefix "/" $path | relURL` in `partials/icon.html` and layout.
3. **Chromium headless rendering**: No Japanese fonts in Docker env → 盆 monogram rendered as tofu boxes. Switched to Latin "T" in avatar. Future: any glyph-specific UX needs SVG paths or pre-rendered images.
4. **Playwright Docker deps**: Headless Chromium needs system libs absent from base image: `libnss3 libnspr4 libatk1.0-0t64 libatspi2.0-0t64 libxcomposite1 libxdamage1 libcups2t64`. Documented in `scripts/capture-screenshots.sh`.
## What We Tried
- Initial icon partial used inline SVGs only (8 icons) — ran out of coverage, switched to two-source vendoring.
- Attempted to use Lucide as single source — failed (no brand icons by design).
- First deploy: brand icons invisible in screenshots — diagnostic: SVGs had viewBox only, zero dimensions in flex layout.
- Japanese avatar: rendered in local dev, broke in CI — switching glyph is safer than shipping fonts.
## Root Cause Analysis
**Icon library research was shallow.** The plan correctly identified the split strategy, but we didn't validate that Simple Icons SVGs lack intrinsic dimensions until deploy. A quick inspection of one SVG from each library before implementation would have caught this.
**URL function assumptions didn't account for subpath deploys.** Standard Hugo docs assume domain root. GitHub Pages subpaths require different handling. Should've tested asset paths locally with `baseURL: /bonsai/` before CI.
**No validation that Chromium env matched design expectations.** The avatar glyph choice was visual-first; zero verification that it would render in the CI environment.
## Lessons Learned
1. **Inspect library assets, don't trust API docs.** Download one SVG from each icon library, check for width/height attributes, test in a flex container.
2. **Test baseURL subpaths locally.** Hugo's GitHub Pages subpath handling is a gotcha. Always `hugo server --baseURL http://localhost:1313/bonsai/` before deploy.
3. **Headless rendering != local rendering.** Fonts, system libs, and environment assumptions bite hard. Build and screenshot in CI early, not on final push.
4. **Document dependency chains in scripts.** The Playwright setup needed specific system packages. Future: add precheck or include install hints in README.
## Next Steps
- Monitor PR #708 review feedback from Hugo catalog maintainers.
- If layout changes are requested, we have the icon system and CI infrastructure in place to iterate.
- For v1.0: consider layout variants (cards/grid) — the data-driven icon partial scales well.
- Keep screenshot capture script maintenance low: Playwright + Chromium is reliable once deps are documented.
**Owner**: @tiennm99
**Timeline**: v0.2 planning after catalog acceptance feedback
@@ -0,0 +1,4 @@
---
title: "Color themes"
type: themes
---
+5 -2
View File
@@ -10,8 +10,11 @@ disableKinds = ["taxonomy", "term", "RSS", "sitemap", "404"]
name = "Tien Nguyen"
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
# avatar omitted intentionally so the demo showcases the initials fallback
# (the SVG circle uses the active palette's accent color)
colorTheme = "sakura" # try "bonsai" (default), "sakura", "sumi", "koi"
themeToggle = true
jobTitle = "Software Engineer"
footerText = "© 2026 · made with bonsai"
[[params.links]]
Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 37 KiB

+1 -1
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="{{ site.LanguageCode | default `en` }}" data-theme="auto">
<html lang="{{ site.LanguageCode | default `en` }}" data-theme="auto" data-bonsai-theme="{{ site.Params.colorTheme | default `bonsai` }}">
<head>
{{- partial "head.html" . -}}
</head>
@@ -0,0 +1,30 @@
{{- /* Render bio avatar.
If params.avatar is set, render <img>.
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 -}}
<img class="bio__avatar" src="{{ $avatar | strings.TrimPrefix "/" | relURL }}" alt="{{ $name }}" loading="eager" />
{{- 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)" -}}
<svg class="bio__avatar bio__avatar--initials" viewBox="0 0 112 112" width="112" height="112" role="img" aria-label="{{ $name }}">
<circle cx="56" cy="56" r="56" fill="{{ $bg }}"/>
<text x="56" y="74" text-anchor="middle"
font-family="ui-serif, Georgia, serif"
font-size="56" font-weight="600"
fill="var(--bonsai-bg)">{{ $initials }}</text>
</svg>
{{- end -}}
+1 -4
View File
@@ -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 -}}
<article class="bio">
{{- if $avatar }}
<img class="bio__avatar" src="{{ $avatar | strings.TrimPrefix "/" | relURL }}" alt="{{ $name }}" loading="eager" />
{{- end }}
{{ partial "avatar.html" . }}
<h1 class="bio__name">{{ $name }}</h1>
@@ -5,5 +5,8 @@
{{- else -}}
<p>© {{ now.Year }} {{ site.Params.name | default site.Title }}</p>
{{- end -}}
{{- if site.Params.themeToggle -}}
{{ partial "theme-toggle-button.html" . }}
{{- end -}}
</footer>
{{- end -}}
+2
View File
@@ -20,6 +20,8 @@
{{- end }}
<meta name="twitter:card" content="summary" />
{{ partial "schema-person.html" . }}
<link rel="icon" href="{{ (site.Params.favicon | default `favicon.ico`) | strings.TrimPrefix "/" | relURL }}" />
<link rel="stylesheet" href="{{ `css/bonsai.css` | relURL }}" />
{{- if site.Params.themeToggle }}
@@ -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 -}}
<script type="application/ld+json">{{ $payload | jsonify | safeJS }}</script>
{{- end -}}
@@ -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). */ -}}
<button type="button" class="theme-toggle"
data-bonsai-theme-toggle
aria-label="Toggle light and dark theme"
aria-pressed="false"
title="Toggle theme">
<svg class="theme-toggle__sun" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<circle cx="12" cy="12" r="4" fill="currentColor"/>
<g stroke="currentColor" stroke-width="2" stroke-linecap="round">
<line x1="12" y1="2" x2="12" y2="5"/>
<line x1="12" y1="19" x2="12" y2="22"/>
<line x1="2" y1="12" x2="5" y2="12"/>
<line x1="19" y1="12" x2="22" y2="12"/>
<line x1="4.9" y1="4.9" x2="7" y2="7"/>
<line x1="17" y1="17" x2="19.1" y2="19.1"/>
<line x1="4.9" y1="19.1" x2="7" y2="17"/>
<line x1="17" y1="7" x2="19.1" y2="4.9"/>
</g>
</svg>
<svg class="theme-toggle__moon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" fill="currentColor"/>
</svg>
</button>
+26
View File
@@ -0,0 +1,26 @@
{{ define "main" }}
<article style="text-align:center;">
<h1 style="font-family:var(--bonsai-font-display);">Color themes</h1>
<p style="color:var(--bonsai-muted);max-width:32rem;margin:0 auto 2rem;">
Set <code>colorTheme = "bonsai"</code> in your <code>hugo.toml</code> <code>[params]</code> block. Each palette has light + dark variants.
</p>
</article>
{{- $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")
-}}
<div class="themes-gallery">
{{- range $palettes }}
<div data-bonsai-theme="{{ .name }}" class="themes-gallery__card">
<h3 class="themes-gallery__name">{{ .label }}</h3>
<p class="themes-gallery__hex">accent {{ .accent }}</p>
<span class="themes-gallery__chip">link button</span>
<span class="themes-gallery__chip themes-gallery__chip--accent">accent</span>
</div>
{{- end }}
</div>
{{ end }}
+225 -19
View File
@@ -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,104 @@ 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;
}
/* Show moon in light, sun in dark.
The button shows the icon for the mode you'll switch TO.
Per-class rules avoid the specificity battle vs `.theme-toggle svg`. */
.theme-toggle .theme-toggle__sun { display: none; }
.theme-toggle .theme-toggle__moon { display: block; }
@media (prefers-color-scheme: dark) {
html:not([data-theme="light"]) .theme-toggle .theme-toggle__sun { display: block; }
html:not([data-theme="light"]) .theme-toggle .theme-toggle__moon { display: none; }
}
[data-theme="dark"] .theme-toggle .theme-toggle__sun { display: block; }
[data-theme="dark"] .theme-toggle .theme-toggle__moon { display: none; }
[data-theme="light"] .theme-toggle .theme-toggle__sun { display: none; }
[data-theme="light"] .theme-toggle .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) {
+14 -5
View File
@@ -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();
});
})();