@iyulab/router 0.5.0 → 0.5.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.
Files changed (3) hide show
  1. package/dist/index.d.ts +178 -103
  2. package/dist/index.js +222 -355
  3. package/package.json +2 -3
package/dist/index.d.ts CHANGED
@@ -18,22 +18,38 @@ declare interface BaseRouteConfig {
18
18
  */
19
19
  title?: string;
20
20
  /**
21
- * 라우트에 대응하는 렌더링 함수
22
- * - 라우트 정보를 받아 HTMLElement, ReactElement, 또는 LitElement TemplateResult를 반환합니다.
23
- * @param info 라우팅 정보
21
+ * 라우터 경로는 string 또는 URLPattern을 사용할 수 있습니다.
22
+ * string일 경우 자동으로 URLPattern으로 변환됩니다.
23
+ * @default '/'
24
+ * @example
25
+ * - "/user/:id/:name"
26
+ * - "/user/:id/:name?"
27
+ * - "/user/:id/:name*"
28
+ * - "/user/:id/:name+"
29
+ * - "/user/:id/:name{1,3}"
30
+ * @link
31
+ * https://developer.mozilla.org/en-US/docs/Web/API/URLPattern
32
+ */
33
+ path?: string | URLPattern;
34
+ /**
35
+ * 라우트 정보를 받아 렌더링 결과를 반환합니다.
36
+ * @param ctx 현재 라우팅 정보 및, 진행 상태 콜백을 포함하는 Context 객체가 인자로 전달됩니다.
24
37
  * @example
25
38
  * ```typescript
26
39
  * const route = {
27
- * path: '/user',
28
- * render: (info) => html`<user-page .routeInfo=${info}></user-page>`,
40
+ * path: '/user:id',
41
+ * render: async (ctx) => {
42
+ * // 사용자 정보를 비동기로 가져오는 예시
43
+ * const userId = ctx.params.id;
44
+ * ctx.progress(30);
45
+ * const userData = await fetchUserData(userId);
46
+ * ctx.progress(70);
47
+ * return html`<user-profile .data=${userData}></user-profile>`;
48
+ * }
29
49
  * }
30
50
  * ```
31
51
  */
32
- render?: (info: RouteInfo) => Promise<RenderResult> | RenderResult;
33
- /**
34
- * 중첩 라우트
35
- */
36
- children?: RouteConfig[];
52
+ render?: (ctx: RouteContext) => Promise<RenderResult> | RenderResult;
37
53
  /**
38
54
  * 라우터 URL 변경시 렌더링을 강제할지 여부
39
55
  * - 기본값으로 children을 가질때 false로 설정되며, children이 없을 경우 true로 설정됩니다.
@@ -43,19 +59,60 @@ declare interface BaseRouteConfig {
43
59
  }
44
60
 
45
61
  /**
46
- * 인덱스 라우트 타입
62
+ * 컨텐츠 로드시 나타나는 에러
47
63
  */
48
- declare interface IndexRouteConfig extends BaseRouteConfig {
64
+ export declare class ContentLoadError extends RouteError {
65
+ constructor(original?: Error | any);
66
+ }
67
+
68
+ /**
69
+ * 컨텐츠 렌더링시 발생하는 에러
70
+ */
71
+ export declare class ContentRenderError extends RouteError {
72
+ constructor(original?: Error | any);
73
+ }
74
+
75
+ export declare type FallbackRenderResult = HTMLElement | ReactElement | TemplateResult<1>;
76
+
77
+ export declare interface FallbackRouteConfig {
49
78
  /**
50
- * true일 경우 인덱스 라우트입니다.
51
- * path는 강제로 빈 문자열로 설정됩니다.
79
+ * 브라우저의 타이틀이 설정에 따라 변경됩니다.
52
80
  */
53
- index: true;
81
+ title?: string;
54
82
  /**
55
- * 인덱스 라우트의 URLPattern (자동 설정됨)
56
- * 부모 라우트의 basepath를 상속받습니다.
83
+ * 라우팅 실패 표시할 렌더링 결과를 반환합니다.
84
+ * - 오류가 발생할 경우 또는 렌더링 결과가 false일 경우 호출됩니다.
85
+ * @param ctx 현재 라우팅 정보 및 오류 정보를 포함하는 Context 객체가 인자로 전달됩니다.
86
+ * @example
87
+ * ```typescript
88
+ * const fallbackRoute = {
89
+ * title: 'Not Found',
90
+ * render: (ctx) => {
91
+ * if (ctx.error) {
92
+ * return html`<error-page .error=${ctx.error}></error-page>`;
93
+ * }
94
+ * return html`<not-found-page></not-found-page>`;
95
+ * }
96
+ * }
97
+ * ```
57
98
  */
58
- path?: URLPattern;
99
+ render?: (ctx: FallbackRouteContext) => Promise<FallbackRenderResult> | FallbackRenderResult;
100
+ }
101
+
102
+ export declare interface FallbackRouteContext extends RouteContext {
103
+ /**
104
+ * 라우팅 에러 정보
105
+ * - 라우팅 중 발생한 에러 정보를 포함합니다.
106
+ */
107
+ error: RouteError;
108
+ }
109
+
110
+ declare interface IndexRouteConfig extends BaseRouteConfig {
111
+ /**
112
+ * 현재 경로의 인덱스 라우트임을 나타냅니다.
113
+ * - 인덱스 라우트는 부모 경로와 동일한 경로를 가지며, path는 자동으로 설정됩니다.
114
+ */
115
+ index: true;
59
116
  }
60
117
 
61
118
  /**
@@ -99,10 +156,22 @@ export declare class Link extends LitElement {
99
156
  static styles: CSSResult;
100
157
  }
101
158
 
159
+ declare interface NonIndexRouteConfig extends BaseRouteConfig {
160
+ /**
161
+ * 인덱스 라우트가 아님을 나타냅니다.
162
+ */
163
+ index?: false;
164
+ /**
165
+ * 하위 라우트 설정, 재귀적으로 RouteConfig 배열을 가질 수 있습니다.
166
+ * - 하위 라우트가 있는 경우, 부모 라우트의 경로를 기준으로 매칭됩니다.
167
+ */
168
+ children?: RouteConfig[];
169
+ }
170
+
102
171
  /**
103
172
  * 페이지를 찾을 수 없을 때 발생하는 에러
104
173
  */
105
- export declare class NotFoundRouteError extends RouteError {
174
+ export declare class NotFoundError extends RouteError {
106
175
  constructor(path: string, original?: Error | any);
107
176
  }
108
177
 
@@ -129,22 +198,10 @@ export declare class Outlet extends LitElement {
129
198
  }
130
199
 
131
200
  /**
132
- * 경로 라우트 타입
201
+ * u-outlet 요소를 찾을 수 없을 때 발생하는 에러
133
202
  */
134
- declare interface PathRouteConfig extends BaseRouteConfig {
135
- /**
136
- * 라우터 경로는 string 또는 URLPattern을 사용할 수 있습니다.
137
- * string일 경우 자동으로 URLPattern으로 변환됩니다.
138
- * @example
139
- * - "/user/:id/:name"
140
- * - "/user/:id/:name?"
141
- * - "/user/:id/:name*"
142
- * - "/user/:id/:name+"
143
- * - "/user/:id/:name{1,3}"
144
- * @link
145
- * https://developer.mozilla.org/en-US/docs/Web/API/URLPattern
146
- */
147
- path: string | URLPattern;
203
+ export declare class OutletMissingError extends RouteError {
204
+ constructor();
148
205
  }
149
206
 
150
207
  declare interface RenderOption {
@@ -153,76 +210,21 @@ declare interface RenderOption {
153
210
  content: RenderResult;
154
211
  }
155
212
 
156
- declare type RenderResult = HTMLElement | ReactElement | TemplateResult<1>;
213
+ export declare type RenderResult = HTMLElement | ReactElement | TemplateResult<1> | false;
157
214
 
158
215
  /**
159
216
  * 라우트 시작 이벤트
160
217
  */
161
218
  export declare class RouteBeginEvent extends RouteEvent {
162
- constructor(routeInfo: RouteInfo);
163
- }
164
-
165
- /**
166
- * 라우트 타입 (인덱스 라우트 또는 경로 라우트)
167
- */
168
- export declare type RouteConfig = IndexRouteConfig | PathRouteConfig;
169
-
170
- /**
171
- * 라우트 완료 이벤트
172
- */
173
- export declare class RouteDoneEvent extends RouteEvent {
174
- constructor(routeInfo: RouteInfo);
175
- }
176
-
177
- /**
178
- * 라우팅 에러 정보
179
- */
180
- export declare class RouteError extends Error {
181
- /**
182
- * 에러 코드
183
- * - HTTP 상태 코드 또는 커스텀 에러 코드
184
- * @example 404, 500, 'ROUTE_NOT_FOUND'
185
- */
186
- code: number | string;
187
- /**
188
- * 원본 에러 객체
189
- * - 원본 Error 객체 또는 예외 정보
190
- */
191
- original?: Error | any;
192
- /**
193
- * 에러 발생 시간
194
- * - 에러가 발생한 시간 (ISO 8601 형식)
195
- */
196
- timestamp: string;
197
- constructor(code: number | string, message: string, original?: Error | any);
219
+ constructor(context: RouteContext);
198
220
  }
199
221
 
200
- /**
201
- * 라우트 에러 이벤트
202
- */
203
- export declare class RouteErrorEvent extends RouteEvent {
204
- /** 에러 정보 */
205
- readonly error: RouteError;
206
- constructor(error: RouteError, routeInfo: RouteInfo);
207
- }
208
-
209
- /** 라우터 이벤트 기본 클래스 */
210
- declare abstract class RouteEvent extends Event {
211
- /** 라우팅 정보 */
212
- readonly routeInfo: RouteInfo;
213
- /** 이벤트 발생 시간 */
214
- readonly timestamp: string;
215
- constructor(type: string, routeInfo: RouteInfo, cancelable?: boolean);
216
- /** 이벤트가 취소되었는지 확인 */
217
- get cancelled(): boolean;
218
- /** 이벤트 취소 */
219
- cancel(): void;
220
- }
222
+ export declare type RouteConfig = IndexRouteConfig | NonIndexRouteConfig;
221
223
 
222
224
  /**
223
225
  * 라우터 정보
224
226
  */
225
- export declare interface RouteInfo {
227
+ export declare interface RouteContext {
226
228
  /**
227
229
  * 전체 URL 정보
228
230
  * - 도메인 이름을 포함한 URL의 전체 경로입니다.
@@ -284,6 +286,73 @@ export declare interface RouteInfo {
284
286
  * @example #profile
285
287
  */
286
288
  hash?: string;
289
+ /**
290
+ * 현재 라우팅의 진행 상태를 업데이트하여, window 객체의 'route-progress' 이벤트를 트리거합니다.
291
+ * 여러 라우팅이 호출되는 경우, 가장 최근의 라우팅의 진행 상태만 반영되며, 나머지는 무시됩니다.
292
+ * @param value 진행 상태 값 (0~100)
293
+ */
294
+ progress: (value: number) => void;
295
+ }
296
+
297
+ /**
298
+ * 라우트 완료 이벤트
299
+ */
300
+ export declare class RouteDoneEvent extends RouteEvent {
301
+ constructor(context: RouteContext);
302
+ }
303
+
304
+ /**
305
+ * 라우팅 에러 정보
306
+ */
307
+ export declare class RouteError extends Error {
308
+ /**
309
+ * 에러 코드
310
+ * - HTTP 상태 코드 또는 커스텀 에러 코드
311
+ * @example 404, 500, 'ROUTE_NOT_FOUND'
312
+ */
313
+ code: number | string;
314
+ /**
315
+ * 원본 에러 객체
316
+ * - 원본 Error 객체 또는 예외 정보
317
+ */
318
+ original?: Error | any;
319
+ /**
320
+ * 에러 발생 시간
321
+ * - 에러가 발생한 시간 (ISO 8601 형식)
322
+ */
323
+ timestamp: string;
324
+ constructor(code: number | string, message: string, original?: Error | any);
325
+ }
326
+
327
+ /**
328
+ * 라우트 에러 이벤트
329
+ */
330
+ export declare class RouteErrorEvent extends RouteEvent {
331
+ /** 에러 정보 */
332
+ readonly error: RouteError;
333
+ constructor(context: RouteContext, error: RouteError);
334
+ }
335
+
336
+ /** 라우터 이벤트 기본 클래스 */
337
+ declare abstract class RouteEvent extends Event {
338
+ /** 라우팅 정보 */
339
+ readonly context: RouteContext;
340
+ /** 이벤트 발생 시간 */
341
+ readonly timestamp: string;
342
+ constructor(type: string, context: RouteContext, cancelable?: boolean);
343
+ /** 이벤트가 취소되었는지 확인 */
344
+ get cancelled(): boolean;
345
+ /** 이벤트 취소 */
346
+ cancel(): void;
347
+ }
348
+
349
+ /**
350
+ * 라우트 진행 이벤트
351
+ */
352
+ export declare class RouteProgressEvent extends RouteEvent {
353
+ /** 진행 상태 값 (0~100) */
354
+ readonly progress: number;
355
+ constructor(context: RouteContext, progress: number);
287
356
  }
288
357
 
289
358
  /**
@@ -293,21 +362,20 @@ export declare class Router {
293
362
  private readonly _rootElement;
294
363
  private readonly _basepath;
295
364
  private readonly _routes;
365
+ private readonly _fallback?;
296
366
  /** 현재 라우팅 요청 ID */
297
367
  private _requestID?;
298
368
  /** 현재 라우팅 정보 */
299
- private _routeInfo?;
369
+ private _context?;
300
370
  constructor(config: RouterConfig);
301
- /** 초기 라우팅 처리, TODO: 제거 */
302
- private waitConnected;
303
371
  /** 객체를 정리하고 이벤트 리스너를 제거합니다. */
304
372
  destroy(): void;
305
373
  /** 라우터의 기본 경로 반환 */
306
374
  get basepath(): string;
307
- /** 등록된 라우트 반환 */
375
+ /** 등록된 라우트 정보 반환 */
308
376
  get routes(): RouteConfig[];
309
377
  /** 현재 라우팅 정보 반환 */
310
- get routeInfo(): RouteInfo | undefined;
378
+ get context(): RouteContext | undefined;
311
379
  /**
312
380
  * 지정한 경로의 클라이언트 라우팅을 수행합니다. 상대경로일 경우 basepath와 조합되어 이동합니다.
313
381
  * @param href 이동할 경로
@@ -339,12 +407,22 @@ export declare interface RouterConfig {
339
407
  * - 라우트는 URLPattern을 사용하여 경로를 탐색합니다.
340
408
  * - 라우트는 렌더링할 엘리먼트 또는 컴포넌트를 지정합니다.
341
409
  */
342
- routes: RouteConfig[];
410
+ routes?: RouteConfig[];
411
+ /**
412
+ * 라우트 매칭 실패 또는 오류 발생 시 대체 라우트 설정
413
+ * - 지정된 설정이 없을 경우, 기본 오류 페이지가 렌더링됩니다.
414
+ */
415
+ fallback?: FallbackRouteConfig;
343
416
  /**
344
417
  * `a` 태그 클릭 시 클라이언트 라우팅을 수행할지 여부를 설정합니다.
345
418
  * @default true
346
419
  */
347
420
  useIntercept?: boolean;
421
+ /**
422
+ * 초기 로드 시 현재 URL로 라우팅을 자동으로 수행할지 여부를 설정합니다.
423
+ * @default true
424
+ */
425
+ initialLoad?: boolean;
348
426
  }
349
427
 
350
428
  export declare const ULink: ReactWebComponent<Link, {}>;
@@ -354,12 +432,9 @@ export declare const UOutlet: ReactWebComponent<Outlet, {}>;
354
432
  export { }
355
433
 
356
434
  declare global {
357
- interface Window {
358
- route: RouteInfo;
359
- }
360
-
361
435
  interface WindowEventMap {
362
436
  'route-begin': RouteBeginEvent;
437
+ 'route-progress': RouteProgressEvent;
363
438
  'route-done': RouteDoneEvent;
364
439
  'route-error': RouteErrorEvent;
365
440
  }
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ import React from "react";
2
2
  import { LitElement, html, css, render } from "lit";
3
3
  import { state, property, customElement } from "lit/decorators.js";
4
4
  import { createRoot } from "react-dom/client";
5
+ import { unsafeHTML } from "lit-html/directives/unsafe-html.js";
5
6
  const e = /* @__PURE__ */ new Set(["children", "localName", "ref", "style", "className"]), n = /* @__PURE__ */ new WeakMap(), t = (e2, t2, o2, l, a) => {
6
7
  const s = a?.[t2];
7
8
  void 0 === s ? (e2[t2] = o2, null == o2 && t2 in HTMLElement.prototype && e2.removeAttribute(t2)) : o2 !== l && ((e3, t3, o3) => {
@@ -64,7 +65,9 @@ function parseUrl(url, basepath) {
64
65
  pathname: urlObj.pathname,
65
66
  query: new URLSearchParams(urlObj.search),
66
67
  hash: urlObj.hash,
67
- params: {}
68
+ params: {},
69
+ progress: () => {
70
+ }
68
71
  };
69
72
  }
70
73
  function absolutePath(...paths) {
@@ -213,7 +216,10 @@ class Outlet extends LitElement {
213
216
  this.routeId = id;
214
217
  this.clear();
215
218
  if (!this.container) {
216
- throw new Error("DOM이 초기화되지 않았습니다.");
219
+ throw new Error("Outlet container is not initialized.");
220
+ }
221
+ if (content === false) {
222
+ throw new Error("Content is false, cannot render.");
217
223
  }
218
224
  if (content instanceof HTMLElement) {
219
225
  this.container.appendChild(content);
@@ -270,19 +276,30 @@ class RouteError extends Error {
270
276
  }
271
277
  }
272
278
  }
273
- class NotFoundRouteError extends RouteError {
279
+ class NotFoundError extends RouteError {
274
280
  constructor(path, original) {
275
281
  super(404, `Page not found: ${path}`, original);
276
- this.name = "NotFoundError";
277
- if (Error.captureStackTrace) {
278
- Error.captureStackTrace(this, NotFoundRouteError);
279
- }
282
+ }
283
+ }
284
+ class OutletMissingError extends RouteError {
285
+ constructor() {
286
+ super("OUTLET_MISSING", "Router outlet element not found. Add <u-outlet> to your template.");
287
+ }
288
+ }
289
+ class ContentLoadError extends RouteError {
290
+ constructor(original) {
291
+ super("CONTENT_LOAD_FAILED", "Failed to load route content. Check browser console for details.", original);
292
+ }
293
+ }
294
+ class ContentRenderError extends RouteError {
295
+ constructor(original) {
296
+ super("CONTENT_RENDER_FAILED", "Failed to render route component. Check browser console for details.", original);
280
297
  }
281
298
  }
282
299
  class RouteEvent extends Event {
283
- constructor(type, routeInfo, cancelable = false) {
300
+ constructor(type, context, cancelable = false) {
284
301
  super(type, { bubbles: true, composed: true, cancelable });
285
- this.routeInfo = routeInfo;
302
+ this.context = context;
286
303
  this.timestamp = (/* @__PURE__ */ new Date()).toISOString();
287
304
  }
288
305
  /** 이벤트가 취소되었는지 확인 */
@@ -297,277 +314,118 @@ class RouteEvent extends Event {
297
314
  }
298
315
  }
299
316
  class RouteBeginEvent extends RouteEvent {
300
- constructor(routeInfo) {
301
- super("route-begin", routeInfo, false);
317
+ constructor(context) {
318
+ super("route-begin", context, false);
319
+ }
320
+ }
321
+ class RouteProgressEvent extends RouteEvent {
322
+ constructor(context, progress) {
323
+ super("route-progress", context, false);
324
+ this.progress = progress;
302
325
  }
303
326
  }
304
327
  class RouteDoneEvent extends RouteEvent {
305
- constructor(routeInfo) {
306
- super("route-done", routeInfo, false);
328
+ constructor(context) {
329
+ super("route-done", context, false);
307
330
  }
308
331
  }
309
332
  class RouteErrorEvent extends RouteEvent {
310
- constructor(error, routeInfo) {
311
- super("route-error", routeInfo, false);
333
+ constructor(context, error) {
334
+ super("route-error", context, false);
312
335
  this.error = error;
313
336
  }
314
337
  }
338
+ const ban = '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16">\n <path d="M15 8a6.97 6.97 0 0 0-1.71-4.584l-9.874 9.875A7 7 0 0 0 15 8M2.71 12.584l9.874-9.875a7 7 0 0 0-9.874 9.874ZM16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0"/>\n</svg>';
339
+ const __vite_glob_0_0 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
340
+ __proto__: null,
341
+ default: ban
342
+ }, Symbol.toStringTag, { value: "Module" }));
343
+ const boxSeam = '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16">\n <path d="M8.186 1.113a.5.5 0 0 0-.372 0L1.846 3.5l2.404.961L10.404 2zm3.564 1.426L5.596 5 8 5.961 14.154 3.5zm3.25 1.7-6.5 2.6v7.922l6.5-2.6V4.24zM7.5 14.762V6.838L1 4.239v7.923zM7.443.184a1.5 1.5 0 0 1 1.114 0l7.129 2.852A.5.5 0 0 1 16 3.5v8.662a1 1 0 0 1-.629.928l-7.185 2.874a.5.5 0 0 1-.372 0L.63 13.09a1 1 0 0 1-.63-.928V3.5a.5.5 0 0 1 .314-.464z"/>\n</svg>';
344
+ const __vite_glob_0_1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
345
+ __proto__: null,
346
+ default: boxSeam
347
+ }, Symbol.toStringTag, { value: "Module" }));
348
+ const exclamationTriangle = '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16">\n <path d="M7.938 2.016A.13.13 0 0 1 8.002 2a.13.13 0 0 1 .063.016.15.15 0 0 1 .054.057l6.857 11.667c.036.06.035.124.002.183a.2.2 0 0 1-.054.06.1.1 0 0 1-.066.017H1.146a.1.1 0 0 1-.066-.017.2.2 0 0 1-.054-.06.18.18 0 0 1 .002-.183L7.884 2.073a.15.15 0 0 1 .054-.057m1.044-.45a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767z"/>\n <path d="M7.002 12a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 5.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0z"/>\n</svg>';
349
+ const __vite_glob_0_2 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
350
+ __proto__: null,
351
+ default: exclamationTriangle
352
+ }, Symbol.toStringTag, { value: "Module" }));
353
+ const palette = '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16">\n <path d="M8 5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3m4 3a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3M5.5 7a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m.5 6a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3"/>\n <path d="M16 8c0 3.15-1.866 2.585-3.567 2.07C11.42 9.763 10.465 9.473 10 10c-.603.683-.475 1.819-.351 2.92C9.826 14.495 9.996 16 8 16a8 8 0 1 1 8-8m-8 7c.611 0 .654-.171.655-.176.078-.146.124-.464.07-1.119-.014-.168-.037-.37-.061-.591-.052-.464-.112-1.005-.118-1.462-.01-.707.083-1.61.704-2.314.369-.417.845-.578 1.272-.618.404-.038.812.026 1.16.104.343.077.702.186 1.025.284l.028.008c.346.105.658.199.953.266.653.148.904.083.991.024C14.717 9.38 15 9.161 15 8a7 7 0 1 0-7 7"/>\n</svg>';
354
+ const __vite_glob_0_3 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
355
+ __proto__: null,
356
+ default: palette
357
+ }, Symbol.toStringTag, { value: "Module" }));
358
+ const personLock = '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16">\n <path d="M11 5a3 3 0 1 1-6 0 3 3 0 0 1 6 0M8 7a2 2 0 1 0 0-4 2 2 0 0 0 0 4m0 5.996V14H3s-1 0-1-1 1-4 6-4q.845.002 1.544.107a4.5 4.5 0 0 0-.803.918A11 11 0 0 0 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664zM9 13a1 1 0 0 1 1-1v-1a2 2 0 1 1 4 0v1a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1zm3-3a1 1 0 0 0-1 1v1h2v-1a1 1 0 0 0-1-1"/>\n</svg>';
359
+ const __vite_glob_0_4 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
360
+ __proto__: null,
361
+ default: personLock
362
+ }, Symbol.toStringTag, { value: "Module" }));
363
+ const search = '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16">\n <path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001q.044.06.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1 1 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0"/>\n</svg>';
364
+ const __vite_glob_0_5 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
365
+ __proto__: null,
366
+ default: search
367
+ }, Symbol.toStringTag, { value: "Module" }));
368
+ const stopwatch = '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16">\n <path d="M8.5 5.6a.5.5 0 1 0-1 0v2.9h-3a.5.5 0 0 0 0 1H8a.5.5 0 0 0 .5-.5z"/>\n <path d="M6.5 1A.5.5 0 0 1 7 .5h2a.5.5 0 0 1 0 1v.57c1.36.196 2.594.78 3.584 1.64l.012-.013.354-.354-.354-.353a.5.5 0 0 1 .707-.708l1.414 1.415a.5.5 0 1 1-.707.707l-.353-.354-.354.354-.013.012A7 7 0 1 1 7 2.071V1.5a.5.5 0 0 1-.5-.5M8 3a6 6 0 1 0 .001 12A6 6 0 0 0 8 3"/>\n</svg>';
369
+ const __vite_glob_0_6 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
370
+ __proto__: null,
371
+ default: stopwatch
372
+ }, Symbol.toStringTag, { value: "Module" }));
373
+ const wifiOff = '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16">\n <path d="M10.706 3.294A12.6 12.6 0 0 0 8 3C5.259 3 2.723 3.882.663 5.379a.485.485 0 0 0-.048.736.52.52 0 0 0 .668.05A11.45 11.45 0 0 1 8 4q.946 0 1.852.148zM8 6c-1.905 0-3.68.56-5.166 1.526a.48.48 0 0 0-.063.745.525.525 0 0 0 .652.065 8.45 8.45 0 0 1 3.51-1.27zm2.596 1.404.785-.785q.947.362 1.785.907a.482.482 0 0 1 .063.745.525.525 0 0 1-.652.065 8.5 8.5 0 0 0-1.98-.932zM8 10l.933-.933a6.5 6.5 0 0 1 2.013.637c.285.145.326.524.1.75l-.015.015a.53.53 0 0 1-.611.09A5.5 5.5 0 0 0 8 10m4.905-4.905.747-.747q.886.451 1.685 1.03a.485.485 0 0 1 .047.737.52.52 0 0 1-.668.05 11.5 11.5 0 0 0-1.811-1.07M9.02 11.78c.238.14.236.464.04.66l-.707.706a.5.5 0 0 1-.707 0l-.707-.707c-.195-.195-.197-.518.04-.66A2 2 0 0 1 8 11.5c.374 0 .723.102 1.021.28zm4.355-9.905a.53.53 0 0 1 .75.75l-10.75 10.75a.53.53 0 0 1-.75-.75z"/>\n</svg>';
374
+ const __vite_glob_0_7 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
375
+ __proto__: null,
376
+ default: wifiOff
377
+ }, Symbol.toStringTag, { value: "Module" }));
378
+ const wrenchAdjustable = '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16">\n <path d="M16 4.5a4.5 4.5 0 0 1-1.703 3.526L13 5l2.959-1.11q.04.3.041.61"/>\n <path d="M11.5 9c.653 0 1.273-.139 1.833-.39L12 5.5 11 3l3.826-1.53A4.5 4.5 0 0 0 7.29 6.092l-6.116 5.096a2.583 2.583 0 1 0 3.638 3.638L9.908 8.71A4.5 4.5 0 0 0 11.5 9m-1.292-4.361-.596.893.809-.27a.25.25 0 0 1 .287.377l-.596.893.809-.27.158.475-1.5.5a.25.25 0 0 1-.287-.376l.596-.893-.809.27a.25.25 0 0 1-.287-.377l.596-.893-.809.27-.158-.475 1.5-.5a.25.25 0 0 1 .287.376M3 14a1 1 0 1 1 0-2 1 1 0 0 1 0 2"/>\n</svg>';
379
+ const __vite_glob_0_8 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
380
+ __proto__: null,
381
+ default: wrenchAdjustable
382
+ }, Symbol.toStringTag, { value: "Module" }));
315
383
  const styles = css`
316
384
  :host {
317
385
  display: flex;
386
+ flex-direction: column;
318
387
  justify-content: center;
319
388
  align-items: center;
320
389
  min-height: 100vh;
321
390
  width: 100%;
322
- padding: 2rem;
323
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
324
- background: var(--route-error-background, linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%));
325
- color: var(--route-error-color, #2d3748);
326
- line-height: 1.6;
327
- }
328
-
329
- .container {
330
- max-width: 520px;
331
- margin: 0 auto;
332
391
  text-align: center;
333
- background: var(--route-error-container-bg, rgba(255, 255, 255, 0.95));
334
- backdrop-filter: blur(10px);
335
- border-radius: var(--route-error-border-radius, 24px);
336
- padding: 3rem 2rem;
337
- box-shadow: var(--route-error-box-shadow, 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04));
338
- border: 1px solid var(--route-error-border, rgba(255, 255, 255, 0.2));
339
- animation: slideUp 0.6s ease-out;
340
- }
341
-
342
- @keyframes slideUp {
343
- from {
344
- opacity: 0;
345
- transform: translateY(30px);
346
- }
347
- to {
348
- opacity: 1;
349
- transform: translateY(0);
350
- }
392
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
393
+ overflow: auto;
394
+ user-select: none;
351
395
  }
352
396
 
353
397
  .icon {
354
- font-size: 5rem;
355
- margin-bottom: 1.5rem;
356
- animation: bounce 0.8s ease-out 0.2s both;
357
- filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.1));
358
- }
359
-
360
- @keyframes bounce {
361
- 0%, 20%, 53%, 80%, 100% {
362
- transform: translate3d(0, 0, 0);
363
- }
364
- 40%, 43% {
365
- transform: translate3d(0, -10px, 0);
366
- }
367
- 70% {
368
- transform: translate3d(0, -5px, 0);
369
- }
370
- 90% {
371
- transform: translate3d(0, -2px, 0);
372
- }
398
+ display: contents;
399
+ font-size: 6rem;
400
+ color: var(--route-error-color, #4a5568);
401
+ opacity: 0.85;
373
402
  }
374
403
 
375
404
  .code {
376
405
  font-size: 2rem;
377
406
  font-weight: 700;
378
- margin-bottom: 1rem;
379
- color: var(--route-error-code-color, #4a5568);
380
- letter-spacing: -0.025em;
407
+ margin: 1rem 0;
408
+ color: var(--route-error-code-color, #1a202c);
409
+ letter-spacing: -0.5px;
381
410
  }
382
411
 
383
412
  .message {
384
- font-size: 1.125rem;
385
- margin-bottom: 2.5rem;
413
+ font-size: 1rem;
386
414
  color: var(--route-error-message-color, #718096);
387
- font-weight: 400;
388
- max-width: 400px;
389
- margin-left: auto;
390
- margin-right: auto;
391
- }
392
-
393
- .actions {
394
- display: flex;
395
- gap: 1rem;
396
- justify-content: center;
397
- flex-wrap: wrap;
398
- }
399
-
400
- .button {
401
- position: relative;
402
- padding: 0.875rem 2rem;
403
- border: none;
404
- border-radius: 12px;
405
- font-size: 0.95rem;
406
- font-weight: 600;
407
- cursor: pointer;
408
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
409
- text-decoration: none;
410
- display: inline-flex;
411
- align-items: center;
412
- justify-content: center;
413
- gap: 0.5rem;
414
- font-family: inherit;
415
- min-width: 120px;
416
- overflow: hidden;
417
- user-select: none;
418
- -webkit-tap-highlight-color: transparent;
419
- }
420
-
421
- .button:first-child {
422
- background: var(--route-error-primary-button-bg, linear-gradient(135deg, #667eea 0%, #764ba2 100%));
423
- color: var(--route-error-primary-button-color, white);
424
- box-shadow: 0 4px 15px 0 rgba(102, 126, 234, 0.4);
425
- }
426
-
427
- .button:first-child:hover {
428
- transform: translateY(-2px);
429
- box-shadow: 0 8px 25px 0 rgba(102, 126, 234, 0.5);
430
- }
431
-
432
- .button:first-child:active {
433
- transform: translateY(0);
434
- }
435
-
436
- .button:last-child {
437
- background: var(--route-error-secondary-button-bg, rgba(255, 255, 255, 0.9));
438
- color: var(--route-error-secondary-button-color, #4a5568);
439
- border: 2px solid var(--route-error-secondary-button-border, rgba(74, 85, 104, 0.2));
440
- backdrop-filter: blur(10px);
441
- }
442
-
443
- .button:last-child:hover {
444
- background: var(--route-error-secondary-button-hover-bg, rgba(255, 255, 255, 1));
445
- border-color: var(--route-error-secondary-button-hover-border, rgba(74, 85, 104, 0.4));
446
- transform: translateY(-1px);
447
- }
448
-
449
- .button:focus-visible {
450
- outline: 2px solid var(--route-error-focus-color, #667eea);
451
- outline-offset: 2px;
452
- }
453
-
454
- .button::before {
455
- content: '';
456
- position: absolute;
457
- top: 0;
458
- left: -100%;
459
- width: 100%;
460
- height: 100%;
461
- background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
462
- transition: left 0.5s;
463
- }
464
-
465
- .button:hover::before {
466
- left: 100%;
467
- }
468
-
469
- @media (max-width: 640px) {
470
- :host {
471
- padding: 1rem;
472
- }
473
-
474
- .container {
475
- padding: 2rem 1.5rem;
476
- border-radius: 20px;
477
- }
478
-
479
- .icon {
480
- font-size: 4rem;
481
- }
482
-
483
- .code {
484
- font-size: 1.75rem;
485
- }
486
-
487
- .message {
488
- font-size: 1rem;
489
- margin-bottom: 2rem;
490
- }
491
-
492
- .actions {
493
- flex-direction: column;
494
- align-items: center;
495
- gap: 0.75rem;
496
- }
497
-
498
- .button {
499
- width: 100%;
500
- max-width: 280px;
501
- padding: 1rem 2rem;
502
- }
503
- }
504
-
505
- @media (max-width: 480px) {
506
- .container {
507
- margin: 1rem;
508
- padding: 1.5rem 1rem;
509
- }
510
-
511
- .icon {
512
- font-size: 3.5rem;
513
- }
415
+ max-width: 600px;
416
+ line-height: 1.6;
514
417
  }
515
418
 
516
419
  @media (prefers-color-scheme: dark) {
517
- :host {
518
- background: var(--route-error-dark-background, linear-gradient(135deg, #1a202c 0%, #2d3748 100%));
519
- color: var(--route-error-dark-color, #e2e8f0);
520
- }
521
-
522
- .container {
523
- background: var(--route-error-dark-container-bg, rgba(45, 55, 72, 0.95));
524
- border: 1px solid var(--route-error-dark-border, rgba(255, 255, 255, 0.1));
420
+ .icon {
421
+ color: var(--route-error-dark-color, #a0aec0);
422
+ opacity: 0.9;
525
423
  }
526
-
527
424
  .code {
528
425
  color: var(--route-error-dark-code-color, #f7fafc);
529
426
  }
530
-
531
427
  .message {
532
- color: var(--route-error-dark-message-color, #a0aec0);
533
- }
534
-
535
- .button:first-child {
536
- background: var(--route-error-dark-primary-button-bg, linear-gradient(135deg, #553c9a 0%, #764ba2 100%));
537
- box-shadow: 0 4px 15px 0 rgba(85, 60, 154, 0.4);
538
- }
539
-
540
- .button:first-child:hover {
541
- box-shadow: 0 8px 25px 0 rgba(85, 60, 154, 0.5);
542
- }
543
-
544
- .button:last-child {
545
- background: var(--route-error-dark-secondary-button-bg, rgba(74, 85, 104, 0.3));
546
- color: var(--route-error-dark-secondary-button-color, #e2e8f0);
547
- border: 2px solid var(--route-error-dark-secondary-button-border, rgba(226, 232, 240, 0.2));
548
- }
549
-
550
- .button:last-child:hover {
551
- background: var(--route-error-dark-secondary-button-hover-bg, rgba(74, 85, 104, 0.5));
552
- border-color: var(--route-error-dark-secondary-button-hover-border, rgba(226, 232, 240, 0.4));
553
- }
554
- }
555
-
556
- @media (prefers-reduced-motion: reduce) {
557
- .container {
558
- animation: none;
559
- }
560
-
561
- .icon {
562
- animation: none;
563
- }
564
-
565
- .button::before {
566
- display: none;
567
- }
568
-
569
- .button {
570
- transition: none;
428
+ color: var(--route-error-dark-message-color, #cbd5e0);
571
429
  }
572
430
  }
573
431
  `;
@@ -581,34 +439,29 @@ var __decorateClass = (decorators, target, key, kind) => {
581
439
  if (kind && result) __defProp(target, key, result);
582
440
  return result;
583
441
  };
442
+ const icons = Object.entries(/* @__PURE__ */ Object.assign({
443
+ "../assets/ban.svg": __vite_glob_0_0,
444
+ "../assets/box-seam.svg": __vite_glob_0_1,
445
+ "../assets/exclamation-triangle.svg": __vite_glob_0_2,
446
+ "../assets/palette.svg": __vite_glob_0_3,
447
+ "../assets/person-lock.svg": __vite_glob_0_4,
448
+ "../assets/search.svg": __vite_glob_0_5,
449
+ "../assets/stopwatch.svg": __vite_glob_0_6,
450
+ "../assets/wifi-off.svg": __vite_glob_0_7,
451
+ "../assets/wrench-adjustable.svg": __vite_glob_0_8
452
+ })).reduce((acc, [path, content]) => {
453
+ const name = path.split("/").pop()?.replace(".svg", "") || "";
454
+ acc[name] = content.default;
455
+ return acc;
456
+ }, {});
584
457
  let ErrorPage = class extends LitElement {
585
458
  render() {
586
459
  const error = this.error || this.getDefaultError();
587
460
  const icon = this.getErrorIcon(error.code);
588
461
  return html`
589
- <div class="container" role="alert" aria-live="polite">
590
- <div class="icon" aria-hidden="true">${icon}</div>
591
- <div class="code" aria-label="Error code">${error.code}</div>
592
- <div class="message">${error.message}</div>
593
-
594
- <div class="actions">
595
- <button
596
- class="button"
597
- @click=${this.handleGoBack}
598
- title="Go back to previous page"
599
- aria-label="Go back to previous page">
600
- ← Go Back
601
- </button>
602
-
603
- <button
604
- class="button"
605
- @click=${this.handleRefresh}
606
- title="Refresh the current page"
607
- aria-label="Refresh the current page">
608
- 🔄 Refresh
609
- </button>
610
- </div>
611
- </div>
462
+ <div class="icon">${unsafeHTML(icon)}</div>
463
+ <div class="code">${error.code}</div>
464
+ <div class="message">${error.message}</div>
612
465
  `;
613
466
  }
614
467
  /** 기본 에러 정보 반환 */
@@ -617,31 +470,31 @@ let ErrorPage = class extends LitElement {
617
470
  }
618
471
  /** 에러 코드에 따른 기본 아이콘 반환 */
619
472
  getErrorIcon(code) {
473
+ const codeStr = String(code);
620
474
  const numericCode = typeof code === "string" ? parseInt(code) : code;
475
+ switch (codeStr) {
476
+ case "OUTLET_NOT_FOUND":
477
+ return icons["box-seam"] || "📦";
478
+ case "CONTENT_LOAD_FAILED":
479
+ return icons["wifi-off"] || "📡";
480
+ case "RENDER_FAILED":
481
+ return icons["palette"] || "🎨";
482
+ }
621
483
  switch (numericCode) {
622
484
  case 404:
623
- return "🔍";
485
+ return icons["search"] || "🔍";
624
486
  case 403:
625
- return "🔒";
487
+ return icons["ban"] || "🚫";
626
488
  case 401:
627
- return "🔑";
489
+ return icons["person-lock"] || "🔐";
628
490
  case 429:
629
- return "⏱️";
491
+ return icons["stopwatch"] || "⏱️";
630
492
  case 503:
631
- return "🛠️";
632
- case 500:
493
+ return icons["wrench-adjustable"] || "🛠️";
633
494
  default:
634
- return "⚠️";
495
+ return icons["exclamation-triangle"] || "⚠️";
635
496
  }
636
497
  }
637
- /** 뒤로가기 */
638
- handleGoBack() {
639
- window.history.back();
640
- }
641
- /** 새로고침 */
642
- handleRefresh() {
643
- window.location.reload();
644
- }
645
498
  };
646
499
  ErrorPage.styles = styles;
647
500
  __decorateClass([
@@ -675,7 +528,7 @@ function findOutlet(element) {
675
528
  function findOutletOrThrow(element) {
676
529
  const outlet = findOutlet(element);
677
530
  if (!outlet) {
678
- throw new Error("No Outlet component found in the root element.");
531
+ throw new OutletMissingError();
679
532
  }
680
533
  return outlet;
681
534
  }
@@ -695,51 +548,41 @@ function findAnchorFromEvent(e2) {
695
548
  function setRoutes(routes, basepath) {
696
549
  for (const route of routes) {
697
550
  route.id ||= getRandomID();
698
- if ("index" in route && route.index) {
551
+ if (route.index === true) {
699
552
  route.path = new URLPattern({ pathname: `${basepath}{/}?` });
700
- } else if ("path" in route && route.path) {
553
+ route.force ||= true;
554
+ } else {
701
555
  if (typeof route.path === "string") {
702
556
  const absolutePathStr = absolutePath(basepath, route.path);
703
557
  route.path = new URLPattern({ pathname: `${absolutePathStr}{/}?` });
558
+ } else if (route.path instanceof URLPattern) ;
559
+ else {
560
+ route.path = new URLPattern({ pathname: `${basepath}{/}?` });
704
561
  }
705
- } else {
706
- throw new Error('Route must have either "index" or "path" property defined.');
707
- }
708
- if (route.children && route.children.length > 0) {
709
- let childBasepath;
710
- if ("index" in route) {
711
- childBasepath = basepath;
562
+ if (route.children && route.children.length > 0) {
563
+ const childBasepath = route.path.pathname.replace("{/}?", "");
564
+ route.children = setRoutes(route.children, childBasepath);
565
+ route.force ||= false;
712
566
  } else {
713
- if (typeof route.path === "string") {
714
- childBasepath = absolutePath(basepath, route.path);
715
- } else {
716
- childBasepath = route.path.pathname.replace("{/}?", "");
717
- }
567
+ route.force ||= true;
718
568
  }
719
- route.children = setRoutes(route.children, childBasepath);
720
- route.force ||= false;
721
- } else {
722
- route.force ||= true;
723
569
  }
724
570
  }
725
571
  return routes;
726
572
  }
727
573
  function getRoutes(pathname, routes) {
728
574
  for (const route of routes) {
729
- if (route.children) {
575
+ if (route.index !== true && route.children && route.children.length > 0) {
730
576
  const childRoutes = getRoutes(pathname, route.children);
731
577
  if (childRoutes.length > 0) {
732
578
  return [route, ...childRoutes];
733
579
  }
734
580
  }
735
- let matches = false;
736
- if ("index" in route && route.index && route.path) {
737
- matches = route.path.test({ pathname });
738
- } else if ("path" in route && route.path instanceof URLPattern) {
739
- matches = route.path.test({ pathname });
740
- }
741
- if (matches) {
742
- return [route];
581
+ if (route.path instanceof URLPattern) {
582
+ const isMatch = route.path.test({ pathname });
583
+ if (isMatch) return [route];
584
+ } else {
585
+ throw new Error("Route path must be an instance of URLPattern, Something wrong in setRoutes function.");
743
586
  }
744
587
  }
745
588
  return [];
@@ -769,44 +612,36 @@ class Router {
769
612
  };
770
613
  this._rootElement = config.root;
771
614
  this._basepath = absolutePath(config.basepath || "/");
772
- this._routes = setRoutes(config.routes, this._basepath);
773
- this.waitConnected();
615
+ this._routes = setRoutes(config.routes || [], this._basepath);
616
+ this._fallback = config.fallback;
774
617
  window.removeEventListener("popstate", this.handleWindowPopstate);
775
618
  window.addEventListener("popstate", this.handleWindowPopstate);
776
619
  if (config.useIntercept !== false) {
777
620
  this._rootElement.removeEventListener("click", this.handleRootClick);
778
621
  this._rootElement.addEventListener("click", this.handleRootClick);
779
622
  }
780
- }
781
- /** 초기 라우팅 처리, TODO: 제거 */
782
- async waitConnected() {
783
- let outlet = findOutlet(this._rootElement);
784
- let count = 0;
785
- while (!outlet && count < 20) {
786
- await new Promise((resolve) => setTimeout(resolve, 50));
787
- outlet = findOutlet(this._rootElement);
788
- count++;
623
+ if (config.initialLoad !== false) {
624
+ void this.go(window.location.href);
789
625
  }
790
- this.handleWindowPopstate();
791
626
  }
792
627
  /** 객체를 정리하고 이벤트 리스너를 제거합니다. */
793
628
  destroy() {
794
629
  window.removeEventListener("popstate", this.handleWindowPopstate);
795
630
  this._rootElement.removeEventListener("click", this.handleRootClick);
796
- this._routeInfo = void 0;
797
631
  this._requestID = void 0;
632
+ this._context = void 0;
798
633
  }
799
634
  /** 라우터의 기본 경로 반환 */
800
635
  get basepath() {
801
636
  return this._basepath;
802
637
  }
803
- /** 등록된 라우트 반환 */
638
+ /** 등록된 라우트 정보 반환 */
804
639
  get routes() {
805
640
  return this._routes;
806
641
  }
807
642
  /** 현재 라우팅 정보 반환 */
808
- get routeInfo() {
809
- return this._routeInfo;
643
+ get context() {
644
+ return this._context;
810
645
  }
811
646
  /**
812
647
  * 지정한 경로의 클라이언트 라우팅을 수행합니다. 상대경로일 경우 basepath와 조합되어 이동합니다.
@@ -815,68 +650,100 @@ class Router {
815
650
  async go(href) {
816
651
  const requestID = getRandomID();
817
652
  this._requestID = requestID;
818
- const routeInfo = parseUrl(href, this._basepath);
819
- if (routeInfo.href === this._routeInfo?.href) return;
653
+ const context = parseUrl(href, this._basepath);
654
+ if (context.href === this._context?.href) return;
655
+ const progressCallback = (value) => {
656
+ if (this._requestID !== requestID) return;
657
+ const progress = Math.max(0, Math.min(100, Math.round(value)));
658
+ window.dispatchEvent(new RouteProgressEvent(context, progress));
659
+ };
660
+ context.progress = progressCallback;
661
+ if (context.href !== window.location.href) {
662
+ window.history.pushState({ basepath: context.basepath }, "", context.href);
663
+ } else {
664
+ window.history.replaceState({ basepath: context.basepath }, "", context.href);
665
+ }
666
+ let outlet = void 0;
820
667
  try {
821
668
  if (this._requestID !== requestID) return;
822
- window.dispatchEvent(new RouteBeginEvent(routeInfo));
823
- const routes = getRoutes(routeInfo.pathname, this._routes);
669
+ window.dispatchEvent(new RouteBeginEvent(context));
670
+ const routes = getRoutes(context.pathname, this._routes);
824
671
  const lastRoute = routes[routes.length - 1];
825
- if (lastRoute && "path" in lastRoute && lastRoute.path instanceof URLPattern) {
826
- routeInfo.params = lastRoute.path.exec({ pathname: routeInfo.pathname })?.pathname.groups || {};
672
+ if (lastRoute && lastRoute.path instanceof URLPattern) {
673
+ context.params = lastRoute.path.exec({ pathname: context.pathname })?.pathname.groups || {};
827
674
  }
828
- this._routeInfo = routeInfo;
829
- window.route = routeInfo;
830
- if (this._requestID !== requestID) return;
675
+ this._context = context;
676
+ outlet = findOutletOrThrow(this._rootElement);
677
+ let title = void 0;
678
+ let content = null;
679
+ let element = null;
831
680
  if (routes.length === 0) {
832
- throw new NotFoundRouteError(routeInfo.href);
681
+ throw new NotFoundError(context.href);
833
682
  }
834
- let outlet = findOutletOrThrow(this._rootElement);
835
- let title = void 0;
836
683
  for (const route of routes) {
837
684
  if (this._requestID !== requestID) return;
838
685
  if (!route.render) continue;
839
- const content = await route.render(routeInfo);
840
- const element = await outlet.renderContent({ id: route.id, content, force: route.force });
686
+ try {
687
+ content = await route.render(context);
688
+ if (content === false || content === null) {
689
+ throw new Error("Failed to load content for the route.");
690
+ }
691
+ } catch (LoadError) {
692
+ throw new ContentLoadError(LoadError);
693
+ }
694
+ if (this._requestID !== requestID) return;
695
+ try {
696
+ element = await outlet.renderContent({ id: route.id, content, force: route.force });
697
+ } catch (renderError) {
698
+ throw new ContentRenderError(renderError);
699
+ }
841
700
  outlet = findOutlet(element) || outlet;
842
701
  title = route.title || title;
843
702
  }
844
703
  document.title = title || document.title;
845
- if (this._requestID !== requestID) return;
846
- if (routeInfo.href !== window.location.href) {
847
- window.history.pushState({ basepath: routeInfo.basepath }, "", routeInfo.href);
848
- } else {
849
- window.history.replaceState({ basepath: routeInfo.basepath }, "", routeInfo.href);
850
- }
851
- window.dispatchEvent(new RouteDoneEvent(routeInfo));
704
+ window.dispatchEvent(new RouteDoneEvent(context));
852
705
  } catch (error) {
853
- const routeError = new RouteError(
706
+ const routeError = error instanceof RouteError ? error : new RouteError(
854
707
  error.status || error.code || "UNKNOWN_ERROR",
855
708
  error.message || "An unexpected error occurred",
856
709
  error
857
710
  );
858
- window.dispatchEvent(new RouteErrorEvent(routeError, routeInfo));
859
- console.error("Routing error:", error);
711
+ window.dispatchEvent(new RouteErrorEvent(context, routeError));
712
+ console.error("Routing error:", routeError.original);
860
713
  try {
861
- const errorEl = new ErrorPage();
862
- errorEl.error = routeError;
863
- document.body.innerHTML = "";
864
- document.body.appendChild(errorEl);
714
+ if (this._fallback && this._fallback.render && outlet) {
715
+ const fallbackContent = await this._fallback.render({ ...context, error: routeError });
716
+ outlet.renderContent({ id: "#fallback", content: fallbackContent, force: true });
717
+ document.title = this._fallback.title || document.title;
718
+ } else {
719
+ const errorContent = new ErrorPage();
720
+ errorContent.error = error;
721
+ if (outlet) {
722
+ outlet.renderContent({ id: "#error", content: errorContent, force: true });
723
+ } else {
724
+ document.body.innerHTML = "";
725
+ document.body.appendChild(errorContent);
726
+ }
727
+ }
865
728
  } catch (pageError) {
866
729
  console.error("Failed to render error component:", pageError);
867
- console.error("Original error:", error);
730
+ console.error("Original error:", routeError.original || routeError);
868
731
  }
869
732
  }
870
733
  }
871
734
  }
872
735
  export {
736
+ ContentLoadError,
737
+ ContentRenderError,
873
738
  Link,
874
- NotFoundRouteError,
739
+ NotFoundError,
875
740
  Outlet,
741
+ OutletMissingError,
876
742
  RouteBeginEvent,
877
743
  RouteDoneEvent,
878
744
  RouteError,
879
745
  RouteErrorEvent,
746
+ RouteProgressEvent,
880
747
  Router,
881
748
  ULink,
882
749
  UOutlet
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iyulab/router",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "A modern client-side router for web applications with support for Lit and React components",
5
5
  "keywords": [
6
6
  "lit",
@@ -42,9 +42,8 @@
42
42
  "devDependencies": {
43
43
  "@lit/react": "^1.0.8",
44
44
  "@types/node": "^24.10.1",
45
- "@types/react": "^19.2.3",
45
+ "@types/react": "^19.2.5",
46
46
  "@types/react-dom": "^19.2.3",
47
- "tslib": "^2.8.1",
48
47
  "typescript": "^5.9.3",
49
48
  "vite": "^7.2.2",
50
49
  "vite-plugin-dts": "^4.5.4"