@iyulab/modern-app 0.20.1 → 0.22.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 +49 -0
- package/README.md +3 -3
- 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/internals/locale.d.ts +1 -1
- package/dist/layouts/SidebarLayout.d.ts +11 -0
- package/dist/layouts/SidebarLayout.js +77 -45
- package/dist/layouts/SidebarLayout.styles.js +52 -1
- package/dist/layouts/SidebarLayout.types.d.ts +1 -1
- package/dist/react.d.ts +3 -1
- package/dist/react.js +1 -1
- package/dist/translate.d.ts +39 -0
- package/dist/translate.js +31 -0
- package/package.json +1 -1
- package/skills/modern-app/SKILL.md +5 -2
- package/skills/modern-app/references/layout.md +40 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.22.0] - 2026-09-18
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`SidebarLayout` gained a `slot="overlay"`** — a route-independent panel shown above the main
|
|
8
|
+
content (e.g. a record detail), independent of the current route, without wrapping every route in
|
|
9
|
+
a shared layout or losing that route's scroll position. Fill the slot to show it; remove or
|
|
10
|
+
reassign the slotted node to dismiss it (`hidden` does not close it). Mirrors
|
|
11
|
+
`u-master-detail-layout`'s existing `slot="detail"` idiom rather than a new one. Adds a
|
|
12
|
+
`:state(overlay)` custom state, `inert` on the route content while the overlay has content, and a
|
|
13
|
+
built-in close button that fires a non-cancelable `overlay-close` event; the React wrapper maps
|
|
14
|
+
this to `onOverlayClose`. Documented in the layout skill reference under "Overlay content".
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **The shell's keyboard scroll handling no longer intercepts a scrollable container nested inside
|
|
19
|
+
route content.** Space, Page Up/Down, Home, End and the arrow keys inside a nested scroll box
|
|
20
|
+
(a detail pane, a split view) were previously captured and cancelled by the shell instead of
|
|
21
|
+
scrolling that box.
|
|
22
|
+
- **The mobile layout (`state="mobile"`/`"mobile-open"`) no longer loses internal scrolling.** The
|
|
23
|
+
shell's scroll region lacked an explicit minimum-size override, so it could grow to the full
|
|
24
|
+
content height and stop scrolling within the viewport instead.
|
|
25
|
+
- **An open overlay prints in normal document flow instead of being clipped** to the main content's
|
|
26
|
+
box, and no longer renders off-screen if opened while the content underneath was scrolled.
|
|
27
|
+
- The documented "Overlay content" example now actually closes the overlay (it previously toggled
|
|
28
|
+
`hidden`, which does not empty the slot).
|
|
29
|
+
|
|
30
|
+
## [0.21.0] - 2026-09-17
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **`translate()` — a reactive translation directive**, exported from `@iyulab/modern-app`. It is
|
|
35
|
+
bound to the i18next instance `app.load({ i18n })` initializes and re-renders its part when the
|
|
36
|
+
language changes, when resources arrive later, and once i18next finishes initializing (until then
|
|
37
|
+
it renders an empty string rather than the key). A disconnected template stops listening and
|
|
38
|
+
catches up when reconnected. Sidebar item labels already accept a directive result, so
|
|
39
|
+
`label: translate('nav::home')` now works without a third-party package; the documentation
|
|
40
|
+
previously pointed to `lit-i18n` for this. Migrating is an import swap plus dropping
|
|
41
|
+
`initLitI18n` from `i18n.plugins`.
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- **A collapsed sidebar no longer leaves translated items without an accessible name.** In the
|
|
46
|
+
collapsed state the item label was removed with `hidden`, which also removes it from the
|
|
47
|
+
accessibility tree; an `aria-label` was substituted only when the label was a plain string, so an
|
|
48
|
+
item labelled with a directive was announced with no name. The label is now hidden visually and
|
|
49
|
+
stays in the accessibility tree, so it names the link, button or group whatever its type. Items
|
|
50
|
+
no longer carry an `aria-label` in the collapsed state.
|
|
51
|
+
|
|
3
52
|
## [0.20.1] - 2026-09-17
|
|
4
53
|
|
|
5
54
|
### Fixed
|
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
|
|
@@ -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 };
|
|
@@ -26,7 +26,7 @@ export interface ModernAppLocaleStrings {
|
|
|
26
26
|
/** Empty state — records exist but the current query matched none. */
|
|
27
27
|
noResultsTitle: string;
|
|
28
28
|
noResultsDescription: string;
|
|
29
|
-
/**
|
|
29
|
+
/** Accessible label for an overlay-mode close button (master-detail layout, sidebar layout overlay). */
|
|
30
30
|
detailClose: string;
|
|
31
31
|
/** Sidebar layout — accessible label for the mobile-header menu toggle button. */
|
|
32
32
|
toggleMobileMenu: string;
|
|
@@ -12,6 +12,14 @@ export declare class SidebarLayout extends StyledElement<SidebarParts> {
|
|
|
12
12
|
/** 크롬 문자열(토글 버튼 접근성 라벨 등) 로케일 — `registerLocale`로 등록한 언어 태그 */
|
|
13
13
|
locale: string;
|
|
14
14
|
progressBarEl: UProgressBar;
|
|
15
|
+
/** overlay 슬롯 배정 상태 — CSS `:has()`로는 알 수 없다(`internals/slotted.ts` 참조). */
|
|
16
|
+
private hasOverlay;
|
|
17
|
+
/**
|
|
18
|
+
* `:state(overlay)`를 싣는 자리. `MasterDetailLayout`과 동일 패턴.
|
|
19
|
+
* ⚠생성자에서 한 번만 붙인다 — `attachInternals()`는 같은 인스턴스에 두 번 부르면
|
|
20
|
+
* `NotSupportedError`이고, `connectedCallback`은 재연결마다 다시 돈다.
|
|
21
|
+
*/
|
|
22
|
+
private readonly internals;
|
|
15
23
|
/** 현재 라우터 컨텍스트 */
|
|
16
24
|
context: RouteContext | null;
|
|
17
25
|
/**
|
|
@@ -24,6 +32,7 @@ export declare class SidebarLayout extends StyledElement<SidebarParts> {
|
|
|
24
32
|
connectedCallback(): void;
|
|
25
33
|
disconnectedCallback(): void;
|
|
26
34
|
protected willUpdate(changedProperties: PropertyValues): void;
|
|
35
|
+
protected updated(changed: PropertyValues): void;
|
|
27
36
|
protected firstUpdated(changedProperties: PropertyValues): void;
|
|
28
37
|
/**
|
|
29
38
|
* 개발 모드 사용 안내: 이 셸은 `:host { height: 100% }` 로 부모를 채우는데, 부모(커스텀
|
|
@@ -61,6 +70,8 @@ export declare class SidebarLayout extends StyledElement<SidebarParts> {
|
|
|
61
70
|
private handleRouteDone;
|
|
62
71
|
/** 라우트 에러 핸들러 */
|
|
63
72
|
private handleRouteError;
|
|
73
|
+
private handleOverlaySlotChange;
|
|
74
|
+
private handleOverlayClose;
|
|
64
75
|
/** .main 키보드 스크롤 핸들러 (WCAG 2.1 SC 2.1.1) */
|
|
65
76
|
private _handleMainKeydown;
|
|
66
77
|
/** 화면 크기 변경에 따른 사이드바 상태 업데이트 */
|
|
@@ -4,32 +4,44 @@ import n from "../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMet
|
|
|
4
4
|
import r from "../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
|
|
5
5
|
import { StyledElement as i } from "../internals/StyledElement.js";
|
|
6
6
|
import { getLocaleStrings as a } from "../internals/locale.js";
|
|
7
|
+
import { slotHasContent as o } from "../internals/slotted.js";
|
|
7
8
|
import "../components/SidebarSection.js";
|
|
8
|
-
import { DEFAULT_NAV_ICON as
|
|
9
|
+
import { DEFAULT_NAV_ICON as s } from "../internals/nav-icon.js";
|
|
9
10
|
import "../components/SidebarLink.js";
|
|
10
11
|
import "../components/SidebarGroup.js";
|
|
11
12
|
import "../components/SidebarButton.js";
|
|
12
|
-
import { styles as
|
|
13
|
-
import { html as
|
|
14
|
-
import { customElement as
|
|
13
|
+
import { styles as c } from "./SidebarLayout.styles.js";
|
|
14
|
+
import { html as l, nothing as u } from "lit";
|
|
15
|
+
import { customElement as d, property as f, query as p, state as m } from "lit/decorators.js";
|
|
15
16
|
import "@iyulab/components/dist/components/button/UButton.js";
|
|
16
17
|
import "@iyulab/components/dist/components/icon/UIcon.js";
|
|
17
|
-
import { unsafeHTML as
|
|
18
|
-
import { repeat as
|
|
19
|
-
import { ifDefined as
|
|
20
|
-
import { createDevWarner as
|
|
21
|
-
import { UProgressBar as
|
|
18
|
+
import { unsafeHTML as h } from "lit/directives/unsafe-html.js";
|
|
19
|
+
import { repeat as g } from "lit/directives/repeat.js";
|
|
20
|
+
import { ifDefined as _ } from "lit/directives/if-defined.js";
|
|
21
|
+
import { createDevWarner as v } from "@iyulab/components/dist/utilities/devWarning.js";
|
|
22
|
+
import { UProgressBar as y } from "@iyulab/components/dist/components/progress-bar/UProgressBar.js";
|
|
22
23
|
//#region src/layouts/SidebarLayout.ts
|
|
23
|
-
var
|
|
24
|
-
function
|
|
24
|
+
var b = v("@iyulab/modern-app"), x = 0;
|
|
25
|
+
function S(e) {
|
|
25
26
|
let t = e.tagName;
|
|
26
27
|
if (t === "INPUT" || t === "TEXTAREA" || t === "SELECT" || e.isContentEditable) return !0;
|
|
27
28
|
let n = e.getAttribute("role");
|
|
28
29
|
return n === "textbox" || n === "searchbox" || n === "combobox" || n === "spinbutton";
|
|
29
30
|
}
|
|
30
|
-
|
|
31
|
+
function C(e) {
|
|
32
|
+
let t = getComputedStyle(e);
|
|
33
|
+
return (t.overflowY === "auto" || t.overflowY === "scroll") && e.scrollHeight > e.clientHeight;
|
|
34
|
+
}
|
|
35
|
+
function w(e, t) {
|
|
36
|
+
for (let n of e) if (n instanceof HTMLElement) {
|
|
37
|
+
if (C(n)) return n === t;
|
|
38
|
+
if (n === t) break;
|
|
39
|
+
}
|
|
40
|
+
return !0;
|
|
41
|
+
}
|
|
42
|
+
var T = class extends i {
|
|
31
43
|
constructor(...t) {
|
|
32
|
-
super(...t), this.state = "default", this.locale = "", this.context = null, this.unsizedWarnKey = `unsized:${++
|
|
44
|
+
super(...t), this.state = "default", this.locale = "", this.hasOverlay = !1, this.internals = typeof this.attachInternals == "function" ? this.attachInternals() : void 0, this.context = null, this.unsizedWarnKey = `unsized:${++x}`, this.isMatchedLink = (e) => !this.context || !e ? !1 : (e = typeof e == "string" ? new URLPattern(e, window.location.origin) : e, e.test(this.context.path, window.location.origin)), this.handleToggleButtonClick = () => {
|
|
33
45
|
let t = e.screen ?? "large";
|
|
34
46
|
t === "large" ? this.state = this.state === "default" ? "slim" : "default" : t === "medium" ? this.state = this.state === "slim" ? "modal" : "slim" : t === "small" ? this.state = this.state === "mobile" ? "mobile-open" : "mobile" : console.warn("Unknown screen size:", t);
|
|
35
47
|
}, this.handleBackdropClick = () => {
|
|
@@ -48,11 +60,15 @@ var S = class extends i {
|
|
|
48
60
|
this.progressBarEl.status = "error", this.progressBarEl.value = 100, setTimeout(() => {
|
|
49
61
|
this.progressBarEl.removeAttribute("visible"), this.progressBarEl.status = "default";
|
|
50
62
|
}, 300);
|
|
63
|
+
}, this.handleOverlaySlotChange = (e) => {
|
|
64
|
+
this.hasOverlay = o(e.target);
|
|
65
|
+
}, this.handleOverlayClose = () => {
|
|
66
|
+
this.fire("overlay-close", { cancelable: !1 });
|
|
51
67
|
}, this._handleMainKeydown = (e) => {
|
|
52
68
|
let t = e.composedPath()[0];
|
|
53
|
-
if (t instanceof HTMLElement &&
|
|
69
|
+
if (t instanceof HTMLElement && S(t)) return;
|
|
54
70
|
let n = this.shadowRoot?.querySelector(".main");
|
|
55
|
-
if (!n) return;
|
|
71
|
+
if (!n || !w(e.composedPath(), n)) return;
|
|
56
72
|
let r = n.clientHeight;
|
|
57
73
|
switch (e.key) {
|
|
58
74
|
case " ":
|
|
@@ -83,7 +99,7 @@ var S = class extends i {
|
|
|
83
99
|
};
|
|
84
100
|
}
|
|
85
101
|
static {
|
|
86
|
-
this.styles = [super.styles,
|
|
102
|
+
this.styles = [super.styles, c];
|
|
87
103
|
}
|
|
88
104
|
get mainElement() {
|
|
89
105
|
return this.shadowRoot?.querySelector(".main") ?? null;
|
|
@@ -97,6 +113,9 @@ var S = class extends i {
|
|
|
97
113
|
willUpdate(e) {
|
|
98
114
|
super.willUpdate(e), e.has("config") && (this.styles = this.config?.styles);
|
|
99
115
|
}
|
|
116
|
+
updated(e) {
|
|
117
|
+
super.updated(e), e.has("hasOverlay") && this.internals?.states?.[this.hasOverlay ? "add" : "delete"]("overlay");
|
|
118
|
+
}
|
|
100
119
|
firstUpdated(e) {
|
|
101
120
|
super.firstUpdated(e), this.warnIfUnsized();
|
|
102
121
|
}
|
|
@@ -104,11 +123,11 @@ var S = class extends i {
|
|
|
104
123
|
process.env.NODE_ENV !== "production" && requestAnimationFrame(() => {
|
|
105
124
|
if (!this.isConnected) return;
|
|
106
125
|
let e = this.getBoundingClientRect().height;
|
|
107
|
-
e >= 200 ||
|
|
126
|
+
e >= 200 || b(this.unsizedWarnKey, `u-sidebar-layout is only ${Math.round(e)}px tall — its height: 100% found no sized ancestor, so the shell sits at its own chrome height and the route area has almost no room. Give the root element a height (e.g. #app { height: 100vh } — app.load() does this for document.body).`);
|
|
108
127
|
});
|
|
109
128
|
}
|
|
110
129
|
render() {
|
|
111
|
-
return this.config ?
|
|
130
|
+
return this.config ? l`
|
|
112
131
|
<!-- Mobile Header -->
|
|
113
132
|
<div class="mobile-header" part="mobile-header" ?hidden="${!this.state.startsWith("mobile")}">
|
|
114
133
|
${this.renderLogo()}
|
|
@@ -145,38 +164,51 @@ var S = class extends i {
|
|
|
145
164
|
|
|
146
165
|
<!-- Sidebar Navigation Menu -->
|
|
147
166
|
<nav class="sidebar-main" part="sidebar-main" scrollable
|
|
148
|
-
aria-label=${this.config.mainAriaLabel ??
|
|
149
|
-
${
|
|
167
|
+
aria-label=${this.config.mainAriaLabel ?? u}>
|
|
168
|
+
${g(t(this.config.main ?? [], this.config.hasPermission), (e, t) => t, (e) => this.renderItem(e))}
|
|
150
169
|
</nav>
|
|
151
170
|
|
|
152
171
|
<!-- Sidebar Footer -->
|
|
153
172
|
<div class="sidebar-footer" part="sidebar-footer">
|
|
154
|
-
${
|
|
173
|
+
${g(t(this.config.footer ?? [], this.config.hasPermission), (e, t) => t, (e) => this.renderItem(e))}
|
|
155
174
|
</div>
|
|
156
175
|
</aside>
|
|
157
176
|
|
|
158
177
|
<!-- Main Content -->
|
|
159
|
-
<div class="main
|
|
160
|
-
<
|
|
178
|
+
<div class="main-region">
|
|
179
|
+
<div class="main" part="main" scrollable tabindex="-1" @keydown=${this._handleMainKeydown}>
|
|
180
|
+
<u-progress-bar part="progress"></u-progress-bar>
|
|
161
181
|
|
|
162
|
-
|
|
182
|
+
<div class="main-content" ?inert=${this.hasOverlay}>
|
|
183
|
+
<slot></slot>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<div class="overlay ${this.hasOverlay ? "" : "empty"}" part="overlay">
|
|
188
|
+
<u-button class="overlay-close" part="overlay-close" variant="ghost"
|
|
189
|
+
aria-label=${a(this.locale || void 0).detailClose}
|
|
190
|
+
@click=${this.handleOverlayClose}>
|
|
191
|
+
<u-icon lib="internal" name="x"></u-icon>
|
|
192
|
+
</u-button>
|
|
193
|
+
<slot name="overlay" @slotchange=${this.handleOverlaySlotChange}></slot>
|
|
194
|
+
</div>
|
|
163
195
|
</div>
|
|
164
196
|
|
|
165
197
|
<!-- Backdrop for modal state -->
|
|
166
198
|
<div class="backdrop" ?hidden="${this.state !== "modal"}"
|
|
167
199
|
@click="${this.handleBackdropClick}"
|
|
168
200
|
></div>
|
|
169
|
-
` :
|
|
201
|
+
` : u;
|
|
170
202
|
}
|
|
171
203
|
renderItem(e) {
|
|
172
|
-
if (!e) return
|
|
204
|
+
if (!e) return u;
|
|
173
205
|
if (e.type === "html") {
|
|
174
206
|
let t = e.render(this.state);
|
|
175
|
-
return typeof t == "string" ?
|
|
207
|
+
return typeof t == "string" ? h(t) : l`${t}`;
|
|
176
208
|
}
|
|
177
|
-
if (e.type === "button") return
|
|
209
|
+
if (e.type === "button") return l`
|
|
178
210
|
<u-sidebar-button
|
|
179
|
-
id=${
|
|
211
|
+
id=${_(e.id)}
|
|
180
212
|
?compact=${this.state === "slim"}
|
|
181
213
|
.icon="${e.icon}"
|
|
182
214
|
.lib="${e.lib}"
|
|
@@ -187,7 +219,7 @@ var S = class extends i {
|
|
|
187
219
|
`;
|
|
188
220
|
if (e.type === "link") {
|
|
189
221
|
let t = this.isMatchedLink(e.pattern || e.href);
|
|
190
|
-
return
|
|
222
|
+
return l`
|
|
191
223
|
<u-sidebar-link
|
|
192
224
|
?compact=${this.state === "slim"}
|
|
193
225
|
?selected=${t}
|
|
@@ -202,18 +234,18 @@ var S = class extends i {
|
|
|
202
234
|
></u-sidebar-link>
|
|
203
235
|
`;
|
|
204
236
|
}
|
|
205
|
-
if (e.type === "section") return
|
|
237
|
+
if (e.type === "section") return l`
|
|
206
238
|
<u-sidebar-section
|
|
207
239
|
?compact=${this.state === "slim"}
|
|
208
240
|
.mainTitle="${e.title}"
|
|
209
241
|
.subTitle="${e.subTitle}"
|
|
210
242
|
.styles="${e.styles}">
|
|
211
|
-
${
|
|
243
|
+
${g(e.items, (e, t) => t, (e) => this.renderItem(e))}
|
|
212
244
|
</u-sidebar-section>
|
|
213
245
|
`;
|
|
214
246
|
if (e.type === "group") {
|
|
215
247
|
let t = e.items.some((e) => this.isMatchedLink(e.pattern || e.href));
|
|
216
|
-
return
|
|
248
|
+
return l`
|
|
217
249
|
<u-sidebar-group
|
|
218
250
|
?compact=${this.state === "slim"}
|
|
219
251
|
?selected=${t}
|
|
@@ -222,34 +254,34 @@ var S = class extends i {
|
|
|
222
254
|
.lib="${e.lib}"
|
|
223
255
|
.label="${e.label}"
|
|
224
256
|
.styles="${e.styles}">
|
|
225
|
-
${
|
|
257
|
+
${g(e.items, (e, t) => t, (e) => this.renderItem(e))}
|
|
226
258
|
</u-sidebar-group>
|
|
227
259
|
`;
|
|
228
260
|
}
|
|
229
|
-
return
|
|
261
|
+
return u;
|
|
230
262
|
}
|
|
231
263
|
renderLogo() {
|
|
232
264
|
let e = this.config?.logo, t = this.config?.title ?? "Home";
|
|
233
|
-
if (!e || typeof e == "string") return e ?
|
|
265
|
+
if (!e || typeof e == "string") return e ? l`
|
|
234
266
|
<u-link class="logo-link" aria-label=${t}>
|
|
235
267
|
<u-icon class="logo"
|
|
236
268
|
.name="${e}"
|
|
237
|
-
.fallback=${
|
|
269
|
+
.fallback=${s}
|
|
238
270
|
></u-icon>
|
|
239
271
|
</u-link>
|
|
240
|
-
` :
|
|
272
|
+
` : l`<u-icon class="logo"></u-icon>`;
|
|
241
273
|
if (typeof e == "function") {
|
|
242
274
|
let n = e(this.state);
|
|
243
|
-
return
|
|
275
|
+
return l`
|
|
244
276
|
<u-link class="logo-link" aria-label=${t}>
|
|
245
277
|
<span class="logo">
|
|
246
|
-
${typeof n == "string" ?
|
|
278
|
+
${typeof n == "string" ? h(n) : n}
|
|
247
279
|
</span>
|
|
248
280
|
</u-link>
|
|
249
281
|
`;
|
|
250
282
|
}
|
|
251
|
-
return
|
|
252
|
-
<u-link class="logo-link" .href=${e.href} aria-label=${e.alt ?
|
|
283
|
+
return l`
|
|
284
|
+
<u-link class="logo-link" .href=${e.href} aria-label=${e.alt ? u : t}>
|
|
253
285
|
<img class="logo"
|
|
254
286
|
src="${e.src}"
|
|
255
287
|
alt="${e.alt ?? ""}"
|
|
@@ -258,9 +290,9 @@ var S = class extends i {
|
|
|
258
290
|
`;
|
|
259
291
|
}
|
|
260
292
|
};
|
|
261
|
-
r([
|
|
293
|
+
r([f({
|
|
262
294
|
type: String,
|
|
263
295
|
reflect: !0
|
|
264
|
-
}), n("design:type", Object)],
|
|
296
|
+
}), n("design:type", Object)], T.prototype, "state", void 0), r([f({ type: Object }), n("design:type", Object)], T.prototype, "config", void 0), r([f({ type: String }), n("design:type", Object)], T.prototype, "locale", void 0), r([p("u-progress-bar"), n("design:type", y === void 0 ? Object : y)], T.prototype, "progressBarEl", void 0), r([m(), n("design:type", Object)], T.prototype, "hasOverlay", void 0), r([m(), n("design:type", Object)], T.prototype, "context", void 0), T = r([d("u-sidebar-layout")], T);
|
|
265
297
|
//#endregion
|
|
266
|
-
export {
|
|
298
|
+
export { T as SidebarLayout };
|
|
@@ -183,15 +183,29 @@ var t = e`
|
|
|
183
183
|
* (CSS 2.1 §10.6.4: 절대배치 자손의 containing block 은 가장 가까운 positioned
|
|
184
184
|
* 조상의 «패딩 박스»이고, 그 경계는 조상 자신의 padding 값에 밀리지 않는다).
|
|
185
185
|
* 풀블리드를 원하는 소비자는 layout.styles.main = { padding: '0' } 로 되돌린다. */
|
|
186
|
-
.main {
|
|
186
|
+
.main-region {
|
|
187
187
|
position: relative;
|
|
188
188
|
flex: 1;
|
|
189
|
+
/* :host([state="mobile"]) 은 flex-direction: column 이라 이 축(높이)이 주축이 된다 —
|
|
190
|
+
min-height: 0 없이는 flex item 의 자동 최소 크기가 콘텐츠 기준(.main 의 min-content
|
|
191
|
+
높이)으로 잡혀, flex-shrink 가 있어도 뷰포트보다 작게 줄지 못한다. 그 결과 .main 의
|
|
192
|
+
overflow: auto 가 한 번도 발동하지 않고 셸 전체가 콘텐츠 높이만큼 부풀어 오른다. */
|
|
193
|
+
min-height: 0;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.main {
|
|
197
|
+
position: relative;
|
|
198
|
+
height: 100%;
|
|
189
199
|
padding: var(--u-space-3xl, 32px);
|
|
190
200
|
background: var(--u-bg-color, #FFFFFF);
|
|
191
201
|
overflow: auto;
|
|
192
202
|
outline: none;
|
|
193
203
|
}
|
|
194
204
|
|
|
205
|
+
.main-content {
|
|
206
|
+
height: 100%;
|
|
207
|
+
}
|
|
208
|
+
|
|
195
209
|
.main u-progress-bar {
|
|
196
210
|
--progress-bar-height: 4px;
|
|
197
211
|
--progress-bar-track-color: transparent;
|
|
@@ -211,6 +225,25 @@ var t = e`
|
|
|
211
225
|
transform: translateY(0);
|
|
212
226
|
}
|
|
213
227
|
|
|
228
|
+
.overlay {
|
|
229
|
+
position: absolute;
|
|
230
|
+
inset: 0;
|
|
231
|
+
z-index: 1;
|
|
232
|
+
padding: var(--u-space-3xl, 32px);
|
|
233
|
+
overflow: auto;
|
|
234
|
+
background: var(--u-panel-bg-color, #FFFFFF);
|
|
235
|
+
}
|
|
236
|
+
.overlay.empty {
|
|
237
|
+
display: none;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.overlay-close {
|
|
241
|
+
position: absolute;
|
|
242
|
+
top: var(--u-space-sm, 8px);
|
|
243
|
+
right: var(--u-space-sm, 8px);
|
|
244
|
+
z-index: 2;
|
|
245
|
+
}
|
|
246
|
+
|
|
214
247
|
/* Backdrop for modal mode */
|
|
215
248
|
.backdrop {
|
|
216
249
|
position: absolute;
|
|
@@ -242,10 +275,28 @@ var t = e`
|
|
|
242
275
|
.main u-progress-bar {
|
|
243
276
|
display: none;
|
|
244
277
|
}
|
|
278
|
+
.main-region {
|
|
279
|
+
height: auto;
|
|
280
|
+
}
|
|
245
281
|
.main {
|
|
282
|
+
height: auto;
|
|
246
283
|
padding: 0;
|
|
247
284
|
overflow: visible;
|
|
248
285
|
}
|
|
286
|
+
.main-content {
|
|
287
|
+
height: auto;
|
|
288
|
+
}
|
|
289
|
+
:host(:state(overlay)) .main-content {
|
|
290
|
+
display: none;
|
|
291
|
+
}
|
|
292
|
+
:host(:state(overlay)) .overlay {
|
|
293
|
+
position: static;
|
|
294
|
+
padding: 0;
|
|
295
|
+
overflow: visible;
|
|
296
|
+
}
|
|
297
|
+
.overlay-close {
|
|
298
|
+
display: none;
|
|
299
|
+
}
|
|
249
300
|
}
|
|
250
301
|
`;
|
|
251
302
|
//#endregion
|
|
@@ -12,7 +12,7 @@ export type { SidebarSectionConfig } from '../components/SidebarSection';
|
|
|
12
12
|
export type { SidebarGroupConfig } from '../components/SidebarGroup';
|
|
13
13
|
export type { SidebarButtonConfig } from '../components/SidebarButton';
|
|
14
14
|
/** 사이드바 레이아웃 컴포넌트의 요소(part) 타입 */
|
|
15
|
-
export type SidebarParts = 'host' | 'mobile-header' | 'sidebar' | 'sidebar-header' | 'sidebar-main' | 'sidebar-footer' | 'main' | 'progress';
|
|
15
|
+
export type SidebarParts = 'host' | 'mobile-header' | 'sidebar' | 'sidebar-header' | 'sidebar-main' | 'sidebar-footer' | 'main' | 'progress' | 'overlay' | 'overlay-close';
|
|
16
16
|
/** 사이드바 상태 타입 */
|
|
17
17
|
export type SidebarState = 'default' | 'slim' | 'modal' | 'mobile' | 'mobile-open';
|
|
18
18
|
/** 사이드바 안에 HTML 또는 엘리먼트를 직접 렌더링하는 설정 */
|
package/dist/react.d.ts
CHANGED
|
@@ -9,7 +9,9 @@ export type { ScreenSize, ScreenObserverConfig, ScreenResizeEvent } from './inte
|
|
|
9
9
|
* a raw custom element handles that correctly under React 19 (property assignment) but not
|
|
10
10
|
* React 18 (JSX attributes stringify), so this wrapper keeps `config` working on both.
|
|
11
11
|
*/
|
|
12
|
-
export declare const SidebarLayout: import('@lit/react').ReactWebComponent<SidebarLayoutElement, {
|
|
12
|
+
export declare const SidebarLayout: import('@lit/react').ReactWebComponent<SidebarLayoutElement, {
|
|
13
|
+
onOverlayClose: EventName<CustomEvent>;
|
|
14
|
+
}>;
|
|
13
15
|
export type SidebarLayoutProps = React.ComponentProps<typeof SidebarLayout>;
|
|
14
16
|
/**
|
|
15
17
|
* React wrapper for `<u-wizard>`. `Wizard` dispatches a `step-change` custom event, but a raw
|
package/dist/react.js
CHANGED
|
@@ -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
|
@@ -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
|
---
|
|
@@ -258,6 +258,8 @@ Parts available for `styles` overrides on the root layout:
|
|
|
258
258
|
| `sidebar-footer` | Pinned footer area |
|
|
259
259
|
| `main` | Main content area |
|
|
260
260
|
| `progress` | Top progress bar |
|
|
261
|
+
| `overlay` | Route-independent overlay panel above `main` |
|
|
262
|
+
| `overlay-close` | Overlay's close button |
|
|
261
263
|
|
|
262
264
|
---
|
|
263
265
|
|
|
@@ -298,6 +300,44 @@ To keep a piece of chrome on paper, restore it through its part:
|
|
|
298
300
|
⚠ Heights or `overflow` set through `layout.styles` are inline styles and still apply when
|
|
299
301
|
printing — scope such values to the screen in your own CSS instead.
|
|
300
302
|
|
|
303
|
+
## Overlay content
|
|
304
|
+
|
|
305
|
+
`<u-sidebar-layout>` has a second slot, `slot="overlay"`, that floats above `main` — independent
|
|
306
|
+
of routing. Fill it to show a panel (e.g. a record's detail) over whatever screen is currently
|
|
307
|
+
active, without wrapping every route or losing that screen's scroll position; empty it to remove
|
|
308
|
+
the panel. The route content underneath becomes `inert` while the overlay has content, and a
|
|
309
|
+
built-in close button fires `overlay-close` (not cancelable) when clicked:
|
|
310
|
+
|
|
311
|
+
```html
|
|
312
|
+
<u-sidebar-layout id="shell">
|
|
313
|
+
<!-- route content goes in the default slot, e.g. via app.load()'s <u-outlet> -->
|
|
314
|
+
</u-sidebar-layout>
|
|
315
|
+
|
|
316
|
+
<script>
|
|
317
|
+
function openOrderDetail(order) {
|
|
318
|
+
const panel = document.createElement('div');
|
|
319
|
+
panel.slot = 'overlay';
|
|
320
|
+
panel.textContent = `Order #${order.id}`;
|
|
321
|
+
shell.appendChild(panel);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
shell.addEventListener('overlay-close', () => {
|
|
325
|
+
shell.querySelector('[slot="overlay"]')?.remove();
|
|
326
|
+
});
|
|
327
|
+
</script>
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
⚠ `hidden`/`display: none` on the slotted panel does not close the overlay — the slot must
|
|
331
|
+
actually be emptied (removed or reassigned) for `slotHasContent` to see it as closed.
|
|
332
|
+
|
|
333
|
+
To keep the underlying route mounted while a URL parameter drives the overlay open/closed (so a
|
|
334
|
+
query-string change doesn't remount the whole screen), give the route a `key` that excludes that
|
|
335
|
+
parameter — see [`docs/routing.md`](../../../docs/routing.md) `RouteConfig.key`.
|
|
336
|
+
|
|
337
|
+
No `overlayBreakpoint`/responsive toggle exists here — unlike `u-master-detail-layout`, this
|
|
338
|
+
overlay is always an overlay, never a side-by-side pane. Use `u-master-detail-layout` instead
|
|
339
|
+
when you want the panel to sit *beside* content on wide screens.
|
|
340
|
+
|
|
301
341
|
## Responsive behaviour
|
|
302
342
|
|
|
303
343
|
| Screen width | Sidebar state |
|