@simplysm/solid 13.0.53 → 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 (222) hide show
  1. package/README.md +6 -2
  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.css +28 -10
  56. package/dist/components/data/sheet/DataSheet.js +1 -1
  57. package/dist/components/data/sheet/DataSheet.js.map +2 -2
  58. package/dist/components/data/sheet/DataSheet.styles.d.ts.map +1 -1
  59. package/dist/components/data/sheet/DataSheet.styles.js +1 -1
  60. package/dist/components/data/sheet/DataSheet.styles.js.map +1 -1
  61. package/dist/components/disclosure/Dialog.d.ts +16 -10
  62. package/dist/components/disclosure/Dialog.d.ts.map +1 -1
  63. package/dist/components/disclosure/Dialog.js +126 -91
  64. package/dist/components/disclosure/Dialog.js.map +2 -2
  65. package/dist/components/disclosure/DialogContext.d.ts +2 -4
  66. package/dist/components/disclosure/DialogContext.d.ts.map +1 -1
  67. package/dist/components/disclosure/DialogContext.js.map +1 -1
  68. package/dist/components/disclosure/DialogProvider.d.ts.map +1 -1
  69. package/dist/components/disclosure/DialogProvider.js +14 -9
  70. package/dist/components/disclosure/DialogProvider.js.map +2 -2
  71. package/dist/components/disclosure/Dropdown.d.ts +46 -22
  72. package/dist/components/disclosure/Dropdown.d.ts.map +1 -1
  73. package/dist/components/disclosure/Dropdown.js +100 -65
  74. package/dist/components/disclosure/Dropdown.js.map +2 -2
  75. package/dist/components/feedback/notification/NotificationBanner.d.ts.map +1 -1
  76. package/dist/components/feedback/notification/NotificationBanner.js +3 -3
  77. package/dist/components/feedback/notification/NotificationBanner.js.map +1 -1
  78. package/dist/components/feedback/notification/NotificationBell.d.ts.map +1 -1
  79. package/dist/components/feedback/notification/NotificationBell.js +84 -84
  80. package/dist/components/feedback/notification/NotificationBell.js.map +2 -2
  81. package/dist/components/form-control/Invalid.js +1 -1
  82. package/dist/components/form-control/combobox/Combobox.d.ts +6 -3
  83. package/dist/components/form-control/combobox/Combobox.d.ts.map +1 -1
  84. package/dist/components/form-control/combobox/Combobox.js +150 -168
  85. package/dist/components/form-control/combobox/Combobox.js.map +2 -2
  86. package/dist/components/form-control/combobox/ComboboxContext.d.ts +3 -0
  87. package/dist/components/form-control/combobox/ComboboxContext.d.ts.map +1 -1
  88. package/dist/components/form-control/combobox/ComboboxContext.js.map +1 -1
  89. package/dist/components/form-control/date-range-picker/DateRangePicker.d.ts +0 -2
  90. package/dist/components/form-control/date-range-picker/DateRangePicker.d.ts.map +1 -1
  91. package/dist/components/form-control/date-range-picker/DateRangePicker.js +9 -17
  92. package/dist/components/form-control/date-range-picker/DateRangePicker.js.map +2 -2
  93. package/dist/components/form-control/field/DatePicker.d.ts.map +1 -1
  94. package/dist/components/form-control/field/DatePicker.js +3 -2
  95. package/dist/components/form-control/field/DatePicker.js.map +2 -2
  96. package/dist/components/form-control/field/DateTimePicker.d.ts.map +1 -1
  97. package/dist/components/form-control/field/DateTimePicker.js +3 -2
  98. package/dist/components/form-control/field/DateTimePicker.js.map +2 -2
  99. package/dist/components/form-control/field/Field.styles.d.ts.map +1 -1
  100. package/dist/components/form-control/field/Field.styles.js +2 -1
  101. package/dist/components/form-control/field/Field.styles.js.map +1 -1
  102. package/dist/components/form-control/field/NumberInput.d.ts +15 -5
  103. package/dist/components/form-control/field/NumberInput.d.ts.map +1 -1
  104. package/dist/components/form-control/field/NumberInput.js +181 -141
  105. package/dist/components/form-control/field/NumberInput.js.map +2 -2
  106. package/dist/components/form-control/field/TextInput.d.ts +9 -5
  107. package/dist/components/form-control/field/TextInput.d.ts.map +1 -1
  108. package/dist/components/form-control/field/TextInput.js +199 -154
  109. package/dist/components/form-control/field/TextInput.js.map +2 -2
  110. package/dist/components/form-control/field/TimePicker.d.ts.map +1 -1
  111. package/dist/components/form-control/field/TimePicker.js +3 -2
  112. package/dist/components/form-control/field/TimePicker.js.map +2 -2
  113. package/dist/components/form-control/select/Select.d.ts +3 -3
  114. package/dist/components/form-control/select/Select.d.ts.map +1 -1
  115. package/dist/components/form-control/select/Select.js +116 -100
  116. package/dist/components/form-control/select/Select.js.map +2 -2
  117. package/dist/components/form-control/select/SelectContext.d.ts +9 -1
  118. package/dist/components/form-control/select/SelectContext.d.ts.map +1 -1
  119. package/dist/components/form-control/select/SelectContext.js.map +1 -1
  120. package/dist/components/form-control/select/SelectItem.d.ts.map +1 -1
  121. package/dist/components/form-control/select/SelectItem.js +77 -67
  122. package/dist/components/form-control/select/SelectItem.js.map +2 -2
  123. package/dist/components/form-control/state-preset/StatePreset.d.ts.map +1 -1
  124. package/dist/components/form-control/state-preset/StatePreset.js +1 -1
  125. package/dist/components/form-control/state-preset/StatePreset.js.map +1 -1
  126. package/dist/components/layout/topbar/Topbar.d.ts +2 -0
  127. package/dist/components/layout/topbar/Topbar.d.ts.map +1 -1
  128. package/dist/components/layout/topbar/Topbar.js +2 -0
  129. package/dist/components/layout/topbar/Topbar.js.map +2 -2
  130. package/dist/components/layout/topbar/TopbarActions.d.ts +3 -0
  131. package/dist/components/layout/topbar/TopbarActions.d.ts.map +1 -0
  132. package/dist/components/layout/topbar/TopbarActions.js +17 -0
  133. package/dist/components/layout/topbar/TopbarActions.js.map +6 -0
  134. package/dist/components/layout/topbar/TopbarContainer.d.ts +1 -1
  135. package/dist/components/layout/topbar/TopbarContainer.d.ts.map +1 -1
  136. package/dist/components/layout/topbar/TopbarContainer.js +21 -12
  137. package/dist/components/layout/topbar/TopbarContainer.js.map +2 -2
  138. package/dist/components/layout/topbar/TopbarContext.d.ts +9 -0
  139. package/dist/components/layout/topbar/TopbarContext.d.ts.map +1 -0
  140. package/dist/components/layout/topbar/TopbarContext.js +29 -0
  141. package/dist/components/layout/topbar/TopbarContext.js.map +6 -0
  142. package/dist/components/layout/topbar/TopbarMenu.d.ts.map +1 -1
  143. package/dist/components/layout/topbar/TopbarMenu.js +63 -57
  144. package/dist/components/layout/topbar/TopbarMenu.js.map +2 -2
  145. package/dist/components/layout/topbar/TopbarUser.d.ts.map +1 -1
  146. package/dist/components/layout/topbar/TopbarUser.js +53 -54
  147. package/dist/components/layout/topbar/TopbarUser.js.map +2 -2
  148. package/dist/hooks/createControllableStore.d.ts +29 -0
  149. package/dist/hooks/createControllableStore.d.ts.map +1 -0
  150. package/dist/hooks/createControllableStore.js +19 -0
  151. package/dist/hooks/createControllableStore.js.map +6 -0
  152. package/dist/index.d.ts +6 -1
  153. package/dist/index.d.ts.map +1 -1
  154. package/dist/index.js +7 -2
  155. package/dist/index.js.map +1 -1
  156. package/dist/styles/patterns.styles.d.ts.map +1 -1
  157. package/dist/styles/patterns.styles.js +7 -1
  158. package/dist/styles/patterns.styles.js.map +1 -1
  159. package/docs/data-components.md +428 -0
  160. package/docs/disclosure.md +65 -35
  161. package/docs/form-controls.md +18 -3
  162. package/docs/helpers.md +0 -39
  163. package/docs/hooks.md +39 -0
  164. package/docs/layout.md +70 -1
  165. package/package.json +4 -3
  166. package/src/components/data/crud-detail/CrudDetail.tsx +346 -0
  167. package/src/components/data/crud-detail/CrudDetailAfter.tsx +19 -0
  168. package/src/components/data/crud-detail/CrudDetailBefore.tsx +19 -0
  169. package/src/components/data/crud-detail/CrudDetailTools.tsx +19 -0
  170. package/src/components/data/crud-detail/types.ts +58 -0
  171. package/src/components/data/crud-sheet/CrudSheet.tsx +628 -0
  172. package/src/components/data/crud-sheet/CrudSheetColumn.tsx +34 -0
  173. package/src/components/data/crud-sheet/CrudSheetFilter.tsx +21 -0
  174. package/src/components/data/crud-sheet/CrudSheetHeader.tsx +19 -0
  175. package/src/components/data/crud-sheet/CrudSheetTools.tsx +21 -0
  176. package/src/components/data/crud-sheet/types.ts +133 -0
  177. package/src/components/data/kanban/Kanban.tsx +72 -65
  178. package/src/components/data/kanban/KanbanContext.ts +7 -1
  179. package/src/components/data/list/ListItem.tsx +31 -18
  180. package/src/components/data/sheet/DataSheet.css +28 -10
  181. package/src/components/data/sheet/DataSheet.styles.ts +1 -1
  182. package/src/components/data/sheet/DataSheet.tsx +1 -1
  183. package/src/components/disclosure/Dialog.tsx +143 -105
  184. package/src/components/disclosure/DialogContext.ts +2 -4
  185. package/src/components/disclosure/DialogProvider.tsx +4 -2
  186. package/src/components/disclosure/Dropdown.tsx +174 -86
  187. package/src/components/feedback/notification/NotificationBanner.tsx +3 -9
  188. package/src/components/feedback/notification/NotificationBell.tsx +51 -57
  189. package/src/components/form-control/Invalid.tsx +1 -1
  190. package/src/components/form-control/combobox/Combobox.tsx +109 -133
  191. package/src/components/form-control/combobox/ComboboxContext.ts +4 -1
  192. package/src/components/form-control/date-range-picker/DateRangePicker.tsx +6 -16
  193. package/src/components/form-control/field/DatePicker.tsx +4 -1
  194. package/src/components/form-control/field/DateTimePicker.tsx +3 -0
  195. package/src/components/form-control/field/Field.styles.ts +1 -0
  196. package/src/components/form-control/field/NumberInput.tsx +131 -86
  197. package/src/components/form-control/field/TextInput.tsx +139 -88
  198. package/src/components/form-control/field/TimePicker.tsx +3 -0
  199. package/src/components/form-control/select/Select.tsx +85 -67
  200. package/src/components/form-control/select/SelectContext.ts +12 -1
  201. package/src/components/form-control/select/SelectItem.tsx +39 -18
  202. package/src/components/form-control/state-preset/StatePreset.tsx +1 -0
  203. package/src/components/layout/topbar/Topbar.tsx +3 -0
  204. package/src/components/layout/topbar/TopbarActions.tsx +8 -0
  205. package/src/components/layout/topbar/TopbarContainer.tsx +9 -5
  206. package/src/components/layout/topbar/TopbarContext.ts +36 -0
  207. package/src/components/layout/topbar/TopbarMenu.tsx +52 -55
  208. package/src/components/layout/topbar/TopbarUser.tsx +28 -31
  209. package/src/hooks/createControllableStore.ts +47 -0
  210. package/src/index.ts +6 -1
  211. package/src/styles/patterns.styles.ts +7 -1
  212. package/tailwind.css +4 -0
  213. package/dist/helpers/splitSlots.d.ts +0 -25
  214. package/dist/helpers/splitSlots.d.ts.map +0 -1
  215. package/dist/helpers/splitSlots.js +0 -25
  216. package/dist/helpers/splitSlots.js.map +0 -6
  217. package/dist/hooks/createItemTemplate.d.ts +0 -17
  218. package/dist/hooks/createItemTemplate.d.ts.map +0 -1
  219. package/dist/hooks/createItemTemplate.js +0 -40
  220. package/dist/hooks/createItemTemplate.js.map +0 -6
  221. package/src/helpers/splitSlots.ts +0 -51
  222. package/src/hooks/createItemTemplate.tsx +0 -42
@@ -5,6 +5,7 @@ import {
5
5
  For,
6
6
  type JSX,
7
7
  type ParentComponent,
8
+ onCleanup,
8
9
  Show,
9
10
  splitProps,
10
11
  } from "solid-js";
@@ -15,17 +16,18 @@ import { Icon } from "../../display/Icon";
15
16
  import { Dropdown } from "../../disclosure/Dropdown";
16
17
  import { List } from "../../data/list/List";
17
18
  import { SelectContext, type SelectContextValue } from "./SelectContext";
19
+ import { useSelectContext } from "./SelectContext";
18
20
  import { SelectItem } from "./SelectItem";
19
21
  import { ripple } from "../../../directives/ripple";
20
- import { splitSlots } from "../../../helpers/splitSlots";
21
22
  import { borderDefault, type ComponentSize, textMuted } from "../../../styles/tokens.styles";
22
23
  import { createControllableSignal } from "../../../hooks/createControllableSignal";
23
- import { createItemTemplate } from "../../../hooks/createItemTemplate";
24
24
  import { chevronWrapperClass, getTriggerClass } from "../DropdownTrigger.styles";
25
25
  import { Invalid } from "../Invalid";
26
26
 
27
27
  void ripple;
28
28
 
29
+ type SlotAccessor = (() => JSX.Element) | undefined;
30
+
29
31
  // Select 전용 스타일
30
32
  const multiTagClass = clsx("rounded", "bg-base-200 px-1", "dark:bg-base-600");
31
33
  const selectedValueClass = clsx("flex-1", "whitespace-nowrap");
@@ -37,8 +39,9 @@ interface SelectActionProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement>
37
39
 
38
40
  const SelectAction: ParentComponent<SelectActionProps> = (props) => {
39
41
  const [local, rest] = splitProps(props, ["children", "class"]);
42
+ const ctx = useSelectContext();
40
43
 
41
- return (
44
+ ctx.setAction(() => (
42
45
  <button
43
46
  {...rest}
44
47
  type="button"
@@ -63,17 +66,31 @@ const SelectAction: ParentComponent<SelectActionProps> = (props) => {
63
66
  >
64
67
  {local.children}
65
68
  </button>
66
- );
69
+ ));
70
+ onCleanup(() => ctx.setAction(undefined));
71
+ return null;
67
72
  };
68
73
 
69
74
  /**
70
75
  * 드롭다운 상단 커스텀 영역 서브 컴포넌트
71
76
  */
72
- const SelectHeader: ParentComponent = (props) => <div data-select-header>{props.children}</div>;
77
+ const SelectHeader: ParentComponent = (props) => {
78
+ const ctx = useSelectContext();
79
+ // eslint-disable-next-line solid/reactivity -- 슬롯 accessor로 저장, JSX tracked scope에서 호출됨
80
+ ctx.setHeader(() => props.children);
81
+ onCleanup(() => ctx.setHeader(undefined));
82
+ return null;
83
+ };
73
84
 
74
- const { TemplateSlot: SelectItemTemplate, getTemplate: getSelectItemTemplate } = createItemTemplate<
75
- [item: unknown, index: number, depth: number]
76
- >("data-select-item-template");
85
+ const SelectItemTemplate = <TArgs extends unknown[]>(props: {
86
+ children: (...args: TArgs) => JSX.Element;
87
+ }) => {
88
+ const ctx = useSelectContext();
89
+ // eslint-disable-next-line solid/reactivity -- 렌더 함수를 signal에 저장, JSX tracked scope에서 호출됨
90
+ ctx.setItemTemplate(props.children as (...args: unknown[]) => JSX.Element);
91
+ onCleanup(() => ctx.setItemTemplate(undefined));
92
+ return null;
93
+ };
77
94
 
78
95
  // Props 정의
79
96
 
@@ -214,8 +231,6 @@ export const Select: SelectComponent = <T,>(props: SelectProps<T>) => {
214
231
  "touchMode",
215
232
  ]);
216
233
 
217
- let triggerRef!: HTMLDivElement;
218
-
219
234
  const [open, setOpen] = createSignal(false);
220
235
 
221
236
  // 선택된 값 관리 (controlled/uncontrolled 패턴)
@@ -256,18 +271,26 @@ export const Select: SelectComponent = <T,>(props: SelectProps<T>) => {
256
271
  setOpen(false);
257
272
  };
258
273
 
274
+ // 슬롯 signals
275
+ const [header, _setHeader] = createSignal<SlotAccessor>();
276
+ const setHeader = (content: SlotAccessor) => _setHeader(() => content);
277
+ const [action, _setAction] = createSignal<SlotAccessor>();
278
+ const setAction = (content: SlotAccessor) => _setAction(() => content);
279
+ const [itemTemplate, _setItemTemplate] = createSignal<
280
+ ((...args: unknown[]) => JSX.Element) | undefined
281
+ >();
282
+ const setItemTemplate = (fn: ((...args: unknown[]) => JSX.Element) | undefined) =>
283
+ _setItemTemplate(() => fn);
284
+
259
285
  // Context 값
260
286
  const contextValue: SelectContextValue<T> = {
261
287
  multiple: () => local.multiple ?? false,
262
288
  isSelected,
263
289
  toggleValue,
264
290
  closeDropdown,
265
- };
266
-
267
- // 트리거 클릭
268
- const handleTriggerClick = () => {
269
- if (local.disabled) return;
270
- setOpen((v) => !v);
291
+ setHeader,
292
+ setAction,
293
+ setItemTemplate,
271
294
  };
272
295
 
273
296
  // 트리거 키보드 처리 (Enter/Space만 처리, ArrowUp/Down은 Dropdown이 처리)
@@ -297,32 +320,26 @@ export const Select: SelectComponent = <T,>(props: SelectProps<T>) => {
297
320
  class: local.class,
298
321
  });
299
322
 
300
- // 내부 컴포넌트: Provider 안에서 children을 resolve
323
+ // 내부 컴포넌트: Provider 안에서 children을 resolve하여 슬롯 등록을 트리거
301
324
  const SelectInner: ParentComponent = (innerProps) => {
325
+ // children() resolve로 서브 컴포넌트 등록 트리거 (Header, Action, ItemTemplate은 null 반환)
302
326
  const resolved = children(() => innerProps.children);
303
- const [slots, items] = splitSlots(resolved, [
304
- "selectHeader",
305
- "selectAction",
306
- "selectItemTemplate",
307
- ] as const);
308
327
 
309
328
  // itemTemplate 함수 추출
310
329
  const getItemTemplate = ():
311
330
  | ((item: T, index: number, depth: number) => JSX.Element)
312
331
  | undefined => {
313
- return getSelectItemTemplate(slots().selectItemTemplate) as
314
- | ((item: T, index: number, depth: number) => JSX.Element)
315
- | undefined;
332
+ return itemTemplate() as ((item: T, index: number, depth: number) => JSX.Element) | undefined;
316
333
  };
317
334
 
318
335
  // items 재귀 렌더링
319
336
  const renderItems = (itemList: T[], depth: number): JSX.Element => {
320
- const itemTemplate = getItemTemplate();
337
+ const tpl = getItemTemplate();
321
338
  return (
322
339
  <For each={itemList}>
323
340
  {(item, index) => (
324
341
  <SelectItem value={item}>
325
- {itemTemplate ? itemTemplate(item, index(), depth) : String(item)}
342
+ {tpl ? tpl(item, index(), depth) : String(item)}
326
343
  <Show when={local.getChildren?.(item, index(), depth)} keyed>
327
344
  {(itemChildren) => (
328
345
  <Show when={itemChildren.length > 0}>
@@ -343,9 +360,9 @@ export const Select: SelectComponent = <T,>(props: SelectProps<T>) => {
343
360
  if (local.renderValue) {
344
361
  return local.renderValue(value);
345
362
  }
346
- const itemTemplate = getItemTemplate();
347
- if (itemTemplate) {
348
- return itemTemplate(value, 0, 0);
363
+ const tpl = getItemTemplate();
364
+ if (tpl) {
365
+ return tpl(value, 0, 0);
349
366
  }
350
367
  return <>{String(value)}</>;
351
368
  };
@@ -372,33 +389,43 @@ export const Select: SelectComponent = <T,>(props: SelectProps<T>) => {
372
389
 
373
390
  return (
374
391
  <div {...rest} data-select class={clsx("group", local.inset ? "flex" : "inline-flex")}>
375
- <div
376
- ref={triggerRef}
377
- use:ripple={!local.disabled}
378
- role="combobox"
379
- aria-haspopup="listbox"
380
- aria-expanded={open()}
381
- aria-disabled={local.disabled || undefined}
382
- aria-required={local.required || undefined}
383
- tabIndex={local.disabled ? -1 : 0}
384
- class={twMerge(
385
- getTriggerClassName(),
386
- slots().selectAction.length > 0 &&
387
- clsx(
388
- "rounded-r-none border-r-0",
389
- "group-focus-within:border-primary-400 dark:group-focus-within:border-primary-400",
390
- ),
391
- )}
392
- style={local.style}
393
- onClick={handleTriggerClick}
394
- onKeyDown={handleTriggerKeyDown}
395
- >
396
- <div class={selectedValueClass}>{renderSelectedValue()}</div>
397
- <div class={chevronWrapperClass}>
398
- <Icon icon={IconChevronDown} size="1em" />
399
- </div>
400
- </div>
401
- <Show when={slots().selectAction.length > 0}>
392
+ <Dropdown disabled={local.disabled} open={open()} onOpenChange={setOpen} keyboardNav>
393
+ <Dropdown.Trigger>
394
+ <div
395
+ use:ripple={!local.disabled}
396
+ role="combobox"
397
+ aria-haspopup="listbox"
398
+ aria-expanded={open()}
399
+ aria-disabled={local.disabled || undefined}
400
+ aria-required={local.required || undefined}
401
+ tabIndex={local.disabled ? -1 : 0}
402
+ class={twMerge(
403
+ getTriggerClassName(),
404
+ action() !== undefined &&
405
+ clsx(
406
+ "rounded-r-none border-r-0",
407
+ "group-focus-within:border-primary-400 dark:group-focus-within:border-primary-400",
408
+ ),
409
+ )}
410
+ style={local.style}
411
+ onKeyDown={handleTriggerKeyDown}
412
+ >
413
+ <div class={selectedValueClass}>{renderSelectedValue()}</div>
414
+ <div class={chevronWrapperClass}>
415
+ <Icon icon={IconChevronDown} size="1em" />
416
+ </div>
417
+ </div>
418
+ </Dropdown.Trigger>
419
+ <Dropdown.Content>
420
+ <Show when={header()}>{header()!()}</Show>
421
+ <List inset role="listbox">
422
+ <Show when={local.items} fallback={resolved()}>
423
+ {renderItems(local.items!, 0)}
424
+ </Show>
425
+ </List>
426
+ </Dropdown.Content>
427
+ </Dropdown>
428
+ <Show when={action()}>
402
429
  <div
403
430
  class={clsx(
404
431
  "contents",
@@ -406,18 +433,9 @@ export const Select: SelectComponent = <T,>(props: SelectProps<T>) => {
406
433
  "[&>[data-select-action]+[data-select-action]]:-ml-px",
407
434
  )}
408
435
  >
409
- {slots().selectAction}
436
+ {action()!()}
410
437
  </div>
411
438
  </Show>
412
-
413
- <Dropdown triggerRef={() => triggerRef} open={open()} onOpenChange={setOpen} keyboardNav>
414
- <Show when={slots().selectHeader.length > 0}>{slots().selectHeader.single()}</Show>
415
- <List inset role="listbox">
416
- <Show when={local.items} fallback={items()}>
417
- {renderItems(local.items!, 0)}
418
- </Show>
419
- </List>
420
- </Dropdown>
421
439
  </div>
422
440
  );
423
441
  };
@@ -1,4 +1,6 @@
1
- import { createContext, useContext, type Accessor } from "solid-js";
1
+ import { createContext, useContext, type Accessor, type JSX } from "solid-js";
2
+
3
+ type SlotAccessor = (() => JSX.Element) | undefined;
2
4
 
3
5
  export interface SelectContextValue<TValue = unknown> {
4
6
  /** 다중 선택 모드 여부 */
@@ -12,6 +14,15 @@ export interface SelectContextValue<TValue = unknown> {
12
14
 
13
15
  /** 드롭다운 닫기 */
14
16
  closeDropdown: () => void;
17
+
18
+ /** 헤더 슬롯 등록 */
19
+ setHeader: (content: SlotAccessor) => void;
20
+
21
+ /** 액션 슬롯 등록 */
22
+ setAction: (content: SlotAccessor) => void;
23
+
24
+ /** 아이템 템플릿 등록 */
25
+ setItemTemplate: (fn: ((...args: unknown[]) => JSX.Element) | undefined) => void;
15
26
  }
16
27
 
17
28
  export const SelectContext = createContext<SelectContextValue>();
@@ -1,4 +1,13 @@
1
- import { children, type JSX, type ParentComponent, Show, splitProps } from "solid-js";
1
+ import {
2
+ createContext,
3
+ createSignal,
4
+ type JSX,
5
+ onCleanup,
6
+ type ParentComponent,
7
+ Show,
8
+ splitProps,
9
+ useContext,
10
+ } from "solid-js";
2
11
  import { twMerge } from "tailwind-merge";
3
12
  import { IconCheck } from "@tabler/icons-solidjs";
4
13
  import { Icon } from "../../display/Icon";
@@ -6,7 +15,6 @@ import { useSelectContext } from "./SelectContext";
6
15
  import { ripple } from "../../../directives/ripple";
7
16
  import { List } from "../../data/list/List";
8
17
  import { Collapse } from "../../disclosure/Collapse";
9
- import { splitSlots } from "../../../helpers/splitSlots";
10
18
  import {
11
19
  listItemBaseClass,
12
20
  listItemSelectedClass,
@@ -18,17 +26,24 @@ import {
18
26
 
19
27
  void ripple;
20
28
 
29
+ type SlotAccessor = (() => JSX.Element) | undefined;
30
+
31
+ interface SelectItemSlotsContextValue {
32
+ setChildren: (content: SlotAccessor) => void;
33
+ }
34
+
35
+ const SelectItemSlotsContext = createContext<SelectItemSlotsContextValue>();
36
+
21
37
  /**
22
38
  * 중첩 아이템을 담는 서브 컴포넌트
23
39
  */
24
- const SelectItemChildren: ParentComponent = (props) => (
25
- <div class="flex" data-select-item-children>
26
- <div class={listItemIndentGuideClass} />
27
- <List inset class="flex-1">
28
- {props.children}
29
- </List>
30
- </div>
31
- );
40
+ const SelectItemChildren: ParentComponent = (props) => {
41
+ const ctx = useContext(SelectItemSlotsContext)!;
42
+ // eslint-disable-next-line solid/reactivity -- slot accessor: children is lazily read at render time
43
+ ctx.setChildren(() => props.children);
44
+ onCleanup(() => ctx.setChildren(undefined));
45
+ return null;
46
+ };
32
47
 
33
48
  export interface SelectItemProps<TValue = unknown> extends Omit<
34
49
  JSX.ButtonHTMLAttributes<HTMLButtonElement>,
@@ -68,10 +83,9 @@ export const SelectItem: SelectItemComponent = <T,>(
68
83
 
69
84
  const context = useSelectContext<T>();
70
85
 
71
- const resolved = children(() => local.children);
72
- const [slots, content] = splitSlots(resolved, ["selectItemChildren"] as const);
73
-
74
- const hasChildren = () => slots().selectItemChildren.length > 0;
86
+ const [childrenSlot, _setChildrenSlot] = createSignal<SlotAccessor>();
87
+ const setChildrenSlot = (content: SlotAccessor) => _setChildrenSlot(() => content);
88
+ const hasChildren = () => childrenSlot() !== undefined;
75
89
  const isSelected = () => context.isSelected(local.value);
76
90
  const useRipple = () => !local.disabled;
77
91
 
@@ -97,7 +111,7 @@ export const SelectItem: SelectItemComponent = <T,>(
97
111
  const getCheckIconClass = () => getListItemSelectedIconClass(isSelected());
98
112
 
99
113
  return (
100
- <>
114
+ <SelectItemSlotsContext.Provider value={{ setChildren: setChildrenSlot }}>
101
115
  <button
102
116
  {...rest}
103
117
  type="button"
@@ -114,12 +128,19 @@ export const SelectItem: SelectItemComponent = <T,>(
114
128
  <Show when={context.multiple() && !hasChildren()}>
115
129
  <Icon icon={IconCheck} class={getCheckIconClass()} />
116
130
  </Show>
117
- <span class={listItemContentClass}>{content()}</span>
131
+ <span class={listItemContentClass}>{local.children}</span>
118
132
  </button>
119
133
  <Show when={hasChildren()}>
120
- <Collapse open={true}>{slots().selectItemChildren.single()}</Collapse>
134
+ <Collapse open={true}>
135
+ <div class="flex">
136
+ <div class={listItemIndentGuideClass} />
137
+ <List inset class="flex-1">
138
+ {childrenSlot()!()}
139
+ </List>
140
+ </div>
141
+ </Collapse>
121
142
  </Show>
122
- </>
143
+ </SelectItemSlotsContext.Provider>
123
144
  );
124
145
  };
125
146
 
@@ -289,6 +289,7 @@ function StatePresetInner<TValue>(props: StatePresetProps<TValue>): JSX.Element
289
289
  type="text"
290
290
  class={resolvedInputClass()}
291
291
  placeholder="이름..."
292
+ autocomplete="one-time-code"
292
293
  value={inputValue()}
293
294
  onInput={(e) => setInputValue(e.currentTarget.value)}
294
295
  onKeyDown={handleInputKeyDown}
@@ -5,6 +5,7 @@ import { Icon } from "../../display/Icon";
5
5
  import { twMerge } from "tailwind-merge";
6
6
  import { Button } from "../../form-control/Button";
7
7
  import { useSidebarContextOptional } from "../sidebar/SidebarContext";
8
+ import { TopbarActions } from "./TopbarActions";
8
9
  import { TopbarContainer } from "./TopbarContainer";
9
10
  import { TopbarMenu } from "./TopbarMenu";
10
11
  import { TopbarUser } from "./TopbarUser";
@@ -58,6 +59,7 @@ export interface TopbarProps extends JSX.HTMLAttributes<HTMLElement> {
58
59
  * ```
59
60
  */
60
61
  interface TopbarComponent extends ParentComponent<TopbarProps> {
62
+ Actions: typeof TopbarActions;
61
63
  Container: typeof TopbarContainer;
62
64
  Menu: typeof TopbarMenu;
63
65
  User: typeof TopbarUser;
@@ -88,6 +90,7 @@ const TopbarBase: ParentComponent<TopbarProps> = (props) => {
88
90
  };
89
91
 
90
92
  export const Topbar = TopbarBase as TopbarComponent;
93
+ Topbar.Actions = TopbarActions;
91
94
  Topbar.Container = TopbarContainer;
92
95
  Topbar.Menu = TopbarMenu;
93
96
  Topbar.User = TopbarUser;
@@ -0,0 +1,8 @@
1
+ import { type Component, useContext } from "solid-js";
2
+ import { TopbarContext } from "./TopbarContext";
3
+
4
+ export const TopbarActions: Component = () => {
5
+ const context = useContext(TopbarContext);
6
+
7
+ return <span data-topbar-actions>{context?.actions()}</span>;
8
+ };
@@ -1,6 +1,7 @@
1
- import { type JSX, type ParentComponent, splitProps } from "solid-js";
1
+ import { type JSX, type ParentComponent, splitProps, createSignal } from "solid-js";
2
2
  import clsx from "clsx";
3
3
  import { twMerge } from "tailwind-merge";
4
+ import { TopbarContext } from "./TopbarContext";
4
5
 
5
6
  const containerClass = clsx("flex h-full flex-col");
6
7
 
@@ -13,7 +14,7 @@ export interface TopbarContainerProps extends JSX.HTMLAttributes<HTMLDivElement>
13
14
  *
14
15
  * @remarks
15
16
  * - `flex flex-col h-full` 구조로 Topbar와 콘텐츠를 수직 배치
16
- * - Context 없이 순수 레이아웃 역할만 수행
17
+ * - TopbarContext.Provider로 actions 상태 공유
17
18
  * - 부모 요소에 높이가 지정되어야 함
18
19
  *
19
20
  * @example
@@ -29,12 +30,15 @@ export interface TopbarContainerProps extends JSX.HTMLAttributes<HTMLDivElement>
29
30
  */
30
31
  export const TopbarContainer: ParentComponent<TopbarContainerProps> = (props) => {
31
32
  const [local, rest] = splitProps(props, ["children", "class"]);
33
+ const [actions, setActions] = createSignal<JSX.Element | undefined>(undefined);
32
34
 
33
35
  const getClassName = () => twMerge(containerClass, local.class);
34
36
 
35
37
  return (
36
- <div {...rest} data-topbar-container class={getClassName()}>
37
- {local.children}
38
- </div>
38
+ <TopbarContext.Provider value={{ actions, setActions }}>
39
+ <div {...rest} data-topbar-container class={getClassName()}>
40
+ {local.children}
41
+ </div>
42
+ </TopbarContext.Provider>
39
43
  );
40
44
  };
@@ -0,0 +1,36 @@
1
+ import {
2
+ createContext,
3
+ useContext,
4
+ onCleanup,
5
+ type Accessor,
6
+ type JSX,
7
+ type Setter,
8
+ } from "solid-js";
9
+
10
+ export interface TopbarContextValue {
11
+ actions: Accessor<JSX.Element | undefined>;
12
+ setActions: Setter<JSX.Element | undefined>;
13
+ }
14
+
15
+ export const TopbarContext = createContext<TopbarContextValue>();
16
+
17
+ export function useTopbarActionsAccessor(): Accessor<JSX.Element | undefined> {
18
+ const context = useContext(TopbarContext);
19
+ if (!context) {
20
+ throw new Error("useTopbarActionsAccessor는 Topbar.Container 내부에서만 사용할 수 있습니다");
21
+ }
22
+ return context.actions;
23
+ }
24
+
25
+ export function createTopbarActions(accessor: () => JSX.Element): void {
26
+ const context = useContext(TopbarContext);
27
+ if (!context) {
28
+ throw new Error("createTopbarActions는 Topbar.Container 내부에서만 사용할 수 있습니다");
29
+ }
30
+
31
+ context.setActions(() => accessor());
32
+
33
+ onCleanup(() => {
34
+ context.setActions(undefined);
35
+ });
36
+ }
@@ -63,7 +63,6 @@ export interface TopbarMenuProps extends Omit<JSX.HTMLAttributes<HTMLElement>, "
63
63
  export const TopbarMenu: Component<TopbarMenuProps> = (props) => {
64
64
  const [local, rest] = splitProps(props, ["menus", "class"]);
65
65
  const [mobileMenuOpen, setMobileMenuOpen] = createSignal(false);
66
- let mobileButtonRef: HTMLButtonElement | undefined;
67
66
 
68
67
  return (
69
68
  <>
@@ -74,28 +73,26 @@ export const TopbarMenu: Component<TopbarMenuProps> = (props) => {
74
73
 
75
74
  {/* 모바일 햄버거 (640px 미만에서만 표시) */}
76
75
  <div class={mobileWrapperClass}>
77
- <Button
78
- ref={mobileButtonRef}
79
- variant="ghost"
80
- onClick={() => setMobileMenuOpen((v) => !v)}
81
- aria-label="메뉴"
82
- aria-haspopup="menu"
83
- aria-expanded={mobileMenuOpen()}
84
- >
85
- <Icon icon={IconDotsVertical} size="1.25em" />
86
- </Button>
87
- <Dropdown
88
- triggerRef={() => mobileButtonRef}
89
- open={mobileMenuOpen()}
90
- onOpenChange={setMobileMenuOpen}
91
- >
92
- <List inset>
93
- <For each={local.menus}>
94
- {(menu) => (
95
- <TopbarMenuDropdownItem menu={menu} onClose={() => setMobileMenuOpen(false)} />
96
- )}
97
- </For>
98
- </List>
76
+ <Dropdown open={mobileMenuOpen()} onOpenChange={setMobileMenuOpen}>
77
+ <Dropdown.Trigger>
78
+ <Button
79
+ variant="ghost"
80
+ aria-label="메뉴"
81
+ aria-haspopup="menu"
82
+ aria-expanded={mobileMenuOpen()}
83
+ >
84
+ <Icon icon={IconDotsVertical} size="1.25em" />
85
+ </Button>
86
+ </Dropdown.Trigger>
87
+ <Dropdown.Content>
88
+ <List inset>
89
+ <For each={local.menus}>
90
+ {(menu) => (
91
+ <TopbarMenuDropdownItem menu={menu} onClose={() => setMobileMenuOpen(false)} />
92
+ )}
93
+ </For>
94
+ </List>
95
+ </Dropdown.Content>
99
96
  </Dropdown>
100
97
  </div>
101
98
  </>
@@ -111,7 +108,6 @@ const TopbarMenuButton: Component<TopbarMenuButtonProps> = (props) => {
111
108
  const navigate = useNavigate();
112
109
 
113
110
  const [open, setOpen] = createSignal(false);
114
- let buttonRef: HTMLButtonElement | undefined;
115
111
 
116
112
  const hasChildren = () => props.menu.children !== undefined && props.menu.children.length > 0;
117
113
  const isExternalLink = () => props.menu.href?.includes("://") ?? false;
@@ -133,10 +129,8 @@ const TopbarMenuButton: Component<TopbarMenuButtonProps> = (props) => {
133
129
  return false;
134
130
  });
135
131
 
136
- const handleClick = () => {
137
- if (hasChildren()) {
138
- setOpen((v) => !v);
139
- } else if (props.menu.href !== undefined) {
132
+ const handleNavigate = () => {
133
+ if (props.menu.href !== undefined) {
140
134
  if (isExternalLink()) {
141
135
  window.open(props.menu.href, "_blank", "noopener,noreferrer");
142
136
  } else {
@@ -145,39 +139,42 @@ const TopbarMenuButton: Component<TopbarMenuButtonProps> = (props) => {
145
139
  }
146
140
  };
147
141
 
148
- return (
149
- <>
150
- <Button
151
- ref={buttonRef}
152
- variant={isSelected() ? "solid" : "ghost"}
153
- theme={isSelected() ? "primary" : "base"}
154
- onClick={handleClick}
155
- class={menuButtonContentClass}
156
- aria-haspopup={hasChildren() ? "menu" : undefined}
157
- aria-expanded={hasChildren() ? open() : undefined}
158
- >
159
- <Show when={props.menu.icon}>
160
- <Icon icon={props.menu.icon!} />
161
- </Show>
162
- <span>{props.menu.title}</span>
163
- <Show when={hasChildren()}>
164
- <Icon
165
- icon={IconChevronDown}
166
- size="1em"
167
- class={clsx("transition-transform", open() && "rotate-180")}
168
- />
169
- </Show>
170
- </Button>
142
+ const buttonContent = () => (
143
+ <Button
144
+ variant={isSelected() ? "solid" : "ghost"}
145
+ theme={isSelected() ? "primary" : "base"}
146
+ class={menuButtonContentClass}
147
+ aria-haspopup={hasChildren() ? "menu" : undefined}
148
+ aria-expanded={hasChildren() ? open() : undefined}
149
+ onClick={hasChildren() ? undefined : handleNavigate}
150
+ >
151
+ <Show when={props.menu.icon}>
152
+ <Icon icon={props.menu.icon!} />
153
+ </Show>
154
+ <span>{props.menu.title}</span>
171
155
  <Show when={hasChildren()}>
172
- <Dropdown triggerRef={() => buttonRef} open={open()} onOpenChange={setOpen}>
156
+ <Icon
157
+ icon={IconChevronDown}
158
+ size="1em"
159
+ class={clsx("transition-transform", open() && "rotate-180")}
160
+ />
161
+ </Show>
162
+ </Button>
163
+ );
164
+
165
+ return (
166
+ <Show when={hasChildren()} fallback={buttonContent()}>
167
+ <Dropdown open={open()} onOpenChange={setOpen}>
168
+ <Dropdown.Trigger>{buttonContent()}</Dropdown.Trigger>
169
+ <Dropdown.Content>
173
170
  <List inset>
174
171
  <For each={props.menu.children}>
175
172
  {(child) => <TopbarMenuDropdownItem menu={child} onClose={() => setOpen(false)} />}
176
173
  </For>
177
174
  </List>
178
- </Dropdown>
179
- </Show>
180
- </>
175
+ </Dropdown.Content>
176
+ </Dropdown>
177
+ </Show>
181
178
  );
182
179
  };
183
180