@iyulab/modern-app 0.10.0 → 0.11.1

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,6 +1,66 @@
1
1
  # Changelog
2
2
 
3
- ## [0.10.0] - 2026-08-04
3
+ ## [0.11.1] - 2026-08-04
4
+
5
+ ### Fixed
6
+
7
+ - 🔴**`@iyulab/components` 의존 범위가 실제로 필요한 것보다 느슨했다** — `^1.21.0` 이었으나
8
+ `0.11.0` 의 **내비 아이콘 폴백은 `u-icon[fallback]`(1.24.0)** 을 쓴다. 범위가 그것을
9
+ «요구»하지 않으므로, components 를 `1.22.x` 에 고정한 소비자에게는 폴백이 **조용히
10
+ 동작하지 않는다** — 아이콘 없는 그룹 항목이 접힌 사이드바에서 다시 빈 버튼이 된다.
11
+ ⇒ `^1.24.0` 으로 좁혔다.
12
+
13
+ ★**이것을 드러낸 것은 CI 였다** — `0.11.0` 태그 직전의 main 빌드가 아직 게시되지 않은
14
+ `1.24.0` 대신 `1.23.0` 을 설치했고, 그 조합에서 폴백 회귀 7건이 실패했다.
15
+ ***«범위가 담는다»와 «범위가 요구한다»는 다르다.***
16
+
17
+ ## [0.11.0] - 2026-08-04
18
+
19
+ ### Fixed
20
+
21
+ - 🔴**`u-action-bar[sticky]` 가 부모 밖으로 넘쳤다 — 가로 스크롤바가 생기고 주 액션이 잘렸다.**
22
+ 좌우 음수 마진(`margin: 0 calc(-1 * var(--u-space-md))`)으로 «가장자리까지» 늘리고 있었는데,
23
+ 그것은 ***부모가 정확히 그만큼의 가로 패딩을 갖고 있다***는 가정이다. 라이브러리가 소비자의
24
+ 여백을 알 수는 없다 ⇒ 음수 마진을 제거하고 세로 패딩만 남겼다.
25
+ 실측: 1034px 뷰포트에서 우측 12px 초과 → 저장 버튼 클리핑.
26
+
27
+ ### Added
28
+
29
+ - 🔴**내비게이션 항목에 아이콘이 없거나 해석에 실패하면 기본 아이콘(큐브)을 그린다.**
30
+ 사이드바가 접히면 라벨이 숨고 아이콘만 남는데, 그 아이콘이 없으면 그 메뉴는 **높이만 있고
31
+ 아무것도 없는 빈 줄**이 되어 ***그 화면으로 갈 수단이 통째로 사라진다.***
32
+ ⚠«이름이 없을 때»만 막으면 부족하다 — 실측 사례는 *이름은 있는데 파일이 없는* 쪽이었다.
33
+ 그래서 폴백은 `u-icon` 의 **해석 실패 지점**에 걸린다(`u-icon[fallback]`, `@iyulab/components`).
34
+ 폴백 SVG 는 **인라인**이다 — 다시 네트워크에 의존하면 같은 실패가 재현된다.
35
+
36
+ 🔴**그리고 그 폴백이 호출부 셋 중 둘에만 걸려 있었다** — `u-sidebar-group` 은
37
+ `?hidden` 으로 폴백을 무력화한 채였고, 접힌 상태에서는 라벨과 캐럿도 함께 숨으므로
38
+ ***그룹 버튼이 통째로 비어 있었다.*** 세 종류를 한 목록으로 도는 회귀 7건을 신설했다
39
+ (종전에는 이 축의 테스트가 **0건**이었고, 그래서 누락이 조용했다).
40
+
41
+ ### Documentation
42
+
43
+ - 🔴**LOB 프리미티브 6종이 스킬 문서에 아예 없었다** — `u-page-header`·`u-group-box`·
44
+ `u-info-section`·`u-info-field`·`u-empty-state`·`u-action-bar`. 여섯 다 `index.ts` 에서
45
+ **공개**되고 화면이 그것으로 서는데, 스킬은 `app.load()` 설정만 설명하고 있었다.
46
+ 스킬은 소비자(그리고 에이전트)가 API 를 찾는 자리이므로 **거기 없으면 없는 것과 같다.**
47
+ ⇒ `references/components/*.md` 여섯과 목차 절을 더했다.
48
+
49
+ ### Changed
50
+
51
+ - **셸 크롬의 타이포가 디자인 토큰 스케일의 단을 읽는다**(`--u-text-*`).
52
+ 종전에는 사이드바 계열 5개 파일이 **20건 전부 리터럴**이었고, 같은 패키지의 콘텐츠
53
+ 프리미티브(`u-page-header`·`u-group-box`·`u-info-field`·`u-empty-state`)는 **23건 전부
54
+ 토큰**이었다 — 스케일이 있는데 셸에는 닿지 않는 상태였다.
55
+ 단 배정은 관측값 근사가 아니라 **용도**로 했다: 앱 제목 → `subtitle` · 섹션 제목 →
56
+ `overline` · 섹션 부제 → `caption` · 내비 항목 → `label`.
57
+
58
+ ⚠**소비자 영향 — 사이드바 글자가 바뀐다**: 내비 항목 `14px/500` → `13px/600`(작고 굵게),
59
+ 앱 제목 `18px/700` → `16px/600`, 섹션 제목 `12px` → `11px`.
60
+ ★**배치는 바뀌지 않는다** — 내비 행 높이(라벨 줄 박스 19.5px가 20px 아이콘 아래에 들어간다)와
61
+ 헤더 제목 줄 박스(16 × 1.5 = 24px, 종전 `line-height` 와 동일)를 실측으로 고정했다.
62
+ ⚠**아이콘 크기는 타이포가 아니다** — `u-icon`·`.caret`·`.logo`·`.toggler` 의 `font-size` 는
63
+ 도형 크기라 그대로 두었다.
4
64
 
5
65
  ### Added
6
66
 
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Iyulab, Inc.
3
+ Copyright (c) 2025 iyulab
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -37,14 +37,20 @@ var t = e`
37
37
  margin-left: auto;
38
38
  }
39
39
 
40
- /* 화면 하단에 고정 — 긴 폼에서 저장 버튼을 찾아 스크롤하지 않아도 된다. */
40
+ /* 화면 하단에 고정 — 긴 폼에서 저장 버튼을 찾아 스크롤하지 않아도 된다.
41
+
42
+ 🔴**음수 마진으로 «가장자리까지 늘리는» 것을 하지 않는다.** 첫 판이
43
+ margin: 0 calc(-1 * var(--u-space-md, 12px)) 로 좌우를 넘겼는데, 그것은
44
+ ***부모가 정확히 그만큼의 가로 패딩을 갖고 있다***고 가정하는 것이다. 실측에서
45
+ 그 가정이 깨졌다 — 샘플 화면에서 좌우 12px 씩 넘쳐 **가로 스크롤바가 생기고
46
+ 주 액션(저장)이 잘렸다.** 라이브러리가 소비자의 여백을 알 수는 없다.
47
+ ⚠이 주석에 백틱을 쓰지 말 것 — css 태그드 템플릿을 그 자리에서 끝낸다. */
41
48
  :host([sticky]) {
42
49
  position: sticky;
43
50
  bottom: 0;
44
51
  background-color: var(--u-bg-color, #FFFFFF);
45
52
  border-top: 1px solid var(--u-border-color-weak, #EEEEEE);
46
- padding: var(--u-space-md, 12px);
47
- margin: 0 calc(-1 * var(--u-space-md, 12px));
53
+ padding: var(--u-space-md, 12px) 0;
48
54
  }
49
55
 
50
56
  /* 좁을 때: 두 무리가 각자 한 줄을 차지하고 주 액션이 위로 온다.
@@ -59,8 +59,17 @@ var t = e`
59
59
  display: none;
60
60
  }
61
61
 
62
+ /* 🔴**본문은 세로 흐름이고, 그 리듬을 여기서 정한다.**
63
+ 종전에는 padding 만 있어 블록 자식이 **간격 없이 붙었고**, 그보다 나쁘게는
64
+ shrink-to-fit 요소들이 각자 다른 폭으로 앉아 **왼쪽 끝이 어긋났다**(실측: 알림 넷을
65
+ 나란히 두면 계단처럼 내려갔다). 소비자가 그것을 고치려면 자기 CSS 를 쓰게 되고,
66
+ 그 순간 이 패키지가 존재하는 이유가 사라진다.
67
+ ⇒ flex column + gap. 자식은 폭을 채우고 간격은 토큰이 정한다. */
62
68
  .body {
63
69
  padding: var(--u-space-xl, 20px);
70
+ display: flex;
71
+ flex-direction: column;
72
+ gap: var(--u-space-md, 12px);
64
73
  }
65
74
 
66
75
  /* 좁을 때: 헤더의 툴바(검색·필터)가 제목 아래로 내려가고 폭을 채운다.
@@ -1,23 +1,25 @@
1
1
  import e from "../_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateMetadata.js";
2
2
  import t from "../_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorate.js";
3
3
  import { StyledElement as n } from "../internals/StyledElement.js";
4
- import { styles as r } from "./SidebarButton.styles.js";
5
- import { html as i } from "lit";
6
- import { customElement as a, property as o } from "lit/decorators.js";
4
+ import { DEFAULT_NAV_ICON as r } from "../internals/nav-icon.js";
5
+ import { styles as i } from "./SidebarButton.styles.js";
6
+ import { html as a } from "lit";
7
+ import { customElement as o, property as s } from "lit/decorators.js";
7
8
  import "@iyulab/components/dist/components/icon/UIcon.js";
8
9
  //#region src/components/SidebarButton.ts
9
- var s = class extends n {
10
+ var c = class extends n {
10
11
  constructor(...e) {
11
12
  super(...e), this.compact = !1;
12
13
  }
13
14
  static {
14
- this.styles = [super.styles, r];
15
+ this.styles = [super.styles, i];
15
16
  }
16
17
  render() {
17
- return i`
18
+ return a`
18
19
  <button part="base" ?compact=${this.compact}>
19
- <u-icon part="icon" ?hidden=${!this.icon}
20
+ <u-icon part="icon"
20
21
  .name=${this.icon}
22
+ .fallback=${r}
21
23
  ></u-icon>
22
24
  <span part="label" ?hidden=${this.compact}>
23
25
  ${this.label}
@@ -26,9 +28,9 @@ var s = class extends n {
26
28
  `;
27
29
  }
28
30
  };
29
- t([o({
31
+ t([s({
30
32
  type: Boolean,
31
33
  reflect: !0
32
- }), e("design:type", Object)], s.prototype, "compact", void 0), t([o({ type: String }), e("design:type", String)], s.prototype, "icon", void 0), t([o({ type: String }), e("design:type", Object)], s.prototype, "label", void 0), s = t([a("u-sidebar-button")], s);
34
+ }), e("design:type", Object)], c.prototype, "compact", void 0), t([s({ type: String }), e("design:type", String)], c.prototype, "icon", void 0), t([s({ type: String }), e("design:type", Object)], c.prototype, "label", void 0), c = t([o("u-sidebar-button")], c);
33
35
  //#endregion
34
- export { s as SidebarButton };
36
+ export { c as SidebarButton };
@@ -45,11 +45,12 @@ var t = e`
45
45
  font-size: 20px;
46
46
  }
47
47
 
48
+ /* ★SidebarLink 와 같은 자리·같은 단(label). 두 곳이 어긋나면 목록이 계단이 된다. */
48
49
  span {
49
50
  flex: 1;
50
- font-size: 14px;
51
- font-weight: 500;
52
- line-height: 20px;
51
+ font-size: var(--u-text-label-size, 13px);
52
+ font-weight: var(--u-text-label-weight, 600);
53
+ line-height: var(--u-text-label-leading, 1.5);
53
54
  white-space: nowrap;
54
55
  overflow: hidden;
55
56
  text-overflow: ellipsis;
@@ -1,29 +1,37 @@
1
1
  import e from "../_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateMetadata.js";
2
2
  import t from "../_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorate.js";
3
3
  import { StyledElement as n } from "../internals/StyledElement.js";
4
- import { SidebarLink as r } from "./SidebarLink.js";
5
- import { styles as i } from "./SidebarGroup.styles.js";
6
- import { html as a } from "lit";
7
- import { customElement as o, property as s } from "lit/decorators.js";
4
+ import { DEFAULT_NAV_ICON as r } from "../internals/nav-icon.js";
5
+ import { SidebarLink as i } from "./SidebarLink.js";
6
+ import { styles as a } from "./SidebarGroup.styles.js";
7
+ import { html as o } from "lit";
8
+ import { customElement as s, property as c } from "lit/decorators.js";
8
9
  import "@iyulab/components/dist/components/icon/UIcon.js";
9
10
  //#region src/components/SidebarGroup.ts
10
- var c = class extends n {
11
+ var l = class extends n {
11
12
  constructor(...e) {
12
13
  super(...e), this.compact = !1, this.selected = !1, this.collapsed = !0, this.handleButtonClick = (e) => {
13
- this.compact ? ((this.shadowRoot?.querySelector("slot")?.assignedElements({ flatten: !0 }).find((e) => e instanceof r))?.renderRoot.querySelector("u-link"))?.click() : this.collapsed = !this.collapsed;
14
+ this.compact ? ((this.shadowRoot?.querySelector("slot")?.assignedElements({ flatten: !0 }).find((e) => e instanceof i))?.renderRoot.querySelector("u-link"))?.click() : this.collapsed = !this.collapsed;
14
15
  };
15
16
  }
16
17
  static {
17
- this.styles = [super.styles, i];
18
+ this.styles = [super.styles, a];
18
19
  }
19
20
  render() {
20
- return a`
21
+ return o`
21
22
  <button part="header"
22
23
  ?compact=${this.compact}
23
24
  ?selected=${this.selected}
24
25
  @click=${this.handleButtonClick}>
25
- <u-icon class="icon" part="icon" ?hidden=${!this.icon}
26
+ <!--
27
+ ⚠ ?hidden 을 걸지 않는다. 접힌 상태에서는 라벨과 캐럿이 모두 숨으므로,
28
+ 아이콘까지 숨기면 **버튼이 통째로 비어 누를 것이 없어진다** — 이것이
29
+ SidebarLink·SidebarButton 에서 폴백을 도입한 바로 그 결함이고, 이 자리만
30
+ 빠져 있었다.
31
+ -->
32
+ <u-icon class="icon" part="icon"
26
33
  .name=${this.icon}
34
+ .fallback=${r}
27
35
  ></u-icon>
28
36
  <span class="label" part="label" ?hidden=${this.compact}>
29
37
  ${this.label}
@@ -42,15 +50,15 @@ var c = class extends n {
42
50
  `;
43
51
  }
44
52
  };
45
- t([s({
53
+ t([c({
46
54
  type: Boolean,
47
55
  reflect: !0
48
- }), e("design:type", Boolean)], c.prototype, "compact", void 0), t([s({
56
+ }), e("design:type", Boolean)], l.prototype, "compact", void 0), t([c({
49
57
  type: Boolean,
50
58
  reflect: !0
51
- }), e("design:type", Boolean)], c.prototype, "selected", void 0), t([s({
59
+ }), e("design:type", Boolean)], l.prototype, "selected", void 0), t([c({
52
60
  type: Boolean,
53
61
  reflect: !0
54
- }), e("design:type", Boolean)], c.prototype, "collapsed", void 0), t([s({ type: String }), e("design:type", String)], c.prototype, "icon", void 0), t([s({ type: String }), e("design:type", Object)], c.prototype, "label", void 0), c = t([o("u-sidebar-group")], c);
62
+ }), e("design:type", Boolean)], l.prototype, "collapsed", void 0), t([c({ type: String }), e("design:type", String)], l.prototype, "icon", void 0), t([c({ type: String }), e("design:type", Object)], l.prototype, "label", void 0), l = t([s("u-sidebar-group")], l);
55
63
  //#endregion
56
- export { c as SidebarGroup };
64
+ export { l as SidebarGroup };
@@ -53,11 +53,16 @@ var t = e`
53
53
  font-size: 20px;
54
54
  }
55
55
 
56
+ /*
57
+ * ★그룹도 자식 링크와 같은 label 단이다. 종전에는 굵기로 위계를 냈지만(600 vs 500),
58
+ * 자식은 이미 **32px 들여쓰기 + 2px 레일 + 캐럿**으로 갈린다 — 굵기 한 단을 위계에
59
+ * 더 쓰면 스케일 밖의 다섯 번째 값이 생긴다. 위계는 배치가 지고 단은 용도가 진다.
60
+ */
56
61
  .label {
57
62
  flex: 1;
58
- font-size: 14px;
59
- line-height: 20px;
60
- font-weight: 600;
63
+ font-size: var(--u-text-label-size, 13px);
64
+ line-height: var(--u-text-label-leading, 1.5);
65
+ font-weight: var(--u-text-label-weight, 600);
61
66
  overflow: hidden;
62
67
  white-space: nowrap;
63
68
  text-overflow: ellipsis;
@@ -1,24 +1,26 @@
1
1
  import e from "../_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateMetadata.js";
2
2
  import t from "../_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorate.js";
3
3
  import { StyledElement as n } from "../internals/StyledElement.js";
4
- import { styles as r } from "./SidebarLink.styles.js";
5
- import { html as i } from "lit";
6
- import { customElement as a, property as o } from "lit/decorators.js";
4
+ import { DEFAULT_NAV_ICON as r } from "../internals/nav-icon.js";
5
+ import { styles as i } from "./SidebarLink.styles.js";
6
+ import { html as a } from "lit";
7
+ import { customElement as o, property as s } from "lit/decorators.js";
7
8
  import "@iyulab/components/dist/components/icon/UIcon.js";
8
9
  //#region src/components/SidebarLink.ts
9
- var s = class extends n {
10
+ var c = class extends n {
10
11
  constructor(...e) {
11
12
  super(...e), this.selected = !1, this.compact = !1;
12
13
  }
13
14
  static {
14
- this.styles = [super.styles, r];
15
+ this.styles = [super.styles, i];
15
16
  }
16
17
  render() {
17
- return i`
18
+ return a`
18
19
  <u-link .href=${this.href || "#"}>
19
20
  <div class="container" part="base" ?compact=${this.compact}>
20
- <u-icon part="icon" ?hidden=${!this.icon}
21
+ <u-icon part="icon"
21
22
  .name=${this.icon}
23
+ .fallback=${r}
22
24
  ></u-icon>
23
25
  <span part="label" ?hidden=${this.compact}>
24
26
  ${this.label}
@@ -28,9 +30,9 @@ var s = class extends n {
28
30
  `;
29
31
  }
30
32
  };
31
- t([o({
33
+ t([s({
32
34
  type: Boolean,
33
35
  reflect: !0
34
- }), e("design:type", Object)], s.prototype, "selected", void 0), t([o({ type: Boolean }), e("design:type", Object)], s.prototype, "compact", void 0), t([o({ type: String }), e("design:type", String)], s.prototype, "icon", void 0), t([o({ type: String }), e("design:type", Object)], s.prototype, "label", void 0), t([o({ type: String }), e("design:type", String)], s.prototype, "href", void 0), t([o({ type: String }), e("design:type", Object)], s.prototype, "pattern", void 0), s = t([a("u-sidebar-link")], s);
36
+ }), e("design:type", Object)], c.prototype, "selected", void 0), t([s({ type: Boolean }), e("design:type", Object)], c.prototype, "compact", void 0), t([s({ type: String }), e("design:type", String)], c.prototype, "icon", void 0), t([s({ type: String }), e("design:type", Object)], c.prototype, "label", void 0), t([s({ type: String }), e("design:type", String)], c.prototype, "href", void 0), t([s({ type: String }), e("design:type", Object)], c.prototype, "pattern", void 0), c = t([o("u-sidebar-link")], c);
35
37
  //#endregion
36
- export { s as SidebarLink };
38
+ export { c as SidebarLink };
@@ -50,11 +50,16 @@ var t = e`
50
50
  font-size: 20px;
51
51
  }
52
52
 
53
+ /*
54
+ * ★내비 항목은 «대상의 짧은 이름» = label 단이다(용도 배정 — DL-157-4).
55
+ * ⚠행 높이는 바뀌지 않는다: 13 x 1.5 = 19.5px < 아이콘 20px 이라 행을 잡는 것은
56
+ * 여전히 아이콘이다. 접힌 사이드바(아이콘만)와 펼친 사이드바의 행 높이가 같아야 한다.
57
+ */
53
58
  span {
54
59
  flex: 1;
55
- font-size: 14px;
56
- line-height: 20px;
57
- font-weight: 500;
60
+ font-size: var(--u-text-label-size, 13px);
61
+ line-height: var(--u-text-label-leading, 1.5);
62
+ font-weight: var(--u-text-label-weight, 600);
58
63
  white-space: nowrap;
59
64
  overflow: hidden;
60
65
  text-overflow: ellipsis;
@@ -19,20 +19,28 @@ var t = e`
19
19
  텍스트 단은 --u-txt-color-weak 하나뿐이고 그 값은 흰 배경에서 2.68:1 이라
20
20
  WCAG AA(4.5:1)에 미달한다. 현행 두 값은 6.19:1 · 4.61:1 로 통과한다.
21
21
  ⇒ 역할 층에 AA 를 넘는 보조 텍스트 단이 생기면 그때 옮긴다. */
22
+ /*
23
+ * ★크기·굵기·자간은 스케일의 단으로 옮겼다(용도 배정 — DL-157-4). 위 주석은 **색**에
24
+ * 대한 것이고 그대로 유효하다: 두 색은 여전히 팔레트를 직접 읽는다.
25
+ * ⚠ overline 에 대문자 변환을 붙이지 않는다 — 이 줄에 ellipsis 가 있어 잘림이 늘고,
26
+ * 섹션 이름은 소비자가 쓴 글자다(CJK 에는 효과도 없다).
27
+ */
22
28
  .title {
23
29
  color: var(--u-neutral-700, #616161);
24
- font-size: 12px;
25
- font-weight: 700;
26
- letter-spacing: 0.5px;
30
+ font-size: var(--u-text-overline-size, 11px);
31
+ font-weight: var(--u-text-overline-weight, 700);
32
+ line-height: var(--u-text-overline-leading, 1.45);
33
+ letter-spacing: var(--u-text-overline-tracking, 0.06em);
27
34
  white-space: nowrap;
28
35
  overflow: hidden;
29
36
  text-overflow: ellipsis;
30
37
  }
31
-
38
+
32
39
  .subtitle {
33
40
  color: var(--u-neutral-600, #757575);
34
- font-size: 11px;
35
- font-weight: 300;
41
+ font-size: var(--u-text-caption-size, 12px);
42
+ font-weight: var(--u-text-caption-weight, 400);
43
+ line-height: var(--u-text-caption-leading, 1.5);
36
44
  }
37
45
 
38
46
  .items {
@@ -0,0 +1,19 @@
1
+ /**
2
+ * 내비게이션 항목의 **기본 아이콘** — 아이콘이 없거나 해석에 실패했을 때 대신 그린다.
3
+ *
4
+ * 🔴**왜 필요한가 — «안 보인다»가 «누를 수 없다»가 되는 자리다.**
5
+ * 사이드바가 접히면(`compact`) 라벨이 숨고 아이콘만 남는다. 그 아이콘이 없으면 그 메뉴는
6
+ * **높이만 있고 아무것도 없는 빈 줄**이 되어, 사용자가 그 화면으로 갈 수단이 통째로 사라진다.
7
+ * 실측으로 밟았다 — 샘플의 `Items` 메뉴가 `icon: 'table'` 을 선언했는데 그 파일이 없어
8
+ * 아이콘이 그려지지 않았고, 접으면 고를 수 없었다.
9
+ *
10
+ * ⚠**«이름이 없을 때»만 막으면 부족하다.** 실제 사례는 *이름은 있는데 해석이 실패한* 쪽이었다
11
+ * (404 · 파싱 실패). 그래서 폴백은 `u-icon` 의 **해석 실패 지점**에 걸려 있다(`fallback` 속성).
12
+ *
13
+ * ⚠**이 SVG 는 인라인이다 — 파일을 받아오지 않는다.** 폴백이 다시 네트워크에 의존하면
14
+ * *"아이콘 폴더에 아무것도 없는 소비자"* 에게 같은 실패가 그대로 재현된다.
15
+ *
16
+ * 모양은 **큐브**다 — 특정 의미를 주장하지 않으면서 «항목»을 뜻하는 중립 기호이고,
17
+ * 이 리포의 다른 아이콘과 같은 계열(Bootstrap Icons, 16 viewBox, `currentColor` fill)이다.
18
+ */
19
+ export declare const DEFAULT_NAV_ICON = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\"><path d=\"M8.186 1.113a.5.5 0 0 0-.372 0L1.846 3.5 8 5.961 14.154 3.5zM15 4.239l-6.5 2.6v7.922l6.5-2.6V4.24zM7.5 14.762V6.838L1 4.239v7.923zM7.443.184a1.5 1.5 0 0 1 1.114 0l7.129 2.852A.5.5 0 0 1 16 3.5v8.662a1 1 0 0 1-.629.928l-7.185 2.874a.5.5 0 0 1-.372 0L.63 13.09a1 1 0 0 1-.63-.928V3.5a.5.5 0 0 1 .314-.464z\"/></svg>";
@@ -0,0 +1,4 @@
1
+ //#region src/internals/nav-icon.ts
2
+ var e = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\"><path d=\"M8.186 1.113a.5.5 0 0 0-.372 0L1.846 3.5 8 5.961 14.154 3.5zM15 4.239l-6.5 2.6v7.922l6.5-2.6V4.24zM7.5 14.762V6.838L1 4.239v7.923zM7.443.184a1.5 1.5 0 0 1 1.114 0l7.129 2.852A.5.5 0 0 1 16 3.5v8.662a1 1 0 0 1-.629.928l-7.185 2.874a.5.5 0 0 1-.372 0L.63 13.09a1 1 0 0 1-.63-.928V3.5a.5.5 0 0 1 .314-.464z\"/></svg>";
3
+ //#endregion
4
+ export { e as DEFAULT_NAV_ICON };
@@ -29,12 +29,19 @@ var t = e`
29
29
  object-fit: contain;
30
30
  }
31
31
 
32
+ /*
33
+ * ★단 배정은 «관측값 근사»가 아니라 «용도»로 한다(DL-157-4) — 이 자리는 앱의 제목이다.
34
+ * 종전 18px 은 스케일에 없는 값이었고, 같은 판정을 이미 u-dialog·u-drawer 제목에
35
+ * 내린 적이 있다(L2-3, 18px → subtitle).
36
+ * ⚠줄 박스는 바뀌지 않는다: 16 x 1.5 = 24px = 종전 line-height. 헤더 높이 불변.
37
+ */
32
38
  .title {
33
39
  flex: 1;
34
40
  color: var(--u-txt-color, #212121);
35
- font-size: 18px;
36
- line-height: 24px;
37
- font-weight: 700;
41
+ font-size: var(--u-text-subtitle-size, 16px);
42
+ line-height: var(--u-text-subtitle-leading, 1.5);
43
+ font-weight: var(--u-text-subtitle-weight, 600);
44
+ letter-spacing: var(--u-text-subtitle-tracking, 0);
38
45
  white-space: nowrap;
39
46
  overflow: hidden;
40
47
  text-overflow: ellipsis;
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.10.0",
4
+ "version": "0.11.1",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "web-framework",
@@ -37,11 +37,12 @@
37
37
  "test": "vitest run",
38
38
  "test:watch": "vitest",
39
39
  "start": "vite",
40
- "build": "vite build",
41
- "test:browser": "vitest run --project=browser"
40
+ "build": "npm run typecheck && vite build",
41
+ "test:browser": "vitest run --project=browser",
42
+ "typecheck": "tsc --noEmit"
42
43
  },
43
44
  "dependencies": {
44
- "@iyulab/components": "^1.21.0",
45
+ "@iyulab/components": "^1.24.0",
45
46
  "@iyulab/router": "^0.10.2",
46
47
  "i18next": "^26.3.6",
47
48
  "lit": "^3.3.3"
@@ -214,6 +214,20 @@ Sidebar states: `'default'` | `'slim'` | `'modal'` | `'mobile'` | `'mobile-open'
214
214
 
215
215
  ---
216
216
 
217
+ ## LOB primitives
218
+
219
+ Screen-building components this package registers. They exist so the same skeleton is not
220
+ re-assembled per screen — that is where a product stops looking like one product.
221
+
222
+ - [`u-page-header`](./references/components/page-header.md) — Screen header: back link, title, subtitle, status and action slots
223
+ - [`u-group-box`](./references/components/group-box.md) — Titled card with trailing header actions; the unit detail screens are built from
224
+ - [`u-info-section`](./references/components/info-section.md) — Responsive grid for label/value pairs; container width picks the column count
225
+ - [`u-info-field`](./references/components/info-field.md) — Read-only label/value pair that keeps "not set" and "zero" distinct
226
+ - [`u-empty-state`](./references/components/empty-state.md) — Empty list or empty search, worded differently because they are different facts
227
+ - [`u-action-bar`](./references/components/action-bar.md) — Closing action row with a separated destructive group
228
+
229
+ ---
230
+
217
231
  ## Full `AppConfig` reference
218
232
 
219
233
  See [references/api.md](./references/api.md) for all TypeScript interfaces.
@@ -0,0 +1,38 @@
1
+ # u-action-bar
2
+
3
+ **Tag:** `u-action-bar`
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.
7
+
8
+ ```html
9
+ <u-action-bar sticky>
10
+ <u-button variant="solid">Save</u-button>
11
+ <u-button variant="outline">Cancel</u-button>
12
+ <u-button slot="danger" color="danger">Delete</u-button>
13
+ </u-action-bar>
14
+ ```
15
+
16
+ ## Slots
17
+
18
+ | Name | Description |
19
+ |------|-------------|
20
+ | *(default)* | Primary actions |
21
+ | `danger` | Destructive actions, pushed to the trailing edge |
22
+
23
+ ## Properties
24
+
25
+ | Property | Type | Default | Reflect | Description |
26
+ |----------|------|---------|---------|-------------|
27
+ | `sticky` | `boolean` | `false` | ✓ | Pin to the bottom of the scroll container |
28
+
29
+ ## CSS Parts
30
+
31
+ | Part | Description |
32
+ |------|-------------|
33
+ | `main` | Primary action group |
34
+ | `danger` | Destructive action group |
35
+
36
+ ⚠ `sticky` adds **vertical** padding only. It deliberately does not stretch to the container
37
+ edges with negative margins — that would assume the parent has exactly that much horizontal
38
+ padding, and when the assumption is wrong the bar overflows and clips its own buttons.
@@ -0,0 +1,43 @@
1
+ # u-empty-state
2
+
3
+ **Tag:** `u-empty-state`
4
+
5
+ Shown when a list or a search comes back with nothing.
6
+
7
+ 🔴 **"there is no data" and "no results matched" are different facts.** The first means
8
+ *nothing has been created yet* and the next action is **create**. The second means *nothing
9
+ matches these conditions* and the next action is **change the filter**. Show the same wording
10
+ for both and a user with a filter still applied reads it as *"my data disappeared"*.
11
+
12
+ ```html
13
+ <u-empty-state variant="no-data" title="No orders yet">
14
+ <u-button slot="actions">New order</u-button>
15
+ </u-empty-state>
16
+
17
+ <u-empty-state variant="no-results"></u-empty-state>
18
+ ```
19
+
20
+ ## Slots
21
+
22
+ | Name | Description |
23
+ |------|-------------|
24
+ | `icon` | Replaces the built-in icon |
25
+ | `actions` | Next-step buttons |
26
+
27
+ ## Properties
28
+
29
+ | Property | Type | Default | Reflect | Description |
30
+ |----------|------|---------|---------|-------------|
31
+ | `variant` | `'no-data'\|'no-results'` | `'no-data'` | | Which fact is being shown; changes the default wording |
32
+ | `title` | `string` | `''` | | Override the default title |
33
+ | `description` | `string` | `''` | | Override the default description |
34
+ | `locale` | `string` | `''` | | Locale tag override for built-in strings |
35
+
36
+ ## CSS Parts
37
+
38
+ | Part | Description |
39
+ |------|-------------|
40
+ | `icon` · `title` · `description` · `actions` | The four regions |
41
+
42
+ ⚠ Default wording is **English** — register others with `registerLocale('ko', …)`, or pass
43
+ `title`/`description` per screen.
@@ -0,0 +1,41 @@
1
+ # u-group-box
2
+
3
+ **Tag:** `u-group-box`
4
+
5
+ A titled card — the unit LOB detail screens are built from.
6
+
7
+ `u-card` gives you a surface. What actually repeats on these screens is *surface + title +
8
+ trailing actions*, and assembling that by hand is how title sizes, padding, and dividers end
9
+ up different on every screen.
10
+
11
+ ```html
12
+ <u-group-box title="Receivables">
13
+ <a slot="actions" href="/receivables">Payments</a>
14
+ …body…
15
+ </u-group-box>
16
+ ```
17
+
18
+ The body is a flex column with token spacing, so block children get vertical rhythm and share
19
+ a left edge without any consumer CSS.
20
+
21
+ ## Slots
22
+
23
+ | Name | Description |
24
+ |------|-------------|
25
+ | *(default)* | Body content |
26
+ | `actions` | Trailing controls in the header |
27
+
28
+ ## Properties
29
+
30
+ | Property | Type | Default | Reflect | Description |
31
+ |----------|------|---------|---------|-------------|
32
+ | `title` | `string` | `''` | | Header title |
33
+ | `divider` | `boolean` | `false` | | Rule between header and body. Off by default — many rules make a screen noisy |
34
+ | `flush` | `boolean` | `false` | | Remove body padding, for tables and lists that draw their own edges |
35
+
36
+ ## CSS Parts
37
+
38
+ | Part | Description |
39
+ |------|-------------|
40
+ | `header` · `title` · `actions` | Header row |
41
+ | `body` | Body wrapper |
@@ -0,0 +1,45 @@
1
+ # u-info-field
2
+
3
+ **Tag:** `u-info-field`
4
+
5
+ A read-only label/value pair.
6
+
7
+ 🔴 **"not set yet" and "zero" are different facts.**
8
+
9
+ ```
10
+ null · undefined · '' → blank placeholder (—)
11
+ 0 · false · '0' → rendered as-is
12
+ ```
13
+
14
+ Leaving that rule to whoever writes the screen guarantees it drifts. It has: an order with
15
+ *quantity 0* and an order whose *quantity had not been decided* rendered identically as `—`,
16
+ and those are different states of the business. So the component owns the rule.
17
+
18
+ ```html
19
+ <u-info-field label="Quantity" .value=${order.quantity} numeric></u-info-field>
20
+ <u-info-field label="Customer">Acme Printing</u-info-field> <!-- slot wins over value -->
21
+ ```
22
+
23
+ ## Slots
24
+
25
+ | Name | Description |
26
+ |------|-------------|
27
+ | *(default)* | Value content; takes precedence over `value` |
28
+
29
+ ## Properties
30
+
31
+ | Property | Type | Default | Reflect | Description |
32
+ |----------|------|---------|---------|-------------|
33
+ | `label` | `string` | `''` | | Field label |
34
+ | `value` | `unknown` | — | | Value; property only (`attribute: false`) |
35
+ | `blank` | `string` | `'—'` | | Placeholder for null/undefined/empty — **not** for `0` or `false` |
36
+ | `numeric` | `boolean` | `false` | | Right-align with tabular figures so digits line up vertically |
37
+
38
+ ⚠ `numeric` earns its keep in **tables**, where columns of figures are compared by eye. On a
39
+ lone field it just pushes the value away from its label.
40
+
41
+ ## CSS Parts
42
+
43
+ | Part | Description |
44
+ |------|-------------|
45
+ | `label` · `value` | The two elements |
@@ -0,0 +1,35 @@
1
+ # u-info-section
2
+
3
+ **Tag:** `u-info-section`
4
+
5
+ Responsive grid for `u-info-field` pairs.
6
+
7
+ **You do not count the columns.** Hand-built label/value grids usually hardcode a column count
8
+ per screen, which then breaks only at tablet width — on a screen nobody opens, so it stays
9
+ broken. Here the **container width** decides (`auto-fit`, not media queries), so the same
10
+ markup works inside a side panel.
11
+
12
+ ```html
13
+ <u-info-section>
14
+ <u-info-field label="Part" value="Standard"></u-info-field>
15
+ <u-info-field label="Quantity" .value=${0} numeric></u-info-field>
16
+ </u-info-section>
17
+ ```
18
+
19
+ ## Slots
20
+
21
+ | Name | Description |
22
+ |------|-------------|
23
+ | *(default)* | `u-info-field` elements |
24
+
25
+ ## Properties
26
+
27
+ | Property | Type | Default | Reflect | Description |
28
+ |----------|------|---------|---------|-------------|
29
+ | `min` | `number` | `160` | | Minimum column width in px. Raise it for domains with long labels |
30
+
31
+ ## CSS Parts
32
+
33
+ | Part | Description |
34
+ |------|-------------|
35
+ | `grid` | Grid container |
@@ -0,0 +1,46 @@
1
+ # u-page-header
2
+
3
+ **Tag:** `u-page-header`
4
+
5
+ The same skeleton at the top of every LOB screen: optional back link, title, subtitle,
6
+ a status slot, and an actions slot.
7
+
8
+ Hand-rolling this per screen is where a product stops looking like one product — title sizes
9
+ drift, action alignment goes slightly off, and the narrow-viewport rule gets forgotten on the
10
+ screens nobody opens.
11
+
12
+ ```html
13
+ <u-page-header title="Order G-2026-I-0629" subtitle="Received 2026-02-24" back="/orders">
14
+ <span slot="status"><u-tag color="success">Confirmed</u-tag></span>
15
+ <span slot="actions"><u-button>Edit</u-button></span>
16
+ </u-page-header>
17
+ ```
18
+
19
+ ## Slots
20
+
21
+ | Name | Description |
22
+ |------|-------------|
23
+ | `status` | Badge or tag beside the title |
24
+ | `actions` | Buttons on the trailing edge |
25
+
26
+ ## Properties
27
+
28
+ | Property | Type | Default | Reflect | Description |
29
+ |----------|------|---------|---------|-------------|
30
+ | `title` | `string` | `''` | | Page title |
31
+ | `subtitle` | `string` | — | | Secondary line under the title |
32
+ | `back` | `string` | — | | Back-link href; renders a back affordance before the title |
33
+ | `backLabel` | `string` | `''` | | Back-link text (`back-label`). Empty falls back to the locale registry, then English |
34
+ | `locale` | `string` | `''` | | Locale tag override for built-in strings |
35
+
36
+ ## CSS Parts
37
+
38
+ | Part | Description |
39
+ |------|-------------|
40
+ | `back` | Back link |
41
+ | `heading` | Title + subtitle wrapper |
42
+ | `title` · `subtitle` | Text elements |
43
+ | `status` · `actions` | Slot wrappers |
44
+
45
+ ⚠ Built-in strings default to **English** — this package is a generic layer and cannot pick a
46
+ language. Register others with `registerLocale('ko', { back: '뒤로' })`.