@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 +49 -0
- package/README.md +30 -0
- package/dist/components/ULink.d.ts +1 -1
- package/dist/components/UOutlet.d.ts +1 -0
- package/dist/index.js +9 -10
- package/dist/react.js +1 -1
- package/dist/{share-ByPkeTq1.js → share-tRZgJ1SR.js} +56 -1
- package/dist/types/RouteConfig.d.ts +0 -7
- package/package.json +5 -2
- package/skills/iyulab-router/SKILL.md +1 -2
- package/skills/iyulab-router/references/components.md +30 -0
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
|
-
* 넘지 않으므로 접근성 트리에 자동 반영되지 않는다(
|
|
67
|
+
* 넘지 않으므로 접근성 트리에 자동 반영되지 않는다(실측 — 속성은
|
|
68
68
|
* 붙어 있는데 접근성 트리의 `aria-current`는 계속 비어 있음). `render()`가 이
|
|
69
69
|
* 값을 읽어 내부 `<a>`에 직접 옮긴다.
|
|
70
70
|
*
|
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-
|
|
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
|
-
* (레이아웃)는 상수 키로
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
* 존중한다: `false` 는 «유지», `true` 는 «새로».
|
|
305
|
+
* (레이아웃)는 상수 키로 유지된다. 그 밖의 규칙은 소비자가 `key` 로 직접 준다 — 예전의
|
|
306
|
+
* `force` 불리언은 이 두 값(«유지»·«새로»)밖에 표현하지 못해 `key` 로 대체됐고 0.13.0 에서
|
|
307
|
+
* 제거됐다(`force: false` ≡ `key: () => ''` · `force: true` ≡ 기본값).
|
|
309
308
|
*/
|
|
310
|
-
function defaultKey(
|
|
311
|
-
return
|
|
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(
|
|
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(
|
|
337
|
-
} else route.key ??= defaultKey(
|
|
335
|
+
route.key ??= defaultKey(true);
|
|
336
|
+
} else route.key ??= defaultKey(false);
|
|
338
337
|
}
|
|
339
338
|
}
|
|
340
339
|
return routes;
|
package/dist/react.js
CHANGED
|
@@ -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
|
-
* 넘지 않으므로 접근성 트리에 자동 반영되지 않는다(
|
|
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.
|
|
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": "^
|
|
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.
|