@simplysm/solid 13.0.62 → 13.0.64

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 (86) hide show
  1. package/README.md +6 -0
  2. package/dist/components/data/crud-detail/CrudDetail.d.ts.map +1 -1
  3. package/dist/components/data/crud-detail/CrudDetail.js +62 -41
  4. package/dist/components/data/crud-detail/CrudDetail.js.map +2 -2
  5. package/dist/components/data/crud-sheet/CrudSheet.d.ts.map +1 -1
  6. package/dist/components/data/crud-sheet/CrudSheet.js +164 -19
  7. package/dist/components/data/crud-sheet/CrudSheet.js.map +2 -2
  8. package/dist/components/data/crud-sheet/types.d.ts +9 -3
  9. package/dist/components/data/crud-sheet/types.d.ts.map +1 -1
  10. package/dist/components/data/sheet/DataSheet.d.ts.map +1 -1
  11. package/dist/components/data/sheet/DataSheet.js +3 -2
  12. package/dist/components/data/sheet/DataSheet.js.map +2 -2
  13. package/dist/components/form-control/checkbox/Checkbox.d.ts.map +1 -1
  14. package/dist/components/form-control/checkbox/Checkbox.js +10 -10
  15. package/dist/components/form-control/checkbox/Checkbox.js.map +2 -2
  16. package/dist/components/form-control/checkbox/Checkbox.styles.d.ts.map +1 -1
  17. package/dist/components/form-control/checkbox/Checkbox.styles.js +2 -2
  18. package/dist/components/form-control/checkbox/Checkbox.styles.js.map +1 -1
  19. package/dist/components/form-control/checkbox/Radio.d.ts.map +1 -1
  20. package/dist/components/form-control/checkbox/Radio.js +13 -13
  21. package/dist/components/form-control/checkbox/Radio.js.map +2 -2
  22. package/dist/components/form-control/data-select-button/DataSelectButton.d.ts +38 -0
  23. package/dist/components/form-control/data-select-button/DataSelectButton.d.ts.map +1 -0
  24. package/dist/components/form-control/data-select-button/DataSelectButton.js +184 -0
  25. package/dist/components/form-control/data-select-button/DataSelectButton.js.map +6 -0
  26. package/dist/components/form-control/select/Select.d.ts +7 -3
  27. package/dist/components/form-control/select/Select.d.ts.map +1 -1
  28. package/dist/components/form-control/select/Select.js +146 -45
  29. package/dist/components/form-control/select/Select.js.map +2 -2
  30. package/dist/components/form-control/select-list/SelectList.d.ts +54 -0
  31. package/dist/components/form-control/select-list/SelectList.d.ts.map +1 -0
  32. package/dist/components/form-control/select-list/SelectList.js +280 -0
  33. package/dist/components/form-control/select-list/SelectList.js.map +6 -0
  34. package/dist/components/form-control/select-list/SelectListContext.d.ts +13 -0
  35. package/dist/components/form-control/select-list/SelectListContext.d.ts.map +1 -0
  36. package/dist/components/form-control/select-list/SelectListContext.js +14 -0
  37. package/dist/components/form-control/select-list/SelectListContext.js.map +6 -0
  38. package/dist/components/form-control/shared-data/SharedDataSelect.d.ts +32 -0
  39. package/dist/components/form-control/shared-data/SharedDataSelect.d.ts.map +1 -0
  40. package/dist/components/form-control/shared-data/SharedDataSelect.js +74 -0
  41. package/dist/components/form-control/shared-data/SharedDataSelect.js.map +6 -0
  42. package/dist/components/form-control/shared-data/SharedDataSelectButton.d.ts +29 -0
  43. package/dist/components/form-control/shared-data/SharedDataSelectButton.d.ts.map +1 -0
  44. package/dist/components/form-control/shared-data/SharedDataSelectButton.js +17 -0
  45. package/dist/components/form-control/shared-data/SharedDataSelectButton.js.map +6 -0
  46. package/dist/components/form-control/shared-data/SharedDataSelectList.d.ts +29 -0
  47. package/dist/components/form-control/shared-data/SharedDataSelectList.d.ts.map +1 -0
  48. package/dist/components/form-control/shared-data/SharedDataSelectList.js +80 -0
  49. package/dist/components/form-control/shared-data/SharedDataSelectList.js.map +6 -0
  50. package/dist/features/address/AddressSearch.d.ts +8 -0
  51. package/dist/features/address/AddressSearch.d.ts.map +1 -0
  52. package/dist/features/address/AddressSearch.js +72 -0
  53. package/dist/features/address/AddressSearch.js.map +6 -0
  54. package/dist/index.d.ts +6 -0
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +6 -0
  57. package/dist/index.js.map +1 -1
  58. package/dist/providers/shared-data/SharedDataContext.d.ts +14 -0
  59. package/dist/providers/shared-data/SharedDataContext.d.ts.map +1 -1
  60. package/dist/providers/shared-data/SharedDataContext.js.map +1 -1
  61. package/dist/providers/shared-data/SharedDataProvider.d.ts.map +1 -1
  62. package/dist/providers/shared-data/SharedDataProvider.js +5 -1
  63. package/dist/providers/shared-data/SharedDataProvider.js.map +2 -2
  64. package/docs/data-components.md +15 -4
  65. package/docs/form-controls.md +257 -0
  66. package/docs/hooks.md +30 -0
  67. package/docs/providers.md +7 -0
  68. package/package.json +3 -3
  69. package/src/components/data/crud-detail/CrudDetail.tsx +51 -26
  70. package/src/components/data/crud-sheet/CrudSheet.tsx +157 -20
  71. package/src/components/data/crud-sheet/types.ts +13 -3
  72. package/src/components/data/sheet/DataSheet.tsx +6 -7
  73. package/src/components/form-control/checkbox/Checkbox.styles.ts +2 -2
  74. package/src/components/form-control/checkbox/Checkbox.tsx +18 -20
  75. package/src/components/form-control/checkbox/Radio.tsx +18 -20
  76. package/src/components/form-control/data-select-button/DataSelectButton.tsx +279 -0
  77. package/src/components/form-control/select/Select.tsx +192 -36
  78. package/src/components/form-control/select-list/SelectList.tsx +385 -0
  79. package/src/components/form-control/select-list/SelectListContext.ts +23 -0
  80. package/src/components/form-control/shared-data/SharedDataSelect.tsx +101 -0
  81. package/src/components/form-control/shared-data/SharedDataSelectButton.tsx +47 -0
  82. package/src/components/form-control/shared-data/SharedDataSelectList.tsx +85 -0
  83. package/src/features/address/AddressSearch.tsx +75 -0
  84. package/src/index.ts +18 -0
  85. package/src/providers/shared-data/SharedDataContext.ts +14 -0
  86. package/src/providers/shared-data/SharedDataProvider.tsx +4 -0
@@ -0,0 +1,385 @@
1
+ import {
2
+ children,
3
+ createEffect,
4
+ createMemo,
5
+ createSignal,
6
+ For,
7
+ type JSX,
8
+ onCleanup,
9
+ type ParentComponent,
10
+ Show,
11
+ splitProps,
12
+ } from "solid-js";
13
+ import clsx from "clsx";
14
+ import { twMerge } from "tailwind-merge";
15
+ import { List } from "../../data/list/List";
16
+ import { Pagination } from "../../data/Pagination";
17
+ import { TextInput } from "../field/TextInput";
18
+ import { createSlotSignal } from "../../../hooks/createSlotSignal";
19
+ import { SelectListContext, type SelectListContextValue } from "./SelectListContext";
20
+ import { useSelectListContext } from "./SelectListContext";
21
+ import {
22
+ listItemBaseClass,
23
+ listItemSelectedClass,
24
+ listItemContentClass,
25
+ listItemDisabledClass,
26
+ } from "../../data/list/ListItem.styles";
27
+ import { ripple } from "../../../directives/ripple";
28
+ import { textMuted } from "../../../styles/tokens.styles";
29
+
30
+ void ripple;
31
+
32
+ // ─── 서브 컴포넌트 ──────────────────────────────────────
33
+
34
+ /**
35
+ * Header 슬롯 서브 컴포넌트
36
+ */
37
+ const SelectListHeader: ParentComponent = (props) => {
38
+ const ctx = useSelectListContext();
39
+ // eslint-disable-next-line solid/reactivity -- 슬롯 accessor로 저장, JSX tracked scope에서 호출됨
40
+ ctx.setHeader(() => props.children);
41
+ onCleanup(() => ctx.setHeader(undefined));
42
+ return null;
43
+ };
44
+
45
+ /**
46
+ * Filter 슬롯 서브 컴포넌트
47
+ */
48
+ const SelectListFilter: ParentComponent = (props) => {
49
+ const ctx = useSelectListContext();
50
+ // eslint-disable-next-line solid/reactivity -- 슬롯 accessor로 저장, JSX tracked scope에서 호출됨
51
+ ctx.setFilter(() => props.children);
52
+ onCleanup(() => ctx.setFilter(undefined));
53
+ return null;
54
+ };
55
+
56
+ /**
57
+ * ItemTemplate 서브 컴포넌트
58
+ */
59
+ const SelectListItemTemplate = <TArgs extends unknown[]>(props: {
60
+ children: (...args: TArgs) => JSX.Element;
61
+ }) => {
62
+ const ctx = useSelectListContext();
63
+ // eslint-disable-next-line solid/reactivity -- 렌더 함수를 signal에 저장, JSX tracked scope에서 호출됨
64
+ ctx.setItemTemplate(props.children as (...args: unknown[]) => JSX.Element);
65
+ onCleanup(() => ctx.setItemTemplate(undefined));
66
+ return null;
67
+ };
68
+
69
+ // ─── Props ──────────────────────────────────────────────
70
+
71
+ export interface SelectListProps<TValue> {
72
+ /** 목록 아이템 배열 */
73
+ items: TValue[];
74
+
75
+ /** 현재 선택된 값 */
76
+ value?: TValue;
77
+
78
+ /** 값 변경 콜백 */
79
+ onValueChange?: (value: TValue | undefined) => void;
80
+
81
+ /** 필수 선택 여부 (true이면 미지정 항목 숨김) */
82
+ required?: boolean;
83
+
84
+ /** 비활성화 */
85
+ disabled?: boolean;
86
+
87
+ /** 검색 텍스트 추출 함수 (있으면 검색 TextInput 자동 표시) */
88
+ getSearchText?: (item: TValue) => string;
89
+
90
+ /** 숨김 여부 필터 */
91
+ getIsHidden?: (item: TValue) => boolean;
92
+
93
+ /** 커스텀 필터 함수 */
94
+ filterFn?: (item: TValue, index: number) => boolean;
95
+
96
+ /** 값 변경 가드 (false 반환 시 변경 차단) */
97
+ canChange?: (item: TValue | undefined) => boolean | Promise<boolean>;
98
+
99
+ /** 페이지 크기 (있으면 Pagination 자동 표시) */
100
+ pageSize?: number;
101
+
102
+ /** 헤더 텍스트 (Header 슬롯보다 우선순위 낮음) */
103
+ header?: string;
104
+
105
+ /** 커스텀 class */
106
+ class?: string;
107
+
108
+ /** 커스텀 style */
109
+ style?: JSX.CSSProperties;
110
+
111
+ /** 서브 컴포넌트용 children */
112
+ children?: JSX.Element;
113
+ }
114
+
115
+ // ─── 스타일 ──────────────────────────────────────────────
116
+
117
+ const containerClass = clsx("inline-flex flex-col gap-1");
118
+
119
+ const headerClass = clsx("px-2 py-1 text-sm font-semibold");
120
+
121
+ // ─── 컴포넌트 ───────────────────────────────────────────
122
+
123
+ interface SelectListComponent {
124
+ <TValue = unknown>(props: SelectListProps<TValue>): JSX.Element;
125
+ Header: typeof SelectListHeader;
126
+ Filter: typeof SelectListFilter;
127
+ ItemTemplate: typeof SelectListItemTemplate;
128
+ }
129
+
130
+ export const SelectList: SelectListComponent = <TValue,>(props: SelectListProps<TValue>) => {
131
+ const [local, rest] = splitProps(props as SelectListProps<TValue> & { children?: JSX.Element }, [
132
+ "children",
133
+ "class",
134
+ "style",
135
+ "items",
136
+ "value",
137
+ "onValueChange",
138
+ "required",
139
+ "disabled",
140
+ "getSearchText",
141
+ "getIsHidden",
142
+ "filterFn",
143
+ "canChange",
144
+ "pageSize",
145
+ "header",
146
+ ]);
147
+
148
+ // ─── 내부 상태 ─────────────────────────────────────────
149
+
150
+ const [searchText, setSearchText] = createSignal("");
151
+ const [page, setPage] = createSignal(1);
152
+
153
+ // ─── 슬롯 signals ────────────────────────────────────
154
+
155
+ const [headerSlot, setHeader] = createSlotSignal();
156
+ const [filterSlot, setFilter] = createSlotSignal();
157
+ const [itemTemplate, _setItemTemplate] = createSignal<
158
+ ((...args: unknown[]) => JSX.Element) | undefined
159
+ >();
160
+ const setItemTemplate = (fn: ((...args: unknown[]) => JSX.Element) | undefined) =>
161
+ _setItemTemplate(() => fn);
162
+
163
+ // Context 값
164
+ const contextValue: SelectListContextValue = {
165
+ setHeader,
166
+ setFilter,
167
+ setItemTemplate,
168
+ };
169
+
170
+ // ─── items 변경 시 value 자동 재매칭 ──────────────────
171
+
172
+ createEffect(() => {
173
+ const currentItems = local.items;
174
+ const currentValue = local.value;
175
+ if (currentValue === undefined) return;
176
+
177
+ // 새 items에서 현재 value를 참조로 찾기
178
+ const found = currentItems.find((item) => item === currentValue);
179
+ if (found !== undefined) {
180
+ // 이미 같은 참조면 아무 것도 하지 않음
181
+ return;
182
+ }
183
+
184
+ // 참조가 없으면 현재 value를 유지 (호출하지 않음)
185
+ });
186
+
187
+ // ─── 필터링 파이프라인 ─────────────────────────────────
188
+
189
+ // getIsHidden 필터 → 검색 필터 → filterFn
190
+ const filteredItems = createMemo(() => {
191
+ let result = local.items;
192
+
193
+ // getIsHidden 필터
194
+ if (local.getIsHidden) {
195
+ const fn = local.getIsHidden;
196
+ result = result.filter((item) => !fn(item));
197
+ }
198
+
199
+ // 검색 필터
200
+ const search = searchText().trim().toLowerCase();
201
+ if (search && local.getSearchText) {
202
+ const getText = local.getSearchText;
203
+ result = result.filter((item) => getText(item).toLowerCase().includes(search));
204
+ }
205
+
206
+ // filterFn
207
+ if (local.filterFn) {
208
+ const fn = local.filterFn;
209
+ result = result.filter((item, index) => fn(item, index));
210
+ }
211
+
212
+ return result;
213
+ });
214
+
215
+ // 페이지 수 계산
216
+ const totalPageCount = createMemo(() => {
217
+ if (local.pageSize == null) return 1;
218
+ return Math.max(1, Math.ceil(filteredItems().length / local.pageSize));
219
+ });
220
+
221
+ // 검색이나 필터 변경 시 페이지 리셋
222
+ createEffect(() => {
223
+ // filteredItems에 의존
224
+ void filteredItems();
225
+ setPage(1);
226
+ });
227
+
228
+ // 페이지 슬라이스
229
+ const displayItems = createMemo(() => {
230
+ const items = filteredItems();
231
+ if (local.pageSize == null) return items;
232
+
233
+ const start = (page() - 1) * local.pageSize;
234
+ const end = start + local.pageSize;
235
+ return items.slice(start, end);
236
+ });
237
+
238
+ // ─── 선택/토글 핸들러 ─────────────────────────────────
239
+
240
+ const handleSelect = async (item: TValue | undefined) => {
241
+ if (local.disabled) return;
242
+
243
+ // canChange 가드
244
+ if (local.canChange) {
245
+ const allowed = await local.canChange(item);
246
+ if (!allowed) return;
247
+ }
248
+
249
+ // 토글: 이미 선택된 값을 다시 클릭하면 선택 해제 (required가 아닐 때만)
250
+ if (item !== undefined && item === local.value && !local.required) {
251
+ local.onValueChange?.(undefined);
252
+ } else {
253
+ local.onValueChange?.(item);
254
+ }
255
+ };
256
+
257
+ // ─── 아이템 렌더링 ────────────────────────────────────
258
+
259
+ const getItemTemplate = (): ((item: TValue, index: number) => JSX.Element) | undefined => {
260
+ return itemTemplate() as ((item: TValue, index: number) => JSX.Element) | undefined;
261
+ };
262
+
263
+ const renderItem = (item: TValue, index: number): JSX.Element => {
264
+ const tpl = getItemTemplate();
265
+ if (tpl) {
266
+ return tpl(item, index);
267
+ }
268
+ return <>{String(item)}</>;
269
+ };
270
+
271
+ // ─── 내부 렌더링 ──────────────────────────────────────
272
+
273
+ const SelectListInner: ParentComponent = (innerProps) => {
274
+ // children() resolve로 서브 컴포넌트 등록 트리거
275
+ const resolved = children(() => innerProps.children);
276
+ // resolved는 사용하지 않지만 서브 컴포넌트가 등록되도록 evaluate 필요
277
+ void resolved;
278
+
279
+ return (
280
+ <div
281
+ {...rest}
282
+ data-select-list
283
+ class={twMerge(containerClass, local.class)}
284
+ style={local.style}
285
+ >
286
+ {/* Header: 슬롯 우선, 없으면 props.header 텍스트 */}
287
+ <Show
288
+ when={headerSlot()}
289
+ fallback={
290
+ <Show when={local.header}>
291
+ <div class={headerClass}>{local.header}</div>
292
+ </Show>
293
+ }
294
+ >
295
+ {headerSlot()!()}
296
+ </Show>
297
+
298
+ {/* Filter: 슬롯 우선, 없으면 getSearchText 있을 때 TextInput */}
299
+ <Show
300
+ when={filterSlot()}
301
+ fallback={
302
+ <Show when={local.getSearchText}>
303
+ <TextInput
304
+ value={searchText()}
305
+ onValueChange={setSearchText}
306
+ placeholder="검색..."
307
+ inset
308
+ disabled={local.disabled}
309
+ />
310
+ </Show>
311
+ }
312
+ >
313
+ {filterSlot()!()}
314
+ </Show>
315
+
316
+ {/* Pagination */}
317
+ <Show when={local.pageSize != null && totalPageCount() > 1}>
318
+ <Pagination
319
+ page={page()}
320
+ onPageChange={setPage}
321
+ totalPageCount={totalPageCount()}
322
+ size="sm"
323
+ />
324
+ </Show>
325
+
326
+ {/* List */}
327
+ <List inset>
328
+ {/* 미지정 항목 (required가 아닐 때) */}
329
+ <Show when={!local.required}>
330
+ <button
331
+ type="button"
332
+ use:ripple={!local.disabled}
333
+ class={twMerge(
334
+ listItemBaseClass,
335
+ local.value === undefined && listItemSelectedClass,
336
+ local.disabled && listItemDisabledClass,
337
+ )}
338
+ data-list-item
339
+ role="treeitem"
340
+ aria-selected={local.value === undefined || undefined}
341
+ aria-disabled={local.disabled || undefined}
342
+ tabIndex={local.disabled ? -1 : 0}
343
+ onClick={() => handleSelect(undefined)}
344
+ >
345
+ <span class={clsx(listItemContentClass, textMuted)}>미지정</span>
346
+ </button>
347
+ </Show>
348
+
349
+ {/* 아이템 목록 */}
350
+ <For each={displayItems()}>
351
+ {(item, index) => (
352
+ <button
353
+ type="button"
354
+ use:ripple={!local.disabled}
355
+ class={twMerge(
356
+ listItemBaseClass,
357
+ item === local.value && listItemSelectedClass,
358
+ local.disabled && listItemDisabledClass,
359
+ )}
360
+ data-list-item
361
+ role="treeitem"
362
+ aria-selected={item === local.value || undefined}
363
+ aria-disabled={local.disabled || undefined}
364
+ tabIndex={local.disabled ? -1 : 0}
365
+ onClick={() => handleSelect(item)}
366
+ >
367
+ <span class={listItemContentClass}>{renderItem(item, index())}</span>
368
+ </button>
369
+ )}
370
+ </For>
371
+ </List>
372
+ </div>
373
+ );
374
+ };
375
+
376
+ return (
377
+ <SelectListContext.Provider value={contextValue}>
378
+ <SelectListInner>{local.children}</SelectListInner>
379
+ </SelectListContext.Provider>
380
+ );
381
+ };
382
+
383
+ SelectList.Header = SelectListHeader;
384
+ SelectList.Filter = SelectListFilter;
385
+ SelectList.ItemTemplate = SelectListItemTemplate;
@@ -0,0 +1,23 @@
1
+ import { createContext, useContext, type JSX } from "solid-js";
2
+ import type { SlotAccessor } from "../../../hooks/createSlotSignal";
3
+
4
+ export interface SelectListContextValue {
5
+ /** Header 슬롯 등록 */
6
+ setHeader: (content: SlotAccessor) => void;
7
+
8
+ /** Filter 슬롯 등록 */
9
+ setFilter: (content: SlotAccessor) => void;
10
+
11
+ /** ItemTemplate 등록 */
12
+ setItemTemplate: (fn: ((...args: unknown[]) => JSX.Element) | undefined) => void;
13
+ }
14
+
15
+ export const SelectListContext = createContext<SelectListContextValue>();
16
+
17
+ export function useSelectListContext(): SelectListContextValue {
18
+ const context = useContext(SelectListContext);
19
+ if (!context) {
20
+ throw new Error("useSelectListContext는 SelectList 컴포넌트 내부에서만 사용할 수 있습니다");
21
+ }
22
+ return context;
23
+ }
@@ -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 "../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 "../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
+ }