@primestyleai/tryon 5.10.100 → 5.10.102

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.
@@ -7,8 +7,11 @@ import type { TranslateFn } from "../../i18n";
7
7
  * Desktop: split — photo upload on the left, Do/Don't/Pro-Tip on the right.
8
8
  * Mobile: stacked — title + preview + checklist + START TRY-ON.
9
9
  */
10
- export declare function PhotoGuideView({ measurementType, onBack, onSubmit, t, }: {
10
+ export declare function PhotoGuideView({ measurementType, apiUrl, apiKey, onBack, onSubmit, t, }: {
11
11
  measurementType?: "body" | "face" | "head" | "foot";
12
+ /** Backend URL for the age-check call (passed through to PhotoUploadZone). */
13
+ apiUrl?: string;
14
+ apiKey?: string;
12
15
  onBack: () => void;
13
16
  onSubmit: (file: File) => void;
14
17
  t: TranslateFn;
@@ -0,0 +1,42 @@
1
+ import type { TranslateFn } from "../../i18n";
2
+ /**
3
+ * Single source of truth for photo upload UX across the SDK. Handles:
4
+ * • drag / drop (with proper enter+over+leave+drop handlers — drop fails
5
+ * silently in Chromium without preventDefault on every event)
6
+ * • click-to-open file picker
7
+ * • optional Gemini age-check (when apiUrl + apiKey are supplied)
8
+ * • size + type validation
9
+ * • internal preview, processing spinner, rejection card
10
+ *
11
+ * Used by PhotoGuideView, SizeResultView's photo-guide overlay, and any
12
+ * other place a customer is asked for a photo. Replaces the duplicated
13
+ * handlePhotoSelect blocks in BodyProfileView / CreateProfileWizard /
14
+ * AccessorySizeView (incrementally — those still have their own state).
15
+ */
16
+ export type PhotoUploadZoneVariant = "block" | "inline";
17
+ export declare function PhotoUploadZone({ apiUrl, apiKey, previewUrl, requireAgeConfirm, ageConfirmed, disabled, hintLine, emptyTitle, variant, onPhotoAccepted, onClearPhoto, t, }: {
18
+ /** When provided with apiKey, runs Gemini age-check before accepting. */
19
+ apiUrl?: string;
20
+ apiKey?: string;
21
+ /** External preview URL (e.g. parent already has a previewUrl from an
22
+ * earlier flow). Pass null to render the empty drop state. */
23
+ previewUrl?: string | null;
24
+ /** When true, the zone refuses uploads until ageConfirmed is true. */
25
+ requireAgeConfirm?: boolean;
26
+ ageConfirmed?: boolean;
27
+ /** Disables click + drop (e.g. while parent is processing). */
28
+ disabled?: boolean;
29
+ /** Optional copy under the empty-state title. */
30
+ hintLine?: string;
31
+ /** Custom empty-state title. Defaults to "Upload your photo". */
32
+ emptyTitle?: string;
33
+ /** "block" (default) — full upload card with title/hint/icon.
34
+ * "inline" — just the drop area sized to its parent (preview-only). */
35
+ variant?: PhotoUploadZoneVariant;
36
+ /** Called with the validated File once it passes age-check + size/type
37
+ * validation. Parent is responsible for compressing or sending it. */
38
+ onPhotoAccepted: (file: File) => void;
39
+ /** Called when the user removes the current photo (via the X on preview). */
40
+ onClearPhoto?: () => void;
41
+ t: TranslateFn;
42
+ }): import("react/jsx-runtime").JSX.Element;
@@ -17929,6 +17929,110 @@ const STYLES$1 = `
17929
17929
 
17930
17930
  /* Upload hover overlay */
17931
17931
  .ps-tryon-upload-hover:hover .ps-tryon-upload-hover-overlay { opacity: 1 !important; }
17932
+
17933
+ /* ─────────── Unified PhotoUploadZone ─────────── */
17934
+ .ps-photo-zone {
17935
+ flex: 1; min-height: 220px;
17936
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
17937
+ border: 2px dashed var(--ps-border-color);
17938
+ border-radius: 0.5vw;
17939
+ background: var(--ps-bg-secondary);
17940
+ cursor: pointer; position: relative; overflow: hidden;
17941
+ transition: border-color 0.18s, background 0.18s, transform 0.18s;
17942
+ }
17943
+ .ps-photo-zone:hover { border-color: var(--ps-accent); background: rgba(33,84,239,0.02); }
17944
+ .ps-photo-zone.ps-photo-zone-drag {
17945
+ border-color: var(--ps-accent); border-style: solid;
17946
+ background: rgba(33,84,239,0.06);
17947
+ transform: scale(1.005);
17948
+ }
17949
+ .ps-photo-zone.ps-photo-zone-has { border: none; cursor: default; padding: 0; }
17950
+ .ps-photo-zone.ps-photo-zone-inline { min-height: 100%; height: 100%; }
17951
+
17952
+ .ps-photo-zone-empty {
17953
+ display: flex; flex-direction: column; align-items: center; gap: 0.4vw;
17954
+ padding: 1vw; text-align: center; pointer-events: none;
17955
+ }
17956
+ .ps-photo-zone-title { font-size: 0.85vw; font-weight: 600; color: var(--ps-text-primary); }
17957
+ .ps-photo-zone-hint { font-size: 0.6vw; color: var(--ps-text-muted); line-height: 1.4; max-width: 24vw; }
17958
+
17959
+ .ps-photo-zone-img {
17960
+ width: 100%; height: 100%; object-fit: contain;
17961
+ display: block; cursor: pointer;
17962
+ }
17963
+ .ps-photo-zone-hover-overlay {
17964
+ position: absolute; inset: 0;
17965
+ display: flex; align-items: center; justify-content: center;
17966
+ background: rgba(0,0,0,0.4); opacity: 0; transition: opacity 0.2s;
17967
+ color: #fff; font-size: 0.8vw; font-weight: 600;
17968
+ border-radius: 0.5vw; cursor: pointer;
17969
+ }
17970
+ .ps-photo-zone:hover .ps-photo-zone-hover-overlay { opacity: 1; }
17971
+ .ps-photo-zone-remove {
17972
+ position: absolute; top: 0.5vw; right: 0.5vw;
17973
+ background: rgba(0,0,0,0.55); color: #fff; border: none; border-radius: 50%;
17974
+ width: 24px; height: 24px; min-width: 24px;
17975
+ display: flex; align-items: center; justify-content: center;
17976
+ cursor: pointer; transition: background 0.15s;
17977
+ z-index: 2;
17978
+ }
17979
+ .ps-photo-zone-remove:hover { background: rgba(239,68,68,0.85); }
17980
+
17981
+ .ps-photo-zone-processing {
17982
+ display: flex; flex-direction: column; align-items: center; gap: 0.5vw;
17983
+ padding: 1vw;
17984
+ }
17985
+ .ps-photo-zone-spinner {
17986
+ width: 28px; height: 28px;
17987
+ border: 3px solid rgba(33,84,239,0.18);
17988
+ border-top-color: var(--ps-accent);
17989
+ border-radius: 50%;
17990
+ animation: ps-spin 0.7s linear infinite;
17991
+ }
17992
+ .ps-photo-zone-status { font-size: 0.7vw; color: var(--ps-text-secondary); }
17993
+
17994
+ .ps-photo-zone-rejection {
17995
+ display: flex; flex-direction: column; align-items: center; gap: 0.5vw;
17996
+ padding: 1vw 1.2vw; max-width: 22vw; text-align: center;
17997
+ cursor: default;
17998
+ }
17999
+ .ps-photo-zone-rejection-icon {
18000
+ width: 36px; height: 36px; border-radius: 50%;
18001
+ background: rgba(239,68,68,0.12); color: #dc2626;
18002
+ display: flex; align-items: center; justify-content: center;
18003
+ font-size: 18px; font-weight: 700;
18004
+ }
18005
+ .ps-photo-zone-rejection-title { font-size: 0.85vw; font-weight: 700; color: var(--ps-text-primary); }
18006
+ .ps-photo-zone-rejection-msg { font-size: 0.65vw; color: var(--ps-text-secondary); line-height: 1.5; }
18007
+ .ps-photo-zone-rejection-cta {
18008
+ margin-top: 0.3vw; padding: 0.55vw 1vw;
18009
+ background: var(--ps-accent); color: #fff; border: none;
18010
+ border-radius: 0.4vw; font-family: inherit;
18011
+ font-size: 0.7vw; font-weight: 600; cursor: pointer;
18012
+ transition: opacity 0.15s;
18013
+ }
18014
+ .ps-photo-zone-rejection-cta:hover { opacity: 0.9; }
18015
+
18016
+ .ps-photo-zone-error {
18017
+ position: absolute; bottom: 0.6vw; left: 0.6vw; right: 0.6vw;
18018
+ background: rgba(239,68,68,0.08); color: #dc2626;
18019
+ border: 1px solid rgba(239,68,68,0.2); border-radius: 0.4vw;
18020
+ padding: 0.4vw 0.6vw; font-size: 0.6vw; line-height: 1.4;
18021
+ text-align: center; pointer-events: none;
18022
+ }
18023
+
18024
+ @media (max-width: 700px) {
18025
+ .ps-photo-zone { min-height: 200px; border-radius: 12px; }
18026
+ .ps-photo-zone-title { font-size: 14px; }
18027
+ .ps-photo-zone-hint { font-size: 11px; max-width: 90%; }
18028
+ .ps-photo-zone-hover-overlay { font-size: 13px; }
18029
+ .ps-photo-zone-status { font-size: 12px; }
18030
+ .ps-photo-zone-rejection { max-width: 90%; gap: 8px; padding: 14px; }
18031
+ .ps-photo-zone-rejection-title { font-size: 14px; }
18032
+ .ps-photo-zone-rejection-msg { font-size: 12px; }
18033
+ .ps-photo-zone-rejection-cta { font-size: 13px; padding: 10px 16px; border-radius: 8px; }
18034
+ .ps-photo-zone-error { font-size: 11px; padding: 8px 10px; border-radius: 8px; }
18035
+ }
17932
18036
  `;
17933
18037
  function CameraIcon$1({ size = 18 }) {
17934
18038
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", children: [
@@ -18675,26 +18779,19 @@ function EngagingTryOnView({
18675
18779
  }, 200);
18676
18780
  return () => clearInterval(id2);
18677
18781
  }, []);
18678
- const aiFacts = [
18679
- t2("Our model is analyzing 150+ body landmarks for the perfect fit"),
18680
- t2("Calibrating fabric drape against your body proportions"),
18681
- t2("Cross-checking fit against millions of garment patterns"),
18682
- t2("Rendering shadows and highlights to match your photo's lighting")
18683
- ];
18782
+ const aiFact = t2("Our model is analyzing 150+ body landmarks for the perfect fit");
18684
18783
  const styleTips = [
18685
- t2("Pair this with neutral tones for a balanced look"),
18686
- t2("Layer with a fitted base for sharper silhouette"),
18687
- t2("Cuff once for a relaxed everyday feel")
18784
+ t2("Match your belt to your shoes never your pants"),
18785
+ t2("Leave the bottom button of a suit jacket undone"),
18786
+ t2("Cuff a pocket square so it peeks 1–2 cm above the pocket"),
18787
+ t2("Roll sleeves twice for a relaxed, intentional finish"),
18788
+ t2("A tie tip should land at the middle of your belt buckle"),
18789
+ t2("Cufflinks should sit half an inch past the jacket sleeve")
18688
18790
  ];
18689
- const [factIdx, setFactIdx] = reactExports.useState(0);
18690
18791
  const [tipIdx, setTipIdx] = reactExports.useState(0);
18691
18792
  reactExports.useEffect(() => {
18692
- const f2 = setInterval(() => setFactIdx((i) => (i + 1) % aiFacts.length), 4e3);
18693
18793
  const s = setInterval(() => setTipIdx((i) => (i + 1) % styleTips.length), 5e3);
18694
- return () => {
18695
- clearInterval(f2);
18696
- clearInterval(s);
18697
- };
18794
+ return () => clearInterval(s);
18698
18795
  }, []);
18699
18796
  const garmentLine = productMaterial?.trim() || (productDescription?.trim() ? productDescription.trim().slice(0, 90) + (productDescription.trim().length > 90 ? "…" : "") : null) || t2("Crafted with quality materials for everyday comfort");
18700
18797
  const Panel = /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "ps-tryon-processing-v2-panel", children: [
@@ -18747,7 +18844,7 @@ function EngagingTryOnView({
18747
18844
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ps-tryon-processing-v2-card-icon ps-fact", children: /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M12 2v6M12 16v6M4.93 4.93l4.24 4.24M14.83 14.83l4.24 4.24M2 12h6M16 12h6M4.93 19.07l4.24-4.24M14.83 9.17l4.24-4.24" }) }) }),
18748
18845
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "ps-tryon-processing-v2-card-text", children: [
18749
18846
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ps-tryon-processing-v2-card-head", children: t2("AI Fact") }),
18750
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ps-tryon-processing-v2-card-body", children: aiFacts[factIdx] }, factIdx)
18847
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ps-tryon-processing-v2-card-body", children: aiFact })
18751
18848
  ] })
18752
18849
  ] })
18753
18850
  ] })
@@ -21928,8 +22025,191 @@ function UploadView({
21928
22025
  }
21929
22026
  ) });
21930
22027
  }
22028
+ function PhotoUploadZone({
22029
+ apiUrl,
22030
+ apiKey,
22031
+ previewUrl,
22032
+ requireAgeConfirm = false,
22033
+ ageConfirmed,
22034
+ disabled = false,
22035
+ hintLine,
22036
+ emptyTitle,
22037
+ variant = "block",
22038
+ onPhotoAccepted,
22039
+ onClearPhoto,
22040
+ t: t2
22041
+ }) {
22042
+ const inputRef = reactExports.useRef(null);
22043
+ const dragDepthRef = reactExports.useRef(0);
22044
+ const [dragOver, setDragOver] = reactExports.useState(false);
22045
+ const [processing, setProcessing] = reactExports.useState(false);
22046
+ const [statusText, setStatusText] = reactExports.useState("");
22047
+ const [rejection, setRejection] = reactExports.useState(null);
22048
+ const [localError, setLocalError] = reactExports.useState(null);
22049
+ reactExports.useEffect(() => {
22050
+ if (!previewUrl) {
22051
+ setRejection(null);
22052
+ setLocalError(null);
22053
+ }
22054
+ }, [previewUrl]);
22055
+ const acceptFile = async (file) => {
22056
+ if (disabled || processing) return;
22057
+ setLocalError(null);
22058
+ setRejection(null);
22059
+ if (requireAgeConfirm && ageConfirmed !== true) {
22060
+ setLocalError(t2("Please confirm that the person in the photo is 18 or older before uploading."));
22061
+ return;
22062
+ }
22063
+ if (!file.type.startsWith("image/")) {
22064
+ setLocalError(t2("Please upload an image file"));
22065
+ return;
22066
+ }
22067
+ if (file.size > 10 * 1024 * 1024) {
22068
+ setLocalError(t2("Image must be under 10MB"));
22069
+ return;
22070
+ }
22071
+ if (apiUrl && apiKey) {
22072
+ setProcessing(true);
22073
+ setStatusText(t2("Analyzing photo…"));
22074
+ try {
22075
+ const ageResult = await checkAgeBeforeUpload(file, apiUrl, apiKey);
22076
+ if (!ageResult.isAdult) {
22077
+ const reason = ageResult.reasoning?.trim() || t2("This photo appears to be of a minor. Please upload a photo of someone 18 or older.");
22078
+ setRejection(reason);
22079
+ setProcessing(false);
22080
+ setStatusText("");
22081
+ return;
22082
+ }
22083
+ try {
22084
+ await compressImage(file, { maxDimension: 1024, quality: 0.85 });
22085
+ } catch {
22086
+ }
22087
+ } catch {
22088
+ } finally {
22089
+ setProcessing(false);
22090
+ setStatusText("");
22091
+ }
22092
+ }
22093
+ onPhotoAccepted(file);
22094
+ };
22095
+ const onClick = () => {
22096
+ if (disabled || processing) return;
22097
+ inputRef.current?.click();
22098
+ };
22099
+ const onDragEnter = (e) => {
22100
+ e.preventDefault();
22101
+ e.stopPropagation();
22102
+ if (disabled || processing) return;
22103
+ dragDepthRef.current += 1;
22104
+ setDragOver(true);
22105
+ };
22106
+ const onDragOver = (e) => {
22107
+ e.preventDefault();
22108
+ e.stopPropagation();
22109
+ if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
22110
+ };
22111
+ const onDragLeave = (e) => {
22112
+ e.preventDefault();
22113
+ e.stopPropagation();
22114
+ dragDepthRef.current = Math.max(0, dragDepthRef.current - 1);
22115
+ if (dragDepthRef.current === 0) setDragOver(false);
22116
+ };
22117
+ const onDrop = (e) => {
22118
+ e.preventDefault();
22119
+ e.stopPropagation();
22120
+ dragDepthRef.current = 0;
22121
+ setDragOver(false);
22122
+ if (disabled || processing) return;
22123
+ const file = e.dataTransfer?.files?.[0];
22124
+ if (file) void acceptFile(file);
22125
+ };
22126
+ const onChange = (e) => {
22127
+ const file = e.target.files?.[0];
22128
+ if (file) void acceptFile(file);
22129
+ if (inputRef.current) inputRef.current.value = "";
22130
+ };
22131
+ const removePhoto = () => {
22132
+ setRejection(null);
22133
+ setLocalError(null);
22134
+ onClearPhoto?.();
22135
+ };
22136
+ const isInline = variant === "inline";
22137
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
22138
+ "div",
22139
+ {
22140
+ className: `ps-photo-zone${dragOver ? " ps-photo-zone-drag" : ""}${isInline ? " ps-photo-zone-inline" : ""}${previewUrl ? " ps-photo-zone-has" : ""}`,
22141
+ onClick: previewUrl ? void 0 : onClick,
22142
+ onDragEnter,
22143
+ onDragOver,
22144
+ onDragLeave,
22145
+ onDrop,
22146
+ children: [
22147
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
22148
+ "input",
22149
+ {
22150
+ ref: inputRef,
22151
+ type: "file",
22152
+ accept: "image/jpeg,image/png,image/webp",
22153
+ style: { display: "none" },
22154
+ onChange
22155
+ }
22156
+ ),
22157
+ previewUrl && !rejection && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
22158
+ /* @__PURE__ */ jsxRuntimeExports.jsx("img", { src: previewUrl, alt: t2("Your photo"), className: "ps-photo-zone-img" }),
22159
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ps-photo-zone-hover-overlay", onClick, title: t2("Click to change photo"), children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t2("Click to change photo") }) }),
22160
+ onClearPhoto && /* @__PURE__ */ jsxRuntimeExports.jsx(
22161
+ "button",
22162
+ {
22163
+ type: "button",
22164
+ className: "ps-photo-zone-remove",
22165
+ onClick: (e) => {
22166
+ e.stopPropagation();
22167
+ removePhoto();
22168
+ },
22169
+ "aria-label": t2("Remove photo"),
22170
+ children: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", width: "14", height: "14", children: [
22171
+ /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
22172
+ /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
22173
+ ] })
22174
+ }
22175
+ )
22176
+ ] }),
22177
+ !previewUrl && !rejection && !processing && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "ps-photo-zone-empty", children: [
22178
+ /* @__PURE__ */ jsxRuntimeExports.jsx(UploadIcon, { size: 32 }),
22179
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "ps-photo-zone-title", children: emptyTitle || t2("Upload your photo") }),
22180
+ hintLine && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "ps-photo-zone-hint", children: hintLine }),
22181
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "ps-photo-zone-hint", children: t2("JPEG, PNG up to 10MB · click or drop") })
22182
+ ] }),
22183
+ processing && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "ps-photo-zone-processing", children: [
22184
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ps-photo-zone-spinner" }),
22185
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "ps-photo-zone-status", children: statusText })
22186
+ ] }),
22187
+ rejection && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "ps-photo-zone-rejection", onClick: (e) => e.stopPropagation(), children: [
22188
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ps-photo-zone-rejection-icon", "aria-hidden": "true", children: "!" }),
22189
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ps-photo-zone-rejection-title", children: t2("Different photo needed") }),
22190
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ps-photo-zone-rejection-msg", children: rejection }),
22191
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
22192
+ "button",
22193
+ {
22194
+ type: "button",
22195
+ className: "ps-photo-zone-rejection-cta",
22196
+ onClick: () => {
22197
+ setRejection(null);
22198
+ inputRef.current?.click();
22199
+ },
22200
+ children: t2("Choose another photo")
22201
+ }
22202
+ )
22203
+ ] }),
22204
+ localError && !rejection && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ps-photo-zone-error", onClick: (e) => e.stopPropagation(), children: localError })
22205
+ ]
22206
+ }
22207
+ );
22208
+ }
21931
22209
  function PhotoGuideView({
21932
22210
  measurementType = "body",
22211
+ apiUrl,
22212
+ apiKey,
21933
22213
  onBack,
21934
22214
  onSubmit,
21935
22215
  t: t2
@@ -21937,8 +22217,6 @@ function PhotoGuideView({
21937
22217
  const isMobile = useIsMobile();
21938
22218
  const [guideFile, setGuideFile] = reactExports.useState(null);
21939
22219
  const [guidePreviewUrl, setGuidePreviewUrl] = reactExports.useState(null);
21940
- const [, setGuideDragOver] = reactExports.useState(false);
21941
- const guideInputRef = reactExports.useRef(null);
21942
22220
  reactExports.useEffect(() => {
21943
22221
  if (!guideFile) {
21944
22222
  setGuidePreviewUrl(null);
@@ -21958,49 +22236,17 @@ function PhotoGuideView({
21958
22236
  /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "ps-pm-title", children: t2("Review your photo") }),
21959
22237
  /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "ps-pm-subtitle", children: measurementType === "face" ? t2("A clear, front-facing face photo — no glasses on — gives us the most accurate try-on.") : measurementType === "head" ? t2("Face the camera with your head and shoulders in frame, leaving space above your head.") : t2("Ensure your full body is visible for the most accurate virtual try-on.") })
21960
22238
  ] }),
21961
- /* @__PURE__ */ jsxRuntimeExports.jsx(
21962
- "input",
21963
- {
21964
- ref: guideInputRef,
21965
- type: "file",
21966
- accept: "image/jpeg,image/png,image/webp",
21967
- style: { display: "none" },
21968
- onChange: (e) => {
21969
- const f2 = e.target.files?.[0];
21970
- if (f2) setGuideFile(f2);
21971
- }
21972
- }
21973
- ),
21974
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ps-pm-preview", children: guideFile && guidePreviewUrl ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
21975
- /* @__PURE__ */ jsxRuntimeExports.jsx("img", { src: guidePreviewUrl, alt: t2("Your photo"), className: "ps-pm-preview-img" }),
21976
- /* @__PURE__ */ jsxRuntimeExports.jsx(
21977
- "button",
21978
- {
21979
- type: "button",
21980
- className: "ps-pm-preview-remove",
21981
- onClick: () => setGuideFile(null),
21982
- "aria-label": t2("Remove photo"),
21983
- children: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", width: "14", height: "14", children: [
21984
- /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
21985
- /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
21986
- ] })
21987
- }
21988
- )
21989
- ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(
21990
- "button",
22239
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ps-pm-preview", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
22240
+ PhotoUploadZone,
21991
22241
  {
21992
- type: "button",
21993
- className: "ps-pm-preview-empty",
21994
- onClick: () => guideInputRef.current?.click(),
21995
- children: [
21996
- /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", width: "32", height: "32", children: [
21997
- /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
21998
- /* @__PURE__ */ jsxRuntimeExports.jsx("polyline", { points: "17 8 12 3 7 8" }),
21999
- /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "12", y1: "3", x2: "12", y2: "15" })
22000
- ] }),
22001
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "ps-pm-preview-empty-title", children: t2("Tap to upload") }),
22002
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "ps-pm-preview-empty-hint", children: t2("JPEG, PNG up to 10MB") })
22003
- ]
22242
+ apiUrl,
22243
+ apiKey,
22244
+ previewUrl: guidePreviewUrl,
22245
+ variant: "inline",
22246
+ onPhotoAccepted: (f2) => setGuideFile(f2),
22247
+ onClearPhoto: () => setGuideFile(null),
22248
+ emptyTitle: t2("Tap to upload"),
22249
+ t: t2
22004
22250
  }
22005
22251
  ) }),
22006
22252
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "ps-pm-checklist", children: [
@@ -22039,7 +22285,7 @@ function PhotoGuideView({
22039
22285
  {
22040
22286
  type: "button",
22041
22287
  className: "ps-pm-secondary-btn",
22042
- onClick: () => guideInputRef.current?.click(),
22288
+ onClick: () => setGuideFile(null),
22043
22289
  children: t2("RETAKE PHOTO")
22044
22290
  }
22045
22291
  ) : /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -22056,60 +22302,20 @@ function PhotoGuideView({
22056
22302
  }
22057
22303
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { display: "flex", flexDirection: "column", padding: "1.5vw", width: "100%", height: "100%", background: "var(--ps-bg-primary)", borderRadius: "0.8vw", overflow: "hidden" }, children: [
22058
22304
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { display: "flex", gap: "1.2vw", flex: 1, alignItems: "stretch", minHeight: 0, overflow: "hidden" }, children: [
22059
- /* @__PURE__ */ jsxRuntimeExports.jsxs(
22060
- "div",
22305
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { flex: 1, display: "flex", minHeight: 0 }, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
22306
+ PhotoUploadZone,
22061
22307
  {
22062
- onClick: () => guideInputRef.current?.click(),
22063
- onDragOver: (e) => {
22064
- e.preventDefault();
22065
- setGuideDragOver(true);
22066
- },
22067
- onDragLeave: () => setGuideDragOver(false),
22068
- onDrop: (e) => {
22069
- e.preventDefault();
22070
- setGuideDragOver(false);
22071
- const f2 = e.dataTransfer.files[0];
22072
- if (f2) setGuideFile(f2);
22073
- },
22074
- style: {
22075
- flex: 1,
22076
- display: "flex",
22077
- flexDirection: "column",
22078
- alignItems: "center",
22079
- justifyContent: "center",
22080
- border: guideFile ? "none" : "2px dashed var(--ps-border-color)",
22081
- borderRadius: "0.5vw",
22082
- cursor: "pointer",
22083
- position: "relative",
22084
- background: "var(--ps-bg-secondary)",
22085
- overflow: "hidden"
22086
- },
22087
- className: "ps-tryon-upload-hover",
22088
- children: [
22089
- guideFile && guidePreviewUrl ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
22090
- /* @__PURE__ */ jsxRuntimeExports.jsx("img", { src: guidePreviewUrl, alt: "preview", style: { width: "100%", height: "100%", objectFit: "contain" } }),
22091
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ps-tryon-upload-hover-overlay", style: { position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", background: "rgba(0,0,0,0.4)", opacity: 0, transition: "opacity 0.2s", borderRadius: "0.5vw" }, children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: "#fff", fontSize: "0.8vw", fontWeight: 600 }, children: t2("Click to change photo") }) })
22092
- ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
22093
- /* @__PURE__ */ jsxRuntimeExports.jsx(UploadIcon, { size: 32 }),
22094
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { fontSize: "0.85vw", fontWeight: 600, color: "var(--ps-text-primary)", marginTop: "0.5vw" }, children: t2("Upload your photo") }),
22095
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { fontSize: "0.6vw", color: "var(--ps-text-muted)", marginTop: "0.2vw" }, children: measurementType === "face" ? t2("Click or drag a close-up face photo") : measurementType === "head" ? t2("Click or drag a head-and-shoulders photo") : t2("Click or drag a full-body photo") })
22096
- ] }),
22097
- /* @__PURE__ */ jsxRuntimeExports.jsx(
22098
- "input",
22099
- {
22100
- ref: guideInputRef,
22101
- type: "file",
22102
- accept: "image/jpeg,image/png,image/webp",
22103
- style: { display: "none" },
22104
- onChange: (e) => {
22105
- const f2 = e.target.files?.[0];
22106
- if (f2) setGuideFile(f2);
22107
- }
22108
- }
22109
- )
22110
- ]
22308
+ apiUrl,
22309
+ apiKey,
22310
+ previewUrl: guidePreviewUrl,
22311
+ variant: "inline",
22312
+ emptyTitle: t2("Upload your photo"),
22313
+ hintLine: measurementType === "face" ? t2("Click or drag a close-up face photo") : measurementType === "head" ? t2("Click or drag a head-and-shoulders photo") : t2("Click or drag a full-body photo"),
22314
+ onPhotoAccepted: (f2) => setGuideFile(f2),
22315
+ onClearPhoto: () => setGuideFile(null),
22316
+ t: t2
22111
22317
  }
22112
- ),
22318
+ ) }),
22113
22319
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { flex: 1, display: "flex", flexDirection: "column", justifyContent: "center", gap: "0.6vw" }, children: [
22114
22320
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { fontSize: "0.85vw", fontWeight: 700, color: "var(--ps-text-primary)", marginBottom: "0.3vw" }, children: t2("How to take the best photo") }),
22115
22321
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { background: "#ddfbe7", borderRadius: "0.5vw", padding: "0.6vw 0.8vw" }, children: [
@@ -23183,6 +23389,30 @@ function CreateProfileWizard({ onSave, onCancel, apiUrl, apiKey, onPhotoPreview,
23183
23389
  if (!photoBase64 && ageConfirmed === true) setUploadHoverCpw(true);
23184
23390
  },
23185
23391
  onMouseLeave: () => setUploadHoverCpw(false),
23392
+ onDragEnter: (e) => {
23393
+ e.preventDefault();
23394
+ e.stopPropagation();
23395
+ },
23396
+ onDragOver: (e) => {
23397
+ e.preventDefault();
23398
+ e.stopPropagation();
23399
+ if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
23400
+ },
23401
+ onDragLeave: (e) => {
23402
+ e.preventDefault();
23403
+ e.stopPropagation();
23404
+ },
23405
+ onDrop: (e) => {
23406
+ e.preventDefault();
23407
+ e.stopPropagation();
23408
+ if (photoBase64 || ageConfirmed !== true) return;
23409
+ const file = e.dataTransfer?.files?.[0];
23410
+ if (!file || !photoInputRef.current) return;
23411
+ const dt = new DataTransfer();
23412
+ dt.items.add(file);
23413
+ photoInputRef.current.files = dt.files;
23414
+ photoInputRef.current.dispatchEvent(new Event("change", { bubbles: true }));
23415
+ },
23186
23416
  style: {
23187
23417
  flex: 1,
23188
23418
  display: "flex",
@@ -25666,6 +25896,30 @@ function BodyProfileView({
25666
25896
  if (!photoPreview && ageConfirmed === true) setUploadHover(true);
25667
25897
  },
25668
25898
  onMouseLeave: () => setUploadHover(false),
25899
+ onDragEnter: (e) => {
25900
+ e.preventDefault();
25901
+ e.stopPropagation();
25902
+ },
25903
+ onDragOver: (e) => {
25904
+ e.preventDefault();
25905
+ e.stopPropagation();
25906
+ if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
25907
+ },
25908
+ onDragLeave: (e) => {
25909
+ e.preventDefault();
25910
+ e.stopPropagation();
25911
+ },
25912
+ onDrop: (e) => {
25913
+ e.preventDefault();
25914
+ e.stopPropagation();
25915
+ if (photoPreview || ageConfirmed !== true) return;
25916
+ const file = e.dataTransfer?.files?.[0];
25917
+ if (!file || !fileInputRef.current) return;
25918
+ const dt = new DataTransfer();
25919
+ dt.items.add(file);
25920
+ fileInputRef.current.files = dt.files;
25921
+ fileInputRef.current.dispatchEvent(new Event("change", { bubbles: true }));
25922
+ },
25669
25923
  style: {
25670
25924
  flex: 1,
25671
25925
  display: "flex",
@@ -25959,7 +26213,7 @@ function BodyProfileView({
25959
26213
  ] }),
25960
26214
  /* @__PURE__ */ jsxRuntimeExports.jsx("p", { style: { margin: "0.2vw 0 0", fontSize: "0.65vw", color: "var(--ps-text-muted)" }, children: t2("These calibrate the AI — all required.") })
25961
26215
  ] }),
25962
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "ps-bp-system-toggle", style: { alignSelf: "center" }, children: [
26216
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "ps-bp-system-toggle", style: { alignSelf: "center", marginTop: "1.2vw" }, children: [
25963
26217
  /* @__PURE__ */ jsxRuntimeExports.jsx("button", { className: `ps-bp-system-btn${!isImperialMode ? " ps-bp-system-active" : ""}`, onClick: photoSwitchToMetric, type: "button", children: t2("Metric") }),
25964
26218
  /* @__PURE__ */ jsxRuntimeExports.jsx("button", { className: `ps-bp-system-btn${isImperialMode ? " ps-bp-system-active" : ""}`, onClick: photoSwitchToImperial, type: "button", children: t2("Imperial") })
25965
26219
  ] }),
@@ -29480,6 +29734,8 @@ function PrimeStyleTryonInner({
29480
29734
  PhotoGuideView,
29481
29735
  {
29482
29736
  measurementType: detectMeasurementType(productTitle),
29737
+ apiUrl: getApiUrl(apiUrl),
29738
+ apiKey: getApiKey(),
29483
29739
  onBack: () => setView("no-chart"),
29484
29740
  onSubmit: (file) => {
29485
29741
  handleFileSelect(file);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primestyleai/tryon",
3
- "version": "5.10.100",
3
+ "version": "5.10.102",
4
4
  "description": "PrimeStyle Virtual Try-On SDK — React component & Web Component",
5
5
  "type": "module",
6
6
  "main": "dist/primestyle-tryon.js",