@my-life-buddies/cli 0.14.1 → 0.15.0

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.
Files changed (188) hide show
  1. package/README.md +63 -15
  2. package/dist/bin/buddy.js +28 -9
  3. package/dist/bin/buddy.js.map +2 -2
  4. package/dist/bin/core.js +1293 -603
  5. package/dist/bin/core.js.map +4 -4
  6. package/dist/bin/preview.js +43 -8
  7. package/dist/bin/preview.js.map +2 -2
  8. package/dist/web/app.js +11 -6
  9. package/dist/web/widgets.css +59 -28
  10. package/dist/web/widgets.js +230 -76
  11. package/package.json +3 -3
  12. package/resources/README.md +6 -50
  13. package/resources/buddy-creator/INSTALL.md +0 -25
  14. package/resources/buddy-creator/PATCHES.md +0 -53
  15. package/resources/buddy-creator/SKILL.md +0 -76
  16. package/resources/buddy-creator/THIRD_PARTY_NOTICES.md +0 -154
  17. package/resources/buddy-creator/agents/openai.yaml +0 -4
  18. package/resources/buddy-creator/assets/licenses/bail.txt +0 -26
  19. package/resources/buddy-creator/assets/licenses/ccount.txt +0 -26
  20. package/resources/buddy-creator/assets/licenses/character-entities-html4.txt +0 -26
  21. package/resources/buddy-creator/assets/licenses/character-entities-legacy.txt +0 -26
  22. package/resources/buddy-creator/assets/licenses/character-entities.txt +0 -26
  23. package/resources/buddy-creator/assets/licenses/character-reference-invalid.txt +0 -26
  24. package/resources/buddy-creator/assets/licenses/comma-separated-tokens.txt +0 -26
  25. package/resources/buddy-creator/assets/licenses/debug.txt +0 -24
  26. package/resources/buddy-creator/assets/licenses/decode-named-character-reference.txt +0 -26
  27. package/resources/buddy-creator/assets/licenses/dequal.txt +0 -25
  28. package/resources/buddy-creator/assets/licenses/devlop.txt +0 -26
  29. package/resources/buddy-creator/assets/licenses/escape-string-regexp.txt +0 -13
  30. package/resources/buddy-creator/assets/licenses/estree-util-is-identifier-name.txt +0 -26
  31. package/resources/buddy-creator/assets/licenses/extend.txt +0 -27
  32. package/resources/buddy-creator/assets/licenses/hast-util-to-jsx-runtime.txt +0 -26
  33. package/resources/buddy-creator/assets/licenses/hast-util-whitespace.txt +0 -26
  34. package/resources/buddy-creator/assets/licenses/html-url-attributes.txt +0 -25
  35. package/resources/buddy-creator/assets/licenses/inline-style-parser.txt +0 -13
  36. package/resources/buddy-creator/assets/licenses/is-alphabetical.txt +0 -26
  37. package/resources/buddy-creator/assets/licenses/is-alphanumerical.txt +0 -26
  38. package/resources/buddy-creator/assets/licenses/is-decimal.txt +0 -26
  39. package/resources/buddy-creator/assets/licenses/is-hexadecimal.txt +0 -26
  40. package/resources/buddy-creator/assets/licenses/is-plain-obj.txt +0 -13
  41. package/resources/buddy-creator/assets/licenses/longest-streak.txt +0 -26
  42. package/resources/buddy-creator/assets/licenses/markdown-table.txt +0 -26
  43. package/resources/buddy-creator/assets/licenses/mdast-util-find-and-replace.txt +0 -26
  44. package/resources/buddy-creator/assets/licenses/mdast-util-from-markdown.txt +0 -26
  45. package/resources/buddy-creator/assets/licenses/mdast-util-gfm-autolink-literal.txt +0 -26
  46. package/resources/buddy-creator/assets/licenses/mdast-util-gfm-footnote.txt +0 -26
  47. package/resources/buddy-creator/assets/licenses/mdast-util-gfm-strikethrough.txt +0 -26
  48. package/resources/buddy-creator/assets/licenses/mdast-util-gfm-table.txt +0 -26
  49. package/resources/buddy-creator/assets/licenses/mdast-util-gfm-task-list-item.txt +0 -26
  50. package/resources/buddy-creator/assets/licenses/mdast-util-gfm.txt +0 -26
  51. package/resources/buddy-creator/assets/licenses/mdast-util-mdx-expression.txt +0 -26
  52. package/resources/buddy-creator/assets/licenses/mdast-util-mdx-jsx.txt +0 -26
  53. package/resources/buddy-creator/assets/licenses/mdast-util-mdxjs-esm.txt +0 -26
  54. package/resources/buddy-creator/assets/licenses/mdast-util-phrasing.txt +0 -27
  55. package/resources/buddy-creator/assets/licenses/mdast-util-to-hast.txt +0 -26
  56. package/resources/buddy-creator/assets/licenses/mdast-util-to-markdown.txt +0 -26
  57. package/resources/buddy-creator/assets/licenses/mdast-util-to-string.txt +0 -26
  58. package/resources/buddy-creator/assets/licenses/micromark-core-commonmark.txt +0 -26
  59. package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-autolink-literal.txt +0 -26
  60. package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-footnote.txt +0 -26
  61. package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-strikethrough.txt +0 -26
  62. package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-table.txt +0 -26
  63. package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-tagfilter.txt +0 -26
  64. package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-task-list-item.txt +0 -26
  65. package/resources/buddy-creator/assets/licenses/micromark-extension-gfm.txt +0 -26
  66. package/resources/buddy-creator/assets/licenses/micromark-factory-destination.txt +0 -26
  67. package/resources/buddy-creator/assets/licenses/micromark-factory-label.txt +0 -26
  68. package/resources/buddy-creator/assets/licenses/micromark-factory-space.txt +0 -26
  69. package/resources/buddy-creator/assets/licenses/micromark-factory-title.txt +0 -26
  70. package/resources/buddy-creator/assets/licenses/micromark-factory-whitespace.txt +0 -26
  71. package/resources/buddy-creator/assets/licenses/micromark-util-character.txt +0 -26
  72. package/resources/buddy-creator/assets/licenses/micromark-util-chunked.txt +0 -26
  73. package/resources/buddy-creator/assets/licenses/micromark-util-classify-character.txt +0 -26
  74. package/resources/buddy-creator/assets/licenses/micromark-util-combine-extensions.txt +0 -26
  75. package/resources/buddy-creator/assets/licenses/micromark-util-decode-numeric-character-reference.txt +0 -26
  76. package/resources/buddy-creator/assets/licenses/micromark-util-decode-string.txt +0 -26
  77. package/resources/buddy-creator/assets/licenses/micromark-util-encode.txt +0 -26
  78. package/resources/buddy-creator/assets/licenses/micromark-util-html-tag-name.txt +0 -26
  79. package/resources/buddy-creator/assets/licenses/micromark-util-normalize-identifier.txt +0 -26
  80. package/resources/buddy-creator/assets/licenses/micromark-util-resolve-all.txt +0 -26
  81. package/resources/buddy-creator/assets/licenses/micromark-util-sanitize-uri.txt +0 -26
  82. package/resources/buddy-creator/assets/licenses/micromark-util-subtokenize.txt +0 -26
  83. package/resources/buddy-creator/assets/licenses/micromark-util-symbol.txt +0 -26
  84. package/resources/buddy-creator/assets/licenses/micromark-util-types.txt +0 -26
  85. package/resources/buddy-creator/assets/licenses/micromark.txt +0 -26
  86. package/resources/buddy-creator/assets/licenses/ms.txt +0 -25
  87. package/resources/buddy-creator/assets/licenses/parse-entities.txt +0 -26
  88. package/resources/buddy-creator/assets/licenses/phosphor-icons--react.txt +0 -25
  89. package/resources/buddy-creator/assets/licenses/property-information.txt +0 -26
  90. package/resources/buddy-creator/assets/licenses/react-dom.txt +0 -25
  91. package/resources/buddy-creator/assets/licenses/react-markdown.txt +0 -25
  92. package/resources/buddy-creator/assets/licenses/react.txt +0 -25
  93. package/resources/buddy-creator/assets/licenses/remark-gfm.txt +0 -26
  94. package/resources/buddy-creator/assets/licenses/remark-parse.txt +0 -25
  95. package/resources/buddy-creator/assets/licenses/remark-rehype.txt +0 -26
  96. package/resources/buddy-creator/assets/licenses/remark-stringify.txt +0 -25
  97. package/resources/buddy-creator/assets/licenses/scheduler.txt +0 -25
  98. package/resources/buddy-creator/assets/licenses/space-separated-tokens.txt +0 -26
  99. package/resources/buddy-creator/assets/licenses/stringify-entities.txt +0 -26
  100. package/resources/buddy-creator/assets/licenses/style-to-js.txt +0 -26
  101. package/resources/buddy-creator/assets/licenses/style-to-object.txt +0 -26
  102. package/resources/buddy-creator/assets/licenses/trim-lines.txt +0 -26
  103. package/resources/buddy-creator/assets/licenses/trough.txt +0 -25
  104. package/resources/buddy-creator/assets/licenses/types--debug.txt +0 -25
  105. package/resources/buddy-creator/assets/licenses/types--estree-jsx.txt +0 -25
  106. package/resources/buddy-creator/assets/licenses/types--estree.txt +0 -25
  107. package/resources/buddy-creator/assets/licenses/types--hast.txt +0 -25
  108. package/resources/buddy-creator/assets/licenses/types--mdast.txt +0 -25
  109. package/resources/buddy-creator/assets/licenses/types--ms.txt +0 -25
  110. package/resources/buddy-creator/assets/licenses/types--unist.txt +0 -25
  111. package/resources/buddy-creator/assets/licenses/ungap--structured-clone.txt +0 -19
  112. package/resources/buddy-creator/assets/licenses/unified.txt +0 -25
  113. package/resources/buddy-creator/assets/licenses/unist-util-is.txt +0 -26
  114. package/resources/buddy-creator/assets/licenses/unist-util-position.txt +0 -26
  115. package/resources/buddy-creator/assets/licenses/unist-util-stringify-position.txt +0 -26
  116. package/resources/buddy-creator/assets/licenses/unist-util-visit-parents.txt +0 -26
  117. package/resources/buddy-creator/assets/licenses/unist-util-visit.txt +0 -26
  118. package/resources/buddy-creator/assets/licenses/vfile-message.txt +0 -26
  119. package/resources/buddy-creator/assets/licenses/vfile.txt +0 -25
  120. package/resources/buddy-creator/assets/licenses/zwitch.txt +0 -26
  121. package/resources/buddy-creator/assets/preview/assets/index-B5VmkoSx.js +0 -39
  122. package/resources/buddy-creator/assets/preview/assets/index-DWKP6b-3.css +0 -4
  123. package/resources/buddy-creator/assets/preview/brand-logo.png +0 -0
  124. package/resources/buddy-creator/assets/preview/brand.css +0 -100
  125. package/resources/buddy-creator/assets/preview/index.html +0 -7
  126. package/resources/buddy-creator/assets/preview/reader.css +0 -165
  127. package/resources/buddy-creator/assets/preview/reader.js +0 -135
  128. package/resources/buddy-creator/assets/preview/widgets.css +0 -1
  129. package/resources/buddy-creator/assets/preview/widgets.js +0 -168
  130. package/resources/buddy-creator/assets/widget-reference/conversation.png +0 -0
  131. package/resources/buddy-creator/assets/widget-reference/expanded.png +0 -0
  132. package/resources/buddy-creator/assets/widget-v2/app.js +0 -62
  133. package/resources/buddy-creator/assets/widget-v2/style.css +0 -1
  134. package/resources/buddy-creator/assets/widget-v3/app.js +0 -65
  135. package/resources/buddy-creator/assets/widget-v3/style.css +0 -103
  136. package/resources/buddy-creator/assets/widget-v4/app.js +0 -66
  137. package/resources/buddy-creator/assets/widget-v4/style.css +0 -187
  138. package/resources/buddy-creator/references/artifact-schema.md +0 -70
  139. package/resources/buddy-creator/references/catalog.json +0 -401
  140. package/resources/buddy-creator/references/definition.md +0 -24
  141. package/resources/buddy-creator/references/dialogue-examples.md +0 -133
  142. package/resources/buddy-creator/references/host-guide.md +0 -219
  143. package/resources/buddy-creator/references/interview.md +0 -235
  144. package/resources/buddy-creator/references/knowledge.md +0 -40
  145. package/resources/buddy-creator/references/methods.md +0 -77
  146. package/resources/buddy-creator/references/opening.md +0 -14
  147. package/resources/buddy-creator/references/preview-panel.md +0 -66
  148. package/resources/buddy-creator/references/recovery.md +0 -46
  149. package/resources/buddy-creator/references/service.md +0 -76
  150. package/resources/buddy-creator/references/widgets-protocol.md +0 -83
  151. package/resources/buddy-creator/references/widgets.md +0 -99
  152. package/resources/buddy-creator/scripts/buddy.py +0 -108
  153. package/resources/buddy-creator/scripts/buddy_client.py +0 -67
  154. package/resources/buddy-creator/scripts/buddy_core.py +0 -865
  155. package/resources/buddy-creator/scripts/completion.py +0 -296
  156. package/resources/buddy-creator/scripts/interview.py +0 -215
  157. package/resources/buddy-creator/scripts/preview.py +0 -617
  158. package/resources/buddy-creator/scripts/preview_panel.py +0 -80
  159. package/resources/buddy-creator/scripts/sources.py +0 -239
  160. package/resources/buddy-creator/scripts/widget_render.py +0 -66
  161. package/resources/buddy-creator/scripts/widget_render_v1.py +0 -176
  162. package/resources/buddy-creator/scripts/widget_render_v2.py +0 -98
  163. package/resources/buddy-creator/scripts/widget_render_v3.py +0 -86
  164. package/resources/buddy-creator/scripts/widget_render_v4.py +0 -121
  165. package/resources/buddy-creator/scripts/widgets.py +0 -227
  166. package/resources/buddy-creator/version.json +0 -8
  167. package/resources/buddy-creator.manifest.json +0 -791
  168. package/resources/dada-object-avatar/SKILL.md +0 -37
  169. package/resources/dada-object-avatar/agents/openai.yaml +0 -4
  170. package/resources/dada-object-avatar/assets/examples/cat.png +0 -0
  171. package/resources/dada-object-avatar/assets/examples/cooking.png +0 -0
  172. package/resources/dada-object-avatar/assets/examples/divination.png +0 -0
  173. package/resources/dada-object-avatar/assets/examples/doctor.png +0 -0
  174. package/resources/dada-object-avatar/assets/examples/relationship.png +0 -0
  175. package/resources/dada-object-avatar/assets/examples/work.png +0 -0
  176. package/resources/dada-object-avatar/assets/preview.png +0 -0
  177. package/resources/dada-object-avatar/references/evaluation-rubric.md +0 -22
  178. package/resources/dada-object-avatar/references/examples.json +0 -8
  179. package/resources/dada-object-avatar/references/prompt-template.md +0 -27
  180. package/resources/dada-object-avatar/references/style-system.md +0 -54
  181. package/resources/dada-object-avatar/references/subject-design.md +0 -23
  182. package/resources/dada-object-avatar/references/training-guide.md +0 -14
  183. package/resources/dada-object-avatar/references/training-log.jsonl +0 -0
  184. package/resources/dada-object-avatar/references/training-record.schema.json +0 -20
  185. package/resources/dada-object-avatar/scripts/choose_gaze.py +0 -36
  186. package/resources/dada-object-avatar/scripts/record_example.py +0 -22
  187. package/resources/dada-object-avatar/scripts/validate_png.py +0 -52
  188. package/resources/dada-object-avatar.manifest.json +0 -85
package/dist/web/app.js CHANGED
@@ -191,6 +191,8 @@
191
191
  idle: hosted ? "已连接但 DEV 未启动" : "未启动",
192
192
  disconnected: "未连接本地工程",
193
193
  starting: "正在启动",
194
+ reloading: "正在重载",
195
+ reload_failed: "重载失败 · 等待修正",
194
196
  ready: "DEV 运行中",
195
197
  stopping: "正在停止",
196
198
  failed: "启动失败",
@@ -201,6 +203,7 @@
201
203
  if (!next || !Number.isFinite(next.revision)) return false;
202
204
  if (Number.isFinite(state.dev?.revision) && next.revision < state.dev.revision) return false;
203
205
  if (next.phase === "ready" && state.dev.phase !== "ready") state.simulatorLoaded = false;
206
+ if (state.dev.phase === "reload_failed" && ["ready", "reloading"].includes(next.phase)) elements.errorBanner.hidden = true;
204
207
  state.dev = next;
205
208
  if (next.ready?.buddy) state.buddy = next.ready.buddy;
206
209
  return true;
@@ -254,8 +257,8 @@
254
257
  elements.connectionState.dataset.phase = phase;
255
258
  elements.connectionLabel.textContent = phase === "disconnected" ? "未连接本地工程" : phaseCopy(phase);
256
259
  elements.devControl.disabled = !connectionReady || Boolean(devAction) || phase === "starting" || phase === "stopping";
257
- elements.devControl.dataset.action = phase === "ready" ? "stop" : "start";
258
- elements.devControl.textContent = devAction === "start" ? "正在启动…" : devAction === "stop" ? "正在停止…" : phase === "failed" ? "重试启动" : phase === "ready" ? "停止 DEV" : phase === "starting" ? "正在启动…" : phase === "stopping" ? "正在停止…" : "启动 DEV";
260
+ elements.devControl.dataset.action = ["ready", "reloading", "reload_failed"].includes(phase) ? "stop" : "start";
261
+ elements.devControl.textContent = devAction === "start" ? "正在启动…" : devAction === "stop" ? "正在停止…" : phase === "failed" ? "重试启动" : ["ready", "reloading", "reload_failed"].includes(phase) ? "停止 DEV" : phase === "starting" ? "正在启动…" : phase === "stopping" ? "正在停止…" : "启动 DEV";
259
262
  elements.environmentAgent.textContent = phase === "ready"
260
263
  ? `${state.dev.ready?.agentId || "已就绪"} · CLI 托管`
261
264
  : phaseCopy(phase);
@@ -277,7 +280,7 @@
277
280
  updateSendButton();
278
281
  syncPolling();
279
282
  if (
280
- phase === "failed"
283
+ (phase === "failed" || phase === "reload_failed")
281
284
  && state.dev.error?.message
282
285
  && state.reportedFailureRevision !== state.dev.revision
283
286
  ) {
@@ -301,6 +304,8 @@
301
304
  const copy = !connectionChecked ? ["正在检查连接", "读取本地工程的连接状态。"]
302
305
  : phase === "disconnected" ? [connectionInstance ? "本地连接已断开" : "连接本地工程", connectionFailure || (connectionInstance ? "正在等待重新连接。保留当前记录,连接恢复后继续调试。" : "点击上方「连接工程」,复制指令到终端运行。")]
303
306
  : phase === "starting" ? ["正在启动调试", "正在准备运行环境,启动完成后即可发送消息。"]
307
+ : phase === "reloading" ? ["正在重载", "本地文件已修改,正在重新加载;当前回复可能中断。"]
308
+ : phase === "reload_failed" ? ["重载失败", state.dev.error?.message || "修正文件并保存后自动重试,也可停止 DEV。"]
304
309
  : phase === "stopping" ? ["正在停止调试", "等待运行环境退出,当前记录会保留。"]
305
310
  : phase === "failed" ? ["调试未能启动", "查看上方错误信息,修复工程后点击「重试启动」。"]
306
311
  : phase === "idle" ? ["工程已连接", "点击上方「启动 DEV」,开始体验搭子。"]
@@ -311,7 +316,7 @@
311
316
  guide.hidden = phase === "ready" && (runs.length > 0 || (state.debug.diagnostics?.records || []).some(record => record.kind !== "log"));
312
317
  $("#workbench-state-title").textContent = copy[0];
313
318
  $("#workbench-state-copy").textContent = copy[1];
314
- $("#connection-description").textContent = !connectionChecked ? "检查中" : phase === "disconnected" ? "等待连接" : "仅用于本地调试";
319
+ $("#connection-description").textContent = !connectionChecked ? "检查中" : phase === "disconnected" ? "等待连接" : "保存自动重载,可能中断回复;Widget 数据不自动迁移。";
315
320
  const connectButton = $("#get-connect-command");
316
321
  connectButton.className = `button ${connectionReady ? "button-secondary" : "button-primary"}`;
317
322
  connectButton.textContent = connectionReady ? "连接指令" : connectionInstance ? "重新连接" : "连接工程";
@@ -556,7 +561,7 @@
556
561
  }
557
562
 
558
563
  const widgets = window.createSimulatorWidgets?.({ api, avatar: () => messageAvatar("assistant") });
559
- function renderWidgets() { widgets?.update({ active: state.dev.phase === "ready" && !state.resetting, conversationRevision: state.simulator.conversationRevision, devRevision: state.dev.revision }); }
564
+ function renderWidgets() { widgets?.update({ reloading: ["reloading", "reload_failed"].includes(state.dev.phase), active: state.dev.phase === "ready" && !state.resetting, conversationRevision: state.simulator.conversationRevision, devRevision: state.dev.revision }); }
560
565
 
561
566
  let messageSignature = "";
562
567
  let followMessages = true;
@@ -689,7 +694,7 @@
689
694
  async function controlDev() {
690
695
  if (!connectionReady || state.dev.phase === "starting" || state.dev.phase === "stopping") return;
691
696
  if (devAction) return;
692
- devAction = state.dev.phase === "ready" ? "stop" : "start";
697
+ devAction = ["ready", "reloading", "reload_failed"].includes(state.dev.phase) ? "stop" : "start";
693
698
  elements.errorBanner.hidden = true;
694
699
  renderDev();
695
700
  try {
@@ -1,31 +1,62 @@
1
- /* Native points expressed against the simulator's 415 pt canvas. */
2
- .widget-shortcut { position:absolute; z-index:3; top:24.1cqw; left:2.89cqw; max-width:calc(100% - 5.78cqw); }
3
- .widget-shortcut[hidden], .widget-overlay[hidden] { display:none; }
4
- .widget-pill { display:flex; align-items:center; gap:1.93cqw; min-height:12.53cqw; max-width:62.65cqw; border-radius:99px; padding:1.93cqw 4.82cqw 1.93cqw 3.855cqw; color:#20252b; background:linear-gradient(135deg,#fff,#bbf1a7); border:.5px solid #20252b1f; font-size:3.855cqw; font-weight:400; box-shadow:none; }
5
- .widget-pill .widget-pill-avatar { width:6.747cqw; height:6.747cqw; flex:none; border-radius:50%; }
1
+ /* iOS ff1845b: dimensions in native points, scaled once with the device canvas. */
2
+ .widget-shortcut { position:absolute; z-index:3; top:calc(var(--widget-unit, 1px) * 100); left:calc(var(--widget-unit, 1px) * 12); max-width:calc(100% - calc(var(--widget-unit, 1px) * 24)); }
3
+ .widget-shortcut[hidden], .widget-overlay[hidden], .widget-more[hidden] { display:none; }
4
+ .widget-pill { display:flex; align-items:center; gap:calc(var(--widget-unit, 1px) * 8); height:calc(var(--widget-unit, 1px) * 52); max-width:calc(var(--widget-unit, 1px) * 260); border-radius:99px; padding:0 calc(var(--widget-unit, 1px) * 20) 0 calc(var(--widget-unit, 1px) * 16); color:#000; background:linear-gradient(110deg,#fff,#bbf1a7); border:calc(var(--widget-unit, 1px) * .5) solid #0000001f; font-size:calc(var(--widget-unit, 1px) * 16); font-weight:400; box-shadow:none; transition:padding .22s ease-in-out; }
5
+ .widget-pill .widget-pill-avatar { width:calc(var(--widget-unit, 1px) * 28); height:calc(var(--widget-unit, 1px) * 28); flex:none; border-radius:50%; }
6
6
  .widget-pill-title { white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
7
- .widget-chevron { margin-left:1.9cqw; font-size:2.65cqw; }
8
- .widget-choices,.widget-options { position:absolute; z-index:5; top:calc(100% + 2cqw); left:0; width:55.9cqw; padding:1cqw 0; border-radius:3cqw; background:#f7f7f9; box-shadow:0 3cqw 9cqw #0003; overflow:hidden; }
9
- .widget-choices button,.widget-options button { display:block; width:100%; min-height:10.6cqw; padding:2.9cqw 3.855cqw; text-align:left; background:transparent; color:#20252b; font-size:3.37cqw; font-weight:400; border-bottom:.3px solid #0002; }
10
- .widget-overlay { position:absolute; z-index:10; inset:0; display:flex; align-items:center; justify-content:center; background:#0003; animation:widget-fade .2s ease-in-out; }
11
- .widget-modal { position:relative; width:calc(100% - 7.71cqw); height:min(134.94cqw,calc(100% - 11.57cqw)); max-width:560px; border-radius:5.783cqw; background:#fff; color:#20252b; overflow:hidden; display:flex; flex-direction:column; padding-bottom:2.89cqw; }
12
- .widget-modal-header { display:flex; gap:1.93cqw; align-items:center; padding:1.93cqw 1.93cqw 0; flex:none; }
13
- .widget-modal-header button { width:10.6cqw; height:10.6cqw; flex:none; padding:0; background:transparent; color:inherit; font-size:5.3cqw; font-weight:400; }
14
- .widget-modal-header h3 { flex:1; margin:0; text-align:center; font-size:3.37cqw; font-weight:500; line-height:1.4; }
15
- .widget-modal-body { flex:1; min-height:0; overflow:auto; text-align:center; font-size:3.37cqw; }
16
- .widget-modal-body>p,.widget-modal-body>h4 { padding:0 5.78cqw; }
17
- .widget-modal-body>h4,.widget-loading { margin-top:25%; }
18
- .widget-modal-body>button { background:#f3f3f5; color:inherit; padding:2.9cqw; border-radius:2cqw; }
19
- .widget-document { width:100%; height:100%; border:0; display:block; background:white; }
20
- .widget-options { top:12.5cqw; right:2cqw; left:auto; }
21
- .widget-options p { padding:3cqw; font-size:3.37cqw; }
22
- .widget-pin-note { position:absolute; top:14.45cqw; left:50%; transform:translateX(-50%); white-space:nowrap; border-radius:99px; background:#f5f5f5ed; padding:2.9cqw; font-size:2.89cqw; }
23
- .widget-entry { width:71.57cqw; max-width:100%; padding:2.89cqw; border:.3px solid #dadbdd; border-radius:3.855cqw; background:#fff; color:#20252b; font-size:3.3735cqw; }
24
- .widget-entry h4 { margin:0 0 2.89cqw; font:500 3.3735cqw/1.5 sans-serif; }
25
- .widget-entry p { margin:0; color:#717782; line-height:1.5; overflow-wrap:anywhere; }
26
- .widget-entry-details { display:flex; align-items:center; gap:2.89cqw; }
27
- .widget-entry-photo { width:15.42cqw; height:15.42cqw; border-radius:50%; object-fit:cover; flex:none; }
28
- .widget-entry-open { width:100%; min-height:10.6cqw; margin-top:2.89cqw; background:#f3f3f5; color:inherit; border-radius:1.93cqw; font-size:3.3735cqw; font-weight:500; }
7
+ .widget-chevron { margin-left:calc(var(--widget-unit, 1px) * 8); font-size:calc(var(--widget-unit, 1px) * 18); }
8
+ .widget-shortcut[data-phase="collapsed"] .widget-pill { width:calc(var(--widget-unit, 1px) * 52); padding:0 calc(var(--widget-unit, 1px) * 12); }
9
+ .widget-shortcut[data-phase="collapsed"] :is(.widget-pill-title,.widget-chevron) { display:none; }
10
+ .widget-choices,.widget-options { position:absolute; z-index:5; background:#fff; border:calc(var(--widget-unit, 1px) * .3) solid #0000001f; border-radius:calc(var(--widget-unit, 1px) * 16); box-shadow:0 calc(var(--widget-unit, 1px) * 4) calc(var(--widget-unit, 1px) * 16) #0000000d; overflow:auto; }
11
+ .widget-choices { top:calc(100% + calc(var(--widget-unit, 1px) * 8)); left:0; width:calc(var(--widget-unit, 1px) * 240); max-height:calc(var(--widget-unit, 1px) * 320); padding:calc(var(--widget-unit, 1px) * 4) 0; }
12
+ .widget-choices button { display:flex; align-items:center; justify-content:space-between; gap:calc(var(--widget-unit, 1px) * 8); width:100%; min-height:calc(var(--widget-unit, 1px) * 48); padding:calc(var(--widget-unit, 1px) * 12) calc(var(--widget-unit, 1px) * 16); text-align:left; background:transparent; color:#000; font-size:calc(var(--widget-unit, 1px) * 16); font-weight:400; }
13
+ .widget-choices button>span:first-child { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
14
+ .widget-arrow { color:#0009; }
15
+ .widget-choices button:hover,.widget-options button:hover { background:#f1f3f4; }
16
+ .widget-overlay { position:absolute; z-index:10; inset:0; display:flex; align-items:center; justify-content:center; background:#0003; opacity:1; animation:widget-fade .22s ease-in-out; transition:opacity .22s ease-in-out; }
17
+ .widget-overlay[data-state="closing"] { opacity:0; pointer-events:none; }
18
+ .widget-modal { position:relative; width:calc(100% - calc(var(--widget-unit, 1px) * 32)); height:65%; border-radius:calc(var(--widget-unit, 1px) * 24); background:#fff; color:#000; overflow:hidden; display:flex; flex-direction:column; padding-bottom:calc(var(--widget-unit, 1px) * 12); }
19
+ .widget-modal-header { display:flex; gap:0; align-items:center; min-height:calc(var(--widget-unit, 1px) * 52); padding:calc(var(--widget-unit, 1px) * 4) calc(var(--widget-unit, 1px) * 4) 0; flex:none; }
20
+ .widget-modal-header button,.widget-more-slot { width:calc(var(--widget-unit, 1px) * 44); height:calc(var(--widget-unit, 1px) * 44); flex:none; padding:0; background:transparent; color:#000; font-size:calc(var(--widget-unit, 1px) * 22); font-weight:400; }
21
+ .widget-modal-header h3 { flex:1; min-width:0; margin:0; text-align:center; font-size:calc(var(--widget-unit, 1px) * 14); font-weight:500; line-height:1.4; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
22
+ .widget-modal-body { flex:1; position:relative; min-height:0; overflow:hidden; font-size:calc(var(--widget-unit, 1px) * 14); }
23
+ .widget-document { position:absolute; top:0; left:0; border:0; display:block; background:#fff; }
24
+ .widget-loading,.widget-failure { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:calc(var(--widget-unit, 1px) * 12); padding:calc(var(--widget-unit, 1px) * 24); text-align:center; color:#0009; }
25
+ .widget-loading p,.widget-failure p { margin:0; font-size:calc(var(--widget-unit, 1px) * 14); line-height:1.5; }
26
+ .widget-failure h4 { margin:0; font-size:calc(var(--widget-unit, 1px) * 17); font-weight:600; color:#000; }
27
+ .widget-spinner { width:calc(var(--widget-unit, 1px) * 20); height:calc(var(--widget-unit, 1px) * 20); border:calc(var(--widget-unit, 1px) * 2) solid #0002; border-top-color:#0009; border-radius:50%; animation:widget-spin .85s linear infinite; }
28
+ .widget-failure-mark { display:grid; place-items:center; width:calc(var(--widget-unit, 1px) * 32); height:calc(var(--widget-unit, 1px) * 32); border:calc(var(--widget-unit, 1px) * 1.5) solid #0006; border-radius:50%; font-size:calc(var(--widget-unit, 1px) * 22); }
29
+ .widget-failure button { margin-top:calc(var(--widget-unit, 1px) * 4); padding:calc(var(--widget-unit, 1px) * 10) calc(var(--widget-unit, 1px) * 16); border-radius:calc(var(--widget-unit, 1px) * 8); background:#f1f3f4; color:#000; font-size:calc(var(--widget-unit, 1px) * 14); }
30
+ .widget-options { top:calc(var(--widget-unit, 1px) * 52); right:calc(var(--widget-unit, 1px) * 8); width:calc(var(--widget-unit, 1px) * 232); padding:calc(var(--widget-unit, 1px) * 8) 0; }
31
+ /* Menu rhythm and monochrome icons match the native action menu. */
32
+ .widget-options button { position:relative; display:flex; align-items:center; gap:calc(var(--widget-unit, 1px) * 6); width:100%; min-height:calc(var(--widget-unit, 1px) * 36); padding:calc(var(--widget-unit, 1px) * 6) calc(var(--widget-unit, 1px) * 16); text-align:left; background:transparent; color:#000; font-size:calc(var(--widget-unit, 1px) * 14); font-weight:400; }
33
+ .widget-options button+button { margin-top:calc(var(--widget-unit, 1px) * 8); }
34
+ .widget-options button+button::before { content:""; position:absolute; top:calc(var(--widget-unit, 1px) * -4); left:calc(var(--widget-unit, 1px) * 16); right:calc(var(--widget-unit, 1px) * 16); height:calc(var(--widget-unit, 1px) * .3); background:#00000026; }
35
+ .widget-options svg { width:calc(var(--widget-unit, 1px) * 20); height:calc(var(--widget-unit, 1px) * 20); flex:none; fill:none; stroke:currentColor; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:round; }
36
+ .widget-options .widget-destructive,.widget-confirm .widget-destructive { color:#d73340; }
37
+ .widget-options button:disabled,.widget-confirm button:disabled { opacity:.45; cursor:wait; }
38
+ .widget-confirm-backdrop { position:absolute; inset:0; z-index:7; display:grid; place-items:center; background:#0003; padding:calc(var(--widget-unit, 1px) * 20); }
39
+ .widget-confirm { width:calc(var(--widget-unit, 1px) * 270); max-width:100%; padding:calc(var(--widget-unit, 1px) * 20) calc(var(--widget-unit, 1px) * 16) calc(var(--widget-unit, 1px) * 10); border-radius:calc(var(--widget-unit, 1px) * 16); background:#fff; text-align:center; }
40
+ .widget-confirm h4 { margin:0 0 calc(var(--widget-unit, 1px) * 8); font-size:calc(var(--widget-unit, 1px) * 17); font-weight:600; }
41
+ .widget-confirm p { margin:0; font-size:calc(var(--widget-unit, 1px) * 13); line-height:1.5; }
42
+ .widget-confirm-actions { display:flex; margin-top:calc(var(--widget-unit, 1px) * 16); border-top:calc(var(--widget-unit, 1px) * .5) solid #0002; }
43
+ .widget-confirm-actions button { flex:1; padding:calc(var(--widget-unit, 1px) * 12); font-size:calc(var(--widget-unit, 1px) * 16); background:transparent; color:#000; }
44
+ .widget-native-share { position:absolute; z-index:6; inset:calc(var(--widget-unit, 1px) * 52) 0 0; padding:calc(var(--widget-unit, 1px) * 28) calc(var(--widget-unit, 1px) * 24); background:#fff; font-size:calc(var(--widget-unit, 1px) * 14); }
45
+ .widget-native-share h4 { margin:0 0 calc(var(--widget-unit, 1px) * 12); font-size:calc(var(--widget-unit, 1px) * 17); }
46
+ .widget-native-share p { line-height:1.6; color:#0009; margin:0 0 calc(var(--widget-unit, 1px) * 24); }
47
+ .widget-native-share button { display:block; width:100%; margin-top:calc(var(--widget-unit, 1px) * 12); padding:calc(var(--widget-unit, 1px) * 12); border-radius:calc(var(--widget-unit, 1px) * 8); color:#000; background:#f1f3f4; font-size:calc(var(--widget-unit, 1px) * 14); }
48
+ .widget-pin-note { position:absolute; z-index:8; top:calc(17.5% + calc(var(--widget-unit, 1px) * 58)); left:50%; transform:translateX(-50%); max-width:90%; border-radius:calc(var(--widget-unit, 1px) * 16); background:#f1f3f4f5; box-shadow:0 calc(var(--widget-unit, 1px) * 4) calc(var(--widget-unit, 1px) * 16) #0001; padding:calc(var(--widget-unit, 1px) * 12) calc(var(--widget-unit, 1px) * 16); font-size:calc(var(--widget-unit, 1px) * 12); text-align:center; }
49
+ .widget-entry { width:calc(var(--widget-unit, 1px) * 297); max-width:100%; padding:calc(var(--widget-unit, 1px) * 12); border:calc(var(--widget-unit, 1px) * .3) solid #00000026; border-radius:calc(var(--widget-unit, 1px) * 16); background:#fff; color:#000; font-size:calc(var(--widget-unit, 1px) * 14); }
50
+ .widget-entry h4 { margin:0 0 calc(var(--widget-unit, 1px) * 12); font:500 calc(var(--widget-unit, 1px) * 14)/1.4 -apple-system,BlinkMacSystemFont,sans-serif; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
51
+ .widget-entry p { margin:0; color:#000; font-size:calc(var(--widget-unit, 1px) * 14); line-height:1.4; overflow-wrap:anywhere; display:-webkit-box; -webkit-line-clamp:4; -webkit-box-orient:vertical; overflow:hidden; }
52
+ .widget-entry p.widget-secondary { color:#0009; }
53
+ .widget-entry-details { display:flex; align-items:center; gap:calc(var(--widget-unit, 1px) * 12); }
54
+ .widget-entry-photo { width:calc(var(--widget-unit, 1px) * 64); height:calc(var(--widget-unit, 1px) * 64); border-radius:50%; object-fit:cover; flex:none; }
55
+ .widget-entry-open { width:100%; height:calc(var(--widget-unit, 1px) * 40); min-height:0; padding:0; margin-top:calc(var(--widget-unit, 1px) * 12); background:#f1f3f4; color:#000; border-radius:calc(var(--widget-unit, 1px) * 8); font-size:calc(var(--widget-unit, 1px) * 14); font-weight:500; }
56
+ .widget-entry-metadata { display:flex; min-width:0; margin-top:calc(var(--widget-unit, 1px) * 12); font-size:calc(var(--widget-unit, 1px) * 11); line-height:1.4; color:#0009; }
57
+ .widget-creator { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
58
+ .widget-permission { flex:none; }
29
59
  .widget-entry button:focus-visible,.widget-shortcut button:focus-visible,.widget-overlay button:focus-visible { outline:2px solid #007a9b; outline-offset:2px; }
30
60
  @keyframes widget-fade { from { opacity:0; } to { opacity:1; } }
31
- @media(prefers-reduced-motion:reduce) { .widget-overlay { animation-duration:.1s; } }
61
+ @keyframes widget-spin { to { transform:rotate(360deg); } }
62
+ @media(prefers-reduced-motion:reduce) { .widget-overlay { animation-duration:.1s; transition-duration:.1s; } .widget-pill { transition:none; } }
@@ -1,126 +1,280 @@
1
- /* Client reference: my-life-buddies-ios dev 547818d, DadaConversationWidgets / DadaHostedWidgetPage. */
1
+ /* Client reference: my-life-buddies-ios dev ff1845b (2026-09-16).
2
+ Native point sizes share the phone's scale; Web content remains in an opaque sandbox. */
2
3
  (() => {
3
- const previewUrl = path => window.previewApiUrl?.(path) ?? path;
4
- const el = (tag, className, text) => { const n = document.createElement(tag); n.className = className; if (text !== undefined) n.textContent = text; return n; };
4
+ const el = (tag, className = '', text) => {
5
+ const node = document.createElement(tag); node.className = className;
6
+ if (text !== undefined) node.textContent = text;
7
+ return node;
8
+ };
9
+ const button = (label, action, className = '') => {
10
+ const node = el('button', className, label); node.type = 'button'; node.addEventListener('click', action); return node;
11
+ };
12
+ const menuIcon = name => {
13
+ const paths = {
14
+ share: 'M12 15V3m-4 4 4-4 4 4M7 11H5v10h14V11h-2',
15
+ pin: 'm9 3 6 0-1 6 4 4v2H6v-2l4-4ZM12 15v6',
16
+ unpin: 'm3 3 18 18M9 3h6l-1 6 4 4v2M6 15h6m0 0v6',
17
+ trash: 'M4 7h16M9 7V4h6v3M6 7l1 14h10l1-14M10 11v6m4-6v6',
18
+ };
19
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
20
+ svg.setAttribute('viewBox', '0 0 24 24'); svg.setAttribute('aria-hidden', 'true');
21
+ const path = document.createElementNS(svg.namespaceURI, 'path'); path.setAttribute('d', paths[name]);
22
+ svg.append(path); return svg;
23
+ };
5
24
  window.createSimulatorWidgets = ({ api, avatar }) => {
6
25
  const shortcut = document.querySelector('#widget-shortcut');
7
26
  const overlay = document.querySelector('#widget-overlay');
8
27
  const phone = document.querySelector('.phone-screen');
9
- let items = [], scope = '', generation = 0, ready = false, loading = false, lastRefresh = 0, selected, focusBefore, openRevision = 0;
10
- let failed = false, renderSignature = '', frameObserver;
11
- function close() {
12
- frameObserver?.disconnect(); frameObserver = undefined;
13
- const wasOpen = !overlay.hidden;
14
- openRevision++; selected = undefined; overlay.hidden = true; overlay.replaceChildren();
28
+ const device = document.querySelector('.phone');
29
+ let reopenId, conversationScope;
30
+ let items = [], scope = '', generation = 0, ready = false, loading = false, lastRefresh = 0;
31
+ let failed = false, signature = '', selected, focusBefore, openRevision = 0, frameObserver;
32
+ let collapseTimer, closeTimer, loadTimer, documentFrame, documentToken, documentReady;
33
+ let phase = 'normal', choices, pill, busy = false, refreshPromise;
34
+ let documentLifecycle = false, canDelete = false;
35
+ const timers = () => { clearTimeout(loadTimer); frameObserver?.disconnect(); frameObserver = undefined; documentFrame = undefined; documentReady = undefined; };
36
+ const scale = () => device.clientWidth / 415;
37
+ const resize = new ResizeObserver(() => { phone.style.setProperty('--widget-unit', `${scale()}px`); });
38
+ resize.observe(device);
39
+ function setPhase(value) { phase = value; shortcut.dataset.phase = value; pill?.setAttribute('aria-expanded', String(value === 'choosing')); }
40
+ function scheduleCollapse() {
41
+ clearTimeout(collapseTimer);
42
+ if (!ready || !items.length || selected || document.hidden || phase !== 'normal') return;
43
+ collapseTimer = setTimeout(() => setPhase('collapsed'), 5000);
44
+ }
45
+ function closeChoices(reset = true) {
46
+ choices?.remove(); choices = undefined;
47
+ if (reset) { setPhase('normal'); scheduleCollapse(); }
48
+ }
49
+ function finishClose() {
50
+ overlay.hidden = true; overlay.replaceChildren(); delete overlay.dataset.state;
15
51
  for (const child of phone.children) if (child !== overlay) child.inert = false;
16
- if (wasOpen) focusBefore?.focus?.();
52
+ if (focusBefore?.isConnected) focusBefore.focus(); focusBefore = undefined; setPhase('normal'); scheduleCollapse();
53
+ }
54
+ function close(immediate = false) {
55
+ if (busy && !immediate) return;
56
+ clearTimeout(closeTimer); timers(); openRevision++; selected = undefined;
57
+ if (immediate || overlay.hidden) { finishClose(); return; }
58
+ overlay.dataset.state = 'closing';
59
+ closeTimer = setTimeout(finishClose, window.matchMedia('(prefers-reduced-motion:reduce)').matches ? 100 : 220);
17
60
  }
18
- function button(label, action, className = '') { const n = el('button', className, label); n.type = 'button'; n.addEventListener('click', action); return n; }
19
61
  function render() {
20
- const signature = JSON.stringify([ready, items, failed]);
21
- if (signature === renderSignature) return;
22
- renderSignature = signature;
23
- shortcut.replaceChildren(); shortcut.hidden = !ready || (!items.length && !failed);
62
+ const next = JSON.stringify([ready, items, failed]);
63
+ if (next === signature) return;
64
+ signature = next;
65
+ const choosing = phase === 'choosing';
66
+ closeChoices(false); shortcut.replaceChildren(); shortcut.hidden = !ready || (!items.length && !failed);
24
67
  if (shortcut.hidden) return;
25
68
  if (!items.length) { shortcut.append(button('重新加载小挂件', () => refresh(true), 'widget-pill')); return; }
26
- const pill = button('', () => {
27
- if (items.length === 1) { void open(items[0]); return; }
28
- const old = shortcut.querySelector('.widget-choices');
29
- if (old) { old.remove(); pill.setAttribute('aria-expanded', 'false'); return; }
30
- const choices = el('div', 'widget-choices'); choices.setAttribute('role', 'menu');
31
- for (const item of items) { const b = button(item.title + '', () => { choices.remove(); pill.setAttribute('aria-expanded', 'false'); void open(item); }); b.setAttribute('role', 'menuitem'); choices.append(b); }
32
- shortcut.append(choices); pill.setAttribute('aria-expanded', 'true'); choices.firstChild?.focus();
69
+ const title = items.length === 1 ? items[0].title : '全部小挂件';
70
+ pill = button('', () => {
71
+ clearTimeout(collapseTimer);
72
+ if (items.length === 1) { open(items[0]); return; }
73
+ if (choices) { closeChoices(); return; }
74
+ setPhase('choosing');
75
+ choices = el('div', 'widget-choices'); choices.setAttribute('role', 'menu'); choices.setAttribute('aria-label', '会话小挂件列表');
76
+ for (const item of items) {
77
+ const row = button('', () => { closeChoices(false); open(item); }); row.setAttribute('role', 'menuitem');
78
+ row.append(el('span', '', item.title), el('span', 'widget-arrow', '›')); choices.append(row);
79
+ }
80
+ shortcut.append(choices); choices.firstChild?.focus();
33
81
  }, 'widget-pill');
82
+ pill.setAttribute('aria-label', title);
34
83
  const image = avatar(); image.classList.add('widget-pill-avatar');
35
- pill.append(image, el('span', 'widget-pill-title', items.length === 1 ? items[0].title : '全部小挂件'));
36
- if (items.length > 1) { pill.append(el('span', 'widget-chevron', '')); pill.setAttribute('aria-haspopup', 'menu'); pill.setAttribute('aria-expanded', 'false'); }
37
- shortcut.append(pill);
84
+ pill.append(image, el('span', 'widget-pill-title', title));
85
+ if (items.length > 1) { pill.append(el('span', 'widget-chevron', '')); pill.setAttribute('aria-haspopup', 'menu'); }
86
+ shortcut.append(pill); setPhase(choosing ? 'normal' : phase);
38
87
  }
39
88
  async function refresh(force = false) {
40
- if (!ready || loading || (!force && Date.now() - lastRefresh < 1500)) return;
89
+ if (!ready) return;
90
+ if (loading) return refreshPromise;
91
+ if (!force && Date.now() - lastRefresh < 1500) return;
41
92
  loading = true; const mine = generation;
42
93
  try {
43
- const value = await api('/api/simulator/widgets');
94
+ refreshPromise = api('/api/simulator/widgets');
95
+ const value = await refreshPromise;
44
96
  if (mine !== generation || !ready) return;
97
+ const previousIDs = items.map(item => item.id).join(',');
45
98
  items = value.widgets || []; failed = false;
46
- if (selected && !items.some(item => item.id === selected.id)) close();
99
+ documentLifecycle = value.capabilities?.documentLifecycle === true;
100
+ canDelete = value.capabilities?.deleteWidget === true;
101
+ if (selected && !items.some(item => item.id === selected.id)) close(true);
102
+ if (previousIDs !== items.map(item => item.id).join(',')) { setPhase('normal'); scheduleCollapse(); }
103
+ // Revoked ownership must also remove an already open owner menu.
104
+ if (selected && items.find(item => item.id === selected.id)?.role !== 'owner') {
105
+ overlay.querySelector('.widget-options')?.remove();
106
+ overlay.querySelector('.widget-confirm-backdrop')?.remove();
107
+ overlay.querySelector('.widget-native-share')?.remove();
108
+ const more = overlay.querySelector('.widget-more'); if (more) more.hidden = true;
109
+ }
47
110
  } catch { if (mine === generation) failed = true; }
48
111
  finally { if (mine === generation) { loading = false; lastRefresh = Date.now(); render(); } }
49
112
  }
113
+ function notify(text, error = false) {
114
+ overlay.querySelector('.widget-pin-note')?.remove();
115
+ const note = el('div', `widget-pin-note${error ? ' is-error' : ''}`, text); note.setAttribute('role', 'status'); overlay.append(note);
116
+ setTimeout(() => note.remove(), 4000);
117
+ }
118
+ function showConfirmation(card, item, mine, current) {
119
+ const shade = el('div', 'widget-confirm-backdrop');
120
+ const dialog = el('section', 'widget-confirm'); dialog.setAttribute('role', 'alertdialog'); dialog.setAttribute('aria-label', '删除小挂件?');
121
+ dialog.append(el('h4', '', '删除小挂件?'), el('p', '', '将删除小挂件和其中的数据,所有会话里的引用及首页固定都会失效。此操作不可恢复。'));
122
+ const actions = el('div', 'widget-confirm-actions');
123
+ const cancel = button('取消', () => { if (!busy) { shade.remove(); card.querySelector('.widget-more')?.focus(); } });
124
+ const remove = button('删除', async () => {
125
+ if (busy) return;
126
+ busy = true; cancel.disabled = true; remove.disabled = true; remove.textContent = '正在删除…';
127
+ try {
128
+ await api(`/api/simulator/widgets/${encodeURIComponent(item.id)}`, {method:'DELETE'});
129
+ if (mine !== openRevision || current !== generation) return;
130
+ items = items.filter(value => value.id !== item.id); render(); busy = false; close();
131
+ } catch {
132
+ if (mine !== openRevision || current !== generation) return;
133
+ shade.remove(); notify('操作失败,请稍后重试', true);
134
+ } finally { if (mine === openRevision && current === generation) busy = false; }
135
+ }, 'widget-destructive');
136
+ actions.append(cancel, remove); dialog.append(actions); shade.append(dialog); card.append(shade); cancel.focus();
137
+ }
138
+ function showShare(card) {
139
+ // Sharing chooses real conversations in the App; the simulator never fabricates recipients.
140
+ const notice = el('div', 'widget-native-share');
141
+ notice.append(el('h4', '', '在搭搭 App 中分享'), el('p', '', '使用真机预览打开这个搭子,在小挂件的「分享」中选择聊天。'));
142
+ notice.append(button('打开真机预览', () => { close(true); document.querySelector('#pair-device').click(); }), button('返回小挂件', () => notice.remove()));
143
+ card.append(notice); notice.querySelector('button').focus();
144
+ }
145
+ function ownerMenu(card, item, mine, current) {
146
+ const old = card.querySelector('.widget-options'); if (old) { old.remove(); return; }
147
+ const currentItem = items.find(value => value.id === item.id);
148
+ if (currentItem?.role !== 'owner' || busy) return;
149
+ const menu = el('div', 'widget-options'); menu.setAttribute('role', 'menu');
150
+ const share = button('分享', () => { menu.remove(); showShare(card); });
151
+ const pin = button(currentItem.isPinned ? '从首页取消固定' : '固定到首页', async () => {
152
+ if (busy) return; busy = true; menu.querySelectorAll('button').forEach(node => { node.disabled = true; });
153
+ try {
154
+ await api(`/api/simulator/widgets/${encodeURIComponent(item.id)}/pin`, {method:currentItem.isPinned ? 'DELETE' : 'PUT'});
155
+ if (mine !== openRevision || current !== generation) return;
156
+ currentItem.isPinned = !currentItem.isPinned; menu.remove(); notify(currentItem.isPinned ? '已固定到首页' : '已从首页取消固定');
157
+ } catch {
158
+ if (mine === openRevision && current === generation) { menu.remove(); notify('操作失败,请稍后重试', true); }
159
+ } finally { if (mine === openRevision && current === generation) busy = false; }
160
+ });
161
+ const remove = button('删除小挂件', () => { menu.remove(); showConfirmation(card, item, mine, current); }, 'widget-destructive');
162
+ share.prepend(menuIcon('share')); pin.prepend(menuIcon(currentItem.isPinned ? 'unpin' : 'pin')); remove.prepend(menuIcon('trash'));
163
+ for (const row of [share, pin, ...(canDelete ? [remove] : [])]) { row.setAttribute('role', 'menuitem'); menu.append(row); }
164
+ card.append(menu); share.focus();
165
+ }
50
166
  async function open(item) {
51
167
  if (!item.type || !ready) return;
52
168
  const mine = ++openRevision, current = generation;
53
169
  if (overlay.hidden) focusBefore = document.activeElement;
54
- frameObserver?.disconnect();
170
+ clearTimeout(collapseTimer); clearTimeout(closeTimer); closeChoices(false); timers(); busy = false;
55
171
  selected = item; document.querySelector('#message-input').blur();
56
- overlay.replaceChildren(); overlay.hidden = false;
172
+ overlay.replaceChildren(); overlay.hidden = false; overlay.dataset.state = 'opening';
57
173
  const card = el('section', 'widget-modal'); card.setAttribute('role', 'dialog'); card.setAttribute('aria-modal', 'true'); card.setAttribute('aria-label', item.title);
58
174
  const header = el('header', 'widget-modal-header');
59
- const dismiss = button('×', close); dismiss.setAttribute('aria-label', '关闭小挂件');
60
- const more = button('···', () => {
61
- const old = card.querySelector('.widget-options'); if (old) { old.remove(); return; }
62
- const menu = el('div', 'widget-options'); menu.setAttribute('role', 'menu');
63
- const currentItem = items.find(value => value.id === item.id);
64
- if (currentItem) menu.append(button(currentItem.isPinned ? '从首页取消固定' : '固定到首页', async () => {
65
- menu.querySelectorAll('button').forEach(b => { b.disabled = true; });
66
- try {
67
- await api(`/api/simulator/widgets/${encodeURIComponent(item.id)}/pin`, { method: currentItem.isPinned ? 'DELETE' : 'PUT' });
68
- if (mine !== openRevision || current !== generation) return;
69
- currentItem.isPinned = !currentItem.isPinned; menu.remove();
70
- const note = el('div', 'widget-pin-note', currentItem.isPinned ? '已固定到首页' : '已从首页取消固定'); note.setAttribute('role', 'status'); card.append(note); setTimeout(() => note.remove(), 4000);
71
- } catch (error) { if (mine === openRevision) { menu.replaceChildren(el('p', '', error.message), button('知道了', () => menu.remove())); } }
72
- }));
73
- menu.append(button('刷新', () => open(item))); card.append(menu);
74
- }); more.setAttribute('aria-label', '小挂件更多');
75
- header.append(dismiss, el('h3', '', item.title), more);
76
- const body = el('div', 'widget-modal-body'); body.append(el('p', 'widget-loading', '正在打开小挂件…'));
77
- card.append(header, body); overlay.append(card);
175
+ const dismiss = button('×', () => close()); dismiss.setAttribute('aria-label', '关闭小挂件');
176
+ const more = button('···', () => ownerMenu(card, item, mine, current), 'widget-more'); more.setAttribute('aria-label', '小挂件更多'); more.hidden = true;
177
+ const slot = el('div', 'widget-more-slot'); slot.append(more);
178
+ header.append(dismiss, el('h3', '', item.title), slot);
179
+ const body = el('div', 'widget-modal-body'); body.setAttribute('aria-busy', 'true');
180
+ const loadingView = el('div', 'widget-loading'); loadingView.setAttribute('role', 'status');
181
+ loadingView.append(el('span', 'widget-spinner'), el('p', '', '正在打开小挂件…'));
182
+ body.append(loadingView); card.append(header, body); overlay.append(card);
78
183
  for (const child of phone.children) if (child !== overlay) child.inert = true;
79
184
  dismiss.focus();
80
- try {
81
- const url = previewUrl(`/api/simulator/widgets/${encodeURIComponent(item.id)}`);
82
- // Probe errors in the parent; actual HTML executes only inside the opaque sandbox document.
83
- const response = await fetch(url, { credentials: 'same-origin' });
84
- if (!response.ok) { const error = await response.json(); throw new Error(error.error?.message || error.error || '小挂件暂时无法打开'); }
85
- await response.arrayBuffer();
185
+ const failedLoad = () => {
86
186
  if (mine !== openRevision || current !== generation) return;
87
- const frame = el('iframe', 'widget-document'); frame.title = item.title; frame.setAttribute('sandbox', 'allow-scripts'); frame.referrerPolicy = 'no-referrer'; frame.src = url;
88
- body.replaceChildren(frame);
89
- frameObserver = new ResizeObserver(() => {
90
- const scale = body.clientWidth / 383;
91
- if (!scale) return;
92
- frame.style.width = '383px'; frame.style.height = `${body.clientHeight / scale}px`;
93
- frame.style.transform = `scale(${scale})`; frame.style.transformOrigin = 'top left';
94
- });
95
- frameObserver.observe(body);
96
- } catch (error) {
187
+ timers(); body.setAttribute('aria-busy', 'false');
188
+ const failure = el('div', 'widget-failure'); failure.setAttribute('role', 'status');
189
+ failure.append(el('span', 'widget-failure-mark', '!'), el('h4', '', '小挂件暂时无法打开'), el('p', '', '加载失败,请稍后重试'), button('重新加载', () => open(item)));
190
+ body.replaceChildren(failure);
191
+ };
192
+ const frame = el('iframe', 'widget-document'); frame.title = item.title; frame.setAttribute('sandbox', 'allow-scripts'); frame.referrerPolicy = 'no-referrer'; frame.style.visibility = 'hidden';
193
+ documentFrame = frame; documentToken = crypto.randomUUID();
194
+ const url = new URL(window.previewApiUrl?.(`/api/simulator/widgets/${encodeURIComponent(item.id)}`) ?? `/api/simulator/widgets/${encodeURIComponent(item.id)}`, location.href);
195
+ url.searchParams.set('render', documentToken); frame.src = url.href;
196
+ documentReady = state => {
97
197
  if (mine !== openRevision || current !== generation) return;
98
- body.replaceChildren(el('h4', '', '小挂件暂时无法打开'), el('p', '', error.message), button('重新加载', () => open(item)));
99
- }
198
+ if (state !== 'ready') { failedLoad(); return; }
199
+ clearTimeout(loadTimer); loadingView.remove(); frame.style.visibility = 'visible'; body.setAttribute('aria-busy', 'false');
200
+ };
201
+ frame.addEventListener('error', failedLoad);
202
+ // Older CLI bridges cannot send lifecycle signals. Keep their document load usable.
203
+ frame.addEventListener('load', () => { if (!documentLifecycle) documentReady?.('ready'); });
204
+ body.append(frame);
205
+ frameObserver = new ResizeObserver(() => {
206
+ const factor = scale(); if (!factor) return;
207
+ frame.style.width = `${body.clientWidth / factor}px`; frame.style.height = `${body.clientHeight / factor}px`;
208
+ frame.style.transform = `scale(${factor})`; frame.style.transformOrigin = 'top left';
209
+ }); frameObserver.observe(body);
210
+ loadTimer = setTimeout(failedLoad, 15000);
211
+ await refresh(true);
212
+ if (mine === openRevision && current === generation) more.hidden = failed || items.find(value => value.id === item.id)?.role !== 'owner';
100
213
  }
214
+ window.addEventListener('message', event => {
215
+ if (!documentFrame || event.source !== documentFrame.contentWindow || event.data?.channel !== 'buddy-widget-document' || event.data?.render !== documentToken) return;
216
+ documentReady?.(event.data.state);
217
+ });
101
218
  overlay.addEventListener('click', event => { if (event.target === overlay) close(); });
219
+ document.addEventListener('pointerdown', event => {
220
+ if (choices && !shortcut.contains(event.target)) closeChoices();
221
+ const menu = overlay.querySelector('.widget-options');
222
+ if (menu && !menu.contains(event.target) && !event.target.closest('.widget-more')) menu.remove();
223
+ });
102
224
  document.addEventListener('keydown', event => {
103
- if (event.key === 'Escape') { if (!overlay.hidden) close(); else { shortcut.querySelector('.widget-choices')?.remove(); shortcut.querySelector('button')?.setAttribute('aria-expanded', 'false'); } }
225
+ if (event.key === 'Escape') {
226
+ if (busy) return;
227
+ const nested = overlay.querySelector('.widget-confirm-backdrop,.widget-native-share,.widget-options');
228
+ if (nested) { nested.remove(); overlay.querySelector('.widget-more')?.focus(); } else if (!overlay.hidden) close(); else closeChoices();
229
+ }
104
230
  if (event.key === 'Tab' && !overlay.hidden) {
105
- const controls = [...overlay.querySelectorAll('button:not(:disabled), iframe')]; const first = controls[0], last = controls.at(-1);
231
+ const scope = overlay.querySelector('.widget-confirm,.widget-native-share') || overlay;
232
+ const controls = [...scope.querySelectorAll('button:not(:disabled), iframe')].filter(node => !node.hidden && node.getClientRects().length);
233
+ const first = controls[0], last = controls.at(-1);
106
234
  if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus(); }
107
235
  else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus(); }
108
236
  }
109
237
  });
110
- document.addEventListener('visibilitychange', () => { if (!document.hidden) void refresh(true); });
238
+ document.addEventListener('visibilitychange', () => {
239
+ clearTimeout(collapseTimer); closeChoices(false);
240
+ if (!document.hidden) { setPhase('normal'); scheduleCollapse(); void refresh(true); }
241
+ });
242
+ window.addEventListener('pagehide', () => { resize.disconnect(); timers(); clearTimeout(collapseTimer); clearTimeout(closeTimer); });
111
243
  return {
112
- update({ active, conversationRevision, devRevision }) {
244
+ update({active, conversationRevision, devRevision, reloading = false}) {
113
245
  const key = `${conversationRevision || 0}:${devRevision || 0}`;
114
- if (key !== scope || !active) { scope = key; generation++; items = []; failed = false; loading = false; lastRefresh = 0; close(); }
115
- ready = active; render(); void refresh();
246
+ if (conversationScope !== conversationRevision || (!active && !reloading)) reopenId = undefined;
247
+ else if (selected && (key !== scope || reloading)) reopenId = selected.id;
248
+ conversationScope = conversationRevision;
249
+ if (key !== scope || !active) {
250
+ scope = key; generation++; ready = false; items = []; failed = false; loading = false; lastRefresh = 0; busy = false; documentLifecycle = false; canDelete = false;
251
+ clearTimeout(collapseTimer); close(true);
252
+ }
253
+ ready = active; render();
254
+ const mine = generation;
255
+ void refresh().then(() => {
256
+ if (!ready || mine !== generation || !reopenId) return;
257
+ const item = items.find(value => value.id === reopenId);
258
+ reopenId = undefined;
259
+ if (item) void open(item);
260
+ });
116
261
  },
117
- reset() { generation++; ready = false; items = []; loading = false; failed = false; close(); render(); },
262
+ reset() { reopenId = undefined; generation++; ready = false; items = []; loading = false; failed = false; busy = false; clearTimeout(collapseTimer); close(true); render(); },
118
263
  card(item) {
119
264
  const card = el('div', 'widget-entry'); card.append(el('h4', '', item.title));
120
265
  const details = el('div', 'widget-entry-details');
121
- if (item.type && item.imageMediaId) { const image = el('img', 'widget-entry-photo'); image.alt = `${item.title}的照片`; image.src = previewUrl(`/api/simulator/widgets/${encodeURIComponent(item.id)}/image?mediaId=${encodeURIComponent(item.imageMediaId)}`); image.addEventListener('error', () => { image.hidden = true; }); details.append(image); }
122
- details.append(el('p', '', item.type ? item.summary?.trim() || '打开查看最新内容' : '此小挂件已删除或不可用')); card.append(details);
266
+ if (item.type && item.imageMediaId) {
267
+ const image = el('img', 'widget-entry-photo'); image.alt = `${item.title}的照片`;
268
+ image.src = window.previewApiUrl?.(`/api/simulator/widgets/${encodeURIComponent(item.id)}/image?mediaId=${encodeURIComponent(item.imageMediaId)}`) ?? `/api/simulator/widgets/${encodeURIComponent(item.id)}/image?mediaId=${encodeURIComponent(item.imageMediaId)}`;
269
+ image.addEventListener('error', () => { image.hidden = true; }); details.append(image);
270
+ }
271
+ details.append(el('p', item.type && item.summary?.trim() ? '' : 'widget-secondary', item.type ? item.summary?.trim() || '打开查看最新内容' : '此小挂件已删除或不可用')); card.append(details);
123
272
  if (item.type) card.append(button('去查看', () => open(item), 'widget-entry-open'));
273
+ const permission = {owner:'可管理、分享',editor:'可编辑',viewer:'仅支持查看'}[item.role];
274
+ const metadata = el('div', 'widget-entry-metadata');
275
+ if (item.creatorName?.trim()) metadata.append(el('span', 'widget-creator', `${item.creatorName.trim()}创建`));
276
+ if (permission) metadata.append(el('span', 'widget-permission', (metadata.children.length ? '|' : '') + permission));
277
+ if (metadata.children.length) card.append(metadata);
124
278
  return card;
125
279
  },
126
280
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@my-life-buddies/cli",
3
- "version": "0.14.1",
3
+ "version": "0.15.0",
4
4
  "type": "module",
5
5
  "description": "搭搭 Developer Platform 命令行工具",
6
6
  "publishConfig": {
@@ -39,7 +39,7 @@
39
39
  "ws": "8.21.3"
40
40
  },
41
41
  "devDependencies": {
42
- "@buddy/cli-core": "0.14.1",
43
- "@buddy/preview": "0.14.1"
42
+ "@buddy/cli-core": "0.15.0",
43
+ "@buddy/preview": "0.15.0"
44
44
  }
45
45
  }