@ismaelsoilet/synapsejs 1.9.0 → 1.11.1

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 (38) hide show
  1. package/README.md +4 -3
  2. package/bin/synapse.ts +74 -2
  3. package/package.json +2 -2
  4. package/src/client/components.tsx +459 -1
  5. package/src/client/context.tsx +23 -3
  6. package/src/client/error-boundary.tsx +59 -0
  7. package/src/client/hooks.ts +100 -1
  8. package/src/client/toast.tsx +219 -0
  9. package/src/client/upload.ts +153 -0
  10. package/src/client.ts +37 -2
  11. package/src/compiler/client-bundler.ts +94 -2
  12. package/src/compiler/context-bench.ts +2 -0
  13. package/src/compiler/contract.ts +1 -1
  14. package/src/compiler/oracle-runner.ts +10 -1
  15. package/src/compiler/scaffolder.ts +113 -4
  16. package/src/compiler/slice-splitter.ts +21 -3
  17. package/src/compiler/slice-templates.ts +234 -10
  18. package/src/compiler/typescript-api.ts +33 -0
  19. package/src/core/action-context.ts +50 -1
  20. package/src/core/cron-parser.ts +167 -0
  21. package/src/core/database-client.ts +35 -0
  22. package/src/core/index.ts +2 -0
  23. package/src/core/jobs.ts +37 -0
  24. package/src/core/oauth-client.ts +322 -0
  25. package/src/core/postgres-client.ts +37 -0
  26. package/src/core/postgres-queue.ts +51 -2
  27. package/src/core/query-builder.ts +166 -34
  28. package/src/core/session-context.ts +45 -4
  29. package/src/core/session-token.ts +16 -1
  30. package/src/core/sqlite-client.ts +20 -0
  31. package/src/mcp/server.ts +257 -1
  32. package/src/mcp/tools.ts +92 -1
  33. package/src/project-status.ts +103 -0
  34. package/src/runtime/client-entry.ts +11 -5
  35. package/src/runtime/queue-engine.ts +120 -5
  36. package/src/runtime/security-headers.ts +35 -0
  37. package/src/runtime/server.ts +300 -58
  38. package/templates/starter/package.json +2 -2
@@ -0,0 +1,59 @@
1
+ import { Component, type ErrorInfo, type ReactNode } from 'react';
2
+
3
+ export interface SliceErrorBoundaryProps {
4
+ children: ReactNode;
5
+ fallback?: ReactNode | ((error: Error) => ReactNode);
6
+ onError?: (error: Error, info: ErrorInfo) => void;
7
+ }
8
+
9
+ interface SliceErrorBoundaryState {
10
+ hasError: boolean;
11
+ error: Error | null;
12
+ }
13
+
14
+ export class SliceErrorBoundary extends Component<SliceErrorBoundaryProps, SliceErrorBoundaryState> {
15
+ override state: SliceErrorBoundaryState = {
16
+ hasError: false,
17
+ error: null
18
+ };
19
+
20
+ static getDerivedStateFromError(error: Error): SliceErrorBoundaryState {
21
+ return { hasError: true, error };
22
+ }
23
+
24
+ override componentDidCatch(error: Error, info: ErrorInfo): void {
25
+ if (this.props.onError) {
26
+ this.props.onError(error, info);
27
+ } else {
28
+ console.error('[SliceErrorBoundary] Erro capturado na interface da fatia:', error, info);
29
+ }
30
+ }
31
+
32
+ override render(): ReactNode {
33
+ if (this.state.hasError && this.state.error) {
34
+ if (typeof this.props.fallback === 'function') {
35
+ return this.props.fallback(this.state.error);
36
+ }
37
+ if (this.props.fallback) {
38
+ return this.props.fallback;
39
+ }
40
+ return (
41
+ <div
42
+ role="alert"
43
+ style={{
44
+ padding: '1rem',
45
+ border: '1px solid #f43f5e',
46
+ borderRadius: '0.375rem',
47
+ background: 'rgba(244, 63, 94, 0.1)',
48
+ color: '#f43f5e',
49
+ fontFamily: 'system-ui, -apple-system, sans-serif'
50
+ }}
51
+ >
52
+ <strong style={{ display: 'block', marginBottom: '0.25rem' }}>Erro na interface da fatia:</strong>
53
+ <span>{this.state.error.message}</span>
54
+ </div>
55
+ );
56
+ }
57
+ return this.props.children;
58
+ }
59
+ }
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import type { Result } from '../core/machine-types';
3
3
  import { useSynapseContext } from './context';
4
+ import { useToast } from './toast';
4
5
 
5
6
  export interface UseSubscriptionOptions {
6
7
  enabled?: boolean;
@@ -90,6 +91,7 @@ export interface UseActionResult<TPayload, TData, TError> {
90
91
  export interface UseActionOptions<TData, TError> {
91
92
  onSuccess?: (data: TData) => void;
92
93
  onError?: (error: TError) => void;
94
+ toast?: boolean | { success?: string; error?: string };
93
95
  }
94
96
 
95
97
  /**
@@ -104,6 +106,7 @@ export function useAction<TPayload = unknown, TData = unknown, TError = string>(
104
106
  const [data, setData] = useState<TData | null>(null);
105
107
  const [error, setError] = useState<TError | null>(null);
106
108
  const [isSuccess, setIsSuccess] = useState<boolean>(false);
109
+ const toast = useToast();
107
110
 
108
111
  const ctx = useSynapseContext();
109
112
  const resolvedAction =
@@ -141,10 +144,24 @@ export function useAction<TPayload = unknown, TData = unknown, TError = string>(
141
144
  setIsSuccess(true);
142
145
  setError(null);
143
146
  optionsRef.current?.onSuccess?.(result.value);
147
+ if (optionsRef.current?.toast) {
148
+ const successMsg =
149
+ typeof optionsRef.current.toast === 'object' && optionsRef.current.toast.success
150
+ ? optionsRef.current.toast.success
151
+ : 'Ação executada com sucesso!';
152
+ toast.success(successMsg);
153
+ }
144
154
  } else {
145
155
  setError(result.error);
146
156
  setIsSuccess(false);
147
157
  optionsRef.current?.onError?.(result.error);
158
+ if (optionsRef.current?.toast) {
159
+ const errorMsg =
160
+ typeof optionsRef.current.toast === 'object' && optionsRef.current.toast.error
161
+ ? optionsRef.current.toast.error
162
+ : String(result.error);
163
+ toast.error(errorMsg);
164
+ }
148
165
  }
149
166
  return result;
150
167
  } catch (err: unknown) {
@@ -152,12 +169,15 @@ export function useAction<TPayload = unknown, TData = unknown, TError = string>(
152
169
  setError(failure);
153
170
  setIsSuccess(false);
154
171
  optionsRef.current?.onError?.(failure);
172
+ if (optionsRef.current?.toast) {
173
+ toast.error(String(failure));
174
+ }
155
175
  return { ok: false, error: failure };
156
176
  } finally {
157
177
  setIsSubmitting(false);
158
178
  }
159
179
  },
160
- [resolvedAction]
180
+ [resolvedAction, toast]
161
181
  );
162
182
 
163
183
  return {
@@ -171,3 +191,82 @@ export function useAction<TPayload = unknown, TData = unknown, TError = string>(
171
191
  reset
172
192
  };
173
193
  }
194
+
195
+ /**
196
+ * Hook for reading dynamic URL route parameters (e.g. /:domain/:name/:id).
197
+ * Resolves from window.location pathname segments or SSR injected route params.
198
+ */
199
+ export function useParams<T extends Record<string, string> = Record<string, string>>(): T {
200
+ const ctx = useSynapseContext();
201
+ const [params] = useState<T>(() => {
202
+ if (ctx.props?.__synapse_params) {
203
+ return ctx.props.__synapse_params as T;
204
+ }
205
+ if (typeof window !== 'undefined') {
206
+ const globalParams = (window as unknown as { __SYNAPSE_ROUTE_PARAMS__?: T }).__SYNAPSE_ROUTE_PARAMS__;
207
+ if (globalParams) return globalParams;
208
+ const segments = window.location.pathname.split('/').filter(Boolean);
209
+ if (segments.length >= 3) {
210
+ const remaining = segments.slice(2);
211
+ const mapped: Record<string, string> = { id: remaining[0] };
212
+ for (let i = 0; i < remaining.length; i++) {
213
+ mapped[`param${i + 1}`] = remaining[i];
214
+ }
215
+ return mapped as T;
216
+ }
217
+ }
218
+ return {} as T;
219
+ });
220
+
221
+ return params;
222
+ }
223
+
224
+ /**
225
+ * Hook for reading and mutating query string search parameters seamlessly.
226
+ */
227
+ export function useSearchParams(): [
228
+ URLSearchParams,
229
+ (updater: Record<string, string | null | undefined> | URLSearchParams) => void
230
+ ] {
231
+ const ctx = useSynapseContext();
232
+ const [searchParams, setSearchParamsState] = useState<URLSearchParams>(() => {
233
+ if (typeof window !== 'undefined') {
234
+ return new URLSearchParams(window.location.search);
235
+ }
236
+ if (ctx.url) {
237
+ try {
238
+ const u = new URL(ctx.url, 'http://localhost');
239
+ return u.searchParams;
240
+ } catch {
241
+ // fallback
242
+ }
243
+ }
244
+ return new URLSearchParams();
245
+ });
246
+
247
+ const setSearchParams = useCallback((updater: Record<string, string | null | undefined> | URLSearchParams) => {
248
+ if (typeof window === 'undefined') return;
249
+
250
+ const next = new URLSearchParams(window.location.search);
251
+ if (updater instanceof URLSearchParams) {
252
+ for (const [k, v] of updater.entries()) {
253
+ next.set(k, v);
254
+ }
255
+ } else {
256
+ for (const [k, v] of Object.entries(updater)) {
257
+ if (v === null || v === undefined) {
258
+ next.delete(k);
259
+ } else {
260
+ next.set(k, v);
261
+ }
262
+ }
263
+ }
264
+
265
+ const queryStr = next.toString();
266
+ const newUrl = queryStr ? `${window.location.pathname}?${queryStr}` : window.location.pathname;
267
+ window.history.pushState({}, '', newUrl);
268
+ setSearchParamsState(next);
269
+ }, []);
270
+
271
+ return [searchParams, setSearchParams];
272
+ }
@@ -0,0 +1,219 @@
1
+ import { createContext, type ReactNode, useCallback, useContext, useEffect, useRef, useState } from 'react';
2
+
3
+ // ============================================================================
4
+ // TOAST NOTIFICATION TYPES
5
+ // ============================================================================
6
+ export type ToastType = 'success' | 'error' | 'warning' | 'info';
7
+
8
+ export interface ToastItem {
9
+ id: string;
10
+ type: ToastType;
11
+ title?: string;
12
+ message: string;
13
+ durationMs?: number;
14
+ }
15
+
16
+ export interface ToastContextValue {
17
+ toast: (options: Omit<ToastItem, 'id'>) => string;
18
+ success: (message: string, title?: string) => string;
19
+ error: (message: string, title?: string) => string;
20
+ warning: (message: string, title?: string) => string;
21
+ info: (message: string, title?: string) => string;
22
+ dismiss: (id: string) => void;
23
+ clear: () => void;
24
+ }
25
+
26
+ const ToastContext = createContext<ToastContextValue | null>(null);
27
+
28
+ let globalToastEmitter: ((item: Omit<ToastItem, 'id'>) => string) | null = null;
29
+
30
+ // ============================================================================
31
+ // TOAST PROVIDER & FLOATING STACK
32
+ // ============================================================================
33
+ export interface ToastProviderProps {
34
+ children?: ReactNode;
35
+ position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';
36
+ maxToasts?: number;
37
+ }
38
+
39
+ export function ToastProvider({ children, position = 'bottom-right', maxToasts = 5 }: ToastProviderProps) {
40
+ const [toasts, setToasts] = useState<ToastItem[]>([]);
41
+ const timersRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());
42
+
43
+ const dismiss = useCallback((id: string) => {
44
+ const timer = timersRef.current.get(id);
45
+ if (timer) {
46
+ clearTimeout(timer);
47
+ timersRef.current.delete(id);
48
+ }
49
+ setToasts((prev) => prev.filter((t) => t.id !== id));
50
+ }, []);
51
+
52
+ const clear = useCallback(() => {
53
+ for (const timer of timersRef.current.values()) {
54
+ clearTimeout(timer);
55
+ }
56
+ timersRef.current.clear();
57
+ setToasts([]);
58
+ }, []);
59
+
60
+ const toast = useCallback(
61
+ (options: Omit<ToastItem, 'id'>): string => {
62
+ const id = `toast_${Math.random().toString(36).substring(2, 9)}`;
63
+ const duration = options.durationMs ?? 4000;
64
+ const newItem: ToastItem = { ...options, id };
65
+
66
+ setToasts((prev) => {
67
+ const next = [...prev, newItem];
68
+ if (next.length > maxToasts) {
69
+ const evicted = next.shift();
70
+ if (evicted) {
71
+ const timer = timersRef.current.get(evicted.id);
72
+ if (timer) clearTimeout(timer);
73
+ timersRef.current.delete(evicted.id);
74
+ }
75
+ }
76
+ return next;
77
+ });
78
+
79
+ if (duration > 0) {
80
+ const timer = setTimeout(() => {
81
+ dismiss(id);
82
+ }, duration);
83
+ timersRef.current.set(id, timer);
84
+ }
85
+
86
+ return id;
87
+ },
88
+ [dismiss, maxToasts]
89
+ );
90
+
91
+ const success = useCallback((message: string, title?: string) => toast({ type: 'success', message, title }), [toast]);
92
+ const error = useCallback((message: string, title?: string) => toast({ type: 'error', message, title }), [toast]);
93
+ const warning = useCallback((message: string, title?: string) => toast({ type: 'warning', message, title }), [toast]);
94
+ const info = useCallback((message: string, title?: string) => toast({ type: 'info', message, title }), [toast]);
95
+
96
+ useEffect(() => {
97
+ globalToastEmitter = toast;
98
+ return () => {
99
+ globalToastEmitter = null;
100
+ for (const timer of timersRef.current.values()) {
101
+ clearTimeout(timer);
102
+ }
103
+ timersRef.current.clear();
104
+ };
105
+ }, [toast]);
106
+
107
+ const positionStyles = {
108
+ 'top-right': 'top-4 right-4 items-end',
109
+ 'top-left': 'top-4 left-4 items-start',
110
+ 'bottom-right': 'bottom-4 right-4 items-end',
111
+ 'bottom-left': 'bottom-4 left-4 items-start'
112
+ };
113
+
114
+ const typeConfig: Record<ToastType, { bg: string; border: string; text: string; icon: string }> = {
115
+ success: {
116
+ bg: 'bg-emerald-950/90',
117
+ border: 'border-emerald-800',
118
+ text: 'text-emerald-300',
119
+ icon: '✓'
120
+ },
121
+ error: {
122
+ bg: 'bg-rose-950/90',
123
+ border: 'border-rose-800',
124
+ text: 'text-rose-300',
125
+ icon: '✕'
126
+ },
127
+ warning: {
128
+ bg: 'bg-amber-950/90',
129
+ border: 'border-amber-800',
130
+ text: 'text-amber-300',
131
+ icon: '⚠'
132
+ },
133
+ info: {
134
+ bg: 'bg-sky-950/90',
135
+ border: 'border-sky-800',
136
+ text: 'text-sky-300',
137
+ icon: 'ℹ'
138
+ }
139
+ };
140
+
141
+ return (
142
+ <ToastContext.Provider value={{ toast, success, error, warning, info, dismiss, clear }}>
143
+ {children}
144
+ <div
145
+ aria-live="polite"
146
+ className={`fixed z-50 flex flex-col gap-2 max-w-sm w-full pointer-events-none p-4 ${positionStyles[position]}`}
147
+ >
148
+ {toasts.map((item) => {
149
+ const conf = typeConfig[item.type] || typeConfig.info;
150
+ return (
151
+ <div
152
+ key={item.id}
153
+ role="alert"
154
+ className={`pointer-events-auto flex items-start gap-3 p-4 rounded-xl border backdrop-blur-md shadow-2xl transition-all duration-200 transform translate-y-0 text-xs w-full ${conf.bg} ${conf.border}`}
155
+ >
156
+ <span
157
+ className={`h-5 w-5 rounded-full flex items-center justify-center font-bold text-xs shrink-0 ${conf.text} bg-slate-900/60 border ${conf.border}`}
158
+ >
159
+ {conf.icon}
160
+ </span>
161
+ <div className="flex-1 min-w-0">
162
+ {item.title && <h5 className={`font-bold mb-0.5 ${conf.text}`}>{item.title}</h5>}
163
+ <p className="text-slate-200 leading-relaxed break-words">{item.message}</p>
164
+ </div>
165
+ <button
166
+ type="button"
167
+ onClick={() => dismiss(item.id)}
168
+ aria-label="Fechar notificação"
169
+ className="text-slate-400 hover:text-white transition-colors text-sm font-mono shrink-0 ml-1 p-0.5"
170
+ >
171
+ ✕
172
+ </button>
173
+ </div>
174
+ );
175
+ })}
176
+ </div>
177
+ </ToastContext.Provider>
178
+ );
179
+ }
180
+
181
+ // ============================================================================
182
+ // HOOK & IMPERATIVE API
183
+ // ============================================================================
184
+ export function useToast(): ToastContextValue {
185
+ const ctx = useContext(ToastContext);
186
+ if (!ctx) {
187
+ // Fallback gracioso caso chamado fora do provider ou no server
188
+ return {
189
+ toast: (opts) => {
190
+ if (globalToastEmitter) return globalToastEmitter(opts);
191
+ if (typeof console !== 'undefined') console.log(`[Toast ${opts.type}] ${opts.message}`);
192
+ return 'noop';
193
+ },
194
+ success: (msg, title) => {
195
+ if (globalToastEmitter) return globalToastEmitter({ type: 'success', message: msg, title });
196
+ if (typeof console !== 'undefined') console.log(`[Toast success] ${msg}`);
197
+ return 'noop';
198
+ },
199
+ error: (msg, title) => {
200
+ if (globalToastEmitter) return globalToastEmitter({ type: 'error', message: msg, title });
201
+ if (typeof console !== 'undefined') console.error(`[Toast error] ${msg}`);
202
+ return 'noop';
203
+ },
204
+ warning: (msg, title) => {
205
+ if (globalToastEmitter) return globalToastEmitter({ type: 'warning', message: msg, title });
206
+ if (typeof console !== 'undefined') console.warn(`[Toast warning] ${msg}`);
207
+ return 'noop';
208
+ },
209
+ info: (msg, title) => {
210
+ if (globalToastEmitter) return globalToastEmitter({ type: 'info', message: msg, title });
211
+ if (typeof console !== 'undefined') console.info(`[Toast info] ${msg}`);
212
+ return 'noop';
213
+ },
214
+ dismiss: () => {},
215
+ clear: () => {}
216
+ };
217
+ }
218
+ return ctx;
219
+ }
@@ -0,0 +1,153 @@
1
+ import { useCallback, useRef, useState } from 'react';
2
+ import type { Result } from '../core/machine-types';
3
+
4
+ export interface UseUploadOptions {
5
+ /** Target upload URL (default: /_synapse/files/general) */
6
+ url?: string;
7
+ /** Maximum allowed file size in bytes (default: 50MB) */
8
+ maxBytes?: number;
9
+ /** Allowed MIME types (e.g. ['image/png', 'image/jpeg', 'application/pdf']) */
10
+ allowedTypes?: string[];
11
+ /** Callback fired as upload progress increments */
12
+ onProgress?: (percent: number, loaded: number, total: number) => void;
13
+ /** Callback fired on successful upload completion */
14
+ onSuccess?: (response: unknown) => void;
15
+ /** Callback fired on upload failure */
16
+ onError?: (error: Error) => void;
17
+ }
18
+
19
+ export interface UseUploadResult {
20
+ upload: (file: File) => Promise<Result<unknown, string>>;
21
+ progress: number;
22
+ loaded: number;
23
+ total: number;
24
+ isUploading: boolean;
25
+ abort: () => void;
26
+ reset: () => void;
27
+ }
28
+
29
+ /**
30
+ * Universal browser hook for client-side file uploads with granular progress events and abort controls.
31
+ */
32
+ export function useUpload(options: UseUploadOptions = {}): UseUploadResult {
33
+ const [progress, setProgress] = useState<number>(0);
34
+ const [loaded, setLoaded] = useState<number>(0);
35
+ const [total, setTotal] = useState<number>(0);
36
+ const [isUploading, setIsUploading] = useState<boolean>(false);
37
+ const xhrRef = useRef<XMLHttpRequest | null>(null);
38
+
39
+ const reset = useCallback(() => {
40
+ if (xhrRef.current) {
41
+ xhrRef.current.abort();
42
+ xhrRef.current = null;
43
+ }
44
+ setProgress(0);
45
+ setLoaded(0);
46
+ setTotal(0);
47
+ setIsUploading(false);
48
+ }, []);
49
+
50
+ const abort = useCallback(() => {
51
+ if (xhrRef.current) {
52
+ xhrRef.current.abort();
53
+ xhrRef.current = null;
54
+ setIsUploading(false);
55
+ }
56
+ }, []);
57
+
58
+ const upload = useCallback(
59
+ async (file: File): Promise<Result<unknown, string>> => {
60
+ // 1. Client-Side Pre-Flight Validations
61
+ if (options.maxBytes && file.size > options.maxBytes) {
62
+ const err = `Arquivo excede o tamanho máximo permitido (${options.maxBytes} bytes)`;
63
+ options.onError?.(new Error(err));
64
+ return { ok: false, error: 'FILE_TOO_LARGE' };
65
+ }
66
+
67
+ if (options.allowedTypes && options.allowedTypes.length > 0) {
68
+ if (!options.allowedTypes.includes(file.type)) {
69
+ const err = `Tipo de arquivo "${file.type}" não permitido. Aceitos: ${options.allowedTypes.join(', ')}`;
70
+ options.onError?.(new Error(err));
71
+ return { ok: false, error: 'FILE_TYPE_NOT_ALLOWED' };
72
+ }
73
+ }
74
+
75
+ setIsUploading(true);
76
+ setProgress(0);
77
+ setLoaded(0);
78
+ setTotal(file.size);
79
+
80
+ return new Promise((resolve) => {
81
+ const xhr = new XMLHttpRequest();
82
+ xhrRef.current = xhr;
83
+
84
+ const targetUrl = options.url || '/_synapse/files/general';
85
+ xhr.open('POST', targetUrl, true);
86
+ xhr.setRequestHeader('Accept', 'application/json');
87
+
88
+ xhr.upload.onprogress = (e) => {
89
+ if (e.lengthComputable) {
90
+ const pct = Math.round((e.loaded / e.total) * 100);
91
+ setProgress(pct);
92
+ setLoaded(e.loaded);
93
+ setTotal(e.total);
94
+ options.onProgress?.(pct, e.loaded, e.total);
95
+ }
96
+ };
97
+
98
+ xhr.onload = () => {
99
+ setIsUploading(false);
100
+ xhrRef.current = null;
101
+ if (xhr.status >= 200 && xhr.status < 300) {
102
+ setProgress(100);
103
+ try {
104
+ const res = JSON.parse(xhr.responseText);
105
+ options.onSuccess?.(res);
106
+ resolve({ ok: true, value: res });
107
+ } catch {
108
+ options.onSuccess?.(xhr.responseText);
109
+ resolve({ ok: true, value: xhr.responseText });
110
+ }
111
+ } else {
112
+ let errorMsg = `Upload falhou com status HTTP ${xhr.status}`;
113
+ try {
114
+ const parsed = JSON.parse(xhr.responseText);
115
+ if (parsed.error) errorMsg = parsed.error;
116
+ } catch {}
117
+ options.onError?.(new Error(errorMsg));
118
+ resolve({ ok: false, error: errorMsg });
119
+ }
120
+ };
121
+
122
+ xhr.onerror = () => {
123
+ setIsUploading(false);
124
+ xhrRef.current = null;
125
+ const err = new Error('Erro de rede ao enviar arquivo.');
126
+ options.onError?.(err);
127
+ resolve({ ok: false, error: 'NETWORK_ERROR' });
128
+ };
129
+
130
+ xhr.onabort = () => {
131
+ setIsUploading(false);
132
+ xhrRef.current = null;
133
+ resolve({ ok: false, error: 'UPLOAD_ABORTED' });
134
+ };
135
+
136
+ const formData = new FormData();
137
+ formData.append('file', file, file.name);
138
+ xhr.send(formData);
139
+ });
140
+ },
141
+ [options]
142
+ );
143
+
144
+ return {
145
+ upload,
146
+ progress,
147
+ loaded,
148
+ total,
149
+ isUploading,
150
+ abort,
151
+ reset
152
+ };
153
+ }
package/src/client.ts CHANGED
@@ -23,12 +23,28 @@ export {
23
23
  type DataFormProps,
24
24
  DataTable,
25
25
  type DataTableProps,
26
+ Drawer,
27
+ type DrawerProps,
28
+ DropdownItem,
29
+ type DropdownItemProps,
30
+ DropdownMenu,
31
+ type DropdownMenuProps,
26
32
  expandNestedObject,
27
33
  type FormField,
28
34
  getNestedProperty,
35
+ Modal,
36
+ type ModalProps,
29
37
  Pagination,
30
38
  type PaginationProps,
31
- setNestedProperty
39
+ setNestedProperty,
40
+ Tab,
41
+ TabList,
42
+ type TabListProps,
43
+ TabPanel,
44
+ type TabPanelProps,
45
+ type TabProps,
46
+ Tabs,
47
+ type TabsProps
32
48
  } from './client/components';
33
49
  // Context & SSR data isolation
34
50
  export {
@@ -39,13 +55,19 @@ export {
39
55
  useSession,
40
56
  useSynapseContext
41
57
  } from './client/context';
42
- // Reactive Action & Realtime Subscription Hooks
58
+ export {
59
+ SliceErrorBoundary,
60
+ type SliceErrorBoundaryProps
61
+ } from './client/error-boundary';
62
+ // Reactive Action, Routing & Realtime Subscription Hooks
43
63
  export {
44
64
  type UseActionOptions,
45
65
  type UseActionResult,
46
66
  type UseSubscriptionOptions,
47
67
  type UseSubscriptionResult,
48
68
  useAction,
69
+ useParams,
70
+ useSearchParams,
49
71
  useSubscription
50
72
  } from './client/hooks';
51
73
  export {
@@ -60,6 +82,19 @@ export {
60
82
  type SliceOutletProviderProps
61
83
  } from './client/outlet';
62
84
  export { SubSlice, type SubSliceProps } from './client/sub-slice';
85
+ export {
86
+ type ToastContextValue,
87
+ type ToastItem,
88
+ ToastProvider,
89
+ type ToastProviderProps,
90
+ type ToastType,
91
+ useToast
92
+ } from './client/toast';
93
+ export {
94
+ type UseUploadOptions,
95
+ type UseUploadResult,
96
+ useUpload
97
+ } from './client/upload';
63
98
  export {
64
99
  type UseWebSocketOptions,
65
100
  type UseWebSocketResult,