@iyulab/router 0.7.6 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as isExternalUrl, i as absolutePath, n as __decorate, o as parseUrl, r as __decorateMetadata, s as UOutlet, t as ULink } from "./share-sbAElOI7.js";
1
+ import { a as isExternalUrl, i as absolutePath, n as __decorate, o as parseUrl, r as __decorateMetadata, s as UOutlet, t as ULink } from "./share-CUGwxZKa.js";
2
2
  import { LitElement, css, html } from "lit";
3
3
  import { customElement, property } from "lit/decorators.js";
4
4
  //#region src/types/RouteError.ts
@@ -24,6 +24,14 @@ var NotFoundError = class extends RouteError {
24
24
  }
25
25
  };
26
26
  /**
27
+ * enter 가드가 false를 반환하여 접근이 거부되었을 때 발생하는 에러
28
+ */
29
+ var AccessDeniedError = class extends RouteError {
30
+ constructor(path) {
31
+ super(403, `Access denied: ${path}`);
32
+ }
33
+ };
34
+ /**
27
35
  * u-outlet 요소를 찾을 수 없을 때 발생하는 에러
28
36
  */
29
37
  var OutletMissingError = class extends RouteError {
@@ -105,8 +113,12 @@ var RouteErrorEvent = class extends RouteEvent {
105
113
  };
106
114
  //#endregion
107
115
  //#region src/components/UErrorPage.ts
108
- var _ref;
116
+ var _ref, _ref2;
109
117
  var UErrorPage = class UErrorPage extends LitElement {
118
+ constructor(error) {
119
+ super();
120
+ this.error = error;
121
+ }
110
122
  render() {
111
123
  const error = this.error || this.getDefaultError();
112
124
  return html`
@@ -121,14 +133,13 @@ var UErrorPage = class UErrorPage extends LitElement {
121
133
  }
122
134
  /** 에러 코드에 따른 기본 아이콘 반환 */
123
135
  getErrorIcon(code) {
124
- const codeStr = String(code);
125
- const numericCode = typeof code === "string" ? parseInt(code) : code;
126
- switch (codeStr) {
136
+ switch (String(code)) {
127
137
  case "OUTLET_MISSING": return "📦";
128
138
  case "CONTENT_LOAD_FAILED": return "📡";
129
139
  case "CONTENT_RENDER_FAILED": return "🎨";
140
+ case "ACCESS_DENIED": return "🚫";
130
141
  }
131
- switch (numericCode) {
142
+ switch (typeof code === "string" ? parseInt(code) : code) {
132
143
  case 404: return "🔍";
133
144
  case 403: return "🚫";
134
145
  case 401: return "🔐";
@@ -194,8 +205,8 @@ var UErrorPage = class UErrorPage extends LitElement {
194
205
  `;
195
206
  }
196
207
  };
197
- __decorate([property({ type: Object }), __decorateMetadata("design:type", typeof (_ref = typeof RouteError !== "undefined" && RouteError) === "function" ? _ref : Object)], UErrorPage.prototype, "error", void 0);
198
- UErrorPage = __decorate([customElement("u-error-page")], UErrorPage);
208
+ __decorate([property({ type: Object }), __decorateMetadata("design:type", typeof (_ref2 = typeof RouteError !== "undefined" && RouteError) === "function" ? _ref2 : Object)], UErrorPage.prototype, "error", void 0);
209
+ UErrorPage = __decorate([customElement("u-error-page"), __decorateMetadata("design:paramtypes", [typeof (_ref = typeof RouteError !== "undefined" && RouteError) === "function" ? _ref : Object])], UErrorPage);
199
210
  //#endregion
200
211
  //#region src/internals/crypto-helpers.ts
201
212
  /**
@@ -214,10 +225,11 @@ function getRandomID() {
214
225
  * `u-outlet` 엘리먼트를 찾아 반환합니다.
215
226
  *
216
227
  * @param element 검색을 시작할 HTMLElement
228
+ * @param skip element 자신을 검사에서 제외할지 여부 (기본값: false)
217
229
  * @returns 찾은 UOutlet 엘리먼트 또는 undefined
218
230
  */
219
- function findOutlet(element) {
220
- if (element.tagName === "U-OUTLET") return element;
231
+ function findOutlet(element, skip = false) {
232
+ if (!skip && element instanceof UOutlet) return element;
221
233
  const roots = element.shadowRoot ? [element.shadowRoot, element] : [element];
222
234
  for (const root of roots) for (const child of Array.from(root.children)) {
223
235
  const result = findOutlet(child);
@@ -322,15 +334,55 @@ function getRoutes(routes, pathname) {
322
334
  return [];
323
335
  }
324
336
  //#endregion
337
+ //#region src/internals/RouteTracker.ts
338
+ /**
339
+ * 라우팅 체인 상태를 추적합니다.
340
+ * - 방문한 href를 기록하여 리다이렉트 사이클을 감지합니다.
341
+ * - 이미 실행된 route enter를 기록하여 중복 실행을 방지합니다.
342
+ */
343
+ var RouteTracker = class {
344
+ constructor() {
345
+ this._history = /* @__PURE__ */ new Set();
346
+ this._processed = /* @__PURE__ */ new Set();
347
+ }
348
+ /** 새 네비게이션 시작 시 상태를 초기화합니다. */
349
+ reset() {
350
+ this._history.clear();
351
+ this._processed.clear();
352
+ }
353
+ /**
354
+ * href 방문을 기록하고 사이클 여부를 반환합니다.
355
+ * @returns 사이클이 감지되면 true
356
+ */
357
+ visit(href) {
358
+ if (this._history.has(href)) {
359
+ console.error("Router: Redirect cycle detected:", [...this._history, href].join(" → "));
360
+ return true;
361
+ }
362
+ this._history.add(href);
363
+ return false;
364
+ }
365
+ /**
366
+ * route enter를 아직 실행하지 않았다면 키를 등록하고 true를 반환합니다.
367
+ * 이미 실행된 route라면 false를 반환합니다 (중첩 라우트 redirect 체인에서 중복 방지).
368
+ */
369
+ enter(route) {
370
+ const key = route.id ?? (route.path instanceof URLPattern ? route.path.pathname : String(route.path));
371
+ if (this._processed.has(key)) return false;
372
+ this._processed.add(key);
373
+ return true;
374
+ }
375
+ };
376
+ //#endregion
325
377
  //#region src/Router.ts
326
378
  /**
327
379
  * `lit-element`, `react`를 지원하는 SPA 클라이언트 라우터 객체입니다.
328
380
  */
329
381
  var Router = class {
330
382
  constructor(config) {
383
+ this._tracker = new RouteTracker();
331
384
  this.handleWindowPopstate = async (_) => {
332
- const href = window.location.href;
333
- await this.go(href);
385
+ await this.go(window.location.href);
334
386
  };
335
387
  this.handleDocumentClick = async (e) => {
336
388
  try {
@@ -353,6 +405,7 @@ var Router = class {
353
405
  this._basepath = absolutePath(config.basepath || "/");
354
406
  this._routes = setRoutes(config.routes || [], this._basepath);
355
407
  this._fallback = config.fallback;
408
+ this._enter = config.enter;
356
409
  window.addEventListener("popstate", this.handleWindowPopstate);
357
410
  if (config.useIntercept !== false) document.addEventListener("click", this.handleDocumentClick);
358
411
  if (config.initialLoad !== false) waitOutlet(this._rootElement).then(() => {
@@ -381,92 +434,100 @@ var Router = class {
381
434
  /**
382
435
  * 지정한 경로의 클라이언트 라우팅을 수행합니다. 상대경로일 경우 basepath와 조합되어 이동합니다.
383
436
  * @param href 이동할 경로
437
+ * @param options 네비게이션 옵션
384
438
  */
385
- async go(href) {
439
+ async go(href, options) {
440
+ if (!options?.isRedirect) this._tracker.reset();
386
441
  const requestID = getRandomID();
387
442
  this._requestID = requestID;
388
443
  const context = parseUrl(href, this._basepath);
389
- if (context.href !== window.location.href) window.history.pushState({ basepath: context.basepath }, "", context.href);
390
- else window.history.replaceState({ basepath: context.basepath }, "", context.href);
391
- const progressCallback = (value) => {
392
- if (this._requestID !== requestID) return;
393
- const progress = Math.max(0, Math.min(100, Math.round(value)));
394
- window.dispatchEvent(new RouteProgressEvent(context, progress));
444
+ if (this._tracker.visit(context.href)) return;
445
+ const useReplace = options?.isRedirect || options?.replace || context.href === window.location.href;
446
+ const historyState = {
447
+ basepath: context.basepath,
448
+ ...options?.state
395
449
  };
396
- context.progress = progressCallback;
450
+ if (useReplace) window.history.replaceState(historyState, "", context.href);
451
+ else window.history.pushState(historyState, "", context.href);
397
452
  let outlet = void 0;
453
+ let title = void 0;
398
454
  try {
399
- if (this._requestID !== requestID) return;
400
- window.dispatchEvent(new RouteBeginEvent(context));
455
+ outlet = findOutletOrThrow(this._rootElement);
401
456
  const routes = getRoutes(this._routes, context.pathname);
457
+ if (routes.length === 0) throw new NotFoundError(context.href);
402
458
  const lastRoute = routes[routes.length - 1];
403
- if (lastRoute && lastRoute.path instanceof URLPattern) context.params = lastRoute.path.exec({ pathname: context.pathname })?.pathname.groups || {};
404
- const mergedMeta = {};
405
- for (const route of routes) if (route.meta) Object.assign(mergedMeta, route.meta);
406
- context.meta = mergedMeta;
459
+ if (lastRoute.path instanceof URLPattern) context.params = lastRoute.path.exec({ pathname: context.pathname })?.pathname.groups || {};
460
+ context.progress = (value) => {
461
+ if (this._requestID !== requestID) return;
462
+ const progress = Math.max(0, Math.min(100, Math.round(value)));
463
+ window.dispatchEvent(new RouteProgressEvent(context, progress));
464
+ };
465
+ if (this._enter && !options?.isRedirect) {
466
+ const result = await this._enter(context);
467
+ if (this._requestID !== requestID) return;
468
+ if (result === false) throw new AccessDeniedError(context.pathname);
469
+ if (typeof result === "string") return void this.go(result, { isRedirect: true });
470
+ }
407
471
  this._context = context;
408
- outlet = findOutletOrThrow(this._rootElement);
409
- let title = void 0;
410
- let content = null;
411
- if (routes.length === 0) throw new NotFoundError(context.href);
472
+ window.dispatchEvent(new RouteBeginEvent(context));
412
473
  for (const route of routes) {
413
474
  if (this._requestID !== requestID) return;
475
+ context.metadata = {
476
+ ...context.metadata,
477
+ ...route.metadata
478
+ };
479
+ if (route.enter && this._tracker.enter(route)) {
480
+ const result = await route.enter(context);
481
+ if (this._requestID !== requestID) return;
482
+ if (result === false) throw new AccessDeniedError(context.pathname);
483
+ if (typeof result === "string") return void this.go(result, { isRedirect: true });
484
+ }
414
485
  if (!route.render) continue;
486
+ let content;
415
487
  try {
416
488
  content = await route.render(context);
417
489
  if (content === false || content === void 0 || content === null) throw new Error("Failed to load content for the route.");
418
- } catch (LoadError) {
419
- throw new ContentLoadError(LoadError);
490
+ } catch (e) {
491
+ throw new ContentLoadError(e);
420
492
  }
421
493
  try {
422
- outlet.render({
494
+ outlet.render(content, {
423
495
  id: route.id,
424
- value: content,
425
496
  force: route.force
426
497
  });
427
- } catch (renderError) {
428
- throw new ContentRenderError(renderError);
498
+ } catch (e) {
499
+ throw new ContentRenderError(e);
429
500
  }
430
- outlet = findOutlet(outlet) || outlet;
501
+ outlet = findOutlet(outlet, true) || outlet;
431
502
  title = route.title || title;
432
503
  }
433
- document.title = title || document.title;
434
504
  window.dispatchEvent(new RouteDoneEvent(context));
435
505
  } catch (error) {
436
506
  const routeError = error instanceof RouteError ? error : new RouteError(error?.status || error?.code || "UNKNOWN_ERROR", error?.message || "An unexpected error occurred", error);
437
507
  window.dispatchEvent(new RouteErrorEvent(context, routeError));
438
- console.error("Routing error:", routeError.original);
508
+ console.error("Routing error:", routeError.original || routeError);
439
509
  try {
440
- if (this._fallback && this._fallback.render && outlet) {
441
- const fallbackContent = await this._fallback.render({
442
- ...context,
443
- error: routeError
444
- });
445
- outlet.render({
446
- id: "#fallback",
447
- value: fallbackContent,
448
- force: true
449
- });
450
- document.title = this._fallback.title || document.title;
451
- } else {
452
- const errorContent = new UErrorPage();
453
- errorContent.error = error;
454
- if (outlet) outlet.render({
455
- id: "#error",
456
- value: errorContent,
457
- force: true
458
- });
459
- else {
460
- document.body.innerHTML = "";
461
- document.body.appendChild(errorContent);
462
- }
510
+ const content = this._fallback?.render ? await this._fallback.render({
511
+ ...context,
512
+ error: routeError
513
+ }) : new UErrorPage(routeError);
514
+ if (outlet) outlet.render(content, {
515
+ id: getRandomID(),
516
+ force: true
517
+ });
518
+ else {
519
+ document.body.innerHTML = "";
520
+ document.body.appendChild(content instanceof Node ? content : new UErrorPage(routeError));
463
521
  }
522
+ title = this._fallback?.title || routeError.message || "Error";
464
523
  } catch (pageError) {
465
524
  console.error("Failed to render error component:", pageError);
466
525
  console.error("Original error:", routeError.original || routeError);
467
526
  }
527
+ } finally {
528
+ document.title = title || document.title;
468
529
  }
469
530
  }
470
531
  };
471
532
  //#endregion
472
- export { ContentLoadError, ContentRenderError, NotFoundError, OutletMissingError, RouteBeginEvent, RouteDoneEvent, RouteError, RouteErrorEvent, RouteProgressEvent, Router, ULink, UOutlet };
533
+ export { AccessDeniedError, ContentLoadError, ContentRenderError, NotFoundError, OutletMissingError, RouteBeginEvent, RouteDoneEvent, RouteError, RouteErrorEvent, RouteProgressEvent, Router, ULink, UOutlet };
package/dist/react.d.ts CHANGED
@@ -10,8 +10,6 @@ declare interface RenderOption {
10
10
  id?: string;
11
11
  /** 강제 렌더링 여부 */
12
12
  force?: boolean;
13
- /** 렌더링할 값 */
14
- value: unknown;
15
13
  }
16
14
 
17
15
  /**
@@ -36,6 +34,16 @@ declare class ULink_2 extends LitElement {
36
34
  * - `_top`: 최상위 프레임에서 링크 열기
37
35
  */
38
36
  target?: string;
37
+ /**
38
+ * 링크 관계 rel 속성
39
+ *
40
+ * - `noopener`: target이 _blank인 경우 보안 강화 (window.opener 차단)
41
+ * - `noreferrer`: target이 _blank인 경우 보안 강화 + Referer 헤더 제거
42
+ * - `external`: 외부 링크임을 명시 (SEO/접근성에 도움)
43
+ * - `nofollow`: 검색 엔진이 링크를 따라가지 않도록 지시 (SEO에 영향)
44
+ * - 그 외 rel 값도 그대로 전달됩니다.
45
+ */
46
+ rel?: string;
39
47
  /**
40
48
  * 링크 대상 URL, 다음 사항에 따라 SPA 라우팅 또는 브라우저 네비게이션이 결정됩니다.
41
49
  *
@@ -82,7 +90,7 @@ declare class UOutlet_2 extends HTMLElement {
82
90
  /**
83
91
  * 주어진 렌더링 옵션에 따라 컨텐츠를 렌더링합니다.
84
92
  */
85
- render({ id, value, force }: RenderOption): Promise<void>;
93
+ render(value: unknown, options?: RenderOption): Promise<void>;
86
94
  /**
87
95
  * 기존 DOM을 삭제하여, 초기 상태로 되돌립니다.
88
96
  */
package/dist/react.js CHANGED
@@ -1,4 +1,4 @@
1
- import { s as UOutlet$1, t as ULink$1 } from "./share-sbAElOI7.js";
1
+ import { s as UOutlet$1, t as ULink$1 } from "./share-CUGwxZKa.js";
2
2
  import React from "react";
3
3
  import { createComponent } from "@lit/react";
4
4
  //#region src/react.ts
@@ -9,9 +9,9 @@ var UOutlet = class extends HTMLElement {
9
9
  /**
10
10
  * 주어진 렌더링 옵션에 따라 컨텐츠를 렌더링합니다.
11
11
  */
12
- async render({ id, value, force }) {
13
- if (this.routeId === id && force === false) return;
14
- this.routeId = id;
12
+ async render(value, options) {
13
+ if (this.routeId === options?.id && options?.force === false) return;
14
+ this.routeId = options?.id;
15
15
  this.reset();
16
16
  if (value === null) throw new Error("Content is null and cannot be rendered.");
17
17
  if (typeof value !== "object") throw new Error("Content is not a valid renderable object.");
@@ -88,7 +88,7 @@ function parseUrl(url, basepath) {
88
88
  hash: urlObj.hash,
89
89
  params: {},
90
90
  progress: () => {},
91
- meta: {}
91
+ metadata: {}
92
92
  };
93
93
  }
94
94
  /**
@@ -126,12 +126,12 @@ function catchBasepath(basepath) {
126
126
  return basepath;
127
127
  }
128
128
  //#endregion
129
- //#region \0@oxc-project+runtime@0.122.0/helpers/decorateMetadata.js
129
+ //#region \0@oxc-project+runtime@0.123.0/helpers/decorateMetadata.js
130
130
  function __decorateMetadata(k, v) {
131
131
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
132
132
  }
133
133
  //#endregion
134
- //#region \0@oxc-project+runtime@0.122.0/helpers/decorate.js
134
+ //#region \0@oxc-project+runtime@0.123.0/helpers/decorate.js
135
135
  function __decorate(decorators, target, key, desc) {
136
136
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
137
137
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -189,7 +189,11 @@ var ULink = class ULink extends LitElement {
189
189
  }
190
190
  render() {
191
191
  return html`
192
- <a target=${ifDefined(this.target)} href=${this.compute(this.href)}>
192
+ <a
193
+ href=${this.compute(this.href)}
194
+ target=${ifDefined(this.target)}
195
+ rel=${ifDefined(this.rel)}
196
+ >
193
197
  <slot></slot>
194
198
  </a>
195
199
  `;
@@ -231,6 +235,7 @@ var ULink = class ULink extends LitElement {
231
235
  }
232
236
  };
233
237
  __decorate([property({ type: String }), __decorateMetadata("design:type", String)], ULink.prototype, "target", void 0);
238
+ __decorate([property({ type: String }), __decorateMetadata("design:type", String)], ULink.prototype, "rel", void 0);
234
239
  __decorate([property({ type: String }), __decorateMetadata("design:type", String)], ULink.prototype, "href", void 0);
235
240
  ULink = __decorate([customElement("u-link")], ULink);
236
241
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iyulab/router",
3
- "version": "0.7.6",
3
+ "version": "0.9.0",
4
4
  "description": "A modern client-side router for web applications with support for Lit and React components",
5
5
  "keywords": [
6
6
  "lit",
@@ -1,16 +1,16 @@
1
1
  ---
2
2
  name: iyulab-router
3
- description: Client-side SPA router for Lit and React with URLPattern-based matching, nested routes, fallback handling, and route events. Use when working with @iyulab/router setting up routing, defining routes, handling navigation, nested layouts with <u-outlet>, or listening to route lifecycle events.
3
+ description: Client-side SPA router for Lit and React with URLPattern matching, nested routes, route guards, metadata merging, fallback handling, and route events. Use when working with @iyulab/router to define routes, add guards, handle navigation, or integrate <u-outlet>/<u-link>.
4
4
  license: MIT
5
5
  compatibility: Browser environments only (requires URLPattern and History API)
6
6
  metadata:
7
7
  author: iyulab
8
- version: "0.7.4"
8
+ version: "0.9.0"
9
9
  ---
10
10
 
11
11
  # @iyulab/router
12
12
 
13
- Client-side router supporting Lit and React renders, nested routes, and URLPattern-based matching.
13
+ Client-side router supporting Lit and React renders, nested routes, route guards, and URLPattern-based matching.
14
14
 
15
15
  ## Install
16
16
 
@@ -40,9 +40,19 @@ import { html } from 'lit';
40
40
  const router = new Router({
41
41
  root: document.body, // required — mount element containing <u-outlet>
42
42
  basepath: '/', // optional
43
+ enter: (ctx) => {
44
+ if (!isAuthenticated() && ctx.pathname !== '/login') return '/login';
45
+ return true;
46
+ },
43
47
  routes: [
44
48
  { index: true, render: () => html`<home-page></home-page>` },
45
49
  { path: '/user/:id', render: (ctx) => html`<user-page .id=${ctx.params.id}></user-page>` },
50
+ {
51
+ path: '/admin',
52
+ metadata: { role: 'admin' },
53
+ enter: (ctx) => ctx.metadata.role === 'admin' || '/forbidden',
54
+ render: () => html`<admin-page></admin-page>`,
55
+ },
46
56
  ],
47
57
  fallback: {
48
58
  render: (ctx) => html`<error-page .error=${ctx.error}></error-page>`
@@ -57,9 +67,10 @@ const router = new Router({
57
67
  | `path` | `string \| URLPattern` | URLPattern path; omit when `index: true` |
58
68
  | `index` | `true` | Marks route as index of its parent path |
59
69
  | `render` | `(ctx) => unknown` | Returns Lit `TemplateResult`, React element, or `HTMLElement` |
70
+ | `enter` | `(ctx) => string \| boolean \| Promise<string \| boolean>` | Guard before route render (`false` cancel, `string` redirect) |
60
71
  | `children` | `RouteConfig[]` | Nested routes; parent must render `<u-outlet>` |
61
72
  | `title` | `string` | Sets `document.title` on match |
62
- | `meta` | `Record<string, unknown>` | Arbitrary metadata (auth, layout, analytics) |
73
+ | `metadata` | `Record<string, unknown>` | Arbitrary metadata (auth, layout, analytics) |
63
74
  | `force` | `boolean` | Force re-render on URL change (default `true` for leaf routes) |
64
75
 
65
76
  ## RouteContext Fields
@@ -69,7 +80,7 @@ ctx.params // URLPattern captured params
69
80
  ctx.pathname // path without query/hash
70
81
  ctx.path // full path including query + hash
71
82
  ctx.query // URLSearchParams
72
- ctx.meta // merged meta from matched route chain
83
+ ctx.metadata // merged metadata from matched route chain
73
84
  ctx.progress // (value: number) => void — report 0–100 loading progress
74
85
  ```
75
86
 
@@ -103,6 +114,12 @@ import { ULink } from '@iyulab/router/react';
103
114
  <ULink href="/about">About</ULink>
104
115
  ```
105
116
 
117
+ `<u-link>` supports `href`, `target`, and `rel`.
118
+
119
+ ```html
120
+ <u-link href="https://example.com" target="_blank" rel="noopener noreferrer">External</u-link>
121
+ ```
122
+
106
123
  ## Route Events (window)
107
124
 
108
125
  | Event | Fired when |
@@ -120,4 +137,9 @@ import { ULink } from '@iyulab/router/react';
120
137
  | `CONTENT_LOAD_ERROR` | `ContentLoadError` |
121
138
  | `CONTENT_RENDER_ERROR` | `ContentRenderError` |
122
139
 
123
- See [references/REFERENCE.md](references/REFERENCE.md) for URL parameter patterns, React usage, and advanced examples.
140
+ References:
141
+ - [references/routing-basics.md](references/routing-basics.md)
142
+ - [references/url-pattern.md](references/url-pattern.md)
143
+ - [references/guards-and-metadata.md](references/guards-and-metadata.md)
144
+ - [references/components.md](references/components.md)
145
+ - [references/events-and-errors.md](references/events-and-errors.md)
@@ -0,0 +1,43 @@
1
+ # Components
2
+
3
+ ## Lit Usage
4
+
5
+ ```ts
6
+ import '@iyulab/router';
7
+ import { html } from 'lit';
8
+
9
+ html`
10
+ <nav>
11
+ <u-link href="/">Home</u-link>
12
+ <u-link href="/docs">Docs</u-link>
13
+ <u-link href="https://example.com" target="_blank" rel="noopener noreferrer">External</u-link>
14
+ </nav>
15
+ <main>
16
+ <u-outlet></u-outlet>
17
+ </main>
18
+ `;
19
+ ```
20
+
21
+ ## React Wrappers
22
+
23
+ ```tsx
24
+ import { ULink, UOutlet } from '@iyulab/router/react';
25
+
26
+ export function AppRoot() {
27
+ return (
28
+ <div>
29
+ <nav>
30
+ <ULink href="/">Home</ULink>
31
+ <ULink href="/about">About</ULink>
32
+ </nav>
33
+ <main>
34
+ <UOutlet />
35
+ </main>
36
+ </div>
37
+ );
38
+ }
39
+ ```
40
+
41
+ ## Nested Outlet Rule
42
+
43
+ A parent route must render `<u-outlet>` to host child route content.
@@ -0,0 +1,39 @@
1
+ # Events and Errors
2
+
3
+ ## Route Events
4
+
5
+ ```ts
6
+ window.addEventListener('route-begin', (e) => {
7
+ console.log(e.context.pathname);
8
+ });
9
+
10
+ window.addEventListener('route-progress', (e) => {
11
+ progressBar.value = e.progress;
12
+ });
13
+
14
+ window.addEventListener('route-done', (e) => {
15
+ analytics.track(e.context.pathname);
16
+ });
17
+
18
+ window.addEventListener('route-error', (e) => {
19
+ errorTracker.report(e.error);
20
+ });
21
+ ```
22
+
23
+ ## Fallback
24
+
25
+ ```ts
26
+ fallback: {
27
+ render: (ctx) => {
28
+ const { code, message } = ctx.error;
29
+ if (code === 'NOT_FOUND') return html`<not-found-page></not-found-page>`;
30
+ return html`<error-page .message=${message}></error-page>`;
31
+ }
32
+ }
33
+ ```
34
+
35
+ ## Error Codes
36
+
37
+ - `NOT_FOUND`
38
+ - `CONTENT_LOAD_ERROR`
39
+ - `CONTENT_RENDER_ERROR`
@@ -0,0 +1,50 @@
1
+ # Guards and Metadata
2
+
3
+ ## Global Guard
4
+
5
+ ```ts
6
+ const router = new Router({
7
+ root: document.body,
8
+ enter: (ctx) => {
9
+ if (!isAuthenticated() && ctx.pathname !== '/login') return '/login';
10
+ return true;
11
+ },
12
+ routes: [...],
13
+ });
14
+ ```
15
+
16
+ ## Route Guard
17
+
18
+ ```ts
19
+ {
20
+ path: '/admin',
21
+ enter: () => hasRole('admin') || '/forbidden',
22
+ render: () => html`<admin-page></admin-page>`
23
+ }
24
+ ```
25
+
26
+ Guard return values:
27
+ - `true` or `undefined`: continue
28
+ - `false`: cancel navigation
29
+ - `string`: redirect
30
+
31
+ ## Route Metadata
32
+
33
+ ```ts
34
+ {
35
+ path: '/admin',
36
+ metadata: { requiresAuth: true, section: 'admin' },
37
+ render: (ctx) => {
38
+ // merged metadata from matched chain
39
+ console.log(ctx.metadata);
40
+ return html`<admin-layout><u-outlet></u-outlet></admin-layout>`;
41
+ },
42
+ children: [
43
+ {
44
+ path: 'settings',
45
+ metadata: { tab: 'settings' },
46
+ render: (ctx) => html`<settings-page .metadata=${ctx.metadata}></settings-page>`
47
+ }
48
+ ]
49
+ }
50
+ ```
@@ -0,0 +1,40 @@
1
+ # Routing Basics
2
+
3
+ ## Minimal Setup
4
+
5
+ ```ts
6
+ import { Router } from '@iyulab/router';
7
+ import { html } from 'lit';
8
+
9
+ const router = new Router({
10
+ root: document.body,
11
+ basepath: '/',
12
+ routes: [
13
+ { index: true, render: () => html`<home-page></home-page>` },
14
+ { path: '/users/:id', render: (ctx) => html`<user-page .id=${ctx.params.id}></user-page>` },
15
+ ],
16
+ fallback: {
17
+ render: (ctx) => html`<error-page .error=${ctx.error}></error-page>`,
18
+ },
19
+ });
20
+ ```
21
+
22
+ ## RouterConfig Options
23
+
24
+ | Option | Default | Description |
25
+ |---|---|---|
26
+ | `root` | - | Mount element (required) |
27
+ | `basepath` | `'/'` | URL base path |
28
+ | `routes` | `[]` | Route definitions |
29
+ | `enter` | - | Global guard before navigation |
30
+ | `fallback` | built-in error page | Error/404 handler |
31
+ | `useIntercept` | `true` | Intercept `<a>` clicks for client routing |
32
+ | `initialLoad` | `true` | Auto-navigate on initialization |
33
+
34
+ ## Navigation
35
+
36
+ ```ts
37
+ router.go('/dashboard');
38
+ router.go('settings');
39
+ router.destroy();
40
+ ```