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