@iyulab/modern-app 0.22.1 → 0.24.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 +70 -0
- package/dist/index.d.ts +1 -0
- package/dist/layouts/SidebarLayout.d.ts +12 -0
- package/dist/layouts/SidebarLayout.js +33 -18
- package/dist/layouts/SidebarLayout.styles.js +30 -0
- package/dist/layouts/SidebarLayout.types.d.ts +34 -1
- package/package.json +2 -2
- package/skills/modern-app/references/layout.md +34 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,75 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.24.0] - 2026-09-22
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **The shell draws its own chrome without reaching the network.** The mobile and sidebar
|
|
8
|
+
toggles were hard-coded to `lib="bootstrap"`, which `@iyulab/components` resolves over a CDN.
|
|
9
|
+
An app that registers its own bundled icons still could not reach those two, because the shell
|
|
10
|
+
never routed through that registration — so on a network-isolated deployment the toggles
|
|
11
|
+
stayed blank and a failing request repeated on every render. Working around it from outside
|
|
12
|
+
meant intercepting a globally registered library name, with an unclear blast radius. Both
|
|
13
|
+
toggles now resolve from the bundled internal set, as the overlay close button already did.
|
|
14
|
+
- That asymmetry was a small defect of its own: one shell drew two different close glyphs, a
|
|
15
|
+
CDN-backed one in the mobile header and a bundled one on the overlay. **The visible change is
|
|
16
|
+
a slightly different stroke on three controls.**
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **`icons` config.** Overrides both the source and the individual names, per key. It takes
|
|
21
|
+
names as well as the library because the defaults are bundled-set names and another set is not
|
|
22
|
+
guaranteed to carry them — pointing the shell at your own icons needs both halves. See
|
|
23
|
+
`docs/configuration.md`.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- 🔴 **The `@iyulab/components` peer range is now `>=1.43.0`** (was `>=1.42.0`). The shell's
|
|
28
|
+
chrome depends on `menu-2` and `layout-sidebar` existing in that package's built-in set, and
|
|
29
|
+
those names arrive in **1.43.0**. The old range admitted 1.42.x, which would have let
|
|
30
|
+
`components@1.42.4` install alongside this version with no warning at all — and then the shell
|
|
31
|
+
icons would be blank whether or not the deployment had network access, which is a wider
|
|
32
|
+
failure than the one this release fixes. Publishing in the right order does not prevent that:
|
|
33
|
+
ordering is a property of how we release, while the range is what a consumer is allowed to
|
|
34
|
+
install.
|
|
35
|
+
|
|
36
|
+
### Notes
|
|
37
|
+
|
|
38
|
+
- The regression test resolves each default name **against the bundle** rather than only
|
|
39
|
+
asserting which name the shell asks for. Asserting the name alone would let a typo blank the
|
|
40
|
+
icon with every assertion still passing.
|
|
41
|
+
|
|
42
|
+
## [0.23.0] - 2026-09-21
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- **Route content no longer paints through an open overlay.** The overlay frame sits at
|
|
47
|
+
`z-index: 1`, while route content routinely holds positioned descendants with a higher one —
|
|
48
|
+
a data grid's sticky header row and pinned columns sit at 2–4. Neither the content area nor
|
|
49
|
+
its wrapper established a stacking context, so those competed with the overlay frame in the
|
|
50
|
+
same context and won: the list showed through the panel meant to cover it. While the overlay
|
|
51
|
+
is open, the wrapper holding route content is now its own stacking context, so no `z-index`
|
|
52
|
+
in route content can outrank the panel. Raising the frame's own `z-index` was rejected as a
|
|
53
|
+
fix — it only moves the finish line, and a larger value in route content would win again.
|
|
54
|
+
The boundary applies only while the overlay is open, so nothing else about how route content
|
|
55
|
+
paints changes.
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
|
|
59
|
+
- **`part="main-content"`** on the wrapper holding route content, so the `inert` the shell
|
|
60
|
+
applies while the overlay is open is visible from outside. It is applied to this wrapper and
|
|
61
|
+
not to `part="main"` — that one is the scroll container and stays interactive — which made it
|
|
62
|
+
easy to read `inert` off `part="main"`, see `false`, and conclude the shell was not applying
|
|
63
|
+
it at all.
|
|
64
|
+
|
|
65
|
+
### Documentation
|
|
66
|
+
|
|
67
|
+
- **The overlay's ownership contract is now written down** — what the shell provides (placement,
|
|
68
|
+
paint order, `inert`, the close affordance, keeping the route mounted) and what stays with the
|
|
69
|
+
consumer (moving and restoring focus, Escape-to-close, any scrim, the panel's accessible
|
|
70
|
+
name). `docs/layout.md` had never documented the overlay slot at all, and its parts table was
|
|
71
|
+
missing `overlay` and `overlay-close`; both now match the skill pack's reference.
|
|
72
|
+
|
|
3
73
|
## [0.22.1] - 2026-09-19
|
|
4
74
|
|
|
5
75
|
### Fixed
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export type * from './types/AppConfigs';
|
|
|
3
3
|
export type * from './types/AppOptions';
|
|
4
4
|
export type * from './types/AuthConfig';
|
|
5
5
|
export type { SidebarPermissionGuard } from './layouts/SidebarPermission';
|
|
6
|
+
export type { SidebarIconsConfig } from './layouts/SidebarLayout.types';
|
|
6
7
|
export { filterSidebarItems } from './layouts/filterSidebarItems.js';
|
|
7
8
|
export { PageHeader } from './components/PageHeader.js';
|
|
8
9
|
export { GroupBox } from './components/GroupBox.js';
|
|
@@ -55,6 +55,18 @@ export declare class SidebarLayout extends StyledElement<SidebarParts> {
|
|
|
55
55
|
* 🔴로고는 홈(또는 `href`)으로 가는 **링크**다 — 종전엔 클릭만 받는 `img`·`span`·`u-icon` 이라 키보드로 닿지 않았고
|
|
56
56
|
* 보조기술에 링크로 드러나지 않았다. `u-link` 는 `href` 가 없으면 basepath 로 SPA 이동하므로 종전 `app.navigate('')` 와 같다.
|
|
57
57
|
*/
|
|
58
|
+
/**
|
|
59
|
+
* 셸이 **자기 chrome 으로** 그리는 아이콘의 기본값 — 전부 `internal` 번들이라 네트워크를 타지 않는다.
|
|
60
|
+
*
|
|
61
|
+
* 🔴**0.24.0 이전에는 토글러 둘이 `bootstrap`(= jsdelivr CDN 조회)이었다.** 소비앱이 자기
|
|
62
|
+
* 아이콘을 빌드 시점에 전부 구워 등록해도 **셸이 쓰는 것만은 그 등록을 타지 않아**, 폐쇄망
|
|
63
|
+
* 배포에서 사이드바 토글이 빈 채로 남고 매번 실패하는 요청이 쌓였다. ⚠그리고 셸이 스스로
|
|
64
|
+
* 갈려 있었다 — 오버레이 닫기는 `internal`, 모바일 닫기는 `bootstrap` 이라 **한 셸이 서로
|
|
65
|
+
* 다른 X 를 둘 그렸다.**
|
|
66
|
+
*/
|
|
67
|
+
private static readonly DEFAULT_ICONS;
|
|
68
|
+
/** 설정이 준 값이 있으면 그것을, 없으면 기본값을 돌려준다(키 단위 부분 오버라이드). */
|
|
69
|
+
private icon;
|
|
58
70
|
private renderLogo;
|
|
59
71
|
/** 사이드바 토글 핸들러 */
|
|
60
72
|
private handleToggleButtonClick;
|
|
@@ -21,27 +21,30 @@ import { ifDefined as _ } from "lit/directives/if-defined.js";
|
|
|
21
21
|
import { createDevWarner as v } from "@iyulab/components/dist/utilities/devWarning.js";
|
|
22
22
|
import { UProgressBar as y } from "@iyulab/components/dist/components/progress-bar/UProgressBar.js";
|
|
23
23
|
//#region src/layouts/SidebarLayout.ts
|
|
24
|
-
var b = v("@iyulab/modern-app"),
|
|
25
|
-
function
|
|
24
|
+
var b, x = v("@iyulab/modern-app"), S = 0;
|
|
25
|
+
function C(e) {
|
|
26
26
|
let t = e.tagName;
|
|
27
27
|
if (t === "INPUT" || t === "TEXTAREA" || t === "SELECT" || e.isContentEditable) return !0;
|
|
28
28
|
let n = e.getAttribute("role");
|
|
29
29
|
return n === "textbox" || n === "searchbox" || n === "combobox" || n === "spinbutton";
|
|
30
30
|
}
|
|
31
|
-
function
|
|
31
|
+
function w(e) {
|
|
32
32
|
let t = getComputedStyle(e);
|
|
33
33
|
return (t.overflowY === "auto" || t.overflowY === "scroll") && e.scrollHeight > e.clientHeight;
|
|
34
34
|
}
|
|
35
|
-
function
|
|
35
|
+
function T(e, t) {
|
|
36
36
|
for (let n of e) if (n instanceof HTMLElement) {
|
|
37
|
-
if (
|
|
37
|
+
if (w(n)) return n === t;
|
|
38
38
|
if (n === t) break;
|
|
39
39
|
}
|
|
40
40
|
return !0;
|
|
41
41
|
}
|
|
42
|
-
var
|
|
42
|
+
var E = class extends i {
|
|
43
|
+
static {
|
|
44
|
+
b = this;
|
|
45
|
+
}
|
|
43
46
|
constructor(...t) {
|
|
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:${++
|
|
47
|
+
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:${++S}`, 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 = () => {
|
|
45
48
|
let t = e.screen ?? "large";
|
|
46
49
|
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);
|
|
47
50
|
}, this.handleBackdropClick = () => {
|
|
@@ -66,9 +69,9 @@ var T = class extends i {
|
|
|
66
69
|
this.fire("overlay-close", { cancelable: !1 });
|
|
67
70
|
}, this._handleMainKeydown = (e) => {
|
|
68
71
|
let t = e.composedPath()[0];
|
|
69
|
-
if (t instanceof HTMLElement &&
|
|
72
|
+
if (t instanceof HTMLElement && C(t)) return;
|
|
70
73
|
let n = this.shadowRoot?.querySelector(".main");
|
|
71
|
-
if (!n || !
|
|
74
|
+
if (!n || !T(e.composedPath(), n)) return;
|
|
72
75
|
let r = n.clientHeight;
|
|
73
76
|
switch (e.key) {
|
|
74
77
|
case " ":
|
|
@@ -123,7 +126,7 @@ var T = class extends i {
|
|
|
123
126
|
process.env.NODE_ENV !== "production" && requestAnimationFrame(() => {
|
|
124
127
|
if (!this.isConnected) return;
|
|
125
128
|
let e = this.getBoundingClientRect().height;
|
|
126
|
-
e >= 200 ||
|
|
129
|
+
e >= 200 || x(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).`);
|
|
127
130
|
});
|
|
128
131
|
}
|
|
129
132
|
render() {
|
|
@@ -138,8 +141,8 @@ var T = class extends i {
|
|
|
138
141
|
aria-label=${a(this.locale || void 0).toggleMobileMenu}
|
|
139
142
|
@click=${this.handleToggleButtonClick}>
|
|
140
143
|
<u-icon
|
|
141
|
-
lib
|
|
142
|
-
name=${this.state === "mobile-open" ? "
|
|
144
|
+
lib=${this.icon("lib")}
|
|
145
|
+
name=${this.state === "mobile-open" ? this.icon("close") : this.icon("menu")}
|
|
143
146
|
></u-icon>
|
|
144
147
|
</u-button>
|
|
145
148
|
</div>
|
|
@@ -156,8 +159,8 @@ var T = class extends i {
|
|
|
156
159
|
aria-label=${a(this.locale || void 0).toggleSidebar}
|
|
157
160
|
@click=${this.handleToggleButtonClick}>
|
|
158
161
|
<u-icon
|
|
159
|
-
lib
|
|
160
|
-
name
|
|
162
|
+
lib=${this.icon("lib")}
|
|
163
|
+
name=${this.icon("sidebarToggle")}
|
|
161
164
|
></u-icon>
|
|
162
165
|
</u-button>
|
|
163
166
|
</div>
|
|
@@ -179,7 +182,7 @@ var T = class extends i {
|
|
|
179
182
|
<div class="main" part="main" scrollable tabindex="-1" @keydown=${this._handleMainKeydown}>
|
|
180
183
|
<u-progress-bar part="progress"></u-progress-bar>
|
|
181
184
|
|
|
182
|
-
<div class="main-content" ?inert=${this.hasOverlay}>
|
|
185
|
+
<div class="main-content" part="main-content" ?inert=${this.hasOverlay}>
|
|
183
186
|
<slot></slot>
|
|
184
187
|
</div>
|
|
185
188
|
</div>
|
|
@@ -188,7 +191,7 @@ var T = class extends i {
|
|
|
188
191
|
<u-button class="overlay-close" part="overlay-close" variant="ghost"
|
|
189
192
|
aria-label=${a(this.locale || void 0).detailClose}
|
|
190
193
|
@click=${this.handleOverlayClose}>
|
|
191
|
-
<u-icon lib
|
|
194
|
+
<u-icon lib=${this.icon("lib")} name=${this.icon("overlayClose")}></u-icon>
|
|
192
195
|
</u-button>
|
|
193
196
|
<slot name="overlay" @slotchange=${this.handleOverlaySlotChange}></slot>
|
|
194
197
|
</div>
|
|
@@ -260,6 +263,18 @@ var T = class extends i {
|
|
|
260
263
|
}
|
|
261
264
|
return u;
|
|
262
265
|
}
|
|
266
|
+
static {
|
|
267
|
+
this.DEFAULT_ICONS = {
|
|
268
|
+
lib: "internal",
|
|
269
|
+
menu: "menu-2",
|
|
270
|
+
close: "x",
|
|
271
|
+
sidebarToggle: "layout-sidebar",
|
|
272
|
+
overlayClose: "x"
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
icon(e) {
|
|
276
|
+
return this.config?.icons?.[e] ?? b.DEFAULT_ICONS[e];
|
|
277
|
+
}
|
|
263
278
|
renderLogo() {
|
|
264
279
|
let e = this.config?.logo, t = this.config?.title ?? "Home";
|
|
265
280
|
if (!e || typeof e == "string") return e ? l`
|
|
@@ -293,6 +308,6 @@ var T = class extends i {
|
|
|
293
308
|
r([f({
|
|
294
309
|
type: String,
|
|
295
310
|
reflect: !0
|
|
296
|
-
}), n("design:type", Object)],
|
|
311
|
+
}), n("design:type", Object)], E.prototype, "state", void 0), r([f({ type: Object }), n("design:type", Object)], E.prototype, "config", void 0), r([f({ type: String }), n("design:type", Object)], E.prototype, "locale", void 0), r([p("u-progress-bar"), n("design:type", y === void 0 ? Object : y)], E.prototype, "progressBarEl", void 0), r([m(), n("design:type", Object)], E.prototype, "hasOverlay", void 0), r([m(), n("design:type", Object)], E.prototype, "context", void 0), E = b = r([d("u-sidebar-layout")], E);
|
|
297
312
|
//#endregion
|
|
298
|
-
export {
|
|
313
|
+
export { E as SidebarLayout };
|
|
@@ -211,6 +211,36 @@ var t = e`
|
|
|
211
211
|
height: 100%;
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
/*
|
|
215
|
+
* 오버레이가 열린 동안 «본문 영역»을 스태킹 컨텍스트로 만든다.
|
|
216
|
+
*
|
|
217
|
+
* 오버레이 틀은 z-index: 1 이고, 라우트 본문은 그보다 큰 z 를 일상적으로 쓴다 —
|
|
218
|
+
* 데이터 그리드의 sticky 머리행·고정 열이 2~4 에 있다. .main 도 .main-region 도
|
|
219
|
+
* 스태킹 컨텍스트를 만들지 않으므로 그것들과 오버레이 틀이 «같은 컨텍스트에서 직접
|
|
220
|
+
* 겨루고», 본문이 이긴다 — 덮으라고 띄운 패널을 목록이 뚫고 나온다.
|
|
221
|
+
*
|
|
222
|
+
* ⚠값을 올리는 처방(z-index 를 50 으로)은 소비자의 z 어휘가 커지면 같은 경주를 다시
|
|
223
|
+
* 한다. 경계를 세워야 한다.
|
|
224
|
+
*
|
|
225
|
+
* ⚠경계를 «어디에» 세우는가가 갈린다 — 넷을 픽셀 대조로 실측하고 골랐다:
|
|
226
|
+
* · .overlay 자신을 컨텍스트로 → 자기 z 는 여전히 1 이라 바깥에서 3 에게 진다. ✗
|
|
227
|
+
* · .main-region 을 컨텍스트로 → 본문과 오버레이를 «같은» 새 컨텍스트에 함께
|
|
228
|
+
* 가두므로 그 안에서 순서가 그대로다. ✗
|
|
229
|
+
* · .main / .main-content 를 «상시» 컨텍스트로 → 오버레이는 이기지만 본문 안의
|
|
230
|
+
* position: fixed 팝오버가 사이드바(z 1000) 위로 못 올라간다(상시 동작 변경).
|
|
231
|
+
* .main 쪽은 셸 자신의 진행 막대(z 100)까지 오버레이 아래로 내린다. ✗
|
|
232
|
+
* · 이 규칙 — 오버레이가 열린 동안만 .main-content 를 컨텍스트로 → 세 축 모두 유지. ✓
|
|
233
|
+
*
|
|
234
|
+
* ⇒ 경계가 필요한 조건은 정확히 «오버레이가 열려 있다» 이고, 그때만 세운다. 닫혀 있으면
|
|
235
|
+
* 겨룰 상대가 없다(.overlay.empty 는 display: none 이다).
|
|
236
|
+
* ⚠ .main 이 아니라 .main-content 인 것도 의도다 — 가두는 대상은 «소비자의 라우트 본문»
|
|
237
|
+
* 이지 셸 자신의 chrome 이 아니다. 진행 막대는 .main-content 밖(.main 직속)이라 이
|
|
238
|
+
* 경계에 걸리지 않고, 오버레이가 열린 채로 일어나는 라우트 전환을 계속 보고한다.
|
|
239
|
+
*/
|
|
240
|
+
:host(:state(overlay)) .main-content {
|
|
241
|
+
isolation: isolate;
|
|
242
|
+
}
|
|
243
|
+
|
|
214
244
|
.main u-progress-bar {
|
|
215
245
|
--progress-bar-height: 4px;
|
|
216
246
|
--progress-bar-track-color: transparent;
|
|
@@ -11,8 +11,36 @@ export type { SidebarLinkConfig } from '../components/SidebarLink';
|
|
|
11
11
|
export type { SidebarSectionConfig } from '../components/SidebarSection';
|
|
12
12
|
export type { SidebarGroupConfig } from '../components/SidebarGroup';
|
|
13
13
|
export type { SidebarButtonConfig } from '../components/SidebarButton';
|
|
14
|
+
/**
|
|
15
|
+
* 셸이 **자기 chrome 으로** 그리는 아이콘의 출처와 이름. 지정한 키만 기본값을 대체한다.
|
|
16
|
+
*
|
|
17
|
+
* 🔴**기본값은 네트워크를 타지 않는다.** `internal` 은 `@iyulab/components` 가 빌드 시점에
|
|
18
|
+
* 굽는 번들이라, 폐쇄망·오프라인 배포에서도 셸 아이콘이 비지 않는다. 0.24.0 이전에는 셸의
|
|
19
|
+
* 토글러 둘이 `bootstrap`(= jsdelivr CDN 조회)을 썼고, 소비자가 자기 아이콘을 전부 번들로
|
|
20
|
+
* 구워 등록해도 **셸이 쓰는 것만은 그 등록을 타지 않았다.**
|
|
21
|
+
*
|
|
22
|
+
* ⚠**`lib` 만 바꿀 때는 이름도 함께 봐야 한다** — 기본 이름들은 `internal` 번들의 이름이고,
|
|
23
|
+
* 다른 lib 이 같은 이름을 갖는다는 보장이 없다. 자기 아이콘 세트로 갈아끼울 때는 보통
|
|
24
|
+
* `lib` 과 이름을 함께 준다:
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* icons: { lib: 'app', menu: 'list', close: 'x-lg', sidebarToggle: 'layout-sidebar' }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export interface SidebarIconsConfig {
|
|
31
|
+
/** 아래 이름들을 해석할 아이콘 lib. 기본 `'internal'`(번들 — 네트워크 없음). */
|
|
32
|
+
lib?: string;
|
|
33
|
+
/** 모바일 헤더의 메뉴 «열기». 기본 `'menu-2'`. */
|
|
34
|
+
menu?: string;
|
|
35
|
+
/** 모바일 헤더의 메뉴 «닫기». 기본 `'x'`. */
|
|
36
|
+
close?: string;
|
|
37
|
+
/** 사이드바 접기/펼치기 토글. 기본 `'layout-sidebar'`. */
|
|
38
|
+
sidebarToggle?: string;
|
|
39
|
+
/** 오버레이 닫기 버튼. 기본 `'x'`. */
|
|
40
|
+
overlayClose?: string;
|
|
41
|
+
}
|
|
14
42
|
/** 사이드바 레이아웃 컴포넌트의 요소(part) 타입 */
|
|
15
|
-
export type SidebarParts = 'host' | 'mobile-header' | 'sidebar' | 'sidebar-header' | 'sidebar-main' | 'sidebar-footer' | 'main' | 'progress' | 'overlay' | 'overlay-close';
|
|
43
|
+
export type SidebarParts = 'host' | 'mobile-header' | 'sidebar' | 'sidebar-header' | 'sidebar-main' | 'sidebar-footer' | 'main' | 'main-content' | 'progress' | 'overlay' | 'overlay-close';
|
|
16
44
|
/** 사이드바 상태 타입 */
|
|
17
45
|
export type SidebarState = 'default' | 'slim' | 'modal' | 'mobile' | 'mobile-open';
|
|
18
46
|
/** 사이드바 안에 HTML 또는 엘리먼트를 직접 렌더링하는 설정 */
|
|
@@ -64,6 +92,11 @@ export interface SidebarLayoutConfig {
|
|
|
64
92
|
* "아무것도 안 함" — Vue Router의 `scrollBehavior` 미지정 기본값과 동일하다(breaking 아님).
|
|
65
93
|
*/
|
|
66
94
|
scrollBehavior?: (context: RouteContext, main: HTMLElement) => void;
|
|
95
|
+
/**
|
|
96
|
+
* 셸 chrome 아이콘의 출처·이름 오버라이드. 지정한 키만 기본값을 대체한다.
|
|
97
|
+
* 기본값은 전부 `internal` 번들이라 **네트워크를 타지 않는다** — `SidebarIconsConfig` 참조.
|
|
98
|
+
*/
|
|
99
|
+
icons?: SidebarIconsConfig;
|
|
67
100
|
/** 사이드바 스타일 맵 */
|
|
68
101
|
styles?: StyleMap<SidebarParts>;
|
|
69
102
|
}
|
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.24.0",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"iyulab",
|
|
7
7
|
"web-framework",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"lit": "^3.3.3"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
|
-
"@iyulab/components": ">=1.
|
|
67
|
+
"@iyulab/components": ">=1.43.0",
|
|
68
68
|
"@lit/react": ">=1.0.8",
|
|
69
69
|
"react": ">=18.0.0"
|
|
70
70
|
},
|
|
@@ -27,6 +27,9 @@ interface SidebarLayoutConfig {
|
|
|
27
27
|
/** Called on `route-done` (before focus moves to the container) with the new route's `RouteContext` and the scroll container itself — implement reset/save/restore here. Unset does nothing (same default as Vue Router's unset `scrollBehavior`). Also reachable outside the hook via the element's `.mainElement` accessor. */
|
|
28
28
|
scrollBehavior?: (context: RouteContext, main: HTMLElement) => void;
|
|
29
29
|
|
|
30
|
+
/** Shell chrome icon source and names — `{ lib?, menu?, close?, sidebarToggle?, overlayClose? }`, only the keys you set replace the defaults. Defaults come from the bundled `internal` set, so the shell needs no network to draw its own toggles; give `lib` **and** the names to point it at your own set. */
|
|
31
|
+
icons?: SidebarIconsConfig;
|
|
32
|
+
|
|
30
33
|
/** Per-part style overrides (CSS custom properties / inline styles). */
|
|
31
34
|
styles?: StyleMap<SidebarParts>;
|
|
32
35
|
}
|
|
@@ -256,7 +259,8 @@ Parts available for `styles` overrides on the root layout:
|
|
|
256
259
|
| `sidebar-header` | Logo + title area |
|
|
257
260
|
| `sidebar-main` | Scrollable main nav area |
|
|
258
261
|
| `sidebar-footer` | Pinned footer area |
|
|
259
|
-
| `main` | Main content area |
|
|
262
|
+
| `main` | Main content area (the scroll container) |
|
|
263
|
+
| `main-content` | Wrapper holding route content inside `main` — the shell puts `inert` here while the overlay is open |
|
|
260
264
|
| `progress` | Top progress bar |
|
|
261
265
|
| `overlay` | Route-independent overlay panel above `main` |
|
|
262
266
|
| `overlay-close` | Overlay's close button |
|
|
@@ -334,6 +338,35 @@ To keep the underlying route mounted while a URL parameter drives the overlay op
|
|
|
334
338
|
query-string change doesn't remount the whole screen), give the route a `key` that excludes that
|
|
335
339
|
parameter — see [`docs/routing.md`](../../../docs/routing.md) `RouteConfig.key`.
|
|
336
340
|
|
|
341
|
+
### What the shell owns, and what it does not
|
|
342
|
+
|
|
343
|
+
The overlay is a **layer**, not a modal dialog. Splitting that precisely is what keeps a consumer
|
|
344
|
+
from either re-implementing what the shell already does, or dropping what it does not.
|
|
345
|
+
|
|
346
|
+
| Concern | Owner | How |
|
|
347
|
+
|---|---|---|
|
|
348
|
+
| Placement, size and scrolling of the panel | shell | `part="overlay"` covers the main region and scrolls on its own |
|
|
349
|
+
| Painting above route content | shell | while the overlay is open, `part="main-content"` becomes its own stacking context, so a `z-index` in route content cannot paint through the panel |
|
|
350
|
+
| Making route content non-interactive | shell | `inert` on `part="main-content"`, which propagates through the slot into your route content |
|
|
351
|
+
| A close affordance | shell | `part="overlay-close"`, firing the non-cancelable `overlay-close` event |
|
|
352
|
+
| Keeping the route mounted underneath | shell | the overlay is independent of routing |
|
|
353
|
+
| **Moving focus into the panel when it opens** | **consumer** | focus the panel or its first control yourself |
|
|
354
|
+
| **Restoring focus when it closes** | **consumer** | remember the trigger and re-focus it |
|
|
355
|
+
| **Escape to close** | **consumer** | the shell binds no key; listen for it and empty the slot |
|
|
356
|
+
| **A backdrop / dimmed scrim** | **consumer** | the panel is opaque and full-bleed by design; add a scrim inside your panel if you want one |
|
|
357
|
+
| **Announcing the panel to assistive tech** | **consumer** | put `role`/`aria-label` (or `aria-modal`, if you have made it modal) on *your* panel — the shell does not know what it holds |
|
|
358
|
+
|
|
359
|
+
⚠ **`inert` is applied to `part="main-content"`, not to `part="main"`.** `part="main"` is the scroll
|
|
360
|
+
container and stays interactive so it can keep scrolling; the wrapper inside it is what goes inert.
|
|
361
|
+
Reading `inert` off `part="main"` reports `false` and says nothing about whether the shell applied it.
|
|
362
|
+
There is also no reliable way to check it by enumeration — `querySelectorAll` still returns focusable
|
|
363
|
+
elements inside an inert subtree. Inertness shows up when something tries to *take* focus, not when
|
|
364
|
+
you count what is there.
|
|
365
|
+
|
|
366
|
+
⚠ **Page-level scroll is not locked**, deliberately. In the shell model `part="main"` is the scroller
|
|
367
|
+
and the overlay covers it, so there is nothing behind it to scroll. If your app instead lets the
|
|
368
|
+
document itself scroll, locking that is yours.
|
|
369
|
+
|
|
337
370
|
No `overlayBreakpoint`/responsive toggle exists here — unlike `u-master-detail-layout`, this
|
|
338
371
|
overlay is always an overlay, never a side-by-side pane. Use `u-master-detail-layout` instead
|
|
339
372
|
when you want the panel to sit *beside* content on wide screens.
|