@iyulab/modern-app 0.30.2 → 0.32.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 CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.32.0] - 2026-09-28
4
+
5
+ ### Changed
6
+
7
+ - **The shell's overlay panel is a layer in `@iyulab/components`' layer stack.** Escape inside the
8
+ panel still fires `overlay-close`, and a drawer, dialog or popover opened inside the panel still
9
+ closes first — the order now comes from `OverlayManager` (layers stack in the order they opened)
10
+ instead of the shell waiting a task to see who else handled the key. The Escape that closes the
11
+ panel is now marked consumed (`defaultPrevented`), like every layer's; an Escape pressed outside
12
+ the panel is still left alone. Requires `@iyulab/components` 1.49.0 or later.
13
+ - The sidebar and the mobile header read `--u-layer-shell` (1000 and one above it, as before), the
14
+ shared stacking token from `@iyulab/components`.
15
+
16
+ ## [0.31.0] - 2026-09-28
17
+
18
+ ### Changed
19
+
20
+ - **Depends on `@iyulab/router` `^0.17.0`**, where `@lit/react` is an optional peer. Apps that import
21
+ `@iyulab/modern-app/react` already install `@lit/react` (an optional peer here too); nothing else
22
+ changes.
23
+
24
+ ### Fixed
25
+
26
+ - **`document.querySelector('u-sidebar-layout')` is typed as `SidebarLayout`.** The shell was the
27
+ one element without an `HTMLElementTagNameMap` entry, so reaching `mainElement` from a query
28
+ needed a cast.
29
+
3
30
  ## [0.30.2] - 2026-09-28
4
31
 
5
32
  ### Fixed
@@ -132,18 +132,13 @@ export declare class SidebarLayout extends StyledElement<SidebarParts> {
132
132
  */
133
133
  private restoreOverlayFocus;
134
134
  /**
135
- * 패널 안의 Escape 는 닫기 버튼과 같은 `overlay-close` 를 낸다 — 단, **안쪽 층이 모두 판정한 뒤에**.
135
+ * 패널 안의 Escape 는 닫기 버튼과 같은 `overlay-close` 를 낸다.
136
136
  *
137
- * 셸의 오버레이는 가장 바깥 층이다. 그런데 window 의 같은 단계 리스너는 «등록 순서» 로 돌고,
138
- * 셸은 오버레이가 열릴 때 등록하므로 패널 안에서 «나중에» 연 층(`u-drawer`·`u-dialog` — 역시
139
- * window 에서 받는다)보다 먼저 돈다. 거기서 판정하면 바깥이 닫히고 안쪽이 남는다(거꾸로다).
140
- * ⇒ 리스너에서는 판정하지 않고 **디스패치가 끝난 뒤**(다음 태스크) `defaultPrevented` 를 본다 —
141
- * 그때는 어느 순서로 등록된 층이든 이미 자기 몫을 가져갔다. 한 번의 Escape 는 한 층만 닫는다.
142
- * ⚠마이크로태스크로는 부족하다 — 네이티브 이벤트에서는 리스너 사이마다 마이크로태스크가 비워져,
143
- * 뒤에 등록된 층보다 먼저 돈다.
144
- * ⚠셸은 `preventDefault` 하지 않는다 — 가장 바깥 층이 키를 «먹었다» 고 표시하면 안쪽 층의 가드
145
- * (`defaultPrevented` 면 닫지 않는다)를 오염시킨다.
146
- * 경로에 패널이 없으면(사이드바, 패널 위에 띄운 대화상자) 받지 않는다.
137
+ * 패널은 components 의 층 스택(`OverlayManager.openLayer`)에 선 한 층이다 — 층은 여는 순서로 쌓이고
138
+ * Escape 한 번은 가장 위 층 하나만 닫으므로, 패널 «안» 에서 나중에 연 서랍·대화상자·팝오버가 먼저
139
+ * 닫힌다. 종전에는 셸이 window 에서 직접 받아 «등록 순서» 가 층 순서를 거슬렀고, 그것을 다음
140
+ * 태스크로 미룬 판정으로 메웠다 — 이제 순서는 한 곳이 정한다.
141
+ * 비모달 패널이라 **포커스가 패널 안일 때만** 닫는다(사이드바에서 누른 Escape 는 거절 — 소비하지 않는다).
147
142
  */
148
143
  private handleOverlayEscape;
149
144
  private handleOverlayClose;
@@ -152,3 +147,8 @@ export declare class SidebarLayout extends StyledElement<SidebarParts> {
152
147
  /** 화면 크기 변경에 따른 사이드바 상태 업데이트 */
153
148
  private handleScreenResize;
154
149
  }
150
+ declare global {
151
+ interface HTMLElementTagNameMap {
152
+ 'u-sidebar-layout': SidebarLayout;
153
+ }
154
+ }
@@ -20,32 +20,33 @@ import { repeat as g } from "lit/directives/repeat.js";
20
20
  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 { isFocusCandidate as y, querySelectorDeep as b } from "@iyulab/components/dist/utilities/elements.js";
23
- import { UProgressBar as x } from "@iyulab/components/dist/components/progress-bar/UProgressBar.js";
23
+ import { OverlayManager as x } from "@iyulab/components/dist/utilities/OverlayManager.js";
24
+ import { UProgressBar as S } from "@iyulab/components/dist/components/progress-bar/UProgressBar.js";
24
25
  //#region src/layouts/SidebarLayout.ts
25
- var S, C = v("@iyulab/modern-app"), w = 0;
26
- function T(e) {
26
+ var C, w = v("@iyulab/modern-app"), T = 0;
27
+ function E(e) {
27
28
  let t = e.tagName;
28
29
  if (t === "INPUT" || t === "TEXTAREA" || t === "SELECT" || e.isContentEditable) return !0;
29
30
  let n = e.getAttribute("role");
30
31
  return n === "textbox" || n === "searchbox" || n === "combobox" || n === "spinbutton";
31
32
  }
32
- function E(e) {
33
+ function D(e) {
33
34
  let t = getComputedStyle(e);
34
35
  return (t.overflowY === "auto" || t.overflowY === "scroll") && e.scrollHeight > e.clientHeight;
35
36
  }
36
- function D(e, t) {
37
+ function O(e, t) {
37
38
  for (let n of e) if (n instanceof HTMLElement) {
38
- if (E(n)) return n === t;
39
+ if (D(n)) return n === t;
39
40
  if (n === t) break;
40
41
  }
41
42
  return !0;
42
43
  }
43
- function O() {
44
+ function k() {
44
45
  let e = document.activeElement;
45
46
  for (; e?.shadowRoot?.activeElement;) e = e.shadowRoot.activeElement;
46
47
  return e;
47
48
  }
48
- function k(e, t) {
49
+ function A(e, t) {
49
50
  let n = t;
50
51
  for (; n;) {
51
52
  if (n === e) return !0;
@@ -53,12 +54,12 @@ function k(e, t) {
53
54
  }
54
55
  return !1;
55
56
  }
56
- var A = class extends i {
57
+ var j = class extends i {
57
58
  static {
58
- S = this;
59
+ C = this;
59
60
  }
60
61
  constructor(...t) {
61
- super(...t), this.state = "default", this.locale = "", this.hasOverlay = !1, this.hasNotice = !1, this.overlayTrigger = null, this.internals = typeof this.attachInternals == "function" ? this.attachInternals() : void 0, this.context = null, this.unsizedWarnKey = `unsized:${++w}`, 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 = () => {
62
+ super(...t), this.state = "default", this.locale = "", this.hasOverlay = !1, this.hasNotice = !1, this.overlayTrigger = null, this.internals = typeof this.attachInternals == "function" ? this.attachInternals() : void 0, this.context = null, this.unsizedWarnKey = `unsized:${++T}`, 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 = () => {
62
63
  let t = this.screenSize ?? e.screen ?? "large";
63
64
  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);
64
65
  }, this.handleBackdropClick = () => {
@@ -80,25 +81,22 @@ var A = class extends i {
80
81
  }, this.handleOverlaySlotChange = (e) => {
81
82
  let t = o(e.target);
82
83
  if (t && !this.hasOverlay) {
83
- let e = O();
84
+ let e = k();
84
85
  this.overlayTrigger = e && e !== document.body ? e : null;
85
86
  }
86
87
  this.hasOverlay = t;
87
88
  }, this.handleNoticeSlotChange = (e) => {
88
89
  this.hasNotice = o(e.target);
89
90
  }, this.handleOverlayEscape = (e) => {
90
- if (e.key !== "Escape" || e.isComposing || !this.hasOverlay) return;
91
91
  let t = this.shadowRoot?.querySelector(".overlay");
92
- t && e.composedPath().includes(t) && setTimeout(() => {
93
- !e.defaultPrevented && this.hasOverlay && this.handleOverlayClose();
94
- }, 0);
92
+ return !this.hasOverlay || !t || !e.composedPath().includes(t) ? !1 : (this.handleOverlayClose(), !0);
95
93
  }, this.handleOverlayClose = () => {
96
94
  this.fire("overlay-close", { cancelable: !1 });
97
95
  }, this._handleMainKeydown = (e) => {
98
96
  let t = e.composedPath()[0];
99
- if (t instanceof HTMLElement && T(t)) return;
97
+ if (t instanceof HTMLElement && E(t)) return;
100
98
  let n = this.shadowRoot?.querySelector(".main");
101
- if (!n || !D(e.composedPath(), n)) return;
99
+ if (!n || !O(e.composedPath(), n)) return;
102
100
  let r = n.clientHeight;
103
101
  switch (e.key) {
104
102
  case " ":
@@ -138,13 +136,13 @@ var A = class extends i {
138
136
  super.connectedCallback(), window.addEventListener("route-begin", this.handleRouteBegin), window.addEventListener("route-done", this.handleRouteDone), window.addEventListener("route-progress", this.handleRouteProgress), window.addEventListener("route-error", this.handleRouteError), window.addEventListener("screen-resize", this.handleScreenResize);
139
137
  }
140
138
  disconnectedCallback() {
141
- window.removeEventListener("route-begin", this.handleRouteBegin), window.removeEventListener("route-done", this.handleRouteDone), window.removeEventListener("route-progress", this.handleRouteProgress), window.removeEventListener("route-error", this.handleRouteError), window.removeEventListener("screen-resize", this.handleScreenResize), window.removeEventListener("keydown", this.handleOverlayEscape), super.disconnectedCallback();
139
+ window.removeEventListener("route-begin", this.handleRouteBegin), window.removeEventListener("route-done", this.handleRouteDone), window.removeEventListener("route-progress", this.handleRouteProgress), window.removeEventListener("route-error", this.handleRouteError), window.removeEventListener("screen-resize", this.handleScreenResize), x.closeLayer(this), super.disconnectedCallback();
142
140
  }
143
141
  willUpdate(e) {
144
142
  super.willUpdate(e), e.has("config") && (this.styles = this.config?.styles);
145
143
  }
146
144
  updated(e) {
147
- super.updated(e), e.has("hasOverlay") && (this.internals?.states?.[this.hasOverlay ? "add" : "delete"]("overlay"), this.hasOverlay ? (window.addEventListener("keydown", this.handleOverlayEscape), this.focusOverlay()) : e.get("hasOverlay") === !0 && (window.removeEventListener("keydown", this.handleOverlayEscape), this.restoreOverlayFocus()));
145
+ super.updated(e), e.has("hasOverlay") && (this.internals?.states?.[this.hasOverlay ? "add" : "delete"]("overlay"), this.hasOverlay ? (x.openLayer(this, this.handleOverlayEscape), this.focusOverlay()) : e.get("hasOverlay") === !0 && (x.closeLayer(this), this.restoreOverlayFocus()));
148
146
  }
149
147
  firstUpdated(e) {
150
148
  super.firstUpdated(e), this.warnIfUnsized();
@@ -153,7 +151,7 @@ var A = class extends i {
153
151
  process.env.NODE_ENV !== "production" && requestAnimationFrame(() => {
154
152
  if (!this.isConnected) return;
155
153
  let e = this.getBoundingClientRect().height;
156
- e >= 200 || C(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).`);
154
+ e >= 200 || w(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).`);
157
155
  });
158
156
  }
159
157
  render() {
@@ -305,7 +303,7 @@ var A = class extends i {
305
303
  };
306
304
  }
307
305
  icon(e) {
308
- return this.config?.icons?.[e] ?? S.DEFAULT_ICONS[e];
306
+ return this.config?.icons?.[e] ?? C.DEFAULT_ICONS[e];
309
307
  }
310
308
  renderLogo() {
311
309
  let e = this.config?.logo, t = this.config?.title ?? "Home";
@@ -342,12 +340,12 @@ var A = class extends i {
342
340
  async focusRoute(e) {
343
341
  let t = this.routeContent();
344
342
  if (await Promise.all(t.map((e) => e.updateComplete)), this.hasOverlay || !e.isConnected) return;
345
- let n = O();
346
- if (n && t.some((e) => k(e, n))) return;
343
+ let n = k();
344
+ if (n && t.some((e) => A(e, n))) return;
347
345
  let r = b(t, "[autofocus]", y);
348
346
  r?.focus();
349
- let i = O();
350
- r && i && k(r, i) || e.focus({ preventScroll: !0 });
347
+ let i = k();
348
+ r && i && A(r, i) || e.focus({ preventScroll: !0 });
351
349
  }
352
350
  overlayPanels() {
353
351
  return (this.shadowRoot?.querySelector("slot[name=\"overlay\"]"))?.assignedElements({ flatten: !0 }) ?? [];
@@ -355,19 +353,19 @@ var A = class extends i {
355
353
  async focusOverlay() {
356
354
  let e = this.overlayPanels();
357
355
  if (await Promise.all(e.map((e) => e.updateComplete)), !this.hasOverlay) return;
358
- let t = O();
359
- t && e.some((e) => k(e, t)) || (b(e, "[autofocus]", y) ?? b(e, "input, select, textarea, u-input, u-textarea, u-select, u-checkbox, u-radio, u-switch, u-slider", y) ?? this.shadowRoot?.querySelector(".overlay-close"))?.focus();
356
+ let t = k();
357
+ t && e.some((e) => A(e, t)) || (b(e, "[autofocus]", y) ?? b(e, "input, select, textarea, u-input, u-textarea, u-select, u-checkbox, u-radio, u-switch, u-slider", y) ?? this.shadowRoot?.querySelector(".overlay-close"))?.focus();
360
358
  }
361
359
  restoreOverlayFocus() {
362
360
  let e = this.overlayTrigger;
363
361
  this.overlayTrigger = null;
364
- let t = O();
362
+ let t = k();
365
363
  t && t !== document.body || (e?.isConnected ? e.focus() : this.mainElement?.focus({ preventScroll: !0 }));
366
364
  }
367
365
  };
368
366
  r([f({
369
367
  type: String,
370
368
  reflect: !0
371
- }), n("design:type", Object)], A.prototype, "state", void 0), r([f({ type: Object }), n("design:type", Object)], A.prototype, "config", void 0), r([f({ type: String }), n("design:type", Object)], A.prototype, "locale", void 0), r([p("u-progress-bar"), n("design:type", x === void 0 ? Object : x)], A.prototype, "progressBarEl", void 0), r([m(), n("design:type", Object)], A.prototype, "hasOverlay", void 0), r([m(), n("design:type", Object)], A.prototype, "hasNotice", void 0), r([m(), n("design:type", Object)], A.prototype, "context", void 0), A = S = r([d("u-sidebar-layout")], A);
369
+ }), n("design:type", Object)], j.prototype, "state", void 0), r([f({ type: Object }), n("design:type", Object)], j.prototype, "config", void 0), r([f({ type: String }), n("design:type", Object)], j.prototype, "locale", void 0), r([p("u-progress-bar"), n("design:type", S === void 0 ? Object : S)], j.prototype, "progressBarEl", void 0), r([m(), n("design:type", Object)], j.prototype, "hasOverlay", void 0), r([m(), n("design:type", Object)], j.prototype, "hasNotice", void 0), r([m(), n("design:type", Object)], j.prototype, "context", void 0), j = C = r([d("u-sidebar-layout")], j);
372
370
  //#endregion
373
- export { A as SidebarLayout };
371
+ export { j as SidebarLayout };
@@ -75,7 +75,7 @@ var t = e`
75
75
  /* Sidebar Container */
76
76
  .sidebar {
77
77
  position: relative;
78
- z-index: 1000;
78
+ z-index: var(--u-layer-shell, 1000);
79
79
  display: flex;
80
80
  flex-direction: column;
81
81
  justify-content: space-between;
@@ -126,7 +126,7 @@ var t = e`
126
126
  /* Mobile Header */
127
127
  .mobile-header {
128
128
  position: relative;
129
- z-index: 1001;
129
+ z-index: calc(var(--u-layer-shell, 1000) + 1);
130
130
  display: flex;
131
131
  flex-direction: row;
132
132
  align-items: center;
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.30.2",
4
+ "version": "0.32.0",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "web-framework",
@@ -59,12 +59,12 @@
59
59
  "test:react-types": "tsc -p tsconfig.react-smoke.json"
60
60
  },
61
61
  "dependencies": {
62
- "@iyulab/router": "^0.16.0",
62
+ "@iyulab/router": "^0.17.0",
63
63
  "i18next": "^26.3.6",
64
64
  "lit": "^3.3.3"
65
65
  },
66
66
  "peerDependencies": {
67
- "@iyulab/components": ">=1.47.1",
67
+ "@iyulab/components": ">=1.49.0",
68
68
  "@lit/react": ">=1.0.8",
69
69
  "react": ">=18.0.0"
70
70
  },
@@ -77,18 +77,19 @@
77
77
  }
78
78
  },
79
79
  "devDependencies": {
80
+ "@iyulab/components": ">=1.49.0",
80
81
  "@lit/react": "^1.0.8",
81
82
  "@types/node": "^26.1.1",
82
83
  "@types/react": "^19.2.14",
83
84
  "@types/react-dom": "^19.2.3",
84
85
  "@vitest/browser-playwright": "^5.0.0",
86
+ "glob": "^13.0.6",
85
87
  "happy-dom": "^20.10.6",
86
88
  "react": "^19.2.7",
87
89
  "react-dom": "^19.2.7",
88
90
  "typescript": "^6.0.2",
89
91
  "vite": "^8.1.4",
90
92
  "vite-plugin-dts": "^5.0.3",
91
- "vitest": "^5.0.0",
92
- "@iyulab/components": ">=1.47.1"
93
+ "vitest": "^5.0.0"
93
94
  }
94
95
  }
@@ -347,7 +347,9 @@ click — by the same rule as the overlay panel (see *What the shell owns, and w
347
347
  and it marks where the new screen starts.
348
348
 
349
349
  So a screen that must receive input on entry — a barcode scan field, a search box — only needs
350
- `autofocus` on that control. While the overlay is open the shell does none of this: the route
350
+ `autofocus` on that control. The same holds for a React screen (`autoFocus` on a native input, or `autofocus`
351
+ on a component such as `UInput`) — `@iyulab/router` 0.16.1 and later report `route-done` only after
352
+ React has committed the screen. While the overlay is open the shell does none of this: the route
351
353
  underneath is inert and focus stays in the panel.
352
354
 
353
355
  ## Printing
@@ -416,7 +418,7 @@ from either re-implementing what the shell already does, or dropping what it doe
416
418
  | Keeping the route mounted underneath | shell | the overlay is independent of routing |
417
419
  | Moving focus into the panel when it opens | shell | an `[autofocus]` element in your panel, else its first input control, else `part="overlay-close"` — searched through shadow roots, after the panel's first render, so a panel that is itself a component works the same. If you already moved focus into the panel, the shell leaves it there |
418
420
  | Restoring focus when it closes | shell | back to the control that held focus when the panel opened — only if focus fell to `<body>`; if you moved it somewhere on purpose, that stands. With no such control (opened from code), focus goes to `part="main"` |
419
- | Escape to close | shell | Escape inside the panel fires the same `overlay-close` as the button — you still empty the slot. The shell decides **after** every layer inside the panel has: an Escape consumed there (`preventDefault()` — an open list, popover, drawer or dialog, whenever it was opened) closes only that layer. The shell does not mark the Escape consumed itself |
421
+ | Escape to close | shell | Escape inside the panel fires the same `overlay-close` as the button — you still empty the slot. The panel is a layer in `@iyulab/components`' layer stack: layers stack in the order they opened and one Escape closes the topmost, so a list, popover, drawer or dialog opened inside the panel closes first. The Escape that closes the panel is marked consumed (`defaultPrevented`); an Escape pressed outside the panel is left alone. For an app surface of your own, `OverlayManager.openLayer` joins the same order |
420
422
  | **A backdrop / dimmed scrim** | **consumer** | the panel is opaque and full-bleed by design; add a scrim inside your panel if you want one |
421
423
  | **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 |
422
424