@astrale-os/shell-react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (378) hide show
  1. package/LICENSE +202 -0
  2. package/dist/application/apps/app-host.d.ts +31 -0
  3. package/dist/application/apps/app-host.d.ts.map +1 -0
  4. package/dist/application/apps/app-host.js +59 -0
  5. package/dist/application/apps/app-logo.d.ts +19 -0
  6. package/dist/application/apps/app-logo.d.ts.map +1 -0
  7. package/dist/application/apps/app-logo.js +31 -0
  8. package/dist/application/apps/apps.hook.d.ts +12 -0
  9. package/dist/application/apps/apps.hook.d.ts.map +1 -0
  10. package/dist/application/apps/apps.hook.js +51 -0
  11. package/dist/application/apps/open-app.hook.d.ts +10 -0
  12. package/dist/application/apps/open-app.hook.d.ts.map +1 -0
  13. package/dist/application/apps/open-app.hook.js +7 -0
  14. package/dist/application/index.d.ts +18 -0
  15. package/dist/application/index.d.ts.map +1 -0
  16. package/dist/application/index.js +12 -0
  17. package/dist/application/roles.hook.d.ts +7 -0
  18. package/dist/application/roles.hook.d.ts.map +1 -0
  19. package/dist/application/roles.hook.js +23 -0
  20. package/dist/application/spaces/current-space.hook.d.ts +7 -0
  21. package/dist/application/spaces/current-space.hook.d.ts.map +1 -0
  22. package/dist/application/spaces/current-space.hook.js +27 -0
  23. package/dist/application/spaces/current-space.store.d.ts +32 -0
  24. package/dist/application/spaces/current-space.store.d.ts.map +1 -0
  25. package/dist/application/spaces/current-space.store.js +79 -0
  26. package/dist/application/spaces/space.hook.d.ts +5 -0
  27. package/dist/application/spaces/space.hook.d.ts.map +1 -0
  28. package/dist/application/spaces/space.hook.js +25 -0
  29. package/dist/application/spaces/spaces.hook.d.ts +14 -0
  30. package/dist/application/spaces/spaces.hook.d.ts.map +1 -0
  31. package/dist/application/spaces/spaces.hook.js +82 -0
  32. package/dist/application/user/user.hook.d.ts +10 -0
  33. package/dist/application/user/user.hook.d.ts.map +1 -0
  34. package/dist/application/user/user.hook.js +19 -0
  35. package/dist/application/user/user.store.d.ts +35 -0
  36. package/dist/application/user/user.store.d.ts.map +1 -0
  37. package/dist/application/user/user.store.js +51 -0
  38. package/dist/auth/act-as.context.d.ts +40 -0
  39. package/dist/auth/act-as.context.d.ts.map +1 -0
  40. package/dist/auth/act-as.context.js +84 -0
  41. package/dist/auth/auth.hook.d.ts +3 -0
  42. package/dist/auth/auth.hook.d.ts.map +1 -0
  43. package/dist/auth/auth.hook.js +25 -0
  44. package/dist/auth/can.hook.d.ts +40 -0
  45. package/dist/auth/can.hook.d.ts.map +1 -0
  46. package/dist/auth/can.hook.js +119 -0
  47. package/dist/auth/index.d.ts +6 -0
  48. package/dist/auth/index.d.ts.map +1 -0
  49. package/dist/auth/index.js +5 -0
  50. package/dist/auth/self.hook.d.ts +3 -0
  51. package/dist/auth/self.hook.d.ts.map +1 -0
  52. package/dist/auth/self.hook.js +11 -0
  53. package/dist/auth/session-kernel.d.ts +4 -0
  54. package/dist/auth/session-kernel.d.ts.map +1 -0
  55. package/dist/auth/session-kernel.js +21 -0
  56. package/dist/graph/action.d.ts +22 -0
  57. package/dist/graph/action.d.ts.map +1 -0
  58. package/dist/graph/action.js +85 -0
  59. package/dist/graph/async.d.ts +11 -0
  60. package/dist/graph/async.d.ts.map +1 -0
  61. package/dist/graph/async.js +60 -0
  62. package/dist/graph/call/call.hook.d.ts +21 -0
  63. package/dist/graph/call/call.hook.d.ts.map +1 -0
  64. package/dist/graph/call/call.hook.js +23 -0
  65. package/dist/graph/call/stream.hook.d.ts +28 -0
  66. package/dist/graph/call/stream.hook.d.ts.map +1 -0
  67. package/dist/graph/call/stream.hook.js +137 -0
  68. package/dist/graph/graph.context.d.ts +4 -0
  69. package/dist/graph/graph.context.d.ts.map +1 -0
  70. package/dist/graph/graph.context.js +10 -0
  71. package/dist/graph/guards.error.d.ts +9 -0
  72. package/dist/graph/guards.error.d.ts.map +1 -0
  73. package/dist/graph/guards.error.js +7 -0
  74. package/dist/graph/index.d.ts +20 -0
  75. package/dist/graph/index.d.ts.map +1 -0
  76. package/dist/graph/index.js +9 -0
  77. package/dist/graph/memory/base.d.ts +21 -0
  78. package/dist/graph/memory/base.d.ts.map +1 -0
  79. package/dist/graph/memory/base.js +33 -0
  80. package/dist/graph/memory/read/envelope.d.ts +71 -0
  81. package/dist/graph/memory/read/envelope.d.ts.map +1 -0
  82. package/dist/graph/memory/read/envelope.js +29 -0
  83. package/dist/graph/memory/read/idle.d.ts +18 -0
  84. package/dist/graph/memory/read/idle.d.ts.map +1 -0
  85. package/dist/graph/memory/read/idle.js +16 -0
  86. package/dist/graph/memory/read/read.d.ts +9 -0
  87. package/dist/graph/memory/read/read.d.ts.map +1 -0
  88. package/dist/graph/memory/read/read.js +87 -0
  89. package/dist/graph/memory/scope.d.ts +7 -0
  90. package/dist/graph/memory/scope.d.ts.map +1 -0
  91. package/dist/graph/memory/scope.js +25 -0
  92. package/dist/graph/mutate/move.hook.d.ts +7 -0
  93. package/dist/graph/mutate/move.hook.d.ts.map +1 -0
  94. package/dist/graph/mutate/move.hook.js +49 -0
  95. package/dist/graph/mutate/mutate.hook.d.ts +17 -0
  96. package/dist/graph/mutate/mutate.hook.d.ts.map +1 -0
  97. package/dist/graph/mutate/mutate.hook.js +45 -0
  98. package/dist/graph/query/children.hook.d.ts +29 -0
  99. package/dist/graph/query/children.hook.d.ts.map +1 -0
  100. package/dist/graph/query/children.hook.js +64 -0
  101. package/dist/graph/query/graph.hook.d.ts +9 -0
  102. package/dist/graph/query/graph.hook.d.ts.map +1 -0
  103. package/dist/graph/query/graph.hook.js +5 -0
  104. package/dist/graph/query/neighbors.hook.d.ts +7 -0
  105. package/dist/graph/query/neighbors.hook.d.ts.map +1 -0
  106. package/dist/graph/query/neighbors.hook.js +79 -0
  107. package/dist/graph/query/node.hook.d.ts +14 -0
  108. package/dist/graph/query/node.hook.d.ts.map +1 -0
  109. package/dist/graph/query/node.hook.js +43 -0
  110. package/dist/graph/query/query.hook.d.ts +14 -0
  111. package/dist/graph/query/query.hook.d.ts.map +1 -0
  112. package/dist/graph/query/query.hook.js +23 -0
  113. package/dist/index.d.ts +43 -0
  114. package/dist/index.d.ts.map +1 -0
  115. package/dist/index.js +34 -0
  116. package/dist/intents/envelope.d.ts +33 -0
  117. package/dist/intents/envelope.d.ts.map +1 -0
  118. package/dist/intents/envelope.js +19 -0
  119. package/dist/intents/index.d.ts +6 -0
  120. package/dist/intents/index.d.ts.map +1 -0
  121. package/dist/intents/index.js +3 -0
  122. package/dist/intents/listen-intent.hook.d.ts +16 -0
  123. package/dist/intents/listen-intent.hook.d.ts.map +1 -0
  124. package/dist/intents/listen-intent.hook.js +75 -0
  125. package/dist/intents/reply.d.ts +9 -0
  126. package/dist/intents/reply.d.ts.map +1 -0
  127. package/dist/intents/reply.js +14 -0
  128. package/dist/intents/send-intent.hook.d.ts +12 -0
  129. package/dist/intents/send-intent.hook.d.ts.map +1 -0
  130. package/dist/intents/send-intent.hook.js +86 -0
  131. package/dist/navigation/adapters/adapter.d.ts +15 -0
  132. package/dist/navigation/adapters/adapter.d.ts.map +1 -0
  133. package/dist/navigation/adapters/adapter.js +1 -0
  134. package/dist/navigation/adapters/memory.adapter.d.ts +9 -0
  135. package/dist/navigation/adapters/memory.adapter.d.ts.map +1 -0
  136. package/dist/navigation/adapters/memory.adapter.js +15 -0
  137. package/dist/navigation/adapters/url.adapter.d.ts +6 -0
  138. package/dist/navigation/adapters/url.adapter.d.ts.map +1 -0
  139. package/dist/navigation/adapters/url.adapter.js +59 -0
  140. package/dist/navigation/history.d.ts +63 -0
  141. package/dist/navigation/history.d.ts.map +1 -0
  142. package/dist/navigation/history.js +144 -0
  143. package/dist/navigation/hooks/ancestors.hook.d.ts +13 -0
  144. package/dist/navigation/hooks/ancestors.hook.d.ts.map +1 -0
  145. package/dist/navigation/hooks/ancestors.hook.js +56 -0
  146. package/dist/navigation/hooks/location.hook.d.ts +4 -0
  147. package/dist/navigation/hooks/location.hook.d.ts.map +1 -0
  148. package/dist/navigation/hooks/location.hook.js +17 -0
  149. package/dist/navigation/hooks/navigate.hook.d.ts +12 -0
  150. package/dist/navigation/hooks/navigate.hook.d.ts.map +1 -0
  151. package/dist/navigation/hooks/navigate.hook.js +33 -0
  152. package/dist/navigation/hooks/scope.hook.d.ts +12 -0
  153. package/dist/navigation/hooks/scope.hook.d.ts.map +1 -0
  154. package/dist/navigation/hooks/scope.hook.js +21 -0
  155. package/dist/navigation/index.d.ts +16 -0
  156. package/dist/navigation/index.d.ts.map +1 -0
  157. package/dist/navigation/index.js +10 -0
  158. package/dist/navigation/link.d.ts +11 -0
  159. package/dist/navigation/link.d.ts.map +1 -0
  160. package/dist/navigation/link.js +62 -0
  161. package/dist/navigation/location.d.ts +48 -0
  162. package/dist/navigation/location.d.ts.map +1 -0
  163. package/dist/navigation/location.js +88 -0
  164. package/dist/navigation/scope.context.d.ts +64 -0
  165. package/dist/navigation/scope.context.d.ts.map +1 -0
  166. package/dist/navigation/scope.context.js +178 -0
  167. package/dist/navigation/snapshot.d.ts +22 -0
  168. package/dist/navigation/snapshot.d.ts.map +1 -0
  169. package/dist/navigation/snapshot.js +65 -0
  170. package/dist/schema/bind-read.d.ts +37 -0
  171. package/dist/schema/bind-read.d.ts.map +1 -0
  172. package/dist/schema/bind-read.js +205 -0
  173. package/dist/schema/binding.d.ts +21 -0
  174. package/dist/schema/binding.d.ts.map +1 -0
  175. package/dist/schema/binding.js +55 -0
  176. package/dist/schema/domain.hook.d.ts +10 -0
  177. package/dist/schema/domain.hook.d.ts.map +1 -0
  178. package/dist/schema/domain.hook.js +7 -0
  179. package/dist/schema/function.hook.d.ts +9 -0
  180. package/dist/schema/function.hook.d.ts.map +1 -0
  181. package/dist/schema/function.hook.js +40 -0
  182. package/dist/schema/index.d.ts +10 -0
  183. package/dist/schema/index.d.ts.map +1 -0
  184. package/dist/schema/index.js +8 -0
  185. package/dist/schema/kernel.hook.d.ts +10 -0
  186. package/dist/schema/kernel.hook.d.ts.map +1 -0
  187. package/dist/schema/kernel.hook.js +8 -0
  188. package/dist/schema/method.hook.d.ts +14 -0
  189. package/dist/schema/method.hook.d.ts.map +1 -0
  190. package/dist/schema/method.hook.js +49 -0
  191. package/dist/schema/tag.d.ts +11 -0
  192. package/dist/schema/tag.d.ts.map +1 -0
  193. package/dist/schema/tag.js +11 -0
  194. package/dist/schema/types.d.ts +73 -0
  195. package/dist/schema/types.d.ts.map +1 -0
  196. package/dist/schema/types.js +1 -0
  197. package/dist/schema/write.d.ts +15 -0
  198. package/dist/schema/write.d.ts.map +1 -0
  199. package/dist/schema/write.js +45 -0
  200. package/dist/selection/index.d.ts +4 -0
  201. package/dist/selection/index.d.ts.map +1 -0
  202. package/dist/selection/index.js +4 -0
  203. package/dist/selection/live.d.ts +21 -0
  204. package/dist/selection/live.d.ts.map +1 -0
  205. package/dist/selection/live.js +116 -0
  206. package/dist/selection/scope.context.d.ts +26 -0
  207. package/dist/selection/scope.context.d.ts.map +1 -0
  208. package/dist/selection/scope.context.js +100 -0
  209. package/dist/selection/selection.hook.d.ts +4 -0
  210. package/dist/selection/selection.hook.d.ts.map +1 -0
  211. package/dist/selection/selection.hook.js +14 -0
  212. package/dist/session/boot/boot.d.ts +19 -0
  213. package/dist/session/boot/boot.d.ts.map +1 -0
  214. package/dist/session/boot/boot.js +45 -0
  215. package/dist/session/boot/ownership.d.ts +18 -0
  216. package/dist/session/boot/ownership.d.ts.map +1 -0
  217. package/dist/session/boot/ownership.js +39 -0
  218. package/dist/session/boot/status.d.ts +24 -0
  219. package/dist/session/boot/status.d.ts.map +1 -0
  220. package/dist/session/boot/status.js +11 -0
  221. package/dist/session/connection/connection.hook.d.ts +3 -0
  222. package/dist/session/connection/connection.hook.d.ts.map +1 -0
  223. package/dist/session/connection/connection.hook.js +12 -0
  224. package/dist/session/connection/connection.store.d.ts +24 -0
  225. package/dist/session/connection/connection.store.d.ts.map +1 -0
  226. package/dist/session/connection/connection.store.js +31 -0
  227. package/dist/session/index.d.ts +8 -0
  228. package/dist/session/index.d.ts.map +1 -0
  229. package/dist/session/index.js +6 -0
  230. package/dist/session/kernel-client.hook.d.ts +9 -0
  231. package/dist/session/kernel-client.hook.d.ts.map +1 -0
  232. package/dist/session/kernel-client.hook.js +5 -0
  233. package/dist/session/kernel-instance.hook.d.ts +9 -0
  234. package/dist/session/kernel-instance.hook.d.ts.map +1 -0
  235. package/dist/session/kernel-instance.hook.js +10 -0
  236. package/dist/session/provider.d.ts +31 -0
  237. package/dist/session/provider.d.ts.map +1 -0
  238. package/dist/session/provider.js +165 -0
  239. package/dist/session/session.context.d.ts +18 -0
  240. package/dist/session/session.context.d.ts.map +1 -0
  241. package/dist/session/session.context.js +8 -0
  242. package/dist/session/shell.hook.d.ts +4 -0
  243. package/dist/session/shell.hook.d.ts.map +1 -0
  244. package/dist/session/shell.hook.js +15 -0
  245. package/dist/window/adopt.d.ts +27 -0
  246. package/dist/window/adopt.d.ts.map +1 -0
  247. package/dist/window/adopt.js +27 -0
  248. package/dist/window/hooks/close-guard.hook.d.ts +6 -0
  249. package/dist/window/hooks/close-guard.hook.d.ts.map +1 -0
  250. package/dist/window/hooks/close-guard.hook.js +90 -0
  251. package/dist/window/hooks/target-node.hook.d.ts +7 -0
  252. package/dist/window/hooks/target-node.hook.d.ts.map +1 -0
  253. package/dist/window/hooks/target-node.hook.js +77 -0
  254. package/dist/window/hooks/views-for.hook.d.ts +8 -0
  255. package/dist/window/hooks/views-for.hook.d.ts.map +1 -0
  256. package/dist/window/hooks/views-for.hook.js +51 -0
  257. package/dist/window/hooks/window.hook.d.ts +4 -0
  258. package/dist/window/hooks/window.hook.d.ts.map +1 -0
  259. package/dist/window/hooks/window.hook.js +14 -0
  260. package/dist/window/hooks/windows.hook.d.ts +15 -0
  261. package/dist/window/hooks/windows.hook.d.ts.map +1 -0
  262. package/dist/window/hooks/windows.hook.js +25 -0
  263. package/dist/window/host.d.ts +31 -0
  264. package/dist/window/host.d.ts.map +1 -0
  265. package/dist/window/host.js +270 -0
  266. package/dist/window/hub.d.ts +41 -0
  267. package/dist/window/hub.d.ts.map +1 -0
  268. package/dist/window/hub.js +100 -0
  269. package/dist/window/index.d.ts +15 -0
  270. package/dist/window/index.d.ts.map +1 -0
  271. package/dist/window/index.js +10 -0
  272. package/dist/window/lifecycle/mount.d.ts +47 -0
  273. package/dist/window/lifecycle/mount.d.ts.map +1 -0
  274. package/dist/window/lifecycle/mount.js +92 -0
  275. package/dist/window/lifecycle/states.d.ts +33 -0
  276. package/dist/window/lifecycle/states.d.ts.map +1 -0
  277. package/dist/window/lifecycle/states.js +7 -0
  278. package/dist/window/placement.d.ts +33 -0
  279. package/dist/window/placement.d.ts.map +1 -0
  280. package/dist/window/placement.js +57 -0
  281. package/dist/window/stage.d.ts +3 -0
  282. package/dist/window/stage.d.ts.map +1 -0
  283. package/dist/window/stage.js +41 -0
  284. package/package.json +59 -0
  285. package/src/application/apps/app-host.tsx +113 -0
  286. package/src/application/apps/app-logo.tsx +56 -0
  287. package/src/application/apps/apps.hook.ts +81 -0
  288. package/src/application/apps/open-app.hook.ts +18 -0
  289. package/src/application/index.ts +22 -0
  290. package/src/application/roles.hook.ts +41 -0
  291. package/src/application/spaces/current-space.hook.ts +43 -0
  292. package/src/application/spaces/current-space.store.ts +110 -0
  293. package/src/application/spaces/space.hook.ts +41 -0
  294. package/src/application/spaces/spaces.hook.ts +113 -0
  295. package/src/application/user/user.hook.ts +27 -0
  296. package/src/application/user/user.store.ts +85 -0
  297. package/src/auth/act-as.context.tsx +124 -0
  298. package/src/auth/auth.hook.ts +36 -0
  299. package/src/auth/can.hook.ts +149 -0
  300. package/src/auth/index.ts +6 -0
  301. package/src/auth/self.hook.ts +14 -0
  302. package/src/auth/session-kernel.ts +24 -0
  303. package/src/graph/action.ts +127 -0
  304. package/src/graph/async.ts +87 -0
  305. package/src/graph/call/call.hook.ts +42 -0
  306. package/src/graph/call/stream.hook.ts +200 -0
  307. package/src/graph/graph.context.ts +14 -0
  308. package/src/graph/guards.error.ts +58 -0
  309. package/src/graph/index.ts +20 -0
  310. package/src/graph/memory/base.ts +50 -0
  311. package/src/graph/memory/read/envelope.ts +98 -0
  312. package/src/graph/memory/read/idle.ts +24 -0
  313. package/src/graph/memory/read/read.ts +103 -0
  314. package/src/graph/memory/scope.ts +29 -0
  315. package/src/graph/mutate/move.hook.ts +70 -0
  316. package/src/graph/mutate/mutate.hook.ts +69 -0
  317. package/src/graph/query/children.hook.ts +136 -0
  318. package/src/graph/query/graph.hook.ts +13 -0
  319. package/src/graph/query/neighbors.hook.ts +124 -0
  320. package/src/graph/query/node.hook.ts +83 -0
  321. package/src/graph/query/query.hook.ts +42 -0
  322. package/src/index.ts +202 -0
  323. package/src/intents/envelope.ts +52 -0
  324. package/src/intents/index.ts +6 -0
  325. package/src/intents/listen-intent.hook.ts +92 -0
  326. package/src/intents/reply.ts +24 -0
  327. package/src/intents/send-intent.hook.ts +111 -0
  328. package/src/navigation/adapters/adapter.ts +13 -0
  329. package/src/navigation/adapters/memory.adapter.ts +22 -0
  330. package/src/navigation/adapters/url.adapter.ts +61 -0
  331. package/src/navigation/history.ts +170 -0
  332. package/src/navigation/hooks/ancestors.hook.ts +71 -0
  333. package/src/navigation/hooks/location.hook.ts +21 -0
  334. package/src/navigation/hooks/navigate.hook.ts +43 -0
  335. package/src/navigation/hooks/scope.hook.ts +34 -0
  336. package/src/navigation/index.ts +17 -0
  337. package/src/navigation/link.tsx +110 -0
  338. package/src/navigation/location.ts +122 -0
  339. package/src/navigation/scope.context.tsx +267 -0
  340. package/src/navigation/snapshot.ts +79 -0
  341. package/src/schema/bind-read.ts +319 -0
  342. package/src/schema/binding.ts +91 -0
  343. package/src/schema/domain.hook.ts +16 -0
  344. package/src/schema/function.hook.ts +68 -0
  345. package/src/schema/index.ts +25 -0
  346. package/src/schema/kernel.hook.ts +17 -0
  347. package/src/schema/method.hook.ts +103 -0
  348. package/src/schema/tag.ts +19 -0
  349. package/src/schema/types.ts +150 -0
  350. package/src/schema/write.ts +80 -0
  351. package/src/selection/index.ts +5 -0
  352. package/src/selection/live.ts +142 -0
  353. package/src/selection/scope.context.tsx +151 -0
  354. package/src/selection/selection.hook.ts +18 -0
  355. package/src/session/boot/boot.ts +57 -0
  356. package/src/session/boot/ownership.ts +54 -0
  357. package/src/session/boot/status.ts +32 -0
  358. package/src/session/connection/connection.hook.ts +16 -0
  359. package/src/session/connection/connection.store.ts +58 -0
  360. package/src/session/index.ts +8 -0
  361. package/src/session/kernel-client.hook.ts +13 -0
  362. package/src/session/kernel-instance.hook.ts +22 -0
  363. package/src/session/provider.tsx +205 -0
  364. package/src/session/session.context.ts +27 -0
  365. package/src/session/shell.hook.ts +19 -0
  366. package/src/window/adopt.ts +57 -0
  367. package/src/window/hooks/close-guard.hook.ts +100 -0
  368. package/src/window/hooks/target-node.hook.ts +94 -0
  369. package/src/window/hooks/views-for.hook.ts +82 -0
  370. package/src/window/hooks/window.hook.ts +18 -0
  371. package/src/window/hooks/windows.hook.ts +40 -0
  372. package/src/window/host.tsx +347 -0
  373. package/src/window/hub.ts +156 -0
  374. package/src/window/index.ts +17 -0
  375. package/src/window/lifecycle/mount.ts +134 -0
  376. package/src/window/lifecycle/states.ts +23 -0
  377. package/src/window/placement.ts +84 -0
  378. package/src/window/stage.tsx +46 -0
@@ -0,0 +1,111 @@
1
+ import type { IntentMessage, IntentRegistry, Shell } from '@astrale-os/shell'
2
+
3
+ /**
4
+ * useSendIntent (API §11) — a stable typed sender. Each call attaches a
5
+ * `correlationId`, subscribes for the matching `intentReply` on the origin channel
6
+ * BEFORE sending, then resolves with the reply's result. The promise also resolves
7
+ * (`void`) if the target window closes or this hook unmounts — a pending reply
8
+ * never strands.
9
+ */
10
+ import { useCallback, useEffect, useRef } from 'react'
11
+
12
+ import type { IntentPayload, IntentReply } from './envelope'
13
+
14
+ import { useShell } from '../session/shell.hook'
15
+ import { INTENT_REPLY, NOOP, intentMessage } from './envelope'
16
+
17
+ /** The typed sender returned by {@link useSendIntent}. */
18
+ export interface SendIntent {
19
+ <K extends keyof IntentRegistry>(
20
+ name: K,
21
+ payload: IntentPayload<K>,
22
+ /** `to`: `'parent'` (reserved) or a child windowId. Default: the parent, if any. */
23
+ opts?: { to?: 'parent' | string },
24
+ ): Promise<IntentReply<K> | void>
25
+ }
26
+
27
+ type Target =
28
+ | { readonly kind: 'parent' }
29
+ | { readonly kind: 'child'; readonly windowId: string }
30
+ | { readonly kind: 'invalid'; readonly reason: string }
31
+
32
+ function resolveTarget(shell: Shell, to: string | undefined): Target {
33
+ if (to === undefined) {
34
+ return shell.parent !== null
35
+ ? { kind: 'parent' }
36
+ : { kind: 'invalid', reason: 'no target — pass opts.to (a child windowId) from a root shell' }
37
+ }
38
+ if (to === 'parent') {
39
+ return shell.parent !== null
40
+ ? { kind: 'parent' }
41
+ : { kind: 'invalid', reason: "opts.to 'parent' but this shell has no parent channel" }
42
+ }
43
+ return { kind: 'child', windowId: to }
44
+ }
45
+
46
+ export function useSendIntent(): SendIntent {
47
+ const shell = useShell()
48
+ // Unmount settlers for in-flight sends, so a pending reply resolves on teardown.
49
+ const pendingRef = useRef<Set<() => void>>(new Set())
50
+ useEffect(() => {
51
+ const pending = pendingRef.current
52
+ return () => {
53
+ for (const settle of pending) settle()
54
+ pending.clear()
55
+ }
56
+ }, [])
57
+
58
+ return useCallback(
59
+ <K extends keyof IntentRegistry>(
60
+ name: K,
61
+ payload: IntentPayload<K>,
62
+ opts?: { to?: 'parent' | string },
63
+ ): Promise<IntentReply<K> | void> => {
64
+ const target = resolveTarget(shell, opts?.to)
65
+ if (target.kind === 'invalid') {
66
+ return Promise.reject(new Error(`useSendIntent: ${target.reason}`))
67
+ }
68
+ const correlationId = crypto.randomUUID()
69
+ const message = intentMessage(name, payload, correlationId)
70
+ const pending = pendingRef.current
71
+
72
+ return new Promise<IntentReply<K> | void>((resolve) => {
73
+ let done = false
74
+ let offReply = NOOP
75
+ let offClose = NOOP
76
+ const onUnmount = (): void => settle(undefined)
77
+ function settle(value: IntentReply<K> | void): void {
78
+ if (done) return
79
+ done = true
80
+ offReply()
81
+ offClose()
82
+ pending.delete(onUnmount)
83
+ resolve(value)
84
+ }
85
+ const onReply = (m: IntentMessage): void => {
86
+ if (m.envelope.name !== INTENT_REPLY) return
87
+ const reply = m.envelope.payload as { correlationId: string; result: unknown }
88
+ if (reply.correlationId !== correlationId) return
89
+ settle(reply.result as IntentReply<K>)
90
+ }
91
+
92
+ pending.add(onUnmount)
93
+ if (target.kind === 'parent') {
94
+ const parent = shell.parent!
95
+ offReply = parent.on('intent', onReply)
96
+ parent.send(message)
97
+ } else {
98
+ const { windowId } = target
99
+ offReply = shell.children.on('intent', (wid, m) => {
100
+ if (wid === windowId) onReply(m)
101
+ })
102
+ offClose = shell.children.onClose((wid) => {
103
+ if (wid === windowId) settle(undefined)
104
+ })
105
+ shell.children.send(windowId, message)
106
+ }
107
+ })
108
+ },
109
+ [shell],
110
+ )
111
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The nav persistence port (API §8.1). The pluggable seam a `NavScope` mirrors its
3
+ * location through. `href` is the URL-form probe `<Link>` needs: present → an
4
+ * `<a href>` (cmd/ctrl-click falls through); absent → `role="link"`.
5
+ */
6
+ import type { SerializedLocation } from '../history'
7
+
8
+ export interface NavAdapter {
9
+ read(): SerializedLocation | null
10
+ write(loc: SerializedLocation, opts: { replace: boolean }): void
11
+ subscribe(onExternalChange: () => void): () => void
12
+ href?(loc: SerializedLocation): string
13
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * memoryAdapter (API §8.1) — the default scope backing: holds one location in a
3
+ * closure (per-pane state, no URL form → Link renders `role="link"`).
4
+ */
5
+ import type { SerializedLocation } from '../history'
6
+ import type { NavAdapter } from './adapter'
7
+
8
+ /** In-memory adapter — the default scope backing (per-pane state, no URL). */
9
+ export function memoryAdapter(initial?: SerializedLocation): NavAdapter {
10
+ let current: SerializedLocation | null = initial ?? null
11
+ const listeners = new Set<() => void>()
12
+ return {
13
+ read: () => current,
14
+ write: (loc) => {
15
+ current = loc
16
+ },
17
+ subscribe: (cb) => {
18
+ listeners.add(cb)
19
+ return () => listeners.delete(cb)
20
+ },
21
+ }
22
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * urlAdapter (API §8.1) — the History-API adapter: mirrors the location into the
3
+ * query string (URL form → `<a href>`), owning the nav query keys (`param`,
4
+ * `view`, the location's `params`).
5
+ */
6
+ import type { SerializedLocation } from '../history'
7
+ import type { NavAdapter } from './adapter'
8
+
9
+ const VIEW_KEY = 'view'
10
+
11
+ /** History-API adapter — the location lives in the query string. */
12
+ export function urlAdapter(opts?: { param?: string }): NavAdapter {
13
+ const param = opts?.param ?? 'node'
14
+ const hasWindow = (): boolean => typeof window !== 'undefined'
15
+
16
+ const build = (loc: SerializedLocation): string => {
17
+ const url = new URL(window.location.href)
18
+ const sp = new URLSearchParams()
19
+ sp.set(param, loc.node)
20
+ if (loc.view !== undefined) sp.set(VIEW_KEY, loc.view)
21
+ for (const [k, v] of Object.entries(loc.params ?? {})) {
22
+ if (k !== param && k !== VIEW_KEY) sp.set(k, v)
23
+ }
24
+ url.search = sp.toString()
25
+ return `${url.pathname}${url.search}${url.hash}`
26
+ }
27
+
28
+ return {
29
+ read() {
30
+ if (!hasWindow()) return null
31
+ const sp = new URL(window.location.href).searchParams
32
+ const node = sp.get(param)
33
+ if (node === null) return null
34
+ const view = sp.get(VIEW_KEY) ?? undefined
35
+ const params: Record<string, string> = {}
36
+ for (const [k, v] of sp) {
37
+ if (k !== param && k !== VIEW_KEY) params[k] = v
38
+ }
39
+ return {
40
+ node,
41
+ ...(view !== undefined ? { view } : {}),
42
+ ...(Object.keys(params).length > 0 ? { params } : {}),
43
+ }
44
+ },
45
+ write(loc, { replace }) {
46
+ if (!hasWindow()) return
47
+ const href = build(loc)
48
+ if (replace) window.history.replaceState(null, '', href)
49
+ else window.history.pushState(null, '', href)
50
+ },
51
+ subscribe(cb) {
52
+ if (!hasWindow()) return () => undefined
53
+ const handler = (): void => cb()
54
+ window.addEventListener('popstate', handler)
55
+ return () => window.removeEventListener('popstate', handler)
56
+ },
57
+ href(loc) {
58
+ return hasWindow() ? build(loc) : loc.node
59
+ },
60
+ }
61
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * The per-scope history model (API §8.2) — a framework-free entries+cursor stack
3
+ * with a version counter for `useSyncExternalStore`, its serialized entry shape
4
+ * (`SerializedLocation`/`NavSnapshot` + equality/clone), and the seed precedence.
5
+ * Source of truth for back/forward; the adapter is a projection sink around it.
6
+ */
7
+
8
+ /** The serializable projection persisted in history / URLs / snapshots. */
9
+ export interface SerializedLocation {
10
+ node: string
11
+ view?: string
12
+ params?: Record<string, string>
13
+ }
14
+
15
+ /** A versioned history snapshot — round-trips through `NavScope.restore`. */
16
+ export interface NavSnapshot {
17
+ v: 1
18
+ entries: SerializedLocation[]
19
+ cursor: number
20
+ }
21
+
22
+ /** Structural equality of two serialized locations (node + view + params). */
23
+ export function sameLocation(a: SerializedLocation, b: SerializedLocation): boolean {
24
+ return a.node === b.node && a.view === b.view && sameParams(a.params, b.params)
25
+ }
26
+
27
+ function sameParams(a?: Record<string, string>, b?: Record<string, string>): boolean {
28
+ if (a === b) return true
29
+ const ak = a ? Object.keys(a) : []
30
+ const bk = b ? Object.keys(b) : []
31
+ if (ak.length !== bk.length) return false
32
+ return ak.every((k) => a?.[k] === b?.[k])
33
+ }
34
+
35
+ /** A deep-enough clone for a serialized entry (params copied). */
36
+ export function cloneLocation(loc: SerializedLocation): SerializedLocation {
37
+ return {
38
+ node: loc.node,
39
+ ...(loc.view !== undefined ? { view: loc.view } : {}),
40
+ ...(loc.params !== undefined ? { params: { ...loc.params } } : {}),
41
+ }
42
+ }
43
+
44
+ export class NavHistory {
45
+ private entries: SerializedLocation[]
46
+ private cursor: number
47
+ private version = 0
48
+ private readonly listeners = new Set<() => void>()
49
+
50
+ constructor(entries: SerializedLocation[], cursor: number) {
51
+ this.entries = entries
52
+ this.cursor = cursor
53
+ }
54
+
55
+ subscribe(listener: () => void): () => void {
56
+ this.listeners.add(listener)
57
+ return () => this.listeners.delete(listener)
58
+ }
59
+
60
+ getVersion(): number {
61
+ return this.version
62
+ }
63
+
64
+ get current(): SerializedLocation | null {
65
+ return this.entries[this.cursor] ?? null
66
+ }
67
+
68
+ get canBack(): boolean {
69
+ return this.cursor > 0
70
+ }
71
+
72
+ get canForward(): boolean {
73
+ return this.cursor >= 0 && this.cursor < this.entries.length - 1
74
+ }
75
+
76
+ snapshot(): NavSnapshot {
77
+ return { v: 1, entries: this.entries.map(cloneLocation), cursor: this.cursor }
78
+ }
79
+
80
+ /** Push (or replace the current entry with) a location. No-op on same location. */
81
+ push(loc: SerializedLocation, replace: boolean): boolean {
82
+ const cur = this.current
83
+ if (cur !== null && sameLocation(cur, loc)) return false
84
+ if (replace && cur !== null) {
85
+ this.entries = [...this.entries]
86
+ this.entries[this.cursor] = loc
87
+ } else {
88
+ this.entries = [...this.entries.slice(0, this.cursor + 1), loc]
89
+ this.cursor = this.entries.length - 1
90
+ }
91
+ this.emit()
92
+ return true
93
+ }
94
+
95
+ back(): boolean {
96
+ if (!this.canBack) return false
97
+ this.cursor--
98
+ this.emit()
99
+ return true
100
+ }
101
+
102
+ forward(): boolean {
103
+ if (!this.canForward) return false
104
+ this.cursor++
105
+ this.emit()
106
+ return true
107
+ }
108
+
109
+ /**
110
+ * Reconcile an externally-observed location (browser back/forward, URL edit):
111
+ * step the cursor when it matches an adjacent entry, else push it fresh.
112
+ */
113
+ syncExternal(loc: SerializedLocation): boolean {
114
+ const cur = this.current
115
+ if (cur !== null && sameLocation(cur, loc)) return false
116
+ const back = this.entries[this.cursor - 1]
117
+ if (back !== undefined && sameLocation(back, loc)) {
118
+ this.cursor--
119
+ this.emit()
120
+ return true
121
+ }
122
+ const fwd = this.entries[this.cursor + 1]
123
+ if (fwd !== undefined && sameLocation(fwd, loc)) {
124
+ this.cursor++
125
+ this.emit()
126
+ return true
127
+ }
128
+ return this.push(loc, false)
129
+ }
130
+
131
+ /** Replace the whole stack (restore revalidation prune). */
132
+ reset(entries: SerializedLocation[], cursor: number): void {
133
+ this.entries = entries
134
+ this.cursor = cursor
135
+ this.emit()
136
+ }
137
+
138
+ private emit(): void {
139
+ this.version++
140
+ for (const listener of this.listeners) listener()
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Seed the initial entries+cursor from the three sources, in precedence order:
146
+ * a `restore` snapshot (revalidated later against the memory), then the
147
+ * adapter's current read, then `initial`; empty (idle) when none yields one.
148
+ * A live adapter read that disagrees with a restored cursor is appended as the
149
+ * current entry (a deep link overrides the saved position but keeps its back
150
+ * history).
151
+ */
152
+ export function seedEntries(
153
+ fromAdapter: SerializedLocation | null,
154
+ restore: { entries: SerializedLocation[]; cursor: number } | undefined,
155
+ initialSerialized: SerializedLocation | null,
156
+ ): { entries: SerializedLocation[]; cursor: number } {
157
+ if (restore !== undefined && restore.entries.length > 0) {
158
+ const entries = restore.entries.map(cloneLocation)
159
+ let cursor = Math.min(Math.max(restore.cursor, 0), entries.length - 1)
160
+ if (fromAdapter !== null && !sameLocation(fromAdapter, entries[cursor])) {
161
+ entries.splice(cursor + 1)
162
+ entries.push(cloneLocation(fromAdapter))
163
+ cursor = entries.length - 1
164
+ }
165
+ return { entries, cursor }
166
+ }
167
+ if (fromAdapter !== null) return { entries: [cloneLocation(fromAdapter)], cursor: 0 }
168
+ if (initialSerialized !== null) return { entries: [initialSerialized], cursor: 0 }
169
+ return { entries: [], cursor: -1 }
170
+ }
@@ -0,0 +1,71 @@
1
+ import type { PathLike } from '@astrale-os/kernel-client/graph'
2
+ import type { LiveResult, ShapeInput } from '@astrale-os/kernel-client/store'
3
+
4
+ /**
5
+ * useAncestors (API §8.2) — the live, inclusive root-first ancestor chain of a
6
+ * node (a move re-renders it). Defaults to the nearest scope's location node;
7
+ * passing a node/path makes it usable outside a scope. Drops any masked ancestor.
8
+ */
9
+ import { useMemo } from 'react'
10
+
11
+ import type { ReadState } from '../../graph/memory/read/envelope'
12
+ import type { Node } from '../location'
13
+
14
+ import { idleReadState } from '../../graph/memory/read/idle'
15
+ import { useMemory } from '../../graph/memory/scope'
16
+ import { chainPaths, pathHint, pathOf } from '../location'
17
+ import { useHistoryVersion, useOptionalNavCtx, useMemoryShape } from '../scope.context'
18
+ import { serializeTarget } from '../snapshot'
19
+
20
+ /**
21
+ * The root-first ancestor chain (inclusive of the node itself), live: a move
22
+ * re-renders it. `node` defaults to the nearest scope's location node; passing a
23
+ * node/path makes it usable outside a scope. The chain drops any ancestor whose
24
+ * record is masked/absent.
25
+ */
26
+ export function useAncestors(
27
+ node?: Node | PathLike,
28
+ ): ReadState & { readonly chain: readonly Node[] } {
29
+ const memory = useMemory()
30
+ const ctx = useOptionalNavCtx()
31
+ useHistoryVersion(ctx?.history ?? null)
32
+
33
+ const baseRef =
34
+ node !== undefined ? serializeTarget(node).node : (ctx?.history.current?.node ?? null)
35
+ const baseResult = useMemoryShape(memory, baseRef)
36
+ const basePath = baseResult?.node ? pathOf(baseResult.node) : pathHint(baseRef ?? '')
37
+
38
+ const paths = useMemo(() => (basePath !== '' ? chainPaths(basePath) : EMPTY_PATHS), [basePath])
39
+ const shape = useMemo<ShapeInput | null>(
40
+ () => (basePath !== '' ? (q) => q.from(...chainPaths(basePath)) : null),
41
+ [basePath],
42
+ )
43
+ const chainResult = useMemoryShape(memory, shape)
44
+ const chain = useMemo<readonly Node[]>(() => {
45
+ if (chainResult === null) return EMPTY_CHAIN
46
+ const byPath = new Map<string, Node>()
47
+ for (const n of chainResult.roots) byPath.set(pathOf(n), n)
48
+ const out: Node[] = []
49
+ for (const p of paths) {
50
+ const hit = byPath.get(p)
51
+ if (hit !== undefined) out.push(hit)
52
+ }
53
+ return out
54
+ }, [paths, chainResult])
55
+
56
+ return { ...readStateOf(chainResult), chain }
57
+ }
58
+
59
+ const EMPTY_PATHS: readonly string[] = Object.freeze([])
60
+ const EMPTY_CHAIN: readonly Node[] = Object.freeze([])
61
+
62
+ function readStateOf(result: LiveResult | null): ReadState {
63
+ if (result === null) return idleReadState()
64
+ return {
65
+ pending: result.pending,
66
+ error: result.error,
67
+ live: result.live,
68
+ seq: result.seq,
69
+ refetch: result.refetch,
70
+ }
71
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * useLocation (API §8.2) — the nearest scope's live, id-anchored location (live
3
+ * path/node, missing flag). Throws outside any {@link NavScope}.
4
+ */
5
+ import { useMemo } from 'react'
6
+
7
+ import type { NavLocation } from '../location'
8
+
9
+ import { buildLocation, useSettled } from '../location'
10
+ import { useHistoryVersion, useNavCtx, useMemoryShape } from '../scope.context'
11
+
12
+ /** The nearest scope's live location. Throws outside any {@link NavScope}. */
13
+ export function useLocation(): NavLocation {
14
+ const ctx = useNavCtx()
15
+ useHistoryVersion(ctx.history)
16
+ const entry = ctx.history.current
17
+ const shape = entry?.node ?? null
18
+ const result = useMemoryShape(ctx.memory, shape)
19
+ const settled = useSettled(result, shape)
20
+ return useMemo(() => buildLocation(entry, result, settled), [entry, result, settled])
21
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * useNavigate (API §8.2) — the nearest scope's imperative navigation surface
3
+ * (back/forward/up + live canBack/canForward, wrapped in transitions). A
4
+ * per-consumer handle that delegates to the scope's ONE stable callable.
5
+ */
6
+ import { useMemo } from 'react'
7
+
8
+ import type { MutableNavigate, NavCtx, Navigate } from '../scope.context'
9
+
10
+ import { useHistoryVersion, useNavCtx } from '../scope.context'
11
+
12
+ /** The nearest scope's imperative navigation surface. Throws outside a scope. */
13
+ export function useNavigate(): Navigate {
14
+ return useNavigateHandle(useNavCtx())
15
+ }
16
+
17
+ /**
18
+ * A per-consumer navigate handle with fresh `canBack`/`canForward`, delegating to
19
+ * the scope's ONE stable callable. Render-pure: it never mutates the shared
20
+ * callable during render (a concurrent render could otherwise read flags from a
21
+ * newer history version than its own snapshot). The wrapper's identity changes
22
+ * only when navigation availability changes.
23
+ */
24
+ export function useNavigateHandle(ctx: NavCtx): Navigate {
25
+ useHistoryVersion(ctx.history)
26
+ const canBack = ctx.history.canBack
27
+ const canForward = ctx.history.canForward
28
+ return useMemo(
29
+ () => wrapNavigate(ctx.navigate, canBack, canForward),
30
+ [ctx.navigate, canBack, canForward],
31
+ )
32
+ }
33
+
34
+ function wrapNavigate(base: MutableNavigate, canBack: boolean, canForward: boolean): Navigate {
35
+ const fn = ((to: Parameters<Navigate>[0], opts?: Parameters<Navigate>[1]) =>
36
+ base(to, opts)) as MutableNavigate
37
+ fn.back = base.back
38
+ fn.forward = base.forward
39
+ fn.up = base.up
40
+ fn.canBack = canBack
41
+ fn.canForward = canForward
42
+ return fn
43
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * useNavScope (API §8.2) — the nearest scope, for chrome/persistence: the live
3
+ * location, a `snapshot()` handle, and the imperative navigate. Throws outside a
4
+ * scope.
5
+ */
6
+ import { useCallback, useMemo } from 'react'
7
+
8
+ import type { NavSnapshot } from '../history'
9
+ import type { NavLocation } from '../location'
10
+ import type { Navigate } from '../scope.context'
11
+
12
+ import { buildLocation, useSettled } from '../location'
13
+ import { useNavCtx, useMemoryShape } from '../scope.context'
14
+ import { useNavigateHandle } from './navigate.hook'
15
+
16
+ /** The nearest scope, for chrome/persistence (API §8.2). */
17
+ export interface NavScopeHandle {
18
+ readonly location: NavLocation
19
+ snapshot(): NavSnapshot
20
+ navigate: Navigate
21
+ }
22
+
23
+ /** The nearest scope, for chrome/persistence (API §8.2). Throws outside a scope. */
24
+ export function useNavScope(): NavScopeHandle {
25
+ const ctx = useNavCtx()
26
+ const navigate = useNavigateHandle(ctx)
27
+ const entry = ctx.history.current
28
+ const shape = entry?.node ?? null
29
+ const result = useMemoryShape(ctx.memory, shape)
30
+ const settled = useSettled(result, shape)
31
+ const location = useMemo(() => buildLocation(entry, result, settled), [entry, result, settled])
32
+ const snapshot = useCallback(() => ctx.history.snapshot(), [ctx.history])
33
+ return { location, snapshot, navigate }
34
+ }
@@ -0,0 +1,17 @@
1
+ // navigation — API §8 (NavScope, useLocation, useNavigate, Link, useAncestors,
2
+ // useNavScope, adapters). See SHELL_REACT_API.md.
3
+ export { NavScope } from './scope.context'
4
+ export type { MissingPolicy, NavScopeProps, Navigate } from './scope.context'
5
+ export { useLocation } from './hooks/location.hook'
6
+ export { useNavigate } from './hooks/navigate.hook'
7
+ export { useAncestors } from './hooks/ancestors.hook'
8
+ export { useNavScope } from './hooks/scope.hook'
9
+ export type { NavScopeHandle } from './hooks/scope.hook'
10
+ export { Link } from './link'
11
+ export type { LinkProps } from './link'
12
+ export { memoryAdapter } from './adapters/memory.adapter'
13
+ export { urlAdapter } from './adapters/url.adapter'
14
+ export type { NavAdapter } from './adapters/adapter'
15
+ export type { NavLocation } from './location'
16
+ export type { NavSnapshot, SerializedLocation } from './history'
17
+ export type { NavTarget } from './snapshot'
@@ -0,0 +1,110 @@
1
+ import type {
2
+ AnchorHTMLAttributes,
3
+ FocusEvent,
4
+ KeyboardEvent,
5
+ MouseEvent,
6
+ PointerEvent,
7
+ ReactElement,
8
+ ReactNode,
9
+ } from 'react'
10
+
11
+ /**
12
+ * Link (API §8.2) — navigates the nearest scope. Renders `<a href>` when the
13
+ * scope's adapter exposes a URL form (cmd/ctrl/middle-click then falls through
14
+ * to the browser), else an href-less `<a role="link">` with identical behavior.
15
+ * Default prefetch `'intent'` warms the target shape on hover/pointer-down/focus.
16
+ */
17
+ import { useCallback, useEffect } from 'react'
18
+
19
+ import type { NavTarget } from './snapshot'
20
+
21
+ import { useNavCtx } from './scope.context'
22
+ import { serializeTarget } from './snapshot'
23
+
24
+ export interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
25
+ to: NavTarget
26
+ replace?: boolean
27
+ /** Warm the target shape ahead of navigation. Default `'intent'`. */
28
+ prefetch?: 'intent' | 'render' | false
29
+ children: ReactNode
30
+ }
31
+
32
+ export function Link(props: LinkProps): ReactElement {
33
+ const {
34
+ to,
35
+ replace,
36
+ prefetch = 'intent',
37
+ children,
38
+ onClick,
39
+ onPointerEnter,
40
+ onPointerDown,
41
+ onFocus,
42
+ onKeyDown,
43
+ ...rest
44
+ } = props
45
+ const ctx = useNavCtx()
46
+ const target = serializeTarget(to)
47
+ const href = ctx.adapter.href?.(target)
48
+ const nodeRef = target.node
49
+
50
+ const preload = useCallback(() => {
51
+ void ctx.memory?.preload(nodeRef)
52
+ }, [ctx.memory, nodeRef])
53
+
54
+ useEffect(() => {
55
+ if (prefetch === 'render') preload()
56
+ }, [prefetch, preload])
57
+
58
+ const intent = useCallback(() => {
59
+ if (prefetch === 'intent') preload()
60
+ }, [prefetch, preload])
61
+
62
+ const go = useCallback(() => ctx.navigate(to, { replace }), [ctx, to, replace])
63
+
64
+ const handleClick = (e: MouseEvent<HTMLAnchorElement>): void => {
65
+ onClick?.(e)
66
+ if (e.defaultPrevented) return
67
+ const modified = e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0
68
+ if (href !== undefined && modified) return // let the browser open it (new tab / window)
69
+ e.preventDefault()
70
+ go()
71
+ }
72
+
73
+ const handleKeyDown = (e: KeyboardEvent<HTMLAnchorElement>): void => {
74
+ onKeyDown?.(e)
75
+ if (e.defaultPrevented || href !== undefined) return // a real anchor handles Enter itself
76
+ if (e.key === 'Enter' || e.key === ' ') {
77
+ e.preventDefault()
78
+ go()
79
+ }
80
+ }
81
+
82
+ const handlePointerEnter = (e: PointerEvent<HTMLAnchorElement>): void => {
83
+ onPointerEnter?.(e)
84
+ intent()
85
+ }
86
+ const handlePointerDown = (e: PointerEvent<HTMLAnchorElement>): void => {
87
+ onPointerDown?.(e)
88
+ intent()
89
+ }
90
+ const handleFocus = (e: FocusEvent<HTMLAnchorElement>): void => {
91
+ onFocus?.(e)
92
+ intent()
93
+ }
94
+
95
+ const linkRole = href !== undefined ? { href } : { role: 'link', tabIndex: rest.tabIndex ?? 0 }
96
+
97
+ return (
98
+ <a
99
+ {...rest}
100
+ {...linkRole}
101
+ onClick={handleClick}
102
+ onKeyDown={handleKeyDown}
103
+ onPointerEnter={handlePointerEnter}
104
+ onPointerDown={handlePointerDown}
105
+ onFocus={handleFocus}
106
+ >
107
+ {children}
108
+ </a>
109
+ )
110
+ }