@livenetworks/ashlar 1.6.1 → 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 (559) 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/{js → components}/ln-confirm/README.md +69 -18
  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 -171
  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/_ln-confirm.scss +0 -24
  414. package/js/ln-confirm/ln-confirm.js +0 -1
  415. package/js/ln-core/crypto.js +0 -83
  416. package/js/ln-core/helpers.js +0 -788
  417. package/js/ln-core/index.js +0 -7
  418. package/js/ln-core/persist.js +0 -97
  419. package/js/ln-couchdb-connector/ln-couchdb-connector.js +0 -1
  420. package/js/ln-data-coordinator/ln-data-coordinator.js +0 -1
  421. package/js/ln-data-coordinator/src/data-read-policy.js +0 -18
  422. package/js/ln-data-store/ln-data-store.js +0 -1
  423. package/js/ln-date/ln-date.js +0 -1
  424. package/js/ln-date/src/ln-date.js +0 -603
  425. package/js/ln-debug/README.md +0 -58
  426. package/js/ln-debug/ln-debug.js +0 -1
  427. package/js/ln-debug/src/ln-debug.js +0 -19
  428. package/js/ln-dropdown/ln-dropdown.js +0 -1
  429. package/js/ln-dropdown/ln-dropdown.scss +0 -15
  430. package/js/ln-editor/ln-editor.js +0 -1
  431. package/js/ln-editor/ln-editor.scss +0 -43
  432. package/js/ln-external-links/ln-external-links.js +0 -1
  433. package/js/ln-fill/ln-fill.js +0 -1
  434. package/js/ln-filter/README.md +0 -189
  435. package/js/ln-filter/ln-filter-dev.scss +0 -8
  436. package/js/ln-filter/ln-filter.js +0 -1
  437. package/js/ln-filter/src/ln-filter.js +0 -419
  438. package/js/ln-form/ln-form.js +0 -1
  439. package/js/ln-http/ln-http.js +0 -1
  440. package/js/ln-link/ln-link.js +0 -1
  441. package/js/ln-list/README.md +0 -129
  442. package/js/ln-list/ln-list.js +0 -1
  443. package/js/ln-list/ln-list.scss +0 -28
  444. package/js/ln-modal/ln-modal-dev.scss +0 -13
  445. package/js/ln-modal/ln-modal.js +0 -1
  446. package/js/ln-modal/ln-modal.scss +0 -35
  447. package/js/ln-modal-coordinator/README.md +0 -99
  448. package/js/ln-modal-coordinator/ln-modal-coordinator-dev.scss +0 -8
  449. package/js/ln-modal-coordinator/ln-modal-coordinator.js +0 -1
  450. package/js/ln-nav/ln-nav.js +0 -1
  451. package/js/ln-number/README.md +0 -149
  452. package/js/ln-number/ln-number.js +0 -1
  453. package/js/ln-number/src/ln-number.js +0 -443
  454. package/js/ln-options/ln-options.js +0 -1
  455. package/js/ln-popover/ln-popover.js +0 -1
  456. package/js/ln-progress/ln-progress.js +0 -1
  457. package/js/ln-progress/src/ln-progress.js +0 -98
  458. package/js/ln-router/ln-router.js +0 -1
  459. package/js/ln-search/README.md +0 -158
  460. package/js/ln-search/ln-search-dev.scss +0 -8
  461. package/js/ln-search/ln-search.js +0 -1
  462. package/js/ln-search/src/ln-search.js +0 -117
  463. package/js/ln-slug/ln-slug.js +0 -1
  464. package/js/ln-sort/ln-sort.js +0 -1
  465. package/js/ln-sort/src/ln-sort.js +0 -178
  466. package/js/ln-sortable/ln-sortable.js +0 -1
  467. package/js/ln-stat/ln-stat.js +0 -1
  468. package/js/ln-table/ln-table.js +0 -1
  469. package/js/ln-table/ln-table.scss +0 -11
  470. package/js/ln-table-coordinator/README.md +0 -73
  471. package/js/ln-table-coordinator/ln-table-coordinator.js +0 -1
  472. package/js/ln-table-coordinator/src/ln-table-coordinator.js +0 -160
  473. package/js/ln-tabs/ln-tabs-dev.scss +0 -18
  474. package/js/ln-tabs/ln-tabs.js +0 -1
  475. package/js/ln-tabs/src/ln-tabs.js +0 -188
  476. package/js/ln-time/ln-time.js +0 -1
  477. package/js/ln-toast/ln-toast.js +0 -1
  478. package/js/ln-toggle/ln-toggle.js +0 -1
  479. package/js/ln-tooltip/ln-tooltip.js +0 -1
  480. package/js/ln-tooltip/ln-tooltip.scss +0 -9
  481. package/js/ln-translations/ln-translations.js +0 -1
  482. package/js/ln-upload/README.md +0 -255
  483. package/js/ln-upload/ln-upload.js +0 -1
  484. package/js/ln-upload/ln-upload.scss +0 -20
  485. package/js/ln-upload/src/ln-upload.js +0 -423
  486. package/js/ln-validate/ln-validate.js +0 -1
  487. package/scss/components/_button.scss +0 -20
  488. package/scss/components/_confirm.scss +0 -5
  489. package/scss/components/_form.scss +0 -107
  490. package/scss/components/_status-badge.scss +0 -4
  491. package/scss/components/_toast.scss +0 -68
  492. package/scss/components/_upload.scss +0 -51
  493. package/scss/config/_theme.scss +0 -216
  494. package/scss/config/mixins/_btn.scss +0 -179
  495. package/scss/config/mixins/_confirm.scss +0 -31
  496. package/scss/config/mixins/_dropdown.scss +0 -141
  497. package/scss/config/mixins/_popover.scss +0 -64
  498. package/scss/ln-ashlar-dev.scss +0 -39
  499. /package/{js → components}/ln-core/positioning.js +0 -0
  500. /package/{js → components}/ln-core/reactive.js +0 -0
  501. /package/{js → components}/ln-filter/ln-filter.scss +0 -0
  502. /package/{js → components}/ln-icon/ln-icon.js +0 -0
  503. /package/{js → components}/ln-icon/src/ln-icon.js +0 -0
  504. /package/{js → components}/ln-search/ln-search.scss +0 -0
  505. /package/{js → components}/ln-toast/template.html +0 -0
  506. /package/{scss → theme}/components/_accordion.scss +0 -0
  507. /package/{scss → theme}/components/_ajax.scss +0 -0
  508. /package/{scss → theme}/components/_alert.scss +0 -0
  509. /package/{scss → theme}/components/_avatar.scss +0 -0
  510. /package/{scss → theme}/components/_breadcrumbs.scss +0 -0
  511. /package/{scss → theme}/components/_card.scss +0 -0
  512. /package/{scss → theme}/components/_chip.scss +0 -0
  513. /package/{scss → theme}/components/_circular-progress.scss +0 -0
  514. /package/{scss → theme}/components/_editor.scss +0 -0
  515. /package/{scss → theme}/components/_empty-state.scss +0 -0
  516. /package/{scss → theme}/components/_filter.scss +0 -0
  517. /package/{scss → theme}/components/_link.scss +0 -0
  518. /package/{scss → theme}/components/_loader.scss +0 -0
  519. /package/{scss → theme}/components/_modal.scss +0 -0
  520. /package/{scss → theme}/components/_nav.scss +0 -0
  521. /package/{scss → theme}/components/_page-header.scss +0 -0
  522. /package/{scss → theme}/components/_popover.scss +0 -0
  523. /package/{scss → theme}/components/_progress.scss +0 -0
  524. /package/{scss → theme}/components/_prose.scss +0 -0
  525. /package/{scss → theme}/components/_scrollbar.scss +0 -0
  526. /package/{scss → theme}/components/_sections.scss +0 -0
  527. /package/{scss → theme}/components/_sidebar.scss +0 -0
  528. /package/{scss → theme}/components/_stat-card.scss +0 -0
  529. /package/{scss → theme}/components/_stepper.scss +0 -0
  530. /package/{scss → theme}/components/_timeline.scss +0 -0
  531. /package/{scss → theme}/components/_tooltip.scss +0 -0
  532. /package/{scss → theme}/components/_translations.scss +0 -0
  533. /package/{scss → theme}/config/_icons.scss +0 -0
  534. /package/{scss → theme}/config/_mixins.scss +0 -0
  535. /package/{scss → theme}/config/mixins/_ajax.scss +0 -0
  536. /package/{scss → theme}/config/mixins/_avatar.scss +0 -0
  537. /package/{scss → theme}/config/mixins/_breadcrumbs.scss +0 -0
  538. /package/{scss → theme}/config/mixins/_breakpoints.scss +0 -0
  539. /package/{scss → theme}/config/mixins/_chip.scss +0 -0
  540. /package/{scss → theme}/config/mixins/_circular-progress.scss +0 -0
  541. /package/{scss → theme}/config/mixins/_display.scss +0 -0
  542. /package/{scss → theme}/config/mixins/_empty-state.scss +0 -0
  543. /package/{scss → theme}/config/mixins/_focus.scss +0 -0
  544. /package/{scss → theme}/config/mixins/_interaction.scss +0 -0
  545. /package/{scss → theme}/config/mixins/_kbd.scss +0 -0
  546. /package/{scss → theme}/config/mixins/_link.scss +0 -0
  547. /package/{scss → theme}/config/mixins/_page-header.scss +0 -0
  548. /package/{scss → theme}/config/mixins/_position.scss +0 -0
  549. /package/{scss → theme}/config/mixins/_progress.scss +0 -0
  550. /package/{scss → theme}/config/mixins/_prose.scss +0 -0
  551. /package/{scss → theme}/config/mixins/_shadows.scss +0 -0
  552. /package/{scss → theme}/config/mixins/_sizing.scss +0 -0
  553. /package/{scss → theme}/config/mixins/_spacing.scss +0 -0
  554. /package/{scss → theme}/config/mixins/_stat-card.scss +0 -0
  555. /package/{scss → theme}/config/mixins/_stepper.scss +0 -0
  556. /package/{scss → theme}/config/mixins/_timeline.scss +0 -0
  557. /package/{scss → theme}/config/mixins/_transitions.scss +0 -0
  558. /package/{scss → theme}/config/mixins/_translations.scss +0 -0
  559. /package/{scss → theme}/tabler-icons.txt +0 -0
@@ -1,160 +1,171 @@
1
- # ln-ajax
2
-
3
- A zero-dependency, event-driven **HTML Fragment Swapping Primitive** that intercepts clicks on `<a>` elements and submits on `<form>` tags to enable instant, SPA-like navigation without full page reloads.
4
-
5
- It communicates via a structured server JSON protocol, exchanging targeted DOM updates, updating browser history states, and re-attaching lifecycle managers to newly injected nodes.
6
-
7
- ---
8
-
9
- ## 🧭 Philosophy & Architecture
10
-
11
- 1. **HTML-First Swapping:** The server remains the single source of truth for both data and markup. Instead of client routers rendering JSON arrays, the server compiles standard HTML fragments and returns them inside a structured JSON payload.
12
- 2. **Selective DOM Merges:** The response maps selector IDs (e.g. `main-content`) directly to their new HTML chunks, replacing only the specified regions in-place.
13
- 3. **Transparent Enhancements:** Intercepts only native interactions (links to the same origin, form submissions). Safely falls back to native browser redirects on errors or external hosts.
14
-
15
- ---
16
-
17
- ## 📦 Minimal Blueprint
18
-
19
- Wrap interactive elements or entire layouts with the `data-ln-ajax` selector.
20
-
21
- ```html
22
- <div data-ln-ajax>
23
- <!-- Clicking this fetches /dashboard and swaps only the returned targets -->
24
- <a href="/dashboard">Dashboard</a>
25
-
26
- <!-- Submitting this posts data and swaps target parts on success -->
27
- <form method="POST" action="/users/create">
28
- <input name="username" type="text" required>
29
- <button type="submit">Create User</button>
30
- </form>
31
-
32
- <!-- Exclude specific elements from AJAX handling -->
33
- <a href="/logout" data-ln-ajax="false">Logout</a>
34
- </div>
35
- ```
36
-
37
- ---
38
-
39
- ## 🛠️ Declarative API Contract
40
-
41
- ### HTML Attributes
42
-
43
- | Attribute | Elements | Values | Description |
44
- | :--- | :--- | :--- | :--- |
45
- | `data-ln-ajax` | Container, `<a>`, `<form>` | *none* (empty attribute) | Activates AJAX capture on the element and its descendants. Acts as a boolean flag. |
46
- | `data-ln-ajax="false"` | `<a>`, `<form>` | `"false"` | **The only supported value.** Excludes the specific link or form from AJAX interception. |
47
-
48
- > [!IMPORTANT]
49
- > **Attribute Value Restriction:** The `data-ln-ajax` attribute operates as a presence-based trigger (like a boolean flag).
50
- > * **The only valid and supported value is `"false"`**, which is used to exclude/opt-out specific elements inside an AJAX-enabled container.
51
- > * **Do not use custom values** (e.g., `data-ln-ajax="loginform"` or `data-ln-ajax="my-widget"`). The component does **not** accept, process, or assign behavior based on other string values. Using custom values will simply activate normal AJAX routing (acting the same as an empty attribute) but is a major anti-pattern that confuses both developers and AI agents.
52
-
53
- ### Server Response Protocol
54
-
55
- The server must return JSON with the `application/json` Content-Type:
56
-
57
- ```json
58
- {
59
- "title": "New Dashboard Page",
60
- "content": {
61
- "main-content": "<h1>Dashboard</h1><p>Welcome back!</p>",
62
- "sidebar-nav": "<ul><li>Active Nav Item</li></ul>"
63
- },
64
- "message": {
65
- "type": "success",
66
- "title": "User Created",
67
- "body": "The user was registered successfully."
68
- }
69
- }
70
- ```
71
-
72
- * **`title`**: Updates `document.title` on page swap.
73
- * **`content`**: Key-value pairs matching container `id` selectors to their new `innerHTML` content.
74
- * **`message`**: Optional. If present, automatically dispatches `ln-toast:enqueue` on the `window` to trigger native notifications.
75
-
76
- ---
77
-
78
- ## ⚡ DOM Events
79
-
80
- All events are dispatched on the initiating element (`<a>` or `<form>`) and bubble.
81
-
82
- | Event | Cancelable | Description | Payload (`detail`) |
83
- | :--- | :--- | :--- | :--- |
84
- | `ln-ajax:before-start` | **Yes** | Fires before any network activity. Call `e.preventDefault()` to cancel. | `{ method, url }` |
85
- | `ln-ajax:start` | No | Fires as the loader class is added and fetch begins. | `{ method, url }` |
86
- | `ln-ajax:success` | No | Fires after successful DOM swaps. | `{ method, url, data }` |
87
- | `ln-ajax:error` | No | Fires on HTTP status failure or network rejects. | `{ method, url, status, data }` or `{ method, url, error }` |
88
- | `ln-ajax:complete` | No | Fires at the very end of the lifecycle (success or error). | `{ method, url }` |
89
-
90
- ---
91
-
92
- ## ⚠️ Common Pitfalls
93
-
94
- - **Missing DOM IDs on Swap Targets:** If the server returns a key in `content` that does not match a mounting ID in the active document (e.g. `id="main-content"`), that segment swap fails silently.
95
- - **Forgetting CSRF Meta:** `ln-ajax` automatically reads `<meta name="csrf-token" content="...">` to inject the `X-CSRF-TOKEN` header on non-GET calls. If this meta tag is missing, POST/PUT requests may fail authentication.
96
- - **Breaking External Links:** Links with different hostnames are ignored automatically, but absolute paths on the same host are captured. Ensure assets/downloads use `data-ln-ajax="false"`.
97
- - **Forms carrying `data-ln-form-scope` are skipped entirely** (one-time `console.warn`) — the `ln-data-coordinator` write pipeline takes precedence over ajax progressive enhancement.
98
- - **Respects `e.defaultPrevented`.** If a prior `submit` listener on the same form (e.g. a validation gate) already called `preventDefault()`, `ln-ajax` does nothing — no fetch, no loader class.
99
-
100
- ---
101
-
102
- ## 🔧 Internals
103
-
104
- Source: `js/ln-ajax/ln-ajax.js`.
105
-
106
- ### Request lifecycle
107
-
108
- In order, per intercepted click/submit:
109
-
110
- 1. **Intercept** — click on `<a>` or submit on `<form>`. Modifier-key clicks (Ctrl/Cmd/middle) and `#`-href links pass through untouched.
111
- 2. **External-hostname skip** — if the link's hostname differs from `location.hostname`, ln-ajax aborts and the browser follows the link normally (no opt-out needed).
112
- 3. **`ln-ajax:before-start`** — cancelable; `preventDefault()` stops here (no spinner, no fetch).
113
- 4. **Spinner mount** — `.ln-ajax--loading` on the trigger, `<span class="ln-ajax-spinner">` appended, form buttons disabled.
114
- 5. **`ln-ajax:start`** — non-cancelable; fetch is about to begin.
115
- 6. **`fetch()`** — built from `href`/`action`, `method`, `FormData`. Headers always include `X-Requested-With`, `Accept: application/json`, and `X-CSRF-TOKEN` from the page meta. A `<input name="_method">` (auto-added by `ln-form`'s RESTful mode) rides in `FormData` transparently — the HTTP verb stays POST while `_method` travels in the body.
116
- 7. **Response** — see the branches below.
117
- 8. **`ln-ajax:success` / `ln-ajax:error`** — dispatched on the trigger, bubbling.
118
- 9. **Cleanup** — loading class + spinner removed, buttons re-enabled.
119
- 10. **`ln-ajax:complete`** — always fires last, success or error.
120
-
121
- Response branches:
122
- - **HTTP error** (`!response.ok`): parse JSON, dispatch `ln-ajax:error` `{ method, url, status, data }`, auto-toast if `data.message`.
123
- - **Fetch rejection** (network / JSON parse): dispatch `ln-ajax:error` `{ method, url, error }`.
124
- - **Success**: update `document.title`, swap `target.innerHTML` for each `data.content[id]`, auto-toast if `data.message`, `history.pushState` for `<a>` and GET `<form>`.
125
-
126
- ### MutationObserver flow
127
-
128
- A single observer on `document.body` handles three cases:
129
- - **New `data-ln-ajax` root injected** → `new lnAjax(node)` attaches listeners (covers AJAX-swapped frames and `innerHTML` writes).
130
- - **Elements injected inside a live root** → `findElements` re-runs on the subtree and attaches to any new `<a>`/`<form>` — no full re-init of the root.
131
- - **`data-ln-ajax` set on an existing element** → the attributes branch runs `new lnAjax(node)`, so programmatic opt-in works.
132
-
133
- ### DOM mutations performed
134
-
135
- | Phase | Mutation |
136
- |-------|----------|
137
- | Request start | `.ln-ajax--loading` on trigger |
138
- | Request start | `<span class="ln-ajax-spinner">` appended to trigger |
139
- | Request start | `disabled` on all `<button>` descendants of a `<form>` trigger |
140
- | Success | `target.innerHTML` replaced per `id` in `data.content` |
141
- | Success | `document.title` updated from `data.title` |
142
- | Success (`<a>` / GET `<form>`) | `history.pushState` |
143
- | Completion | loading class + spinner removed, form buttons re-enabled |
144
-
145
- ### `findElements` local divergence
146
-
147
- ln-ajax uses a local `findElements` rather than the ln-core helper of the same name: it needs a `{ links, forms }` partition (to bind `click` on links and `submit` on forms in one pass), where ln-core returns a flat list. The two must not be merged without updating all call sites.
148
-
149
- ### Trust boundary
150
-
151
- ln-ajax operates under a trusted, same-origin model: it expects every HTML fragment in the JSON response to be safe. It performs **no** client-side sanitization or regex script/attribute filtering — that would be fragile and would corrupt valid markup (e.g. stripping `data-ln-confirm` values that contain event-like substrings). Sanitizing user-submitted HTML is the server's job, done before the fragment is rendered. See [Security §5 — AJAX Fragment Trust Boundary](../../docs/architecture/security.md#5-ajax-fragment-trust-boundary).
152
-
153
- ### Error detail shape
154
-
155
- `ln-ajax:error` carries one of two shapes depending on failure mode:
156
-
157
- - **HTTP-status error** (response received, `!ok`): `{ method, url, status, data }` — `data` is the parsed body (may contain `message`).
158
- - **Fetch rejection** (network / DNS / JSON parse): `{ method, url, error }` — the caught `Error`, no `status`/`data`.
159
-
160
- A single `ln-ajax:error` listener must guard with `'status' in e.detail` before reading `status`, and `'error' in e.detail` before reading `error`. (`ln-api-connector`, the data-layer transport tier, emits a single unified error shape — `{ action, error, status, data }` — and is the pattern to prefer for new data-flow code.)
1
+ # ln-ajax
2
+
3
+ > Applied to a container, link, or form via `data-ln-ajax`. It intercepts native `<a>` clicks and `<form>` submits,
4
+ > calling `e.preventDefault()` and issuing an AJAX request with `X-Requested-With: XMLHttpRequest`. On receiving a JSON
5
+ > response containing HTML fragments (`{ content: { "#id": "<html>" } }`), it replaces the target elements in the DOM in-place
6
+ > and updates browser history via `history.pushState()`.
7
+
8
+ ---
9
+
10
+ ## 🧭 Philosophy & Architecture
11
+
12
+ 1. **HTML-First Swapping:** The server remains the single source of truth for both data and markup. Instead of client routers rendering JSON arrays, the server compiles standard HTML fragments and returns them inside a structured JSON payload.
13
+ 2. **Selective DOM Merges:** The response maps selector IDs (e.g. `main-content`) directly to their new HTML chunks, replacing only the specified regions in-place.
14
+ 3. **Transparent Enhancements:** Intercepts only native interactions (links to the same origin, form submissions). Safely falls back to native browser redirects on errors or external hosts.
15
+
16
+ ---
17
+
18
+ ## 📦 Minimal Blueprint
19
+
20
+ Wrap interactive elements or entire layouts with the `data-ln-ajax` selector.
21
+
22
+ ```html
23
+ <div data-ln-ajax>
24
+ <!-- Clicking this fetches /dashboard and swaps only the returned targets -->
25
+ <a href="/dashboard">Dashboard</a>
26
+
27
+ <!-- Submitting this posts data and swaps target parts on success -->
28
+ <form method="POST" action="/users/create">
29
+ <input name="username" type="text" required>
30
+ <button type="submit">Create User</button>
31
+ </form>
32
+
33
+ <!-- Exclude specific elements from AJAX handling -->
34
+ <a href="/logout" data-ln-ajax="false">Logout</a>
35
+ </div>
36
+ ```
37
+
38
+ ---
39
+
40
+ ## 🛠️ Declarative API Contract
41
+
42
+ ### HTML Attributes
43
+
44
+ | Attribute | Elements | Values | Description |
45
+ | :--- | :--- | :--- | :--- |
46
+ | `data-ln-ajax` | Container, `<a>`, `<form>` | *none* (empty attribute) | Activates AJAX capture on the element and its descendants. Acts as a boolean flag. |
47
+ | `data-ln-ajax="false"` | `<a>`, `<form>` | `"false"` | **The only supported value.** Excludes the specific link or form from AJAX interception. |
48
+
49
+ > [!IMPORTANT]
50
+ > **Attribute Value Restriction:** The `data-ln-ajax` attribute operates as a presence-based trigger (like a boolean flag).
51
+ > * **The only valid and supported value is `"false"`**, which is used to exclude/opt-out specific elements inside an AJAX-enabled container.
52
+ > * **Do not use custom values** (e.g., `data-ln-ajax="loginform"` or `data-ln-ajax="my-widget"`). The component does **not** accept, process, or assign behavior based on other string values. Using custom values will simply activate normal AJAX routing (acting the same as an empty attribute) but is a major anti-pattern that confuses both developers and AI agents.
53
+
54
+ ### Server Response Protocol
55
+
56
+ The server must return JSON with the `application/json` Content-Type:
57
+
58
+ ```json
59
+ {
60
+ "title": "New Dashboard Page",
61
+ "content": {
62
+ "main-content": "<h1>Dashboard</h1><p>Welcome back!</p>",
63
+ "sidebar-nav": "<ul><li>Active Nav Item</li></ul>"
64
+ },
65
+ "message": {
66
+ "type": "success",
67
+ "title": "User Created",
68
+ "body": "The user was registered successfully."
69
+ }
70
+ }
71
+ ```
72
+
73
+ * **`title`**: Updates `document.title` on page swap.
74
+ * **`content`**: Key-value pairs matching container `id` selectors to their new `innerHTML` content.
75
+ * **`message`**: Optional. When present, handled and dispatched as UI toasts by Layer 2 coordinators (such as `ln-ui-coordinator`). The `message.body` property is required to display the notification text; `message.title` and `message.type` (`"success"` | `"error"` | `"warning"` | `"info"`) are optional.
76
+
77
+ ---
78
+
79
+ ## ⚡ DOM Events
80
+
81
+ All events are dispatched on the initiating element (`<a>` or `<form>`) and bubble.
82
+
83
+ | Event | Cancelable | Description | Payload (`detail`) |
84
+ | :--- | :--- | :--- | :--- |
85
+ | `ln-ajax:before-start` | **Yes** | Fires before any network activity. Call `e.preventDefault()` to cancel. | `{ method, url }` |
86
+ | `ln-ajax:start` | No | Fires as the loader class is added and fetch begins. | `{ method, url }` |
87
+ | `ln-ajax:success` | No | Fires after successful DOM swaps. | `{ method, url, data }` |
88
+ | `ln-ajax:error` | No | Fires on HTTP status failure or network rejects. | `{ method, url, status, data, error }` |
89
+ | `ln-ajax:complete` | No | Fires at the very end of the lifecycle (success or error). | `{ method, url }` |
90
+ | `ln-ajax:aborted` | No | Fires when a newer GET supersedes this in-flight GET. Ends the lifecycle in place of `success`/`error`/`complete`. | `{ method, url }` |
91
+
92
+ Every `ln-ajax:start` is followed by exactly one of `ln-ajax:complete` or `ln-ajax:aborted`.
93
+
94
+ ---
95
+
96
+ ## ⚠️ Common Pitfalls
97
+
98
+ - **Missing DOM IDs on Swap Targets:** If the server returns a key in `content` that does not match a mounting ID in the active document (e.g. `id="main-content"`), that segment swap fails silently.
99
+ - **Forgetting CSRF Meta:** `ln-ajax` automatically reads `<meta name="csrf-token" content="...">` to inject the `X-CSRF-TOKEN` header on non-GET calls. If this meta tag is missing, POST/PUT requests may fail authentication.
100
+ - **Breaking External Links:** Links with different hostnames are ignored automatically, but absolute paths on the same host are captured. Ensure assets/downloads use `data-ln-ajax="false"`.
101
+ - **Forms carrying `data-ln-data-coordinator-scope` are skipped entirely** (one-time `console.warn`) — the `ln-data-coordinator` write pipeline takes precedence over ajax progressive enhancement.
102
+ - **Respects `e.defaultPrevented`.** If a prior `submit` listener on the same form (e.g. a validation gate) already called `preventDefault()`, `ln-ajax` does nothing — no fetch, no loader class.
103
+
104
+ ---
105
+
106
+ ## 🔧 Internals
107
+
108
+ Source: `components/ln-ajax/ln-ajax.js`.
109
+
110
+ ### Request lifecycle
111
+
112
+ In order, per intercepted click/submit:
113
+
114
+ 1. **Intercept** — click on `<a>` or submit on `<form>`. Modifier-key clicks (Ctrl/Cmd/middle) and `#`-href links pass through untouched.
115
+ 2. **External-hostname skip** — if the link's hostname differs from `location.hostname`, ln-ajax aborts and the browser follows the link normally (no opt-out needed).
116
+ 3. **`ln-ajax:before-start`** — cancelable; `preventDefault()` stops here (no spinner, no fetch).
117
+ - **Supersede (GET only)** — if not prevented, a GET aborts the in-flight GET, wherever it started: its response writes document-wide (`document.title`, ids, history). The aborted request cleans up and emits `ln-ajax:aborted` before the new one starts. Other methods are never aborted and never abort.
118
+ 4. **Spinner mount** — `.ln-ajax--loading` on the trigger, `<span class="ln-ajax-spinner">` appended, form buttons disabled.
119
+ 5. **`ln-ajax:start`** — non-cancelable; fetch is about to begin.
120
+ 6. **`fetch()`** — built from `href`/`action`, `method`, `FormData`. Headers always include `X-Requested-With`, `Accept: application/json`, and `X-CSRF-TOKEN` from the page meta. A `<input name="_method">` (auto-added by `ln-form`'s RESTful mode) rides in `FormData` transparently — the HTTP verb stays POST while `_method` travels in the body.
121
+ 7. **Response** — see the branches below.
122
+ 8. **`ln-ajax:success` / `ln-ajax:error`** — dispatched on the trigger, bubbling.
123
+ 9. **Cleanup** — loading class + spinner removed, buttons re-enabled.
124
+ 10. **`ln-ajax:complete`** — fires last, success or error. A superseded GET ends with `ln-ajax:aborted` instead.
125
+
126
+ Response branches:
127
+ - **HTTP error or non-JSON body** (`!response.ok`, or a body `JSON.parse` rejects): dispatch `ln-ajax:error` `{ method, url, status, data, error }`.
128
+ - **Fetch rejection** (network / DNS / offline): dispatch `ln-ajax:error` `{ method, url, status: 0, data: null, error }`.
129
+ - **Success**: update `document.title`, swap `target.innerHTML` for each `data.content[id]`, `history.pushState` for `<a>` and GET `<form>`.
130
+
131
+ `data.message` on `success`/`error` is toasted by `ln-ui-coordinator`, not by ln-ajax.
132
+ - **Superseded** (GET only): clean up, dispatch `ln-ajax:aborted` `{ method, url }`. No `success`/`error`/`complete`, no DOM swap, no `pushState`.
133
+
134
+ ### MutationObserver flow
135
+
136
+ A single observer on `document.body` handles three cases:
137
+ - **New `data-ln-ajax` root injected** → `new lnAjax(node)` attaches listeners (covers AJAX-swapped frames and `innerHTML` writes).
138
+ - **Elements injected inside a live root** → `findElements` re-runs on the subtree and attaches to any new `<a>`/`<form>` — no full re-init of the root.
139
+ - **`data-ln-ajax` set on an existing element** → the attributes branch runs `new lnAjax(node)`, so programmatic opt-in works.
140
+
141
+ ### DOM mutations performed
142
+
143
+ | Phase | Mutation |
144
+ |-------|----------|
145
+ | Request start | `.ln-ajax--loading` on trigger |
146
+ | Request start | `<span class="ln-ajax-spinner">` appended to trigger |
147
+ | Request start | `disabled` on all `<button>` descendants of a `<form>` trigger |
148
+ | Success | `target.innerHTML` replaced per `id` in `data.content` |
149
+ | Success | `document.title` updated from `data.title` |
150
+ | Success (`<a>` / GET `<form>`) | `history.pushState` |
151
+ | Completion / abort | loading class + spinner removed, form buttons re-enabled |
152
+
153
+ ### `findElements` local divergence
154
+
155
+ ln-ajax uses a local `findElements` rather than the ln-core helper of the same name: it needs a `{ links, forms }` partition (to bind `click` on links and `submit` on forms in one pass), where ln-core returns a flat list. The two must not be merged without updating all call sites.
156
+
157
+ ### Trust boundary
158
+
159
+ ln-ajax operates under a trusted, same-origin model: it expects every HTML fragment in the JSON response to be safe. It performs **no** client-side sanitization or regex script/attribute filtering — that would be fragile and would corrupt valid markup (e.g. stripping `data-ln-confirm` values that contain event-like substrings). Sanitizing user-submitted HTML is the server's job, done before the fragment is rendered. See [Security §5 — AJAX Fragment Trust Boundary](../../docs/architecture/security.md#5-ajax-fragment-trust-boundary).
160
+
161
+ ### Error detail shape
162
+
163
+ `ln-ajax:error` always carries all five keys `{ method, url, status, data, error }`:
164
+
165
+ | Failure | `status` | `data` | `error` |
166
+ |---|---|---|---|
167
+ | HTTP error (`!ok`) | response status | parsed body (may contain `message`), or `null` | `null` |
168
+ | Body is not JSON | response status | `null` | the `SyntaxError` |
169
+ | Network / DNS / offline | `0` | `null` | the caught `Error` |
170
+
171
+ `status === 0` identifies a network failure.
@@ -0,0 +1,8 @@
1
+ @use '../../theme/config/mixins' as *;
2
+
3
+ [data-ln-debug] {
4
+ // 1. AJAX Form Action Check: forms with data-ln-ajax must declare an action URL
5
+ form[data-ln-ajax]:not([action]) {
6
+ @include dev-inline-error("form[data-ln-ajax] is missing a required 'action' attribute");
7
+ }
8
+ }
@@ -0,0 +1 @@
1
+ (function(){"use strict";function U(){return typeof window>"u"?!1:window.lnDebug===!0||window.lnCore&&window.lnCore._debugSink?!0:typeof document<"u"&&document.body?document.body.hasAttribute("data-ln-debug")||document.body.querySelector("[data-ln-debug]")!==null:!1}function F(e){if(typeof window>"u")return!1;if(window.lnDebug===!0)return!0;if(window.lnCore&&window.lnCore._debugIsContained)return e?window.lnCore._debugIsContained(e):window.lnCore._debugSink!==null;if(e&&e.closest){const t=e.closest("[data-ln-debug]");return!(!t||typeof document<"u"&&t===document.documentElement)}return typeof document<"u"&&document.body?document.body.hasAttribute("data-ln-debug"):!1}if(typeof window<"u"&&(window.lnCore=window.lnCore||{},!window.lnCore._warnBound)){window.lnCore._warnBound=!0;const e=console.warn;console.warn=function(...t){if(typeof t[0]=="string"&&(t[0].startsWith("[ln-")||t[0].startsWith("[lnCore"))){let r=null;for(let l=1;l<t.length;l++)if(t[l]&&t[l].nodeType===1){r=t[l];break}if(!F(r))return}e.apply(console,t)}}function y(e,t,n){const r=n||{};window.lnCore._debugSink&&window.lnCore._debugSink("event",t,e,r),e.dispatchEvent(new CustomEvent(t,{bubbles:!0,detail:r}))}function G(e,t,n){const r=n||{};window.lnCore._debugSink&&window.lnCore._debugSink("event",t,e,r);const l=new CustomEvent(t,{bubbles:!0,cancelable:!0,detail:r});return e.dispatchEvent(l),l}function E(e,t){if(!document.body){document.addEventListener("DOMContentLoaded",function(){E(e,t)}),console.warn("["+t+'] Script loaded before <body> — add "defer" to your <script> tag');return}e()}function K(e,t){if(e.ctrlKey||e.metaKey||e.shiftKey||e.altKey||e.button!==0||!t)return!1;const n=t.getAttribute("href");return!(!n||t.getAttribute("target")==="_blank"||t.hasAttribute("download")||n.startsWith("mailto:")||n.startsWith("tel:")||n==="#"||n.startsWith("#")||t.hostname&&t.hostname!==window.location.hostname)}const q={};function H(e,t){q[e]=t}function P(e){return q[e]||{ingress:t=>t,egress:t=>t}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=H,window.lnCore.getDataMapper=P);function X(){typeof window>"u"||(window.lnCore=window.lnCore||{},!window.lnCore._localeObserverBound&&(window.lnCore._localeObserverBound=!0,E(function(){new MutationObserver(function(){y(document,"ln-core:locale-change",{})}).observe(document.documentElement,{attributes:!0,attributeFilter:["lang"],subtree:!0})},"ln-core")))}const M={};function J(e,t){if(!e||typeof t!="object")return;const n=e.toLowerCase().split("-")[0];M[n]=t}function Q(e){if(!e)return null;const t=e.toLowerCase().split("-")[0];return M[t]||null}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerLocaleFallback=J,window.lnCore.getLocaleFallback=Q,window.lnCore.ensureLocaleObserver=X);const O={};function z(e,t){O[e]||(O[e]=document.querySelector('[data-ln-template="'+e+'"]'));const n=O[e];return n?n.content.cloneNode(!0):(console.warn("["+(t||"ln-core")+'] Template "'+e+'" not found'),null)}function W(e,t){if(!e||!t)return e;const n=e.querySelectorAll("[data-ln-field]");for(let o=0;o<n.length;o++){const u=n[o],s=u.getAttribute("data-ln-field");t[s]!=null&&(u.textContent=t[s])}const r=e.querySelectorAll("[data-ln-attr]");for(let o=0;o<r.length;o++){const u=r[o],s=u.getAttribute("data-ln-attr").split(",");for(let d=0;d<s.length;d++){const h=s[d].trim().split(":");if(h.length!==2)continue;const p=h[0].trim(),a=h[1].trim();t[a]!=null&&u.setAttribute(p,t[a])}}const l=e.querySelectorAll("[data-ln-show]");for(let o=0;o<l.length;o++){const u=l[o],s=u.getAttribute("data-ln-show");s in t&&u.classList.toggle("hidden",!t[s])}const i=e.querySelectorAll("[data-ln-class]");for(let o=0;o<i.length;o++){const u=i[o],s=u.getAttribute("data-ln-class").split(",");for(let d=0;d<s.length;d++){const h=s[d].trim().split(":");if(h.length!==2)continue;const p=h[0].trim(),a=h[1].trim();a in t&&u.classList.toggle(p,!!t[a])}}return e}function Y(e,t){e.matches&&e.matches("[data-ln-form], [data-ln-fillable]")&&(window.lnCore._debugSink&&window.lnCore._debugSink("event","ln-fill",e,t??null),e.dispatchEvent(new CustomEvent("ln-fill",{detail:t??null,bubbles:!0})));const n=e.querySelectorAll("[data-ln-form], [data-ln-fillable]");for(let r=0;r<n.length;r++)window.lnCore._debugSink&&window.lnCore._debugSink("event","ln-fill",n[r],t??null),n[r].dispatchEvent(new CustomEvent("ln-fill",{detail:t??null,bubbles:!0}));return e}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore._fillBound||(window.lnCore._fillBound=!0,document.addEventListener("ln-fill",function(e){if(!(!e.target.matches||!e.target.matches("[data-ln-fillable]")))if(e.detail)W(e.target,e.detail);else{const t=e.target.querySelectorAll("[data-ln-field]");for(let n=0;n<t.length;n++)t[n].textContent=""}})));function I(e,t){if(!e||!t)return e;const n=document.createTreeWalker(e,NodeFilter.SHOW_TEXT);for(;n.nextNode();){const i=n.currentNode;i.textContent.indexOf("{{")!==-1&&(i.textContent=i.textContent.replace(/\{\{\s*(\w+)\s*\}\}/g,function(o,u){return t[u]!==void 0?t[u]:""}))}const r=function(i,o){return t[o]!==void 0?t[o]:""},l=Array.from(e.querySelectorAll("*"));e.nodeType===1&&l.push(e);for(let i=0;i<l.length;i++){const o=l[i],u=o.attributes;for(let s=0;s<u.length;s++){const d=u[s];d.value.indexOf("{{")!==-1&&o.setAttribute(d.name,d.value.replace(/\{\{\s*(\w+)\s*\}\}/g,r))}}return e}function Z(e,t,n,r,l,i){const o={};for(let s=0;s<e.children.length;s++){const d=e.children[s],h=d.getAttribute("data-ln-render-key");h&&(o[h]=d)}const u=document.createDocumentFragment();for(let s=0;s<t.length;s++){const d=t[s],h=String(r(d));let p=o[h];if(p)l(p,d,s);else{const a=z(n,i);if(!a||(I(a,d),p=a.firstElementChild,!p))continue;p.setAttribute("data-ln-render-key",h),l(p,d,s)}u.appendChild(p)}e.textContent="",e.appendChild(u)}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.fillTemplate=I,window.lnCore.fill=W,window.lnCore.lnFill=Y,window.lnCore.renderList=Z);function ee(e,t,n,r,l){if(!e||t===null)return!0;const i=r||"ln-component",o=e.type;if(o==="trigger"||o==="marker"||o==="string"||o==="list"||!o||t===""&&e.fallback!==void 0)return!0;const u=s=>{l?console.warn(s,l):console.warn(s)};if(o==="boolean"){const s=t.trim().toLowerCase();return s!==""&&s!=="true"&&s!=="false"&&s!=="1"&&s!=="0"?(u(`[${i}] Invalid value "${t}" for boolean attribute "${n}". Allowed: "true", "false", or presence-only.`),!1):!0}if(o==="enum"){const s=e.values||[];return s.includes(t)?!0:(u(`[${i}] Invalid value "${t}" for attribute "${n}". Allowed: ${s.join(", ")}. Fallback: "${e.fallback}".`),!1)}if(o==="integer"){if(!/^-?\d+$/.test(t))return u(`[${i}] Invalid integer "${t}" for attribute "${n}". Fallback: ${e.fallback}.`),!1;const s=parseInt(t,10);return e.min!==void 0&&s<e.min?(u(`[${i}] Value ${s} for attribute "${n}" is less than min (${e.min}).`),!1):e.max!==void 0&&s>e.max?(u(`[${i}] Value ${s} for attribute "${n}" is greater than max (${e.max}).`),!1):!0}if(o==="float")return isNaN(Number(t))?(u(`[${i}] Invalid float "${t}" for attribute "${n}". Fallback: ${e.fallback}.`),!1):!0;if(o==="json")try{return JSON.parse(t),!0}catch(s){return u(`[${i}] Invalid JSON for attribute "${n}": ${s.message}. Fallback: ${e.fallback}.`),!1}return!0}function te(e){if(e.onAttrChange&&!e.declared)return null;const t=new Set;if(e.effects)for(const n in e.effects)t.add(n);if(e.onAttrChange&&e.declared)for(const n of e.declared)t.add(n);return t}function ne(e,t,n,r){if(e.nodeType!==1)return;const i=t.indexOf("[")!==-1||t.indexOf(".")!==-1||t.indexOf("#")!==-1?t:"["+t+"]",o=Array.from(e.querySelectorAll(i));e.matches&&e.matches(i)&&o.push(e);for(const u of o)if(!u[n]){window.lnCore._persistSink&&u.hasAttribute("data-ln-persist")&&window.lnCore._persistSink(u,t);try{u[n]=new r(u)}catch(s){console.error("["+n+"] init failed",u,s)}}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore._bootHolds=window.lnCore._bootHolds||0,window.lnCore._bootQueue=window.lnCore._bootQueue||[]);function N(){window.lnCore=window.lnCore||{};const e=window.lnCore._attrRegistry=window.lnCore._attrRegistry||{byAttr:new Map,reactive:[],persist:[]};return e.byReactive=e.byReactive||new Map,e.reactiveWildcard=e.reactiveWildcard||[],e}function re(e){const t=N(),n=e.observed||[];for(let r=0;r<n.length;r++){const l=n[r];t.byAttr.has(l)||t.byAttr.set(l,[]),t.byAttr.get(l).push(e)}if(t.byDeclaredAttr=t.byDeclaredAttr||new Map,e.attributes)for(const r in e.attributes){t.byDeclaredAttr.has(r)||t.byDeclaredAttr.set(r,[]);const l=t.byDeclaredAttr.get(r);l.some(i=>i.componentTag===e.componentTag)||l.push({spec:e.attributes[r],componentTag:e.componentTag})}if(e.onAttrChange||e.effects){t.reactive.push(e);const r=te(e);if(r===null)t.reactiveWildcard.push(e);else for(const l of r)t.byReactive.has(l)||t.byReactive.set(l,[]),t.byReactive.get(l).push(e)}e.persist&&t.persist.push(e)}function B(e,t,n,r){for(let l=0;l<e.length;l++){const i=e[l];if(!t[i.attribute])continue;const o=i.effects&&i.effects[n];o?o(t,n,r):i.onAttrChange&&(!i.declared||i.declared.has(n))&&i.onAttrChange(t,n,r)}}function oe(e){const t=e.target,n=e.attributeName;if(e.oldValue===t.getAttribute(n))return;const r=N(),l=r.byAttr.get(n);if(U()&&r.byDeclaredAttr&&r.byDeclaredAttr.has(n)&&F(t)){const i=r.byDeclaredAttr.get(n),o=t.getAttribute(n);for(let u=0;u<i.length;u++)ee(i[u].spec,o,n,i[u].componentTag,t)}if(window.lnCore._debugSink&&(n.indexOf("data-ln-")===0||l)&&window.lnCore._debugSink("attr",n,t,{oldValue:e.oldValue,newValue:t.getAttribute(n)}),n.indexOf("data-ln-")===0){const i=r.byReactive.get(n);i&&B(i,t,n,e.oldValue),r.reactiveWildcard.length&&B(r.reactiveWildcard,t,n,e.oldValue)}if(l)for(let i=0;i<l.length;i++){const o=l[i];if(o.handler){o.handler(t,n,e.oldValue);continue}o.onAttributeChange&&t[o.attribute]?o.onAttributeChange(t,n):(ne(t,o.selector,o.attribute,o.ComponentFn),o.onInit&&o.onInit(t))}}function ie(){window.lnCore=window.lnCore||{},!window.lnCore._attrObserverBound&&(window.lnCore._attrObserverBound=!0,E(function(){new MutationObserver(function(t){for(let n=0;n<t.length;n++)try{oe(t[n])}catch(r){console.error("[ln-core] mutation handler failed",t[n].target,r)}}).observe(document.body,{attributes:!0,subtree:!0,attributeOldValue:!0})},"ln-core"))}function le(e,t){re({observed:e,handler:t}),ie()}function R(e){return(e||"").replace(/^#/,"")}function D(e){const t=e===void 0?location.hash:e,n={},r=R(t);if(!r)return n;const l=r.split("&");for(let i=0;i<l.length;i++){const o=l[i];if(!o)continue;const u=o.indexOf(":"),s=u>-1?o.slice(0,u):o,d=u>-1?o.slice(u+1):"";if(s)try{n[s]=decodeURIComponent(d)}catch{n[s]=d}}return n}function se(e){if(!e)return null;const t=D();return e in t?t[e]:null}function ae(e,t){if(!e)return;const n=D();t==null?delete n[e]:n[e]=String(t);const l=Object.keys(n).map(function(i){const o=n[i];return o===""?i:i+":"+encodeURIComponent(o)}).join("&");R(location.hash)!==l&&(location.hash=l)}function ue(e){return e.button===1||e.ctrlKey||e.metaKey||e.shiftKey?!1:(e.preventDefault(),!0)}function ce(e,t){if(!e||!e.hasAttribute("data-ln-hash"))return null;const n=e.getAttribute("data-ln-hash");if(n&&n.trim()!=="")return n.trim();const r=e.getAttribute("data-ln-sort")||e.getAttribute("data-ln-search-for")||e.getAttribute("data-ln-search")||e.getAttribute("data-ln-filter")||e.id;return r?t?r+"-"+t:r:t||null}function fe(e,t){return!t||t==="none"||e===null||e===void 0?null:String(e)+"."+t}function de(e){return!e||typeof e!="string"?null:e.endsWith(".asc")?{fieldOrColumn:e.slice(0,-4),direction:"asc"}:e.endsWith(".desc")?{fieldOrColumn:e.slice(0,-5),direction:"desc"}:null}function we(e,t){return!e||!Array.isArray(t)||t.length===0?null:e+":"+t.map(encodeURIComponent).join(",")}function be(e){if(!e||typeof e!="string")return null;const t=e.indexOf(":");if(t===-1)return null;const n=e.slice(0,t),r=e.slice(t+1),l=r?r.split(",").map(function(i){try{return decodeURIComponent(i)}catch{return i}}).filter(Boolean):[];return{key:n,values:l}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.hashParse=D,window.lnCore.hashGet=se,window.lnCore.hashSet=ae,window.lnCore.hashLinkClick=ue,window.lnCore.resolveHashNamespace=ce,window.lnCore.hashSortEncode=fe,window.lnCore.hashSortDecode=de,window.lnCore.hashFilterEncode=we,window.lnCore.hashFilterDecode=be),(function(){const e="data-ln-ajax",t="lnAjax",n="data-ln-data-coordinator-scope";if(window[t]!==void 0)return;let r=null;function l(a){if(!a.hasAttribute(e)||a[t])return;a[t]=!0;const c=d(a);i(c.links),o(c.forms)}function i(a){for(const c of a){if(c[t+"Trigger"]||c.hostname&&c.hostname!==window.location.hostname)continue;const w=c.getAttribute("href");if(w&&w.includes("#"))continue;const f=function(m){if(!K(m,c))return;m.preventDefault();const C=c.getAttribute("href");C&&s("GET",C,null,c)};c.addEventListener("click",f),c[t+"Trigger"]=f}}function o(a){for(const c of a){if(c[t+"Trigger"])continue;if(c.hasAttribute(n)){c[t+"ScopeWarned"]||(c[t+"ScopeWarned"]=!0,console.warn("[ln-ajax] Form has data-ln-data-coordinator-scope — the ln-data-coordinator write pipeline takes precedence; skipping ajax interception for this form."));continue}const w=function(f){if(f.defaultPrevented)return;f.preventDefault();const m=c.method.toUpperCase(),C=c.action,T=new FormData(c);for(const v of c.querySelectorAll('button, input[type="submit"]'))v.disabled=!0;s(m,C,T,c,function(){for(const v of c.querySelectorAll('button, input[type="submit"]'))v.disabled=!1})};c.addEventListener("submit",w),c[t+"Trigger"]=w}}function u(a){if(!a[t])return;const c=d(a);for(const w of c.links)w[t+"Trigger"]&&(w.removeEventListener("click",w[t+"Trigger"]),delete w[t+"Trigger"]);for(const w of c.forms)w[t+"Trigger"]&&(w.removeEventListener("submit",w[t+"Trigger"]),delete w[t+"Trigger"]);delete a[t]}function s(a,c,w,f,m){if(G(f,"ln-ajax:before-start",{method:a,url:c}).defaultPrevented)return;a==="GET"&&r&&r(),y(f,"ln-ajax:start",{method:a,url:c}),f.classList.add("ln-ajax--loading");const T=document.createElement("span");T.className="ln-ajax-spinner",f.appendChild(T);function v(){f.classList.remove("ln-ajax--loading");const b=f.querySelector(".ln-ajax-spinner");b&&b.remove(),m&&m()}let A=c;const V=document.querySelector('meta[name="csrf-token"]'),L=V?V.getAttribute("content"):null;w instanceof FormData&&L&&w.append("_token",L);const j={method:a,headers:{"X-Requested-With":"XMLHttpRequest",Accept:"application/json"}};if(L&&(j.headers["X-CSRF-TOKEN"]=L),a==="GET"&&w){const b=new URLSearchParams(w);A=c+(c.includes("?")?"&":"?")+b.toString()}else a!=="GET"&&w&&(j.body=w);let _=null;if(a==="GET"){const b=new AbortController;j.signal=b.signal,_=function(){r=null,b.abort(),v(),y(f,"ln-ajax:aborted",{method:a,url:A})},r=_}fetch(A,j).then(function(b){const $=b.ok,g=b.status;return b.text().then(function(S){let k=null,x=null;if(S&&S.trim())try{k=JSON.parse(S)}catch(he){x=he}return{ok:$,status:g,data:k,parseError:x}})}).then(function(b){_&&r===_&&(r=null);const $=b.status,g=b.data,S=b.parseError;if(b.ok&&!S){if(g&&g.title&&(document.title=g.title),g&&g.content)for(const k in g.content){const x=document.getElementById(k);x&&(x.innerHTML=g.content[k])}if(f.tagName==="A"){const k=f.getAttribute("href");k&&window.history.pushState({ajax:!0},"",k)}else f.tagName==="FORM"&&f.method.toUpperCase()==="GET"&&window.history.pushState({ajax:!0},"",A);y(f,"ln-ajax:success",{method:a,url:A,data:g})}else y(f,"ln-ajax:error",{method:a,url:A,status:$,data:g,error:S||null});v(),y(f,"ln-ajax:complete",{method:a,url:A})}).catch(function(b){b&&b.name==="AbortError"||(_&&r===_&&(r=null),y(f,"ln-ajax:error",{method:a,url:A,status:0,data:null,error:b}),v(),y(f,"ln-ajax:complete",{method:a,url:A}))})}function d(a){const c={links:[],forms:[]};return a.tagName==="A"&&a.getAttribute(e)!=="false"?c.links.push(a):a.tagName==="FORM"&&a.getAttribute(e)!=="false"?c.forms.push(a):(c.links=Array.from(a.querySelectorAll('a:not([data-ln-ajax="false"])')),c.forms=Array.from(a.querySelectorAll('form:not([data-ln-ajax="false"])'))),c}function h(){E(function(){new MutationObserver(function(c){for(const w of c)if(w.type==="childList"){for(const f of w.addedNodes)if(f.nodeType===1&&(l(f),!f.hasAttribute(e))){for(const C of f.querySelectorAll("["+e+"]"))l(C);const m=f.closest&&f.closest("["+e+"]");if(m&&m.getAttribute(e)!=="false"){const C=d(f);i(C.links),o(C.forms)}}}}).observe(document.body,{childList:!0,subtree:!0}),le([e],function(c){l(c)})},"ln-ajax")}function p(){for(const a of document.querySelectorAll("["+e+"]"))l(a)}window[t]=l,window[t].destroy=u,h(),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",p):p()})()})();
@@ -0,0 +1,30 @@
1
+ {
2
+ "$comment": "This file is partly generated. The $comment, component, generator, and each attribute's direction, sources, type, values, fallback, and description keys are owned by the generator and synchronized from JS source code. All other keys are preserved and safe to hand-author. Regenerate with `npm run sync:ln-schemas`.",
3
+ "component": "ln-ajax",
4
+ "generator": "scripts/sync-ln-schemas.mjs",
5
+ "attributes": {
6
+ "data-ln-ajax": {
7
+ "direction": "author",
8
+ "sources": [
9
+ "ln-ajax-dev.scss",
10
+ "src/ln-ajax.js"
11
+ ],
12
+ "type": "marker",
13
+ "description": "Enables AJAX interception of form submissions and anchor clicks within container"
14
+ },
15
+ "data-ln-data-coordinator-scope": {
16
+ "direction": "author",
17
+ "sources": [
18
+ "src/ln-ajax.js"
19
+ ],
20
+ "type": "string",
21
+ "description": "Excludes form from ln-ajax interception to let data coordinator handle submission"
22
+ },
23
+ "data-ln-debug": {
24
+ "direction": null,
25
+ "sources": [
26
+ "ln-ajax-dev.scss"
27
+ ]
28
+ }
29
+ }
30
+ }
@@ -1,12 +1,22 @@
1
- import { guardBody, dispatch, dispatchCancelable, shouldInterceptLink } from '../../ln-core';
1
+ import { guardBody, dispatch, dispatchCancelable, shouldInterceptLink, observeAttributes } from '../../ln-core';
2
2
 
3
3
  (function () {
4
4
  const DOM_SELECTOR = 'data-ln-ajax';
5
5
  const DOM_ATTRIBUTE = 'lnAjax';
6
- const SCOPE_ATTR = 'data-ln-form-scope';
6
+ const SCOPE_ATTR = 'data-ln-data-coordinator-scope';
7
7
 
8
8
  if (window[DOM_ATTRIBUTE] !== undefined) return;
9
9
 
10
+ // A GET response writes document-wide (title, ids, history), so a newer GET
11
+ // supersedes the in-flight one wherever it started. Other methods are never aborted.
12
+ let abortInflightGet = null;
13
+
14
+ // ─── Attribute Contract (SSOT) ──────────────────────────
15
+ const ATTRIBUTES = {
16
+ 'data-ln-ajax': { type: 'marker', description: 'Enables AJAX interception of form submissions and anchor clicks within container' },
17
+ 'data-ln-data-coordinator-scope': { type: 'string', description: 'Excludes form from ln-ajax interception to let data coordinator handle submission' }
18
+ };
19
+
10
20
  function _constructor(domRoot) {
11
21
  if (!domRoot.hasAttribute(DOM_SELECTOR)) return;
12
22
  if (domRoot[DOM_ATTRIBUTE]) return;
@@ -49,7 +59,7 @@ import { guardBody, dispatch, dispatchCancelable, shouldInterceptLink } from '..
49
59
  if (form.hasAttribute(SCOPE_ATTR)) {
50
60
  if (!form[DOM_ATTRIBUTE + 'ScopeWarned']) {
51
61
  form[DOM_ATTRIBUTE + 'ScopeWarned'] = true;
52
- console.warn('[ln-ajax] Form has data-ln-form-scope — the ln-data-coordinator write pipeline takes precedence; skipping ajax interception for this form.');
62
+ console.warn('[ln-ajax] Form has data-ln-data-coordinator-scope — the ln-data-coordinator write pipeline takes precedence; skipping ajax interception for this form.');
53
63
  }
54
64
  continue;
55
65
  }
@@ -101,6 +111,8 @@ import { guardBody, dispatch, dispatchCancelable, shouldInterceptLink } from '..
101
111
  const before = dispatchCancelable(element, 'ln-ajax:before-start', { method: method, url: url });
102
112
  if (before.defaultPrevented) return;
103
113
 
114
+ if (method === 'GET' && abortInflightGet) abortInflightGet();
115
+
104
116
  dispatch(element, 'ln-ajax:start', { method: method, url: url });
105
117
 
106
118
  element.classList.add('ln-ajax--loading');
@@ -143,22 +155,48 @@ import { guardBody, dispatch, dispatchCancelable, shouldInterceptLink } from '..
143
155
  options.body = data;
144
156
  }
145
157
 
158
+ let abort = null;
159
+ if (method === 'GET') {
160
+ const controller = new AbortController();
161
+ options.signal = controller.signal;
162
+ abort = function () {
163
+ abortInflightGet = null;
164
+ controller.abort();
165
+ _cleanup();
166
+ dispatch(element, 'ln-ajax:aborted', { method: method, url: finalUrl });
167
+ };
168
+ abortInflightGet = abort;
169
+ }
170
+
146
171
  fetch(finalUrl, options)
147
172
  .then(function (response) {
148
173
  const ok = response.ok;
149
- return response.json().then(function (data) {
150
- return { ok: ok, status: response.status, data: data };
174
+ const status = response.status;
175
+ return response.text().then(function (text) {
176
+ let data = null;
177
+ let parseError = null;
178
+ if (text && text.trim()) {
179
+ try {
180
+ data = JSON.parse(text);
181
+ } catch (err) {
182
+ parseError = err;
183
+ }
184
+ }
185
+ return { ok: ok, status: status, data: data, parseError: parseError };
151
186
  });
152
187
  })
153
188
  .then(function (result) {
189
+ if (abort && abortInflightGet === abort) abortInflightGet = null;
190
+ const status = result.status;
154
191
  const data = result.data;
192
+ const parseError = result.parseError;
155
193
 
156
- if (result.ok) {
157
- if (data.title) {
194
+ if (result.ok && !parseError) {
195
+ if (data && data.title) {
158
196
  document.title = data.title;
159
197
  }
160
198
 
161
- if (data.content) {
199
+ if (data && data.content) {
162
200
  for (const targetId in data.content) {
163
201
  const targetElement = document.getElementById(targetId);
164
202
  if (targetElement) {
@@ -178,29 +216,26 @@ import { guardBody, dispatch, dispatchCancelable, shouldInterceptLink } from '..
178
216
 
179
217
  dispatch(element, 'ln-ajax:success', { method: method, url: finalUrl, data: data });
180
218
  } else {
181
- dispatch(element, 'ln-ajax:error', { method: method, url: finalUrl, status: result.status, data: data });
219
+ dispatch(element, 'ln-ajax:error', {
220
+ method: method,
221
+ url: finalUrl,
222
+ status: status,
223
+ data: data,
224
+ error: parseError || null
225
+ });
182
226
  }
183
227
 
184
- // Auto-dispatch response message as a toast event.
185
- // Any listener (ln-toast by default) can pick it up.
186
- if (data.message) {
187
- const msg = data.message;
188
- window.dispatchEvent(new CustomEvent('ln-toast:enqueue', {
189
- detail: {
190
- type: msg.type || (result.ok ? 'success' : 'error'),
191
- title: msg.title || '',
192
- message: msg.body || ''
193
- }
194
- }));
195
- }
196
-
197
- dispatch(element, 'ln-ajax:complete', { method: method, url: finalUrl });
198
228
  _cleanup();
229
+ dispatch(element, 'ln-ajax:complete', { method: method, url: finalUrl });
199
230
  })
200
231
  .catch(function (error) {
201
- dispatch(element, 'ln-ajax:error', { method: method, url: finalUrl, error: error });
202
- dispatch(element, 'ln-ajax:complete', { method: method, url: finalUrl });
232
+ // Superseded: abort() already cleaned up and emitted ln-ajax:aborted
233
+ if (error && error.name === 'AbortError') return;
234
+ if (abort && abortInflightGet === abort) abortInflightGet = null;
235
+ // True network failure / DNS / offline (fetch itself threw)
236
+ dispatch(element, 'ln-ajax:error', { method: method, url: finalUrl, status: 0, data: null, error: error });
203
237
  _cleanup();
238
+ dispatch(element, 'ln-ajax:complete', { method: method, url: finalUrl });
204
239
  });
205
240
  }
206
241
 
@@ -243,17 +278,17 @@ import { guardBody, dispatch, dispatchCancelable, shouldInterceptLink } from '..
243
278
  }
244
279
  }
245
280
  }
246
- } else if (mutation.type === 'attributes') {
247
- _constructor(mutation.target);
248
281
  }
249
282
  }
250
283
  });
251
284
 
252
285
  observer.observe(document.body, {
253
286
  childList: true,
254
- subtree: true,
255
- attributes: true,
256
- attributeFilter: [DOM_SELECTOR]
287
+ subtree: true
288
+ });
289
+
290
+ observeAttributes([DOM_SELECTOR], function (el) {
291
+ _constructor(el);
257
292
  });
258
293
  }, 'ln-ajax');
259
294
  }