@iyulab/modern-app 0.18.18 → 0.19.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,43 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.19.0] - 2026-09-10
4
+
5
+ ### Changed
6
+
7
+ - **Fallback literals for `--u-txt-color-weak` follow `@iyulab/components` 1.40.0.** They are
8
+ only used when that stylesheet is not loaded; with it, nothing changes here.
9
+
10
+ ### Fixed
11
+
12
+ - **`u-master-detail-layout` ignored a change to `overlayBreakpoint` until the element
13
+ happened to resize.** The overlay decision has two inputs — the component's own width
14
+ and the breakpoint — but it was only ever evaluated inside the `ResizeObserver`
15
+ callback, so setting the prop at runtime left the `overlay` attribute on its previous
16
+ verdict. Setting it once before first render always worked, which is why the gap went
17
+ unnoticed. It is now re-evaluated whenever either input changes.
18
+
19
+ ### Changed
20
+
21
+ - **`@iyulab/components` moved from `dependencies` to `peerDependencies`.** It owns
22
+ process-global state — the custom element registry and module-level singletons — so a
23
+ duplicate copy in the install tree splits that state with no error: the notification layer
24
+ stops seeing overlays that a different copy registered, which is exactly the failure the
25
+ 1.39.0 toast fix addresses. Consumers already installing `@iyulab/components` are unaffected;
26
+ the declaration now says that this package uses the copy they installed.
27
+
28
+ ## [0.18.19] - 2026-09-04
29
+
30
+ ### Added
31
+
32
+ - **`SidebarLayout` had no way for a consumer to control scroll position on route
33
+ change, and no official way to reach its scroll container at all** — only an
34
+ undocumented shadow-DOM `part="main"` selector, a style-only CSS hook rather than a
35
+ JS access contract. Added `layout.scrollBehavior(context, main)`, called on every
36
+ `route-done` before focus moves to the container, and a public `mainElement`
37
+ accessor on the element for reading/writing scroll position outside the hook (e.g.
38
+ saving a position from a `route-begin` listener). Unset `scrollBehavior` does
39
+ nothing, matching Vue Router's own default for an unset `scrollBehavior`.
40
+
3
41
  ## [0.18.18] - 2026-09-04
4
42
 
5
43
  ### Fixed
@@ -1,4 +1,4 @@
1
- //#region \0@oxc-project+runtime@0.148.0/helpers/esm/decorate.js
1
+ //#region \0@oxc-project+runtime@0.149.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.148.0/helpers/esm/decorateMetadata.js
1
+ //#region \0@oxc-project+runtime@0.149.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.148.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.149.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";
@@ -1,5 +1,5 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.149.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";
@@ -7,7 +7,7 @@ var t = e`
7
7
  align-items: center;
8
8
  text-align: center;
9
9
  padding: var(--u-space-4xl, 40px) var(--u-space-xl, 20px);
10
- color: var(--u-txt-color-weak, #757575);
10
+ color: var(--u-txt-color-weak, #616161);
11
11
  }
12
12
 
13
13
  .icon {
@@ -1,5 +1,5 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.149.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";
@@ -1,5 +1,5 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.149.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";
@@ -10,7 +10,7 @@ var t = e`
10
10
  font-size: var(--u-text-caption-size, 12px);
11
11
  font-weight: var(--u-text-caption-weight, 400);
12
12
  line-height: var(--u-text-caption-leading, 1.5);
13
- color: var(--u-txt-color-weak, #757575);
13
+ color: var(--u-txt-color-weak, #616161);
14
14
  margin-bottom: var(--u-space-3xs, 2px);
15
15
  }
16
16
 
@@ -37,7 +37,7 @@ var t = e`
37
37
  ⚠더 흐리게 만들고 싶은 유혹이 있는데, 이 자리가 안 읽히면 사용자는 빈 칸인지
38
38
  로딩 중인지 구별하지 못한다. */
39
39
  .value.blank {
40
- color: var(--u-txt-color-weak, #757575);
40
+ color: var(--u-txt-color-weak, #616161);
41
41
  font-weight: var(--u-text-body-weight, 400);
42
42
  }
43
43
 
@@ -71,7 +71,7 @@ var t = e`
71
71
  }
72
72
 
73
73
  .trend.tone-neutral {
74
- color: var(--u-txt-color-weak, #757575);
74
+ color: var(--u-txt-color-weak, #616161);
75
75
  }
76
76
  `;
77
77
  //#endregion
@@ -1,5 +1,5 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.149.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";
@@ -52,6 +52,20 @@ export declare class MasterDetailLayout extends StyledElement<ElementParts> {
52
52
  private hasDetail;
53
53
  private resizeObserver?;
54
54
  connectedCallback(): void;
55
+ /**
56
+ * 오버레이 판정 한 곳. **두 입력이 있고 둘 다 바뀔 수 있다** — 자기 폭과
57
+ * `overlayBreakpoint`.
58
+ *
59
+ * 🔴**종전에는 이 판정이 ResizeObserver 콜백 «안에만» 있었다.** 그래서 `overlayBreakpoint`
60
+ * 를 런타임에 바꿔도 **폭이 함께 바뀌지 않으면 아무 일도 일어나지 않았다** — prop 은
61
+ * 새 값을 갖고 있는데 `overlay` 속성은 옛 판정 그대로다. 값에 반응하는 prop 으로
62
+ * 선언·문서화해 놓고 실제로는 리사이즈에만 반응한 셈이라, 이 리포가 「선언 ≠ 동작」이라
63
+ * 부르는 부류다.
64
+ *
65
+ * @param width 0 이면 아직 레이아웃되지 않은 과도 상태일 수 있다(예: 조상이 `display:none`)
66
+ * — 그 순간의 값으로 오버레이를 강제하지 않는다.
67
+ */
68
+ private applyOverlay;
55
69
  disconnectedCallback(): void;
56
70
  protected updated(changed: PropertyValues): void;
57
71
  /**
@@ -1,5 +1,5 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.149.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";
@@ -22,15 +22,17 @@ var u = class extends n {
22
22
  }
23
23
  connectedCallback() {
24
24
  super.connectedCallback(), this.resizeObserver = new ResizeObserver((e) => {
25
- let t = e[0]?.contentRect.width ?? 0;
26
- t !== 0 && this.toggleAttribute("overlay", t < this.overlayBreakpoint);
25
+ this.applyOverlay(e[0]?.contentRect.width ?? 0);
27
26
  }), this.resizeObserver.observe(this);
28
27
  }
28
+ applyOverlay(e) {
29
+ e !== 0 && this.toggleAttribute("overlay", e < this.overlayBreakpoint);
30
+ }
29
31
  disconnectedCallback() {
30
32
  super.disconnectedCallback(), this.resizeObserver?.disconnect();
31
33
  }
32
34
  updated(e) {
33
- super.updated(e), e.has("masterSize") && this.style.setProperty("--_master-size", this.masterSize);
35
+ super.updated(e), e.has("masterSize") && this.style.setProperty("--_master-size", this.masterSize), e.has("overlayBreakpoint") && this.applyOverlay(this.getBoundingClientRect().width);
34
36
  }
35
37
  render() {
36
38
  return o`
@@ -1,5 +1,5 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.149.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";
@@ -37,7 +37,7 @@ var t = e`
37
37
  font-size: var(--u-text-caption-size, 12px);
38
38
  font-weight: var(--u-text-caption-weight, 400);
39
39
  line-height: var(--u-text-caption-leading, 1.5);
40
- color: var(--u-txt-color-weak, #757575);
40
+ color: var(--u-txt-color-weak, #616161);
41
41
  }
42
42
 
43
43
  .back {
@@ -1,5 +1,5 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.149.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.148.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.149.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.148.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.149.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.148.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.149.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.148.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.149.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";
@@ -40,7 +40,7 @@ var t = e`
40
40
  border: none;
41
41
  padding: var(--u-space-2xs, 4px) 0;
42
42
  font: inherit;
43
- color: var(--u-txt-color-weak, #757575);
43
+ color: var(--u-txt-color-weak, #616161);
44
44
  cursor: pointer;
45
45
  text-align: left;
46
46
  }
@@ -1,5 +1,5 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.149.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
@@ -14,6 +14,13 @@ export declare class SidebarLayout extends StyledElement<SidebarParts> {
14
14
  progressBarEl: UProgressBar;
15
15
  /** 현재 라우터 컨텍스트 */
16
16
  context: RouteContext | null;
17
+ /**
18
+ * 라우트 컨텐츠의 실제 스크롤 컨테이너(섀도 DOM `[part="main"]`). `scrollTop`을 읽어
19
+ * 위치를 저장하거나, 써서 복원한다 — `part="main"`은 스타일링용 CSS 훅일 뿐 JS 접근
20
+ * 계약이 아니었으므로, 소비자가 이 컨테이너에 안정적으로 접근할 공식 수단으로 신설.
21
+ * 아직 렌더 전이면 `null`.
22
+ */
23
+ get mainElement(): HTMLElement | null;
17
24
  connectedCallback(): void;
18
25
  disconnectedCallback(): void;
19
26
  protected willUpdate(changedProperties: PropertyValues): void;
@@ -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.148.0/helpers/esm/decorateMetadata.js";
4
- import r from "../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js";
3
+ import n from "../_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorateMetadata.js";
4
+ import r from "../_virtual/_@oxc-project_runtime@0.149.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 "../components/SidebarSection.js";
@@ -40,7 +40,9 @@ var v = class extends i {
40
40
  }, this.handleRouteDone = (e) => {
41
41
  this.progressBarEl.value = 100, setTimeout(() => {
42
42
  this.progressBarEl.removeAttribute("visible");
43
- }, 300), (this.shadowRoot?.querySelector(".main"))?.focus({ preventScroll: !0 });
43
+ }, 300);
44
+ let t = this.shadowRoot?.querySelector(".main");
45
+ t && (this.config?.scrollBehavior?.(e.context, t), t.focus({ preventScroll: !0 }));
44
46
  }, this.handleRouteError = (e) => {
45
47
  this.progressBarEl.status = "error", this.progressBarEl.value = 100, setTimeout(() => {
46
48
  this.progressBarEl.removeAttribute("visible"), this.progressBarEl.status = "default";
@@ -82,6 +84,9 @@ var v = class extends i {
82
84
  static {
83
85
  this.styles = [super.styles, o];
84
86
  }
87
+ get mainElement() {
88
+ return this.shadowRoot?.querySelector(".main") ?? null;
89
+ }
85
90
  connectedCallback() {
86
91
  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);
87
92
  }
@@ -1,4 +1,5 @@
1
1
  import { TemplateResult } from 'lit';
2
+ import { RouteContext } from '@iyulab/router';
2
3
  import { StyleMap } from '../internals/StyledElement';
3
4
  import { SidebarLinkConfig } from '../components/SidebarLink';
4
5
  import { SidebarSectionConfig } from '../components/SidebarSection';
@@ -55,6 +56,14 @@ export interface SidebarLayoutConfig {
55
56
  * 미지정 시 필터링하지 않는다(모든 항목 표시). 보통 `@iyulab/enterprise` 의 `hasPermission` 을 넘긴다.
56
57
  */
57
58
  hasPermission?: (code: string) => boolean;
59
+ /**
60
+ * 라우트 전환이 끝날 때(`route-done`, 포커스가 메인 스크롤 컨테이너로 옮겨지기 직전)
61
+ * 호출된다 — 새 라우트의 `RouteContext`와 그 스크롤 컨테이너(`SidebarLayout.mainElement`와
62
+ * 동일 엘리먼트)를 받는다. 스크롤 위치를 리셋·저장·복원하는 로직은 이 훅 안에서 직접
63
+ * 구현한다(예: 목록→상세로 갔다가 돌아올 때 스크롤 위치 복원). 미지정 시 기본값은
64
+ * "아무것도 안 함" — Vue Router의 `scrollBehavior` 미지정 기본값과 동일하다(breaking 아님).
65
+ */
66
+ scrollBehavior?: (context: RouteContext, main: HTMLElement) => void;
58
67
  /** 사이드바 스타일 맵 */
59
68
  styles?: StyleMap<SidebarParts>;
60
69
  }
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.18.18",
4
+ "version": "0.19.0",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "web-framework",
@@ -60,12 +60,12 @@
60
60
  "typecheck": "tsc --noEmit"
61
61
  },
62
62
  "dependencies": {
63
- "@iyulab/components": "^1.27.0",
64
63
  "@iyulab/router": "^0.11.0",
65
64
  "i18next": "^26.3.6",
66
65
  "lit": "^3.3.3"
67
66
  },
68
67
  "peerDependencies": {
68
+ "@iyulab/components": ">=1.27.0",
69
69
  "@lit/react": ">=1.0.8",
70
70
  "react": ">=18.0.0"
71
71
  },
@@ -86,7 +86,7 @@
86
86
  "happy-dom": "^20.10.6",
87
87
  "react": "^19.2.7",
88
88
  "react-dom": "^19.2.7",
89
- "typescript": "^5.9.3",
89
+ "typescript": "^6.0.2",
90
90
  "vite": "^8.1.4",
91
91
  "vite-plugin-dts": "^5.0.3",
92
92
  "vitest": "^4.1.10"
@@ -269,5 +269,5 @@ interface AuthGateContext {
269
269
  | `router` | `Router \| undefined` | Underlying `@iyulab/router` instance |
270
270
  | `screen` | `ScreenSize \| undefined` | Current responsive screen size |
271
271
  | `user` | `unknown` | Authenticated user when the `auth` boot gate is used; `undefined` if unauthenticated or unused |
272
- | `theme` | `Theme` (static) | Theme utility (`get`, `set`, `isInitialized`) |
272
+ | `theme` | `Theme` (static) | Theme utility `get()`, `set()`, `resolved()`, `accent()`, `isInitialized`. Branch on `resolved()` (always `'light'` or `'dark'`), not `get()`, which can be `'system'` |
273
273
  | `i18n` | `i18next` | Raw i18next instance |
@@ -35,6 +35,22 @@ each pane. CSS container-query conditions cannot read a runtime custom property,
35
35
  configurable breakpoint means measuring the host's own width in JavaScript and reflecting an
36
36
  `overlay` attribute — same self-width philosophy, different mechanism, for a fixed reason.
37
37
 
38
+ ## Sizing
39
+
40
+ This is a shell, not a sized box: `:host` is `height: 100%`, so **the parent decides its height**.
41
+ Inside a page shell that is already constrained (an `app.load()` layout, a grid or flex row with a
42
+ height) there is nothing to do.
43
+
44
+ Put it in a container with no height of its own and `height: 100%` has nothing to resolve against,
45
+ so the panes fall back to their content's height rather than the available space. That renders
46
+ fine — it simply is not the full-height split pane you asked for, and the independent per-pane
47
+ scrolling (`overflow: auto` on `master` and `detail`) only means something once the height is
48
+ constrained.
49
+
50
+ ```css
51
+ .page { height: 100%; } /* or any constrained height on the parent */
52
+ ```
53
+
38
54
  ## Slots
39
55
 
40
56
  | Name | Description |
@@ -24,6 +24,9 @@ interface SidebarLayoutConfig {
24
24
  /** Permission filter — hides items (and emptied section/groups) whose requirement fails. Unset shows everything. See "권한 기반 메뉴 필터" below. */
25
25
  hasPermission?: (code: string) => boolean;
26
26
 
27
+ /** Called on `route-done` (before focus moves to the container) with the new route's `RouteContext` and the scroll container itself — implement reset/save/restore here. Unset does nothing (same default as Vue Router's unset `scrollBehavior`). Also reachable outside the hook via the element's `.mainElement` accessor. */
28
+ scrollBehavior?: (context: RouteContext, main: HTMLElement) => void;
29
+
27
30
  /** Per-part style overrides (CSS custom properties / inline styles). */
28
31
  styles?: StyleMap<SidebarParts>;
29
32
  }
@@ -225,13 +228,19 @@ for `for="#id"` anchoring to resolve (see the `id` note above).
225
228
  }
226
229
  ```
227
230
 
228
- ⚠**Pick `placement` based on `state`, not a fixed value.** On `mobile`/`mobile-open`, the sidebar
229
- itself widens to occupy nearly the full screen a sideways placement (`right-start`, the natural
230
- choice for a desktop flyout) then has no room on either side, and `flip()` correctly declines to
231
- flip when the opposite side has none either. The popover renders off-screen and is invisible. A
232
- vertical placement (`bottom-start`) has room regardless of sidebar width and works at every state.
233
- Confirmed empirically in `tests/browser/sidebar-popover-submenu.browser.test.ts` — this is not a
234
- `strategy="absolute"` vs `"fixed"` distinction, switching strategy does not change the outcome.
231
+ **A fixed `placement` is safe as of `@iyulab/components@1.37.1`.** On `mobile`/`mobile-open`
232
+ the sidebar widens to occupy nearly the full screen, so a sideways placement (`right-start`,
233
+ the natural desktop flyout) has room on neither side. `flip()` now falls back **across the**
234
+ **axis** in exactly that case, landing the popover vertically instead of off-screen.
235
+
236
+ Against an older `components` it did render off-screen and invisible `flip()` only ever
237
+ considered the opposite side on the same axis, found no room there either, and gave up. If you
238
+ pin below `1.37.1`, keep choosing `placement` from `state` as the snippet above does.
239
+
240
+ Choosing from `state` is still reasonable when you want to *decide* the direction rather than
241
+ let `flip()` pick it. Either way this is not a `strategy="absolute"` vs `"fixed"` distinction —
242
+ switching strategy changes nothing. Both behaviours are pinned in
243
+ `tests/browser/sidebar-popover-submenu.browser.test.ts`.
235
244
 
236
245
  ---
237
246
 
@@ -252,6 +261,21 @@ Parts available for `styles` overrides on the root layout:
252
261
 
253
262
  ---
254
263
 
264
+ ## Sizing
265
+
266
+ `<u-sidebar-layout>` is a shell: `:host` is `height: 100%` with `overflow: hidden`, so **its
267
+ height comes from the parent** — it never sizes itself. `app.load()` covers the default case: when
268
+ `root` is `document.body` it sets `margin: 0; width: 100vw; height: 100vh` on the body for you.
269
+
270
+ ⚠ **A custom `root` receives no styling.** Hand it a container with no height of its own and
271
+ `height: 100%` has nothing to resolve against: the shell renders at whatever its own chrome
272
+ resolves to (measured: about 133px) instead of filling the screen — with no error and nothing in
273
+ the console. Give that container a height:
274
+
275
+ ```css
276
+ #app { height: 100vh; } /* or 100%, inside an already-constrained ancestor */
277
+ ```
278
+
255
279
  ## Responsive behaviour
256
280
 
257
281
  | Screen width | Sidebar state |