@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
@@ -1,15 +1,17 @@
1
1
  import {
2
- type Component,
2
+ createContext,
3
3
  createEffect,
4
4
  createMemo,
5
+ createSignal,
5
6
  type JSX,
7
+ onCleanup,
8
+ type ParentComponent,
6
9
  Show,
7
10
  splitProps,
8
- createSignal,
11
+ useContext,
9
12
  } from "solid-js";
10
13
  import clsx from "clsx";
11
14
  import { twMerge } from "tailwind-merge";
12
- import type { IconProps as TablerIconProps } from "@tabler/icons-solidjs";
13
15
  import { createControllableSignal } from "../../../hooks/createControllableSignal";
14
16
  import {
15
17
  type FieldSize,
@@ -17,7 +19,6 @@ import {
17
19
  fieldGapClasses,
18
20
  getFieldWrapperClass,
19
21
  } from "./Field.styles";
20
- import { Icon } from "../../display/Icon";
21
22
  import { PlaceholderFallback } from "./FieldPlaceholder";
22
23
  import { Invalid } from "../../form-control/Invalid";
23
24
 
@@ -29,6 +30,22 @@ const numberInputClass = clsx(
29
30
  "[&::-webkit-inner-spin-button]:appearance-none",
30
31
  );
31
32
 
33
+ type SlotAccessor = (() => JSX.Element) | undefined;
34
+
35
+ interface NumberInputSlotsContextValue {
36
+ setPrefix: (content: SlotAccessor) => void;
37
+ }
38
+
39
+ const NumberInputSlotsContext = createContext<NumberInputSlotsContextValue>();
40
+
41
+ const NumberInputPrefix: ParentComponent = (props) => {
42
+ const ctx = useContext(NumberInputSlotsContext)!;
43
+ // eslint-disable-next-line solid/reactivity -- slot accessor: children은 렌더 시점에 lazy 평가됨
44
+ ctx.setPrefix(() => props.children);
45
+ onCleanup(() => ctx.setPrefix(undefined));
46
+ return null;
47
+ };
48
+
32
49
  export interface NumberInputProps {
33
50
  /** 입력 값 */
34
51
  value?: number;
@@ -66,9 +83,6 @@ export interface NumberInputProps {
66
83
  /** 커스텀 style */
67
84
  style?: JSX.CSSProperties;
68
85
 
69
- /** 접두 아이콘 */
70
- prefixIcon?: Component<TablerIconProps>;
71
-
72
86
  /** 필수 입력 여부 */
73
87
  required?: boolean;
74
88
 
@@ -83,6 +97,9 @@ export interface NumberInputProps {
83
97
 
84
98
  /** touchMode: 포커스 해제 후에만 에러 표시 */
85
99
  touchMode?: boolean;
100
+
101
+ /** 자식 요소 (Prefix 슬롯 등) */
102
+ children?: JSX.Element;
86
103
  }
87
104
 
88
105
  /**
@@ -161,6 +178,11 @@ function isValidNumberInput(str: string): boolean {
161
178
  return /^-?\d*\.?\d*$/.test(cleanStr);
162
179
  }
163
180
 
181
+ interface NumberInputComponent {
182
+ (props: NumberInputProps): JSX.Element;
183
+ Prefix: typeof NumberInputPrefix;
184
+ }
185
+
164
186
  /**
165
187
  * NumberInput 컴포넌트
166
188
  *
@@ -174,9 +196,14 @@ function isValidNumberInput(str: string): boolean {
174
196
  *
175
197
  * // 최소 소수점 자릿수 지정
176
198
  * <NumberInput value={price()} minDigits={2} />
199
+ *
200
+ * // Prefix 슬롯
201
+ * <NumberInput value={price()}>
202
+ * <NumberInput.Prefix>₩</NumberInput.Prefix>
203
+ * </NumberInput>
177
204
  * ```
178
205
  */
179
- export const NumberInput: Component<NumberInputProps> = (props) => {
206
+ export const NumberInput: NumberInputComponent = (props) => {
180
207
  const [local, rest] = splitProps(props, [
181
208
  "value",
182
209
  "onValueChange",
@@ -188,7 +215,6 @@ export const NumberInput: Component<NumberInputProps> = (props) => {
188
215
  "readonly",
189
216
  "size",
190
217
  "inset",
191
- "prefixIcon",
192
218
  "required",
193
219
  "min",
194
220
  "max",
@@ -196,6 +222,7 @@ export const NumberInput: Component<NumberInputProps> = (props) => {
196
222
  "touchMode",
197
223
  "class",
198
224
  "style",
225
+ "children",
199
226
  ]);
200
227
 
201
228
  // 입력 중인 상태를 추적하기 위한 내부 문자열 상태
@@ -208,6 +235,10 @@ export const NumberInput: Component<NumberInputProps> = (props) => {
208
235
  onChange: () => local.onValueChange,
209
236
  });
210
237
 
238
+ const [prefix, _setPrefix] = createSignal<SlotAccessor>();
239
+ const setPrefix = (content: SlotAccessor) => _setPrefix(() => content);
240
+ const prefixEl = () => prefix() !== undefined;
241
+
211
242
  // 외부 값 변경 시 입력 문자열 동기화
212
243
  createEffect(() => {
213
244
  const val = value();
@@ -269,17 +300,11 @@ export const NumberInput: Component<NumberInputProps> = (props) => {
269
300
  disabled: local.disabled,
270
301
  inset: local.inset,
271
302
  includeCustomClass: includeCustomClass && local.class,
272
- extra: local.prefixIcon && fieldGapClasses[local.size ?? "default"],
303
+ extra: prefixEl() && fieldGapClasses[local.size ?? "default"],
273
304
  });
274
305
 
275
306
  const isEditable = () => !local.disabled && !local.readonly;
276
307
 
277
- const prefixIconEl = () => (
278
- <Show when={local.prefixIcon}>
279
- <Icon icon={local.prefixIcon!} class="shrink-0 opacity-70" />
280
- </Show>
281
- );
282
-
283
308
  // 유효성 검사 메시지 (순서대로 검사, 최초 실패 메시지 반환)
284
309
  const errorMsg = createMemo(() => {
285
310
  const v = value();
@@ -292,83 +317,103 @@ export const NumberInput: Component<NumberInputProps> = (props) => {
292
317
  });
293
318
 
294
319
  return (
295
- <Invalid
296
- message={errorMsg()}
297
- variant={local.inset ? "dot" : "border"}
298
- touchMode={local.touchMode}
299
- >
300
- <Show
301
- when={local.inset}
302
- fallback={
303
- // standalone 모드: 기존 Show 패턴 유지
304
- <Show
305
- when={isEditable()}
306
- fallback={
307
- <div
308
- {...rest}
309
- data-number-field
310
- class={twMerge(getWrapperClass(true), "sd-number-field", "justify-end")}
311
- style={local.style}
312
- title={local.title}
313
- >
314
- {prefixIconEl()}
315
- <PlaceholderFallback
316
- value={formatNumber(value(), local.comma ?? true, local.minDigits)}
320
+ <NumberInputSlotsContext.Provider value={{ setPrefix }}>
321
+ {local.children}
322
+ <Invalid
323
+ message={errorMsg()}
324
+ variant={local.inset ? "dot" : "border"}
325
+ touchMode={local.touchMode}
326
+ >
327
+ <Show
328
+ when={local.inset}
329
+ fallback={
330
+ // standalone 모드: 기존 Show 패턴 유지
331
+ <Show
332
+ when={isEditable()}
333
+ fallback={
334
+ <div
335
+ {...rest}
336
+ data-number-field
337
+ class={twMerge(getWrapperClass(true), "sd-number-field", "justify-end")}
338
+ style={local.style}
339
+ title={local.title}
340
+ >
341
+ <Show when={prefix()}>
342
+ <span class="shrink-0">{prefix()!()}</span>
343
+ </Show>
344
+ <PlaceholderFallback
345
+ value={formatNumber(value(), local.comma ?? true, local.minDigits)}
346
+ placeholder={local.placeholder}
347
+ />
348
+ </div>
349
+ }
350
+ >
351
+ <div {...rest} data-number-field class={getWrapperClass(true)} style={local.style}>
352
+ <Show when={prefix()}>
353
+ <span class="shrink-0">{prefix()!()}</span>
354
+ </Show>
355
+ <input
356
+ type="text"
357
+ inputmode="numeric"
358
+ class={numberInputClass}
359
+ value={displayValue()}
317
360
  placeholder={local.placeholder}
361
+ title={local.title}
362
+ autocomplete="one-time-code"
363
+ onInput={handleInput}
364
+ onFocus={handleFocus}
365
+ onBlur={handleBlur}
318
366
  />
319
367
  </div>
320
- }
321
- >
322
- <div {...rest} data-number-field class={getWrapperClass(true)} style={local.style}>
323
- {prefixIconEl()}
324
- <input
325
- type="text"
326
- inputmode="numeric"
327
- class={numberInputClass}
328
- value={displayValue()}
329
- placeholder={local.placeholder}
330
- title={local.title}
331
- onInput={handleInput}
332
- onFocus={handleFocus}
333
- onBlur={handleBlur}
334
- />
335
- </div>
336
- </Show>
337
- }
338
- >
339
- {/* inset 모드: dual-element overlay 패턴 */}
340
- <div {...rest} data-number-field class={clsx("relative", local.class)} style={local.style}>
368
+ </Show>
369
+ }
370
+ >
371
+ {/* inset 모드: dual-element overlay 패턴 */}
341
372
  <div
342
- data-number-field-content
343
- class={twMerge(getWrapperClass(false), "justify-end")}
344
- style={{ visibility: isEditable() ? "hidden" : undefined }}
345
- title={local.title}
373
+ {...rest}
374
+ data-number-field
375
+ class={clsx("relative", local.class)}
376
+ style={local.style}
346
377
  >
347
- {prefixIconEl()}
348
- <PlaceholderFallback
349
- value={formatNumber(value(), local.comma ?? true, local.minDigits)}
350
- placeholder={local.placeholder}
351
- />
352
- </div>
353
-
354
- <Show when={isEditable()}>
355
- <div class={twMerge(getWrapperClass(false), "absolute left-0 top-0 size-full")}>
356
- {prefixIconEl()}
357
- <input
358
- type="text"
359
- inputmode="numeric"
360
- class={numberInputClass}
361
- value={displayValue()}
378
+ <div
379
+ data-number-field-content
380
+ class={twMerge(getWrapperClass(false), "justify-end")}
381
+ style={{ visibility: isEditable() ? "hidden" : undefined }}
382
+ title={local.title}
383
+ >
384
+ <Show when={prefix()}>
385
+ <span class="shrink-0">{prefix()!()}</span>
386
+ </Show>
387
+ <PlaceholderFallback
388
+ value={formatNumber(value(), local.comma ?? true, local.minDigits)}
362
389
  placeholder={local.placeholder}
363
- title={local.title}
364
- onInput={handleInput}
365
- onFocus={handleFocus}
366
- onBlur={handleBlur}
367
390
  />
368
391
  </div>
369
- </Show>
370
- </div>
371
- </Show>
372
- </Invalid>
392
+
393
+ <Show when={isEditable()}>
394
+ <div class={twMerge(getWrapperClass(false), "absolute left-0 top-0 size-full")}>
395
+ <Show when={prefix()}>
396
+ <span class="shrink-0">{prefix()!()}</span>
397
+ </Show>
398
+ <input
399
+ type="text"
400
+ inputmode="numeric"
401
+ class={numberInputClass}
402
+ value={displayValue()}
403
+ placeholder={local.placeholder}
404
+ title={local.title}
405
+ autocomplete="one-time-code"
406
+ onInput={handleInput}
407
+ onFocus={handleFocus}
408
+ onBlur={handleBlur}
409
+ />
410
+ </div>
411
+ </Show>
412
+ </div>
413
+ </Show>
414
+ </Invalid>
415
+ </NumberInputSlotsContext.Provider>
373
416
  );
374
417
  };
418
+
419
+ NumberInput.Prefix = NumberInputPrefix;
@@ -1,7 +1,17 @@
1
1
  import clsx from "clsx";
2
- import { type Component, createEffect, createMemo, type JSX, Show, splitProps } from "solid-js";
2
+ import {
3
+ createContext,
4
+ createEffect,
5
+ createMemo,
6
+ createSignal,
7
+ type JSX,
8
+ onCleanup,
9
+ type ParentComponent,
10
+ Show,
11
+ splitProps,
12
+ useContext,
13
+ } from "solid-js";
3
14
  import { twMerge } from "tailwind-merge";
4
- import type { IconProps as TablerIconProps } from "@tabler/icons-solidjs";
5
15
  import { createControllableSignal } from "../../../hooks/createControllableSignal";
6
16
  import { createIMEHandler } from "../../../hooks/createIMEHandler";
7
17
  import {
@@ -11,11 +21,26 @@ import {
11
21
  getFieldWrapperClass,
12
22
  } from "./Field.styles";
13
23
  import { PlaceholderFallback } from "./FieldPlaceholder";
14
- import { Icon } from "../../display/Icon";
15
24
  import { Invalid } from "../../form-control/Invalid";
16
25
 
26
+ type SlotAccessor = (() => JSX.Element) | undefined;
27
+
28
+ interface TextInputSlotsContextValue {
29
+ setPrefix: (content: SlotAccessor) => void;
30
+ }
31
+
32
+ const TextInputSlotsContext = createContext<TextInputSlotsContextValue>();
33
+
17
34
  type TextInputType = "text" | "password" | "email";
18
35
 
36
+ const TextInputPrefix: ParentComponent = (props) => {
37
+ const ctx = useContext(TextInputSlotsContext)!;
38
+ // eslint-disable-next-line solid/reactivity -- slot accessor: children은 렌더 시점에 lazy 평가됨
39
+ ctx.setPrefix(() => props.children);
40
+ onCleanup(() => ctx.setPrefix(undefined));
41
+ return null;
42
+ };
43
+
19
44
  export interface TextInputProps {
20
45
  /** 입력 값 */
21
46
  value?: string;
@@ -50,9 +75,6 @@ export interface TextInputProps {
50
75
  /** 입력 포맷 (예: XXX-XXXX-XXXX) */
51
76
  format?: string;
52
77
 
53
- /** 접두 아이콘 */
54
- prefixIcon?: Component<TablerIconProps>;
55
-
56
78
  /** 필수 입력 여부 */
57
79
  required?: boolean;
58
80
 
@@ -76,6 +98,9 @@ export interface TextInputProps {
76
98
 
77
99
  /** 커스텀 style */
78
100
  style?: JSX.CSSProperties;
101
+
102
+ /** children (TextInput.Prefix 슬롯) */
103
+ children?: JSX.Element;
79
104
  }
80
105
 
81
106
  /**
@@ -111,13 +136,16 @@ function applyFormat(value: string, format: string): string {
111
136
  function removeFormat(formattedValue: string, format: string): string {
112
137
  if (!formattedValue || !format) return formattedValue;
113
138
 
114
- let result = "";
139
+ const separators = new Set<string>();
140
+ for (const ch of format) {
141
+ if (ch !== "X") separators.add(ch);
142
+ }
115
143
 
116
- for (let i = 0; i < formattedValue.length; i++) {
117
- if (i >= format.length || format[i] === "X") {
118
- result += formattedValue[i];
144
+ let result = "";
145
+ for (const ch of formattedValue) {
146
+ if (!separators.has(ch)) {
147
+ result += ch;
119
148
  }
120
- // 포맷 문자가 아닌 경우 (구분자) 스킵
121
149
  }
122
150
 
123
151
  return result;
@@ -138,7 +166,12 @@ function removeFormat(formattedValue: string, format: string): string {
138
166
  * <TextInput type="password" placeholder="비밀번호 입력" />
139
167
  * ```
140
168
  */
141
- export const TextInput: Component<TextInputProps> = (props) => {
169
+ interface TextInputComponent {
170
+ (props: TextInputProps): JSX.Element;
171
+ Prefix: typeof TextInputPrefix;
172
+ }
173
+
174
+ const TextInputInner = (props: TextInputProps) => {
142
175
  const [local, rest] = splitProps(props, [
143
176
  "value",
144
177
  "onValueChange",
@@ -151,7 +184,6 @@ export const TextInput: Component<TextInputProps> = (props) => {
151
184
  "size",
152
185
  "inset",
153
186
  "format",
154
- "prefixIcon",
155
187
  "required",
156
188
  "minLength",
157
189
  "maxLength",
@@ -160,6 +192,7 @@ export const TextInput: Component<TextInputProps> = (props) => {
160
192
  "touchMode",
161
193
  "class",
162
194
  "style",
195
+ "children",
163
196
  ]);
164
197
 
165
198
  // controlled/uncontrolled 패턴 지원
@@ -210,6 +243,11 @@ export const TextInput: Component<TextInputProps> = (props) => {
210
243
  ime.handleCompositionEnd(extractValue(e.currentTarget));
211
244
  };
212
245
 
246
+ // Prefix 슬롯 Context 등록
247
+ const [prefix, _setPrefix] = createSignal<SlotAccessor>();
248
+ const setPrefix = (content: SlotAccessor) => _setPrefix(() => content);
249
+ const prefixEl = () => prefix() !== undefined;
250
+
213
251
  // wrapper 클래스 (includeCustomClass=false일 때 local.class 제외 — inset에서 outer에만 적용)
214
252
  const getWrapperClass = (includeCustomClass: boolean) =>
215
253
  getFieldWrapperClass({
@@ -217,18 +255,12 @@ export const TextInput: Component<TextInputProps> = (props) => {
217
255
  disabled: local.disabled,
218
256
  inset: local.inset,
219
257
  includeCustomClass: includeCustomClass && local.class,
220
- extra: local.prefixIcon && fieldGapClasses[local.size ?? "default"],
258
+ extra: prefixEl() && fieldGapClasses[local.size ?? "default"],
221
259
  });
222
260
 
223
261
  // 편집 가능 여부
224
262
  const isEditable = () => !local.disabled && !local.readonly;
225
263
 
226
- const prefixIconEl = () => (
227
- <Show when={local.prefixIcon}>
228
- <Icon icon={local.prefixIcon!} class="shrink-0 opacity-70" />
229
- </Show>
230
- );
231
-
232
264
  // disabled 전환 시 미커밋 조합 값 flush
233
265
  createEffect(() => {
234
266
  if (!isEditable()) {
@@ -252,77 +284,96 @@ export const TextInput: Component<TextInputProps> = (props) => {
252
284
  });
253
285
 
254
286
  return (
255
- <Invalid
256
- message={errorMsg()}
257
- variant={local.inset ? "dot" : "border"}
258
- touchMode={local.touchMode}
259
- >
260
- <Show
261
- when={local.inset}
262
- fallback={
263
- // standalone 모드: 기존 Show 패턴 유지
264
- <Show
265
- when={isEditable()}
266
- fallback={
267
- <div
268
- {...rest}
269
- data-text-field
270
- class={twMerge(getWrapperClass(true), "sd-text-field")}
271
- style={local.style}
272
- title={local.title}
273
- >
274
- {prefixIconEl()}
275
- <PlaceholderFallback value={displayValue()} placeholder={local.placeholder} />
276
- </div>
277
- }
278
- >
279
- <div {...rest} data-text-field class={getWrapperClass(true)} style={local.style}>
280
- {prefixIconEl()}
281
- <input
282
- type={local.type ?? "text"}
283
- class={fieldInputClass}
284
- value={inputValue()}
285
- placeholder={local.placeholder}
286
- title={local.title}
287
- autocomplete={local.autocomplete}
288
- onInput={handleInput}
289
- onCompositionStart={handleCompositionStart}
290
- onCompositionEnd={handleCompositionEnd}
291
- />
292
- </div>
293
- </Show>
294
- }
287
+ <TextInputSlotsContext.Provider value={{ setPrefix }}>
288
+ {local.children}
289
+ <Invalid
290
+ message={errorMsg()}
291
+ variant={local.inset ? "dot" : "border"}
292
+ touchMode={local.touchMode}
295
293
  >
296
- {/* inset 모드: dual-element overlay 패턴 */}
297
- <div {...rest} data-text-field class={clsx("relative", local.class)} style={local.style}>
294
+ <Show
295
+ when={local.inset}
296
+ fallback={
297
+ // standalone 모드: 기존 Show 패턴 유지
298
+ <Show
299
+ when={isEditable()}
300
+ fallback={
301
+ <div
302
+ {...rest}
303
+ data-text-field
304
+ class={twMerge(getWrapperClass(true), "sd-text-field")}
305
+ style={local.style}
306
+ title={local.title}
307
+ >
308
+ <Show when={prefix()}>
309
+ <span class="shrink-0">{prefix()!()}</span>
310
+ </Show>
311
+ <PlaceholderFallback value={displayValue()} placeholder={local.placeholder} />
312
+ </div>
313
+ }
314
+ >
315
+ <div {...rest} data-text-field class={getWrapperClass(true)} style={local.style}>
316
+ <Show when={prefix()}>
317
+ <span class="shrink-0">{prefix()!()}</span>
318
+ </Show>
319
+ <input
320
+ type={local.type ?? "text"}
321
+ class={fieldInputClass}
322
+ value={inputValue()}
323
+ placeholder={local.placeholder}
324
+ title={local.title}
325
+ autocomplete={local.autocomplete ?? "one-time-code"}
326
+ onInput={handleInput}
327
+ onCompositionStart={handleCompositionStart}
328
+ onCompositionEnd={handleCompositionEnd}
329
+ />
330
+ </div>
331
+ </Show>
332
+ }
333
+ >
334
+ {/* inset 모드: dual-element overlay 패턴 */}
298
335
  <div
299
- data-text-field-content
300
- class={getWrapperClass(false)}
301
- style={{ visibility: isEditable() ? "hidden" : undefined }}
302
- title={local.title}
336
+ {...rest}
337
+ data-text-field
338
+ class={clsx("relative", "[text-decoration:inherit]", local.class)}
339
+ style={local.style}
303
340
  >
304
- {prefixIconEl()}
305
- <PlaceholderFallback value={displayValue()} placeholder={local.placeholder} />
306
- </div>
307
-
308
- <Show when={isEditable()}>
309
- <div class={twMerge(getWrapperClass(false), "absolute left-0 top-0 size-full")}>
310
- {prefixIconEl()}
311
- <input
312
- type={local.type ?? "text"}
313
- class={fieldInputClass}
314
- value={inputValue()}
315
- placeholder={local.placeholder}
316
- title={local.title}
317
- autocomplete={local.autocomplete}
318
- onInput={handleInput}
319
- onCompositionStart={handleCompositionStart}
320
- onCompositionEnd={handleCompositionEnd}
321
- />
341
+ <div
342
+ data-text-field-content
343
+ class={getWrapperClass(false)}
344
+ style={{ visibility: isEditable() ? "hidden" : undefined }}
345
+ title={local.title}
346
+ >
347
+ <Show when={prefix()}>
348
+ <span class="shrink-0">{prefix()!()}</span>
349
+ </Show>
350
+ <PlaceholderFallback value={displayValue()} placeholder={local.placeholder} />
322
351
  </div>
323
- </Show>
324
- </div>
325
- </Show>
326
- </Invalid>
352
+
353
+ <Show when={isEditable()}>
354
+ <div class={twMerge(getWrapperClass(false), "absolute left-0 top-0 size-full")}>
355
+ <Show when={prefix()}>
356
+ <span class="shrink-0">{prefix()!()}</span>
357
+ </Show>
358
+ <input
359
+ type={local.type ?? "text"}
360
+ class={fieldInputClass}
361
+ value={inputValue()}
362
+ placeholder={local.placeholder}
363
+ title={local.title}
364
+ autocomplete={local.autocomplete ?? "one-time-code"}
365
+ onInput={handleInput}
366
+ onCompositionStart={handleCompositionStart}
367
+ onCompositionEnd={handleCompositionEnd}
368
+ />
369
+ </div>
370
+ </Show>
371
+ </div>
372
+ </Show>
373
+ </Invalid>
374
+ </TextInputSlotsContext.Provider>
327
375
  );
328
376
  };
377
+
378
+ export const TextInput = TextInputInner as unknown as TextInputComponent;
379
+ TextInput.Prefix = TextInputPrefix;
@@ -151,6 +151,7 @@ export const TimePicker: Component<TimePickerProps> = (props) => {
151
151
  disabled: local.disabled,
152
152
  inset: local.inset,
153
153
  includeCustomClass: includeCustomClass && local.class,
154
+ extra: "min-w-24",
154
155
  });
155
156
 
156
157
  // 편집 가능 여부
@@ -203,6 +204,7 @@ export const TimePicker: Component<TimePickerProps> = (props) => {
203
204
  value={displayValue()}
204
205
  title={local.title}
205
206
  step={getStep()}
207
+ autocomplete="one-time-code"
206
208
  onChange={handleChange}
207
209
  />
208
210
  </div>
@@ -228,6 +230,7 @@ export const TimePicker: Component<TimePickerProps> = (props) => {
228
230
  value={displayValue()}
229
231
  title={local.title}
230
232
  step={getStep()}
233
+ autocomplete="one-time-code"
231
234
  onChange={handleChange}
232
235
  />
233
236
  </div>