@primestyleai/tryon 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.
@@ -0,0 +1 @@
1
+ !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).PrimeStyleTryon={})}(this,function(e){"use strict";class t{constructor(e,t){this.apiKey=e,this.baseUrl=(t||"https://api.primestyleai.com").replace(/\/+$/,"")}get headers(){return{"Content-Type":"application/json",Authorization:`Bearer ${this.apiKey}`}}async submitTryOn(e,t){const s=await fetch(`${this.baseUrl}/api/v1/tryon`,{method:"POST",headers:this.headers,body:JSON.stringify({modelImage:e,garmentImage:t})});if(!s.ok){const e=await s.json().catch(()=>({}));if(402===s.status)throw new n(e.message||"Insufficient tokens","INSUFFICIENT_TOKENS");throw new n(e.message||"Failed to submit try-on","API_ERROR")}return s.json()}async getStatus(e){const t=await fetch(`${this.baseUrl}/api/v1/tryon/status/${e}`,{headers:this.headers});if(!t.ok){const e=await t.json().catch(()=>({}));throw new n(e.message||"Failed to get status","API_ERROR")}return t.json()}getStreamUrl(){return`${this.baseUrl}/api/v1/tryon/stream?key=${encodeURIComponent(this.apiKey)}`}}class n extends Error{constructor(e,t){super(e),this.name="PrimeStyleError",this.code=t}}class s{constructor(e){this.eventSource=null,this.listeners=new Map,this.reconnectTimer=null,this.reconnectAttempts=0,this.maxReconnectAttempts=5,this.streamUrl=e}connect(){this.eventSource||(this.eventSource=new EventSource(this.streamUrl),this.eventSource.addEventListener("vto-update",e=>{try{const t=JSON.parse(e.data);this.emit(t.galleryId,t)}catch{}}),this.eventSource.onopen=()=>{this.reconnectAttempts=0},this.eventSource.onerror=()=>{this.eventSource?.close(),this.eventSource=null,this.scheduleReconnect()})}scheduleReconnect(){if(this.reconnectAttempts>=this.maxReconnectAttempts)return;if(0===this.listeners.size)return;const e=Math.min(1e3*2**this.reconnectAttempts,3e4);this.reconnectAttempts++,this.reconnectTimer=setTimeout(()=>{this.connect()},e)}onJob(e,t){return this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t),this.eventSource||this.connect(),()=>{const n=this.listeners.get(e);n&&(n.delete(t),0===n.size&&this.listeners.delete(e)),0===this.listeners.size&&this.disconnect()}}emit(e,t){const n=this.listeners.get(e);n&&n.forEach(e=>e(t))}disconnect(){this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.eventSource&&(this.eventSource.close(),this.eventSource=null),this.listeners.clear(),this.reconnectAttempts=0}}function r(){const e=document.querySelector('meta[property="og:image"]');if(e?.content)return e.content;const t=document.querySelectorAll('script[type="application/ld+json"]');for(const s of t)try{const e=o(JSON.parse(s.textContent||""));if(e)return e}catch{}const n=["[data-product-image] img","[data-product-image]",".product-image img",".product-gallery img","#product-image",".product__media img",".product-single__photo img",".woocommerce-product-gallery img",".product-media img"];for(const s of n){const e=document.querySelector(s);if(e){const t=e.src||e.dataset.src||e.dataset.zoom;if(t)return t}}return null}function o(e){if(!e||"object"!=typeof e)return null;if(Array.isArray(e)){for(const t of e){const e=o(t);if(e)return e}return null}const t=e;if("Product"===t["@type"]||"IndividualProduct"===t["@type"]){const e=t.image;if("string"==typeof e)return e;if(Array.isArray(e)&&"string"==typeof e[0])return e[0];if(e&&"object"==typeof e){const t=e;if("string"==typeof t.url)return t.url;if("string"==typeof t.contentUrl)return t.contentUrl}}return Array.isArray(t["@graph"])?o(t["@graph"]):null}const i=1024;function a(e){return new Promise((t,n)=>{const s=new FileReader;s.onload=()=>{const e=new Image;e.onload=()=>{try{const s=document.createElement("canvas");let{width:r,height:o}=e;(r>i||o>i)&&(r>o?(o=Math.round(o*i/r),r=i):(r=Math.round(r*i/o),o=i)),s.width=r,s.height=o;const a=s.getContext("2d");if(!a)return void n(new Error("Canvas context not available"));a.drawImage(e,0,0,r,o);const l=s.toDataURL("image/jpeg",.85);t(l)}catch(s){n(s)}},e.onerror=()=>n(new Error("Failed to load image")),e.src=s.result},s.onerror=()=>n(new Error("Failed to read file")),s.readAsDataURL(e)})}function l(e){return["image/jpeg","image/png","image/webp"].includes(e.type)}const p={camera:'<svg viewBox="0 0 24 24"><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"/><circle cx="12" cy="13" r="4"/></svg>',upload:'<svg viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>',x:'<svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>',alert:'<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>'};class d extends HTMLElement{constructor(){super(),this.apiClient=null,this.sseClient=null,this.sseUnsubscribe=null,this.state="idle",this.selectedFile=null,this.previewUrl=null,this.resultImageUrl=null,this.errorMessage=null,this.currentJobId=null,this.productImageUrl=null,this.buttonStyles={},this.modalStyles={},this.shadow=this.attachShadow({mode:"open"})}static get observedAttributes(){return["api-key","api-url","product-image","button-text","locale","show-powered-by","button-styles","modal-styles"]}connectedCallback(){this.init(),this.render()}disconnectedCallback(){this.cleanup()}attributeChangedCallback(e,t,n){if("api-key"!==e&&"api-url"!==e||this.initApi(),"product-image"===e&&(this.productImageUrl=n),"button-styles"===e)try{this.buttonStyles=JSON.parse(n)}catch{}if("modal-styles"===e)try{this.modalStyles=JSON.parse(n)}catch{}this.isConnected&&this.render()}setButtonStyles(e){this.buttonStyles={...this.buttonStyles,...e},this.applyButtonStyles()}setModalStyles(e){this.modalStyles={...this.modalStyles,...e},this.applyModalStyles()}open(){this.state="upload",this.render(),this.emit("ps:open")}close(){this.state="idle",this.resetUpload(),this.render(),this.emit("ps:close")}detectProduct(){const e=r();return e&&(this.productImageUrl=e,this.emit("ps:product-detected",{imageUrl:e})),e}init(){const e=this.getAttribute("button-styles");if(e)try{this.buttonStyles=JSON.parse(e)}catch{}const t=this.getAttribute("modal-styles");if(t)try{this.modalStyles=JSON.parse(t)}catch{}this.productImageUrl=this.getAttribute("product-image")||null,this.productImageUrl||(this.productImageUrl=r(),this.productImageUrl&&this.emit("ps:product-detected",{imageUrl:this.productImageUrl})),this.initApi()}initApi(){const e=this.getAttribute("api-key");if(!e)return;const n=this.getAttribute("api-url")||void 0;this.apiClient=new t(e,n),this.sseClient=new s(this.apiClient.getStreamUrl())}cleanup(){this.sseUnsubscribe&&(this.sseUnsubscribe(),this.sseUnsubscribe=null),this.sseClient&&(this.sseClient.disconnect(),this.sseClient=null),this.previewUrl&&URL.revokeObjectURL(this.previewUrl)}emit(e,t){this.dispatchEvent(new CustomEvent(e,{bubbles:!0,composed:!0,detail:t}))}get buttonText(){return this.getAttribute("button-text")||"Virtual Try-On"}get showPoweredBy(){return"false"!==this.getAttribute("show-powered-by")}render(){this.shadow.innerHTML="";const e=document.createElement("style");e.textContent="\n :host {\n display: inline-block;\n --ps-primary: #bb945c;\n --ps-primary-hover: #a07d4e;\n --ps-text: #ffffff;\n --ps-text-secondary: #999999;\n --ps-bg: #111211;\n --ps-bg-secondary: #1a1b1a;\n --ps-border: #333333;\n --ps-radius: 12px;\n --ps-font: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n --ps-overlay: rgba(0, 0, 0, 0.6);\n --ps-error: #ef4444;\n --ps-success: #22c55e;\n }\n\n * {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n }\n\n /* ── Button ─────────────────────────────── */\n .ps-button {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n padding: var(--ps-btn-padding, 12px 24px);\n background: var(--ps-btn-bg, var(--ps-primary));\n color: var(--ps-btn-color, #111211);\n font-family: var(--ps-btn-font, var(--ps-font));\n font-size: var(--ps-btn-font-size, 14px);\n font-weight: var(--ps-btn-font-weight, 600);\n border: var(--ps-btn-border, none);\n border-radius: var(--ps-btn-radius, 8px);\n cursor: pointer;\n transition: all 0.2s ease;\n width: var(--ps-btn-width, auto);\n height: var(--ps-btn-height, auto);\n box-shadow: var(--ps-btn-shadow, none);\n line-height: 1;\n white-space: nowrap;\n }\n\n .ps-button:hover {\n background: var(--ps-btn-hover-bg, var(--ps-primary-hover));\n color: var(--ps-btn-hover-color, var(--ps-btn-color, #111211));\n transform: translateY(-1px);\n }\n\n .ps-button:active {\n transform: translateY(0);\n }\n\n .ps-button svg {\n width: var(--ps-btn-icon-size, 18px);\n height: var(--ps-btn-icon-size, 18px);\n fill: none;\n stroke: var(--ps-btn-icon-color, currentColor);\n stroke-width: 2;\n stroke-linecap: round;\n stroke-linejoin: round;\n }\n\n /* ── Modal Overlay ──────────────────────── */\n .ps-overlay {\n position: fixed;\n inset: 0;\n background: var(--ps-modal-overlay, var(--ps-overlay));\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 999999;\n opacity: 0;\n visibility: hidden;\n transition: opacity 0.25s ease, visibility 0.25s ease;\n padding: 16px;\n }\n\n .ps-overlay.ps-open {\n opacity: 1;\n visibility: visible;\n }\n\n /* ── Modal ──────────────────────────────── */\n .ps-modal {\n background: var(--ps-modal-bg, var(--ps-bg));\n color: var(--ps-modal-color, var(--ps-text));\n border-radius: var(--ps-modal-radius, var(--ps-radius));\n width: var(--ps-modal-width, 100%);\n max-width: var(--ps-modal-max-width, 480px);\n max-height: 90vh;\n overflow-y: auto;\n font-family: var(--ps-modal-font, var(--ps-font));\n position: relative;\n transform: translateY(20px) scale(0.97);\n transition: transform 0.25s ease;\n box-shadow: 0 25px 50px rgba(0, 0, 0, 0.4);\n }\n\n .ps-open .ps-modal {\n transform: translateY(0) scale(1);\n }\n\n /* ── Modal Header ───────────────────────── */\n .ps-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 20px 24px;\n background: var(--ps-modal-header-bg, var(--ps-bg-secondary));\n border-bottom: 1px solid var(--ps-border);\n border-radius: var(--ps-modal-radius, var(--ps-radius)) var(--ps-modal-radius, var(--ps-radius)) 0 0;\n }\n\n .ps-header-title {\n font-size: 16px;\n font-weight: 600;\n color: var(--ps-modal-header-color, var(--ps-text));\n }\n\n .ps-close {\n width: 32px;\n height: 32px;\n display: flex;\n align-items: center;\n justify-content: center;\n background: none;\n border: none;\n color: var(--ps-modal-close-color, var(--ps-text-secondary));\n cursor: pointer;\n border-radius: 6px;\n transition: background 0.15s;\n }\n\n .ps-close:hover {\n background: rgba(255, 255, 255, 0.1);\n }\n\n .ps-close svg {\n width: 20px;\n height: 20px;\n stroke: currentColor;\n stroke-width: 2;\n fill: none;\n }\n\n /* ── Modal Body ─────────────────────────── */\n .ps-body {\n padding: 24px;\n }\n\n /* ── Upload Zone ─────────────────────────── */\n .ps-upload-zone {\n border: 2px dashed var(--ps-upload-border, var(--ps-border));\n border-radius: var(--ps-radius);\n padding: 40px 24px;\n text-align: center;\n cursor: pointer;\n transition: all 0.2s ease;\n background: var(--ps-upload-bg, transparent);\n }\n\n .ps-upload-zone:hover,\n .ps-upload-zone.ps-drag-over {\n border-color: var(--ps-primary);\n background: rgba(187, 148, 92, 0.05);\n }\n\n .ps-upload-zone input {\n display: none;\n }\n\n .ps-upload-icon {\n width: 48px;\n height: 48px;\n margin: 0 auto 12px;\n stroke: var(--ps-upload-icon-color, var(--ps-primary));\n fill: none;\n stroke-width: 1.5;\n }\n\n .ps-upload-text {\n font-size: 14px;\n color: var(--ps-upload-color, var(--ps-text));\n margin-bottom: 4px;\n }\n\n .ps-upload-hint {\n font-size: 12px;\n color: var(--ps-text-secondary);\n }\n\n /* ── Preview ────────────────────────────── */\n .ps-preview {\n margin-top: 16px;\n position: relative;\n }\n\n .ps-preview img {\n width: 100%;\n border-radius: var(--ps-radius);\n display: block;\n }\n\n .ps-preview-remove {\n position: absolute;\n top: 8px;\n right: 8px;\n width: 28px;\n height: 28px;\n border-radius: 50%;\n background: rgba(0, 0, 0, 0.6);\n border: none;\n color: white;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 16px;\n transition: background 0.15s;\n }\n\n .ps-preview-remove:hover {\n background: rgba(0, 0, 0, 0.8);\n }\n\n /* ── Submit Button ──────────────────────── */\n .ps-submit {\n width: 100%;\n padding: 14px;\n margin-top: 20px;\n background: var(--ps-modal-primary-bg, var(--ps-primary));\n color: var(--ps-modal-primary-color, #111211);\n font-family: var(--ps-modal-font, var(--ps-font));\n font-size: 14px;\n font-weight: 600;\n border: none;\n border-radius: var(--ps-modal-primary-radius, 8px);\n cursor: pointer;\n transition: all 0.2s ease;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n }\n\n .ps-submit:hover:not(:disabled) {\n opacity: 0.9;\n transform: translateY(-1px);\n }\n\n .ps-submit:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n\n /* ── Processing State ───────────────────── */\n .ps-processing {\n text-align: center;\n padding: 40px 24px;\n }\n\n .ps-spinner {\n width: 48px;\n height: 48px;\n border: 3px solid var(--ps-border);\n border-top-color: var(--ps-loader, var(--ps-primary));\n border-radius: 50%;\n animation: ps-spin 0.8s linear infinite;\n margin: 0 auto 16px;\n }\n\n @keyframes ps-spin {\n to { transform: rotate(360deg); }\n }\n\n .ps-processing-text {\n font-size: 14px;\n color: var(--ps-text);\n margin-bottom: 4px;\n }\n\n .ps-processing-sub {\n font-size: 12px;\n color: var(--ps-text-secondary);\n }\n\n /* ── Result ─────────────────────────────── */\n .ps-result {\n text-align: center;\n }\n\n .ps-result img {\n width: 100%;\n border-radius: var(--ps-result-radius, var(--ps-radius));\n display: block;\n margin-bottom: 16px;\n }\n\n .ps-result-actions {\n display: flex;\n gap: 8px;\n }\n\n .ps-result-actions button {\n flex: 1;\n padding: 12px;\n font-family: var(--ps-modal-font, var(--ps-font));\n font-size: 13px;\n font-weight: 600;\n border-radius: 8px;\n cursor: pointer;\n transition: all 0.2s;\n border: none;\n }\n\n .ps-btn-download {\n background: var(--ps-primary);\n color: #111211;\n }\n\n .ps-btn-download:hover {\n opacity: 0.9;\n }\n\n .ps-btn-retry {\n background: rgba(255, 255, 255, 0.1);\n color: var(--ps-text);\n border: 1px solid var(--ps-border) !important;\n }\n\n .ps-btn-retry:hover {\n background: rgba(255, 255, 255, 0.15);\n }\n\n /* ── Error State ────────────────────────── */\n .ps-error {\n text-align: center;\n padding: 24px;\n }\n\n .ps-error-icon {\n width: 48px;\n height: 48px;\n margin: 0 auto 12px;\n stroke: var(--ps-error);\n fill: none;\n stroke-width: 1.5;\n }\n\n .ps-error-text {\n font-size: 14px;\n color: var(--ps-error);\n margin-bottom: 16px;\n }\n\n /* ── Powered By ─────────────────────────── */\n .ps-powered {\n text-align: center;\n padding: 12px 24px 16px;\n font-size: 11px;\n color: var(--ps-text-secondary);\n }\n\n .ps-powered a {\n color: var(--ps-primary);\n text-decoration: none;\n }\n\n .ps-powered a:hover {\n text-decoration: underline;\n }\n ",this.shadow.appendChild(e);const t=this.createButton();if(this.shadow.appendChild(t),"idle"!==this.state){const e=this.createModal();this.shadow.appendChild(e),requestAnimationFrame(()=>e.classList.add("ps-open"))}this.applyButtonStyles(),this.applyModalStyles()}createButton(){const e=document.createElement("button");return e.className="ps-button",e.innerHTML=`${p.camera}<span>${this.buttonText}</span>`,e.addEventListener("click",()=>this.open()),e}createModal(){const e=document.createElement("div");e.className="ps-overlay",e.addEventListener("click",t=>{t.target===e&&this.close()});const t=document.createElement("div");t.className="ps-modal";const n=document.createElement("div");n.className="ps-header",n.innerHTML='\n <span class="ps-header-title">Virtual Try-On</span>\n ';const s=document.createElement("button");s.className="ps-close",s.innerHTML=p.x,s.addEventListener("click",()=>this.close()),n.appendChild(s),t.appendChild(n);const r=document.createElement("div");switch(r.className="ps-body",this.state){case"upload":r.appendChild(this.createUploadView());break;case"processing":r.appendChild(this.createProcessingView());break;case"result":r.appendChild(this.createResultView());break;case"error":r.appendChild(this.createErrorView())}if(t.appendChild(r),this.showPoweredBy){const e=document.createElement("div");e.className="ps-powered",e.innerHTML='Powered by <a href="https://primestyleai.com" target="_blank" rel="noopener">PrimeStyle AI</a>',t.appendChild(e)}return e.appendChild(t),e}createUploadView(){const e=document.createDocumentFragment();if(this.selectedFile&&this.previewUrl){const t=document.createElement("div");t.className="ps-preview";const n=document.createElement("img");n.src=this.previewUrl,n.alt="Your photo",t.appendChild(n);const s=document.createElement("button");s.className="ps-preview-remove",s.textContent="×",s.addEventListener("click",()=>{this.resetUpload(),this.render()}),t.appendChild(s),e.appendChild(t);const r=document.createElement("button");r.className="ps-submit",r.textContent="Try It On",r.addEventListener("click",()=>this.handleSubmit()),e.appendChild(r)}else{const t=document.createElement("div");t.className="ps-upload-zone";const n=document.createElement("input");n.type="file",n.accept="image/jpeg,image/png,image/webp",n.addEventListener("change",e=>{const t=e.target.files?.[0];t&&this.handleFileSelect(t)}),t.innerHTML=`\n <svg class="ps-upload-icon" viewBox="0 0 24 24">${p.upload.replace(/<\/?svg[^>]*>/g,"")}</svg>\n <p class="ps-upload-text">Drop your photo here or click to upload</p>\n <p class="ps-upload-hint">JPEG, PNG or WebP (max 10MB)</p>\n `,t.appendChild(n),t.addEventListener("click",()=>n.click()),t.addEventListener("dragover",e=>{e.preventDefault(),t.classList.add("ps-drag-over")}),t.addEventListener("dragleave",()=>{t.classList.remove("ps-drag-over")}),t.addEventListener("drop",e=>{e.preventDefault(),t.classList.remove("ps-drag-over");const n=e.dataTransfer?.files?.[0];n&&this.handleFileSelect(n)}),e.appendChild(t)}return e}createProcessingView(){const e=document.createElement("div");return e.className="ps-processing",e.innerHTML='\n <div class="ps-spinner"></div>\n <p class="ps-processing-text">Generating your try-on...</p>\n <p class="ps-processing-sub">This usually takes 15-20 seconds</p>\n ',e}createResultView(){const e=document.createElement("div");if(e.className="ps-result",this.resultImageUrl){const t=document.createElement("img");t.src=this.resultImageUrl,t.alt="Try-on result",e.appendChild(t)}const t=document.createElement("div");t.className="ps-result-actions";const n=document.createElement("button");n.className="ps-btn-download",n.textContent="Download",n.addEventListener("click",()=>this.handleDownload()),t.appendChild(n);const s=document.createElement("button");return s.className="ps-btn-retry",s.textContent="Try Another",s.addEventListener("click",()=>{this.resetUpload(),this.state="upload",this.render()}),t.appendChild(s),e.appendChild(t),e}createErrorView(){const e=document.createElement("div");e.className="ps-error",e.innerHTML=`\n <svg class="ps-error-icon" viewBox="0 0 24 24">${p.alert.replace(/<\/?svg[^>]*>/g,"")}</svg>\n <p class="ps-error-text">${this.errorMessage||"Something went wrong"}</p>\n `;const t=document.createElement("button");return t.className="ps-submit",t.textContent="Try Again",t.addEventListener("click",()=>{this.state="upload",this.errorMessage=null,this.render()}),e.appendChild(t),e}handleFileSelect(e){return l(e)?e.size>10485760?(this.errorMessage="Image must be under 10MB.",this.state="error",void this.render()):(this.selectedFile=e,this.previewUrl=URL.createObjectURL(e),this.emit("ps:upload",{file:e}),void this.render()):(this.errorMessage="Please upload a JPEG, PNG, or WebP image.",this.state="error",void this.render())}async handleSubmit(){if(!this.selectedFile||!this.apiClient||!this.sseClient)return this.errorMessage="SDK not configured. Please provide an API key.",this.state="error",void this.render();if(!this.productImageUrl)return this.errorMessage="No product image found. Please set the product-image attribute.",this.state="error",void this.render();this.state="processing",this.render();try{const e=await a(this.selectedFile),t=await this.apiClient.submitTryOn(e,this.productImageUrl);this.currentJobId=t.jobId,this.emit("ps:processing",{jobId:t.jobId}),this.sseUnsubscribe=this.sseClient.onJob(t.jobId,e=>this.handleVtoUpdate(e)),this.startPolling(t.jobId)}catch(e){const t=e instanceof Error?e.message:"Failed to start try-on";this.errorMessage=t,this.state="error",this.emit("ps:error",{message:t,code:e?.code}),this.render()}}handleVtoUpdate(e){"completed"===e.status&&e.imageUrl?("result"!==this.state||this.resultImageUrl?.startsWith("data:")&&!e.imageUrl.startsWith("data:"))&&(this.resultImageUrl=e.imageUrl,this.state="result",this.emit("ps:complete",{jobId:e.galleryId,imageUrl:e.imageUrl}),this.render()):"failed"===e.status&&(this.errorMessage=e.error||"Try-on generation failed",this.state="error",this.emit("ps:error",{message:this.errorMessage}),this.render())}startPolling(e){let t=0;const n=setInterval(async()=>{if(t++,t>60||"result"===this.state||"idle"===this.state)clearInterval(n);else if(this.apiClient)try{const t=await this.apiClient.getStatus(e);"completed"===t.status&&t.imageUrl?("processing"===this.state&&this.handleVtoUpdate({galleryId:e,status:"completed",imageUrl:t.imageUrl,error:null,timestamp:Date.now()}),clearInterval(n)):"failed"===t.status&&("processing"===this.state&&this.handleVtoUpdate({galleryId:e,status:"failed",imageUrl:null,error:t.message,timestamp:Date.now()}),clearInterval(n))}catch{}else clearInterval(n)},2e3)}handleDownload(){if(!this.resultImageUrl)return;const e=document.createElement("a");e.href=this.resultImageUrl,e.download=`primestyle-tryon-${Date.now()}.png`,e.target="_blank",this.resultImageUrl.startsWith("data:")?e.click():fetch(this.resultImageUrl).then(e=>e.blob()).then(t=>{const n=URL.createObjectURL(t);e.href=n,e.click(),setTimeout(()=>URL.revokeObjectURL(n),100)}).catch(()=>{window.open(this.resultImageUrl,"_blank")})}resetUpload(){this.previewUrl&&URL.revokeObjectURL(this.previewUrl),this.selectedFile=null,this.previewUrl=null,this.resultImageUrl=null,this.errorMessage=null,this.currentJobId=null,this.sseUnsubscribe&&(this.sseUnsubscribe(),this.sseUnsubscribe=null)}applyButtonStyles(){if(!this.shadow.querySelector(".ps-button"))return;const e={backgroundColor:"--ps-btn-bg",textColor:"--ps-btn-color",borderRadius:"--ps-btn-radius",fontSize:"--ps-btn-font-size",fontFamily:"--ps-btn-font",fontWeight:"--ps-btn-font-weight",padding:"--ps-btn-padding",border:"--ps-btn-border",width:"--ps-btn-width",height:"--ps-btn-height",hoverBackgroundColor:"--ps-btn-hover-bg",hoverTextColor:"--ps-btn-hover-color",iconSize:"--ps-btn-icon-size",iconColor:"--ps-btn-icon-color",boxShadow:"--ps-btn-shadow"};for(const[t,n]of Object.entries(e)){const e=this.buttonStyles[t];e&&this.style.setProperty(n,e)}}applyModalStyles(){const e={overlayColor:"--ps-modal-overlay",backgroundColor:"--ps-modal-bg",textColor:"--ps-modal-color",borderRadius:"--ps-modal-radius",width:"--ps-modal-width",maxWidth:"--ps-modal-max-width",fontFamily:"--ps-modal-font",headerBackgroundColor:"--ps-modal-header-bg",headerTextColor:"--ps-modal-header-color",closeButtonColor:"--ps-modal-close-color",uploadBorderColor:"--ps-upload-border",uploadBackgroundColor:"--ps-upload-bg",uploadTextColor:"--ps-upload-color",uploadIconColor:"--ps-upload-icon-color",primaryButtonBackgroundColor:"--ps-modal-primary-bg",primaryButtonTextColor:"--ps-modal-primary-color",primaryButtonBorderRadius:"--ps-modal-primary-radius",loaderColor:"--ps-loader",resultBorderRadius:"--ps-result-radius"};for(const[t,n]of Object.entries(e)){const e=this.modalStyles[t];e&&this.style.setProperty(n,e)}}}"undefined"==typeof window||customElements.get("primestyle-tryon")||customElements.define("primestyle-tryon",d),e.ApiClient=t,e.PrimeStyleError=n,e.PrimeStyleTryon=d,e.SseClient=s,e.compressImage=a,e.detectProductImage=r,e.isValidImageFile=l,Object.defineProperty(e,Symbol.toStringTag,{value:"Module"})});
@@ -0,0 +1,2 @@
1
+ /** Attempts to auto-detect the main product image on the current page. */
2
+ export declare function detectProductImage(): string | null;
@@ -0,0 +1,17 @@
1
+ import type { VtoUpdate } from "./types";
2
+ type VtoUpdateCallback = (update: VtoUpdate) => void;
3
+ export declare class SseClient {
4
+ private eventSource;
5
+ private streamUrl;
6
+ private listeners;
7
+ private reconnectTimer;
8
+ private reconnectAttempts;
9
+ private maxReconnectAttempts;
10
+ constructor(streamUrl: string);
11
+ connect(): void;
12
+ private scheduleReconnect;
13
+ onJob(jobId: string, callback: VtoUpdateCallback): () => void;
14
+ private emit;
15
+ disconnect(): void;
16
+ }
17
+ export {};
@@ -0,0 +1 @@
1
+ export declare function getStyles(): string;
@@ -0,0 +1,99 @@
1
+ /** Configuration options for the PrimeStyle Try-On SDK */
2
+ export interface PrimeStyleConfig {
3
+ /** Your API key (starts with ps_live_) */
4
+ apiKey: string;
5
+ /** API base URL (defaults to https://api.primestyleai.com) */
6
+ apiUrl?: string;
7
+ /** Product image URL to try on */
8
+ productImage?: string;
9
+ /** Button text (defaults to "Virtual Try-On") */
10
+ buttonText?: string;
11
+ /** Locale for i18n (defaults to "en") */
12
+ locale?: string;
13
+ /** Show "Powered by PrimeStyle" badge (defaults to true) */
14
+ showPoweredBy?: boolean;
15
+ }
16
+ /** Button customization options */
17
+ export interface ButtonStyles {
18
+ backgroundColor?: string;
19
+ textColor?: string;
20
+ borderRadius?: string;
21
+ fontSize?: string;
22
+ fontFamily?: string;
23
+ fontWeight?: string;
24
+ padding?: string;
25
+ border?: string;
26
+ width?: string;
27
+ height?: string;
28
+ hoverBackgroundColor?: string;
29
+ hoverTextColor?: string;
30
+ iconSize?: string;
31
+ iconColor?: string;
32
+ boxShadow?: string;
33
+ }
34
+ /** Modal customization options */
35
+ export interface ModalStyles {
36
+ overlayColor?: string;
37
+ backgroundColor?: string;
38
+ textColor?: string;
39
+ borderRadius?: string;
40
+ width?: string;
41
+ maxWidth?: string;
42
+ fontFamily?: string;
43
+ headerBackgroundColor?: string;
44
+ headerTextColor?: string;
45
+ closeButtonColor?: string;
46
+ uploadBorderColor?: string;
47
+ uploadBackgroundColor?: string;
48
+ uploadTextColor?: string;
49
+ uploadIconColor?: string;
50
+ primaryButtonBackgroundColor?: string;
51
+ primaryButtonTextColor?: string;
52
+ primaryButtonBorderRadius?: string;
53
+ loaderColor?: string;
54
+ resultBorderRadius?: string;
55
+ }
56
+ /** Try-on job submission response */
57
+ export interface TryOnResponse {
58
+ jobId: string;
59
+ status: string;
60
+ tokensDeducted: number;
61
+ newBalance: number;
62
+ }
63
+ /** VTO status update from SSE */
64
+ export interface VtoUpdate {
65
+ galleryId: string;
66
+ status: "processing" | "completed" | "failed";
67
+ imageUrl: string | null;
68
+ error: string | null;
69
+ timestamp: number;
70
+ }
71
+ /** Try-on job status (polling) */
72
+ export interface TryOnStatus {
73
+ jobId: string;
74
+ status: "processing" | "completed" | "failed";
75
+ imageUrl: string | null;
76
+ message: string;
77
+ }
78
+ /** Custom events emitted by the component */
79
+ export interface PrimeStyleEvents {
80
+ "ps:open": CustomEvent<void>;
81
+ "ps:close": CustomEvent<void>;
82
+ "ps:upload": CustomEvent<{
83
+ file: File;
84
+ }>;
85
+ "ps:processing": CustomEvent<{
86
+ jobId: string;
87
+ }>;
88
+ "ps:complete": CustomEvent<{
89
+ jobId: string;
90
+ imageUrl: string;
91
+ }>;
92
+ "ps:error": CustomEvent<{
93
+ message: string;
94
+ code?: string;
95
+ }>;
96
+ "ps:product-detected": CustomEvent<{
97
+ imageUrl: string;
98
+ }>;
99
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@primestyleai/tryon",
3
+ "version": "1.0.0",
4
+ "description": "PrimeStyle Virtual Try-On Web Component SDK",
5
+ "type": "module",
6
+ "main": "dist/primestyle-tryon.umd.js",
7
+ "module": "dist/primestyle-tryon.es.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/primestyle-tryon.es.js",
12
+ "require": "./dist/primestyle-tryon.umd.js",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "dev": "vite build --watch",
21
+ "build": "vite build && tsc --emitDeclarationOnly",
22
+ "prepublishOnly": "npm run build"
23
+ },
24
+ "keywords": [
25
+ "virtual-try-on",
26
+ "web-component",
27
+ "fashion",
28
+ "ai",
29
+ "primestyle"
30
+ ],
31
+ "author": "PrimeStyle AI",
32
+ "license": "MIT",
33
+ "devDependencies": {
34
+ "typescript": "^5.5.0",
35
+ "vite": "^5.4.0",
36
+ "terser": "^5.31.0"
37
+ }
38
+ }