@simplysm/solid 13.0.29 → 13.0.31

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,18 +1,10 @@
1
1
  import clsx from "clsx";
2
- import { type Component, type JSX, Show, splitProps } from "solid-js";
2
+ import { type Component, createMemo, type JSX, Show, splitProps } from "solid-js";
3
3
  import { twMerge } from "tailwind-merge";
4
4
  import { DateOnly } from "@simplysm/core-common";
5
5
  import { createControllableSignal } from "../../../hooks/createControllableSignal";
6
- import {
7
- type FieldSize,
8
- fieldBaseClass,
9
- fieldSizeClasses,
10
- fieldInsetClass,
11
- fieldInsetHeightClass,
12
- fieldInsetSizeHeightClasses,
13
- fieldDisabledClass,
14
- fieldInputClass,
15
- } from "./Field.styles";
6
+ import { type FieldSize, fieldInputClass, getFieldWrapperClass } from "./Field.styles";
7
+ import { Invalid } from "../../form-control/Invalid";
16
8
 
17
9
  type DatePickerUnit = "year" | "month" | "date";
18
10
 
@@ -52,13 +44,22 @@ export interface DatePickerProps {
52
44
 
53
45
  /** 커스텀 style */
54
46
  style?: JSX.CSSProperties;
47
+
48
+ /** 필수 입력 여부 */
49
+ required?: boolean;
50
+
51
+ /** 커스텀 유효성 검사 함수 */
52
+ validate?: (value: DateOnly | undefined) => string | undefined;
53
+
54
+ /** touchMode: 포커스 해제 후에만 에러 표시 */
55
+ touchMode?: boolean;
55
56
  }
56
57
 
57
58
  /**
58
- * DateOnly 값을 input value용 문자열로 변환 (ISO 형식)
59
+ * DateOnly 값을 타입에 맞는 문자열로 변환
59
60
  */
60
- function formatValue(value: DateOnly | undefined, type: DatePickerUnit): string {
61
- if (value == null) return "";
61
+ function formatDateValue(value: DateOnly | undefined, type: DatePickerUnit): string | undefined {
62
+ if (value == null) return undefined;
62
63
 
63
64
  switch (type) {
64
65
  case "year":
@@ -109,22 +110,6 @@ function getInputType(type: DatePickerUnit): string {
109
110
  }
110
111
  }
111
112
 
112
- /**
113
- * min/max 속성을 타입에 맞는 문자열로 변환
114
- */
115
- function formatMinMax(value: DateOnly | undefined, type: DatePickerUnit): string | undefined {
116
- if (value == null) return undefined;
117
-
118
- switch (type) {
119
- case "year":
120
- return value.toFormatString("yyyy");
121
- case "month":
122
- return value.toFormatString("yyyy-MM");
123
- case "date":
124
- return value.toFormatString("yyyy-MM-dd");
125
- }
126
- }
127
-
128
113
  /**
129
114
  * DatePicker 컴포넌트
130
115
  *
@@ -165,6 +150,9 @@ export const DatePicker: Component<DatePickerProps> = (props) => {
165
150
  "inset",
166
151
  "class",
167
152
  "style",
153
+ "required",
154
+ "validate",
155
+ "touchMode",
168
156
  ]);
169
157
 
170
158
  // 기본 단위는 date
@@ -177,7 +165,7 @@ export const DatePicker: Component<DatePickerProps> = (props) => {
177
165
  });
178
166
 
179
167
  // 표시 값
180
- const displayValue = () => formatValue(value(), fieldType());
168
+ const displayValue = () => formatDateValue(value(), fieldType()) ?? "";
181
169
 
182
170
  // 값 확정 핸들러 (포커스 아웃 또는 Enter 시)
183
171
  const handleChange: JSX.EventHandler<HTMLInputElement, Event> = (e) => {
@@ -188,79 +176,93 @@ export const DatePicker: Component<DatePickerProps> = (props) => {
188
176
 
189
177
  // wrapper 클래스 (includeCustomClass: 외부 class를 포함할지 여부)
190
178
  const getWrapperClass = (includeCustomClass: boolean) =>
191
- twMerge(
192
- fieldBaseClass,
193
- local.size && fieldSizeClasses[local.size],
194
- local.disabled && fieldDisabledClass,
195
- local.inset && fieldInsetClass + " block",
196
- local.inset && (local.size ? fieldInsetSizeHeightClasses[local.size] : fieldInsetHeightClass),
197
-
198
- includeCustomClass && local.class,
199
- );
179
+ getFieldWrapperClass({
180
+ size: local.size,
181
+ disabled: local.disabled,
182
+ inset: local.inset,
183
+ includeCustomClass: includeCustomClass && local.class,
184
+ });
200
185
 
201
186
  // 편집 가능 여부
202
187
  const isEditable = () => !local.disabled && !local.readonly;
203
188
 
189
+ // 유효성 검사 메시지 (순서대로 검사, 최초 실패 메시지 반환)
190
+ const errorMsg = createMemo(() => {
191
+ const v = value();
192
+ if (local.required && v === undefined) return "필수 입력 항목입니다";
193
+ if (v !== undefined) {
194
+ if (local.min !== undefined && v.tick < local.min.tick)
195
+ return `${local.min}보다 크거나 같아야 합니다`;
196
+ if (local.max !== undefined && v.tick > local.max.tick)
197
+ return `${local.max}보다 작거나 같아야 합니다`;
198
+ }
199
+ return local.validate?.(v);
200
+ });
201
+
204
202
  return (
205
- <Show
206
- when={local.inset}
207
- fallback={
208
- // standalone 모드: 기존 Show 패턴 유지
209
- <Show
210
- when={isEditable()}
211
- fallback={
212
- <div
213
- {...rest}
214
- data-date-field
215
- class={twMerge(getWrapperClass(true), "sd-date-field")}
216
- style={local.style}
217
- title={local.title}
218
- >
219
- {displayValue() || "\u00A0"}
220
- </div>
221
- }
222
- >
223
- <div {...rest} data-date-field class={getWrapperClass(true)} style={local.style}>
224
- <input
225
- type={getInputType(fieldType())}
226
- class={fieldInputClass}
227
- value={displayValue()}
228
- title={local.title}
229
- min={formatMinMax(local.min, fieldType())}
230
- max={formatMinMax(local.max, fieldType())}
231
- onChange={handleChange}
232
- />
233
- </div>
234
- </Show>
235
- }
203
+ <Invalid
204
+ message={errorMsg()}
205
+ variant={local.inset ? "dot" : "border"}
206
+ touchMode={local.touchMode}
236
207
  >
237
- {/* inset 모드: dual-element overlay 패턴 */}
238
- <div
239
- {...rest}
240
- data-date-field
241
- class={twMerge(getWrapperClass(false), "relative", local.class)}
242
- style={local.style}
208
+ <Show
209
+ when={local.inset}
210
+ fallback={
211
+ // standalone 모드: 기존 Show 패턴 유지
212
+ <Show
213
+ when={isEditable()}
214
+ fallback={
215
+ <div
216
+ {...rest}
217
+ data-date-field
218
+ class={twMerge(getWrapperClass(true), "sd-date-field")}
219
+ style={local.style}
220
+ title={local.title}
221
+ >
222
+ {displayValue() || "\u00A0"}
223
+ </div>
224
+ }
225
+ >
226
+ <div {...rest} data-date-field class={getWrapperClass(true)} style={local.style}>
227
+ <input
228
+ type={getInputType(fieldType())}
229
+ class={fieldInputClass}
230
+ value={displayValue()}
231
+ title={local.title}
232
+ min={formatDateValue(local.min, fieldType())}
233
+ max={formatDateValue(local.max, fieldType())}
234
+ onChange={handleChange}
235
+ />
236
+ </div>
237
+ </Show>
238
+ }
243
239
  >
244
- <div
245
- data-date-field-content
246
- style={{ visibility: isEditable() ? "hidden" : undefined }}
247
- title={local.title}
248
- >
249
- {displayValue() || "\u00A0"}
250
- </div>
251
-
252
- <Show when={isEditable()}>
253
- <input
254
- type={getInputType(fieldType())}
255
- class={clsx(fieldInputClass, "absolute left-0 top-0 size-full", "px-2 py-1")}
256
- value={displayValue()}
240
+ {/* inset 모드: dual-element overlay 패턴 */}
241
+ <div {...rest} data-date-field class={clsx("relative", local.class)} style={local.style}>
242
+ <div
243
+ data-date-field-content
244
+ class={getWrapperClass(false)}
245
+ style={{ visibility: isEditable() ? "hidden" : undefined }}
257
246
  title={local.title}
258
- min={formatMinMax(local.min, fieldType())}
259
- max={formatMinMax(local.max, fieldType())}
260
- onChange={handleChange}
261
- />
262
- </Show>
263
- </div>
264
- </Show>
247
+ >
248
+ {displayValue() || "\u00A0"}
249
+ </div>
250
+
251
+ <Show when={isEditable()}>
252
+ <div class={twMerge(getWrapperClass(false), "absolute left-0 top-0 size-full")}>
253
+ <input
254
+ type={getInputType(fieldType())}
255
+ class={fieldInputClass}
256
+ value={displayValue()}
257
+ title={local.title}
258
+ min={formatDateValue(local.min, fieldType())}
259
+ max={formatDateValue(local.max, fieldType())}
260
+ onChange={handleChange}
261
+ />
262
+ </div>
263
+ </Show>
264
+ </div>
265
+ </Show>
266
+ </Invalid>
265
267
  );
266
268
  };
@@ -1,18 +1,10 @@
1
1
  import clsx from "clsx";
2
- import { type Component, type JSX, Show, splitProps } from "solid-js";
2
+ import { type Component, createMemo, type JSX, Show, splitProps } from "solid-js";
3
3
  import { twMerge } from "tailwind-merge";
4
4
  import { DateTime } from "@simplysm/core-common";
5
5
  import { createControllableSignal } from "../../../hooks/createControllableSignal";
6
- import {
7
- type FieldSize,
8
- fieldBaseClass,
9
- fieldSizeClasses,
10
- fieldInsetClass,
11
- fieldInsetHeightClass,
12
- fieldInsetSizeHeightClasses,
13
- fieldDisabledClass,
14
- fieldInputClass,
15
- } from "./Field.styles";
6
+ import { type FieldSize, fieldInputClass, getFieldWrapperClass } from "./Field.styles";
7
+ import { Invalid } from "../../form-control/Invalid";
16
8
 
17
9
  type DateTimePickerUnit = "minute" | "second";
18
10
 
@@ -52,13 +44,25 @@ export interface DateTimePickerProps {
52
44
 
53
45
  /** 커스텀 style */
54
46
  style?: JSX.CSSProperties;
47
+
48
+ /** 필수 입력 여부 */
49
+ required?: boolean;
50
+
51
+ /** 커스텀 유효성 검사 함수 */
52
+ validate?: (value: DateTime | undefined) => string | undefined;
53
+
54
+ /** touchMode: 포커스 해제 후에만 에러 표시 */
55
+ touchMode?: boolean;
55
56
  }
56
57
 
57
58
  /**
58
- * DateTime 값을 input value용 문자열로 변환 (ISO 형식)
59
+ * DateTime 값을 타입에 맞는 문자열로 변환
59
60
  */
60
- function formatValue(value: DateTime | undefined, unit: DateTimePickerUnit): string {
61
- if (value == null) return "";
61
+ function formatDateTimeValue(
62
+ value: DateTime | undefined,
63
+ unit: DateTimePickerUnit,
64
+ ): string | undefined {
65
+ if (value == null) return undefined;
62
66
 
63
67
  switch (unit) {
64
68
  case "minute":
@@ -104,20 +108,6 @@ function parseValue(str: string, unit: DateTimePickerUnit): DateTime | undefined
104
108
  }
105
109
  }
106
110
 
107
- /**
108
- * min/max 속성을 타입에 맞는 문자열로 변환
109
- */
110
- function formatMinMax(value: DateTime | undefined, unit: DateTimePickerUnit): string | undefined {
111
- if (value == null) return undefined;
112
-
113
- switch (unit) {
114
- case "minute":
115
- return value.toFormatString("yyyy-MM-ddTHH:mm");
116
- case "second":
117
- return value.toFormatString("yyyy-MM-ddTHH:mm:ss");
118
- }
119
- }
120
-
121
111
  /**
122
112
  * DateTimePicker 컴포넌트
123
113
  *
@@ -155,6 +145,9 @@ export const DateTimePicker: Component<DateTimePickerProps> = (props) => {
155
145
  "inset",
156
146
  "class",
157
147
  "style",
148
+ "required",
149
+ "validate",
150
+ "touchMode",
158
151
  ]);
159
152
 
160
153
  // 기본 단위는 minute
@@ -167,7 +160,7 @@ export const DateTimePicker: Component<DateTimePickerProps> = (props) => {
167
160
  });
168
161
 
169
162
  // 표시 값
170
- const displayValue = () => formatValue(value(), fieldType());
163
+ const displayValue = () => formatDateTimeValue(value(), fieldType()) ?? "";
171
164
 
172
165
  // 값 확정 핸들러 (포커스 아웃 또는 Enter 시)
173
166
  const handleChange: JSX.EventHandler<HTMLInputElement, Event> = (e) => {
@@ -178,15 +171,12 @@ export const DateTimePicker: Component<DateTimePickerProps> = (props) => {
178
171
 
179
172
  // wrapper 클래스 (includeCustomClass: inset 모드에서는 커스텀 class를 외부 div에 적용)
180
173
  const getWrapperClass = (includeCustomClass: boolean) =>
181
- twMerge(
182
- fieldBaseClass,
183
- local.size && fieldSizeClasses[local.size],
184
- local.disabled && fieldDisabledClass,
185
- local.inset && fieldInsetClass + " block",
186
- local.inset && (local.size ? fieldInsetSizeHeightClasses[local.size] : fieldInsetHeightClass),
187
-
188
- includeCustomClass && local.class,
189
- );
174
+ getFieldWrapperClass({
175
+ size: local.size,
176
+ disabled: local.disabled,
177
+ inset: local.inset,
178
+ includeCustomClass: includeCustomClass && local.class,
179
+ });
190
180
 
191
181
  // 편집 가능 여부
192
182
  const isEditable = () => !local.disabled && !local.readonly;
@@ -194,68 +184,90 @@ export const DateTimePicker: Component<DateTimePickerProps> = (props) => {
194
184
  // step 속성 (second일 때 1)
195
185
  const getStep = () => (fieldType() === "second" ? "1" : undefined);
196
186
 
187
+ // 유효성 검사 메시지 (순서대로 검사, 최초 실패 메시지 반환)
188
+ const errorMsg = createMemo(() => {
189
+ const v = value();
190
+ if (local.required && v === undefined) return "필수 입력 항목입니다";
191
+ if (v !== undefined) {
192
+ if (local.min !== undefined && v.tick < local.min.tick)
193
+ return `${local.min.toFormatString("yyyy-MM-dd HH:mm:ss")}보다 크거나 같아야 합니다`;
194
+ if (local.max !== undefined && v.tick > local.max.tick)
195
+ return `${local.max.toFormatString("yyyy-MM-dd HH:mm:ss")}보다 작거나 같아야 합니다`;
196
+ }
197
+ return local.validate?.(v);
198
+ });
199
+
197
200
  return (
198
- <Show
199
- when={local.inset}
200
- fallback={
201
- // standalone 모드
202
- <Show
203
- when={isEditable()}
204
- fallback={
205
- <div
206
- {...rest}
207
- data-datetime-field
208
- class={twMerge(getWrapperClass(true), "sd-datetime-field")}
209
- style={local.style}
210
- title={local.title}
211
- >
212
- {displayValue() || "\u00A0"}
213
- </div>
214
- }
215
- >
216
- <div {...rest} data-datetime-field class={getWrapperClass(true)} style={local.style}>
217
- <input
218
- type="datetime-local"
219
- class={fieldInputClass}
220
- value={displayValue()}
221
- title={local.title}
222
- min={formatMinMax(local.min, fieldType())}
223
- max={formatMinMax(local.max, fieldType())}
224
- step={getStep()}
225
- onChange={handleChange}
226
- />
227
- </div>
228
- </Show>
229
- }
201
+ <Invalid
202
+ message={errorMsg()}
203
+ variant={local.inset ? "dot" : "border"}
204
+ touchMode={local.touchMode}
230
205
  >
231
- {/* inset 모드: dual-element overlay 패턴 */}
232
- <div
233
- {...rest}
234
- data-datetime-field
235
- class={twMerge(getWrapperClass(false), "relative", local.class)}
236
- style={local.style}
206
+ <Show
207
+ when={local.inset}
208
+ fallback={
209
+ // standalone 모드
210
+ <Show
211
+ when={isEditable()}
212
+ fallback={
213
+ <div
214
+ {...rest}
215
+ data-datetime-field
216
+ class={twMerge(getWrapperClass(true), "sd-datetime-field")}
217
+ style={local.style}
218
+ title={local.title}
219
+ >
220
+ {displayValue() || "\u00A0"}
221
+ </div>
222
+ }
223
+ >
224
+ <div {...rest} data-datetime-field class={getWrapperClass(true)} style={local.style}>
225
+ <input
226
+ type="datetime-local"
227
+ class={fieldInputClass}
228
+ value={displayValue()}
229
+ title={local.title}
230
+ min={formatDateTimeValue(local.min, fieldType())}
231
+ max={formatDateTimeValue(local.max, fieldType())}
232
+ step={getStep()}
233
+ onChange={handleChange}
234
+ />
235
+ </div>
236
+ </Show>
237
+ }
237
238
  >
239
+ {/* inset 모드: dual-element overlay 패턴 */}
238
240
  <div
239
- data-datetime-field-content
240
- style={{ visibility: isEditable() ? "hidden" : undefined }}
241
- title={local.title}
241
+ {...rest}
242
+ data-datetime-field
243
+ class={clsx("relative", local.class)}
244
+ style={local.style}
242
245
  >
243
- {displayValue() || "\u00A0"}
244
- </div>
245
-
246
- <Show when={isEditable()}>
247
- <input
248
- type="datetime-local"
249
- class={clsx(fieldInputClass, "absolute left-0 top-0 size-full", "px-2 py-1")}
250
- value={displayValue()}
246
+ <div
247
+ data-datetime-field-content
248
+ class={getWrapperClass(false)}
249
+ style={{ visibility: isEditable() ? "hidden" : undefined }}
251
250
  title={local.title}
252
- min={formatMinMax(local.min, fieldType())}
253
- max={formatMinMax(local.max, fieldType())}
254
- step={getStep()}
255
- onChange={handleChange}
256
- />
257
- </Show>
258
- </div>
259
- </Show>
251
+ >
252
+ {displayValue() || "\u00A0"}
253
+ </div>
254
+
255
+ <Show when={isEditable()}>
256
+ <div class={twMerge(getWrapperClass(false), "absolute left-0 top-0 size-full")}>
257
+ <input
258
+ type="datetime-local"
259
+ class={fieldInputClass}
260
+ value={displayValue()}
261
+ title={local.title}
262
+ min={formatDateTimeValue(local.min, fieldType())}
263
+ max={formatDateTimeValue(local.max, fieldType())}
264
+ step={getStep()}
265
+ onChange={handleChange}
266
+ />
267
+ </div>
268
+ </Show>
269
+ </div>
270
+ </Show>
271
+ </Invalid>
260
272
  );
261
273
  };
@@ -1,4 +1,5 @@
1
1
  import clsx from "clsx";
2
+ import { twMerge } from "tailwind-merge";
2
3
  import { type ComponentSize, paddingLg, paddingSm, paddingXl } from "../../../styles/tokens.styles";
3
4
  import {
4
5
  fieldSurface,
@@ -54,3 +55,47 @@ export const textAreaSizeClasses: Record<FieldSize, string> = {
54
55
 
55
56
  // input 스타일
56
57
  export const fieldInputClass = inputBase;
58
+
59
+ // prefixIcon gap 클래스 (nested ternary 대체)
60
+ export const fieldGapClasses: Record<FieldSize | "default", string> = {
61
+ default: "gap-2",
62
+ sm: "gap-1.5",
63
+ lg: "gap-3",
64
+ xl: "gap-4",
65
+ };
66
+
67
+ // 공유 wrapper 클래스 생성 함수
68
+ export function getFieldWrapperClass(options: {
69
+ size?: FieldSize;
70
+ disabled?: boolean;
71
+ inset?: boolean;
72
+ includeCustomClass?: string | false;
73
+ extra?: string | false;
74
+ }): string {
75
+ return twMerge(
76
+ fieldBaseClass,
77
+ options.extra,
78
+ options.size && fieldSizeClasses[options.size],
79
+ options.disabled && fieldDisabledClass,
80
+ options.inset && fieldInsetClass,
81
+ options.inset &&
82
+ (options.size ? fieldInsetSizeHeightClasses[options.size] : fieldInsetHeightClass),
83
+ options.includeCustomClass,
84
+ );
85
+ }
86
+
87
+ // Textarea 전용 wrapper 클래스 생성 함수
88
+ export function getTextareaWrapperClass(options: {
89
+ size?: FieldSize;
90
+ disabled?: boolean;
91
+ inset?: boolean;
92
+ includeCustomClass?: string | false;
93
+ }): string {
94
+ return twMerge(
95
+ textAreaBaseClass,
96
+ options.size && textAreaSizeClasses[options.size],
97
+ options.disabled && fieldDisabledClass,
98
+ options.inset && fieldInsetClass,
99
+ options.includeCustomClass,
100
+ );
101
+ }
@@ -0,0 +1,18 @@
1
+ import { type Component, Show } from "solid-js";
2
+ import { textMuted } from "../../../styles/tokens.styles";
3
+
4
+ /** 값이 없을 때 placeholder 또는 NBSP를 표시하는 공유 컴포넌트 */
5
+ export const PlaceholderFallback: Component<{ value?: string; placeholder?: string }> = (props) => (
6
+ <>
7
+ <Show
8
+ when={props.value}
9
+ fallback={
10
+ <Show when={props.placeholder != null && props.placeholder !== ""} fallback={"\u00A0"}>
11
+ <span class={textMuted}>{props.placeholder}</span>
12
+ </Show>
13
+ }
14
+ >
15
+ {props.value}
16
+ </Show>
17
+ </>
18
+ );