@simplysm/solid 13.0.29 → 13.0.30

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 (220) hide show
  1. package/README.md +10 -5
  2. package/dist/components/data/Pagination.d.ts +4 -5
  3. package/dist/components/data/Pagination.d.ts.map +1 -1
  4. package/dist/components/data/Pagination.js +14 -14
  5. package/dist/components/data/Pagination.js.map +2 -2
  6. package/dist/components/data/Table.js +1 -1
  7. package/dist/components/data/calendar/Calendar.js +1 -1
  8. package/dist/components/data/kanban/Kanban.d.ts +9 -9
  9. package/dist/components/data/kanban/Kanban.d.ts.map +1 -1
  10. package/dist/components/data/kanban/Kanban.js +4 -4
  11. package/dist/components/data/kanban/Kanban.js.map +2 -2
  12. package/dist/components/data/sheet/DataSheet.d.ts.map +1 -1
  13. package/dist/components/data/sheet/DataSheet.js +102 -107
  14. package/dist/components/data/sheet/DataSheet.js.map +2 -2
  15. package/dist/components/data/sheet/DataSheet.styles.js +1 -1
  16. package/dist/components/data/sheet/types.d.ts +2 -2
  17. package/dist/components/data/sheet/types.d.ts.map +1 -1
  18. package/dist/components/disclosure/Dialog.d.ts +8 -8
  19. package/dist/components/disclosure/Dialog.d.ts.map +1 -1
  20. package/dist/components/disclosure/Dialog.js +64 -69
  21. package/dist/components/disclosure/Dialog.js.map +2 -2
  22. package/dist/components/disclosure/DialogContext.d.ts +4 -4
  23. package/dist/components/disclosure/DialogContext.d.ts.map +1 -1
  24. package/dist/components/disclosure/DialogProvider.js +8 -8
  25. package/dist/components/disclosure/DialogProvider.js.map +2 -2
  26. package/dist/components/feedback/Progress.d.ts +3 -3
  27. package/dist/components/feedback/Progress.d.ts.map +1 -1
  28. package/dist/components/feedback/Progress.js +1 -1
  29. package/dist/components/feedback/Progress.js.map +2 -2
  30. package/dist/components/feedback/busy/BusyContainer.d.ts +1 -0
  31. package/dist/components/feedback/busy/BusyContainer.d.ts.map +1 -1
  32. package/dist/components/feedback/busy/BusyContainer.js +13 -6
  33. package/dist/components/feedback/busy/BusyContainer.js.map +2 -2
  34. package/dist/components/feedback/notification/NotificationBanner.js +1 -1
  35. package/dist/components/feedback/notification/NotificationBanner.js.map +1 -1
  36. package/dist/components/feedback/notification/NotificationBell.d.ts.map +1 -1
  37. package/dist/components/feedback/notification/NotificationBell.js +4 -2
  38. package/dist/components/feedback/notification/NotificationBell.js.map +2 -2
  39. package/dist/components/feedback/notification/NotificationProvider.d.ts.map +1 -1
  40. package/dist/components/feedback/notification/NotificationProvider.js +1 -0
  41. package/dist/components/feedback/notification/NotificationProvider.js.map +1 -1
  42. package/dist/components/form-control/Invalid.d.ts +4 -2
  43. package/dist/components/form-control/Invalid.d.ts.map +1 -1
  44. package/dist/components/form-control/Invalid.js +81 -41
  45. package/dist/components/form-control/Invalid.js.map +2 -2
  46. package/dist/components/form-control/ThemeToggle.d.ts.map +1 -1
  47. package/dist/components/form-control/ThemeToggle.js +4 -5
  48. package/dist/components/form-control/ThemeToggle.js.map +2 -2
  49. package/dist/components/form-control/checkbox/Checkbox.d.ts +4 -2
  50. package/dist/components/form-control/checkbox/Checkbox.d.ts.map +1 -1
  51. package/dist/components/form-control/checkbox/Checkbox.js +65 -52
  52. package/dist/components/form-control/checkbox/Checkbox.js.map +2 -2
  53. package/dist/components/form-control/checkbox/Checkbox.styles.d.ts +1 -2
  54. package/dist/components/form-control/checkbox/Checkbox.styles.d.ts.map +1 -1
  55. package/dist/components/form-control/checkbox/Checkbox.styles.js +3 -9
  56. package/dist/components/form-control/checkbox/Checkbox.styles.js.map +1 -1
  57. package/dist/components/form-control/checkbox/CheckboxGroup.d.ts +9 -9
  58. package/dist/components/form-control/checkbox/CheckboxGroup.d.ts.map +1 -1
  59. package/dist/components/form-control/checkbox/CheckboxGroup.js +10 -82
  60. package/dist/components/form-control/checkbox/CheckboxGroup.js.map +2 -2
  61. package/dist/components/form-control/checkbox/Radio.d.ts +4 -2
  62. package/dist/components/form-control/checkbox/Radio.d.ts.map +1 -1
  63. package/dist/components/form-control/checkbox/Radio.js +64 -51
  64. package/dist/components/form-control/checkbox/Radio.js.map +2 -2
  65. package/dist/components/form-control/checkbox/RadioGroup.d.ts +9 -9
  66. package/dist/components/form-control/checkbox/RadioGroup.d.ts.map +1 -1
  67. package/dist/components/form-control/checkbox/RadioGroup.js +10 -77
  68. package/dist/components/form-control/checkbox/RadioGroup.js.map +2 -2
  69. package/dist/components/form-control/color-picker/ColorPicker.d.ts +8 -3
  70. package/dist/components/form-control/color-picker/ColorPicker.d.ts.map +1 -1
  71. package/dist/components/form-control/color-picker/ColorPicker.js +43 -26
  72. package/dist/components/form-control/color-picker/ColorPicker.js.map +2 -2
  73. package/dist/components/form-control/combobox/Combobox.d.ts +8 -8
  74. package/dist/components/form-control/combobox/Combobox.d.ts.map +1 -1
  75. package/dist/components/form-control/combobox/Combobox.js +72 -59
  76. package/dist/components/form-control/combobox/Combobox.js.map +2 -2
  77. package/dist/components/form-control/editor/EditorToolbar.d.ts.map +1 -1
  78. package/dist/components/form-control/editor/EditorToolbar.js +3 -2
  79. package/dist/components/form-control/editor/EditorToolbar.js.map +2 -2
  80. package/dist/components/form-control/field/DatePicker.d.ts +6 -0
  81. package/dist/components/form-control/field/DatePicker.d.ts.map +1 -1
  82. package/dist/components/form-control/field/DatePicker.js +138 -117
  83. package/dist/components/form-control/field/DatePicker.js.map +2 -2
  84. package/dist/components/form-control/field/DateTimePicker.d.ts +6 -0
  85. package/dist/components/form-control/field/DateTimePicker.d.ts.map +1 -1
  86. package/dist/components/form-control/field/DateTimePicker.js +138 -115
  87. package/dist/components/form-control/field/DateTimePicker.js.map +2 -2
  88. package/dist/components/form-control/field/Field.styles.d.ts +14 -0
  89. package/dist/components/form-control/field/Field.styles.d.ts.map +1 -1
  90. package/dist/components/form-control/field/Field.styles.js +30 -0
  91. package/dist/components/form-control/field/Field.styles.js.map +1 -1
  92. package/dist/components/form-control/field/FieldPlaceholder.d.ts +7 -0
  93. package/dist/components/form-control/field/FieldPlaceholder.d.ts.map +1 -0
  94. package/dist/components/form-control/field/FieldPlaceholder.js +34 -0
  95. package/dist/components/form-control/field/FieldPlaceholder.js.map +6 -0
  96. package/dist/components/form-control/field/NumberInput.d.ts +10 -0
  97. package/dist/components/form-control/field/NumberInput.d.ts.map +1 -1
  98. package/dist/components/form-control/field/NumberInput.js +149 -115
  99. package/dist/components/form-control/field/NumberInput.js.map +2 -2
  100. package/dist/components/form-control/field/TextInput.d.ts +12 -0
  101. package/dist/components/form-control/field/TextInput.d.ts.map +1 -1
  102. package/dist/components/form-control/field/TextInput.js +162 -116
  103. package/dist/components/form-control/field/TextInput.js.map +2 -2
  104. package/dist/components/form-control/field/Textarea.d.ts +10 -0
  105. package/dist/components/form-control/field/Textarea.d.ts.map +1 -1
  106. package/dist/components/form-control/field/Textarea.js +156 -121
  107. package/dist/components/form-control/field/Textarea.js.map +2 -2
  108. package/dist/components/form-control/field/TimePicker.d.ts +10 -0
  109. package/dist/components/form-control/field/TimePicker.d.ts.map +1 -1
  110. package/dist/components/form-control/field/TimePicker.js +126 -94
  111. package/dist/components/form-control/field/TimePicker.js.map +2 -2
  112. package/dist/components/form-control/select/Select.d.ts +7 -9
  113. package/dist/components/form-control/select/Select.d.ts.map +1 -1
  114. package/dist/components/form-control/select/Select.js +71 -60
  115. package/dist/components/form-control/select/Select.js.map +2 -2
  116. package/dist/components/form-control/state-preset/StatePreset.d.ts.map +1 -1
  117. package/dist/components/form-control/state-preset/StatePreset.js +2 -1
  118. package/dist/components/form-control/state-preset/StatePreset.js.map +2 -2
  119. package/dist/components/layout/sidebar/SidebarMenu.js +1 -1
  120. package/dist/components/layout/sidebar/SidebarMenu.js.map +1 -1
  121. package/dist/components/layout/sidebar/SidebarUser.js +2 -2
  122. package/dist/components/layout/sidebar/SidebarUser.js.map +1 -1
  123. package/dist/hooks/createItemTemplate.d.ts +17 -0
  124. package/dist/hooks/createItemTemplate.d.ts.map +1 -0
  125. package/dist/hooks/createItemTemplate.js +40 -0
  126. package/dist/hooks/createItemTemplate.js.map +6 -0
  127. package/dist/hooks/createPointerDrag.d.ts +13 -0
  128. package/dist/hooks/createPointerDrag.d.ts.map +1 -0
  129. package/dist/hooks/createPointerDrag.js +15 -0
  130. package/dist/hooks/createPointerDrag.js.map +6 -0
  131. package/dist/hooks/createSelectionGroup.d.ts +70 -0
  132. package/dist/hooks/createSelectionGroup.d.ts.map +1 -0
  133. package/dist/hooks/createSelectionGroup.js +141 -0
  134. package/dist/hooks/createSelectionGroup.js.map +6 -0
  135. package/dist/hooks/useLocalStorage.d.ts +5 -3
  136. package/dist/hooks/useLocalStorage.d.ts.map +1 -1
  137. package/dist/hooks/useLocalStorage.js.map +1 -1
  138. package/dist/hooks/{createPwaUpdate.d.ts → usePwaUpdate.d.ts} +2 -2
  139. package/dist/hooks/usePwaUpdate.d.ts.map +1 -0
  140. package/dist/hooks/{createPwaUpdate.js → usePwaUpdate.js} +3 -3
  141. package/dist/hooks/usePwaUpdate.js.map +6 -0
  142. package/dist/hooks/useSyncConfig.d.ts +3 -3
  143. package/dist/hooks/useSyncConfig.d.ts.map +1 -1
  144. package/dist/hooks/useSyncConfig.js +6 -7
  145. package/dist/hooks/useSyncConfig.js.map +1 -1
  146. package/dist/index.d.ts +1 -3
  147. package/dist/index.d.ts.map +1 -1
  148. package/dist/index.js +2 -4
  149. package/dist/index.js.map +1 -1
  150. package/dist/providers/InitializeProvider.js +2 -2
  151. package/dist/providers/InitializeProvider.js.map +2 -2
  152. package/dist/providers/ThemeContext.d.ts.map +1 -1
  153. package/dist/providers/ThemeContext.js +2 -1
  154. package/dist/providers/ThemeContext.js.map +2 -2
  155. package/dist/styles/patterns.styles.d.ts +1 -0
  156. package/dist/styles/patterns.styles.d.ts.map +1 -1
  157. package/dist/styles/patterns.styles.js +11 -0
  158. package/dist/styles/patterns.styles.js.map +1 -1
  159. package/dist/styles/tokens.styles.d.ts +1 -0
  160. package/dist/styles/tokens.styles.d.ts.map +1 -1
  161. package/dist/styles/tokens.styles.js.map +1 -1
  162. package/docs/data-components.md +34 -5
  163. package/docs/disclosure.md +28 -8
  164. package/docs/feedback.md +25 -2
  165. package/docs/form-controls.md +289 -33
  166. package/docs/hooks.md +19 -7
  167. package/docs/layout.md +12 -0
  168. package/docs/providers.md +120 -8
  169. package/docs/styling.md +90 -0
  170. package/package.json +3 -3
  171. package/src/components/data/Pagination.tsx +20 -21
  172. package/src/components/data/Table.tsx +1 -1
  173. package/src/components/data/calendar/Calendar.tsx +1 -1
  174. package/src/components/data/kanban/Kanban.tsx +18 -25
  175. package/src/components/data/sheet/DataSheet.styles.ts +1 -1
  176. package/src/components/data/sheet/DataSheet.tsx +122 -131
  177. package/src/components/data/sheet/types.ts +2 -2
  178. package/src/components/disclosure/Dialog.tsx +87 -100
  179. package/src/components/disclosure/DialogContext.ts +4 -4
  180. package/src/components/disclosure/DialogProvider.tsx +4 -4
  181. package/src/components/feedback/Progress.tsx +9 -5
  182. package/src/components/feedback/busy/BusyContainer.tsx +9 -5
  183. package/src/components/feedback/notification/NotificationBanner.tsx +1 -1
  184. package/src/components/feedback/notification/NotificationBell.tsx +4 -12
  185. package/src/components/feedback/notification/NotificationProvider.tsx +1 -0
  186. package/src/components/form-control/Invalid.tsx +114 -52
  187. package/src/components/form-control/ThemeToggle.tsx +4 -17
  188. package/src/components/form-control/checkbox/Checkbox.styles.ts +2 -9
  189. package/src/components/form-control/checkbox/Checkbox.tsx +39 -28
  190. package/src/components/form-control/checkbox/CheckboxGroup.tsx +18 -97
  191. package/src/components/form-control/checkbox/Radio.tsx +39 -28
  192. package/src/components/form-control/checkbox/RadioGroup.tsx +18 -92
  193. package/src/components/form-control/color-picker/ColorPicker.tsx +36 -16
  194. package/src/components/form-control/combobox/Combobox.tsx +43 -33
  195. package/src/components/form-control/editor/EditorToolbar.tsx +3 -14
  196. package/src/components/form-control/field/DatePicker.tsx +99 -97
  197. package/src/components/form-control/field/DateTimePicker.tsx +107 -95
  198. package/src/components/form-control/field/Field.styles.ts +45 -0
  199. package/src/components/form-control/field/FieldPlaceholder.tsx +18 -0
  200. package/src/components/form-control/field/NumberInput.tsx +122 -94
  201. package/src/components/form-control/field/TextInput.tsx +119 -95
  202. package/src/components/form-control/field/Textarea.tsx +124 -98
  203. package/src/components/form-control/field/TimePicker.tsx +101 -75
  204. package/src/components/form-control/select/Select.tsx +52 -44
  205. package/src/components/form-control/state-preset/StatePreset.tsx +2 -8
  206. package/src/components/layout/sidebar/SidebarMenu.tsx +1 -1
  207. package/src/components/layout/sidebar/SidebarUser.tsx +3 -3
  208. package/src/hooks/createItemTemplate.tsx +42 -0
  209. package/src/hooks/createPointerDrag.ts +28 -0
  210. package/src/hooks/createSelectionGroup.tsx +235 -0
  211. package/src/hooks/useLocalStorage.ts +8 -4
  212. package/src/hooks/{createPwaUpdate.ts → usePwaUpdate.ts} +1 -1
  213. package/src/hooks/useSyncConfig.ts +9 -13
  214. package/src/index.ts +1 -3
  215. package/src/providers/InitializeProvider.tsx +2 -2
  216. package/src/providers/ThemeContext.tsx +2 -1
  217. package/src/styles/patterns.styles.ts +12 -0
  218. package/src/styles/tokens.styles.ts +1 -0
  219. package/dist/hooks/createPwaUpdate.d.ts.map +0 -1
  220. package/dist/hooks/createPwaUpdate.js.map +0 -6
@@ -1,20 +1,25 @@
1
- import { type Component, type JSX, Show, splitProps, createSignal, createEffect } from "solid-js";
1
+ import {
2
+ type Component,
3
+ createEffect,
4
+ createMemo,
5
+ type JSX,
6
+ Show,
7
+ splitProps,
8
+ createSignal,
9
+ } from "solid-js";
2
10
  import clsx from "clsx";
3
11
  import { twMerge } from "tailwind-merge";
4
12
  import type { IconProps as TablerIconProps } from "@tabler/icons-solidjs";
5
13
  import { createControllableSignal } from "../../../hooks/createControllableSignal";
6
14
  import {
7
15
  type FieldSize,
8
- fieldBaseClass,
9
- fieldSizeClasses,
10
- fieldInsetClass,
11
- fieldInsetHeightClass,
12
- fieldInsetSizeHeightClasses,
13
- fieldDisabledClass,
14
16
  fieldInputClass,
17
+ fieldGapClasses,
18
+ getFieldWrapperClass,
15
19
  } from "./Field.styles";
16
- import { textMuted } from "../../../styles/tokens.styles";
17
20
  import { Icon } from "../../display/Icon";
21
+ import { PlaceholderFallback } from "./FieldPlaceholder";
22
+ import { Invalid } from "../../form-control/Invalid";
18
23
 
19
24
  // NumberInput 전용 input 스타일 (우측 정렬 + 스피너 숨김)
20
25
  const numberInputClass = clsx(
@@ -63,6 +68,21 @@ export interface NumberInputProps {
63
68
 
64
69
  /** 접두 아이콘 */
65
70
  prefixIcon?: Component<TablerIconProps>;
71
+
72
+ /** 필수 입력 여부 */
73
+ required?: boolean;
74
+
75
+ /** 최솟값 */
76
+ min?: number;
77
+
78
+ /** 최댓값 */
79
+ max?: number;
80
+
81
+ /** 커스텀 유효성 검사 함수 */
82
+ validate?: (value: number | undefined) => string | undefined;
83
+
84
+ /** touchMode: 포커스 해제 후에만 에러 표시 */
85
+ touchMode?: boolean;
66
86
  }
67
87
 
68
88
  /**
@@ -169,6 +189,11 @@ export const NumberInput: Component<NumberInputProps> = (props) => {
169
189
  "size",
170
190
  "inset",
171
191
  "prefixIcon",
192
+ "required",
193
+ "min",
194
+ "max",
195
+ "validate",
196
+ "touchMode",
172
197
  "class",
173
198
  "style",
174
199
  ]);
@@ -239,23 +264,13 @@ export const NumberInput: Component<NumberInputProps> = (props) => {
239
264
 
240
265
  // wrapper 클래스 (inset 분기에서는 local.class 제외)
241
266
  const getWrapperClass = (includeCustomClass: boolean) =>
242
- twMerge(
243
- fieldBaseClass,
244
- local.prefixIcon &&
245
- (local.size === "sm"
246
- ? "gap-1.5"
247
- : local.size === "lg"
248
- ? "gap-3"
249
- : local.size === "xl"
250
- ? "gap-4"
251
- : "gap-2"),
252
- local.size && fieldSizeClasses[local.size],
253
- local.disabled && fieldDisabledClass,
254
- local.inset && fieldInsetClass,
255
- local.inset && (local.size ? fieldInsetSizeHeightClasses[local.size] : fieldInsetHeightClass),
256
-
257
- includeCustomClass && local.class,
258
- );
267
+ getFieldWrapperClass({
268
+ size: local.size,
269
+ disabled: local.disabled,
270
+ inset: local.inset,
271
+ includeCustomClass: includeCustomClass && local.class,
272
+ extra: local.prefixIcon && fieldGapClasses[local.size ?? "default"],
273
+ });
259
274
 
260
275
  const isEditable = () => !local.disabled && !local.readonly;
261
276
 
@@ -265,82 +280,95 @@ export const NumberInput: Component<NumberInputProps> = (props) => {
265
280
  </Show>
266
281
  );
267
282
 
283
+ // 유효성 검사 메시지 (순서대로 검사, 최초 실패 메시지 반환)
284
+ const errorMsg = createMemo(() => {
285
+ const v = value();
286
+ if (local.required && v === undefined) return "필수 입력 항목입니다";
287
+ if (v !== undefined) {
288
+ if (local.min !== undefined && v < local.min) return `최솟값은 ${local.min}입니다`;
289
+ if (local.max !== undefined && v > local.max) return `최댓값은 ${local.max}입니다`;
290
+ }
291
+ return local.validate?.(v);
292
+ });
293
+
268
294
  return (
269
- <Show
270
- when={local.inset}
271
- fallback={
272
- // standalone 모드: 기존 Show 패턴 유지
273
- <Show
274
- when={isEditable()}
275
- fallback={
276
- <div
277
- {...rest}
278
- data-number-field
279
- class={twMerge(getWrapperClass(true), "sd-number-field", "justify-end")}
280
- style={local.style}
281
- title={local.title}
282
- >
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)}
317
+ placeholder={local.placeholder}
318
+ />
319
+ </div>
320
+ }
321
+ >
322
+ <div {...rest} data-number-field class={getWrapperClass(true)} style={local.style}>
283
323
  {prefixIconEl()}
284
- {formatNumber(value(), local.comma ?? true, local.minDigits) ||
285
- (local.placeholder != null && local.placeholder !== "" ? (
286
- <span class={textMuted}>{local.placeholder}</span>
287
- ) : (
288
- "\u00A0"
289
- ))}
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
+ />
290
335
  </div>
291
- }
292
- >
293
- <div {...rest} data-number-field class={getWrapperClass(true)} style={local.style}>
336
+ </Show>
337
+ }
338
+ >
339
+ {/* inset 모드: dual-element overlay 패턴 */}
340
+ <div {...rest} data-number-field class={clsx("relative", local.class)} style={local.style}>
341
+ <div
342
+ data-number-field-content
343
+ class={twMerge(getWrapperClass(false), "justify-end")}
344
+ style={{ visibility: isEditable() ? "hidden" : undefined }}
345
+ title={local.title}
346
+ >
294
347
  {prefixIconEl()}
295
- <input
296
- type="text"
297
- inputmode="numeric"
298
- class={numberInputClass}
299
- value={displayValue()}
348
+ <PlaceholderFallback
349
+ value={formatNumber(value(), local.comma ?? true, local.minDigits)}
300
350
  placeholder={local.placeholder}
301
- title={local.title}
302
- onInput={handleInput}
303
- onFocus={handleFocus}
304
- onBlur={handleBlur}
305
351
  />
306
352
  </div>
307
- </Show>
308
- }
309
- >
310
- {/* inset 모드: dual-element overlay 패턴 */}
311
- <div {...rest} data-number-field class={clsx("relative", local.class)} style={local.style}>
312
- <div
313
- data-number-field-content
314
- class={twMerge(getWrapperClass(false), "justify-end")}
315
- style={{ visibility: isEditable() ? "hidden" : undefined }}
316
- title={local.title}
317
- >
318
- {prefixIconEl()}
319
- {formatNumber(value(), local.comma ?? true, local.minDigits) ||
320
- (local.placeholder != null && local.placeholder !== "" ? (
321
- <span class={textMuted}>{local.placeholder}</span>
322
- ) : (
323
- "\u00A0"
324
- ))}
325
- </div>
326
353
 
327
- <Show when={isEditable()}>
328
- <div class={twMerge(getWrapperClass(false), "absolute left-0 top-0 size-full")}>
329
- {prefixIconEl()}
330
- <input
331
- type="text"
332
- inputmode="numeric"
333
- class={numberInputClass}
334
- value={displayValue()}
335
- placeholder={local.placeholder}
336
- title={local.title}
337
- onInput={handleInput}
338
- onFocus={handleFocus}
339
- onBlur={handleBlur}
340
- />
341
- </div>
342
- </Show>
343
- </div>
344
- </Show>
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()}
362
+ placeholder={local.placeholder}
363
+ title={local.title}
364
+ onInput={handleInput}
365
+ onFocus={handleFocus}
366
+ onBlur={handleBlur}
367
+ />
368
+ </div>
369
+ </Show>
370
+ </div>
371
+ </Show>
372
+ </Invalid>
345
373
  );
346
374
  };
@@ -1,21 +1,18 @@
1
1
  import clsx from "clsx";
2
- import { type Component, createEffect, type JSX, Show, splitProps } from "solid-js";
2
+ import { type Component, createEffect, createMemo, type JSX, Show, splitProps } from "solid-js";
3
3
  import { twMerge } from "tailwind-merge";
4
4
  import type { IconProps as TablerIconProps } from "@tabler/icons-solidjs";
5
5
  import { createControllableSignal } from "../../../hooks/createControllableSignal";
6
6
  import { createIMEHandler } from "../../../hooks/createIMEHandler";
7
7
  import {
8
- fieldBaseClass,
9
- fieldDisabledClass,
8
+ fieldGapClasses,
10
9
  fieldInputClass,
11
- fieldInsetClass,
12
- fieldInsetHeightClass,
13
- fieldInsetSizeHeightClasses,
14
10
  type FieldSize,
15
- fieldSizeClasses,
11
+ getFieldWrapperClass,
16
12
  } from "./Field.styles";
17
- import { textMuted } from "../../../styles/tokens.styles";
13
+ import { PlaceholderFallback } from "./FieldPlaceholder";
18
14
  import { Icon } from "../../display/Icon";
15
+ import { Invalid } from "../../form-control/Invalid";
19
16
 
20
17
  type TextInputType = "text" | "password" | "email";
21
18
 
@@ -56,6 +53,24 @@ export interface TextInputProps {
56
53
  /** 접두 아이콘 */
57
54
  prefixIcon?: Component<TablerIconProps>;
58
55
 
56
+ /** 필수 입력 여부 */
57
+ required?: boolean;
58
+
59
+ /** 최소 길이 */
60
+ minLength?: number;
61
+
62
+ /** 최대 길이 */
63
+ maxLength?: number;
64
+
65
+ /** 입력 패턴 (정규식 문자열) */
66
+ pattern?: string;
67
+
68
+ /** 커스텀 유효성 검사 함수 */
69
+ validate?: (value: string) => string | undefined;
70
+
71
+ /** touchMode: 포커스 해제 후에만 에러 표시 */
72
+ touchMode?: boolean;
73
+
59
74
  /** 커스텀 class */
60
75
  class?: string;
61
76
 
@@ -99,8 +114,7 @@ function removeFormat(formattedValue: string, format: string): string {
99
114
  let result = "";
100
115
 
101
116
  for (let i = 0; i < formattedValue.length; i++) {
102
- const formatChar = format[i];
103
- if (formatChar === "X") {
117
+ if (i >= format.length || format[i] === "X") {
104
118
  result += formattedValue[i];
105
119
  }
106
120
  // 포맷 문자가 아닌 경우 (구분자) 스킵
@@ -138,6 +152,12 @@ export const TextInput: Component<TextInputProps> = (props) => {
138
152
  "inset",
139
153
  "format",
140
154
  "prefixIcon",
155
+ "required",
156
+ "minLength",
157
+ "maxLength",
158
+ "pattern",
159
+ "validate",
160
+ "touchMode",
141
161
  "class",
142
162
  "style",
143
163
  ]);
@@ -192,23 +212,13 @@ export const TextInput: Component<TextInputProps> = (props) => {
192
212
 
193
213
  // wrapper 클래스 (includeCustomClass=false일 때 local.class 제외 — inset에서 outer에만 적용)
194
214
  const getWrapperClass = (includeCustomClass: boolean) =>
195
- twMerge(
196
- fieldBaseClass,
197
- local.prefixIcon &&
198
- (local.size === "sm"
199
- ? "gap-1.5"
200
- : local.size === "lg"
201
- ? "gap-3"
202
- : local.size === "xl"
203
- ? "gap-4"
204
- : "gap-2"),
205
- local.size && fieldSizeClasses[local.size],
206
- local.disabled && fieldDisabledClass,
207
- local.inset && fieldInsetClass,
208
- local.inset && (local.size ? fieldInsetSizeHeightClasses[local.size] : fieldInsetHeightClass),
209
-
210
- includeCustomClass && local.class,
211
- );
215
+ getFieldWrapperClass({
216
+ size: local.size,
217
+ disabled: local.disabled,
218
+ inset: local.inset,
219
+ includeCustomClass: includeCustomClass && local.class,
220
+ extra: local.prefixIcon && fieldGapClasses[local.size ?? "default"],
221
+ });
212
222
 
213
223
  // 편집 가능 여부
214
224
  const isEditable = () => !local.disabled && !local.readonly;
@@ -226,79 +236,93 @@ export const TextInput: Component<TextInputProps> = (props) => {
226
236
  }
227
237
  });
228
238
 
239
+ // 유효성 검사 메시지 (순서대로 검사, 최초 실패 메시지 반환)
240
+ const errorMsg = createMemo(() => {
241
+ const v = value();
242
+ if (local.required && !v) return "필수 입력 항목입니다";
243
+ if (v) {
244
+ if (local.minLength != null && v.length < local.minLength)
245
+ return `최소 ${local.minLength}자 이상 입력하세요`;
246
+ if (local.maxLength != null && v.length > local.maxLength)
247
+ return `최대 ${local.maxLength}자까지 입력 가능합니다`;
248
+ if (local.pattern != null && !new RegExp(local.pattern).test(v))
249
+ return "입력 형식이 올바르지 않습니다";
250
+ }
251
+ return local.validate?.(v);
252
+ });
253
+
229
254
  return (
230
- <Show
231
- when={local.inset}
232
- fallback={
233
- // standalone 모드: 기존 Show 패턴 유지
234
- <Show
235
- when={isEditable()}
236
- fallback={
237
- <div
238
- {...rest}
239
- data-text-field
240
- class={twMerge(getWrapperClass(true), "sd-text-field")}
241
- style={local.style}
242
- title={local.title}
243
- >
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}>
244
280
  {prefixIconEl()}
245
- {displayValue() ||
246
- (local.placeholder != null && local.placeholder !== "" ? (
247
- <span class={textMuted}>{local.placeholder}</span>
248
- ) : (
249
- "\u00A0"
250
- ))}
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
+ />
251
292
  </div>
252
- }
253
- >
254
- <div {...rest} data-text-field class={getWrapperClass(true)} style={local.style}>
293
+ </Show>
294
+ }
295
+ >
296
+ {/* inset 모드: dual-element overlay 패턴 */}
297
+ <div {...rest} data-text-field class={clsx("relative", local.class)} style={local.style}>
298
+ <div
299
+ data-text-field-content
300
+ class={getWrapperClass(false)}
301
+ style={{ visibility: isEditable() ? "hidden" : undefined }}
302
+ title={local.title}
303
+ >
255
304
  {prefixIconEl()}
256
- <input
257
- type={local.type ?? "text"}
258
- class={fieldInputClass}
259
- value={inputValue()}
260
- placeholder={local.placeholder}
261
- title={local.title}
262
- autocomplete={local.autocomplete}
263
- onInput={handleInput}
264
- onCompositionStart={handleCompositionStart}
265
- onCompositionEnd={handleCompositionEnd}
266
- />
305
+ <PlaceholderFallback value={displayValue()} placeholder={local.placeholder} />
267
306
  </div>
268
- </Show>
269
- }
270
- >
271
- {/* inset 모드: dual-element overlay 패턴 */}
272
- <div
273
- {...rest}
274
- data-text-field
275
- class={twMerge(getWrapperClass(false), "relative", local.class)}
276
- style={local.style}
277
- >
278
- <div data-text-field-content style={{ visibility: isEditable() ? "hidden" : undefined }}>
279
- {prefixIconEl()}
280
- {displayValue() ||
281
- (local.placeholder != null && local.placeholder !== "" ? (
282
- <span class={textMuted}>{local.placeholder}</span>
283
- ) : (
284
- "\u00A0"
285
- ))}
286
- </div>
287
307
 
288
- <Show when={isEditable()}>
289
- <input
290
- type={local.type ?? "text"}
291
- class={clsx(fieldInputClass, "absolute left-0 top-0 size-full", "px-2 py-1")}
292
- value={inputValue()}
293
- placeholder={local.placeholder}
294
- title={local.title}
295
- autocomplete={local.autocomplete}
296
- onInput={handleInput}
297
- onCompositionStart={handleCompositionStart}
298
- onCompositionEnd={handleCompositionEnd}
299
- />
300
- </Show>
301
- </div>
302
- </Show>
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
+ />
322
+ </div>
323
+ </Show>
324
+ </div>
325
+ </Show>
326
+ </Invalid>
303
327
  );
304
328
  };