@iyulab/modern-app 0.53.0 → 0.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.54.0] - 2026-10-14
4
+
5
+ ### Added
6
+
7
+ - **`u-action-bar` `summary` slot** — what the main action will do («1 change · Order number»). It follows the
8
+ danger actions and fills the free width; below 420px it takes its own line between the main actions (on top) and
9
+ the danger actions. Label-size, weak text.
10
+ - `--action-bar-padding-inline` — inline padding for a bar that runs to a side sheet's edges (default `0`).
11
+ - **`u-info-section layout="strip"` — a stat strip.** The totals of a result as cells on one bordered surface, split
12
+ by 1px dividers: all side by side, or all stacked once the strip is narrower than 640px — never three plus one.
13
+ - `u-info-field` `description` — a supporting line below the value (its basis, or the same amount in another unit).
14
+
15
+ ### Fixed
16
+
17
+ - The `u-action-bar` skill reference placed the main actions at the leading edge and the danger actions at the
18
+ trailing edge — the reverse of what the bar does. It now describes the actual layout.
19
+
20
+ ## [0.53.1] - 2026-10-13
21
+
22
+ ### Fixed
23
+
24
+ - **Group headers and sidebar buttons follow the navigation item tokens.** `--app-sidebar-item-gap`,
25
+ `--app-sidebar-item-padding` and `--app-sidebar-item-radius` were read by links only, and `--app-sidebar-icon-size` by
26
+ links and group headers — a theme that set them got three different label starts in one sidebar (links, group
27
+ headers, and footer buttons such as a theme toggle). All three item kinds now read all four. Without the tokens the
28
+ defaults are unchanged.
29
+
30
+ ### Documentation
31
+
32
+ - The unsaved-changes examples are complete snippets for Lit and React, and hold `UnsavedChangesController` in a
33
+ `readonly` field (a `private` one is flagged by `noUnusedLocals`).
34
+
3
35
  ## [0.53.0] - 2026-10-13
4
36
 
5
37
  ### Added
@@ -1,5 +1,5 @@
1
1
  import { StyledElement } from '../internals/StyledElement.js';
2
- type ElementParts = 'host' | 'danger' | 'main';
2
+ type ElementParts = 'host' | 'danger' | 'summary' | 'main';
3
3
  /**
4
4
  * 푸터 액션 바 — 상세·편집 화면 맨 아래의 같은 골격.
5
5
  *
@@ -17,10 +17,13 @@ type ElementParts = 'host' | 'danger' | 'main';
17
17
  * ```
18
18
  *
19
19
  * 배치 규칙: **위험 액션은 왼쪽 끝, 주 액션은 오른쪽 끝** — 거리가 안전장치다.
20
- * 좁아지면 두 무리가 각자 한 줄을 갖고 **주 액션이 위로** 온다(엄지 위치에 위험 액션을
20
+ * 좁아지면 무리마다 한 줄을 갖고 **주 액션이 위로** 온다(엄지 위치에 위험 액션을
21
21
  * 두지 않기 위해서다).
22
22
  *
23
- * 오버라이드: `part`(host·danger·main) + slot 치환.
23
+ * `summary` 슬롯은 주 액션이 무엇을 할지 말하는 글이다(«변경 1건 · 주문 번호») — 위험 액션 뒤에서
24
+ * 남는 폭을 채운다. 시트 가장자리에 붙는 바닥이면 `--action-bar-padding-inline` 으로 안쪽 여백을 준다.
25
+ *
26
+ * 오버라이드: `part`(host·danger·summary·main) + slot 치환.
24
27
  */
25
28
  export declare class ActionBar extends StyledElement<ElementParts> {
26
29
  static styles: import('lit').CSSResultGroup[];
@@ -35,6 +38,8 @@ export declare class ActionBar extends StyledElement<ElementParts> {
35
38
  * (`internals/slotted.ts` 참조). 빈 래퍼가 남으면 주 액션이 가운데로 밀린다.
36
39
  */
37
40
  private hasDanger;
41
+ /** 요약 슬롯 배정 상태 — 비면 래퍼를 접는다(위와 같은 이유). */
42
+ private hasSummary;
38
43
  render(): import('lit-html').TemplateResult<1>;
39
44
  }
40
45
  declare global {
@@ -8,7 +8,7 @@ import { customElement as o, property as s, state as c } from "lit/decorators.js
8
8
  //#region src/components/ActionBar.ts
9
9
  var l = class extends n {
10
10
  constructor(...e) {
11
- super(...e), this.sticky = !1, this.hasDanger = !1;
11
+ super(...e), this.sticky = !1, this.hasDanger = !1, this.hasSummary = !1;
12
12
  }
13
13
  static {
14
14
  this.styles = [super.styles, i];
@@ -20,6 +20,11 @@ var l = class extends n {
20
20
  @slotchange=${(e) => this.hasDanger = r(e.target)}
21
21
  ></slot>
22
22
  </div>
23
+ <div class="summary ${this.hasSummary ? "" : "empty"}" part="summary">
24
+ <slot name="summary"
25
+ @slotchange=${(e) => this.hasSummary = r(e.target)}
26
+ ></slot>
27
+ </div>
23
28
  <div class="main" part="main"><slot></slot></div>
24
29
  `;
25
30
  }
@@ -27,6 +32,6 @@ var l = class extends n {
27
32
  t([s({
28
33
  type: Boolean,
29
34
  reflect: !0
30
- }), e("design:type", Object)], l.prototype, "sticky", void 0), t([c(), e("design:type", Object)], l.prototype, "hasDanger", void 0), l = t([o("u-action-bar")], l);
35
+ }), e("design:type", Object)], l.prototype, "sticky", void 0), t([c(), e("design:type", Object)], l.prototype, "hasDanger", void 0), t([c(), e("design:type", Object)], l.prototype, "hasSummary", void 0), l = t([o("u-action-bar")], l);
31
36
  //#endregion
32
37
  export { l as ActionBar };
@@ -5,7 +5,7 @@ var t = e`
5
5
  display: flex;
6
6
  align-items: center;
7
7
  gap: var(--u-space-sm, 8px);
8
- padding: var(--u-space-md, 12px) 0;
8
+ padding: var(--u-space-md, 12px) var(--action-bar-padding-inline, 0);
9
9
 
10
10
 
11
11
 
@@ -36,6 +36,18 @@ var t = e`
36
36
  display: none;
37
37
  }
38
38
 
39
+
40
+
41
+ .summary {
42
+ flex: 1 1 calc((420px - 100%) * 999);
43
+ min-width: 0;
44
+ font-size: var(--u-text-label-size, 13px);
45
+ color: var(--u-txt-color-weak, #616161);
46
+ }
47
+ .summary.empty {
48
+ display: none;
49
+ }
50
+
39
51
  .main {
40
52
  display: flex;
41
53
  align-items: center;
@@ -56,7 +68,7 @@ var t = e`
56
68
  bottom: 0;
57
69
  background-color: var(--u-bg-color, #FFFFFF);
58
70
  border-top: 1px solid var(--u-border-color-weak, #EEEEEE);
59
- padding: var(--u-space-md, 12px) 0;
71
+ padding: var(--u-space-md, 12px) var(--action-bar-padding-inline, 0);
60
72
  }
61
73
  `;
62
74
  //#endregion
@@ -97,6 +97,11 @@ export declare class InfoField extends StyledElement<ElementParts> {
97
97
  * `--u-success|danger|warning-color-strong`.
98
98
  */
99
99
  tone?: InfoFieldTone;
100
+ /**
101
+ * 값 아래의 보조 줄 — 그 값의 근거나 다른 단위의 같은 양(«1,764,211 units» · «Settled ₩743,188,120»).
102
+ * 추세 문구는 `trendLabel` 이다(방향과 색을 갖는다).
103
+ */
104
+ description?: string;
100
105
  /** 라벨을 이름으로 준 슬롯 위젯과 그때의 라벨 — 소비자가 바꾼 이름은 우리 것이 아니다. */
101
106
  private readonly namedWidgets;
102
107
  /**
@@ -48,12 +48,13 @@ var f = class extends n {
48
48
  ${a ? i`<span aria-hidden="true">${a}</span> ` : ""}${this.trendLabel ?? ""}
49
49
  </div>
50
50
  ` : ""}
51
+ ${this.description ? i`<div class="description" part="description">${this.description}</div>` : ""}
51
52
  `;
52
53
  }
53
54
  };
54
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({
55
56
  type: String,
56
57
  reflect: !0
57
- }), 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);
58
+ }), e("design:type", Object)], f.prototype, "size", void 0), t([o({ type: String }), e("design:type", Object)], f.prototype, "trend", void 0), t([o({ type: String }), e("design:type", String)], f.prototype, "trendLabel", void 0), t([o({ type: String }), e("design:type", Object)], f.prototype, "tone", void 0), t([o({ type: String }), e("design:type", String)], f.prototype, "description", void 0), f = t([a("u-info-field")], f);
58
59
  //#endregion
59
60
  export { f as InfoField, d as isBlank };
@@ -82,6 +82,14 @@ var t = e`
82
82
  margin-top: var(--u-space-3xs, 2px);
83
83
  }
84
84
 
85
+
86
+ .description {
87
+ font-size: var(--u-text-caption-size, 12px);
88
+ line-height: var(--u-text-caption-leading, 1.5);
89
+ color: var(--u-txt-color-weaker, #757575);
90
+ margin-top: var(--u-space-3xs, 2px);
91
+ }
92
+
85
93
  .trend.tone-positive {
86
94
  color: var(--u-success-color-strong, #1B5E20);
87
95
  }
@@ -1,5 +1,7 @@
1
1
  import { StyledElement } from '../internals/StyledElement.js';
2
2
  type ElementParts = 'host' | 'grid';
3
+ /** 배치 — `grid` 라벨-값 칸의 반응형 그리드 · `strip` 지표 띠(구분선으로 나뉜 한 면의 칸). */
4
+ export type InfoSectionLayout = 'grid' | 'strip';
3
5
  /**
4
6
  * 정보 섹션 — `u-info-field` 들의 반응형 그리드.
5
7
  *
@@ -16,11 +18,18 @@ type ElementParts = 'host' | 'grid';
16
18
  * ```
17
19
  *
18
20
  * `min` 으로 한 칸의 최소 폭을 조절한다(기본 160px). 라벨이 긴 도메인은 값을 올린다.
21
+ *
22
+ * `layout="strip"` 은 지표 띠다 — 결과의 합계를 문장이 아니라 칸으로(목록 위 «주문 962 · 청구 ₩…»).
23
+ * 한 면 위에 1px 구분선으로 나뉜 칸들이 **전부 나란히, 아니면 전부 쌓인다**(띠 폭 640px 미만 — 들쭉날쭉한 줄을
24
+ * 만들지 않는다). 임계는 크기 containment 가 아니라 칸의 flex-basis 가 건다(«Holy Albatross» — u-action-bar 와 같은
25
+ * 근거). 구분선은 칸 사이 1px 간격으로 비치는 면의 테두리 색이라 가로·세로가 저절로 바뀐다. 이 배치에서 `min` 은 쓰지 않는다.
19
26
  */
20
27
  export declare class InfoSection extends StyledElement<ElementParts> {
21
28
  static styles: import('lit').CSSResultGroup[];
22
29
  /** 한 칸의 최소 폭(px). 이 값보다 좁아지면 열 수가 줄어든다. */
23
30
  min: number;
31
+ /** 배치 — `grid`(기본) · `strip`(지표 띠). */
32
+ layout: InfoSectionLayout;
24
33
  render(): import('lit-html').TemplateResult<1>;
25
34
  }
26
35
  declare global {
@@ -7,7 +7,7 @@ import { customElement as a, property as o } from "lit/decorators.js";
7
7
  //#region src/components/InfoSection.ts
8
8
  var s = class extends n {
9
9
  constructor(...e) {
10
- super(...e), this.min = 160;
10
+ super(...e), this.min = 160, this.layout = "grid";
11
11
  }
12
12
  static {
13
13
  this.styles = [super.styles, r];
@@ -18,6 +18,9 @@ var s = class extends n {
18
18
  `;
19
19
  }
20
20
  };
21
- t([o({ type: Number }), e("design:type", Object)], s.prototype, "min", void 0), s = t([a("u-info-section")], s);
21
+ t([o({ type: Number }), e("design:type", Object)], s.prototype, "min", void 0), t([o({
22
+ type: String,
23
+ reflect: !0
24
+ }), e("design:type", Object)], s.prototype, "layout", void 0), s = t([a("u-info-section")], s);
22
25
  //#endregion
23
26
  export { s as InfoSection };
@@ -13,6 +13,24 @@ var t = e`
13
13
  grid-template-columns: repeat(auto-fit, minmax(var(--info-min, 160px), 1fr));
14
14
  gap: var(--u-space-lg, 16px) var(--u-space-xl, 20px);
15
15
  }
16
+
17
+
18
+
19
+ :host([layout="strip"]) .grid {
20
+ display: flex;
21
+ flex-wrap: wrap;
22
+ gap: 1px;
23
+ background: var(--u-border-color, #E0E0E0);
24
+ border: 1px solid var(--u-border-color, #E0E0E0);
25
+ border-radius: var(--u-radius-xl, 8px);
26
+ overflow: hidden;
27
+ }
28
+ :host([layout="strip"]) ::slotted(*) {
29
+ flex: 1 1 calc((640px - 100%) * 999);
30
+ min-width: 0;
31
+ padding: var(--u-space-md, 12px) var(--u-space-lg, 16px);
32
+ background: var(--u-bg-color, #FFFFFF);
33
+ }
16
34
  `;
17
35
  //#endregion
18
36
  export { t as styles };
@@ -7,7 +7,7 @@ var t = e`
7
7
  color: var(--app-sidebar-fg, var(--u-txt-color, #212121));
8
8
  background-color: transparent;
9
9
  border: none;
10
- border-radius: 8px;
10
+ border-radius: var(--app-sidebar-item-radius, 8px);
11
11
  transition: all var(--u-duration-normal, 220ms) ease;
12
12
  cursor: pointer;
13
13
  }
@@ -34,8 +34,9 @@ var t = e`
34
34
  flex-direction: row;
35
35
  align-items: center;
36
36
  justify-content: flex-start;
37
- gap: 12px;
38
- padding: 8px 12px;
37
+
38
+ gap: var(--app-sidebar-item-gap, 12px);
39
+ padding: var(--app-sidebar-item-padding, 8px 12px);
39
40
 
40
41
  min-height: var(--u-target-size, 0px);
41
42
  }
@@ -52,7 +53,7 @@ var t = e`
52
53
  u-icon {
53
54
  flex-shrink: 0;
54
55
  color: inherit;
55
- font-size: 20px;
56
+ font-size: var(--app-sidebar-icon-size, 20px);
56
57
  }
57
58
 
58
59
 
@@ -17,13 +17,14 @@ var t = e`
17
17
  flex-direction: row;
18
18
  align-items: center;
19
19
  justify-content: space-between;
20
- gap: 12px;
21
- padding: 8px 12px;
20
+
21
+ gap: var(--app-sidebar-item-gap, 12px);
22
+ padding: var(--app-sidebar-item-padding, 8px 12px);
22
23
 
23
24
  min-height: var(--u-target-size, 0px);
24
25
  background-color: transparent;
25
26
  border: none;
26
- border-radius: 8px;
27
+ border-radius: var(--app-sidebar-item-radius, 8px);
27
28
  transition: all var(--u-duration-normal, 220ms) ease;
28
29
  cursor: pointer;
29
30
  }
package/dist/unsaved.d.ts CHANGED
@@ -36,7 +36,7 @@ export declare function unsavedChangesGuard(_ctx?: RouteLeaveContext): boolean |
36
36
  * @example
37
37
  * ```ts
38
38
  * class OrderPage extends LitElement {
39
- * private unsaved = new UnsavedChangesController(this, () => this.form.dirty);
39
+ * readonly unsaved = new UnsavedChangesController(this, () => this.form.dirty);
40
40
  * }
41
41
  * ```
42
42
  */
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.53.0",
4
+ "version": "0.54.0",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "web-framework",
@@ -244,7 +244,7 @@ Back/Forward included) and the browser asks before the document is left (`before
244
244
  ```ts
245
245
  import { UnsavedChangesController, confirmDiscardChanges } from '@iyulab/modern-app';
246
246
  // Lit
247
- private unsaved = new UnsavedChangesController(this, () => this.form.dirty);
247
+ readonly unsaved = new UnsavedChangesController(this, () => this.form.dirty);
248
248
  // React: useUnsavedChanges(dirty) from '@iyulab/modern-app/react'
249
249
  // Plain: const release = trackUnsavedChanges(() => dirty);
250
250
  // A modal drawer's close is not a navigation — ask from its cancellable `hide` event:
@@ -2,23 +2,29 @@
2
2
 
3
3
  **Tag:** `u-action-bar`
4
4
 
5
- The row of actions that closes a form or a detail screen: primary actions on the leading edge,
6
- destructive ones separated on the trailing edge.
5
+ The row of actions that closes a form or a detail screen: destructive actions at the leading edge, the main
6
+ actions at the trailing edge — the distance between them is the safeguard. Below 420px each group takes its own
7
+ line with the main actions on top, so a destructive action never sits under the thumb.
7
8
 
8
9
  ```html
9
10
  <u-action-bar sticky>
10
- <u-button appearance="solid">Save</u-button>
11
+ <u-button slot="danger" color="danger" appearance="plain">Delete</u-button>
12
+ <span slot="summary">1 change · Order number</span>
11
13
  <u-button appearance="outlined">Cancel</u-button>
12
- <u-button slot="danger" color="danger">Delete</u-button>
14
+ <u-button>Save</u-button>
13
15
  </u-action-bar>
14
16
  ```
15
17
 
18
+ A side sheet's footer runs to the sheet's edges — give it the sheet's inset with `--action-bar-padding-inline`
19
+ rather than negative margins (the bar cannot know its parent's padding).
20
+
16
21
  ## Slots
17
22
 
18
23
  | Name | Description |
19
24
  |------|-------------|
20
- | *(default)* | Primary actions |
21
- | `danger` | Destructive actions, pushed to the trailing edge |
25
+ | *(default)* | Main actions, at the trailing edge |
26
+ | `danger` | Destructive actions, at the leading edge |
27
+ | `summary` | What the main action will do — «1 change · Order number». Follows the danger actions and fills the free width |
22
28
 
23
29
  ## Properties
24
30
 
@@ -30,8 +36,9 @@ destructive ones separated on the trailing edge.
30
36
 
31
37
  | Part | Description |
32
38
  |------|-------------|
33
- | `main` | Primary action group |
39
+ | `main` | Main action group |
34
40
  | `danger` | Destructive action group |
41
+ | `summary` | Summary text wrapper |
35
42
 
36
43
  ## CSS Custom Properties
37
44
 
@@ -41,6 +48,7 @@ destructive ones separated on the trailing edge.
41
48
  | `--u-border-color-weak` | Top border when `sticky` |
42
49
  | `--u-space-sm` | Gap between actions within a group |
43
50
  | `--u-space-md` | Host and sticky-mode vertical padding |
51
+ | `--action-bar-padding-inline` | Inline padding (default `0`) — the sheet inset for a footer that runs edge to edge |
44
52
 
45
53
  ⚠ `sticky` adds **vertical** padding only. It deliberately does not stretch to the container
46
54
  edges with negative margins — that would assume the parent has exactly that much horizontal
@@ -40,6 +40,7 @@ and those are different states of the business. So the component owns the rule.
40
40
  | `size` | `'default'\|'lg'` | `'default'` | ✓ | `'lg'` renders the value at the `title` type-scale step — for dashboard KPI tiles composed inside `u-info-section` |
41
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 |
42
42
  | `trendLabel` | `string` | — | | Trend copy, e.g. `"+12% vs last month"` — wording is the consumer's responsibility |
43
+ | `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` |
43
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 |
44
45
 
45
46
  ⚠ `numeric` no longer right-aligns (it did before 0.25.0). A field is a label/value pair that owns
@@ -83,6 +84,7 @@ as well once it exists.
83
84
  |------|-------------|
84
85
  | `label` · `value` · `trend` | The label, the value, and the (optional) trend indicator |
85
86
  | `unit` | The unit, inside `value` (only when `unit` is set and the value is not blank) |
87
+ | `description` | The supporting line (only when `description` is set) |
86
88
 
87
89
  ## CSS Custom Properties
88
90
 
@@ -16,6 +16,16 @@ markup works inside a side panel.
16
16
  </u-info-section>
17
17
  ```
18
18
 
19
+ A stat strip — the totals above a list, as cells rather than a sentence:
20
+
21
+ ```html
22
+ <u-info-section layout="strip">
23
+ <u-info-field label="Orders" value="962" unit="orders" description="1,764,211 units" size="lg"></u-info-field>
24
+ <u-info-field label="Due in 7 days" value="14" tone="warning" description="3 not started" size="lg"></u-info-field>
25
+ <u-info-field label="Overdue" value="₩0" tone="negative" description="0 orders" size="lg"></u-info-field>
26
+ </u-info-section>
27
+ ```
28
+
19
29
  ## Slots
20
30
 
21
31
  | Name | Description |
@@ -26,7 +36,8 @@ markup works inside a side panel.
26
36
 
27
37
  | Property | Type | Default | Reflect | Description |
28
38
  |----------|------|---------|---------|-------------|
29
- | `min` | `number` | `160` | | Minimum column width in px. Raise it for domains with long labels |
39
+ | `min` | `number` | `160` | | Minimum column width in px. Raise it for domains with long labels. Not used by `layout="strip"` |
40
+ | `layout` | `'grid'\|'strip'` | `'grid'` | ✓ | `grid` — label-value cells in a responsive grid · `strip` — a stat strip: cells on one bordered surface split by 1px dividers, all side by side or all stacked once the strip is narrower than 640px (never a ragged row) |
30
41
 
31
42
  ## CSS Parts
32
43
 
@@ -39,6 +50,8 @@ markup works inside a side panel.
39
50
  | Property | Description |
40
51
  |----------|-------------|
41
52
  | `--u-space-lg` / `--u-space-xl` | Grid gap (row / column) |
53
+ | `--u-border-color` · `--u-radius-xl` · `--u-bg-color` | Strip surface, dividers and corner; cell background |
54
+ | `--u-space-md` / `--u-space-lg` | Strip cell padding (block / inline) |
42
55
 
43
56
  ⚠ Column width is set via the `min` property, not a custom property — it's applied as an inline
44
57
  style per instance, so a `--info-min` override in a stylesheet would lose to it.