@iyulab/modern-app 0.55.0 → 0.55.1
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 +14 -0
- package/dist/components/MasterDetailLayout.d.ts +10 -0
- package/dist/components/MasterDetailLayout.js +21 -5
- package/dist/components/MasterDetailLayout.styles.js +34 -4
- package/dist/react.d.ts +1 -0
- package/package.json +1 -1
- package/skills/modern-app/references/components/master-detail-layout.md +15 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.55.1] - 2026-10-17
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- `u-master-detail-layout`: in a container with no height of its own (the page scrolls a long list), the detail pane
|
|
8
|
+
sticks to the top of the scroll container instead of stretching to the list's height with the record at its top —
|
|
9
|
+
a row picked far down the list showed its detail above the visible area. The pane is capped at the visible height of
|
|
10
|
+
that container and scrolls inside past it. In overlay mode the covering pane is placed over the visible part of the
|
|
11
|
+
list the same way (it covered the whole list from its top). With a height from the parent the panes look as before.
|
|
12
|
+
A `top` set through `::part(detail)` moves the pane below a sticky bar.
|
|
13
|
+
- React: the `u-master-detail-layout` JSX attribute map includes `detail-size`.
|
|
14
|
+
|
|
15
|
+
- Skill reference `u-master-detail-layout`: the `detailSize` row of the properties table is no longer split by the `|` in its type.
|
|
16
|
+
|
|
3
17
|
## [0.55.0] - 2026-10-16
|
|
4
18
|
|
|
5
19
|
### Added
|
|
@@ -67,6 +67,9 @@ export declare class MasterDetailLayout extends StyledElement<ElementParts> {
|
|
|
67
67
|
*/
|
|
68
68
|
private readonly internals;
|
|
69
69
|
private resizeObserver?;
|
|
70
|
+
/** 상세 칸이 붙는 스크롤 영역 — 없으면(문서가 스크롤한다) 뷰포트. */
|
|
71
|
+
private scroller;
|
|
72
|
+
private detailFrame;
|
|
70
73
|
connectedCallback(): void;
|
|
71
74
|
/**
|
|
72
75
|
* 오버레이 판정 한 곳. **두 입력이 있고 둘 다 바뀔 수 있다** — 자기 폭과
|
|
@@ -83,6 +86,13 @@ export declare class MasterDetailLayout extends StyledElement<ElementParts> {
|
|
|
83
86
|
*/
|
|
84
87
|
private applyOverlay;
|
|
85
88
|
disconnectedCallback(): void;
|
|
89
|
+
/**
|
|
90
|
+
* 상세 칸의 높이 상한 — min(그릇 높이, 스크롤 영역 높이 − 칸의 top). 칸은 스크롤 영역 위에
|
|
91
|
+
* 붙으므로(sticky) 그 영역의 보이는 높이가 칸이 보일 수 있는 전부다. 그릇 높이가 그보다 낮으면
|
|
92
|
+
* 그릇 안에 머문다.
|
|
93
|
+
* 다음 프레임에 쓴다 — 크기 관찰 콜백 안에서 크기를 바꾸면 관찰 루프 경고가 난다.
|
|
94
|
+
*/
|
|
95
|
+
private scheduleDetailMax;
|
|
86
96
|
protected updated(changed: PropertyValues): void;
|
|
87
97
|
/**
|
|
88
98
|
* 오버레이 모드의 닫기 버튼이 눌렸을 때. detail 내용을 지우는 것은 소비자 몫이라
|
|
@@ -11,7 +11,14 @@ 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.hasPlaceholder = !1, this.internals = typeof this.attachInternals == "function" ? this.attachInternals() : void 0, this.
|
|
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.scroller = null, this.detailFrame = 0, this.scheduleDetailMax = () => {
|
|
15
|
+
cancelAnimationFrame(this.detailFrame), this.detailFrame = requestAnimationFrame(() => {
|
|
16
|
+
let e = this.shadowRoot?.querySelector(".detail");
|
|
17
|
+
if (!e || !this.isConnected) return;
|
|
18
|
+
let t = this.scroller ? this.scroller.clientHeight : document.documentElement.clientHeight, n = parseFloat(getComputedStyle(e).top) || 0, r = Math.max(0, Math.min(this.getBoundingClientRect().height, t - n));
|
|
19
|
+
this.style.setProperty("--_detail-max", `${r}px`);
|
|
20
|
+
});
|
|
21
|
+
}, this.handleDetailClose = () => {
|
|
15
22
|
this.fire("detail-close", { cancelable: !1 });
|
|
16
23
|
}, this.handleDetailSlotChange = (e) => {
|
|
17
24
|
this.hasDetail = r(e.target);
|
|
@@ -24,17 +31,18 @@ var u = class extends n {
|
|
|
24
31
|
}
|
|
25
32
|
connectedCallback() {
|
|
26
33
|
super.connectedCallback(), this.resizeObserver = new ResizeObserver((e) => {
|
|
27
|
-
this.applyOverlay(
|
|
28
|
-
|
|
34
|
+
for (let t of e) t.target === this && this.applyOverlay(t.contentRect.width);
|
|
35
|
+
this.scheduleDetailMax();
|
|
36
|
+
}), this.resizeObserver.observe(this), this.scroller = d(this), this.scroller ? this.resizeObserver.observe(this.scroller) : window.addEventListener("resize", this.scheduleDetailMax);
|
|
29
37
|
}
|
|
30
38
|
applyOverlay(e) {
|
|
31
39
|
e !== 0 && this.toggleAttribute("overlay", e < this.overlayBreakpoint);
|
|
32
40
|
}
|
|
33
41
|
disconnectedCallback() {
|
|
34
|
-
super.disconnectedCallback(), this.resizeObserver?.disconnect();
|
|
42
|
+
super.disconnectedCallback(), this.resizeObserver?.disconnect(), window.removeEventListener("resize", this.scheduleDetailMax), cancelAnimationFrame(this.detailFrame), this.scroller = null;
|
|
35
43
|
}
|
|
36
44
|
updated(e) {
|
|
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);
|
|
45
|
+
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("hasDetail") || e.has("hasPlaceholder")) && this.scheduleDetailMax(), e.has("overlayBreakpoint") && this.applyOverlay(this.getBoundingClientRect().width);
|
|
38
46
|
}
|
|
39
47
|
render() {
|
|
40
48
|
let e = this.hasDetail ? "" : this.hasPlaceholder ? "placeholder" : "empty";
|
|
@@ -65,5 +73,13 @@ t([c({
|
|
|
65
73
|
type: Number,
|
|
66
74
|
attribute: "overlay-breakpoint"
|
|
67
75
|
}), 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);
|
|
76
|
+
function d(e) {
|
|
77
|
+
let t = e;
|
|
78
|
+
for (;;) {
|
|
79
|
+
let e = t;
|
|
80
|
+
if (t = e instanceof Element && e.assignedSlot ? e.assignedSlot : e.parentNode instanceof ShadowRoot ? e.parentNode.host : e.parentNode, !(t instanceof Element) || t === document.documentElement || t === document.body) return null;
|
|
81
|
+
if (/(auto|scroll|hidden|overlay)/.test(getComputedStyle(t).overflowY)) return t;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
68
84
|
//#endregion
|
|
69
85
|
export { u as MasterDetailLayout };
|
|
@@ -51,10 +51,27 @@ var t = e`
|
|
|
51
51
|
display: none;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
54
66
|
.detail {
|
|
55
67
|
flex: 1 1 auto;
|
|
56
68
|
min-width: 0;
|
|
57
|
-
position:
|
|
69
|
+
position: sticky;
|
|
70
|
+
top: 0;
|
|
71
|
+
align-self: flex-start;
|
|
72
|
+
min-height: min(100%, var(--_detail-max, 100%));
|
|
73
|
+
max-height: var(--_detail-max, none);
|
|
74
|
+
box-sizing: border-box;
|
|
58
75
|
overflow: auto;
|
|
59
76
|
}
|
|
60
77
|
.detail.empty {
|
|
@@ -63,9 +80,14 @@ var t = e`
|
|
|
63
80
|
|
|
64
81
|
|
|
65
82
|
|
|
83
|
+
|
|
84
|
+
|
|
66
85
|
:host([overlay]) .detail {
|
|
67
|
-
|
|
68
|
-
|
|
86
|
+
flex: 0 0 100%;
|
|
87
|
+
margin-left: -100%;
|
|
88
|
+
height: var(--_detail-max, 100%);
|
|
89
|
+
min-height: 0;
|
|
90
|
+
max-height: none;
|
|
69
91
|
z-index: 1;
|
|
70
92
|
background-color: var(--u-panel-bg-color, #FFFFFF);
|
|
71
93
|
box-shadow: var(--u-shadow-lg, 0 4px 12px rgba(0, 0, 0, 0.16), 0 2px 4px rgba(0, 0, 0, 0.06));
|
|
@@ -124,9 +146,17 @@ var t = e`
|
|
|
124
146
|
display: none;
|
|
125
147
|
}
|
|
126
148
|
:host([overlay]) .detail {
|
|
127
|
-
|
|
149
|
+
margin-left: 0;
|
|
150
|
+
height: auto;
|
|
128
151
|
box-shadow: none;
|
|
129
152
|
}
|
|
153
|
+
|
|
154
|
+
.detail {
|
|
155
|
+
position: static;
|
|
156
|
+
align-self: stretch;
|
|
157
|
+
min-height: 0;
|
|
158
|
+
max-height: none;
|
|
159
|
+
}
|
|
130
160
|
}
|
|
131
161
|
`;
|
|
132
162
|
//#endregion
|
package/dist/react.d.ts
CHANGED
|
@@ -107,6 +107,7 @@ declare module 'react' {
|
|
|
107
107
|
};
|
|
108
108
|
'u-master-detail-layout': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
|
|
109
109
|
'master-size'?: string;
|
|
110
|
+
'detail-size'?: string;
|
|
110
111
|
'overlay-breakpoint'?: number | string;
|
|
111
112
|
locale?: string;
|
|
112
113
|
};
|
package/package.json
CHANGED
|
@@ -69,6 +69,20 @@ constrained.
|
|
|
69
69
|
.page { height: 100%; } /* or any constrained height on the parent */
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
+
A long list usually does the opposite on purpose — the page scrolls, and the layout is as tall as
|
|
73
|
+
the list. The detail pane is built for that too: it sticks to the top of the scroll container it
|
|
74
|
+
sits in, so a record picked far down the list is shown beside it, in view. Its height is capped at
|
|
75
|
+
what that container shows (a longer record scrolls inside the pane), and below it there is nothing
|
|
76
|
+
but the list. In overlay mode the covering pane takes the visible height of that container the
|
|
77
|
+
same way, rather than the whole list's height.
|
|
78
|
+
|
|
79
|
+
With a height from the parent the pane fills it, as before. If a sticky bar sits at the top of the
|
|
80
|
+
scroll container, move the pane below it:
|
|
81
|
+
|
|
82
|
+
```css
|
|
83
|
+
u-master-detail-layout::part(detail) { top: 3rem; } /* the cap leaves room for the offset */
|
|
84
|
+
```
|
|
85
|
+
|
|
72
86
|
## Printing
|
|
73
87
|
|
|
74
88
|
On print media the panes follow what was on screen. Side by side, both panes print and grow
|
|
@@ -98,7 +112,7 @@ own margin; that is inherent to printing two columns.
|
|
|
98
112
|
| Property | Type | Default | Reflect | Description |
|
|
99
113
|
|----------|------|---------|---------|-------------|
|
|
100
114
|
| `masterSize` | `string` | `'22rem'` | | Fixed width of the master pane, a CSS length (`master-size`) |
|
|
101
|
-
| `detailSize` | `string
|
|
115
|
+
| `detailSize` | `string \| undefined` | `undefined` | ✓ | Fixed width of the detail pane (`detail-size`) — the master then fills the rest and `masterSize` is not used |
|
|
102
116
|
| `overlayBreakpoint` | `number` | `760` | | Below this self width in px, detail becomes a full overlay (`overlay-breakpoint`) |
|
|
103
117
|
| `locale` | `string` | `''` | | Language tag for the close button's accessible label |
|
|
104
118
|
|