@iyulab/modern-app 0.28.2 → 0.30.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 +45 -0
- package/dist/layouts/SidebarLayout.d.ts +11 -3
- package/dist/layouts/SidebarLayout.js +8 -4
- package/dist/layouts/SidebarLayout.styles.js +20 -27
- package/dist/layouts/SidebarLayout.types.d.ts +7 -0
- package/package.json +2 -2
- package/skills/modern-app/references/layout.md +28 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.30.0] - 2026-09-27
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`noticesAriaLabel`** in the sidebar layout config. When set, the app-level notice stack
|
|
8
|
+
(`slot="notice"`) is a `role="region"` landmark with that name, so screen-reader users can jump
|
|
9
|
+
back to the notices currently shown. Without it, the only way to name the stack was to wrap the
|
|
10
|
+
notices in your own labelled section — which moved the stack's spacing and full width from each
|
|
11
|
+
notice onto the wrapper. The same shape as `mainAriaLabel`; unset keeps today's unnamed container,
|
|
12
|
+
and an empty stack is not rendered, so no empty landmark appears.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **Escape inside the overlay now closes a drawer or dialog opened in the panel before the
|
|
17
|
+
overlay.** 0.28.0 promised that an Escape consumed inside the panel closes only that layer, but
|
|
18
|
+
that held only for layers listening before the shell: the shell listens on `window` from the
|
|
19
|
+
moment the overlay opens, so a `u-drawer` or `u-dialog` opened afterwards listened later. The
|
|
20
|
+
shell judged first, closed the overlay and marked the key consumed — and the drawer, seeing it
|
|
21
|
+
consumed, stayed open: the outer layer closed and the inner one remained. The shell now decides
|
|
22
|
+
after every layer has, and no longer marks the Escape consumed.
|
|
23
|
+
|
|
24
|
+
## [0.29.0] - 2026-09-27
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- 🔴 **A filling screen with a long table works again — do not use 0.28.2.** 0.28.2 made the
|
|
29
|
+
`main-content` wrapper a grid box with `min-height: 100%` to keep the bottom gutter on long
|
|
30
|
+
screens. A grid track is sized from its item's content, and during that sizing a screen's
|
|
31
|
+
`height: 100%` counts as `auto` — so a screen that fills the area with a table holding more rows
|
|
32
|
+
than fit (a toolbar plus `flex: 1; min-height: 0`) grew to every row, and the table never
|
|
33
|
+
scrolled. The wrapper now creates no box at all (`display: contents`), and neither does the
|
|
34
|
+
outlet (`@iyulab/router` 0.16.0, now required): route content sits directly in the scroll
|
|
35
|
+
container, which is the only arrangement where filling screens, filling screens with long tables
|
|
36
|
+
and long flowing screens all behave — including the bottom gutter 0.28.2 set out to restore.
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- `::part(main-content)` no longer takes box styling (`padding`, `background`, `border`) outside
|
|
41
|
+
the overlay; style `::part(main)` or the screen instead. While the overlay is open it becomes a
|
|
42
|
+
box at the area's height — the overlay's stacking boundary — as before.
|
|
43
|
+
- With an app notice showing, a screen that fills the area overflows by the notice's height (as in
|
|
44
|
+
0.28.1). 0.28.2 avoided that only by breaking long tables; the two kinds of screen cannot be told
|
|
45
|
+
apart in CSS.
|
|
46
|
+
- Requires `@iyulab/router` `^0.16.0`, where `<u-outlet>` also has no box.
|
|
47
|
+
|
|
3
48
|
## [0.28.2] - 2026-09-27
|
|
4
49
|
|
|
5
50
|
### Fixed
|
|
@@ -118,9 +118,17 @@ export declare class SidebarLayout extends StyledElement<SidebarParts> {
|
|
|
118
118
|
*/
|
|
119
119
|
private restoreOverlayFocus;
|
|
120
120
|
/**
|
|
121
|
-
* 패널 안의 Escape 는 닫기 버튼과 같은 `overlay-close` 를
|
|
122
|
-
*
|
|
123
|
-
*
|
|
121
|
+
* 패널 안의 Escape 는 닫기 버튼과 같은 `overlay-close` 를 낸다 — 단, **안쪽 층이 모두 판정한 뒤에**.
|
|
122
|
+
*
|
|
123
|
+
* 셸의 오버레이는 가장 바깥 층이다. 그런데 window 의 같은 단계 리스너는 «등록 순서» 로 돌고,
|
|
124
|
+
* 셸은 오버레이가 열릴 때 등록하므로 패널 안에서 «나중에» 연 층(`u-drawer`·`u-dialog` — 역시
|
|
125
|
+
* window 에서 받는다)보다 먼저 돈다. 거기서 판정하면 바깥이 닫히고 안쪽이 남는다(거꾸로다).
|
|
126
|
+
* ⇒ 리스너에서는 판정하지 않고 **디스패치가 끝난 뒤**(다음 태스크) `defaultPrevented` 를 본다 —
|
|
127
|
+
* 그때는 어느 순서로 등록된 층이든 이미 자기 몫을 가져갔다. 한 번의 Escape 는 한 층만 닫는다.
|
|
128
|
+
* ⚠마이크로태스크로는 부족하다 — 네이티브 이벤트에서는 리스너 사이마다 마이크로태스크가 비워져,
|
|
129
|
+
* 뒤에 등록된 층보다 먼저 돈다.
|
|
130
|
+
* ⚠셸은 `preventDefault` 하지 않는다 — 가장 바깥 층이 키를 «먹었다» 고 표시하면 안쪽 층의 가드
|
|
131
|
+
* (`defaultPrevented` 면 닫지 않는다)를 오염시킨다.
|
|
124
132
|
* 경로에 패널이 없으면(사이드바, 패널 위에 띄운 대화상자) 받지 않는다.
|
|
125
133
|
*/
|
|
126
134
|
private handleOverlayEscape;
|
|
@@ -86,9 +86,11 @@ var O = class extends i {
|
|
|
86
86
|
}, this.handleNoticeSlotChange = (e) => {
|
|
87
87
|
this.hasNotice = o(e.target);
|
|
88
88
|
}, this.handleOverlayEscape = (e) => {
|
|
89
|
-
if (e.key !== "Escape" || e.
|
|
89
|
+
if (e.key !== "Escape" || e.isComposing || !this.hasOverlay) return;
|
|
90
90
|
let t = this.shadowRoot?.querySelector(".overlay");
|
|
91
|
-
t && e.composedPath().includes(t) && (
|
|
91
|
+
t && e.composedPath().includes(t) && setTimeout(() => {
|
|
92
|
+
!e.defaultPrevented && this.hasOverlay && this.handleOverlayClose();
|
|
93
|
+
}, 0);
|
|
92
94
|
}, this.handleOverlayClose = () => {
|
|
93
95
|
this.fire("overlay-close", { cancelable: !1 });
|
|
94
96
|
}, this._handleMainKeydown = (e) => {
|
|
@@ -206,8 +208,10 @@ var O = class extends i {
|
|
|
206
208
|
<div class="main" part="main" scrollable tabindex="-1" @keydown=${this._handleMainKeydown}>
|
|
207
209
|
<u-progress-bar part="progress"></u-progress-bar>
|
|
208
210
|
|
|
209
|
-
<div class="main-content
|
|
210
|
-
<div class="notices ${this.hasNotice ? "" : "empty"}" part="notices"
|
|
211
|
+
<div class="main-content" part="main-content" ?inert=${this.hasOverlay}>
|
|
212
|
+
<div class="notices ${this.hasNotice ? "" : "empty"}" part="notices"
|
|
213
|
+
role=${this.config.noticesAriaLabel ? "region" : u}
|
|
214
|
+
aria-label=${this.config.noticesAriaLabel ?? u}>
|
|
211
215
|
<slot name="notice" @slotchange=${this.handleNoticeSlotChange}></slot>
|
|
212
216
|
</div>
|
|
213
217
|
<slot></slot>
|
|
@@ -208,29 +208,23 @@ var t = e`
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
/*
|
|
211
|
-
* 라우트 본문을 감싸는
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
* height: 100%
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
* auto
|
|
220
|
-
*
|
|
211
|
+
* 라우트 본문을 감싸는 래퍼 — 평소에는 «상자를 만들지 않는다»(display: contents).
|
|
212
|
+
*
|
|
213
|
+
* 높이가 정해진 스크롤 컨테이너(.main) 안에서 라우트 화면은 세 형태다: ⑴채움(height: 100%)
|
|
214
|
+
* ⑵채움-큼(채우되 안의 표가 뷰포트보다 많은 행을 자기 안에서 굴린다) ⑶흐름(길어지고 .main 이
|
|
215
|
+
* 스크롤한다). 사이에 상자가 있으면 셋 중 하나가 반드시 깨진다(실측):
|
|
216
|
+
* · height: 100% → 상자가 못 박혀 ⑶의 화면이 밖으로 넘치고, .main 은 끝 padding 을 in-flow
|
|
217
|
+
* 자식(= 이 래퍼) 뒤에 붙이므로 스크롤 끝의 거터가 사라진다.
|
|
218
|
+
* · grid + min-height: 100% → ⑶은 되지만 트랙이 항목의 내용 크기로 잡히고 그 계산 동안
|
|
219
|
+
* height: 100% 는 auto 로 취급되어, ⑵의 표가 모든 행 높이로 자란다.
|
|
220
|
+
* 상자가 없으면 화면이 .main 을 직접 받는다. 아웃렛(u-outlet)도 같은 이유로 상자가 없다.
|
|
221
|
+
*
|
|
222
|
+
* ⚠공지(slot="notice")가 있으면 채우는 화면은 그 높이만큼 넘친다 — 공지는 본문 위 흐름 안에
|
|
223
|
+
* 있고, 채우는 화면의 height: 100% 는 공지를 모른다. 두 형태를 CSS 로 가를 수 없어 알려진
|
|
224
|
+
* 한계로 둔다(공지는 드물고 잠깐이다).
|
|
221
225
|
*/
|
|
222
226
|
.main-content {
|
|
223
|
-
display:
|
|
224
|
-
min-height: 100%;
|
|
225
|
-
}
|
|
226
|
-
/*
|
|
227
|
-
* 공지가 있으면 그 행은 내용 높이로 묶는다. 늘리기는 max 크기가 auto 인 트랙에만
|
|
228
|
-
* 적용되므로 max-content 행은 늘어나지 않고, 남는 높이는 뒤따르는 본문 행이 받는다.
|
|
229
|
-
* 공지가 없으면(.notices 가 display: none) 이 규칙도 없어야 한다 — 있으면 본문의 첫
|
|
230
|
-
* 자식이 이 행에 앉아 채우기를 잃는다.
|
|
231
|
-
*/
|
|
232
|
-
.main-content.has-notice {
|
|
233
|
-
grid-template-rows: max-content;
|
|
227
|
+
display: contents;
|
|
234
228
|
}
|
|
235
229
|
|
|
236
230
|
/*
|
|
@@ -285,6 +279,11 @@ var t = e`
|
|
|
285
279
|
* 경계에 걸리지 않고, 오버레이가 열린 채로 일어나는 라우트 전환을 계속 보고한다.
|
|
286
280
|
*/
|
|
287
281
|
:host(:state(overlay)) .main-content {
|
|
282
|
+
/* 쌓임 맥락은 상자가 있어야 생긴다 — 오버레이가 열린 동안만 상자를 둔다(본문은 inert 이고
|
|
283
|
+
오버레이 아래에 있다). height: 100% 라 채우는 화면은 그대로이고, 흐름 화면의 끝 거터만 그동안
|
|
284
|
+
빠진다. */
|
|
285
|
+
display: block;
|
|
286
|
+
height: 100%;
|
|
288
287
|
isolation: isolate;
|
|
289
288
|
}
|
|
290
289
|
|
|
@@ -365,12 +364,6 @@ var t = e`
|
|
|
365
364
|
padding: 0;
|
|
366
365
|
overflow: visible;
|
|
367
366
|
}
|
|
368
|
-
/* 인쇄는 쪽 나눔이 흐름을 따라야 하므로 grid 를 풀고 높이를 내용에 맡긴다
|
|
369
|
-
(아웃렛의 인쇄 규칙과 같은 이유 — grid 상자 안 끝 블록 여백이 쪽 경계를 넘는다). */
|
|
370
|
-
.main-content {
|
|
371
|
-
display: block;
|
|
372
|
-
min-height: 0;
|
|
373
|
-
}
|
|
374
367
|
:host(:state(overlay)) .main-content {
|
|
375
368
|
display: none;
|
|
376
369
|
}
|
|
@@ -78,6 +78,13 @@ export interface SidebarLayoutConfig {
|
|
|
78
78
|
* 별개 값이다. 미지정 시 종전과 동일하게 랜드마크 이름이 없다(breaking 아님).
|
|
79
79
|
*/
|
|
80
80
|
mainAriaLabel?: string;
|
|
81
|
+
/**
|
|
82
|
+
* 앱 수준 공지 스택(`slot="notice"`)의 접근 가능한 이름. 지정하면 스택이 `role="region"` +
|
|
83
|
+
* `aria-label` 인 랜드마크가 되어, 스크린리더 사용자가 «지금 떠 있는 공지들» 로 건너뛰어
|
|
84
|
+
* 다시 읽을 수 있다(`region` 은 이름이 있을 때만 랜드마크다). 공지가 없으면 스택 자체가 없으므로
|
|
85
|
+
* 빈 랜드마크는 생기지 않는다. 미지정 시 종전과 같이 이름 없는 컨테이너다.
|
|
86
|
+
*/
|
|
87
|
+
noticesAriaLabel?: string;
|
|
81
88
|
/**
|
|
82
89
|
* 메뉴 항목 권한 필터 판정. 지정하면 `requirePermission`/`requireAnyPermission` 를 만족하지
|
|
83
90
|
* 않는 항목을 숨기고, 항목이 모두 걸러진 section/group 은 통째로 숨긴다.
|
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.30.0",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"iyulab",
|
|
7
7
|
"web-framework",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"test:react-types": "tsc -p tsconfig.react-smoke.json"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@iyulab/router": "^0.
|
|
62
|
+
"@iyulab/router": "^0.16.0",
|
|
63
63
|
"i18next": "^26.3.6",
|
|
64
64
|
"lit": "^3.3.3"
|
|
65
65
|
},
|
|
@@ -21,6 +21,9 @@ interface SidebarLayoutConfig {
|
|
|
21
21
|
/** Accessible name for the main nav landmark (`<nav class="sidebar-main">`), reflected as `aria-label`. Unset by default. */
|
|
22
22
|
mainAriaLabel?: string;
|
|
23
23
|
|
|
24
|
+
/** Accessible name for the app-level notice stack (`slot="notice"`). When set, the stack is a `role="region"` landmark with this `aria-label`. Unset by default. */
|
|
25
|
+
noticesAriaLabel?: string;
|
|
26
|
+
|
|
24
27
|
/** Permission filter — hides items (and emptied section/groups) whose requirement fails. Unset shows everything. See "권한 기반 메뉴 필터" below. */
|
|
25
28
|
hasPermission?: (code: string) => boolean;
|
|
26
29
|
|
|
@@ -260,7 +263,7 @@ Parts available for `styles` overrides on the root layout:
|
|
|
260
263
|
| `sidebar-main` | Scrollable main nav area |
|
|
261
264
|
| `sidebar-footer` | Pinned footer area |
|
|
262
265
|
| `main` | Main content area (the scroll container) |
|
|
263
|
-
| `main-content` | Wrapper holding route content inside `main` —
|
|
266
|
+
| `main-content` | Wrapper holding route content inside `main` — creates no box, so route content sits directly in `main` (see [Route content area](#route-content-area)); the shell puts `inert` here while the overlay is open, and gives it a box then as the overlay's stacking boundary |
|
|
264
267
|
| `progress` | Top progress bar |
|
|
265
268
|
| `overlay` | Route-independent overlay panel above `main` |
|
|
266
269
|
| `overlay-close` | Overlay's close button |
|
|
@@ -307,17 +310,28 @@ u-sidebar-layout::part(main) { padding: 24px; }
|
|
|
307
310
|
⚠ `layout.styles` values are inline styles, so they also apply on print media (see [Printing](#printing));
|
|
308
311
|
use `::part(main)` inside `@media screen` if the change is for the screen only.
|
|
309
312
|
|
|
310
|
-
Inside it,
|
|
313
|
+
Inside it, the route content sits **directly** in that scroll container: neither
|
|
314
|
+
`part="main-content"` nor `<u-outlet>` creates a box (`display: contents`). That is what lets all
|
|
315
|
+
three kinds of screen work:
|
|
316
|
+
|
|
317
|
+
| Screen | Behaviour |
|
|
318
|
+
|---|---|
|
|
319
|
+
| **Fills the area** — `height: 100%`, or a layout such as `u-master-detail-layout` | gets the area minus its gutters |
|
|
320
|
+
| **Fills the area, more content than fits** — a toolbar plus a table with `flex: 1; min-height: 0` and more rows than the viewport | stays at the area's height; the table scrolls inside itself |
|
|
321
|
+
| **Flows** — a form or document taller than the area | `main` scrolls, and the bottom gutter stays at the end of the scroll |
|
|
311
322
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
323
|
+
Any box in between breaks one of them — a box pinned at `height: 100%` loses the bottom gutter on
|
|
324
|
+
flowing screens, and a grid box with `min-height: 100%` grows a filling screen to every row of its
|
|
325
|
+
table. So `::part(main-content)` accepts no box styling (`padding`, `background`, `border` do
|
|
326
|
+
nothing); style `::part(main)` or the screen instead.
|
|
316
327
|
|
|
317
|
-
App notices (`slot="notice"`) sit at the top of
|
|
318
|
-
|
|
328
|
+
⚠ **App notices push a filling screen down.** Notices (`slot="notice"`) sit at the top of the
|
|
329
|
+
content at their own height, and a screen's `height: 100%` does not know about them, so while a
|
|
330
|
+
notice shows, a filling screen overflows by the notice's height. The two kinds of screen cannot be
|
|
331
|
+
told apart in CSS; notices are rare and short-lived.
|
|
319
332
|
|
|
320
|
-
|
|
333
|
+
While the overlay is open, `part="main-content"` becomes a box at the area's height, because a
|
|
334
|
+
stacking context needs one — route content is inert and under the overlay then.
|
|
321
335
|
|
|
322
336
|
## Printing
|
|
323
337
|
|
|
@@ -385,7 +399,7 @@ from either re-implementing what the shell already does, or dropping what it doe
|
|
|
385
399
|
| Keeping the route mounted underneath | shell | the overlay is independent of routing |
|
|
386
400
|
| Moving focus into the panel when it opens | shell | an `[autofocus]` element in your panel, else its first input control, else `part="overlay-close"`. If you already moved focus into the panel, the shell leaves it there |
|
|
387
401
|
| 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"` |
|
|
388
|
-
| Escape to close | shell | Escape inside the panel fires the same `overlay-close` as the button — you still empty the slot.
|
|
402
|
+
| 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 |
|
|
389
403
|
| **A backdrop / dimmed scrim** | **consumer** | the panel is opaque and full-bleed by design; add a scrim inside your panel if you want one |
|
|
390
404
|
| **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 |
|
|
391
405
|
|
|
@@ -424,6 +438,10 @@ Put them in `slot="notice"`; the shell stacks them at the top of the route conte
|
|
|
424
438
|
- **Full width.** Each notice takes the width of the content box, whatever its text length.
|
|
425
439
|
- **Empty takes no space** — the stack has no margin while nothing is slotted.
|
|
426
440
|
- **Inert with the route content** while the overlay is open.
|
|
441
|
+
- **A named landmark on request.** Set `noticesAriaLabel` in the layout config and the stack
|
|
442
|
+
becomes a `role="region"` with that name, so screen-reader users can jump back to the notices
|
|
443
|
+
currently shown. Put the notices in the slot directly — wrapping them in your own named section
|
|
444
|
+
puts the stack's spacing and width on the wrapper instead of on each notice.
|
|
427
445
|
- Which surface a notice is (toast, banner or modal) is decided by *who ends it* — a notice the time
|
|
428
446
|
ends is a toast (`app.success`), not a slotted banner.
|
|
429
447
|
|