@iyulab/modern-app 0.26.0 → 0.28.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 +50 -0
- package/README.md +12 -9
- package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/components/ActionBar.js +2 -2
- package/dist/components/EmptyState.d.ts +2 -2
- package/dist/components/EmptyState.js +2 -2
- package/dist/components/GroupBox.d.ts +11 -2
- package/dist/components/GroupBox.js +4 -4
- package/dist/components/GroupBox.styles.js +8 -0
- package/dist/components/InfoField.js +2 -2
- package/dist/components/InfoSection.js +2 -2
- package/dist/components/MasterDetailLayout.js +2 -2
- package/dist/components/PageHeader.d.ts +1 -1
- package/dist/components/PageHeader.js +2 -2
- package/dist/components/SidebarButton.js +2 -2
- package/dist/components/SidebarGroup.js +2 -2
- package/dist/components/SidebarLink.js +2 -2
- package/dist/components/SidebarSection.js +2 -2
- package/dist/components/Wizard.js +2 -2
- package/dist/internals/StyledElement.js +2 -2
- package/dist/internals/locale.js +13 -1
- package/dist/layouts/SidebarLayout.d.ts +31 -0
- package/dist/layouts/SidebarLayout.js +59 -10
- package/dist/layouts/SidebarLayout.styles.js +25 -0
- package/package.json +3 -3
- package/skills/modern-app/SKILL.md +12 -9
- package/skills/modern-app/references/components/empty-state.md +3 -3
- package/skills/modern-app/references/components/group-box.md +4 -1
- package/skills/modern-app/references/components/page-header.md +2 -3
- package/skills/modern-app/references/layout.md +27 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.28.0] - 2026-09-24
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`slot="notice"` on `u-sidebar-layout` — a place for app-level notices.** A connection-lost
|
|
8
|
+
banner or a new-version notice belongs to the shell, not to one screen; a screen that places its
|
|
9
|
+
own fixed banner collides with the next notice someone adds. Slotted notices stack at the top of
|
|
10
|
+
the route content, in flow (they scroll away with the content instead of holding a strip of a
|
|
11
|
+
small screen, and never cover it), each at the full content width. The stack takes no space
|
|
12
|
+
while empty and goes inert with the route content while the overlay is open. New `notices` part.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- **The shell now owns the overlay's keyboard contract.** It already made route content `inert`
|
|
17
|
+
while the overlay is open — and the control that opened the overlay lives in that content, so
|
|
18
|
+
its focus fell to `<body>` the moment the panel appeared. Every consumer re-implemented the same
|
|
19
|
+
three steps; the shell now does them:
|
|
20
|
+
- **focus moves into the panel** when it opens — an `[autofocus]` element, else the first input
|
|
21
|
+
control, else the close button. If you already moved focus into the panel, it stays there.
|
|
22
|
+
- **focus returns to the opening control** when it closes, if focus fell to `<body>`; if you
|
|
23
|
+
moved it elsewhere on purpose, that stands. With no opening control, it goes to `part="main"`.
|
|
24
|
+
- **Escape inside the panel fires `overlay-close`**, the same event as the close button — you
|
|
25
|
+
still empty the slot. An Escape already consumed inside the panel (an open list or popover)
|
|
26
|
+
closes only that layer.
|
|
27
|
+
Existing consumer code that does any of this itself keeps working: the shell steps aside when
|
|
28
|
+
focus is already where it would put it, and emptying the slot twice is harmless.
|
|
29
|
+
- Requires `@iyulab/components` 1.45.2 or later (`u-button.focus()`).
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- A route transition that completes while the overlay is open no longer pulls focus out of the
|
|
34
|
+
panel onto the (inert) route's scroll container.
|
|
35
|
+
|
|
36
|
+
## [0.27.0] - 2026-09-24
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- **`u-group-box[meta]` — secondary text beside the title.** List-like sections of a detail screen
|
|
41
|
+
almost always carry a count (`3 items`, `2/5 done`), and with `title` being a plain string the
|
|
42
|
+
count was drawn at the title's size, weight and color, so the section name and its current
|
|
43
|
+
state read as one heading. `meta` draws that text one step below the title — body size and
|
|
44
|
+
weight, weak color, tabular figures — on the same baseline. It sits inside the heading, so the
|
|
45
|
+
heading's accessible name includes it. It is not drawn without a `title`. New `meta` part.
|
|
46
|
+
- **Korean built-in strings.** 0.26.0 moved the language choice to `Locale.set()`, but the namespace
|
|
47
|
+
held only an English table, so an app that already called `Locale.set('ko')` still got the
|
|
48
|
+
sidebar toggle's and overlay close button's accessible names, the back link, empty-state copy and
|
|
49
|
+
wizard labels in English. A Korean table now ships alongside English — the same scope as
|
|
50
|
+
`@iyulab/flex-table`. English stays the default; a Korean registration of your own overrides only
|
|
51
|
+
the keys it gives.
|
|
52
|
+
|
|
3
53
|
## [0.26.0] - 2026-09-23
|
|
4
54
|
|
|
5
55
|
### Changed
|
package/README.md
CHANGED
|
@@ -121,23 +121,26 @@ html`<p>${translate('common::greeting')}</p>`;
|
|
|
121
121
|
### Built-in strings
|
|
122
122
|
|
|
123
123
|
The package's own strings — the page header's back link, empty-state copy, the shell's toggle and
|
|
124
|
-
close labels, the wizard's buttons and step announcement —
|
|
125
|
-
|
|
126
|
-
language once, with `Locale.set()
|
|
124
|
+
close labels, the wizard's buttons and step announcement — live in the `modern-app` namespace of
|
|
125
|
+
`@iyulab/components`' `Locale`. English and Korean are built in, English being the default; pick the
|
|
126
|
+
language once, with `Locale.set()` — the primitives and `@iyulab/flex-table` follow the same call.
|
|
127
|
+
Register other languages, or your own wording, in the same namespace:
|
|
127
128
|
|
|
128
129
|
```typescript
|
|
129
130
|
import { Locale } from '@iyulab/components';
|
|
130
131
|
import { modernAppLocale } from '@iyulab/modern-app';
|
|
131
132
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
Locale.set('ko'); // built-in Korean — nothing to register
|
|
134
|
+
|
|
135
|
+
modernAppLocale.register('ja', {
|
|
136
|
+
back: '戻る',
|
|
137
|
+
toggleSidebar: 'サイドバーの切り替え',
|
|
138
|
+
wizardStepAnnouncement: 'ステップ {index}/{total}: {label}',
|
|
136
139
|
});
|
|
137
|
-
Locale.set('ko');
|
|
138
140
|
```
|
|
139
141
|
|
|
140
|
-
A partial table is enough — untranslated keys fall back to English
|
|
142
|
+
A partial table is enough — untranslated keys fall back to English, and a registration for a
|
|
143
|
+
built-in language overrides only the keys it gives. An element's own `locale`
|
|
141
144
|
attribute overrides the active language for that element. `registerLocale`/`setDefaultLocale` still
|
|
142
145
|
work but are deprecated: a `setDefaultLocale()` call takes precedence over `Locale` until it is
|
|
143
146
|
called with `undefined`.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.151.0/helpers/esm/decorate.js
|
|
2
2
|
function e(e, t, n, r) {
|
|
3
3
|
var i = arguments.length, a = i < 3 ? t : r === null ? r = Object.getOwnPropertyDescriptor(t, n) : r, o;
|
|
4
4
|
if (typeof Reflect == "object" && typeof Reflect.decorate == "function") a = Reflect.decorate(e, t, n, r);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.151.0/helpers/esm/decorateMetadata.js
|
|
2
2
|
function e(e, t) {
|
|
3
3
|
if (typeof Reflect == "object" && typeof Reflect.metadata == "function") return Reflect.metadata(e, t);
|
|
4
4
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import e from "../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import t from "../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import e from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import t from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
|
|
3
3
|
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
4
|
import { slotHasContent as r } from "../internals/slotted.js";
|
|
5
5
|
import { styles as i } from "./ActionBar.styles.js";
|
|
@@ -19,7 +19,7 @@ type ElementParts = 'host' | 'icon' | 'title' | 'description' | 'actions';
|
|
|
19
19
|
* ```
|
|
20
20
|
*
|
|
21
21
|
* ⚠**기본 문구는 영어다** — 이 패키지는 범용 층이라 특정 언어를 기본값으로 가질 수 없다.
|
|
22
|
-
*
|
|
22
|
+
* 한국어 표는 내장돼 있고(언어는 `Locale.set()`), 다른 언어는 `modernAppLocale.register(lang, …)` 로 소비자가 등록한다. 화면별로 덮으려면
|
|
23
23
|
* `title`·`description` 을 준다.
|
|
24
24
|
*
|
|
25
25
|
* ★이 줄은 «한국어다»라고 적혀 있었고 **같은 파일의 `locale` 프로퍼티 주석이 «영어다»라고
|
|
@@ -37,7 +37,7 @@ export declare class EmptyState extends StyledElement<ElementParts> {
|
|
|
37
37
|
/**
|
|
38
38
|
* 언어 태그 — 이 요소만 문서와 다른 언어로 쓸 때. 비우면 `Locale` 의 활성 로케일.
|
|
39
39
|
* ⚠기본 문구는 영어다 — 이 패키지는 범용 층이라 특정 언어를 기본값으로 가질 수 없다.
|
|
40
|
-
*
|
|
40
|
+
* 한국어 표는 내장돼 있다 — 다른 언어는 `modernAppLocale.register(lang, …)` 로 소비자가 등록한다.
|
|
41
41
|
*/
|
|
42
42
|
locale: string;
|
|
43
43
|
/** 액션 슬롯 배정 상태 — CSS `:has()` 로는 알 수 없다(`internals/slotted.ts` 참조). */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import e from "../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import t from "../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import e from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import t from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
|
|
3
3
|
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
4
|
import { getLocaleStrings as r } from "../internals/locale.js";
|
|
5
5
|
import { slotHasContent as i } from "../internals/slotted.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { StyledElement } from '../internals/StyledElement.js';
|
|
2
2
|
import { default as React } from 'react';
|
|
3
|
-
type ElementParts = 'host' | 'header' | 'title' | 'actions' | 'body';
|
|
3
|
+
type ElementParts = 'host' | 'header' | 'title' | 'meta' | 'actions' | 'body';
|
|
4
4
|
/** 제목 단계 — 페이지 제목(h1)은 `u-page-header` 몫이라 2 부터다. */
|
|
5
5
|
export type GroupBoxLevel = 2 | 3 | 4 | 5 | 6;
|
|
6
6
|
/**
|
|
@@ -18,13 +18,21 @@ export type GroupBoxLevel = 2 | 3 | 4 | 5 | 6;
|
|
|
18
18
|
* </u-group-box>
|
|
19
19
|
* ```
|
|
20
20
|
*
|
|
21
|
-
* 오버라이드: `part`(host·header·title·actions·body) + slot 치환.
|
|
21
|
+
* 오버라이드: `part`(host·header·title·meta·actions·body) + slot 치환.
|
|
22
22
|
* `divider` 속성으로 제목과 본문 사이 구분선을 켠다(기본 꺼짐 — 선이 많으면 화면이 시끄럽다).
|
|
23
23
|
*/
|
|
24
24
|
export declare class GroupBox extends StyledElement<ElementParts> {
|
|
25
25
|
static styles: import('lit').CSSResultGroup[];
|
|
26
26
|
/** 카드 제목. 비우면 헤더 자체를 렌더하지 않는다. */
|
|
27
27
|
title: string;
|
|
28
|
+
/**
|
|
29
|
+
* Secondary text shown after the title — a count, progress or short status
|
|
30
|
+
* (`3 items`, `2/5 done`), drawn one step below the title: body size and weight, weak color.
|
|
31
|
+
*
|
|
32
|
+
* It sits inside the title's heading, so the heading's accessible name reads it too. Not drawn
|
|
33
|
+
* without a `title`: it describes the title.
|
|
34
|
+
*/
|
|
35
|
+
meta?: string;
|
|
28
36
|
/** 제목과 본문 사이에 구분선을 넣는다. */
|
|
29
37
|
divider: boolean;
|
|
30
38
|
/** 본문 여백을 없앤다 — 표를 카드 가장자리까지 붙일 때. */
|
|
@@ -60,6 +68,7 @@ declare module 'react' {
|
|
|
60
68
|
divider?: boolean;
|
|
61
69
|
flush?: boolean;
|
|
62
70
|
level?: GroupBoxLevel;
|
|
71
|
+
meta?: string;
|
|
63
72
|
};
|
|
64
73
|
}
|
|
65
74
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import e from "../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import t from "../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import e from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import t from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
|
|
3
3
|
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
4
|
import { slotHasContent as r } from "../internals/slotted.js";
|
|
5
5
|
import { styles as i } from "./GroupBox.styles.js";
|
|
@@ -24,7 +24,7 @@ var d = {
|
|
|
24
24
|
let e = !!this.title || this.hasActions;
|
|
25
25
|
return a`
|
|
26
26
|
<div class="header ${this.divider ? "divider" : ""} ${e ? "" : "empty"}" part="header">
|
|
27
|
-
${l`<${d[this.level] ?? d[3]} class="title" part="title">${this.title}</${d[this.level] ?? d[3]}>`}
|
|
27
|
+
${l`<${d[this.level] ?? d[3]} class="title" part="title">${this.title}${this.title && this.meta ? a` <span class="meta" part="meta">${this.meta}</span>` : ""}</${d[this.level] ?? d[3]}>`}
|
|
28
28
|
<div class="actions ${this.hasActions ? "" : "empty"}" part="actions">
|
|
29
29
|
<slot name="actions"
|
|
30
30
|
@slotchange=${(e) => this.hasActions = r(e.target)}
|
|
@@ -35,7 +35,7 @@ var d = {
|
|
|
35
35
|
`;
|
|
36
36
|
}
|
|
37
37
|
};
|
|
38
|
-
t([s({ type: String }), e("design:type", Object)], f.prototype, "title", void 0), t([s({ type: Boolean }), e("design:type", Object)], f.prototype, "divider", void 0), t([s({ type: Boolean }), e("design:type", Object)], f.prototype, "flush", void 0), t([s({
|
|
38
|
+
t([s({ type: String }), e("design:type", Object)], f.prototype, "title", void 0), t([s({ type: String }), e("design:type", String)], f.prototype, "meta", void 0), t([s({ type: Boolean }), e("design:type", Object)], f.prototype, "divider", void 0), t([s({ type: Boolean }), e("design:type", Object)], f.prototype, "flush", void 0), t([s({
|
|
39
39
|
type: Number,
|
|
40
40
|
reflect: !0
|
|
41
41
|
}), e("design:type", Object)], f.prototype, "level", void 0), t([c(), e("design:type", Object)], f.prototype, "hasActions", void 0), f = t([o("u-group-box")], f);
|
|
@@ -42,6 +42,14 @@ var t = e`
|
|
|
42
42
|
letter-spacing: var(--u-text-subtitle-tracking, 0);
|
|
43
43
|
color: var(--u-txt-color, #212121);
|
|
44
44
|
}
|
|
45
|
+
/* 제목 옆 보조 메타(건수·진척) — 제목보다 한 단 아래. 헤딩 안의 인라인이라 기준선을 공유하고,
|
|
46
|
+
접근성 이름에도 함께 읽힌다. 숫자가 바뀌어도 폭이 흔들리지 않게 고정폭 숫자. */
|
|
47
|
+
.meta {
|
|
48
|
+
font-size: var(--u-text-body-size, 14px);
|
|
49
|
+
font-weight: var(--u-text-body-weight, 400);
|
|
50
|
+
color: var(--u-txt-color-weak, #616161);
|
|
51
|
+
font-variant-numeric: tabular-nums;
|
|
52
|
+
}
|
|
45
53
|
|
|
46
54
|
.actions {
|
|
47
55
|
flex: 0 0 auto;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import e from "../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import t from "../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import e from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import t from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
|
|
3
3
|
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
4
|
import { styles as r } from "./InfoField.styles.js";
|
|
5
5
|
import { html as i } from "lit";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import e from "../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import t from "../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import e from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import t from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
|
|
3
3
|
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
4
|
import { styles as r } from "./InfoSection.styles.js";
|
|
5
5
|
import { html as i } from "lit";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import e from "../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import t from "../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import e from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import t from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
|
|
3
3
|
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
4
|
import { getLocaleStrings as r } from "../internals/locale.js";
|
|
5
5
|
import { slotHasContent as i } from "../internals/slotted.js";
|
|
@@ -33,7 +33,7 @@ export declare class PageHeader extends StyledElement<ElementParts> {
|
|
|
33
33
|
/**
|
|
34
34
|
* 뒤로가기 링크 문구. 비우면 locale 레지스트리 값, 그것도 없으면 **영어**(`Back`).
|
|
35
35
|
* ⚠이 패키지는 범용 층이라 특정 언어를 기본값으로 가질 수 없다 —
|
|
36
|
-
*
|
|
36
|
+
* 한국어 표는 내장돼 있다 — 다른 언어는 `modernAppLocale.register(lang, { back: … })` 로 소비자가 등록한다.
|
|
37
37
|
*/
|
|
38
38
|
backLabel: string;
|
|
39
39
|
/** 언어 태그 — 이 요소만 문서와 다른 언어로 쓸 때. 비우면 `Locale` 의 활성 로케일. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import e from "../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import t from "../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import e from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import t from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
|
|
3
3
|
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
4
|
import { getLocaleStrings as r } from "../internals/locale.js";
|
|
5
5
|
import { slotHasContent as i } from "../internals/slotted.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import e from "../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import t from "../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import e from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import t from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
|
|
3
3
|
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
4
|
import { DEFAULT_NAV_ICON as r } from "../internals/nav-icon.js";
|
|
5
5
|
import { styles as i } from "./SidebarButton.styles.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import e from "../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import t from "../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import e from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import t from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
|
|
3
3
|
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
4
|
import { DEFAULT_NAV_ICON as r } from "../internals/nav-icon.js";
|
|
5
5
|
import { SidebarLink as i } from "./SidebarLink.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import e from "../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import t from "../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import e from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import t from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
|
|
3
3
|
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
4
|
import { DEFAULT_NAV_ICON as r } from "../internals/nav-icon.js";
|
|
5
5
|
import { styles as i } from "./SidebarLink.styles.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import e from "../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import t from "../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import e from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import t from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
|
|
3
3
|
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
4
|
import { styles as r } from "./SidebarSection.styles.js";
|
|
5
5
|
import { html as i } from "lit";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import e from "../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import t from "../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import e from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import t from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
|
|
3
3
|
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
4
|
import { getLocaleStrings as r } from "../internals/locale.js";
|
|
5
5
|
import { styles as i } from "./Wizard.styles.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import e from "../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import t from "../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import e from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import t from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
|
|
3
3
|
import { property as n } from "lit/decorators.js";
|
|
4
4
|
import { UElement as r } from "@iyulab/components/dist/components/UElement.js";
|
|
5
5
|
//#region src/internals/StyledElement.ts
|
package/dist/internals/locale.js
CHANGED
|
@@ -13,7 +13,19 @@ var t = {
|
|
|
13
13
|
wizardNext: "Next",
|
|
14
14
|
wizardStepAnnouncement: "Step {index} of {total}: {label}"
|
|
15
15
|
}, n = e.namespace("modern-app");
|
|
16
|
-
n.register("en", t)
|
|
16
|
+
n.register("en", t), n.register("ko", {
|
|
17
|
+
back: "뒤로",
|
|
18
|
+
noDataTitle: "아직 항목이 없습니다",
|
|
19
|
+
noDataDescription: "",
|
|
20
|
+
noResultsTitle: "일치하는 결과가 없습니다",
|
|
21
|
+
noResultsDescription: "검색어나 필터를 바꿔 보세요.",
|
|
22
|
+
detailClose: "닫기",
|
|
23
|
+
toggleMobileMenu: "메뉴 열고 닫기",
|
|
24
|
+
toggleSidebar: "사이드바 열고 닫기",
|
|
25
|
+
wizardBack: "이전",
|
|
26
|
+
wizardNext: "다음",
|
|
27
|
+
wizardStepAnnouncement: "{total}단계 중 {index}단계: {label}"
|
|
28
|
+
});
|
|
17
29
|
var r, i = /* @__PURE__ */ new Map(), a = "\0modern-app:announcer:";
|
|
18
30
|
function o(e, t) {
|
|
19
31
|
let { wizardStepAnnouncement: r, ...o } = t, s = { ...o };
|
|
@@ -14,6 +14,14 @@ export declare class SidebarLayout extends StyledElement<SidebarParts> {
|
|
|
14
14
|
progressBarEl: UProgressBar;
|
|
15
15
|
/** overlay 슬롯 배정 상태 — CSS `:has()`로는 알 수 없다(`internals/slotted.ts` 참조). */
|
|
16
16
|
private hasOverlay;
|
|
17
|
+
/** notice 슬롯 배정 상태 — 비었으면 스택 자체가 자리를 차지하지 않는다. */
|
|
18
|
+
private hasNotice;
|
|
19
|
+
/**
|
|
20
|
+
* 오버레이를 연 컨트롤 — 닫힐 때 포커스를 되돌릴 곳.
|
|
21
|
+
* ★슬롯 배정 «시점» 에 잡는다. 렌더가 본문에 `inert` 를 걸면 그 컨트롤이 쥐던 포커스는
|
|
22
|
+
* `<body>` 로 떨어지므로, `updated()` 에서 읽으면 이미 늦다.
|
|
23
|
+
*/
|
|
24
|
+
private overlayTrigger;
|
|
17
25
|
/**
|
|
18
26
|
* `:state(overlay)`를 싣는 자리. `MasterDetailLayout`과 동일 패턴.
|
|
19
27
|
* ⚠생성자에서 한 번만 붙인다 — `attachInternals()`는 같은 인스턴스에 두 번 부르면
|
|
@@ -93,6 +101,29 @@ export declare class SidebarLayout extends StyledElement<SidebarParts> {
|
|
|
93
101
|
/** 라우트 에러 핸들러 */
|
|
94
102
|
private handleRouteError;
|
|
95
103
|
private handleOverlaySlotChange;
|
|
104
|
+
private handleNoticeSlotChange;
|
|
105
|
+
/** 오버레이 패널에 배정된 요소들 */
|
|
106
|
+
private overlayPanels;
|
|
107
|
+
/**
|
|
108
|
+
* 열린 패널로 포커스를 옮긴다 — 순서는 `UOverlayElement` 와 같다:
|
|
109
|
+
* `[autofocus]` → 첫 입력 컨트롤 → (없으면) 셸의 닫기 버튼.
|
|
110
|
+
* 닫기 버튼으로 떨어지는 것은 의도다 — 포커스가 `<body>` 에 남는 것보다 항상 낫고,
|
|
111
|
+
* 셸이 늘 가진 유일한 컨트롤이다. 소비자가 이미 패널 안으로 옮겨 두었으면 건드리지 않는다.
|
|
112
|
+
*/
|
|
113
|
+
private focusOverlay;
|
|
114
|
+
/**
|
|
115
|
+
* 닫힌 뒤 포커스를 연 컨트롤로 되돌린다. 포커스가 `<body>` 로 떨어졌을 때만 —
|
|
116
|
+
* 소비자가 닫으며 다른 곳으로 옮겼다면 그것을 존중한다.
|
|
117
|
+
* 연 컨트롤이 없으면(프로그램이 연 경우) 메인 스크롤러로 — 라우트 완료 때와 같은 자리다.
|
|
118
|
+
*/
|
|
119
|
+
private restoreOverlayFocus;
|
|
120
|
+
/**
|
|
121
|
+
* 패널 안의 Escape 는 닫기 버튼과 같은 `overlay-close` 를 낸다.
|
|
122
|
+
* window 버블 단계에서 받는다 — 패널 안의 목록·팝오버가 자기 층을 닫으며 먹은 키
|
|
123
|
+
* (`defaultPrevented`)는 그 층의 몫이다. 한 번의 Escape 는 한 층만 닫는다.
|
|
124
|
+
* 경로에 패널이 없으면(사이드바, 패널 위에 띄운 대화상자) 받지 않는다.
|
|
125
|
+
*/
|
|
126
|
+
private handleOverlayEscape;
|
|
96
127
|
private handleOverlayClose;
|
|
97
128
|
/** .main 키보드 스크롤 핸들러 (WCAG 2.1 SC 2.1.1) */
|
|
98
129
|
private _handleMainKeydown;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { app as e } from "../App.js";
|
|
2
2
|
import { filterSidebarItems as t } from "./filterSidebarItems.js";
|
|
3
|
-
import n from "../_virtual/_@oxc-project_runtime@0.
|
|
4
|
-
import r from "../_virtual/_@oxc-project_runtime@0.
|
|
3
|
+
import n from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
|
|
4
|
+
import r from "../_virtual/_@oxc-project_runtime@0.151.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
7
|
import { slotHasContent as o } from "../internals/slotted.js";
|
|
@@ -39,12 +39,25 @@ function T(e, t) {
|
|
|
39
39
|
}
|
|
40
40
|
return !0;
|
|
41
41
|
}
|
|
42
|
-
|
|
42
|
+
function E() {
|
|
43
|
+
let e = document.activeElement;
|
|
44
|
+
for (; e?.shadowRoot?.activeElement;) e = e.shadowRoot.activeElement;
|
|
45
|
+
return e;
|
|
46
|
+
}
|
|
47
|
+
function D(e, t) {
|
|
48
|
+
let n = t;
|
|
49
|
+
for (; n;) {
|
|
50
|
+
if (n === e) return !0;
|
|
51
|
+
n = n.parentNode ?? n.host ?? null;
|
|
52
|
+
}
|
|
53
|
+
return !1;
|
|
54
|
+
}
|
|
55
|
+
var O = class extends i {
|
|
43
56
|
static {
|
|
44
57
|
b = this;
|
|
45
58
|
}
|
|
46
59
|
constructor(...t) {
|
|
47
|
-
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:${++S}`, 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 = () => {
|
|
60
|
+
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:${++S}`, 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 = () => {
|
|
48
61
|
let t = this.screenSize ?? e.screen ?? "large";
|
|
49
62
|
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);
|
|
50
63
|
}, this.handleBackdropClick = () => {
|
|
@@ -58,13 +71,24 @@ var E = class extends i {
|
|
|
58
71
|
this.progressBarEl.removeAttribute("visible");
|
|
59
72
|
}, 300);
|
|
60
73
|
let t = this.shadowRoot?.querySelector(".main");
|
|
61
|
-
t && (this.config?.scrollBehavior?.(e.context, t), t.focus({ preventScroll: !0 }));
|
|
74
|
+
t && (this.config?.scrollBehavior?.(e.context, t), this.hasOverlay || t.focus({ preventScroll: !0 }));
|
|
62
75
|
}, this.handleRouteError = (e) => {
|
|
63
76
|
this.progressBarEl.status = "error", this.progressBarEl.value = 100, setTimeout(() => {
|
|
64
77
|
this.progressBarEl.removeAttribute("visible"), this.progressBarEl.status = "default";
|
|
65
78
|
}, 300);
|
|
66
79
|
}, this.handleOverlaySlotChange = (e) => {
|
|
67
|
-
|
|
80
|
+
let t = o(e.target);
|
|
81
|
+
if (t && !this.hasOverlay) {
|
|
82
|
+
let e = E();
|
|
83
|
+
this.overlayTrigger = e && e !== document.body ? e : null;
|
|
84
|
+
}
|
|
85
|
+
this.hasOverlay = t;
|
|
86
|
+
}, this.handleNoticeSlotChange = (e) => {
|
|
87
|
+
this.hasNotice = o(e.target);
|
|
88
|
+
}, this.handleOverlayEscape = (e) => {
|
|
89
|
+
if (e.key !== "Escape" || e.defaultPrevented || e.isComposing || !this.hasOverlay) return;
|
|
90
|
+
let t = this.shadowRoot?.querySelector(".overlay");
|
|
91
|
+
t && e.composedPath().includes(t) && (e.preventDefault(), this.handleOverlayClose());
|
|
68
92
|
}, this.handleOverlayClose = () => {
|
|
69
93
|
this.fire("overlay-close", { cancelable: !1 });
|
|
70
94
|
}, this._handleMainKeydown = (e) => {
|
|
@@ -111,13 +135,13 @@ var E = class extends i {
|
|
|
111
135
|
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);
|
|
112
136
|
}
|
|
113
137
|
disconnectedCallback() {
|
|
114
|
-
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), super.disconnectedCallback();
|
|
138
|
+
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();
|
|
115
139
|
}
|
|
116
140
|
willUpdate(e) {
|
|
117
141
|
super.willUpdate(e), e.has("config") && (this.styles = this.config?.styles);
|
|
118
142
|
}
|
|
119
143
|
updated(e) {
|
|
120
|
-
super.updated(e), e.has("hasOverlay") && this.internals?.states?.[this.hasOverlay ? "add" : "delete"]("overlay");
|
|
144
|
+
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()));
|
|
121
145
|
}
|
|
122
146
|
firstUpdated(e) {
|
|
123
147
|
super.firstUpdated(e), this.warnIfUnsized();
|
|
@@ -183,6 +207,9 @@ var E = class extends i {
|
|
|
183
207
|
<u-progress-bar part="progress"></u-progress-bar>
|
|
184
208
|
|
|
185
209
|
<div class="main-content" part="main-content" ?inert=${this.hasOverlay}>
|
|
210
|
+
<div class="notices ${this.hasNotice ? "" : "empty"}" part="notices">
|
|
211
|
+
<slot name="notice" @slotchange=${this.handleNoticeSlotChange}></slot>
|
|
212
|
+
</div>
|
|
186
213
|
<slot></slot>
|
|
187
214
|
</div>
|
|
188
215
|
</div>
|
|
@@ -304,10 +331,32 @@ var E = class extends i {
|
|
|
304
331
|
</u-link>
|
|
305
332
|
`;
|
|
306
333
|
}
|
|
334
|
+
overlayPanels() {
|
|
335
|
+
return (this.shadowRoot?.querySelector("slot[name=\"overlay\"]"))?.assignedElements({ flatten: !0 }) ?? [];
|
|
336
|
+
}
|
|
337
|
+
focusOverlay() {
|
|
338
|
+
let e = this.overlayPanels(), t = E();
|
|
339
|
+
if (t && e.some((e) => D(e, t))) return;
|
|
340
|
+
let n = (t) => {
|
|
341
|
+
for (let n of e) {
|
|
342
|
+
if (n.matches(t)) return n;
|
|
343
|
+
let e = n.querySelector(t);
|
|
344
|
+
if (e) return e;
|
|
345
|
+
}
|
|
346
|
+
return null;
|
|
347
|
+
};
|
|
348
|
+
(n("[autofocus]") ?? n("input, select, textarea, u-input, u-textarea, u-select, u-checkbox, u-radio, u-switch, u-slider") ?? this.shadowRoot?.querySelector(".overlay-close"))?.focus();
|
|
349
|
+
}
|
|
350
|
+
restoreOverlayFocus() {
|
|
351
|
+
let e = this.overlayTrigger;
|
|
352
|
+
this.overlayTrigger = null;
|
|
353
|
+
let t = E();
|
|
354
|
+
t && t !== document.body || (e?.isConnected ? e.focus() : this.mainElement?.focus({ preventScroll: !0 }));
|
|
355
|
+
}
|
|
307
356
|
};
|
|
308
357
|
r([f({
|
|
309
358
|
type: String,
|
|
310
359
|
reflect: !0
|
|
311
|
-
}), n("design:type", Object)],
|
|
360
|
+
}), n("design:type", Object)], O.prototype, "state", void 0), r([f({ type: Object }), n("design:type", Object)], O.prototype, "config", void 0), r([f({ type: String }), n("design:type", Object)], O.prototype, "locale", void 0), r([p("u-progress-bar"), n("design:type", y === void 0 ? Object : y)], O.prototype, "progressBarEl", void 0), r([m(), n("design:type", Object)], O.prototype, "hasOverlay", void 0), r([m(), n("design:type", Object)], O.prototype, "hasNotice", void 0), r([m(), n("design:type", Object)], O.prototype, "context", void 0), O = b = r([d("u-sidebar-layout")], O);
|
|
312
361
|
//#endregion
|
|
313
|
-
export {
|
|
362
|
+
export { O as SidebarLayout };
|
|
@@ -211,6 +211,31 @@ var t = e`
|
|
|
211
211
|
height: 100%;
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
/*
|
|
215
|
+
* 앱 수준 공지 스택 — 한 화면이 아니라 셸에 속한다.
|
|
216
|
+
* 본문 맨 위 «흐름 안»에 둔다. 세 대안을 소비자가 먼저 실측해 기각했다:
|
|
217
|
+
* · fixed → 본문을 덮는다.
|
|
218
|
+
* · 셸 바깥 흐름 → 셸 높이에 더해져 이중 스크롤.
|
|
219
|
+
* · .main 안 sticky → 좁은 화면에서 공지 둘이 상시 3할을 점유한다. 배너는
|
|
220
|
+
* «막지 않는 것»이 값이라 상시 점유가 그 값을 깎는다.
|
|
221
|
+
* .main-content 안에 있으므로 오버레이가 열리면 본문과 함께 inert 가 된다.
|
|
222
|
+
* 폭은 스택이 정한다 — 공지(u-alert)는 자기 폭이 fit-content 라 문구 길이대로
|
|
223
|
+
* 들쭉날쭉해진다. 배치 컨테이너의 결정이다.
|
|
224
|
+
*/
|
|
225
|
+
.notices {
|
|
226
|
+
display: flex;
|
|
227
|
+
flex-direction: column;
|
|
228
|
+
gap: var(--u-space-sm, 8px);
|
|
229
|
+
margin-bottom: var(--u-space-lg, 16px);
|
|
230
|
+
}
|
|
231
|
+
.notices.empty {
|
|
232
|
+
display: none;
|
|
233
|
+
}
|
|
234
|
+
slot[name="notice"]::slotted(*) {
|
|
235
|
+
width: 100%;
|
|
236
|
+
box-sizing: border-box;
|
|
237
|
+
}
|
|
238
|
+
|
|
214
239
|
/*
|
|
215
240
|
* 오버레이가 열린 동안 «본문 영역»을 스태킹 컨텍스트로 만든다.
|
|
216
241
|
*
|
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.28.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.45.
|
|
67
|
+
"@iyulab/components": ">=1.45.2",
|
|
68
68
|
"@lit/react": ">=1.0.8",
|
|
69
69
|
"react": ">=18.0.0"
|
|
70
70
|
},
|
|
@@ -89,6 +89,6 @@
|
|
|
89
89
|
"vite": "^8.1.4",
|
|
90
90
|
"vite-plugin-dts": "^5.0.3",
|
|
91
91
|
"vitest": "^5.0.0",
|
|
92
|
-
"@iyulab/components": ">=1.
|
|
92
|
+
"@iyulab/components": ">=1.45.2"
|
|
93
93
|
}
|
|
94
94
|
}
|
|
@@ -144,23 +144,26 @@ html`<p>${translate('namespace::greeting')}</p>`;
|
|
|
144
144
|
### Built-in strings (`modernAppLocale`)
|
|
145
145
|
|
|
146
146
|
The package's own strings — the page header's back link, empty-state copy, the shell's toggle and
|
|
147
|
-
close labels, the wizard's buttons and step announcement —
|
|
148
|
-
|
|
149
|
-
language once, with `Locale.set()
|
|
147
|
+
close labels, the wizard's buttons and step announcement — live in the `modern-app` namespace of
|
|
148
|
+
`@iyulab/components`' `Locale`. English and Korean are built in, English being the default; pick the
|
|
149
|
+
language once, with `Locale.set()` — the primitives and `@iyulab/flex-table` follow the same call.
|
|
150
|
+
Register other languages, or your own wording, in the same namespace:
|
|
150
151
|
|
|
151
152
|
```typescript
|
|
152
153
|
import { Locale } from '@iyulab/components';
|
|
153
154
|
import { modernAppLocale } from '@iyulab/modern-app';
|
|
154
155
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
Locale.set('ko'); // built-in Korean — nothing to register
|
|
157
|
+
|
|
158
|
+
modernAppLocale.register('ja', {
|
|
159
|
+
back: '戻る',
|
|
160
|
+
toggleSidebar: 'サイドバーの切り替え',
|
|
161
|
+
wizardStepAnnouncement: 'ステップ {index}/{total}: {label}',
|
|
159
162
|
});
|
|
160
|
-
Locale.set('ko');
|
|
161
163
|
```
|
|
162
164
|
|
|
163
|
-
A partial table is enough — untranslated keys fall back to English
|
|
165
|
+
A partial table is enough — untranslated keys fall back to English, and a registration for a
|
|
166
|
+
built-in language overrides only the keys it gives. An element's own `locale`
|
|
164
167
|
attribute overrides the active language for that element. `registerLocale`/`setDefaultLocale` still
|
|
165
168
|
work but are deprecated: a `setDefaultLocale()` call takes precedence over `Locale` until it is
|
|
166
169
|
called with `undefined`.
|
|
@@ -54,6 +54,6 @@ for both and a user with a filter still applied reads it as *"my data disappeare
|
|
|
54
54
|
| `--u-space-lg` | Actions row top spacing |
|
|
55
55
|
| `--u-space-sm` | Gap between actions |
|
|
56
56
|
|
|
57
|
-
⚠ Default wording is **English
|
|
58
|
-
|
|
59
|
-
screen.
|
|
57
|
+
⚠ Default wording is **English**, with Korean built in — choose the language with `Locale.set()` of
|
|
58
|
+
`@iyulab/components`, register others with `modernAppLocale.register(lang, …)`, or pass
|
|
59
|
+
`title`/`description` per screen.
|
|
@@ -9,7 +9,7 @@ trailing actions*, and assembling that by hand is how title sizes, padding, and
|
|
|
9
9
|
up different on every screen.
|
|
10
10
|
|
|
11
11
|
```html
|
|
12
|
-
<u-group-box title="Receivables">
|
|
12
|
+
<u-group-box title="Receivables" meta="3 items">
|
|
13
13
|
<a slot="actions" href="/receivables">Payments</a>
|
|
14
14
|
…body…
|
|
15
15
|
</u-group-box>
|
|
@@ -30,6 +30,7 @@ a left edge without any consumer CSS.
|
|
|
30
30
|
| Property | Type | Default | Reflect | Description |
|
|
31
31
|
|----------|------|---------|---------|-------------|
|
|
32
32
|
| `title` | `string` | `''` | | Header title |
|
|
33
|
+
| `meta` | `string` | — | | Secondary text after the title — a count, progress or short status (`3 items`, `2/5 done`), one step below the title: body size and weight, weak color, tabular figures. It sits inside the heading, so the heading's accessible name includes it. Not drawn without `title` |
|
|
33
34
|
| `divider` | `boolean` | `false` | | Rule between header and body. Off by default — many rules make a screen noisy |
|
|
34
35
|
| `flush` | `boolean` | `false` | | Remove body padding, for tables and lists that draw their own edges |
|
|
35
36
|
| `level` | `2|3|4|5|6` | `3` | ✓ | Heading level of the title in the document outline. Set `2` for a box directly under `u-page-header` (the page's `h1`), so the outline does not skip a level. Semantics only — the title looks the same at every level |
|
|
@@ -39,6 +40,7 @@ a left edge without any consumer CSS.
|
|
|
39
40
|
| Part | Description |
|
|
40
41
|
|------|-------------|
|
|
41
42
|
| `header` · `title` · `actions` | Header row |
|
|
43
|
+
| `meta` | Secondary text inside the title |
|
|
42
44
|
| `body` | Body wrapper |
|
|
43
45
|
|
|
44
46
|
## CSS Custom Properties
|
|
@@ -49,6 +51,7 @@ a left edge without any consumer CSS.
|
|
|
49
51
|
| `--u-text-subtitle-weight` | Title font weight |
|
|
50
52
|
| `--u-text-subtitle-leading` | Title line height |
|
|
51
53
|
| `--u-text-subtitle-tracking` | Title letter spacing |
|
|
54
|
+
| `--u-text-body-size` / `--u-text-body-weight` / `--u-txt-color-weak` | `meta` size, weight and color |
|
|
52
55
|
| `--u-bg-color-raised` | Surface background |
|
|
53
56
|
| `--u-border-color-weak` | Border and header divider color |
|
|
54
57
|
| `--u-radius-2xl` | Corner radius |
|
|
@@ -58,6 +58,5 @@ screens nobody opens.
|
|
|
58
58
|
| `--u-space-2xs` | Title-to-subtitle spacing |
|
|
59
59
|
| `--u-space-3xs` | Back-arrow-to-label spacing |
|
|
60
60
|
|
|
61
|
-
⚠ Built-in strings default to **English
|
|
62
|
-
|
|
63
|
-
language with `Locale.set()` of `@iyulab/components`.
|
|
61
|
+
⚠ Built-in strings default to **English**, with Korean built in — choose the language with
|
|
62
|
+
`Locale.set()` of `@iyulab/components`. Register others with `modernAppLocale.register(lang, { back: … })`.
|
|
@@ -264,6 +264,7 @@ Parts available for `styles` overrides on the root layout:
|
|
|
264
264
|
| `progress` | Top progress bar |
|
|
265
265
|
| `overlay` | Route-independent overlay panel above `main` |
|
|
266
266
|
| `overlay-close` | Overlay's close button |
|
|
267
|
+
| `notices` | Stack of app-level notices at the top of the route content (`slot="notice"`) |
|
|
267
268
|
|
|
268
269
|
---
|
|
269
270
|
|
|
@@ -350,9 +351,9 @@ from either re-implementing what the shell already does, or dropping what it doe
|
|
|
350
351
|
| Making route content non-interactive | shell | `inert` on `part="main-content"`, which propagates through the slot into your route content |
|
|
351
352
|
| A close affordance | shell | `part="overlay-close"`, firing the non-cancelable `overlay-close` event |
|
|
352
353
|
| Keeping the route mounted underneath | shell | the overlay is independent of routing |
|
|
353
|
-
|
|
|
354
|
-
|
|
|
355
|
-
|
|
|
354
|
+
| 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 |
|
|
355
|
+
| 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"` |
|
|
356
|
+
| Escape to close | shell | Escape inside the panel fires the same `overlay-close` as the button — you still empty the slot. An Escape already consumed inside the panel (an open list or popover calling `preventDefault()`) closes only that layer |
|
|
356
357
|
| **A backdrop / dimmed scrim** | **consumer** | the panel is opaque and full-bleed by design; add a scrim inside your panel if you want one |
|
|
357
358
|
| **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 |
|
|
358
359
|
|
|
@@ -371,6 +372,29 @@ No `overlayBreakpoint`/responsive toggle exists here — unlike `u-master-detail
|
|
|
371
372
|
overlay is always an overlay, never a side-by-side pane. Use `u-master-detail-layout` instead
|
|
372
373
|
when you want the panel to sit *beside* content on wide screens.
|
|
373
374
|
|
|
375
|
+
|
|
376
|
+
## App-level notices
|
|
377
|
+
|
|
378
|
+
App-level notices — the server is unreachable, a new version is ready — belong to the shell, not to
|
|
379
|
+
one screen: a screen that places its own fixed banner collides with the next notice someone adds.
|
|
380
|
+
Put them in `slot="notice"`; the shell stacks them at the top of the route content.
|
|
381
|
+
|
|
382
|
+
```html
|
|
383
|
+
<u-sidebar-layout>
|
|
384
|
+
<u-alert slot="notice" status="warning" open>Server unreachable — retrying.</u-alert>
|
|
385
|
+
<u-alert slot="notice" status="info" open closable>A new version is ready.</u-alert>
|
|
386
|
+
</u-sidebar-layout>
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
- **In flow, above the route content.** Notices scroll away with the content rather than holding a
|
|
390
|
+
strip of a small screen, and they never cover it. A banner's value is that it does not block;
|
|
391
|
+
a permanently pinned stack would eat into that on a phone.
|
|
392
|
+
- **Full width.** Each notice takes the width of the content box, whatever its text length.
|
|
393
|
+
- **Empty takes no space** — the stack has no margin while nothing is slotted.
|
|
394
|
+
- **Inert with the route content** while the overlay is open.
|
|
395
|
+
- Which surface a notice is (toast, banner or modal) is decided by *who ends it* — a notice the time
|
|
396
|
+
ends is a toast (`app.success`), not a slotted banner.
|
|
397
|
+
|
|
374
398
|
## Responsive behaviour
|
|
375
399
|
|
|
376
400
|
| Screen width | Sidebar state |
|