Skip to content

v1.17.0 — a11y hardening + trust batch

Choose a tag to compare

@LarsArtmann LarsArtmann released this 13 Sep 11:01
· 548 commits to master since this release

Changed

  • Website: full Astro → templ conversion. templcomponents.lars.software is
    now generated by a Go static-site generator (website/ module) that renders
    every page through templ-components itself — layout.Base (SEO head,
    canonical, JSON-LD, skip link, ThemeScript), display.Button,
    layout.ThemeToggle, the icons package, and errorpage.NotFound404 for
    the 404 page. Docs moved from Starlight MDX to plain markdown rendered by
    goldmark + chroma with a sidebar/TOC/prev-next layout, git-based
    last-updated dates, and edit links. Better-than-before wins: hero counts
    are derived from the codebase at build time (the old page claimed stale
    94/102/37/9), fonts are self-hosted with preloads, highlighting CSS is
    regenerated per build (dual github/github-dark), sitemaps carry git
    lastmod, the newsletter popup no longer uses an inline onsubmit
    handler, and CI builds the site with Go + the Tailwind CLI instead of
    pnpm + Astro. Behavior parity preserved: same slugs, same localStorage
    theme keys, same Buttondown form, mobile nav, scroll-reveal, and
    motion-reduce fallbacks.

Added

  • wire.DecodeForm[T] — server-side counterpart to form-encoded wire
    submissions. Decodes POST bodies and GET query parameters into any struct
    via form:"name" tags (string, int, int64, bool; HTML checkbox "on"
    handled), leaving absent fields at zero values and failing loudly on
    malformed values (ErrUnsupportedFormField for other kinds). First
    consumer: display.ParseKanbanMove, now a thin domain-validation layer on
    top — its documented "form body or query parameters" contract is finally
    true for GET too. Consumers writing their own wired handlers no longer
    hand-roll ParseForm + PostForm.Get chains.
  • layout.Minimal gains optional SEO head tags — robots noindex,
    canonical, hreflang alternates, and JSON-LD, with identical semantics to
    PageProps.SEO. Both shells now emit the tags through one shared
    sub-template, so static/print documents (the Minimal use case) can opt
    into structured data without giving up the zero-dependency default (the
    zero value emits nothing). Unblocks the #156 adoption reason.
  • axe-core accessibility sweep over the live demo routes (visualtest):
    the vendored axe-core 4.11.1 runtime (MPL-2.0) audits all 7 demo pages
    (light + dark for index/forms) in real Chromium, with a positive-control
    test proving the harness detects violations and a baseline ledger
    (visualtest/testdata/axe_baseline.json) that fails CI on any new
    critical/serious finding. Accepted palette-convention contrast debt is
    documented in the baseline.
  • Demo click-through e2e suite (visualtest/demo_flows_e2e_test.go):
    browser-level proof of five headline demo flows against the real demo
    server — LoadMore through EndOfList, ConfirmDelete (native confirm
    auto-accepted), LoadingButton busy gate, multipart upload echo, and kanban
    move buttons on both htmx and Datastar boards. Every flow test bounds its
    tab context (2 min) so a wedged renderer fails the test instead of hanging
    the binary.
  • Demo route smoke test (visualtest/demo_smoke_test.go): all 7 demo
    routes must render their unique page title with zero 500s in the server
    log, so CI's existing Visual Regression job now fails when the demo a
    consumer copies from is broken — previously nothing in CI exercised the
    demo server at all.
  • Compiled-CSS target list is single-sourced in
    scripts/compiled-css-targets.txt: scripts/release.sh (recompile),
    scripts/check-css-minified.sh (minification), and
    utils.TestCompiledCSSInventory (tracked-set + existence guards) all read
    the same <input> <output> list — previously the list lived in bash AND Go
    and could drift independently. The minification guard now covers ALL three
    compiled targets (previously only static/app.css), and CONTRIBUTING.md
    states the rule: a committed artifact needs a named consumer.
  • Hook-adoption guard (Guard 0): .githooks/pre-commit now warns loudly
    when git config core.hooksPath is unset or points elsewhere (a clone
    running a stale .git/hooks/ copy), with the one-line fix
    (scripts/setup-hooks.sh). README's contributing section names the step.

Changed

  • Compiled-CSS resurrection loop closed. The BuildFlow daemon re-added six
    deleted .out.css artifacts within hours of the cleanup (504856b8 after
    43f1522f) — utils.TestCompiledCSSInventory caught it immediately.
    .gitignore now ignores *.out.css repo-wide except the one tracked
    distribution artifact, and the guard now asserts the git-tracked set
    (fail-loud on missing git; untracked daemon litter in the worktree is
    reported informationally), so daemon rebuilds can no longer re-stage zombie
    artifacts or false-fail the guard.
  • Pre-commit hooks are now tracked in .githooks/pre-commit (fast guards
    • BuildFlow), activated per clone via scripts/setup-hooks.sh
      (git config core.hooksPath .githooks). Previously the hook lived only in
      .git/hooks/ — untracked, silently absent for fresh clones.
      scripts/pre-commit.sh remains the full pre-push verify and is
      intentionally not wired into the hook.
  • utils.TestDocsCountDrift now guards every prose count — icons
    (README heading said 106 while the same README said 102), HTML golden
    baselines (ROADMAP/FEATURES said 175; actual 242), FEATURES visual goldens
    (114 → 125), and AGENTS golden files (102 → 242) are all machine-checked
    against the tree. The informational TestSkillComponentCount ghost guard
    (computed counts, compared nothing, skipped on missing fixture) is deleted;
    TestDocsCountDrift covers SKILL.md totals, per-package headings, and
    icons for real. A new TestVersionMatchesReadmeBadge closes the
    hand-edited version-badge drift gap.
  • Lint baseline is now literally zero across all 8 linted modules. The
    seven pre-existing findings (gocognit ×5 in oversized test functions,
    one noctx on exec.Command, one stale nolint:goconst directive) are
    fixed by refactoring — table-driven constructors test, extracted scan
    helpers, exec.CommandContext — so CI's "0 findings" claim is true again.
  • The two same-named templ-components-theme.css files now cross-reference
    each other in their headers.
    The repo-root file (palette-override
    @theme example) and the templates/ file (ADR-0008 semantic-token
    layer, release.sh compile input) are easy to confuse; each header now
    states what it is, what it is NOT, and where the sibling lives.

Removed

  • Ten dead compiled-CSS artifacts deleted (~208 KB of recurring
    daemon-recompile churn). The repo tracked compiled .out.css output for
    four theme presets, the tc CLI starter kit, a root theme file, a
    byte-identical duplicate of the demo stylesheet (demo.out.css), and a
    website stylesheet (global.out.css) — none had any in-repo consumer: the
    preset scaffolder (tc new) they were committed for never shipped, the
    CLI only reads app.css/custom.css from its embedded starter, the demo
    serves static/app.css, and the website compiles its own CSS via
    @tailwindcss/vite. The three legitimate compiled distribution targets
    remain (examples/demo/static/app.css, templates/styles.css,
    templates/templ-components-theme.out.css), scripts/release.sh now
    recompiles exactly those, and a new utils.TestCompiledCSSInventory guard
    fails the build if any stray .out.css re-enters the tree.

Fixed

  • display.KanbanBoard no longer widens the page on phone-width viewports.
    The board's fixed-width columns (~1200px intrinsic) propagated their
    min-content width through any grid/flex ancestor that lacked min-w-0,
    so on a 375px phone the whole document scrolled sideways instead of the
    board scrolling internally. The board root now carries min-w-0 — found
    by the new mobile sweep, not by a human.
  • Labeled form controls are now always associated with their labels.
    forms.Input, Textarea, Select, DatePicker, FileInput, Checkbox,
    Radio, RadioGroup options, Rating stars, Slider, TagsInput, and
    Combobox derive the control's DOM id from the field Name when no
    explicit ID is set, so the rendered <label> always points at its control.
    Previously, a labeled field rendered without an ID produced an unassociated
    label (or an invalid for="") — a WCAG 1.3.1/4.1.2 failure that axe-core
    reports as a critical label violation. FilterDropdown additionally gains
    an accessible name fallback ("Filter") and TagsInput ("Add tag").
  • Form controls with role="progressbar"/role="img" are never unnamed.
    feedback.ProgressBar now emits an accessible name (AriaLabel → Label →
    "Progress"), and display.BarChart/Heatmap fall back to "Bar chart" /
    "Heatmap" for their role="img" wrappers when no AriaLabel is set —
    fixing axe aria-progressbar-name/role-img-alt violations for consumers
    who skipped the label props.
  • display.Tabs emits aria-controls only for tabs that own a panel.
    Content-less tab links previously referenced non-existent panel ids (axe
    aria-valid-attr-value critical).
  • forms.Calendar uses role="group" instead of an invalid bare
    role="grid".
    Grid semantics require row structure the day-picker does
    not have (axe aria-required-children/aria-required-parent).
  • display.Carousel's scroll-snap track is keyboard-focusable (tabindex
    • "Slides" label), satisfying axe scrollable-region-focusable for the
      horizontally scrolling element itself.
  • display.StatCard trend colors meet WCAG contrast in light mode
    (text-green-700/text-amber-700 instead of -600 shades that measured
    3.2:1 on white).
  • charts/echarts.EChart renders role="img" when an AriaLabel is set —
    aria-label on a bare <div> is a prohibited attribute (axe
    aria-prohibited-attr).
  • Demo pages pass the axe sweep. Raw demo inputs/selects gained labels,
    scrollable code blocks are focusable, and in-paragraph links are underlined.
  • Tooltip dismiss/re-show scripts no longer throw on document-targeted
    events.
    The shared tooltip singleton's keydown/mouseenter/focusin
    listeners called e.target.closest(...) unguarded; a mouseenter fired on
    document itself (every time the pointer enters the window on a page with
    a tooltip) threw a console TypeError. All three now route through a
    target guard.