@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,15 +1,19 @@
1
1
  import { type JSX, type ParentComponent, Show, splitProps } from "solid-js";
2
2
  import clsx from "clsx";
3
3
  import { twMerge } from "tailwind-merge";
4
- import { type SemanticTheme, themeTokens } from "../../styles/tokens.styles";
4
+ import {
5
+ type ComponentSizeCompact,
6
+ type SemanticTheme,
7
+ themeTokens,
8
+ } from "../../styles/tokens.styles";
5
9
 
6
10
  export type ProgressTheme = SemanticTheme;
7
- export type ProgressSize = "sm" | "lg";
8
11
 
9
12
  export interface ProgressProps extends JSX.HTMLAttributes<HTMLDivElement> {
13
+ /** 진행률 (0~1 범위, 0 = 0%, 1 = 100%) */
10
14
  value: number;
11
15
  theme?: ProgressTheme;
12
- size?: ProgressSize;
16
+ size?: ComponentSizeCompact;
13
17
  inset?: boolean;
14
18
  }
15
19
 
@@ -21,7 +25,7 @@ const baseClass = clsx(
21
25
  "border border-base-200 dark:border-base-700",
22
26
  );
23
27
 
24
- const sizeClasses: Record<"default" | ProgressSize, string> = {
28
+ const sizeClasses: Record<"default" | ComponentSizeCompact, string> = {
25
29
  default: "py-1 px-2",
26
30
  sm: "py-0.5 px-2",
27
31
  lg: "py-2 px-3",
@@ -46,7 +50,7 @@ export const Progress: ParentComponent<ProgressProps> = (props) => {
46
50
  return clsx("absolute left-0 top-0 h-full", "z-[1]", "transition-all", barThemeClasses[theme]);
47
51
  };
48
52
 
49
- const getPercentText = () => (local.value * 100).toFixed(2) + "%";
53
+ const getPercentText = () => (Math.max(0, Math.min(1, local.value)) * 100).toFixed(2) + "%";
50
54
 
51
55
  return (
52
56
  <div data-progress class={getClassName()} {...rest}>
@@ -15,6 +15,7 @@ import "./BusyContainer.css";
15
15
 
16
16
  export interface BusyContainerProps extends Omit<JSX.HTMLAttributes<HTMLDivElement>, "children"> {
17
17
  busy?: boolean;
18
+ ready?: boolean;
18
19
  variant?: BusyVariant;
19
20
  message?: string;
20
21
  progressPercent?: number;
@@ -57,6 +58,7 @@ const barIndicatorClass = clsx("absolute left-0 top-0", "h-1 w-full", "bg-white
57
58
  export const BusyContainer: ParentComponent<BusyContainerProps> = (props) => {
58
59
  const [local, rest] = splitProps(props, [
59
60
  "busy",
61
+ "ready",
60
62
  "variant",
61
63
  "message",
62
64
  "progressPercent",
@@ -68,11 +70,13 @@ export const BusyContainer: ParentComponent<BusyContainerProps> = (props) => {
68
70
  const currVariant = (): BusyVariant => local.variant ?? busyCtx?.variant() ?? "spinner";
69
71
 
70
72
  // 애니메이션 상태 (mount transition)
71
- const { mounted, animating, unmount } = createMountTransition(() => !!local.busy);
73
+ const { mounted, animating, unmount } = createMountTransition(
74
+ () => local.ready === false || !!local.busy,
75
+ );
72
76
 
73
77
  const handleTransitionEnd = (e: TransitionEvent) => {
74
78
  if (e.propertyName !== "opacity") return;
75
- if (!local.busy) {
79
+ if (local.ready !== false && !local.busy) {
76
80
  unmount();
77
81
  }
78
82
  };
@@ -82,7 +86,7 @@ export const BusyContainer: ParentComponent<BusyContainerProps> = (props) => {
82
86
 
83
87
  createEffect(() => {
84
88
  const handleKeyDownCapture = (e: KeyboardEvent) => {
85
- if (local.busy) {
89
+ if (local.ready === false || local.busy) {
86
90
  e.preventDefault();
87
91
  e.stopPropagation();
88
92
  }
@@ -117,7 +121,7 @@ export const BusyContainer: ParentComponent<BusyContainerProps> = (props) => {
117
121
  <Show when={currVariant() === "spinner"}>
118
122
  <div class={spinnerClass} />
119
123
  </Show>
120
- <Show when={currVariant() === "bar" && local.busy}>
124
+ <Show when={currVariant() === "bar" && (local.ready === false || local.busy)}>
121
125
  <div class={barIndicatorClass}>
122
126
  <div
123
127
  class={clsx(
@@ -155,7 +159,7 @@ export const BusyContainer: ParentComponent<BusyContainerProps> = (props) => {
155
159
  </Show>
156
160
  </div>
157
161
  </Show>
158
- {local.children}
162
+ <Show when={local.ready !== false}>{local.children}</Show>
159
163
  </div>
160
164
  );
161
165
  };
@@ -65,7 +65,7 @@ export const NotificationBanner: Component = () => {
65
65
  class={clsx(baseClass, themeClasses[item().theme])}
66
66
  >
67
67
  <div class={contentClass}>
68
- <span class="font-semibold">{item().title}</span>
68
+ <span class="font-bold">{item().title}</span>
69
69
  <Show when={item().message}>
70
70
  <pre class={messageClass}>{item().message}</pre>
71
71
  </Show>
@@ -1,26 +1,18 @@
1
1
  import { type Component, createSignal, For, Show } from "solid-js";
2
2
  import { IconBell } from "@tabler/icons-solidjs";
3
3
  import clsx from "clsx";
4
+ import { twMerge } from "tailwind-merge";
4
5
  import { useNotification } from "./NotificationContext";
5
6
  import { Dropdown } from "../../disclosure/Dropdown";
6
7
  import { Icon } from "../../display/Icon";
7
8
  import { NotificationBanner } from "./NotificationBanner";
9
+ import { iconButtonBase } from "../../../styles/patterns.styles";
8
10
 
9
11
  export interface NotificationBellProps {
10
12
  showBanner?: boolean;
11
13
  }
12
14
 
13
- const buttonClass = clsx(
14
- "relative",
15
- "p-2",
16
- "rounded-full",
17
- "hover:bg-base-100",
18
- "dark:hover:bg-base-700",
19
- "transition-colors",
20
- "focus:outline-none",
21
- "focus-visible:ring-2",
22
- "focus-visible:ring-primary-500",
23
- );
15
+ const buttonClass = twMerge(iconButtonBase, "relative", "p-2", "rounded-full");
24
16
 
25
17
  const badgeClass = clsx(
26
18
  "absolute",
@@ -108,7 +100,7 @@ export const NotificationBell: Component<NotificationBellProps> = (props) => {
108
100
  >
109
101
  <div class="p-2">
110
102
  <div class={dropdownHeaderClass}>
111
- <span class="font-semibold">알림</span>
103
+ <span class="font-bold">알림</span>
112
104
  <Show when={notification.items().length > 0}>
113
105
  <button
114
106
  type="button"
@@ -125,6 +125,7 @@ export const NotificationProvider: ParentComponent = (props) => {
125
125
  const latest = latestUnread();
126
126
  if (latest) {
127
127
  setDismissedBannerId(latest.id);
128
+ markAsRead(latest.id);
128
129
  }
129
130
  };
130
131
 
@@ -1,69 +1,131 @@
1
- import { type ParentComponent, createEffect, splitProps } from "solid-js";
2
- import clsx from "clsx";
3
- import { twMerge } from "tailwind-merge";
1
+ import { type ParentComponent, children, createEffect, createSignal, onCleanup } from "solid-js";
4
2
  import "@simplysm/core-browser";
5
3
 
6
4
  export interface InvalidProps {
7
5
  /** Validation error message. Non-empty = invalid. */
8
6
  message?: string;
9
- /** Custom class */
10
- class?: string;
7
+ /** Visual indicator variant */
8
+ variant?: "border" | "dot";
9
+ /** When true, visual display only appears after target loses focus */
10
+ touchMode?: boolean;
11
11
  }
12
12
 
13
- const anchorClass = clsx(
14
- "relative inline-block",
15
- "size-0 align-top",
16
- "overflow-visible",
17
- "pointer-events-none select-none",
18
- );
13
+ export const Invalid: ParentComponent<InvalidProps> = (props) => {
14
+ const hiddenInputEl = document.createElement("input");
15
+ hiddenInputEl.type = "text";
16
+ hiddenInputEl.style.cssText =
17
+ "position:absolute; bottom:0; left:50%; width:1px; height:1px; opacity:0; pointer-events:none; z-index:-10;";
18
+ hiddenInputEl.autocomplete = "off";
19
+ hiddenInputEl.tabIndex = -1;
20
+ hiddenInputEl.setAttribute("aria-hidden", "true");
19
21
 
20
- const indicatorClass = clsx("absolute left-0.5 top-0.5", "size-1.5 rounded-full", "bg-danger-500");
22
+ const [touched, setTouched] = createSignal(false);
21
23
 
22
- const hiddenInputClass = clsx(
23
- "absolute bottom-0 left-0.5",
24
- "size-px opacity-0",
25
- "pointer-events-none -z-10",
26
- "select-none",
27
- );
24
+ const resolved = children(() => props.children);
28
25
 
29
- export const Invalid: ParentComponent<InvalidProps> = (props) => {
30
- const [local, rest] = splitProps(props, ["message", "class", "children"]);
26
+ // message 변경 setCustomValidity 반응형 업데이트 (touchMode 무관하게 항상)
27
+ createEffect(() => {
28
+ const msg = props.message ?? "";
29
+ hiddenInputEl.setCustomValidity(msg);
30
+ });
31
+
32
+ // target에 relative 설정 + hidden input을 target 내부에 삽입
33
+ createEffect(() => {
34
+ const targetEl = resolved.toArray().find((el): el is HTMLElement => el instanceof HTMLElement);
35
+ if (!targetEl) return;
36
+
37
+ const computedPosition = getComputedStyle(targetEl).position;
38
+ if (computedPosition === "static") {
39
+ targetEl.style.position = "relative";
40
+ }
41
+
42
+ targetEl.appendChild(hiddenInputEl);
31
43
 
32
- let hiddenInputEl!: HTMLInputElement;
44
+ onCleanup(() => {
45
+ if (hiddenInputEl.parentElement === targetEl) {
46
+ targetEl.removeChild(hiddenInputEl);
47
+ }
48
+ });
49
+ });
33
50
 
34
- // message 변경 시 setCustomValidity 반응형 업데이트
51
+ // 시각적 표시 처리
35
52
  createEffect(() => {
36
- const msg = local.message ?? "";
37
- hiddenInputEl.setCustomValidity(msg);
53
+ const variant = props.variant ?? "dot";
54
+ const message = props.message ?? "";
55
+ const touchMode = props.touchMode ?? false;
56
+ const isTouched = touched();
57
+
58
+ const targetEl = resolved.toArray().find((el): el is HTMLElement => el instanceof HTMLElement);
59
+
60
+ if (!targetEl) return;
61
+
62
+ const shouldShow = message !== "" && (!touchMode || isTouched);
63
+
64
+ if (variant === "border") {
65
+ if (shouldShow) {
66
+ targetEl.classList.add("border-danger-500");
67
+ } else {
68
+ targetEl.classList.remove("border-danger-500");
69
+ }
70
+
71
+ onCleanup(() => {
72
+ targetEl.classList.remove("border-danger-500");
73
+ });
74
+ } else {
75
+ // variant === "dot"
76
+ const existingDot = targetEl.querySelector("[data-invalid-dot]");
77
+ if (existingDot) {
78
+ existingDot.remove();
79
+ }
80
+
81
+ if (shouldShow) {
82
+ const dot = document.createElement("span");
83
+ dot.setAttribute("data-invalid-dot", "");
84
+ dot.style.cssText =
85
+ "position:absolute; top:2px; right:2px; width:6px; height:6px; border-radius:50%; background:red; pointer-events:none;";
86
+ targetEl.appendChild(dot);
87
+ }
88
+
89
+ onCleanup(() => {
90
+ const dot = targetEl.querySelector("[data-invalid-dot]");
91
+ if (dot) {
92
+ dot.remove();
93
+ }
94
+ });
95
+ }
38
96
  });
39
97
 
40
- const handleHiddenInputFocus = (e: FocusEvent) => {
41
- const container = (e.currentTarget as HTMLElement).parentElement;
42
- if (!container) return;
43
- const focusable = container.findFirstFocusableChild();
44
- if (focusable && focusable !== e.currentTarget) {
45
- focusable.focus();
98
+ // touchMode: target에 focusout 이벤트 등록하여 touched 상태 추적
99
+ createEffect(() => {
100
+ if (!(props.touchMode ?? false)) return;
101
+
102
+ const targetEl = resolved.toArray().find((el): el is HTMLElement => el instanceof HTMLElement);
103
+
104
+ if (!targetEl) return;
105
+
106
+ const handleFocusOut = () => {
107
+ setTouched(true);
108
+ };
109
+
110
+ targetEl.addEventListener("focusout", handleFocusOut);
111
+
112
+ onCleanup(() => {
113
+ targetEl.removeEventListener("focusout", handleFocusOut);
114
+ });
115
+ });
116
+
117
+ // hidden input 포커스 시 target의 focusable child로 리디렉션
118
+ hiddenInputEl.addEventListener("focus", () => {
119
+ const targetEl = resolved.toArray().find((el): el is HTMLElement => el instanceof HTMLElement);
120
+
121
+ if (targetEl) {
122
+ const focusable =
123
+ targetEl.findFirstFocusableChild() ?? (targetEl.tabIndex >= 0 ? targetEl : undefined);
124
+ if (focusable && focusable !== hiddenInputEl) {
125
+ focusable.focus();
126
+ }
46
127
  }
47
- };
48
-
49
- return (
50
- <div {...rest} class={twMerge("inline", local.class)}>
51
- <span class={anchorClass}>
52
- <span
53
- class={indicatorClass}
54
- style={{ display: (local.message ?? "") !== "" ? "block" : "none" }}
55
- />
56
- </span>
57
- {local.children}
58
- <input
59
- ref={hiddenInputEl}
60
- type="text"
61
- class={hiddenInputClass}
62
- autocomplete="off"
63
- tabIndex={-1}
64
- aria-hidden="true"
65
- onFocus={handleHiddenInputFocus}
66
- />
67
- </div>
68
- );
128
+ });
129
+
130
+ return <>{resolved()}</>;
69
131
  };
@@ -1,29 +1,16 @@
1
1
  import { type Component, type JSX, splitProps, Switch, Match } from "solid-js";
2
- import clsx from "clsx";
3
2
  import { twMerge } from "tailwind-merge";
4
3
  import { IconSun, IconMoon, IconDeviceDesktop } from "@tabler/icons-solidjs";
5
4
  import { useTheme, type ThemeMode } from "../../providers/ThemeContext";
6
5
  import { Icon } from "../display/Icon";
7
6
  import { ripple } from "../../directives/ripple";
7
+ import { iconButtonBase } from "../../styles/patterns.styles";
8
8
 
9
9
  void ripple;
10
10
 
11
- const baseClass = clsx(
12
- "inline-flex",
13
- "items-center",
14
- "justify-center",
15
- "cursor-pointer",
16
- "rounded",
17
- "transition-colors",
18
- "text-base-500 dark:text-base-400",
19
- "hover:bg-base-200 dark:hover:bg-base-700",
20
- "focus:outline-none",
21
- "focus-visible:ring-2",
22
- );
23
-
24
11
  const sizeClasses: Record<"sm" | "lg", string> = {
25
- sm: clsx("p-1"),
26
- lg: clsx("p-2"),
12
+ sm: "p-1",
13
+ lg: "p-2",
27
14
  };
28
15
 
29
16
  const iconSizes: Record<"sm" | "lg", string> = {
@@ -69,7 +56,7 @@ export const ThemeToggle: Component<ThemeToggleProps> = (props) => {
69
56
  const { mode, cycleMode } = useTheme();
70
57
 
71
58
  const getClassName = () =>
72
- twMerge(baseClass, "p-1.5", local.size && sizeClasses[local.size], local.class);
59
+ twMerge(iconButtonBase, "p-1.5", local.size && sizeClasses[local.size], local.class);
73
60
 
74
61
  const iconSize = () => (local.size ? iconSizes[local.size] : "1.25em");
75
62
 
@@ -10,7 +10,6 @@ import {
10
10
  } from "../../../styles/tokens.styles";
11
11
  import { insetBase, insetFocusOutlineSelf } from "../../../styles/patterns.styles";
12
12
 
13
- export type CheckboxTheme = "primary" | "info" | "success" | "warning" | "danger";
14
13
  export type CheckboxSize = ComponentSize;
15
14
 
16
15
  // wrapper 기본 스타일
@@ -36,14 +35,8 @@ export const indicatorBaseClass = clsx(
36
35
  "transition-colors",
37
36
  );
38
37
 
39
- // 테마별 체크 상태
40
- export const themeCheckedClasses: Record<CheckboxTheme, string> = {
41
- primary: clsx("border-primary-500 bg-primary-500", "text-white"),
42
- info: clsx("border-info-500 bg-info-500", "text-white"),
43
- success: clsx("border-success-500 bg-success-500", "text-white"),
44
- warning: clsx("border-warning-500 bg-warning-500", "text-white"),
45
- danger: clsx("border-danger-500 bg-danger-500", "text-white"),
46
- };
38
+ // 체크 상태 스타일 (primary 고정)
39
+ export const checkedClass = clsx("border-primary-500 bg-primary-500", "text-white");
47
40
 
48
41
  // 사이즈별 스타일
49
42
  export const checkboxSizeClasses: Record<CheckboxSize, string> = {
@@ -1,21 +1,21 @@
1
- import { type JSX, type ParentComponent, Show, splitProps } from "solid-js";
1
+ import { type JSX, type ParentComponent, Show, splitProps, createMemo } from "solid-js";
2
2
  import { twMerge } from "tailwind-merge";
3
3
  import { IconCheck } from "@tabler/icons-solidjs";
4
4
  import { createControllableSignal } from "../../../hooks/createControllableSignal";
5
5
  import { ripple } from "../../../directives/ripple";
6
6
  import { Icon } from "../../display/Icon";
7
7
  import {
8
- type CheckboxTheme,
9
8
  type CheckboxSize,
10
9
  checkboxBaseClass,
11
10
  indicatorBaseClass,
12
- themeCheckedClasses,
11
+ checkedClass,
13
12
  checkboxSizeClasses,
14
13
  checkboxInsetClass,
15
14
  checkboxInsetSizeHeightClasses,
16
15
  checkboxInlineClass,
17
16
  checkboxDisabledClass,
18
17
  } from "./Checkbox.styles";
18
+ import { Invalid } from "../Invalid";
19
19
 
20
20
  // Directive 사용 선언 (TypeScript용)
21
21
  void ripple;
@@ -25,9 +25,11 @@ export interface CheckboxProps {
25
25
  onValueChange?: (value: boolean) => void;
26
26
  disabled?: boolean;
27
27
  size?: CheckboxSize;
28
- theme?: CheckboxTheme;
29
28
  inset?: boolean;
30
29
  inline?: boolean;
30
+ required?: boolean;
31
+ validate?: (value: boolean) => string | undefined;
32
+ touchMode?: boolean;
31
33
  class?: string;
32
34
  style?: JSX.CSSProperties;
33
35
  children?: JSX.Element;
@@ -39,9 +41,11 @@ export const Checkbox: ParentComponent<CheckboxProps> = (props) => {
39
41
  "onValueChange",
40
42
  "disabled",
41
43
  "size",
42
- "theme",
43
44
  "inset",
44
45
  "inline",
46
+ "required",
47
+ "validate",
48
+ "touchMode",
45
49
  "class",
46
50
  "style",
47
51
  "children",
@@ -75,32 +79,39 @@ export const Checkbox: ParentComponent<CheckboxProps> = (props) => {
75
79
  local.class,
76
80
  );
77
81
 
78
- const getIndicatorClass = () => {
79
- const theme = local.theme ?? "primary";
82
+ const getIndicatorClass = () =>
83
+ twMerge(indicatorBaseClass, "rounded-sm", value() && checkedClass);
80
84
 
81
- return twMerge(indicatorBaseClass, "rounded-sm", value() && themeCheckedClasses[theme]);
82
- };
85
+ const errorMsg = createMemo(() => {
86
+ const v = local.value ?? false;
87
+ if (local.required && !v) return "필수 선택 항목입니다";
88
+ return local.validate?.(v);
89
+ });
83
90
 
84
91
  return (
85
- <label
86
- {...rest}
87
- use:ripple={!local.disabled}
88
- role="checkbox"
89
- aria-checked={value()}
90
- tabIndex={local.disabled ? -1 : 0}
91
- class={getWrapperClass()}
92
- style={local.style}
93
- onClick={handleClick}
94
- onKeyDown={handleKeyDown}
95
- >
96
- <div class={getIndicatorClass()}>
97
- <Show when={value()}>
98
- <Icon icon={IconCheck} size="1em" />
99
- </Show>
92
+ <Invalid message={errorMsg()} variant="border" touchMode={local.touchMode}>
93
+ <div class="inline-flex">
94
+ <label
95
+ {...rest}
96
+ use:ripple={!local.disabled}
97
+ role="checkbox"
98
+ aria-checked={value()}
99
+ tabIndex={local.disabled ? -1 : 0}
100
+ class={getWrapperClass()}
101
+ style={local.style}
102
+ onClick={handleClick}
103
+ onKeyDown={handleKeyDown}
104
+ >
105
+ <div class={getIndicatorClass()}>
106
+ <Show when={value()}>
107
+ <Icon icon={IconCheck} size="1em" />
108
+ </Show>
109
+ </div>
110
+ <Show when={local.children}>
111
+ <span>{local.children}</span>
112
+ </Show>
113
+ </label>
100
114
  </div>
101
- <Show when={local.children}>
102
- <span>{local.children}</span>
103
- </Show>
104
- </label>
115
+ </Invalid>
105
116
  );
106
117
  };
@@ -1,60 +1,18 @@
1
- import { type JSX, type ParentComponent, createContext, splitProps, useContext } from "solid-js";
2
- import { twMerge } from "tailwind-merge";
3
- import { createControllableSignal } from "../../../hooks/createControllableSignal";
1
+ import { type JSX } from "solid-js";
4
2
  import { Checkbox } from "./Checkbox";
5
- import type { CheckboxSize, CheckboxTheme } from "./Checkbox.styles";
6
-
7
- interface CheckboxGroupContextValue<TValue> {
8
- value: () => TValue[];
9
- toggle: (item: TValue) => void;
10
- disabled: () => boolean;
11
- size: () => CheckboxSize | undefined;
12
- theme: () => CheckboxTheme | undefined;
13
- inline: () => boolean;
14
- inset: () => boolean;
15
- }
16
-
17
- const CheckboxGroupContext = createContext<CheckboxGroupContextValue<any>>();
18
-
19
- // --- CheckboxGroup.Item ---
20
-
21
- interface CheckboxGroupItemProps<TValue> {
22
- value: TValue;
23
- disabled?: boolean;
24
- children?: JSX.Element;
25
- }
26
-
27
- function CheckboxGroupItemInner<TValue>(props: CheckboxGroupItemProps<TValue>) {
28
- const ctx = useContext(CheckboxGroupContext);
29
- if (!ctx) throw new Error("CheckboxGroup.Item은 CheckboxGroup 내부에서만 사용할 수 있습니다");
30
-
31
- const isSelected = () => ctx.value().includes(props.value);
32
-
33
- return (
34
- <Checkbox
35
- value={isSelected()}
36
- onValueChange={() => ctx.toggle(props.value)}
37
- disabled={props.disabled ?? ctx.disabled()}
38
- size={ctx.size()}
39
- theme={ctx.theme()}
40
- inline={ctx.inline()}
41
- inset={ctx.inset()}
42
- >
43
- {props.children}
44
- </Checkbox>
45
- );
46
- }
47
-
48
- // --- CheckboxGroup ---
3
+ import { createSelectionGroup } from "../../../hooks/createSelectionGroup";
4
+ import type { CheckboxSize } from "./Checkbox.styles";
49
5
 
50
6
  interface CheckboxGroupProps<TValue> {
51
7
  value?: TValue[];
52
8
  onValueChange?: (value: TValue[]) => void;
53
9
  disabled?: boolean;
54
10
  size?: CheckboxSize;
55
- theme?: CheckboxTheme;
56
11
  inline?: boolean;
57
12
  inset?: boolean;
13
+ required?: boolean;
14
+ validate?: (value: TValue[]) => string | undefined;
15
+ touchMode?: boolean;
58
16
  class?: string;
59
17
  style?: JSX.CSSProperties;
60
18
  children?: JSX.Element;
@@ -62,55 +20,18 @@ interface CheckboxGroupProps<TValue> {
62
20
 
63
21
  interface CheckboxGroupComponent {
64
22
  <TValue = unknown>(props: CheckboxGroupProps<TValue>): JSX.Element;
65
- Item: typeof CheckboxGroupItemInner;
23
+ Item: <TValue = unknown>(props: {
24
+ value: TValue;
25
+ disabled?: boolean;
26
+ children?: JSX.Element;
27
+ }) => JSX.Element;
66
28
  }
67
29
 
68
- const CheckboxGroupInner: ParentComponent<CheckboxGroupProps<unknown>> = (props) => {
69
- const [local, rest] = splitProps(props, [
70
- "value",
71
- "onValueChange",
72
- "disabled",
73
- "size",
74
- "theme",
75
- "inline",
76
- "inset",
77
- "class",
78
- "style",
79
- "children",
80
- ]);
81
-
82
- const [value, setValue] = createControllableSignal({
83
- value: () => local.value ?? [],
84
- onChange: () => local.onValueChange,
85
- });
86
-
87
- const toggle = (item: unknown) => {
88
- setValue((prev) => {
89
- if (prev.includes(item)) {
90
- return prev.filter((v) => v !== item);
91
- }
92
- return [...prev, item];
93
- });
94
- };
95
-
96
- const contextValue: CheckboxGroupContextValue<unknown> = {
97
- value,
98
- toggle,
99
- disabled: () => local.disabled ?? false,
100
- size: () => local.size,
101
- theme: () => local.theme,
102
- inline: () => local.inline ?? false,
103
- inset: () => local.inset ?? false,
104
- };
105
-
106
- return (
107
- <CheckboxGroupContext.Provider value={contextValue}>
108
- <div {...rest} class={twMerge("inline-flex", local.class)} style={local.style}>
109
- {local.children}
110
- </div>
111
- </CheckboxGroupContext.Provider>
112
- );
113
- };
30
+ const { Group } = createSelectionGroup({
31
+ mode: "multiple",
32
+ contextName: "CheckboxGroup",
33
+ ItemComponent: Checkbox,
34
+ emptyErrorMsg: "항목을 선택해 주세요",
35
+ });
114
36
 
115
- export const CheckboxGroup = CheckboxGroupInner as unknown as CheckboxGroupComponent;
116
- CheckboxGroup.Item = CheckboxGroupItemInner;
37
+ export const CheckboxGroup = Group as unknown as CheckboxGroupComponent;