@syncular/console 0.0.0

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/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@syncular/console",
3
+ "version": "0.0.0",
4
+ "description": "Embeddable Syncular console UI",
5
+ "license": "MIT",
6
+ "author": "Benjamin Kniffler",
7
+ "homepage": "https://syncular.dev",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/syncular/syncular.git",
11
+ "directory": "packages/console"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/syncular/syncular/issues"
15
+ },
16
+ "keywords": [
17
+ "sync",
18
+ "offline-first",
19
+ "realtime",
20
+ "console",
21
+ "react"
22
+ ],
23
+ "private": false,
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "type": "module",
28
+ "exports": {
29
+ ".": {
30
+ "bun": "./src/index.ts",
31
+ "browser": "./src/index.ts",
32
+ "import": {
33
+ "types": "./dist/index.d.ts",
34
+ "default": "./dist/index.js"
35
+ }
36
+ },
37
+ "./styles.css": "./src/styles/globals.css"
38
+ },
39
+ "scripts": {
40
+ "tsgo": "tsgo --noEmit",
41
+ "build": "tsgo",
42
+ "release": "bunx syncular-publish"
43
+ },
44
+ "dependencies": {
45
+ "@syncular/observability-sentry": "0.0.0",
46
+ "@syncular/transport-http": "0.0.0",
47
+ "@syncular/ui": "0.0.0",
48
+ "@tanstack/react-query": "^5.90.21",
49
+ "@tanstack/react-router": "^1.159.5",
50
+ "lucide-react": "^0.563.0"
51
+ },
52
+ "peerDependencies": {
53
+ "react": "^19.0.0",
54
+ "react-dom": "^19.0.0"
55
+ },
56
+ "devDependencies": {
57
+ "@syncular/config": "0.0.0",
58
+ "@types/react": "^19",
59
+ "@types/react-dom": "^19",
60
+ "react": "^19.2.4",
61
+ "react-dom": "^19.2.4"
62
+ },
63
+ "files": [
64
+ "dist",
65
+ "src"
66
+ ]
67
+ }
package/src/App.tsx ADDED
@@ -0,0 +1,44 @@
1
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2
+ import { createRouter, RouterProvider } from '@tanstack/react-router';
3
+ import { ConnectionProvider } from './hooks/ConnectionContext';
4
+ import { routeTree } from './routeTree';
5
+
6
+ function getInjectedBasepath(): string {
7
+ if (typeof document === 'undefined') return '/';
8
+ const meta = document.querySelector<HTMLMetaElement>(
9
+ 'meta[name="syncular-console-basepath"]'
10
+ );
11
+ const value = meta?.content?.trim();
12
+ if (!value) return '/';
13
+ if (value === '/') return '/';
14
+ return value.startsWith('/') ? value.replace(/\/$/, '') : '/';
15
+ }
16
+
17
+ const basepath = getInjectedBasepath();
18
+
19
+ const queryClient = new QueryClient({
20
+ defaultOptions: {
21
+ queries: {
22
+ staleTime: 5000,
23
+ retry: 1,
24
+ },
25
+ },
26
+ });
27
+
28
+ const router = createRouter({ routeTree, basepath });
29
+
30
+ declare module '@tanstack/react-router' {
31
+ interface Register {
32
+ router: typeof router;
33
+ }
34
+ }
35
+
36
+ export function App() {
37
+ return (
38
+ <QueryClientProvider client={queryClient}>
39
+ <ConnectionProvider>
40
+ <RouterProvider router={router} />
41
+ </ConnectionProvider>
42
+ </QueryClientProvider>
43
+ );
44
+ }
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Connection context for shared connection state across components
3
+ */
4
+
5
+ import type { SyncClient } from '@syncular/transport-http';
6
+ import {
7
+ createContext,
8
+ type ReactNode,
9
+ useCallback,
10
+ useContext,
11
+ useEffect,
12
+ useMemo,
13
+ useState,
14
+ } from 'react';
15
+ import {
16
+ type ConnectionConfig,
17
+ createConsoleClient,
18
+ testConnection,
19
+ } from '../lib/api';
20
+ import { useLocalStorage } from './useLocalStorage';
21
+
22
+ interface ConnectionState {
23
+ isConnected: boolean;
24
+ isConnecting: boolean;
25
+ error: string | null;
26
+ client: SyncClient | null;
27
+ }
28
+
29
+ interface ConnectionContextValue {
30
+ config: ConnectionConfig | null;
31
+ setConfig: (config: ConnectionConfig | null) => void;
32
+ isConnected: boolean;
33
+ isConnecting: boolean;
34
+ error: string | null;
35
+ client: SyncClient | null;
36
+ connect: (
37
+ overrideConfig?: ConnectionConfig,
38
+ options?: { persistOverride?: boolean }
39
+ ) => Promise<boolean>;
40
+ disconnect: (options?: { clearSavedConfig?: boolean }) => void;
41
+ clearError: () => void;
42
+ }
43
+
44
+ interface ConnectionProviderProps {
45
+ children: ReactNode;
46
+ defaultConfig?: ConnectionConfig | null;
47
+ }
48
+
49
+ const ConnectionContext = createContext<ConnectionContextValue | null>(null);
50
+
51
+ export function ConnectionProvider({
52
+ children,
53
+ defaultConfig = null,
54
+ }: ConnectionProviderProps) {
55
+ const [config, setConfigStorage] = useLocalStorage<ConnectionConfig | null>(
56
+ 'sync-console-connection',
57
+ null
58
+ );
59
+
60
+ const [state, setState] = useState<ConnectionState>({
61
+ isConnected: false,
62
+ isConnecting: false,
63
+ error: null,
64
+ client: null,
65
+ });
66
+
67
+ // Resolve initial config: saved config -> provided defaults
68
+ useEffect(() => {
69
+ if (config?.serverUrl?.trim() && config.token?.trim()) {
70
+ return;
71
+ }
72
+
73
+ if (defaultConfig?.serverUrl?.trim() && defaultConfig.token?.trim()) {
74
+ setConfigStorage({
75
+ serverUrl: defaultConfig.serverUrl.trim(),
76
+ token: defaultConfig.token.trim(),
77
+ });
78
+ return;
79
+ }
80
+ }, [setConfigStorage, config, defaultConfig]);
81
+
82
+ const connect = useCallback(
83
+ async (
84
+ overrideConfig?: ConnectionConfig,
85
+ options?: { persistOverride?: boolean }
86
+ ) => {
87
+ const effectiveConfig = overrideConfig ?? config;
88
+ if (!effectiveConfig) {
89
+ setState((s) => ({ ...s, error: 'No connection configured' }));
90
+ return false;
91
+ }
92
+
93
+ const normalizedConfig: ConnectionConfig = {
94
+ serverUrl: effectiveConfig.serverUrl?.trim() ?? '',
95
+ token: effectiveConfig.token?.trim() ?? '',
96
+ };
97
+
98
+ // Validate config has required fields
99
+ if (!normalizedConfig.serverUrl) {
100
+ setState((s) => ({ ...s, error: 'Server URL is required' }));
101
+ return false;
102
+ }
103
+ if (!normalizedConfig.token) {
104
+ setState((s) => ({ ...s, error: 'Token is required' }));
105
+ return false;
106
+ }
107
+
108
+ if (overrideConfig && (options?.persistOverride ?? true)) {
109
+ setConfigStorage(normalizedConfig);
110
+ }
111
+
112
+ setState((s) => ({ ...s, isConnecting: true, error: null }));
113
+
114
+ try {
115
+ const client = createConsoleClient(normalizedConfig);
116
+ const ok = await testConnection(client);
117
+
118
+ if (ok) {
119
+ setState({
120
+ isConnected: true,
121
+ isConnecting: false,
122
+ client,
123
+ error: null,
124
+ });
125
+ return true;
126
+ }
127
+ setState({
128
+ isConnected: false,
129
+ isConnecting: false,
130
+ client: null,
131
+ error: 'Failed to connect',
132
+ });
133
+ return false;
134
+ } catch (err) {
135
+ setState({
136
+ isConnected: false,
137
+ isConnecting: false,
138
+ client: null,
139
+ error: err instanceof Error ? err.message : 'Connection failed',
140
+ });
141
+ return false;
142
+ }
143
+ },
144
+ [config, setConfigStorage]
145
+ );
146
+
147
+ const disconnect = useCallback(
148
+ (options?: { clearSavedConfig?: boolean }) => {
149
+ if (options?.clearSavedConfig) {
150
+ setConfigStorage(null);
151
+ }
152
+
153
+ setState({
154
+ isConnected: false,
155
+ isConnecting: false,
156
+ client: null,
157
+ error: null,
158
+ });
159
+ },
160
+ [setConfigStorage]
161
+ );
162
+
163
+ const clearError = useCallback(() => {
164
+ setState((s) => ({ ...s, error: null }));
165
+ }, []);
166
+
167
+ const setConfig = useCallback(
168
+ (newConfig: ConnectionConfig | null) => {
169
+ setConfigStorage(newConfig);
170
+ setState({
171
+ isConnected: false,
172
+ isConnecting: false,
173
+ client: null,
174
+ error: null,
175
+ });
176
+ },
177
+ [setConfigStorage]
178
+ );
179
+
180
+ const value = useMemo(
181
+ () => ({
182
+ config,
183
+ setConfig,
184
+ isConnected: state.isConnected,
185
+ isConnecting: state.isConnecting,
186
+ error: state.error,
187
+ client: state.client,
188
+ connect,
189
+ disconnect,
190
+ clearError,
191
+ }),
192
+ [config, setConfig, state, connect, disconnect, clearError]
193
+ );
194
+
195
+ return (
196
+ <ConnectionContext.Provider value={value}>
197
+ {children}
198
+ </ConnectionContext.Provider>
199
+ );
200
+ }
201
+
202
+ export function useConnection(): ConnectionContextValue {
203
+ const context = useContext(ConnectionContext);
204
+ if (!context) {
205
+ throw new Error('useConnection must be used within a ConnectionProvider');
206
+ }
207
+ return context;
208
+ }
209
+
210
+ export function useApiClient(): SyncClient | null {
211
+ const { client } = useConnection();
212
+ return client;
213
+ }
@@ -0,0 +1,9 @@
1
+ export * from './ConnectionContext';
2
+ export * from './useConsoleApi';
3
+ export * from './useInstanceContext';
4
+ export * from './useLiveEvents';
5
+ export * from './useLocalStorage';
6
+ export * from './usePartitionContext';
7
+ export * from './usePreferences';
8
+ export * from './useRequestEvents';
9
+ export * from './useTimeRange';