@dxos/plugin-deck 0.9.1-main.c7dcc2e112 → 0.9.1-staging.ee54ba693a

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 (258) hide show
  1. package/PLUGIN.mdl +3 -1
  2. package/dist/lib/neutral/{DeckLayout-VX2BP4Q2.mjs → DeckLayout-B3RUUV5P.mjs} +4 -3
  3. package/dist/lib/neutral/DeckLayout-B3RUUV5P.mjs.map +7 -0
  4. package/dist/lib/neutral/DeckPlugin.mjs +2 -6
  5. package/dist/lib/neutral/DeckPlugin.mjs.map +3 -3
  6. package/dist/lib/neutral/DeckSettings-Z7GMB7XJ.mjs +32 -0
  7. package/dist/lib/neutral/DeckSettings-Z7GMB7XJ.mjs.map +7 -0
  8. package/dist/lib/neutral/{add-toast-APKTCLIA.mjs → add-toast-BHNEH5UY.mjs} +2 -2
  9. package/dist/lib/neutral/{adjust-7CZI4GK3.mjs → adjust-MJB6UMRQ.mjs} +16 -10
  10. package/dist/lib/neutral/adjust-MJB6UMRQ.mjs.map +7 -0
  11. package/dist/lib/neutral/{app-graph-builder-6HTXRTU2.mjs → app-graph-builder-2F7P4IFQ.mjs} +4 -3
  12. package/dist/lib/neutral/app-graph-builder-2F7P4IFQ.mjs.map +7 -0
  13. package/dist/lib/neutral/capabilities/index.mjs +5 -5
  14. package/dist/lib/neutral/chunk-FKGYHBUB.mjs +8 -0
  15. package/dist/lib/neutral/{chunk-CL7JIOI5.mjs → chunk-ICOJ4GMB.mjs} +10 -6
  16. package/dist/lib/neutral/chunk-ICOJ4GMB.mjs.map +7 -0
  17. package/dist/lib/neutral/{chunk-NGX6RXNV.mjs → chunk-PP32NEVY.mjs} +2 -2
  18. package/dist/lib/neutral/chunk-PP32NEVY.mjs.map +7 -0
  19. package/dist/lib/neutral/chunk-SONATB35.mjs +20 -0
  20. package/dist/lib/neutral/chunk-SONATB35.mjs.map +7 -0
  21. package/dist/lib/neutral/chunk-Z2B5RSVI.mjs +1096 -0
  22. package/dist/lib/neutral/chunk-Z2B5RSVI.mjs.map +7 -0
  23. package/dist/lib/neutral/{close-KJUCFIJ5.mjs → close-2YGB5IIP.mjs} +4 -3
  24. package/dist/lib/neutral/{close-KJUCFIJ5.mjs.map → close-2YGB5IIP.mjs.map} +1 -1
  25. package/dist/lib/neutral/components/index.mjs +201 -16
  26. package/dist/lib/neutral/components/index.mjs.map +4 -4
  27. package/dist/lib/neutral/containers/index.mjs +6 -11
  28. package/dist/lib/neutral/containers/index.mjs.map +3 -3
  29. package/dist/lib/neutral/hooks/index.mjs +101 -17
  30. package/dist/lib/neutral/hooks/index.mjs.map +4 -4
  31. package/dist/lib/neutral/index.mjs +7 -1
  32. package/dist/lib/neutral/meta.json +1 -1
  33. package/dist/lib/neutral/{notification-tracker-CUDFRDCF.mjs → notification-tracker-Y5CRTWJM.mjs} +4 -2
  34. package/dist/lib/neutral/notification-tracker-Y5CRTWJM.mjs.map +7 -0
  35. package/dist/lib/neutral/{open-VU7YS3HB.mjs → open-P6XQ56M7.mjs} +4 -3
  36. package/dist/lib/neutral/{open-VU7YS3HB.mjs.map → open-P6XQ56M7.mjs.map} +1 -1
  37. package/dist/lib/neutral/operations/index.mjs +1 -1
  38. package/dist/lib/neutral/plugin.mjs +1 -1
  39. package/dist/lib/neutral/{react-surface-O4POQWYL.mjs → react-surface-H63FXUK3.mjs} +2 -2
  40. package/dist/lib/neutral/{react-surface-O4POQWYL.mjs.map → react-surface-H63FXUK3.mjs.map} +1 -1
  41. package/dist/lib/neutral/{revert-workspace-24TKG3I7.mjs → revert-workspace-RXWP2TEE.mjs} +2 -2
  42. package/dist/lib/neutral/{scroll-into-view-3VXT6TWC.mjs → scroll-into-view-HFV5EGYZ.mjs} +2 -2
  43. package/dist/lib/neutral/{set-RFOLTI57.mjs → set-HOOVB7E4.mjs} +4 -3
  44. package/dist/lib/neutral/{set-RFOLTI57.mjs.map → set-HOOVB7E4.mjs.map} +1 -1
  45. package/dist/lib/neutral/{set-layout-mode-AZ73W52Z.mjs → set-layout-mode-XAGSZZGB.mjs} +2 -2
  46. package/dist/lib/neutral/{state-D3YXB5NP.mjs → state-F4X5GPGZ.mjs} +3 -2
  47. package/dist/lib/neutral/{state-D3YXB5NP.mjs.map → state-F4X5GPGZ.mjs.map} +1 -1
  48. package/dist/lib/neutral/{switch-workspace-TBPT23CZ.mjs → switch-workspace-PA7N6EUX.mjs} +2 -2
  49. package/dist/lib/neutral/translations.mjs +2 -2
  50. package/dist/lib/neutral/translations.mjs.map +2 -2
  51. package/dist/lib/neutral/types/index.mjs +1 -1
  52. package/dist/lib/neutral/{update-companion-5LCY6QME.mjs → update-companion-6A2SKQAI.mjs} +14 -6
  53. package/dist/lib/neutral/update-companion-6A2SKQAI.mjs.map +7 -0
  54. package/dist/lib/neutral/{update-complementary-HZ7QXZYE.mjs → update-complementary-K55I5EV5.mjs} +2 -2
  55. package/dist/lib/neutral/{update-dialog-A7W3R5EI.mjs → update-dialog-Z3GEXMVV.mjs} +2 -2
  56. package/dist/lib/neutral/{update-plank-size-XHTYBLSD.mjs → update-plank-size-BTESVV7N.mjs} +2 -2
  57. package/dist/lib/neutral/{update-popover-RDIHG73B.mjs → update-popover-QEVDSZSH.mjs} +2 -2
  58. package/dist/lib/neutral/{update-sidebar-GEI7USA5.mjs → update-sidebar-VSRCN47U.mjs} +2 -2
  59. package/dist/lib/neutral/{url-handler-PUS4X33T.mjs → url-handler-ERZXXEYR.mjs} +3 -2
  60. package/dist/lib/neutral/{url-handler-PUS4X33T.mjs.map → url-handler-ERZXXEYR.mjs.map} +1 -1
  61. package/dist/types/src/DeckPlugin.d.ts.map +1 -1
  62. package/dist/types/src/capabilities/app-graph-builder.d.ts +1 -2
  63. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -1
  64. package/dist/types/src/capabilities/notification-tracker.d.ts +1 -1
  65. package/dist/types/src/capabilities/notification-tracker.d.ts.map +1 -1
  66. package/dist/types/src/capabilities/react-root.d.ts +1 -5
  67. package/dist/types/src/capabilities/react-root.d.ts.map +1 -1
  68. package/dist/types/src/capabilities/react-surface.d.ts +1 -1
  69. package/dist/types/src/capabilities/settings.d.ts +1 -3
  70. package/dist/types/src/capabilities/settings.d.ts.map +1 -1
  71. package/dist/types/src/capabilities/state.d.ts +1 -187
  72. package/dist/types/src/capabilities/state.d.ts.map +1 -1
  73. package/dist/types/src/capabilities/tools.d.ts +1 -1
  74. package/dist/types/src/capabilities/url-handler.d.ts +1 -1
  75. package/dist/types/src/components/Companion/Companion.d.ts +17 -0
  76. package/dist/types/src/components/Companion/Companion.d.ts.map +1 -0
  77. package/dist/types/src/components/Companion/index.d.ts +2 -0
  78. package/dist/types/src/components/Companion/index.d.ts.map +1 -0
  79. package/dist/types/src/components/Matrix/Matrix.d.ts +1 -1
  80. package/dist/types/src/components/Matrix/Matrix.d.ts.map +1 -1
  81. package/dist/types/src/components/Matrix/Matrix.stories.d.ts +2 -2
  82. package/dist/types/src/components/Matrix/Matrix.stories.d.ts.map +1 -1
  83. package/dist/types/src/components/Matrix/index.d.ts +1 -1
  84. package/dist/types/src/components/Matrix/index.d.ts.map +1 -1
  85. package/dist/types/src/components/Pane/Pane.d.ts +45 -0
  86. package/dist/types/src/components/Pane/Pane.d.ts.map +1 -0
  87. package/dist/types/src/components/Pane/Pane.stories.d.ts +7 -0
  88. package/dist/types/src/components/Pane/Pane.stories.d.ts.map +1 -0
  89. package/dist/types/src/components/Pane/index.d.ts +2 -0
  90. package/dist/types/src/components/Pane/index.d.ts.map +1 -0
  91. package/dist/types/src/components/Plank/Plank.d.ts +74 -0
  92. package/dist/types/src/components/Plank/Plank.d.ts.map +1 -0
  93. package/dist/types/src/components/Plank/Plank.stories.d.ts +6 -0
  94. package/dist/types/src/components/Plank/Plank.stories.d.ts.map +1 -0
  95. package/dist/types/src/components/Plank/index.d.ts +2 -0
  96. package/dist/types/src/components/Plank/index.d.ts.map +1 -0
  97. package/dist/types/src/components/index.d.ts +3 -2
  98. package/dist/types/src/components/index.d.ts.map +1 -1
  99. package/dist/types/src/containers/Deck/Deck.d.ts +1 -1
  100. package/dist/types/src/containers/Deck/Deck.d.ts.map +1 -1
  101. package/dist/types/src/containers/Deck/Deck.stories.d.ts +2 -2
  102. package/dist/types/src/containers/Deck/DeckPlank.d.ts +21 -0
  103. package/dist/types/src/containers/Deck/DeckPlank.d.ts.map +1 -0
  104. package/dist/types/src/containers/Deck/DeckViewport.d.ts +3 -1
  105. package/dist/types/src/containers/Deck/DeckViewport.d.ts.map +1 -1
  106. package/dist/types/src/containers/{Plank → Deck}/PlankControls.d.ts +3 -10
  107. package/dist/types/src/containers/Deck/PlankControls.d.ts.map +1 -0
  108. package/dist/types/src/containers/Deck/PlankFallback.d.ts +6 -0
  109. package/dist/types/src/containers/Deck/PlankFallback.d.ts.map +1 -0
  110. package/dist/types/src/containers/Deck/useDeckPlank.d.ts +49 -0
  111. package/dist/types/src/containers/Deck/useDeckPlank.d.ts.map +1 -0
  112. package/dist/types/src/containers/DeckLayout/DeckLayout.stories.d.ts +2 -2
  113. package/dist/types/src/containers/DeckLayout/DeckLayout.stories.d.ts.map +1 -1
  114. package/dist/types/src/{components → containers}/DeckSettings/DeckSettings.d.ts +1 -1
  115. package/dist/types/src/containers/DeckSettings/DeckSettings.d.ts.map +1 -0
  116. package/dist/types/src/{components → containers}/DeckSettings/DeckSettings.stories.d.ts +2 -2
  117. package/dist/types/src/{components → containers}/DeckSettings/DeckSettings.stories.d.ts.map +1 -1
  118. package/dist/types/src/containers/DeckSettings/index.d.ts.map +1 -0
  119. package/dist/types/src/containers/index.d.ts +1 -1
  120. package/dist/types/src/containers/index.d.ts.map +1 -1
  121. package/dist/types/src/hooks/index.d.ts +2 -0
  122. package/dist/types/src/hooks/index.d.ts.map +1 -1
  123. package/dist/types/src/hooks/useCompanionSplit.d.ts +28 -0
  124. package/dist/types/src/hooks/useCompanionSplit.d.ts.map +1 -0
  125. package/dist/types/src/hooks/useCompanions.d.ts +11 -7
  126. package/dist/types/src/hooks/useCompanions.d.ts.map +1 -1
  127. package/dist/types/src/hooks/useDeckCompanions.d.ts +2 -2
  128. package/dist/types/src/hooks/useDeckCompanions.d.ts.map +1 -1
  129. package/dist/types/src/hooks/useDeckState.d.ts +1 -1
  130. package/dist/types/src/hooks/useDeckState.d.ts.map +1 -1
  131. package/dist/types/src/hooks/useSelectedCompanionVariant.d.ts +6 -0
  132. package/dist/types/src/hooks/useSelectedCompanionVariant.d.ts.map +1 -0
  133. package/dist/types/src/index.d.ts +1 -0
  134. package/dist/types/src/index.d.ts.map +1 -1
  135. package/dist/types/src/meta.d.ts +4 -0
  136. package/dist/types/src/meta.d.ts.map +1 -1
  137. package/dist/types/src/operations/adjust.d.ts.map +1 -1
  138. package/dist/types/src/operations/update-companion.d.ts.map +1 -1
  139. package/dist/types/src/translations.d.ts +2 -2
  140. package/dist/types/src/translations.d.ts.map +1 -1
  141. package/dist/types/src/types/DeckCapabilities.d.ts +2 -2
  142. package/dist/types/src/types/DeckCapabilities.d.ts.map +1 -1
  143. package/dist/types/src/types/DeckOperation.d.ts +2 -2
  144. package/dist/types/src/types/DeckOperation.d.ts.map +1 -1
  145. package/dist/types/src/types/schema.d.ts +7 -7
  146. package/dist/types/src/types/schema.d.ts.map +1 -1
  147. package/dist/types/src/util/companion-view-state.d.ts +16 -0
  148. package/dist/types/src/util/companion-view-state.d.ts.map +1 -0
  149. package/dist/types/src/util/index.d.ts +1 -0
  150. package/dist/types/src/util/index.d.ts.map +1 -1
  151. package/dist/types/src/util/plank-url-params.d.ts +2 -0
  152. package/dist/types/src/util/plank-url-params.d.ts.map +1 -1
  153. package/dist/types/tsconfig.tsbuildinfo +1 -1
  154. package/package.json +49 -47
  155. package/src/DeckPlugin.ts +1 -2
  156. package/src/capabilities/app-graph-builder.ts +3 -2
  157. package/src/capabilities/notification-tracker.ts +3 -1
  158. package/src/capabilities/react-surface.tsx +1 -1
  159. package/src/components/Companion/Companion.tsx +91 -0
  160. package/src/components/Companion/index.ts +5 -0
  161. package/src/components/Matrix/Matrix.stories.tsx +13 -26
  162. package/src/components/Matrix/Matrix.tsx +3 -3
  163. package/src/components/Matrix/SPEC.md +1 -1
  164. package/src/components/Matrix/index.ts +1 -1
  165. package/src/components/Pane/Pane.stories.tsx +92 -0
  166. package/src/components/Pane/Pane.tsx +178 -0
  167. package/src/components/Pane/index.ts +5 -0
  168. package/src/components/Plank/Plank.stories.tsx +103 -0
  169. package/src/components/Plank/Plank.tsx +149 -0
  170. package/src/components/Plank/index.ts +5 -0
  171. package/src/components/index.ts +3 -4
  172. package/src/containers/Deck/Deck.tsx +3 -3
  173. package/src/containers/Deck/DeckPlank.tsx +187 -0
  174. package/src/containers/Deck/DeckViewport.tsx +116 -243
  175. package/src/containers/{Plank → Deck}/PlankControls.tsx +19 -17
  176. package/src/containers/Deck/PlankFallback.tsx +42 -0
  177. package/src/containers/Deck/useDeckPlank.ts +219 -0
  178. package/src/containers/DeckLayout/DeckLayout.stories.tsx +38 -51
  179. package/src/containers/DeckLayout/Dialog.tsx +1 -1
  180. package/src/{components → containers}/DeckSettings/DeckSettings.stories.tsx +1 -1
  181. package/src/containers/DeckSettings/DeckSettings.tsx +42 -0
  182. package/src/containers/Sidebar/ComplementarySidebar.tsx +2 -2
  183. package/src/containers/index.ts +1 -1
  184. package/src/hooks/index.ts +2 -0
  185. package/src/hooks/useCompanionSplit.ts +78 -0
  186. package/src/hooks/useCompanions.ts +39 -7
  187. package/src/hooks/useDeckCompanions.ts +3 -3
  188. package/src/hooks/useDeckState.ts +1 -1
  189. package/src/hooks/useSelectedCompanionVariant.ts +14 -0
  190. package/src/index.ts +7 -0
  191. package/src/operations/adjust.ts +23 -12
  192. package/src/operations/update-companion.ts +10 -6
  193. package/src/translations.ts +2 -2
  194. package/src/types/DeckCapabilities.ts +1 -1
  195. package/src/types/DeckOperation.ts +2 -1
  196. package/src/types/schema.ts +5 -4
  197. package/src/util/companion-view-state.ts +28 -0
  198. package/src/util/index.ts +1 -0
  199. package/src/util/plank-url-params.test.ts +5 -0
  200. package/src/util/plank-url-params.ts +3 -1
  201. package/src/util/sanitize-persisted-state.test.ts +1 -1
  202. package/src/util/set-active.test.ts +1 -1
  203. package/dist/lib/neutral/DeckLayout-VX2BP4Q2.mjs.map +0 -7
  204. package/dist/lib/neutral/DeckSettings-CQQZJ2YS.mjs +0 -27
  205. package/dist/lib/neutral/DeckSettings-CQQZJ2YS.mjs.map +0 -7
  206. package/dist/lib/neutral/adjust-7CZI4GK3.mjs.map +0 -7
  207. package/dist/lib/neutral/app-graph-builder-6HTXRTU2.mjs.map +0 -7
  208. package/dist/lib/neutral/chunk-CL7JIOI5.mjs.map +0 -7
  209. package/dist/lib/neutral/chunk-DNG3L5QN.mjs +0 -1304
  210. package/dist/lib/neutral/chunk-DNG3L5QN.mjs.map +0 -7
  211. package/dist/lib/neutral/chunk-NGX6RXNV.mjs.map +0 -7
  212. package/dist/lib/neutral/chunk-UZLAR4DR.mjs +0 -8
  213. package/dist/lib/neutral/notification-tracker-CUDFRDCF.mjs.map +0 -7
  214. package/dist/lib/neutral/update-companion-5LCY6QME.mjs.map +0 -7
  215. package/dist/types/src/components/DeckSettings/DeckSettings.d.ts.map +0 -1
  216. package/dist/types/src/components/DeckSettings/index.d.ts.map +0 -1
  217. package/dist/types/src/containers/Plank/Plank.d.ts +0 -17
  218. package/dist/types/src/containers/Plank/Plank.d.ts.map +0 -1
  219. package/dist/types/src/containers/Plank/Plank.stories.d.ts +0 -67
  220. package/dist/types/src/containers/Plank/Plank.stories.d.ts.map +0 -1
  221. package/dist/types/src/containers/Plank/PlankComponent.d.ts +0 -17
  222. package/dist/types/src/containers/Plank/PlankComponent.d.ts.map +0 -1
  223. package/dist/types/src/containers/Plank/PlankContent.d.ts +0 -10
  224. package/dist/types/src/containers/Plank/PlankContent.d.ts.map +0 -1
  225. package/dist/types/src/containers/Plank/PlankControls.d.ts.map +0 -1
  226. package/dist/types/src/containers/Plank/PlankError.d.ts +0 -15
  227. package/dist/types/src/containers/Plank/PlankError.d.ts.map +0 -1
  228. package/dist/types/src/containers/Plank/PlankHeading.d.ts +0 -22
  229. package/dist/types/src/containers/Plank/PlankHeading.d.ts.map +0 -1
  230. package/dist/types/src/containers/Plank/PlankLoading.d.ts +0 -3
  231. package/dist/types/src/containers/Plank/PlankLoading.d.ts.map +0 -1
  232. package/dist/types/src/containers/Plank/PlankRoot.d.ts +0 -37
  233. package/dist/types/src/containers/Plank/PlankRoot.d.ts.map +0 -1
  234. package/dist/types/src/containers/Plank/index.d.ts +0 -4
  235. package/dist/types/src/containers/Plank/index.d.ts.map +0 -1
  236. package/src/components/DeckSettings/DeckSettings.tsx +0 -34
  237. package/src/containers/Plank/Plank.stories.tsx +0 -108
  238. package/src/containers/Plank/Plank.tsx +0 -22
  239. package/src/containers/Plank/PlankComponent.tsx +0 -223
  240. package/src/containers/Plank/PlankContent.tsx +0 -45
  241. package/src/containers/Plank/PlankError.tsx +0 -82
  242. package/src/containers/Plank/PlankHeading.tsx +0 -236
  243. package/src/containers/Plank/PlankLoading.tsx +0 -10
  244. package/src/containers/Plank/PlankRoot.tsx +0 -49
  245. package/src/containers/Plank/index.ts +0 -7
  246. /package/dist/lib/neutral/{add-toast-APKTCLIA.mjs.map → add-toast-BHNEH5UY.mjs.map} +0 -0
  247. /package/dist/lib/neutral/{chunk-UZLAR4DR.mjs.map → chunk-FKGYHBUB.mjs.map} +0 -0
  248. /package/dist/lib/neutral/{revert-workspace-24TKG3I7.mjs.map → revert-workspace-RXWP2TEE.mjs.map} +0 -0
  249. /package/dist/lib/neutral/{scroll-into-view-3VXT6TWC.mjs.map → scroll-into-view-HFV5EGYZ.mjs.map} +0 -0
  250. /package/dist/lib/neutral/{set-layout-mode-AZ73W52Z.mjs.map → set-layout-mode-XAGSZZGB.mjs.map} +0 -0
  251. /package/dist/lib/neutral/{switch-workspace-TBPT23CZ.mjs.map → switch-workspace-PA7N6EUX.mjs.map} +0 -0
  252. /package/dist/lib/neutral/{update-complementary-HZ7QXZYE.mjs.map → update-complementary-K55I5EV5.mjs.map} +0 -0
  253. /package/dist/lib/neutral/{update-dialog-A7W3R5EI.mjs.map → update-dialog-Z3GEXMVV.mjs.map} +0 -0
  254. /package/dist/lib/neutral/{update-plank-size-XHTYBLSD.mjs.map → update-plank-size-BTESVV7N.mjs.map} +0 -0
  255. /package/dist/lib/neutral/{update-popover-RDIHG73B.mjs.map → update-popover-QEVDSZSH.mjs.map} +0 -0
  256. /package/dist/lib/neutral/{update-sidebar-GEI7USA5.mjs.map → update-sidebar-VSRCN47U.mjs.map} +0 -0
  257. /package/dist/types/src/{components → containers}/DeckSettings/index.d.ts +0 -0
  258. /package/src/{components → containers}/DeckSettings/index.ts +0 -0
@@ -0,0 +1,219 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { useCallback, useEffect, useMemo } from 'react';
6
+
7
+ import { useOperationInvoker } from '@dxos/app-framework/ui';
8
+ import { LayoutOperation } from '@dxos/app-toolkit';
9
+ import { type AttentionSigilAction } from '@dxos/app-toolkit/ui';
10
+ import { useAppGraph } from '@dxos/app-toolkit/ui';
11
+ import { Graph, type Node, useActionRunner, useActions, useNode } from '@dxos/plugin-graph';
12
+ import { getLinkedVariant, useAttention } from '@dxos/react-ui-attention';
13
+
14
+ import { useBreakpoints, useCompanions, useDeckState, useSelectedCompanion, useSelectedCompanionVariant } from '#hooks';
15
+ import { meta } from '#meta';
16
+ import { DeckOperation, type LayoutMode, PLANK_COMPANION_TYPE, type ResolvedPart } from '#types';
17
+
18
+ /** Sigil-menu dispositions surfaced as plank actions. */
19
+ const PLANK_ACTION_DISPOSITIONS = ['list-item', 'list-item-primary', 'heading-list-item'];
20
+
21
+ /** Capability flags that gate the plank toolbar controls. */
22
+ export type PlankCapabilities = {
23
+ deck?: boolean;
24
+ solo?: boolean;
25
+ incrementStart?: boolean;
26
+ incrementEnd?: boolean;
27
+ fullscreen?: boolean;
28
+ companion?: boolean;
29
+ };
30
+
31
+ export type UseDeckPlankOptions = {
32
+ id: string;
33
+ /** Resolved part for the primary plank (`solo` | `multi` | `complementary`). */
34
+ part: ResolvedPart;
35
+ layoutMode: LayoutMode;
36
+ /** Ordered active planks (multi mode); enables increment/close-range semantics. */
37
+ active?: string[];
38
+ /** Whether the companion pane should be shown for this plank (gated further by attention in multi-mode). */
39
+ companionShown?: boolean;
40
+ deckEnabled?: boolean;
41
+ };
42
+
43
+ export type DeckPlank = {
44
+ node: Node.Node | undefined;
45
+ companions: Node.Node[];
46
+ resolvedCompanionId: string | undefined;
47
+ currentCompanion: Node.Node | undefined;
48
+ hasCompanion: boolean;
49
+ /** Splitter orientation for the companion pane (defaults to `horizontal`). */
50
+ companionOrientation: 'horizontal' | 'vertical';
51
+ capabilities: PlankCapabilities;
52
+ /** Grouped sigil-menu actions, or `undefined` when the node is unresolved. */
53
+ sigilActions: AttentionSigilAction[][] | undefined;
54
+ popoverAnchorId?: string;
55
+ scrollIntoView?: string;
56
+ onAction: (action: AttentionSigilAction) => void;
57
+ onAdjust: (type: DeckOperation.PartAdjustment) => void;
58
+ onResize: (size: number) => void;
59
+ onScrollIntoView: (subject?: string) => void;
60
+ onUpdateCompanion: (companion: string | null) => void;
61
+ };
62
+
63
+ /**
64
+ * Resolves the graph node, companions, capabilities and sigil actions for a deck plank, and exposes the
65
+ * operation dispatchers that mutate deck layout state. This re-homes the framework wiring that the legacy
66
+ * `PlankContainer`/`PlankHeading` bundled, so the presentational components stay free of capabilities.
67
+ */
68
+ export const useDeckPlank = ({
69
+ id,
70
+ part,
71
+ layoutMode,
72
+ active,
73
+ companionShown,
74
+ deckEnabled,
75
+ }: UseDeckPlankOptions): DeckPlank => {
76
+ const { graph } = useAppGraph();
77
+ const { invokePromise } = useOperationInvoker();
78
+ const { state, deck } = useDeckState();
79
+ const runAction = useActionRunner();
80
+ const breakpoint = useBreakpoints();
81
+ const node = useNode(graph, id);
82
+ // Subscribe reactively to the node's actions: they are loaded asynchronously by `Graph.expand`
83
+ // below, and the node atom does not re-emit when action edges arrive, so a one-shot read would
84
+ // leave a freshly-created plank's sigil menu empty until an unrelated re-render.
85
+ const actions = useActions(graph, node?.id);
86
+ const companions = useCompanions(id);
87
+ const { hasAttention } = useAttention(id);
88
+ const selectedVariant = useSelectedCompanionVariant();
89
+
90
+ // The companion is shown when open; in multi mode it attaches only to the attended plank (hidden until
91
+ // a plank gains attention). Which companion shows follows the globally-selected variant (view state),
92
+ // falling back to the first when none is stored.
93
+ const showCompanion = !!companionShown && (layoutMode !== 'multi' || hasAttention);
94
+ const { companionId } = useSelectedCompanion(companions, showCompanion ? selectedVariant : undefined);
95
+ const resolvedCompanionId = showCompanion ? companionId : undefined;
96
+ const currentCompanion = companions.find((companion) => companion.id === resolvedCompanionId);
97
+ const hasCompanion = !!(resolvedCompanionId && currentCompanion);
98
+ const companionOrientation = deck.companionOrientation ?? 'horizontal';
99
+
100
+ // Ordering within the active stack drives the increment-start/end affordances.
101
+ const index = active ? active.findIndex((entryId) => entryId === id) : -1;
102
+ const isOrdered = !!active && index >= 0;
103
+ const canIncrementStart = isOrdered && index > 0;
104
+ const canIncrementEnd = isOrdered && index < (active?.length ?? 1) - 1;
105
+
106
+ const isCompanionNode = node?.type === PLANK_COMPANION_TYPE;
107
+ const capabilities = useMemo<PlankCapabilities>(
108
+ () => ({
109
+ deck: deckEnabled ?? true,
110
+ solo: breakpoint !== 'mobile' && (part === 'solo' || part === 'multi'),
111
+ incrementStart: canIncrementStart,
112
+ incrementEnd: canIncrementEnd,
113
+ fullscreen: !isCompanionNode,
114
+ // Offer to open the companion (solo, or the attended plank in multi) when one exists and isn't shown.
115
+ companion: !isCompanionNode && companions.length > 0 && !hasCompanion && (layoutMode !== 'multi' || hasAttention),
116
+ }),
117
+ [
118
+ deckEnabled,
119
+ breakpoint,
120
+ part,
121
+ canIncrementStart,
122
+ canIncrementEnd,
123
+ isCompanionNode,
124
+ layoutMode,
125
+ companions.length,
126
+ hasCompanion,
127
+ hasAttention,
128
+ ],
129
+ );
130
+
131
+ // Load the node's child actions so the sigil menu is populated.
132
+ useEffect(() => {
133
+ const frame = requestAnimationFrame(() => {
134
+ if (node) {
135
+ void Graph.expand(graph, node.id, 'child');
136
+ }
137
+ });
138
+
139
+ return () => cancelAnimationFrame(frame);
140
+ }, [graph, node]);
141
+
142
+ const variant = isCompanionNode ? getLinkedVariant(id) : undefined;
143
+ const sigilActions = useMemo<AttentionSigilAction[][] | undefined>(() => {
144
+ if (!node) {
145
+ return undefined;
146
+ }
147
+ if (variant) {
148
+ return [];
149
+ }
150
+
151
+ return [actions.filter((action) => PLANK_ACTION_DISPOSITIONS.includes(action.properties.disposition))].filter(
152
+ (group) => group.length > 0,
153
+ );
154
+ }, [actions, node, variant]);
155
+
156
+ const onAction = useCallback(
157
+ (action: AttentionSigilAction) => {
158
+ // Only actions whose `data` is a function are runnable graph actions; the menu-action view type
159
+ // (AttentionSigilAction) is widened, so narrow at this runtime-checked boundary.
160
+ if (typeof action.data === 'function') {
161
+ void runAction(action as Node.Action, { parent: node, caller: meta.profile.key });
162
+ }
163
+ },
164
+ [node, runAction],
165
+ );
166
+
167
+ const onAdjust = useCallback(
168
+ (type: DeckOperation.PartAdjustment) => {
169
+ if (type === 'close') {
170
+ if (part === 'complementary') {
171
+ return invokePromise(LayoutOperation.UpdateComplementary, { state: 'collapsed' });
172
+ }
173
+ if (active) {
174
+ // Close the plank and everything to its right (stack pop).
175
+ const closeIndex = active.indexOf(id);
176
+ const toClose = closeIndex !== -1 ? active.slice(closeIndex) : [id];
177
+ return invokePromise(LayoutOperation.Close, { subject: toClose });
178
+ }
179
+ return invokePromise(LayoutOperation.Close, { subject: [id] });
180
+ }
181
+
182
+ return invokePromise(DeckOperation.Adjust, { type, id });
183
+ },
184
+ [invokePromise, part, active, id],
185
+ );
186
+
187
+ const onResize = useCallback(
188
+ (size: number) => invokePromise(DeckOperation.UpdatePlankSize, { id, size }),
189
+ [invokePromise, id],
190
+ );
191
+
192
+ const onScrollIntoView = useCallback(
193
+ (subject?: string) => invokePromise(LayoutOperation.ScrollIntoView, { subject }),
194
+ [invokePromise],
195
+ );
196
+
197
+ const onUpdateCompanion = useCallback(
198
+ (companion: string | null) => invokePromise(LayoutOperation.UpdateCompanion, { subject: companion }),
199
+ [invokePromise],
200
+ );
201
+
202
+ return {
203
+ node,
204
+ companions,
205
+ resolvedCompanionId,
206
+ currentCompanion,
207
+ hasCompanion,
208
+ companionOrientation,
209
+ capabilities,
210
+ sigilActions,
211
+ popoverAnchorId: state.popoverAnchorId,
212
+ scrollIntoView: state.scrollIntoView,
213
+ onAction,
214
+ onAdjust,
215
+ onResize,
216
+ onScrollIntoView,
217
+ onUpdateCompanion,
218
+ };
219
+ };
@@ -17,10 +17,12 @@ import { GraphBuilder, Node, NodeMatcher, useConnections } from '@dxos/plugin-gr
17
17
  import { corePlugins } from '@dxos/plugin-testing';
18
18
  import { random } from '@dxos/random';
19
19
  import { useAsyncEffect } from '@dxos/react-hooks';
20
- import { Icon, List, ListItem, Panel } from '@dxos/react-ui';
20
+ import { Panel } from '@dxos/react-ui';
21
21
  import { linkedSegment } from '@dxos/react-ui-attention';
22
+ import { Listbox } from '@dxos/react-ui-list';
22
23
  import { Syntax } from '@dxos/react-ui-syntax-highlighter';
23
24
  import { Loading, withLayout } from '@dxos/react-ui/testing';
25
+ import { Position } from '@dxos/util';
24
26
 
25
27
  import { OperationHandler } from '#capabilities';
26
28
  import { meta as pluginMeta } from '#meta';
@@ -28,11 +30,11 @@ import { translations } from '#translations';
28
30
  import {
29
31
  DeckCapabilities,
30
32
  type EphemeralDeckState,
33
+ PLANK_COMPANION_TYPE,
31
34
  type Settings,
32
35
  type StoredDeckState,
33
36
  defaultDeck,
34
37
  getMode,
35
- PLANK_COMPANION_TYPE,
36
38
  } from '#types';
37
39
 
38
40
  import { DeckLayout } from './DeckLayout';
@@ -245,7 +247,7 @@ const TestPlugin = Plugin.define(pluginMeta).pipe(
245
247
  label: 'Companion Alpha',
246
248
  icon: 'ph--sidebar--regular',
247
249
  data: { variant: 'alpha', parentId: node.id },
248
- position: 'first',
250
+ position: Position.first,
249
251
  }),
250
252
  AppNode.makeCompanion({
251
253
  id: linkedSegment('beta'),
@@ -276,24 +278,23 @@ const NavContainer = forwardRef<HTMLDivElement, NavContainerProps>((_props, forw
276
278
 
277
279
  return (
278
280
  <div className='dx-container overflow-y-auto p-2' ref={forwardedRef}>
279
- <List>
280
- {items.map((node) => (
281
- <ListItem.Root
282
- key={node.id}
283
- classNames={activeSet.has(node.id) ? 'bg-current-surface' : undefined}
284
- onClick={() => void invokePromise(LayoutOperation.Set, { subject: [node.id] })}
285
- >
286
- {node.properties.icon && (
287
- <ListItem.Endcap>
288
- <Icon icon={node.properties.icon} />
289
- </ListItem.Endcap>
290
- )}
291
- <ListItem.Heading classNames='cursor-pointer'>
292
- {typeof node.properties.label === 'string' ? node.properties.label : node.id}
293
- </ListItem.Heading>
294
- </ListItem.Root>
295
- ))}
296
- </List>
281
+ <Listbox.Root>
282
+ <Listbox.Content aria-label='Navigation'>
283
+ {items.map((node) => (
284
+ <Listbox.Item
285
+ key={node.id}
286
+ id={node.id}
287
+ classNames={activeSet.has(node.id) ? 'bg-current-surface' : undefined}
288
+ onClick={() => void invokePromise(LayoutOperation.Set, { subject: [node.id] })}
289
+ >
290
+ <Listbox.ItemContent
291
+ icon={node.properties.icon}
292
+ title={typeof node.properties.label === 'string' ? node.properties.label : node.id}
293
+ />
294
+ </Listbox.Item>
295
+ ))}
296
+ </Listbox.Content>
297
+ </Listbox.Root>
297
298
  </div>
298
299
  );
299
300
  });
@@ -312,36 +313,22 @@ const ItemComponent = ({ id }: ItemComponentProps) => {
312
313
  );
313
314
 
314
315
  return (
315
- <List>
316
- {items.map((node) => {
317
- const open = () =>
318
- void invokePromise(LayoutOperation.Open, { subject: [node.id], pivotId: id, navigation: 'immediate' });
319
- return (
320
- <ListItem.Root
321
- key={node.id}
322
- classNames='dx-hover cursor-pointer'
323
- role='button'
324
- tabIndex={0}
325
- onClick={open}
326
- onKeyDown={(event) => {
327
- if (event.key === 'Enter' || event.key === ' ') {
328
- event.preventDefault();
329
- open();
330
- }
331
- }}
332
- >
333
- {node.properties.icon && (
334
- <ListItem.Endcap>
335
- <Icon icon={node.properties.icon} size={4} />
336
- </ListItem.Endcap>
337
- )}
338
- <ListItem.Heading classNames='truncate'>
339
- {typeof node.properties.label === 'string' ? node.properties.label : node.id}
340
- </ListItem.Heading>
341
- </ListItem.Root>
342
- );
343
- })}
344
- </List>
316
+ <Listbox.Root>
317
+ <Listbox.Content aria-label='Items'>
318
+ {items.map((node) => {
319
+ const open = () =>
320
+ void invokePromise(LayoutOperation.Open, { subject: [node.id], pivotId: id, navigation: 'immediate' });
321
+ return (
322
+ <Listbox.Item key={node.id} id={node.id} classNames='dx-hover cursor-pointer' onClick={open}>
323
+ <Listbox.ItemContent
324
+ icon={node.properties.icon}
325
+ title={typeof node.properties.label === 'string' ? node.properties.label : node.id}
326
+ />
327
+ </Listbox.Item>
328
+ );
329
+ })}
330
+ </Listbox.Content>
331
+ </Listbox.Root>
345
332
  );
346
333
  };
347
334
 
@@ -10,7 +10,7 @@ import { AlertDialog, Dialog as NaturalDialog } from '@dxos/react-ui';
10
10
 
11
11
  import { useDeckState } from '#hooks';
12
12
 
13
- import { PlankErrorFallback } from '../Plank';
13
+ import { PlankErrorFallback } from '../Deck/PlankFallback';
14
14
 
15
15
  export const Dialog = () => {
16
16
  const { state, updateEphemeral } = useDeckState();
@@ -11,7 +11,7 @@ import { translations } from '#translations';
11
11
  import { DeckSettings } from './DeckSettings';
12
12
 
13
13
  const meta = {
14
- title: 'plugins/plugin-deck/components/DeckSettings',
14
+ title: 'plugins/plugin-deck/containers/DeckSettings',
15
15
  tags: ['settings'],
16
16
  component: DeckSettings,
17
17
  decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
@@ -0,0 +1,42 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ import { type AppSurface } from '@dxos/app-toolkit/ui';
8
+ import { useTranslation } from '@dxos/react-ui';
9
+ import { Form } from '@dxos/react-ui-form';
10
+
11
+ import { meta } from '#meta';
12
+ import { Settings } from '#types';
13
+
14
+ const isSocket = !!(globalThis as any).__args;
15
+
16
+ export type DeckSettingsProps = AppSurface.SettingsProps<Settings.Settings>;
17
+
18
+ export const DeckSettings = ({ settings, onSettingsChange }: DeckSettingsProps) => {
19
+ const { t } = useTranslation(meta.profile.key);
20
+
21
+ return (
22
+ <Form.Root
23
+ variant='settings'
24
+ schema={Settings.Settings}
25
+ values={settings}
26
+ readonly={!onSettingsChange}
27
+ onValuesChanged={(values) => onSettingsChange?.((current) => ({ ...current, ...values }))}
28
+ >
29
+ <Form.Viewport scroll>
30
+ <Form.Content>
31
+ <Form.Section title={meta.profile.name ?? meta.profile.key}>
32
+ <Form.FieldSet
33
+ filter={(properties) =>
34
+ isSocket ? properties.filter((property) => property.name !== 'enableNativeRedirect') : properties
35
+ }
36
+ />
37
+ </Form.Section>
38
+ </Form.Content>
39
+ </Form.Viewport>
40
+ </Form.Root>
41
+ );
42
+ };
@@ -7,7 +7,7 @@ import React, { type MouseEvent, useCallback, useEffect, useMemo, useState } fro
7
7
  import { Surface, useOperationInvoker } from '@dxos/app-framework/ui';
8
8
  import { LayoutOperation } from '@dxos/app-toolkit';
9
9
  import { AppSurface } from '@dxos/app-toolkit/ui';
10
- import { IconButton, type Label, Main, Panel, toLocalizedString, Toolbar, useTranslation } from '@dxos/react-ui';
10
+ import { IconButton, type Label, Main, Panel, Toolbar, toLocalizedString, useTranslation } from '@dxos/react-ui';
11
11
  import { getLinkedVariant } from '@dxos/react-ui-attention';
12
12
  import { Tabs } from '@dxos/react-ui-tabs';
13
13
  import { iconSize, mx } from '@dxos/ui-theme';
@@ -17,7 +17,7 @@ import { meta } from '#meta';
17
17
  import { getMode } from '#types';
18
18
 
19
19
  import { layoutAppliesTopbar } from '../../util';
20
- import { PlankErrorFallback, PlankLoading } from '../Plank';
20
+ import { PlankErrorFallback, PlankLoading } from '../Deck/PlankFallback';
21
21
  import { ToggleComplementarySidebarButton } from './SidebarButton';
22
22
 
23
23
  const label = ['complementary-sidebar.title', { ns: meta.profile.key }] satisfies Label;
@@ -5,7 +5,7 @@
5
5
  import { type ComponentType, lazy } from 'react';
6
6
 
7
7
  export * from './Deck';
8
- export * from './Plank';
9
8
  export * from './Sidebar';
10
9
 
11
10
  export const DeckLayout: ComponentType<any> = lazy(() => import('./DeckLayout'));
11
+ export const DeckSettings: ComponentType<any> = lazy(() => import('./DeckSettings'));
@@ -4,8 +4,10 @@
4
4
 
5
5
  export * from './useBreakpoints';
6
6
  export * from './useCompanions';
7
+ export * from './useCompanionSplit';
7
8
  export * from './useDeckCompanions';
8
9
  export * from './useDeckState';
9
10
  export * from './useMainSize';
10
11
  export * from './useNodeActionExpander';
11
12
  export * from './useSelectedCompanion';
13
+ export * from './useSelectedCompanionVariant';
@@ -0,0 +1,78 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import * as Schema from 'effect/Schema';
6
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
7
+
8
+ import { type SplitterOrientation } from '@dxos/react-ui';
9
+ import { type AspectDef, defineViewState, useViewState, useViewStateActions } from '@dxos/react-ui-attention';
10
+
11
+ import { COMPANION_VIEW_STATE_CONTEXT } from '../util';
12
+
13
+ /** Companion split point (rem) used until the user resizes a given orientation. */
14
+ export const DEFAULT_COMPANION_SIZE = 30;
15
+
16
+ /** Trailing delay before a resize is committed to storage (drags fire continuously). */
17
+ const PERSIST_DELAY = 250;
18
+
19
+ const CompanionSplitSchema = Schema.Struct({
20
+ horizontal: Schema.optional(Schema.Number),
21
+ vertical: Schema.optional(Schema.Number),
22
+ }).pipe(Schema.mutable);
23
+
24
+ export type CompanionSplit = Schema.Schema.Type<typeof CompanionSplitSchema>;
25
+
26
+ /**
27
+ * Companion splitter sizes (rem), memoized separately for each orientation so toggling the companion
28
+ * between side-by-side and stacked restores that orientation's last split point. Stored in the `local`
29
+ * backend (localStorage, persisted across reloads) via react-ui-attention view state.
30
+ */
31
+ export const companionSplitAspect: AspectDef<CompanionSplit> = defineViewState<CompanionSplit>({
32
+ key: 'deck-companion-split',
33
+ backend: 'local',
34
+ schema: CompanionSplitSchema,
35
+ defaultValue: () => ({}),
36
+ });
37
+
38
+ export type UseCompanionSplit = {
39
+ /** The remembered split point for the current orientation (or the default). */
40
+ size: number;
41
+ onSizeChange: (size: number) => void;
42
+ };
43
+
44
+ /**
45
+ * Reads and writes the global companion splitter size keyed by `orientation`. A live size keeps dragging
46
+ * smooth while the persisted write is debounced (and flushed on unmount) to avoid per-frame storage writes.
47
+ */
48
+ export const useCompanionSplit = (orientation: SplitterOrientation): UseCompanionSplit => {
49
+ const split = useViewState(companionSplitAspect, COMPANION_VIEW_STATE_CONTEXT);
50
+ const { update } = useViewStateActions(companionSplitAspect, COMPANION_VIEW_STATE_CONTEXT);
51
+ const storedSize = split[orientation] ?? DEFAULT_COMPANION_SIZE;
52
+
53
+ // Drive the controlled Splitter from local state so a drag tracks the pointer without a storage
54
+ // round-trip per frame; reseed when the orientation switches (recall) or storage changes externally.
55
+ const [liveSize, setLiveSize] = useState(storedSize);
56
+ useEffect(() => setLiveSize(storedSize), [storedSize, orientation]);
57
+
58
+ // Flush the trailing write on unmount so the final size is not dropped inside the debounce window.
59
+ const pending = useRef<{ timer: ReturnType<typeof setTimeout>; flush: () => void } | undefined>(undefined);
60
+ useEffect(() => () => pending.current?.flush(), []);
61
+
62
+ const onSizeChange = useCallback(
63
+ (next: number) => {
64
+ setLiveSize(next);
65
+ if (pending.current) {
66
+ clearTimeout(pending.current.timer);
67
+ }
68
+ const flush = () => {
69
+ pending.current = undefined;
70
+ update((prev) => ({ ...prev, [orientation]: next }));
71
+ };
72
+ pending.current = { timer: setTimeout(flush, PERSIST_DELAY), flush };
73
+ },
74
+ [update, orientation],
75
+ );
76
+
77
+ return useMemo(() => ({ size: liveSize, onSizeChange }), [liveSize, onSizeChange]);
78
+ };
@@ -2,17 +2,49 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { useMemo } from 'react';
5
+ import { RegistryContext } from '@effect-atom/atom-react';
6
+ import { useContext, useEffect, useState } from 'react';
6
7
 
7
8
  import { useAppGraph } from '@dxos/app-toolkit/ui';
8
- import { useConnections } from '@dxos/plugin-graph';
9
- import { byPosition } from '@dxos/util';
9
+ import { type Node } from '@dxos/plugin-graph';
10
+ import { Position } from '@dxos/util';
10
11
 
11
12
  import { PLANK_COMPANION_TYPE } from '#types';
12
13
 
13
- export const useCompanions = (id?: string) => {
14
+ /**
15
+ * Companion (child) nodes for a plank.
16
+ *
17
+ * The node's child-connections atom is read in a commit-phase effect rather than during render.
18
+ * Subscribing to it during render (via `useConnections`/`useAtomValue`) recomputes the shared atom and
19
+ * synchronously notifies its other subscribers — notably navtree items rendering the same node — which
20
+ * surfaces as a React "cannot update a component while rendering a different component" warning. Reading
21
+ * it from an effect defers that notification to the commit phase where cross-component updates are allowed.
22
+ */
23
+ export const useCompanions = (id?: string): Node.Node[] => {
14
24
  const { graph } = useAppGraph();
15
- const nodes = useConnections(graph, id, 'child');
16
- const companions = nodes.filter((node) => node.type === PLANK_COMPANION_TYPE);
17
- return useMemo(() => companions.toSorted((a, b) => byPosition(a.properties, b.properties)), [companions]);
25
+ const registry = useContext(RegistryContext);
26
+ const [companions, setCompanions] = useState<Node.Node[]>([]);
27
+
28
+ useEffect(() => {
29
+ if (!id) {
30
+ setCompanions([]);
31
+ return;
32
+ }
33
+
34
+ const atom = graph.connections(id, 'child');
35
+ const update = () => {
36
+ const next = registry
37
+ .get(atom)
38
+ .filter((node) => node.type === PLANK_COMPANION_TYPE)
39
+ .toSorted((a, b) => Position.compare(a.properties, b.properties));
40
+ setCompanions((prev) =>
41
+ prev.length === next.length && prev.every((node, index) => node === next[index]) ? prev : next,
42
+ );
43
+ };
44
+
45
+ update();
46
+ return registry.subscribe(atom, update);
47
+ }, [graph, registry, id]);
48
+
49
+ return companions;
18
50
  };
@@ -6,7 +6,7 @@ import { useAppGraph } from '@dxos/app-toolkit/ui';
6
6
  import { Node, type Node as NodeType } from '@dxos/plugin-graph';
7
7
  import { useConnections } from '@dxos/plugin-graph';
8
8
  import { type Label } from '@dxos/ui-types/translations';
9
- import { type Position, byPosition } from '@dxos/util';
9
+ import { Position } from '@dxos/util';
10
10
 
11
11
  import { DECK_COMPANION_TYPE } from '#types';
12
12
 
@@ -18,7 +18,7 @@ export type DeckCompanion = NodeType.Node<
18
18
  // TODO(burdon): Scroll area should be controlled by surface.
19
19
  /** If true, the panel will not be wrapped in a scroll area. */
20
20
  fixed?: boolean;
21
- position?: Position;
21
+ position?: Position.Position;
22
22
  joyride?: string;
23
23
  }
24
24
  >;
@@ -27,5 +27,5 @@ export const useDeckCompanions = (): DeckCompanion[] => {
27
27
  const { graph } = useAppGraph();
28
28
  const connections = useConnections(graph, Node.RootId, 'child');
29
29
  const companions = connections.filter((node) => node.type === DECK_COMPANION_TYPE) as DeckCompanion[];
30
- return companions.toSorted((a, b) => byPosition(a.properties, b.properties));
30
+ return companions.toSorted((a, b) => Position.compare(a.properties, b.properties));
31
31
  };
@@ -11,9 +11,9 @@ import { invariant } from '@dxos/invariant';
11
11
 
12
12
  import {
13
13
  DeckCapabilities,
14
- type EphemeralDeckState,
15
14
  type DeckPluginState,
16
15
  type DeckState,
16
+ type EphemeralDeckState,
17
17
  type StoredDeckState,
18
18
  } from '#types';
19
19
 
@@ -0,0 +1,14 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { useViewState } from '@dxos/react-ui-attention';
6
+
7
+ import { COMPANION_VIEW_STATE_CONTEXT, companionVariantAspect } from '../util';
8
+
9
+ /**
10
+ * Reads the globally-selected companion variant (persisted via view state) so reopening the companion
11
+ * restores the last-selected tab rather than resetting to the first.
12
+ */
13
+ export const useSelectedCompanionVariant = (): string | undefined =>
14
+ useViewState(companionVariantAspect, COMPANION_VIEW_STATE_CONTEXT).variant;
package/src/index.ts CHANGED
@@ -4,3 +4,10 @@
4
4
 
5
5
  export * from './meta';
6
6
  export * from './types';
7
+ // Companion selection view-state aspect — public so consumers (e.g. plugin-assistant) can read the
8
+ // globally-selected companion variant that used to live on `DeckState.companionVariant`.
9
+ export {
10
+ COMPANION_VIEW_STATE_CONTEXT,
11
+ type CompanionSelection,
12
+ companionVariantAspect,
13
+ } from './util/companion-view-state';