Skip to content

v0.2.1

Choose a tag to compare

@github-actions github-actions released this 08 Aug 12:59
· 129 commits to main since this release
3dfd789

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), the postal-jp mask feeding the
    postal-address lookup with OOB autofill, debounced draft autosave, the
    unsaved-changes guard (badge on data-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-infinite recipe — 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 an aria-live end-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). revealed is a window-viewport trigger (it does not fire in
    overflow containers) — the docs spell out the intersect carve-out,
    and the spec pins two-batch loading with a short viewport. API
    tests, docs en/ja, cross-engine Playwright suite.

  • csv-import recipe — 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 real row/field/message
    error <table>) plus, when importable rows exist, a confirm form
    whose hidden token references the validated batch;
    POST /imports/<token>/commit executes exactly what was validated
    (HX-Trigger toast + a items:changed event 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-views recipe — 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-include of the filter form and gets the views strip back;
    Apply is a plain GET /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 is data-hx-delete on each chip's ×; duplicate names are
    422 field-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-columns recipe — 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
    (shared cols name, GET-not-POST, target resolution, no-JS
    action+method=get parity).

  • installTime() — client-side <time> localization (theme F of
    the business-app gap analysis). Servers render UTC and a
    machine-readable datetime; the auto-installed behavior rewrites the
    visible text in the viewer's zone and language via Intl:
    data-hc-time="relative" ("3 minutes ago" / 「3 分前」,
    Intl.RelativeTimeFormat with one shared 30 s refresh interval, the
    absolute localized timestamp mirrored into title unless one
    exists), or datetime / date / time absolute rendering with
    data-hc-time-style. The datetime attribute 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 —
    Intl carries 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' }) in beforeEach,
    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 at dist/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 print rules. Everything lives in
    @media print inside the hc.utilities layer, 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 into hc.css: printing a screen layout is a product
    decision, one <link>/@import away. 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 (422 field-errors, 401 session-expiry, 409
    edit-conflict, 413/5xx toast branches) — plus the one
    consolidated htmx:beforeSwap allowance
    ([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-conflict recipe — 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 hidden version rides every
    save, a stale save answers 409 steered by HX-Retarget into 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=1 means "I saw v13 and
    chose to overwrite"; a record that moved again re-conflicts),
    Reload swaps the form outerHTML from the current record, and
    the <form method="dialog"> escape keeps editing. Both action
    buttons ride the shipped data-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-expiry recipe — 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 with HX-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
    requestConfig and replays it through htmx.ajax() when the
    login response fires hc:sessionrenewed — the user's click completes
    instead of vanishing. One slot, latest wins; a rotated CSRF token is
    picked up fresh by installCsrfHeader on replay; nothing survives a
    page load. The docs demo frame's beforeSwap allowance widens from
    422 to [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 stubbed htmx.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).

  • autosave recipe — 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:2s is
    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 …/draft returns the form re-rendered from the
    draft with data-dirty preset — draft content is unsaved by
    definition, and the guard warns from the attribute alone), and the
    interplay with installDirtyGuard: the draft's htmx:afterRequest
    has a different elt, 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-changes recipe — warn before
    edits are lost
    (PR 2 of
    plans/hc-form-safety-plan-en.md;
    the 29th recipe, client-only). data-hc-dirty-guard on a form:
    baseline snapshot on first focus (via new FormData(form), so
    installFormat's canonical wire values mean display regrouping is
    never "dirty"), data-dirty + hc:dirtychange on real changes, the
    browser's beforeunload prompt while dirty (never during the form's
    own submission — the submit event fires only after constraint
    validation), a localized window.confirm on boosted-link navigation
    (dirtyguard.leave, en/ja), and clean-on-save keyed on
    htmx:afterRequest element identity — a draft autosaver inside
    the form deliberately does not clean the guard. The dirty check is
    attribute-driven, so a server may render data-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-address recipe — 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 an aria-live hint slot
    plus out-of-band outerHTML re-renders of the address inputs
    (stable ids keep the label associations; autocomplete tokens keep
    browser autofill alive). Multiple hits come back as candidate buttons
    re-calling with &choice=<n>; not-found is a hint; malformed is
    422 through the standard allowance. No new JavaScript and no new
    public API — the mask shipped in #471; this PR is contract +
    composition. Ships as recipes/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-mask renders 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 same formdata hook installFormat uses. No maxlength is
    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 the formdata event, which fires
    for both the htmx path (new FormData(form)) and the native submit.
    data-decimals pads (never rounds); data-locale overrides 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-phase change listener so htmx triggers read
    the normalized value, with the same formdata safety 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 (synthetic formdata — 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 to default and self-heals on the next pick.

Security

  • Range-scoped pnpm overrides floor two vulnerable transitive
    dev-toolchain deps flagged by Dependabot: nanoid ≥ 3.3.17
    (GHSA-2v37-7h3g-55p8, via postcss) and js-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.