# Changelog

All notable changes to this project are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## 0.7.13 - 2026-07-21

### Changed
- Vocabulary sweep completing the 0.7.11 `jst.theme` rename: all remaining
  design-system "token" phrasing in example prose, comments, the jst skill and
  `llms.txt` now uses CSS's own names — CSS variables / custom properties /
  theme variables. The chart interop helper `readToken()` is now `readVar()`.
  Untouched, deliberately: the compiler's lexer tokens (`tokens.js` and the
  parser), CSRF and signed-URL tokens, the VS Code grammar's TextMate tokens,
  and historical changelog entries.

## 0.7.12 - 2026-07-21

### Added
- **Server-driven interactions — the HATEOAS way** (`examples/hateoas_recipes.html`):
  ten recipes for the interactions that tempt a JSON endpoint — drag-and-drop
  list reorder, kanban card move, click-to-edit, instant toggle, blur-time
  server validation, bulk actions, delete with the undo affordance travelling
  IN the returned HTML, autosave, self-terminating job polling, and
  search-as-you-type — each done as forms and links in, rendered HTML fragments
  out. Every recipe shows the live demo, a wire log of the exact round-trip
  (form-encoded request, HTML response, no JSON anywhere), a plain-Rails
  `render partial:` server sketch, and the JSON reflex it replaces. The page's
  mock backend intercepts `fetch` and answers like a small RESTful server, so
  the demos run on static hosting with real request/response mechanics.
- A matching **"Server interactions: the HATEOAS way" section in `llms.txt` and
  the in-repo jst skill**, aimed at coding agents that default to JSON APIs:
  state transitions go over forms/links; never a JSON endpoint for what a
  fragment can express; morph stateful regions; respond with the smallest
  common ancestor when several regions change; affordances (undo, next actions,
  poll triggers) arrive as hypermedia.
- Smoke coverage driving the drag-reorder, edit, delete/undo and job-polling
  round-trips, and asserting `application/json` never appears on the wire.

## 0.7.11 - 2026-07-21

### Added
- A bare `jst-transition` attribute opts ANY swap mode (not just the
  `jst-swap="transition"` innerHTML alias) into a View Transition, wherever
  it's set: declaratively on a jst-nav element, or programmatically via
  `navigate(url, { transition: true })`, which sets `jst-transition` on its
  synthesised driver element so the enhancement pipeline has one source of
  truth (#49). Bare only, since `jst-transition="name"` is already jst.js's
  own morph enter/leave/move CSS-transition attribute. `swapContent()` is
  now exported from `jst-nav.js` for direct testing.
- **Astryx skin** (`data-theme="astryx"`), a twelfth theme skin modelled on
  Meta's Astryx neutral theme: a grey page with white raised cards in light mode
  — inverted from JST's own look, where the surface sits a shade darker than the
  page — a near-black accent that flips to near-white in dark mode, a blue focus
  ring, generous radius and whisper-faint borders, with an inset light ring
  lifting elevated surfaces in dark mode. Offered in both re-skin pickers (the
  landing page and the component gallery). (Ideas adapted from Meta's Astryx
  design system, MIT.)
- **Motion variables** — `--jst-motion-fast` (125ms), `--jst-motion` (250ms),
  `--jst-motion-slow` (500ms) and `--jst-ease`. Component transitions read from
  them, so a skin re-times the whole UI in one place; the Astryx skin slows
  `--jst-motion` to 300ms to show motion is a theme variable too. Continuous
  loops (spinner, skeleton shimmer) keep their literal periods.
- **Neutral temperature variables** — `--jst-neutral-h` and `--jst-neutral-c`
  drive the hue and chroma of all five neutral roles (`--jst-bg`, `--jst-surface`,
  `--jst-fg`, `--jst-muted`, `--jst-border`) at once, in both light and dark, so
  one line shifts the whole grey family cool→warm.
- **Radius scale** — `--jst-radius` now derives `--jst-radius-s` (chips, badges,
  small inner elements) and `--jst-radius-l` (containers: cards, dialogs,
  popovers, toasts). A skin that overrides `--jst-radius` scales all three
  coherently (w3css's `0` still squares everything).
- A **skin-parity guard** in `tools/prerelease_check.mjs`: it extracts the skin
  set from `jst-components.css`, `examples/components/gallery-manifest.js` and
  both re-skin `<select>`s, and fails if any of the four drift apart — the
  duplication the shared gallery module can't fully eliminate.

### Changed
- **Breaking (pre-1.0): the `jst.theme` cascade layer.** The first sub-layer,
  which holds the `--jst-*` custom properties, is renamed from `jst.tokens` to
  `jst.theme` across `jst-layout.css`, `jst-components.css` and the shared
  gallery module. An app that layers overrides against `@layer jst.tokens` must
  retarget them at `@layer jst.theme`.
- The dismissable alert example computes its removal-timeout fallback from the
  live transition duration, so a skin that slows `--jst-motion` no longer cuts
  the collapse animation off early.

## 0.7.10 - 2026-07-12

Landing-page regressions from the 0.7.9 gallery overhaul, reported from real
browsing. Docs/examples only; no runtime changes.

### Fixed
- All 26 examples are back on the landing page — 0.7.9 wrongly reduced it to
  three. The card manifest and renderer now live in one shared module
  (`examples/components/gallery-manifest.js`) consumed by both the landing and
  the gallery page, so the two can't drift; the landing grid keeps the re-skin
  dropdown, per-example standalone links and view-source panels.
- Scroll-stealing eliminated at the root: the three whole-app frames (kanban,
  todo, slots) now carry `scrolling="no"` plus a reserved height close to
  their settled size, and autosize adds 2px of rounding slack — a trackpad
  gesture can never latch onto a transiently-scrollable frame again (this
  latch was also the "janky scrolling" feel).
- The hero counter button appears much sooner: `modulepreload` links flatten
  jst.js's four-hop import waterfall (compiler → parser → lexer/tokens/
  input_reader) into one round-trip, on both the landing and the docs page.

## 0.7.9 - 2026-07-11

Example-gallery overhaul from a full read-through of the live docs: per-example
frames, one badge convention, distinct theme skins, and the missing example
variants. No runtime changes; vendored apps only need the two stylesheets
(`jst-layout.css`, `jst-components.css`) if they use the component library.

### Changed
- The component gallery (`examples/components_cross_section.html`) is rebuilt
  as a shell of 26 cards, each loading its own mini page from
  `examples/components/` in its own iframe with "Open standalone" and
  "View source" — overlay examples (modal, drawer, toast) get frames tall
  enough for the overlay to actually appear in view. The re-skin dropdown
  re-themes every frame live via `postMessage`.
- The landing page no longer embeds whole example pages behind a
  "click to interact" shield; the shield (and its double-click) is gone
  entirely — per-example frames sized to content have no internal scroll to
  steal, so the first click lands.
- One badge convention sitewide, recorded in
  `docs/design/component-library.md`: green badges say only `HTML + CSS`,
  `CSS only` or `HTML + CSS + JS`; JST badges name the literal tag
  (`<jst-tabs>`, never "JST component"); purple badges name the layout hook as
  written (`<jst-reel>`, `[data-container]`); third-party badges name the
  library and version. Every gallery/recipes page carries a colour legend, and
  specific APIs (`:user-invalid`, `command`, …) moved into descriptions.
- Every example now opens with a one-sentence description of what it
  demonstrates, written for a developer evaluating JST; the self-proof tone is
  gone from the gallery, recipes pages and landing.
- The eleven `data-theme` skins are now structurally distinct: per-skin border,
  elevation and surface treatment (Material and DaisyUI go borderless with
  raised surfaces and filled inputs — cards only; tables, lists and accordions
  keep faint dividers — W3.CSS goes dead flat and square, and so on). New
  `--jst-shadow-surface` token carries resting card elevation; `jst-box`
  consumes it.

### Added
- Combobox example gained an async mode: a debounced handler fetches and
  assigns `el.options` with a visible loading state — same element, no
  separate remote API.
- Alert example gained dismissable alerts with an animated, reduced-motion-safe
  collapse.
- Skeleton example gained shape variants (text, avatar, card) and a live
  `<jst-include>` swap demo with a replay control.
- Validation example explains that `type=email` accepts TLD-less addresses
  (`a@b` is legal intranet email) and shows `pattern` as the strictness knob.
- The empty-state example is now live: removing every row reveals the designed
  empty state; inviting adds one back.
- New platform recipe: a nested docs sidebar with an active-item bar driven by
  scrollspy (`platform_recipes.html#recipe-sidebar-nav`).

### Fixed
- Reveal-on-scroll now animates everywhere it can: the demo carries its own
  scrollport so `animation-timeline: view()` has something to track (inside
  the old whole-page iframe embed there was no scrollport at all, so the
  boxes never moved). The smoke test now asserts the actual reveal behaviour.
- Theme names with digits (`w3css`) were rejected by the theme-parameter
  validation, making that skin unselectable.

## 0.7.8 - 2026-07-11

Fixes from reading the recipe pages on an iPhone.

### Fixed

- **Icons vanished in Safari.** A sprite reference (`<svg><use href="#...">`)
  without a `viewBox` has no intrinsic aspect ratio, so `jst-icon`'s
  `inline-size: auto` resolved to 0 and the icon disappeared (Chromium is
  forgiving, Safari is not). Two-part fix: `jst-icon > svg` now carries
  `aspect-ratio: auto 1 / 1` (intrinsic ratio when a viewBox provides one,
  square fallback when it does not), and the icons recipe adds `viewBox` to
  the referencing svgs with the reason stated.

### Changed

- **`qr-panel` renamed `qr-code`** in the template recipes. The natural name
  was sitting right there.

Body-HTML `$()` interpolation (so a value can render via a plain function
call, no template or wrapper element) is now designed in #83.

## 0.7.7 - 2026-07-10

The component-gap sweep (#80): every pattern a component library ships now has
a shown answer, so "JST has no X" always resolves to a working page.

### Added

- **`examples/platform_recipes.html`.** Fourteen recipes the platform already
  covers, each a working demo with its complete copyable source: date/time/
  number inputs, file input, video (all three Pro in Web Awesome), progress
  ring, stepper, tree view (native `<details>`), floating label, one-time code,
  context menu (`[popover]` + invokers), enter/exit animation
  (`@starting-style`), before/after comparison slider, split panel (native
  `resize`), scrollspy (`onreveal`, the one JST item), inline SVG sprite icons.
  Hermetic: zero external requests.
- **`examples/template_recipes.html`.** "JST ships the template, not a sealed
  element": copyable `<script type="jst">` recipes whose shown source is
  injected from the live template tag, so the code you read is provably the
  code running. `jst-rating` (keyboard accessible, half steps, change event
  up), `jst-copy-button`, `jst-relative-time` (Intl.RelativeTimeFormat with a
  unit-sized refresh tick), `jst-format-bytes` (manual unit ladder: Intl's
  compact notation collides "B"-for-billion with the byte symbol at giga
  scale), `jst-format-number`/`jst-format-date`, plus third-party minis:
  markdown through marked + DOMPurify + `trustedHTML` (the sanitization
  lesson, with a live defused script/javascript: payload) and a QR panel on a
  `jst-preserve` mount. Recipes live in the examples, not
  `jst-components.html`: the shipped library stays lean, you own your copy.
- **Coverage audit completed.** `docs/design/component-library.md` now audits
  the full Web Awesome inventory (19 new rows: helpers, media, Pro charts) and
  every skip/recipe row links its live demo anchor. Per-element `onresize` is
  tracked separately (#81).

## 0.7.6 - 2026-07-10

Round two from the Web Awesome audit: prove the themeability claim (#71), and
prove "a layer, not a walled garden" with real third-party libraries.

### Added

- **Live theme knobs on the docs page (#71).** The docs page's own chrome now
  derives from the `--jst-*` tokens (it loads `jst-layout.css`; its bespoke
  palette re-plumbed onto token roles), and an inline `jst-theme-knobs` JST
  component writes tokens inline on `<html>`: accent picker (1x1-canvas
  oklch-to-hex sync, since computed styles serialize oklch as oklch),
  space/ratio/radius/font-size sliders, system/light/dark scheme, localStorage
  persistence reapplied before first paint, reset. Inline styles beating the
  `jst.tokens` layer (renamed `jst.theme` in 0.7.11) is the cascade-layer story
  demonstrating itself. Plus a
  "Theming JST" docs section: the token contract, how to theme, the layer model.
- **Third-party interop examples.** `examples/third_party_charts.html`: one
  dataset through Chart.js 4.5.0, uPlot 1.6.32, and Observable Plot 0.6.17, each
  inside a JST component (`once()` setup with cleanup, `jst-preserve` mounts so
  morph re-renders keep the live canvas/SVG, colors read from the tokens, one
  Randomize republishes all three). `examples/webawesome_interop.html`:
  free-tier Web Awesome components inside JST, attributes down into
  `wa-rating`, its `change` event back up into JST-rendered state, and
  `--wa-*` brand tokens bridged from `--jst-*` (mapped on `body` so token
  overrides re-resolve). Every section badge-labeled NATIVE / JST /
  THIRD-PARTY. Smoke checks stay green without CDN access.
- **Skill: "Upgrading a vendored copy".** `CHANGELOG.md` is the migration
  guide: find the vendored version, read the entries crossed, copy ALL
  distributables in one move, run `tools/lint.mjs` + `tools/codemod.mjs` from
  the target tag, browser-verify in dev mode.

## 0.7.5 - 2026-07-10

Prompted by a competitive audit against Web Awesome: two things it ships that
JST should too, done the JST way.

### Added

- **Agent-facing docs: `llms.txt` + a `jst` skill (#77).** `.claude/skills/jst/SKILL.md`
  is the canonical instruction set for AI agents working on a JST page:
  component authoring, handler semantics, jst-nav, the token contract, known
  gotchas. `llms.txt` at the site root points agents at it, including how to
  fetch the skill pinned to the version an app has vendored
  (`grep "export const version" jst.js` → the matching `v<version>` tag).

### Changed

- **All CSS now lives in the `jst` cascade layer (#77).** `jst-layout.css`
  declares `@layer jst.tokens, jst.base, jst.primitives, jst.components` (the
  first since renamed `jst.theme` in 0.7.11) and
  fills the first three; `jst-components.css` fills the last. Consequence: any
  unlayered app stylesheet outranks jst styles regardless of specificity, so
  overriding a jst default needs a plain selector, never a specificity fight,
  and a single property can be stripped locally with `revert-layer`. Token
  overrides at `:root` behave as before.

## 0.7.4 - 2026-07-07

Found adopting morph across a real app (#74): a whole-region `jst-swap="morph"`
reloaded an `<iframe>` whose `src` carries a per-render signed token.

### Added

- **`jst-preserve` freezes a node against morph (#74).** morph treats the
  incoming HTML as the source of truth for a plain element's attributes and
  children, so an attribute that legitimately changes every render (a signed
  iframe `src`) is overwritten and the element reloads, and client state the
  DOM does not capture (a `<video>` mid-playback, a `<canvas>`, a third-party
  widget) is lost. Mark the node `jst-preserve` and morph leaves it entirely
  alone: attributes, form properties, and the whole subtree. `jst-key` does not
  cover this, since a keyed match still has its attributes reconciled. Registered
  custom elements and `<jst-slot>` are already preservation boundaries; this is
  the lever for plain elements. Keep emitting the element server-side (a bare
  stub is fine, its attributes are ignored) and pair it with `jst-key` when
  siblings can shift. Documented in the README ("Preserving DOM across morph").

## 0.7.3 - 2026-07-07

Follow-up from adopting v0.7.2's morph in budget_app and brent-mail (#68):
the whole-region swap pattern could not upgrade to a state-preserving morph.

### Fixed

- **`jst-swap="morph"` composes with `jst-select` (#68).** `jst-select`
  returns the matched element's outer HTML, but morph treated that HTML as
  the target's new children, so `jst-target="main" jst-swap="morph"
  jst-select="main"` nested `main` inside itself. When the response came
  through `jst-select` and its single root element is the target (same tag),
  jst-nav now morphs element-to-element: the root's own attributes reconcile
  onto the target and its children morph, instead of nesting. This makes
  morph a drop-in upgrade for every `outerHTML` region swap - inputs keep
  focus, open `<details>` and loaded iframes survive a redirect-back action.
  A plain `jst-swap="morph"` without `jst-select` keeps its child-list
  meaning unchanged; element-style also applies without a select when the
  root and target carry the same `id` (a form re-rendering itself as its own
  root through `jst-swap-4xx="morph"`). `JST.morph(target, next)` gains the
  same behaviour directly: a single element whose tag matches the target
  morphs outerHTML-style, reconciling that element's own attributes; a string
  or a `DocumentFragment` (e.g. `template.content`) stays children-style as
  before.

## 0.7.2 - 2026-07-07

Gaps found converting budget_app and brent-mail from htmx/Turbo (#63, #64,
#65, #66) - all four in the jst-nav error and confirmation paths.

### Fixed

- **`jst-swap="morph"` actually morphs (#66).** The swap table delegated to
  `JST.morph`, but jst.js never exported its morph engine, so the attribute
  silently behaved as `innerHTML` in every install - you thought focus,
  scroll, and open/closed state were preserved and they were not. jst.js now
  exports **`morph(target, next)`** (also on `window.JST.morph`): it morphs
  the target's children to match the incoming HTML, preserving node identity
  where the structure lines up, with `jst-key` pairing keyed children across
  reorders. Incoming HTML is the source of truth for attributes and form
  values. Loading jst-nav without jst.js now warns once before falling back
  to `innerHTML` instead of pretending.

### Added

- **`jst:response-error` carries the response body (#63).** The event detail
  gains `text`: the body stream is consumed by the library before the event
  fires, so a listener could not recover a server-provided error message
  (Rails `render plain: "…", status: 422`) any other way. Show the server's
  message in a toast straight from the event, no hidden-sink workaround.
- **Error routing honours a swap mode (#64).** `jst-swap-4xx` / `jst-swap-5xx`
  / `jst-swap-error` control how a routed error response lands, defaulting to
  `innerHTML` as before. The canonical use: a form that re-renders itself on
  validation failure sets `jst-target-4xx="#its-own-id"` +
  `jst-swap-4xx="outerHTML"`, so the response can contain the form's real
  root instead of restructuring partials around an innerHTML-shaped wrapper.
  The replaced region is re-scanned, so the re-rendered form is wired for the
  next submit.
- **`jst-confirm` is pluggable (#65).** Apps that ban browser dialogs set
  `JST.nav.confirm = (message, el) => boolean | Promise<boolean>` and the
  attribute drives their inline UI instead of `window.confirm` (which stays
  the default). Async by design: arm-and-disarm confirmation flows just work,
  and every app stops hand-rolling the same cancelable `jst:before-request`
  dance.

## 0.7.1 - 2026-07-07

Fixes from three production apps migrating to 0.6/0.7 (#57, #58, #60, #61).

### Fixed

- **A swap that finds no target is a detectable miss, not a silent success
  (#57).** The target is re-resolved at write time: with a View Transition the
  write runs inside an async update callback, and an overlapping swap could
  detach the node resolved at response time - `outerHTML` on a parentless node
  is a silent spec no-op, yet the URL changed and `jst:swapped` fired. Now a
  fresh, connected match wins; a still-connected original is kept; nothing
  emits a bubbling **`jst:swap-missed`** and skips the history entry and
  `jst:swapped`. The connected-node dispatch fallback (#48) now also covers
  `jst:before-swap`, `jst:after-request`, `jst:response-error`, and
  `jst:send-error`.

### Added

- **`navigate(url, options)`: programmatic navigation with the full pipeline
  (#58, #60).** The enhanced-element pipeline (bubbling lifecycle events,
  `confirm`, `select`, history) driven from JS against a library-owned driver
  anchor, so document-level delegated listeners see exactly what a clicked
  link produces. Options: `target`, `swap`, `select`, `confirm`, `method`,
  `pushUrl`/`replaceUrl` (true = the request URL, or an explicit string), and
  `dataset` for app attributes your listeners read. Returns the Response.
  `swap()` stays the bare primitive; the 0.6.0 migration table row that
  equated it with `performRequest` is amended.
- **Migration tooling (#61).** codemod drops a `jst-get` that duplicates an
  identical `href` on the same anchor; lint gives `<button jst-get/jst-action>`
  the two rewrites inline (link for navigation, one-button form for an
  action); `lint --js-strings` scans string/template literals in `.js` files
  for removed syntax, so markup built in JS stops sailing past the HTML scan.

## 0.7.0 - 2026-07-07

The component library becomes consumable: one fragment of component
definitions plus two stylesheets, loadable at runtime or precompiled.

### Added

- **`jst-components.html`: the consumable component fragment.** The five JST
  components (`jst-palette`, `jst-tabs`, `jst-toaster`, `jst-combobox`,
  `jst-table`) ship as one fragment. Runtime consumption:
  `<jst-include src="/jst-components.html">` and the definitions
  auto-register on arrival (properties set before the upgrade are
  preserved). Strict CSP: `tools/precompile.mjs jst-components.html`
  compiles all five. The cross-section demo consumes the fragment the same
  way an app would.
- **The color system uses the modern color features end to end.**
  `contrast-color()` picks the foreground for every on-color surface (accent,
  ok, warn, error) where supported, with hand-tuned fallbacks (the warning
  badge's text is dark on amber, not white); `color-mix()` derives tinted
  status surfaces for alerts and toasts, table stripes, and the selection
  color from the same tokens; the caret follows the accent. Everything still
  flows from `light-dark()` + oklch + the relative-color ramp.
- **Carousel scroll buttons position via anchor positioning.** The buttons'
  containing block is not the reel, so plain insets escaped it; an
  `anchor-name`/`anchor-scope` pair overlays them just inside the reel's
  edges (several carousels can share a page). Carousels also hide their
  scrollbar: the dot markers are the indicator. Verified in Chrome: a real
  click on the arrow pages exactly one slide, zero JS.
- **Dark mode verified.** The full library reviewed under an emulated dark
  scheme: tokens flip every surface, the status tints and contrast-picked
  foregrounds hold, and the one real finding (escaped carousel buttons) is
  the fix above.
- **Minified stylesheets.** `jst-layout.min.css` and `jst-components.min.css`
  are built (and size-reported) by `npm run build` alongside the JS bundles.

- **`commands(event, map)` handler helper.** The Invoker Commands router: a
  `commandfor` target handling several custom commands routes them without a
  hand-written `switch` - `oncommand="commands(event, { '--save': save,
  '--revert': revert })"` dispatches on `event.command` and ignores unmatched
  commands. In scope in template handler bodies, `JST.fn.commands` elsewhere,
  and emitted by the precompiler. `examples/invoker_commands.html` uses it for
  its host-level command listener.
- **jst-components: the audit gaps, closed as thin CSS.** Skeleton
  (`.jst-skeleton`), badges (`.jst-badge` variants), avatar (`.jst-avatar`),
  breadcrumb (`.jst-breadcrumb`), pagination (`.jst-pagination`), drawer
  (`.jst-drawer`, the modal's side-sheet variant), and join/input groups
  (`.jst-join` + `.jst-addon`, covering button groups). Classless base gains
  `:user-invalid` validation styling, `::file-selector-button`, `kbd`,
  `.jst-visually-hidden`, and status tokens (`--jst-ok/--jst-warn/--jst-error`);
  the accordion animates via `::details-content` where supported. Driven by a
  coverage audit vs Bootstrap, Web Awesome, Pico, daisyUI, Open Props, and
  Radix (see the design doc).
- **jst-components: the patterns Brent's real apps kept hand-rolling.**
  Mined from agent_app, budget_app, notes_app, and loops_app: button variants
  (`data-variant="quiet | ghost | danger"` on any button), record lists
  (`.jst-list` with `data-main`/`data-actions` rows), empty states
  (`.jst-empty`), stat tiles (`.jst-stat` with `data-trend`), and page headers
  (`.jst-page-header` + `.jst-eyebrow`).
- **jst-layout: the modern-CSS showpieces.** `jst-reel[data-carousel]` (a
  snapping carousel; engines with CSS carousels add zero-JS arrows and dot
  markers), `.jst-reveal` (scroll-driven fade-in via
  `animation-timeline: view()`, off under reduced motion), `[data-container]`
  (opt-in container-query context), and the lazy accordion pattern: a
  `<jst-include when="visible">` inside a closed `<details>` fetches only
  when the panel opens - pure composition, smoke-tested.
- **jst-components: command palette (`jst-palette`).** Global shortcut via
  `keys()` (Cmd/Ctrl+K), type-to-filter, arrow/Enter/Escape keyboard model,
  backdrop dismiss, and a bubbling `run` event the page executes. Composes
  the shared floating-panel rules; none of the surveyed libraries ship one.
- **jst-layout: the long-form and app-chrome misses.** `.jst-prose` (flow
  rhythm for article content, since the reset zeroes margins for app
  composition), `.jst-kv` key-value pairs, `[data-scheme]` manual dark-mode
  toggle riding `light-dark()`, `.jst-skip-link`, toaster `data-position`
  corners, and striped tables (`jst-table[striped]` / `table.jst-striped`).
- **jst-include: symmetric reveal margin.** An include the user scrolls past
  (now above the viewport) loads too; previously only the approach from below
  triggered the fetch.
- **jst-layout: the full Every Layout primitive set.** switcher, cover, frame,
  reel, imposter, and icon join stack, cluster, grid, sidebar, center, and box
  in `jst-layout.css` - all CSS-only. `examples/layout_primitives.html`
  demonstrates each one, and the example smoke suite exercises both library
  demo pages.

## 0.6.0 - 2026-07-05

**Breaking.** One handler semantics everywhere, and jst-nav reduced to pure
enhancement. Two rules now cover the whole library:

> **An `on<event>` value is a function body** - the native inline-handler
> contract (`event` in scope, `this` = the element) - in body HTML, in
> templates, for native events and synthetic ones alike.
>
> **jst-nav enhances elements that already act** (links navigate, forms
> submit); it never invents behaviour on inert elements - that's a component's
> job - and never evaluates attribute strings.

This replaces v0.5.0's expression handlers (`onclick="$(fn)"`) and cause
attributes (`jst-on*`, `jst-load`, `jst-poll`, shapers) one release later -
deliberately: v0.5.0 had zero downstream users and the uniform model closes a
real trap (the same attribute text meaning different things in templates vs
body HTML). `tools/codemod.mjs` migrates v0.4 **and** v0.5 spellings;
`tools/lint.mjs` flags everything left.

### Migration table

| Old (v0.4/v0.5) | New (v0.6.0) |
| --- | --- |
| `onclick="$(() => el.count++)"` | `onclick="el.count++"` |
| `onclick="$(handler)"` | `onclick="handler(event)"` |
| `onsubmit="$(prevent(fn))"` / `onsubmit.prevent=` | `onsubmit="event.preventDefault(); fn(event)"` |
| `onclick="$(stop(fn))"` / `.stop` | `onclick="event.stopPropagation(); fn(event)"` |
| `$(self(fn))` / `.self` | `if (event.target !== this) return; fn(event)` |
| `oninput="$(debounce(300, fn))"` / `.debounce.300` | `oninput="debounce(event, 300, () => fn(event))"` |
| `oninput="$(changed(fn))"` | `oninput="if (changed(event)) fn(event)"` |
| `$(throttle(1000, fn))` | `if (!throttle(event, 1000)) return; fn(event)` |
| `onkeydown="$(keys({ Enter: fn }))"` / `.enter` | `onkeydown="keys(event, { Enter: () => fn(event) })"` |
| `.capture` `.passive` `.once` `.outside` | **unchanged** (registration-only modifiers) |
| `<div jst-get="/x" jst-load>` | `<jst-include src="/x">` (jst-behaviors.js) |
| `<div jst-get="/x" jst-load="lazy">` | `<jst-include src="/x" when="visible">` |
| `<div jst-get="/x" jst-poll="2s" jst-target="this">` | `setInterval(() => swap('#region', '/x'), 2000)` |
| `<input jst-get="/s" jst-oninput="typeahead">` + `JST.nav.shape(…)` | `<input oninput="typeahead(event)">` + a named function calling `swap()` |
| `<a jst-get="/p" jst-target="#out">` | `<a href="/p" jst-target="#out">` (URL from native `href`) |
| `<button jst-action="/x" method="delete" …>` | a one-button `<form action="/x" method="delete" …>` (what Rails' `button_to` renders) |
| `jst-trigger="…"` (any spec) | an event, a timer, or a component - plain JS |
| `JST.nav.request(el)` / `performRequest` | `navigate(url, options)` for the full pipeline (lifecycle events, confirm, history; added in 0.7.1 - the row previously pointed at `swap()`, which is the bare fetch + swap with none of those) |

### Added

- **Uniform handler bodies.** Template `on<event>` values compile as
  `function (event) { body }` in render scope (closures over template params
  work) and attach via `addEventListener` (`this` = the element). Copying a
  handler between a template and plain HTML no longer changes its meaning.
- **Handler helpers.** `changed(event)` / `throttle(event, ms)` guards,
  `debounce(event, ms, fn)`, `keys(event, map)` - called *inside* any handler
  body; state keyed per element + event type + delay in WeakMaps (a 300ms
  validate and a 2s autosave on one input never collide). In scope in template
  handler bodies; `JST.fn.*` / module exports elsewhere. `prevent`/`stop`/
  `self` are deleted - they're native statements.
- **Synthetic `reveal` event.** Binding `onreveal` makes JST observe the
  element (IntersectionObserver) and dispatch a real `CustomEvent('reveal')`
  each time it scrolls into view - so `addEventListener('reveal', …)` works
  too. Observation is released on disconnect.
- **`configure({ unsafeInlineHandlers: true })`.** Opt-in wiring of the on*
  attributes the platform does not implement, written inline in plain body
  HTML: component custom events (`onitem-selected`) and synthetic events
  (`onreveal`). JST evaluates the attribute string the way the browser
  evaluates its own handlers; browser-owned names are never double-wired.
  Default off; deliberately not
  coupled to `dev` (a flag that changes security semantics between dev and
  prod is a footgun); never enable on pages that interpolate untrusted data
  into HTML.
- **`swap(target, url, options)`** - jst-nav's imperative primitive: the same
  pipeline as the declarative attributes (JST-Request + CSRF headers,
  `select`, out-of-band swaps, re-scan), callable from any handler. Returns
  the `Response`.
- **`<jst-include src when="visible">`** (jst-behaviors.js) - a region whose
  content lives at a URL: `src` for an HTML fragment, eager by default, lazy
  on reveal. A self-filling region is a *component with well-defined
  behaviour*, not a div wearing magic attributes.

### Changed

- **jst-nav is enhancement-only.** Links and forms carrying any effect
  attribute (`jst-target`, `jst-swap`, …) are upgraded on their **native
  activation**; URL and verb come from native `href`/`action`/`method`.
  `jst-get`/`jst-action`/`jst-trigger`/`jst-on*`/`jst-load`/`jst-poll`,
  shapers, and `request()`/`performRequest` are removed and fail loud with the
  rewrite. `jst-boost`, OOB swaps, CSRF, history, `jst-confirm`, error routing
  and the cancelable lifecycle events are unchanged.
- **Codemod + lint cover the full migration.** `codemod.mjs` rewrites v0.4
  dotted modifiers, v0.5 expression handlers and wrapper combinators (166
  handler rewrites across this repo were done by the tool itself); `lint.mjs`
  flags `$()` in handler bodies, removed modifiers and removed nav attributes,
  and `--csp` lists native inline handlers for strict-CSP migrations.

### Docs

- **The `.md` docs tree is gone.** The documentation is one onboarding page -
  `docs/index.html` - ordered to teach (components → syntax → handlers → data
  flow → fragments/nav → security/CSP), with annotated examples, the
  script-gadget rule, the two-spellings CSP toggle, and the
  "how far to take strict CSP" ladder (islands, not an accidental SPA).

## 0.5.0 - 2026-07-04

**Breaking.** The behaviour microsyntaxes are gone, replaced by one rule that
now holds across the whole library:

> **HTML says where behaviour attaches; JS says what the behaviour is.**
> Dotted modifiers and directive values configure *registration/wiring* only -
> everything about *when a handler fires* is plain JavaScript.

Two grammars were removed: the template behaviour modifiers
(`onclick.prevent`, `.debounce.300`, key filters) and jst-nav's htmx-style
`jst-trigger` spec (`keyup changed delay:300ms`). Both fail loud with the exact
rewrite; `tools/codemod.mjs` migrates template bindings automatically and
`tools/lint.mjs` flags everything (including leftover `jst-trigger`).

### Migration table

Every removed form has exactly one rewrite:

| Old (0.4.x) | New (0.5.0) |
| --- | --- |
| `onclick.prevent="$(fn)"` | `onclick="$(prevent(fn))"` |
| `onclick.stop="$(fn)"` | `onclick="$(stop(fn))"` |
| `onclick.self="$(fn)"` | `onclick="$(self(fn))"` |
| `oninput.debounce.300="$(fn)"` | `oninput="$(debounce(300, fn))"` |
| `onkeydown.enter="$(fn)"` | `onkeydown="$(keys({ Enter: fn }))"` |
| `onkeydown.enter.prevent="$(fn)"` | `onkeydown="$(keys({ Enter: prevent(fn) }))"` |
| `.capture` `.passive` `.once` `.outside` | **unchanged** (registration-only modifiers) |
| `jst-trigger="click"` (bare event) | `jst-onclick` (any event: `jst-on<event>`) |
| `jst-trigger="keyup changed delay:300ms"` | `jst-oninput="typeahead"` + `JST.nav.shape('typeahead', fire => changed(debounce(300, fire)))` |
| `jst-trigger="… throttle:1s"` | a shaper using `throttle(1000, fire)` |
| `jst-trigger="revealed"` | `jst-load="lazy"` |
| `jst-trigger="load"` | `jst-load` |
| `jst-trigger="every 2s"` | `jst-poll="2s"` |
| `jst-trigger="keydown[Shift+D] from:body"` | `addEventListener` on `body` + `keys({ 'Shift+D': … })` + `JST.nav.request(el)` |
| `jst-trigger="click once"` | a once-gating shaper, or hand-wired `{ once: true }` |

Composition order note: the old modifier chain secretly ordered operations for
you; combinators make it visible. `prevent(debounce(300, fn))` cancels the
default synchronously and debounces the work - `debounce(300, prevent(fn))`
would call `preventDefault()` 300ms too late. The codemod emits the correct
nesting.

### Added

- **Handler combinators in core.** `prevent`, `stop`, `self`, `changed`,
  `debounce`, `throttle`, `keys` - plain functions that wrap a handler to shape
  when it runs. In scope bare inside every template expression, published as
  `JST.fn.*`, and exported from `jst.js`. They compose (`changed(debounce(300,
  fn))`) and, unlike the removed grammar, support user abstraction: name your
  app's behaviours (`const typeahead = fn => changed(debounce(300, fn))`) and
  reuse them.
- **jst-nav causes.** Every element is a *cause → request → effect* sentence.
  The cause is spelled the way HTML spells causes - in the attribute name:
  `jst-on<event>` overrides the default event; `jst-on<event>="name"` gates it
  through a **shaper** registered with `JST.nav.shape(name, fire => handler)` -
  an inert name in the markup, JS behaviour in your app, built from the same
  combinators. `jst-load` fires on wire, `jst-load="lazy"` on reveal (like
  native `loading="lazy"`), `jst-poll="2s"` on an interval. Unknown shaper
  names fail loud after page load; a late registration heals the element.
  `JST.nav.request(el)` is the public escape hatch for exotic causes (global
  shortcuts, `from:`-style delegation) - `performRequest` remains as an alias.
- **`jst-lint --csp`.** Flags native inline `on<event>=` handlers in usage HTML
  (evaluated by the browser; blocked under a strict CSP) and points at the
  inert `jst-on<event>="name"` spelling. Template handlers are exempt - they
  compile to `addEventListener`.
- **Lint + codemod migration coverage.** `tools/lint.mjs` flags removed
  behaviour modifiers (with the exact combinator rewrite) and leftover
  `jst-trigger` in usage HTML; `tools/codemod.mjs` rewrites template modifier
  bindings to combinators automatically, keeping registration modifiers.

### Security

- **Directive values are names, never code - documented as a hard line.**
  jst-nav reads attribute values from the live DOM, so evaluating them would
  make the library a script gadget (a CSP bypass for injected HTML). Shaper
  references are inert strings; there is no expression evaluation in any
  directive. See `docs/security-model.md`.

### Docs

- `docs/directives.md` rewritten around the **cause → request → effect** model
  (the word "trigger" is gone); documents shapers, the two-spellings CSP
  toggle, and the escape hatch.
- `docs/writing-jst.md` documents the combinators, composition-order semantics,
  and the registration-only modifier rule.
- `docs/security-model.md` adds the script-gadget section.

## 0.4.4 - 2026-07-02

Fail-loud compile errors, TypeScript declarations, and jst-nav CSRF/URL/timing
fixes from the audit round. No breaking runtime changes (the `attrs=` shorthand
removal is a compile-time migration, covered by codemod + lint).

### Added

- **TypeScript declarations.** The npm package now ships hand-written `.d.ts`
  files for the runtime API, template helper boundary, `window.JST`, `JST.nav`,
  and `JST.behaviors`, with package `types` / `exports` entries and no build
  step.
- **Consumer testing docs.** Added jsdom/custom-elements and Playwright testing
  patterns for real custom elements rendered into light DOM.
- **`jst-nav` sends the CSRF token (#45).** Unsafe (non-`GET`) **same-origin**
  requests now carry the server's token from `<meta name="csrf-token">` as the
  `X-CSRF-Token` header (the Rails/Laravel/Turbo convention), so the non-form
  directive paths (a link doing a `POST`, a boosted click) stop tripping
  `InvalidAuthenticityToken`. Same-origin-only, on by default; remap via
  `JST.nav.csrf.headerName` / disable via `JST.nav.csrf.metaName = ''`.
- **`jst-replace-url` (#50.1).** Replaces the current history entry
  (`history.replaceState`) instead of pushing - for filters / in-place changes
  that shouldn't add a back-button step. Mirrors `jst-push-url`; replace wins if
  both are present.
- **Cancelable `jst:before-swap` (#47).** Fires after the response is read (and
  `jst-select` applied) but before OOB/swap/history/`jst:swapped`;
  `preventDefault()` drops the response entirely. Enables request-racing /
  supersession (drop a stale or wrong-target response) at the framework level.
  Detail: `{ el, html, response }`.

### Fixed

- **`${ ... }` wrapping HTML now fails at compile time (#53).** The compiler
  rejects control flow that wraps template HTML in the block form and points at
  the `$ if (...) {` / `$ }` line form. Compile failures now define a visible
  error element in runtime mode instead of rendering empty when `dev:false`.
- **`jst-trigger="… throttle:Ns"` was a no-op.** The modifier parsed but the
  handler never applied it (only `delay:` was wired). It now rate-limits on the
  leading edge - fires immediately, then drops events for the interval. `delay:`
  (debounce) and `throttle:` (rate-limit) are now both real and complementary.

### Changed

- **Removed the `attrs="…"` declaration shorthand.** Template inputs now have one
  spelling: `attributes="…"`. `attrs="…"` is a compile-time migration error;
  `tools/lint.mjs` flags it and `tools/codemod.mjs` rewrites it.

### Docs

- **Documented the `jst-trigger` modifiers.** `throttle:` and `once` were
  supported (well, `once` was; `throttle:` is now) but missing from the docs.
  The `jst-trigger` section now has an explicit modifier table
  (`changed` / `delay:` / `throttle:` / `from:` / `once`) and names the
  event-plus-modifiers value-spec grammar.
- **No imperative ajax API, by design (#50.3).** Documented in `directives.md`
  ("Coming from `htmx.ajax()`? You don't need an imperative API") why JST has no
  `JST.nav.navigate()`: htmx needs `htmx.process()` to wire inserted nodes, but
  JST's `MutationObserver` upgrades them automatically - so a programmatic
  fetch-and-swap is one line of plain `fetch` + `insertAdjacentHTML`.
- **Corrected the README size claim.** "Roughly 15 KB runtime/compiler" →
  measured numbers: 10 KB gzipped (33 KB minified) for the full build, or 6 KB
  gzipped runtime-only with precompiled templates.

## 0.4.3 - 2026-06-29

Bug fixes and docs from real-app integration (downstream OKF / agent_app reports).
No breaking changes.

### Fixed

- **View Transitions degrade quietly (#43).** `view-transition` (the `jst.js`
  attribute) and `jst-swap="transition"` (jst-nav) now skip the transition on a
  hidden document or under `prefers-reduced-motion`, and catch its abort, instead
  of surfacing an unhandled `InvalidStateError`. The DOM still updates.
- **`jst:swapped` correctness (#48).** It now fires *after* the DOM update (awaits
  the View-Transition update callback) and is dispatched on a **connected** node,
  so a delegated `document`-level listener still receives it when an `outerHTML`
  swap detached the trigger (`detail.el` still identifies the source).
- **Vendoring (#41).** Dropped `utils.js` - a dev/test scratch file with a dangling
  `test_suite.js` import, imported by nothing at runtime - from the published
  `files` and the vendor docs.

### Docs

- **"A property write re-renders the component"** is now the headline of the binding
  model, with the **async-render** behaviour stated loudly (#51).
- jst-nav **request → swap lifecycle order** + what's cancelable + the
  **`JST-Request: true`** header contract + fragment-scope (#51, #46).
- **Vendor the whole import graph** (or the single-file `jst.global.js`) - vendoring
  `jst.js` alone breaks rendering (#51).
- `once()` **load-order** gotcha for the host-a-widget pattern, with the
  dynamic-import fix (#42); `configure()`-vs-auto-init ordering.

## 0.4.2 - 2026-06-29

Docs and tooling only - the runtime is byte-identical to 0.4.1. (Docs are part of
the deliverable: a doc fix that isn't tagged leaves the released docs stale.)

### Docs

- Finished the `props`→`attributes` rename in **prose**: the "props down, events
  up" mantra → "attributes down, events up", the concept references across the
  docs, and - most visibly - the two landing pages' syntax-highlighted `props=`
  code examples (the keyword sweep couldn't match the span-broken markup).
- Slimmed the "Upgrading across breaking releases" section to name the CHANGELOG
  as the single migration source (de-duplicated README ↔ `install.md`,
  version-agnostic, covers `props=`→`attributes=`); fixed stale CDN-pin examples.

### Tooling

- **`tools/prerelease_check.mjs`** (`npm run check:release`, also the last step of
  `npm test`): verifies the version agrees across `package.json` / `jst.js` /
  `CHANGELOG` and that no doc pins an old release. In the RELEASING.md process.

## 0.4.1 - 2026-06-28

Additive - **no breaking changes**.

### Added

- **`view-transition` attribute.** Put it on a component *instance*
  (`<my-list view-transition>`) to wrap that instance's re-renders in the
  browser's View Transition API. It's a usage-site, **per-instance** choice (the
  consumer's presentation decision) - the component template is unchanged. The
  first paint is never animated; it degrades to an instant render where the API
  is unsupported. Style with `::view-transition-*` CSS. See
  `examples/view_transition_component.html` (and `examples/view_transitions.html`
  for a from-scratch explainer of View Transitions).
- **`docs/from-other-frameworks.md`** - an explicit "other frameworks do X; in
  JST you do Y" map: shared store / context → pass via **attributes**; refs →
  `el.querySelector`; component two-way → explicit `.attr` + event (`jst-model`
  stays for native inputs only); `computed`/`effect`/`watch` → cheap inline, pass
  the value in, or `once()` (expensive derivation is business logic - keep it out
  of templates); `fx-ignore`/`x-ignore` → wrap the widget in a component + project
  via a slot; scoped styles → light DOM + global CSS.

### Changed

- **Framework parity:** `fixi/fx-ignore` (i)→✓ - encapsulating a third-party
  widget behind a component interface and projecting its DOM via a slot (preserved
  across re-renders) is the idiomatic pattern, not a workaround. **fixi now 18/0.**
- **Closed the `jst-ignore` proposal (#6):** a slot already gives the uncontrolled
  region (projected nodes are moved, not re-rendered), so no directive is needed.

## 0.4.0 - 2026-06-28

The directive release: two opt-in libraries of declarative, string-valued
attributes for *usage* HTML - `jst-nav` (server-driven nav) and `jst-behaviors`
(client behaviors) - plus a reverse-infinite-scroll pattern. **Additive; no
breaking changes** (new opt-in files; nothing to migrate).

### Added

- **`jst-nav`** - HTMX/fixi-shaped server-driven navigation: `jst-get` /
  `jst-action` + the native `method` attribute (verbs), `jst-target` (100% CSS
  selectors: bare / `this` / `closest` / `find` / `closest…find`), `jst-swap`
  (innerHTML / outerHTML / insert-adjacent / delete / none / morph / transition,
  + out-of-band via `jst-swap-oob`), `jst-select`, `jst-push-url` + history,
  `jst-boost`, `jst-confirm`, error routing (`jst-target-4xx`/`-5xx`), and
  `jst-trigger` (`load` / `revealed` / `every Ns` / `keyup changed delay:Nms` /
  key-filtered `keydown[Shift+D] from:body`). In-flight requests abort on
  re-trigger; a `jst-request` class marks the trigger during the fetch.
- **`jst-behaviors`** - Alpine-shaped client behaviors for what the platform
  doesn't give free: `jst-intersect` (reveal / lazy media) and `jst-teleport`
  (portal). (Toggle / dismiss / outside-click are native - Invoker Commands +
  Popover + `<dialog>`.)
- **Reverse infinite scroll** - `jst-swap="afterbegin"` prepends older content
  and preserves scroll position (`examples/jst_nav.html`).
- **Builds** - minified `jst-nav.min.js` (~7.5 KB) + `jst-behaviors.min.js`
  (~2.2 KB) as opt-in add-ons that work with any core build.
- **`docs/directives.md`** - the directive reference.

### Changed

- **Framework parity: every directive-addressable partial is now an exact ✓** -
  HTMX 16/0, fixi 17/1 (only `fx-ignore`, proposal #6), alpine `x-teleport`. The
  corpus moves to **102 exact / 24 partial / 126**. The remaining `(i)`s
  (alpine ref/effect/store/watch, most Vue, some React/Lit) are inherent
  fine-grained-reactivity / framework-feature differences, not directive gaps.

## 0.3.0 - 2026-06-28

A breaking release: the template input keyword is renamed `props=` → `attributes=`.
Also lands first-class Invoker Commands support, an opt-in component-library
**preview** (`jst-layout` + `jst-components`), and three more frameworks in the
parity study (Svelte, Solid, Angular).

### Changed (breaking)

- **The template input declaration `props="…"` is renamed to `attributes="…"`**,
  with `attrs="…"` accepted as a shorthand alias. A template's inputs *are* HTML
  attributes; the new name says so (and sheds the React-flavoured "props"). The
  old `props="…"` is **removed with no alias** - a template still using it throws
  a clear, actionable error. Invalid-identifier and reserved-name errors now say
  "attribute" instead of "prop".

  **Migration** - rename the one keyword on every template *definition*:

  | 0.2.x | 0.3.0 |
  | --- | --- |
  | `<script type="jst" name="todo-item" props="item onToggle">` | `<script type="jst" name="todo-item" attributes="item onToggle">` |

  Nothing else changes: usage sites (`<todo-item item="…">`), `.prop="$(…)"`
  property bindings, and `on<event>` handlers are unaffected. `npm run lint` flags
  any leftover `props=` on a `<script type="jst">` open tag with `file:line:col`.

### Added

- **Invoker Commands API support (#29).** `command`/`commandfor` and custom
  `--commands` work across the JST boundary with **no new core code**: `oncommand`
  rides the existing `on*` → `addEventListener` binding. Custom-command events
  fire on the `commandfor` target (`bubbles: false`); for events up to a parent,
  the target re-emits via `el.emit()`. Verified across multiple instances with no
  synthetic id system. See `examples/invoker_commands.html`.
- **Component-library preview (opt-in, CSS-first).** `jst-layout.css` (design
  tokens + classless base + layout primitives) and `jst-components.css`
  (Modal/Accordion/Dropdown/Tabs/Toast/Combobox/Table + theme skins), with a
  "which JST tech?" badge and a 10-framework re-skin demo
  (`examples/components_cross_section.html`). **Preview** - not yet packaged for
  distribution.
- **Framework-parity expansion.** Svelte, Solid, and Angular added to the study
  (18 JST reimplementations) - now **9 frameworks / 126 examples**.

### Tooling

- `tools/lint.mjs` gains an open-tag rule that statically flags the removed
  `props=` keyword on a `<script type="jst">` tag.

## 0.2.3 - 2026-06-24

Adds the runtime-only builds (no compiler) for precompiled deployments, a
`--global` precompile target to match, and a full "which build, and when" guide.
No breaking changes (patch bump).

### Added

- **Runtime-only builds - `jst.runtime.js` (ESM) and `jst.runtime.global.js`
  (+ `.min`, classic).** Same runtime as the full builds with the compile
  pipeline omitted (`compiler`/`parser`/`lexer`/`interpreter`/`tokens`/
  `input_reader`), for apps whose templates are precompiled. About **40% smaller**
  (~16.6 KB vs ~28.2 KB minified) and inherently strict-CSP - no `new Function`.
  They render only precompiled templates; an inline `<script type="jst">` that
  reaches them throws a clear "precompile this" error. (#5)
- **`precompile.mjs --global`** - emits a classic script that reads
  `window.JST.registerPrecompiledTemplate` (no ES import), to pair with
  `jst.runtime.global.js` for a no-build/`file://` precompiled drop-in. The
  default ESM output now recommends `--runtime ./jst.runtime.js`.
- **`examples/runtime_precompiled.html`** - browser smoke-tested page loading the
  runtime-only global build with a precompiled template.

### Docs

- A full delivery-modes guide in `install.md`: the four client builds
  (full/runtime-only × ES-module/classic) with a quick chooser, a
  Precompiled + runtime-only section, and a table disambiguating the **two build
  tools** - `precompile.mjs` (compiles *your* templates) vs `build_global.mjs`
  (bundles *the framework*). `production.md` documents the prod-and-dev
  "server compiles, client renders" workflow; `known-gaps.md` and README updated.

## 0.2.2 - 2026-06-24

Adds the classic/global build so JST runs with no server (and from `file://`),
fixes a latent bug in the inlined standalone demo, and documents the delivery
modes. No breaking changes (patch bump).

### Added

- **`jst.global.js` + `jst.global.min.js` - classic/global build.** The whole
  runtime concatenated into one non-module script that exposes `window.JST` and
  self-initializes. Because it has no `import` statements it loads from `file://`
  with no server and no build step - for prototypes, copied/generated single
  files, and one-line CDN drop-ins. Built from the module sources by
  `npm run build` (`tools/build_global.mjs`); a `build:check` mode in CI keeps the
  committed artifacts in sync with `jst.js`. Both ship on npm and as release
  assets. (file-open mode, previously listed as planned)
- **`examples/global_build.html`** - a browser smoke-tested page that loads the
  global build via a plain `<script src>` and renders a component.

### Fixed

- **`concerns-standalone.html` was broken at runtime.** The hand-assembled inline
  bundle stripped `import * as Tokens from './tokens.js'` without re-creating a
  `Tokens` object, so the lexer threw `ReferenceError: Tokens is not defined` as
  soon as it tokenized a template - the "open directly from `file://`" demo never
  worked. It is now regenerated from the modules by the build (which emits the
  namespace shim) and covered by a functional test.

### Docs

- **Delivery modes** (ES module / global build / precompiled), the global build,
  `file://` usage, CDN pinning, and dev-vs-prod serving documented in
  `install.md`, with `production.md` and `known-gaps.md` updated to match
  (file-open mode marked shipped).

## 0.2.1 - 2026-06-24

Follow-up release from upgrading a real server-rendered app to v0.2. Fixes a
correctness gap in initial-load template resolution, ships migration tooling so a
breaking-syntax leftover fails the build instead of a browser render, exposes the
runtime version, and fills two doc gaps. No breaking changes (patch bump).

### Fixed

- **`resolveTemplate` now resolves components already in the initial HTML.**
  Auto-init runs at module eval - before an importing module can call
  `configure({ resolveTemplate })` - so components present in the server-rendered
  HTML were scanned while the resolver was still `null` and silently never
  upgraded (only later-injected components worked, via the observer). `configure()`
  now re-runs the missing-template scan when a resolver is set after init. The scan
  is idempotent (it ignores already-registered names and coalesces in-flight
  fetches), so this is safe and cheap. Consumers can delete the eager
  fetch-and-register workaround. (#21)

### Added

- **`tools/codemod.mjs` (`jst-codemod`)** - mechanical `@event` → `on<event>`
  migration that rewrites bindings only inside `<script type="jst">` blocks
  (preserving modifiers and the `$(...)` value), so it is safe to run over server
  views and fragments without touching `@media`, decorators, emails, or
  other-framework `@click`. `--dry-run` previews. (#22)
- **`tools/lint.mjs` (`jst-lint`)** - scans every `<script type="jst">` block
  across any file type for removed/renamed syntax (`@event`, `raw()`,
  `unsafeHTML()`, `document.jst`) and exits non-zero with `file:line:col`, turning
  a render-time-only failure into a build/CI failure. `--runtime jst.js` also
  flags a stale vendored runtime (`jst-ssr`/`document.jst`). Dogfooded over JST's
  own surfaces via `npm run test:lint`. (#22)
- **`JST.version`** - the loaded runtime version as an ES export (`import { version }`)
  and on `window.JST`, sourced from `package.json` and kept in sync by a drift test.
  With `configure({ dev: true })` the runtime also logs `JST x.y.z` once on load,
  so confirming an upgrade (vs. a stale cache that renders identically) is a
  one-liner. (#23)

### Docs

- **Serving and caching the no-build assets** - dev `Cache-Control: no-cache`,
  prod fingerprint/version - in `install.md`, to preempt stale-asset confusion. (#24)
- **Server-rendered initial data and large payloads** - JSON attribute for small
  structured data; a `<script type="application/json">` sidecar read in `once()`
  (or slot projection) for large/newline-heavy payloads - in `writing-jst.md`. (#24)
- **Upgrading across breaking releases** and **Which version is live** sections in
  `install.md` documenting the codemod, lint, and `JST.version`.

## 0.2.0 - 2026-06-23

Breaking cleanup release. Event-handler syntax moves to the native `on<event>`
form, the trusted-HTML helper is consolidated to a single name, the `document.jst`
global is removed in favour of the ES-module exports plus a reduced `window.JST`,
and the unrequested `jst-ssr` adoption feature is removed because it contradicts
JST's rendering model (the server ships a component fragment + data; the client's
`jst.js` is the sole renderer - nothing pre-rendered is inserted into the DOM).
Major stays `0`; per 0.x semantics the breaking changes bump the minor.

### Changed (breaking)

- **Event handlers use `on<event>` instead of `@event`.** `@click="$(fn)"` becomes
  `onclick="$(fn)"`; modifiers are retained on the new name
  (`onclick.stop`, `onkeydown.enter.prevent`, `onsubmit.prevent`,
  `oninput.debounce.300`, `onclick.outside`, `onclick.once`). The value must still
  be exactly one `$(...)` expression. Leftover `@event="$(...)"` now throws a
  compile error pointing at the `on<event>` replacement.
- **Raw inline JavaScript in `on*` attributes is rejected.** An `on*` handler whose
  value is not a single `$(...)` expression (e.g. `onclick="alert(1)"`) is a
  compile error, so a JST `on*` handler can never silently become a native inline
  handler.
- **`on*` is reserved for event handlers; the event name must start with a
  letter.** An `on…="$(...)"` attribute whose event name does not start with a
  letter (e.g. `on3d-ready`, `on-foo`) now throws a clear compile error - and is
  flagged in the VS Code editor (diagnostics run the real compiler) - instead of
  silently degrading to a literal attribute (#19).

### Removed (breaking)

- **`raw()` and `unsafeHTML()` helpers.** Use `trustedHTML()` - now the only
  opt-out-of-escaping helper. The render-function helper signature drops `raw`/
  `unsafeHTML`; precompiled output is regenerated accordingly.
- **`document.jst`.** State now lives in the module; the ES-module exports are
  canonical and a **reduced `window.JST`** global mirrors them (`configure`,
  `trustedHTML`, `url`, `registerCustomElementFromTemplate`,
  `registerPrecompiledTemplate`, `initializeTemplates`, and the live `config`).
  `window.JST` no longer exposes `raw`, `unsafeHTML`, or `templates`.
- **`jst-ssr` / SSR adoption.** The `#hydrating` field and all `jst-ssr` handling
  are gone (slot capture/observe and slot detach are unconditional again). The SSR
  hydration test, the "SSR hydration plus projected slots" known-gap, and the SSR
  claims in the precompile note and docs are removed. Passing trusted rendered
  content (e.g. markdown) as an attribute placed with `trustedHTML(...)` is still
  legitimate - that is data on an attribute, not pre-rendered UI structure.

### Added

- **Light-DOM id-collision guidance.** `docs/writing-jst.md` gains a worked
  "Avoiding id collisions in the light DOM" section (prefer classes + scoped
  `el.querySelector`; derive a unique per-instance id for `label for`/`aria-*`),
  and `docs/known-gaps.md` links to it.
- **Standalone-openable landing examples.** Each embedded example on `index.html`
  has a prominent, keyboard- and screen-reader-accessible **"Open standalone ↗"**
  action so it can be interacted with, inspected in DevTools, and view-sourced on
  its own page, without the iframe scroll/interactivity trap.

### Fixed

- **Nested `.prop` propagation on a parent morph (regression-tested).** When a
  parent re-renders (including via `setAttribute`, the morph-by-attribute pattern),
  data passed to a nested managed child via a `.prop="$(expr)"` binding now
  provably flows into the child's props - `morphNode` syncs the fresh binding
  markers onto the existing managed child before its early return, and the parent's
  whole-subtree binding pass re-applies them. New runtime tests cover the
  string/object `setAttribute` path and a router-parent-delegating-to-child case,
  and a load-bearing comment documents why the order must not change.
- **`on*`/`.prop` sequences in template text are no longer misread as bindings.**
  Binding detection is now gated to genuine tag-attribute position (tracking
  tag/quote state, so a `>` inside an earlier attribute value doesn't defeat a
  later binding), so prose like `online="$(x)"` in text content is left as literal
  text instead of binding a spurious `line` event or erroring (#19).

### Migration (old → new)

| Old (0.1.0) | New (0.2.0) |
|---|---|
| `@click="$(fn)"` | `onclick="$(fn)"` |
| `@keydown.enter.prevent="$(fn)"` | `onkeydown.enter.prevent="$(fn)"` |
| `@submit.prevent` / `@click.outside` / `@click.once` | `onsubmit.prevent` / `onclick.outside` / `onclick.once` |
| `@my-event="$(fn)"` (custom event) | `onmy-event="$(fn)"` |
| `onclick="doThing()"` (raw inline JS - was silently a native handler) | `onclick="$(doThing)"` (must be one `$(...)`) |
| `$(raw(x))` / `$(unsafeHTML(x))` | `$(trustedHTML(x))` |
| `document.jst.configure({...})` | `import { configure } from './jst.js'` - or `window.JST.configure({...})` |
| `document.jst.config` | `window.JST.config` (or the `config` returned by `configure`) |
| `window.JST.raw` / `window.JST.unsafeHTML` / `window.JST.templates` | removed - use `trustedHTML` / ES imports |
| `<my-cmp jst-ssr>…server HTML…</my-cmp>` | removed - send the component fragment + data (`<my-cmp …></my-cmp>`) and let `jst.js` render it |

## 0.1.1 - 2026-06-21

### Changed

- **`once()` runs after the DOM commits.** The template body runs as a
  string-build pass before the rendered DOM and projected slots exist, so
  `once()` setup that touched the component's own DOM ran too early. Setup is now
  deferred to a microtask that runs after the render commits. The function setup
  returns is registered as disconnect cleanup, so hosting a third-party widget is
  `once('key', () => mount(el))` with no manual teardown wiring. A per-connection
  epoch discards a stale setup across a synchronous disconnect then reconnect.

### Documentation

- **Lifecycle and `once()` timing.** The string-build vs commit model, inline
  `${ ... }` vs `once()`, and hosting a third-party widget through a slot so the
  morpher never recreates its nodes.
- **Component granularity.** Guidance on when not to make a component: inline a
  library rather than wrap it in a slot-only component, and server-render a
  static surface with a module instead of a component.
- **Known gaps.** Calling module code from a template currently needs a global,
  and there is no uncontrolled-region directive for template-generated widget
  hosts.


## 0.1.0 - 2026-06-17

First release: a fail-loud, no-build component model with a real reconciler, a
proper lifecycle, a strict-CSP production path, and a HATEOAS-first story. This
release integrates two parallel hardening efforts into one runtime.

### Added

- **`props="..."` declaration model.** Components declare their inputs explicitly
  via a `props` attribute (case-preserved) instead of inferring them from bare
  attributes. Fixes reserved attribute-name collisions (`class`, `id`, `style`)
  and the camelCase/kebab-case mismatch between HTML attributes and template
  variables. Reserved/helper names (`class`, `el`, `raw`, JS keywords) are
  rejected at compile time.
- **Keyed reconciliation via `jst-key`.** List rendering matches nodes by key
  across updates, preserving element identity, DOM state, focus, and listeners
  instead of rebuilding the subtree (incl. reorder, mixed keyed/unkeyed siblings,
  nested keyed lists, table rows, SVG).
- **Property-aware, focus-safe form morphing.** Updates patch live properties
  (`value`, `checked`, `selected`) rather than attributes; controlled
  (template-declared) values win, uncontrolled fields keep user state, and focus
  and caret/selection survive unrelated re-renders.
- **CSS transitions via `jst-transition`** (Vue-style enter/leave classes; the
  framework toggles classes and timing, CSS animates; leave waits for
  `transitionend` with a fallback).
- **Dynamic slots.** Default, named, and late-inserted slot content project
  correctly (`$(slot())` / `$(slot('name', 'fallback'))`).
- **Lifecycle hooks `once()` and `onDisconnect()`** for one-time setup and
  teardown on disconnect (incl. document-listener cleanup for `@event.outside`).
- **`@event` modifiers:** `.prevent .stop .self .once .capture .passive`, key
  guards, `.debounce[.ms]`, and `.outside` (document-level, cleaned up on
  disconnect).
- **`jst-model` local form sugar.** Binds a control to the component's own host
  property (read `el[prop]`, write `el[prop]` on input) - local component-owned
  UI state. Parent/server-owned state stays explicit with `.value` + emitted
  events, keeping the boundary props-down / events-up.
- **`url()`, `raw()` / `unsafeHTML()` helpers** for URL-scheme sanitizing and
  explicit, opt-in raw HTML insertion.
- **Trust-boundary configuration.** `document.jst` / `window.JST` expose
  `configure`, helpers, and registration. `configure({ dev, autoRegister,
  autoRegisterRoot, resolveTemplate })` controls dev errors, fragment
  auto-registration, its scope, and lazy template resolution. With
  `autoRegister: false`, inserted `<script type="jst">` are ignored but a trusted
  `resolveTemplate` allowlist can still resolve missing components.
- **Precompiled / strict-CSP mode.** `tools/precompile.mjs` compiles templates to
  a plain ES module (no `new Function`) that registers through the normal runtime
  (`registerPrecompiledTemplate`), so morphing, keyed reconciliation, form-state,
  and modifiers are all preserved under `script-src 'self'`.
- **Dev-mode error overlay** (`.jst-error`) and contained, fail-loud render/compile
  errors.
- **HATEOAS service-worker demo** (`demo/hateoas/`) - HTML fragments that carry
  their own auto-registering `<script type="jst">` definitions.
- **CI** (`.github/workflows/ci.yml`): node + browser + examples + parity +
  agentic + VS Code tooling, with a real Chrome gate via `CHROME_PATH`.

### Fixed

- **Post-commit `once()` setup.** Lifecycle setup now runs in a microtask after
  rendered DOM is committed, skips setup if the element disconnects first, and
  still registers a returned disconnect cleanup.
- **Fail-loud lexer/compiler.** Malformed bindings throw at compile time instead
  of silently degrading - including `.prop`/`@event` values with more than one
  `$(...)` expression or literal text around the expression. Compile errors are
  contained to the offending component rather than taking down the page.
- **JS-token-aware scanner.** `$(...)` / `${...}` and the `$ line` directive skip
  strings, template literals (with nested `${}`), regexes, and comments;
  regex-vs-division and `<`-tag-vs-less-than are resolved by the previous
  significant token. Identifiers with trailing digits (`$item1`) lex correctly.
- **Inert HTML comments.** `<!-- ... -->` content is ignored by the compiler and
  never treated as a binding.
