@iyulab/modern-app 0.34.2 → 0.35.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/CHANGELOG.md +20 -0
- package/README.md +35 -2
- package/dist/components/ActionBar.d.ts +0 -10
- package/dist/components/EmptyState.d.ts +0 -13
- package/dist/components/GroupBox.d.ts +0 -15
- package/dist/components/InfoField.d.ts +0 -24
- package/dist/components/InfoSection.d.ts +0 -10
- package/dist/components/MasterDetailLayout.d.ts +0 -12
- package/dist/components/PageHeader.d.ts +0 -15
- package/dist/react.d.ts +58 -0
- package/package.json +2 -2
- package/skills/modern-app/SKILL.md +9 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.35.0] - 2026-10-06
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **Breaking — the JSX types for the plain content-primitive tags moved to `@iyulab/modern-app/react`.**
|
|
8
|
+
The main entry's type declarations imported `react`, so a TypeScript app without React (`react` is
|
|
9
|
+
an optional peer) failed to type-check with `skipLibCheck: false` — seven `TS2307: Cannot find
|
|
10
|
+
module 'react'` errors from `ActionBar`, `EmptyState`, `GroupBox`, `InfoField`, `InfoSection`,
|
|
11
|
+
`MasterDetailLayout` and `PageHeader`. The main entry no longer references React. A React app
|
|
12
|
+
that imports anything from `@iyulab/modern-app/react` keeps the tag types as before; one that
|
|
13
|
+
only writes the plain tags in JSX adds `import type {} from '@iyulab/modern-app/react'` once.
|
|
14
|
+
|
|
15
|
+
### Documentation
|
|
16
|
+
|
|
17
|
+
- **The Quick Start's menu icons render.** Its `icon: 'home'` / `'users'` had no `lib`, so they were
|
|
18
|
+
looked up in the app's own `/assets/icons/` and, in a fresh app, drawn as the placeholder. The
|
|
19
|
+
example now names `lib: 'tabler'`, and a new «Icons» section explains where names resolve from.
|
|
20
|
+
- **`routerMode: 'hash'` is described** under Navigation, for static hosts that serve one document.
|
|
21
|
+
- Requires `@iyulab/router` 0.19.1 for shell links to carry the `#` address on a first visit in hash mode.
|
|
22
|
+
|
|
3
23
|
## [0.34.2] - 2026-10-06
|
|
4
24
|
|
|
5
25
|
### 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 셸.
|
|
@@ -92,15 +91,4 @@ declare global {
|
|
|
92
91
|
'u-master-detail-layout': MasterDetailLayout;
|
|
93
92
|
}
|
|
94
93
|
}
|
|
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
94
|
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 {};
|
package/dist/react.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { default as React } from 'react';
|
|
|
2
2
|
import { EventName } from '@lit/react';
|
|
3
3
|
import { SidebarLayout as SidebarLayoutElement } from './layouts/SidebarLayout.js';
|
|
4
4
|
import { Wizard as WizardElement, WizardStepChangeDetail } 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
|
/**
|
|
@@ -27,3 +29,59 @@ 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.
|
|
4
|
+
"version": "0.35.0",
|
|
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.
|
|
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',
|
|
41
|
-
{ type: 'link', icon: 'users', label: '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
|