@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.
- package/README.md +6 -0
- package/dist/components/data/sheet/DataSheet.d.ts.map +1 -1
- package/dist/components/data/sheet/DataSheet.js +3 -2
- package/dist/components/data/sheet/DataSheet.js.map +2 -2
- package/dist/components/features/address/AddressSearch.d.ts +8 -0
- package/dist/components/features/address/AddressSearch.d.ts.map +1 -0
- package/dist/components/features/address/AddressSearch.js +72 -0
- package/dist/components/features/address/AddressSearch.js.map +6 -0
- package/dist/components/features/crud-detail/CrudDetail.d.ts.map +1 -0
- package/dist/components/{data → features}/crud-detail/CrudDetail.js +62 -41
- package/dist/components/features/crud-detail/CrudDetail.js.map +6 -0
- package/dist/components/features/crud-detail/CrudDetailAfter.d.ts.map +1 -0
- package/dist/components/{data → features}/crud-detail/CrudDetailAfter.js.map +1 -1
- package/dist/components/features/crud-detail/CrudDetailBefore.d.ts.map +1 -0
- package/dist/components/{data → features}/crud-detail/CrudDetailBefore.js.map +1 -1
- package/dist/components/features/crud-detail/CrudDetailTools.d.ts.map +1 -0
- package/dist/components/{data → features}/crud-detail/CrudDetailTools.js.map +1 -1
- package/dist/components/features/crud-detail/types.d.ts.map +1 -0
- package/dist/components/features/crud-sheet/CrudSheet.d.ts.map +1 -0
- package/dist/components/{data → features}/crud-sheet/CrudSheet.js +166 -21
- package/dist/components/features/crud-sheet/CrudSheet.js.map +6 -0
- package/dist/components/features/crud-sheet/CrudSheetColumn.d.ts.map +1 -0
- package/dist/components/{data → features}/crud-sheet/CrudSheetColumn.js +1 -1
- package/dist/components/{data → features}/crud-sheet/CrudSheetColumn.js.map +1 -1
- package/dist/components/features/crud-sheet/CrudSheetFilter.d.ts.map +1 -0
- package/dist/components/{data → features}/crud-sheet/CrudSheetFilter.js.map +1 -1
- package/dist/components/features/crud-sheet/CrudSheetHeader.d.ts.map +1 -0
- package/dist/components/{data → features}/crud-sheet/CrudSheetHeader.js.map +1 -1
- package/dist/components/features/crud-sheet/CrudSheetTools.d.ts.map +1 -0
- package/dist/components/{data → features}/crud-sheet/CrudSheetTools.js.map +1 -1
- package/dist/components/{data → features}/crud-sheet/types.d.ts +10 -4
- package/dist/components/features/crud-sheet/types.d.ts.map +1 -0
- package/dist/components/features/data-select-button/DataSelectButton.d.ts +38 -0
- package/dist/components/features/data-select-button/DataSelectButton.d.ts.map +1 -0
- package/dist/components/features/data-select-button/DataSelectButton.js +184 -0
- package/dist/components/features/data-select-button/DataSelectButton.js.map +6 -0
- package/dist/components/features/permission-table/PermissionTable.d.ts.map +1 -0
- package/dist/components/{data → features}/permission-table/PermissionTable.js +1 -1
- package/dist/components/{data → features}/permission-table/PermissionTable.js.map +1 -1
- package/dist/components/features/shared-data/SharedDataSelect.d.ts +32 -0
- package/dist/components/features/shared-data/SharedDataSelect.d.ts.map +1 -0
- package/dist/components/features/shared-data/SharedDataSelect.js +74 -0
- package/dist/components/features/shared-data/SharedDataSelect.js.map +6 -0
- package/dist/components/features/shared-data/SharedDataSelectButton.d.ts +29 -0
- package/dist/components/features/shared-data/SharedDataSelectButton.d.ts.map +1 -0
- package/dist/components/features/shared-data/SharedDataSelectButton.js +17 -0
- package/dist/components/features/shared-data/SharedDataSelectButton.js.map +6 -0
- package/dist/components/features/shared-data/SharedDataSelectList.d.ts +29 -0
- package/dist/components/features/shared-data/SharedDataSelectList.d.ts.map +1 -0
- package/dist/components/features/shared-data/SharedDataSelectList.js +80 -0
- package/dist/components/features/shared-data/SharedDataSelectList.js.map +6 -0
- package/dist/components/form-control/checkbox/Checkbox.d.ts.map +1 -1
- package/dist/components/form-control/checkbox/Checkbox.js +10 -10
- package/dist/components/form-control/checkbox/Checkbox.js.map +2 -2
- package/dist/components/form-control/checkbox/Checkbox.styles.d.ts.map +1 -1
- package/dist/components/form-control/checkbox/Checkbox.styles.js +2 -2
- package/dist/components/form-control/checkbox/Checkbox.styles.js.map +1 -1
- package/dist/components/form-control/checkbox/Radio.d.ts.map +1 -1
- package/dist/components/form-control/checkbox/Radio.js +13 -13
- package/dist/components/form-control/checkbox/Radio.js.map +2 -2
- package/dist/components/form-control/select/Select.d.ts +7 -3
- package/dist/components/form-control/select/Select.d.ts.map +1 -1
- package/dist/components/form-control/select/Select.js +146 -45
- package/dist/components/form-control/select/Select.js.map +2 -2
- package/dist/components/form-control/select-list/SelectList.d.ts +54 -0
- package/dist/components/form-control/select-list/SelectList.d.ts.map +1 -0
- package/dist/components/form-control/select-list/SelectList.js +280 -0
- package/dist/components/form-control/select-list/SelectList.js.map +6 -0
- package/dist/components/form-control/select-list/SelectListContext.d.ts +13 -0
- package/dist/components/form-control/select-list/SelectListContext.d.ts.map +1 -0
- package/dist/components/form-control/select-list/SelectListContext.js +14 -0
- package/dist/components/form-control/select-list/SelectListContext.js.map +6 -0
- package/dist/index.d.ts +11 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/providers/ServiceClientContext.d.ts +5 -5
- package/dist/providers/ServiceClientContext.d.ts.map +1 -1
- package/dist/providers/ServiceClientProvider.d.ts.map +1 -1
- package/dist/providers/ServiceClientProvider.js +12 -8
- package/dist/providers/ServiceClientProvider.js.map +2 -2
- package/dist/providers/shared-data/SharedDataContext.d.ts +16 -2
- package/dist/providers/shared-data/SharedDataContext.d.ts.map +1 -1
- package/dist/providers/shared-data/SharedDataContext.js.map +1 -1
- package/dist/providers/shared-data/SharedDataProvider.d.ts +1 -2
- package/dist/providers/shared-data/SharedDataProvider.d.ts.map +1 -1
- package/dist/providers/shared-data/SharedDataProvider.js +27 -13
- package/dist/providers/shared-data/SharedDataProvider.js.map +2 -2
- package/docs/data-components.md +15 -4
- package/docs/form-controls.md +257 -0
- package/docs/hooks.md +30 -0
- package/docs/providers.md +7 -0
- package/package.json +5 -3
- package/src/components/data/sheet/DataSheet.tsx +6 -7
- package/src/components/features/address/AddressSearch.tsx +75 -0
- package/src/components/{data → features}/crud-detail/CrudDetail.tsx +51 -26
- package/src/components/{data → features}/crud-sheet/CrudSheet.tsx +160 -23
- package/src/components/{data → features}/crud-sheet/CrudSheetColumn.tsx +1 -1
- package/src/components/{data → features}/crud-sheet/types.ts +14 -4
- package/src/components/features/data-select-button/DataSelectButton.tsx +279 -0
- package/src/components/{data → features}/permission-table/PermissionTable.tsx +1 -1
- package/src/components/features/shared-data/SharedDataSelect.tsx +101 -0
- package/src/components/features/shared-data/SharedDataSelectButton.tsx +47 -0
- package/src/components/features/shared-data/SharedDataSelectList.tsx +85 -0
- package/src/components/form-control/checkbox/Checkbox.styles.ts +2 -2
- package/src/components/form-control/checkbox/Checkbox.tsx +18 -20
- package/src/components/form-control/checkbox/Radio.tsx +18 -20
- package/src/components/form-control/select/Select.tsx +192 -36
- package/src/components/form-control/select-list/SelectList.tsx +385 -0
- package/src/components/form-control/select-list/SelectListContext.ts +23 -0
- package/src/index.ts +29 -5
- package/src/providers/ServiceClientContext.ts +5 -5
- package/src/providers/ServiceClientProvider.tsx +17 -12
- package/src/providers/shared-data/SharedDataContext.ts +16 -2
- package/src/providers/shared-data/SharedDataProvider.tsx +33 -17
- package/dist/components/data/crud-detail/CrudDetail.d.ts.map +0 -1
- package/dist/components/data/crud-detail/CrudDetail.js.map +0 -6
- package/dist/components/data/crud-detail/CrudDetailAfter.d.ts.map +0 -1
- package/dist/components/data/crud-detail/CrudDetailBefore.d.ts.map +0 -1
- package/dist/components/data/crud-detail/CrudDetailTools.d.ts.map +0 -1
- package/dist/components/data/crud-detail/types.d.ts.map +0 -1
- package/dist/components/data/crud-sheet/CrudSheet.d.ts.map +0 -1
- package/dist/components/data/crud-sheet/CrudSheet.js.map +0 -6
- package/dist/components/data/crud-sheet/CrudSheetColumn.d.ts.map +0 -1
- package/dist/components/data/crud-sheet/CrudSheetFilter.d.ts.map +0 -1
- package/dist/components/data/crud-sheet/CrudSheetHeader.d.ts.map +0 -1
- package/dist/components/data/crud-sheet/CrudSheetTools.d.ts.map +0 -1
- package/dist/components/data/crud-sheet/types.d.ts.map +0 -1
- package/dist/components/data/permission-table/PermissionTable.d.ts.map +0 -1
- /package/dist/components/{data → features}/crud-detail/CrudDetail.d.ts +0 -0
- /package/dist/components/{data → features}/crud-detail/CrudDetailAfter.d.ts +0 -0
- /package/dist/components/{data → features}/crud-detail/CrudDetailAfter.js +0 -0
- /package/dist/components/{data → features}/crud-detail/CrudDetailBefore.d.ts +0 -0
- /package/dist/components/{data → features}/crud-detail/CrudDetailBefore.js +0 -0
- /package/dist/components/{data → features}/crud-detail/CrudDetailTools.d.ts +0 -0
- /package/dist/components/{data → features}/crud-detail/CrudDetailTools.js +0 -0
- /package/dist/components/{data → features}/crud-detail/types.d.ts +0 -0
- /package/dist/components/{data → features}/crud-detail/types.js +0 -0
- /package/dist/components/{data → features}/crud-detail/types.js.map +0 -0
- /package/dist/components/{data → features}/crud-sheet/CrudSheet.d.ts +0 -0
- /package/dist/components/{data → features}/crud-sheet/CrudSheetColumn.d.ts +0 -0
- /package/dist/components/{data → features}/crud-sheet/CrudSheetFilter.d.ts +0 -0
- /package/dist/components/{data → features}/crud-sheet/CrudSheetFilter.js +0 -0
- /package/dist/components/{data → features}/crud-sheet/CrudSheetHeader.d.ts +0 -0
- /package/dist/components/{data → features}/crud-sheet/CrudSheetHeader.js +0 -0
- /package/dist/components/{data → features}/crud-sheet/CrudSheetTools.d.ts +0 -0
- /package/dist/components/{data → features}/crud-sheet/CrudSheetTools.js +0 -0
- /package/dist/components/{data → features}/crud-sheet/types.js +0 -0
- /package/dist/components/{data → features}/crud-sheet/types.js.map +0 -0
- /package/dist/components/{data → features}/permission-table/PermissionTable.d.ts +0 -0
- /package/src/components/{data → features}/crud-detail/CrudDetailAfter.tsx +0 -0
- /package/src/components/{data → features}/crud-detail/CrudDetailBefore.tsx +0 -0
- /package/src/components/{data → features}/crud-detail/CrudDetailTools.tsx +0 -0
- /package/src/components/{data → features}/crud-detail/types.ts +0 -0
- /package/src/components/{data → features}/crud-sheet/CrudSheetFilter.tsx +0 -0
- /package/src/components/{data → features}/crud-sheet/CrudSheetHeader.tsx +0 -0
- /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 "
|
|
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
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
>
|
|
105
|
-
<
|
|
106
|
-
<
|
|
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
|
-
</
|
|
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
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
>
|
|
106
|
-
<
|
|
107
|
-
<
|
|
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
|
-
</
|
|
109
|
+
</div>
|
|
110
|
+
<Show when={local.children}>
|
|
111
|
+
<span>{local.children}</span>
|
|
112
|
+
</Show>
|
|
115
113
|
</div>
|
|
116
114
|
</Invalid>
|
|
117
115
|
);
|