@firetable/project-xiaochun 0.1.15 → 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.15 | 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;
@@ -31,7 +31,7 @@ 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
33
  var XC_WINDOW_CORNER_RADIUS = 20;
34
- var XC_UI_PARTS = ["chat", "bubble", "outfit", "scene"];
34
+ var XC_UI_PARTS = ["chat", "bubble", "outfit", "scene", "lang", "github"];
35
35
  var XC_UI_PART_SET = new Set(XC_UI_PARTS);
36
36
  var XC_UI_LEGACY_PARTS = ["chat", "bubble"];
37
37
  function parseXcUiList(items) {
@@ -49,6 +49,49 @@ function normalizeXcUiOption(v) {
49
49
  if (Array.isArray(v)) return parseXcUiList(v);
50
50
  return { parts: [], unknown: [], legacy: false };
51
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
+ }
52
95
  var XC_ID_RE = /^[a-z][a-z0-9_]{0,63}$/;
53
96
  var isXcId = (v) => typeof v === "string" && XC_ID_RE.test(v);
54
97
  function xcMessage(type, payload, id) {
@@ -165,7 +208,20 @@ var GestureGate = class {
165
208
  var FALLBACK_PLACEHOLDER = "data:image/svg+xml;utf8," + encodeURIComponent(
166
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>'
167
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
+ };
168
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
+ };
169
225
  function createXiaochun(options) {
170
226
  if (typeof window === "undefined" || typeof document === "undefined") {
171
227
  throw new Error("[project-xiaochun] createXiaochun must run in a browser");
@@ -220,6 +276,16 @@ function createXiaochun(options) {
220
276
  }
221
277
  const uiParse = normalizeXcUiOption(options.ui);
222
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
+ }
223
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'].`);
224
290
  if (uiParse.unknown.length) console.warn(`[project-xiaochun] ignoring unknown ui part(s): ${uiParse.unknown.join(", ")} (known: ${XC_UI_PARTS.join(", ")})`);
225
291
  const outfitOpt = options.outfit ?? (isXcId(options.model) ? options.model : void 0);
@@ -234,11 +300,12 @@ function createXiaochun(options) {
234
300
  wrapper.setAttribute("data-xiaochun", "");
235
301
  const ws = wrapper.style;
236
302
  ws.position = position === "inline" ? "relative" : "fixed";
237
- ws.width = css(options.width, "320px");
238
- ws.height = css(options.height, "480px");
303
+ ws.width = css(options.width, defaultWidthCss(position !== "inline"));
304
+ ws.height = css(options.height, defaultHeightCss(position !== "inline"));
239
305
  ws.maxWidth = "100%";
240
306
  ws.overflow = "visible";
241
307
  ws.contain = "layout style";
308
+ noSelect(ws);
242
309
  ws.boxShadow = "var(--xc-shadow, none)";
243
310
  const applyWrapperBg = () => {
244
311
  ws.background = transparent ? "" : "var(--xc-bg, transparent)";
@@ -273,8 +340,8 @@ function createXiaochun(options) {
273
340
  img.src = options.placeholder || FALLBACK_PLACEHOLDER;
274
341
  img.alt = "Project XiaoChun";
275
342
  img.decoding = "async";
276
- img.width = 320;
277
- img.height = 480;
343
+ img.width = XC_DEFAULT_WIDTH;
344
+ img.height = XC_DEFAULT_HEIGHT;
278
345
  placeholderEl = img;
279
346
  }
280
347
  const ps = placeholderEl.style;
@@ -288,6 +355,7 @@ function createXiaochun(options) {
288
355
  ps.transition = "opacity .25s ease";
289
356
  ps.pointerEvents = "auto";
290
357
  ps.cursor = lazy === false ? "default" : "pointer";
358
+ noSelect(ps);
291
359
  placeholderEl.addEventListener("click", () => activate());
292
360
  wrapper.appendChild(placeholderEl);
293
361
  }
@@ -446,6 +514,11 @@ function createXiaochun(options) {
446
514
  if (transparent) u.searchParams.set("transparent", "1");
447
515
  if (uiParts.length) u.searchParams.set("ui", uiParts.join(","));
448
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");
449
522
  if (options.heavy) u.searchParams.set("heavy", options.heavy);
450
523
  if (options.controls === false) u.searchParams.set("controls", "0");
451
524
  if (initialScene && isXcId(initialScene)) u.searchParams.set("scene", initialScene);
@@ -477,6 +550,7 @@ function createXiaochun(options) {
477
550
  s.colorScheme = "normal";
478
551
  s.borderRadius = `var(--xc-radius, ${radiusValue()})`;
479
552
  f.setAttribute("part", "iframe");
553
+ noSelect(s);
480
554
  s.opacity = "0";
481
555
  s.transition = "opacity .25s ease";
482
556
  s.pointerEvents = passthrough ? "none" : "auto";
@@ -582,6 +656,9 @@ function createXiaochun(options) {
582
656
  ack(data.id);
583
657
  break;
584
658
  }
659
+ case "xc.lang-changed":
660
+ emit("lang-changed", data.payload);
661
+ break;
585
662
  case "xc.prefetched":
586
663
  ack(data.id, data.payload);
587
664
  break;
@@ -826,6 +903,7 @@ function createXiaochun(options) {
826
903
  emit(activeGesture.kind, { phase: "end", ...b });
827
904
  activeGesture = null;
828
905
  lockSelection(false);
906
+ saveBox();
829
907
  }
830
908
  function onGesture(kind, raw) {
831
909
  if (destroyed || !caps?.gestures) return;
@@ -854,6 +932,7 @@ function createXiaochun(options) {
854
932
  if (p.phase === "end") {
855
933
  activeGesture = null;
856
934
  lockSelection(false);
935
+ saveBox();
857
936
  }
858
937
  }
859
938
  function syncGestures() {
@@ -878,11 +957,63 @@ function createXiaochun(options) {
878
957
  const onWindowResize = () => {
879
958
  if (!userMoved || activeGesture || destroyed) return;
880
959
  const cur = readBox();
881
- const fit = fitBox(cur, viewport());
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);
882
962
  if (fit.left === cur.left && fit.top === cur.top && fit.width === cur.width && fit.height === cur.height) return;
883
963
  applyBox(fit, position === "inline" ? { left: cur.left - inlineShift.x, top: cur.top - inlineShift.y } : null, fit.width !== cur.width || fit.height !== cur.height);
884
964
  };
885
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();
886
1017
  scheduleLazy();
887
1018
  const api = {
888
1019
  element: wrapper,
@@ -939,11 +1070,44 @@ function createXiaochun(options) {
939
1070
  setConfig: (cfg) => send("xc.setConfig", cfg),
940
1071
  setSize(width, height) {
941
1072
  if (destroyed) return;
942
- ws.width = css(width, "320px");
943
- ws.height = css(height, "480px");
1073
+ ws.width = css(width, defaultWidthCss(position !== "inline"));
1074
+ ws.height = css(height, defaultHeightCss(position !== "inline"));
1075
+ if (ws.maxWidth === "none") ws.maxWidth = "100%";
944
1076
  invalidateRect();
945
1077
  },
946
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
+ },
947
1111
  setBorderRadius(radius) {
948
1112
  radiusOpt = radius;
949
1113
  applyRadius();
@@ -1034,14 +1198,20 @@ var OBSERVED = [
1034
1198
  "placeholder",
1035
1199
  "heavy",
1036
1200
  "ui",
1201
+ "ui-autohide",
1202
+ "camera-fov",
1203
+ "camera-distance",
1204
+ "camera-height",
1205
+ "camera-intro",
1037
1206
  "controls",
1038
1207
  "allowed-origins",
1039
1208
  "cross-origin-isolated",
1040
1209
  "persist",
1210
+ "persist-box",
1041
1211
  "allow-custom-model",
1042
1212
  "prefetch"
1043
1213
  ];
1044
- var REBUILD = /* @__PURE__ */ new Set(["src", "position", "lazy", "placeholder", "heavy", "ui", "controls", "allowed-origins", "transparent", "cross-origin-isolated", "persist", "allow-custom-model", "prefetch"]);
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"]);
1045
1215
  function radiusAttr(v) {
1046
1216
  if (v === null || v.trim() === "") return void 0;
1047
1217
  const t = v.trim();
@@ -1057,22 +1227,37 @@ function prefetchAttr(v) {
1057
1227
  if (v === "" || v === "true") return true;
1058
1228
  return v.split(",").map((s) => s.trim()).filter(Boolean);
1059
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
+ }
1060
1235
  function uiAttr(v) {
1061
1236
  if (v === null || v === "false" || v === "0") return void 0;
1062
1237
  if (v === "" || v === "true" || v === "1") return true;
1063
1238
  return v.split(",").map((s) => s.trim()).filter(Boolean);
1064
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
+ }
1065
1250
  function parsePx(v) {
1066
1251
  const m = v?.trim().match(/^(\d+(?:\.\d+)?)\s*x\s*(\d+(?:\.\d+)?)$/i);
1067
1252
  return m ? { w: Number(m[1]), h: Number(m[2]) } : void 0;
1068
1253
  }
1069
1254
  function parseSize(v) {
1070
- if (!v) return { width: "320px", height: "480px" };
1255
+ if (!v) return { width: void 0, height: void 0 };
1071
1256
  const m = v.trim().split(/x/i);
1072
1257
  const px = (s) => /^\d+(\.\d+)?$/.test(s) ? `${s}px` : s;
1073
1258
  if (m.length >= 2) return { width: px(m[0]), height: px(m[1]) };
1074
1259
  const w = parseFloat(m[0]);
1075
- 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 };
1076
1261
  }
1077
1262
  function createElementClass() {
1078
1263
  class XiaochunAvatarElementImpl extends HTMLElement {
@@ -1127,6 +1312,8 @@ function createElementClass() {
1127
1312
  } else if (name === "border-radius") c.setBorderRadius(radiusAttr(newV));
1128
1313
  else if (name === "draggable") c.setDraggable(boolAttr(this, "draggable", false));
1129
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
+ });
1130
1317
  else if (name === "mic") void c.mic(boolAttr(this, "mic", false)).catch(() => {
1131
1318
  });
1132
1319
  else if (name === "paused") {
@@ -1159,10 +1346,13 @@ function createElementClass() {
1159
1346
  outfit: this.getAttribute("outfit") ?? this.getAttribute("model") ?? void 0,
1160
1347
  scene: this.getAttribute("scene") ?? void 0,
1161
1348
  persist: this.getAttribute("persist") || void 0,
1349
+ persistBox: persistBoxAttr(this.getAttribute("persist-box")),
1162
1350
  allowCustomModel: boolAttr(this, "allow-custom-model", false),
1163
1351
  prefetch: prefetchAttr(this.getAttribute("prefetch")),
1164
1352
  heavy: this.getAttribute("heavy") ?? void 0,
1165
1353
  ui: uiAttr(this.getAttribute("ui")),
1354
+ uiAutoHide: parseXcUiAutoHide(this.getAttribute("ui-autohide")),
1355
+ camera: cameraAttrs(this),
1166
1356
  controls: boolAttr(this, "controls", true),
1167
1357
  crossOriginIsolated: boolAttr(this, "cross-origin-isolated", false)
1168
1358
  });
@@ -1176,6 +1366,7 @@ function createElementClass() {
1176
1366
  fwd("error", "xc-error");
1177
1367
  fwd("outfit-changed", "xc-outfit-changed");
1178
1368
  fwd("scene-changed", "xc-scene-changed");
1369
+ fwd("lang-changed", "xc-lang-changed");
1179
1370
  fwd("move", "xc-move");
1180
1371
  fwd("resize", "xc-resize");
1181
1372
  if (boolAttr(this, "paused", false)) c.pause();
@@ -1215,6 +1406,9 @@ function createElementClass() {
1215
1406
  getScenes() {
1216
1407
  return this.requireClient().getScenes();
1217
1408
  }
1409
+ clearPersistedBox(opts) {
1410
+ this.client?.clearPersistedBox(opts);
1411
+ }
1218
1412
  prefetch(ids) {
1219
1413
  return this.requireClient().prefetch(ids);
1220
1414
  }