@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
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* window 에서
|
|
140
|
-
*
|
|
141
|
-
*
|
|
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 {
|
|
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
|
|
26
|
-
function
|
|
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
|
|
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
|
|
37
|
+
function O(e, t) {
|
|
37
38
|
for (let n of e) if (n instanceof HTMLElement) {
|
|
38
|
-
if (
|
|
39
|
+
if (D(n)) return n === t;
|
|
39
40
|
if (n === t) break;
|
|
40
41
|
}
|
|
41
42
|
return !0;
|
|
42
43
|
}
|
|
43
|
-
function
|
|
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
|
|
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
|
|
57
|
+
var j = class extends i {
|
|
57
58
|
static {
|
|
58
|
-
|
|
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:${++
|
|
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 =
|
|
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
|
|
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 &&
|
|
97
|
+
if (t instanceof HTMLElement && E(t)) return;
|
|
100
98
|
let n = this.shadowRoot?.querySelector(".main");
|
|
101
|
-
if (!n || !
|
|
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),
|
|
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 ? (
|
|
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 ||
|
|
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] ??
|
|
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 =
|
|
346
|
-
if (n && t.some((e) =>
|
|
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 =
|
|
350
|
-
r && i &&
|
|
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 =
|
|
359
|
-
t && e.some((e) =>
|
|
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 =
|
|
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)],
|
|
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 {
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|