@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 +22 -0
- package/dist/components/InfoField.d.ts +26 -2
- package/dist/components/InfoField.js +18 -9
- package/dist/components/InfoField.styles.js +45 -0
- package/dist/components/MasterDetailLayout.styles.js +8 -2
- package/dist/components/SidebarButton.d.ts +13 -1
- package/dist/components/SidebarButton.js +8 -7
- package/dist/components/SidebarButton.styles.js +22 -0
- package/dist/index.d.ts +1 -0
- package/dist/layouts/SidebarLayout.js +2 -0
- package/dist/layouts/SidebarLayout.types.d.ts +9 -1
- package/dist/react.d.ts +5 -1
- package/package.json +1 -1
- package/skills/modern-app/references/components/info-field.md +30 -3
- package/skills/modern-app/references/layout.md +18 -0
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`.
|
|
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
|
|
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",
|
|
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 ${
|
|
46
|
-
${
|
|
47
|
-
<div class="trend tone-${
|
|
48
|
-
|
|
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({
|
|
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-
|
|
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-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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';
|
|
@@ -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
|
@@ -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 (
|
|
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'`
|