Skip to content

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 30 Aug 14:16
· 16 commits to main since this release
31d590e

The post-data-grid consolidation release, PRs #578–#602: the
business-flow recipe programmes (result-cap / snapshot-pager, the
contracts five — async-job / line-items / reference-lookup /
workflow-actions / idempotency-key — and the gap follow-ups incl.
installNetworkRetry(), the 58th behavior, and the confirm-page
template), the docs information-architecture overhaul, and the
demo-quality + class-wide QA audits (#595–#602) with their core
hardening. 53 recipes · 5 templates · 58 behaviors.

Minor rather than patch for two flagged default changes, per
VERSIONING.md (both under Changed below): installSortable now
ships a default drag feel (pointer-tracking lift, sibling FLIP,
motion-safe), and the multicombobox check mark is mask-painted so it
follows the accent instead of a hardcoded blue. Everything else is
strictly additive or a fix.

The CLI goes to 0.4.3 to re-bundle the recipe set (nine new recipes
since 0.4.2 plus the audited scaffolds — its own source is unchanged;
prepack syncs the repo-root recipes/, so new recipes need a
republish to reach hc add). editor-kit has no shipped change and
stays at 0.2.0.

Added

  • Docs information-architecture overhaul
    (plan) — findability work at the
    68-component / 53-recipe scale, no URL moves:

    • the Recipes sidebar and index share nine goal groups (Forms /
      Form safety / Actions / Business flows / Data grid / Search &
      filter / Loading & regions / Server push & chat / Overlays &
      notifications) instead of a flat alphabetical 53-entry list;
    • recipes/datagrid — the Data grid guide, mapping the
      component, the page template, and the thirteen grid recipes in
      build order (display → operate → edit → bulk & scale);
    • landing-page counts are generated from manifest.json at build
      time (Count.astro), replacing hand-typed numbers that had
      rotted to "25 recipes";
    • component pages gain a generated Used in recipes list
      (UsedInRecipes.astro inverts the recipe pages' component links
      per locale — 28 pages today, self-updating);
    • the Tokens section is relabelled Theming (ja: テーマ) — URLs
      unchanged — ending the collision with Fundamentals → Tokens;
    • Fundamentals splits into Core concepts (six must-reads) and
      Deep dives; Integrations is ordered by journey (htmx →
      plain-html → frameworks); sidebar groups are collapsed at rest;
    • a search-synonym sweep adds a uniform Also known as / 別名 line
      to 86 component and recipe pages (both locales), so Pagefind
      matches modal / snackbar / typeahead / 楽観ロック / 二重送信防止
      and the rest of the names users actually type.
  • fundamentals/audit-trail guide — who changed what, when, as
    doctrine: the appended row (actor from the session never the
    form, domain verb, server clock, request_id, one-sentence
    summary, changed-fields before → after), write points mapped to
    the shipped contracts (409 losers and 422s write nothing; bulk
    writes per-row + batch; undo-delete writes delete and restore;
    idempotency-key replays write no second entry — the trail is
    exactly-once because commits are), append-only with corrections as
    new entries, and the hc-timeline read side (data-hc-time dates,
    lazy-panel loading, beforeend load-more, hc-code diffs on
    demand). Docs-only, EN + ja; the fundamentals reading-order list
    also regains its missing print and errors bullets (docs drift).
    (plan)

  • templates/confirm-page — the 入力 → 確認 → 完了 flow as a full-page
    template: one #flow region with whole-region outerHTML swaps
    (the multi-step-form shape), an input step that 422s into field
    errors, a review step the server renders from what it parsed
    (the read-back is the point) with the values as hidden fields and
    an idempotency key minted at the review render, a named-button
    Back (formnovalidate, never history.back()), and a done step
    whose double submit replays the same receipt. Zero new JS/CSS;
    live demo + dedicated demo API + tests. The templates index also
    gains its missing data-entry row (docs drift).
    (plan)

  • network-retry recipe + installNetworkRetry() — surface the
    request that got no answer at all (offline / dropped socket /
    declared data-hx-request timeout), the one error with no server
    response to narrate with: htmx:sendError / htmx:timeout render
    a retry hc-alert (role="status", i18n keys networkRetry.failed
    / networkRetry.retry, per-host data-hc-network-retry-message /
    -label overrides) into the client-owned [data-hc-network-retry]
    host — one slot latest-wins, re-rendered never stacked. Retry
    re-issues through htmx.ajax(verb, path, { source }) with no
    values override (a fresh attempt re-collecting current inputs, so
    an idempotency-key field rides along unchanged — the marquee
    pairing); any real response on the failed element clears the
    banner, the failure's own status 0 never does; never auto-retries.
    The fundamentals/errors map gains its "(no response)" row. Live
    demo + demo API included.
    (plan)

  • unread-badge recipe — the notification count in app chrome as a
    wire contract: the fragment is the nav item (badge + accessible
    name change in the same swap; the hc-badge is aria-hidden
    presentation), it polls itself (the async-job self-swap rule, so
    the server owns the cadence), zero renders no badge, past the cap
    display and name both say "more than N", the fragment is never a
    live region, and every response that changes unread state ships
    the corrected fragment out-of-band. data-hc-unread is a contract
    marker only. Zero new JS/CSS; live demo + demo API included.
    (plan)

  • async-job recipe — one contract for work that outlives a request
    (CSV exports, PDF rendering, batch imports): the kick-off answers
    202 with a job card that polls itself (data-hx-target="this" +
    outerHTML, so terminal cards stop polling by carrying no trigger
    and the server owns the poll cadence), with enumerated terminal
    states (done / failed / cancelled / expired-as-tombstone) and
    no-op-200 cancel. Zero new JS/CSS; live demo + demo API included.
    (plan)

  • line-items recipe — the order/quote/invoice detail table as a
    hypermedia contract: rows align positionally by repeated names
    (tree-order serialization), add/remove/recalculate are the same
    whole-form POST + outerHTML swap, the server owns all arithmetic
    (totals render "—" while any row is invalid), and 422 echoes the
    bad raw value back. Zero new JS/CSS; live demo + demo API included.
    (plan)

  • reference-lookup recipe — the master-reference field (F4-style):
    a visible *_code input validated on change, a hidden *_id
    (opaque token) as the submitted identity, a remote-dialog +
    live-search picker whose rows re-render the whole field, inactive
    masters rendered visible-but-refused, and the rule that an
    unresolved code always clears the id. Zero new JS/CSS (composes
    installRemoteDialog()/installCloseDialog()); live demo +
    demo API included.
    (plan)

  • workflow-actions recipe — a record's lifecycle as a
    server-rendered actions region: the server renders only the legal
    transitions (can-never = not rendered; could-but-not-now =
    aria-disabled + reason), the verb is the button and the version
    rides along, whole-region outerHTML swaps keep state / version /
    stepper / buttons in lockstep, comment-required transitions 422
    with the field rendered only then, and stale or illegal transitions
    409 with the region re-rendered from current truth. Zero new
    JS/CSS; live demo + demo API included.
    (plan)

  • idempotency-key recipe — server-side duplicate-submit defence:
    one fresh opaque key per rendered form, first commit stores
    key → (request-hash, response), a replayed key gets the original
    response back (never an error), same-key-different-payload is a
    real 422 conflict, and validation failures leave the key live so
    the corrected resubmit can commit. Composes with PRG, async-job
    kick-offs, workflow transitions, and session-expiry replays. Zero
    new JS/CSS; live demo + demo API included.
    (plan)

  • result-cap recipe — bound what one search may return: LIMIT cap+1
    detection, "cap+" counts, and a persistent truncation banner
    (hc-alert warning, role="status", marked data-hc-result-cap) or
    a documented hard-reject mode for process-everything work queues.
    Zero new JS/CSS; live demo + demo API included.
    (plan)

  • datagrid-snapshot-pager recipe — freeze a work queue's membership
    at search time: the form carries every hit's opaque row key
    (name="keys", tree-order serialization), paging POSTs the whole
    ordered list and the server slices; processed rows stay visible as
    processed, vanished rows render as tombstones, and page boundaries
    never shift under the user. Composes with datagrid-bulk-actions
    (keys vs ids naming rule) and result-cap (the cap bounds the
    snapshot). Zero new JS/CSS; live demo + demo API included.
    (plan)

  • hc-popover__body / hc-popover__footer — the structure parts the
    datagrid filter / sort / column panels already used are now real:
    a stacked, fieldset-resetting body (--hc-popover-body-gap) and an
    end-aligned action row (--hc-popover-footer-gap; separation via
    padding, so unlayered host margin-resets can't collapse it).
    Documented on the popover page with the panel pattern.

Changed

  • The multicombobox selected check mark is painted through a CSS mask
    instead of a hardcoded-color data-URI image, so the documented
    --hc-multicombobox-option-check-color token actually applies: the
    glyph now follows data-color accents (it was sRGB blue-600 under
    every theme) and renders in SelectedItem under forced colors.

  • installSortable drags now feel like drags: the item in flight
    tracks the pointer and gets a default shadow lift (with
    grab/grabbing cursors on the handle), displaced siblings
    FLIP-slide into their new slots (keyboard moves too), and the drop
    settles the item into place. The motion rides
    --hc-motion-duration-fast, skips entirely under
    prefers-reduced-motion, and never touches the DOM order — reorder
    decisions use layout geometry with transforms subtracted, so
    mid-animation rects cannot make the slot math oscillate. The
    documented data-dragging / data-grabbed hooks stay open for
    overrides.

Fixed

  • Behavior lifecycle hardening — the two classes the swap-resilience
    audit (#600) deliberately deferred:
    • Departed instances are now detached and forgotten. Every
      install observer kept a Map of live attachments and only ever
      handled ADDED nodes: an instance swapped out of the document
      stayed in the Map forever — its detacher never ran, so listeners
      on shared targets stayed registered (the datagrid's window
      hashchange listener and its shared overflow-tooltip node, the
      splitter's document-level pointermove/up) and the Map pinned the
      whole detached subtree against garbage collection, growing without
      bound on long-lived pages. A shared lifecycle.js helper now
      prunes disconnected instances (running their detachers) whenever a
      mutation batch removes nodes, wired into all 26 Map-keeping
      behaviors plus installNavCurrent's container set. Elements that
      merely move in the same batch are left alone.
    • The remaining once-cached behaviors rebind after inner swaps
      (the #600 stale-detection pattern): installMenubar re-wires the
      cross-menu ←/→ switch when a dropdown is replaced or a new top
      item arrives; installContextMenu re-resolves a replaced (or
      late-arriving) id-referenced menu and no-ops instead of throwing
      on a disconnected one; installSpy rebuilds its
      IntersectionObserver when a tracked section or link is swapped
      (a detached section's zero rect was corrupting the active pick);
      installCarousel re-stamps slide ARIA and regenerates its dots
      when the slide set changes under a surviving viewport;
      installSplitter re-wires a replaced handle.
  • Data-grid page template: the Filters panel's Cancel button did
    nothing
    (user report — the dialog would not close, and the record
    editor's Cancel was dead the same way). Cancel was a
    formmethod="dialog" submit button inside the htmx-enhanced filter
    form, and htmx kills that idiom twice over: shouldCancel only
    exempts a form whose own method is dialog (the submitter's
    formmethod is never consulted), so the native close is
    preventDefaulted — and issueAjaxRequest then silently refuses a
    non-HTTP formmethod, so no request is issued either. The template
    now follows the remote-dialog contract's idiom everywhere: the
    footer sits outside the form, Cancel is its own
    <form method="dialog">, and Apply / Save reach the form via the
    form attribute (demo, record fragment, page fences and prose,
    en + ja). Escape always worked; Cancel now matches it.
  • Swap-resilience hardening across the behaviors, from a class-wide
    audit of the #596–#598 defect patterns ("wired once at attach,
    broken by the next htmx swap"):
    • installDatagrid never attached to a grid whose table arrives
      by swap — an empty .hc-datagrid shell filled by
      hx-trigger="load" + hx-swap="innerHTML" (the datagrid-sort /
      -columns / -filter demos' exact shape) bailed at attach time and
      the install observer only watched for added .hc-datagrid nodes,
      never for content arriving inside one. On those demos keyboard
      navigation, header sorting, selection events and sticky
      measurement were all dead. The observer now resolves the owning
      grid of every added node and (re)binds whenever the grid's table
      is not the one the attachment bound — which also repairs the
      wholesale table replacement every sort / filter response performs.
    • The datagrid-sort header fast path was never wired: the
      contract promises a header click mirrors the wire into
      input[data-hc-datagrid-sort] and returns the sorted page, but
      neither the scaffolds nor the demo carried that input or any
      hc:datagridsort listener — with the attach fix in place a click
      would have cycled aria-sort while sorting nothing. The scaffolds,
      demo and contract now ship the pair (a hidden wire input outside
      the panel form + data-hx-trigger="hc:datagridsort" +
      data-hx-include on the grid), and checks.json guards it.
    • installMenu had the pre-#597 popover defect: anchor-name and
      ARIA were written onto the trigger once, at attach — a trigger
      re-rendered out of band (saved-views' applied-view label) lost
      them and the menu opened unanchored. The current trigger is now
      re-resolved and re-wired on every open, and the open also
      re-stamps [autofocus] and re-wires submenus after an item-list
      re-render (previously: no initial focus, submenus dead).
    • installNavmenu / installTooltip / installHovercard
      rebind when a swap replaces their triggers (or panels) inside a
      surviving root — each attachment now knows when it is stale and
      the install observers watch for re-rendered triggers, not only
      for added roots. Tooltip and hovercard also no-op instead of
      throwing InvalidStateError when a stale attachment fires
      showPopover() on a node a swap already removed.
    • Unhandled promise rejections: installSessionExpiry's 401
      replay now swallows the htmx.ajax rejection the same way
      installNetworkRetry does (#596), and the datagrid's range-copy
      clipboard.writeText() failure (permission / focus) is a
      graceful no-op instead of an uncaught rejection.
  • Docs & recipe sweep for the remaining #595/#596-class residue, from
    a class-wide audit:
    • data-entry template: inherited hx-sync made the form a
      single-request mutex.
      data-hx-sync="this:abort" on the form is
      inherited by the postal lookup and the autosave region, so all
      three request sources shared one in-flight slot — a Save clicked
      during the 2 s draft tick was silently dropped (no request, no
      error). #596's data-hx-disinherit gains hx-sync (demo,
      skeleton fences, en + ja); browser-verified with the draft POST
      held in flight while Save goes through.
    • datagrid-edit-conflict / -edit-errors: the record tbody's
      js: hx-vals now carries data-hx-disinherit="hx-vals"
      —
      descendant buttons (Overwrite / Discard / Cancel) inherited an
      expression written for the hc:datagridedit CustomEvent; on a
      click it evaluates against a MouseEvent (stray version param
      today, a ReferenceError the moment a trigger modifier such as
      delay: or hx-confirm detaches the evaluation from the event).
      Scaffolds, demo handlers and fences (en + ja).
    • Phantom classes removed (the hc-list treatment): the
      datagrid-prefs scaffolds' SR status region wore hc-visually-hidden
      — a class that does not exist (the utility is .hc-sr-only), so
      the "Saved" announcements rendered visible; also
      hc-alert__description → hc-alert__body (editor-kit demo),
      hc-form (dialog/field fences + filter-popover demo), hc-search
      (live-search demo), hc-stat (layout fence), hc-card__title
      (density fence), and the pager demo's hc-pagination__ellipsis
      (now a bare aria-hidden span). All en + ja.
    • blocks stat tiles double-padded: the dashboard/report tiles put
      bare children directly under .hc-card with an inline
      padding:1rem, so the loose-content rule padded every child again
      (~2 rem effective, the .3rem stack gap drowned). The tiles now
      use hc-card__body — one density-aware padding, working gap.
    • line-items' name-shadowing caution refined (page en + ja and
      contract): every form property shadows, but what bites is who
      reads it — remove (htmx swaps) and elements (format / mask /
      multi-value iterate form.elements) are fatal, while action /
      method are read only as content attributes by htmx and this kit,
      which is why the bulk-action recipes' name="action" verb buttons
      are safe. The old blanket wording contradicted the kit's own
      canonical markup.
  • The SSE demos (sse-toast, sse-updates) were dead for anyone who
    reached them late: their scripted streams are one-shot (~15 s /
    ~23 s), start on page load, and close themselves — scroll down
    after that and nothing ever happens. Each demo now has a Replay
    the stream
    button that swaps a fresh SSE scope in (a new
    EventSource via a /scope fragment route), with the sequence
    described beside it; the end-of-stream marker now points at Replay
    instead of "reload the page".
  • Popovers no longer pin to the viewport's top-left corner. Three
    layers, found from a user report on the datagrid-sort panel:
    • Core CSS: .hc-popover { margin: 0 } clobbered the UA's
      top-layer centring (inset: 0; margin: auto), so every popover
      without data-side opened at (0,0) — including the popover
      component page's own demo. The base margin is now auto; the
      anchored paths set their own margins and are unaffected. (The
      docs site additionally re-asserts it unlayered in preview.css,
      because Starlight's universal margin reset beats @layer rules —
      the same counter it already carried for modal dialogs.)
    • Core behavior: installPopover wired the anchor to the
      trigger once, at attach time — a server-re-rendered trigger (the
      sort panel's, which displays the sort state and comes back out of
      band) lost the inline anchor-name and the aria wiring, un-
      anchoring the panel. The current trigger is now re-resolved and
      re-wired on every open.
    • Recipes/templates: the single-trigger toolbar panels
      (datagrid-sort / -columns / -prefs, filter-popover, and the
      data-grid-page demo's sort/columns panels) now anchor at their
      trigger with data-side="bottom" data-align="start" — demos,
      scaffolds, and fences (en + ja). datagrid-filter's panel
      deliberately stays a bare (browser-centred) popover — several
      controls open it, so no single anchor is right — and the page now
      says so. The vrt-overlays baselines were regenerated for the
      centred bare popover.
  • Full interactive QA pass over every live demo (72 pages driven in
    headless Chromium — all 53 recipes, the 5 templates, and the
    overlay components — with generic invariants after every step:
    console/page errors, 404/5xx, duplicate ids, open dialogs and
    popovers visible inside the viewport, data-side popovers anchored
    near their triggers, progress/meter content boxes not squashed, and
    DOM lints for the two bug classes below). Three additional defects
    found and fixed:
    • installNetworkRetry: a retry that failed again surfaced an
      uncaught promise rejection (htmx.ajax rejects with undefined) —
      the failure path is already handled by the sendError/timeout
      re-render, so the rejection is now swallowed.
    • data-entry template (demo + skeleton, en + ja): the form-level
      data-hx-disabled-elt="find button[type=submit]" was inherited
      by the postal-lookup input and the autosave div, which have no
      submit-button descendants — htmx logged an error on every lookup
      and every draft tick. The form now carries
      data-hx-disinherit="hx-disabled-elt", with the reason
      documented in the skeleton comment.
    • The docs site had no favicon at all — every page load 404ed on
      favicon.svg. Added the mark (accent square, "hc").
  • async-job: the job cards put their contents — including the
    <progress> — directly under .hc-card, so the card's
    loose-content padding landed on each child; with box-sizing: border-box the 0.5rem-tall progress became a 2rem all-padding bar
    whose content box (where the fill paints) had zero height — the bar
    looked inert at any percentage. Card contents now ride in a
    hc-card__body stack (demo, scaffolds, page fences, en + ja).
  • line-items: the remove button was name="remove", and named form
    controls shadow the form element's DOM API — form.remove became
    the button, htmx's outerHTML swap of the form threw on
    target.remove(), and the old form never left the page: every add
    / remove / recalc duplicated the whole table. Renamed to
    name="remove-row" across the demo, scaffold, contract and pages,
    with a documented caution (the same trap bites submit, action,
    method, reset, elements).
  • Demo & code quality audit (every component/recipe/template page's
    live demo and Code tab reviewed against the shipped CSS/JS and the
    demo API, both locales):
    • Core: the tabs overflow chevrons read an undefined
      --hc-tabs-tab-color (now --hc-tabs-tab-fg); keyboard focus on
      menu items now consumes the documented
      --hc-menu-item-focus-bg instead of silently reusing hover;
      hc-datagrid's error slot gained the missing
      data-tone="warning" rule so a confirmable warning row no longer
      wears the rejection palette.
    • Live demos, browser-verified: reference-lookup's searcher
      dialog no longer closes on the first search keystroke (the
      live-search form opts out of close-on-success); remote-dialog's
      422 re-opens the dialog in its error state (retargeted at the
      dialog root — the old closest-dialog outerHTML landed a closed,
      invisible dialog); row-detail's Open selected no longer
      navigates the whole page to the API URL (one 303 serves both
      paths, exactly the contract); lazy-tree's restricted branch
      answers a No-access leaf (an empty 200 could never clear
      aria-busy); bulk-errors' failure toasts are sticky
      (duration: 0) and its dead report link is explicitly
      illustrative; the docs' mobile display-settings pickers work
      (per-instance ids — five ids were duplicated on every page).
    • Docs: data-region rewritten around the contract's outerHTML
      self-replacing shape; HX-Retarget/HX-Reswap no longer presented
      as an alternative to the 422 beforeSwap allowance (they only
      steer a permitted swap); phantom classes removed everywhere
      (hc-list, hc-item__label → __title,
      hc-input-group__addon → hc-input-addon, <hc-spinner>,
      hc-code__legend, hc-form, hc-toolbar__separator); shell
      hamburger fences regained hc-shell__toggle; token tables
      reconciled with the CSS (missing calendar-range / switch-warning /
      inputotp / dialog-duration / datagrid zebra & attention / field
      applied-marker entries added; never-consumed avatar-border and
      multicombobox check-color removed; bad -sm-/-label- shorthand
      expansions fixed); label wrappers, aria wiring and
      type="button" restored across fences; scaffold contracts
      normalized to the data-hx-swap-oob spelling.

Full details in CHANGELOG.md.