@iyulab/modern-app 0.21.0 → 0.22.1

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 CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.22.1] - 2026-09-19
4
+
5
+ ### Fixed
6
+
7
+ - **A wide route content no longer pushes the desktop shell past the viewport.** In 0.22.0 the
8
+ scroll region's wrapper lacked a horizontal minimum-size override, so a body wider than the
9
+ available width (a data grid whose columns sum past the viewport) grew the wrapper beyond the
10
+ viewport: the right-hand columns were clipped and neither the content area nor the page scrolled
11
+ horizontally. The wide content now scrolls inside the content area again, as it did before 0.22.0.
12
+ This is the horizontal counterpart of the mobile height fix shipped in 0.22.0.
13
+
14
+ ## [0.22.0] - 2026-09-18
15
+
16
+ ### Added
17
+
18
+ - **`SidebarLayout` gained a `slot="overlay"`** — a route-independent panel shown above the main
19
+ content (e.g. a record detail), independent of the current route, without wrapping every route in
20
+ a shared layout or losing that route's scroll position. Fill the slot to show it; remove or
21
+ reassign the slotted node to dismiss it (`hidden` does not close it). Mirrors
22
+ `u-master-detail-layout`'s existing `slot="detail"` idiom rather than a new one. Adds a
23
+ `:state(overlay)` custom state, `inert` on the route content while the overlay has content, and a
24
+ built-in close button that fires a non-cancelable `overlay-close` event; the React wrapper maps
25
+ this to `onOverlayClose`. Documented in the layout skill reference under "Overlay content".
26
+
27
+ ### Fixed
28
+
29
+ - **The shell's keyboard scroll handling no longer intercepts a scrollable container nested inside
30
+ route content.** Space, Page Up/Down, Home, End and the arrow keys inside a nested scroll box
31
+ (a detail pane, a split view) were previously captured and cancelled by the shell instead of
32
+ scrolling that box.
33
+ - **The mobile layout (`state="mobile"`/`"mobile-open"`) no longer loses internal scrolling.** The
34
+ shell's scroll region lacked an explicit minimum-size override, so it could grow to the full
35
+ content height and stop scrolling within the viewport instead.
36
+ - **An open overlay prints in normal document flow instead of being clipped** to the main content's
37
+ box, and no longer renders off-screen if opened while the content underneath was scrolled.
38
+ - The documented "Overlay content" example now actually closes the overlay (it previously toggled
39
+ `hidden`, which does not empty the slot).
40
+
3
41
  ## [0.21.0] - 2026-09-17
4
42
 
5
43
  ### Added
@@ -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
- /** Master-detail layout — accessible label for the overlay-mode close button. */
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 o } from "../internals/nav-icon.js";
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 s } from "./SidebarLayout.styles.js";
13
- import { html as c, nothing as l } from "lit";
14
- import { customElement as u, property as d, query as f, state as p } from "lit/decorators.js";
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 m } from "lit/directives/unsafe-html.js";
18
- import { repeat as h } from "lit/directives/repeat.js";
19
- import { ifDefined as g } from "lit/directives/if-defined.js";
20
- import { createDevWarner as _ } from "@iyulab/components/dist/utilities/devWarning.js";
21
- import { UProgressBar as v } from "@iyulab/components/dist/components/progress-bar/UProgressBar.js";
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 y = _("@iyulab/modern-app"), b = 0;
24
- function x(e) {
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
- var S = class extends i {
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:${++b}`, 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 = () => {
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 && x(t)) return;
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, s];
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 || y(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).`);
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 ? c`
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 ?? l}>
149
- ${h(t(this.config.main ?? [], this.config.hasPermission), (e, t) => t, (e) => this.renderItem(e))}
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
- ${h(t(this.config.footer ?? [], this.config.hasPermission), (e, t) => t, (e) => this.renderItem(e))}
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" part="main" scrollable tabindex="-1" @keydown=${this._handleMainKeydown}>
160
- <u-progress-bar part="progress"></u-progress-bar>
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
- <slot></slot>
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
- ` : l;
201
+ ` : u;
170
202
  }
171
203
  renderItem(e) {
172
- if (!e) return l;
204
+ if (!e) return u;
173
205
  if (e.type === "html") {
174
206
  let t = e.render(this.state);
175
- return typeof t == "string" ? m(t) : c`${t}`;
207
+ return typeof t == "string" ? h(t) : l`${t}`;
176
208
  }
177
- if (e.type === "button") return c`
209
+ if (e.type === "button") return l`
178
210
  <u-sidebar-button
179
- id=${g(e.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 c`
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 c`
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
- ${h(e.items, (e, t) => t, (e) => this.renderItem(e))}
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 c`
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
- ${h(e.items, (e, t) => t, (e) => this.renderItem(e))}
257
+ ${g(e.items, (e, t) => t, (e) => this.renderItem(e))}
226
258
  </u-sidebar-group>
227
259
  `;
228
260
  }
229
- return l;
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 ? c`
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=${o}
269
+ .fallback=${s}
238
270
  ></u-icon>
239
271
  </u-link>
240
- ` : c`<u-icon class="logo"></u-icon>`;
272
+ ` : l`<u-icon class="logo"></u-icon>`;
241
273
  if (typeof e == "function") {
242
274
  let n = e(this.state);
243
- return c`
275
+ return l`
244
276
  <u-link class="logo-link" aria-label=${t}>
245
277
  <span class="logo">
246
- ${typeof n == "string" ? m(n) : n}
278
+ ${typeof n == "string" ? h(n) : n}
247
279
  </span>
248
280
  </u-link>
249
281
  `;
250
282
  }
251
- return c`
252
- <u-link class="logo-link" .href=${e.href} aria-label=${e.alt ? l : t}>
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([d({
293
+ r([f({
262
294
  type: String,
263
295
  reflect: !0
264
- }), n("design:type", Object)], S.prototype, "state", void 0), r([d({ type: Object }), n("design:type", Object)], S.prototype, "config", void 0), r([d({ type: String }), n("design:type", Object)], S.prototype, "locale", void 0), r([f("u-progress-bar"), n("design:type", v === void 0 ? Object : v)], S.prototype, "progressBarEl", void 0), r([p(), n("design:type", Object)], S.prototype, "context", void 0), S = r([u("u-sidebar-layout")], S);
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 { S as SidebarLayout };
298
+ export { T as SidebarLayout };
@@ -183,15 +183,34 @@ 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
+ /* flex item 의 자동 최소 크기는 콘텐츠 기준(.main 의 min-content)이다 — 이 래퍼는
190
+ overflow 가 visible 이라 그 하한이 그대로 살아, flex-shrink 가 있어도 셸 밖으로
191
+ 커진다. 그러면 .main 의 overflow: auto 가 한 번도 발동하지 않는다. 주축이 어느 쪽이든
192
+ 같은 원인이라 두 축을 함께 푼다:
193
+ · 높이 — :host([state="mobile"]) 은 flex-direction: column 이라 min-height 가 필요하다.
194
+ 없으면 셸 전체가 콘텐츠 높이만큼 부풀어 오른다.
195
+ · 너비 — 데스크톱(row)에서는 min-width 가 필요하다. 없으면 넓은 표 하나가 셸을
196
+ 뷰포트 밖으로 밀어 오른쪽 열이 잘리고 가로 스크롤도 생기지 않는다. */
197
+ min-height: 0;
198
+ min-width: 0;
199
+ }
200
+
201
+ .main {
202
+ position: relative;
203
+ height: 100%;
189
204
  padding: var(--u-space-3xl, 32px);
190
205
  background: var(--u-bg-color, #FFFFFF);
191
206
  overflow: auto;
192
207
  outline: none;
193
208
  }
194
209
 
210
+ .main-content {
211
+ height: 100%;
212
+ }
213
+
195
214
  .main u-progress-bar {
196
215
  --progress-bar-height: 4px;
197
216
  --progress-bar-track-color: transparent;
@@ -211,6 +230,25 @@ var t = e`
211
230
  transform: translateY(0);
212
231
  }
213
232
 
233
+ .overlay {
234
+ position: absolute;
235
+ inset: 0;
236
+ z-index: 1;
237
+ padding: var(--u-space-3xl, 32px);
238
+ overflow: auto;
239
+ background: var(--u-panel-bg-color, #FFFFFF);
240
+ }
241
+ .overlay.empty {
242
+ display: none;
243
+ }
244
+
245
+ .overlay-close {
246
+ position: absolute;
247
+ top: var(--u-space-sm, 8px);
248
+ right: var(--u-space-sm, 8px);
249
+ z-index: 2;
250
+ }
251
+
214
252
  /* Backdrop for modal mode */
215
253
  .backdrop {
216
254
  position: absolute;
@@ -242,10 +280,28 @@ var t = e`
242
280
  .main u-progress-bar {
243
281
  display: none;
244
282
  }
283
+ .main-region {
284
+ height: auto;
285
+ }
245
286
  .main {
287
+ height: auto;
246
288
  padding: 0;
247
289
  overflow: visible;
248
290
  }
291
+ .main-content {
292
+ height: auto;
293
+ }
294
+ :host(:state(overlay)) .main-content {
295
+ display: none;
296
+ }
297
+ :host(:state(overlay)) .overlay {
298
+ position: static;
299
+ padding: 0;
300
+ overflow: visible;
301
+ }
302
+ .overlay-close {
303
+ display: none;
304
+ }
249
305
  }
250
306
  `;
251
307
  //#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
@@ -8,7 +8,7 @@ var a = i({
8
8
  react: r,
9
9
  tagName: "u-sidebar-layout",
10
10
  elementClass: n,
11
- events: {}
11
+ events: { onOverlayClose: "overlay-close" }
12
12
  }), o = i({
13
13
  react: r,
14
14
  tagName: "u-wizard",
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.21.0",
4
+ "version": "0.22.1",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "web-framework",
@@ -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 |