# Changelog

## [0.24.2] - 2026-10-15

### Fixed

- **The `@iyulab/router/react` type declarations compile on their own.** They carried the global tag and event map
  merges (`u-outlet`, `route-begin` … `route-error`, `u-router-navigate`) without the types they name, so a project
  checking library types (`skipLibCheck: false`) failed with six errors when it imported the React entry. Those names
  now refer to the main entry's exports. Types only — runtime unchanged.

### Added

- `@iyulab/components` (>=1.41.0) as an optional peer dependency. `u-error-page` reads its font from
  `--u-font-base` when that sheet is loaded and falls back to its own stack otherwise; nothing else changes.

## [0.24.1] - 2026-10-14

### Fixed

- `u-error-page` takes its font from `--u-font-base` when the page defines it (falling back to the previous stack),
  so the error screen matches a themed app.

## [0.24.0] - 2026-10-13

### Added

- **`leave` guards** — `RouteConfig.leave` and `RouterConfig.leave`, the other side of `enter`. A guard runs before
  the current screen is replaced and returns `false` (or a promise of it) to stay: the screen and the address do not
  change. It receives `{ from, to }` (`RouteLeaveContext` — `to` is the destination's address; its `params` and
  `metadata` are empty). It runs for every navigation that changes the path or query: links, `go()`, `navigate()`,
  a selection channel that goes through the router, Back and Forward. A hash-only change, redirect hops and the
  first route do not ask. Route guards of the current screen run child first, then the global one; a guard that
  throws counts as `false`. Leaving the document (reload, another site, closing the tab) is `beforeunload`'s job.
- **Guards that answer right away cost nothing.** Guards are asked in order without waiting while they return
  booleans, so a navigation whose guards say `true` at once goes on in the same tick, as before. Only a guard that
  returns a promise (an in-app dialog) is waited for.
- **Back and Forward can be held.** The History API cannot cancel a traversal, so when a guard does not let one
  through right away the router returns to the entry you were on, waits for the answer, and goes the same number of
  entries again if the guard allows it. To count entries, history state now carries `entryIndex` next to `entryKey`.

### Changed

- **A selection channel follows the router's `leave` answer.** With `query` and a running router, a pick that a
  guard blocks leaves the channel as it was; while a guard is still answering (a dialog), the value changes when the
  navigation starts (`route-begin`). A pick the guards let through at once changes the value in `set()`, as before.
  Back and Forward to an entry the router wrote are followed through the router too.
- **A second copy of the router says so.** When `<u-link>`, `<u-outlet>` or `<u-error-page>` is already defined by
  a different class, loading the router throws an error that names the likely cause — two copies of
  `@iyulab/router` in one app — and how to check it (`npm ls @iyulab/router`), instead of the browser's
  «name has already been used». Two copies do not share the running router or selection channels.
- The installation section says to keep one copy, and that a package declaring the router as a broad peer (`"*"`)
  can keep an older copy installed after an upgrade without any install error.

## [0.23.0] - 2026-10-12

### Added

- **Selection channels** — `selection<T>(name, options)` gives several views one «current pick» to follow: a list,
  a side panel and an open tab share it by name. Each value comes with an `AbortSignal` that the next pick aborts, so
  a late answer for an earlier pick is dropped. With `query` the address is the source — deep links, reloads and Back
  carry the pick (`history: 'replace'` by default, `'push'` for an entry per pick); with a router the change goes
  through it, without one only the history entry is rewritten. `follow()` lets a view pin itself. Lit
  `SelectionController` and React `useSelection` (`@iyulab/router/react`) bind it to a view.

## [0.22.0] - 2026-10-12

### Added

- **`RouteContext.navigation`** — `{ type, entryKey }`. `type` is how the route was entered: `push`, `replace`,
  `reload` or `traverse` (Back/Forward returned to an existing entry) — the platform Navigation API's vocabulary.
  `entryKey` names the history entry: a new entry gets a new key, `replace` keeps the slot's key, and coming back
  to an entry gives its key again, so per-entry memory (scroll, selection) can be restored on `traverse`. The key is
  kept in `history.state` with the router's other fields, which now win over same-named fields in `state`.
- **`RouterConfig.scrollRestoration`** (off by default) — per-entry scroll memory. Back, Forward and a reload return
  to where you were reading; a new navigation starts at the top (or the `#hash` element); `replace` leaves the
  position alone. A screen that grows after rendering is waited for (up to 1.5 s) until it reaches the saved
  position, and scrolling first cancels the restore. `{ container: () => el }` names an inner scrolling box — a shell
  whose main area scrolls instead of the document, which the browser's own restoration does not reach. While on,
  `history.scrollRestoration` is `'manual'`.
- **`navigate(href, options)`** — asks the running router for a navigation without holding it (a shell, an
  auth-expiry handler). With no router listening it becomes a document navigation.

### Changed

- **`<u-link>` no longer pushes a history entry and dispatches a synthetic `popstate`.** It asks the router to
  navigate (`navigate()`), so a `popstate` your app hears is a real Back or Forward, and the new entry carries the
  router's state. Code that imitated the old idiom (`history.pushState(…)` + `dispatchEvent(new PopStateEvent(…))`)
  still routes, as a `traverse`; move it to `navigate()`.
- `RouteContext` has a required `navigation` field — a hand-built context needs one.

### Fixed

- **A history entry keeps its state when you come back to it.** `go(href, { state })` promised the state could be
  read again from `history.state`, but Back and Forward routed through the same `go()` that overwrote the entry with
  `{ basepath, mode }`, so the state was gone the moment the entry returned — the browser had handed it back in the
  `popstate` event. A reload did the same. Entering the same entry (Back, Forward, reload, navigating to the current
  address) now updates the router's fields on top of the entry's state; navigating to the current address with a new
  `state` adds to it. Replacing the entry with another address (`replace`, redirects) still starts fresh.

## [0.21.2] - 2026-10-09

### Fixed

- **Expected routing outcomes are no longer logged as errors.** Every failed navigation went to `console.error` as
  `Routing error: …`, including the ones the fallback draws as normal screens — no route matched (404), a guard's
  refusal (403), a route's own `NotFoundError`. An app whose monitoring collects console errors received one for
  every navigation a user was not allowed to make. Failures with a 4xx code are now left to the fallback and the
  `route-error` event; a failed load or render is still logged with its cause.

## [0.21.1] - 2026-10-08

### Fixed

- **The fallback's tab title no longer outlives the failure.** A route without a `title` leaves the document title
  as it is, so after a failed navigation (404, a guard's refusal) the next successful route kept the failure's title
  ("Page not found") instead of the app's own. The router now remembers the title from before the failure and
  restores it when a route without a title completes. A route with a `title` sets it as before.

## [0.21.0] - 2026-10-08

### Added

- **`fallback.title` may be a function of the failure** — `(ctx) => string | undefined`, called with the same
  context as `fallback.render`. A string gave every failure the same tab title, so a fallback that drew different
  screens for 403 and 404 could not title them apart, and without a title the tab showed the diagnostic message
  (`Page not found: http://…/x`). Returning nothing still falls back to the message.

### Changed

- **A `RouteError` thrown from a route's `render()` reaches the fallback unchanged.** It used to be wrapped in a
  `ContentLoadError` (code `CONTENT_LOAD_FAILED`), so a screen could not say "this record does not exist" with
  `throw new NotFoundError(ctx.pathname)` and get the 404 screen. Anything else thrown from `render()` is still a
  `ContentLoadError` carrying the original.

### Documentation

- The skill reference listed error codes that do not exist (`NOT_FOUND`, `CONTENT_LOAD_ERROR`,
  `CONTENT_RENDER_ERROR`). It now lists the real ones (`404`, `403`, `OUTLET_MISSING`, `CONTENT_LOAD_FAILED`,
  `CONTENT_RENDER_FAILED`) with their classes, and its fallback example compares `code === 404`.

## [0.20.2] - 2026-10-08

### Fixed

- **`hidden` hides `u-error-page`.** Its host's `display: flex` outranked the browser's `[hidden]` rule, so a hidden
  error page kept drawing.

## [0.20.1] - 2026-10-07

### Fixed

- **Links to the app root keep the trailing slash under a sub-path basepath.** With `basepath: '/app/'`, a `<u-link>`
  without `href` (the usual logo link) rendered `/app`, clicking it put `/app` in the address bar, and `go('')`
  did the same. Servers serve a sub-path app at `/app/` — Vite answers `/app` with 404 — so opening the logo in a
  new tab, copying its address, or reloading after clicking it left the app. All three now use `/app/`; other
  relative paths are joined as before.

## [0.20.0] - 2026-10-06

### Added

- **`currentRoutePath()`** — the current location as a route path (path + query + hash, the `RouteContext.path`
  form), read in the router's mode with no router instance. A widget that follows `route-begin` and is connected
  after the first navigation — a shell rendered as a nested route's parent — had missed that event and knew no
  current route until the next navigation; it now starts from this.

## [0.19.2] - 2026-10-06

### Documentation

- **The README's Quick Start runs as written.** It passed `document.body` as `root`, which has no
  `<u-outlet>`, so copying it produced "Router outlet element not found" and an empty page — the
  note about the outlet came after the code. The example is now one complete page: the outlet in
  the markup and the router in a module script.

## [0.19.1] - 2026-10-06

### Fixed

- **A `<u-link>` rendered before the router is created now follows the router's mode and basepath.**
  A link reads them when it renders; an app that draws its shell (and the links in it) before
  creating the router left those links in the history form on a first visit — in `hash` mode a link
  to `/users` kept `href="/users"`, so opening it in a new tab or copying its address led to a
  different screen. Clicks were unaffected. The router now announces its state when it is created,
  and connected links redraw their address.

## [0.19.0] - 2026-10-05

### Changed

- **`lit` is an optional peer dependency instead of a dependency.** `<u-link>` and the built-in error
  page are now standard custom elements, and `<u-outlet>` loads Lit only when a route returns a Lit
  template. An app whose routes return elements or React content no longer installs Lit.
  **Migration:** an app whose routes return Lit templates already has `lit` — nothing to do; if it
  came only through the router, add `lit` to your dependencies.
- **`<u-link>` updates synchronously and no longer has `updateComplete`.** Setting `href`, `target`,
  `rel`, `navigate` or the forwarded `aria-*` attributes rewrites the inner `<a>` at once. Code that
  awaited `link.updateComplete` can drop the `await`.

## [0.18.0] - 2026-10-05

### Added

- **Hash mode: `new Router({ mode: 'hash' })`.** The route lives after `#` (`/app/#/orders/7`), so a
  refresh or a deep link works on a static host that serves one document, with no server-side
  fallback. `basepath`, route paths, `go()`, `ctx.pathname` and `ctx.query` apply to the part after
  `#`; `<u-link>` renders `#/…` hrefs; back/forward, `#/…` anchors and editing the address's `#` all
  route. The default stays `'history'`. `router.mode` reports which one is in use.

### Changed

- Optional React peers promise only the tested majors: `@lit/react ^1.0.8`, `react` and
  `react-dom` `^18.0.0 || ^19.0.0` (were `>=` ranges with no upper bound).

## [0.17.0] - 2026-09-28

### Changed

- **`@lit/react` is now an optional peer dependency instead of a dependency.** Only the
  `@iyulab/router/react` wrappers use it, yet every app installing the router installed it —
  the same arrangement `@iyulab/components` and `@iyulab/modern-app` already use for their React
  subpaths. **Migration:** if you import `@iyulab/router/react`, add `@lit/react` alongside
  `react`.

## [0.16.1] - 2026-09-28

### Fixed

- **`route-done` now fires after a React route has committed.** `<u-outlet>` handed React
  content to `root.render()`, which only schedules the commit, and reported the route done right
  away — so anything listening for `route-done` (a layout placing focus on the new screen, a
  title or analytics hook reading the page) saw an empty outlet, and an `autofocus` field in a
  React screen was never found. The outlet now commits the React tree before it returns, and for
  Lit content waits for the top-level elements' first render, so `route-done` means the new
  screen is on the page.

## [0.16.0] - 2026-09-27

### Changed

- **`<u-outlet>` no longer creates a box: its rule is now `display: contents`.** A route screen
  lays out as if it were a direct child of the element that contains the outlet. The 0.15 rule
  (`display: grid; min-height: 100%`) broke the most common list screen: a screen that fills the
  area with `height: 100%` and holds a table with `flex: 1; min-height: 0` and more rows than fit.
  A grid track is sized from its item's content, and during that sizing the screen's
  `height: 100%` counts as `auto` — so the track, the screen and the table grew to every row, and
  the table never scrolled. The 0.14 rule (`height: 100%`) handled that screen but pinned the
  outlet, so a long flowing screen overflowed it and the container's bottom padding dropped out of
  the scroll. No single box gives the parent's height to filling screens and the content's height
  to flowing ones; with no box, each screen gets its parent directly.
  **What changes for you:** `padding`, `background` or `border` on `u-outlet` no longer apply, and
  its `getBoundingClientRect()` is all zeros — style the containing element or the screen instead.
  To give the outlet a box again, write `u-outlet { display: block; }` (specificity zero, no
  `!important` needed). The 0.15 print rule and column track are gone with the box: there is no
  box to trap a last block's margin in print, or to widen past its parent.

## [0.15.2] - 2026-09-23

### Fixed

- **A wide descendant no longer widens the route screen past the outlet.** 0.15.0 made
  `<u-outlet>` a grid container without declaring a column track. The implicit column is `auto`,
  and a grid item's default `min-width: auto` passes its content's minimum width up to the track,
  so a wide table widened the whole screen — even inside an `overflow-x: auto` box — and anything
  aligned to the screen's right edge, such as a toolbar's last button, was pushed off screen. The
  track is now `minmax(0, 1fr)`: the screen is exactly as wide as the outlet, a narrow screen still
  fills it, and wide content scrolls inside its own box. Print is unaffected (the outlet is a block
  box there). The rule keeps zero specificity, so `u-outlet { grid-template-columns: auto; }`
  restores the previous track.

## [0.15.1] - 2026-09-17

### Fixed

- **Printing no longer adds a trailing blank page when a screen's last block has a bottom
  margin.** 0.15.0 made `<u-outlet>` a grid container in every medium. A grid container keeps
  its children's margins inside its own box, so the last block's bottom margin added to the
  outlet's height; when the content ended within that margin of a page boundary, the outlet
  spilled onto a page that held only the margin. In print media the outlet is now a block box,
  which lets the margin collapse past it and be truncated at the page break:

  ```css
  @media print { :where(u-outlet) { display: block; } }
  ```

  Screen layout is unchanged. The grid only matters when the parent has a definite height, and
  an application shell normally sets one for screen media only.

## [0.15.0] - 2026-09-16

### Changed

- **`<u-outlet>` now grows when a screen overflows a fixed-height parent.** 0.14.0 declared
  `display: block; height: 100%`, which gives a screen the height it needs to fill a sized
  parent but also pins the outlet to exactly that height. A screen taller than the parent
  therefore overflowed the outlet's box instead of growing it, and a scrolling ancestor adds
  its end padding to the scrollable area of its *in-flow children*, not of what those children
  overflow with. The end gutter dropped out of the scrollable area: scrolling to the bottom of
  a long screen left the content flush against the container's edge, while the gutter survived
  on the other three sides.

  Swapping `height` for `min-height` is not the fix. A percentage height only resolves against
  a parent whose `height` is specified, and a minimum does not satisfy that — so on a block (or
  flex) box a descendant's `height: 100%` computes to `auto`, and every layout built to fill the
  viewport collapses to its content height.

  A grid container gives both behaviors at once. A grid item stretches to its area by default,
  which fills without resolving a percentage, so the chain survives while the container's own
  height is indefinite — and because that height is `auto`, the box grows past the minimum when
  content demands it. The rule is now:

  ```css
  :where(u-outlet) { display: grid; min-height: 100%; }
  ```

  Multiple children behave as before: a track stretches, but an item with a definite height does
  not. The rule deliberately omits `align-content` and relies on the initial `normal` — setting
  `align-content: start` stops the track from stretching and silently voids the fill chain.

  Specificity is still zero, so an application's own `u-outlet { … }` rule wins without
  `!important`, exactly as in 0.14.0. `u-outlet { display: block; height: 100% }` restores the
  0.14.0 box model and `u-outlet { display: inline }` restores the pre-0.14.0 one.

### Contract addition

- **Making the outlet *smaller* now takes two declarations.** `min-height` is a floor, so
  `u-outlet { height: 200px }` on its own still measures the parent's height. Write
  `u-outlet { min-height: 0; height: 200px }`. Making the outlet larger, or replacing its
  `display`, needs nothing extra. This is the only behavioral difference for an application
  that was already overriding the rule.

### Tests

- The browser suite covering the outlet's box model grows from 9 cases to 15. The overflow
  topology — a parent with a definite height holding a taller child — was not covered before,
  which is why the previous rule shipped green: the suite pinned "an auto-height parent leaves
  the outlet auto" but never its opposite. Also pinned now: the two-declaration override above,
  multiple children keeping their own heights, and `align-content` breaking the fill chain.

## [0.14.0] - 2026-09-16

### Changed

- **`<u-outlet>` now declares its own `display: block`.** A custom element's UA default is
  `inline`, and the outlet had no styles of its own — so a container meant to hold a route's
  block-level screen generated an inline box. Consumers had to restate `u-outlet { display: block }`
  in every application that cared, which is a rule only the package that defines the element can
  reasonably own.

  Two independent applications reported this from opposite directions on the same day — one
  through printing (a short document gaining a blank trailing page), one through screen layout
  (a table not reaching the bottom of the viewport). Both traced it to the same missing
  declaration.

  The declaration is `display: block; height: 100%`. The height half is not cosmetic: changing
  `display` alone also changes which box a percentage height resolves against. While the outlet
  was inline it was not a block container, so a screen's `height: 100%` resolved against the
  block above it; making the outlet a block moves that reference onto the outlet itself, whose
  height is `auto`, which silently voids the percentage and collapses full-height layouts to
  their content height (measured: 747px → 60px for a screen built on
  `@iyulab/modern-app`'s master-detail layout). `height: 100%` restores the chain, and resolves
  to `auto` whenever the parent's height is `auto` — so ordinary document flow and printing,
  where the shell releases its height, are unaffected.

  The rule is adopted as a constructable stylesheet on whichever tree the outlet is connected to
  (the document, or the shadow root when the outlet lives in one), written as
  `:where(u-outlet)` so its specificity is zero — any `u-outlet { … }` rule an application writes
  still wins without `!important`, regardless of sheet order. Where constructable sheets are not
  available the rule is added as a `<style>` element instead.

  **This changes layout in normal flow**, not only when printing: an inline box and a block box
  differ in margin collapsing, and a block outlet can be given a height or a percentage size,
  which an inline one silently ignored. Applications that place the outlet inside a flex or grid
  container see no change — flex and grid items were already blockified. To keep the previous
  behavior, set `u-outlet { display: inline }`; to keep the box but not the height, set
  `u-outlet { height: auto }`. Neither needs `!important`.

## [0.13.0] - 2026-09-13

### Removed

- **`RouteConfig.force`** — deprecated in 0.12.0, removed as announced. It only ever expressed
  the two values `key` already covers: `force: false` is `key: () => ''` (keep the mounted
  content across every navigation that matches the route), `force: true` is the default
  (`ctx => ctx.href` — remount when the URL changes at all). Replace one with the other; a
  route that never set `force` is unaffected. The property is gone from the type, so a stale
  usage fails at compile time rather than silently doing nothing.

## [0.12.0] - 2026-09-13

### Added

- **`RouteConfig.key` — the route decides when its content is remounted.** Each navigation
  computes `key(ctx)`; while it is unchanged the outlet keeps the mounted content and re-renders
  it in place with the new `ctx` — a Lit template is rendered into the same part (elements and
  their state survive, only bindings change), a React element into the same root (component
  state survives, props change), and an `HTMLElement` is left as it is. A changed key unmounts
  and mounts fresh, as every navigation did before. Click interception, `go()` and `popstate`
  all follow the one rule because it lives on the route, not on the call. The default is
  `ctx => ctx.href` for leaf routes, so nothing changes until a route opts in:
  `{ path: '/orders', key: ctx => ctx.pathname, render: ctx => html`<orders-page .selectedId=${ctx.query.get('id')}></orders-page>` }`
  keeps the list — its scroll, selection and loaded rows — while `?id=` opens and closes a
  detail overlay.

### Changed

- **A layout route (one with `children`) is now re-rendered in place when a child changes.** It
  was already kept across child navigations, but its `render(ctx)` result was thrown away, so a
  layout could never react to the current `ctx` (an active-menu highlight, a breadcrumb). It now
  receives the new `ctx` through the same in-place path; its inner `<u-outlet>` and the child
  content are untouched.
- **Outlet renders are serialised.** A render that arrives while a React mount is still awaiting
  its dynamic import now waits for that mount instead of racing it.

### Fixed

- **`force: false` on a leaf route was silently ignored.** Defaults were applied with
  `route.force ||= true`, which turns an explicit `false` into `true` — the documented option
  could not be set on the routes it was documented for. `force` is now deprecated in favour of
  `key` (`false` ≡ a constant key, `true` ≡ the default) and, for this release, honoured as
  written on every route.

### Deprecated

- `RouteConfig.force` — use `key`. Still honoured in this release; removed in the next minor.

## [0.11.5] - 2026-09-10

### Fixed

- **`u-error-page`'s three colour hooks were documented as having no default.** They were
  described as falling back to the inherited colour, when the host block in fact declares a
  concrete colour for each — `--error-icon-color` `#4a5568` (dark `#a0aec0`),
  `--error-code-color` `#1a202c` (dark `#f7fafc`), `--error-message-color` `#718096`
  (dark `#cbd5e0`). A consumer reading the old text would expect the page to pick up the
  surrounding colour and find that it does not. The documentation now also states the
  consequence those literals carry: this page does not follow the host application's design
  tokens, and replacing it wholesale is what `fallback.render` is for. No behaviour changed.

## [0.11.4] - 2026-09-01

### Fixed

- **Three error messages gave no way to act on the failure.** `UOutlet.render()`
  threw a generic "not supported content type" without saying what type it
  actually received or what's supported; `getRoutes()` threw a garbled,
  comma-spliced message that leaked an internal function name
  (`setRoutes`) instead of the actual `path` value; and a route whose
  `render()` returned a non-renderable value threw "Failed to load content
  for the route." with no route path or id to identify which route failed.
  All three now interpolate the actual value/route and name the expected
  shape.

## [0.11.3] - 2026-08-31

### Fixed

- **`waitOutlet()` could hang forever in a backgrounded tab.** The wait loop
  only re-checked for the outlet after each `requestAnimationFrame`
  resolved, and a fully suspended tab can stop firing `rAF` entirely (not
  just throttle it), so the loop never exited. It now races each `rAF`
  wait against a `setTimeout` for the remaining budget, and does one more
  outlet check immediately before throwing — the deadline can pass in the
  exact frame the outlet became ready, and without that final check that
  read as a false timeout.

## [0.11.2] - 2026-08-25

### Fixed

- **`aria-current`/`aria-label` set on `<u-link>` never reached the accessibility tree.** The
  host attribute was present, but the actual interactive node exposed to assistive technology
  is the native `<a>` rendered inside the shadow root — ARIA content attributes on a shadow
  host do not cross the shadow boundary to label or mark current a descendant. Navigation
  links relying on `aria-current="page"` to announce the active item, or on `aria-label` for
  their accessible name, were exposed with neither. `render()` now forwards both host
  attributes onto the internal `<a>`, and changes made after connection are observed and
  re-rendered.

## [0.11.1] - 2026-08-20

### Fixed

- **Route matching threw `ReferenceError: URLPattern is not defined` on browsers that ship
  without the URL Pattern API.** The router constructs routes with the global `URLPattern`
  constructor directly; browsers that have not yet shipped it (older Safari and Firefox
  releases are still common in the field) failed synchronously in `new Router(...)`, before
  any route could render. A guarded polyfill (`urlpattern-polyfill`, self-installs only when
  `globalThis.URLPattern` is absent) is now imported by the router's internals, so browsers
  without native support get a working fallback and browsers with native support are
  unaffected at runtime.

### Internal

- Router's own `URLPattern` construction now runs through a small local type shim — the
  polyfill package's bundled types have not yet caught up to the constructor's `options`
  argument (`ignoreCase`), even though its runtime implementation already supports it.

## [0.11.0] - 2026-08-07

### Added

- **`<u-link navigate="document">` — link to a same-origin path the router does not own.**
  Whether a link was "external" was decided by an origin comparison alone, so there was no way to
  point at a same-origin path that is not a SPA route: a static docs site, a server-rendered page,
  a download endpoint, an auth redirect. The router intercepted the click and, with no matching
  route, the screen fell through to not-found.

  The three available escapes all meant something else: a different origin (the requirement is the
  *same* origin — cookies, session, reverse proxy), `target="_blank"` (forces a new tab; same-tab
  navigation was unexpressible), and a `#` fragment (not another document).

  `navigate` defaults to `router`, so existing behaviour is unchanged. The rendered anchor carries
  `data-navigate="document"`, which the router's global anchor delegation also honours — the
  element handler alone is not enough, since a link pointing at a *registered* route would
  otherwise be intercepted there instead.

  The name is deliberately not `external`: this module already uses `isExternalUrl`/`isExternal`
  with origin semantics, and the question here is about the document, not the origin.

### Fixed

- **`sideEffects` omitted the source-resolved entry barrel.** The allowlist covered the built
  artifacts with a directory-wide glob, but the source-form barrel and the `react` wrapper entry
  were outside it. The published artifacts
  were unaffected — the shipped allowlist already covered them — but a consumer resolving this
  package from source (a workspace sibling) could have the barrel elided, dropping the element
  registrations it pulls in. The failure is silent: no error, and unregistered custom elements
  render nothing. The source-form entry points are now declared alongside the artifact ones.

## [0.10.4] - 2026-08-01

### Documentation

- 체인지로그에서 **발견 정황 서술과 내부 참조를 제거**했다. 결함이 무엇이고 어떤 조건에서
  재현되는지는 그대로다. 코드 변경은 없다 — 게시본 문서를 정리하기 위한 패치 릴리스다.

## [0.10.3] - 2026-07-24

### Fixed
- **root 안에 `<u-outlet>` 이 없을 때 초기화 실패가 삼켜지던 문제 수정** (React + Vite 소비 앱의 E2E 실측). `waitOutlet` 은 타임아웃 시 서술적 오류로 reject 하지만, `Router` 생성자가 `void waitOutlet(...).then(...)` 로 거부를 처리하지 않아 폴링 타임아웃(최대 10초) 뒤 unhandled rejection 으로만 새어 나갔다 — 소비자에겐 원인 없는 빈 화면이었다. 이제 `.catch()` 로 `console.error('Router initialization failed:', …)` 에 명시적으로 표면화한다.

### Documentation
- README에 "React + Vite" 섹션 추가 — 아웃렛의 `react-dom/client` 동적 import 가 Vite dev 사전번들(`optimizeDeps`)에서 깨지는 CJS interop 을 `optimizeDeps.exclude: ['@iyulab/router']` + `include: ['react-dom/client']` 로 해소하는 소비 설정 명시(초안 #2).
- Quick Start 에 root 의 `<u-outlet>` 요구 + 부재 시 로그 메시지 명시.

## [0.10.2] - 2026-07-15

### Changed
- Click interception now reuses the route match computed while deciding whether to intercept an anchor, instead of recomputing it inside `go()` — avoids a duplicate `getRoutes` call per intercepted click
- Bumped `@types/node`, `@types/react`, `happy-dom`, `vite`, `vite-plugin-dts`, `vitest` devDependencies

## [0.10.1] - 2026-07-02

### Documentation
- `RouteConfig.render` JSDoc: clarified that returning a React element directly lets `<u-outlet>` auto-manage `createRoot`/`root.unmount()` — wrapping it in a manually-created container `HTMLElement` bypasses this and leaks the React root on route change. Added a React `@example` alongside the existing Lit one.

## [0.10.0] - 2026-06-11

### Changed
- Click interception now checks route matching before `preventDefault()`: same-origin anchors that do not match any registered route are passed through to native browser navigation instead of rendering the fallback (soft-404). This fixes apps with `basepath: '/'` greedily intercepting every same-origin link — e.g. cross-app links on a shared host

### Added
- Vitest-based test infrastructure (`npm test`) with coverage for anchor click interception behavior

## [0.9.3] - 2026-04-24

### Fixed
- Document click handler now scopes interception to the router's `_rootElement` instead of `document`, preventing accidental capture of clicks outside the router's DOM subtree
- Click handler no longer intercepts same-origin anchors pointing outside `basepath` — when `basepath` is not `/`, links targeting paths outside the basepath are passed through to browser navigation instead of falling through to the fallback route

## [0.9.2] - 2026-04-13

### Fixed
- Nested route rendering now waits for child outlet readiness after parent render, preventing children from being rendered into the previous outlet when the next `<u-outlet>` is created inside a component shadow root
- `Router.go()` now awaits `UOutlet.render()` before resolving the next outlet in the matched route chain
- `waitOutlet()` now prefers component lifecycle-aware readiness checks (`customElements.whenDefined`, `updateComplete`, next animation frame) during outlet discovery

## [0.9.1] - 2026-04-08

### Fixed
- Redirect cycle detection no longer triggers false positives on initial navigation — `visit()` check is now skipped for the first `go()` call and only applied within redirect chains (`isRedirect: true`)

## [0.9.0] - 2026-04-08

### Added
- `NavigateOptions` — new optional second parameter for `go()` with `isRedirect`, `replace`, and `state` fields
- `replace` option — navigate without pushing a new browser history entry (`replaceState`)
- `state` option — attach custom state object to `history.pushState` / `replaceState`
- `AccessDeniedError` — new error class (HTTP 403) thrown when an `enter` guard returns `false`; renders an error page with `ACCESS_DENIED` code
- Redirect cycle detection — logs an error and halts routing if the same URL is visited more than once within a single navigation chain
- `UErrorPage` now accepts an optional `RouteError` in its constructor for direct instantiation

### Changed
- **Breaking:** `enter` returning `false` now throws `AccessDeniedError` and renders a 403 error page instead of silently aborting navigation
- Redirect navigations (`isRedirect: true`) use `replaceState` — intermediate redirect URLs are no longer pushed onto the browser history stack
- Global `enter` guard is skipped on redirect hops — runs only once per user-initiated navigation
- Route-level `enter` hooks are deduplicated within a redirect chain — each route's `enter` executes at most once per navigation cycle
- `document.title` is now updated in a `finally` block — title is set regardless of whether routing succeeds or fails

## [0.8.0] - 2026-04-08

### Added
- Added navigation guards via `enter` hooks at both router level (`RouterConfig.enter`) and route level (`RouteConfig.enter`) with redirect/cancel flow support
- Added `rel` attribute support to `<u-link>` for secure external navigation patterns (for example `noopener noreferrer`)

### Changed
- **Breaking:** Renamed route metadata fields from `meta` to `metadata` (`RouteConfig.metadata`, `RouteContext.metadata`)
- Updated nested outlet resolution to prefer child outlet discovery inside the current outlet, improving deep nested route rendering behavior

## [0.7.6] - 2026-04-02

### Fixed
- Added `skills/` and `CHANGELOG.md` to npm `files` field — both were missing from the published package, making `npx skills add ./node_modules/@iyulab/router` non-functional

## [0.7.5] - 2026-04-02

### Changed
- `@lit/react` promoted from optional peer dependency to direct dependency — React integration now works without a separate `@lit/react` install
- Added Agent Skills definition (`skills/iyulab-router`) for AI agent tooling support

## [0.7.4] - 2026-03-05

### Fixed
- `findOutlet()` now searches both shadow DOM and light DOM simultaneously

## [0.7.3] - 2026-03-05

### Changed
- **Breaking:** Removed `RenderResult` and `FallbackRenderResult` types — `render` function return type is now `unknown`
- **Breaking:** Moved React dependencies (`react`, `react-dom`, `@lit/react`) to optional peer dependencies — Lit-only projects no longer require a React install
- Simplified `findOutlet` — removed shadow/light DOM branching and redundant `querySelector` traversal

### Fixed
- `UOutlet` now dynamically imports `react-dom/client` — prevents import failure in React-free environments
- Corrected `UErrorPage` error code string mismatches (`OUTLET_NOT_FOUND` → `OUTLET_MISSING`, `RENDER_FAILED` → `CONTENT_RENDER_FAILED`)
- Fixed edge case in `catchBasepath` where an empty `restPath` was treated as falsy, causing trailing slashes to be dropped
- Changed `ULink.getBasepath()` fallback from `""` to `"/"` to prevent incorrect path generation on initial access
- Applied optional chaining in `Router.go()` catch block to prevent property access errors when a primitive value is thrown

## [0.7.2] - 2026-02-09

### Changed
- Updated dependencies to latest versions

## [0.7.1] - 2026-02-09

### Fixed
- Fixed silent failure when passing `<u-outlet>` element directly as `Router` root (#1)
- `findOutlet()` now recognizes the root element itself as a valid outlet
- Improved `waitOutlet()` timeout error message with root element context for easier debugging

## [0.7.0] - 2026-02-09

### Added
- `meta` field on `RouteConfig` — attach arbitrary key-value data to any route
- `RouteContext.meta` — populated at navigation time with metadata merged from the full matched route chain (parent → child order, child overrides parent)

## [0.6.2] - 2026-01-21

### Changed
- **Breaking:** Refactored `UOutlet` from `LitElement` to native `HTMLElement` for improved performance and reduced bundle size
- **Breaking:** `UErrorPage` CSS custom properties renamed: `--route-icon-color` → `--error-icon-color`, `--route-code-color` → `--error-code-color`, `--route-message-color` → `--error-message-color`
- `ULink` click event handling moved to host element level for better encapsulation
- `@lit/react` moved from devDependency to dependency for proper React integration
- `UErrorPage` replaced inline SVG icon imports with emoji icons; SVG asset files removed

## [0.6.1] - 2026-01-20

### Added
- Added `global.d.ts` import to main entry point

### Changed
- Moved click event listener from root element to document level for more reliable event handling

### Fixed
- Fixed `UErrorPage` CSS syntax error (trailing semicolon in CSS block)
- Fixed initial route loading to wait for outlet element readiness before navigation

### Removed
- Removed unused `computedHref` reactive state from `ULink`

## [0.6.0] - 2026-01-15

### Added
- Dedicated `react.ts` export entry providing React-compatible `UOutlet` and `ULink` wrappers
- `ULink` now supports the `target` attribute, enabling standard browser behavior (e.g. `_blank`)

### Changed
- Renamed custom elements: `ErrorPage` → `UErrorPage`, `Link` → `ULink`, `Outlet` → `UOutlet`
- Refactored internal module organization

## [0.5.3] - 2025-12-04

### Changed
- Removed restriction that prevented re-navigation to the current URL

## [0.5.2] - 2025-11-17

### Added
- `initialLoad` option on `RouterConfig` — controls whether the router navigates to the current URL on initialization
- `fallback` option on `RouterConfig` — defines a custom render function for error and not-found states
- `RouteProgressEvent` dispatched during async route loading

### Changed
- Renamed `RouteInfo` to `RouteContext`; added `progress` callback to context for reporting load progress
- Improved error handling and display in the built-in error page component

### Removed
- Removed `children` from `NonIndexRouteConfig` to simplify the type interface
- Removed `route` property from the `window` object to reduce global namespace pollution

## [0.5.1] - 2025-11-13

### Fixed
- Improved route error handling

## [0.5.0] - 2025-11-12

### Changed
- Route `render` functions now support both synchronous and asynchronous return values

### Removed
- Dropped CommonJS build output — ESM only

## [0.4.0] - 2025-11-11

### Added
- `destroy()` method on `Router` to cleanly remove event listeners
- `useIntercept` option on `RouterConfig` — controls whether anchor tag clicks are intercepted for client-side routing
- Global type declarations (`global.d.ts`)

## [0.3.0] - 2025-10-28

### Changed
- **Breaking:** Complete router architecture overhaul
- **Breaking:** `RouteConfig` split into `IndexRouteConfig` and `PathRouteConfig`
- **Breaking:** `RouteError` converted to a class; added `NotFoundRouteError`
- **Breaking:** Route event names changed: `route-start` → `route-begin`, `route-end` → `route-done`
- Simplified `Outlet` rendering via a unified `renderContent` method

### Added
- Improved `ErrorPage` component styling and usability
- Unified route rendering using render functions

## [0.2.1] - 2025-10-27

### Changed
- Refactored routing mechanism for improved performance
- Enhanced error handling with custom error pages

### Removed
- Removed `connect()` and `disconnect()` methods from `Router`
- Removed `notfound` configuration option
- Removed legacy route progress events
- added ErrorPage component for error handling
- added Route events: `route-start`, `route-end`, `route-error`
- improved TypeScript types and interfaces

## [0.1.0] - 2025-04-25

### Added
- Initial release
