@firetable/project-xiaochun 0.1.15 → 0.1.17

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/README-CN.md CHANGED
@@ -152,12 +152,14 @@ await xc.speakAudio(arrayBuffer, { text: '你好', motion: true, lipsync: true }
152
152
  await xc.speakAudio(blob); // Blob(mp3 / wav / ogg 等)
153
153
  await xc.speakAudio('https://cdn.example.com/voice.mp3'); // URL:默认由宿主页 fetch({ fetch: 'frame' } = 交给 iframe 去 fetch)
154
154
  await xc.speakAudio(pcm, { format: 'pcm16', sampleRate: 24000 }); // 无头原始 PCM 必须给 sampleRate(8000–96000)
155
+ await xc.speakAudio(pcm, { format: 'pcm16', sampleRate: 16000, audible: false }); // 只做动作和口型, iframe 增益为 0
155
156
 
156
157
  const s = xc.speakAudioStream({ sampleRate: 24000 }); // 流式:比如 TTS 服务边合成边返回的 PCM 块
157
158
  s.write(int16Chunk); s.write(next); s.end(); await s.done; // s.abort() 立即停止
158
159
  // 随时可取消:传 { signal: abortController.signal }
159
160
  ```
160
161
 
162
+ * `audible` 默认 `true`。`false` 仍会解码、生成动作并驱动口型,但 iframe 增益为 0,声音可以由宿主页面自己播放。
161
163
  * 失败时 reject:`bad_request`(无法解码 / 空音频 / URL 或 sampleRate 不合法)、`unsupported`、`failed`。
162
164
  * 浏览器仍要求宿主页先有用户手势才能出声,iframe 也要有 `allow="autoplay"`(SDK 已自动设置)。
163
165
  * 新的 `say()` / `speakAudio()` 会打断正在进行的那一次,被打断的 Promise 会 resolve。
@@ -187,22 +189,24 @@ xc.on('scene-changed', (p) => console.log(p.id));
187
189
  * **偏好保存**:iframe 用**自己的** localStorage 记住用户最近的服装 / 场景(键 `xiaochun_wearing_outfit` / `xiaochun_scene_theme`,与主站一致)。优先级:显式的 `outfit` / `scene`(URL 或 SDK 选项)> 已保存 > 默认;存的 id 不在白名单会被忽略并清掉;存储被拦截 / 分区时静默回退默认。第三方存储按顶层站点分区,每个宿主站各一份。
188
190
  * **`persist`**(可选)另外把它们存在宿主页 localStorage,并作为显式值传回 iframe,因此会盖过 iframe 自己存的。默认 `false`。
189
191
  * **`prefetch`**(默认关)只把服装文件下载进 iframe 的 IndexedDB(不解压不合成),串行、排在 EMAGE 加载之后,除非你点名否则不含婚纱。`await xc.prefetch(['xiaochun_cheongsam'])` 任何模式都能用;自动预取需要 `heavy: 'eager'`。用户开了省流量模式则跳过。之后 `setOutfit` 不再走网络(解压合成约 0.9 秒仍在)。
190
- * **内置按钮**:见下一节(`ui: ['outfit', 'scene']`)。
192
+ * **内置按钮**:见下一节(`ui: ['outfit', 'scene', 'lang', 'github']`、`uiAutoHide`)。
191
193
  * **自定义模型**:`setModel({ url })` **默认关闭**,需要 `allowCustomModel: true` 显式开启。
192
194
  * **迁移**:选项 `model` → `outfit`;旧的 `setModel('base')` 不再可用(列表请用 `getOutfits()`)。
193
195
 
194
- ### 🔘 内置换装 / 换场景按钮(`ui`)
196
+ ### 🔘 内置按钮(`ui`、`uiAutoHide`)
195
197
 
196
198
  `ui` 是**部件名数组**,不写就什么都不显示。
197
199
 
198
200
  ```ts
199
- createXiaochun({ container: '#avatar', ui: ['outfit', 'scene'] }); // 也可以 ['chat', 'bubble', 'outfit', 'scene']
201
+ createXiaochun({ container: '#avatar', ui: ['outfit', 'scene', 'lang', 'github'] }); // 部件:chat · bubble · outfit · scene · lang · github
200
202
  ```
201
203
  ```html
202
- <xiaochun-avatar ui="outfit,scene"></xiaochun-avatar> <!-- URL 写法: /embed?ui=chat,outfit,scene -->
204
+ <xiaochun-avatar ui="outfit,scene,lang,github" ui-autohide="false"></xiaochun-avatar> <!-- URL 写法: /embed?ui=outfit,scene,lang,github&uiAutoHide=false -->
203
205
  ```
204
206
  React:`<Xiaochun ui={['outfit', 'scene']} />`。运行时:`xc.setConfig({ ui: ['outfit'] })`。
205
207
 
208
+ * **何时显示(`uiAutoHide`,与桌面端一致)**:默认 `'transparent'`——**透明**场景下按钮和聊天栏初始隐藏,**单击角色**才出现(再点角色或点空白收起;10 秒无操作也自动收起,悬停在它们上面或菜单打开时不收);亮 / 暗场景一直显示。`true` 把"点击才出现"扩展到所有场景,`false` 保持一直显示。拖动 iframe(`draggable`)不算单击(位移 ≤ 6px 才算)。限制:透明场景下点宿主页空白处到不了 iframe(穿透),所以收起靠 10 秒超时或再点角色;触屏第一次轻触只用来唤醒命中检测。需要 iframe 声明 `capabilities.ui.autoHide`,旧 iframe 忽略该选项、常显。
209
+ * **语言 / GitHub 按钮**:`lang` 弹出菜单(简体中文 / English / 日本語),选择后立即生效,存在 **iframe 自己的** localStorage(`xiaochun_embed_lang`),并发 `lang-changed`(`{ lang, previous?, initial? }`,握手后会发一次 `initial: true`)。只记用户自己点的——显式 `lang` 选项和 `setConfig({ lang })` 不写存储。`github` 是 `<a target="_blank" rel="noopener noreferrer">`,指向项目仓库。
206
210
  * **和主站 TopHeader 同一套组件**:玻璃质感按钮(触屏 44×44,桌面 36×36)、下拉菜单、`Shirt` / `MountainSnow` / `Check` / `Loader2` 图标、同一批 i18n 文案(随 `lang`)。放在右上角,不遮角色。服装列表来自 `capabilities.outfits`(不含裸模、没有自定义 URL),每行只显示名字(菜单里不显示文件体积)、加载中转圈、当前穿着打 ✓。
207
211
  * **与 `setOutfit` / `setScene` 同一条路径**:同样的白名单、同一个串行 last-wins 队列,连点会出现轻提示"busy",并照常发 `outfit-changed` / `scene-changed`。宿主用 SDK 调用时按钮状态同步。点按钮不会触发转身/拖动手势。
208
212
  * **透明场景**:按钮参与穿透命中(点按钮不会漏到宿主页,空白处仍穿透)。触屏 + 透明场景下,第一次轻触只用来唤醒命中检测(落在宿主页上),第二次才落到按钮。
@@ -237,6 +241,8 @@ xiaochun-avatar::part(iframe) { outline: 1px solid #0002; }
237
241
 
238
242
  可用的 part:`mount` · `wrapper` · `iframe` · `placeholder`。
239
243
 
244
+ **不会被选区染蓝**:宿主页的文字选区(拖选划过、Cmd/Ctrl+A)跨过 iframe 时,Chrome 会给整块 iframe 盖一层蓝色高亮。SDK 给外壳、占位图、iframe 都设了 `user-select: none`(CSSOM 内联样式,不注入 `<style>`,不受宿主 CSP 影响);宿主自己的文字照常可选,`pointer-events` 和透明穿透不变。别在 `::part(iframe)` 上把 `user-select` 改回 `auto`。
245
+
240
246
  ---
241
247
 
242
248
  ## 🔌 `xiaochun://` 与 `xc.*`
@@ -357,18 +363,21 @@ createXiaochun({
357
363
  | `placeholder` | 内置 SVG | 图片 URL、元素或 `false` |
358
364
  | `transparent` | `false` | 背景透明叠在页面上(同时开启指针穿透),等价于 `scene: 'transparent'` |
359
365
  | `scene` | — | 初始场景:`'light' \| 'dark' \| 'transparent'`(见 `getScenes()`)。未知 id 会被忽略并触发 `error { code: 'unknown_id' }` |
360
- | `width`、`height` | `320`、`480` | px 或任意 CSS 长度,务必设置 |
366
+ | `width`、`height` | `600`、`1080` | px 或任意 CSS 长度。**默认值受视口限制**:宽 = `min(600px, 100vw)`(外壳另有 `max-width: 100%`,窄容器不溢出),高 = `min(1080px, 100svh)`(悬浮 `position` 还会扣掉 `--xc-offset-x/y` 边距,整块不会顶出屏幕);显式传入的值原样使用。运行时 `setSize(w, h)`(传 `undefined` = 恢复默认) |
361
367
  | `position` | `'inline'` | `'inline' \| 'bottom-right' \| 'bottom-left'` |
362
368
  | `draggable` | `false` | 手势拖动:按住角色(不要压在内置按钮上)拖 = 移动 iframe,限制在视口内;内联 / 悬浮模式都生效。见下文「手势」 |
363
369
  | `borderRadius` | 非透明 `20px` / 透明 `0` | 外壳圆角(数字 = px 或任意 CSS 长度)。light / dark 场景默认与桌面版窗口圆角同值(20px),透明场景不裁角。运行时用 `setBorderRadius()`;`--xc-radius` CSS 变量优先;`0` = 方角 |
364
370
  | `resizable` | `false` | 拖四个角缩放 iframe(与桌面版同一套 40px 热区 / 光标 / 圆弧),运行时状态,**不重建 iframe**。可传 `true` 或 `{ minWidth, minHeight, maxWidth, maxHeight }`(默认最小 120×180,最大 = 视口) |
365
- | `lang` | — | `'zh-CN' \| 'en' \| 'ja'` |
371
+ | `lang` | 自动 | `'zh-CN' \| 'en' \| 'ja'`。优先级:此选项 > 用户上次在内置语言按钮里选的(存在 iframe 自己的 localStorage `xiaochun_embed_lang`)> 浏览器语言 > `zh-CN`。运行时 `setConfig({ lang })` 不持久化。变化时发 `lang-changed` |
366
372
  | `outfit` | 默认服装 | 初始服装 id(如 `xiaochun_maid`,见 `getOutfits()`)。未知 id 回退默认服装并触发 `error { code: 'unknown_id' }` |
367
373
  | `model` | — | **已弃用**,请改用 `outfit`(会打印 `console.warn`)。不再接受 https URL,见 `allowCustomModel` |
368
374
  | `allowCustomModel` | `false` | 显式开启后才允许 `setModel({ url })` 加载任意 https `.vrm` / `.vrmaddon` / `.vrmbase`。第三方文件会在 iframe 里解析,仅对可信 URL 打开 |
375
+ | `camera` | — | 相机取景 `{ fov, distance, height, intro }`:`fov` 15–60°(默认 30,视距按 fov 自动补偿,角色大小基本不变),`distance` 1–15 米(默认约 2.5;首次取景时盖过 iframe 保存的缩放 / 俯仰),`height` ±1 米取景高度偏移(iframe 不保存),`intro: false` 不播推镜头。越界值夹到范围;没有 `pitch`(相机只绕角色上下俯仰,左右是角色自身的 bodyTurn)。创建期写进 `?cameraFov=` 等 URL 参数;运行时用 `setConfig({ camera })`(缺省键不变、`null` 恢复默认,立即重新取景并取消进行中的推镜头)。旧版 `/embed`(无 `capabilities.camera`)忽略。优先级:显式 > iframe 保存的视角 > 默认。 |
376
+ | `persistBox` | `false` | 在**宿主页** localStorage 记住拖动 / 缩放后的位置和大小,下次创建时恢复:`true`(key `xiaochun:box`)或命名空间字符串(`xiaochun:box:<名字>`)。需要 `draggable` / `resizable`。优先级:显式 `width` / `height` > 已保存 > 默认;恢复时钳制到当前视口。`clearPersistedBox({ reset? })` 清除。见「手势」一节 |
369
377
  | `persist` | `false` | 可选:另把服装 + 场景偏好保存在**宿主页**的 localStorage(iframe 本来就会存一份自己的):`false` \| `'host'`(`xiaochun:prefs`)\| 自定义 key。显式的 `outfit` / `scene` 选项优先于已保存的偏好;宿主保存的优先于 iframe 自己存的 |
370
378
  | `prefetch` | `false` | `true`(全部服装,婚纱 13.9 MB 除外)或 `string[]`。首次加载完成后自动发一次,**仅在 `heavy: 'eager'` 时**;否则请自己调用 `prefetch()` |
371
- | `ui` | `[]` | iframe 内要显示的内置界面部件,数组,可选 `'chat'`(聊天栏)· `'bubble'`(头顶气泡)· `'outfit'`(换装按钮)· `'scene'`(换场景按钮)。不写/空 = 都不显示;未知名字被忽略并 `console.warn`。见下文「内置换装 / 换场景按钮」 |
379
+ | `ui` | `[]` | iframe 内要显示的内置界面部件,数组,可选 `'chat'`(聊天栏)· `'bubble'`(头顶气泡)· `'outfit'`(换装按钮)· `'scene'`(换场景按钮)· `'lang'`(语言切换)· `'github'`(GitHub 链接,新标签页打开)。不写/空 = 都不显示;未知名字被忽略并 `console.warn`。见下文「内置按钮」 |
380
+ | `uiAutoHide` | `'transparent'` | 内置界面何时显示,**与桌面端一致**:`'transparent'` = 只有透明(桌宠)场景初始隐藏、单击角色才出现,亮 / 暗场景常显;`true` = 所有场景都点击才出现;`false` = 一直显示(旧行为)。运行时 `setConfig({ uiAutoHide })` 热切换 |
372
381
  | `heavy` | `'lazy'` | `'lazy'`:首次使用才加载 WebLLM / EMAGE · `'eager'`:预加载 |
373
382
  | `controls` | `true` | iframe 内滚轮缩放,默认开(与主站一致)。透明场景:只有指针在角色上才缩放,其余位置滚轮仍滚动宿主页;不透明场景:iframe 铺满,该区域内滚轮 = 缩放,**会吞掉该区域的页面滚动**。`false` 锁定(`?controls=0`) |
374
383
  | `autoPause` | `true` | 滚出视口自动暂停 |
@@ -380,7 +389,7 @@ createXiaochun({
380
389
 
381
390
  **实例**:`ready` · `say(text, { mode: 'speak' \| 'chat' })` · `speakAudio(source, opts)` · `speakAudioStream(opts)` · `motion(nameOrUrlOrOptions)` · `expression(name)` · `setOutfit(id)` · `setScene(id)` · `getOutfits()` · `getScenes()` · `prefetch(ids?)` · `outfit` / `scene`(只读)· `setModel(outfitOrUrl)` *(旧)* · `setConfig(cfg)` · `setSize(w, h)` · `getBox()` · `setDraggable(on)` · `setResizable(on | limits)` · `startListening()` / `stopListening()` / `mic(on)` · `pause()` / `resume()` · `activate()` · `destroy()` · `on(event, cb)` · `lookAt()` *(协议已预留,目前返回 `unsupported`)*。
382
391
 
383
- **事件**:`handshake` · `ready` · `progress` · `state` · `stt` · `utterance`(`phase: 'start' | 'end'`,`kind: 'text' | 'audio'`)· `hit-region` · `outfit-changed` · `scene-changed` · `move` / `resize`(`{ phase: 'start' | 'move' | 'end', left, top, width, height }`)· `error`(新增 `busy`、`unknown_id`;旧 iframe 上开手势会收到一次 `unsupported`)· `destroy`。`progress.phase` 为 `'model' | 'outfit' | 'prefetch'`。
392
+ **事件**:`handshake` · `ready` · `progress` · `state` · `stt` · `utterance`(`phase: 'start' | 'end'`,`kind: 'text' | 'audio'`)· `hit-region` · `outfit-changed` · `scene-changed` · `lang-changed`(`{ lang, previous?, initial? }`)· `move` / `resize`(`{ phase: 'start' | 'move' | 'end', left, top, width, height }`)· `error`(新增 `busy`、`unknown_id`;旧 iframe 上开手势会收到一次 `unsupported`)· `destroy`。`progress.phase` 为 `'model' | 'outfit' | 'prefetch'`。
384
393
 
385
394
  ### `<xiaochun-avatar>`
386
395
 
@@ -390,8 +399,9 @@ createXiaochun({
390
399
  | `outfit` | — | 服装 id;运行时修改 = `setOutfit`(**热更新,不重建 iframe**) |
391
400
  | `scene` | — | `light` · `dark` · `transparent`;运行时修改 = `setScene`(热更新) |
392
401
  | `model` | — | `outfit` 的**已弃用**别名(`outfit` 优先) |
393
- | `persist` / `prefetch` / `allow-custom-model` | — | 同对应选项;修改会重建 |
394
- | `lang` | — | `zh-CN` · `en` · `ja`;运行时修改 = `setConfig` |
402
+ | `camera-fov` / `camera-distance` / `camera-height` / `camera-intro` | — | 同 `camera`(热更新 `setConfig({ camera })`,不重建;去掉属性 = 恢复默认) |
403
+ | `persist` / `persist-box` / `prefetch` / `allow-custom-model` | — | 同对应选项(`persist-box=""` = 默认 key,其它字符串 = 命名空间);修改会重建 |
404
+ | `lang` | 自动 | `zh-CN` · `en` · `ja`;不写 = 用户上次的选择 > 浏览器语言 > `zh-CN`;运行时修改 = `setConfig`(不持久化) |
395
405
  | `mic` | `false` | 开关听写(模型加载完成后生效) |
396
406
  | `transparent` | `true` | `"false"` 关闭 |
397
407
  | `draggable` | `false` | 手势拖动(内联 / 悬浮都行);运行时修改 = `setDraggable`(热更新) |
@@ -399,15 +409,16 @@ createXiaochun({
399
409
  | `border-radius` | 非透明 `20px` / 透明 `0` | 外壳圆角(数字 = px 或 CSS 长度);运行时修改 = `setBorderRadius`(热更新) |
400
410
  | `min-size` / `max-size` | — | 缩放限幅,格式同 `size`(如 `min-size="160x240"`) |
401
411
  | `position` | `inline` | `inline` · `bottom-right` · `bottom-left` |
402
- | `size` | `320x480` | `"280"`(高 = 宽 × 1.5)、`"320x480"`、`"100%x480px"`;运行时修改 = `setSize`(**热更新,不重建 iframe**) |
412
+ | `size` | `600x1080`(受视口限制) | `"280"`(高 = 宽 × 1.5)、`"320x480"`、`"100%x480px"`;运行时修改 = `setSize`(**热更新,不重建 iframe**) |
403
413
  | `lazy` | 空闲 + 视口 | `"click"` 仅点击;`"false"` 立即创建 |
404
414
  | `paused` | `false` | `pause()` / `resume()` |
405
- | `ui` | — | 逗号分隔的部件名,如 `ui="outfit,scene"`(不写 = 都不显示;未知项忽略并 warn)。改它会重建;运行时用 `setConfig({ ui })` |
415
+ | `ui` | — | 逗号分隔的部件名,如 `ui="outfit,scene,lang,github"`(不写 = 都不显示;未知项忽略并 warn)。改它会重建;运行时用 `setConfig({ ui })` |
416
+ | `ui-autohide` | `transparent` | `"transparent"`(默认,仅透明场景点击才出现)· `"true"`(所有场景)· `"false"`(一直显示)。改它会重建;运行时用 `setConfig({ uiAutoHide })` |
406
417
  | `controls` | 开 | `controls="false"` 锁定 iframe 内滚轮缩放 |
407
418
  | `placeholder` / `heavy` / `allowed-origins` | — | 同 `createXiaochun` |
408
419
  | `cross-origin-isolated` | `false` | 同 `createXiaochun({ crossOriginIsolated })`;修改会重建 iframe |
409
420
 
410
- **事件**(`CustomEvent`,`composed`,`detail` = 协议 payload):`xc-ready` · `xc-progress` · `xc-state` · `xc-stt` · `xc-utterance` · `xc-outfit-changed` · `xc-scene-changed` · `xc-move` · `xc-resize` · `xc-error`。
421
+ **事件**(`CustomEvent`,`composed`,`detail` = 协议 payload):`xc-ready` · `xc-progress` · `xc-state` · `xc-stt` · `xc-utterance` · `xc-outfit-changed` · `xc-scene-changed` · `xc-lang-changed` · `xc-move` · `xc-resize` · `xc-error`。
411
422
  **方法**:`say` · `speakAudio` · `speakAudioStream` · `motion` · `expression` · `setOutfit` · `setScene` · `getOutfits` · `getScenes` · `prefetch` · `destroy`;`el.client` 可拿到完整的 SDK 实例。
412
423
 
413
424
  ---
@@ -433,7 +444,8 @@ xc.setSize(240, 360); // 程序化改尺寸(同样热更新)
433
444
  * 透明场景 + 穿透:只有角色、(开了 `resizable` 时)四角 40px 热区会接管指针,其余位置仍穿透到你的页面;内置按钮不会成为拖动 / 缩放起点。
434
445
  * 拖动时的选中蓝框两侧都已抑制(`user-select: none`、阻止 `selectstart` / `dragstart`、缩放时用 pointer capture)。
435
446
  * 旧版 `/embed`(没有 `capabilities.gestures`)上开内联 `draggable` 或 `resizable`,会触发一次 `error { code: 'unsupported', command: 'gestures' }`。
436
- * 试玩:`examples/embed-host.html?draggable=1&resizable=1`。
447
+ * **记住位置和大小(`persistBox`,默认关)**:开了 `draggable` / `resizable` 后,`persistBox: true`(或命名空间字符串 → key `xiaochun:box:<名字>`)会在每次拖动 / 缩放**结束**时把盒子存进**宿主页**的 localStorage(读写都包了 try/catch,隐私模式 / 配额满只是不记忆),下次创建时恢复。优先级:显式 `width` / `height` > 已保存 > 默认(600×1080);位置没有显式选项(`position` 只是预设锚点),所以已保存的位置始终生效。**想恢复用户缩放后的大小,就不要传 `width` / `height`。** 恢复时钳制到*当前*视口(大小夹在 `[最小, 视口]`,最小 120×180 或你的 `resizable` 限幅;悬浮盒子整块拉回屏幕内;内联盒子横向夹进视口、纵向不越过文档顶部)。保存时的 `position` 模式与当前不同、或数据损坏,都会被忽略并清掉。只在开了手势时恢复。`xc.clearPersistedBox()` 删除已保存的值;`xc.clearPersistedBox({ reset: true })` 还会把盒子还原到初始位置 / 大小。元素属性 `persist-box`(`""` / `"true"` / 命名空间),React `persistBox` prop + `ref.clearPersistedBox()`。
448
+ * 试玩:`examples/embed-host.html?draggable=1&resizable=1&persistBox=1`。
437
449
 
438
450
  ## 🧪 本地试玩 (Try It Locally)
439
451
 
package/README.md CHANGED
@@ -152,12 +152,14 @@ await xc.speakAudio(arrayBuffer, { text: 'Hi', motion: true, lipsync: true }); /
152
152
  await xc.speakAudio(blob); // Blob (mp3 / wav / ogg …)
153
153
  await xc.speakAudio('https://cdn.example.com/voice.mp3'); // URL: fetched by the host page by default ({ fetch: 'frame' } = fetched inside the iframe)
154
154
  await xc.speakAudio(pcm, { format: 'pcm16', sampleRate: 24000 }); // headerless PCM needs sampleRate (8000–96000)
155
+ await xc.speakAudio(pcm, { format: 'pcm16', sampleRate: 16000, audible: false }); // motion + lip-sync only; iframe gain is 0
155
156
 
156
157
  const s = xc.speakAudioStream({ sampleRate: 24000 }); // streaming: e.g. PCM chunks from a TTS server
157
158
  s.write(int16Chunk); s.write(next); s.end(); await s.done; // s.abort() stops immediately
158
159
  // Cancel any time: pass { signal: abortController.signal }
159
160
  ```
160
161
 
162
+ * `audible` defaults to `true`. `false` still decodes the audio, generates motion, and drives lip-sync, but the iframe gain is 0 so the host page can be the only speaker.
161
163
  * Rejects with `bad_request` (undecodable / empty audio, bad URL or sampleRate), `unsupported`, or `failed`.
162
164
  * Browsers still require a user gesture on the host page before audio can play, and the iframe needs `allow="autoplay"` (the SDK sets it).
163
165
  * A newer `say()` / `speakAudio()` call preempts the one in progress, and the preempted promise resolves.
@@ -187,22 +189,24 @@ xc.on('scene-changed', (p) => console.log(p.id));
187
189
  * **Preferences**: the iframe remembers the user's last outfit/scene in **its own** localStorage (`xiaochun_wearing_outfit` / `xiaochun_scene_theme`, the same keys as the main site). Priority: explicit `outfit` / `scene` (URL or SDK options) > saved > default; unknown saved ids are ignored and cleared; if storage is blocked or partitioned it silently falls back to the defaults. Third-party storage is partitioned per top-level site, so each host site has its own copy.
188
190
  * **`persist`** (optional) additionally keeps them in the host page's localStorage and passes them back as explicit values, so it overrides the iframe's own copy. Default `false`.
189
191
  * **`prefetch`** (off by default) downloads outfit files into the iframe's IndexedDB only (no decode/compose), serially, after EMAGE is loaded, never the wedding outfit unless you name it. `await xc.prefetch(['xiaochun_cheongsam'])` works in any mode; the automatic variant needs `heavy: 'eager'`. Skipped when the user enabled Data Saver. A later `setOutfit` then skips the network (the ~0.9 s decode/compose remains).
190
- * **Built-in buttons**: see the next section (`ui: ['outfit', 'scene']`).
192
+ * **Built-in buttons**: see the next section (`ui: ['outfit', 'scene', 'lang', 'github']`, `uiAutoHide`).
191
193
  * **Custom models**: `setModel({ url })` is **off by default**; pass `allowCustomModel: true` to opt in.
192
194
  * **Migrating**: option `model` → `outfit`; the legacy `setModel('base')` no longer works (use `getOutfits()` for the list).
193
195
 
194
- ### 🔘 Built-in outfit / scene buttons (`ui`)
196
+ ### 🔘 Built-in buttons (`ui`, `uiAutoHide`)
195
197
 
196
198
  `ui` is an **array of part names**; nothing is shown unless you list it.
197
199
 
198
200
  ```ts
199
- createXiaochun({ container: '#avatar', ui: ['outfit', 'scene'] }); // or ['chat', 'bubble', 'outfit', 'scene']
201
+ createXiaochun({ container: '#avatar', ui: ['outfit', 'scene', 'lang', 'github'] }); // parts: chat · bubble · outfit · scene · lang · github
200
202
  ```
201
203
  ```html
202
- <xiaochun-avatar ui="outfit,scene"></xiaochun-avatar> <!-- URL form: /embed?ui=chat,outfit,scene -->
204
+ <xiaochun-avatar ui="outfit,scene,lang,github" ui-autohide="false"></xiaochun-avatar> <!-- URL form: /embed?ui=outfit,scene,lang,github&uiAutoHide=false -->
203
205
  ```
204
206
  React: `<Xiaochun ui={['outfit', 'scene']} />`. At runtime: `xc.setConfig({ ui: ['outfit'] })`.
205
207
 
208
+ * **When it shows (`uiAutoHide`, same as the desktop app)**: default `'transparent'` — in the **transparent** scene the buttons and chat bar start hidden and a **single click on the character** shows them (click the character again, or empty space, to hide; they also hide after 10 s idle, and stay while you hover them or a menu is open); in light / dark scenes they are always visible. `true` extends click-to-show to every scene, `false` keeps them always visible. Dragging the iframe (`draggable`) never counts as a click (≤ 6 px movement). Limits: in the transparent scene a click on blank host-page space never reaches the iframe (pass-through), so hiding relies on the 10 s timeout or clicking the character; on touch the first tap only wakes hit detection. Needs an iframe with `capabilities.ui.autoHide`; older iframes ignore it and always show.
209
+ * **Language / GitHub buttons**: `lang` opens a menu (简体中文 / English / 日本語); the choice applies immediately, is saved in the **iframe's own** localStorage (`xiaochun_embed_lang`) and fires `lang-changed` (`{ lang, previous?, initial? }`; one `initial: true` after the handshake). Only the user's own pick is saved — an explicit `lang` option and `setConfig({ lang })` are not. `github` is an `<a target="_blank" rel="noopener noreferrer">` to the project repo.
206
210
  * **Same components as the main site's TopHeader**: glass buttons (44×44 on touch, 36×36 on desktop), the dropdown menu, the `Shirt` / `MountainSnow` / `Check` / `Loader2` icons and the same i18n strings (follows `lang`). Placed top-right so the character stays uncovered. The list comes from `capabilities.outfits` (no bare model, no custom URLs) and shows just the outfit names (no file sizes in the menu; `capabilities.outfits[].sizeMB` is still provided for your own UI), a spinner on the loading row and a ✓ on the worn one.
207
211
  * **Same path as `setOutfit` / `setScene`**: same whitelist, same serial last-wins queue, a light "busy" hint when you click fast, and the usual `outfit-changed` / `scene-changed` events. Calls you make through the SDK keep the buttons in sync. Clicking a button never triggers the body-turn / drag gestures.
208
212
  * **Transparent scene**: the buttons take part in the pass-through hit test (clicks on them do not fall through to your page; blank space still does). On touch + transparent, the first tap only wakes hit detection (it lands on your page), the second tap reaches the button.
@@ -237,6 +241,8 @@ xiaochun-avatar::part(iframe) { outline: 1px solid #0002; }
237
241
 
238
242
  Parts: `mount` · `wrapper` · `iframe` · `placeholder`.
239
243
 
244
+ **No blue selection tint**: when a host-page text selection crosses the iframe (drag-select, Cmd/Ctrl+A), Chrome paints the whole iframe blue. The SDK sets `user-select: none` (inline CSSOM styles, no `<style>` injected, so host CSP is unaffected) on the shell, placeholder and iframe; your own text stays selectable and pointer-events / transparent pass-through are untouched. Don't override `user-select` on `::part(iframe)` back to `auto`.
245
+
240
246
  ---
241
247
 
242
248
  ## 🔌 `xiaochun://` and `xc.*`
@@ -357,18 +363,21 @@ createXiaochun({
357
363
  | `placeholder` | built-in SVG | Image URL, element, or `false` |
358
364
  | `transparent` | `false` | Transparent background over your page (with pointer pass-through). Same as `scene: 'transparent'` |
359
365
  | `scene` | — | Initial scene: `'light' \| 'dark' \| 'transparent'` (see `getScenes()`). Unknown id → ignored + `error { code: 'unknown_id' }` |
360
- | `width`, `height` | `320`, `480` | px or any CSS length. Always set them |
366
+ | `width`, `height` | `600`, `1080` | px or any CSS length. **Defaults are clamped to the viewport**: width = `min(600px, 100vw)` (and the shell has `max-width: 100%`, so it never overflows a narrow container), height = `min(1080px, 100svh)` (floating `position`s also subtract the `--xc-offset-x/y` margins so the box never sticks out of the screen). Explicit values are used as-is. Runtime: `setSize(w, h)` (`undefined` = back to the default) |
361
367
  | `position` | `'inline'` | `'inline' \| 'bottom-right' \| 'bottom-left'` |
362
368
  | `draggable` | `false` | Gesture drag: press and drag the character (not a built-in button) to move the iframe, clamped to the viewport. Works in inline and floating modes. See [Gestures](#gestures-drag-and-corner-resize) |
363
369
  | `borderRadius` | opaque `20px` / transparent `0` | Shell corner radius (number = px, or any CSS length). The default for light / dark scenes equals the desktop app's window radius (20 px); transparent scenes are not clipped. Live via `setBorderRadius()`; the `--xc-radius` CSS variable wins; `0` = square corners |
364
370
  | `resizable` | `false` | Drag a corner (same 40 px hot zone / cursors / arcs as the desktop app) to resize the iframe at runtime, **without rebuilding it**. `true` or `{ minWidth, minHeight, maxWidth, maxHeight }` (default min 120×180, max = viewport) |
365
- | `lang` | — | `'zh-CN' \| 'en' \| 'ja'` |
371
+ | `lang` | auto | `'zh-CN' \| 'en' \| 'ja'`. Priority: this option > the language the user last picked in the built-in language button (saved in the iframe's own localStorage `xiaochun_embed_lang`) > browser language > `zh-CN`. `setConfig({ lang })` at runtime is not persisted. Changes fire `lang-changed` |
366
372
  | `outfit` | default outfit | Initial outfit id (e.g. `xiaochun_maid`, see `getOutfits()`). Unknown id → default outfit + `error { code: 'unknown_id' }` |
367
373
  | `model` | — | **Deprecated**, use `outfit` (a `console.warn` is printed). An https URL is no longer accepted here; see `allowCustomModel` |
368
374
  | `allowCustomModel` | `false` | Opt in to `setModel({ url })` for arbitrary https `.vrm` / `.vrmaddon` / `.vrmbase`. Third-party files are parsed inside the iframe, so enable it only for URLs you trust |
375
+ | `camera` | — | Camera framing `{ fov, distance, height, intro }`: `fov` 15–60° (default 30; distance auto-compensates so the character keeps its size), `distance` 1–15 m (default ≈2.5; overrides the iframe's saved zoom/pitch on first fit), `height` ±1 m framing offset (not saved by the iframe), `intro: false` skips the dolly-in. Out-of-range values are clamped; there is no `pitch` (the camera only tilts up/down around the character, left/right is the character's body turn). Sent as `?cameraFov=` etc. at create time; at runtime use `setConfig({ camera })` (omitted key = unchanged, `null` = back to default; re-frames immediately and cancels a running dolly-in). Old `/embed` (no `capabilities.camera`) ignores it. Priority: explicit > the iframe's saved view > default. |
376
+ | `persistBox` | `false` | Remember the iframe's position & size after drag / resize in **your page's** localStorage and restore it on the next create: `true` (key `xiaochun:box`) or a namespace string (`xiaochun:box:<name>`). Needs `draggable` / `resizable`. Priority: explicit `width` / `height` > saved > default; restore is clamped to the current viewport. `clearPersistedBox({ reset? })` clears it. See [Gestures](#gestures-drag-and-corner-resize) |
369
377
  | `persist` | `false` | Optionally also remember outfit + scene in **the host page's** localStorage (the iframe already keeps its own copy): `false` \| `'host'` (`xiaochun:prefs`) \| a custom key. Explicit `outfit` / `scene` options win over saved values; the saved copy wins over the iframe's own |
370
378
  | `prefetch` | `false` | `true` (all outfits except the 13.9 MB wedding) or `string[]`. Auto-sent once after the first load, **only with `heavy: 'eager'`**; otherwise call `prefetch()` yourself |
371
- | `ui` | `[]` | Built-in UI parts to show inside the iframe: an array of `'chat'` (chat bar) · `'bubble'` (speech bubble) · `'outfit'` (outfit button) · `'scene'` (scene button). Unset / empty = none. Unknown names are ignored with a `console.warn`. See "Built-in outfit / scene buttons" below |
379
+ | `ui` | `[]` | Built-in UI parts to show inside the iframe: an array of `'chat'` (chat bar) · `'bubble'` (speech bubble) · `'outfit'` (outfit button) · `'scene'` (scene button) · `'lang'` (language switcher) · `'github'` (GitHub link, opens in a new tab). Unset / empty = none. Unknown names are ignored with a `console.warn`. See "Built-in buttons" below |
380
+ | `uiAutoHide` | `'transparent'` | When the built-in UI is visible, **same as the desktop app**: `'transparent'` = only the transparent (desk-pet) scene hides it until you click the character; light / dark scenes always show it. `true` = click-to-show in every scene; `false` = always visible (the previous behaviour). Live via `setConfig({ uiAutoHide })` |
372
381
  | `heavy` | `'lazy'` | `'lazy'`: load WebLLM / EMAGE on first use · `'eager'`: preload |
373
382
  | `controls` | `true` | Wheel zoom inside the iframe, on by default like the main site. Transparent scene: zooms only while the pointer is on the character, everywhere else the wheel scrolls your page. Opaque scenes: the iframe fills its area, so the wheel zooms there and **swallows page scrolling over that area**. `false` locks it (`?controls=0`) |
374
383
  | `autoPause` | `true` | Pause when out of the viewport |
@@ -380,7 +389,7 @@ createXiaochun({
380
389
 
381
390
  **Instance**: `ready` · `say(text, { mode: 'speak' \| 'chat' })` · `speakAudio(source, opts)` · `speakAudioStream(opts)` · `motion(nameOrUrlOrOptions)` · `expression(name)` · `setOutfit(id)` · `setScene(id)` · `getOutfits()` · `getScenes()` · `prefetch(ids?)` · `outfit` / `scene` (getters) · `setModel(outfitOrUrl)` *(legacy)* · `setConfig(cfg)` · `setSize(w, h)` · `getBox()` · `setDraggable(on)` · `setResizable(on | limits)` · `startListening()` / `stopListening()` / `mic(on)` · `pause()` / `resume()` · `activate()` · `destroy()` · `on(event, cb)` · `lookAt()` *(reserved in the protocol; currently returns `unsupported`)*.
382
391
 
383
- **Events**: `handshake` · `ready` · `progress` · `state` · `stt` · `utterance` (`phase: 'start' | 'end'`, `kind: 'text' | 'audio'`) · `hit-region` · `outfit-changed` · `scene-changed` · `move` / `resize` (`{ phase: 'start' | 'move' | 'end', left, top, width, height }`) · `error` (now also `busy`, `unknown_id`; one `unsupported` if gestures are enabled on an old iframe) · `destroy`. `progress.phase` is `'model' | 'outfit' | 'prefetch'`.
392
+ **Events**: `handshake` · `ready` · `progress` · `state` · `stt` · `utterance` (`phase: 'start' | 'end'`, `kind: 'text' | 'audio'`) · `hit-region` · `outfit-changed` · `scene-changed` · `lang-changed` (`{ lang, previous?, initial? }`) · `move` / `resize` (`{ phase: 'start' | 'move' | 'end', left, top, width, height }`) · `error` (now also `busy`, `unknown_id`; one `unsupported` if gestures are enabled on an old iframe) · `destroy`. `progress.phase` is `'model' | 'outfit' | 'prefetch'`.
384
393
 
385
394
  ### `<xiaochun-avatar>`
386
395
 
@@ -390,8 +399,9 @@ createXiaochun({
390
399
  | `outfit` | — | Outfit id; changing it at runtime = `setOutfit` (**live update, no iframe rebuild**) |
391
400
  | `scene` | — | `light` · `dark` · `transparent`; runtime change = `setScene` (live) |
392
401
  | `model` | — | **Deprecated** alias of `outfit` (`outfit` wins) |
393
- | `persist` / `prefetch` / `allow-custom-model` | — | Same as the options; changing them rebuilds |
394
- | `lang` | — | `zh-CN` · `en` · `ja`; runtime change = `setConfig` |
402
+ | `camera-fov` / `camera-distance` / `camera-height` / `camera-intro` | — | Same as `camera` (hot-updated via `setConfig({ camera })`, no rebuild; removing an attribute = default) |
403
+ | `persist` / `persist-box` / `prefetch` / `allow-custom-model` | — | Same as the options (`persist-box=""` = default key, any other string = namespace); changing them rebuilds |
404
+ | `lang` | auto | `zh-CN` · `en` · `ja`; unset = the user's saved pick > browser language > `zh-CN`; runtime change = `setConfig` (not persisted) |
395
405
  | `mic` | `false` | Toggle dictation (takes effect once the model is loaded) |
396
406
  | `transparent` | `true` | `"false"` turns it off |
397
407
  | `draggable` | `false` | Gesture drag (inline and floating). Runtime change = `setDraggable` (live) |
@@ -399,15 +409,16 @@ createXiaochun({
399
409
  | `border-radius` | opaque `20px` / transparent `0` | Shell corner radius (number = px or CSS length); runtime change = `setBorderRadius` (live) |
400
410
  | `min-size` / `max-size` | — | Resize limits, same format as `size` (e.g. `min-size="160x240"`) |
401
411
  | `position` | `inline` | `inline` · `bottom-right` · `bottom-left` |
402
- | `size` | `320x480` | `"280"` (height = width × 1.5), `"320x480"`, `"100%x480px"`; runtime change = `setSize` (**live, no iframe rebuild**) |
412
+ | `size` | `600x1080` (viewport-clamped) | `"280"` (height = width × 1.5), `"320x480"`, `"100%x480px"`; runtime change = `setSize` (**live, no iframe rebuild**) |
403
413
  | `lazy` | idle + viewport | `"click"` for click only; `"false"` for immediate |
404
414
  | `paused` | `false` | `pause()` / `resume()` |
405
- | `ui` | — | Comma-separated parts, e.g. `ui="outfit,scene"` (unset = none; unknown names ignored with a warn). Changing it rebuilds; use `setConfig({ ui })` at runtime |
415
+ | `ui` | — | Comma-separated parts, e.g. `ui="outfit,scene,lang,github"` (unset = none; unknown names ignored with a warn). Changing it rebuilds; use `setConfig({ ui })` at runtime |
416
+ | `ui-autohide` | `transparent` | `"transparent"` (default; click-to-show only in the transparent scene) · `"true"` (every scene) · `"false"` (always visible). Changing it rebuilds; use `setConfig({ uiAutoHide })` at runtime |
406
417
  | `controls` | on | `controls="false"` locks wheel zoom inside the iframe |
407
418
  | `placeholder` / `heavy` / `allowed-origins` | — | Same as `createXiaochun` |
408
419
  | `cross-origin-isolated` | `false` | Same as `createXiaochun({ crossOriginIsolated })`; changing it rebuilds the iframe |
409
420
 
410
- **Events** (`CustomEvent`, `composed`, `detail` = protocol payload): `xc-ready` · `xc-progress` · `xc-state` · `xc-stt` · `xc-utterance` · `xc-outfit-changed` · `xc-scene-changed` · `xc-move` · `xc-resize` · `xc-error`.
421
+ **Events** (`CustomEvent`, `composed`, `detail` = protocol payload): `xc-ready` · `xc-progress` · `xc-state` · `xc-stt` · `xc-utterance` · `xc-outfit-changed` · `xc-scene-changed` · `xc-lang-changed` · `xc-move` · `xc-resize` · `xc-error`.
411
422
  **Methods**: `say` · `speakAudio` · `speakAudioStream` · `motion` · `expression` · `setOutfit` · `setScene` · `getOutfits` · `getScenes` · `prefetch` · `destroy`; `el.client` gives you the full SDK instance.
412
423
 
413
424
  ---
@@ -433,7 +444,8 @@ xc.setSize(240, 360); // programmatic resize (also live)
433
444
  * Transparent scene + pass-through: only the character and (when `resizable`) the four 40 px corner zones take over the pointer; everywhere else still falls through to your page. Built-in buttons are never a drag / resize start.
434
445
  * The blue text-selection box during a drag is suppressed on both sides (`user-select: none`, `selectstart` / `dragstart` blocked, pointer capture while resizing).
435
446
  * Against an older `/embed` without `capabilities.gestures`, enabling inline `draggable` or `resizable` emits one `error { code: 'unsupported', command: 'gestures' }`.
436
- * Try it: `examples/embed-host.html?draggable=1&resizable=1`.
447
+ * **Remember position & size (`persistBox`, off by default)**: with `draggable` / `resizable` on, `persistBox: true` (or a namespace string → key `xiaochun:box:<name>`) saves the box to **your page's** localStorage when a drag / resize **ends** (all reads / writes are try/catch'd — private mode or a full quota just means "not remembered") and restores it the next time the instance is created. Priority: explicit `width` / `height` > saved > default (600×1080); position has no explicit option (`position` is only an anchor preset), so a saved position always wins. **To get a user-resized size back, don't pass `width` / `height`.** Restoring is clamped to the *current* viewport (size within `[min, viewport]`, min 120×180 or your `resizable` limits; floating boxes pulled fully on-screen; inline boxes clamped horizontally and kept below the document top). A saved box whose `position` mode differs from the current one, or corrupt data, is ignored and removed. Only restored when a gesture is enabled. `xc.clearPersistedBox()` removes the saved value; `xc.clearPersistedBox({ reset: true })` also puts the box back to its initial place / size. Element: `persist-box` (`""` / `"true"` / a namespace), React: `persistBox` prop + `ref.clearPersistedBox()`.
448
+ * Try it: `examples/embed-host.html?draggable=1&resizable=1&persistBox=1`.
437
449
 
438
450
  ## 🧪 Try It Locally
439
451
 
@@ -1,23 +1,25 @@
1
1
  /**
2
2
  * avatar-element.ts — `<xiaochun-avatar>` Web Component (Shadow DOM 内包 iframe)。
3
3
  *
4
- * 属性: src outfit scene model(deprecated) lang mic transparent draggable resizable min-size max-size border-radius position size lazy paused placeholder heavy ui controls allowed-origins cross-origin-isolated persist allow-custom-model
4
+ * 属性: src outfit scene model(deprecated) lang mic transparent draggable resizable min-size max-size border-radius position size lazy paused placeholder heavy ui ui-autohide controls allowed-origins cross-origin-isolated persist allow-custom-model
5
5
  * - 布尔属性: 缺省取默认值; "" / "true" = true; "false" = false。
6
- * - size="320x480" 或 size="320" (高 = 宽 × 1.5); 也可写 CSS 长度 "100%x480px"。改 size **热更新** (setSize, 不重建 iframe)。
6
+ * - size="320x480" 或 size="320" (高 = 宽 × 1.5); 也可写 CSS 长度 "100%x480px"。缺省 = 600x1080 (宽不超过容器、高不超过视口)。改 size **热更新** (setSize, 不重建 iframe)。
7
7
  * - draggable: 手势拖动 (角色上按住拖, 内联 / 悬浮都行); resizable: 四角缩放热区 (对角固定, 不重建 iframe); 两者默认关, 改属性热更新。
8
8
  * min-size / max-size="120x180": resizable 的最小 / 最大尺寸 (px, 缺省最小 120x180、最大只受视口限制)。
9
9
  * - border-radius: 外壳圆角 (数字 = px 或 CSS 长度); 缺省: 非透明场景 20px (与 Tauri 桌宠窗口同值) / 透明 0; 改属性热更新 (setBorderRadius)。
10
10
  * - outfit: 内置服装 id (xiaochun_maid); 改属性**热更新** (setOutfit, 不重建 iframe)。model 是 outfit 的旧别名 (deprecated)。
11
11
  * - scene: light | dark | transparent; 改属性**热更新** (setScene, 不重建 iframe)。
12
- * - ui: 要显示的 iframe 内置界面部件, 逗号分隔: chat,bubble,outfit,scene (例 ui="outfit,scene"); 缺省 = 都不显示; 改它会重建。
12
+ * - ui: 要显示的 iframe 内置界面部件, 逗号分隔: chat,bubble,outfit,scene,lang,github (例 ui="outfit,scene,lang,github"); 缺省 = 都不显示; 改它会重建。
13
13
  * 旧写法 ui / ui="true" 已弃用 (= chat,bubble, 会 console.warn)。
14
+ * - ui-autohide: 内置界面的显示策略: 缺省 / "transparent" = 与 Tauri 一致 (只在透明场景点击出现, 亮暗场景常显); "true" = 所有场景点击出现; "false" = 一直显示。改它会重建。
15
+ * - persist-box: 记住用户拖动 / 缩放后的位置和大小 (宿主 localStorage): "" / "true" = 默认 key, 其它字符串 = 命名空间 (xiaochun:box:<名字>); 需配合 draggable / resizable; 显式 size 优先于已保存的大小。改它会重建。
14
16
  * - persist: "host" 或自定义 localStorage key; 另把服装/场景偏好存在宿主页 (可选; 默认不存, iframe 自己的 localStorage 仍会记住)。
15
17
  * - prefetch: "" / "true" = 预取全部内置服装 (婚纱除外), 或逗号分隔的 id 列表; 只在 heavy="eager" 时自动触发 (默认关)。
16
18
  * - allow-custom-model: 允许 setModel({url}) 加载任意 https 模型 (默认关闭)。
17
19
  * 样式 (CSS 自定义属性, 可写在 <xiaochun-avatar> 上或任意祖先上; 取值范围/效果见 client.ts 里的注释与 docs/EMBED.md §样式):
18
20
  * --xc-radius --xc-shadow --xc-z-index --xc-offset-x --xc-offset-y --xc-bg
19
21
  * 可用 ::part() 定制外壳: ::part(mount) ::part(wrapper) ::part(iframe) ::part(placeholder) (iframe 内部不可被宿主 CSS 影响)
20
- * 事件 (CustomEvent, composed, detail = 协议 payload): xc-ready(模型加载完) xc-progress xc-state xc-stt xc-utterance xc-error xc-outfit-changed xc-scene-changed
22
+ * 事件 (CustomEvent, composed, detail = 协议 payload): xc-ready(模型加载完) xc-progress xc-state xc-stt xc-utterance xc-error xc-outfit-changed xc-scene-changed xc-lang-changed
21
23
  * xc-move / xc-resize (用户拖动 / 缩放, detail = {phase, left, top, width, height})
22
24
  * 方法: say(text) speakAudio(source, opts) speakAudioStream(opts) motion(m) expression(name) setOutfit(id) setScene(id) getOutfits() getScenes() destroy() (+ client 属性拿到完整 SDK 实例)
23
25
  */
@@ -41,6 +43,10 @@ export interface XiaochunAvatarElement extends HTMLElement {
41
43
  setScene(id: string): Promise<void>;
42
44
  getOutfits(): Promise<XcOutfitInfo[]>;
43
45
  getScenes(): Promise<XcSceneInfo[]>;
46
+ /** 清除 persist-box 保存的位置 / 大小; { reset: true } 同时还原到初始位置和大小。 */
47
+ clearPersistedBox(opts?: {
48
+ reset?: boolean;
49
+ }): void;
44
50
  /** 预取服装资源到 iframe 的 IndexedDB; ids 省略 = 全部 (婚纱除外)。 */
45
51
  prefetch(ids?: string[]): Promise<XcPrefetchedPayload>;
46
52
  destroy(): void;
@@ -1,10 +1,11 @@
1
- /*! @firetable/project-xiaochun v0.1.15 | MIT | https://github.com/FireTable/project-xiaochun */
1
+ /*! @firetable/project-xiaochun v0.1.17 | MIT | https://github.com/FireTable/project-xiaochun */
2
2
  import {
3
3
  createXiaochun
4
- } from "./chunk-KQSKHPVF.js";
4
+ } from "./chunk-6EHOJ2CJ.js";
5
5
  import {
6
- isXcId
7
- } from "./chunk-KKRZ3SUD.js";
6
+ isXcId,
7
+ parseXcUiAutoHide
8
+ } from "./chunk-BJF55B44.js";
8
9
 
9
10
  // src/avatar-element.ts
10
11
  var OBSERVED = [
@@ -27,14 +28,20 @@ var OBSERVED = [
27
28
  "placeholder",
28
29
  "heavy",
29
30
  "ui",
31
+ "ui-autohide",
32
+ "camera-fov",
33
+ "camera-distance",
34
+ "camera-height",
35
+ "camera-intro",
30
36
  "controls",
31
37
  "allowed-origins",
32
38
  "cross-origin-isolated",
33
39
  "persist",
40
+ "persist-box",
34
41
  "allow-custom-model",
35
42
  "prefetch"
36
43
  ];
37
- var REBUILD = /* @__PURE__ */ new Set(["src", "position", "lazy", "placeholder", "heavy", "ui", "controls", "allowed-origins", "transparent", "cross-origin-isolated", "persist", "allow-custom-model", "prefetch"]);
44
+ 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"]);
38
45
  function radiusAttr(v) {
39
46
  if (v === null || v.trim() === "") return void 0;
40
47
  const t = v.trim();
@@ -50,22 +57,37 @@ function prefetchAttr(v) {
50
57
  if (v === "" || v === "true") return true;
51
58
  return v.split(",").map((s) => s.trim()).filter(Boolean);
52
59
  }
60
+ function persistBoxAttr(v) {
61
+ if (v === null || v === "false" || v === "0") return void 0;
62
+ if (v === "" || v === "true" || v === "1") return true;
63
+ return v;
64
+ }
53
65
  function uiAttr(v) {
54
66
  if (v === null || v === "false" || v === "0") return void 0;
55
67
  if (v === "" || v === "true" || v === "1") return true;
56
68
  return v.split(",").map((s) => s.trim()).filter(Boolean);
57
69
  }
70
+ function cameraAttrs(el) {
71
+ const num = (name) => {
72
+ const v = el.getAttribute(name);
73
+ if (v === null || v.trim() === "") return null;
74
+ const n = Number(v);
75
+ return Number.isFinite(n) ? n : null;
76
+ };
77
+ const i = el.getAttribute("camera-intro");
78
+ return { fov: num("camera-fov"), distance: num("camera-distance"), height: num("camera-height"), intro: i === null ? null : i !== "false" && i !== "0" };
79
+ }
58
80
  function parsePx(v) {
59
81
  const m = v?.trim().match(/^(\d+(?:\.\d+)?)\s*x\s*(\d+(?:\.\d+)?)$/i);
60
82
  return m ? { w: Number(m[1]), h: Number(m[2]) } : void 0;
61
83
  }
62
84
  function parseSize(v) {
63
- if (!v) return { width: "320px", height: "480px" };
85
+ if (!v) return { width: void 0, height: void 0 };
64
86
  const m = v.trim().split(/x/i);
65
87
  const px = (s) => /^\d+(\.\d+)?$/.test(s) ? `${s}px` : s;
66
88
  if (m.length >= 2) return { width: px(m[0]), height: px(m[1]) };
67
89
  const w = parseFloat(m[0]);
68
- return Number.isFinite(w) && /^\d+(\.\d+)?$/.test(m[0]) ? { width: `${w}px`, height: `${Math.round(w * 1.5)}px` } : { width: m[0], height: "480px" };
90
+ 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 };
69
91
  }
70
92
  function createElementClass() {
71
93
  class XiaochunAvatarElementImpl extends HTMLElement {
@@ -120,6 +142,8 @@ function createElementClass() {
120
142
  } else if (name === "border-radius") c.setBorderRadius(radiusAttr(newV));
121
143
  else if (name === "draggable") c.setDraggable(boolAttr(this, "draggable", false));
122
144
  else if (name === "resizable" || name === "min-size" || name === "max-size") c.setResizable(this.resizableOption());
145
+ else if (name.startsWith("camera-")) void c.setConfig({ camera: cameraAttrs(this) }).catch(() => {
146
+ });
123
147
  else if (name === "mic") void c.mic(boolAttr(this, "mic", false)).catch(() => {
124
148
  });
125
149
  else if (name === "paused") {
@@ -152,10 +176,13 @@ function createElementClass() {
152
176
  outfit: this.getAttribute("outfit") ?? this.getAttribute("model") ?? void 0,
153
177
  scene: this.getAttribute("scene") ?? void 0,
154
178
  persist: this.getAttribute("persist") || void 0,
179
+ persistBox: persistBoxAttr(this.getAttribute("persist-box")),
155
180
  allowCustomModel: boolAttr(this, "allow-custom-model", false),
156
181
  prefetch: prefetchAttr(this.getAttribute("prefetch")),
157
182
  heavy: this.getAttribute("heavy") ?? void 0,
158
183
  ui: uiAttr(this.getAttribute("ui")),
184
+ uiAutoHide: parseXcUiAutoHide(this.getAttribute("ui-autohide")),
185
+ camera: cameraAttrs(this),
159
186
  controls: boolAttr(this, "controls", true),
160
187
  crossOriginIsolated: boolAttr(this, "cross-origin-isolated", false)
161
188
  });
@@ -169,6 +196,7 @@ function createElementClass() {
169
196
  fwd("error", "xc-error");
170
197
  fwd("outfit-changed", "xc-outfit-changed");
171
198
  fwd("scene-changed", "xc-scene-changed");
199
+ fwd("lang-changed", "xc-lang-changed");
172
200
  fwd("move", "xc-move");
173
201
  fwd("resize", "xc-resize");
174
202
  if (boolAttr(this, "paused", false)) c.pause();
@@ -208,6 +236,9 @@ function createElementClass() {
208
236
  getScenes() {
209
237
  return this.requireClient().getScenes();
210
238
  }
239
+ clearPersistedBox(opts) {
240
+ this.client?.clearPersistedBox(opts);
241
+ }
211
242
  prefetch(ids) {
212
243
  return this.requireClient().prefetch(ids);
213
244
  }
@@ -236,4 +267,4 @@ export {
236
267
  XIAOCHUN_ELEMENT_TAG,
237
268
  defineXiaochunElement
238
269
  };
239
- //# sourceMappingURL=chunk-T6EHFEEZ.js.map
270
+ //# sourceMappingURL=chunk-4RT7LXB5.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/avatar-element.ts"],
4
+ "sourcesContent": ["/**\n * avatar-element.ts \u2014 `<xiaochun-avatar>` Web Component (Shadow DOM \u5185\u5305 iframe)\u3002\n *\n * \u5C5E\u6027: src outfit scene model(deprecated) lang mic transparent draggable resizable min-size max-size border-radius position size lazy paused placeholder heavy ui ui-autohide controls allowed-origins cross-origin-isolated persist allow-custom-model\n * - \u5E03\u5C14\u5C5E\u6027: \u7F3A\u7701\u53D6\u9ED8\u8BA4\u503C; \"\" / \"true\" = true; \"false\" = false\u3002\n * - size=\"320x480\" \u6216 size=\"320\" (\u9AD8 = \u5BBD \u00D7 1.5); \u4E5F\u53EF\u5199 CSS \u957F\u5EA6 \"100%x480px\"\u3002\u7F3A\u7701 = 600x1080 (\u5BBD\u4E0D\u8D85\u8FC7\u5BB9\u5668\u3001\u9AD8\u4E0D\u8D85\u8FC7\u89C6\u53E3)\u3002\u6539 size **\u70ED\u66F4\u65B0** (setSize, \u4E0D\u91CD\u5EFA iframe)\u3002\n * - draggable: \u624B\u52BF\u62D6\u52A8 (\u89D2\u8272\u4E0A\u6309\u4F4F\u62D6, \u5185\u8054 / \u60AC\u6D6E\u90FD\u884C); resizable: \u56DB\u89D2\u7F29\u653E\u70ED\u533A (\u5BF9\u89D2\u56FA\u5B9A, \u4E0D\u91CD\u5EFA iframe); \u4E24\u8005\u9ED8\u8BA4\u5173, \u6539\u5C5E\u6027\u70ED\u66F4\u65B0\u3002\n * min-size / max-size=\"120x180\": resizable \u7684\u6700\u5C0F / \u6700\u5927\u5C3A\u5BF8 (px, \u7F3A\u7701\u6700\u5C0F 120x180\u3001\u6700\u5927\u53EA\u53D7\u89C6\u53E3\u9650\u5236)\u3002\n * - border-radius: \u5916\u58F3\u5706\u89D2 (\u6570\u5B57 = px \u6216 CSS \u957F\u5EA6); \u7F3A\u7701: \u975E\u900F\u660E\u573A\u666F 20px (\u4E0E Tauri \u684C\u5BA0\u7A97\u53E3\u540C\u503C) / \u900F\u660E 0; \u6539\u5C5E\u6027\u70ED\u66F4\u65B0 (setBorderRadius)\u3002\n * - outfit: \u5185\u7F6E\u670D\u88C5 id (xiaochun_maid); \u6539\u5C5E\u6027**\u70ED\u66F4\u65B0** (setOutfit, \u4E0D\u91CD\u5EFA iframe)\u3002model \u662F outfit \u7684\u65E7\u522B\u540D (deprecated)\u3002\n * - scene: light | dark | transparent; \u6539\u5C5E\u6027**\u70ED\u66F4\u65B0** (setScene, \u4E0D\u91CD\u5EFA iframe)\u3002\n * - ui: \u8981\u663E\u793A\u7684 iframe \u5185\u7F6E\u754C\u9762\u90E8\u4EF6, \u9017\u53F7\u5206\u9694: chat,bubble,outfit,scene,lang,github (\u4F8B ui=\"outfit,scene,lang,github\"); \u7F3A\u7701 = \u90FD\u4E0D\u663E\u793A; \u6539\u5B83\u4F1A\u91CD\u5EFA\u3002\n * \u65E7\u5199\u6CD5 ui / ui=\"true\" \u5DF2\u5F03\u7528 (= chat,bubble, \u4F1A console.warn)\u3002\n * - ui-autohide: \u5185\u7F6E\u754C\u9762\u7684\u663E\u793A\u7B56\u7565: \u7F3A\u7701 / \"transparent\" = \u4E0E Tauri \u4E00\u81F4 (\u53EA\u5728\u900F\u660E\u573A\u666F\u70B9\u51FB\u51FA\u73B0, \u4EAE\u6697\u573A\u666F\u5E38\u663E); \"true\" = \u6240\u6709\u573A\u666F\u70B9\u51FB\u51FA\u73B0; \"false\" = \u4E00\u76F4\u663E\u793A\u3002\u6539\u5B83\u4F1A\u91CD\u5EFA\u3002\n * - persist-box: \u8BB0\u4F4F\u7528\u6237\u62D6\u52A8 / \u7F29\u653E\u540E\u7684\u4F4D\u7F6E\u548C\u5927\u5C0F (\u5BBF\u4E3B localStorage): \"\" / \"true\" = \u9ED8\u8BA4 key, \u5176\u5B83\u5B57\u7B26\u4E32 = \u547D\u540D\u7A7A\u95F4 (xiaochun:box:<\u540D\u5B57>); \u9700\u914D\u5408 draggable / resizable; \u663E\u5F0F size \u4F18\u5148\u4E8E\u5DF2\u4FDD\u5B58\u7684\u5927\u5C0F\u3002\u6539\u5B83\u4F1A\u91CD\u5EFA\u3002\n * - persist: \"host\" \u6216\u81EA\u5B9A\u4E49 localStorage key; \u53E6\u628A\u670D\u88C5/\u573A\u666F\u504F\u597D\u5B58\u5728\u5BBF\u4E3B\u9875 (\u53EF\u9009; \u9ED8\u8BA4\u4E0D\u5B58, iframe \u81EA\u5DF1\u7684 localStorage \u4ECD\u4F1A\u8BB0\u4F4F)\u3002\n * - prefetch: \"\" / \"true\" = \u9884\u53D6\u5168\u90E8\u5185\u7F6E\u670D\u88C5 (\u5A5A\u7EB1\u9664\u5916), \u6216\u9017\u53F7\u5206\u9694\u7684 id \u5217\u8868; \u53EA\u5728 heavy=\"eager\" \u65F6\u81EA\u52A8\u89E6\u53D1 (\u9ED8\u8BA4\u5173)\u3002\n * - allow-custom-model: \u5141\u8BB8 setModel({url}) \u52A0\u8F7D\u4EFB\u610F https \u6A21\u578B (\u9ED8\u8BA4\u5173\u95ED)\u3002\n * \u6837\u5F0F (CSS \u81EA\u5B9A\u4E49\u5C5E\u6027, \u53EF\u5199\u5728 <xiaochun-avatar> \u4E0A\u6216\u4EFB\u610F\u7956\u5148\u4E0A; \u53D6\u503C\u8303\u56F4/\u6548\u679C\u89C1 client.ts \u91CC\u7684\u6CE8\u91CA\u4E0E docs/EMBED.md \u00A7\u6837\u5F0F):\n * --xc-radius --xc-shadow --xc-z-index --xc-offset-x --xc-offset-y --xc-bg\n * \u53EF\u7528 ::part() \u5B9A\u5236\u5916\u58F3: ::part(mount) ::part(wrapper) ::part(iframe) ::part(placeholder) (iframe \u5185\u90E8\u4E0D\u53EF\u88AB\u5BBF\u4E3B CSS \u5F71\u54CD)\n * \u4E8B\u4EF6 (CustomEvent, composed, detail = \u534F\u8BAE payload): xc-ready(\u6A21\u578B\u52A0\u8F7D\u5B8C) xc-progress xc-state xc-stt xc-utterance xc-error xc-outfit-changed xc-scene-changed xc-lang-changed\n * xc-move / xc-resize (\u7528\u6237\u62D6\u52A8 / \u7F29\u653E, detail = {phase, left, top, width, height})\n * \u65B9\u6CD5: say(text) speakAudio(source, opts) speakAudioStream(opts) motion(m) expression(name) setOutfit(id) setScene(id) getOutfits() getScenes() destroy() (+ client \u5C5E\u6027\u62FF\u5230\u5B8C\u6574 SDK \u5B9E\u4F8B)\n */\nimport {\n createXiaochun,\n type XiaochunAudioOptions,\n type XiaochunAudioSource,\n type XiaochunAudioStream,\n type XiaochunInstance,\n type XiaochunLazy,\n type XiaochunPosition,\n type XiaochunResizeLimits,\n} from './client';\nimport { isXcId, parseXcUiAutoHide } from './protocol';\nimport type { XcCamera, XcUiPart, XcExpressionPayload, XcHeavyMode, XcLang, XcMotionPayload, XcOutfitInfo, XcPrefetchedPayload, XcSceneInfo } from './protocol';\n\nconst OBSERVED = [\n 'src', 'outfit', 'scene', 'model', 'lang', 'mic', 'transparent', 'draggable', 'resizable', 'min-size', 'max-size', 'border-radius', 'position', 'size', 'lazy',\n 'paused', 'placeholder', 'heavy', 'ui', 'ui-autohide', 'camera-fov', 'camera-distance', 'camera-height', 'camera-intro', 'controls', 'allowed-origins', 'cross-origin-isolated', 'persist', 'persist-box', 'allow-custom-model', 'prefetch',\n] as const;\n/** \u6539\u4E86\u8FD9\u4E9B\u8981\u91CD\u5EFA iframe; \u5176\u4F59 (outfit / scene / model / lang / mic / paused / size / draggable / resizable / min-size / max-size / border-radius / camera-*) \u53EF\u4EE5\u70ED\u66F4\u65B0\u3002 */\nconst REBUILD = new Set(['src', 'position', 'lazy', 'placeholder', 'heavy', 'ui', 'ui-autohide', 'controls', 'allowed-origins', 'transparent', 'cross-origin-isolated', 'persist', 'persist-box', 'allow-custom-model', 'prefetch']);\n\n/** border-radius \u5C5E\u6027: \u7EAF\u6570\u5B57 = px, \u5176\u4F59\u5F53 CSS \u957F\u5EA6; \u7F3A\u7701 = undefined (\u8D70 SDK \u9ED8\u8BA4: \u975E\u900F\u660E 20px / \u900F\u660E 0)\u3002 */\nfunction radiusAttr(v: string | null): number | string | undefined {\n if (v === null || v.trim() === '') return undefined;\n const t = v.trim();\n return /^\\d+(\\.\\d+)?$/.test(t) ? Number(t) : t;\n}\n\nfunction boolAttr(el: Element, name: string, dflt: boolean): boolean {\n const v = el.getAttribute(name);\n if (v === null) return dflt;\n return v !== 'false';\n}\n\n/** prefetch \u5C5E\u6027: \u7F3A\u7701/false = \u5173; \"\" / \"true\" = \u5168\u90E8 (\u5A5A\u7EB1\u9664\u5916); \u5176\u5B83 = \u9017\u53F7\u5206\u9694\u7684 id \u5217\u8868\u3002 */\nfunction prefetchAttr(v: string | null): boolean | string[] {\n if (v === null || v === 'false') return false;\n if (v === '' || v === 'true') return true;\n return v.split(',').map((s) => s.trim()).filter(Boolean);\n}\n\n/** persist-box \u5C5E\u6027: \"\" / \"true\" \u2192 true; \"false\" / \u7F3A\u7701 \u2192 \u4E0D\u8BB0; \u5176\u5B83\u5B57\u7B26\u4E32 = \u547D\u540D\u7A7A\u95F4\u3002 */\nfunction persistBoxAttr(v: string | null): boolean | string | undefined {\n if (v === null || v === 'false' || v === '0') return undefined;\n if (v === '' || v === 'true' || v === '1') return true;\n return v;\n}\n\n/** ui \u5C5E\u6027: \u7F3A\u7701 / \"false\" / \"0\" = \u4E0D\u663E\u793A; \u9017\u53F7\u5206\u9694\u7684\u90E8\u4EF6\u540D (chat,bubble,outfit,scene,lang,github); \"\" / \"true\" / \"1\" = \u5F03\u7528\u7684\u65E7\u5199\u6CD5 (client \u91CC warn)\u3002 */\nfunction uiAttr(v: string | null): XcUiPart[] | boolean | undefined {\n if (v === null || v === 'false' || v === '0') return undefined;\n if (v === '' || v === 'true' || v === '1') return true;\n return v.split(',').map((s) => s.trim()).filter(Boolean) as XcUiPart[];\n}\n\n/** camera-fov / camera-distance / camera-height / camera-intro \u5C5E\u6027 \u2192 XcCamera; \u6CA1\u5199 / \u975E\u6CD5\u7684\u9879 = null (\u521B\u5EFA\u65F6\u5FFD\u7565, \u70ED\u66F4\u65B0\u65F6 = \u6062\u590D\u9ED8\u8BA4)\u3002 */\nfunction cameraAttrs(el: Element): XcCamera {\n const num = (name: string): number | null => {\n const v = el.getAttribute(name);\n if (v === null || v.trim() === '') return null;\n const n = Number(v);\n return Number.isFinite(n) ? n : null;\n };\n const i = el.getAttribute('camera-intro');\n return { fov: num('camera-fov'), distance: num('camera-distance'), height: num('camera-height'), intro: i === null ? null : i !== 'false' && i !== '0' };\n}\n\n/** \"120x180\" \u2192 {w,h} (px \u6570\u5B57); \u975E\u6CD5 \u2192 undefined\u3002 */\nfunction parsePx(v: string | null): { w: number; h: number } | undefined {\n const m = v?.trim().match(/^(\\d+(?:\\.\\d+)?)\\s*x\\s*(\\d+(?:\\.\\d+)?)$/i);\n return m ? { w: Number(m[1]), h: Number(m[2]) } : undefined;\n}\n\n/** size \u5C5E\u6027 \u2192 \u5BBD\u9AD8; \u6CA1\u5199\u7684\u7EF4\u5EA6\u8FD4\u56DE undefined = \u7528 SDK \u9ED8\u8BA4 (600x1080, \u53D7\u89C6\u53E3\u9650\u5236)\u3002 */\nfunction parseSize(v: string | null): { width: string | undefined; height: string | undefined } {\n if (!v) return { width: undefined, height: undefined };\n const m = v.trim().split(/x/i);\n const px = (s: string) => (/^\\d+(\\.\\d+)?$/.test(s) ? `${s}px` : s);\n if (m.length >= 2) return { width: px(m[0]), height: px(m[1]) };\n const w = parseFloat(m[0]);\n return Number.isFinite(w) && /^\\d+(\\.\\d+)?$/.test(m[0]) ? { width: `${w}px`, height: `${Math.round(w * 1.5)}px` } : { width: m[0], height: undefined };\n}\n\n/** \u516C\u5F00\u7C7B\u578B (\u7C7B\u672C\u8EAB\u5728\u6D4F\u89C8\u5668\u91CC\u624D\u521B\u5EFA, \u8FD9\u6837 SSR / Node \u4E0B import \u672C\u5305\u4E0D\u4F1A\u56E0 HTMLElement \u7F3A\u5931\u800C\u629B\u9519)\u3002 */\nexport interface XiaochunAvatarElement extends HTMLElement {\n client: XiaochunInstance | null;\n say(text: string, opts?: { mode?: 'speak' | 'chat' }): Promise<void>;\n /** \u76F4\u63A5\u64AD\u653E\u5BBF\u4E3B\u7ED9\u7684\u97F3\u9891 (\u4E0D\u8D70 TTS), \u540C\u65F6\u7531 EMAGE \u751F\u6210\u52A8\u4F5C + \u53E3\u578B\u3002 */\n speakAudio(source: XiaochunAudioSource, opts?: XiaochunAudioOptions): Promise<void>;\n speakAudioStream(opts: XiaochunAudioOptions & { sampleRate: number }): XiaochunAudioStream;\n motion(m: XcMotionPayload | string): Promise<void>;\n expression(name: XcExpressionPayload['name']): Promise<void>;\n /** \u6362\u5185\u7F6E\u670D\u88C5 (\u4E32\u884C + last-wins, \u89C1 XiaochunInstance.setOutfit)\u3002 */\n setOutfit(id: string): Promise<void>;\n setScene(id: string): Promise<void>;\n getOutfits(): Promise<XcOutfitInfo[]>;\n getScenes(): Promise<XcSceneInfo[]>;\n /** \u6E05\u9664 persist-box \u4FDD\u5B58\u7684\u4F4D\u7F6E / \u5927\u5C0F; { reset: true } \u540C\u65F6\u8FD8\u539F\u5230\u521D\u59CB\u4F4D\u7F6E\u548C\u5927\u5C0F\u3002 */\n clearPersistedBox(opts?: { reset?: boolean }): void;\n /** \u9884\u53D6\u670D\u88C5\u8D44\u6E90\u5230 iframe \u7684 IndexedDB; ids \u7701\u7565 = \u5168\u90E8 (\u5A5A\u7EB1\u9664\u5916)\u3002 */\n prefetch(ids?: string[]): Promise<XcPrefetchedPayload>;\n destroy(): void;\n}\n\nfunction createElementClass(): CustomElementConstructor {\n class XiaochunAvatarElementImpl extends HTMLElement {\n static get observedAttributes() { return [...OBSERVED]; }\n\n /** \u5E95\u5C42 SDK \u5B9E\u4F8B (\u8FDE\u63A5 DOM \u540E\u53EF\u7528)\u3002 */\n client: XiaochunInstance | null = null;\n private mountEl: HTMLDivElement | null = null;\n private rebuildScheduled = false;\n\n connectedCallback(): void {\n if (!this.shadowRoot) {\n const root = this.attachShadow({ mode: 'open' });\n // :host \u9ED8\u8BA4 inline-block + max-width:100% (\u914D\u5408 SDK \u7684\u56FA\u5B9A\u5BBD\u9AD8, \u4E0D\u4F1A\u6491\u7834\u7A84\u5C4F\u5BB9\u5668)\u3002\n // \u60AC\u6D6E (position=bottom-right|bottom-left) \u65F6 SDK \u5185\u90E8\u7528 position:fixed, \u4E0D\u53D7 :host \u76D2\u6A21\u578B\u5F71\u54CD\u3002\n root.innerHTML = '<style>:host{display:inline-block;max-width:100%}:host([hidden]){display:none}</style><div part=\"mount\"></div>';\n }\n this.mountEl = this.shadowRoot!.querySelector('div');\n this.build();\n }\n\n disconnectedCallback(): void {\n this.destroy();\n }\n\n attributeChangedCallback(name: string, oldV: string | null, newV: string | null): void {\n if (!this.client || oldV === newV) return;\n if (REBUILD.has(name)) {\n if (!this.rebuildScheduled) {\n this.rebuildScheduled = true;\n queueMicrotask(() => { this.rebuildScheduled = false; if (this.isConnected) { this.destroy(); this.build(); } });\n }\n return;\n }\n const c = this.client;\n // \u70ED\u66F4\u65B0: \u5931\u8D25 (busy / unknown_id / unsupported) \u90FD\u4F1A\u4F5C\u4E3A xc-error \u4E8B\u4EF6\u629B\u7ED9\u5BBF\u4E3B, \u8FD9\u91CC\u4E0D\u91CD\u590D\u5904\u7406\n if ((name === 'outfit' || name === 'model') && newV) {\n // outfit \u4E0E model(\u65E7\u522B\u540D) \u540C\u65F6\u5B58\u5728\u65F6 outfit \u4F18\u5148\n const want = name === 'model' && this.hasAttribute('outfit') ? null : newV;\n if (want) void (isXcId(want) ? c.setOutfit(want) : c.setModel(want)).catch(() => {}); // \u65E7\u7528\u6CD5 model=\"https://\u2026\" \u4ECD\u8D70 setModel\n }\n else if (name === 'scene' && newV) void c.setScene(newV).catch(() => {});\n else if (name === 'lang' && newV) void c.setConfig({ lang: newV as XcLang }).catch(() => {});\n else if (name === 'size') { const { width, height } = parseSize(newV); c.setSize(width, height); }\n else if (name === 'border-radius') c.setBorderRadius(radiusAttr(newV));\n else if (name === 'draggable') c.setDraggable(boolAttr(this, 'draggable', false));\n else if (name === 'resizable' || name === 'min-size' || name === 'max-size') c.setResizable(this.resizableOption());\n else if (name.startsWith('camera-')) void c.setConfig({ camera: cameraAttrs(this) }).catch(() => {});\n else if (name === 'mic') void c.mic(boolAttr(this, 'mic', false)).catch(() => {});\n else if (name === 'paused') { if (boolAttr(this, 'paused', false)) c.pause(); else c.resume(); }\n }\n\n private build(): void {\n if (!this.mountEl || this.client) return;\n const { width, height } = parseSize(this.getAttribute('size'));\n const placeholder = this.getAttribute('placeholder');\n const allowed = this.getAttribute('allowed-origins');\n const lazyAttr = this.getAttribute('lazy');\n const lazy: XiaochunLazy = lazyAttr === 'click' ? 'click' : lazyAttr === 'false' ? false : true;\n const c = createXiaochun({\n container: this.mountEl,\n src: this.getAttribute('src') || undefined,\n allowedOrigins: allowed ? allowed.split(',').map((s) => s.trim()).filter(Boolean) : undefined,\n lazy,\n placeholder: placeholder === 'none' ? false : placeholder || undefined,\n transparent: boolAttr(this, 'transparent', true), // \u60AC\u6D6E\u5934\u50CF\u9ED8\u8BA4\u900F\u660E\n width, height,\n position: (this.getAttribute('position') as XiaochunPosition | null) ?? 'inline',\n draggable: boolAttr(this, 'draggable', false),\n resizable: this.resizableOption(),\n borderRadius: radiusAttr(this.getAttribute('border-radius')),\n lang: (this.getAttribute('lang') as XcLang | null) ?? undefined,\n outfit: this.getAttribute('outfit') ?? this.getAttribute('model') ?? undefined,\n scene: this.getAttribute('scene') ?? undefined,\n persist: this.getAttribute('persist') || undefined,\n persistBox: persistBoxAttr(this.getAttribute('persist-box')),\n allowCustomModel: boolAttr(this, 'allow-custom-model', false),\n prefetch: prefetchAttr(this.getAttribute('prefetch')),\n heavy: (this.getAttribute('heavy') as XcHeavyMode | null) ?? undefined,\n ui: uiAttr(this.getAttribute('ui')),\n uiAutoHide: parseXcUiAutoHide(this.getAttribute('ui-autohide')),\n camera: cameraAttrs(this),\n controls: boolAttr(this, 'controls', true),\n crossOriginIsolated: boolAttr(this, 'cross-origin-isolated', false),\n });\n this.client = c;\n const fwd = (ev: string, name: string) => c.on(ev as 'ready', (detail) =>\n this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true })));\n fwd('ready', 'xc-ready');\n fwd('progress', 'xc-progress');\n fwd('state', 'xc-state');\n fwd('stt', 'xc-stt');\n fwd('utterance', 'xc-utterance');\n fwd('error', 'xc-error');\n fwd('outfit-changed', 'xc-outfit-changed');\n fwd('scene-changed', 'xc-scene-changed');\n fwd('lang-changed', 'xc-lang-changed');\n fwd('move', 'xc-move');\n fwd('resize', 'xc-resize');\n if (boolAttr(this, 'paused', false)) c.pause();\n if (boolAttr(this, 'mic', false)) void c.ready.then(() => c.mic(true)).catch(() => {});\n }\n\n /** resizable / min-size / max-size \u5C5E\u6027 \u2192 SDK \u7684 resizable \u9009\u9879 (\u7F3A\u7701 / \"false\" = \u5173)\u3002 */\n private resizableOption(): false | XiaochunResizeLimits {\n if (!boolAttr(this, 'resizable', false)) return false;\n const min = parsePx(this.getAttribute('min-size')), max = parsePx(this.getAttribute('max-size'));\n return { minWidth: min?.w, minHeight: min?.h, maxWidth: max?.w, maxHeight: max?.h };\n }\n\n say(text: string, opts?: { mode?: 'speak' | 'chat' }): Promise<void> {\n return this.requireClient().say(text, opts);\n }\n speakAudio(source: XiaochunAudioSource, opts?: XiaochunAudioOptions): Promise<void> {\n return this.requireClient().speakAudio(source, opts);\n }\n speakAudioStream(opts: XiaochunAudioOptions & { sampleRate: number }): XiaochunAudioStream {\n return this.requireClient().speakAudioStream(opts);\n }\n motion(m: XcMotionPayload | string): Promise<void> { return this.requireClient().motion(m); }\n expression(name: XcExpressionPayload['name']): Promise<void> { return this.requireClient().expression(name); }\n setOutfit(id: string): Promise<void> { return this.requireClient().setOutfit(id); }\n setScene(id: string): Promise<void> { return this.requireClient().setScene(id); }\n getOutfits(): Promise<XcOutfitInfo[]> { return this.requireClient().getOutfits(); }\n getScenes(): Promise<XcSceneInfo[]> { return this.requireClient().getScenes(); }\n clearPersistedBox(opts?: { reset?: boolean }): void { this.client?.clearPersistedBox(opts); }\n prefetch(ids?: string[]): Promise<XcPrefetchedPayload> { return this.requireClient().prefetch(ids); }\n\n destroy(): void {\n this.client?.destroy();\n this.client = null;\n }\n\n private requireClient(): XiaochunInstance {\n if (!this.client) throw new Error('<xiaochun-avatar> is not connected');\n return this.client;\n }\n }\n\n return XiaochunAvatarElementImpl;\n}\n\nexport const XIAOCHUN_ELEMENT_TAG = 'xiaochun-avatar';\n\n/** \u6CE8\u518C\u81EA\u5B9A\u4E49\u5143\u7D20 (\u5E42\u7B49)\u3002import '@firetable/project-xiaochun/element' \u4F1A\u81EA\u52A8\u8C03\u7528\u3002 */\nexport function defineXiaochunElement(tag: string = XIAOCHUN_ELEMENT_TAG): CustomElementConstructor | null {\n if (typeof customElements === 'undefined' || typeof HTMLElement === 'undefined') return null;\n const existing = customElements.get(tag);\n if (existing) return existing;\n const ctor = createElementClass();\n customElements.define(tag, ctor);\n return ctor;\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }