@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,16 +1,11 @@
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 { createControllableSignal } from "../../../hooks/createControllableSignal";
5
5
  import { createIMEHandler } from "../../../hooks/createIMEHandler";
6
- import {
7
- type FieldSize,
8
- textAreaBaseClass as fieldTextAreaBaseClass,
9
- textAreaSizeClasses,
10
- fieldInsetClass,
11
- fieldDisabledClass,
12
- } from "./Field.styles";
13
- import { textMuted } from "../../../styles/tokens.styles";
6
+ import { type FieldSize, textAreaSizeClasses, getTextareaWrapperClass } from "./Field.styles";
7
+ import { PlaceholderFallback } from "./FieldPlaceholder";
8
+ import { Invalid } from "../../form-control/Invalid";
14
9
 
15
10
  export interface TextareaProps {
16
11
  /** 입력 값 */
@@ -40,6 +35,21 @@ export interface TextareaProps {
40
35
  /** 최소 줄 수 (기본값: 1) */
41
36
  minRows?: number;
42
37
 
38
+ /** 필수 입력 여부 */
39
+ required?: boolean;
40
+
41
+ /** 최소 길이 */
42
+ minLength?: number;
43
+
44
+ /** 최대 길이 */
45
+ maxLength?: number;
46
+
47
+ /** 커스텀 유효성 검사 함수 */
48
+ validate?: (value: string) => string | undefined;
49
+
50
+ /** touchMode: 포커스 해제 후에만 에러 표시 */
51
+ touchMode?: boolean;
52
+
43
53
  /** 커스텀 class */
44
54
  class?: string;
45
55
 
@@ -79,6 +89,11 @@ export const Textarea: Component<TextareaProps> = (props) => {
79
89
  "size",
80
90
  "inset",
81
91
  "minRows",
92
+ "required",
93
+ "minLength",
94
+ "maxLength",
95
+ "validate",
96
+ "touchMode",
82
97
  "class",
83
98
  "style",
84
99
  ]);
@@ -136,14 +151,12 @@ export const Textarea: Component<TextareaProps> = (props) => {
136
151
 
137
152
  // wrapper 클래스 (includeCustomClass=false일 때 local.class 제외 — inset에서 outer에만 적용)
138
153
  const getWrapperClass = (includeCustomClass: boolean) =>
139
- twMerge(
140
- fieldTextAreaBaseClass,
141
- local.size && textAreaSizeClasses[local.size],
142
- local.disabled && fieldDisabledClass,
143
- local.inset && fieldInsetClass,
144
-
145
- includeCustomClass && local.class,
146
- );
154
+ getTextareaWrapperClass({
155
+ size: local.size,
156
+ disabled: local.disabled,
157
+ inset: local.inset,
158
+ includeCustomClass: includeCustomClass && local.class,
159
+ });
147
160
 
148
161
  const getTextareaClass = () =>
149
162
  twMerge(textareaBaseClass, local.size && textAreaSizeClasses[local.size], local.inset && "p-0");
@@ -158,100 +171,113 @@ export const Textarea: Component<TextareaProps> = (props) => {
158
171
  }
159
172
  });
160
173
 
174
+ // 유효성 검사 메시지 (순서대로 검사, 최초 실패 메시지 반환)
175
+ const errorMsg = createMemo(() => {
176
+ const v = value();
177
+ if (local.required && !v) return "필수 입력 항목입니다";
178
+ if (v) {
179
+ if (local.minLength != null && v.length < local.minLength)
180
+ return `최소 ${local.minLength}자 이상 입력하세요`;
181
+ if (local.maxLength != null && v.length > local.maxLength)
182
+ return `최대 ${local.maxLength}자까지 입력 가능합니다`;
183
+ }
184
+ return local.validate?.(v);
185
+ });
186
+
161
187
  return (
162
- <Show
163
- when={local.inset}
164
- fallback={
165
- // standalone 모드: 기존 Show 패턴 유지
166
- <Show
167
- when={isEditable()}
168
- fallback={
188
+ <Invalid message={errorMsg()} variant="border" touchMode={local.touchMode}>
189
+ <Show
190
+ when={local.inset}
191
+ fallback={
192
+ // standalone 모드: 기존 Show 패턴 유지
193
+ <Show
194
+ when={isEditable()}
195
+ fallback={
196
+ <div
197
+ {...rest}
198
+ data-textarea-field
199
+ class={getWrapperClass(true)}
200
+ style={{ "white-space": "pre-wrap", "word-break": "break-all", ...local.style }}
201
+ title={local.title}
202
+ >
203
+ <PlaceholderFallback value={value()} placeholder={local.placeholder} />
204
+ </div>
205
+ }
206
+ >
169
207
  <div
170
208
  {...rest}
171
209
  data-textarea-field
172
210
  class={getWrapperClass(true)}
173
- style={{ "white-space": "pre-wrap", "word-break": "break-all", ...local.style }}
174
- title={local.title}
211
+ style={{ position: "relative", ...local.style }}
175
212
  >
176
- {value() ||
177
- (local.placeholder != null && local.placeholder !== "" ? (
178
- <span class={textMuted}>{local.placeholder}</span>
179
- ) : (
180
- "\u00A0"
181
- ))}
213
+ <div
214
+ data-hidden-content
215
+ style={{
216
+ "visibility": "hidden",
217
+ "white-space": "pre-wrap",
218
+ "word-break": "break-all",
219
+ }}
220
+ >
221
+ {contentForHeight()}
222
+ </div>
223
+
224
+ <textarea
225
+ class={getTextareaClass()}
226
+ value={value()}
227
+ placeholder={local.placeholder}
228
+ title={local.title}
229
+ onKeyDown={handleKeyDown}
230
+ onInput={handleInput}
231
+ onCompositionStart={handleCompositionStart}
232
+ onCompositionEnd={handleCompositionEnd}
233
+ />
182
234
  </div>
183
- }
235
+ </Show>
236
+ }
237
+ >
238
+ {/* inset 모드: dual-element overlay 패턴 */}
239
+ <div
240
+ {...rest}
241
+ data-textarea-field
242
+ class={clsx("relative", local.class)}
243
+ style={local.style}
184
244
  >
185
245
  <div
186
- {...rest}
187
- data-textarea-field
188
- class={getWrapperClass(true)}
189
- style={{ position: "relative", ...local.style }}
246
+ data-textarea-field-content
247
+ class={getWrapperClass(false)}
248
+ style={{
249
+ "visibility": isEditable() ? "hidden" : undefined,
250
+ "white-space": "pre-wrap",
251
+ "word-break": "break-all",
252
+ }}
253
+ title={local.title}
190
254
  >
255
+ {isEditable() ? (
256
+ contentForHeight()
257
+ ) : (
258
+ <PlaceholderFallback value={value()} placeholder={local.placeholder} />
259
+ )}
260
+ </div>
261
+
262
+ <Show when={isEditable()}>
191
263
  <div
192
- data-hidden-content
193
- style={{
194
- "visibility": "hidden",
195
- "white-space": "pre-wrap",
196
- "word-break": "break-all",
197
- }}
264
+ class={twMerge(getWrapperClass(false), "absolute left-0 top-0 size-full")}
265
+ style={{ position: "relative" }}
198
266
  >
199
- {contentForHeight()}
267
+ <textarea
268
+ class={twMerge(textareaBaseClass, local.size && textAreaSizeClasses[local.size])}
269
+ value={value()}
270
+ placeholder={local.placeholder}
271
+ title={local.title}
272
+ onKeyDown={handleKeyDown}
273
+ onInput={handleInput}
274
+ onCompositionStart={handleCompositionStart}
275
+ onCompositionEnd={handleCompositionEnd}
276
+ />
200
277
  </div>
201
-
202
- <textarea
203
- class={getTextareaClass()}
204
- value={value()}
205
- placeholder={local.placeholder}
206
- title={local.title}
207
- onKeyDown={handleKeyDown}
208
- onInput={handleInput}
209
- onCompositionStart={handleCompositionStart}
210
- onCompositionEnd={handleCompositionEnd}
211
- />
212
- </div>
213
- </Show>
214
- }
215
- >
216
- {/* inset 모드: dual-element overlay 패턴 */}
217
- <div
218
- {...rest}
219
- data-textarea-field
220
- class={twMerge(getWrapperClass(false), "relative", local.class)}
221
- style={local.style}
222
- >
223
- <div
224
- data-textarea-field-content
225
- style={{
226
- "visibility": isEditable() ? "hidden" : undefined,
227
- "white-space": "pre-wrap",
228
- "word-break": "break-all",
229
- }}
230
- title={local.title}
231
- >
232
- {isEditable()
233
- ? contentForHeight()
234
- : value() ||
235
- (local.placeholder != null && local.placeholder !== "" ? (
236
- <span class={textMuted}>{local.placeholder}</span>
237
- ) : (
238
- "\u00A0"
239
- ))}
278
+ </Show>
240
279
  </div>
241
-
242
- <Show when={isEditable()}>
243
- <textarea
244
- class={twMerge(textareaBaseClass, local.size && textAreaSizeClasses[local.size])}
245
- value={value()}
246
- placeholder={local.placeholder}
247
- title={local.title}
248
- onKeyDown={handleKeyDown}
249
- onInput={handleInput}
250
- onCompositionStart={handleCompositionStart}
251
- onCompositionEnd={handleCompositionEnd}
252
- />
253
- </Show>
254
- </div>
255
- </Show>
280
+ </Show>
281
+ </Invalid>
256
282
  );
257
283
  };
@@ -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 { Time } 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 TimePickerUnit = "minute" | "second";
18
10
 
@@ -46,6 +38,21 @@ export interface TimePickerProps {
46
38
 
47
39
  /** 커스텀 style */
48
40
  style?: JSX.CSSProperties;
41
+
42
+ /** 최소 시간 */
43
+ min?: Time;
44
+
45
+ /** 최대 시간 */
46
+ max?: Time;
47
+
48
+ /** 필수 입력 여부 */
49
+ required?: boolean;
50
+
51
+ /** 커스텀 유효성 검사 함수 */
52
+ validate?: (value: Time | undefined) => string | undefined;
53
+
54
+ /** touchMode: 포커스 해제 후에만 에러 표시 */
55
+ touchMode?: boolean;
49
56
  }
50
57
 
51
58
  /**
@@ -111,6 +118,11 @@ export const TimePicker: Component<TimePickerProps> = (props) => {
111
118
  "inset",
112
119
  "class",
113
120
  "style",
121
+ "min",
122
+ "max",
123
+ "required",
124
+ "validate",
125
+ "touchMode",
114
126
  ]);
115
127
 
116
128
  // 기본 단위는 minute
@@ -134,15 +146,12 @@ export const TimePicker: Component<TimePickerProps> = (props) => {
134
146
 
135
147
  // wrapper 클래스
136
148
  const getWrapperClass = (includeCustomClass: boolean) =>
137
- twMerge(
138
- fieldBaseClass,
139
- local.size && fieldSizeClasses[local.size],
140
- local.disabled && fieldDisabledClass,
141
- local.inset && fieldInsetClass + " block",
142
- local.inset && (local.size ? fieldInsetSizeHeightClasses[local.size] : fieldInsetHeightClass),
143
-
144
- includeCustomClass && local.class,
145
- );
149
+ getFieldWrapperClass({
150
+ size: local.size,
151
+ disabled: local.disabled,
152
+ inset: local.inset,
153
+ includeCustomClass: includeCustomClass && local.class,
154
+ });
146
155
 
147
156
  // 편집 가능 여부
148
157
  const isEditable = () => !local.disabled && !local.readonly;
@@ -150,64 +159,81 @@ export const TimePicker: Component<TimePickerProps> = (props) => {
150
159
  // step 속성 (second일 때 1)
151
160
  const getStep = () => (fieldType() === "second" ? "1" : undefined);
152
161
 
162
+ // 유효성 검사 메시지 (순서대로 검사, 최초 실패 메시지 반환)
163
+ const errorMsg = createMemo(() => {
164
+ const v = value();
165
+ if (local.required && v === undefined) return "필수 입력 항목입니다";
166
+ if (v !== undefined) {
167
+ if (local.min !== undefined && v.tick < local.min.tick)
168
+ return `${local.min.toFormatString("HH:mm:ss")}보다 크거나 같아야 합니다`;
169
+ if (local.max !== undefined && v.tick > local.max.tick)
170
+ return `${local.max.toFormatString("HH:mm:ss")}보다 작거나 같아야 합니다`;
171
+ }
172
+ return local.validate?.(v);
173
+ });
174
+
153
175
  return (
154
- <Show
155
- when={local.inset}
156
- fallback={
157
- // standalone 모드
158
- <Show
159
- when={isEditable()}
160
- fallback={
161
- <div
162
- {...rest}
163
- data-time-field
164
- class={twMerge(getWrapperClass(true), "sd-time-field")}
165
- style={local.style}
166
- title={local.title}
167
- >
168
- {displayValue() || "\u00A0"}
169
- </div>
170
- }
171
- >
172
- <div {...rest} data-time-field class={getWrapperClass(true)} style={local.style}>
173
- <input
174
- type="time"
175
- class={fieldInputClass}
176
- value={displayValue()}
177
- title={local.title}
178
- step={getStep()}
179
- onChange={handleChange}
180
- />
181
- </div>
182
- </Show>
183
- }
176
+ <Invalid
177
+ message={errorMsg()}
178
+ variant={local.inset ? "dot" : "border"}
179
+ touchMode={local.touchMode}
184
180
  >
185
- {/* inset 모드: dual-element overlay 패턴 */}
186
- <div
187
- {...rest}
188
- data-time-field
189
- class={twMerge(getWrapperClass(false), "relative", local.class)}
190
- style={local.style}
181
+ <Show
182
+ when={local.inset}
183
+ fallback={
184
+ // standalone 모드
185
+ <Show
186
+ when={isEditable()}
187
+ fallback={
188
+ <div
189
+ {...rest}
190
+ data-time-field
191
+ class={twMerge(getWrapperClass(true), "sd-time-field")}
192
+ style={local.style}
193
+ title={local.title}
194
+ >
195
+ {displayValue() || "\u00A0"}
196
+ </div>
197
+ }
198
+ >
199
+ <div {...rest} data-time-field class={getWrapperClass(true)} style={local.style}>
200
+ <input
201
+ type="time"
202
+ class={fieldInputClass}
203
+ value={displayValue()}
204
+ title={local.title}
205
+ step={getStep()}
206
+ onChange={handleChange}
207
+ />
208
+ </div>
209
+ </Show>
210
+ }
191
211
  >
192
- <div
193
- data-time-field-content
194
- style={{ visibility: isEditable() ? "hidden" : undefined }}
195
- title={local.title}
196
- >
197
- {displayValue() || "\u00A0"}
198
- </div>
199
-
200
- <Show when={isEditable()}>
201
- <input
202
- type="time"
203
- class={clsx(fieldInputClass, "absolute left-0 top-0 size-full", "px-2 py-1")}
204
- value={displayValue()}
212
+ {/* inset 모드: dual-element overlay 패턴 */}
213
+ <div {...rest} data-time-field class={clsx("relative", local.class)} style={local.style}>
214
+ <div
215
+ data-time-field-content
216
+ class={getWrapperClass(false)}
217
+ style={{ visibility: isEditable() ? "hidden" : undefined }}
205
218
  title={local.title}
206
- step={getStep()}
207
- onChange={handleChange}
208
- />
209
- </Show>
210
- </div>
211
- </Show>
219
+ >
220
+ {displayValue() || "\u00A0"}
221
+ </div>
222
+
223
+ <Show when={isEditable()}>
224
+ <div class={twMerge(getWrapperClass(false), "absolute left-0 top-0 size-full")}>
225
+ <input
226
+ type="time"
227
+ class={fieldInputClass}
228
+ value={displayValue()}
229
+ title={local.title}
230
+ step={getStep()}
231
+ onChange={handleChange}
232
+ />
233
+ </div>
234
+ </Show>
235
+ </div>
236
+ </Show>
237
+ </Invalid>
212
238
  );
213
239
  };
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  children,
3
+ createMemo,
3
4
  createSignal,
4
5
  For,
5
6
  type JSX,
@@ -19,7 +20,9 @@ import { ripple } from "../../../directives/ripple";
19
20
  import { splitSlots } from "../../../helpers/splitSlots";
20
21
  import { borderDefault, type ComponentSize, textMuted } from "../../../styles/tokens.styles";
21
22
  import { createControllableSignal } from "../../../hooks/createControllableSignal";
23
+ import { createItemTemplate } from "../../../hooks/createItemTemplate";
22
24
  import { chevronWrapperClass, getTriggerClass } from "../DropdownTrigger.styles";
25
+ import { Invalid } from "../Invalid";
23
26
 
24
27
  void ripple;
25
28
 
@@ -40,23 +43,23 @@ const SelectAction: ParentComponent<SelectActionProps> = (props) => {
40
43
  {...rest}
41
44
  type="button"
42
45
  data-select-action
46
+ use:ripple
43
47
  class={twMerge(
44
48
  clsx(
45
49
  "border",
46
50
  borderDefault,
47
51
  "px-1.5",
48
- "rounded-r",
49
52
  "font-bold text-primary-500",
50
53
  "hover:bg-base-100 dark:hover:bg-base-700",
54
+ "group-focus-within:border-y-primary-400",
55
+ "last:group-focus-within:border-r-primary-400",
56
+ "dark:group-focus-within:border-y-primary-400",
57
+ "dark:last:group-focus-within:border-r-primary-400",
58
+ "focus:relative focus:z-10 focus:border-primary-400",
59
+ "dark:focus:border-primary-400",
51
60
  ),
52
61
  local.class,
53
62
  )}
54
- onClick={(e) => {
55
- e.stopPropagation();
56
- if (typeof rest.onClick === "function") {
57
- rest.onClick(e);
58
- }
59
- }}
60
63
  >
61
64
  {local.children}
62
65
  </button>
@@ -68,33 +71,9 @@ const SelectAction: ParentComponent<SelectActionProps> = (props) => {
68
71
  */
69
72
  const SelectHeader: ParentComponent = (props) => <div data-select-header>{props.children}</div>;
70
73
 
71
- /**
72
- * items prop 방식일 때 아이템 렌더링 템플릿
73
- *
74
- * 함수 참조를 저장하기 위해 전역 Map 사용
75
- */
76
- interface SelectItemTemplateProps<TValue> {
77
- children: (item: TValue, index: number, depth: number) => JSX.Element;
78
- }
79
-
80
- // 템플릿 함수를 저장하는 전역 Map (WeakMap 사용하여 메모리 누수 방지)
81
- const templateFnMap = new WeakMap<
82
- HTMLElement,
83
- (item: unknown, index: number, depth: number) => JSX.Element
84
- >();
85
-
86
- const SelectItemTemplate = <T,>(props: SelectItemTemplateProps<T>) => (
87
- <span
88
- ref={(el) => {
89
- templateFnMap.set(
90
- el,
91
- props.children as (item: unknown, index: number, depth: number) => JSX.Element,
92
- );
93
- }}
94
- data-select-item-template
95
- style={{ display: "none" }}
96
- />
97
- );
74
+ const { TemplateSlot: SelectItemTemplate, getTemplate: getSelectItemTemplate } = createItemTemplate<
75
+ [item: unknown, index: number, depth: number]
76
+ >("data-select-item-template");
98
77
 
99
78
  // Props 정의
100
79
 
@@ -115,6 +94,12 @@ interface SelectCommonProps {
115
94
  /** 테두리 없는 스타일 */
116
95
  inset?: boolean;
117
96
 
97
+ /** 커스텀 유효성 검사 함수 */
98
+ validate?: (value: unknown) => string | undefined;
99
+
100
+ /** touchMode: 포커스 해제 후에만 에러 표시 */
101
+ touchMode?: boolean;
102
+
118
103
  /** 커스텀 class */
119
104
  class?: string;
120
105
 
@@ -225,6 +210,8 @@ export const Select: SelectComponent = <T,>(props: SelectProps<T>) => {
225
210
  "items",
226
211
  "getChildren",
227
212
  "renderValue",
213
+ "validate",
214
+ "touchMode",
228
215
  ]);
229
216
 
230
217
  let triggerRef!: HTMLDivElement;
@@ -293,6 +280,14 @@ export const Select: SelectComponent = <T,>(props: SelectProps<T>) => {
293
280
  }
294
281
  };
295
282
 
283
+ // 유효성 검사 메시지
284
+ const errorMsg = createMemo(() => {
285
+ const v = getValue();
286
+ if (local.required && (v === undefined || v === null || v === ""))
287
+ return "필수 입력 항목입니다";
288
+ return local.validate?.(v);
289
+ });
290
+
296
291
  // 트리거 클래스
297
292
  const getTriggerClassName = () =>
298
293
  getTriggerClass({
@@ -315,10 +310,7 @@ export const Select: SelectComponent = <T,>(props: SelectProps<T>) => {
315
310
  const getItemTemplate = ():
316
311
  | ((item: T, index: number, depth: number) => JSX.Element)
317
312
  | undefined => {
318
- const templateSlots = slots().selectItemTemplate;
319
- if (templateSlots.length === 0) return undefined;
320
- // WeakMap에서 함수 참조 가져오기
321
- return templateFnMap.get(templateSlots[0]) as
313
+ return getSelectItemTemplate(slots().selectItemTemplate) as
322
314
  | ((item: T, index: number, depth: number) => JSX.Element)
323
315
  | undefined;
324
316
  };
@@ -379,7 +371,7 @@ export const Select: SelectComponent = <T,>(props: SelectProps<T>) => {
379
371
  };
380
372
 
381
373
  return (
382
- <div {...rest} data-select class={local.inset ? "flex" : "inline-flex"}>
374
+ <div {...rest} data-select class={clsx("group", local.inset ? "flex" : "inline-flex")}>
383
375
  <div
384
376
  ref={triggerRef}
385
377
  use:ripple={!local.disabled}
@@ -391,7 +383,11 @@ export const Select: SelectComponent = <T,>(props: SelectProps<T>) => {
391
383
  tabIndex={local.disabled ? -1 : 0}
392
384
  class={twMerge(
393
385
  getTriggerClassName(),
394
- slots().selectAction.length > 0 && "rounded-r-none border-r-0",
386
+ slots().selectAction.length > 0 &&
387
+ clsx(
388
+ "rounded-r-none border-r-0",
389
+ "group-focus-within:border-primary-400 dark:group-focus-within:border-primary-400",
390
+ ),
395
391
  )}
396
392
  style={local.style}
397
393
  onClick={handleTriggerClick}
@@ -402,7 +398,17 @@ export const Select: SelectComponent = <T,>(props: SelectProps<T>) => {
402
398
  <Icon icon={IconChevronDown} size="1em" />
403
399
  </div>
404
400
  </div>
405
- <Show when={slots().selectAction.length > 0}>{slots().selectAction}</Show>
401
+ <Show when={slots().selectAction.length > 0}>
402
+ <div
403
+ class={clsx(
404
+ "contents",
405
+ "[&>[data-select-action]:last-child]:rounded-r",
406
+ "[&>[data-select-action]+[data-select-action]]:-ml-px",
407
+ )}
408
+ >
409
+ {slots().selectAction}
410
+ </div>
411
+ </Show>
406
412
 
407
413
  <Dropdown triggerRef={() => triggerRef} open={open()} onOpenChange={setOpen} keyboardNav>
408
414
  <Show when={slots().selectHeader.length > 0}>{slots().selectHeader.single()}</Show>
@@ -417,9 +423,11 @@ export const Select: SelectComponent = <T,>(props: SelectProps<T>) => {
417
423
  };
418
424
 
419
425
  return (
420
- <SelectContext.Provider value={contextValue as SelectContextValue}>
421
- <SelectInner>{local.children}</SelectInner>
422
- </SelectContext.Provider>
426
+ <Invalid message={errorMsg()} variant="border" touchMode={local.touchMode}>
427
+ <SelectContext.Provider value={contextValue as SelectContextValue}>
428
+ <SelectInner>{local.children}</SelectInner>
429
+ </SelectContext.Provider>
430
+ </Invalid>
423
431
  );
424
432
  };
425
433