@syncsnap/react 1.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/README.md +23 -0
- package/dist/index.d.ts +81 -0
- package/dist/index.js +560 -0
- package/dist/index.js.map +1 -0
- package/dist/styles/globals.css +122 -0
- package/dist/theme.css +130 -0
- package/package.json +84 -0
package/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# @syncsnap/react
|
|
2
|
+
|
|
3
|
+
Syncsnap client SDK for React — upload UI, QR code, and file-sync components.
|
|
4
|
+
|
|
5
|
+
## Example
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { SyncsnapUploadButton } from '@syncsnap/react';
|
|
9
|
+
|
|
10
|
+
<SyncsnapUploadButton
|
|
11
|
+
buttonText="Scan to upload"
|
|
12
|
+
onJobCreated={(job) => {
|
|
13
|
+
console.log('Job created :', job);
|
|
14
|
+
}}
|
|
15
|
+
onCompleted={(job, result) => {
|
|
16
|
+
console.log('Job completed', job, result);
|
|
17
|
+
}}
|
|
18
|
+
/>;
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Docs
|
|
22
|
+
|
|
23
|
+
**[Documentation →](https://docs.syncsnap.xyz)**
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
type JobStatus = 'pending' | 'in_progress' | 'completed' | 'failed';
|
|
4
|
+
interface Job {
|
|
5
|
+
id: string;
|
|
6
|
+
projectId: string;
|
|
7
|
+
status: JobStatus;
|
|
8
|
+
fileName?: string | null;
|
|
9
|
+
createdAt: string;
|
|
10
|
+
updatedAt: string;
|
|
11
|
+
}
|
|
12
|
+
interface CreateJobResponse {
|
|
13
|
+
id: string;
|
|
14
|
+
projectId: string;
|
|
15
|
+
status: JobStatus;
|
|
16
|
+
createdAt: string;
|
|
17
|
+
updatedAt: string;
|
|
18
|
+
}
|
|
19
|
+
/** Pre-signed download URL returned by the syncsnap API when a job is completed. */
|
|
20
|
+
interface PresignedUrlResponse {
|
|
21
|
+
url: string;
|
|
22
|
+
fileName: string;
|
|
23
|
+
expiration: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Download URL response. When the server is configured with `onCompleted`,
|
|
27
|
+
* its return value is included as `completedPayload`.
|
|
28
|
+
*/
|
|
29
|
+
interface CompletedJobResponse<T = unknown> extends PresignedUrlResponse {
|
|
30
|
+
completedPayload?: T;
|
|
31
|
+
}
|
|
32
|
+
interface UseSyncsnapJobOptions {
|
|
33
|
+
createJobUrl?: string;
|
|
34
|
+
getJobUrl?: (jobId: string) => string;
|
|
35
|
+
/** URL for the server's wait-for-completion endpoint (server polls until job completes). Defaults to getJobUrl(jobId) + '/wait'. */
|
|
36
|
+
getWaitForCompletionUrl?: (jobId: string) => string;
|
|
37
|
+
/** Passed to the wait endpoint as query params. Default timeoutMs: 120000, intervalMs: 2000. */
|
|
38
|
+
waitTimeoutMs?: number;
|
|
39
|
+
waitIntervalMs?: number;
|
|
40
|
+
autoStart?: boolean;
|
|
41
|
+
onJobCreated?: (job: CreateJobResponse) => void;
|
|
42
|
+
/** Called when the job finishes. Result is whatever the server's onCompleted callback returned (when no callback is set, result is the presigned download URL). */
|
|
43
|
+
onCompleted?: (job: Job, result?: unknown) => void;
|
|
44
|
+
onError?: (error: Error) => void;
|
|
45
|
+
}
|
|
46
|
+
interface UseSyncsnapJobResult {
|
|
47
|
+
job: Job | null;
|
|
48
|
+
jobId: string | null;
|
|
49
|
+
status: JobStatus | null;
|
|
50
|
+
error: Error | null;
|
|
51
|
+
/** True while waiting for the server to complete the job (wait request in flight). */
|
|
52
|
+
isWaiting: boolean;
|
|
53
|
+
start: () => Promise<void>;
|
|
54
|
+
reset: () => void;
|
|
55
|
+
}
|
|
56
|
+
interface SyncsnapQrCodeProps {
|
|
57
|
+
jobId: string;
|
|
58
|
+
baseUrl?: string;
|
|
59
|
+
size?: number;
|
|
60
|
+
className?: string;
|
|
61
|
+
errorCorrectionLevel?: 'L' | 'M' | 'Q' | 'H';
|
|
62
|
+
}
|
|
63
|
+
interface SyncsnapUploadButtonProps extends UseSyncsnapJobOptions {
|
|
64
|
+
buttonText?: string;
|
|
65
|
+
className?: string;
|
|
66
|
+
qrSize?: number;
|
|
67
|
+
qrBaseUrl?: string;
|
|
68
|
+
errorCorrectionLevel?: 'L' | 'M' | 'Q' | 'H';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
declare function SyncsnapQrCode({ jobId, baseUrl, size, className, errorCorrectionLevel, }: SyncsnapQrCodeProps): react_jsx_runtime.JSX.Element | null;
|
|
72
|
+
|
|
73
|
+
declare function SyncsnapUploadButton({ buttonText, className, qrSize, qrBaseUrl, errorCorrectionLevel, onCompleted: userOnCompleted, onError: userOnError, ...options }: SyncsnapUploadButtonProps): react_jsx_runtime.JSX.Element;
|
|
74
|
+
|
|
75
|
+
declare function useSyncsnapJob(options?: UseSyncsnapJobOptions): UseSyncsnapJobResult;
|
|
76
|
+
|
|
77
|
+
declare function createUploadUrl(jobId: string, options?: {
|
|
78
|
+
baseUrl?: string;
|
|
79
|
+
}): string;
|
|
80
|
+
|
|
81
|
+
export { type CompletedJobResponse, type CreateJobResponse, type Job, type JobStatus, type PresignedUrlResponse, SyncsnapQrCode, type SyncsnapQrCodeProps, SyncsnapUploadButton, type SyncsnapUploadButtonProps, type UseSyncsnapJobOptions, type UseSyncsnapJobResult, createUploadUrl, useSyncsnapJob };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
// src/components/SyncsnapQrCode.tsx
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import QRCode from "qrcode";
|
|
4
|
+
|
|
5
|
+
// src/utils.ts
|
|
6
|
+
function createUploadUrl(jobId, options) {
|
|
7
|
+
const baseUrl = options?.baseUrl ?? "https://upload.syncsnap.xyz/";
|
|
8
|
+
const url = new URL(baseUrl);
|
|
9
|
+
url.searchParams.set("job_id", jobId);
|
|
10
|
+
return url.toString();
|
|
11
|
+
}
|
|
12
|
+
async function fetchJson(url, init) {
|
|
13
|
+
const res = await fetch(url, init);
|
|
14
|
+
const data = await res.json().catch(() => ({}));
|
|
15
|
+
if (!res.ok) {
|
|
16
|
+
const message = typeof data === "object" && data && "error" in data && data.error ? String(data.error) : `Syncsnap request failed (${res.status})`;
|
|
17
|
+
throw new Error(message);
|
|
18
|
+
}
|
|
19
|
+
return data;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/components/SyncsnapQrCode.tsx
|
|
23
|
+
import { jsx } from "react/jsx-runtime";
|
|
24
|
+
function SyncsnapQrCode({
|
|
25
|
+
jobId,
|
|
26
|
+
baseUrl,
|
|
27
|
+
size = 240,
|
|
28
|
+
className,
|
|
29
|
+
errorCorrectionLevel = "M"
|
|
30
|
+
}) {
|
|
31
|
+
const [dataUrl, setDataUrl] = useState(null);
|
|
32
|
+
const [error, setError] = useState(null);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
let mounted = true;
|
|
35
|
+
const url = createUploadUrl(jobId, { baseUrl });
|
|
36
|
+
QRCode.toDataURL(url, {
|
|
37
|
+
width: size,
|
|
38
|
+
errorCorrectionLevel
|
|
39
|
+
}).then((value) => {
|
|
40
|
+
if (!mounted) return;
|
|
41
|
+
setDataUrl(value);
|
|
42
|
+
setError(null);
|
|
43
|
+
}).catch((err) => {
|
|
44
|
+
if (!mounted) return;
|
|
45
|
+
setDataUrl(null);
|
|
46
|
+
setError(
|
|
47
|
+
err instanceof Error ? err : new Error("Failed to generate QR")
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
return () => {
|
|
51
|
+
mounted = false;
|
|
52
|
+
};
|
|
53
|
+
}, [jobId, baseUrl, size, errorCorrectionLevel]);
|
|
54
|
+
if (error) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
if (!dataUrl) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return /* @__PURE__ */ jsx(
|
|
61
|
+
"img",
|
|
62
|
+
{
|
|
63
|
+
src: dataUrl,
|
|
64
|
+
alt: "Syncsnap QR code",
|
|
65
|
+
width: size,
|
|
66
|
+
height: size,
|
|
67
|
+
className,
|
|
68
|
+
style: {
|
|
69
|
+
display: "block",
|
|
70
|
+
margin: "0 auto",
|
|
71
|
+
width: size,
|
|
72
|
+
height: size
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/components/SyncsnapUploadButton.tsx
|
|
79
|
+
import { useCallback as useCallback2, useEffect as useEffect3, useState as useState3 } from "react";
|
|
80
|
+
|
|
81
|
+
// src/hooks/useSyncsnapJob.ts
|
|
82
|
+
import { useCallback, useEffect as useEffect2, useRef, useState as useState2 } from "react";
|
|
83
|
+
function defaultGetJobUrl(jobId) {
|
|
84
|
+
return `/api/syncsnap/job/${encodeURIComponent(jobId)}`;
|
|
85
|
+
}
|
|
86
|
+
function defaultGetWaitForCompletionUrl(getJobUrl) {
|
|
87
|
+
return (jobId) => {
|
|
88
|
+
const base = getJobUrl(jobId).replace(/\/$/, "");
|
|
89
|
+
return `${base}/wait`;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function useSyncsnapJob(options = {}) {
|
|
93
|
+
const getJobUrlFn = options.getJobUrl ?? defaultGetJobUrl;
|
|
94
|
+
const {
|
|
95
|
+
createJobUrl = "/api/syncsnap/job",
|
|
96
|
+
getWaitForCompletionUrl = defaultGetWaitForCompletionUrl(getJobUrlFn),
|
|
97
|
+
waitTimeoutMs = 12e4,
|
|
98
|
+
waitIntervalMs = 2e3,
|
|
99
|
+
autoStart = false,
|
|
100
|
+
onJobCreated,
|
|
101
|
+
onCompleted,
|
|
102
|
+
onError
|
|
103
|
+
} = options;
|
|
104
|
+
const [job, setJob] = useState2(null);
|
|
105
|
+
const [error, setError] = useState2(null);
|
|
106
|
+
const [isWaiting, setIsWaiting] = useState2(false);
|
|
107
|
+
const abortControllerRef = useRef(null);
|
|
108
|
+
const start = useCallback(async () => {
|
|
109
|
+
setError(null);
|
|
110
|
+
abortControllerRef.current?.abort();
|
|
111
|
+
abortControllerRef.current = new AbortController();
|
|
112
|
+
const signal = abortControllerRef.current.signal;
|
|
113
|
+
try {
|
|
114
|
+
const created = await fetchJson(createJobUrl, {
|
|
115
|
+
method: "POST"
|
|
116
|
+
});
|
|
117
|
+
const createdJob = {
|
|
118
|
+
...created,
|
|
119
|
+
fileName: null
|
|
120
|
+
};
|
|
121
|
+
setJob(createdJob);
|
|
122
|
+
onJobCreated?.(created);
|
|
123
|
+
const waitUrl = getWaitForCompletionUrl(created.id);
|
|
124
|
+
const url = new URL(
|
|
125
|
+
waitUrl,
|
|
126
|
+
typeof window !== "undefined" ? window.location.origin : "http://localhost"
|
|
127
|
+
);
|
|
128
|
+
url.searchParams.set("timeoutMs", String(waitTimeoutMs));
|
|
129
|
+
url.searchParams.set("intervalMs", String(waitIntervalMs));
|
|
130
|
+
setIsWaiting(true);
|
|
131
|
+
const { job: finalJob, result } = await fetchJson(
|
|
132
|
+
url.toString(),
|
|
133
|
+
{
|
|
134
|
+
signal
|
|
135
|
+
}
|
|
136
|
+
);
|
|
137
|
+
setJob(null);
|
|
138
|
+
setIsWaiting(false);
|
|
139
|
+
onCompleted?.(finalJob, result);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
setIsWaiting(false);
|
|
142
|
+
setJob(null);
|
|
143
|
+
if (signal.aborted) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const error2 = err instanceof Error ? err : new Error("Syncsnap job failed");
|
|
147
|
+
setError(error2);
|
|
148
|
+
onError?.(error2);
|
|
149
|
+
}
|
|
150
|
+
}, [
|
|
151
|
+
createJobUrl,
|
|
152
|
+
getWaitForCompletionUrl,
|
|
153
|
+
waitTimeoutMs,
|
|
154
|
+
waitIntervalMs,
|
|
155
|
+
onJobCreated,
|
|
156
|
+
onCompleted,
|
|
157
|
+
onError
|
|
158
|
+
]);
|
|
159
|
+
const reset = useCallback(() => {
|
|
160
|
+
abortControllerRef.current?.abort();
|
|
161
|
+
abortControllerRef.current = null;
|
|
162
|
+
setJob(null);
|
|
163
|
+
setIsWaiting(false);
|
|
164
|
+
setError(null);
|
|
165
|
+
}, []);
|
|
166
|
+
useEffect2(() => {
|
|
167
|
+
if (autoStart) {
|
|
168
|
+
void start();
|
|
169
|
+
}
|
|
170
|
+
}, [autoStart, start]);
|
|
171
|
+
return {
|
|
172
|
+
job,
|
|
173
|
+
jobId: job?.id ?? null,
|
|
174
|
+
status: job?.status ?? null,
|
|
175
|
+
error,
|
|
176
|
+
isWaiting,
|
|
177
|
+
start,
|
|
178
|
+
reset
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/components/ui/button.tsx
|
|
183
|
+
import * as React from "react";
|
|
184
|
+
import { cva } from "class-variance-authority";
|
|
185
|
+
import { Slot } from "radix-ui";
|
|
186
|
+
|
|
187
|
+
// src/lib/utils.ts
|
|
188
|
+
import { clsx } from "clsx";
|
|
189
|
+
import { twMerge } from "tailwind-merge";
|
|
190
|
+
function cn(...inputs) {
|
|
191
|
+
return twMerge(clsx(inputs));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/components/ui/button.tsx
|
|
195
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
196
|
+
var buttonVariants = cva(
|
|
197
|
+
"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-lg border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 aria-invalid:ring-3 [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none",
|
|
198
|
+
{
|
|
199
|
+
variants: {
|
|
200
|
+
variant: {
|
|
201
|
+
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
|
202
|
+
outline: "border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground",
|
|
203
|
+
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
|
204
|
+
ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground",
|
|
205
|
+
destructive: "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30",
|
|
206
|
+
link: "text-primary underline-offset-4 hover:underline"
|
|
207
|
+
},
|
|
208
|
+
size: {
|
|
209
|
+
default: "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
210
|
+
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
211
|
+
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
|
212
|
+
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
|
|
213
|
+
icon: "size-8",
|
|
214
|
+
"icon-xs": "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
|
215
|
+
"icon-sm": "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
|
216
|
+
"icon-lg": "size-9"
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
defaultVariants: {
|
|
220
|
+
variant: "default",
|
|
221
|
+
size: "default"
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
);
|
|
225
|
+
var Button = React.forwardRef(function Button2({
|
|
226
|
+
className,
|
|
227
|
+
variant = "default",
|
|
228
|
+
size = "default",
|
|
229
|
+
asChild = false,
|
|
230
|
+
...props
|
|
231
|
+
}, ref) {
|
|
232
|
+
const Comp = asChild ? Slot.Root : "button";
|
|
233
|
+
return /* @__PURE__ */ jsx2(
|
|
234
|
+
Comp,
|
|
235
|
+
{
|
|
236
|
+
ref,
|
|
237
|
+
"data-slot": "button",
|
|
238
|
+
"data-variant": variant,
|
|
239
|
+
"data-size": size,
|
|
240
|
+
className: cn(buttonVariants({ variant, size, className })),
|
|
241
|
+
...props
|
|
242
|
+
}
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
Button.displayName = "Button";
|
|
246
|
+
|
|
247
|
+
// src/components/ui/dialog.tsx
|
|
248
|
+
import * as React2 from "react";
|
|
249
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
250
|
+
import { XIcon } from "lucide-react";
|
|
251
|
+
import { jsx as jsx3, jsxs } from "react/jsx-runtime";
|
|
252
|
+
function Dialog({
|
|
253
|
+
...props
|
|
254
|
+
}) {
|
|
255
|
+
return /* @__PURE__ */ jsx3(DialogPrimitive.Root, { "data-slot": "dialog", ...props });
|
|
256
|
+
}
|
|
257
|
+
function DialogPortal({
|
|
258
|
+
...props
|
|
259
|
+
}) {
|
|
260
|
+
return /* @__PURE__ */ jsx3(DialogPrimitive.Portal, { "data-slot": "dialog-portal", ...props });
|
|
261
|
+
}
|
|
262
|
+
var DialogOverlay = React2.forwardRef(function DialogOverlay2({ className, style, ...props }, ref) {
|
|
263
|
+
return /* @__PURE__ */ jsx3(
|
|
264
|
+
DialogPrimitive.Overlay,
|
|
265
|
+
{
|
|
266
|
+
ref,
|
|
267
|
+
"data-slot": "dialog-overlay",
|
|
268
|
+
className: cn(
|
|
269
|
+
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50",
|
|
270
|
+
className
|
|
271
|
+
),
|
|
272
|
+
style: {
|
|
273
|
+
position: "fixed",
|
|
274
|
+
inset: 0,
|
|
275
|
+
zIndex: 50,
|
|
276
|
+
backgroundColor: "rgba(0,0,0,0.5)",
|
|
277
|
+
...style
|
|
278
|
+
},
|
|
279
|
+
...props
|
|
280
|
+
}
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
DialogOverlay.displayName = "DialogOverlay";
|
|
284
|
+
function DialogContent({
|
|
285
|
+
className,
|
|
286
|
+
children,
|
|
287
|
+
showCloseButton = true,
|
|
288
|
+
style,
|
|
289
|
+
...props
|
|
290
|
+
}) {
|
|
291
|
+
return /* @__PURE__ */ jsxs(DialogPortal, { children: [
|
|
292
|
+
/* @__PURE__ */ jsx3(DialogOverlay, {}),
|
|
293
|
+
/* @__PURE__ */ jsx3(
|
|
294
|
+
"div",
|
|
295
|
+
{
|
|
296
|
+
style: {
|
|
297
|
+
position: "fixed",
|
|
298
|
+
inset: 0,
|
|
299
|
+
zIndex: 50,
|
|
300
|
+
display: "flex",
|
|
301
|
+
alignItems: "center",
|
|
302
|
+
justifyContent: "center",
|
|
303
|
+
padding: "1rem",
|
|
304
|
+
pointerEvents: "none"
|
|
305
|
+
},
|
|
306
|
+
children: /* @__PURE__ */ jsxs(
|
|
307
|
+
DialogPrimitive.Content,
|
|
308
|
+
{
|
|
309
|
+
"data-slot": "dialog-content",
|
|
310
|
+
className: cn(
|
|
311
|
+
"bg-background data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-4 rounded-xl p-4 text-sm ring-1 duration-100 sm:max-w-sm w-full outline-none",
|
|
312
|
+
className
|
|
313
|
+
),
|
|
314
|
+
style: {
|
|
315
|
+
...style,
|
|
316
|
+
position: "relative",
|
|
317
|
+
pointerEvents: "auto",
|
|
318
|
+
maxWidth: "24rem",
|
|
319
|
+
width: "100%",
|
|
320
|
+
padding: "1rem",
|
|
321
|
+
backgroundColor: "white",
|
|
322
|
+
borderRadius: "0.75rem",
|
|
323
|
+
boxShadow: "0 25px 50px -12px rgba(0,0,0,0.25)"
|
|
324
|
+
},
|
|
325
|
+
...props,
|
|
326
|
+
children: [
|
|
327
|
+
children,
|
|
328
|
+
showCloseButton && /* @__PURE__ */ jsx3(DialogPrimitive.Close, { "data-slot": "dialog-close", asChild: true, children: /* @__PURE__ */ jsxs(
|
|
329
|
+
Button,
|
|
330
|
+
{
|
|
331
|
+
variant: "ghost",
|
|
332
|
+
className: "absolute top-2 right-2",
|
|
333
|
+
size: "icon-sm",
|
|
334
|
+
children: [
|
|
335
|
+
/* @__PURE__ */ jsx3(XIcon, {}),
|
|
336
|
+
/* @__PURE__ */ jsx3("span", { className: "sr-only", children: "Close" })
|
|
337
|
+
]
|
|
338
|
+
}
|
|
339
|
+
) })
|
|
340
|
+
]
|
|
341
|
+
}
|
|
342
|
+
)
|
|
343
|
+
}
|
|
344
|
+
)
|
|
345
|
+
] });
|
|
346
|
+
}
|
|
347
|
+
function DialogHeader({ className, ...props }) {
|
|
348
|
+
return /* @__PURE__ */ jsx3(
|
|
349
|
+
"div",
|
|
350
|
+
{
|
|
351
|
+
"data-slot": "dialog-header",
|
|
352
|
+
className: cn("gap-2 flex flex-col", className),
|
|
353
|
+
...props
|
|
354
|
+
}
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
function DialogTitle({
|
|
358
|
+
className,
|
|
359
|
+
...props
|
|
360
|
+
}) {
|
|
361
|
+
return /* @__PURE__ */ jsx3(
|
|
362
|
+
DialogPrimitive.Title,
|
|
363
|
+
{
|
|
364
|
+
"data-slot": "dialog-title",
|
|
365
|
+
className: cn("text-base leading-none font-medium", className),
|
|
366
|
+
...props
|
|
367
|
+
}
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
function DialogDescription({
|
|
371
|
+
className,
|
|
372
|
+
...props
|
|
373
|
+
}) {
|
|
374
|
+
return /* @__PURE__ */ jsx3(
|
|
375
|
+
DialogPrimitive.Description,
|
|
376
|
+
{
|
|
377
|
+
"data-slot": "dialog-description",
|
|
378
|
+
className: cn(
|
|
379
|
+
"text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3",
|
|
380
|
+
className
|
|
381
|
+
),
|
|
382
|
+
...props
|
|
383
|
+
}
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// src/components/SyncsnapUploadButton.tsx
|
|
388
|
+
import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
389
|
+
function SyncsnapUploadButton({
|
|
390
|
+
buttonText = "Start upload",
|
|
391
|
+
className,
|
|
392
|
+
qrSize = 240,
|
|
393
|
+
qrBaseUrl,
|
|
394
|
+
errorCorrectionLevel = "M",
|
|
395
|
+
onCompleted: userOnCompleted,
|
|
396
|
+
onError: userOnError,
|
|
397
|
+
...options
|
|
398
|
+
}) {
|
|
399
|
+
const [dialogOpen, setDialogOpen] = useState3(false);
|
|
400
|
+
const [completedResult, setCompletedResult] = useState3(null);
|
|
401
|
+
const [completedError, setCompletedError] = useState3(null);
|
|
402
|
+
const handleCompleted = useCallback2(
|
|
403
|
+
(job, result) => {
|
|
404
|
+
setCompletedResult({ job, result });
|
|
405
|
+
setCompletedError(null);
|
|
406
|
+
setDialogOpen(false);
|
|
407
|
+
userOnCompleted?.(job, result);
|
|
408
|
+
},
|
|
409
|
+
[userOnCompleted]
|
|
410
|
+
);
|
|
411
|
+
const handleError = useCallback2(
|
|
412
|
+
(err) => {
|
|
413
|
+
setCompletedError(err);
|
|
414
|
+
setCompletedResult(null);
|
|
415
|
+
setDialogOpen(false);
|
|
416
|
+
userOnError?.(err);
|
|
417
|
+
},
|
|
418
|
+
[userOnError]
|
|
419
|
+
);
|
|
420
|
+
const { jobId, status, start, isWaiting, error, reset } = useSyncsnapJob({
|
|
421
|
+
...options,
|
|
422
|
+
onCompleted: handleCompleted,
|
|
423
|
+
onError: handleError
|
|
424
|
+
});
|
|
425
|
+
useEffect3(() => {
|
|
426
|
+
if (jobId) setDialogOpen(true);
|
|
427
|
+
}, [jobId]);
|
|
428
|
+
const onClick = useCallback2(() => {
|
|
429
|
+
setCompletedResult(null);
|
|
430
|
+
setCompletedError(null);
|
|
431
|
+
setDialogOpen(true);
|
|
432
|
+
void start();
|
|
433
|
+
}, [start]);
|
|
434
|
+
const onOpenChange = useCallback2(
|
|
435
|
+
(open) => {
|
|
436
|
+
setDialogOpen(open);
|
|
437
|
+
if (!open) {
|
|
438
|
+
reset();
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
[reset]
|
|
442
|
+
);
|
|
443
|
+
const downloadUrl = completedResult?.result && typeof completedResult.result === "object" && completedResult.result !== null && "downloadUrl" in completedResult.result ? completedResult.result.downloadUrl : void 0;
|
|
444
|
+
return /* @__PURE__ */ jsxs2("div", { className, children: [
|
|
445
|
+
/* @__PURE__ */ jsx4(Button, { type: "button", onClick, disabled: isWaiting, children: isWaiting ? "Preparing..." : buttonText }),
|
|
446
|
+
error ? /* @__PURE__ */ jsx4("p", { style: { color: "#dc2626", marginTop: 8 }, children: error.message }) : null,
|
|
447
|
+
completedError ? /* @__PURE__ */ jsx4("p", { style: { color: "#dc2626", marginTop: 8 }, children: completedError.message }) : null,
|
|
448
|
+
downloadUrl ? /* @__PURE__ */ jsxs2(
|
|
449
|
+
"div",
|
|
450
|
+
{
|
|
451
|
+
style: {
|
|
452
|
+
marginTop: 16,
|
|
453
|
+
display: "flex",
|
|
454
|
+
flexDirection: "column",
|
|
455
|
+
alignItems: "center"
|
|
456
|
+
},
|
|
457
|
+
children: [
|
|
458
|
+
/* @__PURE__ */ jsx4("p", { style: { margin: "0 0 8px", fontSize: 14, color: "#555" }, children: "Upload complete" }),
|
|
459
|
+
/* @__PURE__ */ jsx4(
|
|
460
|
+
"img",
|
|
461
|
+
{
|
|
462
|
+
src: downloadUrl,
|
|
463
|
+
alt: "Uploaded",
|
|
464
|
+
style: {
|
|
465
|
+
maxWidth: "100%",
|
|
466
|
+
height: "auto",
|
|
467
|
+
borderRadius: 8,
|
|
468
|
+
border: "1px solid #e5e5e5"
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
)
|
|
472
|
+
]
|
|
473
|
+
}
|
|
474
|
+
) : null,
|
|
475
|
+
/* @__PURE__ */ jsx4(Dialog, { open: dialogOpen, onOpenChange, children: /* @__PURE__ */ jsxs2(
|
|
476
|
+
DialogContent,
|
|
477
|
+
{
|
|
478
|
+
style: {
|
|
479
|
+
display: "grid",
|
|
480
|
+
gridTemplateColumns: "1fr",
|
|
481
|
+
justifyItems: "center"
|
|
482
|
+
},
|
|
483
|
+
children: [
|
|
484
|
+
/* @__PURE__ */ jsxs2(DialogHeader, { children: [
|
|
485
|
+
/* @__PURE__ */ jsx4(DialogTitle, { children: "Scan to upload" }),
|
|
486
|
+
/* @__PURE__ */ jsx4(DialogDescription, { children: "Scan this QR code with your phone to upload files" })
|
|
487
|
+
] }),
|
|
488
|
+
jobId ? /* @__PURE__ */ jsxs2(
|
|
489
|
+
"div",
|
|
490
|
+
{
|
|
491
|
+
style: {
|
|
492
|
+
width: "100%",
|
|
493
|
+
display: "flex",
|
|
494
|
+
flexDirection: "column",
|
|
495
|
+
alignItems: "center",
|
|
496
|
+
textAlign: "center",
|
|
497
|
+
marginTop: 16
|
|
498
|
+
},
|
|
499
|
+
children: [
|
|
500
|
+
/* @__PURE__ */ jsx4(
|
|
501
|
+
"div",
|
|
502
|
+
{
|
|
503
|
+
style: {
|
|
504
|
+
display: "flex",
|
|
505
|
+
justifyContent: "center",
|
|
506
|
+
width: "100%"
|
|
507
|
+
},
|
|
508
|
+
children: /* @__PURE__ */ jsx4(
|
|
509
|
+
SyncsnapQrCode,
|
|
510
|
+
{
|
|
511
|
+
jobId,
|
|
512
|
+
baseUrl: qrBaseUrl,
|
|
513
|
+
size: qrSize,
|
|
514
|
+
errorCorrectionLevel
|
|
515
|
+
}
|
|
516
|
+
)
|
|
517
|
+
}
|
|
518
|
+
),
|
|
519
|
+
status ? /* @__PURE__ */ jsxs2("p", { style: { margin: "8px 0 0", fontSize: 14 }, children: [
|
|
520
|
+
"Status: ",
|
|
521
|
+
status
|
|
522
|
+
] }) : null,
|
|
523
|
+
/* @__PURE__ */ jsxs2(
|
|
524
|
+
"p",
|
|
525
|
+
{
|
|
526
|
+
style: {
|
|
527
|
+
margin: "16px 0 0",
|
|
528
|
+
fontSize: 13,
|
|
529
|
+
color: "#64748b"
|
|
530
|
+
},
|
|
531
|
+
children: [
|
|
532
|
+
"Powered by",
|
|
533
|
+
" ",
|
|
534
|
+
/* @__PURE__ */ jsx4(
|
|
535
|
+
"a",
|
|
536
|
+
{
|
|
537
|
+
href: "https://syncsnap.com",
|
|
538
|
+
target: "_blank",
|
|
539
|
+
rel: "noopener noreferrer",
|
|
540
|
+
children: "SyncSnap"
|
|
541
|
+
}
|
|
542
|
+
)
|
|
543
|
+
]
|
|
544
|
+
}
|
|
545
|
+
)
|
|
546
|
+
]
|
|
547
|
+
}
|
|
548
|
+
) : null
|
|
549
|
+
]
|
|
550
|
+
}
|
|
551
|
+
) })
|
|
552
|
+
] });
|
|
553
|
+
}
|
|
554
|
+
export {
|
|
555
|
+
SyncsnapQrCode,
|
|
556
|
+
SyncsnapUploadButton,
|
|
557
|
+
createUploadUrl,
|
|
558
|
+
useSyncsnapJob
|
|
559
|
+
};
|
|
560
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/SyncsnapQrCode.tsx","../src/utils.ts","../src/components/SyncsnapUploadButton.tsx","../src/hooks/useSyncsnapJob.ts","../src/components/ui/button.tsx","../src/lib/utils.ts","../src/components/ui/dialog.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, useState } from 'react';\nimport QRCode from 'qrcode';\nimport type { SyncsnapQrCodeProps } from '../types';\nimport { createUploadUrl } from '../utils';\n\nexport function SyncsnapQrCode({\n jobId,\n baseUrl,\n size = 240,\n className,\n errorCorrectionLevel = 'M',\n}: SyncsnapQrCodeProps) {\n const [dataUrl, setDataUrl] = useState<string | null>(null);\n const [error, setError] = useState<Error | null>(null);\n\n useEffect(() => {\n let mounted = true;\n const url = createUploadUrl(jobId, { baseUrl });\n\n QRCode.toDataURL(url, {\n width: size,\n errorCorrectionLevel,\n })\n .then((value) => {\n if (!mounted) return;\n setDataUrl(value);\n setError(null);\n })\n .catch((err) => {\n if (!mounted) return;\n setDataUrl(null);\n setError(\n err instanceof Error ? err : new Error('Failed to generate QR')\n );\n });\n\n return () => {\n mounted = false;\n };\n }, [jobId, baseUrl, size, errorCorrectionLevel]);\n\n if (error) {\n return null;\n }\n\n if (!dataUrl) {\n return null;\n }\n\n return (\n <img\n src={dataUrl}\n alt=\"Syncsnap QR code\"\n width={size}\n height={size}\n className={className}\n style={{\n display: 'block',\n margin: '0 auto',\n width: size,\n height: size,\n }}\n />\n );\n}\n","export function createUploadUrl(\n jobId: string,\n options?: { baseUrl?: string }\n): string {\n const baseUrl = options?.baseUrl ?? 'https://upload.syncsnap.xyz/';\n const url = new URL(baseUrl);\n url.searchParams.set('job_id', jobId);\n return url.toString();\n}\n\nexport async function fetchJson<T>(\n url: string,\n init?: RequestInit\n): Promise<T> {\n const res = await fetch(url, init);\n const data = (await res.json().catch(() => ({}))) as T & {\n error?: string;\n };\n\n if (!res.ok) {\n const message =\n typeof data === 'object' && data && 'error' in data && data.error\n ? String(data.error)\n : `Syncsnap request failed (${res.status})`;\n throw new Error(message);\n }\n\n return data as T;\n}\n","'use client';\n\nimport { useCallback, useEffect, useState } from 'react';\nimport type { Job, SyncsnapUploadButtonProps } from '../types';\nimport { SyncsnapQrCode } from './SyncsnapQrCode';\nimport { useSyncsnapJob } from '../hooks/useSyncsnapJob';\nimport { Button } from './ui/button';\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n} from './ui/dialog';\n\ntype CompletionResult = { job: Job; result?: unknown };\ntype ResultWithDownloadUrl = { downloadUrl?: string };\n\nexport function SyncsnapUploadButton({\n buttonText = 'Start upload',\n className,\n qrSize = 240,\n qrBaseUrl,\n errorCorrectionLevel = 'M',\n onCompleted: userOnCompleted,\n onError: userOnError,\n ...options\n}: SyncsnapUploadButtonProps) {\n const [dialogOpen, setDialogOpen] = useState(false);\n const [completedResult, setCompletedResult] =\n useState<CompletionResult | null>(null);\n const [completedError, setCompletedError] = useState<Error | null>(null);\n\n const handleCompleted = useCallback(\n (job: Job, result?: unknown) => {\n setCompletedResult({ job, result });\n setCompletedError(null);\n setDialogOpen(false);\n userOnCompleted?.(job, result);\n },\n [userOnCompleted]\n );\n\n const handleError = useCallback(\n (err: Error) => {\n setCompletedError(err);\n setCompletedResult(null);\n setDialogOpen(false);\n userOnError?.(err);\n },\n [userOnError]\n );\n\n const { jobId, status, start, isWaiting, error, reset } = useSyncsnapJob({\n ...options,\n onCompleted: handleCompleted,\n onError: handleError,\n });\n\n // Open dialog when we have a job id\n useEffect(() => {\n if (jobId) setDialogOpen(true);\n }, [jobId]);\n\n const onClick = useCallback(() => {\n setCompletedResult(null);\n setCompletedError(null);\n setDialogOpen(true);\n void start();\n }, [start]);\n\n const onOpenChange = useCallback(\n (open: boolean) => {\n setDialogOpen(open);\n if (!open) {\n reset();\n }\n },\n [reset]\n );\n\n const downloadUrl =\n completedResult?.result &&\n typeof completedResult.result === 'object' &&\n completedResult.result !== null &&\n 'downloadUrl' in completedResult.result\n ? (completedResult.result as ResultWithDownloadUrl).downloadUrl\n : undefined;\n\n return (\n <div className={className}>\n <Button type=\"button\" onClick={onClick} disabled={isWaiting}>\n {isWaiting ? 'Preparing...' : buttonText}\n </Button>\n {error ? (\n <p style={{ color: '#dc2626', marginTop: 8 }}>{error.message}</p>\n ) : null}\n {completedError ? (\n <p style={{ color: '#dc2626', marginTop: 8 }}>\n {completedError.message}\n </p>\n ) : null}\n {downloadUrl ? (\n <div\n style={{\n marginTop: 16,\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'center',\n }}\n >\n <p style={{ margin: '0 0 8px', fontSize: 14, color: '#555' }}>\n Upload complete\n </p>\n <img\n src={downloadUrl}\n alt=\"Uploaded\"\n style={{\n maxWidth: '100%',\n height: 'auto',\n borderRadius: 8,\n border: '1px solid #e5e5e5',\n }}\n />\n </div>\n ) : null}\n <Dialog open={dialogOpen} onOpenChange={onOpenChange}>\n <DialogContent\n style={{\n display: 'grid',\n gridTemplateColumns: '1fr',\n justifyItems: 'center',\n }}\n >\n <DialogHeader>\n <DialogTitle>Scan to upload</DialogTitle>\n <DialogDescription>\n Scan this QR code with your phone to upload files\n </DialogDescription>\n </DialogHeader>\n {jobId ? (\n <div\n style={{\n width: '100%',\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'center',\n textAlign: 'center',\n marginTop: 16,\n }}\n >\n <div\n style={{\n display: 'flex',\n justifyContent: 'center',\n width: '100%',\n }}\n >\n <SyncsnapQrCode\n jobId={jobId}\n baseUrl={qrBaseUrl}\n size={qrSize}\n errorCorrectionLevel={errorCorrectionLevel}\n />\n </div>\n {status ? (\n <p style={{ margin: '8px 0 0', fontSize: 14 }}>\n Status: {status}\n </p>\n ) : null}\n <p\n style={{\n margin: '16px 0 0',\n fontSize: 13,\n color: '#64748b',\n }}\n >\n Powered by{' '}\n <a\n href=\"https://syncsnap.com\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n SyncSnap\n </a>\n </p>\n </div>\n ) : null}\n </DialogContent>\n </Dialog>\n </div>\n );\n}\n","'use client';\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport type {\n CreateJobResponse,\n Job,\n UseSyncsnapJobOptions,\n UseSyncsnapJobResult,\n} from '../types';\nimport { fetchJson } from '../utils';\n\nfunction defaultGetJobUrl(jobId: string): string {\n return `/api/syncsnap/job/${encodeURIComponent(jobId)}`;\n}\n\nfunction defaultGetWaitForCompletionUrl(\n getJobUrl: (jobId: string) => string\n): (jobId: string) => string {\n return (jobId: string): string => {\n const base = getJobUrl(jobId).replace(/\\/$/, '');\n return `${base}/wait`;\n };\n}\n\ninterface WaitCompletionResponse {\n job: Job;\n result?: unknown;\n}\n\nexport function useSyncsnapJob(\n options: UseSyncsnapJobOptions = {}\n): UseSyncsnapJobResult {\n const getJobUrlFn = options.getJobUrl ?? defaultGetJobUrl;\n const {\n createJobUrl = '/api/syncsnap/job',\n getWaitForCompletionUrl = defaultGetWaitForCompletionUrl(getJobUrlFn),\n waitTimeoutMs = 120000,\n waitIntervalMs = 2000,\n autoStart = false,\n onJobCreated,\n onCompleted,\n onError,\n } = options;\n\n const [job, setJob] = useState<Job | null>(null);\n const [error, setError] = useState<Error | null>(null);\n const [isWaiting, setIsWaiting] = useState(false);\n const abortControllerRef = useRef<AbortController | null>(null);\n\n const start = useCallback(async () => {\n setError(null);\n abortControllerRef.current?.abort();\n abortControllerRef.current = new AbortController();\n const signal = abortControllerRef.current.signal;\n\n try {\n const created = await fetchJson<CreateJobResponse>(createJobUrl, {\n method: 'POST',\n });\n const createdJob: Job = {\n ...created,\n fileName: null,\n };\n setJob(createdJob);\n onJobCreated?.(created);\n\n const waitUrl = getWaitForCompletionUrl(created.id);\n const url = new URL(\n waitUrl,\n typeof window !== 'undefined'\n ? window.location.origin\n : 'http://localhost'\n );\n url.searchParams.set('timeoutMs', String(waitTimeoutMs));\n url.searchParams.set('intervalMs', String(waitIntervalMs));\n\n setIsWaiting(true);\n const { job: finalJob, result } = await fetchJson<WaitCompletionResponse>(\n url.toString(),\n {\n signal,\n }\n );\n setJob(null);\n setIsWaiting(false);\n onCompleted?.(finalJob, result);\n } catch (err) {\n setIsWaiting(false);\n setJob(null);\n if (signal.aborted) {\n return;\n }\n const error =\n err instanceof Error ? err : new Error('Syncsnap job failed');\n setError(error);\n onError?.(error);\n }\n }, [\n createJobUrl,\n getWaitForCompletionUrl,\n waitTimeoutMs,\n waitIntervalMs,\n onJobCreated,\n onCompleted,\n onError,\n ]);\n\n const reset = useCallback(() => {\n abortControllerRef.current?.abort();\n abortControllerRef.current = null;\n setJob(null);\n setIsWaiting(false);\n setError(null);\n }, []);\n\n useEffect(() => {\n if (autoStart) {\n void start();\n }\n }, [autoStart, start]);\n\n return {\n job,\n jobId: job?.id ?? null,\n status: job?.status ?? null,\n error,\n isWaiting,\n start,\n reset,\n };\n}\n","import * as React from 'react';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { Slot } from 'radix-ui';\n\nimport { cn } from '../../lib/utils';\n\nconst buttonVariants = cva(\n \"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-lg border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 aria-invalid:ring-3 [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none\",\n {\n variants: {\n variant: {\n default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',\n outline:\n 'border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground',\n secondary:\n 'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',\n ghost:\n 'hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground',\n destructive:\n 'bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30',\n link: 'text-primary underline-offset-4 hover:underline',\n },\n size: {\n default:\n 'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',\n xs: \"h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3\",\n sm: \"h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5\",\n lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3',\n icon: 'size-8',\n 'icon-xs':\n \"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3\",\n 'icon-sm':\n 'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',\n 'icon-lg': 'size-9',\n },\n },\n defaultVariants: {\n variant: 'default',\n size: 'default',\n },\n }\n);\n\nconst Button = React.forwardRef<\n HTMLButtonElement,\n React.ComponentProps<'button'> &\n VariantProps<typeof buttonVariants> & {\n asChild?: boolean;\n }\n>(function Button(\n {\n className,\n variant = 'default',\n size = 'default',\n asChild = false,\n ...props\n },\n ref\n) {\n const Comp = asChild ? Slot.Root : 'button';\n\n return (\n <Comp\n ref={ref}\n data-slot=\"button\"\n data-variant={variant}\n data-size={size}\n className={cn(buttonVariants({ variant, size, className }))}\n {...props}\n />\n );\n});\nButton.displayName = 'Button';\n\nexport { Button, buttonVariants };\n","import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n","import * as React from 'react';\nimport * as DialogPrimitive from '@radix-ui/react-dialog';\n\nimport { cn } from '../../lib/utils';\nimport { Button } from './button';\nimport { XIcon } from 'lucide-react';\n\nfunction Dialog({\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Root>) {\n return <DialogPrimitive.Root data-slot=\"dialog\" {...props} />;\n}\n\nfunction DialogTrigger({\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {\n return <DialogPrimitive.Trigger data-slot=\"dialog-trigger\" {...props} />;\n}\n\nfunction DialogPortal({\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Portal>) {\n return <DialogPrimitive.Portal data-slot=\"dialog-portal\" {...props} />;\n}\n\nfunction DialogClose({\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Close>) {\n return <DialogPrimitive.Close data-slot=\"dialog-close\" {...props} />;\n}\n\nconst DialogOverlay = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Overlay>,\n React.ComponentProps<typeof DialogPrimitive.Overlay>\n>(function DialogOverlay({ className, style, ...props }, ref) {\n return (\n <DialogPrimitive.Overlay\n ref={ref}\n data-slot=\"dialog-overlay\"\n className={cn(\n 'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50',\n className\n )}\n style={{\n position: 'fixed',\n inset: 0,\n zIndex: 50,\n backgroundColor: 'rgba(0,0,0,0.5)',\n ...style,\n }}\n {...props}\n />\n );\n});\nDialogOverlay.displayName = 'DialogOverlay';\n\nfunction DialogContent({\n className,\n children,\n showCloseButton = true,\n style,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Content> & {\n showCloseButton?: boolean;\n}) {\n return (\n <DialogPortal>\n <DialogOverlay />\n {/* Wrapper ensures dialog is centered on screen (flexbox avoids transform/containing-block issues) */}\n <div\n style={{\n position: 'fixed',\n inset: 0,\n zIndex: 50,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n padding: '1rem',\n pointerEvents: 'none',\n }}\n >\n <DialogPrimitive.Content\n data-slot=\"dialog-content\"\n className={cn(\n 'bg-background data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-4 rounded-xl p-4 text-sm ring-1 duration-100 sm:max-w-sm w-full outline-none',\n className\n )}\n style={{\n ...style,\n position: 'relative',\n pointerEvents: 'auto',\n maxWidth: '24rem',\n width: '100%',\n padding: '1rem',\n backgroundColor: 'white',\n borderRadius: '0.75rem',\n boxShadow: '0 25px 50px -12px rgba(0,0,0,0.25)',\n }}\n {...props}\n >\n {children}\n {showCloseButton && (\n <DialogPrimitive.Close data-slot=\"dialog-close\" asChild>\n <Button\n variant=\"ghost\"\n className=\"absolute top-2 right-2\"\n size=\"icon-sm\"\n >\n <XIcon />\n <span className=\"sr-only\">Close</span>\n </Button>\n </DialogPrimitive.Close>\n )}\n </DialogPrimitive.Content>\n </div>\n </DialogPortal>\n );\n}\n\nfunction DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {\n return (\n <div\n data-slot=\"dialog-header\"\n className={cn('gap-2 flex flex-col', className)}\n {...props}\n />\n );\n}\n\nfunction DialogFooter({\n className,\n showCloseButton = false,\n children,\n ...props\n}: React.ComponentProps<'div'> & {\n showCloseButton?: boolean;\n}) {\n return (\n <div\n data-slot=\"dialog-footer\"\n className={cn(\n 'bg-muted/50 -mx-4 -mb-4 rounded-b-xl border-t p-4 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end',\n className\n )}\n {...props}\n >\n {children}\n {showCloseButton && (\n <DialogPrimitive.Close asChild>\n <Button variant=\"outline\">Close</Button>\n </DialogPrimitive.Close>\n )}\n </div>\n );\n}\n\nfunction DialogTitle({\n className,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Title>) {\n return (\n <DialogPrimitive.Title\n data-slot=\"dialog-title\"\n className={cn('text-base leading-none font-medium', className)}\n {...props}\n />\n );\n}\n\nfunction DialogDescription({\n className,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Description>) {\n return (\n <DialogPrimitive.Description\n data-slot=\"dialog-description\"\n className={cn(\n 'text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3',\n className\n )}\n {...props}\n />\n );\n}\n\nexport {\n Dialog,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogOverlay,\n DialogPortal,\n DialogTitle,\n DialogTrigger,\n};\n"],"mappings":"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","names":["useCallback","useEffect","useState","useEffect","useState","useState","error","useEffect","jsx","Button","React","jsx","jsx","DialogOverlay","jsx","jsx","jsx","jsxs","useState","useCallback","useEffect"]}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
@import 'tailwindcss';
|
|
2
|
+
@import 'tw-animate-css';
|
|
3
|
+
@import 'shadcn/tailwind.css';
|
|
4
|
+
|
|
5
|
+
@custom-variant dark (&:is(.dark *));
|
|
6
|
+
|
|
7
|
+
@theme inline {
|
|
8
|
+
--color-background: var(--background);
|
|
9
|
+
--color-foreground: var(--foreground);
|
|
10
|
+
--color-card: var(--card);
|
|
11
|
+
--color-card-foreground: var(--card-foreground);
|
|
12
|
+
--color-popover: var(--popover);
|
|
13
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
14
|
+
--color-primary: var(--primary);
|
|
15
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
16
|
+
--color-secondary: var(--secondary);
|
|
17
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
18
|
+
--color-muted: var(--muted);
|
|
19
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
20
|
+
--color-accent: var(--accent);
|
|
21
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
22
|
+
--color-destructive: var(--destructive);
|
|
23
|
+
--color-destructive-foreground: var(--destructive-foreground);
|
|
24
|
+
--color-border: var(--border);
|
|
25
|
+
--color-input: var(--input);
|
|
26
|
+
--color-ring: var(--ring);
|
|
27
|
+
--color-chart-1: var(--chart-1);
|
|
28
|
+
--color-chart-2: var(--chart-2);
|
|
29
|
+
--color-chart-3: var(--chart-3);
|
|
30
|
+
--color-chart-4: var(--chart-4);
|
|
31
|
+
--color-chart-5: var(--chart-5);
|
|
32
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
33
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
34
|
+
--radius-lg: var(--radius);
|
|
35
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
36
|
+
--color-sidebar: var(--sidebar);
|
|
37
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
38
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
39
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
40
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
41
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
42
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
43
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
:root {
|
|
47
|
+
--radius: 0.625rem;
|
|
48
|
+
--background: oklch(1 0 0);
|
|
49
|
+
--foreground: oklch(0.145 0 0);
|
|
50
|
+
--card: oklch(1 0 0);
|
|
51
|
+
--card-foreground: oklch(0.145 0 0);
|
|
52
|
+
--popover: oklch(1 0 0);
|
|
53
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
54
|
+
--primary: oklch(0.205 0 0);
|
|
55
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
56
|
+
--secondary: oklch(0.97 0 0);
|
|
57
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
58
|
+
--muted: oklch(0.97 0 0);
|
|
59
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
60
|
+
--accent: oklch(0.97 0 0);
|
|
61
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
62
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
63
|
+
--border: oklch(0.922 0 0);
|
|
64
|
+
--input: oklch(0.922 0 0);
|
|
65
|
+
--ring: oklch(0.708 0 0);
|
|
66
|
+
--chart-1: oklch(0.646 0.222 41.116);
|
|
67
|
+
--chart-2: oklch(0.6 0.118 184.704);
|
|
68
|
+
--chart-3: oklch(0.398 0.07 227.392);
|
|
69
|
+
--chart-4: oklch(0.828 0.189 84.429);
|
|
70
|
+
--chart-5: oklch(0.769 0.188 70.08);
|
|
71
|
+
--sidebar: oklch(0.985 0 0);
|
|
72
|
+
--sidebar-foreground: oklch(0.145 0 0);
|
|
73
|
+
--sidebar-primary: oklch(0.205 0 0);
|
|
74
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
75
|
+
--sidebar-accent: oklch(0.97 0 0);
|
|
76
|
+
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
77
|
+
--sidebar-border: oklch(0.922 0 0);
|
|
78
|
+
--sidebar-ring: oklch(0.708 0 0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.dark {
|
|
82
|
+
--background: oklch(0.145 0 0);
|
|
83
|
+
--foreground: oklch(0.985 0 0);
|
|
84
|
+
--card: oklch(0.205 0 0);
|
|
85
|
+
--card-foreground: oklch(0.985 0 0);
|
|
86
|
+
--popover: oklch(0.205 0 0);
|
|
87
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
88
|
+
--primary: oklch(0.922 0 0);
|
|
89
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
90
|
+
--secondary: oklch(0.269 0 0);
|
|
91
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
92
|
+
--muted: oklch(0.269 0 0);
|
|
93
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
94
|
+
--accent: oklch(0.269 0 0);
|
|
95
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
96
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
97
|
+
--border: oklch(1 0 0 / 10%);
|
|
98
|
+
--input: oklch(1 0 0 / 15%);
|
|
99
|
+
--ring: oklch(0.556 0 0);
|
|
100
|
+
--chart-1: oklch(0.488 0.243 264.376);
|
|
101
|
+
--chart-2: oklch(0.696 0.17 162.48);
|
|
102
|
+
--chart-3: oklch(0.769 0.188 70.08);
|
|
103
|
+
--chart-4: oklch(0.627 0.265 303.9);
|
|
104
|
+
--chart-5: oklch(0.645 0.246 16.439);
|
|
105
|
+
--sidebar: oklch(0.205 0 0);
|
|
106
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
107
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
108
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
109
|
+
--sidebar-accent: oklch(0.269 0 0);
|
|
110
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
111
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
112
|
+
--sidebar-ring: oklch(0.556 0 0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@layer base {
|
|
116
|
+
* {
|
|
117
|
+
@apply border-border outline-ring/50;
|
|
118
|
+
}
|
|
119
|
+
body {
|
|
120
|
+
@apply bg-background text-foreground;
|
|
121
|
+
}
|
|
122
|
+
}
|
package/dist/theme.css
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme variables and Tailwind mapping for @syncsnap/react components.
|
|
3
|
+
* Import this in your app's main CSS after Tailwind.
|
|
4
|
+
*
|
|
5
|
+
* Example (Tailwind v4) in app/globals.css:
|
|
6
|
+
* @import "tailwindcss";
|
|
7
|
+
* @import "tw-animate-css";
|
|
8
|
+
* @import "shadcn/tailwind.css";
|
|
9
|
+
* @import "@syncsnap/react/theme";
|
|
10
|
+
*/
|
|
11
|
+
@source "../**/*.{ts,tsx}";
|
|
12
|
+
|
|
13
|
+
@custom-variant dark (&:is(.dark *));
|
|
14
|
+
|
|
15
|
+
@theme inline {
|
|
16
|
+
--color-background: var(--background);
|
|
17
|
+
--color-foreground: var(--foreground);
|
|
18
|
+
--color-card: var(--card);
|
|
19
|
+
--color-card-foreground: var(--card-foreground);
|
|
20
|
+
--color-popover: var(--popover);
|
|
21
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
22
|
+
--color-primary: var(--primary);
|
|
23
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
24
|
+
--color-secondary: var(--secondary);
|
|
25
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
26
|
+
--color-muted: var(--muted);
|
|
27
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
28
|
+
--color-accent: var(--accent);
|
|
29
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
30
|
+
--color-destructive: var(--destructive);
|
|
31
|
+
--color-destructive-foreground: var(--destructive-foreground);
|
|
32
|
+
--color-border: var(--border);
|
|
33
|
+
--color-input: var(--input);
|
|
34
|
+
--color-ring: var(--ring);
|
|
35
|
+
--color-chart-1: var(--chart-1);
|
|
36
|
+
--color-chart-2: var(--chart-2);
|
|
37
|
+
--color-chart-3: var(--chart-3);
|
|
38
|
+
--color-chart-4: var(--chart-4);
|
|
39
|
+
--color-chart-5: var(--chart-5);
|
|
40
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
41
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
42
|
+
--radius-lg: var(--radius);
|
|
43
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
44
|
+
--color-sidebar: var(--sidebar);
|
|
45
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
46
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
47
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
48
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
49
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
50
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
51
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
:root {
|
|
55
|
+
--radius: 0.625rem;
|
|
56
|
+
--background: oklch(1 0 0);
|
|
57
|
+
--foreground: oklch(0.145 0 0);
|
|
58
|
+
--card: oklch(1 0 0);
|
|
59
|
+
--card-foreground: oklch(0.145 0 0);
|
|
60
|
+
--popover: oklch(1 0 0);
|
|
61
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
62
|
+
--primary: oklch(0.205 0 0);
|
|
63
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
64
|
+
--secondary: oklch(0.97 0 0);
|
|
65
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
66
|
+
--muted: oklch(0.97 0 0);
|
|
67
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
68
|
+
--accent: oklch(0.97 0 0);
|
|
69
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
70
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
71
|
+
--border: oklch(0.922 0 0);
|
|
72
|
+
--input: oklch(0.922 0 0);
|
|
73
|
+
--ring: oklch(0.708 0 0);
|
|
74
|
+
--chart-1: oklch(0.646 0.222 41.116);
|
|
75
|
+
--chart-2: oklch(0.6 0.118 184.704);
|
|
76
|
+
--chart-3: oklch(0.398 0.07 227.392);
|
|
77
|
+
--chart-4: oklch(0.828 0.189 84.429);
|
|
78
|
+
--chart-5: oklch(0.769 0.188 70.08);
|
|
79
|
+
--sidebar: oklch(0.985 0 0);
|
|
80
|
+
--sidebar-foreground: oklch(0.145 0 0);
|
|
81
|
+
--sidebar-primary: oklch(0.205 0 0);
|
|
82
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
83
|
+
--sidebar-accent: oklch(0.97 0 0);
|
|
84
|
+
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
85
|
+
--sidebar-border: oklch(0.922 0 0);
|
|
86
|
+
--sidebar-ring: oklch(0.708 0 0);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.dark {
|
|
90
|
+
--background: oklch(0.145 0 0);
|
|
91
|
+
--foreground: oklch(0.985 0 0);
|
|
92
|
+
--card: oklch(0.205 0 0);
|
|
93
|
+
--card-foreground: oklch(0.985 0 0);
|
|
94
|
+
--popover: oklch(0.205 0 0);
|
|
95
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
96
|
+
--primary: oklch(0.922 0 0);
|
|
97
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
98
|
+
--secondary: oklch(0.269 0 0);
|
|
99
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
100
|
+
--muted: oklch(0.269 0 0);
|
|
101
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
102
|
+
--accent: oklch(0.269 0 0);
|
|
103
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
104
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
105
|
+
--border: oklch(1 0 0 / 10%);
|
|
106
|
+
--input: oklch(1 0 0 / 15%);
|
|
107
|
+
--ring: oklch(0.556 0 0);
|
|
108
|
+
--chart-1: oklch(0.488 0.243 264.376);
|
|
109
|
+
--chart-2: oklch(0.696 0.17 162.48);
|
|
110
|
+
--chart-3: oklch(0.769 0.188 70.08);
|
|
111
|
+
--chart-4: oklch(0.627 0.265 303.9);
|
|
112
|
+
--chart-5: oklch(0.645 0.246 16.439);
|
|
113
|
+
--sidebar: oklch(0.205 0 0);
|
|
114
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
115
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
116
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
117
|
+
--sidebar-accent: oklch(0.269 0 0);
|
|
118
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
119
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
120
|
+
--sidebar-ring: oklch(0.556 0 0);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@layer base {
|
|
124
|
+
* {
|
|
125
|
+
@apply border-border outline-ring/50;
|
|
126
|
+
}
|
|
127
|
+
body {
|
|
128
|
+
@apply bg-background text-foreground;
|
|
129
|
+
}
|
|
130
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@syncsnap/react",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Syncsnap client SDK for React",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"module": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./styles": "./dist/styles/globals.css",
|
|
15
|
+
"./theme": "./dist/theme.css"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"lint": "eslint . --max-warnings 0",
|
|
22
|
+
"lint:fix": "eslint . --fix",
|
|
23
|
+
"format": "prettier --write .",
|
|
24
|
+
"format:check": "prettier --check .",
|
|
25
|
+
"build": "tsup src/index.tsx --dts --format esm --out-dir dist --clean --sourcemap && node -e \"const fs=require('fs'); fs.mkdirSync('dist/styles',{recursive:true}); fs.copyFileSync('src/styles/globals.css','dist/styles/globals.css'); fs.copyFileSync('src/styles/theme.css','dist/theme.css');\"",
|
|
26
|
+
"publish": "npm publish --access public",
|
|
27
|
+
"publish:ci": "npm publish --provenance --access public",
|
|
28
|
+
"link": "npm run build && npm link",
|
|
29
|
+
"prepublishOnly": "npm run build",
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"test:watch": "vitest"
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/syncsnap/packages.git",
|
|
36
|
+
"directory": "react"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/syncsnap/packages/react#readme",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/syncsnap/packages/react/issues"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"keywords": [
|
|
46
|
+
"syncsnap",
|
|
47
|
+
"react",
|
|
48
|
+
"sdk",
|
|
49
|
+
"upload",
|
|
50
|
+
"file-sync"
|
|
51
|
+
],
|
|
52
|
+
"license": "MIT",
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@radix-ui/react-dialog": "^1.1.15",
|
|
55
|
+
"class-variance-authority": "^0.7.1",
|
|
56
|
+
"clsx": "^2.1.1",
|
|
57
|
+
"lucide-react": "^0.575.0",
|
|
58
|
+
"qrcode": "^1.5.3",
|
|
59
|
+
"radix-ui": "^1.4.3",
|
|
60
|
+
"shadcn": "^3.8.5",
|
|
61
|
+
"tailwind-merge": "^2.6.1",
|
|
62
|
+
"tw-animate-css": "^1.4.0"
|
|
63
|
+
},
|
|
64
|
+
"peerDependencies": {
|
|
65
|
+
"react": "^18.0.0",
|
|
66
|
+
"tailwindcss": "^3.0.0 || ^4.0.0"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
70
|
+
"@testing-library/react": "^16.0.1",
|
|
71
|
+
"@testing-library/user-event": "^14.5.2",
|
|
72
|
+
"react": "^18.3.1",
|
|
73
|
+
"react-dom": "^18.3.1",
|
|
74
|
+
"@types/qrcode": "^1.5.6",
|
|
75
|
+
"@types/react": "^19.2.13",
|
|
76
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
77
|
+
"jsdom": "^25.0.1",
|
|
78
|
+
"tsup": "^8.0.1",
|
|
79
|
+
"typescript": "^5.4.0",
|
|
80
|
+
"vitest": "^2.1.0",
|
|
81
|
+
"eslint-plugin-react": "^7.37.0",
|
|
82
|
+
"eslint-plugin-react-hooks": "^5.0.0"
|
|
83
|
+
}
|
|
84
|
+
}
|