Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63f8948c48 | ||
|
|
c7422e7895 | ||
|
|
865b4083b2 | ||
|
|
d79a1a1e54 | ||
|
|
c05ed1a640 | ||
|
|
2cb2b567e7 | ||
|
|
d8ffbd4132 | ||
|
|
e4fafef83b | ||
|
|
de5231b6ce | ||
|
|
cce620ca93 | ||
|
|
401296a468 | ||
|
|
feeb6a8731 | ||
|
|
d8c6e8a419 | ||
|
|
35dfa8f3dc | ||
|
|
698f39392f | ||
|
|
689bb80edb | ||
|
|
086b675c31 | ||
|
|
8bdcc2369a | ||
|
|
19f6b85316 | ||
|
|
7809c0b4c8 | ||
|
|
949d73c141 | ||
|
|
eff11d99d8 | ||
|
|
7487aa2c36 | ||
|
|
921390c351 | ||
|
|
7468f3afba | ||
|
|
ce08594810 | ||
|
|
6997564aaa | ||
|
|
50469b0747 | ||
|
|
30da421d7d | ||
|
|
179d456eff | ||
|
|
c163f13eaa | ||
|
|
37ed198b2a | ||
|
|
a67bdc5790 | ||
|
|
f2b6737960 | ||
|
|
3c61e89adb | ||
|
|
abdb0ba3bd | ||
|
|
a968b9dafc | ||
|
|
86cdde9cf4 |
No files matched your search
@@ -20,7 +20,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- id: pages
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
[submodule "themes/bonsai"]
|
||||
path = themes/bonsai
|
||||
url = https://github.com/tiennm99/bonsai.git
|
||||
@@ -1,7 +1,7 @@
|
||||
# iammiti99
|
||||
|
||||
Personal link-in-bio page — built with [Hugo](https://gohugo.io) and the
|
||||
[bonsai](https://github.com/tiennm99/bonsai) theme. Self-hosted Linktree
|
||||
[bonsai](themes/bonsai) theme. Self-hosted Linktree
|
||||
alternative for my own use.
|
||||
|
||||
## How it works
|
||||
@@ -14,7 +14,6 @@ Actions builds with Hugo + bonsai → output deploys to GitHub Pages.
|
||||
Requires Hugo Extended ≥ 0.128.
|
||||
|
||||
```bash
|
||||
git submodule update --init --recursive
|
||||
hugo server # http://localhost:1313
|
||||
hugo --gc --minify # build to ./public
|
||||
```
|
||||
@@ -35,7 +34,7 @@ domain — Hugo will copy it through to the deployed site.
|
||||
hugo.yml # site content — edit params here
|
||||
content/_index.md # placeholder so Hugo renders the index
|
||||
static/images/ # avatar etc.
|
||||
themes/bonsai/ # bonsai theme (git submodule)
|
||||
themes/bonsai/ # bonsai theme (in-repo, full history)
|
||||
.github/workflows/deploy.yml
|
||||
public/ # build output (gitignored)
|
||||
```
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: monday
|
||||
open-pull-requests-limit: 5
|
||||
commit-message:
|
||||
prefix: chore
|
||||
include: scope
|
||||
@@ -0,0 +1,40 @@
|
||||
name: build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: build-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Hugo Extended
|
||||
uses: peaceiris/actions-hugo@v3
|
||||
with:
|
||||
hugo-version: '0.154.0'
|
||||
extended: true
|
||||
|
||||
- name: Build exampleSite
|
||||
working-directory: exampleSite
|
||||
run: hugo --gc --minify --themesDir ../.. --printPathWarnings
|
||||
|
||||
- name: Upload built site
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: exampleSite-public
|
||||
path: exampleSite/public
|
||||
retention-days: 7
|
||||
@@ -0,0 +1,57 @@
|
||||
name: deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- id: pages
|
||||
uses: actions/configure-pages@v6
|
||||
|
||||
- name: Setup Hugo Extended
|
||||
uses: peaceiris/actions-hugo@v3
|
||||
with:
|
||||
hugo-version: '0.154.0'
|
||||
extended: true
|
||||
|
||||
- name: Build with production baseURL
|
||||
working-directory: exampleSite
|
||||
run: |
|
||||
hugo \
|
||||
--gc \
|
||||
--minify \
|
||||
--themesDir ../.. \
|
||||
--baseURL "${{ steps.pages.outputs.base_url }}/"
|
||||
|
||||
- name: Upload Pages artifact
|
||||
uses: actions/upload-pages-artifact@v5
|
||||
with:
|
||||
path: exampleSite/public
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: deployment
|
||||
uses: actions/deploy-pages@v5
|
||||
@@ -0,0 +1,19 @@
|
||||
# Hugo build output
|
||||
public/
|
||||
resources/
|
||||
.hugo_build.lock
|
||||
|
||||
# Hugo modules
|
||||
node_modules/
|
||||
|
||||
# OS / editor
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Local env
|
||||
.env
|
||||
.env.local
|
||||
@@ -0,0 +1,94 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows [SemVer](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Deferred to v0.5
|
||||
- **Auto-generated OG images** (#7) — moved again from v0.4 because no candidate path meets the ≤ 30 KB binary budget. Needs measured prototype before committing.
|
||||
- **Multi-section bio** (#9) — schema design call deferred per the issue's own "2026-Q3 candidate" recommendation.
|
||||
|
||||
## [0.4.0] — 2026-05-10
|
||||
|
||||
A11y, polish, and ergonomics release. All additions are strictly opt-in or transparent; v0.3 sites upgrade with no config edits. Closes #8, #10, #11; defers #7 + #9 to v0.5.
|
||||
|
||||
### Added
|
||||
- **Favicon polish** — opt-in `params.faviconSvg` and `params.appleTouchIcon` for SVG and iOS home-screen icons. Default behavior unchanged when unset.
|
||||
- **Demo-only gallery CSS** — `/themes/` and `/variants/` pages load a separate `static/css/gallery.css`; `static/css/bonsai.css` no longer ships gallery selectors to user sites. Saves ~1.9 KB raw / ~200 B gzipped on every real site. New `head_extra` block in `baseof.html` enables per-page stylesheet additions.
|
||||
- **10 new icons** — set grows from 35 to 45. Brand additions: `bandcamp`, `soundcloud`, `spotify`, `figma`, `dribbble`, `stackoverflow`, `matrix`. Utility additions: `bookopen`, `download`, `heart`. Vendored from Simple Icons v13 (CC0) and Lucide v0.460 (ISC) at the same versions; soft ceiling for v0-line is ~50.
|
||||
- **Optional RSS feed** — opt-in `params.rss = true` renders an RSS 2.0 feed of `[[params.links]]` at `/index.xml` and emits `<link rel="alternate">` in `<head>`. Requires removing `RSS` from `disableKinds`. `pubDate` = build time. Off by default; existing sites unaffected.
|
||||
|
||||
### 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 to v0.5
|
||||
- Auto-generated OG images (#7).
|
||||
- Multi-section bio (#9).
|
||||
|
||||
## [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
|
||||
|
||||
First production-ready release. Live demo at <https://tiennm99.github.io/bonsai/>.
|
||||
|
||||
### Added
|
||||
- 35 icons out of the box: 25 brand (Simple Icons CC0) + 10 utility (Lucide ISC), vendored at build time
|
||||
- `data/icons.yaml` manifest mapping public icon names → vendored SVG paths
|
||||
- Data-driven `partials/icon.html` (13 lines) replacing the hand-curated `if/else if` chain
|
||||
- `scripts/sync-icons.sh` — idempotent vendoring script, version-pinned to `simple-icons@13` + `lucide-static@0.460`
|
||||
- Icon gallery page at `/icons/` (rendered from `exampleSite/content/icons/`)
|
||||
- GitHub Actions `build` workflow — `hugo --gc --minify` on every PR and `main` push
|
||||
- GitHub Actions `deploy` workflow — auto-deploys `exampleSite/` to GitHub Pages on `main`
|
||||
- Dependabot config for weekly action updates
|
||||
- `CONTRIBUTING.md` covering dev setup, icon-add workflow, PR guidelines
|
||||
- `NOTICE` file crediting third-party icon sources
|
||||
|
||||
### Changed
|
||||
- README rewritten with badges, live-demo link, configuration table, all-parameters reference
|
||||
- `exampleSite/hugo.toml` adds Mastodon and Bluesky example links
|
||||
- `theme.toml` `demosite` field points at the live GitHub Pages URL
|
||||
|
||||
## [0.0.1] — 2026-05-01
|
||||
|
||||
### Added
|
||||
- Initial scaffold: theme structure, baseof/index/partial layouts, washi-paper light + sumi-ink dark CSS, 8 hand-curated inline SVG icons (github, globe, mail, twitter, linkedin, youtube, instagram, rss), exampleSite, Apache-2.0 LICENSE
|
||||
@@ -0,0 +1,55 @@
|
||||
# Contributing to Bonsai
|
||||
|
||||
Thanks for your interest. Bonsai is intentionally small — every change should make it simpler, faster, or more useful, never bigger for its own sake.
|
||||
|
||||
## Local development
|
||||
|
||||
```bash
|
||||
git clone https://github.com/tiennm99/bonsai.git
|
||||
cd bonsai/exampleSite
|
||||
hugo server --themesDir ../.. --bind 0.0.0.0
|
||||
```
|
||||
|
||||
Open `http://localhost:1313/`. Edits to `layouts/`, `static/`, `data/`, and `assets/` hot-reload.
|
||||
|
||||
Hugo Extended ≥ 0.128 required. CI pins 0.154.0.
|
||||
|
||||
## Adding an icon
|
||||
|
||||
The icon system is data-driven — adding one means three small edits.
|
||||
|
||||
1. **Vendor the SVG.** Edit `scripts/sync-icons.sh`:
|
||||
- For brand/social icons → add the slug to the `BRAND_SLUGS` list (sourced from [Simple Icons](https://simpleicons.org)).
|
||||
- For UI/utility icons → add the slug to the `UI_SLUGS` list (sourced from [Lucide](https://lucide.dev)).
|
||||
2. **Run the script** to fetch and normalise:
|
||||
```bash
|
||||
./scripts/sync-icons.sh
|
||||
```
|
||||
3. **Register the public name** in `data/icons.yaml`:
|
||||
```yaml
|
||||
newicon: { family: brand, slug: newicon }
|
||||
```
|
||||
4. Rebuild and verify the icon renders in the gallery: `cd exampleSite && hugo server --themesDir ../..` → `http://localhost:1313/icons/`
|
||||
5. Add the new name to the appropriate table in `README.md`.
|
||||
|
||||
## Pull requests
|
||||
|
||||
- Branch from `main`. Keep PRs focused — one concern per PR.
|
||||
- Conventional commit prefixes: `feat:`, `fix:`, `docs:`, `chore:`, `ci:`, `refactor:`.
|
||||
- For visual changes, attach a before/after screenshot (the live demo URL helps reviewers).
|
||||
- CI must be green (`build` workflow runs `hugo --gc --minify` against `exampleSite`).
|
||||
|
||||
## Style
|
||||
|
||||
- HTML & CSS: 2-space indent. Keep `partials/` files under 30 lines where possible.
|
||||
- Hugo templates: prefer `{{- ... -}}` to suppress whitespace; use `partials` for reuse.
|
||||
- CSS: BEM-ish naming (`.bio__name`, `.link__icon`); CSS custom properties for theming.
|
||||
- No build steps for end users — vendored assets only. If you reach for `npm install`, reconsider.
|
||||
|
||||
## Reporting issues
|
||||
|
||||
Include: Hugo version (`hugo version`), theme version/commit, minimal reproduction (a snippet of `hugo.toml` is usually enough), expected vs actual.
|
||||
|
||||
## Philosophy
|
||||
|
||||
YAGNI · KISS · DRY. If a feature would be useful for *some* sites, it doesn't belong here unless it's useful for *most* link-in-bio sites. The strength of Bonsai is what it leaves out.
|
||||
@@ -0,0 +1,202 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
Bonsai Hugo Theme — Third-Party Notices
|
||||
========================================
|
||||
|
||||
This theme vendors SVG icons from two open-source projects:
|
||||
|
||||
1. Simple Icons (brand icons)
|
||||
Source: https://simpleicons.org / https://github.com/simple-icons/simple-icons
|
||||
Version: 13.0.0
|
||||
License: CC0 1.0 Universal (Public Domain Dedication)
|
||||
https://creativecommons.org/publicdomain/zero/1.0/
|
||||
Files: assets/icons/brand/*.svg
|
||||
|
||||
2. Lucide (UI icons)
|
||||
Source: https://lucide.dev / https://github.com/lucide-icons/lucide
|
||||
Version: 0.460.0 (lucide-static)
|
||||
License: ISC License
|
||||
Copyright (c) 2020, Lucide Contributors
|
||||
Permission to use, copy, modify, and/or distribute this software
|
||||
for any purpose with or without fee is hereby granted, provided
|
||||
that the above copyright notice and this permission notice appear
|
||||
in all copies.
|
||||
Files: assets/icons/ui/*.svg
|
||||
@@ -0,0 +1,269 @@
|
||||
# Bonsai
|
||||
|
||||
[](https://github.com/tiennm99/bonsai/actions/workflows/build.yml)
|
||||
[](LICENSE)
|
||||
[](https://gohugo.io)
|
||||
|
||||
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/)** · **[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.
|
||||
|
||||
Bonsai treats your bio page the same way: a quiet, well-pruned page that surfaces only what matters — your name, who you are, and where people can find you.
|
||||
|
||||
## Features
|
||||
|
||||
- **Single-page bio** — name, avatar, tagline, links. Nothing else.
|
||||
- **Data-driven links** — defined in `[[params.links]]`; no content files required.
|
||||
- **45 icons out of the box** — 32 brand (GitHub, Mastodon, Bluesky, Spotify, Figma, Matrix…) + 13 utility (mail, globe, rss, heart, download…). 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.
|
||||
- **Accessible** — semantic HTML, focus-visible outlines, `prefers-reduced-motion`.
|
||||
- **Responsive** — mobile-first, looks right at every viewport.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### As a Git submodule (simplest)
|
||||
|
||||
```bash
|
||||
git submodule add https://github.com/tiennm99/bonsai.git themes/bonsai
|
||||
```
|
||||
|
||||
Add to `hugo.toml`:
|
||||
|
||||
```toml
|
||||
theme = "bonsai"
|
||||
```
|
||||
|
||||
### As a Hugo Module
|
||||
|
||||
```bash
|
||||
hugo mod init github.com/<you>/<your-site>
|
||||
hugo mod get github.com/tiennm99/bonsai
|
||||
```
|
||||
|
||||
Add to `hugo.toml`:
|
||||
|
||||
```toml
|
||||
[module]
|
||||
[[module.imports]]
|
||||
path = "github.com/tiennm99/bonsai"
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Minimal `hugo.toml`:
|
||||
|
||||
```toml
|
||||
baseURL = "https://example.com/"
|
||||
title = "Your Name"
|
||||
theme = "bonsai"
|
||||
|
||||
# Single-page bio — disable everything Hugo doesn't need.
|
||||
disableKinds = ["taxonomy", "term", "RSS", "sitemap", "404"]
|
||||
|
||||
[params]
|
||||
name = "Your Name"
|
||||
tagline = "Tending my little corner of the internet"
|
||||
bio = "Short bio. One sentence is plenty."
|
||||
avatar = "/images/avatar.jpg"
|
||||
|
||||
[[params.links]]
|
||||
title = "GitHub"
|
||||
url = "https://github.com/yourname"
|
||||
icon = "github"
|
||||
|
||||
[[params.links]]
|
||||
title = "Email"
|
||||
url = "mailto:you@example.com"
|
||||
icon = "mail"
|
||||
```
|
||||
|
||||
### All parameters
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `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. 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 (`.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. |
|
||||
| `rss` | bool | `false` | Render an RSS 2.0 feed of `[[params.links]]` at `/index.xml` and emit `<link rel="alternate">` in `<head>`. Requires removing `RSS` from `disableKinds`. |
|
||||
| `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. |
|
||||
|
||||
**Each `[[params.links]]` entry:**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `title` | string | yes | Link label. |
|
||||
| `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 | `#c93f63` |
|
||||
| `sumi` | monochrome ink | `#1a1a1a` |
|
||||
| `koi` | orange + cream | `#bd4c1c` |
|
||||
|
||||
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/)**.
|
||||
|
||||
## RSS feed (opt-in)
|
||||
|
||||
Off by default. To enable a feed of your `[[params.links]]` at `/index.xml`:
|
||||
|
||||
```toml
|
||||
# remove "RSS" from disableKinds — Hugo emits it by default but the theme suggests disabling it
|
||||
disableKinds = ["taxonomy", "term", "sitemap", "404"]
|
||||
|
||||
[params]
|
||||
rss = true
|
||||
```
|
||||
|
||||
The theme renders one `<item>` per link. `pubDate` is the build time (links lack intrinsic dates), so the feed updates whenever the site rebuilds — fine for a curated bio, less ideal for high-frequency feeds.
|
||||
|
||||
## 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>
|
||||
<summary>Brand / Social (32)</summary>
|
||||
|
||||
| Name | Source |
|
||||
|------|--------|
|
||||
| `github` | Simple Icons |
|
||||
| `gitlab` | Simple Icons |
|
||||
| `mastodon` | Simple Icons |
|
||||
| `bsky` | Simple Icons |
|
||||
| `x` | Simple Icons |
|
||||
| `threads` | Simple Icons |
|
||||
| `linkedin` | Simple Icons |
|
||||
| `instagram` | Simple Icons |
|
||||
| `facebook` | Simple Icons |
|
||||
| `tiktok` | Simple Icons |
|
||||
| `youtube` | Simple Icons |
|
||||
| `twitch` | Simple Icons |
|
||||
| `discord` | Simple Icons |
|
||||
| `telegram` | Simple Icons |
|
||||
| `signal` | Simple Icons |
|
||||
| `whatsapp` | Simple Icons |
|
||||
| `reddit` | Simple Icons |
|
||||
| `medium` | Simple Icons |
|
||||
| `devto` | Simple Icons |
|
||||
| `substack` | Simple Icons |
|
||||
| `hashnode` | Simple Icons |
|
||||
| `kofi` | Simple Icons |
|
||||
| `patreon` | Simple Icons |
|
||||
| `buymeacoffee` | Simple Icons |
|
||||
| `paypal` | Simple Icons |
|
||||
| `bandcamp` | Simple Icons |
|
||||
| `soundcloud` | Simple Icons |
|
||||
| `spotify` | Simple Icons |
|
||||
| `figma` | Simple Icons |
|
||||
| `dribbble` | Simple Icons |
|
||||
| `stackoverflow` | Simple Icons |
|
||||
| `matrix` | Simple Icons |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>UI / Utility (13)</summary>
|
||||
|
||||
| Name | Source |
|
||||
|------|--------|
|
||||
| `mail` | Lucide |
|
||||
| `globe` | Lucide |
|
||||
| `link` | Lucide |
|
||||
| `rss` | Lucide |
|
||||
| `calendar` | Lucide |
|
||||
| `phone` | Lucide |
|
||||
| `mappin` | Lucide |
|
||||
| `filetext` | Lucide |
|
||||
| `extlink` | Lucide |
|
||||
| `share` | Lucide |
|
||||
| `bookopen` | Lucide |
|
||||
| `download` | Lucide |
|
||||
| `heart` | Lucide |
|
||||
|
||||
</details>
|
||||
|
||||
Icons are vendored at build time — no CDN fetch at runtime. Live gallery: **[tiennm99.github.io/bonsai/icons/](https://tiennm99.github.io/bonsai/icons/)**.
|
||||
|
||||
To refresh or add icons, edit `scripts/sync-icons.sh` and `data/icons.yaml`, then re-run the script. See [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
git clone https://github.com/tiennm99/bonsai.git
|
||||
cd bonsai/exampleSite
|
||||
hugo server --themesDir ../.. --bind 0.0.0.0
|
||||
```
|
||||
|
||||
Build for inspection:
|
||||
|
||||
```bash
|
||||
cd exampleSite && hugo --themesDir ../.. --gc --minify
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
PRs welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for dev setup, the icon-add workflow, and PR guidelines.
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0 © [tiennm99](https://github.com/tiennm99). See [LICENSE](LICENSE) and [NOTICE](NOTICE) for third-party attributions (Simple Icons CC0, Lucide ISC).
|
||||
@@ -0,0 +1,5 @@
|
||||
+++
|
||||
title = '{{ replace .File.ContentBaseName "-" " " | title }}'
|
||||
date = {{ .Date }}
|
||||
draft = true
|
||||
+++
|
||||
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Bandcamp</title><path d="M0 18.75l7.437-13.5H24l-7.438 13.5H0z"/></svg>
|
||||
|
After Width: | Height: | Size: 150 B |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Bluesky</title><path d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 0 1-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8Z"/></svg>
|
||||
|
After Width: | Height: | Size: 662 B |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Buy Me A Coffee</title><path d="M20.216 6.415l-.132-.666c-.119-.598-.388-1.163-1.001-1.379-.197-.069-.42-.098-.57-.241-.152-.143-.196-.366-.231-.572-.065-.378-.125-.756-.192-1.133-.057-.325-.102-.69-.25-.987-.195-.4-.597-.634-.996-.788a5.723 5.723 0 00-.626-.194c-1-.263-2.05-.36-3.077-.416a25.834 25.834 0 00-3.7.062c-.915.083-1.88.184-2.75.5-.318.116-.646.256-.888.501-.297.302-.393.77-.177 1.146.154.267.415.456.692.58.36.162.737.284 1.123.366 1.075.238 2.189.331 3.287.37 1.218.05 2.437.01 3.65-.118.299-.033.598-.073.896-.119.352-.054.578-.513.474-.834-.124-.383-.457-.531-.834-.473-.466.074-.96.108-1.382.146-1.177.08-2.358.082-3.536.006a22.228 22.228 0 01-1.157-.107c-.086-.01-.18-.025-.258-.036-.243-.036-.484-.08-.724-.13-.111-.027-.111-.185 0-.212h.005c.277-.06.557-.108.838-.147h.002c.131-.009.263-.032.394-.048a25.076 25.076 0 013.426-.12c.674.019 1.347.067 2.017.144l.228.031c.267.04.533.088.798.145.392.085.895.113 1.07.542.055.137.08.288.111.431l.319 1.484a.237.237 0 01-.199.284h-.003c-.037.006-.075.01-.112.015a36.704 36.704 0 01-4.743.295 37.059 37.059 0 01-4.699-.304c-.14-.017-.293-.042-.417-.06-.326-.048-.649-.108-.973-.161-.393-.065-.768-.032-1.123.161-.29.16-.527.404-.675.701-.154.316-.199.66-.267 1-.069.34-.176.707-.135 1.056.087.753.613 1.365 1.37 1.502a39.69 39.69 0 0011.343.376.483.483 0 01.535.53l-.071.697-1.018 9.907c-.041.41-.047.832-.125 1.237-.122.637-.553 1.028-1.182 1.171-.577.131-1.165.2-1.756.205-.656.004-1.31-.025-1.966-.022-.699.004-1.556-.06-2.095-.58-.475-.458-.54-1.174-.605-1.793l-.731-7.013-.322-3.094c-.037-.351-.286-.695-.678-.678-.336.015-.718.3-.678.679l.228 2.185.949 9.112c.147 1.344 1.174 2.068 2.446 2.272.742.12 1.503.144 2.257.156.966.016 1.942.053 2.892-.122 1.408-.258 2.465-1.198 2.616-2.657.34-3.332.683-6.663 1.024-9.995l.215-2.087a.484.484 0 01.39-.426c.402-.078.787-.212 1.074-.518.455-.488.546-1.124.385-1.766zm-1.478.772c-.145.137-.363.201-.578.233-2.416.359-4.866.54-7.308.46-1.748-.06-3.477-.254-5.207-.498-.17-.024-.353-.055-.47-.18-.22-.236-.111-.71-.054-.995.052-.26.152-.609.463-.646.484-.057 1.046.148 1.526.22.577.088 1.156.159 1.737.212 2.48.226 5.002.19 7.472-.14.45-.06.899-.13 1.345-.21.399-.072.84-.206 1.08.206.166.281.188.657.162.974a.544.544 0 01-.169.364zm-6.159 3.9c-.862.37-1.84.788-3.109.788a5.884 5.884 0 01-1.569-.217l.877 9.004c.065.78.717 1.38 1.5 1.38 0 0 1.243.065 1.658.065.447 0 1.786-.065 1.786-.065.783 0 1.434-.6 1.499-1.38l.94-9.95a3.996 3.996 0 00-1.322-.238c-.826 0-1.491.284-2.26.613z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>dev.to</title><path d="M7.42 10.05c-.18-.16-.46-.23-.84-.23H6l.02 2.44.04 2.45.56-.02c.41 0 .63-.07.83-.26.24-.24.26-.36.26-2.2 0-1.91-.02-1.96-.29-2.18zM0 4.94v14.12h24V4.94H0zM8.56 15.3c-.44.58-1.06.77-2.53.77H4.71V8.53h1.4c1.67 0 2.16.18 2.6.9.27.43.29.6.32 2.57.05 2.23-.02 2.73-.47 3.3zm5.09-5.47h-2.47v1.77h1.52v1.28l-.72.04-.75.03v1.77l1.22.03 1.2.04v1.28h-1.6c-1.53 0-1.6-.01-1.87-.3l-.3-.28v-3.16c0-3.02.01-3.18.25-3.48.23-.31.25-.31 1.88-.31h1.64v1.3zm4.68 5.45c-.17.43-.64.79-1 .79-.18 0-.45-.15-.67-.39-.32-.32-.45-.63-.82-2.08l-.9-3.39-.45-1.67h.76c.4 0 .75.02.75.05 0 .06 1.16 4.54 1.26 4.83.04.15.32-.7.73-2.3l.66-2.52.74-.04c.4-.02.73 0 .73.04 0 .14-1.67 6.38-1.8 6.68z"/></svg>
|
||||
|
After Width: | Height: | Size: 773 B |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Discord</title><path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Dribbble</title><path d="M12 24C5.385 24 0 18.615 0 12S5.385 0 12 0s12 5.385 12 12-5.385 12-12 12zm10.12-10.358c-.35-.11-3.17-.953-6.384-.438 1.34 3.684 1.887 6.684 1.992 7.308 2.3-1.555 3.936-4.02 4.395-6.87zm-6.115 7.808c-.153-.9-.75-4.032-2.19-7.77l-.066.02c-5.79 2.015-7.86 6.025-8.04 6.4 1.73 1.358 3.92 2.166 6.29 2.166 1.42 0 2.77-.29 4-.814zm-11.62-2.58c.232-.4 3.045-5.055 8.332-6.765.135-.045.27-.084.405-.12-.26-.585-.54-1.167-.832-1.74C7.17 11.775 2.206 11.71 1.756 11.7l-.004.312c0 2.633.998 5.037 2.634 6.855zm-2.42-8.955c.46.008 4.683.026 9.477-1.248-1.698-3.018-3.53-5.558-3.8-5.928-2.868 1.35-5.01 3.99-5.676 7.17zM9.6 2.052c.282.38 2.145 2.914 3.822 6 3.645-1.365 5.19-3.44 5.373-3.702-1.81-1.61-4.19-2.586-6.795-2.586-.825 0-1.63.1-2.4.285zm10.335 3.483c-.218.29-1.935 2.493-5.724 4.04.24.49.47.985.68 1.486.08.18.15.36.22.53 3.41-.43 6.8.26 7.14.33-.02-2.42-.88-4.64-2.31-6.38z"/></svg>
|
||||
|
After Width: | Height: | Size: 985 B |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Facebook</title><path d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"/></svg>
|
||||
|
After Width: | Height: | Size: 543 B |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Figma</title><path d="M15.852 8.981h-4.588V0h4.588c2.476 0 4.49 2.014 4.49 4.49s-2.014 4.491-4.49 4.491zM12.735 7.51h3.117c1.665 0 3.019-1.355 3.019-3.019s-1.355-3.019-3.019-3.019h-3.117V7.51zm0 1.471H8.148c-2.476 0-4.49-2.014-4.49-4.49S5.672 0 8.148 0h4.588v8.981zm-4.587-7.51c-1.665 0-3.019 1.355-3.019 3.019s1.354 3.02 3.019 3.02h3.117V1.471H8.148zm4.587 15.019H8.148c-2.476 0-4.49-2.014-4.49-4.49s2.014-4.49 4.49-4.49h4.588v8.98zM8.148 8.981c-1.665 0-3.019 1.355-3.019 3.019s1.355 3.019 3.019 3.019h3.117V8.981H8.148zM8.172 24c-2.489 0-4.515-2.014-4.515-4.49s2.014-4.49 4.49-4.49h4.588v4.441c0 2.503-2.047 4.539-4.563 4.539zm-.024-7.51a3.023 3.023 0 0 0-3.019 3.019c0 1.665 1.365 3.019 3.044 3.019 1.705 0 3.093-1.376 3.093-3.068v-2.97H8.148zm7.704 0h-.098c-2.476 0-4.49-2.014-4.49-4.49s2.014-4.49 4.49-4.49h.098c2.476 0 4.49 2.014 4.49 4.49s-2.014 4.49-4.49 4.49zm-.097-7.509c-1.665 0-3.019 1.355-3.019 3.019s1.355 3.019 3.019 3.019h.098c1.665 0 3.019-1.355 3.019-3.019s-1.355-3.019-3.019-3.019h-.098z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
|
||||
|
After Width: | Height: | Size: 823 B |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitLab</title><path d="m23.6004 9.5927-.0337-.0862L20.3.9814a.851.851 0 0 0-.3362-.405.8748.8748 0 0 0-.9997.0539.8748.8748 0 0 0-.29.4399l-2.2055 6.748H7.5375l-2.2057-6.748a.8573.8573 0 0 0-.29-.4412.8748.8748 0 0 0-.9997-.0537.8585.8585 0 0 0-.3362.4049L.4332 9.5015l-.0325.0862a6.0657 6.0657 0 0 0 2.0119 7.0105l.0113.0087.03.0213 4.976 3.7264 2.462 1.8633 1.4995 1.1321a1.0085 1.0085 0 0 0 1.2197 0l1.4995-1.1321 2.4619-1.8633 5.006-3.7489.0125-.01a6.0682 6.0682 0 0 0 2.0094-7.003z"/></svg>
|
||||
|
After Width: | Height: | Size: 574 B |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Hashnode</title><path d="M22.351 8.019l-6.37-6.37a5.63 5.63 0 0 0-7.962 0l-6.37 6.37a5.63 5.63 0 0 0 0 7.962l6.37 6.37a5.63 5.63 0 0 0 7.962 0l6.37-6.37a5.63 5.63 0 0 0 0-7.962zM12 15.953a3.953 3.953 0 1 1 0-7.906 3.953 3.953 0 0 1 0 7.906z"/></svg>
|
||||
|
After Width: | Height: | Size: 328 B |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Instagram</title><path d="M7.0301.084c-1.2768.0602-2.1487.264-2.911.5634-.7888.3075-1.4575.72-2.1228 1.3877-.6652.6677-1.075 1.3368-1.3802 2.127-.2954.7638-.4956 1.6365-.552 2.914-.0564 1.2775-.0689 1.6882-.0626 4.947.0062 3.2586.0206 3.6671.0825 4.9473.061 1.2765.264 2.1482.5635 2.9107.308.7889.72 1.4573 1.388 2.1228.6679.6655 1.3365 1.0743 2.1285 1.38.7632.295 1.6361.4961 2.9134.552 1.2773.056 1.6884.069 4.9462.0627 3.2578-.0062 3.668-.0207 4.9478-.0814 1.28-.0607 2.147-.2652 2.9098-.5633.7889-.3086 1.4578-.72 2.1228-1.3881.665-.6682 1.0745-1.3378 1.3795-2.1284.2957-.7632.4966-1.636.552-2.9124.056-1.2809.0692-1.6898.063-4.948-.0063-3.2583-.021-3.6668-.0817-4.9465-.0607-1.2797-.264-2.1487-.5633-2.9117-.3084-.7889-.72-1.4568-1.3876-2.1228C21.2982 1.33 20.628.9208 19.8378.6165 19.074.321 18.2017.1197 16.9244.0645 15.6471.0093 15.236-.005 11.977.0014 8.718.0076 8.31.0215 7.0301.0839m.1402 21.6932c-1.17-.0509-1.8053-.2453-2.2287-.408-.5606-.216-.96-.4771-1.3819-.895-.422-.4178-.6811-.8186-.9-1.378-.1644-.4234-.3624-1.058-.4171-2.228-.0595-1.2645-.072-1.6442-.079-4.848-.007-3.2037.0053-3.583.0607-4.848.05-1.169.2456-1.805.408-2.2282.216-.5613.4762-.96.895-1.3816.4188-.4217.8184-.6814 1.3783-.9003.423-.1651 1.0575-.3614 2.227-.4171 1.2655-.06 1.6447-.072 4.848-.079 3.2033-.007 3.5835.005 4.8495.0608 1.169.0508 1.8053.2445 2.228.408.5608.216.96.4754 1.3816.895.4217.4194.6816.8176.9005 1.3787.1653.4217.3617 1.056.4169 2.2263.0602 1.2655.0739 1.645.0796 4.848.0058 3.203-.0055 3.5834-.061 4.848-.051 1.17-.245 1.8055-.408 2.2294-.216.5604-.4763.96-.8954 1.3814-.419.4215-.8181.6811-1.3783.9-.4224.1649-1.0577.3617-2.2262.4174-1.2656.0595-1.6448.072-4.8493.079-3.2045.007-3.5825-.006-4.848-.0608M16.953 5.5864A1.44 1.44 0 1 0 18.39 4.144a1.44 1.44 0 0 0-1.437 1.4424M5.8385 12.012c.0067 3.4032 2.7706 6.1557 6.173 6.1493 3.4026-.0065 6.157-2.7701 6.1506-6.1733-.0065-3.4032-2.771-6.1565-6.174-6.1498-3.403.0067-6.156 2.771-6.1496 6.1738M8 12.0077a4 4 0 1 1 4.008 3.9921A3.9996 3.9996 0 0 1 8 12.0077"/></svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Ko-fi</title><path d="M23.881 8.948c-.773-4.085-4.859-4.593-4.859-4.593H.723c-.604 0-.679.798-.679.798s-.082 7.324-.022 11.822c.164 2.424 2.586 2.672 2.586 2.672s8.267-.023 11.966-.049c2.438-.426 2.683-2.566 2.658-3.734 4.352.24 7.422-2.831 6.649-6.916zm-11.062 3.511c-1.246 1.453-4.011 3.976-4.011 3.976s-.121.119-.31.023c-.076-.057-.108-.09-.108-.09-.443-.441-3.368-3.049-4.034-3.954-.709-.965-1.041-2.7-.091-3.71.951-1.01 3.005-1.086 4.363.407 0 0 1.565-1.782 3.468-.963 1.904.82 1.832 3.011.723 4.311zm6.173.478c-.928.116-1.682.028-1.682.028V7.284h1.77s1.971.551 1.971 2.638c0 1.913-.985 2.667-2.059 3.015z"/></svg>
|
||||
|
After Width: | Height: | Size: 698 B |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>LinkedIn</title><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>
|
||||
|
After Width: | Height: | Size: 611 B |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Mastodon</title><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Matrix</title><path d="M.632.55v22.9H2.28V24H0V0h2.28v.55zm7.043 7.26v1.157h.033c.309-.443.683-.784 1.117-1.024.433-.245.936-.365 1.5-.365.54 0 1.033.107 1.481.314.448.208.785.582 1.02 1.108.254-.374.6-.706 1.034-.992.434-.287.95-.43 1.546-.43.453 0 .872.056 1.26.167.388.11.716.286.993.53.276.245.489.559.646.951.152.392.23.863.23 1.417v5.728h-2.349V11.52c0-.286-.01-.559-.032-.812a1.755 1.755 0 0 0-.18-.66 1.106 1.106 0 0 0-.438-.448c-.194-.11-.457-.166-.785-.166-.332 0-.6.064-.803.189a1.38 1.38 0 0 0-.48.499 1.946 1.946 0 0 0-.231.696 5.56 5.56 0 0 0-.06.785v4.768h-2.35v-4.8c0-.254-.004-.503-.018-.752a2.074 2.074 0 0 0-.143-.688 1.052 1.052 0 0 0-.415-.503c-.194-.125-.476-.19-.854-.19-.111 0-.259.024-.439.074-.18.051-.36.143-.53.282-.171.138-.319.337-.439.595-.12.259-.18.6-.18 1.02v4.966H5.46V7.81zm15.693 15.64V.55H21.72V0H24v24h-2.28v-.55z"/></svg>
|
||||
|
After Width: | Height: | Size: 940 B |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Medium</title><path d="M13.54 12a6.8 6.8 0 01-6.77 6.82A6.8 6.8 0 010 12a6.8 6.8 0 016.77-6.82A6.8 6.8 0 0113.54 12zM20.96 12c0 3.54-1.51 6.42-3.38 6.42-1.87 0-3.39-2.88-3.39-6.42s1.52-6.42 3.39-6.42 3.38 2.88 3.38 6.42M24 12c0 3.17-.53 5.75-1.19 5.75-.66 0-1.19-2.58-1.19-5.75s.53-5.75 1.19-5.75C23.47 6.25 24 8.83 24 12z"/></svg>
|
||||
|
After Width: | Height: | Size: 410 B |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Patreon</title><path d="M22.957 7.21c-.004-3.064-2.391-5.576-5.191-6.482-3.478-1.125-8.064-.962-11.384.604C2.357 3.231 1.093 7.391 1.046 11.54c-.039 3.411.302 12.396 5.369 12.46 3.765.047 4.326-4.804 6.068-7.141 1.24-1.662 2.836-2.132 4.801-2.618 3.376-.836 5.678-3.501 5.673-7.031Z"/></svg>
|
||||
|
After Width: | Height: | Size: 370 B |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>PayPal</title><path d="M7.016 19.198h-4.2a.562.562 0 0 1-.555-.65L5.093.584A.692.692 0 0 1 5.776 0h7.222c3.417 0 5.904 2.488 5.846 5.5-.006.25-.027.5-.066.747A6.794 6.794 0 0 1 12.071 12H8.743a.69.69 0 0 0-.682.583l-.325 2.056-.013.083-.692 4.39-.015.087zM19.79 6.142c-.01.087-.01.175-.023.261a7.76 7.76 0 0 1-7.695 6.598H9.007l-.283 1.795-.013.083-.692 4.39-.134.843-.014.088H6.86l-.497 3.15a.562.562 0 0 0 .555.65h3.612c.34 0 .63-.249.683-.585l.952-6.031a.692.692 0 0 1 .683-.584h2.126a6.793 6.793 0 0 0 6.707-5.752c.306-1.95-.466-3.744-1.89-4.906z"/></svg>
|
||||
|
After Width: | Height: | Size: 638 B |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Reddit</title><path d="M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286C.775 23.225 1.097 24 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0Zm4.388 3.199c1.104 0 1.999.895 1.999 1.999 0 1.105-.895 2-1.999 2-.946 0-1.739-.657-1.947-1.539v.002c-1.147.162-2.032 1.15-2.032 2.341v.007c1.776.067 3.4.567 4.686 1.363.473-.363 1.064-.58 1.707-.58 1.547 0 2.802 1.254 2.802 2.802 0 1.117-.655 2.081-1.601 2.531-.088 3.256-3.637 5.876-7.997 5.876-4.361 0-7.905-2.617-7.998-5.87-.954-.447-1.614-1.415-1.614-2.538 0-1.548 1.255-2.802 2.803-2.802.645 0 1.239.218 1.712.585 1.275-.79 2.881-1.291 4.64-1.365v-.01c0-1.663 1.263-3.034 2.88-3.207.188-.911.993-1.595 1.959-1.595Zm-8.085 8.376c-.784 0-1.459.78-1.506 1.797-.047 1.016.64 1.429 1.426 1.429.786 0 1.371-.369 1.418-1.385.047-1.017-.553-1.841-1.338-1.841Zm7.406 0c-.786 0-1.385.824-1.338 1.841.047 1.017.634 1.385 1.418 1.385.785 0 1.473-.413 1.426-1.429-.046-1.017-.721-1.797-1.506-1.797Zm-3.703 4.013c-.974 0-1.907.048-2.77.135-.147.015-.241.168-.183.305.483 1.154 1.622 1.964 2.953 1.964 1.33 0 2.47-.81 2.953-1.964.057-.137-.037-.29-.184-.305-.863-.087-1.795-.135-2.769-.135Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Signal</title><path d="m9.12.35.27 1.09a10.845 10.845 0 0 0-3.015 1.248l-.578-.964A11.955 11.955 0 0 1 9.12.35zm5.76 0-.27 1.09a10.845 10.845 0 0 1 3.015 1.248l.581-.964A11.955 11.955 0 0 0 14.88.35zM1.725 5.797A11.955 11.955 0 0 0 .351 9.119l1.09.27A10.845 10.845 0 0 1 2.69 6.374zm-.6 6.202a10.856 10.856 0 0 1 .122-1.63l-1.112-.168a12.043 12.043 0 0 0 0 3.596l1.112-.169A10.856 10.856 0 0 1 1.125 12zm17.078 10.275-.578-.964a10.845 10.845 0 0 1-3.011 1.247l.27 1.091a11.955 11.955 0 0 0 3.319-1.374zM22.875 12a10.856 10.856 0 0 1-.122 1.63l1.112.168a12.043 12.043 0 0 0 0-3.596l-1.112.169a10.856 10.856 0 0 1 .122 1.63zm.774 2.88-1.09-.27a10.845 10.845 0 0 1-1.248 3.015l.964.581a11.955 11.955 0 0 0 1.374-3.326zm-10.02 7.875a10.952 10.952 0 0 1-3.258 0l-.17 1.112a12.043 12.043 0 0 0 3.597 0zm7.125-4.303a10.914 10.914 0 0 1-2.304 2.302l.668.906a12.019 12.019 0 0 0 2.542-2.535zM18.45 3.245a10.914 10.914 0 0 1 2.304 2.304l.906-.675a12.019 12.019 0 0 0-2.535-2.535zM3.246 5.549A10.914 10.914 0 0 1 5.55 3.245l-.675-.906A12.019 12.019 0 0 0 2.34 4.874zm19.029.248-.964.577a10.845 10.845 0 0 1 1.247 3.011l1.091-.27a11.955 11.955 0 0 0-1.374-3.318zM10.371 1.246a10.952 10.952 0 0 1 3.258 0L13.8.134a12.043 12.043 0 0 0-3.597 0zM3.823 21.957 1.5 22.5l.542-2.323-1.095-.257-.542 2.323a1.125 1.125 0 0 0 1.352 1.352l2.321-.532zm-2.642-3.041 1.095.255.375-1.61a10.828 10.828 0 0 1-1.21-2.952l-1.09.27a11.91 11.91 0 0 0 1.106 2.852zm5.25 2.437-1.61.375.255 1.095 1.185-.275a11.91 11.91 0 0 0 2.851 1.106l.27-1.091a10.828 10.828 0 0 1-2.943-1.217zM12 2.25a9.75 9.75 0 0 0-8.25 14.938l-.938 4 4-.938A9.75 9.75 0 1 0 12 2.25z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>SoundCloud</title><path d="M23.999 14.165c-.052 1.796-1.612 3.169-3.4 3.169h-8.18a.68.68 0 0 1-.675-.683V7.862a.747.747 0 0 1 .452-.724s.75-.513 2.333-.513a5.364 5.364 0 0 1 2.763.755 5.433 5.433 0 0 1 2.57 3.54c.282-.08.574-.121.868-.12.884 0 1.73.358 2.347.992s.948 1.49.922 2.373ZM10.721 8.421c.247 2.98.427 5.697 0 8.672a.264.264 0 0 1-.53 0c-.395-2.946-.22-5.718 0-8.672a.264.264 0 0 1 .53 0ZM9.072 9.448c.285 2.659.37 4.986-.006 7.655a.277.277 0 0 1-.55 0c-.331-2.63-.256-5.02 0-7.655a.277.277 0 0 1 .556 0Zm-1.663-.257c.27 2.726.39 5.171 0 7.904a.266.266 0 0 1-.532 0c-.38-2.69-.257-5.21 0-7.904a.266.266 0 0 1 .532 0Zm-1.647.77a26.108 26.108 0 0 1-.008 7.147.272.272 0 0 1-.542 0 27.955 27.955 0 0 1 0-7.147.275.275 0 0 1 .55 0Zm-1.67 1.769c.421 1.865.228 3.5-.029 5.388a.257.257 0 0 1-.514 0c-.21-1.858-.398-3.549 0-5.389a.272.272 0 0 1 .543 0Zm-1.655-.273c.388 1.897.26 3.508-.01 5.412-.026.28-.514.283-.54 0-.244-1.878-.347-3.54-.01-5.412a.283.283 0 0 1 .56 0Zm-1.668.911c.4 1.268.257 2.292-.026 3.572a.257.257 0 0 1-.514 0c-.241-1.262-.354-2.312-.023-3.572a.283.283 0 0 1 .563 0Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Spotify</title><path d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.181-1.38-.721-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3z"/></svg>
|
||||
|
After Width: | Height: | Size: 698 B |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Stack Overflow</title><path d="M15.725 0l-1.72 1.277 6.39 8.588 1.716-1.277L15.725 0zm-3.94 3.418l-1.369 1.644 8.225 6.85 1.369-1.644-8.225-6.85zm-3.15 4.465l-.905 1.94 9.702 4.517.904-1.94-9.701-4.517zm-1.85 4.86l-.44 2.093 10.473 2.201.44-2.092-10.473-2.203zM1.89 15.47V24h19.19v-8.53h-2.133v6.397H4.021v-6.396H1.89zm4.265 2.133v2.13h10.66v-2.13H6.154Z"/></svg>
|
||||
|
After Width: | Height: | Size: 442 B |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Substack</title><path d="M22.539 8.242H1.46V5.406h21.08v2.836zM1.46 10.812V24L12 18.11 22.54 24V10.812H1.46zM22.54 0H1.46v2.836h21.08V0z"/></svg>
|
||||
|
After Width: | Height: | Size: 224 B |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Telegram</title><path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/></svg>
|
||||
|
After Width: | Height: | Size: 743 B |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Threads</title><path d="M12.186 24h-.007c-3.581-.024-6.334-1.205-8.184-3.509C2.35 18.44 1.5 15.586 1.472 12.01v-.017c.03-3.579.879-6.43 2.525-8.482C5.845 1.205 8.6.024 12.18 0h.014c2.746.02 5.043.725 6.826 2.098 1.677 1.29 2.858 3.13 3.509 5.467l-2.04.569c-1.104-3.96-3.898-5.984-8.304-6.015-2.91.022-5.11.936-6.54 2.717C4.307 6.504 3.616 8.914 3.589 12c.027 3.086.718 5.496 2.057 7.164 1.43 1.783 3.631 2.698 6.54 2.717 2.623-.02 4.358-.631 5.8-2.045 1.647-1.613 1.618-3.593 1.09-4.798-.31-.71-.873-1.3-1.634-1.75-.192 1.352-.622 2.446-1.284 3.272-.886 1.102-2.14 1.704-3.73 1.79-1.202.065-2.361-.218-3.259-.801-1.063-.689-1.685-1.74-1.752-2.964-.065-1.19.408-2.285 1.33-3.082.88-.76 2.119-1.207 3.583-1.291a13.853 13.853 0 0 1 3.02.142c-.126-.742-.375-1.332-.75-1.757-.513-.586-1.308-.883-2.359-.89h-.029c-.844 0-1.992.232-2.721 1.32L7.734 7.847c.98-1.454 2.568-2.256 4.478-2.256h.044c3.194.02 5.097 1.975 5.287 5.388.108.046.216.094.321.142 1.49.7 2.58 1.761 3.154 3.07.797 1.82.871 4.79-1.548 7.158-1.85 1.81-4.094 2.628-7.277 2.65Zm1.003-11.69c-.242 0-.487.007-.739.021-1.836.103-2.98.946-2.916 2.143.067 1.256 1.452 1.839 2.784 1.767 1.224-.065 2.818-.543 3.086-3.71a10.5 10.5 0 0 0-2.215-.221z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>TikTok</title><path d="M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z"/></svg>
|
||||
|
After Width: | Height: | Size: 708 B |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Twitch</title><path d="M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714Z"/></svg>
|
||||
|
After Width: | Height: | Size: 293 B |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>WhatsApp</title><path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>X</title><path d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"/></svg>
|
||||
|
After Width: | Height: | Size: 253 B |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>YouTube</title><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>
|
||||
|
After Width: | Height: | Size: 460 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M12 7v14" />
|
||||
<path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 380 B |
@@ -0,0 +1,17 @@
|
||||
<svg
|
||||
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M8 2v4" />
|
||||
<path d="M16 2v4" />
|
||||
<rect width="18" height="18" x="3" y="4" rx="2" />
|
||||
<path d="M3 10h18" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 332 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="7 10 12 15 17 10" />
|
||||
<line x1="12" x2="12" y1="15" y2="3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 350 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M15 3h6v6" />
|
||||
<path d="M10 14 21 3" />
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 334 B |
@@ -0,0 +1,18 @@
|
||||
<svg
|
||||
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" />
|
||||
<path d="M14 2v4a2 2 0 0 0 2 2h4" />
|
||||
<path d="M10 9H8" />
|
||||
<path d="M16 13H8" />
|
||||
<path d="M16 17H8" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 394 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
|
||||
<path d="M2 12h20" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 333 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 362 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 361 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="20" height="16" x="2" y="4" rx="2" />
|
||||
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 320 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0" />
|
||||
<circle cx="12" cy="10" r="3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 361 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 511 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M4 11a9 9 0 0 1 9 9" />
|
||||
<path d="M4 4a16 16 0 0 1 16 16" />
|
||||
<circle cx="5" cy="19" r="1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 317 B |
@@ -0,0 +1,18 @@
|
||||
<svg
|
||||
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="18" cy="5" r="3" />
|
||||
<circle cx="6" cy="12" r="3" />
|
||||
<circle cx="18" cy="19" r="3" />
|
||||
<line x1="8.59" x2="15.42" y1="13.51" y2="17.49" />
|
||||
<line x1="15.41" x2="8.59" y1="6.51" y2="10.49" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 420 B |
@@ -0,0 +1,54 @@
|
||||
# Icon manifest — maps public icon name → {family, slug}
|
||||
# family: brand → assets/icons/brand/<slug>.svg (Simple Icons v13, CC0 1.0)
|
||||
# family: ui → assets/icons/ui/<slug>.svg (Lucide Static v0.460, ISC)
|
||||
#
|
||||
# To add an icon: run scripts/sync-icons.sh then add an entry here.
|
||||
|
||||
# --- Brand / Social ---
|
||||
github: { family: brand, slug: github }
|
||||
gitlab: { family: brand, slug: gitlab }
|
||||
mastodon: { family: brand, slug: mastodon }
|
||||
bsky: { family: brand, slug: bluesky }
|
||||
x: { family: brand, slug: x }
|
||||
threads: { family: brand, slug: threads }
|
||||
linkedin: { family: brand, slug: linkedin }
|
||||
instagram: { family: brand, slug: instagram }
|
||||
facebook: { family: brand, slug: facebook }
|
||||
tiktok: { family: brand, slug: tiktok }
|
||||
youtube: { family: brand, slug: youtube }
|
||||
twitch: { family: brand, slug: twitch }
|
||||
discord: { family: brand, slug: discord }
|
||||
telegram: { family: brand, slug: telegram }
|
||||
signal: { family: brand, slug: signal }
|
||||
whatsapp: { family: brand, slug: whatsapp }
|
||||
reddit: { family: brand, slug: reddit }
|
||||
medium: { family: brand, slug: medium }
|
||||
devto: { family: brand, slug: devdotto }
|
||||
substack: { family: brand, slug: substack }
|
||||
hashnode: { family: brand, slug: hashnode }
|
||||
kofi: { family: brand, slug: kofi }
|
||||
patreon: { family: brand, slug: patreon }
|
||||
buymeacoffee: { family: brand, slug: buymeacoffee }
|
||||
paypal: { family: brand, slug: paypal }
|
||||
bandcamp: { family: brand, slug: bandcamp }
|
||||
soundcloud: { family: brand, slug: soundcloud }
|
||||
spotify: { family: brand, slug: spotify }
|
||||
figma: { family: brand, slug: figma }
|
||||
dribbble: { family: brand, slug: dribbble }
|
||||
stackoverflow: { family: brand, slug: stackoverflow }
|
||||
matrix: { family: brand, slug: matrix }
|
||||
|
||||
# --- UI / Utility ---
|
||||
mail: { family: ui, slug: mail }
|
||||
globe: { family: ui, slug: globe }
|
||||
link: { family: ui, slug: link }
|
||||
rss: { family: ui, slug: rss }
|
||||
calendar: { family: ui, slug: calendar }
|
||||
phone: { family: ui, slug: phone }
|
||||
mappin: { family: ui, slug: map-pin }
|
||||
filetext: { family: ui, slug: file-text }
|
||||
extlink: { family: ui, slug: external-link }
|
||||
share: { family: ui, slug: share-2 }
|
||||
bookopen: { family: ui, slug: book-open }
|
||||
download: { family: ui, slug: download }
|
||||
heart: { family: ui, slug: heart }
|
||||
@@ -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,63 @@
|
||||
# Bonsai v0.4 a11y + polish pass: 17 audit findings shipped, OG auto-gen still deferred
|
||||
|
||||
**Date**: 2026-05-10 02:30
|
||||
**Severity**: Low
|
||||
**Component**: CSS palettes, Hugo templates, README/CHANGELOG
|
||||
**Status**: Resolved (PR #15 merged)
|
||||
|
||||
## What Happened
|
||||
|
||||
Did a full UI/UX audit of the theme (4 palettes × 2 modes × 3 layouts) via static analysis + rendered HTML — no headless browser available on host (Chromium ARM64 mismatch, bundled Puppeteer wouldn't launch). Audit produced 17 findings; first pass shipped 10 high-confidence fixes inline, then planned + shipped the 7 remaining (P1 accent contrast, P2 hover affordance, P4 favicon polish, P5 gallery landmarks, P6 i18n fallback, P7 `data-theme="auto"` removal, plus Q4 `<noscript>` toggle hide). P3 (auto-rasterized OG images) stayed deferred to v0.4 proper for the same reason it was deferred in v0.3 — binary-weight conflict with the theme's small footprint.
|
||||
|
||||
PR #15: 17 files, +499 / −33, merged green.
|
||||
|
||||
## The Brutal Truth
|
||||
|
||||
The audit was strong because no browser ran. With no screenshot crutch, every finding had to be backed by computed WCAG ratios, CSS reasoning, and curl-of-rendered-HTML — not "looks off" intuition. The findings table came out sharper than my usual pass. Tradeoff: nobody eyeballed the new sakura/koi accents; the PR ships with the screenshot checkbox unchecked and a note recommending manual review post-merge. That's the honest version.
|
||||
|
||||
The fastest part of the work was Phase 1's brand color shift. `#d4456a → #c93f63` (sakura) and `#c8521e → #bd4c1c` (koi) are 4–6% lightness drops — small enough that "cherry blossom" and "koi orange" still read right per the README labels, big enough to clear AA at 4.49 and 4.63 vs bg. The user's call ("darken brand accents" over "chip-only bold") was the right one: the chip is supposed to *look like* a real link button, not a styled exception. Bolding it would have hidden the contrast problem instead of fixing it.
|
||||
|
||||
The slowest part was a self-inflicted wound on Hugo's `i18n` function. Phase 3's plan said "use the two-arg form `i18n KEY FALLBACK`" — that's wrong. Hugo's second arg is template *context*, not a fallback string. Caught it during implementation by re-reading Hugo docs before pushing the wrong pattern. Switched to `{{ with i18n }}{{ . }}{{ else }}…{{ end }}` which actually works. Lesson re-learned: when planning, verify framework signatures from docs, not memory.
|
||||
|
||||
## Technical Details
|
||||
|
||||
1. **Self-approval blocked, single-author repo workaround** — `gh pr review 15 --approve` errors with "Can not approve your own pull request." Fell back to `gh pr merge --merge --delete-branch` directly. Repo's pattern is merge commits (PRs #12, #13) so used `--merge` not `--squash`. Branch + PR flow still useful for the structured changelog and CI gate even when self-merging.
|
||||
|
||||
2. **CSS gzip budget breached, README updated to match reality** — `static/css/bonsai.css` minified+gzipped is now **3,113 B**. README v0.3 said `< 3 KB gzipped CSS`. The 10 prior fixes (FOUC inline script, `:active` rules, `prefers-reduced-motion` extensions, `position: relative` on `.link`, theme-toggle 36 → 44 px) ate 200 B; this PR's `color-mix` hover bg added 34 B. Updated the README claim to `~ 3 KB` instead of clamping the budget. Honest beats aspirational.
|
||||
|
||||
3. **`color-mix(in oklab, ...)` for hover affordance, not `rgba` overlay** — Hover rule was `transform: translateY(-1px); border-color: var(--bonsai-accent);`. Added `background: color-mix(in oklab, var(--bonsai-surface) 94%, var(--bonsai-accent));`. `oklab` keeps perceptual lightness consistent across all 4 palettes — koi (warm) and sumi (neutral) both get the right *amount* of accent without one looking pink and the other muddy. `rgba` on top of `--bonsai-surface` would have shifted hue per-palette unpredictably.
|
||||
|
||||
4. **`<section aria-labelledby>` over wrapping in landmarks** — Gallery pages had `<article>` (intro) followed by a loose `<div class="…-gallery">`. Cards were orphaned siblings of the intro article rather than children. Wrapped each gallery in `<section aria-labelledby="…-heading">` keyed to the page `<h1>`. For variants page, demoted per-card `<section>` → `<article>` to avoid double-region nesting that screen readers announce as redundant. CSS uses class selectors (`.variants-gallery__card`) not element selectors so the swap was safe.
|
||||
|
||||
5. **`<noscript>` style guard for JS-only theme toggle** — Theme toggle button only does work when the deferred JS runs. Without JS it renders but does nothing — broken affordance. Added `<noscript><style>.theme-toggle{display:none!important}</style></noscript>` to `<head>`, but only inside the existing `{{- if site.Params.themeToggle }}` block. Zero cost when the feature is off; full progressive-enhancement guard when it's on.
|
||||
|
||||
6. **`data-theme="auto"` was a no-op, removed without behavior change** — The CSS only ever queried `[data-theme="light"]`, `[data-theme="dark"]`, or `:not([data-theme="..."])`. The `auto` value matched the third bucket — same as having no attribute at all. Removed from `baseof.html`. The inline FOUC script (v0.3) sets `data-theme` from `localStorage` when the user has chosen, so the attribute appears on first paint when meaningful and is absent otherwise. Cleaner state machine.
|
||||
|
||||
## Root Cause Analysis
|
||||
|
||||
- **Audit quality went up because tooling went down.** No headless Chrome forced reasoning instead of pattern-matching against screenshots. Every contrast finding was a computed ratio; every layout claim was math against the actual `clamp()` rules. This is reproducible on any host — no "needs Chrome" friction. The cost was no visual proof artifact; recommended a manual eyeball in the PR description.
|
||||
|
||||
- **Hugo `i18n` second-arg semantics tripped me at planning time.** I planned the wrong pattern (`i18n KEY FALLBACK` — second arg is context, not fallback) and would have shipped it if not for re-reading docs at implementation time. Plans should encode actual function signatures, not vibes.
|
||||
|
||||
- **CSS budget creep is invisible until it crosses a documented number.** README's `< 3 KB gzipped` was load-bearing — it was a *claim*, not just a goal. Ten small fixes pushed it over. No automation caught it. Either the README claim becomes a build-time assertion, or it stays prose and gets updated when reality moves. Picked prose-update for now.
|
||||
|
||||
## Lessons Learned
|
||||
|
||||
1. **No browser, sharper audit.** Static analysis + computed ratios beats vibes-based screenshot review for accessibility work. Keep the discipline even when Chrome is available.
|
||||
|
||||
2. **Verify framework signatures from docs at plan time, not from memory.** The `i18n` two-arg confusion would have shipped wrong if I hadn't re-checked. Plans are cheaper to fix than reverts.
|
||||
|
||||
3. **Update aspirational claims when reality moves.** `< 3 KB gzipped` was true at v0.1, false at v0.4. Updating the README to `~ 3 KB` is healthier than rolling back honest fixes to clear an aging budget.
|
||||
|
||||
4. **Identity-touching changes need user consent, not heuristics.** Asking three focused `AskUserQuestion` calls (P1 accent darken vs chip-bold; P3 OG defer; P4 favicon expand) cost 30 seconds and locked scope cleanly. The wrong call would have either over-reached (auto-rasterize OG image, +150 KB binary) or under-shipped (chip-only bold styling that hides the contrast problem).
|
||||
|
||||
5. **Self-merging is fine when the PR carries its own review surface.** Couldn't approve own PR via `gh pr review` (GitHub blocks it), so the PR description had to do the review's work: structured test plan, computed numbers, files-changed table, the unchecked "visual screenshots" item flagged honestly. Future readers see what was verified and what wasn't. Good enough for a single-author theme; would add a co-reviewer for anything multi-author.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Manual visual eyeball of all 4 palettes × 2 modes × 3 layouts on a real browser. Recommended before tagging `v0.4.0`.
|
||||
- v0.4 candidates still open: auto-rasterized OG images (now the only remaining v0.4 item from the original roadmap), RSS opt-in, multi-section bio, more icons, gallery CSS extraction (`gallery.css` loaded only on demo pages, drop ~2 KB from user sites).
|
||||
- Consider a build-time CSS-budget assertion (Hugo's `resources.PostProcess` + a shell test, or a CI step that gzips and grep-fails). Worth it once the budget becomes a hard contract.
|
||||
- Consider adding a `params.darkAccent` override (raised in audit Q3) — sakura/koi already brighten the accent in dark mode; users can't customize without overriding all 6 CSS vars.
|
||||
|
||||
**Owner**: @tiennm99
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Home"
|
||||
---
|
||||
|
||||
This file exists so Hugo renders the index page. All bio content comes from `[params]` in `hugo.toml`.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: "Icon Gallery"
|
||||
description: "All available icons in the Bonsai theme."
|
||||
layout: "single"
|
||||
type: "icons"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Color themes"
|
||||
type: themes
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Layout variants"
|
||||
type: variants
|
||||
---
|
||||
@@ -0,0 +1,52 @@
|
||||
baseURL = "https://example.com/"
|
||||
languageCode = "en-us"
|
||||
title = "Tien Nguyen"
|
||||
theme = "bonsai"
|
||||
|
||||
# Bonsai is a single-page bio theme — taxonomies/sitemaps off; RSS on for the
|
||||
# v0.4 demo (set params.rss = true and remove "RSS" from disableKinds).
|
||||
disableKinds = ["taxonomy", "term", "sitemap", "404"]
|
||||
|
||||
[params]
|
||||
name = "Tien Nguyen"
|
||||
tagline = "Software engineer · tending small things with care"
|
||||
bio = "Building tools, writing code, growing a quiet corner of the internet."
|
||||
# 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
|
||||
rss = true # opt-in RSS feed of [[params.links]]
|
||||
jobTitle = "Software Engineer"
|
||||
footerText = "© 2026 · made with bonsai"
|
||||
# ogImageUrl = "/og.png" # optional 1200×630 image for social previews
|
||||
|
||||
[[params.links]]
|
||||
title = "GitHub"
|
||||
url = "https://github.com/tiennm99"
|
||||
icon = "github"
|
||||
|
||||
[[params.links]]
|
||||
title = "Personal site"
|
||||
url = "https://example.com"
|
||||
icon = "globe"
|
||||
|
||||
[[params.links]]
|
||||
title = "LinkedIn"
|
||||
url = "https://linkedin.com/in/tiennm99"
|
||||
icon = "linkedin"
|
||||
|
||||
[[params.links]]
|
||||
title = "Email me"
|
||||
url = "mailto:tiennm99@outlook.com"
|
||||
icon = "mail"
|
||||
|
||||
[[params.links]]
|
||||
title = "Mastodon"
|
||||
url = "https://mastodon.social/@tiennm99"
|
||||
icon = "mastodon"
|
||||
|
||||
[[params.links]]
|
||||
title = "Bluesky"
|
||||
url = "https://bsky.app/profile/tiennm99.bsky.social"
|
||||
icon = "bsky"
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200">
|
||||
<circle cx="100" cy="100" r="100" fill="#8b3a2b"/>
|
||||
<text x="100" y="138" font-family="Georgia, serif" font-size="110" font-weight="600" fill="#f4efe6" text-anchor="middle">T</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 280 B |
@@ -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 }}"
|
||||
@@ -0,0 +1,5 @@
|
||||
# Theme images
|
||||
|
||||
Place `screenshot.png` (1500×1000) and `tn.png` (900×600) here for submission to the Hugo themes site.
|
||||
|
||||
See: https://github.com/gohugoio/hugoThemesSiteBuilder#requirements
|
||||
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 37 KiB |
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ site.LanguageCode | default `en` }}" data-bonsai-theme="{{ site.Params.colorTheme | default `bonsai` }}">
|
||||
<head>
|
||||
{{- partial "head.html" . -}}
|
||||
{{- block "head_extra" . }}{{- end }}
|
||||
</head>
|
||||
<body>
|
||||
<main class="bonsai">
|
||||
{{- block "main" . }}{{- end }}
|
||||
</main>
|
||||
{{- partial "footer.html" . -}}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,14 @@
|
||||
{{ define "main" }}
|
||||
<section class="icon-gallery">
|
||||
<h1>{{ .Title }}</h1>
|
||||
<p>{{ .Description | default "All available icons in the Bonsai theme." }}</p>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:1.5rem;margin-top:1.5rem;">
|
||||
{{- range $name, $entry := site.Data.icons }}
|
||||
<div style="display:flex;flex-direction:column;align-items:center;gap:0.4rem;width:80px;">
|
||||
<span style="width:20px;height:20px;">{{ partial "icon.html" $name }}</span>
|
||||
<span style="font-size:0.7rem;text-align:center;word-break:break-all;">{{ $name }}</span>
|
||||
</div>
|
||||
{{- end }}
|
||||
</div>
|
||||
</section>
|
||||
{{ end }}
|
||||
@@ -0,0 +1,3 @@
|
||||
{{ define "main" }}
|
||||
{{- partial "bio-card.html" . -}}
|
||||
{{ end }}
|
||||
@@ -0,0 +1,32 @@
|
||||
{{- /* RSS 2.0 feed of [[params.links]].
|
||||
Only emitted when site.Params.rss is true AND user has removed `RSS`
|
||||
from disableKinds in their hugo.toml. */ -}}
|
||||
{{- if site.Params.rss -}}
|
||||
{{- $name := site.Params.name | default site.Title -}}
|
||||
{{- $tagline := site.Params.tagline | default "" -}}
|
||||
{{- $bio := site.Params.bio | default $tagline -}}
|
||||
{{- $now := now -}}
|
||||
{{ printf "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" | safeHTML }}
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>{{ $name | transform.XMLEscape }}{{ with $tagline }} — {{ . | transform.XMLEscape }}{{ end }}</title>
|
||||
<link>{{ site.BaseURL | absLangURL }}</link>
|
||||
<description>{{ $bio | transform.XMLEscape }}</description>
|
||||
<generator>Hugo + Bonsai</generator>
|
||||
<language>{{ site.Language.LanguageCode | default site.LanguageCode }}</language>
|
||||
<lastBuildDate>{{ $now.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</lastBuildDate>
|
||||
<atom:link href="{{ "index.xml" | absURL }}" rel="self" type="application/rss+xml" />
|
||||
{{- range site.Params.links }}
|
||||
<item>
|
||||
<title>{{ .title | transform.XMLEscape }}</title>
|
||||
<link>{{ .url }}</link>
|
||||
<guid isPermaLink="false">bonsai-link-{{ .url }}</guid>
|
||||
<pubDate>{{ $now.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate>
|
||||
{{- with .description }}
|
||||
<description>{{ . | transform.XMLEscape }}</description>
|
||||
{{- end }}
|
||||
</item>
|
||||
{{- end }}
|
||||
</channel>
|
||||
</rss>
|
||||
{{- end -}}
|
||||
@@ -0,0 +1,36 @@
|
||||
{{- /* 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)" -}}
|
||||
{{- /* 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="currentColor">{{ $initials }}</text>
|
||||
</svg>
|
||||
{{- end -}}
|
||||
@@ -0,0 +1,32 @@
|
||||
{{- $name := site.Params.name | default site.Title -}}
|
||||
{{- $tagline := site.Params.tagline -}}
|
||||
{{- $bio := site.Params.bio -}}
|
||||
{{- $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">
|
||||
{{ partial "avatar.html" . }}
|
||||
|
||||
<h1 class="bio__name">{{ $name }}</h1>
|
||||
|
||||
{{- with $tagline }}
|
||||
<p class="bio__tagline">{{ . }}</p>
|
||||
{{- end }}
|
||||
|
||||
{{- with $bio }}
|
||||
<p class="bio__text">{{ . | markdownify }}</p>
|
||||
{{- end }}
|
||||
|
||||
{{- if $links }}
|
||||
<nav class="bio__links bio__links--{{ $layout }}" aria-label="{{ i18n "nav_links_label" | default "Links" }}">
|
||||
{{- range $links }}
|
||||
{{- partial "link-button.html" . -}}
|
||||
{{- end }}
|
||||
</nav>
|
||||
{{- end }}
|
||||
</article>
|
||||
@@ -0,0 +1,13 @@
|
||||
{{- if site.Params.footer | default true -}}
|
||||
<footer class="bonsai-footer">
|
||||
{{- with site.Params.footerText -}}
|
||||
<p>{{ . | safeHTML }}</p>
|
||||
{{- else -}}
|
||||
{{- $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 -}}
|
||||
@@ -0,0 +1,58 @@
|
||||
{{- $name := site.Params.name | default site.Title -}}
|
||||
{{- $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" />
|
||||
<title>{{ $name }}{{ with $tagline }} — {{ . }}{{ end }}</title>
|
||||
{{- with $description }}
|
||||
<meta name="description" content="{{ . }}" />
|
||||
{{- end }}
|
||||
|
||||
<meta property="og:type" content="profile" />
|
||||
<meta property="og:title" content="{{ $name }}" />
|
||||
{{- with $description }}
|
||||
<meta property="og:description" content="{{ . }}" />
|
||||
{{- end }}
|
||||
{{- with $ogImageUrl }}
|
||||
<meta property="og:image" content="{{ . }}" />
|
||||
<meta name="twitter:image" content="{{ . }}" />
|
||||
{{- end }}
|
||||
<meta name="twitter:card" content="{{ $twitterCard }}" />
|
||||
|
||||
{{ 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.rss }}
|
||||
<link rel="alternate" type="application/rss+xml" title="{{ site.Params.name | default site.Title }}" href="{{ `index.xml` | absURL }}" />
|
||||
{{- end }}
|
||||
{{- 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 }}
|
||||
@@ -0,0 +1,13 @@
|
||||
{{- $name := . -}}
|
||||
{{- $entry := index site.Data.icons $name -}}
|
||||
{{- if $entry -}}
|
||||
{{- $path := printf "icons/%s/%s.svg" $entry.family $entry.slug -}}
|
||||
{{- with resources.Get $path -}}
|
||||
{{- .Content | safeHTML -}}
|
||||
{{- else -}}
|
||||
{{- /* asset missing — fall through to fallback below */ -}}
|
||||
{{- 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="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 -}}
|
||||
@@ -0,0 +1,12 @@
|
||||
{{- $title := .title -}}
|
||||
{{- $url := .url -}}
|
||||
{{- $icon := .icon -}}
|
||||
{{- $external := strings.HasPrefix $url "http" -}}
|
||||
<a class="link"
|
||||
href="{{ $url }}"
|
||||
{{- if $external }} target="_blank" rel="noopener noreferrer"{{ end }}>
|
||||
{{- with $icon }}
|
||||
<span class="link__icon" aria-hidden="true">{{ partial "icon.html" . }}</span>
|
||||
{{- end }}
|
||||
<span class="link__title">{{ $title }}</span>
|
||||
</a>
|
||||
@@ -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="{{ with i18n "theme_toggle_label" }}{{ . }}{{ else }}Toggle light and dark theme{{ end }}"
|
||||
aria-pressed="false"
|
||||
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">
|
||||
<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,30 @@
|
||||
{{ define "head_extra" }}
|
||||
<link rel="stylesheet" href="{{ `css/gallery.css` | relURL }}" />
|
||||
{{ end }}
|
||||
|
||||
{{ define "main" }}
|
||||
<article style="text-align:center;">
|
||||
<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>
|
||||
</article>
|
||||
|
||||
{{- $palettes := slice
|
||||
(dict "name" "bonsai" "label" "Bonsai (washi + vermilion)" "accent" "#8b3a2b")
|
||||
(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" "#bd4c1c")
|
||||
-}}
|
||||
|
||||
<section class="themes-gallery" aria-labelledby="themes-heading">
|
||||
{{- range $palettes }}
|
||||
<div data-bonsai-theme="{{ .name }}" class="themes-gallery__card">
|
||||
<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 }}
|
||||
</section>
|
||||
{{ end }}
|
||||
@@ -0,0 +1,40 @@
|
||||
{{ define "head_extra" }}
|
||||
<link rel="stylesheet" href="{{ `css/gallery.css` | relURL }}" />
|
||||
{{ end }}
|
||||
|
||||
{{ define "main" }}
|
||||
<article style="text-align:center;">
|
||||
<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>
|
||||
</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.")
|
||||
-}}
|
||||
|
||||
<section class="variants-gallery" aria-labelledby="variants-heading">
|
||||
{{- range $variants }}
|
||||
<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">
|
||||
{{- range $sample }}
|
||||
{{- partial "link-button.html" . -}}
|
||||
{{- end }}
|
||||
</nav>
|
||||
</article>
|
||||
{{- end }}
|
||||
</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,48 @@
|
||||
---
|
||||
phase: 2
|
||||
title: Extract gallery CSS (#11)
|
||||
status: completed
|
||||
priority: P2
|
||||
effort: 30m
|
||||
dependencies: []
|
||||
---
|
||||
|
||||
# Phase 2: Extract gallery CSS (#11)
|
||||
|
||||
## Overview
|
||||
|
||||
Move `.themes-gallery__*` + `.variants-gallery__*` selectors out of `static/css/bonsai.css` into a new `static/css/gallery.css` referenced only by the demo pages. End-user sites stop paying for ~300 gzipped bytes of CSS they never use.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
- Modify: `static/css/bonsai.css` — remove gallery rules
|
||||
- Create: `static/css/gallery.css` — new file with the extracted rules
|
||||
- Modify: `layouts/themes/single.html` — load gallery.css for this page only
|
||||
- Modify: `layouts/variants/single.html` — load gallery.css for this page only
|
||||
- Modify: `layouts/_default/baseof.html` (if needed) — add `head_extra` block hook
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Identify and excise gallery selectors from `static/css/bonsai.css`. Selectors: `.themes-gallery`, `.themes-gallery__card`, `.themes-gallery__name`, `.themes-gallery__hex`, `.themes-gallery__chip`, `.themes-gallery__chip--accent`, `.variants-gallery`, `.variants-gallery__card`, `.variants-gallery__name`, `.variants-gallery__desc`, `.variants-gallery__code`. Plus any responsive `@media` rules that target only those selectors.
|
||||
2. Create `static/css/gallery.css` with the excised rules.
|
||||
3. Add a `{{- block "head_extra" . -}}{{- end }}` hook in `layouts/_default/baseof.html` `<head>` (after the main stylesheet link).
|
||||
4. In `layouts/themes/single.html` and `layouts/variants/single.html`, define the block: `{{ define "head_extra" }}<link rel="stylesheet" href="{{ \`css/gallery.css\` | relURL }}">{{ end }}`.
|
||||
5. Verify `/icons/` page does NOT pull in `gallery.css` (it has no `head_extra` block).
|
||||
6. Build + curl: confirm `/themes/` and `/variants/` pull both stylesheets; `/` and `/icons/` pull only `bonsai.css`.
|
||||
7. Measure: `bonsai.css` raw + gzipped should drop ~1,200 raw / ~300 gzipped.
|
||||
8. Update CHANGELOG (Unreleased) and README's CSS budget claim if it shifts back under 3 KB.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] `static/css/bonsai.css` no longer contains gallery selectors
|
||||
- [ ] `static/css/gallery.css` exists and renders gallery markup correctly
|
||||
- [ ] `/themes/` and `/variants/` load both stylesheets
|
||||
- [ ] `/` and `/icons/` load only `bonsai.css`
|
||||
- [ ] `bonsai.css` gzipped < 3 KB
|
||||
- [ ] `hugo --gc --minify` clean
|
||||
- [ ] PR opened against main
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- **Risk:** `head_extra` block name conflicts with future Hugo theme usage. **Mitigation:** name is theme-local, undocumented externally; safe.
|
||||
- **Risk:** missed responsive rules during excision. **Mitigation:** grep `gallery` in CSS before commit; only matches in gallery.css.
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
phase: 3
|
||||
title: Add more icons (#10)
|
||||
status: completed
|
||||
priority: P3
|
||||
effort: 30m
|
||||
dependencies: []
|
||||
---
|
||||
|
||||
# Phase 3: Add more icons (#10)
|
||||
|
||||
## Overview
|
||||
|
||||
Vendor ~10 additional brand + utility SVGs to the existing 35-icon set. Mechanical: add entries to `data/icons.yaml`, run `scripts/sync-icons.sh`, update README tables, verify `/icons/` gallery.
|
||||
|
||||
## Shortlist (≤ 10, picked for breadth)
|
||||
|
||||
Brand (Simple Icons CC0):
|
||||
- `bandcamp` — musicians
|
||||
- `soundcloud` — musicians
|
||||
- `spotify` — artist profiles
|
||||
- `figma` — designers
|
||||
- `dribbble` — designers
|
||||
- `stackoverflow` — devs
|
||||
- `matrix` — fediverse messaging
|
||||
|
||||
Utility (Lucide ISC):
|
||||
- `book-open` — bookshelves, reading lists
|
||||
- `download` — resume / vCard
|
||||
- `heart` — sponsor / support
|
||||
|
||||
Total +10 → 45 icons. Soft ceiling at ~50 for v0-line per issue.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
- Modify: `data/icons.yaml`
|
||||
- Run: `scripts/sync-icons.sh`
|
||||
- Generated: `assets/icons/brand/*.svg`, `assets/icons/ui/*.svg`
|
||||
- Modify: `README.md` icon tables (both `<details>` blocks)
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Inspect current `data/icons.yaml` structure to match the entry schema.
|
||||
2. Append the 10 shortlisted entries (correct slug, license attribution).
|
||||
3. Run `scripts/sync-icons.sh` — verify SVGs land in `assets/icons/brand/` and `assets/icons/ui/`.
|
||||
4. Inspect downloaded SVGs: confirm size budget (each typically < 1 KB).
|
||||
5. Update README icon tables under both `<details>` blocks (brand + utility), keep alphabetical or current order convention.
|
||||
6. Update README intro line if it claims a fixed count (e.g. "35 icons" → "45 icons").
|
||||
7. Build + curl `/icons/` to confirm new icons render in gallery.
|
||||
8. Update CHANGELOG (Unreleased).
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] 10 new SVGs vendored under `assets/icons/`
|
||||
- [ ] `data/icons.yaml` updated with 10 new entries
|
||||
- [ ] README icon tables updated; count line updated
|
||||
- [ ] `/icons/` gallery renders all new icons
|
||||
- [ ] `hugo --gc --minify` clean
|
||||
- [ ] PR opened against main
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- **Risk:** Simple Icons / Lucide rename or remove a slug. **Mitigation:** `sync-icons.sh` fails loudly if slug missing; pick alternates from the original issue list.
|
||||
- **Risk:** SVG size larger than expected (some Simple Icons brand marks are heavy). **Mitigation:** check raw sizes after sync; reject if any single icon > 3 KB.
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
phase: 4
|
||||
title: Optional RSS feed (#8)
|
||||
status: completed
|
||||
priority: P3
|
||||
effort: 45m
|
||||
dependencies: []
|
||||
---
|
||||
|
||||
# Phase 4: Optional RSS feed (#8)
|
||||
|
||||
## Overview
|
||||
|
||||
Add `params.rss = true` opt-in. When enabled, the theme stops adding `RSS` to its `disableKinds` recommendation and renders an RSS feed of `[[params.links]]`. Adds `<link rel="alternate" type="application/rss+xml">` to `<head>`.
|
||||
|
||||
## Design notes
|
||||
|
||||
- Hugo respects user `disableKinds` in their `hugo.toml`. The theme can't force-enable RSS — but it can:
|
||||
- Document the param in README
|
||||
- Provide an `index.rss.xml` template that runs when the user removes `RSS` from `disableKinds`
|
||||
- Add the `<link rel="alternate">` in `head.html` when `params.rss` is true (regardless of `disableKinds` — feed crawler will 404 if user mismatches; that's their config)
|
||||
- Item shape: `<title>` = link title, `<link>` = link URL, `<description>` = optional description, `<pubDate>` = build time (links lack intrinsic dates).
|
||||
|
||||
## Related Code Files
|
||||
|
||||
- Create: `layouts/index.rss.xml` — template for the feed
|
||||
- Modify: `layouts/partials/head.html` — emit `<link rel="alternate">` when `params.rss = true`
|
||||
- Modify: `README.md` — document the new param + the `disableKinds` removal step
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Create `layouts/index.rss.xml` rendering an RSS 2.0 feed:
|
||||
- `<channel>` populated from `params.name`, `params.tagline`, `params.bio`, site `baseURL`, build `time.Now`
|
||||
- `<item>` per `[[params.links]]` entry; `<pubDate>` = build time; `<guid isPermaLink="true">` = link URL
|
||||
2. In `layouts/partials/head.html`, when `site.Params.rss` is true: `<link rel="alternate" type="application/rss+xml" title="…" href="{{ \`index.xml\` | absURL }}">`. Hugo emits `index.xml` for the home RSS template.
|
||||
3. README: add `rss` to the params table; add a short "Enable RSS" subsection explaining that user must remove `RSS` from their `disableKinds` line and set `params.rss = true`.
|
||||
4. Build + curl `/index.xml` — confirm valid RSS 2.0 (well-formed XML, items present); confirm `<head>` of `/` carries the alternate link when `rss = true`.
|
||||
5. Validate against W3C feed validator if reachable; if not, manually inspect for required elements.
|
||||
6. Update CHANGELOG (Unreleased).
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] `layouts/index.rss.xml` exists and renders well-formed RSS 2.0 when feature on
|
||||
- [ ] `<head>` carries `<link rel="alternate">` when `params.rss = true`
|
||||
- [ ] No RSS link emitted when `params.rss` unset/false (default)
|
||||
- [ ] README documents the new param + `disableKinds` step
|
||||
- [ ] `hugo --gc --minify` clean
|
||||
- [ ] PR opened against main
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- **Risk:** User leaves `RSS` in `disableKinds` and the alternate link 404s. **Mitigation:** README is explicit; theme cannot override user's `disableKinds`.
|
||||
- **Risk:** Build-time pubDate makes the feed change every build, polluting feed readers. **Mitigation:** documented as a known limitation; alternative is a per-link `pubDate` field (out of scope).
|
||||
- **Risk:** Emoji/unicode in `params.tagline` breaks XML. **Mitigation:** Hugo's `transform.XMLEscape` or default escaping in templates handles it.
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
phase: 7
|
||||
title: v0.4.0 release prep
|
||||
status: in-progress
|
||||
priority: P3
|
||||
effort: 30m
|
||||
dependencies:
|
||||
- 2
|
||||
- 3
|
||||
- 4
|
||||
---
|
||||
|
||||
# Phase 7: v0.4.0 release prep
|
||||
|
||||
## Overview
|
||||
|
||||
After phases 2, 3, 4 merge to main, sync `CHANGELOG.md`, bump version references, tag `v0.4.0`, push tag.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Pull latest `main`, confirm phases 2/3/4 PRs are merged.
|
||||
2. Move `## [Unreleased]` section to `## [0.4.0] — 2026-05-10` (or actual merge date).
|
||||
3. Add fresh `## [Unreleased]` section above with v0.5 placeholders (#7 OG auto-gen, #9 multi-section bio explicitly listed as deferred from v0.4).
|
||||
4. Cross-check README for stale "v0.3" or version mentions; update where needed.
|
||||
5. Reissue `theme.toml` `min_version` only if a phase added a Hugo feature requiring a newer Hugo version (none expected here).
|
||||
6. Final build: `hugo --gc --minify --themesDir ../..` from `exampleSite/` — clean.
|
||||
7. `git tag v0.4.0 -m "v0.4.0: gallery CSS extraction, more icons, opt-in RSS"`
|
||||
8. `git push origin v0.4.0`
|
||||
9. Run `/ck:journal` for the v0.4.0 release retro.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] CHANGELOG has dated `## [0.4.0]` section
|
||||
- [ ] `## [Unreleased]` section exists with v0.5 deferral notes
|
||||
- [ ] `git tag v0.4.0` exists locally and on remote
|
||||
- [ ] Build clean across all 3 demo pages
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- **Risk:** auto-tag without user approval. **Mitigation:** ask before `git push origin v0.4.0`; tag is the publish event, deserves explicit consent.
|
||||
- **Risk:** stale "35 icons" claim in README after Phase 3. **Mitigation:** Phase 3 success criterion already covers this.
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
title: 'v0.4 release: ship gallery-CSS extraction + more icons + opt-in RSS'
|
||||
description: >-
|
||||
Ship issues #11, #10, #8 each as its own PR; tag v0.4.0. Issues #7 (OG
|
||||
auto-gen) and #9 (multi-section bio) deferred to v0.5 per their own deferral
|
||||
notes.
|
||||
status: in-progress
|
||||
priority: P2
|
||||
created: 2026-05-10T00:00:00.000Z
|
||||
---
|
||||
|
||||
# v0.4 release: ship the three well-scoped v0-line enhancements
|
||||
|
||||
## Overview
|
||||
|
||||
Per scope-check: ship the three well-scoped issues now, defer the two design-heavy ones.
|
||||
|
||||
- **Phase 2** Extract gallery CSS (#11) — pure refactor, smallest scope
|
||||
- **Phase 3** Add more icons (#10) — mechanical, data-driven
|
||||
- **Phase 4** Optional RSS feed (#8) — additive opt-in
|
||||
- **Phase 7** v0.4.0 release prep
|
||||
|
||||
Each phase ships as its own feature branch + PR (matches repo pattern).
|
||||
|
||||
## Deferred to v0.5
|
||||
|
||||
- **#7 Auto-generate OG image** — issue text: *"If no path meets [≤30 KB binary], defer to v0.5"*. Of 5 candidate approaches, only `pyftsubset Inter` + base PNG might fit; needs measured prototype before committing.
|
||||
- **#9 Multi-section bio** — issue text: *"Likely a 2026-Q3 candidate, not v0.4"*. Schema design dispute; needs `/ck:brainstorm` before a plan.
|
||||
|
||||
## Phases (active)
|
||||
|
||||
| Phase | Name | Status |
|
||||
|-------|------|--------|
|
||||
| 2 | [Extract gallery CSS (#11)](./phase-02-extract-gallery-css-11.md) | Completed |
|
||||
| 3 | [Add more icons (#10)](./phase-03-add-more-icons-10.md) | Completed |
|
||||
| 4 | [Optional RSS feed (#8)](./phase-04-optional-rss-feed-8.md) | Completed |
|
||||
| 7 | [v0.4.0 release prep](./phase-07-v0-4-0-release-prep.md) | In Progress |
|
||||
|
||||
(Phase numbers preserved from original 7-phase scaffold for stable references.)
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Phase 7 depends on Phases 2, 3, 4. Each of 2/3/4 is independent and ships as a separate PR.
|
||||
@@ -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.
|
||||