@iyulab/modern-app 0.34.1 → 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 +30 -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/layouts/SidebarLayout.js +36 -18
- 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,35 @@
|
|
|
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
|
+
|
|
23
|
+
## [0.34.2] - 2026-10-06
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **The content area's scroll keys yield to the control that owns them** (`SidebarLayout`). With focus
|
|
28
|
+
on a button in the content, Space scrolled the page by a screen and the button never fired; with
|
|
29
|
+
focus in a widget that moves with the arrows itself (a tree, a menu, a radio group, a grid), each
|
|
30
|
+
arrow moved inside the widget and also scrolled the content. A key a widget already handled is left
|
|
31
|
+
alone, and Space on a button or link presses it.
|
|
32
|
+
|
|
3
33
|
## [0.34.1] - 2026-10-05
|
|
4
34
|
|
|
5
35
|
### 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 {};
|
|
@@ -30,23 +30,40 @@ function E(e) {
|
|
|
30
30
|
let n = e.getAttribute("role");
|
|
31
31
|
return n === "textbox" || n === "searchbox" || n === "combobox" || n === "spinbutton";
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
var D = /* @__PURE__ */ new Set([
|
|
34
|
+
"button",
|
|
35
|
+
"link",
|
|
36
|
+
"checkbox",
|
|
37
|
+
"switch",
|
|
38
|
+
"radio",
|
|
39
|
+
"menuitem",
|
|
40
|
+
"menuitemcheckbox",
|
|
41
|
+
"menuitemradio",
|
|
42
|
+
"option",
|
|
43
|
+
"tab",
|
|
44
|
+
"treeitem"
|
|
45
|
+
]);
|
|
46
|
+
function O(e) {
|
|
47
|
+
let t = e.tagName;
|
|
48
|
+
return t === "BUTTON" || t === "SUMMARY" || t === "A" && e.hasAttribute("href") ? !0 : D.has(e.getAttribute("role") ?? "");
|
|
49
|
+
}
|
|
50
|
+
function k(e) {
|
|
34
51
|
let t = getComputedStyle(e);
|
|
35
52
|
return (t.overflowY === "auto" || t.overflowY === "scroll") && e.scrollHeight > e.clientHeight;
|
|
36
53
|
}
|
|
37
|
-
function
|
|
54
|
+
function A(e, t) {
|
|
38
55
|
for (let n of e) if (n instanceof HTMLElement) {
|
|
39
|
-
if (
|
|
56
|
+
if (k(n)) return n === t;
|
|
40
57
|
if (n === t) break;
|
|
41
58
|
}
|
|
42
59
|
return !0;
|
|
43
60
|
}
|
|
44
|
-
function
|
|
61
|
+
function j() {
|
|
45
62
|
let e = document.activeElement;
|
|
46
63
|
for (; e?.shadowRoot?.activeElement;) e = e.shadowRoot.activeElement;
|
|
47
64
|
return e;
|
|
48
65
|
}
|
|
49
|
-
function
|
|
66
|
+
function M(e, t) {
|
|
50
67
|
let n = t;
|
|
51
68
|
for (; n;) {
|
|
52
69
|
if (n === e) return !0;
|
|
@@ -54,7 +71,7 @@ function A(e, t) {
|
|
|
54
71
|
}
|
|
55
72
|
return !1;
|
|
56
73
|
}
|
|
57
|
-
var
|
|
74
|
+
var N = class extends i {
|
|
58
75
|
static {
|
|
59
76
|
C = this;
|
|
60
77
|
}
|
|
@@ -85,7 +102,7 @@ var j = class extends i {
|
|
|
85
102
|
}, this.handleOverlaySlotChange = (e) => {
|
|
86
103
|
let t = o(e.target);
|
|
87
104
|
if (t && !this.hasOverlay) {
|
|
88
|
-
let e =
|
|
105
|
+
let e = j();
|
|
89
106
|
this.overlayTrigger = e && e !== document.body ? e : null;
|
|
90
107
|
}
|
|
91
108
|
this.hasOverlay = t;
|
|
@@ -97,10 +114,11 @@ var j = class extends i {
|
|
|
97
114
|
}, this.handleOverlayClose = () => {
|
|
98
115
|
this.fire("overlay-close", { cancelable: !1 });
|
|
99
116
|
}, this._handleMainKeydown = (e) => {
|
|
117
|
+
if (e.defaultPrevented) return;
|
|
100
118
|
let t = e.composedPath()[0];
|
|
101
|
-
if (t instanceof HTMLElement && E(t)) return;
|
|
119
|
+
if (t instanceof HTMLElement && E(t) || e.key === " " && t instanceof HTMLElement && O(t)) return;
|
|
102
120
|
let n = this.shadowRoot?.querySelector(".main");
|
|
103
|
-
if (!n || !
|
|
121
|
+
if (!n || !A(e.composedPath(), n)) return;
|
|
104
122
|
let r = n.clientHeight;
|
|
105
123
|
switch (e.key) {
|
|
106
124
|
case " ":
|
|
@@ -350,14 +368,14 @@ var j = class extends i {
|
|
|
350
368
|
async focusRoute(e) {
|
|
351
369
|
let t = this.routeContent();
|
|
352
370
|
if (await Promise.all(t.map((e) => e.updateComplete)), this.hasOverlay || !e.isConnected) return;
|
|
353
|
-
let n =
|
|
354
|
-
n && t.some((e) =>
|
|
371
|
+
let n = j();
|
|
372
|
+
n && t.some((e) => M(e, n)) || this.focusContentStart(t, e);
|
|
355
373
|
}
|
|
356
374
|
focusContentStart(e, t) {
|
|
357
375
|
let n = b(e, "[autofocus]", y);
|
|
358
376
|
n?.focus();
|
|
359
|
-
let r =
|
|
360
|
-
n && r &&
|
|
377
|
+
let r = j();
|
|
378
|
+
n && r && M(n, r) || t.focus({ preventScroll: !0 });
|
|
361
379
|
}
|
|
362
380
|
overlayPanels() {
|
|
363
381
|
return (this.shadowRoot?.querySelector("slot[name=\"overlay\"]"))?.assignedElements({ flatten: !0 }) ?? [];
|
|
@@ -365,19 +383,19 @@ var j = class extends i {
|
|
|
365
383
|
async focusOverlay() {
|
|
366
384
|
let e = this.overlayPanels();
|
|
367
385
|
if (await Promise.all(e.map((e) => e.updateComplete)), !this.hasOverlay) return;
|
|
368
|
-
let t =
|
|
369
|
-
t && e.some((e) =>
|
|
386
|
+
let t = j();
|
|
387
|
+
t && e.some((e) => M(e, t)) || (b(e, "[autofocus]", y) ?? b(e, "input, select, textarea, u-input, u-textarea, u-select, u-checkbox, u-radio, u-switch, u-slider", y) ?? this.shadowRoot?.querySelector(".overlay-close"))?.focus();
|
|
370
388
|
}
|
|
371
389
|
restoreOverlayFocus() {
|
|
372
390
|
let e = this.overlayTrigger;
|
|
373
391
|
this.overlayTrigger = null;
|
|
374
|
-
let t =
|
|
392
|
+
let t = j();
|
|
375
393
|
t && t !== document.body || (e?.isConnected ? e.focus() : this.mainElement?.focus({ preventScroll: !0 }));
|
|
376
394
|
}
|
|
377
395
|
};
|
|
378
396
|
r([f({
|
|
379
397
|
type: String,
|
|
380
398
|
reflect: !0
|
|
381
|
-
}), n("design:type", Object)],
|
|
399
|
+
}), n("design:type", Object)], N.prototype, "state", void 0), r([f({ type: Object }), n("design:type", Object)], N.prototype, "config", void 0), r([f({ type: Object }), n("design:type", typeof Record > "u" ? Object : Record)], N.prototype, "counts", void 0), r([f({ type: String }), n("design:type", Object)], N.prototype, "locale", void 0), r([p("u-progress-bar"), n("design:type", S === void 0 ? Object : S)], N.prototype, "progressBarEl", void 0), r([m(), n("design:type", Object)], N.prototype, "hasOverlay", void 0), r([m(), n("design:type", Object)], N.prototype, "hasNotice", void 0), r([m(), n("design:type", Object)], N.prototype, "context", void 0), N = C = r([d("u-sidebar-layout")], N);
|
|
382
400
|
//#endregion
|
|
383
|
-
export {
|
|
401
|
+
export { N as SidebarLayout };
|
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
|