@iyulab/router 0.15.1 → 0.15.2

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,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.15.2] - 2026-09-23
4
+
5
+ ### Fixed
6
+
7
+ - **A wide descendant no longer widens the route screen past the outlet.** 0.15.0 made
8
+ `<u-outlet>` a grid container without declaring a column track. The implicit column is `auto`,
9
+ and a grid item's default `min-width: auto` passes its content's minimum width up to the track,
10
+ so a wide table widened the whole screen — even inside an `overflow-x: auto` box — and anything
11
+ aligned to the screen's right edge, such as a toolbar's last button, was pushed off screen. The
12
+ track is now `minmax(0, 1fr)`: the screen is exactly as wide as the outlet, a narrow screen still
13
+ fills it, and wide content scrolls inside its own box. Print is unaffected (the outlet is a block
14
+ box there). The rule keeps zero specificity, so `u-outlet { grid-template-columns: auto; }`
15
+ restores the previous track.
16
+
3
17
  ## [0.15.1] - 2026-09-17
4
18
 
5
19
  ### Fixed
package/README.md CHANGED
@@ -149,7 +149,7 @@ rules on whichever tree it is connected to (the document, or the shadow root if
149
149
  one):
150
150
 
151
151
  ```css
152
- :where(u-outlet) { display: grid; min-height: 100%; }
152
+ :where(u-outlet) { display: grid; grid-template-columns: minmax(0, 1fr); min-height: 100%; }
153
153
  @media print { :where(u-outlet) { display: block; } }
154
154
  ```
155
155
 
@@ -181,6 +181,13 @@ The rule deliberately omits `align-content`; it relies on the initial `normal`.
181
181
  When the parent's own height is `auto`, `min-height: 100%` resolves to `auto` too, so ordinary
182
182
  document flow is unaffected.
183
183
 
184
+ The column track is bound to the outlet's width with `minmax(0, 1fr)`. Without it the implicit
185
+ column is `auto`, and a grid item's default `min-width: auto` passes its content's minimum width up
186
+ to the track — so a wide table inside a screen widened the whole screen past the outlet, even when
187
+ the table sat in an `overflow-x: auto` box, and anything aligned to the screen's right edge (a
188
+ toolbar's last button) ended up off screen. With the track bound, the screen is exactly as wide as
189
+ the outlet, a narrow screen still fills it, and wide content scrolls inside its own box.
190
+
184
191
  ### Printing
185
192
 
186
193
  In print media the outlet is a plain block box instead of a grid container. A grid container is
@@ -202,6 +209,7 @@ application writes wins, regardless of sheet order and without `!important`:
202
209
 
203
210
  ```css
204
211
  u-outlet { display: flex; } /* wins */
212
+ u-outlet { grid-template-columns: auto; } /* restores the 0.15.1 column track */
205
213
  u-outlet { display: contents; } /* remove the box entirely */
206
214
  u-outlet { display: block; height: 100%; } /* restores the 0.14.0 behavior */
207
215
  u-outlet { display: inline; } /* restores the pre-0.14.0 behavior */
package/dist/index.d.ts CHANGED
@@ -1,9 +1,619 @@
1
- export * from './types/NavigateOptions';
2
- export * from './types/RouteConfig';
3
- export * from './types/RouteContext';
4
- export * from './types/RouteError';
5
- export * from './types/RouteEvent';
6
- export * from './types/RouterConfig';
7
- export * from './components/UOutlet';
8
- export * from './components/ULink';
9
- export { Router } from './Router';
1
+ import { CSSResult } from 'lit';
2
+ import { LitElement } from 'lit';
3
+ import { PropertyValues } from 'lit';
4
+ import { TemplateResult } from 'lit-html';
5
+
6
+ /**
7
+ * enter 가드가 false를 반환하여 접근이 거부되었을 때 발생하는 에러
8
+ */
9
+ export declare class AccessDeniedError extends RouteError {
10
+ constructor(path: string);
11
+ }
12
+
13
+ /**
14
+ * 공통 라우트 속성
15
+ */
16
+ declare interface BaseRouteConfig {
17
+ /**
18
+ * 라우터에서 사용하는 식별자
19
+ */
20
+ id?: string;
21
+ /**
22
+ * 브라우저의 타이틀이 설정에 따라 변경됩니다.
23
+ */
24
+ title?: string;
25
+ /**
26
+ * 라우터 경로는 string 또는 URLPattern을 사용할 수 있습니다.
27
+ * string일 경우 자동으로 URLPattern으로 변환됩니다.
28
+ * @default '/'
29
+ * @example
30
+ * - "/user/:id/:name"
31
+ * - "/user/:id/:name?"
32
+ * - "/user/:id/:name*"
33
+ * - "/user/:id/:name+"
34
+ * @link
35
+ * https://developer.mozilla.org/en-US/docs/Web/API/URLPattern
36
+ */
37
+ path?: string | URLPattern;
38
+ /**
39
+ * 라우트 정보를 받아 렌더링 결과를 반환합니다.
40
+ * @param ctx 현재 라우팅 정보 및, 진행 상태 콜백을 포함하는 Context 객체가 인자로 전달됩니다.
41
+ * @example
42
+ * ```typescript
43
+ * const route = {
44
+ * path: '/user:id',
45
+ * render: async (ctx) => {
46
+ * // 사용자 정보를 비동기로 가져오는 예시
47
+ * const userId = ctx.params.id;
48
+ * ctx.progress(30);
49
+ * const userData = await fetchUserData(userId);
50
+ * ctx.progress(70);
51
+ * return html`<user-profile .data=${userData}></user-profile>`;
52
+ * }
53
+ * }
54
+ * ```
55
+ * @remarks
56
+ * `render`는 Lit `TemplateResult`, `HTMLElement`, **또는 React 엘리먼트**를 반환할 수 있습니다.
57
+ * React 엘리먼트를 직접 반환하면 내부 `<u-outlet>`이 `createRoot`/`root.unmount()`를
58
+ * 자동으로 관리합니다 — React 루트를 직접 만들어 컨테이너 `HTMLElement`로 감싸 반환하면
59
+ * 이 자동 해제 지점을 우회하게 되어 라우트 전환마다 unmount가 호출되지 않는 누수가 생깁니다.
60
+ * @example
61
+ * ```tsx
62
+ * const route = {
63
+ * path: '/users/:id',
64
+ * render: (ctx) => <UserProfilePage userId={ctx.params.id} />, // React 엘리먼트를 직접 반환
65
+ * };
66
+ * ```
67
+ */
68
+ render?: (ctx: RouteContext) => Promise<unknown> | unknown;
69
+ /**
70
+ * 이 라우트 진입 전에 호출되는 enter 함수입니다.
71
+ * - `string` 반환: 해당 경로로 redirect
72
+ * - `false` 반환: 네비게이션 취소
73
+ * - `true` 반환: 통과
74
+ * @example
75
+ * ```typescript
76
+ * { path: '/admin', enter: (ctx) => ctx.metadata.role === 'admin' || '/forbidden' }
77
+ * ```
78
+ */
79
+ enter?: (ctx: RouteContext) => Promise<string | boolean> | string | boolean;
80
+ /**
81
+ * 이 라우트의 콘텐츠를 **언제 새로 만들 것인가**를 정하는 식별 키입니다.
82
+ *
83
+ * 네비게이션마다 `key(ctx)`를 계산해 직전 값과 비교합니다.
84
+ * - 키가 **바뀌면** 기존 콘텐츠를 내리고 새로 마운트합니다.
85
+ * - 키가 **같으면** 콘텐츠를 유지한 채 `render(ctx)`의 결과로 **제자리 갱신**합니다 —
86
+ * Lit 템플릿은 같은 파트에 다시 렌더(DOM·요소 상태 유지, 바인딩만 갱신), React 엘리먼트는
87
+ * 같은 root에 다시 렌더(컴포넌트 상태 유지, props만 갱신), `HTMLElement`는 기존 인스턴스를
88
+ * 그대로 둡니다(조정할 수단이 없습니다). 새 `ctx`는 따로 전달되지 않고 `render(ctx)`를
89
+ * 통해 도달합니다.
90
+ *
91
+ * 기본값: 자식 라우트가 없으면 `ctx => ctx.href`(URL이 조금이라도 바뀌면 새로), 자식 라우트가
92
+ * 있으면 상수(레이아웃은 유지하고 자식만 바뀝니다).
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * // 쿼리스트링만 바뀌면 페이지를 유지하고 prop만 갱신 — 목록 상태·스크롤이 살아남습니다
97
+ * { path: '/orders', key: ctx => ctx.pathname,
98
+ * render: ctx => html`<orders-page .selectedId=${ctx.query.get('id')}></orders-page>` }
99
+ * // params가 바뀌어도 유지
100
+ * { path: '/orders/:id', key: () => 'orders', render: ctx => html`...` }
101
+ * ```
102
+ */
103
+ key?: (ctx: RouteContext) => string;
104
+ /**
105
+ * 경로 매칭시 대소문자 구분 여부
106
+ * @default false
107
+ */
108
+ ignoreCase?: boolean;
109
+ /**
110
+ * 라우트에 연결할 메타데이터
111
+ * - 인증, SEO, 분석 등의 용도로 사용할 수 있습니다.
112
+ * @example
113
+ * ```typescript
114
+ * { path: '/admin', metadata: { requiresAuth: true, role: 'admin' } }
115
+ * ```
116
+ */
117
+ metadata?: Record<string, unknown>;
118
+ }
119
+
120
+ /**
121
+ * 컨텐츠 로드시 나타나는 에러
122
+ */
123
+ export declare class ContentLoadError extends RouteError {
124
+ constructor(original?: Error | any);
125
+ }
126
+
127
+ /**
128
+ * 컨텐츠 렌더링시 발생하는 에러
129
+ */
130
+ export declare class ContentRenderError extends RouteError {
131
+ constructor(original?: Error | any);
132
+ }
133
+
134
+ export declare interface FallbackRouteConfig {
135
+ /**
136
+ * 브라우저의 타이틀이 설정에 따라 변경됩니다.
137
+ */
138
+ title?: string;
139
+ /**
140
+ * 라우팅 실패 시 표시할 렌더링 결과를 반환합니다.
141
+ * - 오류가 발생할 경우 또는 렌더링 결과가 false일 경우 호출됩니다.
142
+ * @param ctx 현재 라우팅 정보 및 오류 정보를 포함하는 Context 객체가 인자로 전달됩니다.
143
+ * @example
144
+ * ```typescript
145
+ * const fallbackRoute = {
146
+ * title: 'Not Found',
147
+ * render: (ctx) => {
148
+ * if (ctx.error) {
149
+ * return html`<error-page .error=${ctx.error}></error-page>`;
150
+ * }
151
+ * return html`<not-found-page></not-found-page>`;
152
+ * }
153
+ * }
154
+ * ```
155
+ */
156
+ render?: (ctx: FallbackRouteContext) => Promise<unknown> | unknown;
157
+ }
158
+
159
+ export declare interface FallbackRouteContext extends RouteContext {
160
+ /**
161
+ * 라우팅 에러 정보
162
+ * - 라우팅 중 발생한 에러 정보를 포함합니다.
163
+ */
164
+ error: RouteError;
165
+ }
166
+
167
+ declare interface IndexRouteConfig extends BaseRouteConfig {
168
+ /**
169
+ * 현재 경로의 인덱스 라우트임을 나타냅니다.
170
+ * - 인덱스 라우트는 부모 경로와 동일한 경로를 가지며, path는 자동으로 설정됩니다.
171
+ */
172
+ index: true;
173
+ }
174
+
175
+ /**
176
+ * go() 메서드에 전달할 네비게이션 옵션
177
+ */
178
+ export declare interface NavigateOptions {
179
+ /**
180
+ * 리다이렉트로 인한 네비게이션 여부.
181
+ * - `true`이면 히스토리에 새 항목을 추가하지 않고 현재 항목을 교체합니다(replaceState).
182
+ * - 뒤로가기 버튼이 리다이렉트 경유지를 건너뛰게 됩니다.
183
+ * - 리다이렉트 사이클 감지에 사용됩니다.
184
+ * @default false
185
+ */
186
+ isRedirect?: boolean;
187
+ /**
188
+ * 히스토리에 새 항목을 추가하지 않고 현재 항목을 교체합니다(replaceState).
189
+ * - `isRedirect`와 달리 리다이렉트 체인 추적에는 영향을 주지 않습니다.
190
+ * @default false
191
+ */
192
+ replace?: boolean;
193
+ /**
194
+ * pushState / replaceState 호출 시 함께 저장할 커스텀 상태 객체.
195
+ * - `history.state`로 다시 읽을 수 있습니다.
196
+ * @example { from: '/login', referrer: 'email-link' }
197
+ */
198
+ state?: Record<string, unknown>;
199
+ }
200
+
201
+ declare interface NonIndexRouteConfig extends BaseRouteConfig {
202
+ /**
203
+ * 인덱스 라우트가 아님을 나타냅니다.
204
+ */
205
+ index?: false;
206
+ /**
207
+ * 하위 라우트 설정, 재귀적으로 RouteConfig 배열을 가질 수 있습니다.
208
+ * - 하위 라우트가 있는 경우, 부모 라우트의 경로를 기준으로 매칭됩니다.
209
+ */
210
+ children?: RouteConfig[];
211
+ }
212
+
213
+ /**
214
+ * 페이지를 찾을 수 없을 때 발생하는 에러
215
+ */
216
+ export declare class NotFoundError extends RouteError {
217
+ constructor(path: string, original?: Error | any);
218
+ }
219
+
220
+ /**
221
+ * u-outlet 요소를 찾을 수 없을 때 발생하는 에러
222
+ */
223
+ export declare class OutletMissingError extends RouteError {
224
+ constructor();
225
+ }
226
+
227
+ /** 렌더링 옵션 */
228
+ declare interface RenderOption {
229
+ /** 교차 렌더링 방지 ID — 어느 라우트의 콘텐츠인가 */
230
+ id?: string;
231
+ /**
232
+ * 라우트의 식별 키(`RouteConfig.key` 의 결과). 같은 라우트 + 같은 키면 기존 콘텐츠를
233
+ * 유지한 채 제자리 갱신하고, 바뀌면 내리고 새로 마운트한다.
234
+ */
235
+ key?: string;
236
+ }
237
+
238
+ /**
239
+ * 라우트 시작 이벤트
240
+ */
241
+ export declare class RouteBeginEvent extends RouteEvent {
242
+ constructor(context: RouteContext);
243
+ }
244
+
245
+ export declare type RouteConfig = IndexRouteConfig | NonIndexRouteConfig;
246
+
247
+ /**
248
+ * 라우터 정보
249
+ */
250
+ export declare interface RouteContext {
251
+ /**
252
+ * 전체 URL 정보
253
+ * - 도메인 이름을 포함한 URL의 전체 경로입니다.
254
+ * @example https://www.iyulab.com/home/user/1?name=iyu#profile
255
+ */
256
+ href: string;
257
+ /**
258
+ * URL 도메인 이름
259
+ * - URL의 도메인 이름입니다.
260
+ * @example https://www.iyulab.com
261
+ */
262
+ origin: string;
263
+ /**
264
+ * 라우터 URL 기본 경로
265
+ * - 라우터의 현재 basepath입니다.
266
+ * @example
267
+ * if basepath is /app/:id
268
+ * set basepath to /app/123
269
+ */
270
+ basepath: string;
271
+ /**
272
+ * 라우터 URL 전체 경로
273
+ * - 도메인 이름을 제외한 URL의 전체 경로입니다.
274
+ * @example /home/user/1?name=iyu#profile
275
+ */
276
+ path: string;
277
+ /**
278
+ * 라우터 URL 절대 경로
279
+ * - 쿼리스트링과 해시를 제외한 URL 경로입니다.
280
+ * @example /home/user/1
281
+ */
282
+ pathname: string;
283
+ /**
284
+ * 라우터 URL 파라미터
285
+ * - URLPattern을 사용하여 파싱된 파라미터입니다.
286
+ * @example 만약 URL이 /user/:id/:name 일경우
287
+ * ```typescript
288
+ * const id = params.id;
289
+ * const name = params.name;
290
+ * ```
291
+ */
292
+ params: {
293
+ [key: string]: string | undefined;
294
+ };
295
+ /**
296
+ * 라우터 URL 쿼리스트링
297
+ * - URLSearchParams를 사용하여 파싱된 쿼리스트링입니다.
298
+ * @example 만약 URL이 ?page=1&size=10 일경우
299
+ * ```typescript
300
+ * const page = query.get('page');
301
+ * const size = query.get('size');
302
+ * ```
303
+ */
304
+ query: URLSearchParams;
305
+ /**
306
+ * 라우터 URL 해시
307
+ * - URL의 해시값입니다. 해시값은 하나만 사용할 수 있습니다.
308
+ * - #profile#user 두개 이상의 해시값은 하나로 취급합니다.
309
+ * @example #profile
310
+ */
311
+ hash?: string;
312
+ /**
313
+ * 현재 라우팅의 진행 상태를 업데이트하여, window 객체의 'route-progress' 이벤트를 트리거합니다.
314
+ * 여러 라우팅이 호출되는 경우, 가장 최근의 라우팅의 진행 상태만 반영되며, 나머지는 무시됩니다.
315
+ * @param value 진행 상태 값 (0~100)
316
+ */
317
+ progress: (value: number) => void;
318
+ /**
319
+ * 매칭된 라우트 체인의 병합된 메타데이터
320
+ * - 부모 라우트에서 자식 라우트 순서로 병합됩니다.
321
+ */
322
+ metadata: Record<string, unknown>;
323
+ }
324
+
325
+ /**
326
+ * 라우트 완료 이벤트
327
+ */
328
+ export declare class RouteDoneEvent extends RouteEvent {
329
+ constructor(context: RouteContext);
330
+ }
331
+
332
+ /**
333
+ * 라우팅 에러 정보
334
+ */
335
+ export declare class RouteError extends Error {
336
+ /**
337
+ * 에러 코드
338
+ * - HTTP 상태 코드 또는 커스텀 에러 코드
339
+ * @example 404, 500, 'ROUTE_NOT_FOUND'
340
+ */
341
+ code: number | string;
342
+ /**
343
+ * 원본 에러 객체
344
+ * - 원본 Error 객체 또는 예외 정보
345
+ */
346
+ original?: Error | any;
347
+ /**
348
+ * 에러 발생 시간
349
+ * - 에러가 발생한 시간 (ISO 8601 형식)
350
+ */
351
+ timestamp: string;
352
+ constructor(code: number | string, message: string, original?: Error | any);
353
+ }
354
+
355
+ /**
356
+ * 라우트 에러 이벤트
357
+ */
358
+ export declare class RouteErrorEvent extends RouteEvent {
359
+ /** 에러 정보 */
360
+ readonly error: RouteError;
361
+ constructor(context: RouteContext, error: RouteError);
362
+ }
363
+
364
+ /** 라우터 이벤트 기본 클래스 */
365
+ declare abstract class RouteEvent extends Event {
366
+ /** 라우팅 정보 */
367
+ readonly context: RouteContext;
368
+ /** 이벤트 발생 시간 */
369
+ readonly timestamp: string;
370
+ constructor(type: string, context: RouteContext, cancelable?: boolean);
371
+ /** 이벤트가 취소되었는지 확인 */
372
+ get cancelled(): boolean;
373
+ /** 이벤트 취소 */
374
+ cancel(): void;
375
+ }
376
+
377
+ /**
378
+ * 라우트 진행 이벤트
379
+ */
380
+ export declare class RouteProgressEvent extends RouteEvent {
381
+ /** 진행 상태 값 (0~100) */
382
+ readonly progress: number;
383
+ constructor(context: RouteContext, progress: number);
384
+ }
385
+
386
+ /**
387
+ * `lit-element`, `react`를 지원하는 SPA 클라이언트 라우터 객체입니다.
388
+ */
389
+ export declare class Router {
390
+ private readonly _rootElement;
391
+ private readonly _basepath;
392
+ private readonly _routes;
393
+ private readonly _fallback?;
394
+ private readonly _enter?;
395
+ private readonly _tracker;
396
+ /** 현재 라우팅 요청 ID */
397
+ private _requestID?;
398
+ /** 현재 라우팅 정보 */
399
+ private _context?;
400
+ constructor(config: RouterConfig);
401
+ /** 객체를 정리하고 이벤트 리스너를 제거합니다. */
402
+ destroy(): void;
403
+ /** 라우터의 기본 경로 반환 */
404
+ get basepath(): string;
405
+ /** 등록된 라우트 정보 반환 */
406
+ get routes(): RouteConfig[];
407
+ /** 현재 라우팅 정보 반환 */
408
+ get context(): RouteContext | undefined;
409
+ /**
410
+ * 지정한 경로의 클라이언트 라우팅을 수행합니다. 상대경로일 경우 basepath와 조합되어 이동합니다.
411
+ * @param href 이동할 경로
412
+ * @param options 네비게이션 옵션
413
+ * @param routes (internal) 호출자가 이미 계산한 라우트 매칭 결과가 있으면 재사용합니다.
414
+ * `handleRootElementClick`이 가로채기 여부 판단을 위해 미리 계산한 결과를 전달해
415
+ * 동일 pathname에 대한 getRoutes 중복 호출을 피하는 용도입니다. 외부에서 사용하지 마세요.
416
+ */
417
+ go(href: string, options?: NavigateOptions, routes?: RouteConfig[]): Promise<undefined>;
418
+ /** 브라우저 히스토리 이벤트가 발생시 라우팅 처리 */
419
+ private handleWindowPopstate;
420
+ /** 클릭 이벤트에서 라우터로 처리할 앵커를 찾아 클라이언트 라우팅 수행 */
421
+ private handleRootElementClick;
422
+ }
423
+
424
+ /**
425
+ * 라우터 설정
426
+ */
427
+ export declare interface RouterConfig {
428
+ /**
429
+ * 라우터가 연결될 최상위 엘리먼트
430
+ */
431
+ root: HTMLElement;
432
+ /**
433
+ * 라우터의 기본 경로
434
+ * - 라우터의 기본 경로는 URL의 시작점입니다.
435
+ * - URLPattern을 사용하여 경로를 탐색합니다.
436
+ * @default '/'
437
+ */
438
+ basepath?: string;
439
+ /**
440
+ * 라우트 설정
441
+ * - 라우트는 URLPattern을 사용하여 경로를 탐색합니다.
442
+ * - 라우트는 렌더링할 엘리먼트 또는 컴포넌트를 지정합니다.
443
+ */
444
+ routes?: RouteConfig[];
445
+ /**
446
+ * 모든 라우트 전환 전에 호출되는 글로벌 enter 함수입니다.
447
+ * - `string` 반환: 해당 경로로 redirect
448
+ * - `false` 반환: 네비게이션 취소
449
+ * - `true` 반환: 통과
450
+ * @example
451
+ * ```typescript
452
+ * enter: async (ctx) => {
453
+ * if (!isAuthenticated() && ctx.pathname !== '/login') return '/login';
454
+ * }
455
+ * ```
456
+ */
457
+ enter?: (ctx: RouteContext) => Promise<string | boolean> | string | boolean;
458
+ /**
459
+ * 라우트 매칭 실패 또는 오류 발생 시 대체 라우트 설정
460
+ * - 지정된 설정이 없을 경우, 기본 오류 페이지가 렌더링됩니다.
461
+ */
462
+ fallback?: FallbackRouteConfig;
463
+ /**
464
+ * `a` 태그 클릭 시 클라이언트 라우팅을 수행할지 여부를 설정합니다.
465
+ * @default true
466
+ */
467
+ useIntercept?: boolean;
468
+ /**
469
+ * 초기 로드 시 현재 URL로 라우팅을 자동으로 수행할지 여부를 설정합니다.
470
+ * @default true
471
+ */
472
+ initialLoad?: boolean;
473
+ }
474
+
475
+ /**
476
+ * - 클라이언트 라우팅을 지원하는 링크 엘리먼트입니다.
477
+ * - 내부 링크는 클라이언트 라우팅을 수행하고, 외부 링크는 브라우저 기본 네비게이션을 사용합니다.
478
+ * - Ctrl/Meta/Shift/Alt, 중클릭/우클릭 등은 브라우저 기본 동작(새 탭, 컨텍스트 메뉴 등)을 그대로 유지합니다.
479
+ */
480
+ export declare class ULink extends LitElement {
481
+ /** 외부 링크 여부 */
482
+ private isExternal;
483
+ /**
484
+ * 링크 대상 target 속성
485
+ *
486
+ * - `_self`: 현재 창에서 링크 열기 (기본값)
487
+ * - `_blank`: 새 탭/창에서 링크 열기
488
+ * - `_parent`: 부모 프레임에서 링크 열기
489
+ * - `_top`: 최상위 프레임에서 링크 열기
490
+ */
491
+ target?: string;
492
+ /**
493
+ * 링크 관계 rel 속성
494
+ *
495
+ * - `noopener`: target이 _blank인 경우 보안 강화 (window.opener 차단)
496
+ * - `noreferrer`: target이 _blank인 경우 보안 강화 + Referer 헤더 제거
497
+ * - `external`: 외부 링크임을 명시 (SEO/접근성에 도움)
498
+ * - `nofollow`: 검색 엔진이 링크를 따라가지 않도록 지시 (SEO에 영향)
499
+ * - 그 외 rel 값도 그대로 전달됩니다.
500
+ */
501
+ rel?: string;
502
+ /**
503
+ * 링크 대상 URL, 다음 사항에 따라 SPA 라우팅 또는 브라우저 네비게이션이 결정됩니다.
504
+ *
505
+ * - 속성을 정의하지 않으면 설정에서 지정한 `basepath`로 SPA 라우팅합니다.
506
+ * - http(s)로 시작하면 외부 링크로 간주하고 브라우저 네비게이션을 사용합니다.
507
+ * - 절대경로(/...)의 경우 `basepath`로 시작하면 SPA 라우팅합니다, 이외 브라우저 네비게이션을 사용합니다.
508
+ * - 상대경로는 (basepath + 상대경로)로 결합하여 SPA 라우팅합니다.
509
+ * - ?로 시작하면 현재 경로에 쿼리스트링을 추가하여 SPA 라우팅합니다.
510
+ * - #으로 시작하면 브라우저 기본 동작을 사용합니다.
511
+ */
512
+ href?: string;
513
+ /**
514
+ * 이 링크를 라우터가 처리할지, 브라우저의 문서 이동에 맡길지.
515
+ *
516
+ * - `router`(기본): 종전 동작 그대로 — 같은 오리진이면 SPA 이동, 아니면 브라우저에 맡긴다.
517
+ * - `document`: 라우터가 **가로채지 않는다.** 같은 오리진이지만 SPA 라우트가 아닌 경로
518
+ * (정적 문서 사이트, 서버 렌더 페이지, 파일 다운로드 엔드포인트, 인증 리다이렉트)를 가리킬 때 쓴다.
519
+ *
520
+ * ⚠**「외부 오리진」이 아니라 「다른 문서」다.** 종전에는 이 구분이 **오리진 비교 하나**로만
521
+ * 결정돼서, 같은 오리진의 비-SPA 경로를 가리킬 수단이 없었다 — 라우터가 클릭을 가로채고
522
+ * 등록되지 않은 라우트이므로 화면이 not-found 로 떨어졌다. 빠져나갈 길이 셋뿐이었고
523
+ * (다른 오리진 · `target="_blank"` · `#` 프래그먼트) 셋 다 요구와 다르다:
524
+ * 같은 오리진이어야 하고(쿠키·세션·역방향 프록시), **같은 탭**이어야 하며, 다른 문서다.
525
+ *
526
+ * ```html
527
+ * <u-link href="/help/" navigate="document">Help</u-link>
528
+ * ```
529
+ *
530
+ * ⚠**자동 판정을 넓히지 않는다.** 「등록된 라우트와 대조해 미등록이면 문서 이동」도 가능하지만
531
+ * 라우트가 늦게 등록되면 판정이 **시점에 의존**하게 된다. 명시 선언이 예측 가능하다.
532
+ */
533
+ navigate?: "router" | "document";
534
+ connectedCallback(): void;
535
+ disconnectedCallback(): void;
536
+ /**
537
+ * 호스트에 세팅된 `aria-current`/`aria-label`은 실제 접근 가능한(포커스 대상)
538
+ * 엘리먼트가 아니라 — 그 안쪽 shadow DOM 의 네이티브 `<a>`다. 섀도우 경계를
539
+ * 넘지 않으므로 접근성 트리에 자동 반영되지 않는다(실측 — 속성은
540
+ * 붙어 있는데 접근성 트리의 `aria-current`는 계속 비어 있음). `render()`가 이
541
+ * 값을 읽어 내부 `<a>`에 직접 옮긴다.
542
+ *
543
+ * 둘 다 Lit 리액티브 프로퍼티로 선언돼 있지 않아 `observedAttributes`에 없다 —
544
+ * 그 목록에 없는 속성은 `attributeChangedCallback` 자체가 호출되지 않는다
545
+ * (커스텀 엘리먼트 표준 동작). 초기 렌더는 되지만 연결 후 동적 변경은 반영되지
546
+ * 않았다 — 목록에 명시적으로 추가해야 한다.
547
+ */
548
+ static get observedAttributes(): string[];
549
+ attributeChangedCallback(name: string, old: string | null, value: string | null): void;
550
+ protected willUpdate(changedProperties: PropertyValues): void;
551
+ render(): TemplateResult<1>;
552
+ /** a 태그에 주입할 href 값을 계산합니다. */
553
+ private compute;
554
+ /**
555
+ * 클릭 가로채기 핸들러
556
+ * - 좌클릭(0) + 보조키 없음(ctrl/meta/shift/alt 없음) + target이 _self일 때만 SPA 라우팅 고려
557
+ * - 그 외(중클릭/우클릭/보조키/target=_blank 등)는 브라우저 기본 동작 유지
558
+ */
559
+ private handleClick;
560
+ /** 클라이언트 라우팅을 위해 popstate 이벤트를 발생시킵니다. */
561
+ private dispatchPopstate;
562
+ /** basepath를 state에서 꺼내는 헬퍼 */
563
+ private getBasepath;
564
+ static styles: CSSResult;
565
+ }
566
+
567
+ /**
568
+ * LitElement 또는 React 컴포넌트를 렌더링해주는 웹컴포넌트 입니다.
569
+ */
570
+ export declare class UOutlet extends HTMLElement {
571
+ /** 교차 렌더링 방지 id */
572
+ private routeId?;
573
+ /** 마지막으로 렌더한 라우트의 식별 키 */
574
+ private routeKey?;
575
+ /** 마운트된 콘텐츠의 종류 */
576
+ private kind?;
577
+ /** 실제 렌더링 컨텐츠 */
578
+ private root?;
579
+ /** 진행 중인 render — 다음 render 는 이것이 끝난 뒤 판정한다 */
580
+ private pending?;
581
+ connectedCallback(): void;
582
+ /**
583
+ * 주어진 렌더링 옵션에 따라 컨텐츠를 렌더링합니다.
584
+ *
585
+ * 같은 라우트(`id`)에 같은 키(`key`)로 다시 불리면 **제자리 갱신**한다 — Lit 템플릿은
586
+ * 같은 파트에 다시 렌더(요소·상태 유지, 바인딩만 갱신), React 엘리먼트는 같은 root 에 다시
587
+ * 렌더(컴포넌트 상태 유지), `HTMLElement` 는 기존 인스턴스를 그대로 둔다. 매번 `reset()`
588
+ * 을 먼저 부르던 종전 동작이 «쿼리스트링만 바뀌어도 페이지가 재마운트되는» 원인이었다
589
+ * (Lit 의 `render` 도 React 의 `root.render` 도 같은 컨테이너에 다시 부르면 조정한다).
590
+ */
591
+ render(value: unknown, options?: RenderOption): Promise<void>;
592
+ private mount;
593
+ /**
594
+ * 기존 DOM을 삭제하여, 초기 상태로 되돌립니다.
595
+ */
596
+ reset(): void;
597
+ }
598
+
599
+ export { }
600
+
601
+
602
+ declare global {
603
+ interface HTMLElementTagNameMap {
604
+ 'u-outlet': UOutlet;
605
+ }
606
+ }
607
+
608
+
609
+ /**
610
+ * 전역 WindowEventMap에 라우터 이벤트 타입
611
+ */
612
+ declare global {
613
+ interface WindowEventMap {
614
+ 'route-begin': RouteBeginEvent;
615
+ 'route-progress': RouteProgressEvent;
616
+ 'route-done': RouteDoneEvent;
617
+ 'route-error': RouteErrorEvent;
618
+ }
619
+ }
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-Cwn-vX3y.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-DxyaaBox.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