@saolabs/client 1.0.0 → 1.0.2

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/README.md +52 -195
  2. package/dist/index.d.ts +20 -5
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +14 -3
  5. package/dist/index.js.map +1 -1
  6. package/dist/plugins.d.ts +2 -2
  7. package/dist/plugins.js +2 -2
  8. package/dist/src/core/app/Application.d.ts +17 -15
  9. package/dist/src/core/app/Application.d.ts.map +1 -1
  10. package/dist/src/core/app/Application.js +41 -18
  11. package/dist/src/core/app/Application.js.map +1 -1
  12. package/dist/src/core/bootstrap/app.d.ts +1 -1
  13. package/dist/src/core/bootstrap/app.d.ts.map +1 -1
  14. package/dist/src/core/bootstrap/app.js +74 -4
  15. package/dist/src/core/bootstrap/app.js.map +1 -1
  16. package/dist/src/core/bootstrap/bundle.d.ts +41 -0
  17. package/dist/src/core/bootstrap/bundle.d.ts.map +1 -0
  18. package/dist/src/core/bootstrap/bundle.js +98 -0
  19. package/dist/src/core/bootstrap/bundle.js.map +1 -0
  20. package/dist/src/core/bootstrap/providers/ApiServiceProvider.d.ts +1 -1
  21. package/dist/src/core/bootstrap/providers/ApiServiceProvider.d.ts.map +1 -1
  22. package/dist/src/core/bootstrap/providers/ApiServiceProvider.js +3 -3
  23. package/dist/src/core/bootstrap/providers/ApiServiceProvider.js.map +1 -1
  24. package/dist/src/core/bootstrap/providers/CoreServiceProvider.d.ts +1 -1
  25. package/dist/src/core/bootstrap/providers/CoreServiceProvider.d.ts.map +1 -1
  26. package/dist/src/core/bootstrap/providers/CoreServiceProvider.js +11 -8
  27. package/dist/src/core/bootstrap/providers/CoreServiceProvider.js.map +1 -1
  28. package/dist/src/core/bootstrap/providers/HelperServiceProvider.d.ts +3 -1
  29. package/dist/src/core/bootstrap/providers/HelperServiceProvider.d.ts.map +1 -1
  30. package/dist/src/core/bootstrap/providers/HelperServiceProvider.js +8 -3
  31. package/dist/src/core/bootstrap/providers/HelperServiceProvider.js.map +1 -1
  32. package/dist/src/core/bootstrap/providers/RouteServiceProvider.d.ts +2 -1
  33. package/dist/src/core/bootstrap/providers/RouteServiceProvider.d.ts.map +1 -1
  34. package/dist/src/core/bootstrap/providers/RouteServiceProvider.js +14 -4
  35. package/dist/src/core/bootstrap/providers/RouteServiceProvider.js.map +1 -1
  36. package/dist/src/core/bootstrap/providers/ServiceProvider.d.ts +4 -4
  37. package/dist/src/core/bootstrap/providers/ServiceProvider.d.ts.map +1 -1
  38. package/dist/src/core/bootstrap/providers/ServiceProvider.js +5 -5
  39. package/dist/src/core/bootstrap/providers/ServiceProvider.js.map +1 -1
  40. package/dist/src/core/bootstrap/providers/ViewServiceProvider.d.ts +1 -1
  41. package/dist/src/core/bootstrap/providers/ViewServiceProvider.d.ts.map +1 -1
  42. package/dist/src/core/bootstrap/providers/ViewServiceProvider.js +3 -3
  43. package/dist/src/core/bootstrap/providers/ViewServiceProvider.js.map +1 -1
  44. package/dist/src/core/bootstrap/providers/default-providers.d.ts +1 -1
  45. package/dist/src/core/bootstrap/providers/default-providers.d.ts.map +1 -1
  46. package/dist/src/core/bootstrap/providers/default-providers.js +44 -11
  47. package/dist/src/core/bootstrap/providers/default-providers.js.map +1 -1
  48. package/dist/src/core/bootstrap/providers/index.d.ts +9 -9
  49. package/dist/src/core/bootstrap/providers/index.d.ts.map +1 -1
  50. package/dist/src/core/bootstrap/providers/index.js +8 -8
  51. package/dist/src/core/bootstrap/providers/index.js.map +1 -1
  52. package/dist/src/core/bootstrap/providers/provider-order.d.ts +1 -1
  53. package/dist/src/core/bootstrap/providers/provider-order.d.ts.map +1 -1
  54. package/dist/src/core/bootstrap/ssr.d.ts +47 -0
  55. package/dist/src/core/bootstrap/ssr.d.ts.map +1 -0
  56. package/dist/src/core/bootstrap/ssr.js +125 -0
  57. package/dist/src/core/bootstrap/ssr.js.map +1 -0
  58. package/dist/src/core/cache/ViewCache.d.ts +1 -1
  59. package/dist/src/core/cache/ViewCache.d.ts.map +1 -1
  60. package/dist/src/core/cache/index.d.ts +1 -1
  61. package/dist/src/core/cache/index.d.ts.map +1 -1
  62. package/dist/src/core/cache/index.js +1 -1
  63. package/dist/src/core/cache/index.js.map +1 -1
  64. package/dist/src/core/contracts/ApplicationInterface.d.ts +13 -11
  65. package/dist/src/core/contracts/ApplicationInterface.d.ts.map +1 -1
  66. package/dist/src/core/contracts/BlockInterface.d.ts +10 -10
  67. package/dist/src/core/contracts/BlockInterface.d.ts.map +1 -1
  68. package/dist/src/core/contracts/BundleInterface.d.ts +38 -0
  69. package/dist/src/core/contracts/BundleInterface.d.ts.map +1 -0
  70. package/dist/src/core/contracts/BundleInterface.js +2 -0
  71. package/dist/src/core/contracts/BundleInterface.js.map +1 -0
  72. package/dist/src/core/contracts/ComponentInterface.d.ts +7 -6
  73. package/dist/src/core/contracts/ComponentInterface.d.ts.map +1 -1
  74. package/dist/src/core/contracts/ElementInterface.d.ts +91 -27
  75. package/dist/src/core/contracts/ElementInterface.d.ts.map +1 -1
  76. package/dist/src/core/contracts/HeadServiceInterface.d.ts +37 -0
  77. package/dist/src/core/contracts/HeadServiceInterface.d.ts.map +1 -0
  78. package/dist/src/core/contracts/HeadServiceInterface.js +3 -0
  79. package/dist/src/core/contracts/HeadServiceInterface.js.map +1 -0
  80. package/dist/src/core/contracts/HelperInterface.d.ts +9 -1
  81. package/dist/src/core/contracts/HelperInterface.d.ts.map +1 -1
  82. package/dist/src/core/contracts/HttpServiceInterface.d.ts +3 -0
  83. package/dist/src/core/contracts/HttpServiceInterface.d.ts.map +1 -1
  84. package/dist/src/core/contracts/MarkerInterface.d.ts +29 -44
  85. package/dist/src/core/contracts/MarkerInterface.d.ts.map +1 -1
  86. package/dist/src/core/contracts/ReactiveInterface.d.ts +9 -9
  87. package/dist/src/core/contracts/ReactiveInterface.d.ts.map +1 -1
  88. package/dist/src/core/contracts/RouterInterface.d.ts +5 -2
  89. package/dist/src/core/contracts/RouterInterface.d.ts.map +1 -1
  90. package/dist/src/core/contracts/SectionInterface.d.ts +48 -0
  91. package/dist/src/core/contracts/SectionInterface.d.ts.map +1 -0
  92. package/dist/src/core/contracts/SectionInterface.js +2 -0
  93. package/dist/src/core/contracts/SectionInterface.js.map +1 -0
  94. package/dist/src/core/contracts/ServiceProviderInterface.d.ts +3 -3
  95. package/dist/src/core/contracts/ServiceProviderInterface.d.ts.map +1 -1
  96. package/dist/src/core/contracts/ViewControllerInterface.d.ts +116 -15
  97. package/dist/src/core/contracts/ViewControllerInterface.d.ts.map +1 -1
  98. package/dist/src/core/contracts/ViewInterface.d.ts +31 -5
  99. package/dist/src/core/contracts/ViewInterface.d.ts.map +1 -1
  100. package/dist/src/core/contracts/ViewManagerInterface.d.ts +47 -5
  101. package/dist/src/core/contracts/ViewManagerInterface.d.ts.map +1 -1
  102. package/dist/src/core/contracts/ViewStateInterface.d.ts +39 -2
  103. package/dist/src/core/contracts/ViewStateInterface.d.ts.map +1 -1
  104. package/dist/src/core/contracts/common.d.ts +3 -3
  105. package/dist/src/core/contracts/common.js +3 -3
  106. package/dist/src/core/contracts/utils.d.ts +19 -19
  107. package/dist/src/core/contracts/utils.d.ts.map +1 -1
  108. package/dist/src/core/contracts/views.d.ts +1 -22
  109. package/dist/src/core/contracts/views.d.ts.map +1 -1
  110. package/dist/src/core/devtools/hook.d.ts +66 -0
  111. package/dist/src/core/devtools/hook.d.ts.map +1 -0
  112. package/dist/src/core/devtools/hook.js +114 -0
  113. package/dist/src/core/devtools/hook.js.map +1 -0
  114. package/dist/src/core/devtools/index.d.ts +34 -0
  115. package/dist/src/core/devtools/index.d.ts.map +1 -0
  116. package/dist/src/core/devtools/index.js +33 -0
  117. package/dist/src/core/devtools/index.js.map +1 -0
  118. package/dist/src/core/devtools/inspector.d.ts +31 -0
  119. package/dist/src/core/devtools/inspector.d.ts.map +1 -0
  120. package/dist/src/core/devtools/inspector.js +249 -0
  121. package/dist/src/core/devtools/inspector.js.map +1 -0
  122. package/dist/src/core/elements/Block.d.ts +9 -7
  123. package/dist/src/core/elements/Block.d.ts.map +1 -1
  124. package/dist/src/core/elements/Block.js +33 -9
  125. package/dist/src/core/elements/Block.js.map +1 -1
  126. package/dist/src/core/elements/BlockOutlet.d.ts +19 -8
  127. package/dist/src/core/elements/BlockOutlet.d.ts.map +1 -1
  128. package/dist/src/core/elements/BlockOutlet.js +57 -13
  129. package/dist/src/core/elements/BlockOutlet.js.map +1 -1
  130. package/dist/src/core/elements/Component.d.ts +83 -11
  131. package/dist/src/core/elements/Component.d.ts.map +1 -1
  132. package/dist/src/core/elements/Component.js +334 -13
  133. package/dist/src/core/elements/Component.js.map +1 -1
  134. package/dist/src/core/elements/ForeachSlotCache.d.ts +95 -0
  135. package/dist/src/core/elements/ForeachSlotCache.d.ts.map +1 -0
  136. package/dist/src/core/elements/ForeachSlotCache.js +149 -0
  137. package/dist/src/core/elements/ForeachSlotCache.js.map +1 -0
  138. package/dist/src/core/elements/Fragment.d.ts +19 -13
  139. package/dist/src/core/elements/Fragment.d.ts.map +1 -1
  140. package/dist/src/core/elements/Fragment.js +43 -20
  141. package/dist/src/core/elements/Fragment.js.map +1 -1
  142. package/dist/src/core/elements/Html.d.ts +89 -12
  143. package/dist/src/core/elements/Html.d.ts.map +1 -1
  144. package/dist/src/core/elements/Html.js +707 -106
  145. package/dist/src/core/elements/Html.js.map +1 -1
  146. package/dist/src/core/elements/Output.d.ts +34 -10
  147. package/dist/src/core/elements/Output.d.ts.map +1 -1
  148. package/dist/src/core/elements/Output.js +141 -26
  149. package/dist/src/core/elements/Output.js.map +1 -1
  150. package/dist/src/core/elements/Reactive.d.ts +85 -14
  151. package/dist/src/core/elements/Reactive.d.ts.map +1 -1
  152. package/dist/src/core/elements/Reactive.js +315 -41
  153. package/dist/src/core/elements/Reactive.js.map +1 -1
  154. package/dist/src/core/elements/TextElement.d.ts +11 -7
  155. package/dist/src/core/elements/TextElement.d.ts.map +1 -1
  156. package/dist/src/core/elements/TextElement.js +18 -11
  157. package/dist/src/core/elements/TextElement.js.map +1 -1
  158. package/dist/src/core/elements/Wrapper.d.ts +24 -13
  159. package/dist/src/core/elements/Wrapper.d.ts.map +1 -1
  160. package/dist/src/core/elements/Wrapper.js +65 -8
  161. package/dist/src/core/elements/Wrapper.js.map +1 -1
  162. package/dist/src/core/elements/Yield.d.ts +10 -9
  163. package/dist/src/core/elements/Yield.d.ts.map +1 -1
  164. package/dist/src/core/elements/Yield.js +31 -19
  165. package/dist/src/core/elements/Yield.js.map +1 -1
  166. package/dist/src/core/elements/helpers.d.ts +7 -7
  167. package/dist/src/core/elements/helpers.d.ts.map +1 -1
  168. package/dist/src/core/elements/helpers.js +4 -4
  169. package/dist/src/core/elements/index.d.ts +12 -11
  170. package/dist/src/core/elements/index.d.ts.map +1 -1
  171. package/dist/src/core/elements/index.js +12 -11
  172. package/dist/src/core/elements/index.js.map +1 -1
  173. package/dist/src/core/elements/reconcileChildren.d.ts +7 -0
  174. package/dist/src/core/elements/reconcileChildren.d.ts.map +1 -0
  175. package/dist/src/core/elements/reconcileChildren.js +74 -0
  176. package/dist/src/core/elements/reconcileChildren.js.map +1 -0
  177. package/dist/src/core/hellpers/utils.d.ts +2 -2
  178. package/dist/src/core/hellpers/utils.js +4 -4
  179. package/dist/src/core/helpers/ApiClient.d.ts +58 -0
  180. package/dist/src/core/helpers/ApiClient.d.ts.map +1 -0
  181. package/dist/src/core/helpers/ApiClient.js +131 -0
  182. package/dist/src/core/helpers/ApiClient.js.map +1 -0
  183. package/dist/src/core/helpers/app.d.ts +3 -0
  184. package/dist/src/core/helpers/app.d.ts.map +1 -0
  185. package/dist/src/core/helpers/app.js +63 -0
  186. package/dist/src/core/helpers/app.js.map +1 -0
  187. package/dist/src/core/helpers/transition.d.ts +34 -0
  188. package/dist/src/core/helpers/transition.d.ts.map +1 -0
  189. package/dist/src/core/helpers/transition.js +118 -0
  190. package/dist/src/core/helpers/transition.js.map +1 -0
  191. package/dist/src/core/helpers/utils.d.ts +26 -0
  192. package/dist/src/core/helpers/utils.d.ts.map +1 -0
  193. package/dist/src/core/helpers/utils.js +96 -0
  194. package/dist/src/core/helpers/utils.js.map +1 -0
  195. package/dist/src/core/helpers/view.d.ts +39 -0
  196. package/dist/src/core/helpers/view.d.ts.map +1 -0
  197. package/dist/src/core/helpers/view.js +286 -0
  198. package/dist/src/core/helpers/view.js.map +1 -0
  199. package/dist/src/core/routers/Router.d.ts +108 -7
  200. package/dist/src/core/routers/Router.d.ts.map +1 -1
  201. package/dist/src/core/routers/Router.js +444 -68
  202. package/dist/src/core/routers/Router.js.map +1 -1
  203. package/dist/src/core/routers/index.d.ts +2 -2
  204. package/dist/src/core/routers/index.d.ts.map +1 -1
  205. package/dist/src/core/routers/index.js +1 -1
  206. package/dist/src/core/routers/index.js.map +1 -1
  207. package/dist/src/core/services/AssetManager.d.ts +112 -0
  208. package/dist/src/core/services/AssetManager.d.ts.map +1 -0
  209. package/dist/src/core/services/AssetManager.js +361 -0
  210. package/dist/src/core/services/AssetManager.js.map +1 -0
  211. package/dist/src/core/services/BlockManager.d.ts +73 -2
  212. package/dist/src/core/services/BlockManager.d.ts.map +1 -1
  213. package/dist/src/core/services/BlockManager.js +270 -48
  214. package/dist/src/core/services/BlockManager.js.map +1 -1
  215. package/dist/src/core/services/DomService.js +3 -3
  216. package/dist/src/core/services/DomService.js.map +1 -1
  217. package/dist/src/core/services/HeadService.d.ts +24 -0
  218. package/dist/src/core/services/HeadService.d.ts.map +1 -0
  219. package/dist/src/core/services/HeadService.js +120 -0
  220. package/dist/src/core/services/HeadService.js.map +1 -0
  221. package/dist/src/core/services/HelperService.d.ts +14 -3
  222. package/dist/src/core/services/HelperService.d.ts.map +1 -1
  223. package/dist/src/core/services/HelperService.js +32 -2
  224. package/dist/src/core/services/HelperService.js.map +1 -1
  225. package/dist/src/core/services/HttpService.d.ts +8 -0
  226. package/dist/src/core/services/HttpService.d.ts.map +1 -1
  227. package/dist/src/core/services/HttpService.js +121 -47
  228. package/dist/src/core/services/HttpService.js.map +1 -1
  229. package/dist/src/core/services/MarkerRegistry.d.ts +51 -8
  230. package/dist/src/core/services/MarkerRegistry.d.ts.map +1 -1
  231. package/dist/src/core/services/MarkerRegistry.js +135 -14
  232. package/dist/src/core/services/MarkerRegistry.js.map +1 -1
  233. package/dist/src/core/services/PageCache.d.ts +101 -0
  234. package/dist/src/core/services/PageCache.d.ts.map +1 -0
  235. package/dist/src/core/services/PageCache.js +184 -0
  236. package/dist/src/core/services/PageCache.js.map +1 -0
  237. package/dist/src/core/services/SectionManager.d.ts +100 -0
  238. package/dist/src/core/services/SectionManager.d.ts.map +1 -0
  239. package/dist/src/core/services/SectionManager.js +408 -0
  240. package/dist/src/core/services/SectionManager.js.map +1 -0
  241. package/dist/src/core/services/StorageService.js +1 -1
  242. package/dist/src/core/services/TranslationService.d.ts +21 -0
  243. package/dist/src/core/services/TranslationService.d.ts.map +1 -0
  244. package/dist/src/core/services/TranslationService.js +95 -0
  245. package/dist/src/core/services/TranslationService.js.map +1 -0
  246. package/dist/src/core/services/index.d.ts +11 -8
  247. package/dist/src/core/services/index.d.ts.map +1 -1
  248. package/dist/src/core/services/index.js +11 -8
  249. package/dist/src/core/services/index.js.map +1 -1
  250. package/dist/src/core/types/utils.d.ts +8 -8
  251. package/dist/src/core/types/utils.d.ts.map +1 -1
  252. package/dist/src/core/view/LoopContext.d.ts +21 -1
  253. package/dist/src/core/view/LoopContext.d.ts.map +1 -1
  254. package/dist/src/core/view/LoopContext.js +36 -0
  255. package/dist/src/core/view/LoopContext.js.map +1 -1
  256. package/dist/src/core/view/ResourceScope.d.ts +38 -0
  257. package/dist/src/core/view/ResourceScope.d.ts.map +1 -0
  258. package/dist/src/core/view/ResourceScope.js +167 -0
  259. package/dist/src/core/view/ResourceScope.js.map +1 -0
  260. package/dist/src/core/view/Section.d.ts +17 -32
  261. package/dist/src/core/view/Section.d.ts.map +1 -1
  262. package/dist/src/core/view/Section.js +14 -55
  263. package/dist/src/core/view/Section.js.map +1 -1
  264. package/dist/src/core/view/View.d.ts +43 -7
  265. package/dist/src/core/view/View.d.ts.map +1 -1
  266. package/dist/src/core/view/View.js +22 -4
  267. package/dist/src/core/view/View.js.map +1 -1
  268. package/dist/src/core/view/ViewController.d.ts +313 -40
  269. package/dist/src/core/view/ViewController.d.ts.map +1 -1
  270. package/dist/src/core/view/ViewController.js +1002 -147
  271. package/dist/src/core/view/ViewController.js.map +1 -1
  272. package/dist/src/core/view/ViewManager.d.ts +318 -27
  273. package/dist/src/core/view/ViewManager.d.ts.map +1 -1
  274. package/dist/src/core/view/ViewManager.js +1199 -258
  275. package/dist/src/core/view/ViewManager.js.map +1 -1
  276. package/dist/src/core/view/ViewState.d.ts +185 -6
  277. package/dist/src/core/view/ViewState.d.ts.map +1 -1
  278. package/dist/src/core/view/ViewState.js +851 -123
  279. package/dist/src/core/view/ViewState.js.map +1 -1
  280. package/dist/src/core/view/index.d.ts +5 -5
  281. package/dist/src/core/view/index.d.ts.map +1 -1
  282. package/dist/src/core/view/index.js +5 -5
  283. package/dist/src/core/view/index.js.map +1 -1
  284. package/dist/src/testing/index.d.ts +69 -0
  285. package/dist/src/testing/index.d.ts.map +1 -0
  286. package/dist/src/testing/index.js +140 -0
  287. package/dist/src/testing/index.js.map +1 -0
  288. package/index.ts +29 -5
  289. package/package.json +30 -21
  290. package/plugins.ts +2 -2
  291. package/src/core/app/Application.ts +58 -31
  292. package/src/core/bootstrap/app.ts +80 -6
  293. package/src/core/bootstrap/bundle.ts +101 -0
  294. package/src/core/bootstrap/providers/ApiServiceProvider.ts +4 -4
  295. package/src/core/bootstrap/providers/CoreServiceProvider.ts +14 -8
  296. package/src/core/bootstrap/providers/HelperServiceProvider.ts +8 -3
  297. package/src/core/bootstrap/providers/RouteServiceProvider.ts +15 -4
  298. package/src/core/bootstrap/providers/ServiceProvider.ts +8 -8
  299. package/src/core/bootstrap/providers/ViewServiceProvider.ts +3 -3
  300. package/src/core/bootstrap/providers/default-providers.ts +51 -16
  301. package/src/core/bootstrap/providers/index.ts +9 -9
  302. package/src/core/bootstrap/providers/provider-order.test.ts +16 -27
  303. package/src/core/bootstrap/providers/provider-order.ts +1 -1
  304. package/src/core/bootstrap/ssr.ts +136 -0
  305. package/src/core/cache/ViewCache.ts +1 -1
  306. package/src/core/cache/index.ts +1 -1
  307. package/src/core/contracts/ApplicationInterface.ts +13 -11
  308. package/src/core/contracts/BlockInterface.ts +18 -18
  309. package/src/core/contracts/BundleInterface.ts +40 -0
  310. package/src/core/contracts/ComponentInterface.ts +7 -6
  311. package/src/core/contracts/ElementInterface.ts +95 -27
  312. package/src/core/contracts/HeadServiceInterface.ts +41 -0
  313. package/src/core/contracts/HelperInterface.ts +9 -1
  314. package/src/core/contracts/HttpServiceInterface.ts +3 -0
  315. package/src/core/contracts/MarkerInterface.ts +22 -49
  316. package/src/core/contracts/ReactiveInterface.ts +9 -9
  317. package/src/core/contracts/RouterInterface.ts +5 -2
  318. package/src/core/contracts/SectionInterface.ts +49 -0
  319. package/src/core/contracts/ServiceProviderInterface.ts +3 -3
  320. package/src/core/contracts/ViewControllerInterface.ts +124 -16
  321. package/src/core/contracts/ViewInterface.ts +34 -5
  322. package/src/core/contracts/ViewManagerInterface.ts +40 -5
  323. package/src/core/contracts/ViewStateInterface.ts +39 -2
  324. package/src/core/contracts/common.ts +3 -3
  325. package/src/core/contracts/utils.ts +19 -19
  326. package/src/core/contracts/views.ts +2 -22
  327. package/src/core/devtools/hook.ts +142 -0
  328. package/src/core/devtools/index.ts +38 -0
  329. package/src/core/devtools/inspector.ts +270 -0
  330. package/src/core/elements/Block.ts +43 -21
  331. package/src/core/elements/BlockOutlet.ts +61 -21
  332. package/src/core/elements/Component.ts +352 -18
  333. package/src/core/elements/ElementManager.ts +9 -9
  334. package/src/core/elements/ForeachSlotCache.ts +170 -0
  335. package/src/core/elements/Fragment.ts +50 -32
  336. package/src/core/elements/Html.ts +696 -117
  337. package/src/core/elements/Output.ts +147 -30
  338. package/src/core/elements/Reactive.ts +329 -52
  339. package/src/core/elements/TextElement.ts +25 -16
  340. package/src/core/elements/Wrapper.ts +84 -18
  341. package/src/core/elements/Yield.ts +35 -24
  342. package/src/core/elements/helpers.ts +12 -12
  343. package/src/core/elements/index.ts +25 -24
  344. package/src/core/elements/reconcileChildren.ts +70 -0
  345. package/src/core/{hellpers → helpers}/ApiClient.ts +8 -1
  346. package/src/core/{hellpers → helpers}/app.ts +13 -2
  347. package/src/core/helpers/transition.ts +127 -0
  348. package/src/core/{hellpers → helpers}/utils.ts +4 -4
  349. package/src/core/helpers/view.ts +283 -0
  350. package/src/core/routers/Router.ts +458 -64
  351. package/src/core/routers/index.ts +2 -2
  352. package/src/core/services/AssetManager.ts +387 -0
  353. package/src/core/services/BlockManager.ts +272 -50
  354. package/src/core/services/DomService.ts +3 -3
  355. package/src/core/services/HeadService.ts +124 -0
  356. package/src/core/services/HelperService.ts +34 -4
  357. package/src/core/services/HttpService.ts +116 -55
  358. package/src/core/services/MarkerRegistry.ts +142 -16
  359. package/src/core/services/PageCache.ts +239 -0
  360. package/src/core/services/SectionManager.ts +387 -0
  361. package/src/core/services/StorageService.ts +1 -1
  362. package/src/core/services/TranslationService.ts +91 -0
  363. package/src/core/services/index.ts +11 -8
  364. package/src/core/types/utils.ts +8 -8
  365. package/src/core/view/LoopContext.ts +38 -1
  366. package/src/core/view/ResourceScope.ts +174 -0
  367. package/src/core/view/Section.ts +21 -68
  368. package/src/core/view/View.ts +57 -8
  369. package/src/core/view/ViewController.ts +1070 -186
  370. package/src/core/view/ViewManager.ts +1346 -272
  371. package/src/core/view/ViewState.ts +834 -116
  372. package/src/core/view/index.ts +5 -5
  373. package/src/testing/index.ts +184 -0
  374. package/dist/src/core/services/MarkerService.d.ts +0 -299
  375. package/dist/src/core/services/MarkerService.d.ts.map +0 -1
  376. package/dist/src/core/services/MarkerService.js +0 -723
  377. package/dist/src/core/services/MarkerService.js.map +0 -1
  378. package/src/core/services/MarkerService.ts +0 -923
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Router — SPA Router cho OneView v3.
2
+ * Router — SPA Router cho SaoView v3.
3
3
  *
4
4
  * Port từ core/Router nhưng nhẹ hơn (~400 dòng thay vì 960),
5
5
  * tích hợp trực tiếp với ViewManager mới.
@@ -14,7 +14,7 @@
14
14
  * - Browser back/forward handling
15
15
  */
16
16
 
17
- import type { ViewManagerInterface } from "../contracts/ViewManagerInterface";
17
+ import type { ViewManagerInterface } from "../contracts/ViewManagerInterface.js";
18
18
 
19
19
  // ─── Types ──────────────────────────────────────────────────────
20
20
 
@@ -23,12 +23,24 @@ export interface RouteDefinition {
23
23
  path: string;
24
24
  /** View name: 'web.home', 'layouts.main' */
25
25
  component?: string;
26
+ /** Worker-stable alias retained by the server route registry. */
27
+ logicalComponent?: string;
26
28
  /** @deprecated Use component */
27
29
  view?: string;
28
30
  /** Named route identifier */
29
31
  name?: string;
30
32
  /** Route metadata (auth, roles, etc.) */
31
33
  meta?: Record<string, any>;
34
+ /**
35
+ * Route con — path nối vào path cha, `meta` kế thừa từ cha (con ghi đè khi
36
+ * trùng key). Chỉ là tầng CẤU HÌNH: bảng route vẫn phẳng sau khi flatten.
37
+ *
38
+ * Việc giữ nguyên view cha khi chuyển giữa các con là do chuỗi layout lo
39
+ * (`@extends` + `@useBlock`) — view con khai báo cha bằng `@extends`, và
40
+ * `ViewManager` tái dùng đúng instance layout đang active thay vì render
41
+ * lại. Xem docs/GAPS_AND_ROADMAP.md §2.17.
42
+ */
43
+ children?: RouteDefinition[];
32
44
  }
33
45
 
34
46
  export interface Route {
@@ -59,12 +71,16 @@ export interface RouterConfig {
59
71
 
60
72
  export type NavigationGuard = (to: Route, from: ActiveRoute | null, urlPath: string) => boolean | Promise<boolean>;
61
73
  export type AfterNavigationHook = (to: Route, from: ActiveRoute | null) => void;
74
+ type NavigationType = 'push' | 'replace' | 'pop' | 'initial';
75
+ type NavigationRequest = { id: number; path: string; type: NavigationType };
62
76
 
63
77
  // ─── ActiveRoute ────────────────────────────────────────────────
64
78
 
65
79
  export class ActiveRoute {
66
80
  public readonly $route: Route;
67
81
  public readonly $urlPath: string;
82
+ /** Request URI = path + query string (KHÔNG gồm hash) — dùng làm cache key */
83
+ public readonly $uri: string;
68
84
  public readonly $params: Record<string, string>;
69
85
  public readonly $query: Record<string, string>;
70
86
  public readonly $fragment: string;
@@ -74,10 +90,12 @@ export class ActiveRoute {
74
90
  urlPath: string,
75
91
  params: Record<string, string> = {},
76
92
  query: Record<string, string> = {},
77
- fragment: string = ''
93
+ fragment: string = '',
94
+ uri?: string
78
95
  ) {
79
96
  this.$route = route;
80
97
  this.$urlPath = urlPath;
98
+ this.$uri = uri ?? urlPath;
81
99
  this.$params = params;
82
100
  this.$query = query;
83
101
  this.$fragment = fragment;
@@ -158,7 +176,15 @@ export class Router {
158
176
 
159
177
  /** Navigation guards */
160
178
  private _beforeEach: NavigationGuard | null = null;
161
- private _afterEach: AfterNavigationHook | null = null;
179
+ /**
180
+ * NHIỀU hook, không phải một.
181
+ *
182
+ * Trước đây đây là một slot duy nhất, và mỗi layout muốn biết "đã điều hướng
183
+ * xong" đều phải giành lấy nó: layout mới đăng ký đè hook của layout cũ, rồi
184
+ * layout cũ destroy lại xoá hook của layout mới → không còn hook nào. Đúng
185
+ * chuỗi docs → demo → docs. Hook giờ là tập hợp, `afterEach()` trả về hàm huỷ.
186
+ */
187
+ private afterHooks = new Set<AfterNavigationHook>();
162
188
 
163
189
  /** Caches */
164
190
  private routeCache: Map<string, RouteMatch | null> = new Map();
@@ -166,10 +192,13 @@ export class Router {
166
192
  /** State */
167
193
  private isStarted = false;
168
194
  private isNavigating = false;
195
+ private navigationSequence = 0;
196
+ private activeNavigationUrl: string | null = null;
169
197
 
170
198
  /** Bound handlers for cleanup */
171
199
  private _handlePopState: () => void;
172
200
  private _handleAutoNavigation: (e: MouseEvent) => void;
201
+ private _handleViewContextChange: (e: Event) => void;
173
202
 
174
203
  constructor(app?: any) {
175
204
  this.App = app || null;
@@ -179,12 +208,30 @@ export class Router {
179
208
 
180
209
  this._handlePopState = this.handlePopState.bind(this);
181
210
  this._handleAutoNavigation = this.handleAutoNavigation.bind(this);
211
+ this._handleViewContextChange = this.handleViewContextChange.bind(this);
182
212
  }
183
213
 
184
214
  // ─── Configuration ──────────────────────────────────────────
185
215
 
216
+ /**
217
+ * init — nạp config và wire các dependency.
218
+ * Gọi bởi RouteServiceProvider.boot() sau khi tất cả providers đã boot.
219
+ *
220
+ * Thực hiện:
221
+ * 1. configure() — load routes, mode, guards từ config
222
+ * 2. Wire ViewManager — lấy từ App.View nếu chưa set thủ công
223
+ */
186
224
  init(config: RouterConfig): this {
187
-
225
+ if (config && Object.keys(config).length > 0) {
226
+ this.configure(config);
227
+ }
228
+ // Auto-wire ViewManager nếu App đã có View registered
229
+ if (this.App && !this.viewManager) {
230
+ try {
231
+ const vm = (this.App as any).View ?? this.App.get?.('View') ?? null;
232
+ if (vm) this.viewManager = vm;
233
+ } catch (_) { /* View chưa sẵn sàng — sẽ fallback lúc handleRoute */ }
234
+ }
188
235
  return this;
189
236
  }
190
237
 
@@ -235,10 +282,71 @@ export class Router {
235
282
  }
236
283
 
237
284
  /**
238
- * Add multiple routes at once.
285
+ * Nối path con vào path cha. Con bắt đầu bằng '/' là ĐƯỜNG TUYỆT ĐỐI —
286
+ * bỏ qua prefix cha (lối thoát cho route lệch khỏi cây, như Vue Router).
287
+ * Con rỗng ('') = index route, trùng đúng path cha.
288
+ */
289
+ private static joinRoutePath(parent: string, child: string): string {
290
+ if (child.startsWith('/')) return child;
291
+ const base = parent.replace(/\/+$/, '');
292
+ if (!child) return base || '/';
293
+ return `${base}/${child}`.replace(/\/{2,}/g, '/');
294
+ }
295
+
296
+ /**
297
+ * Trải cây route thành bảng phẳng — bảng phẳng là thứ `matchRoute()` duyệt,
298
+ * và nó khớp THEO THỨ TỰ (first match wins), nên thứ tự emit ở đây chính là
299
+ * độ ưu tiên: giữ nguyên thứ tự khai báo để `/users/profile` đứng trước
300
+ * `/users/{id}` đúng như người viết mong đợi.
301
+ *
302
+ * Cha có `children` mà KHÔNG có `component` = nhóm thuần tuý (gom prefix +
303
+ * meta dùng chung), không sinh route nào cho chính nó.
304
+ *
305
+ * Index child (`path: ''`) sinh ra đúng path của cha → khi đó bỏ route
306
+ * riêng của cha, vì khai báo con là chỉ định cụ thể hơn cho URL đó.
307
+ */
308
+ private flattenRouteTree(
309
+ routes: RouteDefinition[],
310
+ parentPath = '',
311
+ parentMeta: Record<string, any> = {},
312
+ ): RouteDefinition[] {
313
+ const flat: RouteDefinition[] = [];
314
+
315
+ for (const route of routes) {
316
+ const { children, ...own } = route;
317
+ const path = Router.joinRoutePath(parentPath, route.path ?? '');
318
+ const meta = { ...parentMeta, ...(route.meta ?? {}) };
319
+
320
+ const flatChildren = children && children.length
321
+ ? this.flattenRouteTree(children, path, meta)
322
+ : [];
323
+
324
+ const component = route.component || route.view || '';
325
+ const coveredByIndexChild = flatChildren.some(c => c.path === path);
326
+ if (component && !coveredByIndexChild) {
327
+ flat.push({ ...own, path, meta });
328
+ }
329
+ flat.push(...flatChildren);
330
+ }
331
+
332
+ return flat;
333
+ }
334
+
335
+ /**
336
+ * Add multiple routes at once. Chấp nhận cây lồng qua `children` — mọi
337
+ * đường vào (`configure`, `replaceRoutes`) đều đi qua đây nên chỉ cần
338
+ * flatten một chỗ.
239
339
  */
240
340
  addRoutes(routes: RouteDefinition[]): this {
341
+ // Route KHÔNG có component (API, redirect, fallback) vẫn cần TÊN để
342
+ // sinh URL — chỉ không vào bảng khớp path. flattenRouteTree lọc chúng
343
+ // ra khỏi nhánh khớp, nên đăng ký tên riêng ở đây.
241
344
  for (const route of routes) {
345
+ if (route.name && !(route.component || route.view)) {
346
+ this.addRouteConfig(route);
347
+ }
348
+ }
349
+ for (const route of this.flattenRouteTree(routes)) {
242
350
  const component = route.component || route.view || '';
243
351
  this.addRoute(route.path, component, route.meta || {});
244
352
  if (route.name) {
@@ -248,6 +356,14 @@ export class Router {
248
356
  return this;
249
357
  }
250
358
 
359
+ /** Atomically replace the materialized route table for a new context revision. */
360
+ replaceRoutes(routes: RouteDefinition[]): this {
361
+ this.routes = [];
362
+ this.routeConfigs = {};
363
+ this.routeCache.clear();
364
+ return this.addRoutes(routes);
365
+ }
366
+
251
367
  /**
252
368
  * Configure router from a config object.
253
369
  */
@@ -255,10 +371,14 @@ export class Router {
255
371
  if (config.mode) this.mode = config.mode;
256
372
  if (config.base) this.base = config.base;
257
373
  if (config.defaultRoute) this.defaultRoute = config.defaultRoute;
258
- if (config.routes) this.addRoutes(config.routes);
259
- if (config.allRoutes) this.addRoutes(config.allRoutes);
374
+ // MỘT danh sách thôi. Server từng phát cả `routes` (chỉ route có
375
+ // component) lẫn `allRoutes` (đủ) — tập con của nhau, nên đăng ký cả
376
+ // hai là mỗi route vào bảng hai lần. `allRoutes` giữ lại làm bí danh
377
+ // cho payload cũ.
378
+ const routes = config.routes ?? config.allRoutes;
379
+ if (routes) this.addRoutes(routes);
260
380
  if (config.beforeEach) this._beforeEach = config.beforeEach;
261
- if (config.afterEach) this._afterEach = config.afterEach;
381
+ if (config.afterEach) this.afterEach(config.afterEach);
262
382
  return this;
263
383
  }
264
384
 
@@ -269,24 +389,21 @@ export class Router {
269
389
  return this;
270
390
  }
271
391
 
272
- afterEach(hook: AfterNavigationHook): this {
273
- this._afterEach = hook;
274
- return this;
392
+ /** Đăng ký hook chạy sau mỗi lần điều hướng. Trả về hàm HUỶ đăng ký. */
393
+ afterEach(hook: AfterNavigationHook): () => void {
394
+ this.afterHooks.add(hook);
395
+ return () => { this.afterHooks.delete(hook); };
275
396
  }
276
397
 
277
398
  // ─── Navigation ─────────────────────────────────────────────
278
399
 
279
400
  /**
280
401
  * Navigate to a URL path.
402
+ * History chỉ được cập nhật SAU khi guard cho phép (trong handleRoute) —
403
+ * guard chặn thì URL không đổi, tránh desync URL ↔ view.
281
404
  */
282
405
  navigate(path: string): void {
283
- if (this.mode === 'history') {
284
- window.history.pushState({}, '', path);
285
- } else {
286
- window.location.hash = path;
287
- }
288
- this.handleRoute(path);
289
- this.currentUri = path;
406
+ this.requestNavigation(path, 'push');
290
407
  }
291
408
 
292
409
  /** Alias for navigate */
@@ -310,13 +427,7 @@ export class Router {
310
427
  * Replace current history entry without adding to stack.
311
428
  */
312
429
  replace(path: string): void {
313
- if (this.mode === 'history') {
314
- window.history.replaceState({}, '', path);
315
- } else {
316
- window.location.hash = path;
317
- }
318
- this.handleRoute(path);
319
- this.currentUri = path;
430
+ this.requestNavigation(path, 'replace');
320
431
  }
321
432
 
322
433
  /**
@@ -446,10 +557,11 @@ export class Router {
446
557
  window.addEventListener('hashchange', this._handlePopState);
447
558
  }
448
559
  document.addEventListener('click', this._handleAutoNavigation);
560
+ window.addEventListener('saola:view-context', this._handleViewContextChange);
449
561
 
450
562
  // Handle initial route
451
563
  if (!skipInitial) {
452
- this.handleRoute(initialPath);
564
+ this.requestNavigation(initialPath, 'initial');
453
565
  }
454
566
 
455
567
  this.isStarted = true;
@@ -462,41 +574,168 @@ export class Router {
462
574
  window.removeEventListener('popstate', this._handlePopState);
463
575
  window.removeEventListener('hashchange', this._handlePopState);
464
576
  document.removeEventListener('click', this._handleAutoNavigation);
577
+ window.removeEventListener('saola:view-context', this._handleViewContextChange);
465
578
  this.isStarted = false;
466
579
  }
467
580
 
581
+ private handleViewContextChange(event: Event): void {
582
+ const state = (event as CustomEvent<Record<string, any>>).detail;
583
+ if (!state || state.changed !== true) return;
584
+
585
+ const vm = this.viewManager ?? this.App?.View;
586
+ const target = this.activeNavigationUrl || this.currentUri
587
+ || (typeof window !== 'undefined'
588
+ ? window.location.pathname + window.location.search + window.location.hash
589
+ : '/');
590
+
591
+ if (vm?.requiresReloadForViewContext?.(state)) {
592
+ vm.cancelNavigation?.();
593
+ this.reloadForViewContext(target, state);
594
+ return;
595
+ }
596
+
597
+ const applied = vm?.applyViewContext?.(state) ?? false;
598
+ if (!applied) return;
599
+
600
+ if (Array.isArray(state.routes)) {
601
+ this.replaceRoutes(state.routes);
602
+ }
603
+
604
+ // If a fetch discovered the change mid-navigation, requestNavigation
605
+ // queues the same target and invalidates the old render generation.
606
+ if (target) this.requestNavigation(target, 'replace');
607
+ }
608
+
609
+ /**
610
+ * Switch to a coherent server-rendered document when the view namespace
611
+ * changes. A short-lived token prevents a bad deployment from reloading the
612
+ * same URL/revision forever.
613
+ */
614
+ private reloadForViewContext(target: string, state: Record<string, any>): void {
615
+ if (typeof window === 'undefined') return;
616
+
617
+ const url = new URL(target || '/', window.location.href);
618
+ const revision = typeof state.revision === 'string' ? state.revision : 'unknown';
619
+ const token = `${revision}:${url.pathname}${url.search}`;
620
+ const storageKey = 'saola:view-context-reload';
621
+ const now = Date.now();
622
+
623
+ try {
624
+ const previousRaw = window.sessionStorage.getItem(storageKey);
625
+ const previous = previousRaw ? JSON.parse(previousRaw) : null;
626
+ if (previous?.token === token && typeof previous.at === 'number' && now - previous.at < 15_000) {
627
+ const message = 'Không thể đồng bộ view context sau khi tải lại. Vui lòng kiểm tra revision và asset đã triển khai.';
628
+ console.error(`[Router] ${message}`, { revision, target: url.href });
629
+ (this.viewManager ?? this.App?.View)?.showError?.(message, {
630
+ revision,
631
+ target: url.href,
632
+ });
633
+ return;
634
+ }
635
+ window.sessionStorage.setItem(storageKey, JSON.stringify({ token, at: now }));
636
+ } catch {
637
+ // Storage có thể bị chặn; coherence vẫn quan trọng hơn loop guard.
638
+ }
639
+
640
+ window.dispatchEvent(new CustomEvent('saola:view-context-reload', {
641
+ detail: { ...state, target: url.href },
642
+ }));
643
+ window.location.replace(url.href);
644
+ }
645
+
468
646
  /**
469
647
  * Full destroy — cleanup everything.
470
648
  */
471
649
  destroy(): void {
472
650
  this.stop();
651
+ this.navigationSequence++;
652
+ this.pendingNavigation = null;
653
+ this.activeNavigationUrl = null;
654
+ this.viewManager?.cancelNavigation?.();
473
655
  this.routes = [];
474
656
  this.routeConfigs = {};
475
657
  this.routeCache.clear();
476
658
  this._beforeEach = null;
477
- this._afterEach = null;
659
+ this.afterHooks.clear();
478
660
  this.currentRoute = null;
661
+ this.liveRegion?.remove();
662
+ this.liveRegion = null;
479
663
  Router.activeRoute = null;
480
664
  }
481
665
 
482
666
  // ─── Helper ────────────────────────────────
483
667
  getFullUrl(): string {
484
668
  if (typeof window === 'undefined') return '';
669
+ if (this.activeNavigationUrl) {
670
+ return new URL(this.activeNavigationUrl, window.location.href).href;
671
+ }
485
672
  return window.location.href;
486
673
  }
487
674
  // ─── Internal: Route Handling ────────────────────────────────
488
675
 
676
+ /** Kiểu navigation nội bộ — quyết định thao tác history + nav type xuống ViewManager */
677
+ private pendingNavigation: NavigationRequest | null = null;
678
+
679
+ /**
680
+ * Tách một URL string thành pathname / query string / fragment.
681
+ * PHẢI tách query + hash TRƯỚC khi match route — pattern chỉ match pathname.
682
+ */
683
+ private splitLocation(raw: string): { pathname: string; queryString: string; fragment: string } {
684
+ let rest = raw ?? '';
685
+ let fragment = '';
686
+ const hashIdx = rest.indexOf('#');
687
+ if (hashIdx !== -1) {
688
+ fragment = rest.slice(hashIdx + 1);
689
+ rest = rest.slice(0, hashIdx);
690
+ }
691
+ let queryString = '';
692
+ const queryIdx = rest.indexOf('?');
693
+ if (queryIdx !== -1) {
694
+ queryString = rest.slice(queryIdx + 1);
695
+ rest = rest.slice(0, queryIdx);
696
+ }
697
+ return { pathname: rest, queryString, fragment };
698
+ }
699
+
489
700
  /**
490
- * Core route handler — match route, run guards, mount view.
701
+ * Điểm vào duy nhất cho mọi navigation (navigate/replace/popstate/initial).
702
+ * Đang navigate dở → ghi nhận request MỚI NHẤT, xử lý sau khi xong
703
+ * (không drop im lặng như trước).
491
704
  */
492
- private async handleRoute(path: string): Promise<void> {
493
- if (this.isNavigating) return;
705
+ private requestNavigation(path: string, type: NavigationType): void {
706
+ const request: NavigationRequest = { id: ++this.navigationSequence, path, type };
707
+ if (this.isNavigating) {
708
+ this.pendingNavigation = request;
709
+ // Cho ViewManager thoát sớm khỏi fetch/render cũ. Request mới nhất
710
+ // sẽ được chạy trong finally, không commit DOM/history trung gian.
711
+ (this.viewManager ?? this.App?.View)?.cancelNavigation?.();
712
+ return;
713
+ }
714
+ void this.handleRoute(path, type, request.id);
715
+ }
716
+
717
+ /**
718
+ * Core route handler — prepare/guard/render trước, chỉ commit history +
719
+ * active route sau khi ViewManager đã mount/hydrate thành công.
720
+ */
721
+ private async handleRoute(
722
+ path: string,
723
+ type: NavigationType = 'push',
724
+ requestId: number = ++this.navigationSequence,
725
+ ): Promise<void> {
494
726
  this.isNavigating = true;
727
+ // Khai ngoài try: `finally` phải nhả được kể cả khi render ném giữa chừng.
728
+ let releaseHeight: () => void = () => {};
495
729
 
496
730
  try {
497
- const normalizedPath = this.normalizePath(path);
498
- const query = this.parseQuery(window.location.search);
499
- const fragment = window.location.hash.substring(1);
731
+ const { pathname, queryString, fragment } = this.splitLocation(path);
732
+ const normalizedPath = this.normalizePath(pathname);
733
+ // Request URI = path + query, KHÔNG gồm hash — cache key của ViewManager
734
+ const uri = queryString ? `${normalizedPath}?${queryString}` : normalizedPath;
735
+ const query = this.parseQuery(queryString);
736
+
737
+ // popstate/hashchange echo về đúng URI đang đứng → bỏ qua
738
+ if (type === 'pop' && uri === this.currentUri) return;
500
739
 
501
740
  const match = this.matchRoute(normalizedPath);
502
741
  if (!match) {
@@ -508,64 +747,208 @@ export class Router {
508
747
  const from = this.currentRoute;
509
748
 
510
749
  // Create ActiveRoute
511
- const activeRoute = new ActiveRoute(route, normalizedPath, params, query, fragment);
750
+ const activeRoute = new ActiveRoute(route, normalizedPath, params, query, fragment, uri);
512
751
 
513
- // Before guard
752
+ // Before guard — chạy TRƯỚC khi đụng vào history: guard chặn thì URL giữ nguyên
514
753
  if (this._beforeEach) {
515
754
  const allow = await this._beforeEach(route, from, normalizedPath);
516
755
  if (allow === false) return;
517
756
  }
757
+ if (requestId !== this.navigationSequence) return;
518
758
 
519
- // Update global state
520
- Router.activeRoute = activeRoute;
521
- this.currentRoute = activeRoute;
759
+ const historyTarget = fragment ? `${uri}#${fragment}` : uri;
760
+ this.activeNavigationUrl = historyTarget;
522
761
 
523
- // Mount view via ViewManager
762
+ // Mount view via ViewManager.
763
+ // Route ĐẦU TIÊN sau SSR: nếu view khớp entry server đã render →
764
+ // hydrateView (claim DOM) thay vì mountView. consumeSSRViewId() chỉ
765
+ // trả id 1 lần → các navigate sau là CSR (SPA takeover).
524
766
  const viewComponent = route.component || route.view;
767
+ let transitionSucceeded = true;
768
+ // View cũ bị gỡ trước khi cây mới dựng xong, nên tài liệu sập chiều
769
+ // cao trong vài frame — footer nhảy vọt lên, thanh cuộn biến mất,
770
+ // trình duyệt kẹp lại scrollY. Giữ chiều cao cũ tới khi thay xong.
771
+ releaseHeight = this.freezeDocumentHeight();
525
772
  if (viewComponent) {
526
- if (this.viewManager) {
527
- await this.viewManager.mountView(viewComponent, params, activeRoute);
528
- } else if (this.App?.View) {
529
- // Fallback: App.View is the ViewManager (registered on app)
530
- await this.App.View.mountView(viewComponent, params, activeRoute);
773
+ const vm = this.viewManager ?? this.App?.View;
774
+ if (vm) {
775
+ const ssrViewId = vm.consumeSSRViewId?.(viewComponent) ?? null;
776
+ let result: any;
777
+ if (ssrViewId) {
778
+ result = await vm.hydrateView(viewComponent, { __SSR_VIEW_ID__: ssrViewId, ...params }, activeRoute);
779
+ } else {
780
+ result = await vm.mountView(viewComponent, params, activeRoute, type === 'pop' ? 'pop' : 'push');
781
+ }
782
+ // mountView(null) có thể là duplicate no-op; xác nhận chain
783
+ // active đang thật sự thuộc URI đích trước khi commit Router.
784
+ transitionSucceeded = result != null
785
+ || vm.getCurrentView?.()?.__ctrl__?.urlPath === uri;
786
+ } else {
787
+ transitionSucceeded = false;
531
788
  }
532
789
  }
790
+ if (requestId !== this.navigationSequence) return;
791
+ if (!transitionSucceeded) {
792
+ if (type === 'pop') this.restoreUrlAfterFailedPop(from);
793
+ return;
794
+ }
533
795
 
534
- // After hook
535
- if (this._afterEach) {
536
- this._afterEach({ ...route, path: normalizedPath }, from);
796
+ // Commit point: từ đây URL, Router state và mounted view cùng
797
+ // đại diện cho một navigation duy nhất.
798
+ if (this.mode === 'history') {
799
+ if (type === 'push') window.history.pushState({}, '', historyTarget);
800
+ else if (type === 'replace') window.history.replaceState({}, '', historyTarget);
801
+ } else if (type === 'push' || type === 'replace') {
802
+ window.location.hash = historyTarget;
803
+ }
804
+
805
+ Router.activeRoute = activeRoute;
806
+ this.currentRoute = activeRoute;
807
+ this.currentUri = uri;
808
+ this.applyScroll(type, fragment);
809
+ this.announceNavigation(type);
810
+
811
+ // After hook — chạy trên BẢN SAO: hook được phép tự huỷ đăng ký trong
812
+ // lúc chạy. Bọc try/catch từng hook: một hook hỏng (view đã destroy
813
+ // chẳng hạn) không được phép làm chết điều hướng của tất cả phần còn lại.
814
+ for (const hook of [...this.afterHooks]) {
815
+ try {
816
+ hook({ ...route, path: normalizedPath }, from);
817
+ } catch (error) {
818
+ console.error('[Router] afterEach hook lỗi:', error);
819
+ }
537
820
  }
538
821
  } catch (error) {
539
822
  console.error('[Router] Navigation error:', error);
540
823
  } finally {
824
+ releaseHeight();
541
825
  this.isNavigating = false;
826
+ if (requestId === this.navigationSequence) this.activeNavigationUrl = null;
827
+ // Có navigation đến trong lúc đang xử lý → chạy request mới nhất
828
+ const pending = this.pendingNavigation;
829
+ this.pendingNavigation = null;
830
+ if (pending) {
831
+ void this.handleRoute(pending.path, pending.type, pending.id);
832
+ }
833
+ }
834
+ }
835
+
836
+ /** Pop đã đổi address bar; render fail thì đưa URL về chain còn active. */
837
+ private restoreUrlAfterFailedPop(from: ActiveRoute | null): void {
838
+ if (this.mode !== 'history' || !from) return;
839
+ const target = from.$fragment ? `${from.$uri}#${from.$fragment}` : from.$uri;
840
+ window.history.replaceState(window.history.state, '', target);
841
+ }
842
+
843
+ /**
844
+ * Ghim chiều cao tài liệu bằng đúng chiều cao hiện tại, trả về hàm nhả.
845
+ * Chỉ chạm `min-height` của body nên không đụng layout khi đã nhả.
846
+ */
847
+ private freezeDocumentHeight(): () => void {
848
+ if (typeof document === 'undefined' || !document.body) return () => {};
849
+ const body = document.body;
850
+ const previous = body.style.minHeight;
851
+ try {
852
+ body.style.minHeight = `${document.documentElement.scrollHeight}px`;
853
+ } catch {
854
+ return () => {};
855
+ }
856
+ return () => { try { body.style.minHeight = previous; } catch { /* noop */ } };
857
+ }
858
+
859
+ private applyScroll(type: NavigationType, fragment: string): void {
860
+ if (typeof window === 'undefined') return;
861
+ if (fragment) {
862
+ let id = fragment;
863
+ try { id = decodeURIComponent(fragment); } catch { /* malformed fragment */ }
864
+ const target = document.getElementById(id) || document.getElementsByName(id)[0];
865
+ target?.scrollIntoView?.();
866
+ return;
867
+ }
868
+ // PageCache tự khôi phục pop position; initial giữ vị trí SSR/browser.
869
+ if (type === 'push' || type === 'replace') {
870
+ // scrollTo(0,0) tuân theo `html { scroll-behavior: smooth }` nếu trang
871
+ // có đặt — biến cú nhảy về đầu thành cuộn chậm ~0.4s chạy SAU khi nội
872
+ // dung đã thay xong, nhìn như treo. Đổi route phải tức thì; smooth để
873
+ // dành cho anchor trong trang (nhánh fragment ở trên).
874
+ try {
875
+ window.scrollTo({ top: 0, left: 0, behavior: 'instant' as ScrollBehavior });
876
+ } catch {
877
+ // Engine cũ chưa biết 'instant' sẽ ném khi validate enum.
878
+ try { window.scrollTo(0, 0); } catch { /* non-browser/test runtime */ }
879
+ }
542
880
  }
543
881
  }
544
882
 
883
+ /** Live region dùng lại giữa các lần điều hướng — tạo lười, chỉ 1 node. */
884
+ private liveRegion: HTMLElement | null = null;
885
+
886
+ /**
887
+ * A11y sau khi điều hướng xong (SPA không tự làm được như full page load):
888
+ * 1. Đưa focus về container view — không làm thì bàn phím vẫn ở link cũ
889
+ * của trang TRƯỚC, Tab tiếp tục từ vị trí không còn tồn tại.
890
+ * 2. Đọc tên trang mới qua live region — screen reader không hề biết
891
+ * nội dung đã đổi vì document không reload.
892
+ *
893
+ * `preventScroll` để không phá `applyScroll()` vừa chạy. Bỏ qua `initial`:
894
+ * lần paint đầu/hydrate không được cướp focus khỏi thứ user đang thao tác.
895
+ */
896
+ private announceNavigation(type: NavigationType): void {
897
+ if (typeof document === 'undefined' || type === 'initial') return;
898
+
899
+ const container = (this.viewManager ?? this.App?.View)?.getContainer?.();
900
+ if (container instanceof HTMLElement) {
901
+ // tabindex=-1: focus được bằng script nhưng KHÔNG chen vào thứ tự Tab.
902
+ if (!container.hasAttribute('tabindex')) container.setAttribute('tabindex', '-1');
903
+ try { container.focus({ preventScroll: true }); } catch { /* jsdom/older browsers */ }
904
+ }
905
+
906
+ if (!this.liveRegion || !this.liveRegion.isConnected) {
907
+ const region = document.createElement('div');
908
+ region.setAttribute('aria-live', 'polite');
909
+ region.setAttribute('aria-atomic', 'true');
910
+ // Ẩn khỏi mắt nhưng KHÔNG display:none / visibility:hidden —
911
+ // hai cái đó làm screen reader bỏ qua luôn.
912
+ region.style.cssText =
913
+ 'position:absolute;width:1px;height:1px;margin:-1px;padding:0;'
914
+ + 'overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0';
915
+ document.body.appendChild(region);
916
+ this.liveRegion = region;
917
+ }
918
+ // Ghi ở frame sau: nội dung mới phải khác nội dung cũ thì AT mới đọc lại.
919
+ const label = document.title || this.currentUri || '';
920
+ this.liveRegion.textContent = '';
921
+ requestAnimationFrame(() => {
922
+ if (this.liveRegion) this.liveRegion.textContent = label;
923
+ });
924
+ }
925
+
545
926
  /**
546
927
  * Handle browser back/forward.
547
928
  */
548
929
  private handlePopState(): void {
549
930
  const path = this.mode === 'history'
550
- ? window.location.pathname + window.location.search
931
+ ? window.location.pathname + window.location.search + window.location.hash
551
932
  : window.location.hash.slice(1) || this.defaultRoute;
552
- this.handleRoute(path);
933
+ this.requestNavigation(path, 'pop');
553
934
  }
554
935
 
555
936
  /**
556
937
  * Auto-navigation: intercept clicks on <a>, [data-nav-link], [data-navigate].
557
938
  */
558
939
  private handleAutoNavigation(e: MouseEvent): void {
940
+ if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
559
941
  const target = e.target as HTMLElement;
942
+ if (!target?.closest) return;
560
943
 
561
944
  // 1. [data-nav-link] — highest priority
562
945
  const navLinkEl = target.closest('[data-nav-link]') as HTMLElement;
563
946
  if (navLinkEl) {
564
947
  if (navLinkEl.hasAttribute('data-nav-disabled')) return;
565
948
  const navPath = navLinkEl.getAttribute('data-nav-link');
566
- if (navPath && navPath.trim() !== '' && navPath !== this.currentUri) {
949
+ if (navPath && navPath.trim() !== '') {
567
950
  e.preventDefault();
568
- this.navigate(navPath);
951
+ if (navPath !== this.currentUri) this.navigate(navPath);
569
952
  return;
570
953
  }
571
954
  }
@@ -575,9 +958,9 @@ export class Router {
575
958
  if (navigateEl) {
576
959
  if (navigateEl.hasAttribute('data-nav-disabled')) return;
577
960
  const navPath = navigateEl.getAttribute('data-navigate');
578
- if (navPath && navPath.trim() !== '' && navPath !== this.currentUri) {
961
+ if (navPath && navPath.trim() !== '') {
579
962
  e.preventDefault();
580
- this.navigate(navPath);
963
+ if (navPath !== this.currentUri) this.navigate(navPath);
581
964
  return;
582
965
  }
583
966
  }
@@ -587,7 +970,8 @@ export class Router {
587
970
  if (!link) return;
588
971
 
589
972
  // Skip: target="_blank", disabled, special protocols
590
- if (link.target === '_blank') return;
973
+ if (link.target && link.target !== '_self') return;
974
+ if (link.hasAttribute('download')) return;
591
975
  if (link.dataset.nav === 'disabled' || link.dataset.nav === 'false') return;
592
976
  const href = link.href;
593
977
  if (href.startsWith('mailto:') || href.startsWith('tel:') || href.startsWith('javascript:')) return;
@@ -598,15 +982,18 @@ export class Router {
598
982
  const currentUrl = new URL(window.location.href);
599
983
  if (linkUrl.host !== currentUrl.host) return; // External
600
984
 
601
- const path = linkUrl.pathname + linkUrl.search;
602
- if (path === this.currentUri) return; // Same page
985
+ const path = linkUrl.pathname + linkUrl.search + linkUrl.hash;
986
+ // Link trỏ về đúng route đang đứng vẫn phải nuốt click. Thoát sớm mà
987
+ // không preventDefault là để browser tự điều hướng — reload cả tài
988
+ // liệu, mất sạch state SPA. Đây là logo và mục menu đang active.
603
989
  e.preventDefault();
990
+ if (path === this.currentUri && !linkUrl.hash) return; // Same page — no-op
604
991
  this.navigate(path);
605
992
  } catch {
606
993
  // Relative URL
607
994
  if (href && !href.startsWith('http') && !href.startsWith('//')) {
608
- if (href === this.currentUri) return;
609
995
  e.preventDefault();
996
+ if (href === this.currentUri) return; // Same page — no-op
610
997
  this.navigate(href);
611
998
  }
612
999
  }
@@ -615,7 +1002,8 @@ export class Router {
615
1002
  // ─── Internal: Pattern Matching ─────────────────────────────
616
1003
 
617
1004
  private matchRoute(path: string): RouteMatch | null {
618
- const normalizedPath = this.normalizePath(path);
1005
+ // Defensive: strip query/hash nếu caller truyền URI đầy đủ
1006
+ const normalizedPath = this.normalizePath(this.splitLocation(path).pathname);
619
1007
 
620
1008
  if (this.routeCache.has(normalizedPath)) {
621
1009
  return this.routeCache.get(normalizedPath)!;
@@ -681,7 +1069,9 @@ export class Router {
681
1069
  regexParts.push('\\/' + escaped);
682
1070
  }
683
1071
 
684
- const regex = new RegExp(`^${regexParts.join('')}$`);
1072
+ // Root path '/' → mọi segment rỗng → regexParts rỗng. Phải khớp '\/'
1073
+ // (nếu để '^$' thì route '/' không bao giờ match — bug gốc).
1074
+ const regex = new RegExp(`^${regexParts.join('') || '\\/'}$`);
685
1075
  const match = normalizedPath.match(regex);
686
1076
  if (!match) return null;
687
1077
 
@@ -714,12 +1104,16 @@ export class Router {
714
1104
  }
715
1105
 
716
1106
  private setActiveRouteForPath(path: string): void {
717
- const normalizedPath = this.normalizePath(path);
1107
+ const { pathname, queryString, fragment } = this.splitLocation(path);
1108
+ const normalizedPath = this.normalizePath(pathname);
718
1109
  const match = this.matchRoute(normalizedPath);
719
1110
  if (match) {
720
- const query = this.parseQuery(window.location.search);
721
- const fragment = window.location.hash.substring(1);
722
- const activeRoute = new ActiveRoute(match.route, normalizedPath, match.params, query, fragment);
1111
+ const uri = queryString ? `${normalizedPath}?${queryString}` : normalizedPath;
1112
+ const query = this.parseQuery(queryString || window.location.search);
1113
+ const activeRoute = new ActiveRoute(
1114
+ match.route, normalizedPath, match.params, query,
1115
+ fragment || window.location.hash.substring(1), uri
1116
+ );
723
1117
  Router.activeRoute = activeRoute;
724
1118
  this.currentRoute = activeRoute;
725
1119
  }