@firetable/project-xiaochun 0.1.14 → 0.1.16

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/dist/react.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! @firetable/project-xiaochun v0.1.14 | MIT | https://github.com/FireTable/project-xiaochun */
1
+ /*! @firetable/project-xiaochun v0.1.16 | MIT | https://github.com/FireTable/project-xiaochun */
2
2
  "use strict";
3
3
  "use client";
4
4
  var __defProp = Object.defineProperty;
@@ -32,6 +32,70 @@ var import_react = require("react");
32
32
  var XC_PROTOCOL_VERSION = 1;
33
33
  var XC_DEFAULT_ORIGIN = "https://xiaochun.firetable.tech";
34
34
  var XC_DEFAULT_SRC = `${XC_DEFAULT_ORIGIN}/embed`;
35
+ var XC_WINDOW_CORNER_RADIUS = 20;
36
+ var XC_UI_PARTS = ["chat", "bubble", "outfit", "scene", "lang", "github"];
37
+ var XC_UI_PART_SET = new Set(XC_UI_PARTS);
38
+ var XC_UI_LEGACY_PARTS = ["chat", "bubble"];
39
+ function parseXcUiList(items) {
40
+ const seen = /* @__PURE__ */ new Set();
41
+ const unknown = [];
42
+ for (const it of items) {
43
+ const name = typeof it === "string" ? it.trim().toLowerCase() : "";
44
+ if (XC_UI_PART_SET.has(name)) seen.add(name);
45
+ else unknown.push(typeof it === "string" ? it.slice(0, 40) : String(typeof it));
46
+ }
47
+ return { parts: XC_UI_PARTS.filter((p) => seen.has(p)), unknown, legacy: false };
48
+ }
49
+ function normalizeXcUiOption(v) {
50
+ if (v === true) return { parts: [...XC_UI_LEGACY_PARTS], unknown: [], legacy: true };
51
+ if (Array.isArray(v)) return parseXcUiList(v);
52
+ return { parts: [], unknown: [], legacy: false };
53
+ }
54
+ function parseXcUiAutoHide(raw) {
55
+ if (raw === true || raw === false || raw === "transparent") return raw;
56
+ if (typeof raw !== "string") return void 0;
57
+ const v = raw.trim().toLowerCase();
58
+ if (v === "1" || v === "true") return true;
59
+ if (v === "0" || v === "false") return false;
60
+ if (v === "transparent") return "transparent";
61
+ return void 0;
62
+ }
63
+ var XC_CAMERA_RANGES = {
64
+ fov: [15, 60],
65
+ distance: [1, 15],
66
+ height: [-1, 1]
67
+ };
68
+ var XC_CAMERA_NUM_KEYS = ["fov", "distance", "height"];
69
+ var XC_CAMERA_KEYS = ["fov", "distance", "height", "intro"];
70
+ function normalizeXcCamera(raw) {
71
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return { ok: false, error: "camera must be an object" };
72
+ const src = raw;
73
+ for (const k of Object.keys(src)) {
74
+ if (!XC_CAMERA_KEYS.includes(k)) return { ok: false, error: `unknown camera key: ${k} (expected ${XC_CAMERA_KEYS.join(" | ")})` };
75
+ }
76
+ const out = {};
77
+ const clamped = [];
78
+ for (const k of XC_CAMERA_NUM_KEYS) {
79
+ const v = src[k];
80
+ if (v === void 0) continue;
81
+ if (v === null) {
82
+ out[k] = null;
83
+ continue;
84
+ }
85
+ if (typeof v !== "number" || !Number.isFinite(v)) return { ok: false, error: `camera.${k} must be a finite number or null` };
86
+ const [lo, hi] = XC_CAMERA_RANGES[k];
87
+ const c = Math.min(hi, Math.max(lo, v));
88
+ if (c !== v) clamped.push(k);
89
+ out[k] = c;
90
+ }
91
+ if (src.intro !== void 0) {
92
+ if (src.intro !== null && typeof src.intro !== "boolean") return { ok: false, error: "camera.intro must be a boolean or null" };
93
+ out.intro = src.intro;
94
+ }
95
+ return { ok: true, camera: out, clamped };
96
+ }
97
+ var XC_ID_RE = /^[a-z][a-z0-9_]{0,63}$/;
98
+ var isXcId = (v) => typeof v === "string" && XC_ID_RE.test(v);
35
99
  function xcMessage(type, payload, id) {
36
100
  const m = { type, v: XC_PROTOCOL_VERSION, payload };
37
101
  if (id !== void 0) m.id = id;
@@ -53,11 +117,113 @@ function normalizeOrigin(input) {
53
117
  }
54
118
  }
55
119
 
120
+ // src/gesture-box.ts
121
+ var DEFAULT_RESIZE_LIMITS = Object.freeze({
122
+ minWidth: 120,
123
+ minHeight: 180,
124
+ maxWidth: Infinity,
125
+ maxHeight: Infinity
126
+ });
127
+ var finitePositive = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
128
+ function normalizeResizable(v) {
129
+ if (!v) return null;
130
+ const o = v === true ? {} : v;
131
+ const minWidth = finitePositive(o.minWidth) ? o.minWidth : DEFAULT_RESIZE_LIMITS.minWidth;
132
+ const minHeight = finitePositive(o.minHeight) ? o.minHeight : DEFAULT_RESIZE_LIMITS.minHeight;
133
+ const maxWidth = finitePositive(o.maxWidth) ? Math.max(o.maxWidth, minWidth) : Infinity;
134
+ const maxHeight = finitePositive(o.maxHeight) ? Math.max(o.maxHeight, minHeight) : Infinity;
135
+ return { minWidth, minHeight, maxWidth, maxHeight };
136
+ }
137
+ var clamp = (v, lo, hi) => Math.min(Math.max(v, lo), Math.max(lo, hi));
138
+ function moveBox(start, totalDx, totalDy, vp) {
139
+ return {
140
+ left: clamp(start.left + totalDx, 0, vp.width - start.width),
141
+ top: clamp(start.top + totalDy, 0, vp.height - start.height),
142
+ width: start.width,
143
+ height: start.height
144
+ };
145
+ }
146
+ function resizeBox(start, corner, totalDx, totalDy, limits, vp) {
147
+ const west = corner === "NW" || corner === "SW";
148
+ const north = corner === "NW" || corner === "NE";
149
+ const right = start.left + start.width, bottom = start.top + start.height;
150
+ const wantW = west ? start.width - totalDx : start.width + totalDx;
151
+ const wantH = north ? start.height - totalDy : start.height + totalDy;
152
+ const roomW = west ? right : vp.width - start.left;
153
+ const roomH = north ? bottom : vp.height - start.top;
154
+ const width = clamp(wantW, limits.minWidth, Math.min(limits.maxWidth, roomW));
155
+ const height = clamp(wantH, limits.minHeight, Math.min(limits.maxHeight, roomH));
156
+ return { left: west ? right - width : start.left, top: north ? bottom - height : start.top, width, height };
157
+ }
158
+ function fitBox(b, vp) {
159
+ const width = Math.min(b.width, Math.max(1, vp.width)), height = Math.min(b.height, Math.max(1, vp.height));
160
+ return { left: clamp(b.left, 0, vp.width - width), top: clamp(b.top, 0, vp.height - height), width, height };
161
+ }
162
+ var CORNERS = /* @__PURE__ */ new Set(["NW", "NE", "SW", "SE"]);
163
+ var PHASES = /* @__PURE__ */ new Set(["start", "move", "end"]);
164
+ var REASONS = /* @__PURE__ */ new Set(["up", "cancel", "blur"]);
165
+ var MAX_GESTURE_DELTA = 2e4;
166
+ var delta = (v) => typeof v === "number" && Number.isFinite(v) && Math.abs(v) <= MAX_GESTURE_DELTA;
167
+ var counter = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
168
+ function parseGesturePayload(kind, raw) {
169
+ if (raw === null || typeof raw !== "object") return null;
170
+ const p = raw;
171
+ if (!counter(p.gesture) || !counter(p.seq) || typeof p.phase !== "string" || !PHASES.has(p.phase)) return null;
172
+ if (!delta(p.dx) || !delta(p.dy) || !delta(p.totalDx) || !delta(p.totalDy)) return null;
173
+ if (p.reason !== void 0 && (typeof p.reason !== "string" || !REASONS.has(p.reason))) return null;
174
+ const out = { gesture: p.gesture, seq: p.seq, phase: p.phase, totalDx: p.totalDx, totalDy: p.totalDy };
175
+ if (kind === "resize") {
176
+ if (typeof p.corner !== "string" || !CORNERS.has(p.corner)) return null;
177
+ out.corner = p.corner;
178
+ }
179
+ return out;
180
+ }
181
+ var GestureGate = class {
182
+ constructor() {
183
+ this.lastGesture = 0;
184
+ this.lastSeq = -1;
185
+ this.open = false;
186
+ }
187
+ get active() {
188
+ return this.open;
189
+ }
190
+ accept(p) {
191
+ if (p.phase === "start") {
192
+ if (p.gesture <= this.lastGesture || p.seq !== 0) return false;
193
+ this.lastGesture = p.gesture;
194
+ this.lastSeq = 0;
195
+ this.open = true;
196
+ return true;
197
+ }
198
+ if (!this.open || p.gesture !== this.lastGesture || p.seq <= this.lastSeq) return false;
199
+ this.lastSeq = p.seq;
200
+ if (p.phase === "end") this.open = false;
201
+ return true;
202
+ }
203
+ /** 宿主主动结束 (关闭手势 / 销毁): 之后同一手势的迟到消息被拒绝。 */
204
+ close() {
205
+ this.open = false;
206
+ }
207
+ };
208
+
56
209
  // src/client.ts
57
210
  var FALLBACK_PLACEHOLDER = "data:image/svg+xml;utf8," + encodeURIComponent(
58
211
  '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 180"><defs><linearGradient id="g" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f6b8ae"/><stop offset="1" stop-color="#ea8377"/></linearGradient></defs><ellipse cx="60" cy="170" rx="34" ry="6" fill="#000" opacity=".12"/><circle cx="60" cy="48" r="26" fill="url(#g)" opacity=".75"/><path d="M26 160c2-40 18-62 34-62s32 22 34 62z" fill="url(#g)" opacity=".6"/></svg>'
59
212
  );
213
+ var noSelect = (st) => {
214
+ st.setProperty("user-select", "none");
215
+ st.setProperty("-webkit-user-select", "none");
216
+ st.setProperty("-webkit-touch-callout", "none");
217
+ st.setProperty("-webkit-tap-highlight-color", "transparent");
218
+ };
60
219
  var css = (v, d) => v === void 0 ? d : typeof v === "number" ? `${v}px` : v;
220
+ var XC_DEFAULT_WIDTH = 600;
221
+ var XC_DEFAULT_HEIGHT = 1080;
222
+ var defaultWidthCss = (floating = false) => floating ? `min(${XC_DEFAULT_WIDTH}px, calc(100vw - 2 * var(--xc-offset-x, 16px)))` : `min(${XC_DEFAULT_WIDTH}px, 100vw)`;
223
+ var defaultHeightCss = (floating = false) => {
224
+ const unit = typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("height", "1svh") ? "svh" : "vh";
225
+ return floating ? `min(${XC_DEFAULT_HEIGHT}px, calc(100${unit} - 2 * var(--xc-offset-y, 16px)))` : `min(${XC_DEFAULT_HEIGHT}px, 100${unit})`;
226
+ };
61
227
  function createXiaochun(options) {
62
228
  if (typeof window === "undefined" || typeof document === "undefined") {
63
229
  throw new Error("[project-xiaochun] createXiaochun must run in a browser");
@@ -79,8 +245,56 @@ function createXiaochun(options) {
79
245
  if (!n) throw new Error(`[project-xiaochun] invalid allowedOrigins entry: ${o} (wildcards are not allowed)`);
80
246
  trusted.add(n);
81
247
  }
82
- const transparent = options.transparent ?? false;
83
- const passthrough = options.passthrough ?? transparent;
248
+ const prefsKey = options.persist === "host" ? "xiaochun:prefs" : typeof options.persist === "string" && options.persist ? options.persist : null;
249
+ const readPrefs = () => {
250
+ if (!prefsKey) return {};
251
+ try {
252
+ const raw = JSON.parse(window.localStorage.getItem(prefsKey) ?? "null");
253
+ const out = {};
254
+ if (raw && typeof raw === "object") {
255
+ if (isXcId(raw.outfit)) out.outfit = raw.outfit;
256
+ if (isXcId(raw.scene)) out.scene = raw.scene;
257
+ }
258
+ return out;
259
+ } catch {
260
+ return {};
261
+ }
262
+ };
263
+ const writePrefs = (patch) => {
264
+ if (!prefsKey) return;
265
+ try {
266
+ const next = { ...readPrefs() };
267
+ for (const [k, v] of Object.entries(patch)) {
268
+ if (v) next[k] = v;
269
+ else delete next[k];
270
+ }
271
+ window.localStorage.setItem(prefsKey, JSON.stringify(next));
272
+ } catch {
273
+ }
274
+ };
275
+ const prefs = readPrefs();
276
+ if (options.model !== void 0 && options.outfit === void 0) {
277
+ console.warn('[project-xiaochun] option "model" is deprecated, use "outfit" (built-in outfit id).');
278
+ }
279
+ const uiParse = normalizeXcUiOption(options.ui);
280
+ const uiParts = uiParse.parts;
281
+ const uiAutoHide = parseXcUiAutoHide(options.uiAutoHide);
282
+ if (options.uiAutoHide !== void 0 && uiAutoHide === void 0) console.warn(`[project-xiaochun] ignoring invalid uiAutoHide: ${String(options.uiAutoHide)} (use true | false | 'transparent')`);
283
+ let camera = {};
284
+ if (options.camera !== void 0) {
285
+ const r = normalizeXcCamera(options.camera);
286
+ if (r.ok) {
287
+ camera = r.camera;
288
+ if (r.clamped.length) console.warn(`[project-xiaochun] camera.${r.clamped.join(", camera.")} out of range, clamped (fov ${XC_CAMERA_RANGES.fov.join("\u2013")}, distance ${XC_CAMERA_RANGES.distance.join("\u2013")}, height ${XC_CAMERA_RANGES.height.join("\u2013")})`);
289
+ } else console.warn(`[project-xiaochun] ignoring invalid camera option: ${r.error}`);
290
+ }
291
+ if (uiParse.legacy) console.warn(`[project-xiaochun] option "ui: true" is deprecated, use an array of parts, e.g. ui: ['chat', 'bubble', 'outfit', 'scene'].`);
292
+ if (uiParse.unknown.length) console.warn(`[project-xiaochun] ignoring unknown ui part(s): ${uiParse.unknown.join(", ")} (known: ${XC_UI_PARTS.join(", ")})`);
293
+ const outfitOpt = options.outfit ?? (isXcId(options.model) ? options.model : void 0);
294
+ const initialOutfit = outfitOpt ?? prefs.outfit;
295
+ const initialScene = options.scene ?? prefs.scene ?? (options.transparent ? "transparent" : void 0);
296
+ let transparent = initialScene === "transparent" || initialScene !== "light" && initialScene !== "dark" && (options.transparent ?? false);
297
+ let passthrough = options.passthrough ?? transparent;
84
298
  const lazy = options.lazy ?? true;
85
299
  const position = options.position ?? "inline";
86
300
  const handshakeTimeout = options.handshakeTimeout ?? 2e4;
@@ -88,14 +302,26 @@ function createXiaochun(options) {
88
302
  wrapper.setAttribute("data-xiaochun", "");
89
303
  const ws = wrapper.style;
90
304
  ws.position = position === "inline" ? "relative" : "fixed";
91
- ws.width = css(options.width, "320px");
92
- ws.height = css(options.height, "480px");
305
+ ws.width = css(options.width, defaultWidthCss(position !== "inline"));
306
+ ws.height = css(options.height, defaultHeightCss(position !== "inline"));
93
307
  ws.maxWidth = "100%";
94
308
  ws.overflow = "visible";
95
309
  ws.contain = "layout style";
96
- ws.borderRadius = "var(--xc-radius, 0)";
310
+ noSelect(ws);
97
311
  ws.boxShadow = "var(--xc-shadow, none)";
98
- if (!transparent) ws.background = "var(--xc-bg, transparent)";
312
+ const applyWrapperBg = () => {
313
+ ws.background = transparent ? "" : "var(--xc-bg, transparent)";
314
+ };
315
+ applyWrapperBg();
316
+ let radiusOpt = options.borderRadius;
317
+ const radiusValue = () => radiusOpt !== void 0 ? css(radiusOpt, "0px") : transparent ? "0px" : `${XC_WINDOW_CORNER_RADIUS}px`;
318
+ const applyRadius = () => {
319
+ const v = `var(--xc-radius, ${radiusValue()})`;
320
+ ws.borderRadius = v;
321
+ if (placeholderEl) placeholderEl.style.borderRadius = v;
322
+ if (iframe) iframe.style.borderRadius = v;
323
+ };
324
+ ws.borderRadius = `var(--xc-radius, ${radiusValue()})`;
99
325
  if (position === "bottom-right") {
100
326
  ws.right = "var(--xc-offset-x, 16px)";
101
327
  ws.bottom = "var(--xc-offset-y, 16px)";
@@ -116,13 +342,13 @@ function createXiaochun(options) {
116
342
  img.src = options.placeholder || FALLBACK_PLACEHOLDER;
117
343
  img.alt = "Project XiaoChun";
118
344
  img.decoding = "async";
119
- img.width = 320;
120
- img.height = 480;
345
+ img.width = XC_DEFAULT_WIDTH;
346
+ img.height = XC_DEFAULT_HEIGHT;
121
347
  placeholderEl = img;
122
348
  }
123
349
  const ps = placeholderEl.style;
124
350
  placeholderEl.setAttribute("part", "placeholder");
125
- ps.borderRadius = "var(--xc-radius, 0)";
351
+ ps.borderRadius = `var(--xc-radius, ${radiusValue()})`;
126
352
  ps.position = "absolute";
127
353
  ps.inset = "0";
128
354
  ps.width = "100%";
@@ -131,6 +357,7 @@ function createXiaochun(options) {
131
357
  ps.transition = "opacity .25s ease";
132
358
  ps.pointerEvents = "auto";
133
359
  ps.cursor = lazy === false ? "default" : "pointer";
360
+ noSelect(ps);
134
361
  placeholderEl.addEventListener("click", () => activate());
135
362
  wrapper.appendChild(placeholderEl);
136
363
  }
@@ -159,25 +386,45 @@ function createXiaochun(options) {
159
386
  let autoPaused = false;
160
387
  let loaded = false;
161
388
  let lastHit = false;
389
+ let caps = null;
390
+ let currentOutfit = null;
391
+ let currentScene = null;
162
392
  const queue = [];
163
393
  const pending = /* @__PURE__ */ new Map();
164
394
  let idSeq = 0;
165
395
  let handshakeTimer = null;
166
396
  let io = null;
167
397
  let idleHandle = null;
168
- const postToFrame = (type, payload, id, transfer) => {
398
+ let resolveHandshake;
399
+ const handshaked = new Promise((res) => {
400
+ resolveHandshake = res;
401
+ });
402
+ const failLocal = (type, id, code, message) => {
403
+ emit("error", { code, message, command: type });
404
+ const p = id ? pending.get(id) : void 0;
405
+ if (p && id) {
406
+ pending.delete(id);
407
+ p.reject(new Error(`[${code}] ${message}`));
408
+ }
409
+ };
410
+ const unsupportedByFrame = (type, id) => failLocal(type, id, "unsupported", `${type} is not supported by this /embed deployment (older version; its xc.ready has no capabilities for it)`);
411
+ const postToFrame = (type, payload, id, transfer, requires) => {
169
412
  if (!port) {
170
- queue.push({ type, payload, id, transfer });
413
+ queue.push({ type, payload, id, transfer, requires });
414
+ return;
415
+ }
416
+ if (requires && !caps?.[requires]) {
417
+ unsupportedByFrame(type, id);
171
418
  return;
172
419
  }
173
420
  port.postMessage(xcMessage(type, payload, id), transfer ?? []);
174
421
  };
175
- const send = (type, payload, wait = false, transfer, fixedId) => {
422
+ const send = (type, payload, wait = false, transfer, fixedId, requires) => {
176
423
  if (destroyed) return Promise.reject(new Error("[project-xiaochun] instance destroyed"));
177
424
  if (!iframe) activate();
178
425
  const id = fixedId ?? `c${++idSeq}`;
179
426
  const p = wait ? new Promise((resolve, reject) => pending.set(id, { resolve, reject, sawEnd: false })) : Promise.resolve();
180
- postToFrame(type, payload, id, transfer);
427
+ postToFrame(type, payload, id, transfer, requires);
181
428
  return p;
182
429
  };
183
430
  const toBuffer = (d) => d instanceof ArrayBuffer ? d : d.buffer.slice(d.byteOffset, d.byteOffset + d.byteLength);
@@ -227,7 +474,7 @@ function createXiaochun(options) {
227
474
  let ended = false;
228
475
  let fmt = rest.format === "pcm16" || rest.format === "float32" ? rest.format : null;
229
476
  let done = Promise.resolve();
230
- const handle2 = {
477
+ const handle = {
231
478
  write(data) {
232
479
  if (ended) throw new Error("[project-xiaochun] audio stream already ended");
233
480
  if (destroyed) throw new Error("[project-xiaochun] instance destroyed");
@@ -261,17 +508,23 @@ function createXiaochun(options) {
261
508
  return done;
262
509
  }
263
510
  };
264
- return handle2;
511
+ return handle;
265
512
  };
266
513
  function buildUrl() {
267
514
  const u = new URL(srcUrl.href);
268
515
  u.searchParams.set("host", window.location.origin);
269
516
  if (transparent) u.searchParams.set("transparent", "1");
270
- if (options.ui) u.searchParams.set("ui", "1");
517
+ if (uiParts.length) u.searchParams.set("ui", uiParts.join(","));
271
518
  if (options.lang) u.searchParams.set("lang", options.lang);
519
+ if (uiAutoHide !== void 0) u.searchParams.set("uiAutoHide", String(uiAutoHide));
520
+ if (typeof camera.fov === "number") u.searchParams.set("cameraFov", String(camera.fov));
521
+ if (typeof camera.distance === "number") u.searchParams.set("cameraDistance", String(camera.distance));
522
+ if (typeof camera.height === "number") u.searchParams.set("cameraHeight", String(camera.height));
523
+ if (typeof camera.intro === "boolean") u.searchParams.set("cameraIntro", camera.intro ? "1" : "0");
272
524
  if (options.heavy) u.searchParams.set("heavy", options.heavy);
273
- if (options.controls) u.searchParams.set("controls", "1");
274
- if (options.model) u.searchParams.set("outfit", options.model);
525
+ if (options.controls === false) u.searchParams.set("controls", "0");
526
+ if (initialScene && isXcId(initialScene)) u.searchParams.set("scene", initialScene);
527
+ if (initialOutfit) u.searchParams.set("outfit", initialOutfit);
275
528
  return u.href;
276
529
  }
277
530
  function activate() {
@@ -297,8 +550,9 @@ function createXiaochun(options) {
297
550
  s.border = "0";
298
551
  s.background = "transparent";
299
552
  s.colorScheme = "normal";
300
- s.borderRadius = "var(--xc-radius, 0)";
553
+ s.borderRadius = `var(--xc-radius, ${radiusValue()})`;
301
554
  f.setAttribute("part", "iframe");
555
+ noSelect(s);
302
556
  s.opacity = "0";
303
557
  s.transition = "opacity .25s ease";
304
558
  s.pointerEvents = passthrough ? "none" : "auto";
@@ -310,7 +564,7 @@ function createXiaochun(options) {
310
564
  if (!port) emit("error", { code: "timeout", message: `no xc.ready from ${frameOrigin} within ${handshakeTimeout}ms` });
311
565
  }, handshakeTimeout);
312
566
  setupAutoPause();
313
- setupPassthrough();
567
+ if (passthrough) enablePassthrough();
314
568
  }
315
569
  const onWindowMessage = (ev) => {
316
570
  if (destroyed || !iframe || ev.source !== iframe.contentWindow) return;
@@ -323,20 +577,39 @@ function createXiaochun(options) {
323
577
  const ch = new MessageChannel();
324
578
  port = ch.port1;
325
579
  port.onmessage = onPortMessage;
580
+ caps = data.payload?.capabilities ?? null;
581
+ const gWant = wantGestures();
582
+ sentGestures = gWant;
583
+ const initConfig = {
584
+ ...options.allowCustomModel ? { allowCustomModel: true } : {},
585
+ ...gWant.move || gWant.resize ? { gestures: gWant } : {}
586
+ };
326
587
  iframe.contentWindow.postMessage(
327
- xcMessage("xc.init", { hostOrigin: window.location.origin }),
588
+ xcMessage("xc.init", { hostOrigin: window.location.origin, ...Object.keys(initConfig).length ? { config: initConfig } : {} }),
328
589
  ev.origin,
329
590
  [ch.port2]
330
591
  );
592
+ warnGestureUnsupported();
331
593
  if (handshakeTimer) {
332
594
  clearTimeout(handshakeTimer);
333
595
  handshakeTimer = null;
334
596
  }
335
597
  emit("handshake", data.payload);
336
- for (const m of queue.splice(0)) port.postMessage(xcMessage(m.type, m.payload, m.id), m.transfer ?? []);
598
+ resolveHandshake();
599
+ for (const m of queue.splice(0)) {
600
+ if (m.requires && !caps?.[m.requires]) unsupportedByFrame(m.type, m.id);
601
+ else port.postMessage(xcMessage(m.type, m.payload, m.id), m.transfer ?? []);
602
+ }
337
603
  syncPause();
338
604
  };
339
605
  window.addEventListener("message", onWindowMessage);
606
+ function ack(id, value) {
607
+ const p = id ? pending.get(id) : void 0;
608
+ if (p && id) {
609
+ pending.delete(id);
610
+ p.resolve(value);
611
+ }
612
+ }
340
613
  function onPortMessage(ev) {
341
614
  const data = ev.data;
342
615
  if (!isXcEnvelope(data)) return;
@@ -347,6 +620,11 @@ function createXiaochun(options) {
347
620
  case "xc.loaded":
348
621
  if (!loaded) {
349
622
  loaded = true;
623
+ if (options.prefetch && options.heavy === "eager" && caps?.prefetch) {
624
+ const ids = Array.isArray(options.prefetch) ? options.prefetch : void 0;
625
+ void api.prefetch(ids).catch(() => {
626
+ });
627
+ }
350
628
  if (iframe) iframe.style.opacity = "1";
351
629
  if (placeholderEl) {
352
630
  const el = placeholderEl;
@@ -359,6 +637,33 @@ function createXiaochun(options) {
359
637
  }
360
638
  emit("ready", data.payload);
361
639
  break;
640
+ case "xc.outfit-changed": {
641
+ const pl = data.payload;
642
+ if (!pl.noop) {
643
+ currentOutfit = pl.id;
644
+ emit("outfit-changed", pl);
645
+ if (!pl.initial && pl.id) writePrefs({ outfit: pl.id });
646
+ }
647
+ ack(data.id);
648
+ break;
649
+ }
650
+ case "xc.scene-changed": {
651
+ const pl = data.payload;
652
+ currentScene = pl.id;
653
+ if (pl.transparent !== transparent) applyTransparency(pl.transparent);
654
+ if (!pl.noop) {
655
+ emit("scene-changed", pl);
656
+ if (!pl.initial) writePrefs({ scene: pl.id });
657
+ }
658
+ ack(data.id);
659
+ break;
660
+ }
661
+ case "xc.lang-changed":
662
+ emit("lang-changed", data.payload);
663
+ break;
664
+ case "xc.prefetched":
665
+ ack(data.id, data.payload);
666
+ break;
362
667
  case "xc.state":
363
668
  emit("state", data.payload);
364
669
  break;
@@ -383,9 +688,16 @@ function createXiaochun(options) {
383
688
  }
384
689
  break;
385
690
  }
691
+ case "xc.gesture-move":
692
+ onGesture("move", data.payload);
693
+ break;
694
+ case "xc.gesture-resize":
695
+ onGesture("resize", data.payload);
696
+ break;
386
697
  case "xc.error": {
387
698
  const { code, message, command } = data.payload;
388
699
  emit("error", { code, message, command });
700
+ if (code === "unknown_id" && (command === "outfit" || command === "scene")) writePrefs({ [command]: null });
389
701
  const p = data.id ? pending.get(data.id) : void 0;
390
702
  if (p && data.id) {
391
703
  pending.delete(data.id);
@@ -442,16 +754,46 @@ function createXiaochun(options) {
442
754
  port.postMessage(xcMessage("xc.pointer", { x, y }));
443
755
  });
444
756
  };
445
- function setupPassthrough() {
446
- if (!passthrough) return;
757
+ let passthroughActive = false;
758
+ function enablePassthrough() {
759
+ if (passthroughActive || !iframe) return;
760
+ passthroughActive = true;
447
761
  window.addEventListener("pointermove", onHostPointer, { passive: true });
762
+ window.addEventListener("pointerdown", onHostPointer, { passive: true });
448
763
  window.addEventListener("scroll", invalidateRect, { passive: true, capture: true });
449
764
  window.addEventListener("resize", invalidateRect, { passive: true });
450
- if (typeof ResizeObserver !== "undefined" && iframe) {
765
+ if (typeof ResizeObserver !== "undefined") {
451
766
  rectRo = new ResizeObserver(invalidateRect);
452
767
  rectRo.observe(iframe);
453
768
  }
454
769
  }
770
+ function disablePassthrough() {
771
+ if (!passthroughActive) return;
772
+ passthroughActive = false;
773
+ window.removeEventListener("pointermove", onHostPointer);
774
+ window.removeEventListener("pointerdown", onHostPointer);
775
+ window.removeEventListener("scroll", invalidateRect, true);
776
+ window.removeEventListener("resize", invalidateRect);
777
+ rectRo?.disconnect();
778
+ rectRo = null;
779
+ if (pointerRaf) {
780
+ cancelAnimationFrame(pointerRaf);
781
+ pointerRaf = 0;
782
+ }
783
+ lastPtr = null;
784
+ }
785
+ function applyTransparency(next) {
786
+ transparent = next;
787
+ applyWrapperBg();
788
+ applyRadius();
789
+ passthrough = next ? options.passthrough ?? true : false;
790
+ lastHit = false;
791
+ lastPosted = null;
792
+ rectCache = null;
793
+ if (passthrough) enablePassthrough();
794
+ else disablePassthrough();
795
+ if (iframe) iframe.style.pointerEvents = passthrough ? "none" : "auto";
796
+ }
455
797
  let pauseIo = null;
456
798
  function setupAutoPause() {
457
799
  if (options.autoPause === false || typeof IntersectionObserver === "undefined") return;
@@ -503,50 +845,177 @@ function createXiaochun(options) {
503
845
  }, { rootMargin: `${options.lazyMargin ?? 200}px` });
504
846
  io.observe(wrapper);
505
847
  }
506
- let handle = null;
507
- if (options.draggable && position !== "inline") {
508
- handle = document.createElement("div");
509
- handle.setAttribute("aria-label", "drag");
510
- Object.assign(handle.style, {
511
- position: "absolute",
512
- left: "0",
513
- top: "0",
514
- width: "28px",
515
- height: "28px",
516
- cursor: "grab",
517
- pointerEvents: "auto",
518
- borderRadius: "8px",
519
- background: "rgba(0,0,0,.25)",
520
- color: "#fff",
521
- font: "14px/28px system-ui",
522
- textAlign: "center",
523
- userSelect: "none",
524
- touchAction: "none",
525
- zIndex: "2",
526
- opacity: "0.5"
527
- });
528
- handle.textContent = "\u283F";
529
- let drag = null;
530
- handle.addEventListener("pointerdown", (e) => {
531
- const r = wrapper.getBoundingClientRect();
532
- drag = { dx: e.clientX - r.left, dy: e.clientY - r.top };
533
- handle.setPointerCapture(e.pointerId);
534
- });
535
- handle.addEventListener("pointermove", (e) => {
536
- if (!drag) return;
537
- ws.left = `${Math.min(Math.max(0, e.clientX - drag.dx), window.innerWidth - 40)}px`;
538
- ws.top = `${Math.min(Math.max(0, e.clientY - drag.dy), window.innerHeight - 40)}px`;
848
+ let draggableOn = options.draggable === true;
849
+ let resizeLimits = normalizeResizable(options.resizable);
850
+ const gates = { move: new GestureGate(), resize: new GestureGate() };
851
+ let activeGesture = null;
852
+ let inlineShift = { x: 0, y: 0 };
853
+ let userMoved = false;
854
+ let sentGestures = null;
855
+ let warnedGesture = false;
856
+ const viewport = () => ({
857
+ width: document.documentElement.clientWidth || window.innerWidth,
858
+ height: document.documentElement.clientHeight || window.innerHeight
859
+ });
860
+ const readBox = () => {
861
+ const r = wrapper.getBoundingClientRect();
862
+ return { left: r.left, top: r.top, width: r.width, height: r.height };
863
+ };
864
+ const wantGestures = () => ({ move: draggableOn && caps?.gestures?.move === true, resize: resizeLimits !== null && caps?.gestures?.resize === true });
865
+ function applyBox(b, inlineBase, withSize) {
866
+ if (withSize) {
867
+ ws.width = `${b.width}px`;
868
+ ws.height = `${b.height}px`;
869
+ ws.maxWidth = "none";
870
+ }
871
+ if (position === "inline") {
872
+ const base = inlineBase ?? { left: b.left - inlineShift.x, top: b.top - inlineShift.y };
873
+ inlineShift = { x: b.left - base.left, y: b.top - base.top };
874
+ ws.setProperty("translate", `${inlineShift.x}px ${inlineShift.y}px`);
875
+ } else {
876
+ ws.left = `${b.left}px`;
877
+ ws.top = `${b.top}px`;
539
878
  ws.right = "auto";
540
879
  ws.bottom = "auto";
541
- invalidateRect();
542
- });
543
- const end = () => {
544
- drag = null;
545
- };
546
- handle.addEventListener("pointerup", end);
547
- handle.addEventListener("pointercancel", end);
548
- wrapper.appendChild(handle);
880
+ }
881
+ userMoved = true;
882
+ invalidateRect();
883
+ }
884
+ let selectionLock = null;
885
+ function lockSelection(on) {
886
+ const de = document.documentElement;
887
+ if (on && !selectionLock) {
888
+ selectionLock = { us: de.style.getPropertyValue("user-select"), wus: de.style.getPropertyValue("-webkit-user-select") };
889
+ de.style.setProperty("user-select", "none");
890
+ de.style.setProperty("-webkit-user-select", "none");
891
+ try {
892
+ window.getSelection()?.removeAllRanges();
893
+ } catch {
894
+ }
895
+ } else if (!on && selectionLock) {
896
+ de.style.setProperty("user-select", selectionLock.us);
897
+ de.style.setProperty("-webkit-user-select", selectionLock.wus);
898
+ selectionLock = null;
899
+ }
900
+ }
901
+ function endActiveGesture(kind) {
902
+ if (!activeGesture || kind && activeGesture.kind !== kind) return;
903
+ gates[activeGesture.kind].close();
904
+ const b = readBox();
905
+ emit(activeGesture.kind, { phase: "end", ...b });
906
+ activeGesture = null;
907
+ lockSelection(false);
908
+ saveBox();
909
+ }
910
+ function onGesture(kind, raw) {
911
+ if (destroyed || !caps?.gestures) return;
912
+ if (kind === "move" ? !draggableOn : resizeLimits === null) return;
913
+ const p = parseGesturePayload(kind, raw);
914
+ if (!p) return;
915
+ if (p.phase === "start" ? activeGesture !== null : activeGesture?.kind !== kind) return;
916
+ if (!gates[kind].accept(p)) return;
917
+ if (p.phase === "start") {
918
+ const start = readBox();
919
+ activeGesture = {
920
+ kind,
921
+ start,
922
+ corner: p.corner,
923
+ inlineBase: position === "inline" ? { left: start.left - inlineShift.x, top: start.top - inlineShift.y } : null
924
+ };
925
+ lockSelection(true);
926
+ emit(kind, { phase: "start", ...start });
927
+ return;
928
+ }
929
+ const a = activeGesture;
930
+ const vp = viewport();
931
+ const next = kind === "move" ? moveBox(a.start, p.totalDx, p.totalDy, vp) : resizeBox(a.start, a.corner, p.totalDx, p.totalDy, resizeLimits, vp);
932
+ applyBox(next, a.inlineBase, kind === "resize");
933
+ emit(kind, { phase: p.phase, ...next });
934
+ if (p.phase === "end") {
935
+ activeGesture = null;
936
+ lockSelection(false);
937
+ saveBox();
938
+ }
939
+ }
940
+ function syncGestures() {
941
+ if (destroyed) return;
942
+ const want = wantGestures();
943
+ if (!want.move) endActiveGesture("move");
944
+ if (!want.resize) endActiveGesture("resize");
945
+ if (!port || !caps) return;
946
+ warnGestureUnsupported();
947
+ if (sentGestures && sentGestures.move === want.move && sentGestures.resize === want.resize) return;
948
+ sentGestures = want;
949
+ if (caps.gestures) port.postMessage(xcMessage("xc.setConfig", { gestures: want }));
950
+ }
951
+ function warnGestureUnsupported() {
952
+ if (warnedGesture || !caps || caps.gestures) return;
953
+ const needMove = draggableOn;
954
+ if (!needMove && resizeLimits === null) return;
955
+ warnedGesture = true;
956
+ const what = [needMove ? "draggable" : "", resizeLimits !== null ? "resizable" : ""].filter(Boolean).join(" / ");
957
+ emit("error", { code: "unsupported", message: `${what} needs gesture support (capabilities.gestures), which this /embed deployment does not advertise (older version)`, command: "gestures" });
958
+ }
959
+ const onWindowResize = () => {
960
+ if (!userMoved || activeGesture || destroyed) return;
961
+ const cur = readBox();
962
+ const vp = viewport();
963
+ const fit = position === "inline" ? { ...fitBox({ ...cur, top: 0 }, { width: vp.width, height: Number.MAX_SAFE_INTEGER }), top: Math.max(cur.top, -window.scrollY) } : fitBox(cur, vp);
964
+ if (fit.left === cur.left && fit.top === cur.top && fit.width === cur.width && fit.height === cur.height) return;
965
+ applyBox(fit, position === "inline" ? { left: cur.left - inlineShift.x, top: cur.top - inlineShift.y } : null, fit.width !== cur.width || fit.height !== cur.height);
966
+ };
967
+ window.addEventListener("resize", onWindowResize, { passive: true });
968
+ const boxKey = options.persistBox === true ? "xiaochun:box" : typeof options.persistBox === "string" && options.persistBox ? `xiaochun:box:${options.persistBox}` : null;
969
+ const saveBox = () => {
970
+ if (!boxKey) return;
971
+ try {
972
+ const b = readBox();
973
+ const at = position === "inline" ? inlineShift : { x: b.left, y: b.top };
974
+ window.localStorage.setItem(boxKey, JSON.stringify({ v: 1, mode: position, x: Math.round(at.x), y: Math.round(at.y), width: Math.round(b.width), height: Math.round(b.height) }));
975
+ } catch {
976
+ }
977
+ };
978
+ const loadBox = () => {
979
+ if (!boxKey) return null;
980
+ try {
981
+ const raw = window.localStorage.getItem(boxKey);
982
+ if (raw === null) return null;
983
+ let o = null;
984
+ try {
985
+ o = JSON.parse(raw);
986
+ } catch {
987
+ }
988
+ const ok = (v, lo, hi) => typeof v === "number" && Number.isFinite(v) && v >= lo && v <= hi;
989
+ if (o && o.v === 1 && o.mode === position && ok(o.x, -1e5, 1e5) && ok(o.y, -1e5, 1e5) && ok(o.width, 1, 1e5) && ok(o.height, 1, 1e5)) {
990
+ return { x: o.x, y: o.y, width: o.width, height: o.height };
991
+ }
992
+ window.localStorage.removeItem(boxKey);
993
+ } catch {
994
+ }
995
+ return null;
996
+ };
997
+ function restoreBox() {
998
+ if (!boxKey || !draggableOn && resizeLimits === null) return;
999
+ const st = loadBox();
1000
+ if (!st) return;
1001
+ const vp = viewport();
1002
+ const lim = resizeLimits ?? DEFAULT_RESIZE_LIMITS;
1003
+ const cur = readBox();
1004
+ const useW = resizeLimits !== null && options.width === void 0;
1005
+ const useH = resizeLimits !== null && options.height === void 0;
1006
+ const clampN = (v, lo, hi) => Math.min(Math.max(v, lo), Math.max(lo, hi));
1007
+ const width = useW ? clampN(st.width, lim.minWidth, Math.min(lim.maxWidth, vp.width)) : cur.width;
1008
+ const height = useH ? clampN(st.height, lim.minHeight, Math.min(lim.maxHeight, vp.height)) : cur.height;
1009
+ if (position === "inline") {
1010
+ const base = { left: cur.left - inlineShift.x, top: cur.top - inlineShift.y };
1011
+ const left = clampN(base.left + st.x, 0, vp.width - width);
1012
+ const top = Math.max(base.top + st.y, -window.scrollY);
1013
+ applyBox({ left, top, width, height }, base, useW || useH);
1014
+ } else {
1015
+ applyBox(fitBox({ left: st.x, top: st.y, width, height }, vp), null, useW || useH);
1016
+ }
549
1017
  }
1018
+ restoreBox();
550
1019
  scheduleLazy();
551
1020
  const api = {
552
1021
  element: wrapper,
@@ -561,8 +1030,98 @@ function createXiaochun(options) {
561
1030
  motion: (m) => send("xc.motion", typeof m === "string" ? m.startsWith("http") || m.startsWith("/") ? { url: m } : { name: m } : m),
562
1031
  expression: (name) => send("xc.expression", { name }),
563
1032
  lookAt: (x, y) => send("xc.lookAt", { x, y }),
564
- setModel: (m) => send("xc.setModel", typeof m === "string" ? /^(https?:)?\/|\.vrm/i.test(m) ? { url: m } : { outfit: m } : m),
1033
+ setOutfit: (id) => {
1034
+ if (!isXcId(id)) return Promise.reject(new Error("[bad_request] invalid outfit id"));
1035
+ return send("xc.setOutfit", { id }, true, void 0, void 0, "outfits");
1036
+ },
1037
+ setScene: (id) => {
1038
+ if (!isXcId(id)) return Promise.reject(new Error("[bad_request] invalid scene id"));
1039
+ return send("xc.setScene", { id }, true, void 0, void 0, "scenes");
1040
+ },
1041
+ prefetch: (ids) => {
1042
+ if (ids !== void 0 && (!Array.isArray(ids) || ids.some((x) => !isXcId(x)))) {
1043
+ return Promise.reject(new Error("[bad_request] prefetch ids must be an array of outfit ids"));
1044
+ }
1045
+ return send("xc.prefetch", ids ? { ids } : {}, true, void 0, void 0, "prefetch");
1046
+ },
1047
+ getOutfits: async () => {
1048
+ if (!iframe) activate();
1049
+ await handshaked;
1050
+ return caps?.outfits ? caps.outfits.slice() : [];
1051
+ },
1052
+ getScenes: async () => {
1053
+ if (!iframe) activate();
1054
+ await handshaked;
1055
+ return caps?.scenes ? caps.scenes.slice() : [];
1056
+ },
1057
+ get outfit() {
1058
+ return currentOutfit;
1059
+ },
1060
+ get scene() {
1061
+ return currentScene;
1062
+ },
1063
+ setModel: (m) => {
1064
+ const msg = typeof m === "string" ? /^(https?:)?\/|\.vrm/i.test(m) ? { url: m } : { outfit: m } : m;
1065
+ if (msg.url !== void 0 && msg.outfit === void 0 && !options.allowCustomModel) {
1066
+ const message = "custom model URLs are disabled; pass allowCustomModel: true to createXiaochun() (only for URLs you trust) or use setOutfit() with a built-in id";
1067
+ emit("error", { code: "unsupported", message, command: "xc.setModel" });
1068
+ return Promise.reject(new Error(`[unsupported] ${message}`));
1069
+ }
1070
+ return send("xc.setModel", msg);
1071
+ },
565
1072
  setConfig: (cfg) => send("xc.setConfig", cfg),
1073
+ setSize(width, height) {
1074
+ if (destroyed) return;
1075
+ ws.width = css(width, defaultWidthCss(position !== "inline"));
1076
+ ws.height = css(height, defaultHeightCss(position !== "inline"));
1077
+ if (ws.maxWidth === "none") ws.maxWidth = "100%";
1078
+ invalidateRect();
1079
+ },
1080
+ getBox: () => readBox(),
1081
+ clearPersistedBox(opts) {
1082
+ if (boxKey) {
1083
+ try {
1084
+ window.localStorage.removeItem(boxKey);
1085
+ } catch {
1086
+ }
1087
+ }
1088
+ if (destroyed || opts?.reset !== true) return;
1089
+ endActiveGesture();
1090
+ ws.width = css(options.width, defaultWidthCss(position !== "inline"));
1091
+ ws.height = css(options.height, defaultHeightCss(position !== "inline"));
1092
+ ws.maxWidth = "100%";
1093
+ if (position === "inline") {
1094
+ inlineShift = { x: 0, y: 0 };
1095
+ ws.removeProperty("translate");
1096
+ } else {
1097
+ ws.left = "";
1098
+ ws.top = "";
1099
+ ws.right = "";
1100
+ ws.bottom = "";
1101
+ if (position === "bottom-right") {
1102
+ ws.right = "var(--xc-offset-x, 16px)";
1103
+ ws.bottom = "var(--xc-offset-y, 16px)";
1104
+ }
1105
+ if (position === "bottom-left") {
1106
+ ws.left = "var(--xc-offset-x, 16px)";
1107
+ ws.bottom = "var(--xc-offset-y, 16px)";
1108
+ }
1109
+ }
1110
+ userMoved = false;
1111
+ invalidateRect();
1112
+ },
1113
+ setBorderRadius(radius) {
1114
+ radiusOpt = radius;
1115
+ applyRadius();
1116
+ },
1117
+ setDraggable(on) {
1118
+ draggableOn = on === true;
1119
+ syncGestures();
1120
+ },
1121
+ setResizable(on) {
1122
+ resizeLimits = normalizeResizable(on);
1123
+ syncGestures();
1124
+ },
566
1125
  startListening: () => send("xc.mic", { enabled: true }),
567
1126
  stopListening: () => send("xc.mic", { enabled: false }),
568
1127
  mic: (enabled) => send("xc.mic", { enabled }),
@@ -587,10 +1146,10 @@ function createXiaochun(options) {
587
1146
  if (handshakeTimer) clearTimeout(handshakeTimer);
588
1147
  if (pointerRaf) cancelAnimationFrame(pointerRaf);
589
1148
  window.removeEventListener("message", onWindowMessage);
590
- window.removeEventListener("pointermove", onHostPointer);
591
- window.removeEventListener("scroll", invalidateRect, true);
592
- window.removeEventListener("resize", invalidateRect);
593
- rectRo?.disconnect();
1149
+ window.removeEventListener("resize", onWindowResize);
1150
+ lockSelection(false);
1151
+ disablePassthrough();
1152
+ resolveHandshake();
594
1153
  try {
595
1154
  port?.close();
596
1155
  } catch {
@@ -629,7 +1188,12 @@ var EVENT_MAP = [
629
1188
  ["stt", "onStt"],
630
1189
  ["utterance", "onUtterance"],
631
1190
  ["hit-region", "onHitRegion"],
1191
+ ["outfit-changed", "onOutfitChanged"],
1192
+ ["scene-changed", "onSceneChanged"],
1193
+ ["lang-changed", "onLangChanged"],
632
1194
  ["error", "onError"],
1195
+ ["move", "onMove"],
1196
+ ["resize", "onResize"],
633
1197
  ["destroy", "onDestroy"]
634
1198
  ];
635
1199
  function useXiaochun(options = {}) {
@@ -644,8 +1208,8 @@ function useXiaochun(options = {}) {
644
1208
  (0, import_react.useEffect)(() => {
645
1209
  if (!el) return;
646
1210
  const cur = live.current;
647
- const { onHandshake: _a, onReady: _b, onProgress: _c, onState: _d, onStt: _e, onUtterance: _f, onHitRegion: _g, onError: _h, onDestroy: _i, paused: _p, mic: _m, allowedOrigins, ...create } = cur;
648
- void [_a, _b, _c, _d, _e, _f, _g, _h, _i, _p, _m];
1211
+ const { onHandshake: _a, onReady: _b, onProgress: _c, onState: _d, onStt: _e, onUtterance: _f, onHitRegion: _g, onError: _h, onDestroy: _i, onOutfitChanged: _j, onSceneChanged: _k, onLangChanged: _l, onMove: _q, onResize: _r, paused: _p, mic: _m, allowedOrigins, ...create } = cur;
1212
+ void [_a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _p, _m, _q, _r];
649
1213
  const inst = createXiaochun({ ...create, allowedOrigins: allowedOrigins?.slice(), container: el });
650
1214
  const offs = [];
651
1215
  for (const [ev, cbName] of EVENT_MAP) {
@@ -675,11 +1239,9 @@ function useXiaochun(options = {}) {
675
1239
  o.lazyMargin,
676
1240
  o.placeholder,
677
1241
  o.transparent,
678
- o.width,
679
- o.height,
680
1242
  o.position,
681
- o.draggable,
682
- o.ui,
1243
+ JSON.stringify(o.ui ?? null),
1244
+ o.uiAutoHide,
683
1245
  o.heavy,
684
1246
  o.controls,
685
1247
  o.autoPause,
@@ -687,9 +1249,16 @@ function useXiaochun(options = {}) {
687
1249
  o.sandbox,
688
1250
  o.handshakeTimeout,
689
1251
  o.crossOriginIsolated,
690
- o.zIndex
1252
+ o.zIndex,
1253
+ o.allowCustomModel,
1254
+ o.persist,
1255
+ o.persistBox,
1256
+ Array.isArray(o.prefetch) ? o.prefetch.join(",") : o.prefetch
691
1257
  ]);
692
- const first = (0, import_react.useRef)({ lang: o.lang, model: o.model });
1258
+ const wantOutfit = o.outfit ?? o.model;
1259
+ const cameraKey = JSON.stringify(o.camera ?? null);
1260
+ const first = (0, import_react.useRef)({ lang: o.lang, outfit: wantOutfit, scene: o.scene, width: o.width, height: o.height, camera: cameraKey });
1261
+ const resizableKey = JSON.stringify(o.resizable ?? false);
693
1262
  (0, import_react.useEffect)(() => {
694
1263
  if (!client || first.current.lang === o.lang) return;
695
1264
  first.current.lang = o.lang;
@@ -697,11 +1266,39 @@ function useXiaochun(options = {}) {
697
1266
  });
698
1267
  }, [client, o.lang]);
699
1268
  (0, import_react.useEffect)(() => {
700
- if (!client || first.current.model === o.model) return;
701
- first.current.model = o.model;
702
- if (o.model) void client.setModel(o.model).catch(() => {
1269
+ if (!client || first.current.outfit === wantOutfit) return;
1270
+ first.current.outfit = wantOutfit;
1271
+ if (wantOutfit) void (isXcId(wantOutfit) ? client.setOutfit(wantOutfit) : client.setModel(wantOutfit)).catch(() => {
703
1272
  });
704
- }, [client, o.model]);
1273
+ }, [client, wantOutfit]);
1274
+ (0, import_react.useEffect)(() => {
1275
+ if (!client || first.current.camera === cameraKey) return;
1276
+ first.current.camera = cameraKey;
1277
+ const c = o.camera;
1278
+ void client.setConfig({ camera: { fov: c?.fov ?? null, distance: c?.distance ?? null, height: c?.height ?? null, intro: c?.intro ?? null } }).catch(() => {
1279
+ });
1280
+ }, [client, cameraKey]);
1281
+ (0, import_react.useEffect)(() => {
1282
+ if (!client || first.current.scene === o.scene) return;
1283
+ first.current.scene = o.scene;
1284
+ if (o.scene) void client.setScene(o.scene).catch(() => {
1285
+ });
1286
+ }, [client, o.scene]);
1287
+ (0, import_react.useEffect)(() => {
1288
+ if (!client || first.current.width === o.width && first.current.height === o.height) return;
1289
+ first.current.width = o.width;
1290
+ first.current.height = o.height;
1291
+ client.setSize(o.width, o.height);
1292
+ }, [client, o.width, o.height]);
1293
+ (0, import_react.useEffect)(() => {
1294
+ client?.setDraggable(o.draggable === true);
1295
+ }, [client, o.draggable]);
1296
+ (0, import_react.useEffect)(() => {
1297
+ client?.setBorderRadius(o.borderRadius);
1298
+ }, [client, o.borderRadius]);
1299
+ (0, import_react.useEffect)(() => {
1300
+ client?.setResizable(o.resizable ?? false);
1301
+ }, [client, resizableKey]);
705
1302
  (0, import_react.useEffect)(() => {
706
1303
  if (!client || o.paused === void 0) return;
707
1304
  if (o.paused) client.pause();
@@ -741,6 +1338,12 @@ function XiaochunInner(props, ref) {
741
1338
  motion: (m) => c()?.motion(m) ?? reject(),
742
1339
  expression: (n) => c()?.expression(n) ?? reject(),
743
1340
  lookAt: (x, y) => c()?.lookAt(x, y) ?? reject(),
1341
+ setOutfit: (id) => c()?.setOutfit(id) ?? reject(),
1342
+ setScene: (id) => c()?.setScene(id) ?? reject(),
1343
+ getOutfits: () => c()?.getOutfits() ?? reject(),
1344
+ getScenes: () => c()?.getScenes() ?? reject(),
1345
+ clearPersistedBox: (opts) => c()?.clearPersistedBox(opts),
1346
+ prefetch: (ids) => c()?.prefetch(ids) ?? reject(),
744
1347
  setModel: (m) => c()?.setModel(m) ?? reject(),
745
1348
  setConfig: (cfg) => c()?.setConfig(cfg) ?? reject(),
746
1349
  startListening: () => c()?.startListening() ?? reject(),
@@ -760,8 +1363,8 @@ function XiaochunInner(props, ref) {
760
1363
  }, []);
761
1364
  const box = {
762
1365
  ...style,
763
- width: css2(props.width, "320px"),
764
- height: css2(props.height, "480px"),
1366
+ width: css2(props.width, defaultWidthCss()),
1367
+ height: css2(props.height, defaultHeightCss()),
765
1368
  maxWidth: "100%",
766
1369
  position: style?.position ?? "relative"
767
1370
  };