@iyulab/modern-app 0.9.0 → 0.10.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,41 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.10.0] - 2026-08-04
4
+
5
+ ### Added
6
+
7
+ - **`u-action-bar` — 상세·편집 화면의 푸터 액션 바.** 초안 R4 의 잔여였다.
8
+
9
+ ```html
10
+ <u-action-bar sticky>
11
+ <u-button slot="danger" color="danger" variant="ghost">삭제</u-button>
12
+ <u-button variant="ghost">취소</u-button>
13
+ <u-button color="primary">저장</u-button>
14
+ </u-action-bar>
15
+ ```
16
+
17
+ **위험 액션은 왼쪽 끝, 주 액션은 오른쪽 끝** — 같은 무리에 두면 «저장»을 누르려다
18
+ «삭제»를 누른다. **거리가 안전장치**이므로 이 분리는 취향이 아니라 기능이다.
19
+ 좁아지면 두 무리가 각자 한 줄을 갖고 **주 액션이 위로** 온다(엄지 위치에 위험 액션을
20
+ 두지 않기 위해서다). `sticky` 로 스크롤 컨테이너 하단에 고정한다.
21
+
22
+ ### Changed
23
+
24
+ - 🔴**접힘 기준이 «화면»에서 «자기 폭»으로 바뀌었다** — `u-page-header` · `u-group-box`.
25
+
26
+ 종전에는 `@media (max-width: …)` 였고, 그러면 이 프리미티브들이 **자기가 얼마나 좁은지와
27
+ 무관하게** 접혔다: 사이드바가 열려 본문이 500px 인 1280px 화면에서는 접히지 않고, 넓은
28
+ 본문을 가진 좁은 화면에서는 불필요하게 접혔다. 컴포넌트가 스스로 판단할 수 있는 것은
29
+ **자기 컨테이너 폭**뿐이다 ⇒ `@container` 로 옮겼다(`container-type: inline-size`).
30
+
31
+ ⚠**시각 변화가 있을 수 있다** — 좁은 컬럼 안의 페이지 헤더는 이제 접히고(종전에는 화면이
32
+ 넓으면 접히지 않았다), 넓은 본문을 가진 태블릿에서는 접히지 않는다(종전에는 접혔다).
33
+
34
+ - **`u-group-box` 의 헤더 툴바가 좁을 때 제목 아래로 내려간다**(컨테이너 480px 이하).
35
+ 제목과 툴바가 한 줄에 남으면 제목은 잘리고 입력은 손가락보다 좁아진다 — 초안 R4 가
36
+ *"좁은 화면에서 접히는 규칙을 라이브러리가 정한다"* 로 요구한 자리다.
37
+
38
+
3
39
  ## [0.9.0] - 2026-08-03
4
40
 
5
41
  > **LOB 레이아웃 프리미티브 신설.** 이 패키지는 종전에 **셸 바깥 껍데기**(사이드바·라우팅·
@@ -0,0 +1,45 @@
1
+ import { StyledElement } from '../internals/StyledElement.js';
2
+ type ElementParts = 'host' | 'danger' | 'main';
3
+ /**
4
+ * 푸터 액션 바 — 상세·편집 화면 맨 아래의 같은 골격.
5
+ *
6
+ * ★**왜 이것이 라이브러리에 있나**: 화면마다 손으로 짜면 ⑴«저장»의 위치가 화면마다
7
+ * 달라지고 ⑵위험 액션(삭제)이 주 액션 **옆에** 놓여 오조작을 부르고 ⑶긴 폼에서
8
+ * 버튼을 찾아 끝까지 스크롤해야 한다. 셋 다 개별로는 사소해 보이지만 **오조작은
9
+ * 되돌릴 수 없다.**
10
+ *
11
+ * ```html
12
+ * <u-action-bar sticky>
13
+ * <u-button slot="danger" color="danger" variant="ghost">삭제</u-button>
14
+ * <u-button variant="ghost">취소</u-button>
15
+ * <u-button color="primary">저장</u-button>
16
+ * </u-action-bar>
17
+ * ```
18
+ *
19
+ * 배치 규칙: **위험 액션은 왼쪽 끝, 주 액션은 오른쪽 끝** — 거리가 안전장치다.
20
+ * 좁아지면 두 무리가 각자 한 줄을 갖고 **주 액션이 위로** 온다(엄지 위치에 위험 액션을
21
+ * 두지 않기 위해서다).
22
+ *
23
+ * 오버라이드: `part`(host·danger·main) + slot 치환.
24
+ */
25
+ export declare class ActionBar extends StyledElement<ElementParts> {
26
+ static styles: import('lit').CSSResultGroup[];
27
+ /**
28
+ * 화면 하단에 고정한다(`position: sticky`).
29
+ * ⚠스크롤 컨테이너 안에서만 의미가 있다 — 부모가 `overflow: auto` 여야 한다.
30
+ */
31
+ sticky: boolean;
32
+ /**
33
+ * 위험 액션 슬롯 배정 상태.
34
+ * ★CSS `:has()` 로는 알 수 없다 — `<slot>` 자신이 자식이라 항상 참이다
35
+ * (`internals/slotted.ts` 참조). 빈 래퍼가 남으면 주 액션이 가운데로 밀린다.
36
+ */
37
+ private hasDanger;
38
+ render(): import('lit-html').TemplateResult<1>;
39
+ }
40
+ declare global {
41
+ interface HTMLElementTagNameMap {
42
+ 'u-action-bar': ActionBar;
43
+ }
44
+ }
45
+ export {};
@@ -0,0 +1,32 @@
1
+ import e from "../_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorate.js";
3
+ import { StyledElement as n } from "../internals/StyledElement.js";
4
+ import { slotHasContent as r } from "../internals/slotted.js";
5
+ import { styles as i } from "./ActionBar.styles.js";
6
+ import { html as a } from "lit";
7
+ import { customElement as o, property as s, state as c } from "lit/decorators.js";
8
+ //#region src/components/ActionBar.ts
9
+ var l = class extends n {
10
+ constructor(...e) {
11
+ super(...e), this.sticky = !1, this.hasDanger = !1;
12
+ }
13
+ static {
14
+ this.styles = [super.styles, i];
15
+ }
16
+ render() {
17
+ return a`
18
+ <div class="danger ${this.hasDanger ? "" : "empty"}" part="danger">
19
+ <slot name="danger"
20
+ @slotchange=${(e) => this.hasDanger = r(e.target)}
21
+ ></slot>
22
+ </div>
23
+ <div class="main" part="main"><slot></slot></div>
24
+ `;
25
+ }
26
+ };
27
+ t([s({
28
+ type: Boolean,
29
+ 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);
31
+ //#endregion
32
+ export { l as ActionBar };
@@ -0,0 +1 @@
1
+ export declare const styles: import('lit').CSSResult;
@@ -0,0 +1,65 @@
1
+ import { css as e } from "lit";
2
+ //#region src/components/ActionBar.styles.ts
3
+ var t = e`
4
+ :host {
5
+ display: flex;
6
+ align-items: center;
7
+ gap: var(--u-space-sm, 8px);
8
+ padding: var(--u-space-md, 12px) 0;
9
+ /* 자기 폭으로 접힌다 — 아래 질의의 기준. 화면 폭이 아니라 자기 상황을 본다.
10
+ ⚠**컨테이너는 «자손»을 위한 것이다** — @container 안에서 :host 자신을 겨냥하면
11
+ 그 질의는 이 호스트가 아니라 **바깥의 다른 컨테이너**를 본다(실측으로 밟았다:
12
+ 360px 에서 접히지 않았다). 그래서 줄바꿈 자체는 여기서 항상 허용하고, 좁을 때의
13
+ «폭 채우기»만 자손 규칙으로 건다. */
14
+ flex-wrap: wrap;
15
+ container-type: inline-size;
16
+ }
17
+
18
+ /* 위험 액션(삭제 등)은 **왼쪽 끝**, 주 액션은 오른쪽 끝.
19
+ ★같은 무리에 두면 «저장»을 누르려다 «삭제»를 누른다 — 거리가 안전장치다.
20
+ 초안(R4)이 *"위험 액션은 시각적으로 분리"* 로 요구한 것이 이 배치다. */
21
+ .danger {
22
+ display: flex;
23
+ align-items: center;
24
+ gap: var(--u-space-sm, 8px);
25
+ margin-right: auto;
26
+ }
27
+ /* 빈 슬롯 래퍼는 접는다 — 슬롯 배정은 slotchange 로 추적한다(internals/slotted.ts).
28
+ ⚠CSS :has() 로는 못 한다: <slot> 자신이 자식이라 :has(*) 가 항상 참이다. */
29
+ .danger.empty {
30
+ display: none;
31
+ }
32
+
33
+ .main {
34
+ display: flex;
35
+ align-items: center;
36
+ gap: var(--u-space-sm, 8px);
37
+ margin-left: auto;
38
+ }
39
+
40
+ /* 화면 하단에 고정 — 긴 폼에서 저장 버튼을 찾아 스크롤하지 않아도 된다. */
41
+ :host([sticky]) {
42
+ position: sticky;
43
+ bottom: 0;
44
+ background-color: var(--u-bg-color, #FFFFFF);
45
+ 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));
48
+ }
49
+
50
+ /* 좁을 때: 두 무리가 각자 한 줄을 차지하고 주 액션이 위로 온다.
51
+ ⚠주 액션을 아래에 두면 위험 액션이 엄지 위치에 온다 — 순서를 뒤집는 이유다. */
52
+ @container (max-width: 420px) {
53
+ .main {
54
+ order: -1;
55
+ width: 100%;
56
+ margin-left: 0;
57
+ }
58
+ .danger {
59
+ width: 100%;
60
+ margin-right: 0;
61
+ }
62
+ }
63
+ `;
64
+ //#endregion
65
+ export { t as styles };
@@ -17,7 +17,13 @@ type ElementParts = 'host' | 'icon' | 'title' | 'description' | 'actions';
17
17
  * <u-empty-state variant="no-results"></u-empty-state>
18
18
  * ```
19
19
  *
20
- * ⚠기본 문구는 한국어다다국어 소비자는 `title`·`description` 준다.
20
+ * ⚠**기본 문구는 영어다** 패키지는 범용 층이라 특정 언어를 기본값으로 가질 수 없다.
21
+ * 한국어는 `registerLocale('ko', …)` 로 소비자가 등록하고, 화면별로 덮으려면
22
+ * `title`·`description` 을 준다.
23
+ *
24
+ * ★이 줄은 «한국어다»라고 적혀 있었고 **같은 파일의 `locale` 프로퍼티 주석이 «영어다»라고
25
+ * 말하고 있었다** — 로케일 이주(0.9.0) 때 클래스 주석만 낡은 것이다. 이 JSDoc 은
26
+ * `dist/**.d.ts` 로 게시되므로, 소비자는 **사실과 반대인 문장**을 읽고 있었다.
21
27
  */
22
28
  export declare class EmptyState extends StyledElement<ElementParts> {
23
29
  static styles: import('lit').CSSResultGroup[];
@@ -3,6 +3,10 @@ import { css as e } from "lit";
3
3
  var t = e`
4
4
  :host {
5
5
  display: block;
6
+ /* 아래 @container 질의의 기준 — 카드는 «화면»이 아니라 «자기 폭»으로 접힌다.
7
+ 사이드바가 열린 1280px 화면의 좁은 본문에서도, 넓은 본문을 가진 태블릿에서도
8
+ 같은 판단을 하려면 기준이 자기 폭이어야 한다. */
9
+ container-type: inline-size;
6
10
  background-color: var(--u-panel-bg-color, #FFFFFF);
7
11
  border: 1px solid var(--u-border-color-weak, #EEEEEE);
8
12
  /* 면(surface) 단 — 컨트롤과 같은 반경을 쓰면 큰 사각형이 각져 보인다. */
@@ -58,6 +62,19 @@ var t = e`
58
62
  .body {
59
63
  padding: var(--u-space-xl, 20px);
60
64
  }
65
+
66
+ /* 좁을 때: 헤더의 툴바(검색·필터)가 제목 아래로 내려가고 폭을 채운다.
67
+ ⚠**제목과 툴바가 한 줄에 남으면 둘 다 읽히지 않는다** — 제목은 잘리고 입력은
68
+ 손가락보다 좁아진다. 초안(R4)이 *"좁은 화면에서 접히는 규칙을 라이브러리가 정한다"*
69
+ 로 요구한 것이 이 자리다. 480px 는 «제목 + 입력»이 함께 설 수 있는 하한이다. */
70
+ @container (max-width: 480px) {
71
+ .header {
72
+ flex-wrap: wrap;
73
+ }
74
+ .actions {
75
+ width: 100%;
76
+ }
77
+ }
61
78
  /* 표를 카드 가장자리까지 붙일 때. 헤더가 있으면 위 여백만 남긴다 —
62
79
  0 으로 만들면 제목과 표가 붙어 읽기 어려워진다. */
63
80
  .body.flush {
@@ -7,6 +7,8 @@ var t = e`
7
7
  gap: var(--u-space-md, 12px);
8
8
  flex-wrap: wrap;
9
9
  margin-bottom: var(--u-space-2xl, 24px);
10
+ /* 아래 @container 질의의 기준 — 이 컴포넌트는 «화면»이 아니라 «자기 폭»으로 접힌다. */
11
+ container-type: inline-size;
10
12
  }
11
13
 
12
14
  .heading {
@@ -76,9 +78,16 @@ var t = e`
76
78
  display: none;
77
79
  }
78
80
 
79
- /* 좁은 화면: 액션이 제목 아래로 내려가고 **왼쪽 정렬**이 된다.
80
- ★오른쪽 정렬을 유지하면 손가락이 닿기 어려운 쪽으로 몰린다. */
81
- @media (max-width: 640px) {
81
+ /* 좁을 때: 액션이 제목 아래로 내려가고 **왼쪽 정렬**이 된다.
82
+ ★오른쪽 정렬을 유지하면 손가락이 닿기 어려운 쪽으로 몰린다.
83
+
84
+ 🔴**«화면»이 아니라 «자기 폭»을 본다(@container).** 종전에는 @media 였고, 그러면
85
+ 이 프리미티브가 **자기가 얼마나 좁은지와 무관하게** 접혔다 — 셸 안에서 사이드바가 열려
86
+ 본문이 500px 인 1280px 화면에서는 접히지 않고, 반대로 넓은 본문을 가진 좁은 화면에서는
87
+ 불필요하게 접혔다. 컴포넌트가 스스로 판단할 수 있는 것은 **자기 컨테이너 폭**뿐이다.
88
+ ⚠container-type: inline-size 는 인라인 축 containment 를 만든다 — 이 호스트는 블록
89
+ 레벨이고 자식이 호스트 폭에 기대므로 무해하다(폭별 회귀 테스트가 감시한다). */
90
+ @container (max-width: 640px) {
82
91
  .actions {
83
92
  margin-left: 0;
84
93
  width: 100%;
package/dist/index.d.ts CHANGED
@@ -9,6 +9,7 @@ export { GroupBox } from './components/GroupBox.js';
9
9
  export { InfoSection } from './components/InfoSection.js';
10
10
  export { InfoField, isBlank } from './components/InfoField.js';
11
11
  export { EmptyState } from './components/EmptyState.js';
12
+ export { ActionBar } from './components/ActionBar.js';
12
13
  export { registerLocale, setDefaultLocale, getLocaleStrings, getDefaultLocale, } from './internals/locale.js';
13
14
  export type { ModernAppLocaleStrings } from './internals/locale.js';
14
15
  export { app };
package/dist/index.js CHANGED
@@ -6,7 +6,8 @@ import { GroupBox as s } from "./components/GroupBox.js";
6
6
  import { InfoSection as c } from "./components/InfoSection.js";
7
7
  import { InfoField as l, isBlank as u } from "./components/InfoField.js";
8
8
  import { EmptyState as d } from "./components/EmptyState.js";
9
+ import { ActionBar as f } from "./components/ActionBar.js";
9
10
  //#region src/index.ts
10
- var f = e;
11
+ var p = e;
11
12
  //#endregion
12
- export { d as EmptyState, s as GroupBox, l as InfoField, c as InfoSection, o as PageHeader, e as app, f as default, t as filterSidebarItems, n as getDefaultLocale, r as getLocaleStrings, u as isBlank, i as registerLocale, a as setDefaultLocale };
13
+ export { f as ActionBar, d as EmptyState, s as GroupBox, l as InfoField, c as InfoSection, o as PageHeader, e as app, p as default, t as filterSidebarItems, n as getDefaultLocale, r as getLocaleStrings, u as isBlank, i as registerLocale, a as setDefaultLocale };
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.9.0",
4
+ "version": "0.10.0",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "web-framework",