@iyulab/components 0.1.10 → 0.1.11

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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.11 (2026-01-06)
4
+ - Fixed d.ts generation error for `Theme` class by exporting the class
5
+ - Fixed `UTreeItem.childrenItems` accessibility for `UTree` component
6
+ - Changed private field to public readonly getter for d.ts compatibility
7
+ - Fixed `UTree.getAllTreeItems` parameter type to accept readonly arrays
8
+
3
9
  ## 0.1.10 (2025-12-19)
4
10
  - Fixed circular dependency issue between `UMenu` and `UMenuItem`
5
11
  - Changed `UMenuItem` to use tagName check instead of `instanceof` for submenu detection
@@ -55,8 +55,8 @@ class FloatingElement extends BaseElement {
55
55
  updated(changedProperties) {
56
56
  super.updated(changedProperties);
57
57
  if (changedProperties.has("visible")) {
58
- this.toggleAttribute("inert", !this.visible);
59
58
  this.toggleAttribute("aria-hidden", !this.visible);
59
+ this.toggleAttribute("inert", !this.visible);
60
60
  this.updateVisibleState(this.visible);
61
61
  }
62
62
  }
@@ -7,15 +7,11 @@ const styles = css`
7
7
  top: 0;
8
8
  left: 0;
9
9
  width: max-content;
10
-
11
10
  opacity: 0;
12
- pointer-events: none;
13
11
  }
14
12
  :host([visible]) {
15
13
  opacity: 1;
16
- pointer-events: auto;
17
14
  }
18
-
19
15
  :host([strategy="absolute"]) {
20
16
  position: absolute;
21
17
  }
@@ -1,25 +1,10 @@
1
- import { nothing } from 'lit';
2
1
  import { BaseElement } from '../BaseElement.js';
3
2
  /**
4
- * Divider 컴포넌트는 엘리먼트 사이에 움직임이 가능한 구분선을 제공합니다.
3
+ * Divider 컴포넌트는 엘리먼트 사이에 구분선을 제공합니다.
5
4
  */
6
5
  export declare class UDivider extends BaseElement {
7
6
  static styles: import('lit').CSSResultGroup[];
8
7
  static dependencies: Record<string, typeof BaseElement>;
9
- /** 마지막 마우스 포인터 위치를 저장합니다. */
10
- private prevPointerPosition;
11
- /** 분할선이 움직이는지 여부를 나타냅니다. */
12
- moving: boolean;
13
- /** 분할 방향을 설정합니다. */
14
- orientation: 'horizontal' | 'vertical';
15
- /** 분할선을 움직일 수 있는지 여부를 설정합니다. */
16
- movable: boolean;
17
- disconnectedCallback(): void;
18
- render(): typeof nothing | import('lit-html').TemplateResult<1>;
19
- /** 마우스 다운 이벤트 핸들러 */
20
- private handleMousedown;
21
- /** 마우스 무브 이벤트 핸들러 */
22
- private handleMousemove;
23
- /** 마우스 업 이벤트 핸들러 */
24
- private handleMouseup;
8
+ /** 수직 방향 여부를 설정합니다. (기본값: 수평) */
9
+ vertical: boolean;
25
10
  }
@@ -1,5 +1,4 @@
1
- import { html, nothing } from 'lit';
2
- import { state, property } from 'lit/decorators.js';
1
+ import { property } from 'lit/decorators.js';
3
2
  import { BaseElement } from '../BaseElement.js';
4
3
  import { styles } from './UDivider.styles.js';
5
4
 
@@ -15,41 +14,7 @@ var __decorateClass = (decorators, target, key, kind) => {
15
14
  class UDivider extends BaseElement {
16
15
  constructor() {
17
16
  super(...arguments);
18
- /** 마지막 마우스 포인터 위치를 저장합니다. */
19
- this.prevPointerPosition = 0;
20
- this.moving = false;
21
- this.orientation = "horizontal";
22
- this.movable = false;
23
- /** 마우스 다운 이벤트 핸들러 */
24
- this.handleMousedown = (e) => {
25
- e.preventDefault();
26
- this.moving = true;
27
- this.prevPointerPosition = this.orientation === "horizontal" ? e.clientX : e.clientY;
28
- this.emit("u-movestart");
29
- document.addEventListener("mousemove", this.handleMousemove);
30
- document.addEventListener("mouseup", this.handleMouseup);
31
- document.body.style.userSelect = "none";
32
- document.body.style.cursor = this.orientation === "horizontal" ? "col-resize" : "row-resize";
33
- };
34
- /** 마우스 무브 이벤트 핸들러 */
35
- this.handleMousemove = (e) => {
36
- if (!this.moving) return;
37
- e.preventDefault();
38
- const currentPointerPosition = this.orientation === "horizontal" ? e.clientX : e.clientY;
39
- const delta = currentPointerPosition - this.prevPointerPosition;
40
- this.emit("u-move", { delta });
41
- this.prevPointerPosition = currentPointerPosition;
42
- };
43
- /** 마우스 업 이벤트 핸들러 */
44
- this.handleMouseup = (_) => {
45
- if (!this.moving) return;
46
- this.moving = false;
47
- this.emit("u-moveend");
48
- document.removeEventListener("mousemove", this.handleMousemove);
49
- document.removeEventListener("mouseup", this.handleMouseup);
50
- document.body.style.userSelect = "";
51
- document.body.style.cursor = "";
52
- };
17
+ this.vertical = false;
53
18
  }
54
19
  static {
55
20
  this.styles = [super.styles, styles];
@@ -57,33 +22,9 @@ class UDivider extends BaseElement {
57
22
  static {
58
23
  this.dependencies = {};
59
24
  }
60
- disconnectedCallback() {
61
- super.disconnectedCallback();
62
- document.removeEventListener("mousemove", this.handleMousemove);
63
- document.removeEventListener("mouseup", this.handleMouseup);
64
- }
65
- render() {
66
- if (this.movable) {
67
- return html`
68
- <div class="handler" part="handler"
69
- ?moving=${this.moving}
70
- orientation=${this.orientation}
71
- @mousedown=${this.handleMousedown}
72
- ></div>
73
- `;
74
- } else {
75
- return nothing;
76
- }
77
- }
78
25
  }
79
- __decorateClass([
80
- state()
81
- ], UDivider.prototype, "moving");
82
- __decorateClass([
83
- property({ type: String, reflect: true })
84
- ], UDivider.prototype, "orientation");
85
26
  __decorateClass([
86
27
  property({ type: Boolean, reflect: true })
87
- ], UDivider.prototype, "movable");
28
+ ], UDivider.prototype, "vertical");
88
29
 
89
30
  export { UDivider };
@@ -2,53 +2,21 @@ import { css } from 'lit';
2
2
 
3
3
  const styles = css`
4
4
  :host {
5
- --divider-size: 2px;
6
- --handler-size: 4px;
7
- --handler-color: var(--u-blue-500, #3b82f6);
5
+ --divider-size: 1px;
6
+ --divider-color: var(--u-neutral-200, #e5e7eb);
7
+ --divider-spacing: 8px;
8
8
  }
9
9
 
10
10
  :host {
11
- position: relative;
12
- background-color: var(--u-neutral-200, #e5e7eb);
13
- }
14
- :host([orientation="horizontal"]) {
15
- width: var(--divider-size, 2px);
16
- min-height: 0px;
17
- }
18
- :host([orientation="vertical"]) {
19
- min-width: 0px;
20
- height: var(--divider-size, 2px);
21
- }
22
-
23
- .handler {
24
- position: absolute;
25
- z-index: 100;
26
11
  display: block;
27
- background-color: var(--handler-color, #3b82f6);
28
- opacity: 0;
29
- transition: opacity 0.2s ease;
30
- }
31
- .handler[orientation="horizontal"] {
32
- top: 0;
33
- bottom: 0;
34
- left: 50%;
35
- transform: translateX(-50%);
36
- width: var(--handler-size, 4px);
37
- height: 100%;
38
- cursor: col-resize;
39
- }
40
- .handler[orientation="vertical"] {
41
- left: 0;
42
- right: 0;
43
- top: 50%;
44
- transform: translateY(-50%);
45
- width: 100%;
46
- height: var(--handler-size, 4px);
47
- cursor: row-resize;
48
- }
49
- .handler[moving],
50
- .handler:hover {
51
- opacity: 1;
12
+ background-color: var(--divider-color);
13
+ height: var(--divider-size);
14
+ margin: var(--divider-spacing) 0;
15
+ }
16
+ :host([vertical]) {
17
+ width: var(--divider-size);
18
+ height: auto;
19
+ margin: 0 var(--divider-spacing);
52
20
  }
53
21
  `;
54
22
 
@@ -1,3 +1,4 @@
1
+ import { nothing } from 'lit';
1
2
  import { BaseElement } from '../BaseElement.js';
2
3
  export type IconLibrary = "internal" | "default" | (string & {});
3
4
  /**
@@ -18,7 +19,9 @@ export declare class UIcon extends BaseElement {
18
19
  * 사용할 아이콘의 이름을 지정합니다.
19
20
  */
20
21
  name?: string;
21
- render(): import('lit-html/directive.js').DirectiveResult<typeof import('lit-html/directives/until.js').UntilDirective>;
22
+ render(): import('lit-html/directive.js').DirectiveResult<{
23
+ new (_partInfo: import('lit-html/directive.js').PartInfo): import('lit-html/directives/until.js').UntilDirective<typeof nothing | Promise<import('lit-html/directive.js').DirectiveResult<typeof import('lit-html/directives/unsafe-html.js').UnsafeHTMLDirective>>>;
24
+ }>;
22
25
  /**
23
26
  * SVG 콘텐츠가 유효한 HTML인지 검사합니다.
24
27
  */
@@ -17,7 +17,6 @@ export declare class UMenu extends FloatingElement {
17
17
  * - default: 일반 메뉴 (수동 제어)
18
18
  * - dropdown: 드롭다운 메뉴 (앵커 클릭 시 토글)
19
19
  * - contextmenu: 컨텍스트 메뉴 (앵커 우클릭 시 표시)
20
- * - submenu: 서브메뉴 (앵커 호버/포커스 시 표시)
21
20
  */
22
21
  type: MenuType;
23
22
  /**
@@ -57,14 +56,12 @@ export declare class UMenu extends FloatingElement {
57
56
  private handleWindowFocusOut;
58
57
  private handleWindowKeydown;
59
58
  private handleWindowPointerDown;
60
- private handleAnchorPointerEnter;
61
- private handleAnchorPointerLeave;
62
- private handleAnchorFocusIn;
63
- private handleAnchorFocusOut;
64
59
  private handleAnchorPointerDown;
65
60
  private handleAnchorKeydown;
66
61
  private handleAnchorContextMenu;
67
- /** 이벤트로부터 활성화된 (비활성화되지 않은) 메뉴 아이템을 반환 */
68
- private getEnabledItemFrom;
62
+ private handleSubmenuAnchorPointerEnter;
63
+ private handleSubmenuAnchorPointerLeave;
64
+ private handleSubmenuAnchorFocusIn;
65
+ private handleSubmenuAnchorFocusOut;
69
66
  }
70
67
  export {};
@@ -24,7 +24,7 @@ class UMenu extends FloatingElement {
24
24
  */
25
25
  this.focusAt = async (index = 0) => {
26
26
  await this.updateComplete;
27
- const items = this.items || [];
27
+ const items = this.getItems("enabled");
28
28
  const length = items.length;
29
29
  if (length === 0) return;
30
30
  index = index < 0 ? Math.max(0, length + index) : Math.min(index, length - 1);
@@ -34,8 +34,10 @@ class UMenu extends FloatingElement {
34
34
  };
35
35
  //#region 기본 이벤트 핸들러
36
36
  this.handleClick = async (e) => {
37
- const item = this.getEnabledItemFrom(e);
38
- if (!item || item.isNested) return;
37
+ const item = e.target;
38
+ if (!(item instanceof UMenuItem)) return;
39
+ if (item.disabled) return;
40
+ if (item.submenu) return;
39
41
  const items = this.getItems("enabled");
40
42
  if (!items.includes(item)) return;
41
43
  const value = item.value || "";
@@ -56,8 +58,9 @@ class UMenu extends FloatingElement {
56
58
  this.emit("u-select", { value });
57
59
  };
58
60
  this.handleKeydown = async (e) => {
59
- const item = this.getEnabledItemFrom(e);
60
- if (!item) return;
61
+ const item = e.target;
62
+ if (!(item instanceof UMenuItem)) return;
63
+ if (item.disabled) return;
61
64
  const items = this.getItems("enabled");
62
65
  if (!items.includes(item)) return;
63
66
  const currentIndex = items.findIndex((i) => i === item);
@@ -87,18 +90,19 @@ class UMenu extends FloatingElement {
87
90
  await this.focusAt(nextIndex);
88
91
  break;
89
92
  case "ArrowLeft":
90
- e.preventDefault();
91
- e.stopPropagation();
92
93
  if (this.type === "submenu") {
94
+ e.preventDefault();
95
+ e.stopPropagation();
96
+ await this.hide();
93
97
  this.anchor?.focus();
94
98
  }
95
99
  break;
96
100
  case "ArrowRight":
97
- e.preventDefault();
98
- e.stopPropagation();
99
- const menu = item.querySelector("u-menu");
100
- if (menu?.type === "submenu") {
101
- await menu?.focusAt(0);
101
+ if (item.submenu) {
102
+ e.preventDefault();
103
+ e.stopPropagation();
104
+ await item.submenu.show();
105
+ await item.submenu.focusAt(0);
102
106
  }
103
107
  break;
104
108
  }
@@ -131,28 +135,6 @@ class UMenu extends FloatingElement {
131
135
  }
132
136
  };
133
137
  //#endregion
134
- //#region 'submenu' 이벤트 핸들러
135
- this.handleAnchorPointerEnter = (e) => {
136
- const item = this.getEnabledItemFrom(e);
137
- if (!item) return;
138
- this.show();
139
- };
140
- this.handleAnchorPointerLeave = async (e) => {
141
- const item = this.getEnabledItemFrom(e);
142
- if (!item) return;
143
- this.hide();
144
- };
145
- this.handleAnchorFocusIn = (e) => {
146
- const item = this.getEnabledItemFrom(e);
147
- if (!item) return;
148
- this.show();
149
- };
150
- this.handleAnchorFocusOut = async (e) => {
151
- const item = this.getEnabledItemFrom(e);
152
- if (!item) return;
153
- this.hide();
154
- };
155
- //#endregion
156
138
  //#region 'dropdown' 이벤트 핸들러
157
139
  this.handleAnchorPointerDown = async (e) => {
158
140
  if (e.button !== 0) return;
@@ -187,14 +169,40 @@ class UMenu extends FloatingElement {
187
169
  };
188
170
  await this.show(virtual, false);
189
171
  };
172
+ //#endregion
173
+ //#region 'submenu' 이벤트 핸들러
174
+ this.handleSubmenuAnchorPointerEnter = () => {
175
+ const anchor = this.anchor;
176
+ if (anchor?.disabled) return;
177
+ this.show();
178
+ };
179
+ this.handleSubmenuAnchorPointerLeave = (e) => {
180
+ const related = e.relatedTarget;
181
+ if (related) {
182
+ const anchor = this.anchor;
183
+ if (anchor?.contains(related) || this.contains(related)) return;
184
+ }
185
+ this.hide();
186
+ };
187
+ this.handleSubmenuAnchorFocusIn = () => {
188
+ const anchor = this.anchor;
189
+ if (anchor?.disabled) return;
190
+ this.show();
191
+ };
192
+ this.handleSubmenuAnchorFocusOut = (e) => {
193
+ const related = e.relatedTarget;
194
+ if (related) {
195
+ const anchor = this.anchor;
196
+ if (anchor?.contains(related) || this.contains(related)) return;
197
+ }
198
+ this.hide();
199
+ };
190
200
  }
191
201
  static {
192
202
  this.styles = [super.styles, styles];
193
203
  }
194
204
  static {
195
- this.dependencies = {
196
- "u-menu-item": UMenuItem
197
- };
205
+ this.dependencies = {};
198
206
  }
199
207
  connectedCallback() {
200
208
  super.connectedCallback();
@@ -228,13 +236,12 @@ class UMenu extends FloatingElement {
228
236
  * 재귀적으로 서브메뉴의 아이템들도 해제됩니다.
229
237
  */
230
238
  clearAll() {
231
- const items = this.items || [];
239
+ const items = this.getItems("all");
232
240
  for (const item of items) {
233
241
  item.selected = false;
234
242
  item.checked = false;
235
- if (item.isNested) {
236
- const submenu = item.querySelector("u-menu");
237
- submenu?.clearAll();
243
+ if (item.submenu) {
244
+ item.submenu.clearAll();
238
245
  }
239
246
  }
240
247
  }
@@ -245,7 +252,7 @@ class UMenu extends FloatingElement {
245
252
  * @return 조건에 맞는 아이템들의 배열입니다
246
253
  */
247
254
  getItems(filter = "all") {
248
- const items = this.items || [];
255
+ const items = this.items?.filter((item) => item instanceof UMenuItem) || [];
249
256
  switch (filter) {
250
257
  case "all":
251
258
  return items;
@@ -267,15 +274,7 @@ class UMenu extends FloatingElement {
267
274
  }
268
275
  /** 앵커에 이벤트 바인딩 */
269
276
  bind(type, target) {
270
- if (type === "submenu") {
271
- this.visible = false;
272
- this.placement ||= "right-start";
273
- this.offset = -8;
274
- target.addEventListener("pointerenter", this.handleAnchorPointerEnter);
275
- target.addEventListener("pointerleave", this.handleAnchorPointerLeave);
276
- target.addEventListener("focusin", this.handleAnchorFocusIn);
277
- target.addEventListener("focusout", this.handleAnchorFocusOut);
278
- } else if (type === "dropdown") {
277
+ if (type === "dropdown") {
279
278
  this.visible = false;
280
279
  this.placement ||= "bottom-start";
281
280
  target.addEventListener("pointerdown", this.handleAnchorPointerDown);
@@ -284,6 +283,13 @@ class UMenu extends FloatingElement {
284
283
  this.visible = false;
285
284
  this.placement ||= "bottom-start";
286
285
  target.addEventListener("contextmenu", this.handleAnchorContextMenu);
286
+ } else if (type === "submenu") {
287
+ this.visible = false;
288
+ this.placement ||= "right-start";
289
+ target.addEventListener("pointerenter", this.handleSubmenuAnchorPointerEnter);
290
+ target.addEventListener("pointerleave", this.handleSubmenuAnchorPointerLeave);
291
+ target.addEventListener("focusin", this.handleSubmenuAnchorFocusIn);
292
+ target.addEventListener("focusout", this.handleSubmenuAnchorFocusOut);
287
293
  } else {
288
294
  this.visible = true;
289
295
  return;
@@ -297,25 +303,18 @@ class UMenu extends FloatingElement {
297
303
  window.removeEventListener("focusout", this.handleWindowFocusOut);
298
304
  window.removeEventListener("keydown", this.handleWindowKeydown);
299
305
  window.removeEventListener("pointerdown", this.handleWindowPointerDown);
300
- target.removeEventListener("pointerenter", this.handleAnchorPointerEnter);
301
- target.removeEventListener("pointerleave", this.handleAnchorPointerLeave);
302
- target.removeEventListener("focusin", this.handleAnchorFocusIn);
303
- target.removeEventListener("focusout", this.handleAnchorFocusOut);
304
306
  target.removeEventListener("pointerdown", this.handleAnchorPointerDown);
305
307
  target.removeEventListener("keydown", this.handleAnchorKeydown);
306
308
  target.removeEventListener("contextmenu", this.handleAnchorContextMenu);
309
+ target.removeEventListener("pointerenter", this.handleSubmenuAnchorPointerEnter);
310
+ target.removeEventListener("pointerleave", this.handleSubmenuAnchorPointerLeave);
311
+ target.removeEventListener("focusin", this.handleSubmenuAnchorFocusIn);
312
+ target.removeEventListener("focusout", this.handleSubmenuAnchorFocusOut);
307
313
  }
308
314
  //#endregion
309
- /** 이벤트로부터 활성화된 (비활성화되지 않은) 메뉴 아이템을 반환 */
310
- getEnabledItemFrom(event) {
311
- const target = event.target;
312
- const item = target.closest("u-menu-item");
313
- if (!item || item.disabled) return null;
314
- return item;
315
- }
316
315
  }
317
316
  __decorateClass([
318
- queryAssignedElements({ flatten: false, selector: "u-menu-item" })
317
+ queryAssignedElements({ flatten: false })
319
318
  ], UMenu.prototype, "items");
320
319
  __decorateClass([
321
320
  property({ type: String, reflect: true })
@@ -22,7 +22,7 @@ const styles = css`
22
22
  opacity: 1;
23
23
  }
24
24
 
25
- /* dropdown/contextmenu/submenu 타입: 플로팅 메뉴 */
25
+ /* dropdown/contextmenu 타입: 플로팅 메뉴 */
26
26
  :host([type="dropdown"]),
27
27
  :host([type="contextmenu"]),
28
28
  :host([type="submenu"]) {
@@ -1,12 +1,15 @@
1
1
  import { PropertyValues } from 'lit';
2
2
  import { BaseElement } from '../BaseElement.js';
3
+ import { UMenu } from '../menu/UMenu.component.js';
3
4
  export declare class UMenuItem extends BaseElement {
4
5
  static styles: import('lit').CSSResultGroup[];
5
6
  static dependencies: Record<string, typeof BaseElement>;
6
- /** 중첩된 메뉴 아이템인지 여부 */
7
- isNested: boolean;
7
+ /** 서브메뉴 요소 */
8
+ submenu: UMenu | null;
8
9
  /** 비활성화 여부 @default false */
9
10
  disabled: boolean;
11
+ /** 로딩 상태 @default false */
12
+ loading: boolean;
10
13
  /** 선택 여부(multiple mode) @default false */
11
14
  checked: boolean;
12
15
  /** 선택 여부(single mode) @default false */
@@ -15,5 +18,8 @@ export declare class UMenuItem extends BaseElement {
15
18
  value: string;
16
19
  protected updated(changedProperties: PropertyValues): void;
17
20
  render(): import('lit-html').TemplateResult<1>;
21
+ /** 기본 슬롯 변경 감지 */
22
+ private handleSlotChange;
23
+ /** submenu 슬롯 변경 감지 */
18
24
  private handleSubmenuSlotChange;
19
25
  }
@@ -1,6 +1,9 @@
1
1
  import { html } from 'lit';
2
2
  import { state, property } from 'lit/decorators.js';
3
3
  import { BaseElement } from '../BaseElement.js';
4
+ import { UIcon } from '../icon/UIcon.component.js';
5
+ import { UMenu } from '../menu/UMenu.component.js';
6
+ import { USpinner } from '../spinner/USpinner.component.js';
4
7
  import { styles } from './UMenuItem.styles.js';
5
8
 
6
9
  var __defProp = Object.defineProperty;
@@ -15,27 +18,30 @@ var __decorateClass = (decorators, target, key, kind) => {
15
18
  class UMenuItem extends BaseElement {
16
19
  constructor() {
17
20
  super(...arguments);
18
- this.isNested = false;
21
+ this.submenu = null;
19
22
  this.disabled = false;
23
+ this.loading = false;
20
24
  this.checked = false;
21
25
  this.selected = false;
22
26
  this.value = "";
27
+ /** 기본 슬롯 변경 감지 */
28
+ this.handleSlotChange = (e) => {
29
+ const slot = e.target;
30
+ const elements = slot.assignedElements({ flatten: true });
31
+ const menu = elements.find((el) => el instanceof UMenu);
32
+ menu?.setAttribute("slot", "submenu");
33
+ };
34
+ /** submenu 슬롯 변경 감지 */
23
35
  this.handleSubmenuSlotChange = (e) => {
24
36
  const slot = e.target;
25
37
  const elements = slot.assignedElements({ flatten: true });
26
- if (elements.length !== 1) {
27
- console.warn(`when using submenu, there must be exactly one UMenu component in the 'submenu' slot.`);
28
- this.isNested = false;
38
+ const menu = elements.find((el) => el instanceof UMenu);
39
+ if (menu) {
40
+ menu.type = "submenu";
41
+ menu.anchor = this;
42
+ this.submenu = menu;
29
43
  } else {
30
- const submenu = elements[0];
31
- if ("anchor" in submenu && "type" in submenu) {
32
- submenu.anchor = this;
33
- submenu.type = "submenu";
34
- this.isNested = true;
35
- } else {
36
- console.warn(`the element assigned to the 'submenu' slot is not a valid UMenu component.`);
37
- this.isNested = false;
38
- }
44
+ this.submenu = null;
39
45
  }
40
46
  };
41
47
  }
@@ -43,13 +49,15 @@ class UMenuItem extends BaseElement {
43
49
  this.styles = [super.styles, styles];
44
50
  }
45
51
  static {
46
- this.dependencies = {};
52
+ this.dependencies = {
53
+ "u-icon": UIcon,
54
+ "u-spinner": USpinner
55
+ };
47
56
  }
48
57
  updated(changedProperties) {
49
58
  super.updated(changedProperties);
50
59
  if (changedProperties.has("disabled")) {
51
- const tabIndex = this.disabled ? "-1" : "0";
52
- this.setAttribute("tabindex", tabIndex);
60
+ this.setAttribute("tabindex", this.disabled ? "-1" : "0");
53
61
  }
54
62
  }
55
63
  render() {
@@ -57,36 +65,40 @@ class UMenuItem extends BaseElement {
57
65
  <u-icon class="prefix icon"
58
66
  ?hidden=${!this.checked}
59
67
  lib="internal"
60
- name="check-lg"
68
+ name="check"
61
69
  ></u-icon>
62
70
 
71
+ <u-spinner class="prefix icon"
72
+ ?hidden=${!this.loading}
73
+ ></u-spinner>
74
+
63
75
  <slot name="prefix"></slot>
64
76
 
65
- <span class="content">
66
- <slot></slot>
77
+ <span class="label">
78
+ <slot @slotchange=${this.handleSlotChange}></slot>
67
79
  </span>
68
80
 
69
81
  <slot name="suffix"></slot>
70
82
 
71
83
  <u-icon class="suffix icon"
72
- ?hidden=${!this.isNested}
84
+ ?hidden=${this.submenu === null}
73
85
  lib="internal"
74
86
  name="chevron-right"
75
87
  ></u-icon>
76
88
 
77
- <slot name="submenu"
78
- ?hidden=${!this.isNested}
79
- @slotchange=${this.handleSubmenuSlotChange}
80
- ></slot>
89
+ <slot name="submenu" @slotchange=${this.handleSubmenuSlotChange}></slot>
81
90
  `;
82
91
  }
83
92
  }
84
93
  __decorateClass([
85
94
  state()
86
- ], UMenuItem.prototype, "isNested");
95
+ ], UMenuItem.prototype, "submenu");
87
96
  __decorateClass([
88
97
  property({ type: Boolean, reflect: true })
89
98
  ], UMenuItem.prototype, "disabled");
99
+ __decorateClass([
100
+ property({ type: Boolean, reflect: true })
101
+ ], UMenuItem.prototype, "loading");
90
102
  __decorateClass([
91
103
  property({ type: Boolean, reflect: true })
92
104
  ], UMenuItem.prototype, "checked");
@@ -22,7 +22,7 @@ const styles = css`
22
22
  pointer-events: none;
23
23
  cursor: not-allowed;
24
24
  }
25
- :host(:not([disabled])[selected]) {
25
+ :host([selected]) {
26
26
  color: var(--selected-color);
27
27
  background-color: var(--selected-bg-color);
28
28
  }
@@ -31,9 +31,9 @@ const styles = css`
31
31
  background-color: var(--u-bg-color-hover);
32
32
  }
33
33
 
34
- /* 콘텐츠 영역 */
35
- .content {
36
- flex: 1 0 auto;
34
+ /* 라벨 영역 */
35
+ .label {
36
+ flex: 1 1 auto;
37
37
  font-size: 1em;
38
38
  line-height: 1.5;
39
39
  overflow: hidden;
@@ -6,11 +6,17 @@ import { BaseElement } from '../BaseElement.js';
6
6
  export declare class USplitPanel extends BaseElement {
7
7
  static styles: import('lit').CSSResultGroup[];
8
8
  static dependencies: Record<string, typeof BaseElement>;
9
+ /** 현재 상태를 저장하는 패널 요소들 */
9
10
  private panels;
10
- private dividers;
11
- private panelSizes;
12
- private panelAdjustSizes;
13
- childElements: HTMLElement[];
11
+ private sizes;
12
+ /** 드래그 시작 시 저장되는 정보 */
13
+ private draggingIndex;
14
+ private dragStartSizes;
15
+ private dragStartPosition;
16
+ private containerSize;
17
+ slotEls?: HTMLElement[];
18
+ /** 스플리터 개수 (렌더링용) */
19
+ splitterCount: number;
14
20
  /** 분할 방향을 설정합니다. 'horizontal'은 좌우 분할, 'vertical'은 상하 분할입니다. */
15
21
  orientation: 'horizontal' | 'vertical';
16
22
  /** 초기 패널 크기 비율을 설정합니다. (예: [30, 70]은 첫 번째 패널이 30%, 두 번째 패널이 70%를 차지) */
@@ -22,9 +28,17 @@ export declare class USplitPanel extends BaseElement {
22
28
  disconnectedCallback(): void;
23
29
  protected willUpdate(changedProperties: PropertyValues): void;
24
30
  render(): import('lit-html').TemplateResult<1>;
25
- private initialize;
31
+ /** 슬롯 변경시 패널 초기화 */
26
32
  private handleSlotChange;
27
- /** 디바이더 드래그 이벤트 핸들러 */
28
- private handleDividerMove;
29
- private getDividerSize;
33
+ /** 패널 크기 초기화 */
34
+ private initialize;
35
+ /** 패널에 새로운 스타일 적용 */
36
+ private updatePanelStyles;
37
+ private handleSplitterMouseDown;
38
+ private handleDocumentMouseMove;
39
+ private handleDocumentMouseUp;
40
+ /** 전역 이벤트 리스너 제거 */
41
+ private removeEventListeners;
42
+ /** 거터 크기 가져오기 */
43
+ private getGutterSize;
30
44
  }
@@ -1,8 +1,7 @@
1
1
  import { html } from 'lit';
2
- import { queryAssignedElements, property } from 'lit/decorators.js';
2
+ import { queryAssignedElements, state, property } from 'lit/decorators.js';
3
3
  import { arrayAttributeConverter } from '../../internals/attribute-converters.js';
4
4
  import { BaseElement } from '../BaseElement.js';
5
- import { UDivider } from '../divider/UDivider.component.js';
6
5
  import { styles } from './USplitPanel.styles.js';
7
6
 
8
7
  var __defProp = Object.defineProperty;
@@ -17,90 +16,113 @@ var __decorateClass = (decorators, target, key, kind) => {
17
16
  class USplitPanel extends BaseElement {
18
17
  constructor() {
19
18
  super(...arguments);
19
+ /** 현재 상태를 저장하는 패널 요소들 */
20
20
  this.panels = [];
21
- this.dividers = [];
22
- this.panelSizes = [];
23
- this.panelAdjustSizes = [];
21
+ this.sizes = [];
22
+ /** 드래그 시작 시 저장되는 정보 */
23
+ this.draggingIndex = -1;
24
+ this.dragStartSizes = [];
25
+ this.dragStartPosition = 0;
26
+ this.containerSize = 0;
27
+ this.splitterCount = 0;
24
28
  this.orientation = "horizontal";
25
29
  this.initRatio = [];
26
30
  this.minSizes = [];
27
31
  this.maxSizes = [];
28
- // 초기화 메서드
29
- this.initialize = async () => {
30
- this.panels = this.childElements.filter((el) => el instanceof UDivider === false);
31
- this.dividers.forEach((divider) => divider.remove());
32
- this.dividers = [];
32
+ /** 슬롯 변경시 패널 초기화 */
33
+ this.handleSlotChange = () => {
34
+ const panels = this.slotEls || [];
35
+ if (panels.every((p, i) => p === this.panels[i])) {
36
+ return;
37
+ }
38
+ this.initialize();
39
+ };
40
+ /** 패널 및 크기 초기화 */
41
+ this.initialize = () => {
42
+ this.panels = this.slotEls || [];
43
+ if (this.panels.length === 0) {
44
+ this.splitterCount = 0;
45
+ return;
46
+ }
47
+ this.splitterCount = Math.max(0, this.panels.length - 1);
33
48
  if (this.initRatio.length === this.panels.length) {
34
- const totalRatio = this.initRatio.reduce((sum, r) => sum + r, 0);
35
- this.panelSizes = this.initRatio.map((r) => r / totalRatio * 100);
49
+ const total = this.initRatio.reduce((sum, r) => sum + r, 0);
50
+ this.sizes = this.initRatio.map((r) => r / total * 100);
36
51
  } else {
37
- const equalRatio = 100 / this.panels.length;
38
- this.panelSizes = this.panels.map(() => equalRatio);
52
+ const equalSize = 100 / this.panels.length;
53
+ this.sizes = this.panels.map(() => equalSize);
39
54
  }
40
- this.panelAdjustSizes = this.panels.map(() => 0);
41
- const property2 = this.orientation === "horizontal" ? "width" : "height";
42
- const dividerSize = this.getDividerSize();
43
- this.panels.forEach((panel, index) => {
44
- const ratio = this.panelSizes[index];
45
- panel.style.flex = "none";
46
- panel.style[property2] = index === 0 || index === this.panels.length - 1 ? `calc(${ratio}% - ${dividerSize / 2}px)` : `calc(${ratio}% - ${dividerSize}px)`;
47
- if (index === this.panels.length - 1) return;
48
- const divider = new UDivider();
49
- divider.orientation = this.orientation;
50
- divider.movable = true;
51
- divider.addEventListener("u-move", this.handleDividerMove);
52
- this.dividers.push(divider);
53
- panel.after(divider);
54
- });
55
+ this.updatePanelStyles();
55
56
  this.requestUpdate();
56
- await this.updateComplete;
57
57
  };
58
- this.handleSlotChange = async () => {
59
- const panels = this.childElements.filter((el) => el instanceof UDivider === false);
60
- if (panels.every((p, i) => p === this.panels[i])) {
61
- return;
62
- }
63
- await this.initialize();
58
+ this.handleSplitterMouseDown = (e, index) => {
59
+ if (e.button !== 0) return;
60
+ e.preventDefault();
61
+ this.draggingIndex = index;
62
+ this.dragStartPosition = this.orientation === "horizontal" ? e.clientX : e.clientY;
63
+ this.dragStartSizes = [...this.sizes];
64
+ const rect = this.getBoundingClientRect();
65
+ this.containerSize = this.orientation === "horizontal" ? rect.width : rect.height;
66
+ this.containerSize -= this.getGutterSize() * this.splitterCount;
67
+ const splitters = this.shadowRoot?.querySelectorAll(".splitter");
68
+ splitters?.[index]?.classList.add("active");
69
+ document.addEventListener("mousemove", this.handleDocumentMouseMove);
70
+ document.addEventListener("mouseup", this.handleDocumentMouseUp);
71
+ document.body.style.userSelect = "none";
72
+ document.body.style.cursor = this.orientation === "horizontal" ? "col-resize" : "row-resize";
64
73
  };
65
- /** 디바이더 드래그 이벤트 핸들러 */
66
- this.handleDividerMove = (event) => {
67
- const divider = event.target;
68
- const delta = event.detail.delta;
69
- const index = this.dividers.indexOf(divider);
70
- if (index === -1) return;
71
- const prevPanel = this.panels[index];
72
- const nextPanel = this.panels[index + 1];
73
- const property2 = this.orientation === "horizontal" ? "width" : "height";
74
- const prevPanelSize = this.panelSizes[index];
75
- const nextPanelSize = this.panelSizes[index + 1];
76
- const prevAdjSize = this.panelAdjustSizes[index] || 0;
77
- const nextAdjSize = this.panelAdjustSizes[index + 1] || 0;
78
- if (delta === 0) return;
79
- if (delta > 0) {
80
- const prevNewAdjSize = prevAdjSize + delta;
81
- const nextNewAdjSize = nextAdjSize - delta;
82
- this.panelAdjustSizes[index] = prevNewAdjSize;
83
- this.panelAdjustSizes[index + 1] = nextNewAdjSize;
84
- } else {
85
- const prevNewAdjSize = prevAdjSize + delta;
86
- const nextNewAdjSize = nextAdjSize - delta;
87
- this.panelAdjustSizes[index] = prevNewAdjSize;
88
- this.panelAdjustSizes[index + 1] = nextNewAdjSize;
74
+ this.handleDocumentMouseMove = (e) => {
75
+ if (this.draggingIndex === -1) return;
76
+ e.preventDefault();
77
+ const currentPosition = this.orientation === "horizontal" ? e.clientX : e.clientY;
78
+ const delta = currentPosition - this.dragStartPosition;
79
+ const deltaPercent = delta / this.containerSize * 100;
80
+ const aIndex = this.draggingIndex;
81
+ const bIndex = this.draggingIndex + 1;
82
+ let newASize = this.dragStartSizes[aIndex] + deltaPercent;
83
+ let newBSize = this.dragStartSizes[bIndex] - deltaPercent;
84
+ const minAPercent = (this.minSizes[aIndex] ?? 0) / this.containerSize * 100;
85
+ const minBPercent = (this.minSizes[bIndex] ?? 0) / this.containerSize * 100;
86
+ const maxAPercent = this.maxSizes[aIndex] ? this.maxSizes[aIndex] / this.containerSize * 100 : 100;
87
+ const maxBPercent = this.maxSizes[bIndex] ? this.maxSizes[bIndex] / this.containerSize * 100 : 100;
88
+ if (newASize < minAPercent) {
89
+ newASize = minAPercent;
90
+ newBSize = this.dragStartSizes[aIndex] + this.dragStartSizes[bIndex] - newASize;
91
+ }
92
+ if (newBSize < minBPercent) {
93
+ newBSize = minBPercent;
94
+ newASize = this.dragStartSizes[aIndex] + this.dragStartSizes[bIndex] - newBSize;
95
+ }
96
+ if (newASize > maxAPercent) {
97
+ newASize = maxAPercent;
98
+ newBSize = this.dragStartSizes[aIndex] + this.dragStartSizes[bIndex] - newASize;
89
99
  }
90
- prevPanel.style[property2] = `calc(${prevPanelSize}% - ${this.getDividerSize() / 2}px + ${this.panelAdjustSizes[index]}px)`;
91
- nextPanel.style[property2] = `calc(${nextPanelSize}% - ${this.getDividerSize() / 2}px + ${this.panelAdjustSizes[index + 1]}px)`;
100
+ if (newBSize > maxBPercent) {
101
+ newBSize = maxBPercent;
102
+ newASize = this.dragStartSizes[aIndex] + this.dragStartSizes[bIndex] - newBSize;
103
+ }
104
+ this.sizes[aIndex] = newASize;
105
+ this.sizes[bIndex] = newBSize;
106
+ this.updatePanelStyles();
107
+ };
108
+ this.handleDocumentMouseUp = () => {
109
+ if (this.draggingIndex === -1) return;
110
+ const splitters = this.shadowRoot?.querySelectorAll(".splitter");
111
+ splitters?.[this.draggingIndex]?.classList.remove("active");
112
+ this.draggingIndex = -1;
113
+ this.removeEventListeners();
114
+ document.body.style.userSelect = "";
115
+ document.body.style.cursor = "";
92
116
  };
93
117
  }
94
118
  static {
95
119
  this.styles = [super.styles, styles];
96
120
  }
97
121
  static {
98
- this.dependencies = {
99
- "u-divider": UDivider
100
- };
122
+ this.dependencies = {};
101
123
  }
102
124
  disconnectedCallback() {
103
- this.dividers.forEach((divider) => divider.remove());
125
+ this.removeEventListeners();
104
126
  super.disconnectedCallback();
105
127
  }
106
128
  willUpdate(changedProperties) {
@@ -111,18 +133,42 @@ class USplitPanel extends BaseElement {
111
133
  }
112
134
  render() {
113
135
  return html`
136
+ ${Array.from({ length: this.splitterCount }, (_, i) => html`
137
+ <div class="splitter" style="order: ${i * 2 + 1}"
138
+ orientation=${this.orientation}
139
+ @mousedown=${(e) => this.handleSplitterMouseDown(e, i)}>
140
+ </div>
141
+ `)}
114
142
  <slot @slotchange=${this.handleSlotChange}></slot>
115
143
  `;
116
144
  }
117
- // Get divider size from CSS variable
118
- getDividerSize() {
119
- const sizeStr = getComputedStyle(this).getPropertyValue("--divider-size").trim();
120
- return parseFloat(sizeStr) || 2;
145
+ /** 패널에 새로운 스타일 적용 */
146
+ updatePanelStyles() {
147
+ const dimension = this.orientation === "horizontal" ? "width" : "height";
148
+ const gutterSize = this.getGutterSize();
149
+ this.panels.forEach((panel, index) => {
150
+ const size = this.sizes[index];
151
+ panel.style[dimension] = `calc(${size}% - ${gutterSize * (this.splitterCount / this.panels.length)}px)`;
152
+ panel.style.order = String(index * 2);
153
+ });
154
+ }
155
+ /** 전역 이벤트 리스너 제거 */
156
+ removeEventListeners() {
157
+ document.removeEventListener("mousemove", this.handleDocumentMouseMove);
158
+ document.removeEventListener("mouseup", this.handleDocumentMouseUp);
159
+ }
160
+ /** 거터 크기 가져오기 */
161
+ getGutterSize() {
162
+ const sizeStr = getComputedStyle(this).getPropertyValue("--splitter-size").trim();
163
+ return parseFloat(sizeStr) || 4;
121
164
  }
122
165
  }
123
166
  __decorateClass([
124
167
  queryAssignedElements({ flatten: true })
125
- ], USplitPanel.prototype, "childElements");
168
+ ], USplitPanel.prototype, "slotEls");
169
+ __decorateClass([
170
+ state()
171
+ ], USplitPanel.prototype, "splitterCount");
126
172
  __decorateClass([
127
173
  property({ type: String, reflect: true })
128
174
  ], USplitPanel.prototype, "orientation");
@@ -1,6 +1,12 @@
1
1
  import { css } from 'lit';
2
2
 
3
3
  const styles = css`
4
+ :host {
5
+ --splitter-size: 4px;
6
+ --splitter-color: var(--u-neutral-200, #e5e7eb);
7
+ --splitter-active-color: var(--u-blue-500, #3b82f6);
8
+ }
9
+
4
10
  :host {
5
11
  position: relative;
6
12
  display: flex;
@@ -14,6 +20,30 @@ const styles = css`
14
20
  :host([orientation="vertical"]) {
15
21
  flex-direction: column;
16
22
  }
23
+
24
+ /* Splitter */
25
+ .splitter {
26
+ flex-shrink: 0;
27
+ background-color: var(--splitter-color);
28
+ transition: background-color 0.15s ease;
29
+ }
30
+ .splitter[orientation="horizontal"] {
31
+ width: var(--splitter-size);
32
+ cursor: col-resize;
33
+ }
34
+ .splitter[orientation="vertical"] {
35
+ height: var(--splitter-size);
36
+ cursor: row-resize;
37
+ }
38
+ .splitter:hover,
39
+ .splitter.active {
40
+ background-color: var(--splitter-active-color);
41
+ }
42
+
43
+ /* Slotted panels */
44
+ ::slotted(*) {
45
+ overflow: auto;
46
+ }
17
47
  `;
18
48
 
19
49
  export { styles };
@@ -1,32 +1,40 @@
1
1
  import { PropertyValues } from 'lit';
2
2
  import { BaseElement } from '../BaseElement.js';
3
3
  /**
4
- * TreeItem 컴포넌트는 트리의 개별 노드를 나타냅니다.
5
- * u-tree 컴포넌트와 함께 사용되며, 중첩된 구조를 지원합니다.
4
+ * UTreeItem 컴포넌트는 트리의 개별 노드를 나타냅니다.
5
+ * UTree 컴포넌트와 함께 사용되며, 중첩된 구조를 지원합니다.
6
6
  */
7
7
  export declare class UTreeItem extends BaseElement {
8
8
  static styles: import('lit').CSSResultGroup[];
9
9
  static dependencies: Record<string, typeof BaseElement>;
10
+ /** DOM 변경 감지를 위한 MutationObserver */
11
+ private mutationObserver?;
12
+ /** 자식 트리 아이템 배열 */
13
+ private _childrenItems;
14
+ /** 자식 트리 아이템 배열 (읽기 전용) */
15
+ get childrenItems(): readonly UTreeItem[];
10
16
  headerEl: HTMLElement;
11
- childrenItems: UTreeItem[];
12
17
  /** 트리 항목이 리프 노드인지 여부입니다. */
13
18
  leaf: boolean;
14
19
  /** 트리 항목의 들여쓰기 레벨입니다. */
15
20
  level: number;
21
+ /** 트리 항목이 확장된 상태인지 여부입니다. */
22
+ expanded: boolean;
16
23
  /** 트리 항목이 비활성화 상태인지 여부입니다. */
17
24
  disabled: boolean;
25
+ /** 로딩 상태 @default false */
26
+ loading: boolean;
18
27
  /** 트리 항목이 선택된 상태인지 여부입니다. */
19
28
  selected: boolean;
20
- /** 트리 항목이 확장된 상태인지 여부입니다. */
21
- expanded: boolean;
22
29
  /** 트리 항목의 값입니다. */
23
30
  value: string;
24
31
  connectedCallback(): void;
32
+ disconnectedCallback(): void;
25
33
  protected updated(changedProperties: PropertyValues): void;
26
34
  render(): import('lit-html').TemplateResult<1>;
27
- /** 슬롯 변경 헤더용 노드와 자식 노드 분리 */
28
- private handleSlotChange;
29
- /** 자식 슬롯 변경 시 자식 노드 재설정 */
35
+ /** 자식 u-tree-item 요소들에 slot 속성 부여 */
36
+ private processChildren;
37
+ /** 자식 슬롯 변경 시 처리 */
30
38
  private handleChildrenSlotChange;
31
39
  /** 헤더 클릭 이벤트를 처리합니다. */
32
40
  private handleHeaderClick;
@@ -1,8 +1,9 @@
1
1
  import { html } from 'lit';
2
- import { query, queryAssignedElements, state, property } from 'lit/decorators.js';
2
+ import { query, state, property } from 'lit/decorators.js';
3
3
  import { BaseElement } from '../BaseElement.js';
4
4
  import { UIcon } from '../icon/UIcon.component.js';
5
5
  import { styles } from './UTreeItem.styles.js';
6
+ import { USpinner } from '../spinner/USpinner.component.js';
6
7
 
7
8
  var __defProp = Object.defineProperty;
8
9
  var __decorateClass = (decorators, target, key, kind) => {
@@ -13,29 +14,27 @@ var __decorateClass = (decorators, target, key, kind) => {
13
14
  if (result) __defProp(target, key, result);
14
15
  return result;
15
16
  };
16
- const _UTreeItem = class _UTreeItem extends BaseElement {
17
+ class UTreeItem extends BaseElement {
17
18
  constructor() {
18
19
  super(...arguments);
20
+ /** 자식 트리 아이템 배열 */
21
+ this._childrenItems = [];
19
22
  this.leaf = true;
20
23
  this.level = 0;
24
+ this.expanded = false;
21
25
  this.disabled = false;
26
+ this.loading = false;
22
27
  this.selected = false;
23
- this.expanded = false;
24
28
  this.value = "";
25
- /** 슬롯 변경 시 헤더용 노드와 자식 노드 분리 */
26
- this.handleSlotChange = (e) => {
29
+ /** 자식 슬롯 변경 시 처리 */
30
+ this.handleChildrenSlotChange = (e) => {
27
31
  const slot = e.target;
28
- const nodes = slot.assignedNodes({ flatten: true });
29
- nodes.forEach((node) => {
30
- if (node instanceof _UTreeItem) {
31
- node.setAttribute("slot", "children");
32
- }
33
- });
34
- };
35
- /** 자식 슬롯 변경 시 자식 노드 재설정 */
36
- this.handleChildrenSlotChange = () => {
37
- this.leaf = this.childrenItems.length === 0;
38
- this.childrenItems.forEach((child) => {
32
+ const assignedElements = slot.assignedElements({ flatten: true });
33
+ this._childrenItems = assignedElements.filter(
34
+ (el) => el.tagName.toLowerCase() === "u-tree-item"
35
+ );
36
+ this.leaf = this._childrenItems.length === 0;
37
+ this._childrenItems.forEach((child) => {
39
38
  child.level = this.level + 1;
40
39
  });
41
40
  };
@@ -60,12 +59,34 @@ const _UTreeItem = class _UTreeItem extends BaseElement {
60
59
  }
61
60
  static {
62
61
  this.dependencies = {
63
- "u-icon": UIcon
62
+ "u-icon": UIcon,
63
+ "u-spinner": USpinner
64
64
  };
65
65
  }
66
+ /** 자식 트리 아이템 배열 (읽기 전용) */
67
+ get childrenItems() {
68
+ return this._childrenItems;
69
+ }
66
70
  connectedCallback() {
67
71
  super.connectedCallback();
68
72
  this.setAttribute("tabindex", this.disabled ? "-1" : "0");
73
+ this.mutationObserver = new MutationObserver((mutations) => {
74
+ for (const mutation of mutations) {
75
+ if (mutation.type === "childList") {
76
+ mutation.addedNodes.forEach((node) => {
77
+ if (node instanceof Element && node.tagName.toLowerCase() === "u-tree-item" && !node.hasAttribute("slot")) {
78
+ node.setAttribute("slot", "children");
79
+ }
80
+ });
81
+ }
82
+ }
83
+ });
84
+ this.mutationObserver.observe(this, { childList: true });
85
+ this.processChildren();
86
+ }
87
+ disconnectedCallback() {
88
+ super.disconnectedCallback();
89
+ this.mutationObserver?.disconnect();
69
90
  }
70
91
  updated(changedProperties) {
71
92
  super.updated(changedProperties);
@@ -73,7 +94,7 @@ const _UTreeItem = class _UTreeItem extends BaseElement {
73
94
  this.setAttribute("tabindex", this.disabled ? "-1" : "0");
74
95
  }
75
96
  if (changedProperties.has("level")) {
76
- this.headerEl.style.paddingLeft = `calc(${this.level} * var(--indent-size, 20px))`;
97
+ this.style.setProperty("--indent-level", this.level.toString());
77
98
  }
78
99
  }
79
100
  render() {
@@ -84,40 +105,57 @@ const _UTreeItem = class _UTreeItem extends BaseElement {
84
105
  lib="internal"
85
106
  name=${this.expanded ? "chevron-down" : "chevron-right"}
86
107
  ></u-icon>
108
+
109
+ <u-spinner class="prefix icon"
110
+ ?hidden=${!this.loading}
111
+ ></u-spinner>
112
+
87
113
  <slot name="prefix"></slot>
88
- <slot @slotchange=${this.handleSlotChange}></slot>
114
+
115
+ <span class="label">
116
+ <slot></slot>
117
+ </span>
118
+
89
119
  <slot name="suffix"></slot>
90
120
  </div>
121
+
91
122
  <div class="children" ?hidden=${!this.expanded}>
92
123
  <slot name="children" @slotchange=${this.handleChildrenSlotChange}></slot>
93
124
  </div>
94
125
  `;
95
126
  }
96
- };
127
+ /** 자식 u-tree-item 요소들에 slot 속성 부여 */
128
+ processChildren() {
129
+ Array.from(this.children).forEach((child) => {
130
+ if (child.tagName.toLowerCase() === "u-tree-item" && !child.hasAttribute("slot")) {
131
+ child.setAttribute("slot", "children");
132
+ }
133
+ });
134
+ }
135
+ }
97
136
  __decorateClass([
98
137
  query(".header")
99
- ], _UTreeItem.prototype, "headerEl");
100
- __decorateClass([
101
- queryAssignedElements({ slot: "children", selector: "u-tree-item", flatten: true })
102
- ], _UTreeItem.prototype, "childrenItems");
138
+ ], UTreeItem.prototype, "headerEl");
103
139
  __decorateClass([
104
140
  state()
105
- ], _UTreeItem.prototype, "leaf");
141
+ ], UTreeItem.prototype, "leaf");
106
142
  __decorateClass([
107
143
  state()
108
- ], _UTreeItem.prototype, "level");
144
+ ], UTreeItem.prototype, "level");
145
+ __decorateClass([
146
+ property({ type: Boolean, reflect: true })
147
+ ], UTreeItem.prototype, "expanded");
109
148
  __decorateClass([
110
149
  property({ type: Boolean, reflect: true })
111
- ], _UTreeItem.prototype, "disabled");
150
+ ], UTreeItem.prototype, "disabled");
112
151
  __decorateClass([
113
152
  property({ type: Boolean, reflect: true })
114
- ], _UTreeItem.prototype, "selected");
153
+ ], UTreeItem.prototype, "loading");
115
154
  __decorateClass([
116
155
  property({ type: Boolean, reflect: true })
117
- ], _UTreeItem.prototype, "expanded");
156
+ ], UTreeItem.prototype, "selected");
118
157
  __decorateClass([
119
158
  property({ type: String })
120
- ], _UTreeItem.prototype, "value");
121
- let UTreeItem = _UTreeItem;
159
+ ], UTreeItem.prototype, "value");
122
160
 
123
161
  export { UTreeItem };
@@ -2,10 +2,13 @@ import { css } from 'lit';
2
2
 
3
3
  const styles = css`
4
4
  :host {
5
- display: block;
6
-
5
+ --indent-level: 0;
7
6
  --indent-size: 20px;
8
7
  }
8
+
9
+ :host {
10
+ display: block;
11
+ }
9
12
  :host([disabled]) {
10
13
  opacity: 0.5;
11
14
  pointer-events: none;
@@ -22,6 +25,7 @@ const styles = css`
22
25
  align-items: center;
23
26
  gap: 6px;
24
27
  padding: 6px 8px;
28
+ padding-left: calc(var(--indent-level, 0) * var(--indent-size));
25
29
  line-height: 1.5;
26
30
  border-radius: 4px;
27
31
  background-color: transparent;
@@ -48,7 +52,8 @@ const styles = css`
48
52
  align-items: center;
49
53
  justify-content: center;
50
54
  }
51
- slot:not([name]) {
55
+
56
+ .label {
52
57
  flex: 1;
53
58
  display: inline-flex;
54
59
  align-items: center;
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ import './components/tree/UTree.js';
19
19
  import './components/tree-item/UTreeItem.js';
20
20
  export { BrowserStorage } from './utilities/BrowserStorage.js';
21
21
  export { notifier } from './utilities/notifier.js';
22
- export { theme } from './utilities/theme.js';
22
+ export { Theme, theme } from './utilities/theme.js';
23
23
  export { UAlert } from './components/alert/UAlert.component.js';
24
24
  export { UButton } from './components/button/UButton.component.js';
25
25
  export { UDialog } from './components/dialog/UDialog.component.js';
@@ -27,7 +27,7 @@ export interface ThemeInitOptions {
27
27
  /**
28
28
  * 현재 문서에 테마를 적용하고 관리하는 유틸리티 클래스입니다.
29
29
  */
30
- declare class Theme {
30
+ export declare class Theme {
31
31
  private static _instance;
32
32
  private readonly STORAGE_THEME_KEY;
33
33
  private storage;
@@ -58,4 +58,3 @@ declare class Theme {
58
58
  * 테마 유틸리티의 싱글톤 인스턴스입니다.
59
59
  */
60
60
  export declare const theme: Theme;
61
- export {};
@@ -137,4 +137,4 @@ class Theme {
137
137
  }
138
138
  const theme = Theme.instance;
139
139
 
140
- export { theme };
140
+ export { Theme, theme };
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": "0.1.10",
4
+ "version": "0.1.11",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "components",
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "@floating-ui/dom": "^1.7.4",
43
- "lit": "^3.3.1",
43
+ "lit": "^3.3.2",
44
44
  "react": "^19.2.3",
45
45
  "reflect-metadata": "^0.2.2"
46
46
  },
@@ -52,7 +52,7 @@
52
52
  "eslint-plugin-lit": "^2.1.1",
53
53
  "globals": "^16.5.0",
54
54
  "typescript": "^5.9.3",
55
- "typescript-eslint": "^8.50.0",
55
+ "typescript-eslint": "^8.50.1",
56
56
  "vite": "^7.3.0",
57
57
  "vite-plugin-dts": "^4.5.4",
58
58
  "vite-plugin-static-copy": "^3.1.4"