synabun 2.0.9 → 2.1.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 (679) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/README.md +25 -2
  3. package/THIRD-PARTY-LICENSES.md +68 -4
  4. package/bin/synabun.js +71 -24
  5. package/docs/whatsapp.md +2 -2
  6. package/hooks/claude-code/pre-websearch.mjs +12 -1
  7. package/hooks/claude-code/shared.mjs +5 -0
  8. package/hooks/claude-code/stop.mjs +1 -1
  9. package/launcher.mjs +9 -3
  10. package/lib/claude-hooks.js +172 -18
  11. package/lib/client-config-repair.js +17 -8
  12. package/lib/desktop-app.js +7 -1
  13. package/lib/external-tools.js +1 -1
  14. package/lib/first-launch-protection.js +57 -12
  15. package/lib/mcp-dist.js +176 -0
  16. package/lib/release-npm-ci.mjs +138 -0
  17. package/lib/release-publish.mjs +91 -0
  18. package/lib/release-scan.mjs +50 -0
  19. package/lib/server-crash-record.js +123 -0
  20. package/lib/server-exit.js +318 -0
  21. package/lib/server-supervisor.js +508 -0
  22. package/lib/start-launcher.js +43 -5
  23. package/lib/tools-cli.js +45 -0
  24. package/lib/update-snapshot.js +228 -0
  25. package/lib/virustotal.mjs +192 -0
  26. package/lib/windows-crash-event.js +269 -0
  27. package/mcp-server/dist/http.d.ts +20 -1
  28. package/mcp-server/dist/http.d.ts.map +1 -1
  29. package/mcp-server/dist/http.js +59 -12
  30. package/mcp-server/dist/http.js.map +1 -1
  31. package/mcp-server/dist/index.d.ts +33 -27
  32. package/mcp-server/dist/index.d.ts.map +1 -1
  33. package/mcp-server/dist/index.js +128 -143
  34. package/mcp-server/dist/index.js.map +1 -1
  35. package/mcp-server/dist/preload.js +1 -1
  36. package/mcp-server/dist/preload.js.map +1 -1
  37. package/mcp-server/dist/services/codex-config-heal.d.ts +6 -2
  38. package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
  39. package/mcp-server/dist/services/codex-config-heal.js +81 -28
  40. package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
  41. package/mcp-server/dist/services/memory-judge-backfill.d.ts +6 -0
  42. package/mcp-server/dist/services/memory-judge-backfill.d.ts.map +1 -1
  43. package/mcp-server/dist/services/memory-judge-backfill.js +7 -1
  44. package/mcp-server/dist/services/memory-judge-backfill.js.map +1 -1
  45. package/mcp-server/dist/services/neural-interface.d.ts +16 -10
  46. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  47. package/mcp-server/dist/services/neural-interface.js +67 -11
  48. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  49. package/mcp-server/dist/services/server-instructions.d.ts +7 -2
  50. package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
  51. package/mcp-server/dist/services/server-instructions.js +20 -62
  52. package/mcp-server/dist/services/server-instructions.js.map +1 -1
  53. package/mcp-server/dist/services/tool-catalog.d.ts +120 -0
  54. package/mcp-server/dist/services/tool-catalog.d.ts.map +1 -0
  55. package/mcp-server/dist/services/tool-catalog.js +339 -0
  56. package/mcp-server/dist/services/tool-catalog.js.map +1 -0
  57. package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts +12 -0
  58. package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts.map +1 -0
  59. package/mcp-server/dist/services/tool-list-refresh-barrier.js +40 -0
  60. package/mcp-server/dist/services/tool-list-refresh-barrier.js.map +1 -0
  61. package/mcp-server/dist/services/tool-list.d.ts +108 -0
  62. package/mcp-server/dist/services/tool-list.d.ts.map +1 -0
  63. package/mcp-server/dist/services/tool-list.js +334 -0
  64. package/mcp-server/dist/services/tool-list.js.map +1 -0
  65. package/mcp-server/dist/services/tool-runtime.d.ts +161 -0
  66. package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -0
  67. package/mcp-server/dist/services/tool-runtime.js +400 -0
  68. package/mcp-server/dist/services/tool-runtime.js.map +1 -0
  69. package/mcp-server/dist/services/whiteboard-geometry.d.ts +26 -2
  70. package/mcp-server/dist/services/whiteboard-geometry.d.ts.map +1 -1
  71. package/mcp-server/dist/services/whiteboard-geometry.js +91 -8
  72. package/mcp-server/dist/services/whiteboard-geometry.js.map +1 -1
  73. package/mcp-server/dist/tools/agents.d.ts +10 -10
  74. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  75. package/mcp-server/dist/tools/agents.js +22 -13
  76. package/mcp-server/dist/tools/agents.js.map +1 -1
  77. package/mcp-server/dist/tools/browser-batch.d.ts +3 -3
  78. package/mcp-server/dist/tools/image-tools.d.ts +1 -1
  79. package/mcp-server/dist/tools/image-tools.js +1 -1
  80. package/mcp-server/dist/tools/skill-tools.d.ts +505 -0
  81. package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -0
  82. package/mcp-server/dist/tools/skill-tools.js +639 -0
  83. package/mcp-server/dist/tools/skill-tools.js.map +1 -0
  84. package/mcp-server/dist/tools/skill.d.ts +7 -0
  85. package/mcp-server/dist/tools/skill.d.ts.map +1 -0
  86. package/mcp-server/dist/tools/skill.js +11 -0
  87. package/mcp-server/dist/tools/skill.js.map +1 -0
  88. package/mcp-server/dist/tools/tool-groups.d.ts +68 -0
  89. package/mcp-server/dist/tools/tool-groups.d.ts.map +1 -0
  90. package/mcp-server/dist/tools/tool-groups.js +147 -0
  91. package/mcp-server/dist/tools/tool-groups.js.map +1 -0
  92. package/mcp-server/dist/tools/whiteboard-format.d.ts.map +1 -1
  93. package/mcp-server/dist/tools/whiteboard-format.js +7 -4
  94. package/mcp-server/dist/tools/whiteboard-format.js.map +1 -1
  95. package/mcp-server/dist/tools/whiteboard-tools.d.ts +64 -13
  96. package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -1
  97. package/mcp-server/dist/tools/whiteboard-tools.js +30 -5
  98. package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -1
  99. package/mcp-server/package-lock.json +2 -2
  100. package/mcp-server/package.json +1 -1
  101. package/mcp-server/src/http.ts +75 -12
  102. package/mcp-server/src/index.ts +147 -154
  103. package/mcp-server/src/preload.ts +12 -12
  104. package/mcp-server/src/services/codex-config-heal.ts +80 -29
  105. package/mcp-server/src/services/memory-judge-backfill.ts +7 -1
  106. package/mcp-server/src/services/neural-interface.ts +79 -20
  107. package/mcp-server/src/services/server-instructions.ts +27 -63
  108. package/mcp-server/src/services/tool-catalog.ts +403 -0
  109. package/mcp-server/src/services/tool-list-refresh-barrier.ts +33 -0
  110. package/mcp-server/src/services/tool-list.ts +410 -0
  111. package/mcp-server/src/services/tool-runtime.ts +457 -0
  112. package/mcp-server/src/services/whiteboard-geometry.ts +94 -9
  113. package/mcp-server/src/tools/agents.ts +22 -14
  114. package/mcp-server/src/tools/image-tools.ts +1 -1
  115. package/mcp-server/src/tools/skill-tools.ts +589 -0
  116. package/mcp-server/src/tools/skill.ts +13 -0
  117. package/mcp-server/src/tools/tool-groups.ts +164 -0
  118. package/mcp-server/src/tools/whiteboard-format.ts +9 -3
  119. package/mcp-server/src/tools/whiteboard-tools.ts +39 -5
  120. package/neural-interface/i18n/en.json +3680 -206
  121. package/neural-interface/i18n/pt-BR.json +3628 -157
  122. package/neural-interface/lib/account-usage.js +5 -3
  123. package/neural-interface/lib/app-controls-api.js +161 -0
  124. package/neural-interface/lib/assistant-api.js +148 -35
  125. package/neural-interface/lib/assistant-brains/claude.js +4 -3
  126. package/neural-interface/lib/assistant-brains/codex-browser-policy-hook.mjs +12 -2
  127. package/neural-interface/lib/assistant-brains/codex-route-gate-hook.mjs +32 -9
  128. package/neural-interface/lib/assistant-brains/codex.js +9 -5
  129. package/neural-interface/lib/assistant-brains/opencode.js +122 -44
  130. package/neural-interface/lib/assistant-brains/opencode2-browser-policy/index.js +59 -0
  131. package/neural-interface/lib/assistant-brains/opencode2-route-gate/index.js +97 -0
  132. package/neural-interface/lib/assistant-catalog.js +23 -9
  133. package/neural-interface/lib/assistant-dispatch.js +874 -156
  134. package/neural-interface/lib/assistant-envelope.js +22 -2
  135. package/neural-interface/lib/assistant-persona.js +80 -11
  136. package/neural-interface/lib/assistant-plan-permissions.js +10 -9
  137. package/neural-interface/lib/assistant-route-gate-codex.js +3 -0
  138. package/neural-interface/lib/assistant-route-gate.js +6 -4
  139. package/neural-interface/lib/assistant-router.js +13 -8
  140. package/neural-interface/lib/assistant-runtime.js +183 -26
  141. package/neural-interface/lib/assistant-task-prompt.js +78 -14
  142. package/neural-interface/lib/assistant-tools.js +51 -0
  143. package/neural-interface/lib/backup-service.js +620 -3
  144. package/neural-interface/lib/backup-zip-reader.js +10 -5
  145. package/neural-interface/lib/backup-zip-writer.js +257 -0
  146. package/neural-interface/lib/browser-execution.js +10 -2
  147. package/neural-interface/lib/browser-guard-cdp.js +83 -0
  148. package/neural-interface/lib/browser-network-guard.js +284 -0
  149. package/neural-interface/lib/browser-policy-roots.js +64 -0
  150. package/neural-interface/lib/browser-tool-policy.js +186 -31
  151. package/neural-interface/lib/claude-agent-bridge.js +4 -2
  152. package/neural-interface/lib/claude-hooks-status.js +103 -0
  153. package/neural-interface/lib/claude-model-catalog.js +64 -14
  154. package/neural-interface/lib/claude-temporary.js +5 -11
  155. package/neural-interface/lib/cli-install.js +18 -138
  156. package/neural-interface/lib/cli-resolver.js +4 -1
  157. package/neural-interface/lib/cli-update-planner.js +32 -31
  158. package/neural-interface/lib/cloudflared/tunnel.js +42 -2
  159. package/neural-interface/lib/codex-mcp-approvals.js +11 -0
  160. package/neural-interface/lib/codex-settings-schema.js +109 -47
  161. package/neural-interface/lib/codex-worker-app-server.js +172 -0
  162. package/neural-interface/lib/confined-fs.js +30 -3
  163. package/neural-interface/lib/crash-notice-api.js +98 -0
  164. package/neural-interface/lib/editor-completion-opencode.js +164 -67
  165. package/neural-interface/lib/exec-loop-command.js +4 -1
  166. package/neural-interface/lib/http-guards.js +8 -2
  167. package/neural-interface/lib/install-details.js +4 -1
  168. package/neural-interface/lib/launch-cache.js +293 -0
  169. package/neural-interface/lib/login-start.js +524 -0
  170. package/neural-interface/lib/mcp-agent-servers.js +67 -0
  171. package/neural-interface/lib/mcp-client-config.js +0 -29
  172. package/neural-interface/lib/mcp-external-servers.js +323 -0
  173. package/neural-interface/lib/{mcp-profile-refresh-coordinator.js → mcp-tools-refresh-coordinator.js} +4 -4
  174. package/neural-interface/lib/morelogin.js +30 -0
  175. package/neural-interface/lib/native-loop-providers.js +565 -50
  176. package/neural-interface/lib/native-loop-runtime.js +287 -41
  177. package/neural-interface/lib/opencode-config.js +557 -0
  178. package/neural-interface/lib/opencode-db-schema.js +58 -0
  179. package/neural-interface/lib/opencode-history.js +49 -204
  180. package/neural-interface/lib/opencode-mcp-ready.js +108 -0
  181. package/neural-interface/lib/opencode-native-boundary.js +44 -0
  182. package/neural-interface/lib/opencode-protocol.js +310 -0
  183. package/neural-interface/lib/opencode-provider-view.js +81 -0
  184. package/neural-interface/lib/opencode-runtime-config.js +32 -15
  185. package/neural-interface/lib/opencode-serve-records.js +624 -0
  186. package/neural-interface/lib/opencode-v2-client.js +53 -131
  187. package/neural-interface/lib/opencode-v2-ws-requests.js +35 -195
  188. package/neural-interface/lib/opencode-worker-activity.js +229 -0
  189. package/neural-interface/lib/opencode-worker-data.js +27 -0
  190. package/neural-interface/lib/opencode2-client.js +796 -0
  191. package/neural-interface/lib/opencode2-proxy.js +76 -0
  192. package/neural-interface/lib/opencode2-translate.js +1057 -0
  193. package/neural-interface/lib/owner-authority.js +249 -0
  194. package/neural-interface/lib/page-session.js +275 -0
  195. package/neural-interface/lib/peer-identity.js +763 -0
  196. package/neural-interface/lib/process-identity.js +361 -0
  197. package/neural-interface/lib/request-gate.js +17 -2
  198. package/neural-interface/lib/rulesets/api.js +1 -1
  199. package/neural-interface/lib/rulesets/fs-safe.js +1 -1
  200. package/neural-interface/lib/rulesets/installer.js +92 -27
  201. package/neural-interface/lib/rulesets/json-edit.js +1 -1
  202. package/neural-interface/lib/session-cache-builder.js +18 -10
  203. package/neural-interface/lib/session-title-generator.js +58 -18
  204. package/neural-interface/lib/skills/api-v2.js +495 -0
  205. package/neural-interface/lib/skills/bundle.js +418 -0
  206. package/neural-interface/lib/skills/catalog.js +294 -0
  207. package/neural-interface/lib/skills/creation-api.js +191 -0
  208. package/neural-interface/lib/skills/creation-brief.js +247 -0
  209. package/neural-interface/lib/skills/creation-sources.js +267 -0
  210. package/neural-interface/lib/skills/creation.js +843 -0
  211. package/neural-interface/lib/skills/deploy.js +564 -0
  212. package/neural-interface/lib/skills/digest.js +119 -0
  213. package/neural-interface/lib/skills/discover.js +111 -0
  214. package/neural-interface/lib/skills/draft-confinement.js +214 -0
  215. package/neural-interface/lib/skills/eval-confinement.js +397 -0
  216. package/neural-interface/lib/skills/eval-sandbox.js +280 -0
  217. package/neural-interface/lib/skills/eval-trace.js +119 -0
  218. package/neural-interface/lib/skills/evals-api.js +212 -0
  219. package/neural-interface/lib/skills/evals.js +1573 -0
  220. package/neural-interface/lib/skills/export.js +200 -0
  221. package/neural-interface/lib/skills/frontmatter.js +64 -0
  222. package/neural-interface/lib/skills/health.js +75 -0
  223. package/neural-interface/lib/skills/json-edit.js +165 -0
  224. package/neural-interface/lib/skills/known-bridge-digests.json +387 -0
  225. package/neural-interface/lib/skills/known-bridges.js +96 -0
  226. package/neural-interface/lib/skills/library.js +823 -0
  227. package/neural-interface/lib/skills/local-store.js +21 -0
  228. package/neural-interface/lib/skills/migration.js +433 -0
  229. package/neural-interface/lib/skills/operations.js +45 -0
  230. package/neural-interface/lib/skills/ownership.js +427 -0
  231. package/neural-interface/lib/skills/packages.js +88 -0
  232. package/neural-interface/lib/skills/reviews.js +212 -0
  233. package/neural-interface/lib/skills/revision.js +80 -0
  234. package/neural-interface/lib/skills/roots.js +262 -0
  235. package/neural-interface/lib/skills/runtime-checks.js +355 -0
  236. package/neural-interface/lib/skills/scan-assets.js +252 -0
  237. package/neural-interface/lib/skills/scan-context.js +26 -0
  238. package/neural-interface/lib/skills/scan-shell.js +324 -0
  239. package/neural-interface/lib/skills/scan.js +364 -0
  240. package/neural-interface/lib/skills/scanners.js +54 -0
  241. package/neural-interface/lib/skills/service.js +522 -0
  242. package/neural-interface/lib/skills/telemetry.js +113 -0
  243. package/neural-interface/lib/sqlite-integrity-worker.js +22 -0
  244. package/neural-interface/lib/style-guide/api.js +4 -3
  245. package/neural-interface/lib/tool-lists.js +124 -0
  246. package/neural-interface/lib/untrusted-file-headers.js +27 -0
  247. package/neural-interface/lib/whatsapp/bridge.js +6 -3
  248. package/neural-interface/lib/worker-confinement.js +1192 -0
  249. package/neural-interface/lib/worker-guard-setting.js +56 -0
  250. package/neural-interface/lib/worker-mcp-channel.js +175 -0
  251. package/neural-interface/lib/worker-relay-ports.js +227 -0
  252. package/neural-interface/lib/worker-resume.js +229 -0
  253. package/neural-interface/lib/worker-sandbox.js +299 -0
  254. package/neural-interface/package-lock.json +1354 -50
  255. package/neural-interface/package.json +6 -2
  256. package/neural-interface/public/index.html +5 -3
  257. package/neural-interface/public/index2d.html +19 -11
  258. package/neural-interface/public/onboarding.html +2 -2
  259. package/neural-interface/public/shared/account-menu.js +8 -5
  260. package/neural-interface/public/shared/agent-runtime-options.js +76 -16
  261. package/neural-interface/public/shared/api.js +1527 -1336
  262. package/neural-interface/public/shared/app-frame.js +192 -0
  263. package/neural-interface/public/shared/assistant/asst-agents-dock.js +205 -19
  264. package/neural-interface/public/shared/assistant/asst-api.js +11 -3
  265. package/neural-interface/public/shared/assistant/asst-ask-styles.js +270 -0
  266. package/neural-interface/public/shared/assistant/asst-ask-tail.js +118 -0
  267. package/neural-interface/public/shared/assistant/asst-ask.js +397 -0
  268. package/neural-interface/public/shared/assistant/asst-brain-picker.js +241 -112
  269. package/neural-interface/public/shared/assistant/asst-composer.js +1 -1
  270. package/neural-interface/public/shared/assistant/asst-control.js +157 -26
  271. package/neural-interface/public/shared/assistant/asst-followups-card.js +383 -0
  272. package/neural-interface/public/shared/assistant/asst-followups.js +417 -0
  273. package/neural-interface/public/shared/assistant/asst-hosts.js +49 -2
  274. package/neural-interface/public/shared/assistant/asst-limit-dock.js +40 -0
  275. package/neural-interface/public/shared/assistant/asst-limit-wait.js +294 -0
  276. package/neural-interface/public/shared/assistant/asst-limit.js +168 -0
  277. package/neural-interface/public/shared/assistant/asst-mascot.js +49 -12
  278. package/neural-interface/public/shared/assistant/asst-menu.js +84 -12
  279. package/neural-interface/public/shared/assistant/asst-panel.js +303 -44
  280. package/neural-interface/public/shared/assistant/asst-rack-styles.js +93 -58
  281. package/neural-interface/public/shared/assistant/asst-redact.js +186 -0
  282. package/neural-interface/public/shared/assistant/asst-render.js +770 -100
  283. package/neural-interface/public/shared/assistant/asst-route.js +83 -12
  284. package/neural-interface/public/shared/assistant/asst-run-stage.js +231 -0
  285. package/neural-interface/public/shared/assistant/asst-sanitize.js +193 -3
  286. package/neural-interface/public/shared/assistant/asst-scene-styles.js +273 -0
  287. package/neural-interface/public/shared/assistant/asst-scenes.js +530 -0
  288. package/neural-interface/public/shared/assistant/asst-sidepanel-styles.js +3 -15
  289. package/neural-interface/public/shared/assistant/asst-sidepanel.js +67 -4
  290. package/neural-interface/public/shared/assistant/asst-slash.js +9 -7
  291. package/neural-interface/public/shared/assistant/asst-state.js +25 -15
  292. package/neural-interface/public/shared/assistant/asst-stop.js +127 -0
  293. package/neural-interface/public/shared/assistant/asst-styles.js +102 -21
  294. package/neural-interface/public/shared/assistant/asst-tool-kinds.js +165 -13
  295. package/neural-interface/public/shared/automation-context.js +5 -2
  296. package/neural-interface/public/shared/cc-dropdown.js +550 -0
  297. package/neural-interface/public/shared/cdx/cdx-context-menu.js +10 -9
  298. package/neural-interface/public/shared/cdx/cdx-copy.js +16 -0
  299. package/neural-interface/public/shared/cdx/cdx-output-model.js +22 -1
  300. package/neural-interface/public/shared/cdx/cdx-output.js +8 -7
  301. package/neural-interface/public/shared/cdx/cdx-panel.js +58 -150
  302. package/neural-interface/public/shared/cdx/cdx-protocol.js +28 -1
  303. package/neural-interface/public/shared/cdx/cdx-render.js +62 -32
  304. package/neural-interface/public/shared/cdx/cdx-requests.js +3 -3
  305. package/neural-interface/public/shared/cdx/cdx-skill-hints.js +54 -0
  306. package/neural-interface/public/shared/cdx/cdx-styles.js +15 -3
  307. package/neural-interface/public/shared/cdx/cdx-tabs.js +201 -74
  308. package/neural-interface/public/shared/cdx/cdx-ws.js +0 -1
  309. package/neural-interface/public/shared/cli-install-view.js +4 -8
  310. package/neural-interface/public/shared/cli-status.js +2 -2
  311. package/neural-interface/public/shared/constants.js +2 -2
  312. package/neural-interface/public/shared/context-menu-areas.js +34 -0
  313. package/neural-interface/public/shared/context-menu-logic.js +243 -0
  314. package/neural-interface/public/shared/context-menu-model.js +366 -0
  315. package/neural-interface/public/shared/context-menu.js +372 -0
  316. package/neural-interface/public/shared/cp/cp-context-menu.js +10 -9
  317. package/neural-interface/public/shared/cp/cp-markdown.js +4 -57
  318. package/neural-interface/public/shared/crash-notice-view.js +168 -0
  319. package/neural-interface/public/shared/data-cache.js +223 -0
  320. package/neural-interface/public/shared/desktop-bridge.js +11 -0
  321. package/neural-interface/public/shared/desktop-titlebar-view.js +78 -1
  322. package/neural-interface/public/shared/desktop-titlebar.js +214 -28
  323. package/neural-interface/public/shared/desktop-update-view.js +35 -5
  324. package/neural-interface/public/shared/editor/editor-panel-logic.js +2 -2
  325. package/neural-interface/public/shared/file-links.js +73 -8
  326. package/neural-interface/public/shared/html-shell.js +78 -86
  327. package/neural-interface/public/shared/i18n.js +2 -0
  328. package/neural-interface/public/shared/launch-prefetch.js +59 -0
  329. package/neural-interface/public/shared/ocp-v2/ocp-v2-approvals.js +7 -7
  330. package/neural-interface/public/shared/ocp-v2/ocp-v2-binding.js +4 -4
  331. package/neural-interface/public/shared/ocp-v2/ocp-v2-caps.js +3 -2
  332. package/neural-interface/public/shared/ocp-v2/ocp-v2-changes-logic.js +3 -101
  333. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +9 -2
  334. package/neural-interface/public/shared/ocp-v2/ocp-v2-compact-button.js +1 -3
  335. package/neural-interface/public/shared/ocp-v2/ocp-v2-composer-logic.js +2 -6
  336. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-menu.js +11 -11
  337. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-model.js +0 -3
  338. package/neural-interface/public/shared/ocp-v2/ocp-v2-events.js +8 -4
  339. package/neural-interface/public/shared/ocp-v2/ocp-v2-manager.js +7 -7
  340. package/neural-interface/public/shared/ocp-v2/ocp-v2-modelpicker.js +54 -17
  341. package/neural-interface/public/shared/ocp-v2/ocp-v2-outcome-notifications.js +16 -0
  342. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +140 -254
  343. package/neural-interface/public/shared/ocp-v2/ocp-v2-plan.js +1 -2
  344. package/neural-interface/public/shared/ocp-v2/ocp-v2-projectbar.js +97 -203
  345. package/neural-interface/public/shared/ocp-v2/ocp-v2-providers.js +11 -9
  346. package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +15 -4
  347. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +7 -22
  348. package/neural-interface/public/shared/ocp-v2/ocp-v2-send-logic.js +401 -0
  349. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +20 -13
  350. package/neural-interface/public/shared/ocp-v2/ocp-v2-session-actions.js +8 -61
  351. package/neural-interface/public/shared/ocp-v2/ocp-v2-sessions-logic.js +53 -82
  352. package/neural-interface/public/shared/ocp-v2/ocp-v2-slash-hints.js +16 -15
  353. package/neural-interface/public/shared/ocp-v2/ocp-v2-state.js +8 -21
  354. package/neural-interface/public/shared/ocp-v2/ocp-v2-status-logic.js +1 -99
  355. package/neural-interface/public/shared/ocp-v2/ocp-v2-status.js +5 -35
  356. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +48 -40
  357. package/neural-interface/public/shared/ocp-v2/{ocp-v2-profile-inheritance.js → ocp-v2-tools-inheritance.js} +16 -16
  358. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +205 -62
  359. package/neural-interface/public/shared/opencode-providers.js +39 -0
  360. package/neural-interface/public/shared/page-session.js +96 -0
  361. package/neural-interface/public/shared/registry.js +5 -3
  362. package/neural-interface/public/shared/retired-2d.js +3 -0
  363. package/neural-interface/public/shared/schedules-view.js +452 -0
  364. package/neural-interface/public/shared/settings/settings-app-pane.js +335 -0
  365. package/neural-interface/public/shared/settings/settings-app.js +243 -0
  366. package/neural-interface/public/shared/settings/settings-claude-hooks.js +135 -0
  367. package/neural-interface/public/shared/settings/settings-data.js +90 -0
  368. package/neural-interface/public/shared/settings/settings-ia.js +23 -6
  369. package/neural-interface/public/shared/settings/settings-opencode.js +89 -0
  370. package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +265 -0
  371. package/neural-interface/public/shared/settings/settings-skill-folders.js +130 -0
  372. package/neural-interface/public/shared/settings/settings-worker-guard.js +72 -0
  373. package/neural-interface/public/shared/sidepanel-session-status.js +49 -0
  374. package/neural-interface/public/shared/skills/ss-args.js +96 -0
  375. package/neural-interface/public/shared/skills/ss-creation.js +219 -0
  376. package/neural-interface/public/shared/skills/ss-dialog.js +130 -0
  377. package/neural-interface/public/shared/skills/ss-doc.js +173 -0
  378. package/neural-interface/public/shared/skills/ss-eval-adapters.js +269 -0
  379. package/neural-interface/public/shared/skills/ss-eval-grade.js +359 -0
  380. package/neural-interface/public/shared/skills/ss-eval-optimise.js +153 -0
  381. package/neural-interface/public/shared/skills/ss-eval-plan.js +315 -0
  382. package/neural-interface/public/shared/skills/ss-evals.js +417 -0
  383. package/neural-interface/public/shared/skills/ss-form.js +159 -0
  384. package/neural-interface/public/shared/skills/ss-frontmatter.js +376 -0
  385. package/neural-interface/public/shared/skills/ss-ids.js +32 -0
  386. package/neural-interface/public/shared/skills/ss-lint.js +141 -0
  387. package/neural-interface/public/shared/skills/ss-presets.js +117 -0
  388. package/neural-interface/public/shared/skills/ss-profiles.js +475 -0
  389. package/neural-interface/public/shared/skills/ss-resources.js +165 -0
  390. package/neural-interface/public/shared/skills/ss-review-view.js +190 -0
  391. package/neural-interface/public/shared/skills/ss-tabs.js +79 -0
  392. package/neural-interface/public/shared/skills/ss-unicode.js +135 -0
  393. package/neural-interface/public/shared/skills/studio/sx-actions.js +222 -0
  394. package/neural-interface/public/shared/skills/studio/sx-cm.js +101 -0
  395. package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +749 -0
  396. package/neural-interface/public/shared/skills/studio/sx-create-ai.js +236 -0
  397. package/neural-interface/public/shared/skills/studio/sx-create-view.js +254 -0
  398. package/neural-interface/public/shared/skills/studio/sx-create.js +126 -0
  399. package/neural-interface/public/shared/skills/studio/sx-diff-view.js +43 -0
  400. package/neural-interface/public/shared/skills/studio/sx-diff.js +133 -0
  401. package/neural-interface/public/shared/skills/studio/sx-docs.js +480 -0
  402. package/neural-interface/public/shared/skills/studio/sx-dom.js +229 -0
  403. package/neural-interface/public/shared/skills/studio/sx-edit-view.js +848 -0
  404. package/neural-interface/public/shared/skills/studio/sx-editor-model.js +193 -0
  405. package/neural-interface/public/shared/skills/studio/sx-errors.js +22 -0
  406. package/neural-interface/public/shared/skills/studio/sx-geometry.js +82 -0
  407. package/neural-interface/public/shared/skills/studio/sx-import.js +262 -0
  408. package/neural-interface/public/shared/skills/studio/sx-inspector.js +308 -0
  409. package/neural-interface/public/shared/skills/studio/sx-install-flow.js +279 -0
  410. package/neural-interface/public/shared/skills/studio/sx-install.js +296 -0
  411. package/neural-interface/public/shared/skills/studio/sx-library-view.js +639 -0
  412. package/neural-interface/public/shared/skills/studio/sx-library.js +475 -0
  413. package/neural-interface/public/shared/skills/studio/sx-main.js +681 -0
  414. package/neural-interface/public/shared/skills/studio/sx-operations-view.js +98 -0
  415. package/neural-interface/public/shared/skills/studio/sx-operations.js +11 -0
  416. package/neural-interface/public/shared/skills/studio/sx-plan.js +158 -0
  417. package/neural-interface/public/shared/skills/studio/sx-publish-view.js +621 -0
  418. package/neural-interface/public/shared/skills/studio/sx-review-view.js +369 -0
  419. package/neural-interface/public/shared/skills/studio/sx-review.js +148 -0
  420. package/neural-interface/public/shared/skills/studio/sx-scan.js +35 -0
  421. package/neural-interface/public/shared/skills/studio/sx-snippets.js +96 -0
  422. package/neural-interface/public/shared/skills/studio/sx-subject.js +248 -0
  423. package/neural-interface/public/shared/skills/studio/sx-test-view.js +994 -0
  424. package/neural-interface/public/shared/skills/studio/sx-test.js +328 -0
  425. package/neural-interface/public/shared/skills/studio/sx-versions-view.js +241 -0
  426. package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +425 -0
  427. package/neural-interface/public/shared/styleguide/sg-panel.js +4 -2
  428. package/neural-interface/public/shared/styles.css +2065 -2150
  429. package/neural-interface/public/shared/synabun-mascot.js +1115 -9
  430. package/neural-interface/public/shared/tool-list-picker.js +55 -0
  431. package/neural-interface/public/shared/tool-list-storage.js +24 -0
  432. package/neural-interface/public/shared/tool-list-view.js +112 -0
  433. package/neural-interface/public/shared/tour/tour-actor.js +268 -0
  434. package/neural-interface/public/shared/tour/tour-beats.js +336 -0
  435. package/neural-interface/public/shared/tour/tour-engine.js +884 -0
  436. package/neural-interface/public/shared/tour/tour-ink.js +390 -0
  437. package/neural-interface/public/shared/tour/tour-stage.js +440 -0
  438. package/neural-interface/public/shared/tour/tour-state.js +40 -0
  439. package/neural-interface/public/shared/tour/tour-tips.js +99 -0
  440. package/neural-interface/public/shared/tour/tour.css +144 -0
  441. package/neural-interface/public/shared/ui-assistant.js +27 -5
  442. package/neural-interface/public/shared/ui-automation-studio.js +155 -152
  443. package/neural-interface/public/shared/ui-bookmarks.js +2 -1
  444. package/neural-interface/public/shared/ui-claude-panel.js +153 -13
  445. package/neural-interface/public/shared/ui-codex-panel.js +1 -0
  446. package/neural-interface/public/shared/ui-command-runner.js +6 -3
  447. package/neural-interface/public/shared/ui-context-menu.js +295 -0
  448. package/neural-interface/public/shared/ui-cost-widget.js +57 -59
  449. package/neural-interface/public/shared/ui-crash-notice.js +187 -0
  450. package/neural-interface/public/shared/ui-detail.js +21 -9
  451. package/neural-interface/public/shared/ui-file-links.js +1 -0
  452. package/neural-interface/public/shared/ui-image-gallery.js +56 -52
  453. package/neural-interface/public/shared/ui-invite.js +18 -2
  454. package/neural-interface/public/shared/ui-keybinds.js +26 -1
  455. package/neural-interface/public/shared/ui-link.js +9 -9
  456. package/neural-interface/public/shared/ui-menubar.js +229 -40
  457. package/neural-interface/public/shared/ui-native-loop-router-state.js +5 -0
  458. package/neural-interface/public/shared/ui-native-loop-router.js +9 -1
  459. package/neural-interface/public/shared/ui-navbar.js +4 -24
  460. package/neural-interface/public/shared/ui-opencode-panel-v2.js +1 -0
  461. package/neural-interface/public/shared/ui-panels.js +96 -65
  462. package/neural-interface/public/shared/ui-schedules-studio.js +2267 -953
  463. package/neural-interface/public/shared/ui-sessions.js +20 -7
  464. package/neural-interface/public/shared/ui-settings.js +679 -496
  465. package/neural-interface/public/shared/ui-sidebar.js +4 -4
  466. package/neural-interface/public/shared/ui-sidepanel-session-entry.js +13 -0
  467. package/neural-interface/public/shared/ui-sidepanel-sessions.js +56 -5
  468. package/neural-interface/public/shared/ui-sidepanel-state.js +23 -12
  469. package/neural-interface/public/shared/ui-sidepanel-tray.js +84 -8
  470. package/neural-interface/public/shared/ui-sidepanel-windows.js +16 -11
  471. package/neural-interface/public/shared/ui-skills.js +42 -2781
  472. package/neural-interface/public/shared/ui-styleguide.js +5 -2
  473. package/neural-interface/public/shared/ui-sync.js +22 -5
  474. package/neural-interface/public/shared/ui-terminal.js +159 -199
  475. package/neural-interface/public/shared/ui-tool-updates.js +3 -1
  476. package/neural-interface/public/shared/ui-tooltip.js +10 -5
  477. package/neural-interface/public/shared/ui-trash.js +14 -5
  478. package/neural-interface/public/shared/ui-tutorial-layout.js +3 -117
  479. package/neural-interface/public/shared/ui-tutorial-preview.js +103 -0
  480. package/neural-interface/public/shared/ui-tutorial-steps.js +3 -61
  481. package/neural-interface/public/shared/ui-tutorial.css +47 -100
  482. package/neural-interface/public/shared/ui-tutorial.js +285 -499
  483. package/neural-interface/public/shared/ui-update.js +121 -16
  484. package/neural-interface/public/shared/ui-whiteboard.js +313 -59
  485. package/neural-interface/public/shared/ui-worker-end.js +4 -0
  486. package/neural-interface/public/shared/ui-workspaces.js +3 -2
  487. package/neural-interface/public/shared/view-variant.js +66 -0
  488. package/neural-interface/public/shared/whatsapp/wa-view.js +1 -1
  489. package/neural-interface/public/shared/whiteboard-list.js +172 -0
  490. package/neural-interface/public/shared/worker-end.js +10 -3
  491. package/neural-interface/public/shared/worker-restart.js +12 -0
  492. package/neural-interface/public/sidepanel-session.html +1 -1
  493. package/neural-interface/public/variant/2d/background.js +2 -1
  494. package/neural-interface/public/variant/2d/context-menu.js +8 -2
  495. package/neural-interface/public/variant/2d/graph.js +42 -20
  496. package/neural-interface/public/variant/2d/hulls.js +14 -9
  497. package/neural-interface/public/variant/2d/lasso.js +11 -7
  498. package/neural-interface/public/variant/2d/main.js +36 -2
  499. package/neural-interface/public/variant/2d/minimap.js +11 -13
  500. package/neural-interface/public/variant/3d/main.js +72 -6
  501. package/neural-interface/public/variant/3d/map-controls.js +24 -9
  502. package/neural-interface/public/vendor/caveat/OFL.txt +93 -0
  503. package/neural-interface/public/vendor/caveat/caveat-latin-400-normal.woff2 +0 -0
  504. package/neural-interface/public/vendor/caveat/caveat-latin-700-normal.woff2 +0 -0
  505. package/neural-interface/public/vendor/caveat/caveat.css +17 -0
  506. package/neural-interface/public/vendor/caveat/manifest.json +12 -0
  507. package/neural-interface/public/vendor/highlight/LICENSE +29 -0
  508. package/neural-interface/public/vendor/highlight/github-dark-dimmed-11.12.0.min.css +9 -0
  509. package/neural-interface/public/vendor/highlight/highlight-11.12.0.min.js +1262 -0
  510. package/neural-interface/public/vendor/highlight/manifest.json +5 -0
  511. package/neural-interface/public/vendor/html2canvas/LICENSE +22 -0
  512. package/neural-interface/public/vendor/html2canvas/html2canvas-1.4.1.min.js +20 -0
  513. package/neural-interface/public/vendor/html2canvas/manifest.json +4 -0
  514. package/neural-interface/public/vendor/marked/LICENSE.md +44 -0
  515. package/neural-interface/public/vendor/marked/manifest.json +4 -0
  516. package/neural-interface/public/vendor/marked/marked-14.1.4.esm.js +2502 -0
  517. package/neural-interface/public/vendor/mermaid/LICENSE +21 -0
  518. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/abnfDiagram-PAR5IXJI.mjs +1 -0
  519. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/architecture-7GRP2DOG-M7G3SWCO.mjs +1 -0
  520. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/architectureDiagram-2PNWDENW.mjs +36 -0
  521. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/blockDiagram-PUWGUBOA.mjs +129 -0
  522. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/c4Diagram-3NLNHZP5.mjs +38 -0
  523. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-2NMSUPJH.mjs +1 -0
  524. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-32MCUW2P.mjs +206 -0
  525. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-3NPNU5GP.mjs +1 -0
  526. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-3SMGFRGC.mjs +1 -0
  527. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-4CAJECRU.mjs +14 -0
  528. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-4OADUZ5Q.mjs +10 -0
  529. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-5IMINLNL.mjs +1 -0
  530. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-5IQCK5RY.mjs +3 -0
  531. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-5MVFYWFU.mjs +1 -0
  532. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-AQ6EADP3.mjs +1 -0
  533. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-AZZRMDJM.mjs +15 -0
  534. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-CLS4B6BI.mjs +168 -0
  535. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-DZP67EKU.mjs +70 -0
  536. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-EICP7K5X.mjs +1 -0
  537. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-GDY2UAB4.mjs +1 -0
  538. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-H7VHZCWX.mjs +1 -0
  539. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-IJBDOHL6.mjs +1 -0
  540. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-JFFVMJIA.mjs +1 -0
  541. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-JQRUD6KW.mjs +1 -0
  542. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-JZGDI5JK.mjs +1 -0
  543. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-KD6SPIPN.mjs +2 -0
  544. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-NF366YI6.mjs +231 -0
  545. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-NUDPKBEF.mjs +32 -0
  546. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-OJINMHTS.mjs +1 -0
  547. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-Q6NGPIAH.mjs +2 -0
  548. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-QJSWEUOL.mjs +132 -0
  549. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-R7JDMUKV.mjs +1 -0
  550. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-RCXAM2GK.mjs +1 -0
  551. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-RT4MAWR3.mjs +1 -0
  552. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-SFMINX4W.mjs +1 -0
  553. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-SLDQ463B.mjs +88 -0
  554. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-STOV2HOB.mjs +1 -0
  555. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-T5OCTHI4.mjs +1 -0
  556. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-TCEHX7R3.mjs +1 -0
  557. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VQC2EELJ.mjs +1 -0
  558. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VRZKL3EH.mjs +1 -0
  559. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VTHDQ7BV.mjs +1 -0
  560. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VY2OSTJR.mjs +2 -0
  561. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-WUBWJARI.mjs +1 -0
  562. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-YBMQ5JGJ.mjs +2 -0
  563. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-YDMLL4PJ.mjs +1 -0
  564. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-Z6V44ARX.mjs +62 -0
  565. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZJJ7VQJP.mjs +339 -0
  566. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZNLK7HRR.mjs +1 -0
  567. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZQN7UHIL.mjs +1 -0
  568. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZS6VBONO.mjs +174 -0
  569. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZUNWM646.mjs +1 -0
  570. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/classDiagram-FGAMPOII.mjs +1 -0
  571. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/classDiagram-v2-AI5RJEK4.mjs +1 -0
  572. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/cose-bilkent-Q72C2OJ3.mjs +1 -0
  573. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/cynefin-OW5HDTMX-6OCTWOGG.mjs +1 -0
  574. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/cynefinDiagram-2OOWUY4M.mjs +62 -0
  575. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/dagre-MPVFI544.mjs +4 -0
  576. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-AE4NWXFW.mjs +24 -0
  577. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-BUJID5UG.mjs +41 -0
  578. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-C44CZ5NI.mjs +24 -0
  579. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-EYOLEXXS.mjs +30 -0
  580. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-P7B6PWOJ.mjs +3 -0
  581. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/ebnfDiagram-FTPZ3PV6.mjs +1 -0
  582. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/erDiagram-H3ERR26V.mjs +99 -0
  583. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/eventmodeling-NTZA5JFV-WKSWQHFS.mjs +1 -0
  584. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/flowDiagram-YHGXBVSY.mjs +1 -0
  585. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/ganttDiagram-RWFTB7ED.mjs +292 -0
  586. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/gitGraph-4MIJSDKK-JH2HKYUO.mjs +1 -0
  587. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/gitGraphDiagram-MAR6GKOP.mjs +106 -0
  588. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/info-A6RAGUB7-3TX3OFWW.mjs +1 -0
  589. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/infoDiagram-LV547AFH.mjs +2 -0
  590. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/ishikawaDiagram-PUJQGNDS.mjs +70 -0
  591. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/journeyDiagram-EW5QJT4W.mjs +139 -0
  592. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/kanban-definition-JSQERAZB.mjs +89 -0
  593. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/katex-CZ4GXH2S.mjs +257 -0
  594. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/mindmap-definition-VBJCLQLM.mjs +96 -0
  595. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/packet-AYTQ26CC-MKII6P3Y.mjs +1 -0
  596. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/pegDiagram-7WILOQTJ.mjs +1 -0
  597. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/pie-WAS4IAKB-6B4OPMJ5.mjs +1 -0
  598. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/pieDiagram-EFA7CBSB.mjs +39 -0
  599. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/quadrantDiagram-WARVYA7P.mjs +7 -0
  600. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/radar-RG4KPBEZ-UPTR3XUL.mjs +1 -0
  601. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-74A4TZTK-MEFUPFMS.mjs +1 -0
  602. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-abnf-HS5TGJTU-R5YG2S5D.mjs +1 -0
  603. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-ebnf-LZEXJU2U-G5QEX2L6.mjs +1 -0
  604. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-peg-WCYAUIDC-H5I2ZRY2.mjs +1 -0
  605. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroadDiagram-5NIQVNLQ.mjs +1 -0
  606. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/requirementDiagram-SXREGOKZ.mjs +84 -0
  607. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/sankeyDiagram-VV74M2VP.mjs +40 -0
  608. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/sequenceDiagram-52ZFIFDL.mjs +162 -0
  609. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/sizeCapture-FGA5AGYP.mjs +1 -0
  610. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/stateDiagram-3QAKPIVI.mjs +1 -0
  611. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/stateDiagram-v2-HN2P4RDZ.mjs +1 -0
  612. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/swimlanes-QLVP42QD.mjs +1 -0
  613. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/swimlanesDiagram-JKPDBFCN.mjs +8 -0
  614. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/timeline-definition-GS45XAXW.mjs +120 -0
  615. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/treeView-Q6P3EWNA-ZRJGANNQ.mjs +1 -0
  616. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/treemap-WGGIJYW6-K64VN5AS.mjs +1 -0
  617. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/vennDiagram-QIQU2ZRJ.mjs +34 -0
  618. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/wardley-WFR3VGLG-D67OWCCH.mjs +1 -0
  619. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/wardleyDiagram-U6YNM66N.mjs +78 -0
  620. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/xychartDiagram-XJAZEDNM.mjs +7 -0
  621. package/neural-interface/public/vendor/mermaid/manifest.json +4 -0
  622. package/neural-interface/public/vendor/mermaid/mermaid-11.17.2.esm.min.mjs +14 -0
  623. package/neural-interface/public/vendor/yaml/LICENSE +13 -0
  624. package/neural-interface/public/vendor/yaml/manifest.json +4 -0
  625. package/neural-interface/public/vendor/yaml/yaml-2.9.1.min.js +137 -0
  626. package/neural-interface/scripts/build-vendor.mjs +116 -3
  627. package/neural-interface/server.js +4834 -2541
  628. package/neural-interface/templates/rulesets/core.md +1 -1
  629. package/neural-interface/templates/rulesets/legacy-hashes.json +18 -0
  630. package/neural-interface/templates/rulesets/manifest.json +8 -8
  631. package/npm-shrinkwrap.json +18 -12
  632. package/package.json +16 -3
  633. package/postinstall.js +9 -4
  634. package/preuninstall.js +1 -1
  635. package/setup.js +97 -322
  636. package/skills/skill-forge/SKILL.md +177 -0
  637. package/skills/skill-forge/evals/evals.json +54 -0
  638. package/skills/skill-forge/references/package.md +105 -0
  639. package/skills/skill-forge/references/review-and-recovery.md +63 -0
  640. package/skills/skill-forge/references/runtime-profiles.md +20 -0
  641. package/skills/skill-forge/references/sources-and-capabilities.md +51 -0
  642. package/skills/synabun/SKILL.md +10 -7
  643. package/skills/synabun/modules/audit.md +1 -5
  644. package/skills/synabun/modules/changelog.md +1 -5
  645. package/skills/synabun/modules/idea.md +1 -1
  646. package/skills/synabun/modules/memorize.md +6 -9
  647. package/skills/synabun/modules/schedule.md +5 -5
  648. package/mcp-server/dist/services/profiles.d.ts +0 -120
  649. package/mcp-server/dist/services/profiles.d.ts.map +0 -1
  650. package/mcp-server/dist/services/profiles.js +0 -387
  651. package/mcp-server/dist/services/profiles.js.map +0 -1
  652. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +0 -97
  653. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +0 -1
  654. package/mcp-server/dist/tools/leonardo-browser-tools.js +0 -228
  655. package/mcp-server/dist/tools/leonardo-browser-tools.js.map +0 -1
  656. package/mcp-server/dist/tools/leonardo.d.ts +0 -13
  657. package/mcp-server/dist/tools/leonardo.d.ts.map +0 -1
  658. package/mcp-server/dist/tools/leonardo.js +0 -21
  659. package/mcp-server/dist/tools/leonardo.js.map +0 -1
  660. package/mcp-server/dist/tools/profile.d.ts +0 -17
  661. package/mcp-server/dist/tools/profile.d.ts.map +0 -1
  662. package/mcp-server/dist/tools/profile.js +0 -114
  663. package/mcp-server/dist/tools/profile.js.map +0 -1
  664. package/mcp-server/src/services/profiles.ts +0 -428
  665. package/mcp-server/src/tools/leonardo-browser-tools.ts +0 -291
  666. package/mcp-server/src/tools/leonardo.ts +0 -30
  667. package/mcp-server/src/tools/profile.ts +0 -122
  668. package/neural-interface/lib/assistant-brains/opencode-browser-policy.js +0 -47
  669. package/neural-interface/lib/assistant-brains/opencode-route-gate.js +0 -62
  670. package/neural-interface/lib/mcp-profile-servers.js +0 -120
  671. package/neural-interface/public/shared/ui-tutorial-draw.js +0 -311
  672. package/skills/leonardo/SKILL.md +0 -168
  673. package/skills/leonardo/icon.svg +0 -1
  674. package/skills/leonardo/modules/browser-guide.md +0 -378
  675. package/skills/leonardo/modules/image-prompter.md +0 -283
  676. package/skills/leonardo/modules/model-advisor.md +0 -100
  677. package/skills/leonardo/modules/prompt-library.md +0 -110
  678. package/skills/leonardo/modules/style-guide.md +0 -186
  679. package/skills/leonardo/modules/video-prompter.md +0 -317
@@ -39,6 +39,11 @@
39
39
  * one-shot (success, error, blocked) has played and held. The same pose with
40
40
  * a new count or ticks keeps its loop running. `opts.unlimited` selects the
41
41
  * budget pose's relaxed infinity variant; project peeks into a folder.
42
+ * The card poses (`MASCOT_CARD_POSES`, the Assistant's follow-ups and
43
+ * question cards): `present` (a warm smile toward what it offers), `pick`
44
+ * (the plan's clipboard, one tick per picked item: `opts.ticks`) and `ask`
45
+ * (a tilted head and a question mark); all three are still between blinks
46
+ * and look where look() points.
42
47
  * - `react(name)` — `MASCOT_REACTIONS`: hop (spring, at most one per 600 ms),
43
48
  * wince, nod, pop (plan: pops the newest tick; ticks the next box too
44
49
  * unless setPose passed `ticks`), startle, pulse, click
@@ -47,6 +52,27 @@
47
52
  * - `look(x, y)` — gaze in [-1, 1] on both axes. Idle: a cursor that moved in
48
53
  * the last 2.5 s wins. watch / computer / runs / wait / blocked follow it.
49
54
  * - `blink()` — one blink.
55
+ * - `point(target | null, { poke, gap })` — rubber-hose arms (rig v3): the
56
+ * arm on the side of `target` (an element, a client rect or a client point)
57
+ * sprouts from the face's lower outer side with a springy overshoot, grows
58
+ * until its fingertip stops `gap` px short of the target and aims its hand
59
+ * (a round palm, one index pill and a thumb nub, in the eyes' pill
60
+ * language) along the line from the hand to the target, on a spring; the
61
+ * eyes look there too, never far enough to come to the arm; null (or a
62
+ * target on the other side) sends it back into the body, quicker than it
63
+ * grew. Pointing again re-measures and carries on from the springs' state.
64
+ * Rects are read when it is called and again after a resize or scroll,
65
+ * never per frame. No part of an arm ever crosses an eye or the smile. Only
66
+ * on a rig at least 96 px wide (`opts.arms` overrides): never a mini, a
67
+ * cameo or a still. Under reduced motion the arm shows its final pose at once.
68
+ * - `poke()` — two quick pokes along the line to the target (a beat landed).
69
+ * - `nudge(vx, vy)` — the host moved the character's box and its velocity
70
+ * changed by (vx, vy) px/s: the arms trail, then wobble back.
71
+ * - `shift(dx, dy)` — the host moved the box by (dx, dy) px since the last
72
+ * measure: the hand keeps pointing at the same place (no layout read).
73
+ * - `remeasure()` — read the rects again (the host's move has landed).
74
+ * - `armExtent()` — the client box the pointing arm takes once grown, or null.
75
+ * - `pointing` — the target, or null.
50
76
  * - `setActive(bool)` — false unsubscribes and shows the pose's still frame
51
77
  * (no loops, no tracking); true resumes.
52
78
  * - `pose` — the current pose name (also the SVG's `data-pose`); `fps` — the
@@ -69,6 +95,11 @@
69
95
  * (never a pause glyph); the host's CSS animates them.
70
96
  * - `mascotPropSvg(kind, { width, height })` + `paintMascotProp(svg, kind)`
71
97
  * — a kind's prop alone, framed to its own box: the ledger's row glyphs.
98
+ * - `mascotBounds(pose)` — the face-unit box a pose can draw in (the eyes over
99
+ * their travel, the smile, its prop): what a host keeps its drawing out of.
100
+ * - Arms: `createArmSim`, `armRest`, `armAim`, `armSide`, `armTargeting`,
101
+ * `armWidths`, `armPath`, `armHandPath`, `armDrawPath`, `armGaze`,
102
+ * `armKeepOut`, `MASCOT_ARM`.
72
103
  * - v1: `mascotSvg`, `mascotRefs`, `poseMascot`, `blinkAmount`, `MASCOT_VIEWBOX`.
73
104
  */
74
105
 
@@ -82,7 +113,9 @@ export const MASCOT_VIEWBOX = '0 0 280 140';
82
113
  /** One pose per tool kind (shared with the activity rack's kind ids). */
83
114
  export const MASCOT_KINDS = Object.freeze(['shell', 'read', 'edit', 'search', 'web', 'subagent', 'plan', 'memory', 'runs', 'computer', 'think', 'mcp']);
84
115
  export const MASCOT_STATES = Object.freeze(['idle', 'sleep', 'watch', 'wait', 'success', 'error', 'blocked', 'offline']);
85
- export const MASCOT_POSES = Object.freeze([...MASCOT_KINDS, ...MASCOT_STATES, 'project', 'budget']);
116
+ /** The Assistant's cards the mascot presents (asst-ask.js): next steps and confirms, decisions, questions. */
117
+ export const MASCOT_CARD_POSES = Object.freeze(['present', 'pick', 'ask']);
118
+ export const MASCOT_POSES = Object.freeze([...MASCOT_KINDS, ...MASCOT_STATES, 'project', 'budget', 'skill', ...MASCOT_CARD_POSES]);
86
119
  export const MASCOT_REACTIONS = Object.freeze(['hop', 'wince', 'nod', 'pop', 'startle', 'pulse', 'click']);
87
120
 
88
121
  /** The character's SVG. Colour comes from `currentColor` (the mouth at 25 %). */
@@ -148,6 +181,8 @@ export function mascotCameoSvg({ className = '' } = {}) {
148
181
  const PROP_BOX = Object.freeze({
149
182
  project: [204, 77, 62, 48],
150
183
  budget: [212, 65, 54, 60],
184
+ skill: [209, 25, 71, 96],
185
+ ask: [210, 0, 46, 66],
151
186
  shell: [144, 116, 42, 26], // the last keycap and the caret
152
187
  read: [65, 20, 150, 88],
153
188
  edit: [188, 93, 56, 44],
@@ -343,6 +378,54 @@ const POSES = {
343
378
  return f;
344
379
  },
345
380
  },
381
+ skill: { // writing a reusable skill: a little scroll, then a pleased sparkle
382
+ motion: 'loop', blinks: true,
383
+ frame(t, o) {
384
+ const f = blankFrame();
385
+ const c = mod(t, 3000);
386
+ const draw = o.still ? 0.85 : clamp01(c / 1800);
387
+ const spark = o.still ? 1 : bump(c, 1800, 900);
388
+ f.face.rot = -3 * (1 - spark);
389
+ f.eyes = [eye(5 + 2 * draw, 6, 1, 0.8 - 0.18 * spark, -3), eye(5 + 2 * draw, 6, 1, 0.94 - 0.2 * spark, 2)];
390
+ f.mouth = mouth(30 + 10 * spark, 8 + 8 * spark, 3, -1);
391
+ f.prop = { name: 'skill', draw, spark, jig: o.still || draw >= 1 ? 0 : Math.sin(TAU * c / 300) };
392
+ return f;
393
+ },
394
+ },
395
+ // Cards the mascot presents (asst-ask.js): still between blinks, the gaze on the option the host points at.
396
+ present: { // next steps, a confirm: a warm smile, the head turned a little toward what it offers
397
+ motion: 'still', blinks: true, look: 70,
398
+ frame(t, o) {
399
+ const f = blankFrame();
400
+ f.eyes = [eye(0, 2, 1, 0.94, -2), eye(0, 2, 1, 0.98, 2)];
401
+ f.mouth = mouth(40, 18, 0, -1);
402
+ f.face.rot = -2;
403
+ return gaze(f, o.look, { x: 16, y: 10 });
404
+ },
405
+ },
406
+ pick: { // decisions: the plan's clipboard ticks a box per picked item (opts.ticks)
407
+ motion: 'still', blinks: true, look: 70,
408
+ frame(t, o) {
409
+ const f = blankFrame();
410
+ f.eyes = [eye(0, 1, 1.02, 0.9, 0), eye(0, 1, 0.98, 0.86, 2)];
411
+ f.mouth = mouth(30, 8);
412
+ gaze(f, o.look, { x: 14, y: 9 });
413
+ f.prop = { name: 'plan', ticks: o.ticks, pop: 1 };
414
+ return f;
415
+ },
416
+ },
417
+ ask: { // a question: the head tilts, one eye wider, a small round mouth, a question mark beside it
418
+ motion: 'still', blinks: true, look: 70,
419
+ frame(t, o) {
420
+ const f = blankFrame();
421
+ f.face.rot = 5;
422
+ f.eyes = [eye(0, -2, 1.02, 1.06, 0), eye(0, -1, 0.98, 0.9, 3)];
423
+ f.mouth = mouth(16, 7, 2, 1, 0, 0.3);
424
+ gaze(f, o.look, { x: 14, y: 9 });
425
+ f.prop = { name: 'ask' };
426
+ return f;
427
+ },
428
+ },
346
429
  // ── States ──
347
430
  idle: {
348
431
  motion: 'still', blinks: true, tracks: true,
@@ -654,6 +737,32 @@ export function stillFrame(name, opts = {}) {
654
737
  return poseFrame(name, 0, { ...opts, still: true });
655
738
  }
656
739
 
740
+ // The face-unit box the eyes and the smile can reach: the eyes (80-200 × 24-96) over their whole travel
741
+ // (±20 × ±14, the widest pose's) and swell, the smile at its widest and lowest.
742
+ const FACE_REACH = Object.freeze({ left: 58, top: 6, right: 222, bottom: 134 });
743
+
744
+ /**
745
+ * The box (face units: { left, top, right, bottom }) pose `name` can draw in:
746
+ * its eyes over their whole travel, the smile and its prop, at the face's own
747
+ * offset (the subagent pose steps aside for its minis). Arms aside. What a
748
+ * host keeps its own drawing out of, so nothing is drawn over the character.
749
+ */
750
+ export function mascotBounds(name, opts = {}) {
751
+ const f = stillFrame(POSES[name] ? name : 'idle', opts);
752
+ const box = { ...FACE_REACH };
753
+ const prop = PROP_BOX[f.prop?.name];
754
+ if (prop) {
755
+ const [x, y, w, h] = prop;
756
+ box.left = Math.min(box.left, x);
757
+ box.top = Math.min(box.top, y);
758
+ box.right = Math.max(box.right, x + w);
759
+ box.bottom = Math.max(box.bottom, y + h);
760
+ }
761
+ const dx = f.face?.dx || 0;
762
+ const dy = f.face?.dy || 0;
763
+ return { left: box.left + dx, top: box.top + dy, right: box.right + dx, bottom: box.bottom + dy };
764
+ }
765
+
657
766
  /** The mouth's quadratic stroke: `w` wide, control `c` below the corners. */
658
767
  export function mouthPath(m) {
659
768
  const hw = m.w / 2;
@@ -787,6 +896,22 @@ const PROPS = {
787
896
  put(p.unlimited ? r.infinity : r.coin, 'transform', `translate(0,${n(-3 * p.lift)})`);
788
897
  },
789
898
  },
899
+ skill: {
900
+ markup: () => '<path d="M218 107V57q0-5 5-5h29q5 0 5 5v50M212 107h45v4q0 7-7 7h-31q-7 0-7-7z" fill="none" stroke="currentColor" stroke-opacity=".7" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>'
901
+ + '<path class="syna-skill-ink" d="M225 72h21M225 85h21M225 98h21" pathLength="1" fill="none" stroke="currentColor" stroke-opacity=".55" stroke-width="3" stroke-linecap="round" stroke-dasharray="1 1"/>'
902
+ + '<g class="syna-skill-pen" fill="currentColor" fill-opacity=".75"><rect x="-3" y="-28" width="6" height="20" rx="3"/><path d="M-2.8,-9 L0,-1 L2.8,-9 Z"/></g>'
903
+ + '<path class="syna-skill-spark" d="M266 28l2.4 7.6L276 38l-7.6 2.4L266 48l-2.4-7.6L256 38l7.6-2.4z" fill="currentColor"/>',
904
+ refs: g => ({ ink: g.querySelector('.syna-skill-ink'), pen: g.querySelector('.syna-skill-pen'), spark: g.querySelector('.syna-skill-spark') }),
905
+ update(r, p, put) {
906
+ const row = Math.min(2, Math.floor(p.draw * 3));
907
+ const x = 225 + 21 * (p.draw * 3 - row);
908
+ const y = 72 + 13 * row + p.jig - 3 * p.spark;
909
+ put(r.ink, 'stroke-dashoffset', n(1 - p.draw));
910
+ put(r.pen, 'transform', `translate(${n(x)},${n(y)}) rotate(35)`);
911
+ put(r.spark, 'transform', `translate(266,38) scale(${n(0.65 + 0.35 * p.spark)}) translate(-266,-38)`);
912
+ put(r.spark, 'fill-opacity', n(0.25 + 0.6 * p.spark));
913
+ },
914
+ },
790
915
  shell: { // three keycaps and a block caret under the chin
791
916
  markup: () => [0, 1, 2].map(i => `<rect class="syna-key" x="${100 + i * 24}" y="127" width="20" height="9" rx="4.5" fill="currentColor" fill-opacity=".4"/>`).join('')
792
917
  + '<rect class="syna-caret" x="174" y="125" width="7" height="12" rx="2" fill="currentColor" fill-opacity=".75"/>',
@@ -921,6 +1046,12 @@ const PROPS = {
921
1046
  });
922
1047
  },
923
1048
  },
1049
+ ask: { // a question mark beside the face, in the outline props' weight
1050
+ markup: () => '<path d="M216,22 C216,10 225,4 233,4 C242,4 250,10 250,19 C250,28 243,32 236,36 C232,38 231,41 231,46" fill="none" stroke="currentColor" stroke-opacity=".7" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>'
1051
+ + '<rect x="227.5" y="54" width="7" height="8" rx="3.5" fill="currentColor" fill-opacity=".7"/>',
1052
+ refs: () => ({}),
1053
+ update() {},
1054
+ },
924
1055
  mcp: { // a two-prong plug on its cord
925
1056
  markup: () => '<path d="M41,106 C41,120 30,122 22,134" fill="none" stroke="currentColor" stroke-opacity=".4" stroke-width="3" stroke-linecap="round"/>'
926
1057
  + '<g class="syna-plug" fill="currentColor" fill-opacity=".65"><rect x="30" y="86" width="22" height="20" rx="7"/><rect x="34" y="74" width="4.5" height="14" rx="2.25"/><rect x="43.5" y="74" width="4.5" height="14" rx="2.25"/></g>',
@@ -931,6 +1062,755 @@ const PROPS = {
931
1062
  },
932
1063
  };
933
1064
 
1065
+ // ── Arms: rubber-hose noodles the rig grows only while it points ──
1066
+ // One arm at each lower outer side of the face, below and outside the eyes,
1067
+ // where a body's sides would be: a fine line (CSS px, so it stays fine at every
1068
+ // size, about the smile's own stroke) from a shoulder anchor, no elbow or joint,
1069
+ // that swells into a hand built from pills like the eyes: a round palm, one
1070
+ // index pill that unfurls to point, a thumb nub. They exist only while a host
1071
+ // points (rig.point): the arm on the target's side sprouts with a springy
1072
+ // overshoot and grows until its fingertip stops just short of the target; the
1073
+ // index aims along the line from the hand to the target, its angle on its own
1074
+ // spring so it never snaps; the other arm may give a tiny balancing flick (a
1075
+ // relaxed hand, index folded); when the pointing ends or the target changes
1076
+ // side the arm goes back into the body, quicker than it grew. Every control
1077
+ // point is a damped spring (the hand the loosest), so an arm lags when the body
1078
+ // travels (rig.nudge), wobbles when it lands and settles into a low idle wiggle
1079
+ // while it is held; a poke moves the hand along the hand-to-target line and
1080
+ // squashes it there; a new target continues from the springs' state. Whatever
1081
+ // the springs do, every point of an arm stays inside its shoulder's free wedge
1082
+ // (ARM_BOUND): the eyes and the smile are never crossed, and while an arm is
1083
+ // out the eyes keep to MASCOT_ARM.gaze so they never come to it; a prop drawn
1084
+ // over a shoulder (the project folder, the budget, the edit pen) moves that
1085
+ // arm's root under it (armShoulder), on a spring, so no arm comes out of a
1086
+ // prop. Noodle and
1087
+ // hand are one path (a nonzero union: no seam where they meet, whatever alpha
1088
+ // the colour carries). Pure here (createArmSim, armRest, armPath, armHandPath,
1089
+ // armDrawPath: no DOM); the rig measures, steps the sim on its own frames and
1090
+ // paints it.
1091
+
1092
+ export const MASCOT_ARM = Object.freeze({
1093
+ // Left, right (face units): outside the eyes (x 80-118, 162-200), below them (y 24-96) and level with the
1094
+ // smile's corners (x 118-162, y 116), so neither eye nor the smile is ever near the root of an arm. A prop
1095
+ // drawn over one (PROP_BOX) moves that root under it: armShoulder.
1096
+ shoulders: Object.freeze([Object.freeze({ x: 62, y: 106 }), Object.freeze({ x: 218, y: 106 })]),
1097
+ reach: 96, // a grown arm, shoulder to the wrist
1098
+ // The line's width in CSS px, at the shoulder and at the wrist: a fine line, about the smile's stroke
1099
+ // (2.5 face units, .86-1.07 px on the hero), with a slight taper. Face units are px / the rig's px per
1100
+ // unit, kept within `units` on a very small or very large character.
1101
+ px: Object.freeze({ w0: 1.2, w1: 0.95 }),
1102
+ units: Object.freeze({ min: 1.8, max: 4.6 }),
1103
+ unitPx: 120 / 280, // px per face unit when a caller says nothing: the empty state's 120 px character
1104
+ // The hand (armHandPath), in the pill language of the eyes: a round palm, one index pill that
1105
+ // unfurls to point along the aim, and a thumb nub. Radii and lengths in face units, around the wrist.
1106
+ hand: Object.freeze({ palm: 6.6, index: 2.9, indexLen: 15, thumb: 2.6, thumbLen: 6.5 }),
1107
+ raise: 1.35, // radians an arm may aim above straight out (never up across the eyes)…
1108
+ lower: 2.3, // …and below it (down and in, under the smile, never across it)
1109
+ // While an arm is out the eyes travel at most this far (face units) and swell at most this much,
1110
+ // so an eye never comes to an arm.
1111
+ gaze: Object.freeze({ x: 12, y: 10, sx: 1.04, sy: 1.08 }),
1112
+ });
1113
+
1114
+ const ARM_T = [1 / 3, 2 / 3, 1];
1115
+ const ARM_SPRINGS = [[520, 0.72], [400, 0.5], [310, 0.36]]; // per control point: stiffness (1/s²), damping ratio
1116
+ const ARM_GROW = [19, 0.46]; // the sprout: ω (rad/s), ζ (≈ 18 % overshoot)
1117
+ const ARM_SHRINK = [27, 1]; // going home: critically damped, quicker
1118
+ const ARM_WIGGLE = [[1.1, 1.05, 0.2], [1.4, 1.45, 1.7], [1.7, 0.8, 3.1]]; // per point: amplitude (units), Hz, phase
1119
+ const ARM_NUDGE = [0.25, 0.55, 0.9]; // how much of a body jolt each point keeps (the hand lags most)
1120
+ const ARM_POKE = { dist: 13, dur: 0.13, gap: 0.24, share: [0.2, 0.55, 1] }; // each poke: units along the aim, s out and back, s between the two, per point
1121
+ const ARM_FLICK = { e: 0.24, angle: 1.05, hold: 0.22 }; // the other arm's balancing flick: reach, aim (down and out), s held
1122
+ const ARM_MAX_SPEED = 900; // units/s a jolt can give a point
1123
+ // Where any point of an arm may be, as seen from its shoulder (0 straight out, + down): the aim's range and a
1124
+ // little more for overshoot, wiggle and trailing. The eyes (at their MASCOT_ARM.gaze limits) and the smile lie
1125
+ // outside it with room to spare; consecutive points never turn more than ARM_KINK, so no segment cuts across.
1126
+ const ARM_BOUND = [-(MASCOT_ARM.raise + 0.12), MASCOT_ARM.lower + 0.1];
1127
+ const ARM_KINK = 2.2;
1128
+ const ARM_HOLD = [0.25, 0.3, 0.35]; // a jolt never folds a point closer to the shoulder than this share of where it rests
1129
+ // What an arm keeps away from (face units, rounded boxes: centre, half sizes, corner radius): each eye over
1130
+ // its MASCOT_ARM.gaze travel and swell, and the smile at its widest and lowest. Every part of an arm (the
1131
+ // line, the palm, the index, the thumb) stays ARM_CLEAR outside them, whatever the springs do.
1132
+ const ARM_KEEP_OUT = Object.freeze([
1133
+ ...[99, 181].map(cx => Object.freeze({ cx, cy: 60, hw: 19 * MASCOT_ARM.gaze.sx + MASCOT_ARM.gaze.x, hh: 36 * MASCOT_ARM.gaze.sy + MASCOT_ARM.gaze.y, r: 19 * MASCOT_ARM.gaze.sx })),
1134
+ Object.freeze({ cx: 140, cy: 120.5, hw: 34, hh: 12.5, r: 6 }),
1135
+ ]);
1136
+ const ARM_CLEAR = 2.5; // face units, about a CSS px on the hero
1137
+ const ARM_MIN_REACH = 0.42; // a target close by: the arm grows at least this much of its reach
1138
+ const ARM_FOCUS = 0.32; // the index aims at the target's middle, this much in from each side, leaning toward the arm
1139
+ const HAND_SQUASH = 0.16; // how much a poke squashes the hand at its peak (shorter along the aim, wider across)
1140
+ const HAND_SQUASH_SPRING = [42, 0.42]; // ω (rad/s), ζ: the squash rebounds a little (a stretch), then settles
1141
+ const HAND_POINT_RATE = 18; // 1/s: how fast the index unfurls on a held arm, or folds once it is let go
1142
+ const HAND_TURN = [20, 0.9]; // ω (rad/s), ζ: the hand's angle follows the line to the target, fluid, never a snap
1143
+ const ARM_FLARE = 18; // units of arm, before the wrist, over which the line swells into the palm
1144
+ // The fingertip's distance past the wrist with the index out, at full size (armHandPath's index pill).
1145
+ const HAND_TIP = (4 + MASCOT_ARM.hand.indexLen + MASCOT_ARM.hand.index) * 1.08;
1146
+
1147
+ /** What an arm keeps away from: the eyes over their travel while it is out, and the smile (face units, rounded boxes). */
1148
+ export function armKeepOut() {
1149
+ return ARM_KEEP_OUT.map(b => ({ ...b }));
1150
+ }
1151
+
1152
+ const ARM_ROOT_MAX_Y = 138; // a root moved under a prop never leaves the face's own box (140 high)
1153
+ const ARM_ROOT_SPRING = [16, 0.9]; // ω (rad/s), ζ: a root that moves under a prop glides there, no pop
1154
+
1155
+ /**
1156
+ * Where the arm on `side` (0 left, 1 right) roots while the face shows `prop`:
1157
+ * its shoulder (MASCOT_ARM.shoulders), or, when that prop's box (PROP_BOX: the
1158
+ * project folder, the budget pill and the edit pen sit at the face's lower
1159
+ * side, over the right shoulder) covers the shoulder, just under the box, so an
1160
+ * arm never comes out of a prop. A prop clear of the shoulder moves nothing.
1161
+ */
1162
+ export function armShoulder(side, prop = null) {
1163
+ const s = MASCOT_ARM.shoulders[side ? 1 : 0];
1164
+ const box = prop ? PROP_BOX[prop] : null;
1165
+ if (!box) return { x: s.x, y: s.y };
1166
+ const [x, y, w, h] = box;
1167
+ const root = MASCOT_ARM.units.max / 2; // the line's radius at its widest
1168
+ if (s.x < x - root || s.x > x + w + root || s.y < y - root || s.y > y + h + root) return { x: s.x, y: s.y };
1169
+ return { x: s.x, y: Math.min(ARM_ROOT_MAX_Y, y + h + root + ARM_CLEAR) };
1170
+ }
1171
+
1172
+ /** Signed distance from (x, y) to a rounded box `b` (negative inside), and the unit normal out of it. */
1173
+ function boxDistance(x, y, b) {
1174
+ const sx = x >= b.cx ? 1 : -1;
1175
+ const sy = y >= b.cy ? 1 : -1;
1176
+ const qx = Math.abs(x - b.cx) - (b.hw - b.r);
1177
+ const qy = Math.abs(y - b.cy) - (b.hh - b.r);
1178
+ if (qx > 0 && qy > 0) {
1179
+ const l = Math.hypot(qx, qy);
1180
+ return { d: l - b.r, nx: (sx * qx) / l, ny: (sy * qy) / l };
1181
+ }
1182
+ if (qx > qy) return { d: qx - b.r, nx: sx, ny: 0 };
1183
+ return { d: qy - b.r, nx: 0, ny: sy };
1184
+ }
1185
+
1186
+ /** How close a disc (x, y, radius r) comes to the face (ARM_KEEP_OUT): the smallest gap, negative when it overlaps. */
1187
+ function faceGap(x, y, r) {
1188
+ let gap = Infinity;
1189
+ for (const b of ARM_KEEP_OUT) gap = Math.min(gap, boxDistance(x, y, b).d - r);
1190
+ return gap;
1191
+ }
1192
+
1193
+ /** The hand's size for an arm `e` of the way out (armDrawPath's). */
1194
+ const handScale = e => clamp(e * 1.5, 0, 1.08);
1195
+
1196
+ /**
1197
+ * The discs a hand covers (face units), turned to `angle` at `wrist` on `side`,
1198
+ * `hs` its size and `pt` how far its index is out: the palm's front, points along
1199
+ * the index and the thumb's tip. armHandPath's pills, sampled.
1200
+ */
1201
+ function handDiscs(wrist, angle, side, hs, pt) {
1202
+ const H = MASCOT_ARM.hand;
1203
+ const cos = Math.cos(angle);
1204
+ const sin = Math.sin(angle);
1205
+ const flip = side ? -1 : 1;
1206
+ const at = (x, y, r) => ({ x: wrist.x + (x * cos - y * flip * sin) * hs, y: wrist.y + (x * sin + y * flip * cos) * hs, r: r * hs });
1207
+ const open = 0.5 + 0.5 * pt;
1208
+ const discs = [at(3.5, 0, H.palm), at(0.5 + 3 * open, 4 + H.thumbLen * open, H.thumb)];
1209
+ for (let i = 0; i <= 4; i += 1) discs.push(at(4 + (H.indexLen * pt * i) / 4, 0, H.index));
1210
+ return discs;
1211
+ }
1212
+
1213
+ /** Does a hand at `angle` keep ARM_CLEAR from the face? */
1214
+ function handClear(wrist, angle, side, hs, pt) {
1215
+ return handDiscs(wrist, angle, side, hs, pt).every(c => faceGap(c.x, c.y, c.r) >= ARM_CLEAR);
1216
+ }
1217
+
1218
+ /** The direction an arm on `side` (0 left, 1 right) points at `angle` (0 straight out, + down). */
1219
+ function armDir(side, angle) {
1220
+ return { x: (side ? 1 : -1) * Math.cos(angle), y: Math.sin(angle) };
1221
+ }
1222
+
1223
+ /** Which arm faces `point` (face units): the side of the face it is on. */
1224
+ export function armSide(point) {
1225
+ return Number(point?.x) >= 140 ? 1 : 0;
1226
+ }
1227
+
1228
+ const clampAim = a => clamp(Number(a) || 0, -MASCOT_ARM.raise, MASCOT_ARM.lower);
1229
+
1230
+ /** The angle the arm on `side` aims at `point` (face units), kept within its swing; `shoulder` is where it roots (armShoulder). */
1231
+ export function armAim(side, point, shoulder = MASCOT_ARM.shoulders[side ? 1 : 0]) {
1232
+ const s = shoulder;
1233
+ const dx = (Number(point?.x) - s.x) * (side ? 1 : -1);
1234
+ const dy = Number(point?.y) - s.y;
1235
+ if (!Number.isFinite(dx) || !Number.isFinite(dy) || (Math.abs(dx) < 1e-6 && Math.abs(dy) < 1e-6)) return 0;
1236
+ return clampAim(Math.atan2(dy, dx));
1237
+ }
1238
+
1239
+ /**
1240
+ * How the arm on `side` meets a target `rect` (face units: { left, top, right,
1241
+ * bottom }): the angle it aims at, the point its index aims at (the target's
1242
+ * middle, leaning toward the arm: its near side for a wide one) and how much of
1243
+ * its reach it grows so the fingertip stops `gap` units short of the target.
1244
+ * `shoulder` is where the arm roots (armShoulder: under a prop that covers it).
1245
+ */
1246
+ export function armTargeting(side, rect, gap = 0, shoulder = MASCOT_ARM.shoulders[side ? 1 : 0]) {
1247
+ const s = shoulder;
1248
+ const l = Number(rect?.left);
1249
+ const t = Number(rect?.top);
1250
+ const r = Number.isFinite(Number(rect?.right)) ? Number(rect.right) : l;
1251
+ const b = Number.isFinite(Number(rect?.bottom)) ? Number(rect.bottom) : t;
1252
+ if (![l, t, r, b].every(Number.isFinite)) return null;
1253
+ const inset = (lo, hi) => { const m = (hi - lo) * ARM_FOCUS; return [lo + m, hi - m]; };
1254
+ const [fx0, fx1] = inset(l, r);
1255
+ const [fy0, fy1] = inset(t, b);
1256
+ const focus = { x: clamp(s.x, fx0, fx1), y: clamp(s.y, fy0, fy1) };
1257
+ const g = Math.max(0, Number(gap) || 0);
1258
+ const dx = Math.max(l - g - s.x, 0, s.x - r - g);
1259
+ const dy = Math.max(t - g - s.y, 0, s.y - b - g);
1260
+ const reach = clamp((Math.hypot(dx, dy) - HAND_TIP) / MASCOT_ARM.reach, ARM_MIN_REACH, 1);
1261
+ return { angle: armAim(side, focus, s), focus, reach };
1262
+ }
1263
+
1264
+ /**
1265
+ * Where the arm's three control points rest (a third, two thirds, the hand):
1266
+ * along its aim, `extension` of its reach, bowed a little like a hose away
1267
+ * from the face. With `wiggleAt` (s) they sway in the low idle wiggle.
1268
+ * `shoulder` is where the arm roots (armShoulder).
1269
+ */
1270
+ export function armRest(side, angle, extension, wiggleAt = null, shoulder = MASCOT_ARM.shoulders[side ? 1 : 0]) {
1271
+ const s = shoulder;
1272
+ const d = armDir(side, angle);
1273
+ // The hose bows away from the face: down for a level arm, outward for a raised or a lowered one.
1274
+ // Its size follows how far that perpendicular leans out-and-down, so it never flips at once.
1275
+ const nx = -d.y;
1276
+ const ny = d.x;
1277
+ const ox = (side ? 1 : -1) * 0.857;
1278
+ const oy = 0.514;
1279
+ const e = Math.max(0, Number(extension) || 0);
1280
+ const len = MASCOT_ARM.reach * e;
1281
+ const bow = 0.16 * len * (nx * ox + ny * oy);
1282
+ const mirror = side ? -1 : 1; // the right arm's perpendicular is the left one's mirror turned round: the wiggle mirrors too
1283
+ return ARM_T.map((t, i) => {
1284
+ let off = Math.sin(Math.PI * t) * bow;
1285
+ let along = len * t;
1286
+ if (wiggleAt != null) {
1287
+ const [a, f, p] = ARM_WIGGLE[i];
1288
+ const k = Math.min(1, e);
1289
+ off += mirror * a * k * Math.sin(TAU * f * wiggleAt + p);
1290
+ along += 0.5 * a * k * Math.sin(TAU * f * 0.7 * wiggleAt + p * 1.7);
1291
+ }
1292
+ return { x: s.x + d.x * along + nx * off, y: s.y + d.y * along + ny * off };
1293
+ });
1294
+ }
1295
+
1296
+ /** A point `t` (0-1) along the Catmull-Rom segment p1 → p2. */
1297
+ function catmull(p0, p1, p2, p3, t) {
1298
+ const t2 = t * t;
1299
+ const t3 = t2 * t;
1300
+ const f = (a, b, c, d) => 0.5 * ((2 * b) + (-a + c) * t + (2 * a - 5 * b + 4 * c - d) * t2 + (-a + 3 * b - 3 * c + d) * t3);
1301
+ return { x: f(p0.x, p1.x, p2.x, p3.x), y: f(p0.y, p1.y, p2.y, p3.y) };
1302
+ }
1303
+
1304
+ const smoothstep = (u) => { const v = clamp01(u); return v * v * (3 - 2 * v); };
1305
+
1306
+ /** The line's width (face units) at the shoulder and at the wrist, for a character drawn at `unitPx` px per unit. */
1307
+ export function armWidths(unitPx = MASCOT_ARM.unitPx) {
1308
+ const k = Number(unitPx) > 0 ? Number(unitPx) : MASCOT_ARM.unitPx;
1309
+ const u = px => clamp(px / k, MASCOT_ARM.units.min, MASCOT_ARM.units.max);
1310
+ return { w0: u(MASCOT_ARM.px.w0), w1: u(MASCOT_ARM.px.w1) };
1311
+ }
1312
+
1313
+ /**
1314
+ * The noodle's two edges through `points` (shoulder first, hand last, face
1315
+ * units), × `scale`: armWidths(unitPx) w0 wide at the shoulder, tapering to w1
1316
+ * at the wrist, and with `cuff` (a half-width, drawn units) swelling over the
1317
+ * last ARM_FLARE units into the palm, so the hand grows out of the arm with no
1318
+ * step. Also the direction the arm ends in (`tangent`, radians) and the
1319
+ * shoulder's radius. null when the arm is too short to draw.
1320
+ */
1321
+ function armOutline(points, scale = 1, cuff = 0, unitPx = MASCOT_ARM.unitPx) {
1322
+ if (!Array.isArray(points) || points.length < 2) return null;
1323
+ const k = clamp(Number(scale) || 0, 0, 1.2);
1324
+ const { w0, w1 } = armWidths(unitPx);
1325
+ const line = [];
1326
+ const steps = 6;
1327
+ for (let i = 0; i < points.length - 1; i += 1) {
1328
+ const p0 = points[Math.max(0, i - 1)];
1329
+ const p3 = points[Math.min(points.length - 1, i + 2)];
1330
+ for (let j = 0; j < steps; j += 1) line.push(catmull(p0, points[i], points[i + 1], p3, j / steps));
1331
+ }
1332
+ line.push(points[points.length - 1]);
1333
+ const at = [0];
1334
+ for (let i = 1; i < line.length; i += 1) at.push(at[i - 1] + Math.hypot(line[i].x - line[i - 1].x, line[i].y - line[i - 1].y));
1335
+ const total = at[at.length - 1];
1336
+ if (!(total > 0.5) || k <= 0.01) return null;
1337
+ const swell = Math.max(0, Number(cuff) || 0);
1338
+ const left = [];
1339
+ const right = [];
1340
+ for (let i = 0; i < line.length; i += 1) {
1341
+ const a = line[Math.max(0, i - 1)];
1342
+ const b = line[Math.min(line.length - 1, i + 1)];
1343
+ const dx = b.x - a.x;
1344
+ const dy = b.y - a.y;
1345
+ const dl = Math.hypot(dx, dy) || 1;
1346
+ let hw = (w0 + (w1 - w0) * (at[i] / total)) * k / 2;
1347
+ if (swell > hw) hw += (swell - hw) * smoothstep((at[i] - (total - ARM_FLARE)) / ARM_FLARE);
1348
+ left.push({ x: line[i].x - (dy / dl) * hw, y: line[i].y + (dx / dl) * hw });
1349
+ right.push({ x: line[i].x + (dy / dl) * hw, y: line[i].y - (dx / dl) * hw });
1350
+ }
1351
+ const end = line[line.length - 1];
1352
+ const before = line[line.length - 2];
1353
+ return { left, right, r0: w0 * k / 2, tangent: Math.atan2(end.y - before.y, end.x - before.x) };
1354
+ }
1355
+
1356
+ /** An outline's path data: smooth edges (quadratics through the midpoints) and a round shoulder. */
1357
+ function outlinePath(o) {
1358
+ const edge = (pts) => {
1359
+ let d = '';
1360
+ for (let i = 1; i < pts.length - 1; i += 1) {
1361
+ const m = { x: (pts[i].x + pts[i + 1].x) / 2, y: (pts[i].y + pts[i + 1].y) / 2 };
1362
+ d += ` Q${n(pts[i].x)},${n(pts[i].y)} ${n(m.x)},${n(m.y)}`;
1363
+ }
1364
+ const last = pts[pts.length - 1];
1365
+ return `${d} L${n(last.x)},${n(last.y)}`;
1366
+ };
1367
+ const back = o.right.slice().reverse();
1368
+ return `M${n(o.left[0].x)},${n(o.left[0].y)}${edge(o.left)} L${n(back[0].x)},${n(back[0].y)}${edge(back)}`
1369
+ + ` A${n(o.r0)},${n(o.r0)} 0 0 1 ${n(o.left[0].x)},${n(o.left[0].y)}Z`;
1370
+ }
1371
+
1372
+ /**
1373
+ * The noodle's outline as path data: one smooth tapered shape, w0 wide at the
1374
+ * shoulder (a rounded end that goes into the body) and w1 at the wrist, × `scale`,
1375
+ * swelling into a `cuff` half-width at the wrist when one is given (armOutline).
1376
+ * '' when the arm is too short to draw. The hand is armHandPath's.
1377
+ */
1378
+ export function armPath(points, scale = 1, cuff = 0, unitPx = MASCOT_ARM.unitPx) {
1379
+ const o = armOutline(points, scale, cuff, unitPx);
1380
+ return o ? outlinePath(o) : '';
1381
+ }
1382
+
1383
+ /** Twice a polygon's signed area (the shoelace sum): its sign is which way round it goes. */
1384
+ const shoelace = (pts) => pts.reduce((s, p, i) => { const q = pts[(i + 1) % pts.length]; return s + p.x * q.y - q.x * p.y; }, 0);
1385
+ const wrapAngle = (a) => Math.atan2(Math.sin(a), Math.cos(a));
1386
+
1387
+ /** A pill from `a` to `b` of radius `r`, as a polygon (local units). */
1388
+ function pillPolygon(a, b, r, segs = 10) {
1389
+ const ang = Math.atan2(b.y - a.y, b.x - a.x);
1390
+ const pts = [];
1391
+ for (let i = 0; i <= segs; i += 1) { const t = ang - Math.PI / 2 + (Math.PI * i) / segs; pts.push({ x: b.x + r * Math.cos(t), y: b.y + r * Math.sin(t) }); }
1392
+ for (let i = 0; i <= segs; i += 1) { const t = ang + Math.PI / 2 + (Math.PI * i) / segs; pts.push({ x: a.x + r * Math.cos(t), y: a.y + r * Math.sin(t) }); }
1393
+ return pts;
1394
+ }
1395
+
1396
+ /**
1397
+ * The hand at `wrist` (face units), pointing along `angle` (radians, screen
1398
+ * coordinates) on the arm of `side` (0 left, 1 right): a round palm the noodle
1399
+ * swells into, one index pill out `point` (0-1) of its length, on the line
1400
+ * through the wrist at `angle` (so the index aims exactly where the hand is
1401
+ * turned), and a thumb nub that opens with it. Built from pills like the eyes.
1402
+ * The thumb is on top for a level arm and outward for a lowered one, on either
1403
+ * side (left mirrors right). `scale` grows it with the arm; `squash` (a poke
1404
+ * landing) shortens it along the aim and widens it across. Path data of three
1405
+ * pills, each the same way round (`sign`, the noodle's, so a nonzero fill
1406
+ * paints the union once, whatever alpha `currentColor` carries). '' when too
1407
+ * small to draw.
1408
+ */
1409
+ export function armHandPath(wrist, angle, side, { scale = 1, point = 1, squash = 0, sign = -1 } = {}) {
1410
+ const H = MASCOT_ARM.hand;
1411
+ const hs = clamp(Number(scale) || 0, 0, 1.2);
1412
+ if (hs <= 0.01 || !Number.isFinite(Number(wrist?.x)) || !Number.isFinite(Number(wrist?.y))) return '';
1413
+ const pt = clamp01(Number(point) || 0);
1414
+ const sq = clamp(Number(squash) || 0, -0.3, 0.3);
1415
+ const th = Number(angle) || 0;
1416
+ const cos = Math.cos(th);
1417
+ const sin = Math.sin(th);
1418
+ const sx = hs * (1 - sq);
1419
+ const sy = hs * (1 + sq) * (side ? -1 : 1); // +y is the thumb's side
1420
+ const toWorld = (p) => { const x = p.x * sx; const y = p.y * sy; return { x: wrist.x + x * cos - y * sin, y: wrist.y + x * sin + y * cos }; };
1421
+ const open = 0.5 + 0.5 * pt; // the thumb: tucked on a relaxed hand, out when it points
1422
+ const pills = [
1423
+ pillPolygon({ x: -0.5, y: 0 }, { x: 3.5, y: 0 }, H.palm),
1424
+ pillPolygon({ x: 4, y: 0 }, { x: 4 + H.indexLen * pt, y: 0 }, H.index),
1425
+ pillPolygon({ x: 0.5, y: 4 }, { x: 0.5 + 3 * open, y: 4 + H.thumbLen * open }, H.thumb),
1426
+ ];
1427
+ const way = sign < 0 ? -1 : 1;
1428
+ return pills.map((poly) => {
1429
+ const w = poly.map(toWorld);
1430
+ if (Math.sign(shoelace(w)) !== way) w.reverse();
1431
+ return `M${w.map(p => `${n(p.x)},${n(p.y)}`).join('L')}Z`;
1432
+ }).join('');
1433
+ }
1434
+
1435
+ /**
1436
+ * Everything one arm draws, as one path: the noodle swelling into the palm, then
1437
+ * the hand at the wrist (armHandPath), turned to the frame's `hand` angle (the
1438
+ * line from the hand to what it points at, on its own spring; the arm's end
1439
+ * when there is none). `frame` is one side of createArmSim().frame(); `side` 0
1440
+ * left, 1 right; `unitPx` the character's px per face unit (the line's width is
1441
+ * in px). One path, every subpath the same way round. '' when there is nothing
1442
+ * to draw.
1443
+ */
1444
+ export function armDrawPath(frame, side, { unitPx = MASCOT_ARM.unitPx } = {}) {
1445
+ if (!frame || !Array.isArray(frame.points) || !(frame.e > 0)) return '';
1446
+ const grown = frame.e / Math.max(0.05, Number(frame.span) > 0 ? Number(frame.span) : 1);
1447
+ const hs = handScale(grown);
1448
+ const sq = clamp(Number(frame.squash) || 0, -0.3, 0.3);
1449
+ const pt = clamp01(frame.point == null ? 1 : Number(frame.point) || 0);
1450
+ const o = armOutline(frame.points, clamp01(grown * 1.7), MASCOT_ARM.hand.palm * hs * (1 + sq), unitPx);
1451
+ if (!o) return '';
1452
+ const wrist = frame.points[frame.points.length - 1];
1453
+ const angle = Number.isFinite(frame.hand) ? frame.hand : o.tangent;
1454
+ const sign = Math.sign(shoelace([...o.left, ...o.right.slice().reverse()])) || -1;
1455
+ return outlinePath(o) + armHandPath(wrist, angle, side, { scale: hs, point: pt, squash: sq, sign });
1456
+ }
1457
+
1458
+ /**
1459
+ * The eyes of `frame` kept to MASCOT_ARM.gaze, `amount` (0-1) of the way (an arm
1460
+ * on its way out or home eases them in and out): while an arm is out an eye
1461
+ * never travels to it. A new frame; `frame` is left as it is.
1462
+ */
1463
+ export function armGaze(frame, amount = 1) {
1464
+ const g = clamp01(Number(amount) || 0);
1465
+ if (!frame?.eyes || g <= 0) return frame;
1466
+ const G = MASCOT_ARM.gaze;
1467
+ const f = cloneFrame(frame);
1468
+ for (const e of f.eyes) {
1469
+ e.dx += (clamp(e.dx, -G.x, G.x) - e.dx) * g;
1470
+ e.dy += (clamp(e.dy, -G.y, G.y) - e.dy) * g;
1471
+ e.sx += (Math.min(e.sx, G.sx) - e.sx) * g;
1472
+ e.sy += (Math.min(e.sy, G.sy) - e.sy) * g;
1473
+ }
1474
+ return f;
1475
+ }
1476
+
1477
+ /**
1478
+ * The arms' motion, with no DOM: two arms of damped springs. aim(side, angle,
1479
+ * { target, reach }) grows that arm `reach` (0-1) of the way and turns its hand
1480
+ * toward `target` (face units; the other one goes home, or flicks once for
1481
+ * balance), release() sends both home, poke() pokes twice, nudge(vx, vy) is a
1482
+ * jolt of the body (units/s: the points keep part of their speed, so they lag),
1483
+ * step(dt) advances, finish() jumps to the end (reduced motion: no overshoot,
1484
+ * no wiggle), frame() says what to draw.
1485
+ */
1486
+ export function createArmSim() {
1487
+ // Per arm: the reach e (0 home, 1 grown) and its speed, the three spring points, the target its index
1488
+ // aims at, and the hand's own numbers: pt (0 a relaxed hand, 1 the index out), sq (the poke's squash,
1489
+ // a small spring) and ha (its angle, on a spring that follows the line to the target).
1490
+ // `span` is the reach the arm was last sent to (1 for the flick): the hand and the index grow with e / span.
1491
+ const arms = [0, 1].map(side => ({
1492
+ side, e: 0, ve: 0, goal: 0, span: 1, angle: 0, pts: null, held: false, flickUntil: -1, pokeAt: -1,
1493
+ pt: 0, sq: 0, vsq: 0, target: null, ha: 0, vha: 0, sh: null,
1494
+ }));
1495
+ const grownOf = a => a.e / Math.max(0.05, a.span);
1496
+ // The face's prop (PROP_BOX's name): an arm whose shoulder it covers roots under it (armShoulder). The
1497
+ // root `sh` is a spring toward that place, so a prop arriving while the arm is out slides it, never pops.
1498
+ let prop = null;
1499
+ const rootGoal = a => armShoulder(a.side, prop);
1500
+ const rootOf = (a) => {
1501
+ if (!a.sh) { const g = rootGoal(a); a.sh = { x: g.x, y: g.y, vx: 0, vy: 0 }; }
1502
+ return a.sh;
1503
+ };
1504
+ /** The two pokes' push (0-1) `t` s after they began: two quick bumps, then nothing. */
1505
+ const pokeBump = (t) => {
1506
+ const bump = (x) => (x > 0 && x < ARM_POKE.dur ? Math.sin(Math.PI * x / ARM_POKE.dur) : 0);
1507
+ return bump(t) + bump(t - ARM_POKE.gap);
1508
+ };
1509
+ let time = 0;
1510
+ const restOf = (a, wiggle) => armRest(a.side, a.angle, a.e, wiggle ? time : null, rootOf(a));
1511
+ const ensurePts = (a) => {
1512
+ if (a.pts) return;
1513
+ a.sh = null; // a fresh sprout roots where the prop lets it, at once
1514
+ a.pts = restOf(a, false).map(p => ({ x: p.x, y: p.y, vx: 0, vy: 0 }));
1515
+ a.ha = Math.atan2(Math.sin(a.angle), (a.side ? 1 : -1) * Math.cos(a.angle)); // the hand comes out along the arm
1516
+ a.vha = 0;
1517
+ };
1518
+ const moving = (a) => !!a.pts && (a.held || a.e > 1e-3 || Math.abs(a.ve) > 1e-3 || Math.abs(a.sq) > 0.002 || Math.abs(a.vsq) > 0.02
1519
+ || Math.abs(a.vha) > 0.02 || (!!a.sh && Math.abs(a.sh.vx) + Math.abs(a.sh.vy) > 0.05)
1520
+ || a.pts.some(p => Math.abs(p.vx) + Math.abs(p.vy) > 0.05));
1521
+
1522
+ /** Where the hand should point: the arm's own end, turned toward the target as far as the index is out. */
1523
+ function handGoal(a) {
1524
+ const hand = a.pts[2];
1525
+ const prev = a.pts[1];
1526
+ const along = Math.hypot(hand.x - prev.x, hand.y - prev.y) > 0.3
1527
+ ? Math.atan2(hand.y - prev.y, hand.x - prev.x)
1528
+ : Math.atan2(Math.sin(a.angle), (a.side ? 1 : -1) * Math.cos(a.angle));
1529
+ if (!a.target) return along;
1530
+ const dx = a.target.x - hand.x;
1531
+ const dy = a.target.y - hand.y;
1532
+ if (Math.hypot(dx, dy) < 4) return a.ha;
1533
+ return along + wrapAngle(Math.atan2(dy, dx) - along) * smoothstep(a.pt);
1534
+ }
1535
+
1536
+ /**
1537
+ * Keep every point inside the shoulder's free wedge (ARM_BOUND, ARM_KINK), no
1538
+ * nearer the shoulder than ARM_HOLD of its rest, and clear of the face
1539
+ * (ARM_KEEP_OUT, by the line's or the palm's radius): a point stopped there
1540
+ * loses the speed that took it out, a little of it coming back as a bounce.
1541
+ */
1542
+ function bound(a, rest) {
1543
+ const s = rootOf(a);
1544
+ const out = a.side ? 1 : -1;
1545
+ const hs = handScale(grownOf(a));
1546
+ let prev = null;
1547
+ a.pts.forEach((p, i) => {
1548
+ const stop = (nx, ny) => { // the speed into (nx, ny)'s opposite is spent
1549
+ const vn = p.vx * nx + p.vy * ny;
1550
+ if (vn < 0) { p.vx -= 1.2 * vn * nx; p.vy -= 1.2 * vn * ny; }
1551
+ };
1552
+ const dx = (p.x - s.x) * out;
1553
+ const dy = p.y - s.y;
1554
+ const r = Math.hypot(dx, dy);
1555
+ const hold = ARM_HOLD[i] * Math.hypot(rest[i].x - s.x, rest[i].y - s.y);
1556
+ if (r >= 0.5) {
1557
+ const th = Math.atan2(dy, dx);
1558
+ let c = th;
1559
+ if (c < ARM_BOUND[0] || c > ARM_BOUND[1]) {
1560
+ c = Math.abs(wrapAngle(th - ARM_BOUND[0])) <= Math.abs(wrapAngle(th - ARM_BOUND[1])) ? ARM_BOUND[0] : ARM_BOUND[1];
1561
+ }
1562
+ if (prev != null && Math.abs(c - prev) > ARM_KINK) c = prev + Math.sign(c - prev) * ARM_KINK;
1563
+ const rr = Math.max(r, hold);
1564
+ if (c !== th || rr !== r) {
1565
+ p.x = s.x + out * rr * Math.cos(c);
1566
+ p.y = s.y + rr * Math.sin(c);
1567
+ // Keep the speed along the radius (outward only, when it was folded in); the sideways part is spent.
1568
+ const ux = out * Math.cos(c);
1569
+ const uy = Math.sin(c);
1570
+ let vr = p.vx * ux + p.vy * uy;
1571
+ if (rr !== r && vr < 0) vr = 0;
1572
+ const vtx = p.vx - (p.vx * ux + p.vy * uy) * ux;
1573
+ const vty = p.vy - (p.vx * ux + p.vy * uy) * uy;
1574
+ const keep = c !== th ? -0.2 : 1;
1575
+ p.vx = vr * ux + keep * vtx;
1576
+ p.vy = vr * uy + keep * vty;
1577
+ }
1578
+ prev = c;
1579
+ } else prev = null;
1580
+ // Clear of the eyes and the smile: by the line's half-width, by the palm for the hand.
1581
+ const need = ARM_CLEAR + (i === 2 ? MASCOT_ARM.hand.palm * hs * 1.16 : MASCOT_ARM.units.max / 2);
1582
+ for (const b of ARM_KEEP_OUT) {
1583
+ const g = boxDistance(p.x, p.y, b);
1584
+ if (g.d < need) { p.x += g.nx * (need - g.d); p.y += g.ny * (need - g.d); stop(g.nx, g.ny); }
1585
+ }
1586
+ });
1587
+ }
1588
+
1589
+ /**
1590
+ * The hand's angle, kept clear of the face: as asked when its palm, index and
1591
+ * thumb keep ARM_CLEAR from ARM_KEEP_OUT (always, for anything beside the
1592
+ * face); otherwise turned the least toward the arm's own end until they do.
1593
+ */
1594
+ function clearAngle(a, angle) {
1595
+ const wrist = a.pts[2];
1596
+ const hs = handScale(grownOf(a));
1597
+ if (hs <= 0.01 || handClear(wrist, angle, a.side, hs, a.pt)) return angle;
1598
+ const prev = a.pts[1];
1599
+ const along = Math.atan2(wrist.y - prev.y, wrist.x - prev.x);
1600
+ const toward = Math.sign(wrapAngle(along - angle)) || 1;
1601
+ for (let k = 1; k <= 24; k += 1) {
1602
+ for (const way of [toward, -toward]) {
1603
+ const c = angle + way * k * 0.07;
1604
+ if (handClear(wrist, c, a.side, hs, a.pt)) return wrapAngle(c);
1605
+ }
1606
+ }
1607
+ return along;
1608
+ }
1609
+
1610
+ function stepArm(a, h) {
1611
+ if (a.flickUntil >= 0 && time >= a.flickUntil) { a.goal = 0; a.flickUntil = -1; }
1612
+ let push = 0;
1613
+ let squash = 0;
1614
+ if (a.pokeAt >= 0) {
1615
+ const since = time - a.pokeAt;
1616
+ if (since > ARM_POKE.gap + ARM_POKE.dur) a.pokeAt = -1;
1617
+ else {
1618
+ push = pokeBump(since);
1619
+ squash = HAND_SQUASH * push; // the hand lands on the target at the bump's peak
1620
+ }
1621
+ }
1622
+ const [w, z] = a.held || a.flickUntil >= 0 ? ARM_GROW : ARM_SHRINK;
1623
+ a.ve += (w * w * (a.goal - a.e) - 2 * z * w * a.ve) * h;
1624
+ a.e += a.ve * h;
1625
+ if (!a.held && a.e < 0) { a.e = 0; a.ve = 0; } // home: into the body, never out the other side
1626
+ // The hand: the index unfurls as a held arm grows (and folds once it is let go: the flick arm and
1627
+ // a retreating one show a relaxed hand); a poke squashes it, and the squash rebounds and settles.
1628
+ a.pt += ((a.held ? clamp01((grownOf(a) - 0.25) / 0.45) : 0) - a.pt) * (1 - Math.exp(-HAND_POINT_RATE * h));
1629
+ const [sw, sz] = HAND_SQUASH_SPRING;
1630
+ a.vsq += (sw * sw * (squash - a.sq) - 2 * sz * sw * a.vsq) * h;
1631
+ a.sq += a.vsq * h;
1632
+ // The root glides to where the face's prop lets it be (armShoulder); it never jumps.
1633
+ const sh = rootOf(a);
1634
+ const g = rootGoal(a);
1635
+ const [rw, rz] = ARM_ROOT_SPRING;
1636
+ sh.vx += (rw * rw * (g.x - sh.x) - 2 * rz * rw * sh.vx) * h;
1637
+ sh.vy += (rw * rw * (g.y - sh.y) - 2 * rz * rw * sh.vy) * h;
1638
+ sh.x += sh.vx * h;
1639
+ sh.y += sh.vy * h;
1640
+ if (Math.abs(sh.x - g.x) + Math.abs(sh.y - g.y) < 0.05 && Math.abs(sh.vx) + Math.abs(sh.vy) < 0.5) { sh.x = g.x; sh.y = g.y; sh.vx = 0; sh.vy = 0; }
1641
+ const rest = restOf(a, a.held);
1642
+ if (push > 0) {
1643
+ // A poke goes along the line the index aims down, the hand most.
1644
+ const ux = Math.cos(a.ha);
1645
+ const uy = Math.sin(a.ha);
1646
+ rest.forEach((p, i) => { p.x += ux * ARM_POKE.dist * ARM_POKE.share[i] * push; p.y += uy * ARM_POKE.dist * ARM_POKE.share[i] * push; });
1647
+ }
1648
+ a.pts.forEach((p, i) => {
1649
+ const [k, zeta] = ARM_SPRINGS[i];
1650
+ const c = 2 * zeta * Math.sqrt(k);
1651
+ p.vx += (k * (rest[i].x - p.x) - c * p.vx) * h;
1652
+ p.vy += (k * (rest[i].y - p.y) - c * p.vy) * h;
1653
+ p.x += p.vx * h;
1654
+ p.y += p.vy * h;
1655
+ });
1656
+ bound(a, rest);
1657
+ const [tw, tz] = HAND_TURN;
1658
+ a.vha += (tw * tw * wrapAngle(clearAngle(a, handGoal(a)) - a.ha) - 2 * tz * tw * a.vha) * h;
1659
+ const turned = wrapAngle(a.ha + a.vha * h);
1660
+ a.ha = clearAngle(a, turned);
1661
+ if (a.ha !== turned) a.vha *= 0.3; // turned back off the face: the swing is spent
1662
+ if (!a.held && a.flickUntil < 0 && a.e < 1e-3 && Math.abs(a.ve) < 1e-3 && a.pts.every(p => Math.abs(p.vx) + Math.abs(p.vy) < 0.05)) {
1663
+ a.e = 0;
1664
+ a.ve = 0;
1665
+ a.pt = 0;
1666
+ a.sq = 0;
1667
+ a.vsq = 0;
1668
+ a.vha = 0;
1669
+ a.pts = null; // home and still: nothing to draw
1670
+ a.sh = null;
1671
+ }
1672
+ }
1673
+
1674
+ return {
1675
+ /** The prop the face shows (PROP_BOX's name, or null): an arm whose shoulder it covers roots under it (armShoulder). */
1676
+ prop(name) {
1677
+ prop = name && PROP_BOX[name] ? name : null;
1678
+ return prop;
1679
+ },
1680
+ /** Where the arm on `side` roots for the prop shown (face units; the glide there is the sim's own). */
1681
+ root(side) { return rootGoal(arms[side ? 1 : 0]); },
1682
+ aim(side, angle, { target = null, reach = 1 } = {}) {
1683
+ const a = arms[side ? 1 : 0];
1684
+ const other = arms[side ? 0 : 1];
1685
+ const sprouting = !a.held && a.e < 0.15;
1686
+ a.angle = clampAim(angle);
1687
+ ensurePts(a);
1688
+ a.held = true;
1689
+ a.goal = clamp(Number.isFinite(Number(reach)) ? Number(reach) : 1, ARM_MIN_REACH, 1);
1690
+ a.span = a.goal;
1691
+ a.target = target && Number.isFinite(Number(target.x)) && Number.isFinite(Number(target.y)) ? { x: Number(target.x), y: Number(target.y) } : null;
1692
+ a.flickUntil = -1;
1693
+ if (other.held) { other.held = false; other.goal = 0; other.pokeAt = -1; } // the target changed side
1694
+ else if (sprouting && other.e < 0.05 && other.flickUntil < 0) {
1695
+ other.angle = ARM_FLICK.angle;
1696
+ other.target = null;
1697
+ ensurePts(other);
1698
+ other.goal = ARM_FLICK.e;
1699
+ other.span = 1;
1700
+ other.flickUntil = time + ARM_FLICK.hold;
1701
+ }
1702
+ },
1703
+ release() {
1704
+ for (const a of arms) { a.held = false; a.goal = 0; a.pokeAt = -1; if (a.flickUntil >= 0) a.flickUntil = time; }
1705
+ },
1706
+ poke() {
1707
+ const a = arms.find(x => x.held);
1708
+ if (a) a.pokeAt = time;
1709
+ },
1710
+ nudge(vx, vy) {
1711
+ const dx = Number.isFinite(Number(vx)) ? Number(vx) : 0;
1712
+ const dy = Number.isFinite(Number(vy)) ? Number(vy) : 0;
1713
+ for (const a of arms) {
1714
+ if (!a.pts) continue;
1715
+ a.pts.forEach((p, i) => {
1716
+ p.vx -= dx * ARM_NUDGE[i];
1717
+ p.vy -= dy * ARM_NUDGE[i];
1718
+ const sp = Math.hypot(p.vx, p.vy);
1719
+ if (sp > ARM_MAX_SPEED) { p.vx *= ARM_MAX_SPEED / sp; p.vy *= ARM_MAX_SPEED / sp; }
1720
+ });
1721
+ }
1722
+ },
1723
+ step(dt) {
1724
+ let left = clamp(Number(dt) || 0, 0, 0.064);
1725
+ while (left > 1e-6) {
1726
+ const h = Math.min(1 / 240, left);
1727
+ left -= h;
1728
+ time += h;
1729
+ for (const a of arms) if (a.pts) stepArm(a, h);
1730
+ }
1731
+ },
1732
+ /** The end of the motion at once: a held arm grown and still, its hand on the target, everything else home. */
1733
+ finish() {
1734
+ for (const a of arms) {
1735
+ a.pokeAt = -1;
1736
+ a.flickUntil = -1;
1737
+ a.ve = 0;
1738
+ a.e = a.held ? a.goal : 0;
1739
+ a.pt = a.held ? 1 : 0;
1740
+ a.sq = 0;
1741
+ a.vsq = 0;
1742
+ a.vha = 0;
1743
+ a.sh = a.held ? { ...rootGoal(a), vx: 0, vy: 0 } : null;
1744
+ a.pts = a.held ? restOf(a, false).map(p => ({ x: p.x, y: p.y, vx: 0, vy: 0 })) : null;
1745
+ if (a.pts) a.ha = clearAngle(a, handGoal(a));
1746
+ }
1747
+ },
1748
+ /** Something still moves (or is held out). */
1749
+ awake: () => arms.some(moving),
1750
+ /** Only the held wiggle is left: no growth, poke, flick, squash, turn or jolt in flight. */
1751
+ settled() {
1752
+ return arms.every((a) => {
1753
+ if (!a.pts) return true;
1754
+ if (!a.held || a.pokeAt >= 0 || a.flickUntil >= 0 || Math.abs(a.ve) > 0.03 || Math.abs(a.e - a.goal) > 0.004) return false;
1755
+ if (Math.abs(a.sq) > 0.004 || Math.abs(a.vsq) > 0.05 || Math.abs(a.pt - 1) > 0.01 || Math.abs(a.vha) > 1) return false; // the wiggle turns it slowly
1756
+ const g = rootGoal(a);
1757
+ if (!a.sh || Math.abs(a.sh.x - g.x) + Math.abs(a.sh.y - g.y) > 0.3) return false; // the root still slides under a prop
1758
+ const rest = restOf(a, true);
1759
+ return a.pts.every((p, i) => Math.abs(p.x - rest[i].x) + Math.abs(p.y - rest[i].y) < 1.6); // the springs trail the wiggle a little
1760
+ });
1761
+ },
1762
+ /** How far out the arms are (0 both home, 1 one grown): the eyes keep to MASCOT_ARM.gaze this much. */
1763
+ presence() {
1764
+ let e = 0;
1765
+ for (const a of arms) if (a.pts) e = Math.max(e, a.held ? 1 : a.e);
1766
+ return clamp01(e * 2.5);
1767
+ },
1768
+ /**
1769
+ * The face-unit box the held arm takes once it has grown (its rest at full
1770
+ * stretch, the hand turned to its target, the fingertip), or null: what a
1771
+ * host keeps its own drawing out of.
1772
+ */
1773
+ extent() {
1774
+ const a = arms.find(x => x.held);
1775
+ if (!a) return null;
1776
+ const s = rootGoal(a);
1777
+ const pts = [s, ...armRest(a.side, a.angle, a.goal, null, s)];
1778
+ const hand = pts[3];
1779
+ const dir = a.target ? Math.atan2(a.target.y - hand.y, a.target.x - hand.x) : Math.atan2(hand.y - pts[2].y, hand.x - pts[2].x);
1780
+ const tip = { x: hand.x + Math.cos(dir) * HAND_TIP, y: hand.y + Math.sin(dir) * HAND_TIP };
1781
+ const r = MASCOT_ARM.hand.palm * 1.1;
1782
+ const xs = [...pts.map(p => p.x), tip.x];
1783
+ const ys = [...pts.map(p => p.y), tip.y];
1784
+ return { left: Math.min(...xs) - r, top: Math.min(...ys) - r, right: Math.max(...xs) + r, bottom: Math.max(...ys) + r };
1785
+ },
1786
+ /** The side held out (0 left, 1 right), or -1. */
1787
+ get side() { const a = arms.find(x => x.held); return a ? a.side : -1; },
1788
+ /**
1789
+ * Per side: null (nothing to draw) or { e, span, points: [shoulder, a third, two thirds, hand], aim,
1790
+ * point, squash, hand, target }: `span` the reach it was sent to (the hand grows with e / span), `aim` the angle the arm was asked for (0 straight out, + down),
1791
+ * `point` 0-1 how far the hand points (1 the index out, 0 a relaxed hand), `squash` the poke's
1792
+ * squash, `hand` the hand's angle (radians, screen coordinates), `target` the point (face units)
1793
+ * its index aims at, or null.
1794
+ */
1795
+ frame() {
1796
+ return arms.map((a) => {
1797
+ if (!a.pts || a.e <= 0.004) return null;
1798
+ const sh = rootOf(a);
1799
+ return {
1800
+ e: a.e,
1801
+ span: a.span,
1802
+ points: [{ x: sh.x, y: sh.y }, ...a.pts.map(p => ({ x: p.x, y: p.y }))],
1803
+ aim: a.angle,
1804
+ point: a.pt,
1805
+ squash: a.sq,
1806
+ hand: a.ha,
1807
+ target: a.target ? { ...a.target } : null,
1808
+ };
1809
+ });
1810
+ },
1811
+ };
1812
+ }
1813
+
934
1814
  // ── Painter: frame → attributes, each written only when it changed ──
935
1815
 
936
1816
  let uidSeq = 0;
@@ -1081,6 +1961,7 @@ function unregister(rig) {
1081
1961
 
1082
1962
  const BLEND_MS = 160;
1083
1963
  const AIM_K = 140; // px: the cursor distance at which the eyes travel half way
1964
+ const ARM_GAP_PX = 6; // px: how far short of what it points at a fingertip stops, unless the host says
1084
1965
  const GLANCES = [[-0.8, -0.35], [0.85, -0.25], [0.45, 0.6], [-0.5, 0.55], [0.1, -0.7]];
1085
1966
  const TRACK_TAU = 200; // the v1 lerp of .08 per 60 Hz frame
1086
1967
  const SETTLE_UNITS = 0.25; // gaze closer than this (viewBox units, a tenth of a pixel at 120 px) has arrived
@@ -1102,7 +1983,7 @@ export function createMascot(container, options) {
1102
1983
  }
1103
1984
  const {
1104
1985
  width = 128, height = 64, rangeX, rangeY, className = 'synabun-mascot',
1105
- pose: firstPose = 'idle', poseOptions: firstOptions, active: startActive,
1986
+ pose: firstPose = 'idle', poseOptions: firstOptions, active: startActive, arms: armsOption,
1106
1987
  } = opts || {};
1107
1988
  const markup = mascotSvg({ width, height, className });
1108
1989
  let svg;
@@ -1149,6 +2030,9 @@ export function createMascot(container, options) {
1149
2030
  let stopPointer = null;
1150
2031
  let stopKey = null;
1151
2032
  let wakeTimer = 0;
2033
+ // Arms only on a character big enough to read them (the empty state's, 120 px): never a mini, a cameo or a still.
2034
+ const armsAllowed = armsOption != null ? !!armsOption : Number(width) >= 96;
2035
+ let arms = null; // { sim, parts, target, unitPx, box, frame, stopWatch } once point() was first called
1152
2036
 
1153
2037
  const def = () => POSES[pose.name];
1154
2038
  const animating = () => !destroyed && active && visible && !isReducedMotion();
@@ -1219,12 +2103,15 @@ export function createMascot(container, options) {
1219
2103
  function nextRate() {
1220
2104
  if (!animating()) return 0;
1221
2105
  if (blend || blinkStart >= 0 || reactions.length || aim || easing() || miniPopping()) return 60;
2106
+ const armRate = arms?.sim.awake() ? (arms.sim.settled() ? 30 : 60) : 0;
2107
+ if (armRate === 60) return 60;
1222
2108
  const d = def();
1223
2109
  const t = poseTime();
1224
- if (d.motion === 'loop') return 30;
1225
- if (d.motion === 'blink') return d.idleFor(t) > 0 ? 0 : 30;
1226
- if (d.motion === 'once') return t < d.settle ? 60 : 0;
1227
- return 0;
2110
+ let rate = 0;
2111
+ if (d.motion === 'loop') rate = 30;
2112
+ else if (d.motion === 'blink') rate = d.idleFor(t) > 0 ? 0 : 30;
2113
+ else if (d.motion === 'once') rate = t < d.settle ? 60 : 0;
2114
+ return Math.max(rate, armRate);
1228
2115
  }
1229
2116
 
1230
2117
  function clearWake() {
@@ -1359,7 +2246,15 @@ export function createMascot(container, options) {
1359
2246
  clearTempClasses();
1360
2247
  look = def().look ? { ...hostLook } : { x: 0, y: 0 };
1361
2248
  lastFrame = stillFrameNow();
1362
- painter.paint(lastFrame);
2249
+ if (arms) { arms.sim.prop(lastFrame?.prop?.name || null); arms.sim.finish(); }
2250
+ paintFace(lastFrame);
2251
+ if (arms) paintArms();
2252
+ }
2253
+
2254
+ /** Paint a face frame; while an arm is out its eyes keep to MASCOT_ARM.gaze (never toward an arm). */
2255
+ function paintFace(f) {
2256
+ if (arms) arms.sim.prop(f?.prop?.name || null); // an arm whose shoulder this prop covers roots under it
2257
+ painter.paint(arms ? armGaze(f, arms.sim.presence()) : f);
1363
2258
  }
1364
2259
 
1365
2260
  function compose() {
@@ -1430,7 +2325,9 @@ export function createMascot(container, options) {
1430
2325
  handleDue();
1431
2326
  if (aim) { aimAt(aim); aim = null; }
1432
2327
  easeLook(dt);
1433
- painter.paint(compose());
2328
+ if (arms) arms.sim.step(dt / 1000);
2329
+ paintFace(compose());
2330
+ if (arms) paintArms();
1434
2331
  const rate = nextRate();
1435
2332
  setRate(rate);
1436
2333
  if (!rate) armWake();
@@ -1461,7 +2358,7 @@ export function createMascot(container, options) {
1461
2358
  const rate = nextRate();
1462
2359
  setRate(rate);
1463
2360
  if (!rate) {
1464
- painter.paint(compose());
2361
+ paintFace(compose());
1465
2362
  armWake();
1466
2363
  } else clearWake();
1467
2364
  }
@@ -1492,6 +2389,162 @@ export function createMascot(container, options) {
1492
2389
  if (!destroyed && pose.name === 'sleep') setPose('idle');
1493
2390
  }
1494
2391
 
2392
+ // ── Arms (rig.point): measured once per target, stepped on this rig's own frames ──
2393
+
2394
+ function ensureArms() {
2395
+ if (arms) return arms;
2396
+ const face = svg.querySelector('.syna-face');
2397
+ if (!face) return null;
2398
+ face.insertAdjacentHTML('afterbegin', '<g class="syna-arms">'
2399
+ + ['left', 'right'].map(side => `<g class="syna-arm" data-side="${side}" display="none"><path class="syna-arm-noodle" fill="currentColor" fill-rule="nonzero"/></g>`).join('')
2400
+ + '</g>');
2401
+ const parts = Array.from(face.firstElementChild.children).map(g => ({ g, noodle: g.firstElementChild, shown: false, d: '', reach: '' }));
2402
+ // box / rect: this SVG's and the target's client rects as last read (shift() moves the box without a read).
2403
+ arms = { sim: createArmSim(), parts, target: null, gap: ARM_GAP_PX, unitPx: 0, box: null, rect: null, frame: 0, stopWatch: null };
2404
+ arms.sim.prop((lastFrame || stillFrameNow())?.prop?.name || null);
2405
+ return arms;
2406
+ }
2407
+
2408
+ /** Draw what the sim holds: one path per arm (noodle and hand), written only when it changed; `data-reach` is the arm's e. */
2409
+ function paintArms() {
2410
+ if (!arms) return;
2411
+ const frame = arms.sim.frame();
2412
+ const unitPx = arms.unitPx || MASCOT_ARM.unitPx;
2413
+ frame.forEach((f, side) => {
2414
+ const part = arms.parts[side];
2415
+ const d = f ? armDrawPath(f, side, { unitPx }) : '';
2416
+ if (!d) {
2417
+ if (part.shown) { part.g.setAttribute('display', 'none'); part.shown = false; }
2418
+ return;
2419
+ }
2420
+ if (!part.shown) { part.g.removeAttribute('display'); part.shown = true; }
2421
+ if (d !== part.d) { part.d = d; part.noodle.setAttribute('d', d); }
2422
+ const reach = f.e.toFixed(3);
2423
+ if (reach !== part.reach) { part.reach = reach; part.g.setAttribute('data-reach', reach); }
2424
+ });
2425
+ if (!arms.sim.awake() && !arms.target) stopArmWatch();
2426
+ }
2427
+
2428
+ /** A target's client rect: an element (read now), a rect, or a point. */
2429
+ function targetBox(target) {
2430
+ if (!target) return null;
2431
+ if (typeof target.getBoundingClientRect === 'function') {
2432
+ if (target.isConnected === false) return null;
2433
+ const r = target.getBoundingClientRect();
2434
+ return r.width || r.height ? r : null;
2435
+ }
2436
+ if (Number.isFinite(target.left) && Number.isFinite(target.top)) {
2437
+ const w = Number(target.width) || Math.max(0, (Number(target.right) || target.left) - target.left);
2438
+ const h = Number(target.height) || Math.max(0, (Number(target.bottom) || target.top) - target.top);
2439
+ return { left: target.left, top: target.top, right: target.left + w, bottom: target.top + h, width: w, height: h };
2440
+ }
2441
+ if (Number.isFinite(target.x) && Number.isFinite(target.y)) return { left: target.x, top: target.y, right: target.x, bottom: target.y, width: 0, height: 0 };
2442
+ return null;
2443
+ }
2444
+
2445
+ /**
2446
+ * Two layout reads (this SVG and the target), never per frame: then aim
2447
+ * (aimArms) from what was read.
2448
+ */
2449
+ function measureArms() {
2450
+ if (!arms?.target || destroyed) return;
2451
+ const box = svg.getBoundingClientRect();
2452
+ const r = targetBox(arms.target);
2453
+ if (!box.width || !r) return;
2454
+ arms.box = { left: box.left, top: box.top, width: box.width, height: box.height };
2455
+ arms.rect = { left: r.left, top: r.top, right: r.right, bottom: r.bottom };
2456
+ aimArms();
2457
+ }
2458
+
2459
+ /**
2460
+ * From the last rects, no layout read: where the target is in face units, the
2461
+ * arm on its side aims there (its index at the target's middle, its fingertip
2462
+ * stopping `gap` px short of it) and the eyes look the same way.
2463
+ */
2464
+ function aimArms() {
2465
+ const box = arms?.box;
2466
+ const r = arms?.rect;
2467
+ if (!box || !r) return;
2468
+ const k = box.width / 280; // CSS px per face unit: the viewBox is 280 wide, the SVG keeps its ratio
2469
+ arms.unitPx = k;
2470
+ const rect = { left: (r.left - box.left) / k, top: (r.top - box.top) / k, right: (r.right - box.left) / k, bottom: (r.bottom - box.top) / k };
2471
+ const side = armSide({ x: (rect.left + rect.right) / 2 });
2472
+ const aimAt = armTargeting(side, rect, arms.gap / k, arms.sim.root(side));
2473
+ if (!aimAt) return;
2474
+ arms.sim.aim(side, aimAt.angle, { target: aimAt.focus, reach: aimAt.reach });
2475
+ // The eyes go the same way, through the rig's own gaze.
2476
+ const vx = (r.left + r.right) / 2 - (box.left + box.width / 2);
2477
+ const vy = (r.top + r.bottom) / 2 - (box.top + box.height / 2);
2478
+ const dist = Math.hypot(vx, vy);
2479
+ if (dist >= 1) {
2480
+ const m = (dist / (dist + AIM_K)) * 1.25;
2481
+ lookAt((vx / dist) * m, (vy / dist) * m);
2482
+ }
2483
+ }
2484
+
2485
+ /** While pointing, a resize or a scroll measures again on the next frame (coalesced). */
2486
+ function watchArms() {
2487
+ if (!arms || arms.stopWatch || typeof window === 'undefined') return;
2488
+ const again = () => {
2489
+ if (!arms || arms.frame) return;
2490
+ arms.frame = requestAnimationFrame(() => { if (!arms) return; arms.frame = 0; measureArms(); wakeArms(); });
2491
+ };
2492
+ window.addEventListener('resize', again, { passive: true });
2493
+ document.addEventListener('scroll', again, { passive: true, capture: true });
2494
+ arms.stopWatch = () => {
2495
+ window.removeEventListener('resize', again);
2496
+ document.removeEventListener('scroll', again, true);
2497
+ };
2498
+ }
2499
+
2500
+ function stopArmWatch() {
2501
+ if (!arms?.stopWatch) return;
2502
+ arms.stopWatch();
2503
+ arms.stopWatch = null;
2504
+ if (arms.frame) { cancelAnimationFrame(arms.frame); arms.frame = 0; }
2505
+ }
2506
+
2507
+ function wakeArms() {
2508
+ if (destroyed || !arms) return;
2509
+ if (!animating()) {
2510
+ const f = lastFrame || stillFrameNow();
2511
+ arms.sim.prop(f?.prop?.name || null);
2512
+ arms.sim.finish();
2513
+ paintArms();
2514
+ paintFace(f);
2515
+ return;
2516
+ }
2517
+ syncClock();
2518
+ ensureRate(arms.sim.settled() ? 30 : 60);
2519
+ requestFrames(BLEND_MS);
2520
+ }
2521
+
2522
+ /**
2523
+ * Point at `target` (an element, a client rect or a client point) with the
2524
+ * arm on its side; null puts the arms away. `gap` (CSS px, default
2525
+ * ARM_GAP_PX) is how far short of the target the fingertip stops (a host that
2526
+ * rings the target keeps it outside the ring). Pointing again (the same target
2527
+ * or a new one) measures again and carries on from the springs' state.
2528
+ */
2529
+ function point(target, { poke: andPoke = false, gap } = {}) {
2530
+ if (destroyed || !armsAllowed) return api;
2531
+ if (target == null) {
2532
+ if (!arms) return api;
2533
+ arms.target = null;
2534
+ arms.sim.release();
2535
+ wakeArms();
2536
+ return api;
2537
+ }
2538
+ if (!ensureArms()) return api;
2539
+ if (gap != null || target !== arms.target) arms.gap = Number(gap) >= 0 ? Number(gap) : ARM_GAP_PX;
2540
+ arms.target = target;
2541
+ measureArms();
2542
+ watchArms();
2543
+ if (andPoke) arms.sim.poke();
2544
+ wakeArms();
2545
+ return api;
2546
+ }
2547
+
1495
2548
  // ── Public API ──
1496
2549
 
1497
2550
  function setPose(name, options2) {
@@ -1577,6 +2630,7 @@ export function createMascot(container, options) {
1577
2630
  stopKey?.();
1578
2631
  stopKey = null;
1579
2632
  unobserve(svg);
2633
+ stopArmWatch();
1580
2634
  svg.removeEventListener('animationend', onAnimationEnd);
1581
2635
  clearTempClasses();
1582
2636
  reactions.length = 0;
@@ -1608,6 +2662,58 @@ export function createMascot(container, options) {
1608
2662
  setPose,
1609
2663
  react,
1610
2664
  setActive,
2665
+ point,
2666
+ /** Two quick pokes toward what the arm points at (the beat landed). */
2667
+ poke() {
2668
+ if (destroyed || !arms?.target) return api;
2669
+ arms.sim.poke();
2670
+ wakeArms();
2671
+ return api;
2672
+ },
2673
+ /**
2674
+ * The body was jolted: its velocity changed by (vx, vy) CSS px/s (a host
2675
+ * moving the character's box: + on take-off, − on landing). The arms keep
2676
+ * part of their old speed, so they trail and then wobble back.
2677
+ */
2678
+ nudge(vx, vy) {
2679
+ if (destroyed || !arms?.unitPx || !animating()) return api;
2680
+ arms.sim.nudge((Number(vx) || 0) / arms.unitPx, (Number(vy) || 0) / arms.unitPx);
2681
+ wakeArms();
2682
+ return api;
2683
+ },
2684
+ /**
2685
+ * The host moved the character's box by (dx, dy) CSS px since the last
2686
+ * measure (its own transform: no layout read here): the hand keeps
2687
+ * pointing at the same place on the page.
2688
+ */
2689
+ shift(dx, dy) {
2690
+ if (destroyed || !arms?.target || !arms.box) return api;
2691
+ const x = Number(dx) || 0;
2692
+ const y = Number(dy) || 0;
2693
+ if (!x && !y) return api;
2694
+ arms.box.left += x;
2695
+ arms.box.top += y;
2696
+ aimArms();
2697
+ wakeArms();
2698
+ return api;
2699
+ },
2700
+ /** The client box the pointing arm takes once grown (from the last measure: no layout read), or null. */
2701
+ armExtent() {
2702
+ if (destroyed || !arms?.target || !arms.box) return null;
2703
+ const e = arms.sim.extent();
2704
+ if (!e) return null;
2705
+ const k = arms.box.width / 280;
2706
+ return { left: arms.box.left + e.left * k, top: arms.box.top + e.top * k, right: arms.box.left + e.right * k, bottom: arms.box.top + e.bottom * k };
2707
+ },
2708
+ /** Read the rects again (the host's move has landed, its scale settled) and aim from them. */
2709
+ remeasure() {
2710
+ if (destroyed || !arms?.target) return api;
2711
+ measureArms();
2712
+ wakeArms();
2713
+ return api;
2714
+ },
2715
+ /** What the arms point at (null when they are away). */
2716
+ get pointing() { return arms?.target || null; },
1611
2717
  /** The current pose name. */
1612
2718
  get pose() { return pose.name; },
1613
2719
  /** The frame rate this rig asks of the ticker right now (0 = still). */