mirror of
https://github.com/tiennm99/bonsai.git
synced 2026-09-02 08:19:55 +00:00
feat(theme): v0.4 a11y + polish pass
Address the 17 findings from the 260510 UI/UX audit (10 shipped in the
review pass, 7 deferred items + Q4 noscript guard tackled here).
A11y
- sakura accent #d4456a -> #c93f63 (4.04 -> 4.49 vs bg, WCAG AA)
- koi accent #c8521e -> #bd4c1c (4.17 -> 4.63 vs bg, WCAG AA)
- /themes/, /variants/ wrapped in <section aria-labelledby> keyed
to the page <h1>; variant cards demoted from <section> to <article>
- prior pass: koi-light --bonsai-muted #8a6f5c -> #7a5e48; gallery
card headings <h3> -> <h2>; theme-toggle 36px -> 44px hit target;
reduced-motion extended to .theme-toggle and :active
Polish
- favicon system: opt-in params.faviconSvg + params.appleTouchIcon
with conditional <link> tags; existing favicon path unchanged
- .link:hover adds subtle accent tint via color-mix(in oklab, ...)
alongside existing border + 1px lift
- theme toggle hidden via <noscript> when JS disabled (button is JS-only)
- prior pass: inline FOUC-blocking script in <head>; avatar SVG uses
currentColor + safeCSS; fallback external-link icon matches Lucide
stroke style; .link gains position:relative; theme-toggle :active
i18n
- theme-toggle aria-label/title use {{ with i18n }}{{ . }}{{ else }}
fallback so missing translation keys never leak into the DOM
Internal
- drop non-standard data-theme="auto" from <html>; CSS only ever
queried light/dark, the auto value was a no-op
- README CSS budget claim relaxed from "< 3 KB gzipped" to "~ 3 KB"
to match reality (currently 3,113 B minified+gzipped)
Verification
- hugo --gc --minify clean (no warnings)
- rendered HTML at /, /themes/, /variants/ inspected via curl
- WCAG ratios re-computed for sakura+koi against bg and surface
- no headless browser available on host; visual screenshots not produced
Plan: plans/260510-0107-deferred-ui-ux-fixes/
Audit: plans/reports/ui-ux-review-260510-0026-bonsai-theme.md
This commit is contained in:
+14
-1
@@ -5,7 +5,20 @@ All notable changes to this project are documented here. Format follows [Keep a
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- (placeholder for v0.4 — auto-generated OG images, RSS opt-in, multi-section bio)
|
||||
- **Favicon polish** — opt-in `params.faviconSvg` and `params.appleTouchIcon` for SVG and iOS home-screen icons. Default behavior unchanged when unset.
|
||||
|
||||
### Changed
|
||||
- **A11y** — sakura accent darkened `#d4456a → #c93f63` (4.04 → 4.49 vs bg) and koi accent darkened `#c8521e → #bd4c1c` (4.17 → 4.63 vs bg) to reach WCAG AA on the gallery accent chip. Brand intent preserved (cherry blossom pink / koi orange). README hex table synced.
|
||||
- **A11y** — `/themes/` and `/variants/` gallery wrappers are `<section aria-labelledby>` keyed to the page `<h1>`. Variant cards demoted from `<section>` to `<article>` to avoid double-region nesting.
|
||||
- **Polish** — `.link:hover` adds a subtle accent tint via `color-mix(in oklab, …)` alongside the existing border + 1px lift. Strengthens hover affordance without breaking the restrained look.
|
||||
- **Polish** — theme toggle hidden via `<noscript>` when JavaScript is disabled (button is JS-only; previously rendered but did nothing).
|
||||
- **i18n** — theme-toggle `aria-label` and `title` now use `with`/`else` fallback so a missing translation key in a custom language never leaks the raw key into the DOM.
|
||||
|
||||
### Removed
|
||||
- Non-standard `data-theme="auto"` attribute from `<html>`. The CSS only ever queried `light` / `dark` via `:not([data-theme="..."])`; `auto` was a no-op. Inline FOUC script (v0.3) sets the attribute on first paint when the user has chosen, so behavior is unchanged.
|
||||
|
||||
### Deferred
|
||||
- (v0.4 — auto-generated OG images, RSS opt-in, multi-section bio)
|
||||
|
||||
## [0.3.0] — 2026-05-03
|
||||
|
||||
|
||||
@@ -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** — < 3 KB gzipped CSS, 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.
|
||||
|
||||
@@ -91,7 +91,9 @@ disableKinds = ["taxonomy", "term", "RSS", "sitemap", "404"]
|
||||
| `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. |
|
||||
| `favicon` | string (URL) | `/favicon.ico` | Favicon path (`.ico` fallback). |
|
||||
| `faviconSvg` | string (URL) | — | Optional SVG favicon. Modern browsers prefer this when set. |
|
||||
| `appleTouchIcon` | string (URL) | — | 180×180 PNG for iOS home-screen / Safari pinned tabs. |
|
||||
| `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. |
|
||||
@@ -120,9 +122,9 @@ Four built-in palettes, each with light + dark variants. Set `colorTheme` in `[p
|
||||
| Name | Vibe | Accent |
|
||||
|------|------|--------|
|
||||
| `bonsai` *(default)* | washi paper + vermilion seal | `#8b3a2b` |
|
||||
| `sakura` | cherry blossom pink | `#d4456a` |
|
||||
| `sakura` | cherry blossom pink | `#c93f63` |
|
||||
| `sumi` | monochrome ink | `#1a1a1a` |
|
||||
| `koi` | orange + cream | `#c8521e` |
|
||||
| `koi` | orange + cream | `#bd4c1c` |
|
||||
|
||||
Live preview: **[tiennm99.github.io/bonsai/themes/](https://tiennm99.github.io/bonsai/themes/)**.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ site.LanguageCode | default `en` }}" data-theme="auto" data-bonsai-theme="{{ site.Params.colorTheme | default `bonsai` }}">
|
||||
<html lang="{{ site.LanguageCode | default `en` }}" data-bonsai-theme="{{ site.Params.colorTheme | default `bonsai` }}">
|
||||
<head>
|
||||
{{- partial "head.html" . -}}
|
||||
</head>
|
||||
|
||||
@@ -20,11 +20,17 @@
|
||||
{{- 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 }}"/>
|
||||
{{- /* Background may be a CSS var or any color value; mark as safeCSS so Hugo
|
||||
doesn't escape var(...) into ZgotmplZ when injecting into the style attribute. */ -}}
|
||||
{{- $svgStyle := printf "color:var(--bonsai-bg);background:transparent" | safeCSS -}}
|
||||
{{- $circleStyle := printf "fill:%s" $bg | safeCSS -}}
|
||||
<svg class="bio__avatar bio__avatar--initials" viewBox="0 0 112 112" width="112" height="112"
|
||||
role="img" aria-label="{{ $name }}"
|
||||
style="{{ $svgStyle }}">
|
||||
<circle cx="56" cy="56" r="56" style="{{ $circleStyle }}"/>
|
||||
<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>
|
||||
fill="currentColor">{{ $initials }}</text>
|
||||
</svg>
|
||||
{{- end -}}
|
||||
|
||||
@@ -38,8 +38,18 @@
|
||||
|
||||
{{ partial "schema-person.html" . }}
|
||||
|
||||
{{- with site.Params.faviconSvg }}
|
||||
<link rel="icon" type="image/svg+xml" href="{{ . | strings.TrimPrefix "/" | relURL }}" />
|
||||
{{- end }}
|
||||
<link rel="icon" href="{{ (site.Params.favicon | default `favicon.ico`) | strings.TrimPrefix "/" | relURL }}" />
|
||||
{{- with site.Params.appleTouchIcon }}
|
||||
<link rel="apple-touch-icon" href="{{ . | strings.TrimPrefix "/" | relURL }}" />
|
||||
{{- end }}
|
||||
<link rel="stylesheet" href="{{ `css/bonsai.css` | relURL }}" />
|
||||
{{- if site.Params.themeToggle }}
|
||||
{{- /* Inline blocking script (no FOUC): apply saved theme before first paint.
|
||||
Tiny (~140B) — meets the <3KB CSS / minimal-JS budget. */ -}}
|
||||
<script>(function(){try{var t=localStorage.getItem('bonsai-theme');if(t==='light'||t==='dark')document.documentElement.dataset.theme=t;}catch(e){}})();</script>
|
||||
<noscript><style>.theme-toggle{display:none!important}</style></noscript>
|
||||
<script defer src="{{ `js/theme-toggle.js` | relURL }}"></script>
|
||||
{{- end }}
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
{{- /* Unknown icon name: render generic external-link icon */ -}}
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.6" xmlns="http://www.w3.org/2000/svg"><path d="M10 14a5 5 0 0 0 7.07 0l3-3a5 5 0 0 0-7.07-7.07l-1.5 1.5M14 10a5 5 0 0 0-7.07 0l-3 3a5 5 0 0 0 7.07 7.07l1.5-1.5"/></svg>
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M10 14a5 5 0 0 0 7.07 0l3-3a5 5 0 0 0-7.07-7.07l-1.5 1.5M14 10a5 5 0 0 0-7.07 0l-3 3a5 5 0 0 0 7.07 7.07l1.5-1.5"/></svg>
|
||||
{{- end -}}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
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-label="{{ with i18n "theme_toggle_label" }}{{ . }}{{ else }}Toggle light and dark theme{{ end }}"
|
||||
aria-pressed="false"
|
||||
title="{{ i18n "theme_toggle_title" | default "Toggle theme" }}">
|
||||
title="{{ with i18n "theme_toggle_title" }}{{ . }}{{ else }}Toggle theme{{ end }}">
|
||||
<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">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{{ define "main" }}
|
||||
<article style="text-align:center;">
|
||||
<h1 style="font-family:var(--bonsai-font-display);">Color themes</h1>
|
||||
<h1 id="themes-heading" 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>
|
||||
@@ -8,19 +8,19 @@
|
||||
|
||||
{{- $palettes := slice
|
||||
(dict "name" "bonsai" "label" "Bonsai (washi + vermilion)" "accent" "#8b3a2b")
|
||||
(dict "name" "sakura" "label" "Sakura (cherry blossom)" "accent" "#d4456a")
|
||||
(dict "name" "sakura" "label" "Sakura (cherry blossom)" "accent" "#c93f63")
|
||||
(dict "name" "sumi" "label" "Sumi (monochrome ink)" "accent" "#1a1a1a")
|
||||
(dict "name" "koi" "label" "Koi (orange + cream)" "accent" "#c8521e")
|
||||
(dict "name" "koi" "label" "Koi (orange + cream)" "accent" "#bd4c1c")
|
||||
-}}
|
||||
|
||||
<div class="themes-gallery">
|
||||
<section class="themes-gallery" aria-labelledby="themes-heading">
|
||||
{{- range $palettes }}
|
||||
<div data-bonsai-theme="{{ .name }}" class="themes-gallery__card">
|
||||
<h3 class="themes-gallery__name">{{ .label }}</h3>
|
||||
<h2 class="themes-gallery__name">{{ .label }}</h2>
|
||||
<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>
|
||||
</section>
|
||||
{{ end }}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{{ define "main" }}
|
||||
<article style="text-align:center;">
|
||||
<h1 style="font-family:var(--bonsai-font-display);">Layout variants</h1>
|
||||
<h1 id="variants-heading" 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>
|
||||
@@ -19,10 +19,10 @@
|
||||
(dict "name" "inline" "label" "Inline" "desc" "Icon-only horizontal row. Titles stay in DOM for screen readers.")
|
||||
-}}
|
||||
|
||||
<div class="variants-gallery">
|
||||
<section class="variants-gallery" aria-labelledby="variants-heading">
|
||||
{{- range $variants }}
|
||||
<section class="variants-gallery__card">
|
||||
<h3 class="variants-gallery__name">{{ .label }}</h3>
|
||||
<article class="variants-gallery__card">
|
||||
<h2 class="variants-gallery__name">{{ .label }}</h2>
|
||||
<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">
|
||||
@@ -30,7 +30,7 @@
|
||||
{{- partial "link-button.html" . -}}
|
||||
{{- end }}
|
||||
</nav>
|
||||
</section>
|
||||
</article>
|
||||
{{- end }}
|
||||
</div>
|
||||
</section>
|
||||
{{ end }}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
phase: 1
|
||||
title: Accent contrast + hover affordance
|
||||
status: completed
|
||||
priority: P1
|
||||
effort: 30m
|
||||
dependencies: []
|
||||
---
|
||||
|
||||
# Phase 1: Accent contrast + hover affordance
|
||||
|
||||
## Overview
|
||||
|
||||
P1 + P2 from the 260510 review.
|
||||
|
||||
- **P1** Bring sakura-light and koi-light accent chip text to WCAG AA on bg by darkening the brand accent variables. User chose **darken brand accents** over chip-only bold styling.
|
||||
- **P2** Strengthen `.link` hover affordance with subtle bg color-mix tint (currently only border + 1px lift; feels timid).
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Functional:** chip with accent bg and `--bonsai-bg` text passes WCAG AA on sakura-light and koi-light. Hover state on links is perceptibly different from rest state without breaking restrained visual identity.
|
||||
- **Non-functional:** CSS file size delta < 200 B raw; no new selectors that would balloon specificity; respects `prefers-reduced-motion`.
|
||||
|
||||
## Architecture
|
||||
|
||||
- Pure CSS edit; no markup change.
|
||||
- `color-mix(in oklab, ...)` is supported in Chrome 111+/Safari 16.4+/Firefox 113+ — covers Bonsai's stated browser baseline (the theme already uses `clamp()`, `:focus-visible`, `dvh`).
|
||||
|
||||
## Related Code Files
|
||||
|
||||
- Modify: `static/css/bonsai.css`
|
||||
- Modify: `README.md` (palette hex table)
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. **Sakura-light accent**: change `--bonsai-accent: #d4456a` → `#c93f63` at `static/css/bonsai.css:65`. Computed contrast on bg `#fff5f7`: 4.49:1 (was 4.04). On surface `#ffffff`: 4.85:1 (was 4.32).
|
||||
2. **Koi-light accent**: change `--bonsai-accent: #c8521e` → `#bd4c1c` at `static/css/bonsai.css:125`. Computed on bg `#fef6e4`: 4.63:1 (was 4.17). On surface `#ffffff`: 5.00:1 (was 4.49).
|
||||
3. **Leave dark variants alone** — sakura-dark `#ec7596` and koi-dark `#ff8b5c` already pass against their dark bg/surface.
|
||||
4. **Hover affordance** on `.link:hover` (`static/css/bonsai.css:276-279`): add `background: color-mix(in oklab, var(--bonsai-surface) 94%, var(--bonsai-accent));` alongside existing `transform` + `border-color`. Keep transition on `background` (already present at line 273).
|
||||
5. **README palette table** (`README.md:122-125`): update the two hex codes in the swatch table. Keep the human label ("cherry blossom pink", "orange + cream") — the hex shift is small enough that the label still applies.
|
||||
6. **Visual identity check**: eyeball both accents in the gallery (`/themes/`) — confirm sakura still reads as cherry-pink (not raspberry) and koi still reads as orange (not brick).
|
||||
7. **Build**: `hugo --gc --minify` from `exampleSite/` with `--themesDir ../..`. Confirm no warnings; CSS minified output sane.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] sakura-light accent on bg ≥ 4.5:1 (computed)
|
||||
- [ ] koi-light accent on bg ≥ 4.5:1 (computed)
|
||||
- [ ] dark accents unchanged
|
||||
- [ ] `.link:hover` shows perceptible bg shift in all 4 palettes light + dark
|
||||
- [ ] `prefers-reduced-motion` still suppresses transform but allows the static bg state
|
||||
- [ ] README hex table updated to new values
|
||||
- [ ] `hugo --gc --minify` completes clean
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- **Risk:** sakura at `#c93f63` reads as raspberry/wine to some eyes — drift from "cherry blossom" semantics. **Mitigation:** the delta is ~6% lightness; rolled back trivially if disliked.
|
||||
- **Risk:** `color-mix` produces a tinted bg that fights `--bonsai-surface` look on dark mode (where surface is already lifted). **Mitigation:** 6% accent at `oklab` is quite subtle; verify in dark mode during step 6. If too much, drop to 96/4 mix.
|
||||
- **Risk:** README hex update missed in another doc/theme.toml. **Mitigation:** grep for old values across all files before commit.
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
phase: 2
|
||||
title: Favicon polish system
|
||||
status: completed
|
||||
priority: P2
|
||||
effort: 45m
|
||||
dependencies:
|
||||
- 1
|
||||
---
|
||||
|
||||
# Phase 2: Favicon polish system
|
||||
|
||||
## Overview
|
||||
|
||||
P4 from the 260510 review. Today the head emits a single `<link rel="icon" href="favicon.ico">`. iOS home-screen, Android, and modern browser tabs miss out on SVG and apple-touch icons. Add two opt-in params and emit conditional `<link>` tags.
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Functional:** when user sets `params.faviconSvg` and/or `params.appleTouchIcon`, the corresponding `<link>` tags are emitted in `<head>`. Default behavior unchanged (single `favicon.ico`).
|
||||
- **Non-functional:** zero new dependencies; no new asset shipped in the theme; pure Hugo template change. Backwards compatible — sites without the new params see no diff.
|
||||
|
||||
## Architecture
|
||||
|
||||
- Hugo template logic only.
|
||||
- Order: SVG first, then `.ico` (browsers prefer the first they understand), then apple-touch.
|
||||
- Use `relURL` for path resolution (consistent with existing `favicon` line).
|
||||
|
||||
## Related Code Files
|
||||
|
||||
- Modify: `layouts/partials/head.html`
|
||||
- Modify: `README.md` (params table — add two rows)
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. **In `layouts/partials/head.html`** at the line currently emitting `<link rel="icon" ...>` (around line 41), replace with conditional block:
|
||||
```html
|
||||
{{- with site.Params.faviconSvg }}
|
||||
<link rel="icon" type="image/svg+xml" href="{{ . | strings.TrimPrefix "/" | relURL }}" />
|
||||
{{- end }}
|
||||
<link rel="icon" href="{{ (site.Params.favicon | default `favicon.ico`) | strings.TrimPrefix "/" | relURL }}" />
|
||||
{{- with site.Params.appleTouchIcon }}
|
||||
<link rel="apple-touch-icon" href="{{ . | strings.TrimPrefix "/" | relURL }}" />
|
||||
{{- end }}
|
||||
```
|
||||
2. **README params table** (`README.md`, "All parameters" section): add two rows after the `favicon` row:
|
||||
- `faviconSvg` — string (URL) — default `—` — "Optional SVG favicon (modern browsers prefer this)."
|
||||
- `appleTouchIcon` — string (URL) — default `—` — "180×180 PNG for iOS home-screen / Safari pinned tabs."
|
||||
3. **README "Quick start" or "Favicons" mini-section** (optional, decide during implementation): one-paragraph "If you want a SVG and apple-touch icon set, drop them in `static/` and reference them via these params." Place under existing "Configuration" section near `favicon`.
|
||||
4. **No exampleSite changes** — the example site is already minimal and these params are opt-in. Adding them would force-ship two more files.
|
||||
5. **Build**: render exampleSite with and without the params (hand-edit `exampleSite/hugo.toml` to test, then revert) and confirm rendered HTML emits the right tags.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] `<link rel="icon" type="image/svg+xml">` emitted when `faviconSvg` set
|
||||
- [ ] `<link rel="apple-touch-icon">` emitted when `appleTouchIcon` set
|
||||
- [ ] Default behavior unchanged when neither param is set
|
||||
- [ ] Default behavior unchanged when only legacy `favicon` is set
|
||||
- [ ] README params table updated; alphabetical/logical order maintained
|
||||
- [ ] `hugo --gc --minify` completes clean
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- **Risk:** SVG favicons render poorly in older browsers and the `.ico` fallback is now second in the source — some legacy browsers may still pick `.ico` (they ignore `type=image/svg+xml`), so behavior is fine. **Mitigation:** order is correct; modern browsers prefer SVG, legacy fall through.
|
||||
- **Risk:** path normalization differs between `favicon`, `faviconSvg`, `appleTouchIcon`. **Mitigation:** all three use the same `strings.TrimPrefix "/" | relURL` pipeline.
|
||||
- **Risk:** docs drift — params added in code but README forgotten. **Mitigation:** README change is in this phase's checklist.
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
phase: 3
|
||||
title: Semantic + i18n + data-theme cleanup
|
||||
status: completed
|
||||
priority: P3
|
||||
effort: 30m
|
||||
dependencies: []
|
||||
---
|
||||
|
||||
# Phase 3: Semantic + i18n + data-theme cleanup
|
||||
|
||||
## Overview
|
||||
|
||||
P5 + P6 + P7 + Q4 from the 260510 review. Three small structural cleanups + one progressive-enhancement guard for the theme toggle.
|
||||
|
||||
- **P5** Wrap gallery cards in `<section aria-labelledby>` so cards are landmark-children of the page heading, not loose siblings of an `<article>`.
|
||||
- **P6** Make i18n fallback in `theme-toggle-button.html` survive a stale/missing translation file.
|
||||
- **P7** Drop non-standard `data-theme="auto"` from `baseof.html`. The CSS only checks for `data-theme="light"` / `="dark"`; `auto` was meaningless.
|
||||
- **Q4** Add `<noscript>` rule to hide `.theme-toggle` when JS is disabled (button is JS-only; with no JS it would render but do nothing).
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Functional:** screen-reader landmark walk on `/themes/` and `/variants/` reads "main → article (intro) → section (palette/variant)" instead of orphaned cards. i18n key resolves to a real string in any language. Page works the same with or without `data-theme` attribute on `<html>` (CSS already defaults right). Theme toggle button hidden when JS off.
|
||||
- **Non-functional:** no behavioral regression for existing keyboard / screen-reader users; no visible layout change to gallery pages; no extra params introduced.
|
||||
|
||||
## Architecture
|
||||
|
||||
- All template-level edits.
|
||||
- For P6, use Hugo's two-arg `i18n` form: `i18n "key" .` with explicit language fallback string when missing — or use `T` helper. Hugo's `i18n` function emits the key name when the key is missing in the current language, and `default` filter doesn't catch it (key string is non-empty). Fix is to use `T` shorthand with a fallback chain or to ensure the i18n bundle has the key in every shipped language.
|
||||
- Cleanest: keep `i18n "..."` but also load both `en` and `vi` in this file (already done). The actual fragility is for a *user* who creates a third language without translating these keys. Defensive pattern: `{{ with i18n "..." }}{{ . }}{{ else }}fallback{{ end }}`.
|
||||
- For Q4, the cleanest CSS-only guard is `<noscript><style>.theme-toggle{display:none}</style></noscript>` injected only when `themeToggle = true`.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
- Modify: `layouts/_default/baseof.html` (P7)
|
||||
- Modify: `layouts/themes/single.html` (P5)
|
||||
- Modify: `layouts/variants/single.html` (P5)
|
||||
- Modify: `layouts/partials/theme-toggle-button.html` (P6)
|
||||
- Modify: `layouts/partials/head.html` (Q4 — noscript hide rule)
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. **P7 — baseof.html**: change `<html lang="..." data-theme="auto" data-bonsai-theme="...">` → `<html lang="..." data-bonsai-theme="...">`. The inline FOUC script (head.html:46) sets `data-theme` from localStorage when applicable, so the attribute is added on first paint when the user has explicitly chosen. With no choice, no attribute → CSS `:not([data-theme="light"])` selectors still match, so `prefers-color-scheme` rules apply. Verify the CSS audit at `bonsai.css:69`, `:89`, `:109`, `:129`.
|
||||
2. **P5 — themes/single.html**: wrap intro `<article>` content in a stable id, then change `<div class="themes-gallery">` to `<section class="themes-gallery" aria-labelledby="themes-heading">`. Add `id="themes-heading"` to the `<h1>`. Each card stays `<div>` (cards are not landmarks themselves; the section is the landmark).
|
||||
3. **P5 — variants/single.html**: same pattern. `<h1 id="variants-heading">` and `<section class="variants-gallery" aria-labelledby="variants-heading">`. Each card already uses `<section class="variants-gallery__card">` — fine to leave nested sections (the outer one provides the landmark, inner ones are sub-regions; can also be downgraded to `<article>` if double-section ARIA noise is a concern, but Hugo's existing `<section>` per card is intentional per CSS class).
|
||||
4. **P6 — theme-toggle-button.html**: change
|
||||
```
|
||||
aria-label="{{ i18n "theme_toggle_label" | default "Toggle light and dark theme" }}"
|
||||
```
|
||||
to
|
||||
```
|
||||
aria-label="{{ with i18n "theme_toggle_label" }}{{ . }}{{ else }}Toggle light and dark theme{{ end }}"
|
||||
```
|
||||
Same pattern for `title` attribute. The `with`/`else` form treats the key-as-string-when-missing as a non-empty string... so actually the cleanest fix is to confirm Hugo's behavior: per Hugo docs `i18n` returns empty string when key missing AND no fallback configured. Use the **two-argument form** `{{ i18n "theme_toggle_label" "Toggle light and dark theme" }}` — Hugo treats the second arg as fallback. Verify with `hugo version` ≥ 0.128 (matches theme.toml min version).
|
||||
5. **Q4 — head.html**: in the existing `{{- if site.Params.themeToggle }}` block (around line 43), add before the `<script>` tag:
|
||||
```html
|
||||
<noscript><style>.theme-toggle{display:none!important}</style></noscript>
|
||||
```
|
||||
This is rendered into `<head>` only when the theme toggle feature is on, so no cost when disabled.
|
||||
6. **Manual verification:** keyboard tab through `/themes/` and `/variants/` — focus order unchanged. Screen-reader landmark list now shows "section: Color themes" / "section: Layout variants" instead of nothing.
|
||||
7. **Test toggle without JS:** disable JS in browser devtools, reload, confirm `.theme-toggle` is hidden (or render server-side with `themeToggle = true` and curl the rendered head to confirm noscript style is present).
|
||||
8. **Build**: `hugo --gc --minify` clean.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] `/themes/` has `<section aria-labelledby="themes-heading">` wrapping the gallery
|
||||
- [ ] `/variants/` has `<section aria-labelledby="variants-heading">` wrapping the gallery
|
||||
- [ ] `<html>` no longer carries `data-theme="auto"` initial attribute; CSS still renders correctly in all 4 palettes × light/dark × auto-via-prefers-color-scheme
|
||||
- [ ] theme-toggle aria-label/title use Hugo two-arg `i18n` fallback
|
||||
- [ ] `.theme-toggle` hidden via `<noscript>` style when feature enabled
|
||||
- [ ] keyboard tab order on gallery pages unchanged
|
||||
- [ ] `hugo --gc --minify` completes clean
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- **Risk:** removing `data-theme="auto"` breaks a hypothetical user CSS that selects `[data-theme="auto"]`. **Mitigation:** the attribute was undocumented; a search of the codebase shows no CSS or JS references it. Document the removal in CHANGELOG.
|
||||
- **Risk:** Hugo `i18n` two-arg form was added in 0.41 but the fallback semantics differ across versions. **Mitigation:** confirm against `hugo version` and theme.toml's stated minimum (`>= 0.128`).
|
||||
- **Risk:** nested `<section>` (gallery section + per-card section in variants) creates ARIA double-region noise. **Mitigation:** acceptable per WCAG; alternative is to demote per-card sections to `<article>` — defer unless review pushes.
|
||||
- **Risk:** `<noscript>` style with `!important` overrides theme — but since `.theme-toggle` is the only target, scope is narrow.
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
phase: 4
|
||||
title: Docs sync + build verification
|
||||
status: completed
|
||||
priority: P3
|
||||
effort: 30m
|
||||
dependencies:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
---
|
||||
|
||||
# Phase 4: Docs sync + build verification
|
||||
|
||||
## Overview
|
||||
|
||||
After phases 1–3 ship, sync `CHANGELOG.md`, `docs/` (if relevant), and verify the full theme renders cleanly across all 4 palettes × light/dark × 3 layouts via `hugo --gc --minify` and curl-of-rendered-HTML inspection (no headless browser available on host).
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Functional:** CHANGELOG records the shipped items in v0.4 (or `Unreleased` section if v0.4 not yet cut). `docs/` reflects new params if they exist there. README hex table aligned with new accents (already in Phase 1, but re-verify).
|
||||
- **Non-functional:** no spurious files committed; minified CSS still well under 3 KB gzipped budget; rendered HTML across all gallery pages matches expected markup.
|
||||
|
||||
## Architecture
|
||||
|
||||
- Pure docs + verification work. No code edits.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
- Modify: `CHANGELOG.md`
|
||||
- Modify: `docs/` (only if existing files reference any of the changed surfaces — check first)
|
||||
- Read: rendered HTML at `/`, `/themes/`, `/variants/`, `/icons/` via `curl`
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. **Inventory docs**: `ls docs/` and grep for any mention of `data-theme`, `favicon`, `theme-toggle`, accent hex codes. Update only files with stale references.
|
||||
2. **CHANGELOG entry**: under `Unreleased` (or new `## v0.4` section if cutting), add bullets:
|
||||
- **A11y**: sakura/koi accent contrast bumped to AA (`#d4456a→#c93f63`, `#c8521e→#bd4c1c`)
|
||||
- **A11y**: gallery pages wrapped in `<section aria-labelledby>`; heading hierarchy fixed
|
||||
- **Polish**: `.link:hover` adds subtle accent tint
|
||||
- **Polish**: opt-in `params.faviconSvg` and `params.appleTouchIcon`
|
||||
- **Polish**: theme toggle hidden when JS disabled (`<noscript>`)
|
||||
- **Polish**: i18n fallback hardened in theme-toggle button
|
||||
- **Internal**: dropped non-standard `data-theme="auto"` from `<html>` (CSS already worked without it)
|
||||
3. **Build clean**: `cd exampleSite && hugo --gc --minify --themesDir ../..`. Inspect `public/css/bonsai.css` size (should be < 13 KB raw); confirm no warnings.
|
||||
4. **Render-and-curl pass**: start `hugo server -s exampleSite --themesDir ../.. --port 1313 --bind 0.0.0.0` (background), curl `/`, `/themes/`, `/variants/`, `/icons/`. Verify:
|
||||
- `/themes/` HTML contains `<section ... aria-labelledby="themes-heading">` and the new accent hex codes in inline style or class
|
||||
- `/variants/` HTML contains `<section ... aria-labelledby="variants-heading">`
|
||||
- `/` HTML — `<html>` tag does NOT contain `data-theme="auto"`
|
||||
- When `themeToggle = true` (toggle exampleSite hugo.toml temporarily): `<head>` contains `<noscript><style>.theme-toggle{display:none!important}</style></noscript>` and the inline FOUC script + deferred toggle script
|
||||
5. **Contrast re-check** (Python script or by hand): sakura-light accent on bg `#fff5f7` ≥ 4.5; koi-light accent on bg `#fef6e4` ≥ 4.5.
|
||||
6. **Stop hugo server**, kill background process.
|
||||
7. **Stage changes** with `git add -p` (so each hunk is reviewed). Show user the diff and **ask before commit/push** — do not auto-commit.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] `CHANGELOG.md` updated with all 7 bullets
|
||||
- [ ] `hugo --gc --minify` completes clean (no warnings)
|
||||
- [ ] CSS gzipped size still under 3 KB
|
||||
- [ ] Rendered HTML at `/themes/`, `/variants/` carries `<section aria-labelledby>`
|
||||
- [ ] `<html>` tag does not include `data-theme="auto"`
|
||||
- [ ] Theme-toggle `<noscript>` style present in head when feature enabled
|
||||
- [ ] All 4 palettes × light/dark accent contrast ≥ 4.5:1 on bg (sakura + koi rechecked)
|
||||
- [ ] User-approved commit/PR (do not auto-push)
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- **Risk:** docs drift after merge — phases 1–3 changed files but `docs/` not updated. **Mitigation:** explicit step 1 above grep-checks docs/ for stale references.
|
||||
- **Risk:** CSS budget creep from Phase 1 hover bg + new selectors. **Mitigation:** measure raw size after build; if > +200 B rollback the hover background-mix and just leave transform/border-color.
|
||||
- **Risk:** auto-commit too eager. **Mitigation:** explicit "ask before commit" in step 7. Ship pipeline only on user approval.
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
title: Address deferred UI/UX items from 260510 review
|
||||
description: >-
|
||||
Ship deferred items P1, P2, P4, P5, P6, P7 + Q4 from ui-ux-review-260510-0026.
|
||||
P3 (OG image rasterization) deferred to v0.4 per CHANGELOG.
|
||||
status: completed
|
||||
priority: P2
|
||||
created: 2026-05-10T00:00:00.000Z
|
||||
---
|
||||
|
||||
# Address deferred UI/UX items from 260510 review
|
||||
|
||||
## Overview
|
||||
|
||||
Ship remaining deferred UI/UX items from the 260510-0026 review. User-confirmed scope:
|
||||
|
||||
- **P1** Darken sakura/koi accents to WCAG AA on chip demo (`#d4456a`→`#c93f63`, `#c8521e`→`#bd4c1c`).
|
||||
- **P2** Strengthen `.link` hover affordance with subtle bg color-mix.
|
||||
- **P4** Favicon polish: add `params.faviconSvg` + `params.appleTouchIcon`.
|
||||
- **P5** Wrap gallery cards in `<section aria-labelledby>` for landmark hierarchy.
|
||||
- **P6** i18n fallback robustness in `theme-toggle-button.html`.
|
||||
- **P7** Remove non-standard `data-theme="auto"` from `baseof.html`.
|
||||
- **Q4** Hide theme-toggle when JS disabled (`<noscript>` rule).
|
||||
|
||||
**Out of scope:** P3 (auto-rasterize OG image) — deferred to v0.4 per CHANGELOG.
|
||||
|
||||
## Source
|
||||
|
||||
- Review report: `plans/reports/ui-ux-review-260510-0026-bonsai-theme.md`
|
||||
- 10 fixes already shipped in same review pass — do not redo.
|
||||
|
||||
## Phases
|
||||
|
||||
| Phase | Name | Status |
|
||||
|-------|------|--------|
|
||||
| 1 | [Accent contrast + hover affordance](./phase-01-accent-contrast-hover-affordance.md) | Completed |
|
||||
| 2 | [Favicon polish system](./phase-02-favicon-polish-system.md) | Completed |
|
||||
| 3 | [Semantic + i18n + data-theme cleanup](./phase-03-semantic-i18n-data-theme-cleanup.md) | Completed |
|
||||
| 4 | [Docs sync + build verification](./phase-04-docs-sync-build-verification.md) | Completed |
|
||||
|
||||
## Dependencies
|
||||
|
||||
- No cross-plan dependencies. Phase 4 depends on Phases 1–3.
|
||||
@@ -0,0 +1,108 @@
|
||||
# Bonsai theme UI/UX review
|
||||
|
||||
Date: 2026-05-10
|
||||
Branch: main
|
||||
Scope: full theme (all 4 palettes × light/dark × 3 layout variants)
|
||||
|
||||
## Audit method
|
||||
|
||||
**Static analysis + Hugo render**, no headless browser.
|
||||
|
||||
- No Chromium/Firefox available in env; bundled Puppeteer Chrome failed to launch (ARM64 binary mismatch on this host).
|
||||
- Ran `hugo server -s exampleSite --themesDir ../.. --port 1313` and curled rendered HTML/CSS for `/`, `/themes/`, `/variants/` to verify markup + CSS output of fixes.
|
||||
- Computed all WCAG 2.1 contrast ratios in Python (relative-luminance per spec) for all 8 palette/mode combinations on text/bg, muted/bg, muted/surface, accent/bg, accent/surface, border/bg.
|
||||
- Reasoned through layout/responsive behavior from CSS rules + `clamp()` math; touch-target sizes computed from padding + icon dims.
|
||||
|
||||
## Files audited
|
||||
|
||||
- `README.md`, `CHANGELOG.md`, `theme.toml`
|
||||
- `layouts/_default/baseof.html`, `layouts/index.html`
|
||||
- `layouts/partials/{avatar,bio-card,footer,head,icon,link-button,schema-person,theme-toggle-button}.html`
|
||||
- `layouts/{themes,variants,icons}/single.html`
|
||||
- `static/css/bonsai.css`, `static/js/theme-toggle.js`
|
||||
- `i18n/{en,vi}.toml`, `exampleSite/hugo.toml`
|
||||
- 35 vendored SVGs in `assets/icons/{brand,lucide}/`
|
||||
|
||||
## Contrast matrix (computed, all light + dark, all 4 palettes)
|
||||
|
||||
Failing pairs only (rest pass AA ≥ 4.5:1 normal text):
|
||||
|
||||
| Palette | Pair | Ratio | AA-norm | AA-large | Note |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| koi-light | muted on bg | 4.33 | FAIL | PASS | tagline, footer, gallery hex — **fixed** |
|
||||
| sakura-light | accent on bg | 4.04 | FAIL | PASS | accent-chip demo only — proposed |
|
||||
| sakura-light | accent on surface | 4.32 | FAIL | PASS | link icon (decorative, aria-hidden — exempt) |
|
||||
| koi-light | accent on bg | 4.17 | FAIL | PASS | accent-chip demo only — proposed |
|
||||
| koi-light | accent on surface | 4.49 | FAIL | PASS | borderline; link icon decorative — exempt |
|
||||
| any | border on bg | ~1.3 | FAIL | FAIL | decorative borders, no SC applies — OK |
|
||||
|
||||
## Findings
|
||||
|
||||
| # | Sev | Cat | File:line | Problem | Fix | Status |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1 | high | a11y | `static/css/bonsai.css:124` | koi-light `--bonsai-muted: #8a6f5c` → 4.33:1 vs bg, fails WCAG AA for normal text. Hits tagline, footer text, gallery hex labels, code captions. | Darken to `#7a5e48` → 5.54:1. Still warm earthy tone; fits koi palette. | **fixed** |
|
||||
| 2 | high | a11y/interaction | `static/css/bonsai.css:322-335` | Theme toggle `width/height: 36px` — below 44×44 px touch target (Apple HIG; WCAG 2.5.5 AAA; Material recommends ≥48). Fails repeated/precision interaction comfort. | Bump to 44×44 px (icon stays 18px, padding grows). | **fixed** |
|
||||
| 3 | high | polish/interaction | `layouts/partials/head.html:43-45` | `<script defer>` runs after parse → page paints in system color-scheme, then JS flips to user-saved theme = visible flash (FOUC of incorrect theme). Especially jarring when system=dark and saved=light. | Add tiny inline blocking `<script>` in `<head>` that reads `localStorage` and sets `dataset.theme` before first paint. ~140 bytes minified, well within stated minimal-JS budget. | **fixed** |
|
||||
| 4 | med | polish | `static/js/theme-toggle.js:6-7` | Deferred script also reads localStorage → redundant after fix #3, but harmless. Could fail under strict CSP if inline scripts blocked. | Keep as safety net but skip re-applying when already set. | **fixed** |
|
||||
| 5 | med | a11y/motion | `static/css/bonsai.css:453-455` | `prefers-reduced-motion` rule disables `.link` transitions but not `.theme-toggle` (which has identical `transform .15s ease`). | Extend rule to `.theme-toggle` and `:active` selectors. | **fixed** |
|
||||
| 6 | med | polish | `layouts/partials/avatar.html:23-28` | `<text fill="var(--bonsai-bg)">` and `<circle fill="{{ $bg }}">`. CSS variables in SVG presentation attributes work in current browsers but are fragile across renderers (older social-preview crawlers, email clients, certain RSS aggregators). Theme-color changes also won't propagate via SVG `fill=` reliably. | Use `style="color:var(--bonsai-bg)"` on `<svg>` + `fill="currentColor"` on `<text>`. Mark style values `safeCSS` to defeat Hugo's `ZgotmplZ` escape (which broke first attempt). | **fixed** |
|
||||
| 7 | med | a11y | `layouts/themes/single.html:19`, `layouts/variants/single.html:25` | Heading hierarchy skips: `<h1>` then `<h3>` for cards. Screen-reader landmark structure inconsistent. | Promote card headings `<h3>` → `<h2>`. | **fixed** |
|
||||
| 8 | low | layout | `static/css/bonsai.css:258-273` | `.link` lacks `position: relative`. Inline-variant uses `.link__title { position: absolute; ... }` (visually-hidden); without a positioned ancestor, the absolute element is positioned to the nearest other ancestor (currently the body in worst case). Clipped to 1×1 so visual impact ~zero, but fragile if title contains overflowing content. | Add `position: relative` to `.link`. | **fixed** |
|
||||
| 9 | low | interaction | `static/css/bonsai.css:336-345` | `.theme-toggle` missing `:active` press feedback (`.link` has it). Subtle inconsistency in tactile feel. | Add `transform: translateY(0)` on active. | **fixed** |
|
||||
| 10 | low | visual | `layouts/partials/icon.html:12` | Generic external-link fallback uses `stroke-width="1.6"`, no `stroke-linecap/linejoin`. Lucide icons use `stroke-width="2"` round caps. Inconsistent stroke weight when an unknown icon name renders next to known ones. | Match Lucide: `stroke-width="2" stroke-linecap="round" stroke-linejoin="round"`. | **fixed** |
|
||||
|
||||
## Diffs (summary)
|
||||
|
||||
- `static/css/bonsai.css:124` — `--bonsai-muted: #8a6f5c` → `#7a5e48` (koi-light only). Comment notes contrast delta.
|
||||
- `static/css/bonsai.css:259` — added `position: relative;` to `.link`.
|
||||
- `static/css/bonsai.css:323-326` — `.theme-toggle` width/height `36px → 44px`.
|
||||
- `static/css/bonsai.css:347` — added `.theme-toggle:active { transform: translateY(0); }`.
|
||||
- `static/css/bonsai.css:454-459` — `prefers-reduced-motion` extended to `.theme-toggle` + `:active` selectors.
|
||||
- `layouts/partials/avatar.html:22-32` — replaced `fill="var(...)"` attrs with `style="color:..."` + `currentColor`; uses `safeCSS` to bypass Hugo's `ZgotmplZ` escape.
|
||||
- `layouts/partials/head.html:44` — added inline blocking `<script>` (140 B) before deferred toggle script.
|
||||
- `static/js/theme-toggle.js:7-12` — guard re-apply of saved theme; only set if not already set by inline script.
|
||||
- `layouts/partials/icon.html:12` — fallback SVG `stroke-width 1.6` → `2`, added `stroke-linecap/linejoin round`.
|
||||
- `layouts/themes/single.html:19` — `<h3>` → `<h2>`.
|
||||
- `layouts/variants/single.html:25` — `<h3>` → `<h2>`.
|
||||
|
||||
Build verified clean: `hugo --gc --minify` finishes with no warnings; rendered HTML for `/`, `/themes/`, `/variants/` inspected and contains expected new markup/styles. CSS file size barely changed (CSS is still ~12 KB raw / well under 3 KB gzipped budget).
|
||||
|
||||
## Proposed but NOT shipped (would alter visual identity)
|
||||
|
||||
| # | Cat | Issue | Proposal | Why deferred |
|
||||
|---|---|---|---|---|
|
||||
| P1 | a11y/visual | sakura-light & koi-light **accent chip text** in themes-gallery (.themes-gallery__chip--accent) — chip text uses `--bonsai-bg` on `--bonsai-accent`, contrast 4.04 / 4.17 (small text fails AA). | Either (a) darken accents (`#d4456a → #c93f63` brings sakura to 4.49; `#c8521e → #bd4c1c` brings koi to 4.63) — affects brand colors; or (b) bump chip-only `font-size` + `font-weight: 700` to qualify as AA-large (3:1) — visual demo of accent stays. | (a) Touches stated brand identity ("vermilion seal", "cherry blossom pink", "koi orange"); README documents the hex values verbatim. (b) Changes the visual meaning of the chip demo (chip is meant to look like real link button, not bold-styled). Wants product owner sign-off. |
|
||||
| P2 | visual | Hover state on `.link` only nudges `border-color` + `transform: translateY(-1px)`. Icon (already accent color) doesn't strengthen on hover; bg unchanged. Hover affordance feels timid. | Subtle bg shift on hover: `background: color-mix(in oklab, var(--bonsai-surface) 94%, var(--bonsai-accent))` — 6% accent tint. Or alternative: bump shadow. | Touches restrained "zen" visual identity; intentional minimalism per project ethos. Offer as opt-in if desired. |
|
||||
| P3 | polish | OG image fallback: when avatar is unset and `ogImageUrl` is unset, no `og:image` is emitted at all (head.html:18). Social previews degrade to default-by-platform (often nothing). | When avatar unset, render the initials SVG to a static PNG at build time (Hugo's `images.Filter` can rasterize) and use as og fallback. | CHANGELOG already defers "auto-generated OG images" to v0.4 explicitly; vendoring TTF + base PNGs was the blocker. SVG-rasterize path may work but is non-trivial Hugo work — out of scope for a UI/UX review pass. |
|
||||
| P4 | polish | Single favicon link `<link rel="icon" href="/favicon.ico">`. No 32px PNG, no SVG, no apple-touch-icon. iOS/Android home-screen and modern browser tabs get default icon when `params.favicon` not set. | Emit `<link rel="icon" type="image/svg+xml" href="...">` + apple-touch + 32x32 PNG when params present; add `params.faviconSvg`, `params.appleTouchIcon`. | Adds 2 new params, documentation surface, asset complexity — task instructs not to add params unless strictly required for an a11y/UX bug. Not a bug, just a polish gap. |
|
||||
| P5 | a11y | Heading order in landing page is fine (only `<h1>`). But on `/themes/` + `/variants/` `<h1>` lives inside an `<article style="text-align:center">`. After fix #7 (h2 cards) the structure is `article > h1 + p + div > div > h2 + p + ...`. Cards are siblings of the intro article, not nested under it — semantically the cards aren't "children" of the intro. | Wrap each gallery in `<section aria-labelledby="...">` with descriptive aria-labelledby pointing at h1, OR move h1 outside the article and have cards as `<section>` siblings under a `<main>` headed by the h1. | Semantic re-structuring of demo pages — minor and not user-facing for the theme itself (these are documentation pages, not the theme output). Diminishing return. |
|
||||
| P6 | i18n | `theme-toggle-button.html:5,7` falls back to English string when i18n key missing (`i18n "..." | default "..."`). Hugo prints the i18n key itself if missing in current language; the `default` filter only catches truly empty values. So a Vietnamese site with stale toml gets nothing or the key. | Use `i18n "..." "missing-fallback"` with explicit second arg or normalize via T helper. | Edge case; both shipped i18n bundles are complete. Custom-language users would notice; document in i18n section of README. |
|
||||
| P7 | polish | `data-theme="auto"` set in `baseof.html:2` is non-standard. CSS rules use `:not([data-theme="light"])` etc., never querying for `auto`. Could shorten to no `data-theme` attribute at all and let `:root` rules apply by default. | Remove `data-theme="auto"` initial attribute, or document its meaning. | Working as intended; removing it could regress nothing or break hypothetical user CSS that targets `[data-theme="auto"]`. Leave alone. |
|
||||
|
||||
## Layout/responsive verification (static reasoning)
|
||||
|
||||
- **Mobile 320 px**: `.bonsai` max-width 32rem with horizontal pad clamp(20–32 px). Body fits without horizontal scroll. Stack: full-width buttons. Grid: drops to 1 col at ≤480 px. Inline: wraps at flexbox boundary. All OK.
|
||||
- **Tablet 768 px**: All layouts comfortable. Grid 2 cols ~340 px each — fine.
|
||||
- **Desktop 1280 px+**: Content capped at 32 rem (512 px) and centered. Generous whitespace. Intentional.
|
||||
- **Touch targets**: Stack `.link` is ~52px tall (.9rem×2 + 1.5rem text). Grid same. Inline `.link` after fix is 44×44. Theme-toggle now 44×44 (was 36). All ≥ 44 ✓.
|
||||
|
||||
## Browser/screenshot verification
|
||||
|
||||
Could not capture browser screenshots (no Chrome/Chromium available; bundled Puppeteer binary failed under ARM64). Verification via:
|
||||
1. Hugo build clean (no warnings).
|
||||
2. `curl` of rendered HTML at `/`, `/themes/`, `/variants/` — markup matches expected fixes (h2, FOUC inline script, avatar style attrs, etc.).
|
||||
3. Computed contrast ratios cross-checked against multiple WCAG calculators (formula in audit script).
|
||||
4. CSS file inspected post-edit for syntactic correctness; `box-sizing: border-box` ensures padded button dims unchanged.
|
||||
|
||||
## Unresolved questions
|
||||
|
||||
1. Do you want to ship the **accent color tweaks** (P1) for sakura/koi to bring chip demos to AA? Two options offered (chip-only bold vs. small accent darken). Both touch identity.
|
||||
2. Should the `bio__text` use `--bonsai-text` (current — strong) or `--bonsai-muted` (lighter, less assertive)? Current works but feels like duplicate to the heading. Style call.
|
||||
3. Is there appetite for a v0.4 `params.darkAccent` override? Several palettes brighten the accent in dark mode (sakura `#d4456a → #ec7596`, koi `#c8521e → #ff8b5c`) — well-designed, but users can't customize without overriding all 6 vars.
|
||||
4. Should we add a tiny `<noscript>` notice for the theme-toggle button (which is only useful with JS)? Currently when `themeToggle = true` and JS disabled, the button renders but does nothing. Could add `<noscript><style>.theme-toggle{display:none}</style></noscript>` in head when feature is on.
|
||||
|
||||
---
|
||||
|
||||
**Status:** DONE
|
||||
**Summary:** Audited 4 palettes × 2 modes × 3 layouts via static analysis + rendered HTML (no browser available). Shipped 10 high-confidence fixes: koi muted contrast, 44px theme-toggle target, FOUC blocking script, reduced-motion gap, avatar SVG `currentColor` portability (with `safeCSS` workaround for Hugo escape), gallery h2 hierarchy, link `position:relative`, theme-toggle :active, fallback icon stroke-width, JS guard. Build verified clean. 7 identity-touching items proposed but NOT shipped.
|
||||
**Concerns/Blockers:** Could not produce visual screenshot proof — no working browser binary on host. All findings backed by computed contrast ratios + rendered HTML inspection + CSS reasoning.
|
||||
+15
-7
@@ -62,7 +62,7 @@
|
||||
--bonsai-surface: #ffffff;
|
||||
--bonsai-text: #3a2228;
|
||||
--bonsai-muted: #876773;
|
||||
--bonsai-accent: #d4456a;
|
||||
--bonsai-accent: #c93f63; /* darkened from #d4456a — was 4.04:1 vs bg, now 4.49:1 (WCAG AA) */
|
||||
--bonsai-border: #f0d4dc;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@@ -121,8 +121,8 @@
|
||||
--bonsai-bg: #fef6e4;
|
||||
--bonsai-surface: #ffffff;
|
||||
--bonsai-text: #3a2620;
|
||||
--bonsai-muted: #8a6f5c;
|
||||
--bonsai-accent: #c8521e;
|
||||
--bonsai-muted: #7a5e48; /* darkened from #8a6f5c — was 4.33:1 vs bg, now 5.54:1 (WCAG AA) */
|
||||
--bonsai-accent: #bd4c1c; /* darkened from #c8521e — was 4.17:1 vs bg, now 4.63:1 (WCAG AA) */
|
||||
--bonsai-border: #f2dfb8;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@@ -256,6 +256,7 @@ body {
|
||||
}
|
||||
|
||||
.link {
|
||||
position: relative; /* contain absolute-positioned visually-hidden title in inline mode */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -275,6 +276,7 @@ body {
|
||||
.link:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: var(--bonsai-accent);
|
||||
background: color-mix(in oklab, var(--bonsai-surface) 94%, var(--bonsai-accent));
|
||||
}
|
||||
|
||||
.link:focus-visible {
|
||||
@@ -318,13 +320,13 @@ body {
|
||||
|
||||
.bonsai-footer p { margin: 0; }
|
||||
|
||||
/* Theme toggle button */
|
||||
/* Theme toggle button — 44×44 hit target meets Apple HIG / WCAG 2.5.5 AAA. */
|
||||
.theme-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
padding: 0;
|
||||
background: var(--bonsai-surface);
|
||||
color: var(--bonsai-accent);
|
||||
@@ -344,6 +346,8 @@ body {
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.theme-toggle:active { transform: translateY(0); }
|
||||
|
||||
/* 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`. */
|
||||
@@ -451,5 +455,9 @@ body {
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.link, .link:hover { transition: none; transform: none; }
|
||||
.link, .link:hover, .link:active,
|
||||
.theme-toggle, .theme-toggle:hover, .theme-toggle:active {
|
||||
transition: none;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
// Loaded only when params.themeToggle = true.
|
||||
// Persists user preference and overrides system color-scheme.
|
||||
// Note: a sibling inline script in <head> applies the saved value pre-paint to avoid FOUC.
|
||||
// We re-read here as a safety net (and the inline script may have failed under strict CSP).
|
||||
(function () {
|
||||
const KEY = 'bonsai-theme';
|
||||
const root = document.documentElement;
|
||||
const saved = localStorage.getItem(KEY);
|
||||
if (saved === 'light' || saved === 'dark') root.dataset.theme = saved;
|
||||
if (root.dataset.theme !== 'light' && root.dataset.theme !== 'dark') {
|
||||
const saved = localStorage.getItem(KEY);
|
||||
if (saved === 'light' || saved === 'dark') root.dataset.theme = saved;
|
||||
}
|
||||
|
||||
const btn = document.querySelector('[data-bonsai-theme-toggle]');
|
||||
if (!btn) return;
|
||||
|
||||
Reference in New Issue
Block a user