@iyulab/modern-app 0.42.1 → 0.43.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 +17 -0
- package/dist/components/SidebarButton.d.ts +9 -0
- package/dist/components/SidebarButton.js +6 -2
- package/dist/components/SidebarButton.styles.js +8 -0
- package/dist/layouts/SidebarLayout.d.ts +5 -0
- package/dist/layouts/SidebarLayout.js +38 -32
- package/package.json +1 -1
- package/skills/modern-app/references/layout.md +16 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.43.0] - 2026-10-07
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Sidebar toggle buttons** — `SidebarButtonConfig.pressed` (`boolean` or a function). The button carries
|
|
8
|
+
`aria-pressed` and, when on, a soft surface and an accented icon (`--app-sidebar-pressed-bg`,
|
|
9
|
+
`--app-sidebar-pressed-icon-color`), visible in the slim sidebar and distinct from the current page's link. A
|
|
10
|
+
setting such as a field or dark mode no longer has to rename itself "turn on / turn off" to show its state. The
|
|
11
|
+
shell reads `pressed` on every render and re-renders after the button's `onClick`.
|
|
12
|
+
|
|
13
|
+
## [0.42.2] - 2026-10-07
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- The sidebar layout registers the `u-progress-bar` it renders with an explicit import. It imported `UProgressBar` for
|
|
18
|
+
a type only, and the published module kept the import by accident (decorator metadata).
|
|
19
|
+
|
|
3
20
|
## [0.42.1] - 2026-10-07
|
|
4
21
|
|
|
5
22
|
### Fixed
|
|
@@ -12,6 +12,13 @@ export interface SidebarButtonConfig extends SidebarPermissionGuard {
|
|
|
12
12
|
lib?: string;
|
|
13
13
|
label?: string | DirectiveResult;
|
|
14
14
|
styles?: StyleMap<ElementParts>;
|
|
15
|
+
/**
|
|
16
|
+
* 켬/끔 버튼(토글)의 지금 상태 — 지정하면 안쪽 버튼이 `aria-pressed` 를 싣고 켜졌을 때 눌린 모양(옅은 면 · 강조 아이콘)으로
|
|
17
|
+
* 그려진다(슬림 모드에서도 아이콘으로 보인다). 이름은 상태와 무관하게 하나로 둔다(«현장 모드» — «켜기/끄기» 가 아니라).
|
|
18
|
+
* 셸은 렌더마다 읽는다: 값이면 설정 객체의 그 값을, 함수면 그 결과를. 그 버튼의 `onClick` 뒤에는 셸이 다시 그리므로,
|
|
19
|
+
* 클릭이 상태를 바꾸는 흔한 경우는 따로 할 일이 없다. 미지정이면 일반 버튼이다.
|
|
20
|
+
*/
|
|
21
|
+
pressed?: boolean | (() => boolean);
|
|
15
22
|
onClick?: (event?: Event) => void;
|
|
16
23
|
}
|
|
17
24
|
/**
|
|
@@ -25,6 +32,8 @@ export declare class SidebarButton extends StyledElement<ElementParts> {
|
|
|
25
32
|
icon?: string;
|
|
26
33
|
/** `icon`을 해석할 등록 라이브러리 이름 */
|
|
27
34
|
lib?: string;
|
|
35
|
+
/** 켬/끔 상태 — 지정되면 토글 버튼(`aria-pressed`). 미지정이면 일반 버튼. */
|
|
36
|
+
pressed?: boolean;
|
|
28
37
|
/** 버튼 텍스트 라벨 */
|
|
29
38
|
label?: string | DirectiveResult;
|
|
30
39
|
/** 이 버튼이 여는 팝업의 종류 — 안쪽 `<button>` 의 `aria-haspopup` 으로 간다(셸의 `type: 'menu'` 트리거). */
|
|
@@ -22,7 +22,8 @@ var l = class extends n {
|
|
|
22
22
|
return a`
|
|
23
23
|
<button part="base" ?compact=${this.compact}
|
|
24
24
|
aria-haspopup=${c(this.haspopup)}
|
|
25
|
-
aria-expanded=${c(this.haspopup ? String(this.expanded) : void 0)}
|
|
25
|
+
aria-expanded=${c(this.haspopup ? String(this.expanded) : void 0)}
|
|
26
|
+
aria-pressed=${c(this.pressed === void 0 ? void 0 : String(this.pressed))}>
|
|
26
27
|
<u-icon part="icon"
|
|
27
28
|
.lib=${this.lib}
|
|
28
29
|
.name=${this.icon}
|
|
@@ -38,6 +39,9 @@ var l = class extends n {
|
|
|
38
39
|
t([s({
|
|
39
40
|
type: Boolean,
|
|
40
41
|
reflect: !0
|
|
41
|
-
}), e("design:type", Object)], l.prototype, "compact", void 0), t([s({ type: String }), e("design:type", String)], l.prototype, "icon", void 0), t([s({ type: String }), e("design:type", String)], l.prototype, "lib", void 0), t([s({
|
|
42
|
+
}), e("design:type", Object)], l.prototype, "compact", void 0), t([s({ type: String }), e("design:type", String)], l.prototype, "icon", void 0), t([s({ type: String }), e("design:type", String)], l.prototype, "lib", void 0), t([s({
|
|
43
|
+
type: Boolean,
|
|
44
|
+
reflect: !0
|
|
45
|
+
}), e("design:type", Boolean)], l.prototype, "pressed", void 0), t([s({ type: String }), e("design:type", Object)], l.prototype, "label", void 0), t([s({ type: String }), e("design:type", String)], l.prototype, "haspopup", void 0), t([s({ type: Boolean }), e("design:type", Object)], l.prototype, "expanded", void 0), l = t([o("u-sidebar-button")], l);
|
|
42
46
|
//#endregion
|
|
43
47
|
export { l as SidebarButton };
|
|
@@ -18,6 +18,14 @@ var t = e`
|
|
|
18
18
|
:host(:active) {
|
|
19
19
|
background-color: var(--u-bg-color-active, #EEEEEE);
|
|
20
20
|
}
|
|
21
|
+
/* 켜진 토글 — «현재 페이지»(활성 링크의 진한 면)와 구별되는 옅은 면 + 강조 아이콘. 위치와 설정이 같은 모양이면 섞인다.
|
|
22
|
+
아이콘 색이 상태를 나르므로 라벨이 숨는 슬림 모드에서도 보인다. */
|
|
23
|
+
:host([pressed]) {
|
|
24
|
+
background-color: var(--app-sidebar-pressed-bg, var(--u-bg-color-active, #EEEEEE));
|
|
25
|
+
}
|
|
26
|
+
:host([pressed]) u-icon {
|
|
27
|
+
color: var(--app-sidebar-pressed-icon-color, var(--u-primary-color, #1976D2));
|
|
28
|
+
}
|
|
21
29
|
|
|
22
30
|
button {
|
|
23
31
|
all: unset;
|
|
@@ -97,6 +97,11 @@ export declare class SidebarLayout extends StyledElement<SidebarParts> {
|
|
|
97
97
|
private renderItem;
|
|
98
98
|
/** 현재 경로와 패턴 매칭 여부 확인 */
|
|
99
99
|
private isMatchedLink;
|
|
100
|
+
/**
|
|
101
|
+
* 버튼 항목 클릭 — 소비자의 `onClick` 을 부른 뒤 다시 그린다. 켬/끔 버튼(`pressed`)은 그 클릭이 상태를 바꾸는 것이
|
|
102
|
+
* 흔하고, 설정 객체의 값이나 함수의 결과는 셸이 렌더할 때만 읽힌다.
|
|
103
|
+
*/
|
|
104
|
+
private handleButtonItemClick;
|
|
100
105
|
/** 브랜드 로고 클릭 핸들러: `href` 지정 시 해당 경로로, 아니면 홈으로 이동 */
|
|
101
106
|
/**
|
|
102
107
|
* 로고 렌더링: 아이콘명(문자열, 기존 동작) | 이미지({src,alt,href}) | 커스텀 렌더 함수.
|
|
@@ -24,16 +24,16 @@ import "@iyulab/components/dist/components/menu-item/UMenuItem.js";
|
|
|
24
24
|
import { createDevWarner as v } from "@iyulab/components/dist/utilities/devWarning.js";
|
|
25
25
|
import { isFocusCandidate as y, querySelectorDeep as b } from "@iyulab/components/dist/utilities/elements.js";
|
|
26
26
|
import { OverlayManager as x } from "@iyulab/components/dist/utilities/OverlayManager.js";
|
|
27
|
-
import
|
|
27
|
+
import "@iyulab/components/dist/components/progress-bar/UProgressBar.js";
|
|
28
28
|
//#region src/layouts/SidebarLayout.ts
|
|
29
|
-
var
|
|
30
|
-
function
|
|
29
|
+
var S, C = v("@iyulab/modern-app"), w = 0;
|
|
30
|
+
function T(e) {
|
|
31
31
|
let t = e.tagName;
|
|
32
32
|
if (t === "INPUT" || t === "TEXTAREA" || t === "SELECT" || e.isContentEditable) return !0;
|
|
33
33
|
let n = e.getAttribute("role");
|
|
34
34
|
return n === "textbox" || n === "searchbox" || n === "combobox" || n === "spinbutton";
|
|
35
35
|
}
|
|
36
|
-
var
|
|
36
|
+
var E = /* @__PURE__ */ new Set([
|
|
37
37
|
"button",
|
|
38
38
|
"link",
|
|
39
39
|
"checkbox",
|
|
@@ -46,27 +46,27 @@ var D = /* @__PURE__ */ new Set([
|
|
|
46
46
|
"tab",
|
|
47
47
|
"treeitem"
|
|
48
48
|
]);
|
|
49
|
-
function
|
|
49
|
+
function D(e) {
|
|
50
50
|
let t = e.tagName;
|
|
51
|
-
return t === "BUTTON" || t === "SUMMARY" || t === "A" && e.hasAttribute("href") ? !0 :
|
|
51
|
+
return t === "BUTTON" || t === "SUMMARY" || t === "A" && e.hasAttribute("href") ? !0 : E.has(e.getAttribute("role") ?? "");
|
|
52
52
|
}
|
|
53
|
-
function
|
|
53
|
+
function O(e) {
|
|
54
54
|
let t = getComputedStyle(e);
|
|
55
55
|
return (t.overflowY === "auto" || t.overflowY === "scroll") && e.scrollHeight > e.clientHeight;
|
|
56
56
|
}
|
|
57
|
-
function
|
|
57
|
+
function k(e, t) {
|
|
58
58
|
for (let n of e) if (n instanceof HTMLElement) {
|
|
59
|
-
if (
|
|
59
|
+
if (O(n)) return n === t;
|
|
60
60
|
if (n === t) break;
|
|
61
61
|
}
|
|
62
62
|
return !0;
|
|
63
63
|
}
|
|
64
|
-
function
|
|
64
|
+
function A() {
|
|
65
65
|
let e = document.activeElement;
|
|
66
66
|
for (; e?.shadowRoot?.activeElement;) e = e.shadowRoot.activeElement;
|
|
67
67
|
return e;
|
|
68
68
|
}
|
|
69
|
-
function
|
|
69
|
+
function j(e, t) {
|
|
70
70
|
let n = t;
|
|
71
71
|
for (; n;) {
|
|
72
72
|
if (n === e) return !0;
|
|
@@ -74,12 +74,12 @@ function M(e, t) {
|
|
|
74
74
|
}
|
|
75
75
|
return !1;
|
|
76
76
|
}
|
|
77
|
-
var
|
|
77
|
+
var M = class extends i {
|
|
78
78
|
static {
|
|
79
|
-
|
|
79
|
+
S = 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.routeLoading = !1, this.mainTabStop = !1, this.overlayTrigger = null, this.internals = typeof this.attachInternals == "function" ? this.attachInternals() : void 0, this.currentPath = null, this.unsizedWarnKey = `unsized:${++
|
|
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.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 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 = () => {
|
|
@@ -112,7 +112,7 @@ var N = class extends i {
|
|
|
112
112
|
}, this.handleOverlaySlotChange = (e) => {
|
|
113
113
|
let t = o(e.target);
|
|
114
114
|
if (t && !this.hasOverlay) {
|
|
115
|
-
let e =
|
|
115
|
+
let e = A();
|
|
116
116
|
this.overlayTrigger = e && e !== document.body ? e : null;
|
|
117
117
|
}
|
|
118
118
|
this.hasOverlay = t;
|
|
@@ -126,9 +126,9 @@ var N = class extends i {
|
|
|
126
126
|
}, this._handleMainKeydown = (e) => {
|
|
127
127
|
if (e.defaultPrevented) return;
|
|
128
128
|
let t = e.composedPath()[0];
|
|
129
|
-
if (t instanceof HTMLElement &&
|
|
129
|
+
if (t instanceof HTMLElement && T(t) || e.key === " " && t instanceof HTMLElement && D(t)) return;
|
|
130
130
|
let n = this.shadowRoot?.querySelector(".main");
|
|
131
|
-
if (!n || !
|
|
131
|
+
if (!n || !k(e.composedPath(), n)) return;
|
|
132
132
|
let r = n.clientHeight;
|
|
133
133
|
switch (e.key) {
|
|
134
134
|
case " ":
|
|
@@ -186,7 +186,7 @@ var N = class extends i {
|
|
|
186
186
|
process.env.NODE_ENV !== "production" && requestAnimationFrame(() => {
|
|
187
187
|
if (!this.isConnected) return;
|
|
188
188
|
let e = this.getBoundingClientRect().height;
|
|
189
|
-
e >= 200 ||
|
|
189
|
+
e >= 200 || C(this.unsizedWarnKey, `u-sidebar-layout is only ${Math.round(e)}px tall — its height: 100% found no sized ancestor, so the shell sits at its own chrome height and the route area has almost no room. Give the root element a height (e.g. #app { height: 100vh } — app.load() does this for document.body).`);
|
|
190
190
|
});
|
|
191
191
|
}
|
|
192
192
|
render() {
|
|
@@ -277,7 +277,7 @@ var N = class extends i {
|
|
|
277
277
|
}
|
|
278
278
|
renderMenu(e) {
|
|
279
279
|
let t = this.menuIds.get(e);
|
|
280
|
-
t || (t = `u-sidebar-menu-${++
|
|
280
|
+
t || (t = `u-sidebar-menu-${++S.menuSeq}`, this.menuIds.set(e, t));
|
|
281
281
|
let n = t, r = this.state === "mobile" || this.state === "mobile-open" ? "bottom-start" : "right-start";
|
|
282
282
|
return u`
|
|
283
283
|
<u-sidebar-button
|
|
@@ -312,8 +312,8 @@ var N = class extends i {
|
|
|
312
312
|
}
|
|
313
313
|
handleMenuHide(e, t) {
|
|
314
314
|
this.openMenu === t && (this.openMenu = null);
|
|
315
|
-
let n = e.currentTarget, r =
|
|
316
|
-
r &&
|
|
315
|
+
let n = e.currentTarget, r = A();
|
|
316
|
+
r && j(n, r) && this.shadowRoot?.querySelector(`#${t}`)?.focus();
|
|
317
317
|
}
|
|
318
318
|
renderItem(e) {
|
|
319
319
|
if (!e) return d;
|
|
@@ -328,7 +328,8 @@ var N = class extends i {
|
|
|
328
328
|
.lib="${e.lib}"
|
|
329
329
|
.label="${e.label}"
|
|
330
330
|
.styles="${e.styles}"
|
|
331
|
-
|
|
331
|
+
.pressed=${typeof e.pressed == "function" ? e.pressed() : e.pressed}
|
|
332
|
+
@click=${(t) => this.handleButtonItemClick(e, t)}
|
|
332
333
|
></u-sidebar-button>
|
|
333
334
|
`;
|
|
334
335
|
if (e.type === "menu") return this.renderMenu(e);
|
|
@@ -376,6 +377,10 @@ var N = class extends i {
|
|
|
376
377
|
}
|
|
377
378
|
return d;
|
|
378
379
|
}
|
|
380
|
+
handleButtonItemClick(e, t) {
|
|
381
|
+
let n = e.onClick?.(t);
|
|
382
|
+
this.requestUpdate(), n instanceof Promise && n.finally(() => this.requestUpdate());
|
|
383
|
+
}
|
|
379
384
|
static {
|
|
380
385
|
this.DEFAULT_ICONS = {
|
|
381
386
|
lib: "internal",
|
|
@@ -386,7 +391,7 @@ var N = class extends i {
|
|
|
386
391
|
};
|
|
387
392
|
}
|
|
388
393
|
icon(e) {
|
|
389
|
-
return this.config?.icons?.[e] ??
|
|
394
|
+
return this.config?.icons?.[e] ?? S.DEFAULT_ICONS[e];
|
|
390
395
|
}
|
|
391
396
|
renderLogo() {
|
|
392
397
|
let e = this.config?.logo, t = this.config?.title ?? "Home";
|
|
@@ -403,6 +408,7 @@ var N = class extends i {
|
|
|
403
408
|
return u`
|
|
404
409
|
<u-link class="logo-link" aria-label=${t}>
|
|
405
410
|
<span class="logo">
|
|
411
|
+
<!-- html-sink: documented HTML contract — a logo function returns markup the developer writes -->
|
|
406
412
|
${typeof n == "string" ? g(n) : n}
|
|
407
413
|
</span>
|
|
408
414
|
</u-link>
|
|
@@ -439,14 +445,14 @@ var N = class extends i {
|
|
|
439
445
|
async focusRoute(e) {
|
|
440
446
|
let t = this.routeContent();
|
|
441
447
|
if (await Promise.all(t.map((e) => e.updateComplete)), this.hasOverlay || !e.isConnected) return;
|
|
442
|
-
let n =
|
|
443
|
-
n && t.some((e) =>
|
|
448
|
+
let n = A();
|
|
449
|
+
n && t.some((e) => j(e, n)) || this.focusContentStart(t, e);
|
|
444
450
|
}
|
|
445
451
|
focusContentStart(e, t) {
|
|
446
452
|
let n = b(e, "[autofocus]", y);
|
|
447
453
|
n?.focus();
|
|
448
|
-
let r =
|
|
449
|
-
n && r &&
|
|
454
|
+
let r = A();
|
|
455
|
+
n && r && j(n, r) || t.focus({ preventScroll: !0 });
|
|
450
456
|
}
|
|
451
457
|
overlayPanels() {
|
|
452
458
|
return (this.shadowRoot?.querySelector("slot[name=\"overlay\"]"))?.assignedElements({ flatten: !0 }) ?? [];
|
|
@@ -454,19 +460,19 @@ var N = class extends i {
|
|
|
454
460
|
async focusOverlay() {
|
|
455
461
|
let e = this.overlayPanels();
|
|
456
462
|
if (await Promise.all(e.map((e) => e.updateComplete)), !this.hasOverlay) return;
|
|
457
|
-
let t =
|
|
458
|
-
t && e.some((e) =>
|
|
463
|
+
let t = A();
|
|
464
|
+
t && e.some((e) => j(e, t)) || (b(e, "[autofocus]", y) ?? b(e, "input, select, textarea, u-input, u-textarea, u-select, u-checkbox, u-radio, u-switch, u-slider", y) ?? this.shadowRoot?.querySelector(".overlay-close"))?.focus();
|
|
459
465
|
}
|
|
460
466
|
restoreOverlayFocus() {
|
|
461
467
|
let e = this.overlayTrigger;
|
|
462
468
|
this.overlayTrigger = null;
|
|
463
|
-
let t =
|
|
469
|
+
let t = A();
|
|
464
470
|
t && t !== document.body || (e?.isConnected ? e.focus() : this.mainElement?.focus({ preventScroll: !0 }));
|
|
465
471
|
}
|
|
466
472
|
};
|
|
467
473
|
r([p({
|
|
468
474
|
type: String,
|
|
469
475
|
reflect: !0
|
|
470
|
-
}), n("design:type", Object)],
|
|
476
|
+
}), n("design:type", Object)], M.prototype, "state", void 0), r([p({ type: Object }), n("design:type", Object)], M.prototype, "config", void 0), r([p({ type: Object }), n("design:type", typeof Record > "u" ? Object : Record)], M.prototype, "counts", void 0), r([p({ type: String }), n("design:type", Object)], M.prototype, "locale", void 0), r([m("u-progress-bar"), n("design:type", Object)], M.prototype, "progressBarEl", void 0), r([h(), n("design:type", Object)], M.prototype, "hasOverlay", void 0), r([h(), n("design:type", Object)], M.prototype, "openMenu", void 0), r([h(), n("design:type", Object)], M.prototype, "hasNotice", void 0), r([h(), n("design:type", Object)], M.prototype, "routeLoading", void 0), r([h(), n("design:type", Object)], M.prototype, "mainTabStop", void 0), r([h(), n("design:type", Object)], M.prototype, "currentPath", void 0), M = S = r([f("u-sidebar-layout")], M);
|
|
471
477
|
//#endregion
|
|
472
|
-
export {
|
|
478
|
+
export { M as SidebarLayout };
|
package/package.json
CHANGED
|
@@ -181,6 +181,7 @@ interface SidebarButtonConfig {
|
|
|
181
181
|
label: string | DirectiveResult;
|
|
182
182
|
onClick: () => void;
|
|
183
183
|
styles?: StyleMap<string>;
|
|
184
|
+
pressed?: boolean | (() => boolean); // a toggle: aria-pressed + the pressed look
|
|
184
185
|
}
|
|
185
186
|
```
|
|
186
187
|
|
|
@@ -190,6 +191,21 @@ Example:
|
|
|
190
191
|
{ type: 'button', icon: 'logout', label: 'Sign Out', onClick: () => auth.signOut() }
|
|
191
192
|
```
|
|
192
193
|
|
|
194
|
+
A setting the person turns on and off (dark mode, a field or compact mode, notifications) is a toggle: give it
|
|
195
|
+
`pressed`. The button then carries `aria-pressed`, and when on it shows a soft surface and an accented icon
|
|
196
|
+
(`--app-sidebar-pressed-bg`, `--app-sidebar-pressed-icon-color`) — visible when the sidebar is slim, and distinct
|
|
197
|
+
from the current page's link. Keep one name for both states. The shell reads `pressed` on every render — a value
|
|
198
|
+
from the config object, or a function's result — and re-renders after the button's `onClick`, so a click that
|
|
199
|
+
flips the state needs nothing more:
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
const fieldMode = {
|
|
203
|
+
type: 'button', icon: 'hand', label: 'Field mode',
|
|
204
|
+
pressed: () => density.isField(),
|
|
205
|
+
onClick: () => density.toggleField(),
|
|
206
|
+
};
|
|
207
|
+
```
|
|
208
|
+
|
|
193
209
|
For a button that opens a menu, use `type: 'menu'` below — not a button plus your own popover.
|
|
194
210
|
|
|
195
211
|
---
|