@primestyleai/tryon 1.2.1 → 1.3.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 CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <a href="https://primestyleai.com">
2
+ <a href="https://myaifitting.com">
3
3
  <img src="https://cdn.jsdelivr.net/npm/@primestyleai/tryon/logo.svg" alt="PrimeStyle AI" width="180" />
4
4
  </a>
5
5
  </p>
@@ -22,10 +22,10 @@
22
22
  </p>
23
23
 
24
24
  <p align="center">
25
- <a href="https://primestyleai.com/developer/demo">Live Demo</a> &bull;
26
- <a href="https://primestyleai.com/docs">Documentation</a> &bull;
27
- <a href="https://primestyleai.com/dashboard/developer/keys">Get API Key</a> &bull;
28
- <a href="https://primestyleai.com/pricing">Pricing</a>
25
+ <a href="https://myaifitting.com/developer/demo">Live Demo</a> &bull;
26
+ <a href="https://myaifitting.com/docs">Documentation</a> &bull;
27
+ <a href="https://myaifitting.com/developer/dashboard/keys">Get API Key</a> &bull;
28
+ <a href="https://myaifitting.com/pricing">Pricing</a>
29
29
  </p>
30
30
 
31
31
  ---
@@ -51,7 +51,7 @@ npm install @primestyleai/tryon
51
51
 
52
52
  ### 2. Set Your API Key
53
53
 
54
- Get your key from the [Developer Dashboard](https://primestyleai.com/dashboard/developer/keys) and add it to your `.env.local`:
54
+ Get your key from the [Developer Dashboard](https://myaifitting.com/developer/dashboard/keys) and add it to your `.env.local`:
55
55
 
56
56
  ```env
57
57
  NEXT_PUBLIC_PRIMESTYLE_API_KEY=ps_live_your_key_here
@@ -324,7 +324,7 @@ Priority: Tailwind/classNames classes > CSS variables (buttonStyles/modalStyles)
324
324
  | Variable | Required | Default | Description |
325
325
  |----------|----------|---------|-------------|
326
326
  | `NEXT_PUBLIC_PRIMESTYLE_API_KEY` | Yes | — | Your PrimeStyle API key |
327
- | `NEXT_PUBLIC_PRIMESTYLE_API_URL` | No | `https://api.primestyleai.com` | Custom API endpoint |
327
+ | `NEXT_PUBLIC_PRIMESTYLE_API_URL` | No | `https://myaifitting.com` | Custom API endpoint |
328
328
 
329
329
  The component reads these automatically — no need to pass them as props.
330
330
 
@@ -340,7 +340,7 @@ Each virtual try-on consumes **tokens** from your account balance.
340
340
  | **Token packs** | Pay-as-you-go, never expire |
341
341
  | **Monthly plans** | Volume discounts for high-traffic stores |
342
342
 
343
- Manage your balance and purchase tokens at [primestyleai.com/dashboard/billing](https://primestyleai.com/dashboard/billing).
343
+ Manage your balance and purchase tokens at [primestyleai.com/developer/dashboard/billing](https://myaifitting.com/developer/dashboard/billing).
344
344
 
345
345
  If a try-on fails, tokens are **automatically refunded** to your account.
346
346
 
@@ -357,7 +357,7 @@ onComplete={(result) => {
357
357
 
358
358
  ## API Key Management
359
359
 
360
- Create and manage your API keys at the [Developer Dashboard](https://primestyleai.com/dashboard/developer/keys).
360
+ Create and manage your API keys at the [Developer Dashboard](https://myaifitting.com/developer/dashboard/keys).
361
361
 
362
362
  - Keys start with `ps_live_` and are shown **once** at creation
363
363
  - Set **allowed domains** to restrict where your key can be used
@@ -421,13 +421,13 @@ export default function ProductPage({ product }) {
421
421
 
422
422
  ## Need Help?
423
423
 
424
- - [Live Demo](https://primestyleai.com/developer/demo) — See it in action
425
- - [Documentation](https://primestyleai.com/docs) — Full API reference
426
- - [Dashboard](https://primestyleai.com/dashboard/developer/keys) — Manage keys & tokens
424
+ - [Live Demo](https://myaifitting.com/developer/demo) — See it in action
425
+ - [Documentation](https://myaifitting.com/docs) — Full API reference
426
+ - [Dashboard](https://myaifitting.com/developer/dashboard/keys) — Manage keys & tokens
427
427
  - [Contact](mailto:support@primestyleai.com) — We're here to help
428
428
 
429
429
  ---
430
430
 
431
431
  <p align="center">
432
- Built with care by <a href="https://primestyleai.com">PrimeStyle AI</a>
432
+ Built with care by <a href="https://myaifitting.com">PrimeStyle AI</a>
433
433
  </p>
@@ -22,6 +22,9 @@ export declare class PrimeStyleTryon extends HTMLElement {
22
22
  setButtonStyles(styles: ButtonStyles): void;
23
23
  /** Configure modal appearance programmatically */
24
24
  setModalStyles(styles: ModalStyles): void;
25
+ private savedScrollY;
26
+ private lockBodyScroll;
27
+ private unlockBodyScroll;
25
28
  /** Open the try-on modal */
26
29
  open(): void;
27
30
  /** Close the try-on modal */
@@ -1,4 +1,4 @@
1
- const DEFAULT_API_URL = "https://api.primestyleai.com";
1
+ const DEFAULT_API_URL = "https://myaifitting.com";
2
2
  class ApiClient {
3
3
  constructor(apiKey, apiUrl) {
4
4
  this.apiKey = apiKey;
@@ -1,5 +1,5 @@
1
- import { A as ApiClient, S as SseClient, i as isValidImageFile, c as compressImage } from "./image-utils-usff6Qu8.js";
2
- import { P } from "./image-utils-usff6Qu8.js";
1
+ import { A as ApiClient, S as SseClient, i as isValidImageFile, c as compressImage } from "./image-utils-DtJVYQIA.js";
2
+ import { P } from "./image-utils-DtJVYQIA.js";
3
3
  function detectProductImage() {
4
4
  const ogImage = document.querySelector(
5
5
  'meta[property="og:image"]'
@@ -459,6 +459,7 @@ class PrimeStyleTryon extends HTMLElement {
459
459
  this.productImageUrl = null;
460
460
  this.buttonStyles = {};
461
461
  this.modalStyles = {};
462
+ this.savedScrollY = 0;
462
463
  this.shadow = this.attachShadow({ mode: "open" });
463
464
  }
464
465
  static get observedAttributes() {
@@ -512,15 +513,33 @@ class PrimeStyleTryon extends HTMLElement {
512
513
  this.modalStyles = { ...this.modalStyles, ...styles };
513
514
  this.applyModalStyles();
514
515
  }
516
+ lockBodyScroll() {
517
+ this.savedScrollY = window.scrollY;
518
+ document.body.style.overflow = "hidden";
519
+ document.body.style.position = "fixed";
520
+ document.body.style.top = `-${this.savedScrollY}px`;
521
+ document.body.style.left = "0";
522
+ document.body.style.right = "0";
523
+ }
524
+ unlockBodyScroll() {
525
+ document.body.style.overflow = "";
526
+ document.body.style.position = "";
527
+ document.body.style.top = "";
528
+ document.body.style.left = "";
529
+ document.body.style.right = "";
530
+ window.scrollTo(0, this.savedScrollY);
531
+ }
515
532
  /** Open the try-on modal */
516
533
  open() {
517
534
  this.state = "upload";
535
+ this.lockBodyScroll();
518
536
  this.render();
519
537
  this.emit("ps:open");
520
538
  }
521
539
  /** Close the try-on modal */
522
540
  close() {
523
541
  this.state = "idle";
542
+ this.unlockBodyScroll();
524
543
  this.resetUpload();
525
544
  this.render();
526
545
  this.emit("ps:close");
@@ -567,6 +586,9 @@ class PrimeStyleTryon extends HTMLElement {
567
586
  this.sseClient = new SseClient(this.apiClient.getStreamUrl());
568
587
  }
569
588
  cleanup() {
589
+ if (this.state !== "idle") {
590
+ this.unlockBodyScroll();
591
+ }
570
592
  if (this.sseUnsubscribe) {
571
593
  this.sseUnsubscribe();
572
594
  this.sseUnsubscribe = null;
@@ -653,7 +675,7 @@ class PrimeStyleTryon extends HTMLElement {
653
675
  if (this.showPoweredBy) {
654
676
  const powered = document.createElement("div");
655
677
  powered.className = "ps-powered";
656
- powered.innerHTML = `Powered by <a href="https://primestyleai.com" target="_blank" rel="noopener">PrimeStyle AI</a>`;
678
+ powered.innerHTML = `Powered by <a href="https://myaifitting.com" target="_blank" rel="noopener">PrimeStyle AI</a>`;
657
679
  modal.appendChild(powered);
658
680
  }
659
681
  overlay.appendChild(modal);
@@ -5,7 +5,7 @@ export interface PrimeStyleTryonProps {
5
5
  productImage: string;
6
6
  /** Button text (defaults to "Virtual Try-On") */
7
7
  buttonText?: string;
8
- /** API base URL — defaults to NEXT_PUBLIC_PRIMESTYLE_API_URL env or https://api.primestyleai.com */
8
+ /** API base URL — defaults to NEXT_PUBLIC_PRIMESTYLE_API_URL env or https://myaifitting.com */
9
9
  apiUrl?: string;
10
10
  /** Show "Powered by PrimeStyle" in modal footer (defaults to true) */
11
11
  showPoweredBy?: boolean;
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsxs, jsx, Fragment } from "react/jsx-runtime";
3
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";
4
+ import { A as ApiClient, S as SseClient, i as isValidImageFile, c as compressImage, P as PrimeStyleError } from "../image-utils-DtJVYQIA.js";
5
5
  function cx(base, override) {
6
6
  return override ? `${base} ${override}` : base;
7
7
  }
@@ -94,7 +94,7 @@ function getApiKey() {
94
94
  return key;
95
95
  }
96
96
  function getApiUrl(override) {
97
- return override || process.env.NEXT_PUBLIC_PRIMESTYLE_API_URL || "https://api.primestyleai.com";
97
+ return override || process.env.NEXT_PUBLIC_PRIMESTYLE_API_URL || "https://myaifitting.com";
98
98
  }
99
99
  function PrimeStyleTryon({
100
100
  productImage,
@@ -119,6 +119,7 @@ function PrimeStyleTryon({
119
119
  const [resultImageUrl, setResultImageUrl] = useState(null);
120
120
  const [errorMessage, setErrorMessage] = useState(null);
121
121
  const [dragOver, setDragOver] = useState(false);
122
+ const [countdown, setCountdown] = useState(20);
122
123
  const fileInputRef = useRef(null);
123
124
  const apiRef = useRef(null);
124
125
  const sseRef = useRef(null);
@@ -138,11 +139,44 @@ function PrimeStyleTryon({
138
139
  if (pollingRef.current) clearInterval(pollingRef.current);
139
140
  };
140
141
  }, [apiUrl]);
142
+ useEffect(() => {
143
+ if (view === "processing") {
144
+ setCountdown(20);
145
+ const interval = setInterval(() => {
146
+ setCountdown((prev) => {
147
+ if (prev <= 1) {
148
+ clearInterval(interval);
149
+ return 0;
150
+ }
151
+ return prev - 1;
152
+ });
153
+ }, 1e3);
154
+ return () => clearInterval(interval);
155
+ }
156
+ }, [view]);
141
157
  useEffect(() => {
142
158
  return () => {
143
159
  if (previewUrl) URL.revokeObjectURL(previewUrl);
144
160
  };
145
161
  }, [previewUrl]);
162
+ useEffect(() => {
163
+ if (view !== "idle") {
164
+ const scrollY = window.scrollY;
165
+ document.body.style.overflow = "hidden";
166
+ document.body.style.position = "fixed";
167
+ document.body.style.top = `-${scrollY}px`;
168
+ document.body.style.left = "0";
169
+ document.body.style.right = "0";
170
+ return () => {
171
+ document.body.style.overflow = "";
172
+ document.body.style.position = "";
173
+ document.body.style.top = "";
174
+ document.body.style.left = "";
175
+ document.body.style.right = "";
176
+ window.scrollTo(0, scrollY);
177
+ };
178
+ }
179
+ }, [view]);
146
180
  const handleOpen = useCallback(() => {
147
181
  setView("upload");
148
182
  onOpen?.();
@@ -187,26 +221,41 @@ function PrimeStyleTryon({
187
221
  if (previewUrl) URL.revokeObjectURL(previewUrl);
188
222
  setPreviewUrl(null);
189
223
  }, [previewUrl]);
224
+ const completedRef = useRef(false);
225
+ const cleanupJob = useCallback(() => {
226
+ if (pollingRef.current) {
227
+ clearInterval(pollingRef.current);
228
+ pollingRef.current = null;
229
+ }
230
+ unsubRef.current?.();
231
+ unsubRef.current = null;
232
+ }, []);
190
233
  const handleVtoUpdate = useCallback(
191
- (update, currentView) => {
234
+ (update) => {
192
235
  if (update.status === "completed" && update.imageUrl) {
193
236
  setResultImageUrl((prev) => {
194
- if (prev?.startsWith("data:") || !prev) return update.imageUrl;
237
+ if (!prev || prev.startsWith("data:")) return update.imageUrl;
195
238
  if (!update.imageUrl.startsWith("data:")) return update.imageUrl;
196
239
  return prev;
197
240
  });
198
- if (currentView !== "result") {
241
+ if (!completedRef.current) {
242
+ completedRef.current = true;
243
+ cleanupJob();
199
244
  setView("result");
200
245
  onComplete?.({ jobId: update.galleryId, imageUrl: update.imageUrl });
201
246
  }
202
247
  } else if (update.status === "failed") {
203
- const msg = update.error || "Try-on generation failed";
204
- setErrorMessage(msg);
205
- setView("error");
206
- onError?.({ message: msg });
248
+ if (!completedRef.current) {
249
+ completedRef.current = true;
250
+ cleanupJob();
251
+ const msg = update.error || "Try-on generation failed";
252
+ setErrorMessage(msg);
253
+ setView("error");
254
+ onError?.({ message: msg });
255
+ }
207
256
  }
208
257
  },
209
- [onComplete, onError]
258
+ [onComplete, onError, cleanupJob]
210
259
  );
211
260
  const handleSubmit = useCallback(async () => {
212
261
  if (!selectedFile || !apiRef.current || !sseRef.current) {
@@ -216,6 +265,7 @@ function PrimeStyleTryon({
216
265
  onError?.({ message: msg, code: "SDK_NOT_CONFIGURED" });
217
266
  return;
218
267
  }
268
+ completedRef.current = false;
219
269
  setView("processing");
220
270
  try {
221
271
  const modelImage = await compressImage(selectedFile);
@@ -226,33 +276,35 @@ function PrimeStyleTryon({
226
276
  onProcessing?.(response.jobId);
227
277
  unsubRef.current = sseRef.current.onJob(
228
278
  response.jobId,
229
- (update) => handleVtoUpdate(update, view)
279
+ (update) => handleVtoUpdate(update)
230
280
  );
231
281
  let attempts = 0;
232
282
  pollingRef.current = setInterval(async () => {
283
+ if (completedRef.current) {
284
+ if (pollingRef.current) clearInterval(pollingRef.current);
285
+ pollingRef.current = null;
286
+ return;
287
+ }
233
288
  attempts++;
234
289
  if (attempts > 60) {
235
290
  if (pollingRef.current) clearInterval(pollingRef.current);
291
+ pollingRef.current = null;
236
292
  return;
237
293
  }
238
294
  try {
239
295
  const status = await apiRef.current.getStatus(response.jobId);
240
296
  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);
297
+ handleVtoUpdate({
298
+ galleryId: response.jobId,
299
+ status: status.status,
300
+ imageUrl: status.imageUrl,
301
+ error: status.status === "failed" ? status.message : null,
302
+ timestamp: Date.now()
303
+ });
252
304
  }
253
305
  } catch {
254
306
  }
255
- }, 2e3);
307
+ }, 3e3);
256
308
  } catch (err) {
257
309
  const message = err instanceof Error ? err.message : "Failed to start try-on";
258
310
  const code = err instanceof PrimeStyleError ? err.code : void 0;
@@ -260,7 +312,7 @@ function PrimeStyleTryon({
260
312
  setView("error");
261
313
  onError?.({ message, code });
262
314
  }
263
- }, [selectedFile, productImage, onProcessing, onError, handleVtoUpdate, view]);
315
+ }, [selectedFile, productImage, onProcessing, onError, handleVtoUpdate]);
264
316
  const handleDownload = useCallback(() => {
265
317
  if (!resultImageUrl) return;
266
318
  if (resultImageUrl.startsWith("data:")) {
@@ -280,13 +332,15 @@ function PrimeStyleTryon({
280
332
  }
281
333
  }, [resultImageUrl]);
282
334
  const handleRetry = useCallback(() => {
335
+ completedRef.current = false;
336
+ cleanupJob();
283
337
  setSelectedFile(null);
284
338
  if (previewUrl) URL.revokeObjectURL(previewUrl);
285
339
  setPreviewUrl(null);
286
340
  setResultImageUrl(null);
287
341
  setErrorMessage(null);
288
342
  setView("upload");
289
- }, [previewUrl]);
343
+ }, [previewUrl, cleanupJob]);
290
344
  const rootVars = {
291
345
  "--ps-btn-bg": btnS.backgroundColor,
292
346
  "--ps-btn-color": btnS.textColor,
@@ -426,13 +480,38 @@ function PrimeStyleTryon({
426
480
  }
427
481
  ),
428
482
  /* @__PURE__ */ jsx(UploadIcon, {}),
429
- /* @__PURE__ */ jsx("p", { className: cx("ps-tryon-upload-text", cn.uploadText), children: "Drop your photo here or click to upload" }),
483
+ /* @__PURE__ */ jsx("p", { className: cx("ps-tryon-upload-text", cn.uploadText), children: "Drop or upload your full body photo!" }),
430
484
  /* @__PURE__ */ jsx("p", { className: cx("ps-tryon-upload-hint", cn.uploadHint), children: "JPEG, PNG or WebP (max 10MB)" })
431
485
  ]
432
486
  }
433
487
  ) }),
434
488
  view === "processing" && /* @__PURE__ */ jsxs("div", { className: "ps-tryon-processing", children: [
435
- /* @__PURE__ */ jsx("div", { className: cx("ps-tryon-spinner", cn.spinner) }),
489
+ /* @__PURE__ */ jsxs("div", { className: "ps-tryon-countdown-ring", children: [
490
+ /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 120 120", children: [
491
+ /* @__PURE__ */ jsx(
492
+ "circle",
493
+ {
494
+ className: "ps-tryon-countdown-track",
495
+ cx: "60",
496
+ cy: "60",
497
+ r: "52"
498
+ }
499
+ ),
500
+ /* @__PURE__ */ jsx(
501
+ "circle",
502
+ {
503
+ className: "ps-tryon-countdown-progress",
504
+ cx: "60",
505
+ cy: "60",
506
+ r: "52",
507
+ style: {
508
+ strokeDashoffset: `${countdown / 20 * 326.73}`
509
+ }
510
+ }
511
+ )
512
+ ] }),
513
+ /* @__PURE__ */ jsx("span", { className: "ps-tryon-countdown-number", children: countdown })
514
+ ] }),
436
515
  /* @__PURE__ */ jsx(
437
516
  "p",
438
517
  {
@@ -450,7 +529,7 @@ function PrimeStyleTryon({
450
529
  "ps-tryon-processing-sub",
451
530
  cn.processingSubText
452
531
  ),
453
- children: "This usually takes 15-20 seconds"
532
+ children: countdown > 0 ? "This usually takes 15-20 seconds" : "Almost there..."
454
533
  }
455
534
  )
456
535
  ] }),
@@ -510,7 +589,7 @@ function PrimeStyleTryon({
510
589
  /* @__PURE__ */ jsx(
511
590
  "a",
512
591
  {
513
- href: "https://primestyleai.com",
592
+ href: "https://myaifitting.com",
514
593
  target: "_blank",
515
594
  rel: "noopener noreferrer",
516
595
  children: "PrimeStyle AI"
@@ -712,6 +791,43 @@ function PrimeStyleTryon({
712
791
  margin: 0 auto 16px;
713
792
  }
714
793
  @keyframes ps-spin { to { transform: rotate(360deg); } }
794
+
795
+ .ps-tryon-countdown-ring {
796
+ position: relative;
797
+ width: 100px;
798
+ height: 100px;
799
+ margin: 0 auto 20px;
800
+ }
801
+ .ps-tryon-countdown-ring svg {
802
+ width: 100%;
803
+ height: 100%;
804
+ transform: rotate(-90deg);
805
+ }
806
+ .ps-tryon-countdown-track {
807
+ fill: none;
808
+ stroke: #333;
809
+ stroke-width: 4;
810
+ }
811
+ .ps-tryon-countdown-progress {
812
+ fill: none;
813
+ stroke: var(--ps-loader, #bb945c);
814
+ stroke-width: 4;
815
+ stroke-linecap: round;
816
+ stroke-dasharray: 326.73;
817
+ transition: stroke-dashoffset 1s linear;
818
+ }
819
+ .ps-tryon-countdown-number {
820
+ position: absolute;
821
+ inset: 0;
822
+ display: flex;
823
+ align-items: center;
824
+ justify-content: center;
825
+ font-size: 28px;
826
+ font-weight: 700;
827
+ color: #fff;
828
+ font-variant-numeric: tabular-nums;
829
+ }
830
+
715
831
  .ps-tryon-processing-text { font-size: 14px; color: #fff; margin: 0 0 4px; }
716
832
  .ps-tryon-processing-sub { font-size: 12px; color: #999; margin: 0; }
717
833
 
package/dist/types.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  export interface PrimeStyleConfig {
3
3
  /** Your API key (starts with ps_live_) */
4
4
  apiKey: string;
5
- /** API base URL (defaults to https://api.primestyleai.com) */
5
+ /** API base URL (defaults to https://myaifitting.com) */
6
6
  apiUrl?: string;
7
7
  /** Product image URL to try on */
8
8
  productImage?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primestyleai/tryon",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "PrimeStyle Virtual Try-On SDK — React component & Web Component",
5
5
  "type": "module",
6
6
  "main": "dist/primestyle-tryon.js",