@iyulab/router 0.5.1 → 0.5.3

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 (5) hide show
  1. package/LICENSE +20 -20
  2. package/README.md +162 -162
  3. package/dist/index.d.ts +177 -119
  4. package/dist/index.js +161 -102
  5. package/package.json +50 -50
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 iyulab
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1
+ MIT License
2
+
3
+ Copyright (c) 2025 iyulab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
package/README.md CHANGED
@@ -1,163 +1,163 @@
1
- # @iyulab/router
2
-
3
- A modern, lightweight client-side router for web applications with support for both Lit and React components.
4
-
5
- ## Features
6
-
7
- - 🚀 **Modern URLPattern-based routing** - Uses native URLPattern API for powerful path matching
8
- - 🔧 **Unified Framework Support** - Works with both Lit and React components using render functions
9
- - 📱 **Client-side Navigation** - History API integration with browser back/forward support
10
- - 🎯 **Nested Routing** - Support for deeply nested route hierarchies with index and path routes
11
- - 🔗 **Smart Link Component** - Automatic external link detection and handling
12
- - 📊 **Route Events** - Track navigation progress with route-begin, route-done, and route-error events
13
- - 🎨 **Flexible Outlet System** - Unified rendering with renderContent method
14
- - 🌐 **Global Route Access** - Access current route information anywhere via `window.route`
15
- - 🔄 **Force Re-rendering** - Control component re-rendering on route changes
16
- - ⚠️ **Enhanced Error Handling** - Built-in ErrorPage component with improved styling
17
-
18
- ## Installation
19
-
20
- ```bash
21
- npm install @iyulab/router
22
- ```
23
-
24
- ## Quick Start
25
-
26
- ### Basic Setup
27
-
28
- ```typescript
29
- import { Router } from '@iyulab/router';
30
- import { html } from 'lit';
31
-
32
- const router = new Router({
33
- root: document.getElementById('app')!,
34
- basepath: '/app',
35
- routes: [
36
- {
37
- index: true,
38
- render: () => html`<home-page></home-page>`
39
- },
40
- {
41
- path: '/user/:id',
42
- render: (routeInfo) => html`<user-page .userId=${routeInfo.params.id}></user-page>`
43
- }
44
- ]
45
- });
46
-
47
- // Start routing
48
- router.go(window.location.href);
49
- ```
50
-
51
- ### Nested Routes
52
-
53
- ```typescript
54
- const routes = [
55
- {
56
- path: '/dashboard',
57
- render: () => html`<dashboard-layout><u-outlet></u-outlet></dashboard-layout>`,
58
- children: [
59
- {
60
- index: true, // Matches /dashboard exactly
61
- render: () => html`<dashboard-home></dashboard-home>`
62
- },
63
- {
64
- path: 'settings',
65
- render: () => html`<dashboard-settings></dashboard-settings>`
66
- }
67
- ]
68
- }
69
- ];
70
- ```
71
-
72
- ### Mixed Framework Support
73
-
74
- ```typescript
75
- import React from 'react';
76
-
77
- const routes = [
78
- // Lit component
79
- {
80
- path: '/lit-page',
81
- render: (routeInfo) => html`<my-lit-component .routeInfo=${routeInfo}></my-lit-component>`
82
- },
83
- // React component
84
- {
85
- path: '/react-page',
86
- render: (routeInfo) => React.createElement(MyReactComponent, { routeInfo })
87
- },
88
- // HTML element
89
- {
90
- path: '/element-page',
91
- render: (routeInfo) => {
92
- const element = document.createElement('my-element');
93
- element.data = routeInfo.params;
94
- return element;
95
- }
96
- }
97
- ];
98
- ```
99
-
100
- ## Usage Examples
101
-
102
- ### Using with Lit Components
103
-
104
- ```typescript
105
- import { LitElement, html } from 'lit';
106
- import { customElement } from 'lit/decorators.js';
107
-
108
- @customElement('app-root')
109
- export class AppRoot extends LitElement {
110
- render() {
111
- return html`
112
- <nav>
113
- <u-link href="/">Home</u-link>
114
- <u-link href="/about">About</u-link>
115
- <u-link href="/user/123">User Profile</u-link>
116
- </nav>
117
- <main>
118
- <u-outlet></u-outlet>
119
- </main>
120
- `;
121
- }
122
- }
123
- ```
124
-
125
- ### Using with React Components
126
-
127
- ```tsx
128
- import React from 'react';
129
- import { Outlet, Link } from '@iyulab/router';
130
-
131
- export function AppRoot() {
132
- return (
133
- <div>
134
- <nav>
135
- <Link href="/">Home</Link>
136
- <Link href="/about">About</Link>
137
- <Link href="/user/123">User Profile</Link>
138
- </nav>
139
- <main>
140
- <Outlet />
141
- </main>
142
- </div>
143
- );
144
- }
145
- ```
146
-
147
- ## Browser Support
148
-
149
- - **URLPattern API**: Required for routing functionality
150
- - **Modern browsers**: Chrome 95+, Firefox 106+, Safari 16.4+
151
- - **Polyfill**: Consider using [urlpattern-polyfill](https://www.npmjs.com/package/urlpattern-polyfill) for older browsers
152
-
153
- ## Contributing
154
-
155
- 1. Fork the repository
156
- 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
157
- 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
158
- 4. Push to the branch (`git push origin feature/amazing-feature`)
159
- 5. Open a Pull Request
160
-
161
- ## License
162
-
1
+ # @iyulab/router
2
+
3
+ A modern, lightweight client-side router for web applications with support for both Lit and React components.
4
+
5
+ ## Features
6
+
7
+ - 🚀 **Modern URLPattern-based routing** - Uses native URLPattern API for powerful path matching
8
+ - 🔧 **Unified Framework Support** - Works with both Lit and React components using render functions
9
+ - 📱 **Client-side Navigation** - History API integration with browser back/forward support
10
+ - 🎯 **Nested Routing** - Support for deeply nested route hierarchies with index and path routes
11
+ - 🔗 **Smart Link Component** - Automatic external link detection and handling
12
+ - 📊 **Route Events** - Track navigation progress with route-begin, route-done, and route-error events
13
+ - 🎨 **Flexible Outlet System** - Unified rendering with renderContent method
14
+ - 🌐 **Global Route Access** - Access current route information anywhere via `window.route`
15
+ - 🔄 **Force Re-rendering** - Control component re-rendering on route changes
16
+ - ⚠️ **Enhanced Error Handling** - Built-in ErrorPage component with improved styling
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install @iyulab/router
22
+ ```
23
+
24
+ ## Quick Start
25
+
26
+ ### Basic Setup
27
+
28
+ ```typescript
29
+ import { Router } from '@iyulab/router';
30
+ import { html } from 'lit';
31
+
32
+ const router = new Router({
33
+ root: document.getElementById('app')!,
34
+ basepath: '/app',
35
+ routes: [
36
+ {
37
+ index: true,
38
+ render: () => html`<home-page></home-page>`
39
+ },
40
+ {
41
+ path: '/user/:id',
42
+ render: (routeInfo) => html`<user-page .userId=${routeInfo.params.id}></user-page>`
43
+ }
44
+ ]
45
+ });
46
+
47
+ // Start routing
48
+ router.go(window.location.href);
49
+ ```
50
+
51
+ ### Nested Routes
52
+
53
+ ```typescript
54
+ const routes = [
55
+ {
56
+ path: '/dashboard',
57
+ render: () => html`<dashboard-layout><u-outlet></u-outlet></dashboard-layout>`,
58
+ children: [
59
+ {
60
+ index: true, // Matches /dashboard exactly
61
+ render: () => html`<dashboard-home></dashboard-home>`
62
+ },
63
+ {
64
+ path: 'settings',
65
+ render: () => html`<dashboard-settings></dashboard-settings>`
66
+ }
67
+ ]
68
+ }
69
+ ];
70
+ ```
71
+
72
+ ### Mixed Framework Support
73
+
74
+ ```typescript
75
+ import React from 'react';
76
+
77
+ const routes = [
78
+ // Lit component
79
+ {
80
+ path: '/lit-page',
81
+ render: (routeInfo) => html`<my-lit-component .routeInfo=${routeInfo}></my-lit-component>`
82
+ },
83
+ // React component
84
+ {
85
+ path: '/react-page',
86
+ render: (routeInfo) => React.createElement(MyReactComponent, { routeInfo })
87
+ },
88
+ // HTML element
89
+ {
90
+ path: '/element-page',
91
+ render: (routeInfo) => {
92
+ const element = document.createElement('my-element');
93
+ element.data = routeInfo.params;
94
+ return element;
95
+ }
96
+ }
97
+ ];
98
+ ```
99
+
100
+ ## Usage Examples
101
+
102
+ ### Using with Lit Components
103
+
104
+ ```typescript
105
+ import { LitElement, html } from 'lit';
106
+ import { customElement } from 'lit/decorators.js';
107
+
108
+ @customElement('app-root')
109
+ export class AppRoot extends LitElement {
110
+ render() {
111
+ return html`
112
+ <nav>
113
+ <u-link href="/">Home</u-link>
114
+ <u-link href="/about">About</u-link>
115
+ <u-link href="/user/123">User Profile</u-link>
116
+ </nav>
117
+ <main>
118
+ <u-outlet></u-outlet>
119
+ </main>
120
+ `;
121
+ }
122
+ }
123
+ ```
124
+
125
+ ### Using with React Components
126
+
127
+ ```tsx
128
+ import React from 'react';
129
+ import { Outlet, Link } from '@iyulab/router';
130
+
131
+ export function AppRoot() {
132
+ return (
133
+ <div>
134
+ <nav>
135
+ <Link href="/">Home</Link>
136
+ <Link href="/about">About</Link>
137
+ <Link href="/user/123">User Profile</Link>
138
+ </nav>
139
+ <main>
140
+ <Outlet />
141
+ </main>
142
+ </div>
143
+ );
144
+ }
145
+ ```
146
+
147
+ ## Browser Support
148
+
149
+ - **URLPattern API**: Required for routing functionality
150
+ - **Modern browsers**: Chrome 95+, Firefox 106+, Safari 16.4+
151
+ - **Polyfill**: Consider using [urlpattern-polyfill](https://www.npmjs.com/package/urlpattern-polyfill) for older browsers
152
+
153
+ ## Contributing
154
+
155
+ 1. Fork the repository
156
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
157
+ 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
158
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
159
+ 5. Open a Pull Request
160
+
161
+ ## License
162
+
163
163
  MIT License - see [LICENSE](LICENSE) file for details.
package/dist/index.d.ts CHANGED
@@ -18,28 +18,48 @@ 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
+ * @link
30
+ * https://developer.mozilla.org/en-US/docs/Web/API/URLPattern
31
+ */
32
+ path?: string | URLPattern;
33
+ /**
34
+ * 라우트 정보를 받아 렌더링 결과를 반환합니다.
35
+ * @param ctx 현재 라우팅 정보 및, 진행 상태 콜백을 포함하는 Context 객체가 인자로 전달됩니다.
24
36
  * @example
25
37
  * ```typescript
26
38
  * const route = {
27
- * path: '/user',
28
- * render: (info) => html`<user-page .routeInfo=${info}></user-page>`,
39
+ * path: '/user:id',
40
+ * render: async (ctx) => {
41
+ * // 사용자 정보를 비동기로 가져오는 예시
42
+ * const userId = ctx.params.id;
43
+ * ctx.progress(30);
44
+ * const userData = await fetchUserData(userId);
45
+ * ctx.progress(70);
46
+ * return html`<user-profile .data=${userData}></user-profile>`;
47
+ * }
29
48
  * }
30
49
  * ```
31
50
  */
32
- render?: (info: RouteInfo) => Promise<RenderResult> | RenderResult;
33
- /**
34
- * 중첩 라우트
35
- */
36
- children?: RouteConfig[];
51
+ render?: (ctx: RouteContext) => Promise<RenderResult> | RenderResult;
37
52
  /**
38
53
  * 라우터 URL 변경시 렌더링을 강제할지 여부
39
54
  * - 기본값으로 children을 가질때 false로 설정되며, children이 없을 경우 true로 설정됩니다.
40
55
  * - true로 설정하면 기존 렌더링을 무시하고 새로 렌더링합니다.
41
56
  */
42
57
  force?: boolean;
58
+ /**
59
+ * 경로 매칭시 대소문자 구분 여부
60
+ * @default false
61
+ */
62
+ ignoreCase?: boolean;
43
63
  }
44
64
 
45
65
  /**
@@ -56,20 +76,47 @@ export declare class ContentRenderError extends RouteError {
56
76
  constructor(original?: Error | any);
57
77
  }
58
78
 
59
- /**
60
- * 인덱스 라우트 타입
61
- */
62
- declare interface IndexRouteConfig extends BaseRouteConfig {
79
+ export declare type FallbackRenderResult = HTMLElement | ReactElement | TemplateResult<1>;
80
+
81
+ export declare interface FallbackRouteConfig {
63
82
  /**
64
- * true일 경우 인덱스 라우트입니다.
65
- * path는 강제로 빈 문자열로 설정됩니다.
83
+ * 브라우저의 타이틀이 설정에 따라 변경됩니다.
66
84
  */
67
- index: true;
85
+ title?: string;
68
86
  /**
69
- * 인덱스 라우트의 URLPattern (자동 설정됨)
70
- * 부모 라우트의 basepath를 상속받습니다.
87
+ * 라우팅 실패 표시할 렌더링 결과를 반환합니다.
88
+ * - 오류가 발생할 경우 또는 렌더링 결과가 false일 경우 호출됩니다.
89
+ * @param ctx 현재 라우팅 정보 및 오류 정보를 포함하는 Context 객체가 인자로 전달됩니다.
90
+ * @example
91
+ * ```typescript
92
+ * const fallbackRoute = {
93
+ * title: 'Not Found',
94
+ * render: (ctx) => {
95
+ * if (ctx.error) {
96
+ * return html`<error-page .error=${ctx.error}></error-page>`;
97
+ * }
98
+ * return html`<not-found-page></not-found-page>`;
99
+ * }
100
+ * }
101
+ * ```
71
102
  */
72
- path?: URLPattern;
103
+ render?: (ctx: FallbackRouteContext) => Promise<FallbackRenderResult> | FallbackRenderResult;
104
+ }
105
+
106
+ export declare interface FallbackRouteContext extends RouteContext {
107
+ /**
108
+ * 라우팅 에러 정보
109
+ * - 라우팅 중 발생한 에러 정보를 포함합니다.
110
+ */
111
+ error: RouteError;
112
+ }
113
+
114
+ declare interface IndexRouteConfig extends BaseRouteConfig {
115
+ /**
116
+ * 현재 경로의 인덱스 라우트임을 나타냅니다.
117
+ * - 인덱스 라우트는 부모 경로와 동일한 경로를 가지며, path는 자동으로 설정됩니다.
118
+ */
119
+ index: true;
73
120
  }
74
121
 
75
122
  /**
@@ -113,6 +160,18 @@ export declare class Link extends LitElement {
113
160
  static styles: CSSResult;
114
161
  }
115
162
 
163
+ declare interface NonIndexRouteConfig extends BaseRouteConfig {
164
+ /**
165
+ * 인덱스 라우트가 아님을 나타냅니다.
166
+ */
167
+ index?: false;
168
+ /**
169
+ * 하위 라우트 설정, 재귀적으로 RouteConfig 배열을 가질 수 있습니다.
170
+ * - 하위 라우트가 있는 경우, 부모 라우트의 경로를 기준으로 매칭됩니다.
171
+ */
172
+ children?: RouteConfig[];
173
+ }
174
+
116
175
  /**
117
176
  * 페이지를 찾을 수 없을 때 발생하는 에러
118
177
  */
@@ -149,101 +208,27 @@ export declare class OutletMissingError extends RouteError {
149
208
  constructor();
150
209
  }
151
210
 
152
- /**
153
- * 경로 라우트 타입
154
- */
155
- declare interface PathRouteConfig extends BaseRouteConfig {
156
- /**
157
- * 라우터 경로는 string 또는 URLPattern을 사용할 수 있습니다.
158
- * string일 경우 자동으로 URLPattern으로 변환됩니다.
159
- * @example
160
- * - "/user/:id/:name"
161
- * - "/user/:id/:name?"
162
- * - "/user/:id/:name*"
163
- * - "/user/:id/:name+"
164
- * - "/user/:id/:name{1,3}"
165
- * @link
166
- * https://developer.mozilla.org/en-US/docs/Web/API/URLPattern
167
- */
168
- path: string | URLPattern;
169
- }
170
-
171
211
  declare interface RenderOption {
172
212
  id?: string;
173
213
  force?: boolean;
174
214
  content: RenderResult;
175
215
  }
176
216
 
177
- declare type RenderResult = HTMLElement | ReactElement | TemplateResult<1>;
217
+ export declare type RenderResult = HTMLElement | ReactElement | TemplateResult<1> | false;
178
218
 
179
219
  /**
180
220
  * 라우트 시작 이벤트
181
221
  */
182
222
  export declare class RouteBeginEvent extends RouteEvent {
183
- constructor(routeInfo: RouteInfo);
223
+ constructor(context: RouteContext);
184
224
  }
185
225
 
186
- /**
187
- * 라우트 타입 (인덱스 라우트 또는 경로 라우트)
188
- */
189
- export declare type RouteConfig = IndexRouteConfig | PathRouteConfig;
190
-
191
- /**
192
- * 라우트 완료 이벤트
193
- */
194
- export declare class RouteDoneEvent extends RouteEvent {
195
- constructor(routeInfo: RouteInfo);
196
- }
197
-
198
- /**
199
- * 라우팅 에러 정보
200
- */
201
- export declare class RouteError extends Error {
202
- /**
203
- * 에러 코드
204
- * - HTTP 상태 코드 또는 커스텀 에러 코드
205
- * @example 404, 500, 'ROUTE_NOT_FOUND'
206
- */
207
- code: number | string;
208
- /**
209
- * 원본 에러 객체
210
- * - 원본 Error 객체 또는 예외 정보
211
- */
212
- original?: Error | any;
213
- /**
214
- * 에러 발생 시간
215
- * - 에러가 발생한 시간 (ISO 8601 형식)
216
- */
217
- timestamp: string;
218
- constructor(code: number | string, message: string, original?: Error | any);
219
- }
220
-
221
- /**
222
- * 라우트 에러 이벤트
223
- */
224
- export declare class RouteErrorEvent extends RouteEvent {
225
- /** 에러 정보 */
226
- readonly error: RouteError;
227
- constructor(error: RouteError, routeInfo: RouteInfo);
228
- }
229
-
230
- /** 라우터 이벤트 기본 클래스 */
231
- declare abstract class RouteEvent extends Event {
232
- /** 라우팅 정보 */
233
- readonly routeInfo: RouteInfo;
234
- /** 이벤트 발생 시간 */
235
- readonly timestamp: string;
236
- constructor(type: string, routeInfo: RouteInfo, cancelable?: boolean);
237
- /** 이벤트가 취소되었는지 확인 */
238
- get cancelled(): boolean;
239
- /** 이벤트 취소 */
240
- cancel(): void;
241
- }
226
+ export declare type RouteConfig = IndexRouteConfig | NonIndexRouteConfig;
242
227
 
243
228
  /**
244
229
  * 라우터 정보
245
230
  */
246
- export declare interface RouteInfo {
231
+ export declare interface RouteContext {
247
232
  /**
248
233
  * 전체 URL 정보
249
234
  * - 도메인 이름을 포함한 URL의 전체 경로입니다.
@@ -305,6 +290,73 @@ export declare interface RouteInfo {
305
290
  * @example #profile
306
291
  */
307
292
  hash?: string;
293
+ /**
294
+ * 현재 라우팅의 진행 상태를 업데이트하여, window 객체의 'route-progress' 이벤트를 트리거합니다.
295
+ * 여러 라우팅이 호출되는 경우, 가장 최근의 라우팅의 진행 상태만 반영되며, 나머지는 무시됩니다.
296
+ * @param value 진행 상태 값 (0~100)
297
+ */
298
+ progress: (value: number) => void;
299
+ }
300
+
301
+ /**
302
+ * 라우트 완료 이벤트
303
+ */
304
+ export declare class RouteDoneEvent extends RouteEvent {
305
+ constructor(context: RouteContext);
306
+ }
307
+
308
+ /**
309
+ * 라우팅 에러 정보
310
+ */
311
+ export declare class RouteError extends Error {
312
+ /**
313
+ * 에러 코드
314
+ * - HTTP 상태 코드 또는 커스텀 에러 코드
315
+ * @example 404, 500, 'ROUTE_NOT_FOUND'
316
+ */
317
+ code: number | string;
318
+ /**
319
+ * 원본 에러 객체
320
+ * - 원본 Error 객체 또는 예외 정보
321
+ */
322
+ original?: Error | any;
323
+ /**
324
+ * 에러 발생 시간
325
+ * - 에러가 발생한 시간 (ISO 8601 형식)
326
+ */
327
+ timestamp: string;
328
+ constructor(code: number | string, message: string, original?: Error | any);
329
+ }
330
+
331
+ /**
332
+ * 라우트 에러 이벤트
333
+ */
334
+ export declare class RouteErrorEvent extends RouteEvent {
335
+ /** 에러 정보 */
336
+ readonly error: RouteError;
337
+ constructor(context: RouteContext, error: RouteError);
338
+ }
339
+
340
+ /** 라우터 이벤트 기본 클래스 */
341
+ declare abstract class RouteEvent extends Event {
342
+ /** 라우팅 정보 */
343
+ readonly context: RouteContext;
344
+ /** 이벤트 발생 시간 */
345
+ readonly timestamp: string;
346
+ constructor(type: string, context: RouteContext, cancelable?: boolean);
347
+ /** 이벤트가 취소되었는지 확인 */
348
+ get cancelled(): boolean;
349
+ /** 이벤트 취소 */
350
+ cancel(): void;
351
+ }
352
+
353
+ /**
354
+ * 라우트 진행 이벤트
355
+ */
356
+ export declare class RouteProgressEvent extends RouteEvent {
357
+ /** 진행 상태 값 (0~100) */
358
+ readonly progress: number;
359
+ constructor(context: RouteContext, progress: number);
308
360
  }
309
361
 
310
362
  /**
@@ -314,21 +366,20 @@ export declare class Router {
314
366
  private readonly _rootElement;
315
367
  private readonly _basepath;
316
368
  private readonly _routes;
369
+ private readonly _fallback?;
317
370
  /** 현재 라우팅 요청 ID */
318
371
  private _requestID?;
319
372
  /** 현재 라우팅 정보 */
320
- private _routeInfo?;
373
+ private _context?;
321
374
  constructor(config: RouterConfig);
322
- /** 초기 라우팅 처리, TODO: 제거 */
323
- private waitConnected;
324
375
  /** 객체를 정리하고 이벤트 리스너를 제거합니다. */
325
376
  destroy(): void;
326
377
  /** 라우터의 기본 경로 반환 */
327
378
  get basepath(): string;
328
- /** 등록된 라우트 반환 */
379
+ /** 등록된 라우트 정보 반환 */
329
380
  get routes(): RouteConfig[];
330
381
  /** 현재 라우팅 정보 반환 */
331
- get routeInfo(): RouteInfo | undefined;
382
+ get context(): RouteContext | undefined;
332
383
  /**
333
384
  * 지정한 경로의 클라이언트 라우팅을 수행합니다. 상대경로일 경우 basepath와 조합되어 이동합니다.
334
385
  * @param href 이동할 경로
@@ -360,12 +411,22 @@ export declare interface RouterConfig {
360
411
  * - 라우트는 URLPattern을 사용하여 경로를 탐색합니다.
361
412
  * - 라우트는 렌더링할 엘리먼트 또는 컴포넌트를 지정합니다.
362
413
  */
363
- routes: RouteConfig[];
414
+ routes?: RouteConfig[];
415
+ /**
416
+ * 라우트 매칭 실패 또는 오류 발생 시 대체 라우트 설정
417
+ * - 지정된 설정이 없을 경우, 기본 오류 페이지가 렌더링됩니다.
418
+ */
419
+ fallback?: FallbackRouteConfig;
364
420
  /**
365
421
  * `a` 태그 클릭 시 클라이언트 라우팅을 수행할지 여부를 설정합니다.
366
422
  * @default true
367
423
  */
368
424
  useIntercept?: boolean;
425
+ /**
426
+ * 초기 로드 시 현재 URL로 라우팅을 자동으로 수행할지 여부를 설정합니다.
427
+ * @default true
428
+ */
429
+ initialLoad?: boolean;
369
430
  }
370
431
 
371
432
  export declare const ULink: ReactWebComponent<Link, {}>;
@@ -374,19 +435,16 @@ export declare const UOutlet: ReactWebComponent<Outlet, {}>;
374
435
 
375
436
  export { }
376
437
 
377
- declare global {
378
- interface Window {
379
- route: RouteInfo;
380
- }
381
-
382
- interface WindowEventMap {
383
- 'route-begin': RouteBeginEvent;
384
- 'route-done': RouteDoneEvent;
385
- 'route-error': RouteErrorEvent;
386
- }
387
-
388
- interface HTMLElementTagNameMap {
389
- 'u-link': Link;
390
- 'u-outlet': Outlet;
391
- }
438
+ declare global {
439
+ interface WindowEventMap {
440
+ 'route-begin': RouteBeginEvent;
441
+ 'route-progress': RouteProgressEvent;
442
+ 'route-done': RouteDoneEvent;
443
+ 'route-error': RouteErrorEvent;
444
+ }
445
+
446
+ interface HTMLElementTagNameMap {
447
+ 'u-link': Link;
448
+ 'u-outlet': Outlet;
449
+ }
392
450
  }
package/dist/index.js CHANGED
@@ -2,7 +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 { unsafeSVG } from "lit/directives/unsafe-svg.js";
5
+ import { unsafeHTML } from "lit-html/directives/unsafe-html.js";
6
6
  const e = /* @__PURE__ */ new Set(["children", "localName", "ref", "style", "className"]), n = /* @__PURE__ */ new WeakMap(), t = (e2, t2, o2, l, a) => {
7
7
  const s = a?.[t2];
8
8
  void 0 === s ? (e2[t2] = o2, null == o2 && t2 in HTMLElement.prototype && e2.removeAttribute(t2)) : o2 !== l && ((e3, t3, o3) => {
@@ -65,7 +65,9 @@ function parseUrl(url, basepath) {
65
65
  pathname: urlObj.pathname,
66
66
  query: new URLSearchParams(urlObj.search),
67
67
  hash: urlObj.hash,
68
- params: {}
68
+ params: {},
69
+ progress: () => {
70
+ }
69
71
  };
70
72
  }
71
73
  function absolutePath(...paths) {
@@ -214,7 +216,10 @@ class Outlet extends LitElement {
214
216
  this.routeId = id;
215
217
  this.clear();
216
218
  if (!this.container) {
217
- throw new Error("DOM이 초기화되지 않았습니다.");
219
+ throw new Error("Outlet container is not initialized.");
220
+ }
221
+ if (typeof content !== "object") {
222
+ throw new Error("Content is not a valid renderable object.");
218
223
  }
219
224
  if (content instanceof HTMLElement) {
220
225
  this.container.appendChild(content);
@@ -292,9 +297,9 @@ class ContentRenderError extends RouteError {
292
297
  }
293
298
  }
294
299
  class RouteEvent extends Event {
295
- constructor(type, routeInfo, cancelable = false) {
300
+ constructor(type, context, cancelable = false) {
296
301
  super(type, { bubbles: true, composed: true, cancelable });
297
- this.routeInfo = routeInfo;
302
+ this.context = context;
298
303
  this.timestamp = (/* @__PURE__ */ new Date()).toISOString();
299
304
  }
300
305
  /** 이벤트가 취소되었는지 확인 */
@@ -309,30 +314,72 @@ class RouteEvent extends Event {
309
314
  }
310
315
  }
311
316
  class RouteBeginEvent extends RouteEvent {
312
- constructor(routeInfo) {
313
- 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;
314
325
  }
315
326
  }
316
327
  class RouteDoneEvent extends RouteEvent {
317
- constructor(routeInfo) {
318
- super("route-done", routeInfo, false);
328
+ constructor(context) {
329
+ super("route-done", context, false);
319
330
  }
320
331
  }
321
332
  class RouteErrorEvent extends RouteEvent {
322
- constructor(error, routeInfo) {
323
- super("route-error", routeInfo, false);
333
+ constructor(context, error) {
334
+ super("route-error", context, false);
324
335
  this.error = error;
325
336
  }
326
337
  }
327
- const __vite_glob_0_0 = '<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>';
328
- const __vite_glob_0_1 = '<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>';
329
- const __vite_glob_0_2 = '<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>';
330
- const __vite_glob_0_3 = '<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>';
331
- const __vite_glob_0_4 = '<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>';
332
- const __vite_glob_0_5 = '<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>';
333
- const __vite_glob_0_6 = '<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>';
334
- const __vite_glob_0_7 = '<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>';
335
- const __vite_glob_0_8 = '<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>';
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" }));
336
383
  const styles = css`
337
384
  :host {
338
385
  display: flex;
@@ -392,9 +439,19 @@ var __decorateClass = (decorators, target, key, kind) => {
392
439
  if (kind && result) __defProp(target, key, result);
393
440
  return result;
394
441
  };
395
- const icons = Object.entries(/* @__PURE__ */ Object.assign({ "../assets/ban.svg": __vite_glob_0_0, "../assets/box-seam.svg": __vite_glob_0_1, "../assets/exclamation-triangle.svg": __vite_glob_0_2, "../assets/palette.svg": __vite_glob_0_3, "../assets/person-lock.svg": __vite_glob_0_4, "../assets/search.svg": __vite_glob_0_5, "../assets/stopwatch.svg": __vite_glob_0_6, "../assets/wifi-off.svg": __vite_glob_0_7, "../assets/wrench-adjustable.svg": __vite_glob_0_8 })).reduce((acc, [path, content]) => {
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]) => {
396
453
  const name = path.split("/").pop()?.replace(".svg", "") || "";
397
- acc[name] = content;
454
+ acc[name] = content.default;
398
455
  return acc;
399
456
  }, {});
400
457
  let ErrorPage = class extends LitElement {
@@ -402,7 +459,7 @@ let ErrorPage = class extends LitElement {
402
459
  const error = this.error || this.getDefaultError();
403
460
  const icon = this.getErrorIcon(error.code);
404
461
  return html`
405
- <div class="icon">${icon}</div>
462
+ <div class="icon">${unsafeHTML(icon)}</div>
406
463
  <div class="code">${error.code}</div>
407
464
  <div class="message">${error.message}</div>
408
465
  `;
@@ -417,25 +474,25 @@ let ErrorPage = class extends LitElement {
417
474
  const numericCode = typeof code === "string" ? parseInt(code) : code;
418
475
  switch (codeStr) {
419
476
  case "OUTLET_NOT_FOUND":
420
- return unsafeSVG(icons["box-seam"] || "📦");
477
+ return icons["box-seam"] || "📦";
421
478
  case "CONTENT_LOAD_FAILED":
422
- return unsafeSVG(icons["wifi-off"] || "📡");
479
+ return icons["wifi-off"] || "📡";
423
480
  case "RENDER_FAILED":
424
- return unsafeSVG(icons["palette"] || "🎨");
481
+ return icons["palette"] || "🎨";
425
482
  }
426
483
  switch (numericCode) {
427
484
  case 404:
428
- return unsafeSVG(icons["search"] || "🔍");
485
+ return icons["search"] || "🔍";
429
486
  case 403:
430
- return unsafeSVG(icons["ban"] || "🚫");
487
+ return icons["ban"] || "🚫";
431
488
  case 401:
432
- return unsafeSVG(icons["person-lock"] || "🔐");
489
+ return icons["person-lock"] || "🔐";
433
490
  case 429:
434
- return unsafeSVG(icons["stopwatch"] || "⏱️");
491
+ return icons["stopwatch"] || "⏱️";
435
492
  case 503:
436
- return unsafeSVG(icons["wrench-adjustable"] || "🛠️");
493
+ return icons["wrench-adjustable"] || "🛠️";
437
494
  default:
438
- return unsafeSVG(icons["exclamation-triangle"] || "⚠️");
495
+ return icons["exclamation-triangle"] || "⚠️";
439
496
  }
440
497
  }
441
498
  };
@@ -491,51 +548,48 @@ function findAnchorFromEvent(e2) {
491
548
  function setRoutes(routes, basepath) {
492
549
  for (const route of routes) {
493
550
  route.id ||= getRandomID();
494
- if ("index" in route && route.index) {
495
- route.path = new URLPattern({ pathname: `${basepath}{/}?` });
496
- } else if ("path" in route && route.path) {
551
+ route.ignoreCase ||= false;
552
+ if (route.index === true) {
553
+ route.path = new URLPattern({ pathname: `${basepath}{/}?` }, {
554
+ ignoreCase: route.ignoreCase
555
+ });
556
+ route.force ||= true;
557
+ } else {
497
558
  if (typeof route.path === "string") {
498
559
  const absolutePathStr = absolutePath(basepath, route.path);
499
- route.path = new URLPattern({ pathname: `${absolutePathStr}{/}?` });
560
+ route.path = new URLPattern({ pathname: `${absolutePathStr}{/}?` }, {
561
+ ignoreCase: route.ignoreCase
562
+ });
563
+ } else if (route.path instanceof URLPattern) ;
564
+ else {
565
+ route.path = new URLPattern({ pathname: `${basepath}{/}?` }, {
566
+ ignoreCase: route.ignoreCase
567
+ });
500
568
  }
501
- } else {
502
- throw new Error('Route must have either "index" or "path" property defined.');
503
- }
504
- if (route.children && route.children.length > 0) {
505
- let childBasepath;
506
- if ("index" in route) {
507
- childBasepath = basepath;
569
+ if (route.children && route.children.length > 0) {
570
+ const childBasepath = route.path.pathname.replace("{/}?", "");
571
+ route.children = setRoutes(route.children, childBasepath);
572
+ route.force ||= false;
508
573
  } else {
509
- if (typeof route.path === "string") {
510
- childBasepath = absolutePath(basepath, route.path);
511
- } else {
512
- childBasepath = route.path.pathname.replace("{/}?", "");
513
- }
574
+ route.force ||= true;
514
575
  }
515
- route.children = setRoutes(route.children, childBasepath);
516
- route.force ||= false;
517
- } else {
518
- route.force ||= true;
519
576
  }
520
577
  }
521
578
  return routes;
522
579
  }
523
580
  function getRoutes(pathname, routes) {
524
581
  for (const route of routes) {
525
- if (route.children) {
582
+ if (route.index !== true && route.children && route.children.length > 0) {
526
583
  const childRoutes = getRoutes(pathname, route.children);
527
584
  if (childRoutes.length > 0) {
528
585
  return [route, ...childRoutes];
529
586
  }
530
587
  }
531
- let matches = false;
532
- if ("index" in route && route.index && route.path) {
533
- matches = route.path.test({ pathname });
534
- } else if ("path" in route && route.path instanceof URLPattern) {
535
- matches = route.path.test({ pathname });
536
- }
537
- if (matches) {
538
- return [route];
588
+ if (route.path instanceof URLPattern) {
589
+ const isMatch = route.path.test({ pathname });
590
+ if (isMatch) return [route];
591
+ } else {
592
+ throw new Error("Route path must be an instance of URLPattern, Something wrong in setRoutes function.");
539
593
  }
540
594
  }
541
595
  return [];
@@ -565,44 +619,36 @@ class Router {
565
619
  };
566
620
  this._rootElement = config.root;
567
621
  this._basepath = absolutePath(config.basepath || "/");
568
- this._routes = setRoutes(config.routes, this._basepath);
569
- this.waitConnected();
622
+ this._routes = setRoutes(config.routes || [], this._basepath);
623
+ this._fallback = config.fallback;
570
624
  window.removeEventListener("popstate", this.handleWindowPopstate);
571
625
  window.addEventListener("popstate", this.handleWindowPopstate);
572
626
  if (config.useIntercept !== false) {
573
627
  this._rootElement.removeEventListener("click", this.handleRootClick);
574
628
  this._rootElement.addEventListener("click", this.handleRootClick);
575
629
  }
576
- }
577
- /** 초기 라우팅 처리, TODO: 제거 */
578
- async waitConnected() {
579
- let outlet = findOutlet(this._rootElement);
580
- let count = 0;
581
- while (!outlet && count < 20) {
582
- await new Promise((resolve) => setTimeout(resolve, 50));
583
- outlet = findOutlet(this._rootElement);
584
- count++;
630
+ if (config.initialLoad !== false) {
631
+ void this.go(window.location.href);
585
632
  }
586
- this.handleWindowPopstate();
587
633
  }
588
634
  /** 객체를 정리하고 이벤트 리스너를 제거합니다. */
589
635
  destroy() {
590
636
  window.removeEventListener("popstate", this.handleWindowPopstate);
591
637
  this._rootElement.removeEventListener("click", this.handleRootClick);
592
- this._routeInfo = void 0;
593
638
  this._requestID = void 0;
639
+ this._context = void 0;
594
640
  }
595
641
  /** 라우터의 기본 경로 반환 */
596
642
  get basepath() {
597
643
  return this._basepath;
598
644
  }
599
- /** 등록된 라우트 반환 */
645
+ /** 등록된 라우트 정보 반환 */
600
646
  get routes() {
601
647
  return this._routes;
602
648
  }
603
649
  /** 현재 라우팅 정보 반환 */
604
- get routeInfo() {
605
- return this._routeInfo;
650
+ get context() {
651
+ return this._context;
606
652
  }
607
653
  /**
608
654
  * 지정한 경로의 클라이언트 라우팅을 수행합니다. 상대경로일 경우 basepath와 조합되어 이동합니다.
@@ -611,31 +657,43 @@ class Router {
611
657
  async go(href) {
612
658
  const requestID = getRandomID();
613
659
  this._requestID = requestID;
614
- const routeInfo = parseUrl(href, this._basepath);
615
- if (routeInfo.href === this._routeInfo?.href) return;
660
+ const context = parseUrl(href, this._basepath);
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
+ const progressCallback = (value) => {
667
+ if (this._requestID !== requestID) return;
668
+ const progress = Math.max(0, Math.min(100, Math.round(value)));
669
+ window.dispatchEvent(new RouteProgressEvent(context, progress));
670
+ };
671
+ context.progress = progressCallback;
616
672
  let outlet = void 0;
617
673
  try {
618
674
  if (this._requestID !== requestID) return;
619
- window.dispatchEvent(new RouteBeginEvent(routeInfo));
620
- const routes = getRoutes(routeInfo.pathname, this._routes);
675
+ window.dispatchEvent(new RouteBeginEvent(context));
676
+ const routes = getRoutes(context.pathname, this._routes);
621
677
  const lastRoute = routes[routes.length - 1];
622
- if (lastRoute && "path" in lastRoute && lastRoute.path instanceof URLPattern) {
623
- routeInfo.params = lastRoute.path.exec({ pathname: routeInfo.pathname })?.pathname.groups || {};
678
+ if (lastRoute && lastRoute.path instanceof URLPattern) {
679
+ context.params = lastRoute.path.exec({ pathname: context.pathname })?.pathname.groups || {};
624
680
  }
625
- this._routeInfo = routeInfo;
626
- window.route = routeInfo;
681
+ this._context = context;
627
682
  outlet = findOutletOrThrow(this._rootElement);
628
683
  let title = void 0;
629
684
  let content = null;
630
685
  let element = null;
631
686
  if (routes.length === 0) {
632
- throw new NotFoundError(routeInfo.href);
687
+ throw new NotFoundError(context.href);
633
688
  }
634
689
  for (const route of routes) {
635
690
  if (this._requestID !== requestID) return;
636
691
  if (!route.render) continue;
637
692
  try {
638
- content = await route.render(routeInfo);
693
+ content = await route.render(context);
694
+ if (content === false || content === void 0 || content === null) {
695
+ throw new Error("Failed to load content for the route.");
696
+ }
639
697
  } catch (LoadError) {
640
698
  throw new ContentLoadError(LoadError);
641
699
  }
@@ -648,29 +706,29 @@ class Router {
648
706
  title = route.title || title;
649
707
  }
650
708
  document.title = title || document.title;
651
- if (this._requestID !== requestID) return;
652
- if (routeInfo.href !== window.location.href) {
653
- window.history.pushState({ basepath: routeInfo.basepath }, "", routeInfo.href);
654
- } else {
655
- window.history.replaceState({ basepath: routeInfo.basepath }, "", routeInfo.href);
656
- }
657
- window.dispatchEvent(new RouteDoneEvent(routeInfo));
709
+ window.dispatchEvent(new RouteDoneEvent(context));
658
710
  } catch (error) {
659
711
  const routeError = error instanceof RouteError ? error : new RouteError(
660
712
  error.status || error.code || "UNKNOWN_ERROR",
661
713
  error.message || "An unexpected error occurred",
662
714
  error
663
715
  );
664
- window.dispatchEvent(new RouteErrorEvent(routeError, routeInfo));
665
- console.error("Routing error:", error.original || error);
716
+ window.dispatchEvent(new RouteErrorEvent(context, routeError));
717
+ console.error("Routing error:", routeError.original);
666
718
  try {
667
- const errorPage = new ErrorPage();
668
- errorPage.error = error;
669
- if (outlet) {
670
- outlet.renderContent({ id: "#", content: errorPage, force: true });
719
+ if (this._fallback && this._fallback.render && outlet) {
720
+ const fallbackContent = await this._fallback.render({ ...context, error: routeError });
721
+ outlet.renderContent({ id: "#fallback", content: fallbackContent, force: true });
722
+ document.title = this._fallback.title || document.title;
671
723
  } else {
672
- document.body.innerHTML = "";
673
- document.body.appendChild(errorPage);
724
+ const errorContent = new ErrorPage();
725
+ errorContent.error = error;
726
+ if (outlet) {
727
+ outlet.renderContent({ id: "#error", content: errorContent, force: true });
728
+ } else {
729
+ document.body.innerHTML = "";
730
+ document.body.appendChild(errorContent);
731
+ }
674
732
  }
675
733
  } catch (pageError) {
676
734
  console.error("Failed to render error component:", pageError);
@@ -690,6 +748,7 @@ export {
690
748
  RouteDoneEvent,
691
749
  RouteError,
692
750
  RouteErrorEvent,
751
+ RouteProgressEvent,
693
752
  Router,
694
753
  ULink,
695
754
  UOutlet
package/package.json CHANGED
@@ -1,51 +1,51 @@
1
- {
2
- "name": "@iyulab/router",
3
- "version": "0.5.1",
4
- "description": "A modern client-side router for web applications with support for Lit and React components",
5
- "keywords": [
6
- "lit",
7
- "react",
8
- "router",
9
- "routing",
10
- "spa",
11
- "navigation",
12
- "client-side"
13
- ],
14
- "license": "MIT",
15
- "author": "iyulab",
16
- "repository": {
17
- "type": "git",
18
- "url": "https://github.com/iyulab/node-router.git"
19
- },
20
- "files": [
21
- "dist",
22
- "package.json",
23
- "README.md",
24
- "LICENSE"
25
- ],
26
- "type": "module",
27
- "types": "dist/index.d.ts",
28
- "exports": {
29
- ".": {
30
- "types": "./dist/index.d.ts",
31
- "import": "./dist/index.js"
32
- }
33
- },
34
- "scripts": {
35
- "build": "vite build"
36
- },
37
- "dependencies": {
38
- "lit": "^3.3.1",
39
- "react": "^19.2.0",
40
- "react-dom": "^19.2.0"
41
- },
42
- "devDependencies": {
43
- "@lit/react": "^1.0.8",
44
- "@types/node": "^24.10.1",
45
- "@types/react": "^19.2.4",
46
- "@types/react-dom": "^19.2.3",
47
- "typescript": "^5.9.3",
48
- "vite": "^7.2.2",
49
- "vite-plugin-dts": "^4.5.4"
50
- }
1
+ {
2
+ "name": "@iyulab/router",
3
+ "version": "0.5.3",
4
+ "description": "A modern client-side router for web applications with support for Lit and React components",
5
+ "keywords": [
6
+ "lit",
7
+ "react",
8
+ "router",
9
+ "routing",
10
+ "spa",
11
+ "navigation",
12
+ "client-side"
13
+ ],
14
+ "license": "MIT",
15
+ "author": "iyulab",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/iyulab/node-router.git"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "package.json",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "type": "module",
27
+ "types": "dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js"
32
+ }
33
+ },
34
+ "scripts": {
35
+ "build": "vite build"
36
+ },
37
+ "dependencies": {
38
+ "lit": "^3.3.1",
39
+ "react": "^19.2.1",
40
+ "react-dom": "^19.2.1"
41
+ },
42
+ "devDependencies": {
43
+ "@lit/react": "^1.0.8",
44
+ "@types/node": "^24.10.1",
45
+ "@types/react": "^19.2.7",
46
+ "@types/react-dom": "^19.2.3",
47
+ "typescript": "^5.9.3",
48
+ "vite": "^7.2.6",
49
+ "vite-plugin-dts": "^4.5.4"
50
+ }
51
51
  }