@iyulab/modern-app 0.34.2 → 0.35.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.35.1] - 2026-10-06
4
+
5
+ ### Fixed
6
+
7
+ - **`u-wizard`'s `step-change`, `u-master-detail-layout`'s `detail-close` and `u-sidebar-layout`'s
8
+ `overlay-close` are typed for TypeScript listeners** (`WizardEventMap`, `MasterDetailLayoutEventMap`,
9
+ `SidebarLayoutEventMap`, exported). `wizard.addEventListener('step-change', (e) => e.detail.to)` no
10
+ longer needs a cast; the React `onStepChange`/`onOverlayClose` props use the same types.
11
+
12
+ ## [0.35.0] - 2026-10-06
13
+
14
+ ### Changed
15
+
16
+ - **Breaking — the JSX types for the plain content-primitive tags moved to `@iyulab/modern-app/react`.**
17
+ The main entry's type declarations imported `react`, so a TypeScript app without React (`react` is
18
+ an optional peer) failed to type-check with `skipLibCheck: false` — seven `TS2307: Cannot find
19
+ module 'react'` errors from `ActionBar`, `EmptyState`, `GroupBox`, `InfoField`, `InfoSection`,
20
+ `MasterDetailLayout` and `PageHeader`. The main entry no longer references React. A React app
21
+ that imports anything from `@iyulab/modern-app/react` keeps the tag types as before; one that
22
+ only writes the plain tags in JSX adds `import type {} from '@iyulab/modern-app/react'` once.
23
+
24
+ ### Documentation
25
+
26
+ - **The Quick Start's menu icons render.** Its `icon: 'home'` / `'users'` had no `lib`, so they were
27
+ looked up in the app's own `/assets/icons/` and, in a fresh app, drawn as the placeholder. The
28
+ example now names `lib: 'tabler'`, and a new «Icons» section explains where names resolve from.
29
+ - **`routerMode: 'hash'` is described** under Navigation, for static hosts that serve one document.
30
+ - Requires `@iyulab/router` 0.19.1 for shell links to carry the `#` address on a first visit in hash mode.
31
+
3
32
  ## [0.34.2] - 2026-10-06
4
33
 
5
34
  ### Fixed
package/README.md CHANGED
@@ -29,8 +29,8 @@ await app.load({
29
29
  logo: { src: '/assets/logo.svg', alt: 'My App' },
30
30
  title: 'My App',
31
31
  main: [
32
- { type: 'link', icon: 'home', label: 'Home', href: '/' },
33
- { type: 'link', icon: 'users', label: 'Users', href: '/users' },
32
+ { type: 'link', icon: 'home', lib: 'tabler', label: 'Home', href: '/' },
33
+ { type: 'link', icon: 'users', lib: 'tabler', label: 'Users', href: '/users' },
34
34
  ],
35
35
  },
36
36
  routes: [
@@ -45,6 +45,21 @@ await app.load({
45
45
  });
46
46
  ```
47
47
 
48
+ ### Icons
49
+
50
+ A menu item's `icon` is a name resolved by `<u-icon>` from `@iyulab/components`:
51
+
52
+ - **with `lib`** — from that library. `tabler`, `heroicons`, `lucide` and `bootstrap` are pre-registered
53
+ and fetched from a CDN on first use (the example above uses `tabler`); register your own with
54
+ `IconRegistry.register()`.
55
+ - **without `lib`** — from your own files at `/assets/icons/<name>.svg` (change the folder with
56
+ `setDefaultBaseUrl()`).
57
+
58
+ A name that resolves to nothing is drawn as a neutral placeholder instead of an empty gap, so a
59
+ missing icon set shows up as every item having the same icon. The shell's own controls (menu and
60
+ sidebar toggles, close) use a bundled set and need no network. See the components
61
+ [icon guide](https://github.com/iyulab/node-components/blob/main/docs/icons.md).
62
+
48
63
  ## Skills Usage
49
64
 
50
65
  AI agent skills for this package are located in `skills/modern-app/`. Install them with `npx skills`:
@@ -69,6 +84,16 @@ app.router?.go('/users/42'); // via router instance
69
84
  app.router?.context; // current RouteContext
70
85
  ```
71
86
 
87
+ Routes live in the path (`/users/42`) by default, which needs the server to answer every path with
88
+ the app. On a static host that serves one document (GitHub Pages, object storage), put the route
89
+ after `#` instead:
90
+
91
+ ```typescript
92
+ await app.load({ routerMode: 'hash', /* … */ }); // https://example.com/#/users/42
93
+ ```
94
+
95
+ Links and `app.navigate()` follow the mode, so the rest of the app is written the same way.
96
+
72
97
  ### Theme
73
98
 
74
99
  ```typescript
@@ -214,6 +239,14 @@ properties that a raw element only handles correctly under React 19.
214
239
  npm install @iyulab/modern-app @lit/react react
215
240
  ```
216
241
 
242
+ The JSX types for using the content primitives as plain tags (`<u-info-field>`, `<u-page-header>`, …)
243
+ live in this subpath too, so a non-React app never needs React's types. Importing anything from
244
+ `@iyulab/modern-app/react` brings them in; a React app that only uses the plain tags adds one line:
245
+
246
+ ```ts
247
+ import type {} from '@iyulab/modern-app/react';
248
+ ```
249
+
217
250
  ```tsx
218
251
  import { SidebarLayout, Wizard, type SidebarItem, type WizardStep } from '@iyulab/modern-app/react';
219
252
 
@@ -1,5 +1,4 @@
1
1
  import { StyledElement } from '../internals/StyledElement.js';
2
- import { default as React } from 'react';
3
2
  type ElementParts = 'host' | 'danger' | 'main';
4
3
  /**
5
4
  * 푸터 액션 바 — 상세·편집 화면 맨 아래의 같은 골격.
@@ -43,13 +42,4 @@ declare global {
43
42
  'u-action-bar': ActionBar;
44
43
  }
45
44
  }
46
- declare module 'react' {
47
- namespace JSX {
48
- interface IntrinsicElements {
49
- 'u-action-bar': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
50
- sticky?: boolean;
51
- };
52
- }
53
- }
54
- }
55
45
  export {};
@@ -1,5 +1,4 @@
1
1
  import { StyledElement } from '../internals/StyledElement.js';
2
- import { default as React } from 'react';
3
2
  type ElementParts = 'host' | 'icon' | 'title' | 'description' | 'actions';
4
3
  /**
5
4
  * 빈 상태 — 목록·검색 결과가 비었을 때.
@@ -50,16 +49,4 @@ declare global {
50
49
  'u-empty-state': EmptyState;
51
50
  }
52
51
  }
53
- declare module 'react' {
54
- namespace JSX {
55
- interface IntrinsicElements {
56
- 'u-empty-state': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
57
- variant?: 'no-data' | 'no-results';
58
- title?: string;
59
- description?: string;
60
- locale?: string;
61
- };
62
- }
63
- }
64
- }
65
52
  export {};
@@ -1,5 +1,4 @@
1
1
  import { StyledElement } from '../internals/StyledElement.js';
2
- import { default as React } from 'react';
3
2
  type ElementParts = 'host' | 'header' | 'title' | 'meta' | 'description' | 'actions' | 'body';
4
3
  /** 제목 단계 — 페이지 제목(h1)은 `u-page-header` 몫이라 2 부터다. */
5
4
  export type GroupBoxLevel = 2 | 3 | 4 | 5 | 6;
@@ -66,18 +65,4 @@ declare global {
66
65
  'u-group-box': GroupBox;
67
66
  }
68
67
  }
69
- declare module 'react' {
70
- namespace JSX {
71
- interface IntrinsicElements {
72
- 'u-group-box': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
73
- title?: string;
74
- divider?: boolean;
75
- flush?: boolean;
76
- level?: GroupBoxLevel;
77
- meta?: string;
78
- description?: string;
79
- };
80
- }
81
- }
82
- }
83
68
  export {};
@@ -1,5 +1,4 @@
1
1
  import { StyledElement } from '../internals/StyledElement.js';
2
- import { default as React } from 'react';
3
2
  type ElementParts = 'host' | 'label' | 'value' | 'unit' | 'trend';
4
3
  export type InfoFieldFormat = 'number' | 'currency' | 'date';
5
4
  export type InfoFieldSize = 'default' | 'lg';
@@ -103,27 +102,4 @@ declare global {
103
102
  'u-info-field': InfoField;
104
103
  }
105
104
  }
106
- declare module 'react' {
107
- namespace JSX {
108
- interface IntrinsicElements {
109
- 'u-info-field': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
110
- label?: string;
111
- /** 클래스 필드와 동일하게 `unknown` — 렌더 로직이 `String(value)`/포맷터로 무엇이
112
- * 오든 처리하므로 `null`·숫자를 그대로 넘길 수 있다(위 클래스 필드 JSDoc 참조). */
113
- value?: unknown;
114
- blank?: string;
115
- numeric?: boolean;
116
- format?: InfoFieldFormat;
117
- currency?: string;
118
- unit?: string;
119
- size?: InfoFieldSize;
120
- trend?: InfoFieldTrend;
121
- /** 프로퍼티는 `trendLabel`이지만 Lit 기본 속성명 규칙(소문자화, kebab
122
- * 아님)상 실제 HTML 속성명은 `trendlabel`이다. */
123
- trendlabel?: string;
124
- tone?: InfoFieldTone;
125
- };
126
- }
127
- }
128
- }
129
105
  export {};
@@ -1,5 +1,4 @@
1
1
  import { StyledElement } from '../internals/StyledElement.js';
2
- import { default as React } from 'react';
3
2
  type ElementParts = 'host' | 'grid';
4
3
  /**
5
4
  * 정보 섹션 — `u-info-field` 들의 반응형 그리드.
@@ -29,13 +28,4 @@ declare global {
29
28
  'u-info-section': InfoSection;
30
29
  }
31
30
  }
32
- declare module 'react' {
33
- namespace JSX {
34
- interface IntrinsicElements {
35
- 'u-info-section': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
36
- min?: number | string;
37
- };
38
- }
39
- }
40
- }
41
31
  export {};
@@ -1,6 +1,5 @@
1
1
  import { PropertyValues } from 'lit';
2
2
  import { StyledElement } from '../internals/StyledElement.js';
3
- import { default as React } from 'react';
4
3
  type ElementParts = 'host' | 'master' | 'divider' | 'detail' | 'detail-close';
5
4
  /**
6
5
  * master›detail 반응형 split-pane 셸.
@@ -87,20 +86,23 @@ export declare class MasterDetailLayout extends StyledElement<ElementParts> {
87
86
  private handleDetailSlotChange;
88
87
  render(): import('lit-html').TemplateResult<1>;
89
88
  }
89
+ /** Events `<u-master-detail-layout>` dispatches — bubbling and composed. */
90
+ export interface MasterDetailLayoutEventMap {
91
+ /** The detail pane's close button was pressed (narrow width, overlay mode). Clearing the `detail` slot is yours. */
92
+ 'detail-close': CustomEvent<null>;
93
+ }
94
+ /** Typed listeners for {@link MasterDetailLayoutEventMap} — element-scoped, the DOM's own pattern (`HTMLMediaElementEventMap`). */
95
+ export interface MasterDetailLayout {
96
+ addEventListener<K extends keyof MasterDetailLayoutEventMap>(type: K, listener: (this: MasterDetailLayout, ev: MasterDetailLayoutEventMap[K]) => unknown, options?: boolean | AddEventListenerOptions): void;
97
+ addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: MasterDetailLayout, ev: HTMLElementEventMap[K]) => unknown, options?: boolean | AddEventListenerOptions): void;
98
+ addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
99
+ removeEventListener<K extends keyof MasterDetailLayoutEventMap>(type: K, listener: (this: MasterDetailLayout, ev: MasterDetailLayoutEventMap[K]) => unknown, options?: boolean | EventListenerOptions): void;
100
+ removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: MasterDetailLayout, ev: HTMLElementEventMap[K]) => unknown, options?: boolean | EventListenerOptions): void;
101
+ removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
102
+ }
90
103
  declare global {
91
104
  interface HTMLElementTagNameMap {
92
105
  'u-master-detail-layout': MasterDetailLayout;
93
106
  }
94
107
  }
95
- declare module 'react' {
96
- namespace JSX {
97
- interface IntrinsicElements {
98
- 'u-master-detail-layout': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
99
- 'master-size'?: string;
100
- 'overlay-breakpoint'?: number | string;
101
- locale?: string;
102
- };
103
- }
104
- }
105
- }
106
108
  export {};
@@ -1,5 +1,4 @@
1
1
  import { StyledElement } from '../internals/StyledElement.js';
2
- import { default as React } from 'react';
3
2
  type ElementParts = 'host' | 'back' | 'heading' | 'eyebrow' | 'title' | 'subtitle' | 'status' | 'actions';
4
3
  /**
5
4
  * 페이지 헤더 — 모든 LOB 화면 최상단의 같은 골격.
@@ -60,18 +59,4 @@ declare global {
60
59
  'u-page-header': PageHeader;
61
60
  }
62
61
  }
63
- declare module 'react' {
64
- namespace JSX {
65
- interface IntrinsicElements {
66
- 'u-page-header': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
67
- title?: string;
68
- subtitle?: string;
69
- eyebrow?: string;
70
- back?: string;
71
- 'back-label'?: string;
72
- locale?: string;
73
- };
74
- }
75
- }
76
- }
77
62
  export {};
@@ -89,6 +89,20 @@ export declare class Wizard extends StyledElement<ElementParts> {
89
89
  private renderStep;
90
90
  render(): import('lit-html').TemplateResult<1>;
91
91
  }
92
+ /** Events `<u-wizard>` dispatches — bubbling and composed. */
93
+ export interface WizardEventMap {
94
+ /** Cancelable: `preventDefault()` keeps the wizard on `from` (validation, unsaved work). */
95
+ 'step-change': CustomEvent<WizardStepChangeDetail>;
96
+ }
97
+ /** Typed listeners for {@link WizardEventMap} — element-scoped, the DOM's own pattern (`HTMLMediaElementEventMap`). */
98
+ export interface Wizard {
99
+ addEventListener<K extends keyof WizardEventMap>(type: K, listener: (this: Wizard, ev: WizardEventMap[K]) => unknown, options?: boolean | AddEventListenerOptions): void;
100
+ addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: Wizard, ev: HTMLElementEventMap[K]) => unknown, options?: boolean | AddEventListenerOptions): void;
101
+ addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
102
+ removeEventListener<K extends keyof WizardEventMap>(type: K, listener: (this: Wizard, ev: WizardEventMap[K]) => unknown, options?: boolean | EventListenerOptions): void;
103
+ removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: Wizard, ev: HTMLElementEventMap[K]) => unknown, options?: boolean | EventListenerOptions): void;
104
+ removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
105
+ }
92
106
  declare global {
93
107
  interface HTMLElementTagNameMap {
94
108
  'u-wizard': Wizard;
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export type * from './types/AppOptions';
4
4
  export type * from './types/AuthConfig';
5
5
  export type { SidebarPermissionGuard } from './layouts/SidebarPermission';
6
6
  export type { SidebarIconsConfig } from './layouts/SidebarLayout.types';
7
+ export type { SidebarLayoutEventMap } from './layouts/SidebarLayout.js';
7
8
  export { filterSidebarItems } from './layouts/filterSidebarItems.js';
8
9
  export { PageHeader } from './components/PageHeader.js';
9
10
  export { GroupBox } from './components/GroupBox.js';
@@ -12,8 +13,9 @@ export { InfoField, isBlank } from './components/InfoField.js';
12
13
  export { EmptyState } from './components/EmptyState.js';
13
14
  export { ActionBar } from './components/ActionBar.js';
14
15
  export { MasterDetailLayout } from './components/MasterDetailLayout.js';
16
+ export type { MasterDetailLayoutEventMap } from './components/MasterDetailLayout.js';
15
17
  export { Wizard } from './components/Wizard.js';
16
- export type { WizardStep, WizardStepState, WizardStepChangeDetail } from './components/Wizard.js';
18
+ export type { WizardStep, WizardStepState, WizardStepChangeDetail, WizardEventMap } from './components/Wizard.js';
17
19
  export { translate } from './translate.js';
18
20
  export { modernAppLocale, registerLocale, setDefaultLocale, getLocaleStrings, getDefaultLocale, } from './internals/locale.js';
19
21
  export type { ModernAppLocaleStrings, ModernAppMessageKey } from './internals/locale.js';
@@ -156,6 +156,20 @@ export declare class SidebarLayout extends StyledElement<SidebarParts> {
156
156
  /** 화면 크기 변경에 따른 사이드바 상태 업데이트 */
157
157
  private handleScreenResize;
158
158
  }
159
+ /** Events `<u-sidebar-layout>` dispatches — bubbling and composed. */
160
+ export interface SidebarLayoutEventMap {
161
+ /** The overlay's close button was pressed, or Escape with focus inside the panel. Emptying the `overlay` slot is yours. */
162
+ 'overlay-close': CustomEvent<null>;
163
+ }
164
+ /** Typed listeners for {@link SidebarLayoutEventMap} — element-scoped, the DOM's own pattern (`HTMLMediaElementEventMap`). */
165
+ export interface SidebarLayout {
166
+ addEventListener<K extends keyof SidebarLayoutEventMap>(type: K, listener: (this: SidebarLayout, ev: SidebarLayoutEventMap[K]) => unknown, options?: boolean | AddEventListenerOptions): void;
167
+ addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: SidebarLayout, ev: HTMLElementEventMap[K]) => unknown, options?: boolean | AddEventListenerOptions): void;
168
+ addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
169
+ removeEventListener<K extends keyof SidebarLayoutEventMap>(type: K, listener: (this: SidebarLayout, ev: SidebarLayoutEventMap[K]) => unknown, options?: boolean | EventListenerOptions): void;
170
+ removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: SidebarLayout, ev: HTMLElementEventMap[K]) => unknown, options?: boolean | EventListenerOptions): void;
171
+ removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
172
+ }
159
173
  declare global {
160
174
  interface HTMLElementTagNameMap {
161
175
  'u-sidebar-layout': SidebarLayout;
package/dist/react.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import { default as React } from 'react';
2
2
  import { EventName } from '@lit/react';
3
- import { SidebarLayout as SidebarLayoutElement } from './layouts/SidebarLayout.js';
4
- import { Wizard as WizardElement, WizardStepChangeDetail } from './components/Wizard.js';
3
+ import { SidebarLayout as SidebarLayoutElement, SidebarLayoutEventMap } from './layouts/SidebarLayout.js';
4
+ import { Wizard as WizardElement, WizardEventMap } from './components/Wizard.js';
5
+ import { GroupBoxLevel } from './components/GroupBox.js';
6
+ import { InfoFieldFormat, InfoFieldSize, InfoFieldTrend, InfoFieldTone } from './components/InfoField.js';
5
7
  export { ScreenObserver } from './internals/ScreenObserver.js';
6
8
  export type { ScreenSize, ScreenObserverConfig, ScreenResizeEvent } from './internals/ScreenObserver.js';
7
9
  /**
@@ -10,7 +12,7 @@ export type { ScreenSize, ScreenObserverConfig, ScreenResizeEvent } from './inte
10
12
  * React 18 (JSX attributes stringify), so this wrapper keeps `config` working on both.
11
13
  */
12
14
  export declare const SidebarLayout: import('@lit/react').ReactWebComponent<SidebarLayoutElement, {
13
- onOverlayClose: EventName<CustomEvent>;
15
+ onOverlayClose: EventName<SidebarLayoutEventMap["overlay-close"]>;
14
16
  }>;
15
17
  export type SidebarLayoutProps = React.ComponentProps<typeof SidebarLayout>;
16
18
  /**
@@ -21,9 +23,65 @@ export type SidebarLayoutProps = React.ComponentProps<typeof SidebarLayout>;
21
23
  * wrapper keeps working on React 18 the same way it does for `SidebarLayout`'s `config`.
22
24
  */
23
25
  export declare const Wizard: import('@lit/react').ReactWebComponent<WizardElement, {
24
- onStepChange: EventName<CustomEvent<WizardStepChangeDetail>>;
26
+ onStepChange: EventName<WizardEventMap["step-change"]>;
25
27
  }>;
26
28
  export type WizardProps = React.ComponentProps<typeof Wizard>;
27
29
  export type { SidebarLayoutElement, WizardElement };
28
30
  export type { SidebarLayoutConfig, SidebarItem, SidebarLogoConfig, SidebarLinkConfig, SidebarSectionConfig, SidebarGroupConfig, SidebarButtonConfig, SidebarHtmlConfig, } from './layouts/SidebarLayout.types.js';
29
31
  export type { WizardStep, WizardStepState, WizardStepChangeDetail } from './components/Wizard.js';
32
+ declare module 'react' {
33
+ namespace JSX {
34
+ interface IntrinsicElements {
35
+ 'u-action-bar': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
36
+ sticky?: boolean;
37
+ };
38
+ 'u-empty-state': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
39
+ variant?: 'no-data' | 'no-results';
40
+ title?: string;
41
+ description?: string;
42
+ locale?: string;
43
+ };
44
+ 'u-group-box': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
45
+ title?: string;
46
+ divider?: boolean;
47
+ flush?: boolean;
48
+ level?: GroupBoxLevel;
49
+ meta?: string;
50
+ description?: string;
51
+ };
52
+ 'u-info-field': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
53
+ label?: string;
54
+ /** 클래스 필드와 동일하게 `unknown` — 렌더 로직이 `String(value)`/포맷터로 무엇이
55
+ * 오든 처리하므로 `null`·숫자를 그대로 넘길 수 있다(`InfoField.value` 의 JSDoc 참조). */
56
+ value?: unknown;
57
+ blank?: string;
58
+ numeric?: boolean;
59
+ format?: InfoFieldFormat;
60
+ currency?: string;
61
+ unit?: string;
62
+ size?: InfoFieldSize;
63
+ trend?: InfoFieldTrend;
64
+ /** 프로퍼티는 `trendLabel`이지만 Lit 기본 속성명 규칙(소문자화, kebab
65
+ * 아님)상 실제 HTML 속성명은 `trendlabel`이다. */
66
+ trendlabel?: string;
67
+ tone?: InfoFieldTone;
68
+ };
69
+ 'u-info-section': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
70
+ min?: number | string;
71
+ };
72
+ 'u-master-detail-layout': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
73
+ 'master-size'?: string;
74
+ 'overlay-breakpoint'?: number | string;
75
+ locale?: string;
76
+ };
77
+ 'u-page-header': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
78
+ title?: string;
79
+ subtitle?: string;
80
+ eyebrow?: string;
81
+ back?: string;
82
+ 'back-label'?: string;
83
+ locale?: string;
84
+ };
85
+ }
86
+ }
87
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@iyulab/modern-app",
3
3
  "description": "web-framework by iyulab based on lit-element",
4
- "version": "0.34.2",
4
+ "version": "0.35.1",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "web-framework",
@@ -60,7 +60,7 @@
60
60
  "test:react-types": "tsc -p tsconfig.react-smoke.json"
61
61
  },
62
62
  "dependencies": {
63
- "@iyulab/router": "^0.19.0",
63
+ "@iyulab/router": "^0.19.1",
64
64
  "i18next": "^26.3.6",
65
65
  "lit": "^3.3.3"
66
66
  },
@@ -37,8 +37,8 @@ await app.load({
37
37
  logo: { src: '/assets/logo.svg', alt: 'My App' },
38
38
  title: 'My App',
39
39
  main: [
40
- { type: 'link', icon: 'home', label: 'Home', href: '/' },
41
- { type: 'link', icon: 'users', label: 'Users', href: '/users' },
40
+ { type: 'link', icon: 'home', lib: 'tabler', label: 'Home', href: '/' },
41
+ { type: 'link', icon: 'users', lib: 'tabler', label: 'Users', href: '/users' },
42
42
  ],
43
43
  },
44
44
  routes: [
@@ -52,6 +52,10 @@ await app.load({
52
52
  });
53
53
  ```
54
54
 
55
+ Menu `icon` names resolve through `<u-icon>`: with `lib` from that library (`tabler`, `heroicons`, `lucide`,
56
+ `bootstrap` are pre-registered, CDN-fetched), without `lib` from the app's own `/assets/icons/<name>.svg`.
57
+ An unresolved name draws a neutral placeholder — every item showing the same icon means the set is missing.
58
+
55
59
  To tear down the app:
56
60
 
57
61
  ```typescript
@@ -70,6 +74,9 @@ app.router?.context; // current RouteContext
70
74
  app.router?.routes; // registered routes
71
75
  ```
72
76
 
77
+ On a static host that serves one document, `app.load({ routerMode: 'hash' })` keeps the route after `#`
78
+ (`/#/users/42`); links and `app.navigate()` follow the mode.
79
+
73
80
  ---
74
81
 
75
82
  ## Theme