@simplysm/solid 13.0.34 → 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.
Files changed (79) hide show
  1. package/README.md +22 -42
  2. package/dist/components/disclosure/DialogContext.d.ts +29 -0
  3. package/dist/components/disclosure/DialogContext.d.ts.map +1 -1
  4. package/dist/components/disclosure/DialogContext.js.map +1 -1
  5. package/dist/components/disclosure/DialogInstanceContext.d.ts +14 -0
  6. package/dist/components/disclosure/DialogInstanceContext.d.ts.map +1 -1
  7. package/dist/components/disclosure/DialogInstanceContext.js.map +1 -1
  8. package/dist/components/feedback/busy/BusyContext.d.ts +18 -0
  9. package/dist/components/feedback/busy/BusyContext.d.ts.map +1 -1
  10. package/dist/components/feedback/busy/BusyContext.js.map +1 -1
  11. package/dist/components/feedback/busy/BusyProvider.d.ts +10 -0
  12. package/dist/components/feedback/busy/BusyProvider.d.ts.map +1 -1
  13. package/dist/components/feedback/busy/BusyProvider.js.map +1 -1
  14. package/dist/components/feedback/notification/NotificationContext.d.ts +29 -0
  15. package/dist/components/feedback/notification/NotificationContext.d.ts.map +1 -1
  16. package/dist/components/feedback/notification/NotificationContext.js.map +1 -1
  17. package/dist/components/feedback/notification/NotificationProvider.d.ts +9 -0
  18. package/dist/components/feedback/notification/NotificationProvider.d.ts.map +1 -1
  19. package/dist/components/feedback/notification/NotificationProvider.js.map +1 -1
  20. package/dist/hooks/useLogger.d.ts +4 -2
  21. package/dist/hooks/useLogger.d.ts.map +1 -1
  22. package/dist/hooks/useLogger.js +11 -4
  23. package/dist/hooks/useLogger.js.map +1 -1
  24. package/dist/hooks/useSyncConfig.d.ts +2 -0
  25. package/dist/hooks/useSyncConfig.d.ts.map +1 -1
  26. package/dist/hooks/useSyncConfig.js +30 -26
  27. package/dist/hooks/useSyncConfig.js.map +1 -1
  28. package/dist/index.d.ts +8 -14
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +21 -15
  31. package/dist/index.js.map +1 -1
  32. package/dist/providers/InitializeProvider.d.ts +33 -0
  33. package/dist/providers/InitializeProvider.d.ts.map +1 -0
  34. package/dist/providers/InitializeProvider.js +75 -0
  35. package/dist/providers/InitializeProvider.js.map +6 -0
  36. package/dist/providers/LoggerContext.d.ts +24 -8
  37. package/dist/providers/LoggerContext.d.ts.map +1 -1
  38. package/dist/providers/LoggerContext.js +13 -13
  39. package/dist/providers/LoggerContext.js.map +2 -2
  40. package/dist/providers/ServiceClientContext.d.ts +13 -0
  41. package/dist/providers/ServiceClientContext.d.ts.map +1 -1
  42. package/dist/providers/ServiceClientContext.js.map +1 -1
  43. package/dist/providers/ServiceClientProvider.d.ts +21 -0
  44. package/dist/providers/ServiceClientProvider.d.ts.map +1 -1
  45. package/dist/providers/ServiceClientProvider.js.map +1 -1
  46. package/dist/providers/SyncStorageContext.d.ts +25 -11
  47. package/dist/providers/SyncStorageContext.d.ts.map +1 -1
  48. package/dist/providers/SyncStorageContext.js +13 -13
  49. package/dist/providers/SyncStorageContext.js.map +2 -2
  50. package/dist/providers/shared-data/SharedDataChangeEvent.d.ts +8 -0
  51. package/dist/providers/shared-data/SharedDataChangeEvent.d.ts.map +1 -1
  52. package/dist/providers/shared-data/SharedDataChangeEvent.js.map +1 -1
  53. package/dist/providers/shared-data/SharedDataContext.d.ts +39 -0
  54. package/dist/providers/shared-data/SharedDataContext.d.ts.map +1 -1
  55. package/dist/providers/shared-data/SharedDataContext.js +1 -3
  56. package/dist/providers/shared-data/SharedDataContext.js.map +1 -1
  57. package/dist/providers/shared-data/SharedDataProvider.d.ts +30 -5
  58. package/dist/providers/shared-data/SharedDataProvider.d.ts.map +1 -1
  59. package/dist/providers/shared-data/SharedDataProvider.js +59 -38
  60. package/dist/providers/shared-data/SharedDataProvider.js.map +2 -2
  61. package/docs/providers.md +70 -195
  62. package/package.json +3 -3
  63. package/src/components/disclosure/DialogContext.ts +29 -0
  64. package/src/components/disclosure/DialogInstanceContext.ts +14 -0
  65. package/src/components/feedback/busy/BusyContext.ts +18 -0
  66. package/src/components/feedback/busy/BusyProvider.tsx +10 -0
  67. package/src/components/feedback/notification/NotificationContext.ts +29 -0
  68. package/src/components/feedback/notification/NotificationProvider.tsx +9 -0
  69. package/src/hooks/useLogger.ts +14 -4
  70. package/src/hooks/useSyncConfig.ts +42 -35
  71. package/src/index.ts +34 -14
  72. package/src/providers/InitializeProvider.tsx +74 -0
  73. package/src/providers/LoggerContext.tsx +39 -10
  74. package/src/providers/ServiceClientContext.ts +13 -0
  75. package/src/providers/ServiceClientProvider.tsx +21 -0
  76. package/src/providers/SyncStorageContext.tsx +40 -15
  77. package/src/providers/shared-data/SharedDataChangeEvent.ts +8 -0
  78. package/src/providers/shared-data/SharedDataContext.ts +40 -3
  79. 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[]>([]);
@@ -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 logAdapter = useLogAdapter();
16
+ const loggerCtx = useLogAdapter();
15
17
 
16
18
  const createLogFunction = (level: LogLevel) => {
17
19
  return (...args: unknown[]) => {
18
- if (logAdapter) {
19
- void logAdapter.write(level, ...args);
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 syncStorage = useSyncStorage();
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
- const initializeFromStorage = async () => {
38
- if (!syncStorage) {
39
- // Use localStorage synchronously
40
- try {
41
- const stored = localStorage.getItem(prefixedKey);
42
- if (stored !== null) {
43
- setValue(() => JSON.parse(stored) as TValue);
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
- } catch {
46
- // Ignore parse errors, keep default value
54
+ setReady(true);
55
+ return;
47
56
  }
48
- setReady(true);
49
- return;
50
- }
51
57
 
52
- // Use syncStorage asynchronously
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 = localStorage.getItem(prefixedKey);
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
- // Ignore parse errors
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
- } finally {
69
- setReady(true);
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
- if (!syncStorage) {
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 syncStorage asynchronously
95
+ // Use custom adapter asynchronously
89
96
  void (async () => {
90
97
  try {
91
- await syncStorage.setItem(prefixedKey, serialized);
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
- export * from "./providers/ConfigContext";
113
- export * from "./providers/SyncStorageContext";
114
- export * from "./providers/LoggerContext";
115
- export * from "./providers/ErrorLoggerProvider";
116
- export * from "./providers/PwaUpdateProvider";
117
- export * from "./providers/ClipboardProvider";
118
- export { useTheme, ThemeProvider } from "./providers/ThemeContext";
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
- export * from "./providers/ServiceClientContext";
121
- export * from "./providers/ServiceClientProvider";
122
- export * from "./providers/shared-data/SharedDataContext";
123
- export * from "./providers/shared-data/SharedDataProvider";
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 { createContext, useContext, type ParentComponent } from "solid-js";
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<LogAdapter | undefined>(undefined);
38
+ export const LoggerContext = createContext<LoggerContextValue>();
21
39
 
22
40
  /**
23
41
  * 로그 어댑터 Context에 접근하는 훅
24
42
  *
25
- * @returns LogAdapter 또는 undefined (Provider가 없으면)
43
+ * @returns LoggerContextValue 또는 undefined (Provider가 없으면)
26
44
  */
27
- export function useLogAdapter(): LogAdapter | undefined {
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 adapter={myLogAdapter}>
58
+ * <LoggerProvider>
37
59
  * <App />
38
60
  * </LoggerProvider>
61
+ *
62
+ * // 자식 컴포넌트에서 나중에 설정:
63
+ * useLogger().configure(myLogAdapter);
39
64
  * ```
40
65
  */
41
- export const LoggerProvider: ParentComponent<{ adapter: LogAdapter }> = (props) => {
42
- return (
43
- // eslint-disable-next-line solid/reactivity -- adapter는 초기 설정값으로 변경되지 않음
44
- <LoggerContext.Provider value={props.adapter}>{props.children}</LoggerContext.Provider>
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) {