@iyulab/components 0.1.5 → 0.1.7

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.
Files changed (82) hide show
  1. package/CHANGELOG.md +18 -18
  2. package/LICENSE +20 -20
  3. package/README.md +33 -33
  4. package/dist/assets/icons/arrow-clockwise.svg.js +1 -1
  5. package/dist/assets/icons/arrow-down.svg.js +1 -1
  6. package/dist/assets/icons/arrow-up.svg.js +1 -1
  7. package/dist/assets/icons/ban.svg.js +1 -1
  8. package/dist/assets/icons/bell-fill.svg.js +1 -1
  9. package/dist/assets/icons/check-circle-fill.svg.js +1 -1
  10. package/dist/assets/icons/check-lg.svg.js +1 -1
  11. package/dist/assets/icons/chevron-down.svg.js +1 -1
  12. package/dist/assets/icons/chevron-left.svg.js +1 -1
  13. package/dist/assets/icons/chevron-right.svg.js +1 -1
  14. package/dist/assets/icons/chevron-up.svg.js +1 -1
  15. package/dist/assets/icons/copy.svg.js +1 -1
  16. package/dist/assets/icons/dash-lg.svg.js +1 -1
  17. package/dist/assets/icons/exclamation-circle-fill.svg.js +1 -1
  18. package/dist/assets/icons/exclamation-triangle-fill.svg.js +1 -1
  19. package/dist/assets/icons/eye-slash.svg.js +1 -1
  20. package/dist/assets/icons/eye.svg.js +1 -1
  21. package/dist/assets/icons/info-circle-fill.svg.js +1 -1
  22. package/dist/assets/icons/layout-sidebar.svg.js +1 -1
  23. package/dist/assets/icons/lightbulb-fill.svg.js +1 -1
  24. package/dist/assets/icons/lightbulb-off.svg.js +1 -1
  25. package/dist/assets/icons/lightbulb.svg.js +1 -1
  26. package/dist/assets/icons/mic-fill.svg.js +1 -1
  27. package/dist/assets/icons/mic-mute-fill.svg.js +1 -1
  28. package/dist/assets/icons/paperclip.svg.js +1 -1
  29. package/dist/assets/icons/pencil-square.svg.js +1 -1
  30. package/dist/assets/icons/plus-lg.svg.js +1 -1
  31. package/dist/assets/icons/search.svg.js +1 -1
  32. package/dist/assets/icons/speedometer.svg.js +1 -1
  33. package/dist/assets/icons/x-lg.svg.js +1 -1
  34. package/dist/assets/styles/dark.css +129 -129
  35. package/dist/assets/styles/dark.css.js +1 -1
  36. package/dist/assets/styles/light.css +129 -129
  37. package/dist/assets/styles/light.css.js +1 -1
  38. package/dist/components/FloatingElement.d.ts +84 -0
  39. package/dist/components/FloatingElement.js +166 -0
  40. package/dist/components/FloatingElement.styles.d.ts +1 -0
  41. package/dist/components/FloatingElement.styles.js +26 -0
  42. package/dist/components/context-menu/ContextMenu.d.ts +20 -39
  43. package/dist/components/context-menu/ContextMenu.js +92 -137
  44. package/dist/components/context-menu/ContextMenu.styles.js +3 -28
  45. package/dist/components/context-menu/index.d.ts +1 -1
  46. package/dist/components/dialog/Dialog.d.ts +14 -7
  47. package/dist/components/dialog/Dialog.js +88 -27
  48. package/dist/components/dialog/Dialog.styles.js +60 -11
  49. package/dist/components/divider/Divider.d.ts +5 -5
  50. package/dist/components/divider/Divider.js +17 -17
  51. package/dist/components/divider/Divider.styles.js +4 -6
  52. package/dist/components/dropdown-menu/DropdownMenu.d.ts +27 -0
  53. package/dist/components/dropdown-menu/DropdownMenu.js +135 -0
  54. package/dist/components/dropdown-menu/DropdownMenu.styles.d.ts +1 -0
  55. package/dist/components/dropdown-menu/DropdownMenu.styles.js +15 -0
  56. package/dist/components/dropdown-menu/index.d.ts +7 -0
  57. package/dist/components/dropdown-menu/index.js +5 -0
  58. package/dist/components/icon/Icon.js +1 -0
  59. package/dist/components/index.d.ts +2 -0
  60. package/dist/components/input/Input.js +1 -1
  61. package/dist/components/input/Input.styles.js +5 -5
  62. package/dist/components/menu/Menu.d.ts +4 -15
  63. package/dist/components/menu/Menu.js +43 -28
  64. package/dist/components/menu/Menu.styles.js +7 -19
  65. package/dist/components/menu-item/MenuItem.d.ts +21 -12
  66. package/dist/components/menu-item/MenuItem.js +184 -15
  67. package/dist/components/menu-item/MenuItem.styles.js +6 -0
  68. package/dist/components/menu-item/index.js +5 -0
  69. package/dist/components/split-panel/SplitPanel.d.ts +10 -2
  70. package/dist/components/split-panel/SplitPanel.js +41 -22
  71. package/dist/components/split-panel/SplitPanel.styles.js +1 -16
  72. package/dist/components/tooltip/Tooltip.d.ts +9 -29
  73. package/dist/components/tooltip/Tooltip.js +27 -149
  74. package/dist/components/tooltip/Tooltip.styles.js +8 -22
  75. package/dist/index.js +4 -0
  76. package/dist/integrations/react/UDropdownMenu.d.ts +8 -0
  77. package/dist/integrations/react/UDropdownMenu.js +10 -0
  78. package/dist/integrations/react/index.d.ts +1 -0
  79. package/dist/integrations/react/index.js +1 -0
  80. package/dist/internals/node-helpers.d.ts +6 -1
  81. package/dist/internals/node-helpers.js +5 -6
  82. package/package.json +59 -59
@@ -123,7 +123,7 @@ class Input extends BaseElement {
123
123
 
124
124
  <slot name="suffix"></slot>
125
125
 
126
- <div class="tools">
126
+ <div class="suffix-items">
127
127
  <u-icon
128
128
  lib="internal"
129
129
  name=${this.showPassword ? "eye-slash" : "eye"}
@@ -97,23 +97,23 @@ const styles = css`
97
97
  margin-left: 0.5em;
98
98
  }
99
99
 
100
- /* Tools 영역 (clear, password toggle 등) */
101
- .tools {
100
+ /* 영역 (clear, password toggle 등) */
101
+ .suffix-items {
102
102
  display: flex;
103
103
  flex-direction: row;
104
104
  align-items: center;
105
105
  }
106
- .tools u-icon {
106
+ .suffix-items u-icon {
107
107
  color: var(--u-icon-color);
108
108
  font-size: 1em;
109
109
  margin-left: 0.5em;
110
110
  transition: color 0.2s ease;
111
111
  cursor: pointer;
112
112
  }
113
- .tools u-icon:hover {
113
+ .suffix-items u-icon:hover {
114
114
  color: var(--u-icon-color-hover);
115
115
  }
116
- .tools u-icon:active {
116
+ .suffix-items u-icon:active {
117
117
  color: var(--u-icon-color-active);
118
118
  }
119
119
 
@@ -1,36 +1,25 @@
1
1
  import { PropertyValues } from 'lit';
2
2
  import { BaseElement } from '../BaseElement.js';
3
- /**
4
- * Menu 컴포넌트는 일반적인 메뉴 컨테이너입니다.
5
- * MenuItem 컴포넌트를 자식으로 포함하여 메뉴를 구성합니다.
6
- */
7
3
  export declare class Menu extends BaseElement {
8
4
  static styles: import('lit').CSSResultGroup[];
9
5
  static dependencies: Record<string, typeof BaseElement>;
10
- /** 슬롯에 할당된 메뉴 항목들을 가져옵니다. */
11
6
  private items;
12
- /** 메뉴가 열려있는지 여부입니다. */
13
7
  open: boolean;
14
- /** 메뉴 항목 중 하나만 선택할 수 있는지 여부입니다. */
15
8
  selectable: boolean;
16
- /** 현재 선택된 메뉴 항목의 값입니다. */
17
9
  value: string;
18
10
  connectedCallback(): void;
19
11
  disconnectedCallback(): void;
20
12
  protected updated(changedProperties: PropertyValues): void;
21
13
  render(): import('lit-html').TemplateResult<1>;
22
- /** 메뉴를 표시합니다. */
23
14
  show: () => Promise<void>;
24
- /** 메뉴를 숨깁니다. */
25
15
  hide: () => Promise<void>;
26
- /** open 상태가 변경될 호출됩니다. */
16
+ /** (추가) 번째 활성 아이템 포커스 */
17
+ focusFirstItem: () => Promise<void>;
27
18
  private updateOpenState;
28
- /** 선택 상태를 업데이트합니다. */
29
19
  private updateSelection;
30
- /** 슬롯 변경 이벤트를 처리합니다. */
31
20
  private handleSlotChange;
32
- /** 메뉴 항목 선택 이벤트를 처리합니다. */
33
21
  private handleSelect;
34
- /** 키보드 네비게이션을 처리합니다. */
22
+ /** 같은 레벨 다른 submenu 닫기 */
23
+ private handleSubmenuOpen;
35
24
  private handleKeydown;
36
25
  }
@@ -19,27 +19,23 @@ class Menu extends BaseElement {
19
19
  this.open = false;
20
20
  this.selectable = false;
21
21
  this.value = "";
22
- /** 메뉴를 표시합니다. */
23
22
  this.show = async () => {
24
23
  await this.updateComplete;
25
- requestAnimationFrame(() => {
26
- this.open = true;
27
- });
24
+ requestAnimationFrame(() => this.open = true);
28
25
  };
29
- /** 메뉴를 숨깁니다. */
30
26
  this.hide = async () => {
31
27
  await this.updateComplete;
32
- requestAnimationFrame(() => {
33
- this.open = false;
34
- });
28
+ requestAnimationFrame(() => this.open = false);
29
+ };
30
+ /** (추가) 첫 번째 활성 아이템 포커스 */
31
+ this.focusFirstItem = async () => {
32
+ await this.updateComplete;
33
+ const enabled = (this.items || []).filter((i) => !i.disabled);
34
+ if (enabled.length) enabled[0].focus();
35
35
  };
36
- /** 슬롯 변경 이벤트를 처리합니다. */
37
36
  this.handleSlotChange = () => {
38
- if (this.selectable && this.value) {
39
- this.updateSelection();
40
- }
37
+ if (this.selectable && this.value) this.updateSelection();
41
38
  };
42
- /** 메뉴 항목 선택 이벤트를 처리합니다. */
43
39
  this.handleSelect = (e) => {
44
40
  const event = e;
45
41
  const menuItem = e.target;
@@ -48,9 +44,16 @@ class Menu extends BaseElement {
48
44
  this.updateSelection();
49
45
  }
50
46
  };
51
- /** 키보드 네비게이션을 처리합니다. */
52
- this.handleKeydown = (e) => {
53
- const items = this.items.filter((item) => !item.disabled);
47
+ /** 같은 레벨 다른 submenu 닫기 */
48
+ this.handleSubmenuOpen = (e) => {
49
+ const openedItem = e.detail?.item;
50
+ if (!openedItem) return;
51
+ (this.items || []).forEach((item) => {
52
+ if (item !== openedItem) item.closeSubmenu?.();
53
+ });
54
+ };
55
+ this.handleKeydown = async (e) => {
56
+ const items = (this.items || []).filter((item) => !item.disabled);
54
57
  if (items.length === 0) return;
55
58
  const currentIndex = items.findIndex((item) => item === e.target);
56
59
  let nextIndex = currentIndex;
@@ -76,14 +79,29 @@ class Menu extends BaseElement {
76
79
  case "Enter":
77
80
  case " ":
78
81
  e.preventDefault();
79
- if (currentIndex >= 0) {
80
- items[currentIndex].click();
82
+ if (currentIndex >= 0) items[currentIndex].click();
83
+ break;
84
+ case "ArrowLeft": {
85
+ const parentItem = this.parentElement;
86
+ if (parentItem?.tagName?.toLowerCase() === "u-menu-item") {
87
+ e.preventDefault();
88
+ parentItem.closeSubmenu?.();
89
+ parentItem.focus?.();
81
90
  }
82
91
  break;
83
- case "Escape":
92
+ }
93
+ case "Escape": {
84
94
  e.preventDefault();
85
- this.hide();
95
+ const parentItem = this.parentElement;
96
+ if (parentItem?.tagName?.toLowerCase() === "u-menu-item") {
97
+ parentItem.closeSubmenu?.();
98
+ parentItem.focus?.();
99
+ } else {
100
+ this.emit("u-request-close");
101
+ await this.hide();
102
+ }
86
103
  break;
104
+ }
87
105
  }
88
106
  };
89
107
  }
@@ -100,10 +118,12 @@ class Menu extends BaseElement {
100
118
  this.setAttribute("role", "menu");
101
119
  this.addEventListener("u-select", this.handleSelect);
102
120
  this.addEventListener("keydown", this.handleKeydown);
121
+ this.addEventListener("u-submenu-open", this.handleSubmenuOpen);
103
122
  }
104
123
  disconnectedCallback() {
105
124
  this.removeEventListener("u-select", this.handleSelect);
106
125
  this.removeEventListener("keydown", this.handleKeydown);
126
+ this.removeEventListener("u-submenu-open", this.handleSubmenuOpen);
107
127
  super.disconnectedCallback();
108
128
  }
109
129
  updated(changedProperties) {
@@ -122,17 +142,12 @@ class Menu extends BaseElement {
122
142
  </div>
123
143
  `;
124
144
  }
125
- /** open 상태가 변경될 때 호출됩니다. */
126
145
  updateOpenState(open) {
127
- if (open) {
128
- this.emit("u-show");
129
- } else {
130
- this.emit("u-hide");
131
- }
146
+ if (open) this.emit("u-show");
147
+ else this.emit("u-hide");
132
148
  }
133
- /** 선택 상태를 업데이트합니다. */
134
149
  updateSelection() {
135
- this.items.forEach((item) => {
150
+ (this.items || []).forEach((item) => {
136
151
  item.selected = item.value === this.value;
137
152
  });
138
153
  }
@@ -2,31 +2,19 @@ import { css } from 'lit';
2
2
 
3
3
  const styles = css`
4
4
  :host {
5
- display: block;
6
- width: max-content;
5
+ display: none;
7
6
  min-width: 180px;
8
-
9
- opacity: 0;
10
- transform: scale(0.95);
11
- transform-origin: top left;
12
- transition: opacity 0.2s ease, transform 0.2s ease;
13
- pointer-events: none;
7
+ padding: 6px;
8
+ border-radius: 8px;
9
+ background: var(--u-menu-bg, #fff);
10
+ box-shadow: 0 10px 24px rgba(0,0,0,0.18);
14
11
  }
15
12
  :host([open]) {
16
- opacity: 1;
17
- transform: scale(1);
18
- pointer-events: auto;
13
+ display: block;
19
14
  }
20
15
 
21
16
  .container {
22
- display: flex;
23
- flex-direction: column;
24
- gap: 2px;
25
- padding: 4px;
26
- border: 1px solid var(--u-border-color, #ddd);
27
- border-radius: 8px;
28
- background-color: var(--u-bg-color, #fff);
29
- overflow: auto;
17
+ display: block;
30
18
  }
31
19
  `;
32
20
 
@@ -1,27 +1,36 @@
1
1
  import { PropertyValues } from 'lit';
2
2
  import { BaseElement } from '../BaseElement.js';
3
- /**
4
- * MenuItem 컴포넌트는 메뉴의 개별 항목을 나타냅니다.
5
- * u-menu 및 u-context-menu와 함께 사용됩니다.
6
- */
7
3
  export declare class MenuItem extends BaseElement {
8
4
  static styles: import('lit').CSSResultGroup[];
9
5
  static dependencies: Record<string, typeof BaseElement>;
10
- /** 메뉴 항목이 비활성화 상태인지 여부입니다. */
11
6
  disabled: boolean;
12
- /** 메뉴 항목이 선택된 상태인지 여부입니다. */
13
7
  selected: boolean;
14
- /** 체크 가능한 경우, 체크 상태인지 여부입니다. */
15
8
  checked: boolean;
16
- /** 메뉴 항목에 체크 표시를 할 수 있는지 여부입니다. */
17
9
  checkable: boolean;
18
- /** 메뉴 항목의 값입니다. */
19
10
  value: string;
11
+ private submenuOpen;
12
+ private containerEl;
13
+ private submenuList;
14
+ private submenuCleanup;
15
+ private closeTimer;
16
+ private get submenu();
20
17
  connectedCallback(): void;
21
- protected updated(changedProperties: PropertyValues): void;
18
+ disconnectedCallback(): void;
19
+ protected updated(changed: PropertyValues): void;
20
+ private updateRole;
22
21
  render(): import('lit-html').TemplateResult<1>;
23
- /** 메뉴 항목의 앞부분을 렌더링합니다. */
24
22
  private renderPrefixItem;
25
- /** 클릭 이벤트를 처리합니다. */
23
+ /** 외부에서 호출 가능하게(부모 menu가 형제 submenu 닫을 때 사용) */
24
+ closeSubmenu: () => Promise<void>;
25
+ private openSubmenu;
26
+ private onSubmenuSelect;
27
+ private scheduleCloseSubmenu;
28
+ private stopSubmenuAutoUpdate;
29
+ private positionSubmenu;
30
+ private onPointerEnter;
31
+ private onPointerLeave;
32
+ private onFocusIn;
33
+ private onFocusOut;
34
+ private handleKeydown;
26
35
  private handleClick;
27
36
  }
@@ -1,5 +1,6 @@
1
- import { html, nothing } from 'lit';
2
- import { property } from 'lit/decorators.js';
1
+ import { nothing, html } from 'lit';
2
+ import { property, state, query, queryAssignedElements } from 'lit/decorators.js';
3
+ import { autoUpdate, computePosition, offset, flip, shift } from '@floating-ui/dom';
3
4
  import { BaseElement } from '../BaseElement.js';
4
5
  import { styles } from './MenuItem.styles.js';
5
6
 
@@ -20,13 +21,125 @@ class MenuItem extends BaseElement {
20
21
  this.checked = false;
21
22
  this.checkable = false;
22
23
  this.value = "";
23
- /** 클릭 이벤트를 처리합니다. */
24
- this.handleClick = (e) => {
24
+ this.submenuOpen = false;
25
+ this.submenuCleanup = null;
26
+ this.closeTimer = null;
27
+ /** 외부에서 호출 가능하게(부모 menu가 형제 submenu 닫을 때 사용) */
28
+ this.closeSubmenu = async () => {
29
+ const sm = this.submenu;
30
+ if (!sm) return;
31
+ this.stopSubmenuAutoUpdate();
32
+ if (this.closeTimer) window.clearTimeout(this.closeTimer);
33
+ this.submenuOpen = false;
34
+ this.setAttribute("aria-expanded", "false");
35
+ await sm.hide();
36
+ };
37
+ this.openSubmenu = async (focusFirst = false) => {
38
+ const sm = this.submenu;
39
+ if (!sm || this.disabled) return;
40
+ if (this.closeTimer) {
41
+ window.clearTimeout(this.closeTimer);
42
+ this.closeTimer = null;
43
+ }
44
+ this.submenuOpen = true;
45
+ this.setAttribute("aria-expanded", "true");
46
+ this.emit("u-submenu-open", { item: this });
47
+ await sm.show();
48
+ await this.positionSubmenu();
49
+ this.stopSubmenuAutoUpdate();
50
+ this.submenuCleanup = autoUpdate(this.containerEl, sm, () => {
51
+ this.positionSubmenu();
52
+ });
53
+ sm.addEventListener("u-select", this.onSubmenuSelect, { once: true });
54
+ if (focusFirst) {
55
+ await sm.focusFirstItem?.();
56
+ }
57
+ };
58
+ this.onSubmenuSelect = () => {
59
+ this.closeSubmenu();
60
+ };
61
+ this.scheduleCloseSubmenu = () => {
62
+ if (!this.submenu) return;
63
+ if (this.closeTimer) window.clearTimeout(this.closeTimer);
64
+ this.closeTimer = window.setTimeout(() => this.closeSubmenu(), 150);
65
+ };
66
+ this.positionSubmenu = async () => {
67
+ const sm = this.submenu;
68
+ if (!sm) return;
69
+ const pos = await computePosition(this.containerEl, sm, {
70
+ strategy: "fixed",
71
+ placement: "right-start",
72
+ middleware: [
73
+ offset(6),
74
+ flip(),
75
+ shift({ padding: 8 })
76
+ ]
77
+ });
78
+ Object.assign(sm.style, {
79
+ position: "fixed",
80
+ left: `${pos.x}px`,
81
+ top: `${pos.y}px`,
82
+ zIndex: "1001",
83
+ transformOrigin: (() => {
84
+ switch (pos.placement) {
85
+ case "left":
86
+ case "left-start":
87
+ case "left-end":
88
+ return "right center";
89
+ case "right":
90
+ case "right-start":
91
+ case "right-end":
92
+ return "left center";
93
+ case "top":
94
+ case "top-start":
95
+ case "top-end":
96
+ return "center bottom";
97
+ default:
98
+ return "center top";
99
+ }
100
+ })()
101
+ });
102
+ };
103
+ this.onPointerEnter = () => {
104
+ if (this.submenu) this.openSubmenu(false);
105
+ };
106
+ this.onPointerLeave = (e) => {
107
+ const sm = this.submenu;
108
+ if (!sm) return;
109
+ const next = e.relatedTarget;
110
+ if (next && (this.contains(next) || sm.contains(next))) return;
111
+ this.scheduleCloseSubmenu();
112
+ };
113
+ this.onFocusIn = () => {
114
+ if (this.submenu) this.openSubmenu(false);
115
+ };
116
+ this.onFocusOut = (e) => {
117
+ const sm = this.submenu;
118
+ if (!sm) return;
119
+ const next = e.relatedTarget;
120
+ if (next && (this.contains(next) || sm.contains(next))) return;
121
+ this.scheduleCloseSubmenu();
122
+ };
123
+ this.handleKeydown = async (e) => {
124
+ if (!this.submenu) return;
125
+ if (e.key === "ArrowRight" || e.key === "Enter" || e.key === " ") {
126
+ e.preventDefault();
127
+ await this.openSubmenu(true);
128
+ }
129
+ };
130
+ this.handleClick = async (e) => {
25
131
  if (this.disabled) {
26
132
  e.preventDefault();
27
133
  e.stopPropagation();
28
134
  return;
29
135
  }
136
+ if (this.submenu) {
137
+ e.preventDefault();
138
+ e.stopPropagation();
139
+ if (this.submenuOpen) await this.closeSubmenu();
140
+ else await this.openSubmenu(true);
141
+ return;
142
+ }
30
143
  if (this.checkable) {
31
144
  this.checked = !this.checked;
32
145
  this.emit("u-check", { checked: this.checked });
@@ -41,41 +154,88 @@ class MenuItem extends BaseElement {
41
154
  static {
42
155
  this.dependencies = {};
43
156
  }
157
+ get submenu() {
158
+ return this.submenuList?.[0];
159
+ }
44
160
  connectedCallback() {
45
161
  super.connectedCallback();
46
- this.setAttribute("role", "menuitem");
47
162
  this.setAttribute("tabindex", this.disabled ? "-1" : "0");
163
+ this.updateRole();
164
+ this.addEventListener("keydown", this.handleKeydown);
165
+ this.addEventListener("pointerenter", this.onPointerEnter);
166
+ this.addEventListener("pointerleave", this.onPointerLeave);
167
+ this.addEventListener("focusin", this.onFocusIn);
168
+ this.addEventListener("focusout", this.onFocusOut);
48
169
  }
49
- updated(changedProperties) {
50
- super.updated(changedProperties);
51
- if (changedProperties.has("disabled")) {
170
+ disconnectedCallback() {
171
+ this.removeEventListener("keydown", this.handleKeydown);
172
+ this.removeEventListener("pointerenter", this.onPointerEnter);
173
+ this.removeEventListener("pointerleave", this.onPointerLeave);
174
+ this.removeEventListener("focusin", this.onFocusIn);
175
+ this.removeEventListener("focusout", this.onFocusOut);
176
+ this.stopSubmenuAutoUpdate();
177
+ if (this.closeTimer) window.clearTimeout(this.closeTimer);
178
+ super.disconnectedCallback();
179
+ }
180
+ updated(changed) {
181
+ super.updated(changed);
182
+ if (changed.has("disabled")) {
52
183
  this.setAttribute("tabindex", this.disabled ? "-1" : "0");
53
184
  this.setAttribute("aria-disabled", this.disabled ? "true" : "false");
185
+ if (this.disabled) this.closeSubmenu();
54
186
  }
55
- if (changedProperties.has("checked")) {
187
+ if (changed.has("checked")) {
56
188
  this.setAttribute("aria-checked", this.checked ? "true" : "false");
57
189
  }
190
+ if (changed.has("checkable")) {
191
+ this.updateRole();
192
+ }
193
+ }
194
+ updateRole() {
195
+ const hasSubmenu = !!this.submenu;
196
+ if (this.checkable) {
197
+ this.setAttribute("role", "menuitemcheckbox");
198
+ this.setAttribute("aria-checked", this.checked ? "true" : "false");
199
+ } else {
200
+ this.setAttribute("role", "menuitem");
201
+ }
202
+ if (hasSubmenu) {
203
+ this.setAttribute("aria-haspopup", "menu");
204
+ this.setAttribute("aria-expanded", this.submenuOpen ? "true" : "false");
205
+ } else {
206
+ this.removeAttribute("aria-haspopup");
207
+ this.removeAttribute("aria-expanded");
208
+ }
58
209
  }
59
210
  render() {
211
+ const hasSubmenu = !!this.submenu;
60
212
  return html`
61
213
  <div class="container" @click=${this.handleClick}>
62
214
  ${this.renderPrefixItem()}
63
215
  <slot name="prefix"></slot>
216
+
64
217
  <span class="content">
65
218
  <slot></slot>
66
219
  </span>
220
+
67
221
  <slot name="suffix"></slot>
222
+ ${hasSubmenu ? html`<span class="submenu-indicator">›</span>` : nothing}
68
223
  </div>
224
+
225
+ <!-- Shoelace 방식 그대로 -->
226
+ <slot name="submenu"></slot>
69
227
  `;
70
228
  }
71
- /** 메뉴 항목의 앞부분을 렌더링합니다. */
72
229
  renderPrefixItem() {
73
230
  if (this.checkable) {
74
- return html`
75
- <span class="checker" ?checked=${this.checked}>✓</span>
76
- `;
77
- } else {
78
- return nothing;
231
+ return html`<span class="checker" ?checked=${this.checked}>✓</span>`;
232
+ }
233
+ return nothing;
234
+ }
235
+ stopSubmenuAutoUpdate() {
236
+ if (this.submenuCleanup) {
237
+ this.submenuCleanup();
238
+ this.submenuCleanup = null;
79
239
  }
80
240
  }
81
241
  }
@@ -94,5 +254,14 @@ __decorateClass([
94
254
  __decorateClass([
95
255
  property({ type: String })
96
256
  ], MenuItem.prototype, "value");
257
+ __decorateClass([
258
+ state()
259
+ ], MenuItem.prototype, "submenuOpen");
260
+ __decorateClass([
261
+ query(".container")
262
+ ], MenuItem.prototype, "containerEl");
263
+ __decorateClass([
264
+ queryAssignedElements({ slot: "submenu", selector: "u-menu" })
265
+ ], MenuItem.prototype, "submenuList");
97
266
 
98
267
  export { MenuItem };
@@ -59,6 +59,12 @@ const styles = css`
59
59
  text-overflow: ellipsis;
60
60
  white-space: nowrap;
61
61
  }
62
+
63
+ .submenu-indicator {
64
+ margin-left: 8px;
65
+ opacity: 0.7;
66
+ user-select: none;
67
+ }
62
68
  `;
63
69
 
64
70
  export { styles };
@@ -0,0 +1,5 @@
1
+ import { MenuItem } from './MenuItem.js';
2
+
3
+ MenuItem.define("u-menu-item");
4
+
5
+ export { MenuItem };
@@ -1,3 +1,4 @@
1
+ import { PropertyValues } from 'lit';
1
2
  import { BaseElement } from '../BaseElement.js';
2
3
  /**
3
4
  * SplitPanel 컴포넌트는 화면을 여러 개의 패널로 분할하여 표시합니다.
@@ -9,14 +10,21 @@ export declare class SplitPanel extends BaseElement {
9
10
  private dividers;
10
11
  private panelSizes;
11
12
  private panelAdjustSizes;
13
+ childElements: HTMLElement[];
12
14
  /** 분할 방향을 설정합니다. 'horizontal'은 좌우 분할, 'vertical'은 상하 분할입니다. */
13
15
  orientation: 'horizontal' | 'vertical';
14
16
  /** 초기 패널 크기 비율을 설정합니다. (예: [30, 70]은 첫 번째 패널이 30%, 두 번째 패널이 70%를 차지) */
15
- initialRatio: number[];
17
+ initRatio: number[];
18
+ /** 각 패널의 최소 크기를 설정합니다. (픽셀 단위) */
19
+ minSizes: number[];
20
+ /** 각 패널의 최대 크기를 설정합니다. (픽셀 단위) */
21
+ maxSizes: number[];
16
22
  disconnectedCallback(): void;
23
+ protected willUpdate(changedProperties: PropertyValues): void;
17
24
  render(): import('lit-html').TemplateResult<1>;
18
- private getDividerSize;
25
+ private initialize;
19
26
  private handleSlotChange;
20
27
  /** 디바이더 드래그 이벤트 핸들러 */
21
28
  private handleDividerMove;
29
+ private getDividerSize;
22
30
  }