@iyulab/modern-app 0.24.0 → 0.26.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 +82 -0
- package/README.md +24 -0
- package/dist/components/EmptyState.d.ts +3 -3
- 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/components/MasterDetailLayout.d.ts +1 -1
- package/dist/components/PageHeader.d.ts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +12 -12
- package/dist/internals/locale.d.ts +16 -27
- package/dist/internals/locale.js +47 -19
- package/dist/layouts/SidebarLayout.d.ts +11 -1
- package/dist/layouts/SidebarLayout.js +2 -2
- package/package.json +2 -2
- package/skills/modern-app/SKILL.md +24 -0
- package/skills/modern-app/references/components/empty-state.md +3 -2
- package/skills/modern-app/references/components/group-box.md +1 -0
- package/skills/modern-app/references/components/info-field.md +34 -3
- package/skills/modern-app/references/components/page-header.md +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,87 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.26.0] - 2026-09-23
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- 🔴 **Built-in strings follow `Locale` of `@iyulab/components` — the language is chosen once, with
|
|
8
|
+
`Locale.set()`.** This package kept a registry of its own (`registerLocale`/`setDefaultLocale`)
|
|
9
|
+
while the primitives and `@iyulab/flex-table` follow `Locale`, so an app that set the language
|
|
10
|
+
once got the shell's `Toggle sidebar`/`Toggle menu`/`Close` labels, the back link and empty-state
|
|
11
|
+
copy in English in the middle of a translated screen — silently. The strings now live in a
|
|
12
|
+
`Locale.namespace('modern-app')`, and resolve through the same chain as the primitives (so
|
|
13
|
+
`lang="zh"` reaches a `zh-CN` table). An element's own `locale` attribute still overrides the
|
|
14
|
+
active language for that element.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`modernAppLocale`** — the namespace handle, for translations:
|
|
19
|
+
`modernAppLocale.register('ko', { back: '뒤로' })`, the same shape as `flexTableLocale.register`.
|
|
20
|
+
`wizardStepAnnouncement` is a template there (`'{total}단계 중 {index}단계: {label}'`).
|
|
21
|
+
**`ModernAppMessageKey`** is its key type.
|
|
22
|
+
|
|
23
|
+
### Deprecated
|
|
24
|
+
|
|
25
|
+
- **`registerLocale` and `setDefaultLocale`.** Both keep working: `registerLocale` forwards to
|
|
26
|
+
the namespace (a function-valued `wizardStepAnnouncement` still works), and a `setDefaultLocale()`
|
|
27
|
+
call still takes precedence over `Locale` until it is called with `undefined` — an app that
|
|
28
|
+
calls both with the same language sees no change and can drop `setDefaultLocale` at its leisure.
|
|
29
|
+
|
|
30
|
+
⚠ **Requires `@iyulab/components` 1.45.0** (for `LocaleNamespace.textIn`, which resolves an
|
|
31
|
+
element's own `locale`).
|
|
32
|
+
|
|
33
|
+
## [0.25.0] - 2026-09-23
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- **`u-info-field` takes a `unit` — `'건'`, `'%'`, `'h'` — drawn after the value one step below
|
|
38
|
+
it: label size, body weight, weak color.** A KPI figure almost always carries a unit, and
|
|
39
|
+
written into the value it was drawn at the `size="lg"` title step, as large and as bold as the
|
|
40
|
+
number; the only way back was restating size, weight and color by hand at every tile, and a tile
|
|
41
|
+
that missed one showed a unit as big as its figure. The unit is hidden while the value is blank
|
|
42
|
+
(`— 건` is not a reading) and follows slotted value content too. Exposed as `::part(unit)`.
|
|
43
|
+
- **`u-group-box` takes a `level` (`2`–`6`, default `3`) for its title's place in the document
|
|
44
|
+
outline.** The title was always an `h3`. A box placed directly under `u-page-header` (the
|
|
45
|
+
page's `h1`) — the usual spot on a detail screen — skipped a level and sat deeper than its sibling
|
|
46
|
+
sections, which screen-reader heading navigation exposes. The box cannot know its own depth, so
|
|
47
|
+
the page that composes it says so: `<u-group-box level="2">`. The level changes semantics only;
|
|
48
|
+
the title looks the same at every level. The default is unchanged.
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
|
|
52
|
+
- 🔴 **`SidebarLayout`'s toggle follows the screen size it was told about, with or without
|
|
53
|
+
`app.load()`.** Size *changes* arrived as `screen-resize` events, but the toggle read the
|
|
54
|
+
`app.screen` singleton, which only exists when `app.load()` created the observer. Assembling the
|
|
55
|
+
shell yourself — as the `/react` entry point does, with `SidebarLayout` and `ScreenObserver` —
|
|
56
|
+
left it empty, so it fell back to `'large'`: on a phone-width screen, opening the menu switched to
|
|
57
|
+
the desktop sidebar squeezed beside the content instead of the mobile overlay, and the close glyph
|
|
58
|
+
never appeared. The layout now remembers the last size it received; `app.screen` remains a
|
|
59
|
+
fallback before the first event.
|
|
60
|
+
|
|
61
|
+
### Changed
|
|
62
|
+
|
|
63
|
+
- **At `size="lg"`, a blank value is drawn at body size rather than the title step**, so `blank`
|
|
64
|
+
can carry a short reason when a figure cannot be computed. The value row keeps its height, so
|
|
65
|
+
tiles in one strip stay level.
|
|
66
|
+
|
|
67
|
+
- 🔴 **`u-info-field`'s `numeric` no longer right-aligns the value — it only sets tabular
|
|
68
|
+
figures.** The same applies to `format="number"` and `format="currency"`, which imply it. A
|
|
69
|
+
field is a label/value pair that owns one `u-info-section` grid cell; there are no
|
|
70
|
+
neighbouring figures in that cell to line up, so right-aligning only pushed the value to the far
|
|
71
|
+
side of the cell from its label. It was most visible in `size="lg"` KPI tiles — label at the
|
|
72
|
+
top left, value at the bottom right — and it was inconsistent: in one tile strip a currency
|
|
73
|
+
figure sat on the right while a plain count sat on the left. Tabular figures stay, so digits
|
|
74
|
+
keep their width as a value changes.
|
|
75
|
+
|
|
76
|
+
⚠ **Visible change:** numeric values in info fields now start under their label. If a layout of
|
|
77
|
+
yours genuinely needs right-aligned figures, style the part:
|
|
78
|
+
`u-info-field::part(value) { text-align: end; }`.
|
|
79
|
+
|
|
80
|
+
### Documentation
|
|
81
|
+
|
|
82
|
+
- The `u-info-field` reference describes `numeric` as tabular figures only, says why it no longer
|
|
83
|
+
aligns, and gives the `::part(value)` rule for layouts that need it.
|
|
84
|
+
|
|
3
85
|
## [0.24.0] - 2026-09-22
|
|
4
86
|
|
|
5
87
|
### Fixed
|
package/README.md
CHANGED
|
@@ -118,6 +118,30 @@ import { translate } from '@iyulab/modern-app';
|
|
|
118
118
|
html`<p>${translate('common::greeting')}</p>`;
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
+
### Built-in strings
|
|
122
|
+
|
|
123
|
+
The package's own strings — the page header's back link, empty-state copy, the shell's toggle and
|
|
124
|
+
close labels, the wizard's buttons and step announcement — are English by default and live in the
|
|
125
|
+
`modern-app` namespace of `@iyulab/components`' `Locale`. Register translations there and pick the
|
|
126
|
+
language once, with `Locale.set()`; the primitives and `@iyulab/flex-table` follow the same call:
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
import { Locale } from '@iyulab/components';
|
|
130
|
+
import { modernAppLocale } from '@iyulab/modern-app';
|
|
131
|
+
|
|
132
|
+
modernAppLocale.register('ko', {
|
|
133
|
+
back: '뒤로',
|
|
134
|
+
toggleSidebar: '사이드바 접기/펼치기',
|
|
135
|
+
wizardStepAnnouncement: '{total}단계 중 {index}단계: {label}',
|
|
136
|
+
});
|
|
137
|
+
Locale.set('ko');
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
A partial table is enough — untranslated keys fall back to English. An element's own `locale`
|
|
141
|
+
attribute overrides the active language for that element. `registerLocale`/`setDefaultLocale` still
|
|
142
|
+
work but are deprecated: a `setDefaultLocale()` call takes precedence over `Locale` until it is
|
|
143
|
+
called with `undefined`.
|
|
144
|
+
|
|
121
145
|
### 부팅 인증 게이트 (`auth`)
|
|
122
146
|
|
|
123
147
|
셸을 만들기 전에 세션을 판정한다. 소비앱이 `app.load()` 앞단에 손으로 짜던 "me 조회 → 미인증이면 로그인, 인증이면 앱 로드" 게이트를 표준화한다. 세션 조회/로그인 HTTP 는 `@iyulab/enterprise` 의 `createAuthClient` 가, 세션-중 401 은 `createODataService` 의 `onUnauthorized` 가 담당한다(프레임워크는 오케스트레이션만 소유).
|
|
@@ -19,7 +19,7 @@ type ElementParts = 'host' | 'icon' | 'title' | 'description' | 'actions';
|
|
|
19
19
|
* ```
|
|
20
20
|
*
|
|
21
21
|
* ⚠**기본 문구는 영어다** — 이 패키지는 범용 층이라 특정 언어를 기본값으로 가질 수 없다.
|
|
22
|
-
* 한국어는 `
|
|
22
|
+
* 한국어는 `modernAppLocale.register('ko', …)` 로 소비자가 등록하고(언어는 `Locale.set()`), 화면별로 덮으려면
|
|
23
23
|
* `title`·`description` 을 준다.
|
|
24
24
|
*
|
|
25
25
|
* ★이 줄은 «한국어다»라고 적혀 있었고 **같은 파일의 `locale` 프로퍼티 주석이 «영어다»라고
|
|
@@ -35,9 +35,9 @@ export declare class EmptyState extends StyledElement<ElementParts> {
|
|
|
35
35
|
/** 보조 설명. 비우면 variant 기본 문구. */
|
|
36
36
|
description: string;
|
|
37
37
|
/**
|
|
38
|
-
* 언어
|
|
38
|
+
* 언어 태그 — 이 요소만 문서와 다른 언어로 쓸 때. 비우면 `Locale` 의 활성 로케일.
|
|
39
39
|
* ⚠기본 문구는 영어다 — 이 패키지는 범용 층이라 특정 언어를 기본값으로 가질 수 없다.
|
|
40
|
-
* 한국어는 `
|
|
40
|
+
* 한국어는 `modernAppLocale.register('ko', …)` 로 소비자가 등록한다.
|
|
41
41
|
*/
|
|
42
42
|
locale: string;
|
|
43
43
|
/** 액션 슬롯 배정 상태 — CSS `:has()` 로는 알 수 없다(`internals/slotted.ts` 참조). */
|
|
@@ -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은 무규칙 — 기본
|
|
@@ -47,7 +47,7 @@ export declare class MasterDetailLayout extends StyledElement<ElementParts> {
|
|
|
47
47
|
*/
|
|
48
48
|
overlayBreakpoint: number;
|
|
49
49
|
/**
|
|
50
|
-
* 언어
|
|
50
|
+
* 언어 태그 — 이 요소만 문서와 다른 언어로 쓸 때. 비우면 `Locale` 의 활성 로케일.
|
|
51
51
|
* 닫기 버튼의 접근성 라벨에만 쓰인다.
|
|
52
52
|
*/
|
|
53
53
|
locale: string;
|
|
@@ -33,10 +33,10 @@ export declare class PageHeader extends StyledElement<ElementParts> {
|
|
|
33
33
|
/**
|
|
34
34
|
* 뒤로가기 링크 문구. 비우면 locale 레지스트리 값, 그것도 없으면 **영어**(`Back`).
|
|
35
35
|
* ⚠이 패키지는 범용 층이라 특정 언어를 기본값으로 가질 수 없다 —
|
|
36
|
-
* 한국어는 `
|
|
36
|
+
* 한국어는 `modernAppLocale.register('ko', { back: '뒤로' })` 로 소비자가 등록한다.
|
|
37
37
|
*/
|
|
38
38
|
backLabel: string;
|
|
39
|
-
/** 언어
|
|
39
|
+
/** 언어 태그 — 이 요소만 문서와 다른 언어로 쓸 때. 비우면 `Locale` 의 활성 로케일. */
|
|
40
40
|
locale: string;
|
|
41
41
|
/**
|
|
42
42
|
* 슬롯 배정 상태. ★CSS 로는 알 수 없다 — `<slot>` 자신이 자식이라 `:has(*)` 가
|
package/dist/index.d.ts
CHANGED
|
@@ -15,8 +15,8 @@ export { MasterDetailLayout } from './components/MasterDetailLayout.js';
|
|
|
15
15
|
export { Wizard } from './components/Wizard.js';
|
|
16
16
|
export type { WizardStep, WizardStepState, WizardStepChangeDetail } from './components/Wizard.js';
|
|
17
17
|
export { translate } from './translate.js';
|
|
18
|
-
export { registerLocale, setDefaultLocale, getLocaleStrings, getDefaultLocale, } from './internals/locale.js';
|
|
19
|
-
export type { ModernAppLocaleStrings } from './internals/locale.js';
|
|
18
|
+
export { modernAppLocale, registerLocale, setDefaultLocale, getLocaleStrings, getDefaultLocale, } from './internals/locale.js';
|
|
19
|
+
export type { ModernAppLocaleStrings, ModernAppMessageKey } from './internals/locale.js';
|
|
20
20
|
export { ScreenObserver } from './internals/ScreenObserver.js';
|
|
21
21
|
export type { ScreenSize, ScreenObserverConfig, ScreenResizeEvent } from './internals/ScreenObserver.js';
|
|
22
22
|
export { app };
|
package/dist/index.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { ScreenObserver as e } from "./internals/ScreenObserver.js";
|
|
2
2
|
import { app as t } from "./App.js";
|
|
3
3
|
import { filterSidebarItems as n } from "./layouts/filterSidebarItems.js";
|
|
4
|
-
import { getDefaultLocale as r, getLocaleStrings as i,
|
|
5
|
-
import { PageHeader as
|
|
6
|
-
import { GroupBox as
|
|
7
|
-
import { InfoSection as
|
|
8
|
-
import { InfoField as
|
|
9
|
-
import { EmptyState as
|
|
10
|
-
import { ActionBar as
|
|
11
|
-
import { MasterDetailLayout as
|
|
12
|
-
import { Wizard as
|
|
13
|
-
import { translate as
|
|
4
|
+
import { getDefaultLocale as r, getLocaleStrings as i, modernAppLocale as a, registerLocale as o, setDefaultLocale as s } from "./internals/locale.js";
|
|
5
|
+
import { PageHeader as c } from "./components/PageHeader.js";
|
|
6
|
+
import { GroupBox as l } from "./components/GroupBox.js";
|
|
7
|
+
import { InfoSection as u } from "./components/InfoSection.js";
|
|
8
|
+
import { InfoField as d, isBlank as f } from "./components/InfoField.js";
|
|
9
|
+
import { EmptyState as p } from "./components/EmptyState.js";
|
|
10
|
+
import { ActionBar as m } from "./components/ActionBar.js";
|
|
11
|
+
import { MasterDetailLayout as h } from "./components/MasterDetailLayout.js";
|
|
12
|
+
import { Wizard as g } from "./components/Wizard.js";
|
|
13
|
+
import { translate as _ } from "./translate.js";
|
|
14
14
|
//#region src/index.ts
|
|
15
|
-
var
|
|
15
|
+
var v = t;
|
|
16
16
|
//#endregion
|
|
17
|
-
export {
|
|
17
|
+
export { m as ActionBar, p as EmptyState, l as GroupBox, d as InfoField, u as InfoSection, h as MasterDetailLayout, c as PageHeader, e as ScreenObserver, g as Wizard, t as app, v as default, n as filterSidebarItems, r as getDefaultLocale, i as getLocaleStrings, f as isBlank, a as modernAppLocale, o as registerLocale, s as setDefaultLocale, _ as translate };
|
|
@@ -1,22 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lightweight locale registry for modern-app chrome strings.
|
|
3
|
-
*
|
|
4
|
-
* Scope: library-internal chrome strings only (empty-state copy, back label).
|
|
5
|
-
* NOT a general i18n framework — the app's own content is the app's business.
|
|
6
|
-
*
|
|
7
|
-
* ★**English is the built-in default.** This package sits in the *generic* layer
|
|
8
|
-
* (see the three-layer split: `components` = neutral primitives, `modern-app` =
|
|
9
|
-
* what a modern app usually provides, `enterprise` = iyulab house style).
|
|
10
|
-
* A generic layer that ships Korean defaults excludes every non-Korean consumer —
|
|
11
|
-
* the same objection this repo raised against baking a house aesthetic into
|
|
12
|
-
* `components`. Language excludes harder than aesthetics: an odd-looking default
|
|
13
|
-
* is awkward, an unreadable one is unusable.
|
|
14
|
-
*
|
|
15
|
-
* Mirrors the registry already established in `u-widgets` — one pattern, not two.
|
|
16
|
-
*
|
|
17
|
-
* registerLocale('ko', { back: '뒤로', noDataTitle: '아직 등록된 항목이 없습니다' });
|
|
18
|
-
* setDefaultLocale('ko');
|
|
19
|
-
*/
|
|
20
1
|
export interface ModernAppLocaleStrings {
|
|
21
2
|
/** Back button label in the page header. */
|
|
22
3
|
back: string;
|
|
@@ -39,18 +20,26 @@ export interface ModernAppLocaleStrings {
|
|
|
39
20
|
wizardStepAnnouncement: (index: number, total: number, label: string) => string;
|
|
40
21
|
}
|
|
41
22
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
23
|
+
* Keys of the `modern-app` namespace. All strings — `wizardStepAnnouncement` is a template with
|
|
24
|
+
* `{index}`, `{total}` and `{label}` placeholders (`'Step {index} of {total}: {label}'`).
|
|
25
|
+
*/
|
|
26
|
+
export type ModernAppMessageKey = keyof ModernAppLocaleStrings;
|
|
27
|
+
/** The `modern-app` chrome-string namespace. Register translations here; pick the language with `Locale.set()`. */
|
|
28
|
+
export declare const modernAppLocale: import('@iyulab/components/dist/utilities/Locale.js').LocaleNamespace<keyof ModernAppLocaleStrings>;
|
|
29
|
+
/**
|
|
30
|
+
* @deprecated Register with `modernAppLocale.register(lang, strings)` and choose the language with
|
|
31
|
+
* `Locale.set()`. This forwards to the namespace; a function-valued `wizardStepAnnouncement` keeps
|
|
32
|
+
* working (the namespace form is a `{index}`/`{total}`/`{label}` template).
|
|
44
33
|
*/
|
|
45
34
|
export declare function registerLocale(lang: string, strings: Partial<ModernAppLocaleStrings>): void;
|
|
46
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* @deprecated Choose the language with `Locale.set()` of `@iyulab/components` — this package now
|
|
37
|
+
* follows it. When called, this still takes precedence over `Locale` for elements without their
|
|
38
|
+
* own `locale`; `setDefaultLocale(undefined)` hands the choice back to `Locale`.
|
|
39
|
+
*/
|
|
47
40
|
export declare function setDefaultLocale(lang: string | undefined): void;
|
|
48
41
|
/**
|
|
49
|
-
* Resolve
|
|
50
|
-
*
|
|
51
|
-
* Order: exact match (`ko-KR`) → base language (`ko`) → English.
|
|
52
|
-
* ⚠The base-language step matters: consumers register `ko` but browsers report
|
|
53
|
-
* `ko-KR`, and without it every registration would silently miss.
|
|
42
|
+
* Resolve every chrome string for a language tag (empty → `setDefaultLocale` → active `Locale`).
|
|
54
43
|
*/
|
|
55
44
|
export declare function getLocaleStrings(lang?: string): ModernAppLocaleStrings;
|
|
56
45
|
/** The English defaults — for tests and reference. */
|
package/dist/internals/locale.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { Locale as e } from "@iyulab/components/dist/utilities/Locale.js";
|
|
1
2
|
//#region src/internals/locale.ts
|
|
2
|
-
var
|
|
3
|
+
var t = {
|
|
3
4
|
back: "Back",
|
|
4
5
|
noDataTitle: "Nothing here yet",
|
|
5
6
|
noDataDescription: "",
|
|
@@ -10,26 +11,53 @@ var e = {
|
|
|
10
11
|
toggleSidebar: "Toggle sidebar",
|
|
11
12
|
wizardBack: "Back",
|
|
12
13
|
wizardNext: "Next",
|
|
13
|
-
wizardStepAnnouncement:
|
|
14
|
-
},
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
wizardStepAnnouncement: "Step {index} of {total}: {label}"
|
|
15
|
+
}, n = e.namespace("modern-app");
|
|
16
|
+
n.register("en", t);
|
|
17
|
+
var r, i = /* @__PURE__ */ new Map(), a = "\0modern-app:announcer:";
|
|
18
|
+
function o(e, t) {
|
|
19
|
+
let { wizardStepAnnouncement: r, ...o } = t, s = { ...o };
|
|
20
|
+
if (r) {
|
|
21
|
+
let t = e.toLowerCase();
|
|
22
|
+
i.set(t, r), s.wizardStepAnnouncement = a + t;
|
|
23
|
+
}
|
|
24
|
+
n.register(e, s);
|
|
20
25
|
}
|
|
21
|
-
function
|
|
22
|
-
|
|
26
|
+
function s(e) {
|
|
27
|
+
r = e;
|
|
23
28
|
}
|
|
24
|
-
function
|
|
25
|
-
let
|
|
26
|
-
|
|
27
|
-
if (t.has(i)) return t.get(i);
|
|
28
|
-
let a = i.split("-")[0];
|
|
29
|
-
return a !== i && t.has(a) ? t.get(a) : e;
|
|
29
|
+
function c(e, t, i) {
|
|
30
|
+
let a = n.textIn(e || r, t, i);
|
|
31
|
+
return a === t ? "" : a;
|
|
30
32
|
}
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
+
function l(e) {
|
|
34
|
+
let t = (t) => c(e, t);
|
|
35
|
+
return {
|
|
36
|
+
back: t("back"),
|
|
37
|
+
noDataTitle: t("noDataTitle"),
|
|
38
|
+
noDataDescription: t("noDataDescription"),
|
|
39
|
+
noResultsTitle: t("noResultsTitle"),
|
|
40
|
+
noResultsDescription: t("noResultsDescription"),
|
|
41
|
+
detailClose: t("detailClose"),
|
|
42
|
+
toggleMobileMenu: t("toggleMobileMenu"),
|
|
43
|
+
toggleSidebar: t("toggleSidebar"),
|
|
44
|
+
wizardBack: t("wizardBack"),
|
|
45
|
+
wizardNext: t("wizardNext"),
|
|
46
|
+
wizardStepAnnouncement: (t, n, r) => {
|
|
47
|
+
let o = c(e, "wizardStepAnnouncement", {
|
|
48
|
+
index: t,
|
|
49
|
+
total: n,
|
|
50
|
+
label: r
|
|
51
|
+
});
|
|
52
|
+
return o.startsWith(a) ? i.get(o.slice(22))(t, n, r) : o;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function u() {
|
|
57
|
+
return {
|
|
58
|
+
...t,
|
|
59
|
+
wizardStepAnnouncement: (e, t, n) => `Step ${e} of ${t}: ${n}`
|
|
60
|
+
};
|
|
33
61
|
}
|
|
34
62
|
//#endregion
|
|
35
|
-
export {
|
|
63
|
+
export { u as getDefaultLocale, l as getLocaleStrings, n as modernAppLocale, o as registerLocale, s as setDefaultLocale };
|
|
@@ -9,7 +9,7 @@ export declare class SidebarLayout extends StyledElement<SidebarParts> {
|
|
|
9
9
|
state: SidebarState;
|
|
10
10
|
/** 사이드바 레이아웃 설정 */
|
|
11
11
|
config?: SidebarLayoutConfig;
|
|
12
|
-
/** 크롬 문자열(토글 버튼 접근성 라벨 등) 로케일 — `
|
|
12
|
+
/** 크롬 문자열(토글 버튼 접근성 라벨 등) 로케일 — 이 요소만 다른 언어로 쓸 때. 비우면 `Locale` 의 활성 로케일 */
|
|
13
13
|
locale: string;
|
|
14
14
|
progressBarEl: UProgressBar;
|
|
15
15
|
/** overlay 슬롯 배정 상태 — CSS `:has()`로는 알 수 없다(`internals/slotted.ts` 참조). */
|
|
@@ -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
|
@@ -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.
|
|
4
|
+
"version": "0.26.0",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"iyulab",
|
|
7
7
|
"web-framework",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"lit": "^3.3.3"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
|
-
"@iyulab/components": ">=1.
|
|
67
|
+
"@iyulab/components": ">=1.45.0",
|
|
68
68
|
"@lit/react": ">=1.0.8",
|
|
69
69
|
"react": ">=18.0.0"
|
|
70
70
|
},
|
|
@@ -141,6 +141,30 @@ html`<p>${translate('namespace::greeting')}</p>`;
|
|
|
141
141
|
// layout: { main: [{ type: 'link', label: translate('nav::home'), href: '/' }] }
|
|
142
142
|
```
|
|
143
143
|
|
|
144
|
+
### Built-in strings (`modernAppLocale`)
|
|
145
|
+
|
|
146
|
+
The package's own strings — the page header's back link, empty-state copy, the shell's toggle and
|
|
147
|
+
close labels, the wizard's buttons and step announcement — are English by default and live in the
|
|
148
|
+
`modern-app` namespace of `@iyulab/components`' `Locale`. Register translations there and pick the
|
|
149
|
+
language once, with `Locale.set()`; the primitives and `@iyulab/flex-table` follow the same call:
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
import { Locale } from '@iyulab/components';
|
|
153
|
+
import { modernAppLocale } from '@iyulab/modern-app';
|
|
154
|
+
|
|
155
|
+
modernAppLocale.register('ko', {
|
|
156
|
+
back: '뒤로',
|
|
157
|
+
toggleSidebar: '사이드바 접기/펼치기',
|
|
158
|
+
wizardStepAnnouncement: '{total}단계 중 {index}단계: {label}',
|
|
159
|
+
});
|
|
160
|
+
Locale.set('ko');
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
A partial table is enough — untranslated keys fall back to English. An element's own `locale`
|
|
164
|
+
attribute overrides the active language for that element. `registerLocale`/`setDefaultLocale` still
|
|
165
|
+
work but are deprecated: a `setDefaultLocale()` call takes precedence over `Locale` until it is
|
|
166
|
+
called with `undefined`.
|
|
167
|
+
|
|
144
168
|
---
|
|
145
169
|
|
|
146
170
|
## Sidebar Layout
|
|
@@ -54,5 +54,6 @@ for both and a user with a filter still applied reads it as *"my data disappeare
|
|
|
54
54
|
| `--u-space-lg` | Actions row top spacing |
|
|
55
55
|
| `--u-space-sm` | Gap between actions |
|
|
56
56
|
|
|
57
|
-
⚠ Default wording is **English** — register others with `
|
|
58
|
-
`title`/`description` per
|
|
57
|
+
⚠ Default wording is **English** — register others with `modernAppLocale.register('ko', …)` and
|
|
58
|
+
choose the language with `Locale.set()` of `@iyulab/components`, or pass `title`/`description` per
|
|
59
|
+
screen.
|
|
@@ -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,56 @@ 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
|
+
|
|
76
|
+
`tone` colors a blank value too, so a reason that needs attention takes `tone="negative"`. Set it
|
|
77
|
+
conditionally (`tone=${mttr == null ? 'negative' : undefined}`) — otherwise the value is toned
|
|
78
|
+
as well once it exists.
|
|
79
|
+
|
|
51
80
|
## CSS Parts
|
|
52
81
|
|
|
53
82
|
| Part | Description |
|
|
54
83
|
|------|-------------|
|
|
55
84
|
| `label` · `value` · `trend` | The label, the value, and the (optional) trend indicator |
|
|
85
|
+
| `unit` | The unit, inside `value` (only when `unit` is set and the value is not blank) |
|
|
56
86
|
|
|
57
87
|
## CSS Custom Properties
|
|
58
88
|
|
|
@@ -66,5 +96,6 @@ lone field it just pushes the value away from its label.
|
|
|
66
96
|
| `--u-text-body-weight` | Value font weight when `blank` |
|
|
67
97
|
| `--u-txt-color` | Value color |
|
|
68
98
|
| `--u-text-title-size` / `-weight` | Value typography when `size="lg"` |
|
|
99
|
+
| `--u-text-label-size` | Unit size |
|
|
69
100
|
| `--u-success-color-strong` | `tone="positive"` color (value and trend) |
|
|
70
101
|
| `--u-danger-color-strong` | `tone="negative"` color (value and trend) |
|
|
@@ -59,4 +59,5 @@ screens nobody opens.
|
|
|
59
59
|
| `--u-space-3xs` | Back-arrow-to-label spacing |
|
|
60
60
|
|
|
61
61
|
⚠ Built-in strings default to **English** — this package is a generic layer and cannot pick a
|
|
62
|
-
language. Register others with `
|
|
62
|
+
language. Register others with `modernAppLocale.register('ko', { back: '뒤로' })` and choose the
|
|
63
|
+
language with `Locale.set()` of `@iyulab/components`.
|