@iyulab/modern-app 0.55.1 → 0.56.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,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.56.0] - 2026-10-17
4
+
5
+ ### Added
6
+
7
+ - `u-info-field` KPI tiles: `appearance="tile"` (a bordered surface — border, radius, padding, surface colour; with a
8
+ `tone` the whole tile takes that tone's soft face and border), `size="xl"` (the figure read at a glance — the
9
+ `display` step, or `--info-field-xl-size`), `blank-reason` (`—` stays on the value line at figure size, the reason
10
+ goes on the line under it) and a `trend-icon` slot at the start of the trend line. `tone` now reflects to the
11
+ attribute.
12
+ - Sidebar `type: 'menu'`: `avatar` (`{ src, initials }`, drawn in place of the icon) and `description` (a second,
13
+ weaker line) — the account block at the foot of the sidebar. The trigger's accessible name is both lines; a slim
14
+ sidebar shows the avatar alone. `SidebarAvatar` is exported.
15
+
16
+ ## [0.55.2] - 2026-10-17
17
+
18
+ ### Fixed
19
+
20
+ - `u-master-detail-layout` overlay mode in a right-to-left document: the covering detail pane sat outside the layout
21
+ (0.55.1 placed it with a physical left margin). It now covers the list in both directions.
22
+ - `u-master-detail-layout` overlay mode: something in the list with its own `z-index` — a table's sticky header row —
23
+ was painted over the covering detail pane. While covered, the list is its own stacking context.
24
+
3
25
  ## [0.55.1] - 2026-10-17
4
26
 
5
27
  ### Fixed
@@ -2,7 +2,8 @@ import { PropertyValues } from 'lit';
2
2
  import { StyledElement } from '../internals/StyledElement.js';
3
3
  type ElementParts = 'host' | 'label' | 'value' | 'unit' | 'trend';
4
4
  export type InfoFieldFormat = 'number' | 'currency' | 'date';
5
- export type InfoFieldSize = 'default' | 'lg';
5
+ export type InfoFieldSize = 'default' | 'lg' | 'xl';
6
+ export type InfoFieldAppearance = 'plain' | 'tile';
6
7
  export type InfoFieldTrend = 'up' | 'down' | 'flat';
7
8
  export type InfoFieldTone = 'positive' | 'negative' | 'warning' | 'neutral';
8
9
  /** 값이 "아직 없음"인가 — `0` 과 `false` 는 **값이다**. */
@@ -23,7 +24,17 @@ export declare function isBlank(v: unknown): boolean;
23
24
  * <u-info-field label="거래처">동서인쇄</u-info-field> <!-- 슬롯이 value 를 이긴다 -->
24
25
  * <u-info-field label="합계" format="currency" currency="KRW" .value=${order.total}></u-info-field>
25
26
  * <u-info-field label="미결 작업지시" size="lg" .value=${12} unit="건"></u-info-field>
27
+ * <u-info-field appearance="tile" size="xl" label="MTBF" .value=${412} unit="h" tone="warning" .trendLabel=${'목표 500h 미달'}></u-info-field>
26
28
  * ```
29
+ *
30
+ * @slot - 값(문자열 `value` 대신 — 슬롯이 이긴다)
31
+ * @slot trend-icon - 추세 줄 앞의 상태 표시(예: 주의 아이콘). 추세 줄이 있을 때(`trend` · `trendLabel`) 그린다.
32
+ * @csspart label - 라벨
33
+ * @csspart value - 값
34
+ * @csspart unit - 단위
35
+ * @csspart trend - 추세 줄
36
+ * @csspart description - 값 아래 보조 줄(값이 비고 `blankReason` 이 있으면 그 사유)
37
+ * @cssprop --info-field-xl-size - `size="xl"` 값의 글자 크기(기본 `--u-text-display-size`)
27
38
  */
28
39
  export declare class InfoField extends StyledElement<ElementParts> {
29
40
  static styles: import('lit').CSSResultGroup[];
@@ -68,7 +79,8 @@ export declare class InfoField extends StyledElement<ElementParts> {
68
79
  /**
69
80
  * Display size. `'lg'` renders the value at the `title` type-scale step
70
81
  * (`--u-text-title-size`/`--u-text-title-weight`) — intended for dashboard KPI tiles
71
- * composed inside `u-info-section`. Reflects to the `size` attribute so
82
+ * composed inside `u-info-section`. `'xl'` is the figure read at a glance (the `display`
83
+ * step, or `--info-field-xl-size`). Reflects to the `size` attribute so
72
84
  * `:host([size="lg"])` styling works.
73
85
  */
74
86
  size: InfoFieldSize;
@@ -97,6 +109,18 @@ export declare class InfoField extends StyledElement<ElementParts> {
97
109
  * `--u-success|danger|warning-color-strong`.
98
110
  */
99
111
  tone?: InfoFieldTone;
112
+ /**
113
+ * `tile` — a bordered surface (border · radius · padding · surface colour) for a grid of KPI tiles; with a `tone`
114
+ * of `warning` / `negative` / `positive` the whole tile takes that tone's soft face and border. `plain` (default) is
115
+ * the bare label-value pair.
116
+ */
117
+ appearance: InfoFieldAppearance;
118
+ /**
119
+ * Why the value is blank — shown on the line under the value (where the description goes) while the value line keeps
120
+ * the `blank` placeholder at full size. Without it a blank `lg`/`xl` value shrinks to body text, since the placeholder
121
+ * may itself be a sentence.
122
+ */
123
+ blankReason?: string;
100
124
  /**
101
125
  * 값 아래의 보조 줄 — 그 값의 근거나 다른 단위의 같은 양(«1,764,211 units» · «Settled ₩743,188,120»).
102
126
  * 추세 문구는 `trendLabel` 이다(방향과 색을 갖는다).
@@ -14,7 +14,7 @@ function d(e) {
14
14
  }
15
15
  var f = class extends n {
16
16
  constructor(...e) {
17
- super(...e), this.label = "", this.blank = "—", this.numeric = !1, this.size = "default", this.namedWidgets = /* @__PURE__ */ new WeakMap();
17
+ super(...e), this.label = "", this.blank = "—", this.numeric = !1, this.size = "default", this.appearance = "plain", this.namedWidgets = /* @__PURE__ */ new WeakMap();
18
18
  }
19
19
  static {
20
20
  this.styles = [super.styles, r];
@@ -33,28 +33,37 @@ var f = class extends n {
33
33
  super.updated(e), e.has("label") && this.nameSlottedWidgets();
34
34
  }
35
35
  get hasSlotted() {
36
- return this.childNodes.length > 0 && [...this.childNodes].some((e) => e.nodeType !== Node.TEXT_NODE || (e.textContent ?? "").trim() !== "");
36
+ return [...this.childNodes].some((e) => e.nodeType === Node.ELEMENT_NODE ? !e.hasAttribute("slot") : e.nodeType === Node.TEXT_NODE && (e.textContent ?? "").trim() !== "");
37
37
  }
38
38
  formatValue() {
39
39
  return this.format === "currency" ? this.currency ? s(Number(this.value), this.currency) : l(Number(this.value)) : this.format === "number" ? l(Number(this.value)) : this.format === "date" ? c(this.value) : String(this.value);
40
40
  }
41
41
  render() {
42
- let e = !this.hasSlotted && d(this.value), t = this.numeric || this.format === "number" || this.format === "currency", n = this.trend !== void 0 || this.trendLabel !== void 0, r = this.tone ?? u(this.trend), a = this.trend === "up" ? "▲" : this.trend === "down" ? "▼" : "", o = !e && this.unit ? i`<span class="unit" part="unit">${this.unit}</span>` : "";
42
+ let e = !this.hasSlotted && d(this.value), t = e && this.blankReason ? this.blankReason : void 0, n = this.numeric || this.format === "number" || this.format === "currency", r = this.trend !== void 0 || this.trendLabel !== void 0, a = this.tone ?? u(this.trend), o = this.trend === "up" ? "▲" : this.trend === "down" ? "▼" : "", s = !e && this.unit ? i`<span class="unit" part="unit">${this.unit}</span>` : "";
43
43
  return i`
44
44
  <div class="label" part="label">${this.label}</div>
45
- <div class="value ${t ? "numeric" : ""} ${e ? "blank" : ""} tone-${r}" part="value">${this.hasSlotted ? i`<slot @slotchange=${() => void this.nameSlottedWidgets()}></slot>` : e ? this.blank : this.formatValue()}${o}</div>
46
- ${n ? i`
47
- <div class="trend tone-${r}" part="trend">
48
- ${a ? i`<span aria-hidden="true">${a}</span> ` : ""}${this.trendLabel ?? ""}
45
+ <div class="value ${n ? "numeric" : ""} ${e ? "blank" : ""} ${t ? "reasoned" : ""} tone-${a}" part="value">${this.hasSlotted ? i`<slot @slotchange=${() => void this.nameSlottedWidgets()}></slot>` : e ? this.blank : this.formatValue()}${s}</div>
46
+ ${r ? i`
47
+ <div class="trend tone-${a}" part="trend">
48
+ <slot name="trend-icon"></slot>${o ? i`<span aria-hidden="true">${o}</span> ` : ""}${this.trendLabel ?? ""}
49
49
  </div>
50
50
  ` : ""}
51
- ${this.description ? i`<div class="description" part="description">${this.description}</div>` : ""}
51
+ ${t ?? this.description ? i`<div class="description" part="description">${t ?? this.description}</div>` : ""}
52
52
  `;
53
53
  }
54
54
  };
55
55
  t([o({ type: String }), e("design:type", Object)], f.prototype, "label", void 0), t([o({ type: String }), e("design:type", Object)], f.prototype, "value", void 0), t([o({ type: String }), e("design:type", Object)], f.prototype, "blank", void 0), t([o({ type: Boolean }), e("design:type", Object)], f.prototype, "numeric", void 0), t([o({ type: String }), e("design:type", Object)], f.prototype, "format", void 0), t([o({ type: String }), e("design:type", String)], f.prototype, "currency", void 0), t([o({ type: String }), e("design:type", String)], f.prototype, "unit", void 0), t([o({
56
56
  type: String,
57
57
  reflect: !0
58
- }), e("design:type", Object)], f.prototype, "size", void 0), t([o({ type: String }), e("design:type", Object)], f.prototype, "trend", void 0), t([o({ type: String }), e("design:type", String)], f.prototype, "trendLabel", void 0), t([o({ type: String }), e("design:type", Object)], f.prototype, "tone", void 0), t([o({ type: String }), e("design:type", String)], f.prototype, "description", void 0), f = t([a("u-info-field")], f);
58
+ }), e("design:type", Object)], f.prototype, "size", void 0), t([o({ type: String }), e("design:type", Object)], f.prototype, "trend", void 0), t([o({ type: String }), e("design:type", String)], f.prototype, "trendLabel", void 0), t([o({
59
+ type: String,
60
+ reflect: !0
61
+ }), e("design:type", Object)], f.prototype, "tone", void 0), t([o({
62
+ type: String,
63
+ reflect: !0
64
+ }), e("design:type", Object)], f.prototype, "appearance", void 0), t([o({
65
+ type: String,
66
+ attribute: "blank-reason"
67
+ }), e("design:type", String)], f.prototype, "blankReason", void 0), t([o({ type: String }), e("design:type", String)], f.prototype, "description", void 0), f = t([a("u-info-field")], f);
59
68
  //#endregion
60
69
  export { f as InfoField, d as isBlank };
@@ -36,6 +36,39 @@ var t = e`
36
36
  }
37
37
 
38
38
 
39
+ :host([size="xl"]) .value {
40
+ font-size: var(--info-field-xl-size, var(--u-text-display-size, 26px));
41
+ font-weight: var(--u-text-display-weight, 700);
42
+ line-height: var(--u-text-display-leading, 1.4);
43
+ letter-spacing: var(--u-text-display-tracking, -0.02em);
44
+ }
45
+
46
+
47
+ :host([appearance="tile"]) {
48
+ padding: var(--u-space-md, 12px);
49
+ border: 1px solid var(--u-border-color, #E0E0E0);
50
+ border-radius: var(--u-radius-lg, 6px);
51
+ background-color: var(--u-bg-color, #FFFFFF);
52
+ box-sizing: border-box;
53
+ }
54
+
55
+ :host([appearance="tile"][tone="warning"]) {
56
+ background-color: var(--u-warning-bg-color, #FFF59D);
57
+ border-color: var(--u-warning-color-weak, #FFEB3B);
58
+ }
59
+ :host([appearance="tile"][tone="negative"]) {
60
+ background-color: var(--u-danger-bg-color, #FFEBEE);
61
+ border-color: var(--u-danger-color-weak, #F44336);
62
+ }
63
+ :host([appearance="tile"][tone="positive"]) {
64
+ background-color: var(--u-success-bg-color, #E8F5E9);
65
+ border-color: var(--u-success-color-weak, #4CAF50);
66
+ }
67
+ @media (forced-colors: active) {
68
+ :host([appearance="tile"]) { border-color: CanvasText; }
69
+ }
70
+
71
+
39
72
 
40
73
 
41
74
  .value.blank {
@@ -52,6 +85,18 @@ var t = e`
52
85
  font-weight: var(--u-text-body-weight, 400);
53
86
  min-height: calc(var(--u-text-title-size, 20px) * var(--u-text-body-leading, 1.6));
54
87
  }
88
+ :host([size="xl"]) .value.blank {
89
+ font-size: var(--u-text-body-size, 14px);
90
+ font-weight: var(--u-text-body-weight, 400);
91
+ min-height: calc(var(--info-field-xl-size, var(--u-text-display-size, 26px)) * var(--u-text-display-leading, 1.4));
92
+ }
93
+
94
+ :host([size="lg"]) .value.blank.reasoned {
95
+ font-size: var(--u-text-title-size, 20px);
96
+ }
97
+ :host([size="xl"]) .value.blank.reasoned {
98
+ font-size: var(--info-field-xl-size, var(--u-text-display-size, 26px));
99
+ }
55
100
 
56
101
 
57
102
 
@@ -33,6 +33,12 @@ var t = e`
33
33
  :host([overlay]) .master {
34
34
  flex: 1 1 auto;
35
35
  }
36
+
37
+
38
+
39
+ :host([overlay]) .master.covered {
40
+ isolation: isolate;
41
+ }
36
42
 
37
43
  .divider {
38
44
  flex: 0 0 auto;
@@ -84,7 +90,7 @@ var t = e`
84
90
 
85
91
  :host([overlay]) .detail {
86
92
  flex: 0 0 100%;
87
- margin-left: -100%;
93
+ margin-inline-start: -100%;
88
94
  height: var(--_detail-max, 100%);
89
95
  min-height: 0;
90
96
  max-height: none;
@@ -146,7 +152,7 @@ var t = e`
146
152
  display: none;
147
153
  }
148
154
  :host([overlay]) .detail {
149
- margin-left: 0;
155
+ margin-inline-start: 0;
150
156
  height: auto;
151
157
  box-shadow: none;
152
158
  }
@@ -2,7 +2,12 @@ import { DirectiveResult } from 'lit/directive.js';
2
2
  import { StyledElement, StyleMap } from '../internals/StyledElement.js';
3
3
  import { SidebarPermissionGuard } from '../layouts/SidebarPermission.js';
4
4
  /** 버튼 항목 부분 */
5
- type ElementParts = 'host' | 'base' | 'icon' | 'label';
5
+ type ElementParts = 'host' | 'base' | 'icon' | 'avatar' | 'label' | 'description';
6
+ /** 사이드바 버튼의 정체 표시 — 사진, 없으면 머리글자(두 글자까지). */
7
+ export interface SidebarAvatar {
8
+ src?: string;
9
+ initials?: string;
10
+ }
6
11
  /** 버튼 항목 구성 */
7
12
  export interface SidebarButtonConfig extends SidebarPermissionGuard {
8
13
  type: 'button';
@@ -40,6 +45,13 @@ export declare class SidebarButton extends StyledElement<ElementParts> {
40
45
  haspopup?: 'menu';
41
46
  /** 팝업이 열려 있는가 — `haspopup` 과 함께 `aria-expanded` 로 간다. 팝업이 없는 버튼에는 붙이지 않는다. */
42
47
  expanded: boolean;
48
+ /**
49
+ * 아이콘 대신 그리는 정체 표시(셸의 계정 메뉴) — 사진(`src`), 없으면 머리글자(`initials`). 꾸밈이라 보조기기에서 숨긴다:
50
+ * 이름은 라벨과 `description` 이 준다.
51
+ */
52
+ avatar?: SidebarAvatar;
53
+ /** 라벨 아래 둘째 줄(역할 · 소속) — 더 작고 약한 글자. 접힌(slim) 사이드바에서는 라벨과 함께 숨고 이름에는 남는다. */
54
+ description?: string | DirectiveResult;
43
55
  /** 포커스는 안쪽 `<button>` 으로 — 호스트는 포커스를 받지 않는다(팝업을 닫을 때 셸이 이것으로 되돌린다). */
44
56
  focus(options?: FocusOptions): void;
45
57
  render(): import('lit-html').TemplateResult<1>;
@@ -7,6 +7,7 @@ import { html as a } from "lit";
7
7
  import { customElement as o, property as s } from "lit/decorators.js";
8
8
  import "@iyulab/components/dist/components/icon/UIcon.js";
9
9
  import { ifDefined as c } from "lit/directives/if-defined.js";
10
+ import "@iyulab/components/dist/components/avatar/UAvatar.js";
10
11
  //#region src/components/SidebarButton.ts
11
12
  var l = class extends n {
12
13
  constructor(...e) {
@@ -24,13 +25,13 @@ var l = class extends n {
24
25
  aria-haspopup=${c(this.haspopup)}
25
26
  aria-expanded=${c(this.haspopup ? String(this.expanded) : void 0)}
26
27
  aria-pressed=${c(this.pressed === void 0 ? void 0 : String(this.pressed))}>
27
- <u-icon part="icon"
28
- .lib=${this.lib}
29
- .name=${this.icon}
30
- .fallback=${r}
31
- ></u-icon>
28
+ ${this.avatar ? a`<u-avatar part="avatar" aria-hidden="true" .image=${this.avatar.src} .label=${this.avatar.initials}></u-avatar>` : a`<u-icon part="icon"
29
+ .lib=${this.lib}
30
+ .name=${this.icon}
31
+ .fallback=${r}
32
+ ></u-icon>`}
32
33
  <span part="label" ?compact=${this.compact}>
33
- ${this.label}
34
+ ${this.description ? a`<span class="line">${this.label}</span><span class="description" part="description">${this.description}</span>` : this.label}
34
35
  </span>
35
36
  </button>
36
37
  `;
@@ -42,6 +43,6 @@ t([s({
42
43
  }), 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
44
  type: Boolean,
44
45
  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);
46
+ }), 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), t([s({ type: Object }), e("design:type", Object)], l.prototype, "avatar", void 0), t([s({ type: String }), e("design:type", Object)], l.prototype, "description", void 0), l = t([o("u-sidebar-button")], l);
46
47
  //#endregion
47
48
  export { l as SidebarButton };
@@ -68,6 +68,28 @@ var t = e`
68
68
  }
69
69
 
70
70
 
71
+ u-avatar {
72
+ flex-shrink: 0;
73
+ width: calc(var(--app-sidebar-icon-size, 20px) + 8px);
74
+ height: calc(var(--app-sidebar-icon-size, 20px) + 8px);
75
+ font-size: var(--u-text-caption-size, 12px);
76
+ }
77
+
78
+
79
+ .line,
80
+ .description {
81
+ display: block;
82
+ overflow: hidden;
83
+ text-overflow: ellipsis;
84
+ }
85
+ .description {
86
+ font-size: var(--u-text-caption-size, 12px);
87
+ font-weight: var(--u-text-body-weight, 400);
88
+ line-height: var(--u-text-caption-leading, 1.5);
89
+ color: var(--u-txt-color-weak, #616161);
90
+ }
91
+
92
+
71
93
 
72
94
 
73
95
 
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ export type { RouteConfig, RouteContext, FallbackRouteConfig, RouteLeaveContext
7
7
  export { RouteError, NotFoundError, AccessDeniedError, ContentLoadError, ContentRenderError, OutletMissingError, } from '@iyulab/router';
8
8
  export { defaultFallback } from './defaultFallback.js';
9
9
  export type { SidebarIconsConfig, SidebarLayoutConfig, SidebarItem, SidebarLogoConfig, SidebarLinkConfig, SidebarSectionConfig, SidebarGroupConfig, SidebarButtonConfig, SidebarMenuConfig, SidebarMenuItemConfig, SidebarHtmlConfig, SidebarState, } from './layouts/SidebarLayout.types';
10
+ export type { SidebarAvatar } from './components/SidebarButton.js';
10
11
  export type { SidebarLayoutEventMap } from './layouts/SidebarLayout.js';
11
12
  export { filterSidebarItems } from './layouts/filterSidebarItems.js';
12
13
  export { PageHeader } from './components/PageHeader.js';
@@ -288,6 +288,8 @@ var M = class extends n {
288
288
  .icon="${e.icon}"
289
289
  .lib="${e.lib}"
290
290
  .label="${e.label}"
291
+ .avatar=${e.avatar}
292
+ .description=${e.description}
291
293
  ></u-sidebar-button>
292
294
  <u-popover for="#${n}" placement=${r} autofocus
293
295
  @show=${() => {
@@ -5,7 +5,7 @@ import { StyleMap } from '../internals/StyledElement';
5
5
  import { SidebarLinkConfig } from '../components/SidebarLink';
6
6
  import { SidebarSectionConfig } from '../components/SidebarSection';
7
7
  import { SidebarGroupConfig } from '../components/SidebarGroup';
8
- import { SidebarButtonConfig } from '../components/SidebarButton';
8
+ import { SidebarButtonConfig, SidebarAvatar } from '../components/SidebarButton';
9
9
  import { SidebarPermissionGuard } from './SidebarPermission';
10
10
  export type { SidebarPermissionGuard } from './SidebarPermission';
11
11
  export type { SidebarLinkConfig } from '../components/SidebarLink';
@@ -82,6 +82,14 @@ export interface SidebarMenuConfig extends SidebarPermissionGuard {
82
82
  label: string | DirectiveResult;
83
83
  icon?: string;
84
84
  lib?: string;
85
+ /**
86
+ * Who the menu belongs to (an account menu at the foot of the sidebar) — drawn in place of the icon: a picture
87
+ * (`src`), else up to two `initials`. Decorative: the name comes from `label` and `description`. A slim sidebar shows
88
+ * the avatar alone.
89
+ */
90
+ avatar?: SidebarAvatar;
91
+ /** A second, weaker line under the label — a role, an organisation. Part of the trigger's accessible name. */
92
+ description?: string | DirectiveResult;
85
93
  items: SidebarMenuItemConfig[];
86
94
  }
87
95
  /** union: section | group | link | button | menu | html */
package/dist/react.d.ts CHANGED
@@ -5,7 +5,7 @@ import { Wizard as WizardElement, WizardEventMap } from './components/Wizard.js'
5
5
  import { GroupBoxLevel } from './components/GroupBox.js';
6
6
  import { EmptyStateVariant } from './components/EmptyState.js';
7
7
  import { TimelineItemColor } from './components/TimelineItem.js';
8
- import { InfoFieldFormat, InfoFieldSize, InfoFieldTrend, InfoFieldTone } from './components/InfoField.js';
8
+ import { InfoFieldFormat, InfoFieldSize, InfoFieldTrend, InfoFieldTone, InfoFieldAppearance } from './components/InfoField.js';
9
9
  export { ScreenObserver } from './internals/ScreenObserver.js';
10
10
  export type { ScreenSize, ScreenObserverConfig, ScreenResizeEvent } from './internals/ScreenObserver.js';
11
11
  /** `useUnsavedChanges` 가 돌려주는 것. */
@@ -55,6 +55,7 @@ export declare const Wizard: import('@lit/react').ReactWebComponent<WizardElemen
55
55
  export type WizardProps = React.ComponentProps<typeof Wizard>;
56
56
  export type { SidebarLayoutElement, WizardElement };
57
57
  export type { SidebarLayoutConfig, SidebarItem, SidebarLogoConfig, SidebarLinkConfig, SidebarSectionConfig, SidebarGroupConfig, SidebarButtonConfig, SidebarMenuConfig, SidebarMenuItemConfig, SidebarHtmlConfig, } from './layouts/SidebarLayout.types.js';
58
+ export type { SidebarAvatar } from './components/SidebarButton.js';
58
59
  export type { WizardStep, WizardStepState, WizardStepChangeDetail } from './components/Wizard.js';
59
60
  declare module 'react' {
60
61
  namespace JSX {
@@ -93,6 +94,9 @@ declare module 'react' {
93
94
  * 아님)상 실제 HTML 속성명은 `trendlabel`이다. */
94
95
  trendlabel?: string;
95
96
  tone?: InfoFieldTone;
97
+ appearance?: InfoFieldAppearance;
98
+ 'blank-reason'?: string;
99
+ description?: string;
96
100
  };
97
101
  'u-info-section': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
98
102
  min?: number | string;
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.55.1",
4
+ "version": "0.56.0",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "web-framework",
@@ -37,11 +37,13 @@ and those are different states of the business. So the component owns the rule.
37
37
  | `format` | `'number'\|'currency'\|'date'` | — | | Renders `value` through `@iyulab/components`' `formatNumber`/`formatCurrency`/`formatDate`; unset falls back to plain `String(value)` |
38
38
  | `currency` | `string` | — | | Currency code for `format="currency"` (e.g. `'KRW'`); omitted degrades to plain number formatting, does not throw |
39
39
  | `unit` | `string` | — | | Unit after the value (e.g. `'건'`, `'%'`, `'h'`), drawn one step below it — label size, body weight, weak color. Hidden while the value is blank. Also follows slotted value content |
40
- | `size` | `'default'\|'lg'` | `'default'` | ✓ | `'lg'` renders the value at the `title` type-scale step — for dashboard KPI tiles composed inside `u-info-section` |
40
+ | `size` | `'default'\|'lg'\|'xl'` | `'default'` | ✓ | `'lg'` renders the value at the `title` type-scale step — for dashboard KPI tiles composed inside `u-info-section`. `'xl'` is the figure read at a glance — the `display` step, or `--info-field-xl-size` |
41
+ | `appearance` | `'plain'\|'tile'` | `'plain'` | ✓ | `'tile'` draws a bordered surface (border · radius · padding · surface) for a grid of KPI tiles; with `tone` `warning`/`negative`/`positive` the whole tile takes that tone's soft face and border |
42
+ | `blankReason` | `string` | — | | Attribute `blank-reason`. Why the value is blank — on the line under the value, while the value line keeps `blank` (`—`) at figure size. Hidden once there is a value |
41
43
  | `trend` | `'up'\|'down'\|'flat'` | — | | Trend direction; renders a trend indicator when set, alongside `trendLabel`; pair with trendLabel for an accessible name — trend alone conveys direction by color only |
42
44
  | `trendLabel` | `string` | — | | Trend copy, e.g. `"+12% vs last month"` — wording is the consumer's responsibility |
43
45
  | `description` | `string` | — | | A supporting line below the value — its basis, or the same amount in another unit (`"1,764,211 units"`). Trend copy belongs in `trendLabel` |
44
- | `tone` | `'positive'\|'negative'\|'warning'\|'neutral'` | — | | Explicit tone override; resolves from `trend` when unset (`up→positive`, `down→negative`, `flat`/unset→`neutral`) but always wins over inference. Colors the value text itself, independent of `trend` — a static figure (e.g. a balance due) can be toned `negative` with no trend arrow. `warning` means "needs attention, not wrong" (a deadline coming up, a rate under target but within limits) and is never inferred |
46
+ | `tone` | `'positive'\|'negative'\|'warning'\|'neutral'` | — | ✓ | Explicit tone override; resolves from `trend` when unset (`up→positive`, `down→negative`, `flat`/unset→`neutral`) but always wins over inference. Colors the value text itself, independent of `trend` — a static figure (e.g. a balance due) can be toned `negative` with no trend arrow. `warning` means "needs attention, not wrong" (a deadline coming up, a rate under target but within limits) and is never inferred |
45
47
 
46
48
  ⚠ `numeric` no longer right-aligns (it did before 0.25.0). A field is a label/value pair that owns
47
49
  one grid cell, with no neighbouring figures to line up, so right-aligning only pushed the value
@@ -78,13 +80,38 @@ stay level:
78
80
  conditionally (`tone=${mttr == null ? 'warning' : undefined}`) — otherwise the value is toned
79
81
  as well once it exists.
80
82
 
83
+ To keep the dash and the reason apart — `—` on the value line at figure size, the reason on the
84
+ line under it — use `blank-reason` instead of a sentence in `blank`:
85
+
86
+ ```html
87
+ <u-info-field appearance="tile" size="xl" label="MTTR" .value=${mttr} unit="h"
88
+ blank-reason="No repairs in this period"></u-info-field>
89
+ ```
90
+
91
+ ### Tiles
92
+
93
+ `appearance="tile"` makes the field a tile of a KPI grid — the grid's gap is the container's. A
94
+ tile with `tone` takes that tone's face and border, so a figure that needs attention stands out
95
+ in the grid, not only by its text color. Put a status icon at the start of the trend line with
96
+ the `trend-icon` slot:
97
+
98
+ ```html
99
+ <u-info-section min="190px">
100
+ <u-info-field appearance="tile" size="xl" label="PM compliance" .value=${71} unit="%"
101
+ tone="warning" .trendLabel=${'Below the 90% target'}>
102
+ <u-icon slot="trend-icon" name="exclamation-triangle"></u-icon>
103
+ </u-info-field>
104
+ <u-info-field appearance="tile" size="xl" label="MTBF" .value=${412} unit="h"></u-info-field>
105
+ </u-info-section>
106
+ ```
107
+
81
108
  ## CSS Parts
82
109
 
83
110
  | Part | Description |
84
111
  |------|-------------|
85
112
  | `label` · `value` · `trend` | The label, the value, and the (optional) trend indicator |
86
113
  | `unit` | The unit, inside `value` (only when `unit` is set and the value is not blank) |
87
- | `description` | The supporting line (only when `description` is set) |
114
+ | `description` | The supporting line (when `description` is set, or the value is blank and `blankReason` is set) |
88
115
 
89
116
  ## CSS Custom Properties
90
117
 
@@ -221,6 +221,8 @@ interface SidebarMenuConfig {
221
221
  label: string | DirectiveResult; // the trigger's label (its accessible name when slim)
222
222
  icon?: string;
223
223
  lib?: string;
224
+ avatar?: { src?: string; initials?: string }; // in place of the icon — an account menu
225
+ description?: string | DirectiveResult; // a second, weaker line; part of the name
224
226
  items: SidebarMenuItemConfig[];
225
227
  }
226
228
 
@@ -255,6 +257,22 @@ The shell owns what only it knows:
255
257
  - **Permissions** — each entry takes `requirePermission`/`requireAnyPermission`; with
256
258
  `hasPermission` set, a menu left with no entries is dropped.
257
259
 
260
+ **Account menu.** `avatar` and `description` turn the trigger into the account block at the foot of
261
+ the sidebar — the avatar in place of the icon, the name on the label line, a role or organisation
262
+ on a smaller line under it. The accessible name is both lines; a slim sidebar shows the avatar
263
+ alone.
264
+
265
+ ```typescript
266
+ footer: [{
267
+ type: 'menu', label: user.name, description: user.role,
268
+ avatar: { src: user.photoUrl, initials: user.initials },
269
+ items: [
270
+ { label: 'Change password', onClick: () => app.navigate('/account/password') },
271
+ { label: 'Sign out', onClick: () => auth.signOut() },
272
+ ],
273
+ }]
274
+ ```
275
+
258
276
  ---
259
277
 
260
278
  ### `SidebarHtmlConfig` — `type: 'html'`