@iyulab/modern-app 0.31.0 → 0.33.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 +30 -0
- package/README.md +1 -0
- package/dist/internals/locale.d.ts +2 -0
- package/dist/internals/locale.js +3 -0
- package/dist/layouts/SidebarLayout.d.ts +10 -11
- package/dist/layouts/SidebarLayout.js +44 -35
- package/dist/layouts/SidebarLayout.styles.js +31 -2
- package/package.json +3 -3
- package/skills/modern-app/references/layout.md +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.33.0] - 2026-09-30
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **«Skip to main content» link in the sidebar layout** — the shell's first Tab stop, shown only while
|
|
8
|
+
it has focus (`part="skip-link"`). Activating it moves focus the way a finished route does: to the
|
|
9
|
+
screen's `[autofocus]` element, else to the route area. A keyboard user no longer tabs through the
|
|
10
|
+
whole menu on every screen (WCAG 2.4.1 Bypass Blocks). The text is the new `skipToContent` key of
|
|
11
|
+
`modernAppLocale` (English and Korean built in).
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **The route area (`part="main"`) is a `<main>` element**, so screen readers list it as the main
|
|
16
|
+
landmark. It was a `<div>`. Styling through `::part(main)` and `layout.styles` is unchanged. If your
|
|
17
|
+
route screens render their own `<main>`, change it to a `<div>` or `<section>` — a page should have
|
|
18
|
+
one main landmark.
|
|
19
|
+
|
|
20
|
+
## [0.32.0] - 2026-09-28
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **The shell's overlay panel is a layer in `@iyulab/components`' layer stack.** Escape inside the
|
|
25
|
+
panel still fires `overlay-close`, and a drawer, dialog or popover opened inside the panel still
|
|
26
|
+
closes first — the order now comes from `OverlayManager` (layers stack in the order they opened)
|
|
27
|
+
instead of the shell waiting a task to see who else handled the key. The Escape that closes the
|
|
28
|
+
panel is now marked consumed (`defaultPrevented`), like every layer's; an Escape pressed outside
|
|
29
|
+
the panel is still left alone. Requires `@iyulab/components` 1.49.0 or later.
|
|
30
|
+
- The sidebar and the mobile header read `--u-layer-shell` (1000 and one above it, as before), the
|
|
31
|
+
shared stacking token from `@iyulab/components`.
|
|
32
|
+
|
|
3
33
|
## [0.31.0] - 2026-09-28
|
|
4
34
|
|
|
5
35
|
### Changed
|
package/README.md
CHANGED
|
@@ -276,6 +276,7 @@ conformance claim for the success criteria it does not list.
|
|
|
276
276
|
|---|---|---|
|
|
277
277
|
| SC 2.5.8 Target Size (Minimum) | Every pointer target this package renders itself — sidebar links, buttons and group headers (expanded and compact), the sidebar logo and toggle, wizard steps, the page header's back link — is at least 24×24 CSS px or meets the spacing exception (24px between centers), and is actually hit at that position | `tests/browser/target-size.browser.test.ts` (real Chromium) |
|
|
278
278
|
| SC 2.1.1 Keyboard (pointer-cursor check) | Nothing this package renders shows a pointer cursor without being an interactive element — the sidebar logo is a link | `tests/browser/target-size.browser.test.ts` |
|
|
279
|
+
| SC 2.4.1 Bypass Blocks | The sidebar layout's first Tab stop is a «Skip to main content» link, visible when focused and at least 24px tall; it moves focus to the screen's `[autofocus]` element or to the route area, which is the page's single `<main>` landmark | `tests/browser/sidebar-layout-skip-link.browser.test.ts` |
|
|
279
280
|
|
|
280
281
|
Buttons placed through slots or rendered as `u-button` without size overrides (wizard back/next, the
|
|
281
282
|
master-detail close button) follow `@iyulab/components`, which measures them in its own gate. Color
|
|
@@ -13,6 +13,8 @@ export interface ModernAppLocaleStrings {
|
|
|
13
13
|
toggleMobileMenu: string;
|
|
14
14
|
/** Sidebar layout — accessible label for the sidebar-header collapse/expand toggle button. */
|
|
15
15
|
toggleSidebar: string;
|
|
16
|
+
/** Sidebar layout — the «skip to main content» link, the first Tab stop of the shell. */
|
|
17
|
+
skipToContent: string;
|
|
16
18
|
/** Wizard — default Back/Next action labels ("Submit" wording is the consumer's call). */
|
|
17
19
|
wizardBack: string;
|
|
18
20
|
wizardNext: string;
|
package/dist/internals/locale.js
CHANGED
|
@@ -9,6 +9,7 @@ var t = {
|
|
|
9
9
|
detailClose: "Close",
|
|
10
10
|
toggleMobileMenu: "Toggle menu",
|
|
11
11
|
toggleSidebar: "Toggle sidebar",
|
|
12
|
+
skipToContent: "Skip to main content",
|
|
12
13
|
wizardBack: "Back",
|
|
13
14
|
wizardNext: "Next",
|
|
14
15
|
wizardStepAnnouncement: "Step {index} of {total}: {label}"
|
|
@@ -22,6 +23,7 @@ n.register("en", t), n.register("ko", {
|
|
|
22
23
|
detailClose: "닫기",
|
|
23
24
|
toggleMobileMenu: "메뉴 열고 닫기",
|
|
24
25
|
toggleSidebar: "사이드바 열고 닫기",
|
|
26
|
+
skipToContent: "본문으로 건너뛰기",
|
|
25
27
|
wizardBack: "이전",
|
|
26
28
|
wizardNext: "다음",
|
|
27
29
|
wizardStepAnnouncement: "{total}단계 중 {index}단계: {label}"
|
|
@@ -53,6 +55,7 @@ function l(e) {
|
|
|
53
55
|
detailClose: t("detailClose"),
|
|
54
56
|
toggleMobileMenu: t("toggleMobileMenu"),
|
|
55
57
|
toggleSidebar: t("toggleSidebar"),
|
|
58
|
+
skipToContent: t("skipToContent"),
|
|
56
59
|
wizardBack: t("wizardBack"),
|
|
57
60
|
wizardNext: t("wizardNext"),
|
|
58
61
|
wizardStepAnnouncement: (t, n, r) => {
|
|
@@ -111,6 +111,10 @@ export declare class SidebarLayout extends StyledElement<SidebarParts> {
|
|
|
111
111
|
* 그리기 전에 올 수 있고, 그때 찾으면 `[autofocus]` 가 아직 없다.
|
|
112
112
|
*/
|
|
113
113
|
private focusRoute;
|
|
114
|
+
/** 본문의 시작으로 포커스 — 화면이 선언한 `[autofocus]`, 없으면 본문(`main`) 자체. */
|
|
115
|
+
private focusContentStart;
|
|
116
|
+
/** «본문 바로가기» — 라우트 완료와 같은 규칙으로 본문의 시작에 포커스를 둔다(`#517`). */
|
|
117
|
+
private handleSkipLinkClick;
|
|
114
118
|
/** 라우트 에러 핸들러 */
|
|
115
119
|
private handleRouteError;
|
|
116
120
|
private handleOverlaySlotChange;
|
|
@@ -132,18 +136,13 @@ export declare class SidebarLayout extends StyledElement<SidebarParts> {
|
|
|
132
136
|
*/
|
|
133
137
|
private restoreOverlayFocus;
|
|
134
138
|
/**
|
|
135
|
-
* 패널 안의 Escape 는 닫기 버튼과 같은 `overlay-close` 를
|
|
139
|
+
* 패널 안의 Escape 는 닫기 버튼과 같은 `overlay-close` 를 낸다.
|
|
136
140
|
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* window 에서
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* ⚠마이크로태스크로는 부족하다 — 네이티브 이벤트에서는 리스너 사이마다 마이크로태스크가 비워져,
|
|
143
|
-
* 뒤에 등록된 층보다 먼저 돈다.
|
|
144
|
-
* ⚠셸은 `preventDefault` 하지 않는다 — 가장 바깥 층이 키를 «먹었다» 고 표시하면 안쪽 층의 가드
|
|
145
|
-
* (`defaultPrevented` 면 닫지 않는다)를 오염시킨다.
|
|
146
|
-
* 경로에 패널이 없으면(사이드바, 패널 위에 띄운 대화상자) 받지 않는다.
|
|
141
|
+
* 패널은 components 의 층 스택(`OverlayManager.openLayer`)에 선 한 층이다 — 층은 여는 순서로 쌓이고
|
|
142
|
+
* Escape 한 번은 가장 위 층 하나만 닫으므로, 패널 «안» 에서 나중에 연 서랍·대화상자·팝오버가 먼저
|
|
143
|
+
* 닫힌다. 종전에는 셸이 window 에서 직접 받아 «등록 순서» 가 층 순서를 거슬렀고, 그것을 다음
|
|
144
|
+
* 태스크로 미룬 판정으로 메웠다 — 이제 순서는 한 곳이 정한다.
|
|
145
|
+
* 비모달 패널이라 **포커스가 패널 안일 때만** 닫는다(사이드바에서 누른 Escape 는 거절 — 소비하지 않는다).
|
|
147
146
|
*/
|
|
148
147
|
private handleOverlayEscape;
|
|
149
148
|
private handleOverlayClose;
|
|
@@ -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 = () => {
|
|
@@ -73,6 +74,10 @@ var A = class extends i {
|
|
|
73
74
|
}, 300);
|
|
74
75
|
let t = this.shadowRoot?.querySelector(".main");
|
|
75
76
|
t && (this.config?.scrollBehavior?.(e.context, t), this.focusRoute(t));
|
|
77
|
+
}, this.handleSkipLinkClick = (e) => {
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
let t = this.shadowRoot?.querySelector(".main");
|
|
80
|
+
t && this.focusContentStart(this.hasOverlay ? [] : this.routeContent(), t);
|
|
76
81
|
}, this.handleRouteError = (e) => {
|
|
77
82
|
this.progressBarEl.status = "error", this.progressBarEl.value = 100, setTimeout(() => {
|
|
78
83
|
this.progressBarEl.removeAttribute("visible"), this.progressBarEl.status = "default";
|
|
@@ -80,25 +85,22 @@ var A = class extends i {
|
|
|
80
85
|
}, this.handleOverlaySlotChange = (e) => {
|
|
81
86
|
let t = o(e.target);
|
|
82
87
|
if (t && !this.hasOverlay) {
|
|
83
|
-
let e =
|
|
88
|
+
let e = k();
|
|
84
89
|
this.overlayTrigger = e && e !== document.body ? e : null;
|
|
85
90
|
}
|
|
86
91
|
this.hasOverlay = t;
|
|
87
92
|
}, this.handleNoticeSlotChange = (e) => {
|
|
88
93
|
this.hasNotice = o(e.target);
|
|
89
94
|
}, this.handleOverlayEscape = (e) => {
|
|
90
|
-
if (e.key !== "Escape" || e.isComposing || !this.hasOverlay) return;
|
|
91
95
|
let t = this.shadowRoot?.querySelector(".overlay");
|
|
92
|
-
t
|
|
93
|
-
!e.defaultPrevented && this.hasOverlay && this.handleOverlayClose();
|
|
94
|
-
}, 0);
|
|
96
|
+
return !this.hasOverlay || !t || !e.composedPath().includes(t) ? !1 : (this.handleOverlayClose(), !0);
|
|
95
97
|
}, this.handleOverlayClose = () => {
|
|
96
98
|
this.fire("overlay-close", { cancelable: !1 });
|
|
97
99
|
}, this._handleMainKeydown = (e) => {
|
|
98
100
|
let t = e.composedPath()[0];
|
|
99
|
-
if (t instanceof HTMLElement &&
|
|
101
|
+
if (t instanceof HTMLElement && E(t)) return;
|
|
100
102
|
let n = this.shadowRoot?.querySelector(".main");
|
|
101
|
-
if (!n || !
|
|
103
|
+
if (!n || !O(e.composedPath(), n)) return;
|
|
102
104
|
let r = n.clientHeight;
|
|
103
105
|
switch (e.key) {
|
|
104
106
|
case " ":
|
|
@@ -138,13 +140,13 @@ var A = class extends i {
|
|
|
138
140
|
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
141
|
}
|
|
140
142
|
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),
|
|
143
|
+
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
144
|
}
|
|
143
145
|
willUpdate(e) {
|
|
144
146
|
super.willUpdate(e), e.has("config") && (this.styles = this.config?.styles);
|
|
145
147
|
}
|
|
146
148
|
updated(e) {
|
|
147
|
-
super.updated(e), e.has("hasOverlay") && (this.internals?.states?.[this.hasOverlay ? "add" : "delete"]("overlay"), this.hasOverlay ? (
|
|
149
|
+
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
150
|
}
|
|
149
151
|
firstUpdated(e) {
|
|
150
152
|
super.firstUpdated(e), this.warnIfUnsized();
|
|
@@ -153,11 +155,16 @@ var A = class extends i {
|
|
|
153
155
|
process.env.NODE_ENV !== "production" && requestAnimationFrame(() => {
|
|
154
156
|
if (!this.isConnected) return;
|
|
155
157
|
let e = this.getBoundingClientRect().height;
|
|
156
|
-
e >= 200 ||
|
|
158
|
+
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
159
|
});
|
|
158
160
|
}
|
|
159
161
|
render() {
|
|
160
162
|
return this.config ? l`
|
|
163
|
+
<!-- 반복 영역 건너뛰기(WCAG 2.4.1) — 셸의 첫 Tab. 포커스될 때만 보인다. -->
|
|
164
|
+
<a class="skip-link" part="skip-link" href="#" @click=${this.handleSkipLinkClick}>
|
|
165
|
+
${a(this.locale || void 0).skipToContent}
|
|
166
|
+
</a>
|
|
167
|
+
|
|
161
168
|
<!-- Mobile Header -->
|
|
162
169
|
<div class="mobile-header" part="mobile-header" ?hidden="${!this.state.startsWith("mobile")}">
|
|
163
170
|
${this.renderLogo()}
|
|
@@ -206,7 +213,7 @@ var A = class extends i {
|
|
|
206
213
|
|
|
207
214
|
<!-- Main Content -->
|
|
208
215
|
<div class="main-region">
|
|
209
|
-
<
|
|
216
|
+
<main class="main" part="main" scrollable tabindex="-1" @keydown=${this._handleMainKeydown}>
|
|
210
217
|
<u-progress-bar part="progress"></u-progress-bar>
|
|
211
218
|
|
|
212
219
|
<div class="main-content" part="main-content" ?inert=${this.hasOverlay}>
|
|
@@ -217,7 +224,7 @@ var A = class extends i {
|
|
|
217
224
|
</div>
|
|
218
225
|
<slot></slot>
|
|
219
226
|
</div>
|
|
220
|
-
</
|
|
227
|
+
</main>
|
|
221
228
|
|
|
222
229
|
<div class="overlay ${this.hasOverlay ? "" : "empty"}" part="overlay">
|
|
223
230
|
<u-button class="overlay-close" part="overlay-close" variant="ghost"
|
|
@@ -305,7 +312,7 @@ var A = class extends i {
|
|
|
305
312
|
};
|
|
306
313
|
}
|
|
307
314
|
icon(e) {
|
|
308
|
-
return this.config?.icons?.[e] ??
|
|
315
|
+
return this.config?.icons?.[e] ?? C.DEFAULT_ICONS[e];
|
|
309
316
|
}
|
|
310
317
|
renderLogo() {
|
|
311
318
|
let e = this.config?.logo, t = this.config?.title ?? "Home";
|
|
@@ -342,12 +349,14 @@ var A = class extends i {
|
|
|
342
349
|
async focusRoute(e) {
|
|
343
350
|
let t = this.routeContent();
|
|
344
351
|
if (await Promise.all(t.map((e) => e.updateComplete)), this.hasOverlay || !e.isConnected) return;
|
|
345
|
-
let n =
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
let
|
|
350
|
-
|
|
352
|
+
let n = k();
|
|
353
|
+
n && t.some((e) => A(e, n)) || this.focusContentStart(t, e);
|
|
354
|
+
}
|
|
355
|
+
focusContentStart(e, t) {
|
|
356
|
+
let n = b(e, "[autofocus]", y);
|
|
357
|
+
n?.focus();
|
|
358
|
+
let r = k();
|
|
359
|
+
n && r && A(n, r) || t.focus({ preventScroll: !0 });
|
|
351
360
|
}
|
|
352
361
|
overlayPanels() {
|
|
353
362
|
return (this.shadowRoot?.querySelector("slot[name=\"overlay\"]"))?.assignedElements({ flatten: !0 }) ?? [];
|
|
@@ -355,19 +364,19 @@ var A = class extends i {
|
|
|
355
364
|
async focusOverlay() {
|
|
356
365
|
let e = this.overlayPanels();
|
|
357
366
|
if (await Promise.all(e.map((e) => e.updateComplete)), !this.hasOverlay) return;
|
|
358
|
-
let t =
|
|
359
|
-
t && e.some((e) =>
|
|
367
|
+
let t = k();
|
|
368
|
+
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
369
|
}
|
|
361
370
|
restoreOverlayFocus() {
|
|
362
371
|
let e = this.overlayTrigger;
|
|
363
372
|
this.overlayTrigger = null;
|
|
364
|
-
let t =
|
|
373
|
+
let t = k();
|
|
365
374
|
t && t !== document.body || (e?.isConnected ? e.focus() : this.mainElement?.focus({ preventScroll: !0 }));
|
|
366
375
|
}
|
|
367
376
|
};
|
|
368
377
|
r([f({
|
|
369
378
|
type: String,
|
|
370
379
|
reflect: !0
|
|
371
|
-
}), n("design:type", Object)],
|
|
380
|
+
}), 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
381
|
//#endregion
|
|
373
|
-
export {
|
|
382
|
+
export { j as SidebarLayout };
|
|
@@ -72,10 +72,39 @@ var t = e`
|
|
|
72
72
|
color: var(--u-txt-color-hover, #1565C0);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/* 본문 바로가기 — 포커스될 때만 보인다(보이지 않을 때는 자리도 차지하지 않는다). */
|
|
76
|
+
.skip-link {
|
|
77
|
+
position: absolute;
|
|
78
|
+
top: 8px;
|
|
79
|
+
left: 8px;
|
|
80
|
+
z-index: calc(var(--u-layer-shell, 1000) + 2);
|
|
81
|
+
width: 1px;
|
|
82
|
+
height: 1px;
|
|
83
|
+
overflow: hidden;
|
|
84
|
+
clip-path: inset(50%);
|
|
85
|
+
white-space: nowrap;
|
|
86
|
+
}
|
|
87
|
+
.skip-link:focus {
|
|
88
|
+
width: auto;
|
|
89
|
+
height: auto;
|
|
90
|
+
min-height: 24px;
|
|
91
|
+
overflow: visible;
|
|
92
|
+
clip-path: none;
|
|
93
|
+
display: inline-flex;
|
|
94
|
+
align-items: center;
|
|
95
|
+
padding: 6px 12px;
|
|
96
|
+
border-radius: var(--u-radius-md, 4px);
|
|
97
|
+
background: var(--u-bg-color, #FFFFFF);
|
|
98
|
+
color: var(--u-txt-color, #212121);
|
|
99
|
+
box-shadow: 0 0 0 2px var(--u-primary-color, #1976D2);
|
|
100
|
+
text-decoration: none;
|
|
101
|
+
outline: none;
|
|
102
|
+
}
|
|
103
|
+
|
|
75
104
|
/* Sidebar Container */
|
|
76
105
|
.sidebar {
|
|
77
106
|
position: relative;
|
|
78
|
-
z-index: 1000;
|
|
107
|
+
z-index: var(--u-layer-shell, 1000);
|
|
79
108
|
display: flex;
|
|
80
109
|
flex-direction: column;
|
|
81
110
|
justify-content: space-between;
|
|
@@ -126,7 +155,7 @@ var t = e`
|
|
|
126
155
|
/* Mobile Header */
|
|
127
156
|
.mobile-header {
|
|
128
157
|
position: relative;
|
|
129
|
-
z-index:
|
|
158
|
+
z-index: calc(var(--u-layer-shell, 1000) + 1);
|
|
130
159
|
display: flex;
|
|
131
160
|
flex-direction: row;
|
|
132
161
|
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.33.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.49.0",
|
|
68
68
|
"@lit/react": ">=1.0.8",
|
|
69
69
|
"react": ">=18.0.0"
|
|
70
70
|
},
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
}
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|
|
80
|
-
"@iyulab/components": ">=1.
|
|
80
|
+
"@iyulab/components": ">=1.49.0",
|
|
81
81
|
"@lit/react": "^1.0.8",
|
|
82
82
|
"@types/node": "^26.1.1",
|
|
83
83
|
"@types/react": "^19.2.14",
|
|
@@ -257,6 +257,7 @@ Parts available for `styles` overrides on the root layout:
|
|
|
257
257
|
| Part | Element |
|
|
258
258
|
|------|---------|
|
|
259
259
|
| `host` | Outer layout shell |
|
|
260
|
+
| `skip-link` | «Skip to main content» link — the shell's first Tab stop, shown only while focused |
|
|
260
261
|
| `mobile-header` | Top bar shown on mobile |
|
|
261
262
|
| `sidebar` | Sidebar panel |
|
|
262
263
|
| `sidebar-header` | Logo + title area |
|
|
@@ -333,6 +334,15 @@ told apart in CSS; notices are rare and short-lived.
|
|
|
333
334
|
While the overlay is open, `part="main-content"` becomes a box at the area's height, because a
|
|
334
335
|
stacking context needs one — route content is inert and under the overlay then.
|
|
335
336
|
|
|
337
|
+
### Skip link and main landmark
|
|
338
|
+
|
|
339
|
+
The route area is a `<main>` landmark (`part="main"`), so a screen reader can jump to it. The shell's
|
|
340
|
+
first Tab stop is a **«Skip to main content»** link (`part="skip-link"`, localized — `skipToContent` in
|
|
341
|
+
`modernAppLocale`), visible only while it has focus. Activating it moves focus by the same rule as a
|
|
342
|
+
finished route (steps 2 and 3 below): your screen's `[autofocus]` element, else `main` itself — so a
|
|
343
|
+
keyboard user does not tab through the whole menu on every screen (WCAG 2.4.1 Bypass Blocks). Do not put
|
|
344
|
+
another `<main>` in your route screens; the shell already provides it.
|
|
345
|
+
|
|
336
346
|
### Focus when a route finishes
|
|
337
347
|
|
|
338
348
|
On every `route-done` the shell places focus so the keyboard works on the new screen without a
|
|
@@ -418,7 +428,7 @@ from either re-implementing what the shell already does, or dropping what it doe
|
|
|
418
428
|
| Keeping the route mounted underneath | shell | the overlay is independent of routing |
|
|
419
429
|
| 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 |
|
|
420
430
|
| 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"` |
|
|
421
|
-
| Escape to close | shell | Escape inside the panel fires the same `overlay-close` as the button — you still empty the slot. The
|
|
431
|
+
| 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 |
|
|
422
432
|
| **A backdrop / dimmed scrim** | **consumer** | the panel is opaque and full-bleed by design; add a scrim inside your panel if you want one |
|
|
423
433
|
| **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 |
|
|
424
434
|
|