@simplysm/solid 13.0.62 → 13.0.65

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 (157) hide show
  1. package/README.md +6 -0
  2. package/dist/components/data/sheet/DataSheet.d.ts.map +1 -1
  3. package/dist/components/data/sheet/DataSheet.js +3 -2
  4. package/dist/components/data/sheet/DataSheet.js.map +2 -2
  5. package/dist/components/features/address/AddressSearch.d.ts +8 -0
  6. package/dist/components/features/address/AddressSearch.d.ts.map +1 -0
  7. package/dist/components/features/address/AddressSearch.js +72 -0
  8. package/dist/components/features/address/AddressSearch.js.map +6 -0
  9. package/dist/components/features/crud-detail/CrudDetail.d.ts.map +1 -0
  10. package/dist/components/{data → features}/crud-detail/CrudDetail.js +62 -41
  11. package/dist/components/features/crud-detail/CrudDetail.js.map +6 -0
  12. package/dist/components/features/crud-detail/CrudDetailAfter.d.ts.map +1 -0
  13. package/dist/components/{data → features}/crud-detail/CrudDetailAfter.js.map +1 -1
  14. package/dist/components/features/crud-detail/CrudDetailBefore.d.ts.map +1 -0
  15. package/dist/components/{data → features}/crud-detail/CrudDetailBefore.js.map +1 -1
  16. package/dist/components/features/crud-detail/CrudDetailTools.d.ts.map +1 -0
  17. package/dist/components/{data → features}/crud-detail/CrudDetailTools.js.map +1 -1
  18. package/dist/components/features/crud-detail/types.d.ts.map +1 -0
  19. package/dist/components/features/crud-sheet/CrudSheet.d.ts.map +1 -0
  20. package/dist/components/{data → features}/crud-sheet/CrudSheet.js +166 -21
  21. package/dist/components/features/crud-sheet/CrudSheet.js.map +6 -0
  22. package/dist/components/features/crud-sheet/CrudSheetColumn.d.ts.map +1 -0
  23. package/dist/components/{data → features}/crud-sheet/CrudSheetColumn.js +1 -1
  24. package/dist/components/{data → features}/crud-sheet/CrudSheetColumn.js.map +1 -1
  25. package/dist/components/features/crud-sheet/CrudSheetFilter.d.ts.map +1 -0
  26. package/dist/components/{data → features}/crud-sheet/CrudSheetFilter.js.map +1 -1
  27. package/dist/components/features/crud-sheet/CrudSheetHeader.d.ts.map +1 -0
  28. package/dist/components/{data → features}/crud-sheet/CrudSheetHeader.js.map +1 -1
  29. package/dist/components/features/crud-sheet/CrudSheetTools.d.ts.map +1 -0
  30. package/dist/components/{data → features}/crud-sheet/CrudSheetTools.js.map +1 -1
  31. package/dist/components/{data → features}/crud-sheet/types.d.ts +10 -4
  32. package/dist/components/features/crud-sheet/types.d.ts.map +1 -0
  33. package/dist/components/features/data-select-button/DataSelectButton.d.ts +38 -0
  34. package/dist/components/features/data-select-button/DataSelectButton.d.ts.map +1 -0
  35. package/dist/components/features/data-select-button/DataSelectButton.js +184 -0
  36. package/dist/components/features/data-select-button/DataSelectButton.js.map +6 -0
  37. package/dist/components/features/permission-table/PermissionTable.d.ts.map +1 -0
  38. package/dist/components/{data → features}/permission-table/PermissionTable.js +1 -1
  39. package/dist/components/{data → features}/permission-table/PermissionTable.js.map +1 -1
  40. package/dist/components/features/shared-data/SharedDataSelect.d.ts +32 -0
  41. package/dist/components/features/shared-data/SharedDataSelect.d.ts.map +1 -0
  42. package/dist/components/features/shared-data/SharedDataSelect.js +74 -0
  43. package/dist/components/features/shared-data/SharedDataSelect.js.map +6 -0
  44. package/dist/components/features/shared-data/SharedDataSelectButton.d.ts +29 -0
  45. package/dist/components/features/shared-data/SharedDataSelectButton.d.ts.map +1 -0
  46. package/dist/components/features/shared-data/SharedDataSelectButton.js +17 -0
  47. package/dist/components/features/shared-data/SharedDataSelectButton.js.map +6 -0
  48. package/dist/components/features/shared-data/SharedDataSelectList.d.ts +29 -0
  49. package/dist/components/features/shared-data/SharedDataSelectList.d.ts.map +1 -0
  50. package/dist/components/features/shared-data/SharedDataSelectList.js +80 -0
  51. package/dist/components/features/shared-data/SharedDataSelectList.js.map +6 -0
  52. package/dist/components/form-control/checkbox/Checkbox.d.ts.map +1 -1
  53. package/dist/components/form-control/checkbox/Checkbox.js +10 -10
  54. package/dist/components/form-control/checkbox/Checkbox.js.map +2 -2
  55. package/dist/components/form-control/checkbox/Checkbox.styles.d.ts.map +1 -1
  56. package/dist/components/form-control/checkbox/Checkbox.styles.js +2 -2
  57. package/dist/components/form-control/checkbox/Checkbox.styles.js.map +1 -1
  58. package/dist/components/form-control/checkbox/Radio.d.ts.map +1 -1
  59. package/dist/components/form-control/checkbox/Radio.js +13 -13
  60. package/dist/components/form-control/checkbox/Radio.js.map +2 -2
  61. package/dist/components/form-control/select/Select.d.ts +7 -3
  62. package/dist/components/form-control/select/Select.d.ts.map +1 -1
  63. package/dist/components/form-control/select/Select.js +146 -45
  64. package/dist/components/form-control/select/Select.js.map +2 -2
  65. package/dist/components/form-control/select-list/SelectList.d.ts +54 -0
  66. package/dist/components/form-control/select-list/SelectList.d.ts.map +1 -0
  67. package/dist/components/form-control/select-list/SelectList.js +280 -0
  68. package/dist/components/form-control/select-list/SelectList.js.map +6 -0
  69. package/dist/components/form-control/select-list/SelectListContext.d.ts +13 -0
  70. package/dist/components/form-control/select-list/SelectListContext.d.ts.map +1 -0
  71. package/dist/components/form-control/select-list/SelectListContext.js +14 -0
  72. package/dist/components/form-control/select-list/SelectListContext.js.map +6 -0
  73. package/dist/index.d.ts +11 -5
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +11 -5
  76. package/dist/index.js.map +1 -1
  77. package/dist/providers/ServiceClientContext.d.ts +5 -5
  78. package/dist/providers/ServiceClientContext.d.ts.map +1 -1
  79. package/dist/providers/ServiceClientProvider.d.ts.map +1 -1
  80. package/dist/providers/ServiceClientProvider.js +12 -8
  81. package/dist/providers/ServiceClientProvider.js.map +2 -2
  82. package/dist/providers/shared-data/SharedDataContext.d.ts +16 -2
  83. package/dist/providers/shared-data/SharedDataContext.d.ts.map +1 -1
  84. package/dist/providers/shared-data/SharedDataContext.js.map +1 -1
  85. package/dist/providers/shared-data/SharedDataProvider.d.ts +1 -2
  86. package/dist/providers/shared-data/SharedDataProvider.d.ts.map +1 -1
  87. package/dist/providers/shared-data/SharedDataProvider.js +27 -13
  88. package/dist/providers/shared-data/SharedDataProvider.js.map +2 -2
  89. package/docs/data-components.md +15 -4
  90. package/docs/form-controls.md +257 -0
  91. package/docs/hooks.md +30 -0
  92. package/docs/providers.md +7 -0
  93. package/package.json +5 -3
  94. package/src/components/data/sheet/DataSheet.tsx +6 -7
  95. package/src/components/features/address/AddressSearch.tsx +75 -0
  96. package/src/components/{data → features}/crud-detail/CrudDetail.tsx +51 -26
  97. package/src/components/{data → features}/crud-sheet/CrudSheet.tsx +160 -23
  98. package/src/components/{data → features}/crud-sheet/CrudSheetColumn.tsx +1 -1
  99. package/src/components/{data → features}/crud-sheet/types.ts +14 -4
  100. package/src/components/features/data-select-button/DataSelectButton.tsx +279 -0
  101. package/src/components/{data → features}/permission-table/PermissionTable.tsx +1 -1
  102. package/src/components/features/shared-data/SharedDataSelect.tsx +101 -0
  103. package/src/components/features/shared-data/SharedDataSelectButton.tsx +47 -0
  104. package/src/components/features/shared-data/SharedDataSelectList.tsx +85 -0
  105. package/src/components/form-control/checkbox/Checkbox.styles.ts +2 -2
  106. package/src/components/form-control/checkbox/Checkbox.tsx +18 -20
  107. package/src/components/form-control/checkbox/Radio.tsx +18 -20
  108. package/src/components/form-control/select/Select.tsx +192 -36
  109. package/src/components/form-control/select-list/SelectList.tsx +385 -0
  110. package/src/components/form-control/select-list/SelectListContext.ts +23 -0
  111. package/src/index.ts +29 -5
  112. package/src/providers/ServiceClientContext.ts +5 -5
  113. package/src/providers/ServiceClientProvider.tsx +17 -12
  114. package/src/providers/shared-data/SharedDataContext.ts +16 -2
  115. package/src/providers/shared-data/SharedDataProvider.tsx +33 -17
  116. package/dist/components/data/crud-detail/CrudDetail.d.ts.map +0 -1
  117. package/dist/components/data/crud-detail/CrudDetail.js.map +0 -6
  118. package/dist/components/data/crud-detail/CrudDetailAfter.d.ts.map +0 -1
  119. package/dist/components/data/crud-detail/CrudDetailBefore.d.ts.map +0 -1
  120. package/dist/components/data/crud-detail/CrudDetailTools.d.ts.map +0 -1
  121. package/dist/components/data/crud-detail/types.d.ts.map +0 -1
  122. package/dist/components/data/crud-sheet/CrudSheet.d.ts.map +0 -1
  123. package/dist/components/data/crud-sheet/CrudSheet.js.map +0 -6
  124. package/dist/components/data/crud-sheet/CrudSheetColumn.d.ts.map +0 -1
  125. package/dist/components/data/crud-sheet/CrudSheetFilter.d.ts.map +0 -1
  126. package/dist/components/data/crud-sheet/CrudSheetHeader.d.ts.map +0 -1
  127. package/dist/components/data/crud-sheet/CrudSheetTools.d.ts.map +0 -1
  128. package/dist/components/data/crud-sheet/types.d.ts.map +0 -1
  129. package/dist/components/data/permission-table/PermissionTable.d.ts.map +0 -1
  130. /package/dist/components/{data → features}/crud-detail/CrudDetail.d.ts +0 -0
  131. /package/dist/components/{data → features}/crud-detail/CrudDetailAfter.d.ts +0 -0
  132. /package/dist/components/{data → features}/crud-detail/CrudDetailAfter.js +0 -0
  133. /package/dist/components/{data → features}/crud-detail/CrudDetailBefore.d.ts +0 -0
  134. /package/dist/components/{data → features}/crud-detail/CrudDetailBefore.js +0 -0
  135. /package/dist/components/{data → features}/crud-detail/CrudDetailTools.d.ts +0 -0
  136. /package/dist/components/{data → features}/crud-detail/CrudDetailTools.js +0 -0
  137. /package/dist/components/{data → features}/crud-detail/types.d.ts +0 -0
  138. /package/dist/components/{data → features}/crud-detail/types.js +0 -0
  139. /package/dist/components/{data → features}/crud-detail/types.js.map +0 -0
  140. /package/dist/components/{data → features}/crud-sheet/CrudSheet.d.ts +0 -0
  141. /package/dist/components/{data → features}/crud-sheet/CrudSheetColumn.d.ts +0 -0
  142. /package/dist/components/{data → features}/crud-sheet/CrudSheetFilter.d.ts +0 -0
  143. /package/dist/components/{data → features}/crud-sheet/CrudSheetFilter.js +0 -0
  144. /package/dist/components/{data → features}/crud-sheet/CrudSheetHeader.d.ts +0 -0
  145. /package/dist/components/{data → features}/crud-sheet/CrudSheetHeader.js +0 -0
  146. /package/dist/components/{data → features}/crud-sheet/CrudSheetTools.d.ts +0 -0
  147. /package/dist/components/{data → features}/crud-sheet/CrudSheetTools.js +0 -0
  148. /package/dist/components/{data → features}/crud-sheet/types.js +0 -0
  149. /package/dist/components/{data → features}/crud-sheet/types.js.map +0 -0
  150. /package/dist/components/{data → features}/permission-table/PermissionTable.d.ts +0 -0
  151. /package/src/components/{data → features}/crud-detail/CrudDetailAfter.tsx +0 -0
  152. /package/src/components/{data → features}/crud-detail/CrudDetailBefore.tsx +0 -0
  153. /package/src/components/{data → features}/crud-detail/CrudDetailTools.tsx +0 -0
  154. /package/src/components/{data → features}/crud-detail/types.ts +0 -0
  155. /package/src/components/{data → features}/crud-sheet/CrudSheetFilter.tsx +0 -0
  156. /package/src/components/{data → features}/crud-sheet/CrudSheetHeader.tsx +0 -0
  157. /package/src/components/{data → features}/crud-sheet/CrudSheetTools.tsx +0 -0
@@ -0,0 +1,279 @@
1
+ import {
2
+ createEffect,
3
+ createMemo,
4
+ createResource,
5
+ createSignal,
6
+ For,
7
+ type JSX,
8
+ on,
9
+ Show,
10
+ splitProps,
11
+ } from "solid-js";
12
+ import clsx from "clsx";
13
+ import { twMerge } from "tailwind-merge";
14
+ import { IconSearch, IconX } from "@tabler/icons-solidjs";
15
+ import { Icon } from "../../display/Icon";
16
+ import { Invalid } from "../../form-control/Invalid";
17
+ import { useDialog, type DialogShowOptions } from "../../disclosure/DialogContext";
18
+ import { createControllableSignal } from "../../../hooks/createControllableSignal";
19
+ import { type ComponentSize, textMuted } from "../../../styles/tokens.styles";
20
+ import {
21
+ triggerBaseClass,
22
+ triggerDisabledClass,
23
+ triggerInsetClass,
24
+ triggerSizeClasses,
25
+ } from "../../form-control/DropdownTrigger.styles";
26
+
27
+ /** 모달에서 반환하는 결과 인터페이스 */
28
+ export interface DataSelectModalResult<TKey> {
29
+ selectedKeys: TKey[];
30
+ }
31
+
32
+ /** DataSelectButton Props */
33
+ export interface DataSelectButtonProps<TItem, TKey = string | number> {
34
+ /** 현재 선택된 키 (단일 또는 다중) */
35
+ value?: TKey | TKey[];
36
+ /** 값 변경 콜백 */
37
+ onValueChange?: (value: TKey | TKey[] | undefined) => void;
38
+
39
+ /** 키로 아이템을 로드하는 함수 */
40
+ load: (keys: TKey[]) => TItem[] | Promise<TItem[]>;
41
+ /** 선택 모달 컴포넌트 팩토리 */
42
+ modal: () => JSX.Element;
43
+ /** 아이템 렌더링 함수 */
44
+ renderItem: (item: TItem) => JSX.Element;
45
+
46
+ /** 다중 선택 모드 */
47
+ multiple?: boolean;
48
+ /** 필수 입력 */
49
+ required?: boolean;
50
+ /** 비활성화 */
51
+ disabled?: boolean;
52
+ /** 트리거 크기 */
53
+ size?: ComponentSize;
54
+ /** 테두리 없는 스타일 */
55
+ inset?: boolean;
56
+
57
+ /** 커스텀 유효성 검사 함수 */
58
+ validate?: (value: unknown) => string | undefined;
59
+ /** touchMode: 포커스 해제 후에만 에러 표시 */
60
+ touchMode?: boolean;
61
+
62
+ /** 다이얼로그 옵션 */
63
+ dialogOptions?: DialogShowOptions;
64
+ }
65
+
66
+ // 스타일
67
+ const containerClass = clsx("inline-flex items-center", "group");
68
+ const selectedValueClass = clsx("flex-1", "whitespace-nowrap", "overflow-hidden", "text-ellipsis");
69
+ const actionButtonClass = clsx(
70
+ "flex-shrink-0",
71
+ "p-0.5",
72
+ "rounded",
73
+ "cursor-pointer",
74
+ "transition-colors",
75
+ "hover:bg-base-200 dark:hover:bg-base-700",
76
+ "focus:outline-none",
77
+ );
78
+
79
+ function getTriggerContainerClass(options: {
80
+ size?: ComponentSize;
81
+ disabled?: boolean;
82
+ inset?: boolean;
83
+ class?: string;
84
+ }): string {
85
+ return twMerge(
86
+ triggerBaseClass,
87
+ "px-2 py-1",
88
+ options.size && triggerSizeClasses[options.size],
89
+ options.disabled && triggerDisabledClass,
90
+ options.inset && triggerInsetClass,
91
+ options.class,
92
+ );
93
+ }
94
+
95
+ export function DataSelectButton<TItem, TKey = string | number>(
96
+ props: DataSelectButtonProps<TItem, TKey>,
97
+ ): JSX.Element {
98
+ const [local] = splitProps(props, [
99
+ "value",
100
+ "onValueChange",
101
+ "load",
102
+ "modal",
103
+ "renderItem",
104
+ "multiple",
105
+ "required",
106
+ "disabled",
107
+ "size",
108
+ "inset",
109
+ "validate",
110
+ "touchMode",
111
+ "dialogOptions",
112
+ ]);
113
+
114
+ const dialog = useDialog();
115
+
116
+ // value를 항상 배열로 정규화
117
+ const normalizeKeys = (value: TKey | TKey[] | undefined): TKey[] => {
118
+ if (value === undefined || value === null) return [];
119
+ if (Array.isArray(value)) return value;
120
+ return [value];
121
+ };
122
+
123
+ // controlled/uncontrolled 패턴
124
+ type ValueType = TKey | TKey[] | undefined;
125
+ const [getValue, setValue] = createControllableSignal<ValueType>({
126
+ value: () => local.value,
127
+ onChange: () => local.onValueChange as ((v: ValueType) => void) | undefined,
128
+ } as Parameters<typeof createControllableSignal<ValueType>>[0]);
129
+
130
+ // load를 위한 키 추적 signal
131
+ // eslint-disable-next-line solid/reactivity -- 초기값은 mount 시점에 한 번만 읽음
132
+ const [loadKeys, setLoadKeys] = createSignal<TKey[]>(normalizeKeys(local.value));
133
+
134
+ // value가 변경되면 loadKeys 업데이트
135
+ createEffect(
136
+ on(
137
+ () => getValue(),
138
+ (value) => {
139
+ setLoadKeys(normalizeKeys(value));
140
+ },
141
+ ),
142
+ );
143
+
144
+ // createResource로 load 호출
145
+ // eslint-disable-next-line solid/reactivity -- createResource의 fetcher는 source 변경 시 호출됨
146
+ const [selectedItems] = createResource(loadKeys, async (keys) => {
147
+ if (keys.length === 0) return [];
148
+ return Promise.resolve(local.load(keys));
149
+ });
150
+
151
+ // 값이 있는지 확인
152
+ const hasValue = createMemo(() => {
153
+ const keys = normalizeKeys(getValue());
154
+ return keys.length > 0;
155
+ });
156
+
157
+ // 지우기 가능 여부
158
+ const clearable = createMemo(() => !local.required && hasValue() && !local.disabled);
159
+
160
+ // 유효성 검사
161
+ const errorMsg = createMemo(() => {
162
+ const v = getValue();
163
+ if (local.required) {
164
+ const keys = normalizeKeys(v);
165
+ if (keys.length === 0) return "필수 입력 항목입니다";
166
+ }
167
+ return local.validate?.(v);
168
+ });
169
+
170
+ // 모달 열기
171
+ const handleOpenModal = async () => {
172
+ if (local.disabled) return;
173
+
174
+ const result = await dialog.show<DataSelectModalResult<TKey>>(
175
+ local.modal,
176
+ local.dialogOptions ?? {},
177
+ );
178
+
179
+ if (result) {
180
+ const newKeys = result.selectedKeys;
181
+ if (local.multiple) {
182
+ setValue(newKeys);
183
+ } else {
184
+ setValue(newKeys.length > 0 ? newKeys[0] : undefined);
185
+ }
186
+ }
187
+ };
188
+
189
+ // 지우기
190
+ const handleClear = (e: MouseEvent) => {
191
+ e.stopPropagation();
192
+ if (local.multiple) {
193
+ setValue([] as unknown as TKey[]);
194
+ } else {
195
+ setValue(undefined);
196
+ }
197
+ };
198
+
199
+ // 선택된 값 표시
200
+ const renderSelectedDisplay = (): JSX.Element => {
201
+ const items = selectedItems();
202
+ if (!items || items.length === 0) {
203
+ return <span class={textMuted} />;
204
+ }
205
+ return (
206
+ <span class="flex items-center gap-1">
207
+ <For each={items}>
208
+ {(item, index) => (
209
+ <>
210
+ <Show when={index() > 0}>
211
+ <span class={textMuted}>,</span>
212
+ </Show>
213
+ {local.renderItem(item)}
214
+ </>
215
+ )}
216
+ </For>
217
+ </span>
218
+ );
219
+ };
220
+
221
+ // 트리거 클래스 계산
222
+ const triggerClassName = () =>
223
+ getTriggerContainerClass({
224
+ size: local.size,
225
+ disabled: local.disabled,
226
+ inset: local.inset,
227
+ });
228
+
229
+ return (
230
+ <Invalid message={errorMsg()} variant="border" touchMode={local.touchMode}>
231
+ <div data-data-select-button class={containerClass}>
232
+ <div
233
+ role="combobox"
234
+ aria-haspopup="dialog"
235
+ aria-expanded={false}
236
+ aria-disabled={local.disabled || undefined}
237
+ aria-required={local.required || undefined}
238
+ tabIndex={local.disabled ? -1 : 0}
239
+ class={triggerClassName()}
240
+ onKeyDown={(e) => {
241
+ if (local.disabled) return;
242
+ if (e.key === "Enter" || e.key === " ") {
243
+ e.preventDefault();
244
+ void handleOpenModal();
245
+ }
246
+ }}
247
+ >
248
+ <div class={selectedValueClass}>{renderSelectedDisplay()}</div>
249
+ <div class="flex items-center gap-0.5">
250
+ <Show when={clearable()}>
251
+ <button
252
+ type="button"
253
+ data-clear-button
254
+ class={twMerge(actionButtonClass, "text-base-400 hover:text-danger-500")}
255
+ onClick={handleClear}
256
+ tabIndex={-1}
257
+ aria-label="선택 해제"
258
+ >
259
+ <Icon icon={IconX} size="0.875em" />
260
+ </button>
261
+ </Show>
262
+ <Show when={!local.disabled}>
263
+ <button
264
+ type="button"
265
+ data-search-button
266
+ class={twMerge(actionButtonClass, "text-base-400 hover:text-primary-500")}
267
+ onClick={() => void handleOpenModal()}
268
+ tabIndex={-1}
269
+ aria-label="검색"
270
+ >
271
+ <Icon icon={IconSearch} size="0.875em" />
272
+ </button>
273
+ </Show>
274
+ </div>
275
+ </div>
276
+ </div>
277
+ </Invalid>
278
+ );
279
+ }
@@ -11,7 +11,7 @@ import {
11
11
  } from "solid-js";
12
12
  import clsx from "clsx";
13
13
  import { twMerge } from "tailwind-merge";
14
- import { DataSheet } from "../sheet/DataSheet";
14
+ import { DataSheet } from "../../data/sheet/DataSheet";
15
15
  import { Checkbox } from "../../form-control/checkbox/Checkbox";
16
16
  import { borderDefault } from "../../../styles/tokens.styles";
17
17
  import type { AppPerm } from "../../../helpers/createAppStructure";
@@ -0,0 +1,101 @@
1
+ import { createMemo, type JSX, mergeProps, splitProps } from "solid-js";
2
+ import { IconEdit, IconSearch } from "@tabler/icons-solidjs";
3
+ import { type SharedDataAccessor } from "../../../providers/shared-data/SharedDataContext";
4
+ import { Select, type SelectProps } from "../../form-control/select/Select";
5
+ import { Icon } from "../../display/Icon";
6
+ import { useDialog } from "../../disclosure/DialogContext";
7
+ import { type ComponentSize } from "../../../styles/tokens.styles";
8
+
9
+ /** SharedDataSelect Props */
10
+ export interface SharedDataSelectProps<TItem> {
11
+ /** 공유 데이터 접근자 */
12
+ data: SharedDataAccessor<TItem>;
13
+
14
+ /** 현재 선택된 값 */
15
+ value?: unknown;
16
+ /** 값 변경 콜백 */
17
+ onValueChange?: (value: unknown) => void;
18
+ /** 다중 선택 모드 */
19
+ multiple?: boolean;
20
+ /** 필수 입력 */
21
+ required?: boolean;
22
+ /** 비활성화 */
23
+ disabled?: boolean;
24
+ /** 트리거 크기 */
25
+ size?: ComponentSize;
26
+ /** 테두리 없는 스타일 */
27
+ inset?: boolean;
28
+
29
+ /** 항목 필터 함수 */
30
+ filterFn?: (item: TItem, index: number) => boolean;
31
+ /** 선택 모달 컴포넌트 팩토리 */
32
+ modal?: () => JSX.Element;
33
+ /** 편집 모달 컴포넌트 팩토리 */
34
+ editModal?: () => JSX.Element;
35
+
36
+ /** 아이템 렌더링 함수 */
37
+ children: (item: TItem, index: number, depth: number) => JSX.Element;
38
+ }
39
+
40
+ export function SharedDataSelect<TItem>(props: SharedDataSelectProps<TItem>): JSX.Element {
41
+ const [local, rest] = splitProps(props, ["data", "filterFn", "modal", "editModal", "children"]);
42
+
43
+ const dialog = useDialog();
44
+
45
+ // filterFn 적용된 items
46
+ const items = createMemo(() => {
47
+ const allItems = local.data.items();
48
+ if (!local.filterFn) return allItems;
49
+ return allItems.filter(local.filterFn);
50
+ });
51
+
52
+ // modal 열기
53
+ const handleOpenModal = async () => {
54
+ if (!local.modal) return;
55
+ await dialog.show(local.modal, {});
56
+ };
57
+
58
+ // editModal 열기
59
+ const handleOpenEditModal = async () => {
60
+ if (!local.editModal) return;
61
+ await dialog.show(local.editModal, {});
62
+ };
63
+
64
+ // Select의 discriminated union (multiple: true | false?)과 TItem → unknown 변환을 위해 mergeProps + as 사용
65
+ // getter로 래핑하여 SolidJS 반응성 lint 규칙 충족
66
+ const selectProps = mergeProps(rest, {
67
+ get items() {
68
+ return items();
69
+ },
70
+ get getChildren() {
71
+ if (!local.data.getParentKey) return undefined;
72
+ // eslint-disable-next-line solid/reactivity -- 반환 함수는 Select 내부 JSX tracked scope에서 호출됨
73
+ return (item: TItem) => {
74
+ const key = local.data.getKey(item);
75
+ return items().filter((child) => local.data.getParentKey!(child) === key);
76
+ };
77
+ },
78
+ get getSearchText() {
79
+ return local.data.getSearchText;
80
+ },
81
+ get getIsHidden() {
82
+ return local.data.getIsHidden;
83
+ },
84
+ }) as unknown as SelectProps;
85
+
86
+ return (
87
+ <Select {...selectProps}>
88
+ <Select.ItemTemplate>{local.children}</Select.ItemTemplate>
89
+ {local.modal && (
90
+ <Select.Action onClick={() => void handleOpenModal()} aria-label="검색">
91
+ <Icon icon={IconSearch} size="1em" />
92
+ </Select.Action>
93
+ )}
94
+ {local.editModal && (
95
+ <Select.Action onClick={() => void handleOpenEditModal()} aria-label="편집">
96
+ <Icon icon={IconEdit} size="1em" />
97
+ </Select.Action>
98
+ )}
99
+ </Select>
100
+ );
101
+ }
@@ -0,0 +1,47 @@
1
+ import { type JSX, splitProps } from "solid-js";
2
+ import { type SharedDataAccessor } from "../../../providers/shared-data/SharedDataContext";
3
+ import {
4
+ DataSelectButton,
5
+ type DataSelectButtonProps,
6
+ } from "../data-select-button/DataSelectButton";
7
+ import { type ComponentSize } from "../../../styles/tokens.styles";
8
+
9
+ /** SharedDataSelectButton Props */
10
+ export interface SharedDataSelectButtonProps<TItem> {
11
+ /** 공유 데이터 접근자 */
12
+ data: SharedDataAccessor<TItem>;
13
+
14
+ /** 현재 선택된 키 (단일 또는 다중) */
15
+ value?: DataSelectButtonProps<TItem>["value"];
16
+ /** 값 변경 콜백 */
17
+ onValueChange?: DataSelectButtonProps<TItem>["onValueChange"];
18
+ /** 다중 선택 모드 */
19
+ multiple?: boolean;
20
+ /** 필수 입력 */
21
+ required?: boolean;
22
+ /** 비활성화 */
23
+ disabled?: boolean;
24
+ /** 트리거 크기 */
25
+ size?: ComponentSize;
26
+ /** 테두리 없는 스타일 */
27
+ inset?: boolean;
28
+
29
+ /** 선택 모달 컴포넌트 팩토리 */
30
+ modal: () => JSX.Element;
31
+ /** 아이템 렌더링 함수 */
32
+ children: (item: TItem) => JSX.Element;
33
+ }
34
+
35
+ export function SharedDataSelectButton<TItem>(
36
+ props: SharedDataSelectButtonProps<TItem>,
37
+ ): JSX.Element {
38
+ const [local, rest] = splitProps(props, ["data", "children"]);
39
+
40
+ return (
41
+ <DataSelectButton
42
+ load={(keys) => local.data.items().filter((item) => keys.includes(local.data.getKey(item)))}
43
+ renderItem={local.children}
44
+ {...rest}
45
+ />
46
+ );
47
+ }
@@ -0,0 +1,85 @@
1
+ import { createMemo, type JSX, Show, splitProps } from "solid-js";
2
+ import { IconExternalLink } from "@tabler/icons-solidjs";
3
+ import { type SharedDataAccessor } from "../../../providers/shared-data/SharedDataContext";
4
+ import { SelectList } from "../../form-control/select-list/SelectList";
5
+ import { Icon } from "../../display/Icon";
6
+ import { useDialog } from "../../disclosure/DialogContext";
7
+
8
+ /** SharedDataSelectList Props */
9
+ export interface SharedDataSelectListProps<TItem> {
10
+ /** 공유 데이터 접근자 */
11
+ data: SharedDataAccessor<TItem>;
12
+
13
+ /** 현재 선택된 값 */
14
+ value?: TItem;
15
+ /** 값 변경 콜백 */
16
+ onValueChange?: (value: TItem | undefined) => void;
17
+ /** 필수 입력 */
18
+ required?: boolean;
19
+ /** 비활성화 */
20
+ disabled?: boolean;
21
+
22
+ /** 항목 필터 함수 */
23
+ filterFn?: (item: TItem, index: number) => boolean;
24
+ /** 값 변경 가드 (false 반환 시 변경 차단) */
25
+ canChange?: (item: TItem | undefined) => boolean | Promise<boolean>;
26
+ /** 페이지 크기 (있으면 Pagination 자동 표시) */
27
+ pageSize?: number;
28
+ /** 헤더 텍스트 */
29
+ header?: string;
30
+ /** 관리 모달 컴포넌트 팩토리 */
31
+ modal?: () => JSX.Element;
32
+
33
+ /** 서브 컴포넌트용 children (ItemTemplate 등) */
34
+ children: JSX.Element;
35
+ }
36
+
37
+ export function SharedDataSelectList<TItem>(props: SharedDataSelectListProps<TItem>): JSX.Element {
38
+ const [local, rest] = splitProps(props, ["data", "filterFn", "modal", "header", "children"]);
39
+
40
+ const dialog = useDialog();
41
+
42
+ // filterFn 적용된 items
43
+ const items = createMemo(() => {
44
+ const allItems = local.data.items();
45
+ if (!local.filterFn) return allItems;
46
+ return allItems.filter(local.filterFn);
47
+ });
48
+
49
+ // modal 열기
50
+ const handleOpenModal = async () => {
51
+ if (!local.modal) return;
52
+ await dialog.show(local.modal, {});
53
+ };
54
+
55
+ return (
56
+ <SelectList
57
+ {...rest}
58
+ items={items()}
59
+ getSearchText={local.data.getSearchText}
60
+ getIsHidden={local.data.getIsHidden}
61
+ >
62
+ {/* header + modal 아이콘을 SelectList.Header로 결합 */}
63
+ <Show when={local.header != null || local.modal != null}>
64
+ <SelectList.Header>
65
+ <div class="flex items-center gap-1">
66
+ <Show when={local.header != null}>
67
+ <span>{local.header}</span>
68
+ </Show>
69
+ <Show when={local.modal != null}>
70
+ <button
71
+ type="button"
72
+ class="inline-flex items-center justify-center rounded p-0.5 text-base-500 hover:text-primary-500 dark:text-base-400 dark:hover:text-primary-400"
73
+ aria-label="관리"
74
+ onClick={() => void handleOpenModal()}
75
+ >
76
+ <Icon icon={IconExternalLink} size="1em" />
77
+ </button>
78
+ </Show>
79
+ </div>
80
+ </SelectList.Header>
81
+ </Show>
82
+ {local.children}
83
+ </SelectList>
84
+ );
85
+ }
@@ -1,6 +1,5 @@
1
1
  import clsx from "clsx";
2
2
  import {
3
- bgSurface,
4
3
  borderDefault,
5
4
  type ComponentSize,
6
5
  disabledOpacity,
@@ -32,7 +31,8 @@ export const indicatorBaseClass = clsx(
32
31
  "size-4",
33
32
  "border",
34
33
  borderDefault,
35
- bgSurface,
34
+ // bgSurface,
35
+ "bg-primary-50 dark:bg-primary-950/30",
36
36
  "transition-colors",
37
37
  );
38
38
 
@@ -90,27 +90,25 @@ export const Checkbox: ParentComponent<CheckboxProps> = (props) => {
90
90
 
91
91
  return (
92
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>
93
+ <div
94
+ {...rest}
95
+ use:ripple={!local.disabled}
96
+ role="checkbox"
97
+ aria-checked={value()}
98
+ tabIndex={local.disabled ? -1 : 0}
99
+ class={getWrapperClass()}
100
+ style={local.style}
101
+ onClick={handleClick}
102
+ onKeyDown={handleKeyDown}
103
+ >
104
+ <div class={getIndicatorClass()}>
105
+ <Show when={value()}>
106
+ <Icon icon={IconCheck} size="1em" />
112
107
  </Show>
113
- </label>
108
+ </div>
109
+ <Show when={local.children}>
110
+ <span>{local.children}</span>
111
+ </Show>
114
112
  </div>
115
113
  </Invalid>
116
114
  );
@@ -91,27 +91,25 @@ export const Radio: ParentComponent<RadioProps> = (props) => {
91
91
 
92
92
  return (
93
93
  <Invalid message={errorMsg()} variant="border" touchMode={local.touchMode}>
94
- <div class="inline-flex">
95
- <label
96
- {...rest}
97
- use:ripple={!local.disabled}
98
- role="radio"
99
- aria-checked={value()}
100
- tabIndex={local.disabled ? -1 : 0}
101
- class={getWrapperClass()}
102
- style={local.style}
103
- onClick={handleClick}
104
- onKeyDown={handleKeyDown}
105
- >
106
- <div class={getIndicatorClass()}>
107
- <Show when={value()}>
108
- <div class={radioDotClass} />
109
- </Show>
110
- </div>
111
- <Show when={local.children}>
112
- <span>{local.children}</span>
94
+ <div
95
+ {...rest}
96
+ use:ripple={!local.disabled}
97
+ role="radio"
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
+ <div class={radioDotClass} />
113
108
  </Show>
114
- </label>
109
+ </div>
110
+ <Show when={local.children}>
111
+ <span>{local.children}</span>
112
+ </Show>
115
113
  </div>
116
114
  </Invalid>
117
115
  );