Repository navigation
v0.4.0
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.jsonat build
time (Count.astro), replacing hand-typed numbers that had
rotted to "25 recipes"; - component pages gain a generated Used in recipes list
(UsedInRecipes.astroinverts 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.
- the Recipes sidebar and index share nine goal groups (Forms /
-
fundamentals/audit-trailguide — who changed what, when, as
doctrine: the appended row (actorfrom 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-timedates,
lazy-panel loading,beforeendload-more, hc-code diffs on
demand). Docs-only, EN + ja; the fundamentals reading-order list
also regains its missingprintanderrorsbullets (docs drift).
(plan) -
templates/confirm-page— the 入力 → 確認 → 完了 flow as a full-page
template: one#flowregion with whole-regionouterHTMLswaps
(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, neverhistory.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 missingdata-entryrow (docs drift).
(plan) -
network-retryrecipe +installNetworkRetry()— surface the
request that got no answer at all (offline / dropped socket /
declareddata-hx-requesttimeout), the one error with no server
response to narrate with:htmx:sendError/htmx:timeoutrender
a retryhc-alert(role="status", i18n keysnetworkRetry.failed
/networkRetry.retry, per-hostdata-hc-network-retry-message/
-labeloverrides) into the client-owned[data-hc-network-retry]
host — one slot latest-wins, re-rendered never stacked. Retry
re-issues throughhtmx.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-badgerecipe — the notification count in app chrome as a
wire contract: the fragment is the nav item (badge + accessible
name change in the same swap; thehc-badgeisaria-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-unreadis a contract
marker only. Zero new JS/CSS; live demo + demo API included.
(plan) -
async-jobrecipe — one contract for work that outlives a request
(CSV exports, PDF rendering, batch imports): the kick-off answers
202with 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-itemsrecipe — 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 +outerHTMLswap, 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-lookuprecipe — the master-reference field (F4-style):
a visible*_codeinput validated onchange, 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-actionsrecipe — 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-regionouterHTMLswaps keep state / version /
stepper / buttons in lockstep, comment-required transitions422
with the field rendered only then, and stale or illegal transitions
409with the region re-rendered from current truth. Zero new
JS/CSS; live demo + demo API included.
(plan) -
idempotency-keyrecipe — 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
real422conflict, 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-caprecipe — bound what one search may return:LIMIT cap+1
detection, "cap+" counts, and a persistent truncation banner
(hc-alertwarning,role="status", markeddata-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-pagerrecipe — 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 withdatagrid-bulk-actions
(keysvsidsnaming rule) andresult-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-colortoken actually applies: the
glyph now followsdata-coloraccents (it was sRGB blue-600 under
every theme) and renders inSelectedItemunder forced colors. -
installSortabledrags now feel like drags: the item in flight
tracks the pointer and gets a default shadow lift (with
grab/grabbingcursors 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
documenteddata-dragging/data-grabbedhooks 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 aMapof 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'swindow
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 sharedlifecycle.jshelper now
prunes disconnected instances (running their detachers) whenever a
mutation batch removes nodes, wired into all 26 Map-keeping
behaviors plusinstallNavCurrent'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):installMenubarre-wires the
cross-menu ←/→ switch when a dropdown is replaced or a new top
item arrives;installContextMenure-resolves a replaced (or
late-arriving) id-referenced menu and no-ops instead of throwing
on a disconnected one;installSpyrebuilds its
IntersectionObserver when a tracked section or link is swapped
(a detached section's zero rect was corrupting the active pick);
installCarouselre-stamps slide ARIA and regenerates its dots
when the slide set changes under a surviving viewport;
installSplitterre-wires a replaced handle.
- Departed instances are now detached and forgotten. Every
- 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:shouldCancelonly
exempts a form whose ownmethodisdialog(the submitter's
formmethodis never consulted), so the native close is
preventDefaulted — andissueAjaxRequestthen silently refuses a
non-HTTPformmethod, 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
formattribute (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"):installDatagridnever attached to a grid whose table arrives
by swap — an empty.hc-datagridshell 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-datagridnodes,
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:datagridsortlistener — with the attach fix in place a click
would have cycledaria-sortwhile 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-includeon the grid), and checks.json guards it. installMenuhad 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
throwingInvalidStateErrorwhen a stale attachment fires
showPopover()on a node a swap already removed.- Unhandled promise rejections:
installSessionExpiry's 401
replay now swallows thehtmx.ajaxrejection the same way
installNetworkRetrydoes (#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-syncmade 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'sdata-hx-disinheritgainshx-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-valsnow carriesdata-hx-disinherit="hx-vals"—
descendant buttons (Overwrite / Discard / Cancel) inherited an
expression written for thehc:datagrideditCustomEvent; on a
click it evaluates against a MouseEvent (strayversionparam
today, aReferenceErrorthe moment a trigger modifier such as
delay:orhx-confirmdetaches the evaluation from the event).
Scaffolds, demo handlers and fences (en + ja). - Phantom classes removed (the
hc-listtreatment): the
datagrid-prefs scaffolds' SR status region worehc-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'shc-pagination__ellipsis
(now a barearia-hiddenspan). All en + ja. - blocks stat tiles double-padded: the dashboard/report tiles put
bare children directly under.hc-cardwith an inline
padding:1rem, so the loose-content rule padded every child again
(~2 rem effective, the.3remstack gap drowned). The tiles now
usehc-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) andelements(format / mask /
multi-value iterateform.elements) are fatal, whileaction/
methodare 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.
- data-entry template: inherited
- 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
EventSourcevia a/scopefragment 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
withoutdata-sideopened at (0,0) — including the popover
component page's own demo. The base margin is nowauto; 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@layerrules —
the same counter it already carried for modal dialogs.) - Core behavior:
installPopoverwired 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 inlineanchor-nameand 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 withdata-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.
- Core CSS:
- 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-sidepopovers 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.ajaxrejects 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; withbox-sizing: border-boxthe 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__bodystack (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.removebecame
the button, htmx'souterHTMLswap 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 bitessubmit,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-bginstead 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 422beforeSwapallowance (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 regainedhc-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 thedata-hx-swap-oobspelling.
- Core: the tabs overflow chevrons read an undefined
Full details in CHANGELOG.md.