mirror of
https://github.com/tiennm99/iammiti99.git
synced 2026-10-14 06:17:54 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
feeb6a8731 | ||
|
|
d8c6e8a419 | ||
|
|
35dfa8f3dc | ||
|
|
698f39392f | ||
|
|
689bb80edb | ||
|
|
086b675c31 | ||
|
|
8bdcc2369a | ||
|
|
19f6b85316 | ||
|
|
7809c0b4c8 | ||
|
|
949d73c141 | ||
|
|
eff11d99d8 | ||
|
|
7487aa2c36 | ||
|
|
921390c351 | ||
|
|
7468f3afba | ||
|
|
ce08594810 |
No files matched your search
+2
-2
@@ -17,7 +17,7 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
run: hugo --gc --minify --themesDir ../.. --printPathWarnings
|
||||
|
||||
- name: Upload built site
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: exampleSite-public
|
||||
path: exampleSite/public
|
||||
|
||||
+4
-4
@@ -18,13 +18,13 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- id: pages
|
||||
uses: actions/configure-pages@v5
|
||||
uses: actions/configure-pages@v6
|
||||
|
||||
- name: Setup Hugo Extended
|
||||
uses: peaceiris/actions-hugo@v3
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
--baseURL "${{ steps.pages.outputs.base_url }}/"
|
||||
|
||||
- name: Upload Pages artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
uses: actions/upload-pages-artifact@v5
|
||||
with:
|
||||
path: exampleSite/public
|
||||
|
||||
@@ -54,4 +54,4 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
uses: actions/deploy-pages@v5
|
||||
@@ -5,7 +5,42 @@ 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.4 — auto-generated OG images, RSS opt-in, multi-section bio)
|
||||
|
||||
## [0.3.0] — 2026-05-03
|
||||
|
||||
Layout, social-preview, and i18n release. All additions are strictly opt-in; v0.2 sites upgrade with no config edits.
|
||||
|
||||
### Added
|
||||
- **Layout variants** — `params.layout` accepts `stack` (default), `grid` (responsive 2-col), or `inline` (icon-only horizontal row). Pure CSS, zero JS. Unknown values warn at build time and fall back to `stack`. Live gallery at `/variants/`.
|
||||
- **OG image controls** — `params.ogImageUrl` for an explicit 1200×630 social-preview image (auto-upgrades `twitter:card` to `summary_large_image`); `params.ogImage = false` to suppress all `og:image` / `twitter:image` tags. Avatar still serves as the fallback when neither is set.
|
||||
- **i18n string externalization** — every theme-rendered string (`nav` aria-label, theme-toggle labels, default footer template) sourced from `i18n/{lang}.toml`. Bundles for `en` (default) and `vi` ship with the theme. Adding a language is a single-file change.
|
||||
|
||||
### Changed
|
||||
- `layouts/partials/bio-card.html` validates `params.layout` against the allowlist and emits `bio__links bio__links--{layout}` so variant rules scope under a parent class (avoids the v0.2-style specificity collision).
|
||||
- `layouts/partials/head.html` resolves OG image via the new `ogImage`/`ogImageUrl` ladder; emits `twitter:image` whenever `og:image` is present.
|
||||
- `layouts/partials/footer.html` uses `i18n "footer_default"` when no `params.footerText` override is set.
|
||||
- `exampleSite/hugo.toml` switches the demo to `layout = "grid"` to make the new feature visible on the live site.
|
||||
|
||||
### Deferred
|
||||
- Auto-generated OG images (text overlay on per-palette base PNGs). Required vendoring a TTF + 4 base PNGs (~150 KB binary) into a theme that prides itself on `< 4 KB CSS`. Moved to v0.4 once a smaller path is found.
|
||||
|
||||
## [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
|
||||
|
||||
|
||||
+63
-4
@@ -6,7 +6,7 @@
|
||||
|
||||
A minimalist Hugo theme for link-in-bio pages, inspired by [Linktree](https://linktr.ee) and the Japanese art of [bonsai](https://en.wikipedia.org/wiki/Bonsai) — *small, curated, intentional*.
|
||||
|
||||
**→ [Live demo](https://tiennm99.github.io/bonsai/)** · **[Icon gallery](https://tiennm99.github.io/bonsai/icons/)**
|
||||
**→ [Live demo](https://tiennm99.github.io/bonsai/)** · **[Icon gallery](https://tiennm99.github.io/bonsai/icons/)** · **[Layout variants](https://tiennm99.github.io/bonsai/variants/)** · **[Color themes](https://tiennm99.github.io/bonsai/themes/)**
|
||||
|
||||
> 盆栽 (bonsai): "tray planting" — the art of growing miniature trees through patient, deliberate cultivation. Every branch placed with care.
|
||||
|
||||
@@ -19,7 +19,7 @@ Bonsai treats your bio page the same way: a quiet, well-pruned page that surface
|
||||
- **35 icons out of the box** — 25 brand (GitHub, Mastodon, Bluesky, X, Threads, LinkedIn, Instagram…) + 10 utility (mail, globe, rss…). Vendored from [Simple Icons](https://simpleicons.org) and [Lucide](https://lucide.dev).
|
||||
- **Light & dark mode** — respects `prefers-color-scheme`; optional toggle.
|
||||
- **Zero JavaScript by default** — pure HTML + CSS; opt-in JS for theme toggle only.
|
||||
- **Fast** — < 4 KB CSS, < 4 KB HTML, no web fonts (system stack), no runtime fetches.
|
||||
- **Fast** — < 3 KB gzipped CSS, no web fonts (system stack), no runtime fetches.
|
||||
- **Accessible** — semantic HTML, focus-visible outlines, `prefers-reduced-motion`.
|
||||
- **Responsive** — mobile-first, looks right at every viewport.
|
||||
|
||||
@@ -88,9 +88,19 @@ 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). |
|
||||
| `layout` | string | `stack` | Link arrangement: `stack`, `grid`, or `inline`. See [Layout variants](#layout-variants). |
|
||||
| `themeToggle` | bool | `false` | Render a sun/moon button in the footer + load the toggle script. |
|
||||
| `ogImage` | bool | `true` | Set `false` to suppress all `og:image` / `twitter:image` tags. |
|
||||
| `ogImageUrl` | string (URL) | — | Explicit OG preview image (1200×630 recommended). Overrides the avatar fallback and upgrades Twitter card to `summary_large_image`. |
|
||||
| `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 +113,55 @@ 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/)**.
|
||||
|
||||
## Layout variants
|
||||
|
||||
Three arrangements for `[[params.links]]`. Pick one via `layout` in `[params]`:
|
||||
|
||||
| Value | Look | Best for |
|
||||
|-------|------|----------|
|
||||
| `stack` *(default)* | Full-width vertical buttons | ≤ 6 link bios; the classic Linktree shape. |
|
||||
| `grid` | Two-column responsive grid (collapses to one column under 480 px) | 6–12 links; balances density and tap-target size. |
|
||||
| `inline` | Icon-only horizontal row | Lots of accounts, short page; titles stay in DOM for screen readers. |
|
||||
|
||||
Live preview: **[tiennm99.github.io/bonsai/variants/](https://tiennm99.github.io/bonsai/variants/)**.
|
||||
|
||||
## i18n
|
||||
|
||||
Theme-rendered strings (nav landmark, theme-toggle labels, default footer) live in `i18n/{lang}.toml`. Bundles for `en` and `vi` ship with the theme.
|
||||
|
||||
To use another language, set Hugo's `defaultContentLanguage` and add a matching `i18n/{lang}.toml`:
|
||||
|
||||
```toml
|
||||
defaultContentLanguage = "fr"
|
||||
```
|
||||
|
||||
```toml
|
||||
# i18n/fr.toml
|
||||
[nav_links_label]
|
||||
other = "Liens"
|
||||
[theme_toggle_label]
|
||||
other = "Basculer le thème clair / sombre"
|
||||
[theme_toggle_title]
|
||||
other = "Basculer le thème"
|
||||
[footer_default]
|
||||
other = "© {{ .year }} {{ .name }}"
|
||||
```
|
||||
|
||||
Missing keys fall back to `en`. User content (`name`, `tagline`, `bio`, link `title`s, `footerText`) is never auto-translated — it stays user-owned.
|
||||
|
||||
## 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,51 @@
|
||||
# Bonsai v0.2: Same-Day Polish Release Caught CSS Specificity and Hugo's JSON Escape Trap
|
||||
|
||||
**Date**: 2026-05-03 20:15
|
||||
**Severity**: Medium
|
||||
**Component**: Theme toggle UI, schema.org markup, CSS cascade, SVG assets
|
||||
**Status**: Resolved
|
||||
|
||||
## What Happened
|
||||
|
||||
Shipped v0.2.0 same day as v0.1.0 (7 hours apart): 4 color theme presets (sakura/sumi/koi via CSS custom properties), schema.org Person JSON-LD, theme-toggle button with sun/moon icons, avatar initials fallback SVG. All 5 phases completed in ~6h. Tagged release; live demo + themes gallery live. No breaking changes — all features opt-in via params.
|
||||
|
||||
## The Brutal Truth
|
||||
|
||||
Adding features that fast after a critical v0.1 release felt reckless but the modularity kept us honest. We caught two ugly bugs only because the screenshot review was rigorous. A CSS specificity silent failure sneaked into production; a Hugo templating anti-pattern almost shipped JSON garbage into `<script>` tags. Both were low-level enough to silently break on edge cases. The discipline of small additive commits meant rollback was one revert, not six.
|
||||
|
||||
## Technical Details
|
||||
|
||||
1. **Hugo `jsonify` double-encodes inside `<script>` context** — Hugo's auto-escape pipeline treats `<script>` block content as JavaScript string literal. Output `{{ $payload | jsonify }}` becomes `"{\"@context\":...}"` (stringified JSON string) instead of raw JSON. Fix: pipe through `safeJS` to mark output as safe for JS context: `{{ $payload | jsonify | safeJS }}`. Manifests as broken schema.org payloads when parsed by search crawlers.
|
||||
|
||||
2. **CSS specificity silent battle on `.theme-toggle`** — `.theme-toggle svg { display: block }` (specificity 0,1,1) silently beat `.theme-toggle__sun { display: none }` (specificity 0,1,0). Both icons rendered at once, toggling did nothing visible. Caught only because screenshot diff showed two moons stacked. Fix: scope all per-icon rules under parent: `.theme-toggle .theme-toggle__sun { display: none }` to match the broader selector's context. Lesson: CSS specificity debugs invisibly.
|
||||
|
||||
3. **Hardcoded color in `exampleSite/static/images/avatar.svg`** — vermilion `fill="#8b3a2b"` locked the avatar to one palette; theme toggle recolored the site but avatar stayed red. Removed avatar from exampleSite entirely to showcase the new initials fallback feature. Felt like a loss until we realized the demo got *better* — validates that initials auto-inherit palette accent and live-update on theme toggle.
|
||||
|
||||
4. **Small + additive release cadence builds confidence in rollback** — v0.1 was large. v0.2 added 4 features in 5 commits. When specificity bug surfaced at final review, the fix was a 2-line CSS change in a single commit. If all v0.2 work were one megacommit, rollback would've nuked 6 hours of work. Discipline in commit granularity pays off.
|
||||
|
||||
## Root Cause Analysis
|
||||
|
||||
- **Hugo template quirks aren't obvious from docs.** `jsonify` works fine in template interpolation but breaks in `<script>`. Needed to test the actual rendered HTML, not just the template logic.
|
||||
- **CSS cascade assumed matching specificity.** Both selectors hit the toggle element; only one was checked. No linting caught this because both selectors are valid — the bug is order + specificity interaction.
|
||||
- **Static assets assumed single-theme context.** SVG fill attributes don't inherit from CSS custom properties by default. Should've validated asset reuse under theme toggle during phase 4 review.
|
||||
- **Commit discipline was the real safety net.** Each phase was one clean commit. Enabled fast iteration without fear of losing work.
|
||||
|
||||
## Lessons Learned
|
||||
|
||||
1. **Test rendered HTML, not template output.** For JSON-LD and `<script>` blocks, inspect the final HTML in DevTools. Auto-escape traps are invisible in template files.
|
||||
|
||||
2. **CSS specificity bugs hide best in visual tests.** Automated CSS linting misses specificity collisions. Screenshot diffs catch them immediately.
|
||||
|
||||
3. **Static SVG assets don't theme well.** If an SVG must recolor under theme toggle, either: remove the fill attribute and inherit from CSS, or generate it dynamically. Hardcoded fills are a gotcha.
|
||||
|
||||
4. **Same-day releases are only safe if phases are modular.** v0.1→v0.2 worked because phases had minimal coupling. Try this with a monolithic feature and you're fighting merge conflicts and rollback chaos.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Monitor schema.org validation via Google Search Console (confirm JSON-LD is parsed correctly after safeJS fix).
|
||||
- Consider CSS specificity audit for all `.theme-toggle__*` variants before v0.3 (preventive).
|
||||
- For future static assets: add a validation step in screenshot capture that checks for hardcoded colors that don't inherit.
|
||||
- v0.3 candidates: layout variants (cards/grid), OG image generation, i18n setup. Keep the same cadence: small, additive, low blast radius.
|
||||
|
||||
**Owner**: @tiennm99
|
||||
**Timeline**: Next release pending catalog PR #708 feedback
|
||||
@@ -0,0 +1,61 @@
|
||||
# Bonsai v0.3: Layout Variants and i18n Shipped Same Day, OG Auto-Generation Cut for Theme Weight
|
||||
|
||||
**Date**: 2026-05-03 12:10
|
||||
**Severity**: Low
|
||||
**Component**: Layout system, i18n bundle, OG meta tags, demo site
|
||||
**Status**: Resolved
|
||||
|
||||
## What Happened
|
||||
|
||||
Shipped v0.3 features ~2 hours after v0.2 wrapped: `params.layout` for stack/grid/inline link arrangements, `i18n/{en,vi}.toml` bundles for theme strings, and `params.ogImageUrl` + `ogImage` controls for social previews. **Cut OG image auto-generation mid-implementation** — vendoring a TTF font + 4 base PNGs (~150 KB) conflicted directly with the theme's `< 4 KB CSS` minimalist principle. Reduced Phase 2 scope to URL override + smart `twitter:card` upgrade; deferred auto-gen to v0.4. All 5 phase files written, 4 of 5 phases done; tag/push pending user authorization.
|
||||
|
||||
## The Brutal Truth
|
||||
|
||||
The hardest thing about v0.3 wasn't writing code — it was killing a feature mid-implementation when its weight didn't match the theme's identity. Plan said "ship OG auto-generation," code said "vendor a 120 KB font," principle said "stay under 4 KB CSS." Three votes, principle won. Better to ship four solid features than five with one obese addition.
|
||||
|
||||
The same-day cadence held because v0.2's modularity work paid dividends: layout variants slot in as one CSS section + one template line, i18n is one Hugo function call replacing literal strings, OG params are six lines of template logic. Zero refactors, zero broken tests, zero rollbacks.
|
||||
|
||||
Caught one real bug in testing: my first i18n smoke test set `languageCode = "vi-VN"` and watched English render. Spent a minute confused before remembering Hugo uses `defaultContentLanguage` (not `languageCode`) to select i18n bundles. That's now documented in the README so the next person doesn't waste the same minute.
|
||||
|
||||
## Technical Details
|
||||
|
||||
1. **CSS specificity discipline carried from v0.2** — Layout variant rules scope under `.bio__links--{variant} .link { ... }`, never naked `.link { ... }`. The v0.2 theme-toggle bug (where `.theme-toggle svg` silently beat `.theme-toggle__sun` at specificity 0,1,1 vs 0,1,0) is fresh enough that the discipline came automatically. No specificity collisions in v0.3.
|
||||
|
||||
2. **Inline layout uses screen-reader-friendly visual hiding** — `.bio__links--inline .link__title` clips the title to a 1×1 px region with `clip: rect(0,0,0,0)` instead of `display: none`. Titles stay in the DOM and accessible to screen readers; visually only the icon shows. Standard pattern, but worth being deliberate about — `display: none` would have hidden them from AT, breaking the link's purpose for non-sighted users.
|
||||
|
||||
3. **Hugo `languageCode` ≠ i18n bundle selector** — `languageCode = "vi-VN"` sets the HTML `lang` attribute and metadata, but Hugo picks `i18n/*.toml` bundles based on `defaultContentLanguage`. For a single-language site, both must be set. Documented this in the README's new i18n section because it's the kind of gotcha that costs people 20 minutes the first time.
|
||||
|
||||
4. **OG image auto-generation killed for theme-weight reasons** — Original plan: vendor a TTF (~50–120 KB Latin+Vietnamese subset of Inter) and 4 base PNGs (~5–20 KB each), use Hugo's `images.Text` filter to overlay name + tagline at build time. Total binary weight: ~100–200 KB. Theme today: ~12 KB CSS + ~13 KB icon SVGs. Ratio: auto-gen would have nearly doubled the theme's binary footprint to support an opt-in feature. Decision: ship the URL override (6 lines of template logic, zero new bytes), defer auto-gen to v0.4 once a smaller path is found. **The feature didn't change; the implementation timeline did.**
|
||||
|
||||
5. **README's `< 4 KB CSS` claim was already stale** — Inherited from v0.1. v0.2's themes-gallery + v0.3's variants-gallery pushed raw CSS to 11.8 KB. Gzipped is still 2.9 KB. Updated the claim to `< 3 KB gzipped CSS`, which is the meaningful number for transfer cost. Lesson: marketing-style claims rot fast; quote sizes that match reality.
|
||||
|
||||
## Root Cause Analysis
|
||||
|
||||
- **Plan optimism vs implementation reality** — On paper, "use Hugo's `images.Text` filter" sounds like a one-partial change. In practice, it's a font-vendoring decision with license review, glyph-coverage analysis, and a binary asset commitment. The plan should have flagged "binary asset weight" as a Phase 2 risk dimension, not just "Hugo template quirks."
|
||||
|
||||
- **Hugo locale resolution is under-documented for single-language sites** — Most Hugo i18n examples assume multilingual setups with `[languages]` blocks. Single-language with non-English bundle is a less-traveled path; the `defaultContentLanguage` requirement is easy to miss.
|
||||
|
||||
- **CSS bundle drift went uncaught for a release cycle** — v0.2 grew the bundle past the README claim and nobody (me) noticed until v0.3 added more. A doctests-style "verify README claims" hook would catch this; not worth building yet, but worth knowing exists as a category of stale-doc bug.
|
||||
|
||||
## Lessons Learned
|
||||
|
||||
1. **Kill features mid-implementation when weight contradicts identity.** A theme that brags about being small can't ship a feature that doubles its binary footprint, even if the feature is desirable. Defer is a perfectly fine answer; ship-anyway-with-bloat is not.
|
||||
|
||||
2. **CSS specificity discipline becomes muscle memory after one incident.** The v0.2 theme-toggle bug was acutely painful enough that v0.3's variant CSS auto-scoped under parent classes without conscious effort. Real bugs teach better than style guides.
|
||||
|
||||
3. **Document the gotchas you trip over in real time.** The `defaultContentLanguage` confusion took 60 seconds to resolve and would take 20 minutes for a fresh user. Three lines in the README's i18n section makes it a non-issue forever.
|
||||
|
||||
4. **Quote real numbers, not aspirational ones.** `< 4 KB CSS` was true once. `< 3 KB gzipped CSS` is true now. The second one will stay true longer because it's tied to a transfer-cost reality, not a frozen-at-launch ideal.
|
||||
|
||||
5. **Same-day cadence works when phases are independent.** Phases 1, 2, 3 touched disjoint files (`bio-card.html`+CSS, `head.html`, `i18n/`+two partials). They could have shipped in any order. That's not architectural luck — it's a result of v0.2's component carve-up making each phase a leaf change instead of a refactor.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Regenerate `images/screenshot.png` + `images/tn.png` from a real browser (no headless tooling available in the dev environment); needed for Hugo themes-site refresh.
|
||||
- Commit + tag `v0.3.0` after screenshots land.
|
||||
- Verify OG previews on Twitter / Facebook / LinkedIn debuggers post-deploy.
|
||||
- v0.4 candidates: revisit OG auto-generation with a smaller path (system-font CSS-rendered SVG behind a rasterizer? Single-palette generic base? User-supplied font path?), RSS opt-in, multi-section bio, more icons.
|
||||
- Consider extracting the gallery CSS (`.themes-gallery__*`, `.variants-gallery__*`) to a separate `gallery.css` loaded only on demo pages — saves ~2 KB on every user's site for CSS that only the demo needs.
|
||||
|
||||
**Owner**: @tiennm99
|
||||
**Timeline**: Tag pending screenshot regen
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Color themes"
|
||||
type: themes
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Layout variants"
|
||||
type: variants
|
||||
---
|
||||
@@ -10,9 +10,14 @@ 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"
|
||||
layout = "grid" # try "stack" (default), "grid", "inline"
|
||||
themeToggle = true
|
||||
jobTitle = "Software Engineer"
|
||||
footerText = "© 2026 · made with bonsai"
|
||||
# ogImageUrl = "/og.png" # optional 1200×630 image for social previews
|
||||
|
||||
[[params.links]]
|
||||
title = "GitHub"
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Bonsai theme — English strings.
|
||||
# Override per language by adding e.g. i18n/vi.toml; missing keys fall back to en.
|
||||
|
||||
[nav_links_label]
|
||||
other = "Links"
|
||||
|
||||
[theme_toggle_label]
|
||||
other = "Toggle light and dark theme"
|
||||
|
||||
[theme_toggle_title]
|
||||
other = "Toggle theme"
|
||||
|
||||
[footer_default]
|
||||
other = "© {{ .year }} {{ .name }}"
|
||||
@@ -0,0 +1,13 @@
|
||||
# Bonsai theme — Vietnamese strings.
|
||||
|
||||
[nav_links_label]
|
||||
other = "Liên kết"
|
||||
|
||||
[theme_toggle_label]
|
||||
other = "Chuyển giao diện sáng / tối"
|
||||
|
||||
[theme_toggle_title]
|
||||
other = "Đổi giao diện"
|
||||
|
||||
[footer_default]
|
||||
other = "© {{ .year }} {{ .name }}"
|
||||
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,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,13 +1,16 @@
|
||||
{{- $name := site.Params.name | default site.Title -}}
|
||||
{{- $tagline := site.Params.tagline -}}
|
||||
{{- $bio := site.Params.bio -}}
|
||||
{{- $avatar := site.Params.avatar -}}
|
||||
{{- $links := site.Params.links -}}
|
||||
{{- $layout := site.Params.layout | default "stack" -}}
|
||||
{{- $validLayouts := slice "stack" "grid" "inline" -}}
|
||||
{{- if not (in $validLayouts $layout) -}}
|
||||
{{- warnf "bonsai: unknown params.layout %q (expected stack|grid|inline), falling back to 'stack'" $layout -}}
|
||||
{{- $layout = "stack" -}}
|
||||
{{- end -}}
|
||||
|
||||
<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>
|
||||
|
||||
@@ -20,7 +23,7 @@
|
||||
{{- end }}
|
||||
|
||||
{{- if $links }}
|
||||
<nav class="bio__links" aria-label="Links">
|
||||
<nav class="bio__links bio__links--{{ $layout }}" aria-label="{{ i18n "nav_links_label" | default "Links" }}">
|
||||
{{- range $links }}
|
||||
{{- partial "link-button.html" . -}}
|
||||
{{- end }}
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
{{- with site.Params.footerText -}}
|
||||
<p>{{ . | safeHTML }}</p>
|
||||
{{- else -}}
|
||||
<p>© {{ now.Year }} {{ site.Params.name | default site.Title }}</p>
|
||||
{{- $ctx := dict "year" now.Year "name" (site.Params.name | default site.Title) -}}
|
||||
<p>{{ i18n "footer_default" $ctx | default (printf "© %d %s" now.Year (site.Params.name | default site.Title)) | safeHTML }}</p>
|
||||
{{- end -}}
|
||||
{{- if site.Params.themeToggle -}}
|
||||
{{ partial "theme-toggle-button.html" . }}
|
||||
{{- end -}}
|
||||
</footer>
|
||||
{{- end -}}
|
||||
@@ -2,6 +2,21 @@
|
||||
{{- $tagline := site.Params.tagline | default "" -}}
|
||||
{{- $description := site.Params.bio | default $tagline | default site.Params.description -}}
|
||||
{{- $avatar := site.Params.avatar -}}
|
||||
{{- /* OG image resolution:
|
||||
- params.ogImage = false → emit nothing.
|
||||
- params.ogImageUrl set → use it (1200×630 expected → summary_large_image).
|
||||
- else fall back to params.avatar (square → summary card). */ -}}
|
||||
{{- $ogImageEnabled := site.Params.ogImage | default true -}}
|
||||
{{- $ogImageUrl := "" -}}
|
||||
{{- $twitterCard := "summary" -}}
|
||||
{{- if $ogImageEnabled -}}
|
||||
{{- if site.Params.ogImageUrl -}}
|
||||
{{- $ogImageUrl = site.Params.ogImageUrl | absURL -}}
|
||||
{{- $twitterCard = "summary_large_image" -}}
|
||||
{{- else if $avatar -}}
|
||||
{{- $ogImageUrl = $avatar | absURL -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
@@ -15,10 +30,13 @@
|
||||
{{- with $description }}
|
||||
<meta property="og:description" content="{{ . }}" />
|
||||
{{- end }}
|
||||
{{- with $avatar }}
|
||||
<meta property="og:image" content="{{ . | absURL }}" />
|
||||
{{- with $ogImageUrl }}
|
||||
<meta property="og:image" content="{{ . }}" />
|
||||
<meta name="twitter:image" content="{{ . }}" />
|
||||
{{- end }}
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:card" content="{{ $twitterCard }}" />
|
||||
|
||||
{{ 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 }}" />
|
||||
|
||||
@@ -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="{{ i18n "theme_toggle_label" | default "Toggle light and dark theme" }}"
|
||||
aria-pressed="false"
|
||||
title="{{ i18n "theme_toggle_title" | default "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>
|
||||
@@ -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 }}
|
||||
@@ -0,0 +1,36 @@
|
||||
{{ define "main" }}
|
||||
<article style="text-align:center;">
|
||||
<h1 style="font-family:var(--bonsai-font-display);">Layout variants</h1>
|
||||
<p style="color:var(--bonsai-muted);max-width:32rem;margin:0 auto 2rem;">
|
||||
Set <code>layout = "stack"</code> (default), <code>"grid"</code>, or <code>"inline"</code> in your <code>hugo.toml</code> <code>[params]</code> block.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
{{- $sample := slice
|
||||
(dict "title" "GitHub" "url" "#" "icon" "github")
|
||||
(dict "title" "LinkedIn" "url" "#" "icon" "linkedin")
|
||||
(dict "title" "Email" "url" "#" "icon" "mail")
|
||||
(dict "title" "Mastodon" "url" "#" "icon" "mastodon")
|
||||
-}}
|
||||
|
||||
{{- $variants := slice
|
||||
(dict "name" "stack" "label" "Stack (default)" "desc" "Full-width vertical buttons. Best for ≤ 6 links.")
|
||||
(dict "name" "grid" "label" "Grid" "desc" "Two-column responsive grid; collapses to one column under 480 px.")
|
||||
(dict "name" "inline" "label" "Inline" "desc" "Icon-only horizontal row. Titles stay in DOM for screen readers.")
|
||||
-}}
|
||||
|
||||
<div class="variants-gallery">
|
||||
{{- range $variants }}
|
||||
<section class="variants-gallery__card">
|
||||
<h3 class="variants-gallery__name">{{ .label }}</h3>
|
||||
<p class="variants-gallery__desc">{{ .desc }}</p>
|
||||
<code class="variants-gallery__code">layout = "{{ .name }}"</code>
|
||||
<nav class="bio__links bio__links--{{ .name }}" aria-label="{{ .label }} sample">
|
||||
{{- range $sample }}
|
||||
{{- partial "link-button.html" . -}}
|
||||
{{- end }}
|
||||
</nav>
|
||||
</section>
|
||||
{{- end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
phase: 1
|
||||
title: "Layout variants (stack/grid/inline)"
|
||||
status: pending
|
||||
priority: P2
|
||||
effort: "2h"
|
||||
dependencies: []
|
||||
---
|
||||
|
||||
# Phase 1: Layout variants (stack/grid/inline)
|
||||
|
||||
## Overview
|
||||
Introduce `params.layout` to let users pick how `[[params.links]]` are arranged. Pure CSS — no JS, no template forks. Default unchanged (stack).
|
||||
|
||||
## Requirements
|
||||
- Functional:
|
||||
- New string param `layout` with values: `stack` (default), `grid`, `inline`.
|
||||
- `stack`: current full-width vertical buttons.
|
||||
- `grid`: 2-col responsive grid (collapses to 1-col below ~480px).
|
||||
- `inline`: icon-only horizontal row (titles become `aria-label`/`title` for a11y); great for users with many short links.
|
||||
- Unknown value → falls back to `stack` and emits a Hugo warning via `errorf`.
|
||||
- Non-functional:
|
||||
- Zero JS additions.
|
||||
- Visible layout class lives on `<nav class="bio__links">` so the cascade stays scoped.
|
||||
- All three variants pass focus-visible + reduced-motion tests.
|
||||
|
||||
## Architecture
|
||||
- Template: `bio-card.html` reads `site.Params.layout`, validates against allowlist, renders `<nav class="bio__links bio__links--{layout}">`.
|
||||
- CSS: add three rule blocks under `/* layouts */`. Variant rules are scoped: `.bio__links--grid .link { ... }`, never `.link { ... }` directly. (Lesson from v0.2 specificity bug.)
|
||||
- `inline` variant changes `link-button.html` rendering? **No** — keep one button template. Hide `.link__title` visually-but-not-from-AT in the inline variant via `.bio__links--inline .link__title { position: absolute; left: -10000px; }` (or use existing `clip-path` pattern); add `aria-label` only when title is hidden via CSS — actually simpler: keep title in DOM, hide visually, screen readers still get it. No template changes needed.
|
||||
|
||||
## Related Code Files
|
||||
- Modify:
|
||||
- `layouts/partials/bio-card.html` — read + validate `layout`, render variant class.
|
||||
- `static/css/bonsai.css` — add 3 variant rule blocks (grid + inline; stack is the default).
|
||||
- `exampleSite/hugo.toml` — uncomment example showing `layout = "grid"` (commented-out for default).
|
||||
- Create: none.
|
||||
- Delete: none.
|
||||
|
||||
## Implementation Steps
|
||||
1. In `bio-card.html`, add validation:
|
||||
```
|
||||
{{- $layout := site.Params.layout | default "stack" -}}
|
||||
{{- $valid := slice "stack" "grid" "inline" -}}
|
||||
{{- if not (in $valid $layout) }}
|
||||
{{- warnf "bonsai: unknown params.layout %q, falling back to 'stack'" $layout -}}
|
||||
{{- $layout = "stack" -}}
|
||||
{{- end -}}
|
||||
```
|
||||
2. Render `<nav class="bio__links bio__links--{{ $layout }}">`.
|
||||
3. CSS — append a `/* === layout variants === */` section after the existing `.bio__links` block. Default `.bio__links` rules stay; variants override only what differs (`grid-template-columns`, `flex-direction`, padding).
|
||||
4. For `inline`: hide `.link__title` via `clip` pattern — preserves screen-reader access; do **not** add `aria-label` (title is still in DOM).
|
||||
5. Add a 480px media query inside `--grid` so it collapses to 1-col on phones.
|
||||
6. Document the new param in README param table (deferred to Phase 4 — but note here so it's not lost).
|
||||
|
||||
## Todo List
|
||||
- [ ] Validate `params.layout` in `bio-card.html` with allowlist + `warnf`.
|
||||
- [ ] Append CSS for `.bio__links--grid`.
|
||||
- [ ] Append CSS for `.bio__links--inline` (icon-only with visually-hidden titles).
|
||||
- [ ] Add 480px breakpoint for `--grid`.
|
||||
- [ ] Verify all 3 variants render correctly via `hugo server` + screenshot diff.
|
||||
- [ ] Verify focus-visible outline works in all variants.
|
||||
- [ ] Verify reduced-motion respected (no new transitions added).
|
||||
|
||||
## Success Criteria
|
||||
- [ ] `params.layout = "stack"` (or unset) renders identically to v0.2.
|
||||
- [ ] `params.layout = "grid"` renders 2-col → 1-col at 480px.
|
||||
- [ ] `params.layout = "inline"` renders icon-only horizontal row; screen readers still announce link titles.
|
||||
- [ ] `params.layout = "garbage"` falls back to stack with a Hugo build warning.
|
||||
- [ ] No new JS bytes shipped.
|
||||
- [ ] CSS bundle stays under 5 KB.
|
||||
|
||||
## Risk Assessment
|
||||
- **CSS specificity collisions** (v0.2 lesson) — mitigate by scoping all variant rules under `.bio__links--{variant} .link`.
|
||||
- **Inline variant accessibility** — visually-hidden title pattern is well-known; verify with VoiceOver / NVDA via screenshot of accessibility tree.
|
||||
- **Grid breakpoint feels arbitrary at 480px** — chosen because most one-handed phone widths are ~390–430px; falls back to stack-equivalent below that.
|
||||
@@ -0,0 +1,103 @@
|
||||
---
|
||||
phase: 2
|
||||
title: "OG image params (override + twitter card upgrade)"
|
||||
status: in-progress
|
||||
priority: P2
|
||||
effort: "0.5h"
|
||||
dependencies: []
|
||||
notes: "Scope reduced from auto-generation. Auto-gen requires font + base PNG vendoring (~150 KB binary), conflicts with minimalist theme principle. Moved to v0.4 candidates."
|
||||
---
|
||||
|
||||
# Phase 2: OG image params (scope-reduced)
|
||||
|
||||
**Reduced from auto-generation.** Reason: build-time text overlay needs Hugo `images.Text` + vendored TTF (~50–120 KB) + 4 base PNGs. That's significant binary weight in a `< 4 KB CSS` theme. Defer auto-gen to v0.4 once a smaller path is found (e.g. system-font CSS-rendered SVG behind a rasterizer).
|
||||
|
||||
## What ships in v0.3
|
||||
- `params.ogImageUrl` — explicit user-provided OG image URL. Takes precedence over `params.avatar`.
|
||||
- `params.ogImage = false` — opt out entirely (emit no `og:image` / `twitter:image`).
|
||||
- When any image present, `twitter:card` upgrades from `summary` to `summary_large_image` if user explicitly opted in via `params.ogImageUrl` (since avatar is square, keep `summary` for avatar fallback).
|
||||
|
||||
## Overview
|
||||
Auto-generate a 1200×630 Open Graph preview image from `params.name`, `params.tagline`, and the active palette accent color. Falls back gracefully if user supplies their own `params.avatar` path that resolves to an OG-suitable image — and skippable entirely via `params.ogImage = false`.
|
||||
|
||||
## Requirements
|
||||
- Functional:
|
||||
- When `params.ogImage` is unset or `true` AND no `params.ogImageUrl` override: theme generates a 1200×630 PNG via Hugo image processing.
|
||||
- Generated image: solid background = palette `bg`; centered name (large) + tagline (smaller) in palette `fg`; small accent stripe on the left in palette `accent`.
|
||||
- When `params.ogImageUrl` is set: use that absolute/relative URL verbatim, skip generation.
|
||||
- When `params.ogImage = false`: emit no `og:image` and no `twitter:image` (back-compat with v0.2 behavior when no avatar set).
|
||||
- Same generated image used for `og:image` AND `twitter:image` (upgrade card to `summary_large_image`).
|
||||
- Non-functional:
|
||||
- Build time impact: < 500ms for default site (one image, one variant).
|
||||
- Generated image is content-hashed by Hugo for cache busting.
|
||||
- Works without any external binary — pure Hugo image API.
|
||||
|
||||
## Architecture
|
||||
- Hugo lacks native text-on-image rendering. Approach: ship a **template-PNG-per-palette** (4 PNGs, ~5 KB each) in `assets/og/` with the palette background + accent stripe baked in, leaving a transparent / solid text region. Then overlay text via SVG-to-image: render an SVG with name + tagline, convert to image via Hugo's `images.Filter` or external pipeline.
|
||||
- **Simpler alternative (chosen):** generate the entire OG image as an **SVG**, return it as `og:image` content. Crawlers (Twitter, Facebook, Slack, Discord, LinkedIn) all accept SVG OG images **except Twitter and Facebook** which strip them. So SVG-only is too risky.
|
||||
- **Final approach:** generate SVG → render to PNG using Hugo's built-in `resources.GetRemote` pattern is no good (no binary). Use Hugo's `images.Text` filter (Hugo 0.108+) which overlays text on an existing image using a TTF font. We ship one base palette PNG per theme (4 PNGs) and one TTF font (~50 KB Inter or system-stack-equivalent open font like Geist Mono — license check required).
|
||||
- Concrete pipeline:
|
||||
1. `assets/og/base-{theme}.png` — 1200×630 solid bg + accent stripe.
|
||||
2. `assets/og/font.ttf` — single open-license sans (Inter Regular ~120 KB, or smaller alt).
|
||||
3. New partial `partials/og-image.html` — does:
|
||||
```
|
||||
{{ $base := resources.Get (printf "og/base-%s.png" $theme) }}
|
||||
{{ $img := $base | images.Filter (images.Text $name (dict "color" $fg "size" 72 "x" 80 "y" 220 "font" $font))
|
||||
| images.Filter (images.Text $tagline (dict "color" $fg "size" 36 "x" 80 "y" 340 "font" $font)) }}
|
||||
{{ $img := $img | resources.Fingerprint }}
|
||||
```
|
||||
4. `head.html` consumes `{{ partial "og-image.html" . }}` to emit `<meta property="og:image">` + `<meta name="twitter:image">` + upgrade `twitter:card` to `summary_large_image`.
|
||||
|
||||
## Related Code Files
|
||||
- Create:
|
||||
- `assets/og/base-bonsai.png` (1200×630 — washi paper bg + vermilion stripe)
|
||||
- `assets/og/base-sakura.png`
|
||||
- `assets/og/base-sumi.png`
|
||||
- `assets/og/base-koi.png`
|
||||
- `assets/og/font.ttf` (single open-license sans; document source + license in NOTICE)
|
||||
- `layouts/partials/og-image.html`
|
||||
- `scripts/generate-og-bases.sh` — repeatable ImageMagick script that generates the 4 base PNGs from palette CSS values, so palette tweaks regenerate cleanly.
|
||||
- Modify:
|
||||
- `layouts/partials/head.html` — replace static `og:image` block with `{{ partial "og-image.html" . }}`; upgrade `twitter:card` to `summary_large_image` when image present.
|
||||
- `README.md` (in Phase 4) — document `params.ogImage` and `params.ogImageUrl`.
|
||||
- `NOTICE` — credit font + base image generation.
|
||||
- Delete: none.
|
||||
|
||||
## Implementation Steps
|
||||
1. Pick font. Candidates: Inter (OFL), Geist (OFL), IBM Plex Sans (OFL). Smallest weight that reads well at 72px. Document choice in NOTICE.
|
||||
2. Write `scripts/generate-og-bases.sh` — uses ImageMagick (`magick -size 1200x630 xc:#bg -fill #accent -draw 'rectangle 0,0 16,630'`) to generate all 4 base PNGs.
|
||||
3. Run script, commit the 4 base PNGs to `assets/og/`.
|
||||
4. Add font file. Verify license, add NOTICE entry.
|
||||
5. Write `layouts/partials/og-image.html`:
|
||||
- Resolve theme via same `site.Params.colorTheme | default "bonsai"` pattern.
|
||||
- Resolve foreground color via a small dict mapping theme → fg hex (matches CSS palette).
|
||||
- Skip generation if `site.Params.ogImage == false` → return empty.
|
||||
- Skip generation if `site.Params.ogImageUrl` set → emit it verbatim, return.
|
||||
- Otherwise: load base PNG, overlay name + tagline via `images.Text`, fingerprint, emit `<meta>` tags.
|
||||
6. Update `head.html` — call partial, remove old conditional.
|
||||
7. Add an exampleSite check: build `hugo --themesDir ../..` and verify `public/og-*.png` exists with overlay text.
|
||||
|
||||
## Todo List
|
||||
- [ ] Pick + license-check OG font; add to `assets/og/`; document in NOTICE.
|
||||
- [ ] Write `scripts/generate-og-bases.sh`; generate all 4 base PNGs; commit.
|
||||
- [ ] Implement `partials/og-image.html` with skip-on-false + url-override + generation paths.
|
||||
- [ ] Wire into `head.html`; upgrade twitter:card to `summary_large_image` when image present.
|
||||
- [ ] Verify generated image renders correctly via `hugo server` → inspect `public/og/*.png`.
|
||||
- [ ] Verify all 4 palette themes produce distinct, readable images.
|
||||
- [ ] Test crawler preview via opengraph.xyz or twitter card validator (manual).
|
||||
|
||||
## Success Criteria
|
||||
- [ ] Default config (no `ogImage*` params) generates a palette-matched OG image.
|
||||
- [ ] `params.ogImageUrl = "/static/my-og.png"` overrides generation, skips Hugo image pipeline.
|
||||
- [ ] `params.ogImage = false` emits no `og:image` / `twitter:image`.
|
||||
- [ ] All 4 themes produce visually-distinct, readable images.
|
||||
- [ ] Build time delta < 500ms for the default exampleSite.
|
||||
- [ ] Image is content-hashed (filename includes fingerprint).
|
||||
|
||||
## Risk Assessment
|
||||
- **Hugo `images.Text` requires Hugo 0.108+** — `theme.toml` already pins `min_version = "0.128"`; safe.
|
||||
- **Font file size** — Inter Regular is ~120 KB; acceptable as a one-time vendored asset, but check if a smaller subset (Latin-only) reduces to ~40 KB. Use `pyftsubset` if so.
|
||||
- **Long names overflow the canvas** — clip text at ~30 chars; document the limit in README.
|
||||
- **Tagline with markdown** — `images.Text` is plain-text only; strip markdown via `plainify` filter before overlay.
|
||||
- **Reusing `og:image` for `twitter:image`** — Twitter's `summary_large_image` requires 2:1 ratio; 1200×630 is close enough (1.9:1) and is the de-facto standard.
|
||||
- **Locale-specific characters** — font must include glyphs for non-Latin names; if user is Vietnamese (per repo author) and font lacks Vietnamese diacritics, image will show tofu. Pick a font with Vietnamese coverage (Inter does).
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
---
|
||||
phase: 3
|
||||
title: "i18n string externalization"
|
||||
status: pending
|
||||
priority: P2
|
||||
effort: "1.5h"
|
||||
dependencies: []
|
||||
---
|
||||
|
||||
# Phase 3: i18n string externalization
|
||||
|
||||
## Overview
|
||||
Move every hardcoded English string in templates to Hugo i18n bundles so non-English sites work end-to-end. Ship `en` (default) + `vi` (smoke test for the maintainer's locale + non-Latin diacritics).
|
||||
|
||||
## Requirements
|
||||
- Functional:
|
||||
- All theme-rendered strings sourced from `i18n/{lang}.toml` via `{{ i18n "key" }}`.
|
||||
- Site `languageCode` (e.g. `vi-VN`) resolves to the matching bundle; falls back to `en` for missing keys.
|
||||
- User-supplied strings (`params.name`, `params.tagline`, `params.bio`, link `title`s, `params.footerText`) remain untouched — those are user content, not theme strings.
|
||||
- Non-functional:
|
||||
- Adding a new language requires one new `.toml` file, no template edits.
|
||||
- Existing English-only sites see no behavior change (en is the default).
|
||||
|
||||
## Architecture
|
||||
- Audit current hardcoded strings in templates:
|
||||
- `bio-card.html`: `aria-label="Links"` (nav landmark).
|
||||
- `theme-toggle-button.html`: button label / aria-label / title (likely "Toggle theme").
|
||||
- `footer.html`: default `© {year} {name}` template (the format itself is i18n-able; `name` stays a param).
|
||||
- `og-image.html` (new in Phase 2): no user-visible strings; skip.
|
||||
- `head.html`: no user-visible strings.
|
||||
- Create `i18n/en.toml` with all extracted keys.
|
||||
- Create `i18n/vi.toml` mirroring `en.toml` with Vietnamese translations (smoke-test diacritics + Hugo's locale resolution).
|
||||
- Replace inline strings with `{{ i18n "key" }}` calls.
|
||||
|
||||
## Related Code Files
|
||||
- Create:
|
||||
- `i18n/en.toml`
|
||||
- `i18n/vi.toml`
|
||||
- Modify:
|
||||
- `layouts/partials/bio-card.html` — replace `aria-label="Links"` with `aria-label="{{ i18n "nav_links_label" }}"`.
|
||||
- `layouts/partials/theme-toggle-button.html` — replace English strings with i18n calls.
|
||||
- `layouts/partials/footer.html` — i18n the default footer template (when `params.footerText` is unset).
|
||||
- `exampleSite/hugo.toml` — keep `languageCode = "en-us"`; add a commented example for `vi-VN`.
|
||||
- Delete: none.
|
||||
|
||||
## Implementation Steps
|
||||
1. Read `theme-toggle-button.html` + `footer.html` to enumerate every English literal.
|
||||
2. Create `i18n/en.toml` with proposed keys:
|
||||
- `nav_links_label = "Links"`
|
||||
- `theme_toggle_label = "Toggle theme"`
|
||||
- `theme_toggle_to_dark = "Switch to dark theme"`
|
||||
- `theme_toggle_to_light = "Switch to light theme"`
|
||||
- `footer_default = "© {{ .year }} {{ .name }}"` — Hugo i18n supports template params via the `dict` 2nd arg.
|
||||
3. Create `i18n/vi.toml` with translated values (e.g. `nav_links_label = "Liên kết"`, `theme_toggle_label = "Đổi giao diện"`).
|
||||
4. Replace template literals with `{{ i18n "key" }}` (or `{{ i18n "key" $ctx }}` where params needed).
|
||||
5. Test: build exampleSite with `languageCode = "en-us"` → strings unchanged. Switch to `languageCode = "vi-VN"` → Vietnamese strings appear.
|
||||
6. Hugo missing-translation behavior: by default Hugo emits `[i18n] translation not found for key 'X'` — verify no warnings during `en` build.
|
||||
|
||||
## Todo List
|
||||
- [ ] Audit all hardcoded English strings in `layouts/partials/`.
|
||||
- [ ] Draft full key list; review for granularity (e.g., separate to_dark / to_light vs single Toggle).
|
||||
- [ ] Write `i18n/en.toml`.
|
||||
- [ ] Write `i18n/vi.toml` (Vietnamese translations).
|
||||
- [ ] Replace all literals in templates with `{{ i18n }}` calls.
|
||||
- [ ] Build exampleSite with `en-us` — verify no missing-translation warnings.
|
||||
- [ ] Build exampleSite with `vi-VN` — verify Vietnamese strings render, including footer template-with-params case.
|
||||
- [ ] Verify diacritics render correctly in HTML.
|
||||
|
||||
## Success Criteria
|
||||
- [ ] No English literals remain in `layouts/partials/*.html` (excluding HTML attribute names + user-content placeholders).
|
||||
- [ ] `i18n/en.toml` and `i18n/vi.toml` ship with the theme.
|
||||
- [ ] Switching `languageCode` in `hugo.toml` flips all theme strings, no template edits needed.
|
||||
- [ ] Adding a new language is a single-file change.
|
||||
|
||||
## Risk Assessment
|
||||
- **Hugo i18n with template params** — `footer_default` uses `{{ .year }}`. Hugo's i18n template params require the right `dict` invocation; verify with the official docs (gohugo.io/content-management/multilingual/#translate-strings-in-templates) before committing.
|
||||
- **vi.toml as smoke test** — if maintainer doesn't speak the target language, machine translation is OK for the smoke test as long as it's labeled. For Vietnamese, the maintainer is a native speaker so this is safe.
|
||||
- **Theme-toggle button has dynamic state** — `to_dark` vs `to_light` switch on current theme. JS-based, so the i18n strings need to be readable by `theme-toggle.js` — pass them as `data-` attributes on the button, not as JS string literals.
|
||||
- **Pluralization not needed** — none of the current strings have count-based variants. Defer Hugo's plural i18n until a real need arises.
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
phase: 4
|
||||
title: "Demo + docs polish"
|
||||
status: pending
|
||||
priority: P2
|
||||
effort: "1.5h"
|
||||
dependencies: [1, 2, 3]
|
||||
---
|
||||
|
||||
# Phase 4: Demo + docs polish
|
||||
|
||||
## Overview
|
||||
Update README, exampleSite, and screenshots to showcase v0.3 additions. Same screenshot-rigor as v0.2 — visual diff is the only thing that catches CSS specificity bugs.
|
||||
|
||||
## Requirements
|
||||
- Functional:
|
||||
- README parameter table includes all new params (`layout`, `ogImage`, `ogImageUrl`).
|
||||
- README has a short section on each new feature (Layout variants, OG images, i18n).
|
||||
- exampleSite demonstrates one of the new layouts (probably `grid`) so the live demo visibly shows the new feature.
|
||||
- Screenshots regenerated showing v0.3 state.
|
||||
- Optional: dedicated `/layouts/` demo page showing all 3 variants side by side, mirroring the existing `/themes/` and `/icons/` galleries.
|
||||
- Non-functional:
|
||||
- README stays under ~250 lines after additions (currently 204) — be terse.
|
||||
- exampleSite remains a one-page bio; gallery pages are content-driven additions.
|
||||
|
||||
## Architecture
|
||||
- Reuse the `/themes/` and `/icons/` gallery pattern: an `exampleSite/content/layouts/_index.md` page rendered by a new `layouts/layouts/single.html` template (Hugo's "layout" page-kind name conflicts will need careful handling — likely call the section `arrangements` or `variants` to avoid colliding with the literal `layouts/` template directory).
|
||||
- **Naming decision:** call the section `variants/` (URL `/variants/`), template `layouts/variants/single.html`.
|
||||
- README diff: add a "## Layout variants" section after "## Color themes"; add an "## i18n" subsection inside Configuration or as its own short section; expand the params table.
|
||||
|
||||
## Related Code Files
|
||||
- Create:
|
||||
- `exampleSite/content/variants/_index.md` — content page for layout gallery.
|
||||
- `layouts/variants/single.html` — renders the gallery (3 mini bio-cards, one per layout variant).
|
||||
- Modify:
|
||||
- `README.md` — params table additions, new feature sections, link to `/variants/` gallery.
|
||||
- `exampleSite/hugo.toml` — set `layout = "grid"` to make the new feature visible on the live demo.
|
||||
- `images/screenshot.png` + `images/tn.png` — regenerate per Hugo themes-site submission requirements.
|
||||
- Delete: none.
|
||||
|
||||
## Implementation Steps
|
||||
1. Update `README.md`:
|
||||
- Add `layout`, `ogImage`, `ogImageUrl` rows to the params table.
|
||||
- Add `## Layout variants` section listing stack/grid/inline with one-line descriptions and a link to `/variants/`.
|
||||
- Add `## i18n` short section: "Theme strings live in `i18n/{lang}.toml`. Set Hugo's `languageCode` and add a matching bundle to translate. Ships with `en` and `vi`."
|
||||
- Add OG image note in the existing Configuration section (one short paragraph).
|
||||
2. Update `exampleSite/hugo.toml`:
|
||||
- Set `layout = "grid"`.
|
||||
- Add commented `ogImageUrl = "..."` example.
|
||||
3. Build the variants gallery:
|
||||
- `exampleSite/content/variants/_index.md` with frontmatter + short intro.
|
||||
- `layouts/variants/single.html` renders 3 mini cards (similar to themes gallery pattern). Each card shows a sample bio rendered with one variant.
|
||||
4. Regenerate screenshots — `exampleSite` running locally, capture at 1280×800 per Hugo themes-site spec; capture both light + dark.
|
||||
5. Validate: build full site, click through `/`, `/themes/`, `/icons/`, `/variants/` — every page renders, no console errors, no missing translations.
|
||||
|
||||
## Todo List
|
||||
- [ ] Add `layout`, `ogImage`, `ogImageUrl` to README params table.
|
||||
- [ ] Add README "Layout variants" + "i18n" sections.
|
||||
- [ ] Update `exampleSite/hugo.toml` to use `layout = "grid"`.
|
||||
- [ ] Create `exampleSite/content/variants/_index.md`.
|
||||
- [ ] Create `layouts/variants/single.html` rendering all 3 variants side by side.
|
||||
- [ ] Regenerate `images/screenshot.png` + `images/tn.png` (light + dark verified).
|
||||
- [ ] Verify all pages: `/`, `/themes/`, `/icons/`, `/variants/`.
|
||||
- [ ] Spot-check OG image preview via Twitter card validator.
|
||||
|
||||
## Success Criteria
|
||||
- [ ] README documents every new v0.3 param with default + description.
|
||||
- [ ] Live demo (after deploy) visibly uses the grid layout.
|
||||
- [ ] `/variants/` page accessible from a link in the bio card or footer.
|
||||
- [ ] Screenshots match the v0.3 demo state.
|
||||
- [ ] No 404s, no missing translations, no console errors.
|
||||
|
||||
## Risk Assessment
|
||||
- **Section name collision** — `layouts/` is Hugo's template directory; using `layouts` as a content section is confusing. Picking `variants/` keeps the section URL clean and avoids any chance of Hugo resolving the section via template autoloading.
|
||||
- **Screenshot drift** — v0.2 already changed CSS palette scoping; another regen now means the v0.2 screenshots become outdated within ~24h. Acceptable since v0.3 is the new baseline; archive v0.2 screenshots in git history only.
|
||||
- **README sprawl** — adding 3 features can easily push README past 300 lines. Discipline: each new section ≤ 15 lines; defer deep dives to inline doc comments or a separate `docs/features.md` if needed.
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
phase: 5
|
||||
title: "v0.3.0 release"
|
||||
status: pending
|
||||
priority: P2
|
||||
effort: "0.5h"
|
||||
dependencies: [4]
|
||||
---
|
||||
|
||||
# Phase 5: v0.3.0 release
|
||||
|
||||
## Overview
|
||||
Cut v0.3.0 — bump CHANGELOG, tag the commit, push, monitor demo deploy, write the journal entry. Same protocol as v0.2.
|
||||
|
||||
## Requirements
|
||||
- Functional:
|
||||
- CHANGELOG `[Unreleased]` block moves to `[0.3.0] — <today>`.
|
||||
- New `[Unreleased]` placeholder added with v0.4 candidates.
|
||||
- Annotated tag `v0.3.0` on the merge commit.
|
||||
- GitHub Pages deploy of the new exampleSite succeeds.
|
||||
- Journal entry under `docs/journals/2026-05-04-v0-3-release.md` (or actual date) following the v0.2 entry's format.
|
||||
- Non-functional:
|
||||
- No breaking changes; all v0.2 sites upgrade with no config edits.
|
||||
|
||||
## Architecture
|
||||
- N/A — release admin only.
|
||||
|
||||
## Related Code Files
|
||||
- Modify:
|
||||
- `CHANGELOG.md`
|
||||
- Create:
|
||||
- `docs/journals/{date}-v0-3-release.md`
|
||||
- Delete: none.
|
||||
|
||||
## Implementation Steps
|
||||
1. Update `CHANGELOG.md`:
|
||||
- Move existing v0.3 placeholder content into a real `## [0.3.0] — <date>` section.
|
||||
- List Added (3 features), Changed (head/twitter:card upgrade, exampleSite layout switch), and any Fixed entries.
|
||||
- Add fresh `## [Unreleased]` block with placeholder bullet for v0.4 candidates (e.g. RSS opt-in, more icons, multi-section bio?).
|
||||
2. Verify build: `cd exampleSite && hugo --themesDir ../.. --gc --minify` — no warnings, no missing translations.
|
||||
3. Commit final changes: `chore(release): v0.3.0 — layout variants, OG images, i18n`.
|
||||
4. Tag: `git tag -a v0.3.0 -m "Release v0.3.0 — layout variants, OG images, i18n"`.
|
||||
5. Push: `git push && git push --tags`.
|
||||
6. Watch GitHub Actions: build + deploy must both go green.
|
||||
7. Verify live demo: <https://tiennm99.github.io/bonsai/> shows new layout, `/variants/`, `/themes/`, `/icons/` all accessible.
|
||||
8. Verify OG image: paste live URL into <https://cards-dev.twitter.com/validator> + <https://www.opengraph.xyz>.
|
||||
9. Write journal entry following v0.2 format (What Happened / Brutal Truth / Technical Details / Root Cause / Lessons / Next Steps).
|
||||
|
||||
## Todo List
|
||||
- [ ] Update `CHANGELOG.md` — move Unreleased to v0.3.0 section.
|
||||
- [ ] Add fresh Unreleased placeholder.
|
||||
- [ ] Final exampleSite build — no warnings.
|
||||
- [ ] Commit + tag + push.
|
||||
- [ ] Verify CI green (build + deploy).
|
||||
- [ ] Verify live demo + new pages.
|
||||
- [ ] Verify OG image via crawler validators.
|
||||
- [ ] Write journal entry.
|
||||
- [ ] (Optional) Submit theme catalog PR update if Hugo themes site re-validation needed.
|
||||
|
||||
## Success Criteria
|
||||
- [ ] `v0.3.0` tag exists on `main`.
|
||||
- [ ] CHANGELOG accurately documents the release.
|
||||
- [ ] Live demo running v0.3 with grid layout visible.
|
||||
- [ ] Twitter / Facebook / LinkedIn previews render the generated OG image.
|
||||
- [ ] No upgrade path issues for v0.2 users (test by reverting exampleSite to v0.2 hugo.toml — site still builds).
|
||||
|
||||
## Risk Assessment
|
||||
- **OG image cache** — Facebook + LinkedIn aggressively cache previews. After release, manually re-scrape via the FB sharing debugger and LinkedIn post inspector to flush.
|
||||
- **Hugo themes catalog** — v0.2 may still be pending review per the v0.2 journal note. Don't open a v0.3 PR there until v0.2 is merged; otherwise the catalog PR collects two pending updates.
|
||||
- **Same-day or next-day release** — v0.2 was same-day after v0.1. v0.3 is bigger (3 features). Realistic target: 1–2 days from start. Don't rush a tag if Phase 4 screenshots aren't fully validated.
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: "v0.3 release — layout variants, OG image generation, i18n"
|
||||
status: completed
|
||||
created: 2026-05-03
|
||||
completed: 2026-05-03
|
||||
shipped_via: "PR #6 (https://github.com/tiennm99/bonsai/pull/6)"
|
||||
journal: "docs/journals/2026-05-03-v0-3-release.md"
|
||||
deferred_to_v0_4: ["OG image auto-generation"]
|
||||
release_admin_pending: ["screenshot regeneration", "v0.3.0 git tag"]
|
||||
---
|
||||
|
||||
# v0.3 Release
|
||||
|
||||
Same cadence as v0.2: small, additive, opt-in. v0.1 / v0.2 sites upgrade with no config edits.
|
||||
|
||||
## Goals
|
||||
- Give users layout choice (stack vs grid vs inline) without losing the minimalist defaults.
|
||||
- Auto-generate Open Graph preview images so shared links look polished without per-site asset work.
|
||||
- Externalize all theme-rendered strings via Hugo i18n so non-English sites work end-to-end.
|
||||
|
||||
## Non-goals
|
||||
- Multi-page support (still single-page bio).
|
||||
- Runtime JS for layout switching.
|
||||
- Translating bio copy itself (user content stays user-owned).
|
||||
|
||||
## Phases
|
||||
|
||||
| # | Title | Status | Notes |
|
||||
|---|-------|--------|-------|
|
||||
| 1 | Layout variants (stack/grid/inline) | ✅ done | New `params.layout`. Pure CSS, scoped under `.bio__links--{variant}`. |
|
||||
| 2 | OG image params (override + twitter card) | ✅ done (scope-reduced) | Auto-generation deferred to v0.4 (binary asset weight conflicted with minimalist principle). |
|
||||
| 3 | i18n string externalization | ✅ done | `i18n/en.toml` + `vi.toml` shipped. Vietnamese smoke-tested. |
|
||||
| 4 | Demo + docs polish | ✅ done (screenshots pending) | README updated, `/variants/` gallery live, demo uses grid. **Screenshots not regenerated yet — user needs to capture on real browser.** |
|
||||
| 5 | v0.3.0 release | ⏸ awaiting user | CHANGELOG ready. Commit + tag + push pending user authorization. |
|
||||
|
||||
## Dependencies
|
||||
- Phase 1–3 are independent (different files). Can be implemented in any order.
|
||||
- Phase 4 depends on 1–3 (needs final params + screenshots).
|
||||
- Phase 5 depends on 4 (needs README + CHANGELOG before tag).
|
||||
|
||||
## Risks (carried from v0.2 lessons)
|
||||
- CSS specificity bugs hide in cascade. Layout variants must be screenshot-tested.
|
||||
- `<script>` / `<style>` template contexts auto-escape — use `safeHTML` / `safeCSS` where needed.
|
||||
- Static assets that should respond to theme must inherit, not hardcode.
|
||||
|
||||
## Files
|
||||
|
||||
```
|
||||
plans/260503-1120-v0-3-release/
|
||||
├── plan.md (this file)
|
||||
├── phase-01-layout-variants.md
|
||||
├── phase-02-og-image-generation.md
|
||||
├── phase-03-i18n-string-externalization.md
|
||||
├── phase-04-demo-docs-polish.md
|
||||
└── phase-05-v0-3-release.md
|
||||
```
|
||||
@@ -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;
|
||||
@@ -113,6 +221,40 @@ body {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
/* === layout variants ===
|
||||
* All variant rules scope under .bio__links--{name} to avoid CSS specificity
|
||||
* collisions with the default .link rules (see v0.2 journal for the lesson).
|
||||
*/
|
||||
.bio__links--grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.bio__links--grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.bio__links--inline {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
.bio__links--inline .link {
|
||||
padding: .75rem;
|
||||
gap: 0;
|
||||
}
|
||||
/* Visually hide the title in inline mode but keep it for screen readers. */
|
||||
.bio__links--inline .link__title {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -168,6 +310,144 @@ 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);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Variants gallery (exampleSite/variants/) — 3 sample bio cards
|
||||
============================================================ */
|
||||
|
||||
.variants-gallery {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 2rem;
|
||||
max-width: 36rem;
|
||||
margin: 2rem auto;
|
||||
}
|
||||
|
||||
.variants-gallery__card {
|
||||
background: var(--bonsai-surface);
|
||||
border: 1px solid var(--bonsai-border);
|
||||
border-radius: var(--bonsai-radius);
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.variants-gallery__name {
|
||||
font-family: var(--bonsai-font-display);
|
||||
font-size: 1.1rem;
|
||||
margin: 0 0 .25rem;
|
||||
color: var(--bonsai-text);
|
||||
}
|
||||
|
||||
.variants-gallery__desc {
|
||||
color: var(--bonsai-muted);
|
||||
font-size: .85rem;
|
||||
margin: 0 0 .5rem;
|
||||
}
|
||||
|
||||
.variants-gallery__code {
|
||||
display: inline-block;
|
||||
margin-bottom: 1rem;
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: .75rem;
|
||||
color: var(--bonsai-muted);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
})();
|
||||
Reference in new issue
Block a user