akanjs 3.0.0-alpha.2 → 3.0.0-alpha.21

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 (388) hide show
  1. package/README.ko.md +1 -1
  2. package/README.md +1 -1
  3. package/base/symbols.ts +4 -0
  4. package/client/clientRuntime.ts +8 -0
  5. package/client/cn.ts +7 -0
  6. package/client/csrTypes.ts +2 -0
  7. package/client/frameDebug.ts +3 -2
  8. package/common/Logger.ts +4 -0
  9. package/common/deepObjectify.ts +21 -7
  10. package/common/index.ts +14 -0
  11. package/common/mcpExposure.ts +99 -0
  12. package/common/pathSet.ts +17 -5
  13. package/common/routeConvention.ts +30 -0
  14. package/constant/crystalize.ts +3 -1
  15. package/constant/fieldInfo.ts +6 -0
  16. package/constant/getDefault.ts +37 -9
  17. package/constant/immerify.ts +1 -1
  18. package/constant/index.ts +2 -0
  19. package/constant/labelOf.ts +17 -0
  20. package/constant/mask.ts +60 -0
  21. package/constant/purify.ts +17 -11
  22. package/constant/types.ts +14 -12
  23. package/dictionary/DictionaryLookup.ts +30 -0
  24. package/dictionary/agent.dictionary.ts +31 -0
  25. package/dictionary/agentTurn.dictionary.ts +29 -0
  26. package/dictionary/base.dictionary.ts +8 -0
  27. package/dictionary/dictInfo.ts +8 -0
  28. package/dictionary/dictionary.ts +9 -3
  29. package/dictionary/index.ts +1 -0
  30. package/document/database.ts +21 -0
  31. package/document/filterMeta.ts +7 -0
  32. package/document/index.ts +1 -0
  33. package/document/into.ts +7 -1
  34. package/document/noDocumentError.ts +12 -0
  35. package/fetch/agentTurn.ts +19 -0
  36. package/fetch/client/fetchClient.ts +71 -13
  37. package/fetch/client/wsClient.ts +17 -2
  38. package/fetch/fetchType/endpointFetch.type.ts +10 -4
  39. package/fetch/index.ts +1 -0
  40. package/local/apps/serverLifecycle/serverLifecycle-local.db +0 -0
  41. package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
  42. package/local/apps/serverLifecycle/serverLifecycle-local.db-wal +0 -0
  43. package/local/apps/serverLifecycle/serverLifecycle-local_solid.db +0 -0
  44. package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
  45. package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-wal +0 -0
  46. package/package.json +11 -9
  47. package/server/akanApp.ts +30 -34
  48. package/server/akanOption.ts +65 -7
  49. package/server/akanServer.ts +172 -7
  50. package/server/animatedImage.ts +83 -0
  51. package/server/artifact/routeClientCache.ts +14 -0
  52. package/server/assetEncoding.ts +47 -0
  53. package/server/devtools/signalSerializer.ts +2 -2
  54. package/server/devtools/types.ts +2 -2
  55. package/server/di/diLifecycle.ts +31 -3
  56. package/server/di/predefinedAdaptor.ts +6 -0
  57. package/server/imageOptimizer.ts +33 -46
  58. package/server/index.ts +1 -2
  59. package/server/mcp/McpAuth.ts +177 -0
  60. package/server/mcp/McpDispatcher.ts +245 -0
  61. package/server/mcp/McpEventStream.ts +76 -0
  62. package/server/mcp/McpExecutionContext.ts +105 -0
  63. package/server/mcp/McpRouter.ts +591 -0
  64. package/server/mcp/index.ts +5 -0
  65. package/server/resolver/CascadeRunner.ts +1 -1
  66. package/server/resolver/database.resolver.ts +42 -5
  67. package/server/resolver/service.resolver.ts +29 -0
  68. package/server/resolver/signal.resolver.ts +16 -1
  69. package/server/routeTreeBuilder.ts +2 -44
  70. package/server/rscWorker.tsx +6 -2
  71. package/server/subRouteIndexDocument.tsx +61 -0
  72. package/server/systemPageDocument.tsx +2 -2
  73. package/server/systemPages.tsx +49 -16
  74. package/server/vendor/akanjs-fetch.ts +1 -0
  75. package/server/webRouter.ts +159 -35
  76. package/service/agent.service.ts +14 -0
  77. package/service/base.service.ts +2 -1
  78. package/service/index.ts +1 -0
  79. package/service/predefinedAdaptor/database.adaptor.ts +78 -24
  80. package/service/predefinedAdaptor/deepseekLlm.ts +230 -0
  81. package/service/predefinedAdaptor/index.ts +3 -0
  82. package/service/predefinedAdaptor/insightQuery.ts +183 -0
  83. package/service/predefinedAdaptor/llm.adaptor.ts +76 -0
  84. package/service/predefinedAdaptor/role.adaptor.ts +2 -0
  85. package/service/predefinedAdaptor/schedule.adaptor.ts +12 -5
  86. package/service/serviceModule.ts +17 -0
  87. package/service/types.ts +4 -0
  88. package/signal/agent/AgentCatalogue.ts +165 -0
  89. package/signal/agent/index.ts +1 -0
  90. package/signal/agent.signal.ts +42 -0
  91. package/signal/agentTurn.ts +8 -0
  92. package/signal/agentTurnStream.ts +48 -0
  93. package/signal/base.signal.ts +2 -1
  94. package/signal/endpointInfo.ts +38 -2
  95. package/signal/guard.ts +13 -1
  96. package/signal/guards.ts +52 -1
  97. package/signal/index.ts +6 -0
  98. package/signal/mcp/McpDocument.ts +286 -0
  99. package/signal/mcp/McpProgress.ts +106 -0
  100. package/signal/mcp/McpUriTemplate.ts +85 -0
  101. package/signal/mcp/Msg.ts +368 -0
  102. package/signal/mcp/index.ts +5 -0
  103. package/signal/mcp/mcpProtocol.ts +120 -0
  104. package/signal/middleware.ts +7 -3
  105. package/signal/openapi/openapi.ts +32 -171
  106. package/signal/schema/JsonSchemaBuilder.ts +211 -0
  107. package/signal/schema/index.ts +1 -0
  108. package/signal/signalContext.ts +85 -9
  109. package/signal/slice.ts +9 -9
  110. package/signal/types.ts +1 -1
  111. package/store/action.ts +11 -22
  112. package/store/actionTag.ts +33 -0
  113. package/store/agent/AgentBridge.ts +323 -0
  114. package/store/agent/AgentContext.ts +74 -0
  115. package/store/agent/AgentPrompts.ts +82 -0
  116. package/store/agent/AgentVisibility.ts +68 -0
  117. package/store/agent/ScreenReader.ts +208 -0
  118. package/store/agent/StoreCatalogue.ts +351 -0
  119. package/store/agent/StoreSurfaceSource.ts +122 -0
  120. package/store/agent/index.ts +9 -0
  121. package/store/agent/storeSurface.ts +28 -0
  122. package/store/agent/types.ts +50 -0
  123. package/store/agentic/StToolBuilder.ts +199 -0
  124. package/store/agentic/attachAgentic.ts +22 -0
  125. package/store/agentic/index.ts +5 -0
  126. package/store/agentic/readableValue.ts +17 -0
  127. package/store/agentic/useStExpose.ts +18 -0
  128. package/store/agentic/useStState.ts +35 -0
  129. package/store/baseSt.ts +7 -0
  130. package/store/databaseStateNames.ts +31 -0
  131. package/store/formSetterNames.ts +21 -0
  132. package/store/index.ts +8 -0
  133. package/store/rootStore.ts +3 -1
  134. package/store/sliceRole.ts +36 -0
  135. package/store/state.ts +2 -12
  136. package/store/store.ts +19 -1
  137. package/store/storeInstance.ts +165 -20
  138. package/store/storeRegistry.ts +16 -1
  139. package/store/types.ts +18 -0
  140. package/store/withSelector.ts +7 -8
  141. package/test/registerDom.ts +4 -0
  142. package/types/base/symbols.d.ts +4 -0
  143. package/types/client/cn.d.ts +5 -0
  144. package/types/client/csrTypes.d.ts +2 -0
  145. package/types/common/Logger.d.ts +2 -0
  146. package/types/common/deepObjectify.d.ts +4 -2
  147. package/types/common/index.d.ts +2 -1
  148. package/types/common/mcpExposure.d.ts +60 -0
  149. package/types/common/routeConvention.d.ts +8 -0
  150. package/types/constant/fieldInfo.d.ts +1 -0
  151. package/types/constant/getDefault.d.ts +5 -0
  152. package/types/constant/index.d.ts +2 -0
  153. package/types/constant/labelOf.d.ts +6 -0
  154. package/types/constant/mask.d.ts +34 -0
  155. package/types/constant/purify.d.ts +2 -2
  156. package/types/constant/types.d.ts +2 -2
  157. package/types/dictionary/DictionaryLookup.d.ts +14 -0
  158. package/types/dictionary/agent.dictionary.d.ts +1 -0
  159. package/types/dictionary/agentTurn.dictionary.d.ts +1 -0
  160. package/types/dictionary/base.dictionary.d.ts +1 -1
  161. package/types/dictionary/dictInfo.d.ts +6 -0
  162. package/types/dictionary/dictionary.d.ts +11 -9
  163. package/types/dictionary/index.d.ts +1 -0
  164. package/types/document/database.d.ts +20 -0
  165. package/types/document/filterMeta.d.ts +1 -0
  166. package/types/document/index.d.ts +1 -0
  167. package/types/document/into.d.ts +6 -1
  168. package/types/document/noDocumentError.d.ts +12 -0
  169. package/types/fetch/agentTurn.d.ts +593 -0
  170. package/types/fetch/client/fetchClient.d.ts +10 -0
  171. package/types/fetch/fetchType/endpointFetch.type.d.ts +5 -3
  172. package/types/fetch/index.d.ts +1 -0
  173. package/types/server/akanOption.d.ts +35 -7
  174. package/types/server/akanServer.d.ts +44 -1
  175. package/types/server/animatedImage.d.ts +7 -0
  176. package/types/server/artifact/routeClientCache.d.ts +6 -0
  177. package/types/server/assetEncoding.d.ts +7 -0
  178. package/types/server/devtools/types.d.ts +2 -2
  179. package/types/server/di/predefinedAdaptor.d.ts +4 -1
  180. package/types/server/index.d.ts +0 -2
  181. package/types/server/mcp/McpAuth.d.ts +62 -0
  182. package/types/server/mcp/McpDispatcher.d.ts +54 -0
  183. package/types/server/mcp/McpEventStream.d.ts +18 -0
  184. package/types/server/mcp/McpExecutionContext.d.ts +44 -0
  185. package/types/server/mcp/McpRouter.d.ts +60 -0
  186. package/types/server/mcp/index.d.ts +5 -0
  187. package/types/server/mcp.d.ts +1 -0
  188. package/types/server/subRouteIndexDocument.d.ts +8 -0
  189. package/types/server/systemPageDocument.d.ts +1 -0
  190. package/types/server/systemPages.d.ts +5 -0
  191. package/types/server/vendor/akanjs-fetch.d.ts +1 -0
  192. package/types/service/agent.service.d.ts +12 -0
  193. package/types/service/base.service.d.ts +4 -0
  194. package/types/service/index.d.ts +1 -0
  195. package/types/service/predefinedAdaptor/database.adaptor.d.ts +24 -2
  196. package/types/service/predefinedAdaptor/deepseekLlm.d.ts +64 -0
  197. package/types/service/predefinedAdaptor/index.d.ts +3 -0
  198. package/types/service/predefinedAdaptor/insightQuery.d.ts +50 -0
  199. package/types/service/predefinedAdaptor/llm.adaptor.d.ts +68 -0
  200. package/types/service/predefinedAdaptor/role.adaptor.d.ts +2 -0
  201. package/types/service/predefinedAdaptor/schedule.adaptor.d.ts +10 -6
  202. package/types/service/types.d.ts +4 -1
  203. package/types/signal/agent/AgentCatalogue.d.ts +100 -0
  204. package/types/signal/agent/index.d.ts +1 -0
  205. package/types/signal/agent.d.ts +1 -0
  206. package/types/signal/agent.signal.d.ts +29 -0
  207. package/types/signal/agentTurn.d.ts +9 -0
  208. package/types/signal/agentTurnStream.d.ts +16 -0
  209. package/types/signal/base.signal.d.ts +1 -1
  210. package/types/signal/endpointInfo.d.ts +21 -3
  211. package/types/signal/guard.d.ts +11 -0
  212. package/types/signal/guards.d.ts +22 -1
  213. package/types/signal/index.d.ts +6 -0
  214. package/types/signal/mcp/McpDocument.d.ts +65 -0
  215. package/types/signal/mcp/McpProgress.d.ts +40 -0
  216. package/types/signal/mcp/McpUriTemplate.d.ts +26 -0
  217. package/types/signal/mcp/Msg.d.ts +143 -0
  218. package/types/signal/mcp/index.d.ts +5 -0
  219. package/types/signal/mcp/mcpProtocol.d.ts +107 -0
  220. package/types/signal/mcp.d.ts +1 -0
  221. package/types/signal/openapi/openapi.d.ts +3 -3
  222. package/types/signal/schema/JsonSchemaBuilder.d.ts +47 -0
  223. package/types/signal/schema/index.d.ts +1 -0
  224. package/types/signal/schema.d.ts +1 -0
  225. package/types/signal/signalContext.d.ts +16 -1
  226. package/types/signal/slice.d.ts +8 -8
  227. package/types/signal/types.d.ts +1 -1
  228. package/types/store/actionTag.d.ts +21 -0
  229. package/types/store/agent/AgentBridge.d.ts +77 -0
  230. package/types/store/agent/AgentContext.d.ts +16 -0
  231. package/types/store/agent/AgentPrompts.d.ts +31 -0
  232. package/types/store/agent/AgentVisibility.d.ts +21 -0
  233. package/types/store/agent/ScreenReader.d.ts +11 -0
  234. package/types/store/agent/StoreCatalogue.d.ts +26 -0
  235. package/types/store/agent/StoreSurfaceSource.d.ts +18 -0
  236. package/types/store/agent/index.d.ts +9 -0
  237. package/types/store/agent/storeSurface.d.ts +14 -0
  238. package/types/store/agent/types.d.ts +49 -0
  239. package/types/store/agent.d.ts +1 -0
  240. package/types/store/agentic/StToolBuilder.d.ts +41 -0
  241. package/types/store/agentic/attachAgentic.d.ts +14 -0
  242. package/types/store/agentic/index.d.ts +5 -0
  243. package/types/store/agentic/readableValue.d.ts +3 -0
  244. package/types/store/agentic/useStExpose.d.ts +8 -0
  245. package/types/store/agentic/useStState.d.ts +12 -0
  246. package/types/store/agentic.d.ts +1 -0
  247. package/types/store/baseSt.d.ts +29 -22
  248. package/types/store/databaseStateNames.d.ts +25 -0
  249. package/types/store/formSetterNames.d.ts +16 -0
  250. package/types/store/index.d.ts +7 -0
  251. package/types/store/rootStore.d.ts +5 -1
  252. package/types/store/sliceRole.d.ts +25 -0
  253. package/types/store/store.d.ts +7 -2
  254. package/types/store/storeInstance.d.ts +33 -1
  255. package/types/store/storeRegistry.d.ts +5 -0
  256. package/types/store/types.d.ts +15 -0
  257. package/types/store/withSelector.d.ts +7 -8
  258. package/types/test/registerDom.d.ts +1 -0
  259. package/types/ui/Agent/Approval.d.ts +7 -0
  260. package/types/ui/Agent/Bubble.d.ts +13 -0
  261. package/types/ui/Agent/Chat.d.ts +24 -0
  262. package/types/ui/Agent/Context.d.ts +9 -0
  263. package/types/ui/Agent/Dock.d.ts +16 -0
  264. package/types/ui/Agent/Guide.d.ts +9 -0
  265. package/types/ui/Agent/Section.d.ts +10 -0
  266. package/types/ui/Agent/StateKey.d.ts +16 -0
  267. package/types/ui/Agent/Tool.d.ts +15 -0
  268. package/types/ui/Agent/Transcript.d.ts +13 -0
  269. package/types/ui/Agent/Zone.d.ts +23 -0
  270. package/types/ui/Agent/fetchRunner.d.ts +12 -0
  271. package/types/ui/Agent/index.d.ts +17 -0
  272. package/types/ui/Agent/index_.d.ts +1 -0
  273. package/types/ui/Agent/sessionHistory.d.ts +12 -0
  274. package/types/ui/Agent.d.ts +1 -0
  275. package/types/ui/Badge.d.ts +1 -1
  276. package/types/ui/Button.d.ts +1 -1
  277. package/types/ui/CsrImage.d.ts +1 -1
  278. package/types/ui/Dropdown.d.ts +2 -0
  279. package/types/ui/Image.d.ts +3 -3
  280. package/types/ui/Layout/BottomInset.d.ts +2 -1
  281. package/types/ui/Layout/index.d.ts +1 -1
  282. package/types/ui/Signal/style.d.ts +4 -1
  283. package/types/ui/UiOverride/context.d.ts +14 -2
  284. package/types/ui/agentAttrs.d.ts +14 -0
  285. package/types/ui/index.d.ts +4 -1
  286. package/types/ui/overlayLayer.d.ts +24 -0
  287. package/types/ui/recipe/badgeRecipe.d.ts +7 -3
  288. package/types/ui/recipe/buttonRecipe.d.ts +7 -3
  289. package/types/ui/recipe/inputRecipe.d.ts +5 -1
  290. package/types/vendor/use-agentic/AgentProvider.d.ts +18 -0
  291. package/types/vendor/use-agentic/AgentScope.d.ts +9 -0
  292. package/types/vendor/use-agentic/AgentSession.d.ts +50 -0
  293. package/types/vendor/use-agentic/Agentic.d.ts +21 -0
  294. package/types/vendor/use-agentic/AgenticSurface.d.ts +44 -0
  295. package/types/vendor/use-agentic/httpRunner.d.ts +15 -0
  296. package/types/vendor/use-agentic/index.d.ts +13 -0
  297. package/types/vendor/use-agentic/surfaceContext.d.ts +5 -0
  298. package/types/vendor/use-agentic/types.d.ts +133 -0
  299. package/types/vendor/use-agentic/useAgent.d.ts +4 -0
  300. package/types/vendor/use-agentic/useAgentGuide.d.ts +6 -0
  301. package/types/vendor/use-agentic/useAgentResource.d.ts +7 -0
  302. package/types/vendor/use-agentic/useAgentState.d.ts +13 -0
  303. package/types/vendor/use-agentic/useAgentTool.d.ts +16 -0
  304. package/types/vendor/use-agentic.d.ts +1 -0
  305. package/types/webkit/index.d.ts +3 -0
  306. package/types/webkit/lazy.d.ts +12 -0
  307. package/types/webkit/useCsrValues.d.ts +3 -3
  308. package/types/webkit/useEscapeKey.d.ts +5 -0
  309. package/types/webkit/useFrameRuntime.d.ts +6 -1
  310. package/types/webkit/useScreenScope.d.ts +18 -0
  311. package/ui/Agent/Approval.tsx +29 -0
  312. package/ui/Agent/Bubble.tsx +74 -0
  313. package/ui/Agent/Chat.tsx +225 -0
  314. package/ui/Agent/Context.tsx +38 -0
  315. package/ui/Agent/Dock.tsx +72 -0
  316. package/ui/Agent/Guide.tsx +16 -0
  317. package/ui/Agent/Section.tsx +24 -0
  318. package/ui/Agent/StateKey.tsx +44 -0
  319. package/ui/Agent/Tool.tsx +66 -0
  320. package/ui/Agent/Transcript.tsx +33 -0
  321. package/ui/Agent/Zone.tsx +48 -0
  322. package/ui/Agent/fetchRunner.ts +30 -0
  323. package/ui/Agent/index.ts +12 -0
  324. package/ui/Agent/index_.tsx +4 -0
  325. package/ui/Agent/sessionHistory.ts +33 -0
  326. package/ui/Badge.tsx +3 -3
  327. package/ui/BottomSheet.tsx +5 -0
  328. package/ui/Button.tsx +6 -2
  329. package/ui/Constant/Doc.tsx +1 -1
  330. package/ui/CsrImage.tsx +1 -1
  331. package/ui/Data/ListContainer.tsx +1 -1
  332. package/ui/DatePicker.tsx +5 -4
  333. package/ui/Dialog/Modal.tsx +12 -15
  334. package/ui/DraggableList.tsx +3 -1
  335. package/ui/Dropdown.tsx +33 -11
  336. package/ui/Field.tsx +30 -16
  337. package/ui/Image.tsx +3 -3
  338. package/ui/Input.tsx +14 -5
  339. package/ui/Layout/BottomInset.tsx +6 -1
  340. package/ui/Load/Units.tsx +11 -2
  341. package/ui/Load/View.tsx +7 -2
  342. package/ui/Loading/ProgressBar.tsx +8 -1
  343. package/ui/Menu.tsx +7 -8
  344. package/ui/Model/EditModal.tsx +37 -2
  345. package/ui/Model/SureToRemove.tsx +2 -1
  346. package/ui/Model/index_.tsx +42 -15
  347. package/ui/ObjectId.tsx +3 -4
  348. package/ui/Pagination.tsx +3 -4
  349. package/ui/Popconfirm.tsx +8 -7
  350. package/ui/Select.tsx +6 -4
  351. package/ui/Signal/RestApi.tsx +43 -16
  352. package/ui/Signal/WebSocket.tsx +1 -1
  353. package/ui/Signal/style.ts +6 -1
  354. package/ui/Switch.tsx +2 -0
  355. package/ui/System/CSR.tsx +6 -2
  356. package/ui/System/ThemeToggle.tsx +55 -33
  357. package/ui/ToggleSelect.tsx +4 -3
  358. package/ui/Tooltip.tsx +2 -1
  359. package/ui/UiOverride/context.ts +14 -2
  360. package/ui/agentAttrs.ts +19 -0
  361. package/ui/index.ts +10 -1
  362. package/ui/overlayLayer.ts +39 -0
  363. package/ui/recipe/badgeRecipe.ts +22 -3
  364. package/ui/recipe/buttonRecipe.ts +51 -2
  365. package/ui/recipe/factory.ts +2 -2
  366. package/ui/recipe/inputRecipe.ts +18 -4
  367. package/vendor/use-agentic/AgentProvider.tsx +39 -0
  368. package/vendor/use-agentic/AgentScope.tsx +20 -0
  369. package/vendor/use-agentic/AgentSession.ts +292 -0
  370. package/vendor/use-agentic/Agentic.tsx +44 -0
  371. package/vendor/use-agentic/AgenticSurface.ts +294 -0
  372. package/vendor/use-agentic/WIRE.md +57 -0
  373. package/vendor/use-agentic/httpRunner.ts +115 -0
  374. package/vendor/use-agentic/index.ts +14 -0
  375. package/vendor/use-agentic/surfaceContext.ts +9 -0
  376. package/vendor/use-agentic/types.ts +144 -0
  377. package/vendor/use-agentic/useAgent.ts +17 -0
  378. package/vendor/use-agentic/useAgentGuide.ts +16 -0
  379. package/vendor/use-agentic/useAgentResource.ts +30 -0
  380. package/vendor/use-agentic/useAgentState.ts +61 -0
  381. package/vendor/use-agentic/useAgentTool.ts +55 -0
  382. package/webkit/bootCsr.tsx +2 -33
  383. package/webkit/index.ts +3 -0
  384. package/webkit/lazy.tsx +27 -3
  385. package/webkit/useCsrValues.ts +121 -4
  386. package/webkit/useEscapeKey.tsx +42 -0
  387. package/webkit/useFrameRuntime.ts +65 -32
  388. package/webkit/useScreenScope.tsx +49 -0
@@ -0,0 +1,30 @@
1
+ "use client";
2
+ import { useEffect, useRef } from "react";
3
+ import { useScopePath, useSurface } from "./surfaceContext";
4
+
5
+ export interface AgentResourceMeta {
6
+ description?: string;
7
+ serialize?: (value: unknown) => unknown;
8
+ report?: boolean;
9
+ }
10
+
11
+ /** Publishes a read-only view of any value the component already holds — derived totals, statuses, labels. */
12
+ export const useAgentResource = (name: string, value: unknown, meta: AgentResourceMeta = {}): void => {
13
+ const surface = useSurface();
14
+ const scope = useScopePath();
15
+ const live = useRef({ value, meta });
16
+ live.current = { value, meta };
17
+ const scopeKey = scope.join(".");
18
+ useEffect(() => {
19
+ const { meta: declared } = live.current;
20
+ return surface.registerResource(scope, {
21
+ name,
22
+ description: declared.description,
23
+ report: declared.report,
24
+ read: () => {
25
+ const { value: current, meta: currentMeta } = live.current;
26
+ return currentMeta.serialize ? currentMeta.serialize(current) : current;
27
+ },
28
+ });
29
+ }, [surface, scopeKey, name]);
30
+ };
@@ -0,0 +1,61 @@
1
+ "use client";
2
+ import { type Dispatch, type SetStateAction, useEffect, useRef, useState } from "react";
3
+ import { useScopePath, useSurface } from "./surfaceContext";
4
+ import type { JsonSchema } from "./types";
5
+
6
+ export interface AgentStateMeta<T> {
7
+ description?: string;
8
+ serialize?: (value: T) => unknown;
9
+ report?: boolean;
10
+ /** Schema of the setter's `value` argument. Without it the state is published read-only. */
11
+ set?: JsonSchema;
12
+ /** Validates or coerces the setter's incoming `value` — throw to refuse the write. Nothing else enforces `set`. */
13
+ parse?: (value: unknown) => T;
14
+ }
15
+
16
+ /** `useState` the agent can read — and write only through a named setter tool, and only when `set` declares one. */
17
+ export const useAgentState = <T>(
18
+ name: string,
19
+ initial: T | (() => T),
20
+ meta: AgentStateMeta<T> = {},
21
+ ): [T, Dispatch<SetStateAction<T>>] => {
22
+ const surface = useSurface();
23
+ const scope = useScopePath();
24
+ const [value, setValue] = useState(initial);
25
+ const live = useRef({ value, meta });
26
+ live.current = { value, meta };
27
+ const scopeKey = scope.join(".");
28
+ useEffect(() => {
29
+ const { meta: declared } = live.current;
30
+ return surface.registerResource(scope, {
31
+ name,
32
+ description: declared.description,
33
+ report: declared.report,
34
+ read: () => {
35
+ const { value: current, meta: currentMeta } = live.current;
36
+ return currentMeta.serialize ? currentMeta.serialize(current) : current;
37
+ },
38
+ });
39
+ }, [surface, scopeKey, name]);
40
+ const writable = !!meta.set;
41
+ useEffect(() => {
42
+ const { meta: declared } = live.current;
43
+ if (!declared.set) return;
44
+ return surface.registerTool(scope, {
45
+ name: `set${name.charAt(0).toUpperCase()}${name.slice(1)}`,
46
+ description: `Set ${name}.${declared.description ? ` ${declared.description}` : ""}`,
47
+ parameters: {
48
+ type: "object",
49
+ properties: { value: declared.set },
50
+ required: ["value"],
51
+ additionalProperties: false,
52
+ },
53
+ effect: "state",
54
+ run: (args) => {
55
+ const parse = live.current.meta.parse;
56
+ setValue(parse ? parse(args.value) : (args.value as T));
57
+ },
58
+ });
59
+ }, [surface, scopeKey, name, writable]);
60
+ return [value, setValue];
61
+ };
@@ -0,0 +1,55 @@
1
+ "use client";
2
+ import { useEffect, useRef } from "react";
3
+ import { useScopePath, useSurface } from "./surfaceContext";
4
+ import type { JsonSchema, ToolConfirm, ToolEffect, ToolGuard } from "./types";
5
+
6
+ export interface AgentToolMeta {
7
+ description?: string;
8
+ parameters?: JsonSchema;
9
+ effect?: ToolEffect;
10
+ confirm?: ToolConfirm;
11
+ guard?: ToolGuard;
12
+ }
13
+
14
+ /**
15
+ * Publishes one tool for as long as the component is mounted.
16
+ *
17
+ * Identity is the scoped name, not the callback — `run`, `guard`, and `confirm` read the latest render through a
18
+ * ref, so there is no dependency array and no stale closure for the agent to hit. `description` and `parameters`
19
+ * are declaration data, read once at mount.
20
+ */
21
+ export const useAgentTool = <Args extends Record<string, unknown> = Record<string, unknown>, Result = unknown>(
22
+ name: string,
23
+ meta: AgentToolMeta,
24
+ run: (args: Args) => Result | Promise<Result>,
25
+ ): ((args?: Args) => Promise<Result>) => {
26
+ const surface = useSurface();
27
+ const scope = useScopePath();
28
+ const live = useRef({ meta, run });
29
+ live.current = { meta, run };
30
+ const call = useRef<((args?: Args) => Promise<Result>) | null>(null);
31
+ if (!call.current) call.current = async (args = {} as Args) => await live.current.run(args);
32
+ const scopeKey = scope.join(".");
33
+ useEffect(() => {
34
+ const { meta: declared } = live.current;
35
+ return surface.registerTool(scope, {
36
+ name,
37
+ description: declared.description,
38
+ parameters: declared.parameters,
39
+ effect: declared.effect,
40
+ ...(declared.confirm === undefined
41
+ ? {}
42
+ : {
43
+ confirm: (args: Record<string, unknown>) => {
44
+ const confirm = live.current.meta.confirm;
45
+ return typeof confirm === "function" ? confirm(args) : (confirm ?? false);
46
+ },
47
+ }),
48
+ ...(declared.guard === undefined
49
+ ? {}
50
+ : { guard: (args: Record<string, unknown>) => live.current.meta.guard?.(args) ?? true }),
51
+ run: (args) => live.current.run(args as Args),
52
+ });
53
+ }, [surface, scopeKey, name]);
54
+ return call.current;
55
+ };
@@ -18,6 +18,7 @@ import {
18
18
  } from "akanjs/client";
19
19
  import {
20
20
  assertUniqueRoutePatterns,
21
+ getRouteExports,
21
22
  Logger,
22
23
  parseAkanI18nEnv,
23
24
  parseBasePaths,
@@ -303,39 +304,7 @@ function validateRouteModuleExports(key: string, mod: RouteModule) {
303
304
  if (!mod.default) throw new Error(`[route-convention] ${key} generated override wrapper has no default export`);
304
305
  return;
305
306
  }
306
- const allowed =
307
- parsed.kind === "page"
308
- ? new Set(["default", "pageConfig", "head", "metadata", "generateHead", "generateMetadata", "Loading"])
309
- : parsed.isInternalRootLayout
310
- ? new Set([
311
- "default",
312
- "pageConfig",
313
- "head",
314
- "metadata",
315
- "generateHead",
316
- "generateMetadata",
317
- "fonts",
318
- "manifest",
319
- "theme",
320
- "reconnect",
321
- "wsConnect",
322
- "layoutStyle",
323
- "gaTrackingId",
324
- "Loading",
325
- "NotFound",
326
- "Error",
327
- ])
328
- : new Set([
329
- "default",
330
- "pageConfig",
331
- "head",
332
- "metadata",
333
- "generateHead",
334
- "generateMetadata",
335
- "Loading",
336
- "NotFound",
337
- "Error",
338
- ]);
307
+ const allowed = getRouteExports(parsed.kind, { rootLayout: parsed.isInternalRootLayout });
339
308
  for (const exportName of Object.keys(mod)) {
340
309
  if (!allowed.has(exportName)) {
341
310
  throw new Error(`[route-convention] unsupported export "${exportName}" in ${key}`);
package/webkit/index.ts CHANGED
@@ -8,9 +8,12 @@ export { useCodepush } from "./useCodepush";
8
8
  export { useContact } from "./useContact";
9
9
  export { useCsrValues } from "./useCsrValues";
10
10
  export { useDebounce } from "./useDebounce";
11
+ export { useEscapeKey } from "./useEscapeKey";
11
12
  export { useFetch, useFetchFn } from "./useFetch";
12
13
  export { useGeoLocation } from "./useGeoLocation";
13
14
  export { useHistory } from "./useHistory";
14
15
  export { useInterval } from "./useInterval";
15
16
  export { useLocation } from "./useLocation";
17
+ export type { ScreenScopeItem } from "./useScreenScope";
18
+ export { useScreenScope } from "./useScreenScope";
16
19
  export { useThrottle } from "./useThrottle";
package/webkit/lazy.tsx CHANGED
@@ -5,7 +5,18 @@ import { forwardRef, lazy as reactLazy } from "react";
5
5
 
6
6
  const isServer = typeof window === "undefined";
7
7
 
8
- type LazyOption = { ssr?: boolean; loading?: () => ReactNode };
8
+ /**
9
+ * `suspense` puts a Suspense boundary around the lazy component, so a chunk that resolves *after* the
10
+ * page is painted — a modal body, a dropdown menu — suspends only itself. Without one the suspension
11
+ * travels up to the nearest boundary, which is the route (`server/routeElementComposer.tsx`), and the
12
+ * whole page repaints as its loading fallback on the first open.
13
+ *
14
+ * It is opt-in rather than the default because a boundary also changes server rendering: under the
15
+ * default `renderMode: "stream"` the boundary's subtree leaves the shell as `loading` and arrives later
16
+ * in the stream. That is fine for an interaction shell and wrong for a page body, which SEO snapshots,
17
+ * prerendering and pre-hydration E2E read out of the shell.
18
+ */
19
+ type LazyOption = { ssr?: boolean; suspense?: boolean; loading?: () => ReactNode };
9
20
  type LazyProps = Record<string, unknown>;
10
21
  type LoadedOf<Loaded> = Loaded extends { default: infer T } ? T : Loaded;
11
22
  type LazyModule = { default: ComponentType<LazyProps> };
@@ -27,7 +38,16 @@ export const lazy = <Loaded,>(loader: () => Promise<Loaded>, option?: LazyOption
27
38
  const LazyInner = reactLazy(async () => normalizeLazyModule(await loader()));
28
39
 
29
40
  if (!ssrFalse) {
30
- const Wrapper = forwardRef<unknown, LazyProps>((props, ref) => <LazyInner {...props} ref={ref as never} />);
41
+
42
+ const Wrapper = forwardRef<unknown, LazyProps>((props, ref) =>
43
+ option?.suspense ? (
44
+ <React.Suspense fallback={renderFallback()}>
45
+ <LazyInner {...props} ref={ref as never} />
46
+ </React.Suspense>
47
+ ) : (
48
+ <LazyInner {...props} ref={ref as never} />
49
+ ),
50
+ );
31
51
  Wrapper.displayName = "LazyWrapper";
32
52
  return Wrapper as unknown as LoadedOf<Loaded>;
33
53
  }
@@ -36,7 +56,11 @@ export const lazy = <Loaded,>(loader: () => Promise<Loaded>, option?: LazyOption
36
56
  const [mounted, setMounted] = React.useState(false);
37
57
  React.useEffect(() => setMounted(true), []);
38
58
  if (!mounted) return <>{renderFallback()}</>;
39
- return <LazyInner {...props} ref={ref as never} />;
59
+ return (
60
+ <React.Suspense fallback={renderFallback()}>
61
+ <LazyInner {...props} ref={ref as never} />
62
+ </React.Suspense>
63
+ );
40
64
  });
41
65
  Gate.displayName = "LazySsrFalseGate";
42
66
  return Gate as unknown as LoadedOf<Loaded>;
@@ -13,6 +13,7 @@ import {
13
13
  type NavigationIntent,
14
14
  normalizeDeepLinkHref,
15
15
  type PageState,
16
+ type PageTransition,
16
17
  type PathRoute,
17
18
  type RouteGuide,
18
19
  type RouteOptions,
@@ -23,13 +24,14 @@ import {
23
24
  } from "akanjs/client";
24
25
  import { loadCapacitorApp } from "akanjs/client/capacitor";
25
26
  import { parseAkanI18nEnv, parseBasePaths } from "akanjs/common";
26
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
27
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
27
28
  import {
28
29
  createFrameSnapshot,
29
30
  createTransitionPlan,
30
31
  FRAME_Z_INDEX,
31
32
  getFramePlatformProfile,
32
33
  getFrameSlotsForSnapshot,
34
+ hasBottomAnchoredKeyboardSlot,
33
35
  hasKeyboardStickySlot,
34
36
  isPendingFrameReady,
35
37
  PENDING_FRAME_READY_TIMEOUT_MS,
@@ -492,7 +494,6 @@ const useStackTrans = (routeState: RouteState): UseCsrTransition => {
492
494
  gestureIntent.current = "pending";
493
495
  stackBackConfigRef.current = null;
494
496
  unlockPageScroll();
495
- void Device.getDevice().hideKeyboard();
496
497
  }
497
498
  if (ix > initThreshold) {
498
499
  gestureIntent.current = "scroll";
@@ -511,6 +512,7 @@ const useStackTrans = (routeState: RouteState): UseCsrTransition => {
511
512
  if (absX < GESTURE_INTENT_THRESHOLD || absX <= absY * GESTURE_AXIS_LOCK_RATIO) return;
512
513
  gestureIntent.current = "gesture";
513
514
  lockPageScroll();
515
+ void Device.getDevice().hideKeyboard();
514
516
  }
515
517
  if (gestureIntent.current !== "gesture") {
516
518
  cancel();
@@ -668,11 +670,11 @@ const useBottomUpTrans = (routeState: RouteState): UseCsrTransition => {
668
670
 
669
671
  const pageBind = useDrag(
670
672
  ({ first, last, movement: [, my], initial: [, iy], cancel }) => {
671
- if (first) void Device.getDevice().hideKeyboard();
672
673
  if (iy > initThreshold) {
673
674
  cancel();
674
675
  return;
675
676
  }
677
+ if (first) void Device.getDevice().hideKeyboard();
676
678
  if (my < transUnitRange[1]) void transUnit.start(transUnitRange[1], { immediate: true });
677
679
  else if (my > transUnitRange[0]) void transUnit.start(transUnitRange[0], { immediate: true });
678
680
  else if (!last) void transUnit.start(my, { immediate: true });
@@ -864,6 +866,7 @@ export const useCsrValues = (rootRouteGuide: RouteGuide, pathRoutes: PathRoute[]
864
866
  const resolvedPendingLocation = resolveLocationWithFrameState(pendingLocation, resolvedPathRouteMap);
865
867
  const platformProfile = getFramePlatformProfile();
866
868
  const accessoryHeight = resolveKeyboardAccessoryHeight(resolvedLocation.pathRoute.path, frameSlots);
869
+ const shouldAnchorContentBottom = hasBottomAnchoredKeyboardSlot(resolvedLocation.pathRoute.path, frameSlots);
867
870
  const keyboardFrame = useKeyboardFrame({
868
871
  bottomSafeArea: resolvedLocation.pathRoute.pageState.bottomSafeArea,
869
872
  sticky: hasKeyboardStickySlot(resolvedLocation.pathRoute.path, frameSlots),
@@ -881,6 +884,103 @@ export const useCsrValues = (rootRouteGuide: RouteGuide, pathRoutes: PathRoute[]
881
884
  }),
882
885
  [viewport, keyboardFrame, accessoryHeight, resolvedLocation.pathRoute.pageState.bottomSafeArea],
883
886
  );
887
+ const contentBottomAnchorRef = useRef<{
888
+ path: string;
889
+ contentViewportHeight: number;
890
+ bottomDistance: number;
891
+ } | null>(null);
892
+ const contentBottomAnchorRafRef = useRef<number | null>(null);
893
+ useLayoutEffect(() => {
894
+ const element = pageContentRef.current;
895
+ const path = resolvedLocation.pathRoute.path;
896
+ if (!element || !shouldAnchorContentBottom || !keyboardFrame.sticky) {
897
+ contentBottomAnchorRef.current = null;
898
+ if (contentBottomAnchorRafRef.current !== null) {
899
+ cancelAnimationFrame(contentBottomAnchorRafRef.current);
900
+ contentBottomAnchorRafRef.current = null;
901
+ }
902
+ return;
903
+ }
904
+
905
+ const previous = contentBottomAnchorRef.current;
906
+ const currentBottomDistance = Math.max(0, element.scrollHeight - element.scrollTop - element.clientHeight);
907
+ if (!previous || previous.path !== path) {
908
+ contentBottomAnchorRef.current = {
909
+ path,
910
+ contentViewportHeight: keyboardLayout.contentViewport.height,
911
+ bottomDistance: currentBottomDistance,
912
+ };
913
+ return;
914
+ }
915
+
916
+ const shouldRestoreBottom = previous.contentViewportHeight !== keyboardLayout.contentViewport.height;
917
+ const bottomDistance = previous.bottomDistance;
918
+ if (shouldRestoreBottom) {
919
+ const nextScrollTop = Math.max(0, element.scrollHeight - element.clientHeight - bottomDistance);
920
+ if (Math.abs(element.scrollTop - nextScrollTop) > 1) {
921
+ debugFrame("keyboard.contentAnchor", {
922
+ path,
923
+ from: element.scrollTop,
924
+ to: nextScrollTop,
925
+ bottomDistance,
926
+ contentViewportHeight: keyboardLayout.contentViewport.height,
927
+ });
928
+ element.scrollTop = nextScrollTop;
929
+ }
930
+ const startedAt = performance.now();
931
+ const duration = (keyboardFrame.animationDuration ?? 0) + 50;
932
+ const keepBottomAnchored = () => {
933
+ const scrollTop = Math.max(0, element.scrollHeight - element.clientHeight - bottomDistance);
934
+ if (Math.abs(element.scrollTop - scrollTop) > 1) element.scrollTop = scrollTop;
935
+ if (performance.now() - startedAt < duration) {
936
+ contentBottomAnchorRafRef.current = requestAnimationFrame(keepBottomAnchored);
937
+ } else {
938
+ contentBottomAnchorRafRef.current = null;
939
+ }
940
+ };
941
+ if (contentBottomAnchorRafRef.current !== null) cancelAnimationFrame(contentBottomAnchorRafRef.current);
942
+ contentBottomAnchorRafRef.current = requestAnimationFrame(keepBottomAnchored);
943
+ }
944
+
945
+ contentBottomAnchorRef.current = {
946
+ path,
947
+ contentViewportHeight: keyboardLayout.contentViewport.height,
948
+ bottomDistance: Math.max(0, element.scrollHeight - element.scrollTop - element.clientHeight),
949
+ };
950
+ return () => {
951
+ if (contentBottomAnchorRafRef.current !== null) {
952
+ cancelAnimationFrame(contentBottomAnchorRafRef.current);
953
+ contentBottomAnchorRafRef.current = null;
954
+ }
955
+ };
956
+ }, [
957
+ pageContentRef,
958
+ resolvedLocation.pathRoute.path,
959
+ shouldAnchorContentBottom,
960
+ keyboardFrame.sticky,
961
+ keyboardFrame.animationDuration,
962
+ keyboardLayout.contentViewport.height,
963
+ ]);
964
+ useEffect(() => {
965
+ const element = pageContentRef.current;
966
+ const path = resolvedLocation.pathRoute.path;
967
+ if (!element || !shouldAnchorContentBottom || !keyboardFrame.sticky) return;
968
+ const updateBottomDistance = () => {
969
+ contentBottomAnchorRef.current = {
970
+ path,
971
+ contentViewportHeight: keyboardLayout.contentViewport.height,
972
+ bottomDistance: Math.max(0, element.scrollHeight - element.scrollTop - element.clientHeight),
973
+ };
974
+ };
975
+ element.addEventListener("scroll", updateBottomDistance, { passive: true });
976
+ return () => element.removeEventListener("scroll", updateBottomDistance);
977
+ }, [
978
+ pageContentRef,
979
+ resolvedLocation.pathRoute.path,
980
+ shouldAnchorContentBottom,
981
+ keyboardFrame.sticky,
982
+ keyboardLayout.contentViewport.height,
983
+ ]);
884
984
  useFrameRuntimeResync({ updateViewport });
885
985
  const shouldPrepareFrameTransition = useCallback(
886
986
  (nextHref?: string) => {
@@ -1245,6 +1345,7 @@ export const useCsrValues = (rootRouteGuide: RouteGuide, pathRoutes: PathRoute[]
1245
1345
  keyboard: keyboardFrame,
1246
1346
  contentViewport: keyboardLayout.contentViewport,
1247
1347
  keyboardAccessory: keyboardLayout.keyboardAccessory,
1348
+ contentAnchor: shouldAnchorContentBottom ? "bottom" : undefined,
1248
1349
  platformProfile,
1249
1350
  zIndex: FRAME_Z_INDEX,
1250
1351
  pageStateByPath: effectivePageStateByPath,
@@ -1262,6 +1363,21 @@ export const useCsrValues = (rootRouteGuide: RouteGuide, pathRoutes: PathRoute[]
1262
1363
  bottomUp: useBottomUpTransition,
1263
1364
  scaleOut: useScaleOutTransition,
1264
1365
  };
1366
+
1367
+ const contentResizeStyle = useMemo(() => {
1368
+ if (!shouldAnchorContentBottom || !keyboardFrame.sticky) return null;
1369
+ const duration = keyboardFrame.animationDuration ?? 420;
1370
+ const easing = keyboardFrame.animationEasing ?? "cubic-bezier(0.16, 1, 0.3, 1)";
1371
+ return {
1372
+ transition: `height ${duration}ms ${easing}, padding-bottom ${duration}ms ${easing}`,
1373
+ willChange: "height, padding-bottom",
1374
+ };
1375
+ }, [shouldAnchorContentBottom, keyboardFrame.sticky, keyboardFrame.animationDuration, keyboardFrame.animationEasing]);
1376
+ const csrTransition = useCsrTransitionMap[resolvedLocation.pathRoute.pageState.transition];
1377
+ const page: PageTransition | null =
1378
+ contentResizeStyle && csrTransition.page
1379
+ ? { ...csrTransition.page, contentStyle: { ...csrTransition.page.contentStyle, ...contentResizeStyle } }
1380
+ : csrTransition.page;
1265
1381
  const nativeBackStateRef = useRef({
1266
1382
  path: resolvedLocation.pathRoute.path,
1267
1383
  keyboardHeight: keyboardFrame.height,
@@ -1409,6 +1525,7 @@ export const useCsrValues = (rootRouteGuide: RouteGuide, pathRoutes: PathRoute[]
1409
1525
 
1410
1526
  return {
1411
1527
  ...routeState,
1412
- ...useCsrTransitionMap[resolvedLocation.pathRoute.pageState.transition],
1528
+ ...csrTransition,
1529
+ page,
1413
1530
  } satisfies CsrContextType;
1414
1531
  };
@@ -0,0 +1,42 @@
1
+ "use client";
2
+ import { useEffect, useRef } from "react";
3
+
4
+ /**
5
+ * Escape belongs to the topmost dismissable surface only.
6
+ *
7
+ * A per-component `window` listener cannot know that: every open surface hears the same keydown, so a
8
+ * dialog opened from another dialog took both down at once, and a confirm popover inside a modal would
9
+ * have closed the modal under it. One shared stack keeps the order surfaces actually opened in and
10
+ * hands the key to the last one.
11
+ */
12
+ const stack: (() => void)[] = [];
13
+
14
+ const onKeyDown = (event: KeyboardEvent) => {
15
+ if (event.key !== "Escape") return;
16
+ const topmost = stack.at(-1);
17
+ if (!topmost) return;
18
+ event.preventDefault();
19
+ topmost();
20
+ };
21
+
22
+ /**
23
+ * Close `onEscape` on Escape while `active`, but only while this surface is the topmost active one.
24
+ * The callback is read through a ref, so a fresh closure per render does not reshuffle the stack.
25
+ */
26
+ export const useEscapeKey = (active: boolean, onEscape: () => void) => {
27
+ const callbackRef = useRef(onEscape);
28
+ useEffect(() => {
29
+ callbackRef.current = onEscape;
30
+ });
31
+ useEffect(() => {
32
+ if (!active) return;
33
+ const entry = () => callbackRef.current();
34
+ stack.push(entry);
35
+ if (stack.length === 1) window.addEventListener("keydown", onKeyDown);
36
+ return () => {
37
+ const index = stack.lastIndexOf(entry);
38
+ if (index !== -1) stack.splice(index, 1);
39
+ if (stack.length === 0) window.removeEventListener("keydown", onKeyDown);
40
+ };
41
+ }, [active]);
42
+ };
@@ -25,7 +25,10 @@ export type FrameSlotBucket = "active" | "pending";
25
25
  export type FrameSlotMapByBucket = Record<FrameSlotBucket, FrameSlotMap>;
26
26
  export const PENDING_FRAME_READY_TIMEOUT_MS = 80;
27
27
  export const PENDING_FRAME_READY_MAX_TIMEOUT_MS = 120;
28
- export const KEYBOARD_FALLBACK_ANIMATION_DURATION_MS = 285;
28
+ export const KEYBOARD_SHOW_ANIMATION_DURATION_MS = 420;
29
+ export const KEYBOARD_HIDE_ANIMATION_DURATION_MS = 90;
30
+ export const KEYBOARD_SHOW_ANIMATION_EASING = "cubic-bezier(0.16, 1, 0.3, 1)";
31
+ export const KEYBOARD_HIDE_ANIMATION_EASING = "cubic-bezier(0.7, 0, 0.84, 0)";
29
32
 
30
33
  export const FRAME_Z_INDEX = {
31
34
  page: 10,
@@ -204,29 +207,37 @@ export function useFrameViewport() {
204
207
  visualOffsetTop: window.visualViewport?.offsetTop ?? 0,
205
208
  }));
206
209
 
207
- const updateViewport = useCallback((reason = "viewport.change") => {
208
- const visualViewport = window.visualViewport;
209
- const platform = getCurrentPlatform();
210
- const nextViewport = {
211
- width: Math.round(visualViewport?.width ?? window.innerWidth),
212
- height: Math.round(platform === "ios" ? window.innerHeight : (visualViewport?.height ?? window.innerHeight)),
213
- visualWidth: Math.round(visualViewport?.width ?? window.innerWidth),
214
- visualHeight: Math.round(visualViewport?.height ?? window.innerHeight),
215
- visualOffsetTop: Math.round(visualViewport?.offsetTop ?? 0),
216
- };
217
- setViewport((prev) => {
218
- if (
219
- prev.width === nextViewport.width &&
220
- prev.height === nextViewport.height &&
221
- prev.visualWidth === nextViewport.visualWidth &&
222
- prev.visualHeight === nextViewport.visualHeight &&
223
- prev.visualOffsetTop === nextViewport.visualOffsetTop
224
- )
225
- return prev;
226
- debugFrame(reason, { from: prev, to: nextViewport, platform });
227
- return nextViewport;
228
- });
229
- }, []);
210
+ const updateViewport = useCallback(
211
+ (reason = "viewport.change") => {
212
+ const visualViewport = window.visualViewport;
213
+ const platform = getCurrentPlatform();
214
+ const usesAkanKeyboardResize = platform === "ios" || platform === "android";
215
+ const width = Math.round(visualViewport?.width ?? window.innerWidth);
216
+ const currentHeight = Math.round(window.innerHeight);
217
+ const stableHeight =
218
+ usesAkanKeyboardResize && width === viewport.width ? Math.max(viewport.height, currentHeight) : currentHeight;
219
+ const nextViewport = {
220
+ width,
221
+ height: usesAkanKeyboardResize ? stableHeight : Math.round(visualViewport?.height ?? window.innerHeight),
222
+ visualWidth: Math.round(visualViewport?.width ?? window.innerWidth),
223
+ visualHeight: Math.round(visualViewport?.height ?? window.innerHeight),
224
+ visualOffsetTop: Math.round(visualViewport?.offsetTop ?? 0),
225
+ };
226
+ setViewport((prev) => {
227
+ if (
228
+ prev.width === nextViewport.width &&
229
+ prev.height === nextViewport.height &&
230
+ prev.visualWidth === nextViewport.visualWidth &&
231
+ prev.visualHeight === nextViewport.visualHeight &&
232
+ prev.visualOffsetTop === nextViewport.visualOffsetTop
233
+ )
234
+ return prev;
235
+ debugFrame(reason, { from: prev, to: nextViewport, platform });
236
+ return nextViewport;
237
+ });
238
+ },
239
+ [viewport.height],
240
+ );
230
241
 
231
242
  useEffect(() => {
232
243
  updateViewport("viewport.init");
@@ -259,6 +270,10 @@ export function hasKeyboardStickySlot(path: string, frameSlots: FrameSlotMap) {
259
270
  return getKeyboardAccessorySlots(path, frameSlots).length > 0;
260
271
  }
261
272
 
273
+ export function hasBottomAnchoredKeyboardSlot(path: string, frameSlots: FrameSlotMap) {
274
+ return getKeyboardAccessorySlots(path, frameSlots).some((slot) => slot.contentAnchor === "bottom");
275
+ }
276
+
262
277
  export function resolveKeyboardFrame({
263
278
  keyboardHeight,
264
279
  bottomSafeArea,
@@ -274,22 +289,36 @@ export function resolveKeyboardFrame({
274
289
  sticky: boolean;
275
290
  freeze?: boolean;
276
291
  }): KeyboardFrameState {
292
+ const useVisualViewportHeight = platformProfile === "android" && visualViewportKeyboardHeight > 0;
277
293
  const visualFallbackHeight =
278
294
  keyboardHeight <= 0 && visualViewportKeyboardHeight > 0 ? visualViewportKeyboardHeight : 0;
279
- const effectiveKeyboardHeight = keyboardHeight > 0 ? keyboardHeight : visualFallbackHeight;
280
- const source = keyboardHeight > 0 ? "native" : visualFallbackHeight > 0 ? "visualViewport" : "fallback";
295
+ const effectiveKeyboardHeight = useVisualViewportHeight
296
+ ? visualViewportKeyboardHeight
297
+ : keyboardHeight > 0
298
+ ? keyboardHeight
299
+ : visualFallbackHeight;
300
+ const source = useVisualViewportHeight
301
+ ? "visualViewport"
302
+ : keyboardHeight > 0
303
+ ? "native"
304
+ : visualFallbackHeight > 0
305
+ ? "visualViewport"
306
+ : "fallback";
281
307
  const visualCompensation =
282
- platformProfile === "ios" ? 0 : Math.min(visualViewportKeyboardHeight, effectiveKeyboardHeight);
308
+ platformProfile === "web" || platformProfile === "mobileWeb"
309
+ ? Math.min(visualViewportKeyboardHeight, effectiveKeyboardHeight)
310
+ : 0;
283
311
  const offset = sticky && !freeze ? Math.max(0, effectiveKeyboardHeight - visualCompensation) : 0;
312
+ const isShowing = effectiveKeyboardHeight > 0 && !freeze;
284
313
  return {
285
314
  height: effectiveKeyboardHeight,
286
315
  offset,
287
- visible: effectiveKeyboardHeight > 0 && !freeze,
316
+ visible: isShowing,
288
317
  sticky,
289
318
  frozen: freeze,
290
319
  source,
291
- animationDuration: KEYBOARD_FALLBACK_ANIMATION_DURATION_MS,
292
- animationEasing: "ease-out",
320
+ animationDuration: isShowing ? KEYBOARD_SHOW_ANIMATION_DURATION_MS : KEYBOARD_HIDE_ANIMATION_DURATION_MS,
321
+ animationEasing: isShowing ? KEYBOARD_SHOW_ANIMATION_EASING : KEYBOARD_HIDE_ANIMATION_EASING,
293
322
  };
294
323
  }
295
324
 
@@ -344,14 +373,18 @@ export function useKeyboardFrame({
344
373
  }: {
345
374
  bottomSafeArea: number;
346
375
  sticky: boolean;
347
- viewport: { visualHeight: number; visualOffsetTop: number };
376
+ viewport: { height: number; visualHeight: number; visualOffsetTop: number };
348
377
  platformProfile: FramePlatformProfile;
349
378
  freeze?: boolean;
350
379
  }) {
351
380
  const keyboardHeight = st.use.keyboardHeight();
381
+ const androidViewportKeyboardHeight =
382
+ platformProfile === "android" ? Math.max(0, Math.round(viewport.height - window.innerHeight)) : 0;
352
383
  const visualViewportKeyboardHeight = Math.max(
353
384
  0,
354
- Math.round(window.innerHeight - viewport.visualHeight - viewport.visualOffsetTop),
385
+ androidViewportKeyboardHeight > 0
386
+ ? androidViewportKeyboardHeight
387
+ : Math.round(viewport.height - viewport.visualHeight - viewport.visualOffsetTop),
355
388
  );
356
389
  return useMemo(
357
390
  () =>