@iyulab/modern-app 0.24.0 → 0.25.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 +52 -0
- package/dist/components/GroupBox.d.ts +12 -0
- package/dist/components/GroupBox.js +15 -5
- package/dist/components/InfoField.d.ts +15 -4
- package/dist/components/InfoField.js +3 -5
- package/dist/components/InfoField.styles.js +18 -2
- package/dist/layouts/SidebarLayout.d.ts +10 -0
- package/dist/layouts/SidebarLayout.js +2 -2
- package/package.json +1 -1
- package/skills/modern-app/references/components/group-box.md +1 -0
- package/skills/modern-app/references/components/info-field.md +30 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,57 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.25.0] - 2026-09-23
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`u-info-field` takes a `unit` — `'건'`, `'%'`, `'h'` — drawn after the value one step below
|
|
8
|
+
it: label size, body weight, weak color.** A KPI figure almost always carries a unit, and
|
|
9
|
+
written into the value it was drawn at the `size="lg"` title step, as large and as bold as the
|
|
10
|
+
number; the only way back was restating size, weight and color by hand at every tile, and a tile
|
|
11
|
+
that missed one showed a unit as big as its figure. The unit is hidden while the value is blank
|
|
12
|
+
(`— 건` is not a reading) and follows slotted value content too. Exposed as `::part(unit)`.
|
|
13
|
+
- **`u-group-box` takes a `level` (`2`–`6`, default `3`) for its title's place in the document
|
|
14
|
+
outline.** The title was always an `h3`. A box placed directly under `u-page-header` (the
|
|
15
|
+
page's `h1`) — the usual spot on a detail screen — skipped a level and sat deeper than its sibling
|
|
16
|
+
sections, which screen-reader heading navigation exposes. The box cannot know its own depth, so
|
|
17
|
+
the page that composes it says so: `<u-group-box level="2">`. The level changes semantics only;
|
|
18
|
+
the title looks the same at every level. The default is unchanged.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- 🔴 **`SidebarLayout`'s toggle follows the screen size it was told about, with or without
|
|
23
|
+
`app.load()`.** Size *changes* arrived as `screen-resize` events, but the toggle read the
|
|
24
|
+
`app.screen` singleton, which only exists when `app.load()` created the observer. Assembling the
|
|
25
|
+
shell yourself — as the `/react` entry point does, with `SidebarLayout` and `ScreenObserver` —
|
|
26
|
+
left it empty, so it fell back to `'large'`: on a phone-width screen, opening the menu switched to
|
|
27
|
+
the desktop sidebar squeezed beside the content instead of the mobile overlay, and the close glyph
|
|
28
|
+
never appeared. The layout now remembers the last size it received; `app.screen` remains a
|
|
29
|
+
fallback before the first event.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- **At `size="lg"`, a blank value is drawn at body size rather than the title step**, so `blank`
|
|
34
|
+
can carry a short reason when a figure cannot be computed. The value row keeps its height, so
|
|
35
|
+
tiles in one strip stay level.
|
|
36
|
+
|
|
37
|
+
- 🔴 **`u-info-field`'s `numeric` no longer right-aligns the value — it only sets tabular
|
|
38
|
+
figures.** The same applies to `format="number"` and `format="currency"`, which imply it. A
|
|
39
|
+
field is a label/value pair that owns one `u-info-section` grid cell; there are no
|
|
40
|
+
neighbouring figures in that cell to line up, so right-aligning only pushed the value to the far
|
|
41
|
+
side of the cell from its label. It was most visible in `size="lg"` KPI tiles — label at the
|
|
42
|
+
top left, value at the bottom right — and it was inconsistent: in one tile strip a currency
|
|
43
|
+
figure sat on the right while a plain count sat on the left. Tabular figures stay, so digits
|
|
44
|
+
keep their width as a value changes.
|
|
45
|
+
|
|
46
|
+
⚠ **Visible change:** numeric values in info fields now start under their label. If a layout of
|
|
47
|
+
yours genuinely needs right-aligned figures, style the part:
|
|
48
|
+
`u-info-field::part(value) { text-align: end; }`.
|
|
49
|
+
|
|
50
|
+
### Documentation
|
|
51
|
+
|
|
52
|
+
- The `u-info-field` reference describes `numeric` as tabular figures only, says why it no longer
|
|
53
|
+
aligns, and gives the `::part(value)` rule for layouts that need it.
|
|
54
|
+
|
|
3
55
|
## [0.24.0] - 2026-09-22
|
|
4
56
|
|
|
5
57
|
### Fixed
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { StyledElement } from '../internals/StyledElement.js';
|
|
2
2
|
import { default as React } from 'react';
|
|
3
3
|
type ElementParts = 'host' | 'header' | 'title' | 'actions' | 'body';
|
|
4
|
+
/** 제목 단계 — 페이지 제목(h1)은 `u-page-header` 몫이라 2 부터다. */
|
|
5
|
+
export type GroupBoxLevel = 2 | 3 | 4 | 5 | 6;
|
|
4
6
|
/**
|
|
5
7
|
* 그룹 박스 — 제목이 붙은 카드. LOB 상세 화면의 기본 단위다.
|
|
6
8
|
*
|
|
@@ -27,6 +29,15 @@ export declare class GroupBox extends StyledElement<ElementParts> {
|
|
|
27
29
|
divider: boolean;
|
|
28
30
|
/** 본문 여백을 없앤다 — 표를 카드 가장자리까지 붙일 때. */
|
|
29
31
|
flush: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Heading level of the title in the document outline (`2`–`6`, default `3`).
|
|
34
|
+
*
|
|
35
|
+
* The box cannot know how deep it sits in the page, so whoever composes the page says so — a
|
|
36
|
+
* box placed directly under `u-page-header` (the page's `h1`) is usually `2`, and leaving it at
|
|
37
|
+
* `3` skips a level and puts it deeper than its sibling sections. This changes semantics only:
|
|
38
|
+
* the title keeps its visual size at every level. Out-of-range values fall back to `3`.
|
|
39
|
+
*/
|
|
40
|
+
level: GroupBoxLevel;
|
|
30
41
|
/**
|
|
31
42
|
* 액션 슬롯 배정 상태.
|
|
32
43
|
* ★종전에는 'render()' 안에서 'this.querySelector()' 로 라이트 DOM 을 읽었다. 그것은
|
|
@@ -48,6 +59,7 @@ declare module 'react' {
|
|
|
48
59
|
title?: string;
|
|
49
60
|
divider?: boolean;
|
|
50
61
|
flush?: boolean;
|
|
62
|
+
level?: GroupBoxLevel;
|
|
51
63
|
};
|
|
52
64
|
}
|
|
53
65
|
}
|
|
@@ -5,10 +5,17 @@ import { slotHasContent as r } from "../internals/slotted.js";
|
|
|
5
5
|
import { styles as i } from "./GroupBox.styles.js";
|
|
6
6
|
import { html as a } from "lit";
|
|
7
7
|
import { customElement as o, property as s, state as c } from "lit/decorators.js";
|
|
8
|
+
import { html as l, literal as u } from "lit/static-html.js";
|
|
8
9
|
//#region src/components/GroupBox.ts
|
|
9
|
-
var
|
|
10
|
+
var d = {
|
|
11
|
+
2: u`h2`,
|
|
12
|
+
3: u`h3`,
|
|
13
|
+
4: u`h4`,
|
|
14
|
+
5: u`h5`,
|
|
15
|
+
6: u`h6`
|
|
16
|
+
}, f = class extends n {
|
|
10
17
|
constructor(...e) {
|
|
11
|
-
super(...e), this.title = "", this.divider = !1, this.flush = !1, this.hasActions = !1;
|
|
18
|
+
super(...e), this.title = "", this.divider = !1, this.flush = !1, this.level = 3, this.hasActions = !1;
|
|
12
19
|
}
|
|
13
20
|
static {
|
|
14
21
|
this.styles = [super.styles, i];
|
|
@@ -17,7 +24,7 @@ var l = class extends n {
|
|
|
17
24
|
let e = !!this.title || this.hasActions;
|
|
18
25
|
return a`
|
|
19
26
|
<div class="header ${this.divider ? "divider" : ""} ${e ? "" : "empty"}" part="header">
|
|
20
|
-
|
|
27
|
+
${l`<${d[this.level] ?? d[3]} class="title" part="title">${this.title}</${d[this.level] ?? d[3]}>`}
|
|
21
28
|
<div class="actions ${this.hasActions ? "" : "empty"}" part="actions">
|
|
22
29
|
<slot name="actions"
|
|
23
30
|
@slotchange=${(e) => this.hasActions = r(e.target)}
|
|
@@ -28,6 +35,9 @@ var l = class extends n {
|
|
|
28
35
|
`;
|
|
29
36
|
}
|
|
30
37
|
};
|
|
31
|
-
t([s({ type: String }), e("design:type", Object)],
|
|
38
|
+
t([s({ type: String }), e("design:type", Object)], f.prototype, "title", void 0), t([s({ type: Boolean }), e("design:type", Object)], f.prototype, "divider", void 0), t([s({ type: Boolean }), e("design:type", Object)], f.prototype, "flush", void 0), t([s({
|
|
39
|
+
type: Number,
|
|
40
|
+
reflect: !0
|
|
41
|
+
}), e("design:type", Object)], f.prototype, "level", void 0), t([c(), e("design:type", Object)], f.prototype, "hasActions", void 0), f = t([o("u-group-box")], f);
|
|
32
42
|
//#endregion
|
|
33
|
-
export {
|
|
43
|
+
export { f as GroupBox };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { StyledElement } from '../internals/StyledElement.js';
|
|
2
2
|
import { default as React } from 'react';
|
|
3
|
-
type ElementParts = 'host' | 'label' | 'value' | 'trend';
|
|
3
|
+
type ElementParts = 'host' | 'label' | 'value' | 'unit' | 'trend';
|
|
4
4
|
export type InfoFieldFormat = 'number' | 'currency' | 'date';
|
|
5
5
|
export type InfoFieldSize = 'default' | 'lg';
|
|
6
6
|
export type InfoFieldTrend = 'up' | 'down' | 'flat';
|
|
@@ -22,6 +22,7 @@ export declare function isBlank(v: unknown): boolean;
|
|
|
22
22
|
* <u-info-field label="부수" .value=${order.quantity} numeric></u-info-field>
|
|
23
23
|
* <u-info-field label="거래처">동서인쇄</u-info-field> <!-- 슬롯이 value 를 이긴다 -->
|
|
24
24
|
* <u-info-field label="합계" format="currency" currency="KRW" .value=${order.total}></u-info-field>
|
|
25
|
+
* <u-info-field label="미결 작업지시" size="lg" .value=${12} unit="건"></u-info-field>
|
|
25
26
|
* ```
|
|
26
27
|
*/
|
|
27
28
|
export declare class InfoField extends StyledElement<ElementParts> {
|
|
@@ -40,9 +41,12 @@ export declare class InfoField extends StyledElement<ElementParts> {
|
|
|
40
41
|
/** "아직 없음"을 나타낼 문구. */
|
|
41
42
|
blank: string;
|
|
42
43
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
44
|
+
* Numeric value — renders with tabular figures (`tabular-nums`) so digits keep their width
|
|
45
|
+
* when the value changes. Implied by `format="number"`/`"currency"`.
|
|
46
|
+
*
|
|
47
|
+
* It does **not** change alignment: this is a label/value pair, not a table cell, so there are
|
|
48
|
+
* no neighbouring figures to line up and right-aligning only pushes the value away from its
|
|
49
|
+
* label. Where a layout does need right-aligned figures, style `::part(value)`.
|
|
46
50
|
*/
|
|
47
51
|
numeric: boolean;
|
|
48
52
|
/**
|
|
@@ -55,6 +59,12 @@ export declare class InfoField extends StyledElement<ElementParts> {
|
|
|
55
59
|
* omitted, degrades to plain number formatting without a currency symbol (does not throw).
|
|
56
60
|
*/
|
|
57
61
|
currency?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Unit shown after the value — e.g. `'건'`, `'%'`, `'h'`. Drawn one step below the value (label
|
|
64
|
+
* size, body weight, weak color) so a KPI figure keeps its emphasis at `size="lg"`. Hidden while
|
|
65
|
+
* the value is blank: a placeholder has no unit. Follows slotted value content too.
|
|
66
|
+
*/
|
|
67
|
+
unit?: string;
|
|
58
68
|
/**
|
|
59
69
|
* Display size. `'lg'` renders the value at the `title` type-scale step
|
|
60
70
|
* (`--u-text-title-size`/`--u-text-title-weight`) — intended for dashboard KPI tiles
|
|
@@ -105,6 +115,7 @@ declare module 'react' {
|
|
|
105
115
|
numeric?: boolean;
|
|
106
116
|
format?: InfoFieldFormat;
|
|
107
117
|
currency?: string;
|
|
118
|
+
unit?: string;
|
|
108
119
|
size?: InfoFieldSize;
|
|
109
120
|
trend?: InfoFieldTrend;
|
|
110
121
|
/** 프로퍼티는 `trendLabel`이지만 Lit 기본 속성명 규칙(소문자화, kebab
|
|
@@ -26,12 +26,10 @@ var f = class extends n {
|
|
|
26
26
|
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);
|
|
27
27
|
}
|
|
28
28
|
render() {
|
|
29
|
-
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" ? "▼" : "";
|
|
29
|
+
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>` : "";
|
|
30
30
|
return i`
|
|
31
31
|
<div class="label" part="label">${this.label}</div>
|
|
32
|
-
<div class="value ${t ? "numeric" : ""} ${e ? "blank" : ""} tone-${r}" part="value">
|
|
33
|
-
${this.hasSlotted ? i`<slot></slot>` : e ? this.blank : this.formatValue()}
|
|
34
|
-
</div>
|
|
32
|
+
<div class="value ${t ? "numeric" : ""} ${e ? "blank" : ""} tone-${r}" part="value">${this.hasSlotted ? i`<slot></slot>` : e ? this.blank : this.formatValue()}${o}</div>
|
|
35
33
|
${n ? i`
|
|
36
34
|
<div class="trend tone-${r}" part="trend">
|
|
37
35
|
${a ? i`<span aria-hidden="true">${a}</span> ` : ""}${this.trendLabel ?? ""}
|
|
@@ -40,7 +38,7 @@ var f = class extends n {
|
|
|
40
38
|
`;
|
|
41
39
|
}
|
|
42
40
|
};
|
|
43
|
-
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({
|
|
41
|
+
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({
|
|
44
42
|
type: String,
|
|
45
43
|
reflect: !0
|
|
46
44
|
}), 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), f = t([a("u-info-field")], f);
|
|
@@ -23,8 +23,10 @@ var t = e`
|
|
|
23
23
|
overflow-wrap: anywhere;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/* 고정폭 숫자만 — 정렬은 건드리지 않는다. 이 요소는 표의 칸이 아니라 라벨-값 한 쌍이라
|
|
27
|
+
세로로 맞출 이웃 숫자가 없다. 우정렬하면 값이 라벨에서 칸 반대편으로 밀려날 뿐이다.
|
|
28
|
+
표처럼 열을 맞춰야 하는 배치라면 소비자가 ::part(value) 로 정렬한다. */
|
|
26
29
|
.value.numeric {
|
|
27
|
-
text-align: right;
|
|
28
30
|
font-variant-numeric: tabular-nums;
|
|
29
31
|
}
|
|
30
32
|
|
|
@@ -41,9 +43,23 @@ var t = e`
|
|
|
41
43
|
font-weight: var(--u-text-body-weight, 400);
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
/* size="lg" 보다 우선해야 한다 — blank 는 lg 모드에서도 계속 de-emphasize 상태를 유지해야 한다.
|
|
46
|
+
/* size="lg" 보다 우선해야 한다 — blank 는 lg 모드에서도 계속 de-emphasize 상태를 유지해야 한다.
|
|
47
|
+
크기도 본문 단으로 내린다: blank 문구가 «값을 낼 수 없는 사유» 문장일 수 있고, 제목 단에서는
|
|
48
|
+
타일에 담기지 않는다. 대신 값 줄의 높이는 지켜 한 줄에 놓인 타일들의 높이가 갈리지 않게 한다
|
|
49
|
+
(lg 값 줄 = 제목 크기 × 본문 행간 — 위 .value 가 행간을 본문 단에 두기 때문이다). */
|
|
45
50
|
:host([size="lg"]) .value.blank {
|
|
51
|
+
font-size: var(--u-text-body-size, 14px);
|
|
46
52
|
font-weight: var(--u-text-body-weight, 400);
|
|
53
|
+
min-height: calc(var(--u-text-title-size, 20px) * var(--u-text-body-leading, 1.6));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* 단위는 값보다 한 단 아래 — 크기·굵기·색 셋을 함께 낮춰야 KPI 숫자가 강조를 지킨다.
|
|
57
|
+
이 세트를 소비자가 기억해 되돌리게 두면 한 곳이라도 빠진 자리에서 단위가 숫자만큼 커진다. */
|
|
58
|
+
.unit {
|
|
59
|
+
margin-inline-start: 0.25em;
|
|
60
|
+
font-size: var(--u-text-label-size, 13px);
|
|
61
|
+
font-weight: var(--u-text-body-weight, 400);
|
|
62
|
+
color: var(--u-txt-color-weak, #616161);
|
|
47
63
|
}
|
|
48
64
|
|
|
49
65
|
/* tone은 trend 유무와 무관하게 값 텍스트 자체에도 적용된다(neutral은 무규칙 — 기본
|
|
@@ -68,6 +68,16 @@ export declare class SidebarLayout extends StyledElement<SidebarParts> {
|
|
|
68
68
|
/** 설정이 준 값이 있으면 그것을, 없으면 기본값을 돌려준다(키 단위 부분 오버라이드). */
|
|
69
69
|
private icon;
|
|
70
70
|
private renderLogo;
|
|
71
|
+
/**
|
|
72
|
+
* 이 레이아웃이 마지막으로 받은 화면 크기.
|
|
73
|
+
*
|
|
74
|
+
* 크기 «전환» 은 `screen-resize` 이벤트로 받으므로 토글도 같은 출처를 읽어야 한다.
|
|
75
|
+
* 종전에는 토글이 싱글턴 `app.screen` 을 읽었는데, 그것은 `app.load()` 가 옵저버를 만들
|
|
76
|
+
* 때만 존재한다 — `/react` 처럼 레이아웃과 `ScreenObserver` 를 직접 조립하면 항상 비어
|
|
77
|
+
* `'large'` 로 떨어졌고, 모바일 폭에서 메뉴를 열면 데스크톱 사이드바가 펼쳐졌다.
|
|
78
|
+
* `app.screen` 은 이벤트를 아직 못 받았을 때의 폴백으로만 남긴다.
|
|
79
|
+
*/
|
|
80
|
+
private screenSize?;
|
|
71
81
|
/** 사이드바 토글 핸들러 */
|
|
72
82
|
private handleToggleButtonClick;
|
|
73
83
|
/**
|
|
@@ -45,7 +45,7 @@ var E = class extends i {
|
|
|
45
45
|
}
|
|
46
46
|
constructor(...t) {
|
|
47
47
|
super(...t), this.state = "default", this.locale = "", this.hasOverlay = !1, this.internals = typeof this.attachInternals == "function" ? this.attachInternals() : void 0, this.context = null, this.unsizedWarnKey = `unsized:${++S}`, this.isMatchedLink = (e) => !this.context || !e ? !1 : (e = typeof e == "string" ? new URLPattern(e, window.location.origin) : e, e.test(this.context.path, window.location.origin)), this.handleToggleButtonClick = () => {
|
|
48
|
-
let t = e.screen ?? "large";
|
|
48
|
+
let t = this.screenSize ?? e.screen ?? "large";
|
|
49
49
|
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);
|
|
50
50
|
}, this.handleBackdropClick = () => {
|
|
51
51
|
this.state = "slim";
|
|
@@ -98,7 +98,7 @@ var E = class extends i {
|
|
|
98
98
|
e.preventDefault();
|
|
99
99
|
}, this.handleScreenResize = (e) => {
|
|
100
100
|
let t = e.detail.size;
|
|
101
|
-
t === "large" ? this.state = "default" : t === "medium" ? this.state = "slim" : t === "small" ? this.state = "mobile" : console.warn("Unknown screen size:", t);
|
|
101
|
+
this.screenSize = t, t === "large" ? this.state = "default" : t === "medium" ? this.state = "slim" : t === "small" ? this.state = "mobile" : console.warn("Unknown screen size:", t);
|
|
102
102
|
};
|
|
103
103
|
}
|
|
104
104
|
static {
|
package/package.json
CHANGED
|
@@ -32,6 +32,7 @@ a left edge without any consumer CSS.
|
|
|
32
32
|
| `title` | `string` | `''` | | Header title |
|
|
33
33
|
| `divider` | `boolean` | `false` | | Rule between header and body. Off by default — many rules make a screen noisy |
|
|
34
34
|
| `flush` | `boolean` | `false` | | Remove body padding, for tables and lists that draw their own edges |
|
|
35
|
+
| `level` | `2|3|4|5|6` | `3` | ✓ | Heading level of the title in the document outline. Set `2` for a box directly under `u-page-header` (the page's `h1`), so the outline does not skip a level. Semantics only — the title looks the same at every level |
|
|
35
36
|
|
|
36
37
|
## CSS Parts
|
|
37
38
|
|
|
@@ -33,26 +33,52 @@ and those are different states of the business. So the component owns the rule.
|
|
|
33
33
|
| `label` | `string` | `''` | | Field label |
|
|
34
34
|
| `value` | `unknown` | — | | Value; property only (`attribute: false`) |
|
|
35
35
|
| `blank` | `string` | `'—'` | | Placeholder for null/undefined/empty — **not** for `0` or `false` |
|
|
36
|
-
| `numeric` | `boolean` | `false` | |
|
|
36
|
+
| `numeric` | `boolean` | `false` | | Tabular figures, so digits keep their width as the value changes; implied by `format="number"`/`"currency"`. Does **not** change alignment |
|
|
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
|
+
| `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 |
|
|
39
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
41
|
| `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 |
|
|
41
42
|
| `trendLabel` | `string` | — | | Trend copy, e.g. `"+12% vs last month"` — wording is the consumer's responsibility |
|
|
42
43
|
| `tone` | `'positive'\|'negative'\|'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 |
|
|
43
44
|
|
|
44
|
-
⚠ `numeric`
|
|
45
|
-
|
|
45
|
+
⚠ `numeric` no longer right-aligns (it did before 0.25.0). A field is a label/value pair that owns
|
|
46
|
+
one grid cell, with no neighbouring figures to line up, so right-aligning only pushed the value
|
|
47
|
+
to the far side of the cell from its label — most visibly in `size="lg"` KPI tiles, and
|
|
48
|
+
inconsistently, because `format="currency"` implied it while a plain count did not. If a layout
|
|
49
|
+
genuinely needs right-aligned figures, style the part: `u-info-field::part(value) { text-align: end; }`.
|
|
46
50
|
|
|
47
51
|
```html
|
|
48
52
|
<u-info-field label="Total" format="currency" currency="KRW" .value=${order.total}></u-info-field>
|
|
49
53
|
```
|
|
50
54
|
|
|
55
|
+
### Units and KPI tiles
|
|
56
|
+
|
|
57
|
+
A KPI figure almost always carries a unit. Put it in `unit` rather than in the value: the value
|
|
58
|
+
is drawn at the `title` step in `size="lg"`, and a unit written into the value (or the slot) is
|
|
59
|
+
drawn that large and that bold too — undoing it by hand means restating size, weight and color at
|
|
60
|
+
every tile.
|
|
61
|
+
|
|
62
|
+
```html
|
|
63
|
+
<u-info-field label="Open work orders" size="lg" .value=${openCount} unit="건"></u-info-field>
|
|
64
|
+
<u-info-field label="Availability" size="lg" format="number" .value=${98.2} unit="%"></u-info-field>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
At `size="lg"` a blank value is drawn at body size, not the title step, so `blank` can carry a
|
|
68
|
+
short reason when a figure cannot be computed — the row keeps its height, so tiles in one strip
|
|
69
|
+
stay level:
|
|
70
|
+
|
|
71
|
+
```html
|
|
72
|
+
<u-info-field label="MTTR" size="lg" .value=${mttr} unit="h"
|
|
73
|
+
blank="No downtime recorded for the failures"></u-info-field>
|
|
74
|
+
```
|
|
75
|
+
|
|
51
76
|
## CSS Parts
|
|
52
77
|
|
|
53
78
|
| Part | Description |
|
|
54
79
|
|------|-------------|
|
|
55
80
|
| `label` · `value` · `trend` | The label, the value, and the (optional) trend indicator |
|
|
81
|
+
| `unit` | The unit, inside `value` (only when `unit` is set and the value is not blank) |
|
|
56
82
|
|
|
57
83
|
## CSS Custom Properties
|
|
58
84
|
|
|
@@ -66,5 +92,6 @@ lone field it just pushes the value away from its label.
|
|
|
66
92
|
| `--u-text-body-weight` | Value font weight when `blank` |
|
|
67
93
|
| `--u-txt-color` | Value color |
|
|
68
94
|
| `--u-text-title-size` / `-weight` | Value typography when `size="lg"` |
|
|
95
|
+
| `--u-text-label-size` | Unit size |
|
|
69
96
|
| `--u-success-color-strong` | `tone="positive"` color (value and trend) |
|
|
70
97
|
| `--u-danger-color-strong` | `tone="negative"` color (value and trend) |
|