@livenetworks/ashlar 1.6.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (560) hide show
  1. package/LICENSE +641 -0
  2. package/README.md +202 -86
  3. package/{js → components}/COMPONENTS.md +1116 -1108
  4. package/{js → components}/index.js +81 -58
  5. package/{js → components}/ln-accordion/README.md +7 -6
  6. package/components/ln-accordion/ln-accordion-dev.scss +8 -0
  7. package/components/ln-accordion/ln-accordion.js +1 -0
  8. package/components/ln-accordion/ln-accordion.schema.json +30 -0
  9. package/components/ln-accordion/ln-accordion.scss +8 -0
  10. package/{js → components}/ln-accordion/src/ln-accordion.js +8 -2
  11. package/{js → components}/ln-ajax/README.md +171 -160
  12. package/components/ln-ajax/ln-ajax-dev.scss +8 -0
  13. package/components/ln-ajax/ln-ajax.js +1 -0
  14. package/components/ln-ajax/ln-ajax.schema.json +30 -0
  15. package/{js → components}/ln-ajax/src/ln-ajax.js +65 -30
  16. package/{js → components}/ln-api-connector/README.md +27 -5
  17. package/components/ln-api-connector/ln-api-connector.js +1 -0
  18. package/components/ln-api-connector/ln-api-connector.schema.json +96 -0
  19. package/components/ln-api-connector/src/connector-core.js +97 -0
  20. package/components/ln-api-connector/src/ln-api-connector.js +447 -0
  21. package/{js → components}/ln-api-queue/README.md +12 -9
  22. package/components/ln-api-queue/ln-api-queue.js +1 -0
  23. package/components/ln-api-queue/ln-api-queue.schema.json +34 -0
  24. package/{js → components}/ln-api-queue/src/ln-api-queue.js +33 -14
  25. package/{js → components}/ln-api-queue/src/queue-storage.js +7 -3
  26. package/{js → components}/ln-autoresize/README.md +129 -127
  27. package/components/ln-autoresize/ln-autoresize-dev.scss +8 -0
  28. package/components/ln-autoresize/ln-autoresize.js +1 -0
  29. package/components/ln-autoresize/ln-autoresize.schema.json +22 -0
  30. package/{js → components}/ln-autoresize/src/ln-autoresize.js +8 -1
  31. package/{js → components}/ln-autosave/README.md +127 -124
  32. package/components/ln-autosave/ln-autosave-dev.scss +13 -0
  33. package/components/ln-autosave/ln-autosave.js +1 -0
  34. package/components/ln-autosave/ln-autosave.schema.json +48 -0
  35. package/components/ln-autosave/src/autosave-model.js +34 -0
  36. package/{js → components}/ln-autosave/src/ln-autosave.js +65 -58
  37. package/components/ln-chart/README.md +145 -0
  38. package/components/ln-chart/ln-chart-dev.scss +20 -0
  39. package/components/ln-chart/ln-chart.js +1 -0
  40. package/components/ln-chart/ln-chart.schema.json +146 -0
  41. package/components/ln-chart/ln-chart.scss +8 -0
  42. package/components/ln-chart/src/chart-model.js +154 -0
  43. package/components/ln-chart/src/ln-chart.js +176 -0
  44. package/{js → components}/ln-circular-progress/README.md +15 -14
  45. package/components/ln-circular-progress/ln-circular-progress-dev.scss +8 -0
  46. package/components/ln-circular-progress/ln-circular-progress.js +1 -0
  47. package/components/ln-circular-progress/ln-circular-progress.schema.json +42 -0
  48. package/{js → components}/ln-circular-progress/src/ln-circular-progress.js +32 -50
  49. package/components/ln-confirm/README.md +216 -0
  50. package/components/ln-confirm/ln-confirm-dev.scss +24 -0
  51. package/components/ln-confirm/ln-confirm.js +1 -0
  52. package/components/ln-confirm/ln-confirm.schema.json +66 -0
  53. package/components/ln-confirm/ln-confirm.scss +24 -0
  54. package/{js → components}/ln-confirm/src/ln-confirm.js +180 -167
  55. package/{js → components}/ln-core/README.md +374 -149
  56. package/components/ln-core/attrs.js +273 -0
  57. package/components/ln-core/compare.js +40 -0
  58. package/components/ln-core/crypto.js +265 -0
  59. package/components/ln-core/date.js +50 -0
  60. package/components/ln-core/dom.js +115 -0
  61. package/components/ln-core/events.js +122 -0
  62. package/components/ln-core/form.js +136 -0
  63. package/{js → components}/ln-core/hash.js +149 -92
  64. package/components/ln-core/helpers.js +33 -0
  65. package/components/ln-core/http.js +48 -0
  66. package/components/ln-core/index.js +11 -0
  67. package/components/ln-core/lifecycle.js +403 -0
  68. package/components/ln-core/ln-core.schema.json +109 -0
  69. package/components/ln-core/locale.js +55 -0
  70. package/components/ln-core/matching.js +78 -0
  71. package/components/ln-core/number.js +125 -0
  72. package/components/ln-core/progress.js +30 -0
  73. package/components/ln-core/template.js +206 -0
  74. package/{js → components}/ln-core/window-cache.js +247 -213
  75. package/{js → components}/ln-couchdb-connector/README.md +7 -5
  76. package/components/ln-couchdb-connector/ln-couchdb-connector.js +1 -0
  77. package/components/ln-couchdb-connector/ln-couchdb-connector.schema.json +50 -0
  78. package/{js → components}/ln-couchdb-connector/src/ln-couchdb-connector.js +29 -43
  79. package/{js → components}/ln-data-coordinator/README.md +109 -73
  80. package/components/ln-data-coordinator/ln-data-coordinator-dev.scss +8 -0
  81. package/components/ln-data-coordinator/ln-data-coordinator.js +1 -0
  82. package/components/ln-data-coordinator/ln-data-coordinator.schema.json +196 -0
  83. package/components/ln-data-coordinator/src/data-read-policy.js +41 -0
  84. package/{js → components}/ln-data-coordinator/src/ln-data-coordinator.js +411 -154
  85. package/{js → components}/ln-data-coordinator/src/mutation-receipts.js +0 -5
  86. package/{js → components}/ln-data-store/README.md +42 -17
  87. package/components/ln-data-store/ln-data-store-dev.scss +13 -0
  88. package/components/ln-data-store/ln-data-store.js +1 -0
  89. package/components/ln-data-store/ln-data-store.schema.json +84 -0
  90. package/components/ln-data-store/src/data-store-model.js +155 -0
  91. package/{js → components}/ln-data-store/src/ln-data-store.js +407 -105
  92. package/components/ln-data-store/src/window-index.js +119 -0
  93. package/{js → components}/ln-date/README.md +17 -7
  94. package/{js → components}/ln-date/ln-date-dev.scss +14 -14
  95. package/components/ln-date/ln-date.js +1 -0
  96. package/components/ln-date/ln-date.schema.json +92 -0
  97. package/components/ln-date/src/date-model.js +186 -0
  98. package/components/ln-date/src/ln-date.js +486 -0
  99. package/components/ln-debug/README.md +132 -0
  100. package/components/ln-debug/ln-debug.js +1 -0
  101. package/components/ln-debug/ln-debug.schema.json +28 -0
  102. package/components/ln-debug/src/console-sink.js +19 -0
  103. package/components/ln-debug/src/debug-verifier.js +331 -0
  104. package/components/ln-debug/src/gate.js +95 -0
  105. package/components/ln-debug/src/generated-attributes.js +299 -0
  106. package/components/ln-debug/src/ln-debug.js +60 -0
  107. package/{js → components}/ln-dropdown/README.md +150 -140
  108. package/components/ln-dropdown/ln-dropdown-dev.scss +13 -0
  109. package/components/ln-dropdown/ln-dropdown.js +1 -0
  110. package/components/ln-dropdown/ln-dropdown.schema.json +90 -0
  111. package/components/ln-dropdown/ln-dropdown.scss +18 -0
  112. package/{js → components}/ln-dropdown/src/ln-dropdown.js +114 -11
  113. package/{js → components}/ln-editor/README.md +11 -9
  114. package/components/ln-editor/ln-editor-dev.scss +18 -0
  115. package/components/ln-editor/ln-editor.js +1 -0
  116. package/components/ln-editor/ln-editor.schema.json +58 -0
  117. package/components/ln-editor/ln-editor.scss +13 -0
  118. package/{js → components}/ln-editor/src/ln-editor.js +59 -11
  119. package/{js → components}/ln-external-links/README.md +10 -8
  120. package/components/ln-external-links/ln-external-links.js +1 -0
  121. package/components/ln-external-links/ln-external-links.schema.json +18 -0
  122. package/{js → components}/ln-external-links/src/ln-external-links.js +13 -11
  123. package/{js → components}/ln-fill/README.md +6 -8
  124. package/components/ln-fill/ln-fill.js +1 -0
  125. package/components/ln-fill/ln-fill.schema.json +23 -0
  126. package/{js → components}/ln-fill/src/ln-fill.js +7 -1
  127. package/components/ln-filter/README.md +131 -0
  128. package/components/ln-filter/ln-filter-dev.scss +13 -0
  129. package/components/ln-filter/ln-filter.js +1 -0
  130. package/components/ln-filter/ln-filter.schema.json +153 -0
  131. package/components/ln-filter/src/filter-model.js +185 -0
  132. package/components/ln-filter/src/ln-filter.js +435 -0
  133. package/{js → components}/ln-form/README.md +260 -264
  134. package/components/ln-form/ln-form-dev.scss +8 -0
  135. package/components/ln-form/ln-form.js +1 -0
  136. package/components/ln-form/ln-form.schema.json +45 -0
  137. package/{js → components}/ln-form/src/ln-form.js +107 -99
  138. package/{js → components}/ln-http/README.md +11 -6
  139. package/components/ln-http/ln-http.js +1 -0
  140. package/components/ln-http/ln-http.schema.json +6 -0
  141. package/components/ln-http/src/http-core.js +51 -0
  142. package/{js → components}/ln-http/src/ln-http.js +28 -33
  143. package/{js → components}/ln-icon/README.md +16 -9
  144. package/components/ln-icon/ln-icon.schema.json +6 -0
  145. package/components/ln-include/README.md +75 -0
  146. package/components/ln-include/ln-include-dev.scss +13 -0
  147. package/components/ln-include/ln-include.js +1 -0
  148. package/components/ln-include/ln-include.schema.json +23 -0
  149. package/components/ln-include/src/ln-include.js +111 -0
  150. package/components/ln-key/README.md +130 -0
  151. package/components/ln-key/ln-key-dev.scss +18 -0
  152. package/components/ln-key/ln-key.js +1 -0
  153. package/components/ln-key/ln-key.schema.json +56 -0
  154. package/components/ln-key/src/key-model.js +146 -0
  155. package/components/ln-key/src/ln-key.js +201 -0
  156. package/{js → components}/ln-link/README.md +7 -9
  157. package/components/ln-link/ln-link-dev.scss +8 -0
  158. package/components/ln-link/ln-link.js +1 -0
  159. package/components/ln-link/ln-link.schema.json +22 -0
  160. package/{js → components}/ln-link/src/ln-link.js +16 -11
  161. package/components/ln-list/README.md +152 -0
  162. package/components/ln-list/ln-list-dev.scss +8 -0
  163. package/components/ln-list/ln-list.js +1 -0
  164. package/components/ln-list/ln-list.schema.json +166 -0
  165. package/components/ln-list/ln-list.scss +25 -0
  166. package/{js → components}/ln-list/src/ln-list.js +1301 -1216
  167. package/{js → components}/ln-modal/README.md +13 -10
  168. package/components/ln-modal/ln-modal-dev.scss +18 -0
  169. package/components/ln-modal/ln-modal.js +1 -0
  170. package/components/ln-modal/ln-modal.schema.json +54 -0
  171. package/components/ln-modal/ln-modal.scss +51 -0
  172. package/{js → components}/ln-modal/src/ln-modal.js +164 -137
  173. package/{js → components}/ln-nav/README.md +11 -11
  174. package/components/ln-nav/ln-nav.js +1 -0
  175. package/components/ln-nav/ln-nav.schema.json +25 -0
  176. package/{js → components}/ln-nav/src/ln-nav.js +25 -14
  177. package/components/ln-number/README.md +205 -0
  178. package/{js → components}/ln-number/ln-number-dev.scss +13 -13
  179. package/components/ln-number/ln-number.js +1 -0
  180. package/components/ln-number/ln-number.schema.json +63 -0
  181. package/components/ln-number/src/ln-number.js +400 -0
  182. package/components/ln-number/src/number-model.js +31 -0
  183. package/components/ln-obfuscator/README.md +60 -0
  184. package/components/ln-obfuscator/ln-obfuscator.js +1 -0
  185. package/components/ln-obfuscator/ln-obfuscator.schema.json +49 -0
  186. package/components/ln-obfuscator/src/ln-obfuscator.js +159 -0
  187. package/components/ln-obfuscator/src/obfuscator-model.js +250 -0
  188. package/{js → components}/ln-options/README.md +3 -1
  189. package/components/ln-options/ln-options-dev.scss +13 -0
  190. package/components/ln-options/ln-options.js +1 -0
  191. package/components/ln-options/ln-options.schema.json +41 -0
  192. package/{js → components}/ln-options/src/ln-options.js +13 -5
  193. package/components/ln-persist/README.md +151 -0
  194. package/components/ln-persist/ln-persist.js +1 -0
  195. package/components/ln-persist/ln-persist.schema.json +28 -0
  196. package/components/ln-persist/src/ln-persist.js +83 -0
  197. package/components/ln-picklist/README.md +230 -0
  198. package/components/ln-picklist/ln-picklist-dev.scss +22 -0
  199. package/components/ln-picklist/ln-picklist.js +1 -0
  200. package/components/ln-picklist/ln-picklist.schema.json +49 -0
  201. package/components/ln-picklist/src/ln-picklist.js +228 -0
  202. package/{js → components}/ln-popover/README.md +164 -162
  203. package/components/ln-popover/ln-popover-dev.scss +13 -0
  204. package/components/ln-popover/ln-popover.js +1 -0
  205. package/components/ln-popover/ln-popover.schema.json +82 -0
  206. package/components/ln-popover/ln-popover.scss +18 -0
  207. package/{js → components}/ln-popover/src/ln-popover.js +42 -36
  208. package/{js → components}/ln-progress/README.md +410 -413
  209. package/components/ln-progress/ln-progress-dev.scss +8 -0
  210. package/components/ln-progress/ln-progress.js +1 -0
  211. package/components/ln-progress/ln-progress.schema.json +40 -0
  212. package/components/ln-progress/src/ln-progress.js +91 -0
  213. package/components/ln-progress/src/progress-model.js +20 -0
  214. package/{js → components}/ln-router/README.md +443 -432
  215. package/components/ln-router/ln-router-dev.scss +13 -0
  216. package/components/ln-router/ln-router.js +1 -0
  217. package/components/ln-router/ln-router.schema.json +68 -0
  218. package/{js → components}/ln-router/src/ln-router.js +585 -539
  219. package/components/ln-router/src/router-model.js +50 -0
  220. package/components/ln-scroll/README.md +45 -0
  221. package/components/ln-scroll/ln-scroll.js +1 -0
  222. package/components/ln-scroll/ln-scroll.schema.json +85 -0
  223. package/components/ln-scroll/src/ln-scroll.js +150 -0
  224. package/components/ln-search/README.md +188 -0
  225. package/components/ln-search/ln-search-dev.scss +13 -0
  226. package/components/ln-search/ln-search.js +1 -0
  227. package/components/ln-search/ln-search.schema.json +103 -0
  228. package/components/ln-search/src/ln-search.js +330 -0
  229. package/{js → components}/ln-slug/README.md +6 -3
  230. package/components/ln-slug/ln-slug-dev.scss +18 -0
  231. package/components/ln-slug/ln-slug.js +1 -0
  232. package/components/ln-slug/ln-slug.schema.json +23 -0
  233. package/{js → components}/ln-slug/src/ln-slug.js +20 -13
  234. package/components/ln-slug/src/slug-model.js +21 -0
  235. package/{js → components}/ln-sort/README.md +178 -170
  236. package/{js → components}/ln-sort/ln-sort-dev.scss +8 -8
  237. package/components/ln-sort/ln-sort.js +1 -0
  238. package/components/ln-sort/ln-sort.schema.json +104 -0
  239. package/components/ln-sort/src/ln-sort.js +321 -0
  240. package/components/ln-sort/src/sort-model.js +117 -0
  241. package/{js → components}/ln-sortable/README.md +7 -11
  242. package/components/ln-sortable/ln-sortable-dev.scss +13 -0
  243. package/components/ln-sortable/ln-sortable.js +1 -0
  244. package/components/ln-sortable/ln-sortable.schema.json +36 -0
  245. package/{js → components}/ln-sortable/src/ln-sortable.js +8 -13
  246. package/{js → components}/ln-stat/README.md +3 -1
  247. package/components/ln-stat/ln-stat-dev.scss +8 -0
  248. package/components/ln-stat/ln-stat.js +1 -0
  249. package/components/ln-stat/ln-stat.schema.json +32 -0
  250. package/{js → components}/ln-stat/src/ln-stat.js +14 -18
  251. package/components/ln-stat/src/stat-model.js +28 -0
  252. package/{js → components}/ln-table/README.md +37 -24
  253. package/{js → components}/ln-table/ln-table-dev.scss +17 -17
  254. package/components/ln-table/ln-table.js +1 -0
  255. package/components/ln-table/ln-table.schema.json +211 -0
  256. package/components/ln-table/ln-table.scss +15 -0
  257. package/{js → components}/ln-table/src/ln-table.js +1365 -1493
  258. package/components/ln-table/src/table-model.js +98 -0
  259. package/components/ln-table-coordinator/README.md +61 -0
  260. package/components/ln-table-coordinator/ln-table-coordinator.js +1 -0
  261. package/components/ln-table-coordinator/ln-table-coordinator.schema.json +93 -0
  262. package/components/ln-table-coordinator/src/ln-table-coordinator.js +193 -0
  263. package/{js → components}/ln-tabs/README.md +32 -14
  264. package/components/ln-tabs/ln-tabs-dev.scss +24 -0
  265. package/components/ln-tabs/ln-tabs.js +1 -0
  266. package/components/ln-tabs/ln-tabs.schema.json +75 -0
  267. package/components/ln-tabs/ln-tabs.scss +8 -0
  268. package/components/ln-tabs/src/ln-tabs.js +207 -0
  269. package/components/ln-tabs/src/tabs-model.js +82 -0
  270. package/{js → components}/ln-time/README.md +96 -95
  271. package/components/ln-time/ln-time-dev.scss +13 -0
  272. package/components/ln-time/ln-time.js +1 -0
  273. package/components/ln-time/ln-time.schema.json +38 -0
  274. package/{js → components}/ln-time/src/ln-time.js +206 -223
  275. package/components/ln-time/src/time-model.js +64 -0
  276. package/{js → components}/ln-toast/README.md +23 -5
  277. package/components/ln-toast/ln-toast-dev.scss +8 -0
  278. package/components/ln-toast/ln-toast.js +1 -0
  279. package/components/ln-toast/ln-toast.schema.json +65 -0
  280. package/components/ln-toast/ln-toast.scss +59 -0
  281. package/{js → components}/ln-toast/src/ln-toast.js +233 -232
  282. package/{js → components}/ln-toggle/README.md +198 -196
  283. package/{js → components}/ln-toggle/ln-toggle-dev.scss +13 -13
  284. package/components/ln-toggle/ln-toggle.js +1 -0
  285. package/components/ln-toggle/ln-toggle.schema.json +52 -0
  286. package/components/ln-toggle/ln-toggle.scss +37 -0
  287. package/{js → components}/ln-toggle/src/ln-toggle.js +61 -46
  288. package/{js → components}/ln-tooltip/README.md +6 -6
  289. package/{js → components}/ln-tooltip/ln-tooltip-dev.scss +9 -9
  290. package/components/ln-tooltip/ln-tooltip.js +1 -0
  291. package/components/ln-tooltip/ln-tooltip.schema.json +86 -0
  292. package/components/ln-tooltip/ln-tooltip.scss +9 -0
  293. package/{js → components}/ln-tooltip/src/ln-tooltip.js +14 -3
  294. package/{js → components}/ln-translations/README.md +3 -1
  295. package/components/ln-translations/ln-translations-dev.scss +13 -0
  296. package/components/ln-translations/ln-translations.js +1 -0
  297. package/components/ln-translations/ln-translations.schema.json +120 -0
  298. package/{js → components}/ln-translations/src/ln-translations.js +25 -10
  299. package/components/ln-ui-coordinator/README.md +101 -0
  300. package/components/ln-ui-coordinator/ln-ui-coordinator-dev.scss +8 -0
  301. package/components/ln-ui-coordinator/ln-ui-coordinator.js +1 -0
  302. package/components/ln-ui-coordinator/ln-ui-coordinator.schema.json +60 -0
  303. package/{js/ln-modal-coordinator/src/ln-modal-coordinator.js → components/ln-ui-coordinator/src/ln-ui-coordinator.js} +403 -345
  304. package/components/ln-upload/README.md +133 -0
  305. package/components/ln-upload/ln-upload-dev.scss +18 -0
  306. package/components/ln-upload/ln-upload.js +1 -0
  307. package/components/ln-upload/ln-upload.schema.json +193 -0
  308. package/components/ln-upload/ln-upload.scss +20 -0
  309. package/components/ln-upload/src/ln-upload.js +652 -0
  310. package/components/ln-upload/src/upload-model.js +78 -0
  311. package/{js → components}/ln-validate/README.md +182 -180
  312. package/{js → components}/ln-validate/ln-validate-dev.scss +15 -15
  313. package/components/ln-validate/ln-validate.js +1 -0
  314. package/components/ln-validate/ln-validate.schema.json +50 -0
  315. package/components/ln-validate/ln-validate.scss +7 -0
  316. package/{js → components}/ln-validate/src/ln-validate.js +23 -45
  317. package/components/ln-validate/src/validate-model.js +64 -0
  318. package/components/ln-websocket-connector/README.md +166 -0
  319. package/components/ln-websocket-connector/ln-websocket-connector.js +1 -0
  320. package/components/ln-websocket-connector/ln-websocket-connector.schema.json +29 -0
  321. package/components/ln-websocket-connector/src/ln-websocket-connector.js +279 -0
  322. package/ln-ashlar.html-data.json +1796 -0
  323. package/package.json +84 -61
  324. package/{scss → theme}/base/_global.scss +1 -2
  325. package/{scss → theme}/base/_reset.scss +1 -1
  326. package/{scss → theme}/base/_typography.scss +2 -0
  327. package/theme/brand.css +73 -0
  328. package/{scss → theme}/components/_app-shell.scss +8 -0
  329. package/theme/components/_button.scss +24 -0
  330. package/theme/components/_chart.scss +5 -0
  331. package/theme/components/_confirm.scss +6 -0
  332. package/{scss → theme}/components/_date.scss +30 -22
  333. package/{scss → theme}/components/_dropdown.scss +6 -1
  334. package/theme/components/_form.scss +140 -0
  335. package/{scss → theme}/components/_layout.scss +6 -0
  336. package/{scss → theme}/components/_ln-table.scss +1 -6
  337. package/theme/components/_picklist.scss +3 -0
  338. package/{scss → theme}/components/_sort.scss +10 -10
  339. package/theme/components/_status-badge.scss +6 -0
  340. package/{scss → theme}/components/_table.scss +16 -0
  341. package/{scss → theme}/components/_tabs.scss +0 -4
  342. package/theme/components/_toast.scss +48 -0
  343. package/{scss → theme}/components/_toggle.scss +1 -1
  344. package/theme/components/_upload.scss +45 -0
  345. package/{scss → theme}/config/_breakpoints.scss +1 -1
  346. package/theme/config/_contrast.scss +128 -0
  347. package/{scss → theme}/config/_density.scss +191 -205
  348. package/theme/config/_palette.scss +274 -0
  349. package/theme/config/_theme.scss +293 -0
  350. package/{scss → theme}/config/_tokens.scss +285 -441
  351. package/{scss → theme}/config/mixins/_accordion.scss +1 -1
  352. package/{scss → theme}/config/mixins/_alert.scss +1 -0
  353. package/{scss → theme}/config/mixins/_app-shell.scss +4 -2
  354. package/{scss → theme}/config/mixins/_borders.scss +2 -0
  355. package/theme/config/mixins/_btn.scss +175 -0
  356. package/{scss → theme}/config/mixins/_card.scss +55 -46
  357. package/theme/config/mixins/_chart.scss +82 -0
  358. package/{scss → theme}/config/mixins/_collapsible.scss +2 -1
  359. package/{scss → theme}/config/mixins/_colors.scss +8 -14
  360. package/{scss → theme}/config/mixins/_diagnostics.scss +48 -48
  361. package/theme/config/mixins/_dropdown.scss +169 -0
  362. package/{scss → theme}/config/mixins/_editor.scss +0 -7
  363. package/{scss → theme}/config/mixins/_filter.scss +47 -47
  364. package/{scss → theme}/config/mixins/_footer.scss +1 -1
  365. package/{scss → theme}/config/mixins/_form.scss +49 -18
  366. package/{scss → theme}/config/mixins/_index.scss +62 -61
  367. package/{scss → theme}/config/mixins/_layout.scss +6 -2
  368. package/{scss → theme}/config/mixins/_ln-table.scss +31 -10
  369. package/{scss → theme}/config/mixins/_loader.scss +1 -1
  370. package/{scss → theme}/config/mixins/_modal.scss +145 -149
  371. package/{scss → theme}/config/mixins/_motion.scss +13 -0
  372. package/{scss → theme}/config/mixins/_nav.scss +7 -7
  373. package/theme/config/mixins/_picklist.scss +66 -0
  374. package/theme/config/mixins/_popover.scss +96 -0
  375. package/{scss → theme}/config/mixins/_sidebar.scss +7 -4
  376. package/{scss → theme}/config/mixins/_sort.scss +43 -42
  377. package/{scss → theme}/config/mixins/_status-badge.scss +15 -2
  378. package/{scss → theme}/config/mixins/_table.scss +25 -14
  379. package/{scss → theme}/config/mixins/_tabs.scss +1 -1
  380. package/{scss → theme}/config/mixins/_toast.scss +174 -179
  381. package/{scss → theme}/config/mixins/_tooltip.scss +2 -2
  382. package/{scss → theme}/config/mixins/_typography.scss +16 -5
  383. package/{scss → theme}/config/mixins/_upload.scss +8 -6
  384. package/theme/ln-ashlar-core.scss +24 -0
  385. package/theme/ln-ashlar-dev.scss +66 -0
  386. package/theme/ln-ashlar-landing.scss +56 -0
  387. package/{scss/ln-ashlar.scss → theme/ln-ashlar-theme.scss} +63 -69
  388. package/theme/ln-ashlar.scss +8 -0
  389. package/theme/presentation/_bento.scss +151 -0
  390. package/theme/presentation/_cta.scss +111 -0
  391. package/theme/presentation/_effects.scss +216 -0
  392. package/theme/presentation/_features.scss +108 -0
  393. package/theme/presentation/_footer.scss +129 -0
  394. package/theme/presentation/_hero.scss +158 -0
  395. package/theme/presentation/_layout.scss +170 -0
  396. package/theme/presentation/_navbar.scss +140 -0
  397. package/theme/presentation/_pricing.scss +262 -0
  398. package/theme/presentation/_stats.scss +87 -0
  399. package/theme/presentation/_testimonials.scss +124 -0
  400. package/theme/presentation/_tokens.scss +39 -0
  401. package/theme/presentation/_typography.scss +163 -0
  402. package/{scss → theme}/utilities/_animations.scss +38 -0
  403. package/{scss → theme}/utilities/_utilities.scss +6 -0
  404. package/web-types.json +2312 -0
  405. package/js/ln-accordion/ln-accordion.js +0 -1
  406. package/js/ln-ajax/ln-ajax.js +0 -1
  407. package/js/ln-api-connector/ln-api-connector.js +0 -1
  408. package/js/ln-api-connector/src/ln-api-connector.js +0 -368
  409. package/js/ln-api-queue/ln-api-queue.js +0 -1
  410. package/js/ln-autoresize/ln-autoresize.js +0 -1
  411. package/js/ln-autosave/ln-autosave.js +0 -1
  412. package/js/ln-circular-progress/ln-circular-progress.js +0 -1
  413. package/js/ln-confirm/README.md +0 -162
  414. package/js/ln-confirm/_ln-confirm.scss +0 -24
  415. package/js/ln-confirm/ln-confirm.js +0 -1
  416. package/js/ln-core/crypto.js +0 -83
  417. package/js/ln-core/helpers.js +0 -788
  418. package/js/ln-core/index.js +0 -7
  419. package/js/ln-core/persist.js +0 -97
  420. package/js/ln-couchdb-connector/ln-couchdb-connector.js +0 -1
  421. package/js/ln-data-coordinator/ln-data-coordinator.js +0 -1
  422. package/js/ln-data-coordinator/src/data-read-policy.js +0 -18
  423. package/js/ln-data-store/ln-data-store.js +0 -1
  424. package/js/ln-date/ln-date.js +0 -1
  425. package/js/ln-date/src/ln-date.js +0 -603
  426. package/js/ln-debug/README.md +0 -58
  427. package/js/ln-debug/ln-debug.js +0 -1
  428. package/js/ln-debug/src/ln-debug.js +0 -19
  429. package/js/ln-dropdown/ln-dropdown.js +0 -1
  430. package/js/ln-dropdown/ln-dropdown.scss +0 -15
  431. package/js/ln-editor/ln-editor.js +0 -1
  432. package/js/ln-editor/ln-editor.scss +0 -43
  433. package/js/ln-external-links/ln-external-links.js +0 -1
  434. package/js/ln-fill/ln-fill.js +0 -1
  435. package/js/ln-filter/README.md +0 -189
  436. package/js/ln-filter/ln-filter-dev.scss +0 -8
  437. package/js/ln-filter/ln-filter.js +0 -1
  438. package/js/ln-filter/src/ln-filter.js +0 -419
  439. package/js/ln-form/ln-form.js +0 -1
  440. package/js/ln-http/ln-http.js +0 -1
  441. package/js/ln-link/ln-link.js +0 -1
  442. package/js/ln-list/README.md +0 -129
  443. package/js/ln-list/ln-list.js +0 -1
  444. package/js/ln-list/ln-list.scss +0 -28
  445. package/js/ln-modal/ln-modal-dev.scss +0 -13
  446. package/js/ln-modal/ln-modal.js +0 -1
  447. package/js/ln-modal/ln-modal.scss +0 -35
  448. package/js/ln-modal-coordinator/README.md +0 -99
  449. package/js/ln-modal-coordinator/ln-modal-coordinator-dev.scss +0 -8
  450. package/js/ln-modal-coordinator/ln-modal-coordinator.js +0 -1
  451. package/js/ln-nav/ln-nav.js +0 -1
  452. package/js/ln-number/README.md +0 -149
  453. package/js/ln-number/ln-number.js +0 -1
  454. package/js/ln-number/src/ln-number.js +0 -443
  455. package/js/ln-options/ln-options.js +0 -1
  456. package/js/ln-popover/ln-popover.js +0 -1
  457. package/js/ln-progress/ln-progress.js +0 -1
  458. package/js/ln-progress/src/ln-progress.js +0 -98
  459. package/js/ln-router/ln-router.js +0 -1
  460. package/js/ln-search/README.md +0 -158
  461. package/js/ln-search/ln-search-dev.scss +0 -8
  462. package/js/ln-search/ln-search.js +0 -1
  463. package/js/ln-search/src/ln-search.js +0 -117
  464. package/js/ln-slug/ln-slug.js +0 -1
  465. package/js/ln-sort/ln-sort.js +0 -1
  466. package/js/ln-sort/src/ln-sort.js +0 -178
  467. package/js/ln-sortable/ln-sortable.js +0 -1
  468. package/js/ln-stat/ln-stat.js +0 -1
  469. package/js/ln-table/ln-table.js +0 -1
  470. package/js/ln-table/ln-table.scss +0 -11
  471. package/js/ln-table-coordinator/README.md +0 -73
  472. package/js/ln-table-coordinator/ln-table-coordinator.js +0 -1
  473. package/js/ln-table-coordinator/src/ln-table-coordinator.js +0 -160
  474. package/js/ln-tabs/ln-tabs-dev.scss +0 -18
  475. package/js/ln-tabs/ln-tabs.js +0 -1
  476. package/js/ln-tabs/src/ln-tabs.js +0 -188
  477. package/js/ln-time/ln-time.js +0 -1
  478. package/js/ln-toast/ln-toast.js +0 -1
  479. package/js/ln-toggle/ln-toggle.js +0 -1
  480. package/js/ln-tooltip/ln-tooltip.js +0 -1
  481. package/js/ln-tooltip/ln-tooltip.scss +0 -9
  482. package/js/ln-translations/ln-translations.js +0 -1
  483. package/js/ln-upload/README.md +0 -255
  484. package/js/ln-upload/ln-upload.js +0 -1
  485. package/js/ln-upload/ln-upload.scss +0 -20
  486. package/js/ln-upload/src/ln-upload.js +0 -423
  487. package/js/ln-validate/ln-validate.js +0 -1
  488. package/scss/components/_button.scss +0 -20
  489. package/scss/components/_confirm.scss +0 -5
  490. package/scss/components/_form.scss +0 -107
  491. package/scss/components/_status-badge.scss +0 -4
  492. package/scss/components/_toast.scss +0 -68
  493. package/scss/components/_upload.scss +0 -51
  494. package/scss/config/_theme.scss +0 -216
  495. package/scss/config/mixins/_btn.scss +0 -179
  496. package/scss/config/mixins/_confirm.scss +0 -31
  497. package/scss/config/mixins/_dropdown.scss +0 -141
  498. package/scss/config/mixins/_popover.scss +0 -64
  499. package/scss/ln-ashlar-dev.scss +0 -39
  500. /package/{js → components}/ln-core/positioning.js +0 -0
  501. /package/{js → components}/ln-core/reactive.js +0 -0
  502. /package/{js → components}/ln-filter/ln-filter.scss +0 -0
  503. /package/{js → components}/ln-icon/ln-icon.js +0 -0
  504. /package/{js → components}/ln-icon/src/ln-icon.js +0 -0
  505. /package/{js → components}/ln-search/ln-search.scss +0 -0
  506. /package/{js → components}/ln-toast/template.html +0 -0
  507. /package/{scss → theme}/components/_accordion.scss +0 -0
  508. /package/{scss → theme}/components/_ajax.scss +0 -0
  509. /package/{scss → theme}/components/_alert.scss +0 -0
  510. /package/{scss → theme}/components/_avatar.scss +0 -0
  511. /package/{scss → theme}/components/_breadcrumbs.scss +0 -0
  512. /package/{scss → theme}/components/_card.scss +0 -0
  513. /package/{scss → theme}/components/_chip.scss +0 -0
  514. /package/{scss → theme}/components/_circular-progress.scss +0 -0
  515. /package/{scss → theme}/components/_editor.scss +0 -0
  516. /package/{scss → theme}/components/_empty-state.scss +0 -0
  517. /package/{scss → theme}/components/_filter.scss +0 -0
  518. /package/{scss → theme}/components/_link.scss +0 -0
  519. /package/{scss → theme}/components/_loader.scss +0 -0
  520. /package/{scss → theme}/components/_modal.scss +0 -0
  521. /package/{scss → theme}/components/_nav.scss +0 -0
  522. /package/{scss → theme}/components/_page-header.scss +0 -0
  523. /package/{scss → theme}/components/_popover.scss +0 -0
  524. /package/{scss → theme}/components/_progress.scss +0 -0
  525. /package/{scss → theme}/components/_prose.scss +0 -0
  526. /package/{scss → theme}/components/_scrollbar.scss +0 -0
  527. /package/{scss → theme}/components/_sections.scss +0 -0
  528. /package/{scss → theme}/components/_sidebar.scss +0 -0
  529. /package/{scss → theme}/components/_stat-card.scss +0 -0
  530. /package/{scss → theme}/components/_stepper.scss +0 -0
  531. /package/{scss → theme}/components/_timeline.scss +0 -0
  532. /package/{scss → theme}/components/_tooltip.scss +0 -0
  533. /package/{scss → theme}/components/_translations.scss +0 -0
  534. /package/{scss → theme}/config/_icons.scss +0 -0
  535. /package/{scss → theme}/config/_mixins.scss +0 -0
  536. /package/{scss → theme}/config/mixins/_ajax.scss +0 -0
  537. /package/{scss → theme}/config/mixins/_avatar.scss +0 -0
  538. /package/{scss → theme}/config/mixins/_breadcrumbs.scss +0 -0
  539. /package/{scss → theme}/config/mixins/_breakpoints.scss +0 -0
  540. /package/{scss → theme}/config/mixins/_chip.scss +0 -0
  541. /package/{scss → theme}/config/mixins/_circular-progress.scss +0 -0
  542. /package/{scss → theme}/config/mixins/_display.scss +0 -0
  543. /package/{scss → theme}/config/mixins/_empty-state.scss +0 -0
  544. /package/{scss → theme}/config/mixins/_focus.scss +0 -0
  545. /package/{scss → theme}/config/mixins/_interaction.scss +0 -0
  546. /package/{scss → theme}/config/mixins/_kbd.scss +0 -0
  547. /package/{scss → theme}/config/mixins/_link.scss +0 -0
  548. /package/{scss → theme}/config/mixins/_page-header.scss +0 -0
  549. /package/{scss → theme}/config/mixins/_position.scss +0 -0
  550. /package/{scss → theme}/config/mixins/_progress.scss +0 -0
  551. /package/{scss → theme}/config/mixins/_prose.scss +0 -0
  552. /package/{scss → theme}/config/mixins/_shadows.scss +0 -0
  553. /package/{scss → theme}/config/mixins/_sizing.scss +0 -0
  554. /package/{scss → theme}/config/mixins/_spacing.scss +0 -0
  555. /package/{scss → theme}/config/mixins/_stat-card.scss +0 -0
  556. /package/{scss → theme}/config/mixins/_stepper.scss +0 -0
  557. /package/{scss → theme}/config/mixins/_timeline.scss +0 -0
  558. /package/{scss → theme}/config/mixins/_transitions.scss +0 -0
  559. /package/{scss → theme}/config/mixins/_translations.scss +0 -0
  560. /package/{scss → theme}/tabler-icons.txt +0 -0
@@ -1,432 +1,443 @@
1
- # ln-router
2
-
3
- > A zero-dependency, template-driven, client-side SPA router that maps URLs to route templates and renders them declaratively.
4
-
5
- ---
6
-
7
- ## 1. Philosophy & The Primitive Mindset
8
-
9
- In `ln-ashlar`, the core design principle is **orthogonality**. Rather than creating a heavy router that handles data fetching, state management, and HTML sanitization, `ln-router` separates these concerns:
10
-
11
- 1. **The Route Mounter (JavaScript)**: The `ln-router` component only manages path matching, history states (`pushState`/`popstate`), accessibility focus shifting, and DOM swapping. It does not fetch data, sanitise markup, or touch database stores.
12
- 2. **The Declarative Layout (HTML)**: Route configurations and view templates live in HTML as `<template data-ln-route="...">` tags. The router never constructs HTML inside JS. It simply clones the templates into the designated outlet.
13
- 3. **Decoupled Data Binding (Coordinator)**: The coordinator listens to the `ln-router:navigated` event, retrieves params/query, fetches from a store or HTTP endpoint, and updates the mounted view using core helpers:
14
- - **`fillTemplate(clone, data)`**: Replaces inline `{{ placeholder }}` text-node variables in the cloned view. Since it operates strictly on text nodes (via `TreeWalker`), it is 100% safe from reflected XSS.
15
- - **`fill(el, data)`**: Performs attribute-driven binding (`data-ln-field` → `textContent`, `data-ln-attr` → `setAttribute`), which is also inherently secure.
16
-
17
- > [!WARNING]
18
- > **Reflected XSS Security**: The route coordinator is responsible for populating the view. When binding URL parameters (e.g., username in `/profile/:username`) or query parameters to the DOM, **never** use unsafe methods like `innerHTML` or `outerHTML`. Always use `fillTemplate`, `fill`, or set `textContent` directly to avoid reflected XSS injections. For more, see [Security Architecture](file:///c:/laragon/www/ln-ashlar/docs/architecture/security.md#safe-dom-interpolation).
19
-
20
- ---
21
-
22
- ## 2. Minimal Blueprint
23
-
24
- Route templates and outlets are declared in HTML. Triggers are plain same-origin `<a>` tags.
25
-
26
- ```html
27
- <!-- Default Outlet (where views render by default) -->
28
- <main data-ln-outlet></main>
29
-
30
- <!-- Route Templates (lives anywhere, commonly grouped near outlet) -->
31
- <template data-ln-route="/">
32
- <article>
33
- <h1>Home</h1>
34
- <p>Welcome to Ashlar SPA.</p>
35
- </article>
36
- </template>
37
-
38
- <template data-ln-route="/users">
39
- <article>
40
- <h1>Users List</h1>
41
- <ul id="user-list"></ul>
42
- </article>
43
- </template>
44
-
45
- <!-- Parameter route -->
46
- <template data-ln-route="/users/:id" data-ln-route-title="User Details">
47
- <article>
48
- <h1 id="user-name-title">Loading User...</h1>
49
- </article>
50
- </template>
51
-
52
- <!-- Catch-all fallback route -->
53
- <template data-ln-route="*">
54
- <article>
55
- <h1>404 - Not Found</h1>
56
- </article>
57
- </template>
58
- ```
59
-
60
- ### Multi-Region Blueprint
61
-
62
- ```html
63
- <!-- Primary outlet -->
64
- <main data-ln-outlet></main>
65
-
66
- <!-- Persistent sidebar — keeps state on param-only changes -->
67
- <aside id="side" data-ln-route-keep></aside>
68
-
69
- <!-- Primary region templates -->
70
- <template data-ln-route="/users/:id">
71
- <article><h1>User {{id}}</h1></article>
72
- </template>
73
-
74
- <!-- Sidebar templates -->
75
- <template data-ln-route="/users/:id" data-ln-route-target="side">
76
- <section>
77
- <h2>Sidebar: User {{id}}</h2>
78
- <input type="text" placeholder="Type something — survives param changes">
79
- </section>
80
- </template>
81
- ```
82
-
83
- ---
84
-
85
- ## 3. The Declarative API & State Contract
86
-
87
- There is no need to manually instantiate or initialize the router. Discovered `<template data-ln-route>` elements register themselves automatically.
88
-
89
- ```js
90
- import { router } from '../ln-router';
91
-
92
- // Programmatic navigation
93
- router.navigate('/users/42?tab=profile');
94
-
95
- // Programmatic replace state
96
- router.replace('/dashboard');
97
-
98
- // Read-only state query
99
- const current = router.current();
100
- // Returns:
101
- // {
102
- // path: '/users/42?tab=profile',
103
- // params: { id: '42' }, // primary region params
104
- // query: { tab: 'profile' },
105
- // route: {
106
- // pattern: '/users/:id',
107
- // target: null, // The string ID of the custom target outlet, or null if using default
108
- // title: 'User Details',
109
- // templateNode: HTMLTemplateElement
110
- // },
111
- // regions: Map { // NEW — per-region snapshot
112
- // '__primary__' => { route: {...}, params: { id: '42' } },
113
- // 'side' => { route: {...}, params: { id: '42' } } | null
114
- // }
115
- // }
116
- ```
117
-
118
- ### Target Outlet Resolution Fallback
119
- When a route matches, the router resolves the target DOM element to render into by checking the following hierarchy:
120
- 1. An explicit element ID specified by `data-ln-route-target="..."` on the `<template>`.
121
- 2. A default outlet element marked with the `data-ln-outlet` attribute.
122
- 3. The first `<main>` element found in the document.
123
-
124
- If the **primary** outlet cannot be resolved, the router logs a `console.warn` and aborts the navigation, leaving the current page state intact. If an **auxiliary** region's target element cannot be resolved, the router warns and skips only that region — the remaining regions still render.
125
-
126
- ### Attributes
127
- - `data-ln-route="pattern"`: Declares a route on a `<template>`. Supports static segments (`/users`), parameters (`/users/:id`), and wildcards (`*`).
128
- - `data-ln-route-target="id"`: Sets an explicit element ID to render this route into. Templates with no `data-ln-route-target` belong to the **primary region** (default outlet).
129
- - `data-ln-route-title="text"`: Sets `document.title` on successful navigation (primary region only).
130
- - `data-ln-outlet`: Marks the default outlet element (falls back to the first `<main>` element if omitted).
131
- - `data-ln-router-hydrate`: Placed on the outlet element to signal that initial SSR (Server-Side Rendered) content is already present, letting the router skip the initial clone and register events cleanly.
132
- - `data-ln-route-keep`: Placed on a **target element** (not a template). When present, the router skips teardown and DOM swap for that region if the matched template is the same as the one currently mounted — surviving param-only URL changes while preserving DOM state (inputs, scroll, open panels). When the matched template changes, teardown and swap proceed normally. Ignored on the primary outlet (the primary always re-renders).
133
-
134
- ---
135
-
136
- ## 4. Transition Events
137
-
138
- All events bubble from the target outlet element.
139
-
140
- | Event | Cancelable | Detail | Dispatched When |
141
- |---|---|---|---|
142
- | **`ln-router:before-navigate`** | **Yes** | `{ from, to, params, query }` | Fired **once per navigation** on the primary outlet before any region swap. `preventDefault()` aborts the entire navigation (no history update, no region swaps). |
143
- | **`ln-router:navigated`** | No | `{ path, params, query, route, target, region }` | Fired **per region that actually swapped**. `region` is `'__primary__'` for the default outlet, or the target element's id string for auxiliary regions. All existing keys (`path, params, query, route, target`) remain present for backward compatibility. |
144
- | **`ln-router:not-found`** | No | `{ path }` | When the primary region has no match (and no catch-all `*` exists). DOM is left untouched. Not fired on auxiliary-only pages. |
145
-
146
- ### Timing guarantee
147
-
148
- The initial render runs after the full route scan completes — all `<template data-ln-route>` elements in the document are registered before the first navigation fires.
149
- The initial `ln-router:navigated` (on a matched route) and boot-path `ln-router:not-found` (on no match) dispatches are **deferred one microtask** via `queueMicrotask`. This ensures that listeners registered during the same `DOMContentLoaded` burst — i.e. in a `<script defer>` tag that appears after the router bundle — always receive the boot event. When the View Transitions API is available, the deferral is handled via the VT callback rather than literally `queueMicrotask`, but the guarantee is identical — listeners in a later `defer` script still receive the boot event.
150
-
151
- All subsequent navigations (link clicks, `popstate`, programmatic `router.navigate()`/`router.replace()`) dispatch **synchronously**; no deferral applies once the boot phase is complete.
152
-
153
- **Consumers no longer need a `current()` boot-replay block.** The pattern:
154
-
155
- ```js
156
- // OBSOLETE — no longer needed
157
- const booted = window.lnRouter && window.lnRouter.current();
158
- if (booted && !navigatedFired) {
159
- mountRoute(booted);
160
- }
161
- ```
162
-
163
- can be replaced by simply attaching a `ln-router:navigated` listener — it will fire even if the router booted before the listener was added.
164
-
165
- ```js
166
- // Example: Block navigation out of unsaved forms
167
- document.addEventListener('ln-router:before-navigate', (e) => {
168
- if (hasUnsavedChanges && !confirm('Discard changes?')) {
169
- e.preventDefault(); // Cancels transition and keeps current view
170
- }
171
- });
172
- ```
173
-
174
- ## 5. Fragment-only popstate guard
175
-
176
- A `popstate` event whose new URL differs from the current SPA state **only in
177
- the fragment** (path and query are identical) is **skipped** — no navigation,
178
- no outlet teardown, no `ln-router:navigated` event fires.
179
-
180
- This is what makes hash-bound overlays (like `ln-modal`, which uses its
181
- element `id` as the hash namespace) work cleanly with Back/Forward:
182
-
183
- 1. User opens a hash-bound modal → URL gets `#id:value`.
184
- 2. User presses **Back** → browser fires `popstate` to the URL without the
185
- fragment.
186
- 3. ln-router detects path + query are identical → **skips navigation** → the
187
- under-modal view (table, scroll position, search/filter state) is
188
- completely preserved.
189
- 4. `hashchange` fires → ln-modal's `_onHashChange` sees the segment is gone
190
- → `data-ln-modal="close"` → modal closes cleanly.
191
-
192
- Real path/query Back/Forward navigations and link clicks are unaffected. The
193
- `router.current()` return value is left coherent (unchanged) on a skipped
194
- fragment popstate — no stale state is introduced.
195
-
196
- This guard is implemented via `_queryEqual` in the router source.
197
-
198
- ## 6. Accessibility & Screen Reader Continuity
199
-
200
- `ln-router` shifts focus automatically on each user-initiated navigation (click or popstate back/forward) to ensure compatibility with assistive technologies (screen readers). Focus is **not** shifted on the initial page load (boot render) to avoid an unexpected visible focus ring without user action.
201
- 1. **Focus Shifting** (user navigation only): The router looks for the first heading element (`h1` through `h6`) in the newly mounted template. If found, it dynamically assigns `tabindex="-1"` and calls `.focus()` on it. If no heading element is present, focus is shifted to the outlet container itself.
202
- 2. **Title Updates**: When `data-ln-route-title` is defined, the router updates `document.title`, which is announced by screen readers upon focus shifting.
203
- 3. **Scroll Management** (user navigation only): The router automatically scrolls the target element into view via `.scrollIntoView({ block: 'start', behavior: 'instant' })` to reset the viewport for the new content.
204
-
205
- ---
206
-
207
- ## 7. Server Integration & Deep Links
208
-
209
- Since `ln-router` operates client-side using history `pushState`, direct access or page refresh on a deep path (e.g., direct opening of `/users/42`) will bypass the client-side router and hit the web server directly. This results in a server 404 error if not configured correctly.
210
-
211
- ### 1. Server Catch-All Fallback
212
- Your web server must serve the main shell entrypoint (e.g., `index.html` or main layout view) for all deep URLs, allowing the client-side router to boot and handle the path.
213
-
214
- In **Laravel**, define a fallback route at the end of `routes/web.php`:
215
- ```php
216
- Route::fallback(function () {
217
- return view('app'); // returns your main blade layout shell
218
- });
219
- ```
220
-
221
- In **Nginx**, configure the fallback in your site configuration:
222
- ```nginx
223
- location / {
224
- try_files $uri $uri/ /index.html;
225
- }
226
- ```
227
-
228
- ### 2. Relative Assets & `<base>` Tag
229
- If your app references assets using relative paths, deep paths will attempt to load assets relative to the current route path (e.g., loading `/users/assets/app.js` instead of `/assets/app.js`). To prevent this:
230
- - **Configure the base tag**: Add `<base href="/">` in the `<head>` of your main HTML shell.
231
- - **Or use absolute asset paths**: Ensure script and link tags point to absolute URLs (e.g., `/dist/main.js` instead of `dist/main.js`).
232
-
233
- ---
234
-
235
- ## 8. Routing Model — Parallel Regions, Not Nested Routes
236
-
237
- > [!NOTE]
238
- > **No Nested Routing**: `ln-router` does not support nested outlets or child routes (rendering a sub-view inside an already-routed parent). All routes are flat.
239
-
240
- > [!NOTE]
241
- > **Multi-region routing is PARALLEL (sibling), not nested.** When a single URL simultaneously paints a sidebar region and a main region, both regions are siblings at the page level — neither is a child of the other's outlet. The "No Nested Routing" doctrine applies: there is no concept of a parent route whose outlet contains another router outlet.
242
-
243
- ---
244
-
245
- ## 9. Integration Patterns
246
-
247
- ### Coordinator Data Binding (Secure Interpolation)
248
- Coordinators listen to `ln-router:navigated`, fetch data, and populate the view. Use `fillTemplate` for safe `{{ placeholder }}` text-node substitutions:
249
-
250
- ```html
251
- <template data-ln-route="/profile/:username">
252
- <section>
253
- <h1>Profile: {{username}}</h1>
254
- </section>
255
- </template>
256
- ```
257
-
258
- ```js
259
- import { fillTemplate } from '../ln-core';
260
-
261
- document.addEventListener('ln-router:navigated', function (e) {
262
- if (e.detail.route.pattern !== '/profile/:username') return;
263
-
264
- const username = e.detail.params.username;
265
- // Populate read-only placeholders like {{username}} inside the outlet
266
- // fillTemplate walks text nodes and is completely safe from XSS.
267
- fillTemplate(e.detail.target, { username: username });
268
- });
269
- ```
270
-
271
- ---
272
-
273
- ## 10. Integration & Source Files
274
-
275
- `ln-ashlar` is a source-only package. Products should bundle it from source using their asset compilation pipelines (Vite, Laravel Mix, Webpack, etc.):
276
-
277
- - **Vite / ES Modules**:
278
- ```js
279
- import '@livenetworks/ashlar/js/ln-router';
280
- ```
281
- - **Git Submodule ESM import**:
282
- ```js
283
- import 'resources/ln-ashlar/js/ln-router/src/ln-router.js';
284
- ```
285
- - **Development Demo**:
286
- The compiled build in `demo/dist/ln-ashlar.iife.js` exists solely for running the local repository demo environment and should not be referenced in production consumer applications.
287
-
288
- ---
289
-
290
- ## 11. Multi-Region Routing
291
-
292
- One URL can simultaneously paint multiple sibling regions. Each region independently maintains its own route table and matches the current URL.
293
-
294
- ### Declaring regions
295
-
296
- Regions are ordinary DOM elements. Each is identified by an `id`. Route templates target a region via `data-ln-route-target="<id>"`. Templates with no `data-ln-route-target` belong to the **primary region** (the default outlet).
297
-
298
- ```html
299
- <!-- Primary outlet (default region) -->
300
- <main data-ln-outlet></main>
301
-
302
- <!-- Auxiliary region: sidebar -->
303
- <aside id="side" data-ln-route-keep></aside>
304
-
305
- <!-- Primary templates -->
306
- <template data-ln-route="/users/:id" data-ln-route-title="User Details">
307
- <article><h1>User {{id}}</h1></article>
308
- </template>
309
-
310
- <!-- Sidebar templates — same or different patterns -->
311
- <template data-ln-route="/users/:id" data-ln-route-target="side">
312
- <section><h2>Sidebar for User {{id}}</h2></section>
313
- </template>
314
-
315
- <!-- Sidebar catch-all — shown on all other routes -->
316
- <template data-ln-route="*" data-ln-route-target="side">
317
- <section><p>Select a user to see details.</p></section>
318
- </template>
319
- ```
320
-
321
- ### The primary region
322
-
323
- The primary region is the default outlet (`[data-ln-outlet]` or first `<main>`). It is authoritative for:
324
- - The single cancelable `ln-router:before-navigate` event (fires once per navigation)
325
- - `document.title` updates
326
- - Focus shifting and scroll management
327
- - `ln-router:not-found` (when the primary has no match)
328
- - `router.current()` return value
329
-
330
- `data-ln-route-keep` is ignored on the primary region: the primary always tears down and re-renders on every matched navigation.
331
-
332
- > [!IMPORTANT]
333
- > **Each region matches the URL independently.** A route targeting an
334
- > auxiliary region (`data-ln-route-target="…"`) no longer competes with primary
335
- > routes for a single "best match" — every region runs its own match. If you
336
- > have a route like `/users/:id/logs` that targets only an auxiliary region,
337
- > the primary region will independently try to match `/users/42/logs` against
338
- > its own templates and fall back to its `*` catch-all (or fire `not-found`) if
339
- > it has no template for that path. Author a primary template for such paths if
340
- > you want the main outlet to show meaningful content alongside the auxiliary
341
- > region. This is a deliberate consequence of the parallel-regions model.
342
-
343
- ### `data-ln-route-keep` — persistent regions
344
-
345
- Add `data-ln-route-keep` to any target element to make it a **keep region**.
346
-
347
- ```html
348
- <aside id="side" data-ln-route-keep></aside>
349
- ```
350
-
351
- Keep-region logic:
352
- - **Same template matches again** (e.g. param-only change `/users/42` → `/users/43`): the region is left completely untouched. No teardown, no DOM swap, no `ln-router:navigated` event. Input values, scroll position, and open panels survive.
353
- - **Different template matches** (e.g. navigating to a route that has a different sidebar template): normal teardown + DOM swap + `ln-router:navigated` event.
354
- - **No match for this URL**: the region is left as-is (untouched content persists). To clear a keep region on certain routes, declare an explicit empty `<template>` for those patterns.
355
-
356
- Keep is a property of the **region target element**, not of the template.
357
-
358
- ### `navigated` event — per swapped region
359
-
360
- `ln-router:navigated` fires once per region that actually swapped. The `region` key identifies which region fired:
361
-
362
- ```js
363
- document.addEventListener('ln-router:navigated', function (e) {
364
- const { path, params, query, route, target, region } = e.detail;
365
-
366
- if (region === '__primary__') {
367
- // Primary outlet navigated
368
- } else if (region === 'side') {
369
- // Sidebar region swapped
370
- }
371
- });
372
- ```
373
-
374
- The `region` value is `'__primary__'` for the default outlet, or the target element's `id` string for auxiliary regions.
375
-
376
- **Backward compatibility:** all existing keys (`path, params, query, route, target`) remain in the event detail unchanged. Single-outlet pages continue to receive one `navigated` event with the same payload they always have.
377
-
378
- ### `router.current()` — multi-region state
379
-
380
- ```js
381
- const state = router.current();
382
- // {
383
- // path: '/users/42',
384
- // params: { id: '42' }, // primary region params
385
- // query: {},
386
- // route: { ... }, // primary region matched route
387
- // regions: Map {
388
- // '__primary__' => { route: {...}, params: { id: '42' } },
389
- // 'side' => { route: {...}, params: { id: '42' } },
390
- // }
391
- // }
392
- ```
393
-
394
- ### Atomicity
395
-
396
- All region swaps for a single navigation are wrapped in a single `document.startViewTransition` call. History push/replace happens once. `before-navigate` fires once.
397
-
398
- ---
399
-
400
- ## 🔧 Internals
401
-
402
- Source: `js/ln-router/src/ln-router.js`.
403
-
404
- ### Navigation pipeline (order matters)
405
-
406
- 1. Strip hash/query, collapse trailing slashes.
407
- 2. Every region in `regionRegistry` matches independently against its own sorted route list; the primary region's match is authoritative — if it has none (and no catch-all), the router dispatches `ln-router:not-found` and stops. Auxiliary-only pages proceed without firing `not-found`.
408
- 3. `ln-router:before-navigate` fires once on the primary outlet; `preventDefault()` aborts the whole navigation before any region swap or history update.
409
- 4. For each matched region, a keep-region check (`data-ln-route-keep` + same template already mounted) decides whether it enters the swap plan.
410
- 5. History push/replace happens once.
411
- 6. All planned swaps run sequentially inside a single `document.startViewTransition` call — teardown, clone, `replaceChildren`, record in `mountedTemplates`.
412
- 7. `ln-router:navigated` fires per swapped region (`region` key = `'__primary__'` or the target id).
413
-
414
- ### Registry & specificity
415
-
416
- `regionRegistry: Map<regionKey, { routes: Map<pattern, meta>, sorted: meta[] }>` — one route table per region, each independently sorted static > param > wildcard, first-declared wins ties. A duplicate `(pattern, regionKey)` registration is dropped with `console.warn`; the same pattern targeting two different regions is legal.
417
-
418
- ### Teardown & teleport cleanup
419
-
420
- Only regions actually in the swap plan are torn down — a keep-region that skips a swap is never touched. Before `replaceChildren()` clears an outlet, the router walks its descendants for any element carrying an `ln*` property with a `destroy()` method and calls it — this is what prevents leaked window/document listeners and teleported overlay trees (e.g. an open popover in `<body>` whose trigger lived in the outlet) from surviving the swap.
421
-
422
- ### Per-region persistence (keep regions)
423
-
424
- A `WeakMap<Element, HTMLTemplateElement>` (`mountedTemplates`) tracks the currently-mounted template per target. A keep-region skips its swap only when the newly matched template node is the *same* node already mounted there — never for the primary region, which always re-renders.
425
-
426
- ### SSR hydration
427
-
428
- On boot, a region whose target carries `data-ln-router-hydrate` and already has children skips its initial clone swap — the SSR markup is left in place, listeners are wired, and `ln-router:navigated` still fires so a coordinator can bind/fill it.
429
-
430
- ### Boot timing
431
-
432
- A module-level `_booting` flag is true only during the router's own initial `_navigate` call inside `_boot()`. While true, `ln-router:navigated`/`not-found` dispatches are queued via `queueMicrotask` instead of firing synchronously — guaranteeing that listeners registered later in the same `defer`-script burst still receive the boot event. Every subsequent navigation dispatches synchronously.
1
+ # ln-router
2
+
3
+ > Declared globally with route definitions in `<template data-ln-route="...">` elements rendering into `<main data-ln-outlet>` or auxiliary targets (`data-ln-route-target`). It intercepts click events on same-origin `<a>` tags and listens for `popstate`, matches the URL against specificity-ranked route patterns, and clones templates into designated outlets with `document.startViewTransition()` support. It unmounts old views by calling `.destroy()` on child components, leaving data fetching and view filling to route coordinators.
4
+
5
+ ---
6
+
7
+ ## 1. Philosophy & The Primitive Mindset
8
+
9
+ In `ln-ashlar`, the core design principle is **orthogonality**. Rather than creating a heavy router that handles data fetching, state management, and HTML sanitization, `ln-router` separates these concerns:
10
+
11
+ 1. **The Route Mounter (JavaScript)**: The `ln-router` component only manages path matching, history states (`pushState`/`popstate`), accessibility focus shifting, and DOM swapping. It does not fetch data, sanitise markup, or touch database stores.
12
+ 2. **The Declarative Layout (HTML)**: Route configurations and view templates live in HTML as `<template data-ln-route="...">` tags. The router never constructs HTML inside JS. It simply clones the templates into the designated outlet.
13
+ 3. **Decoupled Data Binding (Coordinator)**: The coordinator listens to the `ln-router:navigated` event, retrieves params/query, fetches from a store or HTTP endpoint, and updates the mounted view using core helpers:
14
+ - **`fillTemplate(clone, data)`**: Replaces inline `{{ placeholder }}` text-node variables in the cloned view. Since it operates strictly on text nodes (via `TreeWalker`), it is 100% safe from reflected XSS.
15
+ - **`fill(el, data)`**: Performs attribute-driven binding (`data-ln-field` → `textContent`, `data-ln-attr` → `setAttribute`), which is also inherently secure.
16
+
17
+ > [!WARNING]
18
+ > **Reflected XSS Security**: The route coordinator is responsible for populating the view. When binding URL parameters (e.g., username in `/profile/:username`) or query parameters to the DOM, **never** use unsafe methods like `innerHTML` or `outerHTML`. Always use `fillTemplate`, `fill`, or set `textContent` directly to avoid reflected XSS injections. For more, see [Security Architecture](file:///c:/laragon/www/ln-ashlar/docs/architecture/security.md#safe-dom-interpolation).
19
+
20
+ ---
21
+
22
+ ## 2. Minimal Blueprint
23
+
24
+ Route templates and outlets are declared in HTML. Triggers are plain same-origin `<a>` tags.
25
+
26
+ ```html
27
+ <!-- Default Outlet (where views render by default) -->
28
+ <main data-ln-outlet></main>
29
+
30
+ <!-- Route Templates (lives anywhere, commonly grouped near outlet) -->
31
+ <template data-ln-route="/">
32
+ <article>
33
+ <h1>Home</h1>
34
+ <p>Welcome to Ashlar SPA.</p>
35
+ </article>
36
+ </template>
37
+
38
+ <template data-ln-route="/users">
39
+ <article>
40
+ <h1>Users List</h1>
41
+ <ul id="user-list"></ul>
42
+ </article>
43
+ </template>
44
+
45
+ <!-- Parameter route -->
46
+ <template data-ln-route="/users/:id" data-ln-route-title="User Details">
47
+ <article>
48
+ <h1 id="user-name-title">Loading User...</h1>
49
+ </article>
50
+ </template>
51
+
52
+ <!-- Catch-all fallback route -->
53
+ <template data-ln-route="*">
54
+ <article>
55
+ <h1>404 - Not Found</h1>
56
+ </article>
57
+ </template>
58
+ ```
59
+
60
+ ### Multi-Region Blueprint
61
+
62
+ ```html
63
+ <!-- Primary outlet -->
64
+ <main data-ln-outlet></main>
65
+
66
+ <!-- Persistent sidebar — keeps state on param-only changes -->
67
+ <aside id="side" data-ln-route-keep></aside>
68
+
69
+ <!-- Primary region templates -->
70
+ <template data-ln-route="/users/:id">
71
+ <article><h1>User {{id}}</h1></article>
72
+ </template>
73
+
74
+ <!-- Sidebar templates -->
75
+ <template data-ln-route="/users/:id" data-ln-route-target="side">
76
+ <section>
77
+ <h2>Sidebar: User {{id}}</h2>
78
+ <input type="text" placeholder="Type something — survives param changes">
79
+ </section>
80
+ </template>
81
+ ```
82
+
83
+ ---
84
+
85
+ ## 3. The Declarative API & State Contract
86
+
87
+ There is no need to manually instantiate or initialize the router. Discovered `<template data-ln-route>` elements register themselves automatically.
88
+
89
+ ```js
90
+ import { router } from '../ln-router';
91
+
92
+ // Programmatic navigation
93
+ router.navigate('/users/42?tab=profile');
94
+
95
+ // Programmatic replace state
96
+ router.replace('/dashboard');
97
+
98
+ // Read-only state query
99
+ const current = router.current();
100
+ // Returns:
101
+ // {
102
+ // path: '/users/42?tab=profile',
103
+ // params: { id: '42' }, // primary region params
104
+ // query: { tab: 'profile' },
105
+ // route: { // null on an auxiliary-only navigation
106
+ // pattern: '/users/:id',
107
+ // target: null, // The string ID of the custom target outlet, or null if using default
108
+ // title: 'User Details',
109
+ // templateNode: HTMLTemplateElement
110
+ // },
111
+ // regions: Map { // per-region snapshot
112
+ // '__primary__' => { route: {...}, params: { id: '42' } },
113
+ // 'side' => { route: {...}, params: { id: '42' } } | null
114
+ // }
115
+ // }
116
+ // current() returns null only when the router has never navigated. Once it
117
+ // has, route is null exactly when the primary region has no match for the
118
+ // current URL — an auxiliary-only navigation.
119
+ ```
120
+
121
+ ### Target Outlet Resolution Fallback
122
+ When a route matches, the router resolves the target DOM element to render into by checking the following hierarchy:
123
+ 1. An explicit element ID specified by `data-ln-route-target="..."` on the `<template>`.
124
+ 2. A default outlet element marked with the `data-ln-outlet` attribute.
125
+ 3. The first `<main>` element found in the document.
126
+
127
+ If the **primary** outlet cannot be resolved, the router logs a `console.warn` and aborts the navigation, leaving the current page state intact. If an **auxiliary** region's target element cannot be resolved, the router warns and skips only that region — the remaining regions still render.
128
+
129
+ ### Attributes
130
+ - `data-ln-route="pattern"`: Declares a route on a `<template>`. Supports static segments (`/users`), parameters (`/users/:id`), and wildcards (`*`).
131
+ - `data-ln-route-target="id"`: Sets an explicit element ID to render this route into. Templates with no `data-ln-route-target` belong to the **primary region** (default outlet).
132
+ - `data-ln-route-title="text"`: Sets `document.title` on successful navigation (primary region only).
133
+ - `data-ln-outlet`: Marks the default outlet element (falls back to the first `<main>` element if omitted).
134
+ - `data-ln-router-hydrate`: Placed on the outlet element to signal that initial SSR (Server-Side Rendered) content is already present, letting the router skip the initial clone and register events cleanly.
135
+ - `data-ln-route-keep`: Placed on a **target element** (not a template). When present, the router skips teardown and DOM swap for that region if the matched template is the same as the one currently mounted — surviving param-only URL changes while preserving DOM state (inputs, scroll, open panels). When the matched template changes, teardown and swap proceed normally. `data-ln-route-keep` also opts an auxiliary region out of auto-clear: without it, a region left unmatched by the current URL is emptied automatically; with it, unmatched content is left in place. Ignored on the primary outlet (the primary always re-renders).
136
+
137
+ ---
138
+
139
+ ## 4. Transition Events
140
+
141
+ All events bubble from the target outlet element.
142
+
143
+ | Event | Cancelable | Detail | Dispatched When |
144
+ |---|---|---|---|
145
+ | **`ln-router:before-navigate`** | **Yes** | `{ from, to, params, query }` | Fired **once per navigation with at least one region match** on the primary outlet, before any region swap or clear. `preventDefault()` aborts the entire navigation (no history update, no region swaps). |
146
+ | **`ln-router:navigated`** | No | `{ path, params, query, route, target, region }` | Fired **per region that actually mounted a template**. `region` is `'__primary__'` for the default outlet, or the target element's id string for auxiliary regions. All existing keys (`path, params, query, route, target`) remain present for backward compatibility. |
147
+ | **`ln-router:not-found`** | No | `{ path }` | When **no region matched** the current path (and no catch-all `*` exists anywhere). DOM is left untouched. |
148
+
149
+ ### Timing guarantee
150
+
151
+ The initial render runs after the full route scan completes — all `<template data-ln-route>` elements in the document are registered before the first navigation fires.
152
+ The initial `ln-router:navigated` (on a matched route) and boot-path `ln-router:not-found` (on no match) dispatches are **deferred one microtask** via `queueMicrotask`. This ensures that listeners registered during the same `DOMContentLoaded` burst — i.e. in a `<script defer>` tag that appears after the router bundle — always receive the boot event. When the View Transitions API is available, the deferral is handled via the VT callback rather than literally `queueMicrotask`, but the guarantee is identical — listeners in a later `defer` script still receive the boot event.
153
+
154
+ All subsequent navigations (link clicks, `popstate`, programmatic `router.navigate()`/`router.replace()`) dispatch **synchronously**; no deferral applies once the boot phase is complete.
155
+
156
+ **Consumers no longer need a `current()` boot-replay block.** The pattern:
157
+
158
+ ```js
159
+ // OBSOLETE — no longer needed
160
+ const booted = window.lnRouter && window.lnRouter.current();
161
+ if (booted && !navigatedFired) {
162
+ mountRoute(booted);
163
+ }
164
+ ```
165
+
166
+ can be replaced by simply attaching a `ln-router:navigated` listener — it will fire even if the router booted before the listener was added.
167
+
168
+ ```js
169
+ // Example: Block navigation out of unsaved forms
170
+ document.addEventListener('ln-router:before-navigate', (e) => {
171
+ if (hasUnsavedChanges && !confirm('Discard changes?')) {
172
+ e.preventDefault(); // Cancels transition and keeps current view
173
+ }
174
+ });
175
+ ```
176
+
177
+ ## 5. Fragment-only popstate guard
178
+
179
+ A `popstate` event whose new URL differs from the current SPA state **only in
180
+ the fragment** (path and query are identical) is **skipped** — no navigation,
181
+ no outlet teardown, no `ln-router:navigated` event fires.
182
+
183
+ This is what makes hash-bound overlays (like `ln-modal`, which uses its
184
+ element `id` as the hash namespace) work cleanly with Back/Forward:
185
+
186
+ 1. User opens a hash-bound modal → URL gets `#id:value`.
187
+ 2. User presses **Back** → browser fires `popstate` to the URL without the
188
+ fragment.
189
+ 3. ln-router detects path + query are identical → **skips navigation** → the
190
+ under-modal view (table, scroll position, search/filter state) is
191
+ completely preserved.
192
+ 4. `hashchange` fires → ln-modal's `_onHashChange` sees the segment is gone
193
+ → `data-ln-modal="close"` → modal closes cleanly.
194
+
195
+ Real path/query Back/Forward navigations and link clicks are unaffected. The
196
+ `router.current()` return value is left coherent (unchanged) on a skipped
197
+ fragment popstate — no stale state is introduced.
198
+
199
+ This guard is implemented via `_queryEqual` in the router source.
200
+
201
+ ## 6. Accessibility & Screen Reader Continuity
202
+
203
+ `ln-router` shifts focus automatically on each user-initiated navigation (click or popstate back/forward) to ensure compatibility with assistive technologies (screen readers). Focus is **not** shifted on the initial page load (boot render) to avoid an unexpected visible focus ring without user action.
204
+ 1. **Focus Shifting** (user navigation only): The router looks for the first heading element (`h1` through `h6`) in the newly mounted template. If found, it dynamically assigns `tabindex="-1"` and calls `.focus()` on it. If no heading element is present, focus is shifted to the outlet container itself.
205
+ 2. **Title Updates**: When `data-ln-route-title` is defined, the router updates `document.title`, which is announced by screen readers upon focus shifting.
206
+ 3. **Scroll Management** (user navigation only): The router automatically scrolls the target element into view via `.scrollIntoView({ block: 'start', behavior: 'instant' })` to reset the viewport for the new content.
207
+
208
+ ---
209
+
210
+ ## 7. Server Integration & Deep Links
211
+
212
+ Since `ln-router` operates client-side using history `pushState`, direct access or page refresh on a deep path (e.g., direct opening of `/users/42`) will bypass the client-side router and hit the web server directly. This results in a server 404 error if not configured correctly.
213
+
214
+ ### 1. Server Catch-All Fallback
215
+ Your web server must serve the main shell entrypoint (e.g., `index.html` or main layout view) for all deep URLs, allowing the client-side router to boot and handle the path.
216
+
217
+ In **Laravel**, define a fallback route at the end of `routes/web.php`:
218
+ ```php
219
+ Route::fallback(function () {
220
+ return view('app'); // returns your main blade layout shell
221
+ });
222
+ ```
223
+
224
+ In **Nginx**, configure the fallback in your site configuration:
225
+ ```nginx
226
+ location / {
227
+ try_files $uri $uri/ /index.html;
228
+ }
229
+ ```
230
+
231
+ ### 2. Relative Assets & `<base>` Tag
232
+ If your app references assets using relative paths, deep paths will attempt to load assets relative to the current route path (e.g., loading `/users/assets/app.js` instead of `/assets/app.js`). To prevent this:
233
+ - **Configure the base tag**: Add `<base href="/">` in the `<head>` of your main HTML shell.
234
+ - **Or use absolute asset paths**: Ensure script and link tags point to absolute URLs (e.g., `/dist/main.js` instead of `dist/main.js`).
235
+
236
+ ---
237
+
238
+ ## 8. Routing Model — Parallel Regions, Not Nested Routes
239
+
240
+ > [!NOTE]
241
+ > **No Nested Routing**: `ln-router` does not support nested outlets or child routes (rendering a sub-view inside an already-routed parent). All routes are flat.
242
+
243
+ > [!NOTE]
244
+ > **Multi-region routing is PARALLEL (sibling), not nested.** When a single URL simultaneously paints a sidebar region and a main region, both regions are siblings at the page level — neither is a child of the other's outlet. The "No Nested Routing" doctrine applies: there is no concept of a parent route whose outlet contains another router outlet.
245
+
246
+ ---
247
+
248
+ ## 9. Integration Patterns
249
+
250
+ ### Coordinator Data Binding (Secure Interpolation)
251
+ Coordinators listen to `ln-router:navigated`, fetch data, and populate the view. Use `fillTemplate` for safe `{{ placeholder }}` text-node substitutions:
252
+
253
+ ```html
254
+ <template data-ln-route="/profile/:username">
255
+ <section>
256
+ <h1>Profile: {{username}}</h1>
257
+ </section>
258
+ </template>
259
+ ```
260
+
261
+ ```js
262
+ import { fillTemplate } from '../ln-core';
263
+
264
+ document.addEventListener('ln-router:navigated', function (e) {
265
+ if (e.detail.route.pattern !== '/profile/:username') return;
266
+
267
+ const username = e.detail.params.username;
268
+ // Populate read-only placeholders like {{username}} inside the outlet
269
+ // fillTemplate walks text nodes and is completely safe from XSS.
270
+ fillTemplate(e.detail.target, { username: username });
271
+ });
272
+ ```
273
+
274
+ ---
275
+
276
+ ## 10. Integration & Source Files
277
+
278
+ `ln-ashlar` is a source-only package. Products should bundle it from source using their asset compilation pipelines (Vite, Laravel Mix, Webpack, etc.):
279
+
280
+ - **Vite / ES Modules**:
281
+ ```js
282
+ import '@livenetworks/ashlar/components/ln-router';
283
+ ```
284
+ - **Git Submodule ESM import**:
285
+ ```js
286
+ import 'resources/ln-ashlar/components/ln-router/src/ln-router.js';
287
+ ```
288
+ - **Development Demo**:
289
+ The compiled build in `demo/dist/ln-ashlar.iife.js` exists solely for running the local repository demo environment and should not be referenced in production consumer applications.
290
+
291
+ ---
292
+
293
+ ## 11. Multi-Region Routing
294
+
295
+ One URL can simultaneously paint multiple sibling regions. Each region independently maintains its own route table and matches the current URL.
296
+
297
+ ### Declaring regions
298
+
299
+ Regions are ordinary DOM elements. Each is identified by an `id`. Route templates target a region via `data-ln-route-target="<id>"`. Templates with no `data-ln-route-target` belong to the **primary region** (the default outlet).
300
+
301
+ ```html
302
+ <!-- Primary outlet (default region) -->
303
+ <main data-ln-outlet></main>
304
+
305
+ <!-- Auxiliary region: sidebar -->
306
+ <aside id="side" data-ln-route-keep></aside>
307
+
308
+ <!-- Primary templates -->
309
+ <template data-ln-route="/users/:id" data-ln-route-title="User Details">
310
+ <article><h1>User {{id}}</h1></article>
311
+ </template>
312
+
313
+ <!-- Sidebar templates — same or different patterns -->
314
+ <template data-ln-route="/users/:id" data-ln-route-target="side">
315
+ <section><h2>Sidebar for User {{id}}</h2></section>
316
+ </template>
317
+
318
+ <!-- Sidebar catch-all — shown on all other routes -->
319
+ <template data-ln-route="*" data-ln-route-target="side">
320
+ <section><p>Select a user to see details.</p></section>
321
+ </template>
322
+ ```
323
+
324
+ ### The primary region
325
+
326
+ The primary region is the default outlet (`[data-ln-outlet]` or first `<main>`). Every region matches the URL and swaps independently; the primary is authoritative only for:
327
+ - `router.current()`'s `route`/`params` fields
328
+ - The before-navigate host element (it is a stable container, always resolvable, so it stays the dispatch target even on an auxiliary-only navigation)
329
+
330
+ Title updates and focus/scroll management belong to the navigation's **owner** region: the primary if it swapped, otherwise the first region (in registration order) that swapped. `ln-router:before-navigate` fires once per navigation as long as at least one region matched; `ln-router:not-found` fires only when no region matched at all.
331
+
332
+ `data-ln-route-keep` is ignored on the primary region: the primary always tears down and re-renders on every matched navigation.
333
+
334
+ > [!IMPORTANT]
335
+ > **Each region matches the URL independently.** A route targeting an
336
+ > auxiliary region (`data-ln-route-target="…"`) does not compete with primary
337
+ > routes for a single "best match" — every region runs its own match. If you
338
+ > have a route like `/users/:id/logs` that targets only an auxiliary region,
339
+ > the primary region will independently try to match `/users/42/logs` against
340
+ > its own templates. If it has none, it simply auto-clears (or is left alone
341
+ > if it carries `data-ln-route-keep`) — the auxiliary region still renders.
342
+ > This is a deliberate consequence of the parallel-regions model.
343
+
344
+ ### `data-ln-route-keep` — persistent regions
345
+
346
+ Add `data-ln-route-keep` to any target element to make it a **keep region**.
347
+
348
+ ```html
349
+ <aside id="side" data-ln-route-keep></aside>
350
+ ```
351
+
352
+ Keep-region logic:
353
+ - **Same template matches again** (e.g. param-only change `/users/42` → `/users/43`): the region is left completely untouched. No teardown, no DOM swap, no `ln-router:navigated` event. Input values, scroll position, and open panels survive.
354
+ - **Different template matches** (e.g. navigating to a route that has a different sidebar template): normal teardown + DOM swap + `ln-router:navigated` event.
355
+ - **No match for this URL**: the region is left as-is (untouched content persists). This is the entire purpose of `data-ln-route-keep` — without it, an unmatched region auto-clears (see below).
356
+
357
+ Keep is a property of the **region target element**, not of the template.
358
+
359
+ ### Auto-clear — unmatched regions empty themselves
360
+
361
+ A region without `data-ln-route-keep` that has no match for the current URL is
362
+ automatically emptied: existing components inside it are torn down and its
363
+ content is replaced with nothing. This applies to regions with content
364
+ already present (nothing happens to an already-empty region), and it does not
365
+ fire `ln-router:navigated` — that event means "mounted a template", and a
366
+ clear mounts nothing. `data-ln-route-keep` is the opt-out: an unmatched keep
367
+ region is left exactly as it was.
368
+
369
+ ### `navigated` event — per swapped region
370
+
371
+ `ln-router:navigated` fires once per region that actually swapped. The `region` key identifies which region fired:
372
+
373
+ ```js
374
+ document.addEventListener('ln-router:navigated', function (e) {
375
+ const { path, params, query, route, target, region } = e.detail;
376
+
377
+ if (region === '__primary__') {
378
+ // Primary outlet navigated
379
+ } else if (region === 'side') {
380
+ // Sidebar region swapped
381
+ }
382
+ });
383
+ ```
384
+
385
+ The `region` value is `'__primary__'` for the default outlet, or the target element's `id` string for auxiliary regions.
386
+
387
+ **Backward compatibility:** all existing keys (`path, params, query, route, target`) remain in the event detail unchanged. Single-outlet pages continue to receive one `navigated` event with the same payload they always have.
388
+
389
+ ### `router.current()` — multi-region state
390
+
391
+ ```js
392
+ const state = router.current();
393
+ // {
394
+ // path: '/users/42',
395
+ // params: { id: '42' }, // primary region params
396
+ // query: {},
397
+ // route: { ... }, // primary region matched route
398
+ // regions: Map {
399
+ // '__primary__' => { route: {...}, params: { id: '42' } },
400
+ // 'side' => { route: {...}, params: { id: '42' } },
401
+ // }
402
+ // }
403
+ ```
404
+
405
+ ### Atomicity
406
+
407
+ All region swaps for a single navigation are wrapped in a single `document.startViewTransition` call. History push/replace happens once. `before-navigate` fires once.
408
+
409
+ ---
410
+
411
+ ## 🔧 Internals
412
+
413
+ Source: `components/ln-router/src/ln-router.js`. The region clear/swap/owner decision is a pure function, `planRegions`, in `components/ln-router/src/router-model.js` — it does no DOM access and is covered directly by `tests/ln-router.test.js`.
414
+
415
+ ### Navigation pipeline (order matters)
416
+
417
+ 1. Strip hash/query, collapse trailing slashes.
418
+ 2. Every region in `regionRegistry` matches independently against its own sorted route list. DOM-reading descriptors are built for every region (matched or not), then handed to the pure `planRegions` (`router-model.js`) for the decision. If no region matched, the router dispatches `ln-router:not-found` and stops — this is checked before anything is cleared, so a 404 never wipes the page.
419
+ 3. `ln-router:before-navigate` fires once, on the primary outlet, as long as at least one region matched; `preventDefault()` aborts the whole navigation before any region swap, clear, or history update.
420
+ 4. `planRegions` classifies every region with a resolvable target into `clears` (unmatched, no `data-ln-route-keep`, has content), `swaps` (matched, and not a keep-region already showing the matched template), or neither (keep-region already correct, or unmatched-and-empty).
421
+ 5. History push/replace happens once.
422
+ 6. All planned clears and swaps run sequentially inside a single `document.startViewTransition` call — clears teardown and `replaceChildren()` with nothing; swaps teardown, clone, `replaceChildren`, record in `mountedTemplates`. Title/focus/scroll apply only to the plan's owner region (the primary if it swapped, otherwise the first region — in registration order — that swapped).
423
+ 7. `ln-router:navigated` fires per region that mounted a template (`region` key = `'__primary__'` or the target id). Clears never fire it.
424
+
425
+ ### Registry & specificity
426
+
427
+ `regionRegistry: Map<regionKey, { routes: Map<pattern, meta>, sorted: meta[] }>` — one route table per region, each independently sorted static > param > wildcard, first-declared wins ties. A duplicate `(pattern, regionKey)` registration is dropped with `console.warn`; the same pattern targeting two different regions is legal.
428
+
429
+ ### Teardown & teleport cleanup
430
+
431
+ Only regions actually in `plan.clears` or `plan.swaps` are torn down — a keep-region that matches neither list is never touched. Before `replaceChildren()` empties an outlet (swap or clear), the router walks its descendants for any element carrying an `ln*` property with a `destroy()` method and calls it — this is what prevents leaked window/document listeners and teleported overlay trees (e.g. an open popover in `<body>` whose trigger lived in the outlet) from surviving.
432
+
433
+ ### Per-region persistence (keep regions)
434
+
435
+ A `WeakMap<Element, HTMLTemplateElement>` (`mountedTemplates`) tracks the currently-mounted template per target. A keep-region skips its swap only when the newly matched template node is the *same* node already mounted there — never for the primary region, which always re-renders.
436
+
437
+ ### SSR hydration
438
+
439
+ On boot, a region whose target carries `data-ln-router-hydrate` and already has children skips its initial clone swap — the SSR markup is left in place, listeners are wired, and `ln-router:navigated` still fires so a coordinator can bind/fill it.
440
+
441
+ ### Boot timing
442
+
443
+ A module-level `_booting` flag is true only during the router's own initial `_navigate` call inside `_boot()`. While true, `ln-router:navigated`/`not-found` dispatches are queued via `queueMicrotask` instead of firing synchronously — guaranteeing that listeners registered later in the same `defer`-script burst still receive the boot event. Every subsequent navigation dispatches synchronously.