@cardstack/boxel-cli 0.12.0 → 0.13.0-unstable.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundled-realms/base/command.gts +4 -4
- package/bundled-realms/base/realm-api-keys.gts +35 -3
- package/bundled-realms/pretui/components/chip.gts +47 -2
- package/bundled-realms/pretui/components/chip.md +5 -2
- package/bundled-realms/pretui/components/chip.usage.gts +37 -2
- package/bundled-realms/pretui/components/status-chip.gts +12 -2
- package/bundled-realms/pretui/components/status-chip.md +2 -1
- package/bundled-realms/pretui/components/status-chip.usage.gts +2 -0
- package/bundled-test-harness/assets/{ai-module-creation-test-B9lSKNq1.js → ai-module-creation-test-DxxsXNXe.js} +1 -1
- package/bundled-test-harness/assets/{aibot-message-LXV0UlzI.js → aibot-message-Dg5nzE1b.js} +1 -1
- package/bundled-test-harness/assets/{app-BIQcKrVT.js → app-BEvPLshl.js} +8 -8
- package/bundled-test-harness/assets/{assistant-transcript-test-CG_2-bog.js → assistant-transcript-test-kILRv_pf.js} +1 -1
- package/bundled-test-harness/assets/{binary-upload-dedupe-test-Q7e6bzl1.js → binary-upload-dedupe-test-B_gcWqxF.js} +1 -1
- package/bundled-test-harness/assets/{bundled-base-DldokUSJ.js → bundled-base-CYD5iFX3.js} +3 -3
- package/bundled-test-harness/assets/{bundled-base-modules-test-VAlRx4t3.js → bundled-base-modules-test-bO7yZysu.js} +1 -1
- package/bundled-test-harness/assets/{can-invoke-test-BGmUJHxo.js → can-invoke-test-BBTE4_cE.js} +1 -1
- package/bundled-test-harness/assets/{card-chooser-paging-test-CzooKL4I.js → card-chooser-paging-test-14IqQei3.js} +1 -1
- package/bundled-test-harness/assets/{card-chooser-test-C3fjaDYi.js → card-chooser-test-CRg2JrXQ.js} +1 -1
- package/bundled-test-harness/assets/{card-def-field-def-relationships-test-yqv8ir8Y.js → card-def-field-def-relationships-test-BapYGyoB.js} +1 -1
- package/bundled-test-harness/assets/{card-delete-test-BtjqrqKO.js → card-delete-test-CvbDQUUE.js} +1 -1
- package/bundled-test-harness/assets/{chip-D6Xffhjx.css → chip-B2Ns53A6.css} +35 -9
- package/bundled-test-harness/assets/{chip-BydgljDL.js → chip-DVJE_YDh.js} +11 -3
- package/bundled-test-harness/assets/{choreo-D7dLCdGB.js → choreo-CIrop3HM.js} +16 -35
- package/bundled-test-harness/assets/choreo-Ca2Y2sF3.js +2 -0
- package/bundled-test-harness/assets/{choreo-feature-reel-test--x-L3ZKc.js → choreo-feature-reel-test-CJHms7Sf.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-ZO9vXOxd.js → choreo-gallery-DZm1KZ20.js} +3 -3
- package/bundled-test-harness/assets/{choreo-gallery-build-order-test-DG5UTyc9.js → choreo-gallery-build-order-test-QKyMWELN.js} +4 -4
- package/bundled-test-harness/assets/{choreo-gallery-compositor-test-DJP80SNj.js → choreo-gallery-compositor-test-BQDptxfE.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-cover-test-CHYz7sM4.js → choreo-gallery-cover-test-Do-HYF96.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-crossing-stage-test-BuinlINr.js → choreo-gallery-crossing-stage-test-yJVXghRc.js} +10 -10
- package/bundled-test-harness/assets/{choreo-gallery-demos-alive-test-BjNCGfH9.js → choreo-gallery-demos-alive-test-BCeaaZye.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-drift-test-BJ17f3yv.js → choreo-gallery-drift-test-CJ1yq6R0.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-escort-test-Dadih8-S.js → choreo-gallery-escort-test-DypqSgvs.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-films-test-CCXty_WI.js → choreo-gallery-films-test-ClMy5M_p.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-filter-test-BozCoT_M.js → choreo-gallery-filter-test-CzOmgqgl.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-fold-test-Kez2P3_Y.js → choreo-gallery-fold-test-qNnH6wtC.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-guide-pages-test-BV5oxrYA.js → choreo-gallery-guide-pages-test-BtKWMkTv.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-guides-test-DwYBDgKi.js → choreo-gallery-guides-test-CXMsuDYi.js} +4 -3
- package/bundled-test-harness/assets/{choreo-gallery-hang-test-B-5u7cGJ.js → choreo-gallery-hang-test-BD4JQ04e.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-highlight-test-BZ5Lg8V6.js → choreo-gallery-highlight-test-BK280K2W.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-inline-edit-test-wSlSI-VM.js → choreo-gallery-inline-edit-test-DAgUcaaw.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-interruption-test-DBpCEiro.js → choreo-gallery-interruption-test-DY5KUilC.js} +4 -4
- package/bundled-test-harness/assets/{choreo-gallery-lone-stack-test-aMLrHsns.js → choreo-gallery-lone-stack-test-Dxd_Nb0-.js} +4 -4
- package/bundled-test-harness/assets/{choreo-gallery-navigation-test-B8b-2ZTz.js → choreo-gallery-navigation-test-DWJq12PN.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-playhead-transport-test-B95CYBh7.js → choreo-gallery-playhead-transport-test-DlQ6yTKW.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-poplayout-subtree-test-jZAAICV6.js → choreo-gallery-poplayout-subtree-test-D59W8gYt.js} +5 -5
- package/bundled-test-harness/assets/{choreo-gallery-presence-modes-test-BawIKB-a.js → choreo-gallery-presence-modes-test-BAU-dggP.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-presentation-test-CYIb-8ML.js → choreo-gallery-presentation-test-BqGsinE0.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-reduced-motion-test-C6Vi1WYr.js → choreo-gallery-reduced-motion-test-By0rb2Ai.js} +4 -3
- package/bundled-test-harness/assets/{choreo-gallery-sheet-standalone-test-_e4T5KDI.js → choreo-gallery-sheet-standalone-test-Cfw-4ULF.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-sheet-test-Cv51Te3G.js → choreo-gallery-sheet-test-dm53Pdzb.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-site-test-Cm35kHu_.js → choreo-gallery-site-test-SAlqtZgL.js} +3 -2
- package/bundled-test-harness/assets/{choreo-gallery-stage-Bp6QVqlI.js → choreo-gallery-stage-CU8LvQqX.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-stage-keyboard-test-DDX2cuMa.js → choreo-gallery-stage-keyboard-test-DQ4v5qA4.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-stages-test-Dfa_33_4.js → choreo-gallery-stages-test-CRvaS6w6.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-subdivision-test-CgR3WdcT.js → choreo-gallery-subdivision-test-CtOm-U35.js} +3 -3
- package/bundled-test-harness/assets/{choreo-gallery-table-plan-test-DF-cE3J9.js → choreo-gallery-table-plan-test-_0koIm2u.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-theater-test-BuIyb2ke.js → choreo-gallery-theater-test-yedwQVyP.js} +4 -4
- package/bundled-test-harness/assets/{choreo-gallery-tuning-test-BdZ45YBK.js → choreo-gallery-tuning-test-CmMtfJ5a.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-widget-room-test-HcPX75Dg.js → choreo-gallery-widget-room-test-jnYOas5x.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-wires-test-BhFCIjB1.js → choreo-gallery-wires-test-CSyqjVnm.js} +2 -2
- package/bundled-test-harness/assets/clip--wioup_3.js +2 -0
- package/bundled-test-harness/assets/{clip-BmBVqUyU.js → clip-Cm32sLyn.js} +3 -3
- package/bundled-test-harness/assets/{codeblocks-test-DbZYlmG_.js → codeblocks-test-j4ES37-L.js} +1 -1
- package/bundled-test-harness/assets/{compaction-test-D1G3KCSe.js → compaction-test-CyhMODxy.js} +1 -1
- package/bundled-test-harness/assets/{container-BCOmNUou.css → container-CAEYXGQg.css} +179 -155
- package/bundled-test-harness/assets/{container-XSX5809v.js → container-CATqNgYF.js} +20 -19
- package/bundled-test-harness/assets/{create-listing-modal-test--oGT9P_F.js → create-listing-modal-test-QmWcm9hl.js} +1 -1
- package/bundled-test-harness/assets/{debug-message-test-C8UZUtCt.js → debug-message-test-D-4Dcu8y.js} +1 -1
- package/bundled-test-harness/assets/{externals--YMzwHwd.js → externals-Cw0MmkHM.js} +38 -38
- package/bundled-test-harness/assets/{fallback-banner-live-delete-test-DPK_coJY.js → fallback-banner-live-delete-test-Bzqw4uMs.js} +1 -1
- package/bundled-test-harness/assets/{fallback-banner-test-C2wi6Lu6.js → fallback-banner-test-BMQVK4gy.js} +1 -1
- package/bundled-test-harness/assets/{fallback-models-test-SqdXWB1Z.js → fallback-models-test-DkdjPwvA.js} +1 -1
- package/bundled-test-harness/assets/{file-attachment-test-DT0HK6yS.js → file-attachment-test-yvR5gw9_.js} +1 -1
- package/bundled-test-harness/assets/{film-Zt2YoVoG.js → film-9V69qBWw.js} +6 -6
- package/bundled-test-harness/assets/{film-77OfTGL7.js → film-Z87hSVo_.js} +8 -8
- package/bundled-test-harness/assets/film-h1N8vUjG.js +2 -0
- package/bundled-test-harness/assets/{formatted-aibot-message-test-B6i0k9wW.js → formatted-aibot-message-test-DH3WPvU1.js} +1 -1
- package/bundled-test-harness/assets/{general-test-C0Xb2Idr.js → general-test-D2j4JhrD.js} +1 -1
- 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
- package/bundled-test-harness/assets/{glimmer-motion-and-choreo-test-B2ustNm9.js → glimmer-motion-and-choreo-test-BM7uX8k4.js} +10 -10
- package/bundled-test-harness/assets/group-Cgg5RLul.js +2 -0
- package/bundled-test-harness/assets/{group-BNxp6qON.js → group-DL-b_Hzj.js} +2 -2
- package/bundled-test-harness/assets/{index-query-engine-test-BKOqgBMv.js → index-query-engine-test-DFkm037X.js} +1 -1
- package/bundled-test-harness/assets/{instance-filter-matcher-test-CNIzKZvP.js → instance-filter-matcher-test-UqB2Rto8.js} +1 -1
- package/bundled-test-harness/assets/item-Bu9hhPf2.js +2 -0
- package/bundled-test-harness/assets/{item-BDyVqxdA.js → item-CBEoTOyj.js} +2 -2
- package/bundled-test-harness/assets/{main-BiI-FhMc.js → main-CM62YtMN.js} +1 -1
- package/bundled-test-harness/assets/{markdown-preview-BItI_AEl.js → markdown-preview-D_QssEOi.js} +1 -1
- package/bundled-test-harness/assets/{markdown-preview-test-Bntyikh1.js → markdown-preview-test-nljzB8o_.js} +1 -1
- package/bundled-test-harness/assets/memory-B73-H_Rp.js +223 -0
- package/bundled-test-harness/assets/{model-cost-tier-test-Hu2-LpEw.js → model-cost-tier-test-CDXxio0B.js} +1 -1
- package/bundled-test-harness/assets/{motion-DeM7UCtd.js → motion-BiMvMS08.js} +1 -1
- package/bundled-test-harness/assets/{node-BAM535H5.js → node-BWOLCOMI.js} +95 -6
- package/bundled-test-harness/assets/{operation-lowering-test-CG1gatuH.js → operation-lowering-test-C_CaTJ0F.js} +1 -1
- package/bundled-test-harness/assets/{operator-mode-card-chooser-test-Bo5ZnF_x.js → operator-mode-card-chooser-test-CU5f7q6r.js} +1 -1
- package/bundled-test-harness/assets/{operator-mode-links-test-DVx_LQOU.js → operator-mode-links-test-CoeF3prV.js} +1 -1
- package/bundled-test-harness/assets/{operator-mode-test-CCbFrYgN.js → operator-mode-test-B9c6N5FF.js} +1 -1
- package/bundled-test-harness/assets/{operator-mode-ui-test-Dm2Bf6Vn.js → operator-mode-ui-test-CJQxypf_.js} +1 -1
- package/bundled-test-harness/assets/{overlay-menu-items-test-Bk5AidFD.js → overlay-menu-items-test-CFWZ-tzq.js} +1 -1
- package/bundled-test-harness/assets/{overlays-CY7IgeOl.js → overlays-DZXjQykn.js} +3 -3
- package/bundled-test-harness/assets/{overlays-DhVKmSP-.js → overlays-hrAO43jG.js} +1 -1
- package/bundled-test-harness/assets/{panel-t9QyU1lY.js → panel-CXmB6GMs.js} +15 -75
- package/bundled-test-harness/assets/{panel-CUYJ8oHS.css → panel-D2zkZoIr.css} +39 -31
- package/bundled-test-harness/assets/{past-sessions-test-Dq-Y9T6W.js → past-sessions-test-7FgLJlog.js} +1 -1
- package/bundled-test-harness/assets/{permission-checking-realm-test-CuImx8ie.js → permission-checking-realm-test-Cm9eB-hE.js} +1 -1
- package/bundled-test-harness/assets/pill-menu-Ctn3JNqG.js +154 -0
- package/bundled-test-harness/assets/{pill-menu-test-kpWBGrX5.js → pill-menu-test-Cf2xO2w6.js} +1 -1
- package/bundled-test-harness/assets/plate-CnatwP3n.js +2 -0
- package/bundled-test-harness/assets/{plate-B4Lz2Ulo.js → plate-Cu5EnXQZ.js} +3 -3
- package/bundled-test-harness/assets/player-Bwn5tEsh.js +2 -0
- package/bundled-test-harness/assets/{player-B965XH82.js → player-CVdsx7Ng.js} +3 -3
- package/bundled-test-harness/assets/presence-BKkR2au7.js +553 -0
- package/bundled-test-harness/assets/presence-CcEXVges.js +2 -0
- package/bundled-test-harness/assets/{quick-look-test-VW5_W7dm.js → quick-look-test-DrqgCLlM.js} +1 -1
- package/bundled-test-harness/assets/{realm-api-keys-Tlj0OK1c.css → realm-api-keys-16eN_yJb.css} +35 -25
- package/bundled-test-harness/assets/{realm-api-keys-sg--fohv.js → realm-api-keys-ZJedocgl.js} +11 -5
- package/bundled-test-harness/assets/realm-api-keys-m-EOSWuV.js +2 -0
- package/bundled-test-harness/assets/{realm-api-keys-test-mrgvnhFJ.js → realm-api-keys-test-Ba_1CKJu.js} +3 -5
- package/bundled-test-harness/assets/{realm-api-keys-test-Cy7qSoAd.js → realm-api-keys-test-jXscHLbS.js} +1 -1
- package/bundled-test-harness/assets/{realm-config-DvpP1xf-.js → realm-config-DIOozzdQ.js} +1 -1
- package/bundled-test-harness/assets/{realm-config-policy-standing-test-DJk-ftgH.js → realm-config-policy-standing-test-DEFQEEvl.js} +1 -1
- package/bundled-test-harness/assets/{realm-config-routing-rule-edit-test-CTPiJrAL.js → realm-config-routing-rule-edit-test-CtLYsxzq.js} +1 -1
- package/bundled-test-harness/assets/{realm-config-settings-test-CUa-PjMC.js → realm-config-settings-test-Dsatuv-T.js} +1 -1
- package/bundled-test-harness/assets/{reasoning-test-CeeGGwvf.js → reasoning-test-3p_PygAV.js} +1 -1
- package/bundled-test-harness/assets/{rendered-markdown-B8TH0yN4.js → rendered-markdown-DPEXS7MQ.js} +1 -1
- package/bundled-test-harness/assets/{rendered-markdown-test-CBRsIBqW.js → rendered-markdown-test-DxbJ1nFX.js} +1 -1
- package/bundled-test-harness/assets/{room-message-test-BTxZ_W74.js → room-message-test-BBE8UIPX.js} +1 -1
- package/bundled-test-harness/assets/{run-BSEzn_PH.js → run-CfyzhBXb.js} +2 -2
- package/bundled-test-harness/assets/{schema-generation-test-De1eamwr.js → schema-generation-test-hkFzUr7-.js} +1 -1
- package/bundled-test-harness/assets/{scrolling-test-C7R0UASi.js → scrolling-test-B8CudFE9.js} +2 -2
- package/bundled-test-harness/assets/{searchable-option-test-BiOqxIIs.js → searchable-option-test-B8lZyJij.js} +1 -1
- package/bundled-test-harness/assets/{sending-test-DOXQ0Tvx.js → sending-test-BtH1z2uE.js} +1 -1
- package/bundled-test-harness/assets/{skills-test-BjCYI4Z_.js → skills-test-DIQvoCLm.js} +1 -1
- package/bundled-test-harness/assets/src-CEw1V0fi.js +11 -0
- package/bundled-test-harness/assets/{src-Bhefkhqj.js → src-CbBcj3xm.js} +11 -5
- package/bundled-test-harness/assets/{src-BDDYD1rs.js → src-D8bpj2tb.js} +2 -2
- package/bundled-test-harness/assets/{src-DySBAkac.js → src-Df4Vutrk.js} +3 -3
- package/bundled-test-harness/assets/{stick-to-bottom-resize-test-hN0AjdXP.js → stick-to-bottom-resize-test-Du4B3w2x.js} +4 -4
- package/bundled-test-harness/assets/{store-test-BD99H9dQ.js → store-test-DoHgyfQA.js} +1 -1
- package/bundled-test-harness/assets/{test-support-BcjhIHQa.js → test-support-BOFeeYuE.js} +25 -2
- package/bundled-test-harness/assets/test-support-CXESl4Wf.js +3 -0
- package/bundled-test-harness/assets/test-support-CgWmpebE.js +3 -0
- package/bundled-test-harness/assets/{test-support-Dl6FChlV.js → test-support-xTaMuaOA.js} +4 -27
- package/bundled-test-harness/assets/{tests-BqLDuQog.js → tests-DXX9TrgY.js} +759 -750
- package/bundled-test-harness/assets/{text-input-validator-test-CW6Ay7bo.js → text-input-validator-test-D56aDP2e.js} +1 -1
- package/bundled-test-harness/assets/{titles-CFt3dEls.js → titles-BUbpcMoe.js} +3 -3
- package/bundled-test-harness/assets/titles-D80DwOQU.js +2 -0
- package/bundled-test-harness/assets/{to-device-streaming-test-DRxNCi7Z.js → to-device-streaming-test-Ug8UnlUl.js} +1 -1
- package/bundled-test-harness/assets/{token-usage-test-Ck9rB3Ie.js → token-usage-test-BolKbZVL.js} +1 -1
- package/bundled-test-harness/assets/{tools-DRCjAZCp.js → tools-CkouX4wE.js} +1 -1
- package/bundled-test-harness/assets/{tools-test-B3zniq10.js → tools-test-CdYgRu5Z.js} +1 -1
- package/bundled-test-harness/assets/{turn-settling-test-C0X6hL7x.js → turn-settling-test-CS0PlpTg.js} +1 -1
- package/bundled-test-harness/assets/{turn-summary-0XOoAv3j.js → turn-summary-CAZVdcBd.js} +1 -1
- package/bundled-test-harness/assets/{unshare-key-Dfy1IK9g.js → unshare-key-W_bNI_MF.js} +0 -1
- package/bundled-test-harness/index.html +29 -28
- package/bundled-test-harness/tests/index.html +29 -28
- package/bundled-types/base/command.d.ts +0 -1
- package/bundled-types/choreo/choreo.gts +21 -42
- package/bundled-types/choreo/test-support/index.ts +9 -30
- package/bundled-types/glimmer-motion/index.ts +11 -3
- package/bundled-types/glimmer-motion/memory.ts +302 -0
- package/bundled-types/glimmer-motion/node.ts +125 -14
- package/bundled-types/glimmer-motion/presence-types.ts +13 -5
- package/bundled-types/glimmer-motion/presence.gts +199 -11
- package/bundled-types/glimmer-motion/snapshot.ts +280 -0
- package/bundled-types/glimmer-motion/test-support/index.ts +36 -4
- package/bundled-types/host-app/components/ai-assistant/llm-select.gts +3 -0
- package/bundled-types/host-app/components/ai-assistant/skill-menu/index.gts +3 -0
- package/bundled-types/host-app/components/matrix/room.gts +15 -3
- package/bundled-types/host-app/components/operator-mode/dashboard/add-workspace.gts +5 -3
- package/bundled-types/host-app/components/operator-mode/dashboard/archived-workspace.gts +3 -5
- package/bundled-types/host-app/components/operator-mode/dashboard/catalog-shelf.gts +24 -7
- package/bundled-types/host-app/components/operator-mode/dashboard/workspace.gts +9 -4
- package/bundled-types/host-app/components/pill-menu/index.gts +74 -23
- package/bundled-types/host-app/lib/key-shares.ts +0 -1
- package/bundled-types/host-app/tools/get-key-share-usage.ts +0 -1
- package/dist/index.js +161 -161
- package/package.json +1 -1
- package/src/commands/keys/manage.ts +22 -6
- package/src/lib/user-keys.ts +18 -5
- package/bundled-test-harness/assets/choreo-Cd9yKlEK.js +0 -2
- package/bundled-test-harness/assets/clip-DnEy6oK5.js +0 -2
- package/bundled-test-harness/assets/film-C4NJTl9U.js +0 -2
- package/bundled-test-harness/assets/group-CPQojb-T.js +0 -2
- package/bundled-test-harness/assets/item-DGzMAZ9K.js +0 -2
- package/bundled-test-harness/assets/pill-menu-DaRpDMUm.js +0 -68
- package/bundled-test-harness/assets/plate-B7JjCVMj.js +0 -2
- package/bundled-test-harness/assets/player-DpGXST02.js +0 -2
- package/bundled-test-harness/assets/presence-CmLmFwhO.js +0 -2
- package/bundled-test-harness/assets/presence-Sv4y4tQy.js +0 -230
- package/bundled-test-harness/assets/realm-api-keys-DkdlXgAg.js +0 -2
- package/bundled-test-harness/assets/src-BpmcuCfH.js +0 -10
- package/bundled-test-harness/assets/test-support-CB3auo8h.js +0 -2
- package/bundled-test-harness/assets/test-support-DfGKwg35.js +0 -3
- package/bundled-test-harness/assets/titles-DVhrJZzS.js +0 -2
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import type { PresenceContextProps } from 'motion-dom';
|
|
2
2
|
|
|
3
|
+
import type { MotionEl, MotionProps, PopSeat } from './node.ts';
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
|
-
* A direct child of a
|
|
5
|
-
*
|
|
6
|
-
* getSnapshotBeforeUpdate; see MotionNode.measureForPop
|
|
6
|
+
* A direct child of a <Presence>, which has to be read before the render
|
|
7
|
+
* that removes it: popLayout measures its seat (React's PopChildMeasure does
|
|
8
|
+
* this in getSnapshotBeforeUpdate; see MotionNode.measureForPop), and
|
|
9
|
+
* snapshot copies it (see MotionNode.exitForCopy), pinning the copy to the
|
|
10
|
+
* seat recorded before that render (see MotionNode.recordSeat).
|
|
7
11
|
*/
|
|
8
12
|
export interface PopMeasurable {
|
|
13
|
+
exitForCopy():
|
|
14
|
+
| { element: MotionEl; props: MotionProps; seat: PopSeat | undefined }
|
|
15
|
+
| undefined;
|
|
9
16
|
measureForPop(): void;
|
|
17
|
+
recordSeat(): Element[];
|
|
10
18
|
}
|
|
11
19
|
|
|
12
20
|
/** what one item inside <Presence> hands to its {{motion}} element */
|
|
@@ -16,8 +24,8 @@ export interface PresenceHandle {
|
|
|
16
24
|
readonly context: PresenceContextProps;
|
|
17
25
|
readonly isPresent: boolean;
|
|
18
26
|
readonly key: string;
|
|
19
|
-
readonly mode: 'sync' | 'wait' | 'popLayout';
|
|
20
|
-
/** popLayout: the direct child asks to be
|
|
27
|
+
readonly mode: 'sync' | 'wait' | 'popLayout' | 'snapshot';
|
|
28
|
+
/** popLayout and snapshot: the direct child asks to be read while it is still in flow */
|
|
21
29
|
popCandidate(node: PopMeasurable): () => void;
|
|
22
30
|
/** motion elements that inherit this presence (React: PresenceContext consumers) ask to be refreshed when it flips */
|
|
23
31
|
subscribe(refresh: () => void): () => void;
|
|
@@ -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
|
|
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
|
|
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`,
|
|
127
|
-
// for
|
|
128
|
-
//
|
|
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
|
|
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, () =>
|
|
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
|
-
|
|
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;
|