@simplysm/solid 13.0.33 → 13.0.35
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 +22 -42
- package/dist/components/disclosure/DialogContext.d.ts +29 -0
- package/dist/components/disclosure/DialogContext.d.ts.map +1 -1
- package/dist/components/disclosure/DialogContext.js.map +1 -1
- package/dist/components/disclosure/DialogInstanceContext.d.ts +14 -0
- package/dist/components/disclosure/DialogInstanceContext.d.ts.map +1 -1
- package/dist/components/disclosure/DialogInstanceContext.js.map +1 -1
- package/dist/components/feedback/busy/BusyContext.d.ts +18 -0
- package/dist/components/feedback/busy/BusyContext.d.ts.map +1 -1
- package/dist/components/feedback/busy/BusyContext.js.map +1 -1
- package/dist/components/feedback/busy/BusyProvider.d.ts +10 -0
- package/dist/components/feedback/busy/BusyProvider.d.ts.map +1 -1
- package/dist/components/feedback/busy/BusyProvider.js.map +1 -1
- package/dist/components/feedback/notification/NotificationContext.d.ts +29 -0
- package/dist/components/feedback/notification/NotificationContext.d.ts.map +1 -1
- package/dist/components/feedback/notification/NotificationContext.js.map +1 -1
- package/dist/components/feedback/notification/NotificationProvider.d.ts +9 -0
- package/dist/components/feedback/notification/NotificationProvider.d.ts.map +1 -1
- package/dist/components/feedback/notification/NotificationProvider.js.map +1 -1
- package/dist/hooks/useLogger.d.ts +4 -2
- package/dist/hooks/useLogger.d.ts.map +1 -1
- package/dist/hooks/useLogger.js +11 -4
- package/dist/hooks/useLogger.js.map +1 -1
- package/dist/hooks/useSyncConfig.d.ts +2 -0
- package/dist/hooks/useSyncConfig.d.ts.map +1 -1
- package/dist/hooks/useSyncConfig.js +30 -26
- package/dist/hooks/useSyncConfig.js.map +1 -1
- package/dist/index.d.ts +8 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -15
- package/dist/index.js.map +1 -1
- package/dist/providers/InitializeProvider.d.ts +33 -0
- package/dist/providers/InitializeProvider.d.ts.map +1 -0
- package/dist/providers/InitializeProvider.js +75 -0
- package/dist/providers/InitializeProvider.js.map +6 -0
- package/dist/providers/LoggerContext.d.ts +24 -8
- package/dist/providers/LoggerContext.d.ts.map +1 -1
- package/dist/providers/LoggerContext.js +13 -13
- package/dist/providers/LoggerContext.js.map +2 -2
- package/dist/providers/ServiceClientContext.d.ts +13 -0
- package/dist/providers/ServiceClientContext.d.ts.map +1 -1
- package/dist/providers/ServiceClientContext.js.map +1 -1
- package/dist/providers/ServiceClientProvider.d.ts +21 -0
- package/dist/providers/ServiceClientProvider.d.ts.map +1 -1
- package/dist/providers/ServiceClientProvider.js.map +1 -1
- package/dist/providers/SyncStorageContext.d.ts +25 -11
- package/dist/providers/SyncStorageContext.d.ts.map +1 -1
- package/dist/providers/SyncStorageContext.js +13 -13
- package/dist/providers/SyncStorageContext.js.map +2 -2
- package/dist/providers/shared-data/SharedDataChangeEvent.d.ts +8 -0
- package/dist/providers/shared-data/SharedDataChangeEvent.d.ts.map +1 -1
- package/dist/providers/shared-data/SharedDataChangeEvent.js.map +1 -1
- package/dist/providers/shared-data/SharedDataContext.d.ts +39 -0
- package/dist/providers/shared-data/SharedDataContext.d.ts.map +1 -1
- package/dist/providers/shared-data/SharedDataContext.js +1 -3
- package/dist/providers/shared-data/SharedDataContext.js.map +1 -1
- package/dist/providers/shared-data/SharedDataProvider.d.ts +30 -5
- package/dist/providers/shared-data/SharedDataProvider.d.ts.map +1 -1
- package/dist/providers/shared-data/SharedDataProvider.js +59 -38
- package/dist/providers/shared-data/SharedDataProvider.js.map +2 -2
- package/docs/providers.md +70 -195
- package/package.json +3 -3
- package/src/components/disclosure/DialogContext.ts +29 -0
- package/src/components/disclosure/DialogInstanceContext.ts +14 -0
- package/src/components/feedback/busy/BusyContext.ts +18 -0
- package/src/components/feedback/busy/BusyProvider.tsx +10 -0
- package/src/components/feedback/notification/NotificationContext.ts +29 -0
- package/src/components/feedback/notification/NotificationProvider.tsx +9 -0
- package/src/hooks/useLogger.ts +14 -4
- package/src/hooks/useSyncConfig.ts +42 -35
- package/src/index.ts +34 -14
- package/src/providers/InitializeProvider.tsx +74 -0
- package/src/providers/LoggerContext.tsx +39 -10
- package/src/providers/ServiceClientContext.ts +13 -0
- package/src/providers/ServiceClientProvider.tsx +21 -0
- package/src/providers/SyncStorageContext.tsx +40 -15
- package/src/providers/shared-data/SharedDataChangeEvent.ts +8 -0
- package/src/providers/shared-data/SharedDataContext.ts +40 -3
- package/src/providers/shared-data/SharedDataProvider.tsx +102 -54
|
@@ -1,40 +1,69 @@
|
|
|
1
1
|
import { createContext, useContext, type Accessor, type JSX } from "solid-js";
|
|
2
2
|
|
|
3
|
+
/** 다이얼로그 기본 설정 */
|
|
3
4
|
export interface DialogDefaults {
|
|
5
|
+
/** ESC 키로 닫기 허용 */
|
|
4
6
|
closeOnEscape?: boolean;
|
|
7
|
+
/** 백드롭 클릭으로 닫기 허용 */
|
|
5
8
|
closeOnBackdrop?: boolean;
|
|
6
9
|
}
|
|
7
10
|
|
|
11
|
+
/** 다이얼로그 기본 설정 Context */
|
|
8
12
|
export const DialogDefaultsContext = createContext<Accessor<DialogDefaults>>();
|
|
9
13
|
|
|
14
|
+
/** 프로그래매틱 다이얼로그 옵션 */
|
|
10
15
|
export interface DialogShowOptions {
|
|
16
|
+
/** 다이얼로그 제목 */
|
|
11
17
|
title: string;
|
|
18
|
+
/** 헤더 숨김 */
|
|
12
19
|
hideHeader?: boolean;
|
|
20
|
+
/** 닫기 버튼 표시 */
|
|
13
21
|
closable?: boolean;
|
|
22
|
+
/** 백드롭 클릭으로 닫기 */
|
|
14
23
|
closeOnBackdrop?: boolean;
|
|
24
|
+
/** ESC 키로 닫기 */
|
|
15
25
|
closeOnEscape?: boolean;
|
|
26
|
+
/** 크기 조절 가능 */
|
|
16
27
|
resizable?: boolean;
|
|
28
|
+
/** 드래그 이동 가능 */
|
|
17
29
|
movable?: boolean;
|
|
30
|
+
/** 플로팅 모드 (우하단 고정) */
|
|
18
31
|
float?: boolean;
|
|
32
|
+
/** 전체 화면 채우기 */
|
|
19
33
|
fill?: boolean;
|
|
34
|
+
/** 초기 너비 (px) */
|
|
20
35
|
width?: number;
|
|
36
|
+
/** 초기 높이 (px) */
|
|
21
37
|
height?: number;
|
|
38
|
+
/** 최소 너비 (px) */
|
|
22
39
|
minWidth?: number;
|
|
40
|
+
/** 최소 높이 (px) */
|
|
23
41
|
minHeight?: number;
|
|
42
|
+
/** 플로팅 위치 */
|
|
24
43
|
position?: "bottom-right" | "top-right";
|
|
44
|
+
/** 헤더 커스텀 스타일 */
|
|
25
45
|
headerStyle?: JSX.CSSProperties | string;
|
|
46
|
+
/** 닫기 전 확인 함수 (false 반환 시 닫기 취소) */
|
|
26
47
|
canDeactivate?: () => boolean;
|
|
27
48
|
}
|
|
28
49
|
|
|
50
|
+
/** 프로그래매틱 다이얼로그 Context 값 */
|
|
29
51
|
export interface DialogContextValue {
|
|
52
|
+
/** 다이얼로그를 열고, 닫힐 때까지 대기하여 결과를 반환 */
|
|
30
53
|
show<T = undefined>(
|
|
31
54
|
factory: () => JSX.Element,
|
|
32
55
|
options: DialogShowOptions,
|
|
33
56
|
): Promise<T | undefined>;
|
|
34
57
|
}
|
|
35
58
|
|
|
59
|
+
/** 프로그래매틱 다이얼로그 Context */
|
|
36
60
|
export const DialogContext = createContext<DialogContextValue>();
|
|
37
61
|
|
|
62
|
+
/**
|
|
63
|
+
* 프로그래매틱 다이얼로그에 접근하는 훅
|
|
64
|
+
*
|
|
65
|
+
* @throws DialogProvider가 없으면 에러 발생
|
|
66
|
+
*/
|
|
38
67
|
export function useDialog(): DialogContextValue {
|
|
39
68
|
const ctx = useContext(DialogContext);
|
|
40
69
|
if (!ctx) throw new Error("useDialog는 DialogProvider 내부에서만 사용할 수 있습니다");
|
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import { createContext, useContext } from "solid-js";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* 다이얼로그 인스턴스 (프로그래매틱 다이얼로그 내부에서 사용)
|
|
5
|
+
*/
|
|
3
6
|
export interface DialogInstance<TResult> {
|
|
7
|
+
/** 다이얼로그 닫기 (result는 show()의 Promise로 전달) */
|
|
4
8
|
close: (result?: TResult) => void;
|
|
5
9
|
}
|
|
6
10
|
|
|
11
|
+
/** 다이얼로그 인스턴스 Context */
|
|
7
12
|
export const DialogInstanceContext = createContext<DialogInstance<unknown>>();
|
|
8
13
|
|
|
14
|
+
/**
|
|
15
|
+
* 다이얼로그 인스턴스에 접근하는 훅
|
|
16
|
+
*
|
|
17
|
+
* @remarks
|
|
18
|
+
* DialogProvider.show()로 열린 다이얼로그 내부에서만 값이 존재한다.
|
|
19
|
+
* Provider 외부에서 호출하면 undefined를 반환한다.
|
|
20
|
+
*
|
|
21
|
+
* @returns DialogInstance 또는 undefined (Provider 외부)
|
|
22
|
+
*/
|
|
9
23
|
export function useDialogInstance<TResult = undefined>(): DialogInstance<TResult> | undefined {
|
|
10
24
|
return useContext(DialogInstanceContext) as DialogInstance<TResult> | undefined;
|
|
11
25
|
}
|
|
@@ -1,16 +1,34 @@
|
|
|
1
1
|
import { createContext, useContext, type Accessor } from "solid-js";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Busy 오버레이 표시 방식
|
|
5
|
+
* - `spinner`: 전체 화면 스피너
|
|
6
|
+
* - `bar`: 상단 프로그레스 바
|
|
7
|
+
*/
|
|
3
8
|
export type BusyVariant = "spinner" | "bar";
|
|
4
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Busy 오버레이 Context 값
|
|
12
|
+
*/
|
|
5
13
|
export interface BusyContextValue {
|
|
14
|
+
/** 현재 표시 방식 */
|
|
6
15
|
variant: Accessor<BusyVariant>;
|
|
16
|
+
/** 오버레이 표시 (중첩 호출 가능, 호출 횟수만큼 hide 필요) */
|
|
7
17
|
show: (message?: string) => void;
|
|
18
|
+
/** 오버레이 숨김 (모든 show에 대응하는 hide 호출 후 실제 숨김) */
|
|
8
19
|
hide: () => void;
|
|
20
|
+
/** 프로그레스 바 진행률 설정 (0~100, undefined면 indeterminate) */
|
|
9
21
|
setProgress: (percent: number | undefined) => void;
|
|
10
22
|
}
|
|
11
23
|
|
|
24
|
+
/** Busy 오버레이 Context */
|
|
12
25
|
export const BusyContext = createContext<BusyContextValue>();
|
|
13
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Busy 오버레이에 접근하는 훅
|
|
29
|
+
*
|
|
30
|
+
* @throws BusyProvider가 없으면 에러 발생
|
|
31
|
+
*/
|
|
14
32
|
export function useBusy(): BusyContextValue {
|
|
15
33
|
const context = useContext(BusyContext);
|
|
16
34
|
if (!context) {
|
|
@@ -6,10 +6,20 @@ import { BusyContainer } from "./BusyContainer";
|
|
|
6
6
|
|
|
7
7
|
const overlayClass = clsx("fixed left-0 top-0", "h-screen w-screen", "overflow-hidden");
|
|
8
8
|
|
|
9
|
+
/** BusyProvider 설정 */
|
|
9
10
|
export interface BusyProviderProps {
|
|
11
|
+
/** 표시 방식 (기본값: `"spinner"`) */
|
|
10
12
|
variant?: BusyVariant;
|
|
11
13
|
}
|
|
12
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Busy 오버레이 Provider
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* - show/hide는 중첩 호출 가능 (내부 카운터로 관리)
|
|
20
|
+
* - Portal로 렌더링하여 항상 최상위에 표시
|
|
21
|
+
* - 독립적으로 동작 (다른 Provider 의존성 없음)
|
|
22
|
+
*/
|
|
13
23
|
export const BusyProvider: ParentComponent<BusyProviderProps> = (props) => {
|
|
14
24
|
const [busyCount, setBusyCount] = createSignal(0);
|
|
15
25
|
const [message, setMessage] = createSignal<string | undefined>();
|
|
@@ -1,30 +1,53 @@
|
|
|
1
1
|
import { createContext, useContext, type Accessor } from "solid-js";
|
|
2
2
|
|
|
3
|
+
/** 알림 테마 */
|
|
3
4
|
export type NotificationTheme = "info" | "success" | "warning" | "danger";
|
|
4
5
|
|
|
6
|
+
/** 알림 액션 버튼 */
|
|
5
7
|
export interface NotificationAction {
|
|
8
|
+
/** 버튼 텍스트 */
|
|
6
9
|
label: string;
|
|
10
|
+
/** 클릭 핸들러 */
|
|
7
11
|
onClick: () => void;
|
|
8
12
|
}
|
|
9
13
|
|
|
14
|
+
/** 알림 항목 */
|
|
10
15
|
export interface NotificationItem {
|
|
16
|
+
/** 고유 식별자 */
|
|
11
17
|
id: string;
|
|
18
|
+
/** 테마 (info, success, warning, danger) */
|
|
12
19
|
theme: NotificationTheme;
|
|
20
|
+
/** 알림 제목 */
|
|
13
21
|
title: string;
|
|
22
|
+
/** 알림 메시지 (선택) */
|
|
14
23
|
message?: string;
|
|
24
|
+
/** 액션 버튼 (선택) */
|
|
15
25
|
action?: NotificationAction;
|
|
26
|
+
/** 생성 시각 */
|
|
16
27
|
createdAt: Date;
|
|
28
|
+
/** 읽음 여부 */
|
|
17
29
|
read: boolean;
|
|
18
30
|
}
|
|
19
31
|
|
|
32
|
+
/** 알림 생성 옵션 */
|
|
20
33
|
export interface NotificationOptions {
|
|
34
|
+
/** 알림에 표시할 액션 버튼 */
|
|
21
35
|
action?: NotificationAction;
|
|
22
36
|
}
|
|
23
37
|
|
|
38
|
+
/** 알림 수정 옵션 */
|
|
24
39
|
export interface NotificationUpdateOptions {
|
|
40
|
+
/** true면 읽은 알림을 다시 읽지 않음 상태로 변경 (배너 재표시) */
|
|
25
41
|
renotify?: boolean;
|
|
26
42
|
}
|
|
27
43
|
|
|
44
|
+
/**
|
|
45
|
+
* 알림 시스템 Context 값
|
|
46
|
+
*
|
|
47
|
+
* @remarks
|
|
48
|
+
* 알림 생성, 수정, 삭제 및 읽음 관리를 위한 메서드 제공.
|
|
49
|
+
* 최대 50개까지 유지되며 초과 시 오래된 항목부터 제거.
|
|
50
|
+
*/
|
|
28
51
|
export interface NotificationContextValue {
|
|
29
52
|
// 상태
|
|
30
53
|
items: Accessor<NotificationItem[]>;
|
|
@@ -60,8 +83,14 @@ export interface NotificationContextValue {
|
|
|
60
83
|
clear: () => void;
|
|
61
84
|
}
|
|
62
85
|
|
|
86
|
+
/** 알림 시스템 Context */
|
|
63
87
|
export const NotificationContext = createContext<NotificationContextValue>();
|
|
64
88
|
|
|
89
|
+
/**
|
|
90
|
+
* 알림 시스템에 접근하는 훅
|
|
91
|
+
*
|
|
92
|
+
* @throws NotificationProvider가 없으면 에러 발생
|
|
93
|
+
*/
|
|
65
94
|
export function useNotification(): NotificationContextValue {
|
|
66
95
|
const context = useContext(NotificationContext);
|
|
67
96
|
if (!context) {
|
|
@@ -11,6 +11,15 @@ import { useLogger } from "../../../hooks/useLogger";
|
|
|
11
11
|
|
|
12
12
|
const MAX_ITEMS = 50;
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* 알림 시스템 Provider
|
|
16
|
+
*
|
|
17
|
+
* @remarks
|
|
18
|
+
* - 최대 50개 알림 유지 (초과 시 오래된 항목 자동 제거)
|
|
19
|
+
* - 읽지 않은 최신 알림을 배너로 표시
|
|
20
|
+
* - 스크린 리더용 aria-live region 포함
|
|
21
|
+
* - LoggerProvider가 있으면 에러 알림을 로거에도 기록
|
|
22
|
+
*/
|
|
14
23
|
export const NotificationProvider: ParentComponent = (props) => {
|
|
15
24
|
const logger = useLogger();
|
|
16
25
|
const [items, setItems] = createSignal<NotificationItem[]>([]);
|
package/src/hooks/useLogger.ts
CHANGED
|
@@ -3,20 +3,24 @@ import { useLogAdapter, type LogAdapter } from "../providers/LoggerContext";
|
|
|
3
3
|
|
|
4
4
|
type LogLevel = Parameters<LogAdapter["write"]>[0];
|
|
5
5
|
|
|
6
|
-
interface Logger {
|
|
6
|
+
export interface Logger {
|
|
7
7
|
log: (...args: unknown[]) => void;
|
|
8
8
|
info: (...args: unknown[]) => void;
|
|
9
9
|
warn: (...args: unknown[]) => void;
|
|
10
10
|
error: (...args: unknown[]) => void;
|
|
11
|
+
/** LogAdapter를 나중에 주입. LoggerProvider 내부에서만 사용 가능 */
|
|
12
|
+
configure: (adapter: LogAdapter) => void;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
export function useLogger(): Logger {
|
|
14
|
-
const
|
|
16
|
+
const loggerCtx = useLogAdapter();
|
|
15
17
|
|
|
16
18
|
const createLogFunction = (level: LogLevel) => {
|
|
17
19
|
return (...args: unknown[]) => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
// Lazy read: 매 호출마다 현재 adapter를 확인
|
|
21
|
+
const adapter = loggerCtx?.adapter();
|
|
22
|
+
if (adapter) {
|
|
23
|
+
void adapter.write(level, ...args);
|
|
20
24
|
} else {
|
|
21
25
|
(consola as any)[level](...args);
|
|
22
26
|
}
|
|
@@ -28,5 +32,11 @@ export function useLogger(): Logger {
|
|
|
28
32
|
info: createLogFunction("info"),
|
|
29
33
|
warn: createLogFunction("warn"),
|
|
30
34
|
error: createLogFunction("error"),
|
|
35
|
+
configure: (adapter: LogAdapter) => {
|
|
36
|
+
if (!loggerCtx) {
|
|
37
|
+
throw new Error("configure()는 LoggerProvider 내부에서만 사용할 수 있습니다");
|
|
38
|
+
}
|
|
39
|
+
loggerCtx.configure(adapter);
|
|
40
|
+
},
|
|
31
41
|
};
|
|
32
42
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Accessor, type Setter, createEffect, createSignal } from "solid-js";
|
|
1
|
+
import { type Accessor, type Setter, createEffect, createSignal, untrack } from "solid-js";
|
|
2
2
|
import { useConfig } from "../providers/ConfigContext";
|
|
3
3
|
import { useSyncStorage } from "../providers/SyncStorageContext";
|
|
4
4
|
|
|
@@ -8,6 +8,8 @@ import { useSyncStorage } from "../providers/SyncStorageContext";
|
|
|
8
8
|
* Uses `SyncStorageProvider` storage if available, otherwise falls back to `localStorage`.
|
|
9
9
|
* Designed for data that should persist and sync across devices (e.g., theme, user preferences, DataSheet configs).
|
|
10
10
|
*
|
|
11
|
+
* When the adapter changes via `useSyncStorage().configure()`, re-reads from the new adapter.
|
|
12
|
+
*
|
|
11
13
|
* @param key - Storage key for the config value
|
|
12
14
|
* @param defaultValue - Default value if no stored value exists
|
|
13
15
|
* @returns Tuple of [value accessor, value setter, ready state accessor]
|
|
@@ -28,50 +30,52 @@ export function useSyncConfig<TValue>(
|
|
|
28
30
|
defaultValue: TValue,
|
|
29
31
|
): [Accessor<TValue>, Setter<TValue>, Accessor<boolean>] {
|
|
30
32
|
const config = useConfig();
|
|
31
|
-
const
|
|
33
|
+
const syncStorageCtx = useSyncStorage();
|
|
32
34
|
const prefixedKey = `${config.clientName}.${key}`;
|
|
33
35
|
const [value, setValue] = createSignal<TValue>(defaultValue);
|
|
34
36
|
const [ready, setReady] = createSignal(false);
|
|
35
37
|
|
|
36
|
-
// Initialize from storage
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
// Initialize from storage (reactive to adapter changes via configure())
|
|
39
|
+
createEffect(() => {
|
|
40
|
+
const currentAdapter = syncStorageCtx?.adapter();
|
|
41
|
+
setReady(false);
|
|
42
|
+
|
|
43
|
+
void (async () => {
|
|
44
|
+
if (!currentAdapter) {
|
|
45
|
+
// Use localStorage synchronously
|
|
46
|
+
try {
|
|
47
|
+
const stored = localStorage.getItem(prefixedKey);
|
|
48
|
+
if (stored !== null) {
|
|
49
|
+
setValue(() => JSON.parse(stored) as TValue);
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
// Ignore parse errors, keep default value
|
|
44
53
|
}
|
|
45
|
-
|
|
46
|
-
|
|
54
|
+
setReady(true);
|
|
55
|
+
return;
|
|
47
56
|
}
|
|
48
|
-
setReady(true);
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
57
|
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
const stored = await syncStorage.getItem(prefixedKey);
|
|
55
|
-
if (stored !== null) {
|
|
56
|
-
setValue(() => JSON.parse(stored) as TValue);
|
|
57
|
-
}
|
|
58
|
-
} catch {
|
|
59
|
-
// Fall back to localStorage on error
|
|
58
|
+
// Use custom adapter asynchronously
|
|
60
59
|
try {
|
|
61
|
-
const stored =
|
|
60
|
+
const stored = await currentAdapter.getItem(prefixedKey);
|
|
62
61
|
if (stored !== null) {
|
|
63
62
|
setValue(() => JSON.parse(stored) as TValue);
|
|
64
63
|
}
|
|
65
64
|
} catch {
|
|
66
|
-
//
|
|
65
|
+
// Fall back to localStorage on error
|
|
66
|
+
try {
|
|
67
|
+
const stored = localStorage.getItem(prefixedKey);
|
|
68
|
+
if (stored !== null) {
|
|
69
|
+
setValue(() => JSON.parse(stored) as TValue);
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// Ignore parse errors
|
|
73
|
+
}
|
|
74
|
+
} finally {
|
|
75
|
+
setReady(true);
|
|
67
76
|
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
// Initialize on mount
|
|
74
|
-
void initializeFromStorage();
|
|
77
|
+
})();
|
|
78
|
+
});
|
|
75
79
|
|
|
76
80
|
// Save to storage whenever value changes
|
|
77
81
|
createEffect(() => {
|
|
@@ -79,16 +83,19 @@ export function useSyncConfig<TValue>(
|
|
|
79
83
|
const currentValue = value();
|
|
80
84
|
const serialized = JSON.stringify(currentValue);
|
|
81
85
|
|
|
82
|
-
|
|
86
|
+
// Read adapter untracked to avoid re-running save effect when adapter changes
|
|
87
|
+
const currentAdapter = untrack(() => syncStorageCtx?.adapter());
|
|
88
|
+
|
|
89
|
+
if (!currentAdapter) {
|
|
83
90
|
// Use localStorage synchronously
|
|
84
91
|
localStorage.setItem(prefixedKey, serialized);
|
|
85
92
|
return;
|
|
86
93
|
}
|
|
87
94
|
|
|
88
|
-
// Use
|
|
95
|
+
// Use custom adapter asynchronously
|
|
89
96
|
void (async () => {
|
|
90
97
|
try {
|
|
91
|
-
await
|
|
98
|
+
await currentAdapter.setItem(prefixedKey, serialized);
|
|
92
99
|
} catch {
|
|
93
100
|
// Fall back to localStorage on error
|
|
94
101
|
localStorage.setItem(prefixedKey, serialized);
|
package/src/index.ts
CHANGED
|
@@ -82,7 +82,6 @@ export * from "./components/disclosure/Dropdown";
|
|
|
82
82
|
export * from "./components/disclosure/Dialog";
|
|
83
83
|
export * from "./components/disclosure/DialogContext";
|
|
84
84
|
export * from "./components/disclosure/DialogInstanceContext";
|
|
85
|
-
export * from "./components/disclosure/DialogProvider";
|
|
86
85
|
export * from "./components/disclosure/Tabs";
|
|
87
86
|
|
|
88
87
|
//#endregion
|
|
@@ -92,13 +91,11 @@ export * from "./components/disclosure/Tabs";
|
|
|
92
91
|
// Notification
|
|
93
92
|
export * from "./components/feedback/notification/NotificationContext";
|
|
94
93
|
export * from "./components/feedback/notification/NotificationBell";
|
|
95
|
-
export * from "./components/feedback/notification/NotificationProvider";
|
|
96
94
|
export * from "./components/feedback/notification/NotificationBanner";
|
|
97
95
|
|
|
98
96
|
// Busy
|
|
99
97
|
export * from "./components/feedback/busy/BusyContext";
|
|
100
98
|
export * from "./components/feedback/busy/BusyContainer";
|
|
101
|
-
export * from "./components/feedback/busy/BusyProvider";
|
|
102
99
|
|
|
103
100
|
// Print
|
|
104
101
|
export * from "./components/feedback/print/Print";
|
|
@@ -109,20 +106,43 @@ export * from "./components/feedback/Progress";
|
|
|
109
106
|
|
|
110
107
|
//#region ========== Providers ==========
|
|
111
108
|
|
|
112
|
-
|
|
113
|
-
export
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
export
|
|
117
|
-
|
|
118
|
-
|
|
109
|
+
// Config
|
|
110
|
+
export { type AppConfig, ConfigContext, useConfig } from "./providers/ConfigContext";
|
|
111
|
+
|
|
112
|
+
// SyncStorage
|
|
113
|
+
export {
|
|
114
|
+
type StorageAdapter,
|
|
115
|
+
type SyncStorageContextValue,
|
|
116
|
+
SyncStorageContext,
|
|
117
|
+
useSyncStorage,
|
|
118
|
+
} from "./providers/SyncStorageContext";
|
|
119
|
+
|
|
120
|
+
// Logger
|
|
121
|
+
export { type LogAdapter, type LoggerContextValue } from "./providers/LoggerContext";
|
|
122
|
+
|
|
123
|
+
// Theme
|
|
124
|
+
export { useTheme } from "./providers/ThemeContext";
|
|
119
125
|
export type { ThemeMode, ResolvedTheme } from "./providers/ThemeContext";
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
export
|
|
123
|
-
|
|
126
|
+
|
|
127
|
+
// ServiceClient
|
|
128
|
+
export {
|
|
129
|
+
type ServiceClientContextValue,
|
|
130
|
+
ServiceClientContext,
|
|
131
|
+
useServiceClient,
|
|
132
|
+
} from "./providers/ServiceClientContext";
|
|
133
|
+
|
|
134
|
+
// SharedData
|
|
135
|
+
export type {
|
|
136
|
+
SharedDataDefinition,
|
|
137
|
+
SharedDataAccessor,
|
|
138
|
+
SharedDataValue,
|
|
139
|
+
} from "./providers/shared-data/SharedDataContext";
|
|
140
|
+
export { SharedDataContext, useSharedData } from "./providers/shared-data/SharedDataContext";
|
|
124
141
|
export * from "./providers/shared-data/SharedDataChangeEvent";
|
|
125
142
|
|
|
143
|
+
// InitializeProvider (only exported provider)
|
|
144
|
+
export * from "./providers/InitializeProvider";
|
|
145
|
+
|
|
126
146
|
//#endregion
|
|
127
147
|
|
|
128
148
|
//#region ========== Hooks ==========
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { type ParentComponent } from "solid-js";
|
|
2
|
+
import { ConfigProvider } from "./ConfigContext";
|
|
3
|
+
import { SyncStorageProvider } from "./SyncStorageContext";
|
|
4
|
+
import { LoggerProvider } from "./LoggerContext";
|
|
5
|
+
import { NotificationProvider } from "../components/feedback/notification/NotificationProvider";
|
|
6
|
+
import { NotificationBanner } from "../components/feedback/notification/NotificationBanner";
|
|
7
|
+
import { ErrorLoggerProvider } from "./ErrorLoggerProvider";
|
|
8
|
+
import { PwaUpdateProvider } from "./PwaUpdateProvider";
|
|
9
|
+
import { ClipboardProvider } from "./ClipboardProvider";
|
|
10
|
+
import { ThemeProvider } from "./ThemeContext";
|
|
11
|
+
import { ServiceClientProvider } from "./ServiceClientProvider";
|
|
12
|
+
import { SharedDataProvider } from "./shared-data/SharedDataProvider";
|
|
13
|
+
import { BusyProvider } from "../components/feedback/busy/BusyProvider";
|
|
14
|
+
import { DialogProvider } from "../components/disclosure/DialogProvider";
|
|
15
|
+
import type { BusyVariant } from "../components/feedback/busy/BusyContext";
|
|
16
|
+
|
|
17
|
+
export type { BusyVariant };
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @simplysm/solid 메인 Provider
|
|
21
|
+
*
|
|
22
|
+
* @remarks
|
|
23
|
+
* - 모든 개별 Provider를 올바른 의존성 순서로 네스팅
|
|
24
|
+
* - `clientName`만 prop으로 전달하고, 나머지 설정은 각 hook의 `configure()`로 주입
|
|
25
|
+
* - 개별 Provider를 직접 조합할 필요 없이 이 Provider 하나로 앱을 감싸면 됨
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```tsx
|
|
29
|
+
* <InitializeProvider clientName="my-app">
|
|
30
|
+
* <AppRoot />
|
|
31
|
+
* </InitializeProvider>
|
|
32
|
+
*
|
|
33
|
+
* function AppRoot() {
|
|
34
|
+
* const serviceClient = useServiceClient();
|
|
35
|
+
* onMount(async () => {
|
|
36
|
+
* await serviceClient.connect("main", { port: 3000 });
|
|
37
|
+
* useSyncStorage()!.configure(myStorageAdapter);
|
|
38
|
+
* useLogger().configure(myLogAdapter);
|
|
39
|
+
* useSharedData().configure(definitions);
|
|
40
|
+
* });
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export const InitializeProvider: ParentComponent<{
|
|
45
|
+
clientName: string;
|
|
46
|
+
busyVariant?: BusyVariant;
|
|
47
|
+
}> = (props) => {
|
|
48
|
+
return (
|
|
49
|
+
<ConfigProvider clientName={props.clientName}>
|
|
50
|
+
<SyncStorageProvider>
|
|
51
|
+
<LoggerProvider>
|
|
52
|
+
<NotificationProvider>
|
|
53
|
+
<NotificationBanner />
|
|
54
|
+
<ErrorLoggerProvider>
|
|
55
|
+
<PwaUpdateProvider>
|
|
56
|
+
<ClipboardProvider>
|
|
57
|
+
<ThemeProvider>
|
|
58
|
+
<ServiceClientProvider>
|
|
59
|
+
<SharedDataProvider>
|
|
60
|
+
<BusyProvider variant={props.busyVariant}>
|
|
61
|
+
<DialogProvider>{props.children}</DialogProvider>
|
|
62
|
+
</BusyProvider>
|
|
63
|
+
</SharedDataProvider>
|
|
64
|
+
</ServiceClientProvider>
|
|
65
|
+
</ThemeProvider>
|
|
66
|
+
</ClipboardProvider>
|
|
67
|
+
</PwaUpdateProvider>
|
|
68
|
+
</ErrorLoggerProvider>
|
|
69
|
+
</NotificationProvider>
|
|
70
|
+
</LoggerProvider>
|
|
71
|
+
</SyncStorageProvider>
|
|
72
|
+
</ConfigProvider>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type Accessor,
|
|
3
|
+
createContext,
|
|
4
|
+
createSignal,
|
|
5
|
+
useContext,
|
|
6
|
+
type ParentComponent,
|
|
7
|
+
} from "solid-js";
|
|
2
8
|
|
|
3
9
|
/**
|
|
4
10
|
* 로그 어댑터 인터페이스
|
|
@@ -11,36 +17,59 @@ export interface LogAdapter {
|
|
|
11
17
|
write(severity: "error" | "warn" | "info" | "log", ...data: any[]): Promise<void> | void;
|
|
12
18
|
}
|
|
13
19
|
|
|
20
|
+
/**
|
|
21
|
+
* 로그 어댑터 Context 값
|
|
22
|
+
*
|
|
23
|
+
* @remarks
|
|
24
|
+
* - `adapter`: 현재 설정된 LogAdapter (signal). configure 전에는 undefined
|
|
25
|
+
* - `configure`: adapter를 나중에 주입하는 함수
|
|
26
|
+
*/
|
|
27
|
+
export interface LoggerContextValue {
|
|
28
|
+
adapter: Accessor<LogAdapter | undefined>;
|
|
29
|
+
configure: (adapter: LogAdapter) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
14
32
|
/**
|
|
15
33
|
* 로그 어댑터 Context
|
|
16
34
|
*
|
|
17
35
|
* @remarks
|
|
18
36
|
* Provider가 없으면 `undefined` (useLogger에서 consola로 fallback)
|
|
19
37
|
*/
|
|
20
|
-
export const LoggerContext = createContext<
|
|
38
|
+
export const LoggerContext = createContext<LoggerContextValue>();
|
|
21
39
|
|
|
22
40
|
/**
|
|
23
41
|
* 로그 어댑터 Context에 접근하는 훅
|
|
24
42
|
*
|
|
25
|
-
* @returns
|
|
43
|
+
* @returns LoggerContextValue 또는 undefined (Provider가 없으면)
|
|
26
44
|
*/
|
|
27
|
-
export function useLogAdapter():
|
|
45
|
+
export function useLogAdapter(): LoggerContextValue | undefined {
|
|
28
46
|
return useContext(LoggerContext);
|
|
29
47
|
}
|
|
30
48
|
|
|
31
49
|
/**
|
|
32
50
|
* 로그 어댑터 Provider
|
|
33
51
|
*
|
|
52
|
+
* @remarks
|
|
53
|
+
* - prop 없이 사용. adapter는 `useLogger().configure()`로 나중에 주입
|
|
54
|
+
* - configure 전에는 useLogger가 consola로 fallback
|
|
55
|
+
*
|
|
34
56
|
* @example
|
|
35
57
|
* ```tsx
|
|
36
|
-
* <LoggerProvider
|
|
58
|
+
* <LoggerProvider>
|
|
37
59
|
* <App />
|
|
38
60
|
* </LoggerProvider>
|
|
61
|
+
*
|
|
62
|
+
* // 자식 컴포넌트에서 나중에 설정:
|
|
63
|
+
* useLogger().configure(myLogAdapter);
|
|
39
64
|
* ```
|
|
40
65
|
*/
|
|
41
|
-
export const LoggerProvider: ParentComponent
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
66
|
+
export const LoggerProvider: ParentComponent = (props) => {
|
|
67
|
+
const [adapter, setAdapter] = createSignal<LogAdapter | undefined>();
|
|
68
|
+
|
|
69
|
+
const value: LoggerContextValue = {
|
|
70
|
+
adapter,
|
|
71
|
+
configure: (a: LogAdapter) => setAdapter(() => a),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return <LoggerContext.Provider value={value}>{props.children}</LoggerContext.Provider>;
|
|
46
75
|
};
|
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import { createContext, useContext } from "solid-js";
|
|
2
2
|
import type { ServiceClient, ServiceConnectionConfig } from "@simplysm/service-client";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* WebSocket 서비스 클라이언트 Context 값
|
|
6
|
+
*/
|
|
4
7
|
export interface ServiceClientContextValue {
|
|
8
|
+
/** WebSocket 연결 열기 (key로 다중 연결 관리) */
|
|
5
9
|
connect: (key: string, options?: Partial<ServiceConnectionConfig>) => Promise<void>;
|
|
10
|
+
/** 연결 닫기 */
|
|
6
11
|
close: (key: string) => Promise<void>;
|
|
12
|
+
/** 연결된 클라이언트 인스턴스 가져오기 (연결되지 않은 key면 에러 발생) */
|
|
7
13
|
get: (key: string) => ServiceClient;
|
|
14
|
+
/** 연결 상태 확인 */
|
|
8
15
|
isConnected: (key: string) => boolean;
|
|
9
16
|
}
|
|
10
17
|
|
|
18
|
+
/** WebSocket 서비스 클라이언트 Context */
|
|
11
19
|
export const ServiceClientContext = createContext<ServiceClientContextValue>();
|
|
12
20
|
|
|
21
|
+
/**
|
|
22
|
+
* WebSocket 서비스 클라이언트에 접근하는 훅
|
|
23
|
+
*
|
|
24
|
+
* @throws ServiceClientProvider가 없으면 에러 발생
|
|
25
|
+
*/
|
|
13
26
|
export function useServiceClient(): ServiceClientContextValue {
|
|
14
27
|
const context = useContext(ServiceClientContext);
|
|
15
28
|
if (!context) {
|