@primestyleai/tryon 1.0.1 → 1.1.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.
@@ -0,0 +1,666 @@
1
+ "use client";
2
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
3
+ import { useState, useRef, useEffect, useCallback } from "react";
4
+ import { A as ApiClient, S as SseClient, i as isValidImageFile, c as compressImage, P as PrimeStyleError } from "../image-utils-usff6Qu8.js";
5
+ function CameraIcon({ size = 18 }) {
6
+ return /* @__PURE__ */ jsxs(
7
+ "svg",
8
+ {
9
+ width: size,
10
+ height: size,
11
+ viewBox: "0 0 24 24",
12
+ fill: "none",
13
+ stroke: "currentColor",
14
+ strokeWidth: 2,
15
+ strokeLinecap: "round",
16
+ strokeLinejoin: "round",
17
+ children: [
18
+ /* @__PURE__ */ jsx("path", { d: "M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" }),
19
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "13", r: "4" })
20
+ ]
21
+ }
22
+ );
23
+ }
24
+ function UploadIcon({ size = 48 }) {
25
+ return /* @__PURE__ */ jsxs(
26
+ "svg",
27
+ {
28
+ width: size,
29
+ height: size,
30
+ viewBox: "0 0 24 24",
31
+ fill: "none",
32
+ stroke: "currentColor",
33
+ strokeWidth: 1.5,
34
+ strokeLinecap: "round",
35
+ strokeLinejoin: "round",
36
+ children: [
37
+ /* @__PURE__ */ jsx("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
38
+ /* @__PURE__ */ jsx("polyline", { points: "17 8 12 3 7 8" }),
39
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "3", x2: "12", y2: "15" })
40
+ ]
41
+ }
42
+ );
43
+ }
44
+ function XIcon({ size = 20 }) {
45
+ return /* @__PURE__ */ jsxs(
46
+ "svg",
47
+ {
48
+ width: size,
49
+ height: size,
50
+ viewBox: "0 0 24 24",
51
+ fill: "none",
52
+ stroke: "currentColor",
53
+ strokeWidth: 2,
54
+ strokeLinecap: "round",
55
+ strokeLinejoin: "round",
56
+ children: [
57
+ /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
58
+ /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
59
+ ]
60
+ }
61
+ );
62
+ }
63
+ function AlertIcon({ size = 48 }) {
64
+ return /* @__PURE__ */ jsxs(
65
+ "svg",
66
+ {
67
+ width: size,
68
+ height: size,
69
+ viewBox: "0 0 24 24",
70
+ fill: "none",
71
+ stroke: "currentColor",
72
+ strokeWidth: 1.5,
73
+ strokeLinecap: "round",
74
+ strokeLinejoin: "round",
75
+ children: [
76
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
77
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "8", x2: "12", y2: "12" }),
78
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" })
79
+ ]
80
+ }
81
+ );
82
+ }
83
+ function getApiKey() {
84
+ const key = process.env.NEXT_PUBLIC_PRIMESTYLE_API_KEY ?? "";
85
+ if (!key) {
86
+ throw new PrimeStyleError(
87
+ "Missing NEXT_PUBLIC_PRIMESTYLE_API_KEY in environment variables",
88
+ "MISSING_API_KEY"
89
+ );
90
+ }
91
+ return key;
92
+ }
93
+ function getApiUrl(override) {
94
+ return override || process.env.NEXT_PUBLIC_PRIMESTYLE_API_URL || "https://api.primestyleai.com";
95
+ }
96
+ function PrimeStyleTryon({
97
+ productImage,
98
+ buttonText = "Virtual Try-On",
99
+ apiUrl,
100
+ showPoweredBy = true,
101
+ buttonStyles: btnS = {},
102
+ modalStyles: mdlS = {},
103
+ className,
104
+ style,
105
+ onOpen,
106
+ onClose,
107
+ onUpload,
108
+ onProcessing,
109
+ onComplete,
110
+ onError
111
+ }) {
112
+ const [view, setView] = useState("idle");
113
+ const [selectedFile, setSelectedFile] = useState(null);
114
+ const [previewUrl, setPreviewUrl] = useState(null);
115
+ const [resultImageUrl, setResultImageUrl] = useState(null);
116
+ const [errorMessage, setErrorMessage] = useState(null);
117
+ const [dragOver, setDragOver] = useState(false);
118
+ const fileInputRef = useRef(null);
119
+ const apiRef = useRef(null);
120
+ const sseRef = useRef(null);
121
+ const unsubRef = useRef(null);
122
+ const pollingRef = useRef(null);
123
+ useEffect(() => {
124
+ try {
125
+ const key = getApiKey();
126
+ const url = getApiUrl(apiUrl);
127
+ apiRef.current = new ApiClient(key, url);
128
+ sseRef.current = new SseClient(apiRef.current.getStreamUrl());
129
+ } catch {
130
+ }
131
+ return () => {
132
+ unsubRef.current?.();
133
+ sseRef.current?.disconnect();
134
+ if (pollingRef.current) clearInterval(pollingRef.current);
135
+ };
136
+ }, [apiUrl]);
137
+ useEffect(() => {
138
+ return () => {
139
+ if (previewUrl) URL.revokeObjectURL(previewUrl);
140
+ };
141
+ }, [previewUrl]);
142
+ const handleOpen = useCallback(() => {
143
+ setView("upload");
144
+ onOpen?.();
145
+ }, [onOpen]);
146
+ const handleClose = useCallback(() => {
147
+ setView("idle");
148
+ setSelectedFile(null);
149
+ if (previewUrl) URL.revokeObjectURL(previewUrl);
150
+ setPreviewUrl(null);
151
+ setResultImageUrl(null);
152
+ setErrorMessage(null);
153
+ unsubRef.current?.();
154
+ unsubRef.current = null;
155
+ if (pollingRef.current) {
156
+ clearInterval(pollingRef.current);
157
+ pollingRef.current = null;
158
+ }
159
+ onClose?.();
160
+ }, [onClose, previewUrl]);
161
+ const handleFileSelect = useCallback(
162
+ (file) => {
163
+ if (!isValidImageFile(file)) {
164
+ setErrorMessage("Please upload a JPEG, PNG, or WebP image.");
165
+ setView("error");
166
+ onError?.({ message: "Invalid file type", code: "INVALID_FILE" });
167
+ return;
168
+ }
169
+ if (file.size > 10 * 1024 * 1024) {
170
+ setErrorMessage("Image must be under 10MB.");
171
+ setView("error");
172
+ onError?.({ message: "File too large", code: "FILE_TOO_LARGE" });
173
+ return;
174
+ }
175
+ setSelectedFile(file);
176
+ setPreviewUrl(URL.createObjectURL(file));
177
+ onUpload?.(file);
178
+ },
179
+ [onUpload, onError]
180
+ );
181
+ const handleRemovePreview = useCallback(() => {
182
+ setSelectedFile(null);
183
+ if (previewUrl) URL.revokeObjectURL(previewUrl);
184
+ setPreviewUrl(null);
185
+ }, [previewUrl]);
186
+ const handleVtoUpdate = useCallback(
187
+ (update, currentView) => {
188
+ if (update.status === "completed" && update.imageUrl) {
189
+ setResultImageUrl((prev) => {
190
+ if (prev?.startsWith("data:") || !prev) return update.imageUrl;
191
+ if (!update.imageUrl.startsWith("data:")) return update.imageUrl;
192
+ return prev;
193
+ });
194
+ if (currentView !== "result") {
195
+ setView("result");
196
+ onComplete?.({ jobId: update.galleryId, imageUrl: update.imageUrl });
197
+ }
198
+ } else if (update.status === "failed") {
199
+ const msg = update.error || "Try-on generation failed";
200
+ setErrorMessage(msg);
201
+ setView("error");
202
+ onError?.({ message: msg });
203
+ }
204
+ },
205
+ [onComplete, onError]
206
+ );
207
+ const handleSubmit = useCallback(async () => {
208
+ if (!selectedFile || !apiRef.current || !sseRef.current) {
209
+ const msg = !apiRef.current ? "Missing NEXT_PUBLIC_PRIMESTYLE_API_KEY in environment variables" : "No file selected";
210
+ setErrorMessage(msg);
211
+ setView("error");
212
+ onError?.({ message: msg, code: "SDK_NOT_CONFIGURED" });
213
+ return;
214
+ }
215
+ setView("processing");
216
+ try {
217
+ const modelImage = await compressImage(selectedFile);
218
+ const response = await apiRef.current.submitTryOn(
219
+ modelImage,
220
+ productImage
221
+ );
222
+ onProcessing?.(response.jobId);
223
+ unsubRef.current = sseRef.current.onJob(
224
+ response.jobId,
225
+ (update) => handleVtoUpdate(update, view)
226
+ );
227
+ let attempts = 0;
228
+ pollingRef.current = setInterval(async () => {
229
+ attempts++;
230
+ if (attempts > 60) {
231
+ if (pollingRef.current) clearInterval(pollingRef.current);
232
+ return;
233
+ }
234
+ try {
235
+ const status = await apiRef.current.getStatus(response.jobId);
236
+ if (status.status === "completed" || status.status === "failed") {
237
+ handleVtoUpdate(
238
+ {
239
+ galleryId: response.jobId,
240
+ status: status.status,
241
+ imageUrl: status.imageUrl,
242
+ error: status.status === "failed" ? status.message : null,
243
+ timestamp: Date.now()
244
+ },
245
+ view
246
+ );
247
+ if (pollingRef.current) clearInterval(pollingRef.current);
248
+ }
249
+ } catch {
250
+ }
251
+ }, 2e3);
252
+ } catch (err) {
253
+ const message = err instanceof Error ? err.message : "Failed to start try-on";
254
+ const code = err instanceof PrimeStyleError ? err.code : void 0;
255
+ setErrorMessage(message);
256
+ setView("error");
257
+ onError?.({ message, code });
258
+ }
259
+ }, [selectedFile, productImage, onProcessing, onError, handleVtoUpdate, view]);
260
+ const handleDownload = useCallback(() => {
261
+ if (!resultImageUrl) return;
262
+ if (resultImageUrl.startsWith("data:")) {
263
+ const link = document.createElement("a");
264
+ link.href = resultImageUrl;
265
+ link.download = `primestyle-tryon-${Date.now()}.png`;
266
+ link.click();
267
+ } else {
268
+ fetch(resultImageUrl).then((r) => r.blob()).then((blob) => {
269
+ const url = URL.createObjectURL(blob);
270
+ const link = document.createElement("a");
271
+ link.href = url;
272
+ link.download = `primestyle-tryon-${Date.now()}.png`;
273
+ link.click();
274
+ setTimeout(() => URL.revokeObjectURL(url), 100);
275
+ }).catch(() => window.open(resultImageUrl, "_blank"));
276
+ }
277
+ }, [resultImageUrl]);
278
+ const handleRetry = useCallback(() => {
279
+ setSelectedFile(null);
280
+ if (previewUrl) URL.revokeObjectURL(previewUrl);
281
+ setPreviewUrl(null);
282
+ setResultImageUrl(null);
283
+ setErrorMessage(null);
284
+ setView("upload");
285
+ }, [previewUrl]);
286
+ const rootVars = {
287
+ "--ps-btn-bg": btnS.backgroundColor,
288
+ "--ps-btn-color": btnS.textColor,
289
+ "--ps-btn-radius": btnS.borderRadius,
290
+ "--ps-btn-font-size": btnS.fontSize,
291
+ "--ps-btn-font": btnS.fontFamily,
292
+ "--ps-btn-font-weight": btnS.fontWeight,
293
+ "--ps-btn-padding": btnS.padding,
294
+ "--ps-btn-border": btnS.border,
295
+ "--ps-btn-width": btnS.width,
296
+ "--ps-btn-height": btnS.height,
297
+ "--ps-btn-hover-bg": btnS.hoverBackgroundColor,
298
+ "--ps-btn-hover-color": btnS.hoverTextColor,
299
+ "--ps-btn-icon-size": btnS.iconSize,
300
+ "--ps-btn-icon-color": btnS.iconColor,
301
+ "--ps-btn-shadow": btnS.boxShadow,
302
+ "--ps-modal-overlay": mdlS.overlayColor,
303
+ "--ps-modal-bg": mdlS.backgroundColor,
304
+ "--ps-modal-color": mdlS.textColor,
305
+ "--ps-modal-radius": mdlS.borderRadius,
306
+ "--ps-modal-width": mdlS.width,
307
+ "--ps-modal-max-width": mdlS.maxWidth,
308
+ "--ps-modal-font": mdlS.fontFamily,
309
+ "--ps-modal-header-bg": mdlS.headerBackgroundColor,
310
+ "--ps-modal-header-color": mdlS.headerTextColor,
311
+ "--ps-modal-close-color": mdlS.closeButtonColor,
312
+ "--ps-upload-border": mdlS.uploadBorderColor,
313
+ "--ps-upload-bg": mdlS.uploadBackgroundColor,
314
+ "--ps-upload-color": mdlS.uploadTextColor,
315
+ "--ps-upload-icon-color": mdlS.uploadIconColor,
316
+ "--ps-modal-primary-bg": mdlS.primaryButtonBackgroundColor,
317
+ "--ps-modal-primary-color": mdlS.primaryButtonTextColor,
318
+ "--ps-modal-primary-radius": mdlS.primaryButtonBorderRadius,
319
+ "--ps-loader": mdlS.loaderColor,
320
+ "--ps-result-radius": mdlS.resultBorderRadius
321
+ };
322
+ const cssVars = Object.fromEntries(
323
+ Object.entries(rootVars).filter(([, v]) => v !== void 0)
324
+ );
325
+ return /* @__PURE__ */ jsxs("div", { className, style: { ...cssVars, ...style, display: "inline-block" }, children: [
326
+ /* @__PURE__ */ jsxs("button", { onClick: handleOpen, className: "ps-tryon-btn", children: [
327
+ /* @__PURE__ */ jsx(CameraIcon, { size: parseInt(btnS.iconSize || "18") }),
328
+ /* @__PURE__ */ jsx("span", { children: buttonText })
329
+ ] }),
330
+ view !== "idle" && /* @__PURE__ */ jsx(
331
+ "div",
332
+ {
333
+ className: "ps-tryon-overlay",
334
+ onClick: (e) => {
335
+ if (e.target === e.currentTarget) handleClose();
336
+ },
337
+ children: /* @__PURE__ */ jsxs("div", { className: "ps-tryon-modal", onClick: (e) => e.stopPropagation(), children: [
338
+ /* @__PURE__ */ jsxs("div", { className: "ps-tryon-header", children: [
339
+ /* @__PURE__ */ jsx("span", { className: "ps-tryon-title", children: "Virtual Try-On" }),
340
+ /* @__PURE__ */ jsx("button", { onClick: handleClose, className: "ps-tryon-close", children: /* @__PURE__ */ jsx(XIcon, {}) })
341
+ ] }),
342
+ /* @__PURE__ */ jsxs("div", { className: "ps-tryon-body", children: [
343
+ view === "upload" && /* @__PURE__ */ jsx(Fragment, { children: selectedFile && previewUrl ? /* @__PURE__ */ jsxs(Fragment, { children: [
344
+ /* @__PURE__ */ jsxs("div", { className: "ps-tryon-preview", children: [
345
+ /* @__PURE__ */ jsx("img", { src: previewUrl, alt: "Your photo" }),
346
+ /* @__PURE__ */ jsx(
347
+ "button",
348
+ {
349
+ onClick: handleRemovePreview,
350
+ className: "ps-tryon-preview-remove",
351
+ children: "×"
352
+ }
353
+ )
354
+ ] }),
355
+ /* @__PURE__ */ jsx("button", { onClick: handleSubmit, className: "ps-tryon-submit", children: "Try It On" })
356
+ ] }) : /* @__PURE__ */ jsxs(
357
+ "div",
358
+ {
359
+ className: `ps-tryon-upload ${dragOver ? "ps-tryon-drag-over" : ""}`,
360
+ onClick: () => fileInputRef.current?.click(),
361
+ onDragOver: (e) => {
362
+ e.preventDefault();
363
+ setDragOver(true);
364
+ },
365
+ onDragLeave: () => setDragOver(false),
366
+ onDrop: (e) => {
367
+ e.preventDefault();
368
+ setDragOver(false);
369
+ const file = e.dataTransfer?.files?.[0];
370
+ if (file) handleFileSelect(file);
371
+ },
372
+ children: [
373
+ /* @__PURE__ */ jsx(
374
+ "input",
375
+ {
376
+ ref: fileInputRef,
377
+ type: "file",
378
+ accept: "image/jpeg,image/png,image/webp",
379
+ style: { display: "none" },
380
+ onChange: (e) => {
381
+ const file = e.target.files?.[0];
382
+ if (file) handleFileSelect(file);
383
+ }
384
+ }
385
+ ),
386
+ /* @__PURE__ */ jsx(UploadIcon, {}),
387
+ /* @__PURE__ */ jsx("p", { className: "ps-tryon-upload-text", children: "Drop your photo here or click to upload" }),
388
+ /* @__PURE__ */ jsx("p", { className: "ps-tryon-upload-hint", children: "JPEG, PNG or WebP (max 10MB)" })
389
+ ]
390
+ }
391
+ ) }),
392
+ view === "processing" && /* @__PURE__ */ jsxs("div", { className: "ps-tryon-processing", children: [
393
+ /* @__PURE__ */ jsx("div", { className: "ps-tryon-spinner" }),
394
+ /* @__PURE__ */ jsx("p", { className: "ps-tryon-processing-text", children: "Generating your try-on..." }),
395
+ /* @__PURE__ */ jsx("p", { className: "ps-tryon-processing-sub", children: "This usually takes 15-20 seconds" })
396
+ ] }),
397
+ view === "result" && resultImageUrl && /* @__PURE__ */ jsxs("div", { className: "ps-tryon-result", children: [
398
+ /* @__PURE__ */ jsx("img", { src: resultImageUrl, alt: "Try-on result" }),
399
+ /* @__PURE__ */ jsxs("div", { className: "ps-tryon-result-actions", children: [
400
+ /* @__PURE__ */ jsx("button", { onClick: handleDownload, className: "ps-tryon-btn-download", children: "Download" }),
401
+ /* @__PURE__ */ jsx("button", { onClick: handleRetry, className: "ps-tryon-btn-retry", children: "Try Another" })
402
+ ] })
403
+ ] }),
404
+ view === "error" && /* @__PURE__ */ jsxs("div", { className: "ps-tryon-error", children: [
405
+ /* @__PURE__ */ jsx(AlertIcon, {}),
406
+ /* @__PURE__ */ jsx("p", { className: "ps-tryon-error-text", children: errorMessage || "Something went wrong" }),
407
+ /* @__PURE__ */ jsx("button", { onClick: handleRetry, className: "ps-tryon-submit", children: "Try Again" })
408
+ ] })
409
+ ] }),
410
+ showPoweredBy && /* @__PURE__ */ jsxs("div", { className: "ps-tryon-powered", children: [
411
+ "Powered by",
412
+ " ",
413
+ /* @__PURE__ */ jsx(
414
+ "a",
415
+ {
416
+ href: "https://primestyleai.com",
417
+ target: "_blank",
418
+ rel: "noopener noreferrer",
419
+ children: "PrimeStyle AI"
420
+ }
421
+ )
422
+ ] })
423
+ ] })
424
+ }
425
+ ),
426
+ /* @__PURE__ */ jsx("style", { children: `
427
+ .ps-tryon-btn {
428
+ display: inline-flex;
429
+ align-items: center;
430
+ gap: 8px;
431
+ padding: var(--ps-btn-padding, 12px 24px);
432
+ background: var(--ps-btn-bg, #bb945c);
433
+ color: var(--ps-btn-color, #111211);
434
+ font-family: var(--ps-btn-font, system-ui, -apple-system, sans-serif);
435
+ font-size: var(--ps-btn-font-size, 14px);
436
+ font-weight: var(--ps-btn-font-weight, 600);
437
+ border: var(--ps-btn-border, none);
438
+ border-radius: var(--ps-btn-radius, 8px);
439
+ cursor: pointer;
440
+ transition: all 0.2s ease;
441
+ width: var(--ps-btn-width, auto);
442
+ height: var(--ps-btn-height, auto);
443
+ box-shadow: var(--ps-btn-shadow, none);
444
+ line-height: 1;
445
+ white-space: nowrap;
446
+ }
447
+ .ps-tryon-btn:hover {
448
+ background: var(--ps-btn-hover-bg, #a07d4e);
449
+ color: var(--ps-btn-hover-color, var(--ps-btn-color, #111211));
450
+ transform: translateY(-1px);
451
+ }
452
+ .ps-tryon-btn:active { transform: translateY(0); }
453
+
454
+ .ps-tryon-overlay {
455
+ position: fixed;
456
+ inset: 0;
457
+ background: var(--ps-modal-overlay, rgba(0,0,0,0.6));
458
+ display: flex;
459
+ align-items: center;
460
+ justify-content: center;
461
+ z-index: 999999;
462
+ padding: 16px;
463
+ animation: ps-fade-in 0.2s ease;
464
+ }
465
+ @keyframes ps-fade-in {
466
+ from { opacity: 0; }
467
+ to { opacity: 1; }
468
+ }
469
+
470
+ .ps-tryon-modal {
471
+ background: var(--ps-modal-bg, #111211);
472
+ color: var(--ps-modal-color, #ffffff);
473
+ border-radius: var(--ps-modal-radius, 12px);
474
+ width: var(--ps-modal-width, 100%);
475
+ max-width: var(--ps-modal-max-width, 480px);
476
+ max-height: 90vh;
477
+ overflow-y: auto;
478
+ font-family: var(--ps-modal-font, system-ui, -apple-system, sans-serif);
479
+ box-shadow: 0 25px 50px rgba(0,0,0,0.4);
480
+ animation: ps-slide-up 0.25s ease;
481
+ }
482
+ @keyframes ps-slide-up {
483
+ from { transform: translateY(20px) scale(0.97); opacity: 0; }
484
+ to { transform: translateY(0) scale(1); opacity: 1; }
485
+ }
486
+
487
+ .ps-tryon-header {
488
+ display: flex;
489
+ align-items: center;
490
+ justify-content: space-between;
491
+ padding: 20px 24px;
492
+ background: var(--ps-modal-header-bg, #1a1b1a);
493
+ border-bottom: 1px solid #333;
494
+ border-radius: var(--ps-modal-radius, 12px) var(--ps-modal-radius, 12px) 0 0;
495
+ }
496
+ .ps-tryon-title {
497
+ font-size: 16px;
498
+ font-weight: 600;
499
+ color: var(--ps-modal-header-color, #fff);
500
+ }
501
+ .ps-tryon-close {
502
+ width: 32px;
503
+ height: 32px;
504
+ display: flex;
505
+ align-items: center;
506
+ justify-content: center;
507
+ background: none;
508
+ border: none;
509
+ color: var(--ps-modal-close-color, #999);
510
+ cursor: pointer;
511
+ border-radius: 6px;
512
+ transition: background 0.15s;
513
+ }
514
+ .ps-tryon-close:hover { background: rgba(255,255,255,0.1); }
515
+
516
+ .ps-tryon-body { padding: 24px; }
517
+
518
+ .ps-tryon-upload {
519
+ border: 2px dashed var(--ps-upload-border, #333);
520
+ border-radius: 12px;
521
+ padding: 40px 24px;
522
+ text-align: center;
523
+ cursor: pointer;
524
+ transition: all 0.2s;
525
+ background: var(--ps-upload-bg, transparent);
526
+ display: flex;
527
+ flex-direction: column;
528
+ align-items: center;
529
+ }
530
+ .ps-tryon-upload:hover, .ps-tryon-drag-over {
531
+ border-color: #bb945c;
532
+ background: rgba(187,148,92,0.05);
533
+ }
534
+ .ps-tryon-upload svg {
535
+ color: var(--ps-upload-icon-color, #bb945c);
536
+ margin-bottom: 12px;
537
+ }
538
+ .ps-tryon-upload-text {
539
+ font-size: 14px;
540
+ color: var(--ps-upload-color, #fff);
541
+ margin: 0 0 4px;
542
+ }
543
+ .ps-tryon-upload-hint {
544
+ font-size: 12px;
545
+ color: #999;
546
+ margin: 0;
547
+ }
548
+
549
+ .ps-tryon-preview {
550
+ position: relative;
551
+ margin-bottom: 4px;
552
+ }
553
+ .ps-tryon-preview img {
554
+ width: 100%;
555
+ border-radius: 12px;
556
+ display: block;
557
+ }
558
+ .ps-tryon-preview-remove {
559
+ position: absolute;
560
+ top: 8px;
561
+ right: 8px;
562
+ width: 28px;
563
+ height: 28px;
564
+ border-radius: 50%;
565
+ background: rgba(0,0,0,0.6);
566
+ border: none;
567
+ color: white;
568
+ cursor: pointer;
569
+ display: flex;
570
+ align-items: center;
571
+ justify-content: center;
572
+ font-size: 16px;
573
+ transition: background 0.15s;
574
+ }
575
+ .ps-tryon-preview-remove:hover { background: rgba(0,0,0,0.8); }
576
+
577
+ .ps-tryon-submit {
578
+ width: 100%;
579
+ padding: 14px;
580
+ margin-top: 20px;
581
+ background: var(--ps-modal-primary-bg, #bb945c);
582
+ color: var(--ps-modal-primary-color, #111211);
583
+ font-family: var(--ps-modal-font, system-ui, sans-serif);
584
+ font-size: 14px;
585
+ font-weight: 600;
586
+ border: none;
587
+ border-radius: var(--ps-modal-primary-radius, 8px);
588
+ cursor: pointer;
589
+ transition: all 0.2s;
590
+ display: flex;
591
+ align-items: center;
592
+ justify-content: center;
593
+ gap: 8px;
594
+ }
595
+ .ps-tryon-submit:hover { opacity: 0.9; transform: translateY(-1px); }
596
+ .ps-tryon-submit:disabled { opacity: 0.5; cursor: not-allowed; }
597
+
598
+ .ps-tryon-processing {
599
+ text-align: center;
600
+ padding: 40px 24px;
601
+ }
602
+ .ps-tryon-spinner {
603
+ width: 48px;
604
+ height: 48px;
605
+ border: 3px solid #333;
606
+ border-top-color: var(--ps-loader, #bb945c);
607
+ border-radius: 50%;
608
+ animation: ps-spin 0.8s linear infinite;
609
+ margin: 0 auto 16px;
610
+ }
611
+ @keyframes ps-spin { to { transform: rotate(360deg); } }
612
+ .ps-tryon-processing-text { font-size: 14px; color: #fff; margin: 0 0 4px; }
613
+ .ps-tryon-processing-sub { font-size: 12px; color: #999; margin: 0; }
614
+
615
+ .ps-tryon-result { text-align: center; }
616
+ .ps-tryon-result img {
617
+ width: 100%;
618
+ border-radius: var(--ps-result-radius, 12px);
619
+ display: block;
620
+ margin-bottom: 16px;
621
+ }
622
+ .ps-tryon-result-actions { display: flex; gap: 8px; }
623
+ .ps-tryon-result-actions button {
624
+ flex: 1;
625
+ padding: 12px;
626
+ font-family: var(--ps-modal-font, system-ui, sans-serif);
627
+ font-size: 13px;
628
+ font-weight: 600;
629
+ border-radius: 8px;
630
+ cursor: pointer;
631
+ transition: all 0.2s;
632
+ border: none;
633
+ }
634
+ .ps-tryon-btn-download { background: #bb945c; color: #111211; }
635
+ .ps-tryon-btn-download:hover { opacity: 0.9; }
636
+ .ps-tryon-btn-retry {
637
+ background: rgba(255,255,255,0.1);
638
+ color: #fff;
639
+ border: 1px solid #333 !important;
640
+ }
641
+ .ps-tryon-btn-retry:hover { background: rgba(255,255,255,0.15); }
642
+
643
+ .ps-tryon-error {
644
+ text-align: center;
645
+ padding: 24px;
646
+ display: flex;
647
+ flex-direction: column;
648
+ align-items: center;
649
+ }
650
+ .ps-tryon-error svg { color: #ef4444; margin-bottom: 12px; }
651
+ .ps-tryon-error-text { font-size: 14px; color: #ef4444; margin: 0 0 16px; }
652
+
653
+ .ps-tryon-powered {
654
+ text-align: center;
655
+ padding: 12px 24px 16px;
656
+ font-size: 11px;
657
+ color: #999;
658
+ }
659
+ .ps-tryon-powered a { color: #bb945c; text-decoration: none; }
660
+ .ps-tryon-powered a:hover { text-decoration: underline; }
661
+ ` })
662
+ ] });
663
+ }
664
+ export {
665
+ PrimeStyleTryon
666
+ };