@iyulab/router 0.13.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,43 @@
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
+
3
41
  ## [0.13.0] - 2026-09-13
4
42
 
5
43
  ### Removed
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
@@ -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-A3Fla8oW.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
package/dist/react.js CHANGED
@@ -1,4 +1,4 @@
1
- import { s as UOutlet$1, t as ULink$1 } from "./share-A3Fla8oW.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
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iyulab/router",
3
- "version": "0.13.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,6 +71,7 @@
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",
@@ -80,6 +81,6 @@
80
81
  "typescript": "^6.0.2",
81
82
  "vite": "^8.1.4",
82
83
  "vite-plugin-dts": "^5.0.3",
83
- "vitest": "^4.1.10"
84
+ "vitest": "^5.0.0"
84
85
  }
85
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
 
@@ -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.