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