@iyulab/router 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,54 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.14.0] - 2026-09-16
4
+
5
+ ### Changed
6
+
7
+ - **`<u-outlet>` now declares its own `display: block`.** A custom element's UA default is
8
+ `inline`, and the outlet had no styles of its own — so a container meant to hold a route's
9
+ block-level screen generated an inline box. Consumers had to restate `u-outlet { display: block }`
10
+ in every application that cared, which is a rule only the package that defines the element can
11
+ reasonably own.
12
+
13
+ Two independent applications reported this from opposite directions on the same day — one
14
+ through printing (a short document gaining a blank trailing page), one through screen layout
15
+ (a table not reaching the bottom of the viewport). Both traced it to the same missing
16
+ declaration.
17
+
18
+ The declaration is `display: block; height: 100%`. The height half is not cosmetic: changing
19
+ `display` alone also changes which box a percentage height resolves against. While the outlet
20
+ was inline it was not a block container, so a screen's `height: 100%` resolved against the
21
+ block above it; making the outlet a block moves that reference onto the outlet itself, whose
22
+ height is `auto`, which silently voids the percentage and collapses full-height layouts to
23
+ their content height (measured: 747px → 60px for a screen built on
24
+ `@iyulab/modern-app`'s master-detail layout). `height: 100%` restores the chain, and resolves
25
+ to `auto` whenever the parent's height is `auto` — so ordinary document flow and printing,
26
+ where the shell releases its height, are unaffected.
27
+
28
+ The rule is adopted as a constructable stylesheet on whichever tree the outlet is connected to
29
+ (the document, or the shadow root when the outlet lives in one), written as
30
+ `:where(u-outlet)` so its specificity is zero — any `u-outlet { … }` rule an application writes
31
+ still wins without `!important`, regardless of sheet order. Where constructable sheets are not
32
+ available the rule is added as a `<style>` element instead.
33
+
34
+ **This changes layout in normal flow**, not only when printing: an inline box and a block box
35
+ differ in margin collapsing, and a block outlet can be given a height or a percentage size,
36
+ which an inline one silently ignored. Applications that place the outlet inside a flex or grid
37
+ container see no change — flex and grid items were already blockified. To keep the previous
38
+ behavior, set `u-outlet { display: inline }`; to keep the box but not the height, set
39
+ `u-outlet { height: auto }`. Neither needs `!important`.
40
+
41
+ ## [0.13.0] - 2026-09-13
42
+
43
+ ### Removed
44
+
45
+ - **`RouteConfig.force`** — deprecated in 0.12.0, removed as announced. It only ever expressed
46
+ the two values `key` already covers: `force: false` is `key: () => ''` (keep the mounted
47
+ content across every navigation that matches the route), `force: true` is the default
48
+ (`ctx => ctx.href` — remount when the URL changes at all). Replace one with the other; a
49
+ route that never set `force` is unaffected. The property is gone from the type, so a stale
50
+ usage fails at compile time rather than silently doing nothing.
51
+
3
52
  ## [0.12.0] - 2026-09-13
4
53
 
5
54
  ### Added
package/README.md CHANGED
@@ -141,6 +141,36 @@ const routes = [
141
141
  - `<u-link>`: SPA-aware anchor element
142
142
  - `<u-outlet>`: render target for matched route output
143
143
 
144
+ ## Outlet Layout
145
+
146
+ `<u-outlet>` declares its own `display: block`. A custom element's UA default is
147
+ `inline`, which would put a route's block-level screen inside an inline box — so the
148
+ outlet adopts a single rule on whichever tree it is connected to (the document, or the
149
+ shadow root if it lives in one):
150
+
151
+ ```css
152
+ :where(u-outlet) { display: block; height: 100%; }
153
+ ```
154
+
155
+ `height: 100%` is part of the same rule for a reason. A percentage height resolves against
156
+ the nearest block container, so making the outlet a block moves that reference from the box
157
+ *above* the outlet onto the outlet itself — and an `auto` height there voids the percentage
158
+ silently, collapsing a full-height screen to its content height. Declaring `height: 100%`
159
+ keeps the chain intact, and resolves to `auto` whenever the parent's height is `auto`, so
160
+ ordinary document flow and printing are unaffected.
161
+
162
+ The `:where()` wrapper makes the rule's specificity zero, so **any** `u-outlet { … }`
163
+ rule your application writes wins, regardless of sheet order and without `!important`:
164
+
165
+ ```css
166
+ u-outlet { display: flex; } /* wins */
167
+ u-outlet { height: auto; } /* keep the box, drop the fill */
168
+ u-outlet { display: inline; } /* restores the pre-0.14.0 behavior */
169
+ @media print { u-outlet { … } } /* wins */
170
+ ```
171
+
172
+ The rule is not media-scoped: the outlet is a block box on screen and in print alike.
173
+
144
174
  `<u-link>` supports `href`, `target`, `rel`, and `navigate`.
145
175
 
146
176
  ```html
@@ -64,7 +64,7 @@ export declare class ULink extends LitElement {
64
64
  /**
65
65
  * 호스트에 세팅된 `aria-current`/`aria-label`은 실제 접근 가능한(포커스 대상)
66
66
  * 엘리먼트가 아니라 — 그 안쪽 shadow DOM 의 네이티브 `<a>`다. 섀도우 경계를
67
- * 넘지 않으므로 접근성 트리에 자동 반영되지 않는다(docket #45 실측 — 속성은
67
+ * 넘지 않으므로 접근성 트리에 자동 반영되지 않는다(실측 — 속성은
68
68
  * 붙어 있는데 접근성 트리의 `aria-current`는 계속 비어 있음). `render()`가 이
69
69
  * 값을 읽어 내부 `<a>`에 직접 옮긴다.
70
70
  *
@@ -22,6 +22,7 @@ declare class UOutlet extends HTMLElement {
22
22
  private root?;
23
23
  /** 진행 중인 render — 다음 render 는 이것이 끝난 뒤 판정한다 */
24
24
  private pending?;
25
+ connectedCallback(): void;
25
26
  /**
26
27
  * 주어진 렌더링 옵션에 따라 컨텐츠를 렌더링합니다.
27
28
  *
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as isExternalUrl, i as absolutePath, n as __decorate, o as parseUrl, r as __decorateMetadata, s as UOutlet, t as ULink } from "./share-ByPkeTq1.js";
1
+ import { a as isExternalUrl, i as absolutePath, n as __decorate, o as parseUrl, r as __decorateMetadata, s as UOutlet, t as ULink } from "./share-tRZgJ1SR.js";
2
2
  import { LitElement, css, html } from "lit";
3
3
  import { customElement, property } from "lit/decorators.js";
4
4
  //#region src/types/RouteError.ts
@@ -302,13 +302,12 @@ var createURLPattern = URLPattern;
302
302
  * `key` 가 없을 때의 기본 식별 키 — «언제 새로 만드는가» 의 기본 규칙.
303
303
  *
304
304
  * 자식이 없는 라우트는 URL(`href`) 이 조금이라도 바뀌면 새로 마운트하고, 자식을 가진 라우트
305
- * (레이아웃)는 상수 키로 유지된다 종전의 `force` 기본값(leaf true · parent false)과 같은
306
- * 결과다. 다만 종전 코드는 `route.force ||= true` 라서 소비자가 leaf `force: false`
307
- * 명시해도 `true` 덮였다(`false || true`). `force` deprecate 됐지만 이 판에서는
308
- * 존중한다: `false` 는 «유지», `true` 는 «새로».
305
+ * (레이아웃)는 상수 키로 유지된다. 밖의 규칙은 소비자가 `key` 직접 준다 예전의
306
+ * `force` 불리언은 값(«유지»·«새로»)밖에 표현하지 못해 `key` 로 대체됐고 0.13.0 에서
307
+ * 제거됐다(`force: false` `key: () => ''` · `force: true` 기본값).
309
308
  */
310
- function defaultKey(route, hasChildren) {
311
- return route.force === false || hasChildren && route.force !== true ? () => "" : (ctx) => ctx.href;
309
+ function defaultKey(hasChildren) {
310
+ return hasChildren ? () => "" : (ctx) => ctx.href;
312
311
  }
313
312
  /**
314
313
  * 라우트들을 다음 사항에 따라 재귀적으로 재설정합니다.
@@ -326,15 +325,15 @@ function setRoutes(routes, basepath) {
326
325
  route.ignoreCase ||= false;
327
326
  if (route.index === true) {
328
327
  route.path = new createURLPattern({ pathname: `${basepath}{/}?` }, { ignoreCase: route.ignoreCase });
329
- route.key ??= defaultKey(route, false);
328
+ route.key ??= defaultKey(false);
330
329
  } else {
331
330
  if (typeof route.path === "string") route.path = new createURLPattern({ pathname: `${absolutePath(basepath, route.path)}{/}?` }, { ignoreCase: route.ignoreCase });
332
331
  else if (route.path instanceof URLPattern) {} else route.path = new createURLPattern({ pathname: `${basepath}{/}?` }, { ignoreCase: route.ignoreCase });
333
332
  if (route.children && route.children.length > 0) {
334
333
  const childBasepath = route.path.pathname.replace("{/}?", "");
335
334
  route.children = setRoutes(route.children, childBasepath);
336
- route.key ??= defaultKey(route, true);
337
- } else route.key ??= defaultKey(route, false);
335
+ route.key ??= defaultKey(true);
336
+ } else route.key ??= defaultKey(false);
338
337
  }
339
338
  }
340
339
  return routes;
package/dist/react.js CHANGED
@@ -1,4 +1,4 @@
1
- import { s as UOutlet$1, t as ULink$1 } from "./share-ByPkeTq1.js";
1
+ import { s as UOutlet$1, t as ULink$1 } from "./share-tRZgJ1SR.js";
2
2
  import React from "react";
3
3
  import { createComponent } from "@lit/react";
4
4
  //#region src/react.ts
@@ -3,9 +3,64 @@ import { customElement, property } from "lit/decorators.js";
3
3
  import { ifDefined } from "lit/directives/if-defined.js";
4
4
  //#region src/components/UOutlet.ts
5
5
  /**
6
+ * `<u-outlet>` 의 기본 표시 방식.
7
+ *
8
+ * ★커스텀 엘리먼트의 UA 기본값은 `inline` 이다 — 라우트 화면(블록 요소들)을 인라인 상자에
9
+ * 담으면 block-in-inline 분할이 생겨, 인쇄에서 짧은 문서에 빈 꼬리 쪽이 붙는다. 컨테이너
10
+ * 요소의 표시 방식은 그것을 정의한 쪽이 선언해야 하고, 그 쪽은 이 패키지다.
11
+ * ★`:where(u-outlet)` 로 특이도를 0 으로 둔다 — 소비자의 `u-outlet { … }` 규칙이 시트 순서와
12
+ * 무관하게 `!important` 없이 이긴다.
13
+ * ⚠constructable 시트(`adoptedStyleSheets`)를 쓴다 — `<style>` 요소와 달리 CSP 의 인라인
14
+ * 스타일 제한에 걸리지 않는다. 지원하지 않는 환경에서는 `<style>` 로 대신한다.
15
+ * ⚠매체를 가르지 않는다 — 인라인 컨테이너는 화면에서도 의도된 적이 없다(라인 박스 때문에
16
+ * 높이를 줄 수도, 백분율로 채울 수도 없었다). 인쇄에만 한정하면 화면·인쇄가 서로 다른
17
+ * 상자 모델을 갖게 되어 같은 부류의 차이가 다음에 또 난다.
18
+ * 🔴`height: 100%` 가 함께 있어야 한다 — `display` 만 바꾸면 **백분율 높이의 기준 상자가
19
+ * 바뀐다.** 종전(inline)에는 아웃렛이 블록 컨테이너가 아니라, 라우트 화면의 `height: 100%`
20
+ * 가 그 «위» 의 블록(셸의 본문 영역)에 대해 풀렸다. 아웃렛을 block 으로 만들면 기준이
21
+ * 아웃렛 자신이 되는데 그 높이가 `auto` 라 백분율이 무효가 되고, 화면을 채우도록 만들어진
22
+ * 레이아웃(`u-master-detail-layout` 의 `:host{height:100%}` 등)이 내용 높이로 무너진다.
23
+ * 실측(chromium): 같은 화면이 inline 747px → block 60px → block+height:100% 747px.
24
+ * ⚠부모 높이가 `auto` 면 `100%` 는 `auto` 로 풀리므로 인쇄(셸이 높이를 놓는다)와 일반
25
+ * 문서 흐름에는 영향이 없다 — 이 선언이 «채우기» 를 강요하는 것은 부모가 높이를 가진
26
+ * 경우뿐이고, 그것이 종전 동작이다.
27
+ */
28
+ var OUTLET_DISPLAY_CSS = ":where(u-outlet) { display: block; height: 100%; }";
29
+ /** 시트를 이미 채택한 트리 — 같은 트리에 두 번 넣지 않는다. */
30
+ var styledRoots = /* @__PURE__ */ new WeakSet();
31
+ /**
32
+ * 아웃렛이 실제로 속한 트리에 표시 규칙을 채택한다.
33
+ *
34
+ * ⚠`document` 로 못박지 않는다 — 섀도 루트 안의 `<u-outlet>` 은 문서 시트가 닿지 않아
35
+ * 기본 `inline` 그대로 남는다. 아웃렛을 라이트 DOM 에 두는 것이 이 생태계의 관례이지만,
36
+ * 그 관례를 어긴 배치에서 조용히 규칙이 사라지는 쪽이 더 나쁘다.
37
+ */
38
+ function adoptOutletDisplay(node) {
39
+ const isDocument = node.nodeType === 9;
40
+ const isShadowRoot = node.nodeType === 11 && "host" in node;
41
+ if (!isDocument && !isShadowRoot) return;
42
+ const root = node;
43
+ if (styledRoots.has(root)) return;
44
+ styledRoots.add(root);
45
+ if ("adoptedStyleSheets" in root && typeof CSSStyleSheet !== "undefined" && typeof CSSStyleSheet.prototype.replaceSync === "function") {
46
+ const sheet = new CSSStyleSheet();
47
+ sheet.replaceSync(OUTLET_DISPLAY_CSS);
48
+ root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
49
+ return;
50
+ }
51
+ const ownerDocument = isDocument ? root : node.ownerDocument;
52
+ if (!ownerDocument) return;
53
+ const style = ownerDocument.createElement("style");
54
+ style.textContent = OUTLET_DISPLAY_CSS;
55
+ (isDocument ? root.head : root)?.prepend(style);
56
+ }
57
+ /**
6
58
  * LitElement 또는 React 컴포넌트를 렌더링해주는 웹컴포넌트 입니다.
7
59
  */
8
60
  var UOutlet = class extends HTMLElement {
61
+ connectedCallback() {
62
+ adoptOutletDisplay(this.getRootNode());
63
+ }
9
64
  /**
10
65
  * 주어진 렌더링 옵션에 따라 컨텐츠를 렌더링합니다.
11
66
  *
@@ -1120,7 +1175,7 @@ var ULink = class ULink extends LitElement {
1120
1175
  /**
1121
1176
  * 호스트에 세팅된 `aria-current`/`aria-label`은 실제 접근 가능한(포커스 대상)
1122
1177
  * 엘리먼트가 아니라 — 그 안쪽 shadow DOM 의 네이티브 `<a>`다. 섀도우 경계를
1123
- * 넘지 않으므로 접근성 트리에 자동 반영되지 않는다(docket #45 실측 — 속성은
1178
+ * 넘지 않으므로 접근성 트리에 자동 반영되지 않는다(실측 — 속성은
1124
1179
  * 붙어 있는데 접근성 트리의 `aria-current`는 계속 비어 있음). `render()`가 이
1125
1180
  * 값을 읽어 내부 `<a>`에 직접 옮긴다.
1126
1181
  *
@@ -90,13 +90,6 @@ interface BaseRouteConfig {
90
90
  * ```
91
91
  */
92
92
  key?: (ctx: RouteContext) => string;
93
- /**
94
- * @deprecated `key`로 표현하세요 — `force: false`는 `key: () => ''`(상수 키)와 같고 `force: true`는
95
- * 기본값과 같습니다. 이 판에서는 동작을 유지하며, 다음 minor에서 제거됩니다.
96
- * ⚠이전 판에서는 자식이 없는 라우트에 `force: false`를 줘도 무시됐습니다(기본값 적용 순서의 결함).
97
- * 이제 `force: false`는 모든 라우트에서 «유지»를 뜻합니다.
98
- */
99
- force?: boolean;
100
93
  /**
101
94
  * 경로 매칭시 대소문자 구분 여부
102
95
  * @default false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iyulab/router",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "A modern client-side router for web applications with support for Lit and React components",
5
5
  "keywords": [
6
6
  "lit",
@@ -71,13 +71,16 @@
71
71
  }
72
72
  },
73
73
  "devDependencies": {
74
+ "@vitest/browser-playwright": "^5.0.0",
74
75
  "@types/node": "^26.1.1",
75
76
  "@types/react": "^19.2.17",
76
77
  "@types/react-dom": "^19.2.3",
77
78
  "happy-dom": "^20.10.6",
79
+ "react": "^19.2.8",
80
+ "react-dom": "^19.2.8",
78
81
  "typescript": "^6.0.2",
79
82
  "vite": "^8.1.4",
80
83
  "vite-plugin-dts": "^5.0.3",
81
- "vitest": "^4.1.10"
84
+ "vitest": "^5.0.0"
82
85
  }
83
86
  }
@@ -27,7 +27,7 @@ npm install @iyulab/router
27
27
  | `RouterConfig` | Constructor config type |
28
28
  | `RouteContext` | Passed to every `render()` call |
29
29
  | `FallbackRouteConfig` | Error/404 fallback definition |
30
- | `<u-outlet>` | Renders the matched route output |
30
+ | `<u-outlet>` | Renders the matched route output (a block box — see `references/components.md`) |
31
31
  | `<u-link>` | Client-side navigation anchor |
32
32
  | `UOutlet`, `ULink` | React wrappers (from `@iyulab/router/react`) |
33
33
 
@@ -72,7 +72,6 @@ const router = new Router({
72
72
  | `title` | `string` | Sets `document.title` on match |
73
73
  | `metadata` | `Record<string, unknown>` | Arbitrary metadata (auth, layout, analytics) |
74
74
  | `key` | `(ctx) => string` | When to remount: content is kept and re-rendered in place while the key is unchanged (Lit: same part, React: same root, `HTMLElement`: instance kept). Default `ctx => ctx.href` for leaf routes, a constant for routes with `children`. `key: ctx => ctx.pathname` keeps a page across query-string changes |
75
- | `force` | `boolean` | **Deprecated** — use `key`. `false` ≡ constant key (keep), `true` ≡ default |
76
75
 
77
76
  ## RouteContext Fields
78
77
 
@@ -38,6 +38,36 @@ export function AppRoot() {
38
38
  }
39
39
  ```
40
40
 
41
+ ## Outlet Layout
42
+
43
+ `<u-outlet>` declares its own `display: block`. A custom element's UA default is
44
+ `inline`, which would put a route's block-level screen inside an inline box — so the
45
+ outlet adopts a single rule on whichever tree it is connected to (the document, or the
46
+ shadow root if it lives in one):
47
+
48
+ ```css
49
+ :where(u-outlet) { display: block; height: 100%; }
50
+ ```
51
+
52
+ `height: 100%` is part of the same rule for a reason. A percentage height resolves against
53
+ the nearest block container, so making the outlet a block moves that reference from the box
54
+ *above* the outlet onto the outlet itself — and an `auto` height there voids the percentage
55
+ silently, collapsing a full-height screen to its content height. Declaring `height: 100%`
56
+ keeps the chain intact, and resolves to `auto` whenever the parent's height is `auto`, so
57
+ ordinary document flow and printing are unaffected.
58
+
59
+ The `:where()` wrapper makes the rule's specificity zero, so **any** `u-outlet { … }`
60
+ rule your application writes wins, regardless of sheet order and without `!important`:
61
+
62
+ ```css
63
+ u-outlet { display: flex; } /* wins */
64
+ u-outlet { height: auto; } /* keep the box, drop the fill */
65
+ u-outlet { display: inline; } /* restores the pre-0.14.0 behavior */
66
+ @media print { u-outlet { … } } /* wins */
67
+ ```
68
+
69
+ The rule is not media-scoped: the outlet is a block box on screen and in print alike.
70
+
41
71
  ## Nested Outlet Rule
42
72
 
43
73
  A parent route must render `<u-outlet>` to host child route content.