Repository navigation
v0.2.1
The business-app release: seven themes from the 2026-08-08
line-of-business gap analysis
(plans/hc-input-format-plan-en.md,
plans/hc-form-safety-plan-en.md,
plans/hc-error-paths-plan-en.md,
plans/hc-datagrid-ops-plan-en.md),
PRs #467–#485 — six new behaviors (53 total), nine new recipes
(36 total, five of them zero-new-JS contracts), the opt-in print
stylesheet, the errors-and-recovery map, and the data-entry template.
Strictly additive → patch per VERSIONING.md. The CLI ships 0.4.1 to
re-bundle the nine new recipes.
Added
-
Docs:
templates/data-entry— the business-form stack in one
page (theme G, the closing piece of the 2026-08-08 business-app
gap analysis). A third full-page template (en/ja + sidebar) whose
live preview composes everything the effort shipped into one guarded
form: grouped amounts with raw wire values (installFormat), IME
normalization (installNormalize), thepostal-jpmask feeding the
postal-address lookup with OOB autofill, debounced draft autosave, the
unsaved-changes guard (badge ondata-dirty), double-submit hygiene
(data-hx-sync+data-hx-disabled-elt), and the shared
error-dialog host for session-expiry/edit-conflict. The page
skeleton, a nine-row wiring map (region → behavior → recipe →
contract), and an adapt-it list (version field, real CSRF,
server-side validation, draft hygiene) make it the entry point the
errors-and-recovery and form-safety themes plug into. -
datagrid-infiniterecipe — revealed-sentinel cursor paging
(PR 5, final, of
plans/hc-datagrid-ops-plan-en.md;
the 36th recipe, zero new JS — the datagrid-ops theme closes with
four recipes and no new public API at all). The last row is a
sentinel —data-hx-trigger="revealed"+data-hx-swap="outerHTML"
— replaced by the next<tr>batch plus the next sentinel; cursors
(?after=<id>), not offsets, so append-only lists never shift; the
end of the list is a batch with anaria-liveend-marker row
("15 of 15") instead of a sentinel; stale cursors resume from the
nearest stable point (200, never a 4xx — scrolling is not an
error).revealedis a window-viewport trigger (it does not fire in
overflow containers) — the docs spell out theintersectcarve-out,
and the spec pins two-batch loading with a short viewport. API
tests, docs en/ja, cross-engine Playwright suite. -
csv-importrecipe — upload, validate, confirm (PR 4 of
plans/hc-datagrid-ops-plan-en.md;
the 35th recipe, zero new JS). The missing bulk-in wire contract, in
two honest phases: the file-upload form posts
the CSV and the server validates without importing — the response
is a report fragment (summary line + a realrow/field/message
error<table>) plus, when importable rows exist, a confirm form
whose hiddentokenreferences the validated batch;
POST /imports/<token>/commitexecutes exactly what was validated
(HX-Triggertoast + aitems:changedevent for data-region
pairing), tokens are single-shot (409+ re-upload hint when
expired/consumed — riding the consolidated allowance), re-uploading
replaces the batch, and no-JS is a full-page PRG report. The demo
parses a real tiny CSV (comma, quoted fields,\r\n?) and threads
the batch statelessly through the token (base64url — documented as
the demo trick; real apps hold server-side batches). API tests, docs
en/ja, cross-engine Playwright suite incl. the stale-token 409
branch. -
saved-viewsrecipe — named filter sets as plain links (PR 3 of
plans/hc-datagrid-ops-plan-en.md;
the 34th recipe, zero new JS). The current search's querystring,
named and kept server-side: Save posts a small name form with
data-hx-includeof the filter form and gets the views strip back;
Apply is a plainGET /items?view=<name>link — bookmarkable,
shareable, zero client state — whose response renders the filter
form with the view's values filled, so a view is never opaque;
Delete isdata-hx-deleteon each chip's ×; duplicate names are
422field-errors. Stateless live demo (the demo threads views
through the strip's hidden inputs and says so — real apps store
per-user), docs en/ja, API tests, cross-engine Playwright suite,
checks.json (include-resolves, target-declared, no-JS post parity,
labeled name input). -
datagrid-columnsrecipe — the server-owned column chooser
(PR 2 of
plans/hc-datagrid-ops-plan-en.md;
the 33rd recipe, zero new JS). A filter-popover-shell chooser with
one checkbox per column; Apply GETs the grid URL with repeated
cols=params and the server re-renders the whole grid with exactly
those columns (canonical order; unknown names ignored — the server
is the schema) plus the chooser re-rendered out of band with
matching checked states. No client column-hiding: the server
deciding which columns exist means one round trip, zero state
drift, and print/export match the screen for free (CSV export is a
plain link — documented, not a recipe). Stateless live demo + API
tests, docs en/ja, cross-engine Playwright suite, checks.json
(sharedcolsname, GET-not-POST, target resolution, no-JS
action+method=getparity). -
installTime()— client-side<time>localization (theme F of
the business-app gap analysis). Servers render UTC and a
machine-readabledatetime; the auto-installed behavior rewrites the
visible text in the viewer's zone and language viaIntl:
data-hc-time="relative"("3 minutes ago" / 「3 分前」,
Intl.RelativeTimeFormatwith one shared 30 s refresh interval, the
absolute localized timestamp mirrored intotitleunless one
exists), ordatetime/date/timeabsolute rendering with
data-hc-time-style. Thedatetimeattribute is never touched (the
wire truth), the server-rendered text is the no-JS fallback,
unparseable values are left alone, the language follows the closest
[lang], and htmx-swapped content localizes automatically (the
avatar/nav-current MutationObserver pattern). No message keys —
Intlcarries the language. Documented in the i18n fundamentals
("Localized timestamps", en/ja) + the behaviors roster (now 53);
pinned by a 7-test jsdom suite (fake timers + system time; locale
following; observer pickup; interval refresh; uninstall stops the
clock).
Fixed
-
Browser specs: dialog-opening axe scans emulate reduced motion
— the seven specs this effort added now set
page.emulateMedia({ reducedMotion: 'reduce' })inbeforeEach,
matching the #342 house pattern, after a CI-only recurrence of the
dialog-open contrast flake (PR #479's chromium job; same commit
passed locally and on rerun). -
hc.print.css— opt-in print stylesheet (theme E of the
business-app gap analysis;@hypermedia-components/core/css/print,
also atdist/hc.print.css). Business pages get printed — the order
detail for the courier, the search result for the meeting — and until
now the kit had zero@media printrules. Everything lives in
@media printinside thehc.utilitieslayer, so nothing changes on
screen and on paper the rules win by layer order alone. Doctrine:
paper shows the record, not the app (shell nav/header, toolbars,
menubars, buttons, pagination, toasts, drawers, tooltips, hovercards,
command palette, skeletons, spinners, htmx indicators, and closed
dialogs hide); data survives the page (scroll areas un-clip,
sticky datagrid headers go static, truncated cells un-ellipsize,
<thead>repeats, rows/cards/fields/alerts refuse to split);
ink is honest (shadows drop, prints light regardless of
data-theme, black hairlines, status containers keep a thin border
for grayscale). State attributes stay authoritative —[hidden],
closed details/dialogs, inactive tab panels do not print. Not
concatenated intohc.css: printing a screen layout is a product
decision, one<link>/@importaway. Documented at
fundamentals/print(en/ja, sidebar entry). -
Docs:
fundamentals/errors— the errors & recovery map (PR 2 of
plans/hc-error-paths-plan-en.md,
landed last so its links resolve; the plan's Status records the
reorder). One page (en/ja) turning the scattered failure branches
into a single table — status → what the user sees → which recipe
owns it (422field-errors,401session-expiry,409
edit-conflict,413/5xxtoast branches) — plus the one
consolidatedhtmx:beforeSwapallowance ([401, 409, 422]) the
per-recipe pages repeat in one-status form, the two doctrines
(server-narrated errors; 200-with-truth for domain outcomes), and
double-submit hygiene (data-hx-sync="this:abort"+
data-hx-disabled-elt+ indicator — with the "hand-rolled disabling
gets the error path wrong" warning). Session-expiry and
edit-conflict pages gain their deferred cross-links (en/ja). -
edit-conflictrecipe — optimistic locking via a hidden version
(PR 4, final, of
plans/hc-error-paths-plan-en.md;
the 32nd recipe). Two people open the same record; the slower save
must not silently eat the faster one: a hiddenversionrides every
save, a stale save answers409steered byHX-Retargetinto the
same shared error-dialog host session-expiry uses, and the conflict
dialog — a real theirs/yours<table>— finishes the flow through
ordinary htmx: Overwrite re-submits the user's fields plus the
dialog's fresh hidden version (force=1means "I saw v13 and
chose to overwrite"; a record that moved again re-conflicts),
Reload swaps the formouterHTMLfrom the current record, and
the<form method="dialog">escape keeps editing. Both action
buttons ride the shippeddata-hc-close-dialog-on-success.
Zero new JavaScript, zero new public API — hidden field +
headers + shipped machinery. Live demo (v13-pinned stateless module,
5 API tests), docs (en/ja), and a cross-engine Playwright suite
(conflict dialog, overwrite-wins-and-closes, reload-discards,
keep-editing leaves the stale version, axe with the dialog open). -
installSessionExpiry()+session-expiryrecipe — 401 → login
dialog → request replay (PR 3 of
plans/hc-error-paths-plan-en.md;
the 31st recipe). An expired session turns any interrupted action
into a login<dialog>— the server steers it withHX-Retarget
into the shared[data-hc-remote-dialog-root]host (shipped
machinery opens it), the page-level allowance lets the 401 fragment
swap, and the new ~50-line bridge remembers the interrupted
requestConfigand replays it throughhtmx.ajax()when the
login response fireshc:sessionrenewed— the user's click completes
instead of vanishing. One slot, latest wins; a rotated CSRF token is
picked up fresh byinstallCsrfHeaderon replay; nothing survives a
page load. The docs demo frame's beforeSwap allowance widens from
422to[401, 409, 422](the consolidated shape the error-paths
theme documents). Roster now 52; pinned by a 7-test jsdom suite
(capture/replay/single-slot/host-gating via a stubbedhtmx.ajax)
and a cross-engine Playwright suite whose keystone asserts the
replay: click → 401 dialog → sign in → the approval completes by
itself (cookie-session mock; wrong-password 422 re-renders the
dialog with field-errors). -
autosaverecipe — debounced drafts + restore banner (PR 3,
final, of
plans/hc-form-safety-plan-en.md;
the 30th recipe). A request-owning<div>inside the form posts the
whole form as a draft —input from:closest form changed delay:2sis
the entire debounce loop, zero new JavaScript / zero new public
API. The contract fixes the draft endpoints (drafts stored raw and
never validated; password-type fields dropped server-side), the
restore banner (GET …/draftreturns the form re-rendered from the
draft withdata-dirtypreset — draft content is unsaved by
definition, and the guard warns from the attribute alone), and the
interplay withinstallDirtyGuard: the draft'shtmx:afterRequest
has a differentelt, so a draft save deliberately does not clean
the guard — only the record save does. Ships with a stateless live
demo (demo-api module + 5 tests), docs (en/ja), and a cross-engine
Playwright suite pinning "a typing burst produces exactly one
debounced draft post" via a fixture-side request counter. -
installDirtyGuard()+unsaved-changesrecipe — warn before
edits are lost (PR 2 of
plans/hc-form-safety-plan-en.md;
the 29th recipe, client-only).data-hc-dirty-guardon a form:
baseline snapshot on first focus (vianew FormData(form), so
installFormat's canonical wire values mean display regrouping is
never "dirty"),data-dirty+hc:dirtychangeon real changes, the
browser's beforeunload prompt while dirty (never during the form's
own submission — thesubmitevent fires only after constraint
validation), a localizedwindow.confirmon boosted-link navigation
(dirtyguard.leave, en/ja), and clean-on-save keyed on
htmx:afterRequestelement identity — a draft autosaver inside
the form deliberately does not clean the guard. The dirty check is
attribute-driven, so a server may renderdata-dirty(a restored
draft) and the guard warns before the form is ever focused.
Auto-installed; behaviors roster now 51; pinned by a 9-test jsdom
suite and a cross-engine Playwright spec (real typing, style hook,
clean-on-save via the mock, Chromium-only real beforeunload dialog). -
postal-addressrecipe — postal-code → address autofill (PR 4 of
plans/hc-input-format-plan-en.md;
the 28th recipe). The masked postal input
(data-hc-mask="postal-jp") makes the trigger guard exact —
change[target.value.length==8]fires once, on a complete code — and
the server answers with a status line for anaria-livehint slot
plus out-of-bandouterHTMLre-renders of the address inputs
(stable ids keep the label associations;autocompletetokens keep
browser autofill alive). Multiple hits come back as candidate buttons
re-calling with&choice=<n>; not-found is a hint; malformed is
422through the standard allowance. No new JavaScript and no new
public API — the mask shipped in #471; this PR is contract +
composition. Ships asrecipes/postal-address/with a live demo
(stateless demo-api module + tests), a docs page (en/ja), and a
cross-engine Playwright suite (mask-guarded trigger, OOB fill, the
candidate pick, no-match, axe with the candidate list open). -
installMask()— declarative fixed-format input masks (PR 3 of
plans/hc-input-format-plan-en.md).
data-hc-maskrenders codes as the user types:#/a/A/*
pattern tokens plus literal characters (postal-jp=###-####),
NFKC entry so fullwidth digits fill slots, lazy literals (1234→
123-4), caret-preserving re-renders, and Backspace/Delete that hop a
literal run and always consume a raw character. The wire value is the
displayed canonical form;data-hc-mask-submit="raw"strips literals
via the sameformdatahook installFormat uses. Nomaxlengthis
imposed — the render caps at the mask, so pastes like〒123-4567
clean instead of truncating. Auto-installed; documented on the input
page (en/ja) and the behaviors roster (50); pinned by an 11-test
jsdom suite (caret math, literal hops, submit modes) and a
cross-engine Playwright spec (real typing, caret position, raw wire
snapshot, axe). -
installFormat()/installNormalize()— business-form input
hygiene (PR 2 of
plans/hc-input-format-plan-en.md).
data-hc-format="number"turns a plain text input into an amount
field: fullwidth digits normalize on blur (NFKC) and the display
groups per locale (1,234,567), focus shows the raw value again (no
caret management anywhere), and the wire value stays raw — the
behavior rewrites the entry list in theformdataevent, which fires
for both the htmx path (new FormData(form)) and the native submit.
data-decimalspads (never rounds);data-localeoverrides the
grouping locale.data-hc-normalize="ascii | kana"self-corrects IME
leftovers on commit (fullwidth ASCII → halfwidth, ideographic space →
space; halfwidth kana → fullwidth, hiragana → katakana for furigana
fields) in a capture-phasechangelistener so htmx triggers read
the normalized value, with the sameformdatasafety net. Both are
auto-installed, idempotent, and return uninstallers; no new events,
CSS, or i18n keys. Documented on the input page (grouped amounts +
IME normalization, en/ja) and the behaviors roster (now 49); pinned
by a jsdom suite (syntheticformdata— jsdom lacks the event) and a
cross-engine Playwright spec whose fixture snapshots
new FormData(form)to prove raw-on-the-wire in real browsers.
Fixed
- Docs: the Display settings Color picker follows the accent
pentagon — the header popover still offered the pre-0.2.0 accent
names (indigo / emerald / rose / amber), which core 0.2.0 no longer
ships, so picking them did nothing. The picker, its localStorage
whitelist, and the pre-paint head script now list
teal / lime / orange / fuchsia; a stored pre-0.2.0 value falls
back todefaultand self-heals on the next pick.
Security
- Range-scoped
pnpmoverrides floor two vulnerable transitive
dev-toolchain deps flagged by Dependabot:nanoid≥ 3.3.17
(GHSA-2v37-7h3g-55p8, via postcss) andjs-yaml≥ 4.3.1
(GHSA-5p4m-2wfm-xmqj, via astro/stylelint). Build/lint/docs tooling
only — nothing shipped in the published packages; the overrides
self-retire once the parent ranges resolve at or above the floors.
Full details in CHANGELOG.md.