@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,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
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,9 @@ export * from "./components/form-control/Button";
|
|
|
6
6
|
// Select
|
|
7
7
|
export * from "./components/form-control/select/Select";
|
|
8
8
|
|
|
9
|
+
// SelectList
|
|
10
|
+
export * from "./components/form-control/select-list/SelectList";
|
|
11
|
+
|
|
9
12
|
// Combobox
|
|
10
13
|
export * from "./components/form-control/combobox/Combobox";
|
|
11
14
|
|
|
@@ -57,12 +60,7 @@ export * from "./components/data/Pagination";
|
|
|
57
60
|
export * from "./components/data/sheet/DataSheet";
|
|
58
61
|
export * from "./components/data/sheet/DataSheet.styles";
|
|
59
62
|
export * from "./components/data/sheet/types";
|
|
60
|
-
export * from "./components/data/crud-sheet/CrudSheet";
|
|
61
|
-
export * from "./components/data/crud-sheet/types";
|
|
62
|
-
export * from "./components/data/crud-detail/CrudDetail";
|
|
63
|
-
export * from "./components/data/crud-detail/types";
|
|
64
63
|
export * from "./components/data/calendar/Calendar";
|
|
65
|
-
export * from "./components/data/permission-table/PermissionTable";
|
|
66
64
|
export * from "./components/data/kanban/Kanban";
|
|
67
65
|
export * from "./components/data/kanban/KanbanContext";
|
|
68
66
|
|
|
@@ -171,3 +169,29 @@ export * from "./helpers/mergeStyles";
|
|
|
171
169
|
export * from "./helpers/createAppStructure";
|
|
172
170
|
|
|
173
171
|
//#endregion
|
|
172
|
+
|
|
173
|
+
//#region ========== Features ==========
|
|
174
|
+
|
|
175
|
+
// Address
|
|
176
|
+
export * from "./components/features/address/AddressSearch";
|
|
177
|
+
|
|
178
|
+
// SharedData wrappers
|
|
179
|
+
export * from "./components/features/shared-data/SharedDataSelect";
|
|
180
|
+
export * from "./components/features/shared-data/SharedDataSelectButton";
|
|
181
|
+
export * from "./components/features/shared-data/SharedDataSelectList";
|
|
182
|
+
|
|
183
|
+
// DataSelectButton
|
|
184
|
+
export * from "./components/features/data-select-button/DataSelectButton";
|
|
185
|
+
|
|
186
|
+
// CrudSheet
|
|
187
|
+
export * from "./components/features/crud-sheet/CrudSheet";
|
|
188
|
+
export * from "./components/features/crud-sheet/types";
|
|
189
|
+
|
|
190
|
+
// CrudDetail
|
|
191
|
+
export * from "./components/features/crud-detail/CrudDetail";
|
|
192
|
+
export * from "./components/features/crud-detail/types";
|
|
193
|
+
|
|
194
|
+
// PermissionTable
|
|
195
|
+
export * from "./components/features/permission-table/PermissionTable";
|
|
196
|
+
|
|
197
|
+
//#endregion
|
|
@@ -5,14 +5,14 @@ import type { ServiceClient, ServiceConnectionConfig } from "@simplysm/service-c
|
|
|
5
5
|
* WebSocket 서비스 클라이언트 Context 값
|
|
6
6
|
*/
|
|
7
7
|
export interface ServiceClientContextValue {
|
|
8
|
-
/** WebSocket 연결 열기 (key
|
|
9
|
-
connect: (key
|
|
8
|
+
/** WebSocket 연결 열기 (key 생략 시 "default") */
|
|
9
|
+
connect: (key?: string, options?: Partial<ServiceConnectionConfig>) => Promise<void>;
|
|
10
10
|
/** 연결 닫기 */
|
|
11
|
-
close: (key
|
|
11
|
+
close: (key?: string) => Promise<void>;
|
|
12
12
|
/** 연결된 클라이언트 인스턴스 가져오기 (연결되지 않은 key면 에러 발생) */
|
|
13
|
-
get: (key
|
|
13
|
+
get: (key?: string) => ServiceClient;
|
|
14
14
|
/** 연결 상태 확인 */
|
|
15
|
-
isConnected: (key
|
|
15
|
+
isConnected: (key?: string) => boolean;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/** WebSocket 서비스 클라이언트 Context */
|
|
@@ -45,11 +45,13 @@ export const ServiceClientProvider: ParentComponent = (props) => {
|
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
const connect = async (
|
|
48
|
-
key
|
|
48
|
+
key?: string,
|
|
49
49
|
options?: Partial<ServiceConnectionConfig>,
|
|
50
50
|
): Promise<void> => {
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
const resolvedKey = key ?? "default";
|
|
52
|
+
|
|
53
|
+
if (clientMap.has(resolvedKey)) {
|
|
54
|
+
const existing = clientMap.get(resolvedKey)!;
|
|
53
55
|
if (!existing.connected) {
|
|
54
56
|
throw new Error("이미 연결이 끊긴 클라이언트와 같은 키로 연결을 시도하였습니다.");
|
|
55
57
|
} else {
|
|
@@ -125,27 +127,30 @@ export const ServiceClientProvider: ParentComponent = (props) => {
|
|
|
125
127
|
});
|
|
126
128
|
|
|
127
129
|
await client.connect();
|
|
128
|
-
clientMap.set(
|
|
130
|
+
clientMap.set(resolvedKey, client);
|
|
129
131
|
};
|
|
130
132
|
|
|
131
|
-
const close = async (key
|
|
132
|
-
const
|
|
133
|
+
const close = async (key?: string): Promise<void> => {
|
|
134
|
+
const resolvedKey = key ?? "default";
|
|
135
|
+
const client = clientMap.get(resolvedKey);
|
|
133
136
|
if (client) {
|
|
134
137
|
await client.close();
|
|
135
|
-
clientMap.delete(
|
|
138
|
+
clientMap.delete(resolvedKey);
|
|
136
139
|
}
|
|
137
140
|
};
|
|
138
141
|
|
|
139
|
-
const get = (key
|
|
140
|
-
const
|
|
142
|
+
const get = (key?: string): ServiceClient => {
|
|
143
|
+
const resolvedKey = key ?? "default";
|
|
144
|
+
const client = clientMap.get(resolvedKey);
|
|
141
145
|
if (!client) {
|
|
142
|
-
throw new Error(`연결하지 않은 클라이언트 키입니다. ${
|
|
146
|
+
throw new Error(`연결하지 않은 클라이언트 키입니다. ${resolvedKey}`);
|
|
143
147
|
}
|
|
144
148
|
return client;
|
|
145
149
|
};
|
|
146
150
|
|
|
147
|
-
const isConnected = (key
|
|
148
|
-
const
|
|
151
|
+
const isConnected = (key?: string): boolean => {
|
|
152
|
+
const resolvedKey = key ?? "default";
|
|
153
|
+
const client = clientMap.get(resolvedKey);
|
|
149
154
|
return client?.connected ?? false;
|
|
150
155
|
};
|
|
151
156
|
|
|
@@ -7,8 +7,8 @@ import { type Accessor, createContext, useContext } from "solid-js";
|
|
|
7
7
|
* SharedDataProvider에 전달하여 서버 데이터 구독을 설정한다.
|
|
8
8
|
*/
|
|
9
9
|
export interface SharedDataDefinition<TData> {
|
|
10
|
-
/** 서비스 연결 key (
|
|
11
|
-
serviceKey
|
|
10
|
+
/** 서비스 연결 key (생략 시 "default") */
|
|
11
|
+
serviceKey?: string;
|
|
12
12
|
/** 데이터 조회 함수 (changeKeys가 있으면 해당 항목만 부분 갱신) */
|
|
13
13
|
fetch: (changeKeys?: Array<string | number>) => Promise<TData[]>;
|
|
14
14
|
/** 항목의 고유 key 추출 함수 */
|
|
@@ -17,6 +17,12 @@ export interface SharedDataDefinition<TData> {
|
|
|
17
17
|
orderBy: [(item: TData) => unknown, "asc" | "desc"][];
|
|
18
18
|
/** 서버 이벤트 필터 (같은 name의 이벤트 중 filter가 일치하는 것만 수신) */
|
|
19
19
|
filter?: unknown;
|
|
20
|
+
/** 항목에서 검색 텍스트를 추출하는 함수 */
|
|
21
|
+
getSearchText?: (item: TData) => string;
|
|
22
|
+
/** 항목이 숨김 상태인지 판별하는 함수 */
|
|
23
|
+
getIsHidden?: (item: TData) => boolean;
|
|
24
|
+
/** 항목의 부모 key를 추출하는 함수 (트리 구조 지원) */
|
|
25
|
+
getParentKey?: (item: TData) => string | number | undefined;
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
/**
|
|
@@ -32,6 +38,14 @@ export interface SharedDataAccessor<TData> {
|
|
|
32
38
|
get: (key: string | number | undefined) => TData | undefined;
|
|
33
39
|
/** 서버에 변경 이벤트 전파 (모든 구독자에게 refetch 트리거) */
|
|
34
40
|
emit: (changeKeys?: Array<string | number>) => Promise<void>;
|
|
41
|
+
/** 항목의 고유 key 추출 함수 */
|
|
42
|
+
getKey: (item: TData) => string | number;
|
|
43
|
+
/** 항목에서 검색 텍스트를 추출하는 함수 */
|
|
44
|
+
getSearchText?: (item: TData) => string;
|
|
45
|
+
/** 항목이 숨김 상태인지 판별하는 함수 */
|
|
46
|
+
getIsHidden?: (item: TData) => boolean;
|
|
47
|
+
/** 항목의 부모 key를 추출하는 함수 (트리 구조 지원) */
|
|
48
|
+
getParentKey?: (item: TData) => string | number | undefined;
|
|
35
49
|
}
|
|
36
50
|
|
|
37
51
|
/**
|
|
@@ -18,7 +18,7 @@ import { useLogger } from "../../hooks/useLogger";
|
|
|
18
18
|
* - ServiceClientProvider와 NotificationProvider 내부에서 사용해야 함
|
|
19
19
|
* - LoggerProvider가 있으면 fetch 실패를 로거에도 기록
|
|
20
20
|
* - configure() 호출 전: wait, busy, configure만 접근 가능. 데이터 접근 시 throw
|
|
21
|
-
* - configure() 호출 후: definitions
|
|
21
|
+
* - configure() 호출 후: definitions 등록. 각 key의 items()/get() 첫 접근 시 서버 이벤트 리스너 등록 + fetch (lazy)
|
|
22
22
|
* - 동시 fetch 호출 시 version counter로 데이터 역전 방지
|
|
23
23
|
* - fetch 실패 시 사용자에게 danger 알림 표시
|
|
24
24
|
* - cleanup 시 모든 이벤트 리스너 자동 해제
|
|
@@ -32,7 +32,6 @@ import { useLogger } from "../../hooks/useLogger";
|
|
|
32
32
|
* // 자식 컴포넌트에서 나중에 설정:
|
|
33
33
|
* useSharedData().configure(() => ({
|
|
34
34
|
* users: {
|
|
35
|
-
* serviceKey: "main",
|
|
36
35
|
* fetch: async (changeKeys) => fetchUsers(changeKeys),
|
|
37
36
|
* getKey: (item) => item.id,
|
|
38
37
|
* orderBy: [[(item) => item.name, "asc"]],
|
|
@@ -143,24 +142,37 @@ export function SharedDataProvider(props: { children: JSX.Element }): JSX.Elemen
|
|
|
143
142
|
// eslint-disable-next-line solid/reactivity -- memo 참조를 Map에 저장하는 것은 반응성 접근이 아님
|
|
144
143
|
memoMap.set(name, itemMap);
|
|
145
144
|
|
|
146
|
-
const client = serviceClient.get(def.serviceKey);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
},
|
|
154
|
-
)
|
|
155
|
-
.then((key) => {
|
|
156
|
-
listenerKeyMap.set(name, key);
|
|
157
|
-
});
|
|
145
|
+
const client = serviceClient.get(def.serviceKey ?? "default");
|
|
146
|
+
|
|
147
|
+
let initialized = false;
|
|
148
|
+
|
|
149
|
+
function ensureInitialized() {
|
|
150
|
+
if (initialized) return;
|
|
151
|
+
initialized = true;
|
|
158
152
|
|
|
159
|
-
|
|
153
|
+
// TODO: addEventListener가 resolve 전에 unmount되면 listener orphan 가능
|
|
154
|
+
void client
|
|
155
|
+
.addEventListener(
|
|
156
|
+
SharedDataChangeEvent,
|
|
157
|
+
{ name, filter: def.filter },
|
|
158
|
+
async (changeKeys) => {
|
|
159
|
+
await loadData(name, def, changeKeys);
|
|
160
|
+
},
|
|
161
|
+
)
|
|
162
|
+
.then((key) => {
|
|
163
|
+
listenerKeyMap.set(name, key);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
void loadData(name, def);
|
|
167
|
+
}
|
|
160
168
|
|
|
161
169
|
accessors[name] = {
|
|
162
|
-
items
|
|
170
|
+
items: () => {
|
|
171
|
+
ensureInitialized();
|
|
172
|
+
return items();
|
|
173
|
+
},
|
|
163
174
|
get: (key: string | number | undefined) => {
|
|
175
|
+
ensureInitialized();
|
|
164
176
|
if (key === undefined) return undefined;
|
|
165
177
|
return itemMap().get(key);
|
|
166
178
|
},
|
|
@@ -171,6 +183,10 @@ export function SharedDataProvider(props: { children: JSX.Element }): JSX.Elemen
|
|
|
171
183
|
changeKeys,
|
|
172
184
|
);
|
|
173
185
|
},
|
|
186
|
+
getKey: def.getKey,
|
|
187
|
+
getSearchText: def.getSearchText,
|
|
188
|
+
getIsHidden: def.getIsHidden,
|
|
189
|
+
getParentKey: def.getParentKey,
|
|
174
190
|
};
|
|
175
191
|
}
|
|
176
192
|
}
|
|
@@ -181,7 +197,7 @@ export function SharedDataProvider(props: { children: JSX.Element }): JSX.Elemen
|
|
|
181
197
|
const listenerKey = listenerKeyMap.get(name);
|
|
182
198
|
if (listenerKey != null) {
|
|
183
199
|
const def = currentDefinitions[name];
|
|
184
|
-
const client = serviceClient.get(def.serviceKey);
|
|
200
|
+
const client = serviceClient.get(def.serviceKey ?? "default");
|
|
185
201
|
void client.removeEventListener(listenerKey);
|
|
186
202
|
}
|
|
187
203
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"CrudDetail.d.ts","sourceRoot":"","sources":["..\\..\\..\\..\\src\\components\\data\\crud-detail\\CrudDetail.tsx"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,GAAG,EAKT,MAAM,UAAU,CAAC;AAoBlB,OAAO,EAAE,eAAe,EAAwB,MAAM,mBAAmB,CAAC;AAC1E,OAAO,EAAE,gBAAgB,EAAyB,MAAM,oBAAoB,CAAC;AAC7E,OAAO,EAAE,eAAe,EAAwB,MAAM,mBAAmB,CAAC;AAC1E,OAAO,KAAK,EAKV,eAAe,EAEhB,MAAM,SAAS,CAAC;AAEjB,UAAU,mBAAmB;IAC3B,CAAC,KAAK,SAAS,MAAM,EAAE,KAAK,EAAE,eAAe,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,OAAO,CAAC;IACnE,KAAK,EAAE,OAAO,eAAe,CAAC;IAC9B,MAAM,EAAE,OAAO,gBAAgB,CAAC;IAChC,KAAK,EAAE,OAAO,eAAe,CAAC;CAC/B;AA6SD,eAAO,MAAM,UAAU,EAAgC,mBAAmB,CAAC"}
|