@simplysm/solid 13.0.55 → 13.0.56

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 (181) hide show
  1. package/README.md +3 -1
  2. package/dist/components/data/crud-detail/CrudDetail.d.ts +14 -0
  3. package/dist/components/data/crud-detail/CrudDetail.d.ts.map +1 -0
  4. package/dist/components/data/crud-detail/CrudDetail.js +348 -0
  5. package/dist/components/data/crud-detail/CrudDetail.js.map +6 -0
  6. package/dist/components/data/crud-detail/CrudDetailAfter.d.ts +7 -0
  7. package/dist/components/data/crud-detail/CrudDetailAfter.d.ts.map +1 -0
  8. package/dist/components/data/crud-detail/CrudDetailAfter.js +14 -0
  9. package/dist/components/data/crud-detail/CrudDetailAfter.js.map +6 -0
  10. package/dist/components/data/crud-detail/CrudDetailBefore.d.ts +7 -0
  11. package/dist/components/data/crud-detail/CrudDetailBefore.d.ts.map +1 -0
  12. package/dist/components/data/crud-detail/CrudDetailBefore.js +14 -0
  13. package/dist/components/data/crud-detail/CrudDetailBefore.js.map +6 -0
  14. package/dist/components/data/crud-detail/CrudDetailTools.d.ts +7 -0
  15. package/dist/components/data/crud-detail/CrudDetailTools.d.ts.map +1 -0
  16. package/dist/components/data/crud-detail/CrudDetailTools.js +14 -0
  17. package/dist/components/data/crud-detail/CrudDetailTools.js.map +6 -0
  18. package/dist/components/data/crud-detail/types.d.ts +45 -0
  19. package/dist/components/data/crud-detail/types.d.ts.map +1 -0
  20. package/dist/components/data/crud-detail/types.js +1 -0
  21. package/dist/components/data/crud-detail/types.js.map +6 -0
  22. package/dist/components/data/crud-sheet/CrudSheet.d.ts +17 -0
  23. package/dist/components/data/crud-sheet/CrudSheet.d.ts.map +1 -0
  24. package/dist/components/data/crud-sheet/CrudSheet.js +679 -0
  25. package/dist/components/data/crud-sheet/CrudSheet.js.map +6 -0
  26. package/dist/components/data/crud-sheet/CrudSheetColumn.d.ts +5 -0
  27. package/dist/components/data/crud-sheet/CrudSheetColumn.d.ts.map +1 -0
  28. package/dist/components/data/crud-sheet/CrudSheetColumn.js +29 -0
  29. package/dist/components/data/crud-sheet/CrudSheetColumn.js.map +6 -0
  30. package/dist/components/data/crud-sheet/CrudSheetFilter.d.ts +7 -0
  31. package/dist/components/data/crud-sheet/CrudSheetFilter.d.ts.map +1 -0
  32. package/dist/components/data/crud-sheet/CrudSheetFilter.js +14 -0
  33. package/dist/components/data/crud-sheet/CrudSheetFilter.js.map +6 -0
  34. package/dist/components/data/crud-sheet/CrudSheetHeader.d.ts +7 -0
  35. package/dist/components/data/crud-sheet/CrudSheetHeader.d.ts.map +1 -0
  36. package/dist/components/data/crud-sheet/CrudSheetHeader.js +14 -0
  37. package/dist/components/data/crud-sheet/CrudSheetHeader.js.map +6 -0
  38. package/dist/components/data/crud-sheet/CrudSheetTools.d.ts +7 -0
  39. package/dist/components/data/crud-sheet/CrudSheetTools.d.ts.map +1 -0
  40. package/dist/components/data/crud-sheet/CrudSheetTools.js +14 -0
  41. package/dist/components/data/crud-sheet/CrudSheetTools.js.map +6 -0
  42. package/dist/components/data/crud-sheet/types.d.ts +109 -0
  43. package/dist/components/data/crud-sheet/types.d.ts.map +1 -0
  44. package/dist/components/data/crud-sheet/types.js +1 -0
  45. package/dist/components/data/crud-sheet/types.js.map +6 -0
  46. package/dist/components/data/kanban/Kanban.d.ts.map +1 -1
  47. package/dist/components/data/kanban/Kanban.js +137 -138
  48. package/dist/components/data/kanban/Kanban.js.map +2 -2
  49. package/dist/components/data/kanban/KanbanContext.d.ts +5 -1
  50. package/dist/components/data/kanban/KanbanContext.d.ts.map +1 -1
  51. package/dist/components/data/kanban/KanbanContext.js.map +1 -1
  52. package/dist/components/data/list/ListItem.d.ts.map +1 -1
  53. package/dist/components/data/list/ListItem.js +109 -99
  54. package/dist/components/data/list/ListItem.js.map +2 -2
  55. package/dist/components/data/sheet/DataSheet.js +1 -1
  56. package/dist/components/data/sheet/DataSheet.js.map +2 -2
  57. package/dist/components/data/sheet/DataSheet.styles.d.ts.map +1 -1
  58. package/dist/components/data/sheet/DataSheet.styles.js +1 -1
  59. package/dist/components/data/sheet/DataSheet.styles.js.map +1 -1
  60. package/dist/components/disclosure/Dialog.d.ts +16 -10
  61. package/dist/components/disclosure/Dialog.d.ts.map +1 -1
  62. package/dist/components/disclosure/Dialog.js +126 -91
  63. package/dist/components/disclosure/Dialog.js.map +2 -2
  64. package/dist/components/disclosure/DialogContext.d.ts +2 -4
  65. package/dist/components/disclosure/DialogContext.d.ts.map +1 -1
  66. package/dist/components/disclosure/DialogContext.js.map +1 -1
  67. package/dist/components/disclosure/DialogProvider.d.ts.map +1 -1
  68. package/dist/components/disclosure/DialogProvider.js +14 -9
  69. package/dist/components/disclosure/DialogProvider.js.map +2 -2
  70. package/dist/components/disclosure/Dropdown.d.ts +46 -22
  71. package/dist/components/disclosure/Dropdown.d.ts.map +1 -1
  72. package/dist/components/disclosure/Dropdown.js +100 -65
  73. package/dist/components/disclosure/Dropdown.js.map +2 -2
  74. package/dist/components/feedback/notification/NotificationBanner.d.ts.map +1 -1
  75. package/dist/components/feedback/notification/NotificationBanner.js +3 -3
  76. package/dist/components/feedback/notification/NotificationBanner.js.map +1 -1
  77. package/dist/components/feedback/notification/NotificationBell.d.ts.map +1 -1
  78. package/dist/components/feedback/notification/NotificationBell.js +84 -84
  79. package/dist/components/feedback/notification/NotificationBell.js.map +2 -2
  80. package/dist/components/form-control/combobox/Combobox.d.ts +6 -3
  81. package/dist/components/form-control/combobox/Combobox.d.ts.map +1 -1
  82. package/dist/components/form-control/combobox/Combobox.js +150 -168
  83. package/dist/components/form-control/combobox/Combobox.js.map +2 -2
  84. package/dist/components/form-control/combobox/ComboboxContext.d.ts +3 -0
  85. package/dist/components/form-control/combobox/ComboboxContext.d.ts.map +1 -1
  86. package/dist/components/form-control/combobox/ComboboxContext.js.map +1 -1
  87. package/dist/components/form-control/date-range-picker/DateRangePicker.d.ts +0 -2
  88. package/dist/components/form-control/date-range-picker/DateRangePicker.d.ts.map +1 -1
  89. package/dist/components/form-control/date-range-picker/DateRangePicker.js +9 -17
  90. package/dist/components/form-control/date-range-picker/DateRangePicker.js.map +2 -2
  91. package/dist/components/form-control/field/Field.styles.d.ts.map +1 -1
  92. package/dist/components/form-control/field/Field.styles.js +2 -1
  93. package/dist/components/form-control/field/Field.styles.js.map +1 -1
  94. package/dist/components/form-control/field/NumberInput.d.ts +15 -5
  95. package/dist/components/form-control/field/NumberInput.d.ts.map +1 -1
  96. package/dist/components/form-control/field/NumberInput.js +181 -141
  97. package/dist/components/form-control/field/NumberInput.js.map +2 -2
  98. package/dist/components/form-control/field/TextInput.d.ts +9 -5
  99. package/dist/components/form-control/field/TextInput.d.ts.map +1 -1
  100. package/dist/components/form-control/field/TextInput.js +199 -154
  101. package/dist/components/form-control/field/TextInput.js.map +2 -2
  102. package/dist/components/form-control/select/Select.d.ts +3 -3
  103. package/dist/components/form-control/select/Select.d.ts.map +1 -1
  104. package/dist/components/form-control/select/Select.js +116 -100
  105. package/dist/components/form-control/select/Select.js.map +2 -2
  106. package/dist/components/form-control/select/SelectContext.d.ts +9 -1
  107. package/dist/components/form-control/select/SelectContext.d.ts.map +1 -1
  108. package/dist/components/form-control/select/SelectContext.js.map +1 -1
  109. package/dist/components/form-control/select/SelectItem.d.ts.map +1 -1
  110. package/dist/components/form-control/select/SelectItem.js +77 -67
  111. package/dist/components/form-control/select/SelectItem.js.map +2 -2
  112. package/dist/components/layout/topbar/TopbarMenu.d.ts.map +1 -1
  113. package/dist/components/layout/topbar/TopbarMenu.js +63 -57
  114. package/dist/components/layout/topbar/TopbarMenu.js.map +2 -2
  115. package/dist/components/layout/topbar/TopbarUser.d.ts.map +1 -1
  116. package/dist/components/layout/topbar/TopbarUser.js +53 -54
  117. package/dist/components/layout/topbar/TopbarUser.js.map +2 -2
  118. package/dist/hooks/createControllableStore.d.ts +29 -0
  119. package/dist/hooks/createControllableStore.d.ts.map +1 -0
  120. package/dist/hooks/createControllableStore.js +19 -0
  121. package/dist/hooks/createControllableStore.js.map +6 -0
  122. package/dist/index.d.ts +5 -1
  123. package/dist/index.d.ts.map +1 -1
  124. package/dist/index.js +6 -2
  125. package/dist/index.js.map +1 -1
  126. package/dist/styles/patterns.styles.d.ts.map +1 -1
  127. package/dist/styles/patterns.styles.js +7 -1
  128. package/dist/styles/patterns.styles.js.map +1 -1
  129. package/docs/data-components.md +428 -0
  130. package/docs/disclosure.md +65 -35
  131. package/docs/form-controls.md +18 -3
  132. package/docs/helpers.md +0 -39
  133. package/docs/hooks.md +39 -0
  134. package/package.json +4 -3
  135. package/src/components/data/crud-detail/CrudDetail.tsx +346 -0
  136. package/src/components/data/crud-detail/CrudDetailAfter.tsx +19 -0
  137. package/src/components/data/crud-detail/CrudDetailBefore.tsx +19 -0
  138. package/src/components/data/crud-detail/CrudDetailTools.tsx +19 -0
  139. package/src/components/data/crud-detail/types.ts +58 -0
  140. package/src/components/data/crud-sheet/CrudSheet.tsx +628 -0
  141. package/src/components/data/crud-sheet/CrudSheetColumn.tsx +34 -0
  142. package/src/components/data/crud-sheet/CrudSheetFilter.tsx +21 -0
  143. package/src/components/data/crud-sheet/CrudSheetHeader.tsx +19 -0
  144. package/src/components/data/crud-sheet/CrudSheetTools.tsx +21 -0
  145. package/src/components/data/crud-sheet/types.ts +133 -0
  146. package/src/components/data/kanban/Kanban.tsx +72 -65
  147. package/src/components/data/kanban/KanbanContext.ts +7 -1
  148. package/src/components/data/list/ListItem.tsx +31 -18
  149. package/src/components/data/sheet/DataSheet.styles.ts +1 -1
  150. package/src/components/data/sheet/DataSheet.tsx +1 -1
  151. package/src/components/disclosure/Dialog.tsx +143 -105
  152. package/src/components/disclosure/DialogContext.ts +2 -4
  153. package/src/components/disclosure/DialogProvider.tsx +4 -2
  154. package/src/components/disclosure/Dropdown.tsx +174 -86
  155. package/src/components/feedback/notification/NotificationBanner.tsx +3 -9
  156. package/src/components/feedback/notification/NotificationBell.tsx +51 -57
  157. package/src/components/form-control/combobox/Combobox.tsx +109 -134
  158. package/src/components/form-control/combobox/ComboboxContext.ts +4 -1
  159. package/src/components/form-control/date-range-picker/DateRangePicker.tsx +6 -16
  160. package/src/components/form-control/field/Field.styles.ts +1 -0
  161. package/src/components/form-control/field/NumberInput.tsx +131 -88
  162. package/src/components/form-control/field/TextInput.tsx +139 -88
  163. package/src/components/form-control/select/Select.tsx +85 -67
  164. package/src/components/form-control/select/SelectContext.ts +12 -1
  165. package/src/components/form-control/select/SelectItem.tsx +39 -18
  166. package/src/components/layout/topbar/TopbarMenu.tsx +52 -55
  167. package/src/components/layout/topbar/TopbarUser.tsx +28 -31
  168. package/src/hooks/createControllableStore.ts +47 -0
  169. package/src/index.ts +5 -1
  170. package/src/styles/patterns.styles.ts +7 -1
  171. package/tailwind.css +4 -0
  172. package/dist/helpers/splitSlots.d.ts +0 -25
  173. package/dist/helpers/splitSlots.d.ts.map +0 -1
  174. package/dist/helpers/splitSlots.js +0 -25
  175. package/dist/helpers/splitSlots.js.map +0 -6
  176. package/dist/hooks/createItemTemplate.d.ts +0 -17
  177. package/dist/hooks/createItemTemplate.d.ts.map +0 -1
  178. package/dist/hooks/createItemTemplate.js +0 -40
  179. package/dist/hooks/createItemTemplate.js.map +0 -6
  180. package/src/helpers/splitSlots.ts +0 -51
  181. package/src/hooks/createItemTemplate.tsx +0 -42
@@ -3,6 +3,8 @@ import {
3
3
  type ParentComponent,
4
4
  createSignal,
5
5
  createEffect,
6
+ createContext,
7
+ useContext,
6
8
  onCleanup,
7
9
  Show,
8
10
  splitProps,
@@ -12,20 +14,47 @@ import { createMountTransition } from "../../hooks/createMountTransition";
12
14
  import { Portal } from "solid-js/web";
13
15
  import clsx from "clsx";
14
16
  import { twMerge } from "tailwind-merge";
15
- import { createControllableSignal } from "../../hooks/createControllableSignal";
16
17
  import { mergeStyles } from "../../helpers/mergeStyles";
17
18
  import { borderSubtle } from "../../styles/tokens.styles";
18
19
 
19
- export interface DropdownProps extends Omit<JSX.HTMLAttributes<HTMLDivElement>, "children"> {
20
- /**
21
- * 트리거 요소의 ref (위치 계산 + minWidth 적용)
22
- * position과 함께 사용 불가
23
- */
24
- triggerRef?: () => HTMLElement | undefined;
20
+ // --- DropdownContext (internal) ---
21
+
22
+ type SlotAccessor = (() => JSX.Element) | undefined;
23
+
24
+ interface DropdownContextValue {
25
+ toggle: () => void;
26
+ setTrigger: (content: SlotAccessor) => void;
27
+ setContent: (content: SlotAccessor) => void;
28
+ }
29
+
30
+ const DropdownContext = createContext<DropdownContextValue>();
31
+
32
+ // --- DropdownTrigger ---
33
+
34
+ const DropdownTrigger: ParentComponent = (props) => {
35
+ const ctx = useContext(DropdownContext)!;
36
+ // eslint-disable-next-line solid/reactivity -- slot accessor: children은 렌더 시점에 lazy 평가됨
37
+ ctx.setTrigger(() => props.children);
38
+ onCleanup(() => ctx.setTrigger(undefined));
39
+ return null;
40
+ };
41
+
42
+ // --- DropdownContent ---
43
+
44
+ const DropdownContent: ParentComponent = (props) => {
45
+ const ctx = useContext(DropdownContext)!;
46
+ // eslint-disable-next-line solid/reactivity -- slot accessor: children은 렌더 시점에 lazy 평가됨
47
+ ctx.setContent(() => props.children);
48
+ onCleanup(() => ctx.setContent(undefined));
49
+ return null;
50
+ };
25
51
 
52
+ // --- Dropdown ---
53
+
54
+ export interface DropdownProps {
26
55
  /**
27
56
  * 절대 위치 (컨텍스트 메뉴 등, minWidth 없음)
28
- * triggerRef와 함께 사용 불가
57
+ * Trigger와 함께 사용 시 Trigger 기준 위치 계산
29
58
  */
30
59
  position?: { x: number; y: number };
31
60
 
@@ -44,62 +73,118 @@ export interface DropdownProps extends Omit<JSX.HTMLAttributes<HTMLDivElement>,
44
73
  */
45
74
  maxHeight?: number;
46
75
 
76
+ /**
77
+ * 비활성화 (Trigger 클릭 무시)
78
+ */
79
+ disabled?: boolean;
80
+
47
81
  /**
48
82
  * 키보드 네비게이션 활성화 (Select 등에서 사용)
49
83
  *
50
84
  * direction=down일 때:
51
- * - 트리거에서 ArrowDown 첫 focusable 아이템 포커스
52
- * - 첫 아이템에서 ArrowUp 트리거 포커스
53
- * - 트리거에서 ArrowUp 닫기
85
+ * - 트리거에서 ArrowDown -> 첫 focusable 아이템 포커스
86
+ * - 첫 아이템에서 ArrowUp -> 트리거 포커스
87
+ * - 트리거에서 ArrowUp -> 닫기
54
88
  *
55
89
  * direction=up일 때:
56
- * - 트리거에서 ArrowUp 마지막 focusable 아이템 포커스
57
- * - 마지막 아이템에서 ArrowDown 트리거 포커스
58
- * - 트리거에서 ArrowDown 닫기
90
+ * - 트리거에서 ArrowUp -> 마지막 focusable 아이템 포커스
91
+ * - 마지막 아이템에서 ArrowDown -> 트리거 포커스
92
+ * - 트리거에서 ArrowDown -> 닫기
59
93
  */
60
94
  keyboardNav?: boolean;
61
95
 
62
96
  /**
63
- * children
97
+ * 팝업에 적용할 커스텀 class
98
+ */
99
+ class?: string;
100
+
101
+ /**
102
+ * 팝업에 적용할 커스텀 style
103
+ */
104
+ style?: JSX.CSSProperties;
105
+
106
+ /**
107
+ * children (Dropdown.Trigger, Dropdown.Content)
64
108
  */
65
109
  children: JSX.Element;
66
110
  }
67
111
 
112
+ interface DropdownComponent extends ParentComponent<DropdownProps> {
113
+ Trigger: typeof DropdownTrigger;
114
+ Content: typeof DropdownContent;
115
+ }
116
+
68
117
  /**
69
118
  * 드롭다운 팝업 컴포넌트
70
119
  *
71
- * 위치 계산 + 포털 렌더링 + 닫힘 감지를 담당하며,
72
- * 트리거 요소 렌더링과 열기 이벤트는 사용자가 직접 제어합니다.
120
+ * Trigger/Content 슬롯 패턴으로 트리거와 컨텐츠를 분리합니다.
121
+ * Trigger 클릭 auto-toggle되며, disabled prop으로 비활성화할 수 있습니다.
73
122
  *
74
123
  * @example
75
124
  * ```tsx
76
- * const [open, setOpen] = createSignal(false);
77
- * let buttonRef: HTMLButtonElement;
125
+ * <Dropdown>
126
+ * <Dropdown.Trigger>
127
+ * <Button>열기</Button>
128
+ * </Dropdown.Trigger>
129
+ * <Dropdown.Content>
130
+ * <div>팝업 내용</div>
131
+ * </Dropdown.Content>
132
+ * </Dropdown>
133
+ * ```
78
134
  *
79
- * <Button ref={buttonRef} onClick={() => setOpen(!open())}>열기</Button>
80
- * <Dropdown triggerRef={() => buttonRef} open={open()} onOpenChange={setOpen}>
81
- * <div>팝업 내용</div>
135
+ * @example Context menu (Trigger 없이 position 사용)
136
+ * ```tsx
137
+ * <Dropdown position={{ x: 300, y: 200 }} open={true}>
138
+ * <Dropdown.Content>
139
+ * <div>메뉴</div>
140
+ * </Dropdown.Content>
82
141
  * </Dropdown>
83
142
  * ```
84
143
  */
85
- export const Dropdown: ParentComponent<DropdownProps> = (props) => {
144
+ export const Dropdown: DropdownComponent = ((props: DropdownProps) => {
86
145
  const [local, rest] = splitProps(props, [
87
- "triggerRef",
88
146
  "position",
89
147
  "open",
90
148
  "onOpenChange",
91
149
  "maxHeight",
150
+ "disabled",
92
151
  "keyboardNav",
93
152
  "class",
94
153
  "style",
95
154
  "children",
96
155
  ]);
97
156
 
98
- const [open, setOpen] = createControllableSignal({
99
- value: () => local.open ?? false,
100
- onChange: () => local.onOpenChange,
157
+ const [open, setOpenInternal] = createSignal(false);
158
+
159
+ // props.open 변경 시 내부 상태 동기화
160
+ createEffect(() => {
161
+ const propOpen = local.open;
162
+ if (propOpen !== undefined) {
163
+ setOpenInternal(propOpen);
164
+ }
101
165
  });
102
166
 
167
+ // 콜백 포함 setter
168
+ const setOpen = (value: boolean) => {
169
+ setOpenInternal(value);
170
+ local.onOpenChange?.(value);
171
+ };
172
+
173
+ // toggle 함수 (disabled 체크 포함)
174
+ const toggle = () => {
175
+ if (local.disabled) return;
176
+ setOpen(!open());
177
+ };
178
+
179
+ // 슬롯 등록 시그널
180
+ const [triggerSlot, _setTriggerSlot] = createSignal<SlotAccessor>();
181
+ const setTrigger = (content: SlotAccessor) => _setTriggerSlot(() => content);
182
+ const [contentSlot, _setContentSlot] = createSignal<SlotAccessor>();
183
+ const setContent = (content: SlotAccessor) => _setContentSlot(() => content);
184
+
185
+ // Trigger wrapper ref (위치 계산에 필요)
186
+ let triggerRef: HTMLDivElement | undefined;
187
+
103
188
  // 팝업 ref
104
189
  const [popupRef, setPopupRef] = createSignal<HTMLDivElement>();
105
190
 
@@ -119,11 +204,8 @@ export const Dropdown: ParentComponent<DropdownProps> = (props) => {
119
204
 
120
205
  const style: JSX.CSSProperties = {};
121
206
 
122
- if (local.triggerRef) {
123
- const trigger = local.triggerRef();
124
- if (!trigger) return;
125
-
126
- const rect = trigger.getBoundingClientRect();
207
+ if (triggerRef) {
208
+ const rect = triggerRef.getBoundingClientRect();
127
209
  const viewportHeight = window.innerHeight;
128
210
  const viewportWidth = window.innerWidth;
129
211
 
@@ -193,13 +275,10 @@ export const Dropdown: ParentComponent<DropdownProps> = (props) => {
193
275
  // 팝업 내부 클릭은 무시
194
276
  if (popup?.contains(target)) return;
195
277
 
196
- // triggerRef 모드: 트리거 내부 클릭도 무시
197
- if (local.triggerRef) {
198
- const trigger = local.triggerRef();
199
- if (trigger?.contains(target)) return;
200
- }
278
+ // Trigger 내부 클릭도 무시
279
+ if (triggerRef?.contains(target)) return;
201
280
 
202
- // 외부 클릭 닫기
281
+ // 외부 클릭 -> 닫기
203
282
  setOpen(false);
204
283
  };
205
284
 
@@ -221,13 +300,10 @@ export const Dropdown: ParentComponent<DropdownProps> = (props) => {
221
300
  // 팝업 내부로 이동은 무시
222
301
  if (popup?.contains(relatedTarget)) return;
223
302
 
224
- // triggerRef 모드: 트리거 내부로 이동도 무시
225
- if (local.triggerRef) {
226
- const trigger = local.triggerRef();
227
- if (trigger?.contains(relatedTarget)) return;
228
- }
303
+ // Trigger 내부로 이동도 무시
304
+ if (triggerRef?.contains(relatedTarget)) return;
229
305
 
230
- // 외부로 포커스 이동 닫기
306
+ // 외부로 포커스 이동 -> 닫기
231
307
  setOpen(false);
232
308
  };
233
309
 
@@ -301,8 +377,7 @@ export const Dropdown: ParentComponent<DropdownProps> = (props) => {
301
377
  // List 등에서 이미 처리된 이벤트는 무시
302
378
  if (e.defaultPrevented) return;
303
379
 
304
- const trigger = local.triggerRef?.();
305
- if (!trigger) return;
380
+ if (!triggerRef) return;
306
381
 
307
382
  const dir = direction();
308
383
 
@@ -310,24 +385,13 @@ export const Dropdown: ParentComponent<DropdownProps> = (props) => {
310
385
  // 트리거로 포커스 이동
311
386
  if (dir === "down" && e.key === "ArrowUp") {
312
387
  e.preventDefault();
313
- trigger.focus();
388
+ triggerRef.focus();
314
389
  } else if (dir === "up" && e.key === "ArrowDown") {
315
390
  e.preventDefault();
316
- trigger.focus();
391
+ triggerRef.focus();
317
392
  }
318
393
  };
319
394
 
320
- // 트리거에 키보드 핸들러 등록
321
- createEffect(() => {
322
- if (!local.keyboardNav) return;
323
-
324
- const trigger = local.triggerRef?.();
325
- if (!trigger) return;
326
-
327
- trigger.addEventListener("keydown", handleTriggerKeyDown);
328
- onCleanup(() => trigger.removeEventListener("keydown", handleTriggerKeyDown));
329
- });
330
-
331
395
  // 스크롤 감지
332
396
  createEffect(() => {
333
397
  if (!open()) return;
@@ -363,7 +427,7 @@ export const Dropdown: ParentComponent<DropdownProps> = (props) => {
363
427
  if (e.propertyName !== "opacity") return;
364
428
 
365
429
  if (!open()) {
366
- // 닫힘 애니메이션 완료 DOM에서 제거
430
+ // 닫힘 애니메이션 완료 -> DOM에서 제거
367
431
  unmount();
368
432
  }
369
433
  };
@@ -384,33 +448,57 @@ export const Dropdown: ParentComponent<DropdownProps> = (props) => {
384
448
  };
385
449
 
386
450
  return (
387
- <Show when={mounted()}>
388
- <Portal>
451
+ <DropdownContext.Provider value={{ toggle, setTrigger, setContent }}>
452
+ {local.children}
453
+
454
+ {/* Trigger 슬롯 렌더링 (wrapper div에 click/keyboard handler 부착) */}
455
+ <Show when={triggerSlot()}>
389
456
  <div
390
- {...rest}
391
- ref={setPopupRef}
392
- data-dropdown
393
- class={twMerge(
394
- clsx(
395
- "fixed", // 기본 position: fixed로 설정하여 offsetWidth 측정 정확하게
396
- "z-dropdown",
397
- "bg-white dark:bg-base-800",
398
- "border",
399
- borderSubtle,
400
- "shadow-lg dark:shadow-black/30",
401
- "rounded-md",
402
- "overflow-y-auto",
403
- animationClass(),
404
- ),
405
- local.class,
406
- )}
407
- style={mergeStyles(computedStyle(), local.style, { "max-height": `${maxHeight()}px` })}
408
- onTransitionEnd={handleTransitionEnd}
409
- onKeyDown={handlePopupKeyDown}
457
+ ref={(el) => {
458
+ triggerRef = el;
459
+ }}
460
+ data-dropdown-trigger
461
+ onClick={toggle}
462
+ onKeyDown={handleTriggerKeyDown}
410
463
  >
411
- {local.children}
464
+ {triggerSlot()!()}
412
465
  </div>
413
- </Portal>
414
- </Show>
466
+ </Show>
467
+
468
+ {/* Content 슬롯: Portal + 팝업 */}
469
+ <Show when={mounted()}>
470
+ <Portal>
471
+ <div
472
+ {...rest}
473
+ ref={setPopupRef}
474
+ data-dropdown
475
+ class={twMerge(
476
+ clsx(
477
+ "fixed",
478
+ "z-dropdown",
479
+ "bg-white dark:bg-base-800",
480
+ "border",
481
+ borderSubtle,
482
+ "shadow-lg dark:shadow-black/30",
483
+ "rounded-md",
484
+ "overflow-y-auto",
485
+ animationClass(),
486
+ ),
487
+ local.class,
488
+ )}
489
+ style={mergeStyles(computedStyle(), local.style, {
490
+ "max-height": `${maxHeight()}px`,
491
+ })}
492
+ onTransitionEnd={handleTransitionEnd}
493
+ onKeyDown={handlePopupKeyDown}
494
+ >
495
+ <Show when={contentSlot()}>{contentSlot()!()}</Show>
496
+ </div>
497
+ </Portal>
498
+ </Show>
499
+ </DropdownContext.Provider>
415
500
  );
416
- };
501
+ }) as DropdownComponent;
502
+
503
+ Dropdown.Trigger = DropdownTrigger;
504
+ Dropdown.Content = DropdownContent;
@@ -8,7 +8,7 @@ import { themeTokens } from "../../../styles/tokens.styles";
8
8
 
9
9
  const baseClass = clsx(
10
10
  "fixed",
11
- "top-4",
11
+ "top-8",
12
12
  "right-4",
13
13
  "z-50",
14
14
  "flex",
@@ -31,15 +31,9 @@ const themeClasses: Record<string, string> = {
31
31
  };
32
32
 
33
33
  const contentClass = clsx("flex flex-col", "gap-0.5", "min-w-0");
34
- const messageClass = clsx("text-sm", "opacity-90", "overflow-auto");
34
+ const messageClass = clsx("opacity-90", "overflow-auto");
35
35
  const actionsClass = clsx("flex items-center", "gap-2", "shrink-0");
36
- const actionButtonClass = clsx(
37
- "rounded",
38
- "bg-white/20",
39
- "px-3 py-1",
40
- "text-sm",
41
- "hover:bg-white/30",
42
- );
36
+ const actionButtonClass = clsx("rounded", "bg-white/20", "px-3 py-1", "hover:bg-white/30");
43
37
  const dismissButtonClass = clsx("rounded", "p-1", "hover:bg-white/20");
44
38
 
45
39
  export const NotificationBanner: Component = () => {
@@ -53,7 +53,6 @@ const itemTimeClass = clsx("mt-1 text-xs", "text-base-400");
53
53
  export const NotificationBell: Component<NotificationBellProps> = (props) => {
54
54
  const notification = useNotification();
55
55
  const [open, setOpen] = createSignal(false);
56
- let buttonRef: HTMLButtonElement | undefined;
57
56
 
58
57
  const handleClear = () => {
59
58
  notification.clear();
@@ -73,65 +72,60 @@ export const NotificationBell: Component<NotificationBellProps> = (props) => {
73
72
  <NotificationBanner />
74
73
  </Show>
75
74
 
76
- <button
77
- ref={(el) => (buttonRef = el)}
78
- type="button"
79
- data-notification-bell
80
- class={buttonClass}
81
- aria-label={`알림 ${notification.unreadCount()}개`}
82
- aria-haspopup="true"
83
- aria-expanded={open()}
84
- onClick={() => handleOpenChange(!open())}
85
- >
86
- <Icon icon={IconBell} />
87
- <Show when={notification.unreadCount() > 0}>
88
- <span data-notification-badge aria-hidden="true" class={badgeClass}>
89
- {notification.unreadCount()}
90
- </span>
91
- </Show>
92
- </button>
75
+ <Dropdown open={open()} onOpenChange={handleOpenChange} maxHeight={400}>
76
+ <Dropdown.Trigger>
77
+ <button
78
+ type="button"
79
+ data-notification-bell
80
+ class={buttonClass}
81
+ aria-label={`알림 ${notification.unreadCount()}개`}
82
+ aria-haspopup="true"
83
+ aria-expanded={open()}
84
+ >
85
+ <Icon icon={IconBell} />
86
+ <Show when={notification.unreadCount() > 0}>
87
+ <span data-notification-badge aria-hidden="true" class={badgeClass}>
88
+ {notification.unreadCount()}
89
+ </span>
90
+ </Show>
91
+ </button>
92
+ </Dropdown.Trigger>
93
+ <Dropdown.Content>
94
+ <div class="w-80 p-2">
95
+ <div class={dropdownHeaderClass}>
96
+ <span class="font-bold">알림</span>
97
+ <Show when={notification.items().length > 0}>
98
+ <button
99
+ type="button"
100
+ data-notification-clear
101
+ class={clearButtonClass}
102
+ onClick={handleClear}
103
+ >
104
+ 전체 삭제
105
+ </button>
106
+ </Show>
107
+ </div>
93
108
 
94
- <Dropdown
95
- triggerRef={() => buttonRef}
96
- open={open()}
97
- onOpenChange={handleOpenChange}
98
- maxHeight={400}
99
- class="w-80"
100
- >
101
- <div class="p-2">
102
- <div class={dropdownHeaderClass}>
103
- <span class="font-bold">알림</span>
104
- <Show when={notification.items().length > 0}>
105
- <button
106
- type="button"
107
- data-notification-clear
108
- class={clearButtonClass}
109
- onClick={handleClear}
110
- >
111
- 전체 삭제
112
- </button>
109
+ <Show
110
+ when={notification.items().length > 0}
111
+ fallback={<div class={emptyClass}>알림이 없습니다</div>}
112
+ >
113
+ <div class={listClass}>
114
+ <For each={[...notification.items()].reverse()}>
115
+ {(item) => (
116
+ <div class={clsx(itemBaseClass, themeStyles[item.theme])}>
117
+ <div class="font-medium">{item.title}</div>
118
+ <Show when={item.message}>
119
+ <pre class={itemMessageClass}>{item.message}</pre>
120
+ </Show>
121
+ <div class={itemTimeClass}>{item.createdAt.toLocaleTimeString()}</div>
122
+ </div>
123
+ )}
124
+ </For>
125
+ </div>
113
126
  </Show>
114
127
  </div>
115
-
116
- <Show
117
- when={notification.items().length > 0}
118
- fallback={<div class={emptyClass}>알림이 없습니다</div>}
119
- >
120
- <div class={listClass}>
121
- <For each={[...notification.items()].reverse()}>
122
- {(item) => (
123
- <div class={clsx(itemBaseClass, themeStyles[item.theme])}>
124
- <div class="font-medium">{item.title}</div>
125
- <Show when={item.message}>
126
- <pre class={itemMessageClass}>{item.message}</pre>
127
- </Show>
128
- <div class={itemTimeClass}>{item.createdAt.toLocaleTimeString()}</div>
129
- </div>
130
- )}
131
- </For>
132
- </div>
133
- </Show>
134
- </div>
128
+ </Dropdown.Content>
135
129
  </Dropdown>
136
130
  </>
137
131
  );