@iyulab/modern-app 0.54.0 → 0.55.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,28 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.55.0] - 2026-10-16
4
+
5
+ ### Added
6
+
7
+ - `u-master-detail-layout` `detail-size`: the detail pane takes that width and the master fills the rest — a wide
8
+ list with a record summary beside it. `masterSize` keeps the opposite shape (narrow list, wide detail).
9
+ - `u-master-detail-layout` `slot="detail-placeholder"`: shown in the detail place while nothing is chosen (side by side).
10
+ - `useUnsavedChanges(dirty)` returns `{ markClean }`, and `UnsavedChangesController` has `markClean()`: the current state
11
+ is saved, so navigations until the next render (React) or update (Lit) do not ask. A save handler that navigates in
12
+ the same tick — `await post(draft); navigate(...)` — was asked «Discard unsaved changes?», because the check still
13
+ read the pre-save render’s `dirty`.
14
+
15
+ ### Changed
16
+
17
+ - Console diagnostics begin with `[@iyulab/modern-app]`, so a message in a console shared by several packages says which one wrote it (several also say what to do next).
18
+ - **Breaking:** `u-master-detail-layout`: while the `detail` slot is empty the master takes the whole width (it stayed at
19
+ `masterSize` beside an empty area). To keep the narrow master with a message beside it, use `detail-placeholder`.
20
+
21
+ ### Removed
22
+
23
+ - An unused custom property on `u-sidebar-group` (`--group-accent`) — nothing read it; group colours come from the
24
+ `--app-sidebar-group-*` tokens.
25
+
3
26
  ## [0.54.0] - 2026-10-14
4
27
 
5
28
  ### Added
@@ -1,4 +1,4 @@
1
- //#region \0@oxc-project+runtime@0.153.0/helpers/esm/decorate.js
1
+ //#region \0@oxc-project+runtime@0.154.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.153.0/helpers/esm/decorateMetadata.js
1
+ //#region \0@oxc-project+runtime@0.154.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.153.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.154.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.154.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.153.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.154.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.154.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 { getLocaleStrings as i } from "../internals/locale.js";
@@ -1,5 +1,5 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.154.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.154.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.153.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.154.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.154.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.153.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.154.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.154.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";
@@ -37,8 +37,13 @@ type ElementParts = 'host' | 'master' | 'divider' | 'detail' | 'detail-close';
37
37
  */
38
38
  export declare class MasterDetailLayout extends StyledElement<ElementParts> {
39
39
  static styles: import('lit').CSSResultGroup[];
40
- /** master 영역의 고정 폭(CSS 길이). detail 은 나머지 공간을 채운다. */
40
+ /** master 영역의 고정 폭(CSS 길이). detail 은 나머지 공간을 채운다. detail 이 비면 master 가 전부 쓴다. */
41
41
  masterSize: string;
42
+ /**
43
+ * detail 영역의 고정 폭(CSS 길이) — 주면 detail 이 이 폭이고 master 가 나머지를 채운다(`masterSize` 는 쓰지 않는다).
44
+ * 넓은 목록 곁에 레코드 요약을 붙이는 «곁 패널» 배치. 비우면 종전대로 master 가 고정 폭이다.
45
+ */
46
+ detailSize?: string;
42
47
  /**
43
48
  * 이 값(px, 자기 폭 기준) 미만이면 detail 이 전체 오버레이로 전환된다.
44
49
  * `masterSize` + 최소한의 읽기 가능한 detail 폭을 더한 값보다 커야 두 패널이
@@ -52,6 +57,8 @@ export declare class MasterDetailLayout extends StyledElement<ElementParts> {
52
57
  locale: string;
53
58
  /** detail 슬롯 배정 상태 — CSS `:has()` 로는 알 수 없다(`internals/slotted.ts` 참조). */
54
59
  private hasDetail;
60
+ /** `detail-placeholder` 슬롯 배정 상태 — detail 이 비었을 때 그 자리에 보인다. */
61
+ private hasPlaceholder;
55
62
  /**
56
63
  * `:state(detail)` 을 싣는 자리. 인쇄 규칙이 «한 판만 보이는가» 를 호스트에서 갈라야 해서
57
64
  * 생겼다(styles 의 인쇄 절 참조).
@@ -84,6 +91,7 @@ export declare class MasterDetailLayout extends StyledElement<ElementParts> {
84
91
  */
85
92
  private handleDetailClose;
86
93
  private handleDetailSlotChange;
94
+ private handlePlaceholderSlotChange;
87
95
  render(): import('lit-html').TemplateResult<1>;
88
96
  }
89
97
  /** Events `<u-master-detail-layout>` dispatches — bubbling and composed. */
@@ -1,5 +1,5 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.154.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.154.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 { getLocaleStrings as i } from "../internals/locale.js";
@@ -11,10 +11,12 @@ import "@iyulab/components/dist/components/button/UButton.js";
11
11
  //#region src/components/MasterDetailLayout.ts
12
12
  var u = class extends n {
13
13
  constructor(...e) {
14
- super(...e), this.masterSize = "22rem", this.overlayBreakpoint = 760, this.locale = "", this.hasDetail = !1, this.internals = typeof this.attachInternals == "function" ? this.attachInternals() : void 0, this.handleDetailClose = () => {
14
+ super(...e), this.masterSize = "22rem", this.overlayBreakpoint = 760, this.locale = "", this.hasDetail = !1, this.hasPlaceholder = !1, this.internals = typeof this.attachInternals == "function" ? this.attachInternals() : void 0, this.handleDetailClose = () => {
15
15
  this.fire("detail-close", { cancelable: !1 });
16
16
  }, this.handleDetailSlotChange = (e) => {
17
17
  this.hasDetail = r(e.target);
18
+ }, this.handlePlaceholderSlotChange = (e) => {
19
+ this.hasPlaceholder = r(e.target);
18
20
  };
19
21
  }
20
22
  static {
@@ -32,13 +34,14 @@ var u = class extends n {
32
34
  super.disconnectedCallback(), this.resizeObserver?.disconnect();
33
35
  }
34
36
  updated(e) {
35
- super.updated(e), e.has("masterSize") && this.style.setProperty("--_master-size", this.masterSize), e.has("hasDetail") && this.internals?.states?.[this.hasDetail ? "add" : "delete"]("detail"), e.has("overlayBreakpoint") && this.applyOverlay(this.getBoundingClientRect().width);
37
+ super.updated(e), e.has("masterSize") && this.style.setProperty("--_master-size", this.masterSize), e.has("detailSize") && (this.detailSize ? this.style.setProperty("--_detail-size", this.detailSize) : this.style.removeProperty("--_detail-size")), e.has("hasDetail") && this.internals?.states?.[this.hasDetail ? "add" : "delete"]("detail"), e.has("overlayBreakpoint") && this.applyOverlay(this.getBoundingClientRect().width);
36
38
  }
37
39
  render() {
40
+ let e = this.hasDetail ? "" : this.hasPlaceholder ? "placeholder" : "empty";
38
41
  return o`
39
- <div class="master ${this.hasDetail ? "covered" : ""}" part="master"><slot></slot></div>
40
- <div class="divider ${this.hasDetail ? "" : "empty"}" part="divider"></div>
41
- <div class="detail ${this.hasDetail ? "" : "empty"}" part="detail">
42
+ <div class="master ${this.hasDetail ? "covered" : ""} ${e === "empty" ? "alone" : ""}" part="master"><slot></slot></div>
43
+ <div class="divider ${e}" part="divider"></div>
44
+ <div class="detail ${e}" part="detail">
42
45
  <u-button class="detail-close" part="detail-close" appearance="plain"
43
46
  aria-label=${i(this.locale || void 0).detailClose}
44
47
  @click=${this.handleDetailClose}
@@ -46,6 +49,7 @@ var u = class extends n {
46
49
  <u-icon lib="internal" name="x"></u-icon>
47
50
  </u-button>
48
51
  <slot name="detail" @slotchange=${this.handleDetailSlotChange}></slot>
52
+ <slot name="detail-placeholder" ?hidden=${this.hasDetail} @slotchange=${this.handlePlaceholderSlotChange}></slot>
49
53
  </div>
50
54
  `;
51
55
  }
@@ -54,8 +58,12 @@ t([c({
54
58
  type: String,
55
59
  attribute: "master-size"
56
60
  }), e("design:type", Object)], u.prototype, "masterSize", void 0), t([c({
61
+ type: String,
62
+ attribute: "detail-size",
63
+ reflect: !0
64
+ }), e("design:type", String)], u.prototype, "detailSize", void 0), t([c({
57
65
  type: Number,
58
66
  attribute: "overlay-breakpoint"
59
- }), e("design:type", Object)], u.prototype, "overlayBreakpoint", void 0), t([c({ type: String }), e("design:type", Object)], u.prototype, "locale", void 0), t([l(), e("design:type", Object)], u.prototype, "hasDetail", void 0), u = t([s("u-master-detail-layout")], u);
67
+ }), e("design:type", Object)], u.prototype, "overlayBreakpoint", void 0), t([c({ type: String }), e("design:type", Object)], u.prototype, "locale", void 0), t([l(), e("design:type", Object)], u.prototype, "hasDetail", void 0), t([l(), e("design:type", Object)], u.prototype, "hasPlaceholder", void 0), u = t([s("u-master-detail-layout")], u);
60
68
  //#endregion
61
69
  export { u as MasterDetailLayout };
@@ -17,6 +17,18 @@ var t = e`
17
17
  }
18
18
 
19
19
 
20
+ .master.alone {
21
+ flex: 1 1 auto;
22
+ }
23
+
24
+ :host([detail-size]) .master {
25
+ flex: 1 1 auto;
26
+ }
27
+ :host([detail-size]:not([overlay])) .detail {
28
+ flex: 0 0 var(--_detail-size);
29
+ }
30
+
31
+
20
32
 
21
33
  :host([overlay]) .master {
22
34
  flex: 1 1 auto;
@@ -34,6 +46,10 @@ var t = e`
34
46
  :host([overlay]) .divider {
35
47
  display: none;
36
48
  }
49
+
50
+ :host([overlay]) .detail.placeholder {
51
+ display: none;
52
+ }
37
53
 
38
54
  .detail {
39
55
  flex: 1 1 auto;
@@ -102,6 +118,7 @@ var t = e`
102
118
  overflow: visible;
103
119
  }
104
120
  .detail-close,
121
+ .detail.placeholder,
105
122
  :host([overlay]) .detail-close,
106
123
  :host([overlay]) .master.covered {
107
124
  display: none;
@@ -1,5 +1,5 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.154.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.154.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 { getLocaleStrings as i } from "../internals/locale.js";
@@ -1,5 +1,5 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.154.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.154.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.153.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.154.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.154.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";
@@ -2,9 +2,6 @@ import { css as e } from "lit";
2
2
  //#region src/components/SidebarGroup.styles.ts
3
3
  var t = e`
4
4
  :host {
5
-
6
- --group-accent: var(--app-sidebar-active-bg, var(--u-primary-color, #1976D2));
7
-
8
5
  display: flex;
9
6
  flex-direction: column;
10
7
  color: var(--app-sidebar-fg, var(--u-txt-color, #212121));
@@ -1,5 +1,5 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.154.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.154.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 { DEFAULT_NAV_ICON as i } from "../internals/nav-icon.js";
@@ -1,5 +1,5 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.154.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.154.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,4 +1,4 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.154.0/helpers/esm/decorate.js";
2
2
  import { StyledElement as t } from "../internals/StyledElement.js";
3
3
  import "./TimelineItem.js";
4
4
  import { styles as n } from "./Timeline.styles.js";
@@ -1,5 +1,5 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.154.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.154.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 "./TimelineItem.styles.js";
@@ -1,5 +1,5 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.154.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.154.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.153.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.154.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.154.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
@@ -1,5 +1,5 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.154.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.154.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 { getLocaleStrings as i } from "../internals/locale.js";
@@ -81,7 +81,7 @@ var M = class extends n {
81
81
  constructor(...e) {
82
82
  super(...e), 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.mainTabStop = !1, this.overlayTrigger = null, this.internals = typeof this.attachInternals == "function" ? this.attachInternals() : void 0, this.currentPath = null, this.unsizedWarnKey = `unsized:${++w}`, 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 e = this.screenSize ?? a.screen ?? "large";
84
- e === "large" ? this.state = this.state === "default" ? "slim" : "default" : e === "medium" ? this.state = this.state === "slim" ? "modal" : "slim" : e === "small" ? this.state = this.state === "mobile" ? "mobile-open" : "mobile" : console.warn("Unknown screen size:", e);
84
+ e === "large" ? this.state = this.state === "default" ? "slim" : "default" : e === "medium" ? this.state = this.state === "slim" ? "modal" : "slim" : e === "small" ? this.state = this.state === "mobile" ? "mobile-open" : "mobile" : console.warn("[@iyulab/modern-app] unknown screen size:", e);
85
85
  }, this.handleBackdropClick = () => {
86
86
  this.state = "slim";
87
87
  }, this.handleRouteBegin = (e) => {
@@ -155,7 +155,7 @@ var M = class extends n {
155
155
  e.preventDefault();
156
156
  }, this.handleScreenResize = (e) => {
157
157
  let t = e.detail.size;
158
- this.screenSize = t, t === "large" ? this.state = "default" : t === "medium" ? this.state = "slim" : t === "small" ? this.state = "mobile" : console.warn("Unknown screen size:", t);
158
+ this.screenSize = t, t === "large" ? this.state = "default" : t === "medium" ? this.state = "slim" : t === "small" ? this.state = "mobile" : console.warn("[@iyulab/modern-app] unknown screen size:", t);
159
159
  };
160
160
  }
161
161
  static {
package/dist/react.d.ts CHANGED
@@ -8,6 +8,14 @@ import { TimelineItemColor } from './components/TimelineItem.js';
8
8
  import { InfoFieldFormat, InfoFieldSize, InfoFieldTrend, InfoFieldTone } from './components/InfoField.js';
9
9
  export { ScreenObserver } from './internals/ScreenObserver.js';
10
10
  export type { ScreenSize, ScreenObserverConfig, ScreenResizeEvent } from './internals/ScreenObserver.js';
11
+ /** `useUnsavedChanges` 가 돌려주는 것. */
12
+ export interface UnsavedChangesHandle {
13
+ /**
14
+ * 지금 값은 저장됐다(또는 버려도 된다) — 다음 렌더 전까지의 이동은 묻지 않는다. 저장 · 삭제 성공 뒤 같은 틱에 떠날 때 부른다:
15
+ * 이동 순간의 판정은 아직 저장 전 렌더의 `dirty` 를 본다. 다음 렌더부터는 다시 그 렌더의 값을 본다.
16
+ */
17
+ markClean(): void;
18
+ }
11
19
  /**
12
20
  * React 화면의 «저장 안 한 변경» — 마운트된 동안 판정을 등록한다(`trackUnsavedChanges`). 셸(`app.load()`)이 떠나기 전에
13
21
  * «변경을 버릴까요?» 를 묻고, 문서 이탈(새로 고침 · 탭 닫기)에는 브라우저 확인이 걸린다. 값은 매 렌더의 것을 본다.
@@ -15,11 +23,16 @@ export type { ScreenSize, ScreenObserverConfig, ScreenResizeEvent } from './inte
15
23
  * ```tsx
16
24
  * function OrderForm() {
17
25
  * const [draft, setDraft] = useState(initial);
18
- * useUnsavedChanges(draft !== initial);
26
+ * const unsaved = useUnsavedChanges(draft !== initial);
27
+ * async function save() {
28
+ * const row = await post(draft);
29
+ * unsaved.markClean(); // 저장했다 — 이어지는 이동은 묻지 않는다
30
+ * navigate(`/orders/${row.id}`);
31
+ * }
19
32
  * }
20
33
  * ```
21
34
  */
22
- export declare function useUnsavedChanges(dirty: boolean): void;
35
+ export declare function useUnsavedChanges(dirty: boolean): UnsavedChangesHandle;
23
36
  /**
24
37
  * React wrapper for `<u-sidebar-layout>`. `config` is an `@property({ type: Object })` —
25
38
  * a raw custom element handles that correctly under React 19 (property assignment) but not
package/dist/react.js CHANGED
@@ -7,7 +7,13 @@ import { createComponent as a } from "@lit/react";
7
7
  //#region src/react.ts
8
8
  function o(e) {
9
9
  let n = i.useRef(e);
10
- n.current = e, i.useEffect(() => t(() => n.current), []);
10
+ n.current = e;
11
+ let r = i.useRef(!1);
12
+ return i.useEffect(() => {
13
+ r.current = !1;
14
+ }), i.useEffect(() => t(() => !r.current && n.current), []), i.useMemo(() => ({ markClean: () => {
15
+ r.current = !0;
16
+ } }), []);
11
17
  }
12
18
  var s = a({
13
19
  react: i,
package/dist/unsaved.d.ts CHANGED
@@ -32,7 +32,8 @@ export declare function confirmDiscardChanges(options?: DiscardChangesOptions):
32
32
  */
33
33
  export declare function unsavedChangesGuard(_ctx?: RouteLeaveContext): boolean | Promise<boolean>;
34
34
  /**
35
- * Lit 요소의 «저장 안 한 변경» — 연결된 동안 판정을 등록하고, 떨어지면 해제한다.
35
+ * Lit 요소의 «저장 안 한 변경» — 연결된 동안 판정을 등록하고, 떨어지면 해제한다. 저장 · 삭제 뒤 같은 틱에 떠날 때는
36
+ * `markClean()` — 다음 갱신 전까지의 이동은 묻지 않는다(판정이 폼 상태를 아직 내리지 않았어도).
36
37
  * @example
37
38
  * ```ts
38
39
  * class OrderPage extends LitElement {
@@ -43,7 +44,12 @@ export declare function unsavedChangesGuard(_ctx?: RouteLeaveContext): boolean |
43
44
  export declare class UnsavedChangesController implements ReactiveController {
44
45
  private readonly isDirty;
45
46
  private release?;
47
+ private clean;
48
+ private readonly check;
46
49
  constructor(host: ReactiveControllerHost, isDirty: () => boolean);
50
+ /** 지금 상태는 저장됐다 — 호스트의 다음 갱신 전까지의 이동은 묻지 않는다. */
51
+ markClean(): void;
47
52
  hostConnected(): void;
53
+ hostUpdated(): void;
48
54
  hostDisconnected(): void;
49
55
  }
package/dist/unsaved.js CHANGED
@@ -12,7 +12,7 @@ function i() {
12
12
  for (let e of t) try {
13
13
  if (e()) return !0;
14
14
  } catch (e) {
15
- return console.error("Unsaved-changes check failed — treating the screen as unsaved:", e), !0;
15
+ return console.error("[@iyulab/modern-app] the unsaved-changes check threw — treating the screen as unsaved:", e), !0;
16
16
  }
17
17
  return !1;
18
18
  }
@@ -30,10 +30,16 @@ function o(e) {
30
30
  }
31
31
  var s = class {
32
32
  constructor(e, t) {
33
- this.isDirty = t, e.addController(this);
33
+ this.isDirty = t, this.clean = !1, this.check = () => !this.clean && this.isDirty(), e.addController(this);
34
+ }
35
+ markClean() {
36
+ this.clean = !0;
34
37
  }
35
38
  hostConnected() {
36
- this.release ??= r(this.isDirty);
39
+ this.release ??= r(this.check);
40
+ }
41
+ hostUpdated() {
42
+ this.clean = !1;
37
43
  }
38
44
  hostDisconnected() {
39
45
  this.release?.(), this.release = void 0;
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.54.0",
4
+ "version": "0.55.0",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "web-framework",
@@ -245,7 +245,8 @@ Back/Forward included) and the browser asks before the document is left (`before
245
245
  import { UnsavedChangesController, confirmDiscardChanges } from '@iyulab/modern-app';
246
246
  // Lit
247
247
  readonly unsaved = new UnsavedChangesController(this, () => this.form.dirty);
248
- // React: useUnsavedChanges(dirty) from '@iyulab/modern-app/react'
248
+ // React: const unsaved = useUnsavedChanges(dirty) from '@iyulab/modern-app/react'
249
+ // Saved (or deleted) and leaving in the same tick: unsaved.markClean() before navigate() — until the next render
249
250
  // Plain: const release = trackUnsavedChanges(() => dirty);
250
251
  // A modal drawer's close is not a navigation — ask from its cancellable `hide` event:
251
252
  drawer.addEventListener('hide', async (e) => {
@@ -20,6 +20,24 @@ Swap the `detail` slot's content when the selection changes:
20
20
  </u-master-detail-layout>
21
21
  ```
22
22
 
23
+ While nothing is in the `detail` slot the master takes the whole width — a fixed master with an
24
+ empty two-thirds beside it reads as a broken page. To keep the detail place visible with a «pick a
25
+ record» message instead, put it in `slot="detail-placeholder"`; it shows while `detail` is empty
26
+ and steps aside once it is filled (not in overlay mode, where it would cover the master).
27
+
28
+ ## A wide list with a side panel
29
+
30
+ `masterSize` fixes the master — the mail / settings shape (narrow list, wide detail). For the
31
+ opposite — a wide table with a record summary beside it — set `detail-size`: the detail takes that
32
+ width and the master fills the rest. With the detail empty the table takes everything.
33
+
34
+ ```html
35
+ <u-master-detail-layout detail-size="24rem">
36
+ <u-rich-table …></u-rich-table>
37
+ <aside slot="detail">…selected record…</aside>
38
+ </u-master-detail-layout>
39
+ ```
40
+
23
41
  On a narrow container (self width, not viewport — see below) the detail pane switches to a
24
42
  full overlay above the master pane, with a small close button that fires `detail-close`.
25
43
  Clearing the detail slot yourself (in response to the event) is what actually dismisses it —
@@ -72,13 +90,15 @@ own margin; that is inherent to printing two columns.
72
90
  | Name | Description |
73
91
  |------|-------------|
74
92
  | *(default)* | Master area |
75
- | `detail` | Detail area — present ⇒ shown, empty ⇒ hidden |
93
+ | `detail` | Detail area — present ⇒ shown, empty ⇒ hidden (the master then takes the whole width) |
94
+ | `detail-placeholder` | Shown in the detail place while `detail` is empty — side by side only |
76
95
 
77
96
  ## Properties
78
97
 
79
98
  | Property | Type | Default | Reflect | Description |
80
99
  |----------|------|---------|---------|-------------|
81
100
  | `masterSize` | `string` | `'22rem'` | | Fixed width of the master pane, a CSS length (`master-size`) |
101
+ | `detailSize` | `string | undefined` | `undefined` | ✓ | Fixed width of the detail pane (`detail-size`) — the master then fills the rest and `masterSize` is not used |
82
102
  | `overlayBreakpoint` | `number` | `760` | | Below this self width in px, detail becomes a full overlay (`overlay-breakpoint`) |
83
103
  | `locale` | `string` | `''` | | Language tag for the close button's accessible label |
84
104