@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 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({ 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
+ }), 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 { UProgressBar as S } from "@iyulab/components/dist/components/progress-bar/UProgressBar.js";
27
+ import "@iyulab/components/dist/components/progress-bar/UProgressBar.js";
28
28
  //#region src/layouts/SidebarLayout.ts
29
- var C, w = v("@iyulab/modern-app"), T = 0;
30
- function E(e) {
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 D = /* @__PURE__ */ new Set([
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 O(e) {
49
+ function D(e) {
50
50
  let t = e.tagName;
51
- return t === "BUTTON" || t === "SUMMARY" || t === "A" && e.hasAttribute("href") ? !0 : D.has(e.getAttribute("role") ?? "");
51
+ return t === "BUTTON" || t === "SUMMARY" || t === "A" && e.hasAttribute("href") ? !0 : E.has(e.getAttribute("role") ?? "");
52
52
  }
53
- function k(e) {
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 A(e, t) {
57
+ function k(e, t) {
58
58
  for (let n of e) if (n instanceof HTMLElement) {
59
- if (k(n)) return n === t;
59
+ if (O(n)) return n === t;
60
60
  if (n === t) break;
61
61
  }
62
62
  return !0;
63
63
  }
64
- function j() {
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 M(e, t) {
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 N = class extends i {
77
+ var M = class extends i {
78
78
  static {
79
- C = this;
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:${++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.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 = j();
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 && E(t) || e.key === " " && t instanceof HTMLElement && O(t)) return;
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 || !A(e.composedPath(), n)) return;
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 || w(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).`);
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-${++C.menuSeq}`, this.menuIds.set(e, t));
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 = j();
316
- r && M(n, r) && this.shadowRoot?.querySelector(`#${t}`)?.focus();
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
- @click="${e.onClick}"
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] ?? C.DEFAULT_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 = j();
443
- n && t.some((e) => M(e, n)) || this.focusContentStart(t, 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 = j();
449
- n && r && M(n, r) || t.focus({ preventScroll: !0 });
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 = j();
458
- t && e.some((e) => M(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();
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 = j();
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)], 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, "mainTabStop", void 0), r([h(), n("design:type", Object)], N.prototype, "currentPath", void 0), N = C = r([f("u-sidebar-layout")], N);
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 { N as SidebarLayout };
478
+ export { M as SidebarLayout };
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.42.1",
4
+ "version": "0.43.0",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "web-framework",
@@ -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
  ---