@cardstack/boxel-cli 0.12.0 → 0.13.0-unstable.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 (178) hide show
  1. package/bundled-realms/pretui/components/chip.gts +47 -2
  2. package/bundled-realms/pretui/components/chip.md +5 -2
  3. package/bundled-realms/pretui/components/chip.usage.gts +37 -2
  4. package/bundled-realms/pretui/components/status-chip.gts +12 -2
  5. package/bundled-realms/pretui/components/status-chip.md +2 -1
  6. package/bundled-realms/pretui/components/status-chip.usage.gts +2 -0
  7. package/bundled-test-harness/assets/{ai-module-creation-test-B9lSKNq1.js → ai-module-creation-test-Czsh1MyP.js} +1 -1
  8. package/bundled-test-harness/assets/{aibot-message-LXV0UlzI.js → aibot-message-Dg5nzE1b.js} +1 -1
  9. package/bundled-test-harness/assets/{app-BIQcKrVT.js → app-hXF_bqWy.js} +6 -6
  10. package/bundled-test-harness/assets/{assistant-transcript-test-CG_2-bog.js → assistant-transcript-test-kILRv_pf.js} +1 -1
  11. package/bundled-test-harness/assets/{binary-upload-dedupe-test-Q7e6bzl1.js → binary-upload-dedupe-test-Bt5RqtEk.js} +1 -1
  12. package/bundled-test-harness/assets/{bundled-base-DldokUSJ.js → bundled-base-DtxlKFXx.js} +3 -3
  13. package/bundled-test-harness/assets/{bundled-base-modules-test-VAlRx4t3.js → bundled-base-modules-test-D1fXHMTE.js} +1 -1
  14. package/bundled-test-harness/assets/{can-invoke-test-BGmUJHxo.js → can-invoke-test-DqBuecdl.js} +1 -1
  15. package/bundled-test-harness/assets/{card-chooser-paging-test-CzooKL4I.js → card-chooser-paging-test-Cf8r032c.js} +1 -1
  16. package/bundled-test-harness/assets/{card-chooser-test-C3fjaDYi.js → card-chooser-test-q_qnUKxi.js} +1 -1
  17. package/bundled-test-harness/assets/{card-def-field-def-relationships-test-yqv8ir8Y.js → card-def-field-def-relationships-test-C5xkWMZi.js} +1 -1
  18. package/bundled-test-harness/assets/{card-delete-test-BtjqrqKO.js → card-delete-test-KB4aJuv_.js} +1 -1
  19. package/bundled-test-harness/assets/{chip-D6Xffhjx.css → chip-B2Ns53A6.css} +35 -9
  20. package/bundled-test-harness/assets/{chip-BydgljDL.js → chip-DVJE_YDh.js} +11 -3
  21. package/bundled-test-harness/assets/{choreo-D7dLCdGB.js → choreo-CIrop3HM.js} +16 -35
  22. package/bundled-test-harness/assets/choreo-Ca2Y2sF3.js +2 -0
  23. package/bundled-test-harness/assets/{choreo-feature-reel-test--x-L3ZKc.js → choreo-feature-reel-test-CJHms7Sf.js} +2 -2
  24. package/bundled-test-harness/assets/{choreo-gallery-ZO9vXOxd.js → choreo-gallery-DZm1KZ20.js} +3 -3
  25. package/bundled-test-harness/assets/{choreo-gallery-build-order-test-DG5UTyc9.js → choreo-gallery-build-order-test-QKyMWELN.js} +4 -4
  26. package/bundled-test-harness/assets/{choreo-gallery-compositor-test-DJP80SNj.js → choreo-gallery-compositor-test-BQDptxfE.js} +1 -1
  27. package/bundled-test-harness/assets/{choreo-gallery-cover-test-CHYz7sM4.js → choreo-gallery-cover-test-Do-HYF96.js} +1 -1
  28. package/bundled-test-harness/assets/{choreo-gallery-crossing-stage-test-BuinlINr.js → choreo-gallery-crossing-stage-test-yJVXghRc.js} +10 -10
  29. package/bundled-test-harness/assets/{choreo-gallery-demos-alive-test-BjNCGfH9.js → choreo-gallery-demos-alive-test-BCeaaZye.js} +1 -1
  30. package/bundled-test-harness/assets/{choreo-gallery-drift-test-BJ17f3yv.js → choreo-gallery-drift-test-CJ1yq6R0.js} +1 -1
  31. package/bundled-test-harness/assets/{choreo-gallery-escort-test-Dadih8-S.js → choreo-gallery-escort-test-DypqSgvs.js} +1 -1
  32. package/bundled-test-harness/assets/{choreo-gallery-films-test-CCXty_WI.js → choreo-gallery-films-test-ClMy5M_p.js} +2 -2
  33. package/bundled-test-harness/assets/{choreo-gallery-filter-test-BozCoT_M.js → choreo-gallery-filter-test-CzOmgqgl.js} +1 -1
  34. package/bundled-test-harness/assets/{choreo-gallery-fold-test-Kez2P3_Y.js → choreo-gallery-fold-test-qNnH6wtC.js} +2 -2
  35. package/bundled-test-harness/assets/{choreo-gallery-guide-pages-test-BV5oxrYA.js → choreo-gallery-guide-pages-test-BtKWMkTv.js} +1 -1
  36. package/bundled-test-harness/assets/{choreo-gallery-guides-test-DwYBDgKi.js → choreo-gallery-guides-test-CXMsuDYi.js} +4 -3
  37. package/bundled-test-harness/assets/{choreo-gallery-hang-test-B-5u7cGJ.js → choreo-gallery-hang-test-BD4JQ04e.js} +2 -2
  38. package/bundled-test-harness/assets/{choreo-gallery-highlight-test-BZ5Lg8V6.js → choreo-gallery-highlight-test-BK280K2W.js} +2 -2
  39. package/bundled-test-harness/assets/{choreo-gallery-inline-edit-test-wSlSI-VM.js → choreo-gallery-inline-edit-test-DAgUcaaw.js} +2 -2
  40. package/bundled-test-harness/assets/{choreo-gallery-interruption-test-DBpCEiro.js → choreo-gallery-interruption-test-DY5KUilC.js} +4 -4
  41. package/bundled-test-harness/assets/{choreo-gallery-lone-stack-test-aMLrHsns.js → choreo-gallery-lone-stack-test-Dxd_Nb0-.js} +4 -4
  42. package/bundled-test-harness/assets/{choreo-gallery-navigation-test-B8b-2ZTz.js → choreo-gallery-navigation-test-DWJq12PN.js} +1 -1
  43. package/bundled-test-harness/assets/{choreo-gallery-playhead-transport-test-B95CYBh7.js → choreo-gallery-playhead-transport-test-DlQ6yTKW.js} +1 -1
  44. package/bundled-test-harness/assets/{choreo-gallery-poplayout-subtree-test-jZAAICV6.js → choreo-gallery-poplayout-subtree-test-D59W8gYt.js} +5 -5
  45. package/bundled-test-harness/assets/{choreo-gallery-presence-modes-test-BawIKB-a.js → choreo-gallery-presence-modes-test-BAU-dggP.js} +2 -2
  46. package/bundled-test-harness/assets/{choreo-gallery-presentation-test-CYIb-8ML.js → choreo-gallery-presentation-test-BqGsinE0.js} +2 -2
  47. package/bundled-test-harness/assets/{choreo-gallery-reduced-motion-test-C6Vi1WYr.js → choreo-gallery-reduced-motion-test-By0rb2Ai.js} +4 -3
  48. package/bundled-test-harness/assets/{choreo-gallery-sheet-standalone-test-_e4T5KDI.js → choreo-gallery-sheet-standalone-test-Cfw-4ULF.js} +2 -2
  49. package/bundled-test-harness/assets/{choreo-gallery-sheet-test-Cv51Te3G.js → choreo-gallery-sheet-test-dm53Pdzb.js} +2 -2
  50. package/bundled-test-harness/assets/{choreo-gallery-site-test-Cm35kHu_.js → choreo-gallery-site-test-SAlqtZgL.js} +3 -2
  51. package/bundled-test-harness/assets/{choreo-gallery-stage-Bp6QVqlI.js → choreo-gallery-stage-CU8LvQqX.js} +2 -2
  52. package/bundled-test-harness/assets/{choreo-gallery-stage-keyboard-test-DDX2cuMa.js → choreo-gallery-stage-keyboard-test-DQ4v5qA4.js} +2 -2
  53. package/bundled-test-harness/assets/{choreo-gallery-stages-test-Dfa_33_4.js → choreo-gallery-stages-test-CRvaS6w6.js} +2 -2
  54. package/bundled-test-harness/assets/{choreo-gallery-subdivision-test-CgR3WdcT.js → choreo-gallery-subdivision-test-CtOm-U35.js} +3 -3
  55. package/bundled-test-harness/assets/{choreo-gallery-table-plan-test-DF-cE3J9.js → choreo-gallery-table-plan-test-_0koIm2u.js} +1 -1
  56. package/bundled-test-harness/assets/{choreo-gallery-theater-test-BuIyb2ke.js → choreo-gallery-theater-test-yedwQVyP.js} +4 -4
  57. package/bundled-test-harness/assets/{choreo-gallery-tuning-test-BdZ45YBK.js → choreo-gallery-tuning-test-CmMtfJ5a.js} +1 -1
  58. package/bundled-test-harness/assets/{choreo-gallery-widget-room-test-HcPX75Dg.js → choreo-gallery-widget-room-test-jnYOas5x.js} +2 -2
  59. package/bundled-test-harness/assets/{choreo-gallery-wires-test-BhFCIjB1.js → choreo-gallery-wires-test-CSyqjVnm.js} +2 -2
  60. package/bundled-test-harness/assets/clip--wioup_3.js +2 -0
  61. package/bundled-test-harness/assets/{clip-BmBVqUyU.js → clip-Cm32sLyn.js} +3 -3
  62. package/bundled-test-harness/assets/{codeblocks-test-DbZYlmG_.js → codeblocks-test-DLVRpBAl.js} +1 -1
  63. package/bundled-test-harness/assets/{compaction-test-D1G3KCSe.js → compaction-test-zX01t57K.js} +1 -1
  64. package/bundled-test-harness/assets/{container-XSX5809v.js → container-CXZoEvul.js} +10 -9
  65. package/bundled-test-harness/assets/{create-listing-modal-test--oGT9P_F.js → create-listing-modal-test-DPgVovcL.js} +1 -1
  66. package/bundled-test-harness/assets/{debug-message-test-C8UZUtCt.js → debug-message-test-CWl-ido2.js} +1 -1
  67. package/bundled-test-harness/assets/{externals--YMzwHwd.js → externals-Ddhtv753.js} +37 -37
  68. package/bundled-test-harness/assets/{fallback-banner-live-delete-test-DPK_coJY.js → fallback-banner-live-delete-test-CFbMEFHo.js} +1 -1
  69. package/bundled-test-harness/assets/{fallback-banner-test-C2wi6Lu6.js → fallback-banner-test-3O0imkxJ.js} +1 -1
  70. package/bundled-test-harness/assets/{fallback-models-test-SqdXWB1Z.js → fallback-models-test-hyQfQLuM.js} +1 -1
  71. package/bundled-test-harness/assets/{file-attachment-test-DT0HK6yS.js → file-attachment-test-C63Ttyaj.js} +1 -1
  72. package/bundled-test-harness/assets/{film-Zt2YoVoG.js → film-9V69qBWw.js} +6 -6
  73. package/bundled-test-harness/assets/{film-77OfTGL7.js → film-Z87hSVo_.js} +8 -8
  74. package/bundled-test-harness/assets/film-h1N8vUjG.js +2 -0
  75. package/bundled-test-harness/assets/{formatted-aibot-message-test-B6i0k9wW.js → formatted-aibot-message-test-DH3WPvU1.js} +1 -1
  76. package/bundled-test-harness/assets/{general-test-C0Xb2Idr.js → general-test-wrSeNkrm.js} +1 -1
  77. package/bundled-test-harness/assets/{glimmer-motion-and-choreo-in-cards-test-DFlkZtek.js → glimmer-motion-and-choreo-in-cards-test-Byg3fSOY.js} +32 -32
  78. package/bundled-test-harness/assets/{glimmer-motion-and-choreo-test-B2ustNm9.js → glimmer-motion-and-choreo-test-BM7uX8k4.js} +10 -10
  79. package/bundled-test-harness/assets/group-Cgg5RLul.js +2 -0
  80. package/bundled-test-harness/assets/{group-BNxp6qON.js → group-DL-b_Hzj.js} +2 -2
  81. package/bundled-test-harness/assets/{index-query-engine-test-BKOqgBMv.js → index-query-engine-test-DvfKgn6s.js} +1 -1
  82. package/bundled-test-harness/assets/{instance-filter-matcher-test-CNIzKZvP.js → instance-filter-matcher-test-KqGi-K79.js} +1 -1
  83. package/bundled-test-harness/assets/item-Bu9hhPf2.js +2 -0
  84. package/bundled-test-harness/assets/{item-BDyVqxdA.js → item-CBEoTOyj.js} +2 -2
  85. package/bundled-test-harness/assets/{main-BiI-FhMc.js → main-CoUo04VF.js} +1 -1
  86. package/bundled-test-harness/assets/{markdown-preview-BItI_AEl.js → markdown-preview-D_QssEOi.js} +1 -1
  87. package/bundled-test-harness/assets/{markdown-preview-test-Bntyikh1.js → markdown-preview-test-nljzB8o_.js} +1 -1
  88. package/bundled-test-harness/assets/memory-B73-H_Rp.js +223 -0
  89. package/bundled-test-harness/assets/{model-cost-tier-test-Hu2-LpEw.js → model-cost-tier-test-B5ByLtAP.js} +1 -1
  90. package/bundled-test-harness/assets/{motion-DeM7UCtd.js → motion-BiMvMS08.js} +1 -1
  91. package/bundled-test-harness/assets/{node-BAM535H5.js → node-BWOLCOMI.js} +95 -6
  92. package/bundled-test-harness/assets/{operation-lowering-test-CG1gatuH.js → operation-lowering-test-Bz81V0Gt.js} +1 -1
  93. package/bundled-test-harness/assets/{operator-mode-card-chooser-test-Bo5ZnF_x.js → operator-mode-card-chooser-test-DNOnplV8.js} +1 -1
  94. package/bundled-test-harness/assets/{operator-mode-links-test-DVx_LQOU.js → operator-mode-links-test-BeXsOYMU.js} +1 -1
  95. package/bundled-test-harness/assets/{operator-mode-test-CCbFrYgN.js → operator-mode-test-jdeCXqhV.js} +1 -1
  96. package/bundled-test-harness/assets/{operator-mode-ui-test-Dm2Bf6Vn.js → operator-mode-ui-test-DdiFZylM.js} +1 -1
  97. package/bundled-test-harness/assets/{overlay-menu-items-test-Bk5AidFD.js → overlay-menu-items-test-C_Q4sJlC.js} +1 -1
  98. package/bundled-test-harness/assets/{overlays-CY7IgeOl.js → overlays-DZXjQykn.js} +3 -3
  99. package/bundled-test-harness/assets/{overlays-DhVKmSP-.js → overlays-hrAO43jG.js} +1 -1
  100. package/bundled-test-harness/assets/{panel-t9QyU1lY.js → panel-CXmB6GMs.js} +15 -75
  101. package/bundled-test-harness/assets/{panel-CUYJ8oHS.css → panel-D2zkZoIr.css} +39 -31
  102. package/bundled-test-harness/assets/{past-sessions-test-Dq-Y9T6W.js → past-sessions-test-Bm1UiLD0.js} +1 -1
  103. package/bundled-test-harness/assets/{permission-checking-realm-test-CuImx8ie.js → permission-checking-realm-test-Jq7R-yRT.js} +1 -1
  104. package/bundled-test-harness/assets/pill-menu-Ctn3JNqG.js +154 -0
  105. package/bundled-test-harness/assets/{pill-menu-test-kpWBGrX5.js → pill-menu-test-Cf2xO2w6.js} +1 -1
  106. package/bundled-test-harness/assets/plate-CnatwP3n.js +2 -0
  107. package/bundled-test-harness/assets/{plate-B4Lz2Ulo.js → plate-Cu5EnXQZ.js} +3 -3
  108. package/bundled-test-harness/assets/player-Bwn5tEsh.js +2 -0
  109. package/bundled-test-harness/assets/{player-B965XH82.js → player-CVdsx7Ng.js} +3 -3
  110. package/bundled-test-harness/assets/presence-BKkR2au7.js +553 -0
  111. package/bundled-test-harness/assets/presence-CcEXVges.js +2 -0
  112. package/bundled-test-harness/assets/{quick-look-test-VW5_W7dm.js → quick-look-test-BrnxRRsq.js} +1 -1
  113. package/bundled-test-harness/assets/{realm-api-keys-sg--fohv.js → realm-api-keys-CHPnfcko.js} +1 -1
  114. package/bundled-test-harness/assets/realm-api-keys-GLBmvu4U.js +2 -0
  115. package/bundled-test-harness/assets/{realm-api-keys-test-Cy7qSoAd.js → realm-api-keys-test-CHBGOFcx.js} +1 -1
  116. package/bundled-test-harness/assets/{realm-config-DvpP1xf-.js → realm-config-C7c3B2er.js} +1 -1
  117. package/bundled-test-harness/assets/{realm-config-policy-standing-test-DJk-ftgH.js → realm-config-policy-standing-test-C8aNeb6_.js} +1 -1
  118. package/bundled-test-harness/assets/{realm-config-routing-rule-edit-test-CTPiJrAL.js → realm-config-routing-rule-edit-test-Dmnw-VQz.js} +1 -1
  119. package/bundled-test-harness/assets/{realm-config-settings-test-CUa-PjMC.js → realm-config-settings-test-C6V0Mx35.js} +1 -1
  120. package/bundled-test-harness/assets/{reasoning-test-CeeGGwvf.js → reasoning-test-Dn0Xb42V.js} +1 -1
  121. package/bundled-test-harness/assets/{rendered-markdown-B8TH0yN4.js → rendered-markdown-DPEXS7MQ.js} +1 -1
  122. package/bundled-test-harness/assets/{rendered-markdown-test-CBRsIBqW.js → rendered-markdown-test-DxbJ1nFX.js} +1 -1
  123. package/bundled-test-harness/assets/{room-message-test-BTxZ_W74.js → room-message-test-BBE8UIPX.js} +1 -1
  124. package/bundled-test-harness/assets/{run-BSEzn_PH.js → run-CfyzhBXb.js} +2 -2
  125. package/bundled-test-harness/assets/{scrolling-test-C7R0UASi.js → scrolling-test-sLBz7Gxv.js} +2 -2
  126. package/bundled-test-harness/assets/{searchable-option-test-BiOqxIIs.js → searchable-option-test-Ix9_H5QV.js} +1 -1
  127. package/bundled-test-harness/assets/{sending-test-DOXQ0Tvx.js → sending-test-_Pqzq1bN.js} +1 -1
  128. package/bundled-test-harness/assets/{skills-test-BjCYI4Z_.js → skills-test-DUHRQGiC.js} +1 -1
  129. package/bundled-test-harness/assets/src-CEw1V0fi.js +11 -0
  130. package/bundled-test-harness/assets/{src-Bhefkhqj.js → src-CbBcj3xm.js} +11 -5
  131. package/bundled-test-harness/assets/{src-BDDYD1rs.js → src-D8bpj2tb.js} +2 -2
  132. package/bundled-test-harness/assets/{src-DySBAkac.js → src-Df4Vutrk.js} +3 -3
  133. package/bundled-test-harness/assets/{stick-to-bottom-resize-test-hN0AjdXP.js → stick-to-bottom-resize-test-Du4B3w2x.js} +4 -4
  134. package/bundled-test-harness/assets/{store-test-BD99H9dQ.js → store-test-9C-s0-H6.js} +1 -1
  135. package/bundled-test-harness/assets/{test-support-BcjhIHQa.js → test-support-BOFeeYuE.js} +25 -2
  136. package/bundled-test-harness/assets/test-support-CXESl4Wf.js +3 -0
  137. package/bundled-test-harness/assets/test-support-CgWmpebE.js +3 -0
  138. package/bundled-test-harness/assets/{test-support-Dl6FChlV.js → test-support-xTaMuaOA.js} +4 -27
  139. package/bundled-test-harness/assets/{tests-BqLDuQog.js → tests-CVpu0hZj.js} +437 -437
  140. package/bundled-test-harness/assets/{text-input-validator-test-CW6Ay7bo.js → text-input-validator-test-CcVh-MFa.js} +1 -1
  141. package/bundled-test-harness/assets/{titles-CFt3dEls.js → titles-BUbpcMoe.js} +3 -3
  142. package/bundled-test-harness/assets/titles-D80DwOQU.js +2 -0
  143. package/bundled-test-harness/assets/{to-device-streaming-test-DRxNCi7Z.js → to-device-streaming-test-RFHD62PN.js} +1 -1
  144. package/bundled-test-harness/assets/{token-usage-test-Ck9rB3Ie.js → token-usage-test-B2iJC-UX.js} +1 -1
  145. package/bundled-test-harness/assets/{tools-test-B3zniq10.js → tools-test-BZOWfXML.js} +1 -1
  146. package/bundled-test-harness/assets/{turn-settling-test-C0X6hL7x.js → turn-settling-test-Cvu2rvzM.js} +1 -1
  147. package/bundled-test-harness/assets/{turn-summary-0XOoAv3j.js → turn-summary-CAZVdcBd.js} +1 -1
  148. package/bundled-test-harness/index.html +26 -25
  149. package/bundled-test-harness/tests/index.html +26 -25
  150. package/bundled-types/choreo/choreo.gts +21 -42
  151. package/bundled-types/choreo/test-support/index.ts +9 -30
  152. package/bundled-types/glimmer-motion/index.ts +11 -3
  153. package/bundled-types/glimmer-motion/memory.ts +302 -0
  154. package/bundled-types/glimmer-motion/node.ts +125 -14
  155. package/bundled-types/glimmer-motion/presence-types.ts +13 -5
  156. package/bundled-types/glimmer-motion/presence.gts +199 -11
  157. package/bundled-types/glimmer-motion/snapshot.ts +280 -0
  158. package/bundled-types/glimmer-motion/test-support/index.ts +36 -4
  159. package/bundled-types/host-app/components/ai-assistant/llm-select.gts +3 -0
  160. package/bundled-types/host-app/components/ai-assistant/skill-menu/index.gts +3 -0
  161. package/bundled-types/host-app/components/matrix/room.gts +15 -3
  162. package/bundled-types/host-app/components/pill-menu/index.gts +74 -23
  163. package/package.json +1 -1
  164. package/bundled-test-harness/assets/choreo-Cd9yKlEK.js +0 -2
  165. package/bundled-test-harness/assets/clip-DnEy6oK5.js +0 -2
  166. package/bundled-test-harness/assets/film-C4NJTl9U.js +0 -2
  167. package/bundled-test-harness/assets/group-CPQojb-T.js +0 -2
  168. package/bundled-test-harness/assets/item-DGzMAZ9K.js +0 -2
  169. package/bundled-test-harness/assets/pill-menu-DaRpDMUm.js +0 -68
  170. package/bundled-test-harness/assets/plate-B7JjCVMj.js +0 -2
  171. package/bundled-test-harness/assets/player-DpGXST02.js +0 -2
  172. package/bundled-test-harness/assets/presence-CmLmFwhO.js +0 -2
  173. package/bundled-test-harness/assets/presence-Sv4y4tQy.js +0 -230
  174. package/bundled-test-harness/assets/realm-api-keys-DkdlXgAg.js +0 -2
  175. package/bundled-test-harness/assets/src-BpmcuCfH.js +0 -10
  176. package/bundled-test-harness/assets/test-support-CB3auo8h.js +0 -2
  177. package/bundled-test-harness/assets/test-support-DfGKwg35.js +0 -3
  178. package/bundled-test-harness/assets/titles-DVhrJZzS.js +0 -2
@@ -1,13 +1,15 @@
1
- import { registerDestructor } from '@ember/destroyable';
1
+ import { isDestroying, registerDestructor } from '@ember/destroyable';
2
2
  import type Owner from '@ember/owner';
3
3
  import Component from '@glimmer/component';
4
4
  import { tracked } from '@glimmer/tracking';
5
- import type { PresenceContextProps } from 'motion-dom';
5
+ import { consumeTag, VOLATILE_TAG } from '@glimmer/validator';
6
+ import { frame, type PresenceContextProps } from 'motion-dom';
6
7
 
7
8
  import { snapshotOnRender } from './layout-group.gts';
8
9
  import { flushPendingMounts } from './node.ts';
9
10
  import type { PopMeasurable, PresenceHandle } from './presence-types.ts';
10
11
  import { postRender } from './scheduler.ts';
12
+ import { captureExit, type ExitCopy } from './snapshot.ts';
11
13
 
12
14
  /**
13
15
  * AnimatePresence for Glimmer — the same algorithm as Motion's
@@ -21,6 +23,15 @@ import { postRender } from './scheduler.ts';
21
23
  *
22
24
  * Motion elements nested deeper inside a child inherit its presence through
23
25
  * their parent VisualElement, as React's PresenceContext would provide it.
26
+ *
27
+ * `@mode="snapshot"` is this package's own: a leaver's block is torn down in
28
+ * the render that removes it, and a static copy of the element the handle was
29
+ * passed to plays that element's exit, lifted out of flow at the seat the last
30
+ * frame showed the element in. Use it where a leaver's content comes from
31
+ * state that moves on as it leaves, such as a component's blocks: a live
32
+ * leaver would re-render from that state, and stay focusable, with its ids
33
+ * and form controls, beside its replacement. The copy is kept as a memory
34
+ * (see memory.ts). See snapshot.ts.
24
35
  */
25
36
  interface Signature<T> {
26
37
  Args: {
@@ -30,7 +41,7 @@ interface Signature<T> {
30
41
  initial?: boolean;
31
42
  items: T[];
32
43
  key: (item: T) => string;
33
- mode?: 'sync' | 'wait' | 'popLayout';
44
+ mode?: 'sync' | 'wait' | 'popLayout' | 'snapshot';
34
45
  onExitComplete?: () => void;
35
46
  parent?: PresenceHandle;
36
47
  /** nested presence: exit with the parent handle when it leaves */
@@ -49,7 +60,7 @@ class Entry<T> implements PresenceHandle {
49
60
  readonly id = `presence-${presenceId++}`;
50
61
  private children = new Map<string | number, boolean>();
51
62
  private subscribers = new Set<() => void>();
52
- /** popLayout: the direct children, so they can be measured before the DOM changes */
63
+ /** popLayout and snapshot: the direct children, so they can be read before the DOM changes */
53
64
  private popCandidates = new Set<PopMeasurable>();
54
65
  subscribe(refresh: () => void) {
55
66
  this.subscribers.add(refresh);
@@ -61,6 +72,24 @@ class Entry<T> implements PresenceHandle {
61
72
  this.popCandidates.delete(node);
62
73
  };
63
74
  }
75
+ /** snapshot: each direct child reads its seat now; returns the elements those seats depend on */
76
+ recordSeats(): Element[] {
77
+ const watched: Element[] = [];
78
+ this.popCandidates.forEach((node) => watched.push(...node.recordSeat()));
79
+ return watched;
80
+ }
81
+ /** snapshot: static copies of the direct children, taken before the render that removes them */
82
+ copiesForExit(): ExitCopy[] {
83
+ const copies: ExitCopy[] = [];
84
+ this.popCandidates.forEach((node) => {
85
+ const source = node.exitForCopy();
86
+ const copy = source && captureExit(source);
87
+ if (copy) {
88
+ copies.push(copy);
89
+ }
90
+ });
91
+ return copies;
92
+ }
64
93
  readonly key: string;
65
94
  private owner: Presence<T>;
66
95
  readonly initialBlocked: boolean;
@@ -123,12 +152,16 @@ class Entry<T> implements PresenceHandle {
123
152
  if (!present && this.mode === 'popLayout') {
124
153
  // React's PopChildMeasure measures in getSnapshotBeforeUpdate — BEFORE
125
154
  // the DOM is patched. That timing is the whole of it. This runs from the
126
- // diff in `rendered`, which is Glimmer's equivalent slot: the newcomers
127
- // for this pass have not been inserted yet, so the leaver's offsetTop is
128
- // still the seat it actually occupies.
155
+ // diff in `rendered`, the nearest slot Glimmer has: this <Presence>'s
156
+ // newcomers for the pass have not been inserted yet, so the container
157
+ // they would grow or re-center has not moved. Content earlier in the
158
+ // template than this <Presence> has already been patched for the pass,
159
+ // though, and a leaver it moves is measured where it was moved to.
160
+ // (Snapshot mode pins its copies to a seat recorded before the pass
161
+ // instead; see `seatWatch`.)
129
162
  //
130
163
  // Measure it afterwards and you read a layout that already contains the
131
- // arrivals: the container has grown, a centred one has re-centred, and
164
+ // arrivals: the container has grown, a centered one has re-centered, and
132
165
  // the leaver gets pinned at a place it never was. On a stage that
133
166
  // replays, every generation lands further from home than the last, and
134
167
  // what you see is a stack of ghosts drifting away from the newcomers.
@@ -175,11 +208,23 @@ export class Presence<T> extends Component<Signature<T>> {
175
208
  private pendingItems: T[] = [];
176
209
  private registeredWithParent?: () => void;
177
210
  private readonly selfId = `presence:${presenceId++}`;
211
+ /** snapshot: copies still playing an exit, by the key of the item they copied */
212
+ private copies = new Map<ExitCopy, string>();
213
+ /** snapshot: a read of the children's seats is booked for the next frame */
214
+ private seatReadBooked = false;
215
+ /** snapshot: re-reads the seats when a child, or the box it is pinned inside, resizes */
216
+ private resizes?: ResizeObserver;
217
+ private watched = new Set<Element>();
178
218
  @tracked private version = 0;
179
219
 
180
220
  constructor(owner: Owner, args: Signature<T>['Args']) {
181
221
  super(owner, args);
182
- registerDestructor(this, () => this.registeredWithParent?.());
222
+ registerDestructor(this, () => {
223
+ this.registeredWithParent?.();
224
+ this.copies.forEach((_, copy) => copy.cut());
225
+ this.copies.clear();
226
+ this.stopWatchingSeats();
227
+ });
183
228
  }
184
229
 
185
230
  get parentPresent() {
@@ -195,6 +240,74 @@ export class Presence<T> extends Component<Signature<T>> {
195
240
  return this.presentKeys.includes(key);
196
241
  }
197
242
 
243
+ /* eslint-disable ember/no-side-effects -- a render detector (see LayoutGroup): re-evaluated in every
244
+ render pass, it books a read of the seats the pass leaves the children in */
245
+ /**
246
+ * Snapshot: a leaver's copy is pinned to the seat its element last
247
+ * recorded. Each direct child reads its seat in the frame after every
248
+ * render, and again whenever it or the box it is pinned inside resizes (see
249
+ * MotionNode.recordSeat). The diff cannot read it: Glimmer patches the DOM
250
+ * in template order, so content earlier in the template than this
251
+ * <Presence> has been patched by the time it diffs, and may have moved or
252
+ * resized the leaver. A move that is neither a render nor one of those
253
+ * resizes, such as a transition on a sibling's width, is not seen.
254
+ */
255
+ get seatWatch(): undefined {
256
+ if (this.args.mode !== 'snapshot') {
257
+ this.stopWatchingSeats();
258
+ return undefined;
259
+ }
260
+ consumeTag(VOLATILE_TAG);
261
+ if (!this.seatReadBooked) {
262
+ this.seatReadBooked = true;
263
+ frame.read(() => {
264
+ this.seatReadBooked = false;
265
+ this.readSeats();
266
+ });
267
+ }
268
+ return undefined;
269
+ }
270
+ /* eslint-enable ember/no-side-effects */
271
+
272
+ /**
273
+ * Reads every child's seat. After a render it also moves the resize watch
274
+ * onto the elements those seats depend on now; a read from the watch itself
275
+ * leaves it alone, since a target observed inside a resize callback can be
276
+ * left undelivered, which the browser reports as an error.
277
+ */
278
+ private readSeats(rewatch = true) {
279
+ if (isDestroying(this) || this.args.mode !== 'snapshot') {
280
+ return;
281
+ }
282
+ const watched = new Set<Element>();
283
+ this.entries.forEach((e) =>
284
+ e.recordSeats().forEach((el) => watched.add(el)),
285
+ );
286
+ if (!rewatch) {
287
+ return;
288
+ }
289
+ const resizes = (this.resizes ??= new ResizeObserver(() =>
290
+ this.readSeats(false),
291
+ ));
292
+ this.watched.forEach((el) => {
293
+ if (!watched.has(el)) {
294
+ resizes.unobserve(el);
295
+ }
296
+ });
297
+ watched.forEach((el) => {
298
+ if (!this.watched.has(el)) {
299
+ resizes.observe(el);
300
+ }
301
+ });
302
+ this.watched = watched;
303
+ }
304
+
305
+ private stopWatchingSeats() {
306
+ this.resizes?.disconnect();
307
+ this.resizes = undefined;
308
+ this.watched.clear();
309
+ }
310
+
198
311
  /* eslint-disable ember/no-side-effects -- AnimatePresence's diff of present vs rendered children runs
199
312
  during render; this getter is that render step (entries, order and exit bookkeeping are its output) */
200
313
  get rendered(): Entry<T>[] {
@@ -226,28 +339,50 @@ export class Presence<T> extends Component<Signature<T>> {
226
339
  this.pendingItems = items;
227
340
  const presentKeys = this.presentKeys;
228
341
  const blockInitial = this.firstRender && this.args.initial === false;
342
+ const snapshot = this.args.mode === 'snapshot';
229
343
  items.forEach((item) => {
230
344
  const k = this.args.key(item);
231
345
  const e = this.entries.get(k);
232
346
  if (e) {
233
347
  e.item = item;
234
348
  } else {
349
+ // a static copy cannot be retargeted: an item that comes back
350
+ // replaces the copy of it still leaving
351
+ this.cutCopies(k);
235
352
  this.entries.set(k, new Entry(k, item, this, blockInitial));
236
353
  }
237
354
  });
238
355
  // diff rendered vs present, keeping leavers in their slots (AnimatePresence's splice loop)
239
356
  const exitingKeys: string[] = [];
357
+ const copied: ExitCopy[] = [];
358
+ let snapshotLeft = false;
240
359
  let next = parentPresent ? [...presentKeys] : [];
241
360
  let insertionIndex = 0;
242
361
  for (const k of this.order) {
243
362
  const presentIndex = presentKeys.indexOf(k);
244
- if (presentIndex === -1) {
363
+ if (presentIndex === -1 && snapshot) {
364
+ // copied now, before this render patches the block, so the copy
365
+ // shows the last frame's content (and is pinned to the seat
366
+ // recorded before this render; see `seatWatch`); the block itself
367
+ // leaves with this render
368
+ snapshotLeft = true;
369
+ for (const copy of this.entries.get(k)?.copiesForExit() ?? []) {
370
+ this.copies.set(copy, k);
371
+ copied.push(copy);
372
+ }
373
+ this.entries.delete(k);
374
+ this.exitComplete.delete(k);
375
+ this.exiting.delete(k);
376
+ } else if (presentIndex === -1) {
245
377
  next.splice(insertionIndex++, 0, k);
246
378
  exitingKeys.push(k);
247
379
  } else {
248
380
  insertionIndex = presentIndex + exitingKeys.length + 1;
249
381
  }
250
382
  }
383
+ if (snapshotLeft) {
384
+ postRender(() => this.startCopies(copied));
385
+ }
251
386
  if ((this.args.mode ?? 'sync') === 'wait' && exitingKeys.length) {
252
387
  next = exitingKeys;
253
388
  }
@@ -270,7 +405,16 @@ export class Presence<T> extends Component<Signature<T>> {
270
405
  }
271
406
  }
272
407
  this.firstRender = false;
273
- if (this.args.propagate && !parentPresent && !this.order.length) {
408
+ // a leaving parent waits on this <Presence>: with nothing left to
409
+ // render it is done, unless copies are still playing their exits (they
410
+ // report when they finish; see checkCopiesLeft)
411
+ if (
412
+ this.args.propagate &&
413
+ !parentPresent &&
414
+ !this.order.length &&
415
+ !snapshotLeft &&
416
+ !this.copies.size
417
+ ) {
274
418
  postRender(() => this.safeToRemove());
275
419
  }
276
420
  }
@@ -318,7 +462,51 @@ export class Presence<T> extends Component<Signature<T>> {
318
462
  this.args.parent?.context.onExitComplete?.(this.selfId);
319
463
  }
320
464
 
465
+ /** snapshot: after the render that removed their items, the copies go back in and play their exits */
466
+ private startCopies(copies: ExitCopy[]) {
467
+ const placement = {
468
+ anchorX: this.args.anchorX ?? 'left',
469
+ anchorY: this.args.anchorY ?? 'top',
470
+ custom: this.args.custom,
471
+ } as const;
472
+ for (const copy of copies) {
473
+ if (this.copies.has(copy)) {
474
+ copy.start(placement, () => this.copyLeft(copy));
475
+ }
476
+ }
477
+ // each copy reports its own end; a pass that copied nothing reports here
478
+ if (!copies.length) {
479
+ this.checkCopiesLeft();
480
+ }
481
+ }
482
+
483
+ private copyLeft(copy: ExitCopy) {
484
+ this.copies.delete(copy);
485
+ this.checkCopiesLeft();
486
+ }
487
+
488
+ /** every copy has finished its exit, and no live leaver is still playing one: report as onExit does */
489
+ private checkCopiesLeft() {
490
+ if (this.copies.size || this.exitComplete.size || isDestroying(this)) {
491
+ return;
492
+ }
493
+ if (this.args.propagate) {
494
+ this.safeToRemove();
495
+ }
496
+ this.args.onExitComplete?.();
497
+ }
498
+
499
+ private cutCopies(key: string) {
500
+ this.copies.forEach((k, copy) => {
501
+ if (k === key) {
502
+ this.copies.delete(copy);
503
+ copy.cut();
504
+ }
505
+ });
506
+ }
507
+
321
508
  <template>
509
+ {{this.seatWatch}}
322
510
  {{#each this.rendered key='key' as |e|}}
323
511
  {{yield e.item e}}
324
512
  {{/each}}
@@ -0,0 +1,280 @@
1
+ /**
2
+ * `<Presence @mode="snapshot">`: a leaver's exit played on a static copy of
3
+ * it that the engine owns, instead of on its live block.
4
+ *
5
+ * A Glimmer block re-runs from live state for as long as it is rendered, so a
6
+ * leaver kept on screen for its exit shows state that has already moved on.
7
+ * In snapshot mode the leaver's block is torn down in the render that removes
8
+ * it, as if no <Presence> were there. Before that render patches the
9
+ * leaver's block, the element is copied as it stands. After it, the copy goes
10
+ * back where the element stood (before the first node that followed it and is
11
+ * still there), lifted out of flow at its recorded seat, plays the element's
12
+ * exit, and is removed.
13
+ *
14
+ * The seat is read in every frame that follows a render, and whenever the
15
+ * element or the box it is pinned inside resizes, not at the diff: Glimmer
16
+ * patches the DOM in template order, so by the time <Presence> diffs, content
17
+ * earlier in the template has already been patched for the render, and may
18
+ * have moved or resized the element. Its content is still the last frame's;
19
+ * its box may not be. A move that is neither a render nor one of those
20
+ * resizes is not seen: a transition on a sibling's width can shift the
21
+ * element without resizing either box, and the copy is then pinned where the
22
+ * element stood before that move.
23
+ *
24
+ * The copy is kept as a memory (see memory.ts): it shows the element's last
25
+ * frame, and nothing in it acts. A clone does not carry everything the
26
+ * element showed, so the copy also takes the canvas pixels it showed, the
27
+ * option each of its selects had chosen, and the values its script
28
+ * animations and transitions had reached.
29
+ */
30
+ import type { PresenceContextProps } from 'motion-dom';
31
+
32
+ import {
33
+ elementsOf,
34
+ keepAsMemory,
35
+ readShown,
36
+ type Shown,
37
+ shownOnCopy,
38
+ } from './memory.ts';
39
+ import {
40
+ flushPendingMounts,
41
+ type MotionEl,
42
+ MotionNode,
43
+ type MotionProps,
44
+ type PopSeat,
45
+ seatInsets,
46
+ seatOf,
47
+ } from './node.ts';
48
+ import type { PresenceHandle } from './presence-types.ts';
49
+
50
+ /** where a copy is pinned, and what its exit variants resolve against */
51
+ export interface CopyPlacement {
52
+ anchorX: 'left' | 'right';
53
+ anchorY: 'top' | 'bottom';
54
+ custom: unknown;
55
+ }
56
+
57
+ /** what the diff read from a leaver, for its copy to use after the render */
58
+ interface Captured {
59
+ copy: MotionEl;
60
+ /**
61
+ * the nodes that followed the element: the copy goes back in before the
62
+ * first one the render left in place (the next ones are often the leaver's
63
+ * own whitespace and bounds, which leave with its block)
64
+ */
65
+ following: Node[];
66
+ parent: Node;
67
+ props: MotionProps;
68
+ seat: PopSeat;
69
+ /** what the element showed, keyed to the copy's elements */
70
+ shown: Shown;
71
+ }
72
+
73
+ let copyId = 0;
74
+
75
+ /** an element's CSS property for a keyframe key: `clipPath` → `clip-path`, `--x` stays */
76
+ function cssProperty(key: string) {
77
+ if (key.startsWith('--')) {
78
+ return key;
79
+ }
80
+ return key === 'cssFloat'
81
+ ? 'float'
82
+ : key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
83
+ }
84
+
85
+ const KEYFRAME_META = new Set([
86
+ 'composite',
87
+ 'computedOffset',
88
+ 'easing',
89
+ 'offset',
90
+ ]);
91
+
92
+ /** a script animation or a transition: the copy keeps the value it shows now */
93
+ function freezeComputed(animation: Animation, live: Element, copy: Element) {
94
+ const effect = animation.effect as KeyframeEffect;
95
+ const computed = getComputedStyle(live);
96
+ const style = (copy as HTMLElement | SVGElement).style;
97
+ for (const frame of effect.getKeyframes()) {
98
+ for (const key of Object.keys(frame)) {
99
+ if (!KEYFRAME_META.has(key)) {
100
+ const property = cssProperty(key);
101
+ style.setProperty(property, computed.getPropertyValue(property));
102
+ }
103
+ }
104
+ }
105
+ }
106
+
107
+ function copyCanvas(live: HTMLCanvasElement, copy: HTMLCanvasElement) {
108
+ try {
109
+ copy.getContext('2d')?.drawImage(live, 0, 0);
110
+ } catch {
111
+ // a canvas the copy cannot draw from stays blank
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Copies an element that is about to be removed, as it stands. Called from
117
+ * <Presence>'s diff, before the render patches the element's block. `seat` is
118
+ * the one recorded for it (see MotionNode.recordSeat); an element that is
119
+ * leaving before any seat was recorded is pinned where it stands now.
120
+ */
121
+ export function captureExit(source: {
122
+ element: MotionEl;
123
+ props: MotionProps;
124
+ seat: PopSeat | undefined;
125
+ }): ExitCopy | undefined {
126
+ const { element, props } = source;
127
+ const parent = element.parentNode;
128
+ const seat = source.seat ?? seatOf(element);
129
+ if (!parent || !seat) {
130
+ return undefined;
131
+ }
132
+ const copy = element.cloneNode(true) as MotionEl;
133
+ const live = elementsOf(element);
134
+ const copies = elementsOf(copy);
135
+ const indexOf = new Map(live.map((el, i) => [el, i]));
136
+ for (const animation of element.getAnimations({ subtree: true })) {
137
+ const effect = animation.effect;
138
+ const index =
139
+ !(animation instanceof CSSAnimation) &&
140
+ effect instanceof KeyframeEffect &&
141
+ !effect.pseudoElement &&
142
+ effect.target
143
+ ? indexOf.get(effect.target)
144
+ : undefined;
145
+ if (index !== undefined) {
146
+ freezeComputed(animation, live[index]!, copies[index]!);
147
+ }
148
+ }
149
+ live.forEach((el, i) => {
150
+ if (el instanceof HTMLCanvasElement) {
151
+ copyCanvas(el, copies[i] as HTMLCanvasElement);
152
+ } else if (el instanceof HTMLOptionElement) {
153
+ // a clone's options take their selectedness from the markup, not from
154
+ // the choice the user made
155
+ (copies[i] as HTMLOptionElement).selected = el.selected;
156
+ }
157
+ });
158
+ const following: Node[] = [];
159
+ for (let node = element.nextSibling; node; node = node.nextSibling) {
160
+ following.push(node);
161
+ }
162
+ return new ExitCopy({
163
+ copy,
164
+ following,
165
+ parent,
166
+ props,
167
+ seat,
168
+ shown: shownOnCopy(readShown(element), element, copy),
169
+ });
170
+ }
171
+
172
+ /** one static copy of a leaver, from the render that removed the leaver until its exit ends */
173
+ export class ExitCopy {
174
+ private node?: MotionNode;
175
+ private ended = false;
176
+ /** set once the copy has mounted and its exit has started: before that, a report is the mount's */
177
+ private armed = false;
178
+ private readonly reports = new Map<string | number, boolean>();
179
+ private onDone?: () => void;
180
+ private readonly captured: Captured;
181
+
182
+ constructor(captured: Captured) {
183
+ this.captured = captured;
184
+ }
185
+
186
+ /** after the render: put the copy where its element stood and play the exit */
187
+ start(placement: CopyPlacement, onDone: () => void) {
188
+ this.onDone = onDone;
189
+ const { copy, following, parent, props, shown } = this.captured;
190
+ if (this.ended || !parent.isConnected) {
191
+ this.end(true);
192
+ return;
193
+ }
194
+ this.pin(placement);
195
+ const before = following.find((node) => node.parentNode === parent);
196
+ keepAsMemory(copy, shown, () => parent.insertBefore(copy, before ?? null));
197
+ const node = (this.node = new MotionNode());
198
+ node.update(copy, { ...props, presence: this.handle(placement) });
199
+ flushPendingMounts();
200
+ this.armed = true;
201
+ this.check();
202
+ }
203
+
204
+ /** stop now and remove the copy, without reporting an exit (its item came back) */
205
+ cut() {
206
+ this.end(false);
207
+ }
208
+
209
+ /** lifted out of flow at the seat its element had: inline and important, so the copy's own CSS cannot move it */
210
+ private pin({ anchorX, anchorY }: CopyPlacement) {
211
+ const { copy, seat } = this.captured;
212
+ const { inline, block } = seatInsets(seat, anchorX, anchorY);
213
+ const style = copy.style;
214
+ const set = (property: string, value: string) =>
215
+ style.setProperty(property, value, 'important');
216
+ set('position', 'absolute');
217
+ set('margin', '0');
218
+ set('width', `${seat.w}px`);
219
+ set('height', `${seat.h}px`);
220
+ for (const side of ['top', 'right', 'bottom', 'left']) {
221
+ set(side, 'auto');
222
+ }
223
+ set(inline[0], `${inline[1]}px`);
224
+ set(block[0], `${block[1]}px`);
225
+ }
226
+
227
+ /** the copy is not present, and its exit is done once every motion element registered in it reports */
228
+ private handle(placement: CopyPlacement): PresenceHandle {
229
+ const id = `presence-copy-${copyId++}`;
230
+ const context: PresenceContextProps = {
231
+ id,
232
+ isPresent: false,
233
+ custom: placement.custom,
234
+ register: (childId) => {
235
+ this.reports.set(childId, false);
236
+ return () => {
237
+ this.reports.delete(childId);
238
+ this.check();
239
+ };
240
+ },
241
+ onExitComplete: (childId) => {
242
+ this.reports.set(childId, true);
243
+ this.check();
244
+ },
245
+ };
246
+ return {
247
+ anchorX: placement.anchorX,
248
+ anchorY: placement.anchorY,
249
+ context,
250
+ isPresent: false,
251
+ key: id,
252
+ mode: 'sync',
253
+ popCandidate: () => () => undefined,
254
+ subscribe: () => () => undefined,
255
+ };
256
+ }
257
+
258
+ private check() {
259
+ if (
260
+ this.armed &&
261
+ !this.ended &&
262
+ [...this.reports.values()].every(Boolean)
263
+ ) {
264
+ this.end(true);
265
+ }
266
+ }
267
+
268
+ private end(report: boolean) {
269
+ if (this.ended) {
270
+ return;
271
+ }
272
+ this.ended = true;
273
+ this.node?.destroy();
274
+ this.node = undefined;
275
+ this.captured.copy.remove();
276
+ if (report) {
277
+ this.onDone?.();
278
+ }
279
+ }
280
+ }
@@ -43,6 +43,7 @@ import { rootProjectionNode } from 'motion-dom';
43
43
  import { isMotionIdle, whatIsBusy } from '../activity.ts';
44
44
  import { layoutLoopDetected, resetLayoutLoopGuard } from '../layout.ts';
45
45
  import { resetUngroupedLayoutWarning } from '../layout-group.gts';
46
+ import { MEMORY_ATTRIBUTE } from '../memory.ts';
46
47
  import { setMotionSpeed } from '../speed.ts';
47
48
 
48
49
  export { isMotionIdle, whatIsBusy } from '../activity.ts';
@@ -50,11 +51,11 @@ export { isMotionIdle, whatIsBusy } from '../activity.ts';
50
51
  const nextFrame = () =>
51
52
  new Promise<void>((resolve) => frame.postRender(() => resolve()));
52
53
 
54
+ const testRoot = (): Element =>
55
+ document.querySelector('#ember-testing') ?? document.body;
56
+
53
57
  /** the box QUnit renders the test into; every measurement here is relative to it */
54
- const container = (): DOMRect =>
55
- (
56
- document.querySelector('#ember-testing') ?? document.body
57
- ).getBoundingClientRect();
58
+ const container = (): DOMRect => testRoot().getBoundingClientRect();
58
59
 
59
60
  export interface Box {
60
61
  height: number;
@@ -113,6 +114,37 @@ export function boundsAndShape(
113
114
  return { ...shape(element), ...bounds(element) };
114
115
  }
115
116
 
117
+ /**
118
+ * The first LIVE match for `selector`: never a memory, a leaver kept on screen
119
+ * while its exit plays (a `<Presence @mode="snapshot">` copy, a leaver in a
120
+ * `<Choreo>` orphan layer).
121
+ *
122
+ * A memory's test hooks carry a suffix (`data-test-title-memory`), so a
123
+ * selector made of test hooks already matches only live elements. Any other
124
+ * selector (a class, a role, a tag) can match a memory as well as whatever
125
+ * replaced it, and a bare `querySelector` can answer with the memory: an
126
+ * element whose component has already been torn down, which is inert, and
127
+ * whose box is where the old scene stood. Clicking it does nothing; measuring
128
+ * it measures the past. Neither failure names itself. Such a query, made while
129
+ * an exit may still be playing, should come through here.
130
+ */
131
+ export function live<E extends Element = HTMLElement>(
132
+ selector: string,
133
+ root: ParentNode = testRoot(),
134
+ ): E | null {
135
+ return liveAll<E>(selector, root)[0] ?? null;
136
+ }
137
+
138
+ /** every live match for `selector`, in document order: see `live()` */
139
+ export function liveAll<E extends Element = HTMLElement>(
140
+ selector: string,
141
+ root: ParentNode = testRoot(),
142
+ ): E[] {
143
+ return [...root.querySelectorAll<E>(selector)].filter(
144
+ (el) => !el.closest(`[${MEMORY_ATTRIBUTE}]`),
145
+ );
146
+ }
147
+
116
148
  export interface SettleOptions {
117
149
  /** give up after this long and report what was still moving (default 5000) */
118
150
  timeout?: number;
@@ -20,6 +20,8 @@ export interface LLMOption {
20
20
 
21
21
  interface Signature {
22
22
  Args: {
23
+ /** see PillMenu's `anchorY` */
24
+ anchorY?: 'top' | 'bottom';
23
25
  selected: string;
24
26
  options: LLMOption[];
25
27
  onChange: (selectedLLM: string) => void;
@@ -37,6 +39,7 @@ export default class LLMSelect extends Component<Signature> {
37
39
  <template>
38
40
  <PillMenu
39
41
  class='llm-select'
42
+ @anchorY={{@anchorY}}
40
43
  @onExpand={{@onExpand}}
41
44
  @onCollapse={{@onCollapse}}
42
45
  ...attributes
@@ -32,6 +32,8 @@ const markdownDefRef = {
32
32
  interface Signature {
33
33
  Element: HTMLDivElement | HTMLButtonElement;
34
34
  Args: {
35
+ /** see PillMenu's `anchorY` */
36
+ anchorY?: 'top' | 'bottom';
35
37
  skills: RoomSkill[];
36
38
  onExpand?: () => void;
37
39
  onCollapse?: () => void;
@@ -46,6 +48,7 @@ export default class AiAssistantSkillMenu extends Component<Signature> {
46
48
  <template>
47
49
  <PillMenu
48
50
  class='skill-menu'
51
+ @anchorY={{@anchorY}}
49
52
  @onExpand={{fn this.setExpanded true}}
50
53
  @onCollapse={{fn this.setExpanded false}}
51
54
  title={{if this.isExpanded undefined this.headerText}}