@iyulab/modern-app 0.39.0 → 0.41.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.41.0] - 2026-10-07
4
+
5
+ ### Changed
6
+
7
+ - **`u-group-box` derives its heading level when `level` is unset** — `h2` for a box in no other `u-group-box`
8
+ (the usual place, directly under `u-page-header`'s `h1`), one level deeper than the nearest enclosing box otherwise
9
+ (shadow roots included, capped at `h6`). The default was `h3`, so the most common composition skipped a heading
10
+ level on every screen (axe `heading-order`). An explicit `level` still wins; an out-of-range one now falls back to
11
+ the derived level instead of `3`. A box under a heading of your own should set `level`.
12
+
13
+ ### Fixed
14
+
15
+ - **The sidebar layout's route-loading bar is out of the accessibility tree while idle, and named while it loads**
16
+ (new locale key `pageLoading` — en *Loading page*, ko *페이지 불러오는 중*). It was hidden with `opacity` only, so
17
+ every screen carried an unnamed progress bar for screen readers to meet (axe `aria-progressbar-name`).
18
+
19
+ ## [0.40.0] - 2026-10-07
20
+
21
+ ### Changed
22
+
23
+ - **With `i18n` configured, i18next's language drives the whole app** — at load and on every `changeLanguage()` it is
24
+ handed to `@iyulab/components`' `Locale` (shell, primitives and component chrome follow, re-rendering on screen) and
25
+ to `<html lang>` (WCAG 3.1.1). Before, `changeLanguage('ko')` changed the app's own text while button names, empty
26
+ states and validation messages stayed in the old language, and a first load could show two languages when i18next's
27
+ `lng` differed from `<html lang>` or the browser's. Without `i18n` nothing changes — `Locale.set()` chooses.
28
+ - **Requires `@iyulab/components` 2.8.0** (peer `>=2.8.0`) — the version whose components re-render on a locale switch.
29
+
3
30
  ## [0.39.0] - 2026-10-06
4
31
 
5
32
  ### Fixed
package/README.md CHANGED
@@ -148,7 +148,9 @@ html`<p>${translate('common::greeting')}</p>`;
148
148
  The package's own strings — the page header's back link, empty-state copy, the shell's toggle and
149
149
  close labels, the wizard's buttons and step announcement — live in the `modern-app` namespace of
150
150
  `@iyulab/components`' `Locale`. English and Korean are built in, English being the default; pick the
151
- language once, with `Locale.set()` — the primitives and `@iyulab/flex-table` follow the same call.
151
+ language once, with `Locale.set()` — the primitives and `@iyulab/flex-table` follow the same call. With
152
+ `i18n` configured you do not call it: i18next's language (at load and on every `changeLanguage()`) is
153
+ handed to `Locale` and to `<html lang>`.
152
154
  Register other languages, or your own wording, in the same namespace:
153
155
 
154
156
  ```typescript
package/dist/App.d.ts CHANGED
@@ -14,6 +14,8 @@ declare class App {
14
14
  private _screen?;
15
15
  private _user?;
16
16
  private _gateTeardown?;
17
+ /** i18next 언어 → components `Locale` 동기화 해제(`unload`). */
18
+ private _languageSync?;
17
19
  private constructor();
18
20
  /** 싱글톤 인스턴스 반환 */
19
21
  static get instance(): App;
@@ -32,6 +34,13 @@ declare class App {
32
34
  /** 앱 로드 및 초기화 */
33
35
  load(config: AppConfig): Promise<void>;
34
36
  /** 앱 언로드 */
37
+ /**
38
+ * i18next 를 설정한 앱에서는 **i18next 가 언어의 정본**이다 — 그 언어를 셸·컴포넌트 chrome(`@iyulab/components` 의
39
+ * `Locale`, 이 패키지의 `modernAppLocale` 이 그것을 따른다)과 문서 `<html lang>`(WCAG 3.1.1, 낭독기 발음)에 잇는다.
40
+ * 종전에는 둘이 따로였다: `i18next.changeLanguage('ko')` 가 앱 문장만 바꾸고 버튼 이름·빈 상태·검증 메시지는 옛 언어로,
41
+ * 첫 로드에서도 i18next `lng` 와 `<html lang>`/브라우저 언어가 다르면 한 화면에 두 언어가 섰다.
42
+ */
43
+ private syncLanguage;
35
44
  unload(): void;
36
45
  /** 페이지 이동 */
37
46
  navigate(path: string): void;
package/dist/App.js CHANGED
@@ -4,23 +4,24 @@ import { Router as n } from "@iyulab/router";
4
4
  import { setDefaultBaseUrl as r } from "@iyulab/components/dist/utilities/icons.js";
5
5
  import { Theme as i } from "@iyulab/components/dist/utilities/Theme.js";
6
6
  import { Toast as a } from "@iyulab/components/dist/utilities/Toast.js";
7
+ import { Locale as o } from "@iyulab/components/dist/utilities/Locale.js";
7
8
  //#region src/App.ts
8
- var o = "\n:where(body) { margin: 0; }\n@media screen {\n :where(body) { width: 100vw; height: 100vh; }\n}\n", s;
9
- function c() {
10
- if (!s) {
9
+ var s = "\n:where(body) { margin: 0; }\n@media screen {\n :where(body) { width: 100vw; height: 100vh; }\n}\n", c;
10
+ function l() {
11
+ if (!c) {
11
12
  if ("adoptedStyleSheets" in document && typeof CSSStyleSheet < "u" && typeof CSSStyleSheet.prototype.replaceSync == "function") {
12
13
  let e = new CSSStyleSheet();
13
- e.replaceSync(o), document.adoptedStyleSheets = [...document.adoptedStyleSheets, e], s = e;
14
+ e.replaceSync(s), document.adoptedStyleSheets = [...document.adoptedStyleSheets, e], c = e;
14
15
  } else {
15
16
  let e = document.createElement("style");
16
- e.textContent = o, document.head.prepend(e), s = e;
17
+ e.textContent = s, document.head.prepend(e), c = e;
17
18
  }
18
19
  }
19
20
  }
20
- var l = class o {
21
+ var u = class s {
21
22
  constructor() {}
22
23
  static get instance() {
23
- return o._instance ||= new o(), o._instance;
24
+ return s._instance ||= new s(), s._instance;
24
25
  }
25
26
  get config() {
26
27
  return this._config;
@@ -43,7 +44,7 @@ var l = class o {
43
44
  async load(a) {
44
45
  if (this.unload(), this._config = a, await i.init(a.theme), a.iconBasepath && r(a.iconBasepath), a.i18n) {
45
46
  for (let e of a.i18n.plugins || []) t.use(e);
46
- await t.init(a.i18n);
47
+ await t.init(a.i18n), this.syncLanguage();
47
48
  }
48
49
  if (a.auth) {
49
50
  let e = a.root || document.body, t = () => {
@@ -88,8 +89,16 @@ var l = class o {
88
89
  useIntercept: a.useIntercept
89
90
  });
90
91
  }
92
+ syncLanguage() {
93
+ let e = (e) => {
94
+ e && e !== "cimode" && (o.set(e), typeof document < "u" && (document.documentElement.lang = e));
95
+ };
96
+ e(t.resolvedLanguage ?? t.language);
97
+ let n = (n) => e(t.resolvedLanguage ?? n);
98
+ t.on("languageChanged", n), this._languageSync = () => t.off("languageChanged", n);
99
+ }
91
100
  unload() {
92
- this._gateTeardown &&= (this._gateTeardown(), void 0), this._user = void 0, this._screen &&= (this._screen.destroy(), void 0), this._layout &&= (this._layout.remove(), void 0), this._router &&= (this._router.destroy(), void 0), this._config &&= void 0;
101
+ this._languageSync?.(), this._languageSync = void 0, this._gateTeardown &&= (this._gateTeardown(), void 0), this._user = void 0, this._screen &&= (this._screen.destroy(), void 0), this._layout &&= (this._layout.remove(), void 0), this._router &&= (this._router.destroy(), void 0), this._config &&= void 0;
93
102
  }
94
103
  navigate(e) {
95
104
  this._router?.go(e);
@@ -116,7 +125,7 @@ var l = class o {
116
125
  await a.error(e, { ...t });
117
126
  }
118
127
  async createLayout(e, t) {
119
- e === document.body && c();
128
+ e === document.body && l();
120
129
  let n;
121
130
  if (t.type === "sidebar") {
122
131
  let { SidebarLayout: e } = await import("./layouts/SidebarLayout.js"), r = new e();
@@ -126,4 +135,4 @@ var l = class o {
126
135
  }
127
136
  }.instance;
128
137
  //#endregion
129
- export { l as app };
138
+ export { u as app };
@@ -43,14 +43,18 @@ export declare class GroupBox extends StyledElement<ElementParts> {
43
43
  /** 본문 여백을 없앤다 — 표를 카드 가장자리까지 붙일 때. */
44
44
  flush: boolean;
45
45
  /**
46
- * Heading level of the title in the document outline (`2`–`6`, default `3`).
46
+ * Heading level of the title in the document outline (`2`–`6`).
47
47
  *
48
- * The box cannot know how deep it sits in the page, so whoever composes the page says so — a
49
- * box placed directly under `u-page-header` (the page's `h1`) is usually `2`, and leaving it at
50
- * `3` skips a level and puts it deeper than its sibling sections. This changes semantics only:
51
- * the title keeps its visual size at every level. Out-of-range values fall back to `3`.
48
+ * Unset, it follows the boxes around it: `2` for a box that sits in no other `u-group-box` — the
49
+ * usual place, directly under `u-page-header` (the page's `h1`) — and one deeper than the nearest
50
+ * enclosing box otherwise (shadow roots included), capped at `6`. Set it when the box sits under a
51
+ * heading of your own (a box under your `h2` section is `3`). This changes semantics only: the title
52
+ * keeps its visual size at every level. Out-of-range values fall back to the derived level.
52
53
  */
53
- level: GroupBoxLevel;
54
+ level?: GroupBoxLevel;
55
+ /** The level the title renders at — `level`, or the one derived from enclosing boxes. */
56
+ get headingLevel(): GroupBoxLevel;
57
+ connectedCallback(): void;
54
58
  /**
55
59
  * 액션 슬롯 배정 상태.
56
60
  * ★종전에는 'render()' 안에서 'this.querySelector()' 로 라이트 DOM 을 읽었다. 그것은
@@ -7,25 +7,36 @@ import { html as a, nothing as o } from "lit";
7
7
  import { customElement as s, property as c, state as l } from "lit/decorators.js";
8
8
  import { html as u, literal as d } from "lit/static-html.js";
9
9
  //#region src/components/GroupBox.ts
10
- var f = {
10
+ var f, p = {
11
11
  2: d`h2`,
12
12
  3: d`h3`,
13
13
  4: d`h4`,
14
14
  5: d`h5`,
15
15
  6: d`h6`
16
- }, p = class extends n {
16
+ }, m = class extends n {
17
+ static {
18
+ f = this;
19
+ }
17
20
  constructor(...e) {
18
- super(...e), this.title = "", this.divider = !1, this.flush = !1, this.level = 3, this.hasActions = !1;
21
+ super(...e), this.title = "", this.divider = !1, this.flush = !1, this.hasActions = !1;
19
22
  }
20
23
  static {
21
24
  this.styles = [super.styles, i];
22
25
  }
26
+ get headingLevel() {
27
+ if (this.level && p[this.level]) return this.level;
28
+ for (let e = this.parentNode; e; e = e instanceof ShadowRoot ? e.host : e.parentNode) if (e instanceof f) return Math.min(6, e.headingLevel + 1);
29
+ return 2;
30
+ }
31
+ connectedCallback() {
32
+ super.connectedCallback(), this.requestUpdate();
33
+ }
23
34
  render() {
24
35
  let e = !!this.title || !!this.description || this.hasActions;
25
36
  return a`
26
37
  <div class="header ${this.divider ? "divider" : ""} ${e ? "" : "empty"}" part="header">
27
38
  <div class="titles">
28
- ${u`<${f[this.level] ?? f[3]} class="title" part="title">${this.title}${this.title && this.meta ? a` <span class="meta" part="meta">${this.meta}</span>` : ""}</${f[this.level] ?? f[3]}>`}
39
+ ${u`<${p[this.headingLevel]} class="title" part="title">${this.title}${this.title && this.meta ? a` <span class="meta" part="meta">${this.meta}</span>` : ""}</${p[this.headingLevel]}>`}
29
40
  ${this.description ? a`<p class="description" part="description">${this.description}</p>` : o}
30
41
  </div>
31
42
  <div class="actions ${this.hasActions ? "" : "empty"}" part="actions">
@@ -38,9 +49,9 @@ var f = {
38
49
  `;
39
50
  }
40
51
  };
41
- t([c({ type: String }), e("design:type", Object)], p.prototype, "title", void 0), t([c({ type: String }), e("design:type", String)], p.prototype, "meta", void 0), t([c({ type: String }), e("design:type", String)], p.prototype, "description", void 0), t([c({ type: Boolean }), e("design:type", Object)], p.prototype, "divider", void 0), t([c({ type: Boolean }), e("design:type", Object)], p.prototype, "flush", void 0), t([c({
52
+ t([c({ type: String }), e("design:type", Object)], m.prototype, "title", void 0), t([c({ type: String }), e("design:type", String)], m.prototype, "meta", void 0), t([c({ type: String }), e("design:type", String)], m.prototype, "description", void 0), t([c({ type: Boolean }), e("design:type", Object)], m.prototype, "divider", void 0), t([c({ type: Boolean }), e("design:type", Object)], m.prototype, "flush", void 0), t([c({
42
53
  type: Number,
43
54
  reflect: !0
44
- }), e("design:type", Object)], p.prototype, "level", void 0), t([l(), e("design:type", Object)], p.prototype, "hasActions", void 0), p = t([s("u-group-box")], p);
55
+ }), e("design:type", Object)], m.prototype, "level", void 0), t([l(), e("design:type", Object)], m.prototype, "hasActions", void 0), m = f = t([s("u-group-box")], m);
45
56
  //#endregion
46
- export { p as GroupBox };
57
+ export { m as GroupBox };
@@ -17,6 +17,9 @@ export interface ModernAppLocaleStrings {
17
17
  skipToContent: string;
18
18
  /** Sidebar links with `target: '_blank'` — appended to the link's accessible name (visually hidden). */
19
19
  opensInNewTab: string;
20
+ /** Sidebar layout — accessible name of the route-loading bar at the top of the content (it is hidden from
21
+ * assistive technology while idle). */
22
+ pageLoading: string;
20
23
  /** Wizard — default Back/Next action labels ("Submit" wording is the consumer's call). */
21
24
  wizardBack: string;
22
25
  wizardNext: string;
@@ -11,6 +11,7 @@ var t = {
11
11
  toggleSidebar: "Toggle sidebar",
12
12
  skipToContent: "Skip to main content",
13
13
  opensInNewTab: "(opens in a new tab)",
14
+ pageLoading: "Loading page",
14
15
  wizardBack: "Back",
15
16
  wizardNext: "Next",
16
17
  wizardStepAnnouncement: "Step {index} of {total}: {label}"
@@ -26,6 +27,7 @@ n.register("en", t), n.register("ko", {
26
27
  toggleSidebar: "사이드바 열고 닫기",
27
28
  skipToContent: "본문으로 건너뛰기",
28
29
  opensInNewTab: "(새 창에서 열림)",
30
+ pageLoading: "페이지 불러오는 중",
29
31
  wizardBack: "이전",
30
32
  wizardNext: "다음",
31
33
  wizardStepAnnouncement: "{total}단계 중 {index}단계: {label}"
@@ -59,6 +61,7 @@ function l(e) {
59
61
  toggleSidebar: t("toggleSidebar"),
60
62
  skipToContent: t("skipToContent"),
61
63
  opensInNewTab: t("opensInNewTab"),
64
+ pageLoading: t("pageLoading"),
62
65
  wizardBack: t("wizardBack"),
63
66
  wizardNext: t("wizardNext"),
64
67
  wizardStepAnnouncement: (t, n, r) => {
@@ -25,6 +25,11 @@ export declare class SidebarLayout extends StyledElement<SidebarParts> {
25
25
  private static menuSeq;
26
26
  /** notice 슬롯 배정 상태 — 비었으면 스택 자체가 자리를 차지하지 않는다. */
27
27
  private hasNotice;
28
+ /**
29
+ * 라우트가 불러오는 중인가 — 본문 위 진행 막대를 보이고, 쉬는 동안은 보조기기에서도 뺀다. 종전에는 `opacity: 0` 으로만
30
+ * 숨어 이름 없는 진행 막대가 모든 화면의 접근성 트리에 떠 있었다(axe `aria-progressbar-name`).
31
+ */
32
+ private routeLoading;
28
33
  /**
29
34
  * 오버레이를 연 컨트롤 — 닫힐 때 포커스를 되돌릴 곳.
30
35
  * ★슬롯 배정 «시점» 에 잡는다. 렌더가 본문에 `inert` 를 걸면 그 컨트롤이 쥐던 포커스는
@@ -79,18 +79,18 @@ var N = class extends i {
79
79
  C = this;
80
80
  }
81
81
  constructor(...t) {
82
- super(...t), this.state = "default", this.counts = {}, this.locale = "", this.hasOverlay = !1, this.openMenu = null, this.menuIds = /* @__PURE__ */ new WeakMap(), this.hasNotice = !1, this.overlayTrigger = null, this.internals = typeof this.attachInternals == "function" ? this.attachInternals() : void 0, this.currentPath = null, this.unsizedWarnKey = `unsized:${++T}`, this.isMatchedLink = (e) => this.currentPath === null || !e ? !1 : (e = typeof e == "string" ? new URLPattern(e, window.location.origin) : e, e.test(this.currentPath, window.location.origin)), this.handleToggleButtonClick = () => {
82
+ super(...t), this.state = "default", this.counts = {}, this.locale = "", this.hasOverlay = !1, this.openMenu = null, this.menuIds = /* @__PURE__ */ new WeakMap(), this.hasNotice = !1, this.routeLoading = !1, this.overlayTrigger = null, this.internals = typeof this.attachInternals == "function" ? this.attachInternals() : void 0, this.currentPath = null, this.unsizedWarnKey = `unsized:${++T}`, this.isMatchedLink = (e) => this.currentPath === null || !e ? !1 : (e = typeof e == "string" ? new URLPattern(e, window.location.origin) : e, e.test(this.currentPath, window.location.origin)), this.handleToggleButtonClick = () => {
83
83
  let t = this.screenSize ?? e.screen ?? "large";
84
84
  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);
85
85
  }, this.handleBackdropClick = () => {
86
86
  this.state = "slim";
87
87
  }, this.handleRouteBegin = (e) => {
88
- this.progressBarEl.setAttribute("visible", ""), this.progressBarEl.value = 0, this.state === "modal" && (this.state = "slim"), this.state === "mobile-open" && (this.state = "mobile"), this.currentPath = e.context.path;
88
+ this.routeLoading = !0, this.progressBarEl.value = 0, this.state === "modal" && (this.state = "slim"), this.state === "mobile-open" && (this.state = "mobile"), this.currentPath = e.context.path;
89
89
  }, this.handleRouteProgress = (e) => {
90
90
  this.progressBarEl.value = e.progress;
91
91
  }, this.handleRouteDone = (e) => {
92
92
  this.progressBarEl.value = 100, setTimeout(() => {
93
- this.progressBarEl.removeAttribute("visible");
93
+ this.routeLoading = !1;
94
94
  }, 300);
95
95
  let t = this.shadowRoot?.querySelector(".main");
96
96
  t && (this.config?.scrollBehavior?.(e.context, t), this.focusRoute(t));
@@ -100,7 +100,7 @@ var N = class extends i {
100
100
  t && this.focusContentStart(this.hasOverlay ? [] : this.routeContent(), t);
101
101
  }, this.handleRouteError = (e) => {
102
102
  this.progressBarEl.status = "error", this.progressBarEl.value = 100, setTimeout(() => {
103
- this.progressBarEl.removeAttribute("visible"), this.progressBarEl.status = "default";
103
+ this.routeLoading = !1, this.progressBarEl.status = "default";
104
104
  }, 300);
105
105
  }, this.handleOverlaySlotChange = (e) => {
106
106
  let t = o(e.target);
@@ -238,7 +238,9 @@ var N = class extends i {
238
238
  <!-- Main Content -->
239
239
  <div class="main-region">
240
240
  <main class="main" part="main" scrollable tabindex="-1" @keydown=${this._handleMainKeydown}>
241
- <u-progress-bar part="progress"></u-progress-bar>
241
+ <u-progress-bar part="progress" class=${this.routeLoading ? "loading" : ""}
242
+ aria-hidden=${this.routeLoading ? d : "true"}
243
+ aria-label=${a(this.locale || void 0).pageLoading}></u-progress-bar>
242
244
 
243
245
  <div class="main-content" part="main-content" ?inert=${this.hasOverlay}>
244
246
  <div class="notices ${this.hasNotice ? "" : "empty"}" part="notices"
@@ -442,6 +444,6 @@ var N = class extends i {
442
444
  r([p({
443
445
  type: String,
444
446
  reflect: !0
445
- }), n("design:type", Object)], N.prototype, "state", void 0), r([p({ type: Object }), n("design:type", Object)], N.prototype, "config", void 0), r([p({ type: Object }), n("design:type", typeof Record > "u" ? Object : Record)], N.prototype, "counts", void 0), r([p({ type: String }), n("design:type", Object)], N.prototype, "locale", void 0), r([m("u-progress-bar"), n("design:type", S === void 0 ? Object : S)], N.prototype, "progressBarEl", void 0), r([h(), n("design:type", Object)], N.prototype, "hasOverlay", void 0), r([h(), n("design:type", Object)], N.prototype, "openMenu", void 0), r([h(), n("design:type", Object)], N.prototype, "hasNotice", void 0), r([h(), n("design:type", Object)], N.prototype, "currentPath", void 0), N = C = r([f("u-sidebar-layout")], N);
447
+ }), n("design:type", Object)], N.prototype, "state", void 0), r([p({ type: Object }), n("design:type", Object)], N.prototype, "config", void 0), r([p({ type: Object }), n("design:type", typeof Record > "u" ? Object : Record)], N.prototype, "counts", void 0), r([p({ type: String }), n("design:type", Object)], N.prototype, "locale", void 0), r([m("u-progress-bar"), n("design:type", S === void 0 ? Object : S)], N.prototype, "progressBarEl", void 0), r([h(), n("design:type", Object)], N.prototype, "hasOverlay", void 0), r([h(), n("design:type", Object)], N.prototype, "openMenu", void 0), r([h(), n("design:type", Object)], N.prototype, "hasNotice", void 0), r([h(), n("design:type", Object)], N.prototype, "routeLoading", void 0), r([h(), n("design:type", Object)], N.prototype, "currentPath", void 0), N = C = r([f("u-sidebar-layout")], N);
446
448
  //#endregion
447
449
  export { N as SidebarLayout };
@@ -359,7 +359,7 @@ var t = e`
359
359
  transition: opacity var(--u-duration-slow, 320ms) ease, transform var(--u-duration-slow, 320ms) ease;
360
360
  pointer-events: none;
361
361
  }
362
- .main u-progress-bar[visible] {
362
+ .main u-progress-bar.loading {
363
363
  opacity: 1;
364
364
  transform: translateY(0);
365
365
  }
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.39.0",
4
+ "version": "0.41.0",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "web-framework",
@@ -65,7 +65,7 @@
65
65
  "lit": "^3.3.3"
66
66
  },
67
67
  "peerDependencies": {
68
- "@iyulab/components": ">=2.0.0",
68
+ "@iyulab/components": ">=2.8.0",
69
69
  "@lit/react": "^1.0.8",
70
70
  "react": "^18.0.0 || ^19.0.0"
71
71
  },
@@ -78,7 +78,7 @@
78
78
  }
79
79
  },
80
80
  "devDependencies": {
81
- "@iyulab/components": ">=2.0.0",
81
+ "@iyulab/components": ">=2.8.0",
82
82
  "@lit/react": "^1.0.8",
83
83
  "@types/node": "^26.1.1",
84
84
  "@types/react": "^19.2.14",
@@ -34,7 +34,7 @@ a left edge without any consumer CSS.
34
34
  | `description` | `string` | — | | One line under the title — what the box holds or what its numbers are based on. Caption size, weak color. Opens the header on its own |
35
35
  | `divider` | `boolean` | `false` | | Rule between header and body. Off by default — many rules make a screen noisy |
36
36
  | `flush` | `boolean` | `false` | | Remove body padding, for tables and lists that draw their own edges |
37
- | `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 |
37
+ | `level` | `2|3|4|5|6` | derived | ✓ | Heading level of the title in the document outline. Unset: `2` for a box in no other `u-group-box` (directly under `u-page-header`, the page's `h1`), one deeper than the nearest enclosing box otherwise. Set it when the box sits under a heading of your own. Semantics only — the title looks the same at every level |
38
38
 
39
39
  ## CSS Parts
40
40