@iyulab/components 1.7.0 → 1.7.2

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,26 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.7.2] - 2026-07-19
4
+
5
+ ### Fixed
6
+ - **`Dialog.show()`가 영구 대기(hang)에 빠지던 결함 수정** — 프로미스 executor 가 `async` 였던 탓에 `await dialog.updateComplete` 가 reject 되면 예외가 삼켜지고 `hide` 리스너가 등록조차 되지 않아, `await Dialog.show(...)` 호출자가 **영원히 매달렸다**. 이제 리스너를 `await` **이전에** 등록하고(대기 중 발생한 `hide` 를 놓치던 경합도 함께 해소), 업데이트 실패 시 `console.error` 후 문서화된 "닫힘 = null" 규약대로 `null` 로 종료하며 고아 엘리먼트를 DOM 에서 제거한다. 회귀 테스트 3건 추가(정상 resolve / 닫힘 null / updateComplete reject).
7
+ - `UOverlayElement`: 삼항 연산자를 문(statement)으로 사용하던 `open ? setup() : cleanup()` 을 `if/else` 로 교정.
8
+ - `URating`/`USelect`/`UTree`: `switch` case 블록 안의 `const` 선언이 블록 스코프를 벗어나 다른 case 로 누출될 수 있던 형태를 중괄호 블록으로 격리(`no-case-declarations`).
9
+
10
+ ### Changed
11
+ - **이 패키지의 eslint 가 실제로 동작하기 시작했다.** `eslint.config.js` 의 두 결함 — (1) `files: ["src/**/*"]` 가 ESLint 9 에서 universal 패턴으로 취급돼 `.ts` 를 린팅 대상으로 opt-in 하지 못함, (2) 배열 프리셋(`tseslint.configs.recommended`)을 객체 스프레드해 프리셋이 통째로 무력화됨 — 을 수정했다. `build` 스크립트에 `eslint &&` 게이트가 있었으나 매칭 파일이 0개라 **항상 통과**하고 있었다. 위 결함들은 모두 이 복구로 처음 드러난 것이다.
12
+ - `npm run lint` / `npm run lint:fix` 스크립트 추가(flex-table·u-widgets 와 통일).
13
+ - 내부 타입 정밀화: `Dialog`/`Theme`/`UTooltip`/`UInput`/`UTextarea` 의 `any` 캐스팅을 실제 타입(`CloseOnPolicy[]`, `UInput`, `InputType`, `VirtualElement`, `unknown[]`)으로 교체. 공개 API 시그니처 변경 없음.
14
+
15
+ ## [1.7.1] - 2026-07-19
16
+
17
+ ### Fixed
18
+ - `UInput.type` 을 host 요소로 **reflect** 하도록 수정 — 미반영 시 `u-input[type="number"]::part(input)` 같은 속성 셀렉터가 HTML 속성으로 준 경우에만 매칭되고, React/Lit 의 property 바인딩(`.type=`, `el.type=`)에서는 host 에 속성이 나타나지 않아 무효였다. 형제 컴포넌트 `URadio.type` 은 이미 reflect 하고 있어 리포 내 비일관이기도 했다.
19
+
20
+ ### Documentation
21
+ - `docs/theming.md` 에 `::part()` 커스터마이즈 섹션 신설 — 텍스트 정렬 레시피(`text-align` + `font-variant-numeric: tabular-nums`), 숫자 입력 우측정렬을 기본값으로 두지 않는 근거, 비반영 속성용 클래스 셀렉터 대안, 숫자 포맷팅 책임 범위.
22
+ - `docs/architecture.md` CSS Parts 절에서 `theming.md` 로 상호 링크. `UInput` 의 `@csspart input` JSDoc 보강.
23
+
3
24
  ## [1.7.0] - 2026-07-17
4
25
 
5
26
  ### Fixed
@@ -48,7 +48,8 @@ var UOverlayElement = class extends UElement {
48
48
  }
49
49
  updated(changedProperties) {
50
50
  super.updated(changedProperties);
51
- if (changedProperties.has("open")) this.open ? this.setup() : this.cleanup();
51
+ if (changedProperties.has("open")) if (this.open) this.setup();
52
+ else this.cleanup();
52
53
  }
53
54
  /**
54
55
  * 오버레이를 표시합니다. u-show 이벤트가 취소되면 열리지 않습니다.
@@ -16,7 +16,11 @@ export type InputVariant = 'outlined' | 'filled' | 'underlined' | 'borderless';
16
16
  *
17
17
  * @csspart field - u-field 요소
18
18
  * @csspart container - input과 prefix/suffix를 감싸는 컨테이너
19
- * @csspart input - 네이티브 input 요소
19
+ * @csspart input - 네이티브 input 요소.
20
+ * text-align 등 표현 관련 속성을 지정하지 않으므로 소비앱이 ::part(input)으로 재정의한다.
21
+ * 예: `u-input[type="number"]::part(input) { text-align: right; font-variant-numeric: tabular-nums; }`
22
+ * (숫자 우측정렬은 값이 열로 쌓여 자릿수를 비교할 때 유효하므로 라이브러리 기본값으로 두지 않는다.
23
+ * docs/theming.md 참고)
20
24
  * @csspart popover - 드롭다운 팝오버 요소
21
25
  *
22
26
  * @cssprop --input-popover-width - 드롭다운 팝오버의 너비 (기본값: 앵커(트리거) 너비)
@@ -32,7 +36,7 @@ export declare class UInput extends UFormControlElement<string> {
32
36
  variant: InputVariant;
33
37
  /** 전체 지우기 버튼 표시 여부 */
34
38
  clearable: boolean;
35
- /** input 요소의 type 속성 */
39
+ /** input 요소의 type 속성. 외부 `u-input[type="..."]` 스타일 훅을 위해 reflect 한다 (URadio.type 과 동일). */
36
40
  type: InputType;
37
41
  /** 최소 글자 수 */
38
42
  minlength?: number;
@@ -276,7 +276,10 @@ __decorate([property({
276
276
  type: Boolean,
277
277
  reflect: true
278
278
  }), __decorateMetadata("design:type", Boolean)], UInput.prototype, "clearable", void 0);
279
- __decorate([property({ type: String }), __decorateMetadata("design:type", Object)], UInput.prototype, "type", void 0);
279
+ __decorate([property({
280
+ type: String,
281
+ reflect: true
282
+ }), __decorateMetadata("design:type", Object)], UInput.prototype, "type", void 0);
280
283
  __decorate([property({ type: Number }), __decorateMetadata("design:type", Number)], UInput.prototype, "minlength", void 0);
281
284
  __decorate([property({ type: Number }), __decorateMetadata("design:type", Number)], UInput.prototype, "maxlength", void 0);
282
285
  __decorate([property({ type: String }), __decorateMetadata("design:type", String)], UInput.prototype, "min", void 0);
@@ -119,11 +119,12 @@ var UTree = class UTree extends UElement {
119
119
  items[items.length - 1]?.focus();
120
120
  break;
121
121
  case "Enter":
122
- case " ":
122
+ case " ": {
123
123
  e.preventDefault();
124
124
  const header = focused.shadowRoot?.querySelector(".header");
125
125
  if (header) header.click();
126
126
  break;
127
+ }
127
128
  }
128
129
  };
129
130
  }
@@ -81,39 +81,46 @@ var Dialog = class {
81
81
  * 커스텀 다이얼로그를 표시합니다.
82
82
  * @returns 클릭된 액션의 value 또는 닫힌 경우 null
83
83
  */
84
- static show(options) {
85
- return new Promise(async (resolve) => {
86
- let closeValue = null;
87
- const dialog = this.createDialog(options);
88
- const content = typeof options.content === "string" ? unsafeHTML(options.content) : options.content;
89
- const actions = options.actions && options.actions.length > 0 ? options.actions : null;
90
- render(html`
91
- <div style="min-width: 320px; font-size: 14px; line-height: 1.6;">
92
- ${content}
93
- ${actions ? html`
94
- <div style="display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px;">
95
- ${actions.map((action) => html`
96
- <u-button
97
- variant=${action.variant || "solid"}
98
- @click=${() => {
99
- closeValue = action.value;
100
- dialog.hide();
101
- }}
102
- >${action.label}</u-button>
103
- `)}
104
- </div>
105
- ` : nothing}
106
- </div>
107
- `, dialog);
108
- (options.target || document.body).appendChild(dialog);
109
- await dialog.updateComplete;
110
- dialog.show();
84
+ static async show(options) {
85
+ let closeValue = null;
86
+ const dialog = this.createDialog(options);
87
+ const content = typeof options.content === "string" ? unsafeHTML(options.content) : options.content;
88
+ const actions = options.actions && options.actions.length > 0 ? options.actions : null;
89
+ render(html`
90
+ <div style="min-width: 320px; font-size: 14px; line-height: 1.6;">
91
+ ${content}
92
+ ${actions ? html`
93
+ <div style="display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px;">
94
+ ${actions.map((action) => html`
95
+ <u-button
96
+ variant=${action.variant || "solid"}
97
+ @click=${() => {
98
+ closeValue = action.value;
99
+ dialog.hide();
100
+ }}
101
+ >${action.label}</u-button>
102
+ `)}
103
+ </div>
104
+ ` : nothing}
105
+ </div>
106
+ `, dialog);
107
+ (options.target || document.body).appendChild(dialog);
108
+ const closed = new Promise((resolve) => {
111
109
  dialog.addEventListener("hide", (e) => {
112
110
  if (e.target !== dialog) return;
113
111
  setTimeout(() => dialog.remove(), 300);
114
112
  resolve(closeValue);
115
113
  });
116
114
  });
115
+ try {
116
+ await dialog.updateComplete;
117
+ } catch (error) {
118
+ console.error("[Dialog] 다이얼로그를 표시하지 못했습니다.", error);
119
+ dialog.remove();
120
+ return null;
121
+ }
122
+ dialog.show();
123
+ return closed;
117
124
  }
118
125
  /** 다이얼로그 엘리먼트를 생성하고 옵션을 적용합니다. */
119
126
  static createDialog(options) {
@@ -51,7 +51,7 @@ var Theme = class {
51
51
  }
52
52
  if (options?.useBuiltIn ?? true) {
53
53
  this.log("Import enabled: loading styles via internal assets");
54
- for (let [name, module] of internalStyleBundle) {
54
+ for (const [name, module] of internalStyleBundle) {
55
55
  const style = document.createElement("style");
56
56
  style.setAttribute("data-name", name);
57
57
  style.textContent = module;
@@ -189,7 +189,7 @@ IconRegistry.register("bootstrap", async (name) => {
189
189
  const svg = await response.text();
190
190
  IconCache.set("bootstrap", name, svg);
191
191
  return svg;
192
- } catch (error) {
192
+ } catch {
193
193
  return;
194
194
  }
195
195
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@iyulab/components",
3
3
  "description": "web-components library based on lit-element made by iyulab",
4
- "version": "1.7.0",
4
+ "version": "1.7.2",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "components",
@@ -42,6 +42,8 @@
42
42
  "start": "vite --force",
43
43
  "test": "vitest run",
44
44
  "test:browser": "vitest run --project=browser",
45
+ "lint": "eslint src/",
46
+ "lint:fix": "eslint src/ --fix",
45
47
  "build:plugins": "tsc -p plugins/tsconfig.json",
46
48
  "build": "eslint && vite build && npm run build:plugins"
47
49
  },