@iyulab/modern-app 0.20.0 → 0.21.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 +44 -0
- package/README.md +3 -3
- package/dist/components/MasterDetailLayout.d.ts +10 -0
- package/dist/components/MasterDetailLayout.js +2 -2
- package/dist/components/MasterDetailLayout.styles.js +25 -0
- package/dist/components/SidebarButton.js +8 -9
- package/dist/components/SidebarButton.styles.js +16 -0
- package/dist/components/SidebarGroup.js +9 -11
- package/dist/components/SidebarGroup.styles.js +16 -0
- package/dist/components/SidebarLink.js +1 -3
- package/dist/components/SidebarLink.styles.js +16 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -2
- package/dist/translate.d.ts +39 -0
- package/dist/translate.js +31 -0
- package/package.json +2 -2
- package/skills/modern-app/SKILL.md +5 -2
- package/skills/modern-app/references/components/master-detail-layout.md +18 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.21.0] - 2026-09-17
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`translate()` — a reactive translation directive**, exported from `@iyulab/modern-app`. It is
|
|
8
|
+
bound to the i18next instance `app.load({ i18n })` initializes and re-renders its part when the
|
|
9
|
+
language changes, when resources arrive later, and once i18next finishes initializing (until then
|
|
10
|
+
it renders an empty string rather than the key). A disconnected template stops listening and
|
|
11
|
+
catches up when reconnected. Sidebar item labels already accept a directive result, so
|
|
12
|
+
`label: translate('nav::home')` now works without a third-party package; the documentation
|
|
13
|
+
previously pointed to `lit-i18n` for this. Migrating is an import swap plus dropping
|
|
14
|
+
`initLitI18n` from `i18n.plugins`.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **A collapsed sidebar no longer leaves translated items without an accessible name.** In the
|
|
19
|
+
collapsed state the item label was removed with `hidden`, which also removes it from the
|
|
20
|
+
accessibility tree; an `aria-label` was substituted only when the label was a plain string, so an
|
|
21
|
+
item labelled with a directive was announced with no name. The label is now hidden visually and
|
|
22
|
+
stays in the accessibility tree, so it names the link, button or group whatever its type. Items
|
|
23
|
+
no longer carry an `aria-label` in the collapsed state.
|
|
24
|
+
|
|
25
|
+
## [0.20.1] - 2026-09-17
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **`u-master-detail-layout` no longer adds a trailing blank page when a single pane is printed.**
|
|
30
|
+
The layout is a flex row with scrolling panes, and both keep the bottom margin of a pane's last
|
|
31
|
+
block inside the layout, so the layout grew by that margin; content ending just short of a page
|
|
32
|
+
boundary then spilled onto a page holding only the margin. When a single pane is showing
|
|
33
|
+
(overlay mode, or no detail) the layout now prints as block flow, which lets the margin collapse
|
|
34
|
+
past it and be truncated at the page break. Side-by-side printing is unchanged: two columns are
|
|
35
|
+
flex items and each keeps its own margin.
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- `u-master-detail-layout` exposes a `:state(detail)` custom state while its `detail` slot has
|
|
40
|
+
content.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- Requires `@iyulab/router` `^0.15.1`, which prints the route outlet as a block box. The shell's
|
|
45
|
+
guarantee that a screen's trailing margin collapses out of the page in print depends on it.
|
|
46
|
+
|
|
3
47
|
## [0.20.0] - 2026-09-16
|
|
4
48
|
|
|
5
49
|
### Added
|
package/README.md
CHANGED
|
@@ -113,8 +113,8 @@ await app.load({
|
|
|
113
113
|
app.i18n.t('common::greeting');
|
|
114
114
|
app.i18n.changeLanguage('ko');
|
|
115
115
|
|
|
116
|
-
// Reactive translations in Lit templates
|
|
117
|
-
import { translate } from '
|
|
116
|
+
// Reactive translations in Lit templates — and in shell labels
|
|
117
|
+
import { translate } from '@iyulab/modern-app';
|
|
118
118
|
html`<p>${translate('common::greeting')}</p>`;
|
|
119
119
|
```
|
|
120
120
|
|
|
@@ -263,7 +263,7 @@ contrast likewise comes from that package's tokens.
|
|
|
263
263
|
| [layout.md](./docs/layout.md) | Sidebar layout, all menu item types, responsive behaviour |
|
|
264
264
|
| [theme.md](./docs/theme.md) | Theme init, runtime switching, token layers, shell surface tokens |
|
|
265
265
|
| [notifications.md](./docs/notifications.md) | Toast methods and options |
|
|
266
|
-
| [i18n.md](./docs/i18n.md) | i18next setup, plugins,
|
|
266
|
+
| [i18n.md](./docs/i18n.md) | i18next setup, plugins, the `translate()` directive |
|
|
267
267
|
| [configuration.md](./docs/configuration.md) | Full TypeScript interface reference |
|
|
268
268
|
|
|
269
269
|
## License
|
|
@@ -32,6 +32,9 @@ type ElementParts = 'host' | 'master' | 'divider' | 'detail' | 'detail-close';
|
|
|
32
32
|
* `PageHeader`/`GroupBox`/`ActionBar` 의 고정 중단점 관행을 벗어나려는 것이 아니다.
|
|
33
33
|
*
|
|
34
34
|
* 오버라이드: `part`(host·master·divider·detail·detail-close) + slot 치환.
|
|
35
|
+
*
|
|
36
|
+
* detail 슬롯이 채워져 있는 동안 `:state(detail)` 커스텀 상태를 노출한다
|
|
37
|
+
* (`ElementInternals.states`) — 속성으로 반사하지 않는다(`u-select:state(open)` 과 같은 관례).
|
|
35
38
|
*/
|
|
36
39
|
export declare class MasterDetailLayout extends StyledElement<ElementParts> {
|
|
37
40
|
static styles: import('lit').CSSResultGroup[];
|
|
@@ -50,6 +53,13 @@ export declare class MasterDetailLayout extends StyledElement<ElementParts> {
|
|
|
50
53
|
locale: string;
|
|
51
54
|
/** detail 슬롯 배정 상태 — CSS `:has()` 로는 알 수 없다(`internals/slotted.ts` 참조). */
|
|
52
55
|
private hasDetail;
|
|
56
|
+
/**
|
|
57
|
+
* `:state(detail)` 을 싣는 자리. 인쇄 규칙이 «한 판만 보이는가» 를 호스트에서 갈라야 해서
|
|
58
|
+
* 생겼다(styles 의 인쇄 절 참조).
|
|
59
|
+
* ⚠생성자에서 한 번만 붙인다 — `attachInternals()` 는 같은 인스턴스에 두 번 부르면
|
|
60
|
+
* `NotSupportedError` 이고, `connectedCallback` 은 재연결마다 다시 돈다.
|
|
61
|
+
*/
|
|
62
|
+
private readonly internals;
|
|
53
63
|
private resizeObserver?;
|
|
54
64
|
connectedCallback(): void;
|
|
55
65
|
/**
|
|
@@ -11,7 +11,7 @@ import "@iyulab/components/dist/components/icon/UIcon.js";
|
|
|
11
11
|
//#region src/components/MasterDetailLayout.ts
|
|
12
12
|
var u = class extends n {
|
|
13
13
|
constructor(...e) {
|
|
14
|
-
super(...e), this.masterSize = "22rem", this.overlayBreakpoint = 760, this.locale = "", this.hasDetail = !1, this.handleDetailClose = () => {
|
|
14
|
+
super(...e), this.masterSize = "22rem", this.overlayBreakpoint = 760, this.locale = "", this.hasDetail = !1, this.internals = typeof this.attachInternals == "function" ? this.attachInternals() : void 0, this.handleDetailClose = () => {
|
|
15
15
|
this.fire("detail-close", { cancelable: !1 });
|
|
16
16
|
}, this.handleDetailSlotChange = (e) => {
|
|
17
17
|
this.hasDetail = i(e.target);
|
|
@@ -32,7 +32,7 @@ var u = class extends n {
|
|
|
32
32
|
super.disconnectedCallback(), this.resizeObserver?.disconnect();
|
|
33
33
|
}
|
|
34
34
|
updated(e) {
|
|
35
|
-
super.updated(e), e.has("masterSize") && this.style.setProperty("--_master-size", this.masterSize), e.has("overlayBreakpoint") && this.applyOverlay(this.getBoundingClientRect().width);
|
|
35
|
+
super.updated(e), e.has("masterSize") && this.style.setProperty("--_master-size", this.masterSize), e.has("hasDetail") && this.internals?.states?.[this.hasDetail ? "add" : "delete"]("detail"), e.has("overlayBreakpoint") && this.applyOverlay(this.getBoundingClientRect().width);
|
|
36
36
|
}
|
|
37
37
|
render() {
|
|
38
38
|
return o`
|
|
@@ -74,8 +74,33 @@ var t = e`
|
|
|
74
74
|
* 되돌려 전부 인쇄한다 — 덮여 있던 master 는 찍지 않는다. detail 이 비어 있으면 master 가
|
|
75
75
|
* 화면 그대로 인쇄된다(covered 는 detail 이 채워졌을 때만 붙는다).
|
|
76
76
|
* 나란히 배치 모드는 바꾸지 않는다 — 두 판이 부모 높이를 따라 풀린다.
|
|
77
|
+
*
|
|
78
|
+
* 🔴**한 판만 보일 때는 flex 뼈대를 걷는다**(오버레이 모드 · detail 없음). flex 호스트와
|
|
79
|
+
* overflow: auto 판은 둘 다 독립 서식 문맥이라, 화면 마지막 블록의 아래 여백이 레이아웃
|
|
80
|
+
* «안» 에 갇혀 그만큼 높이가 는다 — 내용 끝이 쪽 경계에서 그 여백 이내에 있으면 여백만 담긴
|
|
81
|
+
* 빈 꼬리 쪽이 찍힌다(router 아웃렛의 인쇄 수정과 같은 기전). 한 판뿐이면 flex 가 인쇄에서
|
|
82
|
+
* 할 일이 없으므로 블록 흐름으로 돌려 여백이 문서 끝까지 접히게 한다(쪽 경계에서 잘린다).
|
|
83
|
+
* ⚠**나란히 배치(detail 있음 · 오버레이 아님)는 원리적으로 가둔다** — 두 열은 flex 항목이고
|
|
84
|
+
* flex 항목은 항상 독립 서식 문맥이다. 화면에 보인 두 열을 그대로 찍는 것이 계약이다.
|
|
85
|
+
* ⚠:state() 를 모르는 엔진에서는 그 선택자만 무효가 되도록 **규칙을 나눴다** — 선택자 목록에
|
|
86
|
+
* 섞으면 목록 전체가 버려져 오버레이 규칙까지 사라진다.
|
|
77
87
|
*/
|
|
78
88
|
@media print {
|
|
89
|
+
:host([overlay]) {
|
|
90
|
+
display: block;
|
|
91
|
+
height: auto;
|
|
92
|
+
}
|
|
93
|
+
:host(:not(:state(detail))) {
|
|
94
|
+
display: block;
|
|
95
|
+
height: auto;
|
|
96
|
+
}
|
|
97
|
+
:host([overlay]) .master,
|
|
98
|
+
:host([overlay]) .detail {
|
|
99
|
+
overflow: visible;
|
|
100
|
+
}
|
|
101
|
+
:host(:not(:state(detail))) .master {
|
|
102
|
+
overflow: visible;
|
|
103
|
+
}
|
|
79
104
|
.detail-close,
|
|
80
105
|
:host([overlay]) .detail-close,
|
|
81
106
|
:host([overlay]) .master.covered {
|
|
@@ -3,11 +3,11 @@ import t from "../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js
|
|
|
3
3
|
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
4
|
import { DEFAULT_NAV_ICON as r } from "../internals/nav-icon.js";
|
|
5
5
|
import { styles as i } from "./SidebarButton.styles.js";
|
|
6
|
-
import { html as a
|
|
7
|
-
import { customElement as
|
|
6
|
+
import { html as a } from "lit";
|
|
7
|
+
import { customElement as o, property as s } from "lit/decorators.js";
|
|
8
8
|
import "@iyulab/components/dist/components/icon/UIcon.js";
|
|
9
9
|
//#region src/components/SidebarButton.ts
|
|
10
|
-
var
|
|
10
|
+
var c = class extends n {
|
|
11
11
|
constructor(...e) {
|
|
12
12
|
super(...e), this.compact = !1;
|
|
13
13
|
}
|
|
@@ -15,24 +15,23 @@ var l = class extends n {
|
|
|
15
15
|
this.styles = [super.styles, i];
|
|
16
16
|
}
|
|
17
17
|
render() {
|
|
18
|
-
let e = this.compact && typeof this.label == "string" ? this.label : o;
|
|
19
18
|
return a`
|
|
20
|
-
<button part="base" ?compact=${this.compact}
|
|
19
|
+
<button part="base" ?compact=${this.compact}>
|
|
21
20
|
<u-icon part="icon"
|
|
22
21
|
.lib=${this.lib}
|
|
23
22
|
.name=${this.icon}
|
|
24
23
|
.fallback=${r}
|
|
25
24
|
></u-icon>
|
|
26
|
-
<span part="label" ?
|
|
25
|
+
<span part="label" ?compact=${this.compact}>
|
|
27
26
|
${this.label}
|
|
28
27
|
</span>
|
|
29
28
|
</button>
|
|
30
29
|
`;
|
|
31
30
|
}
|
|
32
31
|
};
|
|
33
|
-
t([
|
|
32
|
+
t([s({
|
|
34
33
|
type: Boolean,
|
|
35
34
|
reflect: !0
|
|
36
|
-
}), e("design:type", Object)],
|
|
35
|
+
}), e("design:type", Object)], c.prototype, "compact", void 0), t([s({ type: String }), e("design:type", String)], c.prototype, "icon", void 0), t([s({ type: String }), e("design:type", String)], c.prototype, "lib", void 0), t([s({ type: String }), e("design:type", Object)], c.prototype, "label", void 0), c = t([o("u-sidebar-button")], c);
|
|
37
36
|
//#endregion
|
|
38
|
-
export {
|
|
37
|
+
export { c as SidebarButton };
|
|
@@ -55,6 +55,22 @@ var t = e`
|
|
|
55
55
|
overflow: hidden;
|
|
56
56
|
text-overflow: ellipsis;
|
|
57
57
|
}
|
|
58
|
+
|
|
59
|
+
/*
|
|
60
|
+
* 접힌 상태의 라벨 - 시각적으로만 숨긴다. 접근성 트리에는 남아 항목의 이름이 된다
|
|
61
|
+
* (hidden 은 트리에서도 빼서 이름 없는 항목을 만들었다). 절대배치라 flex 배치와 gap 에 끼지 않는다.
|
|
62
|
+
*/
|
|
63
|
+
[part~='label'][compact] {
|
|
64
|
+
position: absolute;
|
|
65
|
+
width: 1px;
|
|
66
|
+
height: 1px;
|
|
67
|
+
padding: 0;
|
|
68
|
+
margin: -1px;
|
|
69
|
+
overflow: hidden;
|
|
70
|
+
clip-path: inset(50%);
|
|
71
|
+
white-space: nowrap;
|
|
72
|
+
border: 0;
|
|
73
|
+
}
|
|
58
74
|
`;
|
|
59
75
|
//#endregion
|
|
60
76
|
export { t as styles };
|
|
@@ -4,11 +4,11 @@ import { StyledElement as n } from "../internals/StyledElement.js";
|
|
|
4
4
|
import { DEFAULT_NAV_ICON as r } from "../internals/nav-icon.js";
|
|
5
5
|
import { SidebarLink as i } from "./SidebarLink.js";
|
|
6
6
|
import { styles as a } from "./SidebarGroup.styles.js";
|
|
7
|
-
import { html as o
|
|
8
|
-
import { customElement as
|
|
7
|
+
import { html as o } from "lit";
|
|
8
|
+
import { customElement as s, property as c } from "lit/decorators.js";
|
|
9
9
|
import "@iyulab/components/dist/components/icon/UIcon.js";
|
|
10
10
|
//#region src/components/SidebarGroup.ts
|
|
11
|
-
var
|
|
11
|
+
var l = class extends n {
|
|
12
12
|
constructor(...e) {
|
|
13
13
|
super(...e), this.compact = !1, this.selected = !1, this.collapsed = !0, this.handleButtonClick = (e) => {
|
|
14
14
|
this.compact ? ((this.shadowRoot?.querySelector("slot")?.assignedElements({ flatten: !0 }).find((e) => e instanceof i))?.renderRoot.querySelector("u-link"))?.click() : this.collapsed = !this.collapsed;
|
|
@@ -18,12 +18,10 @@ var u = class extends n {
|
|
|
18
18
|
this.styles = [super.styles, a];
|
|
19
19
|
}
|
|
20
20
|
render() {
|
|
21
|
-
let e = this.compact && typeof this.label == "string" ? this.label : s;
|
|
22
21
|
return o`
|
|
23
22
|
<button part="header"
|
|
24
23
|
?compact=${this.compact}
|
|
25
24
|
?selected=${this.selected}
|
|
26
|
-
aria-label=${e}
|
|
27
25
|
@click=${this.handleButtonClick}>
|
|
28
26
|
<!--
|
|
29
27
|
⚠ ?hidden 을 걸지 않는다. 접힌 상태에서는 라벨과 캐럿이 모두 숨으므로,
|
|
@@ -36,7 +34,7 @@ var u = class extends n {
|
|
|
36
34
|
.name=${this.icon}
|
|
37
35
|
.fallback=${r}
|
|
38
36
|
></u-icon>
|
|
39
|
-
<span class="label" part="label" ?
|
|
37
|
+
<span class="label" part="label" ?compact=${this.compact}>
|
|
40
38
|
${this.label}
|
|
41
39
|
</span>
|
|
42
40
|
<u-icon class="caret" part="caret" ?hidden=${this.compact}
|
|
@@ -53,15 +51,15 @@ var u = class extends n {
|
|
|
53
51
|
`;
|
|
54
52
|
}
|
|
55
53
|
};
|
|
56
|
-
t([
|
|
54
|
+
t([c({
|
|
57
55
|
type: Boolean,
|
|
58
56
|
reflect: !0
|
|
59
|
-
}), e("design:type", Boolean)],
|
|
57
|
+
}), e("design:type", Boolean)], l.prototype, "compact", void 0), t([c({
|
|
60
58
|
type: Boolean,
|
|
61
59
|
reflect: !0
|
|
62
|
-
}), e("design:type", Boolean)],
|
|
60
|
+
}), e("design:type", Boolean)], l.prototype, "selected", void 0), t([c({
|
|
63
61
|
type: Boolean,
|
|
64
62
|
reflect: !0
|
|
65
|
-
}), e("design:type", Boolean)],
|
|
63
|
+
}), e("design:type", Boolean)], l.prototype, "collapsed", void 0), t([c({ type: String }), e("design:type", String)], l.prototype, "icon", void 0), t([c({ type: String }), e("design:type", String)], l.prototype, "lib", void 0), t([c({ type: String }), e("design:type", Object)], l.prototype, "label", void 0), l = t([s("u-sidebar-group")], l);
|
|
66
64
|
//#endregion
|
|
67
|
-
export {
|
|
65
|
+
export { l as SidebarGroup };
|
|
@@ -93,6 +93,22 @@ var t = e`
|
|
|
93
93
|
height: 0;
|
|
94
94
|
opacity: 0;
|
|
95
95
|
}
|
|
96
|
+
|
|
97
|
+
/*
|
|
98
|
+
* 접힌 상태의 라벨 - 시각적으로만 숨긴다. 접근성 트리에는 남아 항목의 이름이 된다
|
|
99
|
+
* (hidden 은 트리에서도 빼서 이름 없는 항목을 만들었다). 절대배치라 flex 배치와 gap 에 끼지 않는다.
|
|
100
|
+
*/
|
|
101
|
+
[part~='label'][compact] {
|
|
102
|
+
position: absolute;
|
|
103
|
+
width: 1px;
|
|
104
|
+
height: 1px;
|
|
105
|
+
padding: 0;
|
|
106
|
+
margin: -1px;
|
|
107
|
+
overflow: hidden;
|
|
108
|
+
clip-path: inset(50%);
|
|
109
|
+
white-space: nowrap;
|
|
110
|
+
border: 0;
|
|
111
|
+
}
|
|
96
112
|
`;
|
|
97
113
|
//#endregion
|
|
98
114
|
export { t as styles };
|
|
@@ -15,14 +15,12 @@ var l = class extends n {
|
|
|
15
15
|
this.styles = [super.styles, i];
|
|
16
16
|
}
|
|
17
17
|
render() {
|
|
18
|
-
let e = this.compact && typeof this.label == "string" ? this.label : o;
|
|
19
18
|
return a`
|
|
20
19
|
<u-link
|
|
21
20
|
.href=${this.href || "#"}
|
|
22
21
|
.navigate=${this.navigate}
|
|
23
22
|
.target=${this.target}
|
|
24
23
|
aria-current=${this.selected ? "page" : o}
|
|
25
|
-
aria-label=${e}
|
|
26
24
|
>
|
|
27
25
|
<div class="container" part="base" ?compact=${this.compact}>
|
|
28
26
|
<u-icon part="icon"
|
|
@@ -30,7 +28,7 @@ var l = class extends n {
|
|
|
30
28
|
.name=${this.icon}
|
|
31
29
|
.fallback=${r}
|
|
32
30
|
></u-icon>
|
|
33
|
-
<span part="label" ?
|
|
31
|
+
<span part="label" ?compact=${this.compact}>
|
|
34
32
|
${this.label}
|
|
35
33
|
</span>
|
|
36
34
|
</div>
|
|
@@ -69,6 +69,22 @@ var t = e`
|
|
|
69
69
|
overflow: hidden;
|
|
70
70
|
text-overflow: ellipsis;
|
|
71
71
|
}
|
|
72
|
+
|
|
73
|
+
/*
|
|
74
|
+
* 접힌 상태의 라벨 - 시각적으로만 숨긴다. 접근성 트리에는 남아 항목의 이름이 된다
|
|
75
|
+
* (hidden 은 트리에서도 빼서 이름 없는 항목을 만들었다). 절대배치라 flex 배치와 gap 에 끼지 않는다.
|
|
76
|
+
*/
|
|
77
|
+
[part~='label'][compact] {
|
|
78
|
+
position: absolute;
|
|
79
|
+
width: 1px;
|
|
80
|
+
height: 1px;
|
|
81
|
+
padding: 0;
|
|
82
|
+
margin: -1px;
|
|
83
|
+
overflow: hidden;
|
|
84
|
+
clip-path: inset(50%);
|
|
85
|
+
white-space: nowrap;
|
|
86
|
+
border: 0;
|
|
87
|
+
}
|
|
72
88
|
`;
|
|
73
89
|
//#endregion
|
|
74
90
|
export { t as styles };
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export { ActionBar } from './components/ActionBar.js';
|
|
|
13
13
|
export { MasterDetailLayout } from './components/MasterDetailLayout.js';
|
|
14
14
|
export { Wizard } from './components/Wizard.js';
|
|
15
15
|
export type { WizardStep, WizardStepState, WizardStepChangeDetail } from './components/Wizard.js';
|
|
16
|
+
export { translate } from './translate.js';
|
|
16
17
|
export { registerLocale, setDefaultLocale, getLocaleStrings, getDefaultLocale, } from './internals/locale.js';
|
|
17
18
|
export type { ModernAppLocaleStrings } from './internals/locale.js';
|
|
18
19
|
export { ScreenObserver } from './internals/ScreenObserver.js';
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,8 @@ import { EmptyState as f } from "./components/EmptyState.js";
|
|
|
10
10
|
import { ActionBar as p } from "./components/ActionBar.js";
|
|
11
11
|
import { MasterDetailLayout as m } from "./components/MasterDetailLayout.js";
|
|
12
12
|
import { Wizard as h } from "./components/Wizard.js";
|
|
13
|
+
import { translate as g } from "./translate.js";
|
|
13
14
|
//#region src/index.ts
|
|
14
|
-
var
|
|
15
|
+
var _ = t;
|
|
15
16
|
//#endregion
|
|
16
|
-
export { p as ActionBar, f as EmptyState, c as GroupBox, u as InfoField, l as InfoSection, m as MasterDetailLayout, s as PageHeader, e as ScreenObserver, h as Wizard, t as app,
|
|
17
|
+
export { p as ActionBar, f as EmptyState, c as GroupBox, u as InfoField, l as InfoSection, m as MasterDetailLayout, s as PageHeader, e as ScreenObserver, h as Wizard, t as app, _ as default, n as filterSidebarItems, r as getDefaultLocale, i as getLocaleStrings, d as isBlank, a as registerLocale, o as setDefaultLocale, g as translate };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { AsyncDirective } from 'lit/async-directive.js';
|
|
2
|
+
import { TOptions } from 'i18next';
|
|
3
|
+
/**
|
|
4
|
+
* `app.load({ i18n })` 가 초기화하는 i18next 에 붙는 반응형 번역 디렉티브.
|
|
5
|
+
*
|
|
6
|
+
* ★**왜 이 패키지가 갖는가**: i18next 를 초기화하고(`app.load`) 노출하는(`app.i18n`) 것이 이미 이
|
|
7
|
+
* 패키지이고, 셸 설정의 `label` 은 `string | DirectiveResult` 로 **디렉티브를 받도록** 설계돼 있다.
|
|
8
|
+
* 그런데 그 디렉티브가 없어서 소비자는 서드파티를 따로 들여와야 했다 — 이미 소유한 개념의
|
|
9
|
+
* 프리미티브를 내놓지 않은 자리다.
|
|
10
|
+
*
|
|
11
|
+
* ★**구독은 Lit 의 연결 수명 주기로 관리한다** — `disconnected()` 에서 풀고 `reconnected()` 에서 다시
|
|
12
|
+
* 붙으며 그 사이의 변경을 따라잡는다. 주기적으로 끊긴 파트를 훑는 레지스트리가 필요 없다.
|
|
13
|
+
*
|
|
14
|
+
* 갱신 계기는 셋이다: 언어 변경 · 나중에 도착한 리소스(백엔드가 네임스페이스를 늦게 싣는 경우) ·
|
|
15
|
+
* 초기화 완료(셸 설정은 초기화 **전에** 만들어진다 — 그때는 빈 문자열을 그린다. 키를 그리면
|
|
16
|
+
* 번역 전 키가 화면에 잠깐 비친다).
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { translate } from '@iyulab/modern-app';
|
|
21
|
+
* html`<h1>${translate('common::title')}</h1>`;
|
|
22
|
+
* app.load({ layout: { type: 'sidebar', main: [{ type: 'link', label: translate('nav::home'), href: '/' }] } });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
declare class TranslateDirective extends AsyncDirective {
|
|
26
|
+
private keys;
|
|
27
|
+
private options?;
|
|
28
|
+
private subscribed;
|
|
29
|
+
render(keys: string | string[], options?: TOptions): string;
|
|
30
|
+
protected disconnected(): void;
|
|
31
|
+
protected reconnected(): void;
|
|
32
|
+
private translate;
|
|
33
|
+
private readonly refresh;
|
|
34
|
+
private subscribe;
|
|
35
|
+
private unsubscribe;
|
|
36
|
+
}
|
|
37
|
+
/** 현재 언어의 번역을 그리고, 언어·리소스가 바뀌면 그 자리를 다시 그린다. */
|
|
38
|
+
export declare const translate: (keys: string | string[], options?: TOptions | undefined) => import('lit-html/directive.js').DirectiveResult<typeof TranslateDirective>;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import e from "i18next";
|
|
2
|
+
import { AsyncDirective as t, directive as n } from "lit/async-directive.js";
|
|
3
|
+
var r = n(class extends t {
|
|
4
|
+
constructor(...e) {
|
|
5
|
+
super(...e), this.keys = "", this.subscribed = !1, this.refresh = () => {
|
|
6
|
+
this.setValue(this.translate());
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
render(e, t) {
|
|
10
|
+
return this.keys = e, this.options = t, this.isConnected && this.subscribe(), this.translate();
|
|
11
|
+
}
|
|
12
|
+
disconnected() {
|
|
13
|
+
this.unsubscribe();
|
|
14
|
+
}
|
|
15
|
+
reconnected() {
|
|
16
|
+
this.subscribe(), this.refresh();
|
|
17
|
+
}
|
|
18
|
+
translate() {
|
|
19
|
+
if (!e.isInitialized) return "";
|
|
20
|
+
let t = e.t(this.keys, this.options);
|
|
21
|
+
return typeof t == "string" ? t : "";
|
|
22
|
+
}
|
|
23
|
+
subscribe() {
|
|
24
|
+
this.subscribed || (this.subscribed = !0, e.on("languageChanged", this.refresh), e.on("initialized", this.refresh), e.store?.on("added", this.refresh));
|
|
25
|
+
}
|
|
26
|
+
unsubscribe() {
|
|
27
|
+
this.subscribed && (this.subscribed = !1, e.off("languageChanged", this.refresh), e.off("initialized", this.refresh), e.store?.off("added", this.refresh));
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
//#endregion
|
|
31
|
+
export { r as translate };
|
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.21.0",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"iyulab",
|
|
7
7
|
"web-framework",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"test:react-types": "tsc -p tsconfig.react-smoke.json"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@iyulab/router": "^0.15.
|
|
62
|
+
"@iyulab/router": "^0.15.1",
|
|
63
63
|
"i18next": "^26.3.6",
|
|
64
64
|
"lit": "^3.3.3"
|
|
65
65
|
},
|
|
@@ -131,11 +131,14 @@ await app.load({
|
|
|
131
131
|
app.i18n.t('namespace::key');
|
|
132
132
|
```
|
|
133
133
|
|
|
134
|
-
Use in Lit templates with `
|
|
134
|
+
Use in Lit templates — and as a sidebar item `label` — with the `translate()` directive. It
|
|
135
|
+
re-renders when the language changes or resources arrive, and renders an empty string until i18next
|
|
136
|
+
is initialized:
|
|
135
137
|
|
|
136
138
|
```typescript
|
|
137
|
-
import { translate } from '
|
|
139
|
+
import { translate } from '@iyulab/modern-app';
|
|
138
140
|
html`<p>${translate('namespace::greeting')}</p>`;
|
|
141
|
+
// layout: { main: [{ type: 'link', label: translate('nav::home'), href: '/' }] }
|
|
139
142
|
```
|
|
140
143
|
|
|
141
144
|
---
|
|
@@ -59,6 +59,14 @@ In overlay mode, a filled detail prints in full, in flow, instead of the covered
|
|
|
59
59
|
close button is not printed; with the detail empty, the master prints. The overlay state is the
|
|
60
60
|
one measured on screen when printing starts.
|
|
61
61
|
|
|
62
|
+
When a single pane is showing (overlay mode, or no detail), the layout prints as ordinary block
|
|
63
|
+
flow rather than a flex row with scrolling panes. Both of those create an independent formatting
|
|
64
|
+
context, which would keep the bottom margin of the pane's last block inside the layout and make
|
|
65
|
+
the layout that much taller — enough, when content ends just short of a page boundary, to print
|
|
66
|
+
a trailing page holding nothing but the margin. In block flow the margin collapses past the layout
|
|
67
|
+
and is truncated at the page break. Side by side, the two panes are flex items and each keeps its
|
|
68
|
+
own margin; that is inherent to printing two columns.
|
|
69
|
+
|
|
62
70
|
## Slots
|
|
63
71
|
|
|
64
72
|
| Name | Description |
|
|
@@ -80,6 +88,16 @@ one measured on screen when printing starts.
|
|
|
80
88
|
|-------|--------|------------|--------------|
|
|
81
89
|
| `detail-close` | — | No | Overlay-mode close button clicked. Clear the `detail` slot's content in response — the component does not do this for you |
|
|
82
90
|
|
|
91
|
+
## Custom States
|
|
92
|
+
|
|
93
|
+
| State | Description |
|
|
94
|
+
|-------|-------------|
|
|
95
|
+
| `:state(detail)` | Present while the `detail` slot has content |
|
|
96
|
+
|
|
97
|
+
```css
|
|
98
|
+
u-master-detail-layout:state(detail)::part(master) { border-inline-end: 0; }
|
|
99
|
+
```
|
|
100
|
+
|
|
83
101
|
## CSS Parts
|
|
84
102
|
|
|
85
103
|
| Part | Description |
|